Add reusable read-only CodeMirror source viewer
Quality / quality (ubuntu-latest) (push) Failing after 2s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-09-05 21:41:59 +05:30
1 parent 9f2b5bb5d9
commit 01abe351e9
4 files changed
+90 -6

No files matched your search

+8 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/block-editor",
"version": "0.1.12",
"version": "0.1.15",
"type": "module",
"description": "Hydrated accessible editor runtime for WRNexus block documents.",
"exports": {
@@ -16,9 +16,15 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@codemirror/lang-xml": "6.1.0",
"@codemirror/language": "6.12.4",
"@codemirror/state": "6.7.4",
"@codemirror/view": "6.43.11",
"@lezer/highlight": "1.2.3",
"@wrnexus/blocks": "workspace:*",
"@wrnexus/dnd": "workspace:*",
"@wrnexus/reactive": "workspace:*"
"@wrnexus/reactive": "workspace:*",
"codemirror": "6.0.2"
},
"devDependencies": {
"@types/bun": "^1.4.0",
+1
View File
@@ -9,3 +9,4 @@ export * from "./download.ts";
export * from "./image-preview.ts";
export * from "./announcements.ts";
export * from "./kit.ts";
export * from "./source-editor.ts";
@@ -0,0 +1,37 @@
/** Read-only derived output. Loading this entry does not load CodeMirror until opened. */
export async function mountBuilderSourceEditor(parent: HTMLElement, language: "text" | "xml", content: string) {
const [{ EditorView, basicSetup }, { EditorState, Compartment }, { xml }, { HighlightStyle, syntaxHighlighting }, { tags }] = await Promise.all([
import("codemirror"), import("@codemirror/state"), import("@codemirror/lang-xml"),
import("@codemirror/language"), import("@lezer/highlight"),
]);
if (!parent.isConnected) return null;
const stopShortcut = (event: KeyboardEvent) => event.stopPropagation();
parent.addEventListener("keydown", stopShortcut);
const wrapping = new Compartment();
const view = new EditorView({ parent, doc: content, extensions: [
basicSetup, EditorState.readOnly.of(true), EditorView.editable.of(false),
EditorView.contentAttributes.of({ tabindex: "0", "aria-label": "Generated email source" }),
language === "xml" ? xml() : [], wrapping.of([]),
syntaxHighlighting(HighlightStyle.define([
{ tag: tags.tagName, color: "#7dd3fc" }, { tag: tags.attributeName, color: "#c4b5fd" },
{ tag: tags.attributeValue, color: "#a7f3d0" }, { tag: tags.angleBracket, color: "#94a3b8" },
{ tag: tags.comment, color: "#94a3b8" },
])),
EditorView.theme({
"&": { height: "100%", fontSize: "12px", backgroundColor: "#111827", color: "#e5e7eb" },
".cm-scroller": { overflow: "auto", fontFamily: "ui-monospace, Consolas, monospace" },
".cm-gutters": { backgroundColor: "#111827", color: "#94a3b8", borderColor: "#334155" },
".cm-activeLine, .cm-activeLineGutter": { backgroundColor: "transparent" },
"&.cm-focused": { outline: "2px solid var(--wrn-color-primary)", outlineOffset: "-2px" },
}),
] });
let destroyed = false;
const observer = new MutationObserver(() => { if (!parent.isConnected) destroy(); });
function destroy() { if (destroyed) return; destroyed = true; observer.disconnect(); parent.removeEventListener("keydown", stopShortcut); view.destroy(); }
observer.observe(parent.ownerDocument.documentElement, { childList: true, subtree: true });
return {
update(value: string) { if (!destroyed && view.state.doc.toString() !== value) view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } }); },
wrap(enabled: boolean) { if (!destroyed) view.dispatch({ effects: wrapping.reconfigure(enabled ? EditorView.lineWrapping : []) }); },
destroy,
};
}