To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / plugins / redmine_tags / assets / javascripts / tag-it.js @ 1261:4f5d10466283

History | View | Annotate | Download (21.1 KB)

1 1132:9b2f28ecd125 luis
/*
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 1261:4f5d10466283 luis
            allowDuplicates   : false,
32
            caseSensitive     : true,
33 1132:9b2f28ecd125 luis
            fieldName         : 'tags',
34 1261:4f5d10466283 luis
            placeholderText   : null,   // Sets `placeholder` attr on input field.
35
            readOnly          : false,  // Disables editing.
36
            removeConfirmation: false,  // Require confirmation to remove tags.
37
            tagLimit          : null,   // Max number of tags allowed (null for unlimited).
38
39
            // Used for autocomplete, unless you override `autocomplete.source`.
40 1132:9b2f28ecd125 luis
            availableTags     : [],
41
42 1261:4f5d10466283 luis
            // Use to override or add any options to the autocomplete widget.
43
            //
44
            // By default, autocomplete.source will map to availableTags,
45
            // unless overridden.
46
            autocomplete: {},
47
48
            // Shows autocomplete before the user even types anything.
49
            showAutocompleteOnFocus: false,
50
51
            // When enabled, quotes are unneccesary for inputting multi-word tags.
52 1132:9b2f28ecd125 luis
            allowSpaces: false,
53
54
            // The below options are for using a single field instead of several
55
            // for our form values.
56
            //
57
            // When enabled, will use a single hidden field for the form,
58
            // rather than one per tag. It will delimit tags in the field
59
            // with singleFieldDelimiter.
60
            //
61
            // The easiest way to use singleField is to just instantiate tag-it
62
            // on an INPUT element, in which case singleField is automatically
63 1261:4f5d10466283 luis
            // set to true, and singleFieldNode is set to that element. This
64 1132:9b2f28ecd125 luis
            // way, you don't need to fiddle with these options.
65
            singleField: false,
66
67 1261:4f5d10466283 luis
            // This is just used when preloading data from the field, and for
68
            // populating the field with delimited tags as the user adds them.
69 1132:9b2f28ecd125 luis
            singleFieldDelimiter: ',',
70
71
            // Set this to an input DOM node to use an existing form field.
72
            // Any text in it will be erased on init. But it will be
73
            // populated with the text of tags as they are created,
74
            // delimited by singleFieldDelimiter.
75
            //
76
            // If this is not set, we create an input node for it,
77 1261:4f5d10466283 luis
            // with the name given in settings.fieldName.
78 1132:9b2f28ecd125 luis
            singleFieldNode: null,
79
80 1261:4f5d10466283 luis
            // Whether to animate tag removals or not.
81
            animate: true,
82
83 1132:9b2f28ecd125 luis
            // Optionally set a tabindex attribute on the input that gets
84
            // created for tag-it.
85
            tabIndex: null,
86
87 1261:4f5d10466283 luis
            // Event callbacks.
88
            beforeTagAdded      : null,
89
            afterTagAdded       : null,
90 1132:9b2f28ecd125 luis
91 1261:4f5d10466283 luis
            beforeTagRemoved    : null,
92
            afterTagRemoved     : null,
93
94
            onTagClicked        : null,
95
            onTagLimitExceeded  : null,
96
97
98
            // DEPRECATED:
99
            //
100
            // /!\ These event callbacks are deprecated and WILL BE REMOVED at some
101
            // point in the future. They're here for backwards-compatibility.
102
            // Use the above before/after event callbacks instead.
103 1132:9b2f28ecd125 luis
            onTagAdded  : null,
104
            onTagRemoved: null,
105 1261:4f5d10466283 luis
            // `autocomplete.source` is the replacement for tagSource.
106
            tagSource: null
107
            // Do not use the above deprecated options.
108 1132:9b2f28ecd125 luis
        },
109
110
        _create: function() {
111
            // for handling static scoping inside callbacks
112
            var that = this;
113
114
            // There are 2 kinds of DOM nodes this widget can be instantiated on:
115
            //     1. UL, OL, or some element containing either of these.
116
            //     2. INPUT, in which case 'singleField' is overridden to true,
117
            //        a UL is created and the INPUT is hidden.
118
            if (this.element.is('input')) {
119
                this.tagList = $('<ul></ul>').insertAfter(this.element);
120
                this.options.singleField = true;
121
                this.options.singleFieldNode = this.element;
122
                this.element.css('display', 'none');
123
            } else {
124
                this.tagList = this.element.find('ul, ol').andSelf().last();
125
            }
126
127 1261:4f5d10466283 luis
            this.tagInput = $('<input type="text" />').addClass('ui-widget-content');
128
129
            if (this.options.readOnly) this.tagInput.attr('disabled', 'disabled');
130
131 1132:9b2f28ecd125 luis
            if (this.options.tabIndex) {
132 1261:4f5d10466283 luis
                this.tagInput.attr('tabindex', this.options.tabIndex);
133 1132:9b2f28ecd125 luis
            }
134
135 1261:4f5d10466283 luis
            if (this.options.placeholderText) {
136
                this.tagInput.attr('placeholder', this.options.placeholderText);
137
            }
138
139
            if (!this.options.autocomplete.source) {
140
                this.options.autocomplete.source = function(search, showChoices) {
141
                    var filter = search.term.toLowerCase();
142
                    var choices = $.grep(this.options.availableTags, function(element) {
143
                        // Only match autocomplete options that begin with the search term.
144
                        // (Case insensitive.)
145
                        return (element.toLowerCase().indexOf(filter) === 0);
146
                    });
147
                    showChoices(this._subtractArray(choices, this.assignedTags()));
148
                };
149
            }
150
151
            if (this.options.showAutocompleteOnFocus) {
152
                this.tagInput.focus(function(event, ui) {
153
                    that._showAutocomplete();
154 1132:9b2f28ecd125 luis
                });
155
156 1261:4f5d10466283 luis
                if (typeof this.options.autocomplete.minLength === 'undefined') {
157
                    this.options.autocomplete.minLength = 0;
158
                }
159
            }
160
161
            // Bind autocomplete.source callback functions to this context.
162
            if ($.isFunction(this.options.autocomplete.source)) {
163
                this.options.autocomplete.source = $.proxy(this.options.autocomplete.source, this);
164
            }
165
166
            // DEPRECATED.
167 1132:9b2f28ecd125 luis
            if ($.isFunction(this.options.tagSource)) {
168
                this.options.tagSource = $.proxy(this.options.tagSource, this);
169
            }
170
171
            this.tagList
172
                .addClass('tagit')
173
                .addClass('ui-widget ui-widget-content ui-corner-all')
174
                // Create the input field.
175 1261:4f5d10466283 luis
                .append($('<li class="tagit-new"></li>').append(this.tagInput))
176 1132:9b2f28ecd125 luis
                .click(function(e) {
177
                    var target = $(e.target);
178
                    if (target.hasClass('tagit-label')) {
179 1261:4f5d10466283 luis
                        var tag = target.closest('.tagit-choice');
180
                        if (!tag.hasClass('removed')) {
181
                            that._trigger('onTagClicked', e, {tag: tag, tagLabel: that.tagLabel(tag)});
182
                        }
183 1132:9b2f28ecd125 luis
                    } else {
184
                        // Sets the focus() to the input field, if the user
185
                        // clicks anywhere inside the UL. This is needed
186
                        // because the input field needs to be of a small size.
187 1261:4f5d10466283 luis
                        that.tagInput.focus();
188 1132:9b2f28ecd125 luis
                    }
189
                });
190
191
            // Single field support.
192 1261:4f5d10466283 luis
            var addedExistingFromSingleFieldNode = false;
193 1132:9b2f28ecd125 luis
            if (this.options.singleField) {
194
                if (this.options.singleFieldNode) {
195
                    // Add existing tags from the input field.
196
                    var node = $(this.options.singleFieldNode);
197
                    var tags = node.val().split(this.options.singleFieldDelimiter);
198
                    node.val('');
199
                    $.each(tags, function(index, tag) {
200 1261:4f5d10466283 luis
                        that.createTag(tag, null, true);
201
                        addedExistingFromSingleFieldNode = true;
202 1132:9b2f28ecd125 luis
                    });
203
                } else {
204
                    // Create our single field input after our list.
205 1261:4f5d10466283 luis
                    this.options.singleFieldNode = $('<input type="hidden" style="display:none;" value="" name="' + this.options.fieldName + '" />');
206
                    this.tagList.after(this.options.singleFieldNode);
207 1132:9b2f28ecd125 luis
                }
208
            }
209
210 1261:4f5d10466283 luis
            // Add existing tags from the list, if any.
211
            if (!addedExistingFromSingleFieldNode) {
212
                this.tagList.children('li').each(function() {
213
                    if (!$(this).hasClass('tagit-new')) {
214
                        that.createTag($(this).text(), $(this).attr('class'), true);
215
                        $(this).remove();
216
                    }
217
                });
218
            }
219
220 1132:9b2f28ecd125 luis
            // Events.
221 1261:4f5d10466283 luis
            this.tagInput
222 1132:9b2f28ecd125 luis
                .keydown(function(event) {
223
                    // Backspace is not detected within a keypress, so it must use keydown.
224 1261:4f5d10466283 luis
                    if (event.which == $.ui.keyCode.BACKSPACE && that.tagInput.val() === '') {
225 1132:9b2f28ecd125 luis
                        var tag = that._lastTag();
226
                        if (!that.options.removeConfirmation || tag.hasClass('remove')) {
227
                            // When backspace is pressed, the last tag is deleted.
228
                            that.removeTag(tag);
229
                        } else if (that.options.removeConfirmation) {
230
                            tag.addClass('remove ui-state-highlight');
231
                        }
232
                    } else if (that.options.removeConfirmation) {
233
                        that._lastTag().removeClass('remove ui-state-highlight');
234
                    }
235
236
                    // Comma/Space/Enter are all valid delimiters for new tags,
237
                    // except when there is an open quote or if setting allowSpaces = true.
238 1261:4f5d10466283 luis
                    // Tab will also create a tag, unless the tag input is empty,
239
                    // in which case it isn't caught.
240 1132:9b2f28ecd125 luis
                    if (
241 1261:4f5d10466283 luis
                        event.which === $.ui.keyCode.COMMA ||
242
                        event.which === $.ui.keyCode.ENTER ||
243 1132:9b2f28ecd125 luis
                        (
244
                            event.which == $.ui.keyCode.TAB &&
245 1261:4f5d10466283 luis
                            that.tagInput.val() !== ''
246 1132:9b2f28ecd125 luis
                        ) ||
247
                        (
248
                            event.which == $.ui.keyCode.SPACE &&
249
                            that.options.allowSpaces !== true &&
250
                            (
251 1261:4f5d10466283 luis
                                $.trim(that.tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' ||
252 1132:9b2f28ecd125 luis
                                (
253 1261:4f5d10466283 luis
                                    $.trim(that.tagInput.val()).charAt(0) == '"' &&
254
                                    $.trim(that.tagInput.val()).charAt($.trim(that.tagInput.val()).length - 1) == '"' &&
255
                                    $.trim(that.tagInput.val()).length - 1 !== 0
256 1132:9b2f28ecd125 luis
                                )
257
                            )
258
                        )
259
                    ) {
260 1261:4f5d10466283 luis
                        // Enter submits the form if there's no text in the input.
261
                        if (!(event.which === $.ui.keyCode.ENTER && that.tagInput.val() === '')) {
262
                            event.preventDefault();
263
                        }
264
265 1132:9b2f28ecd125 luis
                        that.createTag(that._cleanedInput());
266
267
                        // The autocomplete doesn't close automatically when TAB is pressed.
268
                        // So let's ensure that it closes.
269 1261:4f5d10466283 luis
                        that.tagInput.autocomplete('close');
270 1132:9b2f28ecd125 luis
                    }
271
                }).blur(function(e){
272 1261:4f5d10466283 luis
                    // Create a tag when the element loses focus.
273
                    // If autocomplete is enabled and suggestion was clicked, don't add it.
274
                    if (!that.tagInput.data('autocomplete-open')) {
275
                        that.createTag(that._cleanedInput());
276
                    }
277 1132:9b2f28ecd125 luis
                });
278
279
            // Autocomplete.
280 1261:4f5d10466283 luis
            if (this.options.availableTags || this.options.tagSource || this.options.autocomplete.source) {
281
                var autocompleteOptions = {
282 1132:9b2f28ecd125 luis
                    select: function(event, ui) {
283
                        that.createTag(ui.item.value);
284
                        // Preventing the tag input to be updated with the chosen value.
285
                        return false;
286
                    }
287 1261:4f5d10466283 luis
                };
288
                $.extend(autocompleteOptions, this.options.autocomplete);
289
290
                // tagSource is deprecated, but takes precedence here since autocomplete.source is set by default,
291
                // while tagSource is left null by default.
292
                autocompleteOptions.source = this.options.tagSource || autocompleteOptions.source;
293
294
                this.tagInput.autocomplete(autocompleteOptions).bind('autocompleteopen', function(event, ui) {
295
                    that.tagInput.data('autocomplete-open', true);
296
                }).bind('autocompleteclose', function(event, ui) {
297
                    that.tagInput.data('autocomplete-open', false)
298 1132:9b2f28ecd125 luis
                });
299
            }
300
        },
301
302
        _cleanedInput: function() {
303
            // Returns the contents of the tag input, cleaned and ready to be passed to createTag
304 1261:4f5d10466283 luis
            return $.trim(this.tagInput.val().replace(/^"(.*)"$/, '$1'));
305 1132:9b2f28ecd125 luis
        },
306
307
        _lastTag: function() {
308 1261:4f5d10466283 luis
            return this.tagList.find('.tagit-choice:last:not(.removed)');
309
        },
310
311
        _tags: function() {
312
            return this.tagList.find('.tagit-choice:not(.removed)');
313 1132:9b2f28ecd125 luis
        },
314
315
        assignedTags: function() {
316
            // Returns an array of tag string values
317
            var that = this;
318
            var tags = [];
319
            if (this.options.singleField) {
320
                tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter);
321
                if (tags[0] === '') {
322
                    tags = [];
323
                }
324
            } else {
325 1261:4f5d10466283 luis
                this._tags().each(function() {
326 1132:9b2f28ecd125 luis
                    tags.push(that.tagLabel(this));
327
                });
328
            }
329
            return tags;
330
        },
331
332
        _updateSingleTagsField: function(tags) {
333
            // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter
334 1261:4f5d10466283 luis
            $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter)).trigger('change');
335 1132:9b2f28ecd125 luis
        },
336
337
        _subtractArray: function(a1, a2) {
338
            var result = [];
339
            for (var i = 0; i < a1.length; i++) {
340
                if ($.inArray(a1[i], a2) == -1) {
341
                    result.push(a1[i]);
342
                }
343
            }
344
            return result;
345
        },
346
347
        tagLabel: function(tag) {
348
            // Returns the tag's string label.
349
            if (this.options.singleField) {
350 1261:4f5d10466283 luis
                return $(tag).find('.tagit-label:first').text();
351 1132:9b2f28ecd125 luis
            } else {
352 1261:4f5d10466283 luis
                return $(tag).find('input:first').val();
353 1132:9b2f28ecd125 luis
            }
354
        },
355
356 1261:4f5d10466283 luis
        _showAutocomplete: function() {
357
            this.tagInput.autocomplete('search', '');
358
        },
359
360
        _findTagByLabel: function(name) {
361 1132:9b2f28ecd125 luis
            var that = this;
362 1261:4f5d10466283 luis
            var tag = null;
363
            this._tags().each(function(i) {
364
                if (that._formatStr(name) == that._formatStr(that.tagLabel(this))) {
365
                    tag = $(this);
366 1132:9b2f28ecd125 luis
                    return false;
367
                }
368
            });
369 1261:4f5d10466283 luis
            return tag;
370
        },
371
372
        _isNew: function(name) {
373
            return !this._findTagByLabel(name);
374 1132:9b2f28ecd125 luis
        },
375
376
        _formatStr: function(str) {
377
            if (this.options.caseSensitive) {
378
                return str;
379
            }
380
            return $.trim(str.toLowerCase());
381
        },
382
383 1261:4f5d10466283 luis
        _effectExists: function(name) {
384
            return Boolean($.effects && ($.effects[name] || ($.effects.effect && $.effects.effect[name])));
385
        },
386
387
        createTag: function(value, additionalClass, duringInitialization) {
388 1132:9b2f28ecd125 luis
            var that = this;
389 1261:4f5d10466283 luis
390 1132:9b2f28ecd125 luis
            value = $.trim(value);
391
392 1261:4f5d10466283 luis
            if(this.options.preprocessTag) {
393
                value = this.options.preprocessTag(value);
394
            }
395
396
            if (value === '') {
397
                return false;
398
            }
399
400
            if (!this.options.allowDuplicates && !this._isNew(value)) {
401
                var existingTag = this._findTagByLabel(value);
402
                if (this._trigger('onTagExists', null, {
403
                    existingTag: existingTag,
404
                    duringInitialization: duringInitialization
405
                }) !== false) {
406
                    if (this._effectExists('highlight')) {
407
                        existingTag.effect('highlight');
408
                    }
409
                }
410
                return false;
411
            }
412
413
            if (this.options.tagLimit && this._tags().length >= this.options.tagLimit) {
414
                this._trigger('onTagLimitExceeded', null, {duringInitialization: duringInitialization});
415 1132:9b2f28ecd125 luis
                return false;
416
            }
417
418
            var label = $(this.options.onTagClicked ? '<a class="tagit-label"></a>' : '<span class="tagit-label"></span>').text(value);
419
420
            // Create tag.
421
            var tag = $('<li></li>')
422
                .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all')
423
                .addClass(additionalClass)
424
                .append(label);
425
426 1261:4f5d10466283 luis
            if (this.options.readOnly){
427
                tag.addClass('tagit-choice-read-only');
428
            } else {
429
                tag.addClass('tagit-choice-editable');
430
                // Button for removing the tag.
431
                var removeTagIcon = $('<span></span>')
432
                    .addClass('ui-icon ui-icon-close');
433
                var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X
434
                    .addClass('tagit-close')
435
                    .append(removeTagIcon)
436
                    .click(function(e) {
437
                        // Removes a tag when the little 'x' is clicked.
438
                        that.removeTag(tag);
439
                    });
440
                tag.append(removeTag);
441
            }
442 1132:9b2f28ecd125 luis
443
            // Unless options.singleField is set, each tag has a hidden input field inline.
444 1261:4f5d10466283 luis
            if (!this.options.singleField) {
445
                var escapedValue = label.html();
446
                tag.append('<input type="hidden" style="display:none;" value="' + escapedValue + '" name="' + this.options.fieldName + '" />');
447
            }
448
449
            if (this._trigger('beforeTagAdded', null, {
450
                tag: tag,
451
                tagLabel: this.tagLabel(tag),
452
                duringInitialization: duringInitialization
453
            }) === false) {
454
                return;
455
            }
456
457 1132:9b2f28ecd125 luis
            if (this.options.singleField) {
458
                var tags = this.assignedTags();
459
                tags.push(value);
460
                this._updateSingleTagsField(tags);
461
            }
462
463 1261:4f5d10466283 luis
            // DEPRECATED.
464 1132:9b2f28ecd125 luis
            this._trigger('onTagAdded', null, tag);
465
466 1261:4f5d10466283 luis
            this.tagInput.val('');
467 1132:9b2f28ecd125 luis
468 1261:4f5d10466283 luis
            // Insert tag.
469
            this.tagInput.parent().before(tag);
470
471
            this._trigger('afterTagAdded', null, {
472
                tag: tag,
473
                tagLabel: this.tagLabel(tag),
474
                duringInitialization: duringInitialization
475
            });
476
477
            if (this.options.showAutocompleteOnFocus && !duringInitialization) {
478
                setTimeout(function () { that._showAutocomplete(); }, 0);
479
            }
480 1132:9b2f28ecd125 luis
        },
481 1261:4f5d10466283 luis
482 1132:9b2f28ecd125 luis
        removeTag: function(tag, animate) {
483 1261:4f5d10466283 luis
            animate = typeof animate === 'undefined' ? this.options.animate : animate;
484 1132:9b2f28ecd125 luis
485
            tag = $(tag);
486
487 1261:4f5d10466283 luis
            // DEPRECATED.
488 1132:9b2f28ecd125 luis
            this._trigger('onTagRemoved', null, tag);
489
490 1261:4f5d10466283 luis
            if (this._trigger('beforeTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}) === false) {
491
                return;
492
            }
493
494 1132:9b2f28ecd125 luis
            if (this.options.singleField) {
495
                var tags = this.assignedTags();
496
                var removedTagLabel = this.tagLabel(tag);
497
                tags = $.grep(tags, function(el){
498
                    return el != removedTagLabel;
499
                });
500
                this._updateSingleTagsField(tags);
501
            }
502 1261:4f5d10466283 luis
503 1132:9b2f28ecd125 luis
            if (animate) {
504 1261:4f5d10466283 luis
                tag.addClass('removed'); // Excludes this tag from _tags.
505
                var hide_args = this._effectExists('blind') ? ['blind', {direction: 'horizontal'}, 'fast'] : ['fast'];
506
507
                var thisTag = this;
508
                hide_args.push(function() {
509 1132:9b2f28ecd125 luis
                    tag.remove();
510 1261:4f5d10466283 luis
                    thisTag._trigger('afterTagRemoved', null, {tag: tag, tagLabel: thisTag.tagLabel(tag)});
511
                });
512
513
                tag.fadeOut('fast').hide.apply(tag, hide_args).dequeue();
514 1132:9b2f28ecd125 luis
            } else {
515
                tag.remove();
516 1261:4f5d10466283 luis
                this._trigger('afterTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)});
517 1132:9b2f28ecd125 luis
            }
518 1261:4f5d10466283 luis
519
        },
520
521
        removeTagByLabel: function(tagLabel, animate) {
522
            var toRemove = this._findTagByLabel(tagLabel);
523
            if (!toRemove) {
524
                throw "No such tag exists with the name '" + tagLabel + "'";
525
            }
526
            this.removeTag(toRemove, animate);
527 1132:9b2f28ecd125 luis
        },
528
529
        removeAll: function() {
530
            // Removes all tags.
531
            var that = this;
532 1261:4f5d10466283 luis
            this._tags().each(function(index, tag) {
533 1132:9b2f28ecd125 luis
                that.removeTag(tag, false);
534
            });
535
        }
536
537
    });
538 1261:4f5d10466283 luis
})(jQuery);