virtual-select-plugin
Version:
A javascript plugin for dropdown with virtual scroll
1,601 lines (1,332 loc) • 100 kB
JavaScript
/** 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"> </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"> </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}>×</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