trilium/src/public/javascripts/widgets/note_tree.js

625 lines
21 KiB
JavaScript
Raw Normal View History

import hoistedNoteService from "../services/hoisted_note.js";
import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import contextMenuWidget from "../services/context_menu.js";
import treeKeyBindingService from "../services/tree_keybindings.js";
import treeCache from "../services/tree_cache.js";
import treeBuilder from "../services/tree_builder.js";
import TreeContextMenu from "../services/tree_context_menu.js";
2020-01-12 17:35:33 +08:00
import treeChangesService from "../services/branches.js";
2020-01-12 18:15:23 +08:00
import ws from "../services/ws.js";
2020-01-19 01:01:16 +08:00
import TabAwareWidget from "./tab_aware_widget.js";
2020-01-24 22:44:24 +08:00
import server from "../services/server.js";
import noteCreateService from "../services/note_create.js";
2020-02-15 03:18:09 +08:00
import toastService from "../services/toast.js";
const TPL = `
2020-01-13 03:15:05 +08:00
<div class="tree">
<style>
.tree {
overflow: auto;
flex-grow: 1;
flex-shrink: 1;
flex-basis: 60%;
font-family: var(--tree-font-family);
font-size: var(--tree-font-size);
}
</style>
</div>
`;
2020-01-19 01:01:16 +08:00
export default class NoteTreeWidget extends TabAwareWidget {
2020-02-15 16:43:47 +08:00
constructor(appContext, parent) {
super(appContext, parent);
2020-01-23 03:48:56 +08:00
window.glob.cutIntoNote = () => this.cutIntoNoteListener();
this.tree = null;
}
2020-01-13 03:15:05 +08:00
doRender() {
this.$widget = $(TPL);
this.$widget.on("click", ".unhoist-button", hoistedNoteService.unhoist);
2020-02-15 03:18:09 +08:00
this.$widget.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 => {
if (e.which === 2) {
const node = $.ui.fancytree.getNode(e);
2020-02-11 03:57:56 +08:00
const notePath = treeService.getNotePath(node);
if (notePath) {
const tabContext = this.tabManager.openEmptyTab();
tabContext.setNote(notePath);
}
e.stopPropagation();
e.preventDefault();
}
});
2020-01-13 03:15:05 +08:00
this.initialized = treeBuilder.prepareTree().then(treeData => this.initFancyTree(treeData));
2020-01-13 03:15:05 +08:00
return this.$widget;
}
async initFancyTree(treeData) {
utils.assertArguments(treeData);
this.$widget.fancytree({
autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "dnd5", "clones"],
source: treeData,
scrollParent: this.$widget,
minExpandLevel: 2, // root can't be collapsed
click: (event, data) => {
const targetType = data.targetType;
const node = data.node;
if (targetType === 'title' || targetType === 'icon') {
if (event.shiftKey) {
node.setSelected(!node.isSelected());
node.setFocus(true);
}
else if (event.ctrlKey) {
2020-02-10 04:13:05 +08:00
const tabContext = this.tabManager.openEmptyTab();
2020-02-11 03:57:56 +08:00
const notePath = treeService.getNotePath(node);
tabContext.setNote(notePath);
2020-02-10 04:13:05 +08:00
this.tabManager.activateTab(tabContext.tabId);
}
else {
node.setActive();
2020-01-12 18:15:23 +08:00
this.clearSelectedNodes();
}
return false;
}
},
activate: async (event, data) => {
// click event won't propagate so let's close context menu manually
contextMenuWidget.hideContextMenu();
2020-02-11 03:57:56 +08:00
const notePath = treeService.getNotePath(data.node);
2020-02-08 04:43:02 +08:00
const activeTabContext = this.tabManager.getActiveTabContext();
2020-01-25 04:15:40 +08:00
await activeTabContext.setNote(notePath);
},
2020-01-24 22:44:24 +08:00
expand: (event, data) => this.setExpandedToServer(data.node.data.branchId, true),
collapse: (event, data) => this.setExpandedToServer(data.node.data.branchId, false),
hotkeys: {
2020-01-12 17:35:33 +08:00
keydown: await treeKeyBindingService.getKeyboardBindings(this)
},
dnd5: {
autoExpandMS: 600,
dragStart: (node, data) => {
// don't allow dragging root node
if (node.data.noteId === hoistedNoteService.getHoistedNoteId()
2020-01-12 17:35:33 +08:00
|| node.getParent().data.noteType === 'search') {
return false;
}
node.setSelected(true);
const notes = this.getSelectedNodes().map(node => { return {
noteId: node.data.noteId,
title: node.title
}});
data.dataTransfer.setData("text", JSON.stringify(notes));
// This function MUST be defined to enable dragging for the tree.
// Return false to cancel dragging of node.
return true;
},
dragEnter: (node, data) => true, // allow drop on any node
dragOver: (node, data) => true,
dragDrop: async (node, data) => {
if ((data.hitMode === 'over' && node.data.noteType === 'search') ||
(['after', 'before'].includes(data.hitMode)
&& (node.data.noteId === hoistedNoteService.getHoistedNoteId() || node.getParent().data.noteType === 'search'))) {
2020-01-12 17:35:33 +08:00
const infoDialog = await import('../dialogs/info.js');
await infoDialog.info("Dropping notes into this location is not allowed.");
return;
}
const dataTransfer = data.dataTransfer;
if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) {
const files = [...dataTransfer.files]; // chrome has issue that dataTransfer.files empties after async operation
2020-01-12 19:30:30 +08:00
const importService = await import('../services/import.js');
2020-01-12 17:35:33 +08:00
importService.uploadFiles(node.data.noteId, files, {
safeImport: true,
shrinkImages: true,
textImportedAsText: true,
codeImportedAsCode: true,
explodeArchives: true
});
}
else {
// This function MUST be defined to enable dropping of items on the tree.
// data.hitMode is 'before', 'after', or 'over'.
const selectedBranchIds = this.getSelectedNodes().map(node => node.data.branchId);
if (data.hitMode === "before") {
2020-01-26 18:41:40 +08:00
treeChangesService.moveBeforeBranch(selectedBranchIds, node.data.branchId);
2020-01-12 17:35:33 +08:00
} else if (data.hitMode === "after") {
2020-01-26 18:41:40 +08:00
treeChangesService.moveAfterBranch(selectedBranchIds, node.data.branchId);
2020-01-12 17:35:33 +08:00
} else if (data.hitMode === "over") {
2020-01-26 18:41:40 +08:00
treeChangesService.moveToParentNote(selectedBranchIds, node.data.noteId);
2020-01-12 17:35:33 +08:00
} else {
throw new Error("Unknown hitMode=" + data.hitMode);
}
}
}
},
lazyLoad: function(event, data) {
const noteId = data.node.data.noteId;
data.result = treeCache.getNote(noteId).then(note => treeBuilder.prepareBranch(note));
},
clones: {
highlightActiveClones: true
},
enhanceTitle: async function (event, data) {
const node = data.node;
const $span = $(node.span);
if (node.data.noteId !== 'root'
2020-02-11 03:57:56 +08:00
&& node.data.noteId === hoistedNoteService.getHoistedNoteId()
&& $span.find('.unhoist-button').length === 0) {
const unhoistButton = $('<span>&nbsp; (<a class="unhoist-button">unhoist</a>)</span>');
$span.append(unhoistButton);
}
const note = await treeCache.getNote(node.data.noteId);
if (note.type === 'search' && $span.find('.refresh-search-button').length === 0) {
2020-02-15 16:16:23 +08:00
const refreshSearchButton = $('<span>&nbsp; <span class="refresh-search-button bx bx-refresh" title="Refresh saved search results"></span></span>');
$span.append(refreshSearchButton);
}
},
// this is done to automatically lazy load all expanded search notes after tree load
loadChildren: (event, data) => {
data.node.visit((subNode) => {
// Load all lazy/unloaded child nodes
// (which will trigger `loadChildren` recursively)
if (subNode.isUndefined() && subNode.isExpanded()) {
subNode.load();
}
});
}
});
this.$widget.on('contextmenu', '.fancytree-node', e => {
const node = $.ui.fancytree.getNode(e);
2020-01-12 16:57:28 +08:00
contextMenuWidget.initContextMenu(e, new TreeContextMenu(this, node));
return false; // blocks default browser right click menu
});
this.tree = $.ui.fancytree.getTree(this.$widget);
}
/** @return {FancytreeNode[]} */
getSelectedNodes(stopOnParents = false) {
return this.tree.getSelectedNodes(stopOnParents);
}
/** @return {FancytreeNode[]} */
2020-01-12 18:15:23 +08:00
getSelectedOrActiveNodes(node = null) {
let notes = this.getSelectedNodes(true);
if (notes.length === 0) {
2020-01-12 18:15:23 +08:00
notes.push(node ? node : this.getActiveNode());
}
return notes;
}
2020-02-11 03:57:56 +08:00
collapseTree(node = null) {
if (!node) {
2020-02-11 03:57:56 +08:00
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
node = this.getNodesByNoteId(hoistedNoteId)[0];
}
node.setExpanded(false);
node.visit(node => node.setExpanded(false));
}
2020-01-12 18:15:23 +08:00
/**
* @return {FancytreeNode|null}
*/
getActiveNode() {
return this.tree.getActiveNode();
}
2020-01-12 17:35:33 +08:00
/**
* focused & not active node can happen during multiselection where the node is selected but not activated
* (its content is not displayed in the detail)
* @return {FancytreeNode|null}
*/
getFocusedNode() {
return this.tree.getFocusNode();
}
clearSelectedNodes() {
for (const selectedNode of this.getSelectedNodes()) {
selectedNode.setSelected(false);
}
}
async scrollToActiveNoteListener() {
2020-02-10 04:13:05 +08:00
const activeContext = this.tabManager.getActiveTabContext();
2020-01-12 18:15:23 +08:00
if (activeContext && activeContext.notePath) {
this.tree.setFocus();
const node = await this.expandToNote(activeContext.notePath);
await node.makeVisible({scrollIntoView: true});
node.setFocus();
}
}
/** @return {FancytreeNode} */
async getNodeFromPath(notePath, expand = false, expandOpts = {}) {
utils.assertArguments(notePath);
2020-02-11 03:57:56 +08:00
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
2020-01-12 18:15:23 +08:00
/** @var {FancytreeNode} */
let parentNode = null;
const runPath = await treeService.getRunPath(notePath);
if (!runPath) {
console.error("Could not find run path for notePath:", notePath);
return;
}
for (const childNoteId of runPath) {
if (childNoteId === hoistedNoteId) {
// there must be exactly one node with given hoistedNoteId
parentNode = this.getNodesByNoteId(childNoteId)[0];
continue;
}
// we expand only after hoisted note since before then nodes are not actually present in the tree
if (parentNode) {
if (!parentNode.isLoaded()) {
await parentNode.load();
}
if (expand) {
await parentNode.setExpanded(true, expandOpts);
}
2020-01-30 04:38:58 +08:00
await this.updateNode(parentNode);
2020-01-12 18:15:23 +08:00
let foundChildNode = this.findChildNode(parentNode, childNoteId);
if (!foundChildNode) { // note might be recently created so we'll force reload and try again
await parentNode.load(true);
foundChildNode = this.findChildNode(parentNode, childNoteId);
if (!foundChildNode) {
ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteId}, requested path is ${notePath}`);
return;
}
}
parentNode = foundChildNode;
}
}
return parentNode;
}
/** @return {FancytreeNode} */
findChildNode(parentNode, childNoteId) {
let foundChildNode = null;
for (const childNode of parentNode.getChildren()) {
if (childNode.data.noteId === childNoteId) {
foundChildNode = childNode;
break;
}
}
return foundChildNode;
}
/** @return {FancytreeNode} */
async expandToNote(notePath, expandOpts) {
return this.getNodeFromPath(notePath, true, expandOpts);
}
2020-01-30 04:38:58 +08:00
async updateNode(node) {
const note = treeCache.getNoteFromCache(node.data.noteId);
2020-01-30 04:38:58 +08:00
const branch = treeCache.getBranch(node.data.branchId);
2020-01-12 18:15:23 +08:00
2020-01-30 04:38:58 +08:00
node.data.isProtected = note.isProtected;
node.data.noteType = note.type;
2020-01-12 18:15:23 +08:00
node.folder = note.type === 'search' || note.getChildNoteIds().length > 0;
node.icon = await treeBuilder.getIcon(note);
node.extraClasses = await treeBuilder.getExtraClasses(note);
2020-01-30 04:38:58 +08:00
node.title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
2020-01-12 18:15:23 +08:00
node.renderTitle();
}
/** @return {FancytreeNode[]} */
2020-02-03 05:32:44 +08:00
getNodesByBranchId(branchId) {
2020-01-12 18:15:23 +08:00
utils.assertArguments(branchId);
const branch = treeCache.getBranch(branchId);
return this.getNodesByNoteId(branch.noteId).filter(node => node.data.branchId === branchId);
}
/** @return {FancytreeNode[]} */
getNodesByNoteId(noteId) {
utils.assertArguments(noteId);
const list = this.tree.getNodesByRef(noteId);
return list ? list : []; // if no nodes with this refKey are found, fancy tree returns null
}
async reload(notes) {
await this.tree.reload(notes);
}
2020-02-10 04:53:10 +08:00
createTopLevelNoteListener() { noteCreateService.createNewTopLevelNote(); }
collapseTreeListener() { this.collapseTree(); }
2020-01-12 19:30:30 +08:00
2020-02-10 04:13:05 +08:00
isEnabled() {
return this.tabContext && this.tabContext.isActive();
}
async refresh() {
this.toggle(this.isEnabled());
2020-01-19 01:01:16 +08:00
const oldActiveNode = this.getActiveNode();
if (oldActiveNode) {
oldActiveNode.setActive(false);
2020-01-20 04:12:53 +08:00
oldActiveNode.setFocus(false);
2020-01-19 01:01:16 +08:00
}
if (this.tabContext && this.tabContext.notePath) {
const newActiveNode = await this.getNodeFromPath(this.tabContext.notePath);
if (newActiveNode) {
if (!newActiveNode.isVisible()) {
await this.expandToNote(this.tabContext.notePath);
}
newActiveNode.setActive(true, {noEvents: true});
2020-02-13 05:25:52 +08:00
newActiveNode.makeVisible({scrollIntoView: true});
2020-01-19 01:01:16 +08:00
}
}
}
2020-02-15 03:18:09 +08:00
async refreshSearch() {
const activeNode = this.getActiveNode();
activeNode.load(true);
activeNode.setExpanded(true);
toastService.showMessage("Saved search note refreshed.");
}
2020-01-30 03:14:02 +08:00
async entitiesReloadedListener({loadResults}) {
2020-01-30 04:38:58 +08:00
const noteIdsToUpdate = new Set();
const noteIdsToReload = new Set();
2020-01-26 18:41:40 +08:00
2020-01-30 04:38:58 +08:00
for (const attr of loadResults.getAttributes()) {
if (attr.type === 'label' && ['iconClass', 'cssClass'].includes(attr.name)) {
if (attr.isInheritable) {
noteIdsToReload.add(attr.noteId);
}
else {
noteIdsToUpdate.add(attr.noteId);
}
}
else if (attr.type === 'relation' && attr.name === 'template') {
// missing handling of things inherited from template
noteIdsToReload.add(attr.noteId);
}
}
for (const branch of loadResults.getBranches()) {
for (const node of this.getNodesByBranchId(branch.branchId)) {
if (branch.isDeleted) {
2020-02-11 03:57:56 +08:00
if (node.isActive()) {
2020-02-13 05:25:52 +08:00
const newActiveNode = node.getNextSibling()
|| node.getPrevSibling()
|| node.getParent();
2020-01-30 04:38:58 +08:00
2020-02-13 05:25:52 +08:00
if (newActiveNode) {
newActiveNode.setActive(true, {noEvents: true, noFocus: true});
}
2020-01-30 04:38:58 +08:00
}
2020-01-12 19:30:30 +08:00
2020-02-13 05:25:52 +08:00
node.remove();
noteIdsToUpdate.add(branch.parentNoteId);
2020-01-12 19:30:30 +08:00
}
else {
2020-01-30 04:38:58 +08:00
noteIdsToUpdate.add(branch.noteId);
}
}
if (!branch.isDeleted) {
for (const parentNode of this.getNodesByNoteId(branch.parentNoteId)) {
if (!parentNode.isLoaded()) {
continue;
}
const found = parentNode.getChildren().find(child => child.data.noteId === branch.noteId);
2020-01-12 19:30:30 +08:00
2020-01-30 04:38:58 +08:00
if (!found) {
noteIdsToReload.add(branch.parentNoteId);
}
}
}
}
2020-02-11 03:57:56 +08:00
const activeNode = this.getActiveNode();
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
2020-02-03 05:32:44 +08:00
for (const noteId of loadResults.getNoteIds()) {
2020-02-03 05:33:50 +08:00
noteIdsToUpdate.add(noteId);
2020-02-03 05:32:44 +08:00
}
2020-01-30 04:38:58 +08:00
for (const noteId of noteIdsToReload) {
for (const node of this.getNodesByNoteId(noteId)) {
await node.load(true);
await this.updateNode(node);
}
}
2020-02-03 05:32:44 +08:00
for (const noteId of noteIdsToUpdate) {
2020-01-30 04:38:58 +08:00
for (const node of this.getNodesByNoteId(noteId)) {
await this.updateNode(node);
}
}
for (const {parentNoteId} of loadResults.getNoteReorderings()) {
for (const node of this.getNodesByNoteId(parentNoteId)) {
if (node.isLoaded()) {
node.sortChildren((nodeA, nodeB) => {
const branchA = treeCache.branches[nodeA.data.branchId];
const branchB = treeCache.branches[nodeB.data.branchId];
if (!branchA || !branchB) {
return 0;
}
return branchA.notePosition - branchB.notePosition;
});
2020-01-12 19:30:30 +08:00
}
}
}
2020-02-11 03:57:56 +08:00
if (activeNotePath) {
this.tabManager.getActiveTabContext().setNote(activeNotePath);
2020-01-12 19:30:30 +08:00
}
}
2020-01-23 03:48:56 +08:00
async createNoteAfterListener() {
const node = this.getActiveNode();
const parentNoteId = node.data.parentNoteId;
2020-01-25 16:56:08 +08:00
const isProtected = await treeService.getParentProtectedStatus(node);
2020-01-23 03:48:56 +08:00
2020-02-11 03:57:56 +08:00
if (node.data.noteId === 'root' || node.data.noteId === hoistedNoteService.getHoistedNoteId()) {
2020-01-23 03:48:56 +08:00
return;
}
await noteCreateService.createNote(parentNoteId, {
target: 'after',
targetBranchId: node.data.branchId,
2020-01-23 03:48:56 +08:00
isProtected: isProtected,
saveSelection: true
});
}
async createNoteIntoListener() {
const node = this.getActiveNode();
if (node) {
await noteCreateService.createNote(node.data.noteId, {
2020-01-23 03:48:56 +08:00
isProtected: node.data.isProtected,
saveSelection: false
});
}
}
async cutIntoNoteListener() {
const node = this.getActiveNode();
if (node) {
await noteCreateService.createNote(node.data.noteId, {
2020-01-23 03:48:56 +08:00
isProtected: node.data.isProtected,
saveSelection: true
});
}
}
2020-01-24 22:44:24 +08:00
async setExpandedToServer(branchId, isExpanded) {
utils.assertArguments(branchId);
const expandedNum = isExpanded ? 1 : 0;
await server.put('branches/' + branchId + '/expanded/' + expandedNum);
}
async reloadTreeFromCache() {
const notes = await treeBuilder.prepareTree();
2020-01-24 22:44:24 +08:00
const activeNode = this.getActiveNode();
2020-02-11 03:57:56 +08:00
const activeNotePath = activeNode !== null ? treeService.getNotePath(activeNode) : null;
2020-01-24 22:44:24 +08:00
await this.reload(notes);
if (activeNotePath) {
const node = await this.getNodeFromPath(activeNotePath, true);
await node.setActive(true, {noEvents: true});
}
}
hoistedNoteChangedListener() {
this.reloadTreeFromCache();
2020-01-24 22:44:24 +08:00
}
treeCacheReloadedListener() {
this.reloadTreeFromCache();
2020-01-24 22:44:24 +08:00
}
2020-02-15 17:41:21 +08:00
async moveNotesToCommand() {
const selectedOrActiveBranchIds = this.getSelectedOrActiveNodes().map(node => node.data.branchId);
this.triggerCommand('moveBranchIdsTo', {branchIds: selectedOrActiveBranchIds});
}
}