Files
mermaid-live-editor/src/lib/components/MobileEditor.svelte
T
Sidharth VinodandClaude Fable 5 144b4dc190 fix: Stop mobile editor from reverting keystrokes
currentText was $state, and the validated-state sync $effect both reads
and writes it. Every keystroke (which sets currentText in the CodeMirror
updateListener) therefore re-ran the effect while re-validation was
still in flight, dispatching a full-document replacement with the stale
validatedState text — visibly reverting the keystroke and resetting the
cursor until validation caught up.

The effect only needs to react to validatedState publishes, so make
currentText a plain variable, matching DesktopEditor.

No unit test: the repo has no component-test harness, and CodeMirror
cannot mount under jsdom without one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:51:57 +05:30

101 lines
3.1 KiB
Svelte

<script lang="ts">
import type { EditorProps } from '$/types';
import { validatedState } from '$/util/state.svelte';
import { json, jsonLanguage } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
import { yamlFrontmatter } from '@codemirror/lang-yaml';
import { language } from '@codemirror/language';
import { Compartment, EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { vsCodeDark } from '@fsegurai/codemirror-theme-vscode-dark';
import { vsCodeLight } from '@fsegurai/codemirror-theme-vscode-light';
import { basicSetup } from 'codemirror';
import { mode } from 'mode-watcher';
import { onMount } from 'svelte';
let editorView: EditorView | undefined;
let editorContainer: HTMLDivElement;
// Deliberately not $state: the sync effect below both reads and writes it,
// so a reactive currentText would make every keystroke re-run the effect
// against the not-yet-revalidated state and revert the user's input.
let currentText = '';
const themeCompartment = new Compartment();
const languageCompartment = new Compartment();
const { onUpdate }: EditorProps = $props();
$effect(() => {
editorView?.dispatch({
effects: themeCompartment.reconfigure(mode.current === 'dark' ? vsCodeDark : vsCodeLight)
});
});
onMount(() => {
editorView = new EditorView({
state: EditorState.create({
doc: currentText,
extensions: [
basicSetup,
languageCompartment.of([]),
themeCompartment.of([]),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const newText = update.state.doc.toString();
if (currentText === newText) {
return;
}
currentText = newText;
onUpdate(newText);
}
}),
EditorView.theme({
'&.cm-focused': {
outline: 'none'
},
'&.cm-editor': {
height: '100%'
},
'&.cm-scroller': {
overflow: 'auto'
}
})
]
}),
parent: editorContainer
});
return () => {
editorView?.destroy();
};
});
$effect(() => {
const { editorMode, code, mermaid } = validatedState.current;
const text = editorMode === 'code' ? code : mermaid;
if (currentText === text || !editorView) {
return;
}
currentText = text;
editorView.dispatch({
changes: {
from: 0,
to: editorView.state.doc.length,
insert: text
}
});
const stateLanguage = editorView.state.facet(language);
const isStateJson = stateLanguage === jsonLanguage;
const isCodeJson = editorMode === 'config';
if (stateLanguage && isStateJson === isCodeJson) {
return;
}
editorView.dispatch({
effects: languageCompartment.reconfigure(
isCodeJson ? json() : yamlFrontmatter({ content: markdown() })
)
});
});
</script>
<div bind:this={editorContainer} class="size-full"></div>