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