🔀 merge: update from master

This commit is contained in:
Ademola Adedeji 2025-04-02 08:51:40 -03:00
commit 3785771658
103 changed files with 4282 additions and 1262 deletions

View File

@ -27,7 +27,7 @@ jobs:
- run: npm run build
- run: npm run test:cov
- name: SonarCloud Scan
uses: sonarsource/sonarqube-scan-action@v4.1.0
uses: sonarsource/sonarqube-scan-action@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View File

@ -2,7 +2,7 @@
"packages": [
"packages/*"
],
"version": "3.2.0",
"version": "4.2.2",
"npmClient": "yarn",
"command": {
"publish": {

View File

@ -1,6 +1,6 @@
{
"name": "dockview-angular",
"version": "3.2.0",
"version": "4.2.1",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"keywords": [
"splitview",
@ -54,6 +54,6 @@
"test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview --coverage"
},
"dependencies": {
"dockview-core": "^3.2.0"
"dockview-core": "^4.2.1"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "dockview-core",
"version": "3.2.0",
"version": "4.2.1",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"keywords": [
"splitview",

View File

@ -1,11 +1,12 @@
import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import {
GroupPanelPartInitParameters,
TabPartInitParameters,
IContentRenderer,
ITabRenderer,
} from '../../dockview/types';
import { PanelUpdateEvent } from '../../panel/types';
import { TabLocation } from '../../dockview/framework';
export class DockviewPanelModelMock implements IDockviewPanelModel {
constructor(
@ -17,8 +18,11 @@ export class DockviewPanelModelMock implements IDockviewPanelModel {
//
}
createTabRenderer(tabLocation: TabLocation): ITabRenderer {
return this.tab;
}
init(params: GroupPanelPartInitParameters): void {
init(params: TabPartInitParameters): void {
//
}

View File

@ -10,6 +10,7 @@ describe('groupPanelApi', () => {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const panelMock = jest.fn<DockviewPanel, []>(() => {
@ -51,6 +52,7 @@ describe('groupPanelApi', () => {
onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupViewPanel = new DockviewGroupPanel(
@ -84,6 +86,7 @@ describe('groupPanelApi', () => {
onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupViewPanel = new DockviewGroupPanel(

View File

@ -16,10 +16,10 @@ describe('droptarget', () => {
beforeEach(() => {
element = document.createElement('div');
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 200);
});
test('that dragover events are marked', () => {

View File

@ -8,6 +8,7 @@ import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel';
import { Tab } from '../../../dockview/components/tab/tab';
import { IDockviewPanel } from '../../../dockview/dockviewPanel';
import { fromPartial } from '@total-typescript/shoehorn';
describe('tab', () => {
test('that empty tab has inactive-tab class', () => {
@ -46,15 +47,10 @@ describe('tab', () => {
id: 'testcomponentid',
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
@ -72,38 +68,33 @@ describe('tab', () => {
groupPanel
);
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);
fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toBeCalled();
expect(groupView.canDisplayOverlay).toHaveBeenCalled();
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
});
test('that if you drag over yourself no drop target is shown', () => {
test('that if you drag over yourself a drop target is shown', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
@ -121,10 +112,10 @@ describe('tab', () => {
groupPanel
);
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -136,11 +127,11 @@ describe('tab', () => {
fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
).toBe(1);
});
test('that if you drag over another tab a drop target is shown', () => {
@ -175,10 +166,10 @@ describe('tab', () => {
groupPanel
);
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -229,10 +220,10 @@ describe('tab', () => {
groupPanel
);
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -289,10 +280,10 @@ describe('tab', () => {
groupPanel
);
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100
);

View File

@ -0,0 +1,66 @@
import { Tabs } from '../../../../dockview/components/titlebar/tabs';
import { fromPartial } from '@total-typescript/shoehorn';
import { DockviewGroupPanel } from '../../../../dockview/dockviewGroupPanel';
import { DockviewComponent } from '../../../../dockview/dockviewComponent';
describe('tabs', () => {
describe('disableCustomScrollbars', () => {
test('enabled by default', () => {
const cut = new Tabs(
fromPartial<DockviewGroupPanel>({}),
fromPartial<DockviewComponent>({
options: {},
}),
{
showTabsOverflowControl: true,
}
);
expect(
cut.element.querySelectorAll(
'.dv-scrollable > .dv-tabs-container'
).length
).toBe(1);
});
test('enabled when disabled flag is false', () => {
const cut = new Tabs(
fromPartial<DockviewGroupPanel>({}),
fromPartial<DockviewComponent>({
options: {
scrollbars: 'custom',
},
}),
{
showTabsOverflowControl: true,
}
);
expect(
cut.element.querySelectorAll(
'.dv-scrollable > .dv-tabs-container'
).length
).toBe(1);
});
test('disabled when disabled flag is true', () => {
const cut = new Tabs(
fromPartial<DockviewGroupPanel>({}),
fromPartial<DockviewComponent>({
options: {
scrollbars: 'native',
},
}),
{
showTabsOverflowControl: true,
}
);
expect(
cut.element.querySelectorAll(
'.dv-scrollable > .dv-tabs-container'
).length
).toBe(0);
});
});
});

View File

@ -19,6 +19,7 @@ describe('tabsContainer', () => {
onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -43,16 +44,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0);
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -73,17 +74,17 @@ describe('tabsContainer', () => {
onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const dropTargetContainer = document.createElement('div');
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
// dropTargetContainer: new DropTargetAnchorContainer(
// dropTargetContainer
// ),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
@ -99,16 +100,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0);
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -131,6 +132,10 @@ describe('tabsContainer', () => {
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
// expect(
// dropTargetContainer.getElementsByClassName('dv-drop-target-anchor')
// .length
// ).toBe(1);
});
test('that dropping over the empty space should render a drop target', () => {
@ -140,6 +145,7 @@ describe('tabsContainer', () => {
onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -169,16 +175,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0);
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -204,6 +210,7 @@ describe('tabsContainer', () => {
onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -233,16 +240,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0);
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -268,6 +275,7 @@ describe('tabsContainer', () => {
onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -296,16 +304,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0);
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -337,6 +345,7 @@ describe('tabsContainer', () => {
onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -402,6 +411,7 @@ describe('tabsContainer', () => {
onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -469,6 +479,7 @@ describe('tabsContainer', () => {
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
doSetGroupActive: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -526,6 +537,7 @@ describe('tabsContainer', () => {
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
doSetGroupActive: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -578,6 +590,7 @@ describe('tabsContainer', () => {
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -635,6 +648,7 @@ describe('tabsContainer', () => {
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -703,6 +717,7 @@ describe('tabsContainer', () => {
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -771,6 +786,7 @@ describe('tabsContainer', () => {
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -834,6 +850,7 @@ describe('tabsContainer', () => {
const cut = new TabsContainer(
fromPartial<DockviewComponent>({
options: {},
onDidOptionsChange: jest.fn(),
}),
fromPartial<DockviewGroupPanel>({})
);

View File

@ -133,11 +133,15 @@ describe('dockviewComponent', () => {
},
className: 'test-a test-b',
});
expect(dockview.element.className).toBe('test-a test-b');
expect(dockview.element.className).toBe(
'test-a test-b dockview-theme-abyss'
);
dockview.updateOptions({ className: 'test-b test-c' });
expect(dockview.element.className).toBe('test-b test-c');
expect(dockview.element.className).toBe(
'dockview-theme-abyss test-b test-c'
);
});
describe('memory leakage', () => {
@ -2453,17 +2457,17 @@ describe('dockviewComponent', () => {
const group = dockview.getGroupPanel('panel2')!.api.group;
const viewQuery = group.element.querySelectorAll(
'.dv-groupview > .dv-tabs-and-actions-container > .dv-tabs-container > .dv-tab'
'.dv-groupview > .dv-tabs-and-actions-container > .dv-scrollable > .dv-tabs-container > .dv-tab'
);
expect(viewQuery.length).toBe(2);
const viewQuery2 = group.element.querySelectorAll(
'.dv-groupview > .dv-tabs-and-actions-container > .dv-tabs-container > .dv-tab > .dv-default-tab'
'.dv-groupview > .dv-tabs-and-actions-container > .dv-scrollable > .dv-tabs-container > .dv-tab > .dv-default-tab'
);
expect(viewQuery2.length).toBe(1);
const viewQuery3 = group.element.querySelectorAll(
'.dv-groupview > .dv-tabs-and-actions-container > .dv-tabs-container > .dv-tab > .panel-tab-part-panel2'
'.dv-groupview > .dv-tabs-and-actions-container > .dv-scrollable > .dv-tabs-container > .dv-tab > .panel-tab-part-panel2'
);
expect(viewQuery3.length).toBe(1);
});
@ -3376,10 +3380,10 @@ describe('dockviewComponent', () => {
position: { direction: 'right' },
});
Object.defineProperty(dockview.element, 'clientWidth', {
Object.defineProperty(dockview.element, 'offsetWidth', {
get: () => 100,
});
Object.defineProperty(dockview.element, 'clientHeight', {
Object.defineProperty(dockview.element, 'offsetHeight', {
get: () => 100,
});
@ -6726,8 +6730,55 @@ describe('dockviewComponent', () => {
expect(api.groups.length).toBe(3);
});
describe('updateOptions', () => {
test('gap', () => {
test('add group with custom group is', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(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 group1 = api.addGroup({
id: 'group_1',
direction: 'left',
});
const group2 = api.addGroup({
id: 'group_2',
direction: 'left',
referencePanel: panel1,
});
const group3 = api.addGroup({
id: 'group_3',
direction: 'left',
referenceGroup: panel1.api.group,
});
expect(group1.api.id).toBe('group_1');
expect(group2.api.id).toBe('group_2');
expect(group3.api.id).toBe('group_3');
});
describe('dndEdges', () => {
test('that can init dndEdges property', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
@ -6742,19 +6793,14 @@ describe('dockviewComponent', () => {
throw new Error(`unsupported`);
}
},
gap: 6,
dndEdges: {
size: { value: 100, type: 'pixels' },
activationSize: { value: 5, type: 'percentage' },
},
});
const api = new DockviewApi(dockview);
expect(dockview.gap).toBe(6);
dockview.updateOptions({ gap: 10 });
expect(dockview.gap).toBe(10);
dockview.updateOptions({});
expect(dockview.gap).toBe(10);
dockview.updateOptions({ gap: 15 });
expect(dockview.gap).toBe(15);
dockview.layout(1000, 1000);
});
});

View File

@ -16,6 +16,7 @@ describe('dockviewGroupPanel', () => {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const options = fromPartial<GroupOptions>({});
const cut = new DockviewGroupPanel(accessor, 'test_id', options);
@ -39,6 +40,7 @@ describe('dockviewGroupPanel', () => {
detatch: jest.fn(),
},
doSetGroupActive: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const options = fromPartial<GroupOptions>({});
@ -81,6 +83,7 @@ describe('dockviewGroupPanel', () => {
detatch: jest.fn(),
}),
options: {},
onDidOptionsChange: jest.fn(),
});
const options = fromPartial<GroupOptions>({});
const cut = new DockviewGroupPanel(accessor, 'test_id', options);

View File

@ -24,6 +24,7 @@ import { createOffsetDragOverEvent } from '../__test_utils__/utils';
import { OverlayRenderContainer } from '../../overlay/overlayRenderContainer';
import { Emitter } from '../../events';
import { fromPartial } from '@total-typescript/shoehorn';
import { TabLocation } from '../../dockview/framework';
enum GroupChangeKind2 {
ADD_PANEL,
@ -36,12 +37,16 @@ class TestModel implements IDockviewPanelModel {
readonly contentComponent: string;
readonly tab: ITabRenderer;
constructor(id: string) {
constructor(readonly id: string) {
this.content = new TestHeaderPart(id);
this.contentComponent = id;
this.tab = new TestContentPart(id);
}
createTabRenderer(tabLocation: TabLocation): ITabRenderer {
return new TestHeaderPart(this.id);
}
update(event: PanelUpdateEvent): void {
//
}
@ -270,6 +275,7 @@ describe('dockviewGroupPanelModel', () => {
document.createElement('div'),
fromPartial<DockviewComponent>({})
),
onDidOptionsChange: () => ({ dispose: jest.fn() }),
});
groupview = new DockviewGroupPanel(dockview, 'groupview-1', options);
@ -651,6 +657,7 @@ describe('dockviewGroupPanelModel', () => {
getPanel: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
onDidOptionsChange: jest.fn(),
onDidActivePanelChange: jest.fn(),
});
@ -690,12 +697,12 @@ describe('dockviewGroupPanelModel', () => {
const element = container
.getElementsByClassName('dv-content-container')
.item(0)!;
.item(0)! as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
@ -714,6 +721,7 @@ describe('dockviewGroupPanelModel', () => {
getPanel: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
onDidOptionsChange: jest.fn(),
onDidActivePanelChange: jest.fn(),
});
@ -751,12 +759,12 @@ describe('dockviewGroupPanelModel', () => {
const element = container
.getElementsByClassName('dv-content-container')
.item(0)!;
.item(0)! as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
function run(value: number) {
fireEvent.dragEnter(element);
@ -799,7 +807,7 @@ describe('dockviewGroupPanelModel', () => {
fireEvent.dragEnd(element);
});
test('that should not show drop target if dropping on self', () => {
test('that should show drop target if dropping on self', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {},
@ -812,17 +820,12 @@ describe('dockviewGroupPanelModel', () => {
document.createElement('div'),
fromPartial<DockviewComponent>({})
),
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
@ -850,12 +853,12 @@ describe('dockviewGroupPanelModel', () => {
const element = container
.getElementsByClassName('dv-content-container')
.item(0)!;
.item(0)! as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')],
@ -869,10 +872,10 @@ describe('dockviewGroupPanelModel', () => {
expect(
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
).toBe(1);
});
test('that should not allow drop when dropping on self for same component id', () => {
test('that should allow drop when dropping on self for same component id', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {},
@ -885,6 +888,7 @@ describe('dockviewGroupPanelModel', () => {
document.createElement('div'),
fromPartial<DockviewComponent>({})
),
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -924,12 +928,12 @@ describe('dockviewGroupPanelModel', () => {
const element = container
.getElementsByClassName('dv-content-container')
.item(0)!;
.item(0) as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')],
@ -943,7 +947,7 @@ describe('dockviewGroupPanelModel', () => {
expect(
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
).toBe(1);
});
test('that should not allow drop when not dropping for different component id', () => {
@ -959,6 +963,7 @@ describe('dockviewGroupPanelModel', () => {
document.createElement('div'),
fromPartial<DockviewComponent>({})
),
onDidOptionsChange: jest.fn(),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -998,12 +1003,12 @@ describe('dockviewGroupPanelModel', () => {
const element = container
.getElementsByClassName('dv-content-container')
.item(0)!;
.item(0) as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('anothercomponentid', 'groupviewid', 'panel1')],

View File

@ -3,7 +3,6 @@ import {
Emitter,
Event,
addDisposableListener,
addDisposableWindowListener,
} from '../events';
describe('events', () => {
@ -143,7 +142,7 @@ describe('events', () => {
expect(value).toBe(3);
});
it('addDisposableWindowListener with capture options', () => {
it('addDisposableListener with capture options', () => {
const element = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
@ -151,7 +150,7 @@ describe('events', () => {
const handler = jest.fn();
const disposable = addDisposableWindowListener(
const disposable = addDisposableListener(
element as any,
'pointerdown',
handler,
@ -177,7 +176,7 @@ describe('events', () => {
);
});
it('addDisposableWindowListener without capture options', () => {
it('addDisposableListener without capture options', () => {
const element = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
@ -185,7 +184,7 @@ describe('events', () => {
const handler = jest.fn();
const disposable = addDisposableWindowListener(
const disposable = addDisposableListener(
element as any,
'pointerdown',
handler

View File

@ -58,7 +58,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -84,7 +84,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -121,7 +121,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -188,7 +188,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -198,6 +198,8 @@ describe('gridview', () => {
},
});
expect(container.querySelectorAll('.dv-grid-view').length).toBe(1);
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
@ -242,6 +244,9 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
expect(container.querySelectorAll('.dv-grid-view').length).toBe(1);
gridview.layout(800, 400, true);
const panel1 = gridview.getPanel('panel_1')!;
@ -322,7 +327,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -365,7 +370,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -495,7 +500,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -524,14 +529,14 @@ describe('gridview', () => {
gridview.dispose();
expect(container.childNodes.length).toBe(0);
expect(container.children.length).toBe(0);
});
test('#1/VERTICAL', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -596,7 +601,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -661,7 +666,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -744,7 +749,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -845,7 +850,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -946,7 +951,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -1047,7 +1052,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -1178,7 +1183,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -1309,7 +1314,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -1442,7 +1447,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -1573,7 +1578,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -1704,7 +1709,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -1838,7 +1843,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -1875,7 +1880,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -1911,7 +1916,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -1956,7 +1961,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -2085,7 +2090,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -2218,7 +2223,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -2500,7 +2505,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -2864,7 +2869,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -2881,7 +2886,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
@ -2899,7 +2904,7 @@ describe('gridview', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: true,
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);

View File

@ -9,6 +9,7 @@ describe('gridviewPanel', () => {
onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
} as any;
});

View File

@ -56,22 +56,28 @@ describe('paneview', () => {
paneview.onDidRemoveView((view) => removed.push(view))
);
const view1 = new TestPanel(
'id',
'component',
'headerComponent',
Orientation.VERTICAL,
true,
true
);
const view2 = new TestPanel(
'id2',
'component',
'headerComponent',
Orientation.VERTICAL,
true,
true
);
const view1 = new TestPanel({
id: 'id',
component: 'component',
headerComponent: 'headerComponent',
orientation: Orientation.VERTICAL,
isExpanded: true,
isHeaderVisible: true,
headerSize: 22,
minimumBodySize: 0,
maximumBodySize: Number.MAX_SAFE_INTEGER,
});
const view2 = new TestPanel({
id: 'id2',
component: 'component',
headerComponent: 'headerComponent',
orientation: Orientation.VERTICAL,
isExpanded: true,
isHeaderVisible: true,
headerSize: 22,
minimumBodySize: 0,
maximumBodySize: Number.MAX_SAFE_INTEGER,
});
expect(added.length).toBe(0);
expect(removed.length).toBe(0);
@ -106,22 +112,28 @@ describe('paneview', () => {
orientation: Orientation.HORIZONTAL,
});
const view1 = new TestPanel(
'id',
'component',
'headerComponent',
Orientation.VERTICAL,
true,
true
);
const view2 = new TestPanel(
'id2',
'component',
'headerComponent',
Orientation.VERTICAL,
true,
true
);
const view1 = new TestPanel({
id: 'id',
component: 'component',
headerComponent: 'headerComponent',
orientation: Orientation.VERTICAL,
isExpanded: true,
isHeaderVisible: true,
headerSize: 22,
minimumBodySize: 0,
maximumBodySize: Number.MAX_SAFE_INTEGER,
});
const view2 = new TestPanel({
id: 'id2',
component: 'component',
headerComponent: 'headerComponent',
orientation: Orientation.VERTICAL,
isExpanded: true,
isHeaderVisible: true,
headerSize: 22,
minimumBodySize: 0,
maximumBodySize: Number.MAX_SAFE_INTEGER,
});
paneview.addPane(view1);
paneview.addPane(view2);

View File

@ -11,7 +11,17 @@ import { Orientation } from '../../splitview/splitview';
class TestPanel extends PaneviewPanel {
constructor(id: string, component: string) {
super(id, component, 'header', Orientation.VERTICAL, false, true);
super({
id,
component,
headerComponent: 'header',
orientation: Orientation.VERTICAL,
isExpanded: false,
isHeaderVisible: true,
headerSize: 22,
minimumBodySize: 0,
maximumBodySize: Number.MAX_SAFE_INTEGER,
});
}
getHeaderComponent() {
@ -59,7 +69,7 @@ class TestPanel extends PaneviewPanel {
}
}
describe('componentPaneview', () => {
describe('paneviewComponent', () => {
let container: HTMLElement;
beforeEach(() => {
@ -86,6 +96,7 @@ describe('componentPaneview', () => {
paneview.dispose();
expect(container.parentElement).toBe(root);
expect(container.children.length).toBe(0);
});
test('vertical panels', () => {
@ -179,6 +190,8 @@ describe('componentPaneview', () => {
},
});
expect(container.querySelectorAll('.dv-pane-container').length).toBe(1);
paneview.fromJSON({
size: 6,
views: [
@ -211,6 +224,8 @@ describe('componentPaneview', () => {
],
});
expect(container.querySelectorAll('.dv-pane-container').length).toBe(1);
paneview.layout(400, 800);
const panel1 = paneview.getPanel('panel1');
@ -254,7 +269,7 @@ describe('componentPaneview', () => {
title: 'Panel 1',
},
expanded: true,
minimumSize: 100,
headerSize: 22,
},
{
size: 22,
@ -264,7 +279,7 @@ describe('componentPaneview', () => {
title: 'Panel 2',
},
expanded: false,
minimumSize: 100,
headerSize: 22,
},
{
size: 22,
@ -274,7 +289,7 @@ describe('componentPaneview', () => {
title: 'Panel 3',
},
expanded: false,
minimumSize: 100,
headerSize: 22,
},
],
});
@ -449,6 +464,7 @@ describe('componentPaneview', () => {
component: 'default',
title: 'Panel 1',
},
minimumSize: 100,
expanded: true,
},
{
@ -485,26 +501,27 @@ describe('componentPaneview', () => {
},
expanded: true,
minimumSize: 100,
headerSize: 22,
},
{
size: 122,
size: 22,
data: {
id: 'panel2',
component: 'default',
title: 'Panel 2',
},
expanded: true,
minimumSize: 100,
headerSize: 22,
},
{
size: 356,
size: 456,
data: {
id: 'panel3',
component: 'default',
title: 'Panel 3',
},
expanded: true,
minimumSize: 100,
headerSize: 22,
},
],
});

View File

@ -46,6 +46,7 @@ describe('componentSplitview', () => {
splitview.dispose();
expect(container.parentElement).toBe(root);
expect(container.children.length).toBe(0);
});
test('event leakage', () => {
@ -394,6 +395,10 @@ describe('componentSplitview', () => {
});
splitview.layout(400, 6);
expect(
container.querySelectorAll('.dv-split-view-container').length
).toBe(1);
splitview.fromJSON({
views: [
{
@ -413,6 +418,10 @@ describe('componentSplitview', () => {
activeView: 'panel1',
});
expect(
container.querySelectorAll('.dv-split-view-container').length
).toBe(1);
expect(splitview.length).toBe(3);
expect(JSON.parse(JSON.stringify(splitview.toJSON()))).toEqual({

View File

@ -3,6 +3,8 @@ import {
FloatingGroupOptions,
IDockviewComponent,
MovePanelEvent,
PopoutGroupChangePositionEvent,
PopoutGroupChangeSizeEvent,
SerializedDockview,
} from '../dockview/dockviewComponent';
import {
@ -629,10 +631,6 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.totalPanels;
}
get gap(): number {
return this.component.gap;
}
/**
* Invoked when the active group changes. May be undefined if no group is active.
*/
@ -743,6 +741,14 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.onUnhandledDragOverEvent;
}
get onDidPopoutGroupSizeChange(): Event<PopoutGroupChangeSizeEvent> {
return this.component.onDidPopoutGroupSizeChange;
}
get onDidPopoutGroupPositionChange(): Event<PopoutGroupChangePositionEvent> {
return this.component.onDidPopoutGroupPositionChange;
}
/**
* All panel objects.
*/
@ -914,10 +920,6 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.addPopoutGroup(item, options);
}
setGap(gap: number | undefined): void {
this.component.updateOptions({ gap });
}
updateOptions(options: Partial<DockviewComponentOptions>) {
this.component.updateOptions(options);
}

View File

@ -67,7 +67,7 @@ export abstract class DragHandler extends CompositeDisposable {
* For example: in react-dnd if dataTransfer.types is not set then the dragStart event will be cancelled
* through .preventDefault(). Since this is applied globally to all drag events this would break dockviews
* dnd logic. You can see the code at
* https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542
P * https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542
*/
event.dataTransfer.setData('text/plain', '');
}
@ -75,7 +75,9 @@ export abstract class DragHandler extends CompositeDisposable {
}),
addDisposableListener(this.el, 'dragend', () => {
this.pointerEventsDisposable.dispose();
this.dataDisposable.dispose();
setTimeout(() => {
this.dataDisposable.dispose(); // allow the data to be read by other handlers before disposing
}, 0);
})
);
}

View File

@ -0,0 +1,23 @@
.dv-drop-target-container {
position: absolute;
z-index: 9999;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
pointer-events: none;
overflow: hidden;
--dv-transition-duration: 300ms;
.dv-drop-target-anchor {
position: relative;
border: var(--dv-drag-over-border);
transition: opacity var(--dv-transition-duration) ease-in,
top var(--dv-transition-duration) ease-out,
left var(--dv-transition-duration) ease-out,
width var(--dv-transition-duration) ease-out,
height var(--dv-transition-duration) ease-out;
background-color: var(--dv-drag-over-background-color);
opacity: 1;
}
}

View File

@ -0,0 +1,102 @@
import { CompositeDisposable, Disposable } from '../lifecycle';
import { DropTargetTargetModel } from './droptarget';
export class DropTargetAnchorContainer extends CompositeDisposable {
private _model:
| { root: HTMLElement; overlay: HTMLElement; changed: boolean }
| undefined;
private _outline: HTMLElement | undefined;
private _disabled = false;
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
if (this.disabled === value) {
return;
}
this._disabled = value;
if (value) {
this.model?.clear();
}
}
get model(): DropTargetTargetModel | undefined {
if (this.disabled) {
return undefined;
}
return {
clear: () => {
if (this._model) {
this._model.root.parentElement?.removeChild(
this._model.root
);
}
this._model = undefined;
},
exists: () => {
return !!this._model;
},
getElements: (event?: DragEvent, outline?: HTMLElement) => {
const changed = this._outline !== outline;
this._outline = outline;
if (this._model) {
this._model.changed = changed;
return this._model;
}
const container = this.createContainer();
const anchor = this.createAnchor();
this._model = { root: container, overlay: anchor, changed };
container.appendChild(anchor);
this.element.appendChild(container);
if (event?.target instanceof HTMLElement) {
const targetBox = event.target.getBoundingClientRect();
const box = this.element.getBoundingClientRect();
anchor.style.left = `${targetBox.left - box.left}px`;
anchor.style.top = `${targetBox.top - box.top}px`;
}
return this._model;
},
};
}
constructor(readonly element: HTMLElement, options: { disabled: boolean }) {
super();
this._disabled = options.disabled;
this.addDisposables(
Disposable.from(() => {
this.model?.clear();
})
);
}
private createContainer(): HTMLElement {
const el = document.createElement('div');
el.className = 'dv-drop-target-container';
return el;
}
private createAnchor(): HTMLElement {
const el = document.createElement('div');
el.className = 'dv-drop-target-anchor';
el.style.visibility = 'hidden';
return el;
}
}

View File

@ -1,5 +1,6 @@
.dv-drop-target {
position: relative;
--dv-transition-duration: 70ms;
> .dv-drop-target-dropzone {
position: absolute;
@ -15,10 +16,13 @@
box-sizing: border-box;
height: 100%;
width: 100%;
border: var(--dv-drag-over-border);
background-color: var(--dv-drag-over-background-color);
transition: top 70ms ease-out, left 70ms ease-out,
width 70ms ease-out, height 70ms ease-out,
opacity 0.15s ease-out;
transition: top var(--dv-transition-duration) ease-out,
left var(--dv-transition-duration) ease-out,
width var(--dv-transition-duration) ease-out,
height var(--dv-transition-duration) ease-out,
opacity var(--dv-transition-duration) ease-out;
will-change: transform;
pointer-events: none;

View File

@ -93,10 +93,26 @@ const DEFAULT_SIZE: MeasuredValue = {
const SMALL_WIDTH_BOUNDARY = 100;
const SMALL_HEIGHT_BOUNDARY = 100;
export interface DropTargetTargetModel {
getElements(
event?: DragEvent,
outline?: HTMLElement
): {
root: HTMLElement;
overlay: HTMLElement;
changed: boolean;
};
exists(): boolean;
clear(): void;
}
export interface DroptargetOptions {
canDisplayOverlay: CanDisplayOverlay;
acceptedTargetZones: Position[];
overlayModel?: DroptargetOverlayModel;
getOverrideTarget?: () => DropTargetTargetModel | undefined;
className?: string;
getOverlayOutline?: () => HTMLElement | null;
}
export class Droptarget extends CompositeDisposable {
@ -116,6 +132,18 @@ export class Droptarget extends CompositeDisposable {
private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__';
private static ACTUAL_TARGET: Droptarget | undefined;
private _disabled: boolean;
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = value;
}
get state(): Position | undefined {
return this._state;
}
@ -126,21 +154,35 @@ export class Droptarget extends CompositeDisposable {
) {
super();
this._disabled = false;
// use a set to take advantage of #<set>.has
this._acceptedTargetZonesSet = new Set(
this.options.acceptedTargetZones
);
this.dnd = new DragAndDropObserver(this.element, {
onDragEnter: () => undefined,
onDragEnter: () => {
this.options.getOverrideTarget?.()?.getElements();
},
onDragOver: (e) => {
Droptarget.ACTUAL_TARGET = this;
const overrideTraget = this.options.getOverrideTarget?.();
if (this._acceptedTargetZonesSet.size === 0) {
if (overrideTraget) {
return;
}
this.removeDropTarget();
return;
}
const width = this.element.clientWidth;
const height = this.element.clientHeight;
const target =
this.options.getOverlayOutline?.() ?? this.element;
const width = target.offsetWidth;
const height = target.offsetHeight;
if (width === 0 || height === 0) {
return; // avoid div!0
@ -149,8 +191,8 @@ export class Droptarget extends CompositeDisposable {
const rect = (
e.currentTarget as HTMLElement
).getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const x = (e.clientX ?? 0) - rect.left;
const y = (e.clientY ?? 0) - rect.top;
const quadrant = this.calculateQuadrant(
this._acceptedTargetZonesSet,
@ -172,6 +214,9 @@ export class Droptarget extends CompositeDisposable {
}
if (!this.options.canDisplayOverlay(e, quadrant)) {
if (overrideTraget) {
return;
}
this.removeDropTarget();
return;
}
@ -194,7 +239,9 @@ export class Droptarget extends CompositeDisposable {
this.markAsUsed(e);
if (!this.targetElement) {
if (overrideTraget) {
//
} else if (!this.targetElement) {
this.targetElement = document.createElement('div');
this.targetElement.className = 'dv-drop-target-dropzone';
this.overlayElement = document.createElement('div');
@ -202,8 +249,16 @@ export class Droptarget extends CompositeDisposable {
this._state = 'center';
this.targetElement.appendChild(this.overlayElement);
this.element.classList.add('dv-drop-target');
this.element.append(this.targetElement);
target.classList.add('dv-drop-target');
target.append(this.targetElement);
// this.overlayElement.style.opacity = '0';
// requestAnimationFrame(() => {
// if (this.overlayElement) {
// this.overlayElement.style.opacity = '';
// }
// });
}
this.toggleClasses(quadrant, width, height);
@ -211,10 +266,32 @@ export class Droptarget extends CompositeDisposable {
this._state = quadrant;
},
onDragLeave: () => {
const target = this.options.getOverrideTarget?.();
if (target) {
return;
}
this.removeDropTarget();
},
onDragEnd: () => {
onDragEnd: (e) => {
const target = this.options.getOverrideTarget?.();
if (target && Droptarget.ACTUAL_TARGET === this) {
if (this._state) {
// only stop the propagation of the event if we are dealing with it
// which is only when the target has state
e.stopPropagation();
this._onDrop.fire({
position: this._state,
nativeEvent: e,
});
}
}
this.removeDropTarget();
target?.clear();
},
onDrop: (e) => {
e.preventDefault();
@ -223,6 +300,8 @@ export class Droptarget extends CompositeDisposable {
this.removeDropTarget();
this.options.getOverrideTarget?.()?.clear();
if (state) {
// only stop the propagation of the event if we are dealing with it
// which is only when the target has state
@ -268,7 +347,9 @@ export class Droptarget extends CompositeDisposable {
width: number,
height: number
): void {
if (!this.overlayElement) {
const target = this.options.getOverrideTarget?.();
if (!target && !this.overlayElement) {
return;
}
@ -300,6 +381,103 @@ export class Droptarget extends CompositeDisposable {
}
}
if (target) {
const outlineEl =
this.options.getOverlayOutline?.() ?? this.element;
const elBox = outlineEl.getBoundingClientRect();
const ta = target.getElements(undefined, outlineEl);
const el = ta.root;
const overlay = ta.overlay;
const bigbox = el.getBoundingClientRect();
const rootTop = elBox.top - bigbox.top;
const rootLeft = elBox.left - bigbox.left;
const box = {
top: rootTop,
left: rootLeft,
width: width,
height: height,
};
if (rightClass) {
box.left = rootLeft + width * (1 - size);
box.width = width * size;
} else if (leftClass) {
box.width = width * size;
} else if (topClass) {
box.height = height * size;
} else if (bottomClass) {
box.top = rootTop + height * (1 - size);
box.height = height * size;
}
if (isSmallX && isLeft) {
box.width = 4;
}
if (isSmallX && isRight) {
box.left = rootLeft + width - 4;
box.width = 4;
}
const topPx = `${Math.round(box.top)}px`;
const leftPx = `${Math.round(box.left)}px`;
const widthPx = `${Math.round(box.width)}px`;
const heightPx = `${Math.round(box.height)}px`;
if (
overlay.style.top === topPx &&
overlay.style.left === leftPx &&
overlay.style.width === widthPx &&
overlay.style.height === heightPx
) {
return;
}
overlay.style.top = topPx;
overlay.style.left = leftPx;
overlay.style.width = widthPx;
overlay.style.height = heightPx;
overlay.style.visibility = 'visible';
overlay.className = `dv-drop-target-anchor${
this.options.className ? ` ${this.options.className}` : ''
}`;
toggleClass(overlay, 'dv-drop-target-left', isLeft);
toggleClass(overlay, 'dv-drop-target-right', isRight);
toggleClass(overlay, 'dv-drop-target-top', isTop);
toggleClass(overlay, 'dv-drop-target-bottom', isBottom);
toggleClass(
overlay,
'dv-drop-target-center',
quadrant === 'center'
);
if (ta.changed) {
toggleClass(
overlay,
'dv-drop-target-anchor-container-changed',
true
);
setTimeout(() => {
toggleClass(
overlay,
'dv-drop-target-anchor-container-changed',
false
);
}, 10);
}
return;
}
if (!this.overlayElement) {
return;
}
const box = { top: '0px', left: '0px', width: '100%', height: '100%' };
/**
@ -396,10 +574,12 @@ export class Droptarget extends CompositeDisposable {
private removeDropTarget(): void {
if (this.targetElement) {
this._state = undefined;
this.element.removeChild(this.targetElement);
this.targetElement.parentElement?.classList.remove(
'dv-drop-target'
);
this.targetElement.remove();
this.targetElement = undefined;
this.overlayElement = undefined;
this.element.classList.remove('dv-drop-target');
}
}
}

View File

@ -2,13 +2,14 @@ import { addClasses, removeClasses } from '../dom';
export function addGhostImage(
dataTransfer: DataTransfer,
ghostElement: HTMLElement
ghostElement: HTMLElement,
options?: { x?: number; y?: number }
): void {
// class dockview provides to force ghost image to be drawn on a different layer and prevent weird rendering issues
addClasses(ghostElement, 'dv-dragged');
document.body.appendChild(ghostElement);
dataTransfer.setDragImage(ghostElement, 0, 0);
dataTransfer.setDragImage(ghostElement, options?.x ?? 0, options?.y ?? 0);
setTimeout(() => {
removeClasses(ghostElement, 'dv-dragged');

View File

@ -72,9 +72,11 @@ export class GroupDragHandler extends DragHandler {
ghostElement.style.lineHeight = '20px';
ghostElement.style.borderRadius = '12px';
ghostElement.style.position = 'absolute';
ghostElement.style.pointerEvents = 'none';
ghostElement.style.top = '-9999px';
ghostElement.textContent = `Multiple Panels (${this.group.size})`;
addGhostImage(dataTransfer, ghostElement);
addGhostImage(dataTransfer, ghostElement, { y: -10, x: 30 });
}
return {

View File

@ -55,7 +55,15 @@ export class ContentContainer
this.addDisposables(this._onDidFocus, this._onDidBlur);
const target = group.dropTargetContainer;
this.dropTarget = new Droptarget(this.element, {
getOverlayOutline: () => {
return accessor.options.theme?.dndPanelOverlay === 'group'
? this.element.parentElement
: null;
},
className: 'dv-drop-target-content',
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
canDisplayOverlay: (event, position) => {
if (
@ -76,26 +84,12 @@ export class ContentContainer
}
if (data && data.viewId === this.accessor.id) {
if (data.groupId === this.group.id) {
if (position === 'center') {
// don't allow to drop on self for center position
return false;
}
if (data.panelId === null) {
// don't allow group move to drop anywhere on self
return false;
}
}
const groupHasOnePanelAndIsActiveDragElement =
this.group.panels.length === 1 &&
data.groupId === this.group.id;
return !groupHasOnePanelAndIsActiveDragElement;
return true;
}
return this.group.canDisplayOverlay(event, position, 'content');
},
getOverrideTarget: target ? () => target.model : undefined,
});
this.addDisposables(this.dropTarget);

View File

@ -0,0 +1,82 @@
import { addDisposableListener } from '../../events';
import {
CompositeDisposable,
Disposable,
MutableDisposable,
} from '../../lifecycle';
export class PopupService extends CompositeDisposable {
private readonly _element: HTMLElement;
private _active: HTMLElement | null = null;
private readonly _activeDisposable = new MutableDisposable();
constructor(private readonly root: HTMLElement) {
super();
this._element = document.createElement('div');
this._element.className = 'dv-popover-anchor';
this._element.style.position = 'relative';
this.root.prepend(this._element);
this.addDisposables(
Disposable.from(() => {
this.close();
}),
this._activeDisposable
);
}
openPopover(
element: HTMLElement,
position: { x: number; y: number }
): void {
this.close();
const wrapper = document.createElement('div');
wrapper.style.position = 'absolute';
wrapper.style.zIndex = '99';
wrapper.appendChild(element);
const anchorBox = this._element.getBoundingClientRect();
const offsetX = anchorBox.left;
const offsetY = anchorBox.top;
wrapper.style.top = `${position.y - offsetY}px`;
wrapper.style.left = `${position.x - offsetX}px`;
this._element.appendChild(wrapper);
this._active = wrapper;
this._activeDisposable.value = new CompositeDisposable(
addDisposableListener(window, 'pointerdown', (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
let el: HTMLElement | null = target;
while (el && el !== wrapper) {
el = el?.parentElement ?? null;
}
if (el) {
return; // clicked within popover
}
this.close();
})
);
}
close(): void {
if (this._active) {
this._active.remove();
this._activeDisposable.dispose();
this._active = null;
}
}
}

View File

@ -58,15 +58,13 @@
position: relative;
height: 100%;
display: flex;
min-width: 80px;
align-items: center;
padding: 0px 8px;
white-space: nowrap;
text-overflow: ellipsis;
.dv-default-tab-content {
padding: 0px 8px;
flex-grow: 1;
margin-right: 4px;
}
.dv-default-tab-action {

View File

@ -16,6 +16,7 @@ import {
} from '../../../dnd/droptarget';
import { DragHandler } from '../../../dnd/abstractDragHandler';
import { IDockviewPanel } from '../../dockviewPanel';
import { addGhostImage } from '../../../dnd/ghost';
class TabDragHandler extends DragHandler {
private readonly panelTransfer =
@ -92,7 +93,8 @@ export class Tab extends CompositeDisposable {
);
this.dropTarget = new Droptarget(this._element, {
acceptedTargetZones: ['center'],
acceptedTargetZones: ['left', 'right'],
overlayModel: { activationSize: { value: 50, type: 'percentage' } },
canDisplayOverlay: (event, position) => {
if (this.group.locked) {
return false;
@ -101,15 +103,7 @@ export class Tab extends CompositeDisposable {
const data = getPanelData();
if (data && this.accessor.id === data.viewId) {
if (
data.panelId === null &&
data.groupId === this.group.id
) {
// don't allow group move to drop on self
return false;
}
return this.panel.id !== data.panelId;
return true;
}
return this.group.model.canDisplayOverlay(
@ -118,6 +112,7 @@ export class Tab extends CompositeDisposable {
'tab'
);
},
getOverrideTarget: () => group.model.dropTargetContainer?.model,
});
this.onWillShowOverlay = this.dropTarget.onWillShowOverlay;
@ -127,6 +122,23 @@ export class Tab extends CompositeDisposable {
this._onDropped,
this._onDragStart,
dragHandler.onDragStart((event) => {
if (event.dataTransfer) {
const style = getComputedStyle(this.element);
const newNode = this.element.cloneNode(true) as HTMLElement;
Array.from(style).forEach((key) =>
newNode.style.setProperty(
key,
style.getPropertyValue(key),
style.getPropertyPriority(key)
)
);
newNode.style.position = 'absolute';
addGhostImage(event.dataTransfer, newNode, {
y: -10,
x: 30,
});
}
this._onDragStart.fire(event);
}),
dragHandler,

View File

@ -0,0 +1,19 @@
.dv-tabs-overflow-dropdown-default {
height: 100%;
color: var(--dv-activegroup-hiddenpanel-tab-color);
margin: var(--dv-tab-margin);
display: flex;
align-items: center;
flex-shrink: 0;
padding: 0.25rem 0.5rem;
cursor: pointer;
> span {
padding-left: 0.25rem;
}
> svg {
transform: rotate(90deg);
}
}

View File

@ -0,0 +1,25 @@
import { createChevronRightButton } from '../../../svg';
export type DropdownElement = {
element: HTMLElement;
update: (params: { tabs: number }) => void;
dispose?: () => void;
};
export function createDropdownElementHandle(): DropdownElement {
const el = document.createElement('div');
el.className = 'dv-tabs-overflow-dropdown-default';
const text = document.createElement('span');
text.textContent = ``;
const icon = createChevronRightButton();
el.appendChild(icon);
el.appendChild(text);
return {
element: el,
update: (params: { tabs: number }) => {
text.textContent = `${params.tabs}`;
},
};
}

View File

@ -0,0 +1,79 @@
.dv-tabs-container {
display: flex;
height: 100%;
overflow: auto;
scrollbar-width: thin; // firefox
&.dv-horizontal {
.dv-tab {
&:not(:first-child)::before {
content: ' ';
position: absolute;
top: 0;
left: 0;
z-index: 5;
pointer-events: none;
background-color: var(--dv-tab-divider-color);
width: 1px;
height: 100%;
}
}
}
&::-webkit-scrollbar {
height: 3px;
}
/* Track */
&::-webkit-scrollbar-track {
background: transparent;
}
/* Handle */
&::-webkit-scrollbar-thumb {
background: var(--dv-tabs-container-scrollbar-color);
}
}
.dv-scrollable {
> .dv-tabs-container {
overflow: hidden;
}
}
.dv-tab {
-webkit-user-drag: element;
outline: none;
padding: 0.25rem 0.5rem;
cursor: pointer;
position: relative;
box-sizing: border-box;
font-size: var(--dv-tab-font-size);
margin: var(--dv-tab-margin);
}
.dv-tabs-overflow-container {
flex-direction: column;
height: unset;
border: 1px solid var(--dv-tab-divider-color);
background-color: var(--dv-group-view-background-color);
.dv-tab {
&:not(:last-child) {
border-bottom: 1px solid var(--dv-tab-divider-color);
}
}
.dv-active-tab {
background-color: var(
--dv-activegroup-visiblepanel-tab-background-color
);
color: var(--dv-activegroup-visiblepanel-tab-color);
}
.dv-inactive-tab {
background-color: var(
--dv-activegroup-hiddenpanel-tab-background-color
);
color: var(--dv-activegroup-hiddenpanel-tab-color);
}
}

View File

@ -0,0 +1,345 @@
import { getPanelData } from '../../../dnd/dataTransfer';
import {
isChildEntirelyVisibleWithinParent,
OverflowObserver,
} from '../../../dom';
import { addDisposableListener, Emitter, Event } from '../../../events';
import {
CompositeDisposable,
Disposable,
IValueDisposable,
MutableDisposable,
} from '../../../lifecycle';
import { Scrollbar } from '../../../scrollbar';
import { DockviewComponent } from '../../dockviewComponent';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel';
import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel';
import { Tab } from '../tab/tab';
import { TabDragEvent, TabDropIndexEvent } from './tabsContainer';
export class Tabs extends CompositeDisposable {
private readonly _element: HTMLElement;
private readonly _tabsList: HTMLElement;
private readonly _observerDisposable = new MutableDisposable();
private _tabs: IValueDisposable<Tab>[] = [];
private selectedIndex = -1;
private _showTabsOverflowControl = false;
private readonly _onTabDragStart = new Emitter<TabDragEvent>();
readonly onTabDragStart: Event<TabDragEvent> = this._onTabDragStart.event;
private readonly _onDrop = new Emitter<TabDropIndexEvent>();
readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event;
private readonly _onWillShowOverlay =
new Emitter<WillShowOverlayLocationEvent>();
readonly onWillShowOverlay: Event<WillShowOverlayLocationEvent> =
this._onWillShowOverlay.event;
private readonly _onOverflowTabsChange = new Emitter<{
tabs: string[];
reset: boolean;
}>();
readonly onOverflowTabsChange = this._onOverflowTabsChange.event;
get showTabsOverflowControl(): boolean {
return this._showTabsOverflowControl;
}
set showTabsOverflowControl(value: boolean) {
if (this._showTabsOverflowControl == value) {
return;
}
this._showTabsOverflowControl = value;
if (value) {
const observer = new OverflowObserver(this._tabsList);
this._observerDisposable.value = new CompositeDisposable(
observer,
observer.onDidChange((event) => {
const hasOverflow = event.hasScrollX || event.hasScrollY;
this.toggleDropdown({ reset: !hasOverflow });
}),
addDisposableListener(this._tabsList, 'scroll', () => {
this.toggleDropdown({ reset: false });
})
);
}
}
get element(): HTMLElement {
return this._element;
}
get panels(): string[] {
return this._tabs.map((_) => _.value.panel.id);
}
get size(): number {
return this._tabs.length;
}
get tabs(): Tab[] {
return this._tabs.map((_) => _.value);
}
constructor(
private readonly group: DockviewGroupPanel,
private readonly accessor: DockviewComponent,
options: {
showTabsOverflowControl: boolean;
}
) {
super();
this._tabsList = document.createElement('div');
this._tabsList.className = 'dv-tabs-container dv-horizontal';
this.showTabsOverflowControl = options.showTabsOverflowControl;
if (accessor.options.scrollbars === 'native') {
this._element = this._tabsList;
} else {
const scrollbar = new Scrollbar(this._tabsList);
this._element = scrollbar.element;
this.addDisposables(scrollbar);
}
this.element.role = "tablist"
this.addDisposables(
this._onOverflowTabsChange,
this._observerDisposable,
this._onWillShowOverlay,
this._onDrop,
this._onTabDragStart,
this.accessor.onDidActivePanelChange((e) => {
if (e?.api.group === this.group) {
this.selectedIndex = this.indexOf(e.id);
} else {
this.selectedIndex = -1;
}
}),
addDisposableListener(this.element, 'pointerdown', (event) => {
if (event.defaultPrevented) {
return;
}
const isLeftClick = event.button === 0;
if (isLeftClick) {
this.accessor.doSetGroupActive(this.group);
}
}),
addDisposableListener(this.element, 'keydown', (event) => {
if (event.defaultPrevented) {
return;
}
let tab: IValueDisposable<Tab> | undefined = undefined;
switch (event.key) {
case 'ArrowLeft': {
if (this.selectedIndex > 0) {
tab = this._tabs[this.selectedIndex - 1];
}
break;
}
case 'ArrowRight': {
if (this.selectedIndex + 1 < this.size) {
tab = this._tabs[this.selectedIndex + 1];
}
break;
}
case 'Home':
tab = this._tabs[0];
break;
case 'End':
tab = this._tabs[this.size - 1];
break;
}
if (tab == null) {
return;
}
this.group.model.openPanel(tab.value.panel);
}),
Disposable.from(() => {
for (const { value, disposable } of this._tabs) {
disposable.dispose();
value.dispose();
}
this._tabs = [];
})
);
}
indexOf(id: string): number {
return this._tabs.findIndex((tab) => tab.value.panel.id === id);
}
isActive(tab: Tab): boolean {
return (
this.selectedIndex > -1 &&
this._tabs[this.selectedIndex].value === tab
);
}
setActivePanel(panel: IDockviewPanel): void {
let runningWidth = 0;
for (const tab of this._tabs) {
const isActivePanel = panel.id === tab.value.panel.id;
tab.value.setActive(isActivePanel);
tab.value.panel.runEvents()
if (isActivePanel) {
const element = tab.value.element;
const parentElement = element.parentElement!;
if (
runningWidth < parentElement.scrollLeft ||
runningWidth + element.clientWidth >
parentElement.scrollLeft + parentElement.clientWidth
) {
parentElement.scrollLeft = runningWidth;
}
}
runningWidth += tab.value.element.clientWidth;
}
}
openPanel(panel: IDockviewPanel, index: number = this._tabs.length): void {
if (this._tabs.find((tab) => tab.value.panel.id === panel.id)) {
return;
}
const tab = new Tab(panel, this.accessor, this.group);
tab.setContent(panel.view.tab);
const disposable = new CompositeDisposable(
tab.onDragStart((event) => {
this._onTabDragStart.fire({ nativeEvent: event, panel });
}),
tab.onPointerDown((event) => {
if (event.defaultPrevented) {
return;
}
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
const isFloatingWithOnePanel =
this.group.api.location.type === 'floating' &&
this.size === 1;
if (
isFloatingGroupsEnabled &&
!isFloatingWithOnePanel &&
event.shiftKey
) {
event.preventDefault();
const panel = this.accessor.getGroupPanel(tab.panel.id);
const { top, left } = tab.element.getBoundingClientRect();
const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect();
this.accessor.addFloatingGroup(panel as DockviewPanel, {
x: left - rootLeft,
y: top - rootTop,
inDragMode: true,
});
return;
}
switch (event.button) {
case 0: // left click or touch
if (this.group.activePanel !== panel) {
this.group.model.openPanel(panel);
}
break;
}
}),
tab.onDrop((event) => {
this._onDrop.fire({
event: event.nativeEvent,
index: this._tabs.findIndex((x) => x.value === tab),
});
}),
tab.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire(
new WillShowOverlayLocationEvent(event, {
kind: 'tab',
panel: this.group.activePanel,
api: this.accessor.api,
group: this.group,
getData: getPanelData,
})
);
})
);
const value: IValueDisposable<Tab> = { value: tab, disposable };
this.addTab(value, index);
}
delete(id: string): void {
const index = this.indexOf(id);
const tabToRemove = this._tabs.splice(index, 1)[0];
const { value, disposable } = tabToRemove;
disposable.dispose();
value.dispose();
value.element.remove();
}
private addTab(
tab: IValueDisposable<Tab>,
index: number = this._tabs.length
): void {
if (index < 0 || index > this._tabs.length) {
throw new Error('invalid location');
}
this._tabsList.insertBefore(
tab.value.element,
this._tabsList.children[index]
);
this._tabs = [
...this._tabs.slice(0, index),
tab,
...this._tabs.slice(index),
];
if (this.selectedIndex < 0) {
this.selectedIndex = index;
}
}
private toggleDropdown(options: { reset: boolean }): void {
const tabs = options.reset
? []
: this._tabs
.filter(
(tab) =>
!isChildEntirelyVisibleWithinParent(
tab.value.element,
this._tabsList
)
)
.map((x) => x.value.panel.id);
this._onOverflowTabsChange.fire({ tabs, reset: options.reset });
}
}

View File

@ -7,17 +7,22 @@
font-size: var(--dv-tabs-and-actions-container-font-size);
&.dv-single-tab.dv-full-width-single-tab {
.dv-tabs-container {
flex-grow: 1;
.dv-tab {
.dv-scrollable {
flex-grow: 1;
}
}
}
.dv-void-container {
flex-grow: 0;
}
.dv-tabs-container {
flex-grow: 1;
.dv-tab {
flex-grow: 1;
padding: 0px;
}
}
.dv-void-container {
flex-grow: 0;
}
}
.dv-void-container {
@ -26,46 +31,7 @@
cursor: grab;
}
.dv-tabs-container {
.dv-right-actions-container {
display: flex;
overflow-x: overlay;
overflow-y: hidden;
scrollbar-width: thin; // firefox
&::-webkit-scrollbar {
height: 3px;
}
/* Track */
&::-webkit-scrollbar-track {
background: transparent;
}
/* Handle */
&::-webkit-scrollbar-thumb {
background: var(--dv-tabs-container-scrollbar-color);
}
.dv-tab {
-webkit-user-drag: element;
outline: none;
min-width: 75px;
cursor: pointer;
position: relative;
box-sizing: border-box;
&:not(:first-child)::before {
content: ' ';
position: absolute;
top: 0;
left: 0;
z-index: 5;
pointer-events: none;
background-color: var(--dv-tab-divider-color);
width: 1px;
height: 100%;
}
}
}
}

View File

@ -1,17 +1,23 @@
import {
IDisposable,
CompositeDisposable,
IValueDisposable,
Disposable,
MutableDisposable,
} from '../../../lifecycle';
import { addDisposableListener, Emitter, Event } from '../../../events';
import { Tab } from '../tab/tab';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { VoidContainer } from './voidContainer';
import { toggleClass } from '../../../dom';
import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel';
import { IDockviewPanel } from '../../dockviewPanel';
import { DockviewComponent } from '../../dockviewComponent';
import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel';
import { getPanelData } from '../../../dnd/dataTransfer';
import { Tabs } from './tabs';
import {
createDropdownElementHandle,
DropdownElement,
} from './tabOverflowControl';
export interface TabDropIndexEvent {
readonly event: DragEvent;
@ -56,25 +62,28 @@ export class TabsContainer
implements ITabsContainer
{
private readonly _element: HTMLElement;
private readonly tabContainer: HTMLElement;
private readonly tabs: Tabs;
private readonly rightActionsContainer: HTMLElement;
private readonly leftActionsContainer: HTMLElement;
private readonly preActionsContainer: HTMLElement;
private readonly voidContainer: VoidContainer;
private tabs: IValueDisposable<Tab>[] = [];
private selectedIndex = -1;
private rightActions: HTMLElement | undefined;
private leftActions: HTMLElement | undefined;
private preActions: HTMLElement | undefined;
private _hidden = false;
private dropdownPart: DropdownElement | null = null;
private _overflowTabs: string[] = [];
private readonly _dropdownDisposable = new MutableDisposable();
private readonly _onDrop = new Emitter<TabDropIndexEvent>();
readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event;
private readonly _onTabDragStart = new Emitter<TabDragEvent>();
readonly onTabDragStart: Event<TabDragEvent> = this._onTabDragStart.event;
get onTabDragStart(): Event<TabDragEvent> {
return this.tabs.onTabDragStart;
}
private readonly _onGroupDragStart = new Emitter<GroupDragEvent>();
readonly onGroupDragStart: Event<GroupDragEvent> =
@ -86,11 +95,11 @@ export class TabsContainer
this._onWillShowOverlay.event;
get panels(): string[] {
return this.tabs.map((_) => _.value.panel.id);
return this.tabs.panels;
}
get size(): number {
return this.tabs.length;
return this.tabs.size;
}
get hidden(): boolean {
@ -102,6 +111,118 @@ export class TabsContainer
this.element.style.display = value ? 'none' : '';
}
get element(): HTMLElement {
return this._element;
}
constructor(
private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel
) {
super();
this._element = document.createElement('div');
this._element.className = 'dv-tabs-and-actions-container';
toggleClass(
this._element,
'dv-full-width-single-tab',
this.accessor.options.singleTabMode === 'fullwidth'
);
this.rightActionsContainer = document.createElement('div');
this.rightActionsContainer.className = 'dv-right-actions-container';
this.leftActionsContainer = document.createElement('div');
this.leftActionsContainer.className = 'dv-left-actions-container';
this.preActionsContainer = document.createElement('div');
this.preActionsContainer.className = 'dv-pre-actions-container';
this.tabs = new Tabs(group, accessor, {
showTabsOverflowControl: !accessor.options.disableTabsOverflowList,
});
this.voidContainer = new VoidContainer(this.accessor, this.group);
this._element.appendChild(this.preActionsContainer);
this._element.appendChild(this.tabs.element);
this._element.appendChild(this.leftActionsContainer);
this._element.appendChild(this.voidContainer.element);
this._element.appendChild(this.rightActionsContainer);
this.addDisposables(
this.tabs.onDrop((e) => this._onDrop.fire(e)),
this.tabs.onWillShowOverlay((e) => this._onWillShowOverlay.fire(e)),
accessor.onDidOptionsChange(() => {
this.tabs.showTabsOverflowControl =
!accessor.options.disableTabsOverflowList;
}),
this.tabs.onOverflowTabsChange((event) => {
this.toggleDropdown(event);
}),
this.tabs,
this._onWillShowOverlay,
this._onDrop,
this._onGroupDragStart,
this.voidContainer,
this.voidContainer.onDragStart((event) => {
this._onGroupDragStart.fire({
nativeEvent: event,
group: this.group,
});
}),
this.voidContainer.onDrop((event) => {
this._onDrop.fire({
event: event.nativeEvent,
index: this.tabs.size,
});
}),
this.voidContainer.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire(
new WillShowOverlayLocationEvent(event, {
kind: 'header_space',
panel: this.group.activePanel,
api: this.accessor.api,
group: this.group,
getData: getPanelData,
})
);
}),
addDisposableListener(
this.voidContainer.element,
'pointerdown',
(event) => {
if (event.defaultPrevented) {
return;
}
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
if (
isFloatingGroupsEnabled &&
event.shiftKey &&
this.group.api.location.type !== 'floating'
) {
event.preventDefault();
const { top, left } =
this.element.getBoundingClientRect();
const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect();
this.accessor.addFloatingGroup(this.group, {
x: left - rootLeft + 20,
y: top - rootTop + 20,
inDragMode: true,
});
}
}
)
);
}
show(): void {
if (!this.hidden) {
this.element.style.display = '';
@ -154,319 +275,124 @@ export class TabsContainer
}
}
get element(): HTMLElement {
return this._element;
isActive(tab: Tab): boolean {
return this.tabs.isActive(tab);
}
public isActive(tab: Tab): boolean {
return (
this.selectedIndex > -1 &&
this.tabs[this.selectedIndex].value === tab
);
indexOf(id: string): number {
return this.tabs.indexOf(id);
}
public indexOf(id: string): number {
return this.tabs.findIndex((tab) => tab.value.panel.id === id);
}
constructor(
private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel
) {
super();
this._element = document.createElement('div');
this._element.className = 'dv-tabs-and-actions-container';
toggleClass(
this._element,
'dv-full-width-single-tab',
this.accessor.options.singleTabMode === 'fullwidth'
);
this.rightActionsContainer = document.createElement('div');
this.rightActionsContainer.className = 'dv-right-actions-container';
this.leftActionsContainer = document.createElement('div');
this.leftActionsContainer.className = 'dv-left-actions-container';
this.preActionsContainer = document.createElement('div');
this.preActionsContainer.className = 'dv-pre-actions-container';
this.tabContainer = document.createElement('div');
this.tabContainer.className = 'dv-tabs-container';
this.tabContainer.role = 'tablist';
this.voidContainer = new VoidContainer(this.accessor, this.group);
this._element.appendChild(this.preActionsContainer);
this._element.appendChild(this.tabContainer);
this._element.appendChild(this.leftActionsContainer);
this._element.appendChild(this.voidContainer.element);
this._element.appendChild(this.rightActionsContainer);
this.addDisposables(
this._onWillShowOverlay,
this._onDrop,
this._onTabDragStart,
this._onGroupDragStart,
this.accessor.onDidActivePanelChange((e) => {
if (e?.api.group === this.group) {
this.selectedIndex = this.indexOf(e.id);
} else {
this.selectedIndex = -1;
}
}),
this.voidContainer,
this.voidContainer.onDragStart((event) => {
this._onGroupDragStart.fire({
nativeEvent: event,
group: this.group,
});
}),
this.voidContainer.onDrop((event) => {
this._onDrop.fire({
event: event.nativeEvent,
index: this.tabs.length,
});
}),
this.voidContainer.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire(
new WillShowOverlayLocationEvent(event, {
kind: 'header_space',
panel: this.group.activePanel,
api: this.accessor.api,
group: this.group,
getData: getPanelData,
})
);
}),
addDisposableListener(
this.voidContainer.element,
'pointerdown',
(event) => {
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
if (
isFloatingGroupsEnabled &&
event.shiftKey &&
this.group.api.location.type !== 'floating'
) {
event.preventDefault();
const { top, left } =
this.element.getBoundingClientRect();
const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect();
this.accessor.addFloatingGroup(this.group, {
x: left - rootLeft + 20,
y: top - rootTop + 20,
inDragMode: true,
});
}
}
),
addDisposableListener(this.tabContainer, 'pointerdown', (event) => {
if (event.defaultPrevented) {
return;
}
const isLeftClick = event.button === 0;
if (isLeftClick) {
this.accessor.doSetGroupActive(this.group);
}
}),
addDisposableListener(this.tabContainer, 'keydown', (event) => {
if (event.defaultPrevented) {
return;
}
let tab: IValueDisposable<Tab> | undefined = undefined;
switch (event.key) {
case 'ArrowLeft': {
if (this.selectedIndex > 0) {
tab = this.tabs[this.selectedIndex - 1];
}
break;
}
case 'ArrowRight': {
if (this.selectedIndex + 1 < this.size) {
tab = this.tabs[this.selectedIndex + 1];
}
break;
}
case 'Home':
tab = this.tabs[0];
break;
case 'End':
tab = this.tabs[this.size - 1];
break;
}
if (tab == null) {
return;
}
this.group.model.openPanel(tab.value.panel);
})
);
}
public setActive(_isGroupActive: boolean) {
setActive(_isGroupActive: boolean) {
// noop
}
public delete(id: string): void {
const index = this.tabs.findIndex((tab) => tab.value.panel.id === id);
const tabToRemove = this.tabs.splice(index, 1)[0];
if (!tabToRemove) {
throw new Error(`dockview: Tab not found`);
}
const { value, disposable } = tabToRemove;
disposable.dispose();
value.dispose();
value.element.remove();
delete(id: string): void {
this.tabs.delete(id);
this.updateClassnames();
}
public setActivePanel(panel: IDockviewPanel): void {
this.tabs.forEach((tab) => {
const isActivePanel = panel.id === tab.value.panel.id;
tab.value.setActive(isActivePanel);
tab.value.panel.runEvents();
});
setActivePanel(panel: IDockviewPanel): void {
this.tabs.setActivePanel(panel);
}
public openPanel(
panel: IDockviewPanel,
index: number = this.tabs.length
): void {
if (this.tabs.find((tab) => tab.value.panel.id === panel.id)) {
return;
}
const tab = new Tab(panel, this.accessor, this.group);
tab.setContent(panel.view.tab);
const disposable = new CompositeDisposable(
tab.onDragStart((event) => {
this._onTabDragStart.fire({ nativeEvent: event, panel });
}),
tab.onPointerDown((event) => {
if (event.defaultPrevented) {
return;
}
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
const isFloatingWithOnePanel =
this.group.api.location.type === 'floating' &&
this.size === 1;
if (
isFloatingGroupsEnabled &&
!isFloatingWithOnePanel &&
event.shiftKey
) {
event.preventDefault();
const panel = this.accessor.getGroupPanel(tab.panel.id);
const { top, left } = tab.element.getBoundingClientRect();
const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect();
this.accessor.addFloatingGroup(panel as DockviewPanel, {
x: left - rootLeft,
y: top - rootTop,
inDragMode: true,
});
return;
}
switch (event.button) {
case 0: // left click or touch
if (this.group.activePanel !== panel) {
this.group.model.openPanel(panel);
}
break;
}
}),
tab.onDrop((event) => {
this._onDrop.fire({
event: event.nativeEvent,
index: this.tabs.findIndex((x) => x.value === tab),
});
}),
tab.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire(
new WillShowOverlayLocationEvent(event, {
kind: 'tab',
panel: this.group.activePanel,
api: this.accessor.api,
group: this.group,
getData: getPanelData,
})
);
})
);
const value: IValueDisposable<Tab> = { value: tab, disposable };
this.addTab(value, index);
openPanel(panel: IDockviewPanel, index: number = this.tabs.size): void {
this.tabs.openPanel(panel, index);
this.updateClassnames();
}
public closePanel(panel: IDockviewPanel): void {
closePanel(panel: IDockviewPanel): void {
this.delete(panel.id);
}
public dispose(): void {
super.dispose();
for (const { value, disposable } of this.tabs) {
disposable.dispose();
value.dispose();
}
this.tabs = [];
}
private addTab(
tab: IValueDisposable<Tab>,
index: number = this.tabs.length
): void {
if (index < 0 || index > this.tabs.length) {
throw new Error('invalid location');
}
this.tabContainer.insertBefore(
tab.value.element,
this.tabContainer.children[index]
);
this.tabs = [
...this.tabs.slice(0, index),
tab,
...this.tabs.slice(index),
];
if (this.selectedIndex < 0) {
this.selectedIndex = index;
}
this.updateClassnames();
}
private updateClassnames(): void {
toggleClass(this._element, 'dv-single-tab', this.size === 1);
}
private toggleDropdown(options: { tabs: string[]; reset: boolean }): void {
const tabs = options.reset ? [] : options.tabs;
this._overflowTabs = tabs;
if (this._overflowTabs.length > 0 && this.dropdownPart) {
this.dropdownPart.update({ tabs: tabs.length });
return;
}
if (this._overflowTabs.length === 0) {
this._dropdownDisposable.dispose();
return;
}
const root = document.createElement('div');
root.className = 'dv-tabs-overflow-dropdown-root';
const part = createDropdownElementHandle();
part.update({ tabs: tabs.length });
this.dropdownPart = part;
root.appendChild(part.element);
this.rightActionsContainer.prepend(root);
this._dropdownDisposable.value = new CompositeDisposable(
Disposable.from(() => {
root.remove();
this.dropdownPart?.dispose?.();
this.dropdownPart = null;
}),
addDisposableListener(
root,
'pointerdown',
(event) => {
event.preventDefault();
},
{ capture: true }
),
addDisposableListener(root, 'click', (event) => {
const el = document.createElement('div');
el.style.overflow = 'auto';
el.className = 'dv-tabs-overflow-container';
for (const tab of this.tabs.tabs.filter((tab) =>
this._overflowTabs.includes(tab.panel.id)
)) {
const panelObject = this.group.panels.find(
(panel) => panel === tab.panel
)!;
const tabComponent =
panelObject.view.createTabRenderer('headerOverflow');
const child = tabComponent.element;
const wrapper = document.createElement('div');
toggleClass(wrapper, 'dv-tab', true);
toggleClass(
wrapper,
'dv-active-tab',
panelObject.api.isActive
);
toggleClass(
wrapper,
'dv-inactive-tab',
!panelObject.api.isActive
);
wrapper.addEventListener('mousedown', () => {
this.accessor.popupService.close();
tab.element.scrollIntoView();
tab.panel.api.setActive();
});
wrapper.appendChild(child);
el.appendChild(wrapper);
}
this.accessor.popupService.openPopover(el, {
x: event.clientX,
y: event.clientY,
});
})
);
}
}

View File

@ -1,4 +1,3 @@
import { last } from '../../../array';
import { getPanelData } from '../../../dnd/dataTransfer';
import {
Droptarget,
@ -10,6 +9,7 @@ import { DockviewComponent } from '../../dockviewComponent';
import { addDisposableListener, Emitter, Event } from '../../../events';
import { CompositeDisposable } from '../../../lifecycle';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel';
export class VoidContainer extends CompositeDisposable {
private readonly _element: HTMLElement;
@ -54,16 +54,7 @@ export class VoidContainer extends CompositeDisposable {
const data = getPanelData();
if (data && this.accessor.id === data.viewId) {
if (
data.panelId === null &&
data.groupId === this.group.id
) {
// don't allow group move to drop on self
return false;
}
// don't show the overlay if the tab being dragged is the last panel of this group
return last(this.group.panels)?.id !== data.panelId;
return true;
}
return group.model.canDisplayOverlay(
@ -72,6 +63,7 @@ export class VoidContainer extends CompositeDisposable {
'header_space'
);
},
getOverrideTarget: () => group.model.dropTargetContainer?.model,
});
this.onWillShowOverlay = this.dropTraget.onWillShowOverlay;

View File

@ -18,34 +18,38 @@
.dv-groupview {
&.dv-active-group {
> .dv-tabs-and-actions-container > .dv-tabs-container > .dv-tab {
&.dv-active-tab {
background-color: var(
--dv-activegroup-visiblepanel-tab-background-color
);
color: var(--dv-activegroup-visiblepanel-tab-color);
}
&.dv-inactive-tab {
background-color: var(
--dv-activegroup-hiddenpanel-tab-background-color
);
color: var(--dv-activegroup-hiddenpanel-tab-color);
> .dv-tabs-and-actions-container {
.dv-tabs-container > .dv-tab {
&.dv-active-tab {
background-color: var(
--dv-activegroup-visiblepanel-tab-background-color
);
color: var(--dv-activegroup-visiblepanel-tab-color);
}
&.dv-inactive-tab {
background-color: var(
--dv-activegroup-hiddenpanel-tab-background-color
);
color: var(--dv-activegroup-hiddenpanel-tab-color);
}
}
}
}
&.dv-inactive-group {
> .dv-tabs-and-actions-container > .dv-tabs-container > .dv-tab {
&.dv-active-tab {
background-color: var(
--dv-inactivegroup-visiblepanel-tab-background-color
);
color: var(--dv-inactivegroup-visiblepanel-tab-color);
}
&.dv-inactive-tab {
background-color: var(
--dv-inactivegroup-hiddenpanel-tab-background-color
);
color: var(--dv-inactivegroup-hiddenpanel-tab-color);
> .dv-tabs-and-actions-container {
.dv-tabs-container > .dv-tab {
&.dv-active-tab {
background-color: var(
--dv-inactivegroup-visiblepanel-tab-background-color
);
color: var(--dv-inactivegroup-visiblepanel-tab-color);
}
&.dv-inactive-tab {
background-color: var(
--dv-inactivegroup-hiddenpanel-tab-background-color
);
color: var(--dv-inactivegroup-hiddenpanel-tab-color);
}
}
}
}

View File

@ -14,7 +14,7 @@ import {
import { tail, sequenceEquals, remove } from '../array';
import { DockviewPanel, IDockviewPanel } from './dockviewPanel';
import { CompositeDisposable, Disposable } from '../lifecycle';
import { Event, Emitter, addDisposableWindowListener } from '../events';
import { Event, Emitter, addDisposableListener } from '../events';
import { Watermark } from './components/watermark/watermark';
import { IWatermarkRenderer, GroupviewPanelState } from './types';
import { sequentialNumberGenerator } from '../math';
@ -54,7 +54,10 @@ import { Parameters } from '../panel/types';
import { Overlay } from '../overlay/overlay';
import {
addTestId,
Classnames,
getDockviewTheme,
onDidWindowResizeEnd,
onDidWindowMoveEnd,
toggleClass,
watchElementResize,
} from '../dom';
@ -74,6 +77,9 @@ import {
} from '../overlay/overlayRenderContainer';
import { PopoutWindow } from '../popoutWindow';
import { StrictEventsSequencing } from './strictEventsSequencing';
import { PopupService } from './components/popupService';
import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer';
import { themeAbyss } from './theme';
const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = {
activationSize: { type: 'pixels', value: 10 },
@ -186,12 +192,23 @@ export interface DockviewMaximizedGroupChanged {
isMaximized: boolean;
}
export interface PopoutGroupChangeSizeEvent {
width: number;
height: number;
group: DockviewGroupPanel;
}
export interface PopoutGroupChangePositionEvent {
screenX: number;
screenY: number;
group: DockviewGroupPanel;
}
export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly activePanel: IDockviewPanel | undefined;
readonly totalPanels: number;
readonly panels: IDockviewPanel[];
readonly orientation: Orientation;
readonly gap: number;
readonly onDidDrop: Event<DockviewDidDropEvent>;
readonly onWillDrop: Event<DockviewWillDropEvent>;
readonly onWillShowOverlay: Event<WillShowOverlayLocationEvent>;
@ -207,6 +224,8 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly onUnhandledDragOverEvent: Event<DockviewDndOverlayEvent>;
readonly onDidMovePanel: Event<MovePanelEvent>;
readonly onDidMaximizedGroupChange: Event<DockviewMaximizedGroupChanged>;
readonly onDidPopoutGroupSizeChange: Event<PopoutGroupChangeSizeEvent>;
readonly onDidPopoutGroupPositionChange: Event<PopoutGroupChangePositionEvent>;
readonly options: DockviewComponentOptions;
updateOptions(options: DockviewOptions): void;
moveGroupOrPanel(options: MoveGroupOrPanelOptions): void;
@ -253,9 +272,12 @@ export class DockviewComponent
private readonly _deserializer = new DefaultDockviewDeserialzier(this);
private readonly _api: DockviewApi;
private _options: Exclude<DockviewComponentOptions, 'orientation'>;
private watermark: IWatermarkRenderer | null = null;
private _watermark: IWatermarkRenderer | null = null;
private readonly _themeClassnames: Classnames;
readonly overlayRenderContainer: OverlayRenderContainer;
readonly popupService: PopupService;
readonly rootDropTargetContainer: DropTargetAnchorContainer;
private readonly _onWillDragPanel = new Emitter<TabDragEvent>();
readonly onWillDragPanel: Event<TabDragEvent> = this._onWillDragPanel.event;
@ -287,6 +309,16 @@ export class DockviewComponent
private readonly _onDidAddPanel = new Emitter<IDockviewPanel>();
readonly onDidAddPanel: Event<IDockviewPanel> = this._onDidAddPanel.event;
private readonly _onDidPopoutGroupSizeChange =
new Emitter<PopoutGroupChangeSizeEvent>();
readonly onDidPopoutGroupSizeChange: Event<PopoutGroupChangeSizeEvent> =
this._onDidPopoutGroupSizeChange.event;
private readonly _onDidPopoutGroupPositionChange =
new Emitter<PopoutGroupChangePositionEvent>();
readonly onDidPopoutGroupPositionChange: Event<PopoutGroupChangePositionEvent> =
this._onDidPopoutGroupPositionChange.event;
private readonly _onDidLayoutFromJSON = new Emitter<void>();
readonly onDidLayoutFromJSON: Event<void> = this._onDidLayoutFromJSON.event;
@ -320,6 +352,9 @@ export class DockviewComponent
readonly onDidAddGroup: Event<DockviewGroupPanel> =
this._onDidAddGroup.event;
private readonly _onDidOptionsChange = new Emitter<void>();
readonly onDidOptionsChange: Event<void> = this._onDidOptionsChange.event;
private readonly _onDidActiveGroupChange = new Emitter<
DockviewGroupPanel | undefined
>();
@ -360,10 +395,6 @@ export class DockviewComponent
return this._api;
}
get gap(): number {
return this.gridview.margin;
}
get floatingGroups(): DockviewFloatingGroupPanel[] {
return this._floatingGroups;
}
@ -377,95 +408,27 @@ export class DockviewComponent
: undefined,
disableAutoResizing: options.disableAutoResizing,
locked: options.locked,
margin: options.gap,
margin: options.theme?.gap ?? 0,
className: options.className,
});
this._options = options;
this.popupService = new PopupService(this.element);
this._themeClassnames = new Classnames(this.element);
this._api = new DockviewApi(this);
this.rootDropTargetContainer = new DropTargetAnchorContainer(
this.element,
{ disabled: true }
);
this.overlayRenderContainer = new OverlayRenderContainer(
this.gridview.element,
this
);
toggleClass(this.gridview.element, 'dv-dockview', true);
toggleClass(this.element, 'dv-debug', !!options.debug);
if (options.debug) {
this.addDisposables(new StrictEventsSequencing(this));
}
this.addDisposables(
this.overlayRenderContainer,
this._onWillDragPanel,
this._onWillDragGroup,
this._onWillShowOverlay,
this._onDidActivePanelChange,
this._onDidAddPanel,
this._onDidRemovePanel,
this._onDidLayoutFromJSON,
this._onDidDrop,
this._onWillDrop,
this._onDidMovePanel,
this._onDidAddGroup,
this._onDidRemoveGroup,
this._onDidActiveGroupChange,
this._onUnhandledDragOverEvent,
this._onDidMaximizedGroupChange,
this.onDidViewVisibilityChangeMicroTaskQueue(() => {
this.updateWatermark();
}),
this.onDidAdd((event) => {
if (!this._moving) {
this._onDidAddGroup.fire(event);
}
}),
this.onDidRemove((event) => {
if (!this._moving) {
this._onDidRemoveGroup.fire(event);
}
}),
this.onDidActiveChange((event) => {
if (!this._moving) {
this._onDidActiveGroupChange.fire(event);
}
}),
this.onDidMaximizedChange((event) => {
this._onDidMaximizedGroupChange.fire({
group: event.panel,
isMaximized: event.isMaximized,
});
}),
Event.any(
this.onDidAdd,
this.onDidRemove
)(() => {
this.updateWatermark();
}),
Event.any<unknown>(
this.onDidAddPanel,
this.onDidRemovePanel,
this.onDidAddGroup,
this.onDidRemove,
this.onDidMovePanel,
this.onDidActivePanelChange
)(() => {
this._bufferOnDidLayoutChange.fire();
}),
Disposable.from(() => {
// iterate over a copy of the array since .dispose() mutates the original array
for (const group of [...this._floatingGroups]) {
group.dispose();
}
// iterate over a copy of the array since .dispose() mutates the original array
for (const group of [...this._popoutGroups]) {
group.disposable.dispose();
}
})
);
this._options = options;
this._rootDropTarget = new Droptarget(this.element, {
className: 'dv-drop-target-edge',
canDisplayOverlay: (event, position) => {
const data = getPanelData();
@ -505,10 +468,96 @@ export class DockviewComponent
},
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
overlayModel:
this.options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL,
options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL,
getOverrideTarget: () => this.rootDropTargetContainer?.model,
});
this.updateDropTargetModel(options);
toggleClass(this.gridview.element, 'dv-dockview', true);
toggleClass(this.element, 'dv-debug', !!options.debug);
this.updateTheme();
this.updateWatermark();
if (options.debug) {
this.addDisposables(new StrictEventsSequencing(this));
}
this.addDisposables(
this.rootDropTargetContainer,
this.overlayRenderContainer,
this._onWillDragPanel,
this._onWillDragGroup,
this._onWillShowOverlay,
this._onDidActivePanelChange,
this._onDidAddPanel,
this._onDidRemovePanel,
this._onDidLayoutFromJSON,
this._onDidDrop,
this._onWillDrop,
this._onDidMovePanel,
this._onDidAddGroup,
this._onDidRemoveGroup,
this._onDidActiveGroupChange,
this._onUnhandledDragOverEvent,
this._onDidMaximizedGroupChange,
this._onDidOptionsChange,
this._onDidPopoutGroupSizeChange,
this._onDidPopoutGroupPositionChange,
this.onDidViewVisibilityChangeMicroTaskQueue(() => {
this.updateWatermark();
}),
this.onDidAdd((event) => {
if (!this._moving) {
this._onDidAddGroup.fire(event);
}
}),
this.onDidRemove((event) => {
if (!this._moving) {
this._onDidRemoveGroup.fire(event);
}
}),
this.onDidActiveChange((event) => {
if (!this._moving) {
this._onDidActiveGroupChange.fire(event);
}
}),
this.onDidMaximizedChange((event) => {
this._onDidMaximizedGroupChange.fire({
group: event.panel,
isMaximized: event.isMaximized,
});
}),
Event.any(
this.onDidAdd,
this.onDidRemove
)(() => {
this.updateWatermark();
}),
Event.any<unknown>(
this.onDidAddPanel,
this.onDidRemovePanel,
this.onDidAddGroup,
this.onDidRemove,
this.onDidMovePanel,
this.onDidActivePanelChange,
this.onDidPopoutGroupPositionChange,
this.onDidPopoutGroupSizeChange
)(() => {
this._bufferOnDidLayoutChange.fire();
}),
Disposable.from(() => {
// iterate over a copy of the array since .dispose() mutates the original array
for (const group of [...this._floatingGroups]) {
group.dispose();
}
// iterate over a copy of the array since .dispose() mutates the original array
for (const group of [...this._popoutGroups]) {
group.disposable.dispose();
}
}),
this._rootDropTarget,
this._rootDropTarget.onWillShowOverlay((event) => {
if (this.gridview.length > 0 && event.position === 'center') {
@ -571,10 +620,6 @@ export class DockviewComponent
}),
this._rootDropTarget
);
this._api = new DockviewApi(this);
this.updateWatermark();
}
override setVisible(panel: DockviewGroupPanel, visible: boolean): void {
@ -756,6 +801,15 @@ export class DockviewComponent
popoutContainer.appendChild(group.element);
const anchor = document.createElement('div');
const dropTargetContainer = new DropTargetAnchorContainer(
anchor,
{ disabled: this.rootDropTargetContainer.disabled }
);
popoutContainer.appendChild(anchor);
group.model.dropTargetContainer = dropTargetContainer;
group.model.location = {
type: 'popout',
getWindow: () => _window.window!,
@ -803,22 +857,37 @@ export class DockviewComponent
},
};
const _onDidWindowPositionChange = onDidWindowMoveEnd(
_window.window!
);
popoutWindowDisposable.addDisposables(
_onDidWindowPositionChange,
onDidWindowResizeEnd(_window.window!, () => {
this._onDidPopoutGroupSizeChange.fire({
width: _window.window!.innerWidth,
height: _window.window!.innerHeight,
group,
});
}),
_onDidWindowPositionChange.event(() => {
this._onDidPopoutGroupPositionChange.fire({
screenX: _window.window!.screenX,
screenY: _window.window!.screenX,
group,
});
}),
/**
* ResizeObserver seems slow here, I do not know why but we don't need it
* since we can reply on the window resize event as we will occupy the full
* window dimensions
*/
addDisposableWindowListener(
_window.window!,
'resize',
() => {
group.layout(
_window.window!.innerWidth,
_window.window!.innerHeight
);
}
),
addDisposableListener(_window.window!, 'resize', () => {
group.layout(
_window.window!.innerWidth,
_window.window!.innerHeight
);
}),
overlayRenderContainer,
Disposable.from(() => {
if (this.isDisposed) {
@ -848,6 +917,8 @@ export class DockviewComponent
} else if (this.getPanel(group.id)) {
group.model.renderContainer =
this.overlayRenderContainer;
group.model.dropTargetContainer =
this.rootDropTargetContainer;
returnedGroup = group;
const alreadyRemoved = !this._popoutGroups.find(
@ -1100,7 +1171,10 @@ export class DockviewComponent
this.updateWatermark();
}
private orthogonalize(position: Position): DockviewGroupPanel {
private orthogonalize(
position: Position,
options?: GroupOptions
): DockviewGroupPanel {
switch (position) {
case 'top':
case 'bottom':
@ -1126,10 +1200,14 @@ export class DockviewComponent
case 'top':
case 'left':
case 'center':
return this.createGroupAtLocation([0]); // insert into first position
return this.createGroupAtLocation([0], undefined, options); // insert into first position
case 'bottom':
case 'right':
return this.createGroupAtLocation([this.gridview.length]); // insert into last position
return this.createGroupAtLocation(
[this.gridview.length],
undefined,
options
); // insert into last position
default:
throw new Error(`unsupported position ${position}`);
}
@ -1162,18 +1240,14 @@ export class DockviewComponent
}
}
if ('rootOverlayModel' in options) {
this._rootDropTarget.setOverlayModel(
options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL
);
}
if ('gap' in options) {
this.gridview.margin = options.gap ?? 0;
}
this.updateDropTargetModel(options);
this._options = { ...this.options, ...options };
if ('theme' in options) {
this.updateTheme();
}
this.layout(this.gridview.width, this.gridview.height, true);
}
@ -1749,24 +1823,24 @@ export class DockviewComponent
(x) => x.api.location.type === 'grid' && x.api.isVisible
).length === 0
) {
if (!this.watermark) {
this.watermark = this.createWatermarkComponent();
if (!this._watermark) {
this._watermark = this.createWatermarkComponent();
this.watermark.init({
this._watermark.init({
containerApi: new DockviewApi(this),
});
const watermarkContainer = document.createElement('div');
watermarkContainer.className = 'dv-watermark-container';
addTestId(watermarkContainer, 'watermark-component');
watermarkContainer.appendChild(this.watermark.element);
watermarkContainer.appendChild(this._watermark.element);
this.gridview.element.appendChild(watermarkContainer);
}
} else if (this.watermark) {
this.watermark.element.parentElement!.remove();
this.watermark.dispose?.();
this.watermark = null;
} else if (this._watermark) {
this._watermark.element.parentElement!.remove();
this._watermark.dispose?.();
this._watermark = null;
}
}
@ -1808,7 +1882,8 @@ export class DockviewComponent
}
} else {
const group = this.orthogonalize(
directionToPosition(<Direction>options.direction)
directionToPosition(<Direction>options.direction),
options
);
if (!options.skipSetActive) {
this.doSetGroupAndPanelActive(group);
@ -2408,9 +2483,11 @@ export class DockviewComponent
if (this._moving) {
return;
}
if (event.panel !== this.activePanel) {
return;
}
if (this._onDidActivePanelChange.value !== event.panel) {
this._onDidActivePanelChange.fire(event.panel);
}
@ -2474,9 +2551,10 @@ export class DockviewComponent
private createGroupAtLocation(
location: number[],
size?: number
size?: number,
options?: GroupOptions
): DockviewGroupPanel {
const group = this.createGroup();
const group = this.createGroup(options);
this.doAddGroup(group, location, size);
return group;
}
@ -2493,4 +2571,44 @@ export class DockviewComponent
? rootOrientation
: orthogonal(rootOrientation);
}
private updateDropTargetModel(options: Partial<DockviewComponentOptions>) {
if ('dndEdges' in options) {
this._rootDropTarget.disabled =
typeof options.dndEdges === 'boolean' &&
options.dndEdges === false;
if (
typeof options.dndEdges === 'object' &&
options.dndEdges !== null
) {
this._rootDropTarget.setOverlayModel(options.dndEdges);
} else {
this._rootDropTarget.setOverlayModel(
DEFAULT_ROOT_OVERLAY_MODEL
);
}
}
if ('rootOverlayModel' in options) {
this.updateDropTargetModel({ dndEdges: options.dndEdges });
}
}
private updateTheme(): void {
const theme = this._options.theme ?? themeAbyss;
this._themeClassnames.setClassNames(theme.className);
this.gridview.margin = theme.gap ?? 0;
switch (theme.dndOverlayMounting) {
case 'absolute':
this.rootDropTargetContainer.disabled = false;
break;
case 'relative':
default:
this.rootDropTargetContainer.disabled = true;
break;
}
}
}

View File

@ -39,6 +39,7 @@ import {
import { OverlayRenderContainer } from '../overlay/overlayRenderContainer';
import { TitleEvent } from '../api/dockviewPanelApi';
import { Contraints } from '../gridview/gridviewPanel';
import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer';
interface GroupMoveEvent {
groupId: string;
@ -265,6 +266,8 @@ export class DockviewGroupPanelModel
private mostRecentlyUsed: IDockviewPanel[] = [];
private _overwriteRenderContainer: OverlayRenderContainer | null = null;
private _overwriteDropTargetContainer: DropTargetAnchorContainer | null =
null;
private readonly _onDidChange = new Emitter<IViewSize | undefined>();
readonly onDidChange: Event<IViewSize | undefined> =
@ -535,6 +538,17 @@ export class DockviewGroupPanelModel
);
}
set dropTargetContainer(value: DropTargetAnchorContainer | null) {
this._overwriteDropTargetContainer = value;
}
get dropTargetContainer(): DropTargetAnchorContainer | null {
return (
this._overwriteDropTargetContainer ??
this.accessor.rootDropTargetContainer
);
}
initialize(): void {
if (this.options.panels) {
this.options.panels.forEach((panel) => {
@ -1049,6 +1063,29 @@ export class DockviewGroupPanelModel
const data = getPanelData();
if (data && data.viewId === this.accessor.id) {
if (type === 'content') {
if (data.groupId === this.id) {
// don't allow to drop on self for center position
if (position === 'center') {
return;
}
if (data.panelId === null) {
// don't allow group move to drop anywhere on self
return;
}
}
}
if (type === 'header') {
if (data.groupId === this.id) {
if (data.panelId === null) {
return;
}
}
}
if (data.panelId === null) {
// this is a group move dnd event
const { groupId } = data;

View File

@ -4,10 +4,10 @@ import {
IContentRenderer,
ITabRenderer,
} from './types';
import { DockviewGroupPanel } from './dockviewGroupPanel';
import { IDisposable } from '../lifecycle';
import { IDockviewComponent } from './dockviewComponent';
import { PanelUpdateEvent } from '../panel/types';
import { TabLocation } from './framework';
export interface IDockviewPanelModel extends IDisposable {
readonly contentComponent: string;
@ -17,13 +17,16 @@ export interface IDockviewPanelModel extends IDisposable {
update(event: PanelUpdateEvent): void;
layout(width: number, height: number): void;
init(params: GroupPanelPartInitParameters): void;
updateParentGroup(group: DockviewGroupPanel, isPanelVisible: boolean): void;
createTabRenderer(tabLocation: TabLocation): ITabRenderer;
}
export class DockviewPanelModel implements IDockviewPanelModel {
private readonly _content: IContentRenderer;
private readonly _tab: ITabRenderer;
private _params: GroupPanelPartInitParameters | undefined;
private _updateEvent: PanelUpdateEvent | undefined;
get content(): IContentRenderer {
return this._content;
}
@ -42,16 +45,23 @@ export class DockviewPanelModel implements IDockviewPanelModel {
this._tab = this.createTabComponent(this.id, tabComponent);
}
init(params: GroupPanelPartInitParameters): void {
this.content.init(params);
this.tab.init(params);
createTabRenderer(tabLocation: TabLocation): ITabRenderer {
const cmp = this.createTabComponent(this.id, this.tabComponent);
if (this._params) {
cmp.init({ ...this._params, tabLocation });
}
if (this._updateEvent) {
cmp.update?.(this._updateEvent);
}
return cmp;
}
updateParentGroup(
_group: DockviewGroupPanel,
_isPanelVisible: boolean
): void {
// noop
init(params: GroupPanelPartInitParameters): void {
this._params = params;
this.content.init(params);
this.tab.init({ ...params, tabLocation: 'header' });
}
layout(width: number, height: number): void {
@ -59,6 +69,8 @@ export class DockviewPanelModel implements IDockviewPanelModel {
}
update(event: PanelUpdateEvent): void {
this._updateEvent = event;
this.content.update?.(event);
this.tab.update?.(event);
}

View File

@ -11,9 +11,11 @@ export interface IGroupPanelBaseProps<T extends { [index: string]: any } = any>
containerApi: DockviewApi;
}
export type TabLocation = 'header' | 'headerOverflow';
export type IDockviewPanelHeaderProps<
T extends { [index: string]: any } = any
> = IGroupPanelBaseProps<T>;
> = IGroupPanelBaseProps<T> & { tabLocation: TabLocation };
export type IDockviewPanelProps<T extends { [index: string]: any } = any> =
IGroupPanelBaseProps<T>;

View File

@ -17,6 +17,7 @@ import { IGroupHeaderProps } from './framework';
import { FloatingGroupOptions } from './dockviewComponent';
import { Contraints } from '../gridview/gridviewPanel';
import { AcceptableEvent, IAcceptableEvent } from '../events';
import { DockviewTheme } from './theme';
export interface IHeaderActionsRenderer extends IDisposable {
readonly element: HTMLElement;
@ -52,18 +53,29 @@ export interface DockviewOptions {
popoutUrl?: string;
defaultRenderer?: DockviewPanelRenderer;
debug?: boolean;
rootOverlayModel?: DroptargetOverlayModel;
locked?: boolean;
disableDnd?: boolean;
className?: string;
// #start dnd
dndEdges?: false | DroptargetOverlayModel;
/**
* Pixel gap between groups
*/
gap?: number;
* @deprecated use `dndEdges` instead. To be removed in a future version.
* */
rootOverlayModel?: DroptargetOverlayModel;
disableDnd?: boolean;
// #end dnd
locked?: boolean;
className?: string;
/**
* Define the behaviour of the dock when there are no panels to display. Defaults to `watermark`.
*/
noPanelsOverlay?: 'emptyGroup' | 'watermark';
theme?: DockviewTheme;
disableTabsOverflowList?: boolean;
/**
* Select `native` to use built-in scrollbar behaviours and `custom` to use an internal implementation
* that allows for improved scrollbar overlay UX.
*
* This is only applied to the tab header section. Defaults to `custom`.
*/
scrollbars?: 'native' | 'custom';
}
export interface DockviewDndOverlayEvent extends IAcceptableEvent {
@ -106,9 +118,12 @@ export const PROPERTY_KEYS_DOCKVIEW: (keyof DockviewOptions)[] = (() => {
rootOverlayModel: undefined,
locked: undefined,
disableDnd: undefined,
gap: undefined,
className: undefined,
noPanelsOverlay: undefined,
dndEdges: undefined,
theme: undefined,
disableTabsOverflowList: undefined,
scrollbars: undefined,
};
return Object.keys(properties) as (keyof DockviewOptions)[];

View File

@ -0,0 +1,70 @@
export interface DockviewTheme {
/**
* The name of the theme
*/
name: string;
/**
* The class name to apply to the theme containing the CSS variables settings.
*/
className: string;
/**
* The gap between the groups
*/
gap?: number;
/**
* The mouting position of the overlay shown when dragging a panel. `absolute`
* will mount the overlay to root of the dockview component whereas `relative` will mount the overlay to the group container.
*/
dndOverlayMounting?: 'absolute' | 'relative';
/**
* When dragging a panel, the overlay can either encompass the panel contents or the entire group including the tab header space.
*/
dndPanelOverlay?: 'content' | 'group';
}
export const themeDark: DockviewTheme = {
name: 'dark',
className: 'dockview-theme-dark',
};
export const themeLight: DockviewTheme = {
name: 'light',
className: 'dockview-theme-light',
};
export const themeVisualStudio: DockviewTheme = {
name: 'visualStudio',
className: 'dockview-theme-vs',
};
export const themeAbyss: DockviewTheme = {
name: 'abyss',
className: 'dockview-theme-abyss',
};
export const themeDracula: DockviewTheme = {
name: 'dracula',
className: 'dockview-theme-dracula',
};
export const themeReplit: DockviewTheme = {
name: 'replit',
className: 'dockview-theme-replit',
gap: 10,
};
export const themeAbyssSpaced: DockviewTheme = {
name: 'abyssSpaced',
className: 'dockview-theme-abyss-spaced',
gap: 10,
dndOverlayMounting: 'absolute',
dndPanelOverlay: 'group',
};
export const themeLightSpaced: DockviewTheme = {
name: 'lightSpaced',
className: 'dockview-theme-light-spaced',
gap: 10,
dndOverlayMounting: 'absolute',
dndPanelOverlay: 'group',
};

View File

@ -4,6 +4,7 @@ import { DockviewApi } from '../api/component.api';
import { Optional } from '../types';
import { IDockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer';
import { TabLocation } from './framework';
export interface HeaderPartInitParameters {
title: string;
@ -34,10 +35,14 @@ export interface IWatermarkRenderer
init: (params: WatermarkRendererInitParameters) => void;
}
export interface TabPartInitParameters extends GroupPanelPartInitParameters {
tabLocation: TabLocation;
}
export interface ITabRenderer
extends Optional<Omit<IPanel, 'id'>, RendererMethodOptionalList> {
readonly element: HTMLElement;
init(parameters: GroupPanelPartInitParameters): void;
init(parameters: TabPartInitParameters): void;
}
export interface IContentRenderer

View File

@ -2,7 +2,6 @@ import {
Event as DockviewEvent,
Emitter,
addDisposableListener,
addDisposableWindowListener,
} from './events';
import { IDisposable, CompositeDisposable } from './lifecycle';
@ -112,8 +111,11 @@ export function isAncestor(
return false;
}
export function getElementsByTagName(tag: string): HTMLElement[] {
return Array.prototype.slice.call(document.getElementsByTagName(tag), 0);
export function getElementsByTagName(
tag: string,
document: ParentNode
): HTMLElement[] {
return Array.prototype.slice.call(document.querySelectorAll(tag), 0);
}
export interface IFocusTracker extends IDisposable {
@ -122,7 +124,7 @@ export interface IFocusTracker extends IDisposable {
refreshState?(): void;
}
export function trackFocus(element: HTMLElement | Window): IFocusTracker {
export function trackFocus(element: HTMLElement): IFocusTracker {
return new FocusTracker(element);
}
@ -138,7 +140,7 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker {
private readonly _refreshStateHandler: () => void;
constructor(element: HTMLElement | Window) {
constructor(element: HTMLElement) {
super();
this.addDisposables(this._onDidFocus, this._onDidBlur);
@ -181,21 +183,12 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker {
}
};
if (element instanceof HTMLElement) {
this.addDisposables(
addDisposableListener(element, 'focus', onFocus, true)
);
this.addDisposables(
addDisposableListener(element, 'blur', onBlur, true)
);
} else {
this.addDisposables(
addDisposableWindowListener(element, 'focus', onFocus, true)
);
this.addDisposables(
addDisposableWindowListener(element, 'blur', onBlur, true)
);
}
this.addDisposables(
addDisposableListener(element, 'focus', onFocus, true)
);
this.addDisposables(
addDisposableListener(element, 'blur', onBlur, true)
);
}
refreshState(): void {
@ -288,11 +281,36 @@ export function addTestId(element: HTMLElement, id: string): void {
element.setAttribute('data-testid', id);
}
export function disableIframePointEvents() {
const iframes: HTMLElement[] = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
/**
* Should be more efficient than element.querySelectorAll("*") since there
* is no need to store every element in-memory using this approach
*/
function allTagsNamesInclusiveOfShadowDoms(tagNames: string[]) {
const iframes: HTMLElement[] = [];
function findIframesInNode(node: Element) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (tagNames.includes(node.tagName)) {
iframes.push(node as HTMLElement);
}
if (node.shadowRoot) {
findIframesInNode(<any>node.shadowRoot);
}
for (const child of node.children) {
findIframesInNode(child);
}
}
}
findIframesInNode(document.documentElement);
return iframes;
}
export function disableIframePointEvents(rootNode: ParentNode = document) {
const iframes = allTagsNamesInclusiveOfShadowDoms(['IFRAME', 'WEBVIEW']);
const original = new WeakMap<HTMLElement, string>(); // don't hold onto HTMLElement references longer than required
@ -357,3 +375,79 @@ export class Classnames {
}
}
}
const DEBOUCE_DELAY = 100;
export function isChildEntirelyVisibleWithinParent(
child: HTMLElement,
parent: HTMLElement
): boolean {
//
const childPosition = getDomNodePagePosition(child);
const parentPosition = getDomNodePagePosition(parent);
if (childPosition.left < parentPosition.left) {
return false;
}
if (
childPosition.left + childPosition.width >
parentPosition.left + parentPosition.width
) {
return false;
}
return true;
}
export function onDidWindowMoveEnd(window: Window): Emitter<void> {
const emitter = new Emitter<void>();
let previousScreenX = window.screenX;
let previousScreenY = window.screenY;
let timeout: any;
const checkMovement = () => {
if (window.closed) {
return;
}
const currentScreenX = window.screenX;
const currentScreenY = window.screenY;
if (
currentScreenX !== previousScreenX ||
currentScreenY !== previousScreenY
) {
clearTimeout(timeout);
timeout = setTimeout(() => {
emitter.fire();
}, DEBOUCE_DELAY);
previousScreenX = currentScreenX;
previousScreenY = currentScreenY;
}
requestAnimationFrame(checkMovement);
};
checkMovement();
return emitter;
}
export function onDidWindowResizeEnd(element: Window, cb: () => void) {
let resizeTimeout: any;
const disposable = new CompositeDisposable(
addDisposableListener(element, 'resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
cb();
}, DEBOUCE_DELAY);
})
);
return disposable;
}

View File

@ -193,32 +193,38 @@ export class Emitter<T> implements IDisposable {
}
}
export function addDisposableWindowListener<K extends keyof WindowEventMap>(
export function addDisposableListener<K extends keyof WindowEventMap>(
element: Window,
type: K,
listener: (this: Window, ev: WindowEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): IDisposable {
element.addEventListener(type, listener, options);
return {
dispose: () => {
element.removeEventListener(type, listener, options);
},
};
}
): IDisposable;
export function addDisposableListener<K extends keyof HTMLElementEventMap>(
element: HTMLElement,
type: K,
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): IDisposable;
export function addDisposableListener<
K extends keyof HTMLElementEventMap | keyof WindowEventMap
>(
element: HTMLElement | Window,
type: K,
listener: (
this: K extends keyof HTMLElementEventMap ? HTMLElement : Window,
ev: K extends keyof HTMLElementEventMap
? HTMLElementEventMap[K]
: K extends keyof WindowEventMap
? WindowEventMap[K]
: never
) => any,
options?: boolean | AddEventListenerOptions
): IDisposable {
element.addEventListener(type, listener, options);
element.addEventListener(type, <any>listener, options);
return {
dispose: () => {
element.removeEventListener(type, listener, options);
element.removeEventListener(type, <any>listener, options);
},
};
}

View File

@ -11,6 +11,9 @@ import { Classnames } from '../dom';
const nextLayoutId = sequentialNumberGenerator();
/**
* A direction in which a panel can be moved or placed relative to another panel.
*/
export type Direction = 'left' | 'right' | 'above' | 'below' | 'within';
export function toTarget(direction: Direction): Position {

View File

@ -64,6 +64,7 @@ export {
} from './dockview/framework';
export * from './dockview/options';
export * from './dockview/theme';
export * from './dockview/dockviewPanel';
export { DefaultTab } from './dockview/components/tab/defaultTab';
export {

View File

@ -7,7 +7,6 @@ import {
Emitter,
Event,
addDisposableListener,
addDisposableWindowListener,
} from '../events';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { clamp } from '../math';
@ -258,7 +257,7 @@ export class Overlay extends CompositeDisposable {
iframes.release();
},
},
addDisposableWindowListener(window, 'pointermove', (e) => {
addDisposableListener(window, 'pointermove', (e) => {
const containerRect =
this.options.container.getBoundingClientRect();
const x = e.clientX - containerRect.left;
@ -344,7 +343,7 @@ export class Overlay extends CompositeDisposable {
this.setBounds(bounds);
}),
addDisposableWindowListener(window, 'pointerup', () => {
addDisposableListener(window, 'pointerup', () => {
toggleClass(
this._element,
'dv-resize-container-dragging',
@ -439,7 +438,7 @@ export class Overlay extends CompositeDisposable {
const iframes = disableIframePointEvents();
move.value = new CompositeDisposable(
addDisposableWindowListener(window, 'pointermove', (e) => {
addDisposableListener(window, 'pointermove', (e) => {
const containerRect =
this.options.container.getBoundingClientRect();
const overlayRect =
@ -610,7 +609,7 @@ export class Overlay extends CompositeDisposable {
iframes.release();
},
},
addDisposableWindowListener(window, 'pointerup', () => {
addDisposableListener(window, 'pointerup', () => {
move.dispose();
this._onDidChangeEnd.fire();
})

View File

@ -38,20 +38,37 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel {
readonly onUnhandledDragOverEvent: Event<PaneviewDndOverlayEvent> =
this._onUnhandledDragOverEvent.event;
constructor(
private readonly accessor: IPaneviewComponent,
id: string,
component: string,
headerComponent: string | undefined,
orientation: Orientation,
isExpanded: boolean,
disableDnd: boolean
) {
super(id, component, headerComponent, orientation, isExpanded, true);
readonly accessor: IPaneviewComponent;
constructor(options: {
accessor: IPaneviewComponent;
id: string;
component: string;
headerComponent: string | undefined;
orientation: Orientation;
isExpanded: boolean;
disableDnd: boolean;
headerSize: number;
minimumBodySize: number;
maximumBodySize: number;
}) {
super({
id: options.id,
component: options.component,
headerComponent: options.headerComponent,
orientation: options.orientation,
isExpanded: options.isExpanded,
isHeaderVisible: true,
headerSize: options.headerSize,
minimumBodySize: options.minimumBodySize,
maximumBodySize: options.maximumBodySize,
});
this.accessor = options.accessor;
this.addDisposables(this._onDidDrop, this._onUnhandledDragOverEvent);
if (!disableDnd) {
if (!options.disableDnd) {
this.initDragFeatures();
}
}

View File

@ -21,11 +21,16 @@ import { Classnames } from '../dom';
const nextLayoutId = sequentialNumberGenerator();
const HEADER_SIZE = 22;
const MINIMUM_BODY_SIZE = 0;
const MAXIMUM_BODY_SIZE = Number.MAX_SAFE_INTEGER;
export interface SerializedPaneviewPanel {
snap?: boolean;
priority?: LayoutPriority;
minimumSize?: number;
maximumSize?: number;
headerSize?: number;
data: {
id: string;
component: string;
@ -54,17 +59,23 @@ export class PaneFramework extends DraggablePaneviewPanel {
isExpanded: boolean;
disableDnd: boolean;
accessor: IPaneviewComponent;
headerSize: number;
minimumBodySize: number;
maximumBodySize: number;
}
) {
super(
options.accessor,
options.id,
options.component,
options.headerComponent,
options.orientation,
options.isExpanded,
options.disableDnd
);
super({
accessor: options.accessor,
id: options.id,
component: options.component,
headerComponent: options.headerComponent,
orientation: options.orientation,
isExpanded: options.isExpanded,
disableDnd: options.disableDnd,
headerSize: options.headerSize,
minimumBodySize: options.minimumBodySize,
maximumBodySize: options.maximumBodySize,
});
}
getBodyComponent() {
@ -83,6 +94,7 @@ export interface AddPaneviewComponentOptions<T extends object = Parameters> {
params?: T;
minimumBodySize?: number;
maximumBodySize?: number;
headerSize?: number;
isExpanded?: boolean;
title: string;
index?: number;
@ -277,6 +289,9 @@ export class PaneviewComponent extends Resizable implements IPaneviewComponent {
isExpanded: !!options.isExpanded,
disableDnd: !!this.options.disableDnd,
accessor: this,
headerSize: options.headerSize ?? HEADER_SIZE,
minimumBodySize: MINIMUM_BODY_SIZE,
maximumBodySize: MAXIMUM_BODY_SIZE,
});
this.doAddPanel(view);
@ -344,6 +359,7 @@ export class PaneviewComponent extends Resizable implements IPaneviewComponent {
data: view.toJSON(),
minimumSize: minimum(view.minimumBodySize),
maximumSize: maximum(view.maximumBodySize),
headerSize: view.headerSize,
expanded: view.isExpanded(),
};
});
@ -403,6 +419,9 @@ export class PaneviewComponent extends Resizable implements IPaneviewComponent {
isExpanded: !!view.expanded,
disableDnd: !!this.options.disableDnd,
accessor: this,
headerSize: view.headerSize ?? HEADER_SIZE,
minimumBodySize: view.minimumSize ?? MINIMUM_BODY_SIZE,
maximumBodySize: view.maximumSize ?? MAXIMUM_BODY_SIZE,
});
this.doAddPanel(panel);
@ -476,6 +495,8 @@ export class PaneviewComponent extends Resizable implements IPaneviewComponent {
}
this._viewDisposables.clear();
this.element.remove();
this.paneview.dispose();
}
}

View File

@ -71,22 +71,23 @@ export abstract class PaneviewPanel
readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> =
this._onDidChange.event;
private readonly headerSize = 22;
private _orthogonalSize = 0;
private _size = 0;
private _minimumBodySize = 100;
private _maximumBodySize: number = Number.POSITIVE_INFINITY;
private _minimumBodySize: number;
private _maximumBodySize: number;
private _isExpanded = false;
protected header?: HTMLElement;
protected body?: HTMLElement;
private bodyPart?: IPanePart;
private headerPart?: IPanePart;
private expandedSize = 0;
private animationTimer: any;
private _orientation: Orientation;
private _headerVisible: boolean;
readonly headerSize: number;
readonly headerComponent: string | undefined;
set orientation(value: Orientation) {
this._orientation = value;
}
@ -149,24 +150,37 @@ export abstract class PaneviewPanel
this.header!.style.display = value ? '' : 'none';
}
constructor(
id: string,
component: string,
private readonly headerComponent: string | undefined,
orientation: Orientation,
isExpanded: boolean,
isHeaderVisible: boolean
) {
super(id, component, new PaneviewPanelApiImpl(id, component));
constructor(options: {
id: string;
component: string;
headerComponent: string | undefined;
orientation: Orientation;
isExpanded: boolean;
isHeaderVisible: boolean;
headerSize: number;
minimumBodySize: number;
maximumBodySize: number;
}) {
super(
options.id,
options.component,
new PaneviewPanelApiImpl(options.id, options.component)
);
this.api.pane = this; // TODO cannot use 'this' before 'super'
this.api.initialize(this);
this._isExpanded = isExpanded;
this._headerVisible = isHeaderVisible;
this.headerSize = options.headerSize;
this.headerComponent = options.headerComponent;
this._minimumBodySize = options.minimumBodySize;
this._maximumBodySize = options.maximumBodySize;
this._isExpanded = options.isExpanded;
this._headerVisible = options.isHeaderVisible;
this._onDidChangeExpansionState.fire(this.isExpanded()); // initialize value
this._orientation = orientation;
this._orientation = options.orientation;
this.element.classList.add('dv-pane');
@ -260,9 +274,6 @@ export abstract class PaneviewPanel
this.orientation === Orientation.HORIZONTAL
? [size, orthogonalSize]
: [orthogonalSize, size];
if (this.isExpanded()) {
this.expandedSize = width;
}
super.layout(width, height);
}

View File

@ -1,5 +1,5 @@
import { addStyles } from './dom';
import { Emitter, addDisposableWindowListener } from './events';
import { Emitter, addDisposableListener } from './events';
import { CompositeDisposable, Disposable, IDisposable } from './lifecycle';
import { Box } from './types';
@ -101,7 +101,7 @@ export class PopoutWindow extends CompositeDisposable {
Disposable.from(() => {
externalWindow.close();
}),
addDisposableWindowListener(window, 'beforeunload', () => {
addDisposableListener(window, 'beforeunload', () => {
/**
* before the main window closes we should close this popup too
* to be good citizens
@ -146,7 +146,7 @@ export class PopoutWindow extends CompositeDisposable {
* beforeunload must be registered after load for reasons I could not determine
* otherwise the beforeunload event will not fire when the window is closed
*/
addDisposableWindowListener(
addDisposableListener(
externalWindow,
'beforeunload',
() => {

View File

@ -0,0 +1,28 @@
.dv-scrollable {
position: relative;
overflow: hidden;
.dv-scrollbar-horizontal {
position: absolute;
bottom: 0px;
left: 0px;
height: 4px;
border-radius: 2px;
background-color: transparent;
transition-property: background-color;
transition-timing-function: ease-in-out;
transition-duration: 1s;
transition-delay: 0s;
}
&:hover,
&.dv-scrollable-resizing,
&.dv-scrollable-scrolling {
.dv-scrollbar-horizontal {
background-color: var(
--dv-scrollbar-background-color,
rgba(255, 255, 255, 0.25)
);
}
}
}

View File

@ -0,0 +1,131 @@
import { toggleClass, watchElementResize } from './dom';
import { addDisposableListener } from './events';
import { CompositeDisposable } from './lifecycle';
import { clamp } from './math';
export class Scrollbar extends CompositeDisposable {
private readonly _element: HTMLElement;
private readonly _horizontalScrollbar: HTMLElement;
private _scrollLeft: number = 0;
private _animationTimer: any;
public static MouseWheelSpeed = 1;
get element(): HTMLElement {
return this._element;
}
constructor(private readonly scrollableElement: HTMLElement) {
super();
this._element = document.createElement('div');
this._element.className = 'dv-scrollable';
this._horizontalScrollbar = document.createElement('div');
this._horizontalScrollbar.className = 'dv-scrollbar-horizontal';
this.element.appendChild(scrollableElement);
this.element.appendChild(this._horizontalScrollbar);
this.addDisposables(
addDisposableListener(this.element, 'wheel', (event) => {
this._scrollLeft += event.deltaY * Scrollbar.MouseWheelSpeed;
this.calculateScrollbarStyles();
}),
addDisposableListener(
this._horizontalScrollbar,
'pointerdown',
(event) => {
event.preventDefault();
toggleClass(this.element, 'dv-scrollable-scrolling', true);
const originalClientX = event.clientX;
const originalScrollLeft = this._scrollLeft;
const onPointerMove = (event: PointerEvent) => {
const deltaX = event.clientX - originalClientX;
const { clientWidth } = this.element;
const { scrollWidth } = this.scrollableElement;
const p = clientWidth / scrollWidth;
this._scrollLeft = originalScrollLeft + deltaX / p;
this.calculateScrollbarStyles();
};
const onEnd = () => {
toggleClass(
this.element,
'dv-scrollable-scrolling',
false
);
document.removeEventListener(
'pointermove',
onPointerMove
);
document.removeEventListener('pointerup', onEnd);
document.removeEventListener('pointercancel', onEnd);
};
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onEnd);
document.addEventListener('pointercancel', onEnd);
}
),
addDisposableListener(this.element, 'scroll', () => {
this.calculateScrollbarStyles();
}),
addDisposableListener(this.scrollableElement, 'scroll', () => {
this._scrollLeft = this.scrollableElement.scrollLeft;
this.calculateScrollbarStyles();
}),
watchElementResize(this.element, () => {
toggleClass(this.element, 'dv-scrollable-resizing', true);
if (this._animationTimer) {
clearTimeout(this._animationTimer);
}
this._animationTimer = setTimeout(() => {
clearTimeout(this._animationTimer);
toggleClass(this.element, 'dv-scrollable-resizing', false);
}, 500);
this.calculateScrollbarStyles();
})
);
}
private calculateScrollbarStyles(): void {
const { clientWidth } = this.element;
const { scrollWidth } = this.scrollableElement;
const hasScrollbar = scrollWidth > clientWidth;
if (hasScrollbar) {
const px = clientWidth * (clientWidth / scrollWidth);
this._horizontalScrollbar.style.width = `${px}px`;
this._scrollLeft = clamp(
this._scrollLeft,
0,
this.scrollableElement.scrollWidth - clientWidth
);
this.scrollableElement.scrollLeft = this._scrollLeft;
const percentageComplete =
this._scrollLeft / (scrollWidth - clientWidth);
this._horizontalScrollbar.style.left = `${
(clientWidth - px) * percentageComplete
}px`;
} else {
this._horizontalScrollbar.style.width = `0px`;
this._horizontalScrollbar.style.left = `0px`;
this._scrollLeft = 0;
}
}
}

View File

@ -116,6 +116,7 @@
-moz-user-select: none; // Firefox
-ms-user-select: none; // IE 10 and IE 11
touch-action: none;
background-color: var(--dv-sash-color, transparent);
&:not(.disabled):active,
&:not(.disabled):hover {

View File

@ -219,6 +219,8 @@ export class Splitview {
set margin(value: number) {
this._margin = value;
toggleClass(this.element, 'dv-splitview-has-margin', value !== 0);
}
constructor(

View File

@ -122,6 +122,10 @@ export class SplitviewComponent
}
set splitview(value: Splitview) {
if (this._splitview) {
this._splitview.dispose();
}
this._splitview = value;
this._splitviewChangeDisposable.value = new CompositeDisposable(
@ -425,6 +429,8 @@ export class SplitviewComponent
view.dispose();
}
this.element.remove();
super.dispose();
}
}

View File

@ -1,19 +1,31 @@
@import 'theme/_sash-handle-mixin';
@import 'theme/_drop-target-static-mixin';
@import 'theme/_space-mixin';
@mixin dockview-theme-core-mixin {
--dv-paneview-active-outline-color: dodgerblue;
--dv-tabs-and-actions-container-font-size: 13px;
--dv-tabs-and-actions-container-height: 35px;
--dv-drag-over-background-color: rgba(83, 89, 93, 0.5);
--dv-drag-over-border-color: white;
--dv-drag-over-border-color: transparent;
--dv-tabs-container-scrollbar-color: #888;
--dv-icon-hover-background-color: rgba(90, 93, 94, 0.31);
--dv-floating-box-shadow: 8px 8px 8px 0px rgba(83, 89, 93, 0.5);
--dv-overlay-z-index: 999;
//
--dv-tab-font-size: inherit;
--dv-border-radius: 0px;
--dv-tab-margin: 0;
--dv-sash-color: transparent;
--dv-active-sash-color: transparent;
--dv-active-sash-transition-duration: 0.1s;
--dv-active-sash-transition-delay: 0.5s;
}
@mixin dockview-theme-dark-mixin {
@include dockview-theme-core-mixin();
@include dockview-drop-target-no-travel();
//
--dv-group-view-background-color: #1e1e1e;
@ -37,6 +49,8 @@
@mixin dockview-theme-light-mixin {
@include dockview-theme-core-mixin();
@include dockview-drop-target-no-travel();
//
--dv-group-view-background-color: white;
//
@ -55,6 +69,8 @@
//
--dv-separator-border: rgba(128, 128, 128, 0.35);
--dv-paneview-header-border-color: rgb(51, 51, 51);
--dv-scrollbar-background-color: rgba(0, 0, 0, 0.25);
}
.dockview-theme-dark {
@ -133,30 +149,49 @@
@mixin dockview-theme-abyss-mixin {
@include dockview-theme-core-mixin();
@include dockview-drop-target-no-travel();
--dv-color-abyss-dark: #000c18;
--dv-color-abyss: #10192c;
--dv-color-abyss-light: #1c1c2a;
--dv-color-abyss-lighter: #2b2b4a;
--dv-color-abyss-accent: rgb(91, 30, 207);
--dv-color-abyss-primary-text: white;
--dv-color-abyss-secondary-text: rgb(148, 151, 169);
//
--dv-group-view-background-color: #000c18;
--dv-group-view-background-color: var(--dv-color-abyss-dark);
//
--dv-tabs-and-actions-container-background-color: #1c1c2a;
--dv-tabs-and-actions-container-background-color: var(
--dv-color-abyss-light
);
//
--dv-activegroup-visiblepanel-tab-background-color: #000c18;
--dv-activegroup-hiddenpanel-tab-background-color: #10192c;
--dv-inactivegroup-visiblepanel-tab-background-color: #000c18;
--dv-inactivegroup-hiddenpanel-tab-background-color: #10192c;
--dv-tab-divider-color: #2b2b4a;
--dv-activegroup-visiblepanel-tab-background-color: var(
--dv-color-abyss-dark
);
--dv-activegroup-hiddenpanel-tab-background-color: var(--dv-color-abyss);
--dv-inactivegroup-visiblepanel-tab-background-color: var(
--dv-color-abyss-dark
);
--dv-inactivegroup-hiddenpanel-tab-background-color: var(--dv-color-abyss);
--dv-tab-divider-color: var(--dv-color-abyss-lighter);
//
--dv-activegroup-visiblepanel-tab-color: white;
--dv-activegroup-hiddenpanel-tab-color: rgba(255, 255, 255, 0.5);
--dv-inactivegroup-visiblepanel-tab-color: rgba(255, 255, 255, 0.5);
--dv-inactivegroup-hiddenpanel-tab-color: rgba(255, 255, 255, 0.25);
//
--dv-separator-border: #2b2b4a;
--dv-paneview-header-border-color: #2b2b4a;
--dv-separator-border: var(--dv-color-abyss-lighter);
--dv-paneview-header-border-color: var(--dv-color-abyss-lighter);
--dv-paneview-active-outline-color: #596f99;
}
@mixin dockview-theme-dracula-mixin {
@include dockview-theme-core-mixin();
@include dockview-drop-target-no-travel();
//
--dv-group-view-background-color: #282a36;
//
@ -181,7 +216,7 @@
.dv-groupview {
&.dv-active-group {
> .dv-tabs-and-actions-container {
> .dv-tabs-container {
.dv-tabs-container {
> .dv-tab.dv-active-tab {
position: relative;
@ -201,7 +236,7 @@
}
&.dv-inactive-group {
> .dv-tabs-and-actions-container {
> .dv-tabs-container {
.dv-tabs-container {
> .dv-tab.dv-active-tab {
position: relative;
@ -231,10 +266,17 @@
}
@mixin dockview-design-replit-mixin {
@include dockview-drop-target-no-travel();
.dv-resize-container:has(> .dv-groupview) {
border-radius: 8px;
}
.dv-resize-container {
border-radius: 10px !important;
border: none;
}
.dv-groupview {
overflow: hidden;
border-radius: 10px;
@ -268,59 +310,16 @@
border: 1px solid transparent;
}
}
.dv-vertical > .dv-sash-container > .dv-sash {
&:not(.disabled) {
&::after {
content: '';
height: 4px;
width: 40px;
border-radius: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--dv-separator-handle-background-color);
position: absolute;
}
&:hover {
&::after {
background-color: var(
--dv-separator-handle-hover-background-color
);
}
}
}
}
.dv-horizontal > .dv-sash-container > .dv-sash {
&:not(.disabled) {
&::after {
content: '';
height: 40px;
width: 4px;
border-radius: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--dv-separator-handle-background-color);
position: absolute;
}
&:hover {
&::after {
background-color: var(
--dv-separator-handle-hover-background-color
);
}
}
}
}
}
.dockview-theme-replit {
@include dockview-theme-core-mixin();
@include dockview-design-replit-mixin();
@include dockview-design-handle-mixin();
padding: 10px;
background-color: #ebeced;
//
--dv-group-view-background-color: #ebeced;
//
@ -341,6 +340,117 @@
--dv-paneview-header-border-color: rgb(51, 51, 51);
/////
--dv-separator-handle-background-color: #cfd1d3;
--dv-separator-handle-hover-background-color: #babbbb;
--dv-sash-color: #cfd1d3;
--dv-active-sash-color: #babbbb;
}
.dockview-theme-abyss-spaced {
@include dockview-theme-core-mixin();
@include dockview-design-space-mixin();
// stylesheet
--dv-color-abyss-dark: rgb(11, 6, 17);
--dv-color-abyss: #16121f;
--dv-color-abyss-light: #201d2b;
--dv-color-abyss-lighter: #2a2837;
--dv-color-abyss-accent: rgb(91, 30, 207);
--dv-color-abyss-primary-text: white;
--dv-color-abyss-secondary-text: rgb(148, 151, 169);
//
--dv-drag-over-border: 2px solid var(--dv-color-abyss-accent);
--dv-drag-over-background-color: '';
//
//
--dv-group-view-background-color: var(--dv-color-abyss-dark);
//
--dv-tabs-and-actions-container-background-color: var(--dv-color-abyss);
//
--dv-activegroup-visiblepanel-tab-background-color: var(
--dv-color-abyss-lighter
);
--dv-activegroup-hiddenpanel-tab-background-color: var(
--dv-color-abyss-light
);
--dv-inactivegroup-visiblepanel-tab-background-color: var(
--dv-color-abyss-lighter
);
--dv-inactivegroup-hiddenpanel-tab-background-color: var(
--dv-color-abyss-light
);
--dv-tab-divider-color: transparent;
//
--dv-activegroup-visiblepanel-tab-color: var(--dv-color-abyss-primary-text);
--dv-activegroup-hiddenpanel-tab-color: var(
--dv-color-abyss-secondary-text
);
--dv-inactivegroup-visiblepanel-tab-color: var(
--dv-color-abyss-primary-text
);
--dv-inactivegroup-hiddenpanel-tab-color: var(
--dv-color-abyss-secondary-text
);
//
--dv-separator-border: transparent;
--dv-paneview-header-border-color: rgb(51, 51, 51);
/////
--dv-active-sash-color: var(--dv-color-abyss-accent);
//
--dv-floating-box-shadow: 8px 8px 8px 0px rgba(0, 0, 0, 0.5);
padding: 10px;
background-color: var(--dv-color-abyss-dark);
.dv-resize-container {
.dv-groupview {
border: 2px solid var(--dv-color-abyss-dark);
}
}
}
.dockview-theme-light-spaced {
@include dockview-theme-core-mixin();
@include dockview-design-space-mixin();
//
--dv-drag-over-border: 2px solid rgb(91, 30, 207);
--dv-drag-over-background-color: '';
//
//
--dv-group-view-background-color: #f6f5f9;
//
--dv-tabs-and-actions-container-background-color: white;
//
--dv-activegroup-visiblepanel-tab-background-color: #ededf0;
--dv-activegroup-hiddenpanel-tab-background-color: #f9f9fa;
--dv-inactivegroup-visiblepanel-tab-background-color: #ededf0;
--dv-inactivegroup-hiddenpanel-tab-background-color: #f9f9fa;
--dv-tab-divider-color: transparent;
//
--dv-activegroup-visiblepanel-tab-color: rgb(104, 107, 130);
--dv-activegroup-hiddenpanel-tab-color: rgb(148, 151, 169);
--dv-inactivegroup-visiblepanel-tab-color: rgb(104, 107, 130);
--dv-inactivegroup-hiddenpanel-tab-color: rgb(148, 151, 169);
//
--dv-separator-border: transparent;
--dv-paneview-header-border-color: rgb(51, 51, 51);
/////
--dv-active-sash-color: rgb(91, 30, 207);
//
--dv-floating-box-shadow: 8px 8px 8px 0px rgba(0, 0, 0, 0.1);
padding: 10px;
background-color: #f6f5f9;
--dv-scrollbar-background-color: rgba(0, 0, 0, 0.25);
.dv-resize-container {
.dv-groupview {
border: 2px solid rgb(255, 255, 255, 0.1);
}
}
}

View File

@ -0,0 +1,10 @@
@mixin dockview-drop-target-no-travel {
.dv-drop-target-container {
.dv-drop-target-anchor {
&.dv-drop-target-anchor-container-changed {
opacity: 0;
transition: none;
}
}
}
}

View File

@ -0,0 +1,53 @@
@mixin dockview-design-handle-mixin {
.dv-vertical > .dv-sash-container > .dv-sash {
background-color: transparent;
&:not(.disabled) {
&::after {
content: '';
height: 4px;
width: 40px;
border-radius: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--dv-sash-color);
position: absolute;
}
&:hover,
&:active {
background-color: transparent;
&::after {
background-color: var(--dv-active-sash-color);
}
}
}
}
.dv-horizontal > .dv-sash-container > .dv-sash {
background-color: transparent;
&:not(.disabled) {
&::after {
content: '';
height: 40px;
width: 4px;
border-radius: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--dv-sash-color);
position: absolute;
}
&:hover,
&:active {
background-color: transparent;
&::after {
background-color: var(--dv-active-sash-color);
}
}
}
}
}

View File

@ -0,0 +1,57 @@
@mixin dockview-design-space-mixin {
--dv-tab-font-size: 12px;
--dv-border-radius: 20px;
--dv-tab-margin: 0.5rem 0.25rem;
--dv-tabs-and-actions-container-height: 44px;
--dv-border-radius: 20px;
.dv-resize-container:has(> .dv-groupview) {
border-radius: 8px;
}
.dv-sash {
border-radius: 4px;
}
.dv-drop-target-anchor {
border-radius: calc(var(--dv-border-radius) / 4);
&.dv-drop-target-content {
border-radius: var(--dv-border-radius);
}
}
.dv-resize-container {
border-radius: var(--dv-border-radius) !important;
border: none;
}
.dv-tabs-overflow-container,
.dv-tabs-overflow-dropdown-default {
border-radius: 8px;
height: unset !important;
}
.dv-tab {
border-radius: 8px;
.dv-svg {
height: 8px;
width: 8px;
}
}
.dv-groupview {
border-radius: var(--dv-border-radius);
.dv-tabs-and-actions-container {
padding: 0px calc(var(--dv-border-radius) / 2);
}
.dv-content-container {
background-color: var(
--dv-tabs-and-actions-container-background-color
);
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "dockview-react",
"version": "3.2.0",
"version": "4.2.2",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"keywords": [
"splitview",
@ -54,6 +54,6 @@
"test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview-react --coverage"
},
"dependencies": {
"dockview": "^3.2.0"
"dockview": "^4.2.2"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "dockview-vue",
"version": "3.2.0",
"version": "4.2.1",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"keywords": [
"splitview",
@ -52,7 +52,7 @@
"test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview-vue --coverage"
},
"dependencies": {
"dockview-core": "^3.2.0"
"dockview-core": "^4.2.1"
},
"peerDependencies": {
"vue": "^3.4.0"

View File

@ -2,7 +2,6 @@ import type {
DockviewApi,
DockviewGroupPanel,
DockviewPanelApi,
GroupPanelPartInitParameters,
IContentRenderer,
IDockviewPanelHeaderProps,
IGroupHeaderProps,
@ -12,6 +11,7 @@ import type {
IWatermarkRenderer,
PanelUpdateEvent,
Parameters,
TabPartInitParameters,
WatermarkRendererInitParameters,
} from 'dockview-core';
import {
@ -121,7 +121,7 @@ export class VueRenderer
private _api: DockviewPanelApi | undefined;
private _containerApi: DockviewApi | undefined;
init(parameters: GroupPanelPartInitParameters): void {
init(parameters: TabPartInitParameters): void {
this._api = parameters.api;
this._containerApi = parameters.containerApi;
@ -129,6 +129,7 @@ export class VueRenderer
params: parameters.params,
api: parameters.api,
containerApi: parameters.containerApi,
tabLocation: parameters.tabLocation,
};
this._renderDisposable?.dispose();

View File

@ -1,6 +1,6 @@
{
"name": "dockview",
"version": "3.2.0",
"version": "4.2.2",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"keywords": [
"splitview",
@ -54,7 +54,7 @@
"test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview --coverage"
},
"dependencies": {
"dockview-core": "^3.2.0"
"dockview-core": "^4.2.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"

View File

@ -19,6 +19,7 @@ describe('defaultTab', () => {
render(
<DockviewDefaultTab
tabLocation="header"
api={api}
containerApi={containerApi}
params={params}
@ -41,6 +42,7 @@ describe('defaultTab', () => {
render(
<DockviewDefaultTab
tabLocation="header"
api={api}
containerApi={containerApi}
params={params}
@ -65,6 +67,7 @@ describe('defaultTab', () => {
render(
<DockviewDefaultTab
tabLocation="header"
api={api}
containerApi={containerApi}
params={params}
@ -97,6 +100,7 @@ describe('defaultTab', () => {
render(
<DockviewDefaultTab
tabLocation="header"
api={api}
containerApi={containerApi}
params={params}
@ -122,6 +126,7 @@ describe('defaultTab', () => {
render(
<DockviewDefaultTab
tabLocation="header"
api={api}
containerApi={containerApi}
params={params}
@ -151,6 +156,7 @@ describe('defaultTab', () => {
render(
<DockviewDefaultTab
tabLocation="header"
api={api}
containerApi={containerApi}
params={params}
@ -177,6 +183,7 @@ describe('defaultTab', () => {
render(
<DockviewDefaultTab
tabLocation="header"
api={api}
containerApi={containerApi}
params={params}

View File

@ -35,6 +35,7 @@ export const DockviewDefaultTab: React.FunctionComponent<
onPointerDown,
onPointerUp,
onPointerLeave,
tabLocation,
...rest
}) => {
const title = useTitle(api);
@ -96,7 +97,7 @@ export const DockviewDefaultTab: React.FunctionComponent<
className="dv-default-tab"
>
<span className="dv-default-tab-content">{title}</span>
{!hideClose && (
{!hideClose && tabLocation !== 'headerOverflow' && (
<div
className="dv-default-tab-action"
onPointerDown={onBtnPointerDown}

View File

@ -175,6 +175,7 @@ export const DockviewReact = React.forwardRef(
dockviewRef.current = api;
return () => {
dockviewRef.current = undefined;
api.dispose();
};
}, []);

View File

@ -3,13 +3,13 @@ import { ReactPart, ReactPortalStore } from '../react';
import {
PanelUpdateEvent,
ITabRenderer,
GroupPanelPartInitParameters,
IGroupPanelBaseProps,
TabPartInitParameters,
IDockviewPanelHeaderProps,
} from 'dockview-core';
export class ReactPanelHeaderPart implements ITabRenderer {
private readonly _element: HTMLElement;
private part?: ReactPart<IGroupPanelBaseProps>;
private part?: ReactPart<IDockviewPanelHeaderProps>;
get element(): HTMLElement {
return this._element;
@ -17,7 +17,7 @@ export class ReactPanelHeaderPart implements ITabRenderer {
constructor(
public readonly id: string,
private readonly component: React.FunctionComponent<IGroupPanelBaseProps>,
private readonly component: React.FunctionComponent<IDockviewPanelHeaderProps>,
private readonly reactPortalStore: ReactPortalStore
) {
this._element = document.createElement('div');
@ -30,7 +30,7 @@ export class ReactPanelHeaderPart implements ITabRenderer {
//noop
}
public init(parameters: GroupPanelPartInitParameters): void {
public init(parameters: TabPartInitParameters): void {
this.part = new ReactPart(
this.element,
this.reactPortalStore,
@ -39,6 +39,7 @@ export class ReactPanelHeaderPart implements ITabRenderer {
params: parameters.params,
api: parameters.api,
containerApi: parameters.containerApi,
tabLocation: parameters.tabLocation,
}
);
}

View File

@ -105,6 +105,7 @@ export const GridviewReact = React.forwardRef(
gridviewRef.current = api;
return () => {
gridviewRef.current = undefined;
api.dispose();
};
}, []);

View File

@ -120,6 +120,7 @@ export const PaneviewReact = React.forwardRef(
paneviewRef.current = api;
return () => {
paneviewRef.current = undefined;
api.dispose();
};
}, []);

View File

@ -105,6 +105,7 @@ export const SplitviewReact = React.forwardRef(
splitviewRef.current = api;
return () => {
splitviewRef.current = undefined;
api.dispose();
};
}, []);

View File

@ -7,7 +7,7 @@ export const CloseButton = () => (
viewBox="0 0 28 28"
aria-hidden={'false'}
focusable={false}
className="dockview-svg"
className="dv-svg"
>
<path d="M2.1 27.3L0 25.2L11.55 13.65L0 2.1L2.1 0L13.65 11.55L25.2 0L27.3 2.1L15.75 13.65L27.3 25.2L25.2 27.3L13.65 15.75L2.1 27.3Z"></path>
</svg>
@ -21,7 +21,7 @@ export const ExpandMore = () => {
viewBox="0 0 24 15"
aria-hidden={'false'}
focusable={false}
className="dockview-svg"
className="dv-svg"
>
<path d="M12 14.15L0 2.15L2.15 0L12 9.9L21.85 0.0499992L24 2.2L12 14.15Z" />
</svg>

View File

@ -0,0 +1,24 @@
---
slug: dockview-4.0.0-release
title: Dockview 4.0.0
tags: [release]
---
# Release Notes
Please reference docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
- To control the theme of dockview you should no longer pass a `dv-theme-*` class, instead directly use the `theme` property. See [Themes](https://dockview.dev/demo) for more details. [#850](https://github.com/mathuo/dockview/pull/850)
- Introduces a new dnd overlay model with improved animations and customization options allowing themes to take more fine grained control over the dnd overlay styles. [#850](https://github.com/mathuo/dockview/pull/850)
- Custom scrollbar on tab headers for better UX [#822](https://github.com/mathuo/dockview/pull/822)
- When tabs are hidden within scrollbar hidden tabs can be selected from a dropdown that appears in header [#822](https://github.com/mathuo/dockview/pull/822)
## 🛠 Miscs
- Bug: Remove elements from DOM after disposable for Splitview and Paneview components [#870](https://github.com/mathuo/dockview/pull/870)
## 🔥 Breaking changes
- `setGap(gap: number | undefined): void` and `gap(): number` have been removed. The gap property is now controlled directly within the chosen Theme. See [Themes](https://dockview.dev) for more details.
- `DockviewDefaultTab` requires a `tabLocation` field, to mimic existing behaviour use `tabLocation="header"`.

View File

@ -0,0 +1,19 @@
---
slug: dockview-4.0.1-release
title: Dockview 4.0.1
tags: [release]
---
# Release Notes
Please reference docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
## 🛠 Miscs
- Bug: Fix full-width tab CSS [#880](https://github.com/mathuo/dockview/pull/880)
- Bug: Fix tab divider CSS [#879](https://github.com/mathuo/dockview/pull/879)
## 🔥 Breaking changes

View File

@ -0,0 +1,23 @@
---
slug: dockview-4.1.0-release
title: Dockview 4.1.0
tags: [release]
---
# Release Notes
Please reference docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
- Custom header size for Paneview components [#875](https://github.com/mathuo/dockview/pull/875)
- Add events `onDidPopoutGroupSizeChange` and `onDidPopoutGroupPositionChange` [#876](https://github.com/mathuo/dockview/pull/876)
## 🛠 Miscs
- Bug: Prevent ghost DOM elements after `fromJSON` calls on Splitview components [#881](https://github.com/mathuo/dockview/pull/881)
- Bug: Disable point-events for iframes within shadow DOMs during tab dnd events [#791](https://github.com/mathuo/dockview/pull/791)
- Bug: Custom group id when no reference panel nor group is provided [#882](https://github.com/mathuo/dockview/pull/882)
## 🔥 Breaking changes

View File

@ -0,0 +1,20 @@
---
slug: dockview-4.2.0-release
title: Dockview 4.2.0
tags: [release]
---
# Release Notes
Please reference docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
- `scrollbars` options [#885](https://github.com/mathuo/dockview/pull/885)
## 🛠 Miscs
- Bug: Fix group dnd [#885](https://github.com/mathuo/dockview/pull/885)
## 🔥 Breaking changes

View File

@ -0,0 +1,20 @@
---
slug: dockview-4.2.1-release
title: Dockview 4.2.1
tags: [release]
---
# Release Notes
Please reference docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
## 🛠 Miscs
- Bug: Fix styles and dnd issues [#887](https://github.com/mathuo/dockview/pull/887)
- Bug: Fix options init issues [#888](https://github.com/mathuo/dockview/pull/888)
## 🔥 Breaking changes

View File

@ -0,0 +1,19 @@
---
slug: dockview-4.2.2-release
title: Dockview 4.2.2
tags: [release]
---
# Release Notes
Please reference docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
## 🛠 Miscs
- Bug: React disposable issues [#892](https://github.com/mathuo/dockview/pull/892)
## 🔥 Breaking changes

View File

@ -24,12 +24,12 @@ The dock makes heavy use of drag and drop functionalities.
# Drag And Drop
You can override the conditions of the far edge overlays through the `rootOverlayModel` prop.
You can override the conditions of the far edge overlays through the `dndEdges` prop.
```tsx
<DockviewReact
{...props}
rootOverlayModel={{
dndEdges={{
size: { value: 100, type: 'pixels' },
activationSize: { value: 5, type: 'percentage' },
}}

View File

@ -13,6 +13,8 @@ Panels can be added through the dock api.
<DocRef declaration="DockviewApi" methods={['addPanel']} />
## Opening a Basic Panel
To open a panel requires a unique `id` and the name of the `component` to render.
@ -95,6 +97,8 @@ See [Panel Rendering](/docs/core/panels/rendering).
You can position a panel relative to an existing panel, group using `direction`. If you do not provide a reference panel
or group then the panel will be positioned to the edge of the dock in the specified direction.
<DocRef declaration="Direction" />
#### Relative to another Panel
```ts

View File

@ -6,8 +6,10 @@ title: Theme
import { CSSVariablesTable, ThemeTable } from '@site/src/components/cssVariables';
import { DocRef } from '@site/src/components/ui/reference/docRef';
Theming is controlled through CSS and is highly customizable.
Dockview components accept a `theme` property which is highly customizable, the theme is largly controlled through CSS however some properties can only be adjusted
by direct editing variables of the `theme` object.
Firstly, you should import `dockview.css`:
@ -32,15 +34,34 @@ Firstly, you should import `dockview.css`:
## Provided themes
`dockview` comes with a number of themes which are all CSS classes and can be found [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss).
To use a `dockview` theme the CSS must encapsulate the component. The current list of themes is:
`dockview` comes with a number of built-in themes. Each theme is represented as an object that can be imported.
For dock components you should pass the theme object to the `theme` property, for other components such as split, pane and grid views you should
use set the themes associated CSS class to the `className` property.
```tsx
import { themeAbyss } from "dockview";
// For dock components
theme={themeAbyss}
// For other components
const {className} = themeAbyss;
```
<ThemeTable/>
:::info
The source code for all themes can be found [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss).
The source code for all themes can be found [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss) and the associated CSS [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss).
:::
## Build your own theme
You can define your own `DockviewTheme` object and pass it to the `theme` property.
<DocRef declaration="DockviewTheme" />
## Customizing Theme
The provided themes are controlled primarily through a long list of CSS variables which can be modified by the user either entirely for a new theme

View File

@ -1,6 +1,6 @@
{
"name": "dockview-docs",
"version": "3.2.0",
"version": "4.2.2",
"private": true,
"scripts": {
"build": "npm run build-templates && docusaurus build",
@ -38,7 +38,7 @@
"ag-grid-react": "^31.0.2",
"axios": "^1.6.3",
"clsx": "^2.1.0",
"dockview": "^3.2.0",
"dockview": "^4.2.2",
"prism-react-renderer": "^2.3.1",
"react-dnd": "^16.0.1",
"react-laag": "^2.0.5",

View File

@ -11,6 +11,7 @@
&:hover {
border-radius: 2px;
color: var(--dv-activegroup-visiblepanel-tab-color);
background-color: var(--dv-icon-hover-background-color);
}
}

View File

@ -5,8 +5,10 @@ import {
IDockviewPanelHeaderProps,
IDockviewPanelProps,
DockviewApi,
DockviewTheme,
} from 'dockview';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import './app.scss';
import { defaultConfig } from './defaultLayout';
import { GridActions } from './gridActions';
@ -30,6 +32,20 @@ const Option = (props: {
);
};
const ShadowIframe = (props: IDockviewPanelProps) => {
return (
<iframe
onMouseDown={() => {
if (!props.api.isActive) {
props.api.setActive();
}
}}
style={{ border: 'none', width: '100%', height: '100%' }}
src="https://dockview.dev"
/>
);
};
const components = {
default: (props: IDockviewPanelProps) => {
const isDebug = React.useContext(DebugContext);
@ -80,6 +96,7 @@ const components = {
);
},
nested: (props: IDockviewPanelProps) => {
const theme = React.useContext(ThemeContext);
return (
<DockviewReact
components={components}
@ -95,7 +112,7 @@ const components = {
console.log('remove', e);
});
}}
className={'dockview-theme-abyss'}
theme={theme}
/>
);
},
@ -108,6 +125,7 @@ const components = {
}
}}
style={{
border: 'none',
width: '100%',
height: '100%',
}}
@ -115,6 +133,33 @@ const components = {
/>
);
},
shadowDom: (props: IDockviewPanelProps) => {
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!ref.current) {
return;
}
const shadow = ref.current.attachShadow({
mode: 'open',
});
const shadowRoot = document.createElement('div');
shadowRoot.style.height = '100%';
shadow.appendChild(shadowRoot);
const root = ReactDOM.createRoot(shadowRoot);
root.render(<ShadowIframe {...props} />);
return () => {
root.unmount();
};
}, []);
return <div style={{ height: '100%' }} ref={ref}></div>;
},
};
const headerComponents = {
@ -141,7 +186,9 @@ const WatermarkComponent = () => {
return <div>custom watermark</div>;
};
const DockviewDemo = (props: { theme?: string }) => {
const ThemeContext = React.createContext<DockviewTheme | undefined>(undefined);
const DockviewDemo = (props: { theme?: DockviewTheme }) => {
const [logLines, setLogLines] = React.useState<
{ text: string; timestamp?: Date; backgroundColor?: string }[]
>([]);
@ -380,18 +427,22 @@ const DockviewDemo = (props: { theme?: string }) => {
}}
>
<DebugContext.Provider value={debug}>
<DockviewReact
components={components}
defaultTabComponent={headerComponents.default}
rightHeaderActionsComponent={RightControls}
leftHeaderActionsComponent={LeftControls}
prefixHeaderActionsComponent={PrefixHeaderControls}
watermarkComponent={
watermark ? WatermarkComponent : undefined
}
onReady={onReady}
className={props.theme || 'dockview-theme-abyss'}
/>
<ThemeContext.Provider value={props.theme}>
<DockviewReact
components={components}
defaultTabComponent={headerComponents.default}
rightHeaderActionsComponent={RightControls}
leftHeaderActionsComponent={LeftControls}
prefixHeaderActionsComponent={
PrefixHeaderControls
}
watermarkComponent={
watermark ? WatermarkComponent : undefined
}
onReady={onReady}
theme={props.theme}
/>
</ThemeContext.Provider>
</DebugContext.Provider>
</div>

View File

@ -81,7 +81,7 @@ export const RightControls = (props: IDockviewHeaderActionsProps) => {
alignItems: 'center',
padding: '0px 8px',
height: '100%',
color: 'var(--dv-activegroup-visiblepanel-tab-color)',
color: 'var(--dv-activegroup-hiddenpanel-tab-color)',
}}
>
{props.isGroupActive && <Icon icon="star" />}

View File

@ -2,7 +2,7 @@ import { DockviewApi } from 'dockview';
import * as React from 'react';
import { defaultConfig, nextId } from './defaultLayout';
import { createRoot, Root } from 'react-dom/client';
import { createRoot } from 'react-dom/client';
import { PanelBuilder } from './panelBuilder';
let mount = document.querySelector('.popover-anchor') as HTMLElement | null;
@ -151,12 +151,6 @@ export const GridActions = (props: {
props.api?.addGroup();
};
const [gap, setGap] = React.useState(0);
React.useEffect(() => {
props.api?.setGap(gap);
}, [gap, props.api]);
return (
<div className="action-container">
<div className="button-group">
@ -204,19 +198,6 @@ export const GridActions = (props: {
Reset
</button>
<span style={{ flexGrow: 1 }} />
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ paddingRight: '4px' }}>Grid Gap</span>
<input
style={{ width: 40 }}
type="number"
min={0}
max={99}
step={1}
value={gap}
onChange={(event) => setGap(Number(event.target.value))}
/>
<button onClick={() => setGap(0)}>Reset</button>
</div>
</div>
);
};

View File

@ -180,7 +180,7 @@ const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => {
onReady={onReady}
className={`${props.theme || 'dockview-theme-abyss'}`}
onDidDrop={onDidDrop}
rootOverlayModel={{
dndEdges={{
size: { value: 100, type: 'pixels' },
activationSize: { value: 5, type: 'percentage' },
}}

View File

@ -1,13 +1,16 @@
.DropdownMenuContent {
/* min-width: 220px; */
background-color: rgba(255, 255, 255, 0.1);
background-color: var(--ifm-dropdown-background-color);
color: var(--ifm-color-primary);
border: var(--ifm-dropdown-border);
border-radius: 6px;
padding: 5px;
box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2);
animation-duration: 400ms;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
will-change: transform, opacity;
z-index: 99;
}
.DropdownMenuContent[data-side='top'],
.DropdownMenuSubContent[data-side='top'] {
@ -39,25 +42,33 @@
display: flex;
align-items: center;
justify-content: space-between;
width: 100px;
height: 25px;
width: 120px;
/* height: 25px; */
padding: 4px 8px;
font-size: 13px;
font-size: 1rem;
font-weight: normal;
cursor: pointer;
color: var(--ifm-menu-color);
&:hover {
background-color: var(--ifm-hover-overlay);
}
}
.framework-menu-item-select {
display: flex;
align-items: center;
justify-content: space-between;
width: 120px;
width: 130px;
height: 35px;
padding: 4px 8px;
border-radius: 6px;
font-size: 13px;
background-color: rgba(255, 255, 255, 0.1);
font-size: 1rem;
font-weight: normal;
cursor: pointer;
border: 1px solid rgba(0,0,0, 0.1);
border: 1px solid rgba(60, 60, 66,0.5);
}
@keyframes slideUpAndFade {

View File

@ -1,6 +1,8 @@
import BrowserOnly from '@docusaurus/BrowserOnly';
import { DockviewEmitter } from 'dockview';
import * as React from 'react';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import useBaseUrl from '@docusaurus/useBaseUrl';
import './frameworkSpecific.css';
export interface FrameworkDescriptor {
@ -51,8 +53,7 @@ export function useActiveFramework(): [
return [option, setter];
}
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import useBaseUrl from '@docusaurus/useBaseUrl';
const FrameworkSelector1 = () => {
const [activeFramework, setActiveFramework] = useActiveFramework();

View File

@ -28,7 +28,7 @@
}
}
.dockview-svg {
.dv-svg {
display: inline-block;
fill: currentcolor;
line-height: 1;

View File

@ -17,7 +17,7 @@ const createSvgElementFromPath = (params: {
width={params.width}
viewBox={params.viewbox}
focusable={false}
className={'dockview-svg'}
className={'dv-svg'}
>
<path d={params.path} />
</svg>
@ -54,7 +54,7 @@ export const CodeSandboxButton = (props: {
<a
href={url}
target={'_blank'}
rel='noopener'
rel="noopener"
className="codesandbox-button-content"
>
<span

View File

@ -57,7 +57,7 @@ export const Container = (props: {
const ReactIcon = (props: { height: number; width: number }) => {
return (
<img
// className="dockview-svg"
// className="dv-svg"
style={{ marginRight: '0px 4px' }}
height={props.height}
width={props.width}
@ -69,7 +69,7 @@ const ReactIcon = (props: { height: number; width: number }) => {
const JavascriptIcon = (props: { height: number; width: number }) => {
return (
<img
// className="dockview-svg "
// className="dv-svg "
style={{ marginRight: '0px 4px' }}
height={props.height}
width={props.width}
@ -85,6 +85,7 @@ const themes = [
'dockview-theme-vs',
'dockview-theme-dracula',
'dockview-theme-replit',
'dockview-theme-kraken',
];
function useLocalStorageItem(key: string, defaultValue: string): string {
@ -122,7 +123,6 @@ export const ThemePicker = () => {
return (
<div
style={{
height: '20px',
display: 'flex',
alignItems: 'center',

View File

@ -1,10 +1,11 @@
import * as React from 'react';
import { CodeSandboxButton } from './codeSandboxButton';
import BrowserOnly from '@docusaurus/BrowserOnly';
import { DockviewTheme } from 'dockview';
const ExampleFrame = (props: {
framework: string;
theme?: string;
theme?: DockviewTheme;
id: string;
height?: string;
}) => {

View File

@ -192,6 +192,8 @@ function filter(docs: TypeSystem.Type, methods: string[]) {
.map((v) => filter(v, methods))
.flat();
}
return [docs];
}
if (docs.kind === 'class' || docs.kind === 'interface') {

View File

@ -1,33 +1,54 @@
import {
themeAbyss,
themeDark,
themeDracula,
themeAbyssSpaced,
themeLightSpaced,
themeLight,
themeReplit,
themeVisualStudio,
} from 'dockview';
export const themeConfig = [
{
id: 'dockview-theme-dark',
key: '**[dockview-theme-dark](/demo?theme=dockview-theme-dark)**',
id: themeDark,
key: '**[Dark](/demo?theme=dark)**',
text: '',
},
{
id: 'dockview-theme-light',
key: '**[dockview-theme-light](/demo?theme=dockview-theme-light)**',
id: themeLight,
key: '**[Light](/demo?theme=light)**',
text: '',
},
{
id: 'dockview-theme-vs',
key: '**[dockview-theme-vs](/demo?theme=dockview-theme-vs)**',
id: themeVisualStudio,
key: '**[Visual Studio](/demo?theme=visualStudio)**',
text: 'Based on [Visual Studio](https://visualstudio.microsoft.com)',
},
{
id: 'dockview-theme-abyss',
key: '**[dockview-theme-abyss](/demo?theme=dockview-theme-abyss)**',
id: themeAbyss,
key: '**[Abyss](/demo?theme=abyss)**',
text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) abyss theme',
},
{
id: 'dockview-theme-dracula',
key: '**[dockview-theme-dracula](/demo?theme=dockview-theme-dracula)**',
id: themeDracula,
key: '**[Dracula](/demo?theme=dracula)**',
text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) dracula theme',
},
{
id: 'dockview-theme-replit',
key: '**[dockview-theme-replit](/demo?theme=dockview-theme-replit)**',
id: themeReplit,
key: '**[Replit](/demo?theme=replit)**',
text: 'Based on [Replit](https://replit.com)',
},
{
id: themeLightSpaced,
key: '**[Light Spaced](/demo?theme=lightSpaced)**',
text: '',
},
{
id: themeAbyssSpaced,
key: '**[Abyss Spaced](/demo?theme=abyssSpaced)**',
text: '',
},
];

View File

@ -11,10 +11,10 @@
/* You can override the default Infima variables here. */
:root {
--ifm-font-family-base: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans,
sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol,
Noto Color Emoji;
--ifm-font-family-base: 'IBM Plex Sans', ui-sans-serif, system-ui,
-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue,
Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji,
Segoe UI Symbol, Noto Color Emoji;
--ifm-font-weight-bold: 600;
@ -36,6 +36,9 @@
--ifm-color-primary: black;
--ifm-dropdown-background-color: white;
--ifm-dropdown-border: 1px solid var(--ifm-color-primary-darkest);
--ifm-navbar-link-color: white;
--ifm-navbar-link-hover-color: white;
@ -54,15 +57,18 @@
}
/* --ifm-color-primary: #0c111d; */
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: #21af90;
--ifm-color-primary-darker: #1fa588;
--ifm-color-primary-darkest: #1a8870;
--ifm-color-primary-light: #29d5b0;
--ifm-color-primary-lighter: #32d8b4;
--ifm-color-primary-lightest: #4fddbf;
--ifm-color-primary: #98a2b3;
--ifm-color-primary-dark: #828a99;
--ifm-color-primary-darker: #6a707c;
--ifm-color-primary-darkest: #474b53;
--ifm-color-primary-light: #acb7ca;
--ifm-color-primary-lighter: #bcc9df;
--ifm-color-primary-lightest: #d2e1fa;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
--ifm-dropdown-background-color: #373d4b;
--ifm-dropdown-border: 1px solid var(--ifm-color-primary-darkest);
--dv-docs-markdown-text-color: #cdced8;
}

Some files were not shown because too many files have changed in this diff Show More