Skip to content

Commit

Permalink
Added readonly text note support.
Browse files Browse the repository at this point in the history
  • Loading branch information
antoniotejada committed Apr 25, 2022
1 parent 1a82dee commit 59daa68
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 56 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
153 changes: 99 additions & 54 deletions TocWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()) +
Expand All @@ -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");
Expand Down Expand Up @@ -170,61 +195,81 @@ class TocWidget extends api.NoteContextAwareWidget {
let $li = $('<li style="cursor:pointer">' + m[2] + '</li>');
$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);
}
Expand Down

0 comments on commit 59daa68

Please sign in to comment.