diff --git a/packages/dockview-demo/src/services/sidebarItem.tsx b/packages/dockview-demo/src/services/sidebarItem.tsx new file mode 100644 index 000000000..892b336e4 --- /dev/null +++ b/packages/dockview-demo/src/services/sidebarItem.tsx @@ -0,0 +1,135 @@ +import { ViewContainer } from './viewContainer'; +import * as React from 'react'; +import { toggleClass } from '../dom'; + +export const Container = (props: { + container: ViewContainer; + isActive: boolean; + onDragOver: (e: React.DragEvent) => void; + onDrop: (e: React.DragEvent, direction: 'top' | 'bottom') => void; + onClick: (e: React.MouseEvent) => void; +}) => { + const ref = React.useRef(null); + const [selection, setSelection] = React.useState< + 'top' | 'bottom' | undefined + >(undefined); + const isDragging = React.useRef(false); + + const [dragEntered, setDragEntered] = React.useState(false); + + const onDragOver = (e: React.DragEvent) => { + if (isDragging.current) { + return; + } + + setDragEntered(true); + + e.preventDefault(); + + const target = e.target as HTMLDivElement; + + const width = target.clientWidth; + const height = target.clientHeight; + + if (width === 0 || height === 0) { + return; // avoid div!0 + } + + const x = e.nativeEvent.offsetX; + const y = e.nativeEvent.offsetY; + const xp = (100 * x) / width; + const yp = (100 * y) / height; + + const isTop = yp < 50; + const isBottom = yp >= 50; + + setSelection(isTop ? 'top' : 'bottom'); + + props.onDragOver(e); + }; + + const onDragLeave = (e: React.DragEvent) => { + if (isDragging.current) { + return; + } + + setDragEntered(false); + + setSelection(undefined); + }; + + const onDrop = (e: React.DragEvent) => { + if (isDragging.current) { + return; + } + + setDragEntered(false); + + props.onDrop(e, selection); + + setSelection(undefined); + }; + + const onDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + }; + + const onDragStart = (e: React.DragEvent) => { + isDragging.current = true; + + e.dataTransfer.setData( + 'application/json', + JSON.stringify({ container: props.container.id }) + ); + }; + + const onDragEnd = (e: React.DragEvent) => { + isDragging.current = false; + + setDragEntered(false); + }; + + return ( +
+ {dragEntered && ( +
+ )} + + {props.container.icon} + +
+ ); +}; diff --git a/packages/dockview-demo/src/services/viewContainer.ts b/packages/dockview-demo/src/services/viewContainer.ts index 70245db75..057af131d 100644 --- a/packages/dockview-demo/src/services/viewContainer.ts +++ b/packages/dockview-demo/src/services/viewContainer.ts @@ -45,7 +45,7 @@ export class PaneviewContainer implements ViewContainer { } get views() { - return this._views; + return [...this._views]; } get schema(): SerializedPaneview | undefined { diff --git a/packages/dockview-demo/src/services/viewService.ts b/packages/dockview-demo/src/services/viewService.ts index 1c9dc4d77..95a7769ef 100644 --- a/packages/dockview-demo/src/services/viewService.ts +++ b/packages/dockview-demo/src/services/viewService.ts @@ -34,6 +34,7 @@ export interface IViewService extends IDisposable { targetLocation: number ): void; insertContainerAfter(source: ViewContainer, target: ViewContainer): void; + insertContainerBefore(source: ViewContainer, target: ViewContainer): void; addViews(view: View, viewContainer: ViewContainer, location?: number): void; removeViews(removeViews: View[], viewContainer: ViewContainer): void; getViewContainer(id: string): ViewContainer | undefined; @@ -103,6 +104,23 @@ export class ViewService implements IViewService { this._onDidContainersChange.fire(); } + insertContainerBefore(source: ViewContainer, target: ViewContainer): void { + const sourceIndex = this._viewContainers.findIndex( + (c) => c.id === source.id + ); + + const view = this._viewContainers.splice(sourceIndex, 1)[0]; + + const targetIndex = this._viewContainers.findIndex( + (c) => c.id === target.id + ); + + this._viewContainers.splice(Math.max(targetIndex, 0), 0, view); + this._viewContainers = [...this._viewContainers]; + + this._onDidContainersChange.fire(); + } + addContainer(container: ViewContainer): void { this._viewContainers = [...this._viewContainers, container]; this._activeViewContainerId = container.id; diff --git a/packages/dockview-demo/src/services/widgets.scss b/packages/dockview-demo/src/services/widgets.scss index 7859ccb12..1e8bd3aee 100644 --- a/packages/dockview-demo/src/services/widgets.scss +++ b/packages/dockview-demo/src/services/widgets.scss @@ -3,3 +3,12 @@ background-color: green !important; } } + +.container-item { + display: flex; + justify-content: center; + align-items: center; + height: 48px; + box-sizing: border-box; + position: relative; +} diff --git a/packages/dockview-demo/src/services/widgets.tsx b/packages/dockview-demo/src/services/widgets.tsx index 4408f492d..7fa75c12a 100644 --- a/packages/dockview-demo/src/services/widgets.tsx +++ b/packages/dockview-demo/src/services/widgets.tsx @@ -18,6 +18,7 @@ import { IViewService, ViewService } from './viewService'; import { DefaultView } from './view'; import { RegisteredView, VIEW_REGISTRY } from './viewRegistry'; import { toggleClass } from '../dom'; +import { Container } from './sidebarItem'; import './widgets.scss'; class ViewServiceModel { @@ -172,7 +173,8 @@ export const Activitybar = (props: IGridviewPanelProps) => { }; const onContainerDrop = (targetContainer: ViewContainer) => ( - event: React.DragEvent + event: React.DragEvent, + direction: 'top' | 'bottom' ) => { const data = event.dataTransfer.getData('application/json'); if (data) { @@ -180,10 +182,21 @@ export const Activitybar = (props: IGridviewPanelProps) => { const sourceContainer = viewService.model.getViewContainer( container ); - viewService.model.insertContainerAfter( - sourceContainer, - targetContainer - ); + + switch (direction) { + case 'bottom': + viewService.model.insertContainerAfter( + sourceContainer, + targetContainer + ); + break; + case 'top': + viewService.model.insertContainerBefore( + sourceContainer, + targetContainer + ); + break; + } } }; @@ -204,48 +217,31 @@ export const Activitybar = (props: IGridviewPanelProps) => { } }; + const onDragOver = (container: ViewContainer) => (e: React.DragEvent) => { + const api = registry.get('gridview'); + + const sidebarPanel = api.getPanel('sidebar'); + if (!sidebarPanel.api.isVisible) { + api.setVisible(sidebarPanel, true); + sidebarPanel.focus(); + } + + viewService.model.setActiveViewContainer(container.id); + }; + return (
{containers.map((container, i) => { const isActive = activeContainerid === container.id; return ( -
{ - e.preventDefault(); - onClick(container, true)(e); - }} - onDragEnter={(e) => { - e.preventDefault(); - }} - draggable={true} - onDragStart={(e) => { - e.dataTransfer.setData( - 'application/json', - JSON.stringify({ container: container.id }) - ); - }} - onDrop={onContainerDrop(container)} - style={{ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '48px', - boxSizing: 'border-box', - borderLeft: isActive - ? '1px solid white' - : '1px solid transparent', - }} + - {/* {container.id} */} - - {container.icon} - -
+ container={container} + isActive={isActive} + onDragOver={onDragOver(container)} + onClick={onClick(container)} + onDrop={onContainerDrop(container)} + /> ); })} @@ -360,25 +356,40 @@ export const SidebarPart = (props: { id: string }) => { }; const onDidDrop = (event: PaneviewDropEvent) => { - const data = event.event.getData(); + const data = event.getData(); + + const containerData = event.event.dataTransfer.getData( + 'application/json' + ); + + if (containerData) { + const { container } = JSON.parse(containerData); + + const sourceContainer = viewService.model.getViewContainer( + container + ); + const targetContainer = viewService.model.getViewContainer( + props.id + ); + + sourceContainer.views.forEach((v) => { + viewService.model.moveViewToLocation(v, targetContainer, 0); + }); + + return; + } if (!data) { return; } - const targetPanel = event.event.panel; + const targetPanel = event.panel; const allPanels = event.api.getPanels(); let toIndex = allPanels.indexOf(targetPanel); - // if ( - // event.event.position === Position.Left || - // event.event.position === Position.Top - // ) { - // toIndex = Math.max(0, toIndex - 1); - // } if ( - event.event.position === Position.Right || - event.event.position === Position.Bottom + event.position === Position.Right || + event.position === Position.Bottom ) { toIndex = Math.min(allPanels.length, toIndex + 1); } diff --git a/packages/dockview/src/paneview/draggablePaneviewPanel.ts b/packages/dockview/src/paneview/draggablePaneviewPanel.ts index f070519a3..39f618d34 100644 --- a/packages/dockview/src/paneview/draggablePaneviewPanel.ts +++ b/packages/dockview/src/paneview/draggablePaneviewPanel.ts @@ -10,6 +10,7 @@ import { PanePanelInitParameter, PaneviewPanel, } from './paneviewPanel'; +import { addClasses } from '../dom'; interface ViewContainer { readonly title: string; @@ -56,9 +57,15 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel { } private initDragFeatures() { + if (!this.header) { + return; + } + const id = this.id; - this.header!.draggable = true; - this.header!.tabIndex = 0; + this.header.draggable = true; + this.header.tabIndex = 0; + + addClasses(this.header, 'pane-draggable'); this.handler = new (class PaneDragHandler extends DragHandler { getData(): IDisposable { @@ -75,7 +82,7 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel { }, }; } - })(this.header!); + })(this.header); this.target = new Droptarget(this.element, { validOverlays: 'vertical', diff --git a/packages/dockview/src/paneview/paneview.scss b/packages/dockview/src/paneview/paneview.scss index 5145a7d94..d9744b995 100644 --- a/packages/dockview/src/paneview/paneview.scss +++ b/packages/dockview/src/paneview/paneview.scss @@ -46,6 +46,10 @@ position: relative; outline: none; + &.pane-draggable { + cursor: pointer; + } + &:focus, &:focus-within { &:before { diff --git a/packages/dockview/src/react/paneview/paneview.tsx b/packages/dockview/src/react/paneview/paneview.tsx index b7e36d153..719dc3276 100644 --- a/packages/dockview/src/react/paneview/paneview.tsx +++ b/packages/dockview/src/react/paneview/paneview.tsx @@ -22,9 +22,8 @@ export interface IPaneviewPanelProps> title: string; } -export interface PaneviewDropEvent { +export interface PaneviewDropEvent extends PaneviewDropEvent2 { api: PaneviewApi; - event: PaneviewDropEvent2; } export interface IPaneviewReactProps { @@ -128,7 +127,7 @@ export const PaneviewReact = React.forwardRef( const disposable = paneview.onDidDrop((event) => { if (props.onDidDrop) { props.onDidDrop({ - event, + ...event, api: new PaneviewApi(paneview), }); }