gristlabs_grist-core/app/client/aclui/ACLFormulaEditor.ts

128 lines
3.9 KiB
TypeScript
Raw Normal View History

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';
export interface ACLFormulaOptions {
initialValue: string;
readOnly: boolean;
placeholder: DomArg;
setValue: (value: string) => void;
getSuggestions: (prefix: string) => string[];
}
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');
editor.setOptions({enableLiveAutocompletion: true});
editor.renderer.setShowGutter(false); // Default line numbers to hidden
editor.renderer.setPadding(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) {
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',
// Other completions that depend on doc schema or other rules.
...options.getSuggestions(prefix),
];
}
setupAceEditorCompletions(editor, {getSuggestions});
// Save on blur.
editor.on("blur", () => options.setValue(editor.getValue()));
// 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']);
function resize() {
if (editor.renderer.lineHeight === 0) {
// Reschedule the resize, since it's not ready yet. Seems to happen occasionally.
setTimeout(resize, 50);
}
editorElem.style.width = 'auto';
editorElem.style.height = (Math.max(1, session.getScreenLength()) * editor.renderer.lineHeight) + 'px';
editor.resize();
}
// Set the editor's initial value.
editor.setValue(options.initialValue);
// Resize the editor on change, and initially once it's attached to the page.
editor.on('change', resize);
setTimeout(resize, 0);
return cssConditionInputAce(
cssConditionInputAce.cls('-disabled', options.readOnly),
dom.onDispose(() => editor.destroy()),
editorElem,
);
}
const cssConditionInputAce = styled('div', `
width: 100%;
min-height: 28px;
padding: 5px 6px 5px 6px;
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', `
opacity: 0.5;
`);