chore: iFrame example

This commit is contained in:
mathuo 2023-06-11 15:48:37 +01:00
parent 7905af2945
commit ba3fe82c02
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
12 changed files with 430 additions and 22 deletions

View File

@ -13,6 +13,7 @@
"/packages/docs/sandboxes/externaldnd-dockview",
"/packages/docs/sandboxes/fullwidthtab-dockview",
"/packages/docs/sandboxes/groupcontol-dockview",
"/packages/docs/sandboxes/iframe-dockview",
"/packages/docs/sandboxes/layout-dockview",
"/packages/docs/sandboxes/nativeapp-dockview",
"/packages/docs/sandboxes/nested-dockview",
@ -29,4 +30,4 @@
"/packages/docs/sandboxes/javascript/vanilla-dockview"
],
"node": "16"
}
}

View File

@ -20,10 +20,6 @@ describe('abstractDragHandler', () => {
},
};
}
dispose(): void {
super.dispose();
}
})(element);
expect(element.classList.contains('dv-dragged')).toBeFalsy();
@ -62,10 +58,6 @@ describe('abstractDragHandler', () => {
},
};
}
dispose(): void {
//
}
})(element);
expect(iframe.style.pointerEvents).toBeFalsy();
@ -84,4 +76,46 @@ describe('abstractDragHandler', () => {
handler.dispose();
});
test('that the disabling of pointerEvents is restored on a premature disposal of the handler', () => {
jest.useFakeTimers();
const element = document.createElement('div');
const iframe = document.createElement('iframe');
const webview = document.createElement('webview');
const span = document.createElement('span');
document.body.appendChild(element);
document.body.appendChild(iframe);
document.body.appendChild(webview);
document.body.appendChild(span);
const handler = new (class TestClass extends DragHandler {
constructor(el: HTMLElement) {
super(el);
}
getData(): IDisposable {
return {
dispose: () => {
// /
},
};
}
})(element);
expect(iframe.style.pointerEvents).toBeFalsy();
expect(webview.style.pointerEvents).toBeFalsy();
expect(span.style.pointerEvents).toBeFalsy();
fireEvent.dragStart(element);
expect(iframe.style.pointerEvents).toBe('none');
expect(webview.style.pointerEvents).toBe('none');
expect(span.style.pointerEvents).toBeFalsy();
handler.dispose();
expect(iframe.style.pointerEvents).toBe('auto');
expect(webview.style.pointerEvents).toBe('auto');
expect(span.style.pointerEvents).toBeFalsy();
});
});

View File

@ -7,17 +7,20 @@ import {
} from '../lifecycle';
export abstract class DragHandler extends CompositeDisposable {
private readonly disposable = new MutableDisposable();
private readonly dataDisposable = new MutableDisposable();
private readonly pointerEventsDisposable = new MutableDisposable();
private readonly _onDragStart = new Emitter<void>();
readonly onDragStart = this._onDragStart.event;
private iframes: HTMLElement[] = [];
constructor(protected readonly el: HTMLElement) {
super();
this.addDisposables(this._onDragStart);
this.addDisposables(
this._onDragStart,
this.dataDisposable,
this.pointerEventsDisposable
);
this.configure();
}
@ -28,19 +31,27 @@ export abstract class DragHandler extends CompositeDisposable {
this.addDisposables(
this._onDragStart,
addDisposableListener(this.el, 'dragstart', (event) => {
this.iframes = [
const iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
for (const iframe of this.iframes) {
this.pointerEventsDisposable.value = {
dispose: () => {
for (const iframe of iframes) {
iframe.style.pointerEvents = 'auto';
}
},
};
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
this.el.classList.add('dv-dragged');
setTimeout(() => this.el.classList.remove('dv-dragged'), 0);
this.disposable.value = this.getData(event.dataTransfer);
this.dataDisposable.value = this.getData(event.dataTransfer);
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
@ -61,12 +72,8 @@ export abstract class DragHandler extends CompositeDisposable {
}
}),
addDisposableListener(this.el, 'dragend', () => {
for (const iframe of this.iframes) {
iframe.style.pointerEvents = 'auto';
}
this.iframes = [];
this.disposable.dispose();
this.pointerEventsDisposable.dispose();
this.dataDisposable.dispose();
})
);
}

View File

@ -27,6 +27,7 @@ import RenderingDockview from '@site/sandboxes/rendering-dockview/src/app';
import DockviewExternalDnd from '@site/sandboxes/externaldnd-dockview/src/app';
import DockviewResizeContainer from '@site/sandboxes/resizecontainer-dockview/src/app';
import DockviewTabheight from '@site/sandboxes/tabheight-dockview/src/app';
import DockviewWithIFrames from '@site/sandboxes/iframe-dockview/src/app';
import { attach as attachDockviewVanilla } from '@site/sandboxes/javascript/vanilla-dockview/src/app';
import { attach as attachSimpleDockview } from '@site/sandboxes/javascript/simple-dockview/src/app';
@ -739,6 +740,29 @@ api.group.api.setConstraints(...)
<DockviewConstraints />
</Container>
## iFrames
iFrames required special attention because of a particular behaviour in how iFrames render:
> Re-parenting an iFrame will reload the contents of the iFrame or the rephrase this, moving an iFrame within the DOM will cause a reload of its contents.
You can find many examples of discussions on this. Two reputable forums for example are linked [here](https://bugzilla.mozilla.org/show_bug.cgi?id=254144) and [here](https://github.com/whatwg/html/issues/5484).
The problem with iFrames and `dockview` is that when you hide or move a panel that panels DOM element may be moved within the DOM or removed from the DOM completely.
If your panel contains an iFrame then that iFrame will reload after being re-positioned within the DOM tree and all state in that iFrame will most likely be lost.
`dockview` does not provide a built-in solution to this because it's too specific of a problem to include in the library.
However the below example does show an implementation of a higher-order component `HoistedDockviewPanel`that you could use to work around this problems and make iFrames behave in `dockview`.
What the higher-order component is doing is to hoist the panels contents into a DOM element that is always present and then `position: absolute` that element to match the dimensions of it's linked panel.
The visibility of these hoisted elements is then controlled through some exposed api methods to hide elements that shouldn't be currently shown.
You should open this example in CodeSandbox using the provided link to understand the code and make use of this implemention if required.
<Container sandboxId="iframe-dockview" height={600}>
<DockviewWithIFrames />
</Container>
## Events
A simple example showing events fired by `dockviewz that can be interacted with.

View File

@ -0,0 +1,32 @@
{
"name": "iframe-dockview",
"description": "",
"keywords": [
"dockview"
],
"version": "1.0.0",
"main": "src/index.tsx",
"dependencies": {
"dockview": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"typescript": "^4.9.5",
"react-scripts": "*"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,61 @@
import {
DockviewReact,
DockviewReadyEvent,
IDockviewPanelProps,
} from 'dockview';
import * as React from 'react';
import { HoistedDockviewPanel } from './hoistedDockviewPanel';
const components = {
iframeComponent: HoistedDockviewPanel(
(props: IDockviewPanelProps<{ color: string }>) => {
return (
<iframe
style={{
border: 'none',
width: '100%',
height: '100%',
pointerEvents: 'auto',
}}
src="https://dockview.dev"
/>
);
}
),
basicComponent: () => {
return (
<div style={{ padding: '20px', color: 'white' }}>
{'This panel is just a usual component '}
</div>
);
},
};
export const App: React.FC = () => {
const onReady = (event: DockviewReadyEvent) => {
event.api.addPanel({
id: 'panel_1',
component: 'iframeComponent',
});
event.api.addPanel({
id: 'panel_2',
component: 'iframeComponent',
});
event.api.addPanel({
id: 'panel_3',
component: 'basicComponent',
});
};
return (
<DockviewReact
components={components}
onReady={onReady}
className="dockview-theme-abyss"
/>
);
};
export default App;

View File

@ -0,0 +1,128 @@
import { IDockviewPanelProps } from 'dockview';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
// watch an element for resize
function watchElementResize(
element: HTMLElement,
cb: (entry: ResizeObserverEntry) => void
): { dispose: () => void } {
const observer = new ResizeObserver((entires) => {
requestAnimationFrame(() => {
const firstEntry = entires[0];
cb(firstEntry);
});
});
observer.observe(element);
return {
dispose: () => {
observer.unobserve(element);
observer.disconnect();
},
};
}
// get absolute position of element allowing for scroll position
function getDomNodePagePosition(domNode: HTMLElement): {
left: number;
top: number;
width: number;
height: number;
} {
const { left, top, width, height } = domNode.getBoundingClientRect();
return {
left: left + window.scrollX,
top: top + window.scrollY,
width: width,
height: height,
};
}
function toggleVisibility(element: HTMLElement, isVisible: boolean) {
element.style.visibility = isVisible ? 'visible' : 'hidden';
}
export const HoistedDockviewPanel = <T extends object>(
DockviewPanelComponent: React.FC<IDockviewPanelProps<T>>
) => {
return (props: IDockviewPanelProps<T>) => {
const ref = React.useRef<HTMLDivElement>(null);
const innerRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!ref.current || !innerRef.current) {
return;
}
const positionHoistedPanel = () => {
if (!ref.current || !innerRef.current) {
return;
}
const { left, top, height, width } = getDomNodePagePosition(
ref.current.parentElement! // use the parent element to determine our size
);
innerRef.current.style.left = `${left}px`;
innerRef.current.style.top = `${top}px`;
innerRef.current.style.height = `${height}px`;
innerRef.current.style.width = `${width}px`;
};
const observer = watchElementResize(ref.current, (callback) => {
if (!ref.current || !innerRef.current) {
return;
}
positionHoistedPanel(); // since the dockview-panel has changed we must re-position the hoisted element
});
positionHoistedPanel(); // initial-paint because a resize may not yet have occured
return () => {
observer.dispose(); // cleanup
};
}, []);
React.useEffect(() => {
if (!innerRef.current) {
return;
}
const disposable = props.api.onDidVisibilityChange((event) => {
if (!innerRef.current) {
return;
}
toggleVisibility(innerRef.current, event.isVisible); // subsequent checks of visibility
});
toggleVisibility(innerRef.current, props.api.isVisible); // initial check of visibility
return () => {
disposable.dispose(); // cleanup
};
}, [props.api]);
return (
<div ref={ref}>
{ReactDOM.createPortal(
<div
/** you may want to mark these elements with some kind of attribute id */
ref={innerRef}
style={{
position: 'absolute',
overflow: 'hidden',
pointerEvents: 'none', // prevent this wrapper contain stealing events
}}
>
<DockviewPanelComponent {...props} />
</div>,
document.body // <-- you may choose to mount these 'global' elements to anywhere you see suitable
)}
</div>
);
};
};

View File

@ -0,0 +1,20 @@
import { StrictMode } from 'react';
import * as ReactDOMClient from 'react-dom/client';
import './styles.css';
import 'dockview/dist/styles/dockview.css';
import App from './app';
const rootElement = document.getElementById('root');
if (rootElement) {
const root = ReactDOMClient.createRoot(rootElement);
root.render(
<StrictMode>
<div className="app">
<App />
</div>
</StrictMode>
);
}

View File

@ -0,0 +1,15 @@
body {
margin: 0px;
color: white;
font-family: sans-serif;
text-align: center;
}
#root {
height: 100vh;
width: 100vw;
}
.app {
height: 100%;
}

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react-jsx",
"moduleResolution": "node",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}

View File

@ -27,6 +27,7 @@ import RenderingDockview from '@site/sandboxes/rendering-dockview/src/app';
import DockviewExternalDnd from '@site/sandboxes/externaldnd-dockview/src/app';
import DockviewResizeContainer from '@site/sandboxes/resizecontainer-dockview/src/app';
import DockviewTabheight from '@site/sandboxes/tabheight-dockview/src/app';
import DockviewWithIFrames from '@site/sandboxes/iframe-dockview/src/app';
import { attach as attachDockviewVanilla } from '@site/sandboxes/javascript/vanilla-dockview/src/app';
import { attach as attachSimpleDockview } from '@site/sandboxes/javascript/simple-dockview/src/app';
@ -739,6 +740,29 @@ api.group.api.setConstraints(...)
<DockviewConstraints />
</Container>
## iFrames
iFrames required special attention because of a particular behaviour in how iFrames render:
> Re-parenting an iFrame will reload the contents of the iFrame or the rephrase this, moving an iFrame within the DOM will cause a reload of its contents.
You can find many examples of discussions on this. Two reputable forums for example are linked [here](https://bugzilla.mozilla.org/show_bug.cgi?id=254144) and [here](https://github.com/whatwg/html/issues/5484).
The problem with iFrames and `dockview` is that when you hide or move a panel that panels DOM element may be moved within the DOM or removed from the DOM completely.
If your panel contains an iFrame then that iFrame will reload after being re-positioned within the DOM tree and all state in that iFrame will most likely be lost.
`dockview` does not provide a built-in solution to this because it's too specific of a problem to include in the library.
However the below example does show an implementation of a higher-order component `HoistedDockviewPanel`that you could use to work around this problems and make iFrames behave in `dockview`.
What the higher-order component is doing is to hoist the panels contents into a DOM element that is always present and then `position: absolute` that element to match the dimensions of it's linked panel.
The visibility of these hoisted elements is then controlled through some exposed api methods to hide elements that shouldn't be currently shown.
You should open this example in CodeSandbox using the provided link to understand the code and make use of this implemention if required.
<Container sandboxId="iframe-dockview" height={600}>
<DockviewWithIFrames />
</Container>
## Events
A simple example showing events fired by `dockviewz that can be interacted with.