2020-01-12 04:19:56 +08:00
import hoistedNoteService from "../services/hoisted_note.js" ;
import searchNotesService from "../services/search_notes.js" ;
import treeService from "../services/tree.js" ;
2020-01-12 16:12:13 +08:00
import utils from "../services/utils.js" ;
import contextMenuWidget from "../services/context_menu.js" ;
import treeKeyBindingService from "../services/tree_keybindings.js" ;
import treeCache from "../services/tree_cache.js" ;
import treeBuilder from "../services/tree_builder.js" ;
import TreeContextMenu from "../services/tree_context_menu.js" ;
2020-01-12 17:35:33 +08:00
import treeChangesService from "../services/branches.js" ;
2020-01-12 18:15:23 +08:00
import ws from "../services/ws.js" ;
2020-01-12 19:30:30 +08:00
import appContext from "../services/app_context.js" ;
2020-01-19 01:01:16 +08:00
import TabAwareWidget from "./tab_aware_widget.js" ;
2020-01-24 22:44:24 +08:00
import server from "../services/server.js" ;
2020-02-04 03:07:34 +08:00
import noteCreateService from "../services/note_create.js" ;
2020-01-12 04:19:56 +08:00
const TPL = `
2020-01-13 03:15:05 +08:00
< div class = "tree" >
< style >
. tree {
overflow : auto ;
flex - grow : 1 ;
flex - shrink : 1 ;
flex - basis : 60 % ;
font - family : var ( -- tree - font - family ) ;
font - size : var ( -- tree - font - size ) ;
}
< / s t y l e >
< / d i v >
2020-01-12 04:19:56 +08:00
` ;
2020-01-19 01:01:16 +08:00
export default class NoteTreeWidget extends TabAwareWidget {
2020-01-12 16:12:13 +08:00
constructor ( appContext ) {
super ( appContext ) ;
2020-01-23 03:48:56 +08:00
window . glob . cutIntoNote = ( ) => this . cutIntoNoteListener ( ) ;
2020-01-12 16:12:13 +08:00
this . tree = null ;
}
2020-01-13 03:15:05 +08:00
doRender ( ) {
const $widget = $ ( TPL ) ;
const $tree = $widget ;
2020-01-12 04:19:56 +08:00
$tree . on ( "click" , ".unhoist-button" , hoistedNoteService . unhoist ) ;
$tree . on ( "click" , ".refresh-search-button" , searchNotesService . refreshSearch ) ;
// fancytree doesn't support middle click so this is a way to support it
$widget . on ( 'mousedown' , '.fancytree-title' , e => {
if ( e . which === 2 ) {
const node = $ . ui . fancytree . getNode ( e ) ;
2020-01-25 16:56:08 +08:00
treeService . getNotePath ( node ) . then ( notePath => {
2020-01-12 04:19:56 +08:00
if ( notePath ) {
2020-01-25 00:54:47 +08:00
const tabContext = appContext . openEmptyTab ( ) ;
tabContext . setNote ( notePath ) ;
2020-01-12 04:19:56 +08:00
}
} ) ;
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
}
} ) ;
2020-01-13 03:15:05 +08:00
2020-02-02 05:29:32 +08:00
treeBuilder . prepareTree ( ) . then ( treeData => this . initFancyTree ( $tree , treeData ) ) ;
2020-01-13 03:15:05 +08:00
return $widget ;
2020-01-12 04:19:56 +08:00
}
2020-01-12 16:12:13 +08:00
async initFancyTree ( $tree , treeData ) {
utils . assertArguments ( treeData ) ;
$tree . fancytree ( {
autoScroll : true ,
keyboard : false , // we takover keyboard handling in the hotkeys plugin
extensions : [ "hotkeys" , "dnd5" , "clones" ] ,
source : treeData ,
scrollParent : $tree ,
minExpandLevel : 2 , // root can't be collapsed
click : ( event , data ) => {
const targetType = data . targetType ;
const node = data . node ;
if ( targetType === 'title' || targetType === 'icon' ) {
if ( event . shiftKey ) {
node . setSelected ( ! node . isSelected ( ) ) ;
node . setFocus ( true ) ;
}
else if ( event . ctrlKey ) {
2020-01-25 00:54:47 +08:00
const tabContext = appContext . openEmptyTab ( ) ;
2020-01-25 16:56:08 +08:00
treeService . getNotePath ( node ) . then ( notePath => tabContext . setNote ( notePath ) ) ;
2020-01-25 00:54:47 +08:00
appContext . activateTab ( tabContext . tabId ) ;
2020-01-12 16:12:13 +08:00
}
else {
node . setActive ( ) ;
2020-01-12 18:15:23 +08:00
this . clearSelectedNodes ( ) ;
2020-01-12 16:12:13 +08:00
}
return false ;
}
} ,
activate : async ( event , data ) => {
// click event won't propagate so let's close context menu manually
contextMenuWidget . hideContextMenu ( ) ;
2020-01-25 16:56:08 +08:00
const notePath = await treeService . getNotePath ( data . node ) ;
2020-01-12 16:12:13 +08:00
2020-01-25 04:15:40 +08:00
const activeTabContext = this . appContext . getActiveTabContext ( ) ;
await activeTabContext . setNote ( notePath ) ;
2020-01-12 16:12:13 +08:00
} ,
2020-01-24 22:44:24 +08:00
expand : ( event , data ) => this . setExpandedToServer ( data . node . data . branchId , true ) ,
collapse : ( event , data ) => this . setExpandedToServer ( data . node . data . branchId , false ) ,
2020-01-12 16:12:13 +08:00
hotkeys : {
2020-01-12 17:35:33 +08:00
keydown : await treeKeyBindingService . getKeyboardBindings ( this )
} ,
dnd5 : {
autoExpandMS : 600 ,
dragStart : ( node , data ) => {
// don't allow dragging root node
2020-02-06 05:08:45 +08:00
if ( node . data . noteId === hoistedNoteService . getHoistedNoteId ( )
2020-01-12 17:35:33 +08:00
|| 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 )
2020-02-06 05:08:45 +08:00
&& ( node . data . noteId === hoistedNoteService . getHoistedNoteId ( ) || node . getParent ( ) . data . noteType === 'search' ) ) ) {
2020-01-12 17:35:33 +08:00
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
2020-01-12 19:30:30 +08:00
const importService = await import ( '../services/import.js' ) ;
2020-01-12 17:35:33 +08:00
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" ) {
2020-01-26 18:41:40 +08:00
treeChangesService . moveBeforeBranch ( selectedBranchIds , node . data . branchId ) ;
2020-01-12 17:35:33 +08:00
} else if ( data . hitMode === "after" ) {
2020-01-26 18:41:40 +08:00
treeChangesService . moveAfterBranch ( selectedBranchIds , node . data . branchId ) ;
2020-01-12 17:35:33 +08:00
} else if ( data . hitMode === "over" ) {
2020-01-26 18:41:40 +08:00
treeChangesService . moveToParentNote ( selectedBranchIds , node . data . noteId ) ;
2020-01-12 17:35:33 +08:00
} else {
throw new Error ( "Unknown hitMode=" + data . hitMode ) ;
}
}
}
2020-01-12 16:12:13 +08:00
} ,
lazyLoad : function ( event , data ) {
const noteId = data . node . data . noteId ;
data . result = treeCache . getNote ( noteId ) . then ( note => treeBuilder . prepareBranch ( note ) ) ;
} ,
clones : {
highlightActiveClones : true
} ,
enhanceTitle : async function ( event , data ) {
const node = data . node ;
const $span = $ ( node . span ) ;
if ( node . data . noteId !== 'root'
&& node . data . noteId === await hoistedNoteService . getHoistedNoteId ( )
&& $span . find ( '.unhoist-button' ) . length === 0 ) {
const unhoistButton = $ ( '<span> (<a class="unhoist-button">unhoist</a>)</span>' ) ;
$span . append ( unhoistButton ) ;
}
const note = await treeCache . getNote ( node . data . noteId ) ;
if ( note . type === 'search' && $span . find ( '.refresh-search-button' ) . length === 0 ) {
const refreshSearchButton = $ ( '<span> <span class="refresh-search-button bx bx-recycle" title="Refresh saved search results"></span></span>' ) ;
$span . append ( refreshSearchButton ) ;
}
} ,
// this is done to automatically lazy load all expanded search notes after tree load
loadChildren : ( event , data ) => {
data . node . visit ( ( subNode ) => {
// Load all lazy/unloaded child nodes
// (which will trigger `loadChildren` recursively)
if ( subNode . isUndefined ( ) && subNode . isExpanded ( ) ) {
subNode . load ( ) ;
}
} ) ;
}
} ) ;
$tree . on ( 'contextmenu' , '.fancytree-node' , e => {
const node = $ . ui . fancytree . getNode ( e ) ;
2020-01-12 16:57:28 +08:00
contextMenuWidget . initContextMenu ( e , new TreeContextMenu ( this , node ) ) ;
2020-01-12 16:12:13 +08:00
return false ; // blocks default browser right click menu
} ) ;
2020-01-12 19:30:30 +08:00
this . tree = $ . ui . fancytree . getTree ( $tree ) ;
2020-01-12 16:12:13 +08:00
}
/** @return {FancytreeNode[]} */
getSelectedNodes ( stopOnParents = false ) {
return this . tree . getSelectedNodes ( stopOnParents ) ;
2020-01-12 04:19:56 +08:00
}
2020-01-12 16:12:13 +08:00
/** @return {FancytreeNode[]} */
2020-01-12 18:15:23 +08:00
getSelectedOrActiveNodes ( node = null ) {
2020-01-12 16:12:13 +08:00
let notes = this . getSelectedNodes ( true ) ;
if ( notes . length === 0 ) {
2020-01-12 18:15:23 +08:00
notes . push ( node ? node : this . getActiveNode ( ) ) ;
2020-01-12 16:12:13 +08:00
}
return notes ;
2020-01-12 04:19:56 +08:00
}
2020-01-12 16:12:13 +08:00
async collapseTree ( node = null ) {
if ( ! node ) {
const hoistedNoteId = await hoistedNoteService . getHoistedNoteId ( ) ;
2020-01-22 05:08:41 +08:00
node = this . getNodesByNoteId ( hoistedNoteId ) [ 0 ] ;
2020-01-12 16:12:13 +08:00
}
node . setExpanded ( false ) ;
node . visit ( node => node . setExpanded ( false ) ) ;
2020-01-12 04:19:56 +08:00
}
2020-01-12 16:12:13 +08:00
2020-01-12 18:15:23 +08:00
/ * *
* @ return { FancytreeNode | null }
* /
getActiveNode ( ) {
return this . tree . getActiveNode ( ) ;
}
2020-01-12 17:35:33 +08:00
/ * *
* 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 ) ;
}
}
2020-01-22 05:08:41 +08:00
async scrollToActiveNoteListener ( ) {
2020-01-12 19:30:30 +08:00
const activeContext = appContext . getActiveTabContext ( ) ;
2020-01-12 18:15:23 +08:00
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 ) ;
}
2020-01-30 04:38:58 +08:00
await this . updateNode ( parentNode ) ;
2020-01-12 18:15:23 +08:00
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 ) ;
}
2020-01-30 04:38:58 +08:00
async updateNode ( node ) {
2020-01-12 18:15:23 +08:00
const note = await treeCache . getNote ( node . data . noteId ) ;
2020-01-30 04:38:58 +08:00
const branch = treeCache . getBranch ( node . data . branchId ) ;
2020-01-12 18:15:23 +08:00
2020-01-30 04:38:58 +08:00
node . data . isProtected = note . isProtected ;
node . data . noteType = note . type ;
2020-01-12 18:15:23 +08:00
node . folder = note . type === 'search' || note . getChildNoteIds ( ) . length > 0 ;
node . icon = await treeBuilder . getIcon ( note ) ;
node . extraClasses = await treeBuilder . getExtraClasses ( note ) ;
2020-01-30 04:38:58 +08:00
node . title = ( branch . prefix ? ( branch . prefix + " - " ) : "" ) + note . title ;
2020-01-12 18:15:23 +08:00
node . renderTitle ( ) ;
}
/** @return {FancytreeNode[]} */
2020-02-03 05:32:44 +08:00
getNodesByBranchId ( branchId ) {
2020-01-12 18:15:23 +08:00
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 ) ;
}
2020-01-12 16:12:13 +08:00
createTopLevelNoteListener ( ) { treeService . createNewTopLevelNote ( ) ; }
collapseTreeListener ( ) { this . collapseTree ( ) ; }
2020-01-12 19:30:30 +08:00
2020-01-19 03:49:49 +08:00
async refresh ( ) {
2020-01-19 01:01:16 +08:00
const oldActiveNode = this . getActiveNode ( ) ;
if ( oldActiveNode ) {
oldActiveNode . setActive ( false ) ;
2020-01-20 04:12:53 +08:00
oldActiveNode . setFocus ( false ) ;
2020-01-19 01:01:16 +08:00
}
if ( this . tabContext && this . tabContext . notePath ) {
const newActiveNode = await this . getNodeFromPath ( this . tabContext . notePath ) ;
if ( newActiveNode ) {
if ( ! newActiveNode . isVisible ( ) ) {
await this . expandToNote ( this . tabContext . notePath ) ;
}
newActiveNode . setActive ( true , { noEvents : true } ) ;
}
}
}
2020-01-30 03:14:02 +08:00
async entitiesReloadedListener ( { loadResults } ) {
2020-01-30 04:38:58 +08:00
const noteIdsToUpdate = new Set ( ) ;
const noteIdsToReload = new Set ( ) ;
2020-01-26 18:41:40 +08:00
2020-01-30 04:38:58 +08:00
for ( const attr of loadResults . getAttributes ( ) ) {
if ( attr . type === 'label' && [ 'iconClass' , 'cssClass' ] . includes ( attr . name ) ) {
if ( attr . isInheritable ) {
noteIdsToReload . add ( attr . noteId ) ;
}
else {
noteIdsToUpdate . add ( attr . noteId ) ;
}
}
else if ( attr . type === 'relation' && attr . name === 'template' ) {
// missing handling of things inherited from template
noteIdsToReload . add ( attr . noteId ) ;
}
}
for ( const branch of loadResults . getBranches ( ) ) {
for ( const node of this . getNodesByBranchId ( branch . branchId ) ) {
if ( branch . isDeleted ) {
if ( node . isActive ( ) ) {
let newActive = node . getNextSibling ( ) ;
if ( ! newActive ) {
newActive = node . getPrevSibling ( ) ;
}
if ( ! newActive ) {
newActive = node . getParent ( ) ;
}
2020-02-03 05:32:44 +08:00
const notePath = await treeService . getNotePath ( newActive ) ;
appContext . getActiveTabContext ( ) . setNote ( notePath ) ;
2020-01-30 04:38:58 +08:00
}
2020-01-12 19:30:30 +08:00
node . remove ( ) ;
}
else {
2020-01-30 04:38:58 +08:00
noteIdsToUpdate . add ( branch . noteId ) ;
}
}
if ( ! branch . isDeleted ) {
for ( const parentNode of this . getNodesByNoteId ( branch . parentNoteId ) ) {
if ( ! parentNode . isLoaded ( ) ) {
continue ;
}
const found = parentNode . getChildren ( ) . find ( child => child . data . noteId === branch . noteId ) ;
2020-01-12 19:30:30 +08:00
2020-01-30 04:38:58 +08:00
if ( ! found ) {
noteIdsToReload . add ( branch . parentNoteId ) ;
}
}
}
}
2020-02-03 05:32:44 +08:00
for ( const noteId of loadResults . getNoteIds ( ) ) {
2020-02-03 05:33:50 +08:00
noteIdsToUpdate . add ( noteId ) ;
2020-02-03 05:32:44 +08:00
}
2020-01-30 04:38:58 +08:00
for ( const noteId of noteIdsToReload ) {
for ( const node of this . getNodesByNoteId ( noteId ) ) {
await node . load ( true ) ;
await this . updateNode ( node ) ;
}
}
2020-02-03 05:32:44 +08:00
for ( const noteId of noteIdsToUpdate ) {
2020-01-30 04:38:58 +08:00
for ( const node of this . getNodesByNoteId ( noteId ) ) {
await this . updateNode ( node ) ;
}
}
for ( const { parentNoteId } of loadResults . getNoteReorderings ( ) ) {
for ( const node of this . getNodesByNoteId ( parentNoteId ) ) {
if ( node . isLoaded ( ) ) {
node . sortChildren ( ( nodeA , nodeB ) => {
const branchA = treeCache . branches [ nodeA . data . branchId ] ;
const branchB = treeCache . branches [ nodeB . data . branchId ] ;
if ( ! branchA || ! branchB ) {
return 0 ;
}
return branchA . notePosition - branchB . notePosition ;
} ) ;
2020-01-12 19:30:30 +08:00
}
}
}
2020-02-03 05:32:44 +08:00
const activateNotePath = appContext . getActiveTabNotePath ( ) ;
2020-01-12 19:30:30 +08:00
if ( activateNotePath ) {
const node = await this . getNodeFromPath ( activateNotePath ) ;
if ( node && ! node . isActive ( ) ) {
await node . setActive ( true ) ;
}
}
}
2020-01-20 01:05:06 +08:00
2020-01-23 03:48:56 +08:00
async createNoteAfterListener ( ) {
const node = this . getActiveNode ( ) ;
const parentNoteId = node . data . parentNoteId ;
2020-01-25 16:56:08 +08:00
const isProtected = await treeService . getParentProtectedStatus ( node ) ;
2020-01-23 03:48:56 +08:00
if ( node . data . noteId === 'root' || node . data . noteId === await hoistedNoteService . getHoistedNoteId ( ) ) {
return ;
}
2020-02-04 03:07:34 +08:00
await noteCreateService . createNote ( parentNoteId , {
target : 'after' ,
targetBranchId : node . data . branchId ,
2020-01-23 03:48:56 +08:00
isProtected : isProtected ,
saveSelection : true
} ) ;
}
async createNoteIntoListener ( ) {
const node = this . getActiveNode ( ) ;
if ( node ) {
2020-02-04 03:07:34 +08:00
await noteCreateService . createNote ( node . data . noteId , {
2020-01-23 03:48:56 +08:00
isProtected : node . data . isProtected ,
saveSelection : false
} ) ;
}
}
async cutIntoNoteListener ( ) {
const node = this . getActiveNode ( ) ;
if ( node ) {
2020-02-04 03:07:34 +08:00
await noteCreateService . createNote ( node . data . noteId , {
2020-01-23 03:48:56 +08:00
isProtected : node . data . isProtected ,
saveSelection : true
} ) ;
}
}
2020-01-24 22:44:24 +08:00
async setExpandedToServer ( branchId , isExpanded ) {
utils . assertArguments ( branchId ) ;
const expandedNum = isExpanded ? 1 : 0 ;
await server . put ( 'branches/' + branchId + '/expanded/' + expandedNum ) ;
}
2020-02-02 05:29:32 +08:00
async reloadTreeFromCache ( ) {
const notes = await treeBuilder . prepareTree ( ) ;
2020-01-24 22:44:24 +08:00
const activeNode = this . getActiveNode ( ) ;
2020-01-25 16:56:08 +08:00
const activeNotePath = activeNode !== null ? await treeService . getNotePath ( activeNode ) : null ;
2020-01-24 22:44:24 +08:00
await this . reload ( notes ) ;
if ( activeNotePath ) {
const node = await this . getNodeFromPath ( activeNotePath , true ) ;
await node . setActive ( true , { noEvents : true } ) ;
}
}
hoistedNoteChangedListener ( ) {
2020-02-02 05:29:32 +08:00
this . reloadTreeFromCache ( ) ;
2020-01-24 22:44:24 +08:00
}
2020-02-02 05:29:32 +08:00
treeCacheReloadedListener ( ) {
this . reloadTreeFromCache ( ) ;
2020-01-24 22:44:24 +08:00
}
2020-01-12 04:19:56 +08:00
}