From 56ac42767a575ded03f98a4e70d6373e3b59af7c Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:07:50 +0100 Subject: [PATCH] fix: stale placeholder decorations on empty paragraphs (#2374) --- .../features/editor/extensions/extensions.ts | 3 +- .../features/editor/extensions/placeholder.ts | 64 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 apps/client/src/features/editor/extensions/placeholder.ts diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index 672c669da..bdbd78ad8 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -3,7 +3,8 @@ import { StarterKit } from "@tiptap/starter-kit"; import { Code } from "@tiptap/extension-code"; import { TextAlign } from "@tiptap/extension-text-align"; import { TaskList, TaskItem } from "@tiptap/extension-list"; -import { Placeholder, CharacterCount, UndoRedo } from "@tiptap/extensions"; +import { CharacterCount, UndoRedo } from "@tiptap/extensions"; +import { Placeholder } from "@/features/editor/extensions/placeholder"; import { Superscript } from "@tiptap/extension-superscript"; import SubScript from "@tiptap/extension-subscript"; import { Typography } from "@tiptap/extension-typography"; diff --git a/apps/client/src/features/editor/extensions/placeholder.ts b/apps/client/src/features/editor/extensions/placeholder.ts new file mode 100644 index 000000000..68827c601 --- /dev/null +++ b/apps/client/src/features/editor/extensions/placeholder.ts @@ -0,0 +1,64 @@ +import { isNodeEmpty } from "@tiptap/core"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { Decoration, DecorationSet } from "@tiptap/pm/view"; +import { Placeholder as TiptapPlaceholder } from "@tiptap/extensions"; + +export const Placeholder = TiptapPlaceholder.extend({ + addProseMirrorPlugins() { + const editor = this.editor; + const options = this.options; + const dataAttribute = `data-${options.dataAttribute || "placeholder"}`; + + return [ + new Plugin({ + key: new PluginKey("docmostPlaceholder"), + props: { + decorations: (state) => { + if (options.showOnlyWhenEditable && !editor.isEditable) { + return null; + } + + const { doc, selection } = state; + const { anchor } = selection; + const decorations: Decoration[] = []; + const isEmptyDoc = editor.isEmpty; + + doc.descendants((node, pos) => { + if (!node.type.isTextblock) { + return options.includeChildren; + } + + const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize; + const isEmpty = !node.isLeaf && isNodeEmpty(node); + + if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) { + const emptyNodeClass = + typeof options.emptyNodeClass === "function" + ? options.emptyNodeClass({ editor, node, pos, hasAnchor }) + : options.emptyNodeClass; + const classes = [emptyNodeClass]; + if (isEmptyDoc) { + classes.push(options.emptyEditorClass); + } + + decorations.push( + Decoration.node(pos, pos + node.nodeSize, { + class: classes.join(" "), + [dataAttribute]: + typeof options.placeholder === "function" + ? options.placeholder({ editor, node, pos, hasAnchor }) + : options.placeholder, + }), + ); + } + + return options.includeChildren; + }); + + return DecorationSet.create(doc, decorations); + }, + }, + }), + ]; + }, +});