From 4285d28edc10d8591db187dada151858ec99f08e Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 15 Dec 2025 15:32:45 +0100 Subject: [PATCH 1/2] Fixed placeholder overflow issues --- packages/core/src/editor/Block.css | 8 +-- .../src/extensions/Placeholder/Placeholder.ts | 68 ++++++++++++------- packages/react/src/editor/styles.css | 2 +- 3 files changed, 47 insertions(+), 31 deletions(-) diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css index 1c21c06dc9..9a58e01369 100644 --- a/packages/core/src/editor/Block.css +++ b/packages/core/src/editor/Block.css @@ -542,15 +542,11 @@ NESTED BLOCKS } /* PLACEHOLDERS*/ -.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):before { - /*float: left; */ +/* TODO: should this be here? */ +.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):after { pointer-events: none; - height: 0; - /* width: 0; */ - position: absolute; font-style: italic; } -/* TODO: should this be here? */ /* TEXT COLORS */ [data-style-type="textColor"][data-value="gray"], diff --git a/packages/core/src/extensions/Placeholder/Placeholder.ts b/packages/core/src/extensions/Placeholder/Placeholder.ts index 795a139279..dd718f3b07 100644 --- a/packages/core/src/extensions/Placeholder/Placeholder.ts +++ b/packages/core/src/extensions/Placeholder/Placeholder.ts @@ -23,6 +23,11 @@ export const PlaceholderExtension = createExtension( new Plugin({ key: PLUGIN_KEY, view: (view) => { + view.dom.setAttribute( + "data-selection-empty", + view.state.selection.empty ? "true" : "false", + ); + const uniqueEditorSelector = `placeholder-selector-${v4()}`; view.dom.classList.add(uniqueEditorSelector); const styleEl = document.createElement("style"); @@ -40,9 +45,22 @@ export const PlaceholderExtension = createExtension( const styleSheet = styleEl.sheet!; - const getSelector = (additionalSelectors = "") => - `.${uniqueEditorSelector} .bn-block-content${additionalSelectors} .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):before`; - + const createPlaceholderRule = ( + placeholder: string | undefined, + additionalEditorSelectors = "", + additionalBlockSelectors = "", + ) => { + // Creates CSS rule to set placeholder content at the given selector. + styleSheet.insertRule( + `.${uniqueEditorSelector}${additionalEditorSelectors} .bn-block-content${additionalBlockSelectors} .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):after { content: ${JSON.stringify(placeholder)}; }`, + ); + // Creates CSS rule to hide the trailing break node while the + // placeholder is visible. This is because it's rendered as a + // `br` element, forcing the placeholder onto the next line. + styleSheet.insertRule( + `.${uniqueEditorSelector}${additionalEditorSelectors} .bn-block-content${additionalBlockSelectors} .bn-inline-content > .ProseMirror-trailingBreak:only-child { display: none; }`, + ); + }; try { // FIXME: the names "default" and "emptyDocument" are hardcoded const { @@ -53,30 +71,30 @@ export const PlaceholderExtension = createExtension( // add block specific placeholders for (const [blockType, placeholder] of Object.entries(rest)) { - const blockTypeSelector = `[data-content-type="${blockType}"]`; - - styleSheet.insertRule( - `${getSelector(blockTypeSelector)} { content: ${JSON.stringify( - placeholder, - )}; }`, + createPlaceholderRule( + placeholder, + "[data-selection-empty='true']", + `[data-content-type="${blockType}"]`, + ); + createPlaceholderRule( + placeholder, + "[data-selection-empty='false']", + `[data-content-type="${blockType}"]:not([data-is-empty-and-focused])`, ); } - const onlyBlockSelector = `[data-is-only-empty-block]`; - const mustBeFocusedSelector = `[data-is-empty-and-focused]`; - // placeholder for when there's only one empty block - styleSheet.insertRule( - `${getSelector(onlyBlockSelector)} { content: ${JSON.stringify( - emptyPlaceholder, - )}; }`, + createPlaceholderRule( + emptyPlaceholder, + "[data-selection-empty='true']", + "[data-is-only-empty-block]", ); // placeholder for default blocks, only when the cursor is in the block (mustBeFocused) - styleSheet.insertRule( - `${getSelector(mustBeFocusedSelector)} { content: ${JSON.stringify( - defaultPlaceholder, - )}; }`, + createPlaceholderRule( + defaultPlaceholder, + "[data-selection-empty='true']", + "[data-is-empty-and-focused]", ); } catch (e) { // eslint-disable-next-line no-console @@ -87,6 +105,12 @@ export const PlaceholderExtension = createExtension( } return { + update: (view) => { + view.dom.setAttribute( + "data-selection-empty", + view.state.selection.empty ? "true" : "false", + ); + }, destroy: () => { if (view.root instanceof window.ShadowRoot) { view.root.removeChild(styleEl); @@ -104,10 +128,6 @@ export const PlaceholderExtension = createExtension( return; } - if (!selection.empty) { - return; - } - // Don't show placeholder when the cursor is inside a code block if (selection.$from.parent.type.spec.code) { return; diff --git a/packages/react/src/editor/styles.css b/packages/react/src/editor/styles.css index 3862946986..bc4e2b0721 100644 --- a/packages/react/src/editor/styles.css +++ b/packages/react/src/editor/styles.css @@ -120,7 +120,7 @@ } /* Placeholder styling */ -.bn-inline-content:has(> .ProseMirror-trailingBreak):before { +.bn-inline-content:has(> .ProseMirror-trailingBreak):after { color: var(--bn-colors-side-menu); } From 28b1f447c01cded31f9c8a5716866194cc0c21af Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 15 Dec 2025 19:22:47 +0100 Subject: [PATCH 2/2] Reverted most changes, moved placeholder rendering to be after block content --- packages/core/src/editor/Block.css | 12 ++-- .../src/extensions/Placeholder/Placeholder.ts | 68 +++++++------------ packages/react/src/editor/styles.css | 2 +- 3 files changed, 30 insertions(+), 52 deletions(-) diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css index 9a58e01369..c131eeeb80 100644 --- a/packages/core/src/editor/Block.css +++ b/packages/core/src/editor/Block.css @@ -40,10 +40,6 @@ BASIC STYLES overflow: visible; } -.bn-inline-content { - width: 100%; -} - /* NESTED BLOCKS */ @@ -542,10 +538,12 @@ NESTED BLOCKS } /* PLACEHOLDERS*/ -/* TODO: should this be here? */ -.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):after { - pointer-events: none; +.bn-block-content:has(.ProseMirror-trailingBreak:only-child):after { font-style: italic; + /* Removes text cursor offset. */ + margin-inline: -2px; + pointer-events: none; + max-width: 100%; } /* TEXT COLORS */ diff --git a/packages/core/src/extensions/Placeholder/Placeholder.ts b/packages/core/src/extensions/Placeholder/Placeholder.ts index dd718f3b07..4a3185e353 100644 --- a/packages/core/src/extensions/Placeholder/Placeholder.ts +++ b/packages/core/src/extensions/Placeholder/Placeholder.ts @@ -23,11 +23,6 @@ export const PlaceholderExtension = createExtension( new Plugin({ key: PLUGIN_KEY, view: (view) => { - view.dom.setAttribute( - "data-selection-empty", - view.state.selection.empty ? "true" : "false", - ); - const uniqueEditorSelector = `placeholder-selector-${v4()}`; view.dom.classList.add(uniqueEditorSelector); const styleEl = document.createElement("style"); @@ -45,22 +40,9 @@ export const PlaceholderExtension = createExtension( const styleSheet = styleEl.sheet!; - const createPlaceholderRule = ( - placeholder: string | undefined, - additionalEditorSelectors = "", - additionalBlockSelectors = "", - ) => { - // Creates CSS rule to set placeholder content at the given selector. - styleSheet.insertRule( - `.${uniqueEditorSelector}${additionalEditorSelectors} .bn-block-content${additionalBlockSelectors} .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):after { content: ${JSON.stringify(placeholder)}; }`, - ); - // Creates CSS rule to hide the trailing break node while the - // placeholder is visible. This is because it's rendered as a - // `br` element, forcing the placeholder onto the next line. - styleSheet.insertRule( - `.${uniqueEditorSelector}${additionalEditorSelectors} .bn-block-content${additionalBlockSelectors} .bn-inline-content > .ProseMirror-trailingBreak:only-child { display: none; }`, - ); - }; + const getSelector = (additionalSelectors = "") => + `.${uniqueEditorSelector} .bn-block-content${additionalSelectors}:has(.ProseMirror-trailingBreak:only-child):after`; + try { // FIXME: the names "default" and "emptyDocument" are hardcoded const { @@ -71,30 +53,30 @@ export const PlaceholderExtension = createExtension( // add block specific placeholders for (const [blockType, placeholder] of Object.entries(rest)) { - createPlaceholderRule( - placeholder, - "[data-selection-empty='true']", - `[data-content-type="${blockType}"]`, - ); - createPlaceholderRule( - placeholder, - "[data-selection-empty='false']", - `[data-content-type="${blockType}"]:not([data-is-empty-and-focused])`, + const blockTypeSelector = `[data-content-type="${blockType}"]`; + + styleSheet.insertRule( + `${getSelector(blockTypeSelector)} { content: ${JSON.stringify( + placeholder, + )}; }`, ); } + const onlyBlockSelector = `[data-is-only-empty-block]`; + const mustBeFocusedSelector = `[data-is-empty-and-focused]`; + // placeholder for when there's only one empty block - createPlaceholderRule( - emptyPlaceholder, - "[data-selection-empty='true']", - "[data-is-only-empty-block]", + styleSheet.insertRule( + `${getSelector(onlyBlockSelector)} { content: ${JSON.stringify( + emptyPlaceholder, + )}; }`, ); // placeholder for default blocks, only when the cursor is in the block (mustBeFocused) - createPlaceholderRule( - defaultPlaceholder, - "[data-selection-empty='true']", - "[data-is-empty-and-focused]", + styleSheet.insertRule( + `${getSelector(mustBeFocusedSelector)} { content: ${JSON.stringify( + defaultPlaceholder, + )}; }`, ); } catch (e) { // eslint-disable-next-line no-console @@ -105,12 +87,6 @@ export const PlaceholderExtension = createExtension( } return { - update: (view) => { - view.dom.setAttribute( - "data-selection-empty", - view.state.selection.empty ? "true" : "false", - ); - }, destroy: () => { if (view.root instanceof window.ShadowRoot) { view.root.removeChild(styleEl); @@ -128,6 +104,10 @@ export const PlaceholderExtension = createExtension( return; } + if (!selection.empty) { + return; + } + // Don't show placeholder when the cursor is inside a code block if (selection.$from.parent.type.spec.code) { return; diff --git a/packages/react/src/editor/styles.css b/packages/react/src/editor/styles.css index bc4e2b0721..c72f9f32ad 100644 --- a/packages/react/src/editor/styles.css +++ b/packages/react/src/editor/styles.css @@ -120,7 +120,7 @@ } /* Placeholder styling */ -.bn-inline-content:has(> .ProseMirror-trailingBreak):after { +.bn-block-content:has(.ProseMirror-trailingBreak):after { color: var(--bn-colors-side-menu); }