mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
editor: anchor the scroll position even on pages that have not rendered
This commit is contained in:
@@ -547,6 +547,10 @@ export function Editor(props: EditorProps) {
|
||||
const editor = useEditorManager.getState().getEditor(id)?.editor;
|
||||
const selection = editor?.getSelection();
|
||||
if (selection) Config.set(`${id}:selection`, selection);
|
||||
// the scroll handler is debounced, so a reload right after scrolling
|
||||
// would otherwise lose the position
|
||||
const anchor = editor?.getScrollAnchor();
|
||||
if (anchor) Config.set(`${id}:scroll-anchor`, anchor);
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
|
||||
@@ -59,22 +59,28 @@ function rect(top: number, height: number) {
|
||||
const original = HTMLElement.prototype.getBoundingClientRect;
|
||||
|
||||
/** happy-dom lays nothing out, so blocks and pages get a synthetic geometry. */
|
||||
function stubLayout(editor: Editor) {
|
||||
function stubLayout(editor: Editor, scrollTop = 0) {
|
||||
const dom = editor.view.dom as HTMLElement;
|
||||
HTMLElement.prototype.getBoundingClientRect = function () {
|
||||
if (this === dom) return rect(0, BLOCKS * BLOCK_HEIGHT);
|
||||
if (this === dom) return rect(-scrollTop, BLOCKS * BLOCK_HEIGHT);
|
||||
// the scroll container and anything outside the editor sits at the top
|
||||
if (!dom.contains(this)) return rect(0, 800);
|
||||
const siblings = this.parentElement?.children;
|
||||
const index = siblings ? Array.prototype.indexOf.call(siblings, this) : -1;
|
||||
if (index < 0) return rect(0, 0);
|
||||
if (this.parentElement === dom)
|
||||
return rect(index * PAGE_SIZE * BLOCK_HEIGHT, PAGE_SIZE * BLOCK_HEIGHT);
|
||||
return rect(
|
||||
index * PAGE_SIZE * BLOCK_HEIGHT - scrollTop,
|
||||
PAGE_SIZE * BLOCK_HEIGHT
|
||||
);
|
||||
const pageIndex = Array.prototype.indexOf.call(
|
||||
dom.children,
|
||||
this.parentElement
|
||||
);
|
||||
return rect((pageIndex * PAGE_SIZE + index) * BLOCK_HEIGHT, BLOCK_HEIGHT);
|
||||
return rect(
|
||||
(pageIndex * PAGE_SIZE + index) * BLOCK_HEIGHT - scrollTop,
|
||||
BLOCK_HEIGHT
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -151,6 +157,21 @@ describe("scroll anchor", () => {
|
||||
editor.destroy();
|
||||
});
|
||||
|
||||
test("anchors on a placeholder page using the document", () => {
|
||||
const editor = createEditor(createContainer());
|
||||
// scrolled so the fold sits on page 6, which has never rendered
|
||||
stubLayout(editor, 6 * PAGE_SIZE * BLOCK_HEIGHT);
|
||||
expect(
|
||||
(editor.view.dom.children[6] as HTMLElement).hasAttribute(
|
||||
"data-virtual-placeholder"
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
const anchor = getScrollAnchor(editor.view);
|
||||
expect(anchor?.blockId).toBe("blk300");
|
||||
editor.destroy();
|
||||
});
|
||||
|
||||
test("reports failure for a block that is no longer there", () => {
|
||||
const editor = createEditor(createContainer());
|
||||
stubLayout(editor);
|
||||
|
||||
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
|
||||
import { EditorView } from "@tiptap/pm/view";
|
||||
import { profiler } from "../../utils/profiler.js";
|
||||
import { isPage } from "../paging/split.js";
|
||||
import { findScrollParent, virtualizationKey } from "./viewport-plugin.js";
|
||||
|
||||
export type ScrollAnchor = {
|
||||
@@ -43,21 +44,35 @@ function containerOf(view: EditorView) {
|
||||
*/
|
||||
export function getScrollAnchor(view: EditorView): ScrollAnchor | undefined {
|
||||
const { top } = containerOf(view);
|
||||
const blocks = view.dom.querySelectorAll<HTMLElement>("[data-block-id]");
|
||||
const children = view.dom.children;
|
||||
|
||||
for (const element of blocks) {
|
||||
// Pages carry a block id too, but theirs is regenerated every time a note
|
||||
// is opened and split, so only real blocks make a durable anchor.
|
||||
if (
|
||||
element.hasAttribute("data-page") ||
|
||||
element.hasAttribute("data-virtual-placeholder")
|
||||
)
|
||||
continue;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const element = children[i] as HTMLElement;
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.bottom <= top) continue;
|
||||
const blockId = element.getAttribute("data-block-id");
|
||||
if (!blockId) continue;
|
||||
return { blockId, offset: Math.round(top - rect.top) };
|
||||
|
||||
const node = view.state.doc.child(i);
|
||||
if (!isPage(node)) {
|
||||
const blockId = node.attrs.blockId as string | undefined;
|
||||
return blockId
|
||||
? { blockId, offset: Math.round(top - rect.top) }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
// Inside a rendered page, anchor on the exact block at the fold.
|
||||
for (const child of Array.from(element.children)) {
|
||||
const childRect = child.getBoundingClientRect();
|
||||
if (childRect.bottom <= top) continue;
|
||||
const blockId = child.getAttribute("data-block-id");
|
||||
if (blockId) return { blockId, offset: Math.round(top - childRect.top) };
|
||||
}
|
||||
|
||||
// A page that has not rendered has no blocks to inspect, but the document
|
||||
// still knows which block it starts with.
|
||||
const blockId = node.firstChild?.attrs.blockId as string | undefined;
|
||||
return blockId
|
||||
? { blockId, offset: Math.round(top - rect.top) }
|
||||
: undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user