Merge branch 'master' of https://github.com/mathuo/dockview into 460-locked-mode-prevent-all-mouse-resizing

This commit is contained in:
mathuo 2024-01-30 19:46:09 +00:00
commit 1d7ab22027
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
17 changed files with 572 additions and 247 deletions

View File

@ -2,8 +2,6 @@ name: Deploy Docs
on: on:
workflow_dispatch: workflow_dispatch:
schedule:
- cron: '0 3 * * *' # every day at 3 am UTC
jobs: jobs:
deploy-nightly-demo-app: deploy-nightly-demo-app:
@ -14,7 +12,7 @@ jobs:
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: '18.x' node-version: '20.x'
- uses: actions/cache@v3 - uses: actions/cache@v3
with: with:

View File

@ -14,7 +14,7 @@ jobs:
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: '18.x' node-version: '20.x'
- uses: actions/cache@v3 - uses: actions/cache@v3
with: with:

View File

@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: '18.x' node-version: '20.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- uses: actions/cache@v3 - uses: actions/cache@v3
@ -46,7 +46,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: '18.x' node-version: '20.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- uses: actions/cache@v3 - uses: actions/cache@v3

View File

@ -62,16 +62,19 @@ describe('contentContainer', () => {
const disposable = new CompositeDisposable(); const disposable = new CompositeDisposable();
const dockviewComponent = jest.fn<DockviewComponent, []>(() => { const overlayRenderContainer = new OverlayRenderContainer(
return { document.createElement('div')
renderer: 'onlyWhenVisibile', );
overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div')
),
} as DockviewComponent;
});
const cut = new ContentContainer(dockviewComponent(), jest.fn() as any); const cut = new ContentContainer(
fromPartial<DockviewComponent>({
renderer: 'onlyWhenVisibile',
overlayRenderContainer,
}),
fromPartial<DockviewGroupPanelModel>({
renderContainer: overlayRenderContainer,
})
);
disposable.addDisposables( disposable.addDisposables(
cut.onDidFocus(() => { cut.onDidFocus(() => {
@ -84,12 +87,12 @@ describe('contentContainer', () => {
const contentRenderer = new TestContentRenderer('id-1'); const contentRenderer = new TestContentRenderer('id-1');
const panel = { const panel = fromPartial<IDockviewPanel>({
view: { view: {
content: contentRenderer, content: contentRenderer,
} as Partial<IDockviewPanelModel>, },
api: { renderer: 'onlyWhenVisibile' }, api: { renderer: 'onlyWhenVisibile' },
} as Partial<IDockviewPanel>; });
cut.openPanel(panel as IDockviewPanel); cut.openPanel(panel as IDockviewPanel);
@ -151,13 +154,17 @@ describe('contentContainer', () => {
}); });
test("that panels renderered as 'onlyWhenVisibile' are removed when closed", () => { test("that panels renderered as 'onlyWhenVisibile' are removed when closed", () => {
const overlayRenderContainer = fromPartial<OverlayRenderContainer>({
detatch: jest.fn(),
});
const cut = new ContentContainer( const cut = new ContentContainer(
fromPartial<DockviewComponent>({ fromPartial<DockviewComponent>({
overlayRenderContainer: { overlayRenderContainer,
detatch: jest.fn(),
},
}), }),
fromPartial<DockviewGroupPanelModel>({}) fromPartial<DockviewGroupPanelModel>({
renderContainer: overlayRenderContainer,
})
); );
const panel1 = fromPartial<IDockviewPanel>({ const panel1 = fromPartial<IDockviewPanel>({

View File

@ -1,6 +1,5 @@
import { DockviewComponent } from '../../dockview/dockviewComponent'; import { DockviewComponent } from '../../dockview/dockviewComponent';
import { import {
DockviewDropTargets,
GroupPanelPartInitParameters, GroupPanelPartInitParameters,
IContentRenderer, IContentRenderer,
ITabRenderer, ITabRenderer,
@ -2974,7 +2973,7 @@ describe('dockviewComponent', () => {
expect(showDndOverlay).toHaveBeenCalledWith({ expect(showDndOverlay).toHaveBeenCalledWith({
nativeEvent: eventLeft, nativeEvent: eventLeft,
position: 'left', position: 'left',
target: DockviewDropTargets.Edge, target: 'edge',
getData: getPanelData, getData: getPanelData,
}); });
expect(showDndOverlay).toBeCalledTimes(1); expect(showDndOverlay).toBeCalledTimes(1);
@ -2993,7 +2992,7 @@ describe('dockviewComponent', () => {
expect(showDndOverlay).toHaveBeenCalledWith({ expect(showDndOverlay).toHaveBeenCalledWith({
nativeEvent: eventRight, nativeEvent: eventRight,
position: 'right', position: 'right',
target: DockviewDropTargets.Edge, target: 'edge',
getData: getPanelData, getData: getPanelData,
}); });
expect(showDndOverlay).toBeCalledTimes(2); expect(showDndOverlay).toBeCalledTimes(2);
@ -3012,7 +3011,7 @@ describe('dockviewComponent', () => {
expect(showDndOverlay).toHaveBeenCalledWith({ expect(showDndOverlay).toHaveBeenCalledWith({
nativeEvent: eventTop, nativeEvent: eventTop,
position: 'top', position: 'top',
target: DockviewDropTargets.Edge, target: 'edge',
getData: getPanelData, getData: getPanelData,
}); });
expect(showDndOverlay).toBeCalledTimes(3); expect(showDndOverlay).toBeCalledTimes(3);
@ -3031,7 +3030,7 @@ describe('dockviewComponent', () => {
expect(showDndOverlay).toHaveBeenCalledWith({ expect(showDndOverlay).toHaveBeenCalledWith({
nativeEvent: eventBottom, nativeEvent: eventBottom,
position: 'bottom', position: 'bottom',
target: DockviewDropTargets.Edge, target: 'edge',
getData: getPanelData, getData: getPanelData,
}); });
expect(showDndOverlay).toBeCalledTimes(4); expect(showDndOverlay).toBeCalledTimes(4);
@ -3067,7 +3066,7 @@ describe('dockviewComponent', () => {
expect(showDndOverlay).toHaveBeenCalledWith({ expect(showDndOverlay).toHaveBeenCalledWith({
nativeEvent: eventTop, nativeEvent: eventTop,
position: 'center', position: 'center',
target: DockviewDropTargets.Edge, target: 'edge',
getData: getPanelData, getData: getPanelData,
}); });
expect(showDndOverlay).toBeCalledTimes(5); expect(showDndOverlay).toBeCalledTimes(5);
@ -4435,44 +4434,12 @@ describe('dockviewComponent', () => {
cb(); cb();
} }
}), }),
removeEventListener: jest.fn(),
close: jest.fn(), close: jest.fn(),
}) })
); );
}); });
test('that can remove a popout group', async () => {
const container = document.createElement('div');
const dockview = new DockviewComponent({
parentElement: container,
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.layout(1000, 500);
const panel1 = dockview.addPanel({
id: 'panel_1',
component: 'default',
});
await dockview.addPopoutGroup(panel1);
expect(dockview.panels.length).toBe(1);
expect(dockview.groups.length).toBe(2);
expect(panel1.api.group.api.location.type).toBe('popout');
dockview.removePanel(panel1);
expect(dockview.panels.length).toBe(0);
expect(dockview.groups.length).toBe(0);
});
test('add a popout group', async () => { test('add a popout group', async () => {
const container = document.createElement('div'); const container = document.createElement('div');
@ -4512,6 +4479,39 @@ describe('dockviewComponent', () => {
expect(dockview.panels.length).toBe(2); expect(dockview.panels.length).toBe(2);
}); });
test('that can remove a popout group', async () => {
const container = document.createElement('div');
const dockview = new DockviewComponent({
parentElement: container,
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.layout(1000, 500);
const panel1 = dockview.addPanel({
id: 'panel_1',
component: 'default',
});
await dockview.addPopoutGroup(panel1);
expect(dockview.panels.length).toBe(1);
expect(dockview.groups.length).toBe(2);
expect(panel1.api.group.api.location.type).toBe('popout');
dockview.removePanel(panel1);
expect(dockview.panels.length).toBe(0);
expect(dockview.groups.length).toBe(0);
});
test('move from fixed to popout group and back', async () => { test('move from fixed to popout group and back', async () => {
const container = document.createElement('div'); const container = document.createElement('div');

View File

@ -1,5 +1,4 @@
import { import {
DockviewDropEvent,
IDockviewComponent, IDockviewComponent,
SerializedDockview, SerializedDockview,
} from '../dockview/dockviewComponent'; } from '../dockview/dockviewComponent';
@ -43,6 +42,11 @@ import {
TabDragEvent, TabDragEvent,
} from '../dockview/components/titlebar/tabsContainer'; } from '../dockview/components/titlebar/tabsContainer';
import { Box } from '../types'; import { Box } from '../types';
import {
DockviewDidDropEvent,
DockviewWillDropEvent,
WillShowOverlayLocationEvent,
} from '../dockview/dockviewGroupPanelModel';
export interface CommonApi<T = any> { export interface CommonApi<T = any> {
readonly height: number; readonly height: number;
@ -648,10 +652,27 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
/** /**
* Invoked when a Drag'n'Drop event occurs that the component was unable to handle. Exposed for custom Drag'n'Drop functionality. * Invoked when a Drag'n'Drop event occurs that the component was unable to handle. Exposed for custom Drag'n'Drop functionality.
*/ */
get onDidDrop(): Event<DockviewDropEvent> { get onDidDrop(): Event<DockviewDidDropEvent> {
return this.component.onDidDrop; return this.component.onDidDrop;
} }
/**
* Invoked when a Drag'n'Drop event occurs but before dockview handles it giving the user an opportunity to intecept and
* prevent the event from occuring using the standard `preventDefault()` syntax.
*
* Preventing certain events may causes unexpected behaviours, use carefully.
*/
get onWillDrop(): Event<DockviewWillDropEvent> {
return this.component.onWillDrop;
}
/**
*
*/
get onWillShowOverlay(): Event<WillShowOverlayLocationEvent> {
return this.component.onWillShowOverlay;
}
/** /**
* Invoked before a group is dragged. Exposed for custom Drag'n'Drop functionality. * Invoked before a group is dragged. Exposed for custom Drag'n'Drop functionality.
*/ */

View File

@ -1,10 +1,37 @@
import { toggleClass } from '../dom'; import { toggleClass } from '../dom';
import { Emitter, Event } from '../events'; import { DockviewEvent, Emitter, Event } from '../events';
import { CompositeDisposable } from '../lifecycle'; import { CompositeDisposable } from '../lifecycle';
import { DragAndDropObserver } from './dnd'; import { DragAndDropObserver } from './dnd';
import { clamp } from '../math'; import { clamp } from '../math';
import { Direction } from '../gridview/baseComponentGridview'; import { Direction } from '../gridview/baseComponentGridview';
export interface DroptargetEvent {
readonly position: Position;
readonly nativeEvent: DragEvent;
}
export class WillShowOverlayEvent
extends DockviewEvent
implements DroptargetEvent
{
get nativeEvent(): DragEvent {
return this.options.nativeEvent;
}
get position(): Position {
return this.options.position;
}
constructor(
private readonly options: {
nativeEvent: DragEvent;
position: Position;
}
) {
super();
}
}
export function directionToPosition(direction: Direction): Position { export function directionToPosition(direction: Direction): Position {
switch (direction) { switch (direction) {
case 'above': case 'above':
@ -39,11 +66,6 @@ export function positionToDirection(position: Position): Direction {
} }
} }
export interface DroptargetEvent {
readonly position: Position;
readonly nativeEvent: DragEvent;
}
export type Position = 'top' | 'bottom' | 'left' | 'right' | 'center'; export type Position = 'top' | 'bottom' | 'left' | 'right' | 'center';
export type CanDisplayOverlay = export type CanDisplayOverlay =
@ -70,6 +92,12 @@ 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 DroptargetOptions {
canDisplayOverlay: CanDisplayOverlay;
acceptedTargetZones: Position[];
overlayModel?: DroptargetOverlayModel;
}
export class Droptarget extends CompositeDisposable { export class Droptarget extends CompositeDisposable {
private targetElement: HTMLElement | undefined; private targetElement: HTMLElement | undefined;
private overlayElement: HTMLElement | undefined; private overlayElement: HTMLElement | undefined;
@ -79,6 +107,10 @@ export class Droptarget extends CompositeDisposable {
private readonly _onDrop = new Emitter<DroptargetEvent>(); private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event; readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
private readonly _onWillShowOverlay = new Emitter<WillShowOverlayEvent>();
readonly onWillShowOverlay: Event<WillShowOverlayEvent> =
this._onWillShowOverlay.event;
readonly dnd: DragAndDropObserver; readonly dnd: DragAndDropObserver;
private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__'; private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__';
@ -89,11 +121,7 @@ export class Droptarget extends CompositeDisposable {
constructor( constructor(
private readonly element: HTMLElement, private readonly element: HTMLElement,
private readonly options: { private readonly options: DroptargetOptions
canDisplayOverlay: CanDisplayOverlay;
acceptedTargetZones: Position[];
overlayModel?: DroptargetOverlayModel;
}
) { ) {
super(); super();
@ -142,6 +170,22 @@ export class Droptarget extends CompositeDisposable {
return; return;
} }
const willShowOverlayEvent = new WillShowOverlayEvent({
nativeEvent: e,
position: quadrant,
});
/**
* Provide an opportunity to prevent the overlay appearing and in turn
* any dnd behaviours
*/
this._onWillShowOverlay.fire(willShowOverlayEvent);
if (willShowOverlayEvent.defaultPrevented) {
this.removeDropTarget();
return;
}
if (typeof this.options.canDisplayOverlay === 'boolean') { if (typeof this.options.canDisplayOverlay === 'boolean') {
if (!this.options.canDisplayOverlay) { if (!this.options.canDisplayOverlay) {
this.removeDropTarget(); this.removeDropTarget();
@ -192,7 +236,7 @@ export class Droptarget extends CompositeDisposable {
}, },
}); });
this.addDisposables(this._onDrop, this.dnd); this.addDisposables(this._onDrop, this._onWillShowOverlay, this.dnd);
} }
setTargetZones(acceptedTargetZones: Position[]): void { setTargetZones(acceptedTargetZones: Position[]): void {

View File

@ -10,7 +10,6 @@ import { DockviewComponent } from '../../dockviewComponent';
import { Droptarget } from '../../../dnd/droptarget'; import { Droptarget } from '../../../dnd/droptarget';
import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel'; import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel';
import { getPanelData } from '../../../dnd/dataTransfer'; import { getPanelData } from '../../../dnd/dataTransfer';
import { DockviewDropTargets } from '../../types';
export interface IContentContainer extends IDisposable { export interface IContentContainer extends IDisposable {
readonly dropTarget: Droptarget; readonly dropTarget: Droptarget;
@ -95,11 +94,7 @@ export class ContentContainer
return !groupHasOnePanelAndIsActiveDragElement; return !groupHasOnePanelAndIsActiveDragElement;
} }
return this.group.canDisplayOverlay( return this.group.canDisplayOverlay(event, position, 'panel');
event,
position,
DockviewDropTargets.Panel
);
}, },
}); });
@ -138,7 +133,7 @@ export class ContentContainer
switch (panel.api.renderer) { switch (panel.api.renderer) {
case 'onlyWhenVisibile': case 'onlyWhenVisibile':
this.accessor.overlayRenderContainer.detatch(panel); this.group.renderContainer.detatch(panel);
if (this.panel) { if (this.panel) {
if (doRender) { if (doRender) {
this._element.appendChild( this._element.appendChild(
@ -154,7 +149,7 @@ export class ContentContainer
) { ) {
this._element.removeChild(panel.view.content.element); this._element.removeChild(panel.view.content.element);
} }
container = this.accessor.overlayRenderContainer.attach({ container = this.group.renderContainer.attach({
panel, panel,
referenceContainer: this, referenceContainer: this,
}); });

View File

@ -7,9 +7,14 @@ import {
} from '../../../dnd/dataTransfer'; } from '../../../dnd/dataTransfer';
import { toggleClass } from '../../../dom'; import { toggleClass } from '../../../dom';
import { DockviewComponent } from '../../dockviewComponent'; import { DockviewComponent } from '../../dockviewComponent';
import { DockviewDropTargets, ITabRenderer } from '../../types'; import { ITabRenderer } from '../../types';
import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget'; import {
DroptargetEvent,
Droptarget,
Position,
WillShowOverlayEvent,
} from '../../../dnd/droptarget';
import { DragHandler } from '../../../dnd/abstractDragHandler'; import { DragHandler } from '../../../dnd/abstractDragHandler';
import { IDockviewPanel } from '../../dockviewPanel'; import { IDockviewPanel } from '../../dockviewPanel';
@ -40,18 +45,9 @@ class TabDragHandler extends DragHandler {
} }
} }
export interface ITab extends IDisposable { export class Tab extends CompositeDisposable {
readonly panel: IDockviewPanel;
readonly element: HTMLElement;
setContent: (element: ITabRenderer) => void;
onChanged: Event<MouseEvent>;
onDrop: Event<DroptargetEvent>;
setActive(isActive: boolean): void;
}
export class Tab extends CompositeDisposable implements ITab {
private readonly _element: HTMLElement; private readonly _element: HTMLElement;
private readonly droptarget: Droptarget; private readonly dropTarget: Droptarget;
private content: ITabRenderer | undefined = undefined; private content: ITabRenderer | undefined = undefined;
private readonly _onChanged = new Emitter<MouseEvent>(); private readonly _onChanged = new Emitter<MouseEvent>();
@ -63,6 +59,8 @@ export class Tab extends CompositeDisposable implements ITab {
private readonly _onDragStart = new Emitter<DragEvent>(); private readonly _onDragStart = new Emitter<DragEvent>();
readonly onDragStart = this._onDragStart.event; readonly onDragStart = this._onDragStart.event;
readonly onWillShowOverlay: Event<WillShowOverlayEvent>;
public get element(): HTMLElement { public get element(): HTMLElement {
return this._element; return this._element;
} }
@ -88,7 +86,7 @@ export class Tab extends CompositeDisposable implements ITab {
this.panel this.panel
); );
this.droptarget = new Droptarget(this._element, { this.dropTarget = new Droptarget(this._element, {
acceptedTargetZones: ['center'], acceptedTargetZones: ['center'],
canDisplayOverlay: (event, position) => { canDisplayOverlay: (event, position) => {
if (this.group.locked) { if (this.group.locked) {
@ -112,11 +110,13 @@ export class Tab extends CompositeDisposable implements ITab {
return this.group.model.canDisplayOverlay( return this.group.model.canDisplayOverlay(
event, event,
position, position,
DockviewDropTargets.Tab 'tab'
); );
}, },
}); });
this.onWillShowOverlay = this.dropTarget.onWillShowOverlay;
this.addDisposables( this.addDisposables(
this._onChanged, this._onChanged,
this._onDropped, this._onDropped,
@ -132,10 +132,10 @@ export class Tab extends CompositeDisposable implements ITab {
this._onChanged.fire(event); this._onChanged.fire(event);
}), }),
this.droptarget.onDrop((event) => { this.dropTarget.onDrop((event) => {
this._onDropped.fire(event); this._onDropped.fire(event);
}), }),
this.droptarget this.dropTarget
); );
} }

View File

@ -4,12 +4,14 @@ import {
IValueDisposable, IValueDisposable,
} from '../../../lifecycle'; } from '../../../lifecycle';
import { addDisposableListener, Emitter, Event } from '../../../events'; import { addDisposableListener, Emitter, Event } from '../../../events';
import { ITab, Tab } from '../tab/tab'; import { Tab } from '../tab/tab';
import { DockviewComponent } from '../../dockviewComponent';
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 { DockviewPanel, IDockviewPanel } from '../../dockviewPanel';
import { DockviewComponent } from '../../dockviewComponent';
import { WillShowOverlayEvent } from '../../../dnd/droptarget';
import { DockviewGroupDropLocation } from '../../dockviewGroupPanelModel';
export interface TabDropIndexEvent { export interface TabDropIndexEvent {
readonly event: DragEvent; readonly event: DragEvent;
@ -30,17 +32,21 @@ export interface ITabsContainer extends IDisposable {
readonly element: HTMLElement; readonly element: HTMLElement;
readonly panels: string[]; readonly panels: string[];
readonly size: number; readonly size: number;
readonly onDrop: Event<TabDropIndexEvent>;
readonly onTabDragStart: Event<TabDragEvent>;
readonly onGroupDragStart: Event<GroupDragEvent>;
readonly onWillShowOverlay: Event<{
event: WillShowOverlayEvent;
kind: DockviewGroupDropLocation;
}>;
hidden: boolean; hidden: boolean;
delete: (id: string) => void; delete(id: string): void;
indexOf: (id: string) => number; indexOf(id: string): number;
onDrop: Event<TabDropIndexEvent>; setActive(isGroupActive: boolean): void;
onTabDragStart: Event<TabDragEvent>; setActivePanel(panel: IDockviewPanel): void;
onGroupDragStart: Event<GroupDragEvent>; isActive(tab: Tab): boolean;
setActive: (isGroupActive: boolean) => void; closePanel(panel: IDockviewPanel): void;
setActivePanel: (panel: IDockviewPanel) => void; openPanel(panel: IDockviewPanel, index?: number): void;
isActive: (tab: ITab) => boolean;
closePanel: (panel: IDockviewPanel) => void;
openPanel: (panel: IDockviewPanel, index?: number) => void;
setRightActionsElement(element: HTMLElement | undefined): void; setRightActionsElement(element: HTMLElement | undefined): void;
setLeftActionsElement(element: HTMLElement | undefined): void; setLeftActionsElement(element: HTMLElement | undefined): void;
setPrefixActionsElement(element: HTMLElement | undefined): void; setPrefixActionsElement(element: HTMLElement | undefined): void;
@ -59,7 +65,7 @@ export class TabsContainer
private readonly preActionsContainer: HTMLElement; private readonly preActionsContainer: HTMLElement;
private readonly voidContainer: VoidContainer; private readonly voidContainer: VoidContainer;
private tabs: IValueDisposable<ITab>[] = []; private tabs: IValueDisposable<Tab>[] = [];
private selectedIndex = -1; private selectedIndex = -1;
private rightActions: HTMLElement | undefined; private rightActions: HTMLElement | undefined;
private leftActions: HTMLElement | undefined; private leftActions: HTMLElement | undefined;
@ -77,6 +83,15 @@ export class TabsContainer
readonly onGroupDragStart: Event<GroupDragEvent> = readonly onGroupDragStart: Event<GroupDragEvent> =
this._onGroupDragStart.event; this._onGroupDragStart.event;
private readonly _onWillShowOverlay = new Emitter<{
event: WillShowOverlayEvent;
kind: DockviewGroupDropLocation;
}>();
readonly onWillShowOverlay: Event<{
event: WillShowOverlayEvent;
kind: DockviewGroupDropLocation;
}> = this._onWillShowOverlay.event;
get panels(): string[] { get panels(): string[] {
return this.tabs.map((_) => _.value.panel.id); return this.tabs.map((_) => _.value.panel.id);
} }
@ -150,7 +165,7 @@ export class TabsContainer
return this._element; return this._element;
} }
public isActive(tab: ITab): boolean { public isActive(tab: Tab): boolean {
return ( return (
this.selectedIndex > -1 && this.selectedIndex > -1 &&
this.tabs[this.selectedIndex].value === tab this.tabs[this.selectedIndex].value === tab
@ -167,12 +182,6 @@ export class TabsContainer
) { ) {
super(); super();
this.addDisposables(
this._onDrop,
this._onTabDragStart,
this._onGroupDragStart
);
this._element = document.createElement('div'); this._element = document.createElement('div');
this._element.className = 'tabs-and-actions-container'; this._element.className = 'tabs-and-actions-container';
@ -182,27 +191,6 @@ export class TabsContainer
this.accessor.options.singleTabMode === 'fullwidth' this.accessor.options.singleTabMode === 'fullwidth'
); );
this.addDisposables(
this.accessor.onDidAddPanel((e) => {
if (e.api.group === this.group) {
toggleClass(
this._element,
'dv-single-tab',
this.size === 1
);
}
}),
this.accessor.onDidRemovePanel((e) => {
if (e.api.group === this.group) {
toggleClass(
this._element,
'dv-single-tab',
this.size === 1
);
}
})
);
this.rightActionsContainer = document.createElement('div'); this.rightActionsContainer = document.createElement('div');
this.rightActionsContainer.className = 'right-actions-container'; this.rightActionsContainer.className = 'right-actions-container';
@ -224,6 +212,28 @@ export class TabsContainer
this._element.appendChild(this.rightActionsContainer); this._element.appendChild(this.rightActionsContainer);
this.addDisposables( this.addDisposables(
this.accessor.onDidAddPanel((e) => {
if (e.api.group === this.group) {
toggleClass(
this._element,
'dv-single-tab',
this.size === 1
);
}
}),
this.accessor.onDidRemovePanel((e) => {
if (e.api.group === this.group) {
toggleClass(
this._element,
'dv-single-tab',
this.size === 1
);
}
}),
this._onWillShowOverlay,
this._onDrop,
this._onTabDragStart,
this._onGroupDragStart,
this.voidContainer, this.voidContainer,
this.voidContainer.onDragStart((event) => { this.voidContainer.onDragStart((event) => {
this._onGroupDragStart.fire({ this._onGroupDragStart.fire({
@ -237,6 +247,9 @@ export class TabsContainer
index: this.tabs.length, index: this.tabs.length,
}); });
}), }),
this.voidContainer.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire({ event, kind: 'header_space' });
}),
addDisposableListener( addDisposableListener(
this.voidContainer.element, this.voidContainer.element,
'mousedown', 'mousedown',
@ -286,7 +299,7 @@ export class TabsContainer
} }
private addTab( private addTab(
tab: IValueDisposable<ITab>, tab: IValueDisposable<Tab>,
index: number = this.tabs.length index: number = this.tabs.length
): void { ): void {
if (index < 0 || index > this.tabs.length) { if (index < 0 || index > this.tabs.length) {
@ -396,10 +409,13 @@ export class TabsContainer
event: event.nativeEvent, event: event.nativeEvent,
index: this.tabs.findIndex((x) => x.value === tab), index: this.tabs.findIndex((x) => x.value === tab),
}); });
}),
tab.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire({ event, kind: 'tab' });
}) })
); );
const value: IValueDisposable<ITab> = { value: tab, disposable }; const value: IValueDisposable<Tab> = { value: tab, disposable };
this.addTab(value, index); this.addTab(value, index);
} }

View File

@ -1,16 +1,19 @@
import { last } from '../../../array'; import { last } from '../../../array';
import { getPanelData } from '../../../dnd/dataTransfer'; import { getPanelData } from '../../../dnd/dataTransfer';
import { Droptarget, DroptargetEvent } from '../../../dnd/droptarget'; import {
Droptarget,
DroptargetEvent,
WillShowOverlayEvent,
} from '../../../dnd/droptarget';
import { GroupDragHandler } from '../../../dnd/groupDragHandler'; import { GroupDragHandler } from '../../../dnd/groupDragHandler';
import { DockviewComponent } from '../../dockviewComponent'; 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 { DockviewDropTargets } from '../../types';
export class VoidContainer extends CompositeDisposable { export class VoidContainer extends CompositeDisposable {
private readonly _element: HTMLElement; private readonly _element: HTMLElement;
private readonly voidDropTarget: Droptarget; private readonly dropTraget: Droptarget;
private readonly _onDrop = new Emitter<DroptargetEvent>(); private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event; readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
@ -18,6 +21,8 @@ export class VoidContainer extends CompositeDisposable {
private readonly _onDragStart = new Emitter<DragEvent>(); private readonly _onDragStart = new Emitter<DragEvent>();
readonly onDragStart = this._onDragStart.event; readonly onDragStart = this._onDragStart.event;
readonly onWillShowOverlay: Event<WillShowOverlayEvent>;
get element(): HTMLElement { get element(): HTMLElement {
return this._element; return this._element;
} }
@ -44,7 +49,7 @@ export class VoidContainer extends CompositeDisposable {
const handler = new GroupDragHandler(this._element, accessor, group); const handler = new GroupDragHandler(this._element, accessor, group);
this.voidDropTarget = new Droptarget(this._element, { this.dropTraget = new Droptarget(this._element, {
acceptedTargetZones: ['center'], acceptedTargetZones: ['center'],
canDisplayOverlay: (event, position) => { canDisplayOverlay: (event, position) => {
const data = getPanelData(); const data = getPanelData();
@ -62,23 +67,21 @@ export class VoidContainer extends CompositeDisposable {
return last(this.group.panels)?.id !== data.panelId; return last(this.group.panels)?.id !== data.panelId;
} }
return group.model.canDisplayOverlay( return group.model.canDisplayOverlay(event, position, 'panel');
event,
position,
DockviewDropTargets.Panel
);
}, },
}); });
this.onWillShowOverlay = this.dropTraget.onWillShowOverlay;
this.addDisposables( this.addDisposables(
handler, handler,
handler.onDragStart((event) => { handler.onDragStart((event) => {
this._onDragStart.fire(event); this._onDragStart.fire(event);
}), }),
this.voidDropTarget.onDrop((event) => { this.dropTraget.onDrop((event) => {
this._onDrop.fire(event); this._onDrop.fire(event);
}), }),
this.voidDropTarget this.dropTraget
); );
} }
} }

View File

@ -13,13 +13,9 @@ 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, IDisposable } from '../lifecycle'; import { CompositeDisposable, Disposable, IDisposable } from '../lifecycle';
import { Event, Emitter } from '../events'; import { Event, Emitter, addDisposableWindowListener } from '../events';
import { Watermark } from './components/watermark/watermark'; import { Watermark } from './components/watermark/watermark';
import { import { IWatermarkRenderer, GroupviewPanelState } from './types';
IWatermarkRenderer,
GroupviewPanelState,
DockviewDropTargets,
} from './types';
import { sequentialNumberGenerator } from '../math'; import { sequentialNumberGenerator } from '../math';
import { DefaultDockviewDeserialzier } from './deserializer'; import { DefaultDockviewDeserialzier } from './deserializer';
import { createComponent } from '../panel/componentFactory'; import { createComponent } from '../panel/componentFactory';
@ -44,7 +40,9 @@ import { Orientation, Sizing } from '../splitview/splitview';
import { import {
GroupOptions, GroupOptions,
GroupPanelViewState, GroupPanelViewState,
GroupviewDropEvent, DockviewDidDropEvent,
DockviewWillDropEvent,
WillShowOverlayLocationEvent,
} from './dockviewGroupPanelModel'; } from './dockviewGroupPanelModel';
import { DockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewPanelModel } from './dockviewPanelModel'; import { DockviewPanelModel } from './dockviewPanelModel';
@ -230,18 +228,16 @@ export type DockviewComponentUpdateOptions = Pick<
| 'disableFloatingGroups' | 'disableFloatingGroups'
| 'floatingGroupBounds' | 'floatingGroupBounds'
| 'rootOverlayModel' | 'rootOverlayModel'
| 'disableDnd'
>; >;
export interface DockviewDropEvent extends GroupviewDropEvent {
api: DockviewApi;
group: DockviewGroupPanel | null;
}
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 onDidDrop: Event<DockviewDropEvent>; readonly onDidDrop: Event<DockviewDidDropEvent>;
readonly onWillDrop: Event<DockviewWillDropEvent>;
readonly onWillShowOverlay: Event<WillShowOverlayLocationEvent>;
readonly orientation: Orientation; readonly orientation: Orientation;
updateOptions(options: DockviewComponentUpdateOptions): void; updateOptions(options: DockviewComponentUpdateOptions): void;
moveGroupOrPanel( moveGroupOrPanel(
@ -311,8 +307,16 @@ export class DockviewComponent
readonly onWillDragGroup: Event<GroupDragEvent> = readonly onWillDragGroup: Event<GroupDragEvent> =
this._onWillDragGroup.event; this._onWillDragGroup.event;
private readonly _onDidDrop = new Emitter<DockviewDropEvent>(); private readonly _onDidDrop = new Emitter<DockviewDidDropEvent>();
readonly onDidDrop: Event<DockviewDropEvent> = this._onDidDrop.event; readonly onDidDrop: Event<DockviewDidDropEvent> = this._onDidDrop.event;
private readonly _onWillDrop = new Emitter<DockviewWillDropEvent>();
readonly onWillDrop: Event<DockviewWillDropEvent> = this._onWillDrop.event;
private readonly _onWillShowOverlay =
new Emitter<WillShowOverlayLocationEvent>();
readonly onWillShowOverlay: Event<WillShowOverlayLocationEvent> =
this._onWillShowOverlay.event;
private readonly _onDidRemovePanel = new Emitter<IDockviewPanel>(); private readonly _onDidRemovePanel = new Emitter<IDockviewPanel>();
readonly onDidRemovePanel: Event<IDockviewPanel> = readonly onDidRemovePanel: Event<IDockviewPanel> =
@ -392,11 +396,13 @@ export class DockviewComponent
this.overlayRenderContainer, this.overlayRenderContainer,
this._onWillDragPanel, this._onWillDragPanel,
this._onWillDragGroup, this._onWillDragGroup,
this._onWillShowOverlay,
this._onDidActivePanelChange, this._onDidActivePanelChange,
this._onDidAddPanel, this._onDidAddPanel,
this._onDidRemovePanel, this._onDidRemovePanel,
this._onDidLayoutFromJSON, this._onDidLayoutFromJSON,
this._onDidDrop, this._onDidDrop,
this._onWillDrop,
Event.any( Event.any(
this.onDidAddGroup, this.onDidAddGroup,
this.onDidRemoveGroup this.onDidRemoveGroup
@ -475,7 +481,7 @@ export class DockviewComponent
return this.options.showDndOverlay({ return this.options.showDndOverlay({
nativeEvent: event, nativeEvent: event,
position: position, position: position,
target: DockviewDropTargets.Edge, target: 'edge',
getData: getPanelData, getData: getPanelData,
}); });
} }
@ -489,6 +495,22 @@ export class DockviewComponent
this.addDisposables( this.addDisposables(
this._rootDropTarget.onDrop((event) => { this._rootDropTarget.onDrop((event) => {
const willDropEvent = new DockviewWillDropEvent({
nativeEvent: event.nativeEvent,
position: event.position,
panel: undefined,
api: this._api,
group: undefined,
getData: getPanelData,
kind: 'content',
});
this._onWillDrop.fire(willDropEvent);
if (willDropEvent.defaultPrevented) {
return;
}
const data = getPanelData(); const data = getPanelData();
if (data) { if (data) {
@ -499,12 +521,16 @@ export class DockviewComponent
'center' 'center'
); );
} else { } else {
this._onDidDrop.fire({ this._onDidDrop.fire(
...event, new DockviewDidDropEvent({
api: this._api, nativeEvent: event.nativeEvent,
group: null, position: event.position,
getData: getPanelData, panel: undefined,
}); api: this._api,
group: undefined,
getData: getPanelData,
})
);
} }
}), }),
this._rootDropTarget this._rootDropTarget
@ -536,12 +562,15 @@ export class DockviewComponent
from: DockviewGroupPanel; from: DockviewGroupPanel;
to: DockviewGroupPanel; to: DockviewGroupPanel;
}) { }) {
const activePanel = options.from.activePanel;
const panels = [...options.from.panels].map((panel) => const panels = [...options.from.panels].map((panel) =>
options.from.model.removePanel(panel) options.from.model.removePanel(panel)
); );
panels.forEach((panel) => { panels.forEach((panel) => {
options.to.model.openPanel(panel); options.to.model.openPanel(panel, {
skipSetPanelActive: activePanel !== panel,
});
}); });
} }
@ -599,10 +628,18 @@ export class DockviewComponent
return; return;
} }
const gready = document.createElement('div');
gready.className = 'dv-overlay-render-container';
const overlayRenderContainer = new OverlayRenderContainer(
gready
);
const referenceGroup = const referenceGroup =
item instanceof DockviewPanel ? item.group : item; item instanceof DockviewPanel ? item.group : item;
const group = this.createGroup({ id: groupId }); const group = this.createGroup({ id: groupId });
group.model.renderContainer = overlayRenderContainer;
if (item instanceof DockviewPanel) { if (item instanceof DockviewPanel) {
const panel = referenceGroup.model.removePanel(item); const panel = referenceGroup.model.removePanel(item);
@ -612,9 +649,13 @@ export class DockviewComponent
from: referenceGroup, from: referenceGroup,
to: group, to: group,
}); });
referenceGroup.api.setHidden(false); referenceGroup.api.setHidden(true);
} }
popoutContainer.classList.add('dv-dockview');
popoutContainer.style.overflow = 'hidden';
popoutContainer.appendChild(gready);
popoutContainer.appendChild(group.element); popoutContainer.appendChild(group.element);
group.model.location = { group.model.location = {
@ -630,6 +671,19 @@ export class DockviewComponent
}; };
popoutWindowDisposable.addDisposables( popoutWindowDisposable.addDisposables(
/**
* ResizeObserver seems slow here, I do not know why but we don't need it
* since we can reply on the window resize event as we will occupy the full
* window dimensions
*/
addDisposableWindowListener(
_window.window!,
'resize',
() => {
group.layout(window.innerWidth, window.innerHeight);
}
),
overlayRenderContainer,
Disposable.from(() => { Disposable.from(() => {
if (this.getPanel(referenceGroup.id)) { if (this.getPanel(referenceGroup.id)) {
moveGroupWithoutDestroying({ moveGroupWithoutDestroying({
@ -641,12 +695,16 @@ export class DockviewComponent
referenceGroup.api.setHidden(false); referenceGroup.api.setHidden(false);
} }
this.doRemoveGroup(group); this.doRemoveGroup(group, {
skipPopoutAssociated: true,
});
} else { } else {
const removedGroup = this.doRemoveGroup(group, { const removedGroup = this.doRemoveGroup(group, {
skipDispose: true, skipDispose: true,
skipActive: true, skipActive: true,
}); });
removedGroup.model.renderContainer =
this.overlayRenderContainer;
removedGroup.model.location = { type: 'grid' }; removedGroup.model.location = { type: 'grid' };
this.doAddGroup(removedGroup, [0]); this.doAddGroup(removedGroup, [0]);
} }
@ -1459,6 +1517,7 @@ export class DockviewComponent
| { | {
skipActive?: boolean; skipActive?: boolean;
skipDispose?: boolean; skipDispose?: boolean;
skipPopoutAssociated?: boolean;
} }
| undefined | undefined
): DockviewGroupPanel { ): DockviewGroupPanel {
@ -1498,7 +1557,9 @@ export class DockviewComponent
if (selectedGroup) { if (selectedGroup) {
if (!options?.skipDispose) { if (!options?.skipDispose) {
this.doRemoveGroup(selectedGroup.referenceGroup); if (!options?.skipPopoutAssociated) {
this.removeGroup(selectedGroup.referenceGroup);
}
selectedGroup.popoutGroup.dispose(); selectedGroup.popoutGroup.dispose();
this._groups.delete(group.id); this._groups.delete(group.id);
@ -1753,11 +1814,18 @@ export class DockviewComponent
this.moveGroupOrPanel(view, groupId, itemId, target, index); this.moveGroupOrPanel(view, groupId, itemId, target, index);
}), }),
view.model.onDidDrop((event) => { view.model.onDidDrop((event) => {
this._onDidDrop.fire({ this._onDidDrop.fire(event);
...event, }),
api: this._api, view.model.onWillDrop((event) => {
group: view, this._onWillDrop.fire(event);
}); }),
view.model.onWillShowOverlay((event) => {
if (this.options.disableDnd) {
event.event.preventDefault();
return;
}
this._onWillShowOverlay.fire(event);
}), }),
view.model.onDidAddPanel((event) => { view.model.onDidAddPanel((event) => {
this._onDidAddPanel.fire(event.panel); this._onDidAddPanel.fire(event.panel);

View File

@ -1,9 +1,14 @@
import { DockviewApi } from '../api/component.api'; import { DockviewApi } from '../api/component.api';
import { getPanelData, PanelTransfer } from '../dnd/dataTransfer'; import { getPanelData, PanelTransfer } from '../dnd/dataTransfer';
import { Position } from '../dnd/droptarget'; import { Position, WillShowOverlayEvent } from '../dnd/droptarget';
import { DockviewComponent } from './dockviewComponent'; import { DockviewComponent } from './dockviewComponent';
import { isAncestor, toggleClass } from '../dom'; import { isAncestor, toggleClass } from '../dom';
import { addDisposableListener, Emitter, Event } from '../events'; import {
addDisposableListener,
DockviewEvent,
Emitter,
Event,
} from '../events';
import { IViewSize } from '../gridview/gridview'; import { IViewSize } from '../gridview/gridview';
import { CompositeDisposable } from '../lifecycle'; import { CompositeDisposable } from '../lifecycle';
import { IPanel, PanelInitParameters, PanelUpdateEvent } from '../panel/types'; import { IPanel, PanelInitParameters, PanelUpdateEvent } from '../panel/types';
@ -21,26 +26,7 @@ import { DockviewDropTargets, IWatermarkRenderer } from './types';
import { DockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewGroupPanel } from './dockviewGroupPanel';
import { IDockviewPanel } from './dockviewPanel'; import { IDockviewPanel } from './dockviewPanel';
import { IHeaderActionsRenderer } from './options'; import { IHeaderActionsRenderer } from './options';
import { OverlayRenderContainer } from '../overlayRenderContainer';
export interface DndService {
canDisplayOverlay(
group: IDockviewGroupPanelModel,
event: DragEvent,
target: DockviewDropTargets
): boolean;
onDrop(
group: IDockviewGroupPanelModel,
event: DragEvent,
position: Position,
index?: number
): void;
}
export interface IGroupItem {
id: string;
header: { element: HTMLElement };
body: { element: HTMLElement };
}
interface GroupMoveEvent { interface GroupMoveEvent {
groupId: string; groupId: string;
@ -66,15 +52,69 @@ export interface GroupPanelViewState extends CoreGroupOptions {
id: string; id: string;
} }
export interface GroupviewChangeEvent { export interface DockviewGroupChangeEvent {
readonly panel: IDockviewPanel; readonly panel: IDockviewPanel;
} }
export interface GroupviewDropEvent { export class DockviewDidDropEvent extends DockviewEvent {
readonly nativeEvent: DragEvent; get nativeEvent(): DragEvent {
readonly position: Position; return this.options.nativeEvent;
readonly index?: number; }
getData(): PanelTransfer | undefined;
get position(): Position {
return this.options.position;
}
get panel(): IDockviewPanel | undefined {
return this.options.panel;
}
get group(): DockviewGroupPanel | undefined {
return this.options.group;
}
get api(): DockviewApi {
return this.options.api;
}
constructor(
private readonly options: {
readonly nativeEvent: DragEvent;
readonly position: Position;
readonly panel?: IDockviewPanel;
getData(): PanelTransfer | undefined;
group?: DockviewGroupPanel;
api: DockviewApi;
}
) {
super();
}
getData(): PanelTransfer | undefined {
return this.options.getData();
}
}
export class DockviewWillDropEvent extends DockviewDidDropEvent {
private readonly _kind: DockviewGroupDropLocation;
get kind(): DockviewGroupDropLocation {
return this._kind;
}
constructor(options: {
readonly nativeEvent: DragEvent;
readonly position: Position;
readonly panel?: IDockviewPanel;
getData(): PanelTransfer | undefined;
kind: DockviewGroupDropLocation;
group?: DockviewGroupPanel;
api: DockviewApi;
}) {
super(options);
this._kind = options.kind;
}
} }
export interface IHeader { export interface IHeader {
@ -83,6 +123,8 @@ export interface IHeader {
export type DockviewGroupPanelLocked = boolean | 'no-drop-target'; export type DockviewGroupPanelLocked = boolean | 'no-drop-target';
export type DockviewGroupDropLocation = 'tab' | 'header_space' | 'content';
export interface IDockviewGroupPanelModel extends IPanel { export interface IDockviewGroupPanelModel extends IPanel {
readonly isActive: boolean; readonly isActive: boolean;
readonly size: number; readonly size: number;
@ -90,10 +132,11 @@ export interface IDockviewGroupPanelModel extends IPanel {
readonly activePanel: IDockviewPanel | undefined; readonly activePanel: IDockviewPanel | undefined;
readonly header: IHeader; readonly header: IHeader;
readonly isContentFocused: boolean; readonly isContentFocused: boolean;
readonly onDidDrop: Event<GroupviewDropEvent>; readonly onDidDrop: Event<DockviewDidDropEvent>;
readonly onDidAddPanel: Event<GroupviewChangeEvent>; readonly onWillDrop: Event<DockviewWillDropEvent>;
readonly onDidRemovePanel: Event<GroupviewChangeEvent>; readonly onDidAddPanel: Event<DockviewGroupChangeEvent>;
readonly onDidActivePanelChange: Event<GroupviewChangeEvent>; readonly onDidRemovePanel: Event<DockviewGroupChangeEvent>;
readonly onDidActivePanelChange: Event<DockviewGroupChangeEvent>;
readonly onMove: Event<GroupMoveEvent>; readonly onMove: Event<GroupMoveEvent>;
locked: DockviewGroupPanelLocked; locked: DockviewGroupPanelLocked;
setActive(isActive: boolean): void; setActive(isActive: boolean): void;
@ -135,6 +178,11 @@ export type DockviewGroupLocation =
| { type: 'floating' } | { type: 'floating' }
| { type: 'popout'; getWindow: () => Window }; | { type: 'popout'; getWindow: () => Window };
export interface WillShowOverlayLocationEvent {
event: WillShowOverlayEvent;
kind: DockviewGroupDropLocation;
}
export class DockviewGroupPanelModel export class DockviewGroupPanelModel
extends CompositeDisposable extends CompositeDisposable
implements IDockviewGroupPanelModel implements IDockviewGroupPanelModel
@ -165,8 +213,16 @@ export class DockviewGroupPanelModel
private readonly _onMove = new Emitter<GroupMoveEvent>(); private readonly _onMove = new Emitter<GroupMoveEvent>();
readonly onMove: Event<GroupMoveEvent> = this._onMove.event; readonly onMove: Event<GroupMoveEvent> = this._onMove.event;
private readonly _onDidDrop = new Emitter<GroupviewDropEvent>(); private readonly _onDidDrop = new Emitter<DockviewDidDropEvent>();
readonly onDidDrop: Event<GroupviewDropEvent> = this._onDidDrop.event; readonly onDidDrop: Event<DockviewDidDropEvent> = this._onDidDrop.event;
private readonly _onWillDrop = new Emitter<DockviewWillDropEvent>();
readonly onWillDrop: Event<DockviewWillDropEvent> = this._onWillDrop.event;
private readonly _onWillShowOverlay =
new Emitter<WillShowOverlayLocationEvent>();
readonly onWillShowOverlay: Event<WillShowOverlayLocationEvent> =
this._onWillShowOverlay.event;
private readonly _onTabDragStart = new Emitter<TabDragEvent>(); private readonly _onTabDragStart = new Emitter<TabDragEvent>();
readonly onTabDragStart: Event<TabDragEvent> = this._onTabDragStart.event; readonly onTabDragStart: Event<TabDragEvent> = this._onTabDragStart.event;
@ -175,19 +231,22 @@ export class DockviewGroupPanelModel
readonly onGroupDragStart: Event<GroupDragEvent> = readonly onGroupDragStart: Event<GroupDragEvent> =
this._onGroupDragStart.event; this._onGroupDragStart.event;
private readonly _onDidAddPanel = new Emitter<GroupviewChangeEvent>(); private readonly _onDidAddPanel = new Emitter<DockviewGroupChangeEvent>();
readonly onDidAddPanel: Event<GroupviewChangeEvent> = readonly onDidAddPanel: Event<DockviewGroupChangeEvent> =
this._onDidAddPanel.event; this._onDidAddPanel.event;
private readonly _onDidRemovePanel = new Emitter<GroupviewChangeEvent>(); private readonly _onDidRemovePanel =
readonly onDidRemovePanel: Event<GroupviewChangeEvent> = new Emitter<DockviewGroupChangeEvent>();
readonly onDidRemovePanel: Event<DockviewGroupChangeEvent> =
this._onDidRemovePanel.event; this._onDidRemovePanel.event;
private readonly _onDidActivePanelChange = private readonly _onDidActivePanelChange =
new Emitter<GroupviewChangeEvent>(); new Emitter<DockviewGroupChangeEvent>();
readonly onDidActivePanelChange: Event<GroupviewChangeEvent> = readonly onDidActivePanelChange: Event<DockviewGroupChangeEvent> =
this._onDidActivePanelChange.event; this._onDidActivePanelChange.event;
private readonly _api: DockviewApi;
get element(): HTMLElement { get element(): HTMLElement {
throw new Error('not supported'); throw new Error('not supported');
} }
@ -301,6 +360,8 @@ export class DockviewGroupPanelModel
toggleClass(this.container, 'groupview', true); toggleClass(this.container, 'groupview', true);
this._api = new DockviewApi(this.accessor);
this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel); this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel);
this.contentContainer = new ContentContainer(this.accessor, this); this.contentContainer = new ContentContainer(this.accessor, this);
@ -316,6 +377,7 @@ export class DockviewGroupPanelModel
this.addDisposables( this.addDisposables(
this._onTabDragStart, this._onTabDragStart,
this._onGroupDragStart, this._onGroupDragStart,
this._onWillShowOverlay,
this.tabsContainer.onTabDragStart((event) => { this.tabsContainer.onTabDragStart((event) => {
this._onTabDragStart.fire(event); this._onTabDragStart.fire(event);
}), }),
@ -323,8 +385,14 @@ export class DockviewGroupPanelModel
this._onGroupDragStart.fire(event); this._onGroupDragStart.fire(event);
}), }),
this.tabsContainer.onDrop((event) => { this.tabsContainer.onDrop((event) => {
this.handleDropEvent(event.event, 'center', event.index); this.handleDropEvent(
'header',
event.event,
'center',
event.index
);
}), }),
this.contentContainer.onDidFocus(() => { this.contentContainer.onDidFocus(() => {
this.accessor.doSetGroupActive(this.groupPanel, true); this.accessor.doSetGroupActive(this.groupPanel, true);
}), }),
@ -332,25 +400,57 @@ export class DockviewGroupPanelModel
// noop // noop
}), }),
this.contentContainer.dropTarget.onDrop((event) => { this.contentContainer.dropTarget.onDrop((event) => {
this.handleDropEvent(event.nativeEvent, event.position); this.handleDropEvent(
'content',
event.nativeEvent,
event.position
);
}),
this.tabsContainer.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire(event);
}),
this.contentContainer.dropTarget.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire({ event, kind: 'content' });
}), }),
this._onMove, this._onMove,
this._onDidChange, this._onDidChange,
this._onDidDrop, this._onDidDrop,
this._onWillDrop,
this._onDidAddPanel, this._onDidAddPanel,
this._onDidRemovePanel, this._onDidRemovePanel,
this._onDidActivePanelChange this._onDidActivePanelChange
); );
} }
private _overwriteRenderContainer: OverlayRenderContainer | null = null;
set renderContainer(value: OverlayRenderContainer | null) {
this.panels.forEach((panel) => {
this.renderContainer.detatch(panel);
});
this._overwriteRenderContainer = value;
this.panels.forEach((panel) => {
this.rerender(panel);
});
}
get renderContainer(): OverlayRenderContainer {
return (
this._overwriteRenderContainer ??
this.accessor.overlayRenderContainer
);
}
initialize(): void { initialize(): void {
if (this.options?.panels) { if (this.options.panels) {
this.options.panels.forEach((panel) => { this.options.panels.forEach((panel) => {
this.doAddPanel(panel); this.doAddPanel(panel);
}); });
} }
if (this.options?.activePanel) { if (this.options.activePanel) {
this.openPanel(this.options.activePanel); this.openPanel(this.options.activePanel);
} }
@ -366,7 +466,7 @@ export class DockviewGroupPanelModel
); );
this.addDisposables(this._rightHeaderActions); this.addDisposables(this._rightHeaderActions);
this._rightHeaderActions.init({ this._rightHeaderActions.init({
containerApi: new DockviewApi(this.accessor), containerApi: this._api,
api: this.groupPanel.api, api: this.groupPanel.api,
}); });
this.tabsContainer.setRightActionsElement( this.tabsContainer.setRightActionsElement(
@ -381,7 +481,7 @@ export class DockviewGroupPanelModel
); );
this.addDisposables(this._leftHeaderActions); this.addDisposables(this._leftHeaderActions);
this._leftHeaderActions.init({ this._leftHeaderActions.init({
containerApi: new DockviewApi(this.accessor), containerApi: this._api,
api: this.groupPanel.api, api: this.groupPanel.api,
}); });
this.tabsContainer.setLeftActionsElement( this.tabsContainer.setLeftActionsElement(
@ -396,7 +496,7 @@ export class DockviewGroupPanelModel
); );
this.addDisposables(this._prefixHeaderActions); this.addDisposables(this._prefixHeaderActions);
this._prefixHeaderActions.init({ this._prefixHeaderActions.init({
containerApi: new DockviewApi(this.accessor), containerApi: this._api,
api: this.groupPanel.api, api: this.groupPanel.api,
}); });
this.tabsContainer.setPrefixActionsElement( this.tabsContainer.setPrefixActionsElement(
@ -735,7 +835,7 @@ export class DockviewGroupPanelModel
if (this.isEmpty && !this.watermark) { if (this.isEmpty && !this.watermark) {
const watermark = this.accessor.createWatermarkComponent(); const watermark = this.accessor.createWatermarkComponent();
watermark.init({ watermark.init({
containerApi: new DockviewApi(this.accessor), containerApi: this._api,
group: this.groupPanel, group: this.groupPanel,
}); });
this.watermark = watermark; this.watermark = watermark;
@ -778,6 +878,7 @@ export class DockviewGroupPanelModel
} }
private handleDropEvent( private handleDropEvent(
type: 'header' | 'content',
event: DragEvent, event: DragEvent,
position: Position, position: Position,
index?: number index?: number
@ -786,6 +887,34 @@ export class DockviewGroupPanelModel
return; return;
} }
function getKind(): DockviewGroupDropLocation {
switch (type) {
case 'header':
return typeof index === 'number' ? 'tab' : 'header_space';
case 'content':
return 'content';
}
}
const panel =
typeof index === 'number' ? this.panels[index] : undefined;
const willDropEvent = new DockviewWillDropEvent({
nativeEvent: event,
position,
panel,
getData: () => getPanelData(),
kind: getKind(),
group: this.groupPanel,
api: this._api,
});
this._onWillDrop.fire(willDropEvent);
if (willDropEvent.defaultPrevented) {
return;
}
const data = getPanelData(); const data = getPanelData();
if (data && data.viewId === this.accessor.id) { if (data && data.viewId === this.accessor.id) {
@ -824,12 +953,16 @@ export class DockviewGroupPanelModel
index, index,
}); });
} else { } else {
this._onDidDrop.fire({ this._onDidDrop.fire(
nativeEvent: event, new DockviewDidDropEvent({
position, nativeEvent: event,
index, position,
getData: () => getPanelData(), panel,
}); getData: () => getPanelData(),
group: this.groupPanel,
api: this._api,
})
);
} }
} }

View File

@ -103,6 +103,7 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions {
debug?: boolean; debug?: boolean;
rootOverlayModel?: DroptargetOverlayModel; rootOverlayModel?: DroptargetOverlayModel;
locked?: boolean; locked?: boolean;
disableDnd?: boolean;
} }
export interface PanelOptions<P extends object = Parameters> { export interface PanelOptions<P extends object = Parameters> {

View File

@ -7,12 +7,7 @@ import { Optional } from '../types';
import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewPanelRenderer } from '../overlayRenderContainer'; import { DockviewPanelRenderer } from '../overlayRenderContainer';
export enum DockviewDropTargets { export type DockviewDropTargets = 'tab' | 'panel' | 'tabContainer' | 'edge';
Tab,
Panel,
TabContainer,
Edge,
}
export interface HeaderPartInitParameters { export interface HeaderPartInitParameters {
title: string; title: string;

View File

@ -24,6 +24,18 @@ export namespace Event {
}; };
} }
export class DockviewEvent {
private _defaultPrevented = false;
get defaultPrevented(): boolean {
return this._defaultPrevented;
}
preventDefault(): void {
this._defaultPrevented = true;
}
}
class LeakageMonitor { class LeakageMonitor {
readonly events = new Map<Event<any>, Stacktrace>(); readonly events = new Map<Event<any>, Stacktrace>();

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { import {
DockviewComponent, DockviewComponent,
DockviewDropEvent, DockviewWillDropEvent,
DockviewDndOverlayEvent, DockviewDndOverlayEvent,
GroupPanelFrameworkComponentFactory, GroupPanelFrameworkComponentFactory,
DockviewPanelApi, DockviewPanelApi,
@ -12,6 +12,7 @@ import {
IHeaderActionsRenderer, IHeaderActionsRenderer,
DockviewPanelRenderer, DockviewPanelRenderer,
DroptargetOverlayModel, DroptargetOverlayModel,
DockviewDidDropEvent,
} from 'dockview-core'; } from 'dockview-core';
import { ReactPanelContentPart } from './reactContentPart'; import { ReactPanelContentPart } from './reactContentPart';
import { ReactPanelHeaderPart } from './reactHeaderPart'; import { ReactPanelHeaderPart } from './reactHeaderPart';
@ -61,7 +62,8 @@ export interface IDockviewReactProps {
components: PanelCollection<IDockviewPanelProps>; components: PanelCollection<IDockviewPanelProps>;
tabComponents?: PanelCollection<IDockviewPanelHeaderProps>; tabComponents?: PanelCollection<IDockviewPanelHeaderProps>;
watermarkComponent?: React.FunctionComponent<IWatermarkPanelProps>; watermarkComponent?: React.FunctionComponent<IWatermarkPanelProps>;
onDidDrop?: (event: DockviewDropEvent) => void; onDidDrop?: (event: DockviewDidDropEvent) => void;
onWillDrop?: (event: DockviewWillDropEvent) => void;
showDndOverlay?: (event: DockviewDndOverlayEvent) => boolean; showDndOverlay?: (event: DockviewDndOverlayEvent) => boolean;
hideBorders?: boolean; hideBorders?: boolean;
className?: string; className?: string;
@ -82,6 +84,7 @@ export interface IDockviewReactProps {
defaultRenderer?: DockviewPanelRenderer; defaultRenderer?: DockviewPanelRenderer;
rootOverlayModel?: DroptargetOverlayModel; rootOverlayModel?: DroptargetOverlayModel;
locked?: boolean; locked?: boolean;
disableDnd?: boolean;
} }
const DEFAULT_REACT_TAB = 'props.defaultTabComponent'; const DEFAULT_REACT_TAB = 'props.defaultTabComponent';
@ -185,6 +188,7 @@ export const DockviewReact = React.forwardRef(
debug: props.debug, debug: props.debug,
rootOverlayModel: props.rootOverlayModel, rootOverlayModel: props.rootOverlayModel,
locked: props.locked, locked: props.locked,
disableDnd: props.disableDnd,
}); });
const { clientWidth, clientHeight } = domRef.current; const { clientWidth, clientHeight } = domRef.current;
@ -209,6 +213,16 @@ export const DockviewReact = React.forwardRef(
dockviewRef.current.locked = !!props.locked; dockviewRef.current.locked = !!props.locked;
}, [props.locked]); }, [props.locked]);
React.useEffect(() => {
if (!dockviewRef.current) {
return;
}
dockviewRef.current.updateOptions({
disableDnd: props.disableDnd,
});
}, [props.disableDnd]);
React.useEffect(() => { React.useEffect(() => {
if (!dockviewRef.current) { if (!dockviewRef.current) {
return () => { return () => {
@ -227,6 +241,24 @@ export const DockviewReact = React.forwardRef(
}; };
}, [props.onDidDrop]); }, [props.onDidDrop]);
React.useEffect(() => {
if (!dockviewRef.current) {
return () => {
// noop
};
}
const disposable = dockviewRef.current.onWillDrop((event) => {
if (props.onWillDrop) {
props.onWillDrop(event);
}
});
return () => {
disposable.dispose();
};
}, [props.onWillDrop]);
React.useEffect(() => { React.useEffect(() => {
if (!dockviewRef.current) { if (!dockviewRef.current) {
return; return;