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 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"; const TPL = `

`; const MAX_SEARCH_RESULTS_IN_TREE = 100; // this has to be hanged on the actual elements to effectively intercept and stop click event const cancelClickPropagation = e => e.stopPropagation(); export default class NoteTreeWidget extends NoteContextAwareWidget { constructor() { super(); this.treeName = "main"; // legacy value } 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.$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.$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.setAutoCollapseNoteTree(this.$autoCollapseNoteTree.prop("checked")); this.$treeSettingsPopup.hide(); this.reloadTreeFromCache(); }); // note tree starts initializing already during render which is atypical Promise.all([options.initializedPromise, 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 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, {node}) => { // hidden subtree is hidden hackily - we want it to be present in the tree so that we can switch to it // without reloading the whole tree, but we want it to be hidden when hoisted to root. FancyTree allows // filtering the display only by ascendant - i.e. if the root is visible, all the descendants are as well. // We solve it by hiding the hidden subtree via CSS (class "hidden-node-is-hidden"), // but then we need to prevent activating it, e.g. by keyboard if (hoistedNoteService.getHoistedNoteId() === '_hidden') { // if we're hoisted in hidden subtree, we want to avoid crossing to "visible" tree, // which could happen via UP key from hidden root return node.data.noteId !== 'root'; } // we're not hoisted to hidden subtree, the only way to cross is via DOWN key to the hidden root return 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); }, 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.createLink(notes[0].noteId, {referenceLink: true, autoConvertToImage: true}) .then($link => data.dataTransfer.setData("text/html", $link[0].outerHTML)); } else { Promise.all(notes.map(note => linkService.createLink(note.noteId, {referenceLink: true, autoConvertToImage: true}))).then(links => { const $list = $("