mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +02:00
Plugins: Added support for bi-directional messages in content scripts and webview scripts using postMessage
This commit is contained in:
parent
cbad3b1190
commit
2489409abb
@ -224,6 +224,15 @@ packages/app-cli/tests/support/plugins/multi_selection/api/types.js.map
|
||||
packages/app-cli/tests/support/plugins/multi_selection/src/index.d.ts
|
||||
packages/app-cli/tests/support/plugins/multi_selection/src/index.js
|
||||
packages/app-cli/tests/support/plugins/multi_selection/src/index.js.map
|
||||
packages/app-cli/tests/support/plugins/post_messages/api/index.d.ts
|
||||
packages/app-cli/tests/support/plugins/post_messages/api/index.js
|
||||
packages/app-cli/tests/support/plugins/post_messages/api/index.js.map
|
||||
packages/app-cli/tests/support/plugins/post_messages/api/types.d.ts
|
||||
packages/app-cli/tests/support/plugins/post_messages/api/types.js
|
||||
packages/app-cli/tests/support/plugins/post_messages/api/types.js.map
|
||||
packages/app-cli/tests/support/plugins/post_messages/src/index.d.ts
|
||||
packages/app-cli/tests/support/plugins/post_messages/src/index.js
|
||||
packages/app-cli/tests/support/plugins/post_messages/src/index.js.map
|
||||
packages/app-cli/tests/support/plugins/register_command/api/index.d.ts
|
||||
packages/app-cli/tests/support/plugins/register_command/api/index.js
|
||||
packages/app-cli/tests/support/plugins/register_command/api/index.js.map
|
||||
@ -1016,6 +1025,9 @@ packages/lib/services/KeymapService_keysRegExp.js.map
|
||||
packages/lib/services/KvStore.d.ts
|
||||
packages/lib/services/KvStore.js
|
||||
packages/lib/services/KvStore.js.map
|
||||
packages/lib/services/PostMessageService.d.ts
|
||||
packages/lib/services/PostMessageService.js
|
||||
packages/lib/services/PostMessageService.js.map
|
||||
packages/lib/services/ResourceEditWatcher/index.d.ts
|
||||
packages/lib/services/ResourceEditWatcher/index.js
|
||||
packages/lib/services/ResourceEditWatcher/index.js.map
|
||||
@ -1154,6 +1166,9 @@ packages/lib/services/plugins/api/Joplin.js.map
|
||||
packages/lib/services/plugins/api/JoplinCommands.d.ts
|
||||
packages/lib/services/plugins/api/JoplinCommands.js
|
||||
packages/lib/services/plugins/api/JoplinCommands.js.map
|
||||
packages/lib/services/plugins/api/JoplinContentScripts.d.ts
|
||||
packages/lib/services/plugins/api/JoplinContentScripts.js
|
||||
packages/lib/services/plugins/api/JoplinContentScripts.js.map
|
||||
packages/lib/services/plugins/api/JoplinData.d.ts
|
||||
packages/lib/services/plugins/api/JoplinData.js
|
||||
packages/lib/services/plugins/api/JoplinData.js.map
|
||||
|
15
.gitignore
vendored
15
.gitignore
vendored
@ -213,6 +213,15 @@ packages/app-cli/tests/support/plugins/multi_selection/api/types.js.map
|
||||
packages/app-cli/tests/support/plugins/multi_selection/src/index.d.ts
|
||||
packages/app-cli/tests/support/plugins/multi_selection/src/index.js
|
||||
packages/app-cli/tests/support/plugins/multi_selection/src/index.js.map
|
||||
packages/app-cli/tests/support/plugins/post_messages/api/index.d.ts
|
||||
packages/app-cli/tests/support/plugins/post_messages/api/index.js
|
||||
packages/app-cli/tests/support/plugins/post_messages/api/index.js.map
|
||||
packages/app-cli/tests/support/plugins/post_messages/api/types.d.ts
|
||||
packages/app-cli/tests/support/plugins/post_messages/api/types.js
|
||||
packages/app-cli/tests/support/plugins/post_messages/api/types.js.map
|
||||
packages/app-cli/tests/support/plugins/post_messages/src/index.d.ts
|
||||
packages/app-cli/tests/support/plugins/post_messages/src/index.js
|
||||
packages/app-cli/tests/support/plugins/post_messages/src/index.js.map
|
||||
packages/app-cli/tests/support/plugins/register_command/api/index.d.ts
|
||||
packages/app-cli/tests/support/plugins/register_command/api/index.js
|
||||
packages/app-cli/tests/support/plugins/register_command/api/index.js.map
|
||||
@ -1005,6 +1014,9 @@ packages/lib/services/KeymapService_keysRegExp.js.map
|
||||
packages/lib/services/KvStore.d.ts
|
||||
packages/lib/services/KvStore.js
|
||||
packages/lib/services/KvStore.js.map
|
||||
packages/lib/services/PostMessageService.d.ts
|
||||
packages/lib/services/PostMessageService.js
|
||||
packages/lib/services/PostMessageService.js.map
|
||||
packages/lib/services/ResourceEditWatcher/index.d.ts
|
||||
packages/lib/services/ResourceEditWatcher/index.js
|
||||
packages/lib/services/ResourceEditWatcher/index.js.map
|
||||
@ -1143,6 +1155,9 @@ packages/lib/services/plugins/api/Joplin.js.map
|
||||
packages/lib/services/plugins/api/JoplinCommands.d.ts
|
||||
packages/lib/services/plugins/api/JoplinCommands.js
|
||||
packages/lib/services/plugins/api/JoplinCommands.js.map
|
||||
packages/lib/services/plugins/api/JoplinContentScripts.d.ts
|
||||
packages/lib/services/plugins/api/JoplinContentScripts.js
|
||||
packages/lib/services/plugins/api/JoplinContentScripts.js.map
|
||||
packages/lib/services/plugins/api/JoplinData.d.ts
|
||||
packages/lib/services/plugins/api/JoplinData.js
|
||||
packages/lib/services/plugins/api/JoplinData.js.map
|
||||
|
@ -192,7 +192,7 @@ describe('services_PluginService', function() {
|
||||
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
await joplin.plugins.registerContentScript('markdownItPlugin', 'justtesting', './markdownItTestPlugin.js');
|
||||
await joplin.contentScripts.register('markdownItPlugin', 'justtesting', './markdownItTestPlugin.js');
|
||||
},
|
||||
});
|
||||
`);
|
||||
|
@ -8,7 +8,6 @@ import NoteEditor from '../NoteEditor/NoteEditor';
|
||||
import NoteContentPropertiesDialog from '../NoteContentPropertiesDialog';
|
||||
import ShareNoteDialog from '../ShareNoteDialog';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||
import SideBar from '../SideBar/SideBar';
|
||||
import UserWebview from '../../services/plugins/UserWebview';
|
||||
@ -30,7 +29,6 @@ import { themeStyle } from '@joplin/lib/theme';
|
||||
import validateLayout from '../ResizableLayout/utils/validateLayout';
|
||||
import iterateItems from '../ResizableLayout/utils/iterateItems';
|
||||
import removeItem from '../ResizableLayout/utils/removeItem';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
const { PromptDialog } = require('../PromptDialog.min.js');
|
||||
@ -39,8 +37,6 @@ const PluginManager = require('@joplin/lib/services/PluginManager');
|
||||
const EncryptionService = require('@joplin/lib/services/EncryptionService');
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
|
||||
const logger = Logger.create('MainScreen');
|
||||
|
||||
interface LayerModalState {
|
||||
visible: boolean;
|
||||
message: string;
|
||||
@ -157,7 +153,6 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this);
|
||||
this.noteContentPropertiesDialog_close = this.noteContentPropertiesDialog_close.bind(this);
|
||||
this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this);
|
||||
this.userWebview_message = this.userWebview_message.bind(this);
|
||||
this.resizableLayout_resize = this.resizableLayout_resize.bind(this);
|
||||
this.resizableLayout_renderItem = this.resizableLayout_renderItem.bind(this);
|
||||
this.resizableLayout_moveButtonClick = this.resizableLayout_moveButtonClick.bind(this);
|
||||
@ -567,11 +562,6 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
userWebview_message(event: any) {
|
||||
logger.debug('Got message (WebView => Plugin) (2)', event);
|
||||
PluginService.instance().pluginById(event.pluginId).viewController(event.viewId).emitMessage(event);
|
||||
}
|
||||
|
||||
resizableLayout_resize(event: any) {
|
||||
this.updateMainLayout(event.layout);
|
||||
}
|
||||
@ -584,6 +574,8 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
resizableLayout_renderItem(key: string, event: any) {
|
||||
const eventEmitter = event.eventEmitter;
|
||||
|
||||
// const viewsToRemove:string[] = [];
|
||||
|
||||
const components: any = {
|
||||
sideBar: () => {
|
||||
return <SideBar key={key} />;
|
||||
@ -612,26 +604,46 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
const viewInfo = pluginUtils.viewInfoByViewId(this.props.plugins, event.item.key);
|
||||
|
||||
if (!viewInfo) {
|
||||
// Note that it will happen when the component is rendered
|
||||
// before the plugins have loaded their views, so because of
|
||||
// this we need to keep the view in the layout.
|
||||
//
|
||||
// But it can also be a problem if the view really is invalid
|
||||
// due to a faulty plugin as currently there would be no way to
|
||||
// remove it.
|
||||
console.warn(`Could not find plugin associated with view: ${event.item.key}`);
|
||||
return null;
|
||||
} else {
|
||||
const { view, plugin } = viewInfo;
|
||||
|
||||
return <UserWebview
|
||||
key={view.id}
|
||||
viewId={view.id}
|
||||
themeId={this.props.themeId}
|
||||
html={view.html}
|
||||
scripts={view.scripts}
|
||||
pluginId={plugin.id}
|
||||
borderBottom={true}
|
||||
fitToContent={false}
|
||||
/>;
|
||||
}
|
||||
|
||||
const { view, plugin } = viewInfo;
|
||||
|
||||
return <UserWebview
|
||||
key={view.id}
|
||||
viewId={view.id}
|
||||
themeId={this.props.themeId}
|
||||
html={view.html}
|
||||
scripts={view.scripts}
|
||||
pluginId={plugin.id}
|
||||
onMessage={this.userWebview_message}
|
||||
borderBottom={true}
|
||||
fitToContent={false}
|
||||
/>;
|
||||
} else {
|
||||
throw new Error(`Invalid layout component: ${key}`);
|
||||
}
|
||||
|
||||
throw new Error(`Invalid layout component: ${key}`);
|
||||
// if (viewsToRemove.length) {
|
||||
// window.requestAnimationFrame(() => {
|
||||
// let newLayout = this.props.mainLayout;
|
||||
// for (const itemKey of viewsToRemove) {
|
||||
// newLayout = removeItem(newLayout, itemKey);
|
||||
// }
|
||||
|
||||
// if (newLayout !== this.props.mainLayout) {
|
||||
// console.warn('Removed invalid views:', viewsToRemove);
|
||||
// this.updateMainLayout(newLayout);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
renderPluginDialogs() {
|
||||
@ -650,7 +662,6 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
html={view.html}
|
||||
scripts={view.scripts}
|
||||
pluginId={plugin.id}
|
||||
onMessage={this.userWebview_message}
|
||||
buttons={view.buttons}
|
||||
/>);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import contextMenu from './contextMenu';
|
||||
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import PostMessageService from '@joplin/lib/services/PostMessageService';
|
||||
const BaseItem = require('@joplin/lib/models/BaseItem');
|
||||
const BaseModel = require('@joplin/lib/BaseModel').default;
|
||||
const Resource = require('@joplin/lib/models/Resource.js');
|
||||
@ -95,6 +96,8 @@ export default function useMessageHandler(scrollWhenReady: any, setScrollWhenRea
|
||||
const commandName = arg0.name;
|
||||
const commandArgs = arg0.args || [];
|
||||
void CommandService.instance().execute(commandName, ...commandArgs);
|
||||
} else if (msg === 'postMessageService.message') {
|
||||
void PostMessageService.instance().postMessage(arg0);
|
||||
} else {
|
||||
bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import PostMessageService, { MessageResponse, ResponderComponentType } from '@joplin/lib/services/PostMessageService';
|
||||
import * as React from 'react';
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('@joplin/lib/registry.js');
|
||||
@ -20,6 +21,19 @@ class NoteTextViewerComponent extends React.Component<Props, any> {
|
||||
|
||||
this.webviewRef_ = React.createRef();
|
||||
|
||||
PostMessageService.instance().registerResponder(ResponderComponentType.NoteTextViewer, '', (message: MessageResponse) => {
|
||||
if (!this.webviewRef_?.current?.contentWindow) {
|
||||
reg.logger().warn('Cannot respond to message because target is gone', message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.webviewRef_.current.contentWindow.postMessage({
|
||||
target: 'webview',
|
||||
name: 'postMessageService.response',
|
||||
data: message,
|
||||
}, '*');
|
||||
});
|
||||
|
||||
this.webview_domReady = this.webview_domReady.bind(this);
|
||||
this.webview_ipcMessage = this.webview_ipcMessage.bind(this);
|
||||
this.webview_load = this.webview_load.bind(this);
|
||||
|
@ -55,16 +55,30 @@
|
||||
window.postMessage({ target: 'main', name: methodName, args: [ arg ] }, '*');
|
||||
}
|
||||
|
||||
const webviewApiPromises_ = {};
|
||||
|
||||
// This function is reserved for plugin, currently only to allow
|
||||
// executing a command, but more features could be added to the object
|
||||
// later on.
|
||||
const webviewApi = {
|
||||
executeCommand: function (commandName, ...args) {
|
||||
return ipcProxySendToHost('contentScriptExecuteCommand', {
|
||||
name: commandName,
|
||||
args: args,
|
||||
postMessage: function(contentScriptId, message) {
|
||||
const messageId = 'noteViewer_' + Date.now() + Math.random();
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
webviewApiPromises_[messageId] = { resolve, reject };
|
||||
});
|
||||
}
|
||||
|
||||
ipcProxySendToHost('postMessageService.message', {
|
||||
contentScriptId: contentScriptId,
|
||||
viewId: '',
|
||||
from: 'contentScript',
|
||||
to: 'plugin',
|
||||
id: messageId,
|
||||
content: message,
|
||||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
}
|
||||
|
||||
let pluginAssetsAdded_ = {};
|
||||
@ -75,7 +89,7 @@
|
||||
const ipc = {};
|
||||
|
||||
window.addEventListener('message', webviewLib.logEnabledEventHandler(event => {
|
||||
// Here we only deal with messages that are sent from the main Electro process to the webview.
|
||||
// Here we only deal with messages that are sent from the main Electron process to the webview.
|
||||
if (!event.data || event.data.target !== 'webview') return;
|
||||
|
||||
const callName = event.data.name;
|
||||
@ -367,6 +381,20 @@
|
||||
ipcProxySendToHost('percentScroll', percent);
|
||||
}));
|
||||
|
||||
ipc['postMessageService.response'] = function(event) {
|
||||
const promise = webviewApiPromises_[event.responseId];
|
||||
if (!promise) {
|
||||
console.warn('postMessageService.response: could not find callback for message', event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.error) {
|
||||
promise.reject(event.error);
|
||||
} else {
|
||||
promise.resolve(event.response);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('contextmenu', webviewLib.logEnabledEventHandler(event => {
|
||||
let element = event.target;
|
||||
|
||||
|
@ -15,7 +15,6 @@ const logger = Logger.create('UserWebview');
|
||||
export interface Props {
|
||||
html: string;
|
||||
scripts: string[];
|
||||
onMessage: Function;
|
||||
pluginId: string;
|
||||
viewId: string;
|
||||
themeId: number;
|
||||
@ -119,9 +118,9 @@ function UserWebview(props: Props, ref: any) {
|
||||
useWebviewToPluginMessages(
|
||||
frameWindow(),
|
||||
isReady,
|
||||
props.onMessage,
|
||||
props.pluginId,
|
||||
props.viewId
|
||||
props.viewId,
|
||||
postMessage
|
||||
);
|
||||
|
||||
useScriptLoader(
|
||||
|
@ -102,7 +102,6 @@ export default function UserWebviewDialog(props: Props) {
|
||||
ref={webviewRef}
|
||||
html={props.html}
|
||||
scripts={props.scripts}
|
||||
onMessage={props.onMessage}
|
||||
pluginId={props.pluginId}
|
||||
viewId={props.viewId}
|
||||
themeId={props.themeId}
|
||||
|
@ -1,8 +1,26 @@
|
||||
// This is the API that JS files loaded from the webview can see
|
||||
const webviewApiPromises_ = {};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
const webviewApi = {
|
||||
postMessage: function(message) {
|
||||
window.postMessage({ target: 'plugin', message: message }, '*');
|
||||
const messageId = `userWebview_${Date.now()}${Math.random()}`;
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
webviewApiPromises_[messageId] = { resolve, reject };
|
||||
});
|
||||
|
||||
window.postMessage({
|
||||
target: 'postMessageService.message',
|
||||
message: {
|
||||
from: 'userWebview',
|
||||
to: 'plugin',
|
||||
id: messageId,
|
||||
content: message,
|
||||
},
|
||||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
};
|
||||
|
||||
@ -94,6 +112,21 @@ const webviewApi = {
|
||||
addScript(scriptPath);
|
||||
}
|
||||
},
|
||||
|
||||
'postMessageService.response': (event) => {
|
||||
const message = event.message;
|
||||
const promise = webviewApiPromises_[message.responseId];
|
||||
if (!promise) {
|
||||
console.warn('postMessageService.response: could not find callback for message', message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.error) {
|
||||
promise.reject(message.error);
|
||||
} else {
|
||||
promise.resolve(message.response);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
window.addEventListener('message', ((event) => {
|
||||
|
@ -1,24 +1,27 @@
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import PostMessageService, { MessageResponse, ResponderComponentType } from '@joplin/lib/services/PostMessageService';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const logger = Logger.create('useWebviewToPluginMessages');
|
||||
export default function(frameWindow: any, isReady: boolean, pluginId: string, viewId: string, postMessage: Function) {
|
||||
useEffect(() => {
|
||||
PostMessageService.instance().registerResponder(ResponderComponentType.UserWebview, viewId, (message: MessageResponse) => {
|
||||
postMessage('postMessageService.response', { message });
|
||||
});
|
||||
|
||||
return () => {
|
||||
PostMessageService.instance().unregisterResponder(ResponderComponentType.UserWebview, viewId);
|
||||
};
|
||||
}, [viewId]);
|
||||
|
||||
export default function(frameWindow: any, isReady: boolean, onMessage: Function, pluginId: string, viewId: string) {
|
||||
useEffect(() => {
|
||||
if (!frameWindow) return () => {};
|
||||
|
||||
function onMessage_(event: any) {
|
||||
if (!event.data || event.data.target !== 'plugin') return;
|
||||
if (!event.data || event.data.target !== 'postMessageService.message') return;
|
||||
|
||||
// The message is passed from one component or service to the next
|
||||
// till it reaches its destination, so if something doesn't work
|
||||
// follow the chain of messages searching for the string "Got message"
|
||||
logger.debug('Got message (WebView => Plugin) (1)', pluginId, viewId, event.data.message);
|
||||
|
||||
onMessage({
|
||||
pluginId: pluginId,
|
||||
viewId: viewId,
|
||||
message: event.data.message,
|
||||
void PostMessageService.instance().postMessage({
|
||||
pluginId,
|
||||
viewId,
|
||||
...event.data.message,
|
||||
});
|
||||
}
|
||||
|
||||
@ -27,5 +30,5 @@ export default function(frameWindow: any, isReady: boolean, onMessage: Function,
|
||||
return () => {
|
||||
frameWindow.removeEventListener('message', onMessage_);
|
||||
};
|
||||
}, [frameWindow, onMessage, isReady, pluginId, viewId]);
|
||||
}, [frameWindow, isReady, pluginId, viewId]);
|
||||
}
|
||||
|
@ -4,5 +4,5 @@
|
||||
# It could be used to develop plugins too.
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
PLUGIN_PATH="$SCRIPT_DIR/../app-cli/tests/support/plugins/toc"
|
||||
PLUGIN_PATH="$SCRIPT_DIR/../app-cli/tests/support/plugins/post_messages"
|
||||
npm i --prefix="$PLUGIN_PATH" && npm start -- --dev-plugins "$PLUGIN_PATH"
|
133
packages/lib/services/PostMessageService.ts
Normal file
133
packages/lib/services/PostMessageService.ts
Normal file
@ -0,0 +1,133 @@
|
||||
// Passing messages across the various sandbox boundaries can be complex and is
|
||||
// hard to unit test. This class is an attempt to clarify and track what happens
|
||||
// when messages are sent.
|
||||
//
|
||||
// Essentially it works like this:
|
||||
//
|
||||
// The component that might post messages, for example from a content script to
|
||||
// the plugin, and expect responses:
|
||||
//
|
||||
// - First it registers a responder with the PostMessageService - this is what
|
||||
// will be used to send back responses.
|
||||
// - Whenever it sends a message it calls PostMessageService.postMessage() and
|
||||
// wait for the response
|
||||
// - This class forwards the message to the relevant participant and wait for the
|
||||
// response
|
||||
// - Then it sends back the response to the component using the registered
|
||||
// responder.
|
||||
//
|
||||
// There's still quite a bit of boiler plate code on the content script or
|
||||
// webview side to mask the complexity of passing messages. In particular, it
|
||||
// needs to create and return a promise when a message is posted. Then in
|
||||
// another location, when the response is received, it resolves that promise.
|
||||
// See UserWebviewIndex.js to see how it's done.
|
||||
|
||||
import Logger from '../Logger';
|
||||
import PluginService from './plugins/PluginService';
|
||||
|
||||
const logger = Logger.create('PostMessageService');
|
||||
|
||||
enum MessageParticipant {
|
||||
ContentScript = 'contentScript',
|
||||
Plugin = 'plugin',
|
||||
UserWebview = 'userWebview',
|
||||
}
|
||||
|
||||
export enum ResponderComponentType {
|
||||
NoteTextViewer = 'noteTextViewer',
|
||||
UserWebview = 'userWebview',
|
||||
}
|
||||
|
||||
export interface MessageResponse {
|
||||
responseId: string;
|
||||
response: any;
|
||||
error: any;
|
||||
}
|
||||
|
||||
type MessageResponder = (message: MessageResponse)=> void;
|
||||
|
||||
interface Message {
|
||||
pluginId: string;
|
||||
contentScriptId: string;
|
||||
viewId: string;
|
||||
from: MessageParticipant;
|
||||
to: MessageParticipant;
|
||||
id: string;
|
||||
content: any;
|
||||
}
|
||||
|
||||
export default class PostMessageService {
|
||||
|
||||
private static instance_: PostMessageService;
|
||||
private responders_: Record<string, MessageResponder> = {};
|
||||
|
||||
public static instance(): PostMessageService {
|
||||
if (this.instance_) return this.instance_;
|
||||
this.instance_ = new PostMessageService();
|
||||
return this.instance_;
|
||||
}
|
||||
|
||||
public async postMessage(message: Message) {
|
||||
logger.debug('postMessage:', message);
|
||||
|
||||
let response = null;
|
||||
let error = null;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
this.sendResponse(message, response, error);
|
||||
}
|
||||
|
||||
private sendResponse(message: Message, responseContent: any, error: any) {
|
||||
logger.debug('sendResponse', message, responseContent, error);
|
||||
|
||||
let responder: MessageResponder = null;
|
||||
|
||||
if (message.from === MessageParticipant.ContentScript) {
|
||||
responder = this.responder(ResponderComponentType.NoteTextViewer, message.viewId);
|
||||
} else if (message.from === MessageParticipant.UserWebview) {
|
||||
responder = this.responder(ResponderComponentType.UserWebview, message.viewId);
|
||||
}
|
||||
|
||||
if (!responder) {
|
||||
logger.warn('Cannot respond to message because no responder was found', message);
|
||||
}
|
||||
|
||||
responder({
|
||||
responseId: message.id,
|
||||
response: responseContent,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
private responder(type: ResponderComponentType, viewId: string): any {
|
||||
return this.responders_[[type, viewId].join(':')];
|
||||
}
|
||||
|
||||
public registerResponder(type: ResponderComponentType, viewId: string, responder: MessageResponder) {
|
||||
this.responders_[[type, viewId].join(':')] = responder;
|
||||
}
|
||||
|
||||
public unregisterResponder(type: ResponderComponentType, viewId: string) {
|
||||
delete this.responders_[[type, viewId].join(':')];
|
||||
}
|
||||
|
||||
}
|
@ -31,6 +31,8 @@ export default class Plugin {
|
||||
private dispatch_: Function;
|
||||
private eventEmitter_: any;
|
||||
private devMode_: boolean = false;
|
||||
private messageListener_: Function = null;
|
||||
private contentScriptMessageListeners_: Record<string, Function> = {};
|
||||
|
||||
constructor(baseDir: string, manifest: PluginManifest, scriptText: string, dispatch: Function) {
|
||||
this.baseDir_ = shim.fsDriver().resolve(baseDir);
|
||||
@ -106,6 +108,17 @@ export default class Plugin {
|
||||
return this.contentScripts_[type] ? this.contentScripts_[type] : [];
|
||||
}
|
||||
|
||||
public contentScriptById(id: string): ContentScript {
|
||||
for (const type in this.contentScripts_) {
|
||||
const cs = this.contentScripts_[type];
|
||||
for (const c of cs) {
|
||||
if (c.id === id) return c;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public addViewController(v: ViewController) {
|
||||
if (this.viewControllers_[v.handle]) throw new Error(`View already added or there is already a view with this ID: ${v.handle}`);
|
||||
this.viewControllers_[v.handle] = v;
|
||||
@ -120,4 +133,29 @@ export default class Plugin {
|
||||
logger.warn(`"${this.id}": DEPRECATION NOTICE: ${message} This will stop working in version ${goneInVersion}.`);
|
||||
}
|
||||
|
||||
public emitMessage(message: any) {
|
||||
if (!this.messageListener_) return;
|
||||
return this.messageListener_(message);
|
||||
}
|
||||
|
||||
public onMessage(callback: any) {
|
||||
this.messageListener_ = callback;
|
||||
}
|
||||
|
||||
public onContentScriptMessage(id: string, callback: any) {
|
||||
if (!this.contentScriptById(id)) {
|
||||
// The script could potentially be registered later on, but still
|
||||
// best to print a warning to notify the user of a possible bug.
|
||||
logger.warn(`onContentScriptMessage: No such content script: ${id}`);
|
||||
}
|
||||
|
||||
this.contentScriptMessageListeners_[id] = callback;
|
||||
}
|
||||
|
||||
public emitContentScriptMessage(id: string, message: any) {
|
||||
if (!this.contentScriptMessageListeners_[id]) return;
|
||||
return this.contentScriptMessageListeners_[id](message);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ export default class PluginService extends BaseService {
|
||||
private plugins_: Plugins = {};
|
||||
private runner_: BasePluginRunner = null;
|
||||
|
||||
initialize(appVersion: string, platformImplementation: any, runner: BasePluginRunner, store: any) {
|
||||
public initialize(appVersion: string, platformImplementation: any, runner: BasePluginRunner, store: any) {
|
||||
this.appVersion_ = appVersion;
|
||||
this.store_ = store;
|
||||
this.runner_ = runner;
|
||||
@ -115,6 +115,15 @@ export default class PluginService extends BaseService {
|
||||
return JSON.stringify(settings);
|
||||
}
|
||||
|
||||
public pluginIdByContentScriptId(contentScriptId: string): string {
|
||||
for (const pluginId in this.plugins_) {
|
||||
const plugin = this.plugins_[pluginId];
|
||||
const contentScript = plugin.contentScriptById(contentScriptId);
|
||||
if (contentScript) return pluginId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async parsePluginJsBundle(jsBundleString: string) {
|
||||
const scriptText = jsBundleString;
|
||||
const lines = scriptText.split('\n');
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { ViewHandle } from './utils/createViewHandle';
|
||||
|
||||
export interface EmitMessageEvent {
|
||||
message: any;
|
||||
}
|
||||
|
||||
export default class ViewController {
|
||||
|
||||
private handle_: ViewHandle;
|
||||
@ -36,7 +40,7 @@ export default class ViewController {
|
||||
throw new Error('Must be overriden');
|
||||
}
|
||||
|
||||
public emitMessage(event: any) {
|
||||
public async emitMessage(event: EmitMessageEvent): Promise<any> {
|
||||
console.info('Calling ViewController.emitMessage - but not implemented', event);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import ViewController from './ViewController';
|
||||
import ViewController, { EmitMessageEvent } from './ViewController';
|
||||
import shim from '../../shim';
|
||||
import { ButtonSpec, DialogResult } from './api/types';
|
||||
const { toSystemSlashes } = require('../../path-utils');
|
||||
@ -102,9 +102,9 @@ export default class WebviewController extends ViewController {
|
||||
});
|
||||
}
|
||||
|
||||
public emitMessage(event: any) {
|
||||
public async emitMessage(event: EmitMessageEvent): Promise<any> {
|
||||
if (!this.messageListener_) return;
|
||||
this.messageListener_(event.message);
|
||||
return this.messageListener_(event.message);
|
||||
}
|
||||
|
||||
public onMessage(callback: any) {
|
||||
|
@ -7,6 +7,7 @@ import JoplinCommands from './JoplinCommands';
|
||||
import JoplinViews from './JoplinViews';
|
||||
import JoplinInterop from './JoplinInterop';
|
||||
import JoplinSettings from './JoplinSettings';
|
||||
import JoplinContentScripts from './JoplinContentScripts';
|
||||
|
||||
/**
|
||||
* This is the main entry point to the Joplin API. You can access various services using the provided accessors.
|
||||
@ -33,6 +34,7 @@ export default class Joplin {
|
||||
private views_: JoplinViews = null;
|
||||
private interop_: JoplinInterop = null;
|
||||
private settings_: JoplinSettings = null;
|
||||
private contentScripts_: JoplinContentScripts = null;
|
||||
|
||||
constructor(implementation: any, plugin: Plugin, store: any) {
|
||||
this.data_ = new JoplinData();
|
||||
@ -43,6 +45,7 @@ export default class Joplin {
|
||||
this.views_ = new JoplinViews(implementation.views, plugin, store);
|
||||
this.interop_ = new JoplinInterop();
|
||||
this.settings_ = new JoplinSettings(plugin);
|
||||
this.contentScripts_ = new JoplinContentScripts(plugin);
|
||||
}
|
||||
|
||||
get data(): JoplinData {
|
||||
@ -57,6 +60,10 @@ export default class Joplin {
|
||||
return this.workspace_;
|
||||
}
|
||||
|
||||
get contentScripts(): JoplinContentScripts {
|
||||
return this.contentScripts_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*
|
||||
|
52
packages/lib/services/plugins/api/JoplinContentScripts.ts
Normal file
52
packages/lib/services/plugins/api/JoplinContentScripts.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import Plugin from '../Plugin';
|
||||
import { ContentScriptType } from './types';
|
||||
|
||||
export default class JoplinContentScripts {
|
||||
|
||||
private plugin: Plugin;
|
||||
|
||||
public constructor(plugin: Plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new content script. Unlike regular plugin code, which runs in
|
||||
* a separate process, content scripts run within the main process code and
|
||||
* thus allow improved performances and more customisations in specific
|
||||
* cases. It can be used for example to load a Markdown or editor plugin.
|
||||
*
|
||||
* Note that registering a content script in itself will do nothing - it
|
||||
* will only be loaded in specific cases by the relevant app modules (eg.
|
||||
* the Markdown renderer or the code editor). So it is not a way to inject
|
||||
* and run arbitrary code in the app, which for safety and performance
|
||||
* reasons is not supported.
|
||||
*
|
||||
* The plugin generator provides a way to build any content script you might
|
||||
* want to package as well as its dependencies. See the [Plugin Generator
|
||||
* doc](https://github.com/laurent22/joplin/blob/dev/packages/generator-joplin/README.md)
|
||||
* for more information.
|
||||
*
|
||||
* * [View the renderer demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
|
||||
* * [View the editor demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script)
|
||||
*
|
||||
* See also the [postMessage demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages)
|
||||
*
|
||||
* @param type Defines how the script will be used. See the type definition for more information about each supported type.
|
||||
* @param id A unique ID for the content script.
|
||||
* @param scriptPath Must be a path relative to the plugin main script. For example, if your file content_script.js is next to your index.ts file, you would set `scriptPath` to `"./content_script.js`.
|
||||
*/
|
||||
public async register(type: ContentScriptType, id: string, scriptPath: string) {
|
||||
return this.plugin.registerContentScript(type, id, scriptPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to a messages sent from the content script using postMessage().
|
||||
* See {@link ContentScriptType} for more information as well as the
|
||||
* [postMessage
|
||||
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages)
|
||||
*/
|
||||
public async onMessage(contentScriptId: string, callback: any) {
|
||||
this.plugin.onContentScriptMessage(contentScriptId, callback);
|
||||
}
|
||||
|
||||
}
|
@ -51,30 +51,19 @@ export default class JoplinPlugins {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new content script. Unlike regular plugin code, which runs in
|
||||
* a separate process, content scripts run within the main process code and
|
||||
* thus allow improved performances and more customisations in specific
|
||||
* cases. It can be used for example to load a Markdown or editor plugin.
|
||||
*
|
||||
* Note that registering a content script in itself will do nothing - it
|
||||
* will only be loaded in specific cases by the relevant app modules (eg.
|
||||
* the Markdown renderer or the code editor). So it is not a way to inject
|
||||
* and run arbitrary code in the app, which for safety and performance
|
||||
* reasons is not supported.
|
||||
*
|
||||
* The plugin generator provides a way to build any content script you might
|
||||
* want to package as well as its dependencies. See the [Plugin Generator
|
||||
* doc](https://github.com/laurent22/joplin/blob/dev/packages/generator-joplin/README.md)
|
||||
* for more information.
|
||||
*
|
||||
* * [View the renderer demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
|
||||
* * [View the editor demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script)
|
||||
*
|
||||
* @param type Defines how the script will be used. See the type definition for more information about each supported type.
|
||||
* @param id A unique ID for the content script.
|
||||
* @param scriptPath Must be a path relative to the plugin main script. For example, if your file content_script.js is next to your index.ts file, you would set `scriptPath` to `"./content_script.js`.
|
||||
* @deprecated Use joplin.contentScripts.register()
|
||||
*/
|
||||
public async registerContentScript(type: ContentScriptType, id: string, scriptPath: string) {
|
||||
this.plugin.deprecationNotice('1.8', 'joplin.plugins.registerContentScript() is deprecated in favour of joplin.contentScripts.register()');
|
||||
return this.plugin.registerContentScript(type, id, scriptPath);
|
||||
}
|
||||
|
||||
// public async onMessage(callback: any) {
|
||||
// this.plugin.onMessage(callback);
|
||||
// }
|
||||
|
||||
// public async onContentScriptMessage(id: string, callback: any) {
|
||||
// this.plugin.onContentScriptMessage(id, callback);
|
||||
// }
|
||||
|
||||
}
|
||||
|
@ -57,6 +57,22 @@ export default class JoplinViewsPanels {
|
||||
|
||||
/**
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*
|
||||
* To post a message from the webview to the plugin use:
|
||||
*
|
||||
* ```javascript
|
||||
* const response = await webviewApi.postMessage(message);
|
||||
* ```
|
||||
*
|
||||
* - `message` can be any JavaScript object, string or number
|
||||
* - `response` is whatever was returned by the `onMessage` handler
|
||||
*
|
||||
* Using this mechanism, you can have two-way communication between the
|
||||
* plugin and webview.
|
||||
*
|
||||
* See the [postMessage
|
||||
* 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) {
|
||||
return this.controller(handle).onMessage(callback);
|
||||
|
@ -366,9 +366,31 @@ export interface SettingSection {
|
||||
export type Path = string[];
|
||||
|
||||
// =================================================================
|
||||
// Plugins type
|
||||
// Content Script types
|
||||
// =================================================================
|
||||
|
||||
export type PostMessageHandler = (id: string, message: any)=> Promise<any>;
|
||||
|
||||
/**
|
||||
* When a content script is initialised, it receives a `context` object.
|
||||
*/
|
||||
export interface ContentScriptContext {
|
||||
/**
|
||||
* The plugin ID that registered this content script
|
||||
*/
|
||||
pluginId: string;
|
||||
|
||||
/**
|
||||
* The content script ID, which may be necessary to post messages
|
||||
*/
|
||||
contentScriptId: string;
|
||||
|
||||
/**
|
||||
* Can be used by CodeMirror content scripts to post a message to the plugin
|
||||
*/
|
||||
postMessage: PostMessageHandler;
|
||||
}
|
||||
|
||||
export enum ContentScriptType {
|
||||
/**
|
||||
* Registers a new Markdown-It plugin, which should follow the template
|
||||
@ -394,43 +416,56 @@ export enum ContentScriptType {
|
||||
*
|
||||
* ## Exported members
|
||||
*
|
||||
* - The `context` argument is currently unused but could be used later
|
||||
* on to provide access to your own plugin so that the content script
|
||||
* and plugin can communicate.
|
||||
* - The `context` argument is currently unused but could be used later on
|
||||
* to provide access to your own plugin so that the content script and
|
||||
* plugin can communicate.
|
||||
*
|
||||
* - The **required** `plugin` key is the actual Markdown-It plugin -
|
||||
* check the [official
|
||||
* doc](https://github.com/markdown-it/markdown-it) for more
|
||||
* - The **required** `plugin` key is the actual Markdown-It plugin - check
|
||||
* the [official doc](https://github.com/markdown-it/markdown-it) for more
|
||||
* information. The `options` parameter is of type
|
||||
* [RuleOptions](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml.ts),
|
||||
* which contains a number of options, mostly useful for Joplin's
|
||||
* internal code.
|
||||
* which contains a number of options, mostly useful for Joplin's internal
|
||||
* code.
|
||||
*
|
||||
* - Using the **optional** `assets` key you may specify assets such as
|
||||
* JS or CSS that should be loaded in the rendered HTML document.
|
||||
* Check for example the Joplin [Mermaid
|
||||
* - Using the **optional** `assets` key you may specify assets such as JS
|
||||
* or CSS that should be loaded in the rendered HTML document. Check for
|
||||
* example the Joplin [Mermaid
|
||||
* plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
|
||||
* to see how the data should be structured.
|
||||
*
|
||||
* ## Passing messages from the content script to your plugin
|
||||
* ## Posting messages from the content script to your plugin
|
||||
*
|
||||
* The application provides the following function to allow executing
|
||||
* commands from the rendered HTML code:
|
||||
*
|
||||
* `webviewApi.executeCommand(commandName, ...args)`
|
||||
* ```javascript
|
||||
* const response = await webviewApi.postMessage(contentScriptId, message);
|
||||
* ```
|
||||
*
|
||||
* So you can use this mechanism to pass messages from the note viewer
|
||||
* to your own plugin. To do so you would define a command, using
|
||||
* `joplin.commands.register`, then you would call this command using
|
||||
* the `webviewApi` object. See again [the
|
||||
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
|
||||
* to see how this can be done.
|
||||
* - `contentScriptId` is the ID you've defined when you registered the
|
||||
* content script. You can retrieve it from the
|
||||
* {@link ContentScriptContext | context}.
|
||||
* - `message` can be any basic JavaScript type (number, string, plain
|
||||
* object), but it cannot be a function or class instance.
|
||||
*
|
||||
* When you post a message, the plugin can send back a `response` thus
|
||||
* allowing two-way communication:
|
||||
*
|
||||
* ```javascript
|
||||
* await joplin.contentScripts.onMessage(contentScriptId, (message) => {
|
||||
* // Process message
|
||||
* return response; // Can be any object, string or number
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See {@link JoplinContentScripts.onMessage} for more details, as well as
|
||||
* the [postMessage
|
||||
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages).
|
||||
*
|
||||
* ## Registering an existing Markdown-it plugin
|
||||
*
|
||||
* To include a regular Markdown-It plugin, that doesn't make use of
|
||||
* any Joplin-specific features, you would simply create a file such as
|
||||
* this:
|
||||
* To include a regular Markdown-It plugin, that doesn't make use of any
|
||||
* Joplin-specific features, you would simply create a file such as this:
|
||||
*
|
||||
* ```javascript
|
||||
* module.exports = {
|
||||
@ -443,6 +478,7 @@ export enum ContentScriptType {
|
||||
* ```
|
||||
*/
|
||||
MarkdownItPlugin = 'markdownItPlugin',
|
||||
|
||||
/**
|
||||
* Registers a new CodeMirror plugin, which should follow the template
|
||||
* below.
|
||||
@ -466,42 +502,65 @@ export enum ContentScriptType {
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* - The `context` argument is currently unused but could be used later
|
||||
* on to provide access to your own plugin so that the content script
|
||||
* and plugin can communicate.
|
||||
* - The `context` argument is currently unused but could be used later on
|
||||
* to provide access to your own plugin so that the content script and
|
||||
* plugin can communicate.
|
||||
*
|
||||
* - The `plugin` key is your CodeMirror plugin. This is where you can
|
||||
* register new commands with CodeMirror or interact with the
|
||||
* CodeMirror instance as needed.
|
||||
* register new commands with CodeMirror or interact with the CodeMirror
|
||||
* instance as needed.
|
||||
*
|
||||
* - The `codeMirrorResources` key is an array of CodeMirror resources
|
||||
* that will be loaded and attached to the CodeMirror module. These
|
||||
* are made up of addons, keymaps, and modes. For example, for a
|
||||
* plugin that want's to enable clojure highlighting in code blocks.
|
||||
* `codeMirrorResources` would be set to `['mode/clojure/clojure']`.
|
||||
* - The `codeMirrorResources` key is an array of CodeMirror resources that
|
||||
* will be loaded and attached to the CodeMirror module. These are made up
|
||||
* of addons, keymaps, and modes. For example, for a plugin that want's to
|
||||
* enable clojure highlighting in code blocks. `codeMirrorResources` would
|
||||
* be set to `['mode/clojure/clojure']`.
|
||||
*
|
||||
* - The `codeMirrorOptions` key contains all the
|
||||
* [CodeMirror](https://codemirror.net/doc/manual.html#config)
|
||||
* options that will be set or changed by this plugin. New options
|
||||
* can alse be declared via
|
||||
* [CodeMirror](https://codemirror.net/doc/manual.html#config) options
|
||||
* that will be set or changed by this plugin. New options can alse be
|
||||
* declared via
|
||||
* [`CodeMirror.defineOption`](https://codemirror.net/doc/manual.html#defineOption),
|
||||
* and then have their value set here. For example, a plugin that
|
||||
* enables line numbers would set `codeMirrorOptions` to
|
||||
* `{'lineNumbers': true}`.
|
||||
* and then have their value set here. For example, a plugin that enables
|
||||
* line numbers would set `codeMirrorOptions` to `{'lineNumbers': true}`.
|
||||
*
|
||||
* - Using the **optional** `assets` key you may specify **only** CSS
|
||||
* assets that should be loaded in the rendered HTML document. Check
|
||||
* for example the Joplin [Mermaid
|
||||
* - Using the **optional** `assets` key you may specify **only** CSS assets
|
||||
* that should be loaded in the rendered HTML document. Check for example
|
||||
* the Joplin [Mermaid
|
||||
* plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
|
||||
* to see how the data should be structured.
|
||||
*
|
||||
* One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions`
|
||||
* keys must be provided for the plugin to be valid. Having multiple or
|
||||
* all provided is also okay.
|
||||
* One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions` keys
|
||||
* must be provided for the plugin to be valid. Having multiple or all
|
||||
* provided is also okay.
|
||||
*
|
||||
* See the [demo
|
||||
* See also the [demo
|
||||
* plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script)
|
||||
* for an example of all these keys being used in one plugin.
|
||||
*
|
||||
* ## Posting messages from the content script to your plugin
|
||||
*
|
||||
* In order to post messages to the plugin, you can use the postMessage
|
||||
* function passed to the {@link ContentScriptContext | context}.
|
||||
*
|
||||
* ```javascript
|
||||
* const response = await context.postMessage('messageFromCodeMirrorContentScript');
|
||||
* ```
|
||||
*
|
||||
* When you post a message, the plugin can send back a `response` thus
|
||||
* allowing two-way communication:
|
||||
*
|
||||
* ```javascript
|
||||
* await joplin.contentScripts.onMessage(contentScriptId, (message) => {
|
||||
* // Process message
|
||||
* return response; // Can be any object, string or number
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See {@link JoplinContentScripts.onMessage} for more details, as well as
|
||||
* the [postMessage
|
||||
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages).
|
||||
*
|
||||
*/
|
||||
CodeMirrorPlugin = 'codeMirrorPlugin',
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { PluginStates } from '../reducer';
|
||||
import { ContentScriptType } from '../api/types';
|
||||
import { ContentScriptType, ContentScriptContext, PostMessageHandler } from '../api/types';
|
||||
import { dirname } from '@joplin/renderer/pathUtils';
|
||||
import shim from '../../../shim';
|
||||
import Logger from '../../../Logger';
|
||||
import PluginService from '../PluginService';
|
||||
|
||||
const logger = Logger.create('loadContentScripts');
|
||||
|
||||
@ -12,6 +13,17 @@ export interface ExtraContentScript {
|
||||
assetPath: string;
|
||||
}
|
||||
|
||||
function postMessageHandler(pluginId: string, scriptType: ContentScriptType, contentScriptId: string): PostMessageHandler {
|
||||
return (message: any) => {
|
||||
if (scriptType === ContentScriptType.MarkdownItPlugin) {
|
||||
logger.error('context.postMessage is not available to renderer content scripts');
|
||||
} else {
|
||||
const plugin = PluginService.instance().pluginById(pluginId);
|
||||
return plugin.emitContentScriptMessage(contentScriptId, message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function contentScriptsToRendererRules(plugins: PluginStates): ExtraContentScript[] {
|
||||
return loadContentScripts(plugins, ContentScriptType.MarkdownItPlugin);
|
||||
}
|
||||
@ -35,7 +47,14 @@ function loadContentScripts(plugins: PluginStates, scriptType: ContentScriptType
|
||||
const module = shim.requireDynamic(contentScript.path);
|
||||
if (!module.default || typeof module.default !== 'function') throw new Error(`Content script must export a function under the "default" key: Plugin: ${pluginId}: Script: ${contentScript.id}`);
|
||||
|
||||
const loadedModule = module.default({});
|
||||
const context: ContentScriptContext = {
|
||||
pluginId,
|
||||
contentScriptId: contentScript.id,
|
||||
postMessage: postMessageHandler(pluginId, contentScript.id),
|
||||
};
|
||||
|
||||
const loadedModule = module.default(context);
|
||||
|
||||
if (!loadedModule.plugin && !loadedModule.codeMirrorResources && !loadedModule.codeMirrorOptions) throw new Error(`Content script must export a "plugin" key or a list of CodeMirror assets or define a CodeMirror option: Plugin: ${pluginId}: Script: ${contentScript.id}`);
|
||||
|
||||
output.push({
|
||||
|
Loading…
Reference in New Issue
Block a user