diff --git a/src/templates/app.html b/src/templates/app.html index 246424920..0ec6660a2 100644 --- a/src/templates/app.html +++ b/src/templates/app.html @@ -152,11 +152,21 @@ + + + + + + + + + + \ No newline at end of file diff --git a/static/js/addlink.js b/static/js/addlink.js new file mode 100644 index 000000000..66f183a6f --- /dev/null +++ b/static/js/addlink.js @@ -0,0 +1,96 @@ +$(document).bind('keydown', 'alt+l', function() { + $("#noteAutocomplete").val(''); + $("#linkTitle").val(''); + + const noteDetail = $('#noteDetail'); + noteDetail.summernote('editor.saveRange'); + + $("#insertLinkDialog").dialog({ + modal: true, + width: 500 + }); + + function setDefaultLinkTitle(noteId) { + const note = getNodeByKey(noteId); + if (!note) { + return; + } + + let noteTitle = note.title; + + if (noteTitle.endsWith(" (clone)")) { + noteTitle = noteTitle.substr(0, noteTitle.length - 8); + } + + $("#linkTitle").val(noteTitle); + } + + $("#noteAutocomplete").autocomplete({ + source: getAutocompleteItems(globalAllNoteIds), + minLength: 0, + change: function () { + const val = $("#noteAutocomplete").val(); + const noteId = getNodeIdFromLabel(val); + + 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: function (event, ui) { + const noteId = getNodeIdFromLabel(ui.item.value); + + setDefaultLinkTitle(noteId); + } + }); +}); + +$("#insertLinkForm").submit(function() { + let val = $("#noteAutocomplete").val(); + + const noteId = getNodeIdFromLabel(val); + + if (noteId) { + const linkTitle = $("#linkTitle").val(); + const noteDetail = $('#noteDetail'); + + $("#insertLinkDialog").dialog("close"); + + noteDetail.summernote('editor.restoreRange'); + + noteDetail.summernote('createLink', { + text: linkTitle, + url: 'app#' + noteId, + isNewWindow: true + }); + } + + return false; +}); + +// 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', 'div.popover-content a', function(e) { + const targetUrl = $(e.target).attr("href"); + + const noteIdMatch = /app#([A-Za-z0-9]{22})/.exec(targetUrl); + + if (noteIdMatch !== null) { + const noteId = noteIdMatch[1]; + + getNodeByKey(noteId).setActive(); + + e.preventDefault(); + } +}); + +function getNodeIdFromLabel(label) { + const noteIdMatch = / \(([A-Za-z0-9]{22})\)/.exec(label); + + if (noteIdMatch !== null) { + return noteIdMatch[1]; + } + + return null; +} \ No newline at end of file diff --git a/static/js/contextmenu.js b/static/js/contextmenu.js new file mode 100644 index 000000000..82e9a9b19 --- /dev/null +++ b/static/js/contextmenu.js @@ -0,0 +1,63 @@ +const contextMenuSetup = { + delegate: "span.fancytree-title", + autoFocus: true, + menu: [ + {title: "Insert note here", cmd: "insertNoteHere", uiIcon: "ui-icon-pencil"}, + {title: "Insert child note", cmd: "insertChildNote", uiIcon: "ui-icon-pencil"}, + {title: "Delete", cmd: "delete", uiIcon: "ui-icon-trash"}, + {title: "----"}, + {title: "Cut", cmd: "cut", uiIcon: "ui-icon-scissors"}, + {title: "Copy / clone", cmd: "copy", uiIcon: "ui-icon-copy"}, + {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, + {title: "Paste into", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"} + ], + beforeOpen: function (event, ui) { + const node = $.ui.fancytree.getNode(ui.target); + // Modify menu entries depending on node status + globalTree.contextmenu("enableEntry", "pasteAfter", globalClipboardNoteId !== null); + globalTree.contextmenu("enableEntry", "pasteInto", globalClipboardNoteId !== null); + + // Activate node on right-click + node.setActive(); + // Disable tree keyboard handling + ui.menu.prevKeyboard = node.tree.options.keyboard; + node.tree.options.keyboard = false; + }, + close: function (event, ui) {}, + select: function (event, ui) { + const node = $.ui.fancytree.getNode(ui.target); + + if (ui.cmd === "insertNoteHere") { + const parentKey = getParentKey(node); + const encryption = getParentEncryption(node); + + createNote(node, parentKey, 'after', encryption); + } + else if (ui.cmd === "insertChildNote") { + createNote(node, node.key, 'into'); + } + else if (ui.cmd === "cut") { + globalClipboardNoteId = node.key; + } + else if (ui.cmd === "pasteAfter") { + const subjectNode = getNodeByKey(globalClipboardNoteId); + + moveAfterNode(subjectNode, node); + + globalClipboardNoteId = null; + } + else if (ui.cmd === "pasteInto") { + const subjectNode = getNodeByKey(globalClipboardNoteId); + + moveToNode(subjectNode, node); + + globalClipboardNoteId = null; + } + else if (ui.cmd === "delete") { + deleteNode(node); + } + else { + console.log("Unknown command: " + ui.cmd); + } + } +}; diff --git a/static/js/draganddrop.js b/static/js/draganddrop.js new file mode 100644 index 000000000..677113816 --- /dev/null +++ b/static/js/draganddrop.js @@ -0,0 +1,62 @@ +const dragAndDropSetup = { + autoExpandMS: 600, + draggable: { // modify default jQuery draggable options + zIndex: 1000, + scroll: false, + containment: "parent", + revert: "invalid" + }, + preventRecursiveMoves: true, // Prevent dropping nodes on own descendants + preventVoidMoves: true, // Prevent dropping nodes 'before self', etc. + + dragStart: function (node, data) { + // This function MUST be defined to enable dragging for the tree. + // Return false to cancel dragging of node. + return true; + }, + dragEnter: function (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: function (node, data) { + // return false to prevent auto-expanding data.node on hover + }, + dragOver: function (node, data) { + }, + dragLeave: function (node, data) { + }, + dragStop: function (node, data) { + }, + dragDrop: function (node, data) { + // This function MUST be defined to enable dropping of items on the tree. + // data.hitMode is 'before', 'after', or 'over'. + + if (data.hitMode === "before") { + moveBeforeNode(data.otherNode, node); + } + else if (data.hitMode === "after") { + moveAfterNode(data.otherNode, node); + } + else if (data.hitMode === "over") { + moveToNode(data.otherNode, node); + } + else { + throw new Exception("Unknown hitMode=" + data.hitMode); + } + } +}; diff --git a/static/js/encryption.js b/static/js/encryption.js index 2e8ab5211..e103fc9dd 100644 --- a/static/js/encryption.js +++ b/static/js/encryption.js @@ -14,7 +14,7 @@ function handleEncryption(requireEncryption, modal, callback) { open: function() { if (!modal) { // dialog steals focus for itself, which is not what we want for non-modal (viewing) - getNodeByKey(globalNote.detail.note_id).setFocus(); + getNodeByKey(globalCurrentNote.detail.note_id).setFocus(); } } }); @@ -127,8 +127,8 @@ setInterval(function() { if (globalLastEncryptionOperationDate !== null && new Date().getTime() - globalLastEncryptionOperationDate.getTime() > globalEncryptionKeyTimeToLive) { globalEncryptionKey = null; - if (globalNote.detail.encryption > 0) { - loadNote(globalNote.detail.note_id); + if (globalCurrentNote.detail.encryption > 0) { + loadNote(globalCurrentNote.detail.note_id); for (const noteId of globalAllNoteIds) { const note = getNodeByKey(noteId); @@ -189,7 +189,7 @@ function encryptNote(note) { function encryptNoteAndSendToServer() { handleEncryption(true, true, () => { - const note = globalNote; + const note = globalCurrentNote; updateNoteFromInputs(note); @@ -203,7 +203,7 @@ function encryptNoteAndSendToServer() { function decryptNoteAndSendToServer() { handleEncryption(true, true, () => { - const note = globalNote; + const note = globalCurrentNote; updateNoteFromInputs(note); diff --git a/static/js/init.js b/static/js/init.js index d97adcc12..65daa5d67 100644 --- a/static/js/init.js +++ b/static/js/init.js @@ -1,5 +1,6 @@ $(function() { $(window).resize(function() { + // dynamically setting height of tree and note content to match window's height const fancyTree = $('ul.fancytree-container'); if (fancyTree.length) { @@ -15,6 +16,7 @@ $(function() { $(window).resize(); }); +// hot keys are active also inside inputs and content editables jQuery.hotkeys.options.filterInputAcceptingElements = true; jQuery.hotkeys.options.filterContentEditable = true; @@ -27,176 +29,11 @@ $(document).bind('keydown', 'alt+h', function() { $("#noteDetailWrapper").css("width", hidden ? "750px" : "100%"); }); -$(document).bind('keydown', 'alt+q', function() { - $("#recentNotesDialog").dialog({ - modal: true, - width: 500 - }); - - let recentNotesSelectBox = $('#recentNotesSelectBox'); - - recentNotesSelectBox.find('option').remove(); - - // remove the current note - let recNotes = globalRecentNotes.filter(note => note !== globalNote.detail.note_id); - - $.each(recNotes, function(key, valueNoteId) { - let noteTitle = getFullName(valueNoteId); - - if (!noteTitle) { - return; - } - - let option = $("") - .attr("value", valueNoteId) - .text(noteTitle); - - // select the first one (most recent one) by default - if (key === 0) { - option.attr("selected", "selected"); - } - - recentNotesSelectBox.append(option); - }); -}); - -function setActiveNoteBasedOnRecentNotes() { - let noteId = $("#recentNotesSelectBox option:selected").val(); - - getNodeByKey(noteId).setActive(); - - $("#recentNotesDialog").dialog('close'); -} - -$('#recentNotesSelectBox').keydown(function(e) { - let key = e.which; - - if (key === 13)// the enter key code - { - setActiveNoteBasedOnRecentNotes(); - } -}); - -$('#recentNotesSelectBox').dblclick(function(e) { - setActiveNoteBasedOnRecentNotes(); -}); - -// 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', 'div.popover-content a', function(e) { - const targetUrl = $(e.target).attr("href"); - - const noteIdMatch = /app#([A-Za-z0-9]{22})/.exec(targetUrl); - - if (noteIdMatch !== null) { - const noteId = noteIdMatch[1]; - - getNodeByKey(noteId).setActive(); - - e.preventDefault(); - } -}); - -function getNodeIdFromLabel(label) { - const noteIdMatch = / \(([A-Za-z0-9]{22})\)/.exec(label); - - if (noteIdMatch !== null) { - return noteIdMatch[1]; - } - - return null; -} - -function getAutocompleteItems() { - const autocompleteItems = []; - - for (const noteId of globalAllNoteIds) { - const fullName = getFullName(noteId); - - autocompleteItems.push({ - value: fullName + " (" + noteId + ")", - label: fullName - }); - } - - return autocompleteItems; -} - -$(document).bind('keydown', 'alt+l', function() { - $("#noteAutocomplete").val(''); - $("#linkTitle").val(''); - - const noteDetail = $('#noteDetail'); - noteDetail.summernote('editor.saveRange'); - - $("#insertLinkDialog").dialog({ - modal: true, - width: 500 - }); - - function setDefaultLinkTitle(noteId) { - const note = getNodeByKey(noteId); - if (!note) { - return; - } - - let noteTitle = note.title; - - if (noteTitle.endsWith(" (clone)")) { - noteTitle = noteTitle.substr(0, noteTitle.length - 8); - } - - $("#linkTitle").val(noteTitle); - } - - $("#noteAutocomplete").autocomplete({ - source: getAutocompleteItems(), - minLength: 0, - change: function () { - const val = $("#noteAutocomplete").val(); - const noteId = getNodeIdFromLabel(val); - - 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: function (event, ui) { - const noteId = getNodeIdFromLabel(ui.item.value); - - setDefaultLinkTitle(noteId); - } - }); -}); - -$("#insertLinkForm").submit(function() { - let val = $("#noteAutocomplete").val(); - - const noteId = getNodeIdFromLabel(val); - - if (noteId) { - const linkTitle = $("#linkTitle").val(); - const noteDetail = $('#noteDetail'); - - $("#insertLinkDialog").dialog("close"); - - noteDetail.summernote('editor.restoreRange'); - - noteDetail.summernote('createLink', { - text: linkTitle, - url: 'app#' + noteId, - isNewWindow: true - }); - } - - return false; -}); - $(document).bind('keydown', 'alt+s', function() { $("input[name=search]").focus(); }); +// hide (toggle) everything except for the note content for distraction free writing $(document).bind('keydown', 'alt+t', function() { const date = new Date(); @@ -206,34 +43,8 @@ $(document).bind('keydown', 'alt+t', function() { $('#noteDetail').summernote('insertText', dateString); }); -$(document).bind('keydown', 'alt+j', function() { - $("#jumpToNoteAutocomplete").val(''); - - $("#jumpToNoteDialog").dialog({ - modal: true, - width: 500 - }); - - $("#jumpToNoteAutocomplete").autocomplete({ - source: getAutocompleteItems(), - minLength: 0 - }); -}); - -$("#jumpToNoteForm").submit(function() { - const val = $("#jumpToNoteAutocomplete").val(); - const noteId = getNodeIdFromLabel(val); - - if (noteId) { - getNodeByKey(noteId).setActive(); - - $("#jumpToNoteDialog").dialog('close'); - } - - return false; -}); - $(window).on('beforeunload', function(){ - // asynchronously send the request and don't wait for result + // 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 saveNoteIfChanged(); }); \ No newline at end of file diff --git a/static/js/jumptonote.js b/static/js/jumptonote.js new file mode 100644 index 000000000..af5da96c7 --- /dev/null +++ b/static/js/jumptonote.js @@ -0,0 +1,26 @@ +$(document).bind('keydown', 'alt+j', function() { + $("#jumpToNoteAutocomplete").val(''); + + $("#jumpToNoteDialog").dialog({ + modal: true, + width: 500 + }); + + $("#jumpToNoteAutocomplete").autocomplete({ + source: getAutocompleteItems(globalAllNoteIds), + minLength: 0 + }); +}); + +$("#jumpToNoteForm").submit(function() { + const val = $("#jumpToNoteAutocomplete").val(); + const noteId = getNodeIdFromLabel(val); + + if (noteId) { + getNodeByKey(noteId).setActive(); + + $("#jumpToNoteDialog").dialog('close'); + } + + return false; +}); \ No newline at end of file diff --git a/static/js/note.js b/static/js/note.js index b9da973d6..cac0f7e44 100644 --- a/static/js/note.js +++ b/static/js/note.js @@ -21,6 +21,43 @@ function noteChanged() { isNoteChanged = true; } +function saveNoteIfChanged(callback) { + if (!isNoteChanged) { + if (callback) { + callback(); + } + + return; + } + + const note = globalCurrentNote; + + updateNoteFromInputs(note); + + encryptNoteIfNecessary(note); + + saveNoteToServer(note, callback); +} + +setInterval(saveNoteIfChanged, 5000); + +$(document).ready(function() { + $("#noteTitle").on('input', function() { + noteChanged(); + }); + + $('#noteDetail').summernote({ + airMode: true, + height: 300, + callbacks: { + onChange: noteChanged + } + }); + + // so that tab jumps from note title (which has tabindex 1) + $(".note-editable").attr("tabindex", 2); +}); + function updateNoteFromInputs(note) { let contents = $('#noteDetail').summernote('code'); @@ -54,44 +91,7 @@ function saveNoteToServer(note, callback) { }); } -function saveNoteIfChanged(callback) { - if (!isNoteChanged) { - if (callback) { - callback(); - } - - return; - } - - const note = globalNote; - - updateNoteFromInputs(note); - - encryptNoteIfNecessary(note); - - saveNoteToServer(note, callback); -} - -setInterval(saveNoteIfChanged, 5000); - -$(document).ready(function() { - $("#noteTitle").on('input', function() { - noteChanged(); - }); - - $('#noteDetail').summernote({ - airMode: true, - height: 300, - callbacks: { - onChange: noteChanged - } - }); - - // so that tab jumps from note title (which has tabindex 1) - $(".note-editable").attr("tabindex", 2); -}); - -let globalNote; +let globalCurrentNote; function createNewTopLevelNote() { let rootNode = globalTree.fancytree("getRootNode"); @@ -149,8 +149,6 @@ function createNote(node, parentKey, target, encryption) { }); } -globalRecentNotes = []; - function setNoteBackgroundIfEncrypted(note) { if (note.detail.encryption > 0) { $(".note-editable").addClass("encrypted"); @@ -169,7 +167,7 @@ function setNoteBackgroundIfEncrypted(note) { function loadNote(noteId) { $.get(baseUrl + 'notes/' + noteId).then(function(note) { - globalNote = note; + globalCurrentNote = note; if (newNoteCreated) { newNoteCreated = false; @@ -212,19 +210,4 @@ function loadNote(noteId) { setNoteBackgroundIfEncrypted(note); }); }); -} - -function addRecentNote(noteTreeId, noteContentId) { - const origDate = new Date(); - - setTimeout(function() { - // we include the note into recent list only if the user stayed on the note at least 5 seconds - if (noteTreeId === globalNote.detail.note_id || noteContentId === globalNote.detail.note_id) { - // if it's already there, remove the note - globalRecentNotes = globalRecentNotes.filter(note => note !== noteTreeId); - - globalRecentNotes.unshift(noteTreeId); - } - }, 1500); -} - +} \ No newline at end of file diff --git a/static/js/recentnotes.js b/static/js/recentnotes.js new file mode 100644 index 000000000..4e46db81a --- /dev/null +++ b/static/js/recentnotes.js @@ -0,0 +1,69 @@ +let globalRecentNotes = []; + +function addRecentNote(noteTreeId, noteContentId) { + const origDate = new Date(); + + setTimeout(function() { + // we include the note into recent list only if the user stayed on the note at least 5 seconds + if (noteTreeId === globalCurrentNote.detail.note_id || noteContentId === globalCurrentNote.detail.note_id) { + // if it's already there, remove the note + globalRecentNotes = globalRecentNotes.filter(note => note !== noteTreeId); + + globalRecentNotes.unshift(noteTreeId); + } + }, 1500); +} + +$(document).bind('keydown', 'alt+q', function() { + $("#recentNotesDialog").dialog({ + modal: true, + width: 500 + }); + + let recentNotesSelectBox = $('#recentNotesSelectBox'); + + recentNotesSelectBox.find('option').remove(); + + // remove the current note + let recNotes = globalRecentNotes.filter(note => note !== globalCurrentNote.detail.note_id); + + $.each(recNotes, function(key, valueNoteId) { + let noteTitle = getFullName(valueNoteId); + + if (!noteTitle) { + return; + } + + let option = $("") + .attr("value", valueNoteId) + .text(noteTitle); + + // select the first one (most recent one) by default + if (key === 0) { + option.attr("selected", "selected"); + } + + recentNotesSelectBox.append(option); + }); +}); + +function setActiveNoteBasedOnRecentNotes() { + let noteId = $("#recentNotesSelectBox option:selected").val(); + + getNodeByKey(noteId).setActive(); + + $("#recentNotesDialog").dialog('close'); +} + +$('#recentNotesSelectBox').keydown(function(e) { + let key = e.which; + + if (key === 13)// the enter key code + { + setActiveNoteBasedOnRecentNotes(); + } +}); + +$('#recentNotesSelectBox').dblclick(function(e) { + setActiveNoteBasedOnRecentNotes(); +}); \ No newline at end of file diff --git a/static/js/tree.js b/static/js/tree.js index f89c8b188..5c3f9bdc7 100644 --- a/static/js/tree.js +++ b/static/js/tree.js @@ -25,7 +25,7 @@ function moveToNode(node, toNode) { url: baseUrl + 'notes/' + node.key + '/moveTo/' + toNode.key, type: 'PUT', contentType: "application/json", - success: function (result) { + success: function () { node.moveTo(toNode); toNode.setExpanded(true); @@ -66,12 +66,22 @@ function deleteNode(node) { } } -function getParentKey(node) { - return (node.getParent() === null || node.getParent().key === "root_1") ? "root" : node.getParent().key; -} +function moveNodeUp(node) { + if (node.getParent() !== null) { + $.ajax({ + url: baseUrl + 'notes/' + node.key + '/moveAfter/' + node.getParent().key, + type: 'PUT', + contentType: "application/json", + success: function () { + if (node.getParent() !== null && node.getParent().getChildren().length <= 1) { + node.getParent().folder = false; + node.getParent().renderTitle(); + } -function getParentEncryption(node) { - return node.getParent() === null ? 0 : node.getParent().data.encryption; + node.moveTo(node.getParent(), 'after'); + } + }); + } } const keybindings = { @@ -101,21 +111,7 @@ const keybindings = { } }, "shift+left": function(node) { - if (node.getParent() !== null) { - $.ajax({ - url: baseUrl + 'notes/' + node.key + '/moveAfter/' + node.getParent().key, - type: 'PUT', - contentType: "application/json", - success: function() { - if (node.getParent() !== null && node.getParent().getChildren().length <= 1) { - node.getParent().folder = false; - node.getParent().renderTitle(); - } - - node.moveTo(node.getParent(), 'after'); - } - }); - } + moveNodeUp(node); }, "shift+right": function(node) { let toNode = node.getPrevSibling(); @@ -132,30 +128,47 @@ const keybindings = { let globalAllNoteIds = []; -let globalTree; - -function getNodeByKey(noteId) { - return globalTree.fancytree('getNodeByKey', noteId); -} - -function getFullName(noteId) { - let note = getNodeByKey(noteId); - const path = []; - - while (note) { - path.push(note.title); - - note = note.getParent(); - } - - // remove "root" element - path.pop(); - - return path.reverse().join(" > "); -} +const globalTree = $("#tree"); let globalClipboardNoteId = null; +function prepareNoteTree(notes) { + for (const note of notes) { + globalAllNoteIds.push(note.note_id); + + if (note.encryption > 0) { + note.title = "[encrypted]"; + + note.extraClasses = "encrypted"; + } + else { + note.title = note.note_title; + + if (note.is_clone) { + note.title += " (clone)"; + } + } + + note.key = note.note_id; + note.expanded = note.is_expanded; + + if (note.children && note.children.length > 0) { + prepareNoteTree(note.children); + } + } +} + +function setExpandedToServer(note_id, is_expanded) { + expanded_num = is_expanded ? 1 : 0; + + $.ajax({ + url: baseUrl + 'notes/' + note_id + '/expanded/' + expanded_num, + type: 'PUT', + contentType: "application/json", + success: function(result) {} + }); +} + $(function(){ $.get(baseUrl + 'tree').then(resp => { const notes = resp.notes; @@ -165,46 +178,8 @@ $(function(){ startNoteId = document.location.hash.substr(1); // strip initial # } - function copyTitle(notes) { - for (const note of notes) { - globalAllNoteIds.push(note.note_id); + prepareNoteTree(notes); - if (note.encryption > 0) { - note.title = "[encrypted]"; - - note.extraClasses = "encrypted"; - } - else { - note.title = note.note_title; - - if (note.is_clone) { - note.title += " (clone)"; - } - } - - note.key = note.note_id; - note.expanded = note.is_expanded; - - if (note.children && note.children.length > 0) { - copyTitle(note.children); - } - } - } - - copyTitle(notes); - - function setExpanded(note_id, is_expanded) { - expanded_num = is_expanded ? 1 : 0; - - $.ajax({ - url: baseUrl + 'notes/' + note_id + '/expanded/' + expanded_num, - type: 'PUT', - contentType: "application/json", - success: function(result) {} - }); - } - - globalTree = $("#tree"); globalTree.fancytree({ autoScroll: true, extensions: ["hotkeys", "filter", "dnd"], @@ -215,10 +190,10 @@ $(function(){ saveNoteIfChanged(() => loadNote(node.note_id)); }, expand: function(event, data) { - setExpanded(data.node.key, true); + setExpandedToServer(data.node.key, true); }, collapse: function(event, data) { - setExpanded(data.node.key, false); + setExpandedToServer(data.node.key, false); }, init: function(event, data) { if (startNoteId) { @@ -242,133 +217,10 @@ $(function(){ 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: { - autoExpandMS: 600, - draggable: { // modify default jQuery draggable options - zIndex: 1000, - scroll: false, - containment: "parent", - revert: "invalid" - }, - preventRecursiveMoves: true, // Prevent dropping nodes on own descendants - preventVoidMoves: true, // Prevent dropping nodes 'before self', etc. - - dragStart: function(node, data) { - // This function MUST be defined to enable dragging for the tree. - // Return false to cancel dragging of node. - return true; - }, - dragEnter: function(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: function(node, data) { - // return false to prevent auto-expanding data.node on hover - }, - dragOver: function(node, data) { - }, - dragLeave: function(node, data) { - }, - dragStop: function(node, data) { - }, - dragDrop: function(node, data) { - // This function MUST be defined to enable dropping of items on the tree. - // data.hitMode is 'before', 'after', or 'over'. - - if (data.hitMode === "before") { - moveBeforeNode(data.otherNode, node); - } - else if (data.hitMode === "after") { - moveAfterNode(data.otherNode, node); - } - else if (data.hitMode === "over") { - moveToNode(data.otherNode, node); - } - else { - throw new Exception("Unknown hitMode=" + data.hitMode); - } - } - } + dnd: dragAndDropSetup }); - globalTree.contextmenu({ - delegate: "span.fancytree-title", - autoFocus: true, - menu: [ - {title: "Insert note here", cmd: "insertNoteHere", uiIcon: "ui-icon-pencil"}, - {title: "Insert child note", cmd: "insertChildNote", uiIcon: "ui-icon-pencil"}, - {title: "Delete", cmd: "delete", uiIcon: "ui-icon-trash"}, - {title: "----"}, - {title: "Cut", cmd: "cut", uiIcon: "ui-icon-scissors"}, - {title: "Copy / clone", cmd: "copy", uiIcon: "ui-icon-copy"}, - {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, - {title: "Paste into", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"} - ], - beforeOpen: function (event, ui) { - const node = $.ui.fancytree.getNode(ui.target); - // Modify menu entries depending on node status - globalTree.contextmenu("enableEntry", "pasteAfter", globalClipboardNoteId !== null); - globalTree.contextmenu("enableEntry", "pasteInto", globalClipboardNoteId !== null); - - // Activate node on right-click - node.setActive(); - // Disable tree keyboard handling - ui.menu.prevKeyboard = node.tree.options.keyboard; - node.tree.options.keyboard = false; - }, - close: function (event, ui) {}, - select: function (event, ui) { - const node = $.ui.fancytree.getNode(ui.target); - - if (ui.cmd === "insertNoteHere") { - const parentKey = getParentKey(node); - const encryption = getParentEncryption(node); - - createNote(node, parentKey, 'after', encryption); - } - else if (ui.cmd === "insertChildNote") { - createNote(node, node.key, 'into'); - } - else if (ui.cmd === "cut") { - globalClipboardNoteId = node.key; - } - else if (ui.cmd === "pasteAfter") { - const subjectNode = getNodeByKey(globalClipboardNoteId); - - moveAfterNode(subjectNode, node); - - globalClipboardNoteId = null; - } - else if (ui.cmd === "pasteInto") { - const subjectNode = getNodeByKey(globalClipboardNoteId); - - moveToNode(subjectNode, node); - - globalClipboardNoteId = null; - } - else if (ui.cmd === "delete") { - deleteNode(node); - } - else { - console.log("Unknown command: " + ui.cmd); - } - } - }); + globalTree.contextmenu(contextMenuSetup); }); }); diff --git a/static/js/treeutils.js b/static/js/treeutils.js new file mode 100644 index 000000000..6e5d52d23 --- /dev/null +++ b/static/js/treeutils.js @@ -0,0 +1,27 @@ +function getParentKey(node) { + return (node.getParent() === null || node.getParent().key === "root_1") ? "root" : node.getParent().key; +} + +function getParentEncryption(node) { + return node.getParent() === null ? 0 : node.getParent().data.encryption; +} + +function getNodeByKey(noteId) { + return globalTree.fancytree('getNodeByKey', noteId); +} + +function getFullName(noteId) { + let note = getNodeByKey(noteId); + const path = []; + + while (note) { + path.push(note.title); + + note = note.getParent(); + } + + // remove "root" element + path.pop(); + + return path.reverse().join(" > "); +} \ No newline at end of file diff --git a/static/js/utils.js b/static/js/utils.js index a0c7afdec..9e9c822d4 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -14,6 +14,21 @@ function error(str) { error.fadeOut(10000); } +function getAutocompleteItems(notes) { + const autocompleteItems = []; + + for (const noteId of notes) { + const fullName = getFullName(noteId); + + autocompleteItems.push({ + value: fullName + " (" + noteId + ")", + label: fullName + }); + } + + return autocompleteItems; +} + function uint8ToBase64(u8Arr) { const CHUNK_SIZE = 0x8000; //arbitrary number const length = u8Arr.length;