2021-01-11 23:33:10 +00:00
import ViewController , { EmitMessageEvent } from './ViewController' ;
2020-11-05 16:58:23 +00:00
import shim from '../../shim' ;
2021-02-07 10:09:28 +00:00
import { ButtonSpec , DialogResult , ViewHandle } from './api/types' ;
2020-11-05 16:58:23 +00:00
const { toSystemSlashes } = require ( '../../path-utils' ) ;
2021-11-09 10:50:50 -05:00
import PostMessageService , { MessageParticipant } from '../PostMessageService' ;
2025-06-06 02:00:47 -07:00
import { PluginEditorViewState , PluginViewState } from './reducer' ;
2024-11-08 07:32:05 -08:00
import { defaultWindowId } from '../../reducer' ;
2024-11-10 14:04:46 +00:00
import Logger from '@joplin/utils/Logger' ;
const logger = Logger . create ( 'WebviewController' ) ;
2020-10-09 18:35:46 +01:00
export enum ContainerType {
Panel = 'panel' ,
Dialog = 'dialog' ,
2024-11-10 14:04:46 +00:00
Editor = 'editor' ,
2020-10-09 18:35:46 +01:00
}
export interface Options {
2020-11-12 19:29:22 +00:00
containerType : ContainerType ;
2020-10-09 18:35:46 +01:00
}
interface CloseResponse {
2023-06-30 10:30:29 +01:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-10-09 18:35:46 +01:00
resolve : Function ;
2023-06-30 10:30:29 +01:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-10-09 18:35:46 +01:00
reject : Function ;
}
2021-01-02 13:32:15 +00:00
// TODO: Copied from:
// packages/app-desktop/gui/ResizableLayout/utils/findItemByKey.ts
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-02 13:32:15 +00:00
function findItemByKey ( layout : any , key : string ) : any {
if ( ! layout ) throw new Error ( 'Layout cannot be null' ) ;
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-02 13:32:15 +00:00
function recurseFind ( item : any ) : any {
if ( item . key === key ) return item ;
if ( item . children ) {
for ( const child of item . children ) {
const found = recurseFind ( child ) ;
if ( found ) return found ;
}
}
return null ;
}
return recurseFind ( layout ) ;
}
2025-06-06 02:00:47 -07:00
interface EditorUpdateEvent {
noteId : string ;
newBody : string ;
}
type EditorUpdateListener = ( event : EditorUpdateEvent ) = > void ;
interface SaveNoteEvent {
noteId : string ;
body : string ;
}
type OnSaveNoteCallback = ( saveNoteEvent : SaveNoteEvent ) = > void ;
2020-10-09 18:35:46 +01:00
export default class WebviewController extends ViewController {
2020-11-12 19:13:28 +00:00
private baseDir_ : string ;
2023-06-30 10:30:29 +01:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-11-12 19:13:28 +00:00
private messageListener_ : Function = null ;
2025-06-06 02:00:47 -07:00
private updateListener_ : EditorUpdateListener | null = null ;
2020-11-12 19:13:28 +00:00
private closeResponse_ : CloseResponse = null ;
2024-11-10 14:04:46 +00:00
private containerType_ : ContainerType = null ;
2025-06-06 02:00:47 -07:00
private saveNoteListener_ : OnSaveNoteCallback | null = null ;
2020-10-09 18:35:46 +01:00
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2025-06-06 02:00:47 -07:00
public constructor ( handle : ViewHandle , pluginId : string , store : any , baseDir : string , containerType : ContainerType , parentWindowId : string | null ) {
2021-02-07 10:09:28 +00:00
super ( handle , pluginId , store ) ;
2020-10-09 18:35:46 +01:00
this . baseDir_ = toSystemSlashes ( baseDir , 'linux' ) ;
2024-11-10 14:04:46 +00:00
this . containerType_ = containerType ;
2020-10-09 18:35:46 +01:00
2024-05-02 06:59:50 -07:00
const view : PluginViewState = {
id : this.handle ,
2025-06-06 02:00:47 -07:00
editorTypeId : '' ,
2024-05-02 06:59:50 -07:00
type : this . type ,
containerType : containerType ,
html : '' ,
scripts : [ ] ,
2025-06-06 02:00:47 -07:00
buttons : null ,
fitToContent : true ,
2024-05-02 06:59:50 -07:00
// Opened is used for dialogs and mobile panels (which are shown
// like dialogs):
opened : containerType === ContainerType . Panel ,
2025-06-06 02:00:47 -07:00
active : false ,
parentWindowId ,
2024-05-02 06:59:50 -07:00
} ;
2020-10-09 18:35:46 +01:00
this . store . dispatch ( {
type : 'PLUGIN_VIEW_ADD' ,
pluginId : pluginId ,
2024-05-02 06:59:50 -07:00
view ,
2020-10-09 18:35:46 +01:00
} ) ;
}
2025-06-06 02:00:47 -07:00
public destroy() {
this . store . dispatch ( {
type : 'PLUGIN_VIEW_REMOVE' ,
pluginId : this.pluginId ,
viewId : this.storeView.id ,
} ) ;
}
2020-11-12 19:13:28 +00:00
public get type ( ) : string {
2020-10-09 18:35:46 +01:00
return 'webview' ;
}
2025-06-06 02:00:47 -07:00
// Returns `null` if the view can be shown in any window.
public get parentWindowId ( ) : string {
return this . storeView . parentWindowId ;
}
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-12 19:13:28 +00:00
private setStoreProp ( name : string , value : any ) {
2020-10-09 18:35:46 +01:00
this . store . dispatch ( {
type : 'PLUGIN_VIEW_PROP_SET' ,
pluginId : this.pluginId ,
id : this.handle ,
name : name ,
value : value ,
} ) ;
}
2020-11-12 19:13:28 +00:00
public get html ( ) : string {
2020-10-09 18:35:46 +01:00
return this . storeView . html ;
}
2020-11-12 19:13:28 +00:00
public set html ( html : string ) {
2020-10-09 18:35:46 +01:00
this . setStoreProp ( 'html' , html ) ;
}
2020-11-12 19:13:28 +00:00
public get containerType ( ) : ContainerType {
2020-10-09 18:35:46 +01:00
return this . storeView . containerType ;
}
2020-11-12 19:13:28 +00:00
public async addScript ( path : string ) {
2020-10-09 18:35:46 +01:00
const fullPath = toSystemSlashes ( shim . fsDriver ( ) . resolve ( ` ${ this . baseDir_ } / ${ path } ` ) , 'linux' ) ;
if ( fullPath . indexOf ( this . baseDir_ ) !== 0 ) throw new Error ( ` Script appears to be outside of plugin base directory: ${ fullPath } (Base dir: ${ this . baseDir_ } ) ` ) ;
this . store . dispatch ( {
type : 'PLUGIN_VIEW_PROP_PUSH' ,
pluginId : this.pluginId ,
id : this.handle ,
name : 'scripts' ,
value : fullPath ,
} ) ;
}
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-11-09 10:50:50 -05:00
public postMessage ( message : any ) {
const messageId = ` plugin_ ${ Date . now ( ) } ${ Math . random ( ) } ` ;
void PostMessageService . instance ( ) . postMessage ( {
pluginId : this.pluginId ,
viewId : this.handle ,
2024-11-08 07:32:05 -08:00
windowId : defaultWindowId ,
2021-11-09 10:50:50 -05:00
contentScriptId : null ,
from : MessageParticipant . Plugin ,
to : MessageParticipant.UserWebview ,
id : messageId ,
content : message ,
} ) ;
}
2024-11-10 14:04:46 +00:00
public async emitMessage ( event : EmitMessageEvent ) {
2020-10-09 18:35:46 +01:00
if ( ! this . messageListener_ ) return ;
2024-11-10 14:04:46 +00:00
2021-01-11 23:33:10 +00:00
return this . messageListener_ ( event . message ) ;
2020-10-09 18:35:46 +01:00
}
2025-06-06 02:00:47 -07:00
public emitUpdate ( event : EditorUpdateEvent ) {
2024-11-10 14:04:46 +00:00
if ( ! this . updateListener_ ) return ;
if ( this . containerType_ === ContainerType . Editor && ( ! this . isActive ( ) || ! this . isVisible ( ) ) ) {
logger . info ( 'emitMessage: Not emitting update because editor is disabled or hidden:' , this . pluginId , this . handle , this . isActive ( ) , this . isVisible ( ) ) ;
return ;
}
2025-06-06 02:00:47 -07:00
this . updateListener_ ( event ) ;
2024-11-10 14:04:46 +00:00
}
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-12 19:13:28 +00:00
public onMessage ( callback : any ) {
2020-10-09 18:35:46 +01:00
this . messageListener_ = callback ;
}
2025-06-06 02:00:47 -07:00
public onUpdate ( callback : EditorUpdateListener ) {
2024-11-10 14:04:46 +00:00
this . updateListener_ = callback ;
}
2021-01-02 13:32:15 +00:00
// ---------------------------------------------
// Specific to panels
// ---------------------------------------------
2024-05-02 06:59:50 -07:00
private showWithAppLayout() {
return this . containerType === ContainerType . Panel && ! ! this . store . getState ( ) . mainLayout ;
}
2023-06-30 09:11:26 +01:00
public async show ( show = true ) : Promise < void > {
2024-05-02 06:59:50 -07:00
if ( this . showWithAppLayout ( ) ) {
this . store . dispatch ( {
type : 'MAIN_LAYOUT_SET_ITEM_PROP' ,
itemKey : this.handle ,
propName : 'visible' ,
propValue : show ,
} ) ;
} else {
this . setStoreProp ( 'opened' , show ) ;
}
2021-01-02 13:32:15 +00:00
}
public async hide ( ) : Promise < void > {
return this . show ( false ) ;
}
public get visible ( ) : boolean {
2024-03-09 03:03:57 -08:00
const appState = this . store . getState ( ) ;
// Mobile: There is no appState.mainLayout
2024-05-02 06:59:50 -07:00
if ( ! this . showWithAppLayout ( ) ) {
return this . storeView . opened ;
2024-03-09 03:03:57 -08:00
}
2024-05-02 06:59:50 -07:00
const mainLayout = appState . mainLayout ;
2021-01-02 13:32:15 +00:00
const item = findItemByKey ( mainLayout , this . handle ) ;
return item ? item.visible : false ;
}
2020-10-09 18:35:46 +01:00
// ---------------------------------------------
// Specific to dialogs
// ---------------------------------------------
2020-11-13 18:48:42 +00:00
public async open ( ) : Promise < DialogResult > {
2024-11-08 07:32:05 -08:00
if ( this . closeResponse_ ) {
this . closeResponse_ . resolve ( null ) ;
this . closeResponse_ = null ;
}
2021-02-07 10:09:28 +00:00
this . store . dispatch ( {
type : 'VISIBLE_DIALOGS_ADD' ,
name : this.handle ,
} ) ;
2020-10-09 18:35:46 +01:00
this . setStoreProp ( 'opened' , true ) ;
2023-06-30 10:30:29 +01:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-11-12 19:13:28 +00:00
return new Promise ( ( resolve : Function , reject : Function ) = > {
2020-10-09 18:35:46 +01:00
this . closeResponse_ = { resolve , reject } ;
} ) ;
}
2020-11-25 14:40:25 +00:00
public close() {
2021-02-07 10:09:28 +00:00
this . store . dispatch ( {
type : 'VISIBLE_DIALOGS_REMOVE' ,
name : this.handle ,
} ) ;
2020-10-09 18:35:46 +01:00
this . setStoreProp ( 'opened' , false ) ;
}
2020-11-13 18:48:42 +00:00
public closeWithResponse ( result : DialogResult ) {
2020-10-09 18:35:46 +01:00
this . close ( ) ;
this . closeResponse_ . resolve ( result ) ;
2024-11-08 07:32:05 -08:00
this . closeResponse_ = null ;
2020-10-09 18:35:46 +01:00
}
2020-11-12 19:13:28 +00:00
public get buttons ( ) : ButtonSpec [ ] {
2020-10-09 18:35:46 +01:00
return this . storeView . buttons ;
}
2020-11-12 19:13:28 +00:00
public set buttons ( buttons : ButtonSpec [ ] ) {
2020-10-09 18:35:46 +01:00
this . setStoreProp ( 'buttons' , buttons ) ;
}
2021-08-18 13:09:45 +02:00
public get fitToContent ( ) : boolean {
return this . storeView . fitToContent ;
}
public set fitToContent ( fitToContent : boolean ) {
this . setStoreProp ( 'fitToContent' , fitToContent ) ;
}
2024-11-10 14:04:46 +00:00
// ---------------------------------------------
// Specific to editors
// ---------------------------------------------
2025-06-06 02:00:47 -07:00
public setEditorTypeId ( id : string ) {
this . setStoreProp ( 'editorTypeId' , id ) ;
}
2024-11-10 14:04:46 +00:00
public setActive ( active : boolean ) {
2025-06-06 02:00:47 -07:00
this . setStoreProp ( 'active' , active ) ;
2024-11-10 14:04:46 +00:00
}
public isActive ( ) : boolean {
2025-06-06 02:00:47 -07:00
const state = this . storeView as PluginEditorViewState ;
return state . active ;
}
public setOpened ( visible : boolean ) {
this . setStoreProp ( 'opened' , visible ) ;
2024-11-10 14:04:46 +00:00
}
2025-02-17 13:47:56 +00:00
public isVisible ( ) : boolean {
2025-06-06 02:00:47 -07:00
const state = this . storeView as PluginEditorViewState ;
return state . active && state . opened ;
2024-11-10 14:04:46 +00:00
}
2025-06-06 02:00:47 -07:00
public async requestSaveNote ( event : SaveNoteEvent ) {
if ( ! this . saveNoteListener_ ) {
logger . warn ( 'Note save requested, but no save handler was registered. View ID: ' , this . storeView ? . id ) ;
return ;
}
this . saveNoteListener_ ( event ) ;
2024-11-10 14:04:46 +00:00
}
2025-06-06 02:00:47 -07:00
public onNoteSaveRequested ( listener : OnSaveNoteCallback ) {
this . saveNoteListener_ = listener ;
}
2020-10-09 18:35:46 +01:00
}