/static/js/jquery.tagit.js

https://bitbucket.org/cfarrell1980/contractualsugar · JavaScript · 381 lines · 292 code · 18 blank · 71 comment · 26 complexity · 9611ee9a8b0d9a3dfdb46df2098330f6 MD5 · raw file

  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. $.widget('ui.tagit', {
  29. options: {
  30. itemName : 'item',
  31. fieldName : 'tags',
  32. availableTags : [],
  33. tagSource : null,
  34. removeConfirmation: false,
  35. caseSensitive : true,
  36. // When enabled, quotes are not neccesary
  37. // for inputting multi-word tags.
  38. allowSpaces: false,
  39. // The below options are for using a single field instead of several
  40. // for our form values.
  41. //
  42. // When enabled, will use a single hidden field for the form,
  43. // rather than one per tag. It will delimit tags in the field
  44. // with singleFieldDelimiter.
  45. //
  46. // The easiest way to use singleField is to just instantiate tag-it
  47. // on an INPUT element, in which case singleField is automatically
  48. // set to true, and singleFieldNode is set to that element. This
  49. // way, you don't need to fiddle with these options.
  50. singleField: false,
  51. singleFieldDelimiter: ',',
  52. // Set this to an input DOM node to use an existing form field.
  53. // Any text in it will be erased on init. But it will be
  54. // populated with the text of tags as they are created,
  55. // delimited by singleFieldDelimiter.
  56. //
  57. // If this is not set, we create an input node for it,
  58. // with the name given in settings.fieldName,
  59. // ignoring settings.itemName.
  60. singleFieldNode: null,
  61. // Optionally set a tabindex attribute on the input that gets
  62. // created for tag-it.
  63. tabIndex: null,
  64. // Event callbacks.
  65. onTagAdded : null,
  66. onTagRemoved: null,
  67. onTagClicked: null
  68. },
  69. _create: function() {
  70. // for handling static scoping inside callbacks
  71. var that = this;
  72. // There are 2 kinds of DOM nodes this widget can be instantiated on:
  73. // 1. UL, OL, or some element containing either of these.
  74. // 2. INPUT, in which case 'singleField' is overridden to true,
  75. // a UL is created and the INPUT is hidden.
  76. if (this.element.is('input')) {
  77. this.tagList = $('<ul></ul>').insertAfter(this.element);
  78. this.options.singleField = true;
  79. this.options.singleFieldNode = this.element;
  80. this.element.css('display', 'none');
  81. } else {
  82. this.tagList = this.element.find('ul, ol').andSelf().last();
  83. }
  84. this._tagInput = $('<input type="text">').addClass('ui-widget-content');
  85. if (this.options.tabIndex) {
  86. this._tagInput.attr('tabindex', this.options.tabIndex);
  87. }
  88. this.options.tagSource = this.options.tagSource || function(search, showChoices) {
  89. var filter = search.term.toLowerCase();
  90. var choices = $.grep(that.options.availableTags, function(element) {
  91. // Only match autocomplete options that begin with the search term.
  92. // (Case insensitive.)
  93. return (element.toLowerCase().indexOf(filter) === 0);
  94. });
  95. showChoices(that._subtractArray(choices, that.assignedTags()));
  96. };
  97. this.tagList
  98. .addClass('tagit')
  99. .addClass('ui-widget ui-widget-content ui-corner-all')
  100. // Create the input field.
  101. .append($('<li class="tagit-new"></li>').append(this._tagInput))
  102. .click(function(e) {
  103. var target = $(e.target);
  104. if (target.hasClass('tagit-label')) {
  105. that._trigger('onTagClicked', e, target.closest('.tagit-choice'));
  106. } else {
  107. // Sets the focus() to the input field, if the user
  108. // clicks anywhere inside the UL. This is needed
  109. // because the input field needs to be of a small size.
  110. that._tagInput.focus();
  111. }
  112. });
  113. // Add existing tags from the list, if any.
  114. this.tagList.children('li').each(function() {
  115. if (!$(this).hasClass('tagit-new')) {
  116. that.createTag($(this).html(), $(this).attr('class'));
  117. $(this).remove();
  118. }
  119. });
  120. // Single field support.
  121. if (this.options.singleField) {
  122. if (this.options.singleFieldNode) {
  123. // Add existing tags from the input field.
  124. var node = $(this.options.singleFieldNode);
  125. var tags = node.val().split(this.options.singleFieldDelimiter);
  126. node.val('');
  127. $.each(tags, function(index, tag) {
  128. that.createTag(tag);
  129. });
  130. } else {
  131. // Create our single field input after our list.
  132. this.options.singleFieldNode = this.tagList.after('<input type="hidden" style="display:none;" value="" name="' + this.options.fieldName + '">');
  133. }
  134. }
  135. // Events.
  136. this._tagInput
  137. .keydown(function(event) {
  138. // Backspace is not detected within a keypress, so it must use keydown.
  139. if (event.which == $.ui.keyCode.BACKSPACE && that._tagInput.val() === '') {
  140. var tag = that._lastTag();
  141. if (!that.options.removeConfirmation || tag.hasClass('remove')) {
  142. // When backspace is pressed, the last tag is deleted.
  143. that.removeTag(tag);
  144. } else if (that.options.removeConfirmation) {
  145. tag.addClass('remove ui-state-highlight');
  146. }
  147. } else if (that.options.removeConfirmation) {
  148. that._lastTag().removeClass('remove ui-state-highlight');
  149. }
  150. // Comma/Space/Enter are all valid delimiters for new tags,
  151. // except when there is an open quote or if setting allowSpaces = true.
  152. // Tab will also create a tag, unless the tag input is empty, in which case it isn't caught.
  153. if (
  154. event.which == $.ui.keyCode.COMMA ||
  155. event.which == $.ui.keyCode.ENTER ||
  156. (
  157. event.which == $.ui.keyCode.TAB &&
  158. that._tagInput.val() !== ''
  159. ) ||
  160. (
  161. event.which == $.ui.keyCode.SPACE &&
  162. that.options.allowSpaces !== true &&
  163. (
  164. $.trim(that._tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' ||
  165. (
  166. $.trim(that._tagInput.val()).charAt(0) == '"' &&
  167. $.trim(that._tagInput.val()).charAt($.trim(that._tagInput.val()).length - 1) == '"' &&
  168. $.trim(that._tagInput.val()).length - 1 !== 0
  169. )
  170. )
  171. )
  172. ) {
  173. event.preventDefault();
  174. that.createTag(that._cleanedInput());
  175. // The autocomplete doesn't close automatically when TAB is pressed.
  176. // So let's ensure that it closes.
  177. that._tagInput.autocomplete('close');
  178. }
  179. }).blur(function(e){
  180. // Create a tag when the element loses focus (unless it's empty).
  181. that.createTag(that._cleanedInput());
  182. });
  183. // Autocomplete.
  184. if (this.options.availableTags || this.options.tagSource) {
  185. this._tagInput.autocomplete({
  186. source: this.options.tagSource,
  187. select: function(event, ui) {
  188. // Delete the last tag if we autocomplete something despite the input being empty
  189. // This happens because the input's blur event causes the tag to be created when
  190. // the user clicks an autocomplete item.
  191. // The only artifact of this is that while the user holds down the mouse button
  192. // on the selected autocomplete item, a tag is shown with the pre-autocompleted text,
  193. // and is changed to the autocompleted text upon mouseup.
  194. if (that._tagInput.val() === '') {
  195. that.removeTag(that._lastTag(), false);
  196. }
  197. that.createTag(ui.item.value);
  198. // Preventing the tag input to be updated with the chosen value.
  199. return false;
  200. }
  201. });
  202. }
  203. },
  204. _cleanedInput: function() {
  205. // Returns the contents of the tag input, cleaned and ready to be passed to createTag
  206. return $.trim(this._tagInput.val().replace(/^"(.*)"$/, '$1'));
  207. },
  208. _lastTag: function() {
  209. return this.tagList.children('.tagit-choice:last');
  210. },
  211. assignedTags: function() {
  212. // Returns an array of tag string values
  213. var that = this;
  214. var tags = [];
  215. if (this.options.singleField) {
  216. tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter);
  217. if (tags[0] === '') {
  218. tags = [];
  219. }
  220. } else {
  221. this.tagList.children('.tagit-choice').each(function() {
  222. tags.push(that.tagLabel(this));
  223. });
  224. }
  225. return tags;
  226. },
  227. _updateSingleTagsField: function(tags) {
  228. // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter
  229. $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter));
  230. },
  231. _subtractArray: function(a1, a2) {
  232. var result = [];
  233. for (var i = 0; i < a1.length; i++) {
  234. if ($.inArray(a1[i], a2) == -1) {
  235. result.push(a1[i]);
  236. }
  237. }
  238. return result;
  239. },
  240. tagLabel: function(tag) {
  241. // Returns the tag's string label.
  242. if (this.options.singleField) {
  243. return $(tag).children('.tagit-label').text();
  244. } else {
  245. return $(tag).children('input').val();
  246. }
  247. },
  248. _isNew: function(value) {
  249. var that = this;
  250. var isNew = true;
  251. this.tagList.children('.tagit-choice').each(function(i) {
  252. if (that._formatStr(value) == that._formatStr(that.tagLabel(this))) {
  253. isNew = false;
  254. return;
  255. }
  256. });
  257. return isNew;
  258. },
  259. _formatStr: function(str) {
  260. if (this.options.caseSensitive) {
  261. return str;
  262. }
  263. return $.trim(str.toLowerCase());
  264. },
  265. createTag: function(value, additionalClass) {
  266. that = this;
  267. // Automatically trims the value of leading and trailing whitespace.
  268. value = $.trim(value);
  269. if (!this._isNew(value) || value === '') {
  270. return false;
  271. }
  272. var label = $(this.options.onTagClicked ? '<a class="tagit-label"></a>' : '<span class="tagit-label"></span>').text(value);
  273. // Create tag.
  274. var tag = $('<li></li>')
  275. .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all')
  276. .addClass(additionalClass)
  277. .append(label);
  278. // Button for removing the tag.
  279. var removeTagIcon = $('<span></span>')
  280. .addClass('ui-icon ui-icon-close');
  281. var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X
  282. .addClass('close')
  283. .append(removeTagIcon)
  284. .click(function(e) {
  285. // Removes a tag when the little 'x' is clicked.
  286. that.removeTag(tag);
  287. });
  288. tag.append(removeTag);
  289. // Unless options.singleField is set, each tag has a hidden input field inline.
  290. if (this.options.singleField) {
  291. var tags = this.assignedTags();
  292. tags.push(value);
  293. this._updateSingleTagsField(tags);
  294. } else {
  295. var escapedValue = label.html();
  296. tag.append('<input type="hidden" style="display:none;" value="' + escapedValue + '" name="' + this.options.itemName + '[' + this.options.fieldName + '][]">');
  297. }
  298. this._trigger('onTagAdded', null, tag);
  299. // Cleaning the input.
  300. this._tagInput.val('');
  301. // insert tag
  302. this._tagInput.parent().before(tag);
  303. },
  304. removeTag: function(tag, animate) {
  305. if (typeof animate === 'undefined') { animate = true; }
  306. tag = $(tag);
  307. this._trigger('onTagRemoved', null, tag);
  308. if (this.options.singleField) {
  309. var tags = this.assignedTags();
  310. var removedTagLabel = this.tagLabel(tag);
  311. tags = $.grep(tags, function(el){
  312. return el != removedTagLabel;
  313. });
  314. this._updateSingleTagsField(tags);
  315. }
  316. // Animate the removal.
  317. if (animate) {
  318. tag.fadeOut('fast').hide('blind', {direction: 'horizontal'}, 'fast', function(){
  319. tag.remove();
  320. }).dequeue();
  321. } else {
  322. tag.remove();
  323. }
  324. },
  325. removeAll: function() {
  326. // Removes all tags. Takes an optional `animate` argument.
  327. var that = this;
  328. this.tagList.children('.tagit-choice').each(function(index, tag) {
  329. that.removeTag(tag, false);
  330. });
  331. }
  332. });
  333. })(jQuery);