From ee217d630660f8eab6e9e78f52181ee414359d87 Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 28 May 2022 22:19:29 +0200 Subject: [PATCH 01/23] added iframe note type --- src/public/app/entities/note_short.js | 3 +- src/public/app/services/tree_context_menu.js | 1 + src/public/app/widgets/note_detail.js | 17 +++-- src/public/app/widgets/note_type.js | 3 +- src/public/app/widgets/note_wrapper.js | 2 +- src/public/app/widgets/type_widgets/canvas.js | 4 ++ src/public/app/widgets/type_widgets/iframe.js | 67 +++++++++++++++++++ src/services/note_types.js | 21 +++--- src/services/notes.js | 2 +- 9 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 src/public/app/widgets/type_widgets/iframe.js diff --git a/src/public/app/entities/note_short.js b/src/public/app/entities/note_short.js index 13bae5a93..3203d9c6e 100644 --- a/src/public/app/entities/note_short.js +++ b/src/public/app/entities/note_short.js @@ -17,7 +17,8 @@ const NOTE_TYPE_ICONS = { "book": "bx bx-book", "note-map": "bx bx-map-alt", "mermaid": "bx bx-selection", - "canvas": "bx bx-pen" + "canvas": "bx bx-pen", + "iframe": "bx bx-globe-alt" }; /** diff --git a/src/public/app/services/tree_context_menu.js b/src/public/app/services/tree_context_menu.js index 89d860804..1a0b64213 100644 --- a/src/public/app/services/tree_context_menu.js +++ b/src/public/app/services/tree_context_menu.js @@ -35,6 +35,7 @@ class TreeContextMenu { { title: "Book", command: command, type: "book", uiIcon: "book" }, { title: "Mermaid diagram", command: command, type: "mermaid", uiIcon: "selection" }, { title: "Canvas", command: command, type: "canvas", uiIcon: "pen" }, + { title: "IFrame", command: command, type: "iframe", uiIcon: "globe-alt" }, ]; } diff --git a/src/public/app/widgets/note_detail.js b/src/public/app/widgets/note_detail.js index 0a0790fa8..69af9d507 100644 --- a/src/public/app/widgets/note_detail.js +++ b/src/public/app/widgets/note_detail.js @@ -3,6 +3,12 @@ import protectedSessionHolder from "../services/protected_session_holder.js"; import SpacedUpdate from "../services/spaced_update.js"; import server from "../services/server.js"; import libraryLoader from "../services/library_loader.js"; +import appContext from "../services/app_context.js"; +import keyboardActionsService from "../services/keyboard_actions.js"; +import noteCreateService from "../services/note_create.js"; +import attributeService from "../services/attributes.js"; +import attributeRenderer from "../services/attribute_renderer.js"; + import EmptyTypeWidget from "./type_widgets/empty.js"; import EditableTextTypeWidget from "./type_widgets/editable_text.js"; import EditableCodeTypeWidget from "./type_widgets/editable_code.js"; @@ -13,16 +19,12 @@ import RelationMapTypeWidget from "./type_widgets/relation_map.js"; import CanvasTypeWidget from "./type_widgets/canvas.js"; import ProtectedSessionTypeWidget from "./type_widgets/protected_session.js"; import BookTypeWidget from "./type_widgets/book.js"; -import appContext from "../services/app_context.js"; -import keyboardActionsService from "../services/keyboard_actions.js"; -import noteCreateService from "../services/note_create.js"; import DeletedTypeWidget from "./type_widgets/deleted.js"; import ReadOnlyTextTypeWidget from "./type_widgets/read_only_text.js"; import ReadOnlyCodeTypeWidget from "./type_widgets/read_only_code.js"; import NoneTypeWidget from "./type_widgets/none.js"; -import attributeService from "../services/attributes.js"; import NoteMapTypeWidget from "./type_widgets/note_map.js"; -import attributeRenderer from "../services/attribute_renderer.js"; +import IframeTypeWidget from "./type_widgets/iframe.js"; const TPL = `
@@ -54,7 +56,8 @@ const typeWidgetClasses = { 'canvas': CanvasTypeWidget, 'protected-session': ProtectedSessionTypeWidget, 'book': BookTypeWidget, - 'note-map': NoteMapTypeWidget + 'note-map': NoteMapTypeWidget, + 'iframe': IframeTypeWidget }; export default class NoteDetailWidget extends NoteContextAwareWidget { @@ -154,7 +157,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { // https://github.com/zadam/trilium/issues/2522 this.$widget.toggleClass("full-height", !this.noteContext.hasNoteList() - && ['editable-text', 'editable-code', 'canvas'].includes(this.type) + && ['editable-text', 'editable-code', 'canvas', 'iframe'].includes(this.type) && this.mime !== 'text/x-sqlite;schema=trilium'); } diff --git a/src/public/app/widgets/note_type.js b/src/public/app/widgets/note_type.js index e3206ebc0..0ce02b83f 100644 --- a/src/public/app/widgets/note_type.js +++ b/src/public/app/widgets/note_type.js @@ -12,8 +12,9 @@ const NOTE_TYPES = [ { type: "relation-map", mime: "application/json", title: "Relation Map", selectable: true }, { type: "render", mime: '', title: "Render Note", selectable: true }, { type: "canvas", mime: 'application/json', title: "Canvas", selectable: true }, - { type: "book", mime: '', title: "Book", selectable: true }, { type: "mermaid", mime: 'text/mermaid', title: "Mermaid Diagram", selectable: true }, + { type: "book", mime: '', title: "Book", selectable: true }, + { type: "iframe", mime: '', title: "IFrame", selectable: true }, { type: "code", mime: 'text/plain', title: "Code", selectable: true } ]; diff --git a/src/public/app/widgets/note_wrapper.js b/src/public/app/widgets/note_wrapper.js index 80da4558c..53f734bfb 100644 --- a/src/public/app/widgets/note_wrapper.js +++ b/src/public/app/widgets/note_wrapper.js @@ -36,7 +36,7 @@ export default class NoteWrapperWidget extends FlexContainer { const note = this.noteContext?.note; this.$widget.toggleClass("full-content-width", - ['image', 'mermaid', 'book', 'render', 'canvas'].includes(note?.type) + ['image', 'mermaid', 'book', 'render', 'canvas', 'iframe'].includes(note?.type) || !!note?.hasLabel('fullContentWidth') ); } diff --git a/src/public/app/widgets/type_widgets/canvas.js b/src/public/app/widgets/type_widgets/canvas.js index 23e8cdbbd..0512e3ede 100644 --- a/src/public/app/widgets/type_widgets/canvas.js +++ b/src/public/app/widgets/type_widgets/canvas.js @@ -336,6 +336,10 @@ export default class ExcalidrawTypeWidget extends TypeWidget { setDimensions(dimensions); const onResize = () => { + if (this.note?.type !== 'canvas') { + return; + } + const dimensions = { width: excalidrawWrapperRef.current.getBoundingClientRect().width, height: excalidrawWrapperRef.current.getBoundingClientRect().height diff --git a/src/public/app/widgets/type_widgets/iframe.js b/src/public/app/widgets/type_widgets/iframe.js new file mode 100644 index 000000000..9eaab65f5 --- /dev/null +++ b/src/public/app/widgets/type_widgets/iframe.js @@ -0,0 +1,67 @@ +import TypeWidget from "./type_widget.js"; +import attributeService from "../../services/attributes.js"; + +const TPL = ` +
+
+

This help note is shown because this note of type IFrame HTML doesn't have required label to function properly.

+ +

Please create label with a URL address you want to embed, e.g. #iframeSrc="http://www.google.com"

+
+ + +
`; + +export default class IframeTypeWidget extends TypeWidget { + static getType() { return "iframe"; } + + doRender() { + this.$widget = $(TPL); + this.$noteDetailIframeHelp = this.$widget.find('.note-detail-iframe-help'); + this.$noteDetailIframeContent = this.$widget.find('.note-detail-iframe-content'); + + window.addEventListener('resize', () => this.setDimensions(), false); + + super.doRender(); + } + + async doRefresh(note) { + this.$widget.show(); + this.$noteDetailIframeHelp.hide(); + this.$noteDetailIframeContent.hide(); + + const iframeSrc = this.note.getLabelValue('iframeSrc'); + + if (iframeSrc) { + this.$noteDetailIframeContent + .show() + .attr("src", iframeSrc); + } + else { + this.$noteDetailIframeContent.hide(); + this.$noteDetailIframeHelp.show(); + } + + this.setDimensions(); + + setTimeout(() => this.setDimensions(), 1000); + } + + cleanup() { + this.$noteDetailIframeContent.removeAttribute("src"); + } + + setDimensions() { + const $parent = this.$widget; + + this.$noteDetailIframeContent + .height($parent.height()) + .width($parent.width()); + } + + entitiesReloadedEvent({loadResults}) { + if (loadResults.getAttributes().find(attr => attr.name === 'iframeSrc' && attributeService.isAffecting(attr, this.noteContext.note))) { + this.refresh(); + } + } +} diff --git a/src/services/note_types.js b/src/services/note_types.js index cdc890162..5fc923686 100644 --- a/src/services/note_types.js +++ b/src/services/note_types.js @@ -1,13 +1,14 @@ module.exports = [ - 'text', - 'code', - 'render', - 'file', - 'image', - 'search', - 'relation-map', - 'book', + 'text', + 'code', + 'render', + 'file', + 'image', + 'search', + 'relation-map', + 'book', 'note-map', 'mermaid', - 'canvas' -]; \ No newline at end of file + 'canvas', + 'iframe' +]; diff --git a/src/services/notes.js b/src/services/notes.js index 5e3670727..0a23c72eb 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -55,7 +55,7 @@ function deriveMime(type, mime) { mime = 'text/plain'; } else if (['relation-map', 'search', 'canvas'].includes(type)) { mime = 'application/json'; - } else if (['render', 'book'].includes(type)) { + } else if (['render', 'book', 'iframe'].includes(type)) { mime = ''; } else { mime = 'application/octet-stream'; From 01155ad535740e91e12b0a8adc3a010c4220385e Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 29 May 2022 17:42:09 +0200 Subject: [PATCH 02/23] use webview instead of iframe --- src/public/app/widgets/type_widgets/iframe.js | 2 +- src/services/window.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/public/app/widgets/type_widgets/iframe.js b/src/public/app/widgets/type_widgets/iframe.js index 9eaab65f5..db0141af4 100644 --- a/src/public/app/widgets/type_widgets/iframe.js +++ b/src/public/app/widgets/type_widgets/iframe.js @@ -9,7 +9,7 @@ const TPL = `

Please create label with a URL address you want to embed, e.g. #iframeSrc="http://www.google.com"

- + `; export default class IframeTypeWidget extends TypeWidget { diff --git a/src/services/window.js b/src/services/window.js index 47aff5ad4..89a0a0aa1 100644 --- a/src/services/window.js +++ b/src/services/window.js @@ -67,7 +67,8 @@ async function createMainWindow() { enableRemoteModule: true, nodeIntegration: true, contextIsolation: false, - spellcheck: spellcheckEnabled + spellcheck: spellcheckEnabled, + webviewTag: true }, frame: optionService.getOptionBool('nativeTitleBarVisible'), icon: getIcon() From cce3f9a700b4b5f159989ee6c078793a176dff74 Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 29 May 2022 21:44:26 +0200 Subject: [PATCH 03/23] TOC widget WIP --- src/public/app/layouts/desktop_layout.js | 2 + src/public/app/widgets/collapsible_widget.js | 3 + src/public/app/widgets/toc.js | 258 +++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 src/public/app/widgets/toc.js diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index 2723076ce..446f19a75 100644 --- a/src/public/app/layouts/desktop_layout.js +++ b/src/public/app/layouts/desktop_layout.js @@ -49,6 +49,7 @@ import NoteWrapperWidget from "../widgets/note_wrapper.js"; import BacklinksWidget from "../widgets/backlinks.js"; import SharedInfoWidget from "../widgets/shared_info.js"; import FindWidget from "../widgets/find.js"; +import TocWidget from "../widgets/toc.js"; export default class DesktopLayout { constructor(customWidgets) { @@ -169,6 +170,7 @@ export default class DesktopLayout { .child(...this.customWidgets.get('center-pane')) ) .child(new RightPaneContainer() + .child(new TocWidget()) .child(...this.customWidgets.get('right-pane')) ) ) diff --git a/src/public/app/widgets/collapsible_widget.js b/src/public/app/widgets/collapsible_widget.js index e5563fb7f..bbe60345b 100644 --- a/src/public/app/widgets/collapsible_widget.js +++ b/src/public/app/widgets/collapsible_widget.js @@ -9,6 +9,9 @@ const WIDGET_TPL = ` `; +/** + * TODO: rename, it's not collapsible anymore + */ export default class CollapsibleWidget extends NoteContextAwareWidget { get widgetTitle() { return "Untitled widget"; } diff --git a/src/public/app/widgets/toc.js b/src/public/app/widgets/toc.js new file mode 100644 index 000000000..5a181686f --- /dev/null +++ b/src/public/app/widgets/toc.js @@ -0,0 +1,258 @@ +/** + * Table of contents widget + * (c) Antonio Tejada 2022 + * + * By design there's no support for non-sensical or malformed constructs: + * - headings inside elements (eg Trilium allows headings inside tables, but + * not inside lists) + * - nested headings when using raw HTML

+ * - malformed headings when using raw HTML

+ * - etc. + * + * In those cases the generated TOC may be incorrect or the navigation may lead + * to the wrong heading (although what "right" means in those cases is not + * clear), but it won't crash. + */ + +import attributeService from "../services/attributes.js"; +import CollapsibleWidget from "./collapsible_widget.js"; + +const TPL = `
+ + + +
`; + +/** + * Find a heading node in the parent's children given its index. + * + * @param {Element} parent Parent node to find a headingIndex'th in. + * @param {uint} headingIndex Index for the heading + * @returns {Element|null} Heading node with the given index, null couldn't be + * found (ie malformed like nested headings, etc) + */ +function findHeadingNodeByIndex(parent, headingIndex) { + let headingNode = null; + for (let i = 0; i < parent.childCount; ++i) { + let child = parent.getChild(i); + + // Headings appear as flattened top level children in the CKEditor + // document named as "heading" plus the level, eg "heading2", + // "heading3", "heading2", etc and not nested wrt the heading level. If + // a heading node is found, decrement the headingIndex until zero is + // reached + if (child.name.startsWith("heading")) { + if (headingIndex === 0) { + headingNode = child; + break; + } + headingIndex--; + } + } + + return headingNode; +} + +function findHeadingElementByIndex(parent, headingIndex) { + let headingElement = null; + for (let i = 0; i < parent.children.length; ++i) { + const child = parent.children[i]; + // 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) { + headingElement = child; + break; + } + headingIndex--; + } + } + return headingElement; +} + +export default class TocWidget extends CollapsibleWidget { + get widgetTitle() { + return "Table of Contents"; + } + + isEnabled() { + return super.isEnabled() + && this.note.type === 'text' + && !this.note.hasLabel('noTocWidget'); + } + + async doRenderBody() { + this.$body.empty().append($(TPL)); + this.$toc = this.$body.find('.toc'); + } + + async refreshWithNote(note) { + let toc = ""; + // 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); + } + + this.$toc.html(toc); + } + + /** + * Builds a jquery table of contents. + * + * @param {String} html Note's html content + * @returns {jQuery} ordered list table of headings, nested by heading level + * with an onclick event that will cause the document to scroll to + * the desired position. + */ + getToc(html) { + // Regular expression for headings

...

using non-greedy + // matching and backreferences + const headingTagsRegex = /(.*?)<\/h\1>/g; + + // Use jquery to build the table rather than html text, since it makes + // it easier to set the onclick event that will be executed with the + // right captured callback context + const $toc = $("
    "); + // 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) { + // + // Nest/unnest whatever necessary number of ordered lists + // + const newLevel = m[1]; + const levelDelta = newLevel - curLevel; + if (levelDelta > 0) { + // Open as many lists as newLevel - curLevel + for (let i = 0; i < levelDelta; i++) { + const $ol = $("
      "); + $ols[$ols.length - 1].append($ol); + $ols.push($ol); + } + } else if (levelDelta < 0) { + // Close as many lists as curLevel - newLevel + for (let i = 0; i < -levelDelta; ++i) { + $ols.pop(); + } + } + curLevel = newLevel; + + // + // Create the list item and set up the click callback + // + const $li = $('
    1. ' + m[2] + '
    2. '); + // XXX Do this with CSS? How to inject CSS in doRender? + $li.hover(function () { + $(this).css("font-weight", "bold"); + }).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(); + } + } + }); + $ols[$ols.length - 1].append($li); + } + + return $toc; + } + + 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))) { + + await this.refresh(); + } + } +} From dcf31f8f95872652a908827cdf61fe1a8e5839d2 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 30 May 2022 17:45:59 +0200 Subject: [PATCH 04/23] toc fixes --- package-lock.json | 4 +- src/public/app/widgets/basic_widget.js | 12 ++ src/public/app/widgets/collapsible_widget.js | 4 - .../containers/right_pane_container.js | 21 +- src/public/app/widgets/toc.js | 196 ++++++++++-------- src/public/stylesheets/style.css | 5 +- 6 files changed, 137 insertions(+), 105 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6424b4f78..30a0dbed2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/src/public/app/widgets/basic_widget.js b/src/public/app/widgets/basic_widget.js index be1f09e93..5c8cccdeb 100644 --- a/src/public/app/widgets/basic_widget.js +++ b/src/public/app/widgets/basic_widget.js @@ -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"); } diff --git a/src/public/app/widgets/collapsible_widget.js b/src/public/app/widgets/collapsible_widget.js index bbe60345b..0d108f27e 100644 --- a/src/public/app/widgets/collapsible_widget.js +++ b/src/public/app/widgets/collapsible_widget.js @@ -35,8 +35,4 @@ export default class CollapsibleWidget extends NoteContextAwareWidget { /** for overriding */ async doRenderBody() {} - - isExpanded() { - return this.$bodyWrapper.hasClass("show"); - } } diff --git a/src/public/app/widgets/containers/right_pane_container.js b/src/public/app/widgets/containers/right_pane_container.js index a927045f3..204c48cb4 100644 --- a/src/public/app/widgets/containers/right_pane_container.js +++ b/src/public/app/widgets/containers/right_pane_container.js @@ -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(); + } + } } diff --git a/src/public/app/widgets/toc.js b/src/public/app/widgets/toc.js index 5a181686f..adde8805b 100644 --- a/src/public/app/widgets/toc.js +++ b/src/public/app/widgets/toc.js @@ -26,11 +26,11 @@ const TPL = `
      } .toc ol { - padding-left: 20px; + padding-left: 25px; } .toc > ol { - padding-left: 0; + padding-left: 10px; } @@ -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(); } diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index 647373b6d..7797f4c3a 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -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 { From f19adf3ee0d8ddf6e7dba7aec566ef7180c8c83b Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 30 May 2022 20:50:53 +0200 Subject: [PATCH 05/23] add the ability to sort notes by folders first, closes #2649 --- src/public/app/dialogs/sort_child_notes.js | 3 ++- src/public/app/widgets/toc.js | 2 -- src/routes/api/notes.js | 6 +++--- src/views/dialogs/sort_child_notes.ejs | 11 +++++++++++ 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/public/app/dialogs/sort_child_notes.js b/src/public/app/dialogs/sort_child_notes.js index f6a28dd5f..e855b3fa2 100644 --- a/src/public/app/dialogs/sort_child_notes.js +++ b/src/public/app/dialogs/sort_child_notes.js @@ -9,8 +9,9 @@ let parentNoteId = null; $form.on('submit', async () => { const sortBy = $form.find("input[name='sort-by']:checked").val(); const sortDirection = $form.find("input[name='sort-direction']:checked").val(); + const foldersFirst = $form.find("input[name='sort-folders-first']").is(":checked"); - await server.put(`notes/${parentNoteId}/sort-children`, {sortBy, sortDirection}); + await server.put(`notes/${parentNoteId}/sort-children`, {sortBy, sortDirection, foldersFirst}); utils.closeActiveDialog(); }); diff --git a/src/public/app/widgets/toc.js b/src/public/app/widgets/toc.js index adde8805b..3a95620a3 100644 --- a/src/public/app/widgets/toc.js +++ b/src/public/app/widgets/toc.js @@ -76,8 +76,6 @@ function findHeadingElementByIndex(parent, headingIndex) { // heading level. If a heading node is found, decrement the headingIndex // until zero is reached - console.log(child.tagName, headingIndex); - if (child.tagName.match(/H\d+/i) !== null) { if (headingIndex === 0) { headingElement = child; diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js index 5f334edc8..2b7ccf595 100644 --- a/src/routes/api/notes.js +++ b/src/routes/api/notes.js @@ -94,13 +94,13 @@ function undeleteNote(req) { function sortChildNotes(req) { const noteId = req.params.noteId; - const {sortBy, sortDirection} = req.body; + const {sortBy, sortDirection, foldersFirst} = req.body; - log.info(`Sorting '${noteId}' children with ${sortBy} ${sortDirection}`); + log.info(`Sorting '${noteId}' children with ${sortBy} ${sortDirection}, foldersFirst=${foldersFirst}`); const reverse = sortDirection === 'desc'; - treeService.sortNotes(noteId, sortBy, reverse); + treeService.sortNotes(noteId, sortBy, reverse, foldersFirst); } function protectNote(req) { diff --git a/src/views/dialogs/sort_child_notes.ejs b/src/views/dialogs/sort_child_notes.ejs index 36c6475e0..ed38a0636 100644 --- a/src/views/dialogs/sort_child_notes.ejs +++ b/src/views/dialogs/sort_child_notes.ejs @@ -50,6 +50,17 @@ descending
      + +
      + +
      Folders
      + +
      + + +
      From 7d76fb8bf556a3856ea828196518a503a69a2880 Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 8 Jun 2022 22:52:17 +0200 Subject: [PATCH 22/23] merge fix --- src/services/bulk_actions.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/services/bulk_actions.js b/src/services/bulk_actions.js index 85d24a24b..21d78bbae 100644 --- a/src/services/bulk_actions.js +++ b/src/services/bulk_actions.js @@ -1,8 +1,9 @@ -const log = require("./log.js"); -const noteRevisionService = require("./note_revisions.js"); -const becca = require("../becca/becca.js"); -const cloningService = require("./cloning.js"); -const branchService = require("./branches.js"); +const log = require("./log"); +const noteRevisionService = require("./note_revisions"); +const becca = require("../becca/becca"); +const cloningService = require("./cloning"); +const branchService = require("./branches"); +const utils = require("./utils"); const ACTION_HANDLERS = { addLabel: (action, note) => { @@ -12,7 +13,9 @@ const ACTION_HANDLERS = { note.addRelation(action.relationName, action.targetNoteId); }, deleteNote: (action, note) => { - note.markAsDeleted(); + const deleteId = 'searchbulkaction-' + utils.randomString(10); + + note.deleteNote(deleteId); }, deleteNoteRevisions: (action, note) => { noteRevisionService.eraseNoteRevisions(note.getNoteRevisions().map(rev => rev.noteRevisionId)); From 5fdb462ed5ebec37b69139ec09a02fbf314d8f80 Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 8 Jun 2022 23:44:43 +0200 Subject: [PATCH 23/23] fix activating ribbon tabs after note switch --- src/public/app/widgets/containers/ribbon_container.js | 6 ++++++ src/public/app/widgets/ribbon_widgets/search_definition.js | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/public/app/widgets/containers/ribbon_container.js b/src/public/app/widgets/containers/ribbon_container.js index a344bef3a..c5b371538 100644 --- a/src/public/app/widgets/containers/ribbon_container.js +++ b/src/public/app/widgets/containers/ribbon_container.js @@ -195,6 +195,12 @@ export default class RibbonContainer extends NoteContextAwareWidget { } } + async noteSwitched() { + this.lastActiveComponentId = null; + + await super.noteSwitched(); + } + async refreshWithNote(note, noExplicitActivation = false) { this.lastNoteType = note.type; diff --git a/src/public/app/widgets/ribbon_widgets/search_definition.js b/src/public/app/widgets/ribbon_widgets/search_definition.js index 014af8602..47b1ec543 100644 --- a/src/public/app/widgets/ribbon_widgets/search_definition.js +++ b/src/public/app/widgets/ribbon_widgets/search_definition.js @@ -168,6 +168,10 @@ const OPTION_CLASSES = [ ]; export default class SearchDefinitionWidget extends NoteContextAwareWidget { + get name() { + return "searchDefinition"; + } + isEnabled() { return this.note && this.note.type === 'search'; }