Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions apps/client/src/widgets/sidebar/TableOfContents.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "./TableOfContents.css";

import { CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
import { attributeChangeAffectsHeading, CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
import clsx from "clsx";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";

Expand Down Expand Up @@ -170,11 +170,14 @@ function EditableTextTableOfContents() {

const affectsHeadings = changes.some( change => {
return (
change.type === 'insert' || change.type === 'remove' || (change.type === 'attribute' && change.attributeKey === 'headingLevel')
change.type === 'insert' || change.type === 'remove' ||
(change.type === 'attribute' && attributeChangeAffectsHeading(change, textEditor))
);
});
if (affectsHeadings) {
setHeadings(extractTocFromTextEditor(textEditor));
requestAnimationFrame(() => {
setHeadings(extractTocFromTextEditor(textEditor));
});
}
};

Expand Down
1 change: 1 addition & 0 deletions packages/ckeditor5/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type { EditorConfig, MentionFeed, MentionFeedObjectItem, ModelNode, Model
export type { TemplateDefinition } from "ckeditor5-premium-features";
export { default as buildExtraCommands } from "./extra_slash_commands.js";
export { default as getCkLocale } from "./i18n.js";
export * from "./utils.js";

// Import with sideffects to ensure that type augmentations are present.
import "@triliumnext/ckeditor5-math";
Expand Down
37 changes: 37 additions & 0 deletions packages/ckeditor5/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { DifferItemAttribute, ModelDocumentFragment, ModelElement, ModelNode } from "ckeditor5";
import { CKTextEditor } from "src";

function isHeadingElement(node: ModelElement | ModelNode | ModelDocumentFragment | null): node is ModelElement {
return !!node
&& typeof (node as any).is === "function"
&& (node as any).is("element")
&& typeof (node as any).name === "string"
&& (node as any).name.startsWith("heading");
}

function hasHeadingAncestor(node: ModelElement | ModelNode | ModelDocumentFragment | null): boolean {
let current: ModelElement | ModelNode | ModelDocumentFragment | null = node;
while (current) {
if (isHeadingElement(current)) return true;
current = current.parent;
}
return false;
}

export function attributeChangeAffectsHeading(change: DifferItemAttribute, editor: CKTextEditor): boolean {
if (change.type !== "attribute") return false;

// Fast checks on range boundaries
if (hasHeadingAncestor(change.range.start.parent) || hasHeadingAncestor(change.range.end.parent)) {
return true;
}

// Robust check across the whole changed range
const range = editor.model.createRange(change.range.start, change.range.end);
for (const item of range.getItems()) {
const baseNode = item.is("$textProxy") ? item.parent : item;
if (hasHeadingAncestor(baseNode)) return true;
}

return false;
}