diff --git a/packages/ui/src/RichEditor/PlaceholderExtension.ts b/packages/ui/src/RichEditor/PlaceholderExtension.ts index 9c2fe3c66..047fe92b5 100644 --- a/packages/ui/src/RichEditor/PlaceholderExtension.ts +++ b/packages/ui/src/RichEditor/PlaceholderExtension.ts @@ -3,49 +3,91 @@ // that can be found in the LICENSE file. import { Extension } from "@tiptap/core"; -import { Plugin, PluginKey } from "@tiptap/pm/state"; -import { Decoration, DecorationSet } from "@tiptap/pm/view"; +import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; +import { Decoration, DecorationSet, type EditorView } from "@tiptap/pm/view"; const placeholderKey = new PluginKey("placeholder"); +function computeDecorations( + state: EditorState, + editable: boolean, +): DecorationSet { + if (!editable) return DecorationSet.empty; + + const { selection } = state; + if (!selection.empty) return DecorationSet.empty; + + const $pos = selection.$from; + const node = $pos.parent; + + if (node.type.name !== "paragraph") return DecorationSet.empty; + if (node.content.size !== 0) return DecorationSet.empty; + + if ($pos.depth >= 2) { + const parentName = $pos.node($pos.depth - 1).type.name; + if ( + parentName === "listItem" + || parentName === "tableCell" + || parentName === "tableHeader" + ) { + return DecorationSet.empty; + } + } + + const pos = $pos.before($pos.depth); + + return DecorationSet.create(state.doc, [ + Decoration.node(pos, pos + node.nodeSize, { + "class": "is-empty-focused", + "data-placeholder": "Write or type / for commands\u2026", + }), + ]); +} + export const PlaceholderExtension = Extension.create({ name: "placeholder", addProseMirrorPlugins() { + const { editor } = this; + return [ new Plugin({ key: placeholderKey, + view() { + let lastEditable = editor.isEditable; + return { + update(view: EditorView) { + const editable = editor.isEditable; + if (editable !== lastEditable) { + lastEditable = editable; + view.dispatch( + view.state.tr.setMeta(placeholderKey, true), + ); + } + }, + }; + }, + + state: { + init(_config, state) { + return computeDecorations(state, editor.isEditable); + }, + apply(tr, value, _oldState, newState) { + if ( + !tr.docChanged + && !tr.selectionSet + && !tr.getMeta(placeholderKey) + ) { + return value; + } + return computeDecorations(newState, editor.isEditable); + }, + }, + props: { decorations(state) { - const { selection } = state; - if (!selection.empty) return DecorationSet.empty; - - const $pos = selection.$from; - const node = $pos.parent; - - if (node.type.name !== "paragraph") return DecorationSet.empty; - if (node.content.size !== 0) return DecorationSet.empty; - - if ($pos.depth >= 2) { - const parentName = $pos.node($pos.depth - 1).type.name; - if ( - parentName === "listItem" - || parentName === "tableCell" - || parentName === "tableHeader" - ) { - return DecorationSet.empty; - } - } - - const pos = $pos.before($pos.depth); - - return DecorationSet.create(state.doc, [ - Decoration.node(pos, pos + node.nodeSize, { - "class": "is-empty-focused", - "data-placeholder": "Write or type / for commands\u2026", - }), - ]); + return placeholderKey.getState(state) as DecorationSet; }, }, }),