import {ACIndex, ACItem, buildHighlightedDom} from 'app/client/lib/ACIndex'; import {Autocomplete, IAutocompleteOptions} from 'app/client/lib/autocomplete'; import {theme} from "app/client/ui2018/cssVars"; import {icon} from "app/client/ui2018/icons"; import {menuCssClass} from 'app/client/ui2018/menus'; import {dom, DomElementArg, Holder, IDisposableOwner, Observable, styled} from 'grainjs'; export interface ACSelectItem extends ACItem { value: string; label: string; } /** * Builds a text input with an autocomplete dropdown. * Note that because it is currently only used in the right-side panel, it is designed to avoid * keeping focus. */ export function buildACSelect( owner: IDisposableOwner, options: { disabled?: Observable, acIndex: ACIndex, valueObs: Observable, save: (value: string, item: ACSelectItem|undefined) => Promise|void }, ...args: DomElementArg[] ) { const {acIndex, valueObs, save} = options; const acHolder = Holder.create>(owner); let textInput: HTMLInputElement; const isOpen = () => !acHolder.isEmpty(); const acOpen = () => acHolder.isEmpty() && Autocomplete.create(acHolder, textInput, acOptions); const acClose = () => acHolder.clear(); const finish = () => { acClose(); textInput.blur(); }; const revert = () => { textInput.value = valueObs.get(); finish(); }; const commitOrRevert = async () => { (await commitIfValid()) || revert(); }; const openOrCommit = () => { isOpen() ? commitOrRevert().catch(() => {}) : acOpen(); }; const commitIfValid = async () => { const item = acHolder.get()?.getSelectedItem(); if (item) { textInput.value = item.value; } textInput.disabled = true; try { await save(textInput.value, item); finish(); return true; } catch (e) { return false; } finally { textInput.disabled = false; } }; const onMouseDown = (ev: MouseEvent) => { ev.preventDefault(); // Don't let it affect focus, since we focus/blur manually. if (options.disabled?.get()) { return; } if (!isOpen()) { textInput.focus(); } openOrCommit(); }; const acOptions: IAutocompleteOptions = { menuCssClass: `${menuCssClass} test-acselect-dropdown`, search: async (term: string) => acIndex.search(term), renderItem: (item, highlightFunc) => cssSelectItem(buildHighlightedDom(item.label, highlightFunc, cssMatchText)), getItemText: (item) => item.value, onClick: commitIfValid, }; return cssSelectBtn( textInput = cssInput({type: 'text'}, dom.prop('value', valueObs), dom.on('focus', (ev, elem) => elem.select()), dom.on('blur', commitOrRevert), dom.prop("disabled", (use) => options.disabled ? use(options.disabled) : false), dom.onKeyDown({ Escape: revert, Enter: openOrCommit, ArrowDown: acOpen, Tab: commitIfValid, }), dom.on('input', acOpen), ), dom.on('mousedown', onMouseDown), cssIcon('Dropdown'), ...args ); } const cssSelectBtn = styled('div', ` position: relative; width: 100%; height: 30px; color: ${theme.selectButtonFg}; --icon-color: ${theme.selectButtonFg}; `); const cssSelectItem = styled('li', ` color: ${theme.menuItemFg}; display: block; white-space: pre; overflow: hidden; text-overflow: ellipsis; outline: none; padding: var(--weaseljs-menu-item-padding, 8px 24px); cursor: pointer; &.selected { background-color: ${theme.menuItemSelectedBg}; color: ${theme.menuItemSelectedFg}; } `); const cssInput = styled('input', ` color: ${theme.inputFg}; background-color: ${theme.inputBg}; appearance: none; -webkit-appearance: none; -moz-appearance: none; height: 100%; width: 100%; padding: 0 6px; outline: none; border: 1px solid ${theme.inputBorder}; border-radius: 3px; cursor: pointer; line-height: 16px; cursor: pointer; &:disabled { color: ${theme.inputDisabledFg}; background-color: ${theme.inputDisabledBg}; } &:focus { cursor: initial; outline: none; box-shadow: 0px 0px 2px 2px ${theme.inputFocus}; } &::placeholder { color: ${theme.inputPlaceholderFg}; } `); const cssIcon = styled(icon, ` position: absolute; right: 6px; top: calc(50% - 8px); `); const cssMatchText = styled('span', ` color: ${theme.autocompleteMatchText}; .selected > & { color: ${theme.autocompleteSelectedMatchText}; } `);