Merge branch 'master' of https://github.com/mathuo/dockview into 263-left-header-actions

This commit is contained in:
mathuo 2023-06-21 20:01:53 +01:00
commit 359b0e81d0
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
47 changed files with 1964 additions and 106 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

@ -30,7 +30,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@ -9,16 +9,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
with:
persist-credentials: false
uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '16.x'
- uses: actions/cache@v2
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
@ -26,7 +23,6 @@ jobs:
${{ runner.os }}-node-
- run: yarn install
- run: lerna bootstrap
- run: npm run build
working-directory: packages/dockview-core
- run: npm run build

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
# might be required for sonar to work correctly
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
@ -16,7 +16,7 @@ jobs:
with:
node-version: '16.x'
- uses: actions/cache@v2
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}

View File

@ -3,7 +3,7 @@
"packages/*"
],
"useWorkspaces": true,
"version": "1.7.4",
"version": "1.7.6",
"npmClient": "yarn",
"command": {
"publish": {

View File

@ -1,6 +1,6 @@
{
"name": "dockview-core",
"version": "1.7.4",
"version": "1.7.6",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support",
"main": "./dist/cjs/index.js",
"types": "./dist/cjs/index.d.ts",

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

@ -541,6 +541,8 @@ describe('dockviewComponent', () => {
},
});
// dockview.layout(1000, 1000, true);
expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({
activeGroup: 'group-1',
grid: {
@ -1723,6 +1725,9 @@ describe('dockviewComponent', () => {
test_tab_id: PanelTabPartTest,
},
});
dockview.layout(1000, 1000);
dockview.fromJSON({
activeGroup: 'group-1',
grid: {
@ -1918,6 +1923,8 @@ describe('dockviewComponent', () => {
orientation: Orientation.HORIZONTAL,
});
dockview.layout(1000, 1000);
expect(dockview.orientation).toBe(Orientation.HORIZONTAL);
dockview.fromJSON({
@ -2023,6 +2030,8 @@ describe('dockviewComponent', () => {
orientation: Orientation.HORIZONTAL,
});
dockview.layout(1000, 1000);
expect(dockview.orientation).toBe(Orientation.HORIZONTAL);
dockview.fromJSON({
@ -2163,6 +2172,8 @@ describe('dockviewComponent', () => {
orientation: Orientation.HORIZONTAL,
});
dockview.layout(1000, 1000);
expect(dockview.orientation).toBe(Orientation.HORIZONTAL);
dockview.fromJSON({
@ -2448,4 +2459,164 @@ describe('dockviewComponent', () => {
activeGroup: '1',
});
});
test('check dockview component is rendering to the DOM as expected', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent({
parentElement: container,
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.layout(100, 100);
const panel1 = dockview.addPanel({
id: 'panel1',
component: 'default',
});
expect(dockview.element.querySelectorAll('.view').length).toBe(1);
const panel2 = dockview.addPanel({
id: 'panel2',
component: 'default',
});
expect(dockview.element.querySelectorAll('.view').length).toBe(1);
const panel3 = dockview.addPanel({
id: 'panel3',
component: 'default',
});
expect(dockview.element.querySelectorAll('.view').length).toBe(1);
dockview.moveGroupOrPanel(
panel3.group,
panel3.group.id,
panel3.id,
'right'
);
expect(dockview.groups.length).toBe(2);
expect(dockview.element.querySelectorAll('.view').length).toBe(2);
dockview.moveGroupOrPanel(
panel3.group,
panel2.group.id,
panel2.id,
'bottom'
);
expect(dockview.groups.length).toBe(3);
expect(dockview.element.querySelectorAll('.view').length).toBe(4);
dockview.moveGroupOrPanel(
panel2.group,
panel1.group.id,
panel1.id,
'center'
);
expect(dockview.groups.length).toBe(2);
expect(dockview.element.querySelectorAll('.view').length).toBe(2);
});
test('that fromJSON layouts are resized to the current dimensions', async () => {
const container = document.createElement('div');
const dockview = new DockviewComponent({
parentElement: container,
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
expect(dockview.orientation).toBe(Orientation.HORIZONTAL);
dockview.layout(1000, 500);
dockview.fromJSON({
activeGroup: 'group-1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1', 'panel2'],
id: 'group-1',
activeView: 'panel2',
},
size: 2000,
},
],
size: 1000,
},
height: 1000,
width: 2000,
orientation: Orientation.HORIZONTAL,
},
panels: {
panel1: {
id: 'panel1',
contentComponent: 'default',
title: 'panel1',
},
panel2: {
id: 'panel2',
contentComponent: 'default',
title: 'panel2',
},
},
});
expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({
activeGroup: 'group-1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1', 'panel2'],
id: 'group-1',
activeView: 'panel2',
},
size: 1000,
},
],
size: 500,
},
height: 500,
width: 1000,
orientation: Orientation.HORIZONTAL,
},
panels: {
panel1: {
id: 'panel1',
contentComponent: 'default',
title: 'panel1',
},
panel2: {
id: 'panel2',
contentComponent: 'default',
title: 'panel2',
},
},
});
});
});

View File

@ -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
);
});
});

View File

@ -18,6 +18,10 @@ class MockGridview implements IGridView {
>().event;
element: HTMLElement = document.createElement('div');
constructor() {
this.element.className = 'mock-grid-view';
}
layout(width: number, height: number): void {
//
}
@ -116,4 +120,574 @@ describe('gridview', () => {
checkOrientationFlipsAtEachLevel((gridview as any).root as BranchNode);
});
test('removeView: remove leaf from branch where branch becomes leaf and parent is root', () => {
const gridview = new Gridview(
false,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
gridview.addView(new MockGridview(), Sizing.Distribute, [0]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0]);
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'HORIZONTAL',
root: {
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: {},
size: 500,
type: 'leaf',
},
],
size: 500,
type: 'branch',
},
],
size: 1000,
type: 'branch',
},
width: 1000,
});
expect(
gridview.element.querySelectorAll('.mock-grid-view').length
).toBe(3);
gridview.removeView([1, 0], Sizing.Distribute);
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'HORIZONTAL',
root: {
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: {},
size: 500,
type: 'leaf',
},
],
size: 1000,
type: 'branch',
},
width: 1000,
});
expect(
gridview.element.querySelectorAll('.mock-grid-view').length
).toBe(2);
});
test('removeView: remove leaf from branch where branch remains branch and parent is root', () => {
const gridview = new Gridview(
false,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
gridview.addView(new MockGridview(), Sizing.Distribute, [0]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1, 1]);
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'HORIZONTAL',
root: {
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: [
{
data: {},
size: 333,
type: 'leaf',
},
{
data: {},
size: 333,
type: 'leaf',
},
{
data: {},
size: 334,
type: 'leaf',
},
],
size: 500,
type: 'branch',
},
],
size: 1000,
type: 'branch',
},
width: 1000,
});
expect(
gridview.element.querySelectorAll('.mock-grid-view').length
).toBe(4);
gridview.removeView([1, 0], Sizing.Distribute);
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'HORIZONTAL',
root: {
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: {},
size: 500,
type: 'leaf',
},
],
size: 500,
type: 'branch',
},
],
size: 1000,
type: 'branch',
},
width: 1000,
});
expect(
gridview.element.querySelectorAll('.mock-grid-view').length
).toBe(3);
});
test('removeView: remove leaf where parent is root', () => {
const gridview = new Gridview(
false,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
gridview.addView(new MockGridview(), Sizing.Distribute, [0]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0]);
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'HORIZONTAL',
root: {
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: {},
size: 500,
type: 'leaf',
},
],
size: 500,
type: 'branch',
},
],
size: 1000,
type: 'branch',
},
width: 1000,
});
expect(
gridview.element.querySelectorAll('.mock-grid-view').length
).toBe(3);
gridview.removeView([0], Sizing.Distribute);
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'VERTICAL',
root: {
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: {},
size: 500,
type: 'leaf',
},
],
size: 1000,
type: 'branch',
},
width: 1000,
});
expect(
gridview.element.querySelectorAll('.mock-grid-view').length
).toBe(2);
});
test('removeView: remove leaf from branch where branch becomes leaf and parent is not root', () => {
const gridview = new Gridview(
false,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
gridview.addView(new MockGridview(), Sizing.Distribute, [0]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0, 0]);
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'HORIZONTAL',
root: {
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: [
{
data: [
{
data: {},
size: 250,
type: 'leaf',
},
{
data: {},
size: 250,
type: 'leaf',
},
],
size: 500,
type: 'branch',
},
{
data: {},
size: 500,
type: 'leaf',
},
],
size: 500,
type: 'branch',
},
],
size: 1000,
type: 'branch',
},
width: 1000,
});
expect(
gridview.element.querySelectorAll('.mock-grid-view').length
).toBe(4);
gridview.removeView([1, 0, 0], Sizing.Distribute);
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'HORIZONTAL',
root: {
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: {},
size: 500,
type: 'leaf',
},
],
size: 500,
type: 'branch',
},
],
size: 1000,
type: 'branch',
},
width: 1000,
});
expect(
gridview.element.querySelectorAll('.mock-grid-view').length
).toBe(3);
});
test('removeView: remove leaf from branch where branch remains branch and parent is not root', () => {
const gridview = new Gridview(
false,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
gridview.addView(new MockGridview(), Sizing.Distribute, [0]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0, 0]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0, 1]);
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'HORIZONTAL',
root: {
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: [
{
data: [
{
data: {},
size: 166,
type: 'leaf',
},
{
data: {},
size: 166,
type: 'leaf',
},
{
data: {},
size: 168,
type: 'leaf',
},
],
size: 500,
type: 'branch',
},
{
data: {},
size: 500,
type: 'leaf',
},
],
size: 500,
type: 'branch',
},
],
size: 1000,
type: 'branch',
},
width: 1000,
});
expect(
gridview.element.querySelectorAll('.mock-grid-view').length
).toBe(5);
gridview.removeView([1, 0, 1], Sizing.Distribute);
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'HORIZONTAL',
root: {
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: [
{
data: [
{
data: {},
size: 250,
type: 'leaf',
},
{
data: {},
size: 250,
type: 'leaf',
},
],
size: 500,
type: 'branch',
},
{
data: {},
size: 500,
type: 'leaf',
},
],
size: 500,
type: 'branch',
},
],
size: 1000,
type: 'branch',
},
width: 1000,
});
expect(
gridview.element.querySelectorAll('.mock-grid-view').length
).toBe(4);
});
test('removeView: remove leaf where parent is root', () => {
const gridview = new Gridview(
false,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
gridview.addView(new MockGridview(), Sizing.Distribute, [0]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0, 0]);
gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0, 1]);
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'HORIZONTAL',
root: {
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: [
{
data: [
{
data: {},
size: 166,
type: 'leaf',
},
{
data: {},
size: 166,
type: 'leaf',
},
{
data: {},
size: 168,
type: 'leaf',
},
],
size: 500,
type: 'branch',
},
{
data: {},
size: 500,
type: 'leaf',
},
],
size: 500,
type: 'branch',
},
],
size: 1000,
type: 'branch',
},
width: 1000,
});
expect(
gridview.element.querySelectorAll('.mock-grid-view').length
).toBe(5);
gridview.removeView([1, 1], Sizing.Distribute);
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'HORIZONTAL',
root: {
data: [
{
data: {},
size: 500,
type: 'leaf',
},
{
data: {},
size: 166,
type: 'leaf',
},
{
data: {},
size: 166,
type: 'leaf',
},
{
data: {},
size: 168,
type: 'leaf',
},
],
size: 1000,
type: 'branch',
},
width: 1000,
});
expect(
gridview.element.querySelectorAll('.mock-grid-view').length
).toBe(4);
});
});

View File

@ -471,6 +471,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -528,7 +530,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
// gridview.layout(800, 400);
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -552,7 +555,6 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
// gridview.layout(800, 400, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
@ -587,7 +589,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
// gridview.layout(800, 400);
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -620,7 +623,6 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
// gridview.layout(800, 400, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
@ -664,7 +666,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
// gridview.layout(800, 400);
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -706,7 +709,6 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
// gridview.layout(800, 400, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
@ -759,7 +761,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
// gridview.layout(800, 400);
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -801,7 +804,6 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
// gridview.layout(800, 400, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
@ -854,6 +856,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -895,7 +899,6 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
gridview.layout(800, 400, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
@ -948,7 +951,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
// gridview.layout(800, 400);
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -1005,7 +1009,6 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
// gridview.layout(800, 400, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
@ -1198,6 +1201,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -1254,7 +1259,8 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
gridview.layout(800, 400, true);
// gridview.layout(800, 400, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
@ -1322,6 +1328,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -1445,6 +1453,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -1908,4 +1918,318 @@ describe('gridview', () => {
return disposable.dispose();
});
test('that fromJSON layouts are resized to the current dimensions', async () => {
const container = document.createElement('div');
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
});
gridview.layout(1600, 800);
gridview.fromJSON({
grid: {
height: 400,
width: 800,
orientation: Orientation.HORIZONTAL,
root: {
type: 'branch',
size: 400,
data: [
{
type: 'leaf',
size: 200,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 400,
data: [
{
type: 'leaf',
size: 250,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 150,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 200,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
height: 800,
width: 1600,
orientation: Orientation.HORIZONTAL,
root: {
type: 'branch',
size: 800,
data: [
{
type: 'leaf',
size: 400,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 800,
data: [
{
type: 'leaf',
size: 500,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 300,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 400,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
});
test('that a deep layout with fromJSON dimensions identical to the current dimensions loads', async () => {
const container = document.createElement('div');
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
});
gridview.layout(5000, 5000);
gridview.fromJSON({
grid: {
height: 5000,
width: 5000,
orientation: Orientation.HORIZONTAL,
root: {
type: 'branch',
size: 5000,
data: [
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 2000,
data: [
{
type: 'branch',
size: 4000,
data: [
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 1000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_5',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_6',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
height: 5000,
width: 5000,
orientation: Orientation.HORIZONTAL,
root: {
type: 'branch',
size: 5000,
data: [
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 2000,
data: [
{
type: 'branch',
size: 4000,
data: [
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 1000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_5',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_6',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
});
});

View File

@ -408,4 +408,85 @@ describe('componentPaneview', () => {
expect(panel1Spy).toHaveBeenCalledTimes(1);
expect(panel2Spy).toHaveBeenCalledTimes(1);
});
test('that fromJSON layouts are resized to the current dimensions', async () => {
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
},
});
paneview.layout(400, 600);
paneview.fromJSON({
size: 6,
views: [
{
size: 1,
data: {
id: 'panel1',
component: 'testPanel',
title: 'Panel 1',
},
expanded: true,
},
{
size: 2,
data: {
id: 'panel2',
component: 'testPanel',
title: 'Panel 2',
},
expanded: true,
},
{
size: 3,
data: {
id: 'panel3',
component: 'testPanel',
title: 'Panel 3',
},
expanded: true,
},
],
});
// heights slightly differ because header height isn't accounted for
expect(JSON.parse(JSON.stringify(paneview.toJSON()))).toEqual({
size: 600,
views: [
{
size: 122,
data: {
id: 'panel1',
component: 'testPanel',
title: 'Panel 1',
},
expanded: true,
minimumSize: 100,
},
{
size: 122,
data: {
id: 'panel2',
component: 'testPanel',
title: 'Panel 2',
},
expanded: true,
minimumSize: 100,
},
{
size: 356,
data: {
id: 'panel3',
component: 'testPanel',
title: 'Panel 3',
},
expanded: true,
minimumSize: 100,
},
],
});
});
});

View File

@ -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;
@ -84,6 +84,8 @@ describe('splitview', () => {
beforeEach(() => {
container = document.createElement('div');
container.className = 'container';
jest.clearAllMocks();
});
test('vertical splitview', () => {
@ -596,4 +598,82 @@ describe('splitview', () => {
expect(anyEvents).toBeFalsy();
expect(container.childNodes.length).toBe(0);
});
test('dnd: pointer 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 addEventListenerSpy = jest.spyOn(document, 'addEventListener');
const removeEventListenerSpy = jest.spyOn(
document,
'removeEventListener'
);
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(
sashElement,
new MouseEvent('pointerdown', { clientX: 50, clientY: 100 })
);
expect(addEventListenerSpy).toBeCalledTimes(3);
// 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(
document,
new MouseEvent('pointermove', { clientX: 70, clientY: 110 })
);
expect([view1.size, view2.size]).toEqual([220, 180]);
// expect a delta move of 75 - 70 = 5
fireEvent(
document,
new MouseEvent('pointermove', { clientX: 75, clientY: 110 })
);
expect([view1.size, view2.size]).toEqual([225, 175]);
// end the drag event
fireEvent(
document,
new MouseEvent('pointerup', { clientX: 70, clientY: 110 })
);
expect(removeEventListenerSpy).toBeCalledTimes(3);
// expect pointer-eventes on views to be restored
expect(view1.element.parentElement!.style.pointerEvents).toBe('');
expect(view2.element.parentElement!.style.pointerEvents).toBe('');
fireEvent(
document,
new MouseEvent('pointermove', { clientX: 100, clientY: 100 })
);
// expect no additional resizes
expect([view1.size, view2.size]).toEqual([225, 175]);
// expect no additional document listeners
expect(addEventListenerSpy).toBeCalledTimes(3);
expect(removeEventListenerSpy).toBeCalledTimes(3);
});
});

View File

@ -330,7 +330,7 @@ describe('componentSplitview', () => {
testPanel: TestPanel,
},
});
splitview.layout(600, 400);
splitview.layout(400, 6);
splitview.fromJSON({
views: [
@ -535,4 +535,57 @@ describe('componentSplitview', () => {
expect(panel1Spy).toHaveBeenCalledTimes(1);
expect(panel2Spy).toHaveBeenCalledTimes(1);
});
test('that fromJSON layouts are resized to the current dimensions', async () => {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL,
components: {
testPanel: TestPanel,
},
});
splitview.layout(400, 600);
splitview.fromJSON({
views: [
{
size: 1,
data: { id: 'panel1', component: 'testPanel' },
snap: false,
},
{
size: 2,
data: { id: 'panel2', component: 'testPanel' },
snap: true,
},
{ size: 3, data: { id: 'panel3', component: 'testPanel' } },
],
size: 6,
orientation: Orientation.VERTICAL,
activeView: 'panel1',
});
expect(JSON.parse(JSON.stringify(splitview.toJSON()))).toEqual({
views: [
{
size: 100,
data: { id: 'panel1', component: 'testPanel' },
snap: false,
},
{
size: 200,
data: { id: 'panel2', component: 'testPanel' },
snap: true,
},
{
size: 300,
data: { id: 'panel3', component: 'testPanel' },
snap: false,
},
],
size: 600,
orientation: Orientation.VERTICAL,
activeView: 'panel1',
});
});
});

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

@ -413,6 +413,10 @@ export class DockviewComponent
throw new Error('root must be of type branch');
}
// take note of the existing dimensions
const width = this.width;
const height = this.height;
this.gridview.deserialize(grid, {
fromJSON: (node: ISerializedLeafNode<GroupPanelViewState>) => {
const { id, locked, hideHeader, views, activeView } = node.data;
@ -454,6 +458,8 @@ export class DockviewComponent
},
});
this.layout(width, height);
if (typeof activeGroup === 'string') {
const panel = this.getPanel(activeGroup);
if (panel) {
@ -461,8 +467,6 @@ export class DockviewComponent
}
}
this.gridview.layout(this.width, this.height);
this._onDidLayoutFromJSON.fire();
}

View File

@ -162,7 +162,7 @@ export function addDisposableWindowListener<K extends keyof WindowEventMap>(
return {
dispose: () => {
element.removeEventListener(type, listener);
element.removeEventListener(type, listener, options);
},
};
}
@ -177,7 +177,7 @@ export function addDisposableListener<K extends keyof HTMLElementEventMap>(
return {
dispose: () => {
element.removeEventListener(type, listener);
element.removeEventListener(type, listener, options);
},
};
}

View File

@ -371,8 +371,7 @@ export class Gridview implements IDisposable {
root,
orientation,
deserializer,
orthogonalSize,
true
orthogonalSize
) as BranchNode;
}
@ -380,8 +379,7 @@ export class Gridview implements IDisposable {
node: ISerializedNode,
orientation: Orientation,
deserializer: IViewDeserializer,
orthogonalSize: number,
isRoot = false
orthogonalSize: number
): Node {
let result: Node;
if (node.type === 'branch') {
@ -398,14 +396,12 @@ export class Gridview implements IDisposable {
} as INodeDescriptor;
});
// HORIZONTAL => height=orthogonalsize width=size
// VERTICAL => height=size width=orthogonalsize
result = new BranchNode(
orientation,
this.proportionalLayout,
this.styles,
isRoot ? orthogonalSize : node.size,
isRoot ? node.size : orthogonalSize,
orthogonalSize, // <- size - flips at each depth
node.size, // <- orthogonal size - flips at each depth
children
);
} else {
@ -678,67 +674,82 @@ export class Gridview implements IDisposable {
throw new Error('Invalid location');
}
const node = parent.children[index];
const nodeToRemove = parent.children[index];
if (!(node instanceof LeafNode)) {
if (!(nodeToRemove instanceof LeafNode)) {
throw new Error('Invalid location');
}
const view = node.view;
node.dispose(); // dispose of node
parent.removeChild(index, sizing);
nodeToRemove.dispose();
const child = parent.removeChild(index, sizing);
child.dispose();
if (parent.children.length === 0) {
return view;
if (parent.children.length !== 1) {
return nodeToRemove.view;
}
if (parent.children.length > 1) {
return view;
}
// if the parent has only one child and we know the parent is a BranchNode we can make the tree
// more efficiently spaced by replacing the parent BranchNode with the child.
// if that child is a LeafNode then we simply replace the BranchNode with the child otherwise if the child
// is a BranchNode too we should spread it's children into the grandparent.
// refer to the remaining child as the sibling
const sibling = parent.children[0];
if (pathToParent.length === 0) {
// parent is root
// if the parent is root
if (sibling instanceof LeafNode) {
return view;
// if the sibling is a leaf node no action is required
return nodeToRemove.view;
}
// we must promote sibling to be the new root
const child = parent.removeChild(0, sizing);
child.dispose();
// otherwise the sibling is a branch node. since the parent is the root and the root has only one child
// which is a branch node we can just set this branch node to be the new root node
// for good housekeeping we'll removing the sibling from it's existing tree
parent.removeChild(0, sizing);
// and set that sibling node to be root
this.root = sibling;
return view;
return nodeToRemove.view;
}
// otherwise the parent is apart of a large sub-tree
const [grandParent, ..._] = [...pathToParent].reverse();
const [parentIndex, ...__] = [...rest].reverse();
const isSiblingVisible = parent.isChildVisible(0);
const childNode = parent.removeChild(0, sizing);
childNode.dispose();
// either way we need to remove the sibling from it's existing tree
parent.removeChild(0, sizing);
// note the sizes of all of the grandparents children
const sizes = grandParent.children.map((_size, i) =>
grandParent.getChildSize(i)
);
const parentNode = grandParent.removeChild(parentIndex, sizing);
parentNode.dispose();
// remove the parent from the grandparent since we are moving the sibling to take the parents place
// this parent is no longer used and can be disposed of
grandParent.removeChild(parentIndex, sizing).dispose();
if (sibling instanceof BranchNode) {
// replace the parent with the siblings children
sizes.splice(
parentIndex,
1,
...sibling.children.map((c) => c.size)
);
// and add those siblings to the grandparent
for (let i = 0; i < sibling.children.length; i++) {
const child = sibling.children[i];
grandParent.addChild(child, child.size, parentIndex + i);
}
} else {
// otherwise create a new leaf node and add that to the grandparent
const newSibling = new LeafNode(
sibling.view,
orthogonal(sibling.orientation),
@ -747,14 +758,19 @@ export class Gridview implements IDisposable {
const siblingSizing = isSiblingVisible
? sibling.orthogonalSize
: Sizing.Invisible(sibling.orthogonalSize);
grandParent.addChild(newSibling, siblingSizing, parentIndex);
}
// the containing node of the sibling is no longer required and can be disposed of
sibling.dispose();
// resize everything
for (let i = 0; i < sizes.length; i++) {
grandParent.resizeChild(i, sizes[i]);
}
return view;
return nodeToRemove.view;
}
public layout(width: number, height: number): void {

View File

@ -176,6 +176,10 @@ export class GridviewComponent
const queue: Function[] = [];
// take note of the existing dimensions
const width = this.width;
const height = this.height;
this.gridview.deserialize(grid, {
fromJSON: (node) => {
const { data } = node;
@ -215,7 +219,7 @@ export class GridviewComponent
},
});
this.layout(this.width, this.height, true);
this.layout(width, height);
queue.forEach((f) => f());

View File

@ -360,6 +360,10 @@ export class PaneviewComponent extends Resizable implements IPaneviewComponent {
const queue: Function[] = [];
// take note of the existing dimensions
const width = this.width;
const height = this.height;
this.paneview = new Paneview(this.element, {
orientation: Orientation.VERTICAL,
descriptor: {
@ -437,7 +441,7 @@ export class PaneviewComponent extends Resizable implements IPaneviewComponent {
},
});
this.layout(this.width, this.height);
this.layout(width, height);
queue.forEach((f) => f());

View File

@ -106,6 +106,7 @@
-webkit-user-select: none; // Safari
-moz-user-select: none; // Firefox
-ms-user-select: none; // IE 10 and IE 11
touch-action: none;
&:active {
transition: background-color 0.1s ease-in-out;

View File

@ -393,7 +393,7 @@ export class Splitview {
const sash = document.createElement('div');
sash.className = 'sash';
const onStart = (event: MouseEvent) => {
const onPointerStart = (event: PointerEvent) => {
for (const item of this.viewItems) {
item.enabled = false;
}
@ -486,13 +486,12 @@ export class Splitview {
size: snappedViewItem.size,
};
}
//
const mousemove = (mousemoveEvent: MouseEvent) => {
const onPointerMove = (event: PointerEvent) => {
const current =
this._orientation === Orientation.HORIZONTAL
? mousemoveEvent.clientX
: mousemoveEvent.clientY;
? event.clientX
: event.clientY;
const delta = current - start;
this.resize(
@ -521,24 +520,24 @@ export class Splitview {
this.saveProportions();
document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mouseup', end);
document.removeEventListener('mouseend', end);
document.removeEventListener('pointermove', onPointerMove);
document.removeEventListener('pointerup', end);
document.removeEventListener('pointercancel', end);
this._onDidSashEnd.fire(undefined);
};
document.addEventListener('mousemove', mousemove);
document.addEventListener('mouseup', end);
document.addEventListener('mouseend', end);
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', end);
document.addEventListener('pointercancel', end);
};
sash.addEventListener('mousedown', onStart);
sash.addEventListener('pointerdown', onPointerStart);
const sashItem: ISashItem = {
container: sash,
disposable: () => {
sash.removeEventListener('mousedown', onStart);
sash.removeEventListener('pointerdown', onPointerStart);
this.sashContainer.removeChild(sash);
},
};

View File

@ -337,6 +337,10 @@ export class SplitviewComponent
const queue: Function[] = [];
// take note of the existing dimensions
const width = this.width;
const height = this.height;
this.splitview = new Splitview(this.element, {
orientation,
proportionalLayout: this.options.proportionalLayout,
@ -387,7 +391,7 @@ export class SplitviewComponent
},
});
this.layout(this.width, this.height);
this.layout(width, height);
queue.forEach((f) => f());

View File

@ -1,6 +1,6 @@
{
"name": "dockview",
"version": "1.7.4",
"version": "1.7.6",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support",
"main": "./dist/cjs/index.js",
"types": "./dist/cjs/index.d.ts",
@ -56,7 +56,7 @@
"author": "https://github.com/mathuo",
"license": "MIT",
"dependencies": {
"dockview-core": "^1.7.4"
"dockview-core": "^1.7.6"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.1",

View File

@ -20,7 +20,7 @@ import Link from '@docusaurus/Link';
- Provide a default React tab implementation to allow for simple changes to tab renderer without rewritting the entire tab
- Override the default tab in `ReactDockview` with the `defaultTabComponent` prop
- Group controls renderer [#138](https://github.com/mathuo/dockview/pull/138)
- Provide the `groupControlComponent` prop in `ReactDockview` to create custom control components for groups. <Link to="../../docs/components/dockview/#group-controls-panel">Go</Link>
- Provide the `groupControlComponent` prop in `ReactDockview` to create custom control components for groups.
## 🛠 Miscs

View File

@ -0,0 +1,17 @@
---
slug: dockview-1.7.5-release
title: Dockview 1.7.5
tags: [release]
---
# Release Notes
Please reference to docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
## 🛠 Miscs
- Fix [#255](https://github.com/mathuo/dockview/issues/255)
## 🔥 Breaking changes

View File

@ -0,0 +1,20 @@
---
slug: dockview-1.7.6-release
title: Dockview 1.7.6
tags: [release]
---
# Release Notes
Please reference to docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
- Touch support for resize handles [#278](https://github.com/mathuo/dockview/pull/278)
## 🛠 Miscs
- Internal cleanup [#275](https://github.com/mathuo/dockview/pull/275)
- iframe docs [#273](https://github.com/mathuo/dockview/pull/273)
## 🔥 Breaking changes

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';
@ -740,6 +741,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

@ -1,6 +1,6 @@
{
"name": "dockview-docs",
"version": "1.7.4",
"version": "1.7.6",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
@ -22,7 +22,7 @@
"@minoru/react-dnd-treeview": "^3.4.3",
"axios": "^1.3.3",
"clsx": "^1.2.1",
"dockview": "^1.7.4",
"dockview": "^1.7.6",
"prism-react-renderer": "^1.3.5",
"react": "^18.2.0",
"react-dnd": "^16.0.1",

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={{
pointerEvents: 'none',
border: 'none',
width: '100%',
height: '100%',
}}
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,91 @@
import { IDockviewPanelProps } from 'dockview';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
// 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);
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`;
};
React.useEffect(() => {
if (!innerRef.current) {
return;
}
const disposable1 = props.api.onDidVisibilityChange((event) => {
if (!innerRef.current) {
return;
}
toggleVisibility(innerRef.current, event.isVisible); // subsequent checks of visibility
});
const disposable2 = props.api.onDidDimensionsChange(() => {
positionHoistedPanel();
});
positionHoistedPanel();
return () => {
disposable1.dispose(); // cleanup
disposable2.dispose();
};
}, [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.

View File

@ -1,3 +1,3 @@
[
"1.7.4"
]
"1.7.6"
]