");
+
+ for (const key in result) {
+ rowEl.append($("| ").html(result[key]));
+ }
+
+ $resultBody.append(rowEl);
+ }
+}
+
+$(document).bind('keydown', 'alt+o', showDialog);
+
+$query.bind('keydown', 'ctrl+return', execute);
+
+$executeButton.click(execute);
+
+export default {
+ showDialog
+};
\ No newline at end of file
diff --git a/src/public/javascripts/drag_and_drop.js b/src/public/javascripts/drag_and_drop.js
index e27eb019a..252bc0160 100644
--- a/src/public/javascripts/drag_and_drop.js
+++ b/src/public/javascripts/drag_and_drop.js
@@ -1,5 +1,8 @@
"use strict";
+import treeService from './note_tree.js';
+import treeChanges from './tree_changes.js';
+
const dragAndDropSetup = {
autoExpandMS: 600,
draggable: { // modify default jQuery draggable options
@@ -65,3 +68,5 @@ const dragAndDropSetup = {
}
}
};
+
+export default dragAndDropSetup;
\ No newline at end of file
diff --git a/src/public/javascripts/export.js b/src/public/javascripts/export.js
index 5cd62052b..9c105b58a 100644
--- a/src/public/javascripts/export.js
+++ b/src/public/javascripts/export.js
@@ -1,39 +1,41 @@
"use strict";
-const exportService = (function () {
- function exportSubTree(noteId) {
- const url = utils.getHost() + "/api/export/" + noteId + "?protectedSessionId="
- + encodeURIComponent(protected_session.getProtectedSessionId());
+import treeService from './note_tree.js';
+import protected_session from './protected_session.js';
+import utils from './utils.js';
- utils.download(url);
- }
+function exportSubTree(noteId) {
+ const url = utils.getHost() + "/api/export/" + noteId + "?protectedSessionId="
+ + encodeURIComponent(protected_session.getProtectedSessionId());
- let importNoteId;
+ utils.download(url);
+}
- function importSubTree(noteId) {
- importNoteId = noteId;
+let importNoteId;
- $("#import-upload").trigger('click');
- }
+function importSubTree(noteId) {
+ importNoteId = noteId;
- $("#import-upload").change(async function() {
- const formData = new FormData();
- formData.append('upload', this.files[0]);
+ $("#import-upload").trigger('click');
+}
- await $.ajax({
- url: baseApiUrl + 'import/' + importNoteId,
- headers: server.getHeaders(),
- data: formData,
- type: 'POST',
- contentType: false, // NEEDED, DON'T OMIT THIS
- processData: false, // NEEDED, DON'T OMIT THIS
- });
+$("#import-upload").change(async function() {
+ const formData = new FormData();
+ formData.append('upload', this.files[0]);
- await treeService.reload();
+ await $.ajax({
+ url: baseApiUrl + 'import/' + importNoteId,
+ headers: server.getHeaders(),
+ data: formData,
+ type: 'POST',
+ contentType: false, // NEEDED, DON'T OMIT THIS
+ processData: false, // NEEDED, DON'T OMIT THIS
});
- return {
- exportSubTree,
- importSubTree
- };
-})();
\ No newline at end of file
+ await treeService.reload();
+});
+
+export default {
+ exportSubTree,
+ importSubTree
+};
\ No newline at end of file
diff --git a/src/public/javascripts/init.js b/src/public/javascripts/init.js
index 2c3b7ad6c..4c1054e96 100644
--- a/src/public/javascripts/init.js
+++ b/src/public/javascripts/init.js
@@ -1,252 +1,258 @@
"use strict";
-const initService = (function() {
- // hot keys are active also inside inputs and content editables
- jQuery.hotkeys.options.filterInputAcceptingElements = false;
- jQuery.hotkeys.options.filterContentEditable = false;
- jQuery.hotkeys.options.filterTextInputs = false;
+import treeService from './note_tree.js';
+import link from './link.js';
+import messaging from './messaging.js';
+import noteEditor from './note_editor.js';
+import treeUtils from './tree_utils.js';
+import utils from './utils.js';
+import server from './server.js';
- $(document).bind('keydown', 'alt+m', e => {
- $(".hide-toggle").toggleClass("suppressed");
+// hot keys are active also inside inputs and content editables
+jQuery.hotkeys.options.filterInputAcceptingElements = false;
+jQuery.hotkeys.options.filterContentEditable = false;
+jQuery.hotkeys.options.filterTextInputs = false;
- e.preventDefault();
- });
+$(document).bind('keydown', 'alt+m', e => {
+ $(".hide-toggle").toggleClass("suppressed");
- // hide (toggle) everything except for the note content for distraction free writing
- $(document).bind('keydown', 'alt+t', e => {
- const date = new Date();
- const dateString = utils.formatDateTime(date);
+ e.preventDefault();
+});
- link.addTextToEditor(dateString);
+// hide (toggle) everything except for the note content for distraction free writing
+$(document).bind('keydown', 'alt+t', e => {
+ const date = new Date();
+ const dateString = utils.formatDateTime(date);
- e.preventDefault();
- });
+ link.addTextToEditor(dateString);
- $(document).bind('keydown', 'f5', () => {
- utils.reloadApp();
+ e.preventDefault();
+});
+
+$(document).bind('keydown', 'f5', () => {
+ utils.reloadApp();
+
+ return false;
+});
+
+$(document).bind('keydown', 'ctrl+r', () => {
+ utils.reloadApp();
+
+ return false;
+});
+
+$(document).bind('keydown', 'ctrl+shift+i', () => {
+ if (utils.isElectron()) {
+ require('electron').remote.getCurrentWindow().toggleDevTools();
return false;
- });
+ }
+});
- $(document).bind('keydown', 'ctrl+r', () => {
- utils.reloadApp();
+$(document).bind('keydown', 'ctrl+f', () => {
+ if (utils.isElectron()) {
+ const searchInPage = require('electron-in-page-search').default;
+ const remote = require('electron').remote;
+
+ const inPageSearch = searchInPage(remote.getCurrentWebContents());
+
+ inPageSearch.openSearchWindow();
return false;
- });
+ }
+});
- $(document).bind('keydown', 'ctrl+shift+i', () => {
- if (utils.isElectron()) {
- require('electron').remote.getCurrentWindow().toggleDevTools();
+$(document).bind('keydown', "ctrl+shift+up", () => {
+ const node = treeService.getCurrentNode();
+ node.navigate($.ui.keyCode.UP, true);
- return false;
+ $("#note-detail").focus();
+
+ return false;
+});
+
+$(document).bind('keydown', "ctrl+shift+down", () => {
+ const node = treeService.getCurrentNode();
+ node.navigate($.ui.keyCode.DOWN, true);
+
+ $("#note-detail").focus();
+
+ return false;
+});
+
+$(document).bind('keydown', 'ctrl+-', () => {
+ if (utils.isElectron()) {
+ const webFrame = require('electron').webFrame;
+
+ if (webFrame.getZoomFactor() > 0.2) {
+ webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1);
}
- });
-
- $(document).bind('keydown', 'ctrl+f', () => {
- if (utils.isElectron()) {
- const searchInPage = require('electron-in-page-search').default;
- const remote = require('electron').remote;
-
- const inPageSearch = searchInPage(remote.getCurrentWebContents());
-
- inPageSearch.openSearchWindow();
-
- return false;
- }
- });
-
- $(document).bind('keydown', "ctrl+shift+up", () => {
- const node = treeService.getCurrentNode();
- node.navigate($.ui.keyCode.UP, true);
-
- $("#note-detail").focus();
return false;
- });
+ }
+});
- $(document).bind('keydown', "ctrl+shift+down", () => {
- const node = treeService.getCurrentNode();
- node.navigate($.ui.keyCode.DOWN, true);
+$(document).bind('keydown', 'ctrl+=', () => {
+ if (utils.isElectron()) {
+ const webFrame = require('electron').webFrame;
- $("#note-detail").focus();
+ webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1);
return false;
- });
+ }
+});
- $(document).bind('keydown', 'ctrl+-', () => {
- if (utils.isElectron()) {
- const webFrame = require('electron').webFrame;
+$("#note-title").bind('keydown', 'return', () => $("#note-detail").focus());
- if (webFrame.getZoomFactor() > 0.2) {
- webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1);
- }
+$(window).on('beforeunload', () => {
+ // this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
+ // this sends the request asynchronously and doesn't wait for result
+ noteEditor.saveNoteIfChanged();
+});
- return false;
- }
- });
+// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
+$.ui.autocomplete.filter = (array, terms) => {
+ if (!terms) {
+ return array;
+ }
- $(document).bind('keydown', 'ctrl+=', () => {
- if (utils.isElectron()) {
- const webFrame = require('electron').webFrame;
+ const startDate = new Date();
- webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1);
+ const results = [];
+ const tokens = terms.toLowerCase().split(" ");
- return false;
- }
- });
+ for (const item of array) {
+ const lcLabel = item.label.toLowerCase();
- $("#note-title").bind('keydown', 'return', () => $("#note-detail").focus());
-
- $(window).on('beforeunload', () => {
- // this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
- // this sends the request asynchronously and doesn't wait for result
- noteEditor.saveNoteIfChanged();
- });
-
- // Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
- $.ui.autocomplete.filter = (array, terms) => {
- if (!terms) {
- return array;
+ const found = tokens.every(token => lcLabel.indexOf(token) !== -1);
+ if (!found) {
+ continue;
}
- const startDate = new Date();
+ // this is not completely correct and might cause minor problems with note with names containing this " / "
+ const lastSegmentIndex = lcLabel.lastIndexOf(" / ");
- const results = [];
- const tokens = terms.toLowerCase().split(" ");
+ if (lastSegmentIndex !== -1) {
+ const lastSegment = lcLabel.substr(lastSegmentIndex + 3);
- for (const item of array) {
- const lcLabel = item.label.toLowerCase();
+ // at least some token needs to be in the last segment (leaf note), otherwise this
+ // particular note is not that interesting (query is satisfied by parent note)
+ const foundInLastSegment = tokens.some(token => lastSegment.indexOf(token) !== -1);
- const found = tokens.every(token => lcLabel.indexOf(token) !== -1);
- if (!found) {
+ if (!foundInLastSegment) {
continue;
}
-
- // this is not completely correct and might cause minor problems with note with names containing this " / "
- const lastSegmentIndex = lcLabel.lastIndexOf(" / ");
-
- if (lastSegmentIndex !== -1) {
- const lastSegment = lcLabel.substr(lastSegmentIndex + 3);
-
- // at least some token needs to be in the last segment (leaf note), otherwise this
- // particular note is not that interesting (query is satisfied by parent note)
- const foundInLastSegment = tokens.some(token => lastSegment.indexOf(token) !== -1);
-
- if (!foundInLastSegment) {
- continue;
- }
- }
-
- results.push(item);
-
- if (results.length > 100) {
- break;
- }
}
- console.log("Search took " + (new Date().getTime() - startDate.getTime()) + "ms");
+ results.push(item);
- return results;
- };
+ if (results.length > 100) {
+ break;
+ }
+ }
- $(document).tooltip({
- items: "#note-detail a",
- content: function(callback) {
- const notePath = link.getNotePathFromLink($(this).attr("href"));
+ console.log("Search took " + (new Date().getTime() - startDate.getTime()) + "ms");
- if (notePath !== null) {
- const noteId = treeUtils.getNoteIdFromNotePath(notePath);
+ return results;
+};
- noteEditor.loadNote(noteId).then(note => callback(note.detail.content));
- }
- },
- close: function(event, ui)
- {
- ui.tooltip.hover(function()
+$(document).tooltip({
+ items: "#note-detail a",
+ content: function(callback) {
+ const notePath = link.getNotePathFromLink($(this).attr("href"));
+
+ if (notePath !== null) {
+ const noteId = treeUtils.getNoteIdFromNotePath(notePath);
+
+ noteEditor.loadNote(noteId).then(note => callback(note.detail.content));
+ }
+ },
+ close: function(event, ui)
+ {
+ ui.tooltip.hover(function()
+ {
+ $(this).stop(true).fadeTo(400, 1);
+ },
+ function()
+ {
+ $(this).fadeOut('400', function()
{
- $(this).stop(true).fadeTo(400, 1);
- },
- function()
- {
- $(this).fadeOut('400', function()
- {
- $(this).remove();
- });
+ $(this).remove();
});
- }
- });
+ });
+ }
+});
- window.onerror = function (msg, url, lineNo, columnNo, error) {
- const string = msg.toLowerCase();
+window.onerror = function (msg, url, lineNo, columnNo, error) {
+ const string = msg.toLowerCase();
- let message = "Uncaught error: ";
+ let message = "Uncaught error: ";
- if (string.indexOf("script error") > -1){
- message += 'No details available';
- }
- else {
- message += [
- 'Message: ' + msg,
- 'URL: ' + url,
- 'Line: ' + lineNo,
- 'Column: ' + columnNo,
- 'Error object: ' + JSON.stringify(error)
- ].join(' - ');
- }
-
- messaging.logError(message);
-
- return false;
- };
-
- $("#logout-button").toggle(!utils.isElectron());
-
- $(document).ready(() => {
- server.get("script/startup").then(scriptBundles => {
- for (const bundle of scriptBundles) {
- utils.executeBundle(bundle);
- }
- });
- });
-
- if (utils.isElectron()) {
- require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) {
- // this might occur when day note had to be created
- if (!await treeService.noteExists(parentNoteId)) {
- await treeService.reload();
- }
-
- await treeService.activateNode(parentNoteId);
-
- setTimeout(() => {
- const node = treeService.getCurrentNode();
-
- treeService.createNote(node, node.data.noteId, 'into', node.data.isProtected);
- }, 500);
- });
+ if (string.indexOf("script error") > -1){
+ message += 'No details available';
+ }
+ else {
+ message += [
+ 'Message: ' + msg,
+ 'URL: ' + url,
+ 'Line: ' + lineNo,
+ 'Column: ' + columnNo,
+ 'Error object: ' + JSON.stringify(error)
+ ].join(' - ');
}
- function uploadAttachment() {
- $("#attachment-upload").trigger('click');
- }
+ messaging.logError(message);
- $("#upload-attachment-button").click(uploadAttachment);
+ return false;
+};
- $("#attachment-upload").change(async function() {
- const formData = new FormData();
- formData.append('upload', this.files[0]);
+$("#logout-button").toggle(!utils.isElectron());
- const resp = await $.ajax({
- url: baseApiUrl + 'attachments/upload/' + noteEditor.getCurrentNoteId(),
- headers: server.getHeaders(),
- data: formData,
- type: 'POST',
- contentType: false, // NEEDED, DON'T OMIT THIS
- processData: false, // NEEDED, DON'T OMIT THIS
- });
-
- await treeService.reload();
-
- await treeService.activateNode(resp.noteId);
+$(document).ready(() => {
+ server.get("script/startup").then(scriptBundles => {
+ for (const bundle of scriptBundles) {
+ utils.executeBundle(bundle);
+ }
});
-})();
+});
+
+if (utils.isElectron()) {
+ require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) {
+ // this might occur when day note had to be created
+ if (!await treeService.noteExists(parentNoteId)) {
+ await treeService.reload();
+ }
+
+ await treeService.activateNode(parentNoteId);
+
+ setTimeout(() => {
+ const node = treeService.getCurrentNode();
+
+ treeService.createNote(node, node.data.noteId, 'into', node.data.isProtected);
+ }, 500);
+ });
+}
+
+function uploadAttachment() {
+ $("#attachment-upload").trigger('click');
+}
+
+$("#upload-attachment-button").click(uploadAttachment);
+
+$("#attachment-upload").change(async function() {
+ const formData = new FormData();
+ formData.append('upload', this.files[0]);
+
+ const resp = await $.ajax({
+ url: baseApiUrl + 'attachments/upload/' + noteEditor.getCurrentNoteId(),
+ headers: server.getHeaders(),
+ data: formData,
+ type: 'POST',
+ contentType: false, // NEEDED, DON'T OMIT THIS
+ processData: false, // NEEDED, DON'T OMIT THIS
+ });
+
+ await treeService.reload();
+
+ await treeService.activateNode(resp.noteId);
+});
diff --git a/src/public/javascripts/link.js b/src/public/javascripts/link.js
index 25b2961de..c06c85549 100644
--- a/src/public/javascripts/link.js
+++ b/src/public/javascripts/link.js
@@ -1,103 +1,105 @@
"use strict";
-const link = (function() {
- function getNotePathFromLink(url) {
- const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url);
+import treeService from './note_tree.js';
+import noteEditor from './note_editor.js';
+import treeUtils from './tree_utils.js';
- if (notePathMatch === null) {
- return null;
- }
- else {
- return notePathMatch[1];
- }
- }
-
- function getNodePathFromLabel(label) {
- const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label);
-
- if (notePathMatch !== null) {
- return notePathMatch[1];
- }
+function getNotePathFromLink(url) {
+ const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url);
+ if (notePathMatch === null) {
return null;
}
+ else {
+ return notePathMatch[1];
+ }
+}
- function createNoteLink(notePath, noteTitle) {
- if (!noteTitle) {
- const noteId = treeUtils.getNoteIdFromNotePath(notePath);
+function getNodePathFromLabel(label) {
+ const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label);
- noteTitle = treeService.getNoteTitle(noteId);
+ if (notePathMatch !== null) {
+ return notePathMatch[1];
+ }
+
+ return null;
+}
+
+function createNoteLink(notePath, noteTitle) {
+ if (!noteTitle) {
+ const noteId = treeUtils.getNoteIdFromNotePath(notePath);
+
+ noteTitle = treeService.getNoteTitle(noteId);
+ }
+
+ const noteLink = $("", {
+ href: 'javascript:',
+ text: noteTitle
+ }).attr('action', 'note')
+ .attr('note-path', notePath);
+
+ return noteLink;
+}
+
+function goToLink(e) {
+ e.preventDefault();
+
+ const $link = $(e.target);
+ let notePath = $link.attr("note-path");
+
+ if (!notePath) {
+ const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
+
+ if (!address) {
+ return;
}
- const noteLink = $("", {
- href: 'javascript:',
- text: noteTitle
- }).attr('action', 'note')
- .attr('note-path', notePath);
+ if (address.startsWith('http')) {
+ window.open(address, '_blank');
- return noteLink;
- }
-
- function goToLink(e) {
- e.preventDefault();
-
- const $link = $(e.target);
- let notePath = $link.attr("note-path");
-
- if (!notePath) {
- const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
-
- if (!address) {
- return;
- }
-
- if (address.startsWith('http')) {
- window.open(address, '_blank');
-
- return;
- }
-
- notePath = getNotePathFromLink(address);
+ return;
}
- treeService.activateNode(notePath);
+ notePath = getNotePathFromLink(address);
+ }
- // this is quite ugly hack, but it seems like we can't close the tooltip otherwise
- $("[role='tooltip']").remove();
+ treeService.activateNode(notePath);
- if (glob.activeDialog) {
- try {
- glob.activeDialog.dialog('close');
- }
- catch (e) {}
+ // this is quite ugly hack, but it seems like we can't close the tooltip otherwise
+ $("[role='tooltip']").remove();
+
+ if (glob.activeDialog) {
+ try {
+ glob.activeDialog.dialog('close');
}
+ catch (e) {}
}
+}
- function addLinkToEditor(linkTitle, linkHref) {
- const editor = noteEditor.getEditor();
- const doc = editor.document;
+function addLinkToEditor(linkTitle, linkHref) {
+ const editor = noteEditor.getEditor();
+ const doc = editor.document;
- doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection);
- }
+ doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection);
+}
- function addTextToEditor(text) {
- const editor = noteEditor.getEditor();
- const doc = editor.document;
+function addTextToEditor(text) {
+ const editor = noteEditor.getEditor();
+ const doc = editor.document;
- doc.enqueueChanges(() => editor.data.insertText(text), doc.selection);
- }
+ doc.enqueueChanges(() => editor.data.insertText(text), doc.selection);
+}
- // when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
- // of opening the link in new window/tab
- $(document).on('click', "a[action='note']", goToLink);
- $(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
- $(document).on('dblclick', '#note-detail a', goToLink);
+// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
+// of opening the link in new window/tab
+$(document).on('click', "a[action='note']", goToLink);
+$(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
+$(document).on('dblclick', '#note-detail a', goToLink);
- return {
- getNodePathFromLabel,
- getNotePathFromLink,
- createNoteLink,
- addLinkToEditor,
- addTextToEditor
- };
-})();
\ No newline at end of file
+export default {
+ getNodePathFromLabel,
+ getNotePathFromLink,
+ createNoteLink,
+ addLinkToEditor,
+ addTextToEditor
+};
\ No newline at end of file
diff --git a/src/public/javascripts/messaging.js b/src/public/javascripts/messaging.js
index 61d6cbca4..3bc626c10 100644
--- a/src/public/javascripts/messaging.js
+++ b/src/public/javascripts/messaging.js
@@ -1,115 +1,118 @@
"use strict";
-const messaging = (function() {
- const $changesToPushCount = $("#changes-to-push-count");
+import treeService from './note_tree.js';
+import noteEditor from './note_editor.js';
+import sync from './sync.js';
+import utils from './utils.js';
- function logError(message) {
- console.log(utils.now(), message); // needs to be separate from .trace()
- console.trace();
+const $changesToPushCount = $("#changes-to-push-count");
- if (ws && ws.readyState === 1) {
- ws.send(JSON.stringify({
- type: 'log-error',
- error: message
- }));
- }
- }
-
- function messageHandler(event) {
- const message = JSON.parse(event.data);
-
- if (message.type === 'sync') {
- lastPingTs = new Date().getTime();
-
- if (message.data.length > 0) {
- console.log(utils.now(), "Sync data: ", message.data);
-
- lastSyncId = message.data[message.data.length - 1].id;
- }
-
- const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId);
-
- if (syncData.some(sync => sync.entityName === 'branches')
- || syncData.some(sync => sync.entityName === 'notes')) {
-
- console.log(utils.now(), "Reloading tree because of background changes");
-
- treeService.reload();
- }
-
- if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === noteEditor.getCurrentNoteId())) {
- utils.showMessage('Reloading note because of background changes');
-
- noteEditor.reload();
- }
-
- if (syncData.some(sync => sync.entityName === 'recent_notes')) {
- console.log(utils.now(), "Reloading recent notes because of background changes");
-
- recentNotes.reload();
- }
-
- // we don't detect image changes here since images themselves are immutable and references should be
- // updated in note detail as well
-
- $changesToPushCount.html(message.changesToPushCount);
- }
- else if (message.type === 'sync-hash-check-failed') {
- utils.utils.showError("Sync check failed!", 60000);
- }
- else if (message.type === 'consistency-checks-failed') {
- utils.showError("Consistency checks failed! See logs for details.", 50 * 60000);
- }
- }
-
- function connectWebSocket() {
- const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws';
-
- // use wss for secure messaging
- const ws = new WebSocket(protocol + "://" + location.host);
- ws.onopen = event => console.log(utils.now(), "Connected to server with WebSocket");
- ws.onmessage = messageHandler;
- ws.onclose = function(){
- // Try to reconnect in 5 seconds
- setTimeout(() => connectWebSocket(), 5000);
- };
-
- return ws;
- }
-
- const ws = connectWebSocket();
-
- let lastSyncId = glob.maxSyncIdAtLoad;
- let lastPingTs = new Date().getTime();
- let connectionBrokenNotification = null;
-
- setInterval(async () => {
- if (new Date().getTime() - lastPingTs > 30000) {
- if (!connectionBrokenNotification) {
- connectionBrokenNotification = $.notify({
- // options
- message: "Lost connection to server"
- },{
- // settings
- type: 'danger',
- delay: 100000000 // keep it until we explicitly close it
- });
- }
- }
- else if (connectionBrokenNotification) {
- await connectionBrokenNotification.close();
- connectionBrokenNotification = null;
-
- utils.showMessage("Re-connected to server");
- }
+function logError(message) {
+ console.log(utils.now(), message); // needs to be separate from .trace()
+ console.trace();
+ if (ws && ws.readyState === 1) {
ws.send(JSON.stringify({
- type: 'ping',
- lastSyncId: lastSyncId
+ type: 'log-error',
+ error: message
}));
- }, 1000);
+ }
+}
- return {
- logError
+function messageHandler(event) {
+ const message = JSON.parse(event.data);
+
+ if (message.type === 'sync') {
+ lastPingTs = new Date().getTime();
+
+ if (message.data.length > 0) {
+ console.log(utils.now(), "Sync data: ", message.data);
+
+ lastSyncId = message.data[message.data.length - 1].id;
+ }
+
+ const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId);
+
+ if (syncData.some(sync => sync.entityName === 'branches')
+ || syncData.some(sync => sync.entityName === 'notes')) {
+
+ console.log(utils.now(), "Reloading tree because of background changes");
+
+ treeService.reload();
+ }
+
+ if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === noteEditor.getCurrentNoteId())) {
+ utils.showMessage('Reloading note because of background changes');
+
+ noteEditor.reload();
+ }
+
+ if (syncData.some(sync => sync.entityName === 'recent_notes')) {
+ console.log(utils.now(), "Reloading recent notes because of background changes");
+
+ recentNotes.reload();
+ }
+
+ // we don't detect image changes here since images themselves are immutable and references should be
+ // updated in note detail as well
+
+ $changesToPushCount.html(message.changesToPushCount);
+ }
+ else if (message.type === 'sync-hash-check-failed') {
+ utils.utils.showError("Sync check failed!", 60000);
+ }
+ else if (message.type === 'consistency-checks-failed') {
+ utils.showError("Consistency checks failed! See logs for details.", 50 * 60000);
+ }
+}
+
+function connectWebSocket() {
+ const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws';
+
+ // use wss for secure messaging
+ const ws = new WebSocket(protocol + "://" + location.host);
+ ws.onopen = event => console.log(utils.now(), "Connected to server with WebSocket");
+ ws.onmessage = messageHandler;
+ ws.onclose = function(){
+ // Try to reconnect in 5 seconds
+ setTimeout(() => connectWebSocket(), 5000);
};
-})();
\ No newline at end of file
+
+ return ws;
+}
+
+const ws = connectWebSocket();
+
+let lastSyncId = glob.maxSyncIdAtLoad;
+let lastPingTs = new Date().getTime();
+let connectionBrokenNotification = null;
+
+setInterval(async () => {
+ if (new Date().getTime() - lastPingTs > 30000) {
+ if (!connectionBrokenNotification) {
+ connectionBrokenNotification = $.notify({
+ // options
+ message: "Lost connection to server"
+ },{
+ // settings
+ type: 'danger',
+ delay: 100000000 // keep it until we explicitly close it
+ });
+ }
+ }
+ else if (connectionBrokenNotification) {
+ await connectionBrokenNotification.close();
+ connectionBrokenNotification = null;
+
+ utils.showMessage("Re-connected to server");
+ }
+
+ ws.send(JSON.stringify({
+ type: 'ping',
+ lastSyncId: lastSyncId
+ }));
+}, 1000);
+
+export default {
+ logError
+};
\ No newline at end of file
diff --git a/src/public/javascripts/note_editor.js b/src/public/javascripts/note_editor.js
index 6e86c19ec..e2f1e0668 100644
--- a/src/public/javascripts/note_editor.js
+++ b/src/public/javascripts/note_editor.js
@@ -1,400 +1,404 @@
"use strict";
-const noteEditor = (function() {
- const $noteTitle = $("#note-title");
+import treeService from './note_tree.js';
+import noteType from './note_type.js';
+import protected_session from './protected_session.js';
+import utils from './utils.js';
+import server from './server.js';
- const $noteDetail = $('#note-detail');
- const $noteDetailCode = $('#note-detail-code');
- const $noteDetailSearch = $('#note-detail-search');
- const $noteDetailRender = $('#note-detail-render');
- const $noteDetailAttachment = $('#note-detail-attachment');
+const $noteTitle = $("#note-title");
- const $protectButton = $("#protect-button");
- const $unprotectButton = $("#unprotect-button");
- const $noteDetailWrapper = $("#note-detail-wrapper");
- const $noteIdDisplay = $("#note-id-display");
- const $labelList = $("#label-list");
- const $labelListInner = $("#label-list-inner");
- const $attachmentFileName = $("#attachment-filename");
- const $attachmentFileType = $("#attachment-filetype");
- const $attachmentFileSize = $("#attachment-filesize");
- const $attachmentDownload = $("#attachment-download");
- const $attachmentOpen = $("#attachment-open");
- const $searchString = $("#search-string");
+const $noteDetail = $('#note-detail');
+const $noteDetailCode = $('#note-detail-code');
+const $noteDetailSearch = $('#note-detail-search');
+const $noteDetailRender = $('#note-detail-render');
+const $noteDetailAttachment = $('#note-detail-attachment');
- const $executeScriptButton = $("#execute-script-button");
+const $protectButton = $("#protect-button");
+const $unprotectButton = $("#unprotect-button");
+const $noteDetailWrapper = $("#note-detail-wrapper");
+const $noteIdDisplay = $("#note-id-display");
+const $labelList = $("#label-list");
+const $labelListInner = $("#label-list-inner");
+const $attachmentFileName = $("#attachment-filename");
+const $attachmentFileType = $("#attachment-filetype");
+const $attachmentFileSize = $("#attachment-filesize");
+const $attachmentDownload = $("#attachment-download");
+const $attachmentOpen = $("#attachment-open");
+const $searchString = $("#search-string");
- let editor = null;
- let codeEditor = null;
+const $executeScriptButton = $("#execute-script-button");
- let currentNote = null;
+let editor = null;
+let codeEditor = null;
- let noteChangeDisabled = false;
+let currentNote = null;
- let isNoteChanged = false;
+let noteChangeDisabled = false;
- function getCurrentNote() {
- return currentNote;
+let isNoteChanged = false;
+
+function getCurrentNote() {
+ return currentNote;
+}
+
+function getCurrentNoteId() {
+ return currentNote ? currentNote.detail.noteId : null;
+}
+
+function noteChanged() {
+ if (noteChangeDisabled) {
+ return;
}
- function getCurrentNoteId() {
- return currentNote ? currentNote.detail.noteId : null;
+ isNoteChanged = true;
+}
+
+async function reload() {
+ // no saving here
+
+ await loadNoteToEditor(getCurrentNoteId());
+}
+
+async function switchToNote(noteId) {
+ if (getCurrentNoteId() !== noteId) {
+ await saveNoteIfChanged();
+
+ await loadNoteToEditor(noteId);
+ }
+}
+
+async function saveNoteIfChanged() {
+ if (!isNoteChanged) {
+ return;
}
- function noteChanged() {
- if (noteChangeDisabled) {
- return;
+ const note = getCurrentNote();
+
+ updateNoteFromInputs(note);
+
+ await saveNoteToServer(note);
+
+ if (note.detail.isProtected) {
+ protected_session.touchProtectedSession();
+ }
+}
+
+function updateNoteFromInputs(note) {
+ if (note.detail.type === 'text') {
+ let content = editor.getData();
+
+ // if content is only tags/whitespace (typically ), then just make it empty
+ // this is important when setting new note to code
+ if (jQuery(content).text().trim() === '' && !content.includes(" ");
+
+ $noteDetail.show();
}
+ else if (currentNote.detail.type === 'code') {
+ if (!codeEditor) {
+ await utils.requireLibrary(utils.CODE_MIRROR);
- async function switchToNote(noteId) {
- if (getCurrentNoteId() !== noteId) {
- await saveNoteIfChanged();
+ CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
+ CodeMirror.keyMap.default["Tab"] = "indentMore";
- await loadNoteToEditor(noteId);
- }
- }
+ CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
- async function saveNoteIfChanged() {
- if (!isNoteChanged) {
- return;
- }
-
- const note = noteEditor.getCurrentNote();
-
- updateNoteFromInputs(note);
-
- await saveNoteToServer(note);
-
- if (note.detail.isProtected) {
- protected_session.touchProtectedSession();
- }
- }
-
- function updateNoteFromInputs(note) {
- if (note.detail.type === 'text') {
- let content = editor.getData();
-
- // if content is only tags/whitespace (typically ), then just make it empty
- // this is important when setting new note to code
- if (jQuery(content).text().trim() === '' && !content.includes(" 0) {
+ for (const attr of labels) {
+ $labelListInner.append(utils.formatLabel(attr) + " ");
+ }
+
+ $labelList.show();
}
-
- async function setContent(content) {
- if (currentNote.detail.type === 'text') {
- if (!editor) {
- await utils.requireLibrary(utils.CKEDITOR);
-
- editor = await BalloonEditor.create($noteDetail[0], {});
-
- editor.document.on('change', noteChanged);
- }
-
- // temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
- editor.setData(content ? content : "");
-
- $noteDetail.show();
- }
- else if (currentNote.detail.type === 'code') {
- if (!codeEditor) {
- await utils.requireLibrary(utils.CODE_MIRROR);
-
- CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
- CodeMirror.keyMap.default["Tab"] = "indentMore";
-
- CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
-
- codeEditor = CodeMirror($("#note-detail-code")[0], {
- value: "",
- viewportMargin: Infinity,
- indentUnit: 4,
- matchBrackets: true,
- matchTags: { bothTags: true },
- highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false },
- lint: true,
- gutters: ["CodeMirror-lint-markers"],
- lineNumbers: true
- });
-
- codeEditor.on('change', noteChanged);
- }
-
- $noteDetailCode.show();
-
- // this needs to happen after the element is shown, otherwise the editor won't be refresheds
- codeEditor.setValue(content);
-
- const info = CodeMirror.findModeByMIME(currentNote.detail.mime);
-
- if (info) {
- codeEditor.setOption("mode", info.mime);
- CodeMirror.autoLoadMode(codeEditor, info.mode);
- }
-
- codeEditor.refresh();
- }
- else if (currentNote.detail.type === 'search') {
- $noteDetailSearch.show();
-
- try {
- const json = JSON.parse(content);
-
- $searchString.val(json.searchString);
- }
- catch (e) {
- console.log(e);
- $searchString.val('');
- }
-
- $searchString.on('input', noteChanged);
- }
+ else {
+ $labelList.hide();
}
+}
- async function loadNoteToEditor(noteId) {
- currentNote = await loadNote(noteId);
+async function loadNote(noteId) {
+ return await server.get('notes/' + noteId);
+}
- if (isNewNoteCreated) {
- isNewNoteCreated = false;
+function getEditor() {
+ return editor;
+}
- $noteTitle.focus().select();
- }
+function focus() {
+ const note = getCurrentNote();
- $noteIdDisplay.html(noteId);
+ if (note.detail.type === 'text') {
+ $noteDetail.focus();
+ }
+ else if (note.detail.type === 'code') {
+ codeEditor.focus();
+ }
+ else if (note.detail.type === 'render' || note.detail.type === 'file' || note.detail.type === 'search') {
+ // do nothing
+ }
+ else {
+ utils.throwError('Unrecognized type: ' + note.detail.type);
+ }
+}
- await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false);
+function getCurrentNoteType() {
+ const currentNote = getCurrentNote();
- if (currentNote.detail.isProtected) {
- protected_session.touchProtectedSession();
- }
+ return currentNote ? currentNote.detail.type : null;
+}
- // this might be important if we focused on protected note when not in protected note and we got a dialog
- // to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it.
- protected_session.ensureDialogIsClosed();
-
- $noteDetailWrapper.show();
-
- noteChangeDisabled = true;
-
- $noteTitle.val(currentNote.detail.title);
-
- noteType.setNoteType(currentNote.detail.type);
- noteType.setNoteMime(currentNote.detail.mime);
-
- $noteDetail.hide();
- $noteDetailSearch.hide();
- $noteDetailCode.hide();
- $noteDetailRender.html('').hide();
- $noteDetailAttachment.hide();
-
- if (currentNote.detail.type === 'render') {
- $noteDetailRender.show();
+async function executeCurrentNote() {
+ if (getCurrentNoteType() === 'code') {
+ // make sure note is saved so we load latest changes
+ await saveNoteIfChanged();
+ if (currentNote.detail.mime.endsWith("env=frontend")) {
const bundle = await server.get('script/bundle/' + getCurrentNoteId());
- $noteDetailRender.html(bundle.html);
-
utils.executeBundle(bundle);
}
- else if (currentNote.detail.type === 'file') {
- $noteDetailAttachment.show();
- $attachmentFileName.text(currentNote.labels.original_file_name);
- $attachmentFileSize.text(currentNote.labels.file_size + " bytes");
- $attachmentFileType.text(currentNote.detail.mime);
- }
- else {
- await setContent(currentNote.detail.content);
+ if (currentNote.detail.mime.endsWith("env=backend")) {
+ await server.post('script/run/' + getCurrentNoteId());
}
- noteChangeDisabled = false;
-
- setNoteBackgroundIfProtected(currentNote);
- treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
-
- // after loading new note make sure editor is scrolled to the top
- $noteDetailWrapper.scrollTop(0);
-
- loadLabelList();
+ utils.showMessage("Note executed");
}
+}
- async function loadLabelList() {
- const noteId = getCurrentNoteId();
+$attachmentDownload.click(() => utils.download(getAttachmentUrl()));
- const labels = await server.get('notes/' + noteId + '/labels');
+$attachmentOpen.click(() => {
+ if (utils.isElectron()) {
+ const open = require("open");
- $labelListInner.html('');
-
- if (labels.length > 0) {
- for (const attr of labels) {
- $labelListInner.append(utils.formatLabel(attr) + " ");
- }
-
- $labelList.show();
- }
- else {
- $labelList.hide();
- }
+ open(getAttachmentUrl());
}
-
- async function loadNote(noteId) {
- return await server.get('notes/' + noteId);
+ else {
+ window.location.href = getAttachmentUrl();
}
+});
- function getEditor() {
- return editor;
- }
+function getAttachmentUrl() {
+ // electron needs absolute URL so we extract current host, port, protocol
+ return utils.getHost() + "/api/attachments/download/" + getCurrentNoteId()
+ + "?protectedSessionId=" + encodeURIComponent(protected_session.getProtectedSessionId());
+}
- function focus() {
- const note = getCurrentNote();
+$(document).ready(() => {
+ $noteTitle.on('input', () => {
+ noteChanged();
- if (note.detail.type === 'text') {
- $noteDetail.focus();
- }
- else if (note.detail.type === 'code') {
- codeEditor.focus();
- }
- else if (note.detail.type === 'render' || note.detail.type === 'file' || note.detail.type === 'search') {
- // do nothing
- }
- else {
- utils.throwError('Unrecognized type: ' + note.detail.type);
- }
- }
+ const title = $noteTitle.val();
- function getCurrentNoteType() {
- const currentNote = getCurrentNote();
-
- return currentNote ? currentNote.detail.type : null;
- }
-
- async function executeCurrentNote() {
- if (getCurrentNoteType() === 'code') {
- // make sure note is saved so we load latest changes
- await saveNoteIfChanged();
-
- if (currentNote.detail.mime.endsWith("env=frontend")) {
- const bundle = await server.get('script/bundle/' + getCurrentNoteId());
-
- utils.executeBundle(bundle);
- }
-
- if (currentNote.detail.mime.endsWith("env=backend")) {
- await server.post('script/run/' + getCurrentNoteId());
- }
-
- utils.showMessage("Note executed");
- }
- }
-
- $attachmentDownload.click(() => utils.download(getAttachmentUrl()));
-
- $attachmentOpen.click(() => {
- if (utils.isElectron()) {
- const open = require("open");
-
- open(getAttachmentUrl());
- }
- else {
- window.location.href = getAttachmentUrl();
- }
+ treeService.setNoteTitle(getCurrentNoteId(), title);
});
- function getAttachmentUrl() {
- // electron needs absolute URL so we extract current host, port, protocol
- return utils.getHost() + "/api/attachments/download/" + getCurrentNoteId()
- + "?protectedSessionId=" + encodeURIComponent(protected_session.getProtectedSessionId());
- }
+ // so that tab jumps from note title (which has tabindex 1)
+ $noteDetail.attr("tabindex", 2);
+});
- $(document).ready(() => {
- $noteTitle.on('input', () => {
- noteChanged();
+$(document).bind('keydown', "ctrl+return", executeCurrentNote);
- const title = $noteTitle.val();
+$executeScriptButton.click(executeCurrentNote());
- treeService.setNoteTitle(getCurrentNoteId(), title);
- });
+setInterval(saveNoteIfChanged, 5000);
- // so that tab jumps from note title (which has tabindex 1)
- $noteDetail.attr("tabindex", 2);
- });
-
- $(document).bind('keydown', "ctrl+return", executeCurrentNote);
-
- $executeScriptButton.click(executeCurrentNote());
-
- setInterval(saveNoteIfChanged, 5000);
-
- return {
- reload,
- switchToNote,
- saveNoteIfChanged,
- updateNoteFromInputs,
- saveNoteToServer,
- setNoteBackgroundIfProtected,
- loadNote,
- getCurrentNote,
- getCurrentNoteType,
- getCurrentNoteId,
- newNoteCreated,
- getEditor,
- focus,
- executeCurrentNote,
- loadLabelList,
- setContent
- };
-})();
\ No newline at end of file
+export default {
+ reload,
+ switchToNote,
+ saveNoteIfChanged,
+ updateNoteFromInputs,
+ saveNoteToServer,
+ setNoteBackgroundIfProtected,
+ loadNote,
+ getCurrentNote,
+ getCurrentNoteType,
+ getCurrentNoteId,
+ newNoteCreated,
+ getEditor,
+ focus,
+ executeCurrentNote,
+ loadLabelList,
+ setContent
+};
\ No newline at end of file
diff --git a/src/public/javascripts/note_tree.js b/src/public/javascripts/note_tree.js
index 3f8b67020..1fd0cb8fd 100644
--- a/src/public/javascripts/note_tree.js
+++ b/src/public/javascripts/note_tree.js
@@ -1,5 +1,17 @@
"use strict";
+import contextMenu from './context_menu.js';
+import dragAndDropSetup from './drag_and_drop.js';
+import link from './link.js';
+import messaging from './messaging.js';
+import noteEditor from './note_editor.js';
+import protected_session from './protected_session.js';
+import treeChanges from './tree_changes.js';
+import treeUtils from './tree_utils.js';
+import utils from './utils.js';
+import server from './server.js';
+import recentNotes from './dialogs/recent_notes.js';
+
class TreeCache {
constructor(noteRows, branchRows) {
this.parents = [];
@@ -126,925 +138,923 @@ class Branch {
}
}
-const treeService = (function() {
- let treeCache;
+let treeCache;
- const $tree = $("#tree");
- const $parentList = $("#parent-list");
- const $parentListList = $("#parent-list-inner");
- const $createTopLevelNoteButton = $("#create-top-level-note-button");
- const $collapseTreeButton = $("#collapse-tree-button");
- const $scrollToCurrentNoteButton = $("#scroll-to-current-note-button");
+const $tree = $("#tree");
+const $parentList = $("#parent-list");
+const $parentListList = $("#parent-list-inner");
+const $createTopLevelNoteButton = $("#create-top-level-note-button");
+const $collapseTreeButton = $("#collapse-tree-button");
+const $scrollToCurrentNoteButton = $("#scroll-to-current-note-button");
- let instanceName = null; // should have better place
+let instanceName = null; // should have better place
- let startNotePath = null;
+let startNotePath = null;
- function getNote(noteId) {
- const note = treeCache.getNote(noteId);
+function getNote(noteId) {
+ const note = treeCache.getNote(noteId);
- if (!note) {
- utils.throwError("Can't find title for noteId='" + noteId + "'");
+ if (!note) {
+ utils.throwError("Can't find title for noteId='" + noteId + "'");
+ }
+
+ return note;
+}
+
+function getNoteTitle(noteId, parentNoteId = null) {
+ utils.assertArguments(noteId);
+
+ let title = treeCache.getNote(noteId).title;
+
+ if (parentNoteId !== null) {
+ const branch = treeCache.getBranchByChildParent(noteId, parentNoteId);
+
+ if (branch && branch.prefix) {
+ title = branch.prefix + ' - ' + title;
}
-
- return note;
}
- function getNoteTitle(noteId, parentNoteId = null) {
- utils.assertArguments(noteId);
+ return title;
+}
- let title = treeCache.getNote(noteId).title;
+// note that if you want to access data like noteId or isProtected, you need to go into "data" property
+function getCurrentNode() {
+ return $tree.fancytree("getActiveNode");
+}
- if (parentNoteId !== null) {
- const branch = treeCache.getBranchByChildParent(noteId, parentNoteId);
+function getCurrentNotePath() {
+ const node = getCurrentNode();
- if (branch && branch.prefix) {
- title = branch.prefix + ' - ' + title;
- }
- }
+ return treeUtils.getNotePath(node);
+}
- return title;
+function getNodesByBranchId(branchId) {
+ utils.assertArguments(branchId);
+
+ const branch = treeCache.getBranch(branchId);
+
+ return getNodesByNoteId(branch.noteId).filter(node => node.data.branchId === branchId);
+}
+
+function getNodesByNoteId(noteId) {
+ utils.assertArguments(noteId);
+
+ const list = getTree().getNodesByRef(noteId);
+ return list ? list : []; // if no nodes with this refKey are found, fancy tree returns null
+}
+
+function setPrefix(branchId, prefix) {
+ utils.assertArguments(branchId);
+
+ treeCache.getBranch(branchId).prefix = prefix;
+
+ getNodesByBranchId(branchId).map(node => setNodeTitleWithPrefix(node));
+}
+
+function setNodeTitleWithPrefix(node) {
+ const noteTitle = getNoteTitle(node.data.noteId);
+ const branch = treeCache.getBranch(node.data.branchId);
+
+ const prefix = branch.prefix;
+
+ const title = (prefix ? (prefix + " - ") : "") + noteTitle;
+
+ node.setTitle(utils.escapeHtml(title));
+}
+
+function removeParentChildRelation(parentNoteId, childNoteId) {
+ utils.assertArguments(parentNoteId, childNoteId);
+
+ treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== parentNoteId);
+ treeCache.children[parentNoteId] = treeCache.children[parentNoteId].filter(ch => ch.noteId !== childNoteId);
+
+ delete treeCache.childParentToBranch[childNoteId + '-' + parentNoteId];
+}
+
+function setParentChildRelation(branchId, parentNoteId, childNoteId) {
+ treeCache.parents[childNoteId] = treeCache.parents[childNoteId] || [];
+ treeCache.parents[childNoteId].push(treeCache.getNote(parentNoteId));
+
+ treeCache.children[parentNoteId] = treeCache.children[parentNoteId] || [];
+ treeCache.children[parentNoteId].push(treeCache.getNote(childNoteId));
+
+ treeCache.childParentToBranch[childNoteId + '-' + parentNoteId] = treeCache.getBranch(branchId);
+}
+
+async function prepareBranch(noteRows, branchRows) {
+ utils.assertArguments(noteRows);
+
+ treeCache = new TreeCache(noteRows, branchRows);
+
+ return await prepareBranchInner(treeCache.getNote('root'));
+}
+
+async function getExtraClasses(note) {
+ utils.assertArguments(note);
+
+ const extraClasses = [];
+
+ if (note.isProtected) {
+ extraClasses.push("protected");
}
- // note that if you want to access data like noteId or isProtected, you need to go into "data" property
- function getCurrentNode() {
- return $tree.fancytree("getActiveNode");
+ if ((await note.getParentNotes()).length > 1) {
+ extraClasses.push("multiple-parents");
}
- function getCurrentNotePath() {
- const node = getCurrentNode();
+ extraClasses.push(note.type);
- return treeUtils.getNotePath(node);
+ return extraClasses.join(" ");
+}
+
+async function prepareBranchInner(parentNote) {
+ utils.assertArguments(parentNote);
+
+ const childBranches = await parentNote.getChildBranches();
+
+ if (!childBranches) {
+ messaging.logError(`No children for ${parentNote}. This shouldn't happen.`);
+ return;
}
- function getNodesByBranchId(branchId) {
- utils.assertArguments(branchId);
+ const noteList = [];
- const branch = treeCache.getBranch(branchId);
+ for (const branch of childBranches) {
+ const note = await branch.getNote();
+ const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
- return getNodesByNoteId(branch.noteId).filter(node => node.data.branchId === branchId);
- }
+ const node = {
+ noteId: note.noteId,
+ parentNoteId: branch.parentNoteId,
+ branchId: branch.branchId,
+ isProtected: note.isProtected,
+ title: utils.escapeHtml(title),
+ extraClasses: await getExtraClasses(note),
+ refKey: note.noteId,
+ expanded: note.type !== 'search' && branch.isExpanded
+ };
- function getNodesByNoteId(noteId) {
- utils.assertArguments(noteId);
+ const hasChildren = (await note.getChildNotes()).length > 0;
- const list = getTree().getNodesByRef(noteId);
- return list ? list : []; // if no nodes with this refKey are found, fancy tree returns null
- }
+ if (hasChildren || note.type === 'search') {
+ node.folder = true;
- function setPrefix(branchId, prefix) {
- utils.assertArguments(branchId);
-
- treeCache.getBranch(branchId).prefix = prefix;
-
- getNodesByBranchId(branchId).map(node => setNodeTitleWithPrefix(node));
- }
-
- function setNodeTitleWithPrefix(node) {
- const noteTitle = getNoteTitle(node.data.noteId);
- const branch = treeCache.getBranch(node.data.branchId);
-
- const prefix = branch.prefix;
-
- const title = (prefix ? (prefix + " - ") : "") + noteTitle;
-
- node.setTitle(utils.escapeHtml(title));
- }
-
- function removeParentChildRelation(parentNoteId, childNoteId) {
- utils.assertArguments(parentNoteId, childNoteId);
-
- treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== parentNoteId);
- treeCache.children[parentNoteId] = treeCache.children[parentNoteId].filter(ch => ch.noteId !== childNoteId);
-
- delete treeCache.childParentToBranch[childNoteId + '-' + parentNoteId];
- }
-
- function setParentChildRelation(branchId, parentNoteId, childNoteId) {
- treeCache.parents[childNoteId] = treeCache.parents[childNoteId] || [];
- treeCache.parents[childNoteId].push(treeCache.getNote(parentNoteId));
-
- treeCache.children[parentNoteId] = treeCache.children[parentNoteId] || [];
- treeCache.children[parentNoteId].push(treeCache.getNote(childNoteId));
-
- treeCache.childParentToBranch[childNoteId + '-' + parentNoteId] = treeCache.getBranch(branchId);
- }
-
- async function prepareBranch(noteRows, branchRows) {
- utils.assertArguments(noteRows);
-
- treeCache = new TreeCache(noteRows, branchRows);
-
- return await prepareBranchInner(treeCache.getNote('root'));
- }
-
- async function getExtraClasses(note) {
- utils.assertArguments(note);
-
- const extraClasses = [];
-
- if (note.isProtected) {
- extraClasses.push("protected");
- }
-
- if ((await note.getParentNotes()).length > 1) {
- extraClasses.push("multiple-parents");
- }
-
- extraClasses.push(note.type);
-
- return extraClasses.join(" ");
- }
-
- async function prepareBranchInner(parentNote) {
- utils.assertArguments(parentNote);
-
- const childBranches = await parentNote.getChildBranches();
-
- if (!childBranches) {
- messaging.logError(`No children for ${parentNote}. This shouldn't happen.`);
- return;
- }
-
- const noteList = [];
-
- for (const branch of childBranches) {
- const note = await branch.getNote();
- const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
-
- const node = {
- noteId: note.noteId,
- parentNoteId: branch.parentNoteId,
- branchId: branch.branchId,
- isProtected: note.isProtected,
- title: utils.escapeHtml(title),
- extraClasses: await getExtraClasses(note),
- refKey: note.noteId,
- expanded: note.type !== 'search' && branch.isExpanded
- };
-
- const hasChildren = (await note.getChildNotes()).length > 0;
-
- if (hasChildren || note.type === 'search') {
- node.folder = true;
-
- if (node.expanded && note.type !== 'search') {
- node.children = await prepareBranchInner(note);
- }
- else {
- node.lazy = true;
- }
- }
-
- noteList.push(node);
- }
-
- return noteList;
- }
-
- async function expandToNote(notePath, expandOpts) {
- utils.assertArguments(notePath);
-
- const runPath = await getRunPath(notePath);
-
- const noteId = treeUtils.getNoteIdFromNotePath(notePath);
-
- let parentNoteId = 'root';
-
- for (const childNoteId of runPath) {
- const node = getNodesByNoteId(childNoteId).find(node => node.data.parentNoteId === parentNoteId);
-
- if (childNoteId === noteId) {
- return node;
+ if (node.expanded && note.type !== 'search') {
+ node.children = await prepareBranchInner(note);
}
else {
- await node.setExpanded(true, expandOpts);
+ node.lazy = true;
}
-
- parentNoteId = childNoteId;
}
+
+ noteList.push(node);
}
- async function activateNode(notePath) {
- utils.assertArguments(notePath);
+ return noteList;
+}
- const node = await expandToNote(notePath);
+async function expandToNote(notePath, expandOpts) {
+ utils.assertArguments(notePath);
- await node.setActive();
+ const runPath = await getRunPath(notePath);
- clearSelectedNodes();
+ const noteId = treeUtils.getNoteIdFromNotePath(notePath);
+
+ let parentNoteId = 'root';
+
+ for (const childNoteId of runPath) {
+ const node = getNodesByNoteId(childNoteId).find(node => node.data.parentNoteId === parentNoteId);
+
+ if (childNoteId === noteId) {
+ return node;
+ }
+ else {
+ await node.setExpanded(true, expandOpts);
+ }
+
+ parentNoteId = childNoteId;
}
+}
- /**
- * Accepts notePath and tries to resolve it. Part of the path might not be valid because of note moving (which causes
- * path change) or other corruption, in that case this will try to get some other valid path to the correct note.
- */
- async function getRunPath(notePath) {
- utils.assertArguments(notePath);
+async function activateNode(notePath) {
+ utils.assertArguments(notePath);
- const path = notePath.split("/").reverse();
- path.push('root');
+ const node = await expandToNote(notePath);
- const effectivePath = [];
- let childNoteId = null;
- let i = 0;
+ await node.setActive();
- while (true) {
- if (i >= path.length) {
- break;
+ clearSelectedNodes();
+}
+
+/**
+ * Accepts notePath and tries to resolve it. Part of the path might not be valid because of note moving (which causes
+ * path change) or other corruption, in that case this will try to get some other valid path to the correct note.
+ */
+async function getRunPath(notePath) {
+ utils.assertArguments(notePath);
+
+ const path = notePath.split("/").reverse();
+ path.push('root');
+
+ const effectivePath = [];
+ let childNoteId = null;
+ let i = 0;
+
+ while (true) {
+ if (i >= path.length) {
+ break;
+ }
+
+ const parentNoteId = path[i++];
+
+ if (childNoteId !== null) {
+ const child = treeCache.getNote(childNoteId);
+ const parents = await child.getParentNotes();
+
+ if (!parents) {
+ messaging.logError("No parents found for " + childNoteId);
+ return;
}
- const parentNoteId = path[i++];
+ if (!parents.some(p => p.noteId === parentNoteId)) {
+ console.log(utils.now(), "Did not find parent " + parentNoteId + " for child " + childNoteId);
- if (childNoteId !== null) {
- const child = treeCache.getNote(childNoteId);
- const parents = await child.getParentNotes();
+ if (parents.length > 0) {
+ console.log(utils.now(), "Available parents:", parents);
- if (!parents) {
- messaging.logError("No parents found for " + childNoteId);
+ const someNotePath = await getSomeNotePath(parents[0]);
+
+ if (someNotePath) { // in case it's root the path may be empty
+ const pathToRoot = someNotePath.split("/").reverse();
+
+ for (const noteId of pathToRoot) {
+ effectivePath.push(noteId);
+ }
+ }
+
+ break;
+ }
+ else {
+ messaging.logError("No parents, can't activate node.");
return;
}
-
- if (!parents.some(p => p.noteId === parentNoteId)) {
- console.log(utils.now(), "Did not find parent " + parentNoteId + " for child " + childNoteId);
-
- if (parents.length > 0) {
- console.log(utils.now(), "Available parents:", parents);
-
- const someNotePath = await getSomeNotePath(parents[0]);
-
- if (someNotePath) { // in case it's root the path may be empty
- const pathToRoot = someNotePath.split("/").reverse();
-
- for (const noteId of pathToRoot) {
- effectivePath.push(noteId);
- }
- }
-
- break;
- }
- else {
- messaging.logError("No parents, can't activate node.");
- return;
- }
- }
- }
-
- if (parentNoteId === 'root') {
- break;
- }
- else {
- effectivePath.push(parentNoteId);
- childNoteId = parentNoteId;
}
}
- return effectivePath.reverse();
+ if (parentNoteId === 'root') {
+ break;
+ }
+ else {
+ effectivePath.push(parentNoteId);
+ childNoteId = parentNoteId;
+ }
}
- async function showParentList(noteId, node) {
- utils.assertArguments(noteId, node);
+ return effectivePath.reverse();
+}
- const note = treeCache.getNote(noteId);
- const parents = await note.getParentNotes();
+async function showParentList(noteId, node) {
+ utils.assertArguments(noteId, node);
+
+ const note = treeCache.getNote(noteId);
+ const parents = await note.getParentNotes();
+
+ if (!parents.length) {
+ utils.throwError("Can't find parents for noteId=" + noteId);
+ }
+
+ if (parents.length <= 1) {
+ $parentList.hide();
+ }
+ else {
+ $parentList.show();
+ $parentListList.empty();
+
+ for (const parentNote of parents) {
+ const parentNotePath = await getSomeNotePath(parentNote);
+ // this is to avoid having root notes leading '/'
+ const notePath = parentNotePath ? (parentNotePath + '/' + noteId) : noteId;
+ const title = getNotePathTitle(notePath);
+
+ let item;
+
+ if (node.getParent().data.noteId === parentNote.noteId) {
+ item = $("").attr("title", "Current note").append(title);
+ }
+ else {
+ item = link.createNoteLink(notePath, title);
+ }
+
+ $parentListList.append($("").append(item));
+ }
+ }
+}
+
+function getNotePathTitle(notePath) {
+ utils.assertArguments(notePath);
+
+ const titlePath = [];
+
+ let parentNoteId = 'root';
+
+ for (const noteId of notePath.split('/')) {
+ titlePath.push(getNoteTitle(noteId, parentNoteId));
+
+ parentNoteId = noteId;
+ }
+
+ return titlePath.join(' / ');
+}
+
+async function getSomeNotePath(note) {
+ utils.assertArguments(note);
+
+ const path = [];
+
+ let cur = note;
+
+ while (cur.noteId !== 'root') {
+ path.push(cur.noteId);
+
+ const parents = await cur.getParentNotes();
if (!parents.length) {
- utils.throwError("Can't find parents for noteId=" + noteId);
+ utils.throwError("Can't find parents for " + cur);
}
- if (parents.length <= 1) {
- $parentList.hide();
- }
- else {
- $parentList.show();
- $parentListList.empty();
-
- for (const parentNote of parents) {
- const parentNotePath = await getSomeNotePath(parentNote);
- // this is to avoid having root notes leading '/'
- const notePath = parentNotePath ? (parentNotePath + '/' + noteId) : noteId;
- const title = getNotePathTitle(notePath);
-
- let item;
-
- if (node.getParent().data.noteId === parentNote.noteId) {
- item = $("").attr("title", "Current note").append(title);
- }
- else {
- item = link.createNoteLink(notePath, title);
- }
-
- $parentListList.append($("").append(item));
- }
- }
+ cur = parents[0];
}
- function getNotePathTitle(notePath) {
- utils.assertArguments(notePath);
+ return path.reverse().join('/');
+}
- const titlePath = [];
+async function setExpandedToServer(branchId, isExpanded) {
+ utils.assertArguments(branchId);
- let parentNoteId = 'root';
+ const expandedNum = isExpanded ? 1 : 0;
- for (const noteId of notePath.split('/')) {
- titlePath.push(getNoteTitle(noteId, parentNoteId));
+ await server.put('tree/' + branchId + '/expanded/' + expandedNum);
+}
- parentNoteId = noteId;
- }
+function setCurrentNotePathToHash(node) {
+ utils.assertArguments(node);
- return titlePath.join(' / ');
+ const currentNotePath = treeUtils.getNotePath(node);
+ const currentBranchId = node.data.branchId;
+
+ document.location.hash = currentNotePath;
+
+ recentNotes.addRecentNote(currentBranchId, currentNotePath);
+}
+
+function getSelectedNodes(stopOnParents = false) {
+ return getTree().getSelectedNodes(stopOnParents);
+}
+
+function clearSelectedNodes() {
+ for (const selectedNode of getSelectedNodes()) {
+ selectedNode.setSelected(false);
}
- async function getSomeNotePath(note) {
- utils.assertArguments(note);
+ const currentNode = getCurrentNode();
- const path = [];
+ if (currentNode) {
+ currentNode.setSelected(true);
+ }
+}
- let cur = note;
+function initFancyTree(branch) {
+ utils.assertArguments(branch);
- while (cur.noteId !== 'root') {
- path.push(cur.noteId);
+ const keybindings = {
+ "del": node => {
+ treeChanges.deleteNodes(getSelectedNodes(true));
+ },
+ "ctrl+up": node => {
+ const beforeNode = node.getPrevSibling();
- const parents = await cur.getParentNotes();
-
- if (!parents.length) {
- utils.throwError("Can't find parents for " + cur);
+ if (beforeNode !== null) {
+ treeChanges.moveBeforeNode([node], beforeNode);
}
- cur = parents[0];
- }
-
- return path.reverse().join('/');
- }
-
- async function setExpandedToServer(branchId, isExpanded) {
- utils.assertArguments(branchId);
-
- const expandedNum = isExpanded ? 1 : 0;
-
- await server.put('tree/' + branchId + '/expanded/' + expandedNum);
- }
-
- function setCurrentNotePathToHash(node) {
- utils.assertArguments(node);
-
- const currentNotePath = treeUtils.getNotePath(node);
- const currentBranchId = node.data.branchId;
-
- document.location.hash = currentNotePath;
-
- recentNotes.addRecentNote(currentBranchId, currentNotePath);
- }
-
- function getSelectedNodes(stopOnParents = false) {
- return getTree().getSelectedNodes(stopOnParents);
- }
-
- function clearSelectedNodes() {
- for (const selectedNode of getSelectedNodes()) {
- selectedNode.setSelected(false);
- }
-
- const currentNode = getCurrentNode();
-
- if (currentNode) {
- currentNode.setSelected(true);
- }
- }
-
- function initFancyTree(branch) {
- utils.assertArguments(branch);
-
- const keybindings = {
- "del": node => {
- treeChanges.deleteNodes(getSelectedNodes(true));
- },
- "ctrl+up": node => {
- const beforeNode = node.getPrevSibling();
-
- if (beforeNode !== null) {
- treeChanges.moveBeforeNode([node], beforeNode);
- }
-
- return false;
- },
- "ctrl+down": node => {
- let afterNode = node.getNextSibling();
- if (afterNode !== null) {
- treeChanges.moveAfterNode([node], afterNode);
- }
-
- return false;
- },
- "ctrl+left": node => {
- treeChanges.moveNodeUpInHierarchy(node);
-
- return false;
- },
- "ctrl+right": node => {
- let toNode = node.getPrevSibling();
-
- if (toNode !== null) {
- treeChanges.moveToNode([node], toNode);
- }
-
- return false;
- },
- "shift+up": node => {
- node.navigate($.ui.keyCode.UP, true).then(() => {
- const currentNode = getCurrentNode();
-
- if (currentNode.isSelected()) {
- node.setSelected(false);
- }
-
- currentNode.setSelected(true);
- });
-
- return false;
- },
- "shift+down": node => {
- node.navigate($.ui.keyCode.DOWN, true).then(() => {
- const currentNode = getCurrentNode();
-
- if (currentNode.isSelected()) {
- node.setSelected(false);
- }
-
- currentNode.setSelected(true);
- });
-
- return false;
- },
- "f2": node => {
- editTreePrefix.showDialog(node);
- },
- "alt+-": node => {
- collapseTree(node);
- },
- "alt+s": node => {
- sortAlphabetically(node.data.noteId);
-
- return false;
- },
- "ctrl+a": node => {
- for (const child of node.getParent().getChildren()) {
- child.setSelected(true);
- }
-
- return false;
- },
- "ctrl+c": () => {
- contextMenu.copy(getSelectedNodes());
-
- return false;
- },
- "ctrl+x": () => {
- contextMenu.cut(getSelectedNodes());
-
- return false;
- },
- "ctrl+v": node => {
- contextMenu.pasteInto(node);
-
- return false;
- },
- "return": node => {
- noteEditor.focus();
-
- return false;
- },
- "backspace": node => {
- if (!utils.isTopLevelNode(node)) {
- node.getParent().setActive().then(() => clearSelectedNodes());
- }
- },
- // code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
- // after opening context menu, standard shortcuts don't work, but they are detected here
- // so we essentially takeover the standard handling with our implementation.
- "left": node => {
- node.navigate($.ui.keyCode.LEFT, true).then(() => clearSelectedNodes());
-
- return false;
- },
- "right": node => {
- node.navigate($.ui.keyCode.RIGHT, true).then(() => clearSelectedNodes());
-
- return false;
- },
- "up": node => {
- node.navigate($.ui.keyCode.UP, true).then(() => clearSelectedNodes());
-
- return false;
- },
- "down": node => {
- node.navigate($.ui.keyCode.DOWN, true).then(() => clearSelectedNodes());
-
- return false;
- }
- };
-
- $tree.fancytree({
- autoScroll: true,
- keyboard: false, // we takover keyboard handling in the hotkeys plugin
- extensions: ["hotkeys", "filter", "dnd", "clones"],
- source: branch,
- scrollParent: $("#tree"),
- click: (event, data) => {
- const targetType = data.targetType;
- const node = data.node;
-
- if (targetType === 'title' || targetType === 'icon') {
- if (!event.ctrlKey) {
- node.setActive();
- node.setSelected(true);
-
- clearSelectedNodes();
- }
- else {
- node.setSelected(!node.isSelected());
- }
-
- return false;
- }
- },
- activate: (event, data) => {
- const node = data.node.data;
-
- setCurrentNotePathToHash(data.node);
-
- noteEditor.switchToNote(node.noteId);
-
- showParentList(node.noteId, data.node);
- },
- expand: (event, data) => {
- setExpandedToServer(data.node.data.branchId, true);
- },
- collapse: (event, data) => {
- setExpandedToServer(data.node.data.branchId, false);
- },
- init: (event, data) => {
- const noteId = treeUtils.getNoteIdFromNotePath(startNotePath);
-
- if (treeCache.getNote(noteId) === undefined) {
- // note doesn't exist so don't try to activate it
- startNotePath = null;
- }
-
- if (startNotePath) {
- activateNode(startNotePath);
-
- // looks like this this doesn't work when triggered immediatelly after activating node
- // so waiting a second helps
- setTimeout(scrollToCurrentNote, 1000);
- }
- },
- hotkeys: {
- keydown: keybindings
- },
- filter: {
- autoApply: true, // Re-apply last filter if lazy data is loaded
- autoExpand: true, // Expand all branches that contain matches while filtered
- counter: false, // Show a badge with number of matching child nodes near parent icons
- fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
- hideExpandedCounter: true, // Hide counter badge if parent is expanded
- hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
- highlight: true, // Highlight matches by wrapping inside tags
- leavesOnly: false, // Match end nodes only
- nodata: true, // Display a 'no data' status node if result is empty
- mode: "hide" // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
- },
- dnd: dragAndDropSetup,
- lazyLoad: function(event, data){
- const noteId = data.node.data.noteId;
- const note = getNote(noteId);
-
- if (note.type === 'search') {
- data.result = loadSearchNote(noteId);
- }
- else {
- data.result = prepareBranchInner(note);
- }
- },
- clones: {
- highlightActiveClones: true
- }
- });
-
- $tree.contextmenu(contextMenu.contextMenuSettings);
- }
-
- async function loadSearchNote(searchNoteId) {
- const note = await server.get('notes/' + searchNoteId);
-
- const json = JSON.parse(note.detail.content);
-
- const noteIds = await server.get('search/' + encodeURIComponent(json.searchString));
-
- for (const noteId of noteIds) {
- const branchId = "virt" + utils.randomString(10);
-
- treeCache.addBranch({
- branchId: branchId,
- noteId: noteId,
- parentNoteId: searchNoteId,
- prefix: '',
- virtual: true
- });
- }
-
- return await prepareBranchInner(treeCache.getNote(searchNoteId));
- }
-
- function getTree() {
- return $tree.fancytree('getTree');
- }
-
- async function reload() {
- const notes = await loadTree();
-
- // this will also reload the note content
- await getTree().reload(notes);
- }
-
- function getNotePathFromAddress() {
- return document.location.hash.substr(1); // strip initial #
- }
-
- async function loadTree() {
- const resp = await server.get('tree');
- startNotePath = resp.start_note_path;
- instanceName = resp.instanceName;
-
- if (document.location.hash) {
- startNotePath = getNotePathFromAddress();
- }
-
- return await prepareBranch(resp.notes, resp.branches);
- }
-
- $(() => loadTree().then(branch => initFancyTree(branch)));
-
- function collapseTree(node = null) {
- if (!node) {
- node = $tree.fancytree("getRootNode");
- }
-
- node.setExpanded(false);
-
- node.visit(node => node.setExpanded(false));
- }
-
- $(document).bind('keydown', 'alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument
-
- function scrollToCurrentNote() {
- const node = getCurrentNode();
-
- if (node) {
- node.makeVisible({scrollIntoView: true});
-
- node.setFocus();
- }
- }
-
- function setBranchBackgroundBasedOnProtectedStatus(noteId) {
- getNodesByNoteId(noteId).map(node => node.toggleClass("protected", !!node.data.isProtected));
- }
-
- function setProtected(noteId, isProtected) {
- getNodesByNoteId(noteId).map(node => node.data.isProtected = isProtected);
-
- setBranchBackgroundBasedOnProtectedStatus(noteId);
- }
-
- async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
- if (!parentNoteId) {
- parentNoteId = 'root';
- }
-
- const parentNote = treeCache.getNote(parentNoteId);
- const childNotes = await parentNote.getChildNotes();
-
- if (!childNotes.length) {
- return [];
- }
-
- if (!notePath) {
- notePath = '';
- }
-
- if (!titlePath) {
- titlePath = '';
- }
-
- // https://github.com/zadam/trilium/issues/46
- // unfortunately not easy to implement because we don't have an easy access to note's isProtected property
-
- const autocompleteItems = [];
-
- for (const childNote of childNotes) {
- if (childNote.hideInAutocomplete) {
- continue;
+ return false;
+ },
+ "ctrl+down": node => {
+ let afterNode = node.getNextSibling();
+ if (afterNode !== null) {
+ treeChanges.moveAfterNode([node], afterNode);
}
- const childNotePath = (notePath ? (notePath + '/') : '') + childNote.noteId;
- const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNote.noteId, parentNoteId);
+ return false;
+ },
+ "ctrl+left": node => {
+ treeChanges.moveNodeUpInHierarchy(node);
- autocompleteItems.push({
- value: childTitlePath + ' (' + childNotePath + ')',
- label: childTitlePath
+ return false;
+ },
+ "ctrl+right": node => {
+ let toNode = node.getPrevSibling();
+
+ if (toNode !== null) {
+ treeChanges.moveToNode([node], toNode);
+ }
+
+ return false;
+ },
+ "shift+up": node => {
+ node.navigate($.ui.keyCode.UP, true).then(() => {
+ const currentNode = getCurrentNode();
+
+ if (currentNode.isSelected()) {
+ node.setSelected(false);
+ }
+
+ currentNode.setSelected(true);
});
- const childItems = await getAutocompleteItems(childNote.noteId, childNotePath, childTitlePath);
+ return false;
+ },
+ "shift+down": node => {
+ node.navigate($.ui.keyCode.DOWN, true).then(() => {
+ const currentNode = getCurrentNode();
- for (const childItem of childItems) {
- autocompleteItems.push(childItem);
+ if (currentNode.isSelected()) {
+ node.setSelected(false);
+ }
+
+ currentNode.setSelected(true);
+ });
+
+ return false;
+ },
+ "f2": node => {
+ editTreePrefix.showDialog(node);
+ },
+ "alt+-": node => {
+ collapseTree(node);
+ },
+ "alt+s": node => {
+ sortAlphabetically(node.data.noteId);
+
+ return false;
+ },
+ "ctrl+a": node => {
+ for (const child of node.getParent().getChildren()) {
+ child.setSelected(true);
}
+
+ return false;
+ },
+ "ctrl+c": () => {
+ contextMenu.copy(getSelectedNodes());
+
+ return false;
+ },
+ "ctrl+x": () => {
+ contextMenu.cut(getSelectedNodes());
+
+ return false;
+ },
+ "ctrl+v": node => {
+ contextMenu.pasteInto(node);
+
+ return false;
+ },
+ "return": node => {
+ noteEditor.focus();
+
+ return false;
+ },
+ "backspace": node => {
+ if (!utils.isTopLevelNode(node)) {
+ node.getParent().setActive().then(() => clearSelectedNodes());
+ }
+ },
+ // code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
+ // after opening context menu, standard shortcuts don't work, but they are detected here
+ // so we essentially takeover the standard handling with our implementation.
+ "left": node => {
+ node.navigate($.ui.keyCode.LEFT, true).then(() => clearSelectedNodes());
+
+ return false;
+ },
+ "right": node => {
+ node.navigate($.ui.keyCode.RIGHT, true).then(() => clearSelectedNodes());
+
+ return false;
+ },
+ "up": node => {
+ node.navigate($.ui.keyCode.UP, true).then(() => clearSelectedNodes());
+
+ return false;
+ },
+ "down": node => {
+ node.navigate($.ui.keyCode.DOWN, true).then(() => clearSelectedNodes());
+
+ return false;
}
+ };
- return autocompleteItems;
- }
+ $tree.fancytree({
+ autoScroll: true,
+ keyboard: false, // we takover keyboard handling in the hotkeys plugin
+ extensions: ["hotkeys", "filter", "dnd", "clones"],
+ source: branch,
+ scrollParent: $("#tree"),
+ click: (event, data) => {
+ const targetType = data.targetType;
+ const node = data.node;
- function setNoteTitle(noteId, title) {
- utils.assertArguments(noteId);
+ if (targetType === 'title' || targetType === 'icon') {
+ if (!event.ctrlKey) {
+ node.setActive();
+ node.setSelected(true);
- getNote(noteId).title = title;
+ clearSelectedNodes();
+ }
+ else {
+ node.setSelected(!node.isSelected());
+ }
- getNodesByNoteId(noteId).map(clone => setNodeTitleWithPrefix(clone));
- }
+ return false;
+ }
+ },
+ activate: (event, data) => {
+ const node = data.node.data;
- async function createNewTopLevelNote() {
- const rootNode = $tree.fancytree("getRootNode");
+ setCurrentNotePathToHash(data.node);
- await createNote(rootNode, "root", "into");
- }
+ noteEditor.switchToNote(node.noteId);
- async function createNote(node, parentNoteId, target, isProtected) {
- utils.assertArguments(node, parentNoteId, target);
+ showParentList(node.noteId, data.node);
+ },
+ expand: (event, data) => {
+ setExpandedToServer(data.node.data.branchId, true);
+ },
+ collapse: (event, data) => {
+ setExpandedToServer(data.node.data.branchId, false);
+ },
+ init: (event, data) => {
+ const noteId = treeUtils.getNoteIdFromNotePath(startNotePath);
- // if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted
- // but this is quite weird since user doesn't see WHERE the note is being created so it shouldn't occur often
- if (!isProtected || !protected_session.isProtectedSessionAvailable()) {
- isProtected = false;
- }
+ if (treeCache.getNote(noteId) === undefined) {
+ // note doesn't exist so don't try to activate it
+ startNotePath = null;
+ }
- const newNoteName = "new note";
+ if (startNotePath) {
+ activateNode(startNotePath);
- const result = await server.post('notes/' + parentNoteId + '/children', {
- title: newNoteName,
- target: target,
- target_branchId: node.data.branchId,
- isProtected: isProtected
- });
+ // looks like this this doesn't work when triggered immediatelly after activating node
+ // so waiting a second helps
+ setTimeout(scrollToCurrentNote, 1000);
+ }
+ },
+ hotkeys: {
+ keydown: keybindings
+ },
+ filter: {
+ autoApply: true, // Re-apply last filter if lazy data is loaded
+ autoExpand: true, // Expand all branches that contain matches while filtered
+ counter: false, // Show a badge with number of matching child nodes near parent icons
+ fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
+ hideExpandedCounter: true, // Hide counter badge if parent is expanded
+ hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
+ highlight: true, // Highlight matches by wrapping inside tags
+ leavesOnly: false, // Match end nodes only
+ nodata: true, // Display a 'no data' status node if result is empty
+ mode: "hide" // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
+ },
+ dnd: dragAndDropSetup,
+ lazyLoad: function(event, data){
+ const noteId = data.node.data.noteId;
+ const note = getNote(noteId);
- const note = new NoteShort(treeCache, {
- noteId: result.noteId,
- title: result.title,
- isProtected: result.isProtected,
- type: result.type,
- mime: result.mime
- });
-
- const branch = new Branch(treeCache, result);
-
- treeCache.add(note, branch);
-
- noteEditor.newNoteCreated();
-
- const newNode = {
- title: newNoteName,
- noteId: result.noteId,
- parentNoteId: parentNoteId,
- refKey: result.noteId,
- branchId: result.branchId,
- isProtected: isProtected,
- extraClasses: await getExtraClasses(note)
- };
-
- if (target === 'after') {
- await node.appendSibling(newNode).setActive(true);
- }
- else if (target === 'into') {
- if (!node.getChildren() && node.isFolder()) {
- await node.setExpanded();
+ if (note.type === 'search') {
+ data.result = loadSearchNote(noteId);
}
else {
- node.addChildren(newNode);
+ data.result = prepareBranchInner(note);
}
+ },
+ clones: {
+ highlightActiveClones: true
+ }
+ });
- await node.getLastChild().setActive(true);
+ $tree.contextmenu(contextMenu.contextMenuSettings);
+}
- node.folder = true;
- node.renderTitle();
+async function loadSearchNote(searchNoteId) {
+ const note = await server.get('notes/' + searchNoteId);
+
+ const json = JSON.parse(note.detail.content);
+
+ const noteIds = await server.get('search/' + encodeURIComponent(json.searchString));
+
+ for (const noteId of noteIds) {
+ const branchId = "virt" + utils.randomString(10);
+
+ treeCache.addBranch({
+ branchId: branchId,
+ noteId: noteId,
+ parentNoteId: searchNoteId,
+ prefix: '',
+ virtual: true
+ });
+ }
+
+ return await prepareBranchInner(treeCache.getNote(searchNoteId));
+}
+
+function getTree() {
+ return $tree.fancytree('getTree');
+}
+
+async function reload() {
+ const notes = await loadTree();
+
+ // this will also reload the note content
+ await getTree().reload(notes);
+}
+
+function getNotePathFromAddress() {
+ return document.location.hash.substr(1); // strip initial #
+}
+
+async function loadTree() {
+ const resp = await server.get('tree');
+ startNotePath = resp.start_note_path;
+ instanceName = resp.instanceName;
+
+ if (document.location.hash) {
+ startNotePath = getNotePathFromAddress();
+ }
+
+ return await prepareBranch(resp.notes, resp.branches);
+}
+
+$(() => loadTree().then(branch => initFancyTree(branch)));
+
+function collapseTree(node = null) {
+ if (!node) {
+ node = $tree.fancytree("getRootNode");
+ }
+
+ node.setExpanded(false);
+
+ node.visit(node => node.setExpanded(false));
+}
+
+$(document).bind('keydown', 'alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument
+
+function scrollToCurrentNote() {
+ const node = getCurrentNode();
+
+ if (node) {
+ node.makeVisible({scrollIntoView: true});
+
+ node.setFocus();
+ }
+}
+
+function setBranchBackgroundBasedOnProtectedStatus(noteId) {
+ getNodesByNoteId(noteId).map(node => node.toggleClass("protected", !!node.data.isProtected));
+}
+
+function setProtected(noteId, isProtected) {
+ getNodesByNoteId(noteId).map(node => node.data.isProtected = isProtected);
+
+ setBranchBackgroundBasedOnProtectedStatus(noteId);
+}
+
+async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
+ if (!parentNoteId) {
+ parentNoteId = 'root';
+ }
+
+ const parentNote = treeCache.getNote(parentNoteId);
+ const childNotes = await parentNote.getChildNotes();
+
+ if (!childNotes.length) {
+ return [];
+ }
+
+ if (!notePath) {
+ notePath = '';
+ }
+
+ if (!titlePath) {
+ titlePath = '';
+ }
+
+ // https://github.com/zadam/trilium/issues/46
+ // unfortunately not easy to implement because we don't have an easy access to note's isProtected property
+
+ const autocompleteItems = [];
+
+ for (const childNote of childNotes) {
+ if (childNote.hideInAutocomplete) {
+ continue;
+ }
+
+ const childNotePath = (notePath ? (notePath + '/') : '') + childNote.noteId;
+ const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNote.noteId, parentNoteId);
+
+ autocompleteItems.push({
+ value: childTitlePath + ' (' + childNotePath + ')',
+ label: childTitlePath
+ });
+
+ const childItems = await getAutocompleteItems(childNote.noteId, childNotePath, childTitlePath);
+
+ for (const childItem of childItems) {
+ autocompleteItems.push(childItem);
+ }
+ }
+
+ return autocompleteItems;
+}
+
+function setNoteTitle(noteId, title) {
+ utils.assertArguments(noteId);
+
+ getNote(noteId).title = title;
+
+ getNodesByNoteId(noteId).map(clone => setNodeTitleWithPrefix(clone));
+}
+
+async function createNewTopLevelNote() {
+ const rootNode = $tree.fancytree("getRootNode");
+
+ await createNote(rootNode, "root", "into");
+}
+
+async function createNote(node, parentNoteId, target, isProtected) {
+ utils.assertArguments(node, parentNoteId, target);
+
+ // if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted
+ // but this is quite weird since user doesn't see WHERE the note is being created so it shouldn't occur often
+ if (!isProtected || !protected_session.isProtectedSessionAvailable()) {
+ isProtected = false;
+ }
+
+ const newNoteName = "new note";
+
+ const result = await server.post('notes/' + parentNoteId + '/children', {
+ title: newNoteName,
+ target: target,
+ target_branchId: node.data.branchId,
+ isProtected: isProtected
+ });
+
+ const note = new NoteShort(treeCache, {
+ noteId: result.noteId,
+ title: result.title,
+ isProtected: result.isProtected,
+ type: result.type,
+ mime: result.mime
+ });
+
+ const branch = new Branch(treeCache, result);
+
+ treeCache.add(note, branch);
+
+ noteEditor.newNoteCreated();
+
+ const newNode = {
+ title: newNoteName,
+ noteId: result.noteId,
+ parentNoteId: parentNoteId,
+ refKey: result.noteId,
+ branchId: result.branchId,
+ isProtected: isProtected,
+ extraClasses: await getExtraClasses(note)
+ };
+
+ if (target === 'after') {
+ await node.appendSibling(newNode).setActive(true);
+ }
+ else if (target === 'into') {
+ if (!node.getChildren() && node.isFolder()) {
+ await node.setExpanded();
}
else {
- utils.throwError("Unrecognized target: " + target);
+ node.addChildren(newNode);
}
- clearSelectedNodes(); // to unmark previously active node
+ await node.getLastChild().setActive(true);
- utils.showMessage("Created!");
+ node.folder = true;
+ node.renderTitle();
+ }
+ else {
+ utils.throwError("Unrecognized target: " + target);
}
- async function sortAlphabetically(noteId) {
- await server.put('notes/' + noteId + '/sort');
+ clearSelectedNodes(); // to unmark previously active node
- await reload();
+ utils.showMessage("Created!");
+}
+
+async function sortAlphabetically(noteId) {
+ await server.put('notes/' + noteId + '/sort');
+
+ await reload();
+}
+
+async function noteExists(noteId) {
+ return !!treeCache.getNote(noteId);
+}
+
+function getInstanceName() {
+ return instanceName;
+}
+
+function getBranch(branchId) {
+ return branchMap[branchId];
+}
+
+$(document).bind('keydown', 'ctrl+o', e => {
+ const node = getCurrentNode();
+ const parentNoteId = node.data.parentNoteId;
+ const isProtected = treeUtils.getParentProtectedStatus(node);
+
+ createNote(node, parentNoteId, 'after', isProtected);
+
+ e.preventDefault();
+});
+
+$(document).bind('keydown', 'ctrl+p', e => {
+ const node = getCurrentNode();
+
+ createNote(node, node.data.noteId, 'into', node.data.isProtected);
+
+ e.preventDefault();
+});
+
+$(document).bind('keydown', 'ctrl+del', e => {
+ const node = getCurrentNode();
+
+ treeChanges.deleteNodes([node]);
+
+ e.preventDefault();
+});
+
+$(document).bind('keydown', 'ctrl+.', scrollToCurrentNote);
+
+$(window).bind('hashchange', function() {
+ const notePath = getNotePathFromAddress();
+
+ if (getCurrentNotePath() !== notePath) {
+ console.log("Switching to " + notePath + " because of hash change");
+
+ activateNode(notePath);
}
+});
- async function noteExists(noteId) {
- return !!treeCache.getNote(noteId);
- }
-
- function getInstanceName() {
- return instanceName;
- }
-
- function getBranch(branchId) {
- return branchMap[branchId];
- }
-
- $(document).bind('keydown', 'ctrl+o', e => {
- const node = getCurrentNode();
- const parentNoteId = node.data.parentNoteId;
- const isProtected = treeUtils.getParentProtectedStatus(node);
-
- createNote(node, parentNoteId, 'after', isProtected);
+if (utils.isElectron()) {
+ $(document).bind('keydown', 'alt+left', e => {
+ window.history.back();
e.preventDefault();
});
- $(document).bind('keydown', 'ctrl+p', e => {
- const node = getCurrentNode();
-
- createNote(node, node.data.noteId, 'into', node.data.isProtected);
+ $(document).bind('keydown', 'alt+right', e => {
+ window.history.forward();
e.preventDefault();
});
+}
- $(document).bind('keydown', 'ctrl+del', e => {
- const node = getCurrentNode();
+$createTopLevelNoteButton.click(createNewTopLevelNote);
+$collapseTreeButton.click(collapseTree);
+$scrollToCurrentNoteButton.click(scrollToCurrentNote);
- treeChanges.deleteNodes([node]);
-
- e.preventDefault();
- });
-
- $(document).bind('keydown', 'ctrl+.', scrollToCurrentNote);
-
- $(window).bind('hashchange', function() {
- const notePath = getNotePathFromAddress();
-
- if (getCurrentNotePath() !== notePath) {
- console.log("Switching to " + notePath + " because of hash change");
-
- activateNode(notePath);
- }
- });
-
- if (utils.isElectron()) {
- $(document).bind('keydown', 'alt+left', e => {
- window.history.back();
-
- e.preventDefault();
- });
-
- $(document).bind('keydown', 'alt+right', e => {
- window.history.forward();
-
- e.preventDefault();
- });
- }
-
- $createTopLevelNoteButton.click(createNewTopLevelNote);
- $collapseTreeButton.click(collapseTree);
- $scrollToCurrentNoteButton.click(scrollToCurrentNote);
-
- return {
- reload,
- collapseTree,
- scrollToCurrentNote,
- setBranchBackgroundBasedOnProtectedStatus,
- setProtected,
- getCurrentNode,
- expandToNote,
- activateNode,
- getCurrentNotePath,
- getNoteTitle,
- setCurrentNotePathToHash,
- getAutocompleteItems,
- setNoteTitle,
- createNewTopLevelNote,
- createNote,
- setPrefix,
- getNotePathTitle,
- removeParentChildRelation,
- setParentChildRelation,
- getSelectedNodes,
- sortAlphabetically,
- noteExists,
- getInstanceName,
- getBranch,
- getNote
- };
-})();
\ No newline at end of file
+export default {
+ reload,
+ collapseTree,
+ scrollToCurrentNote,
+ setBranchBackgroundBasedOnProtectedStatus,
+ setProtected,
+ getCurrentNode,
+ expandToNote,
+ activateNode,
+ getCurrentNotePath,
+ getNoteTitle,
+ setCurrentNotePathToHash,
+ getAutocompleteItems,
+ setNoteTitle,
+ createNewTopLevelNote,
+ createNote,
+ setPrefix,
+ getNotePathTitle,
+ removeParentChildRelation,
+ setParentChildRelation,
+ getSelectedNodes,
+ sortAlphabetically,
+ noteExists,
+ getInstanceName,
+ getBranch,
+ getNote
+};
\ No newline at end of file
diff --git a/src/public/javascripts/note_type.js b/src/public/javascripts/note_type.js
index 9403da589..980d7f7bb 100644
--- a/src/public/javascripts/note_type.js
+++ b/src/public/javascripts/note_type.js
@@ -1,145 +1,147 @@
"use strict";
-const noteType = (function() {
- const $executeScriptButton = $("#execute-script-button");
- const noteTypeModel = new NoteTypeModel();
+import treeService from './note_tree.js';
+import noteEditor from './note_editor.js';
+import utils from './utils.js';
- function NoteTypeModel() {
- const self = this;
+const $executeScriptButton = $("#execute-script-button");
+const noteTypeModel = new NoteTypeModel();
- this.type = ko.observable('text');
- this.mime = ko.observable('');
+function NoteTypeModel() {
+ const self = this;
- this.codeMimeTypes = ko.observableArray([
- { mime: 'text/x-csrc', title: 'C' },
- { mime: 'text/x-c++src', title: 'C++' },
- { mime: 'text/x-csharp', title: 'C#' },
- { mime: 'text/x-clojure', title: 'Clojure' },
- { mime: 'text/css', title: 'CSS' },
- { mime: 'text/x-dockerfile', title: 'Dockerfile' },
- { mime: 'text/x-erlang', title: 'Erlang' },
- { mime: 'text/x-feature', title: 'Gherkin' },
- { mime: 'text/x-go', title: 'Go' },
- { mime: 'text/x-groovy', title: 'Groovy' },
- { mime: 'text/x-haskell', title: 'Haskell' },
- { mime: 'text/html', title: 'HTML' },
- { mime: 'message/http', title: 'HTTP' },
- { mime: 'text/x-java', title: 'Java' },
- { mime: 'application/javascript;env=frontend', title: 'JavaScript frontend' },
- { mime: 'application/javascript;env=backend', title: 'JavaScript backend' },
- { mime: 'application/json', title: 'JSON' },
- { mime: 'text/x-kotlin', title: 'Kotlin' },
- { mime: 'text/x-lua', title: 'Lua' },
- { mime: 'text/x-markdown', title: 'Markdown' },
- { mime: 'text/x-objectivec', title: 'Objective C' },
- { mime: 'text/x-pascal', title: 'Pascal' },
- { mime: 'text/x-perl', title: 'Perl' },
- { mime: 'text/x-php', title: 'PHP' },
- { mime: 'text/x-python', title: 'Python' },
- { mime: 'text/x-ruby', title: 'Ruby' },
- { mime: 'text/x-rustsrc', title: 'Rust' },
- { mime: 'text/x-scala', title: 'Scala' },
- { mime: 'text/x-sh', title: 'Shell' },
- { mime: 'text/x-sql', title: 'SQL' },
- { mime: 'text/x-swift', title: 'Swift' },
- { mime: 'text/xml', title: 'XML' },
- { mime: 'text/x-yaml', title: 'YAML' }
- ]);
+ this.type = ko.observable('text');
+ this.mime = ko.observable('');
- this.typeString = function() {
- const type = self.type();
- const mime = self.mime();
+ this.codeMimeTypes = ko.observableArray([
+ { mime: 'text/x-csrc', title: 'C' },
+ { mime: 'text/x-c++src', title: 'C++' },
+ { mime: 'text/x-csharp', title: 'C#' },
+ { mime: 'text/x-clojure', title: 'Clojure' },
+ { mime: 'text/css', title: 'CSS' },
+ { mime: 'text/x-dockerfile', title: 'Dockerfile' },
+ { mime: 'text/x-erlang', title: 'Erlang' },
+ { mime: 'text/x-feature', title: 'Gherkin' },
+ { mime: 'text/x-go', title: 'Go' },
+ { mime: 'text/x-groovy', title: 'Groovy' },
+ { mime: 'text/x-haskell', title: 'Haskell' },
+ { mime: 'text/html', title: 'HTML' },
+ { mime: 'message/http', title: 'HTTP' },
+ { mime: 'text/x-java', title: 'Java' },
+ { mime: 'application/javascript;env=frontend', title: 'JavaScript frontend' },
+ { mime: 'application/javascript;env=backend', title: 'JavaScript backend' },
+ { mime: 'application/json', title: 'JSON' },
+ { mime: 'text/x-kotlin', title: 'Kotlin' },
+ { mime: 'text/x-lua', title: 'Lua' },
+ { mime: 'text/x-markdown', title: 'Markdown' },
+ { mime: 'text/x-objectivec', title: 'Objective C' },
+ { mime: 'text/x-pascal', title: 'Pascal' },
+ { mime: 'text/x-perl', title: 'Perl' },
+ { mime: 'text/x-php', title: 'PHP' },
+ { mime: 'text/x-python', title: 'Python' },
+ { mime: 'text/x-ruby', title: 'Ruby' },
+ { mime: 'text/x-rustsrc', title: 'Rust' },
+ { mime: 'text/x-scala', title: 'Scala' },
+ { mime: 'text/x-sh', title: 'Shell' },
+ { mime: 'text/x-sql', title: 'SQL' },
+ { mime: 'text/x-swift', title: 'Swift' },
+ { mime: 'text/xml', title: 'XML' },
+ { mime: 'text/x-yaml', title: 'YAML' }
+ ]);
- if (type === 'text') {
- return 'Text';
- }
- else if (type === 'code') {
- if (!mime) {
- return 'Code';
- }
- else {
- const found = self.codeMimeTypes().find(x => x.mime === mime);
+ this.typeString = function() {
+ const type = self.type();
+ const mime = self.mime();
- return found ? found.title : mime;
- }
- }
- else if (type === 'render') {
- return 'Render HTML note';
- }
- else if (type === 'file') {
- return 'Attachment';
- }
- else if (type === 'search') {
- // ignore and do nothing, "type" will be hidden since it's not possible to switch to and from search
+ if (type === 'text') {
+ return 'Text';
+ }
+ else if (type === 'code') {
+ if (!mime) {
+ return 'Code';
}
else {
- utils.throwError('Unrecognized type: ' + type);
+ const found = self.codeMimeTypes().find(x => x.mime === mime);
+
+ return found ? found.title : mime;
}
- };
-
- this.isDisabled = function() {
- return self.type() === "file";
- };
-
- async function save() {
- const note = noteEditor.getCurrentNote();
-
- await server.put('notes/' + note.detail.noteId
- + '/type/' + encodeURIComponent(self.type())
- + '/mime/' + encodeURIComponent(self.mime()));
-
- await noteEditor.reload();
-
- // for the note icon to be updated in the tree
- await treeService.reload();
-
- self.updateExecuteScriptButtonVisibility();
}
-
- this.selectText = function() {
- self.type('text');
- self.mime('');
-
- save();
- };
-
- this.selectRender = function() {
- self.type('render');
- self.mime('');
-
- save();
- };
-
- this.selectCode = function() {
- self.type('code');
- self.mime('');
-
- save();
- };
-
- this.selectCodeMime = function(el) {
- self.type('code');
- self.mime(el.mime);
-
- save();
- };
-
- this.updateExecuteScriptButtonVisibility = function() {
- $executeScriptButton.toggle(self.mime().startsWith('application/javascript'));
+ else if (type === 'render') {
+ return 'Render HTML note';
}
- }
-
- ko.applyBindings(noteTypeModel, document.getElementById('note-type'));
-
- return {
- getNoteType: () => noteTypeModel.type(),
- setNoteType: type => noteTypeModel.type(type),
-
- getNoteMime: () => noteTypeModel.mime(),
- setNoteMime: mime => {
- noteTypeModel.mime(mime);
-
- noteTypeModel.updateExecuteScriptButtonVisibility();
+ else if (type === 'file') {
+ return 'Attachment';
+ }
+ else if (type === 'search') {
+ // ignore and do nothing, "type" will be hidden since it's not possible to switch to and from search
+ }
+ else {
+ utils.throwError('Unrecognized type: ' + type);
}
};
-})();
\ No newline at end of file
+
+ this.isDisabled = function() {
+ return self.type() === "file";
+ };
+
+ async function save() {
+ const note = noteEditor.getCurrentNote();
+
+ await server.put('notes/' + note.detail.noteId
+ + '/type/' + encodeURIComponent(self.type())
+ + '/mime/' + encodeURIComponent(self.mime()));
+
+ await noteEditor.reload();
+
+ // for the note icon to be updated in the tree
+ await treeService.reload();
+
+ self.updateExecuteScriptButtonVisibility();
+ }
+
+ this.selectText = function() {
+ self.type('text');
+ self.mime('');
+
+ save();
+ };
+
+ this.selectRender = function() {
+ self.type('render');
+ self.mime('');
+
+ save();
+ };
+
+ this.selectCode = function() {
+ self.type('code');
+ self.mime('');
+
+ save();
+ };
+
+ this.selectCodeMime = function(el) {
+ self.type('code');
+ self.mime(el.mime);
+
+ save();
+ };
+
+ this.updateExecuteScriptButtonVisibility = function() {
+ $executeScriptButton.toggle(self.mime().startsWith('application/javascript'));
+ }
+}
+
+ko.applyBindings(noteTypeModel, document.getElementById('note-type'));
+
+export default {
+ getNoteType: () => noteTypeModel.type(),
+ setNoteType: type => noteTypeModel.type(type),
+
+ getNoteMime: () => noteTypeModel.mime(),
+ setNoteMime: mime => {
+ noteTypeModel.mime(mime);
+
+ noteTypeModel.updateExecuteScriptButtonVisibility();
+ }
+};
\ No newline at end of file
diff --git a/src/public/javascripts/protected_session.js b/src/public/javascripts/protected_session.js
index 7402d114f..9f66e53f4 100644
--- a/src/public/javascripts/protected_session.js
+++ b/src/public/javascripts/protected_session.js
@@ -1,189 +1,192 @@
"use strict";
-const protected_session = (function() {
- const $dialog = $("#protected-session-password-dialog");
- const $passwordForm = $("#protected-session-password-form");
- const $password = $("#protected-session-password");
- const $noteDetailWrapper = $("#note-detail-wrapper");
- const $protectButton = $("#protect-button");
- const $unprotectButton = $("#unprotect-button");
+import treeService from './note_tree.js';
+import noteEditor from './note_editor.js';
+import utils from './utils.js';
+import server from './server.js';
- let protectedSessionDeferred = null;
- let lastProtectedSessionOperationDate = null;
- let protectedSessionTimeout = null;
- let protectedSessionId = null;
+const $dialog = $("#protected-session-password-dialog");
+const $passwordForm = $("#protected-session-password-form");
+const $password = $("#protected-session-password");
+const $noteDetailWrapper = $("#note-detail-wrapper");
+const $protectButton = $("#protect-button");
+const $unprotectButton = $("#unprotect-button");
- $(document).ready(() => {
- server.get('settings/all').then(settings => protectedSessionTimeout = settings.protected_session_timeout);
- });
+let protectedSessionDeferred = null;
+let lastProtectedSessionOperationDate = null;
+let protectedSessionTimeout = null;
+let protectedSessionId = null;
- function setProtectedSessionTimeout(encSessTimeout) {
- protectedSessionTimeout = encSessTimeout;
- }
+$(document).ready(() => {
+ server.get('settings/all').then(settings => protectedSessionTimeout = settings.protected_session_timeout);
+});
- function ensureProtectedSession(requireProtectedSession, modal) {
- const dfd = $.Deferred();
+function setProtectedSessionTimeout(encSessTimeout) {
+ protectedSessionTimeout = encSessTimeout;
+}
- if (requireProtectedSession && !isProtectedSessionAvailable()) {
- protectedSessionDeferred = dfd;
+function ensureProtectedSession(requireProtectedSession, modal) {
+ const dfd = $.Deferred();
- if (treeService.getCurrentNode().data.isProtected) {
- $noteDetailWrapper.hide();
- }
+ if (requireProtectedSession && !isProtectedSessionAvailable()) {
+ protectedSessionDeferred = dfd;
- $dialog.dialog({
- modal: modal,
- width: 400,
- open: () => {
- if (!modal) {
- // dialog steals focus for itself, which is not what we want for non-modal (viewing)
- treeService.getCurrentNode().setFocus();
- }
+ if (treeService.getCurrentNode().data.isProtected) {
+ $noteDetailWrapper.hide();
+ }
+
+ $dialog.dialog({
+ modal: modal,
+ width: 400,
+ open: () => {
+ if (!modal) {
+ // dialog steals focus for itself, which is not what we want for non-modal (viewing)
+ treeService.getCurrentNode().setFocus();
}
- });
- }
- else {
- dfd.resolve();
- }
-
- return dfd.promise();
- }
-
- async function setupProtectedSession() {
- const password = $password.val();
- $password.val("");
-
- const response = await enterProtectedSession(password);
-
- if (!response.success) {
- utils.showError("Wrong password.");
- return;
- }
-
- protectedSessionId = response.protectedSessionId;
-
- $dialog.dialog("close");
-
- noteEditor.reload();
- treeService.reload();
-
- if (protectedSessionDeferred !== null) {
- ensureDialogIsClosed($dialog, $password);
-
- $noteDetailWrapper.show();
-
- protectedSessionDeferred.resolve();
-
- protectedSessionDeferred = null;
- }
- }
-
- function ensureDialogIsClosed() {
- // this may fal if the dialog has not been previously opened
- try {
- $dialog.dialog('close');
- }
- catch (e) {}
-
- $password.val('');
- }
-
- async function enterProtectedSession(password) {
- return await server.post('login/protected', {
- password: password
+ }
});
}
-
- function getProtectedSessionId() {
- return protectedSessionId;
+ else {
+ dfd.resolve();
}
- function resetProtectedSession() {
- protectedSessionId = null;
+ return dfd.promise();
+}
- // most secure solution - guarantees nothing remained in memory
- // since this expires because user doesn't use the app, it shouldn't be disruptive
- utils.reloadApp();
+async function setupProtectedSession() {
+ const password = $password.val();
+ $password.val("");
+
+ const response = await enterProtectedSession(password);
+
+ if (!response.success) {
+ utils.showError("Wrong password.");
+ return;
}
- function isProtectedSessionAvailable() {
- return protectedSessionId !== null;
+ protectedSessionId = response.protectedSessionId;
+
+ $dialog.dialog("close");
+
+ noteEditor.reload();
+ treeService.reload();
+
+ if (protectedSessionDeferred !== null) {
+ ensureDialogIsClosed($dialog, $password);
+
+ $noteDetailWrapper.show();
+
+ protectedSessionDeferred.resolve();
+
+ protectedSessionDeferred = null;
}
+}
- async function protectNoteAndSendToServer() {
- await ensureProtectedSession(true, true);
-
- const note = noteEditor.getCurrentNote();
-
- noteEditor.updateNoteFromInputs(note);
-
- note.detail.isProtected = true;
-
- await noteEditor.saveNoteToServer(note);
-
- treeService.setProtected(note.detail.noteId, note.detail.isProtected);
-
- noteEditor.setNoteBackgroundIfProtected(note);
+function ensureDialogIsClosed() {
+ // this may fal if the dialog has not been previously opened
+ try {
+ $dialog.dialog('close');
}
+ catch (e) {}
- async function unprotectNoteAndSendToServer() {
- await ensureProtectedSession(true, true);
+ $password.val('');
+}
- const note = noteEditor.getCurrentNote();
-
- noteEditor.updateNoteFromInputs(note);
-
- note.detail.isProtected = false;
-
- await noteEditor.saveNoteToServer(note);
-
- treeService.setProtected(note.detail.noteId, note.detail.isProtected);
-
- noteEditor.setNoteBackgroundIfProtected(note);
- }
-
- function touchProtectedSession() {
- if (isProtectedSessionAvailable()) {
- lastProtectedSessionOperationDate = new Date();
- }
- }
-
- async function protectSubTree(noteId, protect) {
- await ensureProtectedSession(true, true);
-
- await server.put('notes/' + noteId + "/protect-sub-tree/" + (protect ? 1 : 0));
-
- utils.showMessage("Request to un/protect sub tree has finished successfully");
-
- treeService.reload();
- noteEditor.reload();
- }
-
- $passwordForm.submit(() => {
- setupProtectedSession();
-
- return false;
+async function enterProtectedSession(password) {
+ return await server.post('login/protected', {
+ password: password
});
+}
- setInterval(() => {
- if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) {
- resetProtectedSession();
- }
- }, 5000);
+function getProtectedSessionId() {
+ return protectedSessionId;
+}
- $protectButton.click(protectNoteAndSendToServer);
- $unprotectButton.click(unprotectNoteAndSendToServer);
+function resetProtectedSession() {
+ protectedSessionId = null;
- return {
- setProtectedSessionTimeout,
- ensureProtectedSession,
- resetProtectedSession,
- isProtectedSessionAvailable,
- protectNoteAndSendToServer,
- unprotectNoteAndSendToServer,
- getProtectedSessionId,
- touchProtectedSession,
- protectSubTree,
- ensureDialogIsClosed
- };
-})();
\ No newline at end of file
+ // most secure solution - guarantees nothing remained in memory
+ // since this expires because user doesn't use the app, it shouldn't be disruptive
+ utils.reloadApp();
+}
+
+function isProtectedSessionAvailable() {
+ return protectedSessionId !== null;
+}
+
+async function protectNoteAndSendToServer() {
+ await ensureProtectedSession(true, true);
+
+ const note = noteEditor.getCurrentNote();
+
+ noteEditor.updateNoteFromInputs(note);
+
+ note.detail.isProtected = true;
+
+ await noteEditor.saveNoteToServer(note);
+
+ treeService.setProtected(note.detail.noteId, note.detail.isProtected);
+
+ noteEditor.setNoteBackgroundIfProtected(note);
+}
+
+async function unprotectNoteAndSendToServer() {
+ await ensureProtectedSession(true, true);
+
+ const note = noteEditor.getCurrentNote();
+
+ noteEditor.updateNoteFromInputs(note);
+
+ note.detail.isProtected = false;
+
+ await noteEditor.saveNoteToServer(note);
+
+ treeService.setProtected(note.detail.noteId, note.detail.isProtected);
+
+ noteEditor.setNoteBackgroundIfProtected(note);
+}
+
+function touchProtectedSession() {
+ if (isProtectedSessionAvailable()) {
+ lastProtectedSessionOperationDate = new Date();
+ }
+}
+
+async function protectSubTree(noteId, protect) {
+ await ensureProtectedSession(true, true);
+
+ await server.put('notes/' + noteId + "/protect-sub-tree/" + (protect ? 1 : 0));
+
+ utils.showMessage("Request to un/protect sub tree has finished successfully");
+
+ treeService.reload();
+ noteEditor.reload();
+}
+
+$passwordForm.submit(() => {
+ setupProtectedSession();
+
+ return false;
+});
+
+setInterval(() => {
+ if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) {
+ resetProtectedSession();
+ }
+}, 5000);
+
+$protectButton.click(protectNoteAndSendToServer);
+$unprotectButton.click(unprotectNoteAndSendToServer);
+
+export default {
+ setProtectedSessionTimeout,
+ ensureProtectedSession,
+ resetProtectedSession,
+ isProtectedSessionAvailable,
+ protectNoteAndSendToServer,
+ unprotectNoteAndSendToServer,
+ getProtectedSessionId,
+ touchProtectedSession,
+ protectSubTree,
+ ensureDialogIsClosed
+};
\ No newline at end of file
diff --git a/src/public/javascripts/script_api.js b/src/public/javascripts/script_api.js
index 8038e795c..7cb1f3132 100644
--- a/src/public/javascripts/script_api.js
+++ b/src/public/javascripts/script_api.js
@@ -1,3 +1,5 @@
+import treeService from './note_tree.js';
+
function ScriptApi(startNote, currentNote) {
const $pluginButtons = $("#plugin-buttons");
@@ -51,4 +53,6 @@ function ScriptApi(startNote, currentNote) {
getInstanceName: treeService.getInstanceName,
runOnServer
}
-}
\ No newline at end of file
+}
+
+export default ScriptApi;
\ No newline at end of file
diff --git a/src/public/javascripts/script_context.js b/src/public/javascripts/script_context.js
index bf8ee68ef..0b77b037f 100644
--- a/src/public/javascripts/script_context.js
+++ b/src/public/javascripts/script_context.js
@@ -1,3 +1,8 @@
+"use strict";
+
+import ScriptApi from './script_api.js';
+import utils from './utils.js';
+
function ScriptContext(startNote, allNotes) {
const modules = {};
@@ -18,4 +23,6 @@ function ScriptContext(startNote, allNotes) {
}
}
};
-}
\ No newline at end of file
+}
+
+export default ScriptContext;
\ No newline at end of file
diff --git a/src/public/javascripts/search_tree.js b/src/public/javascripts/search_tree.js
index bc28dfb36..906250b73 100644
--- a/src/public/javascripts/search_tree.js
+++ b/src/public/javascripts/search_tree.js
@@ -1,5 +1,7 @@
"use strict";
+import treeService from './note_tree.js';
+
const $tree = $("#tree");
const $searchInput = $("input[name='search-text']");
const $resetSearchButton = $("#reset-search-button");
diff --git a/src/public/javascripts/server.js b/src/public/javascripts/server.js
index fbd8693e2..eed75110b 100644
--- a/src/public/javascripts/server.js
+++ b/src/public/javascripts/server.js
@@ -1,101 +1,104 @@
-const server = (function() {
- function getHeaders() {
- let protectedSessionId = null;
+"use strict";
- try { // this is because protected session might not be declared in some cases - like when it's included in migration page
- protectedSessionId = protected_session.getProtectedSessionId();
- }
- catch(e) {}
+import protected_session from './protected_session.js';
+import utils from './utils.js';
- // headers need to be lowercase because node.js automatically converts them to lower case
- // so hypothetical protectedSessionId becomes protectedsessionid on the backend
- return {
- protected_session_id: protectedSessionId,
- source_id: glob.sourceId
- };
+function getHeaders() {
+ let protectedSessionId = null;
+
+ try { // this is because protected session might not be declared in some cases - like when it's included in migration page
+ protectedSessionId = protected_session.getProtectedSessionId();
}
+ catch(e) {}
- async function get(url) {
- return await call('GET', url);
- }
+ // headers need to be lowercase because node.js automatically converts them to lower case
+ // so hypothetical protectedSessionId becomes protectedsessionid on the backend
+ return {
+ protected_session_id: protectedSessionId,
+ source_id: glob.sourceId
+ };
+}
- async function post(url, data) {
- return await call('POST', url, data);
- }
+async function get(url) {
+ return await call('GET', url);
+}
- async function put(url, data) {
- return await call('PUT', url, data);
- }
+async function post(url, data) {
+ return await call('POST', url, data);
+}
- async function remove(url) {
- return await call('DELETE', url);
- }
+async function put(url, data) {
+ return await call('PUT', url, data);
+}
- let i = 1;
- const reqResolves = {};
+async function remove(url) {
+ return await call('DELETE', url);
+}
- async function call(method, url, data) {
- if (utils.isElectron()) {
- const ipc = require('electron').ipcRenderer;
- const requestId = i++;
-
- return new Promise((resolve, reject) => {
- reqResolves[requestId] = resolve;
-
- console.log(utils.now(), "Request #" + requestId + " to " + method + " " + url);
-
- ipc.send('server-request', {
- requestId: requestId,
- headers: getHeaders(),
- method: method,
- url: "/" + baseApiUrl + url,
- data: data
- });
- });
- }
- else {
- return await ajax(url, method, data);
- }
- }
+let i = 1;
+const reqResolves = {};
+async function call(method, url, data) {
if (utils.isElectron()) {
const ipc = require('electron').ipcRenderer;
+ const requestId = i++;
- ipc.on('server-response', (event, arg) => {
- console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode);
+ return new Promise((resolve, reject) => {
+ reqResolves[requestId] = resolve;
- reqResolves[arg.requestId](arg.body);
+ console.log(utils.now(), "Request #" + requestId + " to " + method + " " + url);
- delete reqResolves[arg.requestId];
+ ipc.send('server-request', {
+ requestId: requestId,
+ headers: getHeaders(),
+ method: method,
+ url: "/" + baseApiUrl + url,
+ data: data
+ });
});
}
+ else {
+ return await ajax(url, method, data);
+ }
+}
- async function ajax(url, method, data) {
- const options = {
- url: baseApiUrl + url,
- type: method,
- headers: getHeaders()
- };
+if (utils.isElectron()) {
+ const ipc = require('electron').ipcRenderer;
- if (data) {
- options.data = JSON.stringify(data);
- options.contentType = "application/json";
- }
+ ipc.on('server-response', (event, arg) => {
+ console.log(utils.now(), "Response #" + arg.requestId + ": " + arg.statusCode);
- return await $.ajax(options).catch(e => {
- const message = "Error when calling " + method + " " + url + ": " + e.status + " - " + e.statusText;
- utils.showError(message);
- utils.throwError(message);
- });
+ reqResolves[arg.requestId](arg.body);
+
+ delete reqResolves[arg.requestId];
+ });
+}
+
+async function ajax(url, method, data) {
+ const options = {
+ url: baseApiUrl + url,
+ type: method,
+ headers: getHeaders()
+ };
+
+ if (data) {
+ options.data = JSON.stringify(data);
+ options.contentType = "application/json";
}
- return {
- get,
- post,
- put,
- remove,
- ajax,
- // don't remove, used from CKEditor image upload!
- getHeaders
- }
-})();
\ No newline at end of file
+ return await $.ajax(options).catch(e => {
+ const message = "Error when calling " + method + " " + url + ": " + e.status + " - " + e.statusText;
+ utils.showError(message);
+ utils.throwError(message);
+ });
+}
+
+export default {
+ get,
+ post,
+ put,
+ remove,
+ ajax,
+ // don't remove, used from CKEditor image upload!
+ getHeaders
+};
\ No newline at end of file
diff --git a/src/public/javascripts/sync.js b/src/public/javascripts/sync.js
index 43cf217b5..faa431c74 100644
--- a/src/public/javascripts/sync.js
+++ b/src/public/javascripts/sync.js
@@ -1,31 +1,31 @@
"use strict";
-const syncService = (function() {
- async function syncNow() {
- const result = await server.post('sync/now');
+import utils from './utils.js';
- if (result.success) {
- utils.showMessage("Sync finished successfully.");
- }
- else {
- if (result.message.length > 50) {
- result.message = result.message.substr(0, 50);
- }
+async function syncNow() {
+ const result = await server.post('sync/now');
- utils.showError("Sync failed: " + result.message);
- }
+ if (result.success) {
+ utils.showMessage("Sync finished successfully.");
}
+ else {
+ if (result.message.length > 50) {
+ result.message = result.message.substr(0, 50);
+ }
- $("#sync-now-button").click(syncNow);
-
- async function forceNoteSync(noteId) {
- const result = await server.post('sync/force-note-sync/' + noteId);
-
- utils.showMessage("Note added to sync queue.");
+ utils.showError("Sync failed: " + result.message);
}
+}
- return {
- syncNow,
- forceNoteSync
- };
-})();
\ No newline at end of file
+$("#sync-now-button").click(syncNow);
+
+async function forceNoteSync(noteId) {
+ const result = await server.post('sync/force-note-sync/' + noteId);
+
+ utils.showMessage("Note added to sync queue.");
+}
+
+export default {
+ syncNow,
+ forceNoteSync
+};
\ No newline at end of file
diff --git a/src/public/javascripts/tree_changes.js b/src/public/javascripts/tree_changes.js
index c478b1c84..6238ca3ed 100644
--- a/src/public/javascripts/tree_changes.js
+++ b/src/public/javascripts/tree_changes.js
@@ -1,132 +1,133 @@
"use strict";
-const treeChanges = (function() {
- async function moveBeforeNode(nodesToMove, beforeNode) {
- for (const nodeToMove of nodesToMove) {
- const resp = await server.put('tree/' + nodeToMove.data.branchId + '/move-before/' + beforeNode.data.branchId);
+import treeService from './note_tree.js';
+import utils from './utils.js';
- if (!resp.success) {
- alert(resp.message);
- return;
- }
-
- changeNode(nodeToMove, node => node.moveTo(beforeNode, 'before'));
- }
- }
-
- async function moveAfterNode(nodesToMove, afterNode) {
- nodesToMove.reverse(); // need to reverse to keep the note order
-
- for (const nodeToMove of nodesToMove) {
- const resp = await server.put('tree/' + nodeToMove.data.branchId + '/move-after/' + afterNode.data.branchId);
-
- if (!resp.success) {
- alert(resp.message);
- return;
- }
-
- changeNode(nodeToMove, node => node.moveTo(afterNode, 'after'));
- }
- }
-
- async function moveToNode(nodesToMove, toNode) {
- for (const nodeToMove of nodesToMove) {
- const resp = await server.put('tree/' + nodeToMove.data.branchId + '/move-to/' + toNode.data.noteId);
-
- if (!resp.success) {
- alert(resp.message);
- return;
- }
-
- changeNode(nodeToMove, node => {
- // first expand which will force lazy load and only then move the node
- // if this is not expanded before moving, then lazy load won't happen because it already contains node
- // this doesn't work if this isn't a folder yet, that's why we expand second time below
- toNode.setExpanded(true);
-
- node.moveTo(toNode);
-
- toNode.folder = true;
- toNode.renderTitle();
-
- // this expands the note in case it become the folder only after the move
- toNode.setExpanded(true);
- });
- }
- }
-
- async function deleteNodes(nodes) {
- if (nodes.length === 0 || !confirm('Are you sure you want to delete select note(s) and all the sub-notes?')) {
- return;
- }
-
- for (const node of nodes) {
- await server.remove('tree/' + node.data.branchId);
- }
-
- // following code assumes that nodes contain only top-most selected nodes - getSelectedNodes has been
- // called with stopOnParent=true
- let next = nodes[nodes.length - 1].getNextSibling();
-
- if (!next) {
- next = nodes[0].getPrevSibling();
- }
-
- if (!next && !utils.isTopLevelNode(nodes[0])) {
- next = nodes[0].getParent();
- }
-
- if (next) {
- // activate next element after this one is deleted so we don't lose focus
- next.setActive();
-
- treeService.setCurrentNotePathToHash(next);
- }
-
- treeService.reload();
-
- utils.showMessage("Note(s) has been deleted.");
- }
-
- async function moveNodeUpInHierarchy(node) {
- if (utils.isTopLevelNode(node)) {
- return;
- }
-
- const resp = await server.put('tree/' + node.data.branchId + '/move-after/' + node.getParent().data.branchId);
+async function moveBeforeNode(nodesToMove, beforeNode) {
+ for (const nodeToMove of nodesToMove) {
+ const resp = await server.put('tree/' + nodeToMove.data.branchId + '/move-before/' + beforeNode.data.branchId);
if (!resp.success) {
alert(resp.message);
return;
}
- if (!utils.isTopLevelNode(node) && node.getParent().getChildren().length <= 1) {
- node.getParent().folder = false;
- node.getParent().renderTitle();
+ changeNode(nodeToMove, node => node.moveTo(beforeNode, 'before'));
+ }
+}
+
+async function moveAfterNode(nodesToMove, afterNode) {
+ nodesToMove.reverse(); // need to reverse to keep the note order
+
+ for (const nodeToMove of nodesToMove) {
+ const resp = await server.put('tree/' + nodeToMove.data.branchId + '/move-after/' + afterNode.data.branchId);
+
+ if (!resp.success) {
+ alert(resp.message);
+ return;
}
- changeNode(node, node => node.moveTo(node.getParent(), 'after'));
+ changeNode(nodeToMove, node => node.moveTo(afterNode, 'after'));
+ }
+}
+
+async function moveToNode(nodesToMove, toNode) {
+ for (const nodeToMove of nodesToMove) {
+ const resp = await server.put('tree/' + nodeToMove.data.branchId + '/move-to/' + toNode.data.noteId);
+
+ if (!resp.success) {
+ alert(resp.message);
+ return;
+ }
+
+ changeNode(nodeToMove, node => {
+ // first expand which will force lazy load and only then move the node
+ // if this is not expanded before moving, then lazy load won't happen because it already contains node
+ // this doesn't work if this isn't a folder yet, that's why we expand second time below
+ toNode.setExpanded(true);
+
+ node.moveTo(toNode);
+
+ toNode.folder = true;
+ toNode.renderTitle();
+
+ // this expands the note in case it become the folder only after the move
+ toNode.setExpanded(true);
+ });
+ }
+}
+
+async function deleteNodes(nodes) {
+ if (nodes.length === 0 || !confirm('Are you sure you want to delete select note(s) and all the sub-notes?')) {
+ return;
}
- function changeNode(node, func) {
- utils.assertArguments(node.data.parentNoteId, node.data.noteId);
-
- treeService.removeParentChildRelation(node.data.parentNoteId, node.data.noteId);
-
- func(node);
-
- node.data.parentNoteId = utils.isTopLevelNode(node) ? 'root' : node.getParent().data.noteId;
-
- treeService.setParentChildRelation(node.data.branchId, node.data.parentNoteId, node.data.noteId);
-
- treeService.setCurrentNotePathToHash(node);
+ for (const node of nodes) {
+ await server.remove('tree/' + node.data.branchId);
}
- return {
- moveBeforeNode,
- moveAfterNode,
- moveToNode,
- deleteNodes,
- moveNodeUpInHierarchy
- };
-})();
\ No newline at end of file
+ // following code assumes that nodes contain only top-most selected nodes - getSelectedNodes has been
+ // called with stopOnParent=true
+ let next = nodes[nodes.length - 1].getNextSibling();
+
+ if (!next) {
+ next = nodes[0].getPrevSibling();
+ }
+
+ if (!next && !utils.isTopLevelNode(nodes[0])) {
+ next = nodes[0].getParent();
+ }
+
+ if (next) {
+ // activate next element after this one is deleted so we don't lose focus
+ next.setActive();
+
+ treeService.setCurrentNotePathToHash(next);
+ }
+
+ treeService.reload();
+
+ utils.showMessage("Note(s) has been deleted.");
+}
+
+async function moveNodeUpInHierarchy(node) {
+ if (utils.isTopLevelNode(node)) {
+ return;
+ }
+
+ const resp = await server.put('tree/' + node.data.branchId + '/move-after/' + node.getParent().data.branchId);
+
+ if (!resp.success) {
+ alert(resp.message);
+ return;
+ }
+
+ if (!utils.isTopLevelNode(node) && node.getParent().getChildren().length <= 1) {
+ node.getParent().folder = false;
+ node.getParent().renderTitle();
+ }
+
+ changeNode(node, node => node.moveTo(node.getParent(), 'after'));
+}
+
+function changeNode(node, func) {
+ utils.assertArguments(node.data.parentNoteId, node.data.noteId);
+
+ treeService.removeParentChildRelation(node.data.parentNoteId, node.data.noteId);
+
+ func(node);
+
+ node.data.parentNoteId = utils.isTopLevelNode(node) ? 'root' : node.getParent().data.noteId;
+
+ treeService.setParentChildRelation(node.data.branchId, node.data.parentNoteId, node.data.noteId);
+
+ treeService.setCurrentNotePathToHash(node);
+}
+
+export default {
+ moveBeforeNode,
+ moveAfterNode,
+ moveToNode,
+ deleteNodes,
+ moveNodeUpInHierarchy
+};
\ No newline at end of file
diff --git a/src/public/javascripts/tree_utils.js b/src/public/javascripts/tree_utils.js
index a431dede5..bd15439ed 100644
--- a/src/public/javascripts/tree_utils.js
+++ b/src/public/javascripts/tree_utils.js
@@ -1,40 +1,40 @@
"use strict";
-const treeUtils = (function() {
- const $tree = $("#tree");
+import utils from './utils.js';
- function getParentProtectedStatus(node) {
- return utils.isTopLevelNode(node) ? 0 : node.getParent().data.isProtected;
- }
+const $tree = $("#tree");
- function getNodeByKey(key) {
- return $tree.fancytree('getNodeByKey', key);
- }
+function getParentProtectedStatus(node) {
+ return utils.isTopLevelNode(node) ? 0 : node.getParent().data.isProtected;
+}
- function getNoteIdFromNotePath(notePath) {
- const path = notePath.split("/");
+function getNodeByKey(key) {
+ return $tree.fancytree('getNodeByKey', key);
+}
- return path[path.length - 1];
- }
+function getNoteIdFromNotePath(notePath) {
+ const path = notePath.split("/");
- function getNotePath(node) {
- const path = [];
+ return path[path.length - 1];
+}
- while (node && !utils.isRootNode(node)) {
- if (node.data.noteId) {
- path.push(node.data.noteId);
- }
+function getNotePath(node) {
+ const path = [];
- node = node.getParent();
+ while (node && !utils.isRootNode(node)) {
+ if (node.data.noteId) {
+ path.push(node.data.noteId);
}
- return path.reverse().join("/");
+ node = node.getParent();
}
- return {
- getParentProtectedStatus,
- getNodeByKey,
- getNotePath,
- getNoteIdFromNotePath,
- };
-})();
\ No newline at end of file
+ return path.reverse().join("/");
+}
+
+export default {
+ getParentProtectedStatus,
+ getNodeByKey,
+ getNotePath,
+ getNoteIdFromNotePath,
+};
\ No newline at end of file
diff --git a/src/public/javascripts/utils.js b/src/public/javascripts/utils.js
index 512f41713..5ec6c7a76 100644
--- a/src/public/javascripts/utils.js
+++ b/src/public/javascripts/utils.js
@@ -1,270 +1,272 @@
"use strict";
-const utils = (function() {
- function reloadApp() {
- window.location.reload(true);
+import link from './link.js';
+import messaging from './messaging.js';
+import ScriptContext from './script_context.js';
+
+function reloadApp() {
+ window.location.reload(true);
+}
+
+function showMessage(message) {
+ console.log(now(), "message: ", message);
+
+ $.notify({
+ // options
+ message: message
+ }, {
+ // settings
+ type: 'success',
+ delay: 3000
+ });
+}
+
+function showError(message, delay = 10000) {
+ console.log(now(), "error: ", message);
+
+ $.notify({
+ // options
+ message: message
+ }, {
+ // settings
+ type: 'danger',
+ delay: delay
+ });
+}
+
+function throwError(message) {
+ messaging.logError(message);
+
+ throw new Error(message);
+}
+
+function parseDate(str) {
+ try {
+ return new Date(Date.parse(str));
}
-
- function showMessage(message) {
- console.log(now(), "message: ", message);
-
- $.notify({
- // options
- message: message
- }, {
- // settings
- type: 'success',
- delay: 3000
- });
+ catch (e) {
+ throw new Error("Can't parse date from " + str + ": " + e.stack);
}
+}
- function showError(message, delay = 10000) {
- console.log(now(), "error: ", message);
+function padNum(num) {
+ return (num <= 9 ? "0" : "") + num;
+}
- $.notify({
- // options
- message: message
- }, {
- // settings
- type: 'danger',
- delay: delay
- });
- }
+function formatTime(date) {
+ return padNum(date.getHours()) + ":" + padNum(date.getMinutes());
+}
- function throwError(message) {
- messaging.logError(message);
+function formatTimeWithSeconds(date) {
+ return padNum(date.getHours()) + ":" + padNum(date.getMinutes()) + ":" + padNum(date.getSeconds());
+}
- throw new Error(message);
- }
+function formatDate(date) {
+ return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear();
+}
- function parseDate(str) {
- try {
- return new Date(Date.parse(str));
- }
- catch (e) {
- throw new Error("Can't parse date from " + str + ": " + e.stack);
+function formatDateISO(date) {
+ return date.getFullYear() + "-" + padNum(date.getMonth() + 1) + "-" + padNum(date.getDate());
+}
+
+function formatDateTime(date) {
+ return formatDate(date) + " " + formatTime(date);
+}
+
+function now() {
+ return formatTimeWithSeconds(new Date());
+}
+
+function isElectron() {
+ return window && window.process && window.process.type;
+}
+
+function assertArguments() {
+ for (const i in arguments) {
+ if (!arguments[i]) {
+ throwError(`Argument idx#${i} should not be falsy: ${arguments[i]}`);
}
}
+}
- function padNum(num) {
- return (num <= 9 ? "0" : "") + num;
+function assert(expr, message) {
+ if (!expr) {
+ throwError(message);
+ }
+}
+
+function isTopLevelNode(node) {
+ return isRootNode(node.getParent());
+}
+
+function isRootNode(node) {
+ return node.key === "root_1";
+}
+
+function escapeHtml(str) {
+ return $('').text(str).html();
+}
+
+async function stopWatch(what, func) {
+ const start = new Date();
+
+ const ret = await func();
+
+ const tookMs = new Date().getTime() - start.getTime();
+
+ console.log(`${what} took ${tookMs}ms`);
+
+ return ret;
+}
+
+async function executeBundle(bundle) {
+ const apiContext = ScriptContext(bundle.note, bundle.allNotes);
+
+ return await (function () {
+ return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
+ }.call(apiContext));
+}
+
+function formatValueWithWhitespace(val) {
+ return /[^\w_-]/.test(val) ? '"' + val + '"' : val;
+}
+
+function formatLabel(attr) {
+ let str = "@" + formatValueWithWhitespace(attr.name);
+
+ if (attr.value !== "") {
+ str += "=" + formatValueWithWhitespace(attr.value);
}
- function formatTime(date) {
- return padNum(date.getHours()) + ":" + padNum(date.getMinutes());
+ return str;
+}
+
+const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
+
+const CODE_MIRROR = {
+ js: [
+ "libraries/codemirror/codemirror.js",
+ "libraries/codemirror/addon/mode/loadmode.js",
+ "libraries/codemirror/addon/fold/xml-fold.js",
+ "libraries/codemirror/addon/edit/matchbrackets.js",
+ "libraries/codemirror/addon/edit/matchtags.js",
+ "libraries/codemirror/addon/search/match-highlighter.js",
+ "libraries/codemirror/mode/meta.js",
+ "libraries/codemirror/addon/lint/lint.js",
+ "libraries/codemirror/addon/lint/eslint.js"
+ ],
+ css: [
+ "libraries/codemirror/codemirror.css",
+ "libraries/codemirror/addon/lint/lint.css"
+ ]
+};
+
+const ESLINT = {js: ["libraries/eslint.js"]};
+
+async function requireLibrary(library) {
+ if (library.css) {
+ library.css.map(cssUrl => requireCss(cssUrl));
}
- function formatTimeWithSeconds(date) {
- return padNum(date.getHours()) + ":" + padNum(date.getMinutes()) + ":" + padNum(date.getSeconds());
- }
-
- function formatDate(date) {
- return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear();
- }
-
- function formatDateISO(date) {
- return date.getFullYear() + "-" + padNum(date.getMonth() + 1) + "-" + padNum(date.getDate());
- }
-
- function formatDateTime(date) {
- return formatDate(date) + " " + formatTime(date);
- }
-
- function now() {
- return formatTimeWithSeconds(new Date());
- }
-
- function isElectron() {
- return window && window.process && window.process.type;
- }
-
- function assertArguments() {
- for (const i in arguments) {
- if (!arguments[i]) {
- throwError(`Argument idx#${i} should not be falsy: ${arguments[i]}`);
- }
+ if (library.js) {
+ for (const scriptUrl of library.js) {
+ await requireScript(scriptUrl);
}
}
+}
- function assert(expr, message) {
- if (!expr) {
- throwError(message);
- }
+const dynamicallyLoadedScripts = [];
+
+async function requireScript(url) {
+ if (!dynamicallyLoadedScripts.includes(url)) {
+ dynamicallyLoadedScripts.push(url);
+
+ return await $.ajax({
+ url: url,
+ dataType: "script",
+ cache: true
+ })
+ }
+}
+
+async function requireCss(url) {
+ const css = Array
+ .from(document.querySelectorAll('link'))
+ .map(scr => scr.href);
+
+ if (!css.includes(url)) {
+ $('head').append($('').attr('href', url));
+ }
+}
+
+function getHost() {
+ const url = new URL(window.location.href);
+ return url.protocol + "//" + url.hostname + ":" + url.port;
+}
+
+function download(url) {
+ if (isElectron()) {
+ const remote = require('electron').remote;
+
+ remote.getCurrentWebContents().downloadURL(url);
+ }
+ else {
+ window.location.href = url;
+ }
+}
+
+function toObject(array, fn) {
+ const obj = {};
+
+ for (const item of array) {
+ const ret = fn(item);
+
+ obj[ret[0]] = ret[1];
}
- function isTopLevelNode(node) {
- return isRootNode(node.getParent());
+ return obj;
+}
+
+function randomString(len) {
+ let text = "";
+ const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+ for (let i = 0; i < len; i++) {
+ text += possible.charAt(Math.floor(Math.random() * possible.length));
}
- function isRootNode(node) {
- return node.key === "root_1";
- }
+ return text;
+}
- function escapeHtml(str) {
- return $('').text(str).html();
- }
-
- async function stopWatch(what, func) {
- const start = new Date();
-
- const ret = await func();
-
- const tookMs = new Date().getTime() - start.getTime();
-
- console.log(`${what} took ${tookMs}ms`);
-
- return ret;
- }
-
- async function executeBundle(bundle) {
- const apiContext = ScriptContext(bundle.note, bundle.allNotes);
-
- return await (function () {
- return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
- }.call(apiContext));
- }
-
- function formatValueWithWhitespace(val) {
- return /[^\w_-]/.test(val) ? '"' + val + '"' : val;
- }
-
- function formatLabel(attr) {
- let str = "@" + formatValueWithWhitespace(attr.name);
-
- if (attr.value !== "") {
- str += "=" + formatValueWithWhitespace(attr.value);
- }
-
- return str;
- }
-
- const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
-
- const CODE_MIRROR = {
- js: [
- "libraries/codemirror/codemirror.js",
- "libraries/codemirror/addon/mode/loadmode.js",
- "libraries/codemirror/addon/fold/xml-fold.js",
- "libraries/codemirror/addon/edit/matchbrackets.js",
- "libraries/codemirror/addon/edit/matchtags.js",
- "libraries/codemirror/addon/search/match-highlighter.js",
- "libraries/codemirror/mode/meta.js",
- "libraries/codemirror/addon/lint/lint.js",
- "libraries/codemirror/addon/lint/eslint.js"
- ],
- css: [
- "libraries/codemirror/codemirror.css",
- "libraries/codemirror/addon/lint/lint.css"
- ]
- };
-
- const ESLINT = {js: ["libraries/eslint.js"]};
-
- async function requireLibrary(library) {
- if (library.css) {
- library.css.map(cssUrl => requireCss(cssUrl));
- }
-
- if (library.js) {
- for (const scriptUrl of library.js) {
- await requireScript(scriptUrl);
- }
- }
- }
-
- const dynamicallyLoadedScripts = [];
-
- async function requireScript(url) {
- if (!dynamicallyLoadedScripts.includes(url)) {
- dynamicallyLoadedScripts.push(url);
-
- return await $.ajax({
- url: url,
- dataType: "script",
- cache: true
- })
- }
- }
-
- async function requireCss(url) {
- const css = Array
- .from(document.querySelectorAll('link'))
- .map(scr => scr.href);
-
- if (!css.includes(url)) {
- $('head').append($('').attr('href', url));
- }
- }
-
- function getHost() {
- const url = new URL(window.location.href);
- return url.protocol + "//" + url.hostname + ":" + url.port;
- }
-
- function download(url) {
- if (isElectron()) {
- const remote = require('electron').remote;
-
- remote.getCurrentWebContents().downloadURL(url);
- }
- else {
- window.location.href = url;
- }
- }
-
- function toObject(array, fn) {
- const obj = {};
-
- for (const item of array) {
- const ret = fn(item);
-
- obj[ret[0]] = ret[1];
- }
-
- return obj;
- }
-
- function randomString(len) {
- let text = "";
- const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
-
- for (let i = 0; i < len; i++) {
- text += possible.charAt(Math.floor(Math.random() * possible.length));
- }
-
- return text;
- }
-
- return {
- reloadApp,
- showMessage,
- showError,
- throwError,
- parseDate,
- padNum,
- formatTime,
- formatTimeWithSeconds,
- formatDate,
- formatDateISO,
- formatDateTime,
- now,
- isElectron,
- assertArguments,
- assert,
- isTopLevelNode,
- isRootNode,
- escapeHtml,
- stopWatch,
- executeBundle,
- formatValueWithWhitespace,
- formatLabel,
- requireLibrary,
- CKEDITOR,
- CODE_MIRROR,
- ESLINT,
- getHost,
- download,
- toObject,
- randomString
- };
-})();
\ No newline at end of file
+export default {
+ reloadApp,
+ showMessage,
+ showError,
+ throwError,
+ parseDate,
+ padNum,
+ formatTime,
+ formatTimeWithSeconds,
+ formatDate,
+ formatDateISO,
+ formatDateTime,
+ now,
+ isElectron,
+ assertArguments,
+ assert,
+ isTopLevelNode,
+ isRootNode,
+ escapeHtml,
+ stopWatch,
+ executeBundle,
+ formatValueWithWhitespace,
+ formatLabel,
+ requireLibrary,
+ CKEDITOR,
+ CODE_MIRROR,
+ ESLINT,
+ getHost,
+ download,
+ toObject,
+ randomString
+};
\ No newline at end of file
diff --git a/src/views/index.ejs b/src/views/index.ejs
index f07375e32..50fa5ffcd 100644
--- a/src/views/index.ejs
+++ b/src/views/index.ejs
@@ -521,43 +521,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
|