import * as AceEditor from 'app/client/components/AceEditor'; import {createGroup} from 'app/client/components/commands'; import {makeT} from 'app/client/lib/localization'; import {buildHighlightedCode} from 'app/client/ui/CodeHighlight'; import {theme} from 'app/client/ui2018/cssVars'; import {createMobileButtons, getButtonMargins} from 'app/client/widgets/EditorButtons'; import {EditorPlacement, ISize} from 'app/client/widgets/EditorPlacement'; import {initializeAceOptions} from 'app/client/widgets/FormulaEditor'; import {IEditorCommandGroup} from 'app/client/widgets/NewBaseEditor'; import {ISuggestionWithValue} from 'app/common/ActiveDocAPI'; import { Computed, Disposable, dom, DomElementArg, Holder, IDisposableOwner, Observable, styled, } from 'grainjs'; const t = makeT('DropdownConditionEditor'); interface BuildDropdownConditionEditorOptions { value: Computed; disabled: Computed; onSave(value: string): Promise; onDispose(): void; getAutocompleteSuggestions(prefix: string): ISuggestionWithValue[]; } /** * Builds an editor for dropdown conditions. * * Dropdown conditions are client-evaluated predicate formulas used to filter * items shown in autocomplete dropdowns for Choice and Reference type columns. * * Unlike Python formulas, dropdown conditions only support a very limited set of * features. They are a close relative of ACL formulas, sharing the same underlying * parser and compiler. * * See `sandbox/grist/predicate_formula.py` and `app/common/PredicateFormula.ts` for * more details on parsing and compiling, respectively. */ export function buildDropdownConditionEditor( owner: IDisposableOwner, options: BuildDropdownConditionEditorOptions, ...args: DomElementArg[] ) { const {value, disabled, onSave, onDispose, getAutocompleteSuggestions} = options; return dom.create(buildHighlightedCode, value, {maxLines: 1}, dom.cls(cssDropdownConditionField.className), dom.cls('disabled'), cssDropdownConditionField.cls('-disabled', disabled), {tabIndex: '-1'}, dom.on('focus', (_, refElem) => openDropdownConditionEditor(owner, { refElem, value, onSave, onDispose, getAutocompleteSuggestions, })), ...args, ); } function openDropdownConditionEditor(owner: IDisposableOwner, options: { refElem: Element; value: Computed; onSave: (value: string) => Promise; onDispose: () => void; getAutocompleteSuggestions(prefix: string): ISuggestionWithValue[]; }) { const {refElem, value, onSave, onDispose, getAutocompleteSuggestions} = options; const saveAndDispose = async () => { const editorValue = editor.getValue(); if (editorValue !== value.get()) { await onSave(editorValue); } if (editor.isDisposed()) { return; } editor.dispose(); }; const commands: IEditorCommandGroup = { fieldEditCancel: () => editor.dispose(), fieldEditSaveHere: () => editor.blur(), fieldEditSave: () => editor.blur(), }; const editor = DropdownConditionEditor.create(owner, { editValue: value.get(), commands, onBlur: saveAndDispose, getAutocompleteSuggestions, }); editor.attach(refElem); editor.onDispose(() => onDispose()); } interface DropdownConditionEditorOptions { editValue: string; commands: IEditorCommandGroup; onBlur(): Promise; getAutocompleteSuggestions(prefix: string): ISuggestionWithValue[]; } class DropdownConditionEditor extends Disposable { private _aceEditor: any; private _dom: HTMLElement; private _editorPlacement!: EditorPlacement; private _placementHolder = Holder.create(this); private _isEmpty: Computed; constructor(private _options: DropdownConditionEditorOptions) { super(); const initialValue = _options.editValue; const editorState = Observable.create(this, initialValue); this._aceEditor = this.autoDispose(AceEditor.create({ calcSize: this._calcSize.bind(this), editorState, getSuggestions: _options.getAutocompleteSuggestions, })); this._isEmpty = Computed.create(this, editorState, (_use, state) => state === ''); this.autoDispose(this._isEmpty.addListener(() => this._updateEditorPlaceholder())); const commandGroup = this.autoDispose(createGroup({ ..._options.commands, }, this, true)); this._dom = cssDropdownConditionEditorWrapper( cssDropdownConditionEditor( createMobileButtons(_options.commands), this._aceEditor.buildDom((aceObj: any) => { initializeAceOptions(aceObj); const val = initialValue; const pos = val.length; this._aceEditor.setValue(val, pos); this._aceEditor.attachCommandGroup(commandGroup); if (val === '') { this._updateEditorPlaceholder(); } }) ), ); } public attach(cellElem: Element): void { this._editorPlacement = EditorPlacement.create(this._placementHolder, this._dom, cellElem, { margins: getButtonMargins(), }); this.autoDispose(this._editorPlacement.onReposition.addListener(this._aceEditor.resize, this._aceEditor)); this._aceEditor.onAttach(); this._updateEditorPlaceholder(); this._aceEditor.resize(); this._aceEditor.getEditor().focus(); this._aceEditor.getEditor().on('blur', () => this._options.onBlur()); } public getValue(): string { return this._aceEditor.getValue(); } public blur() { this._aceEditor.getEditor().blur(); } private _updateEditorPlaceholder() { const editor = this._aceEditor.getEditor(); const shouldShowPlaceholder = editor.session.getValue().length === 0; if (editor.renderer.emptyMessageNode) { // Remove the current placeholder if one is present. editor.renderer.scroller.removeChild(editor.renderer.emptyMessageNode); } if (!shouldShowPlaceholder) { editor.renderer.emptyMessageNode = null; } else { editor.renderer.emptyMessageNode = cssDropdownConditionPlaceholder(t('Enter condition.')); editor.renderer.scroller.appendChild(editor.renderer.emptyMessageNode); } } private _calcSize(elem: HTMLElement, desiredElemSize: ISize) { const placeholder: HTMLElement | undefined = this._aceEditor.getEditor().renderer.emptyMessageNode; if (placeholder) { return this._editorPlacement.calcSizeWithPadding(elem, { width: placeholder.scrollWidth, height: placeholder.scrollHeight, }); } else { return this._editorPlacement.calcSizeWithPadding(elem, { width: desiredElemSize.width, height: desiredElemSize.height, }); } } } const cssDropdownConditionField = styled('div', ` flex: auto; cursor: pointer; margin-top: 4px; &-disabled { opacity: 0.4; pointer-events: none; } `); const cssDropdownConditionEditorWrapper = styled('div.default_editor.formula_editor_wrapper', ` border-radius: 3px; `); const cssDropdownConditionEditor = styled('div', ` background-color: ${theme.aceEditorBg}; padding: 5px; z-index: 10; overflow: hidden; flex: none; min-height: 22px; border-radius: 3px; `); const cssDropdownConditionPlaceholder = styled('div', ` color: ${theme.lightText}; font-style: italic; white-space: nowrap; `);