autocomplete_deluxe.js 14 KB

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