stag-suggest.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. (function () {
  2. window.initStagSuggest = function () {
  3. let suggestionsOuter = null;
  4. const debounce = (func, wait) => {
  5. let timeout;
  6. return function executedFunction(...args) {
  7. const later = () => {
  8. clearTimeout(timeout);
  9. func(...args);
  10. };
  11. clearTimeout(timeout);
  12. timeout = setTimeout(later, wait);
  13. };
  14. };
  15. var lastTerm = '';
  16. var returnedFunction = debounce(function (elem) {
  17. let term = elem.val();
  18. let ep = $(elem).attr('stag-suggest-ep'), extra = $(elem).attr('stag-suggest-extra');
  19. if(!!ep) { // remote lookup
  20. if (!!term && term.length >= 2) {
  21. $.get(ep + '?term=' + $.trim(term) + (!!extra ? '&' + extra : ''), function (_data) {
  22. /*
  23. expected return format:
  24. {
  25. success: true,
  26. data: [
  27. {
  28. x: ...,
  29. y: ...,
  30. text: ... // "text" key is "mandatory"
  31. },
  32. {
  33. x: ...,
  34. y: ...,
  35. text: ... // "text" key is "mandatory"
  36. },
  37. ...
  38. ]
  39. }
  40. */
  41. suggestionsOuter.empty();
  42. if(!hasResponseError(_data) && _data.data && _data.data.length) {
  43. for (let i = 0; i < _data.data.length; i++) {
  44. let item = $('<a native href="#" class="d-block suggest-item stag-suggest text-nowrap"/>');
  45. for(let x in _data.data[i]) {
  46. if(_data.data[i].hasOwnProperty(x) && x !== 'text') {
  47. item.attr('data-' + x, _data.data[i][x]);
  48. }
  49. }
  50. item.data('suggest-data', _data.data[i]);
  51. item.html(_data.data[i].text);
  52. if(_data.data[i].sub_text) {
  53. item.append($('<span/>')
  54. .addClass('ml-1 text-sm text-secondary')
  55. .append(' (')
  56. .append(_data.data[i].sub_text)
  57. .append(')')
  58. );
  59. }
  60. if(_data.data[i].text2) {
  61. item.append($('<div/>')
  62. .addClass('text-sm text-secondary')
  63. .html(_data.data[i].text2)
  64. );
  65. }
  66. if(_data.data[i].tooltip) {
  67. item.attr('title', _data.data[i].tooltip);
  68. }
  69. if(i === 0) {
  70. item.addClass('active');
  71. }
  72. suggestionsOuter.append(item);
  73. }
  74. }
  75. else {
  76. suggestionsOuter.html('<span class="d-block no-suggest-items">No matches!</span>');
  77. }
  78. suggestionsOuter.removeClass('d-none');
  79. }, 'json');
  80. lastTerm = term;
  81. } else {
  82. suggestionsOuter.addClass('d-none');
  83. }
  84. }
  85. else { // local lookup
  86. let optionList = $(elem).next().next('.data-option-list');
  87. if(optionList.length) {
  88. let matches = [];
  89. optionList.find('>div').each(function() {
  90. if(!term || $(this).text().toLowerCase().indexOf(term.toLowerCase()) !== -1) {
  91. matches.push($(this).text());
  92. }
  93. });
  94. suggestionsOuter.empty();
  95. if(matches.length) {
  96. for (let i = 0; i < matches.length; i++) {
  97. let item = $('<a native href="#" class="d-block suggest-item stag-suggest text-nowrap"/>');
  98. item.data('suggest-data', {});
  99. item.html(matches[i]);
  100. suggestionsOuter.append(item);
  101. }
  102. }
  103. else {
  104. suggestionsOuter.html('<span class="d-block no-suggest-items">No matches!</span>');
  105. }
  106. suggestionsOuter.removeClass('d-none');
  107. }
  108. }
  109. }, 250);
  110. function handleKeydown(elem, e) {
  111. let term = $.trim(elem.val());
  112. let activeItem = suggestionsOuter.find('.suggest-item.active');
  113. switch (e.which) {
  114. case 27:
  115. if(suggestionsOuter.is(':visible')) {
  116. suggestionsOuter.addClass('d-none');
  117. markEventAsConsumed(e);
  118. return false;
  119. }
  120. break;
  121. case 38:
  122. if(suggestionsOuter.is(':visible')) {
  123. if (activeItem.prev().length) {
  124. activeItem.prev()
  125. .addClass('active')
  126. .siblings().removeClass('active');
  127. activeItem = suggestionsOuter.find('.suggest-item.active');
  128. if (activeItem.length) {
  129. if(activeItem[0].scrollIntoViewIfNeeded) {
  130. activeItem[0].scrollIntoViewIfNeeded(false);
  131. }
  132. else {
  133. activeItem[0].scrollIntoView({
  134. behavior: "auto",
  135. block: "start",
  136. inline: "start"
  137. });
  138. }
  139. }
  140. }
  141. return false;
  142. }
  143. break;
  144. case 40:
  145. if(suggestionsOuter.is(':visible')) {
  146. if (activeItem.next().length) {
  147. activeItem.next()
  148. .addClass('active')
  149. .siblings().removeClass('active');
  150. activeItem = suggestionsOuter.find('.suggest-item.active');
  151. if (activeItem.length) {
  152. if(activeItem[0].scrollIntoViewIfNeeded) {
  153. activeItem[0].scrollIntoViewIfNeeded(false);
  154. }
  155. else {
  156. activeItem[0].scrollIntoView({
  157. behavior: "auto",
  158. block: "end",
  159. inline: "end"
  160. });
  161. }
  162. }
  163. }
  164. return false;
  165. }
  166. break;
  167. case 13:
  168. if(suggestionsOuter.is(':visible')) {
  169. if (activeItem.length) {
  170. activeItem.first().trigger('mousedown');
  171. }
  172. return false;
  173. }
  174. break;
  175. default:
  176. if (!!term) {
  177. suggestionsOuter
  178. .html('<span class="d-block no-suggest-items">Searching...</span>')
  179. .removeClass('d-none');
  180. returnedFunction(elem);
  181. } else {
  182. suggestionsOuter.addClass('d-none');
  183. }
  184. break;
  185. }
  186. }
  187. function handleKeypress(elem, e) {
  188. var term = $.trim(elem.val());
  189. if (!!term || !$(elem).is('[stag-suggest-ep]')) {
  190. suggestionsOuter
  191. .html('<span class="d-block no-suggest-items">Searching...</span>')
  192. .removeClass('d-none');
  193. returnedFunction(elem);
  194. } else {
  195. suggestionsOuter.addClass('d-none');
  196. }
  197. }
  198. $('[stag-suggest]:not([stag-suggest-initialized])').each(function () {
  199. let elem = $(this);
  200. elem.next('.stag-suggestions-container').remove();
  201. $('<div class="stag-suggestions-container position-relative">' +
  202. '<div class="suggestions-outer stag-suggestions position-absolute d-none"></div>' +
  203. '</div>').insertAfter(elem);
  204. elem
  205. .off('focus.stag-suggest')
  206. .on('focus.stag-suggest', function (e) {
  207. if(!$(this).is('[stag-suggest-ep]')) {
  208. suggestionsOuter = $(this).next('.stag-suggestions-container').find('>.suggestions-outer');
  209. return handleKeypress($(this), e);
  210. }
  211. })
  212. .off('keydown.stag-suggest')
  213. .on('keydown.stag-suggest', function (e) {
  214. suggestionsOuter = $(this).next('.stag-suggestions-container').find('>.suggestions-outer');
  215. return handleKeydown($(this), e);
  216. })
  217. .off('paste.stag-suggest')
  218. .on('paste.stag-suggest', function (e) {
  219. window.setTimeout(() => {
  220. suggestionsOuter = $(this).next('.stag-suggestions-container').find('>.suggestions-outer');
  221. return handleKeypress($(this), e);
  222. }, 100);
  223. })
  224. .off('keypress.stag-suggest')
  225. .on('keypress.stag-suggest', function (e) {
  226. suggestionsOuter = $(this).next('.stag-suggestions-container').find('>.suggestions-outer');
  227. return handleKeypress($(this), e);
  228. });
  229. $(this).attr('stag-suggest-initialized', 1);
  230. });
  231. // on auto-suggest selection
  232. $(document).off('mousedown.stag-suggest', '.suggest-item.stag-suggest');
  233. $(document).on('mousedown.stag-suggest', '.suggest-item.stag-suggest', function () {
  234. $('.suggestions-outer.stag-suggestions').addClass('d-none');
  235. let data = $(this).data('suggest-data'),
  236. label = $.trim($(this).text());
  237. // set value
  238. let input = $(this).closest('.position-relative').prev('[stag-suggest]');
  239. // if input is [stag-suggest-text-only] - use only the text bit in label
  240. if(input.is('[stag-suggest-text-only]')) {
  241. label = data.text;
  242. }
  243. input.val(label);
  244. input.data('suggest-data', data);
  245. let scope = input.attr('stag-suggest-scope');
  246. if(!scope) scope = 'form';
  247. scope = $(scope);
  248. for(let x in data) {
  249. if(data.hasOwnProperty(x)) {
  250. input.attr('data-' + x, data[x]);
  251. // auto-populate if there's a field matching data-name="x" in the scope
  252. if(scope.find('[data-name="' + x + '"]').length) {
  253. scope.find('[data-name="' + x + '"]').val(data[x]).trigger('change');
  254. }
  255. }
  256. }
  257. input.trigger('input');
  258. input.trigger('change');
  259. input.trigger('stag-suggest-selected', [input, data]);
  260. return false;
  261. });
  262. // outside click
  263. $(document)
  264. .off('mousedown.stag-suggest-outer-click')
  265. .on('mousedown.stag-suggest-outer-click', function (_e) {
  266. let elem = $(_e.target);
  267. // if mousedown is on the input whose stag-suggest is on display, ignore
  268. if(elem.is('[stag-suggest]') && elem.next('.stag-suggestions-container').find('>.suggestions-outer').is(':visible')) {
  269. return false;
  270. }
  271. if(!elem.is('.stag-suggestions-container') && !elem.closest('.stag-suggestions-container').length) {
  272. if($('.stag-suggestions-container .suggestions-outer:not(.d-none)').length) {
  273. $('.stag-suggestions-container .suggestions-outer').addClass('d-none');
  274. return false;
  275. }
  276. }
  277. });
  278. }
  279. addMCInitializer('stag-suggest', window.initStagSuggest);
  280. })();