2024-03-15 10:40:37 -07:00
|
|
|
import {
|
|
|
|
computed,
|
|
|
|
onBeforeUnmount,
|
2024-05-28 07:58:44 -07:00
|
|
|
onMounted,
|
2024-03-15 10:40:37 -07:00
|
|
|
ref,
|
|
|
|
toValue,
|
|
|
|
watch,
|
2024-05-28 07:58:44 -07:00
|
|
|
watchEffect,
|
|
|
|
type MaybeRefOrGetter,
|
|
|
|
type Ref,
|
2024-03-15 10:40:37 -07:00
|
|
|
} from 'vue';
|
|
|
|
|
|
|
|
import { ensureSyntaxTree } from '@codemirror/language';
|
|
|
|
import type { IDataObject } from 'n8n-workflow';
|
|
|
|
import { Expression, ExpressionExtensions } from 'n8n-workflow';
|
|
|
|
|
|
|
|
import { EXPRESSION_EDITOR_PARSER_TIMEOUT } from '@/constants';
|
|
|
|
import { useNDVStore } from '@/stores/ndv.store';
|
|
|
|
|
|
|
|
import type { TargetItem } from '@/Interface';
|
|
|
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
2024-05-28 07:58:44 -07:00
|
|
|
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
|
|
|
import { closeCursorInfoBox } from '@/plugins/codemirror/tooltips/InfoBoxTooltip';
|
2024-03-15 10:40:37 -07:00
|
|
|
import type { Html, Plaintext, RawSegment, Resolvable, Segment } from '@/types/expressions';
|
|
|
|
import {
|
|
|
|
getExpressionErrorMessage,
|
|
|
|
getResolvableState,
|
|
|
|
isEmptyExpression,
|
|
|
|
} from '@/utils/expressions';
|
2024-05-10 05:39:06 -07:00
|
|
|
import { closeCompletion, completionStatus } from '@codemirror/autocomplete';
|
2024-03-26 07:23:30 -07:00
|
|
|
import {
|
|
|
|
Compartment,
|
2024-05-28 07:58:44 -07:00
|
|
|
EditorSelection,
|
2024-03-26 07:23:30 -07:00
|
|
|
EditorState,
|
|
|
|
type Extension,
|
2024-05-28 07:58:44 -07:00
|
|
|
type SelectionRange,
|
2024-03-26 07:23:30 -07:00
|
|
|
} from '@codemirror/state';
|
2024-03-15 10:40:37 -07:00
|
|
|
import { EditorView, type ViewUpdate } from '@codemirror/view';
|
|
|
|
import { debounce, isEqual } from 'lodash-es';
|
|
|
|
import { useRouter } from 'vue-router';
|
|
|
|
import { useI18n } from '../composables/useI18n';
|
|
|
|
import { useWorkflowsStore } from '../stores/workflows.store';
|
|
|
|
import { useAutocompleteTelemetry } from './useAutocompleteTelemetry';
|
|
|
|
|
|
|
|
export const useExpressionEditor = ({
|
|
|
|
editorRef,
|
|
|
|
editorValue,
|
|
|
|
extensions = [],
|
|
|
|
additionalData = {},
|
|
|
|
skipSegments = [],
|
|
|
|
autocompleteTelemetry,
|
|
|
|
isReadOnly = false,
|
|
|
|
}: {
|
|
|
|
editorRef: Ref<HTMLElement | undefined>;
|
|
|
|
editorValue?: MaybeRefOrGetter<string>;
|
|
|
|
extensions?: MaybeRefOrGetter<Extension[]>;
|
|
|
|
additionalData?: MaybeRefOrGetter<IDataObject>;
|
|
|
|
skipSegments?: MaybeRefOrGetter<string[]>;
|
|
|
|
autocompleteTelemetry?: MaybeRefOrGetter<{ enabled: true; parameterPath: string }>;
|
|
|
|
isReadOnly?: MaybeRefOrGetter<boolean>;
|
|
|
|
}) => {
|
|
|
|
const ndvStore = useNDVStore();
|
|
|
|
const workflowsStore = useWorkflowsStore();
|
|
|
|
const router = useRouter();
|
|
|
|
const workflowHelpers = useWorkflowHelpers({ router });
|
|
|
|
const i18n = useI18n();
|
|
|
|
const editor = ref<EditorView>();
|
|
|
|
const hasFocus = ref(false);
|
|
|
|
const segments = ref<Segment[]>([]);
|
2024-03-26 07:23:30 -07:00
|
|
|
const selection = ref<SelectionRange>(EditorSelection.cursor(0)) as Ref<SelectionRange>;
|
2024-03-15 10:40:37 -07:00
|
|
|
const customExtensions = ref<Compartment>(new Compartment());
|
|
|
|
const readOnlyExtensions = ref<Compartment>(new Compartment());
|
|
|
|
const telemetryExtensions = ref<Compartment>(new Compartment());
|
|
|
|
const autocompleteStatus = ref<'pending' | 'active' | null>(null);
|
2024-05-29 05:37:05 -07:00
|
|
|
const dragging = ref(false);
|
2024-03-15 10:40:37 -07:00
|
|
|
|
|
|
|
const updateSegments = (): void => {
|
|
|
|
const state = editor.value?.state;
|
|
|
|
if (!state) return;
|
|
|
|
|
|
|
|
const rawSegments: RawSegment[] = [];
|
|
|
|
|
|
|
|
const fullTree = ensureSyntaxTree(state, state.doc.length, EXPRESSION_EDITOR_PARSER_TIMEOUT);
|
|
|
|
|
|
|
|
if (fullTree === null) return;
|
|
|
|
|
|
|
|
const skip = ['Program', 'Script', 'Document', ...toValue(skipSegments)];
|
|
|
|
|
|
|
|
fullTree.cursor().iterate((node) => {
|
|
|
|
const text = state.sliceDoc(node.from, node.to);
|
|
|
|
|
|
|
|
if (skip.includes(node.type.name)) return;
|
|
|
|
|
|
|
|
const newSegment: RawSegment = {
|
|
|
|
from: node.from,
|
|
|
|
to: node.to,
|
|
|
|
text,
|
|
|
|
token: node.type.name === 'Resolvable' ? 'Resolvable' : 'Plaintext',
|
|
|
|
};
|
|
|
|
|
|
|
|
// Avoid duplicates
|
|
|
|
if (isEqual(newSegment, rawSegments.at(-1))) return;
|
|
|
|
|
|
|
|
rawSegments.push(newSegment);
|
|
|
|
});
|
|
|
|
|
|
|
|
segments.value = rawSegments.reduce<Segment[]>((acc, segment) => {
|
|
|
|
const { from, to, text, token } = segment;
|
|
|
|
|
|
|
|
if (token === 'Resolvable') {
|
2024-05-09 05:45:31 -07:00
|
|
|
const { resolved, error, fullError } = resolve(text, targetItem.value);
|
2024-03-15 10:40:37 -07:00
|
|
|
acc.push({
|
|
|
|
kind: 'resolvable',
|
|
|
|
from,
|
|
|
|
to,
|
|
|
|
resolvable: text,
|
|
|
|
// TODO:
|
|
|
|
// For some reason, expressions that resolve to a number 0 are breaking preview in the SQL editor
|
|
|
|
// This fixes that but as as TODO we should figure out why this is happening
|
|
|
|
resolved: String(resolved),
|
2024-03-26 07:23:30 -07:00
|
|
|
state: getResolvableState(fullError ?? error, autocompleteStatus.value !== null),
|
2024-03-15 10:40:37 -07:00
|
|
|
error: fullError,
|
|
|
|
});
|
|
|
|
|
|
|
|
return acc;
|
|
|
|
}
|
|
|
|
|
|
|
|
acc.push({ kind: 'plaintext', from, to, plaintext: text });
|
|
|
|
|
|
|
|
return acc;
|
|
|
|
}, []);
|
|
|
|
};
|
|
|
|
|
|
|
|
function readEditorValue(): string {
|
|
|
|
return editor.value?.state.doc.toString() ?? '';
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateHighlighting(): void {
|
|
|
|
if (!editor.value) return;
|
|
|
|
highlighter.removeColor(editor.value, plaintextSegments.value);
|
|
|
|
highlighter.addColor(editor.value, resolvableSegments.value);
|
|
|
|
}
|
|
|
|
|
2024-03-26 07:23:30 -07:00
|
|
|
function updateSelection(viewUpdate: ViewUpdate) {
|
|
|
|
const currentSelection = selection.value;
|
|
|
|
const newSelection = viewUpdate.state.selection.ranges[0];
|
|
|
|
|
|
|
|
if (!currentSelection?.eq(newSelection)) {
|
|
|
|
selection.value = newSelection;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-15 10:40:37 -07:00
|
|
|
const debouncedUpdateSegments = debounce(updateSegments, 200);
|
|
|
|
|
2024-03-26 07:23:30 -07:00
|
|
|
function onEditorUpdate(viewUpdate: ViewUpdate) {
|
2024-03-15 10:40:37 -07:00
|
|
|
autocompleteStatus.value = completionStatus(viewUpdate.view.state);
|
2024-03-26 07:23:30 -07:00
|
|
|
updateSelection(viewUpdate);
|
|
|
|
|
|
|
|
if (!viewUpdate.docChanged) return;
|
2024-03-15 10:40:37 -07:00
|
|
|
|
|
|
|
debouncedUpdateSegments();
|
|
|
|
}
|
|
|
|
|
2024-05-10 05:39:06 -07:00
|
|
|
function blur() {
|
|
|
|
if (editor.value) {
|
|
|
|
editor.value.contentDOM.blur();
|
|
|
|
closeCompletion(editor.value);
|
2024-05-28 07:58:44 -07:00
|
|
|
closeCursorInfoBox(editor.value);
|
2024-05-10 05:39:06 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function blurOnClickOutside(event: MouseEvent) {
|
2024-05-29 05:37:05 -07:00
|
|
|
if (event.target && !dragging.value && !editor.value?.dom.contains(event.target as Node)) {
|
2024-05-10 05:39:06 -07:00
|
|
|
blur();
|
|
|
|
}
|
2024-05-29 05:37:05 -07:00
|
|
|
dragging.value = false;
|
2024-05-10 05:39:06 -07:00
|
|
|
}
|
|
|
|
|
2024-03-15 10:40:37 -07:00
|
|
|
watch(editorRef, () => {
|
|
|
|
const parent = toValue(editorRef);
|
|
|
|
|
|
|
|
if (!parent) return;
|
|
|
|
|
|
|
|
const state = EditorState.create({
|
|
|
|
doc: toValue(editorValue),
|
|
|
|
extensions: [
|
|
|
|
customExtensions.value.of(toValue(extensions)),
|
2024-06-27 08:07:29 -07:00
|
|
|
readOnlyExtensions.value.of([EditorState.readOnly.of(toValue(isReadOnly))]),
|
2024-03-15 10:40:37 -07:00
|
|
|
telemetryExtensions.value.of([]),
|
|
|
|
EditorView.updateListener.of(onEditorUpdate),
|
|
|
|
EditorView.focusChangeEffect.of((_, newHasFocus) => {
|
|
|
|
hasFocus.value = newHasFocus;
|
2024-03-26 07:23:30 -07:00
|
|
|
selection.value = state.selection.ranges[0];
|
2024-05-10 05:39:06 -07:00
|
|
|
if (!newHasFocus) {
|
|
|
|
autocompleteStatus.value = null;
|
|
|
|
debouncedUpdateSegments();
|
|
|
|
}
|
2024-03-15 10:40:37 -07:00
|
|
|
return null;
|
|
|
|
}),
|
|
|
|
EditorView.contentAttributes.of({ 'data-gramm': 'false' }), // disable grammarly
|
2024-05-29 05:37:05 -07:00
|
|
|
EditorView.domEventHandlers({
|
|
|
|
mousedown: () => {
|
|
|
|
dragging.value = true;
|
|
|
|
},
|
|
|
|
}),
|
2024-03-15 10:40:37 -07:00
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
if (editor.value) {
|
|
|
|
editor.value.destroy();
|
|
|
|
}
|
2024-10-21 04:34:57 -07:00
|
|
|
editor.value = new EditorView({ parent, state });
|
2024-03-15 10:40:37 -07:00
|
|
|
debouncedUpdateSegments();
|
|
|
|
});
|
|
|
|
|
|
|
|
watchEffect(() => {
|
|
|
|
if (editor.value) {
|
|
|
|
editor.value.dispatch({
|
|
|
|
effects: customExtensions.value.reconfigure(toValue(extensions)),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
watchEffect(() => {
|
|
|
|
if (editor.value) {
|
|
|
|
editor.value.dispatch({
|
|
|
|
effects: readOnlyExtensions.value.reconfigure([
|
|
|
|
EditorState.readOnly.of(toValue(isReadOnly)),
|
|
|
|
]),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
watchEffect(() => {
|
|
|
|
if (!editor.value) return;
|
|
|
|
|
|
|
|
const newValue = toValue(editorValue);
|
|
|
|
const currentValue = readEditorValue();
|
|
|
|
if (newValue === undefined || newValue === currentValue) return;
|
|
|
|
|
|
|
|
editor.value.dispatch({
|
|
|
|
changes: { from: 0, to: currentValue.length, insert: newValue },
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
watchEffect(() => {
|
|
|
|
const telemetry = toValue(autocompleteTelemetry);
|
|
|
|
if (!telemetry?.enabled) return;
|
|
|
|
|
|
|
|
useAutocompleteTelemetry({
|
|
|
|
editor,
|
|
|
|
parameterPath: telemetry.parameterPath,
|
|
|
|
compartment: telemetryExtensions,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2024-05-10 05:39:06 -07:00
|
|
|
onMounted(() => {
|
|
|
|
document.addEventListener('click', blurOnClickOutside);
|
|
|
|
});
|
|
|
|
|
2024-03-15 10:40:37 -07:00
|
|
|
onBeforeUnmount(() => {
|
2024-05-10 05:39:06 -07:00
|
|
|
document.removeEventListener('click', blurOnClickOutside);
|
2024-03-15 10:40:37 -07:00
|
|
|
editor.value?.destroy();
|
|
|
|
});
|
|
|
|
|
|
|
|
const expressionExtensionNames = computed<Set<string>>(() => {
|
|
|
|
return new Set(
|
|
|
|
ExpressionExtensions.reduce<string[]>((acc, cur) => {
|
|
|
|
return [...acc, ...Object.keys(cur.functions)];
|
|
|
|
}, []),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
function isUncalledExpressionExtension(resolvable: string) {
|
|
|
|
const end = resolvable
|
|
|
|
.replace(/^{{|}}$/g, '')
|
|
|
|
.trim()
|
|
|
|
.split('.')
|
|
|
|
.pop();
|
|
|
|
|
|
|
|
return end !== undefined && expressionExtensionNames.value.has(end);
|
|
|
|
}
|
|
|
|
|
2024-05-09 05:45:31 -07:00
|
|
|
function resolve(resolvable: string, target: TargetItem | null) {
|
2024-03-15 10:40:37 -07:00
|
|
|
const result: { resolved: unknown; error: boolean; fullError: Error | null } = {
|
|
|
|
resolved: undefined,
|
|
|
|
error: false,
|
|
|
|
fullError: null,
|
|
|
|
};
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (!ndvStore.activeNode) {
|
|
|
|
// e.g. credential modal
|
|
|
|
result.resolved = Expression.resolveWithoutWorkflow(resolvable, toValue(additionalData));
|
|
|
|
} else {
|
2024-07-19 03:00:07 -07:00
|
|
|
let opts: Record<string, unknown> = { additionalKeys: toValue(additionalData) };
|
2024-03-15 10:40:37 -07:00
|
|
|
if (ndvStore.isInputParentOfActiveNode) {
|
|
|
|
opts = {
|
2024-05-09 05:45:31 -07:00
|
|
|
targetItem: target ?? undefined,
|
2024-03-15 10:40:37 -07:00
|
|
|
inputNodeName: ndvStore.ndvInputNodeName,
|
|
|
|
inputRunIndex: ndvStore.ndvInputRunIndex,
|
|
|
|
inputBranchIndex: ndvStore.ndvInputBranchIndex,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
result.resolved = workflowHelpers.resolveExpression('=' + resolvable, undefined, opts);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
2024-11-27 08:31:36 -08:00
|
|
|
const hasRunData =
|
|
|
|
!!workflowsStore.workflowExecutionData?.data?.resultData?.runData[
|
|
|
|
ndvStore.activeNode?.name ?? ''
|
|
|
|
];
|
|
|
|
result.resolved = `[${getExpressionErrorMessage(error, hasRunData)}]`;
|
2024-03-15 10:40:37 -07:00
|
|
|
result.error = true;
|
|
|
|
result.fullError = error;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (result.resolved === '') {
|
|
|
|
result.resolved = i18n.baseText('expressionModalInput.empty');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (result.resolved === undefined && isEmptyExpression(resolvable)) {
|
|
|
|
result.resolved = i18n.baseText('expressionModalInput.empty');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (result.resolved === undefined) {
|
|
|
|
result.resolved = isUncalledExpressionExtension(resolvable)
|
|
|
|
? i18n.baseText('expressionEditor.uncalledFunction')
|
|
|
|
: i18n.baseText('expressionModalInput.undefined');
|
|
|
|
|
|
|
|
result.error = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2024-07-12 01:11:59 -07:00
|
|
|
const targetItem = computed<TargetItem | null>(() => ndvStore.expressionTargetItem);
|
2024-03-15 10:40:37 -07:00
|
|
|
|
|
|
|
const resolvableSegments = computed<Resolvable[]>(() => {
|
|
|
|
return segments.value.filter((s): s is Resolvable => s.kind === 'resolvable');
|
|
|
|
});
|
|
|
|
|
|
|
|
const plaintextSegments = computed<Plaintext[]>(() => {
|
|
|
|
return segments.value.filter((s): s is Plaintext => s.kind === 'plaintext');
|
|
|
|
});
|
|
|
|
|
|
|
|
const htmlSegments = computed<Html[]>(() => {
|
|
|
|
return segments.value.filter((s): s is Html => s.kind !== 'resolvable');
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Segments to display in the output of an expression editor.
|
|
|
|
*
|
|
|
|
* Some segments are not displayed when they are _part_ of the result,
|
|
|
|
* but displayed when they are the _entire_ result:
|
|
|
|
*
|
|
|
|
* - `This is a {{ [] }} test` displays as `This is a test`.
|
|
|
|
* - `{{ [] }}` displays as `[Array: []]`.
|
|
|
|
*
|
2024-10-16 03:27:00 -07:00
|
|
|
* - `This is a {{ {} }} test` displays as `This is a [object Object] test`.
|
|
|
|
* - `{{ {} }}` displays as `[Object: {}]`.
|
|
|
|
*
|
|
|
|
* - `This is a {{ [{}] }} test` displays as `This is a [object Object] test`.
|
|
|
|
* - `{{ [] }}` displays as `[Array: []]`.
|
|
|
|
*
|
2024-03-15 10:40:37 -07:00
|
|
|
* Some segments display differently based on context:
|
|
|
|
*
|
|
|
|
* Date displays as
|
|
|
|
* - `Mon Nov 14 2022 17:26:13 GMT+0100 (CST)` when part of the result
|
|
|
|
* - `[Object: "2022-11-14T17:26:13.130Z"]` when the entire result
|
|
|
|
*
|
|
|
|
* Only needed in order to mimic behavior of `ParameterInputHint`.
|
|
|
|
*/
|
|
|
|
const displayableSegments = computed<Segment[]>(() => {
|
|
|
|
const cachedSegments = segments.value;
|
|
|
|
return cachedSegments
|
|
|
|
.map((s) => {
|
|
|
|
if (cachedSegments.length <= 1 || s.kind !== 'resolvable') return s;
|
|
|
|
|
2024-10-16 03:27:00 -07:00
|
|
|
if (typeof s.resolved === 'string') {
|
|
|
|
let resolved = s.resolved;
|
|
|
|
|
|
|
|
if (/\[Object: "\d{4}-\d{2}-\d{2}T/.test(resolved)) {
|
|
|
|
const utcDateString = resolved.replace(/(\[Object: "|\"\])/g, '');
|
|
|
|
resolved = new Date(utcDateString).toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (/\[Object:\s(\{.+\}|\{\})\]/.test(resolved)) {
|
|
|
|
resolved = resolved.replace(/(\[Object: |\]$)/g, '');
|
|
|
|
try {
|
|
|
|
resolved = String(JSON.parse(resolved));
|
|
|
|
} catch (error) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (/\[Array:\s\[.+\]\]/.test(resolved)) {
|
|
|
|
resolved = resolved.replace(/(\[Array: |\]$)/g, '');
|
|
|
|
try {
|
|
|
|
resolved = String(JSON.parse(resolved));
|
|
|
|
} catch (error) {}
|
|
|
|
}
|
2024-03-15 10:40:37 -07:00
|
|
|
|
2024-10-16 03:27:00 -07:00
|
|
|
s.resolved = resolved;
|
2024-03-15 10:40:37 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return s;
|
|
|
|
})
|
|
|
|
.filter((s) => {
|
|
|
|
if (
|
|
|
|
cachedSegments.length > 1 &&
|
|
|
|
s.kind === 'resolvable' &&
|
|
|
|
typeof s.resolved === 'string' &&
|
|
|
|
(s.resolved === '[Array: []]' ||
|
|
|
|
s.resolved === i18n.baseText('expressionModalInput.empty'))
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
watch(
|
2024-05-09 05:45:31 -07:00
|
|
|
[() => workflowsStore.getWorkflowExecution, () => workflowsStore.getWorkflowRunData],
|
2024-03-15 10:40:37 -07:00
|
|
|
debouncedUpdateSegments,
|
|
|
|
);
|
|
|
|
|
2024-05-09 05:45:31 -07:00
|
|
|
watch(targetItem, updateSegments);
|
|
|
|
|
2024-03-15 10:40:37 -07:00
|
|
|
watch(resolvableSegments, updateHighlighting);
|
|
|
|
|
|
|
|
function setCursorPosition(pos: number | 'lastExpression' | 'end'): void {
|
|
|
|
if (pos === 'lastExpression') {
|
|
|
|
const END_OF_EXPRESSION = ' }}';
|
2024-03-29 08:01:32 -07:00
|
|
|
const endOfLastExpression = readEditorValue().lastIndexOf(END_OF_EXPRESSION);
|
2024-09-17 05:10:22 -07:00
|
|
|
pos =
|
|
|
|
endOfLastExpression !== -1 ? endOfLastExpression : (editor.value?.state.doc.length ?? 0);
|
2024-03-15 10:40:37 -07:00
|
|
|
} else if (pos === 'end') {
|
|
|
|
pos = editor.value?.state.doc.length ?? 0;
|
|
|
|
}
|
|
|
|
editor.value?.dispatch({ selection: { head: pos, anchor: pos } });
|
|
|
|
}
|
|
|
|
|
|
|
|
function select(anchor: number, head: number | 'end' = 'end'): void {
|
|
|
|
editor.value?.dispatch({
|
2024-09-17 05:10:22 -07:00
|
|
|
selection: { anchor, head: head === 'end' ? (editor.value?.state.doc.length ?? 0) : head },
|
2024-03-15 10:40:37 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const selectAll = () => select(0, 'end');
|
|
|
|
|
|
|
|
function focus(): void {
|
|
|
|
if (hasFocus.value) return;
|
|
|
|
editor.value?.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
editor,
|
|
|
|
hasFocus,
|
2024-03-26 07:23:30 -07:00
|
|
|
selection,
|
2024-03-15 10:40:37 -07:00
|
|
|
segments: {
|
|
|
|
all: segments,
|
|
|
|
html: htmlSegments,
|
|
|
|
display: displayableSegments,
|
|
|
|
plaintext: plaintextSegments,
|
|
|
|
resolvable: resolvableSegments,
|
|
|
|
},
|
|
|
|
readEditorValue,
|
|
|
|
setCursorPosition,
|
|
|
|
select,
|
|
|
|
selectAll,
|
|
|
|
focus,
|
|
|
|
};
|
|
|
|
};
|