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", "name": "trilium",
"version": "0.51.2", "version": "0.52.0-beta",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "trilium", "name": "trilium",
"version": "0.51.2", "version": "0.52.0-beta",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {

View File

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

View File

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

View File

@ -11,7 +11,9 @@ export default class RightPaneContainer extends FlexContainer {
} }
isEnabled() { 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) { handleEventInChildren(name, data) {
@ -21,13 +23,20 @@ export default class RightPaneContainer extends FlexContainer {
// right pane is displayed only if some child widget is active // 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 // 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 // but these events needs to be finished and only then we check
promise.then(() => { promise.then(() => this.reevaluateIsEnabledCommand());
this.toggleInt(this.isEnabled());
splitService.setupRightPaneResizer();
});
} }
return promise; 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 { .toc ol {
padding-left: 20px; padding-left: 25px;
} }
.toc > ol { .toc > ol {
padding-left: 0; padding-left: 10px;
} }
</style> </style>
@ -75,7 +75,10 @@ function findHeadingElementByIndex(parent, headingIndex) {
// "H" plus the level, eg "H2", "H3", "H2", etc and not nested wrt the // "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 // heading level. If a heading node is found, decrement the headingIndex
// until zero is reached // 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) { if (headingIndex === 0) {
headingElement = child; headingElement = child;
break; break;
@ -86,6 +89,8 @@ function findHeadingElementByIndex(parent, headingIndex) {
return headingElement; return headingElement;
} }
const MIN_HEADING_COUNT = 3;
export default class TocWidget extends CollapsibleWidget { export default class TocWidget extends CollapsibleWidget {
get widgetTitle() { get widgetTitle() {
return "Table of Contents"; return "Table of Contents";
@ -94,7 +99,7 @@ export default class TocWidget extends CollapsibleWidget {
isEnabled() { isEnabled() {
return super.isEnabled() return super.isEnabled()
&& this.note.type === 'text' && this.note.type === 'text'
&& !this.note.hasLabel('noTocWidget'); && !this.note.hasLabel('noToc');
} }
async doRenderBody() { async doRenderBody() {
@ -103,21 +108,23 @@ export default class TocWidget extends CollapsibleWidget {
} }
async refreshWithNote(note) { async refreshWithNote(note) {
let toc = ""; let $toc = "", headingCount = 0;
// Check for type text unconditionally in case alwaysShowWidget is set // Check for type text unconditionally in case alwaysShowWidget is set
if (this.note.type === 'text') { if (this.note.type === 'text') {
const { content } = await note.getNoteComplement(); 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. * Builds a jquery table of contents.
* *
* @param {String} html Note's html content * @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 * with an onclick event that will cause the document to scroll to
* the desired position. * 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 // Note heading 2 is the first level Trilium makes available to the note
let curLevel = 2; let curLevel = 2;
const $ols = [$toc]; 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 // Nest/unnest whatever necessary number of ordered lists
// //
@ -164,93 +172,101 @@ export default class TocWidget extends CollapsibleWidget {
}).mouseout(function () { }).mouseout(function () {
$(this).css("font-weight", "normal"); $(this).css("font-weight", "normal");
}); });
$li.on("click", async () => { $li.on("click", () => this.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 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();
}
}
});
$ols[$ols.length - 1].append($li); $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}) { async entitiesReloadedEvent({loadResults}) {
if (loadResults.isNoteContentReloaded(this.noteId) if (loadResults.isNoteContentReloaded(this.noteId)) {
|| loadResults.getAttributes().find(attr => attr.type === 'label' await this.refresh();
&& attr.name.toLowerCase().includes('readonly') } else if (loadResults.getAttributes().find(attr => attr.type === 'label'
&& attributeService.isAffecting(attr, this.note))) { && (attr.name.toLowerCase().includes('readonly') || attr.name === 'noToc')
&& attributeService.isAffecting(attr, this.note))) {
await this.refresh(); await this.refresh();
} }

View File

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