widgetizing tree WIP

This commit is contained in:
zadam 2020-01-12 11:15:23 +01:00
parent b12e38c231
commit 61474defff
13 changed files with 188 additions and 243 deletions

View File

@ -39,7 +39,7 @@ window.glob.isDesktop = utils.isDesktop;
window.glob.isMobile = utils.isMobile; window.glob.isMobile = utils.isMobile;
// required for CKEditor image upload plugin // required for CKEditor image upload plugin
window.glob.getActiveNode = treeService.getActiveNode; window.glob.getActiveNode = () => appContext.getMainNoteTree().getActiveNode();
window.glob.getHeaders = server.getHeaders; window.glob.getHeaders = server.getHeaders;
window.glob.showAddLinkDialog = () => import('./dialogs/add_link.js').then(d => d.showDialog()); 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)); 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; 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", $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() { async function printActiveNote() {
if ($(this).hasClass("disabled")) { if ($(this).hasClass("disabled")) {

View File

@ -39,7 +39,7 @@ export async function showDialog(nodes) {
} }
async function moveNotesTo(notePath) { async function moveNotesTo(notePath) {
const targetNode = await treeService.getNodeFromPath(notePath); const targetNode = await appContext.getMainNoteTree().getNodeFromPath(notePath);
await treeChangesService.moveToNode(movedNodes, targetNode); await treeChangesService.moveToNode(movedNodes, targetNode);

View File

@ -6,6 +6,7 @@ import contextMenuWidget from "./services/context_menu.js";
import treeChangesService from "./services/branches.js"; import treeChangesService from "./services/branches.js";
import utils from "./services/utils.js"; import utils from "./services/utils.js";
import treeUtils from "./services/tree_utils.js"; import treeUtils from "./services/tree_utils.js";
import appContext from "./services/app_context.js";
window.glob.isDesktop = utils.isDesktop; window.glob.isDesktop = utils.isDesktop;
window.glob.isMobile = utils.isMobile; window.glob.isMobile = utils.isMobile;
@ -89,7 +90,7 @@ async function showTree() {
} }
$detail.on("click", ".note-menu-button", async e => { $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 branch = treeCache.getBranch(node.data.branchId);
const note = await treeCache.getNote(node.data.noteId); const note = await treeCache.getNote(node.data.noteId);
const parentNote = await treeCache.getNote(branch.parentNoteId); const parentNote = await treeCache.getNote(branch.parentNoteId);

View File

@ -17,11 +17,13 @@ class AppContext {
showWidgets() { showWidgets() {
const $leftPane = $("#left-pane"); const $leftPane = $("#left-pane");
this.noteTreeWidget = new NoteTreeWidget(this);
this.widgets = [ this.widgets = [
new GlobalButtonsWidget(this), new GlobalButtonsWidget(this),
new SearchBoxWidget(this), new SearchBoxWidget(this),
new SearchResultsWidget(this), new SearchResultsWidget(this),
new NoteTreeWidget(this) this.noteTreeWidget
]; ];
for (const widget of this.widgets) { for (const widget of this.widgets) {
@ -30,6 +32,13 @@ class AppContext {
$leftPane.append($widget); $leftPane.append($widget);
} }
} }
/**
* @return {NoteTreeWidget}
*/
getMainNoteTree() {
return this.noteTreeWidget;
}
} }
const appContext = new AppContext(); const appContext = new AppContext();

View File

@ -10,6 +10,7 @@ import keyboardActionService from "./keyboard_actions.js";
import hoistedNoteService from "./hoisted_note.js"; import hoistedNoteService from "./hoisted_note.js";
import treeCache from "./tree_cache.js"; import treeCache from "./tree_cache.js";
import server from "./server.js"; import server from "./server.js";
import appContext from "./app_context.js";
const NOTE_REVISIONS = "../dialogs/note_revisions.js"; const NOTE_REVISIONS = "../dialogs/note_revisions.js";
const OPTIONS = "../dialogs/options.js"; const OPTIONS = "../dialogs/options.js";
@ -224,9 +225,7 @@ function registerEntrypoints() {
}); });
keyboardActionService.setGlobalActionHandler("CloneNotesTo", () => import(CLONE_TO).then(d => { keyboardActionService.setGlobalActionHandler("CloneNotesTo", () => import(CLONE_TO).then(d => {
const activeNode = treeService.getActiveNode(); const selectedOrActiveNodes = appContext.getMainNoteTree().getSelectedOrActiveNodes();
const selectedOrActiveNodes = treeService.getSelectedOrActiveNodes(activeNode);
const noteIds = selectedOrActiveNodes.map(node => node.data.noteId); const noteIds = selectedOrActiveNodes.map(node => node.data.noteId);
@ -234,9 +233,7 @@ function registerEntrypoints() {
})); }));
keyboardActionService.setGlobalActionHandler("MoveNotesTo", () => import(MOVE_TO).then(d => { keyboardActionService.setGlobalActionHandler("MoveNotesTo", () => import(MOVE_TO).then(d => {
const activeNode = treeService.getActiveNode(); const selectedOrActiveNodes = appContext.getMainNoteTree().getSelectedOrActiveNodes();
const selectedOrActiveNodes = treeService.getSelectedOrActiveNodes(activeNode);
d.showDialog(selectedOrActiveNodes); d.showDialog(selectedOrActiveNodes);
})); }));
@ -259,14 +256,14 @@ function registerEntrypoints() {
}); });
keyboardActionService.setGlobalActionHandler("EditBranchPrefix", async () => { keyboardActionService.setGlobalActionHandler("EditBranchPrefix", async () => {
const node = treeService.getActiveNode(); const node = appContext.getMainNoteTree().getActiveNode();
const editBranchPrefixDialog = await import("../dialogs/branch_prefix.js"); const editBranchPrefixDialog = await import("../dialogs/branch_prefix.js");
editBranchPrefixDialog.showDialog(node); editBranchPrefixDialog.showDialog(node);
}); });
keyboardActionService.setGlobalActionHandler("ToggleNoteHoisting", async () => { keyboardActionService.setGlobalActionHandler("ToggleNoteHoisting", async () => {
const node = treeService.getActiveNode(); const node = appContext.getMainNoteTree().getActiveNode();
hoistedNoteService.getHoistedNoteId().then(async hoistedNoteId => { hoistedNoteService.getHoistedNoteId().then(async hoistedNoteId => {
if (node.data.noteId === hoistedNoteId) { if (node.data.noteId === hoistedNoteId) {
@ -283,7 +280,7 @@ function registerEntrypoints() {
}); });
keyboardActionService.setGlobalActionHandler("SearchInSubtree", () => { keyboardActionService.setGlobalActionHandler("SearchInSubtree", () => {
const node = treeService.getActiveNode(); const node = appContext.getMainNoteTree().getActiveNode();
searchNotesService.searchInSubtree(node.data.noteId); searchNotesService.searchInSubtree(node.data.noteId);
}); });

View File

@ -11,6 +11,7 @@ import dateNotesService from './date_notes.js';
import StandardWidget from '../widgets/standard_widget.js'; import StandardWidget from '../widgets/standard_widget.js';
import ws from "./ws.js"; import ws from "./ws.js";
import hoistedNoteService from "./hoisted_note.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. * 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) => { this.activateNote = async (notePath, noteLoadedListener) => {
await treeService.activateNote(notePath, async () => { await treeService.activateNote(notePath, async () => {
await treeService.scrollToActiveNote(); await appContext.getMainNoteTree().scrollToActiveNote();
if (noteLoadedListener) { if (noteLoadedListener) {
noteLoadedListener(); noteLoadedListener();

View File

@ -9,6 +9,7 @@ import contextMenuService from "./context_menu.js";
import treeUtils from "./tree_utils.js"; import treeUtils from "./tree_utils.js";
import tabRow from "./tab_row.js"; import tabRow from "./tab_row.js";
import keyboardActionService from "./keyboard_actions.js"; import keyboardActionService from "./keyboard_actions.js";
import appContext from "./app_context.js";
const $tabContentsContainer = $("#note-tab-container"); const $tabContentsContainer = $("#note-tab-container");
const $savedIndicator = $(".saved-indicator"); const $savedIndicator = $(".saved-indicator");
@ -161,22 +162,20 @@ async function showTab(tabId) {
} }
} }
const oldActiveNode = treeService.getActiveNode(); const oldActiveNode = appContext.getMainNoteTree().getActiveNode();
if (oldActiveNode) { if (oldActiveNode) {
oldActiveNode.setActive(false); oldActiveNode.setActive(false);
} }
treeService.clearSelectedNodes();
const newActiveTabContext = getActiveTabContext(); const newActiveTabContext = getActiveTabContext();
if (newActiveTabContext && newActiveTabContext.notePath) { if (newActiveTabContext && newActiveTabContext.notePath) {
const newActiveNode = await treeService.getNodeFromPath(newActiveTabContext.notePath); const newActiveNode = await appContext.getMainNoteTree().getNodeFromPath(newActiveTabContext.notePath);
if (newActiveNode) { if (newActiveNode) {
if (!newActiveNode.isVisible()) { if (!newActiveNode.isVisible()) {
await treeService.expandToNote(newActiveTabContext.notePath); await appContext.getMainNoteTree().expandToNote(newActiveTabContext.notePath);
} }
newActiveNode.setActive(true, {noEvents: true}); 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 // 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 // try to render all those loaded notes one after each other. This only guarantees that correct note
// will be displayed independent of timing // will be displayed independent of timing
const currentTreeNode = treeService.getActiveNode(); const currentTreeNode = appContext.getMainNoteTree().getActiveNode();
if (!newTab && currentTreeNode && currentTreeNode.data.noteId !== loadedNote.noteId) { if (!newTab && currentTreeNode && currentTreeNode.data.noteId !== loadedNote.noteId) {
return; return;
} }

View File

@ -2,9 +2,10 @@ import treeService from './tree.js';
import treeCache from "./tree_cache.js"; import treeCache from "./tree_cache.js";
import server from './server.js'; import server from './server.js';
import toastService from "./toast.js"; import toastService from "./toast.js";
import appContext from "./app_context.js";
async function refreshSearch() { async function refreshSearch() {
const activeNode = treeService.getActiveNode(); const activeNode = appContext.getMainNoteTree().getActiveNode();
activeNode.load(true); activeNode.load(true);
activeNode.setExpanded(true); activeNode.setExpanded(true);

View File

@ -11,50 +11,11 @@ import hoistedNoteService from '../services/hoisted_note.js';
import optionsService from "../services/options.js"; import optionsService from "../services/options.js";
import bundle from "./bundle.js"; import bundle from "./bundle.js";
import keyboardActionService from "./keyboard_actions.js"; import keyboardActionService from "./keyboard_actions.js";
import appContext from "./app_context.js";
let tree;
function setTree(treeInstance) {
tree = treeInstance;
}
let setFrontendAsLoaded; let setFrontendAsLoaded;
const frontendLoaded = new Promise(resolve => { setFrontendAsLoaded = resolve; }); 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) { async function setPrefix(branchId, prefix) {
utils.assertArguments(branchId); utils.assertArguments(branchId);
@ -62,7 +23,7 @@ async function setPrefix(branchId, prefix) {
branch.prefix = prefix; branch.prefix = prefix;
for (const node of await getNodesByBranchId(branchId)) { for (const node of await appContext.getMainNoteTree().getNodesByBranchId(branchId)) {
await setNodeTitleWithPrefix(node); await setNodeTitleWithPrefix(node);
} }
} }
@ -78,80 +39,6 @@ async function setNodeTitleWithPrefix(node) {
node.setTitle(utils.escapeHtml(title)); 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} */ /** @return {FancytreeNode} */
async function activateNote(notePath, noteLoadedListener) { async function activateNote(notePath, noteLoadedListener) {
utils.assertArguments(notePath); utils.assertArguments(notePath);
@ -180,7 +67,7 @@ async function activateNote(notePath, noteLoadedListener) {
utils.closeActiveDialog(); utils.closeActiveDialog();
const node = await expandToNote(notePath); const node = await appContext.getMainNoteTree().expandToNote(notePath);
if (noteLoadedListener) { if (noteLoadedListener) {
noteDetailService.addDetailLoadedListener(node.data.noteId, noteLoadedListener); noteDetailService.addDetailLoadedListener(node.data.noteId, noteLoadedListener);
@ -188,8 +75,6 @@ async function activateNote(notePath, noteLoadedListener) {
await node.setActive(true); await node.setActive(true);
clearSelectedNodes();
return node; return node;
} }
@ -329,23 +214,6 @@ function getSelectedNodes(stopOnParents = false) {
return tree.getSelectedNodes(stopOnParents); 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() { async function treeInitialized() {
if (noteDetailService.getTabContexts().length > 0) { if (noteDetailService.getTabContexts().length > 0) {
// this is just tree reload - tabs are already in place // this is just tree reload - tabs are already in place
@ -427,13 +295,15 @@ async function treeInitialized() {
async function reload() { async function reload() {
const notes = await loadTreeData(); 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 // reactivate originally activated node, but don't trigger note loading
if (activeNotePath) { if (activeNotePath) {
const node = await getNodeFromPath(activeNotePath, true); const node = await appContext.getMainNoteTree().getNodeFromPath(activeNotePath, true);
await node.setActive(true, {noEvents: true}); await node.setActive(true, {noEvents: true});
} }
@ -461,37 +331,8 @@ async function loadTreeData() {
return await treeBuilder.prepareTree(); 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) { function setProtected(noteId, isProtected) {
getNodesByNoteId(noteId).map(node => { appContext.getMainNoteTree().getNodesByNoteId(noteId).map(node => {
node.data.isProtected = isProtected; node.data.isProtected = isProtected;
node.toggleClass("protected", isProtected); node.toggleClass("protected", isProtected);
}); });
@ -504,7 +345,7 @@ async function setNoteTitle(noteId, title) {
note.title = title; note.title = title;
for (const clone of getNodesByNoteId(noteId)) { for (const clone of appContext.getMainNoteTree().getNodesByNoteId(noteId)) {
await setNodeTitleWithPrefix(clone); await setNodeTitleWithPrefix(clone);
} }
} }
@ -512,7 +353,7 @@ async function setNoteTitle(noteId, title) {
async function createNewTopLevelNote() { async function createNewTopLevelNote() {
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId(); const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
const rootNode = getNodesByNoteId(hoistedNoteId)[0]; const rootNode = appContext.getMainNoteTree().getNodesByNoteId(hoistedNoteId)[0];
await createNote(rootNode, hoistedNoteId, "into"); await createNote(rootNode, hoistedNoteId, "into");
} }
@ -605,13 +446,11 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
await newNode.setActive(true); await newNode.setActive(true);
} }
clearSelectedNodes(); // to unmark previously active node
// need to refresh because original doesn't have methods like .getParent() // 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 // 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) { if (newParentNode.key === newNodeData.getParent().key) {
// we've added a note into this one so no need to refresh // we've added a note into this one so no need to refresh
continue; continue;
@ -619,7 +458,7 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
await newParentNode.load(true); // force reload to show up new note await newParentNode.load(true); // force reload to show up new note
await checkFolderStatus(newParentNode); await appContext.getMainNoteTree().checkFolderStatus(newParentNode);
} }
return {note, branch}; return {note, branch};
@ -688,7 +527,7 @@ ws.subscribeToOutsideSyncMessages(async syncData => {
}); });
keyboardActionService.setGlobalActionHandler('CreateNoteAfter', async () => { keyboardActionService.setGlobalActionHandler('CreateNoteAfter', async () => {
const node = getActiveNode(); const node = appContext.getMainNoteTree().getActiveNode();
const parentNoteId = node.data.parentNoteId; const parentNoteId = node.data.parentNoteId;
const isProtected = await treeUtils.getParentProtectedStatus(node); const isProtected = await treeUtils.getParentProtectedStatus(node);
@ -703,7 +542,7 @@ keyboardActionService.setGlobalActionHandler('CreateNoteAfter', async () => {
}); });
async function createNoteInto(saveSelection = false) { async function createNoteInto(saveSelection = false) {
const node = getActiveNode(); const node = appContext.getMainNoteTree().getActiveNode();
if (node) { if (node) {
await createNote(node, node.data.noteId, 'into', { 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) { async function reloadNotes(noteIds, activateNotePath = null) {
if (noteIds.length === 0) { if (noteIds.length === 0) {
return; return;
@ -734,7 +564,7 @@ async function reloadNotes(noteIds, activateNotePath = null) {
} }
for (const noteId of noteIds) { 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); const branch = treeCache.getBranch(node.data.branchId, true);
if (!branch) { if (!branch) {
@ -743,13 +573,13 @@ async function reloadNotes(noteIds, activateNotePath = null) {
else { else {
await node.load(true); await node.load(true);
await checkFolderStatus(node); await appContext.getMainNoteTree().checkFolderStatus(node);
} }
} }
} }
if (activateNotePath) { if (activateNotePath) {
const node = await getNodeFromPath(activateNotePath); const node = await appContext.getMainNoteTree().getNodeFromPath(activateNotePath);
if (node && !node.isActive()) { if (node && !node.isActive()) {
await node.setActive(true); await node.setActive(true);
@ -763,7 +593,7 @@ keyboardActionService.setGlobalActionHandler('CutIntoNote', () => createNoteInto
keyboardActionService.setGlobalActionHandler('CreateNoteInto', createNoteInto); keyboardActionService.setGlobalActionHandler('CreateNoteInto', createNoteInto);
keyboardActionService.setGlobalActionHandler('ScrollToActiveNote', scrollToActiveNote); keyboardActionService.setGlobalActionHandler('ScrollToActiveNote', () => appContext.getMainNoteTree().scrollToActiveNote());
$(window).bind('hashchange', async function() { $(window).bind('hashchange', async function() {
if (isNotePathInAddress()) { if (isNotePathInAddress()) {
@ -784,40 +614,23 @@ async function duplicateNote(noteId, parentNoteId) {
toastService.showMessage(`Note "${origNote.title}" has been duplicated`); toastService.showMessage(`Note "${origNote.title}" has been duplicated`);
} }
function getNodeByKey(key) {
return tree.getNodeByKey(key);
}
frontendLoaded.then(bundle.executeStartupBundles); frontendLoaded.then(bundle.executeStartupBundles);
export default { export default {
reload, reload,
collapseTree,
setProtected, setProtected,
activateNote, activateNote,
getFocusedNode,
getActiveNode,
setNoteTitle, setNoteTitle,
setPrefix, setPrefix,
createNote, createNote,
getSelectedNodes,
getSelectedOrActiveNodes,
clearSelectedNodes,
sortAlphabetically, sortAlphabetically,
loadTreeData, loadTreeData,
treeInitialized, treeInitialized,
setExpandedToServer, setExpandedToServer,
getNodesByNoteId,
checkFolderStatus,
reloadNotes, reloadNotes,
expandToNote,
getNodeFromPath,
resolveNotePath, resolveNotePath,
getSomeNotePath, getSomeNotePath,
focusTree,
scrollToActiveNote,
createNewTopLevelNote, createNewTopLevelNote,
duplicateNote, duplicateNote,
getNodeByKey, getRunPath
setTree
}; };

View File

@ -87,7 +87,7 @@ function getTemplates(treeWidget) {
return false; return false;
}, },
"AddNoteAboveToSelection": () => { "AddNoteAboveToSelection": () => {
const node = treeService.getFocusedNode(); const node = treeWidget.getFocusedNode();
if (!node) { if (!node) {
return; return;

View File

@ -1,4 +1,5 @@
import BasicWidget from "./basic_widget.js"; import BasicWidget from "./basic_widget.js";
import appContext from "../services/app_context.js";
const WIDGET_TPL = ` const WIDGET_TPL = `
<style> <style>
@ -34,7 +35,7 @@ class GlobalButtonsWidget extends BasicWidget {
$createTopLevelNoteButton.on('click', () => this.trigger('createTopLevelNote')); $createTopLevelNoteButton.on('click', () => this.trigger('createTopLevelNote'));
$collapseTreeButton.on('click', () => this.trigger('collapseTree')); $collapseTreeButton.on('click', () => this.trigger('collapseTree'));
$scrollToActiveNoteButton.on('click', () => this.trigger('scrollToActiveNote')); $scrollToActiveNoteButton.on('click', () => appContext.getMainNoteTree().scrollToActiveNote());
$toggleSearchButton.on('click', () => this.trigger('toggleSearch')); $toggleSearchButton.on('click', () => this.trigger('toggleSearch'));
} }
} }

View File

@ -12,6 +12,7 @@ import treeCache from "../services/tree_cache.js";
import treeBuilder from "../services/tree_builder.js"; import treeBuilder from "../services/tree_builder.js";
import TreeContextMenu from "../services/tree_context_menu.js"; import TreeContextMenu from "../services/tree_context_menu.js";
import treeChangesService from "../services/branches.js"; import treeChangesService from "../services/branches.js";
import ws from "../services/ws.js";
const TPL = ` const TPL = `
<style> <style>
@ -47,8 +48,8 @@ export default class NoteTreeWidget extends BasicWidget {
$tree.on("click", ".unhoist-button", hoistedNoteService.unhoist); $tree.on("click", ".unhoist-button", hoistedNoteService.unhoist);
$tree.on("click", ".refresh-search-button", searchNotesService.refreshSearch); $tree.on("click", ".refresh-search-button", searchNotesService.refreshSearch);
// this does not belong here ... // FIXME this does not belong here ...
keyboardActionService.setGlobalActionHandler('CollapseTree', () => treeService.collapseTree()); // don't use shortened form since collapseTree() accepts argument keyboardActionService.setGlobalActionHandler('CollapseTree', () => this.collapseTree()); // don't use shortened form since collapseTree() accepts argument
// fancytree doesn't support middle click so this is a way to support it // fancytree doesn't support middle click so this is a way to support it
$widget.on('mousedown', '.fancytree-title', e => { $widget.on('mousedown', '.fancytree-title', e => {
@ -92,7 +93,7 @@ export default class NoteTreeWidget extends BasicWidget {
else { else {
node.setActive(); node.setActive();
treeService.clearSelectedNodes(); this.clearSelectedNodes();
} }
return false; return false;
@ -231,8 +232,6 @@ export default class NoteTreeWidget extends BasicWidget {
}); });
this.tree = $.ui.fancytree.getTree("#tree"); this.tree = $.ui.fancytree.getTree("#tree");
treeService.setTree(this.tree);
} }
/** @return {FancytreeNode[]} */ /** @return {FancytreeNode[]} */
@ -241,11 +240,11 @@ export default class NoteTreeWidget extends BasicWidget {
} }
/** @return {FancytreeNode[]} */ /** @return {FancytreeNode[]} */
getSelectedOrActiveNodes(node) { getSelectedOrActiveNodes(node = null) {
let notes = this.getSelectedNodes(true); let notes = this.getSelectedNodes(true);
if (notes.length === 0) { if (notes.length === 0) {
notes.push(node); notes.push(node ? node : this.getActiveNode());
} }
return notes; return notes;
@ -263,6 +262,13 @@ export default class NoteTreeWidget extends BasicWidget {
node.visit(node => node.setExpanded(false)); node.visit(node => node.setExpanded(false));
} }
/**
* @return {FancytreeNode|null}
*/
getActiveNode() {
return this.tree.getActiveNode();
}
/** /**
* focused & not active node can happen during multiselection where the node is selected but not activated * focused & not active node can happen during multiselection where the node is selected but not activated
* (its content is not displayed in the detail) * (its content is not displayed in the detail)
@ -278,9 +284,125 @@ export default class NoteTreeWidget extends BasicWidget {
} }
} }
// FIXME since this operates on note details tab context it seems it does not really belong here
async scrollToActiveNote() {
const activeContext = noteDetailService.getActiveTabContext();
if (activeContext && activeContext.notePath) {
this.tree.setFocus();
const node = await this.expandToNote(activeContext.notePath);
await node.makeVisible({scrollIntoView: true});
node.setFocus();
}
}
/** @return {FancytreeNode} */
async getNodeFromPath(notePath, expand = false, expandOpts = {}) {
utils.assertArguments(notePath);
const hoistedNoteId = await hoistedNoteService.getHoistedNoteId();
/** @var {FancytreeNode} */
let parentNode = null;
const runPath = await treeService.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 = this.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 this.checkFolderStatus(parentNode);
let foundChildNode = this.findChildNode(parentNode, childNoteId);
if (!foundChildNode) { // note might be recently created so we'll force reload and try again
await parentNode.load(true);
foundChildNode = this.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} */
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 expandToNote(notePath, expandOpts) {
return this.getNodeFromPath(notePath, true, expandOpts);
}
async 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();
}
/** @return {FancytreeNode[]} */
async getNodesByBranchId(branchId) {
utils.assertArguments(branchId);
const branch = treeCache.getBranch(branchId);
return this.getNodesByNoteId(branch.noteId).filter(node => node.data.branchId === branchId);
}
/** @return {FancytreeNode[]} */
getNodesByNoteId(noteId) {
utils.assertArguments(noteId);
const list = this.tree.getNodesByRef(noteId);
return list ? list : []; // if no nodes with this refKey are found, fancy tree returns null
}
async reload(notes) {
await this.tree.reload(notes);
}
createTopLevelNoteListener() { treeService.createNewTopLevelNote(); } createTopLevelNoteListener() { treeService.createNewTopLevelNote(); }
collapseTreeListener() { this.collapseTree(); } collapseTreeListener() { this.collapseTree(); }
scrollToActiveNoteListener() { treeService.scrollToActiveNote(); }
} }

View File

@ -2,6 +2,7 @@ import BasicWidget from "./basic_widget.js";
import treeService from "../services/tree.js"; import treeService from "../services/tree.js";
import treeCache from "../services/tree_cache.js"; import treeCache from "../services/tree_cache.js";
import toastService from "../services/toast.js"; import toastService from "../services/toast.js";
import appContext from "../services/app_context.js";
const helpText = ` const helpText = `
<strong>Search tips</strong> - also see <button class="btn btn-sm" type="button" data-help-page="Search">complete help on search</button> <strong>Search tips</strong> - also see <button class="btn btn-sm" type="button" data-help-page="Search">complete help on search</button>
@ -118,7 +119,7 @@ export default class SearchBoxWidget extends BasicWidget {
return; return;
} }
let activeNode = treeService.getActiveNode(); let activeNode = appContext.getMainNoteTree().getActiveNode();
const parentNote = await treeCache.getNote(activeNode.data.noteId); const parentNote = await treeCache.getNote(activeNode.data.noteId);
if (parentNote.type === 'search') { if (parentNote.type === 'search') {