feat: setVisible enhancements

This commit is contained in:
mathuo 2024-06-21 21:40:20 +01:00
parent 3652505a08
commit ae88703a8b
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
17 changed files with 373 additions and 39 deletions

View File

@ -10,7 +10,7 @@ import { CompositeDisposable } from '../../lifecycle';
import { Emitter } from '../../events';
import { DockviewPanel, IDockviewPanel } from '../../dockview/dockviewPanel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { fireEvent } from '@testing-library/dom';
import { fireEvent, getByTestId, queryByTestId } from '@testing-library/dom';
import { getPanelData } from '../../dnd/dataTransfer';
import {
GroupDragEvent,
@ -5097,6 +5097,60 @@ describe('dockviewComponent', () => {
expect(panel4.api.isVisible).toBeTruthy();
});
test('setVisible #1', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent({
parentElement: container,
createComponent(options) {
switch (options.name) {
case 'default':
return new PanelContentPartTest(
options.id,
options.name
);
default:
throw new Error(`unsupported`);
}
},
});
const api = new DockviewApi(dockview);
dockview.layout(1000, 1000);
const panel1 = api.addPanel({
id: 'panel1',
component: 'default',
});
const panel2 = api.addPanel({
id: 'panel2',
component: 'default',
position: { referencePanel: panel1, direction: 'below' },
});
const panel3 = api.addPanel({
id: 'panel3',
component: 'default',
position: { referencePanel: panel1, direction: 'below' },
});
expect(api.groups.length).toBe(3);
panel1.group.api.setVisible(false);
panel2.group.api.setVisible(false);
panel3.group.api.setVisible(false);
expect(panel1.group.api.isVisible).toBeFalsy();
expect(panel2.group.api.isVisible).toBeFalsy();
expect(panel3.group.api.isVisible).toBeFalsy();
panel1.group.api.setVisible(true);
expect(panel1.group.api.isVisible).toBeTruthy();
expect(panel2.group.api.isVisible).toBeFalsy();
expect(panel3.group.api.isVisible).toBeFalsy();
});
describe('addPanel', () => {
test('that can add panel', () => {
const container = document.createElement('div');
@ -5429,4 +5483,57 @@ describe('dockviewComponent', () => {
expect(api.panels.length).toBe(3);
expect(api.groups.length).toBe(3);
});
test('that watermark appears when all views are not visible', () => {
jest.useFakeTimers();
const container = document.createElement('div');
const dockview = new DockviewComponent({
parentElement: container,
createComponent(options) {
switch (options.name) {
case 'default':
return new PanelContentPartTest(
options.id,
options.name
);
default:
throw new Error(`unsupported`);
}
},
});
const api = new DockviewApi(dockview);
dockview.layout(1000, 1000);
const panel1 = api.addPanel({
id: 'panel_1',
component: 'default',
});
const panel2 = api.addPanel({
id: 'panel_2',
component: 'default',
position: {
direction: 'right',
},
});
let query = queryByTestId(container, 'watermark-component');
expect(query).toBeFalsy();
panel1.group.api.setVisible(false);
jest.runAllTicks(); // visibility events check fires on microtask-queue
query = queryByTestId(container, 'watermark-component');
expect(query).toBeFalsy();
panel2.group.api.setVisible(false);
jest.runAllTicks(); // visibility events check fires on microtask-queue
query = queryByTestId(container, 'watermark-component');
expect(query).toBeTruthy();
panel1.group.api.setVisible(true);
jest.runAllTicks(); // visibility events check fires on microtask-queue
query = queryByTestId(container, 'watermark-component');
expect(query).toBeFalsy();
});
});

View File

@ -17,13 +17,9 @@ class TestPanel implements IGridPanelView {
_onDidChange = new Emitter<IViewSize | undefined>();
readonly onDidChange = this._onDidChange.event;
get isActive(): boolean {
return true;
}
get params(): Parameters {
return {};
}
isVisible: boolean = true;
isActive: boolean = true;
params: Parameters = {};
constructor(
public readonly id: string,

View File

@ -5,6 +5,7 @@ import {
IGridView,
IViewSize,
SerializedGridview,
getGridLocation,
orthogonal,
} from '../../gridview/gridview';
import { Orientation, Sizing } from '../../splitview/splitview';
@ -18,7 +19,7 @@ class MockGridview implements IGridView {
IViewSize | undefined
>().event;
element: HTMLElement = document.createElement('div');
isVisible: boolean = true;
width: number = 0;
height: number = 0;
@ -1105,4 +1106,102 @@ describe('gridview', () => {
expect(gridview.hasMaximizedView()).toBeFalsy();
});
test('visibility check', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
const view4 = new MockGridview('4');
const view5 = new MockGridview('5');
const view6 = new MockGridview('6');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
gridview.addView(view4, Sizing.Distribute, [1, 1, 0]);
gridview.addView(view5, Sizing.Distribute, [1, 1, 0, 0]);
gridview.addView(view6, Sizing.Distribute, [1, 1, 0, 0, 0]);
/**
* _____________________________________________
* | | |
* | | 2 |
* | | |
* | 1 |_______________________|
* | | | 4 |
* | | 3 |_____________|
* | | | 5 | 6 |
* |_____________________|_________|______|______|
*/
function assertVisibility(visibility: boolean[]) {
expect(gridview.isViewVisible(getGridLocation(view1.element))).toBe(
visibility[0]
);
expect(gridview.isViewVisible(getGridLocation(view2.element))).toBe(
visibility[1]
);
expect(gridview.isViewVisible(getGridLocation(view3.element))).toBe(
visibility[2]
);
expect(gridview.isViewVisible(getGridLocation(view4.element))).toBe(
visibility[3]
);
expect(gridview.isViewVisible(getGridLocation(view5.element))).toBe(
visibility[4]
);
expect(gridview.isViewVisible(getGridLocation(view6.element))).toBe(
visibility[5]
);
}
// hide each view one by one
assertVisibility([true, true, true, true, true, true]);
gridview.setViewVisible(getGridLocation(view5.element), false);
assertVisibility([true, true, true, true, false, true]);
gridview.setViewVisible(getGridLocation(view4.element), false);
assertVisibility([true, true, true, false, false, true]);
gridview.setViewVisible(getGridLocation(view1.element), false);
assertVisibility([false, true, true, false, false, true]);
gridview.setViewVisible(getGridLocation(view2.element), false);
assertVisibility([false, false, true, false, false, true]);
gridview.setViewVisible(getGridLocation(view3.element), false);
assertVisibility([false, false, false, false, false, true]);
gridview.setViewVisible(getGridLocation(view6.element), false);
assertVisibility([false, false, false, false, false, false]);
// un-hide each view one by one
gridview.setViewVisible(getGridLocation(view1.element), true);
assertVisibility([true, false, false, false, false, false]);
gridview.setViewVisible(getGridLocation(view5.element), true);
assertVisibility([true, false, false, false, true, false]);
gridview.setViewVisible(getGridLocation(view6.element), true);
assertVisibility([true, false, false, false, true, true]);
gridview.setViewVisible(getGridLocation(view2.element), true);
assertVisibility([true, true, false, false, true, true]);
gridview.setViewVisible(getGridLocation(view3.element), true);
assertVisibility([true, true, true, false, true, true]);
gridview.setViewVisible(getGridLocation(view4.element), true);
assertVisibility([true, true, true, true, true, true]);
});
});

View File

@ -8,10 +8,8 @@ describe('math', () => {
expect(clamp(55, 40, 50)).toBe(50);
});
it('should throw an error if min > max', () => {
expect(() => clamp(55, 50, 40)).toThrow(
'50 > 40 is an invalid condition'
);
it('if min > max return min', () => {
expect(clamp(55, 50, 40)).toBe(50);
});
});

View File

@ -51,7 +51,7 @@ import { DockviewPanelModel } from './dockviewPanelModel';
import { getPanelData } from '../dnd/dataTransfer';
import { Parameters } from '../panel/types';
import { Overlay } from '../dnd/overlay';
import { toggleClass, watchElementResize } from '../dom';
import { addTestId, toggleClass, watchElementResize } from '../dom';
import { DockviewFloatingGroupPanel } from './dockviewFloatingGroupPanel';
import {
GroupDragEvent,
@ -372,6 +372,9 @@ export class DockviewComponent
this._onDidRemoveGroup,
this._onDidActiveGroupChange,
this._onUnhandledDragOverEvent,
this.onDidViewVisibilityChangeMicroTaskQueue(() => {
this.updateWatermark();
}),
this.onDidAdd((event) => {
if (!this._moving) {
this._onDidAddGroup.fire(event);
@ -1519,6 +1522,7 @@ export class DockviewComponent
const watermarkContainer = document.createElement('div');
watermarkContainer.className = 'dv-watermark-container';
addTestId(watermarkContainer, 'watermark-component');
watermarkContainer.appendChild(this.watermark.element);
this.gridview.element.appendChild(watermarkContainer);

View File

@ -253,3 +253,7 @@ export function isInDocument(element: Element): boolean {
return false;
}
export function addTestId(element: HTMLElement, id: string): void {
element.setAttribute('data-testid', id);
}

View File

@ -93,6 +93,10 @@ export abstract class BaseGrid<T extends IGridPanelView>
readonly onDidLayoutChange: Event<void> =
this._bufferOnDidLayoutChange.onEvent;
private readonly _onDidViewVisibilityChangeMicroTaskQueue = new AsapEvent();
readonly onDidViewVisibilityChangeMicroTaskQueue =
this._onDidViewVisibilityChangeMicroTaskQueue.onEvent;
get id(): string {
return this._id;
}
@ -158,6 +162,12 @@ export abstract class BaseGrid<T extends IGridPanelView>
this.layout(0, 0, true); // set some elements height/widths
this.addDisposables(
this.gridview.onDidViewVisibilityChange(() =>
this._onDidViewVisibilityChangeMicroTaskQueue.fire()
),
this.onDidViewVisibilityChangeMicroTaskQueue(() => {
this.layout(this.width, this.height, true);
}),
Disposable.from(() => {
this.element.parentElement?.removeChild(this.element);
}),

View File

@ -33,9 +33,12 @@ export class BranchNode extends CompositeDisposable implements IView {
readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> =
this._onDidChange.event;
private readonly _onDidVisibilityChange = new Emitter<boolean>();
readonly onDidVisibilityChange: Event<boolean> =
this._onDidVisibilityChange.event;
private readonly _onDidVisibilityChange = new Emitter<{
visible: boolean;
}>();
readonly onDidVisibilityChange: Event<{
visible: boolean;
}> = this._onDidVisibilityChange.event;
get width(): number {
return this.orientation === Orientation.HORIZONTAL
@ -200,10 +203,8 @@ export class BranchNode extends CompositeDisposable implements IView {
this.setupChildrenEvents();
}
setVisible(visible: boolean): void {
for (const child of this.children) {
child.setVisible(visible);
}
setVisible(_visible: boolean): void {
// noop
}
isChildVisible(index: number): boolean {
@ -224,7 +225,9 @@ export class BranchNode extends CompositeDisposable implements IView {
}
const wereAllChildrenHidden = this.splitview.contentSize === 0;
this.splitview.setViewVisible(index, visible);
// }
const areAllChildrenHidden = this.splitview.contentSize === 0;
// If all children are hidden then the parent should hide the entire splitview
@ -233,7 +236,7 @@ export class BranchNode extends CompositeDisposable implements IView {
(visible && wereAllChildrenHidden) ||
(!visible && areAllChildrenHidden)
) {
this._onDidVisibilityChange.fire(visible);
this._onDidVisibilityChange.fire({ visible });
}
}
@ -335,7 +338,7 @@ export class BranchNode extends CompositeDisposable implements IView {
}),
...this.children.map((c, i) => {
if (c instanceof BranchNode) {
return c.onDidVisibilityChange((visible) => {
return c.onDidVisibilityChange(({ visible }) => {
this.setChildVisible(i, visible);
});
}

View File

@ -171,6 +171,7 @@ export interface IGridView {
readonly maximumWidth: number;
readonly minimumHeight: number;
readonly maximumHeight: number;
readonly isVisible: boolean;
priority?: LayoutPriority;
layout(width: number, height: number): void;
toJSON(): object;
@ -287,6 +288,9 @@ export class Gridview implements IDisposable {
readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> =
this._onDidChange.event;
private readonly _onDidViewVisibilityChange = new Emitter<void>();
readonly onDidViewVisibilityChange = this._onDidViewVisibilityChange.event;
private readonly _onDidMaximizedNodeChange = new Emitter<void>();
readonly onDidMaximizedNodeChange = this._onDidMaximizedNodeChange.event;
@ -453,6 +457,8 @@ export class Gridview implements IDisposable {
this.disposable.dispose();
this._onDidChange.dispose();
this._onDidMaximizedNodeChange.dispose();
this._onDidViewVisibilityChange.dispose();
this.root.dispose();
this._maximizedNode = undefined;
this.element.remove();
@ -531,12 +537,12 @@ export class Gridview implements IDisposable {
children
);
} else {
result = new LeafNode(
deserializer.fromJSON(node),
orientation,
orthogonalSize,
node.size
);
const view = deserializer.fromJSON(node);
if (typeof node.visible === 'boolean') {
view.setVisible?.(node.visible);
}
result = new LeafNode(view, orientation, orthogonalSize, node.size);
}
return result;
@ -723,6 +729,8 @@ export class Gridview implements IDisposable {
throw new Error('Invalid from location');
}
this._onDidViewVisibilityChange.fire();
parent.setChildVisible(index, visible);
}

View File

@ -126,6 +126,10 @@ export abstract class GridviewPanel<
return this.api.isActive;
}
get isVisible(): boolean {
return this.api.isVisible;
}
constructor(
id: string,
component: string,

View File

@ -1,6 +1,10 @@
export const clamp = (value: number, min: number, max: number): number => {
if (min > max) {
throw new Error(`${min} > ${max} is an invalid condition`);
/**
* caveat: an error should be thrown here if this was a proper `clamp` function but we need to handle
* cases where `min` > `max` and in those cases return `min`.
*/
return min;
}
return Math.min(max, Math.max(value, min));
};

View File

@ -291,12 +291,8 @@ export class Splitview {
throw new Error('Index out of bounds');
}
toggleClass(this.container, 'visible', visible);
const viewItem = this.viewItems[index];
toggleClass(this.container, 'visible', visible);
viewItem.setVisible(visible, viewItem.size);
this.distributeEmptySpace(index);

View File

@ -77,6 +77,11 @@
border: none;
cursor: pointer;
&:disabled {
color: gray;
cursor: help;
}
span {
font-size: 16px;
}

View File

@ -24,6 +24,7 @@ export const GridActions = (props: {
const onSave = () => {
if (props.api) {
console.log(props.api.toJSON());
localStorage.setItem(
'dv-demo-state',
JSON.stringify(props.api.toJSON())

View File

@ -1,4 +1,8 @@
import { DockviewApi, DockviewGroupLocation } from 'dockview';
import {
DockviewApi,
DockviewGroupLocation,
DockviewGroupPanel,
} from 'dockview';
import * as React from 'react';
const GroupAction = (props: {
@ -12,14 +16,27 @@ const GroupAction = (props: {
};
const isActive = props.activeGroup === props.groupId;
const group = React.useMemo(
() => props.api.getGroup(props.groupId),
[props.api, props.groupId]
const [group, setGroup] = React.useState<DockviewGroupPanel | undefined>(
undefined
);
React.useEffect(() => {
const disposable = props.api.onDidLayoutFromJSON(() => {
setGroup(props.api.getGroup(props.groupId));
});
setGroup(props.api.getGroup(props.groupId));
return () => {
disposable.dispose();
};
}, [props.api, props.groupId]);
const [location, setLocation] =
React.useState<DockviewGroupLocation | null>(null);
const [isMaximized, setIsMaximized] = React.useState<boolean>(false);
const [isVisible, setIsVisible] = React.useState<boolean>(true);
React.useEffect(() => {
if (!group) {
@ -35,12 +52,18 @@ const GroupAction = (props: {
setIsMaximized(group.api.isMaximized());
});
const disposable3 = group.api.onDidVisibilityChange(() => {
setIsVisible(group.api.isVisible);
});
setLocation(group.api.location);
setIsMaximized(group.api.isMaximized());
setIsVisible(group.api.isVisible);
return () => {
disposable.dispose();
disposable2.dispose();
disposable3.dispose();
};
}, [group]);
@ -107,6 +130,23 @@ const GroupAction = (props: {
fullscreen
</span>
</button>
<button
className="demo-icon-button"
onClick={() => {
console.log(group);
if (group) {
if (group.api.isVisible) {
group.api.setVisible(false);
} else {
group.api.setVisible(true);
}
}
}}
>
<span className="material-symbols-outlined">
{isVisible ? 'visibility' : 'visibility_off'}
</span>
</button>
<button
className="demo-icon-button"
onClick={() => {

View File

@ -1,4 +1,5 @@
import { DockviewApi } from 'dockview';
import { DockviewApi, IDockviewPanel } from 'dockview';
import * as React from 'react';
const PanelAction = (props: {
panels: string[];
@ -9,6 +10,50 @@ const PanelAction = (props: {
const onClick = () => {
props.api.getPanel(props.panelId)?.focus();
};
React.useEffect(() => {
const panel = props.api.getPanel(props.panelId);
if (panel) {
const disposable = panel.api.onDidVisibilityChange((event) => {
setVisible(event.isVisible);
});
setVisible(panel.api.isVisible);
return () => {
disposable.dispose();
};
}
}, [props.api, props.panelId]);
const [panel, setPanel] = React.useState<IDockviewPanel | undefined>(
undefined
);
React.useEffect(() => {
const list = [
props.api.onDidLayoutFromJSON(() => {
setPanel(props.api.getPanel(props.panelId));
}),
];
if (panel) {
const disposable = panel.api.onDidVisibilityChange((event) => {
setVisible(event.isVisible);
});
setVisible(panel.api.isVisible);
list.push(disposable);
}
setPanel(props.api.getPanel(props.panelId));
return () => {
list.forEach((l) => l.dispose());
};
}, [props.api, props.panelId]);
const [visible, setVisible] = React.useState<boolean>(true);
return (
<div className="button-action">
<div style={{ display: 'flex' }}>
@ -57,6 +102,15 @@ const PanelAction = (props: {
>
<span className="material-symbols-outlined">close</span>
</button>
<button
title="Panel visiblity cannot be edited manually."
disabled={true}
className="demo-icon-button"
>
<span className="material-symbols-outlined">
{visible ? 'visibility' : 'visibility_off'}
</span>
</button>
</div>
</div>
);

1
packages/docs/static/img/vue-icon.svg vendored Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B