mirror of
https://github.com/zadam/trilium.git
synced 2025-11-02 03:16:11 +01:00
moved all sources to src directory
This commit is contained in:
33
src/public/javascripts/cloning.js
Normal file
33
src/public/javascripts/cloning.js
Normal file
@@ -0,0 +1,33 @@
|
||||
"use strict";
|
||||
|
||||
const cloning = (function() {
|
||||
async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
|
||||
const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, {
|
||||
prefix: prefix
|
||||
});
|
||||
|
||||
if (!resp.success) {
|
||||
alert(resp.message);
|
||||
return;
|
||||
}
|
||||
|
||||
await noteTree.reload();
|
||||
}
|
||||
|
||||
// beware that first arg is noteId and second is noteTreeId!
|
||||
async function cloneNoteAfter(noteId, afterNoteTreeId) {
|
||||
const resp = await server.put('notes/' + noteId + '/clone-after/' + afterNoteTreeId);
|
||||
|
||||
if (!resp.success) {
|
||||
alert(resp.message);
|
||||
return;
|
||||
}
|
||||
|
||||
await noteTree.reload();
|
||||
}
|
||||
|
||||
return {
|
||||
cloneNoteAfter,
|
||||
cloneNoteTo
|
||||
};
|
||||
})();
|
||||
164
src/public/javascripts/context_menu.js
Normal file
164
src/public/javascripts/context_menu.js
Normal file
@@ -0,0 +1,164 @@
|
||||
"use strict";
|
||||
|
||||
const contextMenu = (function() {
|
||||
const treeEl = $("#tree");
|
||||
|
||||
let clipboardIds = [];
|
||||
let clipboardMode = null;
|
||||
|
||||
async function pasteAfter(node) {
|
||||
if (clipboardMode === 'cut') {
|
||||
const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
|
||||
|
||||
await treeChanges.moveAfterNode(nodes, node);
|
||||
|
||||
clipboardIds = [];
|
||||
clipboardMode = null;
|
||||
}
|
||||
else if (clipboardMode === 'copy') {
|
||||
for (const noteId of clipboardIds) {
|
||||
await cloning.cloneNoteAfter(noteId, node.data.noteTreeId);
|
||||
}
|
||||
|
||||
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
|
||||
}
|
||||
else if (clipboardIds.length === 0) {
|
||||
// just do nothing
|
||||
}
|
||||
else {
|
||||
throwError("Unrecognized clipboard mode=" + clipboardMode);
|
||||
}
|
||||
}
|
||||
|
||||
async function pasteInto(node) {
|
||||
if (clipboardMode === 'cut') {
|
||||
const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
|
||||
|
||||
await treeChanges.moveToNode(nodes, node);
|
||||
|
||||
clipboardIds = [];
|
||||
clipboardMode = null;
|
||||
}
|
||||
else if (clipboardMode === 'copy') {
|
||||
for (const noteId of clipboardIds) {
|
||||
await cloning.cloneNoteTo(noteId, node.data.noteId);
|
||||
}
|
||||
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
|
||||
}
|
||||
else if (clipboardIds.length === 0) {
|
||||
// just do nothing
|
||||
}
|
||||
else {
|
||||
throwError("Unrecognized clipboard mode=" + mode);
|
||||
}
|
||||
}
|
||||
|
||||
function copy(nodes) {
|
||||
clipboardIds = nodes.map(node => node.data.noteId);
|
||||
clipboardMode = 'copy';
|
||||
|
||||
showMessage("Note(s) have been copied into clipboard.");
|
||||
}
|
||||
|
||||
function cut(nodes) {
|
||||
clipboardIds = nodes.map(node => node.key);
|
||||
clipboardMode = 'cut';
|
||||
|
||||
showMessage("Note(s) have been cut into clipboard.");
|
||||
}
|
||||
|
||||
const contextMenuSettings = {
|
||||
delegate: "span.fancytree-title",
|
||||
autoFocus: true,
|
||||
menu: [
|
||||
{title: "Insert note here <kbd>Ctrl+O</kbd>", cmd: "insertNoteHere", uiIcon: "ui-icon-plus"},
|
||||
{title: "Insert child note <kbd>Ctrl+P</kbd>", cmd: "insertChildNote", uiIcon: "ui-icon-plus"},
|
||||
{title: "Delete <kbd>Ctrl+Del</kbd>", cmd: "delete", uiIcon: "ui-icon-trash"},
|
||||
{title: "----"},
|
||||
{title: "Edit tree prefix <kbd>F2</kbd>", cmd: "editTreePrefix", uiIcon: "ui-icon-pencil"},
|
||||
{title: "----"},
|
||||
{title: "Protect sub-tree", cmd: "protectSubTree", uiIcon: "ui-icon-locked"},
|
||||
{title: "Unprotect sub-tree", cmd: "unprotectSubTree", uiIcon: "ui-icon-unlocked"},
|
||||
{title: "----"},
|
||||
{title: "Copy / clone <kbd>Ctrl+C</kbd>", cmd: "copy", uiIcon: "ui-icon-copy"},
|
||||
{title: "Cut <kbd>Ctrl+X</kbd>", cmd: "cut", uiIcon: "ui-icon-scissors"},
|
||||
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
|
||||
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
|
||||
{title: "----"},
|
||||
{title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapse-sub-tree", uiIcon: "ui-icon-minus"},
|
||||
{title: "Force note sync", cmd: "force-note-sync", uiIcon: "ui-icon-refresh"},
|
||||
{title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sort-alphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
|
||||
|
||||
],
|
||||
beforeOpen: (event, ui) => {
|
||||
const node = $.ui.fancytree.getNode(ui.target);
|
||||
// Modify menu entries depending on node status
|
||||
treeEl.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
|
||||
treeEl.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
|
||||
|
||||
// Activate node on right-click
|
||||
node.setActive();
|
||||
// Disable tree keyboard handling
|
||||
ui.menu.prevKeyboard = node.tree.options.keyboard;
|
||||
node.tree.options.keyboard = false;
|
||||
},
|
||||
close: (event, ui) => {},
|
||||
select: (event, ui) => {
|
||||
const node = $.ui.fancytree.getNode(ui.target);
|
||||
|
||||
if (ui.cmd === "insertNoteHere") {
|
||||
const parentNoteId = node.data.parentNoteId;
|
||||
const isProtected = treeUtils.getParentProtectedStatus(node);
|
||||
|
||||
noteTree.createNote(node, parentNoteId, 'after', isProtected);
|
||||
}
|
||||
else if (ui.cmd === "insertChildNote") {
|
||||
noteTree.createNote(node, node.data.noteId, 'into');
|
||||
}
|
||||
else if (ui.cmd === "editTreePrefix") {
|
||||
editTreePrefix.showDialog(node);
|
||||
}
|
||||
else if (ui.cmd === "protectSubTree") {
|
||||
protected_session.protectSubTree(node.data.noteId, true);
|
||||
}
|
||||
else if (ui.cmd === "unprotectSubTree") {
|
||||
protected_session.protectSubTree(node.data.noteId, false);
|
||||
}
|
||||
else if (ui.cmd === "copy") {
|
||||
copy(noteTree.getSelectedNodes());
|
||||
}
|
||||
else if (ui.cmd === "cut") {
|
||||
cut(noteTree.getSelectedNodes());
|
||||
}
|
||||
else if (ui.cmd === "pasteAfter") {
|
||||
pasteAfter(node);
|
||||
}
|
||||
else if (ui.cmd === "pasteInto") {
|
||||
pasteInto(node);
|
||||
}
|
||||
else if (ui.cmd === "delete") {
|
||||
treeChanges.deleteNodes(noteTree.getSelectedNodes(true));
|
||||
}
|
||||
else if (ui.cmd === "collapse-sub-tree") {
|
||||
noteTree.collapseTree(node);
|
||||
}
|
||||
else if (ui.cmd === "force-note-sync") {
|
||||
forceNoteSync(node.data.noteId);
|
||||
}
|
||||
else if (ui.cmd === "sort-alphabetically") {
|
||||
noteTree.sortAlphabetically(node.data.noteId);
|
||||
}
|
||||
else {
|
||||
messaging.logError("Unknown command: " + ui.cmd);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
pasteAfter,
|
||||
pasteInto,
|
||||
cut,
|
||||
copy,
|
||||
contextMenuSettings
|
||||
}
|
||||
})();
|
||||
137
src/public/javascripts/dialogs/add_link.js
Normal file
137
src/public/javascripts/dialogs/add_link.js
Normal file
@@ -0,0 +1,137 @@
|
||||
"use strict";
|
||||
|
||||
const addLink = (function() {
|
||||
const dialogEl = $("#add-link-dialog");
|
||||
const formEl = $("#add-link-form");
|
||||
const autoCompleteEl = $("#note-autocomplete");
|
||||
const linkTitleEl = $("#link-title");
|
||||
const clonePrefixEl = $("#clone-prefix");
|
||||
const linkTitleFormGroup = $("#add-link-title-form-group");
|
||||
const prefixFormGroup = $("#add-link-prefix-form-group");
|
||||
const linkTypeEls = $("input[name='add-link-type']");
|
||||
const linkTypeHtmlEl = linkTypeEls.filter('input[value="html"]');
|
||||
|
||||
function setLinkType(linkType) {
|
||||
linkTypeEls.each(function () {
|
||||
$(this).prop('checked', $(this).val() === linkType);
|
||||
});
|
||||
|
||||
linkTypeChanged();
|
||||
}
|
||||
|
||||
function showDialog() {
|
||||
glob.activeDialog = dialogEl;
|
||||
|
||||
if (noteEditor.getCurrentNoteType() === 'text') {
|
||||
linkTypeHtmlEl.prop('disabled', false);
|
||||
|
||||
setLinkType('html');
|
||||
}
|
||||
else {
|
||||
linkTypeHtmlEl.prop('disabled', true);
|
||||
|
||||
setLinkType('selected-to-current');
|
||||
}
|
||||
|
||||
dialogEl.dialog({
|
||||
modal: true,
|
||||
width: 700
|
||||
});
|
||||
|
||||
autoCompleteEl.val('').focus();
|
||||
clonePrefixEl.val('');
|
||||
linkTitleEl.val('');
|
||||
|
||||
function setDefaultLinkTitle(noteId) {
|
||||
const noteTitle = noteTree.getNoteTitle(noteId);
|
||||
|
||||
linkTitleEl.val(noteTitle);
|
||||
}
|
||||
|
||||
autoCompleteEl.autocomplete({
|
||||
source: noteTree.getAutocompleteItems(),
|
||||
minLength: 0,
|
||||
change: () => {
|
||||
const val = autoCompleteEl.val();
|
||||
const notePath = link.getNodePathFromLabel(val);
|
||||
if (!notePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
if (noteId) {
|
||||
setDefaultLinkTitle(noteId);
|
||||
}
|
||||
},
|
||||
// this is called when user goes through autocomplete list with keyboard
|
||||
// at this point the item isn't selected yet so we use supplied ui.item to see WHERE the cursor is
|
||||
focus: (event, ui) => {
|
||||
const notePath = link.getNodePathFromLabel(ui.item.value);
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
setDefaultLinkTitle(noteId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
formEl.submit(() => {
|
||||
const value = autoCompleteEl.val();
|
||||
|
||||
const notePath = link.getNodePathFromLabel(value);
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
if (notePath) {
|
||||
const linkType = $("input[name='add-link-type']:checked").val();
|
||||
|
||||
if (linkType === 'html') {
|
||||
const linkTitle = linkTitleEl.val();
|
||||
|
||||
dialogEl.dialog("close");
|
||||
|
||||
link.addLinkToEditor(linkTitle, '#' + notePath);
|
||||
}
|
||||
else if (linkType === 'selected-to-current') {
|
||||
const prefix = clonePrefixEl.val();
|
||||
|
||||
cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix);
|
||||
|
||||
dialogEl.dialog("close");
|
||||
}
|
||||
else if (linkType === 'current-to-selected') {
|
||||
const prefix = clonePrefixEl.val();
|
||||
|
||||
cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix);
|
||||
|
||||
dialogEl.dialog("close");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
function linkTypeChanged() {
|
||||
const value = linkTypeEls.filter(":checked").val();
|
||||
|
||||
if (value === 'html') {
|
||||
linkTitleFormGroup.show();
|
||||
prefixFormGroup.hide();
|
||||
}
|
||||
else {
|
||||
linkTitleFormGroup.hide();
|
||||
prefixFormGroup.show();
|
||||
}
|
||||
}
|
||||
|
||||
linkTypeEls.change(linkTypeChanged);
|
||||
|
||||
$(document).bind('keydown', 'ctrl+l', e => {
|
||||
showDialog();
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
return {
|
||||
showDialog
|
||||
};
|
||||
})();
|
||||
62
src/public/javascripts/dialogs/attributes.js
Normal file
62
src/public/javascripts/dialogs/attributes.js
Normal file
@@ -0,0 +1,62 @@
|
||||
"use strict";
|
||||
|
||||
const attributesDialog = (function() {
|
||||
const dialogEl = $("#attributes-dialog");
|
||||
const attributesModel = new AttributesModel();
|
||||
|
||||
function AttributesModel() {
|
||||
const self = this;
|
||||
|
||||
this.attributes = ko.observableArray();
|
||||
|
||||
this.loadAttributes = async function() {
|
||||
const noteId = noteEditor.getCurrentNoteId();
|
||||
|
||||
const attributes = await server.get('notes/' + noteId + '/attributes');
|
||||
|
||||
this.attributes(attributes);
|
||||
};
|
||||
|
||||
this.addNewRow = function() {
|
||||
self.attributes.push({
|
||||
attributeId: '',
|
||||
name: '',
|
||||
value: ''
|
||||
});
|
||||
};
|
||||
|
||||
this.save = async function() {
|
||||
const noteId = noteEditor.getCurrentNoteId();
|
||||
|
||||
const attributes = await server.put('notes/' + noteId + '/attributes', this.attributes());
|
||||
|
||||
self.attributes(attributes);
|
||||
|
||||
showMessage("Attributes have been saved.");
|
||||
};
|
||||
}
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = dialogEl;
|
||||
|
||||
dialogEl.dialog({
|
||||
modal: true,
|
||||
width: 800,
|
||||
height: 700
|
||||
});
|
||||
|
||||
attributesModel.loadAttributes();
|
||||
}
|
||||
|
||||
$(document).bind('keydown', 'alt+a', e => {
|
||||
showDialog();
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
ko.applyBindings(attributesModel, document.getElementById('attributes-dialog'));
|
||||
|
||||
return {
|
||||
showDialog
|
||||
};
|
||||
})();
|
||||
45
src/public/javascripts/dialogs/edit_tree_prefix.js
Normal file
45
src/public/javascripts/dialogs/edit_tree_prefix.js
Normal file
@@ -0,0 +1,45 @@
|
||||
"use strict";
|
||||
|
||||
const editTreePrefix = (function() {
|
||||
const dialogEl = $("#edit-tree-prefix-dialog");
|
||||
const formEl = $("#edit-tree-prefix-form");
|
||||
const treePrefixInputEl = $("#tree-prefix-input");
|
||||
const noteTitleEl = $('#tree-prefix-note-title');
|
||||
|
||||
let noteTreeId;
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = dialogEl;
|
||||
|
||||
await dialogEl.dialog({
|
||||
modal: true,
|
||||
width: 500
|
||||
});
|
||||
|
||||
const currentNode = noteTree.getCurrentNode();
|
||||
|
||||
noteTreeId = currentNode.data.noteTreeId;
|
||||
|
||||
treePrefixInputEl.val(currentNode.data.prefix).focus();
|
||||
|
||||
const noteTitle = noteTree.getNoteTitle(currentNode.data.noteId);
|
||||
|
||||
noteTitleEl.html(noteTitle);
|
||||
}
|
||||
|
||||
formEl.submit(() => {
|
||||
const prefix = treePrefixInputEl.val();
|
||||
|
||||
server.put('tree/' + noteTreeId + '/set-prefix', {
|
||||
prefix: prefix
|
||||
}).then(() => noteTree.setPrefix(noteTreeId, prefix));
|
||||
|
||||
dialogEl.dialog("close");
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
showDialog
|
||||
};
|
||||
})();
|
||||
38
src/public/javascripts/dialogs/event_log.js
Normal file
38
src/public/javascripts/dialogs/event_log.js
Normal file
@@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
|
||||
const eventLog = (function() {
|
||||
const dialogEl = $("#event-log-dialog");
|
||||
const listEl = $("#event-log-list");
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = dialogEl;
|
||||
|
||||
dialogEl.dialog({
|
||||
modal: true,
|
||||
width: 800,
|
||||
height: 700
|
||||
});
|
||||
|
||||
const result = await server.get('event-log');
|
||||
|
||||
listEl.html('');
|
||||
|
||||
for (const event of result) {
|
||||
const dateTime = formatDateTime(parseDate(event.dateAdded));
|
||||
|
||||
if (event.noteId) {
|
||||
const noteLink = link.createNoteLink(event.noteId).prop('outerHTML');
|
||||
|
||||
event.comment = event.comment.replace('<note>', noteLink);
|
||||
}
|
||||
|
||||
const eventEl = $('<li>').html(dateTime + " - " + event.comment);
|
||||
|
||||
listEl.append(eventEl);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
showDialog
|
||||
};
|
||||
})();
|
||||
56
src/public/javascripts/dialogs/jump_to_note.js
Normal file
56
src/public/javascripts/dialogs/jump_to_note.js
Normal file
@@ -0,0 +1,56 @@
|
||||
"use strict";
|
||||
|
||||
const jumpToNote = (function() {
|
||||
const dialogEl = $("#jump-to-note-dialog");
|
||||
const autoCompleteEl = $("#jump-to-note-autocomplete");
|
||||
const formEl = $("#jump-to-note-form");
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = dialogEl;
|
||||
|
||||
autoCompleteEl.val('');
|
||||
|
||||
dialogEl.dialog({
|
||||
modal: true,
|
||||
width: 800
|
||||
});
|
||||
|
||||
await autoCompleteEl.autocomplete({
|
||||
source: await stopWatch("building autocomplete", noteTree.getAutocompleteItems),
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedNotePath() {
|
||||
const val = autoCompleteEl.val();
|
||||
return link.getNodePathFromLabel(val);
|
||||
}
|
||||
|
||||
function goToNote() {
|
||||
const notePath = getSelectedNotePath();
|
||||
|
||||
if (notePath) {
|
||||
noteTree.activateNode(notePath);
|
||||
|
||||
dialogEl.dialog('close');
|
||||
}
|
||||
}
|
||||
|
||||
$(document).bind('keydown', 'ctrl+j', e => {
|
||||
showDialog();
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
formEl.submit(() => {
|
||||
const action = dialogEl.find("button:focus").val();
|
||||
|
||||
goToNote();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
showDialog
|
||||
};
|
||||
})();
|
||||
78
src/public/javascripts/dialogs/note_history.js
Normal file
78
src/public/javascripts/dialogs/note_history.js
Normal file
@@ -0,0 +1,78 @@
|
||||
"use strict";
|
||||
|
||||
const noteHistory = (function() {
|
||||
const dialogEl = $("#note-history-dialog");
|
||||
const listEl = $("#note-history-list");
|
||||
const contentEl = $("#note-history-content");
|
||||
const titleEl = $("#note-history-title");
|
||||
|
||||
let historyItems = [];
|
||||
|
||||
async function showCurrentNoteHistory() {
|
||||
await showNoteHistoryDialog(noteEditor.getCurrentNoteId());
|
||||
}
|
||||
|
||||
async function showNoteHistoryDialog(noteId, noteRevisionId) {
|
||||
glob.activeDialog = dialogEl;
|
||||
|
||||
dialogEl.dialog({
|
||||
modal: true,
|
||||
width: 800,
|
||||
height: 700
|
||||
});
|
||||
|
||||
listEl.empty();
|
||||
contentEl.empty();
|
||||
|
||||
historyItems = await server.get('notes-history/' + noteId);
|
||||
|
||||
for (const item of historyItems) {
|
||||
const dateModified = parseDate(item.dateModifiedFrom);
|
||||
|
||||
listEl.append($('<option>', {
|
||||
value: item.noteRevisionId,
|
||||
text: formatDateTime(dateModified)
|
||||
}));
|
||||
}
|
||||
|
||||
if (historyItems.length > 0) {
|
||||
if (!noteRevisionId) {
|
||||
noteRevisionId = listEl.find("option:first").val();
|
||||
}
|
||||
|
||||
listEl.val(noteRevisionId).trigger('change');
|
||||
}
|
||||
else {
|
||||
titleEl.text("No history for this note yet...");
|
||||
}
|
||||
}
|
||||
|
||||
$(document).bind('keydown', 'alt+h', e => {
|
||||
showCurrentNoteHistory();
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
listEl.on('change', () => {
|
||||
const optVal = listEl.find(":selected").val();
|
||||
|
||||
const historyItem = historyItems.find(r => r.noteRevisionId === optVal);
|
||||
|
||||
titleEl.html(historyItem.title);
|
||||
contentEl.html(historyItem.content);
|
||||
});
|
||||
|
||||
$(document).on('click', "a[action='note-history']", event => {
|
||||
const linkEl = $(event.target);
|
||||
const noteId = linkEl.attr('note-path');
|
||||
const noteRevisionId = linkEl.attr('note-history-id');
|
||||
|
||||
showNoteHistoryDialog(noteId, noteRevisionId);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
showCurrentNoteHistory
|
||||
};
|
||||
})();
|
||||
57
src/public/javascripts/dialogs/note_source.js
Normal file
57
src/public/javascripts/dialogs/note_source.js
Normal file
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
|
||||
const noteSource = (function() {
|
||||
const dialogEl = $("#note-source-dialog");
|
||||
const noteSourceEl = $("#note-source");
|
||||
|
||||
function showDialog() {
|
||||
glob.activeDialog = dialogEl;
|
||||
|
||||
dialogEl.dialog({
|
||||
modal: true,
|
||||
width: 800,
|
||||
height: 500
|
||||
});
|
||||
|
||||
const noteText = noteEditor.getCurrentNote().detail.content;
|
||||
|
||||
noteSourceEl.text(formatHtml(noteText));
|
||||
}
|
||||
|
||||
function formatHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = str.trim();
|
||||
|
||||
return formatNode(div, 0).innerHTML.trim();
|
||||
}
|
||||
|
||||
function formatNode(node, level) {
|
||||
const indentBefore = new Array(level++ + 1).join(' ');
|
||||
const indentAfter = new Array(level - 1).join(' ');
|
||||
let textNode;
|
||||
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
textNode = document.createTextNode('\n' + indentBefore);
|
||||
node.insertBefore(textNode, node.children[i]);
|
||||
|
||||
formatNode(node.children[i], level);
|
||||
|
||||
if (node.lastElementChild === node.children[i]) {
|
||||
textNode = document.createTextNode('\n' + indentAfter);
|
||||
node.appendChild(textNode);
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
$(document).bind('keydown', 'ctrl+u', e => {
|
||||
showDialog();
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
return {
|
||||
showDialog
|
||||
};
|
||||
})();
|
||||
89
src/public/javascripts/dialogs/recent_changes.js
Normal file
89
src/public/javascripts/dialogs/recent_changes.js
Normal file
@@ -0,0 +1,89 @@
|
||||
"use strict";
|
||||
|
||||
const recentChanges = (function() {
|
||||
const dialogEl = $("#recent-changes-dialog");
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = dialogEl;
|
||||
|
||||
dialogEl.dialog({
|
||||
modal: true,
|
||||
width: 800,
|
||||
height: 700
|
||||
});
|
||||
|
||||
const result = await server.get('recent-changes/');
|
||||
|
||||
dialogEl.html('');
|
||||
|
||||
const groupedByDate = groupByDate(result);
|
||||
|
||||
for (const [dateDay, dayChanges] of groupedByDate) {
|
||||
const changesListEl = $('<ul>');
|
||||
|
||||
const dayEl = $('<div>').append($('<b>').html(formatDate(dateDay))).append(changesListEl);
|
||||
|
||||
for (const change of dayChanges) {
|
||||
const formattedTime = formatTime(parseDate(change.dateModifiedTo));
|
||||
|
||||
const revLink = $("<a>", {
|
||||
href: 'javascript:',
|
||||
text: 'rev'
|
||||
}).attr('action', 'note-history')
|
||||
.attr('note-path', change.noteId)
|
||||
.attr('note-history-id', change.noteRevisionId);
|
||||
|
||||
let noteLink;
|
||||
|
||||
if (change.current_isDeleted) {
|
||||
noteLink = change.current_title;
|
||||
}
|
||||
else {
|
||||
noteLink = link.createNoteLink(change.noteId, change.title);
|
||||
}
|
||||
|
||||
changesListEl.append($('<li>')
|
||||
.append(formattedTime + ' - ')
|
||||
.append(noteLink)
|
||||
.append(' (').append(revLink).append(')'));
|
||||
}
|
||||
|
||||
dialogEl.append(dayEl);
|
||||
}
|
||||
}
|
||||
|
||||
function groupByDate(result) {
|
||||
const groupedByDate = new Map();
|
||||
const dayCache = {};
|
||||
|
||||
for (const row of result) {
|
||||
let dateDay = parseDate(row.dateModifiedTo);
|
||||
dateDay.setHours(0);
|
||||
dateDay.setMinutes(0);
|
||||
dateDay.setSeconds(0);
|
||||
dateDay.setMilliseconds(0);
|
||||
|
||||
// this stupidity is to make sure that we always use the same day object because Map uses only
|
||||
// reference equality
|
||||
if (dayCache[dateDay]) {
|
||||
dateDay = dayCache[dateDay];
|
||||
}
|
||||
else {
|
||||
dayCache[dateDay] = dateDay;
|
||||
}
|
||||
|
||||
if (!groupedByDate.has(dateDay)) {
|
||||
groupedByDate.set(dateDay, []);
|
||||
}
|
||||
|
||||
groupedByDate.get(dateDay).push(row);
|
||||
}
|
||||
return groupedByDate;
|
||||
}
|
||||
|
||||
$(document).bind('keydown', 'alt+r', showDialog);
|
||||
|
||||
return {
|
||||
showDialog
|
||||
};
|
||||
})();
|
||||
146
src/public/javascripts/dialogs/recent_notes.js
Normal file
146
src/public/javascripts/dialogs/recent_notes.js
Normal file
@@ -0,0 +1,146 @@
|
||||
"use strict";
|
||||
|
||||
const recentNotes = (function() {
|
||||
const dialogEl = $("#recent-notes-dialog");
|
||||
const selectBoxEl = $('#recent-notes-select-box');
|
||||
const jumpToButtonEl = $('#recent-notes-jump-to');
|
||||
const addLinkButtonEl = $('#recent-notes-add-link');
|
||||
const addCurrentAsChildEl = $("#recent-notes-add-current-as-child");
|
||||
const addRecentAsChildEl = $("#recent-notes-add-recent-as-child");
|
||||
const noteDetailEl = $('#note-detail');
|
||||
// list of recent note paths
|
||||
let list = [];
|
||||
|
||||
async function reload() {
|
||||
const result = await server.get('recent-notes');
|
||||
|
||||
list = result.map(r => r.notePath);
|
||||
}
|
||||
|
||||
function addRecentNote(noteTreeId, notePath) {
|
||||
setTimeout(async () => {
|
||||
// we include the note into recent list only if the user stayed on the note at least 5 seconds
|
||||
if (notePath && notePath === noteTree.getCurrentNotePath()) {
|
||||
const result = await server.put('recent-notes/' + noteTreeId + '/' + encodeURIComponent(notePath));
|
||||
|
||||
list = result.map(r => r.notePath);
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function showDialog() {
|
||||
glob.activeDialog = dialogEl;
|
||||
|
||||
dialogEl.dialog({
|
||||
modal: true,
|
||||
width: 800
|
||||
});
|
||||
|
||||
selectBoxEl.find('option').remove();
|
||||
|
||||
// remove the current note
|
||||
const recNotes = list.filter(note => note !== noteTree.getCurrentNotePath());
|
||||
|
||||
$.each(recNotes, (key, valueNotePath) => {
|
||||
const noteTitle = noteTree.getNotePathTitle(valueNotePath);
|
||||
|
||||
const option = $("<option></option>")
|
||||
.attr("value", valueNotePath)
|
||||
.text(noteTitle);
|
||||
|
||||
// select the first one (most recent one) by default
|
||||
if (key === 0) {
|
||||
option.attr("selected", "selected");
|
||||
}
|
||||
|
||||
selectBoxEl.append(option);
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedNotePath() {
|
||||
return selectBoxEl.find("option:selected").val();
|
||||
}
|
||||
|
||||
function getSelectedNoteId() {
|
||||
const notePath = getSelectedNotePath();
|
||||
return treeUtils.getNoteIdFromNotePath(notePath);
|
||||
}
|
||||
|
||||
function setActiveNoteBasedOnRecentNotes() {
|
||||
const notePath = getSelectedNotePath();
|
||||
|
||||
noteTree.activateNode(notePath);
|
||||
|
||||
dialogEl.dialog('close');
|
||||
}
|
||||
|
||||
function addLinkBasedOnRecentNotes() {
|
||||
const notePath = getSelectedNotePath();
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
const linkTitle = noteTree.getNoteTitle(noteId);
|
||||
|
||||
dialogEl.dialog("close");
|
||||
|
||||
link.addLinkToEditor(linkTitle, '#' + notePath);
|
||||
}
|
||||
|
||||
async function addCurrentAsChild() {
|
||||
await cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), getSelectedNoteId());
|
||||
|
||||
dialogEl.dialog("close");
|
||||
}
|
||||
|
||||
async function addRecentAsChild() {
|
||||
await cloning.cloneNoteTo(getSelectedNoteId(), noteEditor.getCurrentNoteId());
|
||||
|
||||
dialogEl.dialog("close");
|
||||
}
|
||||
|
||||
selectBoxEl.keydown(e => {
|
||||
const key = e.which;
|
||||
|
||||
// to get keycodes use http://keycode.info/
|
||||
if (key === 13)// the enter key code
|
||||
{
|
||||
setActiveNoteBasedOnRecentNotes();
|
||||
}
|
||||
else if (key === 76 /* l */) {
|
||||
addLinkBasedOnRecentNotes();
|
||||
}
|
||||
else if (key === 67 /* c */) {
|
||||
addCurrentAsChild();
|
||||
}
|
||||
else if (key === 82 /* r */) {
|
||||
addRecentAsChild()
|
||||
}
|
||||
else {
|
||||
return; // avoid prevent default
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
reload();
|
||||
|
||||
$(document).bind('keydown', 'ctrl+e', e => {
|
||||
showDialog();
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
selectBoxEl.dblclick(e => {
|
||||
setActiveNoteBasedOnRecentNotes();
|
||||
});
|
||||
|
||||
jumpToButtonEl.click(setActiveNoteBasedOnRecentNotes);
|
||||
addLinkButtonEl.click(addLinkBasedOnRecentNotes);
|
||||
addCurrentAsChildEl.click(addCurrentAsChild);
|
||||
addRecentAsChildEl.click(addRecentAsChild);
|
||||
|
||||
return {
|
||||
showDialog,
|
||||
addRecentNote,
|
||||
reload
|
||||
};
|
||||
})();
|
||||
205
src/public/javascripts/dialogs/settings.js
Normal file
205
src/public/javascripts/dialogs/settings.js
Normal file
@@ -0,0 +1,205 @@
|
||||
"use strict";
|
||||
|
||||
const settings = (function() {
|
||||
const dialogEl = $("#settings-dialog");
|
||||
const tabsEl = $("#settings-tabs");
|
||||
|
||||
const settingModules = [];
|
||||
|
||||
function addModule(module) {
|
||||
settingModules.push(module);
|
||||
}
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = dialogEl;
|
||||
|
||||
const settings = await server.get('settings');
|
||||
|
||||
dialogEl.dialog({
|
||||
modal: true,
|
||||
width: 900
|
||||
});
|
||||
|
||||
tabsEl.tabs();
|
||||
|
||||
for (const module of settingModules) {
|
||||
if (module.settingsLoaded) {
|
||||
module.settingsLoaded(settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(settingName, settingValue) {
|
||||
await server.post('settings', {
|
||||
name: settingName,
|
||||
value: settingValue
|
||||
});
|
||||
|
||||
showMessage("Settings change have been saved.");
|
||||
}
|
||||
|
||||
return {
|
||||
showDialog,
|
||||
saveSettings,
|
||||
addModule
|
||||
};
|
||||
})();
|
||||
|
||||
settings.addModule((function() {
|
||||
const formEl = $("#change-password-form");
|
||||
const oldPasswordEl = $("#old-password");
|
||||
const newPassword1El = $("#new-password1");
|
||||
const newPassword2El = $("#new-password2");
|
||||
|
||||
function settingsLoaded(settings) {
|
||||
}
|
||||
|
||||
formEl.submit(() => {
|
||||
const oldPassword = oldPasswordEl.val();
|
||||
const newPassword1 = newPassword1El.val();
|
||||
const newPassword2 = newPassword2El.val();
|
||||
|
||||
oldPasswordEl.val('');
|
||||
newPassword1El.val('');
|
||||
newPassword2El.val('');
|
||||
|
||||
if (newPassword1 !== newPassword2) {
|
||||
alert("New passwords are not the same.");
|
||||
return false;
|
||||
}
|
||||
|
||||
server.post('password/change', {
|
||||
'current_password': oldPassword,
|
||||
'new_password': newPassword1
|
||||
}).then(result => {
|
||||
if (result.success) {
|
||||
alert("Password has been changed. Trilium will be reloaded after you press OK.");
|
||||
|
||||
// password changed so current protected session is invalid and needs to be cleared
|
||||
protected_session.resetProtectedSession();
|
||||
}
|
||||
else {
|
||||
showError(result.message);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
settingsLoaded
|
||||
};
|
||||
})());
|
||||
|
||||
settings.addModule((function() {
|
||||
const formEl = $("#protected-session-timeout-form");
|
||||
const protectedSessionTimeoutEl = $("#protected-session-timeout-in-seconds");
|
||||
const settingName = 'protected_session_timeout';
|
||||
|
||||
function settingsLoaded(settings) {
|
||||
protectedSessionTimeoutEl.val(settings[settingName]);
|
||||
}
|
||||
|
||||
formEl.submit(() => {
|
||||
const protectedSessionTimeout = protectedSessionTimeoutEl.val();
|
||||
|
||||
settings.saveSettings(settingName, protectedSessionTimeout).then(() => {
|
||||
protected_session.setProtectedSessionTimeout(protectedSessionTimeout);
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
settingsLoaded
|
||||
};
|
||||
})());
|
||||
|
||||
settings.addModule((function () {
|
||||
const formEl = $("#history-snapshot-time-interval-form");
|
||||
const timeIntervalEl = $("#history-snapshot-time-interval-in-seconds");
|
||||
const settingName = 'history_snapshot_time_interval';
|
||||
|
||||
function settingsLoaded(settings) {
|
||||
timeIntervalEl.val(settings[settingName]);
|
||||
}
|
||||
|
||||
formEl.submit(() => {
|
||||
settings.saveSettings(settingName, timeIntervalEl.val());
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
settingsLoaded
|
||||
};
|
||||
})());
|
||||
|
||||
settings.addModule((async function () {
|
||||
const appVersionEl = $("#app-version");
|
||||
const dbVersionEl = $("#db-version");
|
||||
const buildDateEl = $("#build-date");
|
||||
const buildRevisionEl = $("#build-revision");
|
||||
|
||||
const appInfo = await server.get('app-info');
|
||||
|
||||
appVersionEl.html(appInfo.app_version);
|
||||
dbVersionEl.html(appInfo.db_version);
|
||||
buildDateEl.html(appInfo.build_date);
|
||||
buildRevisionEl.html(appInfo.build_revision);
|
||||
buildRevisionEl.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.build_revision);
|
||||
|
||||
return {};
|
||||
})());
|
||||
|
||||
settings.addModule((async function () {
|
||||
const forceFullSyncButton = $("#force-full-sync-button");
|
||||
const fillSyncRowsButton = $("#fill-sync-rows-button");
|
||||
const anonymizeButton = $("#anonymize-button");
|
||||
const cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button");
|
||||
const cleanupUnusedImagesButton = $("#cleanup-unused-images-button");
|
||||
const vacuumDatabaseButton = $("#vacuum-database-button");
|
||||
|
||||
forceFullSyncButton.click(async () => {
|
||||
await server.post('sync/force-full-sync');
|
||||
|
||||
showMessage("Full sync triggered");
|
||||
});
|
||||
|
||||
fillSyncRowsButton.click(async () => {
|
||||
await server.post('sync/fill-sync-rows');
|
||||
|
||||
showMessage("Sync rows filled successfully");
|
||||
});
|
||||
|
||||
|
||||
anonymizeButton.click(async () => {
|
||||
await server.post('anonymization/anonymize');
|
||||
|
||||
showMessage("Created anonymized database");
|
||||
});
|
||||
|
||||
cleanupSoftDeletedButton.click(async () => {
|
||||
if (confirm("Do you really want to clean up soft-deleted items?")) {
|
||||
await server.post('cleanup/cleanup-soft-deleted-items');
|
||||
|
||||
showMessage("Soft deleted items have been cleaned up");
|
||||
}
|
||||
});
|
||||
|
||||
cleanupUnusedImagesButton.click(async () => {
|
||||
if (confirm("Do you really want to clean up unused images?")) {
|
||||
await server.post('cleanup/cleanup-unused-images');
|
||||
|
||||
showMessage("Unused images have been cleaned up");
|
||||
}
|
||||
});
|
||||
|
||||
vacuumDatabaseButton.click(async () => {
|
||||
await server.post('cleanup/vacuum-database');
|
||||
|
||||
showMessage("Database has been vacuumed");
|
||||
});
|
||||
|
||||
return {};
|
||||
})());
|
||||
71
src/public/javascripts/dialogs/sql_console.js
Normal file
71
src/public/javascripts/dialogs/sql_console.js
Normal file
@@ -0,0 +1,71 @@
|
||||
"use strict";
|
||||
|
||||
const sqlConsole = (function() {
|
||||
const dialogEl = $("#sql-console-dialog");
|
||||
const queryEl = $('#sql-console-query');
|
||||
const executeButton = $('#sql-console-execute');
|
||||
const resultHeadEl = $('#sql-console-results thead');
|
||||
const resultBodyEl = $('#sql-console-results tbody');
|
||||
|
||||
function showDialog() {
|
||||
glob.activeDialog = dialogEl;
|
||||
|
||||
dialogEl.dialog({
|
||||
modal: true,
|
||||
width: $(window).width(),
|
||||
height: $(window).height()
|
||||
});
|
||||
}
|
||||
|
||||
async function execute() {
|
||||
const sqlQuery = queryEl.val();
|
||||
|
||||
const result = await server.post("sql/execute", {
|
||||
query: sqlQuery
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
showError(result.error);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
showMessage("Query was executed successfully.");
|
||||
}
|
||||
|
||||
const rows = result.rows;
|
||||
|
||||
resultHeadEl.empty();
|
||||
resultBodyEl.empty();
|
||||
|
||||
if (rows.length > 0) {
|
||||
const result = rows[0];
|
||||
const rowEl = $("<tr>");
|
||||
|
||||
for (const key in result) {
|
||||
rowEl.append($("<th>").html(key));
|
||||
}
|
||||
|
||||
resultHeadEl.append(rowEl);
|
||||
}
|
||||
|
||||
for (const result of rows) {
|
||||
const rowEl = $("<tr>");
|
||||
|
||||
for (const key in result) {
|
||||
rowEl.append($("<td>").html(result[key]));
|
||||
}
|
||||
|
||||
resultBodyEl.append(rowEl);
|
||||
}
|
||||
}
|
||||
|
||||
$(document).bind('keydown', 'alt+o', showDialog);
|
||||
|
||||
queryEl.bind('keydown', 'ctrl+return', execute);
|
||||
|
||||
executeButton.click(execute);
|
||||
|
||||
return {
|
||||
showDialog
|
||||
};
|
||||
})();
|
||||
67
src/public/javascripts/drag_and_drop.js
Normal file
67
src/public/javascripts/drag_and_drop.js
Normal file
@@ -0,0 +1,67 @@
|
||||
"use strict";
|
||||
|
||||
const dragAndDropSetup = {
|
||||
autoExpandMS: 600,
|
||||
draggable: { // modify default jQuery draggable options
|
||||
zIndex: 1000,
|
||||
scroll: false,
|
||||
containment: "parent",
|
||||
revert: "invalid"
|
||||
},
|
||||
focusOnClick: true,
|
||||
preventRecursiveMoves: true, // Prevent dropping nodes on own descendants
|
||||
preventVoidMoves: true, // Prevent dropping nodes 'before self', etc.
|
||||
|
||||
dragStart: (node, data) => {
|
||||
// This function MUST be defined to enable dragging for the tree.
|
||||
// Return false to cancel dragging of node.
|
||||
return true;
|
||||
},
|
||||
dragEnter: (node, data) => {
|
||||
/* data.otherNode may be null for non-fancytree droppables.
|
||||
* Return false to disallow dropping on node. In this case
|
||||
* dragOver and dragLeave are not called.
|
||||
* Return 'over', 'before, or 'after' to force a hitMode.
|
||||
* Return ['before', 'after'] to restrict available hitModes.
|
||||
* Any other return value will calc the hitMode from the cursor position.
|
||||
*/
|
||||
// Prevent dropping a parent below another parent (only sort
|
||||
// nodes under the same parent):
|
||||
// if(node.parent !== data.otherNode.parent){
|
||||
// return false;
|
||||
// }
|
||||
// Don't allow dropping *over* a node (would create a child). Just
|
||||
// allow changing the order:
|
||||
// return ["before", "after"];
|
||||
// Accept everything:
|
||||
return true;
|
||||
},
|
||||
dragExpand: (node, data) => {
|
||||
// return false to prevent auto-expanding data.node on hover
|
||||
},
|
||||
dragOver: (node, data) => {},
|
||||
dragLeave: (node, data) => {},
|
||||
dragStop: (node, data) => {},
|
||||
dragDrop: (node, data) => {
|
||||
// This function MUST be defined to enable dropping of items on the tree.
|
||||
// data.hitMode is 'before', 'after', or 'over'.
|
||||
|
||||
const nodeToMove = data.otherNode;
|
||||
nodeToMove.setSelected(true);
|
||||
|
||||
const selectedNodes = noteTree.getSelectedNodes();
|
||||
|
||||
if (data.hitMode === "before") {
|
||||
treeChanges.moveBeforeNode(selectedNodes, node);
|
||||
}
|
||||
else if (data.hitMode === "after") {
|
||||
treeChanges.moveAfterNode(selectedNodes, node);
|
||||
}
|
||||
else if (data.hitMode === "over") {
|
||||
treeChanges.moveToNode(selectedNodes, node);
|
||||
}
|
||||
else {
|
||||
throw new Exception("Unknown hitMode=" + data.hitMode);
|
||||
}
|
||||
}
|
||||
};
|
||||
213
src/public/javascripts/init.js
Normal file
213
src/public/javascripts/init.js
Normal file
@@ -0,0 +1,213 @@
|
||||
"use strict";
|
||||
|
||||
// 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;
|
||||
|
||||
$(document).bind('keydown', 'alt+m', e => {
|
||||
$(".hide-toggle").toggleClass("suppressed");
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// hide (toggle) everything except for the note content for distraction free writing
|
||||
$(document).bind('keydown', 'alt+t', e => {
|
||||
const date = new Date();
|
||||
const dateString = formatDateTime(date);
|
||||
|
||||
link.addTextToEditor(dateString);
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$(document).bind('keydown', 'f5', () => {
|
||||
reloadApp();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).bind('keydown', 'ctrl+r', () => {
|
||||
reloadApp();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).bind('keydown', 'ctrl+shift+i', () => {
|
||||
if (isElectron()) {
|
||||
require('electron').remote.getCurrentWindow().toggleDevTools();
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$(document).bind('keydown', 'ctrl+f', () => {
|
||||
if (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+left", () => {
|
||||
const node = noteTree.getCurrentNode();
|
||||
node.navigate($.ui.keyCode.LEFT, true);
|
||||
|
||||
$("#note-detail").focus();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).bind('keydown', "ctrl+shift+right", () => {
|
||||
const node = noteTree.getCurrentNode();
|
||||
node.navigate($.ui.keyCode.RIGHT, true);
|
||||
|
||||
$("#note-detail").focus();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).bind('keydown', "ctrl+shift+up", () => {
|
||||
const node = noteTree.getCurrentNode();
|
||||
node.navigate($.ui.keyCode.UP, true);
|
||||
|
||||
$("#note-detail").focus();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).bind('keydown', "ctrl+shift+down", () => {
|
||||
const node = noteTree.getCurrentNode();
|
||||
node.navigate($.ui.keyCode.DOWN, true);
|
||||
|
||||
$("#note-detail").focus();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).bind('keydown', 'ctrl+-', () => {
|
||||
if (isElectron()) {
|
||||
const webFrame = require('electron').webFrame;
|
||||
|
||||
if (webFrame.getZoomFactor() > 0.2) {
|
||||
webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$(document).bind('keydown', 'ctrl+=', () => {
|
||||
if (isElectron()) {
|
||||
const webFrame = require('electron').webFrame;
|
||||
|
||||
webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$("#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 [];
|
||||
}
|
||||
|
||||
const startDate = new Date();
|
||||
|
||||
const results = [];
|
||||
const tokens = terms.toLowerCase().split(" ");
|
||||
|
||||
for (const item of array) {
|
||||
let found = true;
|
||||
const lcLabel = item.label.toLowerCase();
|
||||
|
||||
for (const token of tokens) {
|
||||
if (lcLabel.indexOf(token) === -1) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
results.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Search took " + (new Date().getTime() - startDate.getTime()) + "ms");
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
$(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).remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||
const string = msg.toLowerCase();
|
||||
|
||||
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(!isElectron());
|
||||
|
||||
$(document).ready(() => {
|
||||
server.get("script/startup").then(scripts => {
|
||||
for (const script of scripts) {
|
||||
executeScript(script);
|
||||
}
|
||||
});
|
||||
});
|
||||
103
src/public/javascripts/link.js
Normal file
103
src/public/javascripts/link.js
Normal file
@@ -0,0 +1,103 @@
|
||||
"use strict";
|
||||
|
||||
const link = (function() {
|
||||
function getNotePathFromLink(url) {
|
||||
const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url);
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function createNoteLink(notePath, noteTitle) {
|
||||
if (!noteTitle) {
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
||||
|
||||
noteTitle = noteTree.getNoteTitle(noteId);
|
||||
}
|
||||
|
||||
const noteLink = $("<a>", {
|
||||
href: 'javascript:',
|
||||
text: noteTitle
|
||||
}).attr('action', 'note')
|
||||
.attr('note-path', notePath);
|
||||
|
||||
return noteLink;
|
||||
}
|
||||
|
||||
function goToLink(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const linkEl = $(e.target);
|
||||
let notePath = linkEl.attr("note-path");
|
||||
|
||||
if (!notePath) {
|
||||
const address = linkEl.attr("note-path") ? linkEl.attr("note-path") : linkEl.attr('href');
|
||||
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (address.startsWith('http')) {
|
||||
window.open(address, '_blank');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
notePath = getNotePathFromLink(address);
|
||||
}
|
||||
|
||||
noteTree.activateNode(notePath);
|
||||
|
||||
// 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;
|
||||
|
||||
doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection);
|
||||
}
|
||||
|
||||
function addTextToEditor(text) {
|
||||
const editor = noteEditor.getEditor();
|
||||
const doc = editor.document;
|
||||
|
||||
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);
|
||||
|
||||
return {
|
||||
getNodePathFromLabel,
|
||||
getNotePathFromLink,
|
||||
createNoteLink,
|
||||
addLinkToEditor,
|
||||
addTextToEditor
|
||||
};
|
||||
})();
|
||||
115
src/public/javascripts/messaging.js
Normal file
115
src/public/javascripts/messaging.js
Normal file
@@ -0,0 +1,115 @@
|
||||
"use strict";
|
||||
|
||||
const messaging = (function() {
|
||||
const changesToPushCountEl = $("#changes-to-push-count");
|
||||
|
||||
function logError(message) {
|
||||
console.log(now(), message); // needs to be separate from .trace()
|
||||
console.trace();
|
||||
|
||||
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(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 === 'note_tree')
|
||||
|| syncData.some(sync => sync.entityName === 'notes')) {
|
||||
|
||||
console.log(now(), "Reloading tree because of background changes");
|
||||
|
||||
noteTree.reload();
|
||||
}
|
||||
|
||||
if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === noteEditor.getCurrentNoteId())) {
|
||||
showMessage('Reloading note because of background changes');
|
||||
|
||||
noteEditor.reload();
|
||||
}
|
||||
|
||||
if (syncData.some(sync => sync.entityName === 'recent_notes')) {
|
||||
console.log(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
|
||||
|
||||
changesToPushCountEl.html(message.changesToPushCount);
|
||||
}
|
||||
else if (message.type === 'sync-hash-check-failed') {
|
||||
showError("Sync check failed!", 60000);
|
||||
}
|
||||
else if (message.type === 'consistency-checks-failed') {
|
||||
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(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 > 5000) {
|
||||
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;
|
||||
|
||||
showMessage("Re-connected to server");
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'ping',
|
||||
lastSyncId: lastSyncId
|
||||
}));
|
||||
}, 1000);
|
||||
|
||||
return {
|
||||
logError
|
||||
};
|
||||
})();
|
||||
40
src/public/javascripts/migration.js
Normal file
40
src/public/javascripts/migration.js
Normal file
@@ -0,0 +1,40 @@
|
||||
"use strict";
|
||||
|
||||
$(document).ready(() => {
|
||||
server.get('migration').then(result => {
|
||||
const appDbVersion = result.app_db_version;
|
||||
const dbVersion = result.db_version;
|
||||
|
||||
if (appDbVersion === dbVersion) {
|
||||
$("#up-to-date").show();
|
||||
}
|
||||
else {
|
||||
$("#need-to-migrate").show();
|
||||
|
||||
$("#app-db-version").html(appDbVersion);
|
||||
$("#db-version").html(dbVersion);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#run-migration").click(async () => {
|
||||
$("#run-migration").prop("disabled", true);
|
||||
|
||||
$("#migration-result").show();
|
||||
|
||||
const result = await server.post('migration');
|
||||
|
||||
for (const migration of result.migrations) {
|
||||
const row = $('<tr>')
|
||||
.append($('<td>').html(migration.db_version))
|
||||
.append($('<td>').html(migration.name))
|
||||
.append($('<td>').html(migration.success ? 'Yes' : 'No'))
|
||||
.append($('<td>').html(migration.success ? 'N/A' : migration.error));
|
||||
|
||||
if (!migration.success) {
|
||||
row.addClass("danger");
|
||||
}
|
||||
|
||||
$("#migration-table").append(row);
|
||||
}
|
||||
});
|
||||
295
src/public/javascripts/note_editor.js
Normal file
295
src/public/javascripts/note_editor.js
Normal file
@@ -0,0 +1,295 @@
|
||||
"use strict";
|
||||
|
||||
const noteEditor = (function() {
|
||||
const noteTitleEl = $("#note-title");
|
||||
const noteDetailEl = $('#note-detail');
|
||||
const noteDetailCodeEl = $('#note-detail-code');
|
||||
const noteDetailRenderEl = $('#note-detail-render');
|
||||
const protectButton = $("#protect-button");
|
||||
const unprotectButton = $("#unprotect-button");
|
||||
const noteDetailWrapperEl = $("#note-detail-wrapper");
|
||||
const noteIdDisplayEl = $("#note-id-display");
|
||||
|
||||
let editor = null;
|
||||
let codeEditor = null;
|
||||
|
||||
let currentNote = null;
|
||||
|
||||
let noteChangeDisabled = false;
|
||||
|
||||
let isNoteChanged = false;
|
||||
|
||||
function getCurrentNote() {
|
||||
return currentNote;
|
||||
}
|
||||
|
||||
function getCurrentNoteId() {
|
||||
return currentNote ? currentNote.detail.noteId : null;
|
||||
}
|
||||
|
||||
function noteChanged() {
|
||||
if (noteChangeDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const note = noteEditor.getCurrentNote();
|
||||
|
||||
updateNoteFromInputs(note);
|
||||
|
||||
await saveNoteToServer(note);
|
||||
|
||||
if (note.detail.isProtected) {
|
||||
protected_session.touchProtectedSession();
|
||||
}
|
||||
}
|
||||
|
||||
function updateNoteFromInputs(note) {
|
||||
if (note.detail.type === 'text') {
|
||||
note.detail.content = editor.getData();
|
||||
|
||||
// if content is only tags/whitespace (typically <p> </p>), then just make it empty
|
||||
// this is important when setting new note to code
|
||||
if (jQuery(note.detail.content).text().trim() === '') {
|
||||
note.detail.content = ''
|
||||
}
|
||||
}
|
||||
else if (note.detail.type === 'code') {
|
||||
note.detail.content = codeEditor.getValue();
|
||||
}
|
||||
else if (note.detail.type === 'render') {
|
||||
// nothing
|
||||
}
|
||||
else {
|
||||
throwError("Unrecognized type: " + note.detail.type);
|
||||
}
|
||||
|
||||
const title = noteTitleEl.val();
|
||||
|
||||
note.detail.title = title;
|
||||
|
||||
noteTree.setNoteTitle(note.detail.noteId, title);
|
||||
}
|
||||
|
||||
async function saveNoteToServer(note) {
|
||||
await server.put('notes/' + note.detail.noteId, note);
|
||||
|
||||
isNoteChanged = false;
|
||||
|
||||
showMessage("Saved!");
|
||||
}
|
||||
|
||||
function setNoteBackgroundIfProtected(note) {
|
||||
const isProtected = !!note.detail.isProtected;
|
||||
|
||||
noteDetailWrapperEl.toggleClass("protected", isProtected);
|
||||
protectButton.toggle(!isProtected);
|
||||
unprotectButton.toggle(isProtected);
|
||||
}
|
||||
|
||||
let isNewNoteCreated = false;
|
||||
|
||||
function newNoteCreated() {
|
||||
isNewNoteCreated = true;
|
||||
}
|
||||
|
||||
async function loadNoteToEditor(noteId) {
|
||||
currentNote = await loadNote(noteId);
|
||||
|
||||
if (isNewNoteCreated) {
|
||||
isNewNoteCreated = false;
|
||||
|
||||
noteTitleEl.focus().select();
|
||||
}
|
||||
|
||||
noteIdDisplayEl.html(noteId);
|
||||
|
||||
await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false);
|
||||
|
||||
if (currentNote.detail.isProtected) {
|
||||
protected_session.touchProtectedSession();
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
noteDetailWrapperEl.show();
|
||||
|
||||
noteChangeDisabled = true;
|
||||
|
||||
noteTitleEl.val(currentNote.detail.title);
|
||||
|
||||
noteType.setNoteType(currentNote.detail.type);
|
||||
noteType.setNoteMime(currentNote.detail.mime);
|
||||
|
||||
if (currentNote.detail.type === 'text') {
|
||||
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
|
||||
editor.setData(currentNote.detail.content ? currentNote.detail.content : "<p></p>");
|
||||
|
||||
noteDetailEl.show();
|
||||
noteDetailCodeEl.hide();
|
||||
noteDetailRenderEl.html('').hide();
|
||||
}
|
||||
else if (currentNote.detail.type === 'code') {
|
||||
noteDetailEl.hide();
|
||||
noteDetailCodeEl.show();
|
||||
noteDetailRenderEl.html('').hide();
|
||||
|
||||
// this needs to happen after the element is shown, otherwise the editor won't be refresheds
|
||||
codeEditor.setValue(currentNote.detail.content);
|
||||
|
||||
const info = CodeMirror.findModeByMIME(currentNote.detail.mime);
|
||||
|
||||
if (info) {
|
||||
codeEditor.setOption("mode", info.mime);
|
||||
CodeMirror.autoLoadMode(codeEditor, info.mode);
|
||||
}
|
||||
}
|
||||
else if (currentNote.detail.type === 'render') {
|
||||
noteDetailEl.hide();
|
||||
noteDetailCodeEl.hide();
|
||||
noteDetailRenderEl.html('').show();
|
||||
|
||||
const subTree = await server.get('script/subtree/' + getCurrentNoteId());
|
||||
|
||||
noteDetailRenderEl.html(subTree);
|
||||
}
|
||||
else {
|
||||
throwError("Unrecognized type " + currentNote.detail.type);
|
||||
}
|
||||
|
||||
noteChangeDisabled = false;
|
||||
|
||||
setNoteBackgroundIfProtected(currentNote);
|
||||
noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
|
||||
|
||||
// after loading new note make sure editor is scrolled to the top
|
||||
noteDetailWrapperEl.scrollTop(0);
|
||||
}
|
||||
|
||||
async function loadNote(noteId) {
|
||||
return await server.get('notes/' + noteId);
|
||||
}
|
||||
|
||||
function getEditor() {
|
||||
return editor;
|
||||
}
|
||||
|
||||
function focus() {
|
||||
const note = getCurrentNote();
|
||||
|
||||
if (note.detail.type === 'text') {
|
||||
noteDetailEl.focus();
|
||||
}
|
||||
else if (note.detail.type === 'code') {
|
||||
codeEditor.focus();
|
||||
}
|
||||
else if (note.detail.type === 'render') {
|
||||
// do nothing
|
||||
}
|
||||
else {
|
||||
throwError('Unrecognized type: ' + note.detail.type);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
const script = await server.get('script/subtree/' + getCurrentNoteId());
|
||||
|
||||
executeScript(script);
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(() => {
|
||||
noteTitleEl.on('input', () => {
|
||||
noteChanged();
|
||||
|
||||
const title = noteTitleEl.val();
|
||||
|
||||
noteTree.setNoteTitle(getCurrentNoteId(), title);
|
||||
});
|
||||
|
||||
BalloonEditor
|
||||
.create(document.querySelector('#note-detail'), {
|
||||
})
|
||||
.then(edit => {
|
||||
editor = edit;
|
||||
|
||||
editor.document.on('change', noteChanged);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
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 }
|
||||
});
|
||||
|
||||
codeEditor.on('change', noteChanged);
|
||||
|
||||
// so that tab jumps from note title (which has tabindex 1)
|
||||
noteDetailEl.attr("tabindex", 2);
|
||||
});
|
||||
|
||||
$(document).bind('keydown', "ctrl+return", executeCurrentNote);
|
||||
|
||||
setInterval(saveNoteIfChanged, 5000);
|
||||
|
||||
return {
|
||||
reload,
|
||||
switchToNote,
|
||||
saveNoteIfChanged,
|
||||
updateNoteFromInputs,
|
||||
saveNoteToServer,
|
||||
setNoteBackgroundIfProtected,
|
||||
loadNote,
|
||||
getCurrentNote,
|
||||
getCurrentNoteType,
|
||||
getCurrentNoteId,
|
||||
newNoteCreated,
|
||||
getEditor,
|
||||
focus,
|
||||
executeCurrentNote
|
||||
};
|
||||
})();
|
||||
881
src/public/javascripts/note_tree.js
Normal file
881
src/public/javascripts/note_tree.js
Normal file
@@ -0,0 +1,881 @@
|
||||
"use strict";
|
||||
|
||||
const noteTree = (function() {
|
||||
const treeEl = $("#tree");
|
||||
const parentListEl = $("#parent-list");
|
||||
const parentListListEl = $("#parent-list-list");
|
||||
|
||||
let startNotePath = null;
|
||||
let notesTreeMap = {};
|
||||
|
||||
let parentToChildren = {};
|
||||
let childToParents = {};
|
||||
|
||||
let parentChildToNoteTreeId = {};
|
||||
let noteIdToTitle = {};
|
||||
|
||||
function getNoteTreeId(parentNoteId, childNoteId) {
|
||||
assertArguments(parentNoteId, childNoteId);
|
||||
|
||||
const key = parentNoteId + "-" + childNoteId;
|
||||
|
||||
// this can return undefined and client code should deal with it somehow
|
||||
|
||||
return parentChildToNoteTreeId[key];
|
||||
}
|
||||
|
||||
function getNoteTitle(noteId, parentNoteId = null) {
|
||||
assertArguments(noteId);
|
||||
|
||||
let title = noteIdToTitle[noteId];
|
||||
|
||||
if (!title) {
|
||||
throwError("Can't find title for noteId='" + noteId + "'");
|
||||
}
|
||||
|
||||
if (parentNoteId !== null) {
|
||||
const noteTreeId = getNoteTreeId(parentNoteId, noteId);
|
||||
|
||||
if (noteTreeId) {
|
||||
const noteTree = notesTreeMap[noteTreeId];
|
||||
|
||||
if (noteTree.prefix) {
|
||||
title = noteTree.prefix + ' - ' + title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
// note that if you want to access data like noteId or isProtected, you need to go into "data" property
|
||||
function getCurrentNode() {
|
||||
return treeEl.fancytree("getActiveNode");
|
||||
}
|
||||
|
||||
function getCurrentNotePath() {
|
||||
const node = getCurrentNode();
|
||||
|
||||
return treeUtils.getNotePath(node);
|
||||
}
|
||||
|
||||
function getNodesByNoteTreeId(noteTreeId) {
|
||||
assertArguments(noteTreeId);
|
||||
|
||||
const noteTree = notesTreeMap[noteTreeId];
|
||||
|
||||
return getNodesByNoteId(noteTree.noteId).filter(node => node.data.noteTreeId === noteTreeId);
|
||||
}
|
||||
|
||||
function getNodesByNoteId(noteId) {
|
||||
assertArguments(noteId);
|
||||
|
||||
const list = getTree().getNodesByRef(noteId);
|
||||
return list ? list : []; // if no nodes with this refKey are found, fancy tree returns null
|
||||
}
|
||||
|
||||
function setPrefix(noteTreeId, prefix) {
|
||||
assertArguments(noteTreeId);
|
||||
|
||||
notesTreeMap[noteTreeId].prefix = prefix;
|
||||
|
||||
getNodesByNoteTreeId(noteTreeId).map(node => {
|
||||
node.data.prefix = prefix;
|
||||
|
||||
treeUtils.setNodeTitleWithPrefix(node);
|
||||
});
|
||||
}
|
||||
|
||||
function removeParentChildRelation(parentNoteId, childNoteId) {
|
||||
assertArguments(parentNoteId, childNoteId);
|
||||
|
||||
const key = parentNoteId + "-" + childNoteId;
|
||||
|
||||
delete parentChildToNoteTreeId[key];
|
||||
|
||||
parentToChildren[parentNoteId] = parentToChildren[parentNoteId].filter(noteId => noteId !== childNoteId);
|
||||
childToParents[childNoteId] = childToParents[childNoteId].filter(noteId => noteId !== parentNoteId);
|
||||
}
|
||||
|
||||
function setParentChildRelation(noteTreeId, parentNoteId, childNoteId) {
|
||||
assertArguments(noteTreeId, parentNoteId, childNoteId);
|
||||
|
||||
const key = parentNoteId + "-" + childNoteId;
|
||||
|
||||
parentChildToNoteTreeId[key] = noteTreeId;
|
||||
|
||||
if (!parentToChildren[parentNoteId]) {
|
||||
parentToChildren[parentNoteId] = [];
|
||||
}
|
||||
|
||||
parentToChildren[parentNoteId].push(childNoteId);
|
||||
|
||||
if (!childToParents[childNoteId]) {
|
||||
childToParents[childNoteId] = [];
|
||||
}
|
||||
|
||||
childToParents[childNoteId].push(parentNoteId);
|
||||
}
|
||||
|
||||
function prepareNoteTree(notes) {
|
||||
assertArguments(notes);
|
||||
|
||||
parentToChildren = {};
|
||||
childToParents = {};
|
||||
notesTreeMap = {};
|
||||
|
||||
for (const note of notes) {
|
||||
notesTreeMap[note.noteTreeId] = note;
|
||||
|
||||
noteIdToTitle[note.noteId] = note.title;
|
||||
|
||||
delete note.title; // this should not be used. Use noteIdToTitle instead
|
||||
|
||||
setParentChildRelation(note.noteTreeId, note.parentNoteId, note.noteId);
|
||||
}
|
||||
|
||||
return prepareNoteTreeInner('root');
|
||||
}
|
||||
|
||||
function getExtraClasses(note) {
|
||||
assertArguments(note);
|
||||
|
||||
const extraClasses = [];
|
||||
|
||||
if (note.isProtected) {
|
||||
extraClasses.push("protected");
|
||||
}
|
||||
|
||||
if (childToParents[note.noteId].length > 1) {
|
||||
extraClasses.push("multiple-parents");
|
||||
}
|
||||
|
||||
if (note.type === 'code') {
|
||||
extraClasses.push("code");
|
||||
}
|
||||
|
||||
return extraClasses.join(" ");
|
||||
}
|
||||
|
||||
function prepareNoteTreeInner(parentNoteId) {
|
||||
assertArguments(parentNoteId);
|
||||
|
||||
const childNoteIds = parentToChildren[parentNoteId];
|
||||
if (!childNoteIds) {
|
||||
messaging.logError("No children for " + parentNoteId + ". This shouldn't happen.");
|
||||
return;
|
||||
}
|
||||
|
||||
const noteList = [];
|
||||
|
||||
for (const noteId of childNoteIds) {
|
||||
const noteTreeId = getNoteTreeId(parentNoteId, noteId);
|
||||
const noteTree = notesTreeMap[noteTreeId];
|
||||
|
||||
const title = (noteTree.prefix ? (noteTree.prefix + " - ") : "") + noteIdToTitle[noteTree.noteId];
|
||||
|
||||
const node = {
|
||||
noteId: noteTree.noteId,
|
||||
parentNoteId: noteTree.parentNoteId,
|
||||
noteTreeId: noteTree.noteTreeId,
|
||||
isProtected: noteTree.isProtected,
|
||||
prefix: noteTree.prefix,
|
||||
title: escapeHtml(title),
|
||||
extraClasses: getExtraClasses(noteTree),
|
||||
refKey: noteTree.noteId,
|
||||
expanded: noteTree.isExpanded
|
||||
};
|
||||
|
||||
if (parentToChildren[noteId] && parentToChildren[noteId].length > 0) {
|
||||
node.folder = true;
|
||||
|
||||
if (node.expanded) {
|
||||
node.children = prepareNoteTreeInner(noteId);
|
||||
}
|
||||
else {
|
||||
node.lazy = true;
|
||||
}
|
||||
}
|
||||
|
||||
noteList.push(node);
|
||||
}
|
||||
|
||||
return noteList;
|
||||
}
|
||||
|
||||
async function expandToNote(notePath, expandOpts) {
|
||||
assertArguments(notePath);
|
||||
|
||||
const runPath = 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;
|
||||
}
|
||||
else {
|
||||
await node.setExpanded(true, expandOpts);
|
||||
}
|
||||
|
||||
parentNoteId = childNoteId;
|
||||
}
|
||||
}
|
||||
|
||||
async function activateNode(notePath) {
|
||||
assertArguments(notePath);
|
||||
|
||||
const node = await expandToNote(notePath);
|
||||
|
||||
await node.setActive();
|
||||
|
||||
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.
|
||||
*/
|
||||
function getRunPath(notePath) {
|
||||
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 parents = childToParents[childNoteId];
|
||||
|
||||
if (!parents) {
|
||||
messaging.logError("No parents found for " + childNoteId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parents.includes(parentNoteId)) {
|
||||
console.log(now(), "Did not find parent " + parentNoteId + " for child " + childNoteId);
|
||||
|
||||
if (parents.length > 0) {
|
||||
console.log(now(), "Available parents:", parents);
|
||||
|
||||
const someNotePath = 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();
|
||||
}
|
||||
|
||||
function showParentList(noteId, node) {
|
||||
assertArguments(noteId, node);
|
||||
|
||||
const parents = childToParents[noteId];
|
||||
|
||||
if (!parents) {
|
||||
throwError("Can't find parents for noteId=" + noteId);
|
||||
}
|
||||
|
||||
if (parents.length <= 1) {
|
||||
parentListEl.hide();
|
||||
}
|
||||
else {
|
||||
parentListEl.show();
|
||||
parentListListEl.empty();
|
||||
|
||||
for (const parentNoteId of parents) {
|
||||
const parentNotePath = getSomeNotePath(parentNoteId);
|
||||
// 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 === parentNoteId) {
|
||||
item = $("<span/>").attr("title", "Current note").append(title);
|
||||
}
|
||||
else {
|
||||
item = link.createNoteLink(notePath, title);
|
||||
}
|
||||
|
||||
parentListListEl.append($("<li/>").append(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNotePathTitle(notePath) {
|
||||
assertArguments(notePath);
|
||||
|
||||
const titlePath = [];
|
||||
|
||||
let parentNoteId = 'root';
|
||||
|
||||
for (const noteId of notePath.split('/')) {
|
||||
titlePath.push(getNoteTitle(noteId, parentNoteId));
|
||||
|
||||
parentNoteId = noteId;
|
||||
}
|
||||
|
||||
return titlePath.join(' / ');
|
||||
}
|
||||
|
||||
function getSomeNotePath(noteId) {
|
||||
assertArguments(noteId);
|
||||
|
||||
const path = [];
|
||||
|
||||
let cur = noteId;
|
||||
|
||||
while (cur !== 'root') {
|
||||
path.push(cur);
|
||||
|
||||
if (!childToParents[cur]) {
|
||||
throwError("Can't find parents for " + cur);
|
||||
}
|
||||
|
||||
cur = childToParents[cur][0];
|
||||
}
|
||||
|
||||
return path.reverse().join('/');
|
||||
}
|
||||
|
||||
async function setExpandedToServer(noteTreeId, isExpanded) {
|
||||
assertArguments(noteTreeId);
|
||||
|
||||
const expandedNum = isExpanded ? 1 : 0;
|
||||
|
||||
await server.put('tree/' + noteTreeId + '/expanded/' + expandedNum);
|
||||
}
|
||||
|
||||
function setCurrentNotePathToHash(node) {
|
||||
assertArguments(node);
|
||||
|
||||
const currentNotePath = treeUtils.getNotePath(node);
|
||||
const currentNoteTreeId = node.data.noteTreeId;
|
||||
|
||||
document.location.hash = currentNotePath;
|
||||
|
||||
recentNotes.addRecentNote(currentNoteTreeId, 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(noteTree) {
|
||||
assertArguments(noteTree);
|
||||
|
||||
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 (!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;
|
||||
}
|
||||
};
|
||||
|
||||
treeEl.fancytree({
|
||||
autoScroll: true,
|
||||
keyboard: false, // we takover keyboard handling in the hotkeys plugin
|
||||
extensions: ["hotkeys", "filter", "dnd", "clones"],
|
||||
source: noteTree,
|
||||
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.noteTreeId, true);
|
||||
},
|
||||
collapse: (event, data) => {
|
||||
setExpandedToServer(data.node.data.noteTreeId, false);
|
||||
},
|
||||
init: (event, data) => {
|
||||
const noteId = treeUtils.getNoteIdFromNotePath(startNotePath);
|
||||
|
||||
if (noteIdToTitle[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 <mark> 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 node = data.node.data;
|
||||
|
||||
data.result = prepareNoteTreeInner(node.noteId);
|
||||
},
|
||||
clones: {
|
||||
highlightActiveClones: true
|
||||
}
|
||||
});
|
||||
|
||||
treeEl.contextmenu(contextMenu.contextMenuSettings);
|
||||
}
|
||||
|
||||
function getTree() {
|
||||
return treeEl.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 #
|
||||
}
|
||||
|
||||
function loadTree() {
|
||||
return server.get('tree').then(resp => {
|
||||
startNotePath = resp.start_note_path;
|
||||
|
||||
if (document.location.hash) {
|
||||
startNotePath = getNotePathFromAddress();
|
||||
}
|
||||
|
||||
return prepareNoteTree(resp.notes);
|
||||
});
|
||||
}
|
||||
|
||||
$(() => loadTree().then(noteTree => initFancyTree(noteTree)));
|
||||
|
||||
function collapseTree(node = null) {
|
||||
if (!node) {
|
||||
node = treeEl.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 setNoteTreeBackgroundBasedOnProtectedStatus(noteId) {
|
||||
getNodesByNoteId(noteId).map(node => node.toggleClass("protected", !!node.data.isProtected));
|
||||
}
|
||||
|
||||
function setProtected(noteId, isProtected) {
|
||||
getNodesByNoteId(noteId).map(node => node.data.isProtected = isProtected);
|
||||
|
||||
setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
|
||||
}
|
||||
|
||||
function getAutocompleteItems(parentNoteId, notePath, titlePath) {
|
||||
if (!parentNoteId) {
|
||||
parentNoteId = 'root';
|
||||
}
|
||||
|
||||
if (!parentToChildren[parentNoteId]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!notePath) {
|
||||
notePath = '';
|
||||
}
|
||||
|
||||
if (!titlePath) {
|
||||
titlePath = '';
|
||||
}
|
||||
|
||||
const autocompleteItems = [];
|
||||
|
||||
for (const childNoteId of parentToChildren[parentNoteId]) {
|
||||
const childNotePath = (notePath ? (notePath + '/') : '') + childNoteId;
|
||||
const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNoteId, parentNoteId);
|
||||
|
||||
autocompleteItems.push({
|
||||
value: childTitlePath + ' (' + childNotePath + ')',
|
||||
label: childTitlePath
|
||||
});
|
||||
|
||||
const childItems = getAutocompleteItems(childNoteId, childNotePath, childTitlePath);
|
||||
|
||||
for (const childItem of childItems) {
|
||||
autocompleteItems.push(childItem);
|
||||
}
|
||||
}
|
||||
|
||||
return autocompleteItems;
|
||||
}
|
||||
|
||||
function setNoteTitle(noteId, title) {
|
||||
assertArguments(noteId);
|
||||
|
||||
noteIdToTitle[noteId] = title;
|
||||
|
||||
getNodesByNoteId(noteId).map(clone => treeUtils.setNodeTitleWithPrefix(clone));
|
||||
}
|
||||
|
||||
async function createNewTopLevelNote() {
|
||||
const rootNode = treeEl.fancytree("getRootNode");
|
||||
|
||||
await createNote(rootNode, "root", "into");
|
||||
}
|
||||
|
||||
async function createNote(node, parentNoteId, target, isProtected) {
|
||||
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_noteTreeId: node.data.noteTreeId,
|
||||
isProtected: isProtected
|
||||
});
|
||||
|
||||
setParentChildRelation(result.noteTreeId, parentNoteId, result.noteId);
|
||||
|
||||
notesTreeMap[result.noteTreeId] = result;
|
||||
|
||||
noteIdToTitle[result.noteId] = newNoteName;
|
||||
|
||||
noteEditor.newNoteCreated();
|
||||
|
||||
const newNode = {
|
||||
title: newNoteName,
|
||||
noteId: result.noteId,
|
||||
parentNoteId: parentNoteId,
|
||||
refKey: result.noteId,
|
||||
noteTreeId: result.noteTreeId,
|
||||
isProtected: isProtected,
|
||||
extraClasses: getExtraClasses(result.note)
|
||||
};
|
||||
|
||||
if (target === 'after') {
|
||||
node.appendSibling(newNode).setActive(true);
|
||||
}
|
||||
else if (target === 'into') {
|
||||
if (!node.getChildren() && node.isFolder()) {
|
||||
await node.setExpanded();
|
||||
}
|
||||
else {
|
||||
node.addChildren(newNode);
|
||||
}
|
||||
|
||||
node.getLastChild().setActive(true);
|
||||
|
||||
node.folder = true;
|
||||
node.renderTitle();
|
||||
}
|
||||
else {
|
||||
throwError("Unrecognized target: " + target);
|
||||
}
|
||||
|
||||
showMessage("Created!");
|
||||
}
|
||||
|
||||
async function sortAlphabetically(noteId) {
|
||||
await server.put('notes/' + noteId + '/sort');
|
||||
|
||||
await reload();
|
||||
}
|
||||
|
||||
$(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);
|
||||
}
|
||||
});
|
||||
|
||||
if (isElectron()) {
|
||||
$(document).bind('keydown', 'alt+left', e => {
|
||||
window.history.back();
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$(document).bind('keydown', 'alt+right', e => {
|
||||
window.history.forward();
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
reload,
|
||||
collapseTree,
|
||||
scrollToCurrentNote,
|
||||
setNoteTreeBackgroundBasedOnProtectedStatus,
|
||||
setProtected,
|
||||
getCurrentNode,
|
||||
expandToNote,
|
||||
activateNode,
|
||||
getCurrentNotePath,
|
||||
getNoteTitle,
|
||||
setCurrentNotePathToHash,
|
||||
getAutocompleteItems,
|
||||
setNoteTitle,
|
||||
createNewTopLevelNote,
|
||||
createNote,
|
||||
setPrefix,
|
||||
getNotePathTitle,
|
||||
removeParentChildRelation,
|
||||
setParentChildRelation,
|
||||
getSelectedNodes,
|
||||
sortAlphabetically
|
||||
};
|
||||
})();
|
||||
134
src/public/javascripts/note_type.js
Normal file
134
src/public/javascripts/note_type.js
Normal file
@@ -0,0 +1,134 @@
|
||||
"use strict";
|
||||
|
||||
const noteType = (function() {
|
||||
const executeScriptButton = $("#execute-script-button");
|
||||
const noteTypeModel = new NoteTypeModel();
|
||||
|
||||
function NoteTypeModel() {
|
||||
const self = this;
|
||||
|
||||
this.type = ko.observable('text');
|
||||
this.mime = ko.observable('');
|
||||
|
||||
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', title: 'JavaScript' },
|
||||
{ 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.typeString = function() {
|
||||
const type = self.type();
|
||||
const mime = self.mime();
|
||||
|
||||
if (type === 'text') {
|
||||
return 'Text';
|
||||
}
|
||||
else if (type === 'code') {
|
||||
if (!mime) {
|
||||
return 'Code';
|
||||
}
|
||||
else {
|
||||
const found = self.codeMimeTypes().find(x => x.mime === mime);
|
||||
|
||||
return found ? found.title : mime;
|
||||
}
|
||||
}
|
||||
else if (type === 'render') {
|
||||
return 'Render HTML note';
|
||||
}
|
||||
else {
|
||||
throwError('Unrecognized type: ' + type);
|
||||
}
|
||||
};
|
||||
|
||||
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 noteTree.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() === 'application/javascript');
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
})();
|
||||
182
src/public/javascripts/protected_session.js
Normal file
182
src/public/javascripts/protected_session.js
Normal file
@@ -0,0 +1,182 @@
|
||||
"use strict";
|
||||
|
||||
const protected_session = (function() {
|
||||
const dialogEl = $("#protected-session-password-dialog");
|
||||
const passwordFormEl = $("#protected-session-password-form");
|
||||
const passwordEl = $("#protected-session-password");
|
||||
const noteDetailWrapperEl = $("#note-detail-wrapper");
|
||||
|
||||
let protectedSessionDeferred = null;
|
||||
let lastProtectedSessionOperationDate = null;
|
||||
let protectedSessionTimeout = null;
|
||||
let protectedSessionId = null;
|
||||
|
||||
$(document).ready(() => {
|
||||
server.get('settings/all').then(settings => protectedSessionTimeout = settings.protected_session_timeout);
|
||||
});
|
||||
|
||||
function setProtectedSessionTimeout(encSessTimeout) {
|
||||
protectedSessionTimeout = encSessTimeout;
|
||||
}
|
||||
|
||||
function ensureProtectedSession(requireProtectedSession, modal) {
|
||||
const dfd = $.Deferred();
|
||||
|
||||
if (requireProtectedSession && !isProtectedSessionAvailable()) {
|
||||
protectedSessionDeferred = dfd;
|
||||
|
||||
noteDetailWrapperEl.hide();
|
||||
|
||||
dialogEl.dialog({
|
||||
modal: modal,
|
||||
width: 400,
|
||||
open: () => {
|
||||
if (!modal) {
|
||||
// dialog steals focus for itself, which is not what we want for non-modal (viewing)
|
||||
noteTree.getCurrentNode().setFocus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
dfd.resolve();
|
||||
}
|
||||
|
||||
return dfd.promise();
|
||||
}
|
||||
|
||||
async function setupProtectedSession() {
|
||||
const password = passwordEl.val();
|
||||
passwordEl.val("");
|
||||
|
||||
const response = await enterProtectedSession(password);
|
||||
|
||||
if (!response.success) {
|
||||
showError("Wrong password.");
|
||||
return;
|
||||
}
|
||||
|
||||
protectedSessionId = response.protectedSessionId;
|
||||
|
||||
dialogEl.dialog("close");
|
||||
|
||||
noteEditor.reload();
|
||||
noteTree.reload();
|
||||
|
||||
if (protectedSessionDeferred !== null) {
|
||||
ensureDialogIsClosed(dialogEl, passwordEl);
|
||||
|
||||
noteDetailWrapperEl.show();
|
||||
|
||||
protectedSessionDeferred.resolve();
|
||||
|
||||
protectedSessionDeferred = null;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDialogIsClosed() {
|
||||
// this may fal if the dialog has not been previously opened
|
||||
try {
|
||||
dialogEl.dialog('close');
|
||||
}
|
||||
catch (e) {}
|
||||
|
||||
passwordEl.val('');
|
||||
}
|
||||
|
||||
async function enterProtectedSession(password) {
|
||||
return await server.post('login/protected', {
|
||||
password: password
|
||||
});
|
||||
}
|
||||
|
||||
function getProtectedSessionId() {
|
||||
return protectedSessionId;
|
||||
}
|
||||
|
||||
function resetProtectedSession() {
|
||||
protectedSessionId = null;
|
||||
|
||||
// most secure solution - guarantees nothing remained in memory
|
||||
// since this expires because user doesn't use the app, it shouldn't be disruptive
|
||||
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);
|
||||
|
||||
noteTree.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);
|
||||
|
||||
noteTree.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));
|
||||
|
||||
showMessage("Request to un/protect sub tree has finished successfully");
|
||||
|
||||
noteTree.reload();
|
||||
noteEditor.reload();
|
||||
}
|
||||
|
||||
passwordFormEl.submit(() => {
|
||||
setupProtectedSession();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) {
|
||||
resetProtectedSession();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return {
|
||||
setProtectedSessionTimeout,
|
||||
ensureProtectedSession,
|
||||
resetProtectedSession,
|
||||
isProtectedSessionAvailable,
|
||||
protectNoteAndSendToServer,
|
||||
unprotectNoteAndSendToServer,
|
||||
getProtectedSessionId,
|
||||
touchProtectedSession,
|
||||
protectSubTree,
|
||||
ensureDialogIsClosed
|
||||
};
|
||||
})();
|
||||
62
src/public/javascripts/search_tree.js
Normal file
62
src/public/javascripts/search_tree.js
Normal file
@@ -0,0 +1,62 @@
|
||||
"use strict";
|
||||
|
||||
const searchTree = (function() {
|
||||
const treeEl = $("#tree");
|
||||
const searchInputEl = $("input[name='search-text']");
|
||||
const resetSearchButton = $("button#reset-search-button");
|
||||
const searchBoxEl = $("#search-box");
|
||||
|
||||
resetSearchButton.click(resetSearch);
|
||||
|
||||
function toggleSearch() {
|
||||
if (searchBoxEl.is(":hidden")) {
|
||||
searchBoxEl.show();
|
||||
searchInputEl.focus();
|
||||
}
|
||||
else {
|
||||
resetSearch();
|
||||
|
||||
searchBoxEl.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
searchInputEl.val("");
|
||||
|
||||
getTree().clearFilter();
|
||||
}
|
||||
|
||||
function getTree() {
|
||||
return treeEl.fancytree('getTree');
|
||||
}
|
||||
|
||||
searchInputEl.keyup(async e => {
|
||||
const searchText = searchInputEl.val();
|
||||
|
||||
if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") {
|
||||
resetSearchButton.click();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e && e.which === $.ui.keyCode.ENTER) {
|
||||
const noteIds = await server.get('notes?search=' + encodeURIComponent(searchText));
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
await noteTree.expandToNote(noteId, {noAnimation: true, noEvents: true});
|
||||
}
|
||||
|
||||
// Pass a string to perform case insensitive matching
|
||||
getTree().filterBranches(node => noteIds.includes(node.data.noteId));
|
||||
}
|
||||
}).focus();
|
||||
|
||||
$(document).bind('keydown', 'ctrl+s', e => {
|
||||
toggleSearch();
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
return {
|
||||
toggleSearch
|
||||
};
|
||||
})();
|
||||
107
src/public/javascripts/server.js
Normal file
107
src/public/javascripts/server.js
Normal file
@@ -0,0 +1,107 @@
|
||||
const server = (function() {
|
||||
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) {}
|
||||
|
||||
// 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 get(url) {
|
||||
return await call('GET', url);
|
||||
}
|
||||
|
||||
async function post(url, data) {
|
||||
return await call('POST', url, data);
|
||||
}
|
||||
|
||||
async function put(url, data) {
|
||||
return await call('PUT', url, data);
|
||||
}
|
||||
|
||||
async function remove(url) {
|
||||
return await call('DELETE', url);
|
||||
}
|
||||
|
||||
async function exec(params, script) {
|
||||
if (typeof script === "function") {
|
||||
script = script.toString();
|
||||
}
|
||||
|
||||
return await post('script/exec/noteId', { script: script, params: params });
|
||||
}
|
||||
|
||||
let i = 1;
|
||||
const reqResolves = {};
|
||||
|
||||
async function call(method, url, data) {
|
||||
if (isElectron()) {
|
||||
const ipc = require('electron').ipcRenderer;
|
||||
const requestId = i++;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
reqResolves[requestId] = resolve;
|
||||
|
||||
console.log(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);
|
||||
}
|
||||
}
|
||||
|
||||
if (isElectron()) {
|
||||
const ipc = require('electron').ipcRenderer;
|
||||
|
||||
ipc.on('server-response', (event, arg) => {
|
||||
console.log(now(), "Response #" + arg.requestId + ": " + arg.statusCode);
|
||||
|
||||
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 await $.ajax(options).catch(e => {
|
||||
const message = "Error when calling " + method + " " + url + ": " + e.status + " - " + e.statusText;
|
||||
showError(message);
|
||||
throwError(message);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
remove,
|
||||
exec
|
||||
}
|
||||
})();
|
||||
34
src/public/javascripts/setup.js
Normal file
34
src/public/javascripts/setup.js
Normal file
@@ -0,0 +1,34 @@
|
||||
$("#setup-form").submit(() => {
|
||||
const username = $("#username").val();
|
||||
const password1 = $("#password1").val();
|
||||
const password2 = $("#password2").val();
|
||||
|
||||
if (!username) {
|
||||
showAlert("Username can't be empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!password1) {
|
||||
showAlert("Password can't be empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password1 !== password2) {
|
||||
showAlert("Both password fields need be identical.");
|
||||
return false;
|
||||
}
|
||||
|
||||
server.post('setup', {
|
||||
username: username,
|
||||
password: password1
|
||||
}).then(() => {
|
||||
window.location.replace("/");
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
function showAlert(message) {
|
||||
$("#alert").html(message);
|
||||
$("#alert").show();
|
||||
}
|
||||
22
src/public/javascripts/sync.js
Normal file
22
src/public/javascripts/sync.js
Normal file
@@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
|
||||
async function syncNow() {
|
||||
const result = await server.post('sync/now');
|
||||
|
||||
if (result.success) {
|
||||
showMessage("Sync finished successfully.");
|
||||
}
|
||||
else {
|
||||
if (result.message.length > 50) {
|
||||
result.message = result.message.substr(0, 50);
|
||||
}
|
||||
|
||||
showError("Sync failed: " + result.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function forceNoteSync(noteId) {
|
||||
const result = await server.post('sync/force-note-sync/' + noteId);
|
||||
|
||||
showMessage("Note added to sync queue.");
|
||||
}
|
||||
132
src/public/javascripts/tree_changes.js
Normal file
132
src/public/javascripts/tree_changes.js
Normal file
@@ -0,0 +1,132 @@
|
||||
"use strict";
|
||||
|
||||
const treeChanges = (function() {
|
||||
async function moveBeforeNode(nodesToMove, beforeNode) {
|
||||
for (const nodeToMove of nodesToMove) {
|
||||
const resp = await server.put('tree/' + nodeToMove.data.noteTreeId + '/move-before/' + beforeNode.data.noteTreeId);
|
||||
|
||||
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.noteTreeId + '/move-after/' + afterNode.data.noteTreeId);
|
||||
|
||||
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.noteTreeId + '/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.noteTreeId);
|
||||
}
|
||||
|
||||
// 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 && !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();
|
||||
|
||||
noteTree.setCurrentNotePathToHash(next);
|
||||
}
|
||||
|
||||
noteTree.reload();
|
||||
|
||||
showMessage("Note(s) has been deleted.");
|
||||
}
|
||||
|
||||
async function moveNodeUpInHierarchy(node) {
|
||||
if (isTopLevelNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resp = await server.put('tree/' + node.data.noteTreeId + '/move-after/' + node.getParent().data.noteTreeId);
|
||||
|
||||
if (!resp.success) {
|
||||
alert(resp.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!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) {
|
||||
assertArguments(node.data.parentNoteId, node.data.noteId);
|
||||
|
||||
noteTree.removeParentChildRelation(node.data.parentNoteId, node.data.noteId);
|
||||
|
||||
func(node);
|
||||
|
||||
node.data.parentNoteId = isTopLevelNode(node) ? 'root' : node.getParent().data.noteId;
|
||||
|
||||
noteTree.setParentChildRelation(node.data.noteTreeId, node.data.parentNoteId, node.data.noteId);
|
||||
|
||||
noteTree.setCurrentNotePathToHash(node);
|
||||
}
|
||||
|
||||
return {
|
||||
moveBeforeNode,
|
||||
moveAfterNode,
|
||||
moveToNode,
|
||||
deleteNodes,
|
||||
moveNodeUpInHierarchy
|
||||
};
|
||||
})();
|
||||
50
src/public/javascripts/tree_utils.js
Normal file
50
src/public/javascripts/tree_utils.js
Normal file
@@ -0,0 +1,50 @@
|
||||
"use strict";
|
||||
|
||||
const treeUtils = (function() {
|
||||
const treeEl = $("#tree");
|
||||
|
||||
function getParentProtectedStatus(node) {
|
||||
return isTopLevelNode(node) ? 0 : node.getParent().data.isProtected;
|
||||
}
|
||||
|
||||
function getNodeByKey(key) {
|
||||
return treeEl.fancytree('getNodeByKey', key);
|
||||
}
|
||||
|
||||
function getNoteIdFromNotePath(notePath) {
|
||||
const path = notePath.split("/");
|
||||
|
||||
return path[path.length - 1];
|
||||
}
|
||||
|
||||
function getNotePath(node) {
|
||||
const path = [];
|
||||
|
||||
while (node && !isRootNode(node)) {
|
||||
if (node.data.noteId) {
|
||||
path.push(node.data.noteId);
|
||||
}
|
||||
|
||||
node = node.getParent();
|
||||
}
|
||||
|
||||
return path.reverse().join("/");
|
||||
}
|
||||
|
||||
function setNodeTitleWithPrefix(node) {
|
||||
const noteTitle = noteTree.getNoteTitle(node.data.noteId);
|
||||
const prefix = node.data.prefix;
|
||||
|
||||
const title = (prefix ? (prefix + " - ") : "") + noteTitle;
|
||||
|
||||
node.setTitle(escapeHtml(title));
|
||||
}
|
||||
|
||||
return {
|
||||
getParentProtectedStatus,
|
||||
getNodeByKey,
|
||||
getNotePath,
|
||||
getNoteIdFromNotePath,
|
||||
setNodeTitleWithPrefix
|
||||
};
|
||||
})();
|
||||
120
src/public/javascripts/utils.js
Normal file
120
src/public/javascripts/utils.js
Normal file
@@ -0,0 +1,120 @@
|
||||
"use strict";
|
||||
|
||||
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));
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error("Can't parse date from " + str + ": " + e.stack);
|
||||
}
|
||||
}
|
||||
|
||||
function padNum(num) {
|
||||
return (num <= 9 ? "0" : "") + num;
|
||||
}
|
||||
|
||||
function formatTime(date) {
|
||||
return padNum(date.getHours()) + ":" + padNum(date.getMinutes());
|
||||
}
|
||||
|
||||
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]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 $('<div/>').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;
|
||||
}
|
||||
|
||||
function executeScript(script) {
|
||||
eval("(async function() {" + script + "})()");
|
||||
}
|
||||
Reference in New Issue
Block a user