From c51597fde3a600102ed3af9c7d41811db62a45b0 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Mon, 23 Aug 2021 20:53:39 +0100 Subject: [PATCH 1/4] feat: expose dnd functions --- .../src/layout-grid/activitybar.tsx | 33 +- .../src/layout-grid/application.tsx | 4 +- .../src/layout-grid/sidebar.layout.json | 8 +- .../dockview-demo/src/layout-grid/sidebar.tsx | 110 ------- packages/dockview-demo/src/services/view.ts | 34 ++ .../src/services/viewContainer.ts | 112 +++++++ .../dockview-demo/src/services/viewService.ts | 175 +++++++++++ .../dockview-demo/src/services/widgets.tsx | 291 ++++++++++++++++++ packages/dockview/src/index.ts | 1 + packages/dockview/src/paneview/paneview.ts | 6 + .../src/paneview/paneviewComponent.ts | 36 ++- .../dockview/src/react/paneview/paneview.tsx | 37 ++- 12 files changed, 700 insertions(+), 147 deletions(-) create mode 100644 packages/dockview-demo/src/services/view.ts create mode 100644 packages/dockview-demo/src/services/viewContainer.ts create mode 100644 packages/dockview-demo/src/services/viewService.ts create mode 100644 packages/dockview-demo/src/services/widgets.tsx diff --git a/packages/dockview-demo/src/layout-grid/activitybar.tsx b/packages/dockview-demo/src/layout-grid/activitybar.tsx index d3bd310f6..37242391d 100644 --- a/packages/dockview-demo/src/layout-grid/activitybar.tsx +++ b/packages/dockview-demo/src/layout-grid/activitybar.tsx @@ -49,18 +49,27 @@ export const Activitybar = (props: IGridviewPanelProps) => { return (
- -
- -
-
+
+ +
+
+ +
+
+ +
); }; diff --git a/packages/dockview-demo/src/layout-grid/application.tsx b/packages/dockview-demo/src/layout-grid/application.tsx index fd265a651..748396699 100644 --- a/packages/dockview-demo/src/layout-grid/application.tsx +++ b/packages/dockview-demo/src/layout-grid/application.tsx @@ -8,12 +8,12 @@ import { SerializedGridview, GridviewApi, } from 'dockview'; -import { Activitybar } from './activitybar'; +import { Activitybar } from '../services/widgets'; import { Footer } from './footer'; import { Panel } from './panel'; import { TestGrid } from './layoutGrid'; import { useLayoutRegistry } from './registry'; -import { Sidebar } from './sidebar'; +import { Sidebar } from '../services/widgets'; const rootcomponents: { [index: string]: React.FunctionComponent; diff --git a/packages/dockview-demo/src/layout-grid/sidebar.layout.json b/packages/dockview-demo/src/layout-grid/sidebar.layout.json index 0625e7e4c..5dd7eb9ba 100644 --- a/packages/dockview-demo/src/layout-grid/sidebar.layout.json +++ b/packages/dockview-demo/src/layout-grid/sidebar.layout.json @@ -5,7 +5,7 @@ "data": { "id": "1", "component": "controlCenter", - "headerComponent": "default", + "props": {}, "title": "Control Center", "state": {} @@ -17,7 +17,7 @@ "data": { "id": "2", "component": "default", - "headerComponent": "default", + "props": {}, "title": "Panel 1", "state": {} @@ -30,7 +30,7 @@ "data": { "id": "3", "component": "default", - "headerComponent": "default", + "props": {}, "title": "Panel 2", "state": {} @@ -43,7 +43,7 @@ "data": { "id": "4", "component": "default", - "headerComponent": "default", + "props": {}, "title": "Panel 3", "state": {} diff --git a/packages/dockview-demo/src/layout-grid/sidebar.tsx b/packages/dockview-demo/src/layout-grid/sidebar.tsx index 6b3b33754..e849abeed 100644 --- a/packages/dockview-demo/src/layout-grid/sidebar.tsx +++ b/packages/dockview-demo/src/layout-grid/sidebar.tsx @@ -12,107 +12,6 @@ import { ControlCenter } from './controlCenter'; import { toggleClass } from '../dom'; import './sidebar.scss'; -const DefaultHeader = (props: IPaneviewPanelProps) => { - const ref = React.useRef(); - const mouseover = React.useRef(); - - const [url, setUrl] = React.useState( - props.api.isExpanded - ? 'https://fonts.gstatic.com/s/i/materialicons/expand_more/v6/24px.svg' - : 'https://fonts.gstatic.com/s/i/materialicons/chevron_right/v7/24px.svg' - ); - - const toggle = () => { - toggleClass( - ref.current, - 'within', - props.api.isExpanded && mouseover.current - ); - }; - - React.useEffect(() => { - const disposable = new CompositeDisposable( - props.api.onDidExpansionChange((event) => { - setUrl( - event.isExpanded - ? 'https://fonts.gstatic.com/s/i/materialicons/expand_more/v6/24px.svg' - : 'https://fonts.gstatic.com/s/i/materialicons/chevron_right/v7/24px.svg' - ); - toggle(); - }), - props.api.onMouseEnter((ev) => { - mouseover.current = true; - toggle(); - }), - props.api.onMouseLeave((ev) => { - mouseover.current = false; - toggle(); - }) - ); - - return () => { - disposable.dispose(); - }; - }); - - const onClick = (event: React.MouseEvent) => { - if (event.defaultPrevented) { - return; - } - props.api.setExpanded(!props.api.isExpanded); - }; - - const onClickAction = (event: React.MouseEvent) => { - event.preventDefault(); - }; - - return ( -
-
- -
- {props.title} - - -
- ); -}; - const components = { default: (props: IPaneviewPanelProps) => { return
This is an example panel
; @@ -120,10 +19,6 @@ const components = { controlCenter: ControlCenter, }; -const headerComponents = { - default: DefaultHeader, -}; - export const Sidebar = (props: IGridviewPanelProps) => { const api = React.useRef(); @@ -139,25 +34,21 @@ export const Sidebar = (props: IGridviewPanelProps) => { event.api.addPanel({ id: '1', component: 'default', - headerComponent: 'default', title: 'Control Center', }); event.api.addPanel({ id: '2', component: 'default', - headerComponent: 'default', title: 'Panel 1', }); event.api.addPanel({ id: '3', component: 'default', - headerComponent: 'default', title: 'Panel 2', }); event.api.addPanel({ id: '4', component: 'default', - headerComponent: 'default', title: 'Panel 3', }); @@ -196,7 +87,6 @@ export const Sidebar = (props: IGridviewPanelProps) => { }} > { + readonly id: string; + readonly canDelete: boolean; + readonly views: View[]; + readonly schema: T | undefined; + readonly onDidAddView: Event; + readonly onDidRemoveView: Event; + addView(view: View, location?: number): void; + layout(schema: T): void; + removeView(view: View): void; + clear(): void; + toJSON(): object; +} + +export class PaneviewContainer implements ViewContainer { + private readonly _id: string; + private readonly _canDelete: boolean; + private readonly _views: View[]; + + private readonly _onDidAddView = new Emitter(); + readonly onDidAddView = this._onDidAddView.event; + private readonly _onDidRemoveView = new Emitter(); + readonly onDidRemoveView = this._onDidRemoveView.event; + + private _schema: SerializedPaneview | undefined; + + get id() { + return this._id; + } + + get views() { + return this._views; + } + + get canDelete() { + return this._canDelete; + } + + get schema(): SerializedPaneview | undefined { + if (!this._schema) { + this._schema = JSON.parse( + localStorage.getItem(`viewcontainer_${this.id}`) + ); + } + return this._schema; + } + + constructor(id: string, canDelete: boolean) { + this._id = id; + this._canDelete = canDelete; + + const schema = this.schema; + + if (this.schema) { + this._views = this.schema.views.map((v) => { + return new DefaultView(v.data.id, v.data.title, v.expanded); + }); + } else { + this._views = []; + } + // super(); + + // this.addDisposables(this._onDidAddView, this._onDidRemoveView); + } + + layout(schema: SerializedPaneview): void { + this._schema = schema; + localStorage.setItem( + `viewcontainer_${this.id}`, + JSON.stringify(schema) + ); + } + + addView(view: View, location = 0): void { + this._views.splice(location, 0, view); + this._onDidAddView.fire(view); + } + + removeView(view: View): void { + const index = this._views.indexOf(view); + if (index < 0) { + throw new Error('invalid'); + } + this._views.splice(index, 1); + + if (this._schema) { + this._schema = { ...this._schema }; + this._schema.views = this._schema.views.filter( + (v) => v.data.id !== view.id + ); + this.layout(this._schema); + } + + this._onDidRemoveView.fire(view); + } + + clear() { + localStorage.removeItem(`viewcontainer_${this.id}`); + } + + toJSON() { + return { id: this.id, views: this.views.map((v) => ({ id: v.id })) }; + } +} diff --git a/packages/dockview-demo/src/services/viewService.ts b/packages/dockview-demo/src/services/viewService.ts new file mode 100644 index 000000000..a722078e7 --- /dev/null +++ b/packages/dockview-demo/src/services/viewService.ts @@ -0,0 +1,175 @@ +import { Emitter, Event } from 'dockview'; +import { DefaultView, View } from './view'; +import { PaneviewContainer, ViewContainer } from './viewContainer'; + +export interface IViewService { + readonly containers: ViewContainer[]; + readonly onDidActiveContainerChange: Event; + readonly onDidRemoveContainer: Event; + readonly onDidAddContainer: Event; + readonly activeContainer: ViewContainer | undefined; + addContainer(container: ViewContainer): void; + setActiveViewContainer(id: string): void; + getView(id: string): View | undefined; + moveViewToLocation( + view: View, + targetViewContainer: ViewContainer, + targetLocation: number + ): void; + addViews(view: View, viewContainer: ViewContainer, location?: number): void; + removeViews(removeViews: View[], viewContainer: ViewContainer): void; + getViewContainer(id: string): ViewContainer | undefined; + toJSON(): object; + load(layout: any): void; +} + +export class ViewService implements IViewService { + private _viewContainers: ViewContainer[] = []; + private readonly _onDidActiveContainerChange = new Emitter(); + readonly onDidActiveContainerChange = this._onDidActiveContainerChange + .event; + private readonly _onDidRemoveContainer = new Emitter(); + readonly onDidRemoveContainer = this._onDidRemoveContainer.event; + private readonly _onDidAddContainer = new Emitter(); + readonly onDidAddContainer = this._onDidAddContainer.event; + private readonly _onDidContainersChange = new Emitter(); + readonly onDidContainersChange = this._onDidContainersChange.event; + private _activeViewContainerId: string; + + get containers(): ViewContainer[] { + return this._viewContainers; + } + + get activeContainer(): ViewContainer | undefined { + return this._viewContainers.find( + (c) => c.id === this._activeViewContainerId + ); + } + + constructor() { + // + } + + load(layout: any): void { + const { containers, activeContainer } = layout; + + for (const container of containers) { + const { id, views } = container; + const viewContainer = new PaneviewContainer(id, true); + for (const view of views) { + viewContainer.addView( + new DefaultView(view.id, view.title, view.isExpanded) + ); + } + this.addContainer(viewContainer); + } + + this.setActiveViewContainer(activeContainer); + } + + insertContainerAfter(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(targetIndex + 1, 0, view); + this._viewContainers = [...this._viewContainers]; + + this._onDidContainersChange.fire(); + } + + addContainer(container: ViewContainer): void { + this._viewContainers = [...this._viewContainers, container]; + this._activeViewContainerId = container.id; + this._onDidAddContainer.fire(); + } + + removeContainer(container: ViewContainer): void { + this._viewContainers = this._viewContainers.filter( + (c) => c.id !== container.id + ); + + if (this._activeViewContainerId === container.id) { + this._activeViewContainerId = + this._viewContainers.length > 0 + ? this._viewContainers[0].id + : undefined; + } + + this._onDidRemoveContainer.fire(); + } + + setActiveViewContainer(id: string): void { + if (!this._viewContainers.find((c) => c.id === id)) { + throw new Error(`invalid container ${id}`); + } + this._activeViewContainerId = id; + this._onDidActiveContainerChange.fire(); + } + + getView(id: string): View | undefined { + for (const container of Array.from(this._viewContainers.values())) { + const view = container.views.find((v) => v.id === id); + if (view) { + return view; + } + } + return undefined; + } + + moveViewToLocation( + view: View, + targetViewContainer: ViewContainer, + targetLocation: number + ): void { + const sourceViewContainer = this.getViewContainer2(view); + this.removeViews([view], sourceViewContainer); + this.addViews(view, targetViewContainer, targetLocation); + } + + addViews( + view: View, + viewContainer: ViewContainer, + location?: number + ): void { + viewContainer.addView(view, location); + } + + removeViews(removeViews: View[], viewContainer: ViewContainer): void { + for (const view of removeViews) { + viewContainer.removeView(view); + + if (viewContainer.views.length === 0) { + viewContainer.clear(); + this.removeContainer(viewContainer); + } + } + } + + getViewContainer(id: string): ViewContainer | undefined { + return this._viewContainers.find((c) => c.id === id); + } + + getViewContainer2(view: View): ViewContainer | undefined { + for (const container of Array.from(this._viewContainers.values())) { + const v = container.views.find((v) => v.id === view.id); + if (v) { + return container; + } + } + return undefined; + } + + toJSON() { + return { + containers: this.containers.map((c) => c.toJSON()), + activeContainer: this.activeContainer.id, + }; + } +} diff --git a/packages/dockview-demo/src/services/widgets.tsx b/packages/dockview-demo/src/services/widgets.tsx new file mode 100644 index 000000000..7ca8f865c --- /dev/null +++ b/packages/dockview-demo/src/services/widgets.tsx @@ -0,0 +1,291 @@ +import { + CompositeDisposable, + GridviewApi, + IGridviewPanelProps, + IPaneviewPanelProps, + PanelCollection, + PaneviewApi, + PaneviewDropEvent, + PaneviewReact, + PaneviewReadyEvent, + getPaneData, +} from 'dockview'; +import * as React from 'react'; +import { useLayoutRegistry } from '../layout-grid/registry'; +import { PaneviewContainer, ViewContainer } from './viewContainer'; +import { ViewService } from './viewService'; +import { DefaultView } from './view'; + +const viewService = new ViewService(); + +const layout = localStorage.getItem('viewservice'); +if (layout) { + viewService.load(JSON.parse(layout)); +} else { + const container1 = new PaneviewContainer('c1', true); + if (!container1.schema) { + container1.addView(new DefaultView('panel1', 'Panel 1', true)); + container1.addView(new DefaultView('panel2', 'Panel 2', true)); + } + const container2 = new PaneviewContainer('c2', true); + if (!container2.schema) { + container2.addView(new DefaultView('panel3', 'Panel 3', true)); + container2.addView(new DefaultView('panel4', 'Panel 4', true)); + } + viewService.addContainer(container1); + viewService.addContainer(container2); +} + +const save = () => { + localStorage.setItem('viewservice', JSON.stringify(viewService.toJSON())); +}; + +viewService.onDidActiveContainerChange(save); +viewService.onDidRemoveContainer(save); +viewService.onDidAddContainer(save); + +const components: PanelCollection> = { + default: (props: IPaneviewPanelProps<{ viewId: string }>) => { + return ( +
+ {props.params.viewId} +
+ ); + }, +}; + +export const Activitybar = (props: IGridviewPanelProps) => { + const [activeContainerid, setActiveContainerId] = React.useState( + viewService.activeContainer.id + ); + const [containers, setContainers] = React.useState( + viewService.containers + ); + + const registry = useLayoutRegistry(); + + React.useEffect(() => { + const disposable = new CompositeDisposable( + viewService.onDidActiveContainerChange(() => { + setActiveContainerId(viewService.activeContainer.id); + }), + viewService.onDidAddContainer(() => { + setContainers(viewService.containers); + }), + viewService.onDidRemoveContainer(() => { + setContainers(viewService.containers); + }), + viewService.onDidContainersChange(() => { + setContainers(viewService.containers); + }) + ); + + return () => { + disposable.dispose(); + }; + }, []); + + const onClick = (container: ViewContainer, alwaysOpen = false) => ( + event: React.MouseEvent + ) => { + const api = registry.get('gridview'); + + const selectedActive = container.id === activeContainerid; + + const sidebarPanel = api.getPanel('sidebar'); + if (sidebarPanel.api.isVisible) { + if (!alwaysOpen && selectedActive) { + api.setVisible(sidebarPanel, false); + } + } else { + event.preventDefault(); // prevent focus + api.setVisible(sidebarPanel, true); + sidebarPanel.focus(); + } + + viewService.setActiveViewContainer(container.id); + }; + + const onDrop = (targetContainer: ViewContainer) => ( + event: React.DragEvent + ) => { + const data = event.dataTransfer.getData('application/json'); + if (data) { + const { container } = JSON.parse(data); + const sourceContainer = viewService.getViewContainer(container); + viewService.insertContainerAfter(sourceContainer, targetContainer); + } + }; + + const onNewContainer = (event: React.DragEvent) => { + const data = getPaneData(); + if (data) { + const { paneId } = data; + const view = viewService.getView(paneId); + const viewContainer = viewService.getViewContainer2(view); + viewService.removeViews([view], viewContainer); + // viewContainer.removeView(view); + const newContainer = new PaneviewContainer( + `t_${Date.now().toString().substr(5)}`, + true + ); + newContainer.addView(view); + viewService.addContainer(newContainer); + } + }; + + 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={onDrop(container)} + style={{ + height: '48px', + boxSizing: 'border-box', + borderLeft: isActive + ? '1px solid white' + : '1px solid transparent', + }} + key={i} + > + {container.id} +
+ ); + })} +
{ + e.preventDefault(); + }} + onDragEnter={(e) => { + e.preventDefault(); + }} + onDrop={onNewContainer} + style={{ height: '100%', backgroundColor: 'red' }} + >
+
+ ); +}; + +export const Sidebar = () => { + const [sidebarId, setSidebarId] = React.useState( + viewService.activeContainer.id + ); + + React.useEffect(() => { + const disposable = viewService.onDidActiveContainerChange(() => { + setSidebarId(viewService.activeContainer.id); + }); + + return () => { + disposable.dispose(); + }; + }, []); + + return ; +}; + +export const SidebarPart = (props: { id: string }) => { + const [api, setApi] = React.useState(); + + React.useEffect(() => { + if (!api) { + return () => { + // + }; + } + + const viewContainer = viewService.getViewContainer(props.id); + + const disposables = new CompositeDisposable( + api.onDidLayoutChange(() => { + viewContainer.layout(api.toJSON()); + }), + viewContainer.onDidAddView((view) => { + api.addPanel({ + id: view.id, + isExpanded: view.isExpanded, + title: view.title, + component: 'default', + params: { + viewId: view.id, + }, + }); + }), + viewContainer.onDidRemoveView((view) => { + const panel = api.getPanel(view.id); + api.removePanel(panel); + }) + ); + + const schema = viewContainer.schema; + if (schema) { + api.fromJSON(schema); + } else { + api.getPanels().forEach((p) => { + api.removePanel(p); + }); + viewContainer.views.forEach((view) => { + api.addPanel({ + id: view.id, + isExpanded: view.isExpanded, + title: view.title, + component: 'default', + params: { + viewId: view.id, + }, + }); + }); + } + + return () => { + disposables.dispose(); + }; + }, [api, props.id]); + + const onReady = (event: PaneviewReadyEvent) => { + setApi(event.api); + }; + + const onDidDrop = (event: PaneviewDropEvent) => { + const data = getPaneData(); + + if (!data) { + return; + } + + const viewId = data.paneId; + const viewContainer = viewService.getViewContainer(props.id); + const view = viewService.getView(viewId); + + viewService.moveViewToLocation(view, viewContainer, 0); + }; + + if (!props.id) { + return null; + } + + return ( + + ); +}; diff --git a/packages/dockview/src/index.ts b/packages/dockview/src/index.ts index 8ad56d994..b121d7063 100644 --- a/packages/dockview/src/index.ts +++ b/packages/dockview/src/index.ts @@ -16,6 +16,7 @@ export * from './groupview/types'; export * from './dockview/dockviewComponent'; export * from './dockview/options'; export * from './gridview/gridviewComponent'; +export * from './dnd/dataTransfer'; export * from './react'; // TODO: should be conditional on whether user wants the React wrappers diff --git a/packages/dockview/src/paneview/paneview.ts b/packages/dockview/src/paneview/paneview.ts index 9c569c015..8e59de810 100644 --- a/packages/dockview/src/paneview/paneview.ts +++ b/packages/dockview/src/paneview/paneview.ts @@ -96,6 +96,12 @@ export class Paneview extends CompositeDisposable implements IDisposable { this.addDisposables( this.splitview.onDidSashEnd(() => { this._onDidChange.fire(undefined); + }), + this.splitview.onDidAddView(() => { + this._onDidChange.fire(); + }), + this.splitview.onDidRemoveView(() => { + this._onDidChange.fire(); }) ); } diff --git a/packages/dockview/src/paneview/paneviewComponent.ts b/packages/dockview/src/paneview/paneviewComponent.ts index 53503b30a..69e2a96b0 100644 --- a/packages/dockview/src/paneview/paneviewComponent.ts +++ b/packages/dockview/src/paneview/paneviewComponent.ts @@ -128,9 +128,10 @@ export interface IPaneviewComponent extends IDisposable { readonly height: number; readonly minimumSize: number; readonly maximumSize: number; + readonly onDidDrop: Event; + readonly onDidLayoutChange: Event; addPanel(options: AddPaneviewCompponentOptions): IDisposable; layout(width: number, height: number): void; - onDidLayoutChange: Event; toJSON(): SerializedPaneview; fromJSON( serializedPaneview: SerializedPaneview, @@ -142,6 +143,7 @@ export interface IPaneviewComponent extends IDisposable { removePanel(panel: IPaneviewPanel): void; getPanel(id: string): IPaneviewPanel | undefined; movePanel(from: number, to: number): void; + updateOptions(options: Partial): void; } export class PaneviewComponent @@ -199,12 +201,20 @@ export class PaneviewComponent : this.paneview.orthogonalSize; } + private _options: PaneviewComponentOptions; + + get options() { + return this._options; + } + constructor( private element: HTMLElement, - private readonly options: PaneviewComponentOptions + options: PaneviewComponentOptions ) { super(); + this._options = options; + if (!options.components) { options.components = {}; } @@ -224,6 +234,10 @@ export class PaneviewComponent // } + updateOptions(options: Partial): void { + this._options = { ...this.options, ...options }; + } + addPanel(options: AddPaneviewCompponentOptions): IDisposable { const body = createComponent( options.id, @@ -269,17 +283,17 @@ export class PaneviewComponent disableDnd: !!this.options.disableDnd, }); - view.onDidDrop((event) => { - this._onDidDrop.fire(event); - }); + const disposable = new CompositeDisposable( + view.onDidDrop((event) => { + this._onDidDrop.fire(event); + }) + ); const size: Sizing | number = typeof options.size === 'number' ? options.size : Sizing.Distribute; const index = typeof options.index === 'number' ? options.index : undefined; - this.paneview.addPane(view, size, index); - view.init({ params: options.params || {}, minimumBodySize: options.minimumBodySize, @@ -289,13 +303,11 @@ export class PaneviewComponent containerApi: new PaneviewApi(this), }); + this.paneview.addPane(view, size, index); + view.orientation = this.paneview.orientation; - return { - dispose: () => { - // - }, - }; + return disposable; } getPanels(): PaneviewPanel[] { diff --git a/packages/dockview/src/react/paneview/paneview.tsx b/packages/dockview/src/react/paneview/paneview.tsx index d499b5a27..2b7e633d3 100644 --- a/packages/dockview/src/react/paneview/paneview.tsx +++ b/packages/dockview/src/react/paneview/paneview.tsx @@ -62,6 +62,18 @@ export const PaneviewReact = React.forwardRef( }; }, [props.disableAutoResizing]); + React.useEffect(() => { + paneviewRef.current?.updateOptions({ + frameworkComponents: props.components, + }); + }, [props.components]); + + React.useEffect(() => { + paneviewRef.current?.updateOptions({ + headerframeworkComponents: props.headerComponents, + }); + }, [props.headerComponents]); + React.useEffect(() => { const createComponent = ( id: string, @@ -90,12 +102,6 @@ export const PaneviewReact = React.forwardRef( const api = new PaneviewApi(paneview); - const disposable = paneview.onDidDrop((event) => { - if (props.onDidDrop) { - props.onDidDrop({ event, api }); - } - }); - const { clientWidth, clientHeight } = domRef.current!; paneview.layout(clientWidth, clientHeight); @@ -106,11 +112,28 @@ export const PaneviewReact = React.forwardRef( paneviewRef.current = paneview; return () => { - disposable.dispose(); paneview.dispose(); }; }, []); + React.useEffect(() => { + if (!paneviewRef.current) { + return; + } + + const paneview = paneviewRef.current; + + const disposable = paneview.onDidDrop((event) => { + if (props.onDidDrop) { + props.onDidDrop({ event, api: new PaneviewApi(paneview) }); + } + }); + + return () => { + disposable.dispose(); + }; + }, [props.onDidDrop]); + return (
Date: Mon, 23 Aug 2021 20:53:39 +0100 Subject: [PATCH 2/4] feat: expose dnd functions --- packages/dockview-demo/public/index.html | 2 +- .../src/layout-grid/activitybar.tsx | 33 +- .../src/layout-grid/application.tsx | 4 +- .../src/layout-grid/sidebar.layout.json | 8 +- .../dockview-demo/src/layout-grid/sidebar.tsx | 110 ------ packages/dockview-demo/src/services/view.ts | 64 ++++ .../src/services/viewContainer.ts | 127 +++++++ .../src/services/viewRegistry.ts | 49 +++ .../dockview-demo/src/services/viewService.ts | 200 ++++++++++ .../dockview-demo/src/services/widgets.tsx | 357 ++++++++++++++++++ packages/dockview/src/index.ts | 1 + packages/dockview/src/paneview/paneview.ts | 6 + .../src/paneview/paneviewComponent.ts | 36 +- .../dockview/src/react/paneview/paneview.tsx | 39 +- 14 files changed, 888 insertions(+), 148 deletions(-) create mode 100644 packages/dockview-demo/src/services/view.ts create mode 100644 packages/dockview-demo/src/services/viewContainer.ts create mode 100644 packages/dockview-demo/src/services/viewRegistry.ts create mode 100644 packages/dockview-demo/src/services/viewService.ts create mode 100644 packages/dockview-demo/src/services/widgets.tsx diff --git a/packages/dockview-demo/public/index.html b/packages/dockview-demo/public/index.html index 9b54428e6..3e434a577 100644 --- a/packages/dockview-demo/public/index.html +++ b/packages/dockview-demo/public/index.html @@ -1,7 +1,7 @@ - +
diff --git a/packages/dockview-demo/src/layout-grid/activitybar.tsx b/packages/dockview-demo/src/layout-grid/activitybar.tsx index d3bd310f6..37242391d 100644 --- a/packages/dockview-demo/src/layout-grid/activitybar.tsx +++ b/packages/dockview-demo/src/layout-grid/activitybar.tsx @@ -49,18 +49,27 @@ export const Activitybar = (props: IGridviewPanelProps) => { return (
- -
- -
-
+
+ +
+
+ +
+
+ +
); }; diff --git a/packages/dockview-demo/src/layout-grid/application.tsx b/packages/dockview-demo/src/layout-grid/application.tsx index fd265a651..748396699 100644 --- a/packages/dockview-demo/src/layout-grid/application.tsx +++ b/packages/dockview-demo/src/layout-grid/application.tsx @@ -8,12 +8,12 @@ import { SerializedGridview, GridviewApi, } from 'dockview'; -import { Activitybar } from './activitybar'; +import { Activitybar } from '../services/widgets'; import { Footer } from './footer'; import { Panel } from './panel'; import { TestGrid } from './layoutGrid'; import { useLayoutRegistry } from './registry'; -import { Sidebar } from './sidebar'; +import { Sidebar } from '../services/widgets'; const rootcomponents: { [index: string]: React.FunctionComponent; diff --git a/packages/dockview-demo/src/layout-grid/sidebar.layout.json b/packages/dockview-demo/src/layout-grid/sidebar.layout.json index 0625e7e4c..5dd7eb9ba 100644 --- a/packages/dockview-demo/src/layout-grid/sidebar.layout.json +++ b/packages/dockview-demo/src/layout-grid/sidebar.layout.json @@ -5,7 +5,7 @@ "data": { "id": "1", "component": "controlCenter", - "headerComponent": "default", + "props": {}, "title": "Control Center", "state": {} @@ -17,7 +17,7 @@ "data": { "id": "2", "component": "default", - "headerComponent": "default", + "props": {}, "title": "Panel 1", "state": {} @@ -30,7 +30,7 @@ "data": { "id": "3", "component": "default", - "headerComponent": "default", + "props": {}, "title": "Panel 2", "state": {} @@ -43,7 +43,7 @@ "data": { "id": "4", "component": "default", - "headerComponent": "default", + "props": {}, "title": "Panel 3", "state": {} diff --git a/packages/dockview-demo/src/layout-grid/sidebar.tsx b/packages/dockview-demo/src/layout-grid/sidebar.tsx index 6b3b33754..e849abeed 100644 --- a/packages/dockview-demo/src/layout-grid/sidebar.tsx +++ b/packages/dockview-demo/src/layout-grid/sidebar.tsx @@ -12,107 +12,6 @@ import { ControlCenter } from './controlCenter'; import { toggleClass } from '../dom'; import './sidebar.scss'; -const DefaultHeader = (props: IPaneviewPanelProps) => { - const ref = React.useRef(); - const mouseover = React.useRef(); - - const [url, setUrl] = React.useState( - props.api.isExpanded - ? 'https://fonts.gstatic.com/s/i/materialicons/expand_more/v6/24px.svg' - : 'https://fonts.gstatic.com/s/i/materialicons/chevron_right/v7/24px.svg' - ); - - const toggle = () => { - toggleClass( - ref.current, - 'within', - props.api.isExpanded && mouseover.current - ); - }; - - React.useEffect(() => { - const disposable = new CompositeDisposable( - props.api.onDidExpansionChange((event) => { - setUrl( - event.isExpanded - ? 'https://fonts.gstatic.com/s/i/materialicons/expand_more/v6/24px.svg' - : 'https://fonts.gstatic.com/s/i/materialicons/chevron_right/v7/24px.svg' - ); - toggle(); - }), - props.api.onMouseEnter((ev) => { - mouseover.current = true; - toggle(); - }), - props.api.onMouseLeave((ev) => { - mouseover.current = false; - toggle(); - }) - ); - - return () => { - disposable.dispose(); - }; - }); - - const onClick = (event: React.MouseEvent) => { - if (event.defaultPrevented) { - return; - } - props.api.setExpanded(!props.api.isExpanded); - }; - - const onClickAction = (event: React.MouseEvent) => { - event.preventDefault(); - }; - - return ( -
- ); -}; - const components = { default: (props: IPaneviewPanelProps) => { return
This is an example panel
; @@ -120,10 +19,6 @@ const components = { controlCenter: ControlCenter, }; -const headerComponents = { - default: DefaultHeader, -}; - export const Sidebar = (props: IGridviewPanelProps) => { const api = React.useRef(); @@ -139,25 +34,21 @@ export const Sidebar = (props: IGridviewPanelProps) => { event.api.addPanel({ id: '1', component: 'default', - headerComponent: 'default', title: 'Control Center', }); event.api.addPanel({ id: '2', component: 'default', - headerComponent: 'default', title: 'Panel 1', }); event.api.addPanel({ id: '3', component: 'default', - headerComponent: 'default', title: 'Panel 2', }); event.api.addPanel({ id: '4', component: 'default', - headerComponent: 'default', title: 'Panel 3', }); @@ -196,7 +87,6 @@ export const Sidebar = (props: IGridviewPanelProps) => { }} > { + readonly id: string; + readonly views: View[]; + readonly schema: T | any; + readonly icon: string; + readonly onDidAddView: Event; + readonly onDidRemoveView: Event; + addView(view: View, location?: number): void; + layout(schema: T): void; + removeView(view: View): void; + clear(): void; + toJSON(): SerializedViewContainer; +} + +export class PaneviewContainer implements ViewContainer { + private readonly _id: string; + private readonly _views: View[] = []; + + private readonly _onDidAddView = new Emitter(); + readonly onDidAddView = this._onDidAddView.event; + private readonly _onDidRemoveView = new Emitter(); + readonly onDidRemoveView = this._onDidRemoveView.event; + + private _schema: SerializedPaneview | undefined; + + get id() { + return this._id; + } + + get views() { + return this._views; + } + + get schema(): SerializedPaneview | undefined { + if (!this._schema) { + this._schema = JSON.parse( + localStorage.getItem(`viewcontainer_${this.id}`) + ); + } + return this._schema; + } + + get icon(): string { + const defaultIcon = 'search'; + if (this.views.length > 0) { + return this.views.find((v) => !!v.icon)?.icon || defaultIcon; + } + return defaultIcon; + } + + constructor( + id: string, + viewRegistry: IViewRegistry, + views?: SerializedView[] + ) { + this._id = id; + + if (views) { + for (const view of views) { + const registeredView = viewRegistry.getRegisteredView(view.id); + this.addView( + new DefaultView({ + id: view.id, + title: registeredView.title, + isExpanded: view.isExpanded, + isLocationEditable: registeredView.isLocationEditable, + icon: registeredView.icon, + }) + ); + } + } + // this.addDisposables(this._onDidAddView, this._onDidRemoveView); + } + + layout(schema: SerializedPaneview): void { + this._schema = schema; + localStorage.setItem( + `viewcontainer_${this.id}`, + JSON.stringify(schema) + ); + } + + addView(view: View, location = 0): void { + this._views.splice(location, 0, view); + this._onDidAddView.fire(view); + } + + removeView(view: View): void { + const index = this._views.indexOf(view); + if (index < 0) { + throw new Error('invalid'); + } + this._views.splice(index, 1); + + if (this._schema) { + this._schema = { ...this._schema }; + this._schema.views = this._schema.views.filter( + (v) => v.data.id !== view.id + ); + this.layout(this._schema); + } + + this._onDidRemoveView.fire(view); + } + + clear() { + localStorage.removeItem(`viewcontainer_${this.id}`); + } + + toJSON(): SerializedViewContainer { + return { id: this.id, views: this.views.map((v) => v.toJSON()) }; + } +} diff --git a/packages/dockview-demo/src/services/viewRegistry.ts b/packages/dockview-demo/src/services/viewRegistry.ts new file mode 100644 index 000000000..2a77a4ad1 --- /dev/null +++ b/packages/dockview-demo/src/services/viewRegistry.ts @@ -0,0 +1,49 @@ +export interface RegisteredView { + id: string; + icon: string; + title: string; + isLocationEditable: boolean; +} + +export interface IViewRegistry { + getRegisteredView(id: string): RegisteredView | undefined; +} + +export class ViewRegistry { + private readonly _registry = new Map(); + + register(registeredView: RegisteredView): void { + this._registry.set(registeredView.id, registeredView); + } + + getRegisteredView(id: string): RegisteredView | undefined { + return this._registry.get(id); + } +} + +export const VIEW_REGISTRY = new ViewRegistry(); + +VIEW_REGISTRY.register({ + id: 'search_widget', + title: 'search', + icon: 'search', + isLocationEditable: false, +}); +VIEW_REGISTRY.register({ + id: 'home_widget', + title: 'Home', + icon: 'home', + isLocationEditable: true, +}); +VIEW_REGISTRY.register({ + id: 'account_widget', + title: 'Account', + icon: 'account_circle', + isLocationEditable: true, +}); +VIEW_REGISTRY.register({ + id: 'settings_widget', + title: 'Settings', + icon: 'settings', + isLocationEditable: true, +}); diff --git a/packages/dockview-demo/src/services/viewService.ts b/packages/dockview-demo/src/services/viewService.ts new file mode 100644 index 000000000..1c9dc4d77 --- /dev/null +++ b/packages/dockview-demo/src/services/viewService.ts @@ -0,0 +1,200 @@ +import { + CompositeDisposable, + Emitter, + Event, + IDisposable, + IView, +} from 'dockview'; +import { DefaultView, View } from './view'; +import { + PaneviewContainer, + ViewContainer, + SerializedViewContainer, +} from './viewContainer'; +import { IViewRegistry } from './viewRegistry'; + +export interface SerializedViewService { + containers: SerializedViewContainer[]; + activeContainer?: string; +} + +export interface IViewService extends IDisposable { + readonly containers: ViewContainer[]; + readonly onDidActiveContainerChange: Event; + readonly onDidRemoveContainer: Event; + readonly onDidAddContainer: Event; + readonly onDidContainersChange: Event; + readonly activeContainer: ViewContainer | undefined; + addContainer(container: ViewContainer): void; + setActiveViewContainer(id: string): void; + getView(id: string): View | undefined; + moveViewToLocation( + view: View, + targetViewContainer: ViewContainer, + targetLocation: number + ): void; + insertContainerAfter(source: ViewContainer, target: ViewContainer): void; + addViews(view: View, viewContainer: ViewContainer, location?: number): void; + removeViews(removeViews: View[], viewContainer: ViewContainer): void; + getViewContainer(id: string): ViewContainer | undefined; + getViewContainer2(view: View): ViewContainer | undefined; + toJSON(): SerializedViewService; + load(layout: SerializedViewService): void; +} + +export class ViewService implements IViewService { + private _viewContainers: ViewContainer[] = []; + private readonly _onDidActiveContainerChange = new Emitter(); + readonly onDidActiveContainerChange = this._onDidActiveContainerChange + .event; + private readonly _onDidRemoveContainer = new Emitter(); + readonly onDidRemoveContainer = this._onDidRemoveContainer.event; + private readonly _onDidAddContainer = new Emitter(); + readonly onDidAddContainer = this._onDidAddContainer.event; + private readonly _onDidContainersChange = new Emitter(); + readonly onDidContainersChange = this._onDidContainersChange.event; + private _activeViewContainerId: string; + + get containers(): ViewContainer[] { + return this._viewContainers; + } + + get activeContainer(): ViewContainer | undefined { + return this._viewContainers.find( + (c) => c.id === this._activeViewContainerId + ); + } + + constructor(private readonly viewRegistry: IViewRegistry) { + // + } + + load(layout: SerializedViewService): void { + const { containers, activeContainer } = layout; + + for (const container of containers) { + const { id, views } = container; + const viewContainer = new PaneviewContainer( + id, + this.viewRegistry, + views + ); + + this.addContainer(viewContainer); + } + + this.setActiveViewContainer(activeContainer); + } + + insertContainerAfter(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(targetIndex + 1, 0, view); + this._viewContainers = [...this._viewContainers]; + + this._onDidContainersChange.fire(); + } + + addContainer(container: ViewContainer): void { + this._viewContainers = [...this._viewContainers, container]; + this._activeViewContainerId = container.id; + this._onDidAddContainer.fire(); + } + + removeContainer(container: ViewContainer): void { + this._viewContainers = this._viewContainers.filter( + (c) => c.id !== container.id + ); + + if (this._activeViewContainerId === container.id) { + this._activeViewContainerId = + this._viewContainers.length > 0 + ? this._viewContainers[0].id + : undefined; + } + + this._onDidRemoveContainer.fire(); + } + + setActiveViewContainer(id: string): void { + if (!this._viewContainers.find((c) => c.id === id)) { + throw new Error(`invalid container ${id}`); + } + this._activeViewContainerId = id; + this._onDidActiveContainerChange.fire(); + } + + getView(id: string): View | undefined { + for (const container of Array.from(this._viewContainers.values())) { + const view = container.views.find((v) => v.id === id); + if (view) { + return view; + } + } + return undefined; + } + + moveViewToLocation( + view: View, + targetViewContainer: ViewContainer, + targetLocation: number + ): void { + const sourceViewContainer = this.getViewContainer2(view); + this.removeViews([view], sourceViewContainer); + this.addViews(view, targetViewContainer, targetLocation); + } + + addViews( + view: View, + viewContainer: ViewContainer, + location?: number + ): void { + viewContainer.addView(view, location); + } + + removeViews(removeViews: View[], viewContainer: ViewContainer): void { + for (const view of removeViews) { + viewContainer.removeView(view); + + if (viewContainer.views.length === 0) { + viewContainer.clear(); + this.removeContainer(viewContainer); + } + } + } + + getViewContainer(id: string): ViewContainer | undefined { + return this._viewContainers.find((c) => c.id === id); + } + + getViewContainer2(view: View): ViewContainer | undefined { + for (const container of Array.from(this._viewContainers.values())) { + const v = container.views.find((v) => v.id === view.id); + if (v) { + return container; + } + } + return undefined; + } + + toJSON(): SerializedViewService { + return { + containers: this.containers.map((c) => c.toJSON()), + activeContainer: this.activeContainer.id, + }; + } + + dispose(): void { + this._onDidActiveContainerChange.dispose(); + this._onDidAddContainer.dispose(); + this._onDidRemoveContainer.dispose(); + } +} diff --git a/packages/dockview-demo/src/services/widgets.tsx b/packages/dockview-demo/src/services/widgets.tsx new file mode 100644 index 000000000..fb2d17a70 --- /dev/null +++ b/packages/dockview-demo/src/services/widgets.tsx @@ -0,0 +1,357 @@ +import { + CompositeDisposable, + GridviewApi, + IGridviewPanelProps, + IPaneviewPanelProps, + PanelCollection, + PaneviewApi, + PaneviewDropEvent, + PaneviewReact, + PaneviewReadyEvent, + getPaneData, +} from 'dockview'; +import * as React from 'react'; +import { useLayoutRegistry } from '../layout-grid/registry'; +import { PaneviewContainer, ViewContainer } from './viewContainer'; +import { IViewService, ViewService } from './viewService'; +import { DefaultView } from './view'; +import { RegisteredView, VIEW_REGISTRY } from './viewRegistry'; + +class ViewServiceModel { + private readonly viewService: IViewService; + + get model() { + return this.viewService; + } + + constructor() { + this.viewService = new ViewService(VIEW_REGISTRY); + this.init(); + } + + init(): void { + const layout = localStorage.getItem('viewservice'); + if (layout) { + this.viewService.load(JSON.parse(layout)); + } else { + const container1 = new PaneviewContainer( + 'default_container_1', + VIEW_REGISTRY + ); + if (!container1.schema) { + this.addView( + container1, + VIEW_REGISTRY.getRegisteredView('search_widget') + ); + this.addView( + container1, + VIEW_REGISTRY.getRegisteredView('home_widget') + ); + } + const container2 = new PaneviewContainer( + 'default_container_2', + VIEW_REGISTRY + ); + if (!container2.schema) { + this.addView( + container2, + VIEW_REGISTRY.getRegisteredView('account_widget') + ); + this.addView( + container2, + VIEW_REGISTRY.getRegisteredView('settings_widget') + ); + } + this.viewService.addContainer(container1); + this.viewService.addContainer(container2); + } + + const save = () => { + localStorage.setItem( + 'viewservice', + JSON.stringify(this.viewService.toJSON()) + ); + }; + + this.viewService.onDidActiveContainerChange(save); + this.viewService.onDidRemoveContainer(save); + this.viewService.onDidAddContainer(save); + } + + private addView( + container: ViewContainer, + registedView: RegisteredView + ): void { + container.addView( + new DefaultView({ + id: registedView.id, + title: registedView.title, + isExpanded: true, + isLocationEditable: registedView.isLocationEditable, + icon: registedView.icon, + }) + ); + } +} + +const viewService = new ViewServiceModel(); + +const components: PanelCollection> = { + default: (props: IPaneviewPanelProps<{ viewId: string }>) => { + return ( +
+ {props.params.viewId} +
+ ); + }, +}; + +export const Activitybar = (props: IGridviewPanelProps) => { + const [activeContainerid, setActiveContainerId] = React.useState( + viewService.model.activeContainer.id + ); + const [containers, setContainers] = React.useState( + viewService.model.containers + ); + + const registry = useLayoutRegistry(); + + React.useEffect(() => { + const disposable = new CompositeDisposable( + viewService.model.onDidActiveContainerChange(() => { + setActiveContainerId(viewService.model.activeContainer.id); + }), + viewService.model.onDidAddContainer(() => { + setContainers(viewService.model.containers); + }), + viewService.model.onDidRemoveContainer(() => { + setContainers(viewService.model.containers); + }), + viewService.model.onDidContainersChange(() => { + setContainers(viewService.model.containers); + }) + ); + + return () => { + disposable.dispose(); + }; + }, []); + + const onClick = (container: ViewContainer, alwaysOpen = false) => ( + event: React.MouseEvent + ) => { + const api = registry.get('gridview'); + + const selectedActive = container.id === activeContainerid; + + const sidebarPanel = api.getPanel('sidebar'); + if (sidebarPanel.api.isVisible) { + if (!alwaysOpen && selectedActive) { + api.setVisible(sidebarPanel, false); + } + } else { + event.preventDefault(); // prevent focus + api.setVisible(sidebarPanel, true); + sidebarPanel.focus(); + } + + viewService.model.setActiveViewContainer(container.id); + }; + + const onDrop = (targetContainer: ViewContainer) => ( + event: React.DragEvent + ) => { + const data = event.dataTransfer.getData('application/json'); + if (data) { + const { container } = JSON.parse(data); + const sourceContainer = viewService.model.getViewContainer( + container + ); + viewService.model.insertContainerAfter( + sourceContainer, + targetContainer + ); + } + }; + + const onNewContainer = (event: React.DragEvent) => { + const data = getPaneData(); + if (data) { + const { paneId } = data; + const view = viewService.model.getView(paneId); + const viewContainer = viewService.model.getViewContainer2(view); + viewService.model.removeViews([view], viewContainer); + // viewContainer.removeView(view); + const newContainer = new PaneviewContainer( + `t_${Date.now().toString().substr(5)}`, + VIEW_REGISTRY + ); + newContainer.addView(view); + viewService.model.addContainer(newContainer); + } + }; + + 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={onDrop(container)} + style={{ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '48px', + boxSizing: 'border-box', + borderLeft: isActive + ? '1px solid white' + : '1px solid transparent', + }} + key={i} + > + {/* {container.id} */} + + {container.icon} + +
+ ); + })} +
{ + e.preventDefault(); + }} + onDragEnter={(e) => { + e.preventDefault(); + }} + onDrop={onNewContainer} + style={{ height: '100%', backgroundColor: 'red' }} + >
+
+ ); +}; + +export const Sidebar = () => { + const [sidebarId, setSidebarId] = React.useState( + viewService.model.activeContainer.id + ); + + React.useEffect(() => { + const disposable = viewService.model.onDidActiveContainerChange(() => { + setSidebarId(viewService.model.activeContainer.id); + }); + + return () => { + disposable.dispose(); + }; + }, []); + + return ; +}; + +export const SidebarPart = (props: { id: string }) => { + const [api, setApi] = React.useState(); + + React.useEffect(() => { + if (!api) { + return () => { + // + }; + } + + const viewContainer = viewService.model.getViewContainer(props.id); + + const disposables = new CompositeDisposable( + api.onDidLayoutChange(() => { + viewContainer.layout(api.toJSON()); + }), + viewContainer.onDidAddView((view) => { + api.addPanel({ + id: view.id, + isExpanded: view.isExpanded, + title: view.title, + component: 'default', + params: { + viewId: view.id, + }, + }); + }), + viewContainer.onDidRemoveView((view) => { + const panel = api.getPanel(view.id); + api.removePanel(panel); + }) + ); + + const schema = viewContainer.schema; + if (schema) { + api.fromJSON(schema); + } else { + api.getPanels().forEach((p) => { + api.removePanel(p); + }); + viewContainer.views.forEach((view) => { + api.addPanel({ + id: view.id, + isExpanded: view.isExpanded, + title: view.title, + component: 'default', + params: { + viewId: view.id, + }, + }); + }); + } + + return () => { + disposables.dispose(); + }; + }, [api, props.id]); + + const onReady = (event: PaneviewReadyEvent) => { + setApi(event.api); + }; + + const onDidDrop = (event: PaneviewDropEvent) => { + const data = getPaneData(); + + if (!data) { + return; + } + + const viewId = data.paneId; + const viewContainer = viewService.model.getViewContainer(props.id); + const view = viewService.model.getView(viewId); + + viewService.model.moveViewToLocation(view, viewContainer, 0); + }; + + if (!props.id) { + return null; + } + + return ( + + ); +}; diff --git a/packages/dockview/src/index.ts b/packages/dockview/src/index.ts index 8ad56d994..b121d7063 100644 --- a/packages/dockview/src/index.ts +++ b/packages/dockview/src/index.ts @@ -16,6 +16,7 @@ export * from './groupview/types'; export * from './dockview/dockviewComponent'; export * from './dockview/options'; export * from './gridview/gridviewComponent'; +export * from './dnd/dataTransfer'; export * from './react'; // TODO: should be conditional on whether user wants the React wrappers diff --git a/packages/dockview/src/paneview/paneview.ts b/packages/dockview/src/paneview/paneview.ts index 9c569c015..8e59de810 100644 --- a/packages/dockview/src/paneview/paneview.ts +++ b/packages/dockview/src/paneview/paneview.ts @@ -96,6 +96,12 @@ export class Paneview extends CompositeDisposable implements IDisposable { this.addDisposables( this.splitview.onDidSashEnd(() => { this._onDidChange.fire(undefined); + }), + this.splitview.onDidAddView(() => { + this._onDidChange.fire(); + }), + this.splitview.onDidRemoveView(() => { + this._onDidChange.fire(); }) ); } diff --git a/packages/dockview/src/paneview/paneviewComponent.ts b/packages/dockview/src/paneview/paneviewComponent.ts index 53503b30a..69e2a96b0 100644 --- a/packages/dockview/src/paneview/paneviewComponent.ts +++ b/packages/dockview/src/paneview/paneviewComponent.ts @@ -128,9 +128,10 @@ export interface IPaneviewComponent extends IDisposable { readonly height: number; readonly minimumSize: number; readonly maximumSize: number; + readonly onDidDrop: Event; + readonly onDidLayoutChange: Event; addPanel(options: AddPaneviewCompponentOptions): IDisposable; layout(width: number, height: number): void; - onDidLayoutChange: Event; toJSON(): SerializedPaneview; fromJSON( serializedPaneview: SerializedPaneview, @@ -142,6 +143,7 @@ export interface IPaneviewComponent extends IDisposable { removePanel(panel: IPaneviewPanel): void; getPanel(id: string): IPaneviewPanel | undefined; movePanel(from: number, to: number): void; + updateOptions(options: Partial): void; } export class PaneviewComponent @@ -199,12 +201,20 @@ export class PaneviewComponent : this.paneview.orthogonalSize; } + private _options: PaneviewComponentOptions; + + get options() { + return this._options; + } + constructor( private element: HTMLElement, - private readonly options: PaneviewComponentOptions + options: PaneviewComponentOptions ) { super(); + this._options = options; + if (!options.components) { options.components = {}; } @@ -224,6 +234,10 @@ export class PaneviewComponent // } + updateOptions(options: Partial): void { + this._options = { ...this.options, ...options }; + } + addPanel(options: AddPaneviewCompponentOptions): IDisposable { const body = createComponent( options.id, @@ -269,17 +283,17 @@ export class PaneviewComponent disableDnd: !!this.options.disableDnd, }); - view.onDidDrop((event) => { - this._onDidDrop.fire(event); - }); + const disposable = new CompositeDisposable( + view.onDidDrop((event) => { + this._onDidDrop.fire(event); + }) + ); const size: Sizing | number = typeof options.size === 'number' ? options.size : Sizing.Distribute; const index = typeof options.index === 'number' ? options.index : undefined; - this.paneview.addPane(view, size, index); - view.init({ params: options.params || {}, minimumBodySize: options.minimumBodySize, @@ -289,13 +303,11 @@ export class PaneviewComponent containerApi: new PaneviewApi(this), }); + this.paneview.addPane(view, size, index); + view.orientation = this.paneview.orientation; - return { - dispose: () => { - // - }, - }; + return disposable; } getPanels(): PaneviewPanel[] { diff --git a/packages/dockview/src/react/paneview/paneview.tsx b/packages/dockview/src/react/paneview/paneview.tsx index d499b5a27..4e6fb48c9 100644 --- a/packages/dockview/src/react/paneview/paneview.tsx +++ b/packages/dockview/src/react/paneview/paneview.tsx @@ -62,6 +62,18 @@ export const PaneviewReact = React.forwardRef( }; }, [props.disableAutoResizing]); + React.useEffect(() => { + paneviewRef.current?.updateOptions({ + frameworkComponents: props.components, + }); + }, [props.components]); + + React.useEffect(() => { + paneviewRef.current?.updateOptions({ + headerframeworkComponents: props.headerComponents, + }); + }, [props.headerComponents]); + React.useEffect(() => { const createComponent = ( id: string, @@ -90,12 +102,6 @@ export const PaneviewReact = React.forwardRef( const api = new PaneviewApi(paneview); - const disposable = paneview.onDidDrop((event) => { - if (props.onDidDrop) { - props.onDidDrop({ event, api }); - } - }); - const { clientWidth, clientHeight } = domRef.current!; paneview.layout(clientWidth, clientHeight); @@ -106,11 +112,30 @@ export const PaneviewReact = React.forwardRef( paneviewRef.current = paneview; return () => { - disposable.dispose(); paneview.dispose(); }; }, []); + React.useEffect(() => { + if (!paneviewRef.current) { + return () => { + // + }; + } + + const paneview = paneviewRef.current; + + const disposable = paneview.onDidDrop((event) => { + if (props.onDidDrop) { + props.onDidDrop({ event, api: new PaneviewApi(paneview) }); + } + }); + + return () => { + disposable.dispose(); + }; + }, [props.onDidDrop]); + return (
Date: Sat, 2 Oct 2021 12:14:16 +0100 Subject: [PATCH 3/4] test: fix tests --- packages/dockview/src/__tests__/__test_utils__/utils.ts | 2 +- packages/dockview/src/react/paneview/paneview.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/dockview/src/__tests__/__test_utils__/utils.ts b/packages/dockview/src/__tests__/__test_utils__/utils.ts index 2a99e97f0..ab8dc0a9e 100644 --- a/packages/dockview/src/__tests__/__test_utils__/utils.ts +++ b/packages/dockview/src/__tests__/__test_utils__/utils.ts @@ -10,5 +10,5 @@ export function setMockRefElement(node: Partial): void { }, }; - jest.spyOn(React, 'useRef').mockReturnValue(mockRef); + jest.spyOn(React, 'useRef').mockReturnValueOnce(mockRef); } diff --git a/packages/dockview/src/react/paneview/paneview.tsx b/packages/dockview/src/react/paneview/paneview.tsx index 2b7e633d3..99889bece 100644 --- a/packages/dockview/src/react/paneview/paneview.tsx +++ b/packages/dockview/src/react/paneview/paneview.tsx @@ -118,7 +118,9 @@ export const PaneviewReact = React.forwardRef( React.useEffect(() => { if (!paneviewRef.current) { - return; + return () => { + // noop + }; } const paneview = paneviewRef.current; From 29771cace1b0cae265937d505925b6692dde8a0d Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Mon, 4 Oct 2021 20:21:05 +0100 Subject: [PATCH 4/4] feat: draggable panels work --- .../src/services/viewContainer.ts | 13 ++- .../{viewRegistry.ts => viewRegistry.tsx} | 15 +++ .../dockview-demo/src/services/widgets.scss | 5 + .../dockview-demo/src/services/widgets.tsx | 91 ++++++++++++++----- packages/dockview/src/index.ts | 1 + .../src/paneview/draggablePaneviewPanel.ts | 39 ++++++-- packages/dockview/src/paneview/paneview.scss | 5 + .../src/paneview/paneviewComponent.ts | 13 ++- .../dockview/src/react/paneview/paneview.tsx | 34 ++++--- 9 files changed, 161 insertions(+), 55 deletions(-) rename packages/dockview-demo/src/services/{viewRegistry.ts => viewRegistry.tsx} (75%) create mode 100644 packages/dockview-demo/src/services/widgets.scss diff --git a/packages/dockview-demo/src/services/viewContainer.ts b/packages/dockview-demo/src/services/viewContainer.ts index 72f5067c3..70245db75 100644 --- a/packages/dockview-demo/src/services/viewContainer.ts +++ b/packages/dockview-demo/src/services/viewContainer.ts @@ -17,7 +17,7 @@ export interface ViewContainer { readonly views: View[]; readonly schema: T | any; readonly icon: string; - readonly onDidAddView: Event; + readonly onDidAddView: Event<{ view: View; index?: number }>; readonly onDidRemoveView: Event; addView(view: View, location?: number): void; layout(schema: T): void; @@ -30,7 +30,10 @@ export class PaneviewContainer implements ViewContainer { private readonly _id: string; private readonly _views: View[] = []; - private readonly _onDidAddView = new Emitter(); + private readonly _onDidAddView = new Emitter<{ + view: View; + index?: number; + }>(); readonly onDidAddView = this._onDidAddView.event; private readonly _onDidRemoveView = new Emitter(); readonly onDidRemoveView = this._onDidRemoveView.event; @@ -94,9 +97,9 @@ export class PaneviewContainer implements ViewContainer { ); } - addView(view: View, location = 0): void { - this._views.splice(location, 0, view); - this._onDidAddView.fire(view); + addView(view: View, index?: number): void { + this._views.splice(index, 0, view); + this._onDidAddView.fire({ view, index }); } removeView(view: View): void { diff --git a/packages/dockview-demo/src/services/viewRegistry.ts b/packages/dockview-demo/src/services/viewRegistry.tsx similarity index 75% rename from packages/dockview-demo/src/services/viewRegistry.ts rename to packages/dockview-demo/src/services/viewRegistry.tsx index 2a77a4ad1..3304b6d3e 100644 --- a/packages/dockview-demo/src/services/viewRegistry.ts +++ b/packages/dockview-demo/src/services/viewRegistry.tsx @@ -1,8 +1,11 @@ +import React from 'react'; + export interface RegisteredView { id: string; icon: string; title: string; isLocationEditable: boolean; + component: React.FunctionComponent; } export interface IViewRegistry { @@ -28,22 +31,34 @@ VIEW_REGISTRY.register({ title: 'search', icon: 'search', isLocationEditable: false, + component: () => { + return
This is a search bar component
; + }, }); VIEW_REGISTRY.register({ id: 'home_widget', title: 'Home', icon: 'home', isLocationEditable: true, + component: () => { + return
Home
; + }, }); VIEW_REGISTRY.register({ id: 'account_widget', title: 'Account', icon: 'account_circle', isLocationEditable: true, + component: () => { + return
account_circle
; + }, }); VIEW_REGISTRY.register({ id: 'settings_widget', title: 'Settings', icon: 'settings', isLocationEditable: true, + component: () => { + return
settings
; + }, }); diff --git a/packages/dockview-demo/src/services/widgets.scss b/packages/dockview-demo/src/services/widgets.scss new file mode 100644 index 000000000..7859ccb12 --- /dev/null +++ b/packages/dockview-demo/src/services/widgets.scss @@ -0,0 +1,5 @@ +.activity-bar-space { + &.activity-bar-space-dragover { + background-color: green !important; + } +} diff --git a/packages/dockview-demo/src/services/widgets.tsx b/packages/dockview-demo/src/services/widgets.tsx index fb2d17a70..4408f492d 100644 --- a/packages/dockview-demo/src/services/widgets.tsx +++ b/packages/dockview-demo/src/services/widgets.tsx @@ -1,5 +1,6 @@ import { CompositeDisposable, + getPaneData, GridviewApi, IGridviewPanelProps, IPaneviewPanelProps, @@ -8,7 +9,7 @@ import { PaneviewDropEvent, PaneviewReact, PaneviewReadyEvent, - getPaneData, + Position, } from 'dockview'; import * as React from 'react'; import { useLayoutRegistry } from '../layout-grid/registry'; @@ -16,6 +17,8 @@ import { PaneviewContainer, ViewContainer } from './viewContainer'; import { IViewService, ViewService } from './viewService'; import { DefaultView } from './view'; import { RegisteredView, VIEW_REGISTRY } from './viewRegistry'; +import { toggleClass } from '../dom'; +import './widgets.scss'; class ViewServiceModel { private readonly viewService: IViewService; @@ -96,13 +99,23 @@ class ViewServiceModel { const viewService = new ViewServiceModel(); +const colors = { + home_widget: 'red', + account_widget: 'green', + settings_widget: 'yellow', + search_widget: 'orange', +}; + const components: PanelCollection> = { default: (props: IPaneviewPanelProps<{ viewId: string }>) => { - return ( -
- {props.params.viewId} -
- ); + const Component = React.useMemo(() => { + const registeredView = VIEW_REGISTRY.getRegisteredView( + props.params.viewId + ); + return registeredView?.component; + }, [props.params.viewId]); + + return Component ? : null; }, }; @@ -158,7 +171,7 @@ export const Activitybar = (props: IGridviewPanelProps) => { viewService.model.setActiveViewContainer(container.id); }; - const onDrop = (targetContainer: ViewContainer) => ( + const onContainerDrop = (targetContainer: ViewContainer) => ( event: React.DragEvent ) => { const data = event.dataTransfer.getData('application/json'); @@ -192,7 +205,7 @@ export const Activitybar = (props: IGridviewPanelProps) => { }; return ( -
+
{containers.map((container, i) => { const isActive = activeContainerid === container.id; return ( @@ -212,7 +225,7 @@ export const Activitybar = (props: IGridviewPanelProps) => { JSON.stringify({ container: container.id }) ); }} - onDrop={onDrop(container)} + onDrop={onContainerDrop(container)} style={{ display: 'flex', justifyContent: 'center', @@ -235,20 +248,36 @@ export const Activitybar = (props: IGridviewPanelProps) => {
); })} -
{ - e.preventDefault(); - }} - onDragEnter={(e) => { - e.preventDefault(); - }} - onDrop={onNewContainer} - style={{ height: '100%', backgroundColor: 'red' }} - >
+
); }; +const ExtraSpace = (props: { + onNewContainer: (event: React.DragEvent) => void; +}) => { + const ref = React.useRef(null); + + return ( +
{ + e.preventDefault(); + }} + onDragEnter={(e) => { + toggleClass(ref.current, 'activity-bar-space-dragover', true); + e.preventDefault(); + }} + onDragLeave={(e) => { + toggleClass(ref.current, 'activity-bar-space-dragover', false); + }} + onDrop={props.onNewContainer} + style={{ height: '100%', backgroundColor: 'red' }} + >
+ ); +}; + export const Sidebar = () => { const [sidebarId, setSidebarId] = React.useState( viewService.model.activeContainer.id @@ -283,7 +312,7 @@ export const SidebarPart = (props: { id: string }) => { api.onDidLayoutChange(() => { viewContainer.layout(api.toJSON()); }), - viewContainer.onDidAddView((view) => { + viewContainer.onDidAddView(({ view, index }) => { api.addPanel({ id: view.id, isExpanded: view.isExpanded, @@ -292,6 +321,7 @@ export const SidebarPart = (props: { id: string }) => { params: { viewId: view.id, }, + index, }); }), viewContainer.onDidRemoveView((view) => { @@ -330,17 +360,34 @@ export const SidebarPart = (props: { id: string }) => { }; const onDidDrop = (event: PaneviewDropEvent) => { - const data = getPaneData(); + const data = event.event.getData(); if (!data) { return; } + const targetPanel = event.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 + ) { + toIndex = Math.min(allPanels.length, toIndex + 1); + } + const viewId = data.paneId; const viewContainer = viewService.model.getViewContainer(props.id); const view = viewService.model.getView(viewId); - viewService.model.moveViewToLocation(view, viewContainer, 0); + viewService.model.moveViewToLocation(view, viewContainer, toIndex); }; if (!props.id) { diff --git a/packages/dockview/src/index.ts b/packages/dockview/src/index.ts index b121d7063..9f2c6a2dc 100644 --- a/packages/dockview/src/index.ts +++ b/packages/dockview/src/index.ts @@ -17,6 +17,7 @@ export * from './dockview/dockviewComponent'; export * from './dockview/options'; export * from './gridview/gridviewComponent'; export * from './dnd/dataTransfer'; +export { Position } from './dnd/droptarget'; export * from './react'; // TODO: should be conditional on whether user wants the React wrappers diff --git a/packages/dockview/src/paneview/draggablePaneviewPanel.ts b/packages/dockview/src/paneview/draggablePaneviewPanel.ts index 4aea7d119..b26436fda 100644 --- a/packages/dockview/src/paneview/draggablePaneviewPanel.ts +++ b/packages/dockview/src/paneview/draggablePaneviewPanel.ts @@ -9,7 +9,11 @@ import { import { Emitter, Event } from '../events'; import { IDisposable } from '../lifecycle'; import { Orientation } from '../splitview/core/splitview'; -import { PanePanelInitParameter, PaneviewPanel } from './paneviewPanel'; +import { + IPaneviewPanel, + PanePanelInitParameter, + PaneviewPanel, +} from './paneviewPanel'; interface ViewContainer { readonly title: string; @@ -28,11 +32,16 @@ interface IViewContainerService { getViewContainerModel(container: ViewContainer): ViewContainerModel; } +export interface PaneviewDropEvent2 extends DroptargetEvent { + panel: IPaneviewPanel; + getData: () => PaneTransfer | undefined; +} + export abstract class DraggablePaneviewPanel extends PaneviewPanel { private handler: DragHandler | undefined; private target: Droptarget | undefined; - private readonly _onDidDrop = new Emitter(); + private readonly _onDidDrop = new Emitter(); readonly onDidDrop = this._onDidDrop.event; constructor( @@ -93,7 +102,11 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel { const data = getPaneData(); if (!data) { - this._onDidDrop.fire(event); + this._onDidDrop.fire({ + ...event, + panel: this, + getData: () => getPaneData(), + }); return; } @@ -103,20 +116,30 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel { const existingPanel = containerApi.getPanel(id); if (!existingPanel) { - this._onDidDrop.fire(event); + this._onDidDrop.fire({ + ...event, + panel: this, + getData: () => getPaneData(), + }); return; } - const fromIndex = containerApi - .getPanels() - .indexOf(existingPanel); + const allPanels = containerApi.getPanels(); + + const fromIndex = allPanels.indexOf(existingPanel); let toIndex = containerApi.getPanels().indexOf(this); + if ( + event.position === Position.Left || + event.position === Position.Top + ) { + toIndex = Math.max(0, toIndex - 1); + } if ( event.position === Position.Right || event.position === Position.Bottom ) { - toIndex = Math.max(0, toIndex + 1); + toIndex = Math.min(allPanels.length - 1, toIndex + 1); } containerApi.movePanel(fromIndex, toIndex); diff --git a/packages/dockview/src/paneview/paneview.scss b/packages/dockview/src/paneview/paneview.scss index 117835fae..5145a7d94 100644 --- a/packages/dockview/src/paneview/paneview.scss +++ b/packages/dockview/src/paneview/paneview.scss @@ -23,6 +23,11 @@ border-top: 1px solid var(--dv-paneview-header-border-color); } } + + .default-header { + background-color: var(--dv-group-view-background-color); + color: var(--dv-activegroup-visiblepanel-tab-color); + } } &:first-of-type > .pane > .pane-header { diff --git a/packages/dockview/src/paneview/paneviewComponent.ts b/packages/dockview/src/paneview/paneviewComponent.ts index 69e2a96b0..d1816503c 100644 --- a/packages/dockview/src/paneview/paneviewComponent.ts +++ b/packages/dockview/src/paneview/paneviewComponent.ts @@ -22,8 +22,10 @@ import { PanePanelInitParameter, IPaneviewPanel, } from './paneviewPanel'; -import { DraggablePaneviewPanel } from './draggablePaneviewPanel'; -import { DroptargetEvent } from '../dnd/droptarget'; +import { + DraggablePaneviewPanel, + PaneviewDropEvent2, +} from './draggablePaneviewPanel'; export interface SerializedPaneviewPanel { snap?: boolean; @@ -58,6 +60,7 @@ class DefaultHeader extends CompositeDisposable implements IPaneHeaderPart { constructor() { super(); this._element = document.createElement('div'); + this._element.className = 'default-header'; this.addDisposables( addDisposableListener(this.element, 'click', () => { @@ -128,7 +131,7 @@ export interface IPaneviewComponent extends IDisposable { readonly height: number; readonly minimumSize: number; readonly maximumSize: number; - readonly onDidDrop: Event; + readonly onDidDrop: Event; readonly onDidLayoutChange: Event; addPanel(options: AddPaneviewCompponentOptions): IDisposable; layout(width: number, height: number): void; @@ -156,8 +159,8 @@ export class PaneviewComponent private readonly _onDidLayoutChange = new Emitter(); readonly onDidLayoutChange: Event = this._onDidLayoutChange.event; - private readonly _onDidDrop = new Emitter(); - readonly onDidDrop: Event = this._onDidDrop.event; + private readonly _onDidDrop = new Emitter(); + readonly onDidDrop: Event = this._onDidDrop.event; get onDidAddView() { return this._paneview.onDidAddView; diff --git a/packages/dockview/src/react/paneview/paneview.tsx b/packages/dockview/src/react/paneview/paneview.tsx index 4e6fb48c9..d8ca2d9c6 100644 --- a/packages/dockview/src/react/paneview/paneview.tsx +++ b/packages/dockview/src/react/paneview/paneview.tsx @@ -9,7 +9,7 @@ import { PaneviewApi } from '../../api/component.api'; import { PanePanelSection } from './view'; import { PanelCollection, PanelParameters } from '../types'; import { watchElementResize } from '../../dom'; -import { DroptargetEvent } from '../../dnd/droptarget'; +import { PaneviewDropEvent2 } from '../../paneview/draggablePaneviewPanel'; export interface PaneviewReadyEvent { api: PaneviewApi; @@ -24,7 +24,7 @@ export interface IPaneviewPanelProps> export interface PaneviewDropEvent { api: PaneviewApi; - event: DroptargetEvent; + event: PaneviewDropEvent2; } export interface IPaneviewReactProps { @@ -62,18 +62,6 @@ export const PaneviewReact = React.forwardRef( }; }, [props.disableAutoResizing]); - React.useEffect(() => { - paneviewRef.current?.updateOptions({ - frameworkComponents: props.components, - }); - }, [props.components]); - - React.useEffect(() => { - paneviewRef.current?.updateOptions({ - headerframeworkComponents: props.headerComponents, - }); - }, [props.headerComponents]); - React.useEffect(() => { const createComponent = ( id: string, @@ -116,6 +104,19 @@ export const PaneviewReact = React.forwardRef( }; }, []); + React.useEffect(() => { + console.log(paneviewRef.current); + paneviewRef.current?.updateOptions({ + frameworkComponents: props.components, + }); + }, [props.components]); + + React.useEffect(() => { + paneviewRef.current?.updateOptions({ + headerframeworkComponents: props.headerComponents, + }); + }, [props.headerComponents]); + React.useEffect(() => { if (!paneviewRef.current) { return () => { @@ -127,7 +128,10 @@ export const PaneviewReact = React.forwardRef( const disposable = paneview.onDidDrop((event) => { if (props.onDidDrop) { - props.onDidDrop({ event, api: new PaneviewApi(paneview) }); + props.onDidDrop({ + event, + api: new PaneviewApi(paneview), + }); } });