multiselect.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. /**
  2. * Copyright © Magento, Inc. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. define([
  6. 'underscore',
  7. 'jquery',
  8. 'text!mage/multiselect.html',
  9. 'Magento_Ui/js/modal/alert',
  10. 'jquery/ui',
  11. 'jquery/editableMultiselect/js/jquery.multiselect'
  12. ], function (_, $, searchTemplate, alert) {
  13. 'use strict';
  14. $.widget('mage.multiselect2', {
  15. options: {
  16. mselectContainer: 'section.mselect-list',
  17. mselectItemsWrapperClass: 'mselect-items-wrapper',
  18. mselectCheckedClass: 'mselect-checked',
  19. containerClass: 'paginated',
  20. searchInputClass: 'admin__action-multiselect-search',
  21. selectedItemsCountClass: 'admin__action-multiselect-items-selected',
  22. currentPage: 1,
  23. lastAppendValue: 0,
  24. updateDelay: 1000,
  25. optionsLoaded: false
  26. },
  27. /** @inheritdoc */
  28. _create: function () {
  29. $.fn.multiselect.call(this.element, this.options);
  30. },
  31. /** @inheritdoc */
  32. _init: function () {
  33. this.domElement = this.element.get(0);
  34. this.$container = $(this.options.mselectContainer);
  35. this.$wrapper = this.$container.find('.' + this.options.mselectItemsWrapperClass);
  36. this.$item = this.$wrapper.find('div').first();
  37. this.selectedValues = [];
  38. this.values = {};
  39. this.$container.addClass(this.options.containerClass).prepend(searchTemplate);
  40. this.$input = this.$container.find('.' + this.options.searchInputClass);
  41. this.$selectedCounter = this.$container.find('.' + this.options.selectedItemsCountClass);
  42. this.filter = '';
  43. if (this.domElement.options.length) {
  44. this._setLastAppendOption(this.domElement.options[this.domElement.options.length - 1].value);
  45. }
  46. this._initElement();
  47. this._events();
  48. },
  49. /**
  50. * Leave only saved/selected options in select element.
  51. *
  52. * @private
  53. */
  54. _initElement: function () {
  55. this.element.empty();
  56. _.each(this.options.selectedValues, function (value) {
  57. this._createSelectedOption({
  58. value: value,
  59. label: value
  60. });
  61. }, this);
  62. },
  63. /**
  64. * Attach required events.
  65. *
  66. * @private
  67. */
  68. _events: function () {
  69. var onKeyUp = _.debounce(this.onKeyUp, this.options.updateDelay);
  70. _.bindAll(this, 'onScroll', 'onCheck', 'onOptionsChange');
  71. this.$wrapper.on('scroll', this.onScroll);
  72. this.$wrapper.on('change.mselectCheck', '[type=checkbox]', this.onCheck);
  73. this.$input.on('keyup', _.bind(onKeyUp, this));
  74. this.element.on('change.hiddenSelect', this.onOptionsChange);
  75. },
  76. /**
  77. * Behaves multiselect scroll.
  78. */
  79. onScroll: function () {
  80. var height = this.$wrapper.height(),
  81. scrollHeight = this.$wrapper.prop('scrollHeight'),
  82. scrollTop = Math.ceil(this.$wrapper.prop('scrollTop'));
  83. if (!this.options.optionsLoaded && scrollHeight - height <= scrollTop) {
  84. this.loadOptions();
  85. }
  86. },
  87. /**
  88. * Behaves keyup event on input search
  89. */
  90. onKeyUp: function () {
  91. if (this.getSearchCriteria() === this.filter) {
  92. return false;
  93. }
  94. this.setFilter();
  95. this.clearMultiselectOptions();
  96. this.setCurrentPage(0);
  97. this.loadOptions();
  98. },
  99. /**
  100. * Callback for select change event
  101. */
  102. onOptionsChange: function () {
  103. this.selectedValues = _.map(this.domElement.options, function (option) {
  104. this.values[option.value] = true;
  105. return option.value;
  106. }, this);
  107. this._updateSelectedCounter();
  108. },
  109. /**
  110. * Overrides native check behaviour.
  111. *
  112. * @param {Event} event
  113. */
  114. onCheck: function (event) {
  115. var checkbox = event.target,
  116. option = {
  117. value: checkbox.value,
  118. label: $(checkbox).parent('label').text()
  119. };
  120. checkbox.checked ? this._createSelectedOption(option) : this._removeSelectedOption(option);
  121. event.stopPropagation();
  122. },
  123. /**
  124. * Show error message.
  125. *
  126. * @param {String} message
  127. */
  128. onError: function (message) {
  129. alert({
  130. content: message
  131. });
  132. },
  133. /**
  134. * Updates current filter state.
  135. */
  136. setFilter: function () {
  137. this.filter = this.getSearchCriteria() || '';
  138. },
  139. /**
  140. * Reads search input value.
  141. *
  142. * @return {String}
  143. */
  144. getSearchCriteria: function () {
  145. return $.trim(this.$input.val());
  146. },
  147. /**
  148. * Load options data.
  149. */
  150. loadOptions: function () {
  151. var nextPage = this.getCurrentPage() + 1;
  152. this.$wrapper.trigger('processStart');
  153. this.$input.prop('disabled', true);
  154. $.get(this.options.nextPageUrl, {
  155. p: nextPage,
  156. s: this.filter
  157. })
  158. .done(function (response) {
  159. if (response.success) {
  160. this.appendOptions(response.result);
  161. this.setCurrentPage(nextPage);
  162. } else {
  163. this.onError(response.errorMessage);
  164. }
  165. }.bind(this))
  166. .always(function () {
  167. this.$wrapper.trigger('processStop');
  168. this.$input.prop('disabled', false);
  169. if (this.filter) {
  170. this.$input.focus();
  171. }
  172. }.bind(this));
  173. },
  174. /**
  175. * Append loaded options
  176. *
  177. * @param {Array} options
  178. */
  179. appendOptions: function (options) {
  180. var divOptions = [];
  181. if (!options.length) {
  182. return false;
  183. }
  184. if (this.isOptionsLoaded(options)) {
  185. return;
  186. }
  187. options.forEach(function (option) {
  188. if (!this.values[option.value]) {
  189. this.values[option.value] = true;
  190. option.selected = this._isOptionSelected(option);
  191. divOptions.push(this._createMultiSelectOption(option));
  192. this._setLastAppendOption(option.value);
  193. }
  194. }, this);
  195. this.$wrapper.append(divOptions);
  196. },
  197. /**
  198. * Clear multiselect options
  199. */
  200. clearMultiselectOptions: function () {
  201. this._setLastAppendOption(0);
  202. this.values = {};
  203. this.$wrapper.empty();
  204. },
  205. /**
  206. * Checks if all options are already loaded
  207. *
  208. * @return {Boolean}
  209. */
  210. isOptionsLoaded: function (options) {
  211. this.options.optionsLoaded = this.options.lastAppendValue === options[options.length - 1].value;
  212. return this.options.optionsLoaded;
  213. },
  214. /**
  215. * Setter for current page.
  216. *
  217. * @param {Number} page
  218. */
  219. setCurrentPage: function (page) {
  220. this.options.currentPage = page;
  221. },
  222. /**
  223. * Getter for current page.
  224. *
  225. * @return {Number}
  226. */
  227. getCurrentPage: function () {
  228. return this.options.currentPage;
  229. },
  230. /**
  231. * Creates new selected option for select element
  232. *
  233. * @param {Object} option - option object
  234. * @param {String} option.value - option value
  235. * @param {String} option.label - option label
  236. * @private
  237. */
  238. _createSelectedOption: function (option) {
  239. var selectOption = new Option(option.label, option.value, false, true);
  240. this.element.append(selectOption);
  241. this.selectedValues.push(option.value);
  242. this._updateSelectedCounter();
  243. return selectOption;
  244. },
  245. /**
  246. * Remove passed option from select element
  247. *
  248. * @param {Object} option - option object
  249. * @param {String} option.value - option value
  250. * @param {String} option.label - option label
  251. * @return {Object} option
  252. * @private
  253. */
  254. _removeSelectedOption: function (option) {
  255. var unselectedOption = _.findWhere(this.domElement.options, {
  256. value: option.value
  257. });
  258. if (!_.isUndefined(unselectedOption)) {
  259. this.domElement.remove(unselectedOption.index);
  260. this.selectedValues.splice(_.indexOf(this.selectedValues, option.value), 1);
  261. this._updateSelectedCounter();
  262. }
  263. return unselectedOption;
  264. },
  265. /**
  266. * Creates new DIV option for multiselect widget
  267. *
  268. * @param {Object} option - option object
  269. * @param {String} option.value - option value
  270. * @param {String} option.label - option label
  271. * @param {Boolean} option.selected - is option selected
  272. * @private
  273. */
  274. _createMultiSelectOption: function (option) {
  275. var item = this.$item.clone(),
  276. checkbox = item.find('input'),
  277. isSelected = !!option.selected;
  278. checkbox.val(option.value)
  279. .prop('checked', isSelected)
  280. .toggleClass(this.options.mselectCheckedClass, isSelected);
  281. item.find('label > span').text(option.label);
  282. return item;
  283. },
  284. /**
  285. * Checks if passed option should be selected
  286. *
  287. * @param {Object} option - option object
  288. * @param {String} option.value - option value
  289. * @param {String} option.label - option label
  290. * @param {Boolean} option.selected - is option selected
  291. * @return {Boolean}
  292. * @private
  293. */
  294. _isOptionSelected: function (option) {
  295. return !!~this.selectedValues.indexOf(option.value);
  296. },
  297. /**
  298. * Saves last added option value.
  299. *
  300. * @param {Number} value
  301. * @private
  302. */
  303. _setLastAppendOption: function (value) {
  304. this.options.lastAppendValue = value;
  305. },
  306. /**
  307. * Updates counter of selected items.
  308. *
  309. * @private
  310. */
  311. _updateSelectedCounter: function () {
  312. this.$selectedCounter.text(this.selectedValues.length);
  313. }
  314. });
  315. return $.mage.multiselect2;
  316. });