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

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "trilium",
"version": "0.51.2",
"version": "0.52.0-beta",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "trilium",
"version": "0.51.2",
"version": "0.52.0-beta",
"hasInstallScript": true,
"license": "AGPL-3.0-only",
"dependencies": {

View file

@ -103,10 +103,22 @@ class BasicWidget extends Component {
this.$widget.toggleClass('hidden-int', !show);
}
isHiddenInt() {
return this.$widget.hasClass('hidden-int');
}
toggleExt(show) {
this.$widget.toggleClass('hidden-ext', !show);
}
isHiddenExt() {
return this.$widget.hasClass('hidden-ext');
}
canBeShown() {
return !this.isHiddenInt() && !this.isHiddenExt();
}
isVisible() {
return this.$widget.is(":visible");
}

View file

@ -35,8 +35,4 @@ export default class CollapsibleWidget extends NoteContextAwareWidget {
/** for overriding */
async doRenderBody() {}
isExpanded() {
return this.$bodyWrapper.hasClass("show");
}
}

View file

@ -11,7 +11,9 @@ export default class RightPaneContainer extends FlexContainer {
}
isEnabled() {
return super.isEnabled() && this.children.length > 0 && !!this.children.find(ch => ch.isEnabled());
return super.isEnabled()
&& this.children.length > 0
&& !!this.children.find(ch => ch.isEnabled() && ch.canBeShown());
}
handleEventInChildren(name, data) {
@ -21,13 +23,20 @@ export default class RightPaneContainer extends FlexContainer {
// right pane is displayed only if some child widget is active
// we'll reevaluate the visibility based on events which are probable to cause visibility change
// but these events needs to be finished and only then we check
promise.then(() => {
this.toggleInt(this.isEnabled());
splitService.setupRightPaneResizer();
});
promise.then(() => this.reevaluateIsEnabledCommand());
}
return promise;
}
reevaluateIsEnabledCommand() {
const oldToggle = !this.isHiddenInt();
const newToggle = this.isEnabled();
if (oldToggle !== newToggle) {
this.toggleInt(newToggle);
splitService.setupRightPaneResizer();
}
}
}

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();
}

View file

@ -241,8 +241,8 @@ body .CodeMirror {
background-color: #eeeeee
}
.CodeMirror pre.CodeMirror-placeholder {
color: #999 !important;
.CodeMirror pre.CodeMirror-placeholder {
color: #999 !important;
}
#sql-console-query {
@ -943,7 +943,6 @@ input {
border: 0;
height: 100%;
overflow: auto;
max-height: 300px;
}
#right-pane .card-body ul {