autocomplete_deluxe.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. /**
  2. * @file:
  3. * Converts textfield to a autocomplete deluxe widget.
  4. */
  5. (function($) {
  6. Drupal.autocomplete_deluxe = Drupal.autocomplete_deluxe || {};
  7. Drupal.behaviors.autocomplete_deluxe = {
  8. attach: function(context) {
  9. var autocomplete_settings = Drupal.settings.autocomplete_deluxe;
  10. $('input.autocomplete-deluxe-form').once( function() {
  11. if (autocomplete_settings[$(this).attr('id')].multiple === true) {
  12. new Drupal.autocomplete_deluxe.MultipleWidget(this, autocomplete_settings[$(this).attr('id')]);
  13. } else {
  14. new Drupal.autocomplete_deluxe.SingleWidget(autocomplete_settings[$(this).attr('id')]);
  15. }
  16. });
  17. }
  18. };
  19. /**
  20. * Autogrow plugin which auto resizes the input of the multiple value.
  21. *
  22. * http://stackoverflow.com/questions/931207/is-there-a-jquery-autogrow-plugin-for-text-fields
  23. *
  24. */
  25. $.fn.autoGrowInput = function(o) {
  26. o = $.extend({
  27. maxWidth: 1000,
  28. minWidth: 0,
  29. comfortZone: 70
  30. }, o);
  31. this.filter('input:text').each(function(){
  32. var minWidth = o.minWidth || $(this).width(),
  33. val = '',
  34. input = $(this),
  35. testSubject = $('<tester/>').css({
  36. position: 'absolute',
  37. top: -9999,
  38. left: -9999,
  39. width: 'auto',
  40. fontSize: input.css('fontSize'),
  41. fontFamily: input.css('fontFamily'),
  42. fontWeight: input.css('fontWeight'),
  43. letterSpacing: input.css('letterSpacing'),
  44. whiteSpace: 'nowrap'
  45. }),
  46. check = function() {
  47. if (val === (val = input.val())) {return;}
  48. // Enter new content into testSubject
  49. var escaped = val.replace(/&/g, '&amp;').replace(/\s/g,'&nbsp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  50. testSubject.html(escaped);
  51. // Calculate new width + whether to change
  52. var testerWidth = testSubject.width(),
  53. newWidth = (testerWidth + o.comfortZone) >= minWidth ? testerWidth + o.comfortZone : minWidth,
  54. currentWidth = input.width(),
  55. isValidWidthChange = (newWidth < currentWidth && newWidth >= minWidth)
  56. || (newWidth > minWidth && newWidth < o.maxWidth);
  57. // Animate width
  58. if (isValidWidthChange) {
  59. input.width(newWidth);
  60. }
  61. };
  62. testSubject.insertAfter(input);
  63. $(this).bind('keyup keydown blur update', check);
  64. });
  65. return this;
  66. };
  67. Drupal.autocomplete_deluxe.empty = {label: '- ' + Drupal.t('None') + ' -', value: "" };
  68. /**
  69. * EscapeRegex function from jquery autocomplete, is not included in drupal.
  70. */
  71. Drupal.autocomplete_deluxe.escapeRegex = function(value) {
  72. return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/gi, "\\$&");
  73. };
  74. /**
  75. * Filter function from jquery autocomplete, is not included in drupal.
  76. */
  77. Drupal.autocomplete_deluxe.filter = function(array, term) {
  78. var matcher = new RegExp(Drupal.autocomplete_deluxe.escapeRegex(term), "i");
  79. return $.grep(array, function(value) {
  80. return matcher.test(value.label || value.value || value);
  81. });
  82. };
  83. Drupal.autocomplete_deluxe.Widget = function() {
  84. };
  85. Drupal.autocomplete_deluxe.Widget.prototype.uri = null;
  86. /**
  87. * Allows widgets to filter terms.
  88. * @param term
  89. * A term that should be accepted or not.
  90. * @return {Boolean}
  91. * True if the term should be accepted.
  92. */
  93. Drupal.autocomplete_deluxe.Widget.prototype.acceptTerm = function(term) {
  94. return true;
  95. };
  96. Drupal.autocomplete_deluxe.Widget.prototype.init = function(settings) {
  97. if ($.browser.msie && $.browser.version === "6.0") {
  98. return;
  99. }
  100. this.id = settings.input_id;
  101. this.jqObject = $('#' + this.id);
  102. this.uri = settings.uri;
  103. this.multiple = settings.multiple;
  104. this.required = settings.required;
  105. this.limit = settings.limit;
  106. this.synonyms = typeof settings.use_synonyms == 'undefined' ? false : settings.use_synonyms;
  107. this.wrapper = '""';
  108. if (typeof settings.delimiter == 'undefined') {
  109. this.delimiter = true;
  110. } else {
  111. this.delimiter = settings.delimiter.charCodeAt(0);
  112. }
  113. this.items = {};
  114. var self = this;
  115. var parent = this.jqObject.parent();
  116. var parents_parent = this.jqObject.parent().parent();
  117. parents_parent.append(this.jqObject);
  118. parent.remove();
  119. parent = parents_parent;
  120. var generateValues = function(data, term) {
  121. var result = new Array();
  122. for (var terms in data) {
  123. if (self.acceptTerm(terms)) {
  124. result.push({
  125. label: data[terms],
  126. value: terms
  127. });
  128. }
  129. }
  130. if ($.isEmptyObject(result)) {
  131. result.push({
  132. label: Drupal.t("The term '@term' will be added.", {'@term' : term}),
  133. value: term,
  134. newTerm: true
  135. });
  136. }
  137. return result;
  138. };
  139. var cache = {}
  140. var lastXhr = null;
  141. this.source = function(request, response) {
  142. var term = request.term;
  143. if (term in cache) {
  144. response(generateValues(cache[term], term));
  145. return;
  146. }
  147. // Some server collapse two slashes if the term is empty, so insert at
  148. // least a whitespace. This whitespace will later on be trimmed in the
  149. // autocomplete callback.
  150. if (!term) {
  151. term = " ";
  152. }
  153. request.synonyms = self.synonyms;
  154. var url = settings.uri + '/' + term +'/' + self.limit;
  155. lastXhr = $.getJSON(url, request, function(data, status, xhr) {
  156. cache[term] = data;
  157. if (xhr === lastXhr) {
  158. response(generateValues(data, term));
  159. }
  160. });
  161. };
  162. this.jqObject.autocomplete({
  163. 'source' : this.source,
  164. 'minLength': settings.min_length
  165. });
  166. var jqObject = this.jqObject;
  167. var throbber = $('<div class="autocomplete-deluxe-throbber autocomplete-deluxe-closed">&nbsp;</div>').insertAfter(jqObject);
  168. this.jqObject.bind("autocompletesearch", function(event, ui) {
  169. throbber.removeClass('autocomplete-deluxe-closed');
  170. throbber.addClass('autocomplete-deluxe-open');
  171. });
  172. this.jqObject.bind("autocompleteopen", function(event, ui) {
  173. throbber.addClass('autocomplete-deluxe-closed');
  174. throbber.removeClass('autocomplete-deluxe-open');
  175. });
  176. // Monkey patch the _renderItem function jquery so we can highlight the
  177. // text, that we already entered.
  178. $.ui.autocomplete.prototype._renderItem = function( ul, item) {
  179. var t = item.label;
  180. if (this.term != "") {
  181. var escapedValue = Drupal.autocomplete_deluxe.escapeRegex( this.term );
  182. var re = new RegExp('()*""' + escapedValue + '""|' + escapedValue + '()*', 'gi');
  183. var t = item.label.replace(re,"<span class='autocomplete-deluxe-highlight-char'>$&</span>");
  184. }
  185. return $( "<li></li>" )
  186. .data( "item.autocomplete", item )
  187. .append( "<a>" + t + "</a>" )
  188. .appendTo( ul );
  189. };
  190. };
  191. Drupal.autocomplete_deluxe.Widget.prototype.generateValues = function(data) {
  192. var result = new Array();
  193. for (var index in data) {
  194. result.push(data[index]);
  195. }
  196. return result;
  197. };
  198. /**
  199. * Generates a single selecting widget.
  200. */
  201. Drupal.autocomplete_deluxe.SingleWidget = function(settings) {
  202. this.init(settings);
  203. this.setup();
  204. this.jqObject.addClass('autocomplete-deluxe-form-single');
  205. };
  206. Drupal.autocomplete_deluxe.SingleWidget.prototype = new Drupal.autocomplete_deluxe.Widget();
  207. Drupal.autocomplete_deluxe.SingleWidget.prototype.setup = function() {
  208. var jqObject = this.jqObject;
  209. var parent = jqObject.parent();
  210. parent.mousedown(function() {
  211. if (parent.hasClass('autocomplete-deluxe-single-open')) {
  212. jqObject.autocomplete('close');
  213. } else {
  214. jqObject.autocomplete('search', '');
  215. }
  216. });
  217. };
  218. /**
  219. * Creates a multiple selecting widget.
  220. */
  221. Drupal.autocomplete_deluxe.MultipleWidget = function(input, settings) {
  222. this.init(settings);
  223. this.setup();
  224. };
  225. Drupal.autocomplete_deluxe.MultipleWidget.prototype = new Drupal.autocomplete_deluxe.Widget();
  226. Drupal.autocomplete_deluxe.MultipleWidget.prototype.items = new Object();
  227. Drupal.autocomplete_deluxe.MultipleWidget.prototype.acceptTerm = function(term) {
  228. // Accept only terms, that are not in our items list.
  229. return !(term in this.items);
  230. };
  231. Drupal.autocomplete_deluxe.MultipleWidget.Item = function (widget, item) {
  232. if (item.newTerm === true) {
  233. item.label = item.value;
  234. }
  235. this.value = item.value;
  236. this.element = $('<span class="autocomplete-deluxe-item">' + item.label + '</span>');
  237. this.widget = widget;
  238. this.item = item;
  239. var self = this;
  240. var close = $('<a class="autocomplete-deluxe-item-delete" href="javascript:void(0)"></a>').appendTo(this.element);
  241. // Use single quotes because of the double quote encoded stuff.
  242. var input = $('<input type="hidden" value=\'' + this.value + '\'/>').appendTo(this.element);
  243. close.mousedown(function() {
  244. self.remove(item);
  245. });
  246. };
  247. Drupal.autocomplete_deluxe.MultipleWidget.Item.prototype.remove = function() {
  248. this.element.remove();
  249. var values = this.widget.valueForm.val();
  250. var escapedValue = Drupal.autocomplete_deluxe.escapeRegex( this.item.value );
  251. var regex = new RegExp('()*""' + escapedValue + '""|' + escapedValue + '()*', 'gi');
  252. this.widget.valueForm.val(values.replace(regex, ''));
  253. delete this.widget.items[this.value];
  254. };
  255. Drupal.autocomplete_deluxe.MultipleWidget.prototype.setup = function() {
  256. var jqObject = this.jqObject;
  257. var parent = jqObject.parent();
  258. var value_container = jqObject.parent().parent().children('.autocomplete-deluxe-value-container');
  259. var value_input = value_container.children().children();
  260. var items = this.items;
  261. var self = this;
  262. this.valueForm = value_input;
  263. // Override the resize function, so that the suggestion list doesn't resizes
  264. // all the time.
  265. jqObject.data("autocomplete")._resizeMenu = function() {};
  266. jqObject.show();
  267. value_container.hide();
  268. // Add the default values to the box.
  269. var default_values = value_input.val();
  270. default_values = $.trim(default_values);
  271. default_values = default_values.substr(2, default_values.length-4);
  272. default_values = default_values.split('"" ""');
  273. for (var index in default_values) {
  274. var value = default_values[index];
  275. if (value != '') {
  276. // If a terms is encoded in double quotes, then the label should have
  277. // no double quotes.
  278. var label = value.match(/["][\w|\s|\D|]*["]/gi) !== null ? value.substr(1, value.length-2) : value;
  279. var item = {
  280. label : label,
  281. value : value
  282. };
  283. var item = new Drupal.autocomplete_deluxe.MultipleWidget.Item(self, item);
  284. item.element.insertBefore(jqObject);
  285. items[item.value] = item;
  286. }
  287. }
  288. jqObject.addClass('autocomplete-deluxe-multiple');
  289. parent.addClass('autocomplete-deluxe-multiple');
  290. // Adds a value to the list.
  291. this.addValue = function(ui_item) {
  292. var item = new Drupal.autocomplete_deluxe.MultipleWidget.Item(self, ui_item);
  293. item.element.insertBefore(jqObject);
  294. items[ui_item.value] = item;
  295. var new_value = ' ' + self.wrapper + ui_item.value + self.wrapper;
  296. var values = value_input.val();
  297. value_input.val(values + new_value);
  298. jqObject.val('');
  299. };
  300. parent.mouseup(function() {
  301. jqObject.autocomplete('search', '');
  302. jqObject.focus();
  303. });
  304. jqObject.bind("autocompleteselect", function(event, ui) {
  305. self.addValue(ui.item);
  306. jqObject.width(25);
  307. // Return false to prevent setting the last term as value for the jqObject.
  308. return false;
  309. });
  310. jqObject.bind("autocompletechange", function(event, ui) {
  311. jqObject.val('');
  312. });
  313. jqObject.blur(function() {
  314. var last_element = jqObject.parent().children('.autocomplete-deluxe-item').last();
  315. last_element.removeClass('autocomplete-deluxe-item-focus');
  316. });
  317. var clear = false;
  318. jqObject.keypress(function (event) {
  319. var value = jqObject.val();
  320. // If a comma was entered and there is none or more then one comma,or the
  321. // enter key was entered, then enter the new term.
  322. if ((event.which == self.delimiter && (value.split('"').length - 1) != 1) || (event.which == 13 && jqObject.val() != "")) {
  323. value = value.substr(0, value.length);
  324. if (typeof self.items[value] == 'undefined' && value != '') {
  325. var ui_item = {
  326. label: value,
  327. value: value
  328. };
  329. self.addValue(ui_item);
  330. }
  331. clear = true;
  332. if (event.which == 13) {
  333. return false;
  334. }
  335. }
  336. // If the Backspace key was hit and the input is empty
  337. if (event.which == 8 && value == '') {
  338. var last_element = jqObject.parent().children('.autocomplete-deluxe-item').last();
  339. // then mark the last item for deletion or deleted it if already marked.
  340. if (last_element.hasClass('autocomplete-deluxe-item-focus')) {
  341. var value = last_element.children('input').val();
  342. self.items[value].remove(self.items[value]);
  343. jqObject.autocomplete('search', '');
  344. } else {
  345. last_element.addClass('autocomplete-deluxe-item-focus');
  346. }
  347. } else {
  348. // Remove the focus class if any other key was hit.
  349. var last_element = jqObject.parent().children('.autocomplete-deluxe-item').last();
  350. last_element.removeClass('autocomplete-deluxe-item-focus');
  351. }
  352. });
  353. jqObject.autoGrowInput({
  354. comfortZone: 50,
  355. minWidth: 10,
  356. maxWidth: 460
  357. });
  358. jqObject.keyup(function (event) {
  359. if (clear) {
  360. // Trigger the search, so it display the values for an empty string.
  361. jqObject.autocomplete('search', '');
  362. jqObject.val('');
  363. clear = false;
  364. // Return false to prevent entering the last character.
  365. return false;
  366. }
  367. });
  368. };
  369. })(jQuery);