stag-suggest.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  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. if (!!term) {
  19. let ep = $(elem).attr('stag-suggest-ep');
  20. $.get(ep + '?term=' + $.trim(term), function (_data) {
  21. /*
  22. expected return format:
  23. {
  24. success: true,
  25. data: [
  26. {
  27. x: ...,
  28. y: ...,
  29. text: ... // "text" key is "mandatory"
  30. },
  31. {
  32. x: ...,
  33. y: ...,
  34. text: ... // "text" key is "mandatory"
  35. },
  36. ...
  37. ]
  38. }
  39. */
  40. suggestionsOuter.empty();
  41. if(!hasResponseError(_data) && _data.data && _data.data.length) {
  42. for (let i = 0; i < _data.data.length; i++) {
  43. let item = $('<a native href="#" class="d-block suggest-item stag-suggest text-nowrap"/>');
  44. for(let x in _data.data[i]) {
  45. if(_data.data[i].hasOwnProperty(x) && x !== 'text') {
  46. item.attr('data-' + x, _data.data[i][x]);
  47. }
  48. }
  49. item.data('suggest-data', _data.data[i]);
  50. item.html(_data.data[i].text);
  51. if(_data.data[i].sub_text) {
  52. item.append($('<span/>')
  53. .addClass('ml-1 text-sm text-secondary')
  54. .append(' (')
  55. .append(_data.data[i].sub_text)
  56. .append(')')
  57. );
  58. }
  59. if(_data.data[i].text2) {
  60. item.append($('<div/>')
  61. .addClass('text-sm text-secondary')
  62. .html(_data.data[i].text2)
  63. );
  64. }
  65. if(_data.data[i].tooltip) {
  66. item.attr('title', _data.data[i].tooltip);
  67. }
  68. suggestionsOuter.append(item);
  69. }
  70. }
  71. else {
  72. suggestionsOuter.html('<span class="d-block no-suggest-items">No matches!</span>');
  73. }
  74. suggestionsOuter.removeClass('d-none');
  75. }, 'json');
  76. lastTerm = term;
  77. } else {
  78. suggestionsOuter.addClass('d-none');
  79. }
  80. }, 250);
  81. function handleKeydown(elem, e) {
  82. let term = $.trim(elem.val());
  83. let activeItem = suggestionsOuter.find('.suggest-item.active');
  84. switch (e.which) {
  85. case 27:
  86. suggestionsOuter.addClass('d-none');
  87. markEventAsConsumed(e);
  88. return false;
  89. case 38:
  90. if (activeItem.prev().length) {
  91. activeItem.prev()
  92. .addClass('active')
  93. .siblings().removeClass('active');
  94. activeItem = suggestionsOuter.find('.suggest-item.active');
  95. if (activeItem.length) {
  96. activeItem[0].scrollIntoView();
  97. }
  98. }
  99. return false;
  100. case 40:
  101. if (activeItem.next().length) {
  102. activeItem.next()
  103. .addClass('active')
  104. .siblings().removeClass('active');
  105. activeItem = suggestionsOuter.find('.suggest-item.active');
  106. if (activeItem.length) {
  107. activeItem[0].scrollIntoView();
  108. }
  109. }
  110. return false;
  111. case 13:
  112. if (activeItem.length) {
  113. activeItem.first().click();
  114. }
  115. return false;
  116. default:
  117. if (!!term) {
  118. suggestionsOuter
  119. .html('<span class="d-block no-suggest-items">Searching...</span>')
  120. .removeClass('d-none');
  121. returnedFunction(elem);
  122. } else {
  123. suggestionsOuter.addClass('d-none');
  124. }
  125. break;
  126. }
  127. }
  128. function handleKeypress(elem, e) {
  129. var term = $.trim(elem.val());
  130. if (!!term) {
  131. suggestionsOuter
  132. .html('<span class="d-block no-suggest-items">Searching...</span>')
  133. .removeClass('d-none');
  134. returnedFunction(elem);
  135. } else {
  136. suggestionsOuter.addClass('d-none');
  137. }
  138. }
  139. $('[stag-suggest]:not([stag-suggest-initialized])').each(function () {
  140. let elem = $(this);
  141. elem.next('.stag-suggestions-container').remove();
  142. $('<div class="stag-suggestions-container position-relative">' +
  143. '<div class="suggestions-outer stag-suggestions position-absolute d-none"></div>' +
  144. '</div>').insertAfter(elem);
  145. elem
  146. .off('keydown.stag-suggest')
  147. .on('keydown.stag-suggest', function (e) {
  148. suggestionsOuter = $(this).next('.stag-suggestions-container').find('>.suggestions-outer');
  149. return handleKeydown($(this), e);
  150. })
  151. .off('paste.stag-suggest')
  152. .on('paste.stag-suggest', function (e) {
  153. window.setTimeout(() => {
  154. suggestionsOuter = $(this).next('.stag-suggestions-container').find('>.suggestions-outer');
  155. return handleKeypress($(this), e);
  156. }, 100);
  157. })
  158. .off('keypress.stag-suggest')
  159. .on('keypress.stag-suggest', function (e) {
  160. suggestionsOuter = $(this).next('.stag-suggestions-container').find('>.suggestions-outer');
  161. return handleKeypress($(this), e);
  162. });
  163. $(this).attr('stag-suggest-initialized', 1);
  164. });
  165. // on auto-suggest selection
  166. $(document).off('click', '.suggest-item.stag-suggest');
  167. $(document).on('click', '.suggest-item.stag-suggest', function () {
  168. $('.suggestions-outer.stag-suggestions').addClass('d-none');
  169. let data = $(this).data('suggest-data'),
  170. label = $.trim($(this).text());
  171. // set value
  172. let input = $(this).closest('.position-relative').prev('[stag-suggest]');
  173. // if input is [stag-suggest-text-only] - use only the text bit in label
  174. if(input.is('[stag-suggest-text-only]')) {
  175. label = data.text;
  176. }
  177. input.val(label);
  178. input.data('suggest-data', data);
  179. let scope = input.attr('stag-suggest-scope');
  180. if(!scope) scope = 'form';
  181. scope = $(scope);
  182. for(let x in data) {
  183. if(data.hasOwnProperty(x)) {
  184. input.attr('data-' + x, data[x]);
  185. // auto-populate if there's a field matching data-name="x" in the scope
  186. if(scope.find('[data-name="' + x + '"]').length) {
  187. scope.find('[data-name="' + x + '"]').val(data[x]).trigger('change');
  188. }
  189. }
  190. }
  191. input.trigger('input');
  192. input.trigger('change');
  193. input.trigger('stag-suggest-selected', [input, data]);
  194. return false;
  195. });
  196. // outside click
  197. $(document)
  198. .off('mousedown.stag-suggest-outer-click')
  199. .on('mousedown.stag-suggest-outer-click', function (_e) {
  200. let elem = $(_e.target);
  201. // if mousedown is on the input whose stag-suggest is on display, ignore
  202. if(elem.is('[stag-suggest]') && elem.next('.stag-suggestions-container').find('>.suggestions-outer').is(':visible')) {
  203. return false;
  204. }
  205. if(!elem.is('.stag-suggestions-container') && !elem.closest('.stag-suggestions-container').length) {
  206. if($('.stag-suggestions-container .suggestions-outer:not(.d-none)').length) {
  207. $('.stag-suggestions-container .suggestions-outer').addClass('d-none');
  208. return false;
  209. }
  210. }
  211. });
  212. }
  213. addMCInitializer('stag-suggest', window.initStagSuggest);
  214. })();