import hoistedNoteService from "../services/hoisted_note.js"; import treeService from "../services/tree.js"; import utils from "../services/utils.js"; import contextMenu from "../menus/context_menu.js"; import froca from "../services/froca.js"; import branchService from "../services/branches.js"; import ws from "../services/ws.js"; import NoteContextAwareWidget from "./note_context_aware_widget.js"; import server from "../services/server.js"; import noteCreateService from "../services/note_create.js"; import toastService from "../services/toast.js"; import appContext from "../components/app_context.js"; import keyboardActionsService from "../services/keyboard_actions.js"; import clipboard from "../services/clipboard.js"; import protectedSessionService from "../services/protected_session.js"; import linkService from "../services/link.js"; import syncService from "../services/sync.js"; import options from "../services/options.js"; import protectedSessionHolder from "../services/protected_session_holder.js"; import dialogService from "../services/dialog.js"; import shortcutService from "../services/shortcuts.js"; import LauncherContextMenu from "../menus/launcher_context_menu.js"; const TPL = `

`; const MAX_SEARCH_RESULTS_IN_TREE = 100; export default class NoteTreeWidget extends NoteContextAwareWidget { constructor(treeName) { super(); this.treeName = treeName; } doRender() { this.$widget = $(TPL); this.$tree = this.$widget.find('.tree'); this.$treeActions = this.$widget.find(".tree-actions"); this.$tree.on("mousedown", ".unhoist-button", () => hoistedNoteService.unhoist()); this.$tree.on("mousedown", ".refresh-search-button", e => this.refreshSearch(e)); this.$tree.on("mousedown", ".add-note-button", e => { const node = $.ui.fancytree.getNode(e); const parentNotePath = treeService.getNotePath(node); noteCreateService.createNote(parentNotePath, { isProtected: node.data.isProtected }); }); this.$tree.on("mousedown", ".enter-workspace-button", e => { const node = $.ui.fancytree.getNode(e); this.triggerCommand('hoistNote', {noteId: node.data.noteId}); }); // fancytree doesn't support middle click so this is a way to support it this.$tree.on('mousedown', '.fancytree-title', e => { if (e.which === 2) { const node = $.ui.fancytree.getNode(e); const notePath = treeService.getNotePath(node); if (notePath) { appContext.tabManager.openTabWithNoteWithHoisting(notePath); } e.stopPropagation(); e.preventDefault(); } }); 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.$autoCollapseNoteTree = this.$treeSettingsPopup.find('.auto-collapse-note-tree'); 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); this.$autoCollapseNoteTree.prop("checked", this.autoCollapseNoteTree); const top = this.$treeActions[0].offsetTop - (this.$treeSettingsPopup.outerHeight()); const left = Math.max( 0, this.$treeActions[0].offsetLeft - this.$treeSettingsPopup.outerWidth() + this.$treeActions.outerWidth() ); this.$treeSettingsPopup.css({ top, left }).show(); return false; }); this.$treeSettingsPopup.on("click", e => { e.stopPropagation(); }); $(document).on('click', () => this.$treeSettingsPopup.hide()); 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")); await this.setAutoCollapseNoteTree(this.$autoCollapseNoteTree.prop("checked")); this.$treeSettingsPopup.hide(); this.reloadTreeFromCache(); }); froca.initializedPromise.then(() => this.initFancyTree()); this.setupNoteTitleTooltip(); } setupNoteTitleTooltip() { // the following will dynamically set tree item's tooltip if the whole item's text is not currently visible // if the whole text is visible then no tooltip is show since that's unnecessarily distracting // see https://github.com/zadam/trilium/pull/1120 for discussion // code inspired by https://gist.github.com/jtsternberg/c272d7de5b967cec2d3d const isEnclosing = ($container, $sub) => { const conOffset = $container.offset(); const conDistanceFromTop = conOffset.top + $container.outerHeight(true); const conDistanceFromLeft = conOffset.left + $container.outerWidth(true); const subOffset = $sub.offset(); const subDistanceFromTop = subOffset.top + $sub.outerHeight(true); const subDistanceFromLeft = subOffset.left + $sub.outerWidth(true); return conDistanceFromTop > subDistanceFromTop && conOffset.top < subOffset.top && conDistanceFromLeft > subDistanceFromLeft && conOffset.left < subOffset.left; }; this.$tree.on("mouseenter", "span.fancytree-title", e => { e.currentTarget.title = isEnclosing(this.$tree, $(e.currentTarget)) ? "" : e.currentTarget.innerText; }); } get hideArchivedNotes() { return options.is("hideArchivedNotes_" + this.treeName); } 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()); } get autoCollapseNoteTree() { return options.is("autoCollapseNoteTree"); } async setAutoCollapseNoteTree(val) { await options.save("autoCollapseNoteTree", val.toString()); } initFancyTree() { const treeData = [this.prepareRootNode()]; this.$tree.fancytree({ titlesTabbable: true, keyboard: true, extensions: ["dnd5", "clones", "filter"], source: treeData, scrollOfs: { top: 100, bottom: 100 }, scrollParent: this.$tree, minExpandLevel: 2, // root can't be collapsed click: (event, data) => { this.activityDetected(); const targetType = data.targetType; const node = data.node; if (node.isSelected() && targetType === 'icon') { this.triggerCommand('openBulkActionsDialog', { selectedOrActiveNoteIds: this.getSelectedOrActiveNoteIds(node) }); return false; } else if (targetType === 'title' || targetType === 'icon') { if (event.shiftKey) { const activeNode = this.getActiveNode(); if (activeNode.getParent() !== node.getParent()) { return; } this.clearSelectedNodes(); function selectInBetween(first, second) { for (let i = 0; first && first !== second && i < 10000; i++) { first.setSelected(true); first = first.getNextSibling(); } second.setSelected(); } if (activeNode.getIndex() < node.getIndex()) { selectInBetween(activeNode, node); } else { selectInBetween(node, activeNode); } node.setFocus(true); } else if ((!utils.isMac() && event.ctrlKey) || (utils.isMac() && event.metaKey)) { const notePath = treeService.getNotePath(node); appContext.tabManager.openTabWithNoteWithHoisting(notePath); } else if (event.altKey) { node.setSelected(!node.isSelected()); node.setFocus(true); } else if (data.node.isActive()) { // this is important for single column mobile view, otherwise it's not possible to see again previously displayed note this.tree.reactivate(true); } else { node.setActive(); } return false; } }, beforeActivate: (event, data) => { // hidden subtree is hidden hackily, prevent activating it e.g. by keyboard return hoistedNoteService.getHoistedNoteId() === 'hidden' || data.node.data.noteId !== 'hidden'; }, activate: async (event, data) => { // click event won't propagate so let's close context menu manually contextMenu.hide(); this.clearSelectedNodes(); const notePath = treeService.getNotePath(data.node); const activeNoteContext = appContext.tabManager.getActiveContext(); await activeNoteContext.setNote(notePath); if (utils.isMobile()) { this.triggerCommand('setActiveScreen', {screen: 'detail'}); } }, expand: (event, data) => this.setExpanded(data.node.data.branchId, true), collapse: (event, data) => this.setExpanded(data.node.data.branchId, false), filter: { counter: false, mode: "hide", autoExpand: true }, dnd5: { autoExpandMS: 600, preventLazyParents: false, dragStart: (node, data) => { if (['root', 'hidden', 'lbRoot', 'lbAvailableLaunchers', 'lbVisibleLaunchers'].includes(node.data.noteId) || node.data.noteId.startsWith("options")) { return false; } const notes = this.getSelectedOrActiveNodes(node).map(node => ({ noteId: node.data.noteId, branchId: node.data.branchId, title: node.title })); if (notes.length === 1) { linkService.createNoteLink(notes[0].noteId, {referenceLink: true}) .then($link => data.dataTransfer.setData("text/html", $link[0].outerHTML)); } else { Promise.all(notes.map(note => linkService.createNoteLink(note.noteId, {referenceLink: true}))).then(links => { const $list = $("