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 three methods are actually used:
  16. * handleToggle(), handleKeyboardOnSelect() and handleEsc().
  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. } else {
  549. this.shouldHandleClick = true;
  550. this.select.addEventListener('keydown', this.handleKeyboardOnSelect);
  551. this.select.addEventListener('blur', this.handleEsc);
  552. this.select.addEventListener('click', this.handleToggle, true);
  553. this.select.addEventListener('mousedown', this.handleToggle, true);
  554. }
  555. document.addEventListener('click', this.handleClickOutside);
  556. // Set ecl initialized attribute
  557. this.element.setAttribute('data-ecl-auto-initialized', 'true');
  558. ECL.components.set(this.element, this);
  559. }
  560. /**
  561. * Update instance.
  562. *
  563. * @param {Integer} i
  564. */
  565. update(i) {
  566. this.#updateCurrentValue();
  567. this.#updateSelectionsCount(i);
  568. }
  569. /**
  570. * Set the selected value(s) programmatically.
  571. *
  572. * @param {string | Array<string>} values - A string or an array of values or labels to set as selected.
  573. * @param {string} [op='replace'] - The operation mode. Use 'add' to keep the previous selections.
  574. * @throws {Error} Throws an error if an invalid operation mode is provided.
  575. *
  576. * @example
  577. * // Replace current selection with new values
  578. * setCurrentValue(['value1', 'value2']);
  579. *
  580. * // Add to current selection without clearing previous selections
  581. * setCurrentValue(['value3', 'value4'], 'add');
  582. *
  583. */
  584. setCurrentValue(values, op = 'replace') {
  585. if (op !== 'replace' && op !== 'add') {
  586. throw new Error('Invalid operation mode. Use "replace" or "add".');
  587. }
  588. const valuesArray = typeof values === 'string' ? [values] : values;
  589. Array.from(this.select.options).forEach((option) => {
  590. if (op === 'replace') {
  591. option.selected = false;
  592. }
  593. if (
  594. valuesArray.includes(option.value) ||
  595. valuesArray.includes(option.label)
  596. ) {
  597. option.selected = true;
  598. }
  599. });
  600. this.update();
  601. }
  602. /**
  603. * Event callback to show/hide the dropdown
  604. *
  605. * @param {Event} e
  606. * @fires Select#onToggle
  607. * @type {function}
  608. */
  609. handleToggle(e) {
  610. if (this.multiple) {
  611. e.preventDefault();
  612. this.input.classList.toggle('ecl-select--active');
  613. if (this.searchContainer.style.display === 'none') {
  614. this.searchContainer.style.display = 'block';
  615. this.input.setAttribute('aria-expanded', true);
  616. this.isOpen = true;
  617. } else {
  618. this.searchContainer.style.display = 'none';
  619. this.input.setAttribute('aria-expanded', false);
  620. this.isOpen = false;
  621. }
  622. } else if (e.type === 'click' && !this.shouldHandleClick) {
  623. this.shouldHandleClick = true;
  624. this.select.classList.toggle('ecl-select--active');
  625. } else if (e.type === 'mousedown' && this.shouldHandleClick) {
  626. this.shouldHandleClick = false;
  627. this.select.classList.toggle('ecl-select--active');
  628. } else if (e.type === 'keydown') {
  629. this.shouldHandleClick = false;
  630. this.select.classList.toggle('ecl-select--active');
  631. }
  632. const eventData = { opened: this.isOpen, e };
  633. this.trigger('onToggle', eventData);
  634. }
  635. /**
  636. * Register a callback function for a specific event.
  637. *
  638. * @param {string} eventName - The name of the event to listen for.
  639. * @param {Function} callback - The callback function to be invoked when the event occurs.
  640. * @returns {void}
  641. * @memberof Select
  642. * @instance
  643. *
  644. * @example
  645. * // Registering a callback for the 'onToggle' event
  646. * select.on('onToggle', (event) => {
  647. * console.log('Toggle event occurred!', event);
  648. * });
  649. */
  650. on(eventName, callback) {
  651. this.eventManager.on(eventName, callback);
  652. }
  653. /**
  654. * Trigger a component event.
  655. *
  656. * @param {string} eventName - The name of the event to trigger.
  657. * @param {any} eventData - Data associated with the event.
  658. * @memberof Select
  659. * @instance
  660. *
  661. */
  662. trigger(eventName, eventData) {
  663. this.eventManager.trigger(eventName, eventData);
  664. }
  665. /**
  666. * Destroy the component instance.
  667. */
  668. destroy() {
  669. if (this.multiple) {
  670. this.selectMultiple.removeEventListener('focusout', this.handleFocusout);
  671. this.input.removeEventListener('keydown', this.handleKeyboardOnSelect);
  672. this.input.removeEventListener('click', this.handleToggle);
  673. this.search.removeEventListener('keyup', this.handleSearch);
  674. this.search.removeEventListener('keydown', this.handleKeyboardOnSearch);
  675. this.selectAll.removeEventListener('click', this.handleClickSelectAll);
  676. this.selectAll.removeEventListener('keypress', this.handleClickSelectAll);
  677. this.selectAll.removeEventListener(
  678. 'keydown',
  679. this.handleKeyboardOnSelectAll,
  680. );
  681. this.optionsContainer.removeEventListener(
  682. 'keydown',
  683. this.handleKeyboardOnOptions,
  684. );
  685. this.checkboxes.forEach((checkbox) => {
  686. checkbox.removeEventListener('click', this.handleClickSelectAll);
  687. checkbox.removeEventListener('click', this.handleClickOption);
  688. checkbox.removeEventListener('keydown', this.handleKeyboardOnOption);
  689. });
  690. document.removeEventListener('click', this.handleClickOutside);
  691. if (this.closeButton) {
  692. this.closeButton.removeEventListener('click', this.handleEsc);
  693. this.closeButton.removeEventListener(
  694. 'keydown',
  695. this.handleKeyboardOnClose,
  696. );
  697. }
  698. if (this.clearAllButton) {
  699. this.clearAllButton.removeEventListener(
  700. 'click',
  701. this.handleClickOnClearAll,
  702. );
  703. this.clearAllButton.removeEventListener(
  704. 'keydown',
  705. this.handleKeyboardOnClearAll,
  706. );
  707. }
  708. if (this.selectMultiple) {
  709. this.selectMultiple.remove();
  710. }
  711. this.select.parentNode.classList.remove('ecl-select__container--hidden');
  712. } else {
  713. this.select.removeEventListener('focus', this.handleToggle);
  714. }
  715. this.select.removeEventListener('blur', this.handleToggle);
  716. document.removeEventListener('click', this.handleClickOutside);
  717. if (this.element) {
  718. this.element.removeAttribute('data-ecl-auto-initialized');
  719. ECL.components.delete(this.element);
  720. }
  721. }
  722. /**
  723. * Private method to handle the update of the selected options counter.
  724. *
  725. * @param {Integer} i
  726. * @private
  727. */
  728. #updateSelectionsCount(i) {
  729. let selectedOptionsCount = 0;
  730. if (i > 0) {
  731. this.selectionCount.querySelector('span').innerHTML += i;
  732. } else {
  733. selectedOptionsCount = Array.from(this.select.options).filter(
  734. (option) => option.selected,
  735. ).length;
  736. }
  737. if (selectedOptionsCount > 0) {
  738. this.selectionCount.querySelector('span').innerHTML =
  739. selectedOptionsCount;
  740. this.selectionCount.classList.add(
  741. 'ecl-select-multiple-selections-counter--visible',
  742. );
  743. if (this.dropDownToolbar) {
  744. this.dropDownToolbar.style.display = 'flex';
  745. }
  746. } else {
  747. this.selectionCount.classList.remove(
  748. 'ecl-select-multiple-selections-counter--visible',
  749. );
  750. if (this.dropDownToolbar) {
  751. this.dropDownToolbar.style.display = 'none';
  752. }
  753. }
  754. if (selectedOptionsCount >= 100) {
  755. this.selectionCount.classList.add(
  756. 'ecl-select-multiple-selections-counter--xxl',
  757. );
  758. }
  759. }
  760. /**
  761. * Private method to update the select value.
  762. *
  763. * @fires Select#onSelection
  764. * @private
  765. */
  766. #updateCurrentValue() {
  767. const optionSelected = Array.from(this.select.options)
  768. .filter((option) => option.selected) // do not rely on getAttribute as it does not work in all cases
  769. .map((option) => option.text)
  770. .join(', ');
  771. this.input.innerHTML = optionSelected || this.textDefault || '';
  772. if (optionSelected !== '' && this.label) {
  773. this.label.setAttribute(
  774. 'aria-label',
  775. `${this.label.innerText} ${optionSelected}`,
  776. );
  777. } else if (optionSelected === '' && this.label) {
  778. this.label.removeAttribute('aria-label');
  779. }
  780. this.trigger('onSelection', { selected: optionSelected });
  781. // Dispatch a change event once the value of the select has changed.
  782. this.select.dispatchEvent(new window.Event('change', { bubbles: true }));
  783. }
  784. /**
  785. * Private method to handle the focus switch.
  786. *
  787. * @param {upOrDown}
  788. * @private
  789. */
  790. #moveFocus(upOrDown) {
  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 (
  818. this.dropDownToolbar &&
  819. this.dropDownToolbar.style.display === 'flex'
  820. ) {
  821. this.dropDownToolbar.firstChild.focus();
  822. } else {
  823. this.input.focus();
  824. }
  825. }
  826. } else {
  827. const previousSiblings = options
  828. .splice(0, activeIndex)
  829. .filter(
  830. (el) => !el.disabled && el.parentElement.style.display !== 'none',
  831. );
  832. if (previousSiblings.length > 0) {
  833. previousSiblings[previousSiblings.length - 1].focus();
  834. } else {
  835. this.optionsContainer.scrollTop = 0;
  836. if (!this.selectAll.querySelector('input').disabled) {
  837. this.selectAll.querySelector('input').focus();
  838. } else {
  839. this.search.focus();
  840. }
  841. }
  842. }
  843. }
  844. /**
  845. * Event callback to handle the click on a checkbox.
  846. *
  847. * @param {Event} e
  848. * @type {function}
  849. */
  850. handleClickOption(e) {
  851. e.preventDefault();
  852. Select.#checkCheckbox(e);
  853. // Toggle values
  854. const checkbox = e.target.closest('.ecl-checkbox');
  855. Array.from(this.select.options).forEach((option) => {
  856. if (option.text === checkbox.getAttribute('data-select-multiple-value')) {
  857. if (option.getAttribute('selected') || option.selected) {
  858. option.selected = false;
  859. this.selectAll.querySelector('input').checked = false;
  860. } else {
  861. option.selected = true;
  862. }
  863. }
  864. });
  865. this.update();
  866. }
  867. /**
  868. * Event callback to handle the click on the select all checkbox.
  869. *
  870. * @param {Event} e
  871. * @fires Select#onSelectAll
  872. * @type {function}
  873. */
  874. handleClickSelectAll(e) {
  875. e.preventDefault();
  876. // Early returns.
  877. if (this.selectAll.querySelector('input').disabled) {
  878. return;
  879. }
  880. const checked = Select.#checkCheckbox(e);
  881. const options = Array.from(this.select.options).filter((o) => !o.disabled);
  882. const checkboxes = Array.from(
  883. this.searchContainer.querySelectorAll('[data-visible="true"]'),
  884. ).filter((checkbox) => !checkbox.querySelector('input').disabled);
  885. checkboxes.forEach((checkbox) => {
  886. checkbox.querySelector('input').checked = checked;
  887. const option = options.find(
  888. (o) => o.text === checkbox.getAttribute('data-select-multiple-value'),
  889. );
  890. if (option) {
  891. if (checked) {
  892. option.selected = true;
  893. } else {
  894. option.selected = false;
  895. }
  896. }
  897. });
  898. this.update();
  899. this.trigger('onSelectAll', { selected: options });
  900. }
  901. /**
  902. * Event callback to handle moving the focus out of the select.
  903. *
  904. * @param {Event} e
  905. * @type {function}
  906. */
  907. handleFocusout(e) {
  908. if (
  909. e.relatedTarget &&
  910. this.selectMultiple &&
  911. !this.selectMultiple.contains(e.relatedTarget) &&
  912. this.searchContainer.style.display === 'block'
  913. ) {
  914. this.searchContainer.style.display = 'none';
  915. this.input.classList.remove('ecl-select--active');
  916. this.input.setAttribute('aria-expanded', false);
  917. } else if (
  918. e.relatedTarget &&
  919. !this.selectMultiple &&
  920. !this.select.parentNode.contains(e.relatedTarget)
  921. ) {
  922. this.select.blur();
  923. }
  924. }
  925. /**
  926. * Event callback to handle the user typing in the search field.
  927. *
  928. * @param {Event} e
  929. * @fires Select#onSearch
  930. * @type {function}
  931. */
  932. handleSearch(e) {
  933. const dropDownHeight = this.optionsContainer.offsetHeight;
  934. this.visibleOptions = [];
  935. const keyword = e.target.value.toLowerCase();
  936. let eventDetails = {};
  937. if (dropDownHeight > 0) {
  938. this.optionsContainer.style.height = `${dropDownHeight}px`;
  939. }
  940. this.checkboxes.forEach((checkbox) => {
  941. if (
  942. !checkbox
  943. .getAttribute('data-select-multiple-value')
  944. .toLocaleLowerCase()
  945. .includes(keyword)
  946. ) {
  947. checkbox.removeAttribute('data-visible');
  948. checkbox.style.display = 'none';
  949. } else {
  950. checkbox.setAttribute('data-visible', true);
  951. checkbox.style.display = 'flex';
  952. // Highlight keyword in checkbox label.
  953. const checkboxLabelText = checkbox.querySelector(
  954. '.ecl-checkbox__label-text',
  955. );
  956. checkboxLabelText.textContent = checkboxLabelText.textContent.replace(
  957. '.cls-1{fill:none}',
  958. '',
  959. );
  960. if (keyword) {
  961. checkboxLabelText.innerHTML = checkboxLabelText.textContent.replace(
  962. new RegExp(`${keyword}(?!([^<]+)?<)`, 'gi'),
  963. '<b>$&</b>',
  964. );
  965. }
  966. this.visibleOptions.push(checkbox);
  967. }
  968. });
  969. // Select all checkbox follows along.
  970. const checked = this.visibleOptions.filter(
  971. (c) => c.querySelector('input').checked,
  972. );
  973. if (
  974. this.visibleOptions.length === 0 ||
  975. this.visibleOptions.length !== checked.length
  976. ) {
  977. this.selectAll.querySelector('input').checked = false;
  978. } else {
  979. this.selectAll.querySelector('input').checked = true;
  980. }
  981. // Display no-results message.
  982. const noResultsElement = this.searchContainer.querySelector(
  983. '.ecl-select__multiple-no-results',
  984. );
  985. const groups = this.optionsContainer.getElementsByClassName(
  986. 'ecl-select__multiple-group',
  987. );
  988. // eslint-disable-next-line no-restricted-syntax
  989. for (const group of groups) {
  990. group.style.display = 'none';
  991. // eslint-disable-next-line no-restricted-syntax
  992. const groupedCheckboxes = [...group.children].filter((node) =>
  993. node.classList.contains('ecl-checkbox'),
  994. );
  995. groupedCheckboxes.forEach((single) => {
  996. if (single.hasAttribute('data-visible')) {
  997. single.closest('.ecl-select__multiple-group').style.display = 'block';
  998. }
  999. });
  1000. }
  1001. if (this.visibleOptions.length === 0 && !noResultsElement) {
  1002. // Create no-results element.
  1003. const noResultsContainer = document.createElement('div');
  1004. const noResultsLabel = document.createElement('span');
  1005. noResultsContainer.classList.add('ecl-select__multiple-no-results');
  1006. noResultsLabel.innerHTML = this.textNoResults;
  1007. noResultsContainer.appendChild(noResultsLabel);
  1008. this.optionsContainer.appendChild(noResultsContainer);
  1009. } else if (this.visibleOptions.length > 0 && noResultsElement !== null) {
  1010. noResultsElement.parentNode.removeChild(noResultsElement);
  1011. }
  1012. // reset
  1013. if (keyword.length === 0) {
  1014. this.checkboxes.forEach((checkbox) => {
  1015. checkbox.setAttribute('data-visible', true);
  1016. checkbox.style.display = 'flex';
  1017. });
  1018. // Enable select all checkbox.
  1019. this.selectAll.classList.remove('ecl-checkbox--disabled');
  1020. this.selectAll.querySelector('input').disabled = false;
  1021. } else {
  1022. // Disable select all checkbox.
  1023. this.selectAll.classList.add('ecl-checkbox--disabled');
  1024. this.selectAll.querySelector('input').disabled = true;
  1025. }
  1026. if (this.visibleOptions.length > 0) {
  1027. const visibleLabels = this.visibleOptions.map((option) => {
  1028. let label = null;
  1029. const labelEl = queryOne('.ecl-checkbox__label-text', option);
  1030. if (labelEl) {
  1031. label = labelEl.innerHTML.replace(/<\/?b>/g, '');
  1032. }
  1033. return label || '';
  1034. });
  1035. eventDetails = {
  1036. results: visibleLabels,
  1037. text: e.target.value.toLowerCase(),
  1038. };
  1039. } else {
  1040. eventDetails = { results: 'none', text: e.target.value.toLowerCase() };
  1041. }
  1042. this.trigger('onSearch', eventDetails);
  1043. }
  1044. /**
  1045. * Event callback to handle the click outside the select.
  1046. *
  1047. * @param {Event} e
  1048. * @type {function}
  1049. */
  1050. handleClickOutside(e) {
  1051. if (
  1052. e.target &&
  1053. this.selectMultiple &&
  1054. !this.selectMultiple.contains(e.target) &&
  1055. this.searchContainer.style.display === 'block'
  1056. ) {
  1057. this.searchContainer.style.display = 'none';
  1058. this.input.classList.remove('ecl-select--active');
  1059. this.input.setAttribute('aria-expanded', false);
  1060. } else if (
  1061. e.target &&
  1062. !this.selectMultiple &&
  1063. !this.select.parentNode.contains(e.target)
  1064. ) {
  1065. this.select.classList.remove('ecl-select--active');
  1066. }
  1067. }
  1068. /**
  1069. * Event callback to handle keyboard events on the select.
  1070. *
  1071. * @param {Event} e
  1072. * @type {function}
  1073. */
  1074. handleKeyboardOnSelect(e) {
  1075. switch (e.key) {
  1076. case 'Escape':
  1077. e.preventDefault();
  1078. this.handleEsc(e);
  1079. break;
  1080. case ' ':
  1081. case 'Enter':
  1082. this.handleToggle(e);
  1083. if (this.multiple) {
  1084. e.preventDefault();
  1085. this.search.focus();
  1086. }
  1087. break;
  1088. case 'ArrowDown':
  1089. if (this.multiple) {
  1090. e.preventDefault();
  1091. this.handleToggle(e);
  1092. this.search.focus();
  1093. }
  1094. break;
  1095. default:
  1096. }
  1097. }
  1098. /**
  1099. * Event callback to handle keyboard events on the select all checkbox.
  1100. *
  1101. * @param {Event} e
  1102. * @type {function}
  1103. */
  1104. handleKeyboardOnSelectAll(e) {
  1105. switch (e.key) {
  1106. case 'Escape':
  1107. e.preventDefault();
  1108. this.handleEsc(e);
  1109. break;
  1110. case 'ArrowDown':
  1111. e.preventDefault();
  1112. if (this.visibleOptions.length > 0) {
  1113. this.visibleOptions[0].querySelector('input').focus();
  1114. } else {
  1115. this.input.focus();
  1116. }
  1117. break;
  1118. case 'ArrowUp':
  1119. e.preventDefault();
  1120. this.search.focus();
  1121. break;
  1122. case 'Tab':
  1123. e.preventDefault();
  1124. if (e.shiftKey) {
  1125. this.search.focus();
  1126. } else if (this.visibleOptions.length > 0) {
  1127. this.visibleOptions[0].querySelector('input').focus();
  1128. } else {
  1129. this.input.focus();
  1130. }
  1131. break;
  1132. default:
  1133. }
  1134. }
  1135. /**
  1136. * Event callback to handle keyboard events on the dropdown.
  1137. *
  1138. * @param {Event} e
  1139. * @type {function}
  1140. */
  1141. handleKeyboardOnOptions(e) {
  1142. switch (e.key) {
  1143. case 'Escape':
  1144. e.preventDefault();
  1145. this.handleEsc(e);
  1146. break;
  1147. case 'ArrowDown':
  1148. e.preventDefault();
  1149. this.#moveFocus('down');
  1150. break;
  1151. case 'ArrowUp':
  1152. e.preventDefault();
  1153. this.#moveFocus('up');
  1154. break;
  1155. case 'Tab':
  1156. e.preventDefault();
  1157. if (e.shiftKey) {
  1158. this.#moveFocus('up');
  1159. } else {
  1160. this.#moveFocus('down');
  1161. }
  1162. break;
  1163. default:
  1164. }
  1165. }
  1166. /**
  1167. * Event callback to handle keyboard events
  1168. *
  1169. * @param {Event} e
  1170. * @type {function}
  1171. */
  1172. handleKeyboardOnSearch(e) {
  1173. switch (e.key) {
  1174. case 'Escape':
  1175. e.preventDefault();
  1176. this.handleEsc(e);
  1177. break;
  1178. case 'ArrowDown':
  1179. e.preventDefault();
  1180. if (this.selectAll.querySelector('input').disabled) {
  1181. if (this.visibleOptions.length > 0) {
  1182. this.visibleOptions[0].querySelector('input').focus();
  1183. } else {
  1184. this.input.focus();
  1185. }
  1186. } else {
  1187. this.selectAll.querySelector('input').focus();
  1188. }
  1189. break;
  1190. case 'ArrowUp':
  1191. e.preventDefault();
  1192. this.input.focus();
  1193. this.handleToggle(e);
  1194. break;
  1195. default:
  1196. }
  1197. }
  1198. /**
  1199. * Event callback to handle the click on an option.
  1200. *
  1201. * @param {Event} e
  1202. * @type {function}
  1203. */
  1204. handleKeyboardOnOption(e) {
  1205. if (e.key === 'Enter' || e.key === ' ') {
  1206. e.preventDefault();
  1207. this.handleClickOption(e);
  1208. }
  1209. }
  1210. /**
  1211. * Event callback to handle keyboard events on the clear all button.
  1212. *
  1213. * @param {Event} e
  1214. * @fires Select#onReset
  1215. * @type {function}
  1216. */
  1217. handleKeyboardOnClearAll(e) {
  1218. e.preventDefault();
  1219. switch (e.key) {
  1220. case 'Enter':
  1221. case ' ':
  1222. this.handleClickOnClearAll(e);
  1223. this.trigger('onReset', e);
  1224. this.input.focus();
  1225. break;
  1226. case 'ArrowDown':
  1227. this.input.focus();
  1228. break;
  1229. case 'ArrowUp':
  1230. if (this.closeButton) {
  1231. this.closeButton.focus();
  1232. } else {
  1233. // eslint-disable-next-line no-lonely-if
  1234. if (this.visibleOptions.length > 0) {
  1235. this.visibleOptions[this.visibleOptions.length - 1]
  1236. .querySelector('input')
  1237. .focus();
  1238. } else {
  1239. this.search.focus();
  1240. }
  1241. }
  1242. break;
  1243. case 'Tab':
  1244. if (e.shiftKey) {
  1245. if (this.closeButton) {
  1246. this.closeButton.focus();
  1247. } else {
  1248. // eslint-disable-next-line no-lonely-if
  1249. if (this.visibleOptions.length > 0) {
  1250. this.visibleOptions[this.visibleOptions.length - 1]
  1251. .querySelector('input')
  1252. .focus();
  1253. } else {
  1254. this.search.focus();
  1255. }
  1256. }
  1257. } else {
  1258. this.input.focus();
  1259. this.handleToggle(e);
  1260. }
  1261. break;
  1262. default:
  1263. }
  1264. }
  1265. /**
  1266. * Event callback for handling keyboard events in the close button.
  1267. *
  1268. * @param {Event} e
  1269. * @type {function}
  1270. */
  1271. handleKeyboardOnClose(e) {
  1272. e.preventDefault();
  1273. switch (e.key) {
  1274. case 'Enter':
  1275. case ' ':
  1276. this.handleEsc(e);
  1277. this.input.focus();
  1278. break;
  1279. case 'ArrowUp':
  1280. if (this.visibleOptions.length > 0) {
  1281. this.visibleOptions[this.visibleOptions.length - 1]
  1282. .querySelector('input')
  1283. .focus();
  1284. } else {
  1285. this.input.focus();
  1286. this.handleToggle(e);
  1287. }
  1288. break;
  1289. case 'ArrowDown':
  1290. if (this.clearAllButton) {
  1291. this.clearAllButton.focus();
  1292. } else {
  1293. this.input.focus();
  1294. this.handleToggle(e);
  1295. }
  1296. break;
  1297. case 'Tab':
  1298. if (!e.shiftKey) {
  1299. if (this.clearAllButton) {
  1300. this.clearAllButton.focus();
  1301. } else {
  1302. this.input.focus();
  1303. this.handleToggle(e);
  1304. }
  1305. } else {
  1306. // eslint-disable-next-line no-lonely-if
  1307. if (this.visibleOptions.length > 0) {
  1308. this.visibleOptions[this.visibleOptions.length - 1]
  1309. .querySelector('input')
  1310. .focus();
  1311. } else {
  1312. this.input.focus();
  1313. this.handleToggle(e);
  1314. }
  1315. }
  1316. break;
  1317. default:
  1318. }
  1319. }
  1320. /**
  1321. * Event callback to handle different events which will close the dropdown.
  1322. *
  1323. * @param {Event} e
  1324. * @type {function}
  1325. */
  1326. handleEsc(e) {
  1327. if (this.multiple) {
  1328. e.preventDefault();
  1329. this.searchContainer.style.display = 'none';
  1330. this.input.setAttribute('aria-expanded', false);
  1331. this.input.blur();
  1332. this.input.classList.remove('ecl-select--active');
  1333. } else {
  1334. this.select.classList.remove('ecl-select--active');
  1335. }
  1336. }
  1337. /**
  1338. * Event callback to handle the click on the clear all button.
  1339. *
  1340. * @param {Event} e
  1341. * @fires Select#onReset
  1342. * @type {function}
  1343. */
  1344. handleClickOnClearAll(e) {
  1345. e.preventDefault();
  1346. Array.from(this.select.options).forEach((option) => {
  1347. const checkbox = this.selectMultiple.querySelector(
  1348. `[data-select-multiple-value="${option.text}"]`,
  1349. );
  1350. const input = checkbox.querySelector('.ecl-checkbox__input');
  1351. input.checked = false;
  1352. option.selected = false;
  1353. });
  1354. this.selectAll.querySelector('.ecl-checkbox__input').checked = false;
  1355. this.update(0);
  1356. this.trigger('onReset', e);
  1357. }
  1358. /**
  1359. * Event callback to reset the multiple select on form reset.
  1360. *
  1361. * @type {function}
  1362. */
  1363. resetForm() {
  1364. if (this.multiple) {
  1365. // A slight timeout is necessary to execute the function just after the original reset of the form.
  1366. setTimeout(() => {
  1367. Array.from(this.select.options).forEach((option) => {
  1368. const checkbox = this.selectMultiple.querySelector(
  1369. `[data-select-multiple-value="${option.text}"]`,
  1370. );
  1371. const input = checkbox.querySelector('.ecl-checkbox__input');
  1372. if (input.checked) {
  1373. option.selected = true;
  1374. } else {
  1375. option.selected = false;
  1376. }
  1377. });
  1378. this.update(0);
  1379. }, 10);
  1380. }
  1381. }
  1382. }
  1383. export default Select;