1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Mobile: Plugin API: Improve support for the Kanban and similar plugins (#10247)

This commit is contained in:
Henry Heino 2024-04-03 10:56:54 -07:00 committed by GitHub
parent a301470ac5
commit 7caed19a32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 158 additions and 21 deletions

View File

@ -605,6 +605,8 @@ packages/app-mobile/plugins/PluginRunner/backgroundPage/initializeDialogWebView.
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializePluginBackgroundIframe.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/startStopPlugin.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.test.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/makeSandboxedIframe.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/reportUnhandledErrors.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/wrapConsoleLog.js

2
.gitignore vendored
View File

@ -585,6 +585,8 @@ packages/app-mobile/plugins/PluginRunner/backgroundPage/initializeDialogWebView.
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializePluginBackgroundIframe.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/startStopPlugin.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.test.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/makeSandboxedIframe.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/reportUnhandledErrors.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/wrapConsoleLog.js

View File

@ -2,6 +2,7 @@ import { DialogWebViewApi, DialogMainProcessApi } from '../types';
import reportUnhandledErrors from './utils/reportUnhandledErrors';
import wrapConsoleLog from './utils/wrapConsoleLog';
import WebViewToRNMessenger from '../../../utils/ipc/WebViewToRNMessenger';
import getFormData from './utils/getFormData';
let themeCssElement: HTMLStyleElement|null = null;
@ -37,16 +38,7 @@ const initializeDialogWebView = (messageChannelId: string) => {
return includeScriptsOrStyles('js', paths);
},
getFormData: async () => {
const firstForm = document.querySelector('form');
if (!firstForm) return null;
const formData = new FormData(firstForm);
const result = Object.create(null);
for (const key of formData.keys()) {
result[key] = formData.get(key);
}
return result;
return getFormData();
},
setThemeCss: async (css: string) => {
themeCssElement?.remove?.();
@ -60,9 +52,10 @@ const initializeDialogWebView = (messageChannelId: string) => {
// we need to multiply by the devicePixelRatio:
const dpr = window.devicePixelRatio ?? 1;
const element = document.getElementById('joplin-plugin-content') ?? document.body;
return {
width: document.body.clientWidth * dpr,
height: document.body.clientHeight * dpr,
width: element.clientWidth * dpr,
height: element.clientHeight * dpr,
};
},
};

View File

@ -4,6 +4,7 @@ export { runPlugin, stopPlugin } from './startStopPlugin';
const legacyPluginIds = [
'outline',
'ylc395.noteLinkSystem',
'com.github.joplin.kanban',
];
const pathLibrary = require('path');
@ -28,6 +29,7 @@ export const requireModule = (moduleName: string, fromPluginId: string) => {
readFile: () => '',
writeFileSync: () => '',
writeFile: () => '',
appendFile: () => '',
};
}
if (moduleName === 'process') {

View File

@ -0,0 +1,53 @@
import getFormData from './getFormData';
describe('getFormData', () => {
afterEach(() => {
// Remove all forms to prevent tests from conflicting -- getFormData
// uses document.querySelectorAll to select all forms.
document.body.replaceChildren();
});
const addTextInputWithValue = (parent: HTMLElement, value: string, name: string) => {
const input = document.createElement('input');
input.name = name;
input.value = value;
parent.appendChild(input);
};
test('should return data for all forms', () => {
const testForm1 = document.createElement('form');
testForm1.setAttribute('name', 'test-form-1');
addTextInputWithValue(testForm1, 'Test', 'input');
addTextInputWithValue(testForm1, 'Test2', 'another-input');
document.body.appendChild(testForm1);
const testForm2 = document.createElement('form');
testForm2.setAttribute('name', 'another-test-form');
addTextInputWithValue(testForm2, 'Test2', 'text-input');
document.body.appendChild(testForm2);
expect(getFormData()).toMatchObject({
'test-form-1': {
'input': 'Test',
'another-input': 'Test2',
},
'another-test-form': {
'text-input': 'Test2',
},
});
});
test('should auto-number forms without a name', () => {
for (let i = 0; i < 3; i++) {
const testForm = document.createElement('form');
addTextInputWithValue(testForm, `Form ${i}`, 'input');
document.body.appendChild(testForm);
}
const formData = getFormData();
expect(Object.keys(formData)).toHaveLength(3);
expect(formData['form-0']).toMatchObject({ 'input': 'Form 0' });
expect(formData['form-1']).toMatchObject({ 'input': 'Form 1' });
expect(formData['form-2']).toMatchObject({ 'input': 'Form 2' });
});
});

View File

@ -0,0 +1,24 @@
const getFormData = () => {
const forms = document.querySelectorAll('form');
if (forms.length === 0) return null;
const serializeForm = (form: HTMLFormElement) => {
const formData = new FormData(form);
const serializedData: Record<string, any> = {};
for (const key of formData.keys()) {
serializedData[key] = formData.get(key);
}
return serializedData;
};
const result = Object.create(null);
let untitledFormId = 0;
for (const form of forms) {
const formId = form.getAttribute('name') || `form-${untitledFormId++}`;
result[formId] = serializeForm(form);
}
return result;
};
export default getFormData;

View File

@ -33,6 +33,11 @@ const useStyles = (
const useDialogSize = fitToContent && dialogContentSize;
const dialogHasLoaded = !!dialogContentSize;
const maxWidth = windowSize.width * 0.97;
const maxHeight = windowSize.height * 0.95;
const dialogWidth = useDialogSize ? dialogContentSize.width : maxWidth;
const dialogHeight = useDialogSize ? dialogContentSize.height : maxHeight;
return StyleSheet.create({
webView: {
backgroundColor: 'transparent',
@ -41,15 +46,18 @@ const useStyles = (
webViewContainer: {
flexGrow: 1,
flexShrink: 1,
maxWidth,
maxHeight,
width: dialogWidth,
height: dialogHeight,
},
dialog: {
backgroundColor: theme.backgroundColor,
borderRadius: 12,
maxHeight: useDialogSize ? dialogContentSize?.height : undefined,
maxWidth: useDialogSize ? dialogContentSize?.width : undefined,
height: windowSize.height * 0.97,
width: windowSize.width * 0.97,
maxWidth,
maxHeight,
opacity: dialogHasLoaded ? 1 : 0.1,
// Center
@ -58,6 +66,7 @@ const useStyles = (
},
buttonRow: {
flexDirection: 'row',
flexShrink: 0,
padding: 12,
justifyContent: 'flex-end',
},
@ -81,6 +90,7 @@ const PluginDialogWebView: React.FC<Props> = props => {
const dialogSize = useDialogSize({
dialogControl,
webViewLoadCount,
watchForSizeChanges: view.fitToContent,
});
const styles = useStyles(props.themeId, dialogSize, view.fitToContent);

View File

@ -67,14 +67,30 @@ const PluginUserWebView = (props: Props) => {
<title>Plugin Dialog</title>
<style>
body {
padding: 20px;
box-sizing: border-box;
padding: 0;
margin: 0;
color: var(--joplin-color);
/* -apple-system-body allows for correct font scaling on iOS devices */
font: -apple-system-body;
font-family: var(--joplin-font-family, sans-serif);
}
/* We need "display: flex" in order to accurately get the content size */
/* including margin and padding of children */
#joplin-plugin-content {
display: flex;
flex-direction: column;
padding: 10px;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="joplin-plugin-content">
${htmlContent}
</div>
</body>
</html>
`;

View File

@ -1,26 +1,49 @@
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import { useState } from 'react';
import { useRef, useState } from 'react';
import { DialogContentSize, DialogWebViewApi } from '../../types';
import { PixelRatio } from 'react-native';
interface Props {
dialogControl: DialogWebViewApi;
webViewLoadCount: number;
watchForSizeChanges: boolean;
}
const useDialogSize = (props: Props) => {
const { dialogControl, webViewLoadCount } = props;
const [dialogSize, setDialogSize] = useState<DialogContentSize|null>(null);
const lastSizeRef = useRef(dialogSize);
lastSizeRef.current = dialogSize;
useAsyncEffect(async event => {
if (!dialogControl) {
// May happen if the webview is still loading.
return;
}
while (!event.cancelled) {
const contentSize = await dialogControl.getContentSize();
if (event.cancelled) return;
setDialogSize(contentSize);
const lastSize = lastSizeRef.current;
if (contentSize.width !== lastSize?.width || contentSize.height !== lastSize?.height) {
// We use 1000 here because getPixelSizeForLayoutSize is guaranteed to return
// an integer.
const pixelToDpRatio = 1000 / PixelRatio.getPixelSizeForLayoutSize(1000);
setDialogSize({
width: contentSize.width * pixelToDpRatio,
height: contentSize.height * pixelToDpRatio,
});
}
if (!props.watchForSizeChanges) return;
await new Promise<void>(resolve => {
setTimeout(() => {
resolve();
}, 500);
});
}
}, [dialogControl, setDialogSize, webViewLoadCount]);
return dialogSize;

View File

@ -85,6 +85,16 @@ const editorCommands: Record<EditorCommandType, EditorCommandFunction> = {
},
[EditorCommandType.InsertText]: replaceSelectionCommand,
[EditorCommandType.ReplaceSelection]: replaceSelectionCommand,
[EditorCommandType.SetText]: (editor, text: string) => {
editor.dispatch({
changes: [{
from: 0,
to: editor.state.doc.length,
insert: text,
}],
});
},
};
export default editorCommands;

View File

@ -70,6 +70,8 @@ export enum EditorCommandType {
SelectedText = 'selectedText',
InsertText = 'insertText',
ReplaceSelection = 'replaceSelection',
SetText = 'setText',
}
// Because the editor package can run in a WebView, plugin content scripts