🔀 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 build
- run: npm run test:cov - run: npm run test:cov
- name: SonarCloud Scan - name: SonarCloud Scan
uses: sonarsource/sonarqube-scan-action@v4.1.0 uses: sonarsource/sonarqube-scan-action@v5
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View File

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

View File

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

View File

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

View File

@ -1,11 +1,12 @@
import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel'; import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { import {
GroupPanelPartInitParameters, TabPartInitParameters,
IContentRenderer, IContentRenderer,
ITabRenderer, ITabRenderer,
} from '../../dockview/types'; } from '../../dockview/types';
import { PanelUpdateEvent } from '../../panel/types'; import { PanelUpdateEvent } from '../../panel/types';
import { TabLocation } from '../../dockview/framework';
export class DockviewPanelModelMock implements IDockviewPanelModel { export class DockviewPanelModelMock implements IDockviewPanelModel {
constructor( 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(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
options: {}, options: {},
onDidOptionsChange: jest.fn(),
}); });
const panelMock = jest.fn<DockviewPanel, []>(() => { const panelMock = jest.fn<DockviewPanel, []>(() => {
@ -51,6 +52,7 @@ describe('groupPanelApi', () => {
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(), onDidActivePanelChange: jest.fn(),
options: {}, options: {},
onDidOptionsChange: jest.fn(),
}); });
const groupViewPanel = new DockviewGroupPanel( const groupViewPanel = new DockviewGroupPanel(
@ -84,6 +86,7 @@ describe('groupPanelApi', () => {
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(), onDidActivePanelChange: jest.fn(),
options: {}, options: {},
onDidOptionsChange: jest.fn(),
}); });
const groupViewPanel = new DockviewGroupPanel( const groupViewPanel = new DockviewGroupPanel(

View File

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

View File

@ -8,6 +8,7 @@ import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel'; import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel';
import { Tab } from '../../../dockview/components/tab/tab'; import { Tab } from '../../../dockview/components/tab/tab';
import { IDockviewPanel } from '../../../dockview/dockviewPanel'; import { IDockviewPanel } from '../../../dockview/dockviewPanel';
import { fromPartial } from '@total-typescript/shoehorn';
describe('tab', () => { describe('tab', () => {
test('that empty tab has inactive-tab class', () => { test('that empty tab has inactive-tab class', () => {
@ -46,15 +47,10 @@ describe('tab', () => {
id: 'testcomponentid', 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>, []>(() => { const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return { return {
@ -72,38 +68,33 @@ describe('tab', () => {
groupPanel groupPanel
); );
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100 () => 100
); );
fireEvent.dragEnter(cut.element); fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element); fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toBeCalled(); expect(groupView.canDisplayOverlay).toHaveBeenCalled();
expect( expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0); ).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>, []>(() => { const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return { return {
id: 'testcomponentid', 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>, []>(() => { const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return { return {
@ -121,10 +112,10 @@ describe('tab', () => {
groupPanel groupPanel
); );
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -136,11 +127,11 @@ describe('tab', () => {
fireEvent.dragEnter(cut.element); fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element); fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toBeCalledTimes(0); expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect( expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length 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', () => { test('that if you drag over another tab a drop target is shown', () => {
@ -175,10 +166,10 @@ describe('tab', () => {
groupPanel groupPanel
); );
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -229,10 +220,10 @@ describe('tab', () => {
groupPanel groupPanel
); );
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -289,10 +280,10 @@ describe('tab', () => {
groupPanel groupPanel
); );
jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation(
() => 100 () => 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(), onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(), onDidActivePanelChange: jest.fn(),
options: {}, options: {},
onDidOptionsChange: jest.fn(),
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>( const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -43,16 +44,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element const emptySpace = cut.element
.getElementsByClassName('dv-void-container') .getElementsByClassName('dv-void-container')
.item(0); .item(0) as HTMLElement;
if (!emptySpace!) { if (!emptySpace!) {
fail('element not found'); fail('element not found');
} }
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -73,17 +74,17 @@ describe('tabsContainer', () => {
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(), onDidActivePanelChange: jest.fn(),
options: {}, options: {},
onDidOptionsChange: jest.fn(),
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>( const dropTargetContainer = document.createElement('div');
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel; const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
// dropTargetContainer: new DropTargetAnchorContainer(
// dropTargetContainer
// ),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => { const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return { return {
@ -99,16 +100,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element const emptySpace = cut.element
.getElementsByClassName('dv-void-container') .getElementsByClassName('dv-void-container')
.item(0); .item(0) as HTMLElement;
if (!emptySpace!) { if (!emptySpace!) {
fail('element not found'); fail('element not found');
} }
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -131,6 +132,10 @@ describe('tabsContainer', () => {
expect( expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1); ).toBe(1);
// expect(
// dropTargetContainer.getElementsByClassName('dv-drop-target-anchor')
// .length
// ).toBe(1);
}); });
test('that dropping over the empty space should render a drop target', () => { test('that dropping over the empty space should render a drop target', () => {
@ -140,6 +145,7 @@ describe('tabsContainer', () => {
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(), onDidActivePanelChange: jest.fn(),
options: {}, options: {},
onDidOptionsChange: jest.fn(),
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>( const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -169,16 +175,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element const emptySpace = cut.element
.getElementsByClassName('dv-void-container') .getElementsByClassName('dv-void-container')
.item(0); .item(0) as HTMLElement;
if (!emptySpace!) { if (!emptySpace!) {
fail('element not found'); fail('element not found');
} }
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -204,6 +210,7 @@ describe('tabsContainer', () => {
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(), onDidActivePanelChange: jest.fn(),
options: {}, options: {},
onDidOptionsChange: jest.fn(),
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>( const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -233,16 +240,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element const emptySpace = cut.element
.getElementsByClassName('dv-void-container') .getElementsByClassName('dv-void-container')
.item(0); .item(0) as HTMLElement;
if (!emptySpace!) { if (!emptySpace!) {
fail('element not found'); fail('element not found');
} }
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -268,6 +275,7 @@ describe('tabsContainer', () => {
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(), onDidActivePanelChange: jest.fn(),
options: {}, options: {},
onDidOptionsChange: jest.fn(),
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>( const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -296,16 +304,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element const emptySpace = cut.element
.getElementsByClassName('dv-void-container') .getElementsByClassName('dv-void-container')
.item(0); .item(0) as HTMLElement;
if (!emptySpace!) { if (!emptySpace!) {
fail('element not found'); fail('element not found');
} }
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -337,6 +345,7 @@ describe('tabsContainer', () => {
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(), onDidActivePanelChange: jest.fn(),
options: {}, options: {},
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -402,6 +411,7 @@ describe('tabsContainer', () => {
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(), onDidActivePanelChange: jest.fn(),
options: {}, options: {},
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -469,6 +479,7 @@ describe('tabsContainer', () => {
element: document.createElement('div'), element: document.createElement('div'),
addFloatingGroup: jest.fn(), addFloatingGroup: jest.fn(),
doSetGroupActive: jest.fn(), doSetGroupActive: jest.fn(),
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -526,6 +537,7 @@ describe('tabsContainer', () => {
element: document.createElement('div'), element: document.createElement('div'),
addFloatingGroup: jest.fn(), addFloatingGroup: jest.fn(),
doSetGroupActive: jest.fn(), doSetGroupActive: jest.fn(),
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -578,6 +590,7 @@ describe('tabsContainer', () => {
element: document.createElement('div'), element: document.createElement('div'),
addFloatingGroup: jest.fn(), addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(), getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -635,6 +648,7 @@ describe('tabsContainer', () => {
element: document.createElement('div'), element: document.createElement('div'),
addFloatingGroup: jest.fn(), addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(), getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -703,6 +717,7 @@ describe('tabsContainer', () => {
element: document.createElement('div'), element: document.createElement('div'),
addFloatingGroup: jest.fn(), addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(), getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -771,6 +786,7 @@ describe('tabsContainer', () => {
element: document.createElement('div'), element: document.createElement('div'),
addFloatingGroup: jest.fn(), addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(), getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -834,6 +850,7 @@ describe('tabsContainer', () => {
const cut = new TabsContainer( const cut = new TabsContainer(
fromPartial<DockviewComponent>({ fromPartial<DockviewComponent>({
options: {}, options: {},
onDidOptionsChange: jest.fn(),
}), }),
fromPartial<DockviewGroupPanel>({}) fromPartial<DockviewGroupPanel>({})
); );

View File

@ -133,11 +133,15 @@ describe('dockviewComponent', () => {
}, },
className: 'test-a test-b', 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' }); 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', () => { describe('memory leakage', () => {
@ -2453,17 +2457,17 @@ describe('dockviewComponent', () => {
const group = dockview.getGroupPanel('panel2')!.api.group; const group = dockview.getGroupPanel('panel2')!.api.group;
const viewQuery = group.element.querySelectorAll( 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); expect(viewQuery.length).toBe(2);
const viewQuery2 = group.element.querySelectorAll( 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); expect(viewQuery2.length).toBe(1);
const viewQuery3 = group.element.querySelectorAll( 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); expect(viewQuery3.length).toBe(1);
}); });
@ -3376,10 +3380,10 @@ describe('dockviewComponent', () => {
position: { direction: 'right' }, position: { direction: 'right' },
}); });
Object.defineProperty(dockview.element, 'clientWidth', { Object.defineProperty(dockview.element, 'offsetWidth', {
get: () => 100, get: () => 100,
}); });
Object.defineProperty(dockview.element, 'clientHeight', { Object.defineProperty(dockview.element, 'offsetHeight', {
get: () => 100, get: () => 100,
}); });
@ -6726,8 +6730,55 @@ describe('dockviewComponent', () => {
expect(api.groups.length).toBe(3); expect(api.groups.length).toBe(3);
}); });
describe('updateOptions', () => { test('add group with custom group is', () => {
test('gap', () => { 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 container = document.createElement('div');
const dockview = new DockviewComponent(container, { const dockview = new DockviewComponent(container, {
@ -6742,19 +6793,14 @@ describe('dockviewComponent', () => {
throw new Error(`unsupported`); 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.layout(1000, 1000);
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);
}); });
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,8 @@ import {
FloatingGroupOptions, FloatingGroupOptions,
IDockviewComponent, IDockviewComponent,
MovePanelEvent, MovePanelEvent,
PopoutGroupChangePositionEvent,
PopoutGroupChangeSizeEvent,
SerializedDockview, SerializedDockview,
} from '../dockview/dockviewComponent'; } from '../dockview/dockviewComponent';
import { import {
@ -629,10 +631,6 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.totalPanels; 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. * 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; return this.component.onUnhandledDragOverEvent;
} }
get onDidPopoutGroupSizeChange(): Event<PopoutGroupChangeSizeEvent> {
return this.component.onDidPopoutGroupSizeChange;
}
get onDidPopoutGroupPositionChange(): Event<PopoutGroupChangePositionEvent> {
return this.component.onDidPopoutGroupPositionChange;
}
/** /**
* All panel objects. * All panel objects.
*/ */
@ -914,10 +920,6 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.addPopoutGroup(item, options); return this.component.addPopoutGroup(item, options);
} }
setGap(gap: number | undefined): void {
this.component.updateOptions({ gap });
}
updateOptions(options: Partial<DockviewComponentOptions>) { updateOptions(options: Partial<DockviewComponentOptions>) {
this.component.updateOptions(options); 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 * 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 * through .preventDefault(). Since this is applied globally to all drag events this would break dockviews
* dnd logic. You can see the code at * 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', ''); event.dataTransfer.setData('text/plain', '');
} }
@ -75,7 +75,9 @@ export abstract class DragHandler extends CompositeDisposable {
}), }),
addDisposableListener(this.el, 'dragend', () => { addDisposableListener(this.el, 'dragend', () => {
this.pointerEventsDisposable.dispose(); 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 { .dv-drop-target {
position: relative; position: relative;
--dv-transition-duration: 70ms;
> .dv-drop-target-dropzone { > .dv-drop-target-dropzone {
position: absolute; position: absolute;
@ -15,10 +16,13 @@
box-sizing: border-box; box-sizing: border-box;
height: 100%; height: 100%;
width: 100%; width: 100%;
border: var(--dv-drag-over-border);
background-color: var(--dv-drag-over-background-color); background-color: var(--dv-drag-over-background-color);
transition: top 70ms ease-out, left 70ms ease-out, transition: top var(--dv-transition-duration) ease-out,
width 70ms ease-out, height 70ms ease-out, left var(--dv-transition-duration) ease-out,
opacity 0.15s 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; will-change: transform;
pointer-events: none; pointer-events: none;

View File

@ -93,10 +93,26 @@ const DEFAULT_SIZE: MeasuredValue = {
const SMALL_WIDTH_BOUNDARY = 100; const SMALL_WIDTH_BOUNDARY = 100;
const SMALL_HEIGHT_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 { export interface DroptargetOptions {
canDisplayOverlay: CanDisplayOverlay; canDisplayOverlay: CanDisplayOverlay;
acceptedTargetZones: Position[]; acceptedTargetZones: Position[];
overlayModel?: DroptargetOverlayModel; overlayModel?: DroptargetOverlayModel;
getOverrideTarget?: () => DropTargetTargetModel | undefined;
className?: string;
getOverlayOutline?: () => HTMLElement | null;
} }
export class Droptarget extends CompositeDisposable { 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 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 { get state(): Position | undefined {
return this._state; return this._state;
} }
@ -126,21 +154,35 @@ export class Droptarget extends CompositeDisposable {
) { ) {
super(); super();
this._disabled = false;
// use a set to take advantage of #<set>.has // use a set to take advantage of #<set>.has
this._acceptedTargetZonesSet = new Set( this._acceptedTargetZonesSet = new Set(
this.options.acceptedTargetZones this.options.acceptedTargetZones
); );
this.dnd = new DragAndDropObserver(this.element, { this.dnd = new DragAndDropObserver(this.element, {
onDragEnter: () => undefined, onDragEnter: () => {
this.options.getOverrideTarget?.()?.getElements();
},
onDragOver: (e) => { onDragOver: (e) => {
Droptarget.ACTUAL_TARGET = this;
const overrideTraget = this.options.getOverrideTarget?.();
if (this._acceptedTargetZonesSet.size === 0) { if (this._acceptedTargetZonesSet.size === 0) {
if (overrideTraget) {
return;
}
this.removeDropTarget(); this.removeDropTarget();
return; return;
} }
const width = this.element.clientWidth; const target =
const height = this.element.clientHeight; this.options.getOverlayOutline?.() ?? this.element;
const width = target.offsetWidth;
const height = target.offsetHeight;
if (width === 0 || height === 0) { if (width === 0 || height === 0) {
return; // avoid div!0 return; // avoid div!0
@ -149,8 +191,8 @@ export class Droptarget extends CompositeDisposable {
const rect = ( const rect = (
e.currentTarget as HTMLElement e.currentTarget as HTMLElement
).getBoundingClientRect(); ).getBoundingClientRect();
const x = e.clientX - rect.left; const x = (e.clientX ?? 0) - rect.left;
const y = e.clientY - rect.top; const y = (e.clientY ?? 0) - rect.top;
const quadrant = this.calculateQuadrant( const quadrant = this.calculateQuadrant(
this._acceptedTargetZonesSet, this._acceptedTargetZonesSet,
@ -172,6 +214,9 @@ export class Droptarget extends CompositeDisposable {
} }
if (!this.options.canDisplayOverlay(e, quadrant)) { if (!this.options.canDisplayOverlay(e, quadrant)) {
if (overrideTraget) {
return;
}
this.removeDropTarget(); this.removeDropTarget();
return; return;
} }
@ -194,7 +239,9 @@ export class Droptarget extends CompositeDisposable {
this.markAsUsed(e); this.markAsUsed(e);
if (!this.targetElement) { if (overrideTraget) {
//
} else if (!this.targetElement) {
this.targetElement = document.createElement('div'); this.targetElement = document.createElement('div');
this.targetElement.className = 'dv-drop-target-dropzone'; this.targetElement.className = 'dv-drop-target-dropzone';
this.overlayElement = document.createElement('div'); this.overlayElement = document.createElement('div');
@ -202,8 +249,16 @@ export class Droptarget extends CompositeDisposable {
this._state = 'center'; this._state = 'center';
this.targetElement.appendChild(this.overlayElement); this.targetElement.appendChild(this.overlayElement);
this.element.classList.add('dv-drop-target'); target.classList.add('dv-drop-target');
this.element.append(this.targetElement); target.append(this.targetElement);
// this.overlayElement.style.opacity = '0';
// requestAnimationFrame(() => {
// if (this.overlayElement) {
// this.overlayElement.style.opacity = '';
// }
// });
} }
this.toggleClasses(quadrant, width, height); this.toggleClasses(quadrant, width, height);
@ -211,10 +266,32 @@ export class Droptarget extends CompositeDisposable {
this._state = quadrant; this._state = quadrant;
}, },
onDragLeave: () => { onDragLeave: () => {
const target = this.options.getOverrideTarget?.();
if (target) {
return;
}
this.removeDropTarget(); 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(); this.removeDropTarget();
target?.clear();
}, },
onDrop: (e) => { onDrop: (e) => {
e.preventDefault(); e.preventDefault();
@ -223,6 +300,8 @@ export class Droptarget extends CompositeDisposable {
this.removeDropTarget(); this.removeDropTarget();
this.options.getOverrideTarget?.()?.clear();
if (state) { if (state) {
// only stop the propagation of the event if we are dealing with it // only stop the propagation of the event if we are dealing with it
// which is only when the target has state // which is only when the target has state
@ -268,7 +347,9 @@ export class Droptarget extends CompositeDisposable {
width: number, width: number,
height: number height: number
): void { ): void {
if (!this.overlayElement) { const target = this.options.getOverrideTarget?.();
if (!target && !this.overlayElement) {
return; 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%' }; const box = { top: '0px', left: '0px', width: '100%', height: '100%' };
/** /**
@ -396,10 +574,12 @@ export class Droptarget extends CompositeDisposable {
private removeDropTarget(): void { private removeDropTarget(): void {
if (this.targetElement) { if (this.targetElement) {
this._state = undefined; this._state = undefined;
this.element.removeChild(this.targetElement); this.targetElement.parentElement?.classList.remove(
'dv-drop-target'
);
this.targetElement.remove();
this.targetElement = undefined; this.targetElement = undefined;
this.overlayElement = 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( export function addGhostImage(
dataTransfer: DataTransfer, dataTransfer: DataTransfer,
ghostElement: HTMLElement ghostElement: HTMLElement,
options?: { x?: number; y?: number }
): void { ): void {
// class dockview provides to force ghost image to be drawn on a different layer and prevent weird rendering issues // class dockview provides to force ghost image to be drawn on a different layer and prevent weird rendering issues
addClasses(ghostElement, 'dv-dragged'); addClasses(ghostElement, 'dv-dragged');
document.body.appendChild(ghostElement); document.body.appendChild(ghostElement);
dataTransfer.setDragImage(ghostElement, 0, 0); dataTransfer.setDragImage(ghostElement, options?.x ?? 0, options?.y ?? 0);
setTimeout(() => { setTimeout(() => {
removeClasses(ghostElement, 'dv-dragged'); removeClasses(ghostElement, 'dv-dragged');

View File

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

View File

@ -55,7 +55,15 @@ export class ContentContainer
this.addDisposables(this._onDidFocus, this._onDidBlur); this.addDisposables(this._onDidFocus, this._onDidBlur);
const target = group.dropTargetContainer;
this.dropTarget = new Droptarget(this.element, { 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'], acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
canDisplayOverlay: (event, position) => { canDisplayOverlay: (event, position) => {
if ( if (
@ -76,26 +84,12 @@ export class ContentContainer
} }
if (data && data.viewId === this.accessor.id) { if (data && data.viewId === this.accessor.id) {
if (data.groupId === this.group.id) { return true;
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 this.group.canDisplayOverlay(event, position, 'content'); return this.group.canDisplayOverlay(event, position, 'content');
}, },
getOverrideTarget: target ? () => target.model : undefined,
}); });
this.addDisposables(this.dropTarget); 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; position: relative;
height: 100%; height: 100%;
display: flex; display: flex;
min-width: 80px;
align-items: center; align-items: center;
padding: 0px 8px;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
.dv-default-tab-content { .dv-default-tab-content {
padding: 0px 8px;
flex-grow: 1; flex-grow: 1;
margin-right: 4px;
} }
.dv-default-tab-action { .dv-default-tab-action {

View File

@ -16,6 +16,7 @@ import {
} from '../../../dnd/droptarget'; } from '../../../dnd/droptarget';
import { DragHandler } from '../../../dnd/abstractDragHandler'; import { DragHandler } from '../../../dnd/abstractDragHandler';
import { IDockviewPanel } from '../../dockviewPanel'; import { IDockviewPanel } from '../../dockviewPanel';
import { addGhostImage } from '../../../dnd/ghost';
class TabDragHandler extends DragHandler { class TabDragHandler extends DragHandler {
private readonly panelTransfer = private readonly panelTransfer =
@ -92,7 +93,8 @@ export class Tab extends CompositeDisposable {
); );
this.dropTarget = new Droptarget(this._element, { this.dropTarget = new Droptarget(this._element, {
acceptedTargetZones: ['center'], acceptedTargetZones: ['left', 'right'],
overlayModel: { activationSize: { value: 50, type: 'percentage' } },
canDisplayOverlay: (event, position) => { canDisplayOverlay: (event, position) => {
if (this.group.locked) { if (this.group.locked) {
return false; return false;
@ -101,15 +103,7 @@ export class Tab extends CompositeDisposable {
const data = getPanelData(); const data = getPanelData();
if (data && this.accessor.id === data.viewId) { if (data && this.accessor.id === data.viewId) {
if ( return true;
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 this.group.model.canDisplayOverlay( return this.group.model.canDisplayOverlay(
@ -118,6 +112,7 @@ export class Tab extends CompositeDisposable {
'tab' 'tab'
); );
}, },
getOverrideTarget: () => group.model.dropTargetContainer?.model,
}); });
this.onWillShowOverlay = this.dropTarget.onWillShowOverlay; this.onWillShowOverlay = this.dropTarget.onWillShowOverlay;
@ -127,6 +122,23 @@ export class Tab extends CompositeDisposable {
this._onDropped, this._onDropped,
this._onDragStart, this._onDragStart,
dragHandler.onDragStart((event) => { 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); this._onDragStart.fire(event);
}), }),
dragHandler, 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); font-size: var(--dv-tabs-and-actions-container-font-size);
&.dv-single-tab.dv-full-width-single-tab { &.dv-single-tab.dv-full-width-single-tab {
.dv-tabs-container { .dv-scrollable {
flex-grow: 1;
.dv-tab {
flex-grow: 1; flex-grow: 1;
} }
}
.dv-void-container { .dv-tabs-container {
flex-grow: 0; flex-grow: 1;
}
.dv-tab {
flex-grow: 1;
padding: 0px;
}
}
.dv-void-container {
flex-grow: 0;
}
} }
.dv-void-container { .dv-void-container {
@ -26,46 +31,7 @@
cursor: grab; cursor: grab;
} }
.dv-tabs-container { .dv-right-actions-container {
display: flex; 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 { import {
IDisposable, IDisposable,
CompositeDisposable, CompositeDisposable,
IValueDisposable, Disposable,
MutableDisposable,
} from '../../../lifecycle'; } from '../../../lifecycle';
import { addDisposableListener, Emitter, Event } from '../../../events'; import { addDisposableListener, Emitter, Event } from '../../../events';
import { Tab } from '../tab/tab'; import { Tab } from '../tab/tab';
import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { VoidContainer } from './voidContainer'; import { VoidContainer } from './voidContainer';
import { toggleClass } from '../../../dom'; import { toggleClass } from '../../../dom';
import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel'; import { IDockviewPanel } from '../../dockviewPanel';
import { DockviewComponent } from '../../dockviewComponent'; import { DockviewComponent } from '../../dockviewComponent';
import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel'; import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel';
import { getPanelData } from '../../../dnd/dataTransfer'; import { getPanelData } from '../../../dnd/dataTransfer';
import { Tabs } from './tabs';
import {
createDropdownElementHandle,
DropdownElement,
} from './tabOverflowControl';
export interface TabDropIndexEvent { export interface TabDropIndexEvent {
readonly event: DragEvent; readonly event: DragEvent;
@ -56,25 +62,28 @@ export class TabsContainer
implements ITabsContainer implements ITabsContainer
{ {
private readonly _element: HTMLElement; private readonly _element: HTMLElement;
private readonly tabContainer: HTMLElement; private readonly tabs: Tabs;
private readonly rightActionsContainer: HTMLElement; private readonly rightActionsContainer: HTMLElement;
private readonly leftActionsContainer: HTMLElement; private readonly leftActionsContainer: HTMLElement;
private readonly preActionsContainer: HTMLElement; private readonly preActionsContainer: HTMLElement;
private readonly voidContainer: VoidContainer; private readonly voidContainer: VoidContainer;
private tabs: IValueDisposable<Tab>[] = [];
private selectedIndex = -1;
private rightActions: HTMLElement | undefined; private rightActions: HTMLElement | undefined;
private leftActions: HTMLElement | undefined; private leftActions: HTMLElement | undefined;
private preActions: HTMLElement | undefined; private preActions: HTMLElement | undefined;
private _hidden = false; private _hidden = false;
private dropdownPart: DropdownElement | null = null;
private _overflowTabs: string[] = [];
private readonly _dropdownDisposable = new MutableDisposable();
private readonly _onDrop = new Emitter<TabDropIndexEvent>(); private readonly _onDrop = new Emitter<TabDropIndexEvent>();
readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event; readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event;
private readonly _onTabDragStart = new Emitter<TabDragEvent>(); get onTabDragStart(): Event<TabDragEvent> {
readonly onTabDragStart: Event<TabDragEvent> = this._onTabDragStart.event; return this.tabs.onTabDragStart;
}
private readonly _onGroupDragStart = new Emitter<GroupDragEvent>(); private readonly _onGroupDragStart = new Emitter<GroupDragEvent>();
readonly onGroupDragStart: Event<GroupDragEvent> = readonly onGroupDragStart: Event<GroupDragEvent> =
@ -86,11 +95,11 @@ export class TabsContainer
this._onWillShowOverlay.event; this._onWillShowOverlay.event;
get panels(): string[] { get panels(): string[] {
return this.tabs.map((_) => _.value.panel.id); return this.tabs.panels;
} }
get size(): number { get size(): number {
return this.tabs.length; return this.tabs.size;
} }
get hidden(): boolean { get hidden(): boolean {
@ -102,6 +111,118 @@ export class TabsContainer
this.element.style.display = value ? 'none' : ''; 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 { show(): void {
if (!this.hidden) { if (!this.hidden) {
this.element.style.display = ''; this.element.style.display = '';
@ -154,319 +275,124 @@ export class TabsContainer
} }
} }
get element(): HTMLElement { isActive(tab: Tab): boolean {
return this._element; return this.tabs.isActive(tab);
} }
public isActive(tab: Tab): boolean { indexOf(id: string): number {
return ( return this.tabs.indexOf(id);
this.selectedIndex > -1 &&
this.tabs[this.selectedIndex].value === tab
);
} }
public indexOf(id: string): number { setActive(_isGroupActive: boolean) {
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) {
// noop // noop
} }
public delete(id: string): void { delete(id: string): void {
const index = this.tabs.findIndex((tab) => tab.value.panel.id === id); this.tabs.delete(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();
this.updateClassnames(); this.updateClassnames();
} }
public setActivePanel(panel: IDockviewPanel): void { setActivePanel(panel: IDockviewPanel): void {
this.tabs.forEach((tab) => { this.tabs.setActivePanel(panel);
const isActivePanel = panel.id === tab.value.panel.id;
tab.value.setActive(isActivePanel);
tab.value.panel.runEvents();
});
} }
public openPanel( openPanel(panel: IDockviewPanel, index: number = this.tabs.size): void {
panel: IDockviewPanel, this.tabs.openPanel(panel, index);
index: number = this.tabs.length this.updateClassnames();
): 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);
} }
public closePanel(panel: IDockviewPanel): void { closePanel(panel: IDockviewPanel): void {
this.delete(panel.id); 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 { private updateClassnames(): void {
toggleClass(this._element, 'dv-single-tab', this.size === 1); 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 { getPanelData } from '../../../dnd/dataTransfer';
import { import {
Droptarget, Droptarget,
@ -10,6 +9,7 @@ import { DockviewComponent } from '../../dockviewComponent';
import { addDisposableListener, Emitter, Event } from '../../../events'; import { addDisposableListener, Emitter, Event } from '../../../events';
import { CompositeDisposable } from '../../../lifecycle'; import { CompositeDisposable } from '../../../lifecycle';
import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel';
export class VoidContainer extends CompositeDisposable { export class VoidContainer extends CompositeDisposable {
private readonly _element: HTMLElement; private readonly _element: HTMLElement;
@ -54,16 +54,7 @@ export class VoidContainer extends CompositeDisposable {
const data = getPanelData(); const data = getPanelData();
if (data && this.accessor.id === data.viewId) { if (data && this.accessor.id === data.viewId) {
if ( return true;
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 group.model.canDisplayOverlay( return group.model.canDisplayOverlay(
@ -72,6 +63,7 @@ export class VoidContainer extends CompositeDisposable {
'header_space' 'header_space'
); );
}, },
getOverrideTarget: () => group.model.dropTargetContainer?.model,
}); });
this.onWillShowOverlay = this.dropTraget.onWillShowOverlay; this.onWillShowOverlay = this.dropTraget.onWillShowOverlay;

View File

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

View File

@ -14,7 +14,7 @@ import {
import { tail, sequenceEquals, remove } from '../array'; import { tail, sequenceEquals, remove } from '../array';
import { DockviewPanel, IDockviewPanel } from './dockviewPanel'; import { DockviewPanel, IDockviewPanel } from './dockviewPanel';
import { CompositeDisposable, Disposable } from '../lifecycle'; import { CompositeDisposable, Disposable } from '../lifecycle';
import { Event, Emitter, addDisposableWindowListener } from '../events'; import { Event, Emitter, addDisposableListener } from '../events';
import { Watermark } from './components/watermark/watermark'; import { Watermark } from './components/watermark/watermark';
import { IWatermarkRenderer, GroupviewPanelState } from './types'; import { IWatermarkRenderer, GroupviewPanelState } from './types';
import { sequentialNumberGenerator } from '../math'; import { sequentialNumberGenerator } from '../math';
@ -54,7 +54,10 @@ import { Parameters } from '../panel/types';
import { Overlay } from '../overlay/overlay'; import { Overlay } from '../overlay/overlay';
import { import {
addTestId, addTestId,
Classnames,
getDockviewTheme, getDockviewTheme,
onDidWindowResizeEnd,
onDidWindowMoveEnd,
toggleClass, toggleClass,
watchElementResize, watchElementResize,
} from '../dom'; } from '../dom';
@ -74,6 +77,9 @@ import {
} from '../overlay/overlayRenderContainer'; } from '../overlay/overlayRenderContainer';
import { PopoutWindow } from '../popoutWindow'; import { PopoutWindow } from '../popoutWindow';
import { StrictEventsSequencing } from './strictEventsSequencing'; import { StrictEventsSequencing } from './strictEventsSequencing';
import { PopupService } from './components/popupService';
import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer';
import { themeAbyss } from './theme';
const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = {
activationSize: { type: 'pixels', value: 10 }, activationSize: { type: 'pixels', value: 10 },
@ -186,12 +192,23 @@ export interface DockviewMaximizedGroupChanged {
isMaximized: boolean; 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> { export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly activePanel: IDockviewPanel | undefined; readonly activePanel: IDockviewPanel | undefined;
readonly totalPanels: number; readonly totalPanels: number;
readonly panels: IDockviewPanel[]; readonly panels: IDockviewPanel[];
readonly orientation: Orientation; readonly orientation: Orientation;
readonly gap: number;
readonly onDidDrop: Event<DockviewDidDropEvent>; readonly onDidDrop: Event<DockviewDidDropEvent>;
readonly onWillDrop: Event<DockviewWillDropEvent>; readonly onWillDrop: Event<DockviewWillDropEvent>;
readonly onWillShowOverlay: Event<WillShowOverlayLocationEvent>; readonly onWillShowOverlay: Event<WillShowOverlayLocationEvent>;
@ -207,6 +224,8 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly onUnhandledDragOverEvent: Event<DockviewDndOverlayEvent>; readonly onUnhandledDragOverEvent: Event<DockviewDndOverlayEvent>;
readonly onDidMovePanel: Event<MovePanelEvent>; readonly onDidMovePanel: Event<MovePanelEvent>;
readonly onDidMaximizedGroupChange: Event<DockviewMaximizedGroupChanged>; readonly onDidMaximizedGroupChange: Event<DockviewMaximizedGroupChanged>;
readonly onDidPopoutGroupSizeChange: Event<PopoutGroupChangeSizeEvent>;
readonly onDidPopoutGroupPositionChange: Event<PopoutGroupChangePositionEvent>;
readonly options: DockviewComponentOptions; readonly options: DockviewComponentOptions;
updateOptions(options: DockviewOptions): void; updateOptions(options: DockviewOptions): void;
moveGroupOrPanel(options: MoveGroupOrPanelOptions): void; moveGroupOrPanel(options: MoveGroupOrPanelOptions): void;
@ -253,9 +272,12 @@ export class DockviewComponent
private readonly _deserializer = new DefaultDockviewDeserialzier(this); private readonly _deserializer = new DefaultDockviewDeserialzier(this);
private readonly _api: DockviewApi; private readonly _api: DockviewApi;
private _options: Exclude<DockviewComponentOptions, 'orientation'>; private _options: Exclude<DockviewComponentOptions, 'orientation'>;
private watermark: IWatermarkRenderer | null = null; private _watermark: IWatermarkRenderer | null = null;
private readonly _themeClassnames: Classnames;
readonly overlayRenderContainer: OverlayRenderContainer; readonly overlayRenderContainer: OverlayRenderContainer;
readonly popupService: PopupService;
readonly rootDropTargetContainer: DropTargetAnchorContainer;
private readonly _onWillDragPanel = new Emitter<TabDragEvent>(); private readonly _onWillDragPanel = new Emitter<TabDragEvent>();
readonly onWillDragPanel: Event<TabDragEvent> = this._onWillDragPanel.event; readonly onWillDragPanel: Event<TabDragEvent> = this._onWillDragPanel.event;
@ -287,6 +309,16 @@ export class DockviewComponent
private readonly _onDidAddPanel = new Emitter<IDockviewPanel>(); private readonly _onDidAddPanel = new Emitter<IDockviewPanel>();
readonly onDidAddPanel: Event<IDockviewPanel> = this._onDidAddPanel.event; 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>(); private readonly _onDidLayoutFromJSON = new Emitter<void>();
readonly onDidLayoutFromJSON: Event<void> = this._onDidLayoutFromJSON.event; readonly onDidLayoutFromJSON: Event<void> = this._onDidLayoutFromJSON.event;
@ -320,6 +352,9 @@ export class DockviewComponent
readonly onDidAddGroup: Event<DockviewGroupPanel> = readonly onDidAddGroup: Event<DockviewGroupPanel> =
this._onDidAddGroup.event; this._onDidAddGroup.event;
private readonly _onDidOptionsChange = new Emitter<void>();
readonly onDidOptionsChange: Event<void> = this._onDidOptionsChange.event;
private readonly _onDidActiveGroupChange = new Emitter< private readonly _onDidActiveGroupChange = new Emitter<
DockviewGroupPanel | undefined DockviewGroupPanel | undefined
>(); >();
@ -360,10 +395,6 @@ export class DockviewComponent
return this._api; return this._api;
} }
get gap(): number {
return this.gridview.margin;
}
get floatingGroups(): DockviewFloatingGroupPanel[] { get floatingGroups(): DockviewFloatingGroupPanel[] {
return this._floatingGroups; return this._floatingGroups;
} }
@ -377,95 +408,27 @@ export class DockviewComponent
: undefined, : undefined,
disableAutoResizing: options.disableAutoResizing, disableAutoResizing: options.disableAutoResizing,
locked: options.locked, locked: options.locked,
margin: options.gap, margin: options.theme?.gap ?? 0,
className: options.className, 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.overlayRenderContainer = new OverlayRenderContainer(
this.gridview.element, this.gridview.element,
this 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, { this._rootDropTarget = new Droptarget(this.element, {
className: 'dv-drop-target-edge',
canDisplayOverlay: (event, position) => { canDisplayOverlay: (event, position) => {
const data = getPanelData(); const data = getPanelData();
@ -505,10 +468,96 @@ export class DockviewComponent
}, },
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
overlayModel: 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.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,
this._rootDropTarget.onWillShowOverlay((event) => { this._rootDropTarget.onWillShowOverlay((event) => {
if (this.gridview.length > 0 && event.position === 'center') { if (this.gridview.length > 0 && event.position === 'center') {
@ -571,10 +620,6 @@ export class DockviewComponent
}), }),
this._rootDropTarget this._rootDropTarget
); );
this._api = new DockviewApi(this);
this.updateWatermark();
} }
override setVisible(panel: DockviewGroupPanel, visible: boolean): void { override setVisible(panel: DockviewGroupPanel, visible: boolean): void {
@ -756,6 +801,15 @@ export class DockviewComponent
popoutContainer.appendChild(group.element); 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 = { group.model.location = {
type: 'popout', type: 'popout',
getWindow: () => _window.window!, getWindow: () => _window.window!,
@ -803,22 +857,37 @@ export class DockviewComponent
}, },
}; };
const _onDidWindowPositionChange = onDidWindowMoveEnd(
_window.window!
);
popoutWindowDisposable.addDisposables( 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 * 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 * since we can reply on the window resize event as we will occupy the full
* window dimensions * window dimensions
*/ */
addDisposableWindowListener( addDisposableListener(_window.window!, 'resize', () => {
_window.window!, group.layout(
'resize', _window.window!.innerWidth,
() => { _window.window!.innerHeight
group.layout( );
_window.window!.innerWidth, }),
_window.window!.innerHeight
);
}
),
overlayRenderContainer, overlayRenderContainer,
Disposable.from(() => { Disposable.from(() => {
if (this.isDisposed) { if (this.isDisposed) {
@ -848,6 +917,8 @@ export class DockviewComponent
} else if (this.getPanel(group.id)) { } else if (this.getPanel(group.id)) {
group.model.renderContainer = group.model.renderContainer =
this.overlayRenderContainer; this.overlayRenderContainer;
group.model.dropTargetContainer =
this.rootDropTargetContainer;
returnedGroup = group; returnedGroup = group;
const alreadyRemoved = !this._popoutGroups.find( const alreadyRemoved = !this._popoutGroups.find(
@ -1100,7 +1171,10 @@ export class DockviewComponent
this.updateWatermark(); this.updateWatermark();
} }
private orthogonalize(position: Position): DockviewGroupPanel { private orthogonalize(
position: Position,
options?: GroupOptions
): DockviewGroupPanel {
switch (position) { switch (position) {
case 'top': case 'top':
case 'bottom': case 'bottom':
@ -1126,10 +1200,14 @@ export class DockviewComponent
case 'top': case 'top':
case 'left': case 'left':
case 'center': case 'center':
return this.createGroupAtLocation([0]); // insert into first position return this.createGroupAtLocation([0], undefined, options); // insert into first position
case 'bottom': case 'bottom':
case 'right': 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: default:
throw new Error(`unsupported position ${position}`); throw new Error(`unsupported position ${position}`);
} }
@ -1162,18 +1240,14 @@ export class DockviewComponent
} }
} }
if ('rootOverlayModel' in options) { this.updateDropTargetModel(options);
this._rootDropTarget.setOverlayModel(
options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL
);
}
if ('gap' in options) {
this.gridview.margin = options.gap ?? 0;
}
this._options = { ...this.options, ...options }; this._options = { ...this.options, ...options };
if ('theme' in options) {
this.updateTheme();
}
this.layout(this.gridview.width, this.gridview.height, true); 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 (x) => x.api.location.type === 'grid' && x.api.isVisible
).length === 0 ).length === 0
) { ) {
if (!this.watermark) { if (!this._watermark) {
this.watermark = this.createWatermarkComponent(); this._watermark = this.createWatermarkComponent();
this.watermark.init({ this._watermark.init({
containerApi: new DockviewApi(this), containerApi: new DockviewApi(this),
}); });
const watermarkContainer = document.createElement('div'); const watermarkContainer = document.createElement('div');
watermarkContainer.className = 'dv-watermark-container'; watermarkContainer.className = 'dv-watermark-container';
addTestId(watermarkContainer, 'watermark-component'); addTestId(watermarkContainer, 'watermark-component');
watermarkContainer.appendChild(this.watermark.element); watermarkContainer.appendChild(this._watermark.element);
this.gridview.element.appendChild(watermarkContainer); this.gridview.element.appendChild(watermarkContainer);
} }
} else if (this.watermark) { } else if (this._watermark) {
this.watermark.element.parentElement!.remove(); this._watermark.element.parentElement!.remove();
this.watermark.dispose?.(); this._watermark.dispose?.();
this.watermark = null; this._watermark = null;
} }
} }
@ -1808,7 +1882,8 @@ export class DockviewComponent
} }
} else { } else {
const group = this.orthogonalize( const group = this.orthogonalize(
directionToPosition(<Direction>options.direction) directionToPosition(<Direction>options.direction),
options
); );
if (!options.skipSetActive) { if (!options.skipSetActive) {
this.doSetGroupAndPanelActive(group); this.doSetGroupAndPanelActive(group);
@ -2408,9 +2483,11 @@ export class DockviewComponent
if (this._moving) { if (this._moving) {
return; return;
} }
if (event.panel !== this.activePanel) { if (event.panel !== this.activePanel) {
return; return;
} }
if (this._onDidActivePanelChange.value !== event.panel) { if (this._onDidActivePanelChange.value !== event.panel) {
this._onDidActivePanelChange.fire(event.panel); this._onDidActivePanelChange.fire(event.panel);
} }
@ -2474,9 +2551,10 @@ export class DockviewComponent
private createGroupAtLocation( private createGroupAtLocation(
location: number[], location: number[],
size?: number size?: number,
options?: GroupOptions
): DockviewGroupPanel { ): DockviewGroupPanel {
const group = this.createGroup(); const group = this.createGroup(options);
this.doAddGroup(group, location, size); this.doAddGroup(group, location, size);
return group; return group;
} }
@ -2493,4 +2571,44 @@ export class DockviewComponent
? rootOrientation ? rootOrientation
: orthogonal(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 { OverlayRenderContainer } from '../overlay/overlayRenderContainer';
import { TitleEvent } from '../api/dockviewPanelApi'; import { TitleEvent } from '../api/dockviewPanelApi';
import { Contraints } from '../gridview/gridviewPanel'; import { Contraints } from '../gridview/gridviewPanel';
import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer';
interface GroupMoveEvent { interface GroupMoveEvent {
groupId: string; groupId: string;
@ -265,6 +266,8 @@ export class DockviewGroupPanelModel
private mostRecentlyUsed: IDockviewPanel[] = []; private mostRecentlyUsed: IDockviewPanel[] = [];
private _overwriteRenderContainer: OverlayRenderContainer | null = null; private _overwriteRenderContainer: OverlayRenderContainer | null = null;
private _overwriteDropTargetContainer: DropTargetAnchorContainer | null =
null;
private readonly _onDidChange = new Emitter<IViewSize | undefined>(); private readonly _onDidChange = new Emitter<IViewSize | undefined>();
readonly onDidChange: Event<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 { initialize(): void {
if (this.options.panels) { if (this.options.panels) {
this.options.panels.forEach((panel) => { this.options.panels.forEach((panel) => {
@ -1049,6 +1063,29 @@ export class DockviewGroupPanelModel
const data = getPanelData(); const data = getPanelData();
if (data && data.viewId === this.accessor.id) { 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) { if (data.panelId === null) {
// this is a group move dnd event // this is a group move dnd event
const { groupId } = data; const { groupId } = data;

View File

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

View File

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

View File

@ -17,6 +17,7 @@ import { IGroupHeaderProps } from './framework';
import { FloatingGroupOptions } from './dockviewComponent'; import { FloatingGroupOptions } from './dockviewComponent';
import { Contraints } from '../gridview/gridviewPanel'; import { Contraints } from '../gridview/gridviewPanel';
import { AcceptableEvent, IAcceptableEvent } from '../events'; import { AcceptableEvent, IAcceptableEvent } from '../events';
import { DockviewTheme } from './theme';
export interface IHeaderActionsRenderer extends IDisposable { export interface IHeaderActionsRenderer extends IDisposable {
readonly element: HTMLElement; readonly element: HTMLElement;
@ -52,18 +53,29 @@ export interface DockviewOptions {
popoutUrl?: string; popoutUrl?: string;
defaultRenderer?: DockviewPanelRenderer; defaultRenderer?: DockviewPanelRenderer;
debug?: boolean; debug?: boolean;
rootOverlayModel?: DroptargetOverlayModel; // #start dnd
locked?: boolean; dndEdges?: false | DroptargetOverlayModel;
disableDnd?: boolean;
className?: string;
/** /**
* Pixel gap between groups * @deprecated use `dndEdges` instead. To be removed in a future version.
*/ * */
gap?: number; 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`. * Define the behaviour of the dock when there are no panels to display. Defaults to `watermark`.
*/ */
noPanelsOverlay?: 'emptyGroup' | '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 { export interface DockviewDndOverlayEvent extends IAcceptableEvent {
@ -106,9 +118,12 @@ export const PROPERTY_KEYS_DOCKVIEW: (keyof DockviewOptions)[] = (() => {
rootOverlayModel: undefined, rootOverlayModel: undefined,
locked: undefined, locked: undefined,
disableDnd: undefined, disableDnd: undefined,
gap: undefined,
className: undefined, className: undefined,
noPanelsOverlay: undefined, noPanelsOverlay: undefined,
dndEdges: undefined,
theme: undefined,
disableTabsOverflowList: undefined,
scrollbars: undefined,
}; };
return Object.keys(properties) as (keyof DockviewOptions)[]; 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 { Optional } from '../types';
import { IDockviewGroupPanel } from './dockviewGroupPanel'; import { IDockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer'; import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer';
import { TabLocation } from './framework';
export interface HeaderPartInitParameters { export interface HeaderPartInitParameters {
title: string; title: string;
@ -34,10 +35,14 @@ export interface IWatermarkRenderer
init: (params: WatermarkRendererInitParameters) => void; init: (params: WatermarkRendererInitParameters) => void;
} }
export interface TabPartInitParameters extends GroupPanelPartInitParameters {
tabLocation: TabLocation;
}
export interface ITabRenderer export interface ITabRenderer
extends Optional<Omit<IPanel, 'id'>, RendererMethodOptionalList> { extends Optional<Omit<IPanel, 'id'>, RendererMethodOptionalList> {
readonly element: HTMLElement; readonly element: HTMLElement;
init(parameters: GroupPanelPartInitParameters): void; init(parameters: TabPartInitParameters): void;
} }
export interface IContentRenderer export interface IContentRenderer

View File

@ -2,7 +2,6 @@ import {
Event as DockviewEvent, Event as DockviewEvent,
Emitter, Emitter,
addDisposableListener, addDisposableListener,
addDisposableWindowListener,
} from './events'; } from './events';
import { IDisposable, CompositeDisposable } from './lifecycle'; import { IDisposable, CompositeDisposable } from './lifecycle';
@ -112,8 +111,11 @@ export function isAncestor(
return false; return false;
} }
export function getElementsByTagName(tag: string): HTMLElement[] { export function getElementsByTagName(
return Array.prototype.slice.call(document.getElementsByTagName(tag), 0); tag: string,
document: ParentNode
): HTMLElement[] {
return Array.prototype.slice.call(document.querySelectorAll(tag), 0);
} }
export interface IFocusTracker extends IDisposable { export interface IFocusTracker extends IDisposable {
@ -122,7 +124,7 @@ export interface IFocusTracker extends IDisposable {
refreshState?(): void; refreshState?(): void;
} }
export function trackFocus(element: HTMLElement | Window): IFocusTracker { export function trackFocus(element: HTMLElement): IFocusTracker {
return new FocusTracker(element); return new FocusTracker(element);
} }
@ -138,7 +140,7 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker {
private readonly _refreshStateHandler: () => void; private readonly _refreshStateHandler: () => void;
constructor(element: HTMLElement | Window) { constructor(element: HTMLElement) {
super(); super();
this.addDisposables(this._onDidFocus, this._onDidBlur); this.addDisposables(this._onDidFocus, this._onDidBlur);
@ -181,21 +183,12 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker {
} }
}; };
if (element instanceof HTMLElement) { this.addDisposables(
this.addDisposables( addDisposableListener(element, 'focus', onFocus, true)
addDisposableListener(element, 'focus', onFocus, true) );
); this.addDisposables(
this.addDisposables( addDisposableListener(element, 'blur', onBlur, true)
addDisposableListener(element, 'blur', onBlur, true) );
);
} else {
this.addDisposables(
addDisposableWindowListener(element, 'focus', onFocus, true)
);
this.addDisposables(
addDisposableWindowListener(element, 'blur', onBlur, true)
);
}
} }
refreshState(): void { refreshState(): void {
@ -288,11 +281,36 @@ export function addTestId(element: HTMLElement, id: string): void {
element.setAttribute('data-testid', id); element.setAttribute('data-testid', id);
} }
export function disableIframePointEvents() { /**
const iframes: HTMLElement[] = [ * Should be more efficient than element.querySelectorAll("*") since there
...getElementsByTagName('iframe'), * is no need to store every element in-memory using this approach
...getElementsByTagName('webview'), */
]; 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 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, element: Window,
type: K, type: K,
listener: (this: Window, ev: WindowEventMap[K]) => any, listener: (this: Window, ev: WindowEventMap[K]) => any,
options?: boolean | AddEventListenerOptions options?: boolean | AddEventListenerOptions
): IDisposable { ): IDisposable;
element.addEventListener(type, listener, options);
return {
dispose: () => {
element.removeEventListener(type, listener, options);
},
};
}
export function addDisposableListener<K extends keyof HTMLElementEventMap>( export function addDisposableListener<K extends keyof HTMLElementEventMap>(
element: HTMLElement, element: HTMLElement,
type: K, type: K,
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
options?: boolean | AddEventListenerOptions 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 { ): IDisposable {
element.addEventListener(type, listener, options); element.addEventListener(type, <any>listener, options);
return { return {
dispose: () => { 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(); 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 type Direction = 'left' | 'right' | 'above' | 'below' | 'within';
export function toTarget(direction: Direction): Position { export function toTarget(direction: Direction): Position {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { addStyles } from './dom'; import { addStyles } from './dom';
import { Emitter, addDisposableWindowListener } from './events'; import { Emitter, addDisposableListener } from './events';
import { CompositeDisposable, Disposable, IDisposable } from './lifecycle'; import { CompositeDisposable, Disposable, IDisposable } from './lifecycle';
import { Box } from './types'; import { Box } from './types';
@ -101,7 +101,7 @@ export class PopoutWindow extends CompositeDisposable {
Disposable.from(() => { Disposable.from(() => {
externalWindow.close(); externalWindow.close();
}), }),
addDisposableWindowListener(window, 'beforeunload', () => { addDisposableListener(window, 'beforeunload', () => {
/** /**
* before the main window closes we should close this popup too * before the main window closes we should close this popup too
* to be good citizens * 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 * beforeunload must be registered after load for reasons I could not determine
* otherwise the beforeunload event will not fire when the window is closed * otherwise the beforeunload event will not fire when the window is closed
*/ */
addDisposableWindowListener( addDisposableListener(
externalWindow, externalWindow,
'beforeunload', '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 -moz-user-select: none; // Firefox
-ms-user-select: none; // IE 10 and IE 11 -ms-user-select: none; // IE 10 and IE 11
touch-action: none; touch-action: none;
background-color: var(--dv-sash-color, transparent);
&:not(.disabled):active, &:not(.disabled):active,
&:not(.disabled):hover { &:not(.disabled):hover {

View File

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

View File

@ -122,6 +122,10 @@ export class SplitviewComponent
} }
set splitview(value: Splitview) { set splitview(value: Splitview) {
if (this._splitview) {
this._splitview.dispose();
}
this._splitview = value; this._splitview = value;
this._splitviewChangeDisposable.value = new CompositeDisposable( this._splitviewChangeDisposable.value = new CompositeDisposable(
@ -425,6 +429,8 @@ export class SplitviewComponent
view.dispose(); view.dispose();
} }
this.element.remove();
super.dispose(); 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 { @mixin dockview-theme-core-mixin {
--dv-paneview-active-outline-color: dodgerblue; --dv-paneview-active-outline-color: dodgerblue;
--dv-tabs-and-actions-container-font-size: 13px; --dv-tabs-and-actions-container-font-size: 13px;
--dv-tabs-and-actions-container-height: 35px; --dv-tabs-and-actions-container-height: 35px;
--dv-drag-over-background-color: rgba(83, 89, 93, 0.5); --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-tabs-container-scrollbar-color: #888;
--dv-icon-hover-background-color: rgba(90, 93, 94, 0.31); --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-floating-box-shadow: 8px 8px 8px 0px rgba(83, 89, 93, 0.5);
--dv-overlay-z-index: 999; --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-duration: 0.1s;
--dv-active-sash-transition-delay: 0.5s; --dv-active-sash-transition-delay: 0.5s;
} }
@mixin dockview-theme-dark-mixin { @mixin dockview-theme-dark-mixin {
@include dockview-theme-core-mixin(); @include dockview-theme-core-mixin();
@include dockview-drop-target-no-travel();
// //
--dv-group-view-background-color: #1e1e1e; --dv-group-view-background-color: #1e1e1e;
@ -37,6 +49,8 @@
@mixin dockview-theme-light-mixin { @mixin dockview-theme-light-mixin {
@include dockview-theme-core-mixin(); @include dockview-theme-core-mixin();
@include dockview-drop-target-no-travel();
// //
--dv-group-view-background-color: white; --dv-group-view-background-color: white;
// //
@ -55,6 +69,8 @@
// //
--dv-separator-border: rgba(128, 128, 128, 0.35); --dv-separator-border: rgba(128, 128, 128, 0.35);
--dv-paneview-header-border-color: rgb(51, 51, 51); --dv-paneview-header-border-color: rgb(51, 51, 51);
--dv-scrollbar-background-color: rgba(0, 0, 0, 0.25);
} }
.dockview-theme-dark { .dockview-theme-dark {
@ -133,30 +149,49 @@
@mixin dockview-theme-abyss-mixin { @mixin dockview-theme-abyss-mixin {
@include dockview-theme-core-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-visiblepanel-tab-background-color: var(
--dv-activegroup-hiddenpanel-tab-background-color: #10192c; --dv-color-abyss-dark
--dv-inactivegroup-visiblepanel-tab-background-color: #000c18; );
--dv-inactivegroup-hiddenpanel-tab-background-color: #10192c; --dv-activegroup-hiddenpanel-tab-background-color: var(--dv-color-abyss);
--dv-tab-divider-color: #2b2b4a; --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-visiblepanel-tab-color: white;
--dv-activegroup-hiddenpanel-tab-color: rgba(255, 255, 255, 0.5); --dv-activegroup-hiddenpanel-tab-color: rgba(255, 255, 255, 0.5);
--dv-inactivegroup-visiblepanel-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-inactivegroup-hiddenpanel-tab-color: rgba(255, 255, 255, 0.25);
// //
--dv-separator-border: #2b2b4a; --dv-separator-border: var(--dv-color-abyss-lighter);
--dv-paneview-header-border-color: #2b2b4a; --dv-paneview-header-border-color: var(--dv-color-abyss-lighter);
--dv-paneview-active-outline-color: #596f99; --dv-paneview-active-outline-color: #596f99;
} }
@mixin dockview-theme-dracula-mixin { @mixin dockview-theme-dracula-mixin {
@include dockview-theme-core-mixin(); @include dockview-theme-core-mixin();
@include dockview-drop-target-no-travel();
// //
--dv-group-view-background-color: #282a36; --dv-group-view-background-color: #282a36;
// //
@ -181,7 +216,7 @@
.dv-groupview { .dv-groupview {
&.dv-active-group { &.dv-active-group {
> .dv-tabs-and-actions-container { > .dv-tabs-and-actions-container {
> .dv-tabs-container { .dv-tabs-container {
> .dv-tab.dv-active-tab { > .dv-tab.dv-active-tab {
position: relative; position: relative;
@ -201,7 +236,7 @@
} }
&.dv-inactive-group { &.dv-inactive-group {
> .dv-tabs-and-actions-container { > .dv-tabs-and-actions-container {
> .dv-tabs-container { .dv-tabs-container {
> .dv-tab.dv-active-tab { > .dv-tab.dv-active-tab {
position: relative; position: relative;
@ -231,10 +266,17 @@
} }
@mixin dockview-design-replit-mixin { @mixin dockview-design-replit-mixin {
@include dockview-drop-target-no-travel();
.dv-resize-container:has(> .dv-groupview) { .dv-resize-container:has(> .dv-groupview) {
border-radius: 8px; border-radius: 8px;
} }
.dv-resize-container {
border-radius: 10px !important;
border: none;
}
.dv-groupview { .dv-groupview {
overflow: hidden; overflow: hidden;
border-radius: 10px; border-radius: 10px;
@ -268,59 +310,16 @@
border: 1px solid transparent; 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 { .dockview-theme-replit {
@include dockview-theme-core-mixin(); @include dockview-theme-core-mixin();
@include dockview-design-replit-mixin(); @include dockview-design-replit-mixin();
@include dockview-design-handle-mixin();
padding: 10px;
background-color: #ebeced;
// //
--dv-group-view-background-color: #ebeced; --dv-group-view-background-color: #ebeced;
// //
@ -341,6 +340,117 @@
--dv-paneview-header-border-color: rgb(51, 51, 51); --dv-paneview-header-border-color: rgb(51, 51, 51);
///// /////
--dv-separator-handle-background-color: #cfd1d3; --dv-sash-color: #cfd1d3;
--dv-separator-handle-hover-background-color: #babbbb; --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", "name": "dockview-react",
"version": "3.2.0", "version": "4.2.2",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews", "description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"keywords": [ "keywords": [
"splitview", "splitview",
@ -54,6 +54,6 @@
"test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview-react --coverage" "test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview-react --coverage"
}, },
"dependencies": { "dependencies": {
"dockview": "^3.2.0" "dockview": "^4.2.2"
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ export const CloseButton = () => (
viewBox="0 0 28 28" viewBox="0 0 28 28"
aria-hidden={'false'} aria-hidden={'false'}
focusable={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> <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> </svg>
@ -21,7 +21,7 @@ export const ExpandMore = () => {
viewBox="0 0 24 15" viewBox="0 0 24 15"
aria-hidden={'false'} aria-hidden={'false'}
focusable={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" /> <path d="M12 14.15L0 2.15L2.15 0L12 9.9L21.85 0.0499992L24 2.2L12 14.15Z" />
</svg> </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 # 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 ```tsx
<DockviewReact <DockviewReact
{...props} {...props}
rootOverlayModel={{ dndEdges={{
size: { value: 100, type: 'pixels' }, size: { value: 100, type: 'pixels' },
activationSize: { value: 5, type: 'percentage' }, activationSize: { value: 5, type: 'percentage' },
}} }}

View File

@ -13,6 +13,8 @@ Panels can be added through the dock api.
<DocRef declaration="DockviewApi" methods={['addPanel']} /> <DocRef declaration="DockviewApi" methods={['addPanel']} />
## Opening a Basic Panel ## Opening a Basic Panel
To open a panel requires a unique `id` and the name of the `component` to render. 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 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. 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 #### Relative to another Panel
```ts ```ts

View File

@ -6,8 +6,10 @@ title: Theme
import { CSSVariablesTable, ThemeTable } from '@site/src/components/cssVariables'; 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`: Firstly, you should import `dockview.css`:
@ -32,15 +34,34 @@ Firstly, you should import `dockview.css`:
## Provided themes ## 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). `dockview` comes with a number of built-in themes. Each theme is represented as an object that can be imported.
To use a `dockview` theme the CSS must encapsulate the component. The current list of themes is:
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/> <ThemeTable/>
:::info :::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 ## 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 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", "name": "dockview-docs",
"version": "3.2.0", "version": "4.2.2",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "npm run build-templates && docusaurus build", "build": "npm run build-templates && docusaurus build",
@ -38,7 +38,7 @@
"ag-grid-react": "^31.0.2", "ag-grid-react": "^31.0.2",
"axios": "^1.6.3", "axios": "^1.6.3",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"dockview": "^3.2.0", "dockview": "^4.2.2",
"prism-react-renderer": "^2.3.1", "prism-react-renderer": "^2.3.1",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-laag": "^2.0.5", "react-laag": "^2.0.5",

View File

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

View File

@ -5,8 +5,10 @@ import {
IDockviewPanelHeaderProps, IDockviewPanelHeaderProps,
IDockviewPanelProps, IDockviewPanelProps,
DockviewApi, DockviewApi,
DockviewTheme,
} from 'dockview'; } from 'dockview';
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import './app.scss'; import './app.scss';
import { defaultConfig } from './defaultLayout'; import { defaultConfig } from './defaultLayout';
import { GridActions } from './gridActions'; 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 = { const components = {
default: (props: IDockviewPanelProps) => { default: (props: IDockviewPanelProps) => {
const isDebug = React.useContext(DebugContext); const isDebug = React.useContext(DebugContext);
@ -80,6 +96,7 @@ const components = {
); );
}, },
nested: (props: IDockviewPanelProps) => { nested: (props: IDockviewPanelProps) => {
const theme = React.useContext(ThemeContext);
return ( return (
<DockviewReact <DockviewReact
components={components} components={components}
@ -95,7 +112,7 @@ const components = {
console.log('remove', e); console.log('remove', e);
}); });
}} }}
className={'dockview-theme-abyss'} theme={theme}
/> />
); );
}, },
@ -108,6 +125,7 @@ const components = {
} }
}} }}
style={{ style={{
border: 'none',
width: '100%', width: '100%',
height: '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 = { const headerComponents = {
@ -141,7 +186,9 @@ const WatermarkComponent = () => {
return <div>custom watermark</div>; 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< const [logLines, setLogLines] = React.useState<
{ text: string; timestamp?: Date; backgroundColor?: string }[] { text: string; timestamp?: Date; backgroundColor?: string }[]
>([]); >([]);
@ -380,18 +427,22 @@ const DockviewDemo = (props: { theme?: string }) => {
}} }}
> >
<DebugContext.Provider value={debug}> <DebugContext.Provider value={debug}>
<DockviewReact <ThemeContext.Provider value={props.theme}>
components={components} <DockviewReact
defaultTabComponent={headerComponents.default} components={components}
rightHeaderActionsComponent={RightControls} defaultTabComponent={headerComponents.default}
leftHeaderActionsComponent={LeftControls} rightHeaderActionsComponent={RightControls}
prefixHeaderActionsComponent={PrefixHeaderControls} leftHeaderActionsComponent={LeftControls}
watermarkComponent={ prefixHeaderActionsComponent={
watermark ? WatermarkComponent : undefined PrefixHeaderControls
} }
onReady={onReady} watermarkComponent={
className={props.theme || 'dockview-theme-abyss'} watermark ? WatermarkComponent : undefined
/> }
onReady={onReady}
theme={props.theme}
/>
</ThemeContext.Provider>
</DebugContext.Provider> </DebugContext.Provider>
</div> </div>

View File

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

View File

@ -2,7 +2,7 @@ import { DockviewApi } from 'dockview';
import * as React from 'react'; import * as React from 'react';
import { defaultConfig, nextId } from './defaultLayout'; import { defaultConfig, nextId } from './defaultLayout';
import { createRoot, Root } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { PanelBuilder } from './panelBuilder'; import { PanelBuilder } from './panelBuilder';
let mount = document.querySelector('.popover-anchor') as HTMLElement | null; let mount = document.querySelector('.popover-anchor') as HTMLElement | null;
@ -151,12 +151,6 @@ export const GridActions = (props: {
props.api?.addGroup(); props.api?.addGroup();
}; };
const [gap, setGap] = React.useState(0);
React.useEffect(() => {
props.api?.setGap(gap);
}, [gap, props.api]);
return ( return (
<div className="action-container"> <div className="action-container">
<div className="button-group"> <div className="button-group">
@ -204,19 +198,6 @@ export const GridActions = (props: {
Reset Reset
</button> </button>
<span style={{ flexGrow: 1 }} /> <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> </div>
); );
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -192,6 +192,8 @@ function filter(docs: TypeSystem.Type, methods: string[]) {
.map((v) => filter(v, methods)) .map((v) => filter(v, methods))
.flat(); .flat();
} }
return [docs];
} }
if (docs.kind === 'class' || docs.kind === 'interface') { 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 = [ export const themeConfig = [
{ {
id: 'dockview-theme-dark', id: themeDark,
key: '**[dockview-theme-dark](/demo?theme=dockview-theme-dark)**', key: '**[Dark](/demo?theme=dark)**',
text: '', text: '',
}, },
{ {
id: 'dockview-theme-light', id: themeLight,
key: '**[dockview-theme-light](/demo?theme=dockview-theme-light)**', key: '**[Light](/demo?theme=light)**',
text: '', text: '',
}, },
{ {
id: 'dockview-theme-vs', id: themeVisualStudio,
key: '**[dockview-theme-vs](/demo?theme=dockview-theme-vs)**', key: '**[Visual Studio](/demo?theme=visualStudio)**',
text: 'Based on [Visual Studio](https://visualstudio.microsoft.com)', text: 'Based on [Visual Studio](https://visualstudio.microsoft.com)',
}, },
{ {
id: 'dockview-theme-abyss', id: themeAbyss,
key: '**[dockview-theme-abyss](/demo?theme=dockview-theme-abyss)**', key: '**[Abyss](/demo?theme=abyss)**',
text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) abyss theme', text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) abyss theme',
}, },
{ {
id: 'dockview-theme-dracula', id: themeDracula,
key: '**[dockview-theme-dracula](/demo?theme=dockview-theme-dracula)**', key: '**[Dracula](/demo?theme=dracula)**',
text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) dracula theme', text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) dracula theme',
}, },
{ {
id: 'dockview-theme-replit', id: themeReplit,
key: '**[dockview-theme-replit](/demo?theme=dockview-theme-replit)**', key: '**[Replit](/demo?theme=replit)**',
text: 'Based on [Replit](https://replit.com)', 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. */ /* You can override the default Infima variables here. */
:root { :root {
--ifm-font-family-base: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, --ifm-font-family-base: 'IBM Plex Sans', ui-sans-serif, system-ui,
BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue,
sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji,
Noto Color Emoji; Segoe UI Symbol, Noto Color Emoji;
--ifm-font-weight-bold: 600; --ifm-font-weight-bold: 600;
@ -36,6 +36,9 @@
--ifm-color-primary: black; --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-color: white;
--ifm-navbar-link-hover-color: white; --ifm-navbar-link-hover-color: white;
@ -54,15 +57,18 @@
} }
/* --ifm-color-primary: #0c111d; */ /* --ifm-color-primary: #0c111d; */
--ifm-color-primary: #25c2a0; --ifm-color-primary: #98a2b3;
--ifm-color-primary-dark: #21af90; --ifm-color-primary-dark: #828a99;
--ifm-color-primary-darker: #1fa588; --ifm-color-primary-darker: #6a707c;
--ifm-color-primary-darkest: #1a8870; --ifm-color-primary-darkest: #474b53;
--ifm-color-primary-light: #29d5b0; --ifm-color-primary-light: #acb7ca;
--ifm-color-primary-lighter: #32d8b4; --ifm-color-primary-lighter: #bcc9df;
--ifm-color-primary-lightest: #4fddbf; --ifm-color-primary-lightest: #d2e1fa;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); --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; --dv-docs-markdown-text-color: #cdced8;
} }

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