Merge pull request #212 from mathuo/210-setconstraints-on-gridview-enable-size-locking-1

fix: dockview panel group should derive constraints
This commit is contained in:
mathuo 2023-03-22 21:30:36 +01:00 committed by GitHub
commit 464c4fd938
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 418 additions and 214 deletions

View File

@ -50,6 +50,39 @@ describe('dockviewPanel', () => {
disposable.dispose();
});
test('that .setTitle updates the title', () => {
const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {
onDidActiveChange: jest.fn(),
} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
};
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = new groupMock();
const model = <IDockviewPanelModel>new panelModelMock();
const cut = new DockviewPanel('fake-id', accessor, api, group, model);
cut.init({ title: 'myTitle', params: {} });
expect(cut.title).toBe('myTitle');
cut.setTitle('newTitle');
expect(cut.title).toBe('newTitle');
});
test('dispose cleanup', () => {
const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {} as any;

View File

@ -12,7 +12,11 @@ export interface TitleEvent {
* omit visibility modifiers since the visibility of a single group doesn't make sense
* because it belongs to a groupview
*/
export interface DockviewPanelApi extends Omit<GridviewPanelApi, 'setVisible'> {
export interface DockviewPanelApi
extends Omit<
GridviewPanelApi,
'setVisible' | 'onDidConstraintsChange' | 'setConstraints'
> {
readonly group: DockviewGroupPanel;
readonly isGroupActive: boolean;
readonly title: string;

View File

@ -4,13 +4,14 @@ import { GridviewPanelApi } from '../api/gridviewPanelApi';
import {
DockviewGroupPanelModel,
GroupOptions,
IDockviewGroupPanelModel,
IHeader,
} from './dockviewGroupPanelModel';
import { GridviewPanel, IGridviewPanel } from '../gridview/gridviewPanel';
import { IDockviewPanel } from '../dockview/dockviewPanel';
export interface IDockviewGroupPanel extends IGridviewPanel {
model: DockviewGroupPanelModel;
model: IDockviewGroupPanelModel;
locked: boolean;
readonly size: number;
readonly panels: IDockviewPanel[];
@ -25,7 +26,7 @@ export class DockviewGroupPanel
extends GridviewPanel
implements IDockviewGroupPanel
{
private readonly _model: DockviewGroupPanelModel;
private readonly _model: IDockviewGroupPanelModel;
get panels(): IDockviewPanel[] {
return this._model.panels;
@ -39,26 +40,10 @@ export class DockviewGroupPanel
return this._model.size;
}
get model(): DockviewGroupPanelModel {
get model(): IDockviewGroupPanelModel {
return this._model;
}
get minimumHeight(): number {
return this._model.minimumHeight;
}
get maximumHeight(): number {
return this._model.maximumHeight;
}
get minimumWidth(): number {
return this._model.minimumWidth;
}
get maximumWidth(): number {
return this._model.maximumWidth;
}
get locked(): boolean {
return this._model.locked;
}
@ -76,7 +61,10 @@ export class DockviewGroupPanel
id: string,
options: GroupOptions
) {
super(id, 'groupview_default');
super(id, 'groupview_default', {
minimumHeight: 100,
minimumWidth: 100,
});
this._model = new DockviewGroupPanelModel(
this.element,

View File

@ -4,10 +4,9 @@ import { Droptarget, Position } from '../dnd/droptarget';
import { DockviewComponent } from './dockviewComponent';
import { isAncestor, toggleClass } from '../dom';
import { addDisposableListener, Emitter, Event } from '../events';
import { IGridPanelView } from '../gridview/baseComponentGridview';
import { IViewSize } from '../gridview/gridview';
import { CompositeDisposable } from '../lifecycle';
import { PanelInitParameters, PanelUpdateEvent } from '../panel/types';
import { IPanel, PanelInitParameters, PanelUpdateEvent } from '../panel/types';
import {
ContentContainer,
IContentContainer,
@ -82,7 +81,7 @@ export interface IHeader {
height: number | undefined;
}
export interface IDockviewGroupPanelModel extends IGridPanelView {
export interface IDockviewGroupPanelModel extends IPanel {
readonly isActive: boolean;
readonly size: number;
readonly panels: IDockviewPanel[];
@ -95,13 +94,20 @@ export interface IDockviewGroupPanelModel extends IGridPanelView {
readonly onDidActivePanelChange: Event<GroupviewChangeEvent>;
readonly onMove: Event<GroupMoveEvent>;
locked: boolean;
setActive(isActive: boolean): void;
initialize(): void;
// state
isPanelActive: (panel: IDockviewPanel) => boolean;
indexOf(panel: IDockviewPanel): number;
// panel lifecycle
openPanel(
panel: IDockviewPanel,
options?: { index?: number; skipFocus?: boolean }
options?: {
index?: number;
skipFocus?: boolean;
skipSetPanelActive?: boolean;
skipSetGroupActive?: boolean;
}
): void;
closePanel(panel: IDockviewPanel): void;
closeAllPanels(): void;
@ -199,22 +205,6 @@ export class DockviewGroupPanelModel
return this._panels.length === 0;
}
get minimumHeight(): number {
return 100;
}
get maximumHeight(): number {
return Number.MAX_SAFE_INTEGER;
}
get minimumWidth(): number {
return 100;
}
get maximumWidth(): number {
return Number.MAX_SAFE_INTEGER;
}
get hasWatermark(): boolean {
return !!(
this.watermark && this.container.contains(this.watermark.element)

View File

@ -126,9 +126,31 @@ export abstract class GridviewPanel
return this.api.isActive;
}
constructor(id: string, component: string) {
constructor(
id: string,
component: string,
options?: {
minimumWidth?: number;
maximumWidth?: number;
minimumHeight?: number;
maximumHeight?: number;
}
) {
super(id, component, new GridviewPanelApiImpl(id));
if (typeof options?.minimumWidth === 'number') {
this._minimumWidth = options.minimumWidth;
}
if (typeof options?.maximumWidth === 'number') {
this._maximumWidth = options.maximumWidth;
}
if (typeof options?.minimumHeight === 'number') {
this._minimumHeight = options.minimumHeight;
}
if (typeof options?.maximumHeight === 'number') {
this._maximumHeight = options.maximumHeight;
}
this.api.initialize(this); // TODO: required to by-pass 'super before this' requirement
this.addDisposables(

View File

@ -1,92 +0,0 @@
import { trackFocus } from './dom';
import { Emitter, Event } from './events';
import { IDisposable } from './lifecycle';
export interface HostedContainerOptions {
id: string;
parent?: HTMLElement;
}
export class HostedContainer implements IDisposable {
private readonly _element: HTMLElement;
private readonly _onDidFocus = new Emitter<void>();
readonly onDidFocus: Event<void> = this._onDidFocus.event;
private readonly _onDidBlur = new Emitter<void>();
readonly onDidBlur: Event<void> = this._onDidBlur.event;
get element() {
return this._element;
}
constructor(private readonly options: HostedContainerOptions) {
if (!options.parent) {
options.parent = document.getElementById('app') as HTMLElement;
options.parent.style.position = 'relative';
}
this._element = document.createElement('div');
this._element.style.visibility = 'hidden';
this._element.style.overflow = 'hidden';
// this._element.style.pointerEvents = 'none';
this._element.id = `webview-${options.id}`;
this._element.tabIndex = -1;
const { onDidFocus, onDidBlur } = trackFocus(this._element);
onDidFocus(() => this._onDidFocus.fire());
onDidBlur(() => this._onDidBlur.fire());
/**
* When dragging somebody
*/
window.addEventListener('dragstart', () => {
this.element.style.pointerEvents = 'none';
});
window.addEventListener('dragend', () => {
this.element.style.pointerEvents = '';
});
window.addEventListener('mousemove', (ev) => {
if (ev.buttons === 0) {
this.element.style.pointerEvents = '';
}
});
options.parent.appendChild(this._element);
}
hide() {
this._element.style.visibility = 'hidden';
}
show() {
this._element.style.visibility = 'visible';
}
layout(
element: HTMLElement,
dimension?: { width: number; height: number }
) {
if (!this.element || !this.element.parentElement) {
return;
}
const frameRect = element.getBoundingClientRect();
const containerRect =
this.element.parentElement.getBoundingClientRect();
this.element.style.position = 'absolute';
this.element.style.top = `${frameRect.top - containerRect.top}px`;
this.element.style.left = `${frameRect.left - containerRect.left}px`;
this.element.style.width = `${
dimension ? dimension.width : frameRect.width
}px`;
this.element.style.height = `${
dimension ? dimension.height : frameRect.height
}px`;
}
dispose() {
this._element.remove();
}
}

View File

@ -1,5 +1,3 @@
export * from './hostedContainer';
export * from './dnd/dataTransfer';
export { watchElementResize } from './dom';

View File

@ -9,10 +9,9 @@ import {
toggleClass,
getElementsByTagName,
} from '../dom';
import { clamp } from '../math';
import { Event, Emitter } from '../events';
import { pushToStart, pushToEnd, firstIndex } from '../array';
import { range } from '../math';
import { range, clamp } from '../math';
import { ViewItem } from './viewItem';
export enum Orientation {

View File

@ -1,6 +1,5 @@
import * as React from 'react';
import { ReactPart, ReactPortalStore } from '../react';
import { IGroupPanelBaseProps } from './dockview';
import {
PanelUpdateEvent,
DockviewGroupPanel,

View File

@ -32,7 +32,7 @@ import DockviewSetTitle from '@site/sandboxes/updatetitle-dockview/src/app';
Dockview is an abstraction built on top of [Gridviews](./gridview) where each view is a container of many tabbed panels.
<Container>
<Container sandboxId="simple-dockview">
<SimpleDockview />
</Container>
@ -209,7 +209,7 @@ const onReady = (event: DockviewReadyEvent) => {
Here is an example using the above code loading from and saving to localStorage.
If you refresh the page you should notice your layout is loaded as you left it.
<Container>
<Container sandboxId="layout-dockview">
<DockviewPersistance />
</Container>
@ -238,7 +238,7 @@ props.api.group.api.setSize({
You can see an example invoking both approaches below.
<Container>
<Container sandboxId="resize-dockview">
<ResizeDockview />
</Container>
@ -248,7 +248,7 @@ When the dockview is empty you may want to display some fallback content, this i
By default there the watermark has no content but you can provide as a prop to `DockviewReact` a `watermarkComponent`
which will be rendered when there are no panels or groups.
<Container>
<Container sandboxId="watermark-dockview">
<DockviewWatermark />
</Container>
@ -327,7 +327,7 @@ return (
);
```
<Container>
<Container sandboxId="dnd-dockview">
<DndDockview />
</Container>
@ -525,7 +525,7 @@ As a simple example the below attaches a custom event handler for the context me
The below example uses a custom tab renderer to reigster a popover when the user right clicked on a tab.
This still makes use of the `DockviewDefaultTab` since it's only a minor change.
<Container>
<Container sandboxId="customheader-dockview">
<CustomHeadersDockview />
</Container>
@ -550,7 +550,7 @@ api.setTitle('my_new_custom_title');
> Note this only works when using the default tab implementation.
<Container>
<Container sandboxId="updatetitle-dockview">
<DockviewSetTitle />
</Container>
@ -599,7 +599,7 @@ to the entire width of the group. For example:
<DockviewReactComponent singleTabMode="fullwidth" {...otherProps} />
```
<Container>
<Container sandboxId="fullwidthtab-dockview">
<DockviewNative />
</Container>
@ -653,7 +653,7 @@ const GroupControlComponent = (props: IDockviewGroupControlProps) => {
};
```
<Container>
<Container sandboxId="groupcontrol-dockview">
<DockviewGroupControl />
</Container>
@ -669,13 +669,15 @@ api.group.api.setConstraints(...)
> If you specific a constraint on a group and move a panel within that group to another group it will no
> longer be subject to those constraints since those constraints were on the group and not on the individual panel.
<Container height={500}>
<Container height={500} sandboxId="constraints-dockview">
<DockviewConstraints />
</Container>
## Events
<Container height={600}>
A simple example showing events fired by `dockviewz that can be interacted with.
<Container height={600} sandboxId="events-dockview">
<EventsDockview />
</Container>
@ -686,7 +688,7 @@ api.group.api.setConstraints(...)
You can safely create multiple dockview instances within one page and nest dockviews within other dockviews.
If you wish to interact with the drop event from one dockview instance in another dockview instance you can implement the `showDndOverlay` and `onDidDrop` props on `DockviewReact`.
<Container>
<Container sandboxId="nested-dockview">
<NestedDockview />
</Container>

View File

@ -9,41 +9,16 @@ import {
import * as React from 'react';
const components = {
default: (props: IDockviewPanelProps<{ title: string }>) => {
default: (props: IDockviewPanelProps) => {
const [contraints, setContraints] =
React.useState<GridConstraintChangeEvent | null>(null);
React.useEffect(() => {
props.api.group.api.setConstraints({
maximumHeight: 200,
maximumWidth: 200,
props.api.group.api.onDidConstraintsChange((event) => {
setContraints(event);
});
}, []);
React.useEffect(() => {
const disposable1 = new DockviewMutableDisposable();
const disposable = props.api.onDidGroupChange(() => {
disposable1.value = props.api.group.api.onDidConstraintsChange(
(event) => {
setContraints(event);
}
);
});
setContraints({
maximumHeight: props.api.group.maximumHeight,
minimumHeight: props.api.group.minimumHeight,
maximumWidth: props.api.group.maximumWidth,
minimumWidth: props.api.group.minimumWidth,
});
return () => {
disposable1.dispose();
disposable.dispose();
};
}, []);
return (
<div
style={{
@ -53,13 +28,64 @@ const components = {
color: 'white',
}}
>
<span> {props.params.title}</span>
{contraints && (
<div>
<div>{`minHeight=${contraints.minimumHeight}`}</div>
<div>{`maxHeight=${contraints.maximumHeight}`}</div>
<div>{`minWidth=${contraints.minimumWidth}`}</div>
<div>{`maxWidth=${contraints.maximumWidth}`}</div>
<div style={{ fontSize: '13px' }}>
{typeof contraints.maximumHeight === 'number' && (
<div
style={{
border: '1px solid grey',
margin: '2px',
padding: '1px',
}}
>
<span
style={{ color: 'grey' }}
>{`Maximum Height: `}</span>
<span>{`${contraints.maximumHeight}px`}</span>
</div>
)}
{typeof contraints.minimumHeight === 'number' && (
<div
style={{
border: '1px solid grey',
margin: '2px',
padding: '1px',
}}
>
<span
style={{ color: 'grey' }}
>{`Minimum Height: `}</span>
<span>{`${contraints.minimumHeight}px`}</span>
</div>
)}
{typeof contraints.maximumWidth === 'number' && (
<div
style={{
border: '1px solid grey',
margin: '2px',
padding: '1px',
}}
>
<span
style={{ color: 'grey' }}
>{`Maximum Width: `}</span>
<span>{`${contraints.maximumWidth}px`}</span>
</div>
)}
{typeof contraints.minimumWidth === 'number' && (
<div
style={{
border: '1px solid grey',
margin: '2px',
padding: '1px',
}}
>
<span
style={{ color: 'grey' }}
>{`Minimum Width: `}</span>
<span>{`${contraints.minimumWidth}px`}</span>
</div>
)}
</div>
)}
</div>
@ -71,19 +97,40 @@ const App = () => {
const [api, setApi] = React.useState<DockviewApi>();
const onReady = (event: DockviewReadyEvent) => {
event.api.addPanel({
const panel1 = event.api.addPanel({
id: 'panel_1',
component: 'default',
});
event.api.addPanel({
const panel2 = event.api.addPanel({
id: 'panel_2',
component: 'default',
position: {
referencePanel: panel1,
direction: 'right',
},
});
event.api.addPanel({
const panel3 = event.api.addPanel({
id: 'panel_3',
component: 'default',
position: {
referencePanel: panel2,
direction: 'right',
},
});
const panel4 = event.api.addPanel({
id: 'panel_4',
component: 'default',
position: {
direction: 'below',
},
});
panel2.api.group.api.setConstraints({
maximumWidth: 300,
maximumHeight: 300,
});
};

View File

@ -182,49 +182,49 @@ const EventsDockview = () => {
panels: {
panel_1: {
id: 'panel_1',
view: { content: { id: 'default' } },
contentComponent: 'default',
params: { title: 'Panel 1' },
title: 'panel_1',
},
panel_2: {
id: 'panel_2',
view: { content: { id: 'default' } },
contentComponent: 'default',
params: { title: 'Panel 2' },
title: 'panel_2',
},
panel_3: {
id: 'panel_3',
view: { content: { id: 'default' } },
contentComponent: 'default',
params: { title: 'Panel 3' },
title: 'panel_3',
},
panel_4: {
id: 'panel_4',
view: { content: { id: 'default' } },
contentComponent: 'default',
params: { title: 'Panel 4' },
title: 'panel_4',
},
panel_5: {
id: 'panel_5',
view: { content: { id: 'default' } },
contentComponent: 'default',
params: { title: 'Panel 5' },
title: 'panel_5',
},
panel_6: {
id: 'panel_6',
view: { content: { id: 'default' } },
contentComponent: 'default',
params: { title: 'Panel 6' },
title: 'panel_6',
},
panel_8: {
id: 'panel_8',
view: { content: { id: 'default' } },
contentComponent: 'default',
params: { title: 'Panel 8' },
title: 'panel_8',
},
panel_7: {
id: 'panel_7',
view: { content: { id: 'default' } },
contentComponent: 'default',
params: { title: 'Panel 7' },
title: 'panel_7',
},
@ -334,7 +334,7 @@ const EventsDockview = () => {
className="dockview-theme-abyss"
/>
</div>
<div style={{ flexGrow: 1 }}>
<div style={{ flexGrow: 1, paddingTop: '5px' }}>
<Console lines={lines} />
</div>
</div>

View File

@ -0,0 +1,22 @@
{
"name": "vanilla-dockview",
"description": "",
"keywords": [
"dockview"
],
"version": "1.0.0",
"main": "src/index.ts",
"dependencies": {
"dockview-core": "*"
},
"devDependencies": {
"typescript": "^4.9.5"
},
"scripts": {},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}

View File

@ -0,0 +1,43 @@
<!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">
<!--
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,88 @@
import {
DockviewComponent,
IContentRenderer,
IGroupPanelInitParameters,
} from 'dockview';
class DefaultPanel implements IContentRenderer {
private _element: HTMLElement;
get element(): HTMLElement {
return this._element;
}
constructor() {
this._element = document.createElement('div');
}
init(params: IGroupPanelInitParameters): void {
//
}
}
export function attach(parent: HTMLElement): {
dispose: () => void;
} {
const element = document.createElement('div');
element.className = 'dockview-theme-abyss';
parent.appendChild(element);
const dockview = new DockviewComponent(element, {
components: {
default: DefaultPanel,
},
});
const observer = new ResizeObserver((entires) => {
const firstEntry = entires[0];
const { width, height } = firstEntry.contentRect;
dockview.layout(width, height);
});
observer.observe(parent);
const panel1 = dockview.addPanel({
id: 'panel_1',
title: 'Panel 1',
component: 'default',
});
const panel2 = dockview.addPanel({
id: 'panel_2',
title: 'Panel 2',
component: 'default',
position: {
referencePanel: panel1,
direction: 'right',
},
});
const panel3 = dockview.addPanel({
id: 'panel_3',
title: 'Panel 3',
component: 'default',
position: {
referenceGroup: panel2.group,
},
});
const pane4 = dockview.addPanel({
id: 'panel_4',
title: 'Panel 4',
component: 'default',
position: {
direction: 'below',
},
});
return {
dispose: () => {
observer.unobserve(element);
observer.disconnect();
dockview.dispose();
element.remove();
},
};
}

View File

@ -0,0 +1,10 @@
import './styles.css';
import 'dockview/dist/styles/dockview.css';
import { attach } from './app';
const rootElement = document.getElementById('root');
if (rootElement) {
attach(rootElement);
}

View File

@ -0,0 +1,16 @@
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,20 @@
{
"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,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true
}
}

View File

@ -1,7 +1,9 @@
import useBaseUrl from '@docusaurus/useBaseUrl';
import * as React from 'react';
import './container.scss';
const BASE_SANDBOX_URL =
'https://codesandbox.io/s/github/mathuo/dockview/tree/master/packages/docs/sandboxes';
const createSvgElementFromPath = (params: {
height: string;
width: string;
@ -33,9 +35,17 @@ export const Container = (props: {
children?: React.ReactNode;
height?: number;
injectVanillaJS?: (parent: HTMLElement) => void;
sandboxId?: string;
}) => {
const ref = React.useRef<HTMLDivElement>(null);
const url = React.useMemo(() => {
if (!props.sandboxId) {
return '';
}
return `${BASE_SANDBOX_URL}/${props.sandboxId}`;
}, [props.sandboxId]);
React.useEffect(() => {
if (!props.injectVanillaJS) {
return;
@ -64,24 +74,29 @@ export const Container = (props: {
>
<span style={{ flexGrow: 1 }} />
<span
className="codesandbox-button"
style={{ display: 'flex', alignItems: 'center' }}
>
<span className="codesandbox-button-pretext">{`Open in `}</span>
<a
href="https://www.google.com"
target={'_blank'}
className="codesandbox-button-content"
{url && (
<span
className="codesandbox-button"
style={{ display: 'flex', alignItems: 'center' }}
>
<span
style={{ fontWeight: 'bold', paddingRight: '4px' }}
<span className="codesandbox-button-pretext">{`Open in `}</span>
<a
href={url}
target={'_blank'}
className="codesandbox-button-content"
>
CodeSandbox
</span>
<CreateCloseButton />
</a>
</span>
<span
style={{
fontWeight: 'bold',
paddingRight: '4px',
}}
>
CodeSandbox
</span>
<CreateCloseButton />
</a>
</span>
)}
{/* <span
style={{ fontSize: '16px' }}
className="material-symbols-outlined"