selectic
Version:
Smart Select for VueJS 3.x
1,064 lines (1,054 loc) • 148 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var vtyx = require('vtyx');
var vue = require('vue');
function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (!css || typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css_248z = "/* {{{ Variables */\n\n:root {\n --selectic-font-size: 14px;\n --selectic-cursor-disabled: not-allowed;\n\n /* The main element */\n --selectic-color: #555555;\n --selectic-bg: #ffffff;\n\n /* The main element (when disabled) */\n --selectic-color-disabled: #787878;\n --selectic-bg-disabled: #eeeeee;\n\n /* The list */\n --selectic-panel-bg: #f0f0f0;\n --selectic-separator-bordercolor: #cccccc;\n /* --selectic-item-color: var(--selectic-color); /* Can be set in any CSS configuration */\n\n /* The current selected item */\n --selectic-selected-item-color: #428bca;\n\n /* When mouse is over items or by selecting with key arrows */\n --selectic-active-item-color: #ffffff;\n --selectic-active-item-bg: #66afe9;\n\n /* Selected values in main element */\n --selectic-value-bg: #f0f0f0;\n /* --selectic-more-items-bg: var(--selectic-info-bg); /* can be set in any CSS configuration */\n /* --selectic-more-items-color: var(--selectic-info-color); /* can be set in any CSS configuration */\n --selectic-more-items-bg-disabled: #cccccc;\n\n /* Information message */\n --selectic-info-bg: #5bc0de;\n --selectic-info-color: #ffffff;\n\n /* Error message */\n --selectic-error-bg: #b72c29;\n --selectic-error-color: #ffffff;\n\n /* XXX: Currently it is important to keep this size for a correct scroll\n * height estimation */\n --selectic-input-height: 30px;\n}\n\n/* }}} */\n/* {{{ Bootstrap equivalent style */\n\n.selectic .form-control {\n display: block;\n width: 100%;\n height: calc(var(--selectic-input-height) - 2px);\n font-size: var(--selectic-font-size);\n line-height: 1.42857143;\n color: var(--selectic-color);\n background-color: var(--selectic-bg);\n background-image: none;\n border: 1px solid var(--selectic-separator-bordercolor); /* should use a better variable */\n border-radius: 4px;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;\n}\n\n.selectic .has-feedback {\n position: relative;\n}\n\n.selectic .has-feedback .form-control {\n padding-right: calc(var(--selectic-input-height) + 4px);\n}\n\n.selectic .form-control-feedback.fa,\n.selectic .form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: calc(var(--selectic-input-height) + 4px);\n height: calc(var(--selectic-input-height) + 4px);\n line-height: var(--selectic-input-height);\n text-align: center;\n pointer-events: none;\n}\n\n.selectic .alert-info {\n background-color: var(--selectic-info-bg);\n color: var(--selectic-info-color);\n}\n\n.selectic .alert-danger {\n background-color: var(--selectic-error-bg);\n color: var(--selectic-error-color);\n}\n\n/* }}} */\n\n.selectic * {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.selectic.form-control {\n display: inline-block;\n padding: 0;\n cursor: pointer;\n border: unset;\n}\n\n.has-feedback .selectic__icon-container.form-control-feedback {\n right: 0;\n}\n\n/* The input which contains the selected value\n * XXX: This input should stay hidden behind other elements, but is \"visible\"\n * (in term of DOM point of view) in order to get and to trigger the `focus`\n * DOM event. */\n.selectic__input-value {\n position: fixed;\n opacity: 0;\n z-index: -1000;\n top: -100px;\n}\n\n/* XXX: .form-control has been added to this selector to improve priority and\n * override some rules of the original .form-control */\n.selectic-input.form-control {\n display: inline-flex;\n justify-content: space-between;\n overflow: hidden;\n width: 100%;\n min-height: var(--selectic-input-height);\n padding-top: 0;\n padding-bottom: 0;\n padding-left: 5px;\n line-height: calc(var(--selectic-input-height) - 4px);\n color: var(--selectic-color);\n}\n\n.selectic-input__reverse-icon {\n align-self: center;\n margin-right: 3px;\n cursor: default;\n}\n\n.selectic-input__clear-icon {\n align-self: center;\n margin-left: 3px;\n cursor: pointer;\n}\n\n.selectic-input__clear-icon:hover {\n color: var(--selectic-selected-item-color);\n}\n\n.selectic-input.focused {\n border-bottom-left-radius: 0px;\n border-bottom-right-radius: 0px;\n}\n\n.selectic-input.disabled {\n cursor: var(--selectic-cursor-disabled);\n background-color: var(--selectic-bg-disabled);\n}\n\n.selectic-input.disabled .more-items {\n\tbackground-color: var(--selectic-more-items-bg-disabled);\n}\n\n.selectic-input__selected-items {\n display: inline-flex;\n flex-wrap: nowrap;\n align-items: center;\n white-space: nowrap;\n}\n\n.selectic-input__selected-items__placeholder {\n font-style: italic;\n opacity: 0.7;\n white-space: nowrap;\n}\n\n.selectic-icon {\n color: var(--selectic-color);\n text-align: center;\n vertical-align: middle;\n}\n\n.selectic__extended-list {\n position: fixed;\n top: var(--top-position, 0);\n z-index: 2000;\n height: auto;\n max-height: var(--availableSpace);\n background-color: var(--selectic-bg, #ffffff);\n box-shadow: 2px 5px 12px 0px #888888;\n border-radius: 0 0 4px 4px;\n padding: 0;\n width: var(--list-width, 200px);\n min-width: 200px;\n display: grid;\n grid-template-rows: minmax(0, max-content) 1fr;\n}\n\n.selectic__extended-list.selectic-position-top {\n box-shadow: 2px -3px 12px 0px #888888;\n}\n\n.selectic__extended-list__list-container{\n overflow: auto;\n}\n\n.selectic__extended-list__list-items {\n max-height: calc(var(--selectic-input-height) * 10);\n min-width: max-content;\n padding-left: 0;\n}\n\n.selectic-item {\n display: block;\n position: relative;\n box-sizing: border-box;\n padding: 2px 8px;\n color: var(--selectic-item-color, var(--selectic-color));\n min-height: calc(var(--selectic-input-height) - 3px);\n list-style-type: none;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.selectic-item_text {\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n}\n\n.selectic-item:not(.selected) .selectic-item_icon {\n opacity: 0;\n}\n\n.selectic-item_text {\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n}\n\n.selectic-item__active {\n background-color: var(--selectic-active-item-bg);\n color: var(--selectic-active-item-color);\n}\n\n.selectic-item__active:not(.selected) .selectic-item_icon {\n opacity: 0.2;\n}\n\n.selectic-item__active:not(.selected) .single-select_icon {\n opacity: 0;\n}\n\n.selectic-item__active.selectic-item__disabled:not(.selected) .selectic-item_icon {\n opacity: 0;\n}\n\n.selectic-item__disabled {\n color: var(--selectic-color-disabled);\n background-color: var(--selectic-bg-disabled);\n}\n\n.selectic-item__is-in-group {\n padding-left: 2em;\n}\n\n.selectic-item__is-group {\n font-weight: bold;\n cursor: default;\n}\n\n.selectic-item__is-group.selectable {\n cursor: pointer;\n}\n\n.selectic-item.selected {\n color: var(--selectic-selected-item-color);\n}\n\n.selectic-search-scope {\n color: #e0e0e0;\n left: auto;\n right: 10px;\n}\n\n.selectic .form-control-feedback.fa.selectic-search-scope {\n width: calc(var(--selectic-input-height) * 0.75);\n height: calc(var(--selectic-input-height) * 0.75);\n line-height: calc(var(--selectic-input-height) * 0.75);\n}\n\n.selectic__message {\n text-align: center;\n padding: 3px;\n}\n\n.selectic .filter-panel {\n padding: 3px;\n margin-left: 0px;\n margin-right: 0px;\n background-color: var(--selectic-panel-bg);\n border-bottom: 1px solid var(--selectic-separator-bordercolor);\n}\n\n.selectic .panelclosed {\n max-height: 0px;\n transition: max-height 0.3s ease-out;\n overflow: hidden;\n}\n\n.panelopened {\n max-height: 200px;\n transition: max-height 0.3s ease-in;\n overflow: hidden;\n}\n\n.selectic .filter-panel__input {\n padding-left: 0px;\n padding-right: 0px;\n padding-bottom: 10px;\n margin-bottom: 0px;\n}\n\n.selectic .filter-input {\n height: calc(var(--selectic-input-height) * 0.75);\n}\n\n.selectic .checkbox-filter {\n padding: 5px;\n text-align: center;\n}\n\n.selectic .curtain-handler {\n text-align: center;\n}\n\n.selectic .toggle-selectic {\n margin: 5px;\n padding-left: 0px;\n padding-right: 0px;\n}\n\n.selectic .toggle-boolean-select-all-toggle {\n display: inline;\n margin-right: 15px;\n}\n\n.selectic .toggle-boolean-excluding-toggle {\n display: inline;\n margin-right: 15px;\n}\n\n.selectic .single-value {\n display: grid;\n grid-template: \"value icon\" 1fr / max-content max-content;\n\n padding: 2px;\n padding-left: 5px;\n margin-left: 0;\n margin-right: 5px;\n /* margin top/bottom are mainly to create a gutter in multilines */\n margin-top: 2px;\n margin-bottom: 2px;\n\n border-radius: 3px;\n background-color: var(--selectic-value-bg);\n max-height: calc(var(--selectic-input-height) - 10px);\n max-width: 100%;\n min-width: 30px;\n\n overflow: hidden;\n white-space: nowrap;\n line-height: initial;\n vertical-align: middle;\n}\n\n.selectic .more-items {\n display: inline-block;\n\n padding-left: 5px;\n padding-right: 5px;\n border-radius: 10px;\n\n background-color: var(--selectic-more-items-bg, var(--selectic-info-bg));\n color: var(--selectic-more-items-color, var(--selectic-info-color));\n cursor: help;\n}\n\n.selectic-input__selected-items__value {\n grid-area: value;\n align-self: center;\n justify-self: normal;\n text-overflow: ellipsis;\n overflow: hidden;\n white-space: nowrap;\n}\n\n.selectic-input__selected-items__icon {\n grid-area: icon;\n align-self: center;\n justify-self: center;\n margin-left: 5px;\n}\n\n.selectic-input__selected-items__icon:hover {\n color: var(--selectic-selected-item-color);\n}\n\n.selectic__label-disabled {\n opacity: 0.5;\n transition: opacity 400ms;\n}\n\n/* XXX: override padding of bootstrap input-sm.\n * This padding introduce a line shift. */\n.selectic.input-sm {\n padding: 0;\n}\n\n/* {{{ overflow multiline */\n\n.selectic--overflow-multiline,\n.selectic--overflow-multiline.form-control,\n.selectic--overflow-multiline .form-control {\n height: unset;\n}\n\n.selectic--overflow-multiline .selectic-input {\n overflow: unset;\n}\n\n.selectic--overflow-multiline .selectic-input__selected-items {\n flex-wrap: wrap;\n}\n\n/* {{{ icons */\n\n@keyframes selectic-animation-spin {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(359deg);\n }\n}\n\n.selectic__icon {\n height: 1em;\n fill: currentColor;\n}\n\n.selectic-spin {\n animation: selectic-animation-spin 2s infinite linear;\n}\n\n/* }}} */\n";
styleInject(css_248z);
/**
* Clone the object and its inner properties.
* @param obj The object to be clone.
* @param attributes list of attributes to not clone.
* @param refs internal reference to object to avoid cyclic references
* @returns a copy of obj
*/
function deepClone(origObject, ignoreAttributes = [], refs = new WeakMap()) {
const obj = vue.unref(origObject);
/* For circular references */
if (refs.has(obj)) {
return refs.get(obj);
}
if (typeof obj === 'object') {
if (obj === null) {
return obj;
}
if (Array.isArray(obj)) {
const ref = [];
refs.set(obj, ref);
obj.forEach((val, idx) => {
ref[idx] = deepClone(val, ignoreAttributes, refs);
});
return ref;
}
if (obj instanceof RegExp) {
const ref = new RegExp(obj.source, obj.flags);
refs.set(obj, ref);
return ref;
}
/* This should be an object */
const ref = {};
refs.set(obj, ref);
for (const [key, val] of Object.entries(obj)) {
if (ignoreAttributes.includes(key)) {
ref[key] = val;
continue;
}
ref[key] = deepClone(val, ignoreAttributes, refs);
}
return ref;
}
/* This should be a primitive */
return obj;
}
/**
* Escape search string to consider regexp special characters as they
* are and not like special characters.
* Consider * characters as a wildcards characters (meanings 0 or
* more characters) and convert them to .* (the wildcard characters
* in Regexp)
*
* @param {String} name the original string to convert
* @param {String} [flag] mode to apply for regExp
* @return {String} the string ready to use for RegExp format
*/
function convertToRegExp(name, flag = 'i') {
const pattern = name.replace(/[\\^$.+?(){}[\]|]/g, '\\$&')
.replace(/\*/g, '.*');
return new RegExp(pattern, flag);
}
/** Does the same as Object.assign but does not replace if value is undefined */
function assignObject(obj, ...sourceObjects) {
const result = obj;
for (const source of sourceObjects) {
for (const key of Object.keys(source)) {
const typedKey = key;
const value = source[typedKey];
if (value === undefined) {
continue;
}
result[typedKey] = value;
}
}
return result;
}
/**
* Ckeck whether a value is primitive.
* @returns true if val is primitive and false otherwise.
*/
function isPrimitive(val) {
/* The value null is treated explicitly because in JavaScript
* `typeof null === 'object'` is evaluated to `true`.
*/
return val === null || (typeof val !== 'object' && typeof val !== 'function');
}
/**
* Performs a deep comparison between two objects to determine if they
* should be considered equal.
*
* @param objA object to compare to objB.
* @param objB object to compare to objA.
* @param attributes list of attributes to not compare.
* @param refs internal reference to object to avoid cyclic references
* @returns true if objA should be considered equal to objB.
*/
function isDeepEqual(objA, objB, ignoreAttributes = [], refs = new WeakMap()) {
objA = vue.unref(objA);
objB = vue.unref(objB);
/* For primitive types */
if (isPrimitive(objA)) {
return isPrimitive(objB) && Object.is(objA, objB);
}
/* For functions (follow the behavior of _.isEqual and compare functions
* by reference). */
if (typeof objA === 'function') {
return typeof objB === 'function' && objA === objB;
}
/* For circular references */
if (refs.has(objA)) {
return refs.get(objA) === objB;
}
refs.set(objA, objB);
/* For objects */
if (typeof objA === 'object') {
if (typeof objB !== 'object') {
return false;
}
/* For arrays */
if (Array.isArray(objA)) {
return Array.isArray(objB) &&
objA.length === objB.length &&
!objA.some((val, idx) => !isDeepEqual(val, objB[idx], ignoreAttributes, refs));
}
/* For RegExp */
if (objA instanceof RegExp) {
return objB instanceof RegExp &&
objA.source === objB.source &&
objA.flags === objB.flags;
}
/* For Date */
if (objA instanceof Date) {
return objB instanceof Date && objA.getTime() === objB.getTime();
}
/* This should be an object */
const aRec = objA;
const bRec = objB;
const aKeys = Object.keys(aRec).filter((key) => !ignoreAttributes.includes(key));
const bKeys = Object.keys(bRec).filter((key) => !ignoreAttributes.includes(key));
const differentKeyFound = aKeys.some((key) => {
return !bKeys.includes(key) ||
!isDeepEqual(aRec[key], bRec[key], ignoreAttributes, refs);
});
return aKeys.length === bKeys.length && !differentKeyFound;
}
return true;
}
let displayLog = false;
function debug(fName, step, ...args) {
if (!displayLog) {
return;
}
console.log('--%s-- [%s]', fName, step, ...args);
}
/** Enable logs for debugging */
debug.enable = (display) => {
displayLog = display;
};
/* File Purpose:
* It keeps and computes all states at a single place.
* Every inner components of Selectic should communicate with this file to
* change or to get states.
*/
/* For debugging */
debug.enable(false);
/* }}} */
/* {{{ Static */
function changeTexts$1(texts) {
messages = Object.assign(messages, texts);
}
function changeIcons$1(newIcons, newFamilyIcon) {
icons = Object.assign(icons, newIcons);
if (newFamilyIcon) {
defaultFamilyIcon = newFamilyIcon;
}
}
/* }}} */
let messages = {
noFetchMethod: 'Fetch callback is missing: it is not possible to retrieve data.',
searchPlaceholder: 'Search',
searching: 'Searching',
cannotSelectAllSearchedItems: 'Cannot select all items: too much items in the search result.',
cannotSelectAllRevertItems: 'Cannot select all items: some items are not fetched yet.',
selectAll: 'Select all',
excludeResult: 'Invert selection',
reverseSelection: 'The displayed elements are those not selected.',
noData: 'No data',
noResult: 'No results',
clearSelection: 'Clear current selection',
clearSelections: 'Clear all selections',
wrongFormattedData: 'The data fetched is not correctly formatted.',
moreSelectedItem: '+1 other',
moreSelectedItems: '+%d others',
unknownPropertyValue: 'property "%s" has incorrect values.',
wrongQueryResult: 'Query did not return all results.',
};
let defaultFamilyIcon = 'selectic';
let icons = {};
let closePreviousSelectic;
/**
* Time to wait before considering there is no other requests.
* This time is await only if there is already a requested request.
*/
const DEBOUNCE_REQUEST = 250;
/* }}} */
let uid = 0;
class SelecticStore {
constructor(props = {}) {
/* Do not need reactivity */
this.requestId = 0;
this.requestSearchId = 0; /* Used for search request */
this.isRequesting = false;
this._uid = ++uid;
/* {{{ Props */
const defaultProps = {
value: null,
selectionIsExcluded: false,
disabled: false,
options: null,
childOptions: [],
groups: [],
texts: null,
icons: null,
iconFamily: null,
params: {},
fetchCallback: null,
getItemsCallback: null,
keepOpenWithOtherSelectic: false,
};
const propsVal = assignObject(defaultProps, props);
this.props = vue.reactive(propsVal);
/* }}} */
/* {{{ data */
this.state = vue.reactive({
activeItemIdx: -1,
allOptions: [],
allowClearSelection: false,
allowRevert: undefined,
autoDisabled: true,
autoSelect: true,
disabled: false,
disableGroupSelection: false,
dynOptions: [],
filteredOptions: [],
forceSelectAll: 'auto',
groups: new Map(),
hideFilter: false,
internalValue: null,
isOpen: false,
keepFilterOpen: false,
listPosition: 'auto',
multiple: false,
offsetItem: 0,
optionBehaviorOperation: 'sort',
optionBehaviorOrder: ['O', 'D', 'E'],
pageSize: 100,
placeholder: '',
searchText: '',
selectedOptions: null,
selectionIsExcluded: false,
selectionOverflow: 'collapsed',
strictValue: false,
totalAllOptions: Infinity,
totalDynOptions: Infinity,
totalFilteredOptions: Infinity,
status: {
areAllSelected: false,
automaticChange: false,
automaticClose: false,
errorMessage: '',
hasChanged: false,
searching: false,
},
});
this.data = vue.reactive({
labels: Object.assign({}, messages),
icons: Object.assign({}, icons),
iconFamily: defaultFamilyIcon,
itemsPerPage: 10,
doNotUpdate: false,
cacheItem: new Map(),
activeOrder: 'D',
dynOffset: 0,
});
/* }}} */
/* {{{ computed */
this.marginSize = vue.computed(() => {
return this.state.pageSize / 2;
});
this.isPartial = vue.computed(() => {
const state = this.state;
let isPartial = typeof this.props.fetchCallback === 'function';
if (isPartial &&
state.optionBehaviorOperation === 'force' &&
this.data.activeOrder !== 'D') {
isPartial = false;
}
return isPartial;
});
this.hasAllItems = vue.computed(() => {
const state = this.state;
const nbItems = state.totalFilteredOptions + state.groups.size;
return this.state.filteredOptions.length >= nbItems;
});
this.hasFetchedAllItems = vue.computed(() => {
const isPartial = vue.unref(this.isPartial);
if (!isPartial) {
return true;
}
const state = this.state;
return state.dynOptions.length === state.totalDynOptions;
});
this.listOptions = vue.computed(() => {
return this.getListOptions();
});
this.elementOptions = vue.computed(() => {
return this.getElementOptions();
});
this.allowGroupSelection = vue.computed(() => {
return this.state.multiple && !this.isPartial.value && !this.state.disableGroupSelection;
});
/* }}} */
/* {{{ watch */
vue.watch(() => [this.props.options, this.props.childOptions], () => {
this.data.cacheItem.clear();
this.setAutomaticClose();
this.commit('isOpen', false);
this.clearDisplay();
this.buildAllOptions(true);
this.buildSelectedOptions();
}, { deep: true });
vue.watch(() => [this.listOptions, this.elementOptions], () => {
/* TODO: transform allOptions as a computed properties and this
* watcher become useless */
this.buildAllOptions(true);
}, { deep: true });
vue.watch(() => this.props.value, () => {
var _a;
const value = (_a = this.props.value) !== null && _a !== void 0 ? _a : null;
this.commit('internalValue', value);
}, { deep: true });
vue.watch(() => this.props.selectionIsExcluded, () => {
this.commit('selectionIsExcluded', this.props.selectionIsExcluded);
});
vue.watch(() => this.props.disabled, () => {
this.commit('disabled', this.props.disabled);
});
vue.watch(() => this.state.filteredOptions, () => {
let areAllSelected = false;
const hasAllItems = vue.unref(this.hasAllItems);
if (hasAllItems) {
const selectionIsExcluded = +this.state.selectionIsExcluded;
/* eslint-disable-next-line no-bitwise */
areAllSelected = this.state.filteredOptions.every((item) => !!(+item.selected ^ selectionIsExcluded));
}
this.state.status.areAllSelected = areAllSelected;
}, { deep: true });
vue.watch(() => this.state.internalValue, () => {
this.buildSelectedOptions();
/* If there is only one item, and the previous selected value was
* different, then if we change it to the only available item we
* should disable Selectic (user has no more choice).
* This is why it is needed to check autoDisabled here. */
this.checkAutoDisabled();
}, { deep: true });
vue.watch(() => this.state.allOptions, () => {
this.checkAutoSelect();
this.checkAutoDisabled();
}, { deep: true });
vue.watch(() => this.state.totalAllOptions, () => {
this.checkHideFilter();
});
/* }}} */
this.closeSelectic = () => {
this.setAutomaticClose();
this.commit('isOpen', false);
};
const value = deepClone(this.props.value);
/* set initial value for non reactive attribute */
this.cacheRequest = new Map();
const stateParam = deepClone(this.props.params, ['data']);
if (stateParam.optionBehavior) {
this.buildOptionBehavior(stateParam.optionBehavior, stateParam);
delete stateParam.optionBehavior;
}
if (stateParam.hideFilter === 'auto') {
delete stateParam.hideFilter;
}
else if (stateParam.hideFilter === 'open') {
this.state.keepFilterOpen = true;
delete stateParam.hideFilter;
}
/* Update state */
assignObject(this.state, stateParam);
/* XXX: should be done in 2 lines, in order to set the multiple state
* and ensure convertValue run with correct state */
assignObject(this.state, {
internalValue: this.convertTypeValue(value),
selectionIsExcluded: !!this.props.selectionIsExcluded,
disabled: !!this.props.disabled, /* XXX: !! is needed to copy value and not proxy reference */
});
this.checkHideFilter();
if (this.props.texts) {
this.changeTexts(this.props.texts);
}
if (this.props.icons || this.props.iconFamily) {
this.changeIcons(this.props.icons, this.props.iconFamily);
}
this.addGroups(this.props.groups);
this.assertValueType();
this.buildAllOptions();
this.buildSelectedOptions();
this.checkAutoDisabled();
}
/* {{{ methods */
/* {{{ public methods */
commit(name, value) {
const oldValue = this.state[name];
debug('commit', 'start', name, value, 'oldValue:', oldValue);
if (oldValue === value) {
return;
}
this.state[name] = value;
switch (name) {
case 'searchText':
this.state.offsetItem = 0;
this.state.activeItemIdx = -1;
this.clearDisplay();
if (value) {
this.buildFilteredOptions();
}
else {
this.buildAllOptions(true);
}
break;
case 'isOpen':
if (closePreviousSelectic === this.closeSelectic) {
closePreviousSelectic = undefined;
}
if (value) {
if (this.state.disabled) {
this.commit('isOpen', false);
return;
}
this.state.offsetItem = 0;
this.state.activeItemIdx = -1;
this.resetChange();
this.buildFilteredOptions();
if (typeof closePreviousSelectic === 'function') {
closePreviousSelectic();
}
if (!this.props.keepOpenWithOtherSelectic) {
closePreviousSelectic = this.closeSelectic;
}
}
break;
case 'offsetItem':
this.buildFilteredOptions();
break;
case 'internalValue':
this.assertCorrectValue();
this.updateFilteredOptions();
break;
case 'selectionIsExcluded':
this.assertCorrectValue();
this.updateFilteredOptions();
this.buildSelectedOptions();
break;
case 'disabled':
if (value) {
this.setAutomaticClose();
this.commit('isOpen', false);
}
break;
}
debug('commit', '(done)', name);
}
setAutomaticChange() {
this.state.status.automaticChange = true;
setTimeout(() => this.state.status.automaticChange = false, 0);
}
setAutomaticClose() {
this.state.status.automaticClose = true;
setTimeout(() => this.state.status.automaticClose = false, 0);
}
getItem(id) {
let item;
if (this.hasItemInStore(id)) {
item = this.data.cacheItem.get(id);
}
else {
this.getItems([id]);
item = {
id,
text: String(id),
};
}
return this.buildItems([item])[0];
}
async getItems(ids) {
const itemsToFetch = ids.filter((id) => !this.hasItemInStore(id));
const getItemsCallback = this.props.getItemsCallback;
if (itemsToFetch.length && typeof getItemsCallback === 'function') {
const cacheRequest = this.cacheRequest;
const requestId = itemsToFetch.toString();
let promise;
if (cacheRequest.has(requestId)) {
promise = cacheRequest.get(requestId);
}
else {
promise = getItemsCallback(itemsToFetch);
cacheRequest.set(requestId, promise);
promise.then(() => {
cacheRequest.delete(requestId);
});
}
const items = await promise;
const cacheItem = this.data.cacheItem;
for (const item of items) {
if (item) {
cacheItem.set(item.id, item);
}
}
}
return this.buildSelectedItems(ids);
}
selectGroup(id, itemsSelected) {
const state = this.state;
if (!vue.unref(this.allowGroupSelection)) {
return;
}
const selectItem = this.selectItem.bind(this);
let hasChanged = false;
this.data.doNotUpdate = true;
const items = state.filteredOptions.filter((item) => {
const isInGroup = item.group === id && !item.exclusive && !item.disabled;
if (isInGroup) {
hasChanged = selectItem(item.id, itemsSelected, true) || hasChanged;
}
return isInGroup;
});
this.data.doNotUpdate = false;
if (hasChanged && items.length) {
this.updateFilteredOptions();
}
return;
}
selectItem(id, selected, keepOpen = false) {
const state = this.state;
let hasChanged = false;
const item = state.allOptions.find((opt) => opt.id === id);
/* Check that item is not disabled */
if (item === null || item === void 0 ? void 0 : item.disabled) {
return hasChanged;
}
if (state.strictValue && !this.hasValue(id)) {
/* reject invalid values */
return hasChanged;
}
if (state.multiple) {
/* multiple = true */
const internalValue = state.internalValue;
const isAlreadySelected = internalValue.includes(id);
if (selected === undefined) {
selected = !isAlreadySelected;
}
const selectedOptions = Array.isArray(state.selectedOptions)
? state.selectedOptions
: [];
if (id === null) {
/* Keep disabled items: we cannot removed them because they
* are disabled */
const newSelection = selectedOptions.reduce((list, item) => {
if (item.disabled && item.id) {
list.push(item.id);
}
return list;
}, []);
state.internalValue = newSelection;
hasChanged = internalValue.length > newSelection.length;
}
else if (selected && !isAlreadySelected) {
let addItem = true;
if (item === null || item === void 0 ? void 0 : item.exclusive) {
const hasDisabledSelected = selectedOptions.some((opt) => {
return opt.disabled;
});
if (hasDisabledSelected) {
/* do not remove disabled item from selection */
addItem = false;
}
else {
/* clear the current selection because the item is exclusive */
internalValue.splice(0, Infinity);
}
}
else if (internalValue.length === 1) {
const selectedId = internalValue[0];
const selectedItem = state.allOptions.find((opt) => opt.id === selectedId);
if (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.exclusive) {
if (selectedItem.disabled) {
/* If selected item is disabled and exclusive do not change the selection */
addItem = false;
}
else {
/* clear the current selection because the old item was exclusive */
internalValue.pop();
}
}
}
if (addItem) {
internalValue.push(id);
hasChanged = true;
}
}
else if (!selected && isAlreadySelected) {
internalValue.splice(internalValue.indexOf(id), 1);
hasChanged = true;
}
if (hasChanged) {
this.updateFilteredOptions();
}
}
else {
/* multiple = false */
const oldValue = state.internalValue;
if (!keepOpen) {
this.commit('isOpen', false);
}
if (selected === undefined || id === null) {
selected = true;
}
if (!selected) {
if (id !== oldValue) {
return hasChanged;
}
const oldOption = state.selectedOptions;
if (oldOption === null || oldOption === void 0 ? void 0 : oldOption.disabled) {
/* old selection is disabled so do not unselect it */
return hasChanged;
}
id = null;
}
else if (id === oldValue) {
return hasChanged;
}
if (keepOpen) {
/* if keepOpen is true it means that it is an automatic change */
this.setAutomaticChange();
}
this.commit('internalValue', id);
hasChanged = true;
}
if (hasChanged) {
state.status.hasChanged = true;
}
return hasChanged;
}
toggleSelectAll() {
if (!this.state.multiple) {
return;
}
const hasAllItems = vue.unref(this.hasAllItems);
if (!hasAllItems) {
const labels = this.data.labels;
if (this.state.searchText) {
this.state.status.errorMessage = labels.cannotSelectAllSearchedItems;
return;
}
if (!this.state.allowRevert) {
this.state.status.errorMessage = labels.cannotSelectAllRevertItems;
return;
}
const value = this.state.internalValue;
const selectionIsExcluded = !!value.length || !this.state.selectionIsExcluded;
this.state.selectionIsExcluded = selectionIsExcluded;
this.state.internalValue = [];
this.state.status.hasChanged = true;
this.updateFilteredOptions();
return;
}
const selectAll = !this.state.status.areAllSelected;
this.state.status.areAllSelected = selectAll;
this.data.doNotUpdate = true;
this.state.filteredOptions.forEach((item) => this.selectItem(item.id, selectAll));
this.data.doNotUpdate = false;
this.updateFilteredOptions();
}
resetChange() {
this.state.status.hasChanged = false;
}
resetErrorMessage() {
this.state.status.errorMessage = '';
}
clearCache(forceReset = false) {
debug('clearCache', 'start', forceReset);
const isPartial = vue.unref(this.isPartial);
const total = isPartial ? Infinity : 0;
this.data.cacheItem.clear();
this.state.allOptions = [];
this.state.totalAllOptions = total;
this.state.totalDynOptions = total;
this.clearDisplay();
this.state.status.errorMessage = '';
this.state.status.hasChanged = false;
if (forceReset) {
this.state.internalValue = null;
this.state.selectionIsExcluded = false;
this.state.searchText = '';
}
this.assertCorrectValue();
if (forceReset) {
this.buildFilteredOptions();
}
else {
this.buildAllOptions();
}
}
changeGroups(groups) {
this.state.groups.clear();
this.addGroups(groups);
this.buildFilteredOptions();
}
changeTexts(texts) {
this.data.labels = Object.assign({}, this.data.labels, texts);
}
changeIcons(icons, family) {
if (icons) {
this.data.icons = Object.assign({}, this.data.icons, icons);
}
if (typeof family === 'string') {
this.data.iconFamily = family;
}
}
/* }}} */
/* {{{ private methods */
hasValue(id) {
if (id === null) {
return true;
}
return !!this.getValue(id);
}
getValue(id) {
function findId(option) {
return option.id === id;
}
return this.state.filteredOptions.find(findId) ||
this.state.dynOptions.find(findId) ||
vue.unref(this.listOptions).find(findId) ||
vue.unref(this.elementOptions).find(findId);
}
convertTypeValue(oldValue) {
const state = this.state;
const isMultiple = state.multiple;
let newValue = oldValue;
if (isMultiple) {
if (!Array.isArray(oldValue)) {
newValue = oldValue === null ? [] : [oldValue];
}
}
else {
if (Array.isArray(oldValue)) {
const value = oldValue[0];
newValue = typeof value === 'undefined' ? null : value;
}
}
return newValue;
}
assertValueType() {
const state = this.state;
const internalValue = state.internalValue;
const newValue = this.convertTypeValue(internalValue);
if (newValue !== internalValue) {
this.setAutomaticChange();
state.internalValue = newValue;
}
}
assertCorrectValue(applyStrict = false) {
const state = this.state;
this.assertValueType();
const internalValue = state.internalValue;
const selectionIsExcluded = state.selectionIsExcluded;
const isMultiple = state.multiple;
const checkStrict = state.strictValue;
let newValue = internalValue;
const isPartial = vue.unref(this.isPartial);
if (isMultiple) {
const hasFetchedAllItems = vue.unref(this.hasFetchedAllItems);
if (selectionIsExcluded && hasFetchedAllItems) {
newValue = state.allOptions.reduce((values, option) => {
const id = option.id;
if (!internalValue.includes(id)) {
values.push(id);
}
return values;
}, []);
state.selectionIsExcluded = false;
}
}
else {
state.selectionIsExcluded = false;
}
if (checkStrict) {
let isDifferent = false;
let filteredValue;
if (isMultiple) {
filteredValue = newValue
.filter((value) => this.hasItemInStore(value));
isDifferent = filteredValue.length !== newValue.length;
if (isDifferent && isPartial && !applyStrict) {
this.getItems(newValue)
.then(() => this.assertCorrectValue(true));
return;
}
}
else if (newValue !== null && !this.hasItemInStore(newValue)) {
filteredValue = null;
isDifferent = true;
if (isPartial && !applyStrict) {
this.getItems([newValue])
.then(() => this.assertCorrectValue(true));
return;
}
}
if (isDifferent) {
this.setAutomaticChange();
newValue = filteredValue;
}
}
state.internalValue = newValue;
if (state.autoSelect && newValue === null) {
this.checkAutoSelect();
}
}
/** Reset the display cache in order to rebuild it */
clearDisplay() {
debug('clearDisplay', 'start');
this.state.filteredOptions = [];
this.state.totalFilteredOptions = Infinity;
}
/** rebuild the state filteredOptions to normalize their values */
updateFilteredOptions() {
if (!this.data.doNotUpdate) {
this.state.filteredOptions = this.buildItems(this.state.filteredOptions);
this.buildSelectedOptions();
this.updateGroupSelection();
}
}
addGroups(groups) {
groups.forEach((group) => {
this.state.groups.set(group.id, group.text);
});
}
/** This method is for the computed property listOptions */
getListOptions() {
const options = deepClone(this.props.options, ['data']);
const listOptions = [];
if (!Array.isArray(options)) {
return listOptions;
}
const state = this.state;
options.forEach((option) => {
/* manage simple string */
if (typeof option === 'string') {
listOptions.push({
id: option,
text: option,
});
return;
}
const group = option.group;
const subOptions = option.options;
/* check for groups */
if (group && !state.groups.has(group)) {
state.groups.set(group, String(group));
}
/* check for sub options */
if (subOptions) {
const groupId = option.id;
state.groups.set(groupId, option.text);
subOptions.forEach((subOpt) => {
subOpt.group = groupId;
});
listOptions.push(...subOptions);
return;
}
listOptions.push(option);
});
return listOptions;
}
/** This method is for the computed property elementOptions */
getElementOptions() {
const options = deepClone(this.props.childOptions, ['data']);
const childOptions = [];
if (!Array.isArray(options) || options.length === 0) {
return childOptions;
}
const state = this.state;
options.forEach((option) => {
const group = option.group;
const subOptions = option.options;
/* check for groups */
if (group && !state.groups.has(group)) {
state.groups.set(group, String(group));
}
/* check for sub options */
if (subOptions) {
const groupId = option.id;
state.groups.set(groupId, option.text);
const sOpts = subOptions.map((subOpt) => {
return Object.assign({}, subOpt, {
group: groupId,
});
});
childOptions.push(...sOpts);
return;
}
childOptions.push(option);
});
return childOptions;
}
/** Generate the list of all options by combining the 3 option lists */
buildAllOptions(keepFetched = false, stopFetch = false) {
debug('buildAllOptions', 'start', 'keepFetched', keepFetched, 'stopFetch', stopFetch);
const allOptions = [];
let listOptions = [];
let elementOptions = [];
const optionBehaviorOrder = this.state.optionBehaviorOrder;
let length = Infinity;
const isPartial = vue.unref(this.isPartial);
const arrayFromOrder = (orderValue) => {
switch (orderValue) {
case 'O': return listOptions;
case 'D': return this.state.dynOptions;
case 'E': return elementOptions;
}
return [];
};
const lengthFromOrder = (orderValue) => {
switch (orderValue) {
case 'O': return listOptions.length;
case 'D': return this.state.totalDynOptions;
case 'E': return elementOptions.length;
}
return 0;
};
if (!keepFetched) {
if (isPartial) {
this.state.totalAllOptions = Infinity;
this.state.totalDynOptions = Infinity;
}
else {
this.state.totalDynOptions = 0;
}
}
listOptions = vue.unref(this.listOptions);
elementOptions = vue.unref(this.elementOptions);
if (this.state.optionBehaviorOperation === 'force') {
const orderValue = optionBehaviorOrder.find((value) => lengthFromOrder(value) > 0);
allOptions.push(...arrayFromOrder(orderValue));
length = lengthFromOrder(orderValue);
this.data.activeOrder = orderValue;
this.data.dynOffset = 0;
}
else {
/* sort */
let offset = 0;
for (const orderValue of optionBehaviorOrder) {
const list = arrayFromOrder(orderValue);
const lngth = lengthFromOrder(orderValue);
if (orderValue === 'D') {
this.data.dynOffset = offset;
}
else {
offset += lngth;
}
allOptions.push(...list);
if (list.length < lngth) {
/* All dynamic options are not fetched yet */
break;
}
}
this.data.activeOrder = 'D';
length = optionBehaviorOrder.reduce((total, orderValue) => total + lengthFromOrder(orderValue), 0);
}
this.state.allOptions = allOptions;
if (keepFetched) {
this.state.totalAllOptions = length;
}
else {
if (!isPartial) {
this.state.totalAllOptions = allOptions.length;
}
}
if (!stopFetch) {
this.buildFilteredOptions().then(() => {
/* XXX: To recompute for strict mode and auto-select */
this.assertCorrectValue();
});
}
else {
/* Do not fetch again just build filteredOptions */
const search = this.state.searchText;
if (!search) {
this.setFilteredOptions(this.buildGroupItems(allOptions));
return;
}
const options = this.filterOptions(allOptions, search);
this.setFilteredOptions(options);
}
debug('buildAllOptions', 'end', 'allOptions:', this.state.allOptions.length, 'totalAllOptions:', this.state.totalAllOptions);
}
async buildFilteredOptions() {
const state = this.state;
if (!state.isOpen) {
/* Do not try to fetch anything while the select is not open */
return;
}
const allOptions = state.allOptions;
const search = state.searchText;
const totalAllOptions = state.totalAllOptions;
const allOptionsLength = allOptions.length;
let filteredOptionsLength = state.filteredOptions.length;
const hasAllItems = vue.unref(this.hasAllItems);
debug('buildFilteredOptions', 'start', 'hasAllItems:', hasAllItems, 'allOptions', allOptions.length, 'search:', search, 'filteredOptionsLength:', filteredOptionsLength);
if (hasAllItems)