SFRA Server-side Javascript - Source: components/search.js menu

SFRA / Client-side JS / Source: components/search.js

  1. 'use strict';
  2. var debounce = require('lodash/debounce');
  3. var endpoint = $('.suggestions-wrapper').data('url');
  4. var minChars = 1;
  5. var UP_KEY = 38;
  6. var DOWN_KEY = 40;
  7. var DIRECTION_DOWN = 1;
  8. var DIRECTION_UP = -1;
  9. /**
  10. * Retrieves Suggestions element relative to scope
  11. *
  12. * @param {Object} scope - Search input field DOM element
  13. * @return {JQuery} - .suggestions-wrapper element
  14. */
  15. function getSuggestionsWrapper(scope) {
  16. return $(scope).siblings('.suggestions-wrapper');
  17. }
  18. /**
  19. * Determines whether DOM element is inside the .search-mobile class
  20. *
  21. * @param {Object} scope - DOM element, usually the input.search-field element
  22. * @return {boolean} - Whether DOM element is inside div.search-mobile
  23. */
  24. function isMobileSearch(scope) {
  25. return !!$(scope).closest('.search-mobile').length;
  26. }
  27. /**
  28. * Remove modal classes needed for mobile suggestions
  29. *
  30. */
  31. function clearModals() {
  32. $('body').removeClass('modal-open');
  33. $('header').siblings().attr('aria-hidden', 'false');
  34. $('.suggestions').removeClass('modal');
  35. }
  36. /**
  37. * Apply modal classes needed for mobile suggestions
  38. *
  39. * @param {Object} scope - Search input field DOM element
  40. */
  41. function applyModals(scope) {
  42. if (isMobileSearch(scope)) {
  43. $('body').addClass('modal-open');
  44. $('header').siblings().attr('aria-hidden', 'true');
  45. getSuggestionsWrapper(scope).find('.suggestions').addClass('modal');
  46. }
  47. }
  48. /**
  49. * Tear down Suggestions panel
  50. */
  51. function tearDownSuggestions() {
  52. $('input.search-field').val('');
  53. clearModals();
  54. $('.search-mobile .suggestions').unbind('scroll');
  55. $('.suggestions-wrapper').empty();
  56. }
  57. /**
  58. * Toggle search field icon from search to close and vice-versa
  59. *
  60. * @param {string} action - Action to toggle to
  61. */
  62. function toggleSuggestionsIcon(action) {
  63. var mobileSearchIcon = '.search-mobile button.';
  64. var iconSearch = 'fa-search';
  65. var iconSearchClose = 'fa-close';
  66. if (action === 'close') {
  67. $(mobileSearchIcon + iconSearch).removeClass(iconSearch).addClass(iconSearchClose).attr('type', 'button');
  68. } else {
  69. $(mobileSearchIcon + iconSearchClose).removeClass(iconSearchClose).addClass(iconSearch).attr('type', 'submit');
  70. }
  71. }
  72. /**
  73. * Determines whether the "More Content Below" icon should be displayed
  74. *
  75. * @param {Object} scope - DOM element, usually the input.search-field element
  76. */
  77. function handleMoreContentBelowIcon(scope) {
  78. if (($(scope).scrollTop() + $(scope).innerHeight()) >= $(scope)[0].scrollHeight) {
  79. $('.more-below').fadeOut();
  80. } else {
  81. $('.more-below').fadeIn();
  82. }
  83. }
  84. /**
  85. * Positions Suggestions panel on page
  86. *
  87. * @param {Object} scope - DOM element, usually the input.search-field element
  88. */
  89. function positionSuggestions(scope) {
  90. var outerHeight;
  91. var $scope;
  92. var $suggestions;
  93. var top;
  94. if (isMobileSearch(scope)) {
  95. $scope = $(scope);
  96. top = $scope.offset().top;
  97. outerHeight = $scope.outerHeight();
  98. $suggestions = getSuggestionsWrapper(scope).find('.suggestions');
  99. $suggestions.css('top', top + outerHeight);
  100. handleMoreContentBelowIcon(scope);
  101. // Unfortunately, we have to bind this dynamically, as the live scroll event was not
  102. // properly detecting dynamic suggestions element's scroll event
  103. $suggestions.scroll(function () {
  104. handleMoreContentBelowIcon(this);
  105. });
  106. }
  107. }
  108. /**
  109. * Process Ajax response for SearchServices-GetSuggestions
  110. *
  111. * @param {Object|string} response - Empty object literal if null response or string with rendered
  112. * suggestions template contents
  113. */
  114. function processResponse(response) {
  115. var $suggestionsWrapper = getSuggestionsWrapper(this).empty();
  116. $.spinner().stop();
  117. if (typeof (response) !== 'object') {
  118. $suggestionsWrapper.append(response).show();
  119. $(this).siblings('.reset-button').addClass('d-sm-block');
  120. positionSuggestions(this);
  121. if (isMobileSearch(this)) {
  122. toggleSuggestionsIcon('close');
  123. applyModals(this);
  124. }
  125. // Trigger screen reader by setting aria-describedby with the new suggestion message.
  126. var suggestionsList = $('.suggestions .item');
  127. if ($(suggestionsList).length) {
  128. $('input.search-field').attr('aria-describedby', 'search-result-count');
  129. } else {
  130. $('input.search-field').removeAttr('aria-describedby');
  131. }
  132. } else {
  133. $suggestionsWrapper.hide();
  134. }
  135. }
  136. /**
  137. * Retrieve suggestions
  138. *
  139. * @param {Object} scope - Search field DOM element
  140. */
  141. function getSuggestions(scope) {
  142. if ($(scope).val().length >= minChars) {
  143. $.spinner().start();
  144. $.ajax({
  145. context: scope,
  146. url: endpoint + encodeURIComponent($(scope).val()),
  147. method: 'GET',
  148. success: processResponse,
  149. error: function () {
  150. $.spinner().stop();
  151. }
  152. });
  153. } else {
  154. toggleSuggestionsIcon('search');
  155. $(scope).siblings('.reset-button').removeClass('d-sm-block');
  156. clearModals();
  157. getSuggestionsWrapper(scope).empty();
  158. }
  159. }
  160. /**
  161. * Handle Search Suggestion Keyboard Arrow Keys
  162. *
  163. * @param {Integer} direction takes positive or negative number constant, DIRECTION_UP (-1) or DIRECTION_DOWN (+1)
  164. */
  165. function handleArrow(direction) {
  166. // get all li elements in the suggestions list
  167. var suggestionsList = $('.suggestions .item');
  168. if (suggestionsList.filter('.selected').length === 0) {
  169. suggestionsList.first().addClass('selected');
  170. $('input.search-field').each(function () {
  171. $(this).attr('aria-activedescendant', suggestionsList.first()[0].id);
  172. });
  173. } else {
  174. suggestionsList.each(function (index) {
  175. var idx = index + direction;
  176. if ($(this).hasClass('selected')) {
  177. $(this).removeClass('selected');
  178. $(this).removeAttr('aria-selected');
  179. if (suggestionsList.eq(idx).length !== 0) {
  180. suggestionsList.eq(idx).addClass('selected');
  181. suggestionsList.eq(idx).attr('aria-selected', true);
  182. $(this).removeProp('aria-selected');
  183. $('input.search-field').each(function () {
  184. $(this).attr('aria-activedescendant', suggestionsList.eq(idx)[0].id);
  185. });
  186. } else {
  187. suggestionsList.first().addClass('selected');
  188. suggestionsList.first().attr('aria-selected', true);
  189. $('input.search-field').each(function () {
  190. $(this).attr('aria-activedescendant', suggestionsList.first()[0].id);
  191. });
  192. }
  193. return false;
  194. }
  195. return true;
  196. });
  197. }
  198. }
  199. module.exports = function () {
  200. $('form[name="simpleSearch"]').submit(function (e) {
  201. var suggestionsList = $('.suggestions .item');
  202. if (suggestionsList.filter('.selected').length !== 0) {
  203. e.preventDefault();
  204. suggestionsList.filter('.selected').find('a')[0].click();
  205. }
  206. });
  207. $('input.search-field').each(function () {
  208. /**
  209. * Use debounce to avoid making an Ajax call on every single key press by waiting a few
  210. * hundred milliseconds before making the request. Without debounce, the user sees the
  211. * browser blink with every key press.
  212. */
  213. var debounceSuggestions = debounce(getSuggestions, 300);
  214. $(this).on('keyup focus', function (e) {
  215. // Capture Down/Up Arrow Key Events
  216. switch (e.which) {
  217. case DOWN_KEY:
  218. handleArrow(DIRECTION_DOWN);
  219. e.preventDefault(); // prevent moving the cursor
  220. break;
  221. case UP_KEY:
  222. handleArrow(DIRECTION_UP);
  223. e.preventDefault(); // prevent moving the cursor
  224. break;
  225. default:
  226. debounceSuggestions(this, e);
  227. }
  228. });
  229. });
  230. $('body').on('click', function (e) {
  231. if (!$('.suggestions').has(e.target).length && !$(e.target).hasClass('search-field')) {
  232. $('.suggestions').hide();
  233. }
  234. });
  235. $('body').on('click touchend', '.search-mobile button.fa-close', function (e) {
  236. e.preventDefault();
  237. $('.suggestions').hide();
  238. toggleSuggestionsIcon('search');
  239. tearDownSuggestions();
  240. });
  241. $('.site-search .reset-button').on('click', function () {
  242. $(this).removeClass('d-sm-block');
  243. });
  244. };