You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Plugins: Allow posting messages from plugin to webview (#5569)
This commit is contained in:
		| @@ -50,6 +50,20 @@ async function setupWebviewPanel() { | ||||
| 		console.info('PostMessagePlugin (Webview): Responding with:', response); | ||||
| 		return response; | ||||
| 	}); | ||||
|  | ||||
| 	panels.show(view, true); | ||||
|  | ||||
| 	var intervalID = setInterval( | ||||
| 		() => { | ||||
| 			console.info('check if webview is ready...'); | ||||
| 			if(panels.visible(view)) { | ||||
| 				console.info('plugin: sending message to webview. '); | ||||
| 				panels.postMessage(view, 'testingPluginMessage'); | ||||
| 			} | ||||
| 			clearInterval(intervalID); | ||||
| 		} | ||||
| 			, 500 | ||||
| 	); | ||||
| } | ||||
|  | ||||
| joplin.plugins.register({ | ||||
|   | ||||
| @@ -5,6 +5,10 @@ document.addEventListener('click', async (event) => { | ||||
| 		 | ||||
| 		console.info('webview.js: sending message'); | ||||
| 		const response = await webviewApi.postMessage('testingWebviewMessage'); | ||||
| 		console.info('webiew.js: got response:', response); | ||||
| 		console.info('webview.js: got response:', response); | ||||
| 	} | ||||
| }) | ||||
| }) | ||||
|  | ||||
| console.info('webview.js: registering message listener'); | ||||
| webviewApi.onMessage((message) => console.info('webview.js: got message:', message)); | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| // This is the API that JS files loaded from the webview can see | ||||
| const webviewApiPromises_ = {}; | ||||
| let viewMessageHandler_ = () => {}; | ||||
|  | ||||
| // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars | ||||
| const webviewApi = { | ||||
| @@ -22,6 +23,13 @@ const webviewApi = { | ||||
|  | ||||
| 		return promise; | ||||
| 	}, | ||||
|  | ||||
| 	onMessage: function(viewMessageHandler) { | ||||
| 		viewMessageHandler_ = viewMessageHandler; | ||||
| 		window.postMessage({ | ||||
| 			target: 'postMessageService.registerViewMessageHandler', | ||||
| 		}); | ||||
| 	}, | ||||
| }; | ||||
|  | ||||
| (function() { | ||||
| @@ -117,7 +125,7 @@ const webviewApi = { | ||||
| 				const message = event.message; | ||||
| 				const promise = webviewApiPromises_[message.responseId]; | ||||
| 				if (!promise) { | ||||
| 					console.warn('postMessageService.response: could not find callback for message', message); | ||||
| 					console.warn('postMessageService.response: Could not find recorded promise to process message response', message); | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| @@ -127,8 +135,17 @@ const webviewApi = { | ||||
| 					promise.resolve(message.response); | ||||
| 				} | ||||
| 			}, | ||||
|  | ||||
| 			'postMessageService.plugin_message': (message) => { | ||||
| 				if (!viewMessageHandler_) { | ||||
| 					console.warn('postMessageService.plugin_message: Could not process message because no onMessage handler was defined', message); | ||||
| 					return; | ||||
| 				} | ||||
| 				viewMessageHandler_(message); | ||||
| 			}, | ||||
| 		}; | ||||
|  | ||||
| 		// respond to window.postMessage({}) | ||||
| 		window.addEventListener('message', ((event) => { | ||||
| 			if (!event.data || event.data.target !== 'webview') return; | ||||
|  | ||||
|   | ||||
| @@ -16,13 +16,22 @@ export default function(frameWindow: any, isReady: boolean, pluginId: string, vi | ||||
| 		if (!frameWindow) return () => {}; | ||||
|  | ||||
| 		function onMessage_(event: any) { | ||||
| 			if (!event.data || event.data.target !== 'postMessageService.message') return; | ||||
|  | ||||
| 			void PostMessageService.instance().postMessage({ | ||||
| 				pluginId, | ||||
| 				viewId, | ||||
| 				...event.data.message, | ||||
| 			}); | ||||
| 			if (!event.data || !event.data.target) { | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			if (event.data.target === 'postMessageService.registerViewMessageHandler') { | ||||
| 				PostMessageService.instance().registerViewMessageHandler(ResponderComponentType.UserWebview, viewId, (message: MessageResponse) => { | ||||
| 					postMessage('postMessageService.plugin_message', { message }); | ||||
| 				}); | ||||
| 			} else if (event.data.target === 'postMessageService.message') { | ||||
| 				void PostMessageService.instance().postMessage({ | ||||
| 					pluginId, | ||||
| 					viewId, | ||||
| 					...event.data.message, | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		frameWindow.addEventListener('message', onMessage_); | ||||
|   | ||||
| @@ -27,7 +27,7 @@ import PluginService from './plugins/PluginService'; | ||||
|  | ||||
| const logger = Logger.create('PostMessageService'); | ||||
|  | ||||
| enum MessageParticipant { | ||||
| export enum MessageParticipant { | ||||
| 	ContentScript = 'contentScript', | ||||
| 	Plugin = 'plugin', | ||||
| 	UserWebview = 'userWebview', | ||||
| @@ -46,6 +46,8 @@ export interface MessageResponse { | ||||
|  | ||||
| type MessageResponder = (message: MessageResponse)=> void; | ||||
|  | ||||
| type ViewMessageHandler = (message: any)=> void; | ||||
|  | ||||
| interface Message { | ||||
| 	pluginId: string; | ||||
| 	contentScriptId: string; | ||||
| @@ -60,6 +62,7 @@ export default class PostMessageService { | ||||
|  | ||||
| 	private static instance_: PostMessageService; | ||||
| 	private responders_: Record<string, MessageResponder> = {}; | ||||
| 	private viewMessageHandlers_: Record<string, ViewMessageHandler> = {}; | ||||
|  | ||||
| 	public static instance(): PostMessageService { | ||||
| 		if (this.instance_) return this.instance_; | ||||
| @@ -68,26 +71,26 @@ export default class PostMessageService { | ||||
| 	} | ||||
|  | ||||
| 	public async postMessage(message: Message) { | ||||
| 		logger.debug('postMessage:', message); | ||||
|  | ||||
| 		let response = null; | ||||
| 		let error = null; | ||||
|  | ||||
| 		if (message.from === MessageParticipant.Plugin && message.to === MessageParticipant.UserWebview) { | ||||
| 			this.viewMessageHandler(message); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			if (message.from === MessageParticipant.ContentScript && message.to === MessageParticipant.Plugin) { | ||||
|  | ||||
| 				const pluginId = PluginService.instance().pluginIdByContentScriptId(message.contentScriptId); | ||||
| 				if (!pluginId) throw new Error(`Could not find plugin associated with content script "${message.contentScriptId}"`); | ||||
| 				response = await PluginService.instance().pluginById(pluginId).emitContentScriptMessage(message.contentScriptId, message.content); | ||||
|  | ||||
| 			} else if (message.from === MessageParticipant.UserWebview && message.to === MessageParticipant.Plugin) { | ||||
|  | ||||
| 				response = await PluginService.instance().pluginById(message.pluginId).viewController(message.viewId).emitMessage({ message: message.content }); | ||||
|  | ||||
| 			} else { | ||||
|  | ||||
| 				throw new Error(`Unhandled message: ${JSON.stringify(message)}`); | ||||
|  | ||||
| 			} | ||||
| 		} catch (e) { | ||||
| 			error = e; | ||||
| @@ -96,8 +99,18 @@ export default class PostMessageService { | ||||
| 		this.sendResponse(message, response, error); | ||||
| 	} | ||||
|  | ||||
| 	private viewMessageHandler(message: Message) { | ||||
|  | ||||
| 		const viewMessageHandler = this.viewMessageHandlers_[[ResponderComponentType.UserWebview, message.viewId].join(':')]; | ||||
|  | ||||
| 		if (!viewMessageHandler) { | ||||
| 			logger.warn('Cannot receive message because no viewMessageHandler was found', message); | ||||
| 		} else { | ||||
| 			viewMessageHandler(message.content); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private sendResponse(message: Message, responseContent: any, error: any) { | ||||
| 		logger.debug('sendResponse', message, responseContent, error); | ||||
|  | ||||
| 		let responder: MessageResponder = null; | ||||
|  | ||||
| @@ -126,6 +139,14 @@ export default class PostMessageService { | ||||
| 		this.responders_[[type, viewId].join(':')] = responder; | ||||
| 	} | ||||
|  | ||||
| 	public registerViewMessageHandler(type: ResponderComponentType, viewId: string, callback: ViewMessageHandler) { | ||||
| 		this.viewMessageHandlers_[[type, viewId].join(':')] = callback; | ||||
| 	} | ||||
|  | ||||
| 	public unregisterViewMessageHandler(type: ResponderComponentType, viewId: string) { | ||||
| 		delete this.viewMessageHandlers_[[type, viewId].join(':')]; | ||||
| 	} | ||||
|  | ||||
| 	public unregisterResponder(type: ResponderComponentType, viewId: string) { | ||||
| 		delete this.responders_[[type, viewId].join(':')]; | ||||
| 	} | ||||
|   | ||||
| @@ -44,4 +44,8 @@ export default class ViewController { | ||||
| 		console.info('Calling ViewController.emitMessage - but not implemented', event); | ||||
| 	} | ||||
|  | ||||
| 	public postMessage(message: any) { | ||||
| 		console.info('Calling ViewController.postMessage - but not implemented', message); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import ViewController, { EmitMessageEvent } from './ViewController'; | ||||
| import shim from '../../shim'; | ||||
| import { ButtonSpec, DialogResult, ViewHandle } from './api/types'; | ||||
| const { toSystemSlashes } = require('../../path-utils'); | ||||
| import PostMessageService, { MessageParticipant } from '../PostMessageService'; | ||||
|  | ||||
| export enum ContainerType { | ||||
| 	Panel = 'panel', | ||||
| @@ -103,7 +104,24 @@ export default class WebviewController extends ViewController { | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	public postMessage(message: any) { | ||||
|  | ||||
| 		const messageId = `plugin_${Date.now()}${Math.random()}`; | ||||
|  | ||||
| 		void PostMessageService.instance().postMessage({ | ||||
| 			pluginId: this.pluginId, | ||||
| 			viewId: this.handle, | ||||
| 			contentScriptId: null, | ||||
| 			from: MessageParticipant.Plugin, | ||||
| 			to: MessageParticipant.UserWebview, | ||||
| 			id: messageId, | ||||
| 			content: message, | ||||
| 		}); | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	public async emitMessage(event: EmitMessageEvent): Promise<any> { | ||||
|  | ||||
| 		if (!this.messageListener_) return; | ||||
| 		return this.messageListener_(event.message); | ||||
| 	} | ||||
|   | ||||
| @@ -44,14 +44,14 @@ export default class JoplinViewsPanels { | ||||
| 	/** | ||||
| 	 * Sets the panel webview HTML | ||||
| 	 */ | ||||
| 	public async setHtml(handle: ViewHandle, html: string) { | ||||
| 	public async setHtml(handle: ViewHandle, html: string): Promise<string> { | ||||
| 		return this.controller(handle).html = html; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Adds and loads a new JS or CSS files into the panel. | ||||
| 	 */ | ||||
| 	public async addScript(handle: ViewHandle, scriptPath: string) { | ||||
| 	public async addScript(handle: ViewHandle, scriptPath: string): Promise<void> { | ||||
| 		return this.controller(handle).addScript(scriptPath); | ||||
| 	} | ||||
|  | ||||
| @@ -74,10 +74,30 @@ export default class JoplinViewsPanels { | ||||
| 	 * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) for more details. | ||||
| 	 * | ||||
| 	 */ | ||||
| 	public async onMessage(handle: ViewHandle, callback: Function) { | ||||
| 	public async onMessage(handle: ViewHandle, callback: Function): Promise<void> { | ||||
| 		return this.controller(handle).onMessage(callback); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Sends a message to the webview. | ||||
| 	 * | ||||
| 	 * The webview must have registered a message handler prior, otherwise the message is ignored. Use; | ||||
| 	 * | ||||
| 	 * ```javascript | ||||
| 	 * webviewApi.onMessage((message) => { ... }); | ||||
| 	 * ``` | ||||
| 	 * | ||||
| 	 *  - `message` can be any JavaScript object, string or number | ||||
| 	 * | ||||
| 	 * The view API may have only one onMessage handler defined. | ||||
| 	 * This method is fire and forget so no response is returned. | ||||
| 	 * | ||||
| 	 * It is particularly useful when the webview needs to react to events emitted by the plugin or the joplin api. | ||||
| 	 */ | ||||
| 	public postMessage(handle: ViewHandle, message: any): void { | ||||
| 		return this.controller(handle).postMessage(message); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Shows the panel | ||||
| 	 */ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user