2018-11-10 00:11:45 +08:00
import contextMenuWidget from './context_menu.js' ;
2018-03-25 23:09:17 +08:00
import dragAndDropSetup from './drag_and_drop.js' ;
2018-03-26 01:41:29 +08:00
import linkService from './link.js' ;
import messagingService from './messaging.js' ;
import noteDetailService from './note_detail.js' ;
2018-03-26 09:16:57 +08:00
import protectedSessionHolder from './protected_session_holder.js' ;
2018-03-25 23:09:17 +08:00
import treeUtils from './tree_utils.js' ;
import utils from './utils.js' ;
import server from './server.js' ;
2018-03-26 00:29:00 +08:00
import treeCache from './tree_cache.js' ;
2018-03-26 09:29:35 +08:00
import infoService from "./info.js" ;
2018-03-27 11:18:50 +08:00
import treeBuilder from "./tree_builder.js" ;
2018-03-27 11:25:54 +08:00
import treeKeyBindings from "./tree_keybindings.js" ;
2018-03-26 11:25:17 +08:00
import Branch from '../entities/branch.js' ;
2018-04-01 10:23:40 +08:00
import NoteShort from '../entities/note_short.js' ;
2018-12-13 03:39:56 +08:00
import hoistedNoteService from '../services/hoisted_note.js' ;
2018-12-14 06:28:48 +08:00
import confirmDialog from "../dialogs/confirm.js" ;
2019-05-11 03:43:40 +08:00
import optionsInit from "../services/options_init.js" ;
2019-05-04 02:27:38 +08:00
import TreeContextMenu from "./tree_context_menu.js" ;
2017-11-22 09:04:06 +08:00
2018-03-25 23:09:17 +08:00
const $tree = $ ( "#tree" ) ;
const $createTopLevelNoteButton = $ ( "#create-top-level-note-button" ) ;
const $collapseTreeButton = $ ( "#collapse-tree-button" ) ;
2019-03-21 05:28:54 +08:00
const $scrollToActiveNoteButton = $ ( "#scroll-to-active-note-button" ) ;
2018-02-25 10:23:04 +08:00
2018-11-01 06:55:14 +08:00
// focused & not active node can happen during multiselection where the node is selected but not activated
// (its content is not displayed in the detail)
function getFocusedNode ( ) {
const tree = $tree . fancytree ( "getTree" ) ;
return tree . getFocusNode ( ) ;
}
2018-03-25 23:09:17 +08:00
// note that if you want to access data like noteId or isProtected, you need to go into "data" property
2019-03-21 05:28:54 +08:00
function getActiveNode ( ) {
2018-03-25 23:09:17 +08:00
return $tree . fancytree ( "getActiveNode" ) ;
}
2017-11-24 09:12:39 +08:00
2018-03-26 02:49:20 +08:00
async function getNodesByBranchId ( branchId ) {
2018-03-25 23:09:17 +08:00
utils . assertArguments ( branchId ) ;
2017-11-28 23:17:30 +08:00
2018-03-26 02:49:20 +08:00
const branch = await treeCache . getBranch ( branchId ) ;
2017-12-24 00:02:38 +08:00
2018-03-25 23:09:17 +08:00
return getNodesByNoteId ( branch . noteId ) . filter ( node => node . data . branchId === branchId ) ;
}
2017-11-24 09:12:39 +08:00
2018-03-25 23:09:17 +08:00
function getNodesByNoteId ( noteId ) {
utils . assertArguments ( noteId ) ;
2017-12-24 00:02:38 +08:00
2018-03-25 23:09:17 +08:00
const list = getTree ( ) . getNodesByRef ( noteId ) ;
return list ? list : [ ] ; // if no nodes with this refKey are found, fancy tree returns null
}
2017-11-28 23:17:30 +08:00
2018-03-26 02:49:20 +08:00
async function setPrefix ( branchId , prefix ) {
2018-03-25 23:09:17 +08:00
utils . assertArguments ( branchId ) ;
2017-11-28 23:17:30 +08:00
2018-03-26 02:49:20 +08:00
const branch = await treeCache . getBranch ( branchId ) ;
2018-03-13 11:14:09 +08:00
2018-03-26 02:49:20 +08:00
branch . prefix = prefix ;
for ( const node of await getNodesByBranchId ( branchId ) ) {
await setNodeTitleWithPrefix ( node ) ;
}
2018-03-25 23:09:17 +08:00
}
2018-03-13 11:14:09 +08:00
2018-03-26 02:49:20 +08:00
async function setNodeTitleWithPrefix ( node ) {
2018-03-27 09:50:47 +08:00
const noteTitle = await treeUtils . getNoteTitle ( node . data . noteId ) ;
2018-03-26 02:49:20 +08:00
const branch = await treeCache . getBranch ( node . data . branchId ) ;
2018-03-13 11:14:09 +08:00
2018-03-25 23:09:17 +08:00
const prefix = branch . prefix ;
2017-11-28 23:17:30 +08:00
2018-03-25 23:09:17 +08:00
const title = ( prefix ? ( prefix + " - " ) : "" ) + noteTitle ;
2017-12-24 00:02:38 +08:00
2018-03-25 23:09:17 +08:00
node . setTitle ( utils . escapeHtml ( title ) ) ;
}
2018-03-25 22:06:14 +08:00
2019-05-10 03:30:08 +08:00
async function expandToNote ( notePath , expandOpts ) {
return await getNodeFromPath ( notePath , true , expandOpts ) ;
2019-01-02 01:27:36 +08:00
}
2019-05-20 00:21:29 +08:00
function findChildNode ( parentNode , childNoteId ) {
let foundChildNode = null ;
for ( const childNode of parentNode . getChildren ( ) ) {
if ( childNode . data . noteId === childNoteId ) {
foundChildNode = childNode ;
break ;
}
}
return foundChildNode ;
}
2019-05-10 03:30:08 +08:00
async function getNodeFromPath ( notePath , expand = false , expandOpts = { } ) {
2018-03-25 23:09:17 +08:00
utils . assertArguments ( notePath ) ;
2017-12-24 01:19:15 +08:00
2019-01-11 02:53:42 +08:00
const hoistedNoteId = await hoistedNoteService . getHoistedNoteId ( ) ;
2019-05-10 03:30:08 +08:00
let parentNode = null ;
2017-12-04 06:46:56 +08:00
2019-05-10 03:30:08 +08:00
for ( const childNoteId of await getRunPath ( notePath ) ) {
2019-01-11 02:53:42 +08:00
if ( childNoteId === hoistedNoteId ) {
2019-05-10 03:30:08 +08:00
// there must be exactly one node with given hoistedNoteId
parentNode = getNodesByNoteId ( childNoteId ) [ 0 ] ;
continue ;
2019-01-11 02:53:42 +08:00
}
2019-01-02 01:27:36 +08:00
2019-01-11 02:53:42 +08:00
// we expand only after hoisted note since before then nodes are not actually present in the tree
2019-05-10 03:30:08 +08:00
if ( parentNode ) {
if ( ! parentNode . isLoaded ( ) ) {
await parentNode . load ( ) ;
}
2017-12-04 06:46:56 +08:00
2019-05-10 03:30:08 +08:00
if ( expand ) {
parentNode . setExpanded ( true , expandOpts ) ;
2019-01-11 02:53:42 +08:00
}
2018-05-28 00:26:34 +08:00
2019-05-20 00:21:29 +08:00
let foundChildNode = findChildNode ( parentNode , childNoteId ) ;
2019-05-10 03:30:08 +08:00
2019-05-20 00:21:29 +08:00
if ( ! foundChildNode ) { // note might be recently created so we'll force reload and try again
await parentNode . load ( true ) ;
2019-01-11 02:53:42 +08:00
2019-05-20 00:21:29 +08:00
foundChildNode = findChildNode ( parentNode , childNoteId ) ;
if ( ! foundChildNode ) {
messagingService . logError ( ` Can't find node for child node of noteId= ${ childNoteId } for parent of noteId= ${ parentNode . data . noteId } and hoistedNoteId= ${ hoistedNoteId } , requested path is ${ notePath } ` ) ;
return ;
}
2019-01-11 02:53:42 +08:00
}
2018-03-25 23:09:17 +08:00
2019-05-10 03:30:08 +08:00
parentNode = foundChildNode ;
}
2018-01-16 09:54:22 +08:00
}
2019-05-10 03:30:08 +08:00
return parentNode ;
2018-03-25 23:09:17 +08:00
}
2018-01-16 09:54:22 +08:00
2019-01-02 02:32:34 +08:00
async function activateNote ( notePath , noteLoadedListener ) {
2018-03-25 23:09:17 +08:00
utils . assertArguments ( notePath ) ;
2018-01-16 09:54:22 +08:00
2019-01-11 02:53:42 +08:00
// notePath argument can contain only noteId which is not good when hoisted since
// then we need to check the whole note path
const runNotePath = await getRunPath ( notePath ) ;
2019-05-15 04:29:47 +08:00
if ( ! runNotePath ) {
console . log ( "Cannot activate " + notePath ) ;
return ;
}
2018-12-14 06:28:48 +08:00
const hoistedNoteId = await hoistedNoteService . getHoistedNoteId ( ) ;
2019-01-11 02:53:42 +08:00
if ( hoistedNoteId !== 'root' && ! runNotePath . includes ( hoistedNoteId ) ) {
2018-12-14 06:28:48 +08:00
if ( ! await confirmDialog . confirm ( "Requested note is outside of hoisted note subtree. Do you want to unhoist?" ) ) {
return ;
}
// unhoist so we can activate the note
2018-12-16 03:29:08 +08:00
await hoistedNoteService . unhoist ( ) ;
2018-12-14 06:28:48 +08:00
}
2018-11-14 18:17:20 +08:00
if ( glob . activeDialog ) {
glob . activeDialog . modal ( 'hide' ) ;
}
2019-04-15 01:43:44 +08:00
const node = await expandToNote ( notePath ) ;
2018-01-16 09:54:22 +08:00
2019-01-02 02:32:34 +08:00
if ( noteLoadedListener ) {
noteDetailService . addDetailLoadedListener ( node . data . noteId , noteLoadedListener ) ;
2018-08-14 19:50:04 +08:00
}
2018-08-13 14:42:37 +08:00
// we use noFocus because when we reload the tree because of background changes
// we don't want the reload event to steal focus from whatever was focused before
await node . setActive ( true , { noFocus : true } ) ;
2018-01-02 07:53:52 +08:00
2018-03-25 23:09:17 +08:00
clearSelectedNodes ( ) ;
2018-08-13 14:42:37 +08:00
return node ;
2018-03-25 23:09:17 +08:00
}
2017-12-04 06:46:56 +08:00
2019-05-12 03:27:27 +08:00
/ * *
* Accepts notePath which might or might not be valid and returns an existing path as close to the original
* notePath as possible .
* /
async function resolveNotePath ( notePath ) {
const runPath = await getRunPath ( notePath ) ;
2019-05-15 04:29:47 +08:00
return runPath ? runPath . join ( "/" ) : null ;
2019-05-12 03:27:27 +08:00
}
2018-03-25 23:09:17 +08:00
/ * *
* Accepts notePath and tries to resolve it . Part of the path might not be valid because of note moving ( which causes
* path change ) or other corruption , in that case this will try to get some other valid path to the correct note .
* /
async function getRunPath ( notePath ) {
utils . assertArguments ( notePath ) ;
2017-12-24 00:02:38 +08:00
2018-03-25 23:09:17 +08:00
const path = notePath . split ( "/" ) . reverse ( ) ;
2018-05-27 04:16:34 +08:00
if ( ! path . includes ( "root" ) ) {
path . push ( 'root' ) ;
}
2017-11-19 21:47:22 +08:00
2018-12-13 03:39:56 +08:00
const hoistedNoteId = await hoistedNoteService . getHoistedNoteId ( ) ;
2018-03-25 23:09:17 +08:00
const effectivePath = [ ] ;
let childNoteId = null ;
let i = 0 ;
2017-11-20 07:16:50 +08:00
2018-03-25 23:09:17 +08:00
while ( true ) {
if ( i >= path . length ) {
break ;
}
2017-11-30 13:02:32 +08:00
2018-03-25 23:09:17 +08:00
const parentNoteId = path [ i ++ ] ;
2017-11-20 00:28:46 +08:00
2018-03-25 23:09:17 +08:00
if ( childNoteId !== null ) {
2018-03-26 02:49:20 +08:00
const child = await treeCache . getNote ( childNoteId ) ;
2018-11-13 06:34:22 +08:00
if ( ! child ) {
2019-05-15 04:29:47 +08:00
console . log ( "Can't find note " + childNoteId ) ;
return ;
2018-11-13 06:34:22 +08:00
}
2018-03-25 23:09:17 +08:00
const parents = await child . getParentNotes ( ) ;
2017-11-20 00:28:46 +08:00
2018-03-25 23:09:17 +08:00
if ( ! parents ) {
2018-03-26 01:41:29 +08:00
messagingService . logError ( "No parents found for " + childNoteId ) ;
2018-03-25 23:09:17 +08:00
return ;
}
2017-11-27 10:00:42 +08:00
2018-03-25 23:09:17 +08:00
if ( ! parents . some ( p => p . noteId === parentNoteId ) ) {
2018-11-14 18:17:20 +08:00
console . debug ( utils . now ( ) , "Did not find parent " + parentNoteId + " for child " + childNoteId ) ;
2017-11-20 00:28:46 +08:00
2018-03-25 23:09:17 +08:00
if ( parents . length > 0 ) {
2018-11-14 18:17:20 +08:00
console . debug ( utils . now ( ) , "Available parents:" , parents ) ;
2017-12-24 00:02:38 +08:00
2018-03-25 23:09:17 +08:00
const someNotePath = await getSomeNotePath ( parents [ 0 ] ) ;
2017-12-24 01:19:15 +08:00
2018-03-25 23:09:17 +08:00
if ( someNotePath ) { // in case it's root the path may be empty
const pathToRoot = someNotePath . split ( "/" ) . reverse ( ) ;
2017-11-20 07:16:50 +08:00
2018-03-25 23:09:17 +08:00
for ( const noteId of pathToRoot ) {
effectivePath . push ( noteId ) ;
2017-11-30 13:02:32 +08:00
}
2018-05-28 00:26:34 +08:00
effectivePath . push ( 'root' ) ;
2017-11-20 00:28:46 +08:00
}
2017-11-19 21:47:22 +08:00
2018-03-25 23:09:17 +08:00
break ;
}
else {
2018-03-26 01:41:29 +08:00
messagingService . logError ( "No parents, can't activate node." ) ;
2018-03-25 23:09:17 +08:00
return ;
}
2017-11-19 21:47:22 +08:00
}
}
2017-12-04 06:46:56 +08:00
2018-12-13 03:39:56 +08:00
effectivePath . push ( parentNoteId ) ;
childNoteId = parentNoteId ;
if ( parentNoteId === hoistedNoteId ) {
2018-03-25 23:09:17 +08:00
break ;
}
2017-11-19 21:47:22 +08:00
}
2018-03-25 23:09:17 +08:00
return effectivePath . reverse ( ) ;
}
2017-12-24 00:02:38 +08:00
2018-03-25 23:09:17 +08:00
async function getSomeNotePath ( note ) {
utils . assertArguments ( note ) ;
2017-11-22 09:04:06 +08:00
2018-03-25 23:09:17 +08:00
const path = [ ] ;
2017-11-22 09:04:06 +08:00
2018-03-25 23:09:17 +08:00
let cur = note ;
2017-11-22 09:04:06 +08:00
2018-03-25 23:09:17 +08:00
while ( cur . noteId !== 'root' ) {
path . push ( cur . noteId ) ;
2018-03-25 11:00:12 +08:00
2018-03-25 23:09:17 +08:00
const parents = await cur . getParentNotes ( ) ;
2017-12-20 10:40:48 +08:00
2018-03-25 23:09:17 +08:00
if ( ! parents . length ) {
2019-05-20 03:22:35 +08:00
infoService . throwError ( ` Can't find parents for note ${ cur . noteId } ` ) ;
2019-05-20 00:21:29 +08:00
return ;
2017-11-22 09:04:06 +08:00
}
2018-03-25 23:09:17 +08:00
cur = parents [ 0 ] ;
2017-11-22 09:04:06 +08:00
}
2018-03-25 23:09:17 +08:00
return path . reverse ( ) . join ( '/' ) ;
}
2017-11-05 07:28:49 +08:00
2018-03-25 23:09:17 +08:00
async function setExpandedToServer ( branchId , isExpanded ) {
utils . assertArguments ( branchId ) ;
2017-11-05 07:28:49 +08:00
2018-03-25 23:09:17 +08:00
const expandedNum = isExpanded ? 1 : 0 ;
2017-12-24 00:02:38 +08:00
2018-04-02 08:33:10 +08:00
await server . put ( 'branches/' + branchId + '/expanded/' + expandedNum ) ;
2018-03-25 23:09:17 +08:00
}
2017-11-20 07:16:50 +08:00
2018-03-25 23:09:17 +08:00
function getSelectedNodes ( stopOnParents = false ) {
return getTree ( ) . getSelectedNodes ( stopOnParents ) ;
}
2018-01-02 07:53:52 +08:00
2018-03-25 23:09:17 +08:00
function clearSelectedNodes ( ) {
for ( const selectedNode of getSelectedNodes ( ) ) {
selectedNode . setSelected ( false ) ;
2018-01-02 06:59:59 +08:00
}
2019-03-21 05:28:54 +08:00
const currentNode = getActiveNode ( ) ;
2017-12-24 00:02:38 +08:00
2018-03-25 23:09:17 +08:00
if ( currentNode ) {
currentNode . setSelected ( true ) ;
}
}
2017-11-05 07:28:49 +08:00
2018-03-26 02:49:20 +08:00
async function treeInitialized ( ) {
2019-05-12 03:27:27 +08:00
if ( noteDetailService . getTabContexts ( ) . length > 0 ) {
// this is just tree reload - tabs are already in place
return ;
}
2019-05-11 03:43:40 +08:00
let openTabs = [ ] ;
try {
const options = await optionsInit . optionsReady ;
openTabs = JSON . parse ( options . openTabs ) ;
2018-12-31 05:36:39 +08:00
}
2019-05-11 03:43:40 +08:00
catch ( e ) {
messagingService . logError ( "Cannot retrieve open tabs: " + e . stack ) ;
}
const filteredTabs = [ ] ;
2018-12-31 05:36:39 +08:00
2019-05-11 03:43:40 +08:00
for ( const openTab of openTabs ) {
const noteId = treeUtils . getNoteIdFromNotePath ( openTab . notePath ) ;
2018-03-26 02:49:20 +08:00
2019-05-11 03:43:40 +08:00
if ( await treeCache . noteExists ( noteId ) ) {
// note doesn't exist so don't try to open tab for it
filteredTabs . push ( openTab ) ;
}
2018-03-26 02:49:20 +08:00
}
2019-05-11 03:43:40 +08:00
if ( filteredTabs . length === 0 ) {
filteredTabs . push ( {
notePath : 'root' ,
active : true
} ) ;
}
2019-04-14 04:10:16 +08:00
2019-05-15 04:29:47 +08:00
if ( ! filteredTabs . find ( tab => tab . active ) ) {
filteredTabs [ 0 ] . active = true ;
}
2019-05-11 03:43:40 +08:00
for ( const tab of filteredTabs ) {
await noteDetailService . loadNoteDetail ( tab . notePath , {
2019-05-15 04:29:47 +08:00
tabId : tab . tabId ,
2019-05-11 03:43:40 +08:00
newTab : true ,
activate : tab . active
} ) ;
2018-03-26 02:49:20 +08:00
}
2019-05-11 03:43:40 +08:00
// previous opening triggered task to save tab changes but these are bogus changes (this is init)
// so we'll cancel it
noteDetailService . clearOpenTabsTask ( ) ;
2018-03-26 02:49:20 +08:00
}
2019-04-21 03:14:48 +08:00
let ignoreNextActivationNoteId = null ;
2018-04-17 08:40:18 +08:00
function initFancyTree ( tree ) {
utils . assertArguments ( tree ) ;
2018-01-02 07:29:06 +08:00
2018-03-25 23:09:17 +08:00
$tree . fancytree ( {
autoScroll : true ,
keyboard : false , // we takover keyboard handling in the hotkeys plugin
2018-12-24 17:10:36 +08:00
extensions : [ "hotkeys" , "dnd5" , "clones" ] ,
2018-04-17 08:40:18 +08:00
source : tree ,
2018-04-08 21:25:35 +08:00
scrollParent : $tree ,
2018-05-27 04:16:34 +08:00
minExpandLevel : 2 , // root can't be collapsed
2018-03-25 23:09:17 +08:00
click : ( event , data ) => {
const targetType = data . targetType ;
const node = data . node ;
if ( targetType === 'title' || targetType === 'icon' ) {
2019-05-09 01:10:45 +08:00
if ( event . shiftKey ) {
node . setSelected ( ! node . isSelected ( ) ) ;
}
else if ( event . ctrlKey ) {
2019-05-11 03:43:40 +08:00
noteDetailService . loadNoteDetail ( node . data . noteId , { newTab : true } ) ;
2019-05-09 01:10:45 +08:00
}
else {
2018-03-25 23:09:17 +08:00
node . setActive ( ) ;
node . setSelected ( true ) ;
clearSelectedNodes ( ) ;
}
2017-12-26 10:47:32 +08:00
return false ;
2018-03-25 23:09:17 +08:00
}
} ,
2019-04-21 03:14:48 +08:00
beforeActivate : ( event , data ) => {
// this is for the case when tree reload has been called and we don't want to
if ( ignoreNextActivationNoteId && getActiveNode ( ) !== null ) {
2019-05-29 02:22:16 +08:00
ignoreNextActivationNoteId = null ;
2019-04-21 03:14:48 +08:00
return false ;
}
} ,
2019-04-17 03:40:04 +08:00
activate : async ( event , data ) => {
2018-11-01 02:08:31 +08:00
const node = data . node ;
const noteId = node . data . noteId ;
2018-03-25 23:09:17 +08:00
2019-04-21 03:14:48 +08:00
if ( ignoreNextActivationNoteId === noteId ) {
ignoreNextActivationNoteId = null ;
return ;
}
2019-03-29 03:54:17 +08:00
// click event won't propagate so let's close context menu manually
contextMenuWidget . hideContextMenu ( ) ;
2019-05-09 02:14:41 +08:00
const notePath = await treeUtils . getNotePath ( node ) ;
noteDetailService . switchToNote ( notePath ) ;
2018-03-25 23:09:17 +08:00
} ,
2018-03-27 11:25:54 +08:00
expand : ( event , data ) => setExpandedToServer ( data . node . data . branchId , true ) ,
collapse : ( event , data ) => setExpandedToServer ( data . node . data . branchId , false ) ,
2018-03-27 11:48:45 +08:00
init : ( event , data ) => treeInitialized ( ) , // don't collapse to short form
2018-03-25 23:09:17 +08:00
hotkeys : {
2018-03-27 11:25:54 +08:00
keydown : treeKeyBindings
2018-03-25 23:09:17 +08:00
} ,
2018-11-06 04:42:29 +08:00
dnd5 : dragAndDropSetup ,
2018-03-26 07:49:33 +08:00
lazyLoad : function ( event , data ) {
2018-03-25 23:09:17 +08:00
const noteId = data . node . data . noteId ;
2018-08-15 04:50:05 +08:00
2018-03-27 11:18:50 +08:00
data . result = treeCache . getNote ( noteId ) . then ( note => treeBuilder . prepareBranch ( note ) ) ;
2018-03-25 23:09:17 +08:00
} ,
clones : {
highlightActiveClones : true
2018-12-16 03:29:08 +08:00
} ,
2019-01-05 03:18:07 +08:00
enhanceTitle : async function ( event , data ) {
2018-12-16 03:29:08 +08:00
const node = data . node ;
2019-01-05 03:18:07 +08:00
const $span = $ ( node . span ) ;
if ( node . data . noteId !== 'root'
&& node . data . noteId === await hoistedNoteService . getHoistedNoteId ( )
&& $span . find ( '.unhoist-button' ) . length === 0 ) {
2018-12-16 03:29:08 +08:00
const unhoistButton = $ ( '<span> (<a class="unhoist-button">unhoist</a>)</span>' ) ;
2019-01-05 03:18:07 +08:00
$span . append ( unhoistButton ) ;
2018-12-16 03:29:08 +08:00
}
2019-03-30 07:12:32 +08:00
const note = await treeCache . getNote ( node . data . noteId ) ;
if ( note . type === 'search' && $span . find ( '.refresh-search-button' ) . length === 0 ) {
const refreshSearchButton = $ ( '<span> <span class="refresh-search-button jam jam-refresh" title="Refresh saved search results"></span></span>' ) ;
$span . append ( refreshSearchButton ) ;
}
2019-01-09 06:32:03 +08:00
} ,
// this is done to automatically lazy load all expanded search notes after tree load
loadChildren : function ( event , data ) {
data . node . visit ( function ( subNode ) {
// Load all lazy/unloaded child nodes
// (which will trigger `loadChildren` recursively)
if ( subNode . isUndefined ( ) && subNode . isExpanded ( ) ) {
subNode . load ( ) ;
}
} ) ;
2018-03-25 23:09:17 +08:00
}
} ) ;
2017-11-05 07:28:49 +08:00
2018-11-06 19:46:29 +08:00
$tree . on ( 'contextmenu' , '.fancytree-node' , function ( e ) {
2019-05-04 02:27:38 +08:00
const node = $ . ui . fancytree . getNode ( e ) ;
// right click resets selection to just this node
// this is important when e.g. you right click on a note while having different note active
// and then click on delete - obviously you want to delete only that one right-clicked
node . setSelected ( true ) ;
clearSelectedNodes ( ) ;
contextMenuWidget . initContextMenu ( e , new TreeContextMenu ( node ) ) ;
2018-11-06 19:46:29 +08:00
return false ; // blocks default browser right click menu
} ) ;
2018-03-25 23:09:17 +08:00
}
2018-01-02 06:59:59 +08:00
2018-03-25 23:09:17 +08:00
function getTree ( ) {
return $tree . fancytree ( 'getTree' ) ;
}
2018-03-13 11:14:09 +08:00
2018-03-25 23:09:17 +08:00
async function reload ( ) {
const notes = await loadTree ( ) ;
2018-03-13 11:14:09 +08:00
2019-04-21 03:14:48 +08:00
// make sure the reload won't trigger reactivation. This is important especially in cases where we wait for the reload
// to finish to then activate some other note. But since the activate() event is called asynchronously, it may be called
// (or finished calling) after we switched to a different note.
2019-05-12 01:27:33 +08:00
if ( getActiveNode ( ) ) {
ignoreNextActivationNoteId = getActiveNode ( ) . data . noteId ;
}
2019-04-21 03:14:48 +08:00
2018-03-25 23:09:17 +08:00
await getTree ( ) . reload ( notes ) ;
}
2018-03-13 11:27:21 +08:00
2019-01-26 05:18:34 +08:00
function isNotePathInAddress ( ) {
2019-05-15 04:29:47 +08:00
const [ notePath , tabId ] = getHashValueFromAddress ( ) ;
return notePath . startsWith ( "root" )
// empty string is for empty/uninitialized tab
|| ( notePath === '' && ! ! tabId ) ;
2019-01-26 05:18:34 +08:00
}
function getHashValueFromAddress ( ) {
2019-05-15 04:29:47 +08:00
const str = document . location . hash ? document . location . hash . substr ( 1 ) : "" ; // strip initial #
return str . split ( "-" ) ;
2018-03-25 23:09:17 +08:00
}
2018-03-13 11:27:21 +08:00
2019-03-19 06:03:41 +08:00
async function loadTreeCache ( ) {
2018-03-25 23:09:17 +08:00
const resp = await server . get ( 'tree' ) ;
2017-11-23 08:58:56 +08:00
2019-03-19 06:03:41 +08:00
treeCache . load ( resp . notes , resp . branches , resp . relations ) ;
}
async function loadTree ( ) {
await loadTreeCache ( ) ;
return await treeBuilder . prepareTree ( ) ;
2018-03-25 23:09:17 +08:00
}
2017-11-05 22:52:28 +08:00
2019-01-16 03:30:54 +08:00
async function collapseTree ( node = null ) {
2018-03-25 23:09:17 +08:00
if ( ! node ) {
2019-01-16 03:30:54 +08:00
const hoistedNoteId = await hoistedNoteService . getHoistedNoteId ( ) ;
node = getNodesByNoteId ( hoistedNoteId ) [ 0 ] ;
2017-12-20 08:31:02 +08:00
}
2018-03-25 23:09:17 +08:00
node . setExpanded ( false ) ;
2017-11-05 07:28:49 +08:00
2018-03-25 23:09:17 +08:00
node . visit ( node => node . setExpanded ( false ) ) ;
}
2017-11-05 07:28:49 +08:00
2019-05-10 03:30:08 +08:00
async function scrollToActiveNote ( ) {
2019-05-12 03:27:27 +08:00
const activeContext = noteDetailService . getActiveTabContext ( ) ;
2019-05-10 03:30:08 +08:00
if ( activeContext ) {
const node = await expandToNote ( activeContext . notePath ) ;
2017-12-26 23:00:08 +08:00
2018-03-25 23:09:17 +08:00
node . makeVisible ( { scrollIntoView : true } ) ;
2017-12-26 23:00:08 +08:00
2018-03-25 23:09:17 +08:00
node . setFocus ( ) ;
2019-05-10 03:30:08 +08:00
await activateNote ( activeContext . notePath ) ;
2017-11-05 07:28:49 +08:00
}
2018-03-25 23:09:17 +08:00
}
2017-11-05 07:28:49 +08:00
2018-03-25 23:09:17 +08:00
function setBranchBackgroundBasedOnProtectedStatus ( noteId ) {
2018-08-17 21:21:59 +08:00
getNodesByNoteId ( noteId ) . map ( node => node . toggleClass ( "protected" , node . data . isProtected ) ) ;
2018-03-25 23:09:17 +08:00
}
2017-11-05 07:28:49 +08:00
2018-03-25 23:09:17 +08:00
function setProtected ( noteId , isProtected ) {
getNodesByNoteId ( noteId ) . map ( node => node . data . isProtected = isProtected ) ;
2017-11-05 07:28:49 +08:00
2018-03-25 23:09:17 +08:00
setBranchBackgroundBasedOnProtectedStatus ( noteId ) ;
}
2017-11-05 07:28:49 +08:00
2018-03-26 02:49:20 +08:00
async function setNoteTitle ( noteId , title ) {
2018-03-25 23:09:17 +08:00
utils . assertArguments ( noteId ) ;
2017-11-23 08:58:56 +08:00
2018-03-26 10:37:02 +08:00
const note = await treeCache . getNote ( noteId ) ;
note . title = title ;
2017-11-23 12:16:54 +08:00
2018-03-26 02:49:20 +08:00
for ( const clone of getNodesByNoteId ( noteId ) ) {
await setNodeTitleWithPrefix ( clone ) ;
}
2018-03-25 23:09:17 +08:00
}
2017-11-23 12:16:54 +08:00
2018-03-25 23:09:17 +08:00
async function createNewTopLevelNote ( ) {
2019-01-16 03:30:54 +08:00
const hoistedNoteId = await hoistedNoteService . getHoistedNoteId ( ) ;
const rootNode = getNodesByNoteId ( hoistedNoteId ) [ 0 ] ;
2017-12-24 00:02:38 +08:00
2019-03-30 06:24:41 +08:00
await createNote ( rootNode , hoistedNoteId , "into" ) ;
2018-03-25 23:09:17 +08:00
}
2017-11-23 12:16:54 +08:00
2019-04-30 03:18:12 +08:00
async function createNote ( node , parentNoteId , target , extraOptions = { } ) {
2018-03-25 23:09:17 +08:00
utils . assertArguments ( node , parentNoteId , target ) ;
2017-11-23 12:16:54 +08:00
2018-03-25 23:09:17 +08:00
// if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted
// but this is quite weird since user doesn't see WHERE the note is being created so it shouldn't occur often
2019-03-30 06:24:41 +08:00
if ( ! extraOptions . isProtected || ! protectedSessionHolder . isProtectedSessionAvailable ( ) ) {
extraOptions . isProtected = false ;
2018-03-25 23:09:17 +08:00
}
2017-11-23 12:16:54 +08:00
2019-03-15 03:21:27 +08:00
if ( noteDetailService . getActiveNoteType ( ) !== 'text' ) {
2019-03-30 06:24:41 +08:00
extraOptions . saveSelection = false ;
2018-09-07 04:58:46 +08:00
}
2018-11-30 18:37:33 +08:00
else {
// just disable this feature altogether - there's a problem that note containing image or table at the beginning
// of the content will be auto-selected by CKEditor and then CTRL-P with no user interaction will automatically save
// the selection - see https://github.com/ckeditor/ckeditor5/issues/1384
2019-03-30 06:24:41 +08:00
extraOptions . saveSelection = false ;
2018-11-30 18:37:33 +08:00
}
2018-09-07 04:58:46 +08:00
2019-03-30 06:24:41 +08:00
if ( extraOptions . saveSelection ) {
[ extraOptions . title , extraOptions . content ] = parseSelectedHtml ( window . cutToNote . getSelectedHtml ( ) ) ;
2018-09-07 04:58:46 +08:00
}
2019-03-30 06:24:41 +08:00
const newNoteName = extraOptions . title || "new note" ;
2018-03-25 12:20:55 +08:00
2018-04-01 23:42:12 +08:00
const { note , branch } = await server . post ( 'notes/' + parentNoteId + '/children' , {
2018-03-25 23:09:17 +08:00
title : newNoteName ,
2019-03-30 06:24:41 +08:00
content : extraOptions . content ,
2018-03-25 23:09:17 +08:00
target : target ,
target _branchId : node . data . branchId ,
2019-03-30 06:24:41 +08:00
isProtected : extraOptions . isProtected ,
type : extraOptions . type
2018-03-25 23:09:17 +08:00
} ) ;
2018-03-25 12:20:55 +08:00
2019-03-30 06:24:41 +08:00
if ( extraOptions . saveSelection ) {
2018-09-07 04:58:46 +08:00
// we remove the selection only after it was saved to server to make sure we don't lose anything
window . cutToNote . removeSelection ( ) ;
}
2019-05-02 04:19:29 +08:00
await noteDetailService . saveNotesIfChanged ( ) ;
2018-09-07 04:58:46 +08:00
2019-01-11 05:46:08 +08:00
noteDetailService . addDetailLoadedListener ( note . noteId , noteDetailService . focusAndSelectTitle ) ;
2018-11-01 01:21:58 +08:00
2018-04-01 23:42:12 +08:00
const noteEntity = new NoteShort ( treeCache , note ) ;
const branchEntity = new Branch ( treeCache , branch ) ;
2018-01-28 23:37:43 +08:00
2018-04-01 23:42:12 +08:00
treeCache . add ( noteEntity , branchEntity ) ;
2017-11-23 12:16:54 +08:00
2019-03-30 06:55:28 +08:00
let newNode = {
2018-03-25 23:09:17 +08:00
title : newNoteName ,
2018-04-01 23:42:12 +08:00
noteId : branchEntity . noteId ,
2018-03-25 23:09:17 +08:00
parentNoteId : parentNoteId ,
2018-04-01 23:42:12 +08:00
refKey : branchEntity . noteId ,
branchId : branchEntity . branchId ,
2019-03-30 06:24:41 +08:00
isProtected : extraOptions . isProtected ,
2018-11-14 15:42:00 +08:00
extraClasses : await treeBuilder . getExtraClasses ( noteEntity ) ,
2019-03-30 06:24:41 +08:00
icon : await treeBuilder . getIcon ( noteEntity ) ,
folder : extraOptions . type === 'search' ,
lazy : true
2018-03-25 23:09:17 +08:00
} ;
2017-11-23 12:16:54 +08:00
2018-03-25 23:09:17 +08:00
if ( target === 'after' ) {
await node . appendSibling ( newNode ) . setActive ( true ) ;
}
else if ( target === 'into' ) {
if ( ! node . getChildren ( ) && node . isFolder ( ) ) {
await node . setExpanded ( ) ;
2017-11-23 12:16:54 +08:00
}
2019-05-23 02:53:59 +08:00
node . addChildren ( newNode ) ;
2017-11-23 12:16:54 +08:00
2018-03-25 23:09:17 +08:00
await node . getLastChild ( ) . setActive ( true ) ;
2018-02-14 12:25:28 +08:00
2019-01-23 04:21:44 +08:00
const parentNoteEntity = await treeCache . getNote ( node . data . noteId ) ;
2018-03-25 23:09:17 +08:00
node . folder = true ;
2019-01-23 04:21:44 +08:00
node . icon = await treeBuilder . getIcon ( parentNoteEntity ) ; // icon might change into folder
2018-03-25 23:09:17 +08:00
node . renderTitle ( ) ;
2017-11-23 12:16:54 +08:00
}
2018-03-25 23:09:17 +08:00
else {
2018-03-26 09:29:35 +08:00
infoService . throwError ( "Unrecognized target: " + target ) ;
2018-01-14 06:00:40 +08:00
}
2018-03-25 23:09:17 +08:00
clearSelectedNodes ( ) ; // to unmark previously active node
2018-02-13 12:53:00 +08:00
2019-03-30 06:55:28 +08:00
// need to refresh because original doesn't have methods like .getParent()
newNode = getNodesByNoteId ( branchEntity . noteId ) [ 0 ] ;
// following for cycle will make sure that also clones of a parent are refreshed
for ( const newParentNode of getNodesByNoteId ( parentNoteId ) ) {
if ( newParentNode . key === newNode . getParent ( ) . key ) {
// we've added a note into this one so no need to refresh
continue ;
}
await newParentNode . load ( true ) ; // force reload to show up new note
await checkFolderStatus ( newParentNode ) ;
}
2018-10-16 05:45:37 +08:00
return { note , branch } ;
2018-03-25 23:09:17 +08:00
}
2018-02-25 10:23:04 +08:00
2018-09-07 04:58:46 +08:00
/* If first element is heading, parse it out and use it as a new heading. */
function parseSelectedHtml ( selectedHtml ) {
const dom = $ . parseHTML ( selectedHtml ) ;
if ( dom . length > 0 && dom [ 0 ] . tagName && dom [ 0 ] . tagName . match ( /h[1-6]/i ) ) {
const title = $ ( dom [ 0 ] ) . text ( ) ;
2018-09-07 16:26:23 +08:00
// remove the title from content (only first occurence)
const content = selectedHtml . replace ( dom [ 0 ] . outerHTML , "" ) ;
2018-09-07 04:58:46 +08:00
return [ title , content ] ;
}
else {
return [ null , selectedHtml ] ;
}
}
2018-03-25 23:09:17 +08:00
async function sortAlphabetically ( noteId ) {
await server . put ( 'notes/' + noteId + '/sort' ) ;
2018-03-14 07:31:07 +08:00
2018-03-25 23:09:17 +08:00
await reload ( ) ;
}
2017-12-19 12:41:13 +08:00
2018-03-27 10:29:14 +08:00
async function showTree ( ) {
const tree = await loadTree ( ) ;
initFancyTree ( tree ) ;
}
2018-08-01 15:26:02 +08:00
messagingService . subscribeToMessages ( message => {
if ( message . type === 'refresh-tree' ) {
reload ( ) ;
}
} ) ;
messagingService . subscribeToSyncMessages ( syncData => {
2018-03-26 09:16:57 +08:00
if ( syncData . some ( sync => sync . entityName === 'branches' )
|| syncData . some ( sync => sync . entityName === 'notes' ) ) {
console . log ( utils . now ( ) , "Reloading tree because of background changes" ) ;
reload ( ) ;
}
} ) ;
2019-01-03 01:59:08 +08:00
utils . bindShortcut ( 'ctrl+o' , async ( ) => {
2019-03-21 05:28:54 +08:00
const node = getActiveNode ( ) ;
2018-03-25 23:09:17 +08:00
const parentNoteId = node . data . parentNoteId ;
2019-04-17 03:40:04 +08:00
const isProtected = await treeUtils . getParentProtectedStatus ( node ) ;
2017-12-19 12:41:13 +08:00
2019-01-03 01:59:08 +08:00
if ( node . data . noteId === 'root' || node . data . noteId === await hoistedNoteService . getHoistedNoteId ( ) ) {
return ;
}
2019-03-30 06:24:41 +08:00
await createNote ( node , parentNoteId , 'after' , {
isProtected : isProtected ,
saveSelection : true
} ) ;
2018-03-25 23:09:17 +08:00
} ) ;
2017-12-19 12:41:13 +08:00
2019-03-30 06:24:41 +08:00
async function createNoteInto ( ) {
2019-03-21 05:28:54 +08:00
const node = getActiveNode ( ) ;
2017-12-19 12:41:13 +08:00
2019-03-30 06:24:41 +08:00
await createNote ( node , node . data . noteId , 'into' , {
isProtected : node . data . isProtected ,
saveSelection : true
} ) ;
2018-09-07 04:58:46 +08:00
}
2019-03-19 05:33:19 +08:00
async function checkFolderStatus ( node ) {
const children = node . getChildren ( ) ;
const note = await treeCache . getNote ( node . data . noteId ) ;
if ( ! children || children . length === 0 ) {
node . folder = false ;
node . icon = await treeBuilder . getIcon ( note ) ;
node . renderTitle ( ) ;
}
else if ( children && children . length > 0 ) {
node . folder = true ;
node . icon = await treeBuilder . getIcon ( note ) ;
node . renderTitle ( ) ;
}
}
async function reloadNote ( noteId ) {
2019-04-14 04:56:45 +08:00
await treeCache . reloadChildren ( noteId ) ;
2019-04-14 04:10:16 +08:00
2019-03-19 05:33:19 +08:00
for ( const node of getNodesByNoteId ( noteId ) ) {
await node . load ( true ) ;
await checkFolderStatus ( node ) ;
}
}
2018-09-07 04:58:46 +08:00
window . glob . createNoteInto = createNoteInto ;
utils . bindShortcut ( 'ctrl+p' , createNoteInto ) ;
2017-12-18 05:56:30 +08:00
2019-03-21 05:28:54 +08:00
utils . bindShortcut ( 'ctrl+.' , scrollToActiveNote ) ;
2017-12-20 08:54:55 +08:00
2019-04-17 03:40:04 +08:00
$ ( window ) . bind ( 'hashchange' , async function ( ) {
2019-01-26 05:18:34 +08:00
if ( isNotePathInAddress ( ) ) {
2019-05-15 04:29:47 +08:00
const [ notePath , tabId ] = getHashValueFromAddress ( ) ;
console . debug ( ` Switching to ${ notePath } on tab ${ tabId } because of hash change ` ) ;
2017-12-20 08:54:55 +08:00
2019-05-22 02:24:40 +08:00
noteDetailService . switchToTab ( tabId , notePath ) ;
2017-12-20 08:54:55 +08:00
}
2018-03-25 23:09:17 +08:00
} ) ;
2017-12-20 08:54:55 +08:00
2019-05-16 03:50:27 +08:00
// fancytree doesn't support middle click so this is a way to support it
$tree . on ( 'mousedown' , '.fancytree-title' , e => {
if ( e . which === 2 ) {
const node = $ . ui . fancytree . getNode ( e ) ;
treeUtils . getNotePath ( node ) . then ( notePath => {
if ( notePath ) {
noteDetailService . openInTab ( notePath ) ;
}
} ) ;
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
}
} ) ;
2018-03-27 10:11:45 +08:00
utils . bindShortcut ( 'alt+c' , ( ) => collapseTree ( ) ) ; // don't use shortened form since collapseTree() accepts argument
2018-04-14 07:22:12 +08:00
$collapseTreeButton . click ( ( ) => collapseTree ( ) ) ;
2018-03-27 10:11:45 +08:00
2018-03-25 23:09:17 +08:00
$createTopLevelNoteButton . click ( createNewTopLevelNote ) ;
2019-03-21 05:28:54 +08:00
$scrollToActiveNoteButton . click ( scrollToActiveNote ) ;
2018-03-25 23:09:17 +08:00
export default {
reload ,
collapseTree ,
setBranchBackgroundBasedOnProtectedStatus ,
setProtected ,
2018-08-23 18:55:45 +08:00
activateNote ,
2018-11-01 06:55:14 +08:00
getFocusedNode ,
2019-03-21 05:28:54 +08:00
getActiveNode ,
2018-03-25 23:09:17 +08:00
setNoteTitle ,
2018-03-27 09:50:47 +08:00
setPrefix ,
2018-03-25 23:09:17 +08:00
createNote ,
2018-12-29 17:04:59 +08:00
createNoteInto ,
2018-03-25 23:09:17 +08:00
getSelectedNodes ,
2018-03-27 11:25:54 +08:00
clearSelectedNodes ,
2018-03-27 10:29:14 +08:00
sortAlphabetically ,
2018-12-24 17:10:36 +08:00
showTree ,
loadTree ,
treeInitialized ,
2019-01-26 05:18:34 +08:00
setExpandedToServer ,
2019-03-19 05:33:19 +08:00
getNodesByNoteId ,
checkFolderStatus ,
2019-03-19 06:03:41 +08:00
reloadNote ,
2019-04-14 18:18:52 +08:00
loadTreeCache ,
2019-05-10 03:30:08 +08:00
expandToNote ,
2019-05-12 03:27:27 +08:00
getNodeFromPath ,
2019-05-20 00:21:29 +08:00
resolveNotePath ,
getSomeNotePath
2018-03-25 23:09:17 +08:00
} ;