select.js

  1. /* eslint-disable no-return-assign */
  2. import { queryOne } from '@ecl/dom-utils';
  3. import getSystem from '@ecl/builder/utils/getSystem';
  4. import EventManager from '@ecl/event-manager';
  5. import iconSvgAllCheckEc from '@ecl/resources-ec-icons/dist/svg/all/check.svg';
  6. import iconSvgAllCheckEu from '@ecl/resources-eu-icons/dist/svg/all/check.svg';
  7. import iconSvgAllCornerArrowEc from '@ecl/resources-ec-icons/dist/svg/all/corner-arrow.svg';
  8. import iconSvgAllCornerArrowEu from '@ecl/resources-eu-icons/dist/svg/all/corner-arrow.svg';
  9. const system = getSystem();
  10. const iconSvgAllCheck = system === 'eu' ? iconSvgAllCheckEu : iconSvgAllCheckEc;
  11. const iconSvgAllCornerArrow =
  12. system === 'eu' ? iconSvgAllCornerArrowEu : iconSvgAllCornerArrowEc;
  13. const iconSize = system === 'eu' ? 's' : 'xs';
  14. /**
  15. * This API mostly refers to the multiple select, in the default select only two methods are actually used:
  16. * handleKeyboardOnSelect() and handleOptgroup().
  17. *
  18. * For the multiple select there are multiple labels contained in this component. You can set them in 2 ways:
  19. * directly as a string or through data attributes.
  20. * Textual values have precedence and if they are not provided, then DOM data attributes are used.
  21. *
  22. * @param {HTMLElement} element DOM element for component instantiation and scope
  23. * @param {Object} options
  24. * @param {String} options.defaultText The default placeholder
  25. * @param {String} options.searchText The label for search
  26. * @param {String} options.selectAllText The label for select all
  27. * @param {String} options.selectMultipleSelector The data attribute selector of the select multiple
  28. * @param {String} options.defaultTextAttribute The data attribute for the default placeholder text
  29. * @param {String} options.searchTextAttribute The data attribute for the default search text
  30. * @param {String} options.selectAllTextAttribute The data attribute for the select all text
  31. * @param {String} options.noResultsTextAttribute The data attribute for the no results options text
  32. * @param {String} options.closeLabelAttribute The data attribute for the close button
  33. * @param {String} options.clearAllLabelAttribute The data attribute for the clear all button
  34. * @param {String} options.selectMultiplesSelectionCountSelector The selector for the counter of selected options
  35. * @param {String} options.closeButtonLabel The label of the close button
  36. * @param {String} options.clearAllButtonLabel The label of the clear all button
  37. */
  38. export class Select {
  39. /**
  40. * @static
  41. * Shorthand for instance creation and initialisation.
  42. *
  43. * @param {HTMLElement} root DOM element for component instantiation and scope
  44. *
  45. * @return {Select} An instance of Select.
  46. */
  47. static autoInit(root, defaultOptions = {}) {
  48. const select = new Select(root, defaultOptions);
  49. select.init();
  50. root.ECLSelect = select;
  51. return select;
  52. }
  53. /**
  54. * @event Select#onToggle
  55. */
  56. /**
  57. * @event Select#onSelection
  58. */
  59. /**
  60. * @event Select#onSelectAll
  61. */
  62. /**
  63. * @event Select#onReset
  64. */
  65. /**
  66. * @event Select#onSearch
  67. *
  68. */
  69. supportedEvents = [
  70. 'onToggle',
  71. 'onSelection',
  72. 'onSelectAll',
  73. 'onReset',
  74. 'onSearch',
  75. ];
  76. constructor(
  77. element,
  78. {
  79. defaultText = '',
  80. searchText = '',
  81. selectAllText = '',
  82. noResultsText = '',
  83. selectMultipleSelector = '[data-ecl-select-multiple]',
  84. defaultTextAttribute = 'data-ecl-select-default',
  85. searchTextAttribute = 'data-ecl-select-search',
  86. selectAllTextAttribute = 'data-ecl-select-all',
  87. noResultsTextAttribute = 'data-ecl-select-no-results',
  88. closeLabelAttribute = 'data-ecl-select-close',
  89. clearAllLabelAttribute = 'data-ecl-select-clear-all',
  90. selectMultiplesSelectionCountSelector = 'ecl-select-multiple-selections-counter',
  91. closeButtonLabel = '',
  92. clearAllButtonLabel = '',
  93. } = {},
  94. ) {
  95. // Check element
  96. if (!element || element.nodeType !== Node.ELEMENT_NODE) {
  97. throw new TypeError(
  98. 'DOM element should be given to initialize this widget.',
  99. );
  100. }
  101. this.element = element;
  102. this.eventManager = new EventManager();
  103. // Options
  104. this.selectMultipleSelector = selectMultipleSelector;
  105. this.selectMultiplesSelectionCountSelector =
  106. selectMultiplesSelectionCountSelector;
  107. this.defaultTextAttribute = defaultTextAttribute;
  108. this.searchTextAttribute = searchTextAttribute;
  109. this.selectAllTextAttribute = selectAllTextAttribute;
  110. this.noResultsTextAttribute = noResultsTextAttribute;
  111. this.defaultText = defaultText;
  112. this.searchText = searchText;
  113. this.selectAllText = selectAllText;
  114. this.noResultsText = noResultsText;
  115. this.clearAllButtonLabel = clearAllButtonLabel;
  116. this.closeButtonLabel = closeButtonLabel;
  117. this.closeLabelAttribute = closeLabelAttribute;
  118. this.clearAllLabelAttribute = clearAllLabelAttribute;
  119. // Private variables
  120. this.input = null;
  121. this.search = null;
  122. this.checkboxes = null;
  123. this.select = null;
  124. this.selectAll = null;
  125. this.selectIcon = null;
  126. this.textDefault = null;
  127. this.textSearch = null;
  128. this.textSelectAll = null;
  129. this.textNoResults = null;
  130. this.selectMultiple = null;
  131. this.inputContainer = null;
  132. this.optionsContainer = null;
  133. this.visibleOptions = null;
  134. this.searchContainer = null;
  135. this.countSelections = null;
  136. this.form = null;
  137. this.formGroup = null;
  138. this.label = null;
  139. this.helper = null;
  140. this.invalid = null;
  141. this.selectMultipleId = null;
  142. this.multiple =
  143. queryOne(this.selectMultipleSelector, this.element.parentNode) || false;
  144. this.isOpen = false;
  145. // Bind `this` for use in callbacks
  146. this.handleToggle = this.handleToggle.bind(this);
  147. this.handleClickOption = this.handleClickOption.bind(this);
  148. this.handleClickSelectAll = this.handleClickSelectAll.bind(this);
  149. this.handleEsc = this.handleEsc.bind(this);
  150. this.handleFocusout = this.handleFocusout.bind(this);
  151. this.handleSearch = this.handleSearch.bind(this);
  152. this.handleClickOutside = this.handleClickOutside.bind(this);
  153. this.resetForm = this.resetForm.bind(this);
  154. this.handleClickOnClearAll = this.handleClickOnClearAll.bind(this);
  155. this.handleKeyboardOnSelect = this.handleKeyboardOnSelect.bind(this);
  156. this.handleKeyboardOnSelectAll = this.handleKeyboardOnSelectAll.bind(this);
  157. this.handleKeyboardOnSearch = this.handleKeyboardOnSearch.bind(this);
  158. this.handleKeyboardOnOptions = this.handleKeyboardOnOptions.bind(this);
  159. this.handleKeyboardOnOption = this.handleKeyboardOnOption.bind(this);
  160. this.handleKeyboardOnClearAll = this.handleKeyboardOnClearAll.bind(this);
  161. this.handleKeyboardOnClose = this.handleKeyboardOnClose.bind(this);
  162. this.setCurrentValue = this.setCurrentValue.bind(this);
  163. this.update = this.update.bind(this);
  164. }
  165. /**
  166. * Static method to create an svg icon.
  167. *
  168. * @static
  169. * @private
  170. * @returns {HTMLElement}
  171. */
  172. static #createSvgIcon(icon, classes) {
  173. const tempElement = document.createElement('div');
  174. tempElement.innerHTML = icon; // avoiding the use of not-so-stable createElementNs
  175. const svg = tempElement.children[0];
  176. svg.removeAttribute('height');
  177. svg.removeAttribute('width');
  178. svg.setAttribute('focusable', false);
  179. svg.setAttribute('aria-hidden', true);
  180. // The following element is <path> which does not support classList API as others.
  181. svg.setAttribute('class', classes);
  182. return svg;
  183. }
  184. /**
  185. * Static method to create a checkbox element.
  186. *
  187. * @static
  188. * @param {Object} options
  189. * @param {String} options.id
  190. * @param {String} options.text
  191. * @param {String} [options.extraClass] - additional CSS class
  192. * @param {String} [options.disabled] - relevant when re-creating an option
  193. * @param {String} [options.selected] - relevant when re-creating an option
  194. * @param {String} ctx
  195. * @private
  196. * @returns {HTMLElement}
  197. */
  198. static #createCheckbox(options, ctx) {
  199. // Early returns.
  200. if (!options || !ctx) return '';
  201. const { id, text, disabled, selected, extraClass } = options;
  202. if (!id || !text) return '';
  203. // Elements to work with.
  204. const checkbox = document.createElement('div');
  205. const input = document.createElement('input');
  206. const label = document.createElement('label');
  207. const box = document.createElement('span');
  208. const labelText = document.createElement('span');
  209. // Respect optional input parameters.
  210. if (extraClass) {
  211. checkbox.classList.add(extraClass);
  212. }
  213. if (selected) {
  214. input.setAttribute('checked', true);
  215. }
  216. if (disabled) {
  217. checkbox.classList.add('ecl-checkbox--disabled');
  218. box.classList.add('ecl-checkbox__box--disabled');
  219. input.setAttribute('disabled', disabled);
  220. }
  221. // Imperative work follows.
  222. checkbox.classList.add('ecl-checkbox');
  223. checkbox.setAttribute('data-select-multiple-value', text);
  224. input.classList.add('ecl-checkbox__input');
  225. input.setAttribute('type', 'checkbox');
  226. input.setAttribute('id', `${ctx}-${id}`);
  227. input.setAttribute('name', `${ctx}-${id}`);
  228. checkbox.appendChild(input);
  229. label.classList.add('ecl-checkbox__label');
  230. label.setAttribute('for', `${ctx}-${id}`);
  231. box.classList.add('ecl-checkbox__box');
  232. box.setAttribute('aria-hidden', true);
  233. box.appendChild(
  234. Select.#createSvgIcon(
  235. iconSvgAllCheck,
  236. 'ecl-icon ecl-icon--s ecl-checkbox__icon',
  237. ),
  238. );
  239. label.appendChild(box);
  240. labelText.classList.add('ecl-checkbox__label-text');
  241. labelText.innerHTML = text;
  242. label.appendChild(labelText);
  243. checkbox.appendChild(label);
  244. return checkbox;
  245. }
  246. /**
  247. * Static method to generate the select icon
  248. *
  249. * @static
  250. * @private
  251. * @returns {HTMLElement}
  252. */
  253. static #createSelectIcon() {
  254. const wrapper = document.createElement('div');
  255. wrapper.classList.add('ecl-select__icon');
  256. const button = document.createElement('button');
  257. button.classList.add(
  258. 'ecl-button',
  259. 'ecl-button--ghost',
  260. 'ecl-button--icon-only',
  261. );
  262. button.setAttribute('tabindex', '-1');
  263. const labelWrapper = document.createElement('span');
  264. labelWrapper.classList.add('ecl-button__container');
  265. const label = document.createElement('span');
  266. label.classList.add('ecl-button__label');
  267. label.textContent = 'Toggle dropdown';
  268. labelWrapper.appendChild(label);
  269. const icon = Select.#createSvgIcon(
  270. iconSvgAllCornerArrow,
  271. `ecl-icon ecl-icon--${iconSize} ecl-icon--rotate-180`,
  272. );
  273. labelWrapper.appendChild(icon);
  274. button.appendChild(labelWrapper);
  275. wrapper.appendChild(button);
  276. return wrapper;
  277. }
  278. /**
  279. * Static method to programmatically check an ECL-specific checkbox when previously default has been prevented.
  280. *
  281. * @static
  282. * @param {Event} e
  283. * @private
  284. */
  285. static #checkCheckbox(e) {
  286. const input = e.target.closest('.ecl-checkbox').querySelector('input');
  287. input.checked = !input.checked;
  288. return input.checked;
  289. }
  290. /**
  291. * Static method to generate a random string
  292. *
  293. * @static
  294. * @param {number} length
  295. * @private
  296. */
  297. static #generateRandomId(length) {
  298. return Math.random().toString(36).substr(2, length);
  299. }
  300. /**
  301. * Initialise component.
  302. */
  303. init() {
  304. if (!ECL) {
  305. throw new TypeError('Called init but ECL is not present');
  306. }
  307. ECL.components = ECL.components || new Map();
  308. this.select = this.element;
  309. if (this.multiple) {
  310. const containerClasses = Array.from(this.select.parentElement.classList);
  311. this.textDefault =
  312. this.defaultText ||
  313. this.element.getAttribute(this.defaultTextAttribute);
  314. this.textSearch =
  315. this.searchText || this.element.getAttribute(this.searchTextAttribute);
  316. this.textSelectAll =
  317. this.selectAllText ||
  318. this.element.getAttribute(this.selectAllTextAttribute);
  319. this.textNoResults =
  320. this.noResultsText ||
  321. this.element.getAttribute(this.noResultsTextAttribute);
  322. this.closeButtonLabel =
  323. this.closeButtonLabel ||
  324. this.element.getAttribute(this.closeLabelAttribute);
  325. this.clearAllButtonLabel =
  326. this.clearAllButtonLabel ||
  327. this.element.getAttribute(this.clearAllLabelAttribute);
  328. // Retrieve the id from the markup or generate one.
  329. this.selectMultipleId =
  330. this.element.id || `select-multiple-${Select.#generateRandomId(4)}`;
  331. this.element.id = this.selectMultipleId;
  332. this.formGroup = this.element.closest('.ecl-form-group');
  333. if (this.formGroup) {
  334. this.formGroup.setAttribute('role', 'application');
  335. this.label = queryOne('.ecl-form-label', this.formGroup);
  336. this.helper = queryOne('.ecl-help-block', this.formGroup);
  337. this.invalid = queryOne('.ecl-feedback-message', this.formGroup);
  338. }
  339. // Disable focus on default select
  340. this.select.setAttribute('tabindex', '-1');
  341. this.selectMultiple = document.createElement('div');
  342. this.selectMultiple.classList.add('ecl-select__multiple');
  343. // Close the searchContainer when tabbing out of the selectMultiple
  344. this.selectMultiple.addEventListener('focusout', this.handleFocusout);
  345. this.inputContainer = document.createElement('div');
  346. this.inputContainer.classList.add(...containerClasses);
  347. this.selectMultiple.appendChild(this.inputContainer);
  348. this.input = document.createElement('button');
  349. this.input.classList.add('ecl-select', 'ecl-select__multiple-toggle');
  350. this.input.setAttribute('type', 'button');
  351. this.input.setAttribute(
  352. 'aria-controls',
  353. `${this.selectMultipleId}-dropdown`,
  354. );
  355. this.input.setAttribute('id', `${this.selectMultipleId}-toggle`);
  356. this.input.setAttribute('aria-expanded', false);
  357. if (containerClasses.find((c) => c.includes('disabled'))) {
  358. this.input.setAttribute('disabled', true);
  359. }
  360. // Add accessibility attributes
  361. if (this.label) {
  362. this.label.setAttribute('for', `${this.selectMultipleId}-toggle`);
  363. this.input.setAttribute('aria-labelledby', this.label.id);
  364. }
  365. let describedby = '';
  366. if (this.helper) {
  367. describedby = this.helper.id;
  368. }
  369. if (this.invalid) {
  370. describedby = describedby
  371. ? `${describedby} ${this.invalid.id}`
  372. : this.invalid.id;
  373. }
  374. if (describedby) {
  375. this.input.setAttribute('aria-describedby', describedby);
  376. }
  377. this.input.addEventListener('keydown', this.handleKeyboardOnSelect);
  378. this.input.addEventListener('click', this.handleToggle);
  379. this.selectionCount = document.createElement('div');
  380. this.selectionCount.classList.add(
  381. this.selectMultiplesSelectionCountSelector,
  382. );
  383. this.selectionCountText = document.createElement('span');
  384. this.selectionCount.appendChild(this.selectionCountText);
  385. this.inputContainer.appendChild(this.selectionCount);
  386. this.inputContainer.appendChild(this.input);
  387. this.inputContainer.appendChild(Select.#createSelectIcon());
  388. this.searchContainer = document.createElement('div');
  389. this.searchContainer.style.display = 'none';
  390. this.searchContainer.classList.add(
  391. 'ecl-select__multiple-dropdown',
  392. ...containerClasses,
  393. );
  394. this.searchContainer.setAttribute(
  395. 'id',
  396. `${this.selectMultipleId}-dropdown`,
  397. );
  398. this.selectMultiple.appendChild(this.searchContainer);
  399. this.search = document.createElement('input');
  400. this.search.classList.add('ecl-text-input');
  401. this.search.setAttribute('type', 'search');
  402. this.search.setAttribute('placeholder', this.textSearch || '');
  403. this.search.addEventListener('keyup', this.handleSearch);
  404. this.search.addEventListener('search', this.handleSearch);
  405. this.searchContainer.appendChild(this.search);
  406. if (this.textSelectAll) {
  407. const optionsCount = Array.from(this.select.options).filter(
  408. (option) => !option.disabled,
  409. ).length;
  410. this.selectAll = Select.#createCheckbox(
  411. {
  412. id: `all-${Select.#generateRandomId(4)}`,
  413. text: `${this.textSelectAll} (${optionsCount})`,
  414. extraClass: 'ecl-select__multiple-all',
  415. },
  416. this.selectMultipleId,
  417. );
  418. this.selectAll.addEventListener('click', this.handleClickSelectAll);
  419. this.selectAll.addEventListener('keypress', this.handleClickSelectAll);
  420. this.selectAll.addEventListener('change', this.handleClickSelectAll);
  421. this.searchContainer.appendChild(this.selectAll);
  422. }
  423. this.search.addEventListener('keydown', this.handleKeyboardOnSearch);
  424. this.optionsContainer = document.createElement('div');
  425. this.optionsContainer.classList.add('ecl-select__multiple-options');
  426. this.searchContainer.appendChild(this.optionsContainer);
  427. // Toolbar
  428. if (this.clearAllButtonLabel || this.closeButtonLabel) {
  429. this.dropDownToolbar = document.createElement('div');
  430. this.dropDownToolbar.classList.add('ecl-select-multiple-toolbar');
  431. if (this.closeButtonLabel) {
  432. this.closeButton = document.createElement('button');
  433. this.closeButton.textContent = this.closeButtonLabel;
  434. this.closeButton.classList.add('ecl-button', 'ecl-button--primary');
  435. this.closeButton.addEventListener('click', this.handleEsc);
  436. this.closeButton.addEventListener(
  437. 'keydown',
  438. this.handleKeyboardOnClose,
  439. );
  440. if (this.dropDownToolbar) {
  441. this.dropDownToolbar.appendChild(this.closeButton);
  442. this.searchContainer.appendChild(this.dropDownToolbar);
  443. this.dropDownToolbar.style.display = 'none';
  444. }
  445. }
  446. if (this.clearAllButtonLabel) {
  447. this.clearAllButton = document.createElement('button');
  448. this.clearAllButton.textContent = this.clearAllButtonLabel;
  449. this.clearAllButton.classList.add(
  450. 'ecl-button',
  451. 'ecl-button--secondary',
  452. );
  453. this.clearAllButton.addEventListener(
  454. 'click',
  455. this.handleClickOnClearAll,
  456. );
  457. this.clearAllButton.addEventListener(
  458. 'keydown',
  459. this.handleKeyboardOnClearAll,
  460. );
  461. this.dropDownToolbar.appendChild(this.clearAllButton);
  462. }
  463. }
  464. this.selectAll.addEventListener(
  465. 'keydown',
  466. this.handleKeyboardOnSelectAll,
  467. );
  468. this.optionsContainer.addEventListener(
  469. 'keydown',
  470. this.handleKeyboardOnOptions,
  471. );
  472. if (this.select.options && this.select.options.length > 0) {
  473. this.checkboxes = Array.from(this.select.options).map((option) => {
  474. let optgroup = '';
  475. let checkbox = '';
  476. if (option.parentNode.tagName === 'OPTGROUP') {
  477. if (
  478. !queryOne(
  479. `fieldset[data-ecl-multiple-group="${option.parentNode.getAttribute(
  480. 'label',
  481. )}"]`,
  482. this.optionsContainer,
  483. )
  484. ) {
  485. optgroup = document.createElement('fieldset');
  486. const title = document.createElement('legend');
  487. title.classList.add('ecl-select__multiple-group__title');
  488. title.innerHTML = option.parentNode.getAttribute('label');
  489. optgroup.appendChild(title);
  490. optgroup.setAttribute(
  491. 'data-ecl-multiple-group',
  492. option.parentNode.getAttribute('label'),
  493. );
  494. optgroup.classList.add('ecl-select__multiple-group');
  495. this.optionsContainer.appendChild(optgroup);
  496. } else {
  497. optgroup = queryOne(
  498. `fieldset[data-ecl-multiple-group="${option.parentNode.getAttribute(
  499. 'label',
  500. )}"]`,
  501. this.optionsContainer,
  502. );
  503. }
  504. }
  505. if (option.selected) {
  506. this.#updateSelectionsCount(1);
  507. if (this.dropDownToolbar) {
  508. this.dropDownToolbar.style.display = 'flex';
  509. }
  510. }
  511. checkbox = Select.#createCheckbox(
  512. {
  513. // spread operator does not work in storybook context so we map 1:1
  514. id: option.value,
  515. text: option.text,
  516. disabled: option.disabled,
  517. selected: option.selected,
  518. },
  519. this.selectMultipleId,
  520. );
  521. checkbox.setAttribute('data-visible', true);
  522. if (!checkbox.classList.contains('ecl-checkbox--disabled')) {
  523. checkbox.addEventListener('click', this.handleClickOption);
  524. checkbox.addEventListener('keydown', this.handleKeyboardOnOption);
  525. }
  526. if (optgroup) {
  527. optgroup.appendChild(checkbox);
  528. } else {
  529. this.optionsContainer.appendChild(checkbox);
  530. }
  531. return checkbox;
  532. });
  533. } else {
  534. this.checkboxes = [];
  535. }
  536. this.visibleOptions = this.checkboxes;
  537. this.select.parentNode.parentNode.insertBefore(
  538. this.selectMultiple,
  539. this.select.parentNode.nextSibling,
  540. );
  541. this.select.parentNode.classList.add('ecl-select__container--hidden');
  542. // Respect default selected options.
  543. this.#updateCurrentValue();
  544. this.form = this.element.closest('form');
  545. if (this.form) {
  546. this.form.addEventListener('reset', this.resetForm);
  547. }
  548. document.addEventListener('click', this.handleClickOutside);
  549. } else {
  550. // Simple select
  551. this.#handleOptgroup();
  552. this.select.addEventListener('keydown', this.handleKeyboardOnSelect);
  553. }
  554. // Set ecl initialized attribute
  555. this.element.setAttribute('data-ecl-auto-initialized', 'true');
  556. ECL.components.set(this.element, this);
  557. }
  558. /**
  559. * Update instance.
  560. *
  561. * @param {Integer} i
  562. */
  563. update(i) {
  564. this.#updateCurrentValue();
  565. this.#updateSelectionsCount(i);
  566. }
  567. /**
  568. * Set the selected value(s) programmatically.
  569. *
  570. * @param {string | Array<string>} values - A string or an array of values or labels to set as selected.
  571. * @param {string} [op='replace'] - The operation mode. Use 'add' to keep the previous selections.
  572. * @throws {Error} Throws an error if an invalid operation mode is provided.
  573. *
  574. * @example
  575. * // Replace current selection with new values
  576. * setCurrentValue(['value1', 'value2']);
  577. *
  578. * // Add to current selection without clearing previous selections
  579. * setCurrentValue(['value3', 'value4'], 'add');
  580. *
  581. */
  582. setCurrentValue(values, op = 'replace') {
  583. if (op !== 'replace' && op !== 'add') {
  584. throw new Error('Invalid operation mode. Use "replace" or "add".');
  585. }
  586. const valuesArray = typeof values === 'string' ? [values] : values;
  587. Array.from(this.select.options).forEach((option) => {
  588. if (op === 'replace') {
  589. option.selected = false;
  590. }
  591. if (
  592. valuesArray.includes(option.value) ||
  593. valuesArray.includes(option.label)
  594. ) {
  595. option.selected = true;
  596. }
  597. });
  598. this.update();
  599. }
  600. /**
  601. * Event callback to show/hide the dropdown
  602. *
  603. * @param {Event} e
  604. * @fires Select#onToggle
  605. * @type {function}
  606. */
  607. handleToggle(e) {
  608. e.preventDefault();
  609. this.input.classList.toggle('ecl-select--active');
  610. if (this.searchContainer.style.display === 'none') {
  611. this.searchContainer.style.display = 'block';
  612. this.input.setAttribute('aria-expanded', true);
  613. this.isOpen = true;
  614. } else {
  615. this.searchContainer.style.display = 'none';
  616. this.input.setAttribute('aria-expanded', false);
  617. this.isOpen = false;
  618. }
  619. const eventData = { opened: this.isOpen, e };
  620. this.trigger('onToggle', eventData);
  621. }
  622. /**
  623. * Register a callback function for a specific event.
  624. *
  625. * @param {string} eventName - The name of the event to listen for.
  626. * @param {Function} callback - The callback function to be invoked when the event occurs.
  627. * @returns {void}
  628. * @memberof Select
  629. * @instance
  630. *
  631. * @example
  632. * // Registering a callback for the 'onToggle' event
  633. * select.on('onToggle', (event) => {
  634. * console.log('Toggle event occurred!', event);
  635. * });
  636. */
  637. on(eventName, callback) {
  638. this.eventManager.on(eventName, callback);
  639. }
  640. /**
  641. * Trigger a component event.
  642. *
  643. * @param {string} eventName - The name of the event to trigger.
  644. * @param {any} eventData - Data associated with the event.
  645. * @memberof Select
  646. * @instance
  647. *
  648. */
  649. trigger(eventName, eventData) {
  650. this.eventManager.trigger(eventName, eventData);
  651. }
  652. /**
  653. * Destroy the component instance.
  654. */
  655. destroy() {
  656. this.input.removeEventListener('keydown', this.handleKeyboardOnSelect);
  657. if (this.multiple) {
  658. document.removeEventListener('click', this.handleClickOutside);
  659. this.selectMultiple.removeEventListener('focusout', this.handleFocusout);
  660. this.input.removeEventListener('click', this.handleToggle);
  661. this.search.removeEventListener('keyup', this.handleSearch);
  662. this.search.removeEventListener('keydown', this.handleKeyboardOnSearch);
  663. this.selectAll.removeEventListener('click', this.handleClickSelectAll);
  664. this.selectAll.removeEventListener('keypress', this.handleClickSelectAll);
  665. this.selectAll.removeEventListener(
  666. 'keydown',
  667. this.handleKeyboardOnSelectAll,
  668. );
  669. this.optionsContainer.removeEventListener(
  670. 'keydown',
  671. this.handleKeyboardOnOptions,
  672. );
  673. this.checkboxes.forEach((checkbox) => {
  674. checkbox.removeEventListener('click', this.handleClickSelectAll);
  675. checkbox.removeEventListener('click', this.handleClickOption);
  676. checkbox.removeEventListener('keydown', this.handleKeyboardOnOption);
  677. });
  678. if (this.closeButton) {
  679. this.closeButton.removeEventListener('click', this.handleEsc);
  680. this.closeButton.removeEventListener(
  681. 'keydown',
  682. this.handleKeyboardOnClose,
  683. );
  684. }
  685. if (this.clearAllButton) {
  686. this.clearAllButton.removeEventListener(
  687. 'click',
  688. this.handleClickOnClearAll,
  689. );
  690. this.clearAllButton.removeEventListener(
  691. 'keydown',
  692. this.handleKeyboardOnClearAll,
  693. );
  694. }
  695. if (this.selectMultiple) {
  696. this.selectMultiple.remove();
  697. }
  698. this.select.parentNode.classList.remove('ecl-select__container--hidden');
  699. }
  700. if (this.element) {
  701. this.element.removeAttribute('data-ecl-auto-initialized');
  702. ECL.components.delete(this.element);
  703. }
  704. }
  705. /**
  706. * Private method to handle the update of the selected options counter.
  707. *
  708. * @param {Integer} i
  709. * @private
  710. */
  711. #updateSelectionsCount(i) {
  712. let selectedOptionsCount = 0;
  713. if (i > 0) {
  714. this.selectionCount.querySelector('span').innerHTML += i;
  715. } else {
  716. selectedOptionsCount = Array.from(this.select.options).filter(
  717. (option) => option.selected,
  718. ).length;
  719. }
  720. if (selectedOptionsCount > 0) {
  721. this.selectionCount.querySelector('span').innerHTML =
  722. selectedOptionsCount;
  723. this.selectionCount.classList.add(
  724. 'ecl-select-multiple-selections-counter--visible',
  725. );
  726. if (this.dropDownToolbar) {
  727. this.dropDownToolbar.style.display = 'flex';
  728. }
  729. } else {
  730. this.selectionCount.classList.remove(
  731. 'ecl-select-multiple-selections-counter--visible',
  732. );
  733. if (this.dropDownToolbar) {
  734. this.dropDownToolbar.style.display = 'none';
  735. }
  736. }
  737. if (selectedOptionsCount >= 100) {
  738. this.selectionCount.classList.add(
  739. 'ecl-select-multiple-selections-counter--xxl',
  740. );
  741. }
  742. }
  743. /**
  744. * Private method to handle optgroup in single select.
  745. *
  746. * @private
  747. */
  748. #handleOptgroup() {
  749. Array.from(this.select.options).forEach((option) => {
  750. if (option.parentNode.tagName === 'OPTGROUP') {
  751. const groupLabel = option.parentNode.getAttribute('label');
  752. const optionLabel = option.getAttribute('label') || option.textContent;
  753. if (groupLabel && optionLabel) {
  754. option.setAttribute('aria-label', `${groupLabel}: ${optionLabel}`);
  755. }
  756. }
  757. });
  758. }
  759. /**
  760. * Private method to update the select value.
  761. *
  762. * @fires Select#onSelection
  763. * @private
  764. */
  765. #updateCurrentValue() {
  766. const optionSelected = Array.from(this.select.options)
  767. .filter((option) => option.selected) // do not rely on getAttribute as it does not work in all cases
  768. .map((option) => option.text)
  769. .join(', ');
  770. this.input.innerHTML = optionSelected || this.textDefault || '';
  771. if (optionSelected !== '' && this.label) {
  772. this.label.setAttribute(
  773. 'aria-label',
  774. `${this.label.innerText} ${optionSelected}`,
  775. );
  776. } else if (optionSelected === '' && this.label) {
  777. this.label.removeAttribute('aria-label');
  778. }
  779. this.trigger('onSelection', { selected: optionSelected });
  780. // Dispatch a change event once the value of the select has changed.
  781. this.select.dispatchEvent(new window.Event('change', { bubbles: true }));
  782. }
  783. /**
  784. * Private method to handle the focus switch.
  785. *
  786. * @param {upOrDown}
  787. * @param {loop}
  788. * @private
  789. */
  790. #moveFocus(upOrDown, loop = true) {
  791. const activeEl = document.activeElement;
  792. const hasGroups = activeEl.parentElement.parentElement.classList.contains(
  793. 'ecl-select__multiple-group',
  794. );
  795. const options = !hasGroups
  796. ? Array.from(
  797. activeEl.parentElement.parentElement.querySelectorAll(
  798. '.ecl-checkbox__input',
  799. ),
  800. )
  801. : Array.from(
  802. activeEl.parentElement.parentElement.parentElement.querySelectorAll(
  803. '.ecl-checkbox__input',
  804. ),
  805. );
  806. const activeIndex = options.indexOf(activeEl);
  807. if (upOrDown === 'down') {
  808. const nextSiblings = options
  809. .splice(activeIndex + 1, options.length)
  810. .filter(
  811. (el) => !el.disabled && el.parentElement.style.display !== 'none',
  812. );
  813. if (nextSiblings.length > 0) {
  814. nextSiblings[0].focus();
  815. } else {
  816. // eslint-disable-next-line no-lonely-if
  817. if (loop) {
  818. if (
  819. this.dropDownToolbar &&
  820. this.dropDownToolbar.style.display === 'flex'
  821. ) {
  822. this.dropDownToolbar.firstChild.focus();
  823. } else {
  824. this.input.focus();
  825. }
  826. }
  827. }
  828. } else {
  829. const previousSiblings = options
  830. .splice(0, activeIndex)
  831. .filter(
  832. (el) => !el.disabled && el.parentElement.style.display !== 'none',
  833. );
  834. if (previousSiblings.length > 0) {
  835. previousSiblings[previousSiblings.length - 1].focus();
  836. } else {
  837. this.optionsContainer.scrollTop = 0;
  838. if (!this.selectAll.querySelector('input').disabled) {
  839. this.selectAll.querySelector('input').focus();
  840. } else {
  841. this.search.focus();
  842. }
  843. }
  844. }
  845. }
  846. /**
  847. * Event callback to handle the click on a checkbox.
  848. *
  849. * @param {Event} e
  850. * @type {function}
  851. */
  852. handleClickOption(e) {
  853. e.preventDefault();
  854. Select.#checkCheckbox(e);
  855. // Toggle values
  856. const checkbox = e.target.closest('.ecl-checkbox');
  857. Array.from(this.select.options).forEach((option) => {
  858. if (option.text === checkbox.getAttribute('data-select-multiple-value')) {
  859. if (option.getAttribute('selected') || option.selected) {
  860. option.selected = false;
  861. this.selectAll.querySelector('input').checked = false;
  862. } else {
  863. option.selected = true;
  864. }
  865. }
  866. });
  867. this.update();
  868. }
  869. /**
  870. * Event callback to handle the click on the select all checkbox.
  871. *
  872. * @param {Event} e
  873. * @fires Select#onSelectAll
  874. * @type {function}
  875. */
  876. handleClickSelectAll(e) {
  877. e.preventDefault();
  878. // Early returns.
  879. if (this.selectAll.querySelector('input').disabled) {
  880. return;
  881. }
  882. const checked = Select.#checkCheckbox(e);
  883. const options = Array.from(this.select.options).filter((o) => !o.disabled);
  884. const checkboxes = Array.from(
  885. this.searchContainer.querySelectorAll('[data-visible="true"]'),
  886. ).filter((checkbox) => !checkbox.querySelector('input').disabled);
  887. checkboxes.forEach((checkbox) => {
  888. checkbox.querySelector('input').checked = checked;
  889. const option = options.find(
  890. (o) => o.text === checkbox.getAttribute('data-select-multiple-value'),
  891. );
  892. if (option) {
  893. if (checked) {
  894. option.selected = true;
  895. } else {
  896. option.selected = false;
  897. }
  898. }
  899. });
  900. this.update();
  901. this.trigger('onSelectAll', { selected: options });
  902. }
  903. /**
  904. * Event callback to handle moving the focus out of the select.
  905. *
  906. * @param {Event} e
  907. * @type {function}
  908. */
  909. handleFocusout(e) {
  910. if (
  911. e.relatedTarget &&
  912. this.selectMultiple &&
  913. !this.selectMultiple.contains(e.relatedTarget) &&
  914. this.searchContainer.style.display === 'block'
  915. ) {
  916. this.searchContainer.style.display = 'none';
  917. this.input.classList.remove('ecl-select--active');
  918. this.input.setAttribute('aria-expanded', false);
  919. } else if (
  920. e.relatedTarget &&
  921. !this.selectMultiple &&
  922. !this.select.parentNode.contains(e.relatedTarget)
  923. ) {
  924. this.select.blur();
  925. }
  926. }
  927. /**
  928. * Event callback to handle the user typing in the search field.
  929. *
  930. * @param {Event} e
  931. * @fires Select#onSearch
  932. * @type {function}
  933. */
  934. handleSearch(e) {
  935. const dropDownHeight = this.optionsContainer.offsetHeight;
  936. this.visibleOptions = [];
  937. const keyword = e.target.value.toLowerCase();
  938. let eventDetails = {};
  939. if (dropDownHeight > 0) {
  940. this.optionsContainer.style.height = `${dropDownHeight}px`;
  941. }
  942. this.checkboxes.forEach((checkbox) => {
  943. if (
  944. !checkbox
  945. .getAttribute('data-select-multiple-value')
  946. .toLocaleLowerCase()
  947. .includes(keyword)
  948. ) {
  949. checkbox.removeAttribute('data-visible');
  950. checkbox.style.display = 'none';
  951. } else {
  952. checkbox.setAttribute('data-visible', true);
  953. checkbox.style.display = 'flex';
  954. // Highlight keyword in checkbox label.
  955. const checkboxLabelText = checkbox.querySelector(
  956. '.ecl-checkbox__label-text',
  957. );
  958. checkboxLabelText.textContent = checkboxLabelText.textContent.replace(
  959. '.cls-1{fill:none}',
  960. '',
  961. );
  962. if (keyword) {
  963. checkboxLabelText.innerHTML = checkboxLabelText.textContent.replace(
  964. new RegExp(`${keyword}(?!([^<]+)?<)`, 'gi'),
  965. '<b>$&</b>',
  966. );
  967. }
  968. this.visibleOptions.push(checkbox);
  969. }
  970. });
  971. // Select all checkbox follows along.
  972. const checked = this.visibleOptions.filter(
  973. (c) => c.querySelector('input').checked,
  974. );
  975. if (
  976. this.visibleOptions.length === 0 ||
  977. this.visibleOptions.length !== checked.length
  978. ) {
  979. this.selectAll.querySelector('input').checked = false;
  980. } else {
  981. this.selectAll.querySelector('input').checked = true;
  982. }
  983. // Display no-results message.
  984. const noResultsElement = this.searchContainer.querySelector(
  985. '.ecl-select__multiple-no-results',
  986. );
  987. const groups = this.optionsContainer.getElementsByClassName(
  988. 'ecl-select__multiple-group',
  989. );
  990. // eslint-disable-next-line no-restricted-syntax
  991. for (const group of groups) {
  992. group.style.display = 'none';
  993. // eslint-disable-next-line no-restricted-syntax
  994. const groupedCheckboxes = [...group.children].filter((node) =>
  995. node.classList.contains('ecl-checkbox'),
  996. );
  997. groupedCheckboxes.forEach((single) => {
  998. if (single.hasAttribute('data-visible')) {
  999. single.closest('.ecl-select__multiple-group').style.display = 'block';
  1000. }
  1001. });
  1002. }
  1003. if (this.visibleOptions.length === 0 && !noResultsElement) {
  1004. // Create no-results element.
  1005. const noResultsContainer = document.createElement('div');
  1006. const noResultsLabel = document.createElement('span');
  1007. noResultsContainer.classList.add('ecl-select__multiple-no-results');
  1008. noResultsLabel.innerHTML = this.textNoResults;
  1009. noResultsContainer.appendChild(noResultsLabel);
  1010. this.optionsContainer.appendChild(noResultsContainer);
  1011. } else if (this.visibleOptions.length > 0 && noResultsElement !== null) {
  1012. noResultsElement.parentNode.removeChild(noResultsElement);
  1013. }
  1014. // reset
  1015. if (keyword.length === 0) {
  1016. this.checkboxes.forEach((checkbox) => {
  1017. checkbox.setAttribute('data-visible', true);
  1018. checkbox.style.display = 'flex';
  1019. });
  1020. // Enable select all checkbox.
  1021. this.selectAll.classList.remove('ecl-checkbox--disabled');
  1022. this.selectAll.querySelector('input').disabled = false;
  1023. } else {
  1024. // Disable select all checkbox.
  1025. this.selectAll.classList.add('ecl-checkbox--disabled');
  1026. this.selectAll.querySelector('input').disabled = true;
  1027. }
  1028. if (this.visibleOptions.length > 0) {
  1029. const visibleLabels = this.visibleOptions.map((option) => {
  1030. let label = null;
  1031. const labelEl = queryOne('.ecl-checkbox__label-text', option);
  1032. if (labelEl) {
  1033. label = labelEl.innerHTML.replace(/<\/?b>/g, '');
  1034. }
  1035. return label || '';
  1036. });
  1037. eventDetails = {
  1038. results: visibleLabels,
  1039. text: e.target.value.toLowerCase(),
  1040. };
  1041. } else {
  1042. eventDetails = { results: 'none', text: e.target.value.toLowerCase() };
  1043. }
  1044. this.trigger('onSearch', eventDetails);
  1045. }
  1046. /**
  1047. * Event callback to handle the click outside the select.
  1048. *
  1049. * @param {Event} e
  1050. * @type {function}
  1051. */
  1052. handleClickOutside(e) {
  1053. if (
  1054. e.target &&
  1055. this.selectMultiple &&
  1056. !this.selectMultiple.contains(e.target) &&
  1057. this.searchContainer.style.display === 'block'
  1058. ) {
  1059. this.searchContainer.style.display = 'none';
  1060. this.input.classList.remove('ecl-select--active');
  1061. this.input.setAttribute('aria-expanded', false);
  1062. }
  1063. }
  1064. /**
  1065. * Event callback to handle keyboard events on the select.
  1066. *
  1067. * @param {Event} e
  1068. * @type {function}
  1069. */
  1070. handleKeyboardOnSelect(e) {
  1071. switch (e.key) {
  1072. case 'Escape':
  1073. e.preventDefault();
  1074. this.handleEsc(e);
  1075. break;
  1076. case ' ':
  1077. case 'Enter':
  1078. if (this.multiple) {
  1079. e.preventDefault();
  1080. this.handleToggle(e);
  1081. this.search.focus();
  1082. }
  1083. break;
  1084. case 'ArrowDown':
  1085. if (this.multiple) {
  1086. e.preventDefault();
  1087. this.handleToggle(e);
  1088. this.search.focus();
  1089. }
  1090. break;
  1091. default:
  1092. }
  1093. }
  1094. /**
  1095. * Event callback to handle keyboard events on the select all checkbox.
  1096. *
  1097. * @param {Event} e
  1098. * @type {function}
  1099. */
  1100. handleKeyboardOnSelectAll(e) {
  1101. switch (e.key) {
  1102. case 'Escape':
  1103. e.preventDefault();
  1104. this.handleEsc(e);
  1105. break;
  1106. case 'ArrowDown':
  1107. e.preventDefault();
  1108. if (this.visibleOptions.length > 0) {
  1109. this.visibleOptions[0].querySelector('input').focus();
  1110. } else {
  1111. this.input.focus();
  1112. }
  1113. break;
  1114. case 'ArrowUp':
  1115. e.preventDefault();
  1116. this.search.focus();
  1117. break;
  1118. case 'Tab':
  1119. e.preventDefault();
  1120. if (e.shiftKey) {
  1121. this.search.focus();
  1122. } else if (this.visibleOptions.length > 0) {
  1123. this.visibleOptions[0].querySelector('input').focus();
  1124. } else {
  1125. this.input.focus();
  1126. }
  1127. break;
  1128. default:
  1129. }
  1130. }
  1131. /**
  1132. * Event callback to handle keyboard events on the dropdown.
  1133. *
  1134. * @param {Event} e
  1135. * @type {function}
  1136. */
  1137. handleKeyboardOnOptions(e) {
  1138. switch (e.key) {
  1139. case 'Escape':
  1140. e.preventDefault();
  1141. this.handleEsc(e);
  1142. break;
  1143. case 'ArrowDown':
  1144. e.preventDefault();
  1145. this.#moveFocus('down', false);
  1146. break;
  1147. case 'ArrowUp':
  1148. e.preventDefault();
  1149. this.#moveFocus('up');
  1150. break;
  1151. case 'Tab':
  1152. e.preventDefault();
  1153. if (e.shiftKey) {
  1154. this.#moveFocus('up');
  1155. } else {
  1156. this.#moveFocus('down');
  1157. }
  1158. break;
  1159. default:
  1160. }
  1161. }
  1162. /**
  1163. * Event callback to handle keyboard events
  1164. *
  1165. * @param {Event} e
  1166. * @type {function}
  1167. */
  1168. handleKeyboardOnSearch(e) {
  1169. switch (e.key) {
  1170. case 'Escape':
  1171. e.preventDefault();
  1172. this.handleEsc(e);
  1173. break;
  1174. case 'ArrowDown':
  1175. e.preventDefault();
  1176. if (this.selectAll.querySelector('input').disabled) {
  1177. if (this.visibleOptions.length > 0) {
  1178. this.visibleOptions[0].querySelector('input').focus();
  1179. } else {
  1180. this.input.focus();
  1181. }
  1182. } else {
  1183. this.selectAll.querySelector('input').focus();
  1184. }
  1185. break;
  1186. case 'ArrowUp':
  1187. e.preventDefault();
  1188. this.input.focus();
  1189. this.handleToggle(e);
  1190. break;
  1191. default:
  1192. }
  1193. }
  1194. /**
  1195. * Event callback to handle the click on an option.
  1196. *
  1197. * @param {Event} e
  1198. * @type {function}
  1199. */
  1200. handleKeyboardOnOption(e) {
  1201. if (e.key === 'Enter' || e.key === ' ') {
  1202. e.preventDefault();
  1203. this.handleClickOption(e);
  1204. }
  1205. }
  1206. /**
  1207. * Event callback to handle keyboard events on the clear all button.
  1208. *
  1209. * @param {Event} e
  1210. * @fires Select#onReset
  1211. * @type {function}
  1212. */
  1213. handleKeyboardOnClearAll(e) {
  1214. e.preventDefault();
  1215. switch (e.key) {
  1216. case 'Enter':
  1217. case ' ':
  1218. this.handleClickOnClearAll(e);
  1219. this.trigger('onReset', e);
  1220. this.input.focus();
  1221. break;
  1222. case 'ArrowDown':
  1223. this.input.focus();
  1224. break;
  1225. case 'ArrowUp':
  1226. if (this.closeButton) {
  1227. this.closeButton.focus();
  1228. } else {
  1229. // eslint-disable-next-line no-lonely-if
  1230. if (this.visibleOptions.length > 0) {
  1231. this.visibleOptions[this.visibleOptions.length - 1]
  1232. .querySelector('input')
  1233. .focus();
  1234. } else {
  1235. this.search.focus();
  1236. }
  1237. }
  1238. break;
  1239. case 'Tab':
  1240. if (e.shiftKey) {
  1241. if (this.closeButton) {
  1242. this.closeButton.focus();
  1243. } else {
  1244. // eslint-disable-next-line no-lonely-if
  1245. if (this.visibleOptions.length > 0) {
  1246. this.visibleOptions[this.visibleOptions.length - 1]
  1247. .querySelector('input')
  1248. .focus();
  1249. } else {
  1250. this.search.focus();
  1251. }
  1252. }
  1253. } else {
  1254. this.input.focus();
  1255. this.handleToggle(e);
  1256. }
  1257. break;
  1258. default:
  1259. }
  1260. }
  1261. /**
  1262. * Event callback for handling keyboard events in the close button.
  1263. *
  1264. * @param {Event} e
  1265. * @type {function}
  1266. */
  1267. handleKeyboardOnClose(e) {
  1268. e.preventDefault();
  1269. switch (e.key) {
  1270. case 'Enter':
  1271. case ' ':
  1272. this.handleEsc(e);
  1273. this.input.focus();
  1274. break;
  1275. case 'ArrowUp':
  1276. if (this.visibleOptions.length > 0) {
  1277. this.visibleOptions[this.visibleOptions.length - 1]
  1278. .querySelector('input')
  1279. .focus();
  1280. } else {
  1281. this.input.focus();
  1282. this.handleToggle(e);
  1283. }
  1284. break;
  1285. case 'ArrowDown':
  1286. if (this.clearAllButton) {
  1287. this.clearAllButton.focus();
  1288. } else {
  1289. this.input.focus();
  1290. this.handleToggle(e);
  1291. }
  1292. break;
  1293. case 'Tab':
  1294. if (!e.shiftKey) {
  1295. if (this.clearAllButton) {
  1296. this.clearAllButton.focus();
  1297. } else {
  1298. this.input.focus();
  1299. this.handleToggle(e);
  1300. }
  1301. } else {
  1302. // eslint-disable-next-line no-lonely-if
  1303. if (this.visibleOptions.length > 0) {
  1304. this.visibleOptions[this.visibleOptions.length - 1]
  1305. .querySelector('input')
  1306. .focus();
  1307. } else {
  1308. this.input.focus();
  1309. this.handleToggle(e);
  1310. }
  1311. }
  1312. break;
  1313. default:
  1314. }
  1315. }
  1316. /**
  1317. * Event callback to handle different events which will close the dropdown.
  1318. *
  1319. * @param {Event} e
  1320. * @type {function}
  1321. */
  1322. handleEsc(e) {
  1323. if (this.multiple) {
  1324. e.preventDefault();
  1325. this.searchContainer.style.display = 'none';
  1326. this.input.setAttribute('aria-expanded', false);
  1327. this.input.blur();
  1328. this.input.classList.remove('ecl-select--active');
  1329. } else {
  1330. this.select.classList.remove('ecl-select--active');
  1331. }
  1332. }
  1333. /**
  1334. * Event callback to handle the click on the clear all button.
  1335. *
  1336. * @param {Event} e
  1337. * @fires Select#onReset
  1338. * @type {function}
  1339. */
  1340. handleClickOnClearAll(e) {
  1341. e.preventDefault();
  1342. Array.from(this.select.options).forEach((option) => {
  1343. const checkbox = this.selectMultiple.querySelector(
  1344. `[data-select-multiple-value="${option.text}"]`,
  1345. );
  1346. const input = checkbox.querySelector('.ecl-checkbox__input');
  1347. input.checked = false;
  1348. option.selected = false;
  1349. });
  1350. this.selectAll.querySelector('.ecl-checkbox__input').checked = false;
  1351. this.update(0);
  1352. this.trigger('onReset', e);
  1353. }
  1354. /**
  1355. * Event callback to reset the multiple select on form reset.
  1356. *
  1357. * @type {function}
  1358. */
  1359. resetForm() {
  1360. if (this.multiple) {
  1361. // A slight timeout is necessary to execute the function just after the original reset of the form.
  1362. setTimeout(() => {
  1363. Array.from(this.select.options).forEach((option) => {
  1364. const checkbox = this.selectMultiple.querySelector(
  1365. `[data-select-multiple-value="${option.text}"]`,
  1366. );
  1367. const input = checkbox.querySelector('.ecl-checkbox__input');
  1368. if (input.checked) {
  1369. option.selected = true;
  1370. } else {
  1371. option.selected = false;
  1372. }
  1373. });
  1374. this.update(0);
  1375. }, 10);
  1376. }
  1377. }
  1378. }
  1379. export default Select;