From 15393213200e9eb4b15d041803073bd0313a4bf5 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sun, 11 Jun 2023 13:21:32 +0100 Subject: [PATCH 01/19] test: add tests to existing mouse dnd functionality --- .../src/__tests__/splitview/splitview.spec.ts | 52 ++++++++++++++++++- .../dockview-core/src/splitview/splitview.ts | 2 - 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts b/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts index 23c9b1df8..7d14653d2 100644 --- a/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts +++ b/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts @@ -7,7 +7,7 @@ import { Sizing, Splitview, } from '../../splitview/splitview'; - +import { fireEvent } from '@testing-library/dom'; class Testview implements IView { private _element: HTMLElement = document.createElement('div'); private _size = 0; @@ -596,4 +596,54 @@ describe('splitview', () => { expect(anyEvents).toBeFalsy(); expect(container.childNodes.length).toBe(0); }); + + test('dnd: mouse events to move sash', () => { + const splitview = new Splitview(container, { + orientation: Orientation.HORIZONTAL, + proportionalLayout: false, + }); + splitview.layout(400, 500); + + const view1 = new Testview(0, 1000); + const view2 = new Testview(0, 1000); + + splitview.addView(view1); + splitview.addView(view2); + + const sashElement = container + .getElementsByClassName('sash') + .item(0) as HTMLElement; + + // validate the expected state before drag + expect([view1.size, view2.size]).toEqual([200, 200]); + expect(sashElement).toBeTruthy(); + expect(view1.element.parentElement!.style.pointerEvents).toBe(''); + expect(view2.element.parentElement!.style.pointerEvents).toBe(''); + + // start the drag event + fireEvent.mouseDown(sashElement, { clientX: 50, clientY: 100 }); + + // during a sash drag the views should have pointer-events disabled + expect(view1.element.parentElement!.style.pointerEvents).toBe('none'); + expect(view2.element.parentElement!.style.pointerEvents).toBe('none'); + + // expect a delta move of 70 - 50 = 20 + fireEvent.mouseMove(document, { clientX: 70, clientY: 110 }); + expect([view1.size, view2.size]).toEqual([220, 180]); + + // expect a delta move of 75 - 70 = 5 + fireEvent.mouseMove(document, { clientX: 75, clientY: 110 }); + expect([view1.size, view2.size]).toEqual([225, 175]); + + // end the drag event + fireEvent.mouseUp(document); + + // expect pointer-eventes on views to be restored + expect(view1.element.parentElement!.style.pointerEvents).toBe(''); + expect(view2.element.parentElement!.style.pointerEvents).toBe(''); + + fireEvent.mouseMove(document, { clientX: 100, clientY: 100 }); + // expect no additional resizes + expect([view1.size, view2.size]).toEqual([225, 175]); + }); }); diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index 48295a8c7..fd68a4a29 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -523,14 +523,12 @@ export class Splitview { document.removeEventListener('mousemove', mousemove); document.removeEventListener('mouseup', end); - document.removeEventListener('mouseend', end); this._onDidSashEnd.fire(undefined); }; document.addEventListener('mousemove', mousemove); document.addEventListener('mouseup', end); - document.addEventListener('mouseend', end); }; sash.addEventListener('mousedown', onStart); From a39c2938f02ef54e6a55d893874104930c3f1d81 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sun, 11 Jun 2023 14:31:28 +0100 Subject: [PATCH 02/19] test: listener utilities --- .../src/__tests__/events.spec.ts | 141 +++++++++++++++++- packages/dockview-core/src/events.ts | 4 +- 2 files changed, 142 insertions(+), 3 deletions(-) diff --git a/packages/dockview-core/src/__tests__/events.spec.ts b/packages/dockview-core/src/__tests__/events.spec.ts index 6ac17cc5c..532390a07 100644 --- a/packages/dockview-core/src/__tests__/events.spec.ts +++ b/packages/dockview-core/src/__tests__/events.spec.ts @@ -1,4 +1,9 @@ -import { Emitter, Event } from '../events'; +import { + Emitter, + Event, + addDisposableListener, + addDisposableWindowListener, +} from '../events'; describe('events', () => { describe('emitter', () => { @@ -101,4 +106,138 @@ describe('events', () => { emitter3.fire(3); expect(value).toBe(3); }); + + it('addDisposableWindowListener with capture options', () => { + const element = { + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }; + + const handler = jest.fn(); + + const disposable = addDisposableWindowListener( + element as any, + 'mousedown', + handler, + true + ); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.addEventListener).toHaveBeenCalledWith( + 'mousedown', + handler, + true + ); + expect(element.removeEventListener).toBeCalledTimes(0); + + disposable.dispose(); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledWith( + 'mousedown', + handler, + true + ); + }); + + it('addDisposableWindowListener without capture options', () => { + const element = { + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }; + + const handler = jest.fn(); + + const disposable = addDisposableWindowListener( + element as any, + 'mousedown', + handler + ); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.addEventListener).toHaveBeenCalledWith( + 'mousedown', + handler, + undefined + ); + expect(element.removeEventListener).toBeCalledTimes(0); + + disposable.dispose(); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledWith( + 'mousedown', + handler, + undefined + ); + }); + + it('addDisposableListener with capture options', () => { + const element = { + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }; + + const handler = jest.fn(); + + const disposable = addDisposableListener( + element as any, + 'mousedown', + handler, + true + ); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.addEventListener).toHaveBeenCalledWith( + 'mousedown', + handler, + true + ); + expect(element.removeEventListener).toBeCalledTimes(0); + + disposable.dispose(); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledWith( + 'mousedown', + handler, + true + ); + }); + + it('addDisposableListener without capture options', () => { + const element = { + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }; + + const handler = jest.fn(); + + const disposable = addDisposableListener( + element as any, + 'mousedown', + handler + ); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.addEventListener).toHaveBeenCalledWith( + 'mousedown', + handler, + undefined + ); + expect(element.removeEventListener).toBeCalledTimes(0); + + disposable.dispose(); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledWith( + 'mousedown', + handler, + undefined + ); + }); }); diff --git a/packages/dockview-core/src/events.ts b/packages/dockview-core/src/events.ts index 91f27a1b1..cb8d95930 100644 --- a/packages/dockview-core/src/events.ts +++ b/packages/dockview-core/src/events.ts @@ -162,7 +162,7 @@ export function addDisposableWindowListener( return { dispose: () => { - element.removeEventListener(type, listener); + element.removeEventListener(type, listener, options); }, }; } @@ -177,7 +177,7 @@ export function addDisposableListener( return { dispose: () => { - element.removeEventListener(type, listener); + element.removeEventListener(type, listener, options); }, }; } From ba3fe82c02f066415f786da50c26292b8b954bf4 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sun, 11 Jun 2023 15:48:37 +0100 Subject: [PATCH 03/19] chore: iFrame example --- .codesandbox/ci.json | 3 +- .../__tests__/dnd/abstractDragHandler.spec.ts | 50 +++++-- .../src/dnd/abstractDragHandler.ts | 33 +++-- packages/docs/docs/components/dockview.mdx | 24 ++++ .../sandboxes/iframe-dockview/package.json | 32 +++++ .../iframe-dockview/public/index.html | 44 ++++++ .../sandboxes/iframe-dockview/src/app.tsx | 61 +++++++++ .../src/hoistedDockviewPanel.tsx | 128 ++++++++++++++++++ .../sandboxes/iframe-dockview/src/index.tsx | 20 +++ .../sandboxes/iframe-dockview/src/styles.css | 15 ++ .../sandboxes/iframe-dockview/tsconfig.json | 18 +++ .../version-1.7.4/components/dockview.mdx | 24 ++++ 12 files changed, 430 insertions(+), 22 deletions(-) create mode 100644 packages/docs/sandboxes/iframe-dockview/package.json create mode 100644 packages/docs/sandboxes/iframe-dockview/public/index.html create mode 100644 packages/docs/sandboxes/iframe-dockview/src/app.tsx create mode 100644 packages/docs/sandboxes/iframe-dockview/src/hoistedDockviewPanel.tsx create mode 100644 packages/docs/sandboxes/iframe-dockview/src/index.tsx create mode 100644 packages/docs/sandboxes/iframe-dockview/src/styles.css create mode 100644 packages/docs/sandboxes/iframe-dockview/tsconfig.json diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 856df6dc8..f04bc5e88 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -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" -} +} \ No newline at end of file diff --git a/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts b/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts index ebb55aae7..312916ae5 100644 --- a/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts @@ -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(); + }); }); diff --git a/packages/dockview-core/src/dnd/abstractDragHandler.ts b/packages/dockview-core/src/dnd/abstractDragHandler.ts index df3f49177..55e03fa42 100644 --- a/packages/dockview-core/src/dnd/abstractDragHandler.ts +++ b/packages/dockview-core/src/dnd/abstractDragHandler.ts @@ -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(); 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(); }) ); } diff --git a/packages/docs/docs/components/dockview.mdx b/packages/docs/docs/components/dockview.mdx index 176f309db..576c574a3 100644 --- a/packages/docs/docs/components/dockview.mdx +++ b/packages/docs/docs/components/dockview.mdx @@ -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(...) +## 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. + + + + + ## Events A simple example showing events fired by `dockviewz that can be interacted with. diff --git a/packages/docs/sandboxes/iframe-dockview/package.json b/packages/docs/sandboxes/iframe-dockview/package.json new file mode 100644 index 000000000..0a750cb5e --- /dev/null +++ b/packages/docs/sandboxes/iframe-dockview/package.json @@ -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" + ] +} \ No newline at end of file diff --git a/packages/docs/sandboxes/iframe-dockview/public/index.html b/packages/docs/sandboxes/iframe-dockview/public/index.html new file mode 100644 index 000000000..1f8a52426 --- /dev/null +++ b/packages/docs/sandboxes/iframe-dockview/public/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + React App + + + + +
+ + + + diff --git a/packages/docs/sandboxes/iframe-dockview/src/app.tsx b/packages/docs/sandboxes/iframe-dockview/src/app.tsx new file mode 100644 index 000000000..f52656551 --- /dev/null +++ b/packages/docs/sandboxes/iframe-dockview/src/app.tsx @@ -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 ( +