Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 
 
 

1192 wiersze
37 KiB

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