feat(editor): Refactor expression editors and mixins to composition API (#8894)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Elias Meire
2024-03-15 18:40:37 +01:00
committed by GitHub
parent a10120f74e
commit 0c179e4e51
19 changed files with 1661 additions and 1126 deletions

View File

@@ -2,159 +2,142 @@
<div ref="root" :class="$style.editor" @keydown.stop></div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { EditorView, keymap } from '@codemirror/view';
import { EditorState, Prec } from '@codemirror/state';
<script setup lang="ts">
import { history } from '@codemirror/commands';
import { Prec } from '@codemirror/state';
import { EditorView, keymap } from '@codemirror/view';
import { computed, onMounted, ref, toValue, watch } from 'vue';
import { expressionManager } from '@/mixins/expressionManager';
import { completionManager } from '@/mixins/completionManager';
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
import { n8nAutocompletion, n8nLang } from '@/plugins/codemirror/n8nLang';
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
import { inputTheme } from './theme';
import { forceParse } from '@/utils/forceParse';
import { completionStatus } from '@codemirror/autocomplete';
import { inputTheme } from './theme';
import type { IVariableItemSelected } from '@/Interface';
import { useExpressionEditor } from '@/composables/useExpressionEditor';
import {
autocompleteKeyMap,
enterKeyMap,
historyKeyMap,
tabKeyMap,
} from '@/plugins/codemirror/keymap';
import type { Segment } from '@/types/expressions';
import { removeExpressionPrefix } from '@/utils/expressions';
export default defineComponent({
name: 'ExpressionEditorModalInput',
mixins: [expressionManager, completionManager],
props: {
modelValue: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
isReadOnly: {
type: Boolean,
default: false,
},
},
data() {
return {
editor: null as EditorView | null,
};
},
mounted() {
const extensions = [
inputTheme(),
Prec.highest(
keymap.of([
...tabKeyMap(),
...historyKeyMap,
...enterKeyMap,
...autocompleteKeyMap,
{
any: (view, event) => {
if (event.key === 'Escape' && completionStatus(view.state) === null) {
event.stopPropagation();
this.$emit('close');
}
type Props = {
modelValue: string;
path: string;
isReadOnly?: boolean;
};
return false;
},
},
]),
),
n8nLang(),
n8nAutocompletion(),
history(),
expressionInputHandler(),
EditorView.lineWrapping,
EditorView.editable.of(!this.isReadOnly),
EditorState.readOnly.of(this.isReadOnly),
EditorView.contentAttributes.of({ 'data-gramm': 'false' }), // disable grammarly
EditorView.domEventHandlers({ scroll: forceParse }),
EditorView.updateListener.of((viewUpdate) => {
if (!this.editor) return;
this.completionStatus = completionStatus(viewUpdate.view.state);
if (!viewUpdate.docChanged) return;
this.editorState = this.editor.state;
highlighter.removeColor(this.editor, this.plaintextSegments);
highlighter.addColor(this.editor, this.resolvableSegments);
setTimeout(() => {
this.editor?.focus(); // prevent blur on paste
try {
this.trackCompletion(viewUpdate, this.path);
} catch {}
});
this.$emit('change', {
value: this.unresolvedExpression,
segments: this.displayableSegments,
});
}),
];
this.editor = new EditorView({
parent: this.$refs.root as HTMLDivElement,
state: EditorState.create({
doc: this.modelValue.startsWith('=') ? this.modelValue.slice(1) : this.modelValue,
extensions,
}),
});
this.editorState = this.editor.state;
this.editor.focus();
highlighter.addColor(this.editor, this.resolvableSegments);
this.editor.dispatch({
selection: { anchor: this.editor.state.doc.length },
});
this.$emit('change', {
value: this.unresolvedExpression,
segments: this.displayableSegments,
});
},
beforeUnmount() {
this.editor?.destroy();
},
methods: {
itemSelected({ variable }: IVariableItemSelected) {
if (!this.editor || this.isReadOnly) return;
const OPEN_MARKER = '{{';
const CLOSE_MARKER = '}}';
const { doc, selection } = this.editor.state;
const { head } = selection.main;
const isInsideResolvable =
doc.toString().slice(0, head).includes(OPEN_MARKER) &&
doc.toString().slice(head, doc.length).includes(CLOSE_MARKER);
const insert = isInsideResolvable
? variable
: [OPEN_MARKER, variable, CLOSE_MARKER].join(' ');
this.editor.dispatch({
changes: {
from: head,
insert,
},
});
},
},
const props = withDefaults(defineProps<Props>(), {
isReadOnly: false,
});
const emit = defineEmits<{
(event: 'change', value: { value: string; segments: Segment[] }): void;
(event: 'focus'): void;
(event: 'close'): void;
}>();
const root = ref<HTMLElement>();
const extensions = computed(() => [
inputTheme(),
Prec.highest(
keymap.of([
...tabKeyMap(),
...historyKeyMap,
...enterKeyMap,
...autocompleteKeyMap,
{
any: (view, event) => {
if (event.key === 'Escape' && completionStatus(view.state) === null) {
event.stopPropagation();
emit('close');
}
return false;
},
},
]),
),
n8nLang(),
n8nAutocompletion(),
history(),
expressionInputHandler(),
EditorView.lineWrapping,
EditorView.domEventHandlers({ scroll: forceParse }),
]);
const editorValue = ref<string>(removeExpressionPrefix(props.modelValue));
const {
editor: editorRef,
segments,
readEditorValue,
setCursorPosition,
hasFocus,
focus,
} = useExpressionEditor({
editorRef: root,
editorValue,
extensions,
isReadOnly: props.isReadOnly,
autocompleteTelemetry: { enabled: true, parameterPath: props.path },
});
watch(
() => props.modelValue,
(newValue) => {
editorValue.value = removeExpressionPrefix(newValue);
},
);
watch(segments.display, (newSegments) => {
emit('change', {
value: '=' + readEditorValue(),
segments: newSegments,
});
});
watch(hasFocus, (focused) => {
if (focused) {
emit('focus');
}
});
onMounted(() => {
focus();
});
function itemSelected({ variable }: IVariableItemSelected) {
const editor = toValue(editorRef);
if (!editor || props.isReadOnly) return;
const OPEN_MARKER = '{{';
const CLOSE_MARKER = '}}';
const { selection, doc } = editor.state;
const { head } = selection.main;
const isInsideResolvable =
editor.state.sliceDoc(0, head).includes(OPEN_MARKER) &&
editor.state.sliceDoc(head, doc.length).includes(CLOSE_MARKER);
const insert = isInsideResolvable ? variable : [OPEN_MARKER, variable, CLOSE_MARKER].join(' ');
editor.dispatch({
changes: {
from: head,
insert,
},
});
focus();
setCursorPosition(head + insert.length);
}
defineExpose({ itemSelected });
</script>
<style lang="scss" module>