mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-17 18:44:45 +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.d.ts
|
||||||
packages/app-cli/tests/support/plugins/multi_selection/src/index.js
|
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/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.d.ts
|
||||||
packages/app-cli/tests/support/plugins/register_command/api/index.js
|
packages/app-cli/tests/support/plugins/register_command/api/index.js
|
||||||
packages/app-cli/tests/support/plugins/register_command/api/index.js.map
|
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.d.ts
|
||||||
packages/lib/services/KvStore.js
|
packages/lib/services/KvStore.js
|
||||||
packages/lib/services/KvStore.js.map
|
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.d.ts
|
||||||
packages/lib/services/ResourceEditWatcher/index.js
|
packages/lib/services/ResourceEditWatcher/index.js
|
||||||
packages/lib/services/ResourceEditWatcher/index.js.map
|
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.d.ts
|
||||||
packages/lib/services/plugins/api/JoplinCommands.js
|
packages/lib/services/plugins/api/JoplinCommands.js
|
||||||
packages/lib/services/plugins/api/JoplinCommands.js.map
|
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.d.ts
|
||||||
packages/lib/services/plugins/api/JoplinData.js
|
packages/lib/services/plugins/api/JoplinData.js
|
||||||
packages/lib/services/plugins/api/JoplinData.js.map
|
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.d.ts
|
||||||
packages/app-cli/tests/support/plugins/multi_selection/src/index.js
|
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/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.d.ts
|
||||||
packages/app-cli/tests/support/plugins/register_command/api/index.js
|
packages/app-cli/tests/support/plugins/register_command/api/index.js
|
||||||
packages/app-cli/tests/support/plugins/register_command/api/index.js.map
|
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.d.ts
|
||||||
packages/lib/services/KvStore.js
|
packages/lib/services/KvStore.js
|
||||||
packages/lib/services/KvStore.js.map
|
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.d.ts
|
||||||
packages/lib/services/ResourceEditWatcher/index.js
|
packages/lib/services/ResourceEditWatcher/index.js
|
||||||
packages/lib/services/ResourceEditWatcher/index.js.map
|
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.d.ts
|
||||||
packages/lib/services/plugins/api/JoplinCommands.js
|
packages/lib/services/plugins/api/JoplinCommands.js
|
||||||
packages/lib/services/plugins/api/JoplinCommands.js.map
|
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.d.ts
|
||||||
packages/lib/services/plugins/api/JoplinData.js
|
packages/lib/services/plugins/api/JoplinData.js
|
||||||
packages/lib/services/plugins/api/JoplinData.js.map
|
packages/lib/services/plugins/api/JoplinData.js.map
|
||||||
|
@ -192,7 +192,7 @@ describe('services_PluginService', function() {
|
|||||||
|
|
||||||
joplin.plugins.register({
|
joplin.plugins.register({
|
||||||
onStart: async function() {
|
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 NoteContentPropertiesDialog from '../NoteContentPropertiesDialog';
|
||||||
import ShareNoteDialog from '../ShareNoteDialog';
|
import ShareNoteDialog from '../ShareNoteDialog';
|
||||||
import CommandService from '@joplin/lib/services/CommandService';
|
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 { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||||
import SideBar from '../SideBar/SideBar';
|
import SideBar from '../SideBar/SideBar';
|
||||||
import UserWebview from '../../services/plugins/UserWebview';
|
import UserWebview from '../../services/plugins/UserWebview';
|
||||||
@ -30,7 +29,6 @@ import { themeStyle } from '@joplin/lib/theme';
|
|||||||
import validateLayout from '../ResizableLayout/utils/validateLayout';
|
import validateLayout from '../ResizableLayout/utils/validateLayout';
|
||||||
import iterateItems from '../ResizableLayout/utils/iterateItems';
|
import iterateItems from '../ResizableLayout/utils/iterateItems';
|
||||||
import removeItem from '../ResizableLayout/utils/removeItem';
|
import removeItem from '../ResizableLayout/utils/removeItem';
|
||||||
import Logger from '@joplin/lib/Logger';
|
|
||||||
|
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const { PromptDialog } = require('../PromptDialog.min.js');
|
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 EncryptionService = require('@joplin/lib/services/EncryptionService');
|
||||||
const ipcRenderer = require('electron').ipcRenderer;
|
const ipcRenderer = require('electron').ipcRenderer;
|
||||||
|
|
||||||
const logger = Logger.create('MainScreen');
|
|
||||||
|
|
||||||
interface LayerModalState {
|
interface LayerModalState {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
@ -157,7 +153,6 @@ class MainScreenComponent extends React.Component<Props, State> {
|
|||||||
this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this);
|
this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this);
|
||||||
this.noteContentPropertiesDialog_close = this.noteContentPropertiesDialog_close.bind(this);
|
this.noteContentPropertiesDialog_close = this.noteContentPropertiesDialog_close.bind(this);
|
||||||
this.shareNoteDialog_close = this.shareNoteDialog_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_resize = this.resizableLayout_resize.bind(this);
|
||||||
this.resizableLayout_renderItem = this.resizableLayout_renderItem.bind(this);
|
this.resizableLayout_renderItem = this.resizableLayout_renderItem.bind(this);
|
||||||
this.resizableLayout_moveButtonClick = this.resizableLayout_moveButtonClick.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) {
|
resizableLayout_resize(event: any) {
|
||||||
this.updateMainLayout(event.layout);
|
this.updateMainLayout(event.layout);
|
||||||
}
|
}
|
||||||
@ -584,6 +574,8 @@ class MainScreenComponent extends React.Component<Props, State> {
|
|||||||
resizableLayout_renderItem(key: string, event: any) {
|
resizableLayout_renderItem(key: string, event: any) {
|
||||||
const eventEmitter = event.eventEmitter;
|
const eventEmitter = event.eventEmitter;
|
||||||
|
|
||||||
|
// const viewsToRemove:string[] = [];
|
||||||
|
|
||||||
const components: any = {
|
const components: any = {
|
||||||
sideBar: () => {
|
sideBar: () => {
|
||||||
return <SideBar key={key} />;
|
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);
|
const viewInfo = pluginUtils.viewInfoByViewId(this.props.plugins, event.item.key);
|
||||||
|
|
||||||
if (!viewInfo) {
|
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}`);
|
console.warn(`Could not find plugin associated with view: ${event.item.key}`);
|
||||||
return null;
|
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}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
const { view, plugin } = viewInfo;
|
throw new Error(`Invalid layout component: ${key}`);
|
||||||
|
|
||||||
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}
|
|
||||||
/>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
renderPluginDialogs() {
|
||||||
@ -650,7 +662,6 @@ class MainScreenComponent extends React.Component<Props, State> {
|
|||||||
html={view.html}
|
html={view.html}
|
||||||
scripts={view.scripts}
|
scripts={view.scripts}
|
||||||
pluginId={plugin.id}
|
pluginId={plugin.id}
|
||||||
onMessage={this.userWebview_message}
|
|
||||||
buttons={view.buttons}
|
buttons={view.buttons}
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import contextMenu from './contextMenu';
|
|||||||
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
|
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import CommandService from '@joplin/lib/services/CommandService';
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
|
import PostMessageService from '@joplin/lib/services/PostMessageService';
|
||||||
const BaseItem = require('@joplin/lib/models/BaseItem');
|
const BaseItem = require('@joplin/lib/models/BaseItem');
|
||||||
const BaseModel = require('@joplin/lib/BaseModel').default;
|
const BaseModel = require('@joplin/lib/BaseModel').default;
|
||||||
const Resource = require('@joplin/lib/models/Resource.js');
|
const Resource = require('@joplin/lib/models/Resource.js');
|
||||||
@ -95,6 +96,8 @@ export default function useMessageHandler(scrollWhenReady: any, setScrollWhenRea
|
|||||||
const commandName = arg0.name;
|
const commandName = arg0.name;
|
||||||
const commandArgs = arg0.args || [];
|
const commandArgs = arg0.args || [];
|
||||||
void CommandService.instance().execute(commandName, ...commandArgs);
|
void CommandService.instance().execute(commandName, ...commandArgs);
|
||||||
|
} else if (msg === 'postMessageService.message') {
|
||||||
|
void PostMessageService.instance().postMessage(arg0);
|
||||||
} else {
|
} else {
|
||||||
bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
|
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';
|
import * as React from 'react';
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const { reg } = require('@joplin/lib/registry.js');
|
const { reg } = require('@joplin/lib/registry.js');
|
||||||
@ -20,6 +21,19 @@ class NoteTextViewerComponent extends React.Component<Props, any> {
|
|||||||
|
|
||||||
this.webviewRef_ = React.createRef();
|
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_domReady = this.webview_domReady.bind(this);
|
||||||
this.webview_ipcMessage = this.webview_ipcMessage.bind(this);
|
this.webview_ipcMessage = this.webview_ipcMessage.bind(this);
|
||||||
this.webview_load = this.webview_load.bind(this);
|
this.webview_load = this.webview_load.bind(this);
|
||||||
|
@ -55,16 +55,30 @@
|
|||||||
window.postMessage({ target: 'main', name: methodName, args: [ arg ] }, '*');
|
window.postMessage({ target: 'main', name: methodName, args: [ arg ] }, '*');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const webviewApiPromises_ = {};
|
||||||
|
|
||||||
// This function is reserved for plugin, currently only to allow
|
// This function is reserved for plugin, currently only to allow
|
||||||
// executing a command, but more features could be added to the object
|
// executing a command, but more features could be added to the object
|
||||||
// later on.
|
// later on.
|
||||||
const webviewApi = {
|
const webviewApi = {
|
||||||
executeCommand: function (commandName, ...args) {
|
postMessage: function(contentScriptId, message) {
|
||||||
return ipcProxySendToHost('contentScriptExecuteCommand', {
|
const messageId = 'noteViewer_' + Date.now() + Math.random();
|
||||||
name: commandName,
|
|
||||||
args: args,
|
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_ = {};
|
let pluginAssetsAdded_ = {};
|
||||||
@ -75,7 +89,7 @@
|
|||||||
const ipc = {};
|
const ipc = {};
|
||||||
|
|
||||||
window.addEventListener('message', webviewLib.logEnabledEventHandler(event => {
|
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;
|
if (!event.data || event.data.target !== 'webview') return;
|
||||||
|
|
||||||
const callName = event.data.name;
|
const callName = event.data.name;
|
||||||
@ -367,6 +381,20 @@
|
|||||||
ipcProxySendToHost('percentScroll', percent);
|
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 => {
|
document.addEventListener('contextmenu', webviewLib.logEnabledEventHandler(event => {
|
||||||
let element = event.target;
|
let element = event.target;
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ const logger = Logger.create('UserWebview');
|
|||||||
export interface Props {
|
export interface Props {
|
||||||
html: string;
|
html: string;
|
||||||
scripts: string[];
|
scripts: string[];
|
||||||
onMessage: Function;
|
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
viewId: string;
|
viewId: string;
|
||||||
themeId: number;
|
themeId: number;
|
||||||
@ -119,9 +118,9 @@ function UserWebview(props: Props, ref: any) {
|
|||||||
useWebviewToPluginMessages(
|
useWebviewToPluginMessages(
|
||||||
frameWindow(),
|
frameWindow(),
|
||||||
isReady,
|
isReady,
|
||||||
props.onMessage,
|
|
||||||
props.pluginId,
|
props.pluginId,
|
||||||
props.viewId
|
props.viewId,
|
||||||
|
postMessage
|
||||||
);
|
);
|
||||||
|
|
||||||
useScriptLoader(
|
useScriptLoader(
|
||||||
|
@ -102,7 +102,6 @@ export default function UserWebviewDialog(props: Props) {
|
|||||||
ref={webviewRef}
|
ref={webviewRef}
|
||||||
html={props.html}
|
html={props.html}
|
||||||
scripts={props.scripts}
|
scripts={props.scripts}
|
||||||
onMessage={props.onMessage}
|
|
||||||
pluginId={props.pluginId}
|
pluginId={props.pluginId}
|
||||||
viewId={props.viewId}
|
viewId={props.viewId}
|
||||||
themeId={props.themeId}
|
themeId={props.themeId}
|
||||||
|
@ -1,8 +1,26 @@
|
|||||||
// This is the API that JS files loaded from the webview can see
|
// 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
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||||
const webviewApi = {
|
const webviewApi = {
|
||||||
postMessage: function(message) {
|
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);
|
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) => {
|
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';
|
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(() => {
|
useEffect(() => {
|
||||||
if (!frameWindow) return () => {};
|
if (!frameWindow) return () => {};
|
||||||
|
|
||||||
function onMessage_(event: any) {
|
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
|
void PostMessageService.instance().postMessage({
|
||||||
// till it reaches its destination, so if something doesn't work
|
pluginId,
|
||||||
// follow the chain of messages searching for the string "Got message"
|
viewId,
|
||||||
logger.debug('Got message (WebView => Plugin) (1)', pluginId, viewId, event.data.message);
|
...event.data.message,
|
||||||
|
|
||||||
onMessage({
|
|
||||||
pluginId: pluginId,
|
|
||||||
viewId: viewId,
|
|
||||||
message: event.data.message,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,5 +30,5 @@ export default function(frameWindow: any, isReady: boolean, onMessage: Function,
|
|||||||
return () => {
|
return () => {
|
||||||
frameWindow.removeEventListener('message', onMessage_);
|
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.
|
# It could be used to develop plugins too.
|
||||||
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
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"
|
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 dispatch_: Function;
|
||||||
private eventEmitter_: any;
|
private eventEmitter_: any;
|
||||||
private devMode_: boolean = false;
|
private devMode_: boolean = false;
|
||||||
|
private messageListener_: Function = null;
|
||||||
|
private contentScriptMessageListeners_: Record<string, Function> = {};
|
||||||
|
|
||||||
constructor(baseDir: string, manifest: PluginManifest, scriptText: string, dispatch: Function) {
|
constructor(baseDir: string, manifest: PluginManifest, scriptText: string, dispatch: Function) {
|
||||||
this.baseDir_ = shim.fsDriver().resolve(baseDir);
|
this.baseDir_ = shim.fsDriver().resolve(baseDir);
|
||||||
@ -106,6 +108,17 @@ export default class Plugin {
|
|||||||
return this.contentScripts_[type] ? this.contentScripts_[type] : [];
|
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) {
|
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}`);
|
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;
|
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}.`);
|
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 plugins_: Plugins = {};
|
||||||
private runner_: BasePluginRunner = null;
|
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.appVersion_ = appVersion;
|
||||||
this.store_ = store;
|
this.store_ = store;
|
||||||
this.runner_ = runner;
|
this.runner_ = runner;
|
||||||
@ -115,6 +115,15 @@ export default class PluginService extends BaseService {
|
|||||||
return JSON.stringify(settings);
|
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) {
|
private async parsePluginJsBundle(jsBundleString: string) {
|
||||||
const scriptText = jsBundleString;
|
const scriptText = jsBundleString;
|
||||||
const lines = scriptText.split('\n');
|
const lines = scriptText.split('\n');
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { ViewHandle } from './utils/createViewHandle';
|
import { ViewHandle } from './utils/createViewHandle';
|
||||||
|
|
||||||
|
export interface EmitMessageEvent {
|
||||||
|
message: any;
|
||||||
|
}
|
||||||
|
|
||||||
export default class ViewController {
|
export default class ViewController {
|
||||||
|
|
||||||
private handle_: ViewHandle;
|
private handle_: ViewHandle;
|
||||||
@ -36,7 +40,7 @@ export default class ViewController {
|
|||||||
throw new Error('Must be overriden');
|
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);
|
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 shim from '../../shim';
|
||||||
import { ButtonSpec, DialogResult } from './api/types';
|
import { ButtonSpec, DialogResult } from './api/types';
|
||||||
const { toSystemSlashes } = require('../../path-utils');
|
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;
|
if (!this.messageListener_) return;
|
||||||
this.messageListener_(event.message);
|
return this.messageListener_(event.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onMessage(callback: any) {
|
public onMessage(callback: any) {
|
||||||
|
@ -7,6 +7,7 @@ import JoplinCommands from './JoplinCommands';
|
|||||||
import JoplinViews from './JoplinViews';
|
import JoplinViews from './JoplinViews';
|
||||||
import JoplinInterop from './JoplinInterop';
|
import JoplinInterop from './JoplinInterop';
|
||||||
import JoplinSettings from './JoplinSettings';
|
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.
|
* 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 views_: JoplinViews = null;
|
||||||
private interop_: JoplinInterop = null;
|
private interop_: JoplinInterop = null;
|
||||||
private settings_: JoplinSettings = null;
|
private settings_: JoplinSettings = null;
|
||||||
|
private contentScripts_: JoplinContentScripts = null;
|
||||||
|
|
||||||
constructor(implementation: any, plugin: Plugin, store: any) {
|
constructor(implementation: any, plugin: Plugin, store: any) {
|
||||||
this.data_ = new JoplinData();
|
this.data_ = new JoplinData();
|
||||||
@ -43,6 +45,7 @@ export default class Joplin {
|
|||||||
this.views_ = new JoplinViews(implementation.views, plugin, store);
|
this.views_ = new JoplinViews(implementation.views, plugin, store);
|
||||||
this.interop_ = new JoplinInterop();
|
this.interop_ = new JoplinInterop();
|
||||||
this.settings_ = new JoplinSettings(plugin);
|
this.settings_ = new JoplinSettings(plugin);
|
||||||
|
this.contentScripts_ = new JoplinContentScripts(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
get data(): JoplinData {
|
get data(): JoplinData {
|
||||||
@ -57,6 +60,10 @@ export default class Joplin {
|
|||||||
return this.workspace_;
|
return this.workspace_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get contentScripts(): JoplinContentScripts {
|
||||||
|
return this.contentScripts_;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ignore
|
* @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
|
* @deprecated Use joplin.contentScripts.register()
|
||||||
* 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`.
|
|
||||||
*/
|
*/
|
||||||
public async registerContentScript(type: ContentScriptType, id: string, scriptPath: string) {
|
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);
|
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).
|
* 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) {
|
public async onMessage(handle: ViewHandle, callback: Function) {
|
||||||
return this.controller(handle).onMessage(callback);
|
return this.controller(handle).onMessage(callback);
|
||||||
|
@ -366,9 +366,31 @@ export interface SettingSection {
|
|||||||
export type Path = string[];
|
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 {
|
export enum ContentScriptType {
|
||||||
/**
|
/**
|
||||||
* Registers a new Markdown-It plugin, which should follow the template
|
* Registers a new Markdown-It plugin, which should follow the template
|
||||||
@ -394,43 +416,56 @@ export enum ContentScriptType {
|
|||||||
*
|
*
|
||||||
* ## Exported members
|
* ## Exported members
|
||||||
*
|
*
|
||||||
* - The `context` argument is currently unused but could be used later
|
* - The `context` argument is currently unused but could be used later on
|
||||||
* on to provide access to your own plugin so that the content script
|
* to provide access to your own plugin so that the content script and
|
||||||
* and plugin can communicate.
|
* plugin can communicate.
|
||||||
*
|
*
|
||||||
* - The **required** `plugin` key is the actual Markdown-It plugin -
|
* - The **required** `plugin` key is the actual Markdown-It plugin - check
|
||||||
* check the [official
|
* the [official doc](https://github.com/markdown-it/markdown-it) for more
|
||||||
* doc](https://github.com/markdown-it/markdown-it) for more
|
|
||||||
* information. The `options` parameter is of type
|
* information. The `options` parameter is of type
|
||||||
* [RuleOptions](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml.ts),
|
* [RuleOptions](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml.ts),
|
||||||
* which contains a number of options, mostly useful for Joplin's
|
* which contains a number of options, mostly useful for Joplin's internal
|
||||||
* internal code.
|
* code.
|
||||||
*
|
*
|
||||||
* - Using the **optional** `assets` key you may specify assets such as
|
* - Using the **optional** `assets` key you may specify assets such as JS
|
||||||
* JS or CSS that should be loaded in the rendered HTML document.
|
* or CSS that should be loaded in the rendered HTML document. Check for
|
||||||
* Check for example the Joplin [Mermaid
|
* example the Joplin [Mermaid
|
||||||
* plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
|
* plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
|
||||||
* to see how the data should be structured.
|
* 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
|
* The application provides the following function to allow executing
|
||||||
* commands from the rendered HTML code:
|
* 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
|
* - `contentScriptId` is the ID you've defined when you registered the
|
||||||
* to your own plugin. To do so you would define a command, using
|
* content script. You can retrieve it from the
|
||||||
* `joplin.commands.register`, then you would call this command using
|
* {@link ContentScriptContext | context}.
|
||||||
* the `webviewApi` object. See again [the
|
* - `message` can be any basic JavaScript type (number, string, plain
|
||||||
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
|
* object), but it cannot be a function or class instance.
|
||||||
* to see how this can be done.
|
*
|
||||||
|
* 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
|
* ## Registering an existing Markdown-it plugin
|
||||||
*
|
*
|
||||||
* To include a regular Markdown-It plugin, that doesn't make use of
|
* To include a regular Markdown-It plugin, that doesn't make use of any
|
||||||
* any Joplin-specific features, you would simply create a file such as
|
* Joplin-specific features, you would simply create a file such as this:
|
||||||
* this:
|
|
||||||
*
|
*
|
||||||
* ```javascript
|
* ```javascript
|
||||||
* module.exports = {
|
* module.exports = {
|
||||||
@ -443,6 +478,7 @@ export enum ContentScriptType {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
MarkdownItPlugin = 'markdownItPlugin',
|
MarkdownItPlugin = 'markdownItPlugin',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a new CodeMirror plugin, which should follow the template
|
* Registers a new CodeMirror plugin, which should follow the template
|
||||||
* below.
|
* below.
|
||||||
@ -466,42 +502,65 @@ export enum ContentScriptType {
|
|||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* - The `context` argument is currently unused but could be used later
|
* - The `context` argument is currently unused but could be used later on
|
||||||
* on to provide access to your own plugin so that the content script
|
* to provide access to your own plugin so that the content script and
|
||||||
* and plugin can communicate.
|
* plugin can communicate.
|
||||||
*
|
*
|
||||||
* - The `plugin` key is your CodeMirror plugin. This is where you can
|
* - The `plugin` key is your CodeMirror plugin. This is where you can
|
||||||
* register new commands with CodeMirror or interact with the
|
* register new commands with CodeMirror or interact with the CodeMirror
|
||||||
* CodeMirror instance as needed.
|
* instance as needed.
|
||||||
*
|
*
|
||||||
* - The `codeMirrorResources` key is an array of CodeMirror resources
|
* - The `codeMirrorResources` key is an array of CodeMirror resources that
|
||||||
* that will be loaded and attached to the CodeMirror module. These
|
* will be loaded and attached to the CodeMirror module. These are made up
|
||||||
* are made up of addons, keymaps, and modes. For example, for a
|
* of addons, keymaps, and modes. For example, for a plugin that want's to
|
||||||
* plugin that want's to enable clojure highlighting in code blocks.
|
* enable clojure highlighting in code blocks. `codeMirrorResources` would
|
||||||
* `codeMirrorResources` would be set to `['mode/clojure/clojure']`.
|
* be set to `['mode/clojure/clojure']`.
|
||||||
*
|
*
|
||||||
* - The `codeMirrorOptions` key contains all the
|
* - The `codeMirrorOptions` key contains all the
|
||||||
* [CodeMirror](https://codemirror.net/doc/manual.html#config)
|
* [CodeMirror](https://codemirror.net/doc/manual.html#config) options
|
||||||
* options that will be set or changed by this plugin. New options
|
* that will be set or changed by this plugin. New options can alse be
|
||||||
* can alse be declared via
|
* declared via
|
||||||
* [`CodeMirror.defineOption`](https://codemirror.net/doc/manual.html#defineOption),
|
* [`CodeMirror.defineOption`](https://codemirror.net/doc/manual.html#defineOption),
|
||||||
* and then have their value set here. For example, a plugin that
|
* and then have their value set here. For example, a plugin that enables
|
||||||
* enables line numbers would set `codeMirrorOptions` to
|
* line numbers would set `codeMirrorOptions` to `{'lineNumbers': true}`.
|
||||||
* `{'lineNumbers': true}`.
|
|
||||||
*
|
*
|
||||||
* - Using the **optional** `assets` key you may specify **only** CSS
|
* - Using the **optional** `assets` key you may specify **only** CSS assets
|
||||||
* assets that should be loaded in the rendered HTML document. Check
|
* that should be loaded in the rendered HTML document. Check for example
|
||||||
* for example the Joplin [Mermaid
|
* the Joplin [Mermaid
|
||||||
* plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
|
* plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
|
||||||
* to see how the data should be structured.
|
* to see how the data should be structured.
|
||||||
*
|
*
|
||||||
* One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions`
|
* One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions` keys
|
||||||
* keys must be provided for the plugin to be valid. Having multiple or
|
* must be provided for the plugin to be valid. Having multiple or all
|
||||||
* all provided is also okay.
|
* 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)
|
* 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.
|
* 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',
|
CodeMirrorPlugin = 'codeMirrorPlugin',
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { PluginStates } from '../reducer';
|
import { PluginStates } from '../reducer';
|
||||||
import { ContentScriptType } from '../api/types';
|
import { ContentScriptType, ContentScriptContext, PostMessageHandler } from '../api/types';
|
||||||
import { dirname } from '@joplin/renderer/pathUtils';
|
import { dirname } from '@joplin/renderer/pathUtils';
|
||||||
import shim from '../../../shim';
|
import shim from '../../../shim';
|
||||||
import Logger from '../../../Logger';
|
import Logger from '../../../Logger';
|
||||||
|
import PluginService from '../PluginService';
|
||||||
|
|
||||||
const logger = Logger.create('loadContentScripts');
|
const logger = Logger.create('loadContentScripts');
|
||||||
|
|
||||||
@ -12,6 +13,17 @@ export interface ExtraContentScript {
|
|||||||
assetPath: string;
|
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[] {
|
export function contentScriptsToRendererRules(plugins: PluginStates): ExtraContentScript[] {
|
||||||
return loadContentScripts(plugins, ContentScriptType.MarkdownItPlugin);
|
return loadContentScripts(plugins, ContentScriptType.MarkdownItPlugin);
|
||||||
}
|
}
|
||||||
@ -35,7 +47,14 @@ function loadContentScripts(plugins: PluginStates, scriptType: ContentScriptType
|
|||||||
const module = shim.requireDynamic(contentScript.path);
|
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}`);
|
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}`);
|
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({
|
output.push({
|
||||||
|
Loading…
Reference in New Issue
Block a user