Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Desktop: Resolves #8931: Improve support for plugins in the Rich Text Editor (implement webviewApi.postMesage) #10158

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Expand Up @@ -290,6 +290,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
packages/app-desktop/gui/NoteEditor/NoteEditor.js
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -270,6 +270,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
packages/app-desktop/gui/NoteEditor/NoteEditor.js
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
Expand Down
Expand Up @@ -3,7 +3,7 @@ const leftPad = require('left-pad');
export default function(context) {
return {
plugin: function(markdownIt, _options) {
const pluginId = context.pluginId;
const contentScriptId = context.contentScriptId;

const defaultRender = markdownIt.renderer.rules.fence || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options, env, self);
Expand All @@ -14,15 +14,30 @@ export default function(context) {
if (token.info !== 'justtesting') return defaultRender(tokens, idx, options, env, self);

const postMessageWithResponseTest = `
webviewApi.postMessage('${pluginId}', 'justtesting').then(function(response) {
webviewApi.postMessage('${contentScriptId}', 'justtesting').then(function(response) {
console.info('Got response in content script: ' + response);
});
return false;
`;

// Rich text editor support:
// The joplin-editable and joplin-source CSS classes mark the generated div
// as a region that needs special processing when converting back to markdown.
// This element helps Joplin reconstruct the original markdown.
const richTextEditorMetadata = `
<pre
class="joplin-source"
data-joplin-language="justtesting"
data-joplin-source-open="\`\`\`justtesting\n"
data-joplin-source-close="\`\`\`"
>${markdownIt.utils.escapeHtml(token.content)}</pre>
`;

return `
<div class="just-testing">
<p>JUST TESTING: <pre>${leftPad(token.content.trim(), 10, 'x')}</pre></p>
<div class="just-testing joplin-editable">
${richTextEditorMetadata}

<p>JUST TESTING: <pre>${markdownIt.utils.escapeHtml(leftPad(token.content.trim(), 10, 'x'))}</pre></p>
<p><a href="#" onclick="${postMessageWithResponseTest.replace(/\n/g, ' ')}">Click to post a message "justtesting" to plugin and check the response in the console</a></p>
</div>
`;
Expand Down
Expand Up @@ -31,6 +31,7 @@ import { Options as NoteStyleOptions } from '@joplin/renderer/noteStyle';
import markupRenderOptions from '../../utils/markupRenderOptions';
import { DropHandler } from '../../utils/useDropHandler';
import Logger from '@joplin/utils/Logger';
import useWebViewApi from './utils/useWebViewApi';
const md5 = require('md5');
const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales');
Expand Down Expand Up @@ -348,6 +349,8 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
};
}, []);

useWebViewApi(editor);

useEffect(() => {
const theme = themeStyle(props.themeId);
const backgroundColor = props.whiteBackgroundNoteRendering ? lightTheme.backgroundColor : theme.backgroundColor;
Expand Down
@@ -0,0 +1,72 @@
import PluginService from '@joplin/lib/services/plugins/PluginService';
import { useEffect } from 'react';
import { Editor } from 'tinymce';

const useWebViewApi = (editor: Editor) => {
useEffect(() => {
if (!editor) return ()=>{};

const scriptElement = document.createElement('script');
const channelId = `plugin-post-message-${Math.random()}`;
scriptElement.appendChild(document.createTextNode(`
window.webviewApi = {
postMessage: (contentScriptId, message) => {
const channelId = ${JSON.stringify(channelId)};
const messageId = Math.random();
window.parent.postMessage({
channelId,
messageId,
contentScriptId,
message,
}, '*');

const waitForResponse = async () => {
while (true) {
const messageEvent = await new Promise(resolve => {
window.addEventListener('message', event => {
resolve(event);
}, {once: true});
});

if (messageEvent.source !== window.parent || messageEvent.data.messageId !== messageId) {
continue;
}

const data = messageEvent.data;
return data.response;
}
};

return waitForResponse();
},
};
`));
const editorWindow = editor.getWin();
editorWindow.document.head.appendChild(scriptElement);

const onMessageHandler = async (event: MessageEvent) => {
if (event.source !== editorWindow || event.data.channelId !== channelId) {
return;
}

const contentScriptId = event.data.contentScriptId;
const pluginService = PluginService.instance();
const plugin = pluginService.pluginById(
pluginService.pluginIdByContentScriptId(contentScriptId),
);
const result = await plugin.emitContentScriptMessage(contentScriptId, event.data.message);
editorWindow.postMessage({
messageId: event.data.messageId,
response: result,
}, '*');
};
window.addEventListener('message', onMessageHandler);

return () => {
window.removeEventListener('message', onMessageHandler);
scriptElement.remove();
};
}, [editor]);
};

export default useWebViewApi;