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> -->
<html>
<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>
<body>
<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 {
readonly id: string;
readonly isExpanded: boolean;
readonly title: string;
toJSON(): object;
readonly isLocationEditable: boolean;
readonly icon: string;
toJSON(): SerializedView;
}
export class DefaultView implements View {
private readonly _id: string;
private readonly _title: string;
private readonly _isExpanded: boolean;
private readonly _isLocationEditable: boolean;
private readonly _icon: string;
get id() {
get id(): string {
return this._id;
}
get title() {
get title(): string {
return this._title;
}
get isExpanded() {
get isExpanded(): boolean {
return this._isExpanded;
}
constructor(id: string, title: string, isExpanded: boolean) {
this._id = id;
this._title = title;
this._isExpanded = isExpanded;
get isLocationEditable(): boolean {
return this._isLocationEditable;
}
toJSON() {
return { id: this.id, title: this.title, isExpanded: this.isExpanded };
get icon(): string {
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,
SerializedPaneview,
} 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> {
readonly id: string;
readonly canDelete: boolean;
readonly views: View[];
readonly schema: T | undefined;
readonly schema: T | any;
readonly icon: string;
readonly onDidAddView: Event<View>;
readonly onDidRemoveView: Event<View>;
addView(view: View, location?: number): void;
layout(schema: T): void;
removeView(view: View): void;
clear(): void;
toJSON(): object;
toJSON(): SerializedViewContainer;
}
export class PaneviewContainer implements ViewContainer<SerializedPaneview> {
private readonly _id: string;
private readonly _canDelete: boolean;
private readonly _views: View[];
private readonly _views: View[] = [];
private readonly _onDidAddView = new Emitter<View>();
readonly onDidAddView = this._onDidAddView.event;
@ -40,10 +45,6 @@ export class PaneviewContainer implements ViewContainer<SerializedPaneview> {
return this._views;
}
get canDelete() {
return this._canDelete;
}
get schema(): SerializedPaneview | undefined {
if (!this._schema) {
this._schema = JSON.parse(
@ -53,21 +54,35 @@ export class PaneviewContainer implements ViewContainer<SerializedPaneview> {
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 = [];
get icon(): string {
const defaultIcon = 'search';
if (this.views.length > 0) {
return this.views.find((v) => !!v.icon)?.icon || defaultIcon;
}
// super();
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);
}
@ -106,7 +121,7 @@ export class PaneviewContainer implements ViewContainer<SerializedPaneview> {
localStorage.removeItem(`viewcontainer_${this.id}`);
}
toJSON() {
return { id: this.id, views: this.views.map((v) => ({ id: v.id })) };
toJSON(): SerializedViewContainer {
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 { 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 onDidActiveContainerChange: Event<void>;
readonly onDidRemoveContainer: Event<void>;
readonly onDidAddContainer: Event<void>;
readonly onDidContainersChange: Event<void>;
readonly activeContainer: ViewContainer | undefined;
addContainer(container: ViewContainer): void;
setActiveViewContainer(id: string): void;
@ -16,11 +33,13 @@ export interface IViewService {
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;
toJSON(): object;
load(layout: any): void;
getViewContainer2(view: View): ViewContainer | undefined;
toJSON(): SerializedViewService;
load(layout: SerializedViewService): void;
}
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;
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)
);
}
const viewContainer = new PaneviewContainer(
id,
this.viewRegistry,
views
);
this.addContainer(viewContainer);
}
@ -166,10 +185,16 @@ export class ViewService implements IViewService {
return undefined;
}
toJSON() {
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();
}
}

View File

@ -13,36 +13,88 @@ import {
import * as React from 'react';
import { useLayoutRegistry } from '../layout-grid/registry';
import { PaneviewContainer, ViewContainer } from './viewContainer';
import { ViewService } from './viewService';
import { IViewService, ViewService } from './viewService';
import { DefaultView } from './view';
import { RegisteredView, VIEW_REGISTRY } from './viewRegistry';
const viewService = new ViewService();
class ViewServiceModel {
private readonly viewService: IViewService;
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));
get model() {
return this.viewService;
}
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));
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,
})
);
}
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 viewService = new ViewServiceModel();
const components: PanelCollection<IPaneviewPanelProps<any>> = {
default: (props: IPaneviewPanelProps<{ viewId: string }>) => {
@ -56,27 +108,27 @@ const components: PanelCollection<IPaneviewPanelProps<any>> = {
export const Activitybar = (props: IGridviewPanelProps) => {
const [activeContainerid, setActiveContainerId] = React.useState<string>(
viewService.activeContainer.id
viewService.model.activeContainer.id
);
const [containers, setContainers] = React.useState<ViewContainer[]>(
viewService.containers
viewService.model.containers
);
const registry = useLayoutRegistry();
React.useEffect(() => {
const disposable = new CompositeDisposable(
viewService.onDidActiveContainerChange(() => {
setActiveContainerId(viewService.activeContainer.id);
viewService.model.onDidActiveContainerChange(() => {
setActiveContainerId(viewService.model.activeContainer.id);
}),
viewService.onDidAddContainer(() => {
setContainers(viewService.containers);
viewService.model.onDidAddContainer(() => {
setContainers(viewService.model.containers);
}),
viewService.onDidRemoveContainer(() => {
setContainers(viewService.containers);
viewService.model.onDidRemoveContainer(() => {
setContainers(viewService.model.containers);
}),
viewService.onDidContainersChange(() => {
setContainers(viewService.containers);
viewService.model.onDidContainersChange(() => {
setContainers(viewService.model.containers);
})
);
@ -103,7 +155,7 @@ export const Activitybar = (props: IGridviewPanelProps) => {
sidebarPanel.focus();
}
viewService.setActiveViewContainer(container.id);
viewService.model.setActiveViewContainer(container.id);
};
const onDrop = (targetContainer: ViewContainer) => (
@ -112,8 +164,13 @@ export const Activitybar = (props: IGridviewPanelProps) => {
const data = event.dataTransfer.getData('application/json');
if (data) {
const { container } = JSON.parse(data);
const sourceContainer = viewService.getViewContainer(container);
viewService.insertContainerAfter(sourceContainer, targetContainer);
const sourceContainer = viewService.model.getViewContainer(
container
);
viewService.model.insertContainerAfter(
sourceContainer,
targetContainer
);
}
};
@ -121,16 +178,16 @@ export const Activitybar = (props: IGridviewPanelProps) => {
const data = getPaneData();
if (data) {
const { paneId } = data;
const view = viewService.getView(paneId);
const viewContainer = viewService.getViewContainer2(view);
viewService.removeViews([view], viewContainer);
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)}`,
true
VIEW_REGISTRY
);
newContainer.addView(view);
viewService.addContainer(newContainer);
viewService.model.addContainer(newContainer);
}
};
@ -157,6 +214,9 @@ export const Activitybar = (props: IGridviewPanelProps) => {
}}
onDrop={onDrop(container)}
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '48px',
boxSizing: 'border-box',
borderLeft: isActive
@ -165,7 +225,13 @@ export const Activitybar = (props: IGridviewPanelProps) => {
}}
key={i}
>
{container.id}
{/* {container.id} */}
<span
style={{ fontSize: '30px' }}
className="material-icons-outlined"
>
{container.icon}
</span>
</div>
);
})}
@ -185,12 +251,12 @@ export const Activitybar = (props: IGridviewPanelProps) => {
export const Sidebar = () => {
const [sidebarId, setSidebarId] = React.useState<string>(
viewService.activeContainer.id
viewService.model.activeContainer.id
);
React.useEffect(() => {
const disposable = viewService.onDidActiveContainerChange(() => {
setSidebarId(viewService.activeContainer.id);
const disposable = viewService.model.onDidActiveContainerChange(() => {
setSidebarId(viewService.model.activeContainer.id);
});
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(
api.onDidLayoutChange(() => {
@ -271,10 +337,10 @@ export const SidebarPart = (props: { id: string }) => {
}
const viewId = data.paneId;
const viewContainer = viewService.getViewContainer(props.id);
const view = viewService.getView(viewId);
const viewContainer = viewService.model.getViewContainer(props.id);
const view = viewService.model.getView(viewId);
viewService.moveViewToLocation(view, viewContainer, 0);
viewService.model.moveViewToLocation(view, viewContainer, 0);
};
if (!props.id) {

View File

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