You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gristlabs_grist-core/app/client/aclui/ACLFormulaEditor.ts

132 lines
4.4 KiB

import {setupAceEditorCompletions} from 'app/client/components/AceEditorCompletions';
import {colors} from 'app/client/ui2018/cssVars';
import * as ace from 'brace';
import {dom, DomArg, Observable, styled} from 'grainjs';
import debounce from 'lodash/debounce';
export interface ACLFormulaOptions {
initialValue: string;
readOnly: boolean;
placeholder: DomArg;
setValue: (value: string) => void;
getSuggestions: (prefix: string) => string[];
customiseEditor?: (editor: ace.Editor) => void;
}
export function aclFormulaEditor(options: ACLFormulaOptions) {
// Create an element and an editor within it.
const editorElem = dom('div');
const editor: ace.Editor = ace.edit(editorElem);
// Set various editor options.
editor.setTheme('ace/theme/chrome');
// ACE editor resizes automatically when maxLines is set.
editor.setOptions({enableLiveAutocompletion: true, maxLines: 10});
editor.renderer.setShowGutter(false); // Default line numbers to hidden
editor.renderer.setPadding(5);
editor.renderer.setScrollMargin(4, 4, 0, 0);
editor.$blockScrolling = Infinity;
editor.setReadOnly(options.readOnly);
editor.setFontSize('12');
editor.setHighlightActiveLine(false);
const session = editor.getSession();
session.setMode('ace/mode/python');
session.setTabSize(2);
session.setUseWrapMode(false);
// Implement placeholder text since the version of ACE we use doesn't support one.
const showPlaceholder = Observable.create(null, !options.initialValue.length);
editor.renderer.scroller.appendChild(
cssAcePlaceholder(dom.show(showPlaceholder), options.placeholder)
);
editor.on("change", () => showPlaceholder.set(!editor.getValue().length));
async function getSuggestions(prefix: string): Promise<Array<[string, null]>> {
return [
// The few Python keywords and constants we support.
'and', 'or', 'not', 'in', 'is', 'True', 'False', 'None',
// Some grist-specific constants:
'OWNER', 'EDITOR', 'VIEWER',
// The common variables.
'user', 'rec', 'newRec',
// Other completions that depend on doc schema or other rules.
...options.getSuggestions(prefix),
].map(suggestion => [suggestion, null]); // null means no example value
}
setupAceEditorCompletions(editor, {getSuggestions});
// Save on blur.
editor.on("blur", () => options.setValue(editor.getValue()));
// Save changes every 1 second
const save = debounce(() => options.setValue(editor.getValue()), 1000);
editor.on("change", save);
// Blur (and save) on Enter key.
editor.commands.addCommand({
name: 'onEnter',
bindKey: {win: 'Enter', mac: 'Enter'},
exec: () => editor.blur(),
});
// Disable Tab/Shift+Tab commands to restore their regular behavior.
(editor.commands as any).removeCommands(['indent', 'outdent']);
// Set the editor's initial value.
editor.setValue(options.initialValue);
if (options.customiseEditor) {
options.customiseEditor(editor);
}
return cssConditionInputAce(
cssConditionInputAce.cls('-disabled', options.readOnly),
// ACE editor calls preventDefault on clicks into the scrollbar area, which prevents focus
// being set when the click happens to be into there. To ensure we can focus on such clicks
// anyway, listen to the mousedown event in the capture phase.
dom.on('mousedown', () => { editor.focus(); }, {useCapture: true}),
dom.onDispose(() => editor.destroy()),
dom.onDispose(() => save.cancel()),
editorElem,
);
}
const cssConditionInputAce = styled('div', `
width: 100%;
min-height: 28px;
padding: 1px;
border-radius: 3px;
border: 1px solid transparent;
cursor: pointer;
&:hover {
border: 1px solid ${colors.darkGrey};
}
&:not(&-disabled):focus-within {
box-shadow: inset 0 0 0 1px ${colors.cursor};
border-color: ${colors.cursor};
}
&:not(:focus-within) .ace_scroller, &-disabled .ace_scroller {
cursor: unset;
}
&-disabled, &-disabled:hover {
background-color: ${colors.mediumGreyOpaque};
box-shadow: unset;
border-color: transparent;
}
&-disabled .ace-chrome {
background-color: ${colors.mediumGreyOpaque};
}
& .ace_marker-layer, & .ace_cursor-layer {
display: none;
}
&:not(&-disabled) .ace_focus .ace_marker-layer, &:not(&-disabled) .ace_focus .ace_cursor-layer {
display: block;
}
`);
const cssAcePlaceholder = styled('div', `
padding: 4px 5px;
opacity: 0.5;
`);