Compare commits

...

4 Commits

Author SHA1 Message Date
Elian Doran
d3337eab9c Merge branch 'main' into feature/toc_improvements 2026-03-05 21:05:17 +02:00
Elian Doran
8128a8192a refactor(ckeditor): address requested changes 2026-03-05 19:28:52 +02:00
Elian Doran
65514a6fd7 fix(toc): title is extracted before changes are made 2026-03-05 19:08:56 +02:00
Elian Doran
93a7f8c711 fix(toc): not reacting to attribute changes in CKEditor 2026-03-05 19:03:32 +02:00
3 changed files with 35 additions and 3 deletions

View File

@@ -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";
@@ -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));
});
}
};

View File

@@ -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";

View File

@@ -0,0 +1,28 @@
import type { DifferItemAttribute, Editor, ModelDocumentFragment, ModelElement, ModelNode } from "ckeditor5";
function hasHeadingAncestor(node: ModelElement | ModelNode | ModelDocumentFragment | null): boolean {
let current: ModelElement | ModelNode | ModelDocumentFragment | null = node;
while (current) {
if (!!current && current.is('element') && (current as ModelElement).name.startsWith("heading")) return true;
current = current.parent;
}
return false;
}
export function attributeChangeAffectsHeading(change: DifferItemAttribute, editor: Editor): 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;
}