diff --git a/README.md b/README.md index 9f83694..4ccfbef 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Trilium-TocWidget Table of contents [Trilium](https://github.com/zadam/trilium/) widget for -editable text notes. +editable and readonly text notes. ## Features - The ToC is live and automatically updated as new headers are added to the note. -- Works on editable text notes. +- Works on editable and readonly text notes. - Clicking on the ToC navigates the note. - Tested on Trilium Desktop 0.50.3 diff --git a/TocWidget.js b/TocWidget.js index 9b5e775..b592fbe 100644 --- a/TocWidget.js +++ b/TocWidget.js @@ -63,8 +63,8 @@ function debugbreak() { function findHeadingNodeByIndex(parent, headingIndex) { dbg("Finding headingIndex " + headingIndex + " in parent " + parent.name); let headingNode = null; - for (let i = 0, child = null; i < parent.childCount; ++i) { - child = parent.getChild(i); + for (let i = 0; i < parent.childCount; ++i) { + let child = parent.getChild(i); dbg("Inspecting node: " + child.name + ", attrs: " + Array.from(child.getAttributes()) + @@ -88,6 +88,31 @@ function findHeadingNodeByIndex(parent, headingIndex) { return headingNode; } +function findHeadingElementByIndex(parent, headingIndex) { + dbg("Finding headingIndex " + headingIndex + " in parent " + parent.innerHTML); + let headingElement = null; + for (let i = 0; i < parent.children.length; ++i) { + let child = parent.children[i]; + + dbg("Inspecting node: " + child.innerHTML); + + // Headings appear as flattened top level children in the DOM + // named as "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) { + if (headingIndex == 0) { + dbg("Found heading element " + child.tagName); + headingElement = child; + break; + } + headingIndex--; + } + } + return headingElement; +} + class TocWidget extends api.NoteContextAwareWidget { get position() { dbg("getPosition"); @@ -170,61 +195,81 @@ class TocWidget extends api.NoteContextAwareWidget { let $li = $('
  • ' + m[2] + '
  • '); $li.on("click", function () { dbg("clicked"); - api.getActiveTabTextEditor(textEditor => { - const model = textEditor.model; - const doc = model.document; - const root = doc.getRoot(); - - let 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(); + // Check the CSS style for being present and not hidden + // (always editable notes don't have the class, but when toggling + // the readonly button, the CSS is added and then visibility toggled + // rather than removed) + // XXX This could check instead the note readonly attribute? + // XXX Accessing the CSS class like this is probably brittle and + // could change with Trilium versions, is there an api to get + // to this DOM element? + let $readonlyNote = $(".note-detail-readonly-text-content"); + if (($readonlyNote.length > 0) && ($readonlyNote.first().is(":visible"))) { + let parent = $readonlyNote[0]; + let headingElement = findHeadingElementByIndex(parent, headingIndex); + + if (headingElement != null) { + headingElement.scrollIntoView(); } else { warn("Malformed HTML, unable to navigate, TOC rendering is probably wrong too."); } - }); + } else { + api.getActiveTabTextEditor(textEditor => { + const model = textEditor.model; + const doc = model.document; + const root = doc.getRoot(); + + let 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(); + } else { + warn("Malformed HTML, unable to navigate, TOC rendering is probably wrong too."); + } + }); + } }); $ols[$ols.length - 1].append($li); }