UNPKG

virtual-select-plugin

Version:

A javascript plugin for dropdown with virtual scroll

1,601 lines (1,332 loc) 100 kB
/** cSpell:ignore nocheck, Labelledby, vscomp, tabindex, combobox, haspopup, listbox, activedescendant */ /* eslint-disable class-methods-use-this */ // @ts-nocheck import { Utils, DomUtils } from './utils'; const dropboxCloseButtonFullHeight = 48; const searchHeight = 40; const keyDownMethodMapping = { 13: 'onEnterPress', 38: 'onUpArrowPress', 40: 'onDownArrowPress', 46: 'onBackspaceOrDeletePress', // Delete 8: 'onBackspaceOrDeletePress', // Backspace }; const valueLessProps = ['autofocus', 'disabled', 'multiple', 'required']; const nativeProps = ['autofocus', 'class', 'disabled', 'id', 'multiple', 'name', 'placeholder', 'required']; let attrPropsMapping; const dataProps = [ 'additionalClasses', 'additionalDropboxClasses', 'additionalDropboxContainerClasses', 'additionalToggleButtonClasses', 'aliasKey', 'allOptionsSelectedText', 'allowNewOption', 'alwaysShowSelectedOptionsCount', 'alwaysShowSelectedOptionsLabel', 'ariaLabelledby', 'ariaLabelText', 'ariaLabelClearButtonText', 'ariaLabelTagClearButtonText', 'ariaLabelSearchClearButtonText', 'autoSelectFirstOption', 'clearButtonText', 'descriptionKey', 'disableAllOptionsSelectedText', 'disableOptionGroupCheckbox', 'disableSelectAll', 'disableValidation', 'dropboxWidth', 'dropboxWrapper', 'emptyValue', 'enableSecureText', 'focusSelectedOptionOnOpen', 'hasOptionDescription', 'hideClearButton', 'hideValueTooltipOnSelectAll', 'keepAlwaysOpen', 'labelKey', 'markSearchResults', 'maxValues', 'maxWidth', 'minValues', 'moreText', 'noOfDisplayValues', 'noOptionsText', 'noSearchResultsText', 'optionHeight', 'optionSelectedText', 'optionsCount', 'optionsSelectedText', 'popupDropboxBreakpoint', 'popupPosition', 'position', 'search', 'searchByStartsWith', 'searchDelay', 'searchFormLabel', 'searchGroup', 'searchNormalize', 'searchPlaceholderText', 'selectAllOnlyVisible', 'selectAllText', 'setValueAsArray', 'showDropboxAsPopup', 'showOptionsOnlyOnSearch', 'showSelectedOptionsFirst', 'showValueAsTags', 'silentInitialValueSet', 'textDirection', 'tooltipAlignment', 'tooltipFontSize', 'tooltipMaxWidth', 'updatePositionThrottle', 'useGroupValue', 'valueKey', 'zIndex', ]; /** Class representing VirtualSelect */ export class VirtualSelect { /** * @param {virtualSelectOptions} options */ constructor(options) { try { this.createSecureTextElements(); this.setProps(options); this.setDisabledOptions(options.disabledOptions); this.setOptions(options.options); this.render(); } catch (e) { // eslint-disable-next-line no-console console.warn("Couldn't initiate Virtual Select"); // eslint-disable-next-line no-console console.error(e); } } /** render methods - start */ render() { if (!this.$ele) { return; } const { uniqueId } = this; let wrapperClasses = 'vscomp-wrapper'; let toggleButtonClasses = 'vscomp-toggle-button'; const valueTooltip = this.showValueAsTags ? '' : this.getTooltipAttrText(this.placeholder, true, true); const clearButtonTooltip = this.getTooltipAttrText(this.clearButtonText); const ariaLabelledbyText = this.ariaLabelledby ? `aria-labelledby="${this.ariaLabelledby}"` : ''; const ariaLabelText = this.ariaLabelText ? `aria-label="${this.ariaLabelText}"` : ''; const ariaLabelClearBtnTxt = this.ariaLabelClearButtonText ? `aria-label="${this.ariaLabelClearButtonText}"` : ''; let isExpanded = false; if (this.additionalClasses) { wrapperClasses += ` ${this.additionalClasses}`; } if (this.additionalToggleButtonClasses) { toggleButtonClasses += ` ${this.additionalToggleButtonClasses}`; } if (this.multiple) { wrapperClasses += ' multiple'; if (!this.disableSelectAll) { wrapperClasses += ' has-select-all'; } } if (!this.hideClearButton) { wrapperClasses += ' has-clear-button'; } if (this.keepAlwaysOpen) { wrapperClasses += ' keep-always-open'; isExpanded = true; } else { wrapperClasses += ' closed'; } if (this.showAsPopup) { wrapperClasses += ' show-as-popup'; } if (this.hasSearch) { wrapperClasses += ' has-search-input'; } if (this.showValueAsTags) { wrapperClasses += ' show-value-as-tags'; } if (this.textDirection) { wrapperClasses += ` text-direction-${this.textDirection}`; } if (this.popupPosition) { wrapperClasses += ` popup-position-${this.popupPosition.toLowerCase()}`; } // eslint-disable-next-line no-trailing-spaces const html = `<div id="vscomp-ele-wrapper-${uniqueId}" class="vscomp-ele-wrapper ${wrapperClasses}" tabindex="0" role="combobox" aria-haspopup="listbox" aria-controls="vscomp-dropbox-container-${uniqueId}" aria-expanded="${isExpanded}" ${ariaLabelledbyText} ${ariaLabelText}> <input type="hidden" name="${this.name}" class="vscomp-hidden-input"> <div class="${toggleButtonClasses}"> <div class="vscomp-value" ${valueTooltip}> ${this.placeholder} </div> <div class="vscomp-arrow"></div> <div class="vscomp-clear-button toggle-button-child" ${clearButtonTooltip} tabindex="-1" role="button" ${ariaLabelClearBtnTxt}> <i class="vscomp-clear-icon"></i> </div> </div> ${this.renderDropbox({ wrapperClasses })} </div>`; this.$ele.innerHTML = html; this.$body = document.querySelector('body'); this.$wrapper = this.$ele.querySelector('.vscomp-wrapper'); if (this.hasDropboxWrapper) { this.$allWrappers = [this.$wrapper, this.$dropboxWrapper]; this.$dropboxContainer = this.$dropboxWrapper.querySelector('.vscomp-dropbox-container'); DomUtils.addClass(this.$dropboxContainer, 'pop-comp-wrapper'); } else { this.$allWrappers = [this.$wrapper]; this.$dropboxContainer = this.$wrapper.querySelector('.vscomp-dropbox-container'); } this.$toggleButton = this.$ele.querySelector('.vscomp-toggle-button'); this.$clearButton = this.$ele.querySelector('.vscomp-clear-button'); this.$valueText = this.$ele.querySelector('.vscomp-value'); this.$hiddenInput = this.$ele.querySelector('.vscomp-hidden-input'); this.$dropbox = this.$dropboxContainer.querySelector('.vscomp-dropbox'); this.$dropboxCloseButton = this.$dropboxContainer.querySelector('.vscomp-dropbox-close-button'); this.$dropboxContainerBottom = this.$dropboxContainer.querySelector('.vscomp-dropbox-container-bottom'); this.$dropboxContainerTop = this.$dropboxContainer.querySelector('.vscomp-dropbox-container-top'); this.$search = this.$dropboxContainer.querySelector('.vscomp-search-wrapper'); this.$optionsContainer = this.$dropboxContainer.querySelector('.vscomp-options-container'); this.$optionsList = this.$dropboxContainer.querySelector('.vscomp-options-list'); this.$options = this.$dropboxContainer.querySelector('.vscomp-options'); this.$noOptions = this.$dropboxContainer.querySelector('.vscomp-no-options'); this.$noSearchResults = this.$dropboxContainer.querySelector('.vscomp-no-search-results'); this.afterRenderWrapper(); } renderDropbox({ wrapperClasses }) { const $wrapper = this.dropboxWrapper !== 'self' ? document.querySelector(this.dropboxWrapper) : null; let dropboxClasses = 'vscomp-dropbox'; if (this.additionalDropboxClasses) { dropboxClasses += ` ${this.additionalDropboxClasses}`; } let dropboxContainerClasses = 'vscomp-dropbox-container'; if (this.additionalDropboxContainerClasses) { dropboxContainerClasses += ` ${this.additionalDropboxContainerClasses}`; } // eslint-disable-next-line no-trailing-spaces const html = `<div id="vscomp-dropbox-container-${this.uniqueId}" role="listbox" class="${dropboxContainerClasses}"> <div class="vscomp-dropbox-container-top" aria-hidden="true" tabindex="-1">&nbsp;</div> <div class="${dropboxClasses}"> <div class="vscomp-search-wrapper"></div> <div class="vscomp-options-container"> <div class="vscomp-options-loader"></div> <div class="vscomp-options-list"> <div class="vscomp-options"></div> </div> </div> <div class="vscomp-options-bottom-freezer"></div> <div class="vscomp-no-options">${this.noOptionsText}</div> <div class="vscomp-no-search-results">${this.noSearchResultsText}</div> <span class="vscomp-dropbox-close-button"><i class="vscomp-clear-icon"></i></span> </div> <div class="vscomp-dropbox-container-bottom" aria-hidden="true" tabindex="-1">&nbsp;</div> </div>`; if ($wrapper) { const $dropboxWrapper = document.createElement('div'); this.$dropboxWrapper = $dropboxWrapper; this.hasDropboxWrapper = true; $dropboxWrapper.innerHTML = html; $wrapper.appendChild($dropboxWrapper); DomUtils.addClass($dropboxWrapper, `vscomp-dropbox-wrapper ${wrapperClasses}`); if (!this.keepAlwaysOpen) { DomUtils.setAttr($dropboxWrapper, 'tabindex', '-1'); DomUtils.setAria($dropboxWrapper, 'hidden', true); } return ''; } this.hasDropboxWrapper = false; return html; } renderOptions() { let html = ''; const visibleOptions = this.getVisibleOptions(); let checkboxHtml = ''; let newOptionIconHtml = ''; const markSearchResults = !!(this.markSearchResults && this.searchValue); let searchRegex; const { labelRenderer, disableOptionGroupCheckbox, uniqueId, searchGroup } = this; const hasLabelRenderer = typeof labelRenderer === 'function'; const { convertToBoolean } = Utils; let groupName = ''; if (markSearchResults) { searchRegex = new RegExp(`(${Utils.regexEscape(this.searchValue)})(?!([^<]+)?>)`, 'gi'); } if (this.multiple) { checkboxHtml = '<span class="checkbox-icon"></span>'; } if (this.allowNewOption) { const newOptionTooltip = this.getTooltipAttrText('New Option'); newOptionIconHtml = `<span class="vscomp-new-option-icon" ${newOptionTooltip}></span>`; } visibleOptions.forEach((d) => { const { index } = d; let optionLabel; let optionClasses = 'vscomp-option'; const optionTooltip = this.getTooltipAttrText('', true, true); let leftSection = checkboxHtml; let rightSection = ''; let description = ''; let groupIndexText = ''; let ariaLabel = ''; let tabIndexValue = '-1'; const isSelected = convertToBoolean(d.isSelected); let ariaDisabledText = ''; if (d.classNames) { optionClasses += ` ${d.classNames}`; } if (d.isFocused) { tabIndexValue = '0'; optionClasses += ' focused'; } if (d.isDisabled) { optionClasses += ' disabled'; ariaDisabledText = 'aria-disabled="true"'; } if (d.isGroupTitle) { groupName = d.label; optionClasses += ' group-title'; if (disableOptionGroupCheckbox) { leftSection = ''; } } if (isSelected) { optionClasses += ' selected'; } if (d.isGroupOption) { let optionDesc = ''; optionClasses += ' group-option'; groupIndexText = `data-group-index="${d.groupIndex}"`; if (d.customData) { groupName = d.customData.group_name !== undefined ? `${d.customData.group_name}, ` : ''; optionDesc = d.customData.description !== undefined ? ` ${d.customData.description},` : ''; ariaLabel = `aria-label="${groupName} ${d.label}, ${optionDesc}"`; } else { ariaLabel = `aria-label="${groupName}, ${d.label}"`; } } if (hasLabelRenderer) { optionLabel = labelRenderer(d); } else { optionLabel = d.label; } if (d.description) { description = `<div class="vscomp-option-description" ${optionTooltip}>${d.description}</div>`; } if (d.isCurrentNew) { optionClasses += ' current-new'; rightSection += newOptionIconHtml; } else if (markSearchResults && (!d.isGroupTitle || searchGroup)) { optionLabel = optionLabel.replace(searchRegex, '<mark>$1</mark>'); } html += `<div role="option" aria-selected="${isSelected}" id="vscomp-option-${uniqueId}-${index}" class="${optionClasses}" data-value="${d.value}" data-index="${index}" data-visible-index="${d.visibleIndex}" tabindex=${tabIndexValue} ${groupIndexText} ${ariaDisabledText} ${ariaLabel} > ${leftSection} <span class="vscomp-option-text" ${optionTooltip}> ${optionLabel} </span> ${description} ${rightSection} </div>`; }); groupName = ''; this.$options.innerHTML = html; this.$visibleOptions = this.$options.querySelectorAll('.vscomp-option'); this.afterRenderOptions(); } renderSearch() { if (!this.hasSearchContainer) { return; } let checkboxHtml = ''; let searchInput = ''; if (this.multiple && !this.disableSelectAll) { checkboxHtml = `<span class="vscomp-toggle-all-button" tabindex="0" aria-label="${this.selectAllText}"> <span class="checkbox-icon vscomp-toggle-all-checkbox"></span> <span class="vscomp-toggle-all-label">${this.selectAllText}</span> </span>`; } if (this.hasSearch) { const ariaLabelSearchClearBtnTxt = this.ariaLabelSearchClearButtonText ? `aria-label="${this.ariaLabelSearchClearButtonText}"` : ''; searchInput = `<label for="vscomp-search-input-${this.uniqueId}" class="vscomp-search-label" id="vscomp-search-label-${this.uniqueId}" > ${this.searchFormLabel} </label> <input type="text" class="vscomp-search-input" placeholder="${this.searchPlaceholderText}" id="vscomp-search-input-${this.uniqueId}"> <span class="vscomp-search-clear" role="button" ${ariaLabelSearchClearBtnTxt}>&times;</span>`; } const html = `<div class="vscomp-search-container"> ${checkboxHtml} ${searchInput} </div>`; this.$search.innerHTML = html; this.$searchInput = this.$dropboxContainer.querySelector('.vscomp-search-input'); this.$searchClear = this.$dropboxContainer.querySelector('.vscomp-search-clear'); this.$toggleAllButton = this.$dropboxContainer.querySelector('.vscomp-toggle-all-button'); this.$toggleAllCheckbox = this.$dropboxContainer.querySelector('.vscomp-toggle-all-checkbox'); this.addEvent(this.$searchInput, 'input', 'onSearch'); this.addEvent(this.$searchClear, 'click keydown', 'onSearchClear'); this.addEvent(this.$toggleAllButton, 'click', 'onToggleAllOptions'); this.addEvent(this.$dropboxContainerBottom, 'focus', 'onDropboxContainerTopOrBottomFocus'); this.addEvent(this.$dropboxContainerTop, 'focus', 'onDropboxContainerTopOrBottomFocus'); } /** render methods - end */ /** dom event methods - start */ addEvents() { this.addEvent(document, 'click', 'onDocumentClick'); this.addEvent(this.$allWrappers, 'keydown', 'onKeyDown'); this.addEvent(this.$toggleButton, 'click keydown', 'onToggleButtonPress'); this.addEvent(this.$clearButton, 'click keydown', 'onClearButtonClick'); this.addEvent(this.$dropboxContainer, 'click', 'onDropboxContainerClick'); this.addEvent(this.$dropboxCloseButton, 'click', 'onDropboxCloseButtonClick'); this.addEvent(this.$optionsContainer, 'scroll', 'onOptionsScroll'); this.addEvent(this.$options, 'click', 'onOptionsClick'); this.addEvent(this.$options, 'mouseover', 'onOptionsMouseOver'); this.addEvent(this.$options, 'touchmove', 'onOptionsTouchMove'); this.addMutationObserver(); } addEvent($ele, events, method) { if (!$ele) { return; } const eventsArray = Utils.removeArrayEmpty(events.split(' ')); eventsArray.forEach((event) => { const eventsKey = `${method}-${event}`; let callback = this.events[eventsKey]; if (!callback) { callback = this[method].bind(this); this.events[eventsKey] = callback; } DomUtils.addEvent($ele, event, callback); }); } /** dom event methods - start */ removeEvents() { this.removeEvent(document, 'click', 'onDocumentClick'); this.removeEvent(this.$allWrappers, 'keydown', 'onKeyDown'); this.removeEvent(this.$toggleButton, 'click keydown', 'onToggleButtonPress'); this.removeEvent(this.$clearButton, 'click keydown', 'onClearButtonClick'); this.removeEvent(this.$dropboxContainer, 'click', 'onDropboxContainerClick'); this.removeEvent(this.$dropboxCloseButton, 'click', 'onDropboxCloseButtonClick'); this.removeEvent(this.$optionsContainer, 'scroll', 'onOptionsScroll'); this.removeEvent(this.$options, 'click', 'onOptionsClick'); this.removeEvent(this.$options, 'mouseover', 'onOptionsMouseOver'); this.removeEvent(this.$options, 'touchmove', 'onOptionsTouchMove'); this.removeMutationObserver(); } removeEvent($ele, events, method) { if (!$ele) { return; } const eventsArray = Utils.removeArrayEmpty(events.split(' ')); eventsArray.forEach((event) => { const eventsKey = `${method}-${event}`; const callback = this.events[eventsKey]; if (callback) { DomUtils.removeEvent($ele, event, callback); } }); } onDocumentClick(e) { const $eleToKeepOpen = e.target.closest('.vscomp-wrapper'); if ($eleToKeepOpen !== this.$wrapper && $eleToKeepOpen !== this.$dropboxWrapper && this.isOpened()) { this.closeDropbox(); } } onKeyDown(e) { const key = e.which || e.keyCode; const method = keyDownMethodMapping[key]; if (document.activeElement === this.$searchInput && (!e.shiftKey && key === 9) && !this.multiple) { e.preventDefault(); this.focusFirstVisibleOption(); } if (document.activeElement === this.$toggleAllButton && key === 13) { this.toggleAllOptions(); return; } // Handle the Escape key when showing the dropdown as a popup, closing it if (key === 27 || e.key === 'Escape') { const wrapper = this.showAsPopup ? this.$wrapper : this.$dropboxWrapper; if ((document.activeElement === wrapper || wrapper.contains(document.activeElement)) && !this.keepAlwaysOpen) { this.closeDropbox(); return; } } if (method) { this[method](e); } } onEnterPress(e) { e.preventDefault(); if (this.isOpened()) { this.selectFocusedOption(); } else if (this.$ele.disabled === false) { this.openDropbox(); } } onDownArrowPress(e) { e.preventDefault(); if (this.isOpened()) { this.focusOption({ direction: 'next' }); } else { this.openDropbox(); } } onUpArrowPress(e) { e.preventDefault(); if (this.isOpened()) { this.focusOption({ direction: 'previous' }); } else { this.openDropbox(); } } onBackspaceOrDeletePress(e) { if (e.target === this.$wrapper) { e.preventDefault(); if (this.selectedValues.length > 0) { this.reset(); } } } onToggleButtonPress(e) { e.stopPropagation(); if (e.type === 'keydown' && e.code !== 'Enter' && e.code !== 'Space') { return; } const $target = e.target; if ($target.closest('.vscomp-value-tag-clear-button')) { this.removeValue($target.closest('.vscomp-value-tag')); } else if (!$target.closest('.toggle-button-child')) { this.toggleDropbox(); } } onClearButtonClick(e) { if (e.type === 'click') { this.reset(); } else if (e.type === 'keydown' && (e.code === 'Enter' || e.code === 'Space')) { e.stopPropagation(); this.reset(); } } onOptionsScroll() { this.setVisibleOptions(); } onOptionsClick(e) { const $option = e.target.closest('.vscomp-option'); if ($option && !DomUtils.hasClass($option, 'disabled')) { if (DomUtils.hasClass($option, 'group-title')) { this.onGroupTitleClick($option); } else { this.selectOption($option, { event: e }); } } } onGroupTitleClick($ele) { if (!$ele || !this.multiple || this.disableOptionGroupCheckbox) { return; } const isAdding = !DomUtils.hasClass($ele, 'selected'); this.toggleGroupTitleCheckbox($ele, isAdding); this.toggleGroupOptions($ele, isAdding); } onDropboxContainerClick(e) { if (!e.target.closest('.vscomp-dropbox')) { this.closeDropbox(); } } onDropboxCloseButtonClick() { this.closeDropbox(); } onOptionsMouseOver(e) { const $ele = e.target.closest('.vscomp-option'); if ($ele && this.isOpened()) { if (DomUtils.hasClass($ele, 'disabled') || DomUtils.hasClass($ele, 'group-title')) { this.removeOptionFocus(); } else { this.focusOption({ $option: $ele }); } } } onOptionsTouchMove() { this.removeOptionFocus(); } onSearch(e) { e.stopPropagation(); this.setSearchValue(e.target.value, true); } onSearchClear(e) { e.stopPropagation(); if (e.code === 'Enter' || e.code === 'Space' || e.type === 'click') { this.setSearchValue(''); this.focusSearchInput(); } } onToggleAllOptions() { this.toggleAllOptions(); } onDropboxContainerTopOrBottomFocus() { this.closeDropbox(); } onResize() { this.setOptionsContainerHeight(true); } /** to remove dropboxWrapper on removing vscomp-ele when it is rendered outside of vscomp-ele */ addMutationObserver() { if (!this.hasDropboxWrapper) { return; } const $vscompEle = this.$ele; this.mutationObserver = new MutationObserver((mutations) => { let isAdded = false; let isRemoved = false; mutations.forEach((mutation) => { if (!isAdded) { isAdded = [...mutation.addedNodes].some(($ele) => !!($ele === $vscompEle || $ele.contains($vscompEle))); } if (!isRemoved) { isRemoved = [...mutation.removedNodes].some(($ele) => !!($ele === $vscompEle || $ele.contains($vscompEle))); } }); if (isRemoved && !isAdded) { this.destroy(); } }); this.mutationObserver.observe(document.querySelector('body'), { childList: true, subtree: true }); } removeMutationObserver() { this.mutationObserver.disconnect(); } /** dom event methods - end */ /** before event methods - start */ beforeValueSet(isReset) { this.toggleAllOptionsClass(isReset ? false : undefined); } beforeSelectNewValue() { const newOption = this.getNewOption(); const newIndex = newOption.index; this.newValues.push(newOption.value); this.setOptionProp(newIndex, 'isCurrentNew', false); this.setOptionProp(newIndex, 'isNew', true); /** using setTimeout to fix the issue of dropbox getting closed on select */ setTimeout(() => { this.setSearchValue(''); this.focusSearchInput(); }, 0); } /** before event methods - end */ /** after event methods - start */ afterRenderWrapper() { DomUtils.addClass(this.$ele, 'vscomp-ele'); this.renderSearch(); this.setEleStyles(); this.setDropboxStyles(); this.setOptionsHeight(); this.setVisibleOptions(); this.setOptionsContainerHeight(); this.addEvents(); this.setEleProps(); if (!this.keepAlwaysOpen && !this.showAsPopup) { this.initDropboxPopover(); } if (this.initialSelectedValue) { this.setValueMethod(this.initialSelectedValue, this.silentInitialValueSet); } else if (this.autoSelectFirstOption && this.visibleOptions.length) { this.setValueMethod(this.visibleOptions[0].value, this.silentInitialValueSet); } if (this.showOptionsOnlyOnSearch) { this.setSearchValue('', false, true); } if (this.initialDisabled) { this.disable(); } if (this.autofocus) { this.focus(); } } afterRenderOptions() { const visibleOptions = this.getVisibleOptions(); const hasNoOptions = !this.options.length && !this.hasServerSearch; const hasNoSearchResults = !hasNoOptions && !visibleOptions.length; if (!this.allowNewOption || this.hasServerSearch || this.showOptionsOnlyOnSearch) { DomUtils.toggleClass(this.$allWrappers, 'has-no-search-results', hasNoSearchResults); if (hasNoSearchResults) { DomUtils.setAttr(this.$noSearchResults, 'tabindex', '0'); DomUtils.setAttr(this.$noSearchResults, 'aria-hidden', 'false'); } else { DomUtils.setAttr(this.$noSearchResults, 'tabindex', '-1'); DomUtils.setAttr(this.$noSearchResults, 'aria-hidden', 'true'); } } DomUtils.toggleClass(this.$allWrappers, 'has-no-options', hasNoOptions); if (hasNoOptions) { DomUtils.setAttr(this.$noOptions, 'tabindex', '0'); DomUtils.setAttr(this.$noOptions, 'aria-hidden', 'false'); } else { DomUtils.setAttr(this.$noOptions, 'tabindex', '-1'); DomUtils.setAttr(this.$noOptions, 'aria-hidden', 'true'); } this.setOptionAttr(); this.setOptionsPosition(); this.setOptionsTooltip(); if (document.activeElement !== this.$searchInput) { const focusedOption = DomUtils.getElementsBySelector('.focused', this.$dropboxContainer)[0]; if (focusedOption !== undefined) { focusedOption.focus(); } } } afterSetOptionsContainerHeight(reset) { if (reset && this.showAsPopup) { this.setVisibleOptions(); } } afterSetSearchValue() { if (this.hasServerSearch) { clearInterval(this.serverSearchTimeout); this.serverSearchTimeout = setTimeout(() => { this.serverSearch(); }, this.searchDelay); } else { this.setVisibleOptionsCount(); } if (this.selectAllOnlyVisible) { this.toggleAllOptionsClass(); } this.focusOption({ focusFirst: true }); } afterSetVisibleOptionsCount() { this.scrollToTop(); this.setOptionsHeight(); this.setVisibleOptions(); this.updatePosition(); } afterValueSet() { this.scrollToTop(); this.setSearchValue(''); this.renderOptions(); } afterSetOptions(keepValue) { if (keepValue) { this.setSelectedProp(); } this.setOptionsHeight(); this.setVisibleOptions(); if (this.showOptionsOnlyOnSearch) { this.setSearchValue('', false, true); } if (!keepValue) { this.reset(); } } /** after event methods - end */ /** set methods - start */ /** * @param {virtualSelectOptions} params */ setProps(params) { const options = this.setDefaultProps(params); this.setPropsFromElementAttr(options); const { convertToBoolean } = Utils; this.$ele = options.ele; this.dropboxWrapper = options.dropboxWrapper; this.valueKey = options.valueKey; this.labelKey = options.labelKey; this.descriptionKey = options.descriptionKey; this.aliasKey = options.aliasKey; this.optionHeightText = options.optionHeight; this.optionHeight = parseFloat(this.optionHeightText); this.multiple = convertToBoolean(options.multiple); this.hasSearch = convertToBoolean(options.search); this.searchByStartsWith = convertToBoolean(options.searchByStartsWith); this.searchGroup = convertToBoolean(options.searchGroup); this.hideClearButton = convertToBoolean(options.hideClearButton); this.autoSelectFirstOption = convertToBoolean(options.autoSelectFirstOption); this.hasOptionDescription = convertToBoolean(options.hasOptionDescription); this.silentInitialValueSet = convertToBoolean(options.silentInitialValueSet); this.allowNewOption = convertToBoolean(options.allowNewOption); this.markSearchResults = convertToBoolean(options.markSearchResults); this.showSelectedOptionsFirst = convertToBoolean(options.showSelectedOptionsFirst); this.disableSelectAll = convertToBoolean(options.disableSelectAll); this.keepAlwaysOpen = convertToBoolean(options.keepAlwaysOpen); this.showDropboxAsPopup = convertToBoolean(options.showDropboxAsPopup); this.hideValueTooltipOnSelectAll = convertToBoolean(options.hideValueTooltipOnSelectAll); this.showOptionsOnlyOnSearch = convertToBoolean(options.showOptionsOnlyOnSearch); this.selectAllOnlyVisible = convertToBoolean(options.selectAllOnlyVisible); this.alwaysShowSelectedOptionsCount = convertToBoolean(options.alwaysShowSelectedOptionsCount); this.alwaysShowSelectedOptionsLabel = convertToBoolean(options.alwaysShowSelectedOptionsLabel); this.disableAllOptionsSelectedText = convertToBoolean(options.disableAllOptionsSelectedText); this.showValueAsTags = convertToBoolean(options.showValueAsTags); this.disableOptionGroupCheckbox = convertToBoolean(options.disableOptionGroupCheckbox); this.enableSecureText = convertToBoolean(options.enableSecureText); this.setValueAsArray = convertToBoolean(options.setValueAsArray); this.disableValidation = convertToBoolean(options.disableValidation); this.initialDisabled = convertToBoolean(options.disabled); this.required = convertToBoolean(options.required); this.autofocus = convertToBoolean(options.autofocus); this.useGroupValue = convertToBoolean(options.useGroupValue); this.focusSelectedOptionOnOpen = convertToBoolean(options.focusSelectedOptionOnOpen); this.noOptionsText = options.noOptionsText; this.noSearchResultsText = options.noSearchResultsText; this.selectAllText = options.selectAllText; this.searchNormalize = options.searchNormalize; this.searchPlaceholderText = options.searchPlaceholderText; this.searchFormLabel = options.searchFormLabel; this.optionsSelectedText = options.optionsSelectedText; this.optionSelectedText = options.optionSelectedText; this.allOptionsSelectedText = options.allOptionsSelectedText; this.clearButtonText = options.clearButtonText; this.moreText = options.moreText; this.placeholder = options.placeholder; this.position = options.position; this.textDirection = options.textDirection; this.dropboxWidth = options.dropboxWidth; this.tooltipFontSize = options.tooltipFontSize; this.tooltipAlignment = options.tooltipAlignment; this.tooltipMaxWidth = options.tooltipMaxWidth; this.updatePositionThrottle = options.updatePositionThrottle; this.noOfDisplayValues = parseInt(options.noOfDisplayValues); this.zIndex = parseInt(options.zIndex); this.maxValues = parseInt(options.maxValues); this.minValues = parseInt(options.minValues); this.name = this.secureText(options.name); this.additionalClasses = options.additionalClasses; this.additionalDropboxClasses = options.additionalDropboxClasses; this.additionalDropboxContainerClasses = options.additionalDropboxContainerClasses; this.additionalToggleButtonClasses = options.additionalToggleButtonClasses; this.popupDropboxBreakpoint = options.popupDropboxBreakpoint; this.popupPosition = options.popupPosition; this.onServerSearch = options.onServerSearch; this.labelRenderer = options.labelRenderer; this.selectedLabelRenderer = options.selectedLabelRenderer; this.initialSelectedValue = options.selectedValue === 0 ? '0' : options.selectedValue; this.emptyValue = options.emptyValue; this.ariaLabelText = options.ariaLabelText; this.ariaLabelledby = options.ariaLabelledby; this.ariaLabelClearButtonText = options.ariaLabelClearButtonText; this.ariaLabelTagClearButtonText = options.ariaLabelTagClearButtonText; this.ariaLabelSearchClearButtonText = options.ariaLabelSearchClearButtonText; this.maxWidth = options.maxWidth; this.searchDelay = options.searchDelay; this.showDuration = parseInt(options.showDuration); this.hideDuration = parseInt(options.hideDuration); /** @type {string[]} */ this.selectedValues = []; /** @type {virtualSelectOption[]} */ this.selectedOptions = []; this.newValues = []; this.events = {}; this.tooltipEnterDelay = 200; this.searchValue = ''; this.searchValueOriginal = ''; this.isAllSelected = false; if ((options.search === undefined && this.multiple) || this.allowNewOption || this.showOptionsOnlyOnSearch) { this.hasSearch = true; } this.hasServerSearch = typeof this.onServerSearch === 'function'; if (this.maxValues || this.hasServerSearch || this.showOptionsOnlyOnSearch) { this.disableSelectAll = true; this.disableOptionGroupCheckbox = true; } if (this.keepAlwaysOpen) { this.dropboxWrapper = 'self'; } this.showAsPopup = this.showDropboxAsPopup && !this.keepAlwaysOpen && window.innerWidth <= parseFloat(this.popupDropboxBreakpoint); this.hasSearchContainer = this.hasSearch || (this.multiple && !this.disableSelectAll); this.optionsCount = this.getOptionsCount(options.optionsCount); this.halfOptionsCount = Math.ceil(this.optionsCount / 2); this.optionsHeight = this.getOptionsHeight(); this.uniqueId = this.getUniqueId(); } /** * @param {virtualSelectOptions} options */ setDefaultProps(options) { const defaultOptions = { dropboxWrapper: 'self', valueKey: 'value', labelKey: 'label', descriptionKey: 'description', aliasKey: 'alias', ariaLabelText: 'Options list', ariaLabelClearButtonText: 'Clear button', ariaLabelTagClearButtonText: 'Remove option', ariaLabelSearchClearButtonText: 'Clear search input', optionsCount: 5, noOfDisplayValues: 50, optionHeight: '40px', noOptionsText: 'No options found', noSearchResultsText: 'No results found', selectAllText: 'Select All', searchNormalize: false, searchPlaceholderText: 'Search...', searchFormLabel: 'Search', clearButtonText: 'Clear', moreText: 'more...', optionsSelectedText: 'options selected', optionSelectedText: 'option selected', allOptionsSelectedText: 'All', placeholder: 'Select', position: 'bottom left', zIndex: options.keepAlwaysOpen ? 1 : 2, tooltipFontSize: '14px', tooltipAlignment: 'center', tooltipMaxWidth: '300px', updatePositionThrottle: 100, name: '', additionalClasses: '', additionalDropboxClasses: '', additionalDropboxContainerClasses: '', additionalToggleButtonClasses: '', maxValues: 0, showDropboxAsPopup: true, popupDropboxBreakpoint: '576px', popupPosition: 'center', hideValueTooltipOnSelectAll: true, emptyValue: '', searchDelay: 300, focusSelectedOptionOnOpen: true, showDuration: 300, hideDuration: 200, }; if (options.hasOptionDescription) { defaultOptions.optionsCount = 4; defaultOptions.optionHeight = '50px'; } return Object.assign(defaultOptions, options); } setPropsFromElementAttr(options) { const $ele = options.ele; Object.keys(attrPropsMapping).forEach((k) => { let value = $ele.getAttribute(k); if (valueLessProps.indexOf(k) !== -1 && (value === '' || value === 'true')) { value = true; } if (value) { // eslint-disable-next-line no-param-reassign options[attrPropsMapping[k]] = value; } }); } setEleProps() { const { $ele } = this; $ele.virtualSelect = this; $ele.value = this.multiple ? [] : ''; $ele.name = this.name; $ele.disabled = false; $ele.required = this.required; $ele.autofocus = this.autofocus; $ele.multiple = this.multiple; $ele.form = $ele.closest('form'); $ele.reset = VirtualSelect.reset; $ele.setValue = VirtualSelect.setValueMethod; $ele.setOptions = VirtualSelect.setOptionsMethod; $ele.setDisabledOptions = VirtualSelect.setDisabledOptionsMethod; $ele.setEnabledOptions = VirtualSelect.setEnabledOptionsMethod; $ele.toggleSelectAll = VirtualSelect.toggleSelectAll; $ele.isAllSelected = VirtualSelect.isAllSelected; $ele.addOption = VirtualSelect.addOptionMethod; $ele.getNewValue = VirtualSelect.getNewValueMethod; $ele.getDisplayValue = VirtualSelect.getDisplayValueMethod; $ele.getSelectedOptions = VirtualSelect.getSelectedOptionsMethod; $ele.getDisabledOptions = VirtualSelect.getDisabledOptionsMethod; $ele.open = VirtualSelect.openMethod; $ele.close = VirtualSelect.closeMethod; $ele.focus = VirtualSelect.focusMethod; $ele.enable = VirtualSelect.enableMethod; $ele.disable = VirtualSelect.disableMethod; $ele.destroy = VirtualSelect.destroyMethod; $ele.validate = VirtualSelect.validateMethod; $ele.toggleRequired = VirtualSelect.toggleRequiredMethod; if (this.hasDropboxWrapper) { this.$dropboxWrapper.virtualSelect = this; } } setValueMethod(newValue, silentChange) { const valuesMapping = {}; const valuesOrder = {}; let validValues = []; const isMultiSelect = this.multiple; let value = newValue; if (value) { if (!Array.isArray(value)) { value = [value]; } if (isMultiSelect) { const { maxValues } = this; if (maxValues && value.length > maxValues) { value.splice(maxValues); } } else if (value.length > 1) { value = [value[0]]; } /** converting value to string */ value = value.map((v) => (v || v === 0 ? v.toString() : '')); if (this.useGroupValue) { value = this.setGroupOptionsValue(value); } value.forEach((d, i) => { valuesMapping[d] = true; valuesOrder[d] = i; }); if (this.allowNewOption && value) { this.setNewOptionsFromValue(value); } } this.options.forEach((d) => { if (valuesMapping[d.value] === true && !d.isDisabled && !d.isGroupTitle) { // eslint-disable-next-line no-param-reassign d.isSelected = true; validValues.push(d.value); } else { // eslint-disable-next-line no-param-reassign d.isSelected = false; } }); if (isMultiSelect) { if (this.hasOptionGroup) { this.setGroupsSelectedProp(); } /** sorting validValues in the given values order */ validValues.sort((a, b) => valuesOrder[a] - valuesOrder[b]); } else { /** taking first value for single select */ [validValues] = validValues; } this.beforeValueSet(); this.setValue(validValues, { disableEvent: silentChange }); this.afterValueSet(); } setGroupOptionsValue(preparedValues) { const selectedValues = []; const selectedGroups = {}; const valuesMapping = {}; preparedValues.forEach((d) => { valuesMapping[d] = true; }); this.options.forEach((d) => { const { value } = d; const isSelected = valuesMapping[value] === true; if (d.isGroupTitle) { if (isSelected) { selectedGroups[d.index] = true; } } else if (isSelected || selectedGroups[d.groupIndex]) { selectedValues.push(value); } }); return selectedValues; } setGroupsSelectedProp() { const isAllGroupOptionsSelected = this.isAllGroupOptionsSelected.bind(this); this.options.forEach((d) => { if (d.isGroupTitle) { // eslint-disable-next-line no-param-reassign d.isSelected = isAllGroupOptionsSelected(d.index); } }); } setOptionsMethod(options, keepValue) { this.setOptions(options); this.afterSetOptions(keepValue); } setDisabledOptionsMethod(disabledOptions, keepValue = false) { this.setDisabledOptions(disabledOptions, true); if (!keepValue) { this.setValueMethod(null); this.toggleAllOptionsClass(); } this.setVisibleOptions(); } setDisabledOptions(disabledOptions, setOptionsProp = false) { let disabledOptionsArr = []; if (!disabledOptions) { if (setOptionsProp) { this.options.forEach((d) => { // eslint-disable-next-line no-param-reassign d.isDisabled = false; return d; }); } } else if (disabledOptions === true) { if (setOptionsProp) { this.options.forEach((d) => { // eslint-disable-next-line no-param-reassign d.isDisabled = true; disabledOptionsArr.push(d.value); return d; }); } } else { disabledOptionsArr = disabledOptions.map((d) => d.toString()); const disabledOptionsMapping = {}; disabledOptionsArr.forEach((d) => { disabledOptionsMapping[d] = true; }); if (setOptionsProp) { this.options.forEach((d) => { // eslint-disable-next-line no-param-reassign d.isDisabled = disabledOptionsMapping[d.value] === true; return d; }); } } this.disabledOptions = disabledOptionsArr; } setEnabledOptionsMethod(disabledOptions, keepValue = false) { this.setEnabledOptions(disabledOptions); if (!keepValue) { this.setValueMethod(null); this.toggleAllOptionsClass(); } this.setVisibleOptions(); } setEnabledOptions(enabledOptions) { if (enabledOptions === undefined) { return; } const disabledOptionsArr = []; if (enabledOptions === true) { this.options.forEach((d) => { // eslint-disable-next-line no-param-reassign d.isDisabled = false; return d; }); } else { const enabledOptionsMapping = {}; enabledOptions.forEach((d) => { enabledOptionsMapping[d] = true; }); this.options.forEach((d) => { const isDisabled = enabledOptionsMapping[d.value] !== true; // eslint-disable-next-line no-param-reassign d.isDisabled = isDisabled; if (isDisabled) { disabledOptionsArr.push(d.value); } return d; }); } this.disabledOptions = disabledOptionsArr; } setOptions(options = []) { const preparedOptions = []; const hasDisabledOptions = this.disabledOptions.length; const { valueKey, labelKey, descriptionKey, aliasKey, hasOptionDescription } = this; const { getString, convertToBoolean } = Utils; const secureText = this.secureText.bind(this); const getAlias = this.getAlias.bind(this); let index = 0; let hasOptionGroup = false; const disabledOptionsMapping = {}; let hasEmptyValueOption = false; this.disabledOptions.forEach((d) => { disabledOptionsMapping[d] = true; }); const prepareOption = (d) => { if (typeof d !== 'object') { // eslint-disable-next-line no-param-reassign d = { [valueKey]: d, [labelKey]: d }; } const value = secureText(getString(d[valueKey])); const label = secureText(getString(d[labelKey])); const childOptions = d.options; const isGroupTitle = !!childOptions; const option = { index, value, label, labelNormalized: this.searchNormalize ? Utils.normalizeString(label).toLowerCase() : label.toLowerCase(), alias: getAlias(d[aliasKey]), isVisible: convertToBoolean(d.isVisible, true), isNew: d.isNew || false, isGroupTitle, classNames: d.classNames, }; if (!hasEmptyValueOption && value === '') { hasEmptyValueOption = true; } if (hasDisabledOptions) { option.isDisabled = disabledOptionsMapping[value] === true; } if (d.isGroupOption) { option.isGroupOption = true; option.groupIndex = d.groupIndex; } if (hasOptionDescription) { option.description = secureText(getString(d[descriptionKey])); } if (d.customData) { option.customData = d.customData; } preparedOptions.push(option); index += 1; if (isGroupTitle) { const groupIndex = option.index; hasOptionGroup = true; childOptions.forEach((childData) => { // eslint-disable-next-line no-param-reassign childData.isGroupOption = true; // eslint-disable-next-line no-param-reassign childData.groupIndex = groupIndex; prepareOption(childData); }); } }; if (Array.isArray(options)) { options.forEach(prepareOption); } const optionsLength = preparedOptions.length; const { $ele } = this; $ele.options = preparedOptions; $ele.length = optionsLength; this.options = preparedOptions; this.visibleOptionsCount = optionsLength; this.lastOptionIndex = optionsLength - 1; this.newValues = []; this.hasOptionGroup = hasOptionGroup; this.hasEmptyValueOption = hasEmptyValueOption; this.setSortedOptions(); } setServerOptions(options = []) { this.setOptionsMethod(options, true); const { selectedOptions } = this; const newOptions = this.options; let optionsUpdated = false; /** merging already selected options details with new options */ if (selectedOptions.length) { const newOptionsValueMapping = {}; optionsUpdated = true; newOptions.forEach((d) => { newOptionsValueMapping[d.value] = true; }); selectedOptions.forEach((d) => { if (newOptionsValueMapping[d.value] !== true) { // eslint-disable-next-line no-param-reassign d.isVisible = false; newOptions.push(d); } }); this.setOptionsMethod(newOptions, true); } /** merging new search option */ if (this.allowNewOption && this.searchValue) { const hasExactOption = newOptions.some((d) => d.label.toLowerCase() === this.searchValue); if (!hasExactOption) { optionsUpdated = true; this.setNewOption(); } } if (optionsUpdated) { this.setVisibleOptionsCount(); if (this.multiple) { this.toggleAllOptionsClass(); } this.setValueText(); } else { this.updatePosition(); } this.setVisibleOptionsCount(); DomUtils.removeClass(this.$allWrappers, 'server-searching'); } setSelectedOptions() { this.selectedOptions = this.options.filter((d) => d.isSelected); } setSortedOptions() { let sortedOptions = [...this.options]; if (this.showSelectedOptionsFirst && this.selectedValues.length) { if (this.hasOptionGroup) { sortedOptions = this.sortOptionsGroup(sortedOptions); } else { sortedOptions = this.sortOptions(sortedOptions); } } this.sortedOptions = sortedOptions; } setVisibleOptions() { let visibleOptions = [...this.sortedOptions]; const maxOptionsToShow = this.optionsCount * 2; const startIndex = this.getVisibleStartIndex(); const newOption = this.getNewOption(); const endIndex = startIndex + maxOptionsToShow - 1; let i = 0; if (newOption) { newOption.visibleIndex = i; i += 1; } visibleOptions = visibleOptions.filter((d) => { let inView = false; if (d.isVisible && !d.isCurrentNew) { inView = i >= startIndex && i <= endIndex; // eslint-disable-next-line no-param-reassign d.visibleIndex = i; i += 1; } return inView; }); if (newOption) { visibleOptions = [newOption, ...visibleOptions]; } this.visibleOptions = visibleOptions; // update number of visible options this.visibleOptionsCount = visibleOptions.length; this.renderOptions(); } setOptionsPosition(startIndex) { // We use the parseInt to fix a Chrome issue when dealing with decimal pixels in translate3d const top = parseInt((startIndex || this.getVisibleStartIndex()) * this.optionHeight); this.$options.style.transform = `translate3d(0, ${top}px, 0)`; DomUtils.setData(this.$options, 'top', top); } setOptionsTooltip() { const visibleOptions = this.getVisibleOptions(); const { hasOptionDescription } = this; visibleOptions.forEach((d) => { const $optionEle = this.$dropboxContainer.querySelector(`.vscomp-option[data-index="${d.index}"]`); DomUtils.setData($optionEle.querySelector('.vscomp-option-text'), 'tooltip', d.label); if (hasOptionDescription) { DomUtils.setData($optionEle.querySelector('.vscomp-option-description'), 'tooltip', d.description); } }); } setValue(value, { disableEvent = false, disableValidation = false } = {}) { const isValidValue = (this.hasEmptyValueOption && value === '') || value; if (!isValidValue) { this.selectedValues = []; } else if (Array.isArray(value)) { this.selectedValues = [...value]; } else { this.selectedValues = [value]; } const newValue = this.getValue(); this.$ele.value = newValue; this.$hiddenInput.value = this.getInputValue(newValue); this.isMaxValuesSelected = !!(this.maxValues && this.maxValues <= this.selectedValues.length); this.toggleAllOptionsClass(); this.setValueText(); const hasValue = Utils.isNotEmpty(this.selectedValues); DomUtils.toggleClass(this.$allWrappers, 'has-value', hasValue); DomUtils.toggleClass(this.$allWrappers, 'max-value-selected', this.isMaxValuesSelected); DomUtils.setAttr(this.$clearButton, 'tabindex', hasValue ? '0' : '-1'); DomUtils.setAria(this.$clearButton, 'hidden', hasValue === false); if (!disableValidation) { this.validate(); } if (!disableEvent) { DomUtils.dispatchEvent(this.$ele, 'change', true); } } setValueText() { const { multi