Threads readOnly through to EditorState.readOnly + EditorView.editable so script-detail can render a viewer-only editor without intercepting keystrokes upstream. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
3.5 KiB
Svelte
120 lines
3.5 KiB
Svelte
<!--
|
|
CodeMirror-backed text editor for the dashboard.
|
|
|
|
Replaces our plain <textarea> with line numbers, syntax highlighting,
|
|
bracket matching, search/replace (Ctrl+F), and language-aware
|
|
autocomplete. Two-way bound via `value`; the parent treats it the
|
|
same as a textarea.
|
|
|
|
Languages: `rhai` (custom mode, ./rhai-mode.ts) and `json`
|
|
(@codemirror/lang-json).
|
|
-->
|
|
<script lang="ts">
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { basicSetup } from 'codemirror';
|
|
import { EditorView, keymap, placeholder as cmPlaceholder } from '@codemirror/view';
|
|
import { EditorState } from '@codemirror/state';
|
|
import { indentWithTab } from '@codemirror/commands';
|
|
import { json as jsonLang } from '@codemirror/lang-json';
|
|
import { rhai as rhaiLang } from './rhai-mode';
|
|
import { dashboardSyntaxHighlighting, dashboardTheme } from './editor-theme';
|
|
|
|
type Language = 'rhai' | 'json';
|
|
|
|
let {
|
|
value = $bindable(''),
|
|
language = 'rhai' as Language,
|
|
placeholder = '',
|
|
minHeight = '12rem',
|
|
readOnly = false
|
|
}: {
|
|
value?: string;
|
|
language?: Language;
|
|
placeholder?: string;
|
|
minHeight?: string;
|
|
/** When true the editor renders without a cursor and rejects
|
|
* keystrokes. Parent-driven `value` changes still apply via
|
|
* the dispatch path below — this only blocks user edits.
|
|
* Not reactive after mount; re-mount via `{#key}` if needed. */
|
|
readOnly?: boolean;
|
|
} = $props();
|
|
|
|
let host: HTMLDivElement | null = null;
|
|
let view: EditorView | null = null;
|
|
// Guard against the update-listener firing while we're pushing an
|
|
// external value into the editor — without this, parent-driven
|
|
// `value` changes would echo back through the listener and create a
|
|
// dispatch loop in Svelte 5 reactivity.
|
|
let pushingFromOutside = false;
|
|
|
|
function buildExtensions(lang: Language) {
|
|
const extensions = [
|
|
basicSetup,
|
|
lang === 'json' ? jsonLang() : rhaiLang(),
|
|
keymap.of([indentWithTab]),
|
|
dashboardSyntaxHighlighting,
|
|
dashboardTheme,
|
|
// readOnly + editable together: readOnly blocks the
|
|
// underlying transactions, editable suppresses the caret
|
|
// + selection visuals so the user can see it's not
|
|
// editable.
|
|
EditorState.readOnly.of(readOnly),
|
|
EditorView.editable.of(!readOnly),
|
|
EditorView.updateListener.of((update) => {
|
|
if (update.docChanged && !pushingFromOutside) {
|
|
value = update.state.doc.toString();
|
|
}
|
|
})
|
|
];
|
|
if (placeholder) extensions.push(cmPlaceholder(placeholder));
|
|
return extensions;
|
|
}
|
|
|
|
onMount(() => {
|
|
if (!host) return;
|
|
view = new EditorView({
|
|
state: EditorState.create({
|
|
doc: value,
|
|
extensions: buildExtensions(language)
|
|
}),
|
|
parent: host
|
|
});
|
|
});
|
|
|
|
onDestroy(() => {
|
|
view?.destroy();
|
|
view = null;
|
|
});
|
|
|
|
// Push parent-driven `value` updates back into the editor (e.g.
|
|
// when the script is reloaded after Save, or "Format JSON" rewrites
|
|
// the body). We only dispatch when the document genuinely differs
|
|
// from the current `value`.
|
|
$effect(() => {
|
|
if (!view) return;
|
|
const current = view.state.doc.toString();
|
|
if (current !== value) {
|
|
pushingFromOutside = true;
|
|
view.dispatch({
|
|
changes: { from: 0, to: current.length, insert: value }
|
|
});
|
|
pushingFromOutside = false;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<div bind:this={host} class="cm-host" style:min-height={minHeight}></div>
|
|
|
|
<style>
|
|
.cm-host :global(.cm-editor) {
|
|
height: 100%;
|
|
border-radius: 0.375rem;
|
|
border: 1px solid #334155;
|
|
overflow: hidden;
|
|
}
|
|
.cm-host :global(.cm-scroller) {
|
|
font-family:
|
|
ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
|
}
|
|
</style>
|