2022-12-01 13:07:23 +01:00
import froca from "../services/froca.js" ;
import bundleService from "../services/bundle.js" ;
2021-04-24 22:18:25 +02:00
import RootCommandExecutor from "./root_command_executor.js" ;
2025-01-09 18:36:24 +02:00
import Entrypoints , { type SqlExecuteResults } from "./entrypoints.js" ;
2022-12-01 13:07:23 +01:00
import options from "../services/options.js" ;
import utils from "../services/utils.js" ;
import zoomComponent from "./zoom.js" ;
2020-02-07 21:08:55 +01:00
import TabManager from "./tab_manager.js" ;
2022-12-01 13:07:23 +01:00
import Component from "./component.js" ;
import keyboardActionsService from "../services/keyboard_actions.js" ;
2025-01-09 18:36:24 +02:00
import linkService , { type ViewScope } from "../services/link.js" ;
import MobileScreenSwitcherExecutor , { type Screen } from "./mobile_screen_switcher.js" ;
2020-03-17 12:28:02 +01:00
import MainTreeExecutors from "./main_tree_executors.js" ;
2022-12-01 13:07:23 +01:00
import toast from "../services/toast.js" ;
2022-12-01 13:24:34 +01:00
import ShortcutComponent from "./shortcut_component.js" ;
2024-10-15 15:46:34 +08:00
import { t , initLocale } from "../services/i18n.js" ;
2024-12-19 22:06:42 +02:00
import NoteDetailWidget from "../widgets/note_detail.js" ;
2025-01-09 18:36:24 +02:00
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js" ;
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js" ;
import type { ConfirmWithMessageOptions , ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js" ;
import type { Node } from "../services/tree.js" ;
2024-12-22 21:59:08 +02:00
import LoadResults from "../services/load_results.js" ;
2025-01-09 18:36:24 +02:00
import type { Attribute } from "../services/attribute_parser.js" ;
2024-12-23 14:14:38 +02:00
import NoteTreeWidget from "../widgets/note_tree.js" ;
2025-01-09 18:36:24 +02:00
import NoteContext , { type GetTextEditorCallback } from "./note_context.js" ;
2020-01-11 21:19:56 +01:00
2024-07-25 20:55:04 +03:00
interface Layout {
getRootWidget : ( appContext : AppContext ) = > RootWidget ;
}
interface RootWidget extends Component {
2024-12-22 17:56:53 +02:00
render : ( ) = > JQuery < HTMLElement > ;
2024-07-25 20:55:04 +03:00
}
interface BeforeUploadListener extends Component {
beforeUnloadEvent ( ) : boolean ;
}
2024-12-22 19:31:29 +02:00
/ * *
* Base interface for the data / arguments for a given command ( see { @link CommandMappings } ) .
* /
2024-12-22 21:59:08 +02:00
export interface CommandData {
2024-12-23 15:16:41 +02:00
ntxId? : string | null ;
2024-12-21 23:29:17 +02:00
}
2024-12-22 19:31:29 +02:00
/ * *
* Represents a set of commands that are triggered from the context menu , providing information such as the selected note .
* /
export interface ContextMenuCommandData extends CommandData {
node : Node ;
notePath : string ;
noteId? : string ;
selectedOrActiveBranchIds : any ; // TODO: Remove any once type is defined
selectedOrActiveNoteIds : any ; // TODO: Remove any once type is defined
}
2024-12-23 14:10:57 +02:00
export interface NoteCommandData extends CommandData {
notePath : string ;
hoistedNoteId? : string ;
viewScope? : ViewScope ;
}
2024-12-23 15:16:41 +02:00
export interface ExecuteCommandData extends CommandData {
resolve : unknown ;
}
2024-12-22 19:31:29 +02:00
/ * *
* The keys represent the different commands that can be triggered via { @link AppContext # triggerCommand } ( first argument ) , and the values represent the data or arguments definition of the given command . All data for commands must extend { @link CommandData } .
* /
export type CommandMappings = {
2024-12-21 23:57:55 +02:00
"api-log-messages" : CommandData ;
focusOnDetail : Required < CommandData > ;
2025-01-04 11:51:16 +02:00
focusOnSearchDefinition : Required < CommandData > ;
2024-12-21 23:57:55 +02:00
searchNotes : CommandData & {
2025-01-04 11:51:16 +02:00
searchString? : string ;
ancestorNoteId? : string | null ;
} ;
2025-01-04 21:59:35 +02:00
showLaunchBarSubtree : CommandData ;
2025-01-04 11:51:16 +02:00
showOptions : CommandData & {
section : string ;
2024-12-21 23:57:55 +02:00
} ;
showDeleteNotesDialog : CommandData & {
branchIdsToDelete : string [ ] ;
callback : ( value : ResolveOptions ) = > void ;
forceDeleteAllClones : boolean ;
} ;
2024-12-21 23:47:18 +02:00
showConfirmDeleteNoteBoxWithNoteDialog : ConfirmWithTitleOptions ;
2024-12-21 23:57:55 +02:00
openedFileUpdated : CommandData & {
entityType : string ;
entityId : string ;
lastModifiedMs : number ;
filePath : string ;
} ;
focusAndSelectTitle : CommandData & {
isNewNote : boolean ;
} ;
2024-12-21 23:47:18 +02:00
showPromptDialog : PromptDialogOptions ;
showInfoDialog : ConfirmWithMessageOptions ;
showConfirmDialog : ConfirmWithMessageOptions ;
2024-12-23 14:10:57 +02:00
openNewNoteSplit : NoteCommandData ;
2025-01-09 18:07:02 +02:00
openInWindow : NoteCommandData ;
2024-12-22 18:33:57 +02:00
openNoteInNewTab : CommandData ;
openNoteInNewSplit : CommandData ;
openNoteInNewWindow : CommandData ;
2024-12-22 19:31:29 +02:00
openInTab : ContextMenuCommandData ;
openNoteInSplit : ContextMenuCommandData ;
toggleNoteHoisting : ContextMenuCommandData ;
insertNoteAfter : ContextMenuCommandData ;
insertChildNote : ContextMenuCommandData ;
protectSubtree : ContextMenuCommandData ;
unprotectSubtree : ContextMenuCommandData ;
openBulkActionsDialog : ContextMenuCommandData ;
editBranchPrefix : ContextMenuCommandData ;
convertNoteToAttachment : ContextMenuCommandData ;
duplicateSubtree : ContextMenuCommandData ;
expandSubtree : ContextMenuCommandData ;
collapseSubtree : ContextMenuCommandData ;
sortChildNotes : ContextMenuCommandData ;
copyNotePathToClipboard : ContextMenuCommandData ;
recentChangesInSubtree : ContextMenuCommandData ;
cutNotesToClipboard : ContextMenuCommandData ;
copyNotesToClipboard : ContextMenuCommandData ;
pasteNotesFromClipboard : ContextMenuCommandData ;
pasteNotesAfterFromClipboard : ContextMenuCommandData ;
moveNotesTo : ContextMenuCommandData ;
cloneNotesTo : ContextMenuCommandData ;
deleteNotes : ContextMenuCommandData ;
importIntoNote : ContextMenuCommandData ;
exportNote : ContextMenuCommandData ;
searchInSubtree : ContextMenuCommandData ;
addNoteLauncher : ContextMenuCommandData ;
addScriptLauncher : ContextMenuCommandData ;
addWidgetLauncher : ContextMenuCommandData ;
addSpacerLauncher : ContextMenuCommandData ;
moveLauncherToVisible : ContextMenuCommandData ;
moveLauncherToAvailable : ContextMenuCommandData ;
resetLauncher : ContextMenuCommandData ;
2024-12-21 23:57:55 +02:00
executeInActiveNoteDetailWidget : CommandData & {
2025-01-09 18:07:02 +02:00
callback : ( value : NoteDetailWidget | PromiseLike < NoteDetailWidget > ) = > void ;
2024-12-23 15:16:41 +02:00
} ;
2025-01-09 18:07:02 +02:00
executeWithTextEditor : CommandData &
ExecuteCommandData & {
callback? : GetTextEditorCallback ;
} ;
2024-12-23 15:16:41 +02:00
executeWithCodeEditor : CommandData & ExecuteCommandData ;
executeWithContentElement : CommandData & ExecuteCommandData ;
executeWithTypeWidget : CommandData & ExecuteCommandData ;
2024-12-21 23:57:55 +02:00
addTextToActiveEditor : CommandData & {
text : string ;
} ;
2024-12-22 18:33:57 +02:00
/** Works only in the electron context menu. */
replaceMisspelling : CommandData ;
2024-12-22 17:56:53 +02:00
2024-12-21 23:57:55 +02:00
importMarkdownInline : CommandData ;
showPasswordNotSet : CommandData ;
showProtectedSessionPasswordDialog : CommandData ;
closeProtectedSessionPasswordDialog : CommandData ;
2024-12-22 18:33:57 +02:00
copyImageReferenceToClipboard : CommandData ;
copyImageToClipboard : CommandData ;
2024-12-22 19:31:29 +02:00
updateAttributesList : {
2024-12-22 21:59:08 +02:00
attributes : Attribute [ ] ;
2024-12-22 19:31:29 +02:00
} ;
2024-12-22 21:59:08 +02:00
addNewLabel : CommandData ;
addNewRelation : CommandData ;
addNewLabelDefinition : CommandData ;
addNewRelationDefinition : CommandData ;
2024-12-23 14:14:38 +02:00
cloneNoteIdsTo : CommandData & {
noteIds : string [ ] ;
} ;
moveBranchIdsTo : CommandData & {
branchIds : string [ ] ;
} ;
2025-01-04 12:44:40 +02:00
/** Sets the active {@link Screen} (e.g. to toggle the tree sidebar). It triggers the {@link EventMappings.activeScreenChanged} event, but only if the provided <em>screen</em> is different than the current one. */
2024-12-23 14:21:43 +02:00
setActiveScreen : CommandData & {
screen : Screen ;
2025-01-09 18:07:02 +02:00
} ;
2025-01-09 20:20:06 +02:00
closeTab : CommandData ;
closeOtherTabs : CommandData ;
closeRightTabs : CommandData ;
closeAllTabs : CommandData ;
reopenLastTab : CommandData ;
moveTabToNewWindow : CommandData ;
copyTabToNewWindow : CommandData ;
closeActiveTab : CommandData & {
$el : JQuery < HTMLElement >
}
2025-01-09 18:07:02 +02:00
} ;
2024-12-21 23:47:18 +02:00
2024-12-21 23:54:47 +02:00
type EventMappings = {
initialRenderComplete : { } ;
frocaReloaded : { } ;
protectedSessionStarted : { } ;
notesReloaded : {
noteIds : string [ ] ;
} ;
refreshIncludedNote : {
noteId : string ;
} ;
apiLogMessages : {
noteId : string ;
messages : string [ ] ;
} ;
2024-12-22 21:59:08 +02:00
entitiesReloaded : {
2025-01-09 18:07:02 +02:00
loadResults : LoadResults ;
2024-12-22 21:59:08 +02:00
} ;
addNewLabel : CommandData ;
addNewRelation : CommandData ;
2024-12-23 15:16:41 +02:00
sqlQueryResults : CommandData & {
2024-12-23 14:10:57 +02:00
results : SqlExecuteResults ;
2025-01-09 18:07:02 +02:00
} ;
2025-01-04 11:51:16 +02:00
readOnlyTemporarilyDisabled : {
2025-01-09 18:07:02 +02:00
noteContext : NoteContext ;
} ;
2025-01-04 12:44:40 +02:00
/** Triggered when the {@link CommandMappings.setActiveScreen} command is invoked. */
activeScreenChanged : {
activeScreen : Screen ;
2025-01-09 18:07:02 +02:00
} ;
2025-01-07 12:34:10 +02:00
activeContextChanged : {
noteContext : NoteContext ;
2025-01-09 18:07:02 +02:00
} ;
2025-01-07 12:34:10 +02:00
noteSwitched : {
noteContext : NoteContext ;
notePath : string ;
2025-01-09 18:07:02 +02:00
} ;
2025-01-07 12:34:10 +02:00
noteSwitchedAndActivatedEvent : {
noteContext : NoteContext ;
notePath : string ;
2025-01-09 18:07:02 +02:00
} ;
2025-01-07 12:34:10 +02:00
setNoteContext : {
noteContext : NoteContext ;
2025-01-09 18:07:02 +02:00
} ;
2025-01-07 12:34:10 +02:00
noteTypeMimeChangedEvent : {
noteId : string ;
2025-01-09 18:07:02 +02:00
} ;
2025-01-07 12:34:10 +02:00
reEvaluateHighlightsListWidgetVisibility : {
noteId : string | undefined ;
2025-01-09 18:07:02 +02:00
} ;
2025-01-07 12:34:10 +02:00
showHighlightsListWidget : {
noteId : string ;
2025-01-09 18:07:02 +02:00
} ;
2025-01-09 20:20:06 +02:00
hoistedNoteChanged : {
ntxId : string ;
} ;
contextsReopenedEvent : {
mainNtxId : string ;
tabPosition : number ;
} ;
noteContextReorderEvent : {
oldMainNtxId : string ;
newMainNtxId : string ;
} ;
newNoteContextCreated : {
noteContext : NoteContext ;
} ;
noteContextRemovedEvent : {
ntxIds : string [ ] ;
}
2025-01-09 18:07:02 +02:00
} ;
2024-12-21 23:54:47 +02:00
2024-12-22 21:59:08 +02:00
export type EventListener < T extends EventNames > = {
2025-01-09 18:07:02 +02:00
[ key in T as ` ${ key } Event ` ] : ( data : EventData < T > ) = > void ;
} ;
2024-12-22 21:59:08 +02:00
2024-12-23 14:21:43 +02:00
export type CommandListener < T extends CommandNames > = {
2025-01-09 18:07:02 +02:00
[ key in T as ` ${ key } Command ` ] : ( data : CommandListenerData < T > ) = > void ;
} ;
2024-12-23 14:21:43 +02:00
export type CommandListenerData < T extends CommandNames > = CommandMappings [ T ] ;
2024-12-22 21:59:08 +02:00
export type EventData < T extends EventNames > = EventMappings [ T ] ;
2025-01-09 18:07:02 +02:00
type CommandAndEventMappings = CommandMappings & EventMappings ;
2024-12-21 23:54:47 +02:00
2024-12-22 19:31:29 +02:00
/ * *
* This type is a discriminated union which contains all the possible commands that can be triggered via { @link AppContext . triggerCommand } .
* /
2024-12-21 23:47:18 +02:00
export type CommandNames = keyof CommandMappings ;
2024-12-21 23:54:47 +02:00
type EventNames = keyof EventMappings ;
2024-07-25 20:55:04 +03:00
2025-01-09 18:07:02 +02:00
type FilterByValueType < T , ValueType > = { [ K in keyof T ] : T [ K ] extends ValueType ? K : never } [ keyof T ] ;
2024-12-22 19:31:29 +02:00
/ * *
* Generic which filters { @link CommandNames } to provide only those commands that take in as data the desired implementation of { @link CommandData } . Mostly useful for contextual menu , to enforce consistency in the commands .
* /
export type FilteredCommandNames < T extends CommandData > = keyof Pick < CommandMappings , FilterByValueType < CommandMappings , T > > ;
2020-02-16 19:21:17 +01:00
class AppContext extends Component {
2024-07-25 20:55:04 +03:00
isMainWindow : boolean ;
components : Component [ ] ;
beforeUnloadListeners : WeakRef < BeforeUploadListener > [ ] ;
tabManager ! : TabManager ;
layout? : Layout ;
2024-12-23 14:14:38 +02:00
noteTreeWidget? : NoteTreeWidget ;
2024-07-25 20:55:04 +03:00
constructor ( isMainWindow : boolean ) {
2020-04-25 23:52:13 +02:00
super ( ) ;
this . isMainWindow = isMainWindow ;
2022-12-01 13:07:23 +01:00
// non-widget/layout components needed for the application
this . components = [ ] ;
2021-02-27 23:39:02 +01:00
this . beforeUnloadListeners = [ ] ;
2020-04-25 23:52:13 +02:00
}
2024-08-11 08:12:01 +03:00
/ * *
* Must be called as soon as possible , before the creation of any components since this method is in charge of initializing the locale . Any attempts to read translation before this method is called will result in ` undefined ` .
* /
async earlyInit() {
await options . initializedPromise ;
2024-08-11 14:22:37 +03:00
await initLocale ( ) ;
2024-08-11 08:12:01 +03:00
}
2024-07-25 20:55:04 +03:00
setLayout ( layout : Layout ) {
2020-02-06 21:47:31 +01:00
this . layout = layout ;
2020-01-12 19:05:09 +01:00
}
2020-04-25 23:52:13 +02:00
async start() {
2022-12-01 13:07:23 +01:00
this . initComponents ( ) ;
this . renderWidgets ( ) ;
2020-03-29 23:10:45 +02:00
2022-12-18 16:12:29 +01:00
await froca . initializedPromise ;
2022-11-22 22:45:50 +01:00
2020-04-25 23:52:13 +02:00
this . tabManager . loadTabs ( ) ;
2020-02-02 22:32:44 +01:00
2021-03-18 15:09:08 -05:00
setTimeout ( ( ) = > bundleService . executeStartupBundles ( ) , 2000 ) ;
2020-02-02 22:04:28 +01:00
}
2020-01-12 19:05:09 +01:00
2022-12-01 13:07:23 +01:00
initComponents() {
this . tabManager = new TabManager ( ) ;
2025-01-09 18:07:02 +02:00
this . components = [ this . tabManager , new RootCommandExecutor ( ) , new Entrypoints ( ) , new MainTreeExecutors ( ) , new ShortcutComponent ( ) ] ;
2022-12-01 13:07:23 +01:00
if ( utils . isMobile ( ) ) {
this . components . push ( new MobileScreenSwitcherExecutor ( ) ) ;
}
for ( const component of this . components ) {
this . child ( component ) ;
}
if ( utils . isElectron ( ) ) {
this . child ( zoomComponent ) ;
}
}
renderWidgets() {
2024-07-25 20:55:04 +03:00
if ( ! this . layout ) {
throw new Error ( "Missing layout." ) ;
}
2020-02-16 19:21:17 +01:00
const rootWidget = this . layout . getRootWidget ( this ) ;
const $renderedWidget = rootWidget . render ( ) ;
2020-01-14 21:23:32 +01:00
2020-02-16 20:09:59 +01:00
keyboardActionsService . updateDisplayedShortcuts ( $renderedWidget ) ;
2020-02-09 22:31:52 +01:00
$ ( "body" ) . append ( $renderedWidget ) ;
2025-01-09 18:07:02 +02:00
$renderedWidget . on ( "click" , "[data-trigger-command]" , function ( ) {
2023-05-07 10:43:51 +02:00
if ( $ ( this ) . hasClass ( "disabled" ) ) {
return ;
}
2025-01-09 18:07:02 +02:00
const commandName = $ ( this ) . attr ( "data-trigger-command" ) ;
2020-05-05 23:58:52 +02:00
const $component = $ ( this ) . closest ( ".component" ) ;
const component = $component . prop ( "component" ) ;
2020-02-09 22:31:52 +01:00
2025-01-09 18:07:02 +02:00
component . triggerCommand ( commandName , { $el : $ ( this ) } ) ;
2020-02-09 22:31:52 +01:00
} ) ;
2020-02-05 22:08:45 +01:00
2020-02-27 10:03:14 +01:00
this . child ( rootWidget ) ;
2025-01-09 18:07:02 +02:00
this . triggerEvent ( "initialRenderComplete" ) ;
2020-01-11 21:19:56 +01:00
}
2024-12-21 23:54:47 +02:00
// TODO: Remove ignore once all commands are mapped out.
//@ts-ignore
triggerEvent < K extends EventNames | CommandNames > ( name : K , data : CommandAndEventMappings [ K ] = { } ) {
2020-02-29 19:43:19 +01:00
return this . handleEvent ( name , data ) ;
2020-02-01 22:29:32 +01:00
}
2020-02-15 10:41:21 +01:00
2024-12-22 19:31:29 +02:00
triggerCommand < K extends CommandNames > ( name : K , _data? : CommandMappings [ K ] ) {
const data = _data || { } ;
2022-12-01 13:07:23 +01:00
for ( const executor of this . components ) {
2024-07-25 20:55:04 +03:00
const fun = ( executor as any ) [ ` ${ name } Command ` ] ;
2020-02-15 10:41:21 +01:00
2020-02-29 19:43:19 +01:00
if ( fun ) {
return executor . callMethod ( fun , data ) ;
2020-02-15 10:41:21 +01:00
}
}
2023-06-30 11:18:34 +02:00
// this might hint at error, but sometimes this is used by components which are at different places
2020-05-05 23:58:52 +02:00
// in the component tree to communicate with each other
2020-02-17 22:38:46 +01:00
console . debug ( ` Unhandled command ${ name } , converting to event. ` ) ;
2024-12-21 23:54:47 +02:00
return this . triggerEvent ( name , data as CommandAndEventMappings [ K ] ) ;
2020-02-15 10:41:21 +01:00
}
2024-07-25 20:55:04 +03:00
getComponentByEl ( el : HTMLElement ) {
2025-01-09 18:07:02 +02:00
return $ ( el ) . closest ( ".component" ) . prop ( "component" ) ;
2020-02-16 19:21:17 +01:00
}
2021-02-27 23:39:02 +01:00
2024-07-25 20:55:04 +03:00
addBeforeUnloadListener ( obj : BeforeUploadListener ) {
2021-02-27 23:39:02 +01:00
if ( typeof WeakRef !== "function" ) {
// older browsers don't support WeakRef
return ;
}
2024-07-25 20:55:04 +03:00
this . beforeUnloadListeners . push ( new WeakRef < BeforeUploadListener > ( obj ) ) ;
2021-02-27 23:39:02 +01:00
}
2020-01-12 19:05:09 +01:00
}
2020-04-25 23:52:13 +02:00
const appContext = new AppContext ( window . glob . isMainWindow ) ;
2020-01-12 12:48:17 +01:00
2020-02-02 10:41:43 +01:00
// we should save all outstanding changes before the page/app is closed
2025-01-09 18:07:02 +02:00
$ ( window ) . on ( "beforeunload" , ( ) = > {
2021-02-27 23:39:02 +01:00
let allSaved = true ;
2025-01-09 18:07:02 +02:00
appContext . beforeUnloadListeners = appContext . beforeUnloadListeners . filter ( ( wr ) = > ! ! wr . deref ( ) ) ;
2021-02-27 23:39:02 +01:00
for ( const weakRef of appContext . beforeUnloadListeners ) {
const component = weakRef . deref ( ) ;
if ( ! component ) {
continue ;
}
if ( ! component . beforeUnloadEvent ( ) ) {
console . log ( ` Component ${ component . componentId } is not finished saving its state. ` ) ;
2024-10-15 15:46:34 +08:00
toast . showMessage ( t ( "app_context.please_wait_for_save" ) , 10000 ) ;
2021-02-27 23:39:02 +01:00
allSaved = false ;
}
}
if ( ! allSaved ) {
return "some string" ;
}
2020-02-02 10:41:43 +01:00
} ) ;
2025-01-09 18:07:02 +02:00
$ ( window ) . on ( "hashchange" , function ( ) {
const { notePath , ntxId , viewScope } = linkService . parseNavigationStateFromUrl ( window . location . href ) ;
2020-03-21 21:04:34 +01:00
2023-05-07 21:18:21 +02:00
if ( notePath || ntxId ) {
2023-04-11 21:41:55 +02:00
appContext . tabManager . switchToNoteContext ( ntxId , notePath , viewScope ) ;
2020-02-03 20:07:34 +01:00
}
} ) ;
2020-05-11 20:08:55 +02:00
export default appContext ;