Merge branch 'dnd-improvements' of https://github.com/mathuo/dockview into dnd-improvements

This commit is contained in:
mathuo 2021-10-02 12:19:40 +01:00
commit b0cf543d44
7 changed files with 285 additions and 100 deletions

View File

@ -1,7 +1,7 @@
<!-- <!DOCTYPE html> --> <!-- <!DOCTYPE html> -->
<html> <html>
<head> <head>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -1,34 +1,64 @@
export interface SerializedView {
id: string;
isExpanded: boolean;
}
export type ViewOptions = {
id: string;
title: string;
isExpanded: boolean;
isLocationEditable: boolean;
icon: string;
};
export interface View { export interface View {
readonly id: string; readonly id: string;
readonly isExpanded: boolean; readonly isExpanded: boolean;
readonly title: string; readonly title: string;
toJSON(): object; readonly isLocationEditable: boolean;
readonly icon: string;
toJSON(): SerializedView;
} }
export class DefaultView implements View { export class DefaultView implements View {
private readonly _id: string; private readonly _id: string;
private readonly _title: string; private readonly _title: string;
private readonly _isExpanded: boolean; private readonly _isExpanded: boolean;
private readonly _isLocationEditable: boolean;
private readonly _icon: string;
get id() { get id(): string {
return this._id; return this._id;
} }
get title() { get title(): string {
return this._title; return this._title;
} }
get isExpanded() { get isExpanded(): boolean {
return this._isExpanded; return this._isExpanded;
} }
constructor(id: string, title: string, isExpanded: boolean) { get isLocationEditable(): boolean {
this._id = id; return this._isLocationEditable;
this._title = title;
this._isExpanded = isExpanded;
} }
toJSON() { get icon(): string {
return { id: this.id, title: this.title, isExpanded: this.isExpanded }; return this._icon;
}
constructor(options: ViewOptions) {
this._id = options.id;
this._title = options.title;
this._isExpanded = options.isExpanded;
this._isLocationEditable = options.isLocationEditable;
this._icon = options.icon;
}
toJSON(): SerializedView {
return {
id: this.id,
isExpanded: this.isExpanded,
};
} }
} }

View File

@ -4,26 +4,31 @@ import {
Event, Event,
SerializedPaneview, SerializedPaneview,
} from 'dockview'; } from 'dockview';
import { DefaultView, View } from './view'; import { DefaultView, View, SerializedView } from './view';
import { IViewRegistry } from './viewRegistry';
export interface SerializedViewContainer {
readonly id: string;
readonly views: SerializedView[];
}
export interface ViewContainer<T = any> { export interface ViewContainer<T = any> {
readonly id: string; readonly id: string;
readonly canDelete: boolean;
readonly views: View[]; readonly views: View[];
readonly schema: T | undefined; readonly schema: T | any;
readonly icon: string;
readonly onDidAddView: Event<View>; readonly onDidAddView: Event<View>;
readonly onDidRemoveView: Event<View>; readonly onDidRemoveView: Event<View>;
addView(view: View, location?: number): void; addView(view: View, location?: number): void;
layout(schema: T): void; layout(schema: T): void;
removeView(view: View): void; removeView(view: View): void;
clear(): void; clear(): void;
toJSON(): object; toJSON(): SerializedViewContainer;
} }
export class PaneviewContainer implements ViewContainer<SerializedPaneview> { export class PaneviewContainer implements ViewContainer<SerializedPaneview> {
private readonly _id: string; private readonly _id: string;
private readonly _canDelete: boolean; private readonly _views: View[] = [];
private readonly _views: View[];
private readonly _onDidAddView = new Emitter<View>(); private readonly _onDidAddView = new Emitter<View>();
readonly onDidAddView = this._onDidAddView.event; readonly onDidAddView = this._onDidAddView.event;
@ -40,10 +45,6 @@ export class PaneviewContainer implements ViewContainer<SerializedPaneview> {
return this._views; return this._views;
} }
get canDelete() {
return this._canDelete;
}
get schema(): SerializedPaneview | undefined { get schema(): SerializedPaneview | undefined {
if (!this._schema) { if (!this._schema) {
this._schema = JSON.parse( this._schema = JSON.parse(
@ -53,21 +54,35 @@ export class PaneviewContainer implements ViewContainer<SerializedPaneview> {
return this._schema; return this._schema;
} }
constructor(id: string, canDelete: boolean) { get icon(): string {
this._id = id; const defaultIcon = 'search';
this._canDelete = canDelete; if (this.views.length > 0) {
return this.views.find((v) => !!v.icon)?.icon || defaultIcon;
const schema = this.schema; }
return defaultIcon;
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();
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); // this.addDisposables(this._onDidAddView, this._onDidRemoveView);
} }
@ -106,7 +121,7 @@ export class PaneviewContainer implements ViewContainer<SerializedPaneview> {
localStorage.removeItem(`viewcontainer_${this.id}`); localStorage.removeItem(`viewcontainer_${this.id}`);
} }
toJSON() { toJSON(): SerializedViewContainer {
return { id: this.id, views: this.views.map((v) => ({ id: v.id })) }; return { id: this.id, views: this.views.map((v) => v.toJSON()) };
} }
} }

View File

@ -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<string, RegisteredView>();
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,
});

View File

@ -1,12 +1,29 @@
import { Emitter, Event } from 'dockview'; import {
CompositeDisposable,
Emitter,
Event,
IDisposable,
IView,
} from 'dockview';
import { DefaultView, View } from './view'; import { DefaultView, View } from './view';
import { PaneviewContainer, ViewContainer } from './viewContainer'; import {
PaneviewContainer,
ViewContainer,
SerializedViewContainer,
} from './viewContainer';
import { IViewRegistry } from './viewRegistry';
export interface IViewService { export interface SerializedViewService {
containers: SerializedViewContainer[];
activeContainer?: string;
}
export interface IViewService extends IDisposable {
readonly containers: ViewContainer[]; readonly containers: ViewContainer[];
readonly onDidActiveContainerChange: Event<void>; readonly onDidActiveContainerChange: Event<void>;
readonly onDidRemoveContainer: Event<void>; readonly onDidRemoveContainer: Event<void>;
readonly onDidAddContainer: Event<void>; readonly onDidAddContainer: Event<void>;
readonly onDidContainersChange: Event<void>;
readonly activeContainer: ViewContainer | undefined; readonly activeContainer: ViewContainer | undefined;
addContainer(container: ViewContainer): void; addContainer(container: ViewContainer): void;
setActiveViewContainer(id: string): void; setActiveViewContainer(id: string): void;
@ -16,11 +33,13 @@ export interface IViewService {
targetViewContainer: ViewContainer, targetViewContainer: ViewContainer,
targetLocation: number targetLocation: number
): void; ): void;
insertContainerAfter(source: ViewContainer, target: ViewContainer): void;
addViews(view: View, viewContainer: ViewContainer, location?: number): void; addViews(view: View, viewContainer: ViewContainer, location?: number): void;
removeViews(removeViews: View[], viewContainer: ViewContainer): void; removeViews(removeViews: View[], viewContainer: ViewContainer): void;
getViewContainer(id: string): ViewContainer | undefined; getViewContainer(id: string): ViewContainer | undefined;
toJSON(): object; getViewContainer2(view: View): ViewContainer | undefined;
load(layout: any): void; toJSON(): SerializedViewService;
load(layout: SerializedViewService): void;
} }
export class ViewService implements IViewService { export class ViewService implements IViewService {
@ -46,21 +65,21 @@ export class ViewService implements IViewService {
); );
} }
constructor() { constructor(private readonly viewRegistry: IViewRegistry) {
// //
} }
load(layout: any): void { load(layout: SerializedViewService): void {
const { containers, activeContainer } = layout; const { containers, activeContainer } = layout;
for (const container of containers) { for (const container of containers) {
const { id, views } = container; const { id, views } = container;
const viewContainer = new PaneviewContainer(id, true); const viewContainer = new PaneviewContainer(
for (const view of views) { id,
viewContainer.addView( this.viewRegistry,
new DefaultView(view.id, view.title, view.isExpanded) views
); );
}
this.addContainer(viewContainer); this.addContainer(viewContainer);
} }
@ -166,10 +185,16 @@ export class ViewService implements IViewService {
return undefined; return undefined;
} }
toJSON() { toJSON(): SerializedViewService {
return { return {
containers: this.containers.map((c) => c.toJSON()), containers: this.containers.map((c) => c.toJSON()),
activeContainer: this.activeContainer.id, activeContainer: this.activeContainer.id,
}; };
} }
dispose(): void {
this._onDidActiveContainerChange.dispose();
this._onDidAddContainer.dispose();
this._onDidRemoveContainer.dispose();
}
} }

View File

@ -13,36 +13,88 @@ import {
import * as React from 'react'; import * as React from 'react';
import { useLayoutRegistry } from '../layout-grid/registry'; import { useLayoutRegistry } from '../layout-grid/registry';
import { PaneviewContainer, ViewContainer } from './viewContainer'; import { PaneviewContainer, ViewContainer } from './viewContainer';
import { ViewService } from './viewService'; import { IViewService, ViewService } from './viewService';
import { DefaultView } from './view'; import { DefaultView } from './view';
import { RegisteredView, VIEW_REGISTRY } from './viewRegistry';
const viewService = new ViewService(); 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'); const layout = localStorage.getItem('viewservice');
if (layout) { if (layout) {
viewService.load(JSON.parse(layout)); this.viewService.load(JSON.parse(layout));
} else { } else {
const container1 = new PaneviewContainer('c1', true); const container1 = new PaneviewContainer(
'default_container_1',
VIEW_REGISTRY
);
if (!container1.schema) { if (!container1.schema) {
container1.addView(new DefaultView('panel1', 'Panel 1', true)); this.addView(
container1.addView(new DefaultView('panel2', 'Panel 2', true)); container1,
VIEW_REGISTRY.getRegisteredView('search_widget')
);
this.addView(
container1,
VIEW_REGISTRY.getRegisteredView('home_widget')
);
} }
const container2 = new PaneviewContainer('c2', true); const container2 = new PaneviewContainer(
'default_container_2',
VIEW_REGISTRY
);
if (!container2.schema) { if (!container2.schema) {
container2.addView(new DefaultView('panel3', 'Panel 3', true)); this.addView(
container2.addView(new DefaultView('panel4', 'Panel 4', true)); container2,
VIEW_REGISTRY.getRegisteredView('account_widget')
);
this.addView(
container2,
VIEW_REGISTRY.getRegisteredView('settings_widget')
);
} }
viewService.addContainer(container1); this.viewService.addContainer(container1);
viewService.addContainer(container2); this.viewService.addContainer(container2);
} }
const save = () => { const save = () => {
localStorage.setItem('viewservice', JSON.stringify(viewService.toJSON())); localStorage.setItem(
'viewservice',
JSON.stringify(this.viewService.toJSON())
);
}; };
viewService.onDidActiveContainerChange(save); this.viewService.onDidActiveContainerChange(save);
viewService.onDidRemoveContainer(save); this.viewService.onDidRemoveContainer(save);
viewService.onDidAddContainer(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<IPaneviewPanelProps<any>> = { const components: PanelCollection<IPaneviewPanelProps<any>> = {
default: (props: IPaneviewPanelProps<{ viewId: string }>) => { default: (props: IPaneviewPanelProps<{ viewId: string }>) => {
@ -56,27 +108,27 @@ const components: PanelCollection<IPaneviewPanelProps<any>> = {
export const Activitybar = (props: IGridviewPanelProps) => { export const Activitybar = (props: IGridviewPanelProps) => {
const [activeContainerid, setActiveContainerId] = React.useState<string>( const [activeContainerid, setActiveContainerId] = React.useState<string>(
viewService.activeContainer.id viewService.model.activeContainer.id
); );
const [containers, setContainers] = React.useState<ViewContainer[]>( const [containers, setContainers] = React.useState<ViewContainer[]>(
viewService.containers viewService.model.containers
); );
const registry = useLayoutRegistry(); const registry = useLayoutRegistry();
React.useEffect(() => { React.useEffect(() => {
const disposable = new CompositeDisposable( const disposable = new CompositeDisposable(
viewService.onDidActiveContainerChange(() => { viewService.model.onDidActiveContainerChange(() => {
setActiveContainerId(viewService.activeContainer.id); setActiveContainerId(viewService.model.activeContainer.id);
}), }),
viewService.onDidAddContainer(() => { viewService.model.onDidAddContainer(() => {
setContainers(viewService.containers); setContainers(viewService.model.containers);
}), }),
viewService.onDidRemoveContainer(() => { viewService.model.onDidRemoveContainer(() => {
setContainers(viewService.containers); setContainers(viewService.model.containers);
}), }),
viewService.onDidContainersChange(() => { viewService.model.onDidContainersChange(() => {
setContainers(viewService.containers); setContainers(viewService.model.containers);
}) })
); );
@ -103,7 +155,7 @@ export const Activitybar = (props: IGridviewPanelProps) => {
sidebarPanel.focus(); sidebarPanel.focus();
} }
viewService.setActiveViewContainer(container.id); viewService.model.setActiveViewContainer(container.id);
}; };
const onDrop = (targetContainer: ViewContainer) => ( const onDrop = (targetContainer: ViewContainer) => (
@ -112,8 +164,13 @@ export const Activitybar = (props: IGridviewPanelProps) => {
const data = event.dataTransfer.getData('application/json'); const data = event.dataTransfer.getData('application/json');
if (data) { if (data) {
const { container } = JSON.parse(data); const { container } = JSON.parse(data);
const sourceContainer = viewService.getViewContainer(container); const sourceContainer = viewService.model.getViewContainer(
viewService.insertContainerAfter(sourceContainer, targetContainer); container
);
viewService.model.insertContainerAfter(
sourceContainer,
targetContainer
);
} }
}; };
@ -121,16 +178,16 @@ export const Activitybar = (props: IGridviewPanelProps) => {
const data = getPaneData(); const data = getPaneData();
if (data) { if (data) {
const { paneId } = data; const { paneId } = data;
const view = viewService.getView(paneId); const view = viewService.model.getView(paneId);
const viewContainer = viewService.getViewContainer2(view); const viewContainer = viewService.model.getViewContainer2(view);
viewService.removeViews([view], viewContainer); viewService.model.removeViews([view], viewContainer);
// viewContainer.removeView(view); // viewContainer.removeView(view);
const newContainer = new PaneviewContainer( const newContainer = new PaneviewContainer(
`t_${Date.now().toString().substr(5)}`, `t_${Date.now().toString().substr(5)}`,
true VIEW_REGISTRY
); );
newContainer.addView(view); newContainer.addView(view);
viewService.addContainer(newContainer); viewService.model.addContainer(newContainer);
} }
}; };
@ -157,6 +214,9 @@ export const Activitybar = (props: IGridviewPanelProps) => {
}} }}
onDrop={onDrop(container)} onDrop={onDrop(container)}
style={{ style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '48px', height: '48px',
boxSizing: 'border-box', boxSizing: 'border-box',
borderLeft: isActive borderLeft: isActive
@ -165,7 +225,13 @@ export const Activitybar = (props: IGridviewPanelProps) => {
}} }}
key={i} key={i}
> >
{container.id} {/* {container.id} */}
<span
style={{ fontSize: '30px' }}
className="material-icons-outlined"
>
{container.icon}
</span>
</div> </div>
); );
})} })}
@ -185,12 +251,12 @@ export const Activitybar = (props: IGridviewPanelProps) => {
export const Sidebar = () => { export const Sidebar = () => {
const [sidebarId, setSidebarId] = React.useState<string>( const [sidebarId, setSidebarId] = React.useState<string>(
viewService.activeContainer.id viewService.model.activeContainer.id
); );
React.useEffect(() => { React.useEffect(() => {
const disposable = viewService.onDidActiveContainerChange(() => { const disposable = viewService.model.onDidActiveContainerChange(() => {
setSidebarId(viewService.activeContainer.id); setSidebarId(viewService.model.activeContainer.id);
}); });
return () => { return () => {
@ -211,7 +277,7 @@ export const SidebarPart = (props: { id: string }) => {
}; };
} }
const viewContainer = viewService.getViewContainer(props.id); const viewContainer = viewService.model.getViewContainer(props.id);
const disposables = new CompositeDisposable( const disposables = new CompositeDisposable(
api.onDidLayoutChange(() => { api.onDidLayoutChange(() => {
@ -271,10 +337,10 @@ export const SidebarPart = (props: { id: string }) => {
} }
const viewId = data.paneId; const viewId = data.paneId;
const viewContainer = viewService.getViewContainer(props.id); const viewContainer = viewService.model.getViewContainer(props.id);
const view = viewService.getView(viewId); const view = viewService.model.getView(viewId);
viewService.moveViewToLocation(view, viewContainer, 0); viewService.model.moveViewToLocation(view, viewContainer, 0);
}; };
if (!props.id) { if (!props.id) {

View File

@ -119,7 +119,7 @@ export const PaneviewReact = React.forwardRef(
React.useEffect(() => { React.useEffect(() => {
if (!paneviewRef.current) { if (!paneviewRef.current) {
return () => { return () => {
// noop //
}; };
} }