select.js

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