diff --git a/packages/editor/src/extensions/heading/__tests__/__snapshots__/heading.test.ts.snap b/packages/editor/src/extensions/heading/__tests__/__snapshots__/heading.test.ts.snap
index 36139c74a..36f948208 100644
--- a/packages/editor/src/extensions/heading/__tests__/__snapshots__/heading.test.ts.snap
+++ b/packages/editor/src/extensions/heading/__tests__/__snapshots__/heading.test.ts.snap
@@ -3,3 +3,13 @@
exports[`collapse heading > heading collapsed 1`] = `"
Main Heading
paragraph.
Subheading
subheading paragraph
Main heading 2
paragraph another
"`;
exports[`collapse heading > heading uncollapsed 1`] = `"Main Heading
paragraph.
Subheading
subheading paragraph
Main heading 2
paragraph another
"`;
+
+exports[`replacing collapsed heading with another heading level should not unhide content 1`] = `"A collapsed heading
Hidden paragraph
"`;
+
+exports[`replacing collapsed heading with another node (blockquote) should unhide content 1`] = `"A collpased heading
Hidden paragraph
"`;
+
+exports[`replacing collapsed heading with another node (bulletList) should unhide content 1`] = `"Hidden paragraph
"`;
+
+exports[`replacing collapsed heading with another node (codeBlock) should unhide content 1`] = `"A collpased heading
Hidden paragraph
"`;
+
+exports[`replacing collapsed heading with another node (paragraph) should unhide content 1`] = `"A collpased heading
Hidden paragraph
"`;
diff --git a/packages/editor/src/extensions/heading/__tests__/heading.test.ts b/packages/editor/src/extensions/heading/__tests__/heading.test.ts
index 9fda63e91..8f6a8a46d 100644
--- a/packages/editor/src/extensions/heading/__tests__/heading.test.ts
+++ b/packages/editor/src/extensions/heading/__tests__/heading.test.ts
@@ -18,8 +18,9 @@ along with this program. If not, see .
*/
import { test, expect } from "vitest";
-import { createEditor } from "../../../../test-utils/index.js";
+import { createEditor, h } from "../../../../test-utils/index.js";
import { Heading } from "../heading.js";
+import { Editor } from "@tiptap/core";
test("collapse heading", () => {
const { editor } = createEditor({
@@ -52,3 +53,59 @@ test("collapse heading", () => {
expect(editor.getHTML()).toMatchSnapshot("heading uncollapsed");
});
+
+test("replacing collapsed heading with another heading level should not unhide content", () => {
+ const el = h("div", [
+ h("h1", ["A collapsed heading"], { "data-collapsed": "true" }),
+ h("p", ["Hidden paragraph"], { "data-hidden": "true" })
+ ]);
+ const { editor } = createEditor({
+ extensions: {
+ heading: Heading.configure({ levels: [1, 2, 3, 4, 5, 6] })
+ },
+ initialContent: el.outerHTML
+ });
+
+ editor.commands.setTextSelection(0);
+ editor.commands.setHeading({ level: 2 });
+
+ expect(editor.getHTML()).toMatchSnapshot();
+});
+
+const nodes: { name: string; setNode: (editor: Editor) => void }[] = [
+ {
+ name: "paragraph",
+ setNode: (editor) => editor.commands.setParagraph()
+ },
+ {
+ name: "codeBlock",
+ setNode: (editor) => editor.commands.setCodeBlock()
+ },
+ {
+ name: "bulletList",
+ setNode: (editor) => editor.commands.toggleList("bulletList", "listItem")
+ },
+ {
+ name: "blockquote",
+ setNode: (editor) => editor.commands.toggleBlockquote()
+ }
+];
+for (const { name, setNode } of nodes) {
+ test(`replacing collapsed heading with another node (${name}) should unhide content`, () => {
+ const el = h("div", [
+ h("h1", ["A collpased heading"], { "data-collapsed": "true" }),
+ h("p", ["Hidden paragraph"], { "data-hidden": "true" })
+ ]);
+ const { editor } = createEditor({
+ extensions: {
+ heading: Heading.configure({ levels: [1, 2, 3, 4, 5, 6] })
+ },
+ initialContent: el.outerHTML
+ });
+
+ editor.commands.setTextSelection(0);
+ setNode(editor);
+
+ expect(editor.getHTML()).toMatchSnapshot();
+ });
+}
diff --git a/packages/editor/src/extensions/heading/heading.ts b/packages/editor/src/extensions/heading/heading.ts
index 83a1cde0d..45ac83846 100644
--- a/packages/editor/src/extensions/heading/heading.ts
+++ b/packages/editor/src/extensions/heading/heading.ts
@@ -207,7 +207,7 @@ export const Heading = TiptapHeading.extend({
const headingLevel = currentNode.attrs.level;
tr.setNodeAttribute(pos, "collapsed", shouldCollapse);
- toggleNodesUnderHeading(tr, pos, headingLevel, shouldCollapse);
+ toggleNodesUnderPos(tr, pos, headingLevel, shouldCollapse);
}
return true;
});
@@ -255,17 +255,17 @@ export const Heading = TiptapHeading.extend({
}
});
-function toggleNodesUnderHeading(
+function toggleNodesUnderPos(
tr: Transaction,
- headingPos: number,
+ pos: number,
headingLevel: number,
isCollapsing: boolean
) {
const { doc } = tr;
- const headingNode = doc.nodeAt(headingPos);
- if (!headingNode || headingNode.type.name !== "heading") return;
+ const node = doc.nodeAt(pos);
+ if (!node) return;
- let nextPos = headingPos + headingNode.nodeSize;
+ let nextPos = pos + node.nodeSize;
const cursorPos = tr.selection.from;
let shouldMoveCursor = false;
let insideCollapsedHeading = false;
@@ -320,8 +320,8 @@ function toggleNodesUnderHeading(
}
if (shouldMoveCursor) {
- const headingEndPos = headingPos + headingNode.nodeSize - 1;
- tr.setSelection(Selection.near(tr.doc.resolve(headingEndPos)));
+ const endPos = pos + node.nodeSize - 1;
+ tr.setSelection(Selection.near(tr.doc.resolve(endPos)));
}
}
@@ -366,26 +366,27 @@ const headingUpdatePlugin = new Plugin({
let modified = false;
newDoc.descendants((newNode, pos) => {
- if (newNode.type.name === "heading") {
- if (pos >= oldDoc.content.size) return;
+ if (pos >= oldDoc.content.size) return;
- const oldNode = oldDoc.nodeAt(pos);
- if (
- oldNode &&
- oldNode.type.name === "heading" &&
- oldNode.attrs.level !== newNode.attrs.level
- ) {
- /**
- * if the level of a collapsed heading is changed,
- * we need to reset visibility of all the nodes under it as there
- * might be a heading of same or higher level previously
- * hidden under this heading
- */
- if (newNode.attrs.collapsed) {
- toggleNodesUnderHeading(tr, pos, oldNode.attrs.level, false);
- toggleNodesUnderHeading(tr, pos, newNode.attrs.level, true);
- modified = true;
- }
+ const oldNode = oldDoc.nodeAt(pos);
+ if (
+ oldNode &&
+ oldNode.type.name === "heading" &&
+ oldNode.attrs.level !== newNode.attrs.level
+ ) {
+ /**
+ * if the level of a collapsed heading is changed,
+ * we need to reset visibility of all the nodes under it as there
+ * might be a heading of same or higher level previously
+ * hidden under this heading
+ */
+ if (newNode.type.name === "heading" && newNode.attrs.collapsed) {
+ toggleNodesUnderPos(tr, pos, oldNode.attrs.level, false);
+ toggleNodesUnderPos(tr, pos, newNode.attrs.level, true);
+ modified = true;
+ } else if (newNode.type.name !== "heading" && oldNode.attrs.collapsed) {
+ toggleNodesUnderPos(tr, pos, oldNode.attrs.level, false);
+ modified = true;
}
}
});