mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
3112433a58
Summary: Dropdown conditions let you specify a predicate formula that's used to filter choices and references in their respective autocomplete dropdown menus. Test Plan: Python and browser tests (WIP). Reviewers: jarek, paulfitz Reviewed By: jarek Subscribers: dsagal, paulfitz Differential Revision: https://phab.getgrist.com/D4235
235 lines
7.2 KiB
TypeScript
235 lines
7.2 KiB
TypeScript
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<string>;
|
|
disabled: Computed<boolean>;
|
|
onSave(value: string): Promise<void>;
|
|
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<string>;
|
|
onSave: (value: string) => Promise<void>;
|
|
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<void>;
|
|
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<boolean>;
|
|
|
|
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;
|
|
`);
|