diff -r 76efa2333f31 -r 3ec2343f2b85 integration/lib/tag-it.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/integration/lib/tag-it.js Thu Nov 08 18:24:47 2012 +0100 @@ -0,0 +1,392 @@ +/* +* 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: { + itemName : 'item', + fieldName : 'tags', + availableTags : [], + tagSource : null, + removeConfirmation: false, + caseSensitive : true, + placeholderText : null, + + // When enabled, quotes are not neccesary + // 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. + // + // 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, + + 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, + // ignoring settings.itemName. + singleFieldNode: null, + + // Optionally set a tabindex attribute on the input that gets + // created for tag-it. + tabIndex: null, + + + // Event callbacks. + onTagAdded : null, + onTagRemoved: null, + onTagClicked: null + }, + + + _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.tabIndex) { + this._tagInput.attr('tabindex', this.options.tabIndex); + } + if (this.options.placeholderText) { + this._tagInput.attr('placeholder', this.options.placeholderText); + } + + 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); + }); + showChoices(this._subtractArray(choices, this.assignedTags())); + }; + + // Bind tagSource callback functions to this context. + 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')) { + that._trigger('onTagClicked', e, target.closest('.tagit-choice')); + } 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(); + } + }); + + // 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. + 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); + }); + } else { + // Create our single field input after our list. + this.options.singleFieldNode = this.tagList.after(''); + } + } + + // 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 + ) + ) + ) + ) { + 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'); + } + }).blur(function(e){ + // Create a tag when the element loses focus (unless it's empty). + that.createTag(that._cleanedInput()); + }); + + + // Autocomplete. + if (this.options.availableTags || this.options.tagSource) { + this._tagInput.autocomplete({ + source: this.options.tagSource, + 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; + } + }); + } + }, + + _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.children('.tagit-choice:last'); + }, + + 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.tagList.children('.tagit-choice').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)); + }, + + _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).children('.tagit-label').text(); + } else { + return $(tag).children('input').val(); + } + }, + + _isNew: function(value) { + 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; + return false; + } + }); + return isNew; + }, + + _formatStr: function(str) { + if (this.options.caseSensitive) { + return str; + } + return $.trim(str.toLowerCase()); + }, + + createTag: function(value, additionalClass) { + var that = this; + // Automatically trims the value of leading and trailing whitespace. + value = $.trim(value); + + if (!this._isNew(value) || value === '') { + 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); + + // 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 tags = this.assignedTags(); + tags.push(value); + this._updateSingleTagsField(tags); + } else { + var escapedValue = label.html(); + tag.append(''); + } + + this._trigger('onTagAdded', null, tag); + + // Cleaning the input. + this._tagInput.val(''); + + // insert tag + this._tagInput.parent().before(tag); + }, + + removeTag: function(tag, animate) { + animate = animate || this.options.animate; + + tag = $(tag); + + this._trigger('onTagRemoved', null, tag); + + if (this.options.singleField) { + var tags = this.assignedTags(); + var removedTagLabel = this.tagLabel(tag); + tags = $.grep(tags, function(el){ + return el != removedTagLabel; + }); + this._updateSingleTagsField(tags); + } + // Animate the removal. + if (animate) { + tag.fadeOut('fast').hide('blind', {direction: 'horizontal'}, 'fast', function(){ + tag.remove(); + }).dequeue(); + } else { + tag.remove(); + } + }, + + removeAll: function() { + // Removes all tags. + var that = this; + this.tagList.children('.tagit-choice').each(function(index, tag) { + that.removeTag(tag, false); + }); + } + + }); + +})(jQuery); + +