2025-04-10 22:18:12 +02:00
import appContext , { type CommandListenerData , type EventData } from "../components/app_context.js" ;
import type { SetNoteOpts } from "../components/note_context.js" ;
import type FBranch from "../entities/fbranch.js" ;
import type FNote from "../entities/fnote.js" ;
import type { NoteType } from "../entities/fnote.js" ;
2022-08-05 16:44:26 +02:00
import contextMenu from "../menus/context_menu.js" ;
2020-02-17 19:42:52 +01:00
import branchService from "../services/branches.js" ;
2020-02-16 22:14:28 +01:00
import clipboard from "../services/clipboard.js" ;
2022-11-25 15:29:57 +01:00
import dialogService from "../services/dialog.js" ;
2025-04-10 22:18:12 +02:00
import froca from "../services/froca.js" ;
import hoistedNoteService from "../services/hoisted_note.js" ;
2024-09-13 22:22:16 +03:00
import { t } from "../services/i18n.js" ;
2025-04-10 22:18:12 +02:00
import keyboardActionsService from "../services/keyboard_actions.js" ;
import linkService from "../services/link.js" ;
2025-01-28 15:44:15 +02:00
import type LoadResults from "../services/load_results.js" ;
2025-02-24 10:10:34 +02:00
import type { AttributeRow , BranchRow } from "../services/load_results.js" ;
2025-04-10 22:18:12 +02:00
import noteCreateService from "../services/note_create.js" ;
import options from "../services/options.js" ;
import protectedSessionService from "../services/protected_session.js" ;
import protectedSessionHolder from "../services/protected_session_holder.js" ;
import server from "../services/server.js" ;
import shortcutService from "../services/shortcuts.js" ;
import toastService from "../services/toast.js" ;
import treeService from "../services/tree.js" ;
import utils from "../services/utils.js" ;
import ws from "../services/ws.js" ;
import NoteContextAwareWidget from "./note_context_aware_widget.js" ;
2020-01-11 21:19:56 +01:00
2025-04-01 23:24:21 +03:00
const TPL = /*html*/ `
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 ;
}
2024-12-28 10:57:03 +02:00
2020-05-02 00:28:40 +02:00
. tree {
height : 100 % ;
overflow : auto ;
2020-09-08 23:25:21 +02:00
padding - bottom : 35px ;
2021-02-13 23:20:22 +01:00
padding - top : 5px ;
2020-01-12 20:15:05 +01:00
}
2024-12-28 10:57:03 +02:00
2021-11-09 20:26:38 +00:00
. tree - actions {
background - color : var ( -- launcher - pane - background - color ) ;
z - index : 100 ;
position : absolute ;
bottom : 0 ;
display : flex ;
align - items : flex - end ;
justify - content : flex - end ;
2023-03-24 14:46:09 -04:00
right : 17px ;
2023-03-24 15:17:17 -04:00
border - radius : 7px ;
border : 1px solid var ( -- main - border - color ) ;
2021-11-09 20:26:38 +00:00
}
2024-12-28 10:57:03 +02:00
2021-05-27 23:17:13 +02:00
button . tree - floating - button {
2023-03-24 15:17:17 -04:00
margin : 1px ;
2021-05-27 23:17:13 +02:00
font - size : 1.5em ;
2021-11-09 20:26:38 +00:00
padding : 5px ;
2021-05-27 23:17:13 +02:00
max - height : 34px ;
2021-06-06 22:15:51 +02:00
color : var ( -- launcher - pane - text - color ) ;
2021-06-01 23:19:49 +02:00
background - color : var ( -- button - background - color ) ;
2021-05-27 23:17:13 +02:00
border - radius : var ( -- button - border - radius ) ;
border : 1px solid transparent ;
}
2024-12-28 10:57:03 +02:00
2021-05-27 23:17:13 +02:00
button . tree - floating - button :hover {
border : 1px solid var ( -- button - border - color ) ;
}
2024-12-28 10:57:03 +02:00
2021-05-27 23:17:13 +02:00
. collapse - tree - button {
right : 100px ;
2020-11-27 20:40:32 +01:00
}
2024-12-28 10:57:03 +02:00
2020-11-27 20:40:32 +01:00
. scroll - to - active - note - button {
2021-05-27 23:17:13 +02:00
right : 55px ;
2020-11-27 20:40:32 +01:00
}
2024-12-28 10:57:03 +02:00
2020-05-02 00:28:40 +02:00
. tree - settings - button {
2020-11-27 22:02:55 +01:00
right : 10px ;
2020-05-02 00:28:40 +02:00
}
2024-12-28 10:57:03 +02:00
2020-05-02 00:28:40 +02:00
. tree - settings - popup {
2024-12-28 10:57:03 +02:00
display : none ;
position : absolute ;
background - color : var ( -- accented - background - color ) ;
border : 1px solid var ( -- main - border - color ) ;
padding : 20px ;
2020-05-02 00:28:40 +02:00
z - index : 1000 ;
2024-12-28 10:57:03 +02:00
width : 340px ;
2020-11-27 22:02:55 +01:00
border - radius : 10px ;
2020-05-02 00:28:40 +02:00
}
2024-12-28 10:57:03 +02:00
2022-08-05 16:44:26 +02:00
. tree . hidden - node - is - hidden {
display : none ;
}
2020-01-12 20:15:05 +01:00
< / style >
2024-12-28 10:57:03 +02:00
2021-05-27 23:17:13 +02:00
< div class = "tree" > < / div >
2024-12-28 10:57:03 +02:00
2021-11-09 20:26:38 +00:00
< div class = "tree-actions" >
2024-12-28 10:57:03 +02:00
< button class = "tree-floating-button bx bx-layer-minus collapse-tree-button"
title = "${t(" note_tree . collapse - title ")}"
2021-11-09 20:26:38 +00:00
data - trigger - command = "collapseTree" > < / button >
2024-12-28 10:57:03 +02:00
< button class = "tree-floating-button bx bx-crosshair scroll-to-active-note-button"
title = "${t(" note_tree . scroll - active - title ")}"
2021-11-09 20:26:38 +00:00
data - trigger - command = "scrollToActiveNote" > < / button >
2024-12-28 10:57:03 +02:00
< button class = "tree-floating-button bx bxs-tree tree-settings-button"
2024-09-13 22:22:16 +03:00
title = "${t(" note_tree . tree - settings - title ")}" > < / button >
2021-11-09 20:26:38 +00:00
< / div >
2024-12-28 10:57:03 +02:00
2020-05-02 00:28:40 +02:00
< div class = "tree-settings-popup" >
2024-12-12 01:46:43 +02:00
< h4 > $ { t ( "note_tree.tree-settings-title" ) } < / h4 >
2020-05-02 00:28:40 +02:00
< div class = "form-check" >
2025-01-24 22:35:45 +02:00
< label class = "form-check-label tn-checkbox" >
2020-05-02 00:28:40 +02:00
< input class = "form-check-input hide-archived-notes" type = "checkbox" value = "" >
2024-09-13 22:22:16 +03:00
$ { t ( "note_tree.hide-archived-notes" ) }
2020-05-02 00:28:40 +02:00
< / label >
< / div >
2021-03-18 20:11:58 +01:00
< div class = "form-check" >
2025-01-24 22:35:45 +02:00
< label class = "form-check-label tn-checkbox" >
2021-03-18 20:11:58 +01:00
< input class = "form-check-input auto-collapse-note-tree" type = "checkbox" value = "" >
2024-09-13 22:22:16 +03:00
$ { t ( "note_tree.automatically-collapse-notes" ) }
2024-12-28 10:57:03 +02:00
< span class = "bx bx-info-circle"
2024-09-13 22:22:16 +03:00
title = "${t(" note_tree . automatically - collapse - notes - title ")}" > < / span >
2021-03-18 20:11:58 +01:00
< / label >
< / div >
2024-12-28 10:57:03 +02:00
2020-05-02 00:28:40 +02:00
< br / >
2024-12-28 10:57:03 +02:00
2024-09-13 22:22:16 +03:00
< button class = "btn btn-sm btn-primary save-tree-settings-button" type = "submit" > $ { t ( "note_tree.save-changes" ) } < / button >
2020-05-02 00:28:40 +02:00
< / div >
2020-01-12 20:15:05 +01:00
< / div >
2020-01-11 21:19:56 +01:00
` ;
2021-01-25 23:43:36 +01:00
const MAX_SEARCH_RESULTS_IN_TREE = 100 ;
2023-06-13 00:12:55 +02:00
// this has to be hanged on the actual elements to effectively intercept and stop click event
2025-01-28 15:44:15 +02:00
const cancelClickPropagation : JQuery.TypeEventHandler < unknown , unknown , unknown , unknown , any > = ( e ) = > e . stopPropagation ( ) ;
// TODO: Fix once we remove Node.js API from public
type Timeout = NodeJS . Timeout | string | number | undefined ;
// TODO: Deduplicate with server special_notes
type LauncherType = "launcher" | "note" | "script" | "customWidget" | "spacer" ;
// TODO: Deduplicate with the server
interface CreateLauncherResponse {
success : boolean ;
message : string ;
note : {
noteId : string ;
2025-03-02 20:47:57 +01:00
} ;
2025-01-28 15:44:15 +02:00
}
interface ExpandedSubtreeResponse {
2025-03-02 20:47:57 +01:00
branchIds : string [ ] ;
2025-01-28 15:44:15 +02:00
}
interface Node extends Fancytree . NodeData {
noteId : string ;
parentNoteId : string ;
branchId : string ;
isProtected : boolean ;
noteType : NoteType ;
}
interface RefreshContext {
2025-02-24 10:10:34 +02:00
noteIdsToUpdate : Set < string > ;
2025-01-28 15:44:15 +02:00
noteIdsToReload : Set < string > ;
}
2023-06-13 00:12:55 +02:00
2021-05-22 12:35:41 +02:00
export default class NoteTreeWidget extends NoteContextAwareWidget {
2025-01-28 15:44:15 +02:00
private $tree ! : JQuery < HTMLElement > ;
private $treeActions ! : JQuery < HTMLElement > ;
private $treeSettingsButton ! : JQuery < HTMLElement > ;
private $treeSettingsPopup ! : JQuery < HTMLElement > ;
private $saveTreeSettingsButton ! : JQuery < HTMLElement > ;
private $hideArchivedNotesCheckbox ! : JQuery < HTMLElement > ;
private $autoCollapseNoteTree ! : JQuery < HTMLElement > ;
private treeName : "main" ;
private autoCollapseTimeoutId? : Timeout ;
private lastFilteredHoistedNotePath? : string | null ;
private tree ! : Fancytree . Fancytree ;
2022-12-11 13:54:12 +01:00
constructor ( ) {
2020-05-02 00:28:40 +02:00
super ( ) ;
2022-12-11 13:54:12 +01:00
this . treeName = "main" ; // legacy value
2020-05-02 00:28:40 +02:00
}
2020-01-12 20:15:05 +01:00
doRender() {
2020-02-08 21:54:39 +01:00
this . $widget = $ ( TPL ) ;
2025-01-09 18:07:02 +02:00
this . $tree = this . $widget . find ( ".tree" ) ;
2021-11-23 20:37:32 +00:00
this . $treeActions = this . $widget . find ( ".tree-actions" ) ;
2020-01-11 21:19:56 +01:00
2021-02-06 19:58:12 +01:00
this . $tree . on ( "mousedown" , ".unhoist-button" , ( ) = > hoistedNoteService . unhoist ( ) ) ;
2025-01-09 18:07:02 +02:00
this . $tree . on ( "mousedown" , ".refresh-search-button" , ( e ) = > this . refreshSearch ( e ) ) ;
this . $tree . on ( "mousedown" , ".add-note-button" , ( e ) = > {
2025-01-28 15:44:15 +02:00
const node = $ . ui . fancytree . getNode ( e as unknown as Event ) ;
2021-03-03 22:48:06 +01:00
const parentNotePath = treeService . getNotePath ( node ) ;
2020-11-27 23:13:48 +01:00
2021-03-03 22:48:06 +01:00
noteCreateService . createNote ( parentNotePath , {
2020-11-27 23:13:48 +01:00
isProtected : node.data.isProtected
} ) ;
} ) ;
2020-01-11 21:19:56 +01:00
2025-01-09 18:07:02 +02:00
this . $tree . on ( "mousedown" , ".enter-workspace-button" , ( e ) = > {
2025-01-28 15:44:15 +02:00
const node = $ . ui . fancytree . getNode ( e as unknown as Event ) ;
2020-11-29 22:32:31 +01:00
2025-01-09 18:07:02 +02:00
this . triggerCommand ( "hoistNote" , { noteId : node.data.noteId } ) ;
2020-11-29 22:32:31 +01:00
} ) ;
2023-06-30 11:18:34 +02:00
// fancytree doesn't support middle click, so this is a way to support it
2025-01-09 18:07:02 +02:00
this . $tree . on ( "mousedown" , ".fancytree-title" , ( e ) = > {
2020-01-11 21:19:56 +01:00
if ( e . which === 2 ) {
2025-01-28 15:44:15 +02:00
const node = $ . ui . fancytree . getNode ( e as unknown as Event ) ;
2020-01-11 21:19:56 +01:00
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
2025-01-09 18:07:02 +02:00
this . $treeSettingsPopup = this . $widget . find ( ".tree-settings-popup" ) ;
this . $hideArchivedNotesCheckbox = this . $treeSettingsPopup . find ( ".hide-archived-notes" ) ;
this . $autoCollapseNoteTree = this . $treeSettingsPopup . find ( ".auto-collapse-note-tree" ) ;
2020-05-02 00:28:40 +02:00
2025-01-09 18:07:02 +02:00
this . $treeSettingsButton = this . $widget . find ( ".tree-settings-button" ) ;
2025-04-10 22:18:12 +02:00
this . $treeSettingsButton . on ( "click" , ( ) = > {
2020-05-02 00:28:40 +02:00
if ( this . $treeSettingsPopup . is ( ":visible" ) ) {
this . $treeSettingsPopup . hide ( ) ;
return ;
}
this . $hideArchivedNotesCheckbox . prop ( "checked" , this . hideArchivedNotes ) ;
2021-03-18 20:11:58 +01:00
this . $autoCollapseNoteTree . prop ( "checked" , this . autoCollapseNoteTree ) ;
2020-05-02 00:28:40 +02:00
2025-01-28 15:44:15 +02:00
const top = this . $treeActions [ 0 ] . offsetTop - ( this . $treeSettingsPopup . outerHeight ( ) ? ? 0 ) ;
const left = Math . max ( 0 , this . $treeActions [ 0 ] . offsetLeft - ( this . $treeSettingsPopup . outerWidth ( ) ? ? 0 ) + ( this . $treeActions . outerWidth ( ) ? ? 0 ) ) ;
2020-05-02 00:28:40 +02:00
2025-01-09 18:07:02 +02:00
this . $treeSettingsPopup
. css ( {
top ,
left
} )
. show ( ) ;
2020-05-02 13:52:02 +02:00
return false ;
2020-05-02 00:28:40 +02:00
} ) ;
2025-01-09 18:07:02 +02:00
this . $treeSettingsPopup . on ( "click" , ( e ) = > {
e . stopPropagation ( ) ;
} ) ;
2020-05-02 13:52:02 +02:00
2025-01-09 18:07:02 +02:00
$ ( document ) . on ( "click" , ( ) = > this . $treeSettingsPopup . hide ( ) ) ;
2020-05-02 13:52:02 +02:00
2025-01-09 18:07:02 +02:00
this . $saveTreeSettingsButton = this . $treeSettingsPopup . find ( ".save-tree-settings-button" ) ;
this . $saveTreeSettingsButton . on ( "click" , async ( ) = > {
2020-05-02 00:28:40 +02:00
await this . setHideArchivedNotes ( this . $hideArchivedNotesCheckbox . prop ( "checked" ) ) ;
2021-03-18 20:11:58 +01:00
await this . setAutoCollapseNoteTree ( this . $autoCollapseNoteTree . prop ( "checked" ) ) ;
2020-05-02 00:28:40 +02:00
this . $treeSettingsPopup . hide ( ) ;
this . reloadTreeFromCache ( ) ;
} ) ;
2022-12-11 21:27:03 +01:00
// note tree starts initializing already during render which is atypical
Promise . all ( [ options . initializedPromise , froca . initializedPromise ] ) . then ( ( ) = > 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
2025-01-28 15:44:15 +02:00
const isEnclosing = ( $container : JQuery < HTMLElement > , $sub : JQuery < HTMLElement > ) = > {
2023-04-13 10:28:14 +08:00
const conOffset = $container . offset ( ) ;
2025-01-28 15:44:15 +02:00
const conDistanceFromTop = ( conOffset ? . top ? ? 0 ) + ( $container . outerHeight ( true ) ? ? 0 ) ;
const conDistanceFromLeft = ( conOffset ? . left ? ? 0 ) + ( $container . outerWidth ( true ) ? ? 0 ) ;
2020-06-22 22:28:45 +02:00
2023-04-13 10:28:14 +08:00
const subOffset = $sub . offset ( ) ;
2025-01-28 15:44:15 +02:00
const subDistanceFromTop = ( subOffset ? . top ? ? 0 ) + ( $sub . outerHeight ( true ) ? ? 0 ) ;
const subDistanceFromLeft = ( subOffset ? . left ? ? 0 ) + ( $sub . outerWidth ( true ) ? ? 0 ) ;
2020-06-22 22:28:45 +02:00
2025-01-28 15:44:15 +02:00
return conDistanceFromTop > subDistanceFromTop
&& ( conOffset ? . top ? ? 0 ) < ( subOffset ? . top ? ? 0 )
&& conDistanceFromLeft > subDistanceFromLeft
&& ( conOffset ? . left ? ? 0 ) < ( subOffset ? . left ? ? 0 ) ;
2020-06-22 22:28:45 +02:00
} ;
2025-01-09 18:07:02 +02:00
this . $tree . on ( "mouseenter" , "span.fancytree-title" , ( e ) = > {
e . currentTarget . title = isEnclosing ( this . $tree , $ ( e . currentTarget ) ) ? "" : e . currentTarget . innerText ;
2020-06-22 22:28:45 +02:00
} ) ;
}
2020-05-02 00:28:40 +02:00
get hideArchivedNotes() {
2022-12-21 15:19:05 +01:00
return options . is ( ` hideArchivedNotes_ ${ this . treeName } ` ) ;
2020-05-02 00:28:40 +02:00
}
2025-01-28 15:44:15 +02:00
async setHideArchivedNotes ( val : string ) {
2022-12-21 15:19:05 +01:00
await options . save ( ` hideArchivedNotes_ ${ this . treeName } ` , val . toString ( ) ) ;
2020-05-02 00:28:40 +02:00
}
2021-03-18 20:11:58 +01:00
get autoCollapseNoteTree() {
return options . is ( "autoCollapseNoteTree" ) ;
}
2025-01-28 15:44:15 +02:00
async setAutoCollapseNoteTree ( val : string ) {
2021-03-18 20:11:58 +01:00
await options . save ( "autoCollapseNoteTree" , 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
2025-02-24 10:10:34 +02:00
click : ( event , data ) : boolean = > {
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 ;
2025-01-09 18:07:02 +02:00
if ( node . isSelected ( ) && targetType === "icon" ) {
this . triggerCommand ( "openBulkActionsDialog" , {
2022-06-12 13:57:22 +02:00
selectedOrActiveNoteIds : this.getSelectedOrActiveNoteIds ( node )
} ) ;
return false ;
2025-01-09 18:07:02 +02:00
} else if ( targetType === "title" || targetType === "icon" ) {
2020-01-12 09:12:13 +01:00
if ( event . shiftKey ) {
2022-06-03 09:42:35 +02:00
const activeNode = this . getActiveNode ( ) ;
if ( activeNode . getParent ( ) !== node . getParent ( ) ) {
2025-02-24 10:10:34 +02:00
return true ;
2022-06-03 09:42:35 +02:00
}
this . clearSelectedNodes ( ) ;
2025-01-28 15:44:15 +02:00
function selectInBetween ( first : Fancytree.FancytreeNode , second : Fancytree.FancytreeNode ) {
2022-06-03 09:42:35 +02:00
for ( let i = 0 ; first && first !== second && i < 10000 ; i ++ ) {
first . setSelected ( true ) ;
first = first . getNextSibling ( ) ;
}
second . setSelected ( ) ;
}
if ( activeNode . getIndex ( ) < node . getIndex ( ) ) {
selectInBetween ( activeNode , node ) ;
} else {
selectInBetween ( node , activeNode ) ;
}
2020-01-12 09:12:13 +01:00
node . setFocus ( true ) ;
2025-01-09 18:07:02 +02:00
} else if ( ( ! utils . isMac ( ) && event . ctrlKey ) || ( utils . isMac ( ) && event . metaKey ) ) {
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 ) ;
2025-01-09 18:07:02 +02:00
} else if ( event . altKey ) {
2022-06-03 09:42:35 +02:00
node . setSelected ( ! node . isSelected ( ) ) ;
node . setFocus ( true ) ;
2025-01-09 18:07:02 +02:00
} else if ( data . node . isActive ( ) ) {
2020-03-01 15:19:16 +01:00
// this is important for single column mobile view, otherwise it's not possible to see again previously displayed note
2025-01-28 15:44:15 +02:00
this . tree . reactivate ( ) ;
2025-01-09 18:07:02 +02:00
} else {
2020-01-12 09:12:13 +01:00
node . setActive ( ) ;
}
return false ;
}
2025-02-24 10:10:34 +02:00
return true ;
2020-01-12 09:12:13 +01:00
} ,
2025-01-09 18:07:02 +02:00
beforeActivate : ( event , { node } ) = > {
2023-07-15 09:48:30 +02:00
// hidden subtree is hidden hackily - we want it to be present in the tree so that we can switch to it
// without reloading the whole tree, but we want it to be hidden when hoisted to root. FancyTree allows
// filtering the display only by ascendant - i.e. if the root is visible, all the descendants are as well.
// We solve it by hiding the hidden subtree via CSS (class "hidden-node-is-hidden"),
// but then we need to prevent activating it, e.g. by keyboard
2022-12-22 21:01:52 +01:00
2025-01-09 18:07:02 +02:00
if ( hoistedNoteService . getHoistedNoteId ( ) === "_hidden" ) {
2022-12-22 21:01:52 +01:00
// if we're hoisted in hidden subtree, we want to avoid crossing to "visible" tree,
2022-12-22 22:52:04 +01:00
// which could happen via UP key from hidden root
2025-01-09 18:07:02 +02:00
return node . data . noteId !== "root" ;
2022-12-22 21:01:52 +01:00
}
// we're not hoisted to hidden subtree, the only way to cross is via DOWN key to the hidden root
2025-01-09 18:07:02 +02:00
return node . data . noteId !== "_hidden" ;
2022-08-05 16:44:26 +02:00
} ,
2020-01-12 09:12:13 +01:00
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
2025-04-10 22:09:04 +02:00
// hide all dropdowns, fix calendar widget dropdown doesn't close when click on a note
$ ( '.dropdown-menu' ) . parent ( '.dropdown' ) . find ( '[data-bs-toggle="dropdown"]' ) . dropdown ( 'hide' ) ;
2021-04-21 20:38:07 +02:00
this . clearSelectedNodes ( ) ;
2020-02-10 20:57:56 +01:00
const notePath = treeService . getNotePath ( data . node ) ;
2020-01-12 09:12:13 +01:00
2021-05-22 12:35:41 +02:00
const activeNoteContext = appContext . tabManager . getActiveContext ( ) ;
2025-02-24 10:10:34 +02:00
const opts : SetNoteOpts = { } ;
2025-03-03 21:02:18 +01:00
if ( activeNoteContext ? . viewScope ? . viewMode === "contextual-help" ) {
2025-02-07 19:33:26 +02:00
opts . viewScope = activeNoteContext . viewScope ;
}
2025-03-03 21:02:18 +01:00
await activeNoteContext ? . setNote ( notePath , opts ) ;
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 ) = > {
2025-01-09 18:07:02 +02:00
if ( node . data . noteId === "root" || utils . isLaunchBarConfig ( node . data . noteId ) || node . data . noteId . startsWith ( "_options" ) ) {
2022-11-26 14:57:39 +01:00
return false ;
}
2025-01-09 18:07:02 +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
2022-09-13 23:36:59 +02:00
if ( notes . length === 1 ) {
2025-01-09 18:07:02 +02:00
linkService . createLink ( notes [ 0 ] . noteId , { referenceLink : true , autoConvertToImage : true } ) . then ( ( $link ) = > data . dataTransfer . setData ( "text/html" , $link [ 0 ] . outerHTML ) ) ;
} else {
Promise . all ( notes . map ( ( note ) = > linkService . createLink ( note . noteId , { referenceLink : true , autoConvertToImage : true } ) ) ) . then ( ( links ) = > {
const $list = $ ( "<ul>" ) . append ( . . . links . map ( ( $link ) = > $ ( "<li>" ) . append ( $link ) ) ) ;
2022-09-13 23:36:59 +02:00
data . dataTransfer . setData ( "text/html" , $list [ 0 ] . outerHTML ) ;
} ) ;
}
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
} ,
2025-04-10 22:18:12 +02:00
dragEnter : ( node ) = > {
2025-01-09 18:07:02 +02:00
if ( node . data . noteType === "search" ) {
2022-11-26 14:57:39 +01:00
return false ;
2025-01-09 18:07:02 +02:00
} else if ( node . data . noteId === "_lbRoot" ) {
2022-11-26 14:57:39 +01:00
return false ;
2025-01-09 18:07:02 +02:00
} else if ( node . data . noteId . startsWith ( "_options" ) ) {
2022-12-08 15:29:14 +01:00
return false ;
2025-01-09 18:07:02 +02:00
} else if ( node . data . noteType === "launcher" ) {
return [ "before" , "after" ] ;
} else if ( [ "_lbAvailableLaunchers" , "_lbVisibleLaunchers" ] . includes ( node . data . noteId ) ) {
return [ "over" ] ;
2022-11-26 14:57:39 +01:00
} else {
return true ;
}
} ,
2020-01-12 10:35:33 +01:00
dragDrop : async ( node , data ) = > {
2025-01-09 18:07:02 +02:00
if (
( data . hitMode === "over" && node . data . noteType === "search" ) ||
( [ "after" , "before" ] . includes ( data . hitMode ) && ( node . data . noteId === hoistedNoteService . getHoistedNoteId ( ) || node . getParent ( ) . data . noteType === "search" ) )
) {
2022-06-16 19:53:33 +02:00
await dialogService . info ( "Dropping notes into this location is not allowed." ) ;
2020-01-12 10:35:33 +01:00
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
2025-01-09 18:07:02 +02:00
const importService = await import ( "../services/import.js" ) ;
2020-01-12 10:35:33 +01:00
2025-01-09 18:07:02 +02:00
importService . uploadFiles ( "notes" , node . data . noteId , files , {
2020-01-12 10:35:33 +01:00
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
} ) ;
2025-01-09 18:07:02 +02:00
} else {
2020-05-30 10:30:21 +02:00
const jsonStr = dataTransfer . getData ( "text" ) ;
2025-01-28 15:44:15 +02:00
let notes : BranchRow [ ] ;
2020-05-30 10:30:21 +02:00
try {
notes = JSON . parse ( jsonStr ) ;
2025-04-10 22:18:12 +02:00
} catch ( error : unknown ) {
if ( error instanceof Error ) {
logError ( ` Cannot parse JSON ' ${ jsonStr } ' into notes for drop: ${ error . message } ` ) ;
} else {
logError ( ` Cannot parse JSON ' ${ jsonStr } ' into notes for drop: Unknown error ` ) ;
}
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'.
2025-02-24 10:10:34 +02:00
const selectedBranchIds = notes
. map ( ( note ) = > note . branchId )
. filter ( ( branchId ) = > branchId ) as string [ ] ;
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 {
2023-05-04 22:16:18 +02:00
throw new Error ( ` Unknown hitMode ' ${ data . hitMode } ' ` ) ;
2020-01-12 10:35:33 +01:00
}
}
}
2020-01-12 09:12:13 +01:00
} ,
2020-05-02 00:28:40 +02:00
lazyLoad : ( event , data ) = > {
2025-01-09 18:07:02 +02:00
const { noteId , noteType } = data . node . data ;
2020-05-02 12:16:48 +02:00
2025-01-09 18:07:02 +02:00
if ( noteType === "search" ) {
2020-05-02 12:16:48 +02:00
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
2025-01-09 18:07:02 +02:00
data . result = froca
. loadSearchNote ( noteId )
. then ( ( ) = > {
const note = froca . getNoteFromCache ( noteId ) ;
2021-01-25 23:43:36 +01:00
2025-01-09 18:07:02 +02:00
let childNoteIds = note . getChildNoteIds ( ) ;
2021-01-25 23:43:36 +01:00
2025-01-09 18:07:02 +02:00
if ( note . type === "search" && childNoteIds . length > MAX_SEARCH_RESULTS_IN_TREE ) {
childNoteIds = childNoteIds . slice ( 0 , MAX_SEARCH_RESULTS_IN_TREE ) ;
}
2021-01-25 23:43:36 +01:00
2025-01-09 18:07:02 +02:00
return froca . getNotes ( childNoteIds ) ;
} )
. then ( ( ) = > {
const note = froca . getNoteFromCache ( noteId ) ;
2020-01-12 09:12:13 +01:00
2025-01-09 18:07:02 +02:00
return this . prepareChildren ( note ) ;
} ) ;
} else {
data . result = froca . 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
} ,
2025-03-02 20:47:57 +01:00
enhanceTitle : async function (
event : Event ,
data : {
node : Fancytree.FancytreeNode ;
noteId : string ;
}
) {
2020-08-26 22:12:01 +02:00
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-07-26 21:28:45 +02:00
const note = await froca . getNote ( node . data . noteId , true ) ;
2021-07-26 21:11:51 +02:00
2023-06-05 16:26:05 +02:00
if ( ! note ) {
2021-07-26 21:11:51 +02:00
return ;
}
2021-05-22 12:35:41 +02:00
const activeNoteContext = appContext . tabManager . getActiveContext ( ) ;
2021-02-06 19:58:12 +01:00
2020-08-26 22:12:01 +02:00
const $span = $ ( node . span ) ;
2025-01-09 18:07:02 +02:00
$span . find ( ".tree-item-button" ) . remove ( ) ;
2020-11-27 23:13:48 +01:00
2025-01-09 18:07:02 +02:00
const isHoistedNote = activeNoteContext && activeNoteContext . hoistedNoteId === note . noteId && note . noteId !== "root" ;
2021-02-07 20:55:49 +01:00
2025-01-09 18:07:02 +02:00
if ( note . hasLabel ( "workspace" ) && ! isHoistedNote ) {
const $enterWorkspaceButton = $ ( ` <span class="tree-item-button enter-workspace-button bx bx-door-open" title=" ${ t ( "note_tree.hoist-this-note-workspace" ) } "></span> ` ) . on (
"click" ,
cancelClickPropagation
) ;
2020-11-29 22:32:31 +01:00
$span . append ( $enterWorkspaceButton ) ;
}
2025-01-09 18:07:02 +02:00
if ( note . type === "search" ) {
const $refreshSearchButton = $ ( ` <span class="tree-item-button refresh-search-button bx bx-refresh" title=" ${ t ( "note_tree.refresh-saved-search-results" ) } "></span> ` ) . on (
"click" ,
cancelClickPropagation
) ;
2020-11-27 23:13:48 +01:00
$span . append ( $refreshSearchButton ) ;
}
2025-02-02 18:13:47 +02:00
// TODO: Deduplicate with server's notes.ts#getAndValidateParent
if ( ! [ "search" , "launcher" ] . includes ( note . type )
2025-03-03 21:02:18 +01:00
&& ! note . isOptions ( )
&& ! note . isLaunchBarConfig ( )
&& ! note . noteId . startsWith ( "_help" )
) {
2025-01-09 18:07:02 +02:00
const $createChildNoteButton = $ ( ` <span class="tree-item-button add-note-button bx bx-plus" title=" ${ t ( "note_tree.create-child-note" ) } "></span> ` ) . on (
"click" ,
cancelClickPropagation
) ;
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
}
2023-11-06 22:28:25 +01:00
if ( isHoistedNote ) {
2025-01-09 18:07:02 +02:00
const $unhoistButton = $ ( ` <span class="tree-item-button unhoist-button bx bx-door-open" title=" ${ t ( "note_tree.unhoist" ) } "></span> ` ) . on ( "click" , cancelClickPropagation ) ;
2023-11-06 22:28:25 +01:00
$span . append ( $unhoistButton ) ;
}
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 ( ) ;
}
} ) ;
2022-06-03 17:29:08 +02:00
} ,
2025-01-09 18:07:02 +02:00
select : ( event , { node } ) = > {
if ( hoistedNoteService . getHoistedNoteId ( ) === "root" && node . data . noteId === "_hidden" && node . isSelected ( ) ) {
2023-09-13 16:57:24 +02:00
// hidden is hackily hidden from the tree via CSS when root is hoisted
// make sure it's not selected by mistake, it could be e.g. deleted by mistake otherwise
node . setSelected ( false ) ;
return ;
}
2025-01-09 18:07:02 +02:00
$ ( node . span )
. find ( ".fancytree-custom-icon" )
. attr ( "title" , node . isSelected ( ) ? "Apply bulk actions on selected notes" : "" ) ;
2020-01-12 09:12:13 +01:00
}
} ) ;
2024-12-28 10:57:03 +02:00
const isMobile = utils . isMobile ( ) ;
if ( isMobile ) {
2025-01-28 15:44:15 +02:00
let showTimeout : Timeout ;
2024-12-28 10:57:03 +02:00
this . $tree . on ( "touchstart" , ".fancytree-node" , ( e ) = > {
showTimeout = setTimeout ( ( ) = > {
this . showContextMenu ( e ) ;
2025-01-09 18:07:02 +02:00
} , 300 ) ;
2024-12-28 10:57:03 +02:00
} ) ;
2025-04-10 22:18:12 +02:00
this . $tree . on ( "touchmove" , ".fancytree-node" , ( ) = > {
2024-12-28 10:57:03 +02:00
clearTimeout ( showTimeout ) ;
} ) ;
2025-04-10 22:18:12 +02:00
this . $tree . on ( "touchend" , ".fancytree-node" , ( ) = > {
2024-12-28 10:57:03 +02:00
clearTimeout ( showTimeout ) ;
} ) ;
} else {
2025-01-09 18:07:02 +02:00
this . $tree . on ( "contextmenu" , ".fancytree-node" , ( e ) = > {
2024-12-28 10:57:03 +02:00
this . showContextMenu ( e ) ;
return false ; // blocks default browser right click menu
} ) ;
2025-01-09 18:07:02 +02:00
this . getHotKeys ( ) . then ( ( hotKeys ) = > {
2020-08-26 22:12:01 +02:00
for ( const key in hotKeys ) {
const handler = hotKeys [ key ] ;
2025-01-09 18:07:02 +02:00
$ ( this . tree . $container ) . on ( "keydown" , null , key , ( evt ) = > {
2020-08-26 22:12:01 +02:00
const node = this . tree . getActiveNode ( ) ;
return handler ( node , evt ) ;
// return false from the handler will stop default handling.
} ) ;
}
} ) ;
}
2024-12-28 10:57:03 +02:00
this . tree = $ . ui . fancytree . getTree ( this . $tree ) ;
}
2020-01-12 09:12:13 +01:00
2025-01-28 15:44:15 +02:00
showContextMenu ( e : PointerEvent | JQuery . TouchStartEvent | JQuery . ContextMenuEvent ) {
const node = $ . ui . fancytree . getNode ( e as unknown as Event ) ;
2024-12-28 10:57:03 +02:00
const note = froca . getNoteFromCache ( node . data . noteId ) ;
2020-01-12 09:12:13 +01:00
2024-12-28 10:57:03 +02:00
if ( note . isLaunchBarConfig ( ) ) {
2025-01-09 18:07:02 +02:00
import ( "../menus/launcher_context_menu.js" ) . then ( ( { default : LauncherContextMenu } ) = > {
2024-12-28 10:57:03 +02:00
const launcherContextMenu = new LauncherContextMenu ( this , node ) ;
launcherContextMenu . show ( e ) ;
} ) ;
} else {
2025-01-09 18:07:02 +02:00
import ( "../menus/tree_context_menu.js" ) . then ( ( { default : TreeContextMenu } ) = > {
2024-12-28 10:57:03 +02:00
const treeContextMenu = new TreeContextMenu ( this , node ) ;
treeContextMenu . show ( e ) ;
} ) ;
}
2020-05-02 00:28:40 +02:00
}
2020-08-26 16:50:16 +02:00
prepareRootNode() {
2025-01-28 15:44:15 +02:00
const branch = froca . getBranch ( "none_root" ) ;
return branch && this . prepareNode ( branch ) ;
2020-05-02 00:28:40 +02:00
}
2025-01-28 15:44:15 +02:00
prepareChildren ( parentNote : FNote ) {
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
2025-01-09 18:07:02 +02:00
if ( parentNote . type === "search" && childBranches . length > MAX_SEARCH_RESULTS_IN_TREE ) {
2021-01-25 23:43:36 +01:00
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
2025-01-09 18:07:02 +02:00
if ( note . hasLabel ( "archived" ) ) {
2020-08-24 23:33:27 +02:00
continue ;
}
}
2020-08-26 16:50:16 +02:00
const node = this . prepareNode ( branch ) ;
2024-10-25 20:15:12 +03:00
if ( node ) {
noteList . push ( node ) ;
}
2020-08-24 23:33:27 +02:00
}
return noteList ;
}
2025-01-28 15:44:15 +02:00
async updateNode ( node : Fancytree.FancytreeNode ) {
2021-04-16 22:57:37 +02:00
const note = froca . getNoteFromCache ( node . data . noteId ) ;
const branch = froca . getBranch ( node . data . branchId ) ;
2021-02-17 20:59:44 +01:00
if ( ! note ) {
2023-06-14 22:21:22 +02:00
console . log ( ` Node update not possible because note ' ${ node . data . noteId } ' was not found. ` ) ;
2021-02-17 20:59:44 +01:00
return ;
2023-06-14 22:21:22 +02:00
} else if ( ! branch ) {
console . log ( ` Node update not possible because branch ' ${ node . data . branchId } ' was not found. ` ) ;
2021-02-17 20:59:44 +01:00
return ;
}
2025-01-09 18:07:02 +02:00
const title = ` ${ branch . prefix ? ` ${ branch . prefix } - ` : "" } ${ note . title } ` ;
2020-06-24 22:29:53 +02:00
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 ) {
2025-01-09 18:07:02 +02:00
await node . setExpanded ( branch . isExpanded , { noEvents : true , noAnimation : true } ) ;
2020-06-24 22:29:53 +02:00
}
2021-05-24 14:39:44 +02:00
node . renderTitle ( ) ;
2020-06-24 22:29:53 +02:00
}
2025-01-28 15:44:15 +02:00
prepareNode ( branch : FBranch , forceLazy = false ) {
2020-08-26 16:50:16 +02:00
const note = branch . getNoteFromCache ( ) ;
2020-05-02 00:28:40 +02:00
if ( ! note ) {
2024-10-25 20:15:12 +03:00
console . warn ( ` Branch ' ${ branch . branchId } ' has no child note ' ${ branch . noteId } ' ` ) ;
return null ;
2020-05-02 00:28:40 +02:00
}
2025-01-09 18:07:02 +02:00
const title = ` ${ branch . prefix ? ` ${ branch . prefix } - ` : "" } ${ note . title } ` ;
2020-05-02 00:28:40 +02:00
2021-02-13 20:07:08 +01:00
const isFolder = note . isFolder ( ) ;
2020-05-03 13:59:49 +02:00
2025-01-28 15:44:15 +02:00
const node : Node = {
2020-05-02 00:28:40 +02:00
noteId : note.noteId ,
parentNoteId : branch.parentNoteId ,
branchId : branch.branchId ,
isProtected : note.isProtected ,
noteType : note.type ,
title : utils.escapeHtml ( title ) ,
extraClasses : this.getExtraClasses ( note ) ,
2025-01-28 15:44:15 +02:00
icon : note.getIcon ( ) ,
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 ,
2025-01-09 18:07:02 +02: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 ;
}
2025-01-28 15:44:15 +02:00
getExtraClasses ( note : FNote ) {
2020-05-02 00:28:40 +02:00
utils . assertArguments ( note ) ;
const extraClasses = [ ] ;
if ( note . isProtected ) {
extraClasses . push ( "protected" ) ;
}
2021-12-22 15:01:54 +01:00
if ( note . isShared ( ) ) {
extraClasses . push ( "shared" ) ;
}
2023-07-17 22:26:41 +02:00
if ( note . getParentNoteIds ( ) . length > 1 ) {
2025-01-09 18:07:02 +02:00
const realClones = note
. getParentNoteIds ( )
2025-01-28 15:44:15 +02:00
. map ( ( noteId : string ) = > froca . notes [ noteId ] )
. filter ( ( note : FNote ) = > ! ! note )
. filter ( ( note : FNote ) = > ! [ "_share" , "_lbBookmarks" ] . includes ( note . noteId ) && note . type !== "search" ) ;
2021-09-02 22:27:35 +02:00
2022-12-04 13:16:05 +01:00
if ( realClones . length > 1 ) {
2021-09-02 22:27:35 +02:00
extraClasses . push ( "multiple-parents" ) ;
}
2020-05-02 00:28:40 +02:00
}
const cssClass = note . getCssClass ( ) ;
if ( cssClass ) {
extraClasses . push ( cssClass ) ;
}
extraClasses . push ( utils . getNoteTypeClass ( note . type ) ) ;
2025-01-09 18:07:02 +02:00
if ( note . mime ) {
// some notes should not have mime type (e.g. render)
2020-05-02 00:28:40 +02:00
extraClasses . push ( utils . getMimeTypeClass ( note . mime ) ) ;
}
2025-01-09 18:07:02 +02:00
if ( note . hasLabel ( "archived" ) ) {
2020-05-02 00:28:40 +02:00
extraClasses . push ( "archived" ) ;
}
2022-09-25 14:19:30 +02:00
const colorClass = note . getColorClass ( ) ;
if ( colorClass ) {
extraClasses . push ( colorClass ) ;
}
2020-05-02 00:28:40 +02:00
return extraClasses . join ( " " ) ;
2020-01-12 09:12:13 +01:00
}
2021-10-24 14:53:45 +02:00
/** @returns {FancytreeNode[]} */
2020-01-12 09:12:13 +01:00
getSelectedNodes ( stopOnParents = false ) {
return this . tree . getSelectedNodes ( stopOnParents ) ;
2020-01-11 21:19:56 +01:00
}
2025-01-28 15:44:15 +02:00
getSelectedOrActiveNodes ( node : Fancytree.FancytreeNode | null = 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
2025-01-09 18:07:02 +02:00
if ( node && ! nodes . find ( ( n ) = > n . key === node . key ) ) {
2020-05-31 10:32:35 +02:00
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 ( ) ) ;
}
2023-09-13 16:57:24 +02:00
// hidden subtree is hackily hidden via CSS when hoisted to root
// make sure it's never selected for e.g. deletion in such a case
2025-01-09 18:07:02 +02:00
return nodes . filter ( ( node ) = > hoistedNoteService . getHoistedNoteId ( ) !== "root" || node . data . noteId !== "_hidden" ) ;
2020-01-11 21:19:56 +01:00
}
2025-01-28 15:44:15 +02:00
async setExpandedStatusForSubtree ( node : Fancytree.FancytreeNode | null , isExpanded : boolean ) {
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
}
2025-01-28 15:44:15 +02:00
const { branchIds } = await server . put < ExpandedSubtreeResponse > ( ` branches/ ${ node . data . branchId } /expanded-subtree/ ${ isExpanded ? 1 : 0 } ` ) ;
2020-04-30 23:09:25 +02:00
2025-01-09 18:07:02 +02:00
froca . 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 ) ;
2025-01-09 18:07:02 +02:00
if ( node . data . noteId !== hoistedNoteService . getHoistedNoteId ( ) ) {
// hoisted note should always be expanded
await node . setExpanded ( isExpanded , { noEvents : true , noAnimation : true } ) ;
2020-05-03 13:52:12 +02:00
}
2020-04-29 23:13:05 +02:00
} ) ;
2022-11-24 22:59:09 +01:00
2023-11-06 23:11:57 +01:00
await this . filterHoistedBranch ( true ) ;
2023-02-13 15:09:57 +01:00
2023-04-19 21:24:51 +02:00
// don't activate the active note, see discussion in https://github.com/zadam/trilium/issues/3664
2020-04-29 23:13:05 +02:00
}
2025-01-28 15:44:15 +02:00
async expandTree ( node : Fancytree.FancytreeNode | null = null ) {
2020-05-03 13:15:08 +02:00
await this . setExpandedStatusForSubtree ( node , true ) ;
}
2020-04-29 23:13:05 +02:00
2025-01-28 15:44:15 +02:00
async collapseTree ( node : Fancytree.FancytreeNode | null = null ) {
2020-05-03 13:15:08 +02:00
await this . setExpandedStatusForSubtree ( node , false ) ;
2020-01-11 21:19:56 +01:00
}
2020-01-12 09:12:13 +01:00
2025-01-09 18:07:02 +02:00
collapseTreeEvent() {
this . collapseTree ( ) ;
}
2020-11-27 20:40:32 +01:00
2020-01-12 11:15:23 +01:00
/ * *
2023-01-05 23:38:41 +01:00
* @returns { FancytreeNode | null }
2020-01-12 11:15:23 +01:00
* /
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 )
2023-01-05 23:38:41 +01:00
* @returns { FancytreeNode | null }
2020-01-12 10:35:33 +01:00
* /
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() {
2021-05-22 12:35:41 +02:00
const activeContext = appContext . tabManager . getActiveContext ( ) ;
2020-01-12 11:15:23 +01:00
if ( activeContext && activeContext . notePath ) {
2021-04-27 23:05:28 +02:00
this . tree . $container . focus ( ) ;
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 ) ;
2021-03-17 23:17:54 +01:00
if ( node ) {
2025-01-09 18:07:02 +02:00
await node . makeVisible ( { scrollIntoView : true } ) ;
node . setActive ( true , { noEvents : true , noFocus : false } ) ;
2021-03-17 23:17:54 +01:00
}
2020-01-12 11:15:23 +01:00
}
}
2023-01-31 22:35:01 +01:00
async focusTreeEvent() {
this . tree . $container . focus ( ) ;
this . tree . setFocus ( true ) ;
}
2025-01-28 15:44:15 +02:00
async getNodeFromPath ( notePath : string , 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} */
2025-01-09 18:07:02 +02:00
let parentNode = this . getNodesByNoteId ( "root" ) [ 0 ] ;
2020-01-12 11:15:23 +01:00
2021-03-06 20:31:12 +01:00
let resolvedNotePathSegments = await treeService . resolveNotePathToSegments ( notePath , this . hoistedNoteId , logErrors ) ;
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 ;
}
2021-03-06 20:31:12 +01:00
resolvedNotePathSegments = resolvedNotePathSegments . slice ( 1 ) ;
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 ) {
2023-04-13 10:28:14 +08:00
if ( ! parentNode . isExpanded ( ) ) {
2025-01-09 18:07:02 +02:00
await parentNode . setExpanded ( true , { noAnimation : true } ) ;
2023-04-13 10:28:14 +08:00
}
2020-06-03 11:06:45 +02:00
2023-06-30 11:18:34 +02:00
// although the previous line should set the expanded status, it seems to happen asynchronously,
2020-06-03 11:06:45 +02:00
// so we need to make sure it is set properly before calling updateNode which uses this flag
2021-04-16 22:57:37 +02:00
const branch = froca . getBranch ( parentNode . data . branchId ) ;
2025-01-28 15:44:15 +02:00
if ( branch ) {
branch . isExpanded = true ;
}
2020-01-12 11:15:23 +01:00
}
2023-04-13 10:28:14 +08:00
await this . updateNode ( parentNode ) ;
2020-01-12 11:15:23 +01:00
let foundChildNode = this . findChildNode ( parentNode , childNoteId ) ;
2025-01-09 18:07:02 +02:00
if ( ! foundChildNode ) {
// note might be recently created, so we'll force reload and try again
2020-01-12 11:15:23 +01:00
await parentNode . load ( true ) ;
foundChildNode = this . findChildNode ( parentNode , childNoteId ) ;
if ( ! foundChildNode ) {
2020-06-10 23:43:59 +02:00
if ( logErrors ) {
2023-06-30 11:18:34 +02:00
// besides real errors, this can be also caused by hiding of e.g. included images
2023-01-15 21:04:17 +01:00
// these are real notes with real notePath, user can display them in a detail,
2020-08-30 23:12:49 +02:00
// but they don't have a node in the tree
2021-04-16 22:57:37 +02:00
const childNote = await froca . getNote ( childNoteId ) ;
2021-03-17 23:17:54 +01:00
2025-01-09 18:07:02 +02:00
if ( ! childNote || childNote . type !== "image" ) {
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 } `
) ;
2021-03-17 23:17:54 +01:00
}
2020-06-10 23:43:59 +02:00
}
2020-01-12 11:15:23 +01:00
return ;
}
}
parentNode = foundChildNode ;
}
}
return parentNode ;
}
2025-01-28 15:44:15 +02:00
findChildNode ( parentNode : Fancytree.FancytreeNode , childNoteId : string ) {
2025-01-09 18:07:02 +02:00
return parentNode . getChildren ( ) . find ( ( childNode ) = > childNode . data . noteId === childNoteId ) ;
2020-01-12 11:15:23 +01:00
}
2025-01-28 15:44:15 +02:00
async expandToNote ( notePath : string , logErrors = true ) {
2020-06-10 23:43:59 +02:00
return this . getNodeFromPath ( notePath , true , logErrors ) ;
2020-01-12 11:15:23 +01:00
}
2025-01-28 15:44:15 +02:00
getNodesByBranch ( branch : BranchRow ) {
2021-08-24 22:37:00 +02:00
utils . assertArguments ( branch ) ;
2020-01-12 11:15:23 +01:00
2025-02-24 10:10:34 +02:00
if ( ! branch . noteId ) {
return [ ] ;
}
2025-01-09 18:07:02 +02:00
return this . getNodesByNoteId ( branch . noteId ) . filter ( ( node ) = > node . data . branchId === branch . branchId ) ;
2020-01-12 11:15:23 +01:00
}
2025-01-28 15:44:15 +02:00
getNodesByNoteId ( noteId : string ) {
2020-01-12 11:15:23 +01:00
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() {
2021-05-22 12:26:45 +02:00
return ! ! this . noteContext ;
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 ( ) ;
2025-01-09 18:07:02 +02:00
const newActiveNode =
this . noteContext ? . notePath &&
( ! treeService . isNotePathInHiddenSubtree ( this . noteContext . notePath ) || ( await hoistedNoteService . isHoistedInHiddenSubtree ( ) ) ) &&
( await this . getNodeFromPath ( this . noteContext . notePath ) ) ;
2020-06-04 21:44:34 +02:00
2023-06-18 21:50:22 +02:00
if ( newActiveNode !== oldActiveNode ) {
let oldActiveNodeFocused = false ;
if ( oldActiveNode ) {
oldActiveNodeFocused = oldActiveNode . hasFocus ( ) ;
2020-01-18 18:01:16 +01:00
2023-06-18 21:50:22 +02:00
oldActiveNode . setActive ( false ) ;
oldActiveNode . setFocus ( false ) ;
}
2020-01-18 18:01:16 +01:00
if ( newActiveNode ) {
2025-01-28 15:44:15 +02:00
if ( ! newActiveNode . isVisible ( ) && this . noteContext ? . notePath ) {
2021-05-22 12:26:45 +02:00
await this . expandToNote ( this . noteContext . notePath ) ;
2020-01-18 18:01:16 +01:00
}
2025-01-09 18:07:02 +02:00
newActiveNode . setActive ( true , { noEvents : true , noFocus : ! oldActiveNodeFocused } ) ;
newActiveNode . makeVisible ( { scrollIntoView : true } ) ;
2020-01-18 18:01:16 +01:00
}
}
2020-11-22 23:05:02 +01:00
2023-11-06 23:11:57 +01:00
this . filterHoistedBranch ( false ) ;
2020-01-18 18:01:16 +01:00
}
2025-01-28 15:44:15 +02:00
async refreshSearch ( e : JQuery.MouseDownEvent ) {
const activeNode = $ . ui . fancytree . getNode ( e as unknown as Event ) ;
2020-02-14 20:18:09 +01:00
activeNode . load ( true ) ;
2025-01-09 18:07:02 +02:00
activeNode . setExpanded ( true , { noAnimation : true } ) ;
2020-02-14 20:18:09 +01:00
2024-10-20 02:06:08 +03:00
toastService . showMessage ( t ( "note_tree.saved-search-note-refreshed" ) ) ;
2020-02-14 20:18:09 +01:00
}
2025-01-28 15:44:15 +02:00
async batchUpdate ( cb : ( ) = > Promise < void > ) {
2020-04-29 23:13:05 +02:00
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 ( ) ;
2025-01-09 18:07:02 +02:00
} 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 ( ( ) = > {
2021-03-18 20:11:58 +01:00
if ( ! this . autoCollapseNoteTree ) {
return ;
}
2020-08-17 20:58:34 +02:00
/ *
2023-06-30 11:18:34 +02:00
* We ' re collapsing notes after a period of inactivity to "cleanup" the tree - users rarely
2020-08-17 20:58:34 +02:00
* collapse the notes and the tree becomes unusuably large .
* Some context : https : //github.com/zadam/trilium/issues/1192
* /
const noteIdsToKeepExpanded = new Set (
2025-01-09 18:07:02 +02:00
appContext . tabManager
. getNoteContexts ( )
. map ( ( nc ) = > nc . notePathArray )
2020-08-17 20:58:34 +02:00
. flat ( )
) ;
let noneCollapsedYet = true ;
2025-01-09 18:07:02 +02:00
this . tree . getRootNode ( ) . visit ( ( node ) = > {
2020-08-17 20:58:34 +02:00
if ( node . isExpanded ( ) && ! noteIdsToKeepExpanded . has ( node . data . noteId ) ) {
node . setExpanded ( false ) ;
if ( noneCollapsedYet ) {
2024-10-20 02:06:08 +03:00
toastService . showMessage ( t ( "note_tree.auto-collapsing-notes-after-inactivity" ) ) ;
2020-08-17 20:58:34 +02:00
noneCollapsedYet = false ;
}
}
} , false ) ;
2023-02-13 15:09:57 +01:00
2023-11-06 23:11:57 +01:00
this . filterHoistedBranch ( true ) ;
2020-08-17 20:58:34 +02:00
} , 600 * 1000 ) ;
}
2025-01-28 15:44:15 +02:00
async entitiesReloadedEvent ( { loadResults } : EventData < "entitiesReloaded" > ) {
2020-08-17 20:58:34 +02:00
this . activityDetected ( ) ;
2020-08-16 22:57:48 +02:00
if ( loadResults . isEmptyForTree ( ) ) {
return ;
}
2024-01-28 23:08:44 +01:00
const activeNode = this . getActiveNode ( ) ;
const activeNodeFocused = activeNode ? . hasFocus ( ) ;
const activeNotePath = activeNode ? treeService . getNotePath ( activeNode ) : null ;
2023-11-03 08:58:57 +01:00
2025-01-28 15:44:15 +02:00
const refreshCtx : RefreshContext = {
2023-11-03 08:58:57 +01:00
noteIdsToUpdate : new Set ( ) ,
noteIdsToReload : new Set ( )
} ;
2021-03-08 23:10:34 +01:00
2023-11-03 08:58:57 +01:00
this . # processAttributeRows ( loadResults . getAttributeRows ( ) , refreshCtx ) ;
2020-03-29 22:54:14 +02:00
2025-02-24 10:10:34 +02:00
const branchRows = loadResults . getBranchRows ( ) ;
const { movedActiveNode , parentsOfAddedNodes } = await this . # processBranchRows ( branchRows , refreshCtx ) ;
2023-11-03 08:58:57 +01:00
for ( const noteId of loadResults . getNoteIds ( ) ) {
refreshCtx . noteIdsToUpdate . add ( noteId ) ;
}
await this . # executeTreeUpdates ( refreshCtx , loadResults ) ;
2024-01-28 23:08:44 +01:00
await this . # setActiveNode ( activeNotePath , activeNodeFocused , movedActiveNode , parentsOfAddedNodes ) ;
2023-11-03 08:58:57 +01:00
if ( refreshCtx . noteIdsToReload . size > 0 || refreshCtx . noteIdsToUpdate . size > 0 ) {
// workaround for https://github.com/mar10/fancytree/issues/1054
2023-11-06 23:11:57 +01:00
this . filterHoistedBranch ( true ) ;
2023-11-03 08:58:57 +01:00
}
}
2020-01-26 11:41:40 +01:00
2025-02-24 10:10:34 +02:00
# processAttributeRows ( attributeRows : AttributeRow [ ] , refreshCtx : RefreshContext ) {
2023-11-03 08:58:57 +01:00
for ( const attrRow of attributeRows ) {
2025-01-09 18:07:02 +02:00
const dirtyingLabels = [ "iconClass" , "cssClass" , "workspace" , "workspaceIconClass" , "color" ] ;
2022-10-16 23:11:46 +02:00
2025-02-24 10:10:34 +02:00
if ( attrRow . type === "label" && dirtyingLabels . includes ( attrRow . name ? ? "" ) && attrRow . noteId ) {
2023-11-03 08:58:57 +01:00
if ( attrRow . isInheritable ) {
refreshCtx . noteIdsToReload . add ( attrRow . noteId ) ;
} else {
refreshCtx . noteIdsToUpdate . add ( attrRow . noteId ) ;
2020-01-29 21:38:58 +01:00
}
2025-01-28 15:44:15 +02:00
} else if ( attrRow . type === "label" && attrRow . name === "archived" && attrRow . noteId ) {
2023-11-03 08:58:57 +01:00
const note = froca . getNoteFromCache ( attrRow . noteId ) ;
2022-12-14 19:26:39 +01:00
if ( note ) {
// change of archived status can mean the note should not be displayed in the tree at all
// depending on the value of this.hideArchivedNotes
for ( const parentNote of note . getParentNotes ( ) ) {
2023-11-03 08:58:57 +01:00
refreshCtx . noteIdsToReload . add ( parentNote . noteId ) ;
2022-12-14 19:26:39 +01:00
}
}
2025-02-24 10:10:34 +02:00
} else if ( attrRow . type === "relation" && ( attrRow . name === "template" || attrRow . name === "inherit" ) && attrRow . noteId ) {
2020-01-29 21:38:58 +01:00
// missing handling of things inherited from template
2023-11-03 08:58:57 +01:00
refreshCtx . noteIdsToReload . add ( attrRow . noteId ) ;
2025-01-28 15:44:15 +02:00
} else if ( attrRow . type === "relation" && attrRow . name === "imageLink" && attrRow . noteId ) {
2023-11-03 08:58:57 +01:00
const note = froca . getNoteFromCache ( attrRow . noteId ) ;
2020-04-27 23:27:45 +02:00
2025-01-28 15:44:15 +02:00
if ( note && note . getChildNoteIds ( ) . includes ( attrRow . value ? ? "" ) ) {
2023-07-10 18:20:36 +02:00
// there's a new /deleted imageLink between note and its image child - which can show/hide
2023-06-30 11:18:34 +02:00
// the image (if there is an imageLink relation between parent and child,
2023-01-09 23:15:02 +01:00
// then it is assumed to be "contained" in the note and thus does not have to be displayed in the tree)
2023-11-03 08:58:57 +01:00
refreshCtx . noteIdsToReload . add ( attrRow . noteId ) ;
2020-04-27 23:27:45 +02:00
}
}
2020-01-29 21:38:58 +01:00
}
2023-11-03 08:58:57 +01:00
}
2025-02-24 10:10:34 +02:00
async # processBranchRows ( branchRows : BranchRow [ ] , refreshCtx : RefreshContext ) {
2025-01-09 18:07:02 +02:00
const allBranchesDeleted = branchRows . every ( ( branchRow ) = > ! ! branchRow . isDeleted ) ;
2020-01-29 21:38:58 +01:00
2023-04-13 10:28:14 +08:00
// activeNode is supposed to be moved when we find out activeNode is deleted but not all branches are deleted. save it for fixing activeNodePath after all nodes loaded.
let movedActiveNode = null ;
2025-04-10 22:18:12 +02:00
const parentsOfAddedNodes = [ ] ;
2023-04-13 10:28:14 +08:00
2023-11-03 08:58:57 +01:00
for ( const branchRow of branchRows ) {
2025-02-24 10:10:34 +02:00
if ( branchRow . noteId ) {
if ( branchRow . parentNoteId === "_share" ) {
// all shared notes have a sign in the tree, even the descendants of shared notes
refreshCtx . noteIdsToReload . add ( branchRow . noteId ) ;
} else {
// adding noteId itself to update all potential clones
refreshCtx . noteIdsToUpdate . add ( branchRow . noteId ) ;
}
2021-12-22 16:02:36 +01:00
}
2021-02-25 21:16:21 +01:00
2023-10-19 08:58:04 +02:00
if ( branchRow . isDeleted ) {
for ( const node of this . getNodesByBranch ( branchRow ) ) {
2020-02-10 20:57:56 +01:00
if ( node . isActive ( ) ) {
2023-04-13 10:28:14 +08:00
if ( allBranchesDeleted ) {
2025-01-09 18:07:02 +02:00
const newActiveNode = node . getNextSibling ( ) || node . getPrevSibling ( ) || node . getParent ( ) ;
2020-01-29 21:38:58 +01:00
2023-04-13 10:28:14 +08:00
if ( newActiveNode ) {
2025-01-09 18:07:02 +02:00
newActiveNode . setActive ( true , { noEvents : true , noFocus : true } ) ;
2023-04-13 10:28:14 +08:00
}
} else {
movedActiveNode = node ;
2020-02-12 22:25:52 +01:00
}
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
2025-02-24 10:10:34 +02:00
if ( branchRow . parentNoteId ) {
refreshCtx . noteIdsToUpdate . add ( branchRow . parentNoteId ) ;
}
2020-01-12 12:30:30 +01:00
}
2025-02-24 10:10:34 +02:00
} else if ( branchRow . parentNoteId ) {
2023-06-05 16:26:05 +02:00
for ( const parentNode of this . getNodesByNoteId ( branchRow . parentNoteId ) ) {
2025-01-09 18:07:02 +02:00
parentsOfAddedNodes . push ( parentNode ) ;
2023-04-13 10:28:14 +08:00
2025-02-24 10:10:34 +02:00
if ( ! branchRow . noteId || ( parentNode . isFolder ( ) && ! parentNode . isLoaded ( ) ) ) {
2020-01-29 21:38:58 +01:00
continue ;
}
2023-11-03 01:11:47 +01:00
const note = await froca . getNote ( branchRow . noteId ) ;
2025-01-28 15:44:15 +02:00
const frocaBranch = branchRow . branchId ? froca . getBranch ( branchRow . branchId ) : null ;
2025-01-09 18:07:02 +02:00
const foundNode = ( parentNode . getChildren ( ) || [ ] ) . find ( ( child ) = > child . data . noteId === branchRow . noteId ) ;
2023-11-03 01:11:47 +01:00
if ( foundNode ) {
// the branch already exists in the tree
2025-01-28 15:44:15 +02:00
if ( branchRow . isExpanded !== foundNode . isExpanded ( ) && frocaBranch ) {
2023-11-03 08:58:57 +01:00
refreshCtx . noteIdsToReload . add ( frocaBranch . noteId ) ;
2023-11-03 01:11:47 +01:00
}
2025-01-28 15:44:15 +02:00
} else if ( frocaBranch ) {
2020-08-28 14:29:20 +02:00
// make sure it's loaded
2021-09-23 23:30:30 +02:00
// we're forcing lazy since it's not clear if the whole required subtree is in froca
2025-02-24 10:10:34 +02:00
const newNode = this . prepareNode ( frocaBranch , true ) ;
if ( newNode ) {
parentNode . addChildren ( [ newNode ] ) ;
}
2020-06-05 00:07:45 +02:00
2025-01-28 15:44:15 +02:00
if ( frocaBranch ? . isExpanded && note && note . hasChildren ( ) ) {
2023-11-03 08:58:57 +01:00
refreshCtx . noteIdsToReload . add ( frocaBranch . noteId ) ;
2023-10-19 08:58:04 +02:00
}
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
2025-02-24 10:10:34 +02:00
if ( branchRow . parentNoteId ) {
refreshCtx . noteIdsToUpdate . add ( branchRow . parentNoteId ) ;
}
2020-01-29 21:38:58 +01:00
}
}
}
}
2023-11-03 08:58:57 +01:00
return {
movedActiveNode ,
parentsOfAddedNodes
} ;
}
2020-01-29 21:38:58 +01:00
2025-02-24 10:10:34 +02:00
async # executeTreeUpdates ( refreshCtx : RefreshContext , loadResults : LoadResults ) {
2020-04-29 23:13:05 +02:00
await this . batchUpdate ( async ( ) = > {
2023-11-03 08:58:57 +01:00
for ( const noteId of refreshCtx . 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 ) ;
2023-11-03 08:58:57 +01:00
refreshCtx . 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
2023-06-30 11:18:34 +02:00
// for some reason, node update cannot be in the batchUpdate() block (node is not re-rendered)
2023-11-03 08:58:57 +01:00
for ( const noteId of refreshCtx . noteIdsToUpdate ) {
2020-05-05 18:56:12 +02:00
for ( const node of this . getNodesByNoteId ( noteId ) ) {
2023-04-13 10:28:14 +08:00
await this . updateNode ( node ) ;
}
}
2023-11-03 08:58:57 +01:00
}
2023-04-13 10:28:14 +08:00
2025-02-24 10:10:34 +02:00
async # setActiveNode ( activeNotePath : string | null , activeNodeFocused : boolean , movedActiveNode : Fancytree.FancytreeNode | null , parentsOfAddedNodes : Fancytree.FancytreeNode [ ] ) {
2023-04-13 10:28:14 +08:00
if ( movedActiveNode ) {
for ( const parentNode of parentsOfAddedNodes ) {
2025-01-09 18:07:02 +02:00
const foundNode = ( parentNode . getChildren ( ) || [ ] ) . find ( ( child ) = > child . data . noteId === movedActiveNode . data . noteId ) ;
2023-11-03 08:58:57 +01:00
if ( foundNode ) {
2024-01-28 23:08:44 +01:00
activeNotePath = treeService . getNotePath ( foundNode ) ;
2023-11-03 08:58:57 +01:00
break ;
2023-04-13 10:28:14 +08:00
}
2020-05-05 18:56:12 +02:00
}
}
2024-01-28 23:08:44 +01:00
if ( ! activeNotePath ) {
2023-11-03 08:58:57 +01:00
return ;
}
2025-02-24 10:10:34 +02:00
let node : Fancytree.FancytreeNode | null | undefined = await this . expandToNote ( activeNotePath , false ) ;
2024-01-28 23:08:44 +01:00
if ( node && node . data . noteId !== treeService . getNoteIdFromUrl ( activeNotePath ) ) {
2023-11-03 08:58:57 +01: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
2025-02-24 10:10:34 +02:00
const noteId = treeService . getNoteIdFromUrl ( activeNotePath ) ;
if ( noteId ) {
const notesById = this . getNodesByNoteId ( noteId ) ;
2020-03-29 22:54:14 +02:00
2025-02-24 10:10:34 +02:00
// if there are multiple clones, then we'd rather not activate anyone
node = notesById . length === 1 ? notesById [ 0 ] : null ;
}
2023-11-03 08:58:57 +01:00
}
2020-03-18 10:08:16 +01:00
2024-01-28 23:08:44 +01:00
if ( ! node ) {
return ;
}
2021-04-21 22:03:41 +02:00
2024-01-28 23:08:44 +01:00
if ( activeNodeFocused ) {
// needed by Firefox: https://github.com/zadam/trilium/issues/1865
this . tree . $container . focus ( ) ;
2020-01-12 12:30:30 +01:00
}
2024-01-28 23:08:44 +01:00
2025-01-09 18:07:02 +02:00
await node . setActive ( true , { noEvents : true , noFocus : ! activeNodeFocused } ) ;
2020-01-12 12:30:30 +01:00
}
2020-01-19 18:05:06 +01:00
2025-01-28 15:44:15 +02:00
sortChildren ( node : Fancytree.FancytreeNode ) {
2020-06-05 00:07:45 +02:00
node . sortChildren ( ( nodeA , nodeB ) = > {
2021-04-16 22:57:37 +02:00
const branchA = froca . branches [ nodeA . data . branchId ] ;
const branchB = froca . branches [ nodeB . data . branchId ] ;
2020-06-05 00:07:45 +02:00
if ( ! branchA || ! branchB ) {
return 0 ;
}
return branchA . notePosition - branchB . notePosition ;
} ) ;
}
2025-01-28 15:44:15 +02:00
setExpanded ( branchId : string , isExpanded : boolean ) {
2020-01-24 15:44:24 +01:00
utils . assertArguments ( branchId ) ;
2021-04-16 22:57:37 +02:00
const branch = froca . getBranch ( branchId , true ) ;
2020-10-10 21:08:28 +02:00
if ( ! branch ) {
2025-01-09 18:07:02 +02:00
if ( branchId && branchId . startsWith ( "virt" ) ) {
2020-10-12 21:05:34 +02:00
// in case of virtual branches there's nothing to update
return ;
2025-01-09 18:07:02 +02:00
} else {
2020-10-12 21:05:34 +02:00
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
2023-11-06 23:11:57 +01:00
await this . filterHoistedBranch ( true ) ;
2023-01-13 11:25:58 +01:00
2020-01-24 15:44:24 +01:00
if ( activeNotePath ) {
const node = await this . getNodeFromPath ( activeNotePath , true ) ;
2021-06-05 23:35:47 +02:00
if ( node ) {
2025-01-09 18:07:02 +02:00
await node . setActive ( true , { noEvents : true , noFocus : true } ) ;
2021-06-05 23:35:47 +02:00
}
2020-01-24 15:44:24 +01:00
}
}
2025-01-28 15:44:15 +02:00
async hoistedNoteChangedEvent ( { ntxId } : EventData < "hoistedNoteChanged" > ) {
2021-05-22 12:42:34 +02:00
if ( this . isNoteContext ( ntxId ) ) {
2023-11-06 23:11:57 +01:00
await this . filterHoistedBranch ( true ) ;
2020-11-22 23:05:02 +01:00
}
}
2023-11-06 23:11:57 +01:00
async filterHoistedBranch ( forceUpdate = false ) {
2021-07-21 22:47:52 +02:00
if ( ! this . noteContext ) {
return ;
}
2021-02-28 19:46:04 +01:00
2021-07-21 22:47:52 +02:00
const hoistedNotePath = await treeService . resolveNotePath ( this . noteContext . hoistedNoteId ) ;
2023-11-06 23:11:57 +01:00
if ( ! forceUpdate && this . lastFilteredHoistedNotePath === hoistedNotePath ) {
// no need to re-filter if the hoisting did not change
// (helps with flickering on simple note change with large subtrees)
return ;
}
this . lastFilteredHoistedNotePath = hoistedNotePath ;
2025-02-24 10:10:34 +02:00
if ( hoistedNotePath ) {
await this . getNodeFromPath ( hoistedNotePath ) ;
}
2021-07-21 22:47:52 +02:00
2025-01-09 18:07:02 +02:00
if ( this . noteContext . hoistedNoteId === "root" ) {
2021-07-21 22:47:52 +02:00
this . tree . clearFilter ( ) ;
2022-08-05 16:44:26 +02:00
this . toggleHiddenNode ( false ) ; // show everything but the hidden subtree
2021-07-21 22:47:52 +02:00
} else {
// hack when hoisted note is cloned then it could be filtered multiple times while we want only 1
2025-01-09 18:07:02 +02:00
this . tree . filterBranches (
( node ) = >
2025-01-28 15:44:15 +02:00
node . data . noteId === this . noteContext ? . hoistedNoteId && // optimization to not having always resolve the node path
2025-01-09 18:07:02 +02:00
treeService . getNotePath ( node ) === hoistedNotePath
) ;
2022-08-05 16:44:26 +02:00
this . toggleHiddenNode ( true ) ; // hoisting will handle hidden note visibility
2020-11-22 23:05:02 +01:00
}
2020-01-24 15:44:24 +01:00
}
2025-01-28 15:44:15 +02:00
toggleHiddenNode ( show : boolean ) {
2025-01-09 18:07:02 +02:00
const hiddenNode = this . getNodesByNoteId ( "_hidden" ) [ 0 ] ;
2025-02-24 10:10:34 +02:00
// TODO: Check how .li exists here.
$ ( ( hiddenNode as any ) . li ) . toggleClass ( "hidden-node-is-hidden" , ! show ) ;
2022-08-05 16:44:26 +02:00
}
2025-01-28 15:44:15 +02:00
async frocaReloadedEvent() {
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() {
2025-01-09 18:07:02 +02:00
const actions = await keyboardActionsService . getActionsForScope ( "note-tree" ) ;
2025-01-28 15:44:15 +02:00
const hotKeyMap : Record < string , ( node : Fancytree.FancytreeNode , e : JQuery.KeyDownEvent ) = > boolean > = { } ;
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 ) {
2025-01-09 18:07:02 +02:00
hotKeyMap [ shortcutService . normalizeShortcut ( shortcut ) ] = ( node ) = > {
2020-12-05 23:00:28 +01:00
const notePath = treeService . getNotePath ( node ) ;
2025-01-09 18:07:02 +02:00
this . triggerCommand ( action . actionName , { node , notePath } ) ;
2020-03-17 22:49:43 +01:00
return false ;
2025-01-09 18:07:02 +02:00
} ;
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
}
2025-01-28 15:44:15 +02:00
getSelectedOrActiveBranchIds ( node : Fancytree.FancytreeNode ) {
2020-02-16 22:14:28 +01:00
const nodes = this . getSelectedOrActiveNodes ( node ) ;
2025-01-09 18:07:02 +02:00
return nodes . map ( ( node ) = > node . data . branchId ) ;
2020-02-16 22:14:28 +01:00
}
2025-01-28 15:44:15 +02:00
getSelectedOrActiveNoteIds ( node : Fancytree.FancytreeNode ) : string [ ] {
2022-06-12 13:57:22 +02:00
const nodes = this . getSelectedOrActiveNodes ( node ) ;
2025-01-09 18:07:02 +02:00
return nodes . map ( ( node ) = > node . data . noteId ) ;
2022-06-12 13:57:22 +02:00
}
2025-01-28 15:44:15 +02:00
async deleteNotesCommand ( { node } : CommandListenerData < "deleteNotes" > ) {
2025-01-09 18:07:02 +02:00
const branchIds = this . getSelectedOrActiveBranchIds ( node ) . filter ( ( branchId ) = > ! branchId . startsWith ( "virt-" ) ) ; // search results can't be deleted
2021-02-17 23:03:34 +01:00
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
2025-01-28 15:44:15 +02:00
canBeMovedUpOrDown ( node : Fancytree.FancytreeNode ) {
2025-01-09 18:07:02 +02:00
if ( node . data . noteId === "root" ) {
2021-01-23 21:00:59 +01:00
return false ;
}
2021-04-16 22:57:37 +02:00
const parentNote = froca . getNoteFromCache ( node . getParent ( ) . data . noteId ) ;
2021-01-23 21:00:59 +01:00
2025-01-09 18:07:02 +02:00
return ! parentNote ? . hasLabel ( "sorted" ) ;
2021-01-23 21:00:59 +01:00
}
2025-01-28 15:44:15 +02:00
moveNoteUpCommand ( { node } : CommandListenerData < "moveNoteUp" > ) {
if ( ! node || ! this . canBeMovedUpOrDown ( node ) ) {
2021-01-23 21:00:59 +01:00
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
2025-01-28 15:44:15 +02:00
moveNoteDownCommand ( { node } : CommandListenerData < "moveNoteDown" > ) {
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
2025-01-28 15:44:15 +02:00
moveNoteUpInHierarchyCommand ( { node } : CommandListenerData < "moveNoteUpInHierarchy" > ) {
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
2025-01-28 15:44:15 +02:00
moveNoteDownInHierarchyCommand ( { node } : CommandListenerData < "moveNoteDownInHierarchy" > ) {
2020-02-16 22:14:28 +01:00
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 ) {
2025-01-09 18:07:02 +02:00
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 ) {
2025-01-09 18:07:02 +02:00
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 ) ;
}
}
2025-01-28 15:44:15 +02:00
expandSubtreeCommand ( { node } : CommandListenerData < "expandSubtree" > ) {
2020-04-29 23:13:05 +02:00
this . expandTree ( node ) ;
}
2025-01-28 15:44:15 +02:00
collapseSubtreeCommand ( { node } : CommandListenerData < "collapseSubtree" > ) {
2020-02-16 22:14:28 +01:00
this . collapseTree ( node ) ;
}
2025-01-28 15:44:15 +02:00
async recentChangesInSubtreeCommand ( { node } : CommandListenerData < "recentChangesInSubtree" > ) {
2025-01-09 18:07:02 +02:00
this . triggerCommand ( "showRecentChanges" , { ancestorNoteId : node.data.noteId } ) ;
2020-03-29 19:43:04 +02:00
}
2025-01-28 15:44:15 +02:00
selectAllNotesInParentCommand ( { node } : CommandListenerData < "selectAllNotesInParent" > ) {
2020-02-16 22:14:28 +01:00
for ( const child of node . getParent ( ) . getChildren ( ) ) {
child . setSelected ( true ) ;
}
}
2025-01-28 15:44:15 +02:00
copyNotesToClipboardCommand ( { node } : CommandListenerData < "copyNotesToClipboard" > ) {
2020-02-16 22:14:28 +01:00
clipboard . copy ( this . getSelectedOrActiveBranchIds ( node ) ) ;
}
2025-01-28 15:44:15 +02:00
cutNotesToClipboardCommand ( { node } : CommandListenerData < "cutNotesToClipboard" > ) {
2020-02-16 22:14:28 +01:00
clipboard . cut ( this . getSelectedOrActiveBranchIds ( node ) ) ;
}
2025-01-28 15:44:15 +02:00
pasteNotesFromClipboardCommand ( { node } : CommandListenerData < "pasteNotesFromClipboard" > ) {
2020-05-31 22:33:02 +02:00
clipboard . pasteInto ( node . data . branchId ) ;
2020-02-16 22:14:28 +01:00
}
2025-01-28 15:44:15 +02:00
pasteNotesAfterFromClipboardCommand ( { node } : CommandListenerData < "pasteNotesAfterFromClipboard" > ) {
2020-02-16 22:56:40 +01:00
clipboard . pasteAfter ( node . data . branchId ) ;
}
2025-01-28 15:44:15 +02:00
async exportNoteCommand ( { node } : CommandListenerData < "exportNote" > ) {
2020-02-16 22:56:40 +01:00
const notePath = treeService . getNotePath ( node ) ;
2025-01-09 18:07:02 +02:00
this . triggerCommand ( "showExportDialog" , { notePath , defaultType : "subtree" } ) ;
2020-02-16 22:56:40 +01:00
}
2025-01-28 15:44:15 +02:00
async importIntoNoteCommand ( { node } : CommandListenerData < "importIntoNote" > ) {
2025-01-09 18:07:02 +02:00
this . triggerCommand ( "showImportDialog" , { noteId : node.data.noteId } ) ;
2020-02-16 22:56:40 +01:00
}
2025-01-28 15:44:15 +02:00
editNoteTitleCommand ( { node } : CommandListenerData < "editNoteTitle" > ) {
2025-01-09 18:07:02 +02:00
appContext . triggerCommand ( "focusOnTitle" ) ;
2020-02-16 22:14:28 +01:00
}
2025-01-28 15:44:15 +02:00
protectSubtreeCommand ( { node } : CommandListenerData < "protectSubtree" > ) {
2020-02-26 16:37:17 +01:00
protectedSessionService . protectNote ( node . data . noteId , true , true ) ;
2020-02-16 22:56:40 +01:00
}
2025-01-28 15:44:15 +02:00
unprotectSubtreeCommand ( { node } : CommandListenerData < "unprotectSubtree" > ) {
2020-02-26 16:37:17 +01:00
protectedSessionService . protectNote ( node . data . noteId , false , true ) ;
2020-02-16 22:56:40 +01:00
}
2025-01-28 15:44:15 +02:00
duplicateSubtreeCommand ( { node } : CommandListenerData < "duplicateSubtree" > ) {
2020-09-23 22:45:51 +02:00
const nodesToDuplicate = this . getSelectedOrActiveNodes ( node ) ;
for ( const nodeToDuplicate of nodesToDuplicate ) {
2021-04-16 22:57:37 +02:00
const note = froca . 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 ;
}
2021-04-16 22:57:37 +02:00
const branch = froca . getBranch ( nodeToDuplicate . data . branchId ) ;
2020-09-23 22:45:51 +02:00
2025-01-28 15:44:15 +02:00
if ( branch ? . parentNoteId ) {
noteCreateService . duplicateSubtree ( nodeToDuplicate . data . noteId , branch . parentNoteId ) ;
}
2020-09-23 22:45:51 +02:00
}
2020-02-16 22:56:40 +01:00
}
2022-08-07 13:23:03 +02:00
2025-01-28 15:44:15 +02:00
moveLauncherToVisibleCommand ( { selectedOrActiveBranchIds } : CommandListenerData < "moveLauncherToVisible" > ) {
2025-01-18 18:45:13 +02:00
this . # moveLaunchers ( selectedOrActiveBranchIds , "_lbVisibleLaunchers" , "_lbMobileVisibleLaunchers" ) ;
2022-08-07 13:23:03 +02:00
}
2025-01-28 15:44:15 +02:00
moveLauncherToAvailableCommand ( { selectedOrActiveBranchIds } : CommandListenerData < "moveLauncherToAvailable" > ) {
2025-01-18 18:45:13 +02:00
this . # moveLaunchers ( selectedOrActiveBranchIds , "_lbAvailableLaunchers" , "_lbMobileAvailableLaunchers" ) ;
}
2025-01-28 15:44:15 +02:00
# moveLaunchers ( selectedOrActiveBranchIds : string [ ] , desktopParent : string , mobileParent : string ) {
2025-01-18 18:45:13 +02:00
const desktopLaunchersToMove = selectedOrActiveBranchIds . filter ( ( branchId ) = > ! branchId . startsWith ( "_lbMobile" ) ) ;
if ( desktopLaunchersToMove ) {
branchService . moveToParentNote ( desktopLaunchersToMove , "_lbRoot_" + desktopParent ) ;
}
const mobileLaunchersToMove = selectedOrActiveBranchIds . filter ( ( branchId ) = > branchId . startsWith ( "_lbMobile" ) ) ;
if ( mobileLaunchersToMove ) {
branchService . moveToParentNote ( mobileLaunchersToMove , "_lbMobileRoot_" + mobileParent ) ;
}
2022-08-07 13:23:03 +02:00
}
2025-01-28 15:44:15 +02:00
addNoteLauncherCommand ( { node } : CommandListenerData < "addNoteLauncher" > ) {
2025-01-09 18:07:02 +02:00
this . createLauncherNote ( node , "note" ) ;
2022-08-07 13:23:03 +02:00
}
2025-01-28 15:44:15 +02:00
addScriptLauncherCommand ( { node } : CommandListenerData < "addScriptLauncher" > ) {
2025-01-09 18:07:02 +02:00
this . createLauncherNote ( node , "script" ) ;
2022-08-08 23:13:31 +02:00
}
2025-01-28 15:44:15 +02:00
addWidgetLauncherCommand ( { node } : CommandListenerData < "addWidgetLauncher" > ) {
2025-01-09 18:07:02 +02:00
this . createLauncherNote ( node , "customWidget" ) ;
2022-08-07 13:23:03 +02:00
}
2025-01-28 15:44:15 +02:00
addSpacerLauncherCommand ( { node } : CommandListenerData < "addSpacerLauncher" > ) {
2025-01-09 18:07:02 +02:00
this . createLauncherNote ( node , "spacer" ) ;
2022-08-07 13:23:03 +02:00
}
2025-01-28 15:44:15 +02:00
async createLauncherNote ( node : Fancytree.FancytreeNode , launcherType : LauncherType ) {
const resp = await server . post < CreateLauncherResponse > ( ` special-notes/launchers/ ${ node . data . noteId } / ${ launcherType } ` ) ;
2022-08-07 13:23:03 +02:00
if ( ! resp . success ) {
2023-01-05 15:23:22 +01:00
toastService . showError ( resp . message ) ;
2022-08-07 13:23:03 +02:00
}
2022-08-07 15:34:59 +02:00
await ws . waitForMaxKnownEntityChangeId ( ) ;
2025-03-03 21:02:18 +01:00
appContext . tabManager . getActiveContext ( ) ? . setNote ( resp . note . noteId ) ;
2022-08-07 13:23:03 +02:00
}
2020-05-12 10:52:07 +02:00
}