├── 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 | [](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 = $("").addClass(trigger.classes.tagsDropDown).appendTo(elTagList),
518 | imgOrIconTpl = trigger.showImageOrIcon ? templates.tagsListItemImage : templates.tagsListItemIcon,
519 | objPropTransformer = transformObjectProperties(trigger.keys_map);
520 |
521 | _.each(results, function (tag, index) {
522 | var tagItem, localTag = objPropTransformer(tag, false);
523 | localTag.title = utils.highlightTerm(utils.htmlEncode((localTag.title)), query);
524 | tagItem = $(templates.tagsListItem(localTag)).data('tag', tag);
525 | if(localTag.img) {
526 | tagItem = tagItem.prepend(imgOrIconTpl(localTag));
527 | }
528 | tagItem.appendTo(tagsDropDown);
529 |
530 | if (index === 0) {
531 | selectTagListItem(tagItem, trigger.classes.tagActiveDropDown);
532 | }
533 | });
534 |
535 | elTagList.show();
536 | }
537 |
538 | function searchTags (query, triggerChar) {
539 | hideTagList();
540 | settings.onDataRequest.call(this, 'search', query, triggerChar, function (responseData) {
541 | populateTagList(query, triggerChar, responseData);
542 | });
543 | }
544 |
545 | // Public methods
546 | return {
547 | init : function (options) {
548 | if (setSettings(options)) {
549 | initTextarea();
550 | initTagList();
551 | initBeautifier();
552 | initState();
553 | }
554 | },
555 | val : function (callback) {
556 | if (_.isString(callback)) {
557 | var removedTags = _.uniq(_.map(tagsCollection, function (tagPos) { return tagPos[3]; }));
558 | elEditor.trigger('tagsRemoved.textntags', [removedTags]);
559 | elEditor.val(callback);
560 | initState();
561 | return;
562 | } else if (!_.isFunction(callback)) {
563 | return;
564 | }
565 |
566 | var value = tagsCollection.length ? getTaggedText() : getEditorValue();
567 | callback.call(this, value);
568 | },
569 | reset : function () {
570 | var removedTags = _.uniq(_.map(tagsCollection, function (tagPos) { return tagPos[3]; }));
571 | elEditor.trigger('tagsRemoved.textntags', [removedTags]);
572 | elEditor.val('');
573 | tagsCollection = [];
574 | updateBeautifier();
575 | },
576 | getTags : function (callback) {
577 | if (!_.isFunction(callback)) {
578 | return;
579 | }
580 | var tags = _.map(tagsCollection, function (tagPos) { return tagPos[3]; });
581 |
582 | callback.call(this, _.uniq(tags));
583 | },
584 | getTagsMap : function (callback) {
585 | if (!_.isFunction(callback)) {
586 | return;
587 | }
588 |
589 | callback.call(this, tagsCollection);
590 | },
591 | getTagsMapFacebook : function (callback) {
592 | if (!_.isFunction(callback)) {
593 | return;
594 | }
595 | var fbTagsCollection = {}, triggers = settings.triggers;
596 |
597 | _.each(tagsCollection, function (tagPos) {
598 | var objPropTransformer = transformObjectProperties(triggers[tagPos[2]].keys_map),
599 | localTag = objPropTransformer(tagPos[3], false);
600 | fbTagsCollection[tagPos[0]] = [{
601 | id: localTag.id,
602 | name: localTag.title,
603 | type: localTag.type,
604 | offset: tagPos[0],
605 | length: tagPos[1]
606 | }];
607 | });
608 |
609 | callback.call(this, fbTagsCollection);
610 | },
611 | parseTaggedText: function (tagged_text, callback) {
612 | if (!_.isFunction(callback)) {
613 | return;
614 | }
615 |
616 | callback.call(this, parseTaggedText(tagged_text));
617 | }
618 | };
619 | };
620 |
621 | $.fn.textntags = function (methodOrSettings) {
622 | var outerArguments = arguments;
623 |
624 | return this.each(function () {
625 | var ms = methodOrSettings, instance = $.data(this, 'textntags') || $.data(this, 'textntags', new TextNTags(this));
626 |
627 | if (_.isFunction(instance[ms])) {
628 | return instance[ms].apply(this, Array.prototype.slice.call(outerArguments, 1));
629 | } else if (typeof ms === 'object' || !ms) {
630 | return instance.init.call(this, ms);
631 | } else {
632 | $.error('Method ' + ms + ' does not exist');
633 | }
634 | });
635 | };
636 |
637 | })(jQuery, _);
638 |
--------------------------------------------------------------------------------