autocomplete_deluxe.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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. /**
  68. * Unescapes the given string.
  69. */
  70. Drupal.autocomplete_deluxe.unescape = function (input) {
  71. // Unescaping is done via a textarea, since the text inside of it is never
  72. // executed. This method also allows us to support older browsers like
  73. // IE 9 and below.
  74. var textArea = document.createElement('textarea');
  75. textArea.innerHTML = input;
  76. var decoded = textArea.value;
  77. if ('remove' in Element.prototype) {
  78. textArea.remove();
  79. }
  80. return decoded;
  81. };
  82. /**
  83. * If there is no result this label will be shown.
  84. * @type {{label: string, value: string}}
  85. */
  86. Drupal.autocomplete_deluxe.empty = {label: '- ' + Drupal.t('None') + ' -', value: "" };
  87. /**
  88. * EscapeRegex function from jquery autocomplete, is not included in drupal.
  89. */
  90. Drupal.autocomplete_deluxe.escapeRegex = function(value) {
  91. return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/gi, "\\$&");
  92. };
  93. /**
  94. * Filter function from jquery autocomplete, is not included in drupal.
  95. */
  96. Drupal.autocomplete_deluxe.filter = function(array, term) {
  97. var matcher = new RegExp(Drupal.autocomplete_deluxe.escapeRegex(term), "i");
  98. return $.grep(array, function(value) {
  99. return matcher.test(value.label || value.value || value);
  100. });
  101. };
  102. Drupal.autocomplete_deluxe.Widget = function() {
  103. };
  104. /**
  105. * Url for the callback.
  106. */
  107. Drupal.autocomplete_deluxe.Widget.prototype.uri = null;
  108. /**
  109. * Allows widgets to filter terms.
  110. * @param term
  111. * A term that should be accepted or not.
  112. * @return {Boolean}
  113. * True if the term should be accepted.
  114. */
  115. Drupal.autocomplete_deluxe.Widget.prototype.acceptTerm = function(term) {
  116. return true;
  117. };
  118. Drupal.autocomplete_deluxe.Widget.prototype.init = function(settings) {
  119. if(navigator.appVersion.indexOf("MSIE 6.") != -1) {
  120. return;
  121. }
  122. this.id = settings.input_id;
  123. this.jqObject = $('#' + this.id);
  124. this.uri = settings.uri;
  125. this.multiple = settings.multiple;
  126. this.required = settings.required;
  127. this.limit = settings.limit;
  128. this.synonyms = typeof settings.use_synonyms == 'undefined' ? false : settings.use_synonyms;
  129. this.not_found_message = typeof settings.use_synonyms == 'undefined' ? "The term '@term' will be added." : settings.not_found_message;
  130. this.wrapper = '""';
  131. if (typeof settings.delimiter == 'undefined') {
  132. this.delimiter = true;
  133. } else {
  134. this.delimiter = settings.delimiter.charCodeAt(0);
  135. }
  136. this.items = {};
  137. var self = this;
  138. var parent = this.jqObject.parent();
  139. var parents_parent = this.jqObject.parent().parent();
  140. parents_parent.append(this.jqObject);
  141. parent.remove();
  142. parent = parents_parent;
  143. var generateValues = function(data, term) {
  144. var result = new Array();
  145. for (var terms in data) {
  146. if (self.acceptTerm(terms)) {
  147. result.push({
  148. label: data[terms],
  149. value: terms
  150. });
  151. }
  152. }
  153. if ($.isEmptyObject(result)) {
  154. result.push({
  155. label: Drupal.t(self.not_found_message, {'@term' : term}),
  156. value: term,
  157. newTerm: true
  158. });
  159. }
  160. return result;
  161. };
  162. var cache = {};
  163. var lastXhr = null;
  164. this.source = function(request, response) {
  165. var term = request.term;
  166. if (term in cache) {
  167. response(generateValues(cache[term], term));
  168. return;
  169. }
  170. // Some server collapse two slashes if the term is empty, so insert at
  171. // least a whitespace. This whitespace will later on be trimmed in the
  172. // autocomplete callback.
  173. if (!term) {
  174. term = " ";
  175. }
  176. request.synonyms = self.synonyms;
  177. var url = settings.uri + '/' + term +'/' + self.limit;
  178. lastXhr = $.getJSON(url, request, function(data, status, xhr) {
  179. cache[term] = data;
  180. if (xhr === lastXhr) {
  181. response(generateValues(data, term));
  182. }
  183. });
  184. };
  185. this.jqObject.autocomplete({
  186. 'source' : this.source,
  187. 'minLength': settings.min_length
  188. });
  189. var jqObject = this.jqObject;
  190. var autocompleteDataKey = typeof(this.jqObject.data('autocomplete')) === 'object' ? 'item.autocomplete' : 'ui-autocomplete';
  191. var throbber = $('<div class="autocomplete-deluxe-throbber autocomplete-deluxe-closed">&nbsp;</div>').insertAfter(jqObject);
  192. this.jqObject.bind("autocompletesearch", function(event, ui) {
  193. throbber.removeClass('autocomplete-deluxe-closed');
  194. throbber.addClass('autocomplete-deluxe-open');
  195. });
  196. this.jqObject.bind("autocompleteopen", function(event, ui) {
  197. throbber.addClass('autocomplete-deluxe-closed');
  198. throbber.removeClass('autocomplete-deluxe-open');
  199. });
  200. // Monkey patch the _renderItem function jquery so we can highlight the
  201. // text, that we already entered.
  202. $.ui.autocomplete.prototype._renderItem = function( ul, item) {
  203. var t = item.label;
  204. if (this.term != "") {
  205. var escapedValue = Drupal.autocomplete_deluxe.escapeRegex( this.term );
  206. var re = new RegExp('()*""' + escapedValue + '""|' + escapedValue + '()*', 'gi');
  207. var t = item.label.replace(re,"<span class='autocomplete-deluxe-highlight-char'>$&</span>");
  208. }
  209. return $( "<li></li>" )
  210. .data(autocompleteDataKey, item)
  211. .append( "<a>" + t + "</a>" )
  212. .appendTo( ul );
  213. };
  214. };
  215. Drupal.autocomplete_deluxe.Widget.prototype.generateValues = function(data) {
  216. var result = new Array();
  217. for (var index in data) {
  218. result.push(data[index]);
  219. }
  220. return result;
  221. };
  222. /**
  223. * Generates a single selecting widget.
  224. */
  225. Drupal.autocomplete_deluxe.SingleWidget = function(settings) {
  226. this.init(settings);
  227. this.setup();
  228. this.jqObject.addClass('autocomplete-deluxe-form-single');
  229. };
  230. Drupal.autocomplete_deluxe.SingleWidget.prototype = new Drupal.autocomplete_deluxe.Widget();
  231. Drupal.autocomplete_deluxe.SingleWidget.prototype.setup = function() {
  232. var jqObject = this.jqObject;
  233. var parent = jqObject.parent();
  234. parent.mousedown(function() {
  235. if (parent.hasClass('autocomplete-deluxe-single-open')) {
  236. jqObject.autocomplete('close');
  237. } else {
  238. jqObject.autocomplete('search', '');
  239. }
  240. });
  241. };
  242. /**
  243. * Creates a multiple selecting widget.
  244. */
  245. Drupal.autocomplete_deluxe.MultipleWidget = function(input, settings) {
  246. this.init(settings);
  247. this.setup();
  248. };
  249. Drupal.autocomplete_deluxe.MultipleWidget.prototype = new Drupal.autocomplete_deluxe.Widget();
  250. Drupal.autocomplete_deluxe.MultipleWidget.prototype.items = new Object();
  251. Drupal.autocomplete_deluxe.MultipleWidget.prototype.acceptTerm = function(term) {
  252. // Accept only terms, that are not in our items list.
  253. return !(term in this.items);
  254. };
  255. Drupal.autocomplete_deluxe.MultipleWidget.Item = function (widget, item) {
  256. if (item.newTerm === true) {
  257. item.label = item.value;
  258. }
  259. this.value = item.value;
  260. this.element = $('<span class="autocomplete-deluxe-item"></span>');
  261. this.element.text(item.label);
  262. this.widget = widget;
  263. this.item = item;
  264. var self = this;
  265. var close = $('<a class="autocomplete-deluxe-item-delete" href="javascript:void(0)"></a>').appendTo(this.element);
  266. // Use single quotes because of the double quote encoded stuff.
  267. var input = $('<input type="hidden"/>')
  268. input.val(this.value);
  269. input.appendTo(this.element);
  270. close.mousedown(function() {
  271. self.remove(item);
  272. });
  273. };
  274. Drupal.autocomplete_deluxe.MultipleWidget.Item.prototype.remove = function() {
  275. this.element.remove();
  276. var values = this.widget.valueForm.val();
  277. var escapedValue = Drupal.autocomplete_deluxe.escapeRegex( this.item.value );
  278. var regex = new RegExp('()*""' + escapedValue + '""()*', 'gi');
  279. this.widget.valueForm.val(values.replace(regex, ''));
  280. delete this.widget.items[this.value];
  281. };
  282. Drupal.autocomplete_deluxe.MultipleWidget.prototype.setup = function() {
  283. var jqObject = this.jqObject;
  284. var parent = jqObject.parents('.autocomplete-deluxe-container');
  285. var value_container = parent.next();
  286. var value_input = value_container.find('input');
  287. var items = this.items;
  288. var self = this;
  289. this.valueForm = value_input;
  290. // Override the resize function, so that the suggestion list doesn't resizes
  291. // all the time.
  292. var autocompleteDataKey = typeof(this.jqObject.data('autocomplete')) === 'object' ? 'autocomplete' : 'ui-autocomplete';
  293. jqObject.data(autocompleteDataKey)._resizeMenu = function() {};
  294. jqObject.show();
  295. value_container.hide();
  296. // Add the default values to the box.
  297. var default_values = value_input.val();
  298. default_values = $.trim(default_values);
  299. default_values = default_values.substr(2, default_values.length-4);
  300. default_values = default_values.split('"" ""');
  301. for (var index in default_values) {
  302. var value = default_values[index];
  303. if (value != '') {
  304. // If a terms is encoded in double quotes, then the label should have
  305. // no double quotes.
  306. var label = value.match(/["][\w|\s|\D|]*["]/gi) !== null ? value.substr(1, value.length-2) : value;
  307. var item = {
  308. label : label,
  309. value : value
  310. };
  311. var item = new Drupal.autocomplete_deluxe.MultipleWidget.Item(self, item);
  312. item.element.insertBefore(jqObject);
  313. items[item.value] = item;
  314. }
  315. }
  316. jqObject.addClass('autocomplete-deluxe-multiple');
  317. parent.addClass('autocomplete-deluxe-multiple');
  318. // Adds a value to the list.
  319. this.addValue = function(ui_item) {
  320. var item = new Drupal.autocomplete_deluxe.MultipleWidget.Item(self, ui_item);
  321. item.element.insertBefore(jqObject);
  322. items[ui_item.value] = item;
  323. var new_value = ' ' + self.wrapper + ui_item.value + self.wrapper;
  324. var values = value_input.val();
  325. value_input.val(values + new_value);
  326. jqObject.val('');
  327. };
  328. parent.mouseup(function() {
  329. jqObject.autocomplete('search', '');
  330. jqObject.focus();
  331. });
  332. jqObject.bind("autocompleteselect", function(event, ui) {
  333. // JQuery ui autocomplete needs the terms escaped, otherwise it would be
  334. // open to XSS issues. Drupal.autocomplete.Item also escapes on rendering
  335. // the DOM elements. Thus we have to unescape the label here before adding
  336. // the new item.
  337. var item = ui.item;
  338. item.label = Drupal.autocomplete_deluxe.unescape(item.label);
  339. self.addValue(item);
  340. jqObject.width(25);
  341. // Return false to prevent setting the last term as value for the jqObject.
  342. return false;
  343. });
  344. jqObject.bind("autocompletechange", function(event, ui) {
  345. jqObject.val('');
  346. });
  347. jqObject.blur(function() {
  348. var last_element = jqObject.parent().children('.autocomplete-deluxe-item').last();
  349. last_element.removeClass('autocomplete-deluxe-item-focus');
  350. });
  351. var clear = false;
  352. jqObject.keypress(function (event) {
  353. var value = jqObject.val();
  354. // If a comma was entered and there is none or more then one comma,or the
  355. // enter key was entered, then enter the new term.
  356. if ((event.which == self.delimiter && (value.split('"').length - 1) != 1) || (event.which == 13 && jqObject.val() != "")) {
  357. value = value.substr(0, value.length);
  358. if (typeof self.items[value] == 'undefined' && value != '') {
  359. var ui_item = {
  360. label: value,
  361. value: value
  362. };
  363. self.addValue(ui_item);
  364. }
  365. clear = true;
  366. if (event.which == 13) {
  367. return false;
  368. }
  369. }
  370. // If the Backspace key was hit and the input is empty
  371. if (event.which == 8 && value == '') {
  372. var last_element = jqObject.parent().children('.autocomplete-deluxe-item').last();
  373. // then mark the last item for deletion or deleted it if already marked.
  374. if (last_element.hasClass('autocomplete-deluxe-item-focus')) {
  375. var value = last_element.children('input').val();
  376. self.items[value].remove(self.items[value]);
  377. jqObject.autocomplete('search', '');
  378. } else {
  379. last_element.addClass('autocomplete-deluxe-item-focus');
  380. }
  381. } else {
  382. // Remove the focus class if any other key was hit.
  383. var last_element = jqObject.parent().children('.autocomplete-deluxe-item').last();
  384. last_element.removeClass('autocomplete-deluxe-item-focus');
  385. }
  386. });
  387. jqObject.autoGrowInput({
  388. comfortZone: 50,
  389. minWidth: 10,
  390. maxWidth: 460
  391. });
  392. jqObject.keyup(function (event) {
  393. if (clear) {
  394. // Trigger the search, so it display the values for an empty string.
  395. jqObject.autocomplete('search', '');
  396. jqObject.val('');
  397. clear = false;
  398. // Return false to prevent entering the last character.
  399. return false;
  400. }
  401. });
  402. };
  403. })(jQuery);