suggest.js 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199
  1. /**
  2. * Copyright © Magento, Inc. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. (function (root, factory) {
  6. 'use strict';
  7. if (typeof define === 'function' && define.amd) {
  8. define([
  9. 'jquery',
  10. 'mage/template',
  11. 'mage/mage',
  12. 'jquery/ui',
  13. 'mage/backend/menu',
  14. 'mage/translate'
  15. ], factory);
  16. } else {
  17. factory(root.jQuery, root.mageTemplate);
  18. }
  19. }(this, function ($, mageTemplate) {
  20. 'use strict';
  21. /**
  22. * Implement base functionality
  23. */
  24. $.widget('mage.suggest', {
  25. widgetEventPrefix: 'suggest',
  26. options: {
  27. template: '<% if (data.items.length) { %>' +
  28. '<% if (!data.term && !data.allShown() && data.recentShown()) { %>' +
  29. '<h5 class="title"><%- data.recentTitle %></h5>' +
  30. '<% } %>' +
  31. '<ul data-mage-init=\'{"menu":[]}\'>' +
  32. '<% _.each(data.items, function(value){ %>' +
  33. '<% if (!data.itemSelected(value)) { %><li <%= data.optionData(value) %>>' +
  34. '<a href="#"><%- value.label %></a></li><% } %>' +
  35. '<% }); %>' +
  36. '<% if (!data.term && !data.allShown() && data.recentShown()) { %>' +
  37. '<li data-mage-init=\'{"actionLink":{"event":"showAll"}}\' class="show-all">' +
  38. '<a href="#"><%- data.showAllTitle %></a></li>' +
  39. '<% } %>' +
  40. '</ul><% } else { %><span class="mage-suggest-no-records"><%- data.noRecordsText %></span><% } %>',
  41. minLength: 1,
  42. /**
  43. * @type {(String|Array)}
  44. */
  45. source: null,
  46. delay: 500,
  47. loadingClass: 'mage-suggest-state-loading',
  48. events: {},
  49. appendMethod: 'after',
  50. controls: {
  51. selector: ':ui-menu, :mage-menu',
  52. eventsMap: {
  53. focus: ['menufocus'],
  54. blur: ['menublur'],
  55. select: ['menuselect']
  56. }
  57. },
  58. termAjaxArgument: 'label_part',
  59. filterProperty: 'label',
  60. className: null,
  61. inputWrapper: '<div class="mage-suggest"><div class="mage-suggest-inner"></div></div>',
  62. dropdownWrapper: '<div class="mage-suggest-dropdown"></div>',
  63. preventClickPropagation: true,
  64. currentlySelected: null,
  65. submitInputOnEnter: true
  66. },
  67. /**
  68. * Component's constructor
  69. * @private
  70. */
  71. _create: function () {
  72. this._term = null;
  73. this._nonSelectedItem = {
  74. id: '',
  75. label: ''
  76. };
  77. this.templates = {};
  78. this._renderedContext = null;
  79. this._selectedItem = this._nonSelectedItem;
  80. this._control = this.options.controls || {};
  81. this._setTemplate();
  82. this._prepareValueField();
  83. this._render();
  84. this._bind();
  85. },
  86. /**
  87. * Render base elements for suggest component
  88. * @private
  89. */
  90. _render: function () {
  91. var wrapper;
  92. this.dropdown = $(this.options.dropdownWrapper).hide();
  93. wrapper = this.options.className ?
  94. $(this.options.inputWrapper).addClass(this.options.className) :
  95. $(this.options.inputWrapper);
  96. this.element
  97. .wrap(wrapper)[this.options.appendMethod](this.dropdown)
  98. .attr('autocomplete', 'off');
  99. },
  100. /**
  101. * Define a field for storing item id (find in DOM or create a new one)
  102. * @private
  103. */
  104. _prepareValueField: function () {
  105. if (this.options.valueField) {
  106. this.valueField = $(this.options.valueField);
  107. } else {
  108. this.valueField = this._createValueField()
  109. .insertBefore(this.element)
  110. .attr('name', this.element.attr('name'));
  111. this.element.removeAttr('name');
  112. }
  113. },
  114. /**
  115. * Create value field which keeps a id for selected option
  116. * can be overridden in descendants
  117. * @return {jQuery}
  118. * @private
  119. */
  120. _createValueField: function () {
  121. return $('<input/>', {
  122. type: 'hidden'
  123. });
  124. },
  125. /**
  126. * Component's destructor
  127. * @private
  128. */
  129. _destroy: function () {
  130. this.element
  131. .unwrap()
  132. .removeAttr('autocomplete');
  133. if (!this.options.valueField) {
  134. this.element.attr('name', this.valueField.attr('name'));
  135. this.valueField.remove();
  136. }
  137. this.dropdown.remove();
  138. this._off(this.element, 'keydown keyup blur');
  139. },
  140. /**
  141. * Return actual value of an "input"-element
  142. * @return {String}
  143. * @private
  144. */
  145. _value: function () {
  146. return $.trim(this.element[this.element.is(':input') ? 'val' : 'text']());
  147. },
  148. /**
  149. * Pass original event to a control component for handling it as it's own event
  150. * @param {Object} event - event object
  151. * @private
  152. */
  153. _proxyEvents: function (event) {
  154. var fakeEvent = $.extend({}, $.Event(event.type), {
  155. ctrlKey: event.ctrlKey,
  156. keyCode: event.keyCode,
  157. which: event.keyCode
  158. }),
  159. target = this._control.selector ? this.dropdown.find(this._control.selector) : this.dropdown;
  160. target.trigger(fakeEvent);
  161. },
  162. /**
  163. * Bind handlers on specific events
  164. * @private
  165. */
  166. _bind: function () {
  167. this._on($.extend({
  168. /**
  169. * @param {jQuery.Event} event
  170. */
  171. keydown: function (event) {
  172. var keyCode = $.ui.keyCode,
  173. suggestList,
  174. hasSuggestedItems,
  175. hasSelectedItems,
  176. selectedItem;
  177. switch (event.keyCode) {
  178. case keyCode.PAGE_UP:
  179. case keyCode.UP:
  180. if (!event.shiftKey) {
  181. event.preventDefault();
  182. this._proxyEvents(event);
  183. }
  184. suggestList = event.currentTarget.parentNode.getElementsByTagName('ul')[0];
  185. hasSuggestedItems = event.currentTarget
  186. .parentNode.getElementsByTagName('ul')[0].children.length >= 0;
  187. if (hasSuggestedItems) {
  188. selectedItem = $(suggestList.getElementsByClassName('_active')[0])
  189. .removeClass('_active').prev().addClass('_active');
  190. event.currentTarget.value = selectedItem.find('a').text();
  191. }
  192. break;
  193. case keyCode.PAGE_DOWN:
  194. case keyCode.DOWN:
  195. if (!event.shiftKey) {
  196. event.preventDefault();
  197. this._proxyEvents(event);
  198. }
  199. suggestList = event.currentTarget.parentNode.getElementsByTagName('ul')[0];
  200. hasSuggestedItems = event.currentTarget
  201. .parentNode.getElementsByTagName('ul')[0].children.length >= 0;
  202. if (hasSuggestedItems) {
  203. hasSelectedItems = suggestList.getElementsByClassName('_active').length === 0;
  204. if (hasSelectedItems) { //eslint-disable-line max-depth
  205. selectedItem = $(suggestList.children[0]).addClass('_active');
  206. event.currentTarget.value = selectedItem.find('a').text();
  207. } else {
  208. selectedItem = $(suggestList.getElementsByClassName('_active')[0])
  209. .removeClass('_active').next().addClass('_active');
  210. event.currentTarget.value = selectedItem.find('a').text();
  211. }
  212. }
  213. break;
  214. case keyCode.TAB:
  215. if (this.isDropdownShown()) {
  216. this._onSelectItem(event, null);
  217. event.preventDefault();
  218. }
  219. break;
  220. case keyCode.ENTER:
  221. case keyCode.NUMPAD_ENTER:
  222. this._toggleEnter(event);
  223. if (this.isDropdownShown() && this._focused) {
  224. this._proxyEvents(event);
  225. event.preventDefault();
  226. }
  227. break;
  228. case keyCode.ESCAPE:
  229. if (this.isDropdownShown()) {
  230. event.stopPropagation();
  231. }
  232. this.close(event);
  233. this._blurItem();
  234. break;
  235. }
  236. },
  237. /**
  238. * @param {jQuery.Event} event
  239. */
  240. keyup: function (event) {
  241. var keyCode = $.ui.keyCode;
  242. switch (event.keyCode) {
  243. case keyCode.HOME:
  244. case keyCode.END:
  245. case keyCode.PAGE_UP:
  246. case keyCode.PAGE_DOWN:
  247. case keyCode.ESCAPE:
  248. case keyCode.UP:
  249. case keyCode.DOWN:
  250. case keyCode.LEFT:
  251. case keyCode.RIGHT:
  252. case keyCode.TAB:
  253. break;
  254. case keyCode.ENTER:
  255. case keyCode.NUMPAD_ENTER:
  256. if (this.isDropdownShown()) {
  257. event.preventDefault();
  258. }
  259. break;
  260. default:
  261. this.search(event);
  262. }
  263. },
  264. /**
  265. * @param {jQuery.Event} event
  266. */
  267. blur: function (event) {
  268. if (!this.preventBlur) {
  269. this._abortSearch();
  270. this.close(event);
  271. this._change(event);
  272. } else {
  273. this.element.trigger('focus');
  274. }
  275. },
  276. cut: this.search,
  277. paste: this.search,
  278. input: this.search,
  279. selectItem: this._onSelectItem,
  280. click: this.search
  281. }, this.options.events));
  282. this._bindSubmit();
  283. this._bindDropdown();
  284. },
  285. /**
  286. * @param {Object} event
  287. * @private
  288. */
  289. _toggleEnter: function (event) {
  290. var suggestList,
  291. activeItems,
  292. selectedItem;
  293. if (!this.options.submitInputOnEnter) {
  294. event.preventDefault();
  295. }
  296. suggestList = $(event.currentTarget.parentNode).find('ul').first();
  297. activeItems = suggestList.find('._active');
  298. if (activeItems.length >= 0) {
  299. selectedItem = activeItems.first();
  300. if (selectedItem.find('a') && selectedItem.find('a').attr('href') !== undefined) {
  301. window.location = selectedItem.find('a').attr('href');
  302. event.preventDefault();
  303. }
  304. }
  305. },
  306. /**
  307. * Bind handlers for submit on enter
  308. * @private
  309. */
  310. _bindSubmit: function () {
  311. this.element.parents('form').on('submit', function (event) {
  312. if (!this.submitInputOnEnter) {
  313. event.preventDefault();
  314. }
  315. });
  316. },
  317. /**
  318. * @param {Object} e - event object
  319. * @private
  320. */
  321. _change: function (e) {
  322. if (this._term !== this._value()) {
  323. this._trigger('change', e);
  324. }
  325. },
  326. /**
  327. * Bind handlers for dropdown element on specific events
  328. * @private
  329. */
  330. _bindDropdown: function () {
  331. var events = {
  332. /**
  333. * @param {jQuery.Event} e
  334. */
  335. click: function (e) {
  336. // prevent default browser's behavior of changing location by anchor href
  337. e.preventDefault();
  338. },
  339. /**
  340. * @param {jQuery.Event} e
  341. */
  342. mousedown: function (e) {
  343. e.preventDefault();
  344. }
  345. };
  346. $.each(this._control.eventsMap, $.proxy(function (suggestEvent, controlEvents) {
  347. $.each(controlEvents, $.proxy(function (i, handlerName) {
  348. switch (suggestEvent) {
  349. case 'select':
  350. events[handlerName] = this._onSelectItem;
  351. break;
  352. case 'focus':
  353. events[handlerName] = this._focusItem;
  354. break;
  355. case 'blur':
  356. events[handlerName] = this._blurItem;
  357. break;
  358. }
  359. }, this));
  360. }, this));
  361. if (this.options.preventClickPropagation) {
  362. this._on(this.dropdown, events);
  363. }
  364. // Fix for IE 8
  365. this._on(this.dropdown, {
  366. /**
  367. * Mousedown.
  368. */
  369. mousedown: function () {
  370. this.preventBlur = true;
  371. },
  372. /**
  373. * Mouseup.
  374. */
  375. mouseup: function () {
  376. this.preventBlur = false;
  377. }
  378. });
  379. },
  380. /**
  381. * @override
  382. */
  383. _trigger: function (type, event) {
  384. var result = this._superApply(arguments);
  385. if (result === false && event) {
  386. event.stopImmediatePropagation();
  387. event.preventDefault();
  388. }
  389. return result;
  390. },
  391. /**
  392. * Handle focus event of options item
  393. * @param {Object} e - event object
  394. * @param {Object} ui - object that can contain information about focused item
  395. * @private
  396. */
  397. _focusItem: function (e, ui) {
  398. if (ui && ui.item) {
  399. this._focused = $(ui.item).prop('tagName') ?
  400. this._readItemData(ui.item) :
  401. ui.item;
  402. this.element.val(this._focused.label);
  403. this._trigger('focus', e, {
  404. item: this._focused
  405. });
  406. }
  407. },
  408. /**
  409. * Handle blur event of options item
  410. * @private
  411. */
  412. _blurItem: function () {
  413. this._focused = null;
  414. this.element.val(this._term);
  415. },
  416. /**
  417. * @param {Object} e - event object
  418. * @param {Object} item
  419. * @private
  420. */
  421. _onSelectItem: function (e, item) {
  422. if (item && $.type(item) === 'object' && $(e.target).is(this.element)) {
  423. this._focusItem(e, {
  424. item: item
  425. });
  426. }
  427. if (this._trigger('beforeselect', e || null, {
  428. item: this._focused
  429. }) === false) {
  430. return;
  431. }
  432. this._selectItem(e);
  433. this._blurItem();
  434. this._trigger('select', e || null, {
  435. item: this._selectedItem
  436. });
  437. },
  438. /**
  439. * Save selected item and hide dropdown
  440. * @private
  441. * @param {Object} e - event object
  442. */
  443. _selectItem: function (e) {
  444. if (this._focused) {
  445. this._selectedItem = this._focused;
  446. if (this._selectedItem !== this._nonSelectedItem) {
  447. this._term = this._selectedItem.label;
  448. this.valueField.val(this._selectedItem.id);
  449. this.close(e);
  450. }
  451. }
  452. },
  453. /**
  454. * Read option data from item element
  455. * @param {Element} element
  456. * @return {Object}
  457. * @private
  458. */
  459. _readItemData: function (element) {
  460. return element.data('suggestOption') || this._nonSelectedItem;
  461. },
  462. /**
  463. * Check if dropdown is shown
  464. * @return {Boolean}
  465. */
  466. isDropdownShown: function () {
  467. return this.dropdown.is(':visible');
  468. },
  469. /**
  470. * Open dropdown
  471. * @private
  472. * @param {Object} e - event object
  473. */
  474. open: function (e) {
  475. if (!this.isDropdownShown()) {
  476. this.element.addClass('_suggest-dropdown-open');
  477. this.dropdown.show();
  478. this._trigger('open', e);
  479. }
  480. },
  481. /**
  482. * Close and clear dropdown content
  483. * @private
  484. * @param {Object} e - event object
  485. */
  486. close: function (e) {
  487. this._renderedContext = null;
  488. if (this.dropdown.length) {
  489. this.element.removeClass('_suggest-dropdown-open');
  490. this.dropdown.hide().empty();
  491. }
  492. this._trigger('close', e);
  493. },
  494. /**
  495. * Acquire content template
  496. * @private
  497. */
  498. _setTemplate: function () {
  499. this.templateName = 'suggest' + Math.random().toString(36).substr(2);
  500. this.templates[this.templateName] = mageTemplate(this.options.template);
  501. },
  502. /**
  503. * Execute search process
  504. * @public
  505. * @param {Object} e - event object
  506. */
  507. search: function (e) {
  508. var term = this._value();
  509. if ((this._term !== term || term.length === 0) && !this.preventBlur) {
  510. this._term = term;
  511. if ($.type(term) === 'string' && term.length >= this.options.minLength) {
  512. if (this._trigger('search', e) === false) { //eslint-disable-line max-depth
  513. return;
  514. }
  515. this._search(e, term, {});
  516. } else {
  517. this._selectedItem = this._nonSelectedItem;
  518. this._resetSuggestValue();
  519. }
  520. }
  521. },
  522. /**
  523. * Clear suggest hidden input
  524. * @private
  525. */
  526. _resetSuggestValue: function () {
  527. this.valueField.val(this._nonSelectedItem.id);
  528. },
  529. /**
  530. * Actual search method, can be overridden in descendants
  531. * @param {Object} e - event object
  532. * @param {String} term - search phrase
  533. * @param {Object} context - search context
  534. * @private
  535. */
  536. _search: function (e, term, context) {
  537. var response = $.proxy(function (items) {
  538. return this._processResponse(e, items, context || {});
  539. }, this);
  540. this.element.addClass(this.options.loadingClass);
  541. if (this.options.delay) {
  542. if ($.type(this.options.data) !== 'undefined') {
  543. response(this.filter(this.options.data, term));
  544. }
  545. clearTimeout(this._searchTimeout);
  546. this._searchTimeout = this._delay(function () {
  547. this._source(term, response);
  548. }, this.options.delay);
  549. } else {
  550. this._source(term, response);
  551. }
  552. },
  553. /**
  554. * Extend basic context with additional data (search results, search term)
  555. * @param {Object} context
  556. * @return {Object}
  557. * @private
  558. */
  559. _prepareDropdownContext: function (context) {
  560. return $.extend(context, {
  561. items: this._items,
  562. term: this._term,
  563. /**
  564. * @param {Object} item
  565. * @return {String}
  566. */
  567. optionData: function (item) {
  568. return 'data-suggest-option="' +
  569. $('<div>').text(JSON.stringify(item)).html().replace(/"/g, '&quot;') + '"';
  570. },
  571. itemSelected: $.proxy(this._isItemSelected, this),
  572. noRecordsText: $.mage.__('No records found.')
  573. });
  574. },
  575. /**
  576. * @param {Object} item
  577. * @return {Boolean}
  578. * @private
  579. */
  580. _isItemSelected: function (item) {
  581. return item.id == (this._selectedItem && this._selectedItem.id ? //eslint-disable-line eqeqeq
  582. this._selectedItem.id :
  583. this.options.currentlySelected);
  584. },
  585. /**
  586. * Render content of suggest's dropdown
  587. * @param {Object} e - event object
  588. * @param {Array} items - list of label+id objects
  589. * @param {Object} context - template's context
  590. * @private
  591. */
  592. _renderDropdown: function (e, items, context) {
  593. var tmpl = this.templates[this.templateName];
  594. this._items = items;
  595. tmpl = tmpl({
  596. data: this._prepareDropdownContext(context)
  597. });
  598. $(tmpl).appendTo(this.dropdown.empty());
  599. this.dropdown.trigger('contentUpdated')
  600. .find(this._control.selector).on('focus', function (event) {
  601. event.preventDefault();
  602. });
  603. this._renderedContext = context;
  604. this.element.removeClass(this.options.loadingClass);
  605. this.open(e);
  606. },
  607. /**
  608. * @param {Object} e
  609. * @param {Object} items
  610. * @param {Object} context
  611. * @private
  612. */
  613. _processResponse: function (e, items, context) {
  614. var renderer = $.proxy(function (i) {
  615. return this._renderDropdown(e, i, context || {});
  616. }, this);
  617. if (this._trigger('response', e, [items, renderer]) === false) {
  618. return;
  619. }
  620. this._renderDropdown(e, items, context);
  621. },
  622. /**
  623. * Implement search process via spesific source
  624. * @param {String} term - search phrase
  625. * @param {Function} response - search results handler, process search result
  626. * @private
  627. */
  628. _source: function (term, response) {
  629. var o = this.options,
  630. ajaxData;
  631. if ($.isArray(o.source)) {
  632. response(this.filter(o.source, term));
  633. } else if ($.type(o.source) === 'string') {
  634. ajaxData = {};
  635. ajaxData[this.options.termAjaxArgument] = term;
  636. this._xhr = $.ajax($.extend(true, {
  637. url: o.source,
  638. type: 'POST',
  639. dataType: 'json',
  640. data: ajaxData,
  641. success: $.proxy(function (items) {
  642. this.options.data = items;
  643. response.apply(response, arguments);
  644. }, this)
  645. }, o.ajaxOptions || {}));
  646. } else if ($.type(o.source) === 'function') {
  647. o.source.apply(o.source, arguments);
  648. }
  649. },
  650. /**
  651. * Abort search process
  652. * @private
  653. */
  654. _abortSearch: function () {
  655. this.element.removeClass(this.options.loadingClass);
  656. clearTimeout(this._searchTimeout);
  657. },
  658. /**
  659. * Perform filtering in advance loaded items and returns search result
  660. * @param {Array} items - all available items
  661. * @param {String} term - search phrase
  662. * @return {Object}
  663. */
  664. filter: function (items, term) {
  665. var matcher = new RegExp(term.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&'), 'i'),
  666. itemsArray = $.isArray(items) ? items : $.map(items, function (element) {
  667. return element;
  668. }),
  669. property = this.options.filterProperty;
  670. return $.grep(
  671. itemsArray,
  672. function (value) {
  673. return matcher.test(value[property] || value.id || value);
  674. }
  675. );
  676. }
  677. });
  678. /**
  679. * Implement show all functionality and storing and display recent searches
  680. */
  681. $.widget('mage.suggest', $.mage.suggest, {
  682. options: {
  683. showRecent: false,
  684. showAll: false,
  685. storageKey: 'suggest',
  686. storageLimit: 10
  687. },
  688. /**
  689. * @override
  690. */
  691. _create: function () {
  692. var recentItems;
  693. if (this.options.showRecent && window.localStorage) {
  694. recentItems = JSON.parse(localStorage.getItem(this.options.storageKey));
  695. /**
  696. * @type {Array} - list of recently searched items
  697. * @private
  698. */
  699. this._recentItems = $.isArray(recentItems) ? recentItems : [];
  700. }
  701. this._super();
  702. },
  703. /**
  704. * @override
  705. */
  706. _bind: function () {
  707. this._super();
  708. this._on(this.dropdown, {
  709. /**
  710. * @param {jQuery.Event} e
  711. */
  712. showAll: function (e) {
  713. e.stopImmediatePropagation();
  714. e.preventDefault();
  715. this.element.trigger('showAll');
  716. }
  717. });
  718. if (this.options.showRecent || this.options.showAll) {
  719. this._on({
  720. /**
  721. * @param {jQuery.Event} e
  722. */
  723. focus: function (e) {
  724. if (!this.isDropdownShown()) {
  725. this.search(e);
  726. }
  727. },
  728. showAll: this._showAll
  729. });
  730. }
  731. },
  732. /**
  733. * @private
  734. * @param {Object} e - event object
  735. */
  736. _showAll: function (e) {
  737. this._abortSearch();
  738. this._search(e, '', {
  739. _allShown: true
  740. });
  741. },
  742. /**
  743. * @override
  744. */
  745. search: function (e) {
  746. if (!this._value()) {
  747. if (this.options.showRecent) {
  748. if (this._recentItems.length) { //eslint-disable-line max-depth
  749. this._processResponse(e, this._recentItems, {});
  750. } else {
  751. this._showAll(e);
  752. }
  753. } else if (this.options.showAll) {
  754. this._showAll(e);
  755. }
  756. }
  757. this._superApply(arguments);
  758. },
  759. /**
  760. * @override
  761. */
  762. _selectItem: function () {
  763. this._superApply(arguments);
  764. if (this._selectedItem && this._selectedItem.id && this.options.showRecent) {
  765. this._addRecent(this._selectedItem);
  766. }
  767. },
  768. /**
  769. * @override
  770. */
  771. _prepareDropdownContext: function () {
  772. var context = this._superApply(arguments);
  773. return $.extend(context, {
  774. recentShown: $.proxy(function () {
  775. return this.options.showRecent;
  776. }, this),
  777. recentTitle: $.mage.__('Recent items'),
  778. showAllTitle: $.mage.__('Show all...'),
  779. /**
  780. * @return {Boolean}
  781. */
  782. allShown: function () {
  783. return !!context._allShown;
  784. }
  785. });
  786. },
  787. /**
  788. * Add selected item of search result into storage of recents
  789. * @param {Object} item - label+id object
  790. * @private
  791. */
  792. _addRecent: function (item) {
  793. this._recentItems = $.grep(this._recentItems, function (obj) {
  794. return obj.id !== item.id;
  795. });
  796. this._recentItems.unshift(item);
  797. this._recentItems = this._recentItems.slice(0, this.options.storageLimit);
  798. localStorage.setItem(this.options.storageKey, JSON.stringify(this._recentItems));
  799. }
  800. });
  801. /**
  802. * Implement multi suggest functionality
  803. */
  804. $.widget('mage.suggest', $.mage.suggest, {
  805. options: {
  806. multiSuggestWrapper: '<ul class="mage-suggest-choices">' +
  807. '<li class="mage-suggest-search-field" data-role="parent-choice-element"><' +
  808. 'label class="mage-suggest-search-label"></label></li></ul>',
  809. choiceTemplate: '<li class="mage-suggest-choice button"><div><%- text %></div>' +
  810. '<span class="mage-suggest-choice-close" tabindex="-1" ' +
  811. 'data-mage-init=\'{"actionLink":{"event":"removeOption"}}\'></span></li>',
  812. selectedClass: 'mage-suggest-selected'
  813. },
  814. /**
  815. * @override
  816. */
  817. _create: function () {
  818. this.choiceTmpl = mageTemplate(this.options.choiceTemplate);
  819. this._super();
  820. if (this.options.multiselect) {
  821. this.valueField.hide();
  822. }
  823. },
  824. /**
  825. * @override
  826. */
  827. _render: function () {
  828. this._super();
  829. if (this.options.multiselect) {
  830. this._renderMultiselect();
  831. }
  832. },
  833. /**
  834. * Render selected options
  835. * @private
  836. */
  837. _renderMultiselect: function () {
  838. var that = this;
  839. this.element.wrap(this.options.multiSuggestWrapper);
  840. this.elementWrapper = this.element.closest('[data-role="parent-choice-element"]');
  841. $(function () {
  842. that._getOptions()
  843. .each(function (i, option) {
  844. option = $(option);
  845. that._createOption({
  846. id: option.val(),
  847. label: option.text()
  848. });
  849. });
  850. });
  851. },
  852. /**
  853. * @return {Array} array of DOM-elements
  854. * @private
  855. */
  856. _getOptions: function () {
  857. return this.valueField.find('option');
  858. },
  859. /**
  860. * @override
  861. */
  862. _bind: function () {
  863. this._super();
  864. if (this.options.multiselect) {
  865. this._on({
  866. /**
  867. * @param {jQuery.Event} event
  868. */
  869. keydown: function (event) {
  870. if (event.keyCode === $.ui.keyCode.BACKSPACE) {
  871. if (!this._value()) {
  872. this._removeLastAdded(event);
  873. }
  874. }
  875. },
  876. removeOption: this.removeOption
  877. });
  878. }
  879. },
  880. /**
  881. * @param {Array} items
  882. * @return {Array}
  883. * @private
  884. */
  885. _filterSelected: function (items) {
  886. var options = this._getOptions();
  887. return $.grep(items, function (value) {
  888. var itemSelected = false;
  889. $.each(options, function () {
  890. if (value.id == $(this).val()) { //eslint-disable-line eqeqeq
  891. itemSelected = true;
  892. }
  893. });
  894. return !itemSelected;
  895. });
  896. },
  897. /**
  898. * @override
  899. */
  900. _processResponse: function (e, items, context) {
  901. if (this.options.multiselect) {
  902. items = this._filterSelected(items, context);
  903. }
  904. this._superApply([e, items, context]);
  905. },
  906. /**
  907. * @override
  908. */
  909. _prepareValueField: function () {
  910. this._super();
  911. if (this.options.multiselect && !this.options.valueField && this.options.selectedItems) {
  912. $.each(this.options.selectedItems, $.proxy(function (i, item) {
  913. this._addOption(item);
  914. }, this));
  915. }
  916. },
  917. /**
  918. * If "multiselect" option is set, then do not need to clear value for hidden select, to avoid losing of
  919. * previously selected items
  920. * @override
  921. */
  922. _resetSuggestValue: function () {
  923. if (!this.options.multiselect) {
  924. this._super();
  925. }
  926. },
  927. /**
  928. * @override
  929. */
  930. _createValueField: function () {
  931. if (this.options.multiselect) {
  932. return $('<select/>', {
  933. type: 'hidden',
  934. multiple: 'multiple'
  935. });
  936. }
  937. return this._super();
  938. },
  939. /**
  940. * @override
  941. */
  942. _selectItem: function (e) {
  943. if (this.options.multiselect) {
  944. if (this._focused) {
  945. this._selectedItem = this._focused;
  946. /* eslint-disable max-depth */
  947. if (this._selectedItem !== this._nonSelectedItem) {
  948. this._term = '';
  949. this.element.val(this._term);
  950. if (this._isItemSelected(this._selectedItem)) {
  951. $(e.target).removeClass(this.options.selectedClass);
  952. this.removeOption(e, this._selectedItem);
  953. this._selectedItem = this._nonSelectedItem;
  954. } else {
  955. $(e.target).addClass(this.options.selectedClass);
  956. this._addOption(e, this._selectedItem);
  957. }
  958. }
  959. /* eslint-enable max-depth */
  960. }
  961. this.close(e);
  962. } else {
  963. this._superApply(arguments);
  964. }
  965. },
  966. /**
  967. * @override
  968. */
  969. _isItemSelected: function (item) {
  970. if (this.options.multiselect) {
  971. return this.valueField.find('option[value=' + item.id + ']').length > 0;
  972. }
  973. return this._superApply(arguments);
  974. },
  975. /**
  976. *
  977. * @param {Object} item
  978. * @return {Element}
  979. * @private
  980. */
  981. _createOption: function (item) {
  982. var option = this._getOption(item);
  983. if (!option.length) {
  984. option = $('<option>', {
  985. value: item.id,
  986. selected: true
  987. }).text(item.label);
  988. }
  989. return option.data('renderedOption', this._renderOption(item));
  990. },
  991. /**
  992. * Add selected item in to select options
  993. * @param {Object} e - event object
  994. * @param {*} item
  995. * @private
  996. */
  997. _addOption: function (e, item) {
  998. this.valueField.append(this._createOption(item).data('selectTarget', $(e.target)));
  999. },
  1000. /**
  1001. * @param {Object|Element} item
  1002. * @return {Element}
  1003. * @private
  1004. */
  1005. _getOption: function (item) {
  1006. return $(item).prop('tagName') ?
  1007. $(item) :
  1008. this.valueField.find('option[value=' + item.id + ']');
  1009. },
  1010. /**
  1011. * Remove last added option
  1012. * @private
  1013. * @param {Object} e - event object
  1014. */
  1015. _removeLastAdded: function (e) {
  1016. var lastAdded = this._getOptions().last();
  1017. if (lastAdded.length) {
  1018. this.removeOption(e, lastAdded);
  1019. }
  1020. },
  1021. /**
  1022. * Remove item from select options
  1023. * @param {Object} e - event object
  1024. * @param {Object} item
  1025. * @private
  1026. */
  1027. removeOption: function (e, item) {
  1028. var option = this._getOption(item),
  1029. selectTarget = option.data('selectTarget');
  1030. if (selectTarget && selectTarget.length) {
  1031. selectTarget.removeClass(this.options.selectedClass);
  1032. }
  1033. option.data('renderedOption').remove();
  1034. option.remove();
  1035. },
  1036. /**
  1037. * Render visual element of selected item
  1038. * @param {Object} item - selected item
  1039. * @private
  1040. */
  1041. _renderOption: function (item) {
  1042. var tmpl = this.choiceTmpl({
  1043. text: item.label
  1044. });
  1045. return $(tmpl)
  1046. .insertBefore(this.elementWrapper)
  1047. .trigger('contentUpdated')
  1048. .on('removeOption', $.proxy(function (e) {
  1049. this.removeOption(e, item);
  1050. }, this));
  1051. }
  1052. });
  1053. return $.mage.suggest;
  1054. }));