This commit is contained in:
mathuo 2020-08-31 21:43:37 +01:00
parent 47f27633da
commit 62b2e30ed6
34 changed files with 924 additions and 382 deletions

2
.gitignore vendored
View File

@ -2,4 +2,4 @@ node_modules/
dist/ dist/
typedocs/ typedocs/
.DS_Store .DS_Store
lerna-debug.log *-debug.log

1
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1 @@
{}

165
package-lock.json generated
View File

@ -1,5 +1,5 @@
{ {
"name": "splitview", "name": "splitview-root",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
@ -611,6 +611,57 @@
} }
} }
}, },
"@gulp-sourcemaps/identity-map": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-1.0.2.tgz",
"integrity": "sha512-ciiioYMLdo16ShmfHBXJBOFm3xPC4AuwO4xeRpFeHz7WK9PYsWCmigagG2XyzZpubK4a3qNKoUBDhbzHfa50LQ==",
"dev": true,
"requires": {
"acorn": "^5.0.3",
"css": "^2.2.1",
"normalize-path": "^2.1.1",
"source-map": "^0.6.0",
"through2": "^2.0.3"
},
"dependencies": {
"acorn": {
"version": "5.7.4",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz",
"integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==",
"dev": true
},
"normalize-path": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
"integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
"dev": true,
"requires": {
"remove-trailing-separator": "^1.0.1"
}
}
}
},
"@gulp-sourcemaps/map-sources": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz",
"integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o=",
"dev": true,
"requires": {
"normalize-path": "^2.0.1",
"through2": "^2.0.3"
},
"dependencies": {
"normalize-path": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
"integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
"dev": true,
"requires": {
"remove-trailing-separator": "^1.0.1"
}
}
}
},
"@istanbuljs/load-nyc-config": { "@istanbuljs/load-nyc-config": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@ -5788,6 +5839,28 @@
"ms": "^2.1.1" "ms": "^2.1.1"
} }
}, },
"debug-fabulous": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz",
"integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==",
"dev": true,
"requires": {
"debug": "3.X",
"memoizee": "0.4.X",
"object-assign": "4.X"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
}
}
},
"debuglog": { "debuglog": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz",
@ -6467,6 +6540,16 @@
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
"dev": true "dev": true
}, },
"event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
"dev": true,
"requires": {
"d": "1",
"es5-ext": "~0.10.14"
}
},
"eventemitter3": { "eventemitter3": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz",
@ -8809,6 +8892,39 @@
} }
} }
}, },
"gulp-sourcemaps": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-2.6.5.tgz",
"integrity": "sha512-SYLBRzPTew8T5Suh2U8jCSDKY+4NARua4aqjj8HOysBh2tSgT9u4jc1FYirAdPx1akUxxDeK++fqw6Jg0LkQRg==",
"dev": true,
"requires": {
"@gulp-sourcemaps/identity-map": "1.X",
"@gulp-sourcemaps/map-sources": "1.X",
"acorn": "5.X",
"convert-source-map": "1.X",
"css": "2.X",
"debug-fabulous": "1.X",
"detect-newline": "2.X",
"graceful-fs": "4.X",
"source-map": "~0.6.0",
"strip-bom-string": "1.X",
"through2": "2.X"
},
"dependencies": {
"acorn": {
"version": "5.7.4",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz",
"integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==",
"dev": true
},
"detect-newline": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz",
"integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=",
"dev": true
}
}
},
"gulp-typescript": { "gulp-typescript": {
"version": "6.0.0-alpha.1", "version": "6.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-6.0.0-alpha.1.tgz", "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-6.0.0-alpha.1.tgz",
@ -9916,6 +10032,12 @@
"integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=", "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=",
"dev": true "dev": true
}, },
"is-promise": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
"dev": true
},
"is-regex": { "is-regex": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz",
@ -11710,6 +11832,15 @@
"yallist": "^2.1.2" "yallist": "^2.1.2"
} }
}, },
"lru-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
"integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=",
"dev": true,
"requires": {
"es5-ext": "~0.10.2"
}
},
"macos-release": { "macos-release": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz",
@ -11967,6 +12098,22 @@
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"dev": true "dev": true
}, },
"memoizee": {
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz",
"integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==",
"dev": true,
"requires": {
"d": "1",
"es5-ext": "^0.10.45",
"es6-weak-map": "^2.0.2",
"event-emitter": "^0.3.5",
"is-promise": "^2.1",
"lru-queue": "0.1",
"next-tick": "1",
"timers-ext": "^0.1.5"
}
},
"memory-fs": { "memory-fs": {
"version": "0.5.0", "version": "0.5.0",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz",
@ -15658,6 +15805,12 @@
"integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
"dev": true "dev": true
}, },
"strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=",
"dev": true
},
"strip-eof": { "strip-eof": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
@ -15962,6 +16115,16 @@
"setimmediate": "^1.0.4" "setimmediate": "^1.0.4"
} }
}, },
"timers-ext": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz",
"integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==",
"dev": true,
"requires": {
"es5-ext": "~0.10.46",
"next-tick": "1"
}
},
"tmp": { "tmp": {
"version": "0.0.33", "version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",

View File

@ -25,6 +25,7 @@
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-header": "^2.0.9", "gulp-header": "^2.0.9",
"gulp-sass": "^4.1.0", "gulp-sass": "^4.1.0",
"gulp-sourcemaps": "^2.6.5",
"gulp-typescript": "^6.0.0-alpha.1", "gulp-typescript": "^6.0.0-alpha.1",
"jest": "^26.1.0", "jest": "^26.1.0",
"jsdom": "^16.2.2", "jsdom": "^16.2.2",

View File

@ -0,0 +1,33 @@
import * as React from "react";
import { Api, IPanelProps } from "splitview";
export const Editor = (props: IPanelProps & { layoutApi: Api }) => {
const [tabHeight, setTabHeight] = React.useState<number>(0);
React.useEffect(() => {
if (props.layoutApi) {
setTabHeight(props.layoutApi.getTabHeight());
}
}, [props.layoutApi]);
const onTabHeightChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = Number(event.target.value);
if (!Number.isNaN(value)) {
setTabHeight(value);
}
};
const onClick = () => {
props.layoutApi.setTabHeight(tabHeight);
};
return (
<div style={{ height: "100%", backgroundColor: "white", color: "black" }}>
<label>
Tab height
<input onChange={onTabHeightChange} value={tabHeight} type="number" />
<button onClick={onClick}>Apply</button>
</label>
</div>
);
};

View File

@ -10,39 +10,9 @@ import {
GroupChangeKind, GroupChangeKind,
} from "splitview"; } from "splitview";
import { CustomTab } from "./customTab"; import { CustomTab } from "./customTab";
import { Editor } from "./editorPanel";
import { SplitPanel } from "./splitPanel"; import { SplitPanel } from "./splitPanel";
const Editor = (props: IPanelProps & { layoutApi: Api }) => {
const [tabHeight, setTabHeight] = React.useState<number>(0);
React.useEffect(() => {
if (props.layoutApi) {
setTabHeight(props.layoutApi.getTabHeight());
}
}, [props.layoutApi]);
const onTabHeightChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = Number(event.target.value);
if (!Number.isNaN(value)) {
setTabHeight(value);
}
};
const onClick = () => {
props.layoutApi.setTabHeight(tabHeight);
};
return (
<div style={{ height: "100%", backgroundColor: "white", color: "black" }}>
<label>
Tab height
<input onChange={onTabHeightChange} value={tabHeight} type="number" />
<button onClick={onClick}>Apply</button>
</label>
</div>
);
};
const components = { const components = {
inner_component: (props: IPanelProps) => { inner_component: (props: IPanelProps) => {
const _api = React.useRef<Api>(); const _api = React.useRef<Api>();
@ -51,7 +21,7 @@ const components = {
const onReady = (event: OnReadyEvent) => { const onReady = (event: OnReadyEvent) => {
_api.current = event.api; _api.current = event.api;
const layout = props.api.getState()["layout"]; const layout = props.api.getStateKey<object>("layout");
if (layout) { if (layout) {
event.api.deserialize(layout); event.api.deserialize(layout);
} else { } else {

View File

@ -1,6 +1,8 @@
import * as React from "react"; import * as React from "react";
import { import {
CompositeDisposable,
IPanelProps, IPanelProps,
ISplitviewPanelProps,
Orientation, Orientation,
SplitviewFacade, SplitviewFacade,
SplitviewReadyEvent, SplitviewReadyEvent,
@ -8,7 +10,20 @@ import {
import { SplitViewComponent } from "splitview"; import { SplitViewComponent } from "splitview";
const components = { const components = {
default1: (props) => { default1: (props: ISplitviewPanelProps) => {
React.useEffect(() => {
const disposable = new CompositeDisposable();
disposable.addDisposables(
props.api.onDidPanelDimensionChange((event) => {
//
})
);
return () => {
disposable.dispose();
};
}, []);
return <div style={{ height: "100%", width: "100%" }}>hiya</div>; return <div style={{ height: "100%", width: "100%" }}>hiya</div>;
}, },
}; };
@ -18,26 +33,47 @@ export const SplitPanel = (props: IPanelProps) => {
React.useEffect(() => { React.useEffect(() => {
props.api.onDidPanelDimensionChange((event) => { props.api.onDidPanelDimensionChange((event) => {
// const [height,width] = [event.height, event.width] api.current?.layout(event.width, event.height - 20);
// const [size, orthogonalSize] = });
// props.orientation === Orientation.HORIZONTAL
// ? [width, height] api.current.onChange((event) => {
// : [height, width]; props.api.setState("sview_layout", api.current.toJSON());
api.current?.layout(event.width, event.height);
}); });
}, []); }, []);
const onReady = (event: SplitviewReadyEvent) => { const onReady = (event: SplitviewReadyEvent) => {
event.api.addFromComponent({ id: "1", component: "default1" }); const existingLayout = props.api.getStateKey("sview_layout");
event.api.addFromComponent({ id: "2", component: "default1" });
if (existingLayout) {
event.api.deserialize(existingLayout);
} else {
event.api.addFromComponent({ id: "1", component: "default1" });
event.api.addFromComponent({ id: "2", component: "default1" });
}
api.current = event.api; api.current = event.api;
}; };
const onSave = () => {
props.api.setState("sview_layout", api.current.toJSON());
};
return ( return (
<SplitViewComponent <div
components={components} style={{
onReady={onReady} display: "flex",
orientation={Orientation.VERTICAL} flexDirection: "column",
/> height: "100%",
color: "white",
}}
>
<div style={{ height: "20px", flexShrink: 0 }}>
<button onClick={onSave}>save</button>
</div>
<SplitViewComponent
components={components}
onReady={onReady}
orientation={Orientation.VERTICAL}
/>
</div>
); );
}; };

View File

@ -4,6 +4,7 @@
"description": "", "description": "",
"main": "dist/esm/index.js", "main": "dist/esm/index.js",
"types": "dist/esm/index.d.ts", "types": "dist/esm/index.d.ts",
"module": "dist/esm/index.js",
"scripts": { "scripts": {
"build": "gulp run", "build": "gulp run",
"docs": "typedoc" "docs": "typedoc"

View File

@ -7,7 +7,7 @@ import { Event, Emitter, addDisposableListener } from "../events";
import { IGroupAccessor, Layout } from "../layout"; import { IGroupAccessor, Layout } from "../layout";
import { toggleClass } from "../dom"; import { toggleClass } from "../dom";
import { ClosePanelResult, WatermarkPart } from "./panel/parts"; import { ClosePanelResult, WatermarkPart } from "./panel/parts";
import { IPanel } from "./panel/types"; import { IGroupPanel } from "./panel/types";
import { timeoutPromise } from "../async"; import { timeoutPromise } from "../async";
import { import {
extractData, extractData,
@ -52,40 +52,43 @@ interface GroupMoveEvent {
} }
export interface GroupOptions { export interface GroupOptions {
panels: IPanel[]; panels: IGroupPanel[];
activePanel?: IPanel; activePanel?: IGroupPanel;
} }
export interface GroupChangeEvent { export interface GroupChangeEvent {
kind: GroupChangeKind; kind: GroupChangeKind;
panel?: IPanel; panel?: IGroupPanel;
} }
export interface IGroupview extends IDisposable, IGridView { export interface IGroupview extends IDisposable, IGridView {
id: string; id: string;
size: number; size: number;
panels: IPanel[]; panels: IGroupPanel[];
tabHeight: number; tabHeight: number;
setActive: (isActive: boolean) => void; setActive: (isActive: boolean) => void;
// state // state
isPanelActive: (panel: IPanel) => boolean; isPanelActive: (panel: IGroupPanel) => boolean;
isActive: boolean; isActive: boolean;
activePanel: IPanel; activePanel: IGroupPanel;
indexOf(panel: IPanel): number; indexOf(panel: IGroupPanel): number;
// panel lifecycle // panel lifecycle
openPanel(panel: IPanel, index?: number): void; openPanel(panel: IGroupPanel, index?: number): void;
closePanel(panel: IPanel): Promise<boolean>; closePanel(panel: IGroupPanel): Promise<boolean>;
closeAllPanels(): Promise<boolean>; closeAllPanels(): Promise<boolean>;
containsPanel(panel: IPanel): boolean; containsPanel(panel: IGroupPanel): boolean;
removePanel: (panelOrId: IPanel | string) => IPanel; removePanel: (panelOrId: IGroupPanel | string) => IGroupPanel;
// events // events
onDidGroupChange: Event<{ kind: GroupChangeKind }>; onDidGroupChange: Event<{ kind: GroupChangeKind }>;
onMove: Event<GroupMoveEvent>; onMove: Event<GroupMoveEvent>;
// //
startActiveDrag(panel: IPanel): IDisposable; startActiveDrag(panel: IGroupPanel): IDisposable;
// //
moveToNext(options?: { panel?: IPanel; suppressRoll?: boolean }): void; moveToNext(options?: { panel?: IGroupPanel; suppressRoll?: boolean }): void;
moveToPrevious(options?: { panel?: IPanel; suppressRoll?: boolean }): void; moveToPrevious(options?: {
panel?: IGroupPanel;
suppressRoll?: boolean;
}): void;
} }
export interface GroupDropEvent { export interface GroupDropEvent {
@ -100,14 +103,14 @@ export class Groupview extends CompositeDisposable implements IGroupview {
private tabContainer: ITabContainer; private tabContainer: ITabContainer;
private contentContainer: IContentContainer; private contentContainer: IContentContainer;
private _active: boolean; private _active: boolean;
private _activePanel: IPanel; private _activePanel: IGroupPanel;
private dropTarget: Droptarget; private dropTarget: Droptarget;
private watermark: WatermarkPart; private watermark: WatermarkPart;
private _width: number; private _width: number;
private _height: number; private _height: number;
private _panels: IPanel[] = []; private _panels: IGroupPanel[] = [];
private readonly _onMove = new Emitter<GroupMoveEvent>(); private readonly _onMove = new Emitter<GroupMoveEvent>();
readonly onMove: Event<GroupMoveEvent> = this._onMove.event; readonly onMove: Event<GroupMoveEvent> = this._onMove.event;
@ -168,7 +171,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
return Number.MAX_SAFE_INTEGER; return Number.MAX_SAFE_INTEGER;
} }
public indexOf(panel: IPanel) { public indexOf(panel: IGroupPanel) {
return this.tabContainer.indexOf(panel.id); return this.tabContainer.indexOf(panel.id);
} }
@ -179,7 +182,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
}; };
} }
public startActiveDrag(panel: IPanel): IDisposable { public startActiveDrag(panel: IGroupPanel): IDisposable {
const index = this.tabContainer.indexOf(panel.id); const index = this.tabContainer.indexOf(panel.id);
if (index > -1) { if (index > -1) {
const tab = this.tabContainer.at(index); const tab = this.tabContainer.at(index);
@ -193,7 +196,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
return Disposable.NONE; return Disposable.NONE;
} }
public moveToNext(options?: { panel?: IPanel; suppressRoll?: boolean }) { public moveToNext(options?: { panel?: IGroupPanel; suppressRoll?: boolean }) {
if (!options) { if (!options) {
options = {}; options = {};
} }
@ -218,7 +221,10 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.openPanel(this.panels[normalizedIndex]); this.openPanel(this.panels[normalizedIndex]);
} }
public moveToPrevious(options?: { panel?: IPanel; suppressRoll?: boolean }) { public moveToPrevious(options?: {
panel?: IGroupPanel;
suppressRoll?: boolean;
}) {
if (!options) { if (!options) {
options = {}; options = {};
} }
@ -243,7 +249,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.openPanel(this.panels[normalizedIndex]); this.openPanel(this.panels[normalizedIndex]);
} }
public containsPanel(panel: IPanel) { public containsPanel(panel: IGroupPanel) {
return this.panels.includes(panel); return this.panels.includes(panel);
} }
@ -313,7 +319,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.updateContainer(); this.updateContainer();
} }
public openPanel(panel: IPanel, index: number = this.panels.length) { public openPanel(panel: IGroupPanel, index: number = this.panels.length) {
if (this._activePanel === panel) { if (this._activePanel === panel) {
this.accessor.doSetGroupActive(this); this.accessor.doSetGroupActive(this);
return; return;
@ -330,7 +336,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.updateContainer(); this.updateContainer();
} }
public removePanel(groupItemOrId: IPanel | string): IPanel { public removePanel(groupItemOrId: IGroupPanel | string): IGroupPanel {
const id = const id =
typeof groupItemOrId === "string" ? groupItemOrId : groupItemOrId.id; typeof groupItemOrId === "string" ? groupItemOrId : groupItemOrId.id;
@ -386,7 +392,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
return true; return true;
} }
public closePanel = async (panel: IPanel) => { public closePanel = async (panel: IGroupPanel) => {
if (panel.close && (await panel.close()) === ClosePanelResult.DONT_CLOSE) { if (panel.close && (await panel.close()) === ClosePanelResult.DONT_CLOSE) {
return false; return false;
} }
@ -395,7 +401,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
return true; return true;
}; };
private doClose(panel: IPanel) { private doClose(panel: IGroupPanel) {
this._removePanel(panel); this._removePanel(panel);
(this.accessor as Layout).unregisterPanel(panel); (this.accessor as Layout).unregisterPanel(panel);
@ -407,7 +413,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
} }
} }
public isPanelActive(panel: IPanel) { public isPanelActive(panel: IGroupPanel) {
return this._activePanel === panel; return this._activePanel === panel;
} }
@ -447,7 +453,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
} }
} }
private _removePanel(panel: IPanel) { private _removePanel(panel: IGroupPanel) {
const index = this._panels.indexOf(panel); const index = this._panels.indexOf(panel);
const isActivePanel = this._activePanel === panel; const isActivePanel = this._activePanel === panel;
@ -467,7 +473,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
return panel; return panel;
} }
private doRemovePanel(panel: IPanel) { private doRemovePanel(panel: IGroupPanel) {
const index = this.panels.indexOf(panel); const index = this.panels.indexOf(panel);
if (this._activePanel === panel) { if (this._activePanel === panel) {
@ -480,7 +486,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this._onDidGroupChange.fire({ kind: GroupChangeKind.REMOVE_PANEL, panel }); this._onDidGroupChange.fire({ kind: GroupChangeKind.REMOVE_PANEL, panel });
} }
private doAddPanel(panel: IPanel, index: number) { private doAddPanel(panel: IGroupPanel, index: number) {
const existingPanel = this._panels.indexOf(panel); const existingPanel = this._panels.indexOf(panel);
const hasExistingPabel = existingPanel > -1; const hasExistingPabel = existingPanel > -1;
@ -494,7 +500,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this._onDidGroupChange.fire({ kind: GroupChangeKind.ADD_PANEL }); this._onDidGroupChange.fire({ kind: GroupChangeKind.ADD_PANEL });
} }
private doSetActivePanel(panel: IPanel) { private doSetActivePanel(panel: IGroupPanel) {
this._activePanel = panel; this._activePanel = panel;
this.tabContainer.setActivePanel(panel); this.tabContainer.setActivePanel(panel);
panel.layout(this._width, this._height); panel.layout(this._width, this._height);

View File

@ -1,53 +1,39 @@
import { IGroupview } from "../groupview"; import { IGroupview } from "../groupview";
import { Event, Emitter } from "../../events"; import { Event } from "../../events";
import { ClosePanelResult } from "./parts"; import { ClosePanelResult } from "./parts";
import { IPanel } from "./types"; import { IGroupPanel } from "./types";
import { CompositeDisposable, IDisposable } from "../../lifecycle"; import {
BasePanelApi,
IBasePanelApi,
PanelDimensionChangeEvent,
} from "../../panel/api";
export interface PanelStateChangeEvent { export interface PanelStateChangeEvent {
isPanelVisible: boolean; isPanelVisible: boolean;
isGroupActive: boolean; isGroupActive: boolean;
} }
export interface PanelDimensionChangeEvent { export interface PanelApi extends IBasePanelApi {
width: number;
height: number;
}
export interface PanelApi extends IDisposable {
onDidPanelStateChange: Event<PanelStateChangeEvent>; onDidPanelStateChange: Event<PanelStateChangeEvent>;
onDidPanelDimensionChange: Event<PanelDimensionChangeEvent>;
isPanelVisible: boolean; isPanelVisible: boolean;
isGroupActive: boolean; isGroupActive: boolean;
group: IGroupview; group: IGroupview;
close: () => Promise<boolean>; close: () => Promise<boolean>;
setClosePanelHook(callback: () => Promise<ClosePanelResult>): void; setClosePanelHook(callback: () => Promise<ClosePanelResult>): void;
canClose: () => Promise<ClosePanelResult>; canClose: () => Promise<ClosePanelResult>;
setState(key: string, value: any);
setState(state: { [index: string]: any });
getState: () => { [index: string]: any };
onDidStateChange: Event<any>;
onDidDirtyChange: Event<boolean>; onDidDirtyChange: Event<boolean>;
} }
export class PanelApiImpl extends CompositeDisposable implements PanelApi { export class PanelApiImpl extends BasePanelApi implements PanelApi {
private _isPanelVisible: boolean; private _isPanelVisible: boolean;
private _isGroupActive: boolean; private _isGroupActive: boolean;
private _group: IGroupview; private _group: IGroupview;
private _closePanelCallback: () => Promise<ClosePanelResult>; private _closePanelCallback: () => Promise<ClosePanelResult>;
private _state: { [index: string]: any } = {};
private readonly _onDidStateChange = new Emitter<any>();
readonly onDidStateChange: Event<any> = this._onDidStateChange.event;
get onDidPanelStateChange() { get onDidPanelStateChange() {
return this._event; return this._event;
} }
get onDidPanelDimensionChange() {
return this._dimensionEvent;
}
get onDidDirtyChange() { get onDidDirtyChange() {
return this._dirtyEvent; return this._dirtyEvent;
} }
@ -74,12 +60,12 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
constructor( constructor(
private _event: Event<PanelStateChangeEvent>, private _event: Event<PanelStateChangeEvent>,
private _dimensionEvent: Event<PanelDimensionChangeEvent>, _dimensionEvent: Event<PanelDimensionChangeEvent>,
private _dirtyEvent: Event<boolean>, private _dirtyEvent: Event<boolean>,
private panel: IPanel, private panel: IGroupPanel,
group: IGroupview group: IGroupview
) { ) {
super(); super(_dimensionEvent);
this._group = group; this._group = group;
this.addDisposables( this.addDisposables(
@ -90,19 +76,6 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
); );
} }
public setState(key: string | { [index: string]: any }, value?: any) {
if (typeof key === "object") {
this._state = key;
} else {
this._state[key] = value;
}
this._onDidStateChange.fire(undefined);
}
public getState(): { [index: string]: any } {
return this._state;
}
public close() { public close() {
return this.group.closePanel(this.panel); return this.group.closePanel(this.panel);
} }
@ -113,7 +86,5 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
public dispose() { public dispose() {
super.dispose(); super.dispose();
this._onDidStateChange.dispose();
} }
} }

View File

@ -1,16 +1,13 @@
import { IPanel, PanelInitParameters, PanelUpdateEvent } from "./types"; import { IGroupPanel, PanelInitParameters } from "./types";
import { import { PanelApiImpl, PanelStateChangeEvent, PanelApi } from "./api";
PanelApiImpl,
PanelStateChangeEvent,
PanelDimensionChangeEvent,
PanelApi,
} from "./api";
import { Emitter, Event } from "../../events"; import { Emitter, Event } from "../../events";
import { IGroupview, GroupChangeKind } from "../groupview"; import { IGroupview, GroupChangeKind } from "../groupview";
import { MutableDisposable, CompositeDisposable } from "../../lifecycle"; import { MutableDisposable, CompositeDisposable } from "../../lifecycle";
import { PanelContentPart, PanelHeaderPart, ClosePanelResult } from "./parts"; import { PanelContentPart, PanelHeaderPart, ClosePanelResult } from "./parts";
import { PanelDimensionChangeEvent } from "../../panel/api";
import { PanelUpdateEvent } from "../../panel/types";
export class DefaultPanel extends CompositeDisposable implements IPanel { export class DefaultPanel extends CompositeDisposable implements IGroupPanel {
private readonly mutableDisposable = new MutableDisposable(); private readonly mutableDisposable = new MutableDisposable();
private readonly _onDidPanelStateChange = new Emitter<PanelStateChangeEvent>({ private readonly _onDidPanelStateChange = new Emitter<PanelStateChangeEvent>({
emitLastValue: true, emitLastValue: true,

View File

@ -3,13 +3,14 @@ import { IGroupview } from "../groupview";
import { IGroupAccessor } from "../../layout"; import { IGroupAccessor } from "../../layout";
import { PanelApi } from "./api"; import { PanelApi } from "./api";
import { PanelInitParameters } from "./types"; import { PanelInitParameters } from "./types";
import { Constructor } from "../../types";
export enum ClosePanelResult { export enum ClosePanelResult {
CLOSE = "CLOSE", CLOSE = "CLOSE",
DONT_CLOSE = "DONT_CLOSE", DONT_CLOSE = "DONT_CLOSE",
} }
interface Methods extends IDisposable { interface BasePart extends IDisposable {
init?(params: PartInitParameters): void; init?(params: PartInitParameters): void;
setVisible(isPanelVisible: boolean, isGroupVisible: boolean): void; setVisible(isPanelVisible: boolean, isGroupVisible: boolean): void;
} }
@ -22,14 +23,14 @@ export interface PartInitParameters extends PanelInitParameters {
api: PanelApi; api: PanelApi;
} }
export interface PanelHeaderPart extends Methods { export interface PanelHeaderPart extends BasePart {
id: string; id: string;
element: HTMLElement; element: HTMLElement;
layout?(height: string): void; layout?(height: string): void;
toJSON(): {}; toJSON(): {};
} }
export interface PanelContentPart extends Methods { export interface PanelContentPart extends BasePart {
id: string; id: string;
element: HTMLElement; element: HTMLElement;
layout?(width: number, height: number): void; layout?(width: number, height: number): void;
@ -46,13 +47,11 @@ export interface WatermarkPart extends IDisposable {
element: HTMLElement; element: HTMLElement;
} }
export interface PanelHeaderPartConstructor { // constructors
new (): PanelHeaderPart;
}
export interface PanelContentPartConstructor {
new (): PanelContentPart;
}
export interface WatermarkConstructor { export interface PanelHeaderPartConstructor
new (): WatermarkPart; extends Constructor<PanelHeaderPart> {}
} export interface PanelContentPartConstructor
extends Constructor<PanelContentPart> {}
export interface WatermarkConstructor extends Constructor<WatermarkPart> {}

View File

@ -2,31 +2,20 @@ import { IGroupview } from "../groupview";
import { IDisposable, ISerializable } from "../../lifecycle"; import { IDisposable, ISerializable } from "../../lifecycle";
import { Event } from "../../events"; import { Event } from "../../events";
import { PanelHeaderPart, PanelContentPart, ClosePanelResult } from "./parts"; import { PanelHeaderPart, PanelContentPart, ClosePanelResult } from "./parts";
import { InitParameters, IPanel } from "../../panel/types";
// objects
export interface PanelUpdateEvent {
params: { [key: string]: any };
}
// init parameters // init parameters
export interface PanelInitParameters { export interface PanelInitParameters extends InitParameters {
title: string; title: string;
suppressClosable?: boolean; suppressClosable?: boolean;
params: { [index: string]: any };
state?: { [index: string]: any };
} }
// constructors // constructors
export interface PanelConstructor {
new (): IPanel;
}
// panel // panel
export interface IPanel extends IDisposable, ISerializable { export interface IGroupPanel extends IDisposable, ISerializable, IPanel {
id: string; id: string;
header: PanelHeaderPart; header: PanelHeaderPart;
content: PanelContentPart; content: PanelContentPart;
@ -36,8 +25,6 @@ export interface IPanel extends IDisposable, ISerializable {
setVisible(isGroupActive: boolean, group: IGroupview): void; setVisible(isGroupActive: boolean, group: IGroupview): void;
setDirty(isDirty: boolean): void; setDirty(isDirty: boolean): void;
close?(): Promise<ClosePanelResult>; close?(): Promise<ClosePanelResult>;
layout?(width: number, height: number): void;
init?(params: PanelInitParameters & { [index: string]: string }): void; init?(params: PanelInitParameters & { [index: string]: string }): void;
update?(event: PanelUpdateEvent): void;
onDidStateChange: Event<any>; onDidStateChange: Event<any>;
} }

View File

@ -1,4 +1,8 @@
import { IDisposable, CompositeDisposable } from "../../lifecycle"; import {
IDisposable,
CompositeDisposable,
IValueDisposable,
} from "../../lifecycle";
import { addDisposableListener, Emitter, Event } from "../../events"; import { addDisposableListener, Emitter, Event } from "../../events";
import { ITab, Tab, TabInteractionKind } from "../panel/tab/tab"; import { ITab, Tab, TabInteractionKind } from "../panel/tab/tab";
import { removeClasses, addClasses, toggleClass } from "../../dom"; import { removeClasses, addClasses, toggleClass } from "../../dom";
@ -9,7 +13,7 @@ import { IGroupview } from "../groupview";
import { IGroupAccessor } from "../../layout"; import { IGroupAccessor } from "../../layout";
import { last } from "../../array"; import { last } from "../../array";
import { DataTransferSingleton } from "../droptarget/dataTransfer"; import { DataTransferSingleton } from "../droptarget/dataTransfer";
import { IPanel } from "../panel/types"; import { IGroupPanel } from "../panel/types";
export interface ITabContainer extends IDisposable { export interface ITabContainer extends IDisposable {
element: HTMLElement; element: HTMLElement;
@ -21,10 +25,10 @@ export interface ITabContainer extends IDisposable {
at: (index: number) => ITab; at: (index: number) => ITab;
onDropEvent: Event<TabDropEvent>; onDropEvent: Event<TabDropEvent>;
setActive: (isGroupActive: boolean) => void; setActive: (isGroupActive: boolean) => void;
setActivePanel: (panel: IPanel) => void; setActivePanel: (panel: IGroupPanel) => void;
isActive: (tab: ITab) => boolean; isActive: (tab: ITab) => boolean;
closePanel: (panel: IPanel) => void; closePanel: (panel: IGroupPanel) => void;
openPanel: (panel: IPanel, index?: number) => void; openPanel: (panel: IGroupPanel, index?: number) => void;
} }
export class TabContainer extends CompositeDisposable implements ITabContainer { export class TabContainer extends CompositeDisposable implements ITabContainer {
@ -32,10 +36,10 @@ export class TabContainer extends CompositeDisposable implements ITabContainer {
private _element: HTMLElement; private _element: HTMLElement;
private actionContainer: HTMLElement; private actionContainer: HTMLElement;
private tabs: ITab[] = []; private tabs: IValueDisposable<ITab>[] = [];
private selectedIndex: number = -1; private selectedIndex: number = -1;
private active: boolean; private active: boolean;
private activePanel: IPanel; private activePanel: IGroupPanel;
private _visible: boolean = true; private _visible: boolean = true;
private _height: number; private _height: number;
@ -67,20 +71,22 @@ export class TabContainer extends CompositeDisposable implements ITabContainer {
} }
public isActive(tab: ITab) { public isActive(tab: ITab) {
return this.selectedIndex > -1 && this.tabs[this.selectedIndex] === tab; return (
this.selectedIndex > -1 && this.tabs[this.selectedIndex].value === tab
);
} }
public get hasActiveDragEvent() { public get hasActiveDragEvent() {
return !!this.tabs.find((tab) => tab.hasActiveDragEvent); return !!this.tabs.find((tab) => tab.value.hasActiveDragEvent);
} }
public at(index: number) { public at(index: number) {
return this.tabs[index]; return this.tabs[index]?.value;
} }
public indexOf(tabOrId: ITab) { public indexOf(tabOrId: ITab) {
const id = typeof tabOrId === "string" ? tabOrId : tabOrId.id; const id = typeof tabOrId === "string" ? tabOrId : tabOrId.id;
return this.tabs.findIndex((tab) => tab.id === id); return this.tabs.findIndex((tab) => tab.value.id === id);
} }
constructor(private accessor: IGroupAccessor, private group: IGroupview) { constructor(private accessor: IGroupAccessor, private group: IGroupview) {
@ -111,7 +117,7 @@ export class TabContainer extends CompositeDisposable implements ITabContainer {
console.debug("[tabs] invalid drop event"); console.debug("[tabs] invalid drop event");
return; return;
} }
if (!last(this.tabs).hasActiveDragEvent) { if (!last(this.tabs).value.hasActiveDragEvent) {
addClasses(this.tabContainer, "drag-over-target"); addClasses(this.tabContainer, "drag-over-target");
} }
}), }),
@ -132,10 +138,11 @@ export class TabContainer extends CompositeDisposable implements ITabContainer {
} }
removeClasses(this.tabContainer, "drag-over-target"); removeClasses(this.tabContainer, "drag-over-target");
const activetab = this.tabs.find((tab) => tab.hasActiveDragEvent); const activetab = this.tabs.find((tab) => tab.value.hasActiveDragEvent);
const ignore = !!( const ignore = !!(
activetab && event.composedPath().find((x) => activetab.element === x) activetab &&
event.composedPath().find((x) => activetab.value.element === x)
); );
if (ignore) { if (ignore) {
@ -155,13 +162,16 @@ export class TabContainer extends CompositeDisposable implements ITabContainer {
this.active = isGroupActive; this.active = isGroupActive;
} }
private addTab(tab: ITab, index: number = this.tabs.length) { private addTab(
tab: IValueDisposable<ITab>,
index: number = this.tabs.length
) {
if (index < 0 || index > this.tabs.length) { if (index < 0 || index > this.tabs.length) {
throw new Error("invalid location"); throw new Error("invalid location");
} }
this.tabContainer.insertBefore( this.tabContainer.insertBefore(
tab.element, tab.value.element,
this.tabContainer.children[index] this.tabContainer.children[index]
); );
@ -173,28 +183,31 @@ export class TabContainer extends CompositeDisposable implements ITabContainer {
} }
public delete(id: string) { public delete(id: string) {
const index = this.tabs.findIndex((tab) => tab.id === id); const index = this.tabs.findIndex((tab) => tab.value.id === id);
const tab = this.tabs.splice(index, 1)[0]; const tab = this.tabs.splice(index, 1)[0];
tab.element.remove();
const { value, disposable } = tab;
disposable.dispose();
value.element.remove();
} }
public setActivePanel(panel: IPanel) { public setActivePanel(panel: IGroupPanel) {
this.tabs.forEach((tab) => { this.tabs.forEach((tab) => {
const isActivePanel = panel.id === tab.id; const isActivePanel = panel.id === tab.value.id;
tab.setActive(isActivePanel); tab.value.setActive(isActivePanel);
}); });
} }
public openPanel(panel: IPanel, index: number = this.tabs.length) { public openPanel(panel: IGroupPanel, index: number = this.tabs.length) {
if (this.tabs.find((tab) => tab.id === panel.id)) { if (this.tabs.find((tab) => tab.value.id === panel.id)) {
return; return;
} }
const tab = new Tab(panel.id, this.accessor, this.group); const tab = new Tab(panel.id, this.accessor, this.group);
tab.setContent(panel.header.element); tab.setContent(panel.header.element);
// TODO - dispose of resources const disposable = CompositeDisposable.from(
const disposables = CompositeDisposable.from(
tab.onChanged((event) => { tab.onChanged((event) => {
switch (event.kind) { switch (event.kind) {
case TabInteractionKind.CLICK: case TabInteractionKind.CLICK:
@ -209,15 +222,22 @@ export class TabContainer extends CompositeDisposable implements ITabContainer {
}) })
); );
this.addTab(tab, index); const value: IValueDisposable<ITab> = { value: tab, disposable };
this.addTab(value, index);
this.activePanel = panel; this.activePanel = panel;
} }
public closePanel(panel: IPanel) { public closePanel(panel: IGroupPanel) {
this.delete(panel.id); this.delete(panel.id);
} }
public dispose() { public dispose() {
super.dispose(); super.dispose();
this.tabs.forEach((tab) => {
tab.disposable.dispose();
});
this.tabs = [];
} }
} }

View File

@ -1,5 +1,5 @@
export * from "./splitview/splitview"; export * from "./splitview/splitview";
export * from "./splitview/paneview"; export * from "./paneview/paneview";
export * from "./gridview/gridview"; export * from "./gridview/gridview";
export * from "./groupview/groupview"; export * from "./groupview/groupview";
export * from "./groupview/panel/content/content"; export * from "./groupview/panel/content/content";

View File

@ -4,6 +4,7 @@ import {
PanelHeaderPart, PanelHeaderPart,
PanelHeaderPartConstructor, PanelHeaderPartConstructor,
} from "../groupview/panel/parts"; } from "../groupview/panel/parts";
import { FrameworkFactory } from "../types";
import { DefaultTab } from "./components/tab/defaultTab"; import { DefaultTab } from "./components/tab/defaultTab";
export function createContentComponent( export function createContentComponent(
@ -14,7 +15,7 @@ export function createContentComponent(
frameworkComponents: { frameworkComponents: {
[componentName: string]: any; [componentName: string]: any;
}, },
createFrameworkComponent: (id: string, component: any) => PanelContentPart createFrameworkComponent: FrameworkFactory<PanelContentPart>
): PanelContentPart { ): PanelContentPart {
const Component = const Component =
typeof componentName === "string" typeof componentName === "string"
@ -35,7 +36,7 @@ export function createContentComponent(
"you must register a frameworkPanelWrapper to use framework components" "you must register a frameworkPanelWrapper to use framework components"
); );
} }
const wrappedComponent = createFrameworkComponent( const wrappedComponent = createFrameworkComponent.createComponent(
componentName, componentName,
FrameworkComponent FrameworkComponent
); );
@ -52,7 +53,7 @@ export function createTabComponent(
frameworkComponents: { frameworkComponents: {
[componentName: string]: any; [componentName: string]: any;
}, },
createFrameworkComponent: (id: string, component: any) => PanelHeaderPart createFrameworkComponent: FrameworkFactory<PanelHeaderPart>
): PanelHeaderPart { ): PanelHeaderPart {
const Component = const Component =
typeof componentName === "string" typeof componentName === "string"
@ -73,7 +74,7 @@ export function createTabComponent(
"you must register a frameworkPanelWrapper to use framework components" "you must register a frameworkPanelWrapper to use framework components"
); );
} }
const wrappedComponent = createFrameworkComponent( const wrappedComponent = createFrameworkComponent.createComponent(
componentName, componentName,
FrameworkComponent FrameworkComponent
); );

View File

@ -1,22 +1,22 @@
import { IGridView, IViewDeserializer } from "../gridview/gridview"; import { IGridView, IViewDeserializer } from "../gridview/gridview";
import { IPanel } from "../groupview/panel/types"; import { IGroupPanel } from "../groupview/panel/types";
import { Layout } from "./layout"; import { Layout } from "./layout";
export interface IPanelDeserializer { export interface IPanelDeserializer {
fromJSON(panelData: { [index: string]: any }): IPanel; fromJSON(panelData: { [index: string]: any }): IGroupPanel;
} }
export class DefaultDeserializer implements IViewDeserializer { export class DefaultDeserializer implements IViewDeserializer {
constructor( constructor(
private readonly layout: Layout, private readonly layout: Layout,
private panelDeserializer: { createPanel: (id: string) => IPanel } private panelDeserializer: { createPanel: (id: string) => IGroupPanel }
) {} ) {}
public fromJSON(data: { [key: string]: any }): IGridView { public fromJSON(data: { [key: string]: any }): IGridView {
const children = data.views; const children = data.views;
const active = data.activeView; const active = data.activeView;
const panels: IPanel[] = []; const panels: IGroupPanel[] = [];
for (const child of children) { for (const child of children) {
const panel = this.panelDeserializer.createPanel(child); const panel = this.panelDeserializer.createPanel(child);

View File

@ -10,7 +10,7 @@ import {
GroupChangeEvent, GroupChangeEvent,
GroupDropEvent, GroupDropEvent,
} from "../groupview/groupview"; } from "../groupview/groupview";
import { IPanel } from "../groupview/panel/types"; import { IGroupPanel } from "../groupview/panel/types";
import { DefaultPanel } from "../groupview/panel/panel"; import { DefaultPanel } from "../groupview/panel/panel";
import { import {
CompositeDisposable, CompositeDisposable,
@ -103,9 +103,9 @@ export interface IGroupAccessor {
activeGroup: IGroupview; activeGroup: IGroupview;
// //
addPanelFromComponent(options: AddPanelOptions): PanelReference; addPanelFromComponent(options: AddPanelOptions): PanelReference;
addPanel(options: AddPanelOptions): IPanel; addPanel(options: AddPanelOptions): IGroupPanel;
// //
getPanel: (id: string) => IPanel; getPanel: (id: string) => IGroupPanel;
} }
export interface ILayout extends IGroupAccessor, Api {} export interface ILayout extends IGroupAccessor, Api {}
@ -118,10 +118,10 @@ export class Layout extends CompositeDisposable implements ILayout {
private readonly _element: HTMLElement; private readonly _element: HTMLElement;
private readonly _id = nextLayoutId.next(); private readonly _id = nextLayoutId.next();
private readonly groups = new Map<string, IValueDisposable<IGroupview>>(); private readonly groups = new Map<string, IValueDisposable<IGroupview>>();
private readonly panels = new Map<string, IValueDisposable<IPanel>>(); private readonly panels = new Map<string, IValueDisposable<IGroupPanel>>();
private readonly gridview: Gridview = new Gridview(); private readonly gridview: Gridview = new Gridview();
private readonly dirtyPanels = new Set<IPanel>(); private readonly dirtyPanels = new Set<IGroupPanel>();
private readonly debouncedDeque = debounce(this.persist.bind(this), 5000); private readonly debouncedDeque = debounce(this.syncConfigs.bind(this), 5000);
// events // events
private readonly _onDidLayoutChange = new Emitter<GroupChangeEvent>(); private readonly _onDidLayoutChange = new Emitter<GroupChangeEvent>();
readonly onDidLayoutChange: Event<GroupChangeEvent> = this._onDidLayoutChange readonly onDidLayoutChange: Event<GroupChangeEvent> = this._onDidLayoutChange
@ -205,7 +205,7 @@ export class Layout extends CompositeDisposable implements ILayout {
return this._element; return this._element;
} }
public getPanel(id: string): IPanel { public getPanel(id: string): IGroupPanel {
return this.panels.get(id)?.value; return this.panels.get(id)?.value;
} }
@ -305,7 +305,7 @@ export class Layout extends CompositeDisposable implements ILayout {
this.doSetGroupActive(next); this.doSetGroupActive(next);
} }
public registerPanel(panel: IPanel) { public registerPanel(panel: IGroupPanel) {
if (this.panels.has(panel.id)) { if (this.panels.has(panel.id)) {
throw new Error(`panel ${panel.id} already exists`); throw new Error(`panel ${panel.id} already exists`);
} }
@ -319,7 +319,7 @@ export class Layout extends CompositeDisposable implements ILayout {
this._onDidLayoutChange.fire({ kind: GroupChangeKind.PANEL_CREATED }); this._onDidLayoutChange.fire({ kind: GroupChangeKind.PANEL_CREATED });
} }
public unregisterPanel(panel: IPanel) { public unregisterPanel(panel: IGroupPanel) {
if (!this.panels.has(panel.id)) { if (!this.panels.has(panel.id)) {
throw new Error(`panel ${panel.id} doesn't exist`); throw new Error(`panel ${panel.id} doesn't exist`);
} }
@ -339,6 +339,8 @@ export class Layout extends CompositeDisposable implements ILayout {
* @returns A JSON respresentation of the layout * @returns A JSON respresentation of the layout
*/ */
public toJSON() { public toJSON() {
this.syncConfigs();
const data = this.gridview.serialize(); const data = this.gridview.serialize();
const state = { ...this.panelState }; const state = { ...this.panelState };
@ -356,6 +358,43 @@ export class Layout extends CompositeDisposable implements ILayout {
return { grid: data, panels }; return { grid: data, panels };
} }
/**
* Ensure the local copy of the layout state is up-to-date
*/
private syncConfigs() {
const dirtyPanels = Array.from(this.dirtyPanels);
if (dirtyPanels.length === 0) {
console.debug("[layout#syncConfigs] no dirty panels");
}
this.dirtyPanels.clear();
const partialPanelState = dirtyPanels
.map((panel) => this.panels.get(panel.id))
.filter((_) => !!_)
.reduce((collection, panel) => {
collection[panel.value.id] = panel.value.toJSON();
return collection;
}, {});
this.panelState = {
...this.panelState,
...partialPanelState,
};
dirtyPanels
.filter((p) => this.panels.has(p.id))
.forEach((panel) => {
panel.setDirty(false);
this._onDidLayoutChange.fire({ kind: GroupChangeKind.PANEL_CLEAN });
});
this._onDidLayoutChange.fire({
kind: GroupChangeKind.LAYOUT_CONFIG_UPDATED,
});
}
public deserialize(data: any) { public deserialize(data: any) {
this.gridview.clear(); this.gridview.clear();
this.panels.forEach((panel) => { this.panels.forEach((panel) => {
@ -469,7 +508,7 @@ export class Layout extends CompositeDisposable implements ILayout {
}; };
} }
public addPanel(options: AddPanelOptions): IPanel { public addPanel(options: AddPanelOptions): IGroupPanel {
const component = this.createContentComponent(options.componentName); const component = this.createContentComponent(options.componentName);
const tabComponent = this.createTabComponent(options.tabComponentName); const tabComponent = this.createTabComponent(options.tabComponentName);
@ -491,7 +530,7 @@ export class Layout extends CompositeDisposable implements ILayout {
componentName, componentName,
this.options.components, this.options.components,
this.options.frameworkComponents, this.options.frameworkComponents,
this.options.frameworkPanelWrapper.createContentWrapper this.options.frameworkComponentFactory.content
); );
} }
@ -502,7 +541,7 @@ export class Layout extends CompositeDisposable implements ILayout {
componentName, componentName,
this.options.tabComponents, this.options.tabComponents,
this.options.frameworkTabComponents, this.options.frameworkTabComponents,
this.options.frameworkPanelWrapper.createTabWrapper this.options.frameworkComponentFactory.tab
); );
} }
@ -544,7 +583,7 @@ export class Layout extends CompositeDisposable implements ILayout {
this.doRemoveGroup(group); this.doRemoveGroup(group);
} }
private addPanelToNewGroup(panel: IPanel, location: number[] = [0]) { private addPanelToNewGroup(panel: IGroupPanel, location: number[] = [0]) {
let group: IGroupview; let group: IGroupview;
if ( if (
@ -735,48 +774,19 @@ export class Layout extends CompositeDisposable implements ILayout {
this.gridview.layout(size, orthogonalSize); this.gridview.layout(size, orthogonalSize);
} }
private findGroup(panel: IPanel): IGroupview | undefined { private findGroup(panel: IGroupPanel): IGroupview | undefined {
return Array.from(this.groups.values()).find((group) => return Array.from(this.groups.values()).find((group) =>
group.value.containsPanel(panel) group.value.containsPanel(panel)
).value; ).value;
} }
private addDirtyPanel(panel: IPanel) { private addDirtyPanel(panel: IGroupPanel) {
this.dirtyPanels.add(panel); this.dirtyPanels.add(panel);
panel.setDirty(true); panel.setDirty(true);
this._onDidLayoutChange.fire({ kind: GroupChangeKind.PANEL_DIRTY }); this._onDidLayoutChange.fire({ kind: GroupChangeKind.PANEL_DIRTY });
this.debouncedDeque(); this.debouncedDeque();
} }
private persist() {
const dirtyPanels = Array.from(this.dirtyPanels);
this.dirtyPanels.clear();
const partialPanelState = dirtyPanels
.map((p) => this.panels.get(p.id))
.filter((_) => !!_)
.reduce((collection, panel) => {
collection[panel.value.id] = panel.value.toJSON();
return collection;
}, {});
this.panelState = {
...this.panelState,
...partialPanelState,
};
dirtyPanels
.filter((p) => this.panels.has(p.id))
.forEach((panel) => {
panel.setDirty(false);
this._onDidLayoutChange.fire({ kind: GroupChangeKind.PANEL_CLEAN });
});
this._onDidLayoutChange.fire({
kind: GroupChangeKind.LAYOUT_CONFIG_UPDATED,
});
}
private toTarget(direction: "left" | "right" | "above" | "below" | "within") { private toTarget(direction: "left" | "right" | "above" | "below" | "within") {
switch (direction) { switch (direction) {
case "left": case "left":

View File

@ -7,19 +7,20 @@ import {
PanelHeaderPartConstructor, PanelHeaderPartConstructor,
WatermarkConstructor, WatermarkConstructor,
} from "../groupview/panel/parts"; } from "../groupview/panel/parts";
import { IPanel } from "../groupview/panel/types"; import { IGroupPanel } from "../groupview/panel/types";
import { FrameworkFactory } from "../types";
import { Api } from "./layout"; import { Api } from "./layout";
export interface FrameworkPanelWrapper { export interface FrameworkComponentFactory {
createContentWrapper: (id: string, component: any) => PanelContentPart; content: FrameworkFactory<PanelContentPart>;
createTabWrapper: (id: string, component: any) => PanelHeaderPart; tab: FrameworkFactory<PanelHeaderPart>;
} }
export interface TabContextMenuEvent { export interface TabContextMenuEvent {
event: MouseEvent; event: MouseEvent;
api: Api; api: Api;
panelApi: PanelApi; panelApi: PanelApi;
panel: IPanel; panel: IGroupPanel;
} }
export interface LayoutOptions { export interface LayoutOptions {
@ -37,7 +38,7 @@ export interface LayoutOptions {
}; };
watermarkComponent?: WatermarkConstructor; watermarkComponent?: WatermarkConstructor;
watermarkFrameworkComponent?: any; watermarkFrameworkComponent?: any;
frameworkPanelWrapper: FrameworkPanelWrapper; frameworkComponentFactory: FrameworkComponentFactory;
tabHeight?: number; tabHeight?: number;
debug?: boolean; debug?: boolean;
enableExternalDragEvents?: boolean; enableExternalDragEvents?: boolean;

View File

@ -0,0 +1,71 @@
import { Emitter, Event } from "../events";
import { CompositeDisposable, IDisposable } from "../lifecycle";
export interface PanelDimensionChangeEvent {
width: number;
height: number;
}
// try and do a bit better than the 'any' type.
// anything that is serializable JSON should be valid
type StateObject =
| number
| string
| boolean
| undefined
| null
| object
| StateObject[]
| { [key: string]: StateObject };
export interface IBasePanelApi extends IDisposable {
// events
onDidPanelDimensionChange: Event<PanelDimensionChangeEvent>;
// state
setState(key: string, value: StateObject): void;
setState(state: { [key: string]: StateObject }): void;
getState: () => { [key: string]: StateObject };
getStateKey: <T extends StateObject>(key: string) => T;
onDidStateChange: Event<void>;
}
export class BasePanelApi extends CompositeDisposable implements IBasePanelApi {
private _state: { [key: string]: StateObject } = {};
private readonly _onDidStateChange = new Emitter<void>();
readonly onDidStateChange: Event<void> = this._onDidStateChange.event;
get onDidPanelDimensionChange() {
return this._dimensionEvent;
}
constructor(private _dimensionEvent: Event<PanelDimensionChangeEvent>) {
super();
}
public setState(
key: string | { [key: string]: StateObject },
value?: StateObject
) {
if (typeof key === "object") {
this._state = key;
} else {
this._state[key] = value;
}
this._onDidStateChange.fire(undefined);
}
public getState(): { [key: string]: StateObject } {
return this._state;
}
public getStateKey(key: string) {
// TODO - find an alternative to 'as any'
return this._state[key] as any;
}
public dispose() {
super.dispose();
this._onDidStateChange.dispose();
}
}

View File

@ -0,0 +1,14 @@
export interface InitParameters {
params: { [index: string]: any };
state?: { [index: string]: any };
}
export interface PanelUpdateEvent {
params: { [index: string]: any };
}
export interface IPanel {
init?(params: InitParameters): void;
layout?(width: number, height: number): void;
update?(event: PanelUpdateEvent): void;
}

View File

@ -1,4 +1,4 @@
import { SplitView, IView, Orientation } from "./splitview"; import { SplitView, IView, Orientation } from "../splitview/splitview";
import { IDisposable } from "../lifecycle"; import { IDisposable } from "../lifecycle";
import { Emitter } from "../events"; import { Emitter } from "../events";
import { addClasses, removeClasses } from "../dom"; import { addClasses, removeClasses } from "../dom";

View File

@ -1,4 +1,4 @@
import { IPanel } from "../groupview/panel/types"; import { IGroupPanel } from "../groupview/panel/types";
import { Layout } from "../layout/layout"; import { Layout } from "../layout/layout";
import { DefaultPanel } from "../groupview/panel/panel"; import { DefaultPanel } from "../groupview/panel/panel";
import { PanelContentPart, PanelHeaderPart } from "../groupview/panel/parts"; import { PanelContentPart, PanelHeaderPart } from "../groupview/panel/parts";
@ -11,7 +11,7 @@ import {
export class ReactPanelDeserialzier implements IPanelDeserializer { export class ReactPanelDeserialzier implements IPanelDeserializer {
constructor(private readonly layout: Layout) {} constructor(private readonly layout: Layout) {}
public fromJSON(panelData: { [index: string]: any }): IPanel { public fromJSON(panelData: { [index: string]: any }): IGroupPanel {
const panelId = panelData.id; const panelId = panelData.id;
const content = panelData.content; const content = panelData.content;
const tab = panelData.tab; const tab = panelData.tab;
@ -24,14 +24,14 @@ export class ReactPanelDeserialzier implements IPanelDeserializer {
content.id, content.id,
this.layout.options.components, this.layout.options.components,
this.layout.options.frameworkComponents, this.layout.options.frameworkComponents,
this.layout.options.frameworkPanelWrapper.createContentWrapper this.layout.options.frameworkComponentFactory.content
) as PanelContentPart; ) as PanelContentPart;
const headerPart = createTabComponent( const headerPart = createTabComponent(
tab.id, tab.id,
this.layout.options.tabComponents, this.layout.options.tabComponents,
this.layout.options.frameworkPanelWrapper, this.layout.options.frameworkComponentFactory,
this.layout.options.frameworkPanelWrapper.createTabWrapper this.layout.options.frameworkComponentFactory.tab
) as PanelHeaderPart; ) as PanelHeaderPart;
const panel = new DefaultPanel(panelId, headerPart, contentPart); const panel = new DefaultPanel(panelId, headerPart, contentPart);

View File

@ -5,6 +5,7 @@ import { ReactPanelContentPart } from "./reactContentPart";
import { ReactPanelHeaderPart } from "./reactHeaderPart"; import { ReactPanelHeaderPart } from "./reactHeaderPart";
import { IPanelProps } from "./react"; import { IPanelProps } from "./react";
import { ReactPanelDeserialzier } from "./deserializer"; import { ReactPanelDeserialzier } from "./deserializer";
import { FrameworkComponentFactory } from "../layout/options";
export interface OnReadyEvent { export interface OnReadyEvent {
api: Api; api: Api;
@ -55,23 +56,27 @@ export const ReactGrid = (props: IReactGridProps) => {
}; };
}; };
const frameworkPanelWrapper = { const frameworkPanelWrapper: FrameworkComponentFactory = {
createContentWrapper: ( content: {
id: string, createComponent: (
component: React.FunctionComponent<IPanelProps> id: string,
) => { component: React.FunctionComponent<IPanelProps>
return new ReactPanelContentPart(id, component, { addPortal }); ) => {
return new ReactPanelContentPart(id, component, { addPortal });
},
}, },
createTabWrapper: ( tab: {
id: string, createComponent: (
component: React.FunctionComponent<IPanelProps> id: string,
) => { component: React.FunctionComponent<IPanelProps>
return new ReactPanelHeaderPart(id, component, { addPortal }); ) => {
return new ReactPanelHeaderPart(id, component, { addPortal });
},
}, },
}; };
const layout = new Layout({ const layout = new Layout({
frameworkPanelWrapper, frameworkComponentFactory: frameworkPanelWrapper,
frameworkComponents: props.components, frameworkComponents: props.components,
frameworkTabComponents: props.tabComponents, frameworkTabComponents: props.tabComponents,
tabHeight: props.tabHeight, tabHeight: props.tabHeight,

View File

@ -3,6 +3,7 @@ import * as ReactDOM from "react-dom";
import { IDisposable } from "../lifecycle"; import { IDisposable } from "../lifecycle";
import { PanelApi } from "../groupview/panel/api"; import { PanelApi } from "../groupview/panel/api";
import { sequentialNumberGenerator } from "../math"; import { sequentialNumberGenerator } from "../math";
import { IBasePanelApi } from "../panel/api";
export interface IPanelProps { export interface IPanelProps {
api: PanelApi; api: PanelApi;
@ -53,9 +54,9 @@ export class ReactPart implements IDisposable {
constructor( constructor(
private readonly parent: HTMLElement, private readonly parent: HTMLElement,
private readonly api: PanelApi, private readonly api: IBasePanelApi,
private readonly addPortal: (portal: React.ReactPortal) => IDisposable, private readonly addPortal: (portal: React.ReactPortal) => IDisposable,
private readonly component: React.FunctionComponent<IPanelProps>, private readonly component: React.FunctionComponent<{}>,
private readonly parameters: { [key: string]: any } private readonly parameters: { [key: string]: any }
) { ) {
this.createPortal(); this.createPortal();
@ -77,7 +78,7 @@ export class ReactPart implements IDisposable {
let props = { let props = {
api: this.api, api: this.api,
...this.parameters, ...this.parameters,
} as IPanelProps; } as any;
const wrapper = React.createElement(PanelWrapper, { const wrapper = React.createElement(PanelWrapper, {
component: this.component, component: this.component,

View File

@ -0,0 +1,102 @@
import { trackFocus } from "../dom";
import { Emitter } from "../events";
import { BasePanelApi, PanelDimensionChangeEvent } from "../panel/api";
import { CompositeDisposable } from "../lifecycle";
import { IView } from "../splitview/splitview";
import { ReactLayout } from "./layout";
import { ReactPart } from "./react";
import { ISplitviewPanelProps } from "./splitview";
import { PanelUpdateEvent, InitParameters, IPanel } from "../panel/types";
/**
* A no-thrills implementation of IView that renders a React component
*/
export class ReactComponentView
extends CompositeDisposable
implements IView, IPanel {
private _element: HTMLElement;
private part: ReactPart;
private params: { params: any };
private api: BasePanelApi;
private readonly _onDidPanelDimensionsChange = new Emitter<
PanelDimensionChangeEvent
>();
private _onDidChange: Emitter<number | undefined> = new Emitter<
number | undefined
>();
public onDidChange = this._onDidChange.event;
get element() {
return this._element;
}
get minimumSize() {
return 100;
}
// get snapSize() {
// return 100;
// }
get maximumSize() {
return Number.MAX_SAFE_INTEGER;
}
constructor(
public readonly id: string,
private readonly componentName: string,
private readonly component: React.FunctionComponent<ISplitviewPanelProps>,
private readonly parent: ReactLayout
) {
super();
this.api = new BasePanelApi(this._onDidPanelDimensionsChange.event);
if (!this.component) {
throw new Error("React.FunctionalComponent cannot be undefined");
}
this._element = document.createElement("div");
const { onDidFocus } = trackFocus(this.element);
this.addDisposables(
this._onDidPanelDimensionsChange,
onDidFocus(() => {
//
})
);
}
layout(width: number, height: number) {
this._onDidPanelDimensionsChange.fire({ width, height });
}
init(parameters: InitParameters): void {
this.params = parameters;
this.part = new ReactPart(
this.element,
this.api,
this.parent.addPortal,
this.component,
parameters.params
);
}
update(params: PanelUpdateEvent) {
this.params = { ...this.params.params, ...params };
this.part.update(params);
}
toJSON(): object {
return {
id: this.id,
component: this.componentName,
props: this.params.params,
state: this.api.getState(),
};
}
dispose() {
this._onDidPanelDimensionsChange.dispose();
this.api.dispose();
}
}

View File

@ -1,57 +0,0 @@
import { Emitter } from "../events";
import { IView } from "../splitview/splitview";
import { ReactLayout } from "./layout";
import { ReactPart } from "./react";
export class ReactView implements IView {
private _element: HTMLElement;
private part: ReactPart;
private _onDidChange: Emitter<number | undefined> = new Emitter<
number | undefined
>();
public onDidChange = this._onDidChange.event;
get element() {
return this._element;
}
get minimumSize() {
return 100;
}
// get snapSize() {
// return 100;
// }
get maximumSize() {
return Number.MAX_SAFE_INTEGER;
}
constructor(
public readonly id: string,
private readonly component: React.FunctionComponent<{}>,
private readonly parent: ReactLayout
) {
if (!this.component) {
throw new Error("React.FunctionalComponent cannot be undefined");
}
this._element = document.createElement("div");
}
layout(size: number, orthogonalSize: number) {}
init(parameters: { params: any }): void {
this.part = new ReactPart(
this.element,
{} as any,
this.parent.addPortal,
this.component,
parameters.params
);
}
update(params: {}) {
this.part.update(params);
}
}

View File

@ -1,25 +1,44 @@
import * as React from "react"; import * as React from "react";
import { Orientation, SplitView } from "../splitview/splitview"; import { IBasePanelApi } from "../panel/api";
import { ReactView } from "./reactView"; import { IDisposable } from "../lifecycle";
import {
IComponentSplitview,
ComponentSplitview,
} from "../splitview/componentSplitview";
import { Orientation } from "../splitview/splitview";
import { ReactComponentView } from "./reactComponentView";
export interface SplitviewFacade { export interface SplitviewFacade {
addFromComponent(options: { id: string; component: string }): void; addFromComponent(options: {
id: string;
component: string;
params?: { [index: string]: any };
}): void;
layout(size: number, orthogonalSize: number): void; layout(size: number, orthogonalSize: number): void;
onChange: (cb: (event: { proportions: number[] }) => void) => IDisposable;
toJSON: () => any;
deserialize: (data: any) => void;
} }
export interface SplitviewReadyEvent { export interface SplitviewReadyEvent {
api: SplitviewFacade; api: IComponentSplitview;
}
export interface ISplitviewPanelProps {
api: IBasePanelApi;
} }
export interface ISplitviewComponentProps { export interface ISplitviewComponentProps {
orientation: Orientation; orientation: Orientation;
onReady?: (event: SplitviewReadyEvent) => void; onReady?: (event: SplitviewReadyEvent) => void;
components: { [index: string]: React.FunctionComponent<{}> }; components: {
[index: string]: React.FunctionComponent<ISplitviewPanelProps>;
};
} }
export const SplitViewComponent = (props: ISplitviewComponentProps) => { export const SplitViewComponent = (props: ISplitviewComponentProps) => {
const domReference = React.useRef<HTMLDivElement>(); const domReference = React.useRef<HTMLDivElement>();
const splitview = React.useRef<SplitView>(); const splitpanel = React.useRef<IComponentSplitview>();
const [portals, setPortals] = React.useState<React.ReactPortal[]>([]); const [portals, setPortals] = React.useState<React.ReactPortal[]>([]);
const addPortal = React.useCallback((p: React.ReactPortal) => { const addPortal = React.useCallback((p: React.ReactPortal) => {
@ -32,52 +51,29 @@ export const SplitViewComponent = (props: ISplitviewComponentProps) => {
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
splitview.current = new SplitView(domReference.current, { splitpanel.current = new ComponentSplitview(domReference.current, {
orientation: props.orientation, orientation: props.orientation,
frameworkComponents: props.components,
frameworkWrapper: {
createComponent: (id: string, component: any) => {
return new ReactComponentView(id, id, component, { addPortal });
},
},
}); });
const createViewWrapper = (
id: string,
component: React.FunctionComponent<{}>
) => {
return new ReactView(id, component, { addPortal });
};
const facade: SplitviewFacade = {
addFromComponent: (options) => {
const component = props.components[options.component];
const view = createViewWrapper(options.id, component);
splitview.current.addView(view, { type: "distribute" });
view.init({ params: {} });
return {
dispose: () => {
//
},
};
},
layout: (width, height) => {
const [size, orthogonalSize] =
props.orientation === Orientation.HORIZONTAL
? [width, height]
: [height, width];
splitview.current.layout(size, orthogonalSize);
},
};
const { width, height } = domReference.current.getBoundingClientRect(); const { width, height } = domReference.current.getBoundingClientRect();
const [size, orthogonalSize] = const [size, orthogonalSize] =
props.orientation === Orientation.HORIZONTAL props.orientation === Orientation.HORIZONTAL
? [width, height] ? [width, height]
: [height, width]; : [height, width];
splitview.current.layout(size, orthogonalSize); splitpanel.current.layout(size, orthogonalSize);
if (props.onReady) { if (props.onReady) {
props.onReady({ api: facade }); props.onReady({ api: splitpanel.current });
} }
return () => { return () => {
splitview.current.dispose(); splitpanel.current.dispose();
}; };
}, []); }, []);

View File

@ -0,0 +1,128 @@
import { IDisposable } from "../lifecycle";
import { Orientation, SplitView } from "./splitview";
import {
createComponent,
ISerializableView,
SplitPanelOptions,
} from "./options";
export interface IComponentSplitview extends IDisposable {
addFromComponent(options: {
id: string;
component: string;
params?: {
[index: string]: any;
};
}): IDisposable;
layout(width: number, height: number): void;
onChange(cb: (event: { proportions: number[] }) => void): IDisposable;
toJSON(): object;
deserialize(data: any): void;
}
/**
* A high-level implementation of splitview that works using 'panels'
*/
export class ComponentSplitview implements IComponentSplitview {
private splitview: SplitView;
constructor(
private readonly element: HTMLElement,
private readonly options: SplitPanelOptions
) {
if (!options.components) {
options.components = {};
}
if (!options.frameworkComponents) {
options.frameworkComponents = {};
}
this.splitview = new SplitView(this.element, options);
}
addFromComponent(options: {
id: string;
component: string;
params?: {
[index: string]: any;
};
}): IDisposable {
const view = createComponent(
options.component,
this.options.components,
this.options.frameworkComponents,
this.options.frameworkWrapper.createComponent
);
this.registerView(view);
this.splitview.addView(view, { type: "distribute" });
view.init({ params: options.params });
return {
dispose: () => {
//
},
};
}
private registerView(view: ISerializableView) {
//
}
layout(width: number, height: number): void {
const [size, orthogonalSize] =
this.splitview.orientation === Orientation.HORIZONTAL
? [width, height]
: [height, width];
this.splitview.layout(size, orthogonalSize);
}
onChange(cb: (event: { proportions: number[] }) => void): IDisposable {
return this.splitview.onDidSashEnd(() => {
cb({ proportions: this.splitview.proportions });
});
}
toJSON(): object {
const views = this.splitview.getViews().map((v: ISerializableView, i) => {
const size = this.splitview.getViewSize(i);
return { size, data: v.toJSON ? v.toJSON() : {} };
});
return {
views,
size: this.splitview.size,
orientation: this.splitview.orientation,
};
}
deserialize(data: any): void {
const { views, orientation, size } = data;
this.splitview.dispose();
this.splitview = new SplitView(this.element, {
orientation,
descriptor: {
size,
views: views.map((v) => {
const data = v.data;
const view = createComponent(
data.component,
this.options.components,
this.options.frameworkComponents,
this.options.frameworkWrapper.createComponent
);
view.init({ params: v.props });
return { size: v.size, view };
}),
},
});
this.splitview.orientation = orientation;
}
public dispose() {
this.splitview.dispose();
}
}

View File

@ -0,0 +1,58 @@
import { IView, ISplitViewOptions } from "../splitview/splitview";
import { Constructor, FrameworkFactory } from "../types";
export interface ISerializableView extends IView {
toJSON: () => object;
init: (params: { params: any }) => void;
}
export interface SplitPanelOptions extends ISplitViewOptions {
components?: {
[componentName: string]: ISerializableView;
};
frameworkComponents?: {
[componentName: string]: any;
};
frameworkWrapper?: FrameworkFactory<ISerializableView>;
}
export interface ISerializableViewConstructor
extends Constructor<ISerializableView> {}
export function createComponent(
componentName: string | ISerializableViewConstructor | any,
components: {
[componentName: string]: ISerializableView;
},
frameworkComponents: {
[componentName: string]: any;
},
createFrameworkComponent: (id: string, component: any) => ISerializableView
): ISerializableView {
const Component =
typeof componentName === "string"
? components[componentName]
: componentName;
const FrameworkComponent =
typeof componentName === "string"
? frameworkComponents[componentName]
: componentName;
if (Component && FrameworkComponent) {
throw new Error(
`cannot register component ${componentName} as both a component and frameworkComponent`
);
}
if (FrameworkComponent) {
if (!createFrameworkComponent) {
throw new Error(
"you must register a frameworkPanelWrapper to use framework components"
);
}
const wrappedComponent = createFrameworkComponent(
componentName,
FrameworkComponent
);
return wrappedComponent;
}
return new Component() as ISerializableView;
}

View File

@ -68,15 +68,23 @@ export class SplitView {
private sashContainer: HTMLElement; private sashContainer: HTMLElement;
private views: IViewItem[] = []; private views: IViewItem[] = [];
private sashes: ISashItem[] = []; private sashes: ISashItem[] = [];
private orientation: Orientation; private _orientation: Orientation;
private size: number; private _size: number;
private orthogonalSize: number; private _orthogonalSize: number;
private contentSize: number; private contentSize: number;
private _proportions: number[]; private _proportions: number[];
private _onDidSashEnd = new Emitter<any>(); private _onDidSashEnd = new Emitter<any>();
public onDidSashEnd = this._onDidSashEnd.event; public onDidSashEnd = this._onDidSashEnd.event;
get size() {
return this._size;
}
get orthogonalSize() {
return this._orthogonalSize;
}
public get length() { public get length() {
return this.views.length; return this.views.length;
} }
@ -85,6 +93,10 @@ export class SplitView {
return [...this._proportions]; return [...this._proportions];
} }
get orientation() {
return this._orientation;
}
get minimumSize(): number { get minimumSize(): number {
return this.views.reduce((r, item) => r + item.view.minimumSize, 0); return this.views.reduce((r, item) => r + item.view.minimumSize, 0);
} }
@ -99,7 +111,7 @@ export class SplitView {
private readonly container: HTMLElement, private readonly container: HTMLElement,
options: ISplitViewOptions options: ISplitViewOptions
) { ) {
this.orientation = options.orientation; this._orientation = options.orientation;
this.element = this.createContainer(); this.element = this.createContainer();
this.viewContainer = this.createViewContainer(); this.viewContainer = this.createViewContainer();
@ -112,7 +124,7 @@ export class SplitView {
// We have an existing set of view, add them now // We have an existing set of view, add them now
if (options.descriptor) { if (options.descriptor) {
this.size = options.descriptor.size; this._size = options.descriptor.size;
options.descriptor.views.forEach((viewDescriptor, index) => { options.descriptor.views.forEach((viewDescriptor, index) => {
const sizing = viewDescriptor.size; const sizing = viewDescriptor.size;
@ -161,7 +173,7 @@ export class SplitView {
size = clamp( size = clamp(
size, size,
item.view.minimumSize, item.view.minimumSize,
Math.min(item.view.maximumSize, this.size) Math.min(item.view.maximumSize, this._size)
); );
item.size = size; item.size = size;
@ -189,7 +201,7 @@ export class SplitView {
const contentSize = this.views.reduce((r, i) => r + i.size, 0); const contentSize = this.views.reduce((r, i) => r + i.size, 0);
this.resize(this.views.length - 1, this.size - contentSize, undefined, [ this.resize(this.views.length - 1, this._size - contentSize, undefined, [
index, index,
]); ]);
this.distributeEmptySpace(); this.distributeEmptySpace();
@ -250,7 +262,7 @@ export class SplitView {
const cb = (event: MouseEvent) => { const cb = (event: MouseEvent) => {
let start = let start =
this.orientation === Orientation.HORIZONTAL this._orientation === Orientation.HORIZONTAL
? event.clientX ? event.clientX
: event.clientY; : event.clientY;
const sizes = this.views.map((x) => x.size); const sizes = this.views.map((x) => x.size);
@ -259,7 +271,7 @@ export class SplitView {
const mousemove = (event: MouseEvent) => { const mousemove = (event: MouseEvent) => {
const current = const current =
this.orientation === Orientation.HORIZONTAL this._orientation === Orientation.HORIZONTAL
? event.clientX ? event.clientX
: event.clientY; : event.clientY;
const delta = current - start; const delta = current - start;
@ -370,11 +382,11 @@ export class SplitView {
this.addView(view, sizing, to); this.addView(view, sizing, to);
} }
public setOrientation(orientation: Orientation) { set orientation(orientation: Orientation) {
if (orientation === this.orientation) { if (orientation === this._orientation) {
return; return;
} }
this.orientation = orientation; this._orientation = orientation;
const classname = const classname =
orientation === Orientation.HORIZONTAL ? "horizontal" : "vertical"; orientation === Orientation.HORIZONTAL ? "horizontal" : "vertical";
@ -386,8 +398,8 @@ export class SplitView {
} }
public layout(size: number, orthogonalSize: number) { public layout(size: number, orthogonalSize: number) {
this.size = size; this._size = size;
this.orthogonalSize = orthogonalSize; this._orthogonalSize = orthogonalSize;
for (let i = 0; i < this.views.length; i++) { for (let i = 0; i < this.views.length; i++) {
const item = this.views[i]; const item = this.views[i];
@ -412,7 +424,7 @@ export class SplitView {
this.resize( this.resize(
this.views.length - 1, this.views.length - 1,
this.size - contentSize, this._size - contentSize,
undefined, undefined,
lowPriorityIndexes, lowPriorityIndexes,
highPriorityIndexes highPriorityIndexes
@ -423,7 +435,7 @@ export class SplitView {
private distributeEmptySpace() { private distributeEmptySpace() {
let contentSize = this.views.reduce((r, i) => r + i.size, 0); let contentSize = this.views.reduce((r, i) => r + i.size, 0);
let emptyDelta = this.size - contentSize; let emptyDelta = this._size - contentSize;
for (let i = this.views.length - 1; emptyDelta !== 0 && i >= 0; i--) { for (let i = this.views.length - 1; emptyDelta !== 0 && i >= 0; i--) {
const item = this.views[i]; const item = this.views[i];
@ -448,30 +460,30 @@ export class SplitView {
for (let i = 0; i < this.views.length - 1; i++) { for (let i = 0; i < this.views.length - 1; i++) {
sum += this.views[i].size; sum += this.views[i].size;
x.push(sum); x.push(sum);
if (this.orientation === Orientation.HORIZONTAL) { if (this._orientation === Orientation.HORIZONTAL) {
this.sashes[i].container.style.left = `${sum - 2}px`; this.sashes[i].container.style.left = `${sum - 2}px`;
this.sashes[i].container.style.top = `0px`; this.sashes[i].container.style.top = `0px`;
} }
if (this.orientation === Orientation.VERTICAL) { if (this._orientation === Orientation.VERTICAL) {
this.sashes[i].container.style.left = `0px`; this.sashes[i].container.style.left = `0px`;
this.sashes[i].container.style.top = `${sum - 2}px`; this.sashes[i].container.style.top = `${sum - 2}px`;
} }
} }
this.views.forEach((view, i) => { this.views.forEach((view, i) => {
if (this.orientation === Orientation.HORIZONTAL) { if (this._orientation === Orientation.HORIZONTAL) {
view.container.style.width = `${view.size}px`; view.container.style.width = `${view.size}px`;
view.container.style.left = i == 0 ? "0px" : `${x[i - 1]}px`; view.container.style.left = i == 0 ? "0px" : `${x[i - 1]}px`;
view.container.style.top = ""; view.container.style.top = "";
view.container.style.height = ""; view.container.style.height = "";
} }
if (this.orientation === Orientation.VERTICAL) { if (this._orientation === Orientation.VERTICAL) {
view.container.style.height = `${view.size}px`; view.container.style.height = `${view.size}px`;
view.container.style.top = i == 0 ? "0px" : `${x[i - 1]}px`; view.container.style.top = i == 0 ? "0px" : `${x[i - 1]}px`;
view.container.style.width = ""; view.container.style.width = "";
view.container.style.left = ""; view.container.style.left = "";
} }
view.view.layout(view.size, this.orthogonalSize); view.view.layout(view.size, this._orthogonalSize);
}); });
} }
@ -588,12 +600,13 @@ export class SplitView {
private createContainer() { private createContainer() {
const element = document.createElement("div"); const element = document.createElement("div");
const orientationClassname = const orientationClassname =
this.orientation === Orientation.HORIZONTAL ? "horizontal" : "vertical"; this._orientation === Orientation.HORIZONTAL ? "horizontal" : "vertical";
element.className = `split-view-container ${orientationClassname}`; element.className = `split-view-container ${orientationClassname}`;
return element; return element;
} }
public dispose() { public dispose() {
this.element.remove();
for (let i = 0; i < this.element.children.length; i++) { for (let i = 0; i < this.element.children.length; i++) {
if (this.element.children.item[i] === this.element) { if (this.element.children.item[i] === this.element) {
this.element.removeChild(this.element); this.element.removeChild(this.element);

View File

@ -0,0 +1,7 @@
export interface Constructor<T> {
new (): T;
}
export interface FrameworkFactory<T> {
createComponent: (id: string, component: any) => T;
}

View File

@ -5,6 +5,7 @@ const merge = require("merge2");
const header = require("gulp-header"); const header = require("gulp-header");
const gulpSass = require("gulp-sass"); const gulpSass = require("gulp-sass");
const concat = require("gulp-concat"); const concat = require("gulp-concat");
const sourcemaps = require("gulp-sourcemaps");
const headerTemplate = [ const headerTemplate = [
"/**", "/**",
@ -28,7 +29,10 @@ const build = (options) => {
gulp.task("esm", () => { gulp.task("esm", () => {
const ts = gulpTypescript.createProject(tsconfig); const ts = gulpTypescript.createProject(tsconfig);
const tsResult = gulp.src(["src/**/*.ts", "src/**/*.tsx"]).pipe(ts()); const tsResult = gulp
.src(["src/**/*.ts", "src/**/*.tsx"])
.pipe(sourcemaps.init())
.pipe(ts());
return merge([ return merge([
tsResult.dts tsResult.dts
.pipe(header(dtsHeaderTemplate, { pkg: package })) .pipe(header(dtsHeaderTemplate, { pkg: package }))
@ -36,6 +40,9 @@ const build = (options) => {
tsResult.js tsResult.js
.pipe(header(headerTemplate, { pkg: package })) .pipe(header(headerTemplate, { pkg: package }))
.pipe(gulp.dest("./dist/esm")), .pipe(gulp.dest("./dist/esm")),
tsResult
.pipe(sourcemaps.write(".", { includeContent: false }))
.pipe(gulp.dest("./dist/esm")),
]); ]);
}); });