widgetizing tree WIP

This commit is contained in:
zadam 2020-01-12 10:35:33 +01:00
parent d1f679ab90
commit b12e38c231
8 changed files with 284 additions and 242 deletions

View File

@ -1,6 +1,5 @@
import cloning from './services/cloning.js'; import cloning from './services/cloning.js';
import contextMenu from './services/tree_context_menu.js'; import contextMenu from './services/tree_context_menu.js';
import dragAndDropSetup from './services/drag_and_drop.js';
import link from './services/link.js'; import link from './services/link.js';
import ws from './services/ws.js'; import ws from './services/ws.js';
import noteDetailService from './services/note_detail.js'; import noteDetailService from './services/note_detail.js';

View File

@ -1,6 +1,5 @@
import treeService from "./services/tree.js"; import treeService from "./services/tree.js";
import noteDetailService from "./services/note_detail.js"; import noteDetailService from "./services/note_detail.js";
import dragAndDropSetup from "./services/drag_and_drop.js";
import treeCache from "./services/tree_cache.js"; import treeCache from "./services/tree_cache.js";
import treeBuilder from "./services/tree_builder.js"; import treeBuilder from "./services/tree_builder.js";
import contextMenuWidget from "./services/context_menu.js"; import contextMenuWidget from "./services/context_menu.js";

View File

@ -68,6 +68,7 @@ async function moveToNode(branchIdsToMove, newParentNoteId) {
} }
} }
// FIXME used for finding a next note to activate after a delete
async function getNextNode(nodes) { async function getNextNode(nodes) {
// following code assumes that nodes contain only top-most selected nodes - getSelectedNodes has been // following code assumes that nodes contain only top-most selected nodes - getSelectedNodes has been
// called with stopOnParent=true // called with stopOnParent=true
@ -84,10 +85,10 @@ async function getNextNode(nodes) {
return treeUtils.getNotePath(next); return treeUtils.getNotePath(next);
} }
async function deleteNodes(nodes) { async function deleteNodes(branchIdsToDelete) {
nodes = await filterRootNote(nodes); branchIdsToDelete = await filterRootNote(branchIdsToDelete);
if (nodes.length === 0) { if (branchIdsToDelete.length === 0) {
return false; return false;
} }
@ -96,7 +97,15 @@ async function deleteNodes(nodes) {
.append($('<label for="delete-clones-checkbox">') .append($('<label for="delete-clones-checkbox">')
.text("delete also all note clones") .text("delete also all note clones")
.attr("title", "all clones of selected notes will be deleted and as such the whole note will be deleted.")); .attr("title", "all clones of selected notes will be deleted and as such the whole note will be deleted."));
const $nodeTitles = $("<ul>").append(...nodes.map(node => $("<li>").text(node.title)));
const $nodeTitles = $("<ul>");
for (const branchId of branchIdsToDelete) {
const note = await treeCache.getBranch(branchId).getNote();
$nodeTitles.append($("<li>").text(note.title));
}
const $confirmText = $("<div>") const $confirmText = $("<div>")
.append($("<p>").text('This will delete the following notes and their sub-notes: ')) .append($("<p>").text('This will delete the following notes and their sub-notes: '))
.append($nodeTitles) .append($nodeTitles)
@ -114,31 +123,31 @@ async function deleteNodes(nodes) {
let counter = 0; let counter = 0;
for (const node of nodes) { for (const branchIdToDelete of branchIdsToDelete) {
counter++; counter++;
const last = counter === nodes.length; const last = counter === branchIdsToDelete.length;
const query = `?taskId=${taskId}&last=${last ? 'true' : 'false'}`; const query = `?taskId=${taskId}&last=${last ? 'true' : 'false'}`;
if (deleteClones) { const branch = treeCache.getBranch(branchIdToDelete);
await server.remove(`notes/${node.data.noteId}` + query);
noteDetailService.noteDeleted(node.data.noteId); if (deleteClones) {
await server.remove(`notes/${branch.noteId}` + query);
noteDetailService.noteDeleted(branch.noteId);
} }
else { else {
const {noteDeleted} = await server.remove(`branches/${node.data.branchId}` + query); const {noteDeleted} = await server.remove(`branches/${branchIdToDelete}` + query);
if (noteDeleted) { if (noteDeleted) {
noteDetailService.noteDeleted(node.data.noteId); noteDetailService.noteDeleted(branch.noteId);
} }
} }
} }
const nextNotePath = await getNextNode(nodes);
const noteIds = Array.from(new Set(nodes.map(node => node.getParent().data.noteId))); const noteIds = Array.from(new Set(nodes.map(node => node.getParent().data.noteId)));
await treeService.reloadNotes(noteIds, nextNotePath); await treeService.reloadNotes(noteIds);
return true; return true;
} }

View File

@ -1,75 +0,0 @@
import treeService from './tree.js';
import treeChangesService from './branches.js';
import hoistedNoteService from './hoisted_note.js';
const dragAndDropSetup = {
autoExpandMS: 600,
dragStart: (node, data) => {
// don't allow dragging root node
if (node.data.noteId === hoistedNoteService.getHoistedNoteNoPromise()
|| node.getParent().data.noteType === 'search') {
return false;
}
node.setSelected(true);
const notes = treeService.getSelectedNodes().map(node => { return {
noteId: node.data.noteId,
title: node.title
}});
data.dataTransfer.setData("text", JSON.stringify(notes));
// This function MUST be defined to enable dragging for the tree.
// Return false to cancel dragging of node.
return true;
},
dragEnter: (node, data) => true, // allow drop on any node
dragOver: (node, data) => true,
dragDrop: async (node, data) => {
if ((data.hitMode === 'over' && node.data.noteType === 'search') ||
(['after', 'before'].includes(data.hitMode)
&& (node.data.noteId === hoistedNoteService.getHoistedNoteNoPromise() || node.getParent().data.noteType === 'search'))) {
const infoDialog = await import('../dialogs/info.js');
await infoDialog.info("Dropping notes into this location is not allowed.");
return;
}
const dataTransfer = data.dataTransfer;
if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) {
const files = [...dataTransfer.files]; // chrome has issue that dataTransfer.files empties after async operation
const importService = await import('./import.js');
importService.uploadFiles(node.data.noteId, files, {
safeImport: true,
shrinkImages: true,
textImportedAsText: true,
codeImportedAsCode: true,
explodeArchives: true
});
}
else {
// This function MUST be defined to enable dropping of items on the tree.
// data.hitMode is 'before', 'after', or 'over'.
const selectedNodes = treeService.getSelectedNodes();
if (data.hitMode === "before") {
treeChangesService.moveBeforeNode(selectedNodes, node);
} else if (data.hitMode === "after") {
treeChangesService.moveAfterNode(selectedNodes, node);
} else if (data.hitMode === "over") {
treeChangesService.moveToNode(selectedNodes, node);
} else {
throw new Error("Unknown hitMode=" + data.hitMode);
}
}
}
};
export default dragAndDropSetup;

View File

@ -1,5 +1,3 @@
import contextMenuWidget from './context_menu.js';
import dragAndDropSetup from './drag_and_drop.js';
import ws from './ws.js'; import ws from './ws.js';
import noteDetailService from './note_detail.js'; import noteDetailService from './note_detail.js';
import protectedSessionHolder from './protected_session_holder.js'; import protectedSessionHolder from './protected_session_holder.js';
@ -9,10 +7,8 @@ import server from './server.js';
import treeCache from './tree_cache.js'; import treeCache from './tree_cache.js';
import toastService from "./toast.js"; import toastService from "./toast.js";
import treeBuilder from "./tree_builder.js"; import treeBuilder from "./tree_builder.js";
import treeKeyBindingService from "./tree_keybindings.js";
import hoistedNoteService from '../services/hoisted_note.js'; import hoistedNoteService from '../services/hoisted_note.js';
import optionsService from "../services/options.js"; import optionsService from "../services/options.js";
import TreeContextMenu from "./tree_context_menu.js";
import bundle from "./bundle.js"; import bundle from "./bundle.js";
import keyboardActionService from "./keyboard_actions.js"; import keyboardActionService from "./keyboard_actions.js";

View File

@ -133,7 +133,7 @@ class TreeContextMenu {
protectedSessionService.protectSubtree(this.node.data.noteId, false); protectedSessionService.protectSubtree(this.node.data.noteId, false);
} }
else if (cmd === "copy") { else if (cmd === "copy") {
clipboard.copy(this.treeWidget.getSelectedOrActiveNodes(this.node)); clipboard.copy(this.getSelectedOrActiveBranchIds());
} }
else if (cmd === "cloneTo") { else if (cmd === "cloneTo") {
const nodes = this.treeWidget.getSelectedOrActiveNodes(this.node); const nodes = this.treeWidget.getSelectedOrActiveNodes(this.node);
@ -142,7 +142,7 @@ class TreeContextMenu {
import("../dialogs/clone_to.js").then(d => d.showDialog(noteIds)); import("../dialogs/clone_to.js").then(d => d.showDialog(noteIds));
} }
else if (cmd === "cut") { else if (cmd === "cut") {
clipboard.cut(this.treeWidget.getSelectedOrActiveNodes(this.node)); clipboard.cut(this.getSelectedOrActiveBranchIds());
} }
else if (cmd === "moveTo") { else if (cmd === "moveTo") {
const nodes = this.treeWidget.getSelectedOrActiveNodes(this.node); const nodes = this.treeWidget.getSelectedOrActiveNodes(this.node);
@ -150,13 +150,13 @@ class TreeContextMenu {
import("../dialogs/move_to.js").then(d => d.showDialog(nodes)); import("../dialogs/move_to.js").then(d => d.showDialog(nodes));
} }
else if (cmd === "pasteAfter") { else if (cmd === "pasteAfter") {
clipboard.pasteAfter(this.treeWidget, this.node); clipboard.pasteAfter(this.node.data.branchId);
} }
else if (cmd === "pasteInto") { else if (cmd === "pasteInto") {
clipboard.pasteInto(this.node); clipboard.pasteInto(this.node.data.noteId);
} }
else if (cmd === "delete") { else if (cmd === "delete") {
treeChangesService.deleteNodes(this.treeWidget.getSelectedOrActiveNodes(this.node)); treeChangesService.deleteNodes(this.getSelectedOrActiveBranchIds());
} }
else if (cmd === "export") { else if (cmd === "export") {
const exportDialog = await import('../dialogs/export.js'); const exportDialog = await import('../dialogs/export.js');
@ -193,6 +193,12 @@ class TreeContextMenu {
ws.logError("Unknown command: " + cmd); ws.logError("Unknown command: " + cmd);
} }
} }
getSelectedOrActiveBranchIds() {
const nodes = this.treeWidget.getSelectedOrActiveNodes(this.node);
return nodes.map(node => node.data.branchId);
}
} }
export default TreeContextMenu; export default TreeContextMenu;

View File

@ -6,35 +6,54 @@ import clipboard from "./clipboard.js";
import utils from "./utils.js"; import utils from "./utils.js";
import keyboardActionService from "./keyboard_actions.js"; import keyboardActionService from "./keyboard_actions.js";
const fixedKeyBindings = { /**
* @param {NoteTreeWidget} treeWidget
*/
function getFixedKeyBindings(treeWidget) {
return {
// code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin // 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 // after opening context menu, standard shortcuts don't work, but they are detected here
// so we essentially takeover the standard handling with our implementation. // so we essentially takeover the standard handling with our implementation.
"left": node => { "left": node => {
node.navigate($.ui.keyCode.LEFT, true).then(treeService.clearSelectedNodes); node.navigate($.ui.keyCode.LEFT, true).then(treeWidget.clearSelectedNodes);
return false; return false;
}, },
"right": node => { "right": node => {
node.navigate($.ui.keyCode.RIGHT, true).then(treeService.clearSelectedNodes); node.navigate($.ui.keyCode.RIGHT, true).then(treeWidget.clearSelectedNodes);
return false; return false;
}, },
"up": node => { "up": node => {
node.navigate($.ui.keyCode.UP, true).then(treeService.clearSelectedNodes); node.navigate($.ui.keyCode.UP, true).then(treeWidget.clearSelectedNodes);
return false; return false;
}, },
"down": node => { "down": node => {
node.navigate($.ui.keyCode.DOWN, true).then(treeService.clearSelectedNodes); node.navigate($.ui.keyCode.DOWN, true).then(treeWidget.clearSelectedNodes);
return false; return false;
} }
}; };
}
const templates = { /**
* @param {NoteTreeWidget} treeWidget
* @param {FancytreeNode} node
*/
function getSelectedOrActiveBranchIds(treeWidget, node) {
const nodes = treeWidget.getSelectedOrActiveNodes(node);
return nodes.map(node => node.data.branchId);
}
/**
* @param {NoteTreeWidget} treeWidget
*/
function getTemplates(treeWidget) {
return {
"DeleteNotes": node => { "DeleteNotes": node => {
treeChangesService.deleteNodes(treeService.getSelectedOrActiveNodes(node)); treeChangesService.deleteNodes(getSelectedOrActiveBranchIds(treeWidget, node));
}, },
"MoveNoteUp": node => { "MoveNoteUp": node => {
const beforeNode = node.getPrevSibling(); const beforeNode = node.getPrevSibling();
@ -93,7 +112,7 @@ const templates = {
return false; return false;
}, },
"AddNoteBelowToSelection": () => { "AddNoteBelowToSelection": () => {
const node = treeService.getFocusedNode(); const node = treeWidget.getFocusedNode();
if (!node) { if (!node) {
return; return;
@ -118,7 +137,7 @@ const templates = {
return false; return false;
}, },
"CollapseSubtree": node => { "CollapseSubtree": node => {
treeService.collapseTree(node); treeWidget.collapseTree(node);
}, },
"SortChildNotes": node => { "SortChildNotes": node => {
treeService.sortAlphabetically(node.data.noteId); treeService.sortAlphabetically(node.data.noteId);
@ -133,17 +152,17 @@ const templates = {
return false; return false;
}, },
"CopyNotesToClipboard": node => { "CopyNotesToClipboard": node => {
clipboard.copy(treeService.getSelectedOrActiveNodes(node)); clipboard.copy(getSelectedOrActiveBranchIds(treeWidget, node));
return false; return false;
}, },
"CutNotesToClipboard": node => { "CutNotesToClipboard": node => {
clipboard.cut(treeService.getSelectedOrActiveNodes(node)); clipboard.cut(getSelectedOrActiveBranchIds(treeWidget, node));
return false; return false;
}, },
"PasteNotesFromClipboard": node => { "PasteNotesFromClipboard": node => {
clipboard.pasteInto(node); clipboard.pasteInto(node.data.noteId);
return false; return false;
}, },
@ -154,13 +173,19 @@ const templates = {
}, },
"ActivateParentNote": async node => { "ActivateParentNote": async node => {
if (!await hoistedNoteService.isRootNode(node)) { if (!await hoistedNoteService.isRootNode(node)) {
node.getParent().setActive().then(treeService.clearSelectedNodes); node.getParent().setActive().then(treeWidget.clearSelectedNodes);
} }
} }
}; };
}
async function getKeyboardBindings() { /**
const bindings = Object.assign({}, fixedKeyBindings); * @param {NoteTreeWidget} treeWidget
*/
async function getKeyboardBindings(treeWidget) {
const bindings = Object.assign({}, getFixedKeyBindings(treeWidget));
const templates = getTemplates(treeWidget);
for (const actionName in templates) { for (const actionName in templates) {
const action = await keyboardActionService.getAction(actionName); const action = await keyboardActionService.getAction(actionName);

View File

@ -8,10 +8,10 @@ import noteDetailService from "../services/note_detail.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import contextMenuWidget from "../services/context_menu.js"; import contextMenuWidget from "../services/context_menu.js";
import treeKeyBindingService from "../services/tree_keybindings.js"; import treeKeyBindingService from "../services/tree_keybindings.js";
import dragAndDropSetup from "../services/drag_and_drop.js";
import treeCache from "../services/tree_cache.js"; 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";
const TPL = ` const TPL = `
<style> <style>
@ -110,9 +110,77 @@ export default class NoteTreeWidget extends BasicWidget {
collapse: (event, data) => treeService.setExpandedToServer(data.node.data.branchId, false), collapse: (event, data) => treeService.setExpandedToServer(data.node.data.branchId, false),
init: (event, data) => treeService.treeInitialized(), init: (event, data) => treeService.treeInitialized(),
hotkeys: { hotkeys: {
keydown: await treeKeyBindingService.getKeyboardBindings() keydown: await treeKeyBindingService.getKeyboardBindings(this)
},
dnd5: {
autoExpandMS: 600,
dragStart: (node, data) => {
// don't allow dragging root node
if (node.data.noteId === hoistedNoteService.getHoistedNoteNoPromise()
|| node.getParent().data.noteType === 'search') {
return false;
}
node.setSelected(true);
const notes = this.getSelectedNodes().map(node => { return {
noteId: node.data.noteId,
title: node.title
}});
data.dataTransfer.setData("text", JSON.stringify(notes));
// This function MUST be defined to enable dragging for the tree.
// Return false to cancel dragging of node.
return true;
},
dragEnter: (node, data) => true, // allow drop on any node
dragOver: (node, data) => true,
dragDrop: async (node, data) => {
if ((data.hitMode === 'over' && node.data.noteType === 'search') ||
(['after', 'before'].includes(data.hitMode)
&& (node.data.noteId === hoistedNoteService.getHoistedNoteNoPromise() || node.getParent().data.noteType === 'search'))) {
const infoDialog = await import('../dialogs/info.js');
await infoDialog.info("Dropping notes into this location is not allowed.");
return;
}
const dataTransfer = data.dataTransfer;
if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) {
const files = [...dataTransfer.files]; // chrome has issue that dataTransfer.files empties after async operation
const importService = await import('./import.js');
importService.uploadFiles(node.data.noteId, files, {
safeImport: true,
shrinkImages: true,
textImportedAsText: true,
codeImportedAsCode: true,
explodeArchives: true
});
}
else {
// This function MUST be defined to enable dropping of items on the tree.
// data.hitMode is 'before', 'after', or 'over'.
const selectedBranchIds = this.getSelectedNodes().map(node => node.data.branchId);
if (data.hitMode === "before") {
treeChangesService.moveBeforeNode(selectedBranchIds, node.data.branchId);
} else if (data.hitMode === "after") {
treeChangesService.moveAfterNode(selectedBranchIds, node.data.branchId);
} else if (data.hitMode === "over") {
treeChangesService.moveToNode(selectedBranchIds, node.data.noteId);
} else {
throw new Error("Unknown hitMode=" + data.hitMode);
}
}
}
}, },
dnd5: dragAndDropSetup,
lazyLoad: function(event, data) { lazyLoad: function(event, data) {
const noteId = data.node.data.noteId; const noteId = data.node.data.noteId;
@ -195,6 +263,21 @@ export default class NoteTreeWidget extends BasicWidget {
node.visit(node => node.setExpanded(false)); node.visit(node => node.setExpanded(false));
} }
/**
* 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}
*/
getFocusedNode() {
return this.tree.getFocusNode();
}
clearSelectedNodes() {
for (const selectedNode of this.getSelectedNodes()) {
selectedNode.setSelected(false);
}
}
createTopLevelNoteListener() { treeService.createNewTopLevelNote(); } createTopLevelNoteListener() { treeService.createNewTopLevelNote(); }
collapseTreeListener() { this.collapseTree(); } collapseTreeListener() { this.collapseTree(); }