diff --git a/docs/backend_api/entities_branch.js.html b/docs/backend_api/entities_branch.js.html index 7a4d69a8f..9ae543ca5 100644 --- a/docs/backend_api/entities_branch.js.html +++ b/docs/backend_api/entities_branch.js.html @@ -73,6 +73,10 @@ class Branch extends Entity { this.notePosition = maxNotePos === null ? 0 : maxNotePos + 10; } + if (!this.isExpanded) { + this.isExpanded = false; + } + if (!this.isDeleted) { this.isDeleted = false; } diff --git a/docs/frontend_api/FrontendScriptApi.html b/docs/frontend_api/FrontendScriptApi.html index d4415c6ca..3fd3adc2b 100644 --- a/docs/frontend_api/FrontendScriptApi.html +++ b/docs/frontend_api/FrontendScriptApi.html @@ -1240,6 +1240,162 @@ + + + + + + +
| Name | + + +Type | + + + + + +Description | +
|---|---|---|
keyboardShortcut | 
+            
+
+            + + +string + + + + | + + + + + +e.g. "ctrl+shift+a" | +
handler | 
+            
+
+            + + +function + + + + | + + + + + ++ | 
done callback is never executed.
-	 * At least the built-in ForceDirected layout behaves in this way.
-	 *
-	 * @param done An optional callback function that gets executed when the springy algorithm stops,
-	 * either because it ended or because stop() was called.
 	 */
-	Renderer.prototype.start = function(done) {
-		var t = this;
-		this.layout.start(function render() {
-			t.clear();
+	Renderer.prototype.start = function(maxTime) {
+		if (maxTime) {
+			setTimeout(() => this.stop(), maxTime);
+		}
 
-			t.layout.eachEdge(function(edge, spring) {
-				t.drawEdge(edge, spring.point1.p, spring.point2.p);
-			});
-
-			t.layout.eachNode(function(node, point) {
-				t.drawNode(node, point.p);
-			});
-			
-			if (t.onRenderFrame !== undefined) { t.onRenderFrame(); }
-		}, this.onRenderStop, this.onRenderStart);
+		return new Promise((res, rej) => {
+			this.layout.start(res);
+		});
 	};
 
 	Renderer.prototype.stop = function() {
 		this.layout.stop();
 	};
 
-	// Array.forEach implementation for IE support..
-	//https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach
-	if ( !Array.prototype.forEach ) {
-		Array.prototype.forEach = function( callback, thisArg ) {
-			var T, k;
-			if ( this == null ) {
-				throw new TypeError( " this is null or not defined" );
-			}
-			var O = Object(this);
-			var len = O.length >>> 0; // Hack to convert O.length to a UInt32
-			if ( {}.toString.call(callback) != "[object Function]" ) {
-				throw new TypeError( callback + " is not a function" );
-			}
-			if ( thisArg ) {
-				T = thisArg;
-			}
-			k = 0;
-			while( k < len ) {
-				var kValue;
-				if ( k in O ) {
-					kValue = O[ k ];
-					callback.call( T, kValue, k, O );
-				}
-				k++;
-			}
-		};
-	}
-
 	var isEmpty = function(obj) {
 		for (var k in obj) {
 			if (obj.hasOwnProperty(k)) {
diff --git a/package.json b/package.json
index cebd72f1a..57caefc2f 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "name": "trilium",
   "productName": "Trilium Notes",
   "description": "Trilium Notes",
-  "version": "0.36.2",
+  "version": "0.36.3",
   "license": "AGPL-3.0-only",
   "main": "electron.js",
   "bin": {
diff --git a/src/public/javascripts/dialogs/link_map.js b/src/public/javascripts/dialogs/link_map.js
index c62001fae..4a3937a53 100644
--- a/src/public/javascripts/dialogs/link_map.js
+++ b/src/public/javascripts/dialogs/link_map.js
@@ -23,19 +23,18 @@ export async function showDialog() {
     // set default settings
     $maxNotesInput.val(20);
 
-    const note = noteDetailService.getActiveTabNote();
-
-    if (!note) {
-        return;
-    }
-
     $linkMapContainer.css("height", $("body").height() - 150);
 
-    linkMapService = new LinkMapService(note, $linkMapContainer, getOptions());
-
-    linkMapService.render();
+    $linkMapContainer.empty();
 
     $dialog.modal();
 }
 
+$dialog.on('shown.bs.modal', () => {
+    const note = noteDetailService.getActiveTabNote();
+
+    linkMapService = new LinkMapService(note, $linkMapContainer, getOptions());
+    linkMapService.render();
+});
+
 $maxNotesInput.on("input", () => linkMapService.loadNotesAndRelations(getOptions()));
diff --git a/src/public/javascripts/services/branches.js b/src/public/javascripts/services/branches.js
index 519a04e24..adad473bd 100644
--- a/src/public/javascripts/services/branches.js
+++ b/src/public/javascripts/services/branches.js
@@ -26,9 +26,7 @@ async function moveBeforeNode(nodesToMove, beforeNode) {
 
         await changeNode(
             node => node.moveTo(beforeNode, 'before'),
-            nodeToMove,
-            beforeNode.data.noteId,
-            null);
+            nodeToMove);
     }
 }
 
@@ -52,9 +50,7 @@ async function moveAfterNode(nodesToMove, afterNode) {
 
         await changeNode(
             node => node.moveTo(afterNode, 'after'),
-            nodeToMove,
-            null,
-            afterNode.data.noteId);
+            nodeToMove);
     }
 }
 
@@ -62,6 +58,11 @@ async function moveToNode(nodesToMove, toNode) {
     nodesToMove = await filterRootNote(nodesToMove);
 
     for (const nodeToMove of nodesToMove) {
+        if (nodeToMove.data.noteId === await hoistedNoteService.getHoistedNoteId()
+            || nodeToMove.getParent().data.noteType === 'search') {
+            continue;
+        }
+
         const resp = await server.put('branches/' + nodeToMove.data.branchId + '/move-to/' + toNode.data.noteId);
 
         if (!resp.success) {
@@ -156,7 +157,9 @@ async function deleteNodes(nodes) {
 }
 
 async function moveNodeUpInHierarchy(node) {
-    if (await hoistedNoteService.isRootNode(node) || await hoistedNoteService.isTopLevelNode(node)) {
+    if (await hoistedNoteService.isRootNode(node)
+        || await hoistedNoteService.isTopLevelNode(node)
+        || node.getParent().data.noteType === 'search') {
         return;
     }
 
@@ -177,7 +180,7 @@ async function moveNodeUpInHierarchy(node) {
         node);
 }
 
-async function changeNode(func, node, beforeNoteId = null, afterNoteId = null) {
+async function changeNode(func, node) {
     utils.assertArguments(func, node);
 
     const childNoteId = node.data.noteId;
diff --git a/src/public/javascripts/services/clipboard.js b/src/public/javascripts/services/clipboard.js
index a13c335ed..ec198bf1c 100644
--- a/src/public/javascripts/services/clipboard.js
+++ b/src/public/javascripts/services/clipboard.js
@@ -2,6 +2,7 @@ import treeUtils from "./tree_utils.js";
 import treeChangesService from "./branches.js";
 import cloningService from "./cloning.js";
 import toastService from "./toast.js";
+import hoistedNoteService from "./hoisted_note.js";
 
 let clipboardIds = [];
 let clipboardMode = null;
@@ -66,10 +67,16 @@ function copy(nodes) {
 }
 
 function cut(nodes) {
-    clipboardIds = nodes.map(node => node.key);
-    clipboardMode = 'cut';
+    clipboardIds = nodes
+        .filter(node => node.data.noteId !== hoistedNoteService.getHoistedNoteNoPromise())
+        .filter(node => node.getParent().data.noteType !== 'search')
+        .map(node => node.data.noteId);
 
-    toastService.showMessage("Note(s) have been cut into clipboard.");
+    if (clipboardIds.length > 0) {
+        clipboardMode = 'cut';
+
+        toastService.showMessage("Note(s) have been cut into clipboard.");
+    }
 }
 
 function isEmpty() {
diff --git a/src/public/javascripts/services/drag_and_drop.js b/src/public/javascripts/services/drag_and_drop.js
index 2fa1f9087..0b2025e8f 100644
--- a/src/public/javascripts/services/drag_and_drop.js
+++ b/src/public/javascripts/services/drag_and_drop.js
@@ -1,11 +1,13 @@
 import treeService from './tree.js';
 import treeChangesService from './branches.js';
+import hoistedNoteService from './hoisted_note.js';
 
 const dragAndDropSetup = {
     autoExpandMS: 600,
     dragStart: (node, data) => {
         // don't allow dragging root node
-        if (node.data.noteId === 'root') {
+        if (node.data.noteId === hoistedNoteService.getHoistedNoteNoPromise()
+            || node.getParent().data.noteType === 'search') {
             return false;
         }
 
@@ -25,6 +27,17 @@ const dragAndDropSetup = {
     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.getHoistedNoteNoPromise() || node.getParent().data.noteType === 'search'))) {
+
+            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) {
diff --git a/src/public/javascripts/services/frontend_script_api.js b/src/public/javascripts/services/frontend_script_api.js
index d92f8d254..387ec993d 100644
--- a/src/public/javascripts/services/frontend_script_api.js
+++ b/src/public/javascripts/services/frontend_script_api.js
@@ -359,6 +359,13 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
      * @return {Promise}
      */
     this.setHoistedNoteId = hoistedNoteService.setHoistedNoteId;
+
+    /**
+     * @method
+     * @param {string} keyboardShortcut - e.g. "ctrl+shift+a"
+     * @param {function} handler
+     */
+    this.bindGlobalShortcut = utils.bindGlobalShortcut;
 }
 
 export default FrontendScriptApi;
\ No newline at end of file
diff --git a/src/public/javascripts/services/hoisted_note.js b/src/public/javascripts/services/hoisted_note.js
index e7741baf4..ca5b218b1 100644
--- a/src/public/javascripts/services/hoisted_note.js
+++ b/src/public/javascripts/services/hoisted_note.js
@@ -3,12 +3,16 @@ import server from "./server.js";
 import tree from "./tree.js";
 import noteDetailService from "./note_detail.js";
 
-let hoistedNoteId;
+let hoistedNoteId = 'root';
 
 optionsService.waitForOptions().then(options => {
     hoistedNoteId = options.get('hoistedNoteId');
 });
 
+function getHoistedNoteNoPromise() {
+    return hoistedNoteId;
+}
+
 async function getHoistedNoteId() {
     await optionsService.waitForOptions();
 
@@ -49,6 +53,7 @@ async function isRootNode(node) {
 
 export default {
     getHoistedNoteId,
+    getHoistedNoteNoPromise,
     setHoistedNoteId,
     unhoist,
     isTopLevelNode,
diff --git a/src/public/javascripts/services/link_map.js b/src/public/javascripts/services/link_map.js
index f5041b90f..5ecfcb58b 100644
--- a/src/public/javascripts/services/link_map.js
+++ b/src/public/javascripts/services/link_map.js
@@ -119,60 +119,54 @@ export default class LinkMap {
                 stop: params => {}
             });
 
-
             return $noteBox;
         };
 
-        this.renderer = new Springy.Renderer(
-            layout,
-            () => {},
-            (edge, p1, p2) => {
-                const connectionId = this.linkMapContainerId + '-' + edge.source.id + '-' + edge.target.id;
+        this.renderer = new Springy.Renderer(layout);
+        await this.renderer.start(500);
 
-                if ($("#" + connectionId).length > 0) {
-                    return;
-                }
+        layout.eachNode((node, point) => {
+            const $noteBox = getNoteBox(node.id);
+            const middleW = this.$linkMapContainer.width() / 2;
+            const middleH = this.$linkMapContainer.height() / 2;
 
-                getNoteBox(edge.source.id);
-                getNoteBox(edge.target.id);
+            $noteBox
+                .css("left", (middleW + point.p.x * 100) + "px")
+                .css("top", (middleH + point.p.y * 100) + "px");
 
-                const connection = this.jsPlumbInstance.connect({
-                    source: this.noteIdToId(edge.source.id),
-                    target: this.noteIdToId(edge.target.id),
-                    type: 'link'
-                });
-
-                if (connection) {
-                    $(connection.canvas)
-                        .prop("id", connectionId)
-                        .addClass('link-' + edge.source.id)
-                        .addClass('link-' + edge.target.id);
-                }
-                else {
-                    console.log(`connection not created for`, edge);
-                }
-            },
-            (node, p) => {
-                const $noteBox = getNoteBox(node.id);
-                const middleW = this.$linkMapContainer.width() / 2;
-                const middleH = this.$linkMapContainer.height() / 2;
-
-                $noteBox
-                    .css("left", (middleW + p.x * 100) + "px")
-                    .css("top", (middleH + p.y * 100) + "px");
-
-                if ($noteBox.hasClass("link-map-active-note")) {
-                    this.moveToCenterOfElement($noteBox[0]);
-                }
-            },
-            () => {},
-            () => {},
-            () => {
-                this.jsPlumbInstance.repaintEverything();
+            if ($noteBox.hasClass("link-map-active-note")) {
+                this.moveToCenterOfElement($noteBox[0]);
             }
-        );
+        });
 
-        this.renderer.start();
+        layout.eachEdge(edge => {
+            const connectionId = this.linkMapContainerId + '-' + edge.source.id + '-' + edge.target.id;
+
+            if ($("#" + connectionId).length > 0) {
+                return;
+            }
+
+            getNoteBox(edge.source.id);
+            getNoteBox(edge.target.id);
+
+            const connection = this.jsPlumbInstance.connect({
+                source: this.noteIdToId(edge.source.id),
+                target: this.noteIdToId(edge.target.id),
+                type: 'link'
+            });
+
+            if (connection) {
+                $(connection.canvas)
+                    .prop("id", connectionId)
+                    .addClass('link-' + edge.source.id)
+                    .addClass('link-' + edge.target.id);
+            }
+            else {
+                console.log(`connection not created for`, edge);
+            }
+        });
+
+        this.jsPlumbInstance.repaintEverything();
     }
 
     moveToCenterOfElement(element) {
diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js
index 0f278b841..62bca258a 100644
--- a/src/public/javascripts/services/tree.js
+++ b/src/public/javascripts/services/tree.js
@@ -676,6 +676,7 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
         refKey: branchEntity.noteId,
         branchId: branchEntity.branchId,
         isProtected: extraOptions.isProtected,
+        type: noteEntity.type,
         extraClasses: await treeBuilder.getExtraClasses(noteEntity),
         icon: await treeBuilder.getIcon(noteEntity),
         folder: extraOptions.type === 'search',
diff --git a/src/public/javascripts/services/tree_builder.js b/src/public/javascripts/services/tree_builder.js
index edc235a6f..74f12b851 100644
--- a/src/public/javascripts/services/tree_builder.js
+++ b/src/public/javascripts/services/tree_builder.js
@@ -1,6 +1,4 @@
 import utils from "./utils.js";
-import Branch from "../entities/branch.js";
-import server from "./server.js";
 import treeCache from "./tree_cache.js";
 import ws from "./ws.js";
 import hoistedNoteService from "./hoisted_note.js";
@@ -72,6 +70,7 @@ async function prepareNode(branch) {
         parentNoteId: branch.parentNoteId,
         branchId: branch.branchId,
         isProtected: note.isProtected,
+        noteType: note.type,
         title: utils.escapeHtml(title),
         extraClasses: await getExtraClasses(note),
         icon: await getIcon(note),
@@ -110,24 +109,8 @@ async function prepareRealBranch(parentNote) {
 }
 
 async function prepareSearchBranch(note) {
-    const results = await server.get('search-note/' + note.noteId);
+    await treeCache.reloadNotes([note.noteId]);
 
-    // force to load all the notes at once instead of one by one
-    await treeCache.getNotes(results.map(res => res.noteId));
-
-    const {notes, branches} = await server.post('tree/load', { noteIds: [note.noteId] });
-
-    results.forEach((result, index) => branches.push({
-        branchId: "virt" + utils.randomString(10),
-        noteId: result.noteId,
-        parentNoteId: note.noteId,
-        prefix: treeCache.getBranch(result.branchId).prefix,
-        notePosition: (index + 1) * 10
-    }));
-
-    treeCache.addResp(notes, branches);
-
-    // note in cache changed
     const newNote = await treeCache.getNote(note.noteId);
 
     return await prepareRealBranch(newNote);
diff --git a/src/public/javascripts/services/tree_cache.js b/src/public/javascripts/services/tree_cache.js
index 256ad4784..02bfcbfe4 100644
--- a/src/public/javascripts/services/tree_cache.js
+++ b/src/public/javascripts/services/tree_cache.js
@@ -102,6 +102,29 @@ class TreeCache {
         const resp = await server.post('tree/load', { noteIds });
 
         this.addResp(resp.notes, resp.branches);
+
+        for (const note of resp.notes) {
+            if (note.type === 'search') {
+                const searchResults = await server.get('search-note/' + note.noteId);
+
+                // force to load all the notes at once instead of one by one
+                await treeCache.getNotes(searchResults.map(res => res.noteId));
+
+                const branches = resp.branches.filter(b => b.noteId === note.noteId || b.parentNoteId === note.noteId);
+
+                searchResults.forEach((result, index) => branches.push({
+                    // branchId should be repeatable since sometimes we reload some notes without rerendering the tree
+                    branchId: "virt" + result.noteId + '-' + note.noteId,
+                    noteId: result.noteId,
+                    parentNoteId: note.noteId,
+                    prefix: treeCache.getBranch(result.branchId).prefix,
+                    notePosition: (index + 1) * 10
+                }));
+
+                // update this note with standard (parent) branches + virtual (children) branches
+                treeCache.addResp([note], branches);
+            }
+        }
     }
 
     /** @return {Promise