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