Mercurial > hg > soundsoftware-site
changeset 1261:4f5d10466283 redmine-2.2-integration
Updated the TagIt script to the latest version.
author | luisf <luis.figueira@eecs.qmul.ac.uk> |
---|---|
date | Thu, 11 Apr 2013 15:09:12 +0100 |
parents | b18f581b260a |
children | 8fee6c95fb35 |
files | plugins/redmine_tags/assets/javascripts/tag-it.js |
diffstat | 1 files changed, 259 insertions(+), 113 deletions(-) [+] |
line wrap: on
line diff
--- a/plugins/redmine_tags/assets/javascripts/tag-it.js Thu Apr 11 12:34:22 2013 +0100 +++ b/plugins/redmine_tags/assets/javascripts/tag-it.js Thu Apr 11 15:09:12 2013 +0100 @@ -28,21 +28,29 @@ $.widget('ui.tagit', { options: { - itemName : 'item', + allowDuplicates : false, + caseSensitive : true, fieldName : 'tags', + placeholderText : null, // Sets `placeholder` attr on input field. + readOnly : false, // Disables editing. + removeConfirmation: false, // Require confirmation to remove tags. + tagLimit : null, // Max number of tags allowed (null for unlimited). + + // Used for autocomplete, unless you override `autocomplete.source`. availableTags : [], - tagSource : null, - removeConfirmation: false, - caseSensitive : true, - placeholderText : null, - // When enabled, quotes are not neccesary - // for inputting multi-word tags. + // Use to override or add any options to the autocomplete widget. + // + // By default, autocomplete.source will map to availableTags, + // unless overridden. + autocomplete: {}, + + // Shows autocomplete before the user even types anything. + showAutocompleteOnFocus: false, + + // When enabled, quotes are unneccesary for inputting multi-word tags. allowSpaces: false, - // Whether to animate tag removals or not. - animate: true, - // The below options are for using a single field instead of several // for our form values. // @@ -52,10 +60,12 @@ // // The easiest way to use singleField is to just instantiate tag-it // on an INPUT element, in which case singleField is automatically - // set to true, and singleFieldNode is set to that element. This + // set to true, and singleFieldNode is set to that element. This // way, you don't need to fiddle with these options. singleField: false, + // This is just used when preloading data from the field, and for + // populating the field with delimited tags as the user adds them. singleFieldDelimiter: ',', // Set this to an input DOM node to use an existing form field. @@ -64,22 +74,39 @@ // delimited by singleFieldDelimiter. // // If this is not set, we create an input node for it, - // with the name given in settings.fieldName, - // ignoring settings.itemName. + // with the name given in settings.fieldName. singleFieldNode: null, + // Whether to animate tag removals or not. + animate: true, + // Optionally set a tabindex attribute on the input that gets // created for tag-it. tabIndex: null, + // Event callbacks. + beforeTagAdded : null, + afterTagAdded : null, - // Event callbacks. + beforeTagRemoved : null, + afterTagRemoved : null, + + onTagClicked : null, + onTagLimitExceeded : null, + + + // DEPRECATED: + // + // /!\ These event callbacks are deprecated and WILL BE REMOVED at some + // point in the future. They're here for backwards-compatibility. + // Use the above before/after event callbacks instead. onTagAdded : null, onTagRemoved: null, - onTagClicked: null + // `autocomplete.source` is the replacement for tagSource. + tagSource: null + // Do not use the above deprecated options. }, - _create: function() { // for handling static scoping inside callbacks var that = this; @@ -97,25 +124,46 @@ this.tagList = this.element.find('ul, ol').andSelf().last(); } - this._tagInput = $('<input type="text" />').addClass('ui-widget-content'); + this.tagInput = $('<input type="text" />').addClass('ui-widget-content'); + + if (this.options.readOnly) this.tagInput.attr('disabled', 'disabled'); + if (this.options.tabIndex) { - this._tagInput.attr('tabindex', this.options.tabIndex); - } - if (this.options.placeholderText) { - this._tagInput.attr('placeholder', this.options.placeholderText); + this.tagInput.attr('tabindex', this.options.tabIndex); } - this.options.tagSource = this.options.tagSource || function(search, showChoices) { - var filter = search.term.toLowerCase(); - var choices = $.grep(this.options.availableTags, function(element) { - // Only match autocomplete options that begin with the search term. - // (Case insensitive.) - return (element.toLowerCase().indexOf(filter) === 0); + if (this.options.placeholderText) { + this.tagInput.attr('placeholder', this.options.placeholderText); + } + + if (!this.options.autocomplete.source) { + this.options.autocomplete.source = function(search, showChoices) { + var filter = search.term.toLowerCase(); + var choices = $.grep(this.options.availableTags, function(element) { + // Only match autocomplete options that begin with the search term. + // (Case insensitive.) + return (element.toLowerCase().indexOf(filter) === 0); + }); + showChoices(this._subtractArray(choices, this.assignedTags())); + }; + } + + if (this.options.showAutocompleteOnFocus) { + this.tagInput.focus(function(event, ui) { + that._showAutocomplete(); }); - showChoices(this._subtractArray(choices, this.assignedTags())); - }; - // Bind tagSource callback functions to this context. + if (typeof this.options.autocomplete.minLength === 'undefined') { + this.options.autocomplete.minLength = 0; + } + } + + // Bind autocomplete.source callback functions to this context. + if ($.isFunction(this.options.autocomplete.source)) { + this.options.autocomplete.source = $.proxy(this.options.autocomplete.source, this); + } + + // DEPRECATED. if ($.isFunction(this.options.tagSource)) { this.options.tagSource = $.proxy(this.options.tagSource, this); } @@ -124,28 +172,24 @@ .addClass('tagit') .addClass('ui-widget ui-widget-content ui-corner-all') // Create the input field. - .append($('<li class="tagit-new"></li>').append(this._tagInput)) + .append($('<li class="tagit-new"></li>').append(this.tagInput)) .click(function(e) { var target = $(e.target); if (target.hasClass('tagit-label')) { - that._trigger('onTagClicked', e, target.closest('.tagit-choice')); + var tag = target.closest('.tagit-choice'); + if (!tag.hasClass('removed')) { + that._trigger('onTagClicked', e, {tag: tag, tagLabel: that.tagLabel(tag)}); + } } else { // Sets the focus() to the input field, if the user // clicks anywhere inside the UL. This is needed // because the input field needs to be of a small size. - that._tagInput.focus(); + that.tagInput.focus(); } }); - // Add existing tags from the list, if any. - this.tagList.children('li').each(function() { - if (!$(this).hasClass('tagit-new')) { - that.createTag($(this).html(), $(this).attr('class')); - $(this).remove(); - } - }); - // Single field support. + var addedExistingFromSingleFieldNode = false; if (this.options.singleField) { if (this.options.singleFieldNode) { // Add existing tags from the input field. @@ -153,19 +197,31 @@ var tags = node.val().split(this.options.singleFieldDelimiter); node.val(''); $.each(tags, function(index, tag) { - that.createTag(tag); + that.createTag(tag, null, true); + addedExistingFromSingleFieldNode = true; }); } else { // Create our single field input after our list. - this.options.singleFieldNode = this.tagList.after('<input type="hidden" style="display:none;" value="" name="' + this.options.fieldName + '" />'); + this.options.singleFieldNode = $('<input type="hidden" style="display:none;" value="" name="' + this.options.fieldName + '" />'); + this.tagList.after(this.options.singleFieldNode); } } + // Add existing tags from the list, if any. + if (!addedExistingFromSingleFieldNode) { + this.tagList.children('li').each(function() { + if (!$(this).hasClass('tagit-new')) { + that.createTag($(this).text(), $(this).attr('class'), true); + $(this).remove(); + } + }); + } + // Events. - this._tagInput + this.tagInput .keydown(function(event) { // Backspace is not detected within a keypress, so it must use keydown. - if (event.which == $.ui.keyCode.BACKSPACE && that._tagInput.val() === '') { + if (event.which == $.ui.keyCode.BACKSPACE && that.tagInput.val() === '') { var tag = that._lastTag(); if (!that.options.removeConfirmation || tag.hasClass('remove')) { // When backspace is pressed, the last tag is deleted. @@ -179,69 +235,81 @@ // Comma/Space/Enter are all valid delimiters for new tags, // except when there is an open quote or if setting allowSpaces = true. - // Tab will also create a tag, unless the tag input is empty, in which case it isn't caught. + // Tab will also create a tag, unless the tag input is empty, + // in which case it isn't caught. if ( - event.which == $.ui.keyCode.COMMA || - event.which == $.ui.keyCode.ENTER || + event.which === $.ui.keyCode.COMMA || + event.which === $.ui.keyCode.ENTER || ( event.which == $.ui.keyCode.TAB && - that._tagInput.val() !== '' + that.tagInput.val() !== '' ) || ( event.which == $.ui.keyCode.SPACE && that.options.allowSpaces !== true && ( - $.trim(that._tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' || + $.trim(that.tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' || ( - $.trim(that._tagInput.val()).charAt(0) == '"' && - $.trim(that._tagInput.val()).charAt($.trim(that._tagInput.val()).length - 1) == '"' && - $.trim(that._tagInput.val()).length - 1 !== 0 + $.trim(that.tagInput.val()).charAt(0) == '"' && + $.trim(that.tagInput.val()).charAt($.trim(that.tagInput.val()).length - 1) == '"' && + $.trim(that.tagInput.val()).length - 1 !== 0 ) ) ) ) { - event.preventDefault(); + // Enter submits the form if there's no text in the input. + if (!(event.which === $.ui.keyCode.ENTER && that.tagInput.val() === '')) { + event.preventDefault(); + } + that.createTag(that._cleanedInput()); // The autocomplete doesn't close automatically when TAB is pressed. // So let's ensure that it closes. - that._tagInput.autocomplete('close'); + that.tagInput.autocomplete('close'); } }).blur(function(e){ - // Create a tag when the element loses focus (unless it's empty). - that.createTag(that._cleanedInput()); + // Create a tag when the element loses focus. + // If autocomplete is enabled and suggestion was clicked, don't add it. + if (!that.tagInput.data('autocomplete-open')) { + that.createTag(that._cleanedInput()); + } }); - // Autocomplete. - if (this.options.availableTags || this.options.tagSource) { - this._tagInput.autocomplete({ - source: this.options.tagSource, + if (this.options.availableTags || this.options.tagSource || this.options.autocomplete.source) { + var autocompleteOptions = { select: function(event, ui) { - // Delete the last tag if we autocomplete something despite the input being empty - // This happens because the input's blur event causes the tag to be created when - // the user clicks an autocomplete item. - // The only artifact of this is that while the user holds down the mouse button - // on the selected autocomplete item, a tag is shown with the pre-autocompleted text, - // and is changed to the autocompleted text upon mouseup. - if (that._tagInput.val() === '') { - that.removeTag(that._lastTag(), false); - } that.createTag(ui.item.value); // Preventing the tag input to be updated with the chosen value. return false; } + }; + $.extend(autocompleteOptions, this.options.autocomplete); + + // tagSource is deprecated, but takes precedence here since autocomplete.source is set by default, + // while tagSource is left null by default. + autocompleteOptions.source = this.options.tagSource || autocompleteOptions.source; + + this.tagInput.autocomplete(autocompleteOptions).bind('autocompleteopen', function(event, ui) { + that.tagInput.data('autocomplete-open', true); + }).bind('autocompleteclose', function(event, ui) { + that.tagInput.data('autocomplete-open', false) }); } }, _cleanedInput: function() { // Returns the contents of the tag input, cleaned and ready to be passed to createTag - return $.trim(this._tagInput.val().replace(/^"(.*)"$/, '$1')); + return $.trim(this.tagInput.val().replace(/^"(.*)"$/, '$1')); }, _lastTag: function() { - return this.tagList.children('.tagit-choice:last'); + return this.tagList.find('.tagit-choice:last:not(.removed)'); + }, + + _tags: function() { + return this.tagList.find('.tagit-choice:not(.removed)'); }, assignedTags: function() { @@ -254,7 +322,7 @@ tags = []; } } else { - this.tagList.children('.tagit-choice').each(function() { + this._tags().each(function() { tags.push(that.tagLabel(this)); }); } @@ -263,7 +331,7 @@ _updateSingleTagsField: function(tags) { // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter - $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter)); + $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter)).trigger('change'); }, _subtractArray: function(a1, a2) { @@ -279,22 +347,30 @@ tagLabel: function(tag) { // Returns the tag's string label. if (this.options.singleField) { - return $(tag).children('.tagit-label').text(); + return $(tag).find('.tagit-label:first').text(); } else { - return $(tag).children('input').val(); + return $(tag).find('input:first').val(); } }, - _isNew: function(value) { + _showAutocomplete: function() { + this.tagInput.autocomplete('search', ''); + }, + + _findTagByLabel: function(name) { var that = this; - var isNew = true; - this.tagList.children('.tagit-choice').each(function(i) { - if (that._formatStr(value) == that._formatStr(that.tagLabel(this))) { - isNew = false; + var tag = null; + this._tags().each(function(i) { + if (that._formatStr(name) == that._formatStr(that.tagLabel(this))) { + tag = $(this); return false; } }); - return isNew; + return tag; + }, + + _isNew: function(name) { + return !this._findTagByLabel(name); }, _formatStr: function(str) { @@ -304,12 +380,38 @@ return $.trim(str.toLowerCase()); }, - createTag: function(value, additionalClass) { + _effectExists: function(name) { + return Boolean($.effects && ($.effects[name] || ($.effects.effect && $.effects.effect[name]))); + }, + + createTag: function(value, additionalClass, duringInitialization) { var that = this; - // Automatically trims the value of leading and trailing whitespace. + value = $.trim(value); - if (!this._isNew(value) || value === '') { + if(this.options.preprocessTag) { + value = this.options.preprocessTag(value); + } + + if (value === '') { + return false; + } + + if (!this.options.allowDuplicates && !this._isNew(value)) { + var existingTag = this._findTagByLabel(value); + if (this._trigger('onTagExists', null, { + existingTag: existingTag, + duringInitialization: duringInitialization + }) !== false) { + if (this._effectExists('highlight')) { + existingTag.effect('highlight'); + } + } + return false; + } + + if (this.options.tagLimit && this._tags().length >= this.options.tagLimit) { + this._trigger('onTagLimitExceeded', null, {duringInitialization: duringInitialization}); return false; } @@ -321,44 +423,74 @@ .addClass(additionalClass) .append(label); - // Button for removing the tag. - var removeTagIcon = $('<span></span>') - .addClass('ui-icon ui-icon-close'); - var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X - .addClass('tagit-close') - .append(removeTagIcon) - .click(function(e) { - // Removes a tag when the little 'x' is clicked. - that.removeTag(tag); - }); - tag.append(removeTag); + if (this.options.readOnly){ + tag.addClass('tagit-choice-read-only'); + } else { + tag.addClass('tagit-choice-editable'); + // Button for removing the tag. + var removeTagIcon = $('<span></span>') + .addClass('ui-icon ui-icon-close'); + var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X + .addClass('tagit-close') + .append(removeTagIcon) + .click(function(e) { + // Removes a tag when the little 'x' is clicked. + that.removeTag(tag); + }); + tag.append(removeTag); + } // Unless options.singleField is set, each tag has a hidden input field inline. + if (!this.options.singleField) { + var escapedValue = label.html(); + tag.append('<input type="hidden" style="display:none;" value="' + escapedValue + '" name="' + this.options.fieldName + '" />'); + } + + if (this._trigger('beforeTagAdded', null, { + tag: tag, + tagLabel: this.tagLabel(tag), + duringInitialization: duringInitialization + }) === false) { + return; + } + if (this.options.singleField) { var tags = this.assignedTags(); tags.push(value); this._updateSingleTagsField(tags); - } else { - var escapedValue = label.html(); - tag.append('<input type="hidden" style="display:none;" value="' + escapedValue + '" name="' + this.options.itemName + '[' + this.options.fieldName + '][]" />'); } + // DEPRECATED. this._trigger('onTagAdded', null, tag); - // Cleaning the input. - this._tagInput.val(''); + this.tagInput.val(''); - // insert tag - this._tagInput.parent().before(tag); + // Insert tag. + this.tagInput.parent().before(tag); + + this._trigger('afterTagAdded', null, { + tag: tag, + tagLabel: this.tagLabel(tag), + duringInitialization: duringInitialization + }); + + if (this.options.showAutocompleteOnFocus && !duringInitialization) { + setTimeout(function () { that._showAutocomplete(); }, 0); + } }, - + removeTag: function(tag, animate) { - animate = animate || this.options.animate; + animate = typeof animate === 'undefined' ? this.options.animate : animate; tag = $(tag); + // DEPRECATED. this._trigger('onTagRemoved', null, tag); + if (this._trigger('beforeTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}) === false) { + return; + } + if (this.options.singleField) { var tags = this.assignedTags(); var removedTagLabel = this.tagLabel(tag); @@ -367,26 +499,40 @@ }); this._updateSingleTagsField(tags); } - // Animate the removal. + if (animate) { - tag.fadeOut('fast').hide('blind', {direction: 'horizontal'}, 'fast', function(){ + tag.addClass('removed'); // Excludes this tag from _tags. + var hide_args = this._effectExists('blind') ? ['blind', {direction: 'horizontal'}, 'fast'] : ['fast']; + + var thisTag = this; + hide_args.push(function() { tag.remove(); - }).dequeue(); + thisTag._trigger('afterTagRemoved', null, {tag: tag, tagLabel: thisTag.tagLabel(tag)}); + }); + + tag.fadeOut('fast').hide.apply(tag, hide_args).dequeue(); } else { tag.remove(); + this._trigger('afterTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}); } + + }, + + removeTagByLabel: function(tagLabel, animate) { + var toRemove = this._findTagByLabel(tagLabel); + if (!toRemove) { + throw "No such tag exists with the name '" + tagLabel + "'"; + } + this.removeTag(toRemove, animate); }, removeAll: function() { // Removes all tags. var that = this; - this.tagList.children('.tagit-choice').each(function(index, tag) { + this._tags().each(function(index, tag) { that.removeTag(tag, false); }); } }); - -})(jQuery); - - +})(jQuery); \ No newline at end of file