2020-01-11 21:19:56 +01:00
import hoistedNoteService from "../services/hoisted_note.js" ;
import treeService from "../services/tree.js" ;
2020-01-12 09:12:13 +01:00
import utils from "../services/utils.js" ;
2020-02-29 11:28:30 +01:00
import contextMenu from "../services/context_menu.js" ;
2020-01-12 09:12:13 +01:00
import treeCache from "../services/tree_cache.js" ;
2020-02-17 19:42:52 +01:00
import branchService from "../services/branches.js" ;
2020-01-12 11:15:23 +01:00
import ws from "../services/ws.js" ;
2020-01-18 18:01:16 +01:00
import TabAwareWidget from "./tab_aware_widget.js" ;
2020-01-24 15:44:24 +01:00
import server from "../services/server.js" ;
2020-02-03 20:07:34 +01:00
import noteCreateService from "../services/note_create.js" ;
2020-02-14 20:18:09 +01:00
import toastService from "../services/toast.js" ;
2020-02-16 18:11:32 +01:00
import appContext from "../services/app_context.js" ;
2020-02-16 22:14:28 +01:00
import keyboardActionsService from "../services/keyboard_actions.js" ;
import clipboard from "../services/clipboard.js" ;
2020-02-16 22:56:40 +01:00
import protectedSessionService from "../services/protected_session.js" ;
import syncService from "../services/sync.js" ;
2020-05-02 00:28:40 +02:00
import options from "../services/options.js" ;
2020-09-23 22:45:51 +02:00
import protectedSessionHolder from "../services/protected_session_holder.js" ;
2020-01-11 21:19:56 +01:00
const TPL = `
2020-05-02 00:28:40 +02:00
< div class = "tree-wrapper" >
2020-01-12 20:15:05 +01:00
< style >
2020-05-02 00:28:40 +02:00
. tree - wrapper {
2020-01-12 20:15:05 +01:00
flex - grow : 1 ;
flex - shrink : 1 ;
flex - basis : 60 % ;
font - family : var ( -- tree - font - family ) ;
font - size : var ( -- tree - font - size ) ;
2020-05-02 00:28:40 +02:00
position : relative ;
min - height : 0 ;
}
. tree {
height : 100 % ;
overflow : auto ;
2020-09-08 23:25:21 +02:00
padding - bottom : 35 px ;
2021-02-13 23:20:22 +01:00
padding - top : 5 px ;
2020-01-12 20:15:05 +01:00
}
2020-11-29 22:32:31 +01:00
2020-11-27 20:40:32 +01:00
. collapse - tree - button {
position : absolute ;
2020-11-27 21:57:41 +01:00
bottom : 10 px ;
2020-11-27 22:02:55 +01:00
right : 80 px ;
2020-11-27 20:40:32 +01:00
z - index : 100 ;
}
. scroll - to - active - note - button {
position : absolute ;
2020-11-27 21:57:41 +01:00
bottom : 10 px ;
2020-11-27 22:02:55 +01:00
right : 45 px ;
2020-11-27 20:40:32 +01:00
z - index : 100 ;
}
2020-05-02 00:28:40 +02:00
. tree - settings - button {
position : absolute ;
2020-11-27 21:57:41 +01:00
bottom : 10 px ;
2020-11-27 22:02:55 +01:00
right : 10 px ;
2020-05-04 10:03:54 +02:00
z - index : 100 ;
2020-05-02 00:28:40 +02:00
}
. tree - settings - popup {
display : none ;
position : absolute ;
background - color : var ( -- accented - background - color ) ;
border : 1 px solid var ( -- main - border - color ) ;
padding : 20 px ;
z - index : 1000 ;
2020-05-02 13:52:02 +02:00
width : 320 px ;
2020-11-27 22:02:55 +01:00
border - radius : 10 px ;
2020-05-02 00:28:40 +02:00
}
2020-06-03 11:06:45 +02:00
ul . fancytree - container {
outline : none ! important ;
background - color : inherit ! important ;
}
. fancytree - custom - icon {
font - size : 1.3 em ;
}
span . fancytree - title {
color : inherit ! important ;
background : inherit ! important ;
outline : none ! important ;
}
span . fancytree - node . protected > span . fancytree - custom - icon {
filter : drop - shadow ( 2 px 2 px 2 px var ( -- main - text - color ) ) ;
}
span . fancytree - node . multiple - parents . fancytree - title : : after {
content : " *"
}
span . fancytree - node . fancytree - active - clone : not ( . fancytree - active ) . fancytree - title {
font - weight : bold ;
}
/* first nesting level has lower left padding to avoid extra left padding. Other levels are not affected */
. ui - fancytree > li > ul {
padding - left : 5 px ;
}
span . fancytree - active . fancytree - title {
font - weight : bold ;
border - color : var ( -- main - border - color ) ! important ;
border - radius : 5 px ;
}
2020-09-12 23:06:03 +02:00
span . fancytree - active . fancytree - title , span . fancytree - active . fancytree - selected . fancytree - title {
2020-06-03 11:06:45 +02:00
color : var ( -- active - item - text - color ) ! important ;
background - color : var ( -- active - item - background - color ) ! important ;
border - color : var ( -- main - background - color ) ! important ; /* invisible border */
border - radius : 5 px ;
}
span . fancytree - selected . fancytree - title {
color : var ( -- hover - item - text - color ) ! important ;
background - color : var ( -- hover - item - background - color ) ! important ;
border - color : var ( -- main - background - color ) ! important ; /* invisible border */
border - radius : 5 px ;
font - style : italic ;
}
span . fancytree - node : hover span . fancytree - title {
border - color : var ( -- main - border - color ) ! important ;
border - radius : 5 px ;
}
span . fancytree - node . archived {
opacity : 0.6 ;
}
2020-11-29 22:32:31 +01:00
. tree - item - button {
2021-01-30 21:03:39 +01:00
font - size : 120 % ;
2020-11-27 23:13:48 +01:00
cursor : pointer ;
2020-11-29 22:32:31 +01:00
border - radius : 3 px ;
border : 1 px solid var ( -- main - background - color ) ;
margin - left : 5 px ;
2020-12-03 21:50:41 +01:00
margin - top : 2 px ;
2020-11-27 23:13:48 +01:00
}
2020-11-29 22:32:31 +01:00
. tree - item - button : hover {
border - color : var ( -- main - border - color ) ;
}
. add - note - button {
display : none ;
2020-11-27 23:13:48 +01:00
}
span . fancytree - node : hover . add - note - button {
2020-12-03 21:50:41 +01:00
display : inline - block ;
2020-11-27 23:13:48 +01:00
}
2020-01-12 20:15:05 +01:00
< / s t y l e >
2020-05-02 00:28:40 +02:00
2020-11-27 20:40:32 +01:00
< button class = "btn btn-sm icon-button bx bx-layer-minus collapse-tree-button" title = "Collapse note tree" data - trigger - command = "collapseTree" > < / b u t t o n >
< button class = "btn btn-sm icon-button bx bx-crosshair scroll-to-active-note-button" title = "Scroll to active note" data - trigger - command = "scrollToActiveNote" > < / b u t t o n >
2020-05-02 00:28:40 +02:00
< button class = "btn btn-sm icon-button bx bx-cog tree-settings-button" title = "Tree settings" > < / b u t t o n >
< div class = "tree-settings-popup" >
< div class = "form-check" >
< label class = "form-check-label" >
< input class = "form-check-input hide-archived-notes" type = "checkbox" value = "" >
Hide archived notes
< / l a b e l >
< / d i v >
< div class = "form-check" >
< label class = "form-check-label" >
< input class = "form-check-input hide-included-images" type = "checkbox" value = "" >
Hide images included in a note
2020-05-02 13:52:02 +02:00
< span class = "bx bx-info-circle"
title = "Images which are shown in the parent text note will not be displayed in the tree" > < / s p a n >
2020-05-02 00:28:40 +02:00
< / l a b e l >
< / d i v >
< br / >
< button class = "btn btn-sm btn-primary save-tree-settings-button" type = "submit" > Save & apply changes < / b u t t o n >
< / d i v >
< div class = "tree" > < / d i v >
2020-01-12 20:15:05 +01:00
< / d i v >
2020-01-11 21:19:56 +01:00
` ;
2021-01-25 23:43:36 +01:00
const MAX _SEARCH _RESULTS _IN _TREE = 100 ;
2020-02-16 22:56:40 +01:00
export default class NoteTreeWidget extends TabAwareWidget {
2020-05-02 00:28:40 +02:00
constructor ( treeName ) {
super ( ) ;
this . treeName = treeName ;
}
2020-01-12 20:15:05 +01:00
doRender ( ) {
2020-02-08 21:54:39 +01:00
this . $widget = $ ( TPL ) ;
2020-05-02 00:28:40 +02:00
this . $tree = this . $widget . find ( '.tree' ) ;
2020-01-11 21:19:56 +01:00
2021-02-06 19:58:12 +01:00
this . $tree . on ( "mousedown" , ".unhoist-button" , ( ) => hoistedNoteService . unhoist ( ) ) ;
this . $tree . on ( "mousedown" , ".refresh-search-button" , e => this . refreshSearch ( e ) ) ;
this . $tree . on ( "mousedown" , ".add-note-button" , e => {
2020-11-27 23:13:48 +01:00
const node = $ . ui . fancytree . getNode ( e ) ;
noteCreateService . createNote ( node . data . noteId , {
isProtected : node . data . isProtected
} ) ;
} ) ;
2020-01-11 21:19:56 +01:00
2021-02-06 19:58:12 +01:00
this . $tree . on ( "mousedown" , ".enter-workspace-button" , e => {
2020-11-29 22:32:31 +01:00
const node = $ . ui . fancytree . getNode ( e ) ;
this . triggerCommand ( 'hoistNote' , { noteId : node . data . noteId } ) ;
} ) ;
2020-01-11 21:19:56 +01:00
// fancytree doesn't support middle click so this is a way to support it
2020-05-02 00:28:40 +02:00
this . $tree . on ( 'mousedown' , '.fancytree-title' , e => {
2020-01-11 21:19:56 +01:00
if ( e . which === 2 ) {
const node = $ . ui . fancytree . getNode ( e ) ;
2020-02-10 20:57:56 +01:00
const notePath = treeService . getNotePath ( node ) ;
if ( notePath ) {
2020-11-24 23:24:05 +01:00
appContext . tabManager . openTabWithNoteWithHoisting ( notePath ) ;
2020-02-10 20:57:56 +01:00
}
2020-01-11 21:19:56 +01:00
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
}
} ) ;
2020-01-12 20:15:05 +01:00
2020-05-02 00:28:40 +02:00
this . $treeSettingsPopup = this . $widget . find ( '.tree-settings-popup' ) ;
this . $hideArchivedNotesCheckbox = this . $treeSettingsPopup . find ( '.hide-archived-notes' ) ;
this . $hideIncludedImages = this . $treeSettingsPopup . find ( '.hide-included-images' ) ;
this . $treeSettingsButton = this . $widget . find ( '.tree-settings-button' ) ;
this . $treeSettingsButton . on ( "click" , e => {
if ( this . $treeSettingsPopup . is ( ":visible" ) ) {
this . $treeSettingsPopup . hide ( ) ;
return ;
}
this . $hideArchivedNotesCheckbox . prop ( "checked" , this . hideArchivedNotes ) ;
this . $hideIncludedImages . prop ( "checked" , this . hideIncludedImages ) ;
let top = this . $treeSettingsButton [ 0 ] . offsetTop ;
let left = this . $treeSettingsButton [ 0 ] . offsetLeft ;
2020-11-27 22:02:55 +01:00
top -= this . $treeSettingsPopup . outerHeight ( ) + 10 ;
2020-05-02 00:28:40 +02:00
left += this . $treeSettingsButton . outerWidth ( ) - this . $treeSettingsPopup . outerWidth ( ) ;
if ( left < 0 ) {
left = 0 ;
}
this . $treeSettingsPopup . css ( {
display : "block" ,
top : top ,
left : left
} ) . addClass ( "show" ) ;
2020-05-02 13:52:02 +02:00
return false ;
2020-05-02 00:28:40 +02:00
} ) ;
2020-05-02 13:52:02 +02:00
this . $treeSettingsPopup . on ( "click" , e => { e . stopPropagation ( ) ; } ) ;
$ ( document ) . on ( 'click' , ( ) => this . $treeSettingsPopup . hide ( ) ) ;
2020-05-02 00:28:40 +02:00
this . $saveTreeSettingsButton = this . $treeSettingsPopup . find ( '.save-tree-settings-button' ) ;
this . $saveTreeSettingsButton . on ( 'click' , async ( ) => {
await this . setHideArchivedNotes ( this . $hideArchivedNotesCheckbox . prop ( "checked" ) ) ;
await this . setHideIncludedImages ( this . $hideIncludedImages . prop ( "checked" ) ) ;
this . $treeSettingsPopup . hide ( ) ;
this . reloadTreeFromCache ( ) ;
} ) ;
2020-08-25 23:25:15 +02:00
this . initFancyTree ( ) ;
2020-01-12 20:15:05 +01:00
2020-06-22 22:28:45 +02:00
this . setupNoteTitleTooltip ( ) ;
2020-01-11 21:19:56 +01:00
}
2020-06-22 22:28:45 +02:00
setupNoteTitleTooltip ( ) {
// the following will dynamically set tree item's tooltip if the whole item's text is not currently visible
// if the whole text is visible then no tooltip is show since that's unnecessarily distracting
// see https://github.com/zadam/trilium/pull/1120 for discussion
// code inspired by https://gist.github.com/jtsternberg/c272d7de5b967cec2d3d
const isEnclosing = ( $container , $sub ) => {
const conOffset = $container . offset ( ) ;
const conDistanceFromTop = conOffset . top + $container . outerHeight ( true ) ;
const conDistanceFromLeft = conOffset . left + $container . outerWidth ( true ) ;
const subOffset = $sub . offset ( ) ;
const subDistanceFromTop = subOffset . top + $sub . outerHeight ( true ) ;
const subDistanceFromLeft = subOffset . left + $sub . outerWidth ( true ) ;
return conDistanceFromTop > subDistanceFromTop
&& conOffset . top < subOffset . top
&& conDistanceFromLeft > subDistanceFromLeft
&& conOffset . left < subOffset . left ;
} ;
this . $tree . on ( "mouseenter" , "span.fancytree-title" , e => {
e . currentTarget . title = isEnclosing ( this . $tree , $ ( e . currentTarget ) )
? ""
: e . currentTarget . innerText ;
} ) ;
}
2020-05-02 00:28:40 +02:00
get hideArchivedNotes ( ) {
return options . is ( "hideArchivedNotes_" + this . treeName ) ;
}
async setHideArchivedNotes ( val ) {
await options . save ( "hideArchivedNotes_" + this . treeName , val . toString ( ) ) ;
}
get hideIncludedImages ( ) {
return options . is ( "hideIncludedImages_" + this . treeName ) ;
}
async setHideIncludedImages ( val ) {
await options . save ( "hideIncludedImages_" + this . treeName , val . toString ( ) ) ;
}
2020-08-26 16:50:16 +02:00
initFancyTree ( ) {
const treeData = [ this . prepareRootNode ( ) ] ;
2020-01-12 09:12:13 +01:00
2020-05-02 00:28:40 +02:00
this . $tree . fancytree ( {
2020-06-03 11:06:45 +02:00
titlesTabbable : true ,
2020-08-24 23:33:27 +02:00
keyboard : true ,
2020-11-22 23:05:02 +01:00
extensions : [ "dnd5" , "clones" , "filter" ] ,
2020-01-12 09:12:13 +01:00
source : treeData ,
2020-08-09 23:20:57 +02:00
scrollOfs : {
2020-08-24 23:33:27 +02:00
top : 100 ,
bottom : 100
2020-08-09 23:20:57 +02:00
} ,
2020-05-02 00:28:40 +02:00
scrollParent : this . $tree ,
2020-01-12 09:12:13 +01:00
minExpandLevel : 2 , // root can't be collapsed
click : ( event , data ) => {
2020-08-17 20:58:34 +02:00
this . activityDetected ( ) ;
2020-01-12 09:12:13 +01:00
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-02-10 20:57:56 +01:00
const notePath = treeService . getNotePath ( node ) ;
2020-11-24 23:24:05 +01:00
appContext . tabManager . openTabWithNoteWithHoisting ( notePath ) ;
2020-01-12 09:12:13 +01:00
}
2020-03-01 15:19:16 +01:00
else if ( data . node . isActive ( ) ) {
// this is important for single column mobile view, otherwise it's not possible to see again previously displayed note
this . tree . reactivate ( true ) ;
}
2020-01-12 09:12:13 +01:00
else {
node . setActive ( ) ;
2020-01-12 11:15:23 +01:00
this . clearSelectedNodes ( ) ;
2020-01-12 09:12:13 +01:00
}
return false ;
}
} ,
activate : async ( event , data ) => {
// click event won't propagate so let's close context menu manually
2020-02-29 11:28:30 +01:00
contextMenu . hide ( ) ;
2020-01-12 09:12:13 +01:00
2020-02-10 20:57:56 +01:00
const notePath = treeService . getNotePath ( data . node ) ;
2020-01-12 09:12:13 +01:00
2020-02-16 18:11:32 +01:00
const activeTabContext = appContext . tabManager . getActiveTabContext ( ) ;
2020-01-24 21:15:40 +01:00
await activeTabContext . setNote ( notePath ) ;
2020-03-01 15:19:16 +01:00
if ( utils . isMobile ( ) ) {
2021-01-15 23:05:19 +01:00
this . triggerCommand ( 'setActiveScreen' , { screen : 'detail' } ) ;
2020-03-01 15:19:16 +01:00
}
2020-01-12 09:12:13 +01:00
} ,
2020-05-12 10:52:07 +02:00
expand : ( event , data ) => this . setExpanded ( data . node . data . branchId , true ) ,
collapse : ( event , data ) => this . setExpanded ( data . node . data . branchId , false ) ,
2020-11-22 23:05:02 +01:00
filter : {
counter : false ,
mode : "hide" ,
autoExpand : true
} ,
2020-01-12 10:35:33 +01:00
dnd5 : {
autoExpandMS : 600 ,
2021-01-28 21:19:01 +01:00
preventLazyParents : false ,
2020-01-12 10:35:33 +01:00
dragStart : ( node , data ) => {
2020-04-11 15:09:38 +02:00
const notes = this . getSelectedOrActiveNodes ( node ) . map ( node => ( {
2020-01-12 10:35:33 +01:00
noteId : node . data . noteId ,
2020-05-30 10:30:21 +02:00
branchId : node . data . branchId ,
2020-01-12 10:35:33 +01:00
title : node . title
2020-02-28 22:07:08 +01:00
} ) ) ;
2020-01-12 10:35:33 +01:00
data . dataTransfer . setData ( "text" , JSON . stringify ( notes ) ) ;
2020-08-24 23:33:27 +02:00
return true ; // allow dragging to start
2020-01-12 10:35:33 +01:00
} ,
2020-10-10 21:08:28 +02:00
dragEnter : ( node , data ) => node . data . noteType !== 'search' ,
2020-01-12 10:35:33 +01:00
dragDrop : async ( node , data ) => {
if ( ( data . hitMode === 'over' && node . data . noteType === 'search' ) ||
( [ 'after' , 'before' ] . includes ( data . hitMode )
2020-02-05 22:08:45 +01:00
&& ( node . data . noteId === hoistedNoteService . getHoistedNoteId ( ) || node . getParent ( ) . data . noteType === 'search' ) ) ) {
2020-01-12 10:35:33 +01: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 12:30:30 +01:00
const importService = await import ( '../services/import.js' ) ;
2020-01-12 10:35:33 +01:00
importService . uploadFiles ( node . data . noteId , files , {
safeImport : true ,
shrinkImages : true ,
textImportedAsText : true ,
codeImportedAsCode : true ,
2020-05-30 16:15:00 -05:00
explodeArchives : true ,
replaceUnderscoresWithSpaces : true
2020-01-12 10:35:33 +01:00
} ) ;
}
else {
2020-05-30 10:30:21 +02:00
const jsonStr = dataTransfer . getData ( "text" ) ;
let notes = null ;
try {
notes = JSON . parse ( jsonStr ) ;
}
catch ( e ) {
2020-10-12 21:05:34 +02:00
logError ( ` Cannot parse ${ jsonStr } into notes for drop ` ) ;
2020-05-30 10:30:21 +02:00
return ;
}
2020-01-12 10:35:33 +01:00
// This function MUST be defined to enable dropping of items on the tree.
// data.hitMode is 'before', 'after', or 'over'.
2020-05-30 10:30:21 +02:00
const selectedBranchIds = notes . map ( note => note . branchId ) ;
2020-01-12 10:35:33 +01:00
if ( data . hitMode === "before" ) {
2020-02-17 19:42:52 +01:00
branchService . moveBeforeBranch ( selectedBranchIds , node . data . branchId ) ;
2020-01-12 10:35:33 +01:00
} else if ( data . hitMode === "after" ) {
2020-02-17 19:42:52 +01:00
branchService . moveAfterBranch ( selectedBranchIds , node . data . branchId ) ;
2020-01-12 10:35:33 +01:00
} else if ( data . hitMode === "over" ) {
2020-05-30 10:30:21 +02:00
branchService . moveToParentNote ( selectedBranchIds , node . data . branchId ) ;
2020-01-12 10:35:33 +01:00
} else {
throw new Error ( "Unknown hitMode=" + data . hitMode ) ;
}
}
}
2020-01-12 09:12:13 +01:00
} ,
2020-05-02 00:28:40 +02:00
lazyLoad : ( event , data ) => {
2020-05-02 12:16:48 +02:00
const { noteId , noteType } = data . node . data ;
if ( noteType === 'search' ) {
const notePath = treeService . getNotePath ( data . node . getParent ( ) ) ;
// this is a search cycle (search note is a descendant of its own search result)
if ( notePath . includes ( noteId ) ) {
data . result = [ ] ;
return ;
}
2020-01-12 09:12:13 +01:00
2020-08-26 16:50:16 +02:00
data . result = treeCache . reloadNotes ( [ noteId ] ) . then ( ( ) => {
2021-01-25 23:43:36 +01:00
const note = treeCache . getNoteFromCache ( noteId ) ;
let childNoteIds = note . getChildNoteIds ( ) ;
2021-02-23 22:01:02 +01:00
if ( note . type === 'search' && childNoteIds . length > MAX _SEARCH _RESULTS _IN _TREE ) {
2021-01-25 23:43:36 +01:00
childNoteIds = childNoteIds . slice ( 0 , MAX _SEARCH _RESULTS _IN _TREE ) ;
}
return treeCache . getNotes ( childNoteIds ) ;
} ) . then ( ( ) => {
const note = treeCache . getNoteFromCache ( noteId ) ;
2020-01-12 09:12:13 +01:00
2021-01-25 23:43:36 +01:00
return this . prepareChildren ( note ) ;
2020-08-26 16:50:16 +02:00
} ) ;
2020-01-12 09:12:13 +01:00
}
2020-08-26 16:50:16 +02:00
else {
data . result = treeCache . loadSubTree ( noteId ) . then ( note => this . prepareChildren ( note ) ) ;
2020-01-12 09:12:13 +01:00
}
} ,
2020-08-26 16:50:16 +02:00
clones : {
highlightActiveClones : true
} ,
2020-08-26 22:12:01 +02:00
enhanceTitle : async function ( event , data ) {
const node = data . node ;
2021-01-25 23:43:36 +01:00
if ( ! node . data . noteId ) {
// if there's "non-note" node, then don't enhance
// this can happen for e.g. "Load error!" node
return ;
}
2021-02-06 19:58:12 +01:00
const note = await treeCache . getNote ( node . data . noteId ) ;
const activeTabContext = appContext . tabManager . getActiveTabContext ( ) ;
2020-08-26 22:12:01 +02:00
const $span = $ ( node . span ) ;
2021-01-15 23:05:19 +01:00
$span . find ( '.tree-item-button' ) . remove ( ) ;
2020-11-27 23:13:48 +01:00
2021-02-07 20:55:49 +01:00
const isHoistedNote = activeTabContext && activeTabContext . hoistedNoteId === note . noteId && note . noteId !== 'root' ;
if ( isHoistedNote ) {
2021-02-06 19:58:12 +01:00
const $unhoistButton = $ ( '<span class="tree-item-button unhoist-button bx bx-door-open" title="Unhoist"></span>' ) ;
$unhoistButton . on ( 'click' , ( ) => alert ( "bebe" ) ) ;
$span . append ( $unhoistButton ) ;
}
2020-08-26 22:12:01 +02:00
2021-02-07 20:55:49 +01:00
if ( note . hasLabel ( 'workspace' ) && ! isHoistedNote ) {
2020-11-29 22:32:31 +01:00
const $enterWorkspaceButton = $ ( '<span class="tree-item-button enter-workspace-button bx bx-door-open" title="Hoist this note (workspace)"></span>' ) ;
$span . append ( $enterWorkspaceButton ) ;
}
2020-11-27 23:13:48 +01:00
if ( note . type === 'search' ) {
2020-11-29 22:32:31 +01:00
const $refreshSearchButton = $ ( '<span class="tree-item-button refresh-search-button bx bx-refresh" title="Refresh saved search results"></span>' ) ;
2020-11-27 23:13:48 +01:00
$span . append ( $refreshSearchButton ) ;
}
if ( note . type !== 'search' ) {
2020-11-29 22:32:31 +01:00
const $createChildNoteButton = $ ( '<span class="tree-item-button add-note-button bx bx-plus" title="Create child note"></span>' ) ;
2020-08-26 22:12:01 +02:00
2020-11-27 23:13:48 +01:00
$span . append ( $createChildNoteButton ) ;
2020-08-26 22:12:01 +02:00
}
} ,
2020-04-30 23:58:34 +02:00
// this is done to automatically lazy load all expanded notes after tree load
2020-01-12 09:12:13 +01:00
loadChildren : ( event , data ) => {
2020-05-03 13:15:08 +02:00
data . node . visit ( ( subNode ) => {
// Load all lazy/unloaded child nodes
// (which will trigger `loadChildren` recursively)
if ( subNode . isUndefined ( ) && subNode . isExpanded ( ) ) {
subNode . load ( ) ;
}
} ) ;
2020-01-12 09:12:13 +01:00
}
} ) ;
2020-08-26 22:12:01 +02:00
if ( ! utils . isMobile ( ) ) {
this . getHotKeys ( ) . then ( hotKeys => {
for ( const key in hotKeys ) {
const handler = hotKeys [ key ] ;
$ ( this . tree . $container ) . on ( 'keydown' , null , key , evt => {
const node = this . tree . getActiveNode ( ) ;
return handler ( node , evt ) ;
// return false from the handler will stop default handling.
} ) ;
}
} ) ;
}
2020-05-02 00:28:40 +02:00
this . $tree . on ( 'contextmenu' , '.fancytree-node' , e => {
2020-01-12 09:12:13 +01:00
const node = $ . ui . fancytree . getNode ( e ) ;
2020-04-11 22:06:04 +02:00
import ( "../services/tree_context_menu.js" ) . then ( ( { default : TreeContextMenu } ) => {
const treeContextMenu = new TreeContextMenu ( this , node ) ;
treeContextMenu . show ( e ) ;
} ) ;
2020-01-12 09:12:13 +01:00
return false ; // blocks default browser right click menu
} ) ;
2020-05-02 00:28:40 +02:00
this . tree = $ . ui . fancytree . getTree ( this . $tree ) ;
}
2020-08-26 16:50:16 +02:00
prepareRootNode ( ) {
2020-11-22 23:05:02 +01:00
return this . prepareNode ( treeCache . getBranch ( 'root' ) ) ;
2020-05-02 00:28:40 +02:00
}
2020-08-26 16:50:16 +02:00
/ * *
* @ param { NoteShort } parentNote
* /
prepareChildren ( parentNote ) {
2020-08-24 23:33:27 +02:00
utils . assertArguments ( parentNote ) ;
const noteList = [ ] ;
const hideArchivedNotes = this . hideArchivedNotes ;
2021-02-13 20:07:08 +01:00
let childBranches = parentNote . getFilteredChildBranches ( ) ;
2021-01-25 23:43:36 +01:00
if ( childBranches . length > MAX _SEARCH _RESULTS _IN _TREE ) {
childBranches = childBranches . slice ( 0 , MAX _SEARCH _RESULTS _IN _TREE ) ;
}
for ( const branch of childBranches ) {
2020-08-24 23:33:27 +02:00
if ( hideArchivedNotes ) {
2020-08-26 16:50:16 +02:00
const note = branch . getNoteFromCache ( ) ;
2020-08-24 23:33:27 +02:00
if ( note . hasLabel ( 'archived' ) ) {
continue ;
}
}
2020-08-26 16:50:16 +02:00
const node = this . prepareNode ( branch ) ;
2020-08-24 23:33:27 +02:00
noteList . push ( node ) ;
}
return noteList ;
}
2020-06-24 22:29:53 +02:00
updateNode ( node ) {
const note = treeCache . getNoteFromCache ( node . data . noteId ) ;
const branch = treeCache . getBranch ( node . data . branchId ) ;
2021-02-17 20:59:44 +01:00
if ( ! note ) {
console . log ( ` Node update not possible because note ${ node . data . noteId } was not found. ` ) ;
return ;
}
if ( ! branch ) {
console . log ( ` Node update not possible because branch ${ node . data . branchId } was not found. ` ) ;
return ;
}
2020-06-24 22:29:53 +02:00
const title = ( branch . prefix ? ( branch . prefix + " - " ) : "" ) + note . title ;
node . data . isProtected = note . isProtected ;
node . data . noteType = note . type ;
2021-02-13 20:07:08 +01:00
node . folder = note . isFolder ( ) ;
node . icon = note . getIcon ( ) ;
2020-06-24 22:29:53 +02:00
node . extraClasses = this . getExtraClasses ( note ) ;
node . title = utils . escapeHtml ( title ) ;
if ( node . isExpanded ( ) !== branch . isExpanded ) {
2021-01-29 23:33:41 +01:00
node . setExpanded ( branch . isExpanded , { noEvents : true , noAnimation : true } ) ;
2020-06-24 22:29:53 +02:00
}
node . renderTitle ( ) ;
}
2020-08-26 16:50:16 +02:00
/ * *
* @ param { Branch } branch
* /
2020-08-28 14:29:20 +02:00
prepareNode ( branch , forceLazy = false ) {
2020-08-26 16:50:16 +02:00
const note = branch . getNoteFromCache ( ) ;
2020-05-02 00:28:40 +02:00
if ( ! note ) {
2020-08-28 14:29:20 +02:00
throw new Error ( ` Branch " ${ branch . branchId } " has no note " ${ branch . noteId } " ` ) ;
2020-05-02 00:28:40 +02:00
}
const title = ( branch . prefix ? ( branch . prefix + " - " ) : "" ) + note . title ;
2021-02-13 20:07:08 +01:00
const isFolder = note . isFolder ( ) ;
2020-05-03 13:59:49 +02:00
2020-05-02 00:28:40 +02:00
const node = {
noteId : note . noteId ,
parentNoteId : branch . parentNoteId ,
branchId : branch . branchId ,
isProtected : note . isProtected ,
noteType : note . type ,
title : utils . escapeHtml ( title ) ,
extraClasses : this . getExtraClasses ( note ) ,
2020-11-23 22:52:48 +01:00
icon : note . getIcon ( isFolder ) ,
2020-05-02 00:28:40 +02:00
refKey : note . noteId ,
2020-05-02 12:16:48 +02:00
lazy : true ,
2020-05-03 13:59:49 +02:00
folder : isFolder ,
2020-11-23 22:52:48 +01:00
expanded : branch . isExpanded && note . type !== 'search' ,
2020-05-02 00:28:40 +02:00
key : utils . randomString ( 12 ) // this should prevent some "duplicate key" errors
} ;
2020-08-28 14:29:20 +02:00
if ( isFolder && node . expanded && ! forceLazy ) {
2020-08-26 16:50:16 +02:00
node . children = this . prepareChildren ( note ) ;
2020-05-02 00:28:40 +02:00
}
return node ;
}
getExtraClasses ( note ) {
utils . assertArguments ( note ) ;
const extraClasses = [ ] ;
if ( note . isProtected ) {
extraClasses . push ( "protected" ) ;
}
if ( note . getParentNoteIds ( ) . length > 1 ) {
extraClasses . push ( "multiple-parents" ) ;
}
const cssClass = note . getCssClass ( ) ;
if ( cssClass ) {
extraClasses . push ( cssClass ) ;
}
extraClasses . push ( utils . getNoteTypeClass ( note . type ) ) ;
if ( note . mime ) { // some notes should not have mime type (e.g. render)
extraClasses . push ( utils . getMimeTypeClass ( note . mime ) ) ;
}
if ( note . hasLabel ( 'archived' ) ) {
extraClasses . push ( "archived" ) ;
}
return extraClasses . join ( " " ) ;
2020-01-12 09:12:13 +01:00
}
/** @return {FancytreeNode[]} */
getSelectedNodes ( stopOnParents = false ) {
return this . tree . getSelectedNodes ( stopOnParents ) ;
2020-01-11 21:19:56 +01:00
}
2020-01-12 09:12:13 +01:00
/** @return {FancytreeNode[]} */
2020-01-12 11:15:23 +01:00
getSelectedOrActiveNodes ( node = null ) {
2020-05-31 10:32:35 +02:00
const nodes = this . getSelectedNodes ( true ) ;
2020-01-12 09:12:13 +01:00
2020-05-31 10:32:35 +02:00
// the node you start dragging should be included even if not selected
if ( node && ! nodes . find ( n => n . key === node . key ) ) {
nodes . push ( node ) ;
2020-01-12 09:12:13 +01:00
}
2020-05-31 10:32:35 +02:00
if ( nodes . length === 0 ) {
nodes . push ( this . getActiveNode ( ) ) ;
}
return nodes ;
2020-01-11 21:19:56 +01:00
}
2020-05-03 13:15:08 +02:00
async setExpandedStatusForSubtree ( node , isExpanded ) {
2020-01-12 09:12:13 +01:00
if ( ! node ) {
2020-02-10 20:57:56 +01:00
const hoistedNoteId = hoistedNoteService . getHoistedNoteId ( ) ;
2020-01-12 09:12:13 +01:00
2020-01-21 22:08:41 +01:00
node = this . getNodesByNoteId ( hoistedNoteId ) [ 0 ] ;
2020-01-12 09:12:13 +01:00
}
2020-05-03 13:15:08 +02:00
const { branchIds } = await server . put ( ` branches/ ${ node . data . branchId } /expanded-subtree/ ${ isExpanded ? 1 : 0 } ` ) ;
2020-04-30 23:09:25 +02:00
2021-02-04 23:08:16 +01:00
treeCache . getBranches ( branchIds , true )
. forEach ( branch => branch . isExpanded = ! ! isExpanded ) ;
2020-01-12 09:12:13 +01:00
2020-05-03 13:15:08 +02:00
await this . batchUpdate ( async ( ) => {
await node . load ( true ) ;
2020-05-03 13:52:12 +02:00
if ( node . data . noteId !== 'root' ) { // root is always expanded
2021-01-29 23:33:41 +01:00
await node . setExpanded ( isExpanded , { noEvents : true , noAnimation : true } ) ;
2020-05-03 13:52:12 +02:00
}
2020-04-29 23:13:05 +02:00
} ) ;
}
2020-05-03 13:15:08 +02:00
async expandTree ( node = null ) {
await this . setExpandedStatusForSubtree ( node , true ) ;
}
2020-04-29 23:13:05 +02:00
2020-05-03 13:15:08 +02:00
async collapseTree ( node = null ) {
await this . setExpandedStatusForSubtree ( node , false ) ;
2020-01-11 21:19:56 +01:00
}
2020-01-12 09:12:13 +01:00
2021-02-16 21:19:07 +01:00
collapseTreeEvent ( ) { this . collapseTree ( ) ; }
2020-11-27 20:40:32 +01:00
2020-01-12 11:15:23 +01:00
/ * *
* @ return { FancytreeNode | null }
* /
getActiveNode ( ) {
return this . tree . getActiveNode ( ) ;
}
2020-01-12 10:35:33 +01:00
/ * *
2020-02-17 22:14:39 +01: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 )
2020-01-12 10:35:33 +01:00
* @ return { FancytreeNode | null }
* /
getFocusedNode ( ) {
return this . tree . getFocusNode ( ) ;
}
clearSelectedNodes ( ) {
for ( const selectedNode of this . getSelectedNodes ( ) ) {
selectedNode . setSelected ( false ) ;
}
}
2021-02-16 21:19:07 +01:00
async scrollToActiveNoteEvent ( ) {
2020-02-16 18:11:32 +01:00
const activeContext = appContext . tabManager . getActiveTabContext ( ) ;
2020-01-12 11:15:23 +01:00
if ( activeContext && activeContext . notePath ) {
2020-06-03 11:06:45 +02:00
this . tree . setFocus ( true ) ;
2020-01-12 11:15:23 +01:00
const node = await this . expandToNote ( activeContext . notePath ) ;
await node . makeVisible ( { scrollIntoView : true } ) ;
2021-02-24 22:41:24 +01:00
node . setActive ( true , { noEvents : true , noFocus : false } ) ;
2020-01-12 11:15:23 +01:00
}
}
/** @return {FancytreeNode} */
2020-06-10 23:43:59 +02:00
async getNodeFromPath ( notePath , expand = false , logErrors = true ) {
2020-01-12 11:15:23 +01:00
utils . assertArguments ( notePath ) ;
2021-02-28 19:46:04 +01:00
/** @let {FancytreeNode} */
let parentNode = this . getNodesByNoteId ( 'root' ) [ 0 ] ;
2020-01-12 11:15:23 +01:00
2021-02-28 19:46:04 +01:00
const resolvedNotePathSegments = ( await treeService . resolveNotePathToSegments ( notePath , logErrors ) )
. slice ( 1 ) ;
2020-01-12 11:15:23 +01:00
2020-08-24 23:33:27 +02:00
if ( ! resolvedNotePathSegments ) {
2020-06-10 23:43:59 +02:00
if ( logErrors ) {
2020-10-12 21:05:34 +02:00
logError ( "Could not find run path for notePath:" , notePath ) ;
2020-06-10 23:43:59 +02:00
}
2020-01-12 11:15:23 +01:00
return ;
}
2020-08-24 23:33:27 +02:00
for ( const childNoteId of resolvedNotePathSegments ) {
2020-01-12 11:15:23 +01:00
// 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 ) {
2021-01-29 23:33:41 +01:00
await parentNode . setExpanded ( true , { noAnimation : true } ) ;
2020-06-03 11:06:45 +02:00
// although previous line should set the expanded status, it seems to happen asynchronously
// so we need to make sure it is set properly before calling updateNode which uses this flag
const branch = treeCache . getBranch ( parentNode . data . branchId ) ;
branch . isExpanded = true ;
2020-01-12 11:15:23 +01:00
}
2020-03-18 22:42:29 +01:00
this . updateNode ( parentNode ) ;
2020-01-12 11:15:23 +01: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 ) {
2020-06-10 23:43:59 +02:00
if ( logErrors ) {
2020-08-30 23:12:49 +02:00
// besides real errors this can be also caused by hiding of e.g. included images
// these are real notes with real notePath, user can display them in a detail
// but they don't have a node in the tree
2021-02-28 19:46:04 +01:00
ws . logError ( ` Can't find node for child node of noteId= ${ childNoteId } for parent of noteId= ${ parentNode . data . noteId } and hoistedNoteId= ${ hoistedNoteService . getHoistedNoteId ( ) } , requested path is ${ notePath } ` ) ;
2020-06-10 23:43:59 +02:00
}
2020-01-12 11:15:23 +01:00
return ;
}
}
parentNode = foundChildNode ;
}
}
return parentNode ;
}
/** @return {FancytreeNode} */
findChildNode ( parentNode , childNoteId ) {
2020-08-24 23:33:27 +02:00
return parentNode . getChildren ( ) . find ( childNode => childNode . data . noteId === childNoteId ) ;
2020-01-12 11:15:23 +01:00
}
/** @return {FancytreeNode} */
2020-06-10 23:43:59 +02:00
async expandToNote ( notePath , logErrors = true ) {
return this . getNodeFromPath ( notePath , true , logErrors ) ;
2020-01-12 11:15:23 +01:00
}
/** @return {FancytreeNode[]} */
2020-02-02 22:32:44 +01:00
getNodesByBranchId ( branchId ) {
2020-01-12 11:15:23 +01: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
}
2020-02-09 21:13:05 +01:00
isEnabled ( ) {
2020-03-06 23:34:39 +01:00
return ! ! this . tabContext ;
2020-02-09 21:13:05 +01:00
}
2020-01-18 20:49:49 +01:00
async refresh ( ) {
2020-03-06 22:17:07 +01:00
this . toggleInt ( this . isEnabled ( ) ) ;
2020-09-05 21:51:00 +02:00
this . $treeSettingsPopup . hide ( ) ;
2020-02-08 21:54:39 +01:00
2020-08-17 20:58:34 +02:00
this . activityDetected ( ) ;
2020-01-18 18:01:16 +01:00
const oldActiveNode = this . getActiveNode ( ) ;
2020-06-04 21:44:34 +02:00
let oldActiveNodeFocused = false ;
2020-01-18 18:01:16 +01:00
if ( oldActiveNode ) {
2020-06-04 21:44:34 +02:00
oldActiveNodeFocused = oldActiveNode . hasFocus ( ) ;
2020-01-18 18:01:16 +01:00
oldActiveNode . setActive ( false ) ;
2020-01-19 21:12:53 +01:00
oldActiveNode . setFocus ( false ) ;
2020-01-18 18:01:16 +01:00
}
2020-03-23 20:18:54 +01:00
if ( this . tabContext && this . tabContext . notePath && ! this . tabContext . note . isDeleted ) {
2020-01-18 18:01:16 +01:00
const newActiveNode = await this . getNodeFromPath ( this . tabContext . notePath ) ;
if ( newActiveNode ) {
if ( ! newActiveNode . isVisible ( ) ) {
await this . expandToNote ( this . tabContext . notePath ) ;
}
2020-06-04 21:44:34 +02:00
newActiveNode . setActive ( true , { noEvents : true , noFocus : ! oldActiveNodeFocused } ) ;
2020-02-12 22:25:52 +01:00
newActiveNode . makeVisible ( { scrollIntoView : true } ) ;
2020-01-18 18:01:16 +01:00
}
}
2020-11-22 23:05:02 +01:00
this . filterHoistedBranch ( ) ;
2020-01-18 18:01:16 +01:00
}
2020-11-27 23:13:48 +01:00
async refreshSearch ( e ) {
const activeNode = $ . ui . fancytree . getNode ( e ) ;
2020-02-14 20:18:09 +01:00
activeNode . load ( true ) ;
2021-01-29 23:33:41 +01:00
activeNode . setExpanded ( true , { noAnimation : true } ) ;
2020-02-14 20:18:09 +01:00
toastService . showMessage ( "Saved search note refreshed." ) ;
}
2020-04-29 23:13:05 +02:00
async batchUpdate ( cb ) {
try {
// disable rendering during update for increased performance
2020-04-30 23:09:25 +02:00
this . tree . enableUpdate ( false ) ;
2020-04-29 23:13:05 +02:00
await cb ( ) ;
}
finally {
2020-04-30 23:09:25 +02:00
this . tree . enableUpdate ( true ) ;
2020-04-29 23:13:05 +02:00
}
}
2020-08-17 20:58:34 +02:00
activityDetected ( ) {
if ( this . autoCollapseTimeoutId ) {
clearTimeout ( this . autoCollapseTimeoutId ) ;
}
this . autoCollapseTimeoutId = setTimeout ( ( ) => {
/ *
* We ' re collapsing notes after period of inactivity to "cleanup" the tree - users rarely
* collapse the notes and the tree becomes unusuably large .
* Some context : https : //github.com/zadam/trilium/issues/1192
* /
const noteIdsToKeepExpanded = new Set (
appContext . tabManager . getTabContexts ( )
. map ( tc => tc . notePathArray )
. flat ( )
) ;
let noneCollapsedYet = true ;
this . tree . getRootNode ( ) . visit ( node => {
if ( node . isExpanded ( ) && ! noteIdsToKeepExpanded . has ( node . data . noteId ) ) {
node . setExpanded ( false ) ;
if ( noneCollapsedYet ) {
toastService . showMessage ( "Auto collapsing notes after inactivity..." ) ;
noneCollapsedYet = false ;
}
}
} , false ) ;
} , 600 * 1000 ) ;
}
2020-08-28 22:52:57 +02:00
async entitiesReloadedEvent ( { loadResults } ) {
2020-08-17 20:58:34 +02:00
this . activityDetected ( ) ;
2020-08-16 22:57:48 +02:00
if ( loadResults . isEmptyForTree ( ) ) {
return ;
}
2020-03-29 22:54:14 +02:00
const activeNode = this . getActiveNode ( ) ;
2020-06-04 21:44:34 +02:00
const activeNodeFocused = activeNode && activeNode . hasFocus ( ) ;
2020-05-12 13:40:42 +02:00
const nextNode = activeNode ? ( activeNode . getNextSibling ( ) || activeNode . getPrevSibling ( ) || activeNode . getParent ( ) ) : null ;
2020-03-29 22:54:14 +02:00
const activeNotePath = activeNode ? treeService . getNotePath ( activeNode ) : null ;
2020-05-12 13:40:42 +02:00
const nextNotePath = nextNode ? treeService . getNotePath ( nextNode ) : null ;
2020-03-29 22:54:14 +02:00
const activeNoteId = activeNode ? activeNode . data . noteId : null ;
2020-01-29 21:38:58 +01:00
const noteIdsToUpdate = new Set ( ) ;
const noteIdsToReload = new Set ( ) ;
2020-01-26 11:41:40 +01:00
2020-01-29 21:38:58 +01:00
for ( const attr of loadResults . getAttributes ( ) ) {
2021-02-09 20:26:10 +01:00
if ( attr . type === 'label' && [ 'iconClass' , 'cssClass' , 'workspace' , 'workspaceIconClass' , 'archived' ] . includes ( attr . name ) ) {
2020-01-29 21:38:58 +01:00
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 ) ;
}
2020-04-27 23:27:45 +02:00
else if ( attr . type === 'relation' && attr . name === 'imageLink' ) {
const note = treeCache . getNoteFromCache ( attr . noteId ) ;
if ( note && note . getChildNoteIds ( ) . includes ( attr . value ) ) {
// there's new/deleted imageLink betwen note and its image child - which can show/hide
// the image (if there is a imageLink relation between parent and child then it is assumed to be "contained" in the note and thus does not have to be displayed in the tree)
noteIdsToReload . add ( attr . noteId ) ;
}
}
2020-01-29 21:38:58 +01:00
}
for ( const branch of loadResults . getBranches ( ) ) {
2021-02-25 21:16:21 +01:00
// adding noteId itself to update all potential clones
noteIdsToUpdate . add ( branch . noteId ) ;
2020-01-29 21:38:58 +01:00
for ( const node of this . getNodesByBranchId ( branch . branchId ) ) {
if ( branch . isDeleted ) {
2020-02-10 20:57:56 +01:00
if ( node . isActive ( ) ) {
2020-02-12 22:25:52 +01:00
const newActiveNode = node . getNextSibling ( )
|| node . getPrevSibling ( )
|| node . getParent ( ) ;
2020-01-29 21:38:58 +01:00
2020-02-12 22:25:52 +01:00
if ( newActiveNode ) {
newActiveNode . setActive ( true , { noEvents : true , noFocus : true } ) ;
}
2020-01-29 21:38:58 +01:00
}
2020-01-12 12:30:30 +01:00
2020-05-02 12:16:48 +02:00
if ( node . getParent ( ) ) {
node . remove ( ) ;
}
2020-02-12 22:25:52 +01:00
noteIdsToUpdate . add ( branch . parentNoteId ) ;
2020-01-12 12:30:30 +01:00
}
2020-01-29 21:38:58 +01:00
}
if ( ! branch . isDeleted ) {
for ( const parentNode of this . getNodesByNoteId ( branch . parentNoteId ) ) {
2020-02-17 22:47:50 +01:00
if ( parentNode . isFolder ( ) && ! parentNode . isLoaded ( ) ) {
2020-01-29 21:38:58 +01:00
continue ;
}
2020-02-17 22:47:50 +01:00
const found = ( parentNode . getChildren ( ) || [ ] ) . find ( child => child . data . noteId === branch . noteId ) ;
2020-01-12 12:30:30 +01:00
2020-01-29 21:38:58 +01:00
if ( ! found ) {
2020-08-28 14:29:20 +02:00
// make sure it's loaded
await treeCache . getNote ( branch . noteId ) ;
// we're forcing lazy since it's not clear if the whole required subtree is in tree cache
parentNode . addChildren ( [ this . prepareNode ( branch , true ) ] ) ;
2020-06-05 00:07:45 +02:00
this . sortChildren ( parentNode ) ;
2020-09-14 22:48:20 +02:00
// this might be a first child which would force an icon change
noteIdsToUpdate . add ( branch . parentNoteId ) ;
2020-01-29 21:38:58 +01:00
}
}
}
}
2020-02-02 22:32:44 +01:00
for ( const noteId of loadResults . getNoteIds ( ) ) {
2020-02-02 22:33:50 +01:00
noteIdsToUpdate . add ( noteId ) ;
2020-02-02 22:32:44 +01:00
}
2020-01-29 21:38:58 +01:00
2020-04-29 23:13:05 +02:00
await this . batchUpdate ( async ( ) => {
2020-05-05 18:56:12 +02:00
for ( const noteId of noteIdsToReload ) {
2020-04-29 23:13:05 +02:00
for ( const node of this . getNodesByNoteId ( noteId ) ) {
2020-05-05 18:56:12 +02:00
await node . load ( true ) ;
noteIdsToUpdate . add ( noteId ) ;
2020-04-29 23:13:05 +02:00
}
2020-01-29 21:38:58 +01:00
}
2020-04-29 23:13:05 +02:00
for ( const parentNoteId of loadResults . getNoteReorderings ( ) ) {
for ( const node of this . getNodesByNoteId ( parentNoteId ) ) {
if ( node . isLoaded ( ) ) {
2020-06-05 00:07:45 +02:00
this . sortChildren ( node ) ;
2020-04-29 23:13:05 +02:00
}
2020-01-12 12:30:30 +01:00
}
}
2020-04-29 23:13:05 +02:00
} ) ;
2020-01-12 12:30:30 +01:00
2020-05-05 18:56:12 +02:00
// for some reason node update cannot be in the batchUpdate() block (node is not re-rendered)
for ( const noteId of noteIdsToUpdate ) {
for ( const node of this . getNodesByNoteId ( noteId ) ) {
this . updateNode ( node ) ;
}
}
2020-02-10 20:57:56 +01:00
if ( activeNotePath ) {
2020-06-10 23:43:59 +02:00
let node = await this . expandToNote ( activeNotePath , false ) ;
2020-03-29 22:54:14 +02:00
2020-04-02 22:55:11 +02:00
if ( node && node . data . noteId !== activeNoteId ) {
2020-03-29 22:54:14 +02:00
// if the active note has been moved elsewhere then it won't be found by the path
// so we switch to the alternative of trying to find it by noteId
const notesById = this . getNodesByNoteId ( activeNoteId ) ;
2020-03-18 10:08:16 +01:00
2020-03-29 22:54:14 +02:00
// if there are multiple clones then we'd rather not activate any one
node = notesById . length === 1 ? notesById [ 0 ] : null ;
}
if ( node ) {
2020-06-04 21:44:34 +02:00
node . setActive ( true , { noEvents : true , noFocus : true } ) ;
2020-03-29 22:54:14 +02:00
}
2020-05-12 13:40:42 +02:00
else {
// this is used when original note has been deleted and we want to move the focus to the note above/below
2020-06-10 23:43:59 +02:00
node = await this . expandToNote ( nextNotePath , false ) ;
2020-05-12 13:40:42 +02:00
if ( node ) {
2021-02-20 23:17:29 +01:00
// FIXME: this is conceptually wrong
// here note tree is responsible for updating global state of the application
// this should be done by tabcontext / tabmanager and note tree should only listen to
// changes in active note and just set the "active" state
2021-02-22 22:43:54 +01:00
// We don't await since that can bring up infinite cycles when e.g. custom widget does some backend requests which wait for max sync ID processed
appContext . tabManager . getActiveTabContext ( ) . setNote ( nextNotePath ) ;
2020-05-12 13:40:42 +02:00
}
}
2020-06-03 11:06:45 +02:00
const newActiveNode = this . getActiveNode ( ) ;
// return focus if the previously active node was also focused
if ( newActiveNode && activeNodeFocused ) {
2020-06-04 21:44:34 +02:00
await newActiveNode . setFocus ( true ) ;
2020-06-03 11:06:45 +02:00
}
2020-01-12 12:30:30 +01:00
}
2020-11-29 20:55:24 +01:00
if ( noteIdsToReload . size > 0 || noteIdsToUpdate . size > 0 ) {
// workaround for https://github.com/mar10/fancytree/issues/1054
this . filterHoistedBranch ( ) ;
}
2020-01-12 12:30:30 +01:00
}
2020-01-19 18:05:06 +01:00
2020-06-05 00:07:45 +02:00
sortChildren ( node ) {
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 ;
} ) ;
}
2021-02-04 22:05:32 +01:00
setExpanded ( branchId , isExpanded ) {
2020-01-24 15:44:24 +01:00
utils . assertArguments ( branchId ) ;
2020-10-12 21:05:34 +02:00
const branch = treeCache . getBranch ( branchId , true ) ;
2020-10-10 21:08:28 +02:00
if ( ! branch ) {
2020-10-12 21:05:34 +02:00
if ( branchId && branchId . startsWith ( 'virt' ) ) {
// in case of virtual branches there's nothing to update
return ;
}
else {
logError ( ` Cannot find branch= ${ branchId } ` ) ;
return ;
}
2020-10-10 21:08:28 +02:00
}
2020-05-12 10:52:07 +02:00
branch . isExpanded = isExpanded ;
2020-01-24 15:44:24 +01:00
2021-02-04 22:05:32 +01:00
server . put ( ` branches/ ${ branchId } /expanded/ ${ isExpanded ? 1 : 0 } ` ) ;
2020-01-24 15:44:24 +01:00
}
2020-02-01 22:29:32 +01:00
async reloadTreeFromCache ( ) {
2020-01-24 15:44:24 +01:00
const activeNode = this . getActiveNode ( ) ;
2020-02-10 20:57:56 +01:00
const activeNotePath = activeNode !== null ? treeService . getNotePath ( activeNode ) : null ;
2020-01-24 15:44:24 +01:00
2020-08-26 16:50:16 +02:00
const rootNode = this . prepareRootNode ( ) ;
2020-04-30 23:58:34 +02:00
await this . batchUpdate ( async ( ) => {
await this . tree . reload ( [ rootNode ] ) ;
} ) ;
2020-01-24 15:44:24 +01:00
if ( activeNotePath ) {
const node = await this . getNodeFromPath ( activeNotePath , true ) ;
2020-06-04 21:44:34 +02:00
await node . setActive ( true , { noEvents : true , noFocus : true } ) ;
2020-01-24 15:44:24 +01:00
}
}
2020-11-22 23:05:02 +01:00
async hoistedNoteChangedEvent ( { tabId } ) {
if ( this . isTab ( tabId ) ) {
this . filterHoistedBranch ( ) ;
}
}
2021-02-28 19:46:04 +01:00
async filterHoistedBranch ( ) {
2020-11-22 23:05:02 +01:00
if ( this . tabContext ) {
2021-02-28 19:46:04 +01:00
// make sure the hoisted node is loaded (can be unloaded e.g. after tree collapse in another tab)
const hoistedNotePath = await treeService . resolveNotePath ( this . tabContext . hoistedNoteId ) ;
await this . getNodeFromPath ( hoistedNotePath ) ;
2020-11-29 20:55:24 +01:00
if ( this . tabContext . hoistedNoteId === 'root' ) {
this . tree . clearFilter ( ) ;
}
else {
this . tree . filterBranches ( node => node . data . noteId === this . tabContext . hoistedNoteId ) ;
}
2020-11-22 23:05:02 +01:00
}
2020-01-24 15:44:24 +01:00
}
2020-02-16 19:23:49 +01:00
treeCacheReloadedEvent ( ) {
2020-02-01 22:29:32 +01:00
this . reloadTreeFromCache ( ) ;
2020-01-24 15:44:24 +01:00
}
2020-02-15 10:41:21 +01:00
2020-02-16 22:14:28 +01:00
async getHotKeys ( ) {
const actions = await keyboardActionsService . getActionsForScope ( 'note-tree' ) ;
2020-08-24 23:33:27 +02:00
const hotKeyMap = { } ;
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
for ( const action of actions ) {
for ( const shortcut of action . effectiveShortcuts ) {
2020-03-17 22:49:43 +01:00
hotKeyMap [ utils . normalizeShortcut ( shortcut ) ] = node => {
2020-12-05 23:00:28 +01:00
const notePath = treeService . getNotePath ( node ) ;
this . triggerCommand ( action . actionName , { node , notePath } ) ;
2020-03-17 22:49:43 +01:00
return false ;
}
2020-02-16 22:14:28 +01:00
}
}
2020-02-17 22:38:46 +01:00
return hotKeyMap ;
2020-02-16 22:14:28 +01:00
}
/ * *
* @ param { FancytreeNode } node
* /
getSelectedOrActiveBranchIds ( node ) {
const nodes = this . getSelectedOrActiveNodes ( node ) ;
return nodes . map ( node => node . data . branchId ) ;
}
2020-02-16 22:56:40 +01:00
async deleteNotesCommand ( { node } ) {
2021-02-17 23:03:34 +01:00
const branchIds = this . getSelectedOrActiveBranchIds ( node )
. filter ( branchId => ! branchId . startsWith ( 'virt-' ) ) ; // search results can't be deleted
if ( ! branchIds . length ) {
return ;
}
2020-05-12 10:52:07 +02:00
2020-02-17 19:42:52 +01:00
await branchService . deleteNotes ( branchIds ) ;
2020-02-16 22:56:40 +01:00
this . clearSelectedNodes ( ) ;
2020-02-16 22:14:28 +01:00
}
2020-05-12 10:52:07 +02:00
2021-01-23 21:00:59 +01:00
canBeMovedUpOrDown ( node ) {
if ( node . data . noteId === 'root' ) {
return false ;
}
const parentNote = treeCache . getNoteFromCache ( node . getParent ( ) . data . noteId ) ;
if ( parentNote && parentNote . hasLabel ( 'sorted' ) ) {
return false ;
}
return true ;
}
2020-02-16 22:14:28 +01:00
moveNoteUpCommand ( { node } ) {
2021-01-23 21:00:59 +01:00
if ( ! this . canBeMovedUpOrDown ( node ) ) {
return ;
}
2020-02-16 22:14:28 +01:00
const beforeNode = node . getPrevSibling ( ) ;
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
if ( beforeNode !== null ) {
2020-02-17 19:42:52 +01:00
branchService . moveBeforeBranch ( [ node . data . branchId ] , beforeNode . data . branchId ) ;
2020-02-16 22:14:28 +01:00
}
}
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
moveNoteDownCommand ( { node } ) {
2021-01-23 21:00:59 +01:00
if ( ! this . canBeMovedUpOrDown ( node ) ) {
return ;
}
2020-02-16 22:14:28 +01:00
const afterNode = node . getNextSibling ( ) ;
2021-01-23 21:00:59 +01:00
2020-02-16 22:14:28 +01:00
if ( afterNode !== null ) {
2020-02-17 19:42:52 +01:00
branchService . moveAfterBranch ( [ node . data . branchId ] , afterNode . data . branchId ) ;
2020-02-16 22:14:28 +01:00
}
}
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
moveNoteUpInHierarchyCommand ( { node } ) {
2020-02-17 19:42:52 +01:00
branchService . moveNodeUpInHierarchy ( node ) ;
2020-02-16 22:14:28 +01:00
}
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
moveNoteDownInHierarchyCommand ( { node } ) {
const toNode = node . getPrevSibling ( ) ;
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
if ( toNode !== null ) {
2020-05-31 22:33:02 +02:00
branchService . moveToParentNote ( [ node . data . branchId ] , toNode . data . branchId ) ;
2020-02-16 22:14:28 +01:00
}
}
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
addNoteAboveToSelectionCommand ( ) {
const node = this . getFocusedNode ( ) ;
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
if ( ! node ) {
return ;
}
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
if ( node . isActive ( ) ) {
node . setSelected ( true ) ;
}
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
const prevSibling = node . getPrevSibling ( ) ;
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
if ( prevSibling ) {
prevSibling . setActive ( true , { noEvents : true } ) ;
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
if ( prevSibling . isSelected ( ) ) {
node . setSelected ( false ) ;
}
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
prevSibling . setSelected ( true ) ;
}
}
addNoteBelowToSelectionCommand ( ) {
const node = this . getFocusedNode ( ) ;
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
if ( ! node ) {
return ;
}
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
if ( node . isActive ( ) ) {
node . setSelected ( true ) ;
}
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
const nextSibling = node . getNextSibling ( ) ;
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
if ( nextSibling ) {
nextSibling . setActive ( true , { noEvents : true } ) ;
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
if ( nextSibling . isSelected ( ) ) {
node . setSelected ( false ) ;
}
2020-05-12 10:52:07 +02:00
2020-02-16 22:14:28 +01:00
nextSibling . setSelected ( true ) ;
}
}
2020-04-30 23:09:25 +02:00
expandSubtreeCommand ( { node } ) {
2020-04-29 23:13:05 +02:00
this . expandTree ( node ) ;
}
2020-02-16 22:14:28 +01:00
collapseSubtreeCommand ( { node } ) {
this . collapseTree ( node ) ;
}
sortChildNotesCommand ( { node } ) {
2021-02-28 23:40:15 +01:00
import ( "../dialogs/sort_child_notes.js" ) . then ( d => d . showDialog ( node . data . noteId ) ) ;
2020-02-16 22:14:28 +01:00
}
2020-03-29 19:43:04 +02:00
async recentChangesInSubtreeCommand ( { node } ) {
const recentChangesDialog = await import ( '../dialogs/recent_changes.js' ) ;
recentChangesDialog . showDialog ( node . data . noteId ) ;
}
2020-02-16 22:14:28 +01:00
selectAllNotesInParentCommand ( { node } ) {
for ( const child of node . getParent ( ) . getChildren ( ) ) {
child . setSelected ( true ) ;
}
}
copyNotesToClipboardCommand ( { node } ) {
clipboard . copy ( this . getSelectedOrActiveBranchIds ( node ) ) ;
}
cutNotesToClipboardCommand ( { node } ) {
clipboard . cut ( this . getSelectedOrActiveBranchIds ( node ) ) ;
}
pasteNotesFromClipboardCommand ( { node } ) {
2020-05-31 22:33:02 +02:00
clipboard . pasteInto ( node . data . branchId ) ;
2020-02-16 22:14:28 +01:00
}
2020-02-16 22:56:40 +01:00
pasteNotesAfterFromClipboard ( { node } ) {
clipboard . pasteAfter ( node . data . branchId ) ;
}
async exportNoteCommand ( { node } ) {
const exportDialog = await import ( '../dialogs/export.js' ) ;
const notePath = treeService . getNotePath ( node ) ;
exportDialog . showDialog ( notePath , "subtree" ) ;
}
async importIntoNoteCommand ( { node } ) {
const importDialog = await import ( '../dialogs/import.js' ) ;
importDialog . showDialog ( node . data . noteId ) ;
}
forceNoteSyncCommand ( { node } ) {
2020-04-08 20:38:50 +02:00
syncService . forceNoteSync ( node . data . noteId ) ;
2020-02-16 22:56:40 +01:00
}
2020-02-16 22:14:28 +01:00
editNoteTitleCommand ( { node } ) {
2020-02-28 00:11:34 +01:00
appContext . triggerCommand ( 'focusOnTitle' ) ;
2020-02-16 22:14:28 +01:00
}
2020-02-16 22:56:40 +01:00
protectSubtreeCommand ( { node } ) {
2020-02-26 16:37:17 +01:00
protectedSessionService . protectNote ( node . data . noteId , true , true ) ;
2020-02-16 22:56:40 +01:00
}
unprotectSubtreeCommand ( { node } ) {
2020-02-26 16:37:17 +01:00
protectedSessionService . protectNote ( node . data . noteId , false , true ) ;
2020-02-16 22:56:40 +01:00
}
2020-11-19 14:06:32 +01:00
duplicateSubtreeCommand ( { node } ) {
2020-09-23 22:45:51 +02:00
const nodesToDuplicate = this . getSelectedOrActiveNodes ( node ) ;
for ( const nodeToDuplicate of nodesToDuplicate ) {
const note = treeCache . getNoteFromCache ( nodeToDuplicate . data . noteId ) ;
2020-02-16 22:56:40 +01:00
2020-09-23 22:45:51 +02:00
if ( note . isProtected && ! protectedSessionHolder . isProtectedSessionAvailable ( ) ) {
continue ;
}
const branch = treeCache . getBranch ( nodeToDuplicate . data . branchId ) ;
2020-11-19 14:06:32 +01:00
noteCreateService . duplicateSubtree ( nodeToDuplicate . data . noteId , branch . parentNoteId ) ;
2020-09-23 22:45:51 +02:00
}
2020-02-16 22:56:40 +01:00
}
2020-05-12 10:52:07 +02:00
}