1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +02:00

Plugins: Allow posting messages from plugin to webview (#5569)

This commit is contained in:
agerardin 2021-11-09 10:50:50 -05:00 committed by GitHub
parent 200ba858dd
commit 6b31609338
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 126 additions and 19 deletions

View File

@ -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({

View File

@ -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));

View File

@ -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;

View File

@ -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_);

View File

@ -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(':')];
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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
*/