├── CHANGELOG.md ├── assets ├── example2.js ├── data.json ├── examples.js └── style.css ├── docs ├── assets │ ├── example2.js │ ├── examples3.js │ ├── data.json │ ├── examples.js │ └── style.css └── index.html ├── bower.json ├── MIT-LICENSE.txt ├── README.md ├── jquery.mentionsInput.scss ├── jquery.mentionsInput.css ├── jquery.mentionsInput.less ├── lib ├── jquery.events.input.js └── jquery.elastic.js ├── index.html └── jquery.mentionsInput.js /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## ChangeLog 2 | 3 | ### 1.0.1 4 | * Removed elastic-option since it wasn't really working without it. https://github.com/podio/jquery-mentions-input/issues/1) 5 | Fixed issue with space on search queries. ( https://github.com/podio/jquery-mentions-input/issues/24) 6 | 7 | ### 1.0.0 8 | 9 | * Initial release 10 | -------------------------------------------------------------------------------- /assets/example2.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | $('textarea.mention-example2').mentionsInput({ 4 | onDataRequest:function (mode, query, callback) { 5 | $.getJSON('assets/data.json', function(responseData) { 6 | responseData = _.filter(responseData, function(item) { return item.name.toLowerCase().indexOf(query.toLowerCase()) > -1 }); 7 | callback.call(this, responseData); 8 | }); 9 | } 10 | 11 | }); 12 | 13 | }); -------------------------------------------------------------------------------- /docs/assets/example2.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | $('textarea.mention-example2').mentionsInput({ 4 | onDataRequest:function (mode, query, callback) { 5 | $.getJSON('assets/data.json', function(responseData) { 6 | responseData = _.filter(responseData, function(item) { return item.name.toLowerCase().indexOf(query.toLowerCase()) > -1 }); 7 | callback.call(this, responseData); 8 | }); 9 | } 10 | 11 | }); 12 | 13 | }); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-mentions-input", 3 | "version": "1.6.0", 4 | "homepage": "https://github.com/podio/jquery-mentions-input", 5 | "authors": [ 6 | "Kenneth Auchenberg ", 7 | "djhvscf " 8 | ], 9 | "description": "jquery.mentionsInput is a small, but awesome UI component that allows you to '@mention' someone in a text message, just like you are used to on Facebook or Twitter.", 10 | "main": [ 11 | "jquery.mentionsInput.js" 12 | ], 13 | "keywords": [ 14 | "plugin", 15 | "mention", 16 | "jquery" 17 | ], 18 | "dependencies": { 19 | "underscore": "~1.2", 20 | "jquery": "~1.6.x" 21 | }, 22 | "license": "MIT", 23 | "ignore": [ 24 | "**/.*", 25 | "node_modules", 26 | "bower_components", 27 | "test", 28 | "tests", 29 | "docs", 30 | "assets" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Podio, http://podio.com/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /assets/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "name": "Kenneth Auchenberg", 5 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 6 | "type": "contact" 7 | }, 8 | { 9 | "id": 2, 10 | "name": "Jon Froda", 11 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 12 | "type": "contact" 13 | }, 14 | { 15 | "id": 3, 16 | "name": "Anders Pollas", 17 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 18 | "type": "contact" 19 | }, 20 | { 21 | "id": 4, 22 | "name": "Kasper Hulthin", 23 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 24 | "type": "contact" 25 | }, 26 | { 27 | "id": 5, 28 | "name": "Andreas Haugstrup", 29 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 30 | "type": "contact" 31 | }, 32 | { 33 | "id": 6, 34 | "name": "Pete Lacey", 35 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 36 | "type": "contact" 37 | }, 38 | { 39 | "id": 7, 40 | "name": "kenneth@auchenberg.dk", 41 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 42 | "type": "contact" 43 | } 44 | ] -------------------------------------------------------------------------------- /docs/assets/examples3.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | $('input.mention').mentionsInput({ 4 | onDataRequest:function (mode, query, callback) { 5 | var data = [ 6 | { id:1, name:'Kenneth Auchenberg', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 7 | { id:2, name:'Jon Froda', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 8 | { id:3, name:'Anders Pollas', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 9 | { id:4, name:'Kasper Hulthin', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 10 | { id:5, name:'Andreas Haugstrup', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 11 | { id:6, name:'Pete Lacey', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 12 | { id:7, name:'kenneth@auchenberg.dk', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 13 | { id:8, name:'Pete Awesome Lacey', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 14 | { id:9, name:'Kenneth Hulthin', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' } 15 | ]; 16 | 17 | data = _.filter(data, function(item) { return item.name.toLowerCase().indexOf(query.toLowerCase()) > -1 }); 18 | 19 | callback.call(this, data); 20 | } 21 | }); 22 | }); -------------------------------------------------------------------------------- /docs/assets/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "name": "Kenneth Auchenberg", 5 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 6 | "type": "contact" 7 | }, 8 | { 9 | "id": 2, 10 | "name": "Jon Froda", 11 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 12 | "type": "contact" 13 | }, 14 | { 15 | "id": 3, 16 | "name": "Anders Pollas", 17 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 18 | "type": "contact" 19 | }, 20 | { 21 | "id": 4, 22 | "name": "Kasper Hulthin", 23 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 24 | "type": "contact" 25 | }, 26 | { 27 | "id": 5, 28 | "name": "Andreas Haugstrup", 29 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 30 | "type": "contact" 31 | }, 32 | { 33 | "id": 6, 34 | "name": "Pete Lacey", 35 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 36 | "type": "contact" 37 | }, 38 | { 39 | "id": 7, 40 | "name": "kenneth@auchenberg.dk", 41 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 42 | "type": "contact" 43 | }, 44 | { 45 | "id": 8, 46 | "name": "Dennis Hernandez", 47 | "avatar": "http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif", 48 | "type": "contact" 49 | } 50 | ] -------------------------------------------------------------------------------- /docs/assets/examples.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | $('textarea.mention').mentionsInput({ 4 | onDataRequest:function (mode, query, callback) { 5 | var data = [ 6 | { id:1, name:'Kenneth Auchenberg', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 7 | { id:2, name:'Jon Froda', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 8 | { id:3, name:'Anders Pollas', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 9 | { id:4, name:'Kasper Hulthin', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 10 | { id:5, name:'Andreas Haugstrup', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 11 | { id:6, name:'Pete Lacey', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 12 | { id:7, name:'kenneth@auchenberg.dk', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 13 | { id:8, name:'Pete Awesome Lacey', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 14 | { id:9, name:'Kenneth Hulthin', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' } 15 | ]; 16 | 17 | data = _.filter(data, function(item) { return item.name.toLowerCase().indexOf(query.toLowerCase()) > -1 }); 18 | 19 | callback.call(this, data); 20 | } 21 | }); 22 | 23 | $('.get-syntax-text').click(function() { 24 | $('textarea.mention').mentionsInput('val', function(text) { 25 | alert(text); 26 | }); 27 | }); 28 | 29 | $('.get-mentions').click(function() { 30 | $('textarea.mention').mentionsInput('getMentions', function(data) { 31 | alert(JSON.stringify(data)); 32 | }); 33 | }) ; 34 | 35 | }); -------------------------------------------------------------------------------- /assets/examples.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | $('textarea.mention').mentionsInput({ 4 | onDataRequest:function (mode, query, callback) { 5 | var data = [ 6 | { id:1, name:'Kenneth Auchenberg', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 7 | { id:2, name:'Jon Froda', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 8 | { id:3, name:'Anders Pollas', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 9 | { id:4, name:'Kasper Hulthin', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 10 | { id:5, name:'Andreas Haugstrup', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 11 | { id:6, name:'Pete Lacey', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 12 | { id:7, name:'kenneth@auchenberg.dk', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 13 | { id:8, name:'Pete Awesome Lacey', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }, 14 | { id:9, name:'Kenneth Hulthin', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' } 15 | ]; 16 | 17 | data = _.filter(data, function(item) { return item.name.toLowerCase().indexOf(query.toLowerCase()) > -1 }); 18 | 19 | callback.call(this, data); 20 | }, 21 | onCaret: true 22 | }); 23 | 24 | $('.get-syntax-text').click(function() { 25 | $('textarea.mention').mentionsInput('val', function(text) { 26 | alert(text); 27 | }); 28 | }); 29 | 30 | $('.get-mentions').click(function() { 31 | $('textarea.mention').mentionsInput('getMentions', function(data) { 32 | alert(JSON.stringify(data)); 33 | }); 34 | }) ; 35 | 36 | }); -------------------------------------------------------------------------------- /assets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #f6f6f6; 3 | color: #192535; 4 | font-family: 'pt sans', arial, helvetica, sans-serif; 5 | font-size: 16px; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | div.container { 11 | font-size: 16px; 12 | width: 720px; 13 | padding: 50px 0 50px 50px; 14 | } 15 | 16 | div.container > p, 17 | div.container > li, 18 | div.container > ol { 19 | margin: 16px 0 16px 0; 20 | width: 550px; 21 | } 22 | 23 | div.examples { 24 | width: 560px; 25 | } 26 | 27 | .examples .mentions-input-box { 28 | margin-bottom: 10px; 29 | font-size: 14px; 30 | } 31 | 32 | .examples .mentions-input-box textarea { 33 | font-family: 'pt sans', arial, helvetica, sans-serif; 34 | font-size: 14px; 35 | } 36 | 37 | 38 | a, a:visited { 39 | padding: 0 2px; 40 | text-decoration: none; 41 | background: #eee; 42 | color: #00639e; 43 | } 44 | 45 | a:active, a:hover { 46 | color: #000; 47 | background: #bacada; 48 | } 49 | 50 | h1, h2, h3, h4, h5, h6 { 51 | margin-top: 40px; 52 | font-weight: normal; 53 | } 54 | 55 | h1 { 56 | font-size: 56px; 57 | } 58 | 59 | h2 { 60 | font-size: 24px; 61 | } 62 | 63 | b.header { 64 | font-size: 18px; 65 | } 66 | 67 | span.alias { 68 | font-size: 14px; 69 | font-style: italic; 70 | margin-left: 20px; 71 | } 72 | 73 | table { 74 | margin: 16px 0; 75 | padding: 0; 76 | } 77 | 78 | tr, td { 79 | margin: 0; 80 | padding: 0; 81 | vertical-align: top; 82 | } 83 | 84 | td { 85 | padding: 9px 15px 9px 0; 86 | } 87 | 88 | td.definition { 89 | line-height: 18px; 90 | font-size: 14px; 91 | } 92 | 93 | code, pre, tt { 94 | font-family: Monaco, Consolas, "Lucida Console", monospace; 95 | font-size: 12px; 96 | line-height: 18px; 97 | color: #294555; 98 | } 99 | 100 | code { 101 | margin-left: 20px; 102 | } 103 | 104 | pre { 105 | font-size: 12px; 106 | padding: 2px 0 2px 12px; 107 | border-left: 6px solid #99aabb; 108 | margin: 0px 0 30px; 109 | } 110 | 111 | .button { 112 | display: inline-block; 113 | line-height: 24px; 114 | margin-bottom: 4px; 115 | font-weight: normal; 116 | text-shadow: none; 117 | padding: 1px 10px 1px 10px; 118 | white-space: nowrap; 119 | cursor: pointer; 120 | 121 | border-radius: 5px; 122 | 123 | background: #eaeaea; 124 | background: -moz-linear-gradient(top, hsl(0, 0%, 96%), #eaeaea); 125 | background: -webkit-gradient(linear, left top, left bottom, from(#f6f6f6), to(#eaeaea)); 126 | border: 1px solid #dcdcdc; 127 | color: #525252; 128 | } -------------------------------------------------------------------------------- /docs/assets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #f6f6f6; 3 | color: #192535; 4 | font-family: 'pt sans', arial, helvetica, sans-serif; 5 | font-size: 16px; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | div.container { 11 | font-size: 16px; 12 | width: 720px; 13 | padding: 50px 0 50px 50px; 14 | } 15 | 16 | div.container > p, 17 | div.container > li, 18 | div.container > ol { 19 | margin: 16px 0 16px 0; 20 | width: 550px; 21 | } 22 | 23 | div.examples { 24 | width: 560px; 25 | } 26 | 27 | .examples .mentions-input-box { 28 | margin-bottom: 10px; 29 | font-size: 14px; 30 | } 31 | 32 | .examples .mentions-input-box textarea { 33 | font-family: 'pt sans', arial, helvetica, sans-serif; 34 | font-size: 14px; 35 | } 36 | 37 | 38 | a, a:visited { 39 | padding: 0 2px; 40 | text-decoration: none; 41 | background: #eee; 42 | color: #00639e; 43 | } 44 | 45 | a:active, a:hover { 46 | color: #000; 47 | background: #bacada; 48 | } 49 | 50 | h1, h2, h3, h4, h5, h6 { 51 | margin-top: 40px; 52 | font-weight: normal; 53 | } 54 | 55 | h1 { 56 | font-size: 56px; 57 | } 58 | 59 | h2 { 60 | font-size: 24px; 61 | } 62 | 63 | b.header { 64 | font-size: 18px; 65 | } 66 | 67 | span.alias { 68 | font-size: 14px; 69 | font-style: italic; 70 | margin-left: 20px; 71 | } 72 | 73 | table { 74 | margin: 16px 0; 75 | padding: 0; 76 | } 77 | 78 | tr, td { 79 | margin: 0; 80 | padding: 0; 81 | vertical-align: top; 82 | } 83 | 84 | td { 85 | padding: 9px 15px 9px 0; 86 | } 87 | 88 | td.definition { 89 | line-height: 18px; 90 | font-size: 14px; 91 | } 92 | 93 | code, pre, tt { 94 | font-family: Monaco, Consolas, "Lucida Console", monospace; 95 | font-size: 12px; 96 | line-height: 18px; 97 | color: #294555; 98 | } 99 | 100 | code { 101 | margin-left: 20px; 102 | } 103 | 104 | pre { 105 | font-size: 12px; 106 | padding: 2px 0 2px 12px; 107 | border-left: 6px solid #99aabb; 108 | margin: 0px 0 30px; 109 | } 110 | 111 | .button { 112 | display: inline-block; 113 | line-height: 24px; 114 | margin-bottom: 4px; 115 | font-weight: normal; 116 | text-shadow: none; 117 | padding: 1px 10px 1px 10px; 118 | white-space: nowrap; 119 | cursor: pointer; 120 | 121 | border-radius: 5px; 122 | 123 | background: #eaeaea; 124 | background: -moz-linear-gradient(top, hsl(0, 0%, 96%), #eaeaea); 125 | background: -webkit-gradient(linear, left top, left bottom, from(#f6f6f6), to(#eaeaea)); 126 | border: 1px solid #dcdcdc; 127 | color: #525252; 128 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jquery.mentionsInput 2 | ================= 3 | jquery.mentionsInput is a small, but awesome UI component that allows you to "@mention" someone in a text message, just like you are used to on Facebook or Twitter. 4 | 5 | This project is written by [Kenneth Auchenberg](http://kenneth.io), and started as an internal project at [Podio](http://podio.com), but has then been open sourced to give it a life in the community. 6 | 7 | ## Introduction 8 | To get started -- checkout http://podio.github.com/jquery-mentions-input 9 | 10 | ## Latest release 11 | 12 | 1.6.0 (2015-Jan-7) -- https://github.com/podio/jquery-mentions-input/releases/tag/1.6.0 13 | 14 | ## Bugs and Enhancements (next version) 15 | 16 | - [ ] Fix #74 Mention on ordinary input field not textarea 17 | - [ ] Fix #26 Capital letter as trigger character 18 | - [ ] Fix #59 Unicode characters support 19 | - [X] Fix #104 When same text which is to be mentioned already in the content 20 | - [ ] Fix #100 New option for conserve triggerChar in output 21 | 22 | ## License 23 | 24 | MIT License - http://www.opensource.org/licenses/mit-license.php 25 | 26 | ## Dependencies 27 | 28 | jquery.mentionsInput is written as a jQuery extension, so it naturally requires jQuery (1.6+). In addition to jQuery, it also depends on underscore.js (1.2+), which is used to simplify stuff a bit. 29 | 30 | The component is also using the new HTML5 "input" event. This means older browsers like IE8 need a polyfill which emulates the event (it is bundled). 31 | 32 | The component itself is implemented as a small independent function, so it can easily be ported to frameworks other than jQuery. 33 | 34 | Furthermore all utility functions have been centralized in the utils-object, which can be replaced with references if you already got functions like htmlEncode, etc. 35 | 36 | To make the component grow and shrink to fit it’s content, you can include jquery.elastic.js 37 | 38 | ## Browser support 39 | 40 | jquery.mentionsInput has been tested in Firefox 6+, Chrome 15+, and Internet Explorer 8+. 41 | 42 | Please let us know if you see anything weird. And no, we will no make it work for older browsers. Period. 43 | 44 | ## Reporting issues 45 | 46 | Please provide jsFiddle when creating issues! 47 | 48 | It's really saves much time. 49 | 50 | Your feedback is very appreciated! 51 | 52 | ## Roadmap 53 | - Fix open issues. 54 | - Seperate mentionsInput from jQuery, and expose as AMD/CJS module. 55 | - Seperate autocompleter, so it's possible to use bootstrap, jquery, etc-autocompleters 56 | - Define better interface to call methods. 57 | - Add the option to have a hidden-input that contains the syntaxed-version, so it's easier to use out of the box. 58 | - Add unit tests! 59 | - Add mobile support 60 | -------------------------------------------------------------------------------- /jquery.mentionsInput.scss: -------------------------------------------------------------------------------- 1 | .mentions-input-box { 2 | position: relative; 3 | background: #fff; 4 | 5 | textarea { 6 | width: 100%; 7 | display: block; 8 | height: 18px; 9 | padding: 9px; 10 | border: 1px solid #dcdcdc; 11 | border-radius: 3px; 12 | overflow: hidden; 13 | background: transparent; 14 | position: relative; 15 | outline: 0; 16 | resize: none; 17 | 18 | -webkit-box-sizing: border-box; 19 | -moz-box-sizing: border-box; 20 | box-sizing: border-box; 21 | } 22 | 23 | .mentions { 24 | position: absolute; 25 | left: 1px; 26 | right: 0; 27 | top: 1px; 28 | bottom: 0; 29 | padding: 9px; 30 | color: #fff; 31 | overflow: hidden; 32 | 33 | white-space: pre-wrap; 34 | word-wrap: break-word; 35 | 36 | > div { 37 | color: #fff; 38 | white-space: pre-wrap; 39 | width: 100%; 40 | 41 | > strong { 42 | font-weight: normal; 43 | background: #d8dfea; 44 | 45 | > span { 46 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity = 0); 47 | } 48 | } 49 | } 50 | } 51 | 52 | .mentions-autocomplete-list { 53 | display: none; 54 | background: #fff; 55 | border: 1px solid #b2b2b2; 56 | position: absolute; 57 | left: 0; 58 | right: 0; 59 | z-index: 10000; 60 | margin-top: -2px; 61 | 62 | border-radius: 5px; 63 | border-top-right-radius: 0; 64 | border-top-left-radius: 0; 65 | 66 | -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 67 | -moz-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 68 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 69 | 70 | ul { 71 | margin: 0; 72 | padding: 0; 73 | 74 | li { 75 | background-color: #fff; 76 | padding: 0 5px; 77 | margin: 0; 78 | width: auto; 79 | border-bottom: 1px solid #eee; 80 | height: 26px; 81 | line-height: 26px; 82 | overflow: hidden; 83 | cursor: pointer; 84 | list-style: none; 85 | white-space: nowrap; 86 | 87 | &:last-child { 88 | border-radius: 5px; 89 | } 90 | 91 | &:hover, 92 | &.active { 93 | background-color: #f2f2f2; 94 | } 95 | 96 | > img, 97 | > div.icon { 98 | width: 16px; 99 | height: 16px; 100 | float: left; 101 | margin-top: 5px; 102 | margin-right: 5px; 103 | -moz-background-origin: 3px; 104 | 105 | border-radius: 3px; 106 | } 107 | 108 | em { 109 | font-weight: bold; 110 | font-style: none; 111 | } 112 | 113 | b { 114 | background: #ffff99; 115 | font-weight: normal; 116 | } 117 | 118 | } 119 | 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /jquery.mentionsInput.css: -------------------------------------------------------------------------------- 1 | 2 | .mentions-input-box { 3 | position: relative; 4 | background: #fff; 5 | } 6 | 7 | .mentions-input-box textarea { 8 | width: 100%; 9 | display: block; 10 | height: 18px; 11 | padding: 9px; 12 | border: 1px solid #dcdcdc; 13 | border-radius:3px; 14 | overflow: hidden; 15 | background: transparent; 16 | position: relative; 17 | outline: 0; 18 | resize: none; 19 | 20 | -webkit-box-sizing: border-box; 21 | -moz-box-sizing: border-box; 22 | box-sizing: border-box; 23 | } 24 | 25 | .mentions-input-box .mentions-autocomplete-list { 26 | display: none; 27 | background: #fff; 28 | border: 1px solid #b2b2b2; 29 | position: absolute; 30 | left: 0; 31 | right: 0; 32 | z-index: 10000; 33 | margin-top: -2px; 34 | 35 | border-radius:5px; 36 | border-top-right-radius:0; 37 | border-top-left-radius:0; 38 | 39 | -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 40 | -moz-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 41 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 42 | } 43 | 44 | .mentions-input-box .mentions-autocomplete-list ul { 45 | margin: 0; 46 | padding: 0; 47 | } 48 | 49 | .mentions-input-box .mentions-autocomplete-list li { 50 | background-color: #fff; 51 | padding: 0 5px; 52 | margin: 0; 53 | width: auto; 54 | border-bottom: 1px solid #eee; 55 | height: 26px; 56 | line-height: 26px; 57 | overflow: hidden; 58 | cursor: pointer; 59 | list-style: none; 60 | white-space: nowrap; 61 | } 62 | 63 | .mentions-input-box .mentions-autocomplete-list li:last-child { 64 | border-radius:5px; 65 | } 66 | 67 | .mentions-input-box .mentions-autocomplete-list li > img, 68 | .mentions-input-box .mentions-autocomplete-list li > div.icon { 69 | width: 16px; 70 | height: 16px; 71 | float: left; 72 | margin-top:5px; 73 | margin-right: 5px; 74 | -moz-background-origin:3px; 75 | 76 | border-radius:3px; 77 | } 78 | 79 | .mentions-input-box .mentions-autocomplete-list li em { 80 | font-weight: bold; 81 | font-style: none; 82 | } 83 | 84 | .mentions-input-box .mentions-autocomplete-list li:hover, 85 | .mentions-input-box .mentions-autocomplete-list li.active { 86 | background-color: #f2f2f2; 87 | } 88 | 89 | .mentions-input-box .mentions-autocomplete-list li b { 90 | background: #ffff99; 91 | font-weight: normal; 92 | } 93 | 94 | .mentions-input-box .mentions { 95 | position: absolute; 96 | left: 1px; 97 | right: 0; 98 | top: 1px; 99 | bottom: 0; 100 | padding: 9px; 101 | color: #fff; 102 | overflow: hidden; 103 | 104 | white-space: pre-wrap; 105 | word-wrap: break-word; 106 | } 107 | 108 | .mentions-input-box .mentions > div { 109 | color: #fff; 110 | white-space: pre-wrap; 111 | width: 100%; 112 | } 113 | 114 | .mentions-input-box .mentions > div > strong { 115 | font-weight:normal; 116 | background: #d8dfea; 117 | } 118 | 119 | .mentions-input-box .mentions > div > strong > span { 120 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=0); 121 | } 122 | -------------------------------------------------------------------------------- /jquery.mentionsInput.less: -------------------------------------------------------------------------------- 1 | //Variables 2 | @white: #fff; 3 | 4 | //Mixins 5 | .border-radius(@radius: 5px) { 6 | -webkit-border-radius: @radius; 7 | -moz-border-radius: @radius; 8 | border-radius: @radius; 9 | } 10 | 11 | .opacity(@opacity: 100) { 12 | filter: e(%("alpha(opacity=%d)", @opacity)); 13 | -moz-opacity: @opacity / 100; 14 | opacity: @opacity / 100; 15 | } 16 | 17 | //Styles 18 | .mentions-input-box { 19 | position: relative; 20 | background: @white; 21 | 22 | textarea{ 23 | width: 100%; 24 | display: block; 25 | height: 18px; 26 | padding: 9px; 27 | border: 1px solid #dcdcdc; 28 | border-radius:3px; 29 | overflow: hidden; 30 | background: transparent; 31 | position: relative; 32 | outline: 0; 33 | resize: none; 34 | 35 | -webkit-box-sizing: border-box; 36 | -moz-box-sizing: border-box; 37 | box-sizing: border-box; 38 | } 39 | 40 | .mentions{ 41 | position: absolute; 42 | left: 1px; 43 | right: 0; 44 | top: 1px; 45 | bottom: 0; 46 | padding: 9px; 47 | color: @white; 48 | overflow: hidden; 49 | 50 | white-space: pre-wrap; 51 | word-wrap: break-word; 52 | 53 | & > div{ 54 | color: @white; 55 | white-space: pre-wrap; 56 | width: 100%; 57 | 58 | & > strong{ 59 | font-weight:normal; 60 | background: #d8dfea; 61 | 62 | & > span{ 63 | .opacity(0); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | .mentions-input-box .mentions-autocomplete-list { 71 | display: none; 72 | background: @white; 73 | border: 1px solid #b2b2b2; 74 | position: absolute; 75 | left: 0; 76 | right: 0; 77 | z-index: 10000; 78 | margin-top: -2px; 79 | 80 | .border-radius(); 81 | border-top-right-radius:0; 82 | border-top-left-radius:0; 83 | 84 | -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 85 | -moz-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 86 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 87 | 88 | ul{ 89 | margin: 0; 90 | padding: 0; 91 | } 92 | 93 | li{ 94 | background-color: @white; 95 | padding: 0 5px; 96 | margin: 0; 97 | width: auto; 98 | border-bottom: 1px solid #eee; 99 | height: 26px; 100 | line-height: 26px; 101 | overflow: hidden; 102 | cursor: pointer; 103 | list-style: none; 104 | white-space: nowrap; 105 | 106 | &:last-child{ 107 | .border-radius(); 108 | } 109 | 110 | & > img, 111 | & > div.icon{ 112 | width: 16px; 113 | height: 16px; 114 | float: left; 115 | margin-top:5px; 116 | margin-right: 5px; 117 | -moz-background-origin:3px; 118 | 119 | .border-radius(3px); 120 | } 121 | 122 | em{ 123 | font-weight: bold; 124 | font-style: none; 125 | } 126 | 127 | &:hover, 128 | &.active{ 129 | background-color: #f2f2f2; 130 | } 131 | 132 | b{ 133 | background: #ffff99; 134 | font-weight: normal; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /lib/jquery.events.input.js: -------------------------------------------------------------------------------- 1 | /* 2 | jQuery `input` special event v1.0 3 | 4 | http://whattheheadsaid.com/projects/input-special-event 5 | 6 | (c) 2010-2011 Andy Earnshaw 7 | MIT license 8 | www.opensource.org/licenses/mit-license.php 9 | 10 | Modified by Kenneth Auchenberg 11 | * Disabled usage of onPropertyChange event in IE, since its a bit delayed, if you type really fast. 12 | */ 13 | 14 | (function($) { 15 | // Handler for propertychange events only 16 | function propHandler() { 17 | var $this = $(this); 18 | if (window.event.propertyName == "value" && !$this.data("triggering.inputEvent")) { 19 | $this.data("triggering.inputEvent", true).trigger("input"); 20 | window.setTimeout(function () { 21 | $this.data("triggering.inputEvent", false); 22 | }, 0); 23 | } 24 | } 25 | 26 | $.event.special.input = { 27 | setup: function(data, namespaces) { 28 | var timer, 29 | // Get a reference to the element 30 | elem = this, 31 | // Store the current state of the element 32 | state = elem.value, 33 | // Create a dummy element that we can use for testing event support 34 | tester = document.createElement(this.tagName), 35 | // Check for native oninput 36 | oninput = "oninput" in tester || checkEvent(tester), 37 | // Check for onpropertychange 38 | onprop = "onpropertychange" in tester, 39 | // Generate a random namespace for event bindings 40 | ns = "inputEventNS" + ~~(Math.random() * 10000000), 41 | // Last resort event names 42 | evts = ["focus", "blur", "paste", "cut", "keydown", "drop", ""].join("." + ns + " "); 43 | 44 | function checkState() { 45 | var $this = $(elem); 46 | if (elem.value != state && !$this.data("triggering.inputEvent")) { 47 | state = elem.value; 48 | 49 | $this.data("triggering.inputEvent", true).trigger("input"); 50 | window.setTimeout(function () { 51 | $this.data("triggering.inputEvent", false); 52 | }, 0); 53 | } 54 | } 55 | 56 | // Set up a function to handle the different events that may fire 57 | function handler(e) { 58 | // When focusing, set a timer that polls for changes to the value 59 | if (e.type == "focus") { 60 | checkState(); 61 | clearInterval(timer); 62 | timer = window.setInterval(checkState, 250); 63 | } else if (e.type == "blur") { 64 | // When blurring, cancel the aforeset timer 65 | window.clearInterval(timer); 66 | } else { 67 | // For all other events, queue a timer to check state ASAP 68 | window.setTimeout(checkState, 0); 69 | } 70 | } 71 | 72 | // Bind to native event if available 73 | if (oninput) { 74 | return false; 75 | // } else if (onprop) { 76 | // // Else fall back to propertychange if available 77 | // $(this).find("input, textarea").andSelf().filter("input, textarea").bind("propertychange." + ns, propHandler); 78 | } else { 79 | // Else clutch at straws! 80 | $(this).find("input, textarea").andSelf().filter("input, textarea").bind(evts, handler); 81 | } 82 | $(this).data("inputEventHandlerNS", ns); 83 | }, 84 | teardown: function () { 85 | var elem = $(this); 86 | elem.find("input, textarea").unbind(elem.data("inputEventHandlerNS")); 87 | elem.data("inputEventHandlerNS", ""); 88 | } 89 | }; 90 | 91 | // Setup our jQuery shorthand method 92 | $.fn.input = function (handler) { 93 | return handler ? this.bind("input", handler) : this.trigger("input"); 94 | }; 95 | 96 | /* 97 | The following function tests the element for oninput support in Firefox. Many thanks to 98 | http://blog.danielfriesen.name/2010/02/16/html5-browser-maze-oninput-support/ 99 | */ 100 | function checkEvent(el) { 101 | // First check, for if Firefox fixes its issue with el.oninput = function 102 | el.setAttribute("oninput", "return"); 103 | if (typeof el.oninput == "function") { 104 | return true; 105 | } 106 | // Second check, because Firefox doesn't map oninput attribute to oninput property 107 | try { 108 | 109 | // "* Note * : Disabled focus and dispatch of keypress event due to conflict with DOMready, which resulted in scrolling down to the bottom of the page, possibly because layout wasn't finished rendering. 110 | var e = document.createEvent("KeyboardEvent"), 111 | ok = false, 112 | tester = function(e) { 113 | ok = true; 114 | e.preventDefault(); 115 | e.stopPropagation(); 116 | }; 117 | 118 | // e.initKeyEvent("keypress", true, true, window, false, false, false, false, 0, "e".charCodeAt(0)); 119 | 120 | document.body.appendChild(el); 121 | el.addEventListener("input", tester, false); 122 | // el.focus(); 123 | // el.dispatchEvent(e); 124 | el.removeEventListener("input", tester, false); 125 | document.body.removeChild(el); 126 | return ok; 127 | 128 | } catch(error) { 129 | 130 | } 131 | } 132 | })(jQuery); -------------------------------------------------------------------------------- /lib/jquery.elastic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Elastic 3 | * @descripton Elastic is jQuery plugin that grow and shrink your textareas automatically 4 | * @version 1.6.11 5 | * @requires jQuery 1.2.6+ 6 | * 7 | * @author Jan Jarfalk 8 | * @author-email jan.jarfalk@unwrongest.com 9 | * @author-website http://www.unwrongest.com 10 | * 11 | * @licence MIT License - http://www.opensource.org/licenses/mit-license.php 12 | */ 13 | 14 | (function($){ 15 | jQuery.fn.extend({ 16 | elastic: function() { 17 | 18 | // We will create a div clone of the textarea 19 | // by copying these attributes from the textarea to the div. 20 | var mimics = [ 21 | 'paddingTop', 22 | 'paddingRight', 23 | 'paddingBottom', 24 | 'paddingLeft', 25 | 'fontSize', 26 | 'lineHeight', 27 | 'fontFamily', 28 | 'width', 29 | 'fontWeight', 30 | 'border-top-width', 31 | 'border-right-width', 32 | 'border-bottom-width', 33 | 'border-left-width', 34 | 'borderTopStyle', 35 | 'borderTopColor', 36 | 'borderRightStyle', 37 | 'borderRightColor', 38 | 'borderBottomStyle', 39 | 'borderBottomColor', 40 | 'borderLeftStyle', 41 | 'borderLeftColor' 42 | ]; 43 | 44 | return this.each( function() { 45 | 46 | // Elastic only works on textareas 47 | if ( this.type !== 'textarea' ) { 48 | return false; 49 | } 50 | 51 | var $textarea = jQuery(this), 52 | $twin = jQuery('
').css({ 53 | 'position' : 'absolute', 54 | 'display' : 'none', 55 | 'word-wrap' : 'break-word', 56 | 'white-space' :'pre-wrap' 57 | }), 58 | lineHeight = parseInt($textarea.css('line-height'),10) || parseInt($textarea.css('font-size'),'10'), 59 | minheight = parseInt($textarea.css('height'),10) || lineHeight*3, 60 | maxheight = parseInt($textarea.css('max-height'),10) || Number.MAX_VALUE, 61 | goalheight = 0; 62 | 63 | // Opera returns max-height of -1 if not set 64 | if (maxheight < 0) { maxheight = Number.MAX_VALUE; } 65 | 66 | // Append the twin to the DOM 67 | // We are going to meassure the height of this, not the textarea. 68 | $twin.appendTo($textarea.parent()); 69 | 70 | // Copy the essential styles (mimics) from the textarea to the twin 71 | var i = mimics.length; 72 | while(i--){ 73 | $twin.css(mimics[i].toString(),$textarea.css(mimics[i].toString())); 74 | } 75 | 76 | // Updates the width of the twin. (solution for textareas with widths in percent) 77 | function setTwinWidth(){ 78 | var curatedWidth = Math.floor(parseInt($textarea.width(),10)); 79 | if($twin.width() !== curatedWidth){ 80 | $twin.css({'width': curatedWidth + 'px'}); 81 | 82 | // Update height of textarea 83 | update(true); 84 | } 85 | } 86 | 87 | // Sets a given height and overflow state on the textarea 88 | function setHeightAndOverflow(height, overflow){ 89 | 90 | var curratedHeight = Math.floor(parseInt(height,10)); 91 | if($textarea.height() !== curratedHeight){ 92 | $textarea.css({'height': curratedHeight + 'px','overflow':overflow}); 93 | } 94 | } 95 | 96 | // This function will update the height of the textarea if necessary 97 | function update(forced) { 98 | 99 | // Get curated content from the textarea. 100 | var textareaContent = $textarea.val().replace(/&/g,'&').replace(/ {2}/g, ' ').replace(/<|>/g, '>').replace(/\n/g, '
'); 101 | 102 | // Compare curated content with curated twin. 103 | var twinContent = $twin.html().replace(/
/ig,'
'); 104 | 105 | if(forced || textareaContent+' ' !== twinContent){ 106 | 107 | // Add an extra white space so new rows are added when you are at the end of a row. 108 | $twin.html(textareaContent+' '); 109 | 110 | // Change textarea height if twin plus the height of one line differs more than 3 pixel from textarea height 111 | if(Math.abs($twin.height() + lineHeight - $textarea.height()) > 3){ 112 | 113 | var goalheight = $twin.height()+lineHeight; 114 | if(goalheight >= maxheight) { 115 | setHeightAndOverflow(maxheight,'auto'); 116 | } else if(goalheight <= minheight) { 117 | setHeightAndOverflow(minheight,'hidden'); 118 | } else { 119 | setHeightAndOverflow(goalheight,'hidden'); 120 | } 121 | 122 | } 123 | 124 | } 125 | 126 | } 127 | 128 | // Hide scrollbars 129 | $textarea.css({'overflow':'hidden'}); 130 | 131 | // Update textarea size on keyup, change, cut and paste 132 | $textarea.bind('keyup change cut paste', function(){ 133 | update(); 134 | }); 135 | 136 | // Update width of twin if browser or textarea is resized (solution for textareas with widths in percent) 137 | $(window).bind('resize', setTwinWidth); 138 | $textarea.bind('resize', setTwinWidth); 139 | $textarea.bind('update', update); 140 | 141 | // Compact textarea on blur 142 | $textarea.bind('blur',function(){ 143 | if($twin.height() < maxheight){ 144 | if($twin.height() > minheight) { 145 | $textarea.height($twin.height()); 146 | } else { 147 | $textarea.height(minheight); 148 | } 149 | } 150 | }); 151 | 152 | // And this line is to catch the browser paste event 153 | $textarea.bind('input paste',function(e){ setTimeout( update, 250); }); 154 | 155 | // Run update once when elastic is initialized 156 | update(); 157 | 158 | }); 159 | 160 | } 161 | }); 162 | })(jQuery); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | jquery.mentionsInput 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |

jquery.mentionsInput

19 | 20 |

21 | jquery.mentionsInput is a small, but awesome UI component that allows you to "@mention" someone in a text message, just like you are used to on Facebook or Twitter. 22 |

23 | 24 |

This project is written by Kenneth Auchenberg, and started as an internal project at Podio, but has then been open sourced to give it a life in the community.

25 | 26 |

27 | 28 |

Examples

29 | 30 |

Basic example (source)

31 | 32 | 37 | 38 | 39 |

AJAX example. (Getting data by AJAX, and filter in callback - source)

40 | 41 |
42 | 43 |
44 | 45 |

Download and source

46 |

You can grab the latest source from the repository on GitHub by clicking here: https://github.com/podio/jquery-mentions-input.

47 | 48 |

Getting started

49 | 50 |
    51 |
  1. 52 | Add a script reference to jquery.mentionsInput.js:
    53 |
    <script src='jquery.mentionsInput.js' type='text/javascript'></script>
    54 |
  2. 55 |
  3. 56 | Add a bit of markup:
    57 |
    <textarea class='mention'>
    58 |
  4. 59 |
  5. 60 |

    Initialise the mentionsInput:
    61 |

     62 | $('textarea.mention').mentionsInput({
     63 |   onDataRequest:function (mode, query, callback) {
     64 |     var data = [
     65 |       { id:1, name:'Kenneth Auchenberg', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' },
     66 |       { id:2, name:'Jon Froda', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' },
     67 |       { id:3, name:'Anders Pollas', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' },
     68 |       { id:4, name:'Kasper Hulthin', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' },
     69 |       { id:5, name:'Andreas Haugstrup', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' },
     70 |       { id:6, name:'Pete Lacey', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }
     71 |     ];
     72 | 
     73 |     data = _.filter(data, function(item) { return item.name.toLowerCase().indexOf(query.toLowerCase()) > -1 });
     74 | 
     75 |     callback.call(this, data);
     76 |   }
     77 | });
     78 |     
    79 |

    80 | 81 |

    82 | Bam, you are in business. 83 |

    84 |
  6. 85 |
86 | 87 |

Configuration

88 |

jquery.mentionsInput does have a number of extra configuration options which you may change to customise the way it behaves.

89 | 90 |

The meaning of the options and their default values are listed below.

91 | 92 | 93 | 94 | 95 | 96 | 100 | 101 | 102 | 103 | 104 | 108 | 109 | 110 | 111 | 112 | 115 | 116 | 117 | 118 | 119 | 122 | 123 | 124 | 125 | 126 | 129 | 130 | 131 | 132 | 133 | 136 | 137 |
onDataRequestfunction(mode, query, callback) 97 | This function is a callback function which is used to provide data for the autocomplete. When a search starts 98 | this function is called with following arguments: 'search', the query (what's been typed), and a callback function which needs to be called inside onDataRequest with a data collection to be searched on as a first argument. 99 |
triggerChar@ 105 | Trigger character which triggers the mentions search, when the character has been typed into the 106 | mentions input field. 107 |
minChars2 113 | The minimum amount of characters after the trigger character necessary to perform a search. 114 |
showAvatarstrue | false 120 | Toggles whether or not items within the autocomplete-dropdown will be rendered with an icon/avatar. 121 |
classesobject 127 | Object which contains classes used in the layout as key/value pairs. 128 |
templatesobject 134 | Object which contains templates used to render the layout as key/value pairs. 135 |
138 | 139 |

Methods

140 | 141 |

jquery.mentionsInput does expose a number of public methods, you can call on an instance.

142 | 143 | 144 | 145 | 146 | 147 | 150 | 151 | 152 | 153 | 154 | 157 | 158 | 159 | 160 | 161 | 164 | 165 | 166 | 167 | 168 | 171 | 172 |
init 148 | Initialises the mentionsInput component on a specific element. 149 |
reset 155 | Resets the component, clears all mentions. 156 |
val(callback) 162 | An async method which accepts a callback function and returns a value of the input field (including markup) as a first parameter of this function.

This is the value you want to send to your server. 163 |
getMentions(callback) 169 | An async method which accepts a callback function and returns a collection of mentions as hash objects as a first parameter. 170 |
173 | 174 |

Query data structure

175 | 176 |

When the component is preforming a "query" on the data specified through the onDataRequest-callback, it's expecting a specific data structure to be returned.

177 |
178 | {
179 |   'id'    : 1,
180 |   'name'  : 'Kenneth Auchenberg',
181 |   'avatar': 'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif',
182 |   'icon'  : 'icon-16 icon-person',
183 |   'type'  : 'contact'
184 | }
185 | 
186 | 187 |

"avatar" property is a URL used for image avatars when "showAvatars"-option is enabled

188 |

"icon" property is a className used for avatars when "showAvatars"-option is disabled

189 |

"type" property specifies an object type which is used in the marked-up version of the mentions result

190 | 191 | 192 |

Markup format

193 |

When mentions are being added to the input, a marked-up version of the value is generated, to allow the mentions to be extracted, parsed and stored later.

194 |
195 |   This is a message for @[name](type:id)
196 | 
197 |

Like:

198 |
199 |   This is a message for @[Kenneth Auchenberg](contact:1)
200 | 
201 | 202 | 203 |

Browser support

204 |

jquery.mentionsInput has been tested in Firefox 6+, Chrome 15+, and Internet Explorer 8+.

205 |

Please let us know if you see anything weird. And no, we will no make it work for older browsers. Period.

206 | 207 |

Dependencies

208 |

jquery.mentionsInput is written as a jQuery extension, so it naturally requires jQuery (1.6+). In addition to jQuery, it also depends on underscore.js (1.2+), which is used to simplify stuff a bit.

209 | 210 |

The component is also using the new HTML5 "input" event. This means older browsers like IE8 need a polyfill which emulates the event (it is bundled).

211 | 212 |

The component itself is implemented as a small independent function, so it can easily be ported to frameworks other than jQuery.

213 | 214 |

Furthermore all utility functions have been centralized in the utils-object, which can be replaced with references if you already got functions like htmlEncode, etc.

215 | 216 |

To make the component grow and shrink to fit it’s content, you can include jquery.elastic.js

217 | 218 |

License

219 |

MIT License - http://www.opensource.org/licenses/mit-license.php

220 | 221 |

Change Log

222 | 223 |

224 | 1.0.1
225 |

229 |

230 | 231 |

232 | 1.0.0
233 |

    234 |
  • Initial release.
  • 235 |
236 |

237 | 238 |
239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | jquery.mentionsInput 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |

jquery.mentionsInput

19 | 20 |

21 | jquery.mentionsInput is a small, but awesome UI component that allows you to "@mention" someone in a text message, just like you are used to on Facebook or Twitter. 22 |

23 | 24 |

This project is written by Kenneth Auchenberg, and started as an internal project at Podio, but has then been open sourced to give it a life in the community.

25 | 26 |

27 | 28 |

Examples

29 | 30 |

Basic example (source)

31 | 32 | 37 | 38 |

AJAX example. (Getting data by AJAX, and filter in callback - source)

39 | 40 |
41 | 42 |
43 | 44 |

Download and source

45 |

You can grab the latest source from the repository on GitHub by clicking here: https://github.com/podio/jquery-mentions-input.

46 | 47 |

Getting started

48 | 49 |
    50 |
  1. 51 | Add a script reference to jquery.mentionsInput.js:
    52 |
    <script src='jquery.mentionsInput.js' type='text/javascript'></script>
    53 |
  2. 54 |
  3. 55 | Add a bit of markup:
    56 |
    <textarea class='mention'>
    57 |
  4. 58 |
  5. 59 |

    Initialise the mentionsInput:
    60 |

     61 | $('textarea.mention').mentionsInput({
     62 |   onDataRequest:function (mode, query, callback) {
     63 |     var data = [
     64 |       { id:1, name:'Kenneth Auchenberg', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' },
     65 |       { id:2, name:'Jon Froda', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' },
     66 |       { id:3, name:'Anders Pollas', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' },
     67 |       { id:4, name:'Kasper Hulthin', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' },
     68 |       { id:5, name:'Andreas Haugstrup', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' },
     69 |       { id:6, name:'Pete Lacey', 'avatar':'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif', 'type':'contact' }
     70 |     ];
     71 | 
     72 |     data = _.filter(data, function(item) { return item.name.toLowerCase().indexOf(query.toLowerCase()) > -1 });
     73 | 
     74 |     callback.call(this, data);
     75 |   }
     76 | });
     77 |     
    78 |

    79 | 80 |

    81 | Bam, you are in business. 82 |

    83 |
  6. 84 |
85 | 86 |

Configuration

87 |

jquery.mentionsInput does have a number of extra configuration options which you may change to customise the way it behaves.

88 | 89 |

The meaning of the options and their default values are listed below.

90 | 91 | 92 | 93 | 94 | 95 | 99 | 100 | 101 | 102 | 103 | 107 | 108 | 109 | 110 | 111 | 114 | 115 | 116 | 117 | 118 | 121 | 122 | 123 | 124 | 125 | 128 | 129 | 130 | 131 | 132 | 135 | 136 |
onDataRequestfunction(mode, query, callback) 96 | This function is a callback function which is used to provide data for the autocomplete. When a search starts 97 | this function is called with following arguments: 'search', the query (what's been typed), and a callback function which needs to be called inside onDataRequest with a data collection to be searched on as a first argument. 98 |
triggerChar@ 104 | Trigger character which triggers the mentions search, when the character has been typed into the 105 | mentions input field. 106 |
minChars2 112 | The minimum amount of characters after the trigger character necessary to perform a search. 113 |
showAvatarstrue | false 119 | Toggles whether or not items within the autocomplete-dropdown will be rendered with an icon/avatar. 120 |
classesobject 126 | Object which contains classes used in the layout as key/value pairs. 127 |
templatesobject 133 | Object which contains templates used to render the layout as key/value pairs. 134 |
137 | 138 |

Methods

139 | 140 |

jquery.mentionsInput does expose a number of public methods, you can call on an instance.

141 | 142 | 143 | 144 | 145 | 146 | 149 | 150 | 151 | 152 | 153 | 156 | 157 | 158 | 159 | 160 | 163 | 164 | 165 | 166 | 167 | 170 | 171 |
init 147 | Initialises the mentionsInput component on a specific element. 148 |
reset 154 | Resets the component, clears all mentions. 155 |
val(callback) 161 | An async method which accepts a callback function and returns a value of the input field (including markup) as a first parameter of this function.

This is the value you want to send to your server. 162 |
getMentions(callback) 168 | An async method which accepts a callback function and returns a collection of mentions as hash objects as a first parameter. 169 |
172 | 173 |

Query data structure

174 | 175 |

When the component is preforming a "query" on the data specified through the onDataRequest-callback, it's expecting a specific data structure to be returned.

176 |
177 | {
178 |   'id'    : 1,
179 |   'name'  : 'Kenneth Auchenberg',
180 |   'avatar': 'http://cdn0.4dots.com/i/customavatars/avatar7112_1.gif',
181 |   'icon'  : 'icon-16 icon-person',
182 |   'type'  : 'contact'
183 | }
184 | 
185 | 186 |

"avatar" property is a URL used for image avatars when "showAvatars"-option is enabled

187 |

"icon" property is a className used for avatars when "showAvatars"-option is disabled

188 |

"type" property specifies an object type which is used in the marked-up version of the mentions result

189 | 190 | 191 |

Markup format

192 |

When mentions are being added to the input, a marked-up version of the value is generated, to allow the mentions to be extracted, parsed and stored later.

193 |
194 |   This is a message for @[name](type:id)
195 | 
196 |

Like:

197 |
198 |   This is a message for @[Kenneth Auchenberg](contact:1)
199 | 
200 | 201 | 202 |

Browser support

203 |

jquery.mentionsInput has been tested in Firefox 6+, Chrome 15+, and Internet Explorer 8+.

204 |

Please let us know if you see anything weird. And no, we will no make it work for older browsers. Period.

205 | 206 |

Dependencies

207 |

jquery.mentionsInput is written as a jQuery extension, so it naturally requires jQuery (1.6+). In addition to jQuery, it also depends on underscore.js (1.2+), which is used to simplify stuff a bit.

208 | 209 |

The component is also using the new HTML5 "input" event. This means older browsers like IE8 need a polyfill which emulates the event (it is bundled).

210 | 211 |

The component itself is implemented as a small independent function, so it can easily be ported to frameworks other than jQuery.

212 | 213 |

Furthermore all utility functions have been centralized in the utils-object, which can be replaced with references if you already got functions like htmlEncode, etc.

214 | 215 |

To make the component grow and shrink to fit it’s content, you can include jquery.elastic.js

216 | 217 |

License

218 |

MIT License - http://www.opensource.org/licenses/mit-license.php

219 | 220 |

Change Log

221 | 222 |

223 | 1.0.1
224 |

228 |

229 | 230 |

231 | 1.0.0
232 |

    233 |
  • Initial release.
  • 234 |
235 |

236 | 237 |
238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 261 | 262 | 263 | 264 | -------------------------------------------------------------------------------- /jquery.mentionsInput.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Mentions Input 3 | * Version 1.0.2 4 | * Written by: Kenneth Auchenberg (Podio) 5 | * 6 | * Using underscore.js 7 | * 8 | * License: MIT License - http://www.opensource.org/licenses/mit-license.php 9 | */ 10 | 11 | (function ($, _, undefined) { 12 | 13 | // Settings 14 | var KEY = { BACKSPACE : 8, TAB : 9, RETURN : 13, ESC : 27, LEFT : 37, UP : 38, RIGHT : 39, DOWN : 40, COMMA : 188, SPACE : 32, HOME : 36, END : 35 }; // Keys "enum" 15 | 16 | //Default settings 17 | var defaultSettings = { 18 | triggerChar : '@', //Char that respond to event 19 | onDataRequest : $.noop, //Function where we can search the data 20 | minChars : 2, //Minimum chars to fire the event 21 | allowRepeat : false, //Allow repeat mentions 22 | showAvatars : true, //Show the avatars 23 | elastic : true, //Grow the textarea automatically 24 | defaultValue : '', 25 | onCaret : false, 26 | classes : { 27 | autoCompleteItemActive : "active" //Classes to apply in each item 28 | }, 29 | templates : { 30 | wrapper : _.template('
'), 31 | autocompleteList : _.template('
'), 32 | autocompleteListItem : _.template('
  • <%= content %>
  • '), 33 | autocompleteListItemAvatar : _.template(''), 34 | autocompleteListItemIcon : _.template('
    '), 35 | mentionsOverlay : _.template('
    '), 36 | mentionItemSyntax : _.template('@[<%= value %>](<%= type %>:<%= id %>)'), 37 | mentionItemHighlight : _.template('<%= value %>') 38 | } 39 | }; 40 | 41 | //Class util 42 | var utils = { 43 | //Encodes the character with _.escape function (undersocre) 44 | htmlEncode : function (str) { 45 | return _.escape(str); 46 | }, 47 | //Encodes the character to be used with RegExp 48 | regexpEncode : function (str) { 49 | return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); 50 | }, 51 | highlightTerm : function (value, term) { 52 | if (!term && !term.length) { 53 | return value; 54 | } 55 | return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); 56 | }, 57 | //Sets the caret in a valid position 58 | setCaratPosition : function (domNode, caretPos) { 59 | if (domNode.createTextRange) { 60 | var range = domNode.createTextRange(); 61 | range.move('character', caretPos); 62 | range.select(); 63 | } else { 64 | if (domNode.selectionStart) { 65 | domNode.focus(); 66 | domNode.setSelectionRange(caretPos, caretPos); 67 | } else { 68 | domNode.focus(); 69 | } 70 | } 71 | }, 72 | //Deletes the white spaces 73 | rtrim: function(string) { 74 | return string.replace(/\s+$/,""); 75 | } 76 | }; 77 | 78 | //Main class of MentionsInput plugin 79 | var MentionsInput = function (settings) { 80 | 81 | var domInput, 82 | elmInputBox, 83 | elmInputWrapper, 84 | elmAutocompleteList, 85 | elmWrapperBox, 86 | elmMentionsOverlay, 87 | elmActiveAutoCompleteItem, 88 | mentionsCollection = [], 89 | autocompleteItemCollection = {}, 90 | inputBuffer = [], 91 | currentDataQuery = ''; 92 | 93 | //Mix the default setting with the users settings 94 | settings = $.extend(true, {}, defaultSettings, settings ); 95 | 96 | //Initializes the text area target 97 | function initTextarea() { 98 | elmInputBox = $(domInput); //Get the text area target 99 | 100 | //If the text area is already configured, return 101 | if (elmInputBox.attr('data-mentions-input') === 'true') { 102 | return; 103 | } 104 | 105 | elmInputWrapper = elmInputBox.parent(); //Get the DOM element parent 106 | elmWrapperBox = $(settings.templates.wrapper()); 107 | elmInputBox.wrapAll(elmWrapperBox); //Wrap all the text area into the div elmWrapperBox 108 | elmWrapperBox = elmInputWrapper.find('> div.mentions-input-box'); //Obtains the div elmWrapperBox that now contains the text area 109 | 110 | elmInputBox.attr('data-mentions-input', 'true'); //Sets the attribute data-mentions-input to true -> Defines if the text area is already configured 111 | elmInputBox.bind('keydown', onInputBoxKeyDown); //Bind the keydown event to the text area 112 | elmInputBox.bind('keypress', onInputBoxKeyPress); //Bind the keypress event to the text area 113 | elmInputBox.bind('click', onInputBoxClick); //Bind the click event to the text area 114 | elmInputBox.bind('blur', onInputBoxBlur); //Bind the blur event to the text area 115 | 116 | if (navigator.userAgent.indexOf("MSIE 8") > -1) { 117 | elmInputBox.bind('propertychange', onInputBoxInput); //IE8 won't fire the input event, so let's bind to the propertychange 118 | } else { 119 | elmInputBox.bind('input', onInputBoxInput); //Bind the input event to the text area 120 | } 121 | 122 | // Elastic textareas, grow automatically 123 | if( settings.elastic ) { 124 | elmInputBox.elastic(); 125 | } 126 | } 127 | 128 | //Initializes the autocomplete list, append to elmWrapperBox and delegate the mousedown event to li elements 129 | function initAutocomplete() { 130 | elmAutocompleteList = $(settings.templates.autocompleteList()); //Get the HTML code for the list 131 | elmAutocompleteList.appendTo(elmWrapperBox); //Append to elmWrapperBox element 132 | elmAutocompleteList.delegate('li', 'mousedown', onAutoCompleteItemClick); //Delegate the event 133 | } 134 | 135 | //Initializes the mentions' overlay 136 | function initMentionsOverlay() { 137 | elmMentionsOverlay = $(settings.templates.mentionsOverlay()); //Get the HTML code of the mentions' overlay 138 | elmMentionsOverlay.prependTo(elmWrapperBox); //Insert into elmWrapperBox the mentions overlay 139 | } 140 | 141 | //Updates the values of the main variables 142 | function updateValues() { 143 | var syntaxMessage = getInputBoxValue(); //Get the actual value of the text area 144 | 145 | _.each(mentionsCollection, function (mention) { 146 | var textSyntax = settings.templates.mentionItemSyntax(mention); 147 | syntaxMessage = syntaxMessage.replace(new RegExp(utils.regexpEncode(mention.value), 'g'), textSyntax); 148 | }); 149 | 150 | var mentionText = utils.htmlEncode(syntaxMessage); //Encode the syntaxMessage 151 | 152 | _.each(mentionsCollection, function (mention) { 153 | var formattedMention = _.extend({}, mention, {value: utils.htmlEncode(mention.value)}); 154 | var textSyntax = settings.templates.mentionItemSyntax(formattedMention); 155 | var textHighlight = settings.templates.mentionItemHighlight(formattedMention); 156 | 157 | mentionText = mentionText.replace(new RegExp(utils.regexpEncode(textSyntax), 'g'), textHighlight); 158 | }); 159 | 160 | mentionText = mentionText.replace(/\n/g, '
    '); //Replace the escape character for
    161 | mentionText = mentionText.replace(/ {2}/g, '  '); //Replace the 2 preceding token to   162 | 163 | elmInputBox.data('messageText', syntaxMessage); //Save the messageText to elmInputBox 164 | elmInputBox.trigger('updated'); 165 | elmMentionsOverlay.find('div').html(mentionText); //Insert into a div of the elmMentionsOverlay the mention text 166 | } 167 | 168 | //Cleans the buffer 169 | function resetBuffer() { 170 | inputBuffer = []; 171 | } 172 | 173 | //Updates the mentions collection 174 | function updateMentionsCollection() { 175 | var inputText = getInputBoxValue(); //Get the actual value of text area 176 | 177 | //Returns the values that doesn't match the condition 178 | mentionsCollection = _.reject(mentionsCollection, function (mention, index) { 179 | return !mention.value || inputText.indexOf(mention.value) == -1; 180 | }); 181 | mentionsCollection = _.compact(mentionsCollection); //Delete all the falsy values of the array and return the new array 182 | } 183 | 184 | //Adds mention to mentions collections 185 | function addMention(mention) { 186 | 187 | var currentMessage = getInputBoxValue(), 188 | caretStart = elmInputBox[0].selectionStart, 189 | shortestDistance = false, 190 | bestLastIndex = false; 191 | 192 | // Using a regex to figure out positions 193 | var regex = new RegExp("\\" + settings.triggerChar + currentDataQuery, "gi"), 194 | regexMatch; 195 | 196 | while(regexMatch = regex.exec(currentMessage)) { 197 | if (shortestDistance === false || Math.abs(regex.lastIndex - caretStart) < shortestDistance) { 198 | shortestDistance = Math.abs(regex.lastIndex - caretStart); 199 | bestLastIndex = regex.lastIndex; 200 | } 201 | } 202 | 203 | var startCaretPosition = bestLastIndex - currentDataQuery.length - 1; //Set the start caret position (right before the @) 204 | var currentCaretPosition = bestLastIndex; //Set the current caret position (right after the end of the "mention") 205 | 206 | 207 | var start = currentMessage.substr(0, startCaretPosition); 208 | var end = currentMessage.substr(currentCaretPosition, currentMessage.length); 209 | var startEndIndex = (start + mention.value).length + 1; 210 | 211 | // See if there's the same mention in the list 212 | if( !_.find(mentionsCollection, function (object) { return object.id == mention.id; }) ) { 213 | mentionsCollection.push(mention);//Add the mention to mentionsColletions 214 | } 215 | 216 | // Cleaning before inserting the value, otherwise auto-complete would be triggered with "old" inputbuffer 217 | resetBuffer(); 218 | currentDataQuery = ''; 219 | hideAutoComplete(); 220 | 221 | // Mentions and syntax message 222 | var updatedMessageText = start + mention.value + ' ' + end; 223 | elmInputBox.val(updatedMessageText); //Set the value to the txt area 224 | elmInputBox.trigger('mention'); 225 | updateValues(); 226 | 227 | // Set correct focus and selection 228 | elmInputBox.focus(); 229 | utils.setCaratPosition(elmInputBox[0], startEndIndex); 230 | } 231 | 232 | //Gets the actual value of the text area without white spaces from the beginning and end of the value 233 | function getInputBoxValue() { 234 | return $.trim(elmInputBox.val()); 235 | } 236 | 237 | // This is taken straight from live (as of Sep 2012) GitHub code. The 238 | // technique is known around the web. Just google it. Github's is quite 239 | // succint though. NOTE: relies on selectionEnd, which as far as IE is concerned, 240 | // it'll only work on 9+. Good news is nothing will happen if the browser 241 | // doesn't support it. 242 | function textareaSelectionPosition($el) { 243 | var a, b, c, d, e, f, g, h, i, j, k; 244 | if (!(i = $el[0])) return; 245 | if (!$(i).is("textarea")) return; 246 | if (i.selectionEnd == null) return; 247 | g = { 248 | position: "absolute", 249 | overflow: "auto", 250 | whiteSpace: "pre-wrap", 251 | wordWrap: "break-word", 252 | boxSizing: "content-box", 253 | top: 0, 254 | left: -9999 255 | }, h = ["boxSizing", "fontFamily", "fontSize", "fontStyle", "fontVariant", "fontWeight", "height", "letterSpacing", "lineHeight", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textDecoration", "textIndent", "textTransform", "width", "word-spacing"]; 256 | for (j = 0, k = h.length; j < k; j++) e = h[j], g[e] = $(i).css(e); 257 | return c = document.createElement("div"), $(c).css(g), $(i).after(c), b = document.createTextNode(i.value.substring(0, i.selectionEnd)), a = document.createTextNode(i.value.substring(i.selectionEnd)), d = document.createElement("span"), d.innerHTML = " ", c.appendChild(b), c.appendChild(d), c.appendChild(a), c.scrollTop = i.scrollTop, f = $(d).position(), $(c).remove(), f 258 | } 259 | 260 | //same as above function but return offset instead of position 261 | function textareaSelectionOffset($el) { 262 | var a, b, c, d, e, f, g, h, i, j, k; 263 | if (!(i = $el[0])) return; 264 | if (!$(i).is("textarea")) return; 265 | if (i.selectionEnd == null) return; 266 | g = { 267 | position: "absolute", 268 | overflow: "auto", 269 | whiteSpace: "pre-wrap", 270 | wordWrap: "break-word", 271 | boxSizing: "content-box", 272 | top: 0, 273 | left: -9999 274 | }, h = ["boxSizing", "fontFamily", "fontSize", "fontStyle", "fontVariant", "fontWeight", "height", "letterSpacing", "lineHeight", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textDecoration", "textIndent", "textTransform", "width", "word-spacing"]; 275 | for (j = 0, k = h.length; j < k; j++) e = h[j], g[e] = $(i).css(e); 276 | return c = document.createElement("div"), $(c).css(g), $(i).after(c), b = document.createTextNode(i.value.substring(0, i.selectionEnd)), a = document.createTextNode(i.value.substring(i.selectionEnd)), d = document.createElement("span"), d.innerHTML = " ", c.appendChild(b), c.appendChild(d), c.appendChild(a), c.scrollTop = i.scrollTop, f = $(d).offset(), $(c).remove(), f 277 | } 278 | 279 | //Scrolls back to the input after autocomplete if the window has scrolled past the input 280 | function scrollToInput() { 281 | var elmDistanceFromTop = $(elmInputBox).offset().top; //input offset 282 | var bodyDistanceFromTop = $('body').offset().top; //body offset 283 | var distanceScrolled = $(window).scrollTop(); //distance scrolled 284 | 285 | if (distanceScrolled > elmDistanceFromTop) { 286 | //subtracts body distance to handle fixed headers 287 | $(window).scrollTop(elmDistanceFromTop - bodyDistanceFromTop); 288 | } 289 | } 290 | 291 | //Takes the click event when the user select a item of the dropdown 292 | function onAutoCompleteItemClick(e) { 293 | var elmTarget = $(this); //Get the item selected 294 | var mention = autocompleteItemCollection[elmTarget.attr('data-uid')]; //Obtains the mention 295 | 296 | addMention(mention); 297 | scrollToInput(); 298 | return false; 299 | } 300 | 301 | //Takes the click event on text area 302 | function onInputBoxClick(e) { 303 | resetBuffer(); 304 | } 305 | 306 | //Takes the blur event on text area 307 | function onInputBoxBlur(e) { 308 | hideAutoComplete(); 309 | } 310 | 311 | //Takes the input event when users write or delete something 312 | function onInputBoxInput(e) { 313 | updateValues(); 314 | updateMentionsCollection(); 315 | 316 | var triggerCharIndex = _.lastIndexOf(inputBuffer, settings.triggerChar); //Returns the last match of the triggerChar in the inputBuffer 317 | if (triggerCharIndex > -1) { //If the triggerChar is present in the inputBuffer array 318 | currentDataQuery = inputBuffer.slice(triggerCharIndex + 1).join(''); //Gets the currentDataQuery 319 | currentDataQuery = utils.rtrim(currentDataQuery); //Deletes the whitespaces 320 | _.defer(_.bind(doSearch, this, currentDataQuery)); //Invoking the function doSearch ( Bind the function to this) 321 | } 322 | } 323 | 324 | //Takes the keypress event 325 | function onInputBoxKeyPress(e) { 326 | if(e.keyCode !== KEY.BACKSPACE) { //If the key pressed is not the backspace 327 | var typedValue = String.fromCharCode(e.which || e.keyCode); //Takes the string that represent this CharCode 328 | inputBuffer.push(typedValue); //Push the value pressed into inputBuffer 329 | } 330 | } 331 | 332 | //Takes the keydown event 333 | function onInputBoxKeyDown(e) { 334 | 335 | // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT 336 | if (e.keyCode === KEY.LEFT || e.keyCode === KEY.RIGHT || e.keyCode === KEY.HOME || e.keyCode === KEY.END) { 337 | // Defer execution to ensure carat pos has changed after HOME/END keys then call the resetBuffer function 338 | _.defer(resetBuffer); 339 | 340 | // IE9 doesn't fire the oninput event when backspace or delete is pressed. This causes the highlighting 341 | // to stay on the screen whenever backspace is pressed after a highlighed word. This is simply a hack 342 | // to force updateValues() to fire when backspace/delete is pressed in IE9. 343 | if (navigator.userAgent.indexOf("MSIE 9") > -1) { 344 | _.defer(updateValues); //Call the updateValues function 345 | } 346 | 347 | return; 348 | } 349 | 350 | //If the key pressed was the backspace 351 | if (e.keyCode === KEY.BACKSPACE) { 352 | inputBuffer = inputBuffer.slice(0, -1 + inputBuffer.length); // Can't use splice, not available in IE 353 | return; 354 | } 355 | 356 | //If the elmAutocompleteList is hidden 357 | if (!elmAutocompleteList.is(':visible')) { 358 | return true; 359 | } 360 | 361 | switch (e.keyCode) { 362 | case KEY.UP: //If the key pressed was UP or DOWN 363 | case KEY.DOWN: 364 | var elmCurrentAutoCompleteItem = null; 365 | if (e.keyCode === KEY.DOWN) { //If the key pressed was DOWN 366 | if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) { //If elmActiveAutoCompleteItem exits 367 | elmCurrentAutoCompleteItem = elmActiveAutoCompleteItem.next(); //Gets the next li element in the list 368 | } else { 369 | elmCurrentAutoCompleteItem = elmAutocompleteList.find('li').first(); //Gets the first li element found 370 | } 371 | } else { 372 | elmCurrentAutoCompleteItem = $(elmActiveAutoCompleteItem).prev(); //The key pressed was UP and gets the previous li element 373 | } 374 | if (elmCurrentAutoCompleteItem.length) { 375 | selectAutoCompleteItem(elmCurrentAutoCompleteItem); 376 | } 377 | return false; 378 | case KEY.RETURN: //If the key pressed was RETURN or TAB 379 | case KEY.TAB: 380 | if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) { //If the elmActiveAutoCompleteItem exists 381 | elmActiveAutoCompleteItem.trigger('mousedown'); //Calls the mousedown event 382 | return false; 383 | } 384 | break; 385 | } 386 | 387 | return true; 388 | } 389 | 390 | //Hides the autoomplete 391 | function hideAutoComplete() { 392 | elmActiveAutoCompleteItem = null; 393 | elmAutocompleteList.empty().hide(); 394 | } 395 | 396 | //Selects the item in the autocomplete list 397 | function selectAutoCompleteItem(elmItem) { 398 | elmItem.addClass(settings.classes.autoCompleteItemActive); //Add the class active to item 399 | elmItem.siblings().removeClass(settings.classes.autoCompleteItemActive); //Gets all li elements in autocomplete list and remove the class active 400 | 401 | elmActiveAutoCompleteItem = elmItem; //Sets the item to elmActiveAutoCompleteItem 402 | } 403 | 404 | //Populates dropdown 405 | function populateDropdown(query, results) { 406 | elmAutocompleteList.show(); //Shows the autocomplete list 407 | 408 | if(!settings.allowRepeat) { 409 | // Filter items that has already been mentioned 410 | var mentionValues = _.pluck(mentionsCollection, 'value'); 411 | results = _.reject(results, function (item) { 412 | return _.include(mentionValues, item.name); 413 | }); 414 | } 415 | 416 | if (!results.length) { //If there are not elements hide the autocomplete list 417 | hideAutoComplete(); 418 | return; 419 | } 420 | 421 | elmAutocompleteList.empty(); //Remove all li elements in autocomplete list 422 | var elmDropDownList = $("
      ").appendTo(elmAutocompleteList).hide(); //Inserts a ul element to autocomplete div and hide it 423 | 424 | _.each(results, function (item, index) { 425 | var itemUid = _.uniqueId('mention_'); //Gets the item with unique id 426 | 427 | autocompleteItemCollection[itemUid] = _.extend({}, item, {value: item.name}); //Inserts the new item to autocompleteItemCollection 428 | 429 | var elmListItem = $(settings.templates.autocompleteListItem({ 430 | 'id' : utils.htmlEncode(item.id), 431 | 'display' : utils.htmlEncode(item.name), 432 | 'type' : utils.htmlEncode(item.type), 433 | 'content' : utils.highlightTerm(utils.htmlEncode((item.display ? item.display : item.name)), query) 434 | })).attr('data-uid', itemUid); //Inserts the new item to list 435 | 436 | //If the index is 0 437 | if (index === 0) { 438 | selectAutoCompleteItem(elmListItem); 439 | } 440 | 441 | //If show avatars is true 442 | if (settings.showAvatars) { 443 | var elmIcon; 444 | 445 | //If the item has an avatar 446 | if (item.avatar) { 447 | elmIcon = $(settings.templates.autocompleteListItemAvatar({ avatar : item.avatar })); 448 | } else { //If not then we set an default icon 449 | elmIcon = $(settings.templates.autocompleteListItemIcon({ icon : item.icon })); 450 | } 451 | elmIcon.prependTo(elmListItem); //Inserts the elmIcon to elmListItem 452 | } 453 | elmListItem = elmListItem.appendTo(elmDropDownList); //Insets the elmListItem to elmDropDownList 454 | }); 455 | 456 | elmAutocompleteList.show(); //Shows the elmAutocompleteList div 457 | if (settings.onCaret) { 458 | positionAutocomplete(elmAutocompleteList, elmInputBox); 459 | } 460 | elmDropDownList.show(); //Shows the elmDropDownList 461 | } 462 | 463 | //Search into data list passed as parameter 464 | function doSearch(query) { 465 | //If the query is not null, undefined, empty and has the minimum chars 466 | if (query && query.length && query.length >= settings.minChars) { 467 | //Call the onDataRequest function and then call the populateDropDrown 468 | settings.onDataRequest.call(this, 'search', query, function (responseData) { 469 | populateDropdown(query, responseData); 470 | }); 471 | } else { //If the query is null, undefined, empty or has not the minimun chars 472 | hideAutoComplete(); //Hide the autocompletelist 473 | } 474 | } 475 | 476 | function positionAutocomplete(elmAutocompleteList, elmInputBox) { 477 | var elmAutocompleteListPosition = elmAutocompleteList.css('position'); 478 | if (elmAutocompleteListPosition == 'absolute') { 479 | var position = textareaSelectionPosition(elmInputBox), 480 | lineHeight = parseInt(elmInputBox.css('line-height'), 10) || 18; 481 | elmAutocompleteList.css('width', '15em'); // Sort of a guess 482 | elmAutocompleteList.css('left', position.left); 483 | elmAutocompleteList.css('top', lineHeight + position.top); 484 | 485 | //check if the right position of auto complete is larger than the right position of the input 486 | //if yes, reset the left of auto complete list to make it fit the input 487 | var elmInputBoxRight = elmInputBox.offset().left + elmInputBox.width(), 488 | elmAutocompleteListRight = elmAutocompleteList.offset().left + elmAutocompleteList.width(); 489 | if (elmInputBoxRight <= elmAutocompleteListRight) { 490 | elmAutocompleteList.css('left', Math.abs(elmAutocompleteList.position().left - (elmAutocompleteListRight - elmInputBoxRight))); 491 | } 492 | } 493 | else if (elmAutocompleteListPosition == 'fixed') { 494 | var offset = textareaSelectionOffset(elmInputBox), 495 | lineHeight = parseInt(elmInputBox.css('line-height'), 10) || 18; 496 | elmAutocompleteList.css('width', '15em'); // Sort of a guess 497 | elmAutocompleteList.css('left', offset.left + 10000); 498 | elmAutocompleteList.css('top', lineHeight + offset.top); 499 | } 500 | } 501 | 502 | //Resets the text area 503 | function resetInput(currentVal) { 504 | mentionsCollection = []; 505 | var mentionText = utils.htmlEncode(currentVal); 506 | var regex = new RegExp("(" + settings.triggerChar + ")\\[(.*?)\\]\\((.*?):(.*?)\\)", "gi"); 507 | var match, newMentionText = mentionText; 508 | while ((match = regex.exec(mentionText)) != null) { 509 | newMentionText = newMentionText.replace(match[0], match[1] + match[2]); 510 | mentionsCollection.push({ 'id': match[4], 'type': match[3], 'value': match[2], 'trigger': match[1] }); 511 | } 512 | elmInputBox.val(newMentionText); 513 | updateValues(); 514 | } 515 | // Public methods 516 | return { 517 | //Initializes the mentionsInput component on a specific element. 518 | init : function (domTarget) { 519 | 520 | domInput = domTarget; 521 | 522 | initTextarea(); 523 | initAutocomplete(); 524 | initMentionsOverlay(); 525 | resetInput(settings.defaultValue); 526 | 527 | //If the autocomplete list has prefill mentions 528 | if( settings.prefillMention ) { 529 | addMention( settings.prefillMention ); 530 | } 531 | }, 532 | 533 | //An async method which accepts a callback function and returns a value of the input field (including markup) as a first parameter of this function. This is the value you want to send to your server. 534 | val : function (callback) { 535 | if (!_.isFunction(callback)) { 536 | return; 537 | } 538 | callback.call(this, mentionsCollection.length ? elmInputBox.data('messageText') : getInputBoxValue()); 539 | }, 540 | 541 | //Resets the text area value and clears all mentions 542 | reset : function () { 543 | resetInput(); 544 | }, 545 | 546 | //Reinit with the text area value if it was changed programmatically 547 | reinit : function () { 548 | resetInput(false); 549 | }, 550 | 551 | //An async method which accepts a callback function and returns a collection of mentions as hash objects as a first parameter. 552 | getMentions : function (callback) { 553 | if (!_.isFunction(callback)) { 554 | return; 555 | } 556 | callback.call(this, mentionsCollection); 557 | } 558 | }; 559 | }; 560 | 561 | //Main function to include into jQuery and initialize the plugin 562 | $.fn.mentionsInput = function (method, settings) { 563 | 564 | var outerArguments = arguments; //Gets the arguments 565 | //If method is not a function 566 | if (typeof method === 'object' || !method) { 567 | settings = method; 568 | } 569 | 570 | return this.each(function () { 571 | var instance = $.data(this, 'mentionsInput') || $.data(this, 'mentionsInput', new MentionsInput(settings)); 572 | 573 | if (_.isFunction(instance[method])) { 574 | return instance[method].apply(this, Array.prototype.slice.call(outerArguments, 1)); 575 | } else if (typeof method === 'object' || !method) { 576 | return instance.init.call(this, this); 577 | } else { 578 | $.error('Method ' + method + ' does not exist'); 579 | } 580 | }); 581 | }; 582 | 583 | })(jQuery, _); 584 | --------------------------------------------------------------------------------