ExamplesPlaygroundReference Source

coral-spectrum/coral-component-select/src/scripts/Select.js

  1. /**
  2. * Copyright 2019 Adobe. All rights reserved.
  3. * This file is licensed to you under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License. You may obtain a copy
  5. * of the License at http://www.apache.org/licenses/LICENSE-2.0
  6. *
  7. * Unless required by applicable law or agreed to in writing, software distributed under
  8. * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
  9. * OF ANY KIND, either express or implied. See the License for the specific language
  10. * governing permissions and limitations under the License.
  11. */
  12.  
  13. import {BaseComponent} from '../../../coral-base-component';
  14. import {BaseFormField} from '../../../coral-base-formfield';
  15. import {SelectableCollection} from '../../../coral-collection';
  16. import '../../../coral-component-button';
  17. import {Tag} from '../../../coral-component-taglist';
  18. import {SelectList} from '../../../coral-component-list';
  19. import {Icon} from '../../../coral-component-icon';
  20. import '../../../coral-component-popover';
  21. import base from '../templates/base';
  22. import {transform, validate, commons, i18n, Keys} from '../../../coral-utils';
  23. import {Decorator} from '../../../coral-decorator';
  24.  
  25. /**
  26. Enumeration for {@link Select} variants.
  27.  
  28. @typedef {Object} SelectVariantEnum
  29.  
  30. @property {String} DEFAULT
  31. A default, gray Select.
  32. @property {String} QUIET
  33. A Select with no border or background.
  34. */
  35. const variant = {
  36. DEFAULT: 'default',
  37. QUIET: 'quiet'
  38. };
  39.  
  40. const CLASSNAME = '_coral-Dropdown';
  41.  
  42. // used in 'auto' mode to determine if the client is on mobile.
  43. const IS_MOBILE_DEVICE = navigator.userAgent.match(/iPhone|iPad|iPod|Android/i) !== null;
  44.  
  45. /**
  46. Extracts the value from the item in case no explicit value was provided.
  47.  
  48. @param {HTMLElement} item
  49. the item whose value will be extracted.
  50.  
  51. @returns {String} the value that will be submitted for this item.
  52.  
  53. @private
  54. */
  55. const itemValueFromDOM = function (item) {
  56. const attr = item.getAttribute('value');
  57. // checking explicitely for null allows to differenciate between non set values and empty strings
  58. return attr !== null ? attr : item.textContent.replace(/\s{2,}/g, ' ').trim();
  59. };
  60.  
  61. /**
  62. Calculates the difference between two given arrays. It returns the items that are in a that are not in b.
  63.  
  64. @param {Array.<String>} a
  65. @param {Array.<String>} b
  66.  
  67. @returns {Array.<String>}
  68. the difference between the arrays.
  69. */
  70. const arrayDiff = function (a, b) {
  71. return a.filter((item) => !b.some((item2) => item === item2));
  72. };
  73.  
  74. /**
  75. @class Coral.Select
  76. @classdesc A Select component is a form field that allows users to select from a list of options. If this component is
  77. shown on a mobile device, it will show a native select list, instead of the select list styled via Coral Spectrum.
  78. @htmltag coral-select
  79. @extends {HTMLElement}
  80. @extends {BaseComponent}
  81. @extends {BaseFormField}
  82. */
  83. const Select = Decorator(class extends BaseFormField(BaseComponent(HTMLElement)) {
  84. /** @ignore */
  85. constructor() {
  86. super();
  87.  
  88. // Templates
  89. this._elements = {};
  90. base.call(this._elements, {commons, Icon, i18n});
  91.  
  92. const events = {
  93. 'global:click': '_onGlobalClick',
  94. 'global:touchstart': '_onGlobalClick',
  95.  
  96. 'coral-collection:add coral-taglist': '_onInternalEvent',
  97. 'coral-collection:remove coral-taglist': '_onInternalEvent',
  98.  
  99. 'change coral-taglist': '_onTagListChange',
  100. 'change select': '_onNativeSelectChange',
  101. 'click select': '_onNativeSelectClick',
  102. 'click > ._coral-Dropdown-trigger': '_onButtonClick',
  103.  
  104. 'key:space > ._coral-Dropdown-trigger': '_onSpaceKey',
  105. 'key:enter > ._coral-Dropdown-trigger': '_onSpaceKey',
  106. 'key:return > ._coral-Dropdown-trigger': '_onSpaceKey',
  107. 'key:down > ._coral-Dropdown-trigger': '_onSpaceKey',
  108.  
  109. // Messenger
  110. 'coral-select-item:_messengerconnected': '_onMessengerConnected',
  111. };
  112.  
  113. // Overlay
  114. const overlayId = this._elements.overlay.id;
  115. events[`global:capture:coral-collection:add #${overlayId} coral-selectlist`] = '_onSelectListItemAdd';
  116. events[`global:capture:coral-collection:remove #${overlayId} coral-selectlist`] = '_onInternalEvent';
  117. events[`global:capture:coral-selectlist:beforechange #${overlayId}`] = '_onSelectListBeforeChange';
  118. events[`global:capture:coral-selectlist:change #${overlayId}`] = '_onSelectListChange';
  119. events[`global:capture:coral-selectlist:scrollbottom #${overlayId}`] = '_onSelectListScrollBottom';
  120. events[`global:capture:coral-overlay:close #${overlayId}`] = '_onOverlayToggle';
  121. events[`global:capture:coral-overlay:open #${overlayId}`] = '_onOverlayToggle';
  122. events[`global:capture:coral-overlay:positioned #${overlayId}`] = '_onOverlayPositioned';
  123. events[`global:capture:coral-overlay:beforeopen #${overlayId}`] = '_onBeforeOpen';
  124. events[`global:capture:coral-overlay:beforeclose #${overlayId}`] = '_onInternalEvent';
  125. // Keyboard interaction
  126. events[`global:keypress #${overlayId}`] = '_onOverlayKeyPress';
  127. // TODO for some reason this disables tabbing into the select
  128. // events[`global:key:tab #${overlayId} coral-selectlist-item`] = '_onTabKey';
  129. // events[`global:key:tab+shift #${overlayId} coral-selectlist-item`] = '_onTabKey';
  130.  
  131. // Attach events
  132. this._delegateEvents(commons.extend(this._events, events));
  133.  
  134. // Pre-define labellable element
  135. this._labellableElement = this._elements.button;
  136.  
  137. // default value of inner flag to process events
  138. this._bulkSelectionChange = false;
  139.  
  140. // we only have AUTO mode.
  141. this._useNativeInput = IS_MOBILE_DEVICE;
  142.  
  143. this._elements.taglist.reset = () => {
  144. // since reseting a form will call the reset on every component, we need to kill the behavior of the taglist
  145. // otherwise the state will not be accurate
  146. };
  147.  
  148. this._initialValues = [];
  149.  
  150. // Init the collection mutation observer
  151. this.items._startHandlingItems();
  152. }
  153.  
  154. /**
  155. Returns the inner overlay to allow customization.
  156.  
  157. @type {Popover}
  158. @readonly
  159. */
  160. get overlay() {
  161. return this._elements.overlay;
  162. }
  163.  
  164. /**
  165. The item collection.
  166.  
  167. @type {SelectableCollection}
  168. @readonly
  169. */
  170. get items() {
  171. // we do lazy initialization of the collection
  172. if (!this._items) {
  173. this._items = new SelectableCollection({
  174. host: this,
  175. itemTagName: 'coral-select-item',
  176. onItemAdded: this._onItemAdded,
  177. onItemRemoved: this._onItemRemoved,
  178. onCollectionChange: this._onCollectionChange
  179. });
  180. }
  181. return this._items;
  182. }
  183.  
  184. /**
  185. Indicates whether the select accepts multiple selected values.
  186.  
  187. @type {Boolean}
  188. @default false
  189. @htmlattribute multiple
  190. @htmlattributereflected
  191. */
  192. get multiple() {
  193. return this._multiple || false;
  194. }
  195.  
  196. set multiple(value) {
  197. this._multiple = transform.booleanAttr(value);
  198. this._reflectAttribute('multiple', this._multiple);
  199.  
  200. // taglist should not be in DOM if multiple === false
  201. if (!this._multiple) {
  202. this.removeChild(this._elements.taglist);
  203. } else {
  204. this.appendChild(this._elements.taglist);
  205. }
  206.  
  207. // we need to remove and re-add the native select to loose the selection
  208. if (this._nativeInput) {
  209. this.removeChild(this._elements.nativeSelect);
  210. }
  211. this._elements.nativeSelect.multiple = this._multiple;
  212. this._elements.nativeSelect.selectedIndex = -1;
  213.  
  214. if (this._nativeInput) {
  215. if (this._multiple) {
  216. // We might not be rendered yet
  217. if (this._elements.nativeSelect.parentNode) {
  218. this.insertBefore(this._elements.nativeSelect, this._elements.taglist);
  219. }
  220. } else {
  221. this.appendChild(this._elements.nativeSelect);
  222. }
  223. }
  224.  
  225. this._elements.list.multiple = this._multiple;
  226.  
  227. // sets the correct name for value submission
  228. this._setName(this.getAttribute('name') || '');
  229.  
  230. // we need to make sure the selection is valid
  231. this._setStateFromDOM();
  232.  
  233. // everytime multiple changes, the state of the selectlist and taglist need to be updated
  234. this.items.getAll().forEach((item) => {
  235. if (this._multiple && item.hasAttribute('selected')) {
  236. this._addTagToTagList(item);
  237. } else {
  238. // taglist is never used for multiple = false
  239. this._removeTagFromTagList(item);
  240.  
  241. // when multiple = false and the item is selected, the value needs to be updated in the input
  242. if (item.hasAttribute('selected')) {
  243. this._elements.input.value = itemValueFromDOM(item);
  244. }
  245. }
  246. });
  247. }
  248.  
  249. /**
  250. Contains a hint to the user of what can be selected in the component. If no placeholder is provided, the first
  251. option will be displayed in the component.
  252.  
  253. @type {String}
  254. @default ""
  255. @htmlattribute placeholder
  256. @htmlattributereflected
  257. */
  258. // p = placeholder, m = multiple, se = selected
  259. // case 1: p + m + se = p
  260. // case 2: p + m + !se = p
  261. // case 3: !p + !m + se = se
  262. // case 4: !p + !m + !se = firstSelectable (native behavior)
  263. // case 5: p + !m + se = se
  264. // case 6: p + !m + !se = p
  265. // case 7: !p + m + se = 'Select'
  266. // case 8: !p + m + !se = 'Select'
  267. get placeholder() {
  268. return this._placeholder || '';
  269. }
  270.  
  271. set placeholder(value) {
  272. this._placeholder = transform.string(value);
  273. this._reflectAttribute('placeholder', this._placeholder);
  274.  
  275. // case 1: p + m + se = p
  276. // case 2: p + m + !se = p
  277. // case 6: p + !m + !se = p
  278. if (this._placeholder && (this.hasAttribute('multiple') || !this.selectedItem)) {
  279. this._elements.label.classList.add('is-placeholder');
  280. this._elements.label.textContent = this._placeholder;
  281. }
  282. // case 7: !p + m + se = 'Select'
  283. // case 8: !p + m + !se = 'Select'
  284. else if (this.hasAttribute('multiple')) {
  285. this._elements.label.classList.add('is-placeholder');
  286. this._elements.label.textContent = i18n.get('Select');
  287. }
  288. // case 4: !p + !m + !se = firstSelectable (native behavior)
  289. else if (!this.selectedItem) {
  290. // we clean the value because there is no selected item
  291. this._elements.input.value = '';
  292.  
  293. // gets the first candidate for selection
  294. const placeholderItem = this.items._getFirstSelectable();
  295. this._elements.label.classList.remove('is-placeholder');
  296.  
  297. if (placeholderItem) {
  298. // selects using the attribute in case the item is not yet initialized
  299. placeholderItem.setAttribute('selected', '');
  300. this._elements.label.innerHTML = placeholderItem.innerHTML;
  301. } else {
  302. // label must be cleared when there is no placeholder and no item to select
  303. this._elements.label.textContent = '';
  304. }
  305. }
  306. }
  307.  
  308. /**
  309. Name used to submit the data in a form.
  310. @type {String}
  311. @default ""
  312. @htmlattribute name
  313. @htmlattributereflected
  314. */
  315. get name() {
  316. return this.multiple ? this._elements.taglist.name : this._elements.input.name;
  317. }
  318.  
  319. set name(value) {
  320. this._setName(value);
  321. this._reflectAttribute('name', this.name);
  322. }
  323.  
  324. /**
  325. This field's current value.
  326. @type {String}
  327. @default ""
  328. @htmlattribute value
  329. */
  330. get value() {
  331. // we leverage the internal elements to know the value, this way we are always sure that the server submission
  332. // will be correct
  333. return this.multiple ? this._elements.taglist.value : this._elements.input.value;
  334. }
  335.  
  336. set value(value) {
  337. // we rely on the the values property to handle this correctly
  338. this.values = [value];
  339. }
  340.  
  341. /**
  342. The current selected values, as submitted during form submission. When {@link Coral.Select#multiple} is
  343. <code>false</code>, this will be an array of length 1.
  344.  
  345. @type {Array.<String>}
  346. */
  347. get values() {
  348. if (this.multiple) {
  349. return this._elements.taglist.values;
  350. }
  351.  
  352. // if there is a selection, we return whatever value it has assigned
  353. return this.selectedItem ? [this._elements.input.value] : [];
  354. }
  355.  
  356. set values(values) {
  357. if (Array.isArray(values)) {
  358. // when multiple = false, we explicitely ignore the other values and just set the first one
  359. if (!this.multiple && values.length > 1) {
  360. values = [values[0]];
  361. }
  362.  
  363. // gets all the items
  364. const items = this.items.getAll();
  365.  
  366. let itemValue;
  367. // if multiple, we need to explicitely set the selection state of every item
  368. if (this.multiple) {
  369. items.forEach((item) => {
  370. // we use DOM API instead of properties in case the item is not yet initialized
  371. itemValue = itemValueFromDOM(item);
  372. // if the value is located inside the values array, then we set the item as selected
  373. item[values.indexOf(itemValue) !== -1 ? 'setAttribute' : 'removeAttribute']('selected', '');
  374. });
  375. }
  376. // if single selection, we find the first item that matches the value and deselect everything else. in case,
  377. // no item matches the value, we may need to find a selection candidate
  378. else {
  379. let targetItem;
  380. // since multiple = false, there is only 1 value value
  381. const value = values[0] || '';
  382.  
  383. items.forEach((item) => {
  384. // small optimization to avoid calculating the value from every item
  385. if (!targetItem) {
  386. itemValue = itemValueFromDOM(item);
  387.  
  388. if (itemValue === value) {
  389. // selecting the item will cause the taglist or input to be updated
  390. item.setAttribute('selected', '');
  391. // we store the first ocurrence, afterwards we deselect all items
  392. targetItem = item;
  393.  
  394. // since we found our target item, we continue to avoid removing the selected attribute
  395. return;
  396. }
  397. }
  398.  
  399. // every-non targetItem must be deselected
  400. item.removeAttribute('selected');
  401. });
  402.  
  403. // if no targetItem was found, _setStateFromDOM will make sure that the state is valid
  404. if (!targetItem) {
  405. this._setStateFromDOM();
  406. }
  407. }
  408. }
  409. }
  410.  
  411. /**
  412. Whether this field is disabled or not.
  413. @type {Boolean}
  414. @default false
  415. @htmlattribute disabled
  416. @htmlattributereflected
  417. */
  418. get disabled() {
  419. return this._disabled || false;
  420. }
  421.  
  422. set disabled(value) {
  423. this._disabled = transform.booleanAttr(value);
  424. this._reflectAttribute('disabled', this._disabled);
  425.  
  426. this[this._disabled ? 'setAttribute' : 'removeAttribute']('aria-disabled', this._disabled);
  427. this.classList.toggle('is-disabled', this._disabled);
  428.  
  429. this._elements.button.disabled = this._disabled;
  430. this._elements.input.disabled = this._disabled;
  431. this._elements.taglist.disabled = this._disabled;
  432. }
  433.  
  434. /**
  435. Inherited from {@link BaseFormField#invalid}.
  436. */
  437. get invalid() {
  438. return super.invalid;
  439. }
  440.  
  441. set invalid(value) {
  442. super.invalid = value;
  443.  
  444. this.classList.toggle('is-invalid', this.invalid);
  445. this._elements.button.classList.toggle('is-invalid', this.invalid);
  446. this._elements.invalidIcon.hidden = !this.invalid;
  447. }
  448.  
  449. /**
  450. Whether this field is required or not.
  451. @type {Boolean}
  452. @default false
  453. @htmlattribute required
  454. @htmlattributereflected
  455. */
  456. get required() {
  457. return this._required || false;
  458. }
  459.  
  460. set required(value) {
  461. this._required = transform.booleanAttr(value);
  462. this._reflectAttribute('required', this._required);
  463.  
  464. this._elements.input.required = this._required;
  465. this._elements.taglist.required = this._required;
  466. }
  467.  
  468. /**
  469. Whether this field is readOnly or not. Indicating that the user cannot modify the value of the control.
  470. @type {Boolean}
  471. @default false
  472. @htmlattribute readonly
  473. @htmlattributereflected
  474. */
  475. get readOnly() {
  476. return this._readOnly || false;
  477. }
  478.  
  479. set readOnly(value) {
  480. this._readOnly = transform.booleanAttr(value);
  481. this._reflectAttribute('readonly', this._readOnly);
  482.  
  483. this._elements.input.readOnly = this._readOnly;
  484. this._elements.taglist.readOnly = this._readOnly;
  485. this._elements.taglist.disabled = this._readOnly;
  486. }
  487.  
  488. /**
  489. Inherited from {@link BaseFormField#labelled}.
  490. */
  491. get labelled() {
  492. return super.labelled;
  493. }
  494.  
  495. set labelled(value) {
  496. super.labelled = value;
  497.  
  498. if (this.labelled) {
  499. if (!this.labelledBy) {
  500. this._elements.button.setAttribute('aria-labelledby', `${this._elements.button.id} ${this._elements.label.id} ${this.invalid ? this._elements.invalidIcon.id : ''}`);
  501. }
  502. this._elements.nativeSelect.setAttribute('aria-label', value);
  503. } else {
  504. this._elements.button.removeAttribute('aria-label');
  505. this._elements.nativeSelect.removeAttribute('aria-label');
  506. if (!this.labelledBy) {
  507. this._elements.button.removeAttribute('aria-labelledby');
  508. }
  509. }
  510.  
  511. this._elements.taglist.labelled = value;
  512. }
  513.  
  514. /**
  515. Inherited from {@link BaseFormField#labelledBy}.
  516. */
  517. get labelledBy() {
  518. return this._labelledBy;
  519. }
  520.  
  521. set labelledBy(value) {
  522. super.labelledBy = value;
  523. this._labelledBy = super.labelledBy;
  524.  
  525. if (this._labelledBy) {
  526. this._elements.button.setAttribute('aria-labelledby', `${this._labelledBy} ${this._elements.label.id} ${this.invalid ? this._elements.invalidIcon.id : ''}`);
  527. this._elements.nativeSelect.setAttribute('aria-labelledby', this._labelledBy);
  528. } else {
  529. this._elements.nativeSelect.removeAttribute('aria-labelledby');
  530.  
  531. // if the select is also labelled, make sure that aria-labelledby gets restored
  532. if (this.labelled) {
  533. this.labelled = this.labelled;
  534. }
  535. }
  536.  
  537. this._elements.taglist.labelledBy = this._labelledBy;
  538. }
  539.  
  540. /**
  541. Returns the first selected item in the Select. The value <code>null</code> is returned if no element is
  542. selected.
  543.  
  544. @type {?HTMLElement}
  545. @readonly
  546. */
  547. get selectedItem() {
  548. return this.hasAttribute('multiple') ? this.items._getFirstSelected() : this.items._getLastSelected();
  549. }
  550.  
  551. /**
  552. Returns an Array containing the set selected items.
  553.  
  554. @type {Array.<HTMLElement>}
  555. @readonly
  556. */
  557. get selectedItems() {
  558. if (this.hasAttribute('multiple')) {
  559. return this.items._getAllSelected();
  560. }
  561.  
  562. const item = this.selectedItem;
  563. return item ? [item] : [];
  564. }
  565.  
  566. /**
  567. Indicates that the Select is currently loading remote data. This will set the wait indicator inside the list.
  568.  
  569. @type {Boolean}
  570. @default false
  571. @htmlattribute loading
  572. */
  573. get loading() {
  574. return this._elements.list.loading;
  575. }
  576.  
  577. set loading(value) {
  578. this._elements.list.loading = value;
  579. }
  580.  
  581. /**
  582. The Select's variant. See {@link SelectVariantEnum}.
  583.  
  584. @type {SelectVariantEnum}
  585. @default SelectVariantEnum.DEFAULT
  586. @htmlattribute variant
  587. @htmlattributereflected
  588. */
  589. get variant() {
  590. return this._variant || variant.DEFAULT;
  591. }
  592.  
  593. set variant(value) {
  594. value = transform.string(value).toLowerCase();
  595. this._variant = validate.enumeration(variant)(value) && value || variant.DEFAULT;
  596. this._reflectAttribute('variant', this._variant);
  597.  
  598. this._elements.button.classList.toggle('_coral-FieldButton--quiet', this._variant === variant.QUIET);
  599. }
  600.  
  601. /** @ignore */
  602. _setName(value) {
  603. if (this.multiple) {
  604. this._elements.input.name = '';
  605. this._elements.taglist.setAttribute('name', value);
  606. } else {
  607. this._elements.taglist.setAttribute('name', '');
  608. this._elements.input.name = value;
  609. }
  610. }
  611.  
  612. /**
  613. @param {Boolean} [checkAvailableSpace=false]
  614. If <code>true</code>, the event is triggered based on the available space.
  615.  
  616. @private
  617. */
  618. _showOptions(checkAvailableSpace) {
  619. if (checkAvailableSpace) {
  620. // threshold in pixels
  621. const ITEM_SIZE_THRESHOLD = 30;
  622.  
  623. let scrollHeight = this._elements.list.scrollHeight;
  624. const viewportHeight = this._elements.list.clientHeight;
  625. const scrollTop = this._elements.list.scrollTop;
  626. // we should not do this, but it increases performance since we do not need to find the item
  627. const loadIndicator = this._elements.list._elements.loadIndicator;
  628.  
  629. // we remove the size of the load indicator
  630. if (loadIndicator && loadIndicator.parentNode) {
  631. const outerHeight = function (el) {
  632. let height = el.offsetHeight;
  633. const style = getComputedStyle(el);
  634.  
  635. height += parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10);
  636. return height;
  637. };
  638.  
  639. scrollHeight -= outerHeight(loadIndicator);
  640. }
  641.  
  642. // if we are not close to the bottom scroll, we cancel triggering the event
  643. if (scrollTop + viewportHeight < scrollHeight - ITEM_SIZE_THRESHOLD) {
  644. return;
  645. }
  646. }
  647.  
  648. // we do not show the list with native
  649. if (!this._useNativeInput) {
  650. if (!this._elements.overlay.open) {
  651. // Show the overlay
  652. this._elements.overlay.open = true;
  653. }
  654.  
  655. // Force overlay repositioning (remote loading)
  656. requestAnimationFrame(() => {
  657. this._elements.overlay._onAnimate();
  658. this._elements.overlay.reposition();
  659. });
  660. }
  661.  
  662. // Trigger an event
  663. // @todo: maybe we should only trigger this event when the button is toggled and we have space for more items
  664. const event = this.trigger('coral-select:showitems', {
  665. // amount of items in the select
  666. start: this.items.length
  667. });
  668.  
  669. // while using native there is no need to show the loading
  670. if (!this._useNativeInput) {
  671. // if the default is prevented, we should the loading indicator
  672. this._elements.list.loading = event.defaultPrevented;
  673. }
  674.  
  675. // communicate expanded state to assistive technology
  676. this._elements.button.setAttribute('aria-expanded', true);
  677. }
  678.  
  679. /** @private */
  680. _hideOptions() {
  681. // Don't close the overlay if selection = multiple
  682. if (!this.multiple) {
  683. this._elements.overlay.open = false;
  684.  
  685. this.trigger('coral-select:hideitems');
  686. }
  687.  
  688. // communicate collapsed state to assistive technology
  689. this._elements.button.setAttribute('aria-expanded', false);
  690. }
  691.  
  692. /** @ignore */
  693. _onGlobalClick(event) {
  694. if (!this._elements.overlay.open) {
  695. return;
  696. }
  697.  
  698. const eventTargetWithinOverlayTarget = this._elements.button.contains(event.target);
  699. const eventTargetWithinItself = this._elements.overlay.contains(event.target);
  700. if (!eventTargetWithinOverlayTarget && !eventTargetWithinItself) {
  701. this._hideOptions();
  702. }
  703. }
  704.  
  705. /** @private */
  706. _onSelectListItemAdd(event) {
  707. // stops propagation cause the event is internal to the component
  708. event.stopImmediatePropagation();
  709.  
  710. // When items have been added, we are no longer loading
  711. this.loading = false;
  712.  
  713. // Reset height
  714. this._elements.list.style.height = '';
  715.  
  716. // Measure actual height
  717. const style = window.getComputedStyle(this._elements.list);
  718. const height = parseInt(style.height, 10);
  719. const maxHeight = parseInt(style.maxHeight, 10);
  720.  
  721. if (height < maxHeight) {
  722. // Make it scrollable
  723. this._elements.list.style.height = `${height - 1}px`;
  724. }
  725. }
  726.  
  727. _onBeforeOpen(event) {
  728. event.stopImmediatePropagation();
  729.  
  730. // Prevent opening the overlay if select is readonly
  731. if (this.readOnly) {
  732. event.preventDefault();
  733. }
  734.  
  735. // focus first selected or tabbable item when the list expands
  736. this._elements.list._resetTabTarget(true);
  737. }
  738.  
  739. /** @private */
  740. _onInternalEvent(event) {
  741. // stops propagation cause the event is internal to the component
  742. event.stopImmediatePropagation();
  743. }
  744.  
  745. /** @ignore */
  746. _onItemAdded(item) {
  747. const selectListItemParent = this._elements.list;
  748.  
  749. const selectListItem = item._selectListItem || new SelectList.Item();
  750.  
  751. // @todo: Make sure it is added at the right index.
  752. selectListItemParent.appendChild(selectListItem);
  753.  
  754. selectListItem.set({
  755. value: item.value,
  756. content: {
  757. innerHTML: item.innerHTML
  758. },
  759. disabled: item.disabled,
  760. selected: item.selected,
  761. trackingElement: item.trackingElement
  762. }, true);
  763.  
  764. const nativeOption = item._nativeOption || new Option();
  765.  
  766. // @todo: make sure it is added at the right index.
  767. this._elements.nativeSelect.appendChild(nativeOption);
  768.  
  769. // Need to store the initially selected values in the native select so that it can be reset
  770. if (this._initialValues.indexOf(item.value) !== -1) {
  771. nativeOption.setAttribute('selected', 'selected');
  772. }
  773.  
  774. nativeOption.selected = item.selected;
  775. nativeOption.value = item.value;
  776. nativeOption.disabled = item.disabled;
  777. nativeOption.innerHTML = item.innerHTML;
  778.  
  779. if (this.multiple) {
  780. // in case it was selected before it was added
  781. if (item.selected) {
  782. this._addTagToTagList(item);
  783. }
  784. }
  785. // Make sure the input value is set to the selected item
  786. else if (item.selected) {
  787. this._elements.input.value = item.value;
  788. }
  789.  
  790. item._selectListItem = selectListItem;
  791. item._nativeOption = nativeOption;
  792.  
  793. selectListItem._selectItem = item;
  794. nativeOption._selectItem = item;
  795.  
  796. const messenger = item._messenger;
  797.  
  798. if (messenger && messenger.isConnected && messenger.listeners.length === 0) {
  799. // sometimes child get connected before parent, and listeners are not set yet, so we need to reconnect
  800. messenger._connected = false;
  801. messenger.connect();
  802. }
  803. }
  804.  
  805. /** @private */
  806. _onItemRemoved(item) {
  807. if (item._selectListItem) {
  808. item._selectListItem.remove();
  809. item._selectListItem._selectItem = undefined;
  810. item._selectListItem = undefined;
  811. }
  812.  
  813. if (item._nativeOption) {
  814. this._elements.nativeSelect.removeChild(item._nativeOption);
  815. item._nativeOption._selectItem = undefined;
  816. item._nativeOption = undefined;
  817. }
  818.  
  819. this._removeTagFromTagList(item, true);
  820. }
  821.  
  822. /** @private */
  823. _onItemSelected(item) {
  824. // in case the component is not in the DOM or the internals have not been created we force it
  825. if (!item._selectListItem || !item._selectListItem.parentNode) {
  826. this._onItemAdded(item);
  827. }
  828.  
  829. item._selectListItem.selected = true;
  830. item._nativeOption.selected = true;
  831.  
  832. if (this.multiple) {
  833. this._addTagToTagList(item);
  834. // @todo: what happens when ALL items have been selected
  835. // 1. a message is disabled (i18n?)
  836. // 2. we don't try to open the selectlist (native behavior).
  837. } else {
  838. this._elements.input.value = item.value;
  839. }
  840. }
  841.  
  842. /** @private */
  843. _onItemDeselected(item) {
  844. // in case the component is not in the DOM or the internals have not been created we force it
  845. if (!item._selectListItem || !item._selectListItem.parentNode) {
  846. this._onItemAdded(item);
  847. }
  848.  
  849. item._selectListItem.selected = false;
  850. item._nativeOption.selected = false;
  851.  
  852. if (this.multiple) {
  853. // we use the internal reference to remove the related tag from the taglist
  854. this._removeTagFromTagList(item);
  855. }
  856. }
  857.  
  858. /**
  859. Detects when something is about to change inside the select.
  860.  
  861. @private
  862. */
  863. _onSelectListBeforeChange(event) {
  864. // stops propagation cause the event is internal to the component
  865. event.stopImmediatePropagation();
  866.  
  867. // We prevent the selection to change if we're in single selection and the clicked item is already selected
  868. if (!this.multiple && event.detail.item.selected) {
  869. event.preventDefault();
  870. this._elements.overlay.open = false;
  871. }
  872. }
  873.  
  874. /**
  875. Detects when something inside the select list changes.
  876.  
  877. @private
  878. */
  879. _onSelectListChange(event) {
  880. // stops propagation cause the event is internal to the component
  881. event.stopImmediatePropagation();
  882.  
  883. // avoids triggering unnecessary changes in the selectist because selecting items programatically will trigger
  884. // a change event
  885. if (this._bulkSelectionChange) {
  886. return;
  887. }
  888.  
  889. let oldSelection = event.detail.oldSelection || [];
  890. oldSelection = !Array.isArray(oldSelection) ? [oldSelection] : oldSelection;
  891.  
  892. let selection = event.detail.selection || [];
  893. selection = !Array.isArray(selection) ? [selection] : selection;
  894.  
  895. // if the arrays are the same, there is no point in calculating the selection changes
  896. if (event.detail.oldSelection !== event.detail.selection) {
  897. this._bulkSelectionChange = true;
  898.  
  899. // we deselect first the ones that have to go
  900. const removedSelection = arrayDiff(oldSelection, selection);
  901. removedSelection.forEach((listItem) => {
  902. // selectlist will report on removed items
  903. if (listItem._selectItem) {
  904. listItem._selectItem.removeAttribute('selected');
  905. }
  906. });
  907.  
  908. // we only sync the items that changed
  909. const newSelection = arrayDiff(selection, oldSelection);
  910. newSelection.forEach((listItem) => {
  911. if (listItem._selectItem) {
  912. listItem._selectItem.setAttribute('selected', '');
  913. }
  914. });
  915.  
  916. this._bulkSelectionChange = false;
  917.  
  918. // hides the list since something was selected. if the overlay was open, it means there was user interaction so
  919. // the necessary events need to be triggered
  920. if (this._elements.overlay.open) {
  921. // closes and triggers the hideitems event
  922. this._hideOptions();
  923.  
  924. // if there is a change in the added or removed selection, we trigger a change event
  925. if (newSelection.length || removedSelection.length) {
  926. this.trigger('change');
  927. }
  928. }
  929. }
  930. // in case they are the same, we just need to trigger the hideitems event when appropiate, and that is when the
  931. // overlay was previously open
  932. else if (this._elements.overlay.open) {
  933. // closes and triggers the hideitems event
  934. this._hideOptions();
  935. }
  936.  
  937. if (!this.multiple) {
  938. this._trackEvent('change', 'coral-select-item', event, this.selectedItem);
  939. }
  940. }
  941.  
  942. /** @private */
  943. _onTagListChange(event) {
  944. // cancels the change event from the taglist
  945. event.stopImmediatePropagation();
  946.  
  947. // avoids triggering unnecessary changes in the selectist because selecting items programatically will trigger
  948. // a change event
  949. if (this._bulkSelectionChange) {
  950. return;
  951. }
  952.  
  953. this._bulkSelectionChange = true;
  954.  
  955. const values = event.target.values;
  956. // we use the selected items, because they are the only possible items that may change
  957. let itemValue;
  958. this.items._getAllSelected().forEach((item) => {
  959. // we use DOM API instead of properties in case the item is not yet initialized
  960. itemValue = itemValueFromDOM(item);
  961. // if the item is inside the values array, then it has to be selected
  962. item[values.indexOf(itemValue) !== -1 ? 'setAttribute' : 'removeAttribute']('selected', '');
  963. });
  964.  
  965. this._bulkSelectionChange = false;
  966.  
  967. // if the taglist is empty, we should return the focus to the button
  968. if (!values.length) {
  969. this._elements.button.focus();
  970. }
  971.  
  972. // reparents the change event with the select as the target
  973. this.trigger('change');
  974. }
  975.  
  976. /** @private */
  977. _addTagToTagList(item) {
  978. // we prepare the tag
  979. item._tag = item._tag || new Tag();
  980. item._tag.set({
  981. value: item.value,
  982. label: {
  983. innerHTML: item.innerHTML
  984. }
  985. }, true);
  986.  
  987. // we add the new tag at the end
  988. this._elements.taglist.items.add(item._tag);
  989. }
  990.  
  991. /** @private */
  992. _removeTagFromTagList(item, destroy) {
  993. if (item._tag) {
  994. item._tag.remove();
  995. // we only remove the reference if destroy is passed, this allow us to recycle the tags when possible
  996. item._tag = destroy ? undefined : item._tag;
  997. }
  998. }
  999.  
  1000. /** @private */
  1001. _onSelectListScrollBottom(event) {
  1002. // stops propagation cause the event is internal to the component
  1003. event.stopImmediatePropagation();
  1004.  
  1005. if (this._elements.overlay.open) {
  1006. // Checking if the overlay is open guards against debounced scroll events being handled after an overlay has
  1007. // already been closed (e.g. clicking the last element in a selectlist always reopened the overlay emediately
  1008. // after closing)
  1009.  
  1010. // triggers the corresponding event
  1011. // since we got the the event from select list we need to trigger the event
  1012. this._showOptions();
  1013. }
  1014. }
  1015.  
  1016. /** @private */
  1017. _onButtonClick(event) {
  1018. event.preventDefault();
  1019.  
  1020. if (this.disabled || this.readOnly) {
  1021. return;
  1022. }
  1023.  
  1024. // if native is required, we do not need to do anything
  1025. if (!this._useNativeInput) {
  1026. // @todo: this was removed cause otherwise the coral-select:showitems event is never triggered.
  1027. // if this is a multiselect and all items are selected, there should be nothing in the list to focus so do
  1028. // nothing.
  1029. // if (this.multiple && this.selectedItems.length === this.items.length) {
  1030. // return;
  1031. // }
  1032.  
  1033. // Toggle openness
  1034. if (this._elements.overlay.classList.contains('is-open')) {
  1035. this._hideOptions();
  1036. } else {
  1037. // event should be triggered based on the contents
  1038. this._showOptions(true);
  1039. }
  1040. }
  1041. }
  1042.  
  1043. /** @private */
  1044. _onNativeSelectClick() {
  1045. this._showOptions(false);
  1046. }
  1047.  
  1048. _onOverlayKeyPress(event) {
  1049. // Focus on item which text starts with pressed keys
  1050. this._elements.list._onKeyPress(event);
  1051. }
  1052.  
  1053. /** @private */
  1054. _onSpaceKey(event) {
  1055. if (this.disabled || this.readOnly) {
  1056. return;
  1057. }
  1058.  
  1059. event.preventDefault();
  1060.  
  1061. if (this._useNativeInput) {
  1062. // we try to open the native select
  1063. this._elements.nativeSelect.dispatchEvent(new MouseEvent('mousedown'));
  1064. } else if (!this._elements.overlay.open || event.keyCode === Keys.keyToCode('space')) {
  1065. this._elements.button.click();
  1066. }
  1067. }
  1068.  
  1069. /**
  1070. Prevents tab key default handling on selectList Items.
  1071.  
  1072. @private
  1073. */
  1074. // _onTabKey(event) {
  1075. // event.preventDefault();
  1076. // }
  1077.  
  1078. /** @private */
  1079. _onOverlayToggle(event) {
  1080. // stops propagation cause the event is internal to the component
  1081. event.stopImmediatePropagation();
  1082.  
  1083. // Trigger private event instead
  1084. const type = event.type.split(':').pop();
  1085. this.trigger(`coral-select:_overlay${type}`);
  1086.  
  1087. this._elements.button.classList.toggle('is-selected', event.target.open);
  1088.  
  1089. // communicate expanded state to assistive technology
  1090. this._elements.button.setAttribute('aria-expanded', event.target.open);
  1091.  
  1092. if (!event.target.open) {
  1093. this.classList.remove('is-openAbove', 'is-openBelow');
  1094. }
  1095. }
  1096.  
  1097. /** @private */
  1098. _onOverlayPositioned(event) {
  1099. // stops propagation cause the event is internal to the component
  1100. event.stopImmediatePropagation();
  1101.  
  1102. if (this._elements.overlay.open) {
  1103. this._elements.overlay.style.width = `${this.offsetWidth}px`;
  1104. }
  1105. }
  1106.  
  1107. // @todo: while the select is multiple, if everything is deselected no change event will be triggered.
  1108. _onNativeSelectChange(event) {
  1109. // stops propagation cause the event is internal to the component
  1110. event.stopImmediatePropagation();
  1111.  
  1112. // avoids triggering unnecessary changes in the selectist because selecting items programatically will trigger
  1113. // a change event
  1114. if (this._bulkSelectionChange) {
  1115. return;
  1116. }
  1117.  
  1118. this._bulkSelectionChange = true;
  1119. // extracts the native options for the selected items. We use the selected options, instead of the complete
  1120. // options to make the diff since it will normally be a smaller set
  1121. const oldSelectedOptions = this.selectedItems.map((element) => element._nativeOption);
  1122.  
  1123. // we convert the HTMLCollection to an array
  1124. const selectedOptions = Array.prototype.slice.call(event.target.querySelectorAll(':checked'));
  1125.  
  1126. const diff = arrayDiff(oldSelectedOptions, selectedOptions);
  1127. diff.forEach((item) => {
  1128. item._selectItem.selected = false;
  1129. });
  1130.  
  1131. // we only sync the items that changed
  1132. const newSelection = arrayDiff(selectedOptions, oldSelectedOptions);
  1133. newSelection.forEach((item) => {
  1134. item._selectItem.selected = true;
  1135. });
  1136.  
  1137. this._bulkSelectionChange = false;
  1138.  
  1139. // since multiple keeps the select open, we cannot return the focus to the button otherwise the user cannot
  1140. // continue selecting values
  1141. if (!this.multiple) {
  1142. // returns the focus to the button, otherwise the select will keep it
  1143. this._elements.button.focus();
  1144. // since selecting an item closes the native select, we need to trigger an event
  1145. this.trigger('coral-select:hideitems');
  1146. }
  1147.  
  1148. // if the native change event was triggered, then it means there is some new value
  1149. this.trigger('change');
  1150. }
  1151.  
  1152. /**
  1153. This handles content change of coral-select-item and updates its associatives.
  1154.  
  1155. @private
  1156. */
  1157. _onItemContentChange(event) {
  1158. // stops propagation cause the event is internal to the component
  1159. event.stopImmediatePropagation();
  1160.  
  1161. const item = event.target;
  1162. if (item._selectListItem) {
  1163. const content = new SelectList.Item.Content();
  1164. content.innerHTML = item.innerHTML;
  1165. item._selectListItem.content = content;
  1166. }
  1167.  
  1168. if (item._nativeOption) {
  1169. item._nativeOption.innerHTML = item.innerHTML;
  1170. }
  1171.  
  1172. if (item._tag && item._tag.label) {
  1173. item._tag.label.innerHTML = item.innerHTML;
  1174. }
  1175.  
  1176. // since the content changed, we need to sync the placeholder in case it was the selected item
  1177. this._syncSelectedItemPlaceholder();
  1178. }
  1179.  
  1180. /** @private */
  1181. _syncSelectedItemPlaceholder() {
  1182. this.placeholder = this.getAttribute('placeholder');
  1183.  
  1184. // case 3: !p + !m + se = se
  1185. // case 5: p + !m + se = se
  1186. if (this.selectedItem && !this.multiple) {
  1187. this._elements.label.classList.remove('is-placeholder');
  1188. this._elements.label.innerHTML = this.selectedItem.innerHTML;
  1189. }
  1190. }
  1191.  
  1192. /**
  1193. This handles value change of coral-select-item and updates its associatives.
  1194.  
  1195. @private
  1196. */
  1197. _onItemValueChange(event) {
  1198. // stops propagation cause the event is internal to the component
  1199. event.stopImmediatePropagation();
  1200.  
  1201. const item = event.target;
  1202. if (item._selectListItem) {
  1203. item._selectListItem.value = item.value;
  1204. }
  1205.  
  1206. if (item._nativeOption) {
  1207. item._nativeOption.value = item.value;
  1208. }
  1209.  
  1210. if (item._tag) {
  1211. item._tag.value = item.value;
  1212. }
  1213. }
  1214.  
  1215. /**
  1216. This handles disabled change of coral-select-item and updates its associatives.
  1217.  
  1218. @private
  1219. */
  1220. _onItemDisabledChange(event) {
  1221. // stops propagation cause the event is internal to the component
  1222. event.stopImmediatePropagation();
  1223.  
  1224. const item = event.target;
  1225. if (item._selectListItem) {
  1226. item._selectListItem.disabled = item.disabled;
  1227. }
  1228.  
  1229. if (item._nativeOption) {
  1230. item._nativeOption.disabled = item.disabled;
  1231. }
  1232. }
  1233.  
  1234. /**
  1235. In case an item from the initial selection is removed, we need to remove it from the initial values.
  1236.  
  1237. @private
  1238. */
  1239. _validateInitialState(nodes) {
  1240. let item;
  1241. let index;
  1242.  
  1243. // we iterate over all the nodes, checking if they matched the initial value
  1244. for (let i = 0, nodeCount = nodes.length ; i < nodeCount ; i++) {
  1245. // since we are not sure if the item has been upgraded, we try first the attribute, otherwise we extract the
  1246. // value from the textContent
  1247. item = nodes[i];
  1248.  
  1249. index = this._initialValues.indexOf(item.value);
  1250.  
  1251. if (index !== -1) {
  1252. this._initialValues.splice(index, 1);
  1253. }
  1254. }
  1255. }
  1256.  
  1257. /** @private */
  1258. // eslint-disable-next-line no-unused-vars
  1259. _onCollectionChange(addedNodes, removedNodes) {
  1260. // we make sure that items that were part of the initial selection are removed from the internal representation
  1261. this._validateInitialState(removedNodes);
  1262. // makes sure that the selection state matches the multiple variable
  1263. this._setStateFromDOM();
  1264. }
  1265.  
  1266. /**
  1267. Updates the label to reflect the current state. The label needs to be updated when the placeholder changes and
  1268. when the selection changes.
  1269.  
  1270. @private
  1271. */
  1272. _updateLabel() {
  1273. this._syncSelectedItemPlaceholder();
  1274. }
  1275.  
  1276. /**
  1277. Handles the selection state.
  1278.  
  1279. @ignore
  1280. */
  1281. _setStateFromDOM() {
  1282. // if it is not multiple, we need to be sure only one item is selected
  1283. if (!this.hasAttribute('multiple')) {
  1284. // makes sure that only one is selected
  1285. this.items._deselectAllExceptLast();
  1286.  
  1287. // we execute _getFirstSelected instead of _getSelected because it is faster
  1288. const selectedItem = this.items._getFirstSelected();
  1289.  
  1290. // case 1. there is a selected item, so no further change is required
  1291. // case 2. no selected item and no placeholder. an item will be automatically selected
  1292. // case 3. no selected item and a placehoder. we just make sure the value is really empty
  1293. if (!selectedItem) {
  1294. // we clean the value because there is no selected item
  1295. this._elements.input.value = '';
  1296.  
  1297. // when there is no placeholder, we need to force a selection to behave like the native select
  1298. if (transform.string(this.getAttribute('placeholder')) === '') {
  1299. // gets the first candidate for selection
  1300. const selectable = this.items._getFirstSelectable();
  1301.  
  1302. if (selectable) {
  1303. // selects using the attribute in case the item is not yet initialized
  1304. selectable.setAttribute('selected', '');
  1305. // we set the value explicitely, so we do not need to wait for the MO
  1306. this._elements.input.value = itemValueFromDOM(selectable);
  1307. }
  1308. }
  1309. } else {
  1310. // we set the value explicitely, so we do not need to wait for the MO
  1311. this._elements.input.value = itemValueFromDOM(selectedItem);
  1312. }
  1313. }
  1314.  
  1315. // handles the initial item in the select
  1316. this._updateLabel();
  1317. }
  1318.  
  1319. /**
  1320. Handles selecting multiple items. Selection could result a single or multiple selected items.
  1321.  
  1322. @private
  1323. */
  1324. _onItemSelectedChange(event) {
  1325. // we stop propagation since it is a private event
  1326. event.stopImmediatePropagation();
  1327.  
  1328. // the item that was selected
  1329. const item = event.target;
  1330.  
  1331. // setting this to true will ignore any changes from the selectlist al
  1332. this._bulkSelectionChange = true;
  1333.  
  1334. // when the item is selected, we need to enforce the selection mode
  1335. if (item.selected) {
  1336. this._onItemSelected(item);
  1337.  
  1338. if (this.multiple) {
  1339. this._trackEvent('select', 'coral-select-item', event, item);
  1340. }
  1341.  
  1342. // enforces the selection mode
  1343. if (!this.hasAttribute('multiple')) {
  1344. this.items._deselectAllExcept(item);
  1345. }
  1346. } else {
  1347. this._onItemDeselected(item);
  1348.  
  1349. if (this.multiple) {
  1350. this._trackEvent('deselect', 'coral-select-item', event, item);
  1351. }
  1352. }
  1353.  
  1354. this._bulkSelectionChange = false;
  1355.  
  1356. // since there is a change in selection, we need to update the placeholder
  1357. this._updateLabel();
  1358. }
  1359.  
  1360. /**
  1361. Inherited from {@link BaseFormField#clear}.
  1362. */
  1363. clear() {
  1364. this.value = '';
  1365. }
  1366.  
  1367. /**
  1368. Focuses the component.
  1369.  
  1370. @ignore
  1371. */
  1372. focus() {
  1373. if (!this.contains(document.activeElement)) {
  1374. this._elements.button.focus();
  1375. }
  1376. }
  1377.  
  1378. /**
  1379. Inherited from {@link BaseFormField#reset}.
  1380. */
  1381. reset() {
  1382. // reset the values to the initial values
  1383. this.values = this._initialValues;
  1384. }
  1385.  
  1386. /**
  1387. Returns {@link Select} variants.
  1388.  
  1389. @return {SelectVariantEnum}
  1390. */
  1391. static get variant() {
  1392. return variant;
  1393. }
  1394.  
  1395. /** @ignore */
  1396. static get observedAttributes() {
  1397. return super.observedAttributes.concat(['variant', 'multiple', 'placeholder', 'loading']);
  1398. }
  1399.  
  1400. get observedMessages() {
  1401. return {
  1402. 'coral-select-item:_valuechanged': '_onItemValueChange',
  1403. 'coral-select-item:_contentchanged': '_onItemContentChange',
  1404. 'coral-select-item:_disabledchanged': '_onItemDisabledChange',
  1405. 'coral-select-item:_selectedchanged': '_onItemSelectedChange',
  1406. };
  1407. }
  1408.  
  1409. /** @ignore */
  1410. connectedCallback() {
  1411. super.connectedCallback();
  1412.  
  1413. const overlay = this._elements.overlay;
  1414. // Cannot be open by default when rendered
  1415. overlay.removeAttribute('open');
  1416. // Restore in DOM
  1417. if (overlay._parent) {
  1418. overlay._parent.appendChild(overlay);
  1419. }
  1420. }
  1421.  
  1422. /** @ignore */
  1423. render() {
  1424. super.render();
  1425.  
  1426. this.classList.add(CLASSNAME);
  1427.  
  1428. // Default reflected attributes
  1429. if (!this._variant) {
  1430. this.variant = variant.DEFAULT;
  1431. }
  1432.  
  1433. this.classList.toggle(`${CLASSNAME}--native`, this._useNativeInput);
  1434.  
  1435. if (!this._useNativeInput && this.contains(this._elements.nativeSelect)) {
  1436. this.removeChild(this._elements.nativeSelect);
  1437. }
  1438.  
  1439. // handles the initial selection
  1440. this._setStateFromDOM();
  1441.  
  1442. // we need to keep a state of the initial items to be able to reset the component. values is not reliable during
  1443. // initialization since items are not yet initialized
  1444. this.selectedItems.forEach((item) => {
  1445. // we use DOM API instead of properties in case the item is not yet initialized
  1446. this._initialValues.push(itemValueFromDOM(item));
  1447. });
  1448.  
  1449. // Cleanup template elements (supporting cloneNode)
  1450. const templateElements = this.querySelectorAll('[handle]');
  1451. for (let i = 0 ; i < templateElements.length ; ++i) {
  1452. const currentElement = templateElements[i];
  1453. if (currentElement.parentNode === this) {
  1454. this.removeChild(currentElement);
  1455. }
  1456. }
  1457.  
  1458. // Render the main template
  1459. const frag = document.createDocumentFragment();
  1460.  
  1461. frag.appendChild(this._elements.button);
  1462. frag.appendChild(this._elements.input);
  1463. frag.appendChild(this._elements.nativeSelect);
  1464. frag.appendChild(this._elements.taglist);
  1465. frag.appendChild(this._elements.overlay);
  1466.  
  1467. // avoid popper initialisation if popper neither exist nor overlay opened.
  1468. this._elements.overlay._avoidPopperInit = this._elements.overlay.open || this._elements.overlay._popper ? false : true;
  1469.  
  1470. // Assign the button as the target for the overlay
  1471. this._elements.overlay.target = this._elements.button;
  1472. // handles the focus allocation every time the overlay closes
  1473. this._elements.overlay.returnFocusTo(this._elements.button);
  1474.  
  1475. this.appendChild(frag);
  1476.  
  1477. // set this to false after overlay has been connected to avoid connected callback target setting
  1478. delete this._elements.overlay._avoidPopperInit;
  1479. }
  1480.  
  1481. /** @ignore */
  1482. disconnectedCallback() {
  1483. super.disconnectedCallback();
  1484.  
  1485. const overlay = this._elements.overlay;
  1486. // In case it was moved out don't forget to remove it
  1487. if (!this.contains(overlay)) {
  1488. overlay._parent = overlay._repositioned ? document.body : this;
  1489. overlay.remove();
  1490. }
  1491. }
  1492.  
  1493. /**
  1494. Triggered when the {@link Select} could accept external data to be loaded by the user. If <code>preventDefault()</code> is
  1495. called, then a loading indicator will be shown. {@link Select#loading} should be set to false to indicate
  1496. that the data has been successfully loaded.
  1497.  
  1498. @typedef {CustomEvent} coral-select:showitems
  1499.  
  1500. @property {Number} detail.start
  1501. The count of existing items, which is the index where new items should start.
  1502. */
  1503.  
  1504. /**
  1505. Triggered when the {@link Select} hides the UI used to select items. This is typically used to cancel a load request
  1506. because the items will not be shown anymore.
  1507.  
  1508. @typedef {CustomEvent} coral-select:hideitems
  1509. */
  1510. });
  1511.  
  1512. export default Select;