· 6 years ago · Feb 13, 2020, 02:06 PM
1/**
2 * Copyright © Magento, Inc. All rights reserved.
3 * See COPYING.txt for license details.
4 */
5
6/**
7 * @api
8 */
9define([
10 'jquery',
11 'underscore',
12 'mage/template',
13 'matchMedia',
14 'jquery-ui-modules/widget',
15 'jquery-ui-modules/core',
16 'mage/translate'
17], function ($, _, mageTemplate, mediaCheck) {
18 'use strict';
19
20 /**
21 * Check whether the incoming string is not empty or if doesn't consist of spaces.
22 *
23 * @param {String} value - Value to check.
24 * @returns {Boolean}
25 */
26 function isEmpty(value) {
27 return value.length === 0 || value == null || /^\s+$/.test(value);
28 }
29
30 $.widget('storelocator.quickSearchStore', {
31 options: {
32 autocomplete: 'off',
33 minSearchLength: 2,
34 responseFieldElements: 'ul li',
35 selectClass: 'selected',
36 template:
37 '<li class="<%- data.row_class %>" id="qs-option-<%- data.index %>" role="option">' +
38 '<span class="qs-option-name">' +
39 ' <%- data.title %>' +
40 '</span>' +
41 '</li>',
42 submitBtn: 'button[type="submit"]',
43 isExpandable: null,
44 suggestionDelay: 300
45 },
46
47 /** @inheritdoc */
48 _create: function () {
49 this.responseList = {
50 indexList: null,
51 selected: null
52 };
53 this.autoComplete = $(this.options.destinationSelector);
54 this.searchForm = $(this.options.formSelector);
55 this.submitBtn = this.searchForm.find(this.options.submitBtn)[0];
56 this.isExpandable = this.options.isExpandable;
57
58 _.bindAll(this, '_onKeyDown', '_onPropertyChange', '_onSubmit');
59
60 this.submitBtn.disabled = true;
61
62 this.element.attr('autocomplete', this.options.autocomplete);
63
64 mediaCheck({
65 media: '(max-width: 768px)',
66 entry: function () {
67 this.isExpandable = true;
68 }.bind(this),
69 exit: function () {
70 this.isExpandable = true;
71 }.bind(this)
72 });
73
74 this.element.on('blur', $.proxy(function () {
75 setTimeout($.proxy(function () {
76 if (this.autoComplete.is(':hidden')) {
77 this.setActiveState(false);
78 } else {
79 this.element.trigger('focus');
80 }
81 this.autoComplete.hide();
82 this._updateAriaHasPopup(false);
83 }, this), 250);
84 }, this));
85
86 if (this.element.get(0) === document.activeElement) {
87 this.setActiveState(true);
88 }
89
90 this.element.on('focus', this.setActiveState.bind(this, true));
91 this.element.on('keydown', this._onKeyDown);
92 // Prevent spamming the server with requests by waiting till the user has stopped typing for period of time
93 this.element.on('input propertychange', _.debounce(this._onPropertyChange, this.options.suggestionDelay));
94
95 this.searchForm.on('submit', $.proxy(function (e) {
96 this._onSubmit(e);
97 this._updateAriaHasPopup(false);
98 }, this));
99 },
100
101 /**
102 * Sets state of the search field to provided value.
103 *
104 * @param {Boolean} isActive
105 */
106 setActiveState: function (isActive) {
107 var searchValue;
108
109 this.searchForm.toggleClass('active', isActive);
110
111 if (this.isExpandable) {
112 this.element.attr('aria-expanded', isActive);
113 searchValue = this.element.val();
114 this.element.val('');
115 this.element.val(searchValue);
116 }
117 },
118
119 /**
120 * @private
121 * @return {Element} The first element in the suggestion list.
122 */
123 _getFirstVisibleElement: function () {
124 return this.responseList.indexList ? this.responseList.indexList.first() : false;
125 },
126
127 /**
128 * @private
129 * @return {Element} The last element in the suggestion list.
130 */
131 _getLastElement: function () {
132 return this.responseList.indexList ? this.responseList.indexList.last() : false;
133 },
134
135 /**
136 * @private
137 * @param {Boolean} show - Set attribute aria-haspopup to "true/false" for element.
138 */
139 _updateAriaHasPopup: function (show) {
140 if (show) {
141 this.element.attr('aria-haspopup', 'true');
142 } else {
143 this.element.attr('aria-haspopup', 'false');
144 }
145 },
146
147 /**
148 * Clears the item selected from the suggestion list and resets the suggestion list.
149 * @private
150 * @param {Boolean} all - Controls whether to clear the suggestion list.
151 */
152 _resetResponseList: function (all) {
153 this.responseList.selected = null;
154
155 if (all === true) {
156 this.responseList.indexList = null;
157 }
158 },
159
160 /**
161 * Executes when the search box is submitted. Sets the search input field to the
162 * value of the selected item.
163 * @private
164 * @param {Event} e - The submit event
165 */
166 _onSubmit: function (e) {
167 var value = this.element.val();
168
169 if (isEmpty(value)) {
170 e.preventDefault();
171 }
172
173 if (this.responseList.selected) {
174 this.element.val(this.responseList.selected.find('.qs-option-name').text());
175 }
176 },
177
178 /**
179 * Executes when keys are pressed in the search input field. Performs specific actions
180 * depending on which keys are pressed.
181 * @private
182 * @param {Event} e - The key down event
183 * @return {Boolean} Default return type for any unhandled keys
184 */
185 _onKeyDown: function (e) {
186 var keyCode = e.keyCode || e.which;
187
188 switch (keyCode) {
189 case $.ui.keyCode.HOME:
190 if (this._getFirstVisibleElement()) {
191 this._getFirstVisibleElement().addClass(this.options.selectClass);
192 this.responseList.selected = this._getFirstVisibleElement();
193 }
194 break;
195
196 case $.ui.keyCode.END:
197 if (this._getLastElement()) {
198 this._getLastElement().addClass(this.options.selectClass);
199 this.responseList.selected = this._getLastElement();
200 }
201 break;
202
203 case $.ui.keyCode.ESCAPE:
204 this._resetResponseList(true);
205 this.autoComplete.hide();
206 break;
207
208 case $.ui.keyCode.ENTER:
209 this.searchForm.trigger('submit');
210 e.preventDefault();
211 break;
212
213 case $.ui.keyCode.DOWN:
214 if (this.responseList.indexList) {
215 if (!this.responseList.selected) { //eslint-disable-line max-depth
216 this._getFirstVisibleElement().addClass(this.options.selectClass);
217 this.responseList.selected = this._getFirstVisibleElement();
218 } else if (!this._getLastElement().hasClass(this.options.selectClass)) {
219 this.responseList.selected = this.responseList.selected
220 .removeClass(this.options.selectClass).next().addClass(this.options.selectClass);
221 } else {
222 this.responseList.selected.removeClass(this.options.selectClass);
223 this._getFirstVisibleElement().addClass(this.options.selectClass);
224 this.responseList.selected = this._getFirstVisibleElement();
225 }
226 this.element.val(this.responseList.selected.find('.qs-option-name').text());
227 this.element.attr('aria-activedescendant', this.responseList.selected.attr('id'));
228 }
229 break;
230
231 case $.ui.keyCode.UP:
232 if (this.responseList.indexList !== null) {
233 if (!this._getFirstVisibleElement().hasClass(this.options.selectClass)) {
234 this.responseList.selected = this.responseList.selected
235 .removeClass(this.options.selectClass).prev().addClass(this.options.selectClass);
236
237 } else {
238 this.responseList.selected.removeClass(this.options.selectClass);
239 this._getLastElement().addClass(this.options.selectClass);
240 this.responseList.selected = this._getLastElement();
241 }
242 this.element.val(this.responseList.selected.find('.qs-option-name').text());
243 this.element.attr('aria-activedescendant', this.responseList.selected.attr('id'));
244 }
245 break;
246 default:
247 return true;
248 }
249 },
250
251 /**
252 * Executes when the value of the search input field changes. Executes a GET request
253 * to populate a suggestion list based on entered text. Handles click (select), hover,
254 * and mouseout events on the populated suggestion list dropdown.
255 * @private
256 */
257 _onPropertyChange: function () {
258 var searchField = this.element,
259 clonePosition = {
260 position: 'absolute',
261 // Removed to fix display issues
262 // left: searchField.offset().left,
263 // top: searchField.offset().top + searchField.outerHeight(),
264 width: searchField.outerWidth()
265 },
266 source = this.options.template,
267 template = mageTemplate(source),
268 dropdown = $('<ul role="listbox"></ul>'),
269 value = this.element.val();
270
271 this.submitBtn.disabled = isEmpty(value);
272
273 if (value.length >= parseInt(this.options.minSearchLength, 10)) {
274 $.getJSON(this.options.url, {
275 q: value
276 }, $.proxy(function (data) {
277 if (data.length) {
278 $.each(data, function (index, element) {
279 var html;
280
281 element.index = index;
282 html = template({
283 data: element
284 });
285 dropdown.append(html);
286 });
287
288 this._resetResponseList(true);
289
290 this.responseList.indexList = this.autoComplete.html(dropdown)
291 .css(clonePosition)
292 .show()
293 .find(this.options.responseFieldElements + ':visible');
294
295 this.element.removeAttr('aria-activedescendant');
296
297 if (this.responseList.indexList.length) {
298 this._updateAriaHasPopup(true);
299 } else {
300 this._updateAriaHasPopup(false);
301 }
302
303 this.responseList.indexList
304 .on('click', function (e) {
305 this.responseList.selected = $(e.currentTarget);
306 this.searchForm.trigger('submit');
307 }.bind(this))
308 .on('mouseenter mouseleave', function (e) {
309 this.responseList.indexList.removeClass(this.options.selectClass);
310 $(e.target).addClass(this.options.selectClass);
311 this.responseList.selected = $(e.target);
312 this.element.attr('aria-activedescendant', $(e.target).attr('id'));
313 }.bind(this))
314 .on('mouseout', function (e) {
315 if (!this._getLastElement() &&
316 this._getLastElement().hasClass(this.options.selectClass)) {
317 $(e.target).removeClass(this.options.selectClass);
318 this._resetResponseList(false);
319 }
320 }.bind(this));
321 } else {
322 this._resetResponseList(true);
323 this.autoComplete.hide();
324 this._updateAriaHasPopup(false);
325 this.element.removeAttr('aria-activedescendant');
326 }
327 }, this));
328 } else {
329 this._resetResponseList(true);
330 this.autoComplete.hide();
331 this._updateAriaHasPopup(false);
332 this.element.removeAttr('aria-activedescendant');
333 }
334 }
335 });
336
337 return $.storelocator.quickSearchStore;
338});