UNPKG

selectic

Version:
1,064 lines (1,054 loc) 148 kB
'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)