mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +02:00
Plugins: Fixed issue with dialog being empty in some cases
This commit is contained in:
parent
a808281dd2
commit
adde092ea6
@ -763,12 +763,27 @@ packages/app-desktop/services/plugins/UserWebviewDialog.js.map
|
|||||||
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.d.ts
|
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.d.ts
|
||||||
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.js
|
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.js
|
||||||
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.js.map
|
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.js.map
|
||||||
|
packages/app-desktop/services/plugins/hooks/useContentSize.d.ts
|
||||||
|
packages/app-desktop/services/plugins/hooks/useContentSize.js
|
||||||
|
packages/app-desktop/services/plugins/hooks/useContentSize.js.map
|
||||||
|
packages/app-desktop/services/plugins/hooks/useHtmlLoader.d.ts
|
||||||
|
packages/app-desktop/services/plugins/hooks/useHtmlLoader.js
|
||||||
|
packages/app-desktop/services/plugins/hooks/useHtmlLoader.js.map
|
||||||
|
packages/app-desktop/services/plugins/hooks/useScriptLoader.d.ts
|
||||||
|
packages/app-desktop/services/plugins/hooks/useScriptLoader.js
|
||||||
|
packages/app-desktop/services/plugins/hooks/useScriptLoader.js.map
|
||||||
|
packages/app-desktop/services/plugins/hooks/useSubmitHandler.d.ts
|
||||||
|
packages/app-desktop/services/plugins/hooks/useSubmitHandler.js
|
||||||
|
packages/app-desktop/services/plugins/hooks/useSubmitHandler.js.map
|
||||||
packages/app-desktop/services/plugins/hooks/useThemeCss.d.ts
|
packages/app-desktop/services/plugins/hooks/useThemeCss.d.ts
|
||||||
packages/app-desktop/services/plugins/hooks/useThemeCss.js
|
packages/app-desktop/services/plugins/hooks/useThemeCss.js
|
||||||
packages/app-desktop/services/plugins/hooks/useThemeCss.js.map
|
packages/app-desktop/services/plugins/hooks/useThemeCss.js.map
|
||||||
packages/app-desktop/services/plugins/hooks/useViewIsReady.d.ts
|
packages/app-desktop/services/plugins/hooks/useViewIsReady.d.ts
|
||||||
packages/app-desktop/services/plugins/hooks/useViewIsReady.js
|
packages/app-desktop/services/plugins/hooks/useViewIsReady.js
|
||||||
packages/app-desktop/services/plugins/hooks/useViewIsReady.js.map
|
packages/app-desktop/services/plugins/hooks/useViewIsReady.js.map
|
||||||
|
packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.d.ts
|
||||||
|
packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.js
|
||||||
|
packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.js.map
|
||||||
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.d.ts
|
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.d.ts
|
||||||
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
|
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
|
||||||
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js.map
|
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js.map
|
||||||
|
15
.gitignore
vendored
15
.gitignore
vendored
@ -755,12 +755,27 @@ packages/app-desktop/services/plugins/UserWebviewDialog.js.map
|
|||||||
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.d.ts
|
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.d.ts
|
||||||
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.js
|
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.js
|
||||||
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.js.map
|
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.js.map
|
||||||
|
packages/app-desktop/services/plugins/hooks/useContentSize.d.ts
|
||||||
|
packages/app-desktop/services/plugins/hooks/useContentSize.js
|
||||||
|
packages/app-desktop/services/plugins/hooks/useContentSize.js.map
|
||||||
|
packages/app-desktop/services/plugins/hooks/useHtmlLoader.d.ts
|
||||||
|
packages/app-desktop/services/plugins/hooks/useHtmlLoader.js
|
||||||
|
packages/app-desktop/services/plugins/hooks/useHtmlLoader.js.map
|
||||||
|
packages/app-desktop/services/plugins/hooks/useScriptLoader.d.ts
|
||||||
|
packages/app-desktop/services/plugins/hooks/useScriptLoader.js
|
||||||
|
packages/app-desktop/services/plugins/hooks/useScriptLoader.js.map
|
||||||
|
packages/app-desktop/services/plugins/hooks/useSubmitHandler.d.ts
|
||||||
|
packages/app-desktop/services/plugins/hooks/useSubmitHandler.js
|
||||||
|
packages/app-desktop/services/plugins/hooks/useSubmitHandler.js.map
|
||||||
packages/app-desktop/services/plugins/hooks/useThemeCss.d.ts
|
packages/app-desktop/services/plugins/hooks/useThemeCss.d.ts
|
||||||
packages/app-desktop/services/plugins/hooks/useThemeCss.js
|
packages/app-desktop/services/plugins/hooks/useThemeCss.js
|
||||||
packages/app-desktop/services/plugins/hooks/useThemeCss.js.map
|
packages/app-desktop/services/plugins/hooks/useThemeCss.js.map
|
||||||
packages/app-desktop/services/plugins/hooks/useViewIsReady.d.ts
|
packages/app-desktop/services/plugins/hooks/useViewIsReady.d.ts
|
||||||
packages/app-desktop/services/plugins/hooks/useViewIsReady.js
|
packages/app-desktop/services/plugins/hooks/useViewIsReady.js
|
||||||
packages/app-desktop/services/plugins/hooks/useViewIsReady.js.map
|
packages/app-desktop/services/plugins/hooks/useViewIsReady.js.map
|
||||||
|
packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.d.ts
|
||||||
|
packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.js
|
||||||
|
packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.js.map
|
||||||
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.d.ts
|
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.d.ts
|
||||||
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
|
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
|
||||||
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js.map
|
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js.map
|
||||||
|
@ -27,7 +27,7 @@ joplin.plugins.register({
|
|||||||
const result2 = await dialogs.open(handle2);
|
const result2 = await dialogs.open(handle2);
|
||||||
alert('Got result: ' + JSON.stringify(result2));
|
alert('Got result: ' + JSON.stringify(result2));
|
||||||
|
|
||||||
const handle3 = await dialogs.create();
|
const handle3 = await dialogs.create('myDialog3');
|
||||||
await dialogs.setHtml(handle3, `
|
await dialogs.setHtml(handle3, `
|
||||||
<p>Testing dialog with form elements</p>
|
<p>Testing dialog with form elements</p>
|
||||||
<form name="user">
|
<form name="user">
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useRef, useEffect, useState, useImperativeHandle, forwardRef } from 'react';
|
import { useRef, useImperativeHandle, forwardRef } from 'react';
|
||||||
import useViewIsReady from './hooks/useViewIsReady';
|
import useViewIsReady from './hooks/useViewIsReady';
|
||||||
import useThemeCss from './hooks/useThemeCss';
|
import useThemeCss from './hooks/useThemeCss';
|
||||||
|
import useContentSize from './hooks/useContentSize';
|
||||||
|
import useSubmitHandler from './hooks/useSubmitHandler';
|
||||||
|
import useHtmlLoader from './hooks/useHtmlLoader';
|
||||||
|
import useWebviewToPluginMessages from './hooks/useWebviewToPluginMessages';
|
||||||
|
import useScriptLoader from './hooks/useScriptLoader';
|
||||||
const styled = require('styled-components').default;
|
const styled = require('styled-components').default;
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -16,11 +21,8 @@ export interface Props {
|
|||||||
fitToContent?: boolean;
|
fitToContent?: boolean;
|
||||||
borderBottom?: boolean;
|
borderBottom?: boolean;
|
||||||
theme?: any;
|
theme?: any;
|
||||||
}
|
onSubmit?: any;
|
||||||
|
onDismiss?: any;
|
||||||
interface Size {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledFrame = styled.iframe`
|
const StyledFrame = styled.iframe`
|
||||||
@ -61,19 +63,6 @@ function UserWebview(props: Props, ref: any) {
|
|||||||
const viewRef = useRef(null);
|
const viewRef = useRef(null);
|
||||||
const isReady = useViewIsReady(viewRef);
|
const isReady = useViewIsReady(viewRef);
|
||||||
const cssFilePath = useThemeCss({ pluginId: props.pluginId, themeId: props.themeId });
|
const cssFilePath = useThemeCss({ pluginId: props.pluginId, themeId: props.themeId });
|
||||||
const [contentSize, setContentSize] = useState<Size>({ width: minWidth, height: minHeight });
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => {
|
|
||||||
return {
|
|
||||||
formData: function() {
|
|
||||||
if (viewRef.current) {
|
|
||||||
return serializeForms(viewRef.current.contentWindow.document);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function frameWindow() {
|
function frameWindow() {
|
||||||
if (!viewRef.current) return null;
|
if (!viewRef.current) return null;
|
||||||
@ -86,86 +75,54 @@ function UserWebview(props: Props, ref: any) {
|
|||||||
win.postMessage({ target: 'webview', name, args }, '*');
|
win.postMessage({ target: 'webview', name, args }, '*');
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateContentSize() {
|
useImperativeHandle(ref, () => {
|
||||||
const win = frameWindow();
|
return {
|
||||||
if (!win) return null;
|
formData: function() {
|
||||||
|
if (viewRef.current) {
|
||||||
const rect = win.document.getElementById('joplin-plugin-content').getBoundingClientRect();
|
return serializeForms(frameWindow().document);
|
||||||
|
} else {
|
||||||
let w = rect.width;
|
return null;
|
||||||
let h = rect.height;
|
}
|
||||||
if (w < minWidth) w = minWidth;
|
},
|
||||||
if (h < minHeight) h = minHeight;
|
};
|
||||||
|
|
||||||
const newSize = { width: w, height: h };
|
|
||||||
|
|
||||||
setContentSize((current: Size) => {
|
|
||||||
if (current.width === newSize.width && current.height === newSize.height) return current;
|
|
||||||
return newSize;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return newSize;
|
const htmlHash = useHtmlLoader(
|
||||||
}
|
frameWindow(),
|
||||||
|
isReady,
|
||||||
|
postMessage,
|
||||||
|
props.html
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const contentSize = useContentSize(
|
||||||
if (!isReady) return () => {};
|
frameWindow(),
|
||||||
let cancelled = false;
|
htmlHash,
|
||||||
postMessage('setHtml', { html: props.html });
|
minWidth,
|
||||||
|
minHeight,
|
||||||
|
props.fitToContent,
|
||||||
|
isReady
|
||||||
|
);
|
||||||
|
|
||||||
setTimeout(() => {
|
useSubmitHandler(
|
||||||
if (cancelled) return;
|
frameWindow(),
|
||||||
updateContentSize();
|
props.onSubmit,
|
||||||
}, 100);
|
props.onDismiss,
|
||||||
|
htmlHash
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
useWebviewToPluginMessages(
|
||||||
cancelled = true;
|
frameWindow(),
|
||||||
};
|
props.onMessage,
|
||||||
}, [props.html, isReady]);
|
props.pluginId,
|
||||||
|
props.viewId
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useScriptLoader(
|
||||||
if (!isReady) return;
|
postMessage,
|
||||||
postMessage('setScripts', { scripts: props.scripts });
|
isReady,
|
||||||
}, [props.scripts, isReady]);
|
props.scripts,
|
||||||
|
cssFilePath
|
||||||
useEffect(() => {
|
);
|
||||||
if (!isReady || !cssFilePath) return;
|
|
||||||
postMessage('setScript', { script: cssFilePath, key: 'themeCss' });
|
|
||||||
}, [isReady, cssFilePath]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function onMessage(event: any) {
|
|
||||||
if (!event.data || event.data.target !== 'plugin') return;
|
|
||||||
props.onMessage({
|
|
||||||
pluginId: props.pluginId,
|
|
||||||
viewId: props.viewId,
|
|
||||||
message: event.data.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
viewRef.current.contentWindow.addEventListener('message', onMessage);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
viewRef.current.contentWindow.removeEventListener('message', onMessage);
|
|
||||||
};
|
|
||||||
}, [props.onMessage, props.pluginId, props.viewId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!props.fitToContent || !isReady) return () => {};
|
|
||||||
|
|
||||||
// The only reliable way to make sure that the iframe has the same dimensions
|
|
||||||
// as its content is to poll the dimensions at regular intervals. Other methods
|
|
||||||
// work most of the time but will fail in various edge cases. Most reliable way
|
|
||||||
// is probably iframe-resizer package, but still with 40 unfixed bugs.
|
|
||||||
//
|
|
||||||
// Polling in our case is fine since this is only used when displaying plugin
|
|
||||||
// dialogs, which should be short lived. updateContentSize() is also optimised
|
|
||||||
// to do nothing when size hasn't changed.
|
|
||||||
const updateFrameSizeIID = setInterval(updateContentSize, 2000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(updateFrameSizeIID);
|
|
||||||
};
|
|
||||||
}, [props.fitToContent, isReady, minWidth, minHeight]);
|
|
||||||
|
|
||||||
return <StyledFrame
|
return <StyledFrame
|
||||||
id={props.viewId}
|
id={props.viewId}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useRef, useCallback } from 'react';
|
||||||
import { ButtonSpec, DialogResult } from '@joplin/lib/services/plugins/api/types';
|
import { ButtonSpec, DialogResult } from '@joplin/lib/services/plugins/api/types';
|
||||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||||
import WebviewController from '@joplin/lib/services/plugins/WebviewController';
|
import WebviewController from '@joplin/lib/services/plugins/WebviewController';
|
||||||
import * as React from 'react';
|
|
||||||
import { useRef } from 'react';
|
|
||||||
import UserWebview, { Props as UserWebviewProps } from './UserWebview';
|
import UserWebview, { Props as UserWebviewProps } from './UserWebview';
|
||||||
import UserWebviewDialogButtonBar from './UserWebviewDialogButtonBar';
|
import UserWebviewDialogButtonBar from './UserWebviewDialogButtonBar';
|
||||||
const styled = require('styled-components').default;
|
const styled = require('styled-components').default;
|
||||||
@ -49,6 +49,18 @@ function defaultButtons(): ButtonSpec[] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findSubmitButton(buttons: ButtonSpec[]): ButtonSpec | null {
|
||||||
|
return buttons.find((b: ButtonSpec) => {
|
||||||
|
return ['ok', 'yes', 'confirm', 'submit'].includes(b.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDismissButton(buttons: ButtonSpec[]): ButtonSpec | null {
|
||||||
|
return buttons.find((b: ButtonSpec) => {
|
||||||
|
return ['cancel', 'no', 'reject'].includes(b.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default function UserWebviewDialog(props: Props) {
|
export default function UserWebviewDialog(props: Props) {
|
||||||
const webviewRef = useRef(null);
|
const webviewRef = useRef(null);
|
||||||
|
|
||||||
@ -68,6 +80,20 @@ export default function UserWebviewDialog(props: Props) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onSubmit = useCallback(() => {
|
||||||
|
const submitButton = findSubmitButton(buttons);
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.onClick();
|
||||||
|
}
|
||||||
|
}, [buttons]);
|
||||||
|
|
||||||
|
const onDismiss = useCallback(() => {
|
||||||
|
const dismissButton = findDismissButton(buttons);
|
||||||
|
if (dismissButton) {
|
||||||
|
dismissButton.onClick();
|
||||||
|
}
|
||||||
|
}, [buttons]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRoot>
|
<StyledRoot>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
@ -82,6 +108,8 @@ export default function UserWebviewDialog(props: Props) {
|
|||||||
themeId={props.themeId}
|
themeId={props.themeId}
|
||||||
borderBottom={false}
|
borderBottom={false}
|
||||||
fitToContent={true}
|
fitToContent={true}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onDismiss={onDismiss}
|
||||||
/>
|
/>
|
||||||
</UserWebViewWrapper>
|
</UserWebViewWrapper>
|
||||||
<UserWebviewDialogButtonBar buttons={buttons}/>
|
<UserWebviewDialogButtonBar buttons={buttons}/>
|
||||||
|
@ -57,6 +57,13 @@ const webviewApi = {
|
|||||||
const ipc = {
|
const ipc = {
|
||||||
setHtml: (args) => {
|
setHtml: (args) => {
|
||||||
contentElement.innerHTML = args.html;
|
contentElement.innerHTML = args.html;
|
||||||
|
|
||||||
|
console.debug('UserWebView frame: setting html to', args.html);
|
||||||
|
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
console.debug('UserWebView frame: setting html callback', args.hash);
|
||||||
|
window.postMessage({ target: 'UserWebview', message: 'htmlIsSet', hash: args.hash }, '*');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setScript: (args) => {
|
setScript: (args) => {
|
||||||
@ -102,8 +109,14 @@ const webviewApi = {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Send a message to the containing component to notify
|
// Send a message to the containing component to notify it that the
|
||||||
// it that the view content is fully ready.
|
// view content is fully ready.
|
||||||
|
//
|
||||||
|
// Need to send it with a delay to make sure all listeners are
|
||||||
|
// ready when the message is sent.
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
console.debug('UserWebView frame: calling isReady');
|
||||||
window.postMessage({ target: 'UserWebview', message: 'ready' }, '*');
|
window.postMessage({ target: 'UserWebview', message: 'ready' }, '*');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface Size {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function(frameWindow: any, htmlHash: string, minWidth: number, minHeight: number, fitToContent: boolean, isReady: boolean) {
|
||||||
|
const [contentSize, setContentSize] = useState<Size>({
|
||||||
|
width: minWidth,
|
||||||
|
height: minHeight,
|
||||||
|
hash: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateContentSize(hash: string) {
|
||||||
|
if (!frameWindow) return;
|
||||||
|
|
||||||
|
const rect = frameWindow.document.getElementById('joplin-plugin-content').getBoundingClientRect();
|
||||||
|
|
||||||
|
let w = rect.width;
|
||||||
|
let h = rect.height;
|
||||||
|
if (w < minWidth) w = minWidth;
|
||||||
|
if (h < minHeight) h = minHeight;
|
||||||
|
|
||||||
|
const newSize = { width: w, height: h, hash: hash };
|
||||||
|
|
||||||
|
setContentSize((current: Size) => {
|
||||||
|
if (current.width === newSize.width && current.height === newSize.height && current.hash === hash) return current;
|
||||||
|
return newSize;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateContentSize(htmlHash);
|
||||||
|
}, [htmlHash]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fitToContent || !isReady) return () => {};
|
||||||
|
|
||||||
|
function onTick() {
|
||||||
|
updateContentSize(htmlHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The only reliable way to make sure that the iframe has the same dimensions
|
||||||
|
// as its content is to poll the dimensions at regular intervals. Other methods
|
||||||
|
// work most of the time but will fail in various edge cases. Most reliable way
|
||||||
|
// is probably iframe-resizer package, but still with 40 unfixed bugs.
|
||||||
|
//
|
||||||
|
// Polling in our case is fine since this is only used when displaying plugin
|
||||||
|
// dialogs, which should be short lived. updateContentSize() is also optimised
|
||||||
|
// to do nothing when size hasn't changed.
|
||||||
|
const updateFrameSizeIID = setInterval(onTick, 100);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(updateFrameSizeIID);
|
||||||
|
};
|
||||||
|
}, [fitToContent, isReady, minWidth, minHeight, htmlHash]);
|
||||||
|
|
||||||
|
return contentSize;
|
||||||
|
}
|
51
packages/app-desktop/services/plugins/hooks/useHtmlLoader.ts
Normal file
51
packages/app-desktop/services/plugins/hooks/useHtmlLoader.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
|
const md5 = require('md5');
|
||||||
|
|
||||||
|
export default function(frameWindow: any, isReady: boolean, postMessage: Function, html: string) {
|
||||||
|
const [loadedHtmlHash, setLoadedHtmlHash] = useState('');
|
||||||
|
|
||||||
|
const htmlHash = useMemo(() => {
|
||||||
|
return md5(html);
|
||||||
|
}, [html]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!frameWindow) return () => {};
|
||||||
|
|
||||||
|
function onMessage(event: any) {
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
if (!data || data.target !== 'UserWebview') return;
|
||||||
|
|
||||||
|
console.info('useHtmlLoader: message', data);
|
||||||
|
|
||||||
|
// We only update if the HTML that was loaded is the same as
|
||||||
|
// the active one. Otherwise it means the content has been
|
||||||
|
// changed between the moment it was set by the user and the
|
||||||
|
// moment it was loaded in the view.
|
||||||
|
if (data.message === 'htmlIsSet' && data.hash === htmlHash) {
|
||||||
|
setLoadedHtmlHash(data.hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frameWindow.addEventListener('message', onMessage);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
frameWindow.removeEventListener('message', onMessage);
|
||||||
|
};
|
||||||
|
}, [frameWindow, htmlHash]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.info('useHtmlLoader: isReady', isReady);
|
||||||
|
|
||||||
|
if (!isReady) return;
|
||||||
|
|
||||||
|
console.info('useHtmlLoader: setHtml', htmlHash, html);
|
||||||
|
|
||||||
|
postMessage('setHtml', {
|
||||||
|
hash: htmlHash,
|
||||||
|
html: html,
|
||||||
|
});
|
||||||
|
}, [html, htmlHash, isReady]);
|
||||||
|
|
||||||
|
return loadedHtmlHash;
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function(postMessage: Function, isReady: boolean, scripts: string[], cssFilePath: string) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isReady) return;
|
||||||
|
postMessage('setScripts', { scripts: scripts });
|
||||||
|
}, [scripts, isReady]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isReady || !cssFilePath) return;
|
||||||
|
postMessage('setScript', { script: cssFilePath, key: 'themeCss' });
|
||||||
|
}, [isReady, cssFilePath]);
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function(frameWindow: any, onSubmit: Function, onDismiss: Function, loadedHtmlHash: string) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!frameWindow) return () => {};
|
||||||
|
|
||||||
|
function onFormSubmit(event: any) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (onSubmit) onSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyDown(event: any) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
if (onDismiss) onDismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
if (onSubmit) onSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frameWindow.document.addEventListener('submit', onFormSubmit);
|
||||||
|
frameWindow.document.addEventListener('keydown', onKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (frameWindow) frameWindow.document.removeEventListener('submit', onFormSubmit);
|
||||||
|
if (frameWindow) frameWindow.document.removeEventListener('keydown', onKeyDown);
|
||||||
|
};
|
||||||
|
}, [frameWindow, loadedHtmlHash, onSubmit, onDismiss]);
|
||||||
|
}
|
@ -9,7 +9,10 @@ export default function useViewIsReady(viewRef: any) {
|
|||||||
const [iframeContentReady, setIFrameContentReady] = useState(false);
|
const [iframeContentReady, setIFrameContentReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.debug('useViewIsReady ============== Setup Listeners');
|
||||||
|
|
||||||
function onIFrameReady() {
|
function onIFrameReady() {
|
||||||
|
console.debug('useViewIsReady: onIFrameReady');
|
||||||
setIFrameReady(true);
|
setIFrameReady(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,6 +21,8 @@ export default function useViewIsReady(viewRef: any) {
|
|||||||
|
|
||||||
if (!data || data.target !== 'UserWebview') return;
|
if (!data || data.target !== 'UserWebview') return;
|
||||||
|
|
||||||
|
console.debug('useViewIsReady: message', data);
|
||||||
|
|
||||||
if (data.message === 'ready') {
|
if (data.message === 'ready') {
|
||||||
setIFrameContentReady(true);
|
setIFrameContentReady(true);
|
||||||
}
|
}
|
||||||
@ -25,6 +30,8 @@ export default function useViewIsReady(viewRef: any) {
|
|||||||
|
|
||||||
const iframeDocument = viewRef.current.contentWindow.document;
|
const iframeDocument = viewRef.current.contentWindow.document;
|
||||||
|
|
||||||
|
console.debug('useViewIsReady readyState', iframeDocument.readyState);
|
||||||
|
|
||||||
if (iframeDocument.readyState === 'complete') {
|
if (iframeDocument.readyState === 'complete') {
|
||||||
onIFrameReady();
|
onIFrameReady();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function(frameWindow: any, onMessage: Function, pluginId: string, viewId: string) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!frameWindow) return () => {};
|
||||||
|
|
||||||
|
function onMessage(event: any) {
|
||||||
|
if (!event.data || event.data.target !== 'plugin') return;
|
||||||
|
onMessage({
|
||||||
|
pluginId: pluginId,
|
||||||
|
viewId: viewId,
|
||||||
|
message: event.data.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
frameWindow.addEventListener('message', onMessage);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
frameWindow.removeEventListener('message', onMessage);
|
||||||
|
};
|
||||||
|
}, [onMessage, pluginId, viewId]);
|
||||||
|
}
|
@ -4,12 +4,33 @@ import WebviewController, { ContainerType } from '../WebviewController';
|
|||||||
import { ButtonSpec, ViewHandle, DialogResult } from './types';
|
import { ButtonSpec, ViewHandle, DialogResult } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows creating and managing dialogs. A dialog is modal window that contains a webview and a row of buttons. You can update the update the webview using the `setHtml` method.
|
* Allows creating and managing dialogs. A dialog is modal window that
|
||||||
* Dialogs are hidden by default and you need to call `open()` to open them. Once the user clicks on a button, the `open` call will return an object indicating what button was clicked
|
* contains a webview and a row of buttons. You can update the update the
|
||||||
* on. If your HTML content included one or more form, a `formData` object will also be included with the key/value for each form.
|
* webview using the `setHtml` method. Dialogs are hidden by default and
|
||||||
* There is currently no "close" method since the dialog should be thought as a modal one and thus can only be closed by clicking on one of the buttons.
|
* you need to call `open()` to open them. Once the user clicks on a
|
||||||
|
* button, the `open` call will return an object indicating what button was
|
||||||
|
* clicked on.
|
||||||
*
|
*
|
||||||
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/dialog)
|
* ## Retrieving form values
|
||||||
|
*
|
||||||
|
* If your HTML content included one or more forms, a `formData` object
|
||||||
|
* will also be included with the key/value for each form.
|
||||||
|
*
|
||||||
|
* ## Special button IDs
|
||||||
|
*
|
||||||
|
* The following buttons IDs have a special meaning:
|
||||||
|
*
|
||||||
|
* - `ok`, `yes`, `submit`, `confirm`: They are considered "submit" buttons
|
||||||
|
* - `cancel`, `no`, `reject`: They are considered "dismiss" buttons
|
||||||
|
*
|
||||||
|
* This information is used by the application to determine what action
|
||||||
|
* should be done when the user presses "Enter" or "Escape" within the
|
||||||
|
* dialog. If they press "Enter", the first "submit" button will be
|
||||||
|
* automatically clicked. If they press "Escape" the first "dismiss" button
|
||||||
|
* will be automatically clicked.
|
||||||
|
*
|
||||||
|
* [View the demo
|
||||||
|
* plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/dialog)
|
||||||
*/
|
*/
|
||||||
export default class JoplinViewsDialogs {
|
export default class JoplinViewsDialogs {
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user