From 87f436c6eadeaca0527e596f927922a469dcf599 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 9 May 2022 23:13:34 +0200 Subject: [PATCH 001/250] search dialog WIP from custom widget from antoniotejada --- docs/frontend_api/FrontendScriptApi.html | 755 ++++++++++++++---- .../services_frontend_script_api.js.html | 59 +- package-lock.json | 14 +- package.json | 3 +- src/public/app/layouts/desktop_layout.js | 2 + src/public/app/services/entrypoints.js | 23 - .../app/services/frontend_script_api.js | 59 +- src/public/app/services/tab_manager.js | 8 + src/public/app/widgets/find.js | 556 +++++++++++++ 9 files changed, 1280 insertions(+), 199 deletions(-) create mode 100644 src/public/app/widgets/find.js diff --git a/docs/frontend_api/FrontendScriptApi.html b/docs/frontend_api/FrontendScriptApi.html index 4a1c6b2da..6000fae0f 100644 --- a/docs/frontend_api/FrontendScriptApi.html +++ b/docs/frontend_api/FrontendScriptApi.html @@ -1671,7 +1671,7 @@ -

addTextToActiveTabEditor(text)

+

addTextToActiveContextEditor(text)

@@ -1772,7 +1772,146 @@
Source:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

addTextToActiveTabEditor(text)

+ + + + + + +
+ Adds given text to the editor cursor +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
text + + +string + + + + this must be clear text, HTML is not supported.
+ + + + + + +
+ + + + + + + + + + + + + + + + +
Deprecated:
  • use addTextToActiveContextEditor() instead
+ + + + + + + + + + + +
Source:
+
@@ -1928,7 +2067,7 @@
Source:
@@ -2479,114 +2618,7 @@ -

getActiveNoteDetailWidget() → {Promise.<NoteDetailWidget>}

- - - - - - -
- Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the -implementation of actual widget type. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - - - -
-
- Type -
-
- -Promise.<NoteDetailWidget> - - -
-
- - - - - - - - - - - - - -

getActiveTabCodeEditor() → {Promise.<CodeMirror>}

+

getActiveContextCodeEditor() → {Promise.<CodeMirror>}

@@ -2638,7 +2670,7 @@ implementation of actual widget type.
Source:
@@ -2696,7 +2728,7 @@ implementation of actual widget type. -

getActiveTabNote() → {NoteShort}

+

getActiveContextNote() → {NoteShort}

@@ -2744,7 +2776,438 @@ implementation of actual widget type.
Source:
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ active note (loaded into right pane) +
+ + + +
+
+ Type +
+
+ +NoteShort + + +
+
+ + + + + + + + + + + + + +

getActiveContextNotePath() → {Promise.<(string|null)>}

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ returns note path of active note or null if there isn't active note +
+ + + +
+
+ Type +
+
+ +Promise.<(string|null)> + + +
+
+ + + + + + + + + + + + + +

getActiveContextTextEditor() → {Promise.<CKEditor>}

+ + + + + + +
+ See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ instance of CKEditor +
+ + + +
+
+ Type +
+
+ +Promise.<CKEditor> + + +
+
+ + + + + + + + + + + + + +

getActiveNoteDetailWidget() → {Promise.<NoteDetailWidget>}

+ + + + + + +
+ Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the +implementation of actual widget type. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Promise.<NoteDetailWidget> + + +
+
+ + + + + + + + + + + + + +

getActiveTabNote() → {NoteShort}

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
Deprecated:
  • use getActiveContextNote() instead
+ + + + + + + + + + + +
Source:
+
@@ -2838,6 +3301,8 @@ implementation of actual widget type. +
Deprecated:
  • use getActiveContextNotePath() instead
+ @@ -2850,7 +3315,7 @@ implementation of actual widget type.
Source:
@@ -2908,7 +3373,7 @@ implementation of actual widget type. -

getActiveTabTextEditor(callbackopt) → {Promise.<CKEditor>}

+

getActiveTabTextEditor(callbackopt)

@@ -2975,7 +3440,7 @@ implementation of actual widget type. - deprecated (use returned promise): callback receiving "textEditor" instance + callback receiving "textEditor" instance @@ -3004,6 +3469,8 @@ implementation of actual widget type. +
Deprecated:
  • use getActiveContextTextEditor()
+ @@ -3016,7 +3483,7 @@ implementation of actual widget type.
Source:
@@ -3041,28 +3508,6 @@ implementation of actual widget type. -
Returns:
- - -
- instance of CKEditor -
- - - -
-
- Type -
-
- -Promise.<CKEditor> - - -
-
- - @@ -3175,7 +3620,7 @@ implementation of actual widget type.
Source:
@@ -3332,7 +3777,7 @@ implementation of actual widget type.
Source:
@@ -3487,7 +3932,7 @@ implementation of actual widget type.
Source:
@@ -3749,7 +4194,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4212,7 +4657,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4367,7 +4812,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4522,7 +4967,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4959,7 +5404,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5115,7 +5560,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5271,7 +5716,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5408,7 +5853,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5562,7 +6007,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -6503,7 +6948,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -6654,7 +7099,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -7020,7 +7465,7 @@ Typical use case is when new note has been created, we should wait until it is s
Source:
diff --git a/docs/frontend_api/services_frontend_script_api.js.html b/docs/frontend_api/services_frontend_script_api.js.html index 0f47ab437..8a4cbef6e 100644 --- a/docs/frontend_api/services_frontend_script_api.js.html +++ b/docs/frontend_api/services_frontend_script_api.js.html @@ -349,25 +349,61 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain /** * Adds given text to the editor cursor * + * @deprecated use addTextToActiveContextEditor() instead * @param {string} text - this must be clear text, HTML is not supported. * @method */ - this.addTextToActiveTabEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text}); + this.addTextToActiveTabEditor = text => { + console.warn("api.addTextToActiveTabEditor() is deprecated, use addTextToActiveContextEditor() instead."); + + return appContext.triggerCommand('addTextToActiveEditor', {text}); + }; + + /** + * Adds given text to the editor cursor + * + * @param {string} text - this must be clear text, HTML is not supported. + * @method + */ + this.addTextToActiveContextEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text}); + + /** + * @method + * @deprecated use getActiveContextNote() instead + * @returns {NoteShort} active note (loaded into right pane) + */ + this.getActiveTabNote = () => { + console.warn("api.getActiveTabNote() is deprecated, use getActiveContextNote() instead."); + + return appContext.tabManager.getActiveContextNote(); + }; /** * @method * @returns {NoteShort} active note (loaded into right pane) */ - this.getActiveTabNote = () => appContext.tabManager.getActiveContextNote(); + this.getActiveContextNote = () => appContext.tabManager.getActiveContextNote(); + + /** + * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. + * + * @deprecated use getActiveContextTextEditor() + * @method + * @param [callback] - callback receiving "textEditor" instance + */ + this.getActiveTabTextEditor = callback => { + console.warn("api.getActiveTabTextEditor() is deprecated, use getActiveContextTextEditor() instead."); + + return appContext.tabManager.getActiveContextTextEditor(callback); + }; /** * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. * * @method - * @param [callback] - deprecated (use returned promise): callback receiving "textEditor" instance * @returns {Promise<CKEditor>} instance of CKEditor */ - this.getActiveTabTextEditor = callback => new Promise(resolve => appContext.triggerCommand('executeInActiveTextEditor', {callback, resolve})); + this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContextTextEditor(); /** * See https://codemirror.net/doc/manual.html#api @@ -375,7 +411,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * @method * @returns {Promise<CodeMirror>} instance of CodeMirror */ - this.getActiveTabCodeEditor = () => new Promise(resolve => appContext.triggerCommand('executeInActiveCodeEditor', {callback: resolve})); + this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContextCodeEditor(); /** * Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the @@ -388,9 +424,20 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain /** * @method + * @deprecated use getActiveContextNotePath() instead * @returns {Promise<string|null>} returns note path of active note or null if there isn't active note */ - this.getActiveTabNotePath = () => appContext.tabManager.getActiveContextNotePath(); + this.getActiveTabNotePath = () => { + console.warn("api.getActiveTabNotePath() is deprecated, use getActiveContextNotePath() instead."); + + return appContext.tabManager.getActiveContextNotePath(); + }; + + /** + * @method + * @returns {Promise<string|null>} returns note path of active note or null if there isn't active note + */ + this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath(); /** * Returns component which owns given DOM element (the nearest parent component in DOM tree) diff --git a/package-lock.json b/package-lock.json index 626b48aab..1fd5ff38e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "commonmark": "0.30.0", "cookie-parser": "1.4.6", "csurf": "1.11.0", - "dayjs": "1.11.1", + "dayjs": "1.11.2", "ejs": "3.1.7", "electron-debug": "3.2.0", "electron-dl": "3.3.1", @@ -3200,9 +3200,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.1.tgz", - "integrity": "sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA==" + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", + "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" }, "node_modules/debug": { "version": "4.3.3", @@ -13413,9 +13413,9 @@ } }, "dayjs": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.1.tgz", - "integrity": "sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA==" + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", + "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" }, "debug": { "version": "4.3.3", diff --git a/package.json b/package.json index a140c3194..dc38d7816 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,10 @@ "commonmark": "0.30.0", "cookie-parser": "1.4.6", "csurf": "1.11.0", - "dayjs": "1.11.1", + "dayjs": "1.11.2", "ejs": "3.1.7", "electron-debug": "3.2.0", "electron-dl": "3.3.1", - "electron-find": "1.0.7", "electron-window-state": "5.0.3", "@electron/remote": "2.0.8", "express": "4.18.1", diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index 1b85cc71b..a5429521e 100644 --- a/src/public/app/layouts/desktop_layout.js +++ b/src/public/app/layouts/desktop_layout.js @@ -48,6 +48,7 @@ import BookmarkButtons from "../widgets/bookmark_buttons.js"; 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"; export default class DesktopLayout { constructor(customWidgets) { @@ -164,6 +165,7 @@ export default class DesktopLayout { .child(...this.customWidgets.get('node-detail-pane')) ) ) + .child(new FindWidget()) .child(...this.customWidgets.get('center-pane')) ) .child(new RightPaneContainer() diff --git a/src/public/app/services/entrypoints.js b/src/public/app/services/entrypoints.js index 42e4f79ee..10292e2c9 100644 --- a/src/public/app/services/entrypoints.js +++ b/src/public/app/services/entrypoints.js @@ -39,29 +39,6 @@ export default class Entrypoints extends Component { } } - findInTextCommand() { - if (!utils.isElectron()) { - return; - } - - const remote = utils.dynamicRequire('@electron/remote'); - const {FindInPage} = utils.dynamicRequire('electron-find'); - const findInPage = new FindInPage(remote.getCurrentWebContents(), { - offsetTop: 10, - offsetRight: 10, - boxBgColor: 'var(--main-background-color)', - boxShadowColor: '#000', - inputColor: 'var(--input-text-color)', - inputBgColor: 'var(--input-background-color)', - inputFocusColor: '#555', - textColor: 'var(--main-text-color)', - textHoverBgColor: '#555', - caseSelectedColor: 'var(--main-border-color)' - }); - - findInPage.openFindWindow(); - } - async createNoteIntoInboxCommand() { const inboxNote = await dateNoteService.getInboxNote(); diff --git a/src/public/app/services/frontend_script_api.js b/src/public/app/services/frontend_script_api.js index 2cd12f8bc..257fee0df 100644 --- a/src/public/app/services/frontend_script_api.js +++ b/src/public/app/services/frontend_script_api.js @@ -321,25 +321,61 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain /** * Adds given text to the editor cursor * + * @deprecated use addTextToActiveContextEditor() instead * @param {string} text - this must be clear text, HTML is not supported. * @method */ - this.addTextToActiveTabEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text}); + this.addTextToActiveTabEditor = text => { + console.warn("api.addTextToActiveTabEditor() is deprecated, use addTextToActiveContextEditor() instead."); + + return appContext.triggerCommand('addTextToActiveEditor', {text}); + }; + + /** + * Adds given text to the editor cursor + * + * @param {string} text - this must be clear text, HTML is not supported. + * @method + */ + this.addTextToActiveContextEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text}); + + /** + * @method + * @deprecated use getActiveContextNote() instead + * @returns {NoteShort} active note (loaded into right pane) + */ + this.getActiveTabNote = () => { + console.warn("api.getActiveTabNote() is deprecated, use getActiveContextNote() instead."); + + return appContext.tabManager.getActiveContextNote(); + }; /** * @method * @returns {NoteShort} active note (loaded into right pane) */ - this.getActiveTabNote = () => appContext.tabManager.getActiveContextNote(); + this.getActiveContextNote = () => appContext.tabManager.getActiveContextNote(); + + /** + * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. + * + * @deprecated use getActiveContextTextEditor() + * @method + * @param [callback] - callback receiving "textEditor" instance + */ + this.getActiveTabTextEditor = callback => { + console.warn("api.getActiveTabTextEditor() is deprecated, use getActiveContextTextEditor() instead."); + + return appContext.tabManager.getActiveContextTextEditor(callback); + }; /** * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. * * @method - * @param [callback] - deprecated (use returned promise): callback receiving "textEditor" instance * @returns {Promise} instance of CKEditor */ - this.getActiveTabTextEditor = callback => new Promise(resolve => appContext.triggerCommand('executeInActiveTextEditor', {callback, resolve})); + this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContextTextEditor(); /** * See https://codemirror.net/doc/manual.html#api @@ -347,7 +383,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * @method * @returns {Promise} instance of CodeMirror */ - this.getActiveTabCodeEditor = () => new Promise(resolve => appContext.triggerCommand('executeInActiveCodeEditor', {callback: resolve})); + this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContextCodeEditor(); /** * Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the @@ -360,9 +396,20 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain /** * @method + * @deprecated use getActiveContextNotePath() instead * @returns {Promise} returns note path of active note or null if there isn't active note */ - this.getActiveTabNotePath = () => appContext.tabManager.getActiveContextNotePath(); + this.getActiveTabNotePath = () => { + console.warn("api.getActiveTabNotePath() is deprecated, use getActiveContextNotePath() instead."); + + return appContext.tabManager.getActiveContextNotePath(); + }; + + /** + * @method + * @returns {Promise} returns note path of active note or null if there isn't active note + */ + this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath(); /** * Returns component which owns given DOM element (the nearest parent component in DOM tree) diff --git a/src/public/app/services/tab_manager.js b/src/public/app/services/tab_manager.js index 15f939bfe..1250b6948 100644 --- a/src/public/app/services/tab_manager.js +++ b/src/public/app/services/tab_manager.js @@ -193,6 +193,14 @@ export default class TabManager extends Component { return activeNote ? activeNote.type : null; } + async getActiveContextTextEditor(callback) { + return new Promise(resolve => appContext.triggerCommand('executeInActiveTextEditor', {callback, resolve})); + } + + async getActiveContextCodeEditor() { + return new Promise(resolve => appContext.triggerCommand('executeInActiveCodeEditor', {resolve})); + } + async switchToNoteContext(ntxId, notePath) { const noteContext = this.noteContexts.find(nc => nc.ntxId === ntxId) || await this.openEmptyTab(); diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js new file mode 100644 index 000000000..99b7e2f0e --- /dev/null +++ b/src/public/app/widgets/find.js @@ -0,0 +1,556 @@ +/** + * Find in note replacement for Trilium ctrl+f search + * (c) Antonio Tejada 2022 + * + * Features: + * - Find in writeable using ctrl+f and F3 + * - Tested on Trilium Desktop 0.50.3 + * + * Installation: + * - Create a code note of language JS Frontend with the contents of this file + * - Set the owned attributes (alt-a) to #widget + * - Set the owned attributes of any note you don't want to enable finding to + * #noFindWidget + * - Disable Ctrl+f shorcut in Trilium options + * + * Todo: + * - Refactoring/code cleanup + * - Case-sensitive option + * - Regexp option + * - Full word option + * - Find & Replace + * + * Note that many times some common code is replicated between CodeMirror and + * CKEditor codepaths because the CKEditor update is done inside a callback that + * is deferred so the code cannot be put outside of the callback or it will + * execute too early. + * + * See https://github.com/zadam/trilium/discussions/2806 for discussions + */ + +import NoteContextAwareWidget from "./note_context_aware_widget.js"; +import appContext from "../services/app_context.js"; + +function getNoteAttributeValue(note, attributeType, attributeName, defaultValue) { + let attribute = note.getAttribute(attributeType, attributeName); + + let attributeValue = (attribute != null) ? attribute.value : defaultValue; + + return attributeValue; +} + +const findWidgetDelayMillis = 200; +const waitForEnter = (findWidgetDelayMillis < 0); + +const TEMPLATE = `
+
+ +  case +  regexp + 0/0 +
+
`; + +const tag = "FindWidget"; +const debugLevels = ["error", "warn", "info", "log", "debug"]; +const debugLevel = "info"; + +let warn = function() {}; +if (debugLevel >= debugLevels.indexOf("warn")) { + warn = console.warn.bind(console, tag + ": "); +} + +let info = function() {}; +if (debugLevel >= debugLevels.indexOf("info")) { + info = console.info.bind(console, tag + ": "); +} + +let log = function() {}; +if (debugLevel >= debugLevels.indexOf("log")) { + log = console.log.bind(console, tag + ": "); +} + +let dbg = function() {}; +if (debugLevel >= debugLevels.indexOf("debug")) { + dbg = console.debug.bind(console, tag + ": "); +} + +function assert(e, msg) { + console.assert(e, tag + ": " + msg); +} + +function debugbreak() { + debugger; +} + + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +async function getActiveContextCodeEditor() { + return await appContext.tabManager.getActiveContextCodeEditor(); +} + +async function getActiveContextTextEditor() { + return await appContext.tabManager.getActiveContextTextEditor(); +} + +// ck-find-result and ck-find-result_selected are the styles ck-editor +// uses for highlighting matches, use the same one on CodeMirror +// for consistency +const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected"; +const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; + +export default class FindWidget extends NoteContextAwareWidget { + constructor(...args) { + super(...args); + this.$widget = $(TEMPLATE); + this.$findBox = this.$widget.find('#findBox'); + this.$input = this.$widget.find('#input'); + this.$curFound = this.$widget.find('#curFound'); + this.$numFound = this.$widget.find('#numFound'); + this.findResult = null; + this.prevFocus = null; + this.nedle = null; + let findWidget = this; + + // XXX Use api.bindGlobalShortcut? + $(window).keydown(async function (e){ + dbg("keydown on window " + e.key); + if ((e.key == 'F3') || + // Note that for ctrl+f to work, needs to be disabled in Trilium's + // shortcut config menu + // XXX Maybe not if using bindShorcut? + ((e.metaKey || e.ctrlKey) && ((e.key == 'f') || (e.key == 'F')))) { + + const note = appContext.tabManager.getActiveContextNote(); + // Only writeable text and code supported + const readOnly = note.getAttribute("label", "readOnly"); + if (!readOnly && ((note.type == "code") || (note.type == "text"))) { + if (findWidget.$findBox.is(":hidden")) { + + findWidget.$findBox.show(); + findWidget.$input.focus(); + findWidget.$numFound.text(0); + findWidget.$curFound.text(0); + + // Initialize the input field to the text selection, if any + if (note.type == "code") { + let codeEditor = getActiveContextCodeEditor(); + + // highlightSelectionMatches is the overlay that highlights + // the words under the cursor. This occludes the search + // markers style, save it, disable it. Will be restored when + // the focus is back into the note + findWidget.oldHighlightSelectionMatches = codeEditor.getOption("highlightSelectionMatches"); + codeEditor.setOption("highlightSelectionMatches", false); + + // Fill in the findbox with the current selection if any + const selectedText = codeEditor.getSelection() + if (selectedText != "") { + findWidget.$input.val(selectedText); + } + // Directly perform the search if there's some text to find, + // without delaying or waiting for enter + const needle = findWidget.$input.val(); + if (needle != "") { + findWidget.$input.select(); + await findWidget.performFind(needle); + } + } else { + const textEditor = await getActiveContextTextEditor(); + + const selection = textEditor.model.document.selection; + const range = selection.getFirstRange(); + + for (const item of range.getItems()) { + // Fill in the findbox with the current selection if + // any + findWidget.$input.val(item.data); + break; + } + // Directly perform the search if there's some text to + // find, without delaying or waiting for enter + const needle = findWidget.$input.val(); + if (needle != "") { + findWidget.$input.select(); + await findWidget.performFind(needle); + } + } + } + e.preventDefault(); + return false; + } + } + return true; + }); + + findWidget.$input.keydown(async function (e) { + dbg("keydown on input " + e.key); + if ((e.metaKey || e.ctrlKey) && ((e.key == 'F') || (e.key == 'f'))) { + // If ctrl+f is pressed when the findbox is shown, select the + // whole input to find + findWidget.$input.select(); + } else if ((e.key == 'Enter') || (e.key == 'F3')) { + const needle = findWidget.$input.val(); + if (waitForEnter && (findWidget.needle != needle)) { + await findWidget.performFind(needle); + } + let numFound = parseInt(findWidget.$numFound.text()); + let curFound = parseInt(findWidget.$curFound.text()) - 1; + dbg("Finding " + curFound + "/" + numFound + " occurrence of " + findWidget.$input.val()); + if (numFound > 0) { + let delta = e.shiftKey ? -1 : 1; + let nextFound = curFound + delta; + // Wrap around + if (nextFound > numFound - 1) { + nextFound = 0; + } if (nextFound < 0) { + nextFound = numFound - 1; + } + + let needle = findWidget.$input.val(); + findWidget.$curFound.text(nextFound + 1); + + const note = appContext.tabManager.getActiveContextNote(); + if (note.type == "code") { + let codeEditor = getActiveContextCodeEditor(); + let doc = codeEditor.doc; + + // + // Dehighlight current, highlight & scrollIntoView next + // + + let marker = findWidget.findResult[curFound]; + let pos = marker.find(); + marker.clear(); + marker = doc.markText( + pos.from, pos.to, + { "className" : FIND_RESULT_CSS_CLASSNAME } + ); + findWidget.findResult[curFound] = marker; + + marker = findWidget.findResult[nextFound]; + pos = marker.find(); + marker.clear(); + marker = doc.markText( + pos.from, pos.to, + { "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME } + ); + findWidget.findResult[nextFound] = marker; + + codeEditor.scrollIntoView(pos.from); + } else { + assert(note.type == "text", "Expected text note, found " + note.type); + const textEditor = await getActiveContextTextEditor(); + + const model = textEditor.model; + const doc = model.document; + const root = doc.getRoot(); + // See + // Parameters are callback/text, options.matchCase=false, options.wholeWords=false + // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findcommand.js#L44 + // XXX Need to use the callback version for regexp + // needle = escapeRegExp(needle); + // cufFound wrap around assumes findNext and findPrevious + // wraparound, which is what they do + if (delta > 0) { + textEditor.execute('findNext', needle); + } else { + textEditor.execute('findPrevious', needle); + } + } + } + e.preventDefault(); + return false; + } else if (e.key == 'Escape') { + let numFound = parseInt(findWidget.$numFound.text()); + + const note = appContext.tabManager.getActiveContextNote(); + if (note.type == "code") { + let codeEditor = getActiveContextCodeEditor(); + + codeEditor.focus(); + } else { + assert(note.type == "text", "Expected text note, found " + note.type); + const textEditor = await getActiveContextTextEditor(); + textEditor.focus(); + } + } + // e.preventDefault(); + }); + + findWidget.$input.on('input', function (e) { + // XXX This should clear the previous search immediately in all cases + // (the search is stale when waitforenter but also while the + // delay is running for non waitforenter case) + if (!waitForEnter) { + // Clear the previous timeout if any, it's ok if timeoutId is + // null or undefined + clearTimeout(findWidget.timeoutId); + + // Defer the search a few millis so the search doesn't start + // immediately, as this can cause search word typing lag with + // one or two-char searchwords and long notes + // See https://github.com/antoniotejada/Trilium-FindWidget/issues/1 + const needle = findWidget.$input.val(); + findWidget.timeoutId = setTimeout(async function () { + findWidget.timeoutId = null; + await findWidget.performFind(needle); + }, findWidgetDelayMillis); + } + }); + + findWidget.$input.blur(async function () { + findWidget.$findBox.hide(); + + // Restore any state, if there's a current occurrence clear markers + // and scroll to and select the last occurrence + + // XXX Switching to a different tab with crl+tab doesn't invoke + // blur and leaves a stale search which then breaks when + // navigating it + let numFound = parseInt(findWidget.$numFound.text()); + let curFound = parseInt(findWidget.$curFound.text()) - 1; + const note = appContext.tabManager.getActiveContextNote(); + if (note.type == "code") { + let codeEditor = await getActiveContextCodeEditor(); + if (numFound > 0) { + let doc = codeEditor.doc; + let pos = findWidget.findResult[curFound].find(); + // Note setting the selection sets the cursor to + // the end of the selection and scrolls it into + // view + doc.setSelection(pos.from, pos.to); + // Clear all markers + codeEditor.operation(function() { + for (let i = 0; i < findWidget.findResult.length; ++i) { + let marker = findWidget.findResult[i]; + marker.clear(); + } + }); + } + // Restore the highlightSelectionMatches setting + codeEditor.setOption("highlightSelectionMatches", findWidget.oldHighlightSelectionMatches); + findWidget.findResult = null; + findWidget.needle = null; + } else { + assert(note.type == "text", "Expected text note, found " + note.type); + if (numFound > 0) { + const textEditor = await getActiveContextTextEditor(); + // Clear the markers and set the caret to the + // current occurrence + const model = textEditor.model; + let range = findWidget.findResult.results.get(curFound).marker.getRange(); + // From + // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92 + // XXX Roll our own since already done for codeEditor and + // will probably allow more refactoring? + let findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); + findAndReplaceEditing.state.clear(model); + findAndReplaceEditing.stop(); + model.change(writer => { + writer.setSelection(range, 0); + }); + textEditor.editing.view.scrollToTheSelection(); + findWidget.findResult = null; + findWidget.needle = null; + } else { + findWidget.findResult = null; + findWidget.needle = null; + } + } + }); + } + + async performTextNoteFind(needle) { + // Do this even if the needle is empty so the markers are cleared and + // the counters updated + const textEditor = await getActiveContextTextEditor(); + const model = textEditor.model; + let findResult = null; + let numFound = 0; + let curFound = -1; + + // Clear + let findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); + log("findAndReplace clearing"); + findAndReplaceEditing.state.clear(model); + log("findAndReplace stopping"); + findAndReplaceEditing.stop(); + if (needle != "") { + // Parameters are callback/text, options.matchCase=false, options.wholeWords=false + // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findcommand.js#L44 + // XXX Need to use the callback version for regexp + // needle = escapeRegExp(needle); + // let re = new RegExp(needle, 'gi'); + // let m = text.match(re); + // numFound = m ? m.length : 0; + log("findAndReplace starts"); + findResult = textEditor.execute('find', needle); + log("findAndReplace ends"); + numFound = findResult.results.length; + // Find the result beyond the cursor + log("findAndReplace positioning"); + let cursorPos = model.document.selection.getLastPosition(); + for (let i = 0; i < findResult.results.length; ++i) { + let marker = findResult.results.get(i).marker; + let fromPos = marker.getStart(); + if (fromPos.compareWith(cursorPos) != "before") { + curFound = i; + break; + } + } + log("findAndReplace positioned"); + } + + this.findResult = findResult; + this.$numFound.text(numFound); + // Calculate curfound if not already, highlight it as + // selected + if (numFound > 0) { + curFound = Math.max(0, curFound); + // XXX Do this accessing the private data? + // See + // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js + for (let i = 0 ; i < curFound; ++i) { + textEditor.execute('findNext', needle); + } + } + this.$curFound.text(curFound + 1); + this.needle = needle; + } + + async performCodeNoteFind(needle) { + let findResult = null; + let numFound = 0; + let curFound = -1; + + // See https://codemirror.net/addon/search/searchcursor.js for tips + let codeEditor = await getActiveContextCodeEditor(); + let doc = codeEditor.doc; + let text = doc.getValue(); + + // Clear all markers + if (this.findResult != null) { + const findWidget = this; + codeEditor.operation(function() { + for (let i = 0; i < findWidget.findResult.length; ++i) { + let marker = findWidget.findResult[i]; + marker.clear(); + } + }); + } + + if (needle != "") { + needle = escapeRegExp(needle); + + // Find and highlight matches + let re = new RegExp(needle, 'gi'); + let curLine = 0; + let curChar = 0; + let curMatch = null; + findResult = []; + // All those markText take several seconds on eg this ~500-line + // script, batch them inside an operation so they become + // unnoticeable. Alternatively, an overlay could be used, see + // https://codemirror.net/addon/search/match-highlighter.js ? + codeEditor.operation(function() { + for (let i = 0; i < text.length; ++i) { + // Fetch next match if it's the first time or + // if past the current match start + if ((curMatch == null) || (curMatch.index < i)) { + curMatch = re.exec(text); + if (curMatch == null) { + // No more matches + break; + } + } + // Create a non-selected highlight marker for the match, the + // selected marker highlight will be done later + if (i == curMatch.index) { + let fromPos = { "line" : curLine, "ch" : curChar }; + // XXX If multiline is supported, this needs to + // recalculate curLine since the match may span + // lines + let toPos = { "line" : curLine, "ch" : curChar + curMatch[0].length}; + // XXX or css = "color: #f3" + let marker = doc.markText( fromPos, toPos, { "className" : FIND_RESULT_CSS_CLASSNAME }); + findResult.push(marker); + + // Set the first match beyond the cursor as current + // match + if (curFound == -1) { + let cursorPos = codeEditor.getCursor(); + if ((fromPos.line > cursorPos.line) || + ((fromPos.line == cursorPos.line) && + (fromPos.ch >= cursorPos.ch))){ + curFound = numFound; + } + } + + numFound++; + } + // Do line and char position tracking + if (text[i] == "\n") { + curLine++; + curChar = 0; + } else { + curChar++; + } + } + }); + } + + this.findResult = findResult; + this.$numFound.text(numFound); + // Calculate curfound if not already, highlight it as selected + if (numFound > 0) { + curFound = Math.max(0, curFound) + let marker = findResult[curFound]; + let pos = marker.find(); + codeEditor.scrollIntoView(pos.to); + marker.clear(); + findResult[curFound] = doc.markText( pos.from, pos.to, + { "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME } + ); + } + this.$curFound.text(curFound + 1); + this.needle = needle; + } + + async performFind(needle) { + const note = appContext.tabManager.getActiveContextNote(); + if (note.type == "code") { + await this.performCodeNoteFind(needle); + } else { + assert(note.type == "text", "Expected text note, found " + note.type); + await this.performTextNoteFind(needle); + } + } + + isEnabled() { + dbg("isEnabled"); + return super.isEnabled() + && ((this.note.type === 'text') || (this.note.type === 'code')) + && !this.note.hasLabel('noFindWidget'); + } + + doRender() { + dbg("doRender"); + this.$findBox.hide(); + return this.$widget; + } + + async refreshWithNote(note) { + dbg("refreshWithNote"); + } + + async entitiesReloadedEvent({loadResults}) { + dbg("entitiesReloadedEvent"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + this.refresh(); + } + } +} From 9a04a76672df7301c3feb81fcaea07c6ff7a917c Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 10 May 2022 23:42:28 +0200 Subject: [PATCH 002/250] remove recent notes from entity changes migration, #2842 (cherry picked from commit bbbad6776468258355dcb9d821495f00cea1134c) --- db/migrations/0195__remove_recent_notes_from_entity_changes.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 db/migrations/0195__remove_recent_notes_from_entity_changes.sql diff --git a/db/migrations/0195__remove_recent_notes_from_entity_changes.sql b/db/migrations/0195__remove_recent_notes_from_entity_changes.sql new file mode 100644 index 000000000..834db7fe9 --- /dev/null +++ b/db/migrations/0195__remove_recent_notes_from_entity_changes.sql @@ -0,0 +1,2 @@ +-- removing potential remnants of recent notes in entity changes, see https://github.com/zadam/trilium/issues/2842 +DELETE FROM entity_changes WHERE entityName = 'recent_notes'; From 77f8474d83a1d122fb1b1c73687ff5ca39488a50 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 10 May 2022 23:45:06 +0200 Subject: [PATCH 003/250] don't fill recent notes into entity_changes (cherry picked from commit 963c18b8e4d04bce65623f9542a18cdeeffa2d75) --- src/services/entity_changes.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/entity_changes.js b/src/services/entity_changes.js index 209e0c7f5..62eabc944 100644 --- a/src/services/entity_changes.js +++ b/src/services/entity_changes.js @@ -135,7 +135,6 @@ function fillAllEntityChanges() { fillEntityChanges("branches", "branchId"); fillEntityChanges("note_revisions", "noteRevisionId"); fillEntityChanges("note_revision_contents", "noteRevisionId"); - fillEntityChanges("recent_notes", "noteId"); fillEntityChanges("attributes", "attributeId"); fillEntityChanges("etapi_tokens", "etapiTokenId"); fillEntityChanges("options", "name", 'isSynced = 1'); From c421ee79b0df62f452a8802e0ea9ae9a83005dde Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 10 May 2022 23:47:25 +0200 Subject: [PATCH 004/250] increase DB version --- src/services/app_info.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/app_info.js b/src/services/app_info.js index 9ee16221f..60fc6bc68 100644 --- a/src/services/app_info.js +++ b/src/services/app_info.js @@ -4,7 +4,7 @@ const build = require('./build'); const packageJson = require('../../package'); const {TRILIUM_DATA_DIR} = require('./data_dir'); -const APP_DB_VERSION = 194; +const APP_DB_VERSION = 195; const SYNC_VERSION = 25; const CLIPPER_PROTOCOL_VERSION = "1.0"; From 078fc420b0241313150cafb8c5b9ae0033ea0240 Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 14 May 2022 21:06:14 +0200 Subject: [PATCH 005/250] findwidget merge from upstream --- src/public/app/widgets/find.js | 93 +++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 24 deletions(-) diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 99b7e2f0e..80c7fdb6a 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -42,11 +42,15 @@ function getNoteAttributeValue(note, attributeType, attributeName, defaultValue) const findWidgetDelayMillis = 200; const waitForEnter = (findWidgetDelayMillis < 0); +// tabIndex=-1 on the checkbox labels is necessary so when clicking on the label +// the focusout handler is called with relatedTarget equal to the label instead +// of undefined. It's -1 instead of > 0 so they don't tabstop const TEMPLATE = `
-  case -  regexp + + + 0/0
`; @@ -110,9 +114,11 @@ export default class FindWidget extends NoteContextAwareWidget { this.$input = this.$widget.find('#input'); this.$curFound = this.$widget.find('#curFound'); this.$numFound = this.$widget.find('#numFound'); + this.$caseCheck = this.$widget.find("#caseCheck"); + this.$wordCheck = this.$widget.find("#wordCheck"); this.findResult = null; this.prevFocus = null; - this.nedle = null; + this.needle = null; let findWidget = this; // XXX Use api.bindGlobalShortcut? @@ -245,20 +251,14 @@ export default class FindWidget extends NoteContextAwareWidget { assert(note.type == "text", "Expected text note, found " + note.type); const textEditor = await getActiveContextTextEditor(); - const model = textEditor.model; - const doc = model.document; - const root = doc.getRoot(); - // See - // Parameters are callback/text, options.matchCase=false, options.wholeWords=false - // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findcommand.js#L44 - // XXX Need to use the callback version for regexp - // needle = escapeRegExp(needle); - // cufFound wrap around assumes findNext and findPrevious - // wraparound, which is what they do + // There are no parameters for findNext/findPrev + // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js#L57 + // curFound wrap around above assumes findNext and + // findPrevious wraparound, which is what they do if (delta > 0) { - textEditor.execute('findNext', needle); + textEditor.execute('findNext'); } else { - textEditor.execute('findPrevious', needle); + textEditor.execute('findPrevious'); } } } @@ -295,14 +295,37 @@ export default class FindWidget extends NoteContextAwareWidget { // one or two-char searchwords and long notes // See https://github.com/antoniotejada/Trilium-FindWidget/issues/1 const needle = findWidget.$input.val(); + const matchCase = findWidget.$caseCheck.prop("checked"); + const wholeWord = findWidget.$wordCheck.prop("checked"); findWidget.timeoutId = setTimeout(async function () { findWidget.timeoutId = null; - await findWidget.performFind(needle); + await findWidget.performFind(needle, matchCase, wholeWord); }, findWidgetDelayMillis); } }); - findWidget.$input.blur(async function () { + findWidget.$caseCheck.change(function() { + log("caseCheck change"); + findWidget.performFind(); + }); + + findWidget.$wordCheck.change(function() { + log("wordCheck change"); + findWidget.performFind(); + }); + + // Note blur doesn't bubble to parent div, but the parent div needs to + // detect when any of the children are not focused and hide. Use + // focusout instead which does bubble to the parent div. + findWidget.$findBox.focusout(async function (e) { + // e.relatedTarget is the new focused element, note it can be null + // if nothing is being focused + log(`focusout ${e.target.id} related ${e.relatedTarget?.id}`); + if (findWidget.$findBox[0].contains(e.relatedTarget)) { + // The focused element is inside this div, ignore + log("focusout to child, ignoring"); + return; + } findWidget.$findBox.hide(); // Restore any state, if there's a current occurrence clear markers @@ -364,7 +387,7 @@ export default class FindWidget extends NoteContextAwareWidget { }); } - async performTextNoteFind(needle) { + async performTextNoteFind(needle, matchCase, wholeWord) { // Do this even if the needle is empty so the markers are cleared and // the counters updated const textEditor = await getActiveContextTextEditor(); @@ -388,7 +411,8 @@ export default class FindWidget extends NoteContextAwareWidget { // let m = text.match(re); // numFound = m ? m.length : 0; log("findAndReplace starts"); - findResult = textEditor.execute('find', needle); + const options = { "matchCase" : matchCase, "wholeWords" : wholeWord }; + findResult = textEditor.execute('find', needle, options); log("findAndReplace ends"); numFound = findResult.results.length; // Find the result beyond the cursor @@ -422,7 +446,7 @@ export default class FindWidget extends NoteContextAwareWidget { this.needle = needle; } - async performCodeNoteFind(needle) { + async performCodeNoteFind(needle, matchCase, wholeWord) { let findResult = null; let numFound = 0; let curFound = -1; @@ -447,7 +471,14 @@ export default class FindWidget extends NoteContextAwareWidget { needle = escapeRegExp(needle); // Find and highlight matches - let re = new RegExp(needle, 'gi'); + // Find and highlight matches + // XXX Using \\b and not using the unicode flag probably doesn't + // work with non ascii alphabets, findAndReplace uses a more + // complicated regexp, see + // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/utils.js#L145 + const wholeWordChar = wholeWord ? "\\b" : ""; + let re = new RegExp(wholeWordChar + needle + wholeWordChar, + 'g' + (matchCase ? '' : 'i')); let curLine = 0; let curChar = 0; let curMatch = null; @@ -520,13 +551,27 @@ export default class FindWidget extends NoteContextAwareWidget { this.needle = needle; } - async performFind(needle) { + /** + * Perform the find and highlight the find results. + * + * @param needle {string} optional parameter, taken from the input box if + * missing. + * @param matchCase {boolean} optional parameter, taken from the checkbox + * state if missing. + * @param wholeWord {boolean} optional parameter, taken from the checkbox + * state if missing. + */ + async performFind(needle, matchCase, wholeWord) { + needle = (needle == undefined) ? this.$input.val() : needle; + matchCase = (matchCase === undefined) ? this.$caseCheck.prop("checked") : matchCase; + wholeWord = (wholeWord === undefined) ? this.$wordCheck.prop("checked") : wholeWord; + log(`performFind needle:${needle} case:${matchCase} word:${wholeWord}`); const note = appContext.tabManager.getActiveContextNote(); if (note.type == "code") { - await this.performCodeNoteFind(needle); + await this.performCodeNoteFind(needle, matchCase, wholeWord); } else { assert(note.type == "text", "Expected text note, found " + note.type); - await this.performTextNoteFind(needle); + await this.performTextNoteFind(needle, matchCase, wholeWord); } } From 36308c307ba11d7e1d144c8bea25c6af413889b0 Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 14 May 2022 22:33:45 +0200 Subject: [PATCH 006/250] findwidget cleanup --- src/public/app/widgets/find.js | 158 ++++++++++----------------------- 1 file changed, 45 insertions(+), 113 deletions(-) diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 80c7fdb6a..9ac090400 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -1,6 +1,7 @@ /** * Find in note replacement for Trilium ctrl+f search * (c) Antonio Tejada 2022 + * https://github.com/antoniotejada/Trilium-FindWidget * * Features: * - Find in writeable using ctrl+f and F3 @@ -31,74 +32,29 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js"; import appContext from "../services/app_context.js"; -function getNoteAttributeValue(note, attributeType, attributeName, defaultValue) { - let attribute = note.getAttribute(attributeType, attributeName); - - let attributeValue = (attribute != null) ? attribute.value : defaultValue; - - return attributeValue; -} - const findWidgetDelayMillis = 200; const waitForEnter = (findWidgetDelayMillis < 0); // tabIndex=-1 on the checkbox labels is necessary so when clicking on the label // the focusout handler is called with relatedTarget equal to the label instead -// of undefined. It's -1 instead of > 0 so they don't tabstop -const TEMPLATE = `
-
- - - - - 0/0 -
+// of undefined. It's -1 instead of > 0, so they don't tabstop +const TEMPLATE = ` +
+
+ + + + + 0/0 +
`; -const tag = "FindWidget"; -const debugLevels = ["error", "warn", "info", "log", "debug"]; -const debugLevel = "info"; - -let warn = function() {}; -if (debugLevel >= debugLevels.indexOf("warn")) { - warn = console.warn.bind(console, tag + ": "); -} - -let info = function() {}; -if (debugLevel >= debugLevels.indexOf("info")) { - info = console.info.bind(console, tag + ": "); -} - -let log = function() {}; -if (debugLevel >= debugLevels.indexOf("log")) { - log = console.log.bind(console, tag + ": "); -} - -let dbg = function() {}; -if (debugLevel >= debugLevels.indexOf("debug")) { - dbg = console.debug.bind(console, tag + ": "); -} - -function assert(e, msg) { - console.assert(e, tag + ": " + msg); -} - -function debugbreak() { - debugger; -} - - function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } -async function getActiveContextCodeEditor() { - return await appContext.tabManager.getActiveContextCodeEditor(); -} - -async function getActiveContextTextEditor() { - return await appContext.tabManager.getActiveContextTextEditor(); -} +const getActiveContextCodeEditor = async () => await appContext.tabManager.getActiveContextCodeEditor(); +const getActiveContextTextEditor = async () => await appContext.tabManager.getActiveContextTextEditor(); // ck-find-result and ck-find-result_selected are the styles ck-editor // uses for highlighting matches, use the same one on CodeMirror @@ -123,17 +79,16 @@ export default class FindWidget extends NoteContextAwareWidget { // XXX Use api.bindGlobalShortcut? $(window).keydown(async function (e){ - dbg("keydown on window " + e.key); - if ((e.key == 'F3') || + if ((e.key === 'F3') || // Note that for ctrl+f to work, needs to be disabled in Trilium's // shortcut config menu // XXX Maybe not if using bindShorcut? - ((e.metaKey || e.ctrlKey) && ((e.key == 'f') || (e.key == 'F')))) { + ((e.metaKey || e.ctrlKey) && ((e.key === 'f') || (e.key === 'F')))) { const note = appContext.tabManager.getActiveContextNote(); // Only writeable text and code supported const readOnly = note.getAttribute("label", "readOnly"); - if (!readOnly && ((note.type == "code") || (note.type == "text"))) { + if (!readOnly && (note.type === "code" || note.type === "text")) { if (findWidget.$findBox.is(":hidden")) { findWidget.$findBox.show(); @@ -142,7 +97,7 @@ export default class FindWidget extends NoteContextAwareWidget { findWidget.$curFound.text(0); // Initialize the input field to the text selection, if any - if (note.type == "code") { + if (note.type === "code") { let codeEditor = getActiveContextCodeEditor(); // highlightSelectionMatches is the overlay that highlights @@ -154,13 +109,13 @@ export default class FindWidget extends NoteContextAwareWidget { // Fill in the findbox with the current selection if any const selectedText = codeEditor.getSelection() - if (selectedText != "") { + if (selectedText !== "") { findWidget.$input.val(selectedText); } // Directly perform the search if there's some text to find, // without delaying or waiting for enter const needle = findWidget.$input.val(); - if (needle != "") { + if (needle !== "") { findWidget.$input.select(); await findWidget.performFind(needle); } @@ -179,7 +134,7 @@ export default class FindWidget extends NoteContextAwareWidget { // Directly perform the search if there's some text to // find, without delaying or waiting for enter const needle = findWidget.$input.val(); - if (needle != "") { + if (needle !== "") { findWidget.$input.select(); await findWidget.performFind(needle); } @@ -193,19 +148,18 @@ export default class FindWidget extends NoteContextAwareWidget { }); findWidget.$input.keydown(async function (e) { - dbg("keydown on input " + e.key); - if ((e.metaKey || e.ctrlKey) && ((e.key == 'F') || (e.key == 'f'))) { + if ((e.metaKey || e.ctrlKey) && ((e.key === 'F') || (e.key === 'f'))) { // If ctrl+f is pressed when the findbox is shown, select the // whole input to find findWidget.$input.select(); - } else if ((e.key == 'Enter') || (e.key == 'F3')) { + } else if ((e.key === 'Enter') || (e.key === 'F3')) { const needle = findWidget.$input.val(); - if (waitForEnter && (findWidget.needle != needle)) { + if (waitForEnter && (findWidget.needle !== needle)) { await findWidget.performFind(needle); } let numFound = parseInt(findWidget.$numFound.text()); let curFound = parseInt(findWidget.$curFound.text()) - 1; - dbg("Finding " + curFound + "/" + numFound + " occurrence of " + findWidget.$input.val()); + if (numFound > 0) { let delta = e.shiftKey ? -1 : 1; let nextFound = curFound + delta; @@ -220,7 +174,7 @@ export default class FindWidget extends NoteContextAwareWidget { findWidget.$curFound.text(nextFound + 1); const note = appContext.tabManager.getActiveContextNote(); - if (note.type == "code") { + if (note.type === "code") { let codeEditor = getActiveContextCodeEditor(); let doc = codeEditor.doc; @@ -248,7 +202,6 @@ export default class FindWidget extends NoteContextAwareWidget { codeEditor.scrollIntoView(pos.from); } else { - assert(note.type == "text", "Expected text note, found " + note.type); const textEditor = await getActiveContextTextEditor(); // There are no parameters for findNext/findPrev @@ -264,16 +217,15 @@ export default class FindWidget extends NoteContextAwareWidget { } e.preventDefault(); return false; - } else if (e.key == 'Escape') { + } else if (e.key === 'Escape') { let numFound = parseInt(findWidget.$numFound.text()); const note = appContext.tabManager.getActiveContextNote(); - if (note.type == "code") { + if (note.type === "code") { let codeEditor = getActiveContextCodeEditor(); codeEditor.focus(); } else { - assert(note.type == "text", "Expected text note, found " + note.type); const textEditor = await getActiveContextTextEditor(); textEditor.focus(); } @@ -305,12 +257,10 @@ export default class FindWidget extends NoteContextAwareWidget { }); findWidget.$caseCheck.change(function() { - log("caseCheck change"); findWidget.performFind(); }); findWidget.$wordCheck.change(function() { - log("wordCheck change"); findWidget.performFind(); }); @@ -320,10 +270,8 @@ export default class FindWidget extends NoteContextAwareWidget { findWidget.$findBox.focusout(async function (e) { // e.relatedTarget is the new focused element, note it can be null // if nothing is being focused - log(`focusout ${e.target.id} related ${e.relatedTarget?.id}`); if (findWidget.$findBox[0].contains(e.relatedTarget)) { // The focused element is inside this div, ignore - log("focusout to child, ignoring"); return; } findWidget.$findBox.hide(); @@ -337,7 +285,7 @@ export default class FindWidget extends NoteContextAwareWidget { let numFound = parseInt(findWidget.$numFound.text()); let curFound = parseInt(findWidget.$curFound.text()) - 1; const note = appContext.tabManager.getActiveContextNote(); - if (note.type == "code") { + if (note.type === "code") { let codeEditor = await getActiveContextCodeEditor(); if (numFound > 0) { let doc = codeEditor.doc; @@ -359,7 +307,6 @@ export default class FindWidget extends NoteContextAwareWidget { findWidget.findResult = null; findWidget.needle = null; } else { - assert(note.type == "text", "Expected text note, found " + note.type); if (numFound > 0) { const textEditor = await getActiveContextTextEditor(); // Clear the markers and set the caret to the @@ -397,12 +344,10 @@ export default class FindWidget extends NoteContextAwareWidget { let curFound = -1; // Clear - let findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); - log("findAndReplace clearing"); + const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); findAndReplaceEditing.state.clear(model); - log("findAndReplace stopping"); findAndReplaceEditing.stop(); - if (needle != "") { + if (needle !== "") { // Parameters are callback/text, options.matchCase=false, options.wholeWords=false // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findcommand.js#L44 // XXX Need to use the callback version for regexp @@ -410,23 +355,19 @@ export default class FindWidget extends NoteContextAwareWidget { // let re = new RegExp(needle, 'gi'); // let m = text.match(re); // numFound = m ? m.length : 0; - log("findAndReplace starts"); const options = { "matchCase" : matchCase, "wholeWords" : wholeWord }; findResult = textEditor.execute('find', needle, options); - log("findAndReplace ends"); numFound = findResult.results.length; // Find the result beyond the cursor - log("findAndReplace positioning"); - let cursorPos = model.document.selection.getLastPosition(); + const cursorPos = model.document.selection.getLastPosition(); for (let i = 0; i < findResult.results.length; ++i) { - let marker = findResult.results.get(i).marker; - let fromPos = marker.getStart(); - if (fromPos.compareWith(cursorPos) != "before") { + const marker = findResult.results.get(i).marker; + const fromPos = marker.getStart(); + if (fromPos.compareWith(cursorPos) !== "before") { curFound = i; break; } } - log("findAndReplace positioned"); } this.findResult = findResult; @@ -452,16 +393,16 @@ export default class FindWidget extends NoteContextAwareWidget { let curFound = -1; // See https://codemirror.net/addon/search/searchcursor.js for tips - let codeEditor = await getActiveContextCodeEditor(); - let doc = codeEditor.doc; - let text = doc.getValue(); + const codeEditor = await getActiveContextCodeEditor(); + const doc = codeEditor.doc; + const text = doc.getValue(); // Clear all markers if (this.findResult != null) { const findWidget = this; codeEditor.operation(function() { for (let i = 0; i < findWidget.findResult.length; ++i) { - let marker = findWidget.findResult[i]; + const marker = findWidget.findResult[i]; marker.clear(); } }); @@ -477,7 +418,7 @@ export default class FindWidget extends NoteContextAwareWidget { // complicated regexp, see // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/utils.js#L145 const wholeWordChar = wholeWord ? "\\b" : ""; - let re = new RegExp(wholeWordChar + needle + wholeWordChar, + const re = new RegExp(wholeWordChar + needle + wholeWordChar, 'g' + (matchCase ? '' : 'i')); let curLine = 0; let curChar = 0; @@ -500,7 +441,7 @@ export default class FindWidget extends NoteContextAwareWidget { } // Create a non-selected highlight marker for the match, the // selected marker highlight will be done later - if (i == curMatch.index) { + if (i === curMatch.index) { let fromPos = { "line" : curLine, "ch" : curChar }; // XXX If multiline is supported, this needs to // recalculate curLine since the match may span @@ -512,8 +453,8 @@ export default class FindWidget extends NoteContextAwareWidget { // Set the first match beyond the cursor as current // match - if (curFound == -1) { - let cursorPos = codeEditor.getCursor(); + if (curFound === -1) { + const cursorPos = codeEditor.getCursor(); if ((fromPos.line > cursorPos.line) || ((fromPos.line == cursorPos.line) && (fromPos.ch >= cursorPos.ch))){ @@ -524,7 +465,7 @@ export default class FindWidget extends NoteContextAwareWidget { numFound++; } // Do line and char position tracking - if (text[i] == "\n") { + if (text[i] === "\n") { curLine++; curChar = 0; } else { @@ -562,38 +503,29 @@ export default class FindWidget extends NoteContextAwareWidget { * state if missing. */ async performFind(needle, matchCase, wholeWord) { - needle = (needle == undefined) ? this.$input.val() : needle; + needle = (needle === undefined) ? this.$input.val() : needle; matchCase = (matchCase === undefined) ? this.$caseCheck.prop("checked") : matchCase; wholeWord = (wholeWord === undefined) ? this.$wordCheck.prop("checked") : wholeWord; - log(`performFind needle:${needle} case:${matchCase} word:${wholeWord}`); const note = appContext.tabManager.getActiveContextNote(); - if (note.type == "code") { + if (note.type === "code") { await this.performCodeNoteFind(needle, matchCase, wholeWord); } else { - assert(note.type == "text", "Expected text note, found " + note.type); await this.performTextNoteFind(needle, matchCase, wholeWord); } } isEnabled() { - dbg("isEnabled"); return super.isEnabled() && ((this.note.type === 'text') || (this.note.type === 'code')) && !this.note.hasLabel('noFindWidget'); } doRender() { - dbg("doRender"); this.$findBox.hide(); return this.$widget; } - async refreshWithNote(note) { - dbg("refreshWithNote"); - } - async entitiesReloadedEvent({loadResults}) { - dbg("entitiesReloadedEvent"); if (loadResults.isNoteContentReloaded(this.noteId)) { this.refresh(); } From 6778e1e60e234b7072a8be336eee4edb8c4f302f Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 15 May 2022 12:09:30 +0200 Subject: [PATCH 007/250] findwidget cleanup --- src/public/app/widgets/find.js | 142 +++++++++++++++------------------ 1 file changed, 66 insertions(+), 76 deletions(-) diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 9ac090400..2ea338797 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -38,7 +38,7 @@ const waitForEnter = (findWidgetDelayMillis < 0); // tabIndex=-1 on the checkbox labels is necessary so when clicking on the label // the focusout handler is called with relatedTarget equal to the label instead // of undefined. It's -1 instead of > 0, so they don't tabstop -const TEMPLATE = ` +const TPL = `
@@ -63,22 +63,20 @@ const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected"; const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; export default class FindWidget extends NoteContextAwareWidget { - constructor(...args) { - super(...args); - this.$widget = $(TEMPLATE); + doRender() { + this.$widget = $(TPL); this.$findBox = this.$widget.find('#findBox'); + this.$findBox.hide(); this.$input = this.$widget.find('#input'); this.$curFound = this.$widget.find('#curFound'); this.$numFound = this.$widget.find('#numFound'); this.$caseCheck = this.$widget.find("#caseCheck"); this.$wordCheck = this.$widget.find("#wordCheck"); this.findResult = null; - this.prevFocus = null; this.needle = null; - let findWidget = this; // XXX Use api.bindGlobalShortcut? - $(window).keydown(async function (e){ + $(window).keydown(async (e) => { if ((e.key === 'F3') || // Note that for ctrl+f to work, needs to be disabled in Trilium's // shortcut config menu @@ -89,12 +87,12 @@ export default class FindWidget extends NoteContextAwareWidget { // Only writeable text and code supported const readOnly = note.getAttribute("label", "readOnly"); if (!readOnly && (note.type === "code" || note.type === "text")) { - if (findWidget.$findBox.is(":hidden")) { + if (this.$findBox.is(":hidden")) { - findWidget.$findBox.show(); - findWidget.$input.focus(); - findWidget.$numFound.text(0); - findWidget.$curFound.text(0); + this.$findBox.show(); + this.$input.focus(); + this.$numFound.text(0); + this.$curFound.text(0); // Initialize the input field to the text selection, if any if (note.type === "code") { @@ -104,20 +102,20 @@ export default class FindWidget extends NoteContextAwareWidget { // the words under the cursor. This occludes the search // markers style, save it, disable it. Will be restored when // the focus is back into the note - findWidget.oldHighlightSelectionMatches = codeEditor.getOption("highlightSelectionMatches"); + this.oldHighlightSelectionMatches = codeEditor.getOption("highlightSelectionMatches"); codeEditor.setOption("highlightSelectionMatches", false); // Fill in the findbox with the current selection if any const selectedText = codeEditor.getSelection() if (selectedText !== "") { - findWidget.$input.val(selectedText); + this.$input.val(selectedText); } // Directly perform the search if there's some text to find, // without delaying or waiting for enter - const needle = findWidget.$input.val(); + const needle = this.$input.val(); if (needle !== "") { - findWidget.$input.select(); - await findWidget.performFind(needle); + this.$input.select(); + await this.performFind(needle); } } else { const textEditor = await getActiveContextTextEditor(); @@ -128,15 +126,15 @@ export default class FindWidget extends NoteContextAwareWidget { for (const item of range.getItems()) { // Fill in the findbox with the current selection if // any - findWidget.$input.val(item.data); + this.$input.val(item.data); break; } // Directly perform the search if there's some text to // find, without delaying or waiting for enter - const needle = findWidget.$input.val(); + const needle = this.$input.val(); if (needle !== "") { - findWidget.$input.select(); - await findWidget.performFind(needle); + this.$input.select(); + await this.performFind(needle); } } } @@ -147,18 +145,18 @@ export default class FindWidget extends NoteContextAwareWidget { return true; }); - findWidget.$input.keydown(async function (e) { + this.$input.keydown(async e => { if ((e.metaKey || e.ctrlKey) && ((e.key === 'F') || (e.key === 'f'))) { // If ctrl+f is pressed when the findbox is shown, select the // whole input to find - findWidget.$input.select(); + this.$input.select(); } else if ((e.key === 'Enter') || (e.key === 'F3')) { - const needle = findWidget.$input.val(); - if (waitForEnter && (findWidget.needle !== needle)) { - await findWidget.performFind(needle); + const needle = this.$input.val(); + if (waitForEnter && (this.needle !== needle)) { + await this.performFind(needle); } - let numFound = parseInt(findWidget.$numFound.text()); - let curFound = parseInt(findWidget.$curFound.text()) - 1; + const numFound = parseInt(this.$numFound.text()); + const curFound = parseInt(this.$curFound.text()) - 1; if (numFound > 0) { let delta = e.shiftKey ? -1 : 1; @@ -170,8 +168,8 @@ export default class FindWidget extends NoteContextAwareWidget { nextFound = numFound - 1; } - let needle = findWidget.$input.val(); - findWidget.$curFound.text(nextFound + 1); + let needle = this.$input.val(); + this.$curFound.text(nextFound + 1); const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { @@ -182,23 +180,23 @@ export default class FindWidget extends NoteContextAwareWidget { // Dehighlight current, highlight & scrollIntoView next // - let marker = findWidget.findResult[curFound]; + let marker = this.findResult[curFound]; let pos = marker.find(); marker.clear(); marker = doc.markText( pos.from, pos.to, { "className" : FIND_RESULT_CSS_CLASSNAME } ); - findWidget.findResult[curFound] = marker; + this.findResult[curFound] = marker; - marker = findWidget.findResult[nextFound]; + marker = this.findResult[nextFound]; pos = marker.find(); marker.clear(); marker = doc.markText( pos.from, pos.to, { "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME } ); - findWidget.findResult[nextFound] = marker; + this.findResult[nextFound] = marker; codeEditor.scrollIntoView(pos.from); } else { @@ -218,7 +216,7 @@ export default class FindWidget extends NoteContextAwareWidget { e.preventDefault(); return false; } else if (e.key === 'Escape') { - let numFound = parseInt(findWidget.$numFound.text()); + let numFound = parseInt(this.$numFound.text()); const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { @@ -233,48 +231,43 @@ export default class FindWidget extends NoteContextAwareWidget { // e.preventDefault(); }); - findWidget.$input.on('input', function (e) { + this.$input.on('input', () => { // XXX This should clear the previous search immediately in all cases // (the search is stale when waitforenter but also while the // delay is running for non waitforenter case) if (!waitForEnter) { // Clear the previous timeout if any, it's ok if timeoutId is // null or undefined - clearTimeout(findWidget.timeoutId); + clearTimeout(this.timeoutId); // Defer the search a few millis so the search doesn't start // immediately, as this can cause search word typing lag with // one or two-char searchwords and long notes // See https://github.com/antoniotejada/Trilium-FindWidget/issues/1 - const needle = findWidget.$input.val(); - const matchCase = findWidget.$caseCheck.prop("checked"); - const wholeWord = findWidget.$wordCheck.prop("checked"); - findWidget.timeoutId = setTimeout(async function () { - findWidget.timeoutId = null; - await findWidget.performFind(needle, matchCase, wholeWord); + const needle = this.$input.val(); + const matchCase = this.$caseCheck.prop("checked"); + const wholeWord = this.$wordCheck.prop("checked"); + this.timeoutId = setTimeout(async () => { + this.timeoutId = null; + await this.performFind(needle, matchCase, wholeWord); }, findWidgetDelayMillis); } }); - findWidget.$caseCheck.change(function() { - findWidget.performFind(); - }); - - findWidget.$wordCheck.change(function() { - findWidget.performFind(); - }); + this.$caseCheck.change(() => this.performFind()); + this.$wordCheck.change(() => this.performFind()); // Note blur doesn't bubble to parent div, but the parent div needs to // detect when any of the children are not focused and hide. Use // focusout instead which does bubble to the parent div. - findWidget.$findBox.focusout(async function (e) { + this.$findBox.focusout(async (e) => { // e.relatedTarget is the new focused element, note it can be null // if nothing is being focused - if (findWidget.$findBox[0].contains(e.relatedTarget)) { + if (this.$findBox[0].contains(e.relatedTarget)) { // The focused element is inside this div, ignore return; } - findWidget.$findBox.hide(); + this.$findBox.hide(); // Restore any state, if there's a current occurrence clear markers // and scroll to and select the last occurrence @@ -282,37 +275,37 @@ export default class FindWidget extends NoteContextAwareWidget { // XXX Switching to a different tab with crl+tab doesn't invoke // blur and leaves a stale search which then breaks when // navigating it - let numFound = parseInt(findWidget.$numFound.text()); - let curFound = parseInt(findWidget.$curFound.text()) - 1; + let numFound = parseInt(this.$numFound.text()); + let curFound = parseInt(this.$curFound.text()) - 1; const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { let codeEditor = await getActiveContextCodeEditor(); if (numFound > 0) { let doc = codeEditor.doc; - let pos = findWidget.findResult[curFound].find(); + let pos = this.findResult[curFound].find(); // Note setting the selection sets the cursor to // the end of the selection and scrolls it into // view doc.setSelection(pos.from, pos.to); // Clear all markers - codeEditor.operation(function() { - for (let i = 0; i < findWidget.findResult.length; ++i) { - let marker = findWidget.findResult[i]; + codeEditor.operation(() => { + for (let i = 0; i < this.findResult.length; ++i) { + let marker = this.findResult[i]; marker.clear(); } }); } // Restore the highlightSelectionMatches setting - codeEditor.setOption("highlightSelectionMatches", findWidget.oldHighlightSelectionMatches); - findWidget.findResult = null; - findWidget.needle = null; + codeEditor.setOption("highlightSelectionMatches", this.oldHighlightSelectionMatches); + this.findResult = null; + this.needle = null; } else { if (numFound > 0) { const textEditor = await getActiveContextTextEditor(); // Clear the markers and set the caret to the // current occurrence const model = textEditor.model; - let range = findWidget.findResult.results.get(curFound).marker.getRange(); + let range = this.findResult.results.get(curFound).marker.getRange(); // From // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92 // XXX Roll our own since already done for codeEditor and @@ -324,14 +317,16 @@ export default class FindWidget extends NoteContextAwareWidget { writer.setSelection(range, 0); }); textEditor.editing.view.scrollToTheSelection(); - findWidget.findResult = null; - findWidget.needle = null; + this.findResult = null; + this.needle = null; } else { - findWidget.findResult = null; - findWidget.needle = null; + this.findResult = null; + this.needle = null; } } }); + + return this.$widget; } async performTextNoteFind(needle, matchCase, wholeWord) { @@ -400,9 +395,9 @@ export default class FindWidget extends NoteContextAwareWidget { // Clear all markers if (this.findResult != null) { const findWidget = this; - codeEditor.operation(function() { - for (let i = 0; i < findWidget.findResult.length; ++i) { - const marker = findWidget.findResult[i]; + codeEditor.operation(() => { + for (let i = 0; i < this.findResult.length; ++i) { + const marker = this.findResult[i]; marker.clear(); } }); @@ -428,7 +423,7 @@ export default class FindWidget extends NoteContextAwareWidget { // script, batch them inside an operation so they become // unnoticeable. Alternatively, an overlay could be used, see // https://codemirror.net/addon/search/match-highlighter.js ? - codeEditor.operation(function() { + codeEditor.operation(() => { for (let i = 0; i < text.length; ++i) { // Fetch next match if it's the first time or // if past the current match start @@ -520,11 +515,6 @@ export default class FindWidget extends NoteContextAwareWidget { && !this.note.hasLabel('noFindWidget'); } - doRender() { - this.$findBox.hide(); - return this.$widget; - } - async entitiesReloadedEvent({loadResults}) { if (loadResults.isNoteContentReloaded(this.noteId)) { this.refresh(); From c50d8e85dc07c0075eba659b7a89e32136b6d652 Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 15 May 2022 21:03:51 +0200 Subject: [PATCH 008/250] findwidget cleanup --- src/public/app/widgets/find.js | 186 +++++++++++++-------------------- 1 file changed, 71 insertions(+), 115 deletions(-) diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 2ea338797..31769064a 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -1,32 +1,6 @@ /** - * Find in note replacement for Trilium ctrl+f search * (c) Antonio Tejada 2022 * https://github.com/antoniotejada/Trilium-FindWidget - * - * Features: - * - Find in writeable using ctrl+f and F3 - * - Tested on Trilium Desktop 0.50.3 - * - * Installation: - * - Create a code note of language JS Frontend with the contents of this file - * - Set the owned attributes (alt-a) to #widget - * - Set the owned attributes of any note you don't want to enable finding to - * #noFindWidget - * - Disable Ctrl+f shorcut in Trilium options - * - * Todo: - * - Refactoring/code cleanup - * - Case-sensitive option - * - Regexp option - * - Full word option - * - Find & Replace - * - * Note that many times some common code is replicated between CodeMirror and - * CKEditor codepaths because the CKEditor update is done inside a callback that - * is deferred so the code cannot be put outside of the callback or it will - * execute too early. - * - * See https://github.com/zadam/trilium/discussions/2806 for discussions */ import NoteContextAwareWidget from "./note_context_aware_widget.js"; @@ -44,7 +18,6 @@ const TPL = ` - 0/0
`; @@ -75,76 +48,6 @@ export default class FindWidget extends NoteContextAwareWidget { this.findResult = null; this.needle = null; - // XXX Use api.bindGlobalShortcut? - $(window).keydown(async (e) => { - if ((e.key === 'F3') || - // Note that for ctrl+f to work, needs to be disabled in Trilium's - // shortcut config menu - // XXX Maybe not if using bindShorcut? - ((e.metaKey || e.ctrlKey) && ((e.key === 'f') || (e.key === 'F')))) { - - const note = appContext.tabManager.getActiveContextNote(); - // Only writeable text and code supported - const readOnly = note.getAttribute("label", "readOnly"); - if (!readOnly && (note.type === "code" || note.type === "text")) { - if (this.$findBox.is(":hidden")) { - - this.$findBox.show(); - this.$input.focus(); - this.$numFound.text(0); - this.$curFound.text(0); - - // Initialize the input field to the text selection, if any - if (note.type === "code") { - let codeEditor = getActiveContextCodeEditor(); - - // highlightSelectionMatches is the overlay that highlights - // the words under the cursor. This occludes the search - // markers style, save it, disable it. Will be restored when - // the focus is back into the note - this.oldHighlightSelectionMatches = codeEditor.getOption("highlightSelectionMatches"); - codeEditor.setOption("highlightSelectionMatches", false); - - // Fill in the findbox with the current selection if any - const selectedText = codeEditor.getSelection() - if (selectedText !== "") { - this.$input.val(selectedText); - } - // Directly perform the search if there's some text to find, - // without delaying or waiting for enter - const needle = this.$input.val(); - if (needle !== "") { - this.$input.select(); - await this.performFind(needle); - } - } else { - const textEditor = await getActiveContextTextEditor(); - - const selection = textEditor.model.document.selection; - const range = selection.getFirstRange(); - - for (const item of range.getItems()) { - // Fill in the findbox with the current selection if - // any - this.$input.val(item.data); - break; - } - // Directly perform the search if there's some text to - // find, without delaying or waiting for enter - const needle = this.$input.val(); - if (needle !== "") { - this.$input.select(); - await this.performFind(needle); - } - } - } - e.preventDefault(); - return false; - } - } - return true; - }); - this.$input.keydown(async e => { if ((e.metaKey || e.ctrlKey) && ((e.key === 'F') || (e.key === 'f'))) { // If ctrl+f is pressed when the findbox is shown, select the @@ -173,8 +76,8 @@ export default class FindWidget extends NoteContextAwareWidget { const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { - let codeEditor = getActiveContextCodeEditor(); - let doc = codeEditor.doc; + const codeEditor = await getActiveContextCodeEditor(); + const doc = codeEditor.doc; // // Dehighlight current, highlight & scrollIntoView next @@ -216,19 +119,15 @@ export default class FindWidget extends NoteContextAwareWidget { e.preventDefault(); return false; } else if (e.key === 'Escape') { - let numFound = parseInt(this.$numFound.text()); - const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { - let codeEditor = getActiveContextCodeEditor(); - + const codeEditor = await getActiveContextCodeEditor(); codeEditor.focus(); } else { const textEditor = await getActiveContextTextEditor(); textEditor.focus(); } } - // e.preventDefault(); }); this.$input.on('input', () => { @@ -275,14 +174,14 @@ export default class FindWidget extends NoteContextAwareWidget { // XXX Switching to a different tab with crl+tab doesn't invoke // blur and leaves a stale search which then breaks when // navigating it - let numFound = parseInt(this.$numFound.text()); - let curFound = parseInt(this.$curFound.text()) - 1; + const numFound = parseInt(this.$numFound.text()); + const curFound = parseInt(this.$curFound.text()) - 1; const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { - let codeEditor = await getActiveContextCodeEditor(); + const codeEditor = await getActiveContextCodeEditor(); if (numFound > 0) { - let doc = codeEditor.doc; - let pos = this.findResult[curFound].find(); + const doc = codeEditor.doc; + const pos = this.findResult[curFound].find(); // Note setting the selection sets the cursor to // the end of the selection and scrolls it into // view @@ -305,7 +204,7 @@ export default class FindWidget extends NoteContextAwareWidget { // Clear the markers and set the caret to the // current occurrence const model = textEditor.model; - let range = this.findResult.results.get(curFound).marker.getRange(); + const range = this.findResult.results.get(curFound).marker.getRange(); // From // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92 // XXX Roll our own since already done for codeEditor and @@ -329,6 +228,65 @@ export default class FindWidget extends NoteContextAwareWidget { return this.$widget; } + async findInTextEvent() { + const note = appContext.tabManager.getActiveContextNote(); + // Only writeable text and code supported + const readOnly = note.getAttribute("label", "readOnly"); + if (!readOnly && (note.type === "code" || note.type === "text")) { + if (this.$findBox.is(":hidden")) { + + this.$findBox.show(); + this.$input.focus(); + this.$numFound.text(0); + this.$curFound.text(0); + + // Initialize the input field to the text selection, if any + if (note.type === "code") { + const codeEditor = await getActiveContextCodeEditor(); + + // highlightSelectionMatches is the overlay that highlights + // the words under the cursor. This occludes the search + // markers style, save it, disable it. Will be restored when + // the focus is back into the note + this.oldHighlightSelectionMatches = codeEditor.getOption("highlightSelectionMatches"); + codeEditor.setOption("highlightSelectionMatches", false); + + // Fill in the findbox with the current selection if any + const selectedText = codeEditor.getSelection() + if (selectedText !== "") { + this.$input.val(selectedText); + } + // Directly perform the search if there's some text to find, + // without delaying or waiting for enter + const needle = this.$input.val(); + if (needle !== "") { + this.$input.select(); + await this.performFind(needle); + } + } else { + const textEditor = await getActiveContextTextEditor(); + + const selection = textEditor.model.document.selection; + const range = selection.getFirstRange(); + + for (const item of range.getItems()) { + // Fill in the findbox with the current selection if + // any + this.$input.val(item.data); + break; + } + // Directly perform the search if there's some text to + // find, without delaying or waiting for enter + const needle = this.$input.val(); + if (needle !== "") { + this.$input.select(); + await this.performFind(needle); + } + } + } + } + } + async performTextNoteFind(needle, matchCase, wholeWord) { // Do this even if the needle is empty so the markers are cleared and // the counters updated @@ -403,7 +361,7 @@ export default class FindWidget extends NoteContextAwareWidget { }); } - if (needle != "") { + if (needle !== "") { needle = escapeRegExp(needle); // Find and highlight matches @@ -451,7 +409,7 @@ export default class FindWidget extends NoteContextAwareWidget { if (curFound === -1) { const cursorPos = codeEditor.getCursor(); if ((fromPos.line > cursorPos.line) || - ((fromPos.line == cursorPos.line) && + ((fromPos.line === cursorPos.line) && (fromPos.ch >= cursorPos.ch))){ curFound = numFound; } @@ -510,9 +468,7 @@ export default class FindWidget extends NoteContextAwareWidget { } isEnabled() { - return super.isEnabled() - && ((this.note.type === 'text') || (this.note.type === 'code')) - && !this.note.hasLabel('noFindWidget'); + return super.isEnabled() && (this.note.type === 'text' || this.note.type === 'code'); } async entitiesReloadedEvent({loadResults}) { From bb7ad496bf9fe72cbb9d1531c3acb86974d83f93 Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 15 May 2022 22:51:26 +0200 Subject: [PATCH 009/250] findwidget cleanup --- src/public/app/widgets/find.js | 229 +++++++++++++++++++-------------- 1 file changed, 132 insertions(+), 97 deletions(-) diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 31769064a..115efce13 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -14,11 +14,48 @@ const waitForEnter = (findWidgetDelayMillis < 0); // of undefined. It's -1 instead of > 0, so they don't tabstop const TPL = `
-
- - - - 0/0 + + +
+ + +
+ +
+ +
+ +
+ +
+ 0 + / + 0 +
`; @@ -38,41 +75,40 @@ const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; export default class FindWidget extends NoteContextAwareWidget { doRender() { this.$widget = $(TPL); - this.$findBox = this.$widget.find('#findBox'); + this.$findBox = this.$widget.find('.find-widget-box'); this.$findBox.hide(); - this.$input = this.$widget.find('#input'); - this.$curFound = this.$widget.find('#curFound'); - this.$numFound = this.$widget.find('#numFound'); - this.$caseCheck = this.$widget.find("#caseCheck"); - this.$wordCheck = this.$widget.find("#wordCheck"); + this.$input = this.$widget.find('.find-widget-search-term-input'); + this.$currentFound = this.$widget.find('.find-widget-current-found'); + this.$totalFound = this.$widget.find('.find-widget-total-found'); + this.$caseSensitiveCheckbox = this.$widget.find(".find-widget-case-sensitive-checkbox"); + this.$matchWordsCheckbox = this.$widget.find(".find-widget-match-words-checkbox"); this.findResult = null; - this.needle = null; + this.searchTerm = null; this.$input.keydown(async e => { - if ((e.metaKey || e.ctrlKey) && ((e.key === 'F') || (e.key === 'f'))) { + if ((e.metaKey || e.ctrlKey) && (e.key === 'F' || e.key === 'f')) { // If ctrl+f is pressed when the findbox is shown, select the // whole input to find this.$input.select(); - } else if ((e.key === 'Enter') || (e.key === 'F3')) { - const needle = this.$input.val(); - if (waitForEnter && (this.needle !== needle)) { - await this.performFind(needle); + } else if (e.key === 'Enter' || e.key === 'F3') { + const searchTerm = this.$input.val(); + if (waitForEnter && this.searchTerm !== searchTerm) { + await this.performFind(searchTerm); } - const numFound = parseInt(this.$numFound.text()); - const curFound = parseInt(this.$curFound.text()) - 1; + const totalFound = parseInt(this.$totalFound.text()); + const currentFound = parseInt(this.$currentFound.text()) - 1; - if (numFound > 0) { - let delta = e.shiftKey ? -1 : 1; - let nextFound = curFound + delta; + if (totalFound > 0) { + const delta = e.shiftKey ? -1 : 1; + let nextFound = currentFound + delta; // Wrap around - if (nextFound > numFound - 1) { + if (nextFound > totalFound - 1) { nextFound = 0; - } if (nextFound < 0) { - nextFound = numFound - 1; + } else if (nextFound < 0) { + nextFound = totalFound - 1; } - let needle = this.$input.val(); - this.$curFound.text(nextFound + 1); + this.$currentFound.text(nextFound + 1); const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { @@ -83,14 +119,14 @@ export default class FindWidget extends NoteContextAwareWidget { // Dehighlight current, highlight & scrollIntoView next // - let marker = this.findResult[curFound]; + let marker = this.findResult[currentFound]; let pos = marker.find(); marker.clear(); marker = doc.markText( pos.from, pos.to, { "className" : FIND_RESULT_CSS_CLASSNAME } ); - this.findResult[curFound] = marker; + this.findResult[currentFound] = marker; marker = this.findResult[nextFound]; pos = marker.find(); @@ -143,23 +179,23 @@ export default class FindWidget extends NoteContextAwareWidget { // immediately, as this can cause search word typing lag with // one or two-char searchwords and long notes // See https://github.com/antoniotejada/Trilium-FindWidget/issues/1 - const needle = this.$input.val(); - const matchCase = this.$caseCheck.prop("checked"); - const wholeWord = this.$wordCheck.prop("checked"); + const searchTerm = this.$input.val(); + const matchCase = this.$caseSensitiveCheckbox.prop("checked"); + const wholeWord = this.$matchWordsCheckbox.prop("checked"); this.timeoutId = setTimeout(async () => { this.timeoutId = null; - await this.performFind(needle, matchCase, wholeWord); + await this.performFind(searchTerm, matchCase, wholeWord); }, findWidgetDelayMillis); } }); - this.$caseCheck.change(() => this.performFind()); - this.$wordCheck.change(() => this.performFind()); + this.$caseSensitiveCheckbox.change(() => this.performFind()); + this.$matchWordsCheckbox.change(() => this.performFind()); // Note blur doesn't bubble to parent div, but the parent div needs to // detect when any of the children are not focused and hide. Use // focusout instead which does bubble to the parent div. - this.$findBox.focusout(async (e) => { + this.$findBox.on('focusout', async (e) => { // e.relatedTarget is the new focused element, note it can be null // if nothing is being focused if (this.$findBox[0].contains(e.relatedTarget)) { @@ -174,14 +210,14 @@ export default class FindWidget extends NoteContextAwareWidget { // XXX Switching to a different tab with crl+tab doesn't invoke // blur and leaves a stale search which then breaks when // navigating it - const numFound = parseInt(this.$numFound.text()); - const curFound = parseInt(this.$curFound.text()) - 1; + const totalFound = parseInt(this.$totalFound.text()); + const currentFound = parseInt(this.$currentFound.text()) - 1; const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { const codeEditor = await getActiveContextCodeEditor(); - if (numFound > 0) { + if (totalFound > 0) { const doc = codeEditor.doc; - const pos = this.findResult[curFound].find(); + const pos = this.findResult[currentFound].find(); // Note setting the selection sets the cursor to // the end of the selection and scrolls it into // view @@ -197,14 +233,14 @@ export default class FindWidget extends NoteContextAwareWidget { // Restore the highlightSelectionMatches setting codeEditor.setOption("highlightSelectionMatches", this.oldHighlightSelectionMatches); this.findResult = null; - this.needle = null; + this.searchTerm = null; } else { - if (numFound > 0) { + if (totalFound > 0) { const textEditor = await getActiveContextTextEditor(); // Clear the markers and set the caret to the // current occurrence const model = textEditor.model; - const range = this.findResult.results.get(curFound).marker.getRange(); + const range = this.findResult.results.get(currentFound).marker.getRange(); // From // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92 // XXX Roll our own since already done for codeEditor and @@ -217,10 +253,10 @@ export default class FindWidget extends NoteContextAwareWidget { }); textEditor.editing.view.scrollToTheSelection(); this.findResult = null; - this.needle = null; + this.searchTerm = null; } else { this.findResult = null; - this.needle = null; + this.searchTerm = null; } } }); @@ -237,8 +273,8 @@ export default class FindWidget extends NoteContextAwareWidget { this.$findBox.show(); this.$input.focus(); - this.$numFound.text(0); - this.$curFound.text(0); + this.$totalFound.text(0); + this.$currentFound.text(0); // Initialize the input field to the text selection, if any if (note.type === "code") { @@ -258,10 +294,10 @@ export default class FindWidget extends NoteContextAwareWidget { } // Directly perform the search if there's some text to find, // without delaying or waiting for enter - const needle = this.$input.val(); - if (needle !== "") { + const searchTerm = this.$input.val(); + if (searchTerm !== "") { this.$input.select(); - await this.performFind(needle); + await this.performFind(searchTerm); } } else { const textEditor = await getActiveContextTextEditor(); @@ -277,73 +313,73 @@ export default class FindWidget extends NoteContextAwareWidget { } // Directly perform the search if there's some text to // find, without delaying or waiting for enter - const needle = this.$input.val(); - if (needle !== "") { + const searchTerm = this.$input.val(); + if (searchTerm !== "") { this.$input.select(); - await this.performFind(needle); + await this.performFind(searchTerm); } } } } } - async performTextNoteFind(needle, matchCase, wholeWord) { - // Do this even if the needle is empty so the markers are cleared and + async performTextNoteFind(searchTerm, matchCase, wholeWord) { + // Do this even if the searchTerm is empty so the markers are cleared and // the counters updated const textEditor = await getActiveContextTextEditor(); const model = textEditor.model; let findResult = null; - let numFound = 0; - let curFound = -1; + let totalFound = 0; + let currentFound = -1; // Clear const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); findAndReplaceEditing.state.clear(model); findAndReplaceEditing.stop(); - if (needle !== "") { + if (searchTerm !== "") { // Parameters are callback/text, options.matchCase=false, options.wholeWords=false // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findcommand.js#L44 // XXX Need to use the callback version for regexp - // needle = escapeRegExp(needle); - // let re = new RegExp(needle, 'gi'); + // searchTerm = escapeRegExp(searchTerm); + // let re = new RegExp(searchTerm, 'gi'); // let m = text.match(re); - // numFound = m ? m.length : 0; + // totalFound = m ? m.length : 0; const options = { "matchCase" : matchCase, "wholeWords" : wholeWord }; - findResult = textEditor.execute('find', needle, options); - numFound = findResult.results.length; + findResult = textEditor.execute('find', searchTerm, options); + totalFound = findResult.results.length; // Find the result beyond the cursor const cursorPos = model.document.selection.getLastPosition(); for (let i = 0; i < findResult.results.length; ++i) { const marker = findResult.results.get(i).marker; const fromPos = marker.getStart(); if (fromPos.compareWith(cursorPos) !== "before") { - curFound = i; + currentFound = i; break; } } } this.findResult = findResult; - this.$numFound.text(numFound); + this.$totalFound.text(totalFound); // Calculate curfound if not already, highlight it as // selected - if (numFound > 0) { - curFound = Math.max(0, curFound); + if (totalFound > 0) { + currentFound = Math.max(0, currentFound); // XXX Do this accessing the private data? // See // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js - for (let i = 0 ; i < curFound; ++i) { - textEditor.execute('findNext', needle); + for (let i = 0 ; i < currentFound; ++i) { + textEditor.execute('findNext', searchTerm); } } - this.$curFound.text(curFound + 1); - this.needle = needle; + this.$currentFound.text(currentFound + 1); + this.searchTerm = searchTerm; } - async performCodeNoteFind(needle, matchCase, wholeWord) { + async performCodeNoteFind(searchTerm, matchCase, wholeWord) { let findResult = null; - let numFound = 0; - let curFound = -1; + let totalFound = 0; + let currentFound = -1; // See https://codemirror.net/addon/search/searchcursor.js for tips const codeEditor = await getActiveContextCodeEditor(); @@ -352,7 +388,6 @@ export default class FindWidget extends NoteContextAwareWidget { // Clear all markers if (this.findResult != null) { - const findWidget = this; codeEditor.operation(() => { for (let i = 0; i < this.findResult.length; ++i) { const marker = this.findResult[i]; @@ -361,8 +396,8 @@ export default class FindWidget extends NoteContextAwareWidget { }); } - if (needle !== "") { - needle = escapeRegExp(needle); + if (searchTerm !== "") { + searchTerm = escapeRegExp(searchTerm); // Find and highlight matches // Find and highlight matches @@ -371,7 +406,7 @@ export default class FindWidget extends NoteContextAwareWidget { // complicated regexp, see // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/utils.js#L145 const wholeWordChar = wholeWord ? "\\b" : ""; - const re = new RegExp(wholeWordChar + needle + wholeWordChar, + const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, 'g' + (matchCase ? '' : 'i')); let curLine = 0; let curChar = 0; @@ -406,16 +441,16 @@ export default class FindWidget extends NoteContextAwareWidget { // Set the first match beyond the cursor as current // match - if (curFound === -1) { + if (currentFound === -1) { const cursorPos = codeEditor.getCursor(); if ((fromPos.line > cursorPos.line) || ((fromPos.line === cursorPos.line) && (fromPos.ch >= cursorPos.ch))){ - curFound = numFound; + currentFound = totalFound; } } - numFound++; + totalFound++; } // Do line and char position tracking if (text[i] === "\n") { @@ -429,41 +464,41 @@ export default class FindWidget extends NoteContextAwareWidget { } this.findResult = findResult; - this.$numFound.text(numFound); + this.$totalFound.text(totalFound); // Calculate curfound if not already, highlight it as selected - if (numFound > 0) { - curFound = Math.max(0, curFound) - let marker = findResult[curFound]; + if (totalFound > 0) { + currentFound = Math.max(0, currentFound) + let marker = findResult[currentFound]; let pos = marker.find(); codeEditor.scrollIntoView(pos.to); marker.clear(); - findResult[curFound] = doc.markText( pos.from, pos.to, + findResult[currentFound] = doc.markText( pos.from, pos.to, { "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME } ); } - this.$curFound.text(curFound + 1); - this.needle = needle; + this.$currentFound.text(currentFound + 1); + this.searchTerm = searchTerm; } /** * Perform the find and highlight the find results. * - * @param needle {string} optional parameter, taken from the input box if + * @param [searchTerm] {string} optional parameter, taken from the input box if * missing. - * @param matchCase {boolean} optional parameter, taken from the checkbox + * @param [matchCase] {boolean} optional parameter, taken from the checkbox * state if missing. - * @param wholeWord {boolean} optional parameter, taken from the checkbox + * @param [wholeWord] {boolean} optional parameter, taken from the checkbox * state if missing. */ - async performFind(needle, matchCase, wholeWord) { - needle = (needle === undefined) ? this.$input.val() : needle; - matchCase = (matchCase === undefined) ? this.$caseCheck.prop("checked") : matchCase; - wholeWord = (wholeWord === undefined) ? this.$wordCheck.prop("checked") : wholeWord; + async performFind(searchTerm, matchCase, wholeWord) { + searchTerm = (searchTerm === undefined) ? this.$input.val() : searchTerm; + matchCase = (matchCase === undefined) ? this.$caseSensitiveCheckbox.prop("checked") : matchCase; + wholeWord = (wholeWord === undefined) ? this.$matchWordsCheckbox.prop("checked") : wholeWord; const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { - await this.performCodeNoteFind(needle, matchCase, wholeWord); + await this.performCodeNoteFind(searchTerm, matchCase, wholeWord); } else { - await this.performTextNoteFind(needle, matchCase, wholeWord); + await this.performTextNoteFind(searchTerm, matchCase, wholeWord); } } From c51e6107a1fa31203a518aa6f487ccade87e9520 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 16 May 2022 23:56:43 +0200 Subject: [PATCH 010/250] findwidget cleanup --- src/public/app/widgets/find.js | 481 ++++++------------------- src/public/app/widgets/find_in_code.js | 193 ++++++++++ src/public/app/widgets/find_in_text.js | 116 ++++++ 3 files changed, 417 insertions(+), 373 deletions(-) create mode 100644 src/public/app/widgets/find_in_code.js create mode 100644 src/public/app/widgets/find_in_text.js diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 115efce13..b204cffb1 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -5,6 +5,8 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js"; import appContext from "../services/app_context.js"; +import FindInText from "./find_in_text.js"; +import FindInCode from "./find_in_code.js"; const findWidgetDelayMillis = 200; const waitForEnter = (findWidgetDelayMillis < 0); @@ -59,20 +61,14 @@ const TPL = `
`; -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -const getActiveContextCodeEditor = async () => await appContext.tabManager.getActiveContextCodeEditor(); -const getActiveContextTextEditor = async () => await appContext.tabManager.getActiveContextTextEditor(); - -// ck-find-result and ck-find-result_selected are the styles ck-editor -// uses for highlighting matches, use the same one on CodeMirror -// for consistency -const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected"; -const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; - export default class FindWidget extends NoteContextAwareWidget { + constructor() { + super(); + + this.textHandler = new FindInText(); + this.codeHandler = new FindInCode(); + } + doRender() { this.$widget = $(TPL); this.$findBox = this.$widget.find('.find-widget-box'); @@ -81,7 +77,9 @@ export default class FindWidget extends NoteContextAwareWidget { this.$currentFound = this.$widget.find('.find-widget-current-found'); this.$totalFound = this.$widget.find('.find-widget-total-found'); this.$caseSensitiveCheckbox = this.$widget.find(".find-widget-case-sensitive-checkbox"); + this.$caseSensitiveCheckbox.change(() => this.performFind()); this.$matchWordsCheckbox = this.$widget.find(".find-widget-match-words-checkbox"); + this.$matchWordsCheckbox.change(() => this.performFind()); this.findResult = null; this.searchTerm = null; @@ -91,415 +89,142 @@ export default class FindWidget extends NoteContextAwareWidget { // whole input to find this.$input.select(); } else if (e.key === 'Enter' || e.key === 'F3') { - const searchTerm = this.$input.val(); - if (waitForEnter && this.searchTerm !== searchTerm) { - await this.performFind(searchTerm); - } - const totalFound = parseInt(this.$totalFound.text()); - const currentFound = parseInt(this.$currentFound.text()) - 1; - - if (totalFound > 0) { - const delta = e.shiftKey ? -1 : 1; - let nextFound = currentFound + delta; - // Wrap around - if (nextFound > totalFound - 1) { - nextFound = 0; - } else if (nextFound < 0) { - nextFound = totalFound - 1; - } - - this.$currentFound.text(nextFound + 1); - - const note = appContext.tabManager.getActiveContextNote(); - if (note.type === "code") { - const codeEditor = await getActiveContextCodeEditor(); - const doc = codeEditor.doc; - - // - // Dehighlight current, highlight & scrollIntoView next - // - - let marker = this.findResult[currentFound]; - let pos = marker.find(); - marker.clear(); - marker = doc.markText( - pos.from, pos.to, - { "className" : FIND_RESULT_CSS_CLASSNAME } - ); - this.findResult[currentFound] = marker; - - marker = this.findResult[nextFound]; - pos = marker.find(); - marker.clear(); - marker = doc.markText( - pos.from, pos.to, - { "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME } - ); - this.findResult[nextFound] = marker; - - codeEditor.scrollIntoView(pos.from); - } else { - const textEditor = await getActiveContextTextEditor(); - - // There are no parameters for findNext/findPrev - // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js#L57 - // curFound wrap around above assumes findNext and - // findPrevious wraparound, which is what they do - if (delta > 0) { - textEditor.execute('findNext'); - } else { - textEditor.execute('findPrevious'); - } - } - } + await this.findNext(e); e.preventDefault(); return false; } else if (e.key === 'Escape') { - const note = appContext.tabManager.getActiveContextNote(); - if (note.type === "code") { - const codeEditor = await getActiveContextCodeEditor(); - codeEditor.focus(); - } else { - const textEditor = await getActiveContextTextEditor(); - textEditor.focus(); - } + await this.getHandler().close(); } }); - this.$input.on('input', () => { - // XXX This should clear the previous search immediately in all cases - // (the search is stale when waitforenter but also while the - // delay is running for non waitforenter case) - if (!waitForEnter) { - // Clear the previous timeout if any, it's ok if timeoutId is - // null or undefined - clearTimeout(this.timeoutId); - - // Defer the search a few millis so the search doesn't start - // immediately, as this can cause search word typing lag with - // one or two-char searchwords and long notes - // See https://github.com/antoniotejada/Trilium-FindWidget/issues/1 - const searchTerm = this.$input.val(); - const matchCase = this.$caseSensitiveCheckbox.prop("checked"); - const wholeWord = this.$matchWordsCheckbox.prop("checked"); - this.timeoutId = setTimeout(async () => { - this.timeoutId = null; - await this.performFind(searchTerm, matchCase, wholeWord); - }, findWidgetDelayMillis); - } - }); - - this.$caseSensitiveCheckbox.change(() => this.performFind()); - this.$matchWordsCheckbox.change(() => this.performFind()); + this.$input.on('input', () => this.startSearch()); // Note blur doesn't bubble to parent div, but the parent div needs to // detect when any of the children are not focused and hide. Use // focusout instead which does bubble to the parent div. - this.$findBox.on('focusout', async (e) => { + this.$findBox.on('focusout', async e => { // e.relatedTarget is the new focused element, note it can be null // if nothing is being focused if (this.$findBox[0].contains(e.relatedTarget)) { // The focused element is inside this div, ignore return; } - this.$findBox.hide(); - // Restore any state, if there's a current occurrence clear markers - // and scroll to and select the last occurrence - - // XXX Switching to a different tab with crl+tab doesn't invoke - // blur and leaves a stale search which then breaks when - // navigating it - const totalFound = parseInt(this.$totalFound.text()); - const currentFound = parseInt(this.$currentFound.text()) - 1; - const note = appContext.tabManager.getActiveContextNote(); - if (note.type === "code") { - const codeEditor = await getActiveContextCodeEditor(); - if (totalFound > 0) { - const doc = codeEditor.doc; - const pos = this.findResult[currentFound].find(); - // Note setting the selection sets the cursor to - // the end of the selection and scrolls it into - // view - doc.setSelection(pos.from, pos.to); - // Clear all markers - codeEditor.operation(() => { - for (let i = 0; i < this.findResult.length; ++i) { - let marker = this.findResult[i]; - marker.clear(); - } - }); - } - // Restore the highlightSelectionMatches setting - codeEditor.setOption("highlightSelectionMatches", this.oldHighlightSelectionMatches); - this.findResult = null; - this.searchTerm = null; - } else { - if (totalFound > 0) { - const textEditor = await getActiveContextTextEditor(); - // Clear the markers and set the caret to the - // current occurrence - const model = textEditor.model; - const range = this.findResult.results.get(currentFound).marker.getRange(); - // From - // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92 - // XXX Roll our own since already done for codeEditor and - // will probably allow more refactoring? - let findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); - findAndReplaceEditing.state.clear(model); - findAndReplaceEditing.stop(); - model.change(writer => { - writer.setSelection(range, 0); - }); - textEditor.editing.view.scrollToTheSelection(); - this.findResult = null; - this.searchTerm = null; - } else { - this.findResult = null; - this.searchTerm = null; - } - } + await this.closeSearch(); }); return this.$widget; } + startSearch() { + // XXX This should clear the previous search immediately in all cases + // (the search is stale when waitforenter but also while the + // delay is running for non waitforenter case) + if (!waitForEnter) { + // Clear the previous timeout if any, it's ok if timeoutId is + // null or undefined + clearTimeout(this.timeoutId); + + // Defer the search a few millis so the search doesn't start + // immediately, as this can cause search word typing lag with + // one or two-char searchwords and long notes + // See https://github.com/antoniotejada/Trilium-FindWidget/issues/1 + const searchTerm = this.$input.val(); + const matchCase = this.$caseSensitiveCheckbox.prop("checked"); + const wholeWord = this.$matchWordsCheckbox.prop("checked"); + this.timeoutId = setTimeout(async () => { + this.timeoutId = null; + await this.performFind(searchTerm, matchCase, wholeWord); + }, findWidgetDelayMillis); + } + } + + async findNext(e) { + const searchTerm = this.$input.val(); + if (waitForEnter && this.searchTerm !== searchTerm) { + await this.performFind(searchTerm); + } + const totalFound = parseInt(this.$totalFound.text()); + const currentFound = parseInt(this.$currentFound.text()) - 1; + + if (totalFound > 0) { + const direction = e.shiftKey ? -1 : 1; + let nextFound = currentFound + direction; + // Wrap around + if (nextFound > totalFound - 1) { + nextFound = 0; + } else if (nextFound < 0) { + nextFound = totalFound - 1; + } + + this.$currentFound.text(nextFound + 1); + + await this.getHandler().findNext(direction, currentFound, nextFound); + } + } + async findInTextEvent() { const note = appContext.tabManager.getActiveContextNote(); // Only writeable text and code supported const readOnly = note.getAttribute("label", "readOnly"); if (!readOnly && (note.type === "code" || note.type === "text")) { if (this.$findBox.is(":hidden")) { - this.$findBox.show(); this.$input.focus(); this.$totalFound.text(0); this.$currentFound.text(0); - // Initialize the input field to the text selection, if any - if (note.type === "code") { - const codeEditor = await getActiveContextCodeEditor(); + const searchTerm = await this.getHandler().getInitialSearchTerm(); - // highlightSelectionMatches is the overlay that highlights - // the words under the cursor. This occludes the search - // markers style, save it, disable it. Will be restored when - // the focus is back into the note - this.oldHighlightSelectionMatches = codeEditor.getOption("highlightSelectionMatches"); - codeEditor.setOption("highlightSelectionMatches", false); + this.$input.val(searchTerm || ""); - // Fill in the findbox with the current selection if any - const selectedText = codeEditor.getSelection() - if (selectedText !== "") { - this.$input.val(selectedText); - } - // Directly perform the search if there's some text to find, - // without delaying or waiting for enter - const searchTerm = this.$input.val(); - if (searchTerm !== "") { - this.$input.select(); - await this.performFind(searchTerm); - } - } else { - const textEditor = await getActiveContextTextEditor(); - - const selection = textEditor.model.document.selection; - const range = selection.getFirstRange(); - - for (const item of range.getItems()) { - // Fill in the findbox with the current selection if - // any - this.$input.val(item.data); - break; - } - // Directly perform the search if there's some text to - // find, without delaying or waiting for enter - const searchTerm = this.$input.val(); - if (searchTerm !== "") { - this.$input.select(); - await this.performFind(searchTerm); - } + // Directly perform the search if there's some text to + // find, without delaying or waiting for enter + if (searchTerm !== "") { + this.$input.select(); + await this.performFind(searchTerm); } } } } - async performTextNoteFind(searchTerm, matchCase, wholeWord) { - // Do this even if the searchTerm is empty so the markers are cleared and - // the counters updated - const textEditor = await getActiveContextTextEditor(); - const model = textEditor.model; - let findResult = null; - let totalFound = 0; - let currentFound = -1; - - // Clear - const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); - findAndReplaceEditing.state.clear(model); - findAndReplaceEditing.stop(); - if (searchTerm !== "") { - // Parameters are callback/text, options.matchCase=false, options.wholeWords=false - // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findcommand.js#L44 - // XXX Need to use the callback version for regexp - // searchTerm = escapeRegExp(searchTerm); - // let re = new RegExp(searchTerm, 'gi'); - // let m = text.match(re); - // totalFound = m ? m.length : 0; - const options = { "matchCase" : matchCase, "wholeWords" : wholeWord }; - findResult = textEditor.execute('find', searchTerm, options); - totalFound = findResult.results.length; - // Find the result beyond the cursor - const cursorPos = model.document.selection.getLastPosition(); - for (let i = 0; i < findResult.results.length; ++i) { - const marker = findResult.results.get(i).marker; - const fromPos = marker.getStart(); - if (fromPos.compareWith(cursorPos) !== "before") { - currentFound = i; - break; - } - } - } - - this.findResult = findResult; - this.$totalFound.text(totalFound); - // Calculate curfound if not already, highlight it as - // selected - if (totalFound > 0) { - currentFound = Math.max(0, currentFound); - // XXX Do this accessing the private data? - // See - // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js - for (let i = 0 ; i < currentFound; ++i) { - textEditor.execute('findNext', searchTerm); - } - } - this.$currentFound.text(currentFound + 1); - this.searchTerm = searchTerm; - } - - async performCodeNoteFind(searchTerm, matchCase, wholeWord) { - let findResult = null; - let totalFound = 0; - let currentFound = -1; - - // See https://codemirror.net/addon/search/searchcursor.js for tips - const codeEditor = await getActiveContextCodeEditor(); - const doc = codeEditor.doc; - const text = doc.getValue(); - - // Clear all markers - if (this.findResult != null) { - codeEditor.operation(() => { - for (let i = 0; i < this.findResult.length; ++i) { - const marker = this.findResult[i]; - marker.clear(); - } - }); - } - - if (searchTerm !== "") { - searchTerm = escapeRegExp(searchTerm); - - // Find and highlight matches - // Find and highlight matches - // XXX Using \\b and not using the unicode flag probably doesn't - // work with non ascii alphabets, findAndReplace uses a more - // complicated regexp, see - // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/utils.js#L145 - const wholeWordChar = wholeWord ? "\\b" : ""; - const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, - 'g' + (matchCase ? '' : 'i')); - let curLine = 0; - let curChar = 0; - let curMatch = null; - findResult = []; - // All those markText take several seconds on eg this ~500-line - // script, batch them inside an operation so they become - // unnoticeable. Alternatively, an overlay could be used, see - // https://codemirror.net/addon/search/match-highlighter.js ? - codeEditor.operation(() => { - for (let i = 0; i < text.length; ++i) { - // Fetch next match if it's the first time or - // if past the current match start - if ((curMatch == null) || (curMatch.index < i)) { - curMatch = re.exec(text); - if (curMatch == null) { - // No more matches - break; - } - } - // Create a non-selected highlight marker for the match, the - // selected marker highlight will be done later - if (i === curMatch.index) { - let fromPos = { "line" : curLine, "ch" : curChar }; - // XXX If multiline is supported, this needs to - // recalculate curLine since the match may span - // lines - let toPos = { "line" : curLine, "ch" : curChar + curMatch[0].length}; - // XXX or css = "color: #f3" - let marker = doc.markText( fromPos, toPos, { "className" : FIND_RESULT_CSS_CLASSNAME }); - findResult.push(marker); - - // Set the first match beyond the cursor as current - // match - if (currentFound === -1) { - const cursorPos = codeEditor.getCursor(); - if ((fromPos.line > cursorPos.line) || - ((fromPos.line === cursorPos.line) && - (fromPos.ch >= cursorPos.ch))){ - currentFound = totalFound; - } - } - - totalFound++; - } - // Do line and char position tracking - if (text[i] === "\n") { - curLine++; - curChar = 0; - } else { - curChar++; - } - } - }); - } - - this.findResult = findResult; - this.$totalFound.text(totalFound); - // Calculate curfound if not already, highlight it as selected - if (totalFound > 0) { - currentFound = Math.max(0, currentFound) - let marker = findResult[currentFound]; - let pos = marker.find(); - codeEditor.scrollIntoView(pos.to); - marker.clear(); - findResult[currentFound] = doc.markText( pos.from, pos.to, - { "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME } - ); - } - this.$currentFound.text(currentFound + 1); - this.searchTerm = searchTerm; - } - /** * Perform the find and highlight the find results. * - * @param [searchTerm] {string} optional parameter, taken from the input box if - * missing. - * @param [matchCase] {boolean} optional parameter, taken from the checkbox - * state if missing. - * @param [wholeWord] {boolean} optional parameter, taken from the checkbox - * state if missing. + * @param [searchTerm] {string} taken from the input box if missing. + * @param [matchCase] {boolean} taken from the checkbox state if missing. + * @param [wholeWord] {boolean} taken from the checkbox state if missing. */ async performFind(searchTerm, matchCase, wholeWord) { searchTerm = (searchTerm === undefined) ? this.$input.val() : searchTerm; matchCase = (matchCase === undefined) ? this.$caseSensitiveCheckbox.prop("checked") : matchCase; wholeWord = (wholeWord === undefined) ? this.$matchWordsCheckbox.prop("checked") : wholeWord; - const note = appContext.tabManager.getActiveContextNote(); - if (note.type === "code") { - await this.performCodeNoteFind(searchTerm, matchCase, wholeWord); - } else { - await this.performTextNoteFind(searchTerm, matchCase, wholeWord); + + const {totalFound, currentFound} = await this.getHandler().performFind(searchTerm, matchCase, wholeWord); + + this.$totalFound.text(totalFound); + this.$currentFound.text(currentFound); + + this.searchTerm = searchTerm; + } + + async closeSearch() { + this.$findBox.hide(); + + // Restore any state, if there's a current occurrence clear markers + // and scroll to and select the last occurrence + + // XXX Switching to a different tab with crl+tab doesn't invoke + // blur and leaves a stale search which then breaks when + // navigating it + const totalFound = parseInt(this.$totalFound.text()); + const currentFound = parseInt(this.$currentFound.text()) - 1; + + if (totalFound > 0) { + await this.getHandler().cleanup(totalFound, currentFound); } + + this.searchTerm = null; } isEnabled() { @@ -511,4 +236,14 @@ export default class FindWidget extends NoteContextAwareWidget { this.refresh(); } } + + getHandler() { + const note = appContext.tabManager.getActiveContextNote(); + + if (note.type === "code") { + return this.codeHandler; + } else { + return this.textHandler; + } + } } diff --git a/src/public/app/widgets/find_in_code.js b/src/public/app/widgets/find_in_code.js new file mode 100644 index 000000000..764f42f9b --- /dev/null +++ b/src/public/app/widgets/find_in_code.js @@ -0,0 +1,193 @@ +import appContext from "../services/app_context.js"; + +// ck-find-result and ck-find-result_selected are the styles ck-editor +// uses for highlighting matches, use the same one on CodeMirror +// for consistency +const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected"; +const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; + +const getActiveContextCodeEditor = async () => await appContext.tabManager.getActiveContextCodeEditor(); +const escapeRegExp = str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +export default class FindInCode { + async getInitialSearchTerm() { + const codeEditor = await getActiveContextCodeEditor(); + + // highlightSelectionMatches is the overlay that highlights + // the words under the cursor. This occludes the search + // markers style, save it, disable it. Will be restored when + // the focus is back into the note + this.oldHighlightSelectionMatches = codeEditor.getOption("highlightSelectionMatches"); + codeEditor.setOption("highlightSelectionMatches", false); + + // Fill in the findbox with the current selection if any + const selectedText = codeEditor.getSelection() + if (selectedText !== "") { + return selectedText; + } + } + + async performFind(searchTerm, matchCase, wholeWord) { + let findResult = null; + let totalFound = 0; + let currentFound = -1; + + // See https://codemirror.net/addon/search/searchcursor.js for tips + const codeEditor = await getActiveContextCodeEditor(); + const doc = codeEditor.doc; + const text = doc.getValue(); + + // Clear all markers + if (this.findResult != null) { + codeEditor.operation(() => { + for (let i = 0; i < this.findResult.length; ++i) { + const marker = this.findResult[i]; + marker.clear(); + } + }); + } + + if (searchTerm !== "") { + searchTerm = escapeRegExp(searchTerm); + + // Find and highlight matches + // Find and highlight matches + // XXX Using \\b and not using the unicode flag probably doesn't + // work with non ascii alphabets, findAndReplace uses a more + // complicated regexp, see + // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/utils.js#L145 + const wholeWordChar = wholeWord ? "\\b" : ""; + const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, + 'g' + (matchCase ? '' : 'i')); + let curLine = 0; + let curChar = 0; + let curMatch = null; + findResult = []; + // All those markText take several seconds on eg this ~500-line + // script, batch them inside an operation so they become + // unnoticeable. Alternatively, an overlay could be used, see + // https://codemirror.net/addon/search/match-highlighter.js ? + codeEditor.operation(() => { + for (let i = 0; i < text.length; ++i) { + // Fetch next match if it's the first time or + // if past the current match start + if ((curMatch == null) || (curMatch.index < i)) { + curMatch = re.exec(text); + if (curMatch == null) { + // No more matches + break; + } + } + // Create a non-selected highlight marker for the match, the + // selected marker highlight will be done later + if (i === curMatch.index) { + let fromPos = { "line" : curLine, "ch" : curChar }; + // XXX If multiline is supported, this needs to + // recalculate curLine since the match may span + // lines + let toPos = { "line" : curLine, "ch" : curChar + curMatch[0].length}; + // XXX or css = "color: #f3" + let marker = doc.markText( fromPos, toPos, { "className" : FIND_RESULT_CSS_CLASSNAME }); + findResult.push(marker); + + // Set the first match beyond the cursor as current + // match + if (currentFound === -1) { + const cursorPos = codeEditor.getCursor(); + if ((fromPos.line > cursorPos.line) || + ((fromPos.line === cursorPos.line) && + (fromPos.ch >= cursorPos.ch))){ + currentFound = totalFound; + } + } + + totalFound++; + } + // Do line and char position tracking + if (text[i] === "\n") { + curLine++; + curChar = 0; + } else { + curChar++; + } + } + }); + } + + this.findResult = findResult; + + // Calculate curfound if not already, highlight it as selected + if (totalFound > 0) { + currentFound = Math.max(0, currentFound) + let marker = findResult[currentFound]; + let pos = marker.find(); + codeEditor.scrollIntoView(pos.to); + marker.clear(); + findResult[currentFound] = doc.markText( pos.from, pos.to, + { "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME } + ); + } + + return { + totalFound, + currentFound: currentFound + 1 + }; + } + + async findNext(direction, currentFound, nextFound) { + const codeEditor = await getActiveContextCodeEditor(); + const doc = codeEditor.doc; + + // + // Dehighlight current, highlight & scrollIntoView next + // + + let marker = this.findResult[currentFound]; + let pos = marker.find(); + marker.clear(); + marker = doc.markText( + pos.from, pos.to, + { "className" : FIND_RESULT_CSS_CLASSNAME } + ); + this.findResult[currentFound] = marker; + + marker = this.findResult[nextFound]; + pos = marker.find(); + marker.clear(); + marker = doc.markText( + pos.from, pos.to, + { "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME } + ); + this.findResult[nextFound] = marker; + + codeEditor.scrollIntoView(pos.from); + } + + async cleanup(totalFound, currentFound) { + const codeEditor = await getActiveContextCodeEditor(); + + if (totalFound > 0) { + const doc = codeEditor.doc; + const pos = this.findResult[currentFound].find(); + // Note setting the selection sets the cursor to + // the end of the selection and scrolls it into + // view + doc.setSelection(pos.from, pos.to); + // Clear all markers + codeEditor.operation(() => { + for (let i = 0; i < this.findResult.length; ++i) { + let marker = this.findResult[i]; + marker.clear(); + } + }); + } + // Restore the highlightSelectionMatches setting + codeEditor.setOption("highlightSelectionMatches", this.oldHighlightSelectionMatches); + this.findResult = null; + } + + async close() { + const codeEditor = await getActiveContextCodeEditor(); + codeEditor.focus(); + } +} diff --git a/src/public/app/widgets/find_in_text.js b/src/public/app/widgets/find_in_text.js new file mode 100644 index 000000000..5db1a8b63 --- /dev/null +++ b/src/public/app/widgets/find_in_text.js @@ -0,0 +1,116 @@ +import appContext from "../services/app_context.js"; + +const getActiveContextTextEditor = async () => await appContext.tabManager.getActiveContextTextEditor(); + +export default class FindInText { + async getInitialSearchTerm() { + const textEditor = await getActiveContextTextEditor(); + + const selection = textEditor.model.document.selection; + const range = selection.getFirstRange(); + + for (const item of range.getItems()) { + // Fill in the findbox with the current selection if + // any + return item.data; + } + } + + async performFind(searchTerm, matchCase, wholeWord) { + // Do this even if the searchTerm is empty so the markers are cleared and + // the counters updated + const textEditor = await getActiveContextTextEditor(); + const model = textEditor.model; + let findResult = null; + let totalFound = 0; + let currentFound = -1; + + // Clear + const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); + findAndReplaceEditing.state.clear(model); + findAndReplaceEditing.stop(); + if (searchTerm !== "") { + // Parameters are callback/text, options.matchCase=false, options.wholeWords=false + // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findcommand.js#L44 + // XXX Need to use the callback version for regexp + // searchTerm = escapeRegExp(searchTerm); + // let re = new RegExp(searchTerm, 'gi'); + // let m = text.match(re); + // totalFound = m ? m.length : 0; + const options = { "matchCase" : matchCase, "wholeWords" : wholeWord }; + findResult = textEditor.execute('find', searchTerm, options); + totalFound = findResult.results.length; + // Find the result beyond the cursor + const cursorPos = model.document.selection.getLastPosition(); + for (let i = 0; i < findResult.results.length; ++i) { + const marker = findResult.results.get(i).marker; + const fromPos = marker.getStart(); + if (fromPos.compareWith(cursorPos) !== "before") { + currentFound = i; + break; + } + } + } + + this.findResult = findResult; + + // Calculate curfound if not already, highlight it as + // selected + if (totalFound > 0) { + currentFound = Math.max(0, currentFound); + // XXX Do this accessing the private data? + // See + // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js + for (let i = 0 ; i < currentFound; ++i) { + textEditor.execute('findNext', searchTerm); + } + } + + return { + totalFound, + currentFound: currentFound + 1 + }; + } + + async findNext(direction, currentFound, nextFound) { + const textEditor = await getActiveContextTextEditor(); + + // There are no parameters for findNext/findPrev + // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js#L57 + // curFound wrap around above assumes findNext and + // findPrevious wraparound, which is what they do + if (direction > 0) { + textEditor.execute('findNext'); + } else { + textEditor.execute('findPrevious'); + } + } + + async cleanup(totalFound, currentFound) { + if (totalFound > 0) { + const textEditor = await getActiveContextTextEditor(); + // Clear the markers and set the caret to the + // current occurrence + const model = textEditor.model; + const range = this.findResult.results.get(currentFound).marker.getRange(); + // From + // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92 + // XXX Roll our own since already done for codeEditor and + // will probably allow more refactoring? + let findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); + findAndReplaceEditing.state.clear(model); + findAndReplaceEditing.stop(); + model.change(writer => { + writer.setSelection(range, 0); + }); + textEditor.editing.view.scrollToTheSelection(); + } + + this.findResult = null; + } + + async close() { + const textEditor = await getActiveContextTextEditor(); + textEditor.focus(); + } +} From 04379b4e1f58e68087e890e4ccb649a7b21a4420 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 17 May 2022 20:22:33 +0200 Subject: [PATCH 011/250] delay protected session expiration check after DB init, fixes #2855 --- src/services/protected_session.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/services/protected_session.js b/src/services/protected_session.js index ad125deca..22e531738 100644 --- a/src/services/protected_session.js +++ b/src/services/protected_session.js @@ -3,6 +3,7 @@ const log = require('./log'); const dataEncryptionService = require('./data_encryption'); const options = require("./options"); +const sqlInit = require("./sql_init"); let dataKey = null; @@ -63,17 +64,19 @@ function touchProtectedSession() { } } -setInterval(() => { - const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout'); - if (isProtectedSessionAvailable() - && lastProtectedSessionOperationDate - && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) { +sqlInit.dbReady.then(() => { + setInterval(() => { + const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout'); + if (isProtectedSessionAvailable() + && lastProtectedSessionOperationDate + && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) { - resetDataKey(); + resetDataKey(); - require('./ws').reloadFrontend(); - } -}, 30000); + require('./ws').reloadFrontend(); + } + }, 30000); +}); module.exports = { From 5bc629d1c7060d042abebdf0cfeba22ece97b953 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 17 May 2022 20:39:21 +0200 Subject: [PATCH 012/250] moved protected session expiration scheduling #2855 --- src/services/protected_session.js | 25 ++++++++++++------------- src/services/scheduler.js | 3 +++ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/services/protected_session.js b/src/services/protected_session.js index 22e531738..c6098cc2f 100644 --- a/src/services/protected_session.js +++ b/src/services/protected_session.js @@ -3,7 +3,6 @@ const log = require('./log'); const dataEncryptionService = require('./data_encryption'); const options = require("./options"); -const sqlInit = require("./sql_init"); let dataKey = null; @@ -64,20 +63,19 @@ function touchProtectedSession() { } } -sqlInit.dbReady.then(() => { - setInterval(() => { - const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout'); - if (isProtectedSessionAvailable() - && lastProtectedSessionOperationDate - && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) { +function checkProtectedSessionExpiration() { + const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout'); + if (isProtectedSessionAvailable() + && lastProtectedSessionOperationDate + && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) { - resetDataKey(); + resetDataKey(); - require('./ws').reloadFrontend(); - } - }, 30000); -}); + log.info("Expiring protected session"); + require('./ws').reloadFrontend(); + } +} module.exports = { setDataKey, @@ -87,5 +85,6 @@ module.exports = { decrypt, decryptString, decryptNotes, - touchProtectedSession + touchProtectedSession, + checkProtectedSessionExpiration }; diff --git a/src/services/scheduler.js b/src/services/scheduler.js index f2d88e09a..3f253ba9b 100644 --- a/src/services/scheduler.js +++ b/src/services/scheduler.js @@ -6,6 +6,7 @@ const log = require('./log'); const sql = require("./sql"); const becca = require("../becca/becca"); const specialNotesService = require("../services/special_notes"); +const protectedSessionService = require("../services/protected_session"); function getRunAtHours(note) { try { @@ -59,4 +60,6 @@ sqlInit.dbReady.then(() => { setTimeout(cls.wrap(() => specialNotesService.createMissingSpecialNotes()), 10 * 1000); } + + setInterval(() => protectedSessionService.checkProtectedSessionExpiration(), 30000); }); From c24c807921b8a9ace92239bf2bce2456b4dcc00b Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 17 May 2022 20:22:33 +0200 Subject: [PATCH 013/250] delay protected session expiration check after DB init, fixes #2855 --- src/services/protected_session.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/services/protected_session.js b/src/services/protected_session.js index ad125deca..22e531738 100644 --- a/src/services/protected_session.js +++ b/src/services/protected_session.js @@ -3,6 +3,7 @@ const log = require('./log'); const dataEncryptionService = require('./data_encryption'); const options = require("./options"); +const sqlInit = require("./sql_init"); let dataKey = null; @@ -63,17 +64,19 @@ function touchProtectedSession() { } } -setInterval(() => { - const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout'); - if (isProtectedSessionAvailable() - && lastProtectedSessionOperationDate - && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) { +sqlInit.dbReady.then(() => { + setInterval(() => { + const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout'); + if (isProtectedSessionAvailable() + && lastProtectedSessionOperationDate + && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) { - resetDataKey(); + resetDataKey(); - require('./ws').reloadFrontend(); - } -}, 30000); + require('./ws').reloadFrontend(); + } + }, 30000); +}); module.exports = { From 37eb16b2f30f528a2fedd9ef4cc6e6ff084469b4 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 17 May 2022 20:39:21 +0200 Subject: [PATCH 014/250] moved protected session expiration scheduling #2855 --- src/services/protected_session.js | 25 ++++++++++++------------- src/services/scheduler.js | 3 +++ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/services/protected_session.js b/src/services/protected_session.js index 22e531738..c6098cc2f 100644 --- a/src/services/protected_session.js +++ b/src/services/protected_session.js @@ -3,7 +3,6 @@ const log = require('./log'); const dataEncryptionService = require('./data_encryption'); const options = require("./options"); -const sqlInit = require("./sql_init"); let dataKey = null; @@ -64,20 +63,19 @@ function touchProtectedSession() { } } -sqlInit.dbReady.then(() => { - setInterval(() => { - const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout'); - if (isProtectedSessionAvailable() - && lastProtectedSessionOperationDate - && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) { +function checkProtectedSessionExpiration() { + const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout'); + if (isProtectedSessionAvailable() + && lastProtectedSessionOperationDate + && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) { - resetDataKey(); + resetDataKey(); - require('./ws').reloadFrontend(); - } - }, 30000); -}); + log.info("Expiring protected session"); + require('./ws').reloadFrontend(); + } +} module.exports = { setDataKey, @@ -87,5 +85,6 @@ module.exports = { decrypt, decryptString, decryptNotes, - touchProtectedSession + touchProtectedSession, + checkProtectedSessionExpiration }; diff --git a/src/services/scheduler.js b/src/services/scheduler.js index f2d88e09a..3f253ba9b 100644 --- a/src/services/scheduler.js +++ b/src/services/scheduler.js @@ -6,6 +6,7 @@ const log = require('./log'); const sql = require("./sql"); const becca = require("../becca/becca"); const specialNotesService = require("../services/special_notes"); +const protectedSessionService = require("../services/protected_session"); function getRunAtHours(note) { try { @@ -59,4 +60,6 @@ sqlInit.dbReady.then(() => { setTimeout(cls.wrap(() => specialNotesService.createMissingSpecialNotes()), 10 * 1000); } + + setInterval(() => protectedSessionService.checkProtectedSessionExpiration(), 30000); }); From fca0b82610e2eed352f00da784851274a0fe7af2 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 17 May 2022 22:11:45 +0200 Subject: [PATCH 015/250] find widget fixes --- package-lock.json | 25 ++++++------------- package.json | 2 +- src/public/app/widgets/find.js | 1 - .../app/widgets/type_widgets/editable_code.js | 4 +-- 4 files changed, 10 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa9248ab0..57db450ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,14 +24,13 @@ "ejs": "3.1.8", "electron-debug": "3.2.0", "electron-dl": "3.3.1", - "electron-find": "1.0.7", "electron-window-state": "5.0.3", "express": "4.18.1", "express-partial-content": "1.0.2", "express-rate-limit": "6.4.0", "express-session": "1.17.3", "fs-extra": "10.1.0", - "helmet": "5.0.2", + "helmet": "5.1.0", "html": "1.0.0", "html2plaintext": "2.1.4", "http-proxy-agent": "5.0.0", @@ -3758,11 +3757,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/electron-find": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/electron-find/-/electron-find-1.0.7.tgz", - "integrity": "sha512-C2FQJuk8567P2a2loBNwl5c8kwOTQVMB0capgHtPI7zKwZG16X0UxG+sNYZExQfnJ0PA+ecECA/4LcXxQa2TCA==" - }, "node_modules/electron-installer-common": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/electron-installer-common/-/electron-installer-common-0.10.3.tgz", @@ -5940,9 +5934,9 @@ } }, "node_modules/helmet": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz", - "integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.1.0.tgz", + "integrity": "sha512-klsunXs8rgNSZoaUrNeuCiWUxyc+wzucnEnFejUg3/A+CaF589k9qepLZZ1Jehnzig7YbD4hEuscGXuBY3fq+g==", "engines": { "node": ">=12.0.0" } @@ -14125,11 +14119,6 @@ "unused-filename": "^2.1.0" } }, - "electron-find": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/electron-find/-/electron-find-1.0.7.tgz", - "integrity": "sha512-C2FQJuk8567P2a2loBNwl5c8kwOTQVMB0capgHtPI7zKwZG16X0UxG+sNYZExQfnJ0PA+ecECA/4LcXxQa2TCA==" - }, "electron-installer-common": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/electron-installer-common/-/electron-installer-common-0.10.3.tgz", @@ -15684,9 +15673,9 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, "helmet": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz", - "integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg==" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.1.0.tgz", + "integrity": "sha512-klsunXs8rgNSZoaUrNeuCiWUxyc+wzucnEnFejUg3/A+CaF589k9qepLZZ1Jehnzig7YbD4hEuscGXuBY3fq+g==" }, "hosted-git-info": { "version": "2.8.9", diff --git a/package.json b/package.json index 5b7042461..e1a373cc3 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "express-rate-limit": "6.4.0", "express-session": "1.17.3", "fs-extra": "10.1.0", - "helmet": "5.0.2", + "helmet": "5.1.0", "html": "1.0.0", "html2plaintext": "2.1.4", "http-proxy-agent": "5.0.0", diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index b204cffb1..0c8975886 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -80,7 +80,6 @@ export default class FindWidget extends NoteContextAwareWidget { this.$caseSensitiveCheckbox.change(() => this.performFind()); this.$matchWordsCheckbox = this.$widget.find(".find-widget-match-words-checkbox"); this.$matchWordsCheckbox.change(() => this.performFind()); - this.findResult = null; this.searchTerm = null; this.$input.keydown(async e => { diff --git a/src/public/app/widgets/type_widgets/editable_code.js b/src/public/app/widgets/type_widgets/editable_code.js index 7bbc356c3..afffd9e8a 100644 --- a/src/public/app/widgets/type_widgets/editable_code.js +++ b/src/public/app/widgets/type_widgets/editable_code.js @@ -171,13 +171,13 @@ export default class EditableCodeTypeWidget extends TypeWidget { } } - async executeInActiveCodeEditorEvent({callback}) { + async executeInActiveCodeEditorEvent({resolve}) { if (!this.isActive()) { return; } await this.initialized; - callback(this.codeEditor); + resolve(this.codeEditor); } } From 4978a3ff1a68ed8c516864916978456b57f6612c Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 17 May 2022 23:22:28 +0200 Subject: [PATCH 016/250] find widget improvements --- src/public/app/layouts/desktop_layout.js | 2 +- src/public/app/widgets/find.js | 115 ++++++++++------------- 2 files changed, 50 insertions(+), 67 deletions(-) diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index a5429521e..2723076ce 100644 --- a/src/public/app/layouts/desktop_layout.js +++ b/src/public/app/layouts/desktop_layout.js @@ -162,10 +162,10 @@ export default class DesktopLayout { .child(new SearchResultWidget()) .child(new SqlResultWidget()) ) + .child(new FindWidget()) .child(...this.customWidgets.get('node-detail-pane')) ) ) - .child(new FindWidget()) .child(...this.customWidgets.get('center-pane')) ) .child(new RightPaneContainer() diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 0c8975886..0e5ad3b7a 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -4,7 +4,6 @@ */ import NoteContextAwareWidget from "./note_context_aware_widget.js"; -import appContext from "../services/app_context.js"; import FindInText from "./find_in_text.js"; import FindInCode from "./find_in_code.js"; @@ -34,10 +33,18 @@ const TPL = ` .find-widget-found-wrapper { font-weight: bold; } + + .find-widget-search-term-input { + max-width: 250px; + } + + .find-widget-spacer { + flex-grow: 1; + }
- +
+ +
+ +
`; @@ -65,6 +76,8 @@ export default class FindWidget extends NoteContextAwareWidget { constructor() { super(); + this.searchTerm = null; + this.textHandler = new FindInText(); this.codeHandler = new FindInCode(); } @@ -80,7 +93,8 @@ export default class FindWidget extends NoteContextAwareWidget { this.$caseSensitiveCheckbox.change(() => this.performFind()); this.$matchWordsCheckbox = this.$widget.find(".find-widget-match-words-checkbox"); this.$matchWordsCheckbox.change(() => this.performFind()); - this.searchTerm = null; + this.$closeButton = this.$widget.find(".find-widget-close-button"); + this.$closeButton.on("click", () => this.closeSearch()); this.$input.keydown(async e => { if ((e.metaKey || e.ctrlKey) && (e.key === 'F' || e.key === 'f')) { @@ -92,26 +106,12 @@ export default class FindWidget extends NoteContextAwareWidget { e.preventDefault(); return false; } else if (e.key === 'Escape') { - await this.getHandler().close(); + await this.closeSearch(); } }); this.$input.on('input', () => this.startSearch()); - // Note blur doesn't bubble to parent div, but the parent div needs to - // detect when any of the children are not focused and hide. Use - // focusout instead which does bubble to the parent div. - this.$findBox.on('focusout', async e => { - // e.relatedTarget is the new focused element, note it can be null - // if nothing is being focused - if (this.$findBox[0].contains(e.relatedTarget)) { - // The focused element is inside this div, ignore - return; - } - - await this.closeSearch(); - }); - return this.$widget; } @@ -128,12 +128,9 @@ export default class FindWidget extends NoteContextAwareWidget { // immediately, as this can cause search word typing lag with // one or two-char searchwords and long notes // See https://github.com/antoniotejada/Trilium-FindWidget/issues/1 - const searchTerm = this.$input.val(); - const matchCase = this.$caseSensitiveCheckbox.prop("checked"); - const wholeWord = this.$matchWordsCheckbox.prop("checked"); this.timeoutId = setTimeout(async () => { this.timeoutId = null; - await this.performFind(searchTerm, matchCase, wholeWord); + await this.performFind(); }, findWidgetDelayMillis); } } @@ -141,7 +138,7 @@ export default class FindWidget extends NoteContextAwareWidget { async findNext(e) { const searchTerm = this.$input.val(); if (waitForEnter && this.searchTerm !== searchTerm) { - await this.performFind(searchTerm); + await this.performFind(); } const totalFound = parseInt(this.$totalFound.text()); const currentFound = parseInt(this.$currentFound.text()) - 1; @@ -163,41 +160,35 @@ export default class FindWidget extends NoteContextAwareWidget { } async findInTextEvent() { - const note = appContext.tabManager.getActiveContextNote(); // Only writeable text and code supported - const readOnly = note.getAttribute("label", "readOnly"); - if (!readOnly && (note.type === "code" || note.type === "text")) { - if (this.$findBox.is(":hidden")) { - this.$findBox.show(); - this.$input.focus(); - this.$totalFound.text(0); - this.$currentFound.text(0); + const readOnly = await this.noteContext.isReadOnly(); - const searchTerm = await this.getHandler().getInitialSearchTerm(); + if (readOnly || !['text', 'code'].includes(this.note.type) || !this.$findBox.is(":hidden")) { + return; + } - this.$input.val(searchTerm || ""); + this.$findBox.show(); + this.$input.focus(); + this.$totalFound.text(0); + this.$currentFound.text(0); - // Directly perform the search if there's some text to - // find, without delaying or waiting for enter - if (searchTerm !== "") { - this.$input.select(); - await this.performFind(searchTerm); - } - } + const searchTerm = await this.getHandler().getInitialSearchTerm(); + + this.$input.val(searchTerm || ""); + + // Directly perform the search if there's some text to + // find, without delaying or waiting for enter + if (searchTerm !== "") { + this.$input.select(); + await this.performFind(); } } - /** - * Perform the find and highlight the find results. - * - * @param [searchTerm] {string} taken from the input box if missing. - * @param [matchCase] {boolean} taken from the checkbox state if missing. - * @param [wholeWord] {boolean} taken from the checkbox state if missing. - */ - async performFind(searchTerm, matchCase, wholeWord) { - searchTerm = (searchTerm === undefined) ? this.$input.val() : searchTerm; - matchCase = (matchCase === undefined) ? this.$caseSensitiveCheckbox.prop("checked") : matchCase; - wholeWord = (wholeWord === undefined) ? this.$matchWordsCheckbox.prop("checked") : wholeWord; + /** Perform the find and highlight the find results. */ + async performFind() { + const searchTerm = this.$input.val(); + const matchCase = this.$caseSensitiveCheckbox.prop("checked"); + const wholeWord = this.$matchWordsCheckbox.prop("checked"); const {totalFound, currentFound} = await this.getHandler().performFind(searchTerm, matchCase, wholeWord); @@ -212,10 +203,6 @@ export default class FindWidget extends NoteContextAwareWidget { // Restore any state, if there's a current occurrence clear markers // and scroll to and select the last occurrence - - // XXX Switching to a different tab with crl+tab doesn't invoke - // blur and leaves a stale search which then breaks when - // navigating it const totalFound = parseInt(this.$totalFound.text()); const currentFound = parseInt(this.$currentFound.text()) - 1; @@ -226,23 +213,19 @@ export default class FindWidget extends NoteContextAwareWidget { this.searchTerm = null; } - isEnabled() { - return super.isEnabled() && (this.note.type === 'text' || this.note.type === 'code'); - } - async entitiesReloadedEvent({loadResults}) { if (loadResults.isNoteContentReloaded(this.noteId)) { this.refresh(); } } - getHandler() { - const note = appContext.tabManager.getActiveContextNote(); + isEnabled() { + return super.isEnabled() && ['text', 'code'].includes(this.note.type); + } - if (note.type === "code") { - return this.codeHandler; - } else { - return this.textHandler; - } + getHandler() { + return this.note.type === "code" + ? this.codeHandler + : this.textHandler; } } From cd622cbdd7ad0a8ba7b4ad5e24c50077018d5b44 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 17 May 2022 23:53:35 +0200 Subject: [PATCH 017/250] find widget refactoring to use note context --- src/public/app/dialogs/markdown_import.js | 5 +++-- .../app/services/frontend_script_api.js | 6 ++--- src/public/app/services/note_context.js | 15 +++++++++++++ src/public/app/services/tab_manager.js | 8 ------- src/public/app/widgets/find.js | 18 +++++++++------ src/public/app/widgets/find_in_code.js | 21 +++++++++++------- src/public/app/widgets/find_in_text.js | 22 +++++++++++-------- .../app/widgets/type_widgets/editable_code.js | 4 ++-- .../app/widgets/type_widgets/editable_text.js | 4 ++-- 9 files changed, 62 insertions(+), 41 deletions(-) diff --git a/src/public/app/dialogs/markdown_import.js b/src/public/app/dialogs/markdown_import.js index e83522cf7..94712b28e 100644 --- a/src/public/app/dialogs/markdown_import.js +++ b/src/public/app/dialogs/markdown_import.js @@ -16,7 +16,7 @@ async function convertMarkdownToHtml(text) { const result = writer.render(parsed); - appContext.triggerCommand('executeInActiveTextEditor', { + appContext.triggerCommand('executeInTextEditor', { callback: textEditor => { const viewFragment = textEditor.data.processor.toView(result); const modelFragment = textEditor.data.toModel(viewFragment); @@ -24,7 +24,8 @@ async function convertMarkdownToHtml(text) { textEditor.model.insertContent(modelFragment, textEditor.model.document.selection); toastService.showMessage("Markdown content has been imported into the document."); - } + }, + ntxId: this.ntxId }); } diff --git a/src/public/app/services/frontend_script_api.js b/src/public/app/services/frontend_script_api.js index dee3b7a7d..b2eabdc13 100644 --- a/src/public/app/services/frontend_script_api.js +++ b/src/public/app/services/frontend_script_api.js @@ -384,7 +384,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain this.getActiveTabTextEditor = callback => { console.warn("api.getActiveTabTextEditor() is deprecated, use getActiveContextTextEditor() instead."); - return appContext.tabManager.getActiveContextTextEditor(callback); + return appContext.tabManager.getActiveContext()?.getTextEditor(callback); }; /** @@ -393,7 +393,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * @method * @returns {Promise} instance of CKEditor */ - this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContextTextEditor(); + this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContext()?.getTextEditor(); /** * See https://codemirror.net/doc/manual.html#api @@ -401,7 +401,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * @method * @returns {Promise} instance of CodeMirror */ - this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContextCodeEditor(); + this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContext()?.getCodeEditor(); /** * Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the diff --git a/src/public/app/services/note_context.js b/src/public/app/services/note_context.js index 2b7e71fa3..179fd1aea 100644 --- a/src/public/app/services/note_context.js +++ b/src/public/app/services/note_context.js @@ -226,6 +226,21 @@ class NoteContext extends Component { && this.note.mime !== 'text/x-sqlite;schema=trilium' && !this.note.hasLabel('hideChildrenOverview'); } + + async getTextEditor(callback) { + return new Promise(resolve => appContext.triggerCommand('executeInTextEditor', { + callback, + resolve, + ntxId: this.ntxId + })); + } + + async getCodeEditor() { + return new Promise(resolve => appContext.triggerCommand('executeInCodeEditor', { + resolve, + ntxId: this.ntxId + })); + } } export default NoteContext; diff --git a/src/public/app/services/tab_manager.js b/src/public/app/services/tab_manager.js index 1250b6948..15f939bfe 100644 --- a/src/public/app/services/tab_manager.js +++ b/src/public/app/services/tab_manager.js @@ -193,14 +193,6 @@ export default class TabManager extends Component { return activeNote ? activeNote.type : null; } - async getActiveContextTextEditor(callback) { - return new Promise(resolve => appContext.triggerCommand('executeInActiveTextEditor', {callback, resolve})); - } - - async getActiveContextCodeEditor() { - return new Promise(resolve => appContext.triggerCommand('executeInActiveCodeEditor', {resolve})); - } - async switchToNoteContext(ntxId, notePath) { const noteContext = this.noteContexts.find(nc => nc.ntxId === ntxId) || await this.openEmptyTab(); diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 0e5ad3b7a..9e0590ce9 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -78,8 +78,8 @@ export default class FindWidget extends NoteContextAwareWidget { this.searchTerm = null; - this.textHandler = new FindInText(); - this.codeHandler = new FindInCode(); + this.textHandler = new FindInText(this); + this.codeHandler = new FindInCode(this); } doRender() { @@ -155,11 +155,15 @@ export default class FindWidget extends NoteContextAwareWidget { this.$currentFound.text(nextFound + 1); - await this.getHandler().findNext(direction, currentFound, nextFound); + await this.handler.findNext(direction, currentFound, nextFound); } } async findInTextEvent() { + if (!this.isActiveNoteContext()) { + return; + } + // Only writeable text and code supported const readOnly = await this.noteContext.isReadOnly(); @@ -172,7 +176,7 @@ export default class FindWidget extends NoteContextAwareWidget { this.$totalFound.text(0); this.$currentFound.text(0); - const searchTerm = await this.getHandler().getInitialSearchTerm(); + const searchTerm = await this.handler.getInitialSearchTerm(); this.$input.val(searchTerm || ""); @@ -190,7 +194,7 @@ export default class FindWidget extends NoteContextAwareWidget { const matchCase = this.$caseSensitiveCheckbox.prop("checked"); const wholeWord = this.$matchWordsCheckbox.prop("checked"); - const {totalFound, currentFound} = await this.getHandler().performFind(searchTerm, matchCase, wholeWord); + const {totalFound, currentFound} = await this.handler.performFind(searchTerm, matchCase, wholeWord); this.$totalFound.text(totalFound); this.$currentFound.text(currentFound); @@ -207,7 +211,7 @@ export default class FindWidget extends NoteContextAwareWidget { const currentFound = parseInt(this.$currentFound.text()) - 1; if (totalFound > 0) { - await this.getHandler().cleanup(totalFound, currentFound); + await this.handler.cleanup(totalFound, currentFound); } this.searchTerm = null; @@ -223,7 +227,7 @@ export default class FindWidget extends NoteContextAwareWidget { return super.isEnabled() && ['text', 'code'].includes(this.note.type); } - getHandler() { + get handler() { return this.note.type === "code" ? this.codeHandler : this.textHandler; diff --git a/src/public/app/widgets/find_in_code.js b/src/public/app/widgets/find_in_code.js index 764f42f9b..7a6bcbced 100644 --- a/src/public/app/widgets/find_in_code.js +++ b/src/public/app/widgets/find_in_code.js @@ -1,17 +1,22 @@ -import appContext from "../services/app_context.js"; - // ck-find-result and ck-find-result_selected are the styles ck-editor // uses for highlighting matches, use the same one on CodeMirror // for consistency const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected"; const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; -const getActiveContextCodeEditor = async () => await appContext.tabManager.getActiveContextCodeEditor(); const escapeRegExp = str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); export default class FindInCode { + constructor(parent) { + this.parent = parent; + } + + async getCodeEditor() { + return this.parent.noteContext.getCodeEditor(); + } + async getInitialSearchTerm() { - const codeEditor = await getActiveContextCodeEditor(); + const codeEditor = await this.getCodeEditor(); // highlightSelectionMatches is the overlay that highlights // the words under the cursor. This occludes the search @@ -33,7 +38,7 @@ export default class FindInCode { let currentFound = -1; // See https://codemirror.net/addon/search/searchcursor.js for tips - const codeEditor = await getActiveContextCodeEditor(); + const codeEditor = await this.getCodeEditor(); const doc = codeEditor.doc; const text = doc.getValue(); @@ -135,7 +140,7 @@ export default class FindInCode { } async findNext(direction, currentFound, nextFound) { - const codeEditor = await getActiveContextCodeEditor(); + const codeEditor = await this.getCodeEditor(); const doc = codeEditor.doc; // @@ -164,7 +169,7 @@ export default class FindInCode { } async cleanup(totalFound, currentFound) { - const codeEditor = await getActiveContextCodeEditor(); + const codeEditor = await this.getCodeEditor(); if (totalFound > 0) { const doc = codeEditor.doc; @@ -187,7 +192,7 @@ export default class FindInCode { } async close() { - const codeEditor = await getActiveContextCodeEditor(); + const codeEditor = await this.getCodeEditor(); codeEditor.focus(); } } diff --git a/src/public/app/widgets/find_in_text.js b/src/public/app/widgets/find_in_text.js index 5db1a8b63..54f3bd091 100644 --- a/src/public/app/widgets/find_in_text.js +++ b/src/public/app/widgets/find_in_text.js @@ -1,10 +1,14 @@ -import appContext from "../services/app_context.js"; - -const getActiveContextTextEditor = async () => await appContext.tabManager.getActiveContextTextEditor(); - export default class FindInText { + constructor(parent) { + this.parent = parent; + } + + async getTextEditor() { + return this.parent.noteContext.getTextEditor(); + } + async getInitialSearchTerm() { - const textEditor = await getActiveContextTextEditor(); + const textEditor = await this.getTextEditor(); const selection = textEditor.model.document.selection; const range = selection.getFirstRange(); @@ -19,7 +23,7 @@ export default class FindInText { async performFind(searchTerm, matchCase, wholeWord) { // Do this even if the searchTerm is empty so the markers are cleared and // the counters updated - const textEditor = await getActiveContextTextEditor(); + const textEditor = await this.getTextEditor(); const model = textEditor.model; let findResult = null; let totalFound = 0; @@ -73,7 +77,7 @@ export default class FindInText { } async findNext(direction, currentFound, nextFound) { - const textEditor = await getActiveContextTextEditor(); + const textEditor = await this.getTextEditor(); // There are no parameters for findNext/findPrev // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js#L57 @@ -88,7 +92,7 @@ export default class FindInText { async cleanup(totalFound, currentFound) { if (totalFound > 0) { - const textEditor = await getActiveContextTextEditor(); + const textEditor = await this.getTextEditor(); // Clear the markers and set the caret to the // current occurrence const model = textEditor.model; @@ -110,7 +114,7 @@ export default class FindInText { } async close() { - const textEditor = await getActiveContextTextEditor(); + const textEditor = await this.getTextEditor(); textEditor.focus(); } } diff --git a/src/public/app/widgets/type_widgets/editable_code.js b/src/public/app/widgets/type_widgets/editable_code.js index afffd9e8a..2a4097f1c 100644 --- a/src/public/app/widgets/type_widgets/editable_code.js +++ b/src/public/app/widgets/type_widgets/editable_code.js @@ -171,8 +171,8 @@ export default class EditableCodeTypeWidget extends TypeWidget { } } - async executeInActiveCodeEditorEvent({resolve}) { - if (!this.isActive()) { + async executeInCodeEditorEvent({resolve, ntxId}) { + if (!this.isNoteContext(ntxId)) { return; } diff --git a/src/public/app/widgets/type_widgets/editable_text.js b/src/public/app/widgets/type_widgets/editable_text.js index 44469fdc6..f51138289 100644 --- a/src/public/app/widgets/type_widgets/editable_text.js +++ b/src/public/app/widgets/type_widgets/editable_text.js @@ -229,8 +229,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { return !selection.isCollapsed; } - async executeInActiveTextEditorEvent({callback, resolve}) { - if (!this.isActive()) { + async executeInTextEditorEvent({callback, resolve, ntxId}) { + if (!this.isNoteContext(ntxId)) { return; } From 9e089cc7cdb8aa8ee269ed13ab7930879fb2152e Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 18 May 2022 22:56:29 +0200 Subject: [PATCH 018/250] disable COEP, fixes #2858 --- src/app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app.js b/src/app.js index abc3c2451..0b984448c 100644 --- a/src/app.js +++ b/src/app.js @@ -20,7 +20,8 @@ app.set('view engine', 'ejs'); app.use(helmet({ hidePoweredBy: false, // errors out in electron - contentSecurityPolicy: false + contentSecurityPolicy: false, + crossOriginEmbedderPolicy: false })); app.use(express.text({limit: '500mb'})); From 2085dc5ed4090c2173c878c817b099ee0b26d46f Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 19 May 2022 23:00:07 +0200 Subject: [PATCH 019/250] minor canvas note cleanup --- package.json | 2 +- src/public/app/widgets/type_widgets/canvas.js | 135 +++++++----------- 2 files changed, 56 insertions(+), 81 deletions(-) diff --git a/package.json b/package.json index 05f51efe7..5ab7bdc55 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ }, "devDependencies": { "cross-env": "7.0.3", - "electron": "16.2.6", + "electron": "16.2.7", "electron-builder": "23.0.3", "electron-packager": "15.5.1", "electron-rebuild": "3.2.7", diff --git a/src/public/app/widgets/type_widgets/canvas.js b/src/public/app/widgets/type_widgets/canvas.js index 483baa572..754ab1714 100644 --- a/src/public/app/widgets/type_widgets/canvas.js +++ b/src/public/app/widgets/type_widgets/canvas.js @@ -33,42 +33,42 @@ const TPL = ` /** * # Canvas note with excalidraw * @author thfrei 2022-05-11 - * + * * Background: * excalidraw gives great support for hand drawn notes. It also allows to include images and support * for sketching. Excalidraw has a vibrant and active community. - * + * * Functionality: - * We store the excalidraw assets (elements, appState, files) in the note. In addition to that, we + * We store the excalidraw assets (elements, appState, files) in the note. In addition to that, we * export the SVG from the canvas on every update. The SVG is also saved in the note. It is used * for displaying any canvas note inside of a text note as an image. - * + * * Paths not taken. * - excalidraw-to-svg (node.js) could be used to avoid storing the svg in the backend. * We could render the SVG on the fly. However, as of now, it does not render any hand drawn - * (freedraw) paths. There is an issue with Path2D object not present in node-canvas library + * (freedraw) paths. There is an issue with Path2D object not present in node-canvas library * used by jsdom. (See Trilium PR for samples and other issues in respective library. * Link will be added later). Related links: * - https://github.com/Automattic/node-canvas/pull/2013 - * - https://github.com/google/canvas-5-polyfill - * - https://github.com/Automattic/node-canvas/issues/1116 - * - https://www.npmjs.com/package/path2d-polyfill + * - https://github.com/google/canvas-5-polyfill + * - https://github.com/Automattic/node-canvas/issues/1116 + * - https://www.npmjs.com/package/path2d-polyfill * - excalidraw-to-svg (node.js) takes quite some time to load an image (1-2s) * - excalidraw-utils (browser) does render freedraw, however NOT freedraw with background. It is not * used, since it is a big dependency, and has the same functionality as react + excalidraw. * - infinite-drawing-canvas with fabric.js. This library lacked a lot of feature, excalidraw already * has. - * + * * Known issues: * - v0.11.0 of excalidraw does not render freedraw backgrounds in the svg * - the 3 excalidraw fonts should be included in the share and everywhere, so that it is shown * when requiring svg. - * + * * Discussion of storing svg in the note: * - Pro: we will combat bit-rot. Showing the SVG will be very fast and easy, since it is already there. - * - Con: The note will get bigger (~40-50%?), we will generate more bandwith. However, using trilium + * - Con: The note will get bigger (~40-50%?), we will generate more bandwith. However, using trilium * desktop instance mitigates that issue. - * + * * Roadmap: * - Support image-notes as reference in excalidraw * - Support canvas note as reference (svg) in other canvas notes. @@ -95,52 +95,42 @@ export default class ExcalidrawTypeWidget extends TypeWidget { // will be overwritten this.excalidrawRef; this.$render; - this.renderElement; this.$widget; this.reactHandlers; // used to control react state - + this.createExcalidrawReactApp = this.createExcalidrawReactApp.bind(this); this.onChangeHandler = this.onChangeHandler.bind(this); this.isNewSceneVersion = this.isNewSceneVersion.bind(this); } - /** - * (trilium) - * @returns {string} "canvas" - */ static getType() { return "canvas"; } - /** - * (trilium) - * renders note - */ doRender() { this.$widget = $(TPL); this.$widget.toggleClass("full-height", true); // only add this.$render = this.$widget.find('.canvas-render'); - this.renderElement = this.$render.get(0); libraryLoader .requireLibrary(libraryLoader.EXCALIDRAW) .then(() => { const React = window.React; const ReactDOM = window.ReactDOM; - - ReactDOM.unmountComponentAtNode(this.renderElement); - ReactDOM.render(React.createElement(this.createExcalidrawReactApp), this.renderElement); - }) + const renderElement = this.$render.get(0); + + ReactDOM.unmountComponentAtNode(renderElement); + ReactDOM.render(React.createElement(this.createExcalidrawReactApp), renderElement); + }); return this.$widget; } /** - * (trilium) * called to populate the widget container with the note content - * - * @param {note} note + * + * @param {note} note */ async doRefresh(note) { // see if note changed, since we do not get a new class for a new note @@ -150,7 +140,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { this.currentSceneVersion = this.SCENE_VERSION_INITIAL; } this.currentNoteId = note.noteId; - + // get note from backend and put into canvas const noteComplement = await froca.getNoteComplement(note.noteId); @@ -166,25 +156,18 @@ export default class ExcalidrawTypeWidget extends TypeWidget { * note into this fresh note. Probably due to that this note-instance does not get * newly instantiated? */ - if (this.excalidrawRef.current && noteComplement.content === "") { + if (this.excalidrawRef.current && noteComplement.content?.trim() === "") { const sceneData = { - elements: [], + elements: [], appState: {}, collaborators: [] }; - + this.excalidrawRef.current.updateScene(sceneData); } - - /** - * load saved content into excalidraw canvas - */ else if (this.excalidrawRef.current && noteComplement.content) { - let content ={ - elements: [], - appState: [], - files: [], - }; + // load saved content into excalidraw canvas + let content; try { content = JSON.parse(noteComplement.content || ""); @@ -192,6 +175,12 @@ export default class ExcalidrawTypeWidget extends TypeWidget { console.error("Error parsing content. Probably note.type changed", "Starting with empty canvas" , note, noteComplement, err); + + content = { + elements: [], + appState: [], + files: [], + }; } const {elements, appState, files} = content; @@ -207,7 +196,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { appState.offsetTop = boundingClientRect.top; const sceneData = { - elements, + elements, appState, collaborators: [] }; @@ -225,12 +214,10 @@ export default class ExcalidrawTypeWidget extends TypeWidget { fileArray.push(file); } - this.sceneVersion = window.Excalidraw.getSceneVersion(elements); - this.excalidrawRef.current.updateScene(sceneData); this.excalidrawRef.current.addFiles(fileArray); } - + // set initial scene version if (this.currentSceneVersion === this.SCENE_VERSION_INITIAL) { this.currentSceneVersion = this.getSceneVersion(); @@ -238,20 +225,19 @@ export default class ExcalidrawTypeWidget extends TypeWidget { } /** - * (trilium) * gets data from widget container that will be sent via spacedUpdate.scheduleUpdate(); * this is automatically called after this.saveData(); */ async getContent() { const elements = this.excalidrawRef.current.getSceneElements(); const appState = this.excalidrawRef.current.getAppState(); - + /** * A file is not deleted, even though removed from canvas. therefore we only keep * files that are referenced by an element. Maybe this will change with new excalidraw version? */ const files = this.excalidrawRef.current.getFiles(); - + /** * parallel svg export to combat bitrot and enable rendering image for note inclusion, * preview and share. @@ -285,13 +271,10 @@ export default class ExcalidrawTypeWidget extends TypeWidget { svg: svgSafeString, // not needed for excalidraw, used for note_short, content, and image api }; - const contentString = JSON.stringify(content); - - return contentString; + return JSON.stringify(content); } /** - * (trilium) * save content to backend * spacedUpdate is kind of a debouncer. */ @@ -300,8 +283,6 @@ export default class ExcalidrawTypeWidget extends TypeWidget { } onChangeHandler() { - const appState = this.excalidrawRef.current.getAppState() || {}; - // changeHandler is called upon any tiny change in excalidraw. button clicked, hover, etc. // make sure only when a new element is added, we actually save something. const isNewSceneVersion = this.isNewSceneVersion(); @@ -336,11 +317,6 @@ export default class ExcalidrawTypeWidget extends TypeWidget { height: undefined }); - const [viewModeEnabled, setViewModeEnabled] = React.useState(false); - const [zenModeEnabled, setZenModeEnabled] = React.useState(false); - const [gridModeEnabled, setGridModeEnabled] = React.useState(false); - const [synchronized, setSynchronized] = React.useState(true); - React.useEffect(() => { const dimensions = { width: excalidrawWrapperRef.current.getBoundingClientRect().width, @@ -355,9 +331,9 @@ export default class ExcalidrawTypeWidget extends TypeWidget { }; setDimensions(dimensions); }; - + window.addEventListener("resize", onResize); - + return () => window.removeEventListener("resize", onResize); }, [excalidrawWrapperRef]); @@ -366,9 +342,9 @@ export default class ExcalidrawTypeWidget extends TypeWidget { const { nativeEvent } = event.detail; const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey; const isNewWindow = nativeEvent.shiftKey; - const isInternalLink = link.startsWith("/") + const isInternalLink = link.startsWith("/") || link.includes(window.location.origin); - + if (isInternalLink && !isNewTab && !isNewWindow) { // signal that we're handling the redirect ourselves event.preventDefault(); @@ -401,9 +377,9 @@ export default class ExcalidrawTypeWidget extends TypeWidget { onCollabButtonClick: () => { window.alert("You clicked on collab button. No collaboration is implemented."); }, - viewModeEnabled: viewModeEnabled, - zenModeEnabled: zenModeEnabled, - gridModeEnabled: gridModeEnabled, + viewModeEnabled: false, + zenModeEnabled: false, + gridModeEnabled: false, isCollaborating: false, detectScroll: false, handleKeyboardGlobally: false, @@ -412,18 +388,18 @@ export default class ExcalidrawTypeWidget extends TypeWidget { }) ) ); - } + } /** * needed to ensure, that multipleOnChangeHandler calls do not trigger a safe. * we compare the scene version as suggested in: * https://github.com/excalidraw/excalidraw/issues/3014#issuecomment-778115329 - * + * * info: sceneVersions are not incrementing. it seems to be a pseudo-random number */ isNewSceneVersion() { const sceneVersion = this.getSceneVersion(); - + return this.currentSceneVersion === this.SCENE_VERSION_INITIAL // initial scene version update || this.currentSceneVersion !== sceneVersion // ensure scene changed ; @@ -432,8 +408,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { getSceneVersion() { if (this.excalidrawRef) { const elements = this.excalidrawRef.current.getSceneElements(); - const sceneVersion = window.Excalidraw.getSceneVersion(elements); - return sceneVersion; + return window.Excalidraw.getSceneVersion(elements); } else { return this.SCENE_VERSION_ERROR; } @@ -442,11 +417,11 @@ export default class ExcalidrawTypeWidget extends TypeWidget { updateSceneVersion() { this.currentSceneVersion = this.getSceneVersion(); } - + /** * logs to console.log with some predefined title - * - * @param {...any} args + * + * @param {...any} args */ log(...args) { let title = ''; @@ -461,12 +436,12 @@ export default class ExcalidrawTypeWidget extends TypeWidget { /** * replaces exlicraw.com with own assets - * + * * workaround until https://github.com/excalidraw/excalidraw/pull/5065 is merged and published * needed for v0.11.0 - * - * @param {string} string - * @returns + * + * @param {string} string + * @returns */ replaceExternalAssets = (string) => { let result = string; From 942f17b2f436a28eccd341e4ec2f3d04fe4648ca Mon Sep 17 00:00:00 2001 From: dousha Date: Sat, 21 May 2022 13:25:59 +0800 Subject: [PATCH 020/250] fix docker file permissions so 777 is no longer needed --- Dockerfile | 10 ++++++++-- start-docker.sh | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100755 start-docker.sh diff --git a/Dockerfile b/Dockerfile index 7f03916de..0687992b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,13 +18,19 @@ RUN set -x \ nasm \ libpng-dev \ python3 \ + wget \ && npm install --production \ && apk del .build-dependencies +# Some setup tools need to be kept +RUN apk add --no-cache su-exec + # Bundle app source COPY . . -USER node +# Add application user and setup proper volume permissions +RUN adduser -s /bin/false node; exit 0 +# Start the application EXPOSE 8080 -CMD [ "node", "./src/www" ] +CMD [ "./start-docker.sh" ] diff --git a/start-docker.sh b/start-docker.sh new file mode 100755 index 000000000..e214560ea --- /dev/null +++ b/start-docker.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +chown -R node:node /home/node +su-exec node node ./src/www From 819cf0907d056f5bec3ab7f1525b166ad407bf4d Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 21 May 2022 14:00:53 +0200 Subject: [PATCH 021/250] add option to disable auto-download of images for offline storage, #2859 --- src/public/app/dialogs/options/other.js | 24 +++++++++++++++++++++--- src/routes/api/options.js | 3 ++- src/services/notes.js | 4 ++++ src/services/options_init.js | 9 +++++---- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/public/app/dialogs/options/other.js b/src/public/app/dialogs/options/other.js index ff97a0bcd..dde89b6ad 100644 --- a/src/public/app/dialogs/options/other.js +++ b/src/public/app/dialogs/options/other.js @@ -33,7 +33,13 @@ const TPL = `
-

Image compression

+

Images

+ +
+ + +

(pasted HTML can contain references to online images, Trilium will find those references and download the images so that they are available offline)

+
@@ -216,6 +222,15 @@ export default class ProtectedSessionOptions { return false; }); + this.$downloadImagesAutomatically = $("#download-images-automatically"); + + this.$downloadImagesAutomatically.on("change", () => { + const isChecked = this.$downloadImagesAutomatically.prop("checked"); + const opts = { 'downloadImagesAutomatically': isChecked ? 'true' : 'false' }; + + server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved.")); + }); + this.$enableImageCompression = $("#image-compresion-enabled"); this.$imageCompressionWrapper = $("#image-compression-enabled-wraper"); @@ -225,7 +240,7 @@ export default class ProtectedSessionOptions { } else { this.$imageCompressionWrapper.addClass("disabled-field"); } - } + }; this.$enableImageCompression.on("change", () => { const isChecked = this.$enableImageCompression.prop("checked"); @@ -234,7 +249,7 @@ export default class ProtectedSessionOptions { server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved.")); this.setImageCompression(isChecked); - }) + }); } optionsLoaded(options) { @@ -251,6 +266,9 @@ export default class ProtectedSessionOptions { this.$autoReadonlySizeText.val(options['autoReadonlySizeText']); this.$autoReadonlySizeCode.val(options['autoReadonlySizeCode']); + const downloadImagesAutomatically = options['downloadImagesAutomatically'] === 'true'; + this.$downloadImagesAutomatically.prop('checked', downloadImagesAutomatically); + const compressImages = options['compressImages'] === 'true'; this.$enableImageCompression.prop('checked', compressImages); this.setImageCompression(compressImages); diff --git a/src/routes/api/options.js b/src/routes/api/options.js index 3df70f68d..5c38ff4c8 100644 --- a/src/routes/api/options.js +++ b/src/routes/api/options.js @@ -55,7 +55,8 @@ const ALLOWED_OPTIONS = new Set([ 'weeklyBackupEnabled', 'monthlyBackupEnabled', 'maxContentWidth', - 'compressImages' + 'compressImages', + 'downloadImagesAutomatically' ]); function getOptions() { diff --git a/src/services/notes.js b/src/services/notes.js index 067bf4596..5e3670727 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -323,6 +323,10 @@ function replaceUrl(content, url, imageNote) { } function downloadImages(noteId, content) { + if (!optionService.getOptionBool("downloadImagesAutomatically")) { + return content; + } + const imageRe = /]*?\ssrc=['"]([^'">]+)['"]/ig; let imageMatch; diff --git a/src/services/options_init.js b/src/services/options_init.js index 5744148c3..20e9dabb9 100644 --- a/src/services/options_init.js +++ b/src/services/options_init.js @@ -29,13 +29,13 @@ function initNotSyncedOptions(initialized, opts = {}) { optionService.createOption('lastSyncedPush', '0', false); let theme = 'dark'; // default based on the poll in https://github.com/zadam/trilium/issues/2516 - + if (utils.isElectron()) { const {nativeTheme} = require('electron'); - + theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; } - + optionService.createOption('theme', theme, false); optionService.createOption('syncServerHost', opts.syncServerHost || '', false); @@ -83,7 +83,8 @@ const defaultOptions = [ { name: 'weeklyBackupEnabled', value: 'true', isSynced: false }, { name: 'monthlyBackupEnabled', value: 'true', isSynced: false }, { name: 'maxContentWidth', value: '1200', isSynced: false }, - { name: 'compressImages', value: 'true', isSynced: true } + { name: 'compressImages', value: 'true', isSynced: true }, + { name: 'downloadImagesAutomatically', value: 'true', isSynced: true } ]; function initStartupOptions() { From 678e8830444ef2a6a1e7ba35a0a8a90a23f5459c Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 21 May 2022 21:08:24 +0200 Subject: [PATCH 022/250] switch excalidraw theme (light/dark) based on trilium setting --- src/public/app/widgets/type_widgets/canvas.js | 15 ++++++++++----- src/public/stylesheets/theme-dark.css | 4 ++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/public/app/widgets/type_widgets/canvas.js b/src/public/app/widgets/type_widgets/canvas.js index 754ab1714..85a463bf3 100644 --- a/src/public/app/widgets/type_widgets/canvas.js +++ b/src/public/app/widgets/type_widgets/canvas.js @@ -23,6 +23,11 @@ const TPL = ` .zen-mode-transition.App-menu_bottom--transition-left { transform: none; } + + /* collaboration not possible so hide the button */ + .CollabButton { + display: none !important; + } @@ -112,6 +117,8 @@ export default class ExcalidrawTypeWidget extends TypeWidget { this.$widget.toggleClass("full-height", true); // only add this.$render = this.$widget.find('.canvas-render'); + const documentStyle = window.getComputedStyle(document.documentElement); + this.themeStyle = documentStyle.getPropertyValue('--theme-style')?.trim(); libraryLoader .requireLibrary(libraryLoader.EXCALIDRAW) @@ -185,6 +192,8 @@ export default class ExcalidrawTypeWidget extends TypeWidget { const {elements, appState, files} = content; + appState.theme = this.themeStyle; + /** * use widths and offsets of current view, since stored appState has the state from * previous edit. using the stored state would lead to pointer mismatch. @@ -365,18 +374,14 @@ export default class ExcalidrawTypeWidget extends TypeWidget { ref: excalidrawWrapperRef }, React.createElement(Excalidraw.default, { + theme: "light", // not in effect, but causes the theme toggle button to disappear ref: excalidrawRef, width: dimensions.width, height: dimensions.height, - // initialData: InitialData, onPaste: (data, event) => { this.log("Verbose: excalidraw internal paste. No trilium action implemented.", data, event); }, onChange: debounce(this.onChangeHandler, this.DEBOUNCE_TIME_ONCHANGEHANDLER), - // onPointerUpdate: (payload) => console.log(payload), - onCollabButtonClick: () => { - window.alert("You clicked on collab button. No collaboration is implemented."); - }, viewModeEnabled: false, zenModeEnabled: false, gridModeEnabled: false, diff --git a/src/public/stylesheets/theme-dark.css b/src/public/stylesheets/theme-dark.css index 08f0f677e..00b0df5dd 100644 --- a/src/public/stylesheets/theme-dark.css +++ b/src/public/stylesheets/theme-dark.css @@ -79,3 +79,7 @@ body ::-webkit-calendar-picker-indicator { body .CodeMirror { filter: invert(90%) hue-rotate(180deg); } + +.excalidraw.theme--dark { + --theme-filter: invert(80%) hue-rotate(180deg) !important; +} From 308b0f746404c9de4fa9863460c5010a23cdc932 Mon Sep 17 00:00:00 2001 From: Jiahao Lee Date: Sun, 22 May 2022 03:35:26 +0800 Subject: [PATCH 023/250] Remove unneeded packages --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0687992b7..018d634e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,6 @@ RUN set -x \ nasm \ libpng-dev \ python3 \ - wget \ && npm install --production \ && apk del .build-dependencies From 81fd7397e4856579d6b42f2581617c56cd55aadf Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 22 May 2022 13:45:15 +0200 Subject: [PATCH 024/250] added underline to more distinguish selected items in the tree, #2865 --- src/public/stylesheets/tree.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/public/stylesheets/tree.css b/src/public/stylesheets/tree.css index 5c502943c..50cb18b30 100644 --- a/src/public/stylesheets/tree.css +++ b/src/public/stylesheets/tree.css @@ -182,6 +182,10 @@ span.fancytree-selected { font-style: italic; } +span.fancytree-selected .fancytree-title { + text-decoration: underline; +} + span.fancytree-node:hover { border: 1px solid var(--main-border-color); } From 13ccd2ba672c8ad350dd4aa1551763e7bc42352e Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 22 May 2022 13:45:49 +0200 Subject: [PATCH 025/250] remove the canvas dependency (transitive dep of jsdom) in postinstall --- package-lock.json | 551 ++++++---------------------------------------- package.json | 3 +- 2 files changed, 65 insertions(+), 489 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa9248ab0..92b9d4dd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "trilium", "version": "0.51.2", + "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { "@electron/remote": "2.0.8", @@ -73,7 +74,7 @@ }, "devDependencies": { "cross-env": "7.0.3", - "electron": "16.2.6", + "electron": "16.2.7", "electron-builder": "23.0.3", "electron-packager": "15.5.1", "electron-rebuild": "3.2.7", @@ -757,148 +758,6 @@ "node": ">= 10.0.0" } }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz", - "integrity": "sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw==", - "optional": true, - "peer": true, - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "optional": true, - "peer": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "optional": true, - "peer": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "optional": true, - "peer": true, - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "optional": true, - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "optional": true, - "peer": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "optional": true, - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@npmcli/fs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.0.tgz", @@ -1326,15 +1185,15 @@ "dev": true }, "node_modules/abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "devOptional": true + "dev": true }, "node_modules/accepts": { "version": "1.3.8", @@ -2475,60 +2334,6 @@ "url": "https://opencollective.com/browserslist" } }, - "node_modules/canvas": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.9.1.tgz", - "integrity": "sha512-vSQti1uG/2gjv3x6QLOZw7TctfufaerTWbVe+NSduHxxLGB+qf3kFgQ6n66DSnuoINtVUjrLLIK2R+lxrBG07A==", - "hasInstallScript": true, - "optional": true, - "peer": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.15.0", - "simple-get": "^3.0.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/canvas/node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "optional": true, - "peer": true, - "dependencies": { - "mimic-response": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/canvas/node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/canvas/node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "optional": true, - "peer": true, - "dependencies": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -2846,7 +2651,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "devOptional": true, + "dev": true, "bin": { "color-support": "bin.js" } @@ -3286,13 +3091,25 @@ } }, "node_modules/data-urls": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.1.tgz", - "integrity": "sha512-Ds554NeT5Gennfoo9KN50Vh6tpgtvYEwraYjejXnyTpu1C7oXKxdFk75REooENHE8ndTVOJuv+BEs4/J/xcozw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", "dependencies": { - "abab": "^2.0.3", + "abab": "^2.0.6", "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^10.0.0" + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" }, "engines": { "node": ">=12" @@ -3690,9 +3507,9 @@ } }, "node_modules/electron": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/electron/-/electron-16.2.6.tgz", - "integrity": "sha512-FJLnIu318WNh1WigMmWqSidOPwipwym2Qi3Hs/YY6znquztf6ZJuaq/TdJJyHIJHld+znG0hSmq3VbyW5KUr9A==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-16.2.7.tgz", + "integrity": "sha512-aZKF3b00+rqW/HGs8lJM5DhPNj+mOfCuhLSiFXV6J9dQCIRhctJTmToOrwXfbCxvXK8as8eQTNl5uSfnHmH6tA==", "hasInstallScript": true, "dependencies": { "@electron/get": "^1.13.0", @@ -4668,7 +4485,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "devOptional": true + "dev": true }, "node_modules/encodeurl": { "version": "1.0.2", @@ -4682,6 +4499,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -4691,6 +4509,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7180,7 +6999,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "devOptional": true, + "dev": true, "dependencies": { "semver": "^6.0.0" }, @@ -7195,7 +7014,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "devOptional": true, + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -7563,13 +7382,6 @@ "node": ">= 0.10.0" } }, - "node_modules/nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", - "optional": true, - "peer": true - }, "node_modules/nanoid": { "version": "3.1.30", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", @@ -7620,52 +7432,6 @@ "semver": "^7.3.5" } }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "optional": true, - "peer": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "optional": true, - "peer": true - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "optional": true, - "peer": true - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "optional": true, - "peer": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -7817,7 +7583,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "devOptional": true, + "dev": true, "dependencies": { "abbrev": "1" }, @@ -11599,123 +11365,6 @@ } } }, - "@mapbox/node-pre-gyp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz", - "integrity": "sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw==", - "optional": true, - "peer": true, - "requires": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true, - "peer": true - }, - "are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "optional": true, - "peer": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - } - }, - "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", - "optional": true, - "peer": true - }, - "gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "optional": true, - "peer": true, - "requires": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "optional": true, - "peer": true - }, - "npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "optional": true, - "peer": true, - "requires": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "optional": true, - "peer": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "optional": true, - "peer": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "optional": true, - "peer": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, "@npmcli/fs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.0.tgz", @@ -12111,15 +11760,15 @@ "dev": true }, "abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "devOptional": true + "dev": true }, "accepts": { "version": "1.3.8", @@ -13039,49 +12688,6 @@ "integrity": "sha512-4udbs9bc0hfNrcje++AxBuc6PfLNHwh3PO9kbwnfCQWyqtlzg3py0YgFu8jyRTTo85VAz4U+VLxSlID09vNtWA==", "dev": true }, - "canvas": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.9.1.tgz", - "integrity": "sha512-vSQti1uG/2gjv3x6QLOZw7TctfufaerTWbVe+NSduHxxLGB+qf3kFgQ6n66DSnuoINtVUjrLLIK2R+lxrBG07A==", - "optional": true, - "peer": true, - "requires": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.15.0", - "simple-get": "^3.0.3" - }, - "dependencies": { - "decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "optional": true, - "peer": true, - "requires": { - "mimic-response": "^2.0.0" - } - }, - "mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "optional": true, - "peer": true - }, - "simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "optional": true, - "peer": true, - "requires": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - } - } - }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -13322,7 +12928,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "devOptional": true + "dev": true }, "colorette": { "version": "2.0.16", @@ -13650,13 +13256,24 @@ } }, "data-urls": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.1.tgz", - "integrity": "sha512-Ds554NeT5Gennfoo9KN50Vh6tpgtvYEwraYjejXnyTpu1C7oXKxdFk75REooENHE8ndTVOJuv+BEs4/J/xcozw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", "requires": { - "abab": "^2.0.3", + "abab": "^2.0.6", "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^10.0.0" + "whatwg-url": "^11.0.0" + }, + "dependencies": { + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + } } }, "dayjs": { @@ -13964,9 +13581,9 @@ } }, "electron": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/electron/-/electron-16.2.6.tgz", - "integrity": "sha512-FJLnIu318WNh1WigMmWqSidOPwipwym2Qi3Hs/YY6znquztf6ZJuaq/TdJJyHIJHld+znG0hSmq3VbyW5KUr9A==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-16.2.7.tgz", + "integrity": "sha512-aZKF3b00+rqW/HGs8lJM5DhPNj+mOfCuhLSiFXV6J9dQCIRhctJTmToOrwXfbCxvXK8as8eQTNl5uSfnHmH6tA==", "requires": { "@electron/get": "^1.13.0", "@types/node": "^14.6.2", @@ -14700,7 +14317,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "devOptional": true + "dev": true }, "encodeurl": { "version": "1.0.2", @@ -14711,6 +14328,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "optional": true, "requires": { "iconv-lite": "^0.6.2" @@ -14720,6 +14338,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, "optional": true, "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -16643,7 +16262,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "devOptional": true, + "dev": true, "requires": { "semver": "^6.0.0" }, @@ -16652,7 +16271,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "devOptional": true + "dev": true } } }, @@ -16936,13 +16555,6 @@ "xtend": "^4.0.0" } }, - "nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", - "optional": true, - "peer": true - }, "nanoid": { "version": "3.1.30", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", @@ -16981,43 +16593,6 @@ "semver": "^7.3.5" } }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "optional": true, - "peer": true, - "requires": { - "whatwg-url": "^5.0.0" - }, - "dependencies": { - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "optional": true, - "peer": true - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "optional": true, - "peer": true - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "optional": true, - "peer": true, - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - } - }, "node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -17136,7 +16711,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "devOptional": true, + "dev": true, "requires": { "abbrev": "1" } diff --git a/package.json b/package.json index 5ab7bdc55..7d417ddbd 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "webpack": "npx webpack -c webpack-desktop.config.js && npx webpack -c webpack-mobile.config.js && npx webpack -c webpack-setup.config.js", "test": "jasmine", "test-es6": "node -r esm spec-es6/attribute_parser.spec.js ", - "test-all": "npm run test && npm run test-es6" + "test-all": "npm run test && npm run test-es6", + "postinstall": "rimraf ./node_modules/canvas" }, "dependencies": { "@excalidraw/excalidraw": "0.11.0", From 7d39d080f500d45f4602b329d9843719cc418a8e Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 22 May 2022 14:24:47 +0200 Subject: [PATCH 026/250] switching note type will reevalute max content width setting --- src/public/app/widgets/note_wrapper.js | 31 +++++++++++++++++--------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/public/app/widgets/note_wrapper.js b/src/public/app/widgets/note_wrapper.js index 227543c65..80da4558c 100644 --- a/src/public/app/widgets/note_wrapper.js +++ b/src/public/app/widgets/note_wrapper.js @@ -15,25 +15,36 @@ export default class NoteWrapperWidget extends FlexContainer { } setNoteContextEvent({noteContext}) { - this.refresh(noteContext); + this.noteContext = noteContext; + + this.refresh(); } - noteSwitchedAndActivatedEvent({noteContext}) { - this.refresh(noteContext); + noteSwitchedAndActivatedEvent() { + this.refresh(); } - noteSwitchedEvent({noteContext}) { - this.refresh(noteContext); + noteSwitchedEvent() { + this.refresh(); } - activeContextChangedEvent({noteContext}) { - this.refresh(noteContext); + activeContextChangedEvent() { + this.refresh(); } - refresh(noteContext) { + refresh() { + const note = this.noteContext?.note; + this.$widget.toggleClass("full-content-width", - ['image', 'mermaid', 'book', 'render', 'canvas'].includes(noteContext?.note?.type) - || !!noteContext?.note?.hasLabel('fullContentWidth') + ['image', 'mermaid', 'book', 'render', 'canvas'].includes(note?.type) + || !!note?.hasLabel('fullContentWidth') ); } + + async entitiesReloadedEvent({loadResults}) { + // listening on changes of note.type + if (loadResults.isNoteReloaded(this.noteContext?.noteId)) { + this.refresh(); + } + } } From d6931f744118dad17364046f578b5da40f83c614 Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 22 May 2022 14:27:16 +0200 Subject: [PATCH 027/250] fix setting theme on new canvas note --- src/public/app/widgets/type_widgets/canvas.js | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/public/app/widgets/type_widgets/canvas.js b/src/public/app/widgets/type_widgets/canvas.js index 85a463bf3..2805b7478 100644 --- a/src/public/app/widgets/type_widgets/canvas.js +++ b/src/public/app/widgets/type_widgets/canvas.js @@ -153,7 +153,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { // before we load content into excalidraw, make sure excalidraw has loaded while (!this.excalidrawRef || !this.excalidrawRef.current) { - this.log("excalidrawRef not yet loeaded, sleep 200ms..."); + console.log("excalidrawRef not yet loaded, sleep 200ms..."); await sleep(200); } @@ -166,7 +166,9 @@ export default class ExcalidrawTypeWidget extends TypeWidget { if (this.excalidrawRef.current && noteComplement.content?.trim() === "") { const sceneData = { elements: [], - appState: {}, + appState: { + theme: this.themeStyle + }, collaborators: [] }; @@ -379,7 +381,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { width: dimensions.width, height: dimensions.height, onPaste: (data, event) => { - this.log("Verbose: excalidraw internal paste. No trilium action implemented.", data, event); + console.log("Verbose: excalidraw internal paste. No trilium action implemented.", data, event); }, onChange: debounce(this.onChangeHandler, this.DEBOUNCE_TIME_ONCHANGEHANDLER), viewModeEnabled: false, @@ -423,22 +425,6 @@ export default class ExcalidrawTypeWidget extends TypeWidget { this.currentSceneVersion = this.getSceneVersion(); } - /** - * logs to console.log with some predefined title - * - * @param {...any} args - */ - log(...args) { - let title = ''; - if (this.note) { - title = this.note.title; - } else { - title = this.noteId + "nt/na"; - } - - console.log(title, "=", this.noteId, "==", ...args); - } - /** * replaces exlicraw.com with own assets * From 27570a7756b3cb75ea1bb495a9e441dbe2990cd8 Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 22 May 2022 15:27:40 +0200 Subject: [PATCH 028/250] fix for canvas theme after opening help --- src/public/app/widgets/type_widgets/canvas.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/public/app/widgets/type_widgets/canvas.js b/src/public/app/widgets/type_widgets/canvas.js index 2805b7478..23e8cdbbd 100644 --- a/src/public/app/widgets/type_widgets/canvas.js +++ b/src/public/app/widgets/type_widgets/canvas.js @@ -187,7 +187,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { content = { elements: [], - appState: [], + appState: {}, files: [], }; } @@ -376,7 +376,8 @@ export default class ExcalidrawTypeWidget extends TypeWidget { ref: excalidrawWrapperRef }, React.createElement(Excalidraw.default, { - theme: "light", // not in effect, but causes the theme toggle button to disappear + // this makes sure that 1) manual theme switch button is hidden 2) theme stays as it should after opening menu + theme: this.themeStyle, ref: excalidrawRef, width: dimensions.width, height: dimensions.height, From 7c64dc944023781ed54988b0accdd33f24fad226 Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 22 May 2022 23:29:07 +0200 Subject: [PATCH 029/250] fix coloring of backlinks popup in dark theme --- src/public/app/widgets/backlinks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/public/app/widgets/backlinks.js b/src/public/app/widgets/backlinks.js index eb6530826..2e32d5a6f 100644 --- a/src/public/app/widgets/backlinks.js +++ b/src/public/app/widgets/backlinks.js @@ -41,8 +41,8 @@ const TPL = ` right: 10px; width: 400px; border-radius: 10px; - background-color: #eeeeee; - color: #444; + background-color: var(--more-accented-background-color); + color: var(--main-text-color); padding: 20px; overflow-y: auto; } From 541d45116820ff7c866e5f57af46fdcf0ddc4307 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 24 May 2022 21:33:07 +0200 Subject: [PATCH 030/250] tree css changes to make selection and active note more distinct --- src/public/stylesheets/tree.css | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/public/stylesheets/tree.css b/src/public/stylesheets/tree.css index 50cb18b30..fd8d359b8 100644 --- a/src/public/stylesheets/tree.css +++ b/src/public/stylesheets/tree.css @@ -157,33 +157,27 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit padding-left: 20px; } +span.fancytree-active { + color: var(--active-item-text-color) !important; + background-color: var(--active-item-background-color) !important; + border-color: transparent; /* invisible border */ + border-radius: 5px; +} + span.fancytree-active .fancytree-title { font-weight: bold; border: 0; } -span.fancytree-active { +span.fancytree-selected { + color: var(--hover-item-text-color) !important; border-color: var(--main-border-color) !important; border-radius: 5px; } -span.fancytree-active, span.fancytree-active.fancytree-selected { - color: var(--active-item-text-color) !important; - background-color: var(--active-item-background-color) !important; - border-color: var(--main-background-color) !important; /* invisible border */ - border-radius: 5px; -} - -span.fancytree-selected { - color: var(--hover-item-text-color) !important; - background-color: var(--hover-item-background-color) !important; - border-color: var(--main-background-color) !important; /* invisible border */ - border-radius: 5px; - font-style: italic; -} - span.fancytree-selected .fancytree-title { text-decoration: underline; + font-style: italic; } span.fancytree-node:hover { From 53e9c8cdac1d0b39f3f02bc91a874825eae915f3 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 24 May 2022 21:34:32 +0200 Subject: [PATCH 031/250] upgrades --- .gitpod.yml | 2 +- Dockerfile | 2 +- bin/build-server.sh | 2 +- bin/copy-trilium.sh | 4 ++-- dump-db/README.md | 2 +- package-lock.json | 28 +++++++++++++------------- package.json | 6 +++--- src/public/app/services/app_context.js | 1 - trilium.iml | 1 + 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.gitpod.yml b/.gitpod.yml index ab7e12a82..8a4a9a03b 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -2,7 +2,7 @@ image: file: .gitpod.dockerfile tasks: - - before: nvm install 16.14.2 && nvm use 16.14.2 + - before: nvm install 16.15.0 && nvm use 16.15.0 init: npm install command: npm run start-server diff --git a/Dockerfile b/Dockerfile index 018d634e9..7be0d0a39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # !!! Don't try to build this Dockerfile directly, run it through bin/build-docker.sh script !!! -FROM node:16.14.2-alpine +FROM node:16.15.0-alpine # Create app directory WORKDIR /usr/src/app diff --git a/bin/build-server.sh b/bin/build-server.sh index 518b7c125..1df3e0b54 100755 --- a/bin/build-server.sh +++ b/bin/build-server.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash PKG_DIR=dist/trilium-linux-x64-server -NODE_VERSION=16.14.2 +NODE_VERSION=16.15.0 if [ "$1" != "DONTCOPY" ] then diff --git a/bin/copy-trilium.sh b/bin/copy-trilium.sh index 7d85d8a59..22998a749 100755 --- a/bin/copy-trilium.sh +++ b/bin/copy-trilium.sh @@ -5,7 +5,7 @@ if [[ $# -eq 0 ]] ; then exit 1 fi -n exec 16.14.2 npm run webpack +n exec 16.15.0 npm run webpack DIR=$1 @@ -30,7 +30,7 @@ cp -r electron.js $DIR/ cp webpack-* $DIR/ # run in subshell (so we return to original dir) -(cd $DIR && n exec 16.14.2 npm install --only=prod) +(cd $DIR && n exec 16.15.0 npm install --only=prod) # cleanup of useless files in dependencies rm -r $DIR/node_modules/image-q/demo diff --git a/dump-db/README.md b/dump-db/README.md index 7145f22d1..38f3f42b0 100644 --- a/dump-db/README.md +++ b/dump-db/README.md @@ -6,7 +6,7 @@ It is meant as a last resort solution when the standard mean to access your data ## Installation -This tool requires node.js, testing has been done on 16.14.2, but it will probably work on other versions as well. +This tool requires node.js, testing has been done on 16.15.0, but it will probably work on other versions as well. ``` npm install diff --git a/package-lock.json b/package-lock.json index 92b9d4dd7..36bc60d66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "express-rate-limit": "6.4.0", "express-session": "1.17.3", "fs-extra": "10.1.0", - "helmet": "5.0.2", + "helmet": "5.1.0", "html": "1.0.0", "html2plaintext": "2.1.4", "http-proxy-agent": "5.0.0", @@ -46,7 +46,7 @@ "jsdom": "19.0.0", "mime-types": "2.1.35", "multer": "1.4.4", - "node-abi": "3.15.0", + "node-abi": "3.21.0", "normalize-strings": "1.1.1", "open": "8.4.0", "portscanner": "2.2.0", @@ -5759,9 +5759,9 @@ } }, "node_modules/helmet": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz", - "integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.1.0.tgz", + "integrity": "sha512-klsunXs8rgNSZoaUrNeuCiWUxyc+wzucnEnFejUg3/A+CaF589k9qepLZZ1Jehnzig7YbD4hEuscGXuBY3fq+g==", "engines": { "node": ">=12.0.0" } @@ -7413,9 +7413,9 @@ "dev": true }, "node_modules/node-abi": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.15.0.tgz", - "integrity": "sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.21.0.tgz", + "integrity": "sha512-0ChvtQmmNYzXju0fjG0Vfg72q2D8FxUhluvV9uqivtXsKblSekJE2juxfg+9HoSgqPMqCmVEC/GHHtGzi4xYTg==", "dependencies": { "semver": "^7.3.5" }, @@ -15303,9 +15303,9 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, "helmet": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz", - "integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg==" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.1.0.tgz", + "integrity": "sha512-klsunXs8rgNSZoaUrNeuCiWUxyc+wzucnEnFejUg3/A+CaF589k9qepLZZ1Jehnzig7YbD4hEuscGXuBY3fq+g==" }, "hosted-git-info": { "version": "2.8.9", @@ -16577,9 +16577,9 @@ "dev": true }, "node-abi": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.15.0.tgz", - "integrity": "sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.21.0.tgz", + "integrity": "sha512-0ChvtQmmNYzXju0fjG0Vfg72q2D8FxUhluvV9uqivtXsKblSekJE2juxfg+9HoSgqPMqCmVEC/GHHtGzi4xYTg==", "requires": { "semver": "^7.3.5" } diff --git a/package.json b/package.json index 7d417ddbd..1cbd862b4 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "postinstall": "rimraf ./node_modules/canvas" }, "dependencies": { + "@electron/remote": "2.0.8", "@excalidraw/excalidraw": "0.11.0", "archiver": "5.3.1", "async-mutex": "0.3.2", @@ -41,13 +42,12 @@ "electron-dl": "3.3.1", "electron-find": "1.0.7", "electron-window-state": "5.0.3", - "@electron/remote": "2.0.8", "express": "4.18.1", "express-partial-content": "1.0.2", "express-rate-limit": "6.4.0", "express-session": "1.17.3", "fs-extra": "10.1.0", - "helmet": "5.0.2", + "helmet": "5.1.0", "html": "1.0.0", "html2plaintext": "2.1.4", "http-proxy-agent": "5.0.0", @@ -61,7 +61,7 @@ "jsdom": "19.0.0", "mime-types": "2.1.35", "multer": "1.4.4", - "node-abi": "3.15.0", + "node-abi": "3.21.0", "normalize-strings": "1.1.1", "open": "8.4.0", "portscanner": "2.2.0", diff --git a/src/public/app/services/app_context.js b/src/public/app/services/app_context.js index f48ecef4f..77cb59796 100644 --- a/src/public/app/services/app_context.js +++ b/src/public/app/services/app_context.js @@ -11,7 +11,6 @@ import Component from "../widgets/component.js"; import keyboardActionsService from "./keyboard_actions.js"; import MobileScreenSwitcherExecutor from "../widgets/mobile_widgets/mobile_screen_switcher.js"; import MainTreeExecutors from "./main_tree_executors.js"; -import protectedSessionHolder from "./protected_session_holder.js"; import toast from "./toast.js"; class AppContext extends Component { diff --git a/trilium.iml b/trilium.iml index cffe441d0..bfa02661b 100644 --- a/trilium.iml +++ b/trilium.iml @@ -14,6 +14,7 @@ + From 37cb5f5e9acd8b4bd666b013705c863a72f02b68 Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 25 May 2022 20:43:52 +0200 Subject: [PATCH 032/250] added previous/next buttons to the find widget --- package-lock.json | 14 +++++++------- package.json | 2 +- src/public/app/widgets/find.js | 25 +++++++++++++++++++------ 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0301aa51e..6424b4f78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ }, "devDependencies": { "cross-env": "7.0.3", - "electron": "16.2.7", + "electron": "16.2.8", "electron-builder": "23.0.3", "electron-packager": "15.5.1", "electron-rebuild": "3.2.7", @@ -3506,9 +3506,9 @@ } }, "node_modules/electron": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/electron/-/electron-16.2.7.tgz", - "integrity": "sha512-aZKF3b00+rqW/HGs8lJM5DhPNj+mOfCuhLSiFXV6J9dQCIRhctJTmToOrwXfbCxvXK8as8eQTNl5uSfnHmH6tA==", + "version": "16.2.8", + "resolved": "https://registry.npmjs.org/electron/-/electron-16.2.8.tgz", + "integrity": "sha512-KSOytY6SPLsh3iCziztqa/WgJyfDOKzCvNqku9gGIqSdT8CqtV66dTU1SOrKZQjRFLxHaF8LbyxUL1vwe4taqw==", "hasInstallScript": true, "dependencies": { "@electron/get": "^1.13.0", @@ -13575,9 +13575,9 @@ } }, "electron": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/electron/-/electron-16.2.7.tgz", - "integrity": "sha512-aZKF3b00+rqW/HGs8lJM5DhPNj+mOfCuhLSiFXV6J9dQCIRhctJTmToOrwXfbCxvXK8as8eQTNl5uSfnHmH6tA==", + "version": "16.2.8", + "resolved": "https://registry.npmjs.org/electron/-/electron-16.2.8.tgz", + "integrity": "sha512-KSOytY6SPLsh3iCziztqa/WgJyfDOKzCvNqku9gGIqSdT8CqtV66dTU1SOrKZQjRFLxHaF8LbyxUL1vwe4taqw==", "requires": { "@electron/get": "^1.13.0", "@types/node": "^14.6.2", diff --git a/package.json b/package.json index ed817a939..5bf807d9d 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ }, "devDependencies": { "cross-env": "7.0.3", - "electron": "16.2.7", + "electron": "16.2.8", "electron-builder": "23.0.3", "electron-packager": "15.5.1", "electron-rebuild": "3.2.7", diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 9e0590ce9..c95db043c 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -34,8 +34,8 @@ const TPL = ` font-weight: bold; } - .find-widget-search-term-input { - max-width: 250px; + .find-widget-search-term-input-group { + max-width: 300px; } .find-widget-spacer { @@ -44,7 +44,13 @@ 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 93dd9274e79a5eee747cdb5a45a39781084b1e51 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 30 May 2022 13:22:28 +0200 Subject: [PATCH 042/250] fix relative address of lodash debounce, closes #2882 --- src/public/app/widgets/type_widgets/canvas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public/app/widgets/type_widgets/canvas.js b/src/public/app/widgets/type_widgets/canvas.js index 23e8cdbbd..0885e54ac 100644 --- a/src/public/app/widgets/type_widgets/canvas.js +++ b/src/public/app/widgets/type_widgets/canvas.js @@ -2,7 +2,7 @@ import libraryLoader from "../../services/library_loader.js"; import TypeWidget from "./type_widget.js"; import utils from '../../services/utils.js'; import froca from "../../services/froca.js"; -import debounce from "../../../../../libraries/lodash.debounce.js"; +import debounce from "../../../libraries/lodash.debounce.js"; const {sleep} = utils; From dcf31f8f95872652a908827cdf61fe1a8e5839d2 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 30 May 2022 17:45:59 +0200 Subject: [PATCH 043/250] 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 044/250] 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 b4ac41eff8f4c36f4fd1c578fe7b47b68ea7450d Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 6 Jun 2022 21:59:44 +0200 Subject: [PATCH 073/250] fix handlers import, closes #2900 --- src/becca/entities/note.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/becca/entities/note.js b/src/becca/entities/note.js index 0a945bf0f..662a0ba78 100644 --- a/src/becca/entities/note.js +++ b/src/becca/entities/note.js @@ -9,7 +9,6 @@ const entityChangesService = require('../../services/entity_changes'); const AbstractEntity = require("./abstract_entity"); const NoteRevision = require("./note_revision"); const TaskContext = require("../../services/task_context"); -const handlers = require("../../services/handlers"); const LABEL = 'label'; const RELATION = 'relation'; @@ -1143,6 +1142,7 @@ class Note extends AbstractEntity { } // needs to be run before branches and attributes are deleted and thus attached relations disappear + const handlers = require("../../services/handlers"); handlers.runAttachedRelations(this, 'runOnNoteDeletion', this); taskContext.noteDeletionHandlerTriggered = true; From 7609bc78ec03cbf96154194e43bad217af5c1bf5 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 6 Jun 2022 22:27:06 +0200 Subject: [PATCH 074/250] allow ignoring DB version for skipping uncritical migrations in case of a downgrade --- src/services/migration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/migration.js b/src/services/migration.js index e7306a7d0..423c87cf1 100644 --- a/src/services/migration.js +++ b/src/services/migration.js @@ -102,7 +102,7 @@ function isDbUpToDate() { async function migrateIfNecessary() { const currentDbVersion = getDbVersion(); - if (currentDbVersion > appInfo.dbVersion) { + if (currentDbVersion > appInfo.dbVersion && process.env.TRILIUM_IGNORE_DB_VERSION !== 'true') { log.error(`Current DB version ${currentDbVersion} is newer than app db version ${appInfo.dbVersion} which means that it was created by newer and incompatible version of Trilium. Upgrade to latest version of Trilium to resolve this issue.`); utils.crash(); From 15f8173add4363c16fff62390ced6167168c4c06 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 7 Jun 2022 20:11:43 +0200 Subject: [PATCH 075/250] fix ugly hover close button --- src/public/stylesheets/style.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index 62d2d8ef9..bb842dca8 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -968,3 +968,8 @@ input { .note-split.full-content-width { max-width: 999999px; } + +button.close:hover { + text-shadow: none; + color: currentColor; +} From 2d33f570f4ebfa8dad2ad2cda87de99b96d51e1d Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 8 Jun 2022 22:25:00 +0200 Subject: [PATCH 076/250] fix bulk/search action delete --- package-lock.json | 46 +++++++++++++------------- package.json | 6 ++-- src/becca/entities/note.js | 4 +++ src/routes/api/search.js | 13 +++++--- src/services/search/services/search.js | 9 +++-- src/services/sql.js | 4 +-- 6 files changed, 47 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6424b4f78..a873e3fa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "trilium", - "version": "0.51.2", + "version": "0.52.1-beta", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "trilium", - "version": "0.51.2", + "version": "0.52.1-beta", "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { @@ -21,7 +21,7 @@ "commonmark": "0.30.0", "cookie-parser": "1.4.6", "csurf": "1.11.0", - "dayjs": "1.11.2", + "dayjs": "1.11.3", "ejs": "3.1.8", "electron-debug": "3.2.0", "electron-dl": "3.3.1", @@ -65,7 +65,7 @@ "tmp": "0.2.1", "turndown": "7.1.1", "unescape": "1.0.1", - "ws": "8.6.0", + "ws": "8.7.0", "yauzl": "2.10.0" }, "bin": { @@ -82,7 +82,7 @@ "jsdoc": "3.6.10", "lorem-ipsum": "2.0.4", "rcedit": "3.0.1", - "webpack": "5.72.1", + "webpack": "5.73.0", "webpack-cli": "4.9.2" }, "optionalDependencies": { @@ -3115,9 +3115,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", - "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz", + "integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==" }, "node_modules/debug": { "version": "4.3.4", @@ -10252,9 +10252,9 @@ } }, "node_modules/webpack": { - "version": "5.72.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.72.1.tgz", - "integrity": "sha512-dXG5zXCLspQR4krZVR6QgajnZOjW2K/djHvdcRaDQvsjV9z9vaW6+ja5dZOYbqBBjF6kGXka/2ZyxNdc+8Jung==", + "version": "5.73.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.73.0.tgz", + "integrity": "sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -10594,9 +10594,9 @@ } }, "node_modules/ws": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz", - "integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz", + "integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==", "engines": { "node": ">=10.0.0" }, @@ -13271,9 +13271,9 @@ } }, "dayjs": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", - "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz", + "integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==" }, "debug": { "version": "4.3.4", @@ -18776,9 +18776,9 @@ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" }, "webpack": { - "version": "5.72.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.72.1.tgz", - "integrity": "sha512-dXG5zXCLspQR4krZVR6QgajnZOjW2K/djHvdcRaDQvsjV9z9vaW6+ja5dZOYbqBBjF6kGXka/2ZyxNdc+8Jung==", + "version": "5.73.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.73.0.tgz", + "integrity": "sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", @@ -19028,9 +19028,9 @@ } }, "ws": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz", - "integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz", + "integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==", "requires": {} }, "xdg-basedir": { diff --git a/package.json b/package.json index e619e299a..d37eff8eb 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "commonmark": "0.30.0", "cookie-parser": "1.4.6", "csurf": "1.11.0", - "dayjs": "1.11.2", + "dayjs": "1.11.3", "ejs": "3.1.8", "electron-debug": "3.2.0", "electron-dl": "3.3.1", @@ -80,7 +80,7 @@ "tmp": "0.2.1", "turndown": "7.1.1", "unescape": "1.0.1", - "ws": "8.6.0", + "ws": "8.7.0", "yauzl": "2.10.0" }, "devDependencies": { @@ -94,7 +94,7 @@ "jsdoc": "3.6.10", "lorem-ipsum": "2.0.4", "rcedit": "3.0.1", - "webpack": "5.72.1", + "webpack": "5.73.0", "webpack-cli": "4.9.2" }, "optionalDependencies": { diff --git a/src/becca/entities/note.js b/src/becca/entities/note.js index 662a0ba78..4b43f07dc 100644 --- a/src/becca/entities/note.js +++ b/src/becca/entities/note.js @@ -1133,6 +1133,10 @@ class Note extends AbstractEntity { * @param {TaskContext} [taskContext] */ deleteNote(deleteId, taskContext) { + if (this.isDeleted) { + return; + } + if (!deleteId) { deleteId = utils.randomString(10); } diff --git a/src/routes/api/search.js b/src/routes/api/search.js index ca64f24fe..9e4e9ce15 100644 --- a/src/routes/api/search.js +++ b/src/routes/api/search.js @@ -9,15 +9,16 @@ const noteRevisionService = require("../../services/note_revisions"); const branchService = require("../../services/branches"); const cloningService = require("../../services/cloning"); const {formatAttrForSearch} = require("../../services/attribute_formatter"); +const utils = require("../../services/utils.js"); -async function searchFromNoteInt(note) { +function searchFromNoteInt(note) { let searchResultNoteIds; const searchScript = note.getRelationValue('searchScript'); const searchString = note.getLabelValue('searchString'); if (searchScript) { - searchResultNoteIds = await searchFromRelation(note, 'searchScript'); + searchResultNoteIds = searchFromRelation(note, 'searchScript'); } else { const searchContext = new SearchContext({ fastSearch: note.hasLabel('fastSearch'), @@ -61,7 +62,9 @@ async function searchFromNote(req) { const ACTION_HANDLERS = { 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)); @@ -149,7 +152,7 @@ function getActions(note) { .filter(a => !!a); } -async function searchAndExecute(req) { +function searchAndExecute(req) { const note = becca.getNote(req.params.noteId); if (!note) { @@ -165,7 +168,7 @@ async function searchAndExecute(req) { return [400, `Note ${req.params.noteId} is not a search note.`] } - const searchResultNoteIds = await searchFromNoteInt(note); + const searchResultNoteIds = searchFromNoteInt(note); const actions = getActions(note); diff --git a/src/services/search/services/search.js b/src/services/search/services/search.js index c8729e365..b5389569e 100644 --- a/src/services/search/services/search.js +++ b/src/services/search/services/search.js @@ -78,6 +78,10 @@ function findResultsWithExpression(expression, searchContext) { const searchResults = noteSet.notes .map(note => { + if (note.isDeleted) { + return null; + } + const notePathArray = executionContext.noteIdToNotePath[note.noteId] || beccaService.getSomePath(note); if (!notePathArray) { @@ -85,7 +89,8 @@ function findResultsWithExpression(expression, searchContext) { } return new SearchResult(notePathArray); - }); + }) + .filter(note => !!note); for (const res of searchResults) { res.computeScore(searchContext.highlightedTokens); @@ -129,7 +134,7 @@ function parseQueryToExpression(query, searchContext) { structuredExpressionTokens, expression }; - + log.info("Search debug: " + JSON.stringify(searchContext.debugInfo, null, 4)); } diff --git a/src/services/sql.js b/src/services/sql.js index 1f60ef52e..06159abda 100644 --- a/src/services/sql.js +++ b/src/services/sql.js @@ -242,9 +242,9 @@ function transactional(func) { return ret; } catch (e) { - const entityChanges = cls.getAndClearEntityChangeIds(); + const entityChangeIds = cls.getAndClearEntityChangeIds(); - if (entityChanges.length > 0) { + if (entityChangeIds.length > 0) { log.info("Transaction rollback dirtied the becca, forcing reload."); require('../becca/becca_loader').load(); From e206d9cc68e374c12c85a699d230763fa51c8f15 Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 8 Jun 2022 22:27:36 +0200 Subject: [PATCH 077/250] fix selected note text color --- src/public/stylesheets/tree.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/public/stylesheets/tree.css b/src/public/stylesheets/tree.css index fd8d359b8..15999fdc7 100644 --- a/src/public/stylesheets/tree.css +++ b/src/public/stylesheets/tree.css @@ -170,7 +170,6 @@ span.fancytree-active .fancytree-title { } span.fancytree-selected { - color: var(--hover-item-text-color) !important; border-color: var(--main-border-color) !important; border-radius: 5px; } From 7d76fb8bf556a3856ea828196518a503a69a2880 Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 8 Jun 2022 22:52:17 +0200 Subject: [PATCH 078/250] 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 079/250] 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'; } From 31fb02f8103befaeeea90a39afe0eea4bd304d8f Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 9 Jun 2022 21:01:02 +0200 Subject: [PATCH 080/250] titleTemplate should be sanitized on import --- src/services/builtin_attributes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/builtin_attributes.js b/src/services/builtin_attributes.js index 5c4ca4488..ea8276fa6 100644 --- a/src/services/builtin_attributes.js +++ b/src/services/builtin_attributes.js @@ -50,7 +50,7 @@ module.exports = [ { type: 'label', name: 'shareDisallowRobotIndexing' }, { type: 'label', name: 'displayRelations' }, { type: 'label', name: 'hideRelations' }, - { type: 'label', name: 'titleTemplate' }, + { type: 'label', name: 'titleTemplate', isDangerous: true }, // relation names { type: 'relation', name: 'internalLink' }, From 96c4934c00703a93e4887af1bab20d953823c8ed Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 9 Jun 2022 23:38:07 +0200 Subject: [PATCH 081/250] new default keyboard shortcuts alt+up/down/left/right for moving notes on Mac --- src/services/keyboard_actions.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/keyboard_actions.js b/src/services/keyboard_actions.js index 3f2620e3c..2ee04fa5e 100644 --- a/src/services/keyboard_actions.js +++ b/src/services/keyboard_actions.js @@ -98,25 +98,25 @@ const DEFAULT_KEYBOARD_ACTIONS = [ }, { actionName: "moveNoteUp", - defaultShortcuts: ["CommandOrControl+Up"], + defaultShortcuts: isMac ? ["Alt+Up"] : ["CommandOrControl+Up"], description: "Move note up", scope: "note-tree" }, { actionName: "moveNoteDown", - defaultShortcuts: ["CommandOrControl+Down"], + defaultShortcuts: isMac ? ["Alt+Down"] : ["CommandOrControl+Down"], description: "Move note down", scope: "note-tree" }, { actionName: "moveNoteUpInHierarchy", - defaultShortcuts: ["CommandOrControl+Left"], + defaultShortcuts: isMac ? ["Alt+Left"] : ["CommandOrControl+Left"], description: "Move note up in hierarchy", scope: "note-tree" }, { actionName: "moveNoteDownInHierarchy", - defaultShortcuts: ["CommandOrControl+Right"], + defaultShortcuts: isMac ? ["Alt+Right"] : ["CommandOrControl+Right"], description: "Move note down in hierarchy", scope: "note-tree" }, From 23e9bcfdc500fadbe66802827312b1cf20c536fb Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 9 Jun 2022 23:39:48 +0200 Subject: [PATCH 082/250] release 0.52.2 --- package.json | 2 +- src/services/build.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d37eff8eb..45824650a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "trilium", "productName": "Trilium Notes", "description": "Trilium Notes", - "version": "0.52.1-beta", + "version": "0.52.2", "license": "AGPL-3.0-only", "main": "electron.js", "bin": { diff --git a/src/services/build.js b/src/services/build.js index e8699160c..8006241a8 100644 --- a/src/services/build.js +++ b/src/services/build.js @@ -1 +1 @@ -module.exports = { buildDate:"2022-06-05T15:00:25+02:00", buildRevision: "f587e0dfd9177462faef8ad7c39a855c25d03c91" }; +module.exports = { buildDate:"2022-06-09T23:39:48+02:00", buildRevision: "96c4934c00703a93e4887af1bab20d953823c8ed" }; From b530bc548f8fe048d22ea76899d5375982c4d6cd Mon Sep 17 00:00:00 2001 From: sigaloid <69441971+sigaloid@users.noreply.github.com> Date: Sat, 11 Jun 2022 13:27:41 -0400 Subject: [PATCH 083/250] Add messages prior to possibly long operations --- src/public/app/dialogs/options/advanced.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/public/app/dialogs/options/advanced.js b/src/public/app/dialogs/options/advanced.js index 322628300..c0b651ddf 100644 --- a/src/public/app/dialogs/options/advanced.js +++ b/src/public/app/dialogs/options/advanced.js @@ -65,12 +65,16 @@ export default class AdvancedOptions { }); this.$fillEntityChangesButton.on('click', async () => { + toastService.showMessage("Filling entity changes rows..."); + await server.post('sync/fill-entity-changes'); toastService.showMessage("Sync rows filled successfully"); }); this.$anonymizeFullButton.on('click', async () => { + toastService.showMessage("Creating fully anonymized database..."); + const resp = await server.post('database/anonymize/full'); if (!resp.success) { @@ -82,6 +86,8 @@ export default class AdvancedOptions { }); this.$anonymizeLightButton.on('click', async () => { + toastService.showMessage("Creating lightly anonymized database..."); + const resp = await server.post('database/anonymize/light'); if (!resp.success) { @@ -93,18 +99,24 @@ export default class AdvancedOptions { }); this.$vacuumDatabaseButton.on('click', async () => { + toastService.showMessage("Vacuuming database..."); + await server.post('database/vacuum-database'); toastService.showMessage("Database has been vacuumed"); }); this.$findAndFixConsistencyIssuesButton.on('click', async () => { + toastService.showMessage("Finding and fixing consistency issues..."); + await server.post('database/find-and-fix-consistency-issues'); toastService.showMessage("Consistency issues should be fixed."); }); this.$checkIntegrityButton.on('click', async () => { + toastService.showMessage("Checking database integrity..."); + const {results} = await server.get('database/check-integrity'); if (results.length === 1 && results[0].integrity_check === "ok") { From 2115b7604702733bae9b3eab57bfd7377fccbb45 Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 11 Jun 2022 23:29:52 +0200 Subject: [PATCH 084/250] bulk action dialog converted to widget --- package-lock.json | 97 ++++++------- package.json | 6 +- src/public/app/layouts/desktop_layout.js | 4 +- src/public/app/services/tree_context_menu.js | 2 +- .../bulk_actions/abstract_bulk_action.js | 3 +- .../bulk_actions/label/rename_label.js | 4 +- .../bulk_actions/relation/rename_relation.js | 2 +- .../app/widgets/dialogs/bulk_actions.js | 132 ++++++++++++++++++ src/public/app/widgets/note_tree.js | 5 - .../ribbon_widgets/search_definition.js | 6 + src/views/dialogs/bulk_assign_attributes.ejs | 29 +++- 11 files changed, 224 insertions(+), 66 deletions(-) create mode 100644 src/public/app/widgets/dialogs/bulk_actions.js diff --git a/package-lock.json b/package-lock.json index f71861ec6..55be6a476 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "trilium", - "version": "0.52.0-beta", + "version": "0.52.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "trilium", - "version": "0.52.0-beta", + "version": "0.52.2", "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { @@ -21,7 +21,7 @@ "commonmark": "0.30.0", "cookie-parser": "1.4.6", "csurf": "1.11.0", - "dayjs": "1.11.2", + "dayjs": "1.11.3", "ejs": "3.1.8", "electron-debug": "3.2.0", "electron-dl": "3.3.1", @@ -65,7 +65,7 @@ "tmp": "0.2.1", "turndown": "7.1.1", "unescape": "1.0.1", - "ws": "8.7.0", + "ws": "8.8.0", "yauzl": "2.10.0" }, "bin": { @@ -78,9 +78,9 @@ "electron-packager": "15.5.1", "electron-rebuild": "3.2.7", "esm": "3.2.25", - "jasmine": "4.1.0", + "jasmine": "4.2.0", "jsdoc": "3.6.10", - "lorem-ipsum": "2.0.4", + "lorem-ipsum": "2.0.8", "rcedit": "3.0.1", "webpack": "5.73.0", "webpack-cli": "4.9.2" @@ -3093,9 +3093,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", - "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz", + "integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==" }, "node_modules/debug": { "version": "4.3.4", @@ -6337,22 +6337,22 @@ "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" }, "node_modules/jasmine": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.1.0.tgz", - "integrity": "sha512-4VhjbUgwfNS9CBnUMoSWr9tdNgOoOhNIjAD8YRxTn+PmOf4qTSC0Uqhk66dWGnz2vJxtNIU0uBjiwnsp4Ud9VA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.2.0.tgz", + "integrity": "sha512-6SQRXHs5O++mp52PkoJoi9zuLYqp1IaqepRNmAQn5rUBo9VUnckpkkXQQD5PAuCCVVB1ULDImvWYCPV/ZVnaGQ==", "dev": true, "dependencies": { "glob": "^7.1.6", - "jasmine-core": "^4.1.0" + "jasmine-core": "^4.2.0" }, "bin": { "jasmine": "bin/jasmine.js" } }, "node_modules/jasmine-core": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.1.0.tgz", - "integrity": "sha512-8E8BiffCL8sBwK1zU9cbavLe8xpJAgOduSJ6N8PJVv8VosQ/nxVTuXj2kUeHxTlZBVvh24G19ga7xdiaxlceKg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.2.0.tgz", + "integrity": "sha512-OcFpBrIhnbmb9wfI8cqPSJ50pv3Wg4/NSgoZIqHzIwO/2a9qivJWzv8hUvaREIMYYJBas6AvfXATFdVuzzCqVw==", "dev": true }, "node_modules/jest-worker": { @@ -6855,12 +6855,12 @@ } }, "node_modules/lorem-ipsum": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/lorem-ipsum/-/lorem-ipsum-2.0.4.tgz", - "integrity": "sha512-TD+ERYfxjYiUfOyaKU6OH4euumNVeKoo3BxIhokb7bGmoCULsME48onF9NVxYK3CU1z9L5ALnkDkW8lIkHvMNQ==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/lorem-ipsum/-/lorem-ipsum-2.0.8.tgz", + "integrity": "sha512-5RIwHuCb979RASgCJH0VKERn9cQo/+NcAi2BMe9ddj+gp7hujl6BI+qdOG4nVsLDpwWEJwTVYXNKP6BGgbcoGA==", "dev": true, "dependencies": { - "commander": "^2.17.1" + "commander": "^9.3.0" }, "bin": { "lorem-ipsum": "dist/bin/lorem-ipsum.bin.js" @@ -6871,10 +6871,13 @@ } }, "node_modules/lorem-ipsum/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz", + "integrity": "sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } }, "node_modules/lowercase-keys": { "version": "1.0.1", @@ -10538,9 +10541,9 @@ } }, "node_modules/ws": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz", - "integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz", + "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==", "engines": { "node": ">=10.0.0" }, @@ -13191,9 +13194,9 @@ } }, "dayjs": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", - "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz", + "integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==" }, "debug": { "version": "4.3.4", @@ -15649,19 +15652,19 @@ } }, "jasmine": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.1.0.tgz", - "integrity": "sha512-4VhjbUgwfNS9CBnUMoSWr9tdNgOoOhNIjAD8YRxTn+PmOf4qTSC0Uqhk66dWGnz2vJxtNIU0uBjiwnsp4Ud9VA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.2.0.tgz", + "integrity": "sha512-6SQRXHs5O++mp52PkoJoi9zuLYqp1IaqepRNmAQn5rUBo9VUnckpkkXQQD5PAuCCVVB1ULDImvWYCPV/ZVnaGQ==", "dev": true, "requires": { "glob": "^7.1.6", - "jasmine-core": "^4.1.0" + "jasmine-core": "^4.2.0" } }, "jasmine-core": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.1.0.tgz", - "integrity": "sha512-8E8BiffCL8sBwK1zU9cbavLe8xpJAgOduSJ6N8PJVv8VosQ/nxVTuXj2kUeHxTlZBVvh24G19ga7xdiaxlceKg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.2.0.tgz", + "integrity": "sha512-OcFpBrIhnbmb9wfI8cqPSJ50pv3Wg4/NSgoZIqHzIwO/2a9qivJWzv8hUvaREIMYYJBas6AvfXATFdVuzzCqVw==", "dev": true }, "jest-worker": { @@ -16075,18 +16078,18 @@ } }, "lorem-ipsum": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/lorem-ipsum/-/lorem-ipsum-2.0.4.tgz", - "integrity": "sha512-TD+ERYfxjYiUfOyaKU6OH4euumNVeKoo3BxIhokb7bGmoCULsME48onF9NVxYK3CU1z9L5ALnkDkW8lIkHvMNQ==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/lorem-ipsum/-/lorem-ipsum-2.0.8.tgz", + "integrity": "sha512-5RIwHuCb979RASgCJH0VKERn9cQo/+NcAi2BMe9ddj+gp7hujl6BI+qdOG4nVsLDpwWEJwTVYXNKP6BGgbcoGA==", "dev": true, "requires": { - "commander": "^2.17.1" + "commander": "^9.3.0" }, "dependencies": { "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz", + "integrity": "sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==", "dev": true } } @@ -18915,9 +18918,9 @@ } }, "ws": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz", - "integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz", + "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==", "requires": {} }, "xdg-basedir": { diff --git a/package.json b/package.json index a7930f36d..f000c3f07 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "tmp": "0.2.1", "turndown": "7.1.1", "unescape": "1.0.1", - "ws": "8.7.0", + "ws": "8.8.0", "yauzl": "2.10.0" }, "devDependencies": { @@ -90,9 +90,9 @@ "electron-packager": "15.5.1", "electron-rebuild": "3.2.7", "esm": "3.2.25", - "jasmine": "4.1.0", + "jasmine": "4.2.0", "jsdoc": "3.6.10", - "lorem-ipsum": "2.0.4", + "lorem-ipsum": "2.0.8", "rcedit": "3.0.1", "webpack": "5.73.0", "webpack-cli": "4.9.2" diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index 446f19a75..cc70dce10 100644 --- a/src/public/app/layouts/desktop_layout.js +++ b/src/public/app/layouts/desktop_layout.js @@ -50,6 +50,7 @@ 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"; +import BulkActionsDialog from "../widgets/dialogs/bulk_actions.js"; export default class DesktopLayout { constructor(customWidgets) { @@ -174,6 +175,7 @@ export default class DesktopLayout { .child(...this.customWidgets.get('right-pane')) ) ) - ); + ) + .child(new BulkActionsDialog()); } } diff --git a/src/public/app/services/tree_context_menu.js b/src/public/app/services/tree_context_menu.js index 28b8ee256..086beb2cb 100644 --- a/src/public/app/services/tree_context_menu.js +++ b/src/public/app/services/tree_context_menu.js @@ -91,7 +91,7 @@ class TreeContextMenu { enabled: notSearch && noSelectedNotes }, { title: "Import into note", command: "importIntoNote", uiIcon: "bx bx-empty", enabled: notSearch && noSelectedNotes }, - { title: "Bulk assign attributes", command: "bulkAssignAttributes", uiIcon: "bx bx-empty", + { title: "Bulk actions", command: "bulkActions", uiIcon: "bx bx-empty", enabled: true } ].filter(row => row !== null); } diff --git a/src/public/app/widgets/bulk_actions/abstract_bulk_action.js b/src/public/app/widgets/bulk_actions/abstract_bulk_action.js index a863cf395..21c2860c8 100644 --- a/src/public/app/widgets/bulk_actions/abstract_bulk_action.js +++ b/src/public/app/widgets/bulk_actions/abstract_bulk_action.js @@ -1,6 +1,5 @@ import server from "../../services/server.js"; import ws from "../../services/ws.js"; -import Component from "../component.js"; import utils from "../../services/utils.js"; export default class AbstractBulkAction { @@ -48,6 +47,6 @@ export default class AbstractBulkAction { await ws.waitForMaxKnownEntityChangeId(); - await this.triggerCommand('refreshSearchDefinition'); + //await this.triggerCommand('refreshSearchDefinition'); } } diff --git a/src/public/app/widgets/bulk_actions/label/rename_label.js b/src/public/app/widgets/bulk_actions/label/rename_label.js index a215846d7..ec66dede0 100644 --- a/src/public/app/widgets/bulk_actions/label/rename_label.js +++ b/src/public/app/widgets/bulk_actions/label/rename_label.js @@ -5,7 +5,7 @@ const TPL = `
-
Rename label from:
+
Rename label from:
spacedUpdate.scheduleUpdate()); $newLabelName.on('input', () => spacedUpdate.scheduleUpdate()); diff --git a/src/public/app/widgets/bulk_actions/relation/rename_relation.js b/src/public/app/widgets/bulk_actions/relation/rename_relation.js index 9031b88a7..9d5c674cf 100644 --- a/src/public/app/widgets/bulk_actions/relation/rename_relation.js +++ b/src/public/app/widgets/bulk_actions/relation/rename_relation.js @@ -5,7 +5,7 @@ const TPL = `
-
Rename relation from:
+
Rename relation from:
+ + + +
`; + +export default class BulkActionsDialog extends BasicWidget { + doRender() { + this.$widget = $(TPL); + this.$availableActionList = this.$widget.find(".bulk-available-action-list"); + this.$existingActionList = this.$widget.find(".bulk-existing-action-list"); + + this.$widget.on('click', '[data-action-add]', async event => { + const actionName = $(event.target).attr('data-action-add'); + + await bulkActionService.addAction('bulkaction', actionName); + + await this.refresh(); + }); + } + + async refresh() { + this.renderAvailableActions(); + + const bulkActionNote = await froca.getNote('bulkaction'); + + const actions = bulkActionService.parseActions(bulkActionNote); + + this.$existingActionList.empty(); + + if (actions.length > 0) { + this.$existingActionList.append(...actions.map(action => action.render())); + } else { + this.$existingActionList.append($("

None yet ... add an action by clicking one of the available ones above.

")) + } + } + + renderAvailableActions() { + this.$availableActionList.empty(); + + for (const actionGroup of bulkActionService.ACTION_GROUPS) { + const $actionGroupList = $(""); + const $actionGroup = $("") + .append($("").text(actionGroup.title + ": ")) + .append($actionGroupList); + + for (const action of actionGroup.actions) { + $actionGroupList.append( + $('
@@ -79,6 +81,15 @@ export default class BulkActionsDialog extends BasicWidget { await this.refresh(); }); + + this.$executeButton = this.$widget.find(".execute-bulk-actions"); + this.$executeButton.on("click", async () => { + await server.post("bulk-action", { noteIds: this.selectedOrActiveNoteIds }); + + toastService.showMessage("Bulk actions have been executed successfully.", 3000); + + utils.closeActiveDialog(); + }); } async refresh() { @@ -119,12 +130,15 @@ export default class BulkActionsDialog extends BasicWidget { } entitiesReloadedEvent({loadResults}) { - if (loadResults.getAttributes().find(attr => attr.type === 'label' && attr.name === 'action')) { + // only refreshing deleted attrs, otherwise components update themselves + if (loadResults.getAttributes().find(attr => attr.type === 'label' && attr.name === 'action' && attr.isDeleted)) { this.refresh(); } } - async bulkActionsEvent({node}) { + async bulkActionsEvent({selectedOrActiveNoteIds}) { + this.selectedOrActiveNoteIds = selectedOrActiveNoteIds; + await this.refresh(); utils.openDialog(this.$widget); diff --git a/src/public/app/widgets/ribbon_widgets/search_definition.js b/src/public/app/widgets/ribbon_widgets/search_definition.js index eb05c5bb9..3c1451313 100644 --- a/src/public/app/widgets/ribbon_widgets/search_definition.js +++ b/src/public/app/widgets/ribbon_widgets/search_definition.js @@ -307,7 +307,8 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget { } entitiesReloadedEvent({loadResults}) { - if (loadResults.getAttributes().find(attr => attr.type === 'label' && attr.name === 'action')) { + // only refreshing deleted attrs, otherwise components update themselves + if (loadResults.getAttributes().find(attr => attr.type === 'label' && attr.name === 'action' && attr.isDeleted)) { this.refresh(); } } diff --git a/src/routes/api/bulk_action.js b/src/routes/api/bulk_action.js new file mode 100644 index 000000000..20104a46b --- /dev/null +++ b/src/routes/api/bulk_action.js @@ -0,0 +1,14 @@ +const becca = require("../../becca/becca"); +const bulkActionService = require("../../services/bulk_actions"); + +function execute(req) { + const {noteIds} = req.body; + + const bulkActionNote = becca.getNote('bulkaction'); + + bulkActionService.executeActions(bulkActionNote, noteIds); +} + +module.exports = { + execute +}; diff --git a/src/routes/routes.js b/src/routes/routes.js index 29aaf93a1..543c3822c 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -31,6 +31,7 @@ const scriptRoute = require('./api/script'); const senderRoute = require('./api/sender'); const filesRoute = require('./api/files'); const searchRoute = require('./api/search'); +const bulkActionRoute = require('./api/bulk_action'); const specialNotesRoute = require('./api/special_notes'); const noteMapRoute = require('./api/note_map'); const clipperRoute = require('./api/clipper'); @@ -357,6 +358,8 @@ function register(app) { apiRoute(GET, '/api/search/:searchString', searchRoute.search); apiRoute(GET, '/api/search-templates', searchRoute.searchTemplates); + apiRoute(POST, '/api/bulk-action', bulkActionRoute.execute); + route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); // this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username) apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession); From 1bfc5fb77fce9ce0c51208a11e4673b8dd21359a Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 12 Jun 2022 10:35:30 +0200 Subject: [PATCH 086/250] calculate affected counts and take into account includeDescendants when executing --- .../app/widgets/dialogs/bulk_actions.js | 23 +++++++++-- src/routes/api/bulk_action.js | 40 +++++++++++++++++-- src/routes/routes.js | 3 +- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/public/app/widgets/dialogs/bulk_actions.js b/src/public/app/widgets/dialogs/bulk_actions.js index df179c1f7..c9783ff83 100644 --- a/src/public/app/widgets/dialogs/bulk_actions.js +++ b/src/public/app/widgets/dialogs/bulk_actions.js @@ -47,8 +47,8 @@ const TPL = `

Affected notes: 0

- -
@@ -71,6 +71,12 @@ const TPL = ` export default class BulkActionsDialog extends BasicWidget { doRender() { this.$widget = $(TPL); + + this.$includeDescendants = this.$widget.find(".include-descendants"); + this.$includeDescendants.on("change", () => this.refresh()); + + this.$affectedNoteCount = this.$widget.find(".affected-note-count"); + this.$availableActionList = this.$widget.find(".bulk-available-action-list"); this.$existingActionList = this.$widget.find(".bulk-existing-action-list"); @@ -84,7 +90,10 @@ export default class BulkActionsDialog extends BasicWidget { this.$executeButton = this.$widget.find(".execute-bulk-actions"); this.$executeButton.on("click", async () => { - await server.post("bulk-action", { noteIds: this.selectedOrActiveNoteIds }); + await server.post("bulk-action/execute", { + noteIds: this.selectedOrActiveNoteIds, + includeDescendants: this.$includeDescendants.is(":checked") + }); toastService.showMessage("Bulk actions have been executed successfully.", 3000); @@ -95,6 +104,13 @@ export default class BulkActionsDialog extends BasicWidget { async refresh() { this.renderAvailableActions(); + const {affectedNoteCount} = await server.post('bulk-action/affected-notes', { + noteIds: this.selectedOrActiveNoteIds, + includeDescendants: this.$includeDescendants.is(":checked") + }); + + this.$affectedNoteCount.text(affectedNoteCount); + const bulkActionNote = await froca.getNote('bulkaction'); const actions = bulkActionService.parseActions(bulkActionNote); @@ -138,6 +154,7 @@ export default class BulkActionsDialog extends BasicWidget { async bulkActionsEvent({selectedOrActiveNoteIds}) { this.selectedOrActiveNoteIds = selectedOrActiveNoteIds; + this.$includeDescendants.prop("checked", false); await this.refresh(); diff --git a/src/routes/api/bulk_action.js b/src/routes/api/bulk_action.js index 20104a46b..902e9a675 100644 --- a/src/routes/api/bulk_action.js +++ b/src/routes/api/bulk_action.js @@ -2,13 +2,47 @@ const becca = require("../../becca/becca"); const bulkActionService = require("../../services/bulk_actions"); function execute(req) { - const {noteIds} = req.body; + const {noteIds, includeDescendants} = req.body; + + const affectedNoteIds = getAffectedNoteIds(noteIds, includeDescendants); const bulkActionNote = becca.getNote('bulkaction'); - bulkActionService.executeActions(bulkActionNote, noteIds); + bulkActionService.executeActions(bulkActionNote, affectedNoteIds); +} + +function getAffectedNoteCount(req) { + const {noteIds, includeDescendants} = req.body; + + const affectedNoteIds = getAffectedNoteIds(noteIds, includeDescendants); + + return { + affectedNoteCount: affectedNoteIds.size + }; +} + +function getAffectedNoteIds(noteIds, includeDescendants) { + const affectedNoteIds = new Set(); + + for (const noteId of noteIds) { + const note = becca.getNote(noteId); + + if (!note) { + continue; + } + + affectedNoteIds.add(noteId); + + if (includeDescendants) { + for (const descendantNoteId of note.getDescendantNoteIds()) { + affectedNoteIds.add(descendantNoteId); + } + } + } + return affectedNoteIds; } module.exports = { - execute + execute, + getAffectedNoteCount }; diff --git a/src/routes/routes.js b/src/routes/routes.js index 543c3822c..b9f2d5e5e 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -358,7 +358,8 @@ function register(app) { apiRoute(GET, '/api/search/:searchString', searchRoute.search); apiRoute(GET, '/api/search-templates', searchRoute.searchTemplates); - apiRoute(POST, '/api/bulk-action', bulkActionRoute.execute); + apiRoute(POST, '/api/bulk-action/execute', bulkActionRoute.execute); + apiRoute(POST, '/api/bulk-action/affected-notes', bulkActionRoute.getAffectedNoteCount); route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); // this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username) From 4aaa0f8d8c0c1b713fce37cf5bc6dfcca078a5f7 Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 12 Jun 2022 13:57:22 +0200 Subject: [PATCH 087/250] change tree node icon for selected notes to quickly bring up bulk action dialog --- .../app/dialogs/bulk_assign_attributes.js | 48 --------------- src/public/app/services/tree_context_menu.js | 11 +--- .../app/widgets/dialogs/bulk_actions.js | 4 +- src/public/app/widgets/note_tree.js | 23 ++++++- src/public/stylesheets/tree.css | 6 ++ src/views/desktop.ejs | 1 - src/views/dialogs/bulk_assign_attributes.ejs | 61 ------------------- 7 files changed, 31 insertions(+), 123 deletions(-) delete mode 100644 src/public/app/dialogs/bulk_assign_attributes.js delete mode 100644 src/views/dialogs/bulk_assign_attributes.ejs diff --git a/src/public/app/dialogs/bulk_assign_attributes.js b/src/public/app/dialogs/bulk_assign_attributes.js deleted file mode 100644 index d6a31a4dd..000000000 --- a/src/public/app/dialogs/bulk_assign_attributes.js +++ /dev/null @@ -1,48 +0,0 @@ -import utils from "../services/utils.js"; -import bulkActionService from "../services/bulk_action.js"; -import froca from "../services/froca.js"; - -const $dialog = $("#bulk-assign-attributes-dialog"); -const $availableActionList = $("#bulk-available-action-list"); -const $existingActionList = $("#bulk-existing-action-list"); - -$dialog.on('click', '[data-action-add]', async event => { - const actionName = $(event.target).attr('data-action-add'); - - await bulkActionService.addAction('bulkaction', actionName); - - await refresh(); -}); - -for (const actionGroup of bulkActionService.ACTION_GROUPS) { - const $actionGroupList = $(""); - const $actionGroup = $("") - .append($("").text(actionGroup.title + ": ")) - .append($actionGroupList); - - for (const action of actionGroup.actions) { - $actionGroupList.append( - $('
-
Monospace font
+
Monospace (code) font
diff --git a/src/public/app/widgets/type_widgets/editable_text.js b/src/public/app/widgets/type_widgets/editable_text.js index b36048611..bdd7573df 100644 --- a/src/public/app/widgets/type_widgets/editable_text.js +++ b/src/public/app/widgets/type_widgets/editable_text.js @@ -266,10 +266,25 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { await this.initialized; const selection = this.textEditor.model.document.selection; - if (!selection.hasAttribute('linkHref')) return; + const selectedElement = selection.getSelectedElement(); + + if (selectedElement?.name === 'reference') { + // reference link + const notePath = selectedElement.getAttribute('notePath'); + + if (notePath) { + await appContext.tabManager.getActiveContext().setNote(notePath); + return; + } + } + + if (!selection.hasAttribute('linkHref')) { + return; + } const selectedLinkUrl = selection.getAttribute('linkHref'); const notePath = link.getNotePathFromUrl(selectedLinkUrl); + if (notePath) { await appContext.tabManager.getActiveContext().setNote(notePath); } else { From e51276f53290f785e26d7b198840932f7d712e3e Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 21 Jun 2022 23:27:34 +0200 Subject: [PATCH 145/250] floating buttons WIP --- src/public/app/layouts/desktop_layout.js | 9 ++- .../{ => floating_buttons}/backlinks.js | 31 ++------- .../floating_buttons/floating_buttons.js | 32 +++++++++ .../floating_buttons/relation_map_buttons.js | 68 +++++++++++++++++++ .../app/widgets/type_widgets/relation_map.js | 53 --------------- 5 files changed, 112 insertions(+), 81 deletions(-) rename src/public/app/widgets/{ => floating_buttons}/backlinks.js (80%) create mode 100644 src/public/app/widgets/floating_buttons/floating_buttons.js create mode 100644 src/public/app/widgets/floating_buttons/relation_map_buttons.js diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index de933fbb2..70656e413 100644 --- a/src/public/app/layouts/desktop_layout.js +++ b/src/public/app/layouts/desktop_layout.js @@ -46,7 +46,7 @@ import OpenNoteButtonWidget from "../widgets/buttons/open_note_button_widget.js" import MermaidWidget from "../widgets/mermaid.js"; import BookmarkButtons from "../widgets/bookmark_buttons.js"; import NoteWrapperWidget from "../widgets/note_wrapper.js"; -import BacklinksWidget from "../widgets/backlinks.js"; +import BacklinksWidget from "../widgets/floating_buttons/backlinks.js"; import SharedInfoWidget from "../widgets/shared_info.js"; import FindWidget from "../widgets/find.js"; import TocWidget from "../widgets/toc.js"; @@ -75,6 +75,8 @@ import InfoDialog from "../widgets/dialogs/info.js"; import ConfirmDialog from "../widgets/dialogs/confirm.js"; import PromptDialog from "../widgets/dialogs/prompt.js"; import OptionsDialog from "../widgets/dialogs/options.js"; +import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js"; +import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js"; export default class DesktopLayout { constructor(customWidgets) { @@ -177,7 +179,10 @@ export default class DesktopLayout { ) .child(new SharedInfoWidget()) .child(new NoteUpdateStatusWidget()) - .child(new BacklinksWidget()) + .child(new FloatingButtons() + .child(new BacklinksWidget()) + .child(new RelationMapButtons()) + ) .child(new MermaidWidget()) .child( new ScrollingContainer() diff --git a/src/public/app/widgets/backlinks.js b/src/public/app/widgets/floating_buttons/backlinks.js similarity index 80% rename from src/public/app/widgets/backlinks.js rename to src/public/app/widgets/floating_buttons/backlinks.js index 2e32d5a6f..2b622b81a 100644 --- a/src/public/app/widgets/backlinks.js +++ b/src/public/app/widgets/floating_buttons/backlinks.js @@ -1,7 +1,7 @@ -import NoteContextAwareWidget from "./note_context_aware_widget.js"; -import linkService from "../services/link.js"; -import server from "../services/server.js"; -import froca from "../services/froca.js"; +import NoteContextAwareWidget from "../note_context_aware_widget.js"; +import linkService from "../../services/link.js"; +import server from "../../services/server.js"; +import froca from "../../services/froca.js"; const TPL = `
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +boolean + + +
+
+ + + + + + + + + + + + + +

hasAttribute(type, name, valueopt) → {boolean}

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
type + + + + + + + +
name + + + + + + + +
value + + + + <optional>
+ + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
@@ -7147,7 +7357,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7376,7 +7586,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7574,7 +7784,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7772,7 +7982,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7970,7 +8180,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8120,7 +8330,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9042,7 +9252,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -9222,7 +9432,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -9402,7 +9612,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -9597,7 +9807,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -9829,7 +10039,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -10009,7 +10219,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -10169,7 +10379,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -10411,7 +10621,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -10622,7 +10832,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -10833,7 +11043,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
diff --git a/docs/backend_api/NoteRevision.html b/docs/backend_api/NoteRevision.html index ca0fb5b83..c8c829447 100644 --- a/docs/backend_api/NoteRevision.html +++ b/docs/backend_api/NoteRevision.html @@ -1448,7 +1448,7 @@ It's used for seamless note versioning.
Source:
@@ -1550,7 +1550,7 @@ It's used for seamless note versioning.
Source:
@@ -1830,7 +1830,7 @@ It's used for seamless note versioning.
Source:
diff --git a/docs/backend_api/becca_entities_note.js.html b/docs/backend_api/becca_entities_note.js.html index c73b7e304..b81585f36 100644 --- a/docs/backend_api/becca_entities_note.js.html +++ b/docs/backend_api/becca_entities_note.js.html @@ -477,6 +477,12 @@ class Note extends AbstractEntity { return this.inheritableAttributeCache; } + /** + * @param type + * @param name + * @param [value] + * @returns {boolean} + */ hasAttribute(type, name, value) { return !!this.getAttributes().find(attr => attr.type === type @@ -1277,7 +1283,7 @@ class Note extends AbstractEntity { ? this.dateModified : contentMetadata.dateModified, dateCreated: dateUtils.localNowDateTime() - }).save(); + }, true).save(); noteRevision.setContent(content); diff --git a/docs/backend_api/becca_entities_note_revision.js.html b/docs/backend_api/becca_entities_note_revision.js.html index a1f9ca729..f4062ef6c 100644 --- a/docs/backend_api/becca_entities_note_revision.js.html +++ b/docs/backend_api/becca_entities_note_revision.js.html @@ -47,7 +47,7 @@ class NoteRevision extends AbstractEntity { static get primaryKeyName() { return "noteRevisionId"; } static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified"]; } - constructor(row) { + constructor(row, titleDecrypted = false) { super(); /** @type {string} */ @@ -75,13 +75,10 @@ class NoteRevision extends AbstractEntity { /** @type {number} */ this.contentLength = row.contentLength; - if (this.isProtected) { - if (protectedSessionService.isProtectedSessionAvailable()) { - this.title = protectedSessionService.decryptString(this.title); - } - else { - this.title = "[protected]"; - } + if (this.isProtected && !titleDecrypted) { + this.title = protectedSessionService.isProtectedSessionAvailable() + ? protectedSessionService.decryptString(this.title) + : "[protected]"; } } @@ -96,8 +93,8 @@ class NoteRevision extends AbstractEntity { /* * Note revision content has quite special handling - it's not a separate entity, but a lazily loaded - * part of NoteRevision entity with it's own sync. Reason behind this hybrid design is that - * content can be quite large and it's not necessary to load it / fill memory for any note access even + * part of NoteRevision entity with its own sync. Reason behind this hybrid design is that + * content can be quite large, and it's not necessary to load it / fill memory for any note access even * if we don't need a content, especially for bulk operations like search. * * This is the same approach as is used for Note's content. diff --git a/docs/frontend_api/Attribute.html b/docs/frontend_api/Attribute.html index 27344c376..34387fd67 100644 --- a/docs/frontend_api/Attribute.html +++ b/docs/frontend_api/Attribute.html @@ -727,6 +727,108 @@ and relation (representing named relationship between source and target note)(async) getTargetNote() → {Promise.<NoteShort>} + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Promise.<NoteShort> + + +
+
+ + + + + + + + diff --git a/docs/frontend_api/entities_attribute.js.html b/docs/frontend_api/entities_attribute.js.html index e43585495..72e9731b3 100644 --- a/docs/frontend_api/entities_attribute.js.html +++ b/docs/frontend_api/entities_attribute.js.html @@ -61,8 +61,19 @@ class Attribute { return this.froca.notes[this.noteId]; } + /** @returns {Promise<NoteShort>} */ + async getTargetNote() { + const targetNoteId = this.targetNoteId; + + return await this.froca.getNote(targetNoteId, true); + } + get targetNoteId() { // alias - return this.type === 'relation' ? this.value : undefined; + if (this.type !== 'relation') { + throw new Error(`Attribute ${this.attributeId} is not a relation`); + } + + return this.value; } get isAutoLink() { diff --git a/src/public/app/entities/attribute.js b/src/public/app/entities/attribute.js index 80f255d6c..094c5bfa8 100644 --- a/src/public/app/entities/attribute.js +++ b/src/public/app/entities/attribute.js @@ -33,8 +33,19 @@ class Attribute { return this.froca.notes[this.noteId]; } + /** @returns {Promise} */ + async getTargetNote() { + const targetNoteId = this.targetNoteId; + + return await this.froca.getNote(targetNoteId, true); + } + get targetNoteId() { // alias - return this.type === 'relation' ? this.value : undefined; + if (this.type !== 'relation') { + throw new Error(`Attribute ${this.attributeId} is not a relation`); + } + + return this.value; } get isAutoLink() { From 52812c27a1c158d4e0f07e6c3f1612d6cf541d6c Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 14 Jul 2022 23:00:35 +0200 Subject: [PATCH 174/250] useMaxWidth for mermaid pie widget, fixes #2984 --- src/public/app/widgets/mermaid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public/app/widgets/mermaid.js b/src/public/app/widgets/mermaid.js index 7be42e692..094fe1d3d 100644 --- a/src/public/app/widgets/mermaid.js +++ b/src/public/app/widgets/mermaid.js @@ -61,7 +61,7 @@ export default class MermaidWidget extends NoteContextAwareWidget { gantt: { useMaxWidth: false }, "class": { useMaxWidth: false }, state: { useMaxWidth: false }, - pie: { useMaxWidth: false }, + pie: { useMaxWidth: true }, journey: { useMaxWidth: false }, git: { useMaxWidth: false }, }); From 9114b1befb29a96e61c065a333dfabdf633d1bbc Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 14 Jul 2022 23:39:16 +0200 Subject: [PATCH 175/250] make sure the headings in TOC contain text only, https://github.com/zadam/trilium-web-clipper/issues/42 --- src/public/app/widgets/toc.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/public/app/widgets/toc.js b/src/public/app/widgets/toc.js index f8f7a2352..9d7105f22 100644 --- a/src/public/app/widgets/toc.js +++ b/src/public/app/widgets/toc.js @@ -143,7 +143,9 @@ export default class TocWidget extends CollapsibleWidget { // // Create the list item and set up the click callback // - const $li = $('
  • ' + m[2] + '
  • '); + + const headingText = $("
    ").html(m[2]).text(); + const $li = $('
  • ').text(headingText); // XXX Do this with CSS? How to inject CSS in doRender? $li.hover(function () { $(this).css("font-weight", "bold"); From 4ca59dcc5c0e0d0e6f74ae72b6eaf0abdb517ffd Mon Sep 17 00:00:00 2001 From: Tom Free <7283497+thfrei@users.noreply.github.com> Date: Thu, 14 Jul 2022 23:49:30 +0200 Subject: [PATCH 176/250] upgrade to excalidraw v0.12, fix breaking changes --- package-lock.json | 78 +++++++++---------- package.json | 6 +- src/public/app/widgets/type_widgets/canvas.js | 8 +- 3 files changed, 43 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2edb0459d..27cfc80e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "trilium", - "version": "0.53.0-beta", + "version": "0.53.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "trilium", - "version": "0.53.0-beta", + "version": "0.53.2", "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { "@electron/remote": "2.0.8", - "@excalidraw/excalidraw": "0.11.0", + "@excalidraw/excalidraw": "0.12.0", "archiver": "5.3.1", "async-mutex": "0.3.2", "axios": "0.27.2", @@ -51,8 +51,8 @@ "open": "8.4.0", "portscanner": "2.2.0", "rand-token": "1.0.1", - "react": "17.0.2", - "react-dom": "17.0.2", + "react": "18.2.0", + "react-dom": "18.2.0", "request": "2.88.2", "rimraf": "3.0.2", "sanitize-filename": "1.6.3", @@ -246,9 +246,9 @@ } }, "node_modules/@excalidraw/excalidraw": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.11.0.tgz", - "integrity": "sha512-wY0UdnN9JAcBKLzkGlVXiPSKgTO06YeeBhoIy/ezIiMJtfFtWBPjRjJROkdIGqnUmBz5L16MkuXE7b8j1b1ouw==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.12.0.tgz", + "integrity": "sha512-xMPmKmOEgKij43k5m6Koaevb+SBw6La7MT9UDY8Iq7nQCMhA1HQwcUURfSkZ3ERibdQmMsAGtjSLbkX7hrA3+A==", "dependencies": { "dotenv": "10.0.0" }, @@ -8524,28 +8524,26 @@ } }, "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "scheduler": "^0.23.0" }, "peerDependencies": { - "react": "17.0.2" + "react": "^18.2.0" } }, "node_modules/read-config-file": { @@ -8937,12 +8935,11 @@ } }, "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "node_modules/schema-utils": { @@ -11025,9 +11022,9 @@ } }, "@excalidraw/excalidraw": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.11.0.tgz", - "integrity": "sha512-wY0UdnN9JAcBKLzkGlVXiPSKgTO06YeeBhoIy/ezIiMJtfFtWBPjRjJROkdIGqnUmBz5L16MkuXE7b8j1b1ouw==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.12.0.tgz", + "integrity": "sha512-xMPmKmOEgKij43k5m6Koaevb+SBw6La7MT9UDY8Iq7nQCMhA1HQwcUURfSkZ3ERibdQmMsAGtjSLbkX7hrA3+A==", "requires": { "dotenv": "10.0.0" } @@ -17463,22 +17460,20 @@ } }, "react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "requires": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "scheduler": "^0.23.0" } }, "read-config-file": { @@ -17804,12 +17799,11 @@ } }, "scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "schema-utils": { diff --git a/package.json b/package.json index af70156ba..4eef838b0 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ }, "dependencies": { "@electron/remote": "2.0.8", - "@excalidraw/excalidraw": "0.11.0", + "@excalidraw/excalidraw": "0.12.0", "archiver": "5.3.1", "async-mutex": "0.3.2", "axios": "0.27.2", @@ -66,8 +66,8 @@ "open": "8.4.0", "portscanner": "2.2.0", "rand-token": "1.0.1", - "react": "17.0.2", - "react-dom": "17.0.2", + "react": "18.2.0", + "react-dom": "18.2.0", "request": "2.88.2", "rimraf": "3.0.2", "sanitize-filename": "1.6.3", diff --git a/src/public/app/widgets/type_widgets/canvas.js b/src/public/app/widgets/type_widgets/canvas.js index 924c0931a..8be9a7569 100644 --- a/src/public/app/widgets/type_widgets/canvas.js +++ b/src/public/app/widgets/type_widgets/canvas.js @@ -253,7 +253,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { * parallel svg export to combat bitrot and enable rendering image for note inclusion, * preview and share. */ - const svg = await window.Excalidraw.exportToSvg({ + const svg = await window.ExcalidrawLib.exportToSvg({ elements, appState, exportPadding: 5, // 5 px padding @@ -317,7 +317,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { createExcalidrawReactApp() { const React = window.React; - const Excalidraw = window.Excalidraw; + const { Excalidraw } = window.ExcalidrawLib; const excalidrawRef = React.useRef(null); this.excalidrawRef = excalidrawRef; @@ -379,7 +379,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { className: "excalidraw-wrapper", ref: excalidrawWrapperRef }, - React.createElement(Excalidraw.default, { + React.createElement(Excalidraw, { // this makes sure that 1) manual theme switch button is hidden 2) theme stays as it should after opening menu theme: this.themeStyle, ref: excalidrawRef, @@ -420,7 +420,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget { getSceneVersion() { if (this.excalidrawRef) { const elements = this.excalidrawRef.current.getSceneElements(); - return window.Excalidraw.getSceneVersion(elements); + return window.ExcalidrawLib.getSceneVersion(elements); } else { return this.SCENE_VERSION_ERROR; } From e6358afb62b9609fcbb63c9763e5d3d39a991028 Mon Sep 17 00:00:00 2001 From: zadam Date: Fri, 15 Jul 2022 23:35:17 +0200 Subject: [PATCH 177/250] mitigate flickering in note tooltip, #2988 --- src/public/app/services/note_tooltip.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/public/app/services/note_tooltip.js b/src/public/app/services/note_tooltip.js index 77ec358d6..74c4d20ad 100644 --- a/src/public/app/services/note_tooltip.js +++ b/src/public/app/services/note_tooltip.js @@ -59,10 +59,11 @@ async function mouseEnterHandler() { $(this).tooltip({ delay: {"show": 300, "hide": 100}, container: 'body', - placement: 'auto', + // https://github.com/zadam/trilium/issues/2794 https://github.com/zadam/trilium/issues/2988 + // with bottom this flickering happens a bit less + placement: 'bottom', trigger: 'manual', boundary: 'window', - offset: "0, 20", // workaround for https://github.com/zadam/trilium/issues/2794 title: html, html: true, template: '', From 57c5b6d61f57acfdd0ba313dd6b12f0fa1257227 Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 16 Jul 2022 00:15:45 +0200 Subject: [PATCH 178/250] added an option to define a "min TOC headings", #2985 --- src/public/app/widgets/dialogs/options.js | 5 ++ .../app/widgets/dialogs/options/appearance.js | 32 ++-------- .../app/widgets/dialogs/options/text_notes.js | 64 +++++++++++++++++++ src/public/app/widgets/toc.js | 5 +- src/routes/api/options.js | 3 +- src/services/options_init.js | 3 +- 6 files changed, 81 insertions(+), 31 deletions(-) create mode 100644 src/public/app/widgets/dialogs/options/text_notes.js diff --git a/src/public/app/widgets/dialogs/options.js b/src/public/app/widgets/dialogs/options.js index cac0caf64..288bae91d 100644 --- a/src/public/app/widgets/dialogs/options.js +++ b/src/public/app/widgets/dialogs/options.js @@ -34,6 +34,9 @@ const TPL = `
  • + @@ -60,6 +63,7 @@ const TPL = `
    +
    @@ -88,6 +92,7 @@ export default class OptionsDialog extends BasicWidget { (await Promise.all([ import('./options/appearance.js'), import('./options/shortcuts.js'), + import('./options/text_notes.js'), import('./options/code_notes.js'), import('./options/password.js'), import('./options/etapi.js'), diff --git a/src/public/app/widgets/dialogs/options/appearance.js b/src/public/app/widgets/dialogs/options/appearance.js index 18f548df6..fe1abc6d3 100644 --- a/src/public/app/widgets/dialogs/options/appearance.js +++ b/src/public/app/widgets/dialogs/options/appearance.js @@ -32,22 +32,13 @@ const TPL = `
    -
    - - -
    - -
    +
    -
    +
    -
    +
    @@ -79,12 +70,12 @@ const TPL = `
    Main font
    -
    +
    -
    +
    @@ -189,7 +180,6 @@ export default class ApperanceOptions { this.$zoomFactorSelect = $("#zoom-factor-select"); this.$nativeTitleBarSelect = $("#native-title-bar-select"); - this.$headingStyle = $("#heading-style"); this.$themeSelect = $("#theme-select"); this.$overrideThemeFonts = $("#override-theme-fonts"); @@ -236,14 +226,6 @@ export default class ApperanceOptions { server.put('options/nativeTitleBarVisible/' + nativeTitleBarVisible); }); - this.$headingStyle.on('change', () => { - const newHeadingStyle = this.$headingStyle.val(); - - this.toggleBodyClass("heading-style-", newHeadingStyle); - - server.put('options/headingStyle/' + newHeadingStyle); - }); - const optionsToSave = [ 'mainFontFamily', 'mainFontSize', 'treeFontFamily', 'treeFontSize', @@ -284,8 +266,6 @@ export default class ApperanceOptions { this.$nativeTitleBarSelect.val(options.nativeTitleBarVisible === 'true' ? 'show' : 'hide'); - this.$headingStyle.val(options.headingStyle); - const themes = [ { val: 'light', title: 'Light' }, { val: 'dark', title: 'Dark' } diff --git a/src/public/app/widgets/dialogs/options/text_notes.js b/src/public/app/widgets/dialogs/options/text_notes.js new file mode 100644 index 000000000..86510a47a --- /dev/null +++ b/src/public/app/widgets/dialogs/options/text_notes.js @@ -0,0 +1,64 @@ +import server from "../../../services/server.js"; + +const TPL = ` +

    Settings on this options tab are saved automatically after each change.

    + + +

    Heading style

    + + +
    + +

    Table of contents

    + + Table of contents will appear in text notes when the note has more than a defined number of headings. You can customize this number: + +
    + +
    + +

    You can also use this option to effectively disable TOC by setting a very high number.

    +`; + +export default class TextNotesOptions { + constructor() { + $("#options-text-notes").html(TPL); + + this.$body = $("body"); + + this.$headingStyle = $("#heading-style"); + this.$headingStyle.on('change', () => { + const newHeadingStyle = this.$headingStyle.val(); + + this.toggleBodyClass("heading-style-", newHeadingStyle); + + server.put('options/headingStyle/' + newHeadingStyle); + }); + + this.$minTocHeadings = $("#min-toc-headings"); + this.$minTocHeadings.on('change', () => { + const minTocHeadings = this.$minTocHeadings.val(); + + server.put('options/minTocHeadings/' + minTocHeadings); + }); + } + + toggleBodyClass(prefix, value) { + for (const clazz of Array.from(this.$body[0].classList)) { // create copy to safely iterate over while removing classes + if (clazz.startsWith(prefix)) { + this.$body.removeClass(clazz); + } + } + + this.$body.addClass(prefix + value); + } + + async optionsLoaded(options) { + this.$headingStyle.val(options.headingStyle); + this.$minTocHeadings.val(options.minTocHeadings); + } +} diff --git a/src/public/app/widgets/toc.js b/src/public/app/widgets/toc.js index f8f7a2352..c127083f1 100644 --- a/src/public/app/widgets/toc.js +++ b/src/public/app/widgets/toc.js @@ -16,6 +16,7 @@ import attributeService from "../services/attributes.js"; import CollapsibleWidget from "./collapsible_widget.js"; +import options from "../services/options.js"; const TPL = `
    diff --git a/src/public/app/widgets/floating_buttons/relation_map_buttons.js b/src/public/app/widgets/floating_buttons/relation_map_buttons.js index be6ff9d2b..72cdb7bf3 100644 --- a/src/public/app/widgets/floating_buttons/relation_map_buttons.js +++ b/src/public/app/widgets/floating_buttons/relation_map_buttons.js @@ -5,25 +5,21 @@ import toastService from "../../services/toast.js"; const TPL = `
    - + + class="relation-map-reset-pan-zoom floating-button btn bx bx-crop no-print" + title="Reset pan & zoom to initial coordinates and magnification"> -
    +
    `; @@ -47,5 +43,6 @@ export default class RelationMapButtons extends NoteContextAwareWidget { this.$zoomInButton.on('click', () => this.triggerEvent('relationMapResetZoomIn', {ntxId: this.ntxId})); this.$zoomOutButton.on('click', () => this.triggerEvent('relationMapResetZoomOut', {ntxId: this.ntxId})); + this.contentSized(); } } diff --git a/src/public/app/widgets/floating_buttons/zpetne_odkazy.js b/src/public/app/widgets/floating_buttons/zpetne_odkazy.js index 49343df0c..674578614 100644 --- a/src/public/app/widgets/floating_buttons/zpetne_odkazy.js +++ b/src/public/app/widgets/floating_buttons/zpetne_odkazy.js @@ -23,7 +23,6 @@ const TPL = ` display: flex; justify-content: space-between; align-items: center; - z-index: 10; } .backlinks-count { diff --git a/src/public/app/widgets/note_map.js b/src/public/app/widgets/note_map.js index f101a7def..eb9d1a888 100644 --- a/src/public/app/widgets/note_map.js +++ b/src/public/app/widgets/note_map.js @@ -25,9 +25,9 @@ const TPL = `
    } -
    - - +
    + +
    diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index e84186be9..a714ee521 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -602,11 +602,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href padding: 0.7rem 1rem 0 1rem !important; /* make modal header padding slightly smaller */ } -.floating-button { - position: absolute !important; - z-index: 100; -} - #toast-container { position: absolute; width: 100%; From 80887fd3c13baecbdb0619a9f462c5a6fbdb5439 Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 24 Jul 2022 21:30:29 +0200 Subject: [PATCH 196/250] export notes via ETAPI, #3012 --- src/becca/entities/branch.js | 11 +++++---- src/etapi/etapi.openapi.yaml | 32 +++++++++++++++++++++++++ src/etapi/notes.js | 20 ++++++++++++++++ test-etapi/export-note-subtree.http | 37 +++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 test-etapi/export-note-subtree.http diff --git a/src/becca/entities/branch.js b/src/becca/entities/branch.js index cc50831fe..1363c1de1 100644 --- a/src/becca/entities/branch.js +++ b/src/becca/entities/branch.js @@ -70,21 +70,22 @@ class Branch extends AbstractEntity { this.becca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this; + const childNote = this.childNote; + + if (!childNote.parentBranches.includes(this)) { + childNote.parentBranches.push(this); + } + if (this.branchId === 'root') { return; } - const childNote = this.childNote; const parentNote = this.parentNote; if (!childNote.parents.includes(parentNote)) { childNote.parents.push(parentNote); } - if (!childNote.parentBranches.includes(this)) { - childNote.parentBranches.push(this); - } - if (!parentNote.children.includes(childNote)) { parentNote.children.push(childNote); } diff --git a/src/etapi/etapi.openapi.yaml b/src/etapi/etapi.openapi.yaml index 811517af5..1644e90bb 100644 --- a/src/etapi/etapi.openapi.yaml +++ b/src/etapi/etapi.openapi.yaml @@ -228,6 +228,38 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /notes/{noteId}/export: + parameters: + - name: noteId + in: path + required: true + schema: + $ref: '#/components/schemas/EntityId' + - name: format + in: query + required: false + schema: + enum: + - html + - markdown + default: html + get: + description: Exports ZIP file export of a given note subtree. To export whole document, use "root" for noteId + operationId: exportNoteSubtree + responses: + '200': + description: export ZIP file + content: + application/zip: + schema: + type: string + format: binary + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /branches/{branchId}: parameters: - name: branchId diff --git a/src/etapi/notes.js b/src/etapi/notes.js index 788b0f4ff..34fdd5bdd 100644 --- a/src/etapi/notes.js +++ b/src/etapi/notes.js @@ -7,6 +7,7 @@ const TaskContext = require("../services/task_context"); const v = require("./validators"); const searchService = require("../services/search/services/search"); const SearchContext = require("../services/search/search_context"); +const zipExportService = require("../services/export/zip"); function register(router) { eu.route(router, 'get', '/etapi/notes', (req, res, next) => { @@ -123,6 +124,25 @@ function register(router) { return res.sendStatus(204); }); + + eu.route(router, 'get' ,'/etapi/notes/:noteId/export', (req, res, next) => { + const note = eu.getAndCheckNote(req.params.noteId); + const format = req.query.format || "html"; + + if (!["html", "markdown"].includes(format)) { + throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'`); + } + + const taskContext = new TaskContext('no-progress-reporting'); + + // technically a branch is being exported (includes prefix), but it's such a minor difference yet usability pain + // (e.g. branchIds are not seen in UI), that we export "note export" instead. + const branch = note.getParentBranches()[0]; + + console.log(note.getParentBranches()); + + zipExportService.exportToZip(taskContext, branch, format, res); + }); } function parseSearchParams(req) { diff --git a/test-etapi/export-note-subtree.http b/test-etapi/export-note-subtree.http new file mode 100644 index 000000000..28d90a362 --- /dev/null +++ b/test-etapi/export-note-subtree.http @@ -0,0 +1,37 @@ +GET {{triliumHost}}/etapi/notes/root/export +Authorization: {{authToken}} + +> {% + client.assert(response.status === 200); + client.assert(response.headers.valueOf("Content-Type") == "application/zip"); +%} + +### + +GET {{triliumHost}}/etapi/notes/root/export?format=html +Authorization: {{authToken}} + +> {% + client.assert(response.status === 200); + client.assert(response.headers.valueOf("Content-Type") == "application/zip"); +%} + +### + +GET {{triliumHost}}/etapi/notes/root/export?format=markdown +Authorization: {{authToken}} + +> {% + client.assert(response.status === 200); + client.assert(response.headers.valueOf("Content-Type") == "application/zip"); +%} + +### + +GET {{triliumHost}}/etapi/notes/root/export?format=wrong +Authorization: {{authToken}} + +> {% + client.assert(response.status === 400); + client.assert(response.body.code === "UNRECOGNIZED_EXPORT_FORMAT"); +%} From 5a37547b37177d81e12532ba02c40e53562c614e Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 28 Jul 2022 22:42:02 +0200 Subject: [PATCH 197/250] use 16 bytes IV for newly encrypted data, closes #3017 --- src/services/data_encryption.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/services/data_encryption.js b/src/services/data_encryption.js index dd369321e..c372036de 100644 --- a/src/services/data_encryption.js +++ b/src/services/data_encryption.js @@ -30,14 +30,14 @@ function pad(data) { return Buffer.from(data); } -function encrypt(key, plainText, ivLength = 13) { +function encrypt(key, plainText) { if (!key) { throw new Error("No data key!"); } const plainTextBuffer = Buffer.from(plainText); - const iv = crypto.randomBytes(ivLength); + const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-128-cbc', pad(key), pad(iv)); const digest = shaArray(plainTextBuffer).slice(0, 4); @@ -51,7 +51,7 @@ function encrypt(key, plainText, ivLength = 13) { return encryptedDataWithIv.toString('base64'); } -function decrypt(key, cipherText, ivLength = 13) { +function decrypt(key, cipherText) { if (cipherText === null) { return null; } @@ -62,6 +62,10 @@ function decrypt(key, cipherText, ivLength = 13) { try { const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), 'base64'); + + // old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/trilium/issues/3017 + const ivLength = cipherTextBufferWithIv.length % 16 === 0 ? 16 : 13; + const iv = cipherTextBufferWithIv.slice(0, ivLength); const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength); From 698ffd886d15ab904da83c112369f8dbd408314a Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 28 Jul 2022 22:44:28 +0200 Subject: [PATCH 198/250] cleanup --- src/services/password_encryption.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/password_encryption.js b/src/services/password_encryption.js index 3c38d4e91..d41736b21 100644 --- a/src/services/password_encryption.js +++ b/src/services/password_encryption.js @@ -11,7 +11,7 @@ function verifyPassword(password) { if (!dbPasswordHash) { return false; } - + return givenPasswordHash === dbPasswordHash; } @@ -28,7 +28,7 @@ function getDataKey(password) { const encryptedDataKey = optionService.getOption('encryptedDataKey'); - const decryptedDataKey = dataEncryptionService.decrypt(passwordDerivedKey, encryptedDataKey, 16); + const decryptedDataKey = dataEncryptionService.decrypt(passwordDerivedKey, encryptedDataKey); return decryptedDataKey; } From 91bc9eec93fdd6fadb92646cd80489ddbcab598d Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 28 Jul 2022 22:44:55 +0200 Subject: [PATCH 199/250] cleanup --- src/services/password_encryption.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/password_encryption.js b/src/services/password_encryption.js index d41736b21..513e3af2c 100644 --- a/src/services/password_encryption.js +++ b/src/services/password_encryption.js @@ -18,7 +18,7 @@ function verifyPassword(password) { function setDataKey(password, plainTextDataKey) { const passwordDerivedKey = myScryptService.getPasswordDerivedKey(password); - const newEncryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, plainTextDataKey, 16); + const newEncryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, plainTextDataKey); optionService.setOption('encryptedDataKey', newEncryptedDataKey); } From ef6b7a85d569f7daa1515247b40c55d4f46ec495 Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 28 Jul 2022 23:59:41 +0200 Subject: [PATCH 200/250] don't display mermaid if the note is encrypted without protected session --- src/public/app/entities/note_short.js | 5 +++++ src/public/app/layouts/desktop_layout.js | 2 +- src/public/app/widgets/floating_buttons/floating_buttons.js | 4 ++++ src/public/app/widgets/mermaid.js | 4 +++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/public/app/entities/note_short.js b/src/public/app/entities/note_short.js index 138a388ab..345857758 100644 --- a/src/public/app/entities/note_short.js +++ b/src/public/app/entities/note_short.js @@ -3,6 +3,7 @@ import noteAttributeCache from "../services/note_attribute_cache.js"; import ws from "../services/ws.js"; import options from "../services/options.js"; import froca from "../services/froca.js"; +import protectedSessionHolder from "../services/protected_session_holder.js"; const LABEL = 'label'; const RELATION = 'relation'; @@ -812,6 +813,10 @@ class NoteShort { return false; } + + isContentAvailable() { + return !this.isProtected || protectedSessionHolder.isProtectedSessionAvailable() + } } export default NoteShort; diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index 211df0637..70af45140 100644 --- a/src/public/app/layouts/desktop_layout.js +++ b/src/public/app/layouts/desktop_layout.js @@ -180,8 +180,8 @@ export default class DesktopLayout { .child(new SharedInfoWidget()) .child(new NoteUpdateStatusWidget()) .child(new FloatingButtons() - .child(new BacklinksWidget()) .child(new RelationMapButtons()) + .child(new BacklinksWidget()) ) .child(new MermaidWidget()) .child( diff --git a/src/public/app/widgets/floating_buttons/floating_buttons.js b/src/public/app/widgets/floating_buttons/floating_buttons.js index 23e8c96f9..10454d27e 100644 --- a/src/public/app/widgets/floating_buttons/floating_buttons.js +++ b/src/public/app/widgets/floating_buttons/floating_buttons.js @@ -16,6 +16,10 @@ const TPL = ` z-index: 100; } + .floating-buttons-children > * { + margin-left: 10px; + } + .floating-buttons .floating-button { font-size: 130%; padding: 5px 10px 4px 10px; diff --git a/src/public/app/widgets/mermaid.js b/src/public/app/widgets/mermaid.js index 094fe1d3d..ae89edadf 100644 --- a/src/public/app/widgets/mermaid.js +++ b/src/public/app/widgets/mermaid.js @@ -33,7 +33,9 @@ let idCounter = 1; export default class MermaidWidget extends NoteContextAwareWidget { isEnabled() { - return super.isEnabled() && this.note && this.note.type === 'mermaid'; + return super.isEnabled() + && this.note?.type === 'mermaid' + && this.note.isContentAvailable(); } doRender() { From 6c43b92bf1f39926728044e7bfb5c04f3f05b9fb Mon Sep 17 00:00:00 2001 From: zadam Date: Fri, 29 Jul 2022 00:32:28 +0200 Subject: [PATCH 201/250] mermaid export button WIP --- src/public/app/layouts/desktop_layout.js | 2 + src/public/app/services/link.js | 11 ++-- .../floating_buttons/mermaid_export_button.js | 26 ++++++++ src/public/app/widgets/mermaid.js | 60 ++++++++++++++----- 4 files changed, 79 insertions(+), 20 deletions(-) create mode 100644 src/public/app/widgets/floating_buttons/mermaid_export_button.js diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index 70af45140..d36e15982 100644 --- a/src/public/app/layouts/desktop_layout.js +++ b/src/public/app/layouts/desktop_layout.js @@ -77,6 +77,7 @@ import PromptDialog from "../widgets/dialogs/prompt.js"; import OptionsDialog from "../widgets/dialogs/options.js"; import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js"; import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js"; +import MermaidExportButton from "../widgets/floating_buttons/mermaid_export_button.js"; export default class DesktopLayout { constructor(customWidgets) { @@ -181,6 +182,7 @@ export default class DesktopLayout { .child(new NoteUpdateStatusWidget()) .child(new FloatingButtons() .child(new RelationMapButtons()) + .child(new MermaidExportButton()) .child(new BacklinksWidget()) ) .child(new MermaidWidget()) diff --git a/src/public/app/services/link.js b/src/public/app/services/link.js index fb08b8ef1..93c7a128a 100644 --- a/src/public/app/services/link.js +++ b/src/public/app/services/link.js @@ -85,11 +85,16 @@ function getNotePathFromLink($link) { } function goToLink(e) { + const $link = $(e.target).closest("a,.block-link"); + const address = $link.attr('href'); + + if (address.startsWith("data:")) { + return true; + } + e.preventDefault(); e.stopPropagation(); - const $link = $(e.target).closest("a,.block-link"); - const notePath = getNotePathFromLink($link); if (notePath) { @@ -115,8 +120,6 @@ function goToLink(e) { || $link.hasClass("ck-link-actions__preview") // within edit link dialog single click suffices || $link.closest("[contenteditable]").length === 0 // outside of CKEditor single click suffices ) { - const address = $link.attr('href'); - if (address) { if (address.toLowerCase().startsWith('http')) { window.open(address, '_blank'); diff --git a/src/public/app/widgets/floating_buttons/mermaid_export_button.js b/src/public/app/widgets/floating_buttons/mermaid_export_button.js new file mode 100644 index 000000000..85556c929 --- /dev/null +++ b/src/public/app/widgets/floating_buttons/mermaid_export_button.js @@ -0,0 +1,26 @@ +import NoteContextAwareWidget from "../note_context_aware_widget.js"; +import dialogService from "../dialog.js"; +import server from "../../services/server.js"; +import toastService from "../../services/toast.js"; + +const TPL = ` + +`; + +export default class MermaidExportButton extends NoteContextAwareWidget { + isEnabled() { + return super.isEnabled() + && this.note?.type === 'mermaid' + && this.note.isContentAvailable(); + } + + doRender() { + super.doRender(); + + this.$widget = $(TPL); + this.$widget.on('click', () => this.triggerEvent('exportMermaid', {ntxId: this.ntxId})); + this.contentSized(); + } +} diff --git a/src/public/app/widgets/mermaid.js b/src/public/app/widgets/mermaid.js index ae89edadf..089fe4b25 100644 --- a/src/public/app/widgets/mermaid.js +++ b/src/public/app/widgets/mermaid.js @@ -68,29 +68,23 @@ export default class MermaidWidget extends NoteContextAwareWidget { git: { useMaxWidth: false }, }); - const noteComplement = await froca.getNoteComplement(note.noteId); - const content = noteComplement.content || ""; - this.$display.empty(); - const libLoaded = libraryLoader.requireLibrary(libraryLoader.WHEEL_ZOOM); + const wheelZoomLoaded = libraryLoader.requireLibrary(libraryLoader.WHEEL_ZOOM); try { - const idNumber = idCounter++; + const renderedSvg = await this.renderSvg(); + this.$display.html(renderedSvg); - mermaid.mermaidAPI.render('mermaid-graph-' + idNumber, content, async content => { - this.$display.html(content); + await wheelZoomLoaded; - await libLoaded; + this.$display.attr("id", 'mermaid-render-' + idCounter); - this.$display.attr("id", 'mermaid-render-' + idNumber); - - WZoom.create('#mermaid-render-' + idNumber, { - type: 'html', - maxScale: 10, - speed: 20, - zoomOnClick: false - }); + WZoom.create('#mermaid-render-' + idCounter, { + type: 'html', + maxScale: 10, + speed: 20, + zoomOnClick: false }); this.$errorContainer.hide(); @@ -100,9 +94,43 @@ export default class MermaidWidget extends NoteContextAwareWidget { } } + renderSvg() { + return new Promise(async res => { + idCounter++; + + const noteComplement = await froca.getNoteComplement(this.noteId); + const content = noteComplement.content || ""; + + mermaid.mermaidAPI.render('mermaid-graph-' + idCounter, content, res); + }); + } + async entitiesReloadedEvent({loadResults}) { if (loadResults.isNoteContentReloaded(this.noteId)) { await this.refresh(); } } + + async exportMermaidEvent({ntxId}) { + if (!this.isNoteContext(ntxId)) { + return; + } + + const renderedSvg = await this.renderSvg(); + + this.download(this.note.title + ".svg", renderedSvg); + } + + download(filename, text) { + var element = document.createElement('a'); + element.setAttribute('href', 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(text)); + element.setAttribute('download', filename); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); + } } From c727a2bc1b41c3c589b636cd8195e3b44c926024 Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 30 Jul 2022 14:06:25 +0200 Subject: [PATCH 202/250] fix error message on removing bulk actions from search, closes #3027 --- .../app/widgets/dialogs/bulk_actions.js | 24 ++++++++++++------- src/public/app/widgets/mermaid.js | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/public/app/widgets/dialogs/bulk_actions.js b/src/public/app/widgets/dialogs/bulk_actions.js index 67f563fda..5922a1bce 100644 --- a/src/public/app/widgets/dialogs/bulk_actions.js +++ b/src/public/app/widgets/dialogs/bulk_actions.js @@ -6,27 +6,27 @@ import server from "../../services/server.js"; import toastService from "../../services/toast.js"; const TPL = ` -