├── Readme.md ├── MIT-LICENSE.txt ├── jquery-textntags.css └── jquery-textntags.js /Readme.md: -------------------------------------------------------------------------------- 1 | jquery.textntags 2 | ================= 3 | 4 | To get started -- checkout http://daniel-zahariev.github.com/jquery-textntags 5 | 6 | 7 | ***** 8 | 9 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/daniel-zahariev/jquery-textntags/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 10 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Daniel Zahariev 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 | 22 | Copyright (c) 2011 Podio, http://podio.com/ 23 | 24 | Permission is hereby granted, free of charge, to any person obtaining 25 | a copy of this software and associated documentation files (the 26 | "Software"), to deal in the Software without restriction, including 27 | without limitation the rights to use, copy, modify, merge, publish, 28 | distribute, sublicense, and/or sell copies of the Software, and to 29 | permit persons to whom the Software is furnished to do so, subject to 30 | the following conditions: 31 | 32 | The above copyright notice and this permission notice shall be 33 | included in all copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 36 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 37 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 38 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 39 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 40 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 41 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 42 | -------------------------------------------------------------------------------- /jquery-textntags.css: -------------------------------------------------------------------------------- 1 | 2 | .textntags-wrapper { 3 | position: relative; 4 | background: #fff; 5 | } 6 | 7 | .textntags-wrapper textarea { 8 | position: absolute; 9 | left: 0; 10 | right: 0; 11 | top: 0; 12 | bottom: 0; 13 | width: 100%; 14 | display: block; 15 | height: 18px; 16 | padding: 9px; 17 | margin: 0; 18 | border: 1px solid #dcdcdc; 19 | border-radius:3px; 20 | overflow: hidden; 21 | background: transparent; 22 | outline: 0; 23 | resize: none; 24 | font-family: Arial; 25 | font-size: 13px; 26 | line-height: 17px; 27 | 28 | -webkit-box-sizing: border-box; 29 | -moz-box-sizing: border-box; 30 | box-sizing: border-box; 31 | } 32 | @-moz-document url-prefix() { 33 | .textntags-wrapper textarea{ 34 | padding: 9px 8px; 35 | } 36 | } 37 | 38 | .textntags-wrapper .textntags-tag-list { 39 | display: none; 40 | background: #fff; 41 | border: 1px solid #b2b2b2; 42 | position: absolute; 43 | left: 0; 44 | right: 0; 45 | z-index: 10000; 46 | margin-top: -2px; 47 | 48 | border-radius:5px; 49 | border-top-right-radius:0; 50 | border-top-left-radius:0; 51 | 52 | -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 53 | -moz-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 54 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 55 | } 56 | 57 | .textntags-wrapper .textntags-tag-list ul { 58 | margin: 0; 59 | padding: 0; 60 | } 61 | 62 | .textntags-wrapper .textntags-tag-list li { 63 | background-color: #fff; 64 | padding: 0 5px; 65 | margin: 0; 66 | width: auto; 67 | border-bottom: 1px solid #eee; 68 | height: 26px; 69 | line-height: 26px; 70 | overflow: hidden; 71 | cursor: pointer; 72 | list-style: none; 73 | white-space: nowrap; 74 | } 75 | 76 | .textntags-wrapper .textntags-tag-list li:last-child { 77 | border-radius:5px; 78 | } 79 | 80 | .textntags-wrapper .textntags-tag-list li > img, 81 | .textntags-wrapper .textntags-tag-list li > div.icon { 82 | width: 16px; 83 | height: 16px; 84 | float: left; 85 | margin-top:5px; 86 | margin-right: 5px; 87 | -moz-background-origin:3px; 88 | 89 | border-radius:3px; 90 | } 91 | 92 | .textntags-wrapper .textntags-tag-list li em { 93 | font-weight: bold; 94 | font-style: none; 95 | } 96 | 97 | .textntags-wrapper .textntags-tag-list li:hover, 98 | .textntags-wrapper .textntags-tag-list li.active { 99 | background-color: #f2f2f2; 100 | } 101 | 102 | .textntags-wrapper .textntags-tag-list li b { 103 | background: #ffff99; 104 | font-weight: normal; 105 | } 106 | 107 | .textntags-wrapper .textntags-beautifier { 108 | position: relative; 109 | padding: 10px; 110 | color: #fff; 111 | 112 | white-space: pre-wrap; 113 | word-wrap: break-word; 114 | } 115 | 116 | .textntags-wrapper .textntags-beautifier > div { 117 | color: #fff; 118 | white-space: pre-wrap; 119 | width: 100%; 120 | font-family: Arial; 121 | font-size: 13px; 122 | line-height: 17px; 123 | min-height: 17px; 124 | } 125 | 126 | .textntags-wrapper .textntags-beautifier > div > strong { 127 | font-weight:normal; 128 | background: #d8dfea; 129 | line-height: 16px; 130 | } 131 | 132 | .textntags-wrapper .textntags-beautifier > div > strong > span { 133 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=0); 134 | } 135 | -------------------------------------------------------------------------------- /jquery-textntags.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Text'N'Tags (textntags) 3 | * Version 0.1.2 4 | * Written by: Daniel Zahariev 5 | * 6 | * Dependencies: jQuery, underscore.js 7 | * 8 | * License: MIT License - http://www.opensource.org/licenses/mit-license.php 9 | */ 10 | (function ($, _, undefined) { 11 | 12 | // Keys "enum" 13 | var KEY = { V: 86, Z: 90, BACKSPACE : 8, TAB : 9, RETURN : 13, ESC : 27, LEFT : 37, UP : 38, RIGHT : 39, DOWN : 40, COMMA : 188, SPACE : 32, HOME : 36, END : 35, 'DELETE': 46 }; 14 | var defaultSettings = { 15 | onDataRequest : $.noop, 16 | realValOnSubmit : true, 17 | triggers : {'@' : {}}, 18 | templates : { 19 | wrapper : _.template('
'), 20 | beautifier : _.template('
'), 21 | tagHighlight : _.template('$<%= idx %>'), 22 | tagList : _.template('
'), 23 | tagsListItem : _.template('
  • <%= title %>
  • '), 24 | tagsListItemImage : _.template(''), 25 | tagsListItemIcon : _.template('
    ') 26 | } 27 | }; 28 | var trigger_defaults = { 29 | minChars : 2, 30 | uniqueTags : true, 31 | showImageOrIcon : true, 32 | keys_map : {id: 'id', title: 'name', description: '', img: 'avatar', no_img_class: 'icon', type: 'type'}, 33 | syntax : _.template('@[[<%= id %>:<%= type %>:<%= title %>]]'), 34 | parser : /(@)\[\[(\d+):([\w\s\.\-]+):([\w\s@\.,-\/#!$%\^&\*;:{}=\-_`~()]+)\]\]/gi, 35 | parserGroups : {id: 2, type: 3, title: 4}, 36 | classes : { 37 | tagsDropDown : '', 38 | tagActiveDropDown : 'active', 39 | tagHighlight : '' 40 | } 41 | }; 42 | 43 | function transformObjectPropertiesFn(keys_map) { 44 | return function (obj, localToPublic) { 45 | var new_obj = {}; 46 | if (localToPublic) { 47 | _.each(keys_map, function (v, k) { new_obj[v] = obj[k]; }); 48 | } else { 49 | _.each(keys_map, function (v, k) { new_obj[k] = obj[v]; }); 50 | } 51 | return new_obj; 52 | }; 53 | } 54 | var transformObjectProperties = _.memoize(transformObjectPropertiesFn); 55 | 56 | var utils = { 57 | htmlEncode: function (str) { 58 | return _.escape(str); 59 | }, 60 | highlightTerm: function (value, term) { 61 | if (!term && !term.length) { 62 | return value; 63 | } 64 | return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); 65 | }, 66 | setCaratPosition: function (domNode, caretPos) { 67 | if (domNode.createTextRange) { 68 | var range = domNode.createTextRange(); 69 | range.move('character', caretPos); 70 | range.select(); 71 | } else { 72 | if (domNode.selectionStart) { 73 | domNode.focus(); 74 | domNode.setSelectionRange(caretPos, caretPos); 75 | } else { 76 | domNode.focus(); 77 | } 78 | } 79 | } 80 | }; 81 | 82 | var TextNTags = function (editor) { 83 | var settings = null, templates; 84 | var elContainer, elEditor, elBeautifier, elTagList, elTagListItemActive; 85 | var tagsCollection; 86 | var currentTriggerChar, currentDataQuery; 87 | var editorSelectionLength = 0, editorTextLength = 0, editorKeyCode = 0, editorAddingTag = false; 88 | var editorInPasteMode = false, editorPasteStartPosition = 0, editorPasteCutCharacters = 0; 89 | var REGEX_ESCAPE_CHARS = ['[', '^', '$', '.', '|', '?', '*', '+', '(', ')', '\\']; 90 | 91 | function setSettings (options) { 92 | if (settings != null) { 93 | return false; 94 | } 95 | 96 | settings = $.extend(true, {}, defaultSettings, options); 97 | delete settings.triggers['']; 98 | _.each(settings.triggers, function (val, key) { 99 | settings.triggers[key] = $.extend(true, {}, trigger_defaults, val); 100 | if (_.find(REGEX_ESCAPE_CHARS, function(character){ return character === key })) { 101 | var regex_key = "\\" + key; 102 | } else { 103 | var regex_key = key; 104 | } 105 | settings.triggers[key].finder = new RegExp(regex_key + '\\w+(\\s+\\w+)?\\s?$', 'gi'); 106 | }); 107 | 108 | templates = settings.templates; 109 | 110 | return true; 111 | } 112 | 113 | function initTextarea () { 114 | elEditor = $(editor).bind({ 115 | click: onEditorClick, 116 | keydown: onEditorKeyDown, 117 | keypress: onEditorKeyPress, 118 | keyup: onEditorKeyUp, 119 | input: onEditorInput, 120 | blur: onEditorBlur 121 | }); 122 | 123 | elContainer = elEditor.wrapAll($(templates.wrapper())).parent(); 124 | 125 | if (settings.realValOnSubmit) { 126 | elEditor.closest('form').bind('submit.textntags', function (event) { 127 | elContainer.css('visibility', 'hidden'); 128 | elEditor.val(getTaggedText()); 129 | }); 130 | } 131 | } 132 | 133 | function initTagList () { 134 | elTagList = $(templates.tagList()); 135 | elTagList.appendTo(elContainer); 136 | elTagList.delegate('li', 'click', onTagListItemClick); 137 | } 138 | 139 | function initBeautifier () { 140 | elBeautifier = $(templates.beautifier()); 141 | elBeautifier.prependTo(elContainer); 142 | } 143 | 144 | function initState () { 145 | var text_with_tags = getEditorValue(), initialState = parseTaggedText(text_with_tags); 146 | tagsCollection = initialState.tagsCollection; 147 | elEditor.val(initialState.plain_text); 148 | updateBeautifier(); 149 | 150 | if (tagsCollection.length > 0) { 151 | var addedTags = _.uniq(_.map(tagsCollection, function (tagPos) { return tagPos[3]; })); 152 | elEditor.trigger('tagsAdded.textntags', [addedTags]); 153 | } 154 | } 155 | 156 | function getEditorValue () { 157 | return elEditor.val(); 158 | } 159 | 160 | function getBeautifiedText (tagged_text) { 161 | var beautified_text = tagged_text || getTaggedText(); 162 | beautified_text = beautified_text.replace(/&/g,'&').replace(//g,'>'); 163 | _.each(settings.triggers, function (trigger) { 164 | var markup = templates.tagHighlight({idx: trigger.parserGroups.title, class_name: trigger.classes.tagHighlight}); 165 | beautified_text = beautified_text.replace(trigger.parser, markup); 166 | }); 167 | 168 | beautified_text = beautified_text.replace(/\n/g, '
    ­'); 169 | beautified_text = beautified_text.replace(/ {2}/g, '  ') + '­'; 170 | return beautified_text; 171 | } 172 | 173 | function getTaggedText() { 174 | var plain_text = getEditorValue(), 175 | position = 0, tagged_text, triggers = settings.triggers; 176 | 177 | tagged_text = _.map(tagsCollection, function (tagPos) { 178 | var diff_pos = tagPos[0] - position, 179 | diff_text = diff_pos > 0 ? plain_text.substr(position, diff_pos) : '', 180 | objPropTransformer = transformObjectProperties(triggers[tagPos[2]].keys_map), 181 | tagText = triggers[tagPos[2]].syntax(objPropTransformer(tagPos[3], false)); 182 | 183 | position = tagPos[0] + tagPos[1]; 184 | return diff_text + tagText; 185 | }); 186 | 187 | return tagged_text.join('') + plain_text.substr(position); 188 | } 189 | 190 | // it's ready for export 191 | function parseTaggedText (tagged_text) { 192 | if (_.isString(tagged_text) == false) { 193 | return null; 194 | } 195 | var plain_text = '' + tagged_text, tagsColl = [], triggers = settings.triggers; 196 | 197 | _.each(triggers, function (opts, tchar) { 198 | var parts = tagged_text.split(opts.parser), 199 | idx = 0, pos = 0, len = parts.length, 200 | found_tag, found_len, part_len, 201 | max_group = _.max(opts.parserGroups); 202 | 203 | while (idx < len) { 204 | if (parts[idx] == tchar) { 205 | found_tag = {}; 206 | _.each(opts.parserGroups, function (v, k) { 207 | found_tag[opts.keys_map[k]] = parts[idx + v - 1]; 208 | if (k == 'title') { 209 | found_len = parts[idx + v - 1].length; 210 | } 211 | }); 212 | tagsColl.push([pos, found_len, tchar, found_tag]); 213 | part_len = found_len; 214 | idx += max_group; 215 | } else { 216 | part_len = parts[idx].length; 217 | idx += 1; 218 | } 219 | pos += part_len; 220 | } 221 | }); 222 | 223 | tagsColl = _.sortBy(tagsColl, function (tagPos) { return tagPos[0]; }); 224 | 225 | _.each(triggers, function (opts, tchar) { 226 | plain_text = plain_text.replace(opts.parser, '$' + opts.parserGroups.title); 227 | }); 228 | 229 | return { 230 | plain_text: plain_text, 231 | tagged_text: tagged_text, 232 | tagsCollection: tagsColl 233 | }; 234 | } 235 | 236 | function updateBeautifier () { 237 | elBeautifier.find('div').html(getBeautifiedText()); 238 | elEditor.css('height', elBeautifier.outerHeight() + 'px'); 239 | } 240 | 241 | function checkForTrigger(look_ahead) { 242 | look_ahead = look_ahead || 0; 243 | 244 | var selectionStartFix = $.browser.webkit ? 0 : -1, 245 | sStart = elEditor[0].selectionStart + selectionStartFix, 246 | left_text = elEditor.val().substr(0, sStart + look_ahead), 247 | found_trigger, found_trigger_char = null, query; 248 | 249 | if (!left_text || !left_text.length) { 250 | return; 251 | } 252 | 253 | found_trigger = _.find(settings.triggers, function (trigger, tchar) { 254 | var matches = left_text.match(trigger.finder); 255 | if (matches) { 256 | found_trigger_char = tchar; 257 | query = matches[0].substr(tchar.length); 258 | return true; 259 | } 260 | return false; 261 | }); 262 | 263 | if (!found_trigger_char || (found_trigger &&(query.length < found_trigger.minChars))) { 264 | hideTagList(); 265 | } else { 266 | currentDataQuery = query; 267 | currentTriggerChar = found_trigger_char; 268 | _.defer(_.bind(searchTags, this, currentDataQuery, found_trigger_char)); 269 | } 270 | } 271 | 272 | function onEditorClick (e) { 273 | checkForTrigger(0); 274 | } 275 | 276 | function onEditorKeyDown (e) { 277 | var keys = KEY, // store in local var for faster lookup 278 | sStart = elEditor[0].selectionStart, 279 | sEnd = elEditor[0].selectionEnd, 280 | plain_text = elEditor.val(); 281 | 282 | editorSelectionLength = sEnd - sStart; 283 | editorTextLength = plain_text.length; 284 | editorKeyCode = e.keyCode; 285 | 286 | switch (e.keyCode) { 287 | case keys.UP: 288 | case keys.DOWN: 289 | if (!elTagList.is(':visible')) { 290 | return true; 291 | } 292 | 293 | var elCurrentTagListItem = null; 294 | if (e.keyCode == keys.DOWN) { 295 | if (elTagListItemActive && elTagListItemActive.length) { 296 | elCurrentTagListItem = elTagListItemActive.next(); 297 | } else { 298 | elCurrentTagListItem = elTagList.find('li').first(); 299 | } 300 | } else { 301 | if (elTagListItemActive && elTagListItemActive.length) { 302 | elCurrentTagListItem = elTagListItemActive.prev(); 303 | } else { 304 | elCurrentTagListItem = elTagList.find('li').last(); 305 | } 306 | } 307 | 308 | selectTagListItem(elCurrentTagListItem, settings.triggers[currentTriggerChar].classes.tagActiveDropDown); 309 | return false; 310 | 311 | case keys.RETURN: 312 | case keys.TAB: 313 | if (elTagListItemActive && elTagListItemActive.length) { 314 | editorAddingTag = true; 315 | elTagListItemActive.click(); 316 | return false; 317 | } 318 | return true; 319 | 320 | case keys.BACKSPACE: 321 | case keys['DELETE']: 322 | if (e.keyCode == keys.BACKSPACE && sStart == sEnd && sStart > 0) { 323 | sStart -= 1; 324 | } 325 | if(sEnd > sStart) { 326 | removeTagsInRange(sStart, sEnd); 327 | shiftTagsPosition(sStart, sStart - sEnd); 328 | } 329 | return true; 330 | 331 | case keys.LEFT: 332 | case keys.RIGHT: 333 | case keys.HOME: 334 | case keys.END: 335 | _.defer(function () { checkForTrigger.call(this, 0); }); 336 | break; 337 | case keys.V: 338 | // checking for paste 339 | if (e.ctrlKey) { 340 | editorInPasteMode = true; 341 | editorPasteStartPosition = sStart; 342 | editorPasteCutCharacters = sEnd - sStart; 343 | removeTagsInRange(sStart, sEnd); 344 | } 345 | break; 346 | case keys.Z: 347 | if (e.ctrlKey) { 348 | // forbid undo 349 | return false; 350 | } 351 | break; 352 | } 353 | 354 | return true; 355 | } 356 | 357 | function onEditorKeyPress (e) { 358 | if (e.keyCode == KEY.RETURN) { 359 | updateBeautifier(elEditor.val()); 360 | } 361 | if (editorAddingTag) { 362 | if (e.keyCode == KEY.RETURN || e.keyCode == KEY.TAB) { 363 | e.preventDefault(); 364 | } 365 | editorAddingTag = false; 366 | } 367 | } 368 | 369 | function onEditorKeyUp (e) { 370 | if (editorInPasteMode) { 371 | editorInPasteMode = false; 372 | 373 | if (editorSelectionLength > 0) { 374 | return; 375 | } 376 | 377 | var sStart = elEditor[0].selectionStart, 378 | sEnd = elEditor[0].selectionEnd; 379 | 380 | shiftTagsPosition(editorPasteStartPosition, sEnd - editorPasteStartPosition - editorPasteCutCharacters); 381 | updateBeautifier(); 382 | } 383 | } 384 | 385 | function onEditorInput (e) { 386 | var selectionStartFix = $.browser.webkit ? 0 : -1; 387 | if (editorKeyCode != KEY.BACKSPACE && editorKeyCode != KEY['DELETE']) { 388 | if (editorSelectionLength > 0) { 389 | // delete of selection occured 390 | var sStart = elEditor[0].selectionStart + selectionStartFix, 391 | selectionLength = editorSelectionLength, 392 | sEnd = sStart + selectionLength, 393 | tags_shift_positions = elEditor.val().length - editorTextLength; 394 | removeTagsInRange(sStart, sEnd); 395 | shiftTagsPosition(sEnd, tags_shift_positions); 396 | } else if (!editorInPasteMode) { 397 | // char input - shift with 1 398 | var sStart = elEditor[0].selectionStart + selectionStartFix, 399 | sEnd = elEditor[0].selectionEnd + selectionStartFix, 400 | selectionLength = sEnd - sStart; 401 | 402 | if (editorKeyCode == KEY.RETURN) { 403 | shiftTagsPosition(sStart - 1, 1); 404 | removeTagsInRange(sStart, sStart); 405 | } else { 406 | shiftTagsPosition(sStart, 1); 407 | removeTagsInRange(sStart, sStart + 1); 408 | } 409 | } 410 | } 411 | 412 | updateBeautifier(); 413 | 414 | checkForTrigger(1); 415 | } 416 | 417 | function onEditorBlur (e) { 418 | _.delay(hideTagList, 100); 419 | } 420 | 421 | function hideTagList () { 422 | elTagListItemActive = null; 423 | elTagList.hide().empty(); 424 | } 425 | 426 | function onTagListItemClick (e) { 427 | addTag($(this).data('tag')); 428 | return false; 429 | } 430 | 431 | function removeTagsInRange (start, end) { 432 | var removedTags = []; 433 | tagsCollection = _.filter(tagsCollection, function (tagPos) { 434 | var s = tagPos[0], e = s + tagPos[1], 435 | inRange = ((s >= start && s < end) || (e > start && e <= end) || (s < start && e > end)); 436 | if (inRange) { 437 | removedTags.push(tagPos[3]); 438 | } 439 | return !inRange; 440 | }); 441 | 442 | if (removedTags.length > 0) { 443 | elEditor.trigger('tagsRemoved.textntags', [removedTags]); 444 | } 445 | } 446 | 447 | function shiftTagsPosition (afterPosition, position_shift) { 448 | tagsCollection = _.map(tagsCollection, function (tagPos) { 449 | if (tagPos[0] >= afterPosition) { 450 | tagPos[0] += position_shift; 451 | } 452 | return tagPos; 453 | }); 454 | } 455 | 456 | function addTag (tag) { 457 | var trigger = settings.triggers[currentTriggerChar], 458 | objPropTransformer = transformObjectProperties(trigger.keys_map), 459 | localTag = objPropTransformer(tag, false), 460 | plain_text = getEditorValue(), 461 | sStart = elEditor[0].selectionStart, 462 | tagStart = sStart - currentTriggerChar.length - currentDataQuery.length, 463 | newCaretPosition = tagStart + localTag.title.length, 464 | left_text = plain_text.substr(0, tagStart), 465 | right_text = plain_text.substr(sStart), 466 | new_text = left_text + localTag.title + right_text; 467 | 468 | // shift the tags after the current new one 469 | shiftTagsPosition(sStart, newCaretPosition - sStart); 470 | 471 | // explicitly convert to string for comparisons later 472 | tag[trigger.keys_map.id] = '' + tag[trigger.keys_map.id]; 473 | 474 | tagsCollection.push([tagStart, localTag.title.length, currentTriggerChar, tag]); 475 | tagsCollection = _.sortBy(tagsCollection, function (t) { return t[0]; }); 476 | 477 | currentTriggerChar = ''; 478 | currentDataQuery = ''; 479 | hideTagList(); 480 | 481 | elEditor.val(new_text); 482 | updateBeautifier(); 483 | 484 | elEditor.focus(); 485 | utils.setCaratPosition(elEditor[0], newCaretPosition); 486 | 487 | elEditor.trigger('tagsAdded.textntags', [[tag]]); 488 | } 489 | 490 | function selectTagListItem (tagItem, class_name) { 491 | if (tagItem && tagItem.length) { 492 | tagItem.addClass(class_name); 493 | tagItem.siblings().removeClass(class_name); 494 | elTagListItemActive = tagItem; 495 | } else { 496 | elTagListItemActive.removeClass(class_name); 497 | elTagListItemActive = null; 498 | } 499 | } 500 | 501 | function populateTagList (query, triggerChar, results) { 502 | var trigger = settings.triggers[triggerChar]; 503 | 504 | if (trigger.uniqueTags) { 505 | // Filter items that has already been mentioned 506 | var id_key = trigger.keys_map.id, tagIds = _.map(tagsCollection, function (tagPos) { return tagPos[3][id_key]; }); 507 | results = _.reject(results, function (item) { 508 | // converting to string ids 509 | return _.include(tagIds, '' + item[id_key]); 510 | }); 511 | } 512 | 513 | if (!results.length) { 514 | return; 515 | } 516 | 517 | var tagsDropDown = $("