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 @ 1252:fe3777d42b76

History | View | Annotate | Download (15.4 KB)

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