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:
parent
a301470ac5
commit
7caed19a32
@ -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
2
.gitignore
vendored
@ -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
|
||||
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
@ -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') {
|
||||
|
@ -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' });
|
||||
});
|
||||
});
|
@ -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;
|
@ -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);
|
||||
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
const contentSize = await dialogControl.getContentSize();
|
||||
if (event.cancelled) 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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user