UNPKG

@limetech/lime-elements

Version:
483 lines (479 loc) • 17.9 kB
import { r as registerInstance, c as createEvent, h, a as getElement } from './index-DBTJNfo7.js'; import { i as isDescendant } from './dom-B9Ofc5RB.js'; import { T as TAB, E as ESCAPE, a as ENTER, A as ARROW_UP, b as ARROW_DOWN } from './keycodes-rI0IeKpx.js'; import { c as createRandomString } from './random-string-JbKhhoXs.js'; import { a as getIconFillColor, g as getIconName } from './get-icon-props-CgNJbSP4.js'; import { d as debounce } from './debounce-B67JMchz.js'; import './isObject-BJQylLSL.js'; import './toNumber-D6JYpQI9.js'; import './isSymbol-ClAke5ga.js'; import './isObjectLike-oiMYqRQ0.js'; const pickerCss = () => `:host{position:relative;display:block}:host([hidden]){display:none}`; const SEARCH_DEBOUNCE = 300; const CHIP_SET_TAG_NAME = 'limel-chip-set'; const DEFAULT_SEARCHER_MAX_RESULTS = 20; const Picker = class { constructor(hostRef) { registerInstance(this, hostRef); this.change = createEvent(this, "change"); this.interact = createEvent(this, "interact"); this.action = createEvent(this, "action"); /** * True if the picker should be disabled */ this.disabled = false; /** * Set to `true` to disable adding and removing items, * but allow interaction with existing items. */ this.readonly = false; /** * True if the control requires a value */ this.required = false; /** * Set to `true` to indicate that the current value of the input field is * invalid. */ this.invalid = false; /** * Only used if no `searcher` is provided. The picker will then use a * default search function that filters the `allItems` based on the * `text` and `secondaryText` properties of the items. * This way, custom search functions are typically only needed when the * search is done on the server. * For performance reasons, the default searcher will never return more * than 20 items, but if there are more than 20 items, the rest can be * found by typing more characters in the search field. */ this.allItems = []; /** * True if multiple values are allowed */ this.multiple = false; /** * Sets delimiters between chips. Works only when `multiple` is `true`. */ this.delimiter = null; /** * Static actions that can be clicked by the user. */ this.actions = []; /** * Position of the custom static actions in the picker's results dropdown. * Can be set to `'top'` or `'bottom'`. */ this.actionPosition = 'bottom'; /** * Scroll behavior of the custom static actions, when user scrolls * in the picker's results dropdown. Can be set to `'scroll'` which means * the action items will scroll together with the list, or `'sticky'` which * retains their position at the top or bottom of the drop down while * scrolling. */ this.actionScrollBehavior = 'sticky'; /** * Whether badge icons should be used in the result list or not */ this.badgeIcons = false; this.textValue = ''; this.loading = false; this.chips = []; // Should NOT be decorated with State(), since this // should not trigger a re-render by itself. this.chipSetEditMode = false; this.getValueId = (item) => { const value = item.value; if (!!value && typeof value === 'object') { return value.id; } return value; }; this.createChips = (value) => { if (!value) { return []; } if (this.multiple) { const listItems = value; return listItems.map(this.createChip); } const listItem = value; return [this.createChip(listItem)]; }; this.createChip = (listItem) => { const name = getIconName(listItem.icon); const color = getIconFillColor(listItem.icon, listItem.iconColor); const valueId = this.getValueId(listItem); return { id: `${valueId}`, text: listItem.text, removable: true, icon: name ? { name: name, color: color } : undefined, image: listItem.image, value: listItem, menuItems: listItem.actions, }; }; this.search = async (query) => { const timeoutId = setTimeout(() => { this.loading = true; }); const searcher = this.searcher || this.defaultSearcher; const result = (await searcher(this.textValue)); // If the search function resolves immediately, // the loading spinner will not be shown. clearTimeout(timeoutId); this.handleSearchResult(query, result); }; this.defaultSearcher = async (query) => { if (query === '') { return this.allItems.slice(0, DEFAULT_SEARCHER_MAX_RESULTS); } const filteredItems = this.allItems.filter((item) => { let searchText = item.text.toLowerCase(); if (item.secondaryText) { searchText = searchText + ' ' + item.secondaryText.toLowerCase(); } return searchText.includes(query.toLowerCase()); }); return filteredItems.slice(0, DEFAULT_SEARCHER_MAX_RESULTS); }; this.handleTextInput = this.handleTextInput.bind(this); this.handleInputKeyDown = this.handleInputKeyDown.bind(this); this.handleDropdownKeyDown = this.handleDropdownKeyDown.bind(this); this.handleInputFieldFocus = this.handleInputFieldFocus.bind(this); this.handleChange = this.handleChange.bind(this); this.handleInteract = this.handleInteract.bind(this); this.handleListChange = this.handleListChange.bind(this); this.handleActionListChange = this.handleActionListChange.bind(this); this.handleStopEditAndBlur = this.handleStopEditAndBlur.bind(this); this.handleCloseMenu = this.handleCloseMenu.bind(this); this.onListKeyDown = this.onListKeyDown.bind(this); this.portalId = createRandomString(); this.debouncedSearch = debounce(this.search, SEARCH_DEBOUNCE); } componentWillLoad() { this.chips = this.createChips(this.value); } componentDidLoad() { this.chipSet = this.host.shadowRoot.querySelector(CHIP_SET_TAG_NAME); this.updateTabIndex(); } disconnectedCallback() { this.debouncedSearch.cancel(); } onDisabledChange() { this.updateTabIndex(); } updateTabIndex() { if (this.disabled || this.readonly) { this.host.setAttribute('tabindex', '-1'); return; } this.host.setAttribute('tabindex', '0'); } async componentWillUpdate() { this.chipSetEditMode = false; if (this.chipSet) { this.chipSetEditMode = await this.chipSet.getEditMode(); } } render() { const props = {}; if (!this.multiple) { props.maxItems = 1; } return [ h("limel-chip-set", Object.assign({ key: 'e20f22b6907ad1d28f8b8c0847e5080faca09b3b', type: "input", inputType: "search", label: this.label, helperText: this.helperText, leadingIcon: this.leadingIcon, value: this.chips, disabled: this.disabled, invalid: this.invalid, delimiter: this.renderDelimiter(), readonly: this.readonly, required: this.required, searchLabel: this.searchLabel, onInput: this.handleTextInput, onKeyDown: this.handleInputKeyDown, onChange: this.handleChange, onInteract: this.handleInteract, onStartEdit: this.handleInputFieldFocus, onStopEdit: this.handleStopEditAndBlur, emptyInputOnBlur: false, clearAllButton: this.multiple && !this.chipSetEditMode }, props)), this.renderDropdown(), ]; } onChangeValue() { this.chips = this.createChips(this.value); } renderDelimiter() { if (this.multiple) { return this.delimiter; } return null; } /** * Renders the dropdown with the items to pick from, or a spinner if the picker * is waiting for items to be received * * @returns picker dropdown */ renderDropdown() { const dropDownContent = this.getDropdownContent(); const content = []; if (this.shouldShowDropDownContent()) { const actionContent = this.getActionContent(); if (this.actionPosition === 'top') { content.push(actionContent); } if (dropDownContent) { content.push(dropDownContent); } if (this.actionPosition === 'bottom') { content.push(actionContent); } } return this.renderPortal(content); } getActionContent() { var _a, _b; const actionCount = (_b = (_a = this.actions) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0; if (actionCount === 0) { return null; } return [ h("limel-list", { class: { 'static-actions-list': true, 'is-on-top': this.actionPosition === 'top', 'is-at-bottom': this.actionPosition === 'bottom', 'has-position-sticky': this.actionScrollBehavior === 'sticky', }, badgeIcons: true, type: 'selectable', onChange: this.handleActionListChange, items: this.actions.map(this.removeUnusedPropertiesOnAction) }), ]; } removeUnusedPropertiesOnAction(action) { return Object.assign(Object.assign({}, action), { actions: [] }); } shouldShowDropDownContent() { if (this.isFull()) { return false; } return !!this.chipSetEditMode; } getDropdownContent() { var _a; if (!this.shouldShowDropDownContent()) { return; } if (this.loading) { return this.renderSpinner(); } if (!((_a = this.items) === null || _a === void 0 ? void 0 : _a.length)) { return this.renderEmptyMessage(); } return this.renderListResult(); } /** * Returns true if the picker is "full" * The picker is considered to be full if it has a value and only one is allowed * * @returns true if the picker is full */ isFull() { return !this.multiple && !!this.value; } renderSpinner() { return (h("div", { style: { width: '100%', display: 'flex', 'align-items': 'center', 'justify-content': 'center', padding: '1rem 0', } }, h("limel-spinner", { limeBranded: false }))); } renderEmptyMessage() { if (!this.emptyResultMessage) { return; } const style = { color: 'rgb(var(--contrast-1100))', 'text-align': 'center', margin: '0.5rem 1rem', }; return h("p", { style: style }, this.emptyResultMessage); } renderListResult() { return (h("limel-list", { badgeIcons: this.badgeIcons, onChange: this.handleListChange, onKeyDown: this.onListKeyDown, type: "selectable", items: this.items })); } onListKeyDown(event) { const keyFound = [TAB, ESCAPE, ENTER].includes(event.key); if (keyFound) { this.chipSet.setFocus(); } } renderPortal(content = []) { const dropdownZIndex = getComputedStyle(this.host).getPropertyValue('--dropdown-z-index'); return (h("limel-portal", { visible: content.length > 0, containerId: this.portalId, inheritParentWidth: true, containerStyle: { 'z-index': dropdownZIndex } }, h("limel-menu-surface", { open: content.length > 0, allowClicksElement: this.host, style: { '--menu-surface-width': '100%', 'max-height': 'inherit', display: 'flex', }, onDismiss: this.handleCloseMenu }, content))); } /** * Check if a descendant still has focus. If not, reset text value and search result. */ handleStopEditAndBlur() { // In browsers where shadow DOM is not supported activeElement on shadowRoot will return null // However, document.activeElement will return the actual focused element instead of the outermost shadow host const element = this.host.shadowRoot.activeElement || document.activeElement; const portalElement = document.querySelector(`#${this.portalId}`); if (isDescendant(element, this.host) || isDescendant(element, portalElement)) { return; } this.clearInputField(); } /** * Input handler for the input field * * @param event - event */ async handleTextInput(event) { event.stopPropagation(); const query = event.detail; this.textValue = query; this.debouncedSearch(query); // If the search-query is an empty string, bypass debouncing. if (query === '') { this.debouncedSearch.flush(); } } /** * Change handler for the list * * @param event - event */ handleListChange(event) { var _a; event.stopPropagation(); if (!this.value || this.value !== event.detail) { let newValue = event.detail; if (this.multiple) { newValue = [ ...this.value, event.detail, ]; } this.change.emit(newValue); this.items = []; } if (this.multiple) { this.textValue = ''; (_a = this.chipSet) === null || _a === void 0 ? void 0 : _a.setFocus(true); } } /** * Change handler for the list * * @param event - event */ handleActionListChange(event) { event.stopPropagation(); if (!event.detail) { return; } this.action.emit(event.detail.value); this.items = []; } /** * Focus handler for the chip set * Prevent focus if the picker has a value and does not support multiple values */ handleInputFieldFocus() { const query = this.textValue; this.debouncedSearch(query); } handleChange(event) { event.stopPropagation(); let newValue = null; if (this.multiple) { const chips = event.detail; newValue = chips.map((chip) => { return this.value.find((item) => { const valueId = this.getValueId(item); return `${valueId}` === chip.id; }); }); } this.change.emit(newValue); } handleInteract(event) { event.stopPropagation(); this.interact.emit(event.detail ? event.detail.value : event.detail); } /** * Key handler for the input field * Will change focus to the first/last item in the dropdown list to enable selection with the keyboard * * @param event - event */ handleInputKeyDown(event) { const isForwardTab = event.key === TAB && !event.altKey && !event.metaKey && !event.shiftKey; const isUp = event.key === ARROW_UP; const isDown = event.key === ARROW_DOWN; if (!isForwardTab && !isUp && !isDown) { return; } const list = document.querySelector(` #${this.portalId} limel-list`); if (!list) { return; } event.preventDefault(); if (isForwardTab || isDown) { const listElement = list.shadowRoot.querySelector('.mdc-deprecated-list-item:first-child'); listElement.focus(); return; } if (isUp) { const listElement = list.shadowRoot.querySelector('.mdc-deprecated-list-item:last-child'); listElement.focus(); } } /** * Key handler for the dropdown * * @param event - event */ handleDropdownKeyDown(event) { const isEscape = event.key === ESCAPE; if (isEscape) { event.preventDefault(); this.textValue = ''; this.chipSet.setFocus(true); } } handleSearchResult(query, result) { if (query === this.textValue) { this.items = result; if (this.multiple) { const values = this.value; this.items = result.filter((item) => { return !values.includes(item); }); } this.loading = false; } } handleCloseMenu() { if (this.items.length > 0) { return; } this.clearInputField(); } clearInputField() { this.chipSet.emptyInput(); this.textValue = ''; this.handleSearchResult('', []); this.debouncedSearch.cancel(); } static get delegatesFocus() { return true; } get host() { return getElement(this); } static get watchers() { return { "disabled": [{ "onDisabledChange": 0 }], "value": [{ "onChangeValue": 0 }] }; } }; Picker.style = pickerCss(); export { Picker as limel_picker };