From 61474defffb5d67259c6f88545da7e1e063b3063 Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 12 Jan 2020 11:15:23 +0100 Subject: [PATCH] widgetizing tree WIP --- src/public/javascripts/desktop.js | 6 +- src/public/javascripts/dialogs/move_to.js | 2 +- src/public/javascripts/mobile.js | 3 +- .../javascripts/services/app_context.js | 11 +- .../javascripts/services/entrypoints.js | 15 +- .../services/frontend_script_api.js | 3 +- .../javascripts/services/note_detail.js | 11 +- .../javascripts/services/search_notes.js | 3 +- src/public/javascripts/services/tree.js | 229 ++---------------- .../javascripts/services/tree_keybindings.js | 2 +- .../javascripts/widgets/global_buttons.js | 3 +- src/public/javascripts/widgets/note_tree.js | 140 ++++++++++- src/public/javascripts/widgets/search_box.js | 3 +- 13 files changed, 188 insertions(+), 243 deletions(-) diff --git a/src/public/javascripts/desktop.js b/src/public/javascripts/desktop.js index 2ece01974..17a5c182d 100644 --- a/src/public/javascripts/desktop.js +++ b/src/public/javascripts/desktop.js @@ -39,7 +39,7 @@ window.glob.isDesktop = utils.isDesktop; window.glob.isMobile = utils.isMobile; // required for CKEditor image upload plugin -window.glob.getActiveNode = treeService.getActiveNode; +window.glob.getActiveNode = () => appContext.getMainNoteTree().getActiveNode(); window.glob.getHeaders = server.getHeaders; window.glob.showAddLinkDialog = () => import('./dialogs/add_link.js').then(d => d.showDialog()); window.glob.showIncludeNoteDialog = cb => import('./dialogs/include_note.js').then(d => d.showDialog(cb)); @@ -134,11 +134,11 @@ $noteTabContainer.on("click", ".export-note-button", function () { return; } - import('./dialogs/export.js').then(d => d.showDialog(treeService.getActiveNode(), 'single')); + import('./dialogs/export.js').then(d => d.showDialog(appContext.getMainNoteTree().getActiveNode(), 'single')); }); $noteTabContainer.on("click", ".import-files-button", - () => import('./dialogs/import.js').then(d => d.showDialog(treeService.getActiveNode()))); + () => import('./dialogs/import.js').then(d => d.showDialog(appContext.getMainNoteTree().getActiveNode()))); async function printActiveNote() { if ($(this).hasClass("disabled")) { diff --git a/src/public/javascripts/dialogs/move_to.js b/src/public/javascripts/dialogs/move_to.js index 68d570880..2f4d8edb7 100644 --- a/src/public/javascripts/dialogs/move_to.js +++ b/src/public/javascripts/dialogs/move_to.js @@ -39,7 +39,7 @@ export async function showDialog(nodes) { } async function moveNotesTo(notePath) { - const targetNode = await treeService.getNodeFromPath(notePath); + const targetNode = await appContext.getMainNoteTree().getNodeFromPath(notePath); await treeChangesService.moveToNode(movedNodes, targetNode); diff --git a/src/public/javascripts/mobile.js b/src/public/javascripts/mobile.js index 2d55e7fec..76b9910d4 100644 --- a/src/public/javascripts/mobile.js +++ b/src/public/javascripts/mobile.js @@ -6,6 +6,7 @@ import contextMenuWidget from "./services/context_menu.js"; import treeChangesService from "./services/branches.js"; import utils from "./services/utils.js"; import treeUtils from "./services/tree_utils.js"; +import appContext from "./services/app_context.js"; window.glob.isDesktop = utils.isDesktop; window.glob.isMobile = utils.isMobile; @@ -89,7 +90,7 @@ async function showTree() { } $detail.on("click", ".note-menu-button", async e => { - const node = treeService.getActiveNode(); + const node = appContext.getMainNoteTree().getActiveNode(); const branch = treeCache.getBranch(node.data.branchId); const note = await treeCache.getNote(node.data.noteId); const parentNote = await treeCache.getNote(branch.parentNoteId); diff --git a/src/public/javascripts/services/app_context.js b/src/public/javascripts/services/app_context.js index 5ecc22ec7..2bc5f17cd 100644 --- a/src/public/javascripts/services/app_context.js +++ b/src/public/javascripts/services/app_context.js @@ -17,11 +17,13 @@ class AppContext { showWidgets() { const $leftPane = $("#left-pane"); + this.noteTreeWidget = new NoteTreeWidget(this); + this.widgets = [ new GlobalButtonsWidget(this), new SearchBoxWidget(this), new SearchResultsWidget(this), - new NoteTreeWidget(this) + this.noteTreeWidget ]; for (const widget of this.widgets) { @@ -30,6 +32,13 @@ class AppContext { $leftPane.append($widget); } } + + /** + * @return {NoteTreeWidget} + */ + getMainNoteTree() { + return this.noteTreeWidget; + } } const appContext = new AppContext(); diff --git a/src/public/javascripts/services/entrypoints.js b/src/public/javascripts/services/entrypoints.js index 1a2c7867c..a34749ff7 100644 --- a/src/public/javascripts/services/entrypoints.js +++ b/src/public/javascripts/services/entrypoints.js @@ -10,6 +10,7 @@ import keyboardActionService from "./keyboard_actions.js"; import hoistedNoteService from "./hoisted_note.js"; import treeCache from "./tree_cache.js"; import server from "./server.js"; +import appContext from "./app_context.js"; const NOTE_REVISIONS = "../dialogs/note_revisions.js"; const OPTIONS = "../dialogs/options.js"; @@ -224,9 +225,7 @@ function registerEntrypoints() { }); keyboardActionService.setGlobalActionHandler("CloneNotesTo", () => import(CLONE_TO).then(d => { - const activeNode = treeService.getActiveNode(); - - const selectedOrActiveNodes = treeService.getSelectedOrActiveNodes(activeNode); + const selectedOrActiveNodes = appContext.getMainNoteTree().getSelectedOrActiveNodes(); const noteIds = selectedOrActiveNodes.map(node => node.data.noteId); @@ -234,9 +233,7 @@ function registerEntrypoints() { })); keyboardActionService.setGlobalActionHandler("MoveNotesTo", () => import(MOVE_TO).then(d => { - const activeNode = treeService.getActiveNode(); - - const selectedOrActiveNodes = treeService.getSelectedOrActiveNodes(activeNode); + const selectedOrActiveNodes = appContext.getMainNoteTree().getSelectedOrActiveNodes(); d.showDialog(selectedOrActiveNodes); })); @@ -259,14 +256,14 @@ function registerEntrypoints() { }); keyboardActionService.setGlobalActionHandler("EditBranchPrefix", async () => { - const node = treeService.getActiveNode(); + const node = appContext.getMainNoteTree().getActiveNode(); const editBranchPrefixDialog = await import("../dialogs/branch_prefix.js"); editBranchPrefixDialog.showDialog(node); }); keyboardActionService.setGlobalActionHandler("ToggleNoteHoisting", async () => { - const node = treeService.getActiveNode(); + const node = appContext.getMainNoteTree().getActiveNode(); hoistedNoteService.getHoistedNoteId().then(async hoistedNoteId => { if (node.data.noteId === hoistedNoteId) { @@ -283,7 +280,7 @@ function registerEntrypoints() { }); keyboardActionService.setGlobalActionHandler("SearchInSubtree", () => { - const node = treeService.getActiveNode(); + const node = appContext.getMainNoteTree().getActiveNode(); searchNotesService.searchInSubtree(node.data.noteId); }); diff --git a/src/public/javascripts/services/frontend_script_api.js b/src/public/javascripts/services/frontend_script_api.js index 1760e21c0..9f7ac3ea3 100644 --- a/src/public/javascripts/services/frontend_script_api.js +++ b/src/public/javascripts/services/frontend_script_api.js @@ -11,6 +11,7 @@ import dateNotesService from './date_notes.js'; import StandardWidget from '../widgets/standard_widget.js'; import ws from "./ws.js"; import hoistedNoteService from "./hoisted_note.js"; +import appContext from "./app_context.js"; /** * This is the main frontend API interface for scripts. It's published in the local "api" object. @@ -49,7 +50,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte */ this.activateNote = async (notePath, noteLoadedListener) => { await treeService.activateNote(notePath, async () => { - await treeService.scrollToActiveNote(); + await appContext.getMainNoteTree().scrollToActiveNote(); if (noteLoadedListener) { noteLoadedListener(); diff --git a/src/public/javascripts/services/note_detail.js b/src/public/javascripts/services/note_detail.js index ed7a03474..835c66880 100644 --- a/src/public/javascripts/services/note_detail.js +++ b/src/public/javascripts/services/note_detail.js @@ -9,6 +9,7 @@ import contextMenuService from "./context_menu.js"; import treeUtils from "./tree_utils.js"; import tabRow from "./tab_row.js"; import keyboardActionService from "./keyboard_actions.js"; +import appContext from "./app_context.js"; const $tabContentsContainer = $("#note-tab-container"); const $savedIndicator = $(".saved-indicator"); @@ -161,22 +162,20 @@ async function showTab(tabId) { } } - const oldActiveNode = treeService.getActiveNode(); + const oldActiveNode = appContext.getMainNoteTree().getActiveNode(); if (oldActiveNode) { oldActiveNode.setActive(false); } - treeService.clearSelectedNodes(); - const newActiveTabContext = getActiveTabContext(); if (newActiveTabContext && newActiveTabContext.notePath) { - const newActiveNode = await treeService.getNodeFromPath(newActiveTabContext.notePath); + const newActiveNode = await appContext.getMainNoteTree().getNodeFromPath(newActiveTabContext.notePath); if (newActiveNode) { if (!newActiveNode.isVisible()) { - await treeService.expandToNote(newActiveTabContext.notePath); + await appContext.getMainNoteTree().expandToNote(newActiveTabContext.notePath); } newActiveNode.setActive(true, {noEvents: true}); @@ -227,7 +226,7 @@ async function loadNoteDetail(origNotePath, options = {}) { // this is useful when user quickly switches notes (by e.g. holding down arrow) so that we don't // try to render all those loaded notes one after each other. This only guarantees that correct note // will be displayed independent of timing - const currentTreeNode = treeService.getActiveNode(); + const currentTreeNode = appContext.getMainNoteTree().getActiveNode(); if (!newTab && currentTreeNode && currentTreeNode.data.noteId !== loadedNote.noteId) { return; } diff --git a/src/public/javascripts/services/search_notes.js b/src/public/javascripts/services/search_notes.js index 1d31615be..7496fd258 100644 --- a/src/public/javascripts/services/search_notes.js +++ b/src/public/javascripts/services/search_notes.js @@ -2,9 +2,10 @@ import treeService from './tree.js'; import treeCache from "./tree_cache.js"; import server from './server.js'; import toastService from "./toast.js"; +import appContext from "./app_context.js"; async function refreshSearch() { - const activeNode = treeService.getActiveNode(); + const activeNode = appContext.getMainNoteTree().getActiveNode(); activeNode.load(true); activeNode.setExpanded(true); diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index 25a9588e3..e6ee1e466 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -11,50 +11,11 @@ import hoistedNoteService from '../services/hoisted_note.js'; import optionsService from "../services/options.js"; import bundle from "./bundle.js"; import keyboardActionService from "./keyboard_actions.js"; - -let tree; - -function setTree(treeInstance) { - tree = treeInstance; -} +import appContext from "./app_context.js"; let setFrontendAsLoaded; const frontendLoaded = new Promise(resolve => { setFrontendAsLoaded = resolve; }); -/** - * focused & not active node can happen during multiselection where the node is selected but not activated - * (its content is not displayed in the detail) - * @return {FancytreeNode|null} - */ -function getFocusedNode() { - return tree.getFocusNode(); -} - -/** - * note that if you want to access data like noteId or isProtected, you need to go into "data" property - * @return {FancytreeNode|null} - */ -function getActiveNode() { - return tree.getActiveNode(); -} - -/** @return {FancytreeNode[]} */ -async function getNodesByBranchId(branchId) { - utils.assertArguments(branchId); - - const branch = treeCache.getBranch(branchId); - - return getNodesByNoteId(branch.noteId).filter(node => node.data.branchId === branchId); -} - -/** @return {FancytreeNode[]} */ -function getNodesByNoteId(noteId) { - utils.assertArguments(noteId); - - const list = tree.getNodesByRef(noteId); - return list ? list : []; // if no nodes with this refKey are found, fancy tree returns null -} - async function setPrefix(branchId, prefix) { utils.assertArguments(branchId); @@ -62,7 +23,7 @@ async function setPrefix(branchId, prefix) { branch.prefix = prefix; - for (const node of await getNodesByBranchId(branchId)) { + for (const node of await appContext.getMainNoteTree().getNodesByBranchId(branchId)) { await setNodeTitleWithPrefix(node); } } @@ -78,80 +39,6 @@ async function setNodeTitleWithPrefix(node) { node.setTitle(utils.escapeHtml(title)); } -/** @return {FancytreeNode} */ -async function expandToNote(notePath, expandOpts) { - return await getNodeFromPath(notePath, true, expandOpts); -} - -/** @return {FancytreeNode} */ -function findChildNode(parentNode, childNoteId) { - let foundChildNode = null; - - for (const childNode of parentNode.getChildren()) { - if (childNode.data.noteId === childNoteId) { - foundChildNode = childNode; - break; - } - } - - return foundChildNode; -} - -/** @return {FancytreeNode} */ -async function getNodeFromPath(notePath, expand = false, expandOpts = {}) { - utils.assertArguments(notePath); - - const hoistedNoteId = await hoistedNoteService.getHoistedNoteId(); - /** @var {FancytreeNode} */ - let parentNode = null; - - const runPath = await getRunPath(notePath); - - if (!runPath) { - console.error("Could not find run path for notePath:", notePath); - return; - } - - for (const childNoteId of runPath) { - if (childNoteId === hoistedNoteId) { - // there must be exactly one node with given hoistedNoteId - parentNode = getNodesByNoteId(childNoteId)[0]; - - continue; - } - - // we expand only after hoisted note since before then nodes are not actually present in the tree - if (parentNode) { - if (!parentNode.isLoaded()) { - await parentNode.load(); - } - - if (expand) { - await parentNode.setExpanded(true, expandOpts); - } - - await checkFolderStatus(parentNode); - - let foundChildNode = findChildNode(parentNode, childNoteId); - - if (!foundChildNode) { // note might be recently created so we'll force reload and try again - await parentNode.load(true); - - foundChildNode = findChildNode(parentNode, childNoteId); - - if (!foundChildNode) { - ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteId}, requested path is ${notePath}`); - return; - } - } - - parentNode = foundChildNode; - } - } - - return parentNode; -} - /** @return {FancytreeNode} */ async function activateNote(notePath, noteLoadedListener) { utils.assertArguments(notePath); @@ -180,7 +67,7 @@ async function activateNote(notePath, noteLoadedListener) { utils.closeActiveDialog(); - const node = await expandToNote(notePath); + const node = await appContext.getMainNoteTree().expandToNote(notePath); if (noteLoadedListener) { noteDetailService.addDetailLoadedListener(node.data.noteId, noteLoadedListener); @@ -188,8 +75,6 @@ async function activateNote(notePath, noteLoadedListener) { await node.setActive(true); - clearSelectedNodes(); - return node; } @@ -329,23 +214,6 @@ function getSelectedNodes(stopOnParents = false) { return tree.getSelectedNodes(stopOnParents); } -/** @return {FancytreeNode[]} */ -function getSelectedOrActiveNodes(node) { - let notes = getSelectedNodes(true); - - if (notes.length === 0) { - notes.push(node); - } - - return notes; -} - -function clearSelectedNodes() { - for (const selectedNode of getSelectedNodes()) { - selectedNode.setSelected(false); - } -} - async function treeInitialized() { if (noteDetailService.getTabContexts().length > 0) { // this is just tree reload - tabs are already in place @@ -427,13 +295,15 @@ async function treeInitialized() { async function reload() { const notes = await loadTreeData(); - const activeNotePath = getActiveNode() !== null ? await treeUtils.getNotePath(getActiveNode()) : null; + const activeNode = appContext.getMainNoteTree().getActiveNode(); - await tree.reload(notes); + const activeNotePath = activeNode !== null ? await treeUtils.getNotePath(activeNode) : null; + + await appContext.getMainNoteTree().reload(notes); // reactivate originally activated node, but don't trigger note loading if (activeNotePath) { - const node = await getNodeFromPath(activeNotePath, true); + const node = await appContext.getMainNoteTree().getNodeFromPath(activeNotePath, true); await node.setActive(true, {noEvents: true}); } @@ -461,37 +331,8 @@ async function loadTreeData() { return await treeBuilder.prepareTree(); } -async function collapseTree(node = null) { - if (!node) { - const hoistedNoteId = await hoistedNoteService.getHoistedNoteId(); - - node = getNodesByNoteId(hoistedNoteId)[0]; - } - - node.setExpanded(false); - - node.visit(node => node.setExpanded(false)); -} - -function focusTree() { - tree.setFocus(); -} - -async function scrollToActiveNote() { - const activeContext = noteDetailService.getActiveTabContext(); - - if (activeContext && activeContext.notePath) { - focusTree(); - - const node = await expandToNote(activeContext.notePath); - - await node.makeVisible({scrollIntoView: true}); - node.setFocus(); - } -} - function setProtected(noteId, isProtected) { - getNodesByNoteId(noteId).map(node => { + appContext.getMainNoteTree().getNodesByNoteId(noteId).map(node => { node.data.isProtected = isProtected; node.toggleClass("protected", isProtected); }); @@ -504,7 +345,7 @@ async function setNoteTitle(noteId, title) { note.title = title; - for (const clone of getNodesByNoteId(noteId)) { + for (const clone of appContext.getMainNoteTree().getNodesByNoteId(noteId)) { await setNodeTitleWithPrefix(clone); } } @@ -512,7 +353,7 @@ async function setNoteTitle(noteId, title) { async function createNewTopLevelNote() { const hoistedNoteId = await hoistedNoteService.getHoistedNoteId(); - const rootNode = getNodesByNoteId(hoistedNoteId)[0]; + const rootNode = appContext.getMainNoteTree().getNodesByNoteId(hoistedNoteId)[0]; await createNote(rootNode, hoistedNoteId, "into"); } @@ -605,13 +446,11 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) { await newNode.setActive(true); } - clearSelectedNodes(); // to unmark previously active node - // need to refresh because original doesn't have methods like .getParent() - newNodeData = getNodesByNoteId(branchEntity.noteId)[0]; + newNodeData = appContext.getMainNoteTree().getNodesByNoteId(branchEntity.noteId)[0]; // following for cycle will make sure that also clones of a parent are refreshed - for (const newParentNode of getNodesByNoteId(parentNoteId)) { + for (const newParentNode of appContext.getMainNoteTree().getNodesByNoteId(parentNoteId)) { if (newParentNode.key === newNodeData.getParent().key) { // we've added a note into this one so no need to refresh continue; @@ -619,7 +458,7 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) { await newParentNode.load(true); // force reload to show up new note - await checkFolderStatus(newParentNode); + await appContext.getMainNoteTree().checkFolderStatus(newParentNode); } return {note, branch}; @@ -688,7 +527,7 @@ ws.subscribeToOutsideSyncMessages(async syncData => { }); keyboardActionService.setGlobalActionHandler('CreateNoteAfter', async () => { - const node = getActiveNode(); + const node = appContext.getMainNoteTree().getActiveNode(); const parentNoteId = node.data.parentNoteId; const isProtected = await treeUtils.getParentProtectedStatus(node); @@ -703,7 +542,7 @@ keyboardActionService.setGlobalActionHandler('CreateNoteAfter', async () => { }); async function createNoteInto(saveSelection = false) { - const node = getActiveNode(); + const node = appContext.getMainNoteTree().getActiveNode(); if (node) { await createNote(node, node.data.noteId, 'into', { @@ -713,15 +552,6 @@ async function createNoteInto(saveSelection = false) { } } -async function checkFolderStatus(node) { - const note = await treeCache.getNote(node.data.noteId); - - node.folder = note.type === 'search' || note.getChildNoteIds().length > 0; - node.icon = await treeBuilder.getIcon(note); - node.extraClasses = await treeBuilder.getExtraClasses(note); - node.renderTitle(); -} - async function reloadNotes(noteIds, activateNotePath = null) { if (noteIds.length === 0) { return; @@ -734,7 +564,7 @@ async function reloadNotes(noteIds, activateNotePath = null) { } for (const noteId of noteIds) { - for (const node of getNodesByNoteId(noteId)) { + for (const node of appContext.getMainNoteTree().getNodesByNoteId(noteId)) { const branch = treeCache.getBranch(node.data.branchId, true); if (!branch) { @@ -743,13 +573,13 @@ async function reloadNotes(noteIds, activateNotePath = null) { else { await node.load(true); - await checkFolderStatus(node); + await appContext.getMainNoteTree().checkFolderStatus(node); } } } if (activateNotePath) { - const node = await getNodeFromPath(activateNotePath); + const node = await appContext.getMainNoteTree().getNodeFromPath(activateNotePath); if (node && !node.isActive()) { await node.setActive(true); @@ -763,7 +593,7 @@ keyboardActionService.setGlobalActionHandler('CutIntoNote', () => createNoteInto keyboardActionService.setGlobalActionHandler('CreateNoteInto', createNoteInto); -keyboardActionService.setGlobalActionHandler('ScrollToActiveNote', scrollToActiveNote); +keyboardActionService.setGlobalActionHandler('ScrollToActiveNote', () => appContext.getMainNoteTree().scrollToActiveNote()); $(window).bind('hashchange', async function() { if (isNotePathInAddress()) { @@ -784,40 +614,23 @@ async function duplicateNote(noteId, parentNoteId) { toastService.showMessage(`Note "${origNote.title}" has been duplicated`); } -function getNodeByKey(key) { - return tree.getNodeByKey(key); -} - frontendLoaded.then(bundle.executeStartupBundles); export default { reload, - collapseTree, setProtected, activateNote, - getFocusedNode, - getActiveNode, setNoteTitle, setPrefix, createNote, - getSelectedNodes, - getSelectedOrActiveNodes, - clearSelectedNodes, sortAlphabetically, loadTreeData, treeInitialized, setExpandedToServer, - getNodesByNoteId, - checkFolderStatus, reloadNotes, - expandToNote, - getNodeFromPath, resolveNotePath, getSomeNotePath, - focusTree, - scrollToActiveNote, createNewTopLevelNote, duplicateNote, - getNodeByKey, - setTree + getRunPath }; \ No newline at end of file diff --git a/src/public/javascripts/services/tree_keybindings.js b/src/public/javascripts/services/tree_keybindings.js index 45374c37a..b1e99e624 100644 --- a/src/public/javascripts/services/tree_keybindings.js +++ b/src/public/javascripts/services/tree_keybindings.js @@ -87,7 +87,7 @@ function getTemplates(treeWidget) { return false; }, "AddNoteAboveToSelection": () => { - const node = treeService.getFocusedNode(); + const node = treeWidget.getFocusedNode(); if (!node) { return; diff --git a/src/public/javascripts/widgets/global_buttons.js b/src/public/javascripts/widgets/global_buttons.js index 9da161d80..3c5963851 100644 --- a/src/public/javascripts/widgets/global_buttons.js +++ b/src/public/javascripts/widgets/global_buttons.js @@ -1,4 +1,5 @@ import BasicWidget from "./basic_widget.js"; +import appContext from "../services/app_context.js"; const WIDGET_TPL = `