diff -r 78f71aa0a477 -r 547b3ddedf7f integration/lib/tag-it/js/tag-it.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/integration/lib/tag-it/js/tag-it.js Thu May 16 12:58:48 2013 +0200 @@ -0,0 +1,541 @@ +/* +* jQuery UI Tag-it! +* +* @version v2.0 (06/2011) +* +* Copyright 2011, Levy Carneiro Jr. +* Released under the MIT license. +* http://aehlke.github.com/tag-it/LICENSE +* +* Homepage: +* http://aehlke.github.com/tag-it/ +* +* Authors: +* Levy Carneiro Jr. +* Martin Rehfeld +* Tobias Schmidt +* Skylar Challand +* Alex Ehlke +* +* Maintainer: +* Alex Ehlke - Twitter: @aehlke +* +* Dependencies: +* jQuery v1.4+ +* jQuery UI v1.8+ +*/ +(function($) { + + $.widget('ui.tagit', { + options: { + 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 : [], + + // 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, + + // The below options are for using a single field instead of several + // for our form values. + // + // When enabled, will use a single hidden field for the form, + // rather than one per tag. It will delimit tags in the field + // with singleFieldDelimiter. + // + // 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 + // 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. + // Any text in it will be erased on init. But it will be + // populated with the text of tags as they are created, + // delimited by singleFieldDelimiter. + // + // If this is not set, we create an input node for it, + // 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, + + 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, + // `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; + + // There are 2 kinds of DOM nodes this widget can be instantiated on: + // 1. UL, OL, or some element containing either of these. + // 2. INPUT, in which case 'singleField' is overridden to true, + // a UL is created and the INPUT is hidden. + if (this.element.is('input')) { + this.tagList = $('').insertAfter(this.element); + this.options.singleField = true; + this.options.singleFieldNode = this.element; + this.element.css('display', 'none'); + } else { + this.tagList = this.element.find('ul, ol').andSelf().last(); + } + + this.tagInput = $('').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); + } + + 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); + }); + if (!this.options.allowDuplicates) { + choices = this._subtractArray(choices, this.assignedTags()); + } + showChoices(choices); + }; + } + + if (this.options.showAutocompleteOnFocus) { + this.tagInput.focus(function(event, ui) { + that._showAutocomplete(); + }); + + 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); + } + + this.tagList + .addClass('tagit') + .addClass('ui-widget ui-widget-content ui-corner-all') + // Create the input field. + .append($('
  • ').append(this.tagInput)) + .click(function(e) { + var target = $(e.target); + if (target.hasClass('tagit-label')) { + 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(); + } + }); + + // Single field support. + var addedExistingFromSingleFieldNode = false; + if (this.options.singleField) { + if (this.options.singleFieldNode) { + // Add existing tags from the input field. + var node = $(this.options.singleFieldNode); + var tags = node.val().split(this.options.singleFieldDelimiter); + node.val(''); + $.each(tags, function(index, tag) { + that.createTag(tag, null, true); + addedExistingFromSingleFieldNode = true; + }); + } else { + // Create our single field input after our list. + this.options.singleFieldNode = $(''); + 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 + .keydown(function(event) { + // Backspace is not detected within a keypress, so it must use keydown. + 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. + that.removeTag(tag); + } else if (that.options.removeConfirmation) { + tag.addClass('remove ui-state-highlight'); + } + } else if (that.options.removeConfirmation) { + that._lastTag().removeClass('remove ui-state-highlight'); + } + + // 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. + if ( + event.which === $.ui.keyCode.COMMA || + event.which === $.ui.keyCode.ENTER || + ( + event.which == $.ui.keyCode.TAB && + that.tagInput.val() !== '' + ) || + ( + event.which == $.ui.keyCode.SPACE && + that.options.allowSpaces !== true && + ( + $.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 + ) + ) + ) + ) { + // Enter submits the form if there's no text in the input. + if (!(event.which === $.ui.keyCode.ENTER && that.tagInput.val() === '')) { + event.preventDefault(); + } + + // Autocomplete will create its own tag from a selection and close automatically. + if (!that.tagInput.data('autocomplete-open')) { + that.createTag(that._cleanedInput()); + } + } + }).blur(function(e){ + // 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.options.autocomplete.source) { + var autocompleteOptions = { + select: function(event, ui) { + 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')); + }, + + _lastTag: function() { + return this.tagList.find('.tagit-choice:last:not(.removed)'); + }, + + _tags: function() { + return this.tagList.find('.tagit-choice:not(.removed)'); + }, + + assignedTags: function() { + // Returns an array of tag string values + var that = this; + var tags = []; + if (this.options.singleField) { + tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter); + if (tags[0] === '') { + tags = []; + } + } else { + this._tags().each(function() { + tags.push(that.tagLabel(this)); + }); + } + return tags; + }, + + _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)).trigger('change'); + }, + + _subtractArray: function(a1, a2) { + var result = []; + for (var i = 0; i < a1.length; i++) { + if ($.inArray(a1[i], a2) == -1) { + result.push(a1[i]); + } + } + return result; + }, + + tagLabel: function(tag) { + // Returns the tag's string label. + if (this.options.singleField) { + return $(tag).find('.tagit-label:first').text(); + } else { + return $(tag).find('input:first').val(); + } + }, + + _showAutocomplete: function() { + this.tagInput.autocomplete('search', ''); + }, + + _findTagByLabel: function(name) { + var that = this; + var tag = null; + this._tags().each(function(i) { + if (that._formatStr(name) == that._formatStr(that.tagLabel(this))) { + tag = $(this); + return false; + } + }); + return tag; + }, + + _isNew: function(name) { + return !this._findTagByLabel(name); + }, + + _formatStr: function(str) { + if (this.options.caseSensitive) { + return str; + } + return $.trim(str.toLowerCase()); + }, + + _effectExists: function(name) { + return Boolean($.effects && ($.effects[name] || ($.effects.effect && $.effects.effect[name]))); + }, + + createTag: function(value, additionalClass, duringInitialization) { + var that = this; + + value = $.trim(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; + } + + var label = $(this.options.onTagClicked ? '' : '').text(value); + + // Create tag. + var tag = $('
  • ') + .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all') + .addClass(additionalClass) + .append(label); + + if (this.options.readOnly){ + tag.addClass('tagit-choice-read-only'); + } else { + tag.addClass('tagit-choice-editable'); + // Button for removing the tag. + var removeTagIcon = $('') + .addClass('ui-icon ui-icon-close'); + var removeTag = $('\xd7') // \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(''); + } + + 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); + } + + // DEPRECATED. + this._trigger('onTagAdded', null, tag); + + this.tagInput.val(''); + + // 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 = 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); + tags = $.grep(tags, function(el){ + return el != removedTagLabel; + }); + this._updateSingleTagsField(tags); + } + + if (animate) { + 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(); + 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._tags().each(function(index, tag) { + that.removeTag(tag, false); + }); + } + + }); +})(jQuery); +