mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
editor: window long containers however deeply they are nested
This commit is contained in:
@@ -30,7 +30,7 @@ import TableCell from "../../table-cell/index.js";
|
||||
import TableHeader from "../../table-header/index.js";
|
||||
import { Table } from "../../table/index.js";
|
||||
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
|
||||
import { widestChildren } from "../containers.js";
|
||||
import { containersWorthWindowing, widestChildren } from "../containers.js";
|
||||
import { countPages, Page, Paging } from "../index.js";
|
||||
|
||||
const PagedDocument = Node.create({
|
||||
@@ -76,6 +76,19 @@ function outlineListOf(items: number) {
|
||||
return html + "</ul>";
|
||||
}
|
||||
|
||||
/** A list buried `levels` deep, with the long one at the bottom. */
|
||||
function nestedOutlineOf(levels: number, items: number) {
|
||||
let inner = "";
|
||||
for (let i = 0; i < items; i++)
|
||||
inner += `<li data-type="outlineListItem"><p>Point ${i}</p></li>`;
|
||||
let html = `<ul data-type="outlineList">${inner}</ul>`;
|
||||
for (let level = 0; level < levels; level++)
|
||||
html = `<ul data-type="outlineList"${
|
||||
level === levels - 1 ? ` data-block-id="${id()}"` : ""
|
||||
}><li data-type="outlineListItem"><p>Level ${level}</p>${html}</li></ul>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
function tableOf(rows: number) {
|
||||
let html = `<table data-block-id="${id()}"><tbody>`;
|
||||
for (let i = 0; i < rows; i++)
|
||||
@@ -531,6 +544,43 @@ describe("nested virtualization", () => {
|
||||
two.destroy();
|
||||
});
|
||||
|
||||
test("a long list buried under several levels is still windowed", async () => {
|
||||
// outline and task lists nest as deep as the writer likes, and the long
|
||||
// one can be at the bottom of that
|
||||
const editor = createEditor(para("intro") + nestedOutlineOf(5, 300), 100);
|
||||
await created();
|
||||
|
||||
expect(hidden(editor).length).toBeGreaterThan(200);
|
||||
expect(editor.getHTML()).toContain("Point 299");
|
||||
editor.destroy();
|
||||
});
|
||||
|
||||
test("a nested container keeps its name when the text around it changes", async () => {
|
||||
// the window is remembered against this name, so a name that moved with the
|
||||
// text would throw the reader back to the top of the list on every keystroke
|
||||
const editor = createEditor(para("intro") + nestedOutlineOf(5, 300), 100);
|
||||
await created();
|
||||
|
||||
const block = () => editor.state.doc.child(1);
|
||||
const before = containersWorthWindowing(block()).map((c) => c.id);
|
||||
expect(before).toHaveLength(1);
|
||||
|
||||
// inside the same block and above the nested list, so anything naming it
|
||||
// by where it sits would be renaming it on every keystroke
|
||||
let at = -1;
|
||||
const blockStart = editor.state.doc.child(0).nodeSize;
|
||||
editor.state.doc.descendants((node, position) => {
|
||||
if (at < 0 && position > blockStart && node.type.name === "paragraph")
|
||||
at = position + 1;
|
||||
return at < 0;
|
||||
});
|
||||
editor.commands.setTextSelection(at);
|
||||
editor.commands.insertContent("typing above the list");
|
||||
|
||||
expect(containersWorthWindowing(block()).map((c) => c.id)).toEqual(before);
|
||||
editor.destroy();
|
||||
});
|
||||
|
||||
test("a stand-in child keeps the tag its container expects", async () => {
|
||||
const editor = createEditor(para("intro") + listOf(200));
|
||||
await created();
|
||||
|
||||
@@ -25,8 +25,10 @@ import { Node as ProsemirrorNode } from "@tiptap/pm/model";
|
||||
* rendered whole.
|
||||
*/
|
||||
|
||||
const TABLE = "table";
|
||||
|
||||
const WINDOWABLE_CONTAINERS = new Set([
|
||||
"table",
|
||||
TABLE,
|
||||
"bulletList",
|
||||
"orderedList",
|
||||
"taskList",
|
||||
@@ -39,10 +41,19 @@ export const TABLE_ROW_NODE = "tableRow";
|
||||
export const LIST_ITEM_NODE = "listItem";
|
||||
|
||||
/** Fewer children than this and rendering the whole container is cheap. */
|
||||
const MIN_CHILDREN = 100;
|
||||
const MIN_CHILDREN = 50;
|
||||
|
||||
/** How far into a block to look for containers worth windowing. */
|
||||
const MAX_DEPTH = 3;
|
||||
/**
|
||||
* How far into a block to look for containers worth windowing.
|
||||
*
|
||||
* What keeps this walk cheap is not the limit but where it stops: at leaves, at
|
||||
* text, and at any container long enough to window, whose children it never
|
||||
* looks inside. The limit is only a guard against runaway recursion, so it is
|
||||
* set well past anything a writer would type -- outline and task lists nest as
|
||||
* deep as they like, and each level of nesting costs two steps here, one for
|
||||
* the list and one for the item holding the next one.
|
||||
*/
|
||||
const MAX_DEPTH = 20;
|
||||
|
||||
/** How many children to render before anything has been measured. */
|
||||
export const CHILDREN_BEFORE_MEASURING = 30;
|
||||
@@ -51,9 +62,12 @@ export type WindowedContainer = {
|
||||
node: ProsemirrorNode;
|
||||
/** Where the container begins, counted from the start of the block. */
|
||||
offset: number;
|
||||
/** Stable across edits, so a window survives the text changing under it. */
|
||||
id: string;
|
||||
};
|
||||
|
||||
type Found = Omit<WindowedContainer, "id">;
|
||||
|
||||
const containersByBlock = new WeakMap<ProsemirrorNode, WindowedContainer[]>();
|
||||
const widestByTable = new WeakMap<ProsemirrorNode, number[]>();
|
||||
const NO_CHILDREN: number[] = [];
|
||||
@@ -68,15 +82,13 @@ function findContainers(
|
||||
parent: ProsemirrorNode,
|
||||
contentStart: number,
|
||||
depth: number,
|
||||
found: WindowedContainer[]
|
||||
found: Found[]
|
||||
): void {
|
||||
parent.forEach((child, offset) => {
|
||||
if (child.isLeaf || child.isTextblock) return;
|
||||
const start = contentStart + offset;
|
||||
if (isWorthWindowing(child)) {
|
||||
const id = child.attrs.blockId as string | undefined;
|
||||
if (id) found.push({ node: child, offset: start, id });
|
||||
} else if (depth < MAX_DEPTH)
|
||||
if (isWorthWindowing(child)) found.push({ node: child, offset: start });
|
||||
else if (depth < MAX_DEPTH)
|
||||
findContainers(child, start + 1, depth + 1, found);
|
||||
});
|
||||
}
|
||||
@@ -96,13 +108,23 @@ export function containersWorthWindowing(
|
||||
const cached = containersByBlock.get(block);
|
||||
if (cached) return cached;
|
||||
|
||||
const found: WindowedContainer[] = [];
|
||||
const id = block.attrs.blockId as string | undefined;
|
||||
if (!isWorthWindowing(block)) findContainers(block, 1, 0, found);
|
||||
else if (id) found.push({ node: block, offset: 0, id });
|
||||
// Only the blocks of a note are given ids of their own, so a container nested
|
||||
// inside one has none to be named by. Counting them off within their block
|
||||
// names them just as well, and just as steadily: editing the text around a
|
||||
// container does not change how many come before it, so its window survives.
|
||||
const blockId = block.attrs.blockId as string | undefined;
|
||||
const found: Found[] = [];
|
||||
if (blockId) {
|
||||
if (isWorthWindowing(block)) found.push({ node: block, offset: 0 });
|
||||
else findContainers(block, 1, 0, found);
|
||||
}
|
||||
|
||||
containersByBlock.set(block, found);
|
||||
return found;
|
||||
const containers = found.map((container, index) => ({
|
||||
...container,
|
||||
id: `${blockId}#${index}`
|
||||
}));
|
||||
containersByBlock.set(block, containers);
|
||||
return containers;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,7 +137,7 @@ export function containersWorthWindowing(
|
||||
* what the width follows.
|
||||
*/
|
||||
export function widestChildren(container: ProsemirrorNode): number[] {
|
||||
if (container.type.name !== "table") return NO_CHILDREN;
|
||||
if (container.type.name !== TABLE) return NO_CHILDREN;
|
||||
|
||||
const cached = widestByTable.get(container);
|
||||
if (cached) return cached;
|
||||
|
||||
Reference in New Issue
Block a user