|
|
@@ -120,6 +120,7 @@ const emit = defineEmits<{
|
|
|
newContent: string
|
|
|
timestamp: number
|
|
|
}]
|
|
|
+ error: [payload: { code: string; message?: string; blockId?: string }]
|
|
|
}>()
|
|
|
|
|
|
const log = createLogger("Block", props.debug)
|
|
|
@@ -206,6 +207,7 @@ const formattingHandlers: FormattingHandlers = {
|
|
|
|
|
|
const editorRef = useTemplateRef("editorRef")
|
|
|
let view: EditorView | null = null
|
|
|
+const mountError = ref(false)
|
|
|
|
|
|
const { plugin: markdownDecorationsPlugin } = createMarkdownDecorationsPlugin(
|
|
|
props.markerMode ?? "live-preview",
|
|
|
@@ -543,94 +545,107 @@ onMounted(() => {
|
|
|
},
|
|
|
}
|
|
|
|
|
|
- view = new EditorView(mountEl, {
|
|
|
- state: editorState,
|
|
|
- nodeViews: {
|
|
|
- code_block: (node, view, getPos) =>
|
|
|
- new CodeBlockView(
|
|
|
- node,
|
|
|
- view,
|
|
|
- () => getPos() ?? 0,
|
|
|
- delegate,
|
|
|
- props.debug,
|
|
|
- ),
|
|
|
- math_block: (node, view, getPos) =>
|
|
|
- new MathBlockView(node, view, () => getPos() ?? 0),
|
|
|
- ...getRegisteredNodeViews(),
|
|
|
- },
|
|
|
- handleKeyDown,
|
|
|
- handleTextInput(_view, _from, _to, text) {
|
|
|
- if (text.indexOf("\u00a0") !== -1) {
|
|
|
- _view.dispatch(
|
|
|
- _view.state.tr.insertText(text.replace(/\u00a0/g, " "), _from, _to),
|
|
|
- )
|
|
|
- return true
|
|
|
- }
|
|
|
- return false
|
|
|
- },
|
|
|
- handlePaste: createPasteHandler((before, after) =>
|
|
|
- emit("split", before, after),
|
|
|
- ),
|
|
|
- dispatchTransaction(transaction: Transaction) {
|
|
|
- const currentView = view
|
|
|
- if (!currentView) return
|
|
|
- const prevContent = docToContent(currentView.state.doc)
|
|
|
- const newState = currentView.state.apply(transaction)
|
|
|
- currentView.updateState(newState)
|
|
|
-
|
|
|
- if (transaction.docChanged && !transaction.getMeta("externalUpdate")) {
|
|
|
- const newContent = docToContent(newState.doc)
|
|
|
- log.debug("content change", {
|
|
|
- length: newContent.length,
|
|
|
- docChanged: true,
|
|
|
- })
|
|
|
- content.value = newContent
|
|
|
- emit("change", newContent)
|
|
|
- emit("content-change-op", {
|
|
|
- blockId: props.id ?? "",
|
|
|
- previousContent: prevContent,
|
|
|
- newContent,
|
|
|
- timestamp: Date.now(),
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- const { from, to, empty } = newState.selection
|
|
|
- if (from !== lastSelectionFrom || to !== lastSelectionTo) {
|
|
|
- lastSelectionFrom = from
|
|
|
- lastSelectionTo = to
|
|
|
- emit("selection-change", { from, to, empty })
|
|
|
- }
|
|
|
- },
|
|
|
- handleDOMEvents: {
|
|
|
- focus() {
|
|
|
- if (view) {
|
|
|
- view.dispatch(view.state.tr.setMeta("decorations:focus", true))
|
|
|
- emit("focus", { view, handlers: formattingHandlers })
|
|
|
- if (props.id && props.registry) {
|
|
|
- props.registry.notifyFocused(props.id)
|
|
|
- }
|
|
|
+ // Mount the ProseMirror EditorView. If this fails (e.g. invalid schema,
|
|
|
+ // missing DOM, CM6 crash during node view init), catch the error and
|
|
|
+ // fall back to a plain <textarea>.
|
|
|
+ try {
|
|
|
+ view = new EditorView(mountEl, {
|
|
|
+ state: editorState,
|
|
|
+ nodeViews: {
|
|
|
+ code_block: (node, view, getPos) =>
|
|
|
+ new CodeBlockView(
|
|
|
+ node,
|
|
|
+ view,
|
|
|
+ () => getPos() ?? 0,
|
|
|
+ delegate,
|
|
|
+ props.debug,
|
|
|
+ ),
|
|
|
+ math_block: (node, view, getPos) =>
|
|
|
+ new MathBlockView(node, view, () => getPos() ?? 0),
|
|
|
+ ...getRegisteredNodeViews(),
|
|
|
+ },
|
|
|
+ handleKeyDown,
|
|
|
+ handleTextInput(_view, _from, _to, text) {
|
|
|
+ if (text.indexOf("\u00a0") !== -1) {
|
|
|
+ _view.dispatch(
|
|
|
+ _view.state.tr.insertText(text.replace(/\u00a0/g, " "), _from, _to),
|
|
|
+ )
|
|
|
+ return true
|
|
|
}
|
|
|
return false
|
|
|
},
|
|
|
- blur() {
|
|
|
- if (view) {
|
|
|
- view.dispatch(view.state.tr.setMeta("decorations:focus", false))
|
|
|
+ handlePaste: createPasteHandler((before, after) =>
|
|
|
+ emit("split", before, after),
|
|
|
+ ),
|
|
|
+ dispatchTransaction(transaction: Transaction) {
|
|
|
+ const currentView = view
|
|
|
+ if (!currentView) return
|
|
|
+ const prevContent = docToContent(currentView.state.doc)
|
|
|
+ const newState = currentView.state.apply(transaction)
|
|
|
+ currentView.updateState(newState)
|
|
|
+
|
|
|
+ if (transaction.docChanged && !transaction.getMeta("externalUpdate")) {
|
|
|
+ const newContent = docToContent(newState.doc)
|
|
|
+ log.debug("content change", {
|
|
|
+ length: newContent.length,
|
|
|
+ docChanged: true,
|
|
|
+ })
|
|
|
+ content.value = newContent
|
|
|
+ emit("change", newContent)
|
|
|
+ emit("content-change-op", {
|
|
|
+ blockId: props.id ?? "",
|
|
|
+ previousContent: prevContent,
|
|
|
+ newContent,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const { from, to, empty } = newState.selection
|
|
|
+ if (from !== lastSelectionFrom || to !== lastSelectionTo) {
|
|
|
+ lastSelectionFrom = from
|
|
|
+ lastSelectionTo = to
|
|
|
+ emit("selection-change", { from, to, empty })
|
|
|
}
|
|
|
- emit("blur")
|
|
|
- return false
|
|
|
},
|
|
|
- },
|
|
|
- })
|
|
|
+ handleDOMEvents: {
|
|
|
+ focus() {
|
|
|
+ if (view) {
|
|
|
+ view.dispatch(view.state.tr.setMeta("decorations:focus", true))
|
|
|
+ emit("focus", { view, handlers: formattingHandlers })
|
|
|
+ if (props.id && props.registry) {
|
|
|
+ props.registry.notifyFocused(props.id)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ },
|
|
|
+ blur() {
|
|
|
+ if (view) {
|
|
|
+ view.dispatch(view.state.tr.setMeta("decorations:focus", false))
|
|
|
+ }
|
|
|
+ emit("blur")
|
|
|
+ return false
|
|
|
+ },
|
|
|
+ },
|
|
|
+ })
|
|
|
|
|
|
- if (props.focused && view) {
|
|
|
- view.dispatch(view.state.tr.setMeta("decorations:focus", true))
|
|
|
- focusAt(props.cursorPosition)
|
|
|
- }
|
|
|
+ if (props.focused && view) {
|
|
|
+ view.dispatch(view.state.tr.setMeta("decorations:focus", true))
|
|
|
+ focusAt(props.cursorPosition)
|
|
|
+ }
|
|
|
|
|
|
- if (props.id && props.registry) {
|
|
|
- props.registry.register(props.id, {
|
|
|
- focus: focusAt,
|
|
|
- element: mountEl ?? undefined,
|
|
|
+ if (props.id && props.registry) {
|
|
|
+ props.registry.register(props.id, {
|
|
|
+ focus: focusAt,
|
|
|
+ element: mountEl ?? undefined,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ log.error("mount failed", { error: e, blockId: props.id })
|
|
|
+ mountError.value = true
|
|
|
+ emit("error", {
|
|
|
+ code: "editor-mount-failed",
|
|
|
+ message: e instanceof Error ? e.message : String(e),
|
|
|
+ blockId: props.id,
|
|
|
})
|
|
|
}
|
|
|
})
|
|
|
@@ -708,7 +723,14 @@ defineExpose({
|
|
|
|
|
|
<template>
|
|
|
<div ref="menuWrapperRef" style="position: relative">
|
|
|
- <div ref="editorRef" class="w-full min-h-7 text-base"></div>
|
|
|
+ <div v-if="mountError" class="w-full min-h-7 text-base">
|
|
|
+ <textarea
|
|
|
+ v-model="content"
|
|
|
+ class="w-full min-h-7 p-1 text-sm font-mono bg-transparent border border-(--ui-error)/50 rounded-md resize-none outline-none"
|
|
|
+ :aria-label="`Block editor (mount failed)`"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div v-else ref="editorRef" class="w-full min-h-7 text-base"></div>
|
|
|
|
|
|
<EditorSuggestionMenu
|
|
|
ref="menuRef"
|