From aade28739633b9907bab694a8e6013672b24821e Mon Sep 17 00:00:00 2001
From: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Date: Fri, 24 Apr 2026 10:48:49 +0500
Subject: [PATCH] editor: expand outline list && headings on search scroll
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
---
.../search-replace/search-replace.ts | 92 +++++++++++++++----
1 file changed, 72 insertions(+), 20 deletions(-)
diff --git a/packages/editor/src/extensions/search-replace/search-replace.ts b/packages/editor/src/extensions/search-replace/search-replace.ts
index 02eaec305..11ebfbf08 100644
--- a/packages/editor/src/extensions/search-replace/search-replace.ts
+++ b/packages/editor/src/extensions/search-replace/search-replace.ts
@@ -18,7 +18,7 @@ along with this program. If not, see .
*/
import { Extension } from "@tiptap/core";
-import { Decoration, DecorationSet } from "prosemirror-view";
+import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
import {
EditorState,
Plugin,
@@ -28,7 +28,7 @@ import {
} from "prosemirror-state";
import { SearchSettings } from "../../toolbar/stores/search-store.js";
import { tiptapKeys } from "@notesnook/common";
-import { findParentNodeClosestToPos } from "../../utils/prosemirror.js";
+import { toggleNodesUnderPos } from "../heading/index.js";
type DispatchFn = (tr: Transaction) => void;
declare module "@tiptap/core" {
@@ -296,14 +296,13 @@ export const SearchReplace = Extension.create({
)
);
- expandCollapsedCallout(tr, from);
-
- const domNode = this.editor.view.domAtPos(from).node;
- scrollIntoView(domNode);
+ expandCollapsedParents(tr, from);
this.storage.selectedIndex = nextIndex;
tr.setMeta("selectedIndex", nextIndex);
if (dispatch) updateView(state, dispatch);
+
+ scrollIntoView(this.editor.view, from);
return true;
},
moveToPreviousResult:
@@ -325,15 +324,13 @@ export const SearchReplace = Extension.create({
)
);
- expandCollapsedCallout(tr, from);
-
- const domNode = this.editor.view.domAtPos(from).node;
- scrollIntoView(domNode);
+ expandCollapsedParents(tr, from);
this.storage.selectedIndex = prevIndex;
tr.setMeta("selectedIndex", prevIndex);
if (dispatch) updateView(state, dispatch);
+ scrollIntoView(this.editor.view, from);
return true;
},
replace:
@@ -481,20 +478,75 @@ export const SearchReplace = Extension.create({
}
});
-function expandCollapsedCallout(tr: Transaction, pos: number) {
- const $pos = tr.doc.resolve(pos);
- const calloutParent = findParentNodeClosestToPos(
- $pos,
- (node) => node.type.name === "callout"
- );
- if (!calloutParent || !calloutParent.node.attrs.collapsed) return;
+function expandCollapsedParents(tr: Transaction, pos: number) {
+ try {
+ let changed = false;
- tr.setMeta("preventSave", true);
- tr.setNodeAttribute(calloutParent.pos, "collapsed", false);
+ let expandedSomething = true;
+ while (expandedSomething) {
+ const $pos = tr.doc.resolve(pos);
+ expandedSomething = false;
+
+ for (let depth = 1; depth <= $pos.depth; depth++) {
+ const node = $pos.node(depth);
+ const nodePos = $pos.before(depth);
+
+ if (
+ (node.type.name === "callout" ||
+ node.type.name === "outlineListItem") &&
+ node.attrs.collapsed
+ ) {
+ tr.setNodeAttribute(nodePos, "collapsed", false);
+ changed = true;
+ expandedSomething = true;
+ }
+
+ // expand collapsed heading that hid this node via hidden attribute
+ if (node.attrs.hidden) {
+ const parentNode = $pos.node(depth - 1);
+ const parentContentStart =
+ depth === 1 ? 0 : $pos.before(depth - 1) + 1;
+
+ let collapsedHeadingPos = -1;
+ let collapsedHeadingLevel = -1;
+
+ parentNode.forEach((child, offset) => {
+ const childAbsPos = parentContentStart + offset;
+ if (childAbsPos >= nodePos) return;
+ if (
+ child.type.name === "heading" &&
+ child.attrs.collapsed &&
+ !child.attrs.hidden
+ ) {
+ collapsedHeadingPos = childAbsPos;
+ collapsedHeadingLevel = child.attrs.level;
+ }
+ });
+
+ if (collapsedHeadingPos !== -1) {
+ tr.setNodeAttribute(collapsedHeadingPos, "collapsed", false);
+ toggleNodesUnderPos(
+ tr,
+ collapsedHeadingPos,
+ collapsedHeadingLevel,
+ false
+ );
+ changed = true;
+ expandedSomething = true;
+ }
+ }
+ }
+ }
+
+ if (changed) tr.setMeta("preventSave", true);
+ } catch (e) {
+ console.error("Error expanding collapsed parents: ", e);
+ }
}
-function scrollIntoView(domNode: Node) {
+function scrollIntoView(view: EditorView, pos: number) {
setTimeout(() => {
+ const domNode = view.domAtPos(pos).node;
if ("scrollIntoView" in domNode) {
(domNode as Element).scrollIntoView({
behavior: "instant",