mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 09:58:32 +02:00
toc fixes
This commit is contained in:
parent
cce3f9a700
commit
dcf31f8f95
4
package-lock.json
generated
4
package-lock.json
generated
@ -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": {
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
|
@ -35,8 +35,4 @@ export default class CollapsibleWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
/** for overriding */
|
/** for overriding */
|
||||||
async doRenderBody() {}
|
async doRenderBody() {}
|
||||||
|
|
||||||
isExpanded() {
|
|
||||||
return this.$bodyWrapper.hasClass("show");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user