From fe53e2351c3f42df208ad7f4a8e947c927e7917d Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 2 May 2020 00:28:40 +0200 Subject: [PATCH] basic implementation of note tree's config --- package-lock.json | 8 +- package.json | 2 +- .../app/layouts/desktop_main_window_layout.js | 2 +- src/public/app/layouts/mobile_layout.js | 2 +- src/public/app/services/tree_builder.js | 188 ---------- src/public/app/widgets/note_tree.js | 349 +++++++++++++++++- src/routes/api/options.js | 4 +- src/services/options_init.js | 4 +- src/views/desktop.ejs | 2 +- 9 files changed, 343 insertions(+), 218 deletions(-) delete mode 100644 src/public/app/services/tree_builder.js diff --git a/package-lock.json b/package-lock.json index 69ee337f2..d1b5a9a5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "trilium", - "version": "0.41.5", + "version": "0.41.6", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2832,9 +2832,9 @@ "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=" }, "dayjs": { - "version": "1.8.25", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.25.tgz", - "integrity": "sha512-Pk36juDfQQGDCgr0Lqd1kw15w3OS6xt21JaLPE3lCfsEf8KrERGwDNwvK1tRjrjqFC0uZBJncT4smZQ4F+uV5g==" + "version": "1.8.26", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.26.tgz", + "integrity": "sha512-KqtAuIfdNfZR5sJY1Dixr2Is4ZvcCqhb0dZpCOt5dGEFiMzoIbjkTSzUb4QKTCsP+WNpGwUjAFIZrnZvUxxkhw==" }, "debug": { "version": "4.1.1", diff --git a/package.json b/package.json index 79338af86..4d96ee213 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "commonmark": "0.29.1", "cookie-parser": "1.4.5", "csurf": "1.11.0", - "dayjs": "1.8.25", + "dayjs": "1.8.26", "debug": "4.1.1", "ejs": "3.1.2", "electron-debug": "3.0.1", diff --git a/src/public/app/layouts/desktop_main_window_layout.js b/src/public/app/layouts/desktop_main_window_layout.js index 9330c2792..bf0e78b37 100644 --- a/src/public/app/layouts/desktop_main_window_layout.js +++ b/src/public/app/layouts/desktop_main_window_layout.js @@ -103,7 +103,7 @@ export default class DesktopMainWindowLayout { } getRootWidget(appContext) { - appContext.mainTreeWidget = new NoteTreeWidget(); + appContext.mainTreeWidget = new NoteTreeWidget("main"); return new FlexContainer('column') .setParent(appContext) diff --git a/src/public/app/layouts/mobile_layout.js b/src/public/app/layouts/mobile_layout.js index 84f7d7b73..a1eb75cb9 100644 --- a/src/public/app/layouts/mobile_layout.js +++ b/src/public/app/layouts/mobile_layout.js @@ -73,7 +73,7 @@ export default class MobileLayout { .child(new ScreenContainer("tree", 'column') .class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-4 col-xl-4") .child(new MobileGlobalButtonsWidget()) - .child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS))) + .child(new NoteTreeWidget("main").cssBlock(FANCYTREE_CSS))) .child(new ScreenContainer("detail", "column") .class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-8") .child(new FlexContainer('row') diff --git a/src/public/app/services/tree_builder.js b/src/public/app/services/tree_builder.js deleted file mode 100644 index d6eb09fc5..000000000 --- a/src/public/app/services/tree_builder.js +++ /dev/null @@ -1,188 +0,0 @@ -import utils from "./utils.js"; -import treeCache from "./tree_cache.js"; -import ws from "./ws.js"; -import hoistedNoteService from "./hoisted_note.js"; - -async function prepareRootNode() { - await treeCache.initializedPromise; - - const hoistedNoteId = hoistedNoteService.getHoistedNoteId(); - - let hoistedBranch; - - if (hoistedNoteId === 'root') { - hoistedBranch = treeCache.getBranch('root'); - } - else { - const hoistedNote = await treeCache.getNote(hoistedNoteId); - hoistedBranch = (await hoistedNote.getBranches())[0]; - } - - return await prepareNode(hoistedBranch); -} - -async function prepareChildren(note) { - if (note.type === 'search') { - return await prepareSearchNoteChildren(note); - } - else { - return await prepareNormalNoteChildren(note); - } -} - -const NOTE_TYPE_ICONS = { - "file": "bx bx-file", - "image": "bx bx-image", - "code": "bx bx-code", - "render": "bx bx-extension", - "search": "bx bx-file-find", - "relation-map": "bx bx-map-alt", - "book": "bx bx-book" -}; - -function getIconClass(note) { - const labels = note.getLabels('iconClass'); - - return labels.map(l => l.value).join(' '); -} - -function getIcon(note) { - const hoistedNoteId = hoistedNoteService.getHoistedNoteId(); - - const iconClass = getIconClass(note); - - if (iconClass) { - return iconClass; - } - else if (note.noteId === 'root') { - return "bx bx-chevrons-right"; - } - else if (note.noteId === hoistedNoteId) { - return "bx bxs-arrow-from-bottom"; - } - else if (note.type === 'text') { - if (note.hasChildren()) { - return "bx bx-folder"; - } - else { - return "bx bx-note"; - } - } - else { - return NOTE_TYPE_ICONS[note.type]; - } -} - -async function prepareNode(branch) { - const note = await branch.getNote(); - - if (!note) { - throw new Error(`Branch has no note ` + branch.noteId); - } - - const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title; - const hoistedNoteId = hoistedNoteService.getHoistedNoteId(); - - const node = { - noteId: note.noteId, - parentNoteId: branch.parentNoteId, - branchId: branch.branchId, - isProtected: note.isProtected, - noteType: note.type, - title: utils.escapeHtml(title), - extraClasses: getExtraClasses(note), - icon: getIcon(note), - refKey: note.noteId, - expanded: branch.isExpanded || hoistedNoteId === note.noteId, - key: utils.randomString(12) // this should prevent some "duplicate key" errors - }; - - const childBranches = getChildBranchesWithoutImages(note); - - node.folder = childBranches.length > 0 - || note.type === 'search' - - node.lazy = node.folder && !node.expanded; - - if (node.folder && node.expanded) { - node.children = await prepareChildren(note); - } - - return node; -} - -async function prepareNormalNoteChildren(parentNote) { - utils.assertArguments(parentNote); - - const noteList = []; - - for (const branch of getChildBranchesWithoutImages(parentNote)) { - const node = await prepareNode(branch); - - noteList.push(node); - } - - return noteList; -} - -function getChildBranchesWithoutImages(parentNote) { - const childBranches = parentNote.getChildBranches(); - - if (!childBranches) { - ws.logError(`No children for ${parentNote}. This shouldn't happen.`); - return; - } - - const imageLinks = parentNote.getRelations('imageLink'); - - // image is already visible in the parent note so no need to display it separately in the book - return childBranches.filter(branch => !imageLinks.find(rel => rel.value === branch.noteId)); -} - -async function prepareSearchNoteChildren(note) { - await treeCache.reloadNotes([note.noteId]); - - const newNote = await treeCache.getNote(note.noteId); - - return await prepareNormalNoteChildren(newNote); -} - -function getExtraClasses(note) { - utils.assertArguments(note); - - const extraClasses = []; - - if (note.isProtected) { - extraClasses.push("protected"); - } - - if (note.getParentNoteIds().length > 1) { - extraClasses.push("multiple-parents"); - } - - const cssClass = note.getCssClass(); - - if (cssClass) { - extraClasses.push(cssClass); - } - - extraClasses.push(utils.getNoteTypeClass(note.type)); - - if (note.mime) { // some notes should not have mime type (e.g. render) - extraClasses.push(utils.getMimeTypeClass(note.mime)); - } - - if (note.hasLabel('archived')) { - extraClasses.push("archived"); - } - - return extraClasses.join(" "); -} - -export default { - prepareRootNode, - prepareBranch: prepareChildren, - getExtraClasses, - getIcon, - getChildBranchesWithoutImages -} \ No newline at end of file diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js index 278ad98a4..a0b7ba235 100644 --- a/src/public/app/widgets/note_tree.js +++ b/src/public/app/widgets/note_tree.js @@ -3,7 +3,6 @@ import treeService from "../services/tree.js"; import utils from "../services/utils.js"; import contextMenu from "../services/context_menu.js"; import treeCache from "../services/tree_cache.js"; -import treeBuilder from "../services/tree_builder.js"; import branchService from "../services/branches.js"; import ws from "../services/ws.js"; import TabAwareWidget from "./tab_aware_widget.js"; @@ -15,17 +14,24 @@ import keyboardActionsService from "../services/keyboard_actions.js"; import clipboard from "../services/clipboard.js"; import protectedSessionService from "../services/protected_session.js"; import syncService from "../services/sync.js"; +import options from "../services/options.js"; const TPL = ` -
+
+ + + +
+
+ +
+
+ +
+ +
+ + +
+ +
`; +const NOTE_TYPE_ICONS = { + "file": "bx bx-file", + "image": "bx bx-image", + "code": "bx bx-code", + "render": "bx bx-extension", + "search": "bx bx-file-find", + "relation-map": "bx bx-map-alt", + "book": "bx bx-book" +}; + export default class NoteTreeWidget extends TabAwareWidget { + constructor(treeName) { + super(); + + this.treeName = treeName; + } + doRender() { this.$widget = $(TPL); + this.$tree = this.$widget.find('.tree'); - this.$widget.on("click", ".unhoist-button", hoistedNoteService.unhoist); - this.$widget.on("click", ".refresh-search-button", () => this.refreshSearch()); + this.$tree.on("click", ".unhoist-button", hoistedNoteService.unhoist); + this.$tree.on("click", ".refresh-search-button", () => this.refreshSearch()); // fancytree doesn't support middle click so this is a way to support it - this.$widget.on('mousedown', '.fancytree-title', e => { + this.$tree.on('mousedown', '.fancytree-title', e => { if (e.which === 2) { const node = $.ui.fancytree.getNode(e); @@ -67,20 +133,76 @@ export default class NoteTreeWidget extends TabAwareWidget { } }); + this.$treeSettingsPopup = this.$widget.find('.tree-settings-popup'); + this.$hideArchivedNotesCheckbox = this.$treeSettingsPopup.find('.hide-archived-notes'); + this.$hideIncludedImages = this.$treeSettingsPopup.find('.hide-included-images'); + + this.$treeSettingsButton = this.$widget.find('.tree-settings-button'); + this.$treeSettingsButton.on("click", e => { + if (this.$treeSettingsPopup.is(":visible")) { + this.$treeSettingsPopup.hide(); + return; + } + + this.$hideArchivedNotesCheckbox.prop("checked", this.hideArchivedNotes); + this.$hideIncludedImages.prop("checked", this.hideIncludedImages); + + let top = this.$treeSettingsButton[0].offsetTop; + let left = this.$treeSettingsButton[0].offsetLeft; + top += this.$treeSettingsButton.outerHeight(); + left += this.$treeSettingsButton.outerWidth() - this.$treeSettingsPopup.outerWidth(); + + if (left < 0) { + left = 0; + } + + this.$treeSettingsPopup.css({ + display: "block", + top: top, + left: left + }).addClass("show"); + }); + + this.$saveTreeSettingsButton = this.$treeSettingsPopup.find('.save-tree-settings-button'); + this.$saveTreeSettingsButton.on('click', async () => { + await this.setHideArchivedNotes(this.$hideArchivedNotesCheckbox.prop("checked")); + await this.setHideIncludedImages(this.$hideIncludedImages.prop("checked")); + + this.$treeSettingsPopup.hide(); + + this.reloadTreeFromCache(); + }); + this.initialized = this.initFancyTree(); return this.$widget; } - async initFancyTree() { - const treeData = [await treeBuilder.prepareRootNode()]; + get hideArchivedNotes() { + return options.is("hideArchivedNotes_" + this.treeName); + } - this.$widget.fancytree({ + async setHideArchivedNotes(val) { + await options.save("hideArchivedNotes_" + this.treeName, val.toString()); + } + + get hideIncludedImages() { + return options.is("hideIncludedImages_" + this.treeName); + } + + async setHideIncludedImages(val) { + await options.save("hideIncludedImages_" + this.treeName, val.toString()); + } + + async initFancyTree() { + const treeData = [await this.prepareRootNode()]; + + this.$tree.fancytree({ autoScroll: true, keyboard: false, // we takover keyboard handling in the hotkeys plugin extensions: utils.isMobile() ? ["dnd5", "clones"] : ["hotkeys", "dnd5", "clones"], source: treeData, - scrollParent: this.$widget, + scrollParent: this.$tree, minExpandLevel: 2, // root can't be collapsed click: (event, data) => { const targetType = data.targetType; @@ -191,10 +313,10 @@ export default class NoteTreeWidget extends TabAwareWidget { } } }, - lazyLoad: function(event, data) { + lazyLoad: (event, data) => { const noteId = data.node.data.noteId; - data.result = treeCache.getNote(noteId).then(note => treeBuilder.prepareBranch(note)); + data.result = treeCache.getNote(noteId).then(note => this.prepareChildren(note)); }, clones: { highlightActiveClones: true @@ -236,7 +358,7 @@ export default class NoteTreeWidget extends TabAwareWidget { } }); - this.$widget.on('contextmenu', '.fancytree-node', e => { + this.$tree.on('contextmenu', '.fancytree-node', e => { const node = $.ui.fancytree.getNode(e); import("../services/tree_context_menu.js").then(({default: TreeContextMenu}) => { @@ -247,7 +369,191 @@ export default class NoteTreeWidget extends TabAwareWidget { return false; // blocks default browser right click menu }); - this.tree = $.ui.fancytree.getTree(this.$widget); + this.tree = $.ui.fancytree.getTree(this.$tree); + } + + async prepareRootNode() { + await treeCache.initializedPromise; + + const hoistedNoteId = hoistedNoteService.getHoistedNoteId(); + + let hoistedBranch; + + if (hoistedNoteId === 'root') { + hoistedBranch = treeCache.getBranch('root'); + } + else { + const hoistedNote = await treeCache.getNote(hoistedNoteId); + hoistedBranch = (await hoistedNote.getBranches())[0]; + } + + return await this.prepareNode(hoistedBranch); + } + + async prepareChildren(note) { + if (note.type === 'search') { + return await this.prepareSearchNoteChildren(note); + } + else { + return await this.prepareNormalNoteChildren(note); + } + } + + getIconClass(note) { + const labels = note.getLabels('iconClass'); + + return labels.map(l => l.value).join(' '); + } + + getIcon(note) { + const hoistedNoteId = hoistedNoteService.getHoistedNoteId(); + + const iconClass = this.getIconClass(note); + + if (iconClass) { + return iconClass; + } + else if (note.noteId === 'root') { + return "bx bx-chevrons-right"; + } + else if (note.noteId === hoistedNoteId) { + return "bx bxs-arrow-from-bottom"; + } + else if (note.type === 'text') { + if (note.hasChildren()) { + return "bx bx-folder"; + } + else { + return "bx bx-note"; + } + } + else { + return NOTE_TYPE_ICONS[note.type]; + } + } + + async prepareNode(branch) { + const note = await branch.getNote(); + + if (!note) { + throw new Error(`Branch has no note ` + branch.noteId); + } + + const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title; + const hoistedNoteId = hoistedNoteService.getHoistedNoteId(); + + const node = { + noteId: note.noteId, + parentNoteId: branch.parentNoteId, + branchId: branch.branchId, + isProtected: note.isProtected, + noteType: note.type, + title: utils.escapeHtml(title), + extraClasses: this.getExtraClasses(note), + icon: this.getIcon(note), + refKey: note.noteId, + expanded: branch.isExpanded || hoistedNoteId === note.noteId, + key: utils.randomString(12) // this should prevent some "duplicate key" errors + }; + + const childBranches = await this.getChildBranches(note); + + node.folder = childBranches.length > 0 + || note.type === 'search' + + node.lazy = node.folder && !node.expanded; + + if (node.folder && node.expanded) { + node.children = await this.prepareChildren(note); + } + + return node; + } + + async prepareNormalNoteChildren(parentNote) { + utils.assertArguments(parentNote); + + const noteList = []; + + for (const branch of await this.getChildBranches(parentNote)) { + const node = await this.prepareNode(branch); + + noteList.push(node); + } + + return noteList; + } + + async getChildBranches(parentNote) { + let childBranches = parentNote.getChildBranches(); + + if (!childBranches) { + ws.logError(`No children for ${parentNote}. This shouldn't happen.`); + return; + } + + if (this.hideIncludedImages) { + const imageLinks = parentNote.getRelations('imageLink'); + + // image is already visible in the parent note so no need to display it separately in the book + childBranches = childBranches.filter(branch => !imageLinks.find(rel => rel.value === branch.noteId)); + } + + if (this.hideArchivedNotes) { + const filteredBranches = []; + + for (const childBranch of childBranches) { + const childNote = await childBranch.getNote(); + + if (!childNote.hasLabel('archived')) { + filteredBranches.push(childBranch); + } + } + + childBranches = filteredBranches; + } + + return childBranches; + } + + async prepareSearchNoteChildren(note) { + await treeCache.reloadNotes([note.noteId]); + + const newNote = await treeCache.getNote(note.noteId); + + return await this.prepareNormalNoteChildren(newNote); + } + + getExtraClasses(note) { + utils.assertArguments(note); + + const extraClasses = []; + + if (note.isProtected) { + extraClasses.push("protected"); + } + + if (note.getParentNoteIds().length > 1) { + extraClasses.push("multiple-parents"); + } + + const cssClass = note.getCssClass(); + + if (cssClass) { + extraClasses.push(cssClass); + } + + extraClasses.push(utils.getNoteTypeClass(note.type)); + + if (note.mime) { // some notes should not have mime type (e.g. render) + extraClasses.push(utils.getMimeTypeClass(note.mime)); + } + + if (note.hasLabel('archived')) { + extraClasses.push("archived"); + } + + return extraClasses.join(" "); } /** @return {FancytreeNode[]} */ @@ -374,6 +680,7 @@ export default class NoteTreeWidget extends TabAwareWidget { let foundChildNode = this.findChildNode(parentNode, childNoteId); if (!foundChildNode) { // note might be recently created so we'll force reload and try again + parentNode.lazy = true; await parentNode.load(true); foundChildNode = this.findChildNode(parentNode, childNoteId); @@ -410,16 +717,16 @@ export default class NoteTreeWidget extends TabAwareWidget { return this.getNodeFromPath(notePath, true, expandOpts); } - updateNode(node) { + async updateNode(node) { const note = treeCache.getNoteFromCache(node.data.noteId); const branch = treeCache.getBranch(node.data.branchId); node.data.isProtected = note.isProtected; node.data.noteType = note.type; - node.folder = treeBuilder.getChildBranchesWithoutImages(note).length > 0 + node.folder = (await this.getChildBranches(note)).length > 0 || note.type === 'search'; - node.icon = treeBuilder.getIcon(note); - node.extraClasses = treeBuilder.getExtraClasses(note); + node.icon = this.getIcon(note); + node.extraClasses = this.getExtraClasses(note); node.title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title; node.renderTitle(); } @@ -476,6 +783,7 @@ export default class NoteTreeWidget extends TabAwareWidget { async refreshSearch() { const activeNode = this.getActiveNode(); + activeNode.lazy = true; activeNode.load(true); activeNode.setExpanded(true); @@ -569,6 +877,7 @@ export default class NoteTreeWidget extends TabAwareWidget { for (const noteId of noteIdsToReload) { for (const node of this.getNodesByNoteId(noteId)) { + node.lazy = true; await node.load(true); this.updateNode(node); @@ -631,7 +940,7 @@ export default class NoteTreeWidget extends TabAwareWidget { const activeNotePath = activeNode !== null ? treeService.getNotePath(activeNode) : null; - const rootNode = await treeBuilder.prepareRootNode(); + const rootNode = await this.prepareRootNode(); await this.batchUpdate(async () => { await this.tree.reload([rootNode]); diff --git a/src/routes/api/options.js b/src/routes/api/options.js index 33ea40052..fbb94f58e 100644 --- a/src/routes/api/options.js +++ b/src/routes/api/options.js @@ -110,7 +110,9 @@ async function getUserThemes() { function isAllowed(name) { return ALLOWED_OPTIONS.has(name) || name.startsWith("keyboardShortcuts") - || name.endsWith("Collapsed"); + || name.endsWith("Collapsed") + || name.startsWith("hideArchivedNotes") + || name.startsWith("hideIncludedImages"); } module.exports = { diff --git a/src/services/options_init.js b/src/services/options_init.js index 4f1ab32fa..43d8060d9 100644 --- a/src/services/options_init.js +++ b/src/services/options_init.js @@ -82,7 +82,9 @@ const defaultOptions = [ { name: 'rightPaneWidth', value: '25', isSynced: false }, { name: 'rightPaneVisible', value: 'true', isSynced: false }, { name: 'nativeTitleBarVisible', value: 'false', isSynced: false }, - { name: 'eraseNotesAfterTimeInSeconds', value: '604800', isSynced: true } // default is 7 days + { name: 'eraseNotesAfterTimeInSeconds', value: '604800', isSynced: true }, // default is 7 days + { name: 'hideArchivedNotes_main', value: 'false', isSynced: false }, // default is 7 days + { name: 'hideIncludedImages_main', value: 'true', isSynced: false } // default is 7 days ]; async function initStartupOptions() { diff --git a/src/views/desktop.ejs b/src/views/desktop.ejs index e56e4ad0d..dd387edf2 100644 --- a/src/views/desktop.ejs +++ b/src/views/desktop.ejs @@ -64,7 +64,7 @@ - +