toc fixes

This commit is contained in:
zadam
2022-05-30 17:45:59 +02:00
parent cce3f9a700
commit dcf31f8f95
6 changed files with 137 additions and 105 deletions

View File

@@ -26,11 +26,11 @@ const TPL = `<div class="toc-widget">
}
.toc ol {
padding-left: 20px;
padding-left: 25px;
}
.toc > ol {
padding-left: 0;
padding-left: 10px;
}
</style>
@@ -75,7 +75,10 @@ function findHeadingElementByIndex(parent, headingIndex) {
// "H" plus the level, eg "H2", "H3", "H2", etc and not nested wrt the
// heading level. If a heading node is found, decrement the headingIndex
// until zero is reached
if (child.tagName.match(/H\d+/) !== null) {
console.log(child.tagName, headingIndex);
if (child.tagName.match(/H\d+/i) !== null) {
if (headingIndex === 0) {
headingElement = child;
break;
@@ -86,6 +89,8 @@ function findHeadingElementByIndex(parent, headingIndex) {
return headingElement;
}
const MIN_HEADING_COUNT = 3;
export default class TocWidget extends CollapsibleWidget {
get widgetTitle() {
return "Table of Contents";
@@ -94,7 +99,7 @@ export default class TocWidget extends CollapsibleWidget {
isEnabled() {
return super.isEnabled()
&& this.note.type === 'text'
&& !this.note.hasLabel('noTocWidget');
&& !this.note.hasLabel('noToc');
}
async doRenderBody() {
@@ -103,21 +108,23 @@ export default class TocWidget extends CollapsibleWidget {
}
async refreshWithNote(note) {
let toc = "";
let $toc = "", headingCount = 0;
// Check for type text unconditionally in case alwaysShowWidget is set
if (this.note.type === 'text') {
const { content } = await note.getNoteComplement();
toc = await this.getToc(content);
({$toc, headingCount} = await this.getToc(content));
}
this.$toc.html(toc);
this.$toc.html($toc);
this.toggleInt(headingCount >= MIN_HEADING_COUNT);
this.triggerCommand("reevaluateIsEnabled");
}
/**
* Builds a jquery table of contents.
*
* @param {String} html Note's html content
* @returns {jQuery} ordered list table of headings, nested by heading level
* @returns {$toc: jQuery, headingCount: integer} ordered list table of headings, nested by heading level
* with an onclick event that will cause the document to scroll to
* the desired position.
*/
@@ -133,7 +140,8 @@ export default class TocWidget extends CollapsibleWidget {
// Note heading 2 is the first level Trilium makes available to the note
let curLevel = 2;
const $ols = [$toc];
for (let m = null, headingIndex = 0; ((m = headingTagsRegex.exec(html)) !== null); ++headingIndex) {
let headingCount;
for (let m = null, headingIndex = 0; ((m = headingTagsRegex.exec(html)) !== null); headingIndex++) {
//
// Nest/unnest whatever necessary number of ordered lists
//
@@ -164,93 +172,101 @@ export default class TocWidget extends CollapsibleWidget {
}).mouseout(function () {
$(this).css("font-weight", "normal");
});
$li.on("click", async () => {
// A readonly note can change state to "readonly disabled
// temporarily" (ie "edit this note" button) without any
// intervening events, do the readonly calculation at navigation
// time and not at outline creation time
// See https://github.com/zadam/trilium/issues/2828
const isReadOnly = await this.noteContext.isReadOnly();
if (isReadOnly) {
const readonlyTextElement = await this.noteContext.getContentElement();
const headingElement = findHeadingElementByIndex(readonlyTextElement, headingIndex);
if (headingElement != null) {
headingElement.scrollIntoView();
}
} else {
const textEditor = await this.noteContext.getTextEditor();
const model = textEditor.model;
const doc = model.document;
const root = doc.getRoot();
const headingNode = findHeadingNodeByIndex(root, headingIndex);
// headingNode could be null if the html was malformed or
// with headings inside elements, just ignore and don't
// navigate (note that the TOC rendering and other TOC
// entries' navigation could be wrong too)
if (headingNode != null) {
// Setting the selection alone doesn't scroll to the
// caret, needs to be done explicitly and outside of
// the writer change callback so the scroll is
// guaranteed to happen after the selection is
// updated.
// In addition, scrolling to a caret later in the
// document (ie "forward scrolls"), only scrolls
// barely enough to place the caret at the bottom of
// the screen, which is a usability issue, you would
// like the caret to be placed at the top or center
// of the screen.
// To work around that issue, first scroll to the
// end of the document, then scroll to the desired
// point. This causes all the scrolls to be
// "backward scrolls" no matter the current caret
// position, which places the caret at the top of
// the screen.
// XXX This could be fixed in another way by using
// the underlying CKEditor5
// scrollViewportToShowTarget, which allows to
// provide a larger "viewportOffset", but that
// has coding complications (requires calling an
// internal CKEditor utils funcion and passing
// an HTML element, not a CKEditor node, and
// CKEditor5 doesn't seem to have a
// straightforward way to convert a node to an
// HTML element? (in CKEditor4 this was done
// with $(node.$) )
// Scroll to the end of the note to guarantee the
// next scroll is a backwards scroll that places the
// caret at the top of the screen
model.change(writer => {
writer.setSelection(root.getChild(root.childCount - 1), 0);
});
textEditor.editing.view.scrollToTheSelection();
// Backwards scroll to the heading
model.change(writer => {
writer.setSelection(headingNode, 0);
});
textEditor.editing.view.scrollToTheSelection();
}
}
});
$li.on("click", () => this.jumpToHeading(headingIndex));
$ols[$ols.length - 1].append($li);
headingCount = headingIndex;
}
return $toc;
return {
$toc,
headingCount
};
}
async jumpToHeading(headingIndex) {
// A readonly note can change state to "readonly disabled
// temporarily" (ie "edit this note" button) without any
// intervening events, do the readonly calculation at navigation
// time and not at outline creation time
// See https://github.com/zadam/trilium/issues/2828
const isReadOnly = await this.noteContext.isReadOnly();
if (isReadOnly) {
const $readonlyTextContent = await this.noteContext.getContentElement();
const headingElement = findHeadingElementByIndex($readonlyTextContent[0], headingIndex);
if (headingElement != null) {
headingElement.scrollIntoView();
}
} else {
const textEditor = await this.noteContext.getTextEditor();
const model = textEditor.model;
const doc = model.document;
const root = doc.getRoot();
const headingNode = findHeadingNodeByIndex(root, headingIndex);
// headingNode could be null if the html was malformed or
// with headings inside elements, just ignore and don't
// navigate (note that the TOC rendering and other TOC
// entries' navigation could be wrong too)
if (headingNode != null) {
// Setting the selection alone doesn't scroll to the
// caret, needs to be done explicitly and outside of
// the writer change callback so the scroll is
// guaranteed to happen after the selection is
// updated.
// In addition, scrolling to a caret later in the
// document (ie "forward scrolls"), only scrolls
// barely enough to place the caret at the bottom of
// the screen, which is a usability issue, you would
// like the caret to be placed at the top or center
// of the screen.
// To work around that issue, first scroll to the
// end of the document, then scroll to the desired
// point. This causes all the scrolls to be
// "backward scrolls" no matter the current caret
// position, which places the caret at the top of
// the screen.
// XXX This could be fixed in another way by using
// the underlying CKEditor5
// scrollViewportToShowTarget, which allows to
// provide a larger "viewportOffset", but that
// has coding complications (requires calling an
// internal CKEditor utils funcion and passing
// an HTML element, not a CKEditor node, and
// CKEditor5 doesn't seem to have a
// straightforward way to convert a node to an
// HTML element? (in CKEditor4 this was done
// with $(node.$) )
// Scroll to the end of the note to guarantee the
// next scroll is a backwards scroll that places the
// caret at the top of the screen
model.change(writer => {
writer.setSelection(root.getChild(root.childCount - 1), 0);
});
textEditor.editing.view.scrollToTheSelection();
// Backwards scroll to the heading
model.change(writer => {
writer.setSelection(headingNode, 0);
});
textEditor.editing.view.scrollToTheSelection();
}
}
}
async entitiesReloadedEvent({loadResults}) {
if (loadResults.isNoteContentReloaded(this.noteId)
|| loadResults.getAttributes().find(attr => attr.type === 'label'
&& attr.name.toLowerCase().includes('readonly')
&& attributeService.isAffecting(attr, this.note))) {
if (loadResults.isNoteContentReloaded(this.noteId)) {
await this.refresh();
} else if (loadResults.getAttributes().find(attr => attr.type === 'label'
&& (attr.name.toLowerCase().includes('readonly') || attr.name === 'noToc')
&& attributeService.isAffecting(attr, this.note))) {
await this.refresh();
}