|
1 /* |
|
2 * jQuery UI Tag-it! |
|
3 * |
|
4 * @version v2.0 (06/2011) |
|
5 * |
|
6 * Copyright 2011, Levy Carneiro Jr. |
|
7 * Released under the MIT license. |
|
8 * http://aehlke.github.com/tag-it/LICENSE |
|
9 * |
|
10 * Homepage: |
|
11 * http://aehlke.github.com/tag-it/ |
|
12 * |
|
13 * Authors: |
|
14 * Levy Carneiro Jr. |
|
15 * Martin Rehfeld |
|
16 * Tobias Schmidt |
|
17 * Skylar Challand |
|
18 * Alex Ehlke |
|
19 * |
|
20 * Maintainer: |
|
21 * Alex Ehlke - Twitter: @aehlke |
|
22 * |
|
23 * Dependencies: |
|
24 * jQuery v1.4+ |
|
25 * jQuery UI v1.8+ |
|
26 */ |
|
27 (function($) { |
|
28 |
|
29 $.widget('ui.tagit', { |
|
30 options: { |
|
31 itemName : 'item', |
|
32 fieldName : 'tags', |
|
33 availableTags : [], |
|
34 tagSource : null, |
|
35 removeConfirmation: false, |
|
36 caseSensitive : true, |
|
37 placeholderText : null, |
|
38 |
|
39 // When enabled, quotes are not neccesary |
|
40 // for inputting multi-word tags. |
|
41 allowSpaces: false, |
|
42 |
|
43 // Whether to animate tag removals or not. |
|
44 animate: true, |
|
45 |
|
46 // The below options are for using a single field instead of several |
|
47 // for our form values. |
|
48 // |
|
49 // When enabled, will use a single hidden field for the form, |
|
50 // rather than one per tag. It will delimit tags in the field |
|
51 // with singleFieldDelimiter. |
|
52 // |
|
53 // The easiest way to use singleField is to just instantiate tag-it |
|
54 // on an INPUT element, in which case singleField is automatically |
|
55 // set to true, and singleFieldNode is set to that element. This |
|
56 // way, you don't need to fiddle with these options. |
|
57 singleField: false, |
|
58 |
|
59 singleFieldDelimiter: ',', |
|
60 |
|
61 // Set this to an input DOM node to use an existing form field. |
|
62 // Any text in it will be erased on init. But it will be |
|
63 // populated with the text of tags as they are created, |
|
64 // delimited by singleFieldDelimiter. |
|
65 // |
|
66 // If this is not set, we create an input node for it, |
|
67 // with the name given in settings.fieldName, |
|
68 // ignoring settings.itemName. |
|
69 singleFieldNode: null, |
|
70 |
|
71 // Optionally set a tabindex attribute on the input that gets |
|
72 // created for tag-it. |
|
73 tabIndex: null, |
|
74 |
|
75 |
|
76 // Event callbacks. |
|
77 onTagAdded : null, |
|
78 onTagRemoved: null, |
|
79 onTagClicked: null |
|
80 }, |
|
81 |
|
82 |
|
83 _create: function() { |
|
84 // for handling static scoping inside callbacks |
|
85 var that = this; |
|
86 |
|
87 // There are 2 kinds of DOM nodes this widget can be instantiated on: |
|
88 // 1. UL, OL, or some element containing either of these. |
|
89 // 2. INPUT, in which case 'singleField' is overridden to true, |
|
90 // a UL is created and the INPUT is hidden. |
|
91 if (this.element.is('input')) { |
|
92 this.tagList = $('<ul></ul>').insertAfter(this.element); |
|
93 this.options.singleField = true; |
|
94 this.options.singleFieldNode = this.element; |
|
95 this.element.css('display', 'none'); |
|
96 } else { |
|
97 this.tagList = this.element.find('ul, ol').andSelf().last(); |
|
98 } |
|
99 |
|
100 this._tagInput = $('<input type="text" />').addClass('ui-widget-content'); |
|
101 if (this.options.tabIndex) { |
|
102 this._tagInput.attr('tabindex', this.options.tabIndex); |
|
103 } |
|
104 if (this.options.placeholderText) { |
|
105 this._tagInput.attr('placeholder', this.options.placeholderText); |
|
106 } |
|
107 |
|
108 this.options.tagSource = this.options.tagSource || function(search, showChoices) { |
|
109 var filter = search.term.toLowerCase(); |
|
110 var choices = $.grep(this.options.availableTags, function(element) { |
|
111 // Only match autocomplete options that begin with the search term. |
|
112 // (Case insensitive.) |
|
113 return (element.toLowerCase().indexOf(filter) === 0); |
|
114 }); |
|
115 showChoices(this._subtractArray(choices, this.assignedTags())); |
|
116 }; |
|
117 |
|
118 // Bind tagSource callback functions to this context. |
|
119 if ($.isFunction(this.options.tagSource)) { |
|
120 this.options.tagSource = $.proxy(this.options.tagSource, this); |
|
121 } |
|
122 |
|
123 this.tagList |
|
124 .addClass('tagit') |
|
125 .addClass('ui-widget ui-widget-content ui-corner-all') |
|
126 // Create the input field. |
|
127 .append($('<li class="tagit-new"></li>').append(this._tagInput)) |
|
128 .click(function(e) { |
|
129 var target = $(e.target); |
|
130 if (target.hasClass('tagit-label')) { |
|
131 that._trigger('onTagClicked', e, target.closest('.tagit-choice')); |
|
132 } else { |
|
133 // Sets the focus() to the input field, if the user |
|
134 // clicks anywhere inside the UL. This is needed |
|
135 // because the input field needs to be of a small size. |
|
136 that._tagInput.focus(); |
|
137 } |
|
138 }); |
|
139 |
|
140 // Add existing tags from the list, if any. |
|
141 this.tagList.children('li').each(function() { |
|
142 if (!$(this).hasClass('tagit-new')) { |
|
143 that.createTag($(this).html(), $(this).attr('class')); |
|
144 $(this).remove(); |
|
145 } |
|
146 }); |
|
147 |
|
148 // Single field support. |
|
149 if (this.options.singleField) { |
|
150 if (this.options.singleFieldNode) { |
|
151 // Add existing tags from the input field. |
|
152 var node = $(this.options.singleFieldNode); |
|
153 var tags = node.val().split(this.options.singleFieldDelimiter); |
|
154 node.val(''); |
|
155 $.each(tags, function(index, tag) { |
|
156 that.createTag(tag); |
|
157 }); |
|
158 } else { |
|
159 // Create our single field input after our list. |
|
160 this.options.singleFieldNode = this.tagList.after('<input type="hidden" style="display:none;" value="" name="' + this.options.fieldName + '" />'); |
|
161 } |
|
162 } |
|
163 |
|
164 // Events. |
|
165 this._tagInput |
|
166 .keydown(function(event) { |
|
167 // Backspace is not detected within a keypress, so it must use keydown. |
|
168 if (event.which == $.ui.keyCode.BACKSPACE && that._tagInput.val() === '') { |
|
169 var tag = that._lastTag(); |
|
170 if (!that.options.removeConfirmation || tag.hasClass('remove')) { |
|
171 // When backspace is pressed, the last tag is deleted. |
|
172 that.removeTag(tag); |
|
173 } else if (that.options.removeConfirmation) { |
|
174 tag.addClass('remove ui-state-highlight'); |
|
175 } |
|
176 } else if (that.options.removeConfirmation) { |
|
177 that._lastTag().removeClass('remove ui-state-highlight'); |
|
178 } |
|
179 |
|
180 // Comma/Space/Enter are all valid delimiters for new tags, |
|
181 // except when there is an open quote or if setting allowSpaces = true. |
|
182 // Tab will also create a tag, unless the tag input is empty, in which case it isn't caught. |
|
183 if ( |
|
184 event.which == $.ui.keyCode.COMMA || |
|
185 event.which == $.ui.keyCode.ENTER || |
|
186 ( |
|
187 event.which == $.ui.keyCode.TAB && |
|
188 that._tagInput.val() !== '' |
|
189 ) || |
|
190 ( |
|
191 event.which == $.ui.keyCode.SPACE && |
|
192 that.options.allowSpaces !== true && |
|
193 ( |
|
194 $.trim(that._tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' || |
|
195 ( |
|
196 $.trim(that._tagInput.val()).charAt(0) == '"' && |
|
197 $.trim(that._tagInput.val()).charAt($.trim(that._tagInput.val()).length - 1) == '"' && |
|
198 $.trim(that._tagInput.val()).length - 1 !== 0 |
|
199 ) |
|
200 ) |
|
201 ) |
|
202 ) { |
|
203 event.preventDefault(); |
|
204 that.createTag(that._cleanedInput()); |
|
205 |
|
206 // The autocomplete doesn't close automatically when TAB is pressed. |
|
207 // So let's ensure that it closes. |
|
208 that._tagInput.autocomplete('close'); |
|
209 } |
|
210 }).blur(function(e){ |
|
211 // Create a tag when the element loses focus (unless it's empty). |
|
212 that.createTag(that._cleanedInput()); |
|
213 }); |
|
214 |
|
215 |
|
216 // Autocomplete. |
|
217 if (this.options.availableTags || this.options.tagSource) { |
|
218 this._tagInput.autocomplete({ |
|
219 source: this.options.tagSource, |
|
220 select: function(event, ui) { |
|
221 // Delete the last tag if we autocomplete something despite the input being empty |
|
222 // This happens because the input's blur event causes the tag to be created when |
|
223 // the user clicks an autocomplete item. |
|
224 // The only artifact of this is that while the user holds down the mouse button |
|
225 // on the selected autocomplete item, a tag is shown with the pre-autocompleted text, |
|
226 // and is changed to the autocompleted text upon mouseup. |
|
227 if (that._tagInput.val() === '') { |
|
228 that.removeTag(that._lastTag(), false); |
|
229 } |
|
230 that.createTag(ui.item.value); |
|
231 // Preventing the tag input to be updated with the chosen value. |
|
232 return false; |
|
233 } |
|
234 }); |
|
235 } |
|
236 }, |
|
237 |
|
238 _cleanedInput: function() { |
|
239 // Returns the contents of the tag input, cleaned and ready to be passed to createTag |
|
240 return $.trim(this._tagInput.val().replace(/^"(.*)"$/, '$1')); |
|
241 }, |
|
242 |
|
243 _lastTag: function() { |
|
244 return this.tagList.children('.tagit-choice:last'); |
|
245 }, |
|
246 |
|
247 assignedTags: function() { |
|
248 // Returns an array of tag string values |
|
249 var that = this; |
|
250 var tags = []; |
|
251 if (this.options.singleField) { |
|
252 tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter); |
|
253 if (tags[0] === '') { |
|
254 tags = []; |
|
255 } |
|
256 } else { |
|
257 this.tagList.children('.tagit-choice').each(function() { |
|
258 tags.push(that.tagLabel(this)); |
|
259 }); |
|
260 } |
|
261 return tags; |
|
262 }, |
|
263 |
|
264 _updateSingleTagsField: function(tags) { |
|
265 // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter |
|
266 $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter)); |
|
267 }, |
|
268 |
|
269 _subtractArray: function(a1, a2) { |
|
270 var result = []; |
|
271 for (var i = 0; i < a1.length; i++) { |
|
272 if ($.inArray(a1[i], a2) == -1) { |
|
273 result.push(a1[i]); |
|
274 } |
|
275 } |
|
276 return result; |
|
277 }, |
|
278 |
|
279 tagLabel: function(tag) { |
|
280 // Returns the tag's string label. |
|
281 if (this.options.singleField) { |
|
282 return $(tag).children('.tagit-label').text(); |
|
283 } else { |
|
284 return $(tag).children('input').val(); |
|
285 } |
|
286 }, |
|
287 |
|
288 _isNew: function(value) { |
|
289 var that = this; |
|
290 var isNew = true; |
|
291 this.tagList.children('.tagit-choice').each(function(i) { |
|
292 if (that._formatStr(value) == that._formatStr(that.tagLabel(this))) { |
|
293 isNew = false; |
|
294 return false; |
|
295 } |
|
296 }); |
|
297 return isNew; |
|
298 }, |
|
299 |
|
300 _formatStr: function(str) { |
|
301 if (this.options.caseSensitive) { |
|
302 return str; |
|
303 } |
|
304 return $.trim(str.toLowerCase()); |
|
305 }, |
|
306 |
|
307 createTag: function(value, additionalClass) { |
|
308 var that = this; |
|
309 // Automatically trims the value of leading and trailing whitespace. |
|
310 value = $.trim(value); |
|
311 |
|
312 if (!this._isNew(value) || value === '') { |
|
313 return false; |
|
314 } |
|
315 |
|
316 var label = $(this.options.onTagClicked ? '<a class="tagit-label"></a>' : '<span class="tagit-label"></span>').text(value); |
|
317 |
|
318 // Create tag. |
|
319 var tag = $('<li></li>') |
|
320 .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all') |
|
321 .addClass(additionalClass) |
|
322 .append(label); |
|
323 |
|
324 // Button for removing the tag. |
|
325 var removeTagIcon = $('<span></span>') |
|
326 .addClass('ui-icon ui-icon-close'); |
|
327 var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X |
|
328 .addClass('tagit-close') |
|
329 .append(removeTagIcon) |
|
330 .click(function(e) { |
|
331 // Removes a tag when the little 'x' is clicked. |
|
332 that.removeTag(tag); |
|
333 }); |
|
334 tag.append(removeTag); |
|
335 |
|
336 // Unless options.singleField is set, each tag has a hidden input field inline. |
|
337 if (this.options.singleField) { |
|
338 var tags = this.assignedTags(); |
|
339 tags.push(value); |
|
340 this._updateSingleTagsField(tags); |
|
341 } else { |
|
342 var escapedValue = label.html(); |
|
343 tag.append('<input type="hidden" style="display:none;" value="' + escapedValue + '" name="' + this.options.itemName + '[' + this.options.fieldName + '][]" />'); |
|
344 } |
|
345 |
|
346 this._trigger('onTagAdded', null, tag); |
|
347 |
|
348 // Cleaning the input. |
|
349 this._tagInput.val(''); |
|
350 |
|
351 // insert tag |
|
352 this._tagInput.parent().before(tag); |
|
353 }, |
|
354 |
|
355 removeTag: function(tag, animate) { |
|
356 animate = animate || this.options.animate; |
|
357 |
|
358 tag = $(tag); |
|
359 |
|
360 this._trigger('onTagRemoved', null, tag); |
|
361 |
|
362 if (this.options.singleField) { |
|
363 var tags = this.assignedTags(); |
|
364 var removedTagLabel = this.tagLabel(tag); |
|
365 tags = $.grep(tags, function(el){ |
|
366 return el != removedTagLabel; |
|
367 }); |
|
368 this._updateSingleTagsField(tags); |
|
369 } |
|
370 // Animate the removal. |
|
371 if (animate) { |
|
372 tag.fadeOut('fast').hide('blind', {direction: 'horizontal'}, 'fast', function(){ |
|
373 tag.remove(); |
|
374 }).dequeue(); |
|
375 } else { |
|
376 tag.remove(); |
|
377 } |
|
378 }, |
|
379 |
|
380 removeAll: function() { |
|
381 // Removes all tags. |
|
382 var that = this; |
|
383 this.tagList.children('.tagit-choice').each(function(index, tag) { |
|
384 that.removeTag(tag, false); |
|
385 }); |
|
386 } |
|
387 |
|
388 }); |
|
389 |
|
390 })(jQuery); |
|
391 |
|
392 |