This commit is contained in:
mathuo 2020-09-20 21:52:26 +01:00
parent 9fc0603d61
commit 50e483d170
72 changed files with 3276 additions and 3291 deletions

View File

@ -1,6 +1,6 @@
{ {
"trailingComma": "es5", "trailingComma": "es5",
"tabWidth": 4, "tabWidth": 4,
"semi": false, "semi": true,
"singleQuote": true "singleQuote": true
} }

View File

@ -3,11 +3,7 @@
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace. // List of extensions which should be recommended for users of this workspace.
"recommendations": [ "recommendations": ["esbenp.prettier-vscode"],
"esbenp.prettier-vscode"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace. // List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": [ "unwantedRecommendations": []
]
} }

View File

@ -13,4 +13,4 @@ module.exports = {
'<rootDir>/src/__tests__/**/*.spec.tsx', '<rootDir>/src/__tests__/**/*.spec.tsx',
], ],
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setupTests.ts'], setupFilesAfterEnv: ['<rootDir>/src/__tests__/setupTests.ts'],
} };

View File

@ -4,7 +4,8 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1",
"format": "prettier --write ."
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,27 +1,27 @@
import * as React from 'react' import * as React from 'react';
// import { LoadFromConfig } from "./loadFromConfig"; // import { LoadFromConfig } from "./loadFromConfig";
// import { FromApi } from "./fromApi"; // import { FromApi } from "./fromApi";
// import { PaneDemo } from "./pane"; // import { PaneDemo } from "./pane";
import { TestGrid } from './layout-grid/reactgrid' import { TestGrid } from './layout-grid/reactgrid';
import { Application } from './layout-grid/application' import { Application } from './layout-grid/application';
const options = [ const options = [
// { id: "config", component: LoadFromConfig }, // { id: "config", component: LoadFromConfig },
// { id: "api", component: FromApi }, // { id: "api", component: FromApi },
// { id: "pane", component: PaneDemo }, // { id: "pane", component: PaneDemo },
{ id: 'grid', component: Application }, { id: 'grid', component: Application },
] ];
export const App = () => { export const App = () => {
const [value, setValue] = React.useState<string>(options[0].id) const [value, setValue] = React.useState<string>(options[0].id);
const onChange = (event: React.ChangeEvent<HTMLSelectElement>) => const onChange = (event: React.ChangeEvent<HTMLSelectElement>) =>
setValue(event.target.value) setValue(event.target.value);
const Component = React.useMemo( const Component = React.useMemo(
() => options.find((o) => o.id === value)?.component, () => options.find((o) => o.id === value)?.component,
[value] [value]
) );
return ( return (
<div <div
@ -48,5 +48,5 @@ export const App = () => {
</div> </div>
)} )}
</div> </div>
) );
} };

View File

@ -1,6 +1,6 @@
import * as React from 'react' import * as React from 'react';
import * as ReactDOM from 'react-dom' import * as ReactDOM from 'react-dom';
import { App } from './app' import { App } from './app';
import './index.scss' import './index.scss';
ReactDOM.render(<App />, document.getElementById('app')) ReactDOM.render(<App />, document.getElementById('app'));

View File

@ -1,4 +1,4 @@
import * as React from 'react' import * as React from 'react';
import { import {
Orientation, Orientation,
GridviewComponent, GridviewComponent,
@ -6,18 +6,18 @@ import {
GridviewReadyEvent, GridviewReadyEvent,
ComponentGridview, ComponentGridview,
IGridviewPanelProps, IGridviewPanelProps,
} from 'splitview' } from 'splitview';
import { TestGrid } from './reactgrid' import { TestGrid } from './reactgrid';
const rootcomponents: { const rootcomponents: {
[index: string]: React.FunctionComponent<IGridviewPanelProps> [index: string]: React.FunctionComponent<IGridviewPanelProps>;
} = { } = {
sidebar: (props: IGridviewPanelProps) => { sidebar: (props: IGridviewPanelProps) => {
return ( return (
<div style={{ backgroundColor: 'rgb(37,37,38)', height: '100%' }}> <div style={{ backgroundColor: 'rgb(37,37,38)', height: '100%' }}>
sidebar sidebar
</div> </div>
) );
}, },
editor: TestGrid, editor: TestGrid,
panel: () => { panel: () => {
@ -25,12 +25,12 @@ const rootcomponents: {
<div style={{ backgroundColor: 'rgb(30,30,30)', height: '100%' }}> <div style={{ backgroundColor: 'rgb(30,30,30)', height: '100%' }}>
panel panel
</div> </div>
) );
}, },
} };
export const Application = () => { export const Application = () => {
const api = React.useRef<ComponentGridview>() const api = React.useRef<ComponentGridview>();
const onReady = (event: GridviewReadyEvent) => { const onReady = (event: GridviewReadyEvent) => {
// event.api.deserialize(rootLayout); // event.api.deserialize(rootLayout);
@ -38,27 +38,27 @@ export const Application = () => {
id: '1', id: '1',
component: 'sidebar', component: 'sidebar',
snap: true, snap: true,
}) });
event.api.addComponent({ event.api.addComponent({
id: '2', id: '2',
component: 'editor', component: 'editor',
snap: true, snap: true,
position: { reference: '1', direction: 'right' }, position: { reference: '1', direction: 'right' },
priority: LayoutPriority.High, priority: LayoutPriority.High,
}) });
api.current = event.api as ComponentGridview api.current = event.api as ComponentGridview;
} };
React.useEffect(() => { React.useEffect(() => {
const callback = (ev: UIEvent) => { const callback = (ev: UIEvent) => {
const height = window.innerHeight - 20 const height = window.innerHeight - 20;
const width = window.innerWidth const width = window.innerWidth;
api.current?.layout(width, height) api.current?.layout(width, height);
} };
window.addEventListener('resize', callback) window.addEventListener('resize', callback);
callback(undefined) callback(undefined);
api.current.addComponent({ api.current.addComponent({
id: '3', id: '3',
@ -66,12 +66,12 @@ export const Application = () => {
position: { reference: '2', direction: 'below' }, position: { reference: '2', direction: 'below' },
size: 200, size: 200,
snap: true, snap: true,
}) });
return () => { return () => {
window.removeEventListener('resize', callback) window.removeEventListener('resize', callback);
} };
}, []) }, []);
return ( return (
<GridviewComponent <GridviewComponent
@ -79,5 +79,5 @@ export const Application = () => {
onReady={onReady} onReady={onReady}
orientation={Orientation.HORIZONTAL} orientation={Orientation.HORIZONTAL}
/> />
) );
} };

View File

@ -1,6 +1,6 @@
import * as React from 'react' import * as React from 'react';
import { IPanelProps } from 'splitview' import { IPanelProps } from 'splitview';
export const CustomTab = (props: IPanelProps) => { export const CustomTab = (props: IPanelProps) => {
return <div>hello</div> return <div>hello</div>;
} };

View File

@ -1,25 +1,25 @@
import * as React from 'react' import * as React from 'react';
import { Api, IPanelProps } from 'splitview' import { Api, IPanelProps } from 'splitview';
export const Editor = (props: IPanelProps & { layoutApi: Api }) => { export const Editor = (props: IPanelProps & { layoutApi: Api }) => {
const [tabHeight, setTabHeight] = React.useState<number>(0) const [tabHeight, setTabHeight] = React.useState<number>(0);
React.useEffect(() => { React.useEffect(() => {
if (props.layoutApi) { if (props.layoutApi) {
setTabHeight(props.layoutApi.getTabHeight()) setTabHeight(props.layoutApi.getTabHeight());
} }
}, [props.layoutApi]) }, [props.layoutApi]);
const onTabHeightChange = (event: React.ChangeEvent<HTMLInputElement>) => { const onTabHeightChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = Number(event.target.value) const value = Number(event.target.value);
if (!Number.isNaN(value)) { if (!Number.isNaN(value)) {
setTabHeight(value) setTabHeight(value);
}
} }
};
const onClick = () => { const onClick = () => {
props.layoutApi.setTabHeight(tabHeight) props.layoutApi.setTabHeight(tabHeight);
} };
return ( return (
<div <div
@ -35,5 +35,5 @@ export const Editor = (props: IPanelProps & { layoutApi: Api }) => {
<button onClick={onClick}>Apply</button> <button onClick={onClick}>Apply</button>
</label> </label>
</div> </div>
) );
} };

View File

@ -1,4 +1,4 @@
import * as React from 'react' import * as React from 'react';
import { import {
ReactGrid, ReactGrid,
OnReadyEvent, OnReadyEvent,
@ -9,33 +9,33 @@ import {
GroupChangeKind, GroupChangeKind,
IGridviewPanelProps, IGridviewPanelProps,
TabContextMenuEvent, TabContextMenuEvent,
} from 'splitview' } from 'splitview';
import { CustomTab } from './customTab' import { CustomTab } from './customTab';
import { Editor } from './editorPanel' import { Editor } from './editorPanel';
import { SplitPanel } from './splitPanel' import { SplitPanel } from './splitPanel';
const components = { const components = {
inner_component: (props: IPanelProps) => { inner_component: (props: IPanelProps) => {
const _api = React.useRef<Api>() const _api = React.useRef<Api>();
const [api, setApi] = React.useState<Api>() const [api, setApi] = React.useState<Api>();
const onReady = (event: OnReadyEvent) => { const onReady = (event: OnReadyEvent) => {
_api.current = event.api _api.current = event.api;
const layout = props.api.getStateKey<object>('layout') const layout = props.api.getStateKey<object>('layout');
if (layout) { if (layout) {
event.api.deserialize(layout) event.api.deserialize(layout);
} else { } else {
event.api.addPanelFromComponent({ event.api.addPanelFromComponent({
componentName: 'test_component', componentName: 'test_component',
id: 'inner-1', id: 'inner-1',
title: 'inner-1', title: 'inner-1',
}) });
event.api.addPanelFromComponent({ event.api.addPanelFromComponent({
componentName: 'test_component', componentName: 'test_component',
id: 'inner-2', id: 'inner-2',
title: 'inner-2', title: 'inner-2',
}) });
event.api.addPanelFromComponent({ event.api.addPanelFromComponent({
componentName: 'test_component', componentName: 'test_component',
id: nextGuid(), id: nextGuid(),
@ -44,7 +44,7 @@ const components = {
direction: 'within', direction: 'within',
referencePanel: 'inner-1', referencePanel: 'inner-1',
}, },
}) });
event.api.addPanelFromComponent({ event.api.addPanelFromComponent({
componentName: 'test_component', componentName: 'test_component',
id: nextGuid(), id: nextGuid(),
@ -53,37 +53,37 @@ const components = {
direction: 'within', direction: 'within',
referencePanel: 'inner-2', referencePanel: 'inner-2',
}, },
}) });
}
setApi(event.api)
} }
setApi(event.api);
};
React.useEffect(() => { React.useEffect(() => {
const compDis = new CompositeDisposable( const compDis = new CompositeDisposable(
props.api.onDidDimensionsChange((event) => { props.api.onDidDimensionsChange((event) => {
_api.current?.layout(event.width, event.height) _api.current?.layout(event.width, event.height);
}), }),
_api.current.onDidLayoutChange((event) => { _api.current.onDidLayoutChange((event) => {
if (event.kind === GroupChangeKind.LAYOUT_CONFIG_UPDATED) { if (event.kind === GroupChangeKind.LAYOUT_CONFIG_UPDATED) {
props.api.setState('layout', _api.current.toJSON()) props.api.setState('layout', _api.current.toJSON());
} }
}) })
) );
return () => { return () => {
compDis.dispose() compDis.dispose();
} };
}, []) }, []);
React.useEffect(() => { React.useEffect(() => {
if (!api) { if (!api) {
return return;
} }
api.onDidLayoutChange((event) => { api.onDidLayoutChange((event) => {
// on inner grid changes // on inner grid changes
}) });
}, [api]) }, [api]);
return ( return (
<div <div
@ -99,16 +99,16 @@ const components = {
debug={true} debug={true}
/> />
</div> </div>
) );
}, },
test_component: (props: IPanelProps & { [key: string]: any }) => { test_component: (props: IPanelProps & { [key: string]: any }) => {
const [panelState, setPanelState] = React.useState<{ const [panelState, setPanelState] = React.useState<{
isGroupActive: boolean isGroupActive: boolean;
isPanelVisible: boolean isPanelVisible: boolean;
}>({ }>({
isGroupActive: false, isGroupActive: false,
isPanelVisible: false, isPanelVisible: false,
}) });
React.useEffect(() => { React.useEffect(() => {
const disposable = new CompositeDisposable( const disposable = new CompositeDisposable(
@ -116,31 +116,31 @@ const components = {
setPanelState((_) => ({ setPanelState((_) => ({
..._, ..._,
isGroupActive: event.isFocused, isGroupActive: event.isFocused,
})) }));
}), }),
props.api.onDidChangeVisibility((x) => { props.api.onDidChangeVisibility((x) => {
setPanelState((_) => ({ setPanelState((_) => ({
..._, ..._,
isPanelVisible: x.isVisible, isPanelVisible: x.isVisible,
})) }));
}) })
) );
props.api.setClosePanelHook(() => { props.api.setClosePanelHook(() => {
if (confirm('close?')) { if (confirm('close?')) {
return Promise.resolve(ClosePanelResult.CLOSE) return Promise.resolve(ClosePanelResult.CLOSE);
} }
return Promise.resolve(ClosePanelResult.DONT_CLOSE) return Promise.resolve(ClosePanelResult.DONT_CLOSE);
}) });
return () => { return () => {
disposable.dispose() disposable.dispose();
} };
}, []) }, []);
const onClick = () => { const onClick = () => {
props.api.setState('test_key', 'hello') props.api.setState('test_key', 'hello');
} };
const backgroundColor = React.useMemo( const backgroundColor = React.useMemo(
() => () =>
@ -149,7 +149,7 @@ const components = {
Math.random() * 256 Math.random() * 256
)},${Math.floor(Math.random() * 256)})`, )},${Math.floor(Math.random() * 256)})`,
[] []
) );
return ( return (
<div <div
style={{ style={{
@ -164,33 +164,33 @@ const components = {
<div>{`G:${panelState.isGroupActive} P:${panelState.isPanelVisible}`}</div> <div>{`G:${panelState.isGroupActive} P:${panelState.isPanelVisible}`}</div>
<div>{props.text || '-'}</div> <div>{props.text || '-'}</div>
</div> </div>
) );
}, },
editor: Editor, editor: Editor,
split_panel: SplitPanel, split_panel: SplitPanel,
} };
const tabComponents = { const tabComponents = {
default: CustomTab, default: CustomTab,
} };
const nextGuid = (() => { const nextGuid = (() => {
let counter = 0 let counter = 0;
return () => 'panel_' + (counter++).toString() return () => 'panel_' + (counter++).toString();
})() })();
export const TestGrid = (props: IGridviewPanelProps) => { export const TestGrid = (props: IGridviewPanelProps) => {
const _api = React.useRef<Api>() const _api = React.useRef<Api>();
const [api, setApi] = React.useState<Api>() const [api, setApi] = React.useState<Api>();
const onReady = (event: OnReadyEvent) => { const onReady = (event: OnReadyEvent) => {
_api.current = event.api _api.current = event.api;
setApi(event.api) setApi(event.api);
} };
React.useEffect(() => { React.useEffect(() => {
if (!api) { if (!api) {
return return;
} }
const panelReference = api.addPanelFromComponent({ const panelReference = api.addPanelFromComponent({
@ -198,24 +198,24 @@ export const TestGrid = (props: IGridviewPanelProps) => {
id: nextGuid(), id: nextGuid(),
title: 'Item 1', title: 'Item 1',
params: { text: 'how low?' }, params: { text: 'how low?' },
}) });
api.addPanelFromComponent({ api.addPanelFromComponent({
componentName: 'test_component', componentName: 'test_component',
id: 'item2', id: 'item2',
title: 'Item 2', title: 'Item 2',
}) });
api.addPanelFromComponent({ api.addPanelFromComponent({
componentName: 'split_panel', componentName: 'split_panel',
id: nextGuid(), id: nextGuid(),
title: 'Item 3 with a long title', title: 'Item 3 with a long title',
}) });
api.addPanelFromComponent({ api.addPanelFromComponent({
componentName: 'test_component', componentName: 'test_component',
id: nextGuid(), id: nextGuid(),
title: 'Item 3', title: 'Item 3',
position: { direction: 'below', referencePanel: 'item2' }, position: { direction: 'below', referencePanel: 'item2' },
suppressClosable: true, suppressClosable: true,
}) });
// setInterval(() => { // setInterval(() => {
// panelReference.update({ params: { text: `Tick ${Date.now()}` } }); // panelReference.update({ params: { text: `Tick ${Date.now()}` } });
@ -223,38 +223,38 @@ export const TestGrid = (props: IGridviewPanelProps) => {
// }, 1000); // }, 1000);
api.addDndHandle('text/plain', (ev) => { api.addDndHandle('text/plain', (ev) => {
const { event } = ev const { event } = ev;
return { return {
id: 'yellow', id: 'yellow',
componentName: 'test_component', componentName: 'test_component',
} };
}) });
api.addDndHandle('Files', (ev) => { api.addDndHandle('Files', (ev) => {
const { event } = ev const { event } = ev;
ev.event.event.preventDefault() ev.event.event.preventDefault();
return { return {
id: Date.now().toString(), id: Date.now().toString(),
title: event.event.dataTransfer.files[0].name, title: event.event.dataTransfer.files[0].name,
componentName: 'test_component', componentName: 'test_component',
} };
}) });
}, [api]) }, [api]);
const onAdd = () => { const onAdd = () => {
const id = nextGuid() const id = nextGuid();
api.addPanelFromComponent({ api.addPanelFromComponent({
componentName: 'test_component', componentName: 'test_component',
id, id,
}) });
} };
const onAddEmpty = () => { const onAddEmpty = () => {
api.addEmptyGroup() api.addEmptyGroup();
} };
React.useEffect(() => { React.useEffect(() => {
// const callback = (ev: UIEvent) => { // const callback = (ev: UIEvent) => {
@ -269,68 +269,68 @@ export const TestGrid = (props: IGridviewPanelProps) => {
props.api.setConstraints({ props.api.setConstraints({
minimumWidth: () => _api.current.minimumWidth, minimumWidth: () => _api.current.minimumWidth,
minimumHeight: () => _api.current.minimumHeight, minimumHeight: () => _api.current.minimumHeight,
}) });
const disposable = new CompositeDisposable( const disposable = new CompositeDisposable(
_api.current.onDidLayoutChange((event) => { _api.current.onDidLayoutChange((event) => {
console.log(event.kind) console.log(event.kind);
}), }),
props.api.onDidDimensionsChange((event) => { props.api.onDidDimensionsChange((event) => {
const { width, height } = event const { width, height } = event;
_api.current.layout(width, height - 20) _api.current.layout(width, height - 20);
}) })
) );
return () => { return () => {
disposable.dispose() disposable.dispose();
// window.removeEventListener("resize", callback); // window.removeEventListener("resize", callback);
} };
}, []) }, []);
const onConfig = () => { const onConfig = () => {
const data = api.toJSON() const data = api.toJSON();
const stringData = JSON.stringify(data, null, 4) const stringData = JSON.stringify(data, null, 4);
console.log(stringData) console.log(stringData);
localStorage.setItem('layout', stringData) localStorage.setItem('layout', stringData);
} };
const onLoad = async () => { const onLoad = async () => {
const didClose = await api.closeAllGroups() const didClose = await api.closeAllGroups();
if (!didClose) { if (!didClose) {
return return;
} }
const data = localStorage.getItem('layout') const data = localStorage.getItem('layout');
if (data) { if (data) {
const jsonData = JSON.parse(data) const jsonData = JSON.parse(data);
api.deserialize(jsonData) api.deserialize(jsonData);
}
} }
};
const onClear = () => { const onClear = () => {
api.closeAllGroups() api.closeAllGroups();
} };
const onNextGroup = () => { const onNextGroup = () => {
api.moveToNext({ includePanel: true }) api.moveToNext({ includePanel: true });
} };
const onPreviousGroup = () => { const onPreviousGroup = () => {
api.moveToPrevious({ includePanel: true }) api.moveToPrevious({ includePanel: true });
} };
const onNextPanel = () => { const onNextPanel = () => {
api.activeGroup?.moveToNext() api.activeGroup?.moveToNext();
} };
const onPreviousPanel = () => { const onPreviousPanel = () => {
api.activeGroup?.moveToPrevious() api.activeGroup?.moveToPrevious();
} };
const dragRef = React.useRef<HTMLDivElement>() const dragRef = React.useRef<HTMLDivElement>();
React.useEffect(() => { React.useEffect(() => {
if (!api) { if (!api) {
return return;
} }
api.createDragTarget( api.createDragTarget(
{ element: dragRef.current, content: 'drag me' }, { element: dragRef.current, content: 'drag me' },
@ -338,12 +338,12 @@ export const TestGrid = (props: IGridviewPanelProps) => {
id: 'yellow', id: 'yellow',
componentName: 'test_component', componentName: 'test_component',
}) })
) );
}, [api]) }, [api]);
const onDragStart = (event: React.DragEvent) => { const onDragStart = (event: React.DragEvent) => {
event.dataTransfer.setData('text/plain', 'Panel2') event.dataTransfer.setData('text/plain', 'Panel2');
} };
const onAddEditor = () => { const onAddEditor = () => {
api.addPanelFromComponent({ api.addPanelFromComponent({
@ -351,15 +351,15 @@ export const TestGrid = (props: IGridviewPanelProps) => {
componentName: 'editor', componentName: 'editor',
tabComponentName: 'default', tabComponentName: 'default',
params: { layoutApi: api }, params: { layoutApi: api },
}) });
} };
const onTabContextMenu = React.useMemo( const onTabContextMenu = React.useMemo(
() => (event: TabContextMenuEvent) => { () => (event: TabContextMenuEvent) => {
console.log(event) console.log(event);
}, },
[] []
) );
return ( return (
<div <div
@ -421,5 +421,5 @@ export const TestGrid = (props: IGridviewPanelProps) => {
onTabContextMenu={onTabContextMenu} onTabContextMenu={onTabContextMenu}
/> />
</div> </div>
) );
} };

View File

@ -1,4 +1,4 @@
import * as React from 'react' import * as React from 'react';
import { import {
CompositeDisposable, CompositeDisposable,
IPanelProps, IPanelProps,
@ -6,65 +6,65 @@ import {
Orientation, Orientation,
SplitviewFacade, SplitviewFacade,
SplitviewReadyEvent, SplitviewReadyEvent,
} from 'splitview' } from 'splitview';
import { SplitViewComponent } from 'splitview' import { SplitViewComponent } from 'splitview';
const components = { const components = {
default1: (props: ISplitviewPanelProps) => { default1: (props: ISplitviewPanelProps) => {
const [focused, setFocused] = React.useState<boolean>(false) const [focused, setFocused] = React.useState<boolean>(false);
React.useEffect(() => { React.useEffect(() => {
const disposable = new CompositeDisposable( const disposable = new CompositeDisposable(
props.api.onDidFocusChange((event) => { props.api.onDidFocusChange((event) => {
setFocused(event.isFocused) setFocused(event.isFocused);
}) })
) );
return () => { return () => {
disposable.dispose() disposable.dispose();
} };
}, []) }, []);
return ( return (
<div <div
style={{ height: '100%', width: '100%' }} style={{ height: '100%', width: '100%' }}
>{`component [isFocused: ${focused}]`}</div> >{`component [isFocused: ${focused}]`}</div>
) );
}, },
} };
export const SplitPanel = (props: IPanelProps) => { export const SplitPanel = (props: IPanelProps) => {
const api = React.useRef<SplitviewFacade>() const api = React.useRef<SplitviewFacade>();
React.useEffect(() => { React.useEffect(() => {
const disposable = new CompositeDisposable( const disposable = new CompositeDisposable(
props.api.onDidDimensionsChange((event) => { props.api.onDidDimensionsChange((event) => {
api.current?.layout(event.width, event.height - 20) api.current?.layout(event.width, event.height - 20);
}), }),
api.current.onChange((event) => { api.current.onChange((event) => {
props.api.setState('sview_layout', api.current.toJSON()) props.api.setState('sview_layout', api.current.toJSON());
}) })
) );
return () => { return () => {
disposable.dispose() disposable.dispose();
} };
}, []) }, []);
const onReady = (event: SplitviewReadyEvent) => { const onReady = (event: SplitviewReadyEvent) => {
const existingLayout = props.api.getStateKey('sview_layout') const existingLayout = props.api.getStateKey('sview_layout');
if (existingLayout) { if (existingLayout) {
event.api.deserialize(existingLayout) event.api.deserialize(existingLayout);
} else { } else {
event.api.addFromComponent({ id: '1', component: 'default1' }) event.api.addFromComponent({ id: '1', component: 'default1' });
event.api.addFromComponent({ id: '2', component: 'default1' }) event.api.addFromComponent({ id: '2', component: 'default1' });
}
api.current = event.api
} }
api.current = event.api;
};
const onSave = () => { const onSave = () => {
props.api.setState('sview_layout', api.current.toJSON()) props.api.setState('sview_layout', api.current.toJSON());
} };
return ( return (
<div <div
@ -84,5 +84,5 @@ export const SplitPanel = (props: IPanelProps) => {
orientation={Orientation.VERTICAL} orientation={Orientation.VERTICAL}
/> />
</div> </div>
) );
} };

View File

@ -1,4 +1,4 @@
var path = require('path') var path = require('path');
module.exports = { module.exports = {
entry: path.resolve(__dirname, 'src/index.tsx'), entry: path.resolve(__dirname, 'src/index.tsx'),
@ -50,4 +50,4 @@ module.exports = {
contentBase: path.resolve(__dirname, 'public'), contentBase: path.resolve(__dirname, 'public'),
publicPath: '/dist', publicPath: '/dist',
}, },
} };

View File

@ -1,7 +1,7 @@
const gulp = require('gulp') const gulp = require('gulp');
const buildfile = require('../../scripts/build') const buildfile = require('../../scripts/build');
const package = require('./package') const package = require('./package');
buildfile.build({ tsconfig: './tsconfig.build.json', package }) buildfile.build({ tsconfig: './tsconfig.build.json', package });
gulp.task('run', gulp.series(['clean', 'esm', 'sass'])) gulp.task('run', gulp.series(['clean', 'esm', 'sass']));

View File

@ -1,121 +1,121 @@
import * as React from 'react' import * as React from 'react';
import { Orientation } from 'splitview' import { Orientation } from 'splitview';
import { IViewWithReactComponent } from '../splitview' import { IViewWithReactComponent } from '../splitview';
// component view // component view
export interface IPaneComponentProps extends IViewWithReactComponent { export interface IPaneComponentProps extends IViewWithReactComponent {
setExpanded(expanded: boolean): void setExpanded(expanded: boolean): void;
orientation: Orientation orientation: Orientation;
size: number size: number;
orthogonalSize: number orthogonalSize: number;
userprops?: { [index: string]: any } userprops?: { [index: string]: any };
} }
export interface IPaneComponentRef { export interface IPaneComponentRef {
layout: (size: number, orthogonalSize: number) => void layout: (size: number, orthogonalSize: number) => void;
} }
export type PaneComponent = React.ForwardRefRenderFunction< export type PaneComponent = React.ForwardRefRenderFunction<
IPaneComponentRef, IPaneComponentRef,
IPaneComponentProps IPaneComponentProps
> >;
export interface IPaneHeaderComponentProps extends IViewWithReactComponent { export interface IPaneHeaderComponentProps extends IViewWithReactComponent {
setExpanded(expanded: boolean): void setExpanded(expanded: boolean): void;
isExpanded: boolean isExpanded: boolean;
userprops?: { [index: string]: any } userprops?: { [index: string]: any };
} }
export type PaneHeaderComponent = React.ForwardRefRenderFunction< export type PaneHeaderComponent = React.ForwardRefRenderFunction<
{}, {},
IPaneHeaderComponentProps IPaneHeaderComponentProps
> >;
// component view facade // component view facade
export interface IPaneRootProps { export interface IPaneRootProps {
component: PaneComponent component: PaneComponent;
props: {} props: {};
} }
export interface IPaneHeaderRootProps { export interface IPaneHeaderRootProps {
component: PaneHeaderComponent component: PaneHeaderComponent;
props: {} props: {};
} }
export interface IPaneRootRef extends IPaneComponentRef { export interface IPaneRootRef extends IPaneComponentRef {
updateProps: (props: Partial<IPaneComponentProps>) => void updateProps: (props: Partial<IPaneComponentProps>) => void;
} }
export interface IPaneHeaderRootRef { export interface IPaneHeaderRootRef {
updateProps: (props: Partial<IPaneHeaderComponentProps>) => void updateProps: (props: Partial<IPaneHeaderComponentProps>) => void;
} }
export const PaneRoot = React.forwardRef( export const PaneRoot = React.forwardRef(
(props: IPaneRootProps, facadeRef: React.Ref<IPaneRootRef>) => { (props: IPaneRootProps, facadeRef: React.Ref<IPaneRootRef>) => {
const ref = React.useRef<IPaneComponentRef>() const ref = React.useRef<IPaneComponentRef>();
const [facadeProps, setFacadeProps] = React.useState< const [facadeProps, setFacadeProps] = React.useState<
IPaneComponentProps IPaneComponentProps
>() >();
React.useImperativeHandle( React.useImperativeHandle(
facadeRef, facadeRef,
() => { () => {
return { return {
updateProps: (props) => { updateProps: (props) => {
setFacadeProps((_props) => ({ ..._props, ...props })) setFacadeProps((_props) => ({ ..._props, ...props }));
}, },
layout: (size, orthogonalSize) => { layout: (size, orthogonalSize) => {
ref.current?.layout(size, orthogonalSize) ref.current?.layout(size, orthogonalSize);
}, },
} };
}, },
[ref] [ref]
) );
const Component = React.useMemo( const Component = React.useMemo(
() => React.forwardRef(props.component), () => React.forwardRef(props.component),
[props.component] [props.component]
) );
const _props = React.useMemo( const _props = React.useMemo(
() => ({ ...props.props, ...facadeProps, ref }), () => ({ ...props.props, ...facadeProps, ref }),
[props.props, facadeProps] [props.props, facadeProps]
) );
return React.createElement(Component, _props) return React.createElement(Component, _props);
} }
) );
export const PaneHeaderRoot = React.forwardRef( export const PaneHeaderRoot = React.forwardRef(
(props: IPaneHeaderRootProps, facadeRef: React.Ref<IPaneHeaderRootRef>) => { (props: IPaneHeaderRootProps, facadeRef: React.Ref<IPaneHeaderRootRef>) => {
const [facadeProps, setFacadeProps] = React.useState< const [facadeProps, setFacadeProps] = React.useState<
IPaneHeaderComponentProps IPaneHeaderComponentProps
>() >();
React.useImperativeHandle( React.useImperativeHandle(
facadeRef, facadeRef,
() => { () => {
return { return {
updateProps: (props) => { updateProps: (props) => {
setFacadeProps((_props) => ({ ..._props, ...props })) setFacadeProps((_props) => ({ ..._props, ...props }));
}, },
} };
}, },
[] []
) );
const Component = React.useMemo( const Component = React.useMemo(
() => React.forwardRef(props.component), () => React.forwardRef(props.component),
[props.component] [props.component]
) );
const _props = React.useMemo( const _props = React.useMemo(
() => ({ ...props.props, ...facadeProps }), () => ({ ...props.props, ...facadeProps }),
[props.props, facadeProps] [props.props, facadeProps]
) );
return React.createElement(Component, _props) return React.createElement(Component, _props);
} }
) );

View File

@ -1,67 +1,67 @@
import * as React from 'react' import * as React from 'react';
import { IViewWithReactComponent } from '../splitview' import { IViewWithReactComponent } from '../splitview';
// component view // component view
export interface IViewComponentProps export interface IViewComponentProps
extends Omit<IViewWithReactComponent, 'component'> { extends Omit<IViewWithReactComponent, 'component'> {
userprops?: { [index: string]: any } userprops?: { [index: string]: any };
} }
export interface IViewComponentRef { export interface IViewComponentRef {
layout: (size: number, orthogonalSize: number) => void layout: (size: number, orthogonalSize: number) => void;
} }
export type ViewComponent = React.ForwardRefRenderFunction< export type ViewComponent = React.ForwardRefRenderFunction<
IViewComponentRef, IViewComponentRef,
IViewComponentProps IViewComponentProps
> >;
// component view facade // component view facade
export interface IViewRootProps { export interface IViewRootProps {
component: ViewComponent component: ViewComponent;
props: {} props: {};
} }
export interface IViewRootRef extends IViewComponentRef { export interface IViewRootRef extends IViewComponentRef {
updateProps: (props: Partial<IViewComponentProps>) => void updateProps: (props: Partial<IViewComponentProps>) => void;
} }
export const ViewRoot = React.forwardRef( export const ViewRoot = React.forwardRef(
(props: IViewRootProps, facadeRef: React.Ref<IViewRootRef>) => { (props: IViewRootProps, facadeRef: React.Ref<IViewRootRef>) => {
const ref = React.useRef<IViewComponentRef>() const ref = React.useRef<IViewComponentRef>();
const [facadeProps, setFacadeProps] = React.useState< const [facadeProps, setFacadeProps] = React.useState<
IViewComponentProps IViewComponentProps
>() >();
React.useImperativeHandle( React.useImperativeHandle(
facadeRef, facadeRef,
() => { () => {
return { return {
updateProps: (props) => { updateProps: (props) => {
setFacadeProps((_props) => ({ ..._props, ...props })) setFacadeProps((_props) => ({ ..._props, ...props }));
}, },
layout: (size, orthogonalSize) => { layout: (size, orthogonalSize) => {
ref.current?.layout(size, orthogonalSize) ref.current?.layout(size, orthogonalSize);
}, },
} };
}, },
[ref] [ref]
) );
const Component = React.useMemo( const Component = React.useMemo(
() => React.forwardRef(props.component), () => React.forwardRef(props.component),
[props.component] [props.component]
) );
const _props = React.useMemo( const _props = React.useMemo(
() => ({ ...props.props, ...facadeProps, ref }), () => ({ ...props.props, ...facadeProps, ref }),
[props.props, facadeProps] [props.props, facadeProps]
) );
return React.createElement(Component, _props) return React.createElement(Component, _props);
// return <Component ref={ref} {...props.props} {...facadeProps} />; // return <Component ref={ref} {...props.props} {...facadeProps} />;
} }
) );

View File

@ -1,6 +1,6 @@
export * from './splitview' export * from './splitview';
export * from './paneview' export * from './paneview';
export * from './bridge/view' export * from './bridge/view';
export * from './panel/view' export * from './panel/view';
export * from './bridge/pane' export * from './bridge/pane';
export * from './panel/pane' export * from './panel/pane';

View File

@ -1,6 +1,6 @@
import * as React from 'react' import * as React from 'react';
import * as ReactDOM from 'react-dom' import * as ReactDOM from 'react-dom';
import { Pane, IDisposable } from 'splitview' import { Pane, IDisposable } from 'splitview';
import { import {
PaneComponent, PaneComponent,
@ -9,47 +9,47 @@ import {
PaneHeaderComponent, PaneHeaderComponent,
PaneHeaderRoot, PaneHeaderRoot,
IPaneHeaderRootRef, IPaneHeaderRootRef,
} from '../bridge/pane' } from '../bridge/pane';
import { IViewWithReactComponent } from '../splitview' import { IViewWithReactComponent } from '../splitview';
import { IPaneWithReactComponent } from '../paneview' import { IPaneWithReactComponent } from '../paneview';
export class PaneReact extends Pane { export class PaneReact extends Pane {
public readonly id: string public readonly id: string;
private bodyDisposable: IDisposable private bodyDisposable: IDisposable;
private headerDisposable: IDisposable private headerDisposable: IDisposable;
private bodyRef: IPaneRootRef private bodyRef: IPaneRootRef;
private headerRef: IPaneHeaderRootRef private headerRef: IPaneHeaderRootRef;
private disposable: IDisposable private disposable: IDisposable;
constructor( constructor(
private readonly view: IPaneWithReactComponent, private readonly view: IPaneWithReactComponent,
private readonly bodyComponent: PaneComponent, private readonly bodyComponent: PaneComponent,
private readonly options: { private readonly options: {
headerName: string headerName: string;
addPortal: (portal: React.ReactPortal) => IDisposable addPortal: (portal: React.ReactPortal) => IDisposable;
headerComponent?: PaneHeaderComponent headerComponent?: PaneHeaderComponent;
} }
) { ) {
super({ isExpanded: view.isExpanded }) super({ isExpanded: view.isExpanded });
this.layout = this.layout.bind(this) this.layout = this.layout.bind(this);
this.onDidChange = this.onDidChange.bind(this) this.onDidChange = this.onDidChange.bind(this);
this.setRef = this.setRef.bind(this) this.setRef = this.setRef.bind(this);
this.setHeaderRef = this.setHeaderRef.bind(this) this.setHeaderRef = this.setHeaderRef.bind(this);
this.setExpanded = this.setExpanded.bind(this) this.setExpanded = this.setExpanded.bind(this);
this.id = view.id this.id = view.id;
this.minimumSize = view.minimumSize this.minimumSize = view.minimumSize;
this.maximumSize = view.maximumSize this.maximumSize = view.maximumSize;
this.render() this.render();
} }
public renderBody(element: HTMLElement) { public renderBody(element: HTMLElement) {
if (this.bodyDisposable) { if (this.bodyDisposable) {
this.bodyDisposable.dispose() this.bodyDisposable.dispose();
this.bodyDisposable = undefined this.bodyDisposable = undefined;
} }
const bodyPortal = ReactDOM.createPortal( const bodyPortal = ReactDOM.createPortal(
@ -65,21 +65,21 @@ export class PaneReact extends Pane {
}} }}
/>, />,
element element
) );
this.bodyDisposable = this.options.addPortal(bodyPortal) this.bodyDisposable = this.options.addPortal(bodyPortal);
} }
public renderHeader(element: HTMLElement) { public renderHeader(element: HTMLElement) {
if (this.headerDisposable) { if (this.headerDisposable) {
this.headerDisposable.dispose() this.headerDisposable.dispose();
this.disposable?.dispose() this.disposable?.dispose();
this.headerDisposable = undefined this.headerDisposable = undefined;
} }
if (this.options.headerComponent) { if (this.options.headerComponent) {
this.disposable = this.onDidChangeExpansionState((isExpanded) => { this.disposable = this.onDidChangeExpansionState((isExpanded) => {
this.headerRef?.updateProps({ isExpanded }) this.headerRef?.updateProps({ isExpanded });
}) });
const headerPortal = ReactDOM.createPortal( const headerPortal = ReactDOM.createPortal(
<PaneHeaderRoot <PaneHeaderRoot
@ -94,50 +94,50 @@ export class PaneReact extends Pane {
}} }}
/>, />,
element element
) );
this.headerDisposable = this.options.addPortal(headerPortal) this.headerDisposable = this.options.addPortal(headerPortal);
} else { } else {
element.textContent = this.options.headerName element.textContent = this.options.headerName;
element.onclick = () => { element.onclick = () => {
this.setExpanded(!this.isExpanded()) this.setExpanded(!this.isExpanded());
} };
} }
} }
public update(view: IViewWithReactComponent) { public update(view: IViewWithReactComponent) {
this.minimumSize = view.minimumSize this.minimumSize = view.minimumSize;
this.maximumSize = view.maximumSize this.maximumSize = view.maximumSize;
this.render() this.render();
this.bodyRef?.updateProps({ this.bodyRef?.updateProps({
minimumSize: this.minimumSize, minimumSize: this.minimumSize,
maximumSize: this.maximumSize, maximumSize: this.maximumSize,
}) });
} }
public layout(size: number, orthogonalSize: number) { public layout(size: number, orthogonalSize: number) {
super.layout(size, orthogonalSize) super.layout(size, orthogonalSize);
this.orthogonalSize = orthogonalSize this.orthogonalSize = orthogonalSize;
this.bodyRef?.layout(size, orthogonalSize) this.bodyRef?.layout(size, orthogonalSize);
this.bodyRef?.updateProps({ size, orthogonalSize }) this.bodyRef?.updateProps({ size, orthogonalSize });
} }
private setRef(ref: IPaneRootRef) { private setRef(ref: IPaneRootRef) {
this.bodyRef = ref this.bodyRef = ref;
} }
private setHeaderRef(ref: IPaneRootRef) { private setHeaderRef(ref: IPaneRootRef) {
this.headerRef = ref this.headerRef = ref;
this.headerRef?.updateProps({ this.headerRef?.updateProps({
isExpanded: this.isExpanded(), isExpanded: this.isExpanded(),
setExpanded: this.setExpanded, setExpanded: this.setExpanded,
}) });
} }
public dispose() { public dispose() {
this.bodyDisposable?.dispose() this.bodyDisposable?.dispose();
this.headerDisposable?.dispose() this.headerDisposable?.dispose();
this.disposable?.dispose() this.disposable?.dispose();
} }
} }

View File

@ -1,30 +1,30 @@
import * as React from 'react' import * as React from 'react';
import * as ReactDOM from 'react-dom' import * as ReactDOM from 'react-dom';
import { IView, Emitter } from 'splitview' import { IView, Emitter } from 'splitview';
import { IViewRootRef, ViewComponent, ViewRoot } from '../bridge/view' import { IViewRootRef, ViewComponent, ViewRoot } from '../bridge/view';
import { IViewWithReactComponent } from '../splitview' import { IViewWithReactComponent } from '../splitview';
export class ReactRenderView implements IView { export class ReactRenderView implements IView {
private ref: IViewRootRef private ref: IViewRootRef;
private disposable: { dispose: () => void } private disposable: { dispose: () => void };
public readonly id: string public readonly id: string;
private readonly component: ViewComponent private readonly component: ViewComponent;
public readonly props: {} public readonly props: {};
public element: HTMLElement public element: HTMLElement;
public minimumSize: number public minimumSize: number;
public maximumSize: number public maximumSize: number;
public snapSize: number public snapSize: number;
public size: number public size: number;
private readonly _onDidChange = new Emitter<number | undefined>() private readonly _onDidChange = new Emitter<number | undefined>();
public readonly onDidChange = this._onDidChange.event public readonly onDidChange = this._onDidChange.event;
private _rendered = false private _rendered = false;
private _size: number private _size: number;
private _orthogonalSize: number private _orthogonalSize: number;
constructor( constructor(
view: IViewWithReactComponent, view: IViewWithReactComponent,
@ -32,52 +32,52 @@ export class ReactRenderView implements IView {
portal: React.ReactPortal portal: React.ReactPortal
) => { dispose: () => void } ) => { dispose: () => void }
) { ) {
this.layout = this.layout.bind(this) this.layout = this.layout.bind(this);
this.onDidChange = this.onDidChange.bind(this) this.onDidChange = this.onDidChange.bind(this);
this.setRef = this.setRef.bind(this) this.setRef = this.setRef.bind(this);
this.id = view.id this.id = view.id;
this.component = view.component this.component = view.component;
this.props = view.props this.props = view.props;
this.minimumSize = view.minimumSize this.minimumSize = view.minimumSize;
this.maximumSize = view.maximumSize this.maximumSize = view.maximumSize;
this.snapSize = view.snapSize this.snapSize = view.snapSize;
this.element = document.createElement('div') this.element = document.createElement('div');
this.element.id = 'react-attachable-view' this.element.id = 'react-attachable-view';
} }
public update(view: IView) { public update(view: IView) {
this.minimumSize = view.minimumSize this.minimumSize = view.minimumSize;
this.maximumSize = view.maximumSize this.maximumSize = view.maximumSize;
this.snapSize = view.snapSize this.snapSize = view.snapSize;
this.ref?.updateProps({ this.ref?.updateProps({
minimumSize: this.minimumSize, minimumSize: this.minimumSize,
maximumSize: this.maximumSize, maximumSize: this.maximumSize,
snapSize: this.snapSize, snapSize: this.snapSize,
}) });
} }
public layout(size: number, orthogonalSize: number) { public layout(size: number, orthogonalSize: number) {
if (!this._rendered) { if (!this._rendered) {
this.attachReactComponent() this.attachReactComponent();
this._rendered = true this._rendered = true;
} }
this._size = size this._size = size;
this._orthogonalSize = orthogonalSize this._orthogonalSize = orthogonalSize;
this.ref?.layout(size, orthogonalSize) this.ref?.layout(size, orthogonalSize);
} }
private attachReactComponent() { private attachReactComponent() {
const portal = this.createReactElement() const portal = this.createReactElement();
if (this.disposable) { if (this.disposable) {
this.disposable.dispose() this.disposable.dispose();
this.disposable = undefined this.disposable = undefined;
} }
this.disposable = this.addPortal(portal) this.disposable = this.addPortal(portal);
} }
private createReactElement() { private createReactElement() {
@ -94,15 +94,15 @@ export class ReactRenderView implements IView {
}} }}
/>, />,
this.element this.element
) );
} }
private setRef(ref: IViewRootRef) { private setRef(ref: IViewRootRef) {
this.ref = ref this.ref = ref;
this.ref?.layout(this._size, this._orthogonalSize) this.ref?.layout(this._size, this._orthogonalSize);
} }
public dispose() { public dispose() {
this.disposable?.dispose() this.disposable?.dispose();
} }
} }

View File

@ -1,39 +1,39 @@
import * as React from 'react' import * as React from 'react';
import { Orientation, IBaseView, PaneView } from 'splitview' import { Orientation, IBaseView, PaneView } from 'splitview';
import { PaneReact } from './panel/pane' import { PaneReact } from './panel/pane';
import { PaneComponent, PaneHeaderComponent } from './bridge/pane' import { PaneComponent, PaneHeaderComponent } from './bridge/pane';
export interface IPaneWithReactComponent extends IBaseView { export interface IPaneWithReactComponent extends IBaseView {
id: string id: string;
headerId: string headerId: string;
component: PaneComponent component: PaneComponent;
headerComponent: PaneHeaderComponent headerComponent: PaneHeaderComponent;
isExpanded: boolean isExpanded: boolean;
componentProps: {} componentProps: {};
headerProps: {} headerProps: {};
} }
export interface IPaneViewReactProps { export interface IPaneViewReactProps {
orientation: Orientation orientation: Orientation;
onReady?: (event: PaneViewReadyEvent) => void onReady?: (event: PaneViewReadyEvent) => void;
components?: { [index: string]: PaneComponent } components?: { [index: string]: PaneComponent };
headerComponents?: { [index: string]: PaneHeaderComponent } headerComponents?: { [index: string]: PaneHeaderComponent };
size: number size: number;
orthogonalSize: number orthogonalSize: number;
initialLayout?: PaneViewSerializedConfig initialLayout?: PaneViewSerializedConfig;
} }
export interface PaneViewReadyEvent { export interface PaneViewReadyEvent {
api: PaneviewApi api: PaneviewApi;
} }
export interface PaneViewSerializedConfig { export interface PaneViewSerializedConfig {
views: Array< views: Array<
Omit<IPaneWithReactComponent, 'component' | 'headerComponent'> & { Omit<IPaneWithReactComponent, 'component' | 'headerComponent'> & {
size?: number size?: number;
} }
> >;
} }
export interface PaneviewApi { export interface PaneviewApi {
@ -42,27 +42,27 @@ export interface PaneviewApi {
IPaneWithReactComponent, IPaneWithReactComponent,
'component' | 'headerComponent' 'component' | 'headerComponent'
> & { > & {
size?: number size?: number;
index?: number index?: number;
} }
) => void ) => void;
moveView: (from: number, to: number) => void moveView: (from: number, to: number) => void;
toJSON: () => {} toJSON: () => {};
} }
export interface IPaneViewComponentRef { export interface IPaneViewComponentRef {
layout: (size: number, orthogonalSize: number) => void layout: (size: number, orthogonalSize: number) => void;
} }
export const PaneViewComponent = React.forwardRef( export const PaneViewComponent = React.forwardRef(
(props: IPaneViewReactProps, _ref: React.Ref<IPaneViewComponentRef>) => { (props: IPaneViewReactProps, _ref: React.Ref<IPaneViewComponentRef>) => {
const ref = React.useRef<HTMLDivElement>() const ref = React.useRef<HTMLDivElement>();
const dimension = React.useRef<{ const dimension = React.useRef<{
size: number size: number;
orthogonalSize: number orthogonalSize: number;
}>() }>();
const paneview = React.useRef<PaneView>() const paneview = React.useRef<PaneView>();
const [portals, setPortals] = React.useState<React.ReactPortal[]>([]) const [portals, setPortals] = React.useState<React.ReactPortal[]>([]);
const createView = React.useCallback( const createView = React.useCallback(
(_view: IPaneWithReactComponent) => { (_view: IPaneWithReactComponent) => {
@ -70,47 +70,47 @@ export const PaneViewComponent = React.forwardRef(
headerName: 'header', headerName: 'header',
headerComponent: _view.headerComponent, headerComponent: _view.headerComponent,
addPortal: (portal) => { addPortal: (portal) => {
setPortals((portals) => [...portals, portal]) setPortals((portals) => [...portals, portal]);
return { return {
dispose: () => { dispose: () => {
setPortals((portals) => setPortals((portals) =>
portals.filter((_) => _ !== portal) portals.filter((_) => _ !== portal)
) );
}, },
} };
}, },
}) });
}, },
[] []
) );
const hydrate = React.useCallback(() => { const hydrate = React.useCallback(() => {
if (!props.initialLayout || !paneview.current) { if (!props.initialLayout || !paneview.current) {
return return;
} }
const serializedConfig = props.initialLayout const serializedConfig = props.initialLayout;
serializedConfig.views.forEach((view) => { serializedConfig.views.forEach((view) => {
const component = props.components[view.id] const component = props.components[view.id];
const headerComponent = props.headerComponents[view.headerId] const headerComponent = props.headerComponents[view.headerId];
paneview.current.addPane( paneview.current.addPane(
createView({ ...view, component, headerComponent }), createView({ ...view, component, headerComponent }),
view.size view.size
) );
}) });
paneview.current.layout(props.size, props.orthogonalSize) paneview.current.layout(props.size, props.orthogonalSize);
}, [props.initialLayout]) }, [props.initialLayout]);
React.useEffect(() => { React.useEffect(() => {
if (paneview.current && dimension?.current) { if (paneview.current && dimension?.current) {
paneview.current?.layout( paneview.current?.layout(
dimension.current.size, dimension.current.size,
dimension.current.orthogonalSize dimension.current.orthogonalSize
) );
dimension.current = undefined dimension.current = undefined;
} }
}, [paneview.current]) }, [paneview.current]);
// if you put this in a hook it's laggy // if you put this in a hook it's laggy
// paneview.current?.layout(props.size, props.orthogonalSize); // paneview.current?.layout(props.size, props.orthogonalSize);
@ -122,20 +122,20 @@ export const PaneViewComponent = React.forwardRef(
if (!paneview.current) { if (!paneview.current) {
// handle the case when layout is called and paneview doesn't exist yet // handle the case when layout is called and paneview doesn't exist yet
// we cache the values and use them at the first opportunity // we cache the values and use them at the first opportunity
dimension.current = { size, orthogonalSize } dimension.current = { size, orthogonalSize };
} }
paneview.current?.layout(size, orthogonalSize) paneview.current?.layout(size, orthogonalSize);
}, },
}), }),
[paneview] [paneview]
) );
React.useEffect(() => { React.useEffect(() => {
paneview.current = new PaneView(ref.current, { paneview.current = new PaneView(ref.current, {
orientation: props.orientation, orientation: props.orientation,
}) });
hydrate() hydrate();
if (props.onReady) { if (props.onReady) {
props.onReady({ props.onReady({
@ -145,14 +145,14 @@ export const PaneViewComponent = React.forwardRef(
IPaneWithReactComponent, IPaneWithReactComponent,
'component' | 'headerComponent' 'component' | 'headerComponent'
> & { > & {
props?: {} props?: {};
size?: number size?: number;
index?: number index?: number;
} }
) => { ) => {
const component = props.components[options.id] const component = props.components[options.id];
const headerComponent = const headerComponent =
props.headerComponents[options.headerId] props.headerComponents[options.headerId];
paneview.current.addPane( paneview.current.addPane(
createView({ createView({
...options, ...options,
@ -161,14 +161,14 @@ export const PaneViewComponent = React.forwardRef(
}), }),
options.size, options.size,
options.index options.index
) );
paneview.current.layout( paneview.current.layout(
props.size, props.size,
props.orthogonalSize props.orthogonalSize
) );
}, },
moveView: (from: number, to: number) => { moveView: (from: number, to: number) => {
paneview.current.moveView(from, to) paneview.current.moveView(from, to);
}, },
toJSON: () => { toJSON: () => {
return { return {
@ -188,28 +188,28 @@ export const PaneViewComponent = React.forwardRef(
// {} // {}
// ) // )
// ), // ),
} };
}, },
}, },
}) });
} }
paneview.current.layout(props.size, props.orthogonalSize) paneview.current.layout(props.size, props.orthogonalSize);
return () => { return () => {
paneview.current?.dispose() paneview.current?.dispose();
paneview.current = undefined paneview.current = undefined;
} };
}, []) }, []);
React.useEffect(() => { React.useEffect(() => {
paneview.current?.setOrientation(props.orientation) paneview.current?.setOrientation(props.orientation);
}, [props.orientation]) }, [props.orientation]);
return ( return (
<div ref={ref} className="split-view-react-wrapper"> <div ref={ref} className="split-view-react-wrapper">
{portals} {portals}
</div> </div>
) );
} }
) );

View File

@ -1,90 +1,92 @@
import * as React from 'react' import * as React from 'react';
import { SplitView, Orientation, IBaseView } from 'splitview' import { SplitView, Orientation, IBaseView } from 'splitview';
import { ReactRenderView } from './panel/view' import { ReactRenderView } from './panel/view';
import { ViewComponent } from './bridge/view' import { ViewComponent } from './bridge/view';
export interface IViewWithReactComponent extends IBaseView { export interface IViewWithReactComponent extends IBaseView {
id: string id: string;
props?: {} props?: {};
component: ViewComponent component: ViewComponent;
} }
export interface OnReadyEvent { export interface OnReadyEvent {
api: SplitviewApi api: SplitviewApi;
} }
export interface SerializedConfig { export interface SerializedConfig {
views: Array<Omit<IViewWithReactComponent, 'component'> & { size?: number }> views: Array<
Omit<IViewWithReactComponent, 'component'> & { size?: number }
>;
} }
export interface SplitviewApi { export interface SplitviewApi {
add: ( add: (
options: Omit<IViewWithReactComponent, 'component'> & { options: Omit<IViewWithReactComponent, 'component'> & {
size?: number size?: number;
index?: number index?: number;
} }
) => void ) => void;
moveView: (from: number, to: number) => void moveView: (from: number, to: number) => void;
toJSON: () => {} toJSON: () => {};
} }
export interface ISplitViewReactProps { export interface ISplitViewReactProps {
orientation: Orientation orientation: Orientation;
size: number size: number;
orthogonalSize: number orthogonalSize: number;
onReady?: (event: OnReadyEvent) => void onReady?: (event: OnReadyEvent) => void;
components?: { [index: string]: ViewComponent } components?: { [index: string]: ViewComponent };
initialLayout?: SerializedConfig initialLayout?: SerializedConfig;
} }
export interface ISplitViewComponentRef { export interface ISplitViewComponentRef {
layout: (size: number, orthogonalSize: number) => void layout: (size: number, orthogonalSize: number) => void;
} }
export const SplitViewComponent = React.forwardRef( export const SplitViewComponent = React.forwardRef(
(props: ISplitViewReactProps, ref: React.Ref<ISplitViewComponentRef>) => { (props: ISplitViewReactProps, ref: React.Ref<ISplitViewComponentRef>) => {
const containerRef = React.useRef<HTMLDivElement>() const containerRef = React.useRef<HTMLDivElement>();
const splitview = React.useRef<SplitView>() const splitview = React.useRef<SplitView>();
const [portals, setPortals] = React.useState<React.ReactPortal[]>([]) const [portals, setPortals] = React.useState<React.ReactPortal[]>([]);
const hydrate = React.useCallback(() => { const hydrate = React.useCallback(() => {
if (!props.initialLayout || !splitview.current) { if (!props.initialLayout || !splitview.current) {
return return;
} }
const serializedConfig = props.initialLayout const serializedConfig = props.initialLayout;
serializedConfig.views.forEach((view) => { serializedConfig.views.forEach((view) => {
const component = props.components[view.id] const component = props.components[view.id];
splitview.current.addView( splitview.current.addView(
createView({ ...view, component }), createView({ ...view, component }),
view.size view.size
) );
}) });
splitview.current.layout(props.size, props.orthogonalSize) splitview.current.layout(props.size, props.orthogonalSize);
}, [props.initialLayout]) }, [props.initialLayout]);
React.useEffect(() => { React.useEffect(() => {
splitview.current?.setOrientation(props.orientation) splitview.current?.setOrientation(props.orientation);
splitview.current?.layout(props.size, props.orthogonalSize) splitview.current?.layout(props.size, props.orthogonalSize);
}, [props.orientation]) }, [props.orientation]);
React.useImperativeHandle( React.useImperativeHandle(
ref, ref,
() => ({ () => ({
layout: (size, orthogonalSize) => { layout: (size, orthogonalSize) => {
splitview.current?.layout(size, orthogonalSize) splitview.current?.layout(size, orthogonalSize);
}, },
}), }),
[splitview] [splitview]
) );
React.useEffect(() => { React.useEffect(() => {
splitview.current = new SplitView(containerRef.current, { splitview.current = new SplitView(containerRef.current, {
orientation: props.orientation, orientation: props.orientation,
}) });
hydrate() hydrate();
if (props.onReady) { if (props.onReady) {
props.onReady({ props.onReady({
@ -94,24 +96,24 @@ export const SplitViewComponent = React.forwardRef(
IViewWithReactComponent, IViewWithReactComponent,
'component' 'component'
> & { > & {
props?: {} props?: {};
size?: number size?: number;
index?: number index?: number;
} }
) => { ) => {
const component = props.components[options.id] const component = props.components[options.id];
splitview.current.addView( splitview.current.addView(
createView({ ...options, component }), createView({ ...options, component }),
options.size, options.size,
options.index options.index
) );
splitview.current.layout( splitview.current.layout(
props.size, props.size,
props.orthogonalSize props.orthogonalSize
) );
}, },
moveView: (from: number, to: number) => { moveView: (from: number, to: number) => {
splitview.current.moveView(from, to) splitview.current.moveView(from, to);
}, },
toJSON: () => { toJSON: () => {
return { return {
@ -132,37 +134,37 @@ export const SplitViewComponent = React.forwardRef(
{} {}
) )
), ),
} };
}, },
}, },
}) });
} }
splitview.current.layout(props.size, props.orthogonalSize) splitview.current.layout(props.size, props.orthogonalSize);
return () => { return () => {
splitview.current.dispose() splitview.current.dispose();
} };
}, []) }, []);
const createView = React.useCallback( const createView = React.useCallback(
(view: IViewWithReactComponent) => (view: IViewWithReactComponent) =>
new ReactRenderView(view, (portal) => { new ReactRenderView(view, (portal) => {
setPortals((portals) => [...portals, portal]) setPortals((portals) => [...portals, portal]);
return { return {
dispose: () => dispose: () =>
void setPortals((portals) => void setPortals((portals) =>
portals.filter((_) => _ !== portal) portals.filter((_) => _ !== portal)
), ),
} };
}), }),
[] []
) );
return ( return (
<div ref={containerRef} className="split-view-container-react"> <div ref={containerRef} className="split-view-container-react">
{portals} {portals}
</div> </div>
) );
} }
) );

View File

@ -1,7 +1,7 @@
const gulp = require('gulp') const gulp = require('gulp');
const buildfile = require('../../scripts/build') const buildfile = require('../../scripts/build');
const package = require('./package') const package = require('./package');
buildfile.build({ tsconfig: './tsconfig.build.json', package }) buildfile.build({ tsconfig: './tsconfig.build.json', package });
gulp.task('run', gulp.series(['esm', 'sass'])) gulp.task('run', gulp.series(['esm', 'sass']));

View File

@ -1,37 +1,37 @@
export function tail<T>(arr: T[]): [T[], T] { export function tail<T>(arr: T[]): [T[], T] {
if (arr.length === 0) { if (arr.length === 0) {
throw new Error('Invalid tail call') throw new Error('Invalid tail call');
} }
return [arr.slice(0, arr.length - 1), arr[arr.length - 1]] return [arr.slice(0, arr.length - 1), arr[arr.length - 1]];
} }
export function last<T>(arr: T[]): T { export function last<T>(arr: T[]): T {
return arr.length > 0 ? arr[arr.length - 1] : undefined return arr.length > 0 ? arr[arr.length - 1] : undefined;
} }
export function sequenceEquals<T>(arr1: T[], arr2: T[]) { export function sequenceEquals<T>(arr1: T[], arr2: T[]) {
if (arr1.length !== arr2.length) { if (arr1.length !== arr2.length) {
return false return false;
} }
for (let i = 0; i < arr1.length; i++) { for (let i = 0; i < arr1.length; i++) {
if (arr1[i] !== arr2[i]) { if (arr1[i] !== arr2[i]) {
return false return false;
} }
} }
return true return true;
} }
/** /**
* Pushes an element to the start of the array, if found. * Pushes an element to the start of the array, if found.
*/ */
export function pushToStart<T>(arr: T[], value: T): void { export function pushToStart<T>(arr: T[], value: T): void {
const index = arr.indexOf(value) const index = arr.indexOf(value);
if (index > -1) { if (index > -1) {
arr.splice(index, 1) arr.splice(index, 1);
arr.unshift(value) arr.unshift(value);
} }
} }
@ -39,31 +39,31 @@ export function pushToStart<T>(arr: T[], value: T): void {
* Pushes an element to the end of the array, if found. * Pushes an element to the end of the array, if found.
*/ */
export function pushToEnd<T>(arr: T[], value: T): void { export function pushToEnd<T>(arr: T[], value: T): void {
const index = arr.indexOf(value) const index = arr.indexOf(value);
if (index > -1) { if (index > -1) {
arr.splice(index, 1) arr.splice(index, 1);
arr.push(value) arr.push(value);
} }
} }
export const range = (from: number, to: number = undefined) => { export const range = (from: number, to: number = undefined) => {
const result: number[] = [] const result: number[] = [];
if (to === undefined) { if (to === undefined) {
to = from to = from;
from = 0 from = 0;
} }
if (from <= to) { if (from <= to) {
for (let i = from; i < to; i++) { for (let i = from; i < to; i++) {
result.push(i) result.push(i);
} }
} else { } else {
for (let i = from; i > to; i--) { for (let i = from; i > to; i--) {
result.push(i) result.push(i);
} }
} }
return result return result;
} };

View File

@ -1,7 +1,7 @@
export function timeoutPromise(timeout: number): Promise<void> { export function timeoutPromise(timeout: number): Promise<void> {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
resolve() resolve();
}, timeout) }, timeout);
}) });
} }

View File

@ -1,14 +1,14 @@
import { Event, Emitter, addDisposableListener } from './events' import { Event, Emitter, addDisposableListener } from './events';
import { IDisposable, CompositeDisposable } from './lifecycle' import { IDisposable, CompositeDisposable } from './lifecycle';
export function getDomNodePagePosition(domNode: HTMLElement) { export function getDomNodePagePosition(domNode: HTMLElement) {
const bb = domNode.getBoundingClientRect() const bb = domNode.getBoundingClientRect();
return { return {
left: bb.left + window.scrollX, left: bb.left + window.scrollX,
top: bb.top + window.scrollY, top: bb.top + window.scrollY,
width: bb.width, width: bb.width,
height: bb.height, height: bb.height,
} };
} }
/** /**
@ -19,10 +19,10 @@ export const scrollIntoView = (
element: HTMLElement, element: HTMLElement,
container: HTMLElement container: HTMLElement
) => { ) => {
const { inView, breachPoint } = isElementInView(element, container, true) const { inView, breachPoint } = isElementInView(element, container, true);
if (!inView) { if (!inView) {
const adder = -container.offsetTop const adder = -container.offsetTop;
const isUp = breachPoint === 'top' const isUp = breachPoint === 'top';
container.scrollTo({ container.scrollTo({
top: isUp top: isUp
? adder + element.offsetTop ? adder + element.offsetTop
@ -30,108 +30,108 @@ export const scrollIntoView = (
element.offsetTop - element.offsetTop -
container.clientHeight + container.clientHeight +
element.clientHeight, element.clientHeight,
}) });
} }
} };
export const isElementInView = ( export const isElementInView = (
element: HTMLElement, element: HTMLElement,
container: HTMLElement, container: HTMLElement,
fullyInView: boolean fullyInView: boolean
): { inView: boolean; breachPoint?: 'top' | 'bottom' } => { ): { inView: boolean; breachPoint?: 'top' | 'bottom' } => {
const containerOfftsetTop = container.offsetTop const containerOfftsetTop = container.offsetTop;
const containerTop = containerOfftsetTop + container.scrollTop const containerTop = containerOfftsetTop + container.scrollTop;
const containerBottom = const containerBottom =
containerTop + container.getBoundingClientRect().height containerTop + container.getBoundingClientRect().height;
const elementTop = element.offsetTop const elementTop = element.offsetTop;
const elementBottom = elementTop + element.getBoundingClientRect().height const elementBottom = elementTop + element.getBoundingClientRect().height;
const isAbove = fullyInView const isAbove = fullyInView
? containerTop >= elementTop ? containerTop >= elementTop
: elementTop > containerBottom : elementTop > containerBottom;
const isBelow = fullyInView const isBelow = fullyInView
? containerBottom <= elementBottom ? containerBottom <= elementBottom
: elementBottom < containerTop : elementBottom < containerTop;
if (isAbove) { if (isAbove) {
return { inView: false, breachPoint: 'top' } return { inView: false, breachPoint: 'top' };
} }
if (isBelow) { if (isBelow) {
return { inView: false, breachPoint: 'bottom' } return { inView: false, breachPoint: 'bottom' };
} }
return { inView: true } return { inView: true };
} };
export function isHTMLElement(o: any): o is HTMLElement { export function isHTMLElement(o: any): o is HTMLElement {
if (typeof HTMLElement === 'object') { if (typeof HTMLElement === 'object') {
return o instanceof HTMLElement return o instanceof HTMLElement;
} }
return ( return (
o && o &&
typeof o === 'object' && typeof o === 'object' &&
o.nodeType === 1 && o.nodeType === 1 &&
typeof o.nodeName === 'string' typeof o.nodeName === 'string'
) );
} }
export const isInTree = (element: HTMLElement, className: string) => { export const isInTree = (element: HTMLElement, className: string) => {
let _element = element let _element = element;
while (_element) { while (_element) {
if (_element.classList.contains(className)) { if (_element.classList.contains(className)) {
return true return true;
} }
_element = _element.parentElement _element = _element.parentElement;
} }
return false return false;
} };
export const removeClasses = (element: HTMLElement, ...classes: string[]) => { export const removeClasses = (element: HTMLElement, ...classes: string[]) => {
for (const classname of classes) { for (const classname of classes) {
if (element.classList.contains(classname)) { if (element.classList.contains(classname)) {
element.classList.remove(classname) element.classList.remove(classname);
} }
} }
} };
export const addClasses = (element: HTMLElement, ...classes: string[]) => { export const addClasses = (element: HTMLElement, ...classes: string[]) => {
for (const classname of classes) { for (const classname of classes) {
if (!element.classList.contains(classname)) { if (!element.classList.contains(classname)) {
element.classList.add(classname) element.classList.add(classname);
} }
} }
} };
export const toggleClass = ( export const toggleClass = (
element: HTMLElement, element: HTMLElement,
className: string, className: string,
isToggled: boolean isToggled: boolean
) => { ) => {
const hasClass = element.classList.contains(className) const hasClass = element.classList.contains(className);
if (isToggled && !hasClass) { if (isToggled && !hasClass) {
element.classList.add(className) element.classList.add(className);
} }
if (!isToggled && hasClass) { if (!isToggled && hasClass) {
element.classList.remove(className) element.classList.remove(className);
} }
} };
export function firstIndex<T>( export function firstIndex<T>(
array: T[] | ReadonlyArray<T>, array: T[] | ReadonlyArray<T>,
fn: (item: T) => boolean fn: (item: T) => boolean
): number { ): number {
for (let i = 0; i < array.length; i++) { for (let i = 0; i < array.length; i++) {
const element = array[i] const element = array[i];
if (fn(element)) { if (fn(element)) {
return i return i;
} }
} }
return -1 return -1;
} }
export function isAncestor( export function isAncestor(
@ -140,93 +140,93 @@ export function isAncestor(
): boolean { ): boolean {
while (testChild) { while (testChild) {
if (testChild === testAncestor) { if (testChild === testAncestor) {
return true return true;
} }
testChild = testChild.parentNode testChild = testChild.parentNode;
} }
return false return false;
} }
export interface IFocusTracker extends IDisposable { export interface IFocusTracker extends IDisposable {
onDidFocus: Event<void> onDidFocus: Event<void>;
onDidBlur: Event<void> onDidBlur: Event<void>;
refreshState?(): void refreshState?(): void;
} }
export function trackFocus(element: HTMLElement | Window): IFocusTracker { export function trackFocus(element: HTMLElement | Window): IFocusTracker {
return new FocusTracker(element) return new FocusTracker(element);
} }
/** /**
* Track focus on an element. Ensure tabIndex is set when an HTMLElement is not focusable by default * Track focus on an element. Ensure tabIndex is set when an HTMLElement is not focusable by default
*/ */
class FocusTracker extends CompositeDisposable implements IFocusTracker { class FocusTracker extends CompositeDisposable implements IFocusTracker {
private readonly _onDidFocus = new Emitter<void>() private readonly _onDidFocus = new Emitter<void>();
public readonly onDidFocus: Event<void> = this._onDidFocus.event public readonly onDidFocus: Event<void> = this._onDidFocus.event;
private readonly _onDidBlur = new Emitter<void>() private readonly _onDidBlur = new Emitter<void>();
public readonly onDidBlur: Event<void> = this._onDidBlur.event public readonly onDidBlur: Event<void> = this._onDidBlur.event;
private _refreshStateHandler: () => void private _refreshStateHandler: () => void;
constructor(element: HTMLElement | Window) { constructor(element: HTMLElement | Window) {
super() super();
let hasFocus = isAncestor(document.activeElement, <HTMLElement>element) let hasFocus = isAncestor(document.activeElement, <HTMLElement>element);
let loosingFocus = false let loosingFocus = false;
const onFocus = () => { const onFocus = () => {
loosingFocus = false loosingFocus = false;
if (!hasFocus) { if (!hasFocus) {
hasFocus = true hasFocus = true;
this._onDidFocus.fire() this._onDidFocus.fire();
}
} }
};
const onBlur = () => { const onBlur = () => {
if (hasFocus) { if (hasFocus) {
loosingFocus = true loosingFocus = true;
window.setTimeout(() => { window.setTimeout(() => {
if (loosingFocus) { if (loosingFocus) {
loosingFocus = false loosingFocus = false;
hasFocus = false hasFocus = false;
this._onDidBlur.fire() this._onDidBlur.fire();
}
}, 0)
} }
}, 0);
} }
};
this._refreshStateHandler = () => { this._refreshStateHandler = () => {
let currentNodeHasFocus = isAncestor( let currentNodeHasFocus = isAncestor(
document.activeElement, document.activeElement,
<HTMLElement>element <HTMLElement>element
) );
if (currentNodeHasFocus !== hasFocus) { if (currentNodeHasFocus !== hasFocus) {
if (hasFocus) { if (hasFocus) {
onBlur() onBlur();
} else { } else {
onFocus() onFocus();
}
} }
} }
};
this.addDisposables( this.addDisposables(
addDisposableListener(element, 'focus', onFocus, true) addDisposableListener(element, 'focus', onFocus, true)
) );
this.addDisposables( this.addDisposables(
addDisposableListener(element, 'blur', onBlur, true) addDisposableListener(element, 'blur', onBlur, true)
) );
} }
refreshState() { refreshState() {
this._refreshStateHandler() this._refreshStateHandler();
} }
public dispose() { public dispose() {
super.dispose() super.dispose();
this._onDidBlur.dispose() this._onDidBlur.dispose();
this._onDidFocus.dispose() this._onDidFocus.dispose();
} }
} }

View File

@ -1,37 +1,37 @@
import { IDisposable } from './lifecycle' import { IDisposable } from './lifecycle';
export interface Event<T> { export interface Event<T> {
(listener: (e: T) => any): IDisposable (listener: (e: T) => any): IDisposable;
} }
export interface EmitterOptions { export interface EmitterOptions {
emitLastValue?: boolean emitLastValue?: boolean;
} }
export namespace Event { export namespace Event {
export const any = <T>(...children: Event<T>[]): Event<T> => { export const any = <T>(...children: Event<T>[]): Event<T> => {
return (listener: (e: T) => void) => { return (listener: (e: T) => void) => {
const disposables = children.map((child) => child(listener)) const disposables = children.map((child) => child(listener));
return { return {
dispose: () => { dispose: () => {
disposables.forEach((d) => { disposables.forEach((d) => {
d.dispose() d.dispose();
}) });
}, },
} };
} };
} };
} }
// dumb event emitter with better typings than nodes event emitter // dumb event emitter with better typings than nodes event emitter
// https://github.com/microsoft/vscode/blob/master/src/vs/base/common/event.ts // https://github.com/microsoft/vscode/blob/master/src/vs/base/common/event.ts
export class Emitter<T> implements IDisposable { export class Emitter<T> implements IDisposable {
private _event: Event<T> private _event: Event<T>;
private _last: T private _last: T;
private _listeners: Array<(e: T) => any> = [] private _listeners: Array<(e: T) => any> = [];
private _disposed: boolean = false private _disposed: boolean = false;
constructor(private readonly options?: EmitterOptions) {} constructor(private readonly options?: EmitterOptions) {}
@ -39,38 +39,38 @@ export class Emitter<T> implements IDisposable {
if (!this._event) { if (!this._event) {
this._event = (listener: (e: T) => void): IDisposable => { this._event = (listener: (e: T) => void): IDisposable => {
if (this.options?.emitLastValue && this._last !== undefined) { if (this.options?.emitLastValue && this._last !== undefined) {
listener(this._last) listener(this._last);
} }
this._listeners.push(listener) this._listeners.push(listener);
return { return {
dispose: () => { dispose: () => {
const index = this._listeners.indexOf(listener) const index = this._listeners.indexOf(listener);
if (index > -1) { if (index > -1) {
this._listeners.splice(index, 1) this._listeners.splice(index, 1);
} }
}, },
};
};
} }
} return this._event;
}
return this._event
} }
public fire(e: T) { public fire(e: T) {
this._last = e this._last = e;
this._listeners.forEach((listener) => { this._listeners.forEach((listener) => {
listener(e) listener(e);
}) });
} }
public dispose() { public dispose() {
this._listeners = [] this._listeners = [];
this._disposed = true this._disposed = true;
} }
} }
export type EventHandler = HTMLElement | HTMLDocument | Window export type EventHandler = HTMLElement | HTMLDocument | Window;
export const addDisposableListener = <K extends keyof HTMLElementEventMap>( export const addDisposableListener = <K extends keyof HTMLElementEventMap>(
element: EventHandler, element: EventHandler,
@ -78,11 +78,11 @@ export const addDisposableListener = <K extends keyof HTMLElementEventMap>(
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
options?: boolean | AddEventListenerOptions options?: boolean | AddEventListenerOptions
): IDisposable => { ): IDisposable => {
element.addEventListener(type, listener, options) element.addEventListener(type, listener, options);
return { return {
dispose: () => { dispose: () => {
element.removeEventListener(type, listener) element.removeEventListener(type, listener);
}, },
} };
} };

View File

@ -1,9 +1,9 @@
export function debounce<T extends Function>(cb: T, wait: number) { export function debounce<T extends Function>(cb: T, wait: number) {
let timeout: NodeJS.Timeout let timeout: NodeJS.Timeout;
const callable = (...args: any) => { const callable = (...args: any) => {
clearTimeout(timeout) clearTimeout(timeout);
timeout = setTimeout(() => cb(...args), wait) timeout = setTimeout(() => cb(...args), wait);
} };
return <T>(<any>callable) return <T>(<any>callable);
} }

View File

@ -4,106 +4,106 @@ import {
Orientation, Orientation,
Sizing, Sizing,
LayoutPriority, LayoutPriority,
} from '../splitview/splitview' } from '../splitview/splitview';
import { Emitter, Event } from '../events' import { Emitter, Event } from '../events';
import { INodeDescriptor } from './gridview' import { INodeDescriptor } from './gridview';
import { Node } from './types' import { Node } from './types';
import { CompositeDisposable, IDisposable, Disposable } from '../lifecycle' import { CompositeDisposable, IDisposable, Disposable } from '../lifecycle';
export class BranchNode extends CompositeDisposable implements IView { export class BranchNode extends CompositeDisposable implements IView {
readonly element: HTMLElement readonly element: HTMLElement;
private splitview: SplitView private splitview: SplitView;
private _orthogonalSize: number private _orthogonalSize: number;
private _size: number private _size: number;
public readonly children: Node[] = [] public readonly children: Node[] = [];
private readonly _onDidChange = new Emitter<number | undefined>() private readonly _onDidChange = new Emitter<number | undefined>();
readonly onDidChange: Event<number | undefined> = this._onDidChange.event readonly onDidChange: Event<number | undefined> = this._onDidChange.event;
get width(): number { get width(): number {
return this.orientation === Orientation.HORIZONTAL return this.orientation === Orientation.HORIZONTAL
? this.size ? this.size
: this.orthogonalSize : this.orthogonalSize;
} }
get height(): number { get height(): number {
return this.orientation === Orientation.HORIZONTAL return this.orientation === Orientation.HORIZONTAL
? this.orthogonalSize ? this.orthogonalSize
: this.size : this.size;
} }
get minimumSize(): number { get minimumSize(): number {
return this.children.length === 0 return this.children.length === 0
? 0 ? 0
: Math.max(...this.children.map((c) => c.minimumOrthogonalSize)) : Math.max(...this.children.map((c) => c.minimumOrthogonalSize));
} }
get maximumSize(): number { get maximumSize(): number {
return Math.min(...this.children.map((c) => c.maximumOrthogonalSize)) return Math.min(...this.children.map((c) => c.maximumOrthogonalSize));
} }
get minimumOrthogonalSize(): number { get minimumOrthogonalSize(): number {
return this.splitview.minimumSize return this.splitview.minimumSize;
} }
get maximumOrthogonalSize(): number { get maximumOrthogonalSize(): number {
return this.splitview.maximumSize return this.splitview.maximumSize;
} }
get orthogonalSize() { get orthogonalSize() {
return this._orthogonalSize return this._orthogonalSize;
} }
get size() { get size() {
return this._size return this._size;
} }
get minimumWidth(): number { get minimumWidth(): number {
return this.orientation === Orientation.HORIZONTAL return this.orientation === Orientation.HORIZONTAL
? this.minimumOrthogonalSize ? this.minimumOrthogonalSize
: this.minimumSize : this.minimumSize;
} }
get snapSize() { get snapSize() {
return undefined return undefined;
} }
get minimumHeight(): number { get minimumHeight(): number {
return this.orientation === Orientation.HORIZONTAL return this.orientation === Orientation.HORIZONTAL
? this.minimumSize ? this.minimumSize
: this.minimumOrthogonalSize : this.minimumOrthogonalSize;
} }
get maximumWidth(): number { get maximumWidth(): number {
return this.orientation === Orientation.HORIZONTAL return this.orientation === Orientation.HORIZONTAL
? this.maximumOrthogonalSize ? this.maximumOrthogonalSize
: this.maximumSize : this.maximumSize;
} }
get maximumHeight(): number { get maximumHeight(): number {
return this.orientation === Orientation.HORIZONTAL return this.orientation === Orientation.HORIZONTAL
? this.maximumSize ? this.maximumSize
: this.maximumOrthogonalSize : this.maximumOrthogonalSize;
} }
get priority(): LayoutPriority { get priority(): LayoutPriority {
if (this.children.length === 0) { if (this.children.length === 0) {
return LayoutPriority.Normal return LayoutPriority.Normal;
} }
const priorities = this.children.map((c) => const priorities = this.children.map((c) =>
typeof c.priority === 'undefined' typeof c.priority === 'undefined'
? LayoutPriority.Normal ? LayoutPriority.Normal
: c.priority : c.priority
) );
if (priorities.some((p) => p === LayoutPriority.High)) { if (priorities.some((p) => p === LayoutPriority.High)) {
return LayoutPriority.High return LayoutPriority.High;
} else if (priorities.some((p) => p === LayoutPriority.Low)) { } else if (priorities.some((p) => p === LayoutPriority.Low)) {
return LayoutPriority.Low return LayoutPriority.Low;
} }
return LayoutPriority.Normal return LayoutPriority.Normal;
} }
constructor( constructor(
@ -114,133 +114,133 @@ export class BranchNode extends CompositeDisposable implements IView {
childDescriptors?: INodeDescriptor[] childDescriptors?: INodeDescriptor[]
) { ) {
super() super();
this._orthogonalSize = orthogonalSize this._orthogonalSize = orthogonalSize;
this._size = size this._size = size;
this.element = document.createElement('div') this.element = document.createElement('div');
this.element.className = 'branch-node' this.element.className = 'branch-node';
if (!childDescriptors) { if (!childDescriptors) {
this.splitview = new SplitView(this.element, { this.splitview = new SplitView(this.element, {
orientation: this.orientation, orientation: this.orientation,
proportionalLayout, proportionalLayout,
}) });
this.splitview.layout(this.size, this.orthogonalSize) this.splitview.layout(this.size, this.orthogonalSize);
} else { } else {
const descriptor = { const descriptor = {
views: childDescriptors.map((childDescriptor) => { views: childDescriptors.map((childDescriptor) => {
return { return {
view: childDescriptor.node, view: childDescriptor.node,
size: childDescriptor.node.size, size: childDescriptor.node.size,
} };
}), }),
size: this.orthogonalSize, size: this.orthogonalSize,
} };
this.children = childDescriptors.map((c) => c.node) this.children = childDescriptors.map((c) => c.node);
this.splitview = new SplitView(this.element, { this.splitview = new SplitView(this.element, {
orientation: this.orientation, orientation: this.orientation,
descriptor, descriptor,
}) });
} }
this.addDisposables( this.addDisposables(
this.splitview.onDidSashEnd(() => { this.splitview.onDidSashEnd(() => {
this._onDidChange.fire(undefined) this._onDidChange.fire(undefined);
}) })
) );
} }
moveChild(from: number, to: number): void { moveChild(from: number, to: number): void {
if (from === to) { if (from === to) {
return return;
} }
if (from < 0 || from >= this.children.length) { if (from < 0 || from >= this.children.length) {
throw new Error('Invalid from index') throw new Error('Invalid from index');
} }
if (from < to) { if (from < to) {
to-- to--;
} }
this.splitview.moveView(from, to) this.splitview.moveView(from, to);
const child = this._removeChild(from) const child = this._removeChild(from);
this._addChild(child, to) this._addChild(child, to);
} }
getChildSize(index: number): number { getChildSize(index: number): number {
if (index < 0 || index >= this.children.length) { if (index < 0 || index >= this.children.length) {
throw new Error('Invalid index') throw new Error('Invalid index');
} }
return this.splitview.getViewSize(index) return this.splitview.getViewSize(index);
} }
resizeChild(index: number, size: number): void { resizeChild(index: number, size: number): void {
if (index < 0 || index >= this.children.length) { if (index < 0 || index >= this.children.length) {
throw new Error('Invalid index') throw new Error('Invalid index');
} }
this.splitview.resizeView(index, size) this.splitview.resizeView(index, size);
} }
public layout(size: number, orthogonalSize: number) { public layout(size: number, orthogonalSize: number) {
this._size = orthogonalSize this._size = orthogonalSize;
this._orthogonalSize = size this._orthogonalSize = size;
this.splitview.layout(this.size, this.orthogonalSize) this.splitview.layout(this.size, this.orthogonalSize);
} }
public addChild(node: Node, size: number | Sizing, index: number): void { public addChild(node: Node, size: number | Sizing, index: number): void {
if (index < 0 || index > this.children.length) { if (index < 0 || index > this.children.length) {
throw new Error('Invalid index') throw new Error('Invalid index');
} }
this.splitview.addView(node, size, index) this.splitview.addView(node, size, index);
this._addChild(node, index) this._addChild(node, index);
} }
public removeChild(index: number, sizing?: Sizing) { public removeChild(index: number, sizing?: Sizing) {
if (index < 0 || index >= this.children.length) { if (index < 0 || index >= this.children.length) {
throw new Error('Invalid index') throw new Error('Invalid index');
} }
this.splitview.removeView(index, sizing) this.splitview.removeView(index, sizing);
this._removeChild(index) this._removeChild(index);
} }
private _addChild(node: Node, index: number): void { private _addChild(node: Node, index: number): void {
this.children.splice(index, 0, node) this.children.splice(index, 0, node);
this.setupChildrenEvents() this.setupChildrenEvents();
} }
private _removeChild(index: number): Node { private _removeChild(index: number): Node {
const first = index === 0 const first = index === 0;
const last = index === this.children.length - 1 const last = index === this.children.length - 1;
const [child] = this.children.splice(index, 1) const [child] = this.children.splice(index, 1);
this.setupChildrenEvents() this.setupChildrenEvents();
return child return child;
} }
private _childrenDisposable: IDisposable = Disposable.NONE private _childrenDisposable: IDisposable = Disposable.NONE;
private setupChildrenEvents() { private setupChildrenEvents() {
this._childrenDisposable.dispose() this._childrenDisposable.dispose();
this._childrenDisposable = Event.any( this._childrenDisposable = Event.any(
...this.children.map((c) => c.onDidChange) ...this.children.map((c) => c.onDidChange)
)((e) => { )((e) => {
this._onDidChange.fire(e) this._onDidChange.fire(e);
}) });
} }
public dispose() { public dispose() {
super.dispose() super.dispose();
this._childrenDisposable.dispose() this._childrenDisposable.dispose();
this.splitview.dispose() this.splitview.dispose();
this.children.forEach((child) => child.dispose()) this.children.forEach((child) => child.dispose());
} }
} }

View File

@ -1,11 +1,11 @@
import { LayoutPriority, Orientation, Sizing } from '../splitview/splitview' import { LayoutPriority, Orientation, Sizing } from '../splitview/splitview';
import { Position } from '../groupview/droptarget/droptarget' import { Position } from '../groupview/droptarget/droptarget';
import { tail } from '../array' import { tail } from '../array';
import { LeafNode } from './leafNode' import { LeafNode } from './leafNode';
import { BranchNode } from './branchNode' import { BranchNode } from './branchNode';
import { Node } from './types' import { Node } from './types';
import { Emitter, Event } from '../events' import { Emitter, Event } from '../events';
import { IDisposable } from '../lifecycle' import { IDisposable } from '../lifecycle';
function flipNode<T extends Node>( function flipNode<T extends Node>(
node: T, node: T,
@ -18,57 +18,59 @@ function flipNode<T extends Node>(
node.proportionalLayout, node.proportionalLayout,
size, size,
orthogonalSize orthogonalSize
) );
let totalSize = 0 let totalSize = 0;
for (let i = node.children.length - 1; i >= 0; i--) { for (let i = node.children.length - 1; i >= 0; i--) {
const child = node.children[i] const child = node.children[i];
const childSize = const childSize =
child instanceof BranchNode ? child.orthogonalSize : child.size child instanceof BranchNode ? child.orthogonalSize : child.size;
let newSize = let newSize =
node.size === 0 ? 0 : Math.round((size * childSize) / node.size) node.size === 0
totalSize += newSize ? 0
: Math.round((size * childSize) / node.size);
totalSize += newSize;
// The last view to add should adjust to rounding errors // The last view to add should adjust to rounding errors
if (i === 0) { if (i === 0) {
newSize += size - totalSize newSize += size - totalSize;
} }
result.addChild( result.addChild(
flipNode(child, orthogonalSize, newSize), flipNode(child, orthogonalSize, newSize),
newSize, newSize,
0 0
) );
} }
return result as T return result as T;
} else { } else {
return new LeafNode( return new LeafNode(
(node as LeafNode).view, (node as LeafNode).view,
orthogonal(node.orientation), orthogonal(node.orientation),
orthogonalSize orthogonalSize
) as T ) as T;
} }
} }
export function indexInParent(element: HTMLElement): number { export function indexInParent(element: HTMLElement): number {
const parentElement = element.parentElement const parentElement = element.parentElement;
if (!parentElement) { if (!parentElement) {
throw new Error('Invalid grid element') throw new Error('Invalid grid element');
} }
let el = parentElement.firstElementChild let el = parentElement.firstElementChild;
let index = 0 let index = 0;
while (el !== element && el !== parentElement.lastElementChild && el) { while (el !== element && el !== parentElement.lastElementChild && el) {
el = el.nextElementSibling el = el.nextElementSibling;
index++ index++;
} }
return index return index;
} }
/** /**
@ -78,19 +80,19 @@ export function indexInParent(element: HTMLElement): number {
* This will break as soon as DOM structures of the Splitview or Gridview change. * This will break as soon as DOM structures of the Splitview or Gridview change.
*/ */
export function getGridLocation(element: HTMLElement): number[] { export function getGridLocation(element: HTMLElement): number[] {
const parentElement = element.parentElement const parentElement = element.parentElement;
if (!parentElement) { if (!parentElement) {
throw new Error('Invalid grid element') throw new Error('Invalid grid element');
} }
if (/\bgrid-view\b/.test(parentElement.className)) { if (/\bgrid-view\b/.test(parentElement.className)) {
return [] return [];
} }
const index = indexInParent(parentElement) const index = indexInParent(parentElement);
const ancestor = parentElement.parentElement!.parentElement!.parentElement! const ancestor = parentElement.parentElement!.parentElement!.parentElement!;
return [...getGridLocation(ancestor), index] return [...getGridLocation(ancestor), index];
} }
export function getRelativeLocation( export function getRelativeLocation(
@ -98,30 +100,30 @@ export function getRelativeLocation(
location: number[], location: number[],
direction: Position direction: Position
): number[] { ): number[] {
const orientation = getLocationOrientation(rootOrientation, location) const orientation = getLocationOrientation(rootOrientation, location);
const directionOrientation = getDirectionOrientation(direction) const directionOrientation = getDirectionOrientation(direction);
if (orientation === directionOrientation) { if (orientation === directionOrientation) {
let [rest, index] = tail(location) let [rest, index] = tail(location);
if (direction === Position.Right || direction === Position.Bottom) { if (direction === Position.Right || direction === Position.Bottom) {
index += 1 index += 1;
} }
return [...rest, index] return [...rest, index];
} else { } else {
const index = const index =
direction === Position.Right || direction === Position.Bottom direction === Position.Right || direction === Position.Bottom
? 1 ? 1
: 0 : 0;
return [...location, index] return [...location, index];
} }
} }
export function getDirectionOrientation(direction: Position): Orientation { export function getDirectionOrientation(direction: Position): Orientation {
return direction === Position.Top || direction === Position.Bottom return direction === Position.Top || direction === Position.Bottom
? Orientation.VERTICAL ? Orientation.VERTICAL
: Orientation.HORIZONTAL : Orientation.HORIZONTAL;
} }
export function getLocationOrientation( export function getLocationOrientation(
@ -130,88 +132,88 @@ export function getLocationOrientation(
): Orientation { ): Orientation {
return location.length % 2 === 0 return location.length % 2 === 0
? orthogonal(rootOrientation) ? orthogonal(rootOrientation)
: rootOrientation : rootOrientation;
} }
export interface IGridView { export interface IGridView {
readonly element: HTMLElement readonly element: HTMLElement;
readonly minimumWidth: number readonly minimumWidth: number;
readonly maximumWidth: number readonly maximumWidth: number;
readonly minimumHeight: number readonly minimumHeight: number;
readonly maximumHeight: number readonly maximumHeight: number;
readonly priority?: LayoutPriority readonly priority?: LayoutPriority;
layout(width: number, height: number, top: number, left: number): void layout(width: number, height: number, top: number, left: number): void;
toJSON?(): object toJSON?(): object;
fromJSON?(json: object): void fromJSON?(json: object): void;
snap?: boolean snap?: boolean;
} }
const orthogonal = (orientation: Orientation) => const orthogonal = (orientation: Orientation) =>
orientation === Orientation.HORIZONTAL orientation === Orientation.HORIZONTAL
? Orientation.VERTICAL ? Orientation.VERTICAL
: Orientation.HORIZONTAL : Orientation.HORIZONTAL;
const serializeLeafNode = (node: LeafNode) => { const serializeLeafNode = (node: LeafNode) => {
const size = const size =
node.orientation === Orientation.HORIZONTAL node.orientation === Orientation.HORIZONTAL
? node.size ? node.size
: node.orthogonalSize : node.orthogonalSize;
return { return {
size: node.size, size: node.size,
data: node.view.toJSON ? node.view.toJSON() : {}, data: node.view.toJSON ? node.view.toJSON() : {},
type: 'leaf', type: 'leaf',
} };
} };
const serializeBranchNode = (node: BranchNode) => { const serializeBranchNode = (node: BranchNode) => {
const size = const size =
node.orientation === Orientation.HORIZONTAL node.orientation === Orientation.HORIZONTAL
? node.size ? node.size
: node.orthogonalSize : node.orthogonalSize;
return { return {
orientation: node.orientation, orientation: node.orientation,
size, size,
data: node.children.map((child) => { data: node.children.map((child) => {
if (child instanceof LeafNode) { if (child instanceof LeafNode) {
return serializeLeafNode(child) return serializeLeafNode(child);
} }
return serializeBranchNode(child as BranchNode) return serializeBranchNode(child as BranchNode);
}), }),
type: 'branch', type: 'branch',
} };
} };
export interface ISerializedLeafNode { export interface ISerializedLeafNode {
type: 'leaf' type: 'leaf';
data: any data: any;
size: number size: number;
visible?: boolean visible?: boolean;
} }
export interface ISerializedBranchNode { export interface ISerializedBranchNode {
type: 'branch' type: 'branch';
data: ISerializedNode[] data: ISerializedNode[];
size: number size: number;
} }
export type ISerializedNode = ISerializedLeafNode | ISerializedBranchNode export type ISerializedNode = ISerializedLeafNode | ISerializedBranchNode;
export interface INodeDescriptor { export interface INodeDescriptor {
node: Node node: Node;
visible?: boolean visible?: boolean;
} }
export interface IViewDeserializer { export interface IViewDeserializer {
fromJSON: (data: {}) => IGridView fromJSON: (data: {}) => IGridView;
} }
export class Gridview { export class Gridview {
private _root: BranchNode private _root: BranchNode;
public readonly element: HTMLElement public readonly element: HTMLElement;
private readonly _onDidChange = new Emitter<number | undefined>() private readonly _onDidChange = new Emitter<number | undefined>();
readonly onDidChange: Event<number | undefined> = this._onDidChange.event readonly onDidChange: Event<number | undefined> = this._onDidChange.event;
public serialize() { public serialize() {
return { return {
@ -219,34 +221,34 @@ export class Gridview {
height: this.height, height: this.height,
width: this.width, width: this.width,
orientation: this.orientation, orientation: this.orientation,
} };
} }
public dispose() { public dispose() {
this.root.dispose() this.root.dispose();
} }
public clear() { public clear() {
this.root.dispose() this.root.dispose();
this.root = new BranchNode( this.root = new BranchNode(
Orientation.HORIZONTAL, Orientation.HORIZONTAL,
this.proportionalLayout, this.proportionalLayout,
0, 0,
0 0
) );
} }
public deserialize(json: any, deserializer: IViewDeserializer) { public deserialize(json: any, deserializer: IViewDeserializer) {
const orientation = json.orientation const orientation = json.orientation;
const height = json.height const height = json.height;
this.orientation = orientation this.orientation = orientation;
this._deserialize( this._deserialize(
json.root as ISerializedBranchNode, json.root as ISerializedBranchNode,
orientation, orientation,
deserializer, deserializer,
height height
) );
} }
private _deserialize( private _deserialize(
@ -260,7 +262,7 @@ export class Gridview {
orientation, orientation,
deserializer, deserializer,
orthogonalSize orthogonalSize
) as BranchNode ) as BranchNode;
} }
private _deserializeNode( private _deserializeNode(
@ -269,9 +271,9 @@ export class Gridview {
deserializer: IViewDeserializer, deserializer: IViewDeserializer,
orthogonalSize: number orthogonalSize: number
): Node { ): Node {
let result: Node let result: Node;
if (node.type === 'branch') { if (node.type === 'branch') {
const serializedChildren = node.data as ISerializedNode[] const serializedChildren = node.data as ISerializedNode[];
const children = serializedChildren.map((serializedChild) => { const children = serializedChildren.map((serializedChild) => {
return { return {
node: this._deserializeNode( node: this._deserializeNode(
@ -280,8 +282,8 @@ export class Gridview {
deserializer, deserializer,
node.size node.size
), ),
} as INodeDescriptor } as INodeDescriptor;
}) });
result = new BranchNode( result = new BranchNode(
orientation, orientation,
@ -289,276 +291,280 @@ export class Gridview {
node.size, node.size,
orthogonalSize, orthogonalSize,
children children
) );
} else { } else {
result = new LeafNode( result = new LeafNode(
deserializer.fromJSON(node.data), deserializer.fromJSON(node.data),
orientation, orientation,
orthogonalSize, orthogonalSize,
node.size node.size
) );
} }
return result return result;
} }
public get orientation() { public get orientation() {
return this.root.orientation return this.root.orientation;
} }
public set orientation(orientation: Orientation) { public set orientation(orientation: Orientation) {
if (this._root.orientation === orientation) { if (this._root.orientation === orientation) {
return return;
} }
const { size, orthogonalSize } = this._root const { size, orthogonalSize } = this._root;
this.root = flipNode(this._root, orthogonalSize, size) this.root = flipNode(this._root, orthogonalSize, size);
this.root.layout(size, orthogonalSize) this.root.layout(size, orthogonalSize);
} }
private get root(): BranchNode { private get root(): BranchNode {
return this._root return this._root;
} }
private disposable: IDisposable private disposable: IDisposable;
private set root(root: BranchNode) { private set root(root: BranchNode) {
const oldRoot = this._root const oldRoot = this._root;
if (oldRoot) { if (oldRoot) {
this.disposable?.dispose() this.disposable?.dispose();
oldRoot.dispose() oldRoot.dispose();
this.element.removeChild(oldRoot.element) this.element.removeChild(oldRoot.element);
} }
this._root = root this._root = root;
this.disposable = this._root.onDidChange((e) => { this.disposable = this._root.onDidChange((e) => {
this._onDidChange.fire(e) this._onDidChange.fire(e);
}) });
this.element.appendChild(root.element) this.element.appendChild(root.element);
} }
public next(location: number[]) { public next(location: number[]) {
return this.progmaticSelect(location) return this.progmaticSelect(location);
} }
public preivous(location: number[]) { public preivous(location: number[]) {
return this.progmaticSelect(location, true) return this.progmaticSelect(location, true);
} }
private progmaticSelect(location: number[], reverse = false) { private progmaticSelect(location: number[], reverse = false) {
const [rest, index] = tail(location) const [rest, index] = tail(location);
const [path, node] = this.getNode(location) const [path, node] = this.getNode(location);
if (!(node instanceof LeafNode)) { if (!(node instanceof LeafNode)) {
throw new Error('invalid location') throw new Error('invalid location');
} }
const findLeaf = (node: Node, last: boolean): LeafNode => { const findLeaf = (node: Node, last: boolean): LeafNode => {
if (node instanceof LeafNode) { if (node instanceof LeafNode) {
return node return node;
} }
if (node instanceof BranchNode) { if (node instanceof BranchNode) {
return findLeaf( return findLeaf(
node.children[last ? node.children.length - 1 : 0], node.children[last ? node.children.length - 1 : 0],
last last
) );
}
throw new Error('invalid node')
} }
throw new Error('invalid node');
};
for (let i = path.length - 1; i > -1; i--) { for (let i = path.length - 1; i > -1; i--) {
const n = path[i] const n = path[i];
const l = location[i] || 0 const l = location[i] || 0;
const canProgressInCurrentLevel = reverse const canProgressInCurrentLevel = reverse
? l - 1 > -1 ? l - 1 > -1
: l + 1 < n.children.length : l + 1 < n.children.length;
if (canProgressInCurrentLevel) { if (canProgressInCurrentLevel) {
return findLeaf(n.children[reverse ? l - 1 : l + 1], reverse) return findLeaf(n.children[reverse ? l - 1 : l + 1], reverse);
} }
} }
return findLeaf(this.root, reverse) return findLeaf(this.root, reverse);
} }
get width(): number { get width(): number {
return this.root.width return this.root.width;
} }
get height(): number { get height(): number {
return this.root.height return this.root.height;
} }
get minimumWidth(): number { get minimumWidth(): number {
return this.root.minimumWidth return this.root.minimumWidth;
} }
get minimumHeight(): number { get minimumHeight(): number {
return this.root.minimumHeight return this.root.minimumHeight;
} }
get maximumWidth(): number { get maximumWidth(): number {
return this.root.maximumHeight return this.root.maximumHeight;
} }
get maximumHeight(): number { get maximumHeight(): number {
return this.root.maximumHeight return this.root.maximumHeight;
} }
constructor(readonly proportionalLayout: boolean) { constructor(readonly proportionalLayout: boolean) {
this.element = document.createElement('div') this.element = document.createElement('div');
this.element.className = 'grid-view' this.element.className = 'grid-view';
this.root = new BranchNode( this.root = new BranchNode(
Orientation.HORIZONTAL, Orientation.HORIZONTAL,
proportionalLayout, proportionalLayout,
0, 0,
0 0
) );
this.element.appendChild(this.root.element) this.element.appendChild(this.root.element);
} }
public moveView(parentLocation: number[], from: number, to: number): void { public moveView(parentLocation: number[], from: number, to: number): void {
const [, parent] = this.getNode(parentLocation) const [, parent] = this.getNode(parentLocation);
if (!(parent instanceof BranchNode)) { if (!(parent instanceof BranchNode)) {
throw new Error('Invalid location') throw new Error('Invalid location');
} }
parent.moveChild(from, to) parent.moveChild(from, to);
} }
public addView(view: IGridView, size: number | Sizing, location: number[]) { public addView(view: IGridView, size: number | Sizing, location: number[]) {
const [rest, index] = tail(location) const [rest, index] = tail(location);
const [pathToParent, parent] = this.getNode(rest) const [pathToParent, parent] = this.getNode(rest);
if (parent instanceof BranchNode) { if (parent instanceof BranchNode) {
const node = new LeafNode( const node = new LeafNode(
view, view,
orthogonal(parent.orientation), orthogonal(parent.orientation),
parent.orthogonalSize parent.orthogonalSize
) );
parent.addChild(node, size, index) parent.addChild(node, size, index);
} else { } else {
const [grandParent, ..._] = [...pathToParent].reverse() const [grandParent, ..._] = [...pathToParent].reverse();
const [parentIndex, ...__] = [...rest].reverse() const [parentIndex, ...__] = [...rest].reverse();
let newSiblingSize: number | Sizing = 0 let newSiblingSize: number | Sizing = 0;
grandParent.removeChild(parentIndex) grandParent.removeChild(parentIndex);
const newParent = new BranchNode( const newParent = new BranchNode(
parent.orientation, parent.orientation,
this.proportionalLayout, this.proportionalLayout,
parent.size, parent.size,
parent.orthogonalSize parent.orthogonalSize
) );
grandParent.addChild(newParent, parent.size, parentIndex) grandParent.addChild(newParent, parent.size, parentIndex);
const newSibling = new LeafNode( const newSibling = new LeafNode(
parent.view, parent.view,
grandParent.orientation, grandParent.orientation,
parent.size parent.size
) );
newParent.addChild(newSibling, newSiblingSize, 0) newParent.addChild(newSibling, newSiblingSize, 0);
if (typeof size !== 'number' && size.type === 'split') { if (typeof size !== 'number' && size.type === 'split') {
size = { type: 'split', index: 0 } size = { type: 'split', index: 0 };
} }
const node = new LeafNode( const node = new LeafNode(
view, view,
grandParent.orientation, grandParent.orientation,
parent.size parent.size
) );
newParent.addChild(node, size, index) newParent.addChild(node, size, index);
} }
} }
public remove(view: IGridView, sizing?: Sizing) { public remove(view: IGridView, sizing?: Sizing) {
const location = getGridLocation(view.element) const location = getGridLocation(view.element);
return this.removeView(location, sizing) return this.removeView(location, sizing);
} }
removeView(location: number[], sizing?: Sizing): IGridView { removeView(location: number[], sizing?: Sizing): IGridView {
const [rest, index] = tail(location) const [rest, index] = tail(location);
const [pathToParent, parent] = this.getNode(rest) const [pathToParent, parent] = this.getNode(rest);
if (!(parent instanceof BranchNode)) { if (!(parent instanceof BranchNode)) {
throw new Error('Invalid location') throw new Error('Invalid location');
} }
const node = parent.children[index] const node = parent.children[index];
if (!(node instanceof LeafNode)) { if (!(node instanceof LeafNode)) {
throw new Error('Invalid location') throw new Error('Invalid location');
} }
parent.removeChild(index, sizing) parent.removeChild(index, sizing);
if (parent.children.length === 0) { if (parent.children.length === 0) {
throw new Error('Invalid grid state') throw new Error('Invalid grid state');
} }
if (parent.children.length > 1) { if (parent.children.length > 1) {
return node.view return node.view;
} }
if (pathToParent.length === 0) { if (pathToParent.length === 0) {
// parent is root // parent is root
const sibling = parent.children[0] const sibling = parent.children[0];
if (sibling instanceof LeafNode) { if (sibling instanceof LeafNode) {
return node.view return node.view;
} }
// we must promote sibling to be the new root // we must promote sibling to be the new root
parent.removeChild(0, sizing) parent.removeChild(0, sizing);
this.root = sibling this.root = sibling;
return node.view return node.view;
} }
const [grandParent, ..._] = [...pathToParent].reverse() const [grandParent, ..._] = [...pathToParent].reverse();
const [parentIndex, ...__] = [...rest].reverse() const [parentIndex, ...__] = [...rest].reverse();
const sibling = parent.children[0] const sibling = parent.children[0];
parent.removeChild(0, sizing) parent.removeChild(0, sizing);
const sizes = grandParent.children.map((_, i) => const sizes = grandParent.children.map((_, i) =>
grandParent.getChildSize(i) grandParent.getChildSize(i)
) );
grandParent.removeChild(parentIndex, sizing) grandParent.removeChild(parentIndex, sizing);
if (sibling instanceof BranchNode) { if (sibling instanceof BranchNode) {
sizes.splice(parentIndex, 1, ...sibling.children.map((c) => c.size)) sizes.splice(
parentIndex,
1,
...sibling.children.map((c) => c.size)
);
for (let i = 0; i < sibling.children.length; i++) { for (let i = 0; i < sibling.children.length; i++) {
const child = sibling.children[i] const child = sibling.children[i];
grandParent.addChild(child, child.size, parentIndex + i) grandParent.addChild(child, child.size, parentIndex + i);
} }
} else { } else {
const newSibling = new LeafNode( const newSibling = new LeafNode(
sibling.view, sibling.view,
orthogonal(sibling.orientation), orthogonal(sibling.orientation),
sibling.size sibling.size
) );
grandParent.addChild( grandParent.addChild(
newSibling, newSibling,
sibling.orthogonalSize, sibling.orthogonalSize,
parentIndex parentIndex
) );
} }
for (let i = 0; i < sizes.length; i++) { for (let i = 0; i < sizes.length; i++) {
grandParent.resizeChild(i, sizes[i]) grandParent.resizeChild(i, sizes[i]);
} }
return node.view return node.view;
} }
public layout(width: number, height: number) { public layout(width: number, height: number) {
const [size, orthogonalSize] = const [size, orthogonalSize] =
this.root.orientation === Orientation.HORIZONTAL this.root.orientation === Orientation.HORIZONTAL
? [height, width] ? [height, width]
: [width, height] : [width, height];
this.root.layout(size, orthogonalSize) this.root.layout(size, orthogonalSize);
} }
private getNode( private getNode(
@ -567,22 +573,22 @@ export class Gridview {
path: BranchNode[] = [] path: BranchNode[] = []
): [BranchNode[], Node] { ): [BranchNode[], Node] {
if (location.length === 0) { if (location.length === 0) {
return [path, node] return [path, node];
} }
if (!(node instanceof BranchNode)) { if (!(node instanceof BranchNode)) {
throw new Error('Invalid location') throw new Error('Invalid location');
} }
const [index, ...rest] = location const [index, ...rest] = location;
if (index < 0 || index >= node.children.length) { if (index < 0 || index >= node.children.length) {
throw new Error('Invalid location') throw new Error('Invalid location');
} }
const child = node.children[index] const child = node.children[index];
path.push(node) path.push(node);
return this.getNode(rest, child, path) return this.getNode(rest, child, path);
} }
} }

View File

@ -1,73 +1,73 @@
import { IView, LayoutPriority, Orientation } from '../splitview/splitview' import { IView, LayoutPriority, Orientation } from '../splitview/splitview';
import { Emitter, Event } from '../events' import { Emitter, Event } from '../events';
import { IGridView } from './gridview' import { IGridView } from './gridview';
export class LeafNode implements IView { export class LeafNode implements IView {
private readonly _onDidChange = new Emitter<number | undefined>() private readonly _onDidChange = new Emitter<number | undefined>();
readonly onDidChange: Event<number | undefined> = this._onDidChange.event readonly onDidChange: Event<number | undefined> = this._onDidChange.event;
private _size: number private _size: number;
private _orthogonalSize: number private _orthogonalSize: number;
public dispose() {} public dispose() {}
private get minimumWidth(): number { private get minimumWidth(): number {
return this.view.minimumWidth return this.view.minimumWidth;
} }
private get maximumWidth(): number { private get maximumWidth(): number {
return this.view.maximumWidth return this.view.maximumWidth;
} }
private get minimumHeight(): number { private get minimumHeight(): number {
return this.view.minimumHeight return this.view.minimumHeight;
} }
private get maximumHeight(): number { private get maximumHeight(): number {
return this.view.maximumHeight return this.view.maximumHeight;
} }
get priority(): LayoutPriority | undefined { get priority(): LayoutPriority | undefined {
return this.view.priority return this.view.priority;
} }
get snapSize() { get snapSize() {
return this.view.snap ? this.minimumSize / 2 : undefined return this.view.snap ? this.minimumSize / 2 : undefined;
} }
get minimumSize(): number { get minimumSize(): number {
return this.orientation === Orientation.HORIZONTAL return this.orientation === Orientation.HORIZONTAL
? this.minimumHeight ? this.minimumHeight
: this.minimumWidth : this.minimumWidth;
} }
get maximumSize(): number { get maximumSize(): number {
return this.orientation === Orientation.HORIZONTAL return this.orientation === Orientation.HORIZONTAL
? this.maximumHeight ? this.maximumHeight
: this.maximumWidth : this.maximumWidth;
} }
get minimumOrthogonalSize(): number { get minimumOrthogonalSize(): number {
return this.orientation === Orientation.HORIZONTAL return this.orientation === Orientation.HORIZONTAL
? this.minimumWidth ? this.minimumWidth
: this.minimumHeight : this.minimumHeight;
} }
get maximumOrthogonalSize(): number { get maximumOrthogonalSize(): number {
return this.orientation === Orientation.HORIZONTAL return this.orientation === Orientation.HORIZONTAL
? this.maximumWidth ? this.maximumWidth
: this.maximumHeight : this.maximumHeight;
} }
get orthogonalSize() { get orthogonalSize() {
return this._orthogonalSize return this._orthogonalSize;
} }
get size() { get size() {
return this._size return this._size;
} }
get element() { get element() {
return this.view.element return this.view.element;
} }
constructor( constructor(
@ -76,19 +76,19 @@ export class LeafNode implements IView {
orthogonalSize: number, orthogonalSize: number,
size: number = 0 size: number = 0
) { ) {
this._orthogonalSize = orthogonalSize this._orthogonalSize = orthogonalSize;
this._size = size this._size = size;
} }
public layout(size: number, orthogonalSize: number) { public layout(size: number, orthogonalSize: number) {
this._size = size this._size = size;
this._orthogonalSize = orthogonalSize this._orthogonalSize = orthogonalSize;
const [width, height] = const [width, height] =
this.orientation === Orientation.HORIZONTAL this.orientation === Orientation.HORIZONTAL
? [orthogonalSize, size] ? [orthogonalSize, size]
: [size, orthogonalSize] : [size, orthogonalSize];
this.view.layout(width, height, 0, 0) this.view.layout(width, height, 0, 0);
} }
} }

View File

@ -1,4 +1,4 @@
import { BranchNode } from './branchNode' import { BranchNode } from './branchNode';
import { LeafNode } from './leafNode' import { LeafNode } from './leafNode';
export type Node = BranchNode | LeafNode export type Node = BranchNode | LeafNode;

View File

@ -1,22 +1,22 @@
export class ActionContainer { export class ActionContainer {
private _element: HTMLElement private _element: HTMLElement;
private _list: HTMLElement private _list: HTMLElement;
get element() { get element() {
return this._element return this._element;
} }
constructor() { constructor() {
this._element = document.createElement('div') this._element = document.createElement('div');
this._element.className = 'actions-bar' this._element.className = 'actions-bar';
this._list = document.createElement('ul') this._list = document.createElement('ul');
this._list.className = 'actions-container' this._list.className = 'actions-container';
this._element.appendChild(this._list) this._element.appendChild(this._list);
} }
public add(element: HTMLElement) { public add(element: HTMLElement) {
this._list.appendChild(element) this._list.appendChild(element);
} }
} }

View File

@ -1,10 +1,10 @@
import { PanelOptions } from '../../layout/options' import { PanelOptions } from '../../layout/options';
export const DATA_KEY = 'splitview/transfer' export const DATA_KEY = 'splitview/transfer';
export const isPanelTransferEvent = (event: DragEvent) => { export const isPanelTransferEvent = (event: DragEvent) => {
return event.dataTransfer.types.includes(DATA_KEY) return event.dataTransfer.types.includes(DATA_KEY);
} };
export enum DragType { export enum DragType {
ITEM = 'group_drag', ITEM = 'group_drag',
@ -12,113 +12,113 @@ export enum DragType {
} }
export interface DragItem { export interface DragItem {
itemId: string itemId: string;
groupId: string groupId: string;
} }
export interface ExternalDragItem extends PanelOptions {} export interface ExternalDragItem extends PanelOptions {}
export type DataObject = DragItem | ExternalDragItem export type DataObject = DragItem | ExternalDragItem;
/** /**
* Determine whether this data belong to that of an event that was started by * Determine whether this data belong to that of an event that was started by
* dragging a tab component * dragging a tab component
*/ */
export const isTabDragEvent = (data: any): data is DragItem => { export const isTabDragEvent = (data: any): data is DragItem => {
return data.type === DragType.ITEM return data.type === DragType.ITEM;
} };
/** /**
* Determine whether this data belong to that of an event that was started by * Determine whether this data belong to that of an event that was started by
* a custom drag-enable component * a custom drag-enable component
*/ */
export const isCustomDragEvent = (data: any): data is ExternalDragItem => { export const isCustomDragEvent = (data: any): data is ExternalDragItem => {
return data.type === DragType.EXTERNAL return data.type === DragType.EXTERNAL;
} };
export const extractData = (event: DragEvent): DataObject => { export const extractData = (event: DragEvent): DataObject => {
const data = JSON.parse(event.dataTransfer.getData(DATA_KEY)) const data = JSON.parse(event.dataTransfer.getData(DATA_KEY));
if (!data) { if (!data) {
console.warn(`[dragEvent] ${DATA_KEY} data is missing`) console.warn(`[dragEvent] ${DATA_KEY} data is missing`);
} }
if (typeof data.type !== 'string') { if (typeof data.type !== 'string') {
console.warn(`[dragEvent] invalid type ${data.type}`) console.warn(`[dragEvent] invalid type ${data.type}`);
} }
return data return data;
} };
class DataTransfer { class DataTransfer {
private map = new Map<string, string>() private map = new Map<string, string>();
public setData(format: string, data: string) { public setData(format: string, data: string) {
this.map.set(format, data) this.map.set(format, data);
} }
public getData(format: string) { public getData(format: string) {
const data = this.map.get(format) const data = this.map.get(format);
return data return data;
} }
public has(format: string) { public has(format: string) {
return this.map.has(format) return this.map.has(format);
} }
public removeData(format: string) { public removeData(format: string) {
const data = this.getData(format) const data = this.getData(format);
this.map.delete(format) this.map.delete(format);
return data return data;
} }
get size() { get size() {
return this.map.size return this.map.size;
} }
} }
export const DataTransferSingleton = new DataTransfer() export const DataTransferSingleton = new DataTransfer();
/** /**
* A singleton to store transfer data during drag & drop operations that are only valid within the application. * A singleton to store transfer data during drag & drop operations that are only valid within the application.
*/ */
export class LocalSelectionTransfer<T> { export class LocalSelectionTransfer<T> {
private static readonly INSTANCE = new LocalSelectionTransfer() private static readonly INSTANCE = new LocalSelectionTransfer();
private data?: T[] private data?: T[];
private proto?: T private proto?: T;
private constructor() { private constructor() {
// protect against external instantiation // protect against external instantiation
} }
static getInstance<T>(): LocalSelectionTransfer<T> { static getInstance<T>(): LocalSelectionTransfer<T> {
return LocalSelectionTransfer.INSTANCE as LocalSelectionTransfer<T> return LocalSelectionTransfer.INSTANCE as LocalSelectionTransfer<T>;
} }
hasData(proto: T): boolean { hasData(proto: T): boolean {
return proto && proto === this.proto return proto && proto === this.proto;
} }
clearData(proto: T): void { clearData(proto: T): void {
if (this.hasData(proto)) { if (this.hasData(proto)) {
this.proto = undefined this.proto = undefined;
this.data = undefined this.data = undefined;
} }
} }
getData(proto: T): T[] | undefined { getData(proto: T): T[] | undefined {
if (this.hasData(proto)) { if (this.hasData(proto)) {
return this.data return this.data;
} }
return undefined return undefined;
} }
setData(data: T[], proto: T): void { setData(data: T[], proto: T): void {
if (proto) { if (proto) {
this.data = data this.data = data;
this.proto = proto this.proto = proto;
} }
} }
} }

View File

@ -1,5 +1,5 @@
import { Emitter, Event } from '../../events' import { Emitter, Event } from '../../events';
import { DataTransferSingleton } from './dataTransfer' import { DataTransferSingleton } from './dataTransfer';
export enum Position { export enum Position {
Top = 'Top', Top = 'Top',
@ -10,19 +10,19 @@ export enum Position {
} }
export interface DroptargetEvent { export interface DroptargetEvent {
position: Position position: Position;
event: DragEvent event: DragEvent;
} }
const HAS_PROCESSED_KEY = '__drop_target_processed__' const HAS_PROCESSED_KEY = '__drop_target_processed__';
export const hasProcessed = (event: DragEvent) => export const hasProcessed = (event: DragEvent) =>
!!(event as any)[HAS_PROCESSED_KEY] !!(event as any)[HAS_PROCESSED_KEY];
// tagging events as processed is better than calling .stopPropagation() which is the root of all evil // tagging events as processed is better than calling .stopPropagation() which is the root of all evil
const setEventAsProcessed = (event: DragEvent) => { const setEventAsProcessed = (event: DragEvent) => {
event[HAS_PROCESSED_KEY] = true event[HAS_PROCESSED_KEY] = true;
} };
const toggleClassName = ( const toggleClassName = (
element: HTMLElement, element: HTMLElement,
@ -30,36 +30,36 @@ const toggleClassName = (
addOrRemove: boolean addOrRemove: boolean
) => { ) => {
if (addOrRemove && !element.classList.contains(className)) { if (addOrRemove && !element.classList.contains(className)) {
element.classList.add(className) element.classList.add(className);
} else if (!addOrRemove && element.classList.contains(className)) { } else if (!addOrRemove && element.classList.contains(className)) {
element.classList.remove(className) element.classList.remove(className);
} }
} };
export class Droptarget { export class Droptarget {
private target: HTMLElement private target: HTMLElement;
private overlay: HTMLElement private overlay: HTMLElement;
private state: Position | undefined private state: Position | undefined;
private readonly _onDidChange = new Emitter<DroptargetEvent>() private readonly _onDidChange = new Emitter<DroptargetEvent>();
readonly onDidChange: Event<DroptargetEvent> = this._onDidChange.event readonly onDidChange: Event<DroptargetEvent> = this._onDidChange.event;
constructor( constructor(
private element: HTMLElement, private element: HTMLElement,
private options: { private options: {
isDisabled: () => boolean isDisabled: () => boolean;
isDirectional: boolean isDirectional: boolean;
id: string id: string;
enableExternalDragEvents?: boolean enableExternalDragEvents?: boolean;
} }
) { ) {
this.element.addEventListener('dragenter', this.onDragEnter) this.element.addEventListener('dragenter', this.onDragEnter);
} }
public dispose() { public dispose() {
this._onDidChange.dispose() this._onDidChange.dispose();
this.removeDropTarget() this.removeDropTarget();
this.element.removeEventListener('dragenter', this.onDragEnter) this.element.removeEventListener('dragenter', this.onDragEnter);
} }
private onDragEnter = (event: DragEvent) => { private onDragEnter = (event: DragEvent) => {
@ -67,104 +67,104 @@ export class Droptarget {
!this.options.enableExternalDragEvents && !this.options.enableExternalDragEvents &&
!DataTransferSingleton.has(this.options.id) !DataTransferSingleton.has(this.options.id)
) { ) {
console.debug('[droptarget] invalid event') console.debug('[droptarget] invalid event');
return return;
} }
if (this.options.isDisabled()) { if (this.options.isDisabled()) {
return return;
} }
event.preventDefault() event.preventDefault();
if (!this.target) { if (!this.target) {
console.debug('[droptarget] created') console.debug('[droptarget] created');
this.target = document.createElement('div') this.target = document.createElement('div');
this.target.className = 'drop-target-dropzone' this.target.className = 'drop-target-dropzone';
this.overlay = document.createElement('div') this.overlay = document.createElement('div');
this.overlay.className = 'drop-target-selection' this.overlay.className = 'drop-target-selection';
// //
this.target.addEventListener('dragover', this.onDragOver) this.target.addEventListener('dragover', this.onDragOver);
this.target.addEventListener('dragleave', this.onDragLeave) this.target.addEventListener('dragleave', this.onDragLeave);
this.target.addEventListener('drop', this.onDrop) this.target.addEventListener('drop', this.onDrop);
this.target.appendChild(this.overlay) this.target.appendChild(this.overlay);
this.element.classList.add('drop-target') this.element.classList.add('drop-target');
this.element.append(this.target) this.element.append(this.target);
}
} }
};
private onDrop = (event: DragEvent) => { private onDrop = (event: DragEvent) => {
if ( if (
!this.options.enableExternalDragEvents && !this.options.enableExternalDragEvents &&
!DataTransferSingleton.has(this.options.id) !DataTransferSingleton.has(this.options.id)
) { ) {
console.debug('[dragtarget] invalid') console.debug('[dragtarget] invalid');
return return;
} }
console.debug('[dragtarget] drop') console.debug('[dragtarget] drop');
this.removeDropTarget() this.removeDropTarget();
if (!hasProcessed(event)) { if (!hasProcessed(event)) {
this._onDidChange.fire({ position: this.state, event }) this._onDidChange.fire({ position: this.state, event });
} else { } else {
console.debug('[dragtarget] already processed') console.debug('[dragtarget] already processed');
} }
this.state = undefined this.state = undefined;
setEventAsProcessed(event) setEventAsProcessed(event);
} };
private onDragOver = (event: DragEvent) => { private onDragOver = (event: DragEvent) => {
event.preventDefault() event.preventDefault();
if (!this.options.isDirectional) { if (!this.options.isDirectional) {
return return;
} }
const width = this.target.clientWidth const width = this.target.clientWidth;
const height = this.target.clientHeight const height = this.target.clientHeight;
const x = event.offsetX const x = event.offsetX;
const y = event.offsetY const y = event.offsetY;
const xp = (100 * x) / width const xp = (100 * x) / width;
const yp = (100 * y) / height const yp = (100 * y) / height;
const isRight = xp > 80 const isRight = xp > 80;
const isLeft = xp < 20 const isLeft = xp < 20;
const isTop = !isRight && !isLeft && yp < 20 const isTop = !isRight && !isLeft && yp < 20;
const isBottom = !isRight && !isLeft && yp > 80 const isBottom = !isRight && !isLeft && yp > 80;
toggleClassName(this.overlay, 'right', isRight) toggleClassName(this.overlay, 'right', isRight);
toggleClassName(this.overlay, 'left', isLeft) toggleClassName(this.overlay, 'left', isLeft);
toggleClassName(this.overlay, 'top', isTop) toggleClassName(this.overlay, 'top', isTop);
toggleClassName(this.overlay, 'bottom', isBottom) toggleClassName(this.overlay, 'bottom', isBottom);
if (isRight) { if (isRight) {
this.state = Position.Right this.state = Position.Right;
} else if (isLeft) { } else if (isLeft) {
this.state = Position.Left this.state = Position.Left;
} else if (isTop) { } else if (isTop) {
this.state = Position.Top this.state = Position.Top;
} else if (isBottom) { } else if (isBottom) {
this.state = Position.Bottom this.state = Position.Bottom;
} else { } else {
this.state = Position.Center this.state = Position.Center;
}
} }
};
private onDragLeave = (event: DragEvent) => { private onDragLeave = (event: DragEvent) => {
console.debug('[droptarget] leave') console.debug('[droptarget] leave');
this.removeDropTarget() this.removeDropTarget();
} };
private removeDropTarget() { private removeDropTarget() {
if (this.target) { if (this.target) {
this.target.removeEventListener('dragover', this.onDragOver) this.target.removeEventListener('dragover', this.onDragOver);
this.target.removeEventListener('dragleave', this.onDragLeave) this.target.removeEventListener('dragleave', this.onDragLeave);
this.target.removeEventListener('drop', this.onDrop) this.target.removeEventListener('drop', this.onDrop);
this.element.removeChild(this.target) this.element.removeChild(this.target);
this.target = undefined this.target = undefined;
this.element.classList.remove('drop-target') this.element.classList.remove('drop-target');
} }
} }
} }

View File

@ -1,9 +1,9 @@
import { DroptargetEvent } from './droptarget/droptarget' import { DroptargetEvent } from './droptarget/droptarget';
import { IGroupPanel } from './panel/types' import { IGroupPanel } from './panel/types';
export interface TabDropEvent { export interface TabDropEvent {
event: DroptargetEvent event: DroptargetEvent;
index?: number index?: number;
} }
export enum MouseEventKind { export enum MouseEventKind {
@ -12,8 +12,8 @@ export enum MouseEventKind {
} }
export interface LayoutMouseEvent { export interface LayoutMouseEvent {
kind: MouseEventKind kind: MouseEventKind;
event: MouseEvent event: MouseEvent;
panel?: IGroupPanel panel?: IGroupPanel;
tab?: boolean tab?: boolean;
} }

View File

@ -1,20 +1,20 @@
import { IDisposable, CompositeDisposable, Disposable } from '../lifecycle' import { IDisposable, CompositeDisposable, Disposable } from '../lifecycle';
import { ITabContainer, TabContainer } from './titlebar/tabContainer' import { ITabContainer, TabContainer } from './titlebar/tabContainer';
import { IContentContainer, ContentContainer } from './panel/content/content' import { IContentContainer, ContentContainer } from './panel/content/content';
import { IGridView } from '../gridview/gridview' import { Position, Droptarget, DroptargetEvent } from './droptarget/droptarget';
import { Position, Droptarget, DroptargetEvent } from './droptarget/droptarget' import { Event, Emitter, addDisposableListener } from '../events';
import { Event, Emitter, addDisposableListener } from '../events' import { IComponentGridview, 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 { IGroupPanel } from './panel/types';
import { IGroupPanel } from './panel/types' import { timeoutPromise } from '../async';
import { timeoutPromise } from '../async'
import { import {
extractData, extractData,
isTabDragEvent, isTabDragEvent,
isCustomDragEvent, isCustomDragEvent,
isPanelTransferEvent, isPanelTransferEvent,
} from './droptarget/dataTransfer' } from './droptarget/dataTransfer';
import { IBaseGridView } from '../layout/baseGrid';
export const enum GroupChangeKind { export const enum GroupChangeKind {
GROUP_ACTIVE = 'GROUP_ACTIVE', GROUP_ACTIVE = 'GROUP_ACTIVE',
@ -39,221 +39,219 @@ export const enum GroupChangeKind {
} }
export interface IGroupItem { export interface IGroupItem {
id: string id: string;
header: { element: HTMLElement } header: { element: HTMLElement };
body: { element: HTMLElement } body: { element: HTMLElement };
} }
interface GroupMoveEvent { interface GroupMoveEvent {
groupId: string groupId: string;
itemId: string itemId: string;
target: Position target: Position;
index?: number index?: number;
} }
export interface GroupOptions { export interface GroupOptions {
panels: IGroupPanel[] panels: IGroupPanel[];
activePanel?: IGroupPanel activePanel?: IGroupPanel;
} }
export interface GroupChangeEvent { export interface GroupChangeEvent {
kind: GroupChangeKind kind: GroupChangeKind;
panel?: IGroupPanel panel?: IGroupPanel;
} }
export interface IGroupview extends IDisposable, IGridView { export interface IGroupview extends IDisposable, IBaseGridView {
id: string size: number;
size: number panels: IGroupPanel[];
panels: IGroupPanel[] tabHeight: number;
tabHeight: number
setActive: (isActive: boolean) => void
// state // state
isPanelActive: (panel: IGroupPanel) => boolean isPanelActive: (panel: IGroupPanel) => boolean;
isActive: boolean isActive: boolean;
activePanel: IGroupPanel activePanel: IGroupPanel;
indexOf(panel: IGroupPanel): number indexOf(panel: IGroupPanel): number;
// panel lifecycle // panel lifecycle
openPanel(panel: IGroupPanel, index?: number): void openPanel(panel: IGroupPanel, index?: number): void;
closePanel(panel: IGroupPanel): Promise<boolean> closePanel(panel: IGroupPanel): Promise<boolean>;
closeAllPanels(): Promise<boolean> closeAllPanels(): Promise<boolean>;
containsPanel(panel: IGroupPanel): boolean containsPanel(panel: IGroupPanel): boolean;
removePanel: (panelOrId: IGroupPanel | string) => IGroupPanel removePanel: (panelOrId: IGroupPanel | string) => IGroupPanel;
// events // events
onDidGroupChange: Event<{ kind: GroupChangeKind }> onDidGroupChange: Event<{ kind: GroupChangeKind }>;
onMove: Event<GroupMoveEvent> onMove: Event<GroupMoveEvent>;
// //
startActiveDrag(panel: IGroupPanel): IDisposable startActiveDrag(panel: IGroupPanel): IDisposable;
// //
moveToNext(options?: { panel?: IGroupPanel; suppressRoll?: boolean }): void moveToNext(options?: { panel?: IGroupPanel; suppressRoll?: boolean }): void;
moveToPrevious(options?: { moveToPrevious(options?: {
panel?: IGroupPanel panel?: IGroupPanel;
suppressRoll?: boolean suppressRoll?: boolean;
}): void }): void;
} }
export interface GroupDropEvent { export interface GroupDropEvent {
event: DragEvent event: DragEvent;
target: Position target: Position;
index?: number index?: number;
} }
export class Groupview extends CompositeDisposable implements IGroupview { export class Groupview extends CompositeDisposable implements IGroupview {
private _element: HTMLElement private _element: HTMLElement;
private tabContainer: ITabContainer private tabContainer: ITabContainer;
private contentContainer: IContentContainer private contentContainer: IContentContainer;
private _active: boolean private _active: boolean;
private _activePanel: IGroupPanel 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: IGroupPanel[] = [] 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;
private readonly _onDrop = new Emitter<GroupDropEvent>() private readonly _onDrop = new Emitter<GroupDropEvent>();
readonly onDrop: Event<GroupDropEvent> = this._onDrop.event readonly onDrop: Event<GroupDropEvent> = this._onDrop.event;
private readonly _onDidGroupChange = new Emitter<GroupChangeEvent>() private readonly _onDidGroupChange = new Emitter<GroupChangeEvent>();
readonly onDidGroupChange: Event<{ kind: GroupChangeKind }> = this readonly onDidGroupChange: Event<{ kind: GroupChangeKind }> = this
._onDidGroupChange.event ._onDidGroupChange.event;
get activePanel() { get activePanel() {
return this._activePanel return this._activePanel;
} }
get tabHeight() { get tabHeight() {
return this.tabContainer.height return this.tabContainer.height;
} }
set tabHeight(height: number) { set tabHeight(height: number) {
this.tabContainer.height = height this.tabContainer.height = height;
this.layout(this._width, this._height) this.layout(this._width, this._height);
} }
get isActive() { get isActive() {
return this._active return this._active;
} }
get panels() { get panels() {
return this._panels return this._panels;
} }
get element() { get element() {
return this._element return this._element;
} }
get size() { get size() {
return this._panels.length return this._panels.length;
} }
get isEmpty() { get isEmpty() {
return this._panels.length === 0 return this._panels.length === 0;
} }
get minimumHeight() { get minimumHeight() {
return 100 return 100;
} }
get maximumHeight() { get maximumHeight() {
return Number.MAX_SAFE_INTEGER return Number.MAX_SAFE_INTEGER;
} }
get minimumWidth() { get minimumWidth() {
return 100 return 100;
} }
get maximumWidth() { get maximumWidth() {
return Number.MAX_SAFE_INTEGER return Number.MAX_SAFE_INTEGER;
} }
public indexOf(panel: IGroupPanel) { public indexOf(panel: IGroupPanel) {
return this.tabContainer.indexOf(panel.id) return this.tabContainer.indexOf(panel.id);
} }
public toJSON(): object { public toJSON(): object {
return { return {
views: this.panels.map((panel) => panel.id), views: this.panels.map((panel) => panel.id),
activeView: this._activePanel?.id, activeView: this._activePanel?.id,
} };
} }
public startActiveDrag(panel: IGroupPanel): 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);
tab.startDragEvent() tab.startDragEvent();
return { return {
dispose: () => { dispose: () => {
tab.stopDragEvent() tab.stopDragEvent();
}, },
};
} }
} return Disposable.NONE;
return Disposable.NONE
} }
public moveToNext(options?: { public moveToNext(options?: {
panel?: IGroupPanel panel?: IGroupPanel;
suppressRoll?: boolean suppressRoll?: boolean;
}) { }) {
if (!options) { if (!options) {
options = {} options = {};
} }
if (!options.panel) { if (!options.panel) {
options.panel = this.activePanel options.panel = this.activePanel;
} }
const index = this.panels.indexOf(options.panel) const index = this.panels.indexOf(options.panel);
let normalizedIndex: number = undefined let normalizedIndex: number = undefined;
if (index < this.panels.length - 1) { if (index < this.panels.length - 1) {
normalizedIndex = index + 1 normalizedIndex = index + 1;
} else if (!options.suppressRoll) { } else if (!options.suppressRoll) {
normalizedIndex = 0 normalizedIndex = 0;
} }
if (normalizedIndex === undefined) { if (normalizedIndex === undefined) {
return return;
} }
this.openPanel(this.panels[normalizedIndex]) this.openPanel(this.panels[normalizedIndex]);
} }
public moveToPrevious(options?: { public moveToPrevious(options?: {
panel?: IGroupPanel panel?: IGroupPanel;
suppressRoll?: boolean suppressRoll?: boolean;
}) { }) {
if (!options) { if (!options) {
options = {} options = {};
} }
if (!options.panel) { if (!options.panel) {
options.panel = this.activePanel options.panel = this.activePanel;
} }
const index = this.panels.indexOf(options.panel) const index = this.panels.indexOf(options.panel);
let normalizedIndex: number = undefined let normalizedIndex: number = undefined;
if (index > 0) { if (index > 0) {
normalizedIndex = index - 1 normalizedIndex = index - 1;
} else if (!options.suppressRoll) { } else if (!options.suppressRoll) {
normalizedIndex = this.panels.length - 1 normalizedIndex = this.panels.length - 1;
} }
if (normalizedIndex === undefined) { if (normalizedIndex === undefined) {
return return;
} }
this.openPanel(this.panels[normalizedIndex]) this.openPanel(this.panels[normalizedIndex]);
} }
public containsPanel(panel: IGroupPanel) { public containsPanel(panel: IGroupPanel) {
return this.panels.includes(panel) return this.panels.includes(panel);
} }
constructor( constructor(
@ -261,16 +259,16 @@ export class Groupview extends CompositeDisposable implements IGroupview {
public id: string, public id: string,
private options?: GroupOptions private options?: GroupOptions
) { ) {
super() super();
this.addDisposables(this._onMove, this._onDidGroupChange, this._onDrop) this.addDisposables(this._onMove, this._onDidGroupChange, this._onDrop);
this._element = document.createElement('div') this._element = document.createElement('div');
this._element.className = 'groupview' this._element.className = 'groupview';
this._element.tabIndex = -1 this._element.tabIndex = -1;
this.tabContainer = new TabContainer(this.accessor, this) this.tabContainer = new TabContainer(this.accessor, this);
this.contentContainer = new ContentContainer() this.contentContainer = new ContentContainer();
this.dropTarget = new Droptarget(this.contentContainer.element, { this.dropTarget = new Droptarget(this.contentContainer.element, {
isDirectional: true, isDirectional: true,
id: this.accessor.id, id: this.accessor.id,
@ -279,16 +277,16 @@ export class Groupview extends CompositeDisposable implements IGroupview {
return ( return (
this._panels.length === 1 && this._panels.length === 1 &&
this.tabContainer.hasActiveDragEvent this.tabContainer.hasActiveDragEvent
) );
}, },
enableExternalDragEvents: this.accessor.options enableExternalDragEvents: this.accessor.options
.enableExternalDragEvents, .enableExternalDragEvents,
}) });
this._element.append( this._element.append(
this.tabContainer.element, this.tabContainer.element,
this.contentContainer.element this.contentContainer.element
) );
this.addDisposables( this.addDisposables(
this._onMove, this._onMove,
@ -297,7 +295,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.handleDropEvent(event.event, event.index) this.handleDropEvent(event.event, event.index)
), ),
this.contentContainer.onDidFocus(() => { this.contentContainer.onDidFocus(() => {
this.accessor.doSetGroupActive(this) this.accessor.doSetGroupActive(this);
}), }),
this.dropTarget.onDidChange((event) => { this.dropTarget.onDidChange((event) => {
// if we've center dropped on ourself then ignore // if we've center dropped on ourself then ignore
@ -305,97 +303,99 @@ export class Groupview extends CompositeDisposable implements IGroupview {
event.position === Position.Center && event.position === Position.Center &&
this.tabContainer.hasActiveDragEvent this.tabContainer.hasActiveDragEvent
) { ) {
return return;
} }
this.handleDropEvent(event) this.handleDropEvent(event);
}) })
) );
if (options?.panels) { if (options?.panels) {
options.panels.forEach((panel) => { options.panels.forEach((panel) => {
this.openPanel(panel) this.openPanel(panel);
}) });
} }
if (options?.activePanel) { if (options?.activePanel) {
this.openPanel(options?.activePanel) this.openPanel(options?.activePanel);
} }
this.updateContainer() this.updateContainer();
} }
public openPanel(panel: IGroupPanel, 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;
} }
this.doAddPanel(panel, index) this.doAddPanel(panel, index);
this.tabContainer.openPanel(panel, index) this.tabContainer.openPanel(panel, index);
this.contentContainer.openPanel(panel.content.element) this.contentContainer.openPanel(panel.content.element);
this.doSetActivePanel(panel) this.doSetActivePanel(panel);
this.accessor.doSetGroupActive(this) this.accessor.doSetGroupActive(this);
this.updateContainer() this.updateContainer();
} }
public removePanel(groupItemOrId: IGroupPanel | string): IGroupPanel { public removePanel(groupItemOrId: IGroupPanel | string): IGroupPanel {
const id = const id =
typeof groupItemOrId === 'string' ? groupItemOrId : groupItemOrId.id typeof groupItemOrId === 'string'
? groupItemOrId
: groupItemOrId.id;
const panel = this._panels.find((panel) => panel.id === id) const panel = this._panels.find((panel) => panel.id === id);
if (!panel) { if (!panel) {
throw new Error('invalid operation') throw new Error('invalid operation');
} }
return this._removePanel(panel) return this._removePanel(panel);
} }
public async closeAllPanels() { public async closeAllPanels() {
const index = this.panels.indexOf(this._activePanel) const index = this.panels.indexOf(this._activePanel);
if (index > -1) { if (index > -1) {
if (this.panels.indexOf(this._activePanel) < 0) { if (this.panels.indexOf(this._activePanel) < 0) {
console.warn('active panel not tracked') console.warn('active panel not tracked');
} }
const canClose = const canClose =
!this._activePanel.close || !this._activePanel.close ||
(await this._activePanel.close()) === ClosePanelResult.CLOSE (await this._activePanel.close()) === ClosePanelResult.CLOSE;
if (!canClose) { if (!canClose) {
return false return false;
} }
} }
for (let i = 0; i < this.panels.length; i++) { for (let i = 0; i < this.panels.length; i++) {
if (i === index) { if (i === index) {
continue continue;
} }
const panel = this.panels[i] const panel = this.panels[i];
this.openPanel(panel) this.openPanel(panel);
if (panel.close) { if (panel.close) {
await timeoutPromise(0) await timeoutPromise(0);
const canClose = const canClose =
(await panel.close()) === ClosePanelResult.CLOSE (await panel.close()) === ClosePanelResult.CLOSE;
if (!canClose) { if (!canClose) {
return false return false;
} }
} }
} }
if (this.panels.length > 0) { if (this.panels.length > 0) {
// take a copy since we will be edting the array as we iterate through // take a copy since we will be edting the array as we iterate through
const arrPanelCpy = [...this.panels] const arrPanelCpy = [...this.panels];
await Promise.all(arrPanelCpy.map((p) => this.doClose(p))) await Promise.all(arrPanelCpy.map((p) => this.doClose(p)));
} else { } else {
this.accessor.removeGroup(this) this.accessor.removeGroup(this);
} }
return true return true;
} }
public closePanel = async (panel: IGroupPanel) => { public closePanel = async (panel: IGroupPanel) => {
@ -403,159 +403,163 @@ export class Groupview extends CompositeDisposable implements IGroupview {
panel.close && panel.close &&
(await panel.close()) === ClosePanelResult.DONT_CLOSE (await panel.close()) === ClosePanelResult.DONT_CLOSE
) { ) {
return false return false;
} }
this.doClose(panel) this.doClose(panel);
return true return true;
} };
private doClose(panel: IGroupPanel) { private doClose(panel: IGroupPanel) {
this._removePanel(panel) this._removePanel(panel);
;(this.accessor as Layout).unregisterPanel(panel) (this.accessor as Layout).unregisterPanel(panel);
panel.dispose() panel.dispose();
if (this.panels.length === 0) { if (this.panels.length === 0) {
this.accessor.removeGroup(this) this.accessor.removeGroup(this);
} }
} }
public isPanelActive(panel: IGroupPanel) { public isPanelActive(panel: IGroupPanel) {
return this._activePanel === panel return this._activePanel === panel;
} }
public setActive(isActive: boolean) { public setActive(isActive: boolean) {
if (this._active === isActive) { if (this._active === isActive) {
return return;
} }
this._active = isActive this._active = isActive;
toggleClass(this.element, 'active-group', isActive) toggleClass(this.element, 'active-group', isActive);
toggleClass(this.element, 'inactive-group', !isActive) toggleClass(this.element, 'inactive-group', !isActive);
this.tabContainer.setActive(this._active) this.tabContainer.setActive(this._active);
if (!this._activePanel && this.panels.length > 0) { if (!this._activePanel && this.panels.length > 0) {
this.doSetActivePanel(this.panels[0]) this.doSetActivePanel(this.panels[0]);
} }
this.panels.forEach((panel) => panel.setVisible(this._active, this)) this.panels.forEach((panel) => panel.setVisible(this._active, this));
if (this.watermark?.setVisible) { if (this.watermark?.setVisible) {
this.watermark.setVisible(this._active, this) this.watermark.setVisible(this._active, this);
} }
if (isActive) { if (isActive) {
this._onDidGroupChange.fire({ kind: GroupChangeKind.GROUP_ACTIVE }) this._onDidGroupChange.fire({ kind: GroupChangeKind.GROUP_ACTIVE });
} }
} }
public layout(width: number, height: number) { public layout(width: number, height: number) {
this._width = width this._width = width;
this._height = height this._height = height;
if (this._activePanel?.layout) { if (this._activePanel?.layout) {
this._activePanel.layout(this._width, this._height) this._activePanel.layout(this._width, this._height);
} }
} }
private _removePanel(panel: IGroupPanel) { 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;
this.doRemovePanel(panel) this.doRemovePanel(panel);
if (isActivePanel && this.panels.length > 0) { if (isActivePanel && this.panels.length > 0) {
const nextPanel = this.panels[Math.max(0, index - 1)] const nextPanel = this.panels[Math.max(0, index - 1)];
this.openPanel(nextPanel) this.openPanel(nextPanel);
} }
if (this._activePanel && this.panels.length === 0) { if (this._activePanel && this.panels.length === 0) {
this._activePanel = undefined this._activePanel = undefined;
} }
this.updateContainer() this.updateContainer();
return panel return panel;
} }
private doRemovePanel(panel: IGroupPanel) { private doRemovePanel(panel: IGroupPanel) {
const index = this.panels.indexOf(panel) const index = this.panels.indexOf(panel);
if (this._activePanel === panel) { if (this._activePanel === panel) {
this.contentContainer.closePanel() this.contentContainer.closePanel();
} }
this.tabContainer.delete(panel.id) this.tabContainer.delete(panel.id);
this._panels.splice(index, 1) this._panels.splice(index, 1);
this._onDidGroupChange.fire({ this._onDidGroupChange.fire({
kind: GroupChangeKind.REMOVE_PANEL, kind: GroupChangeKind.REMOVE_PANEL,
panel, panel,
}) });
} }
private doAddPanel(panel: IGroupPanel, 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;
if (hasExistingPabel) { if (hasExistingPabel) {
// TODO - need to ensure ordering hasn't changed and if it has need to re-order this.panels // TODO - need to ensure ordering hasn't changed and if it has need to re-order this.panels
return return;
} }
this.panels.splice(index, 0, panel) this.panels.splice(index, 0, panel);
this._onDidGroupChange.fire({ kind: GroupChangeKind.ADD_PANEL }) this._onDidGroupChange.fire({ kind: GroupChangeKind.ADD_PANEL });
} }
private doSetActivePanel(panel: IGroupPanel) { 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);
this._onDidGroupChange.fire({ kind: GroupChangeKind.PANEL_ACTIVE }) this._onDidGroupChange.fire({ kind: GroupChangeKind.PANEL_ACTIVE });
} }
private updateContainer() { private updateContainer() {
toggleClass(this.element, 'empty', this.isEmpty) toggleClass(this.element, 'empty', this.isEmpty);
if (this.accessor.options.watermarkComponent && !this.watermark) { if (this.accessor.options.watermarkComponent && !this.watermark) {
const WatermarkComponent = this.accessor.options.watermarkComponent const WatermarkComponent = this.accessor.options.watermarkComponent;
this.watermark = new WatermarkComponent() this.watermark = new WatermarkComponent();
this.watermark.init({ accessor: this.accessor }) this.watermark.init({ accessor: this.accessor });
} }
this.panels.forEach((panel) => panel.setVisible(this._active, this)) this.panels.forEach((panel) => panel.setVisible(this._active, this));
if (this.isEmpty && !this.watermark?.element.parentNode) { if (this.isEmpty && !this.watermark?.element.parentNode) {
addDisposableListener(this.watermark.element, 'click', () => { addDisposableListener(this.watermark.element, 'click', () => {
if (!this._active) { if (!this._active) {
this.accessor.doSetGroupActive(this) this.accessor.doSetGroupActive(this);
} }
}) });
this.contentContainer.openPanel(this.watermark.element) this.contentContainer.openPanel(this.watermark.element);
this.watermark.setVisible(true, this) this.watermark.setVisible(true, this);
} }
if (!this.isEmpty && this.watermark.element.parentNode) { if (!this.isEmpty && this.watermark.element.parentNode) {
this.watermark.dispose() this.watermark.dispose();
this.watermark = undefined this.watermark = undefined;
this.contentContainer.closePanel() this.contentContainer.closePanel();
} }
} }
private handleDropEvent(event: DroptargetEvent, index?: number) { private handleDropEvent(event: DroptargetEvent, index?: number) {
if (isPanelTransferEvent(event.event)) { if (isPanelTransferEvent(event.event)) {
this.handlePanelDropEvent(event.event, event.position, index) this.handlePanelDropEvent(event.event, event.position, index);
return return;
} }
this._onDrop.fire({ event: event.event, target: event.position, index }) this._onDrop.fire({
event: event.event,
target: event.position,
index,
});
console.debug('[customDropEvent]') console.debug('[customDropEvent]');
} }
private handlePanelDropEvent( private handlePanelDropEvent(
@ -563,16 +567,18 @@ export class Groupview extends CompositeDisposable implements IGroupview {
target: Position, target: Position,
index?: number index?: number
) { ) {
const dataObject = extractData(event) const dataObject = extractData(event);
if (isTabDragEvent(dataObject)) { if (isTabDragEvent(dataObject)) {
const { groupId, itemId } = dataObject const { groupId, itemId } = dataObject;
const isSameGroup = this.id === groupId const isSameGroup = this.id === groupId;
if (isSameGroup && !target) { if (isSameGroup && !target) {
const oldIndex = this.tabContainer.indexOf(itemId) const oldIndex = this.tabContainer.indexOf(itemId);
if (oldIndex === index) { if (oldIndex === index) {
console.debug('[tabs] drop indicates no change in position') console.debug(
return '[tabs] drop indicates no change in position'
);
return;
} }
} }
@ -581,14 +587,14 @@ export class Groupview extends CompositeDisposable implements IGroupview {
groupId: dataObject.groupId, groupId: dataObject.groupId,
itemId: dataObject.itemId, itemId: dataObject.itemId,
index, index,
}) });
} }
if (isCustomDragEvent(dataObject)) { if (isCustomDragEvent(dataObject)) {
let panel = this.accessor.getPanel(dataObject.id) let panel = this.accessor.getPanel(dataObject.id);
if (!panel) { if (!panel) {
panel = this.accessor.addPanel(dataObject) panel = this.accessor.addPanel(dataObject);
} }
this._onMove.fire({ this._onMove.fire({
@ -596,19 +602,19 @@ export class Groupview extends CompositeDisposable implements IGroupview {
groupId: panel.group?.id, groupId: panel.group?.id,
itemId: panel.id, itemId: panel.id,
index, index,
}) });
} }
} }
public dispose() { public dispose() {
for (const panel of this.panels) { for (const panel of this.panels) {
panel.dispose() panel.dispose();
} }
super.dispose() super.dispose();
this.dropTarget.dispose() this.dropTarget.dispose();
this.tabContainer.dispose() this.tabContainer.dispose();
this.contentContainer.dispose() this.contentContainer.dispose();
} }
} }

View File

@ -1,76 +1,76 @@
import { IGroupview } from '../groupview' import { IGroupview } from '../groupview';
import { Emitter, Event } from '../../events' import { Emitter, Event } from '../../events';
import { ClosePanelResult } from './parts' import { ClosePanelResult } from './parts';
import { IGroupPanel } from './types' import { IGroupPanel } from './types';
import { IBaseViewApi, BaseViewApi } from '../../panel/api' import { IBaseViewApi, BaseViewApi } from '../../panel/api';
interface ChangeVisibilityEvent { interface ChangeVisibilityEvent {
isVisible: boolean isVisible: boolean;
} }
export interface IGroupPanelApi extends IBaseViewApi { export interface IGroupPanelApi extends IBaseViewApi {
// events // events
onDidDirtyChange: Event<boolean> onDidDirtyChange: Event<boolean>;
onDidChangeVisibility: Event<ChangeVisibilityEvent> onDidChangeVisibility: Event<ChangeVisibilityEvent>;
// misc // misc
readonly isVisible: boolean readonly isVisible: boolean;
group: IGroupview group: IGroupview;
close: () => Promise<boolean> close: () => Promise<boolean>;
canClose: () => Promise<ClosePanelResult> canClose: () => Promise<ClosePanelResult>;
setClosePanelHook(callback: () => Promise<ClosePanelResult>): void setClosePanelHook(callback: () => Promise<ClosePanelResult>): void;
} }
export class GroupPanelApi extends BaseViewApi implements IGroupPanelApi { export class GroupPanelApi extends BaseViewApi implements IGroupPanelApi {
private _isVisible: boolean private _isVisible: boolean;
private _group: IGroupview private _group: IGroupview;
private _closePanelCallback: () => Promise<ClosePanelResult> private _closePanelCallback: () => Promise<ClosePanelResult>;
readonly _onDidDirtyChange = new Emitter<boolean>() readonly _onDidDirtyChange = new Emitter<boolean>();
readonly onDidDirtyChange = this._onDidDirtyChange.event readonly onDidDirtyChange = this._onDidDirtyChange.event;
readonly _onDidChangeVisibility = new Emitter<ChangeVisibilityEvent>({ readonly _onDidChangeVisibility = new Emitter<ChangeVisibilityEvent>({
emitLastValue: true, emitLastValue: true,
}) });
readonly onDidChangeVisibility: Event<ChangeVisibilityEvent> = this readonly onDidChangeVisibility: Event<ChangeVisibilityEvent> = this
._onDidChangeVisibility.event ._onDidChangeVisibility.event;
get isVisible() { get isVisible() {
return this._isVisible return this._isVisible;
} }
get canClose() { get canClose() {
return this._closePanelCallback return this._closePanelCallback;
} }
set group(value: IGroupview) { set group(value: IGroupview) {
this._group = value this._group = value;
} }
get group() { get group() {
return this._group return this._group;
} }
constructor(private panel: IGroupPanel, group: IGroupview) { constructor(private panel: IGroupPanel, group: IGroupview) {
super() super();
this._group = group this._group = group;
this.addDisposables( this.addDisposables(
this._onDidChangeVisibility, this._onDidChangeVisibility,
this._onDidDirtyChange, this._onDidDirtyChange,
this.onDidChangeVisibility((event) => { this.onDidChangeVisibility((event) => {
this._isVisible = event.isVisible this._isVisible = event.isVisible;
}) })
) );
} }
public close() { public close() {
return this.group.closePanel(this.panel) return this.group.closePanel(this.panel);
} }
public setClosePanelHook(callback: () => Promise<ClosePanelResult>) { public setClosePanelHook(callback: () => Promise<ClosePanelResult>) {
this._closePanelCallback = callback this._closePanelCallback = callback;
} }
public dispose() { public dispose() {
super.dispose() super.dispose();
} }
} }

View File

@ -1,55 +1,55 @@
import { CompositeDisposable, IDisposable } from '../../../lifecycle' import { CompositeDisposable, IDisposable } from '../../../lifecycle';
import { Emitter, Event } from '../../../events' import { Emitter, Event } from '../../../events';
import { trackFocus } from '../../../dom' import { trackFocus } from '../../../dom';
export interface IContentContainer extends IDisposable { export interface IContentContainer extends IDisposable {
onDidFocus: Event<void> onDidFocus: Event<void>;
element: HTMLElement element: HTMLElement;
openPanel: (panel: HTMLElement) => void openPanel: (panel: HTMLElement) => void;
closePanel: () => void closePanel: () => void;
} }
export class ContentContainer export class ContentContainer
extends CompositeDisposable extends CompositeDisposable
implements IContentContainer { implements IContentContainer {
private _element: HTMLElement private _element: HTMLElement;
private content: HTMLElement private content: HTMLElement;
private readonly _onDidFocus = new Emitter<void>() private readonly _onDidFocus = new Emitter<void>();
readonly onDidFocus: Event<void> = this._onDidFocus.event readonly onDidFocus: Event<void> = this._onDidFocus.event;
get element() { get element() {
return this._element return this._element;
} }
constructor() { constructor() {
super() super();
this._element = document.createElement('div') this._element = document.createElement('div');
this._element.className = 'content-container' this._element.className = 'content-container';
this._element.tabIndex = -1 this._element.tabIndex = -1;
const { onDidBlur, onDidFocus } = trackFocus(this._element) const { onDidBlur, onDidFocus } = trackFocus(this._element);
this.addDisposables(onDidFocus(() => this._onDidFocus.fire())) this.addDisposables(onDidFocus(() => this._onDidFocus.fire()));
} }
public openPanel(panel: HTMLElement) { public openPanel(panel: HTMLElement) {
if (this.content) { if (this.content) {
this._element.removeChild(this.content) this._element.removeChild(this.content);
this.content = undefined this.content = undefined;
} }
this.content = panel this.content = panel;
this._element.appendChild(this.content) this._element.appendChild(this.content);
} }
public closePanel() { public closePanel() {
if (this.content) { if (this.content) {
this._element.removeChild(this.content) this._element.removeChild(this.content);
this.content = undefined this.content = undefined;
} }
} }
public dispose() { public dispose() {
super.dispose() super.dispose();
} }
} }

View File

@ -1,30 +1,30 @@
import { IGroupPanel, PanelInitParameters } from './types' import { IGroupPanel, PanelInitParameters } from './types';
import { GroupPanelApi } from './api' import { GroupPanelApi } from './api';
import { Event } from '../../events' import { 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 { PanelUpdateEvent } from '../../panel/types' import { PanelUpdateEvent } from '../../panel/types';
export class DefaultPanel extends CompositeDisposable implements IGroupPanel { export class DefaultPanel extends CompositeDisposable implements IGroupPanel {
private readonly mutableDisposable = new MutableDisposable() private readonly mutableDisposable = new MutableDisposable();
private readonly api: GroupPanelApi private readonly api: GroupPanelApi;
private _group: IGroupview private _group: IGroupview;
private params: PanelInitParameters private params: PanelInitParameters;
readonly onDidStateChange: Event<any> readonly onDidStateChange: Event<any>;
get group() { get group() {
return this._group return this._group;
} }
get header() { get header() {
return this.headerPart return this.headerPart;
} }
get content() { get content() {
return this.contentPart return this.contentPart;
} }
constructor( constructor(
@ -32,22 +32,22 @@ export class DefaultPanel extends CompositeDisposable implements IGroupPanel {
private readonly headerPart: PanelHeaderPart, private readonly headerPart: PanelHeaderPart,
private readonly contentPart: PanelContentPart private readonly contentPart: PanelContentPart
) { ) {
super() super();
this.api = new GroupPanelApi(this, this._group) this.api = new GroupPanelApi(this, this._group);
this.onDidStateChange = this.api.onDidStateChange this.onDidStateChange = this.api.onDidStateChange;
} }
public setDirty(isDirty: boolean) { public setDirty(isDirty: boolean) {
this.api._onDidDirtyChange.fire(isDirty) this.api._onDidDirtyChange.fire(isDirty);
} }
public close(): Promise<ClosePanelResult> { public close(): Promise<ClosePanelResult> {
if (this.api.canClose) { if (this.api.canClose) {
return this.api.canClose() return this.api.canClose();
} }
return Promise.resolve(ClosePanelResult.CLOSE) return Promise.resolve(ClosePanelResult.CLOSE);
} }
public toJSON(): object { public toJSON(): object {
@ -59,7 +59,7 @@ export class DefaultPanel extends CompositeDisposable implements IGroupPanel {
title: this.params.title, title: this.params.title,
suppressClosable: this.params.suppressClosable, suppressClosable: this.params.suppressClosable,
state: this.api.getState(), state: this.api.getState(),
} };
} }
public fromJSON(data: object) { public fromJSON(data: object) {
@ -67,20 +67,20 @@ export class DefaultPanel extends CompositeDisposable implements IGroupPanel {
} }
public update(params: PanelUpdateEvent): void { public update(params: PanelUpdateEvent): void {
this.params.params = { ...this.params.params, ...params } this.params.params = { ...this.params.params, ...params };
this.contentPart.update(params.params) this.contentPart.update(params.params);
this.api._onDidStateChange.fire() this.api._onDidStateChange.fire();
} }
public init(params: PanelInitParameters): void { public init(params: PanelInitParameters): void {
this.params = params this.params = params;
this.api.setState(this.params.state) this.api.setState(this.params.state);
if (this.content.init) { if (this.content.init) {
this.content.init({ ...params, api: this.api }) this.content.init({ ...params, api: this.api });
} }
if (this.header.init) { if (this.header.init) {
this.header.init({ ...params, api: this.api }) this.header.init({ ...params, api: this.api });
} }
} }
@ -93,33 +93,33 @@ export class DefaultPanel extends CompositeDisposable implements IGroupPanel {
} }
public setVisible(isGroupActive: boolean, group: IGroupview) { public setVisible(isGroupActive: boolean, group: IGroupview) {
this._group = group this._group = group;
this.api.group = group this.api.group = group;
this.mutableDisposable.value = this._group.onDidGroupChange((ev) => { this.mutableDisposable.value = this._group.onDidGroupChange((ev) => {
if (ev.kind === GroupChangeKind.GROUP_ACTIVE) { if (ev.kind === GroupChangeKind.GROUP_ACTIVE) {
this.api._onDidChangeVisibility.fire({ this.api._onDidChangeVisibility.fire({
isVisible: this._group.isPanelActive(this), isVisible: this._group.isPanelActive(this),
}) });
} }
}) });
this.api._onDidChangeFocus.fire({ isFocused: isGroupActive }) this.api._onDidChangeFocus.fire({ isFocused: isGroupActive });
this.api._onDidChangeVisibility.fire({ this.api._onDidChangeVisibility.fire({
isVisible: this._group.isPanelActive(this), isVisible: this._group.isPanelActive(this),
}) });
if (this.headerPart.setVisible) { if (this.headerPart.setVisible) {
this.headerPart.setVisible( this.headerPart.setVisible(
this._group.isPanelActive(this), this._group.isPanelActive(this),
isGroupActive isGroupActive
) );
} }
if (this.contentPart.setVisible) { if (this.contentPart.setVisible) {
this.contentPart.setVisible( this.contentPart.setVisible(
this._group.isPanelActive(this), this._group.isPanelActive(this),
isGroupActive isGroupActive
) );
} }
} }
@ -128,14 +128,14 @@ export class DefaultPanel extends CompositeDisposable implements IGroupPanel {
this.api._onDidPanelDimensionChange.fire({ this.api._onDidPanelDimensionChange.fire({
width, width,
height: height - (this.group?.tabHeight || 0), height: height - (this.group?.tabHeight || 0),
}) });
} }
public dispose() { public dispose() {
this.api.dispose() this.api.dispose();
this.mutableDisposable.dispose() this.mutableDisposable.dispose();
this.headerPart.dispose() this.headerPart.dispose();
this.contentPart.dispose() this.contentPart.dispose();
} }
} }

View File

@ -1,9 +1,9 @@
import { IDisposable } from '../../lifecycle' import { IDisposable } from '../../lifecycle';
import { IGroupview } from '../groupview' import { IGroupview } from '../groupview';
import { IGroupAccessor } from '../../layout' import { IGroupAccessor } from '../../layout';
import { IGroupPanelApi } from './api' import { IGroupPanelApi } from './api';
import { PanelInitParameters } from './types' import { PanelInitParameters } from './types';
import { Constructor } from '../../types' import { Constructor } from '../../types';
export enum ClosePanelResult { export enum ClosePanelResult {
CLOSE = 'CLOSE', CLOSE = 'CLOSE',
@ -11,40 +11,40 @@ export enum ClosePanelResult {
} }
interface BasePart 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;
} }
export interface WatermarkPartInitParameters { export interface WatermarkPartInitParameters {
accessor: IGroupAccessor accessor: IGroupAccessor;
} }
export interface PartInitParameters extends PanelInitParameters { export interface PartInitParameters extends PanelInitParameters {
api: IGroupPanelApi api: IGroupPanelApi;
} }
export interface PanelHeaderPart extends BasePart { 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 BasePart { 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;
close?(): Promise<ClosePanelResult> close?(): Promise<ClosePanelResult>;
focus(): void focus(): void;
onHide(): void onHide(): void;
update(params: {}): void update(params: {}): void;
toJSON(): {} toJSON(): {};
} }
export interface WatermarkPart extends IDisposable { export interface WatermarkPart extends IDisposable {
init?: (params: WatermarkPartInitParameters) => void init?: (params: WatermarkPartInitParameters) => void;
setVisible?(visible: boolean, group: IGroupview): void setVisible?(visible: boolean, group: IGroupview): void;
element: HTMLElement element: HTMLElement;
} }
// constructors // constructors

View File

@ -1,56 +1,56 @@
import { addDisposableListener, Emitter, Event } from '../../../events' import { addDisposableListener, Emitter, Event } from '../../../events';
import { Droptarget, DroptargetEvent } from '../../droptarget/droptarget' import { Droptarget, DroptargetEvent } from '../../droptarget/droptarget';
import { CompositeDisposable } from '../../../lifecycle' import { CompositeDisposable } from '../../../lifecycle';
import { IGroupview } from '../../groupview' import { IGroupview } from '../../groupview';
import { import {
DataTransferSingleton, DataTransferSingleton,
DATA_KEY, DATA_KEY,
DragType, DragType,
} from '../../droptarget/dataTransfer' } from '../../droptarget/dataTransfer';
import { toggleClass } from '../../../dom' import { toggleClass } from '../../../dom';
import { IGroupAccessor } from '../../../layout' import { IGroupAccessor } from '../../../layout';
import { LayoutMouseEvent, MouseEventKind } from '../../events' import { LayoutMouseEvent, MouseEventKind } from '../../events';
export interface ITab { export interface ITab {
id: string id: string;
element: HTMLElement element: HTMLElement;
hasActiveDragEvent: boolean hasActiveDragEvent: boolean;
setContent: (element: HTMLElement) => void setContent: (element: HTMLElement) => void;
onChanged: Event<LayoutMouseEvent> onChanged: Event<LayoutMouseEvent>;
onDropped: Event<DroptargetEvent> onDropped: Event<DroptargetEvent>;
setActive(isActive: boolean): void setActive(isActive: boolean): void;
startDragEvent(): void startDragEvent(): void;
stopDragEvent(): void stopDragEvent(): void;
} }
export class Tab extends CompositeDisposable implements ITab { export class Tab extends CompositeDisposable implements ITab {
private _element: HTMLElement private _element: HTMLElement;
private dragInPlayDetails: { id?: string; isDragging: boolean } = { private dragInPlayDetails: { id?: string; isDragging: boolean } = {
isDragging: false, isDragging: false,
} };
private droptarget: Droptarget private droptarget: Droptarget;
private content: HTMLElement private content: HTMLElement;
private readonly _onChanged = new Emitter<LayoutMouseEvent>() private readonly _onChanged = new Emitter<LayoutMouseEvent>();
readonly onChanged: Event<LayoutMouseEvent> = this._onChanged.event readonly onChanged: Event<LayoutMouseEvent> = this._onChanged.event;
private readonly _onDropped = new Emitter<DroptargetEvent>() private readonly _onDropped = new Emitter<DroptargetEvent>();
readonly onDropped: Event<DroptargetEvent> = this._onDropped.event readonly onDropped: Event<DroptargetEvent> = this._onDropped.event;
public get element() { public get element() {
return this._element return this._element;
} }
public get hasActiveDragEvent() { public get hasActiveDragEvent() {
return this.dragInPlayDetails?.isDragging return this.dragInPlayDetails?.isDragging;
} }
public startDragEvent() { public startDragEvent() {
this.dragInPlayDetails = { isDragging: true, id: this.accessor.id } this.dragInPlayDetails = { isDragging: true, id: this.accessor.id };
} }
public stopDragEvent() { public stopDragEvent() {
this.dragInPlayDetails = { isDragging: false, id: undefined } this.dragInPlayDetails = { isDragging: false, id: undefined };
} }
constructor( constructor(
@ -58,73 +58,73 @@ export class Tab extends CompositeDisposable implements ITab {
private readonly accessor: IGroupAccessor, private readonly accessor: IGroupAccessor,
private group: IGroupview private group: IGroupview
) { ) {
super() super();
this.addDisposables(this._onChanged, this._onDropped) this.addDisposables(this._onChanged, this._onDropped);
this._element = document.createElement('div') this._element = document.createElement('div');
this._element.className = 'tab' this._element.className = 'tab';
this._element.draggable = true this._element.draggable = true;
this.addDisposables( this.addDisposables(
addDisposableListener(this._element, 'mousedown', (event) => { addDisposableListener(this._element, 'mousedown', (event) => {
if (event.defaultPrevented) { if (event.defaultPrevented) {
return return;
} }
this._onChanged.fire({ kind: MouseEventKind.CLICK, event }) this._onChanged.fire({ kind: MouseEventKind.CLICK, event });
}), }),
addDisposableListener(this._element, 'contextmenu', (event) => { addDisposableListener(this._element, 'contextmenu', (event) => {
this._onChanged.fire({ this._onChanged.fire({
kind: MouseEventKind.CONTEXT_MENU, kind: MouseEventKind.CONTEXT_MENU,
event, event,
}) });
}), }),
addDisposableListener(this._element, 'dragstart', (event) => { addDisposableListener(this._element, 'dragstart', (event) => {
this.dragInPlayDetails = { this.dragInPlayDetails = {
isDragging: true, isDragging: true,
id: this.accessor.id, id: this.accessor.id,
} };
// set up a custom ghost image // set up a custom ghost image
const dragImage = this._element.cloneNode(true) as HTMLElement const dragImage = this._element.cloneNode(true) as HTMLElement;
const box = this._element.getBoundingClientRect() const box = this._element.getBoundingClientRect();
// if the style of the tab is determined by CSS by a parent element that style will lost // if the style of the tab is determined by CSS by a parent element that style will lost
// therefore we must explicility re-add the style features that we know will be lost // therefore we must explicility re-add the style features that we know will be lost
dragImage.style.height = `${box.height}px` dragImage.style.height = `${box.height}px`;
dragImage.style.width = `${box.width}px` dragImage.style.width = `${box.width}px`;
dragImage.style.position = 'absolute' dragImage.style.position = 'absolute';
dragImage.classList.add('dragging') dragImage.classList.add('dragging');
document.body.appendChild(dragImage) document.body.appendChild(dragImage);
event.dataTransfer.setDragImage( event.dataTransfer.setDragImage(
dragImage, dragImage,
event.offsetX, event.offsetX,
event.offsetY event.offsetY
) );
setTimeout(() => document.body.removeChild(dragImage), 0) setTimeout(() => document.body.removeChild(dragImage), 0);
// configure the data-transfer object // configure the data-transfer object
const data = JSON.stringify({ const data = JSON.stringify({
type: DragType.ITEM, type: DragType.ITEM,
itemId: this.id, itemId: this.id,
groupId: this.group.id, groupId: this.group.id,
}) });
DataTransferSingleton.setData(this.dragInPlayDetails.id, data) DataTransferSingleton.setData(this.dragInPlayDetails.id, data);
event.dataTransfer.setData(DATA_KEY, data) event.dataTransfer.setData(DATA_KEY, data);
event.dataTransfer.effectAllowed = 'move' event.dataTransfer.effectAllowed = 'move';
}), }),
addDisposableListener(this._element, 'dragend', (ev) => { addDisposableListener(this._element, 'dragend', (ev) => {
// drop events fire before dragend so we can remove this safely // drop events fire before dragend so we can remove this safely
DataTransferSingleton.removeData(this.dragInPlayDetails.id) DataTransferSingleton.removeData(this.dragInPlayDetails.id);
this.dragInPlayDetails = { this.dragInPlayDetails = {
isDragging: false, isDragging: false,
id: undefined, id: undefined,
} };
}) })
) );
this.droptarget = new Droptarget(this._element, { this.droptarget = new Droptarget(this._element, {
isDirectional: false, isDirectional: false,
@ -132,30 +132,30 @@ export class Tab extends CompositeDisposable implements ITab {
id: this.accessor.id, id: this.accessor.id,
enableExternalDragEvents: this.accessor.options enableExternalDragEvents: this.accessor.options
.enableExternalDragEvents, .enableExternalDragEvents,
}) });
this.addDisposables( this.addDisposables(
this.droptarget.onDidChange((event) => { this.droptarget.onDidChange((event) => {
this._onDropped.fire(event) this._onDropped.fire(event);
}) })
) );
} }
public setActive(isActive: boolean) { public setActive(isActive: boolean) {
toggleClass(this.element, 'active-tab', isActive) toggleClass(this.element, 'active-tab', isActive);
toggleClass(this.element, 'inactive-tab', !isActive) toggleClass(this.element, 'inactive-tab', !isActive);
} }
public setContent(element: HTMLElement) { public setContent(element: HTMLElement) {
if (this.content) { if (this.content) {
this._element.removeChild(this.content) this._element.removeChild(this.content);
} }
this.content = element this.content = element;
this._element.appendChild(this.content) this._element.appendChild(this.content);
} }
public dispose() { public dispose() {
super.dispose() super.dispose();
this.droptarget.dispose() this.droptarget.dispose();
} }
} }

View File

@ -1,14 +1,14 @@
import { IGroupview } from '../groupview' 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' import { InitParameters, IPanel } from '../../panel/types';
// init parameters // init parameters
export interface PanelInitParameters extends InitParameters { export interface PanelInitParameters extends InitParameters {
title: string title: string;
suppressClosable?: boolean suppressClosable?: boolean;
} }
// constructors // constructors
@ -16,15 +16,15 @@ export interface PanelInitParameters extends InitParameters {
// panel // panel
export interface IGroupPanel extends IDisposable, ISerializable, IPanel { export interface IGroupPanel extends IDisposable, ISerializable, IPanel {
id: string id: string;
header: PanelHeaderPart header: PanelHeaderPart;
content: PanelContentPart content: PanelContentPart;
group: IGroupview group: IGroupview;
focus(): void focus(): void;
onHide(): void onHide(): void;
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>;
init?(params: PanelInitParameters & { [index: string]: string }): void init?(params: PanelInitParameters & { [index: string]: string }): void;
onDidStateChange: Event<any> onDidStateChange: Event<any>;
} }

View File

@ -2,170 +2,170 @@ import {
IDisposable, IDisposable,
CompositeDisposable, CompositeDisposable,
IValueDisposable, IValueDisposable,
} from '../../lifecycle' } from '../../lifecycle';
import { addDisposableListener, Emitter, Event } from '../../events' import { addDisposableListener, Emitter, Event } from '../../events';
import { ITab, Tab } from '../panel/tab/tab' import { ITab, Tab } from '../panel/tab/tab';
import { removeClasses, addClasses, toggleClass } from '../../dom' import { removeClasses, addClasses, toggleClass } from '../../dom';
import { hasProcessed, Position } from '../droptarget/droptarget' import { hasProcessed, Position } from '../droptarget/droptarget';
import { TabDropEvent } from '../events' import { TabDropEvent } from '../events';
import { IGroupview } from '../groupview' 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 { IGroupPanel } from '../panel/types' import { IGroupPanel } from '../panel/types';
import { MouseEventKind } from '../events' import { MouseEventKind } from '../events';
export interface ITabContainer extends IDisposable { export interface ITabContainer extends IDisposable {
element: HTMLElement element: HTMLElement;
visible: boolean visible: boolean;
height: number height: number;
hasActiveDragEvent: boolean hasActiveDragEvent: boolean;
delete: (id: string) => void delete: (id: string) => void;
indexOf: (tabOrId: ITab | string) => number indexOf: (tabOrId: ITab | string) => number;
at: (index: number) => ITab at: (index: number) => ITab;
onDropEvent: Event<TabDropEvent> onDropEvent: Event<TabDropEvent>;
setActive: (isGroupActive: boolean) => void setActive: (isGroupActive: boolean) => void;
setActivePanel: (panel: IGroupPanel) => void setActivePanel: (panel: IGroupPanel) => void;
isActive: (tab: ITab) => boolean isActive: (tab: ITab) => boolean;
closePanel: (panel: IGroupPanel) => void closePanel: (panel: IGroupPanel) => void;
openPanel: (panel: IGroupPanel, index?: number) => void openPanel: (panel: IGroupPanel, index?: number) => void;
} }
export class TabContainer extends CompositeDisposable implements ITabContainer { export class TabContainer extends CompositeDisposable implements ITabContainer {
private tabContainer: HTMLElement private tabContainer: HTMLElement;
private _element: HTMLElement private _element: HTMLElement;
private actionContainer: HTMLElement private actionContainer: HTMLElement;
private tabs: IValueDisposable<ITab>[] = [] private tabs: IValueDisposable<ITab>[] = [];
private selectedIndex: number = -1 private selectedIndex: number = -1;
private active: boolean private active: boolean;
private activePanel: IGroupPanel private activePanel: IGroupPanel;
private _visible: boolean = true private _visible: boolean = true;
private _height: number private _height: number;
private readonly _onDropped = new Emitter<TabDropEvent>() private readonly _onDropped = new Emitter<TabDropEvent>();
readonly onDropEvent: Event<TabDropEvent> = this._onDropped.event readonly onDropEvent: Event<TabDropEvent> = this._onDropped.event;
get visible() { get visible() {
return this._visible return this._visible;
} }
set visible(value: boolean) { set visible(value: boolean) {
this._visible = value this._visible = value;
toggleClass(this.element, 'hidden', !this._visible) toggleClass(this.element, 'hidden', !this._visible);
} }
get height() { get height() {
return this._height return this._height;
} }
set height(value: number) { set height(value: number) {
this._height = value this._height = value;
this._element.style.height = `${this.height}px` this._element.style.height = `${this.height}px`;
} }
public get element() { public get element() {
return this._element return this._element;
} }
public isActive(tab: ITab) { public isActive(tab: ITab) {
return ( return (
this.selectedIndex > -1 && this.selectedIndex > -1 &&
this.tabs[this.selectedIndex].value === tab this.tabs[this.selectedIndex].value === tab
) );
} }
public get hasActiveDragEvent() { public get hasActiveDragEvent() {
return !!this.tabs.find((tab) => tab.value.hasActiveDragEvent) return !!this.tabs.find((tab) => tab.value.hasActiveDragEvent);
} }
public at(index: number) { public at(index: number) {
return this.tabs[index]?.value 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.value.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) {
super() super();
this.addDisposables(this._onDropped) this.addDisposables(this._onDropped);
this._element = document.createElement('div') this._element = document.createElement('div');
this._element.className = 'title-container' this._element.className = 'title-container';
this.height = 35 this.height = 35;
this.actionContainer = document.createElement('div') this.actionContainer = document.createElement('div');
this.actionContainer.className = 'action-container' this.actionContainer.className = 'action-container';
const list = document.createElement('ul') const list = document.createElement('ul');
list.className = 'action-list' list.className = 'action-list';
this.tabContainer = document.createElement('div') this.tabContainer = document.createElement('div');
this.tabContainer.className = 'tab-container' this.tabContainer.className = 'tab-container';
this._element.appendChild(this.tabContainer) this._element.appendChild(this.tabContainer);
this._element.appendChild(this.actionContainer) this._element.appendChild(this.actionContainer);
this.addDisposables( this.addDisposables(
addDisposableListener(this.tabContainer, 'dragenter', (event) => { addDisposableListener(this.tabContainer, 'dragenter', (event) => {
if (!DataTransferSingleton.has(this.accessor.id)) { if (!DataTransferSingleton.has(this.accessor.id)) {
console.debug('[tabs] invalid drop event') console.debug('[tabs] invalid drop event');
return return;
} }
if (!last(this.tabs).value.hasActiveDragEvent) { if (!last(this.tabs).value.hasActiveDragEvent) {
addClasses(this.tabContainer, 'drag-over-target') addClasses(this.tabContainer, 'drag-over-target');
} }
}), }),
addDisposableListener(this.tabContainer, 'dragover', (event) => { addDisposableListener(this.tabContainer, 'dragover', (event) => {
event.preventDefault() event.preventDefault();
}), }),
addDisposableListener(this.tabContainer, 'dragleave', (event) => { addDisposableListener(this.tabContainer, 'dragleave', (event) => {
removeClasses(this.tabContainer, 'drag-over-target') removeClasses(this.tabContainer, 'drag-over-target');
}), }),
addDisposableListener(this.tabContainer, 'drop', (event) => { addDisposableListener(this.tabContainer, 'drop', (event) => {
if (!DataTransferSingleton.has(this.accessor.id)) { if (!DataTransferSingleton.has(this.accessor.id)) {
console.debug('[tabs] invalid drop event') console.debug('[tabs] invalid drop event');
return return;
} }
if (hasProcessed(event)) { if (hasProcessed(event)) {
console.debug('[tab] drop event already processed') console.debug('[tab] drop event already processed');
return return;
} }
removeClasses(this.tabContainer, 'drag-over-target') removeClasses(this.tabContainer, 'drag-over-target');
const activetab = this.tabs.find( const activetab = this.tabs.find(
(tab) => tab.value.hasActiveDragEvent (tab) => tab.value.hasActiveDragEvent
) );
const ignore = !!( const ignore = !!(
activetab && activetab &&
event event
.composedPath() .composedPath()
.find((x) => activetab.value.element === x) .find((x) => activetab.value.element === x)
) );
if (ignore) { if (ignore) {
console.debug('[tabs] ignore event') console.debug('[tabs] ignore event');
return return;
} }
this._onDropped.fire({ this._onDropped.fire({
event: { event, position: Position.Center }, event: { event, position: Position.Center },
index: this.tabs.length - 1, index: this.tabs.length - 1,
});
}) })
}) );
)
} }
public setActive(isGroupActive: boolean) { public setActive(isGroupActive: boolean) {
this.active = isGroupActive this.active = isGroupActive;
} }
private addTab( private addTab(
@ -173,80 +173,80 @@ export class TabContainer extends CompositeDisposable implements ITabContainer {
index: number = this.tabs.length 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.value.element, tab.value.element,
this.tabContainer.children[index] this.tabContainer.children[index]
) );
this.tabs = [ this.tabs = [
...this.tabs.slice(0, index), ...this.tabs.slice(0, index),
tab, tab,
...this.tabs.slice(index), ...this.tabs.slice(index),
] ];
if (this.selectedIndex < 0) { if (this.selectedIndex < 0) {
this.selectedIndex = index this.selectedIndex = index;
} }
} }
public delete(id: string) { public delete(id: string) {
const index = this.tabs.findIndex((tab) => tab.value.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];
const { value, disposable } = tab const { value, disposable } = tab;
disposable.dispose() disposable.dispose();
value.element.remove() value.element.remove();
} }
public setActivePanel(panel: IGroupPanel) { public setActivePanel(panel: IGroupPanel) {
this.tabs.forEach((tab) => { this.tabs.forEach((tab) => {
const isActivePanel = panel.id === tab.value.id const isActivePanel = panel.id === tab.value.id;
tab.value.setActive(isActivePanel) tab.value.setActive(isActivePanel);
}) });
} }
public openPanel(panel: IGroupPanel, index: number = this.tabs.length) { public openPanel(panel: IGroupPanel, index: number = this.tabs.length) {
if (this.tabs.find((tab) => tab.value.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);
const disposable = CompositeDisposable.from( const disposable = CompositeDisposable.from(
tab.onChanged((event) => { tab.onChanged((event) => {
switch (event.kind) { switch (event.kind) {
case MouseEventKind.CLICK: case MouseEventKind.CLICK:
this.group.openPanel(panel) this.group.openPanel(panel);
break break;
} }
this.accessor.fireMouseEvent({ ...event, panel, tab: true }) this.accessor.fireMouseEvent({ ...event, panel, tab: true });
}), }),
tab.onDropped((event) => { tab.onDropped((event) => {
this._onDropped.fire({ event, index: this.indexOf(tab) }) this._onDropped.fire({ event, index: this.indexOf(tab) });
}) })
) );
const value: IValueDisposable<ITab> = { value: tab, disposable } const value: IValueDisposable<ITab> = { value: tab, disposable };
this.addTab(value, index) this.addTab(value, index);
this.activePanel = panel this.activePanel = panel;
} }
public closePanel(panel: IGroupPanel) { public closePanel(panel: IGroupPanel) {
this.delete(panel.id) this.delete(panel.id);
} }
public dispose() { public dispose() {
super.dispose() super.dispose();
this.tabs.forEach((tab) => { this.tabs.forEach((tab) => {
tab.disposable.dispose() tab.disposable.dispose();
}) });
this.tabs = [] this.tabs = [];
} }
} }

View File

@ -1,21 +1,21 @@
export * from './splitview/splitview' export * from './splitview/splitview';
export * from './paneview/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';
export * from './groupview/panel/tab/tab' export * from './groupview/panel/tab/tab';
export * from './events' export * from './events';
export * from './lifecycle' export * from './lifecycle';
export * from './groupview/panel/panel' export * from './groupview/panel/panel';
export * from './groupview/panel/api' export * from './groupview/panel/api';
export * from './react/react' export * from './react/react';
export * from './groupview/panel/types' export * from './groupview/panel/types';
export * from './groupview/panel/parts' export * from './groupview/panel/parts';
export * from './react/layout' export * from './react/layout';
export * from './react/splitview' export * from './react/splitview';
export * from './react/gridview' export * from './react/gridview';
export * from './react/reactContentPart' export * from './react/reactContentPart';
export * from './react/reactHeaderPart' export * from './react/reactHeaderPart';
export * from './react/reactComponentGridView' export * from './react/reactComponentGridView';
export * from './layout' export * from './layout';

View File

@ -0,0 +1,173 @@
import { MovementOptions2 } from '.';
import { getGridLocation, Gridview, IGridView } from '../gridview/gridview';
import { CompositeDisposable, IValueDisposable } from '../lifecycle';
import { sequentialNumberGenerator } from '../math';
const nextLayoutId = sequentialNumberGenerator();
export interface BaseGridOptions {
readonly proportionalLayout?: boolean;
}
export interface IBaseGridView extends IGridView {
id: string;
setActive(isActive: boolean): void;
}
export interface IBaseGrid<T extends IBaseGridView> {
readonly element: HTMLElement;
readonly id: string;
readonly minimumHeight: number;
readonly maximumHeight: number;
readonly minimumWidth: number;
readonly maximumWidth: number;
readonly activeGroup: T;
readonly size: number;
getGroup(id: string): T | undefined;
}
export class BaseGrid<T extends IBaseGridView>
extends CompositeDisposable
implements IBaseGrid<T> {
private readonly _id = nextLayoutId.next();
protected readonly groups = new Map<string, IValueDisposable<T>>();
protected readonly gridview: Gridview;
//
private resizeTimer: NodeJS.Timer;
protected _activeGroup: T;
//
protected _size: number;
protected _orthogonalSize: number;
get id() {
return this._id;
}
get element() {
return this._element;
}
get size() {
return this.groups.size;
}
get minimumHeight() {
return this.gridview.minimumHeight;
}
get maximumHeight() {
return this.gridview.maximumHeight;
}
get minimumWidth() {
return this.gridview.maximumWidth;
}
get maximumWidth() {
return this.gridview.maximumWidth;
}
get activeGroup() {
return this._activeGroup;
}
constructor(
private readonly _element: HTMLElement,
options: BaseGridOptions
) {
super();
this.gridview = new Gridview(!!options.proportionalLayout);
this.element.appendChild(this.gridview.element);
}
public getGroup(id: string): T | undefined {
return this.groups.get(id)?.value;
}
public doSetGroupActive(group: T) {
if (this._activeGroup && this._activeGroup !== group) {
this._activeGroup.setActive(false);
}
group.setActive(true);
this._activeGroup = group;
}
public moveToNext(options?: MovementOptions2) {
if (!options) {
options = {};
}
if (!options.group) {
options.group = this.activeGroup;
}
const location = getGridLocation(options.group.element);
const next = this.gridview.next(location)?.view;
this.doSetGroupActive(next as T);
}
public moveToPrevious(options?: MovementOptions2) {
if (!options) {
options = {};
}
if (!options.group) {
options.group = this.activeGroup;
}
const location = getGridLocation(options.group.element);
const next = this.gridview.preivous(location)?.view;
this.doSetGroupActive(next as T);
}
public layout(
size: number,
orthogonalSize: number,
forceResize?: boolean
): void {
const different =
forceResize ||
size !== this._size ||
orthogonalSize !== this._orthogonalSize;
if (!different) {
return;
}
this.element.style.height = `${orthogonalSize}px`;
this.element.style.width = `${size}px`;
this._size = size;
this._orthogonalSize = orthogonalSize;
this.gridview.layout(size, orthogonalSize);
}
public setAutoResizeToFit(enabled: boolean): void {
if (this.resizeTimer) {
clearInterval(this.resizeTimer);
}
if (enabled) {
this.resizeTimer = setInterval(() => {
this.resizeToFit();
}, 500);
}
}
/**
* Resize the layout to fit the parent container
*/
public resizeToFit(): void {
const {
width,
height,
} = this.element.parentElement.getBoundingClientRect();
this.layout(width, height);
}
public dispose(): void {
super.dispose();
if (this.resizeTimer) {
clearInterval(this.resizeTimer);
this.resizeTimer = undefined;
}
this.gridview.dispose();
}
}

View File

@ -3,87 +3,87 @@ import {
PanelContentPartConstructor, PanelContentPartConstructor,
PanelHeaderPart, PanelHeaderPart,
PanelHeaderPartConstructor, PanelHeaderPartConstructor,
} from '../groupview/panel/parts' } from '../groupview/panel/parts';
import { FrameworkFactory } from '../types' import { FrameworkFactory } from '../types';
import { DefaultTab } from './components/tab/defaultTab' import { DefaultTab } from './components/tab/defaultTab';
export function createContentComponent( export function createContentComponent(
componentName: string | PanelContentPartConstructor | any, componentName: string | PanelContentPartConstructor | any,
components: { components: {
[componentName: string]: PanelContentPartConstructor [componentName: string]: PanelContentPartConstructor;
}, },
frameworkComponents: { frameworkComponents: {
[componentName: string]: any [componentName: string]: any;
}, },
createFrameworkComponent: FrameworkFactory<PanelContentPart> createFrameworkComponent: FrameworkFactory<PanelContentPart>
): PanelContentPart { ): PanelContentPart {
const Component = const Component =
typeof componentName === 'string' typeof componentName === 'string'
? components[componentName] ? components[componentName]
: componentName : componentName;
const FrameworkComponent = const FrameworkComponent =
typeof componentName === 'string' typeof componentName === 'string'
? frameworkComponents[componentName] ? frameworkComponents[componentName]
: componentName : componentName;
if (Component && FrameworkComponent) { if (Component && FrameworkComponent) {
throw new Error( throw new Error(
`cannot register component ${componentName} as both a component and frameworkComponent` `cannot register component ${componentName} as both a component and frameworkComponent`
) );
} }
if (FrameworkComponent) { if (FrameworkComponent) {
if (!createFrameworkComponent) { if (!createFrameworkComponent) {
throw new Error( throw new Error(
'you must register a frameworkPanelWrapper to use framework components' 'you must register a frameworkPanelWrapper to use framework components'
) );
} }
const wrappedComponent = createFrameworkComponent.createComponent( const wrappedComponent = createFrameworkComponent.createComponent(
componentName, componentName,
FrameworkComponent FrameworkComponent
) );
return wrappedComponent return wrappedComponent;
} }
return new Component() as PanelContentPart return new Component() as PanelContentPart;
} }
export function createTabComponent( export function createTabComponent(
componentName: string | PanelHeaderPartConstructor | any, componentName: string | PanelHeaderPartConstructor | any,
components: { components: {
[componentName: string]: PanelHeaderPartConstructor [componentName: string]: PanelHeaderPartConstructor;
}, },
frameworkComponents: { frameworkComponents: {
[componentName: string]: any [componentName: string]: any;
}, },
createFrameworkComponent: FrameworkFactory<PanelHeaderPart> createFrameworkComponent: FrameworkFactory<PanelHeaderPart>
): PanelHeaderPart { ): PanelHeaderPart {
const Component = const Component =
typeof componentName === 'string' typeof componentName === 'string'
? components[componentName] ? components[componentName]
: componentName : componentName;
const FrameworkComponent = const FrameworkComponent =
typeof componentName === 'string' typeof componentName === 'string'
? frameworkComponents[componentName] ? frameworkComponents[componentName]
: componentName : componentName;
if (Component && FrameworkComponent) { if (Component && FrameworkComponent) {
throw new Error( throw new Error(
`cannot register component ${componentName} as both a component and frameworkComponent` `cannot register component ${componentName} as both a component and frameworkComponent`
) );
} }
if (FrameworkComponent) { if (FrameworkComponent) {
if (!createFrameworkComponent) { if (!createFrameworkComponent) {
throw new Error( throw new Error(
'you must register a frameworkPanelWrapper to use framework components' 'you must register a frameworkPanelWrapper to use framework components'
) );
} }
const wrappedComponent = createFrameworkComponent.createComponent( const wrappedComponent = createFrameworkComponent.createComponent(
componentName, componentName,
FrameworkComponent FrameworkComponent
) );
return wrappedComponent return wrappedComponent;
} }
if (!Component) { if (!Component) {
return new DefaultTab() return new DefaultTab();
} }
return new Component() as PanelHeaderPart return new Component() as PanelHeaderPart;
} }

View File

@ -1,165 +1,85 @@
import { Gridview, getRelativeLocation, IGridView } from '../gridview/gridview' import { getRelativeLocation, IGridView } from '../gridview/gridview';
import { Position } from '../groupview/droptarget/droptarget' import { Position } from '../groupview/droptarget/droptarget';
import { getGridLocation } from '../gridview/gridview' import { getGridLocation } from '../gridview/gridview';
import { tail, sequenceEquals } from '../array' import { tail, sequenceEquals } from '../array';
import { import { GroupChangeKind, GroupChangeEvent } from '../groupview/groupview';
GroupChangeKind, import { Disposable, IValueDisposable } from '../lifecycle';
GroupChangeEvent, import { Event, Emitter } from '../events';
GroupDropEvent,
} from '../groupview/groupview'
import { CompositeDisposable, Disposable, IValueDisposable } from '../lifecycle'
import { Event, Emitter } from '../events'
import { DebugWidget } from './components/debug/debug' import { DebugWidget } from './components/debug/debug';
import { sequentialNumberGenerator } from '../math' import { sequentialNumberGenerator } from '../math';
import { IPanelDeserializer } from './deserializer' import { IPanelDeserializer } from './deserializer';
import { createComponent } from '../splitview/options' import { createComponent } from '../splitview/options';
import { LayoutPriority, Orientation } from '../splitview/splitview' import { LayoutPriority, Orientation } from '../splitview/splitview';
import { MovementOptions2 } from './options';
import { GridComponentOptions } from '.';
import { BaseGrid, IBaseGrid, IBaseGridView } from './baseGrid';
const nextLayoutId = sequentialNumberGenerator() const nextLayoutId = sequentialNumberGenerator();
export interface AddComponentOptions { export interface AddComponentOptions {
component: string component: string;
params?: { [key: string]: any } params?: { [key: string]: any };
id: string id: string;
position?: { position?: {
direction?: 'left' | 'right' | 'above' | 'below' | 'within' direction?: 'left' | 'right' | 'above' | 'below' | 'within';
reference: string reference: string;
} };
size?: number size?: number;
priority?: LayoutPriority priority?: LayoutPriority;
snap?: boolean snap?: boolean;
} }
export interface GridComponentOptions { export interface IComponentGridview extends IBaseGridView {
orientation: Orientation init?: (params: { params: any }) => void;
components?: { priority?: LayoutPriority;
[componentName: string]: IComponentGridview
}
frameworkComponents?: {
[componentName: string]: any
}
frameworkComponentFactory: any
tabHeight?: number
} }
export interface IComponentGridview extends IGridView { export interface IComponentGridviewLayout
id: string extends IBaseGrid<IComponentGridview> {
init: (params: { params: any }) => void addComponent(options: AddComponentOptions): void;
priority?: LayoutPriority
}
export interface MovementOptions2 {
group?: IComponentGridview
}
export interface IComponentGridviewLayout {
addComponent(options: AddComponentOptions): void
} }
export class ComponentGridview export class ComponentGridview
extends CompositeDisposable extends BaseGrid<IComponentGridview>
implements IComponentGridviewLayout { implements IComponentGridviewLayout {
private readonly _id = nextLayoutId.next()
private readonly groups = new Map<
string,
IValueDisposable<IComponentGridview>
>()
private readonly gridview: Gridview = new Gridview(false)
// events // events
private readonly _onDidLayoutChange = new Emitter<GroupChangeEvent>() private readonly _onDidLayoutChange = new Emitter<GroupChangeEvent>();
readonly onDidLayoutChange: Event<GroupChangeEvent> = this readonly onDidLayoutChange: Event<GroupChangeEvent> = this
._onDidLayoutChange.event ._onDidLayoutChange.event;
// everything else // everything else
private _size: number
private _orthogonalSize: number private _deserializer: IPanelDeserializer;
private _activeGroup: IComponentGridview private debugContainer: DebugWidget;
private _deserializer: IPanelDeserializer
private resizeTimer: NodeJS.Timer
private debugContainer: DebugWidget
constructor( constructor(
private readonly element: HTMLElement, element: HTMLElement,
public readonly options: GridComponentOptions public readonly options: GridComponentOptions
) { ) {
super() super(element, { proportionalLayout: true });
this.element.appendChild(this.gridview.element)
if (!this.options.components) { if (!this.options.components) {
this.options.components = {} this.options.components = {};
} }
if (!this.options.frameworkComponents) { if (!this.options.frameworkComponents) {
this.options.frameworkComponents = {} this.options.frameworkComponents = {};
} }
this.addDisposables( this.addDisposables(
this.gridview.onDidChange((e) => { this.gridview.onDidChange((e) => {
this._onDidLayoutChange.fire({ kind: GroupChangeKind.LAYOUT }) this._onDidLayoutChange.fire({ kind: GroupChangeKind.LAYOUT });
}) })
) );
}
get minimumHeight() {
return this.gridview.minimumHeight
}
get maximumHeight() {
return this.gridview.maximumHeight
}
get minimumWidth() {
return this.gridview.maximumWidth
}
get maximumWidth() {
return this.gridview.maximumWidth
}
get activeGroup() {
return this._activeGroup
} }
get deserializer() { get deserializer() {
return this._deserializer return this._deserializer;
} }
set deserializer(value: IPanelDeserializer) { set deserializer(value: IPanelDeserializer) {
this._deserializer = value this._deserializer = value;
}
get id() {
return this._id
}
get size() {
return this.groups.size
}
public moveToNext(options?: MovementOptions2) {
if (!options) {
options = {}
}
if (!options.group) {
options.group = this.activeGroup
}
const location = getGridLocation(options.group.element)
const next = this.gridview.next(location)?.view as IComponentGridview
this.doSetGroupActive(next)
}
public moveToPrevious(options?: MovementOptions2) {
if (!options) {
options = {}
}
if (!options.group) {
options.group = this.activeGroup
}
const location = getGridLocation(options.group.element)
const next = this.gridview.preivous(location)
?.view as IComponentGridview
this.doSetGroupActive(next)
} }
/** /**
@ -168,21 +88,21 @@ export class ComponentGridview
* @returns A JSON respresentation of the layout * @returns A JSON respresentation of the layout
*/ */
public toJSON() { public toJSON() {
const data = this.gridview.serialize() const data = this.gridview.serialize();
return { grid: data } return { grid: data };
} }
public deserialize(data: any) { public deserialize(data: any) {
this.gridview.clear() this.gridview.clear();
this.groups.clear() this.groups.clear();
this.fromJSON(data, this.deserializer) this.fromJSON(data, this.deserializer);
this.gridview.layout(this._size, this._orthogonalSize) this.gridview.layout(this._size, this._orthogonalSize);
} }
public fromJSON(data: any, deserializer: IPanelDeserializer) { public fromJSON(data: any, deserializer: IPanelDeserializer) {
const { grid, panels } = data const { grid, panels } = data;
// this.gridview.deserialize( // this.gridview.deserialize(
// grid, // grid,
@ -195,48 +115,26 @@ export class ComponentGridview
// }, // },
// }) // })
// ); // );
this._onDidLayoutChange.fire({ kind: GroupChangeKind.NEW_LAYOUT }) this._onDidLayoutChange.fire({ kind: GroupChangeKind.NEW_LAYOUT });
}
public setAutoResizeToFit(enabled: boolean) {
if (this.resizeTimer) {
clearInterval(this.resizeTimer)
}
if (enabled) {
this.resizeTimer = setInterval(() => {
this.resizeToFit()
}, 500)
}
}
/**
* Resize the layout to fit the parent container
*/
public resizeToFit() {
const {
width,
height,
} = this.element.parentElement.getBoundingClientRect()
this.layout(width, height)
} }
public addComponent(options: AddComponentOptions) { public addComponent(options: AddComponentOptions) {
let relativeLocation: number[] = [0] let relativeLocation: number[] = [0];
if (options.position?.reference) { if (options.position?.reference) {
const referenceGroup = this.groups.get(options.position.reference) const referenceGroup = this.groups.get(options.position.reference)
.value .value;
const target = this.toTarget(options.position.direction) const target = this.toTarget(options.position.direction);
if (target === Position.Center) { if (target === Position.Center) {
throw new Error(`${target} not supported as an option`) throw new Error(`${target} not supported as an option`);
} else { } else {
const location = getGridLocation(referenceGroup.element) const location = getGridLocation(referenceGroup.element);
relativeLocation = getRelativeLocation( relativeLocation = getRelativeLocation(
this.gridview.orientation, this.gridview.orientation,
location, location,
target target
) );
} }
} }
@ -245,28 +143,24 @@ export class ComponentGridview
this.options.components, this.options.components,
this.options.frameworkComponents, this.options.frameworkComponents,
this.options.frameworkComponentFactory.createComponent this.options.frameworkComponentFactory.createComponent
) );
view.init({ params: {} }) view.init({ params: {} });
view.priority = options.priority view.priority = options.priority;
view.snap = options.snap view.snap = options.snap;
this.groups.set(options.id, { this.groups.set(options.id, {
value: view, value: view,
disposable: Disposable.NONE, disposable: Disposable.NONE,
}) });
this.doAddGroup(view, relativeLocation, options.size) this.doAddGroup(view, relativeLocation, options.size);
}
public getGroup(id: string) {
return this.groups.get(id)?.value
} }
public removeGroup(group: IComponentGridview) { public removeGroup(group: IComponentGridview) {
if (group === this._activeGroup) { if (group === this._activeGroup) {
this._activeGroup = undefined this._activeGroup = undefined;
} }
this.doRemoveGroup(group) this.doRemoveGroup(group);
} }
private doAddGroup( private doAddGroup(
@ -274,10 +168,10 @@ export class ComponentGridview
location: number[], location: number[],
size?: number size?: number
) { ) {
this.gridview.addView(group, size ?? { type: 'distribute' }, location) this.gridview.addView(group, size ?? { type: 'distribute' }, location);
this._onDidLayoutChange.fire({ kind: GroupChangeKind.ADD_GROUP }) this._onDidLayoutChange.fire({ kind: GroupChangeKind.ADD_GROUP });
this.doSetGroupActive(group) this.doSetGroupActive(group);
} }
private doRemoveGroup( private doRemoveGroup(
@ -285,94 +179,71 @@ export class ComponentGridview
options?: { skipActive?: boolean; skipDispose?: boolean } options?: { skipActive?: boolean; skipDispose?: boolean }
) { ) {
if (!this.groups.has(group.id)) { if (!this.groups.has(group.id)) {
throw new Error('invalid operation') throw new Error('invalid operation');
} }
const { disposable } = this.groups.get(group.id) const { disposable } = this.groups.get(group.id);
if (!options?.skipDispose) { if (!options?.skipDispose) {
disposable.dispose() disposable.dispose();
this.groups.delete(group.id) this.groups.delete(group.id);
} }
const view = this.gridview.remove(group, { type: 'distribute' }) const view = this.gridview.remove(group, { type: 'distribute' });
this._onDidLayoutChange.fire({ kind: GroupChangeKind.REMOVE_GROUP }) this._onDidLayoutChange.fire({ kind: GroupChangeKind.REMOVE_GROUP });
if (!options?.skipActive && this.groups.size > 0) { if (!options?.skipActive && this.groups.size > 0) {
this.doSetGroupActive(Array.from(this.groups.values())[0].value) this.doSetGroupActive(Array.from(this.groups.values())[0].value);
} }
return view return view;
}
public doSetGroupActive(group: IComponentGridview) {
if (this._activeGroup && this._activeGroup !== group) {
// this._activeGroup.setActive(false);
}
// group.setActive(true);
this._activeGroup = group
} }
public moveGroup( public moveGroup(
referenceGroup: IComponentGridview, referenceGroup: IComponentGridview,
groupId: string, groupId: string,
itemId: string,
target: Position target: Position
) { ) {
const sourceGroup = groupId ? this.groups.get(groupId).value : undefined const sourceGroup = groupId
? this.groups.get(groupId).value
: undefined;
const referenceLocation = getGridLocation(referenceGroup.element) const referenceLocation = getGridLocation(referenceGroup.element);
const targetLocation = getRelativeLocation( const targetLocation = getRelativeLocation(
this.gridview.orientation, this.gridview.orientation,
referenceLocation, referenceLocation,
target target
) );
const [targetParentLocation, to] = tail(targetLocation) const [targetParentLocation, to] = tail(targetLocation);
const sourceLocation = getGridLocation(sourceGroup.element) const sourceLocation = getGridLocation(sourceGroup.element);
const [sourceParentLocation, from] = tail(sourceLocation) const [sourceParentLocation, from] = tail(sourceLocation);
if (sequenceEquals(sourceParentLocation, targetParentLocation)) { if (sequenceEquals(sourceParentLocation, targetParentLocation)) {
// special case when 'swapping' two views within same grid location // special case when 'swapping' two views within same grid location
// if a group has one tab - we are essentially moving the 'group' // if a group has one tab - we are essentially moving the 'group'
// which is equivalent to swapping two views in this case // which is equivalent to swapping two views in this case
this.gridview.moveView(sourceParentLocation, from, to) this.gridview.moveView(sourceParentLocation, from, to);
return return;
} }
// source group will become empty so delete the group // source group will become empty so delete the group
const targetGroup = this.doRemoveGroup(sourceGroup, { const targetGroup = this.doRemoveGroup(sourceGroup, {
skipActive: true, skipActive: true,
skipDispose: true, skipDispose: true,
}) as IComponentGridview }) as IComponentGridview;
// after deleting the group we need to re-evaulate the ref location // after deleting the group we need to re-evaulate the ref location
const updatedReferenceLocation = getGridLocation(referenceGroup.element) const updatedReferenceLocation = getGridLocation(
referenceGroup.element
);
const location = getRelativeLocation( const location = getRelativeLocation(
this.gridview.orientation, this.gridview.orientation,
updatedReferenceLocation, updatedReferenceLocation,
target target
) );
this.doAddGroup(targetGroup, location) this.doAddGroup(targetGroup, location);
}
public layout(size: number, orthogonalSize: number, force?: boolean) {
const different =
force ||
size !== this._size ||
orthogonalSize !== this._orthogonalSize
if (!different) {
return
}
this.element.style.height = `${orthogonalSize}px`
this.element.style.width = `${size}px`
this._size = size
this._orthogonalSize = orthogonalSize
this.gridview.layout(size, orthogonalSize)
} }
private toTarget( private toTarget(
@ -380,31 +251,24 @@ export class ComponentGridview
) { ) {
switch (direction) { switch (direction) {
case 'left': case 'left':
return Position.Left return Position.Left;
case 'right': case 'right':
return Position.Right return Position.Right;
case 'above': case 'above':
return Position.Top return Position.Top;
case 'below': case 'below':
return Position.Bottom return Position.Bottom;
case 'within': case 'within':
default: default:
return Position.Center return Position.Center;
} }
} }
public dispose() { public dispose() {
super.dispose() super.dispose();
this.gridview.dispose() this.debugContainer?.dispose();
this.debugContainer?.dispose() this._onDidLayoutChange.dispose();
if (this.resizeTimer) {
clearInterval(this.resizeTimer)
this.resizeTimer = undefined
}
this._onDidLayoutChange.dispose()
} }
} }

View File

@ -1,59 +1,59 @@
import { CompositeDisposable } from '../../../lifecycle' import { CompositeDisposable } from '../../../lifecycle';
import { Layout } from '../../layout' import { Layout } from '../../layout';
import { GroupChangeKind } from '../../../groupview/groupview' import { GroupChangeKind } from '../../../groupview/groupview';
export class DebugWidget extends CompositeDisposable { export class DebugWidget extends CompositeDisposable {
private _element: HTMLElement private _element: HTMLElement;
constructor(private layout: Layout) { constructor(private layout: Layout) {
super() super();
let container = document.getElementById('layout-debug-container') let container = document.getElementById('layout-debug-container');
if (!container) { if (!container) {
container = document.createElement('div') container = document.createElement('div');
container.id = 'layout-debug-container' container.id = 'layout-debug-container';
container.className = 'layout-debug-container' container.className = 'layout-debug-container';
document.body.appendChild(container) document.body.appendChild(container);
} }
this._element = document.createElement('div') this._element = document.createElement('div');
this._element.innerHTML = this._element.innerHTML =
`<div class='layout-debug-widget'>` + `<div class='layout-debug-widget'>` +
`<div class='layout-debug-widget-row'><span>Groups:</span><span id='group-count'>0</span></div>` + `<div class='layout-debug-widget-row'><span>Groups:</span><span id='group-count'>0</span></div>` +
`<div class='layout-debug-widget-row'><span>Panels:</span><span id='panel-count'>0</span></div>` + `<div class='layout-debug-widget-row'><span>Panels:</span><span id='panel-count'>0</span></div>` +
`</div>` `</div>`;
container.appendChild(this._element) container.appendChild(this._element);
const gc = this._element.querySelector('#group-count') const gc = this._element.querySelector('#group-count');
const pc = this._element.querySelector('#panel-count') const pc = this._element.querySelector('#panel-count');
const events = [ const events = [
GroupChangeKind.PANEL_CREATED, GroupChangeKind.PANEL_CREATED,
GroupChangeKind.PANEL_DESTROYED, GroupChangeKind.PANEL_DESTROYED,
GroupChangeKind.ADD_GROUP, GroupChangeKind.ADD_GROUP,
GroupChangeKind.REMOVE_GROUP, GroupChangeKind.REMOVE_GROUP,
] ];
this.addDisposables( this.addDisposables(
this.layout.onDidLayoutChange((event) => { this.layout.onDidLayoutChange((event) => {
if (events.includes(event.kind)) { if (events.includes(event.kind)) {
gc.textContent = this.layout.size.toString() gc.textContent = this.layout.size.toString();
pc.textContent = this.layout.totalPanels.toString() pc.textContent = this.layout.totalPanels.toString();
} }
}) })
) );
} }
public dispose() { public dispose() {
super.dispose() super.dispose();
this._element.remove() this._element.remove();
const container = document.getElementById('layout-debug-container') const container = document.getElementById('layout-debug-container');
if (container && container.children.length === 0) { if (container && container.children.length === 0) {
container.remove() container.remove();
} }
} }
} }

View File

@ -1,96 +1,96 @@
import { CompositeDisposable, MutableDisposable } from '../../../lifecycle' import { CompositeDisposable, MutableDisposable } from '../../../lifecycle';
import { import {
PanelHeaderPart, PanelHeaderPart,
PartInitParameters, PartInitParameters,
} from '../../../groupview/panel/parts' } from '../../../groupview/panel/parts';
import { addDisposableListener } from '../../../events' import { addDisposableListener } from '../../../events';
import { toggleClass } from '../../../dom' import { toggleClass } from '../../../dom';
export class DefaultTab extends CompositeDisposable implements PanelHeaderPart { export class DefaultTab extends CompositeDisposable implements PanelHeaderPart {
private _element: HTMLElement private _element: HTMLElement;
private _isGroupActive: boolean private _isGroupActive: boolean;
private _isPanelVisible: boolean private _isPanelVisible: boolean;
// //
private _content: HTMLElement private _content: HTMLElement;
private _actionContainer: HTMLElement private _actionContainer: HTMLElement;
private _list: HTMLElement private _list: HTMLElement;
private action: HTMLElement private action: HTMLElement;
// //
private params: PartInitParameters private params: PartInitParameters;
// //
private isDirtyDisposable = new MutableDisposable() private isDirtyDisposable = new MutableDisposable();
get element() { get element() {
return this._element return this._element;
} }
get id() { get id() {
return '__DEFAULT_TAB__' return '__DEFAULT_TAB__';
} }
constructor() { constructor() {
super() super();
this._element = document.createElement('div') this._element = document.createElement('div');
this._element.className = 'default-tab' this._element.className = 'default-tab';
// //
this._content = document.createElement('div') this._content = document.createElement('div');
this._content.className = 'tab-content' this._content.className = 'tab-content';
// //
this._actionContainer = document.createElement('div') this._actionContainer = document.createElement('div');
this._actionContainer.className = 'action-container' this._actionContainer.className = 'action-container';
// //
this._list = document.createElement('ul') this._list = document.createElement('ul');
this._list.className = 'tab-list' this._list.className = 'tab-list';
// //
this.action = document.createElement('a') this.action = document.createElement('a');
this.action.className = 'tab-action' this.action.className = 'tab-action';
// //
this._element.appendChild(this._content) this._element.appendChild(this._content);
this._element.appendChild(this._actionContainer) this._element.appendChild(this._actionContainer);
this._actionContainer.appendChild(this._list) this._actionContainer.appendChild(this._list);
this._list.appendChild(this.action) this._list.appendChild(this.action);
// //
this.addDisposables( this.addDisposables(
addDisposableListener(this._actionContainer, 'mousedown', (ev) => { addDisposableListener(this._actionContainer, 'mousedown', (ev) => {
ev.preventDefault() ev.preventDefault();
}) })
) );
this.render() this.render();
} }
public toJSON() { public toJSON() {
return { id: this.id } return { id: this.id };
} }
public init(params: PartInitParameters) { public init(params: PartInitParameters) {
this.params = params this.params = params;
this._content.textContent = params.title this._content.textContent = params.title;
this.isDirtyDisposable.value = this.params.api.onDidDirtyChange( this.isDirtyDisposable.value = this.params.api.onDidDirtyChange(
(event) => { (event) => {
const isDirty = event const isDirty = event;
toggleClass(this.action, 'dirty', isDirty) toggleClass(this.action, 'dirty', isDirty);
} }
) );
if (!this.params.suppressClosable) { if (!this.params.suppressClosable) {
addDisposableListener(this.action, 'click', (ev) => { addDisposableListener(this.action, 'click', (ev) => {
ev.preventDefault() // ev.preventDefault(); //
this.params.api.close() this.params.api.close();
}) });
} else { } else {
this.action.classList.add('disable-close') this.action.classList.add('disable-close');
} }
} }
public setVisible(isPanelVisible: boolean, isGroupVisible: boolean) { public setVisible(isPanelVisible: boolean, isGroupVisible: boolean) {
this._isPanelVisible = isPanelVisible this._isPanelVisible = isPanelVisible;
this._isGroupActive = isGroupVisible this._isGroupActive = isGroupVisible;
this.render() this.render();
} }
private render() { private render() {

View File

@ -1,81 +1,81 @@
import { import {
WatermarkPart, WatermarkPart,
WatermarkPartInitParameters, WatermarkPartInitParameters,
} from '../../../groupview/panel/parts' } from '../../../groupview/panel/parts';
import { IGroupAccessor } from '../../layout' import { IGroupAccessor } from '../../layout';
import { IGroupview } from '../../../groupview/groupview' import { IGroupview } from '../../../groupview/groupview';
import { ActionContainer } from '../../../groupview/actions/actionsContainer' import { ActionContainer } from '../../../groupview/actions/actionsContainer';
import { addDisposableListener } from '../../../events' import { addDisposableListener } from '../../../events';
import { toggleClass } from '../../../dom' import { toggleClass } from '../../../dom';
import { CompositeDisposable } from '../../../lifecycle' import { CompositeDisposable } from '../../../lifecycle';
export class Watermark extends CompositeDisposable implements WatermarkPart { export class Watermark extends CompositeDisposable implements WatermarkPart {
private _element: HTMLElement private _element: HTMLElement;
private accessor: IGroupAccessor private accessor: IGroupAccessor;
private _visible: boolean private _visible: boolean;
private _group: IGroupview private _group: IGroupview;
constructor() { constructor() {
super() super();
this._element = document.createElement('div') this._element = document.createElement('div');
this._element.className = 'watermark' this._element.className = 'watermark';
const title = document.createElement('div') const title = document.createElement('div');
title.className = 'watermark-title' title.className = 'watermark-title';
const emptySpace = document.createElement('span') const emptySpace = document.createElement('span');
emptySpace.style.flexGrow = '1' emptySpace.style.flexGrow = '1';
const content = document.createElement('div') const content = document.createElement('div');
content.className = 'watermark-content' content.className = 'watermark-content';
this._element.appendChild(title) this._element.appendChild(title);
this._element.appendChild(content) this._element.appendChild(content);
const actions = new ActionContainer() const actions = new ActionContainer();
title.appendChild(emptySpace) title.appendChild(emptySpace);
title.appendChild(actions.element) title.appendChild(actions.element);
const closeAnchor = document.createElement('a') const closeAnchor = document.createElement('a');
closeAnchor.className = 'close-action' closeAnchor.className = 'close-action';
actions.add(closeAnchor) actions.add(closeAnchor);
addDisposableListener(closeAnchor, 'click', (ev) => { addDisposableListener(closeAnchor, 'click', (ev) => {
ev.preventDefault() // ev.preventDefault(); //
this.accessor.removeGroup(this._group) this.accessor.removeGroup(this._group);
}) });
} }
public init(params: WatermarkPartInitParameters) { public init(params: WatermarkPartInitParameters) {
this.accessor = params.accessor this.accessor = params.accessor;
this.addDisposables( this.addDisposables(
this.accessor.onDidLayoutChange((event) => { this.accessor.onDidLayoutChange((event) => {
this.render() this.render();
}) })
) );
this.render() this.render();
} }
public setVisible(visible: boolean, group: IGroupview): void { public setVisible(visible: boolean, group: IGroupview): void {
this._visible = visible this._visible = visible;
this._group = group this._group = group;
this.render() this.render();
} }
get element() { get element() {
return this._element return this._element;
} }
private render() { private render() {
const isOneGroup = this.accessor.size <= 1 const isOneGroup = this.accessor.size <= 1;
toggleClass(this.element, 'has-actions', isOneGroup) toggleClass(this.element, 'has-actions', isOneGroup);
} }
public dispose() { public dispose() {
super.dispose() super.dispose();
} }
} }

View File

@ -1,9 +1,9 @@
import { IGridView, IViewDeserializer } from '../gridview/gridview' import { IGridView, IViewDeserializer } from '../gridview/gridview';
import { IGroupPanel } 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 }): IGroupPanel fromJSON(panelData: { [index: string]: any }): IGroupPanel;
} }
export class DefaultDeserializer implements IViewDeserializer { export class DefaultDeserializer implements IViewDeserializer {
@ -13,22 +13,22 @@ export class DefaultDeserializer implements IViewDeserializer {
) {} ) {}
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: IGroupPanel[] = [] 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);
panels.push(panel) panels.push(panel);
} }
const group = this.layout.createGroup({ const group = this.layout.createGroup({
panels, panels,
activePanel: panels.find((p) => p.id === active), activePanel: panels.find((p) => p.id === active),
}) });
return group return group;
} }
} }

View File

@ -1,3 +1,3 @@
export * from './layout' export * from './layout';
export * from './componentGridview' export * from './componentGridview';
export * from './options' export * from './options';

File diff suppressed because it is too large Load Diff

View File

@ -1,72 +1,91 @@
import { IGroupview } from '../groupview/groupview' import { IGridView } from '../gridview/gridview';
import { IGroupview } from '../groupview/groupview';
import { import {
PanelContentPart, PanelContentPart,
PanelContentPartConstructor, PanelContentPartConstructor,
PanelHeaderPart, PanelHeaderPart,
PanelHeaderPartConstructor, PanelHeaderPartConstructor,
WatermarkConstructor, WatermarkConstructor,
} from '../groupview/panel/parts' } from '../groupview/panel/parts';
import { IGroupPanel } from '../groupview/panel/types' import { IGroupPanel } from '../groupview/panel/types';
import { FrameworkFactory } from '../types' import { Orientation } from '../splitview/splitview';
import { Api } from './layout' import { FrameworkFactory } from '../types';
import { IComponentGridview } from './componentGridview';
import { Api } from './layout';
export interface GroupPanelFrameworkComponentFactory { export interface GroupPanelFrameworkComponentFactory {
content: FrameworkFactory<PanelContentPart> content: FrameworkFactory<PanelContentPart>;
tab: FrameworkFactory<PanelHeaderPart> tab: FrameworkFactory<PanelHeaderPart>;
} }
export interface TabContextMenuEvent { export interface TabContextMenuEvent {
event: MouseEvent event: MouseEvent;
api: Api api: Api;
panel: IGroupPanel panel: IGroupPanel;
}
export interface GridComponentOptions {
orientation: Orientation;
components?: {
[componentName: string]: IComponentGridview;
};
frameworkComponents?: {
[componentName: string]: any;
};
frameworkComponentFactory: any;
tabHeight?: number;
} }
export interface LayoutOptions { export interface LayoutOptions {
tabComponents?: { tabComponents?: {
[componentName: string]: PanelHeaderPartConstructor [componentName: string]: PanelHeaderPartConstructor;
} };
components?: { components?: {
[componentName: string]: PanelContentPartConstructor [componentName: string]: PanelContentPartConstructor;
} };
frameworkTabComponents?: { frameworkTabComponents?: {
[componentName: string]: any [componentName: string]: any;
} };
frameworkComponents?: { frameworkComponents?: {
[componentName: string]: any [componentName: string]: any;
} };
watermarkComponent?: WatermarkConstructor watermarkComponent?: WatermarkConstructor;
watermarkFrameworkComponent?: any watermarkFrameworkComponent?: any;
frameworkComponentFactory: GroupPanelFrameworkComponentFactory frameworkComponentFactory: GroupPanelFrameworkComponentFactory;
tabHeight?: number tabHeight?: number;
debug?: boolean debug?: boolean;
enableExternalDragEvents?: boolean enableExternalDragEvents?: boolean;
} }
export interface PanelOptions { export interface PanelOptions {
componentName: string componentName: string;
tabComponentName?: string tabComponentName?: string;
params?: { [key: string]: any } params?: { [key: string]: any };
id: string id: string;
title?: string title?: string;
suppressClosable?: boolean suppressClosable?: boolean;
} }
export interface AddPanelOptions export interface AddPanelOptions
extends Omit<PanelOptions, 'componentName' | 'tabComponentName'> { extends Omit<PanelOptions, 'componentName' | 'tabComponentName'> {
componentName: string | PanelContentPartConstructor componentName: string | PanelContentPartConstructor;
tabComponentName?: string | PanelHeaderPartConstructor tabComponentName?: string | PanelHeaderPartConstructor;
position?: { position?: {
direction?: 'left' | 'right' | 'above' | 'below' | 'within' direction?: 'left' | 'right' | 'above' | 'below' | 'within';
referencePanel: string referencePanel: string;
} };
} }
export interface AddGroupOptions { export interface AddGroupOptions {
direction?: 'left' | 'right' | 'above' | 'below' direction?: 'left' | 'right' | 'above' | 'below';
referencePanel: string referencePanel: string;
} }
export interface MovementOptions { export interface MovementOptions2 {
group?: IGroupview group?: IGridView;
includePanel?: boolean }
export interface MovementOptions extends MovementOptions2 {
includePanel?: boolean;
group?: IGroupview;
} }

View File

@ -1,56 +1,56 @@
export interface IDisposable { export interface IDisposable {
dispose: () => void dispose: () => void;
} }
export interface IValueDisposable<T> { export interface IValueDisposable<T> {
value: T value: T;
disposable: IDisposable disposable: IDisposable;
} }
export interface ISerializable { export interface ISerializable {
toJSON(): object toJSON(): object;
fromJSON(data: object): void fromJSON(data: object): void;
} }
export namespace Disposable { export namespace Disposable {
export const NONE: IDisposable = { dispose: () => {} } export const NONE: IDisposable = { dispose: () => {} };
} }
export class CompositeDisposable { export class CompositeDisposable {
private disposables: IDisposable[] private disposables: IDisposable[];
public static from(...args: IDisposable[]) { public static from(...args: IDisposable[]) {
return new CompositeDisposable(...args) return new CompositeDisposable(...args);
} }
constructor(...args: IDisposable[]) { constructor(...args: IDisposable[]) {
this.disposables = args this.disposables = args;
} }
public addDisposables(...args: IDisposable[]) { public addDisposables(...args: IDisposable[]) {
args?.forEach((arg) => this.disposables.push(arg)) args?.forEach((arg) => this.disposables.push(arg));
} }
public dispose() { public dispose() {
this.disposables.forEach((arg) => arg.dispose()) this.disposables.forEach((arg) => arg.dispose());
} }
} }
export class MutableDisposable implements IDisposable { export class MutableDisposable implements IDisposable {
private _disposable: IDisposable private _disposable: IDisposable;
constructor() {} constructor() {}
set value(disposable: IDisposable) { set value(disposable: IDisposable) {
if (this._disposable) { if (this._disposable) {
this._disposable.dispose() this._disposable.dispose();
} }
this._disposable = disposable this._disposable = disposable;
} }
public dispose() { public dispose() {
if (this._disposable) { if (this._disposable) {
this._disposable.dispose() this._disposable.dispose();
} }
} }
} }

View File

@ -1,8 +1,8 @@
export const clamp = (value: number, min: number, max: number) => { export const clamp = (value: number, min: number, max: number) => {
return Math.min(max, Math.max(value, min)) return Math.min(max, Math.max(value, min));
} };
export const sequentialNumberGenerator = () => { export const sequentialNumberGenerator = () => {
let value = 1 let value = 1;
return { next: () => (value++).toString() } return { next: () => (value++).toString() };
} };

View File

@ -1,7 +1,7 @@
import { PanelDimensionChangeEvent } from './types' import { PanelDimensionChangeEvent } from './types';
import { Emitter, Event } from '../events' import { Emitter, Event } from '../events';
import { CompositeDisposable, IDisposable } from '../lifecycle' import { CompositeDisposable, IDisposable } from '../lifecycle';
import { FunctionOrValue } from '../types' import { FunctionOrValue } from '../types';
// we've tried to do a bit better than the 'any' type. // we've tried to do a bit better than the 'any' type.
// anything that is serializable JSON should be valid here // anything that is serializable JSON should be valid here
@ -13,74 +13,74 @@ type StateObject =
| null | null
| object | object
| StateObject[] | StateObject[]
| { [key: string]: StateObject } | { [key: string]: StateObject };
interface State { interface State {
[key: string]: StateObject [key: string]: StateObject;
} }
interface ChangeFocusEvent { interface ChangeFocusEvent {
isFocused: boolean isFocused: boolean;
} }
interface PanelConstraintChangeEvent { interface PanelConstraintChangeEvent {
minimumSize?: number | (() => number) minimumSize?: number | (() => number);
maximumSize?: number | (() => number) maximumSize?: number | (() => number);
} }
export interface IBaseViewApi extends IDisposable { export interface IBaseViewApi extends IDisposable {
// events // events
onDidDimensionsChange: Event<PanelDimensionChangeEvent> onDidDimensionsChange: Event<PanelDimensionChangeEvent>;
onDidStateChange: Event<void> onDidStateChange: Event<void>;
onDidFocusChange: Event<ChangeFocusEvent> onDidFocusChange: Event<ChangeFocusEvent>;
// state // state
setState(key: string, value: StateObject): void setState(key: string, value: StateObject): void;
setState(state: State): void setState(state: State): void;
getState: () => State getState: () => State;
getStateKey: <T extends StateObject>(key: string) => T getStateKey: <T extends StateObject>(key: string) => T;
// //
readonly isFocused: boolean readonly isFocused: boolean;
} }
/** /**
* A core api implementation that should be used across all panel-like objects * A core api implementation that should be used across all panel-like objects
*/ */
export class BaseViewApi extends CompositeDisposable implements IBaseViewApi { export class BaseViewApi extends CompositeDisposable implements IBaseViewApi {
private _state: State = {} private _state: State = {};
private _isFocused: boolean private _isFocused: boolean;
readonly _onDidStateChange = new Emitter<void>() readonly _onDidStateChange = new Emitter<void>();
readonly onDidStateChange: Event<void> = this._onDidStateChange.event readonly onDidStateChange: Event<void> = this._onDidStateChange.event;
// //
readonly _onDidPanelDimensionChange = new Emitter< readonly _onDidPanelDimensionChange = new Emitter<
PanelDimensionChangeEvent PanelDimensionChangeEvent
>({ >({
emitLastValue: true, emitLastValue: true,
}) });
readonly onDidDimensionsChange = this._onDidPanelDimensionChange.event readonly onDidDimensionsChange = this._onDidPanelDimensionChange.event;
// //
readonly _onDidChangeFocus = new Emitter<ChangeFocusEvent>({ readonly _onDidChangeFocus = new Emitter<ChangeFocusEvent>({
emitLastValue: true, emitLastValue: true,
}) });
readonly onDidFocusChange: Event<ChangeFocusEvent> = this._onDidChangeFocus readonly onDidFocusChange: Event<ChangeFocusEvent> = this._onDidChangeFocus
.event .event;
// //
get isFocused() { get isFocused() {
return this._isFocused return this._isFocused;
} }
constructor() { constructor() {
super() super();
this.addDisposables( this.addDisposables(
this._onDidStateChange, this._onDidStateChange,
this._onDidChangeFocus, this._onDidChangeFocus,
this._onDidPanelDimensionChange, this._onDidPanelDimensionChange,
this.onDidFocusChange((event) => { this.onDidFocusChange((event) => {
this._isFocused = event.isFocused this._isFocused = event.isFocused;
}) })
) );
} }
public setState( public setState(
@ -88,76 +88,76 @@ export class BaseViewApi extends CompositeDisposable implements IBaseViewApi {
value?: StateObject value?: StateObject
) { ) {
if (typeof key === 'object') { if (typeof key === 'object') {
this._state = key this._state = key;
} else { } else {
this._state[key] = value this._state[key] = value;
} }
this._onDidStateChange.fire(undefined) this._onDidStateChange.fire(undefined);
} }
public getState(): State { public getState(): State {
return this._state return this._state;
} }
public getStateKey<T extends StateObject>(key: string): T { public getStateKey<T extends StateObject>(key: string): T {
return this._state[key] as T return this._state[key] as T;
} }
public dispose() { public dispose() {
super.dispose() super.dispose();
} }
} }
interface PanelConstraintChangeEvent { interface PanelConstraintChangeEvent {
minimumSize?: FunctionOrValue<number> minimumSize?: FunctionOrValue<number>;
maximumSize?: FunctionOrValue<number> maximumSize?: FunctionOrValue<number>;
} }
export interface IPanelApi extends IBaseViewApi { export interface IPanelApi extends IBaseViewApi {
onDidConstraintsChange: Event<PanelConstraintChangeEvent> onDidConstraintsChange: Event<PanelConstraintChangeEvent>;
setConstraints(value: PanelConstraintChangeEvent): void setConstraints(value: PanelConstraintChangeEvent): void;
} }
export class PanelApi extends BaseViewApi implements IBaseViewApi { export class PanelApi extends BaseViewApi implements IBaseViewApi {
readonly _onDidConstraintsChange = new Emitter<PanelConstraintChangeEvent>({ readonly _onDidConstraintsChange = new Emitter<PanelConstraintChangeEvent>({
emitLastValue: true, emitLastValue: true,
}) });
readonly onDidConstraintsChange: Event<PanelConstraintChangeEvent> = this readonly onDidConstraintsChange: Event<PanelConstraintChangeEvent> = this
._onDidConstraintsChange.event ._onDidConstraintsChange.event;
constructor() { constructor() {
super() super();
} }
public setConstraints(value: PanelConstraintChangeEvent) { public setConstraints(value: PanelConstraintChangeEvent) {
this._onDidConstraintsChange.fire(value) this._onDidConstraintsChange.fire(value);
} }
} }
interface GridConstraintChangeEvent { interface GridConstraintChangeEvent {
minimumWidth?: FunctionOrValue<number> minimumWidth?: FunctionOrValue<number>;
minimumHeight?: FunctionOrValue<number> minimumHeight?: FunctionOrValue<number>;
maximumWidth?: FunctionOrValue<number> maximumWidth?: FunctionOrValue<number>;
maximumHeight?: FunctionOrValue<number> maximumHeight?: FunctionOrValue<number>;
} }
export interface IGridApi extends IBaseViewApi { export interface IGridApi extends IBaseViewApi {
onDidConstraintsChange: Event<GridConstraintChangeEvent> onDidConstraintsChange: Event<GridConstraintChangeEvent>;
setConstraints(value: GridConstraintChangeEvent): void setConstraints(value: GridConstraintChangeEvent): void;
} }
export class GridApi extends BaseViewApi implements IBaseViewApi { export class GridApi extends BaseViewApi implements IBaseViewApi {
readonly _onDidConstraintsChange = new Emitter<GridConstraintChangeEvent>({ readonly _onDidConstraintsChange = new Emitter<GridConstraintChangeEvent>({
emitLastValue: true, emitLastValue: true,
}) });
readonly onDidConstraintsChange: Event<GridConstraintChangeEvent> = this readonly onDidConstraintsChange: Event<GridConstraintChangeEvent> = this
._onDidConstraintsChange.event ._onDidConstraintsChange.event;
constructor() { constructor() {
super() super();
} }
public setConstraints(value: GridConstraintChangeEvent) { public setConstraints(value: GridConstraintChangeEvent) {
this._onDidConstraintsChange.fire(value) this._onDidConstraintsChange.fire(value);
} }
} }

View File

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

View File

@ -1,151 +1,151 @@
import { SplitView, IView, Orientation } from '../splitview/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';
export interface IPaneOptions { export interface IPaneOptions {
minimumBodySize?: number minimumBodySize?: number;
maximumBodySize?: number maximumBodySize?: number;
orientation?: Orientation orientation?: Orientation;
isExpanded?: boolean isExpanded?: boolean;
} }
export abstract class Pane implements IView { export abstract class Pane implements IView {
public element: HTMLElement public element: HTMLElement;
private header: HTMLElement private header: HTMLElement;
private body: HTMLElement private body: HTMLElement;
private _onDidChangeExpansionState: Emitter<boolean> = new Emitter< private _onDidChangeExpansionState: Emitter<boolean> = new Emitter<
boolean boolean
>() >();
public onDidChangeExpansionState = this._onDidChangeExpansionState.event public onDidChangeExpansionState = this._onDidChangeExpansionState.event;
private _onDidChange: Emitter<number | undefined> = new Emitter< private _onDidChange: Emitter<number | undefined> = new Emitter<
number | undefined number | undefined
>() >();
public onDidChange = this._onDidChange.event public onDidChange = this._onDidChange.event;
private _minimumBodySize: number private _minimumBodySize: number;
private _maximumBodySize: number private _maximumBodySize: number;
private _minimumSize: number private _minimumSize: number;
private _maximumSize: number private _maximumSize: number;
private _isExpanded: boolean private _isExpanded: boolean;
private _orientation: Orientation private _orientation: Orientation;
private _orthogonalSize: number private _orthogonalSize: number;
private animationTimer: NodeJS.Timeout private animationTimer: NodeJS.Timeout;
private expandedSize: number private expandedSize: number;
private headerSize = 22 private headerSize = 22;
constructor(options: IPaneOptions) { constructor(options: IPaneOptions) {
this.element = document.createElement('div') this.element = document.createElement('div');
this.element.className = 'pane' this.element.className = 'pane';
this._minimumBodySize = this._minimumBodySize =
typeof options.minimumBodySize === 'number' typeof options.minimumBodySize === 'number'
? options.minimumBodySize ? options.minimumBodySize
: 120 : 120;
this._maximumBodySize = this._maximumBodySize =
typeof options.maximumBodySize === 'number' typeof options.maximumBodySize === 'number'
? options.maximumBodySize ? options.maximumBodySize
: Number.POSITIVE_INFINITY : Number.POSITIVE_INFINITY;
this._isExpanded = options.isExpanded this._isExpanded = options.isExpanded;
this.orientation = options.orientation this.orientation = options.orientation;
} }
public get minimumSize(): number { public get minimumSize(): number {
const headerSize = this.headerSize const headerSize = this.headerSize;
const expanded = this.isExpanded() const expanded = this.isExpanded();
const minimumBodySize = expanded const minimumBodySize = expanded
? this._minimumBodySize ? this._minimumBodySize
: this._orientation === Orientation.HORIZONTAL : this._orientation === Orientation.HORIZONTAL
? 50 ? 50
: 0 : 0;
return headerSize + minimumBodySize return headerSize + minimumBodySize;
} }
public get maximumSize(): number { public get maximumSize(): number {
const headerSize = this.headerSize const headerSize = this.headerSize;
const expanded = this.isExpanded() const expanded = this.isExpanded();
const maximumBodySize = expanded const maximumBodySize = expanded
? this._maximumBodySize ? this._maximumBodySize
: this._orientation === Orientation.HORIZONTAL : this._orientation === Orientation.HORIZONTAL
? 50 ? 50
: 0 : 0;
return headerSize + maximumBodySize return headerSize + maximumBodySize;
} }
public isExpanded() { public isExpanded() {
return this._isExpanded return this._isExpanded;
} }
public get orientation() { public get orientation() {
return this._orientation return this._orientation;
} }
public get orthogonalSize() { public get orthogonalSize() {
return this._orthogonalSize return this._orthogonalSize;
} }
public set minimumSize(size: number) { public set minimumSize(size: number) {
this._minimumSize = size this._minimumSize = size;
this._onDidChange.fire(undefined) this._onDidChange.fire(undefined);
} }
public set maximumSize(size: number) { public set maximumSize(size: number) {
this._maximumSize = size this._maximumSize = size;
this._onDidChange.fire(undefined) this._onDidChange.fire(undefined);
} }
public setExpanded(expanded: boolean) { public setExpanded(expanded: boolean) {
this._isExpanded = expanded this._isExpanded = expanded;
if (expanded) { if (expanded) {
if (this.animationTimer) { if (this.animationTimer) {
clearTimeout(this.animationTimer) clearTimeout(this.animationTimer);
} }
this.element.appendChild(this.body) this.element.appendChild(this.body);
} else { } else {
this.animationTimer = setTimeout(() => { this.animationTimer = setTimeout(() => {
this.body.remove() this.body.remove();
}, 200) }, 200);
} }
this._onDidChangeExpansionState.fire(expanded) this._onDidChangeExpansionState.fire(expanded);
this._onDidChange.fire(expanded ? this.expandedSize : undefined) this._onDidChange.fire(expanded ? this.expandedSize : undefined);
} }
public set orientation(orientation: Orientation) { public set orientation(orientation: Orientation) {
this._orientation = orientation this._orientation = orientation;
} }
public set orthogonalSize(size: number) { public set orthogonalSize(size: number) {
this._orthogonalSize = size this._orthogonalSize = size;
} }
public layout(size: number, orthogonalSize: number) { public layout(size: number, orthogonalSize: number) {
if (this.isExpanded()) { if (this.isExpanded()) {
this.expandedSize = size this.expandedSize = size;
} }
} }
public render() { public render() {
this.header = document.createElement('div') this.header = document.createElement('div');
this.header.className = 'pane-header' this.header.className = 'pane-header';
this.header.style.height = `${this.headerSize}px` this.header.style.height = `${this.headerSize}px`;
this.header.style.lineHeight = `${this.headerSize}px` this.header.style.lineHeight = `${this.headerSize}px`;
this.element.appendChild(this.header) this.element.appendChild(this.header);
this.renderHeader(this.header) this.renderHeader(this.header);
// this.updateHeader(); // this.updateHeader();
this.body = document.createElement('div') this.body = document.createElement('div');
this.body.className = 'pane-body' this.body.className = 'pane-body';
this.element.appendChild(this.body) this.element.appendChild(this.body);
this.renderBody(this.body) this.renderBody(this.body);
// if (!this.isExpanded()) { // if (!this.isExpanded()) {
// this.body.remove(); // this.body.remove();
@ -174,103 +174,103 @@ export abstract class Pane implements IView {
// this._dropBackground = this.styles.dropBackground; // this._dropBackground = this.styles.dropBackground;
// } // }
protected abstract renderHeader(container: HTMLElement): void protected abstract renderHeader(container: HTMLElement): void;
protected abstract renderBody(container: HTMLElement): void protected abstract renderBody(container: HTMLElement): void;
} }
interface PaneItem { interface PaneItem {
pane: Pane pane: Pane;
disposable: IDisposable disposable: IDisposable;
} }
export class PaneView implements IDisposable { export class PaneView implements IDisposable {
private element: HTMLElement private element: HTMLElement;
private splitview: SplitView private splitview: SplitView;
private paneItems: PaneItem[] = [] private paneItems: PaneItem[] = [];
private _orientation: Orientation private _orientation: Orientation;
private animationTimer: NodeJS.Timeout private animationTimer: NodeJS.Timeout;
private orthogonalSize: number private orthogonalSize: number;
private size: number private size: number;
constructor(container: HTMLElement, options: { orientation: Orientation }) { constructor(container: HTMLElement, options: { orientation: Orientation }) {
this._orientation = options.orientation ?? Orientation.VERTICAL this._orientation = options.orientation ?? Orientation.VERTICAL;
this.element = document.createElement('div') this.element = document.createElement('div');
this.element.className = 'pane-container' this.element.className = 'pane-container';
this.setupAnimation = this.setupAnimation.bind(this) this.setupAnimation = this.setupAnimation.bind(this);
container.appendChild(this.element) container.appendChild(this.element);
this.splitview = new SplitView(this.element, { this.splitview = new SplitView(this.element, {
orientation: this._orientation, orientation: this._orientation,
}) });
} }
public setOrientation(orientation: Orientation) { public setOrientation(orientation: Orientation) {
this._orientation = orientation this._orientation = orientation;
} }
public addPane(pane: Pane, size?: number, index = this.splitview.length) { public addPane(pane: Pane, size?: number, index = this.splitview.length) {
const disposable = pane.onDidChangeExpansionState(this.setupAnimation) const disposable = pane.onDidChangeExpansionState(this.setupAnimation);
const paneItem: PaneItem = { const paneItem: PaneItem = {
pane, pane,
disposable: { disposable: {
dispose: () => { dispose: () => {
disposable.dispose() disposable.dispose();
}, },
}, },
} };
this.paneItems.splice(index, 0, paneItem) this.paneItems.splice(index, 0, paneItem);
pane.orientation = this._orientation pane.orientation = this._orientation;
pane.orthogonalSize = this.orthogonalSize pane.orthogonalSize = this.orthogonalSize;
this.splitview.addView(pane, size, index) this.splitview.addView(pane, size, index);
} }
public getPanes() { public getPanes() {
return this.splitview.getViews() as Pane[] return this.splitview.getViews() as Pane[];
} }
public removePane(index: number) { public removePane(index: number) {
this.splitview.removeView(index) this.splitview.removeView(index);
const paneItem = this.paneItems.splice(index, 1)[0] const paneItem = this.paneItems.splice(index, 1)[0];
paneItem.disposable.dispose() paneItem.disposable.dispose();
return paneItem return paneItem;
} }
public moveView(from: number, to: number) { public moveView(from: number, to: number) {
const view = this.removePane(from) const view = this.removePane(from);
this.addPane(view.pane, to) this.addPane(view.pane, to);
} }
public layout(size: number, orthogonalSize: number): void { public layout(size: number, orthogonalSize: number): void {
this.orthogonalSize = orthogonalSize this.orthogonalSize = orthogonalSize;
this.size = size this.size = size;
for (const paneItem of this.paneItems) { for (const paneItem of this.paneItems) {
paneItem.pane.orthogonalSize = this.orthogonalSize paneItem.pane.orthogonalSize = this.orthogonalSize;
} }
this.splitview.layout(this.size, this.orthogonalSize) this.splitview.layout(this.size, this.orthogonalSize);
} }
private setupAnimation() { private setupAnimation() {
if (this.animationTimer) { if (this.animationTimer) {
clearTimeout(this.animationTimer) clearTimeout(this.animationTimer);
} }
addClasses(this.element, 'animated') addClasses(this.element, 'animated');
this.animationTimer = setTimeout(() => { this.animationTimer = setTimeout(() => {
this.animationTimer = undefined this.animationTimer = undefined;
removeClasses(this.element, 'animated') removeClasses(this.element, 'animated');
}, 200) }, 200);
} }
public dispose() { public dispose() {
this.paneItems.forEach((paneItem) => { this.paneItems.forEach((paneItem) => {
paneItem.disposable.dispose() paneItem.disposable.dispose();
}) });
} }
} }

View File

@ -1,48 +1,48 @@
import { IGroupPanel } 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';
import { IPanelDeserializer } from '../layout/deserializer' import { IPanelDeserializer } from '../layout/deserializer';
import { import {
createContentComponent, createContentComponent,
createTabComponent, createTabComponent,
} from '../layout/componentFactory' } from '../layout/componentFactory';
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 }): IGroupPanel { 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;
const props = panelData.props const props = panelData.props;
const title = panelData.title const title = panelData.title;
const state = panelData.state const state = panelData.state;
const suppressClosable = panelData.suppressClosable const suppressClosable = panelData.suppressClosable;
const contentPart = createContentComponent( const contentPart = createContentComponent(
content.id, content.id,
this.layout.options.components, this.layout.options.components,
this.layout.options.frameworkComponents, this.layout.options.frameworkComponents,
this.layout.options.frameworkComponentFactory.content 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.frameworkComponentFactory, this.layout.options.frameworkComponentFactory,
this.layout.options.frameworkComponentFactory.tab this.layout.options.frameworkComponentFactory.tab
) as PanelHeaderPart ) as PanelHeaderPart;
const panel = new DefaultPanel(panelId, headerPart, contentPart) const panel = new DefaultPanel(panelId, headerPart, contentPart);
panel.init({ panel.init({
title, title,
suppressClosable, suppressClosable,
params: props || {}, params: props || {},
state: state || {}, state: state || {},
}) });
return panel return panel;
} }
} }

View File

@ -1,43 +1,43 @@
import * as React from 'react' import * as React from 'react';
import { import {
ComponentGridview, ComponentGridview,
IComponentGridviewLayout, IComponentGridviewLayout,
} from '../layout/componentGridview' } from '../layout/componentGridview';
import { IGridApi } from '../panel/api' import { IGridApi } from '../panel/api';
import { Orientation } from '../splitview/splitview' import { Orientation } from '../splitview/splitview';
import { ReactComponentGridView } from './reactComponentGridView' import { ReactComponentGridView } from './reactComponentGridView';
export interface GridviewReadyEvent { export interface GridviewReadyEvent {
api: IComponentGridviewLayout api: IComponentGridviewLayout;
} }
export interface IGridviewPanelProps { export interface IGridviewPanelProps {
api: IGridApi api: IGridApi;
} }
export interface IGridviewComponentProps { export interface IGridviewComponentProps {
orientation: Orientation orientation: Orientation;
onReady?: (event: GridviewReadyEvent) => void onReady?: (event: GridviewReadyEvent) => void;
components: { components: {
[index: string]: React.FunctionComponent<IGridviewPanelProps> [index: string]: React.FunctionComponent<IGridviewPanelProps>;
} };
} }
export const GridviewComponent = (props: IGridviewComponentProps) => { export const GridviewComponent = (props: IGridviewComponentProps) => {
const domReference = React.useRef<HTMLDivElement>() const domReference = React.useRef<HTMLDivElement>();
const gridview = React.useRef<IComponentGridviewLayout>() const gridview = React.useRef<IComponentGridviewLayout>();
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) => {
setPortals((portals) => [...portals, p]) setPortals((portals) => [...portals, p]);
return { return {
dispose: () => { dispose: () => {
setPortals((portals) => setPortals((portals) =>
portals.filter((portal) => portal !== p) portals.filter((portal) => portal !== p)
) );
}, },
} };
}, []) }, []);
React.useEffect(() => { React.useEffect(() => {
gridview.current = new ComponentGridview(domReference.current, { gridview.current = new ComponentGridview(domReference.current, {
@ -47,15 +47,15 @@ export const GridviewComponent = (props: IGridviewComponentProps) => {
createComponent: (id: string, component: any) => { createComponent: (id: string, component: any) => {
return new ReactComponentGridView(id, id, component, { return new ReactComponentGridView(id, id, component, {
addPortal, addPortal,
}) });
}, },
}, },
}) });
if (props.onReady) { if (props.onReady) {
props.onReady({ api: gridview.current }) props.onReady({ api: gridview.current });
} }
}, []) }, []);
return ( return (
<div <div
@ -67,5 +67,5 @@ export const GridviewComponent = (props: IGridviewComponentProps) => {
> >
{portals} {portals}
</div> </div>
) );
} };

View File

@ -1,66 +1,66 @@
import * as React from 'react' import * as React from 'react';
import { IDisposable } from '../lifecycle' import { IDisposable } from '../lifecycle';
import { Layout, Api } from '../layout/layout' import { Layout, Api } from '../layout/layout';
import { ReactPanelContentPart } from './reactContentPart' 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 { import {
GroupPanelFrameworkComponentFactory, GroupPanelFrameworkComponentFactory,
TabContextMenuEvent, TabContextMenuEvent,
} from '../layout/options' } from '../layout/options';
export interface OnReadyEvent { export interface OnReadyEvent {
api: Api api: Api;
} }
export interface ReactLayout { export interface ReactLayout {
addPortal: (portal: React.ReactPortal) => IDisposable addPortal: (portal: React.ReactPortal) => IDisposable;
} }
export interface IReactGridProps { export interface IReactGridProps {
components?: { components?: {
[componentName: string]: React.FunctionComponent<IPanelProps> [componentName: string]: React.FunctionComponent<IPanelProps>;
} };
tabComponents?: { tabComponents?: {
[componentName: string]: React.FunctionComponent<IPanelProps> [componentName: string]: React.FunctionComponent<IPanelProps>;
} };
watermarkComponent?: React.FunctionComponent watermarkComponent?: React.FunctionComponent;
onReady?: (event: OnReadyEvent) => void onReady?: (event: OnReadyEvent) => void;
autoSizeToFitContainer?: boolean autoSizeToFitContainer?: boolean;
serializedLayout?: {} serializedLayout?: {};
deserializer?: { deserializer?: {
fromJSON: ( fromJSON: (
data: any data: any
) => { ) => {
component: React.FunctionComponent<IPanelProps> component: React.FunctionComponent<IPanelProps>;
tabComponent?: React.FunctionComponent<IPanelProps> tabComponent?: React.FunctionComponent<IPanelProps>;
props?: { [key: string]: any } props?: { [key: string]: any };
} };
} };
debug?: boolean debug?: boolean;
tabHeight?: number tabHeight?: number;
enableExternalDragEvents?: boolean enableExternalDragEvents?: boolean;
onTabContextMenu?: (event: TabContextMenuEvent) => void onTabContextMenu?: (event: TabContextMenuEvent) => void;
} }
export const ReactGrid = (props: IReactGridProps) => { export const ReactGrid = (props: IReactGridProps) => {
const domReference = React.useRef<HTMLDivElement>() const domReference = React.useRef<HTMLDivElement>();
const layoutReference = React.useRef<Layout>() const layoutReference = React.useRef<Layout>();
const [portals, setPortals] = React.useState<React.ReactPortal[]>([]) const [portals, setPortals] = React.useState<React.ReactPortal[]>([]);
React.useEffect(() => { React.useEffect(() => {
const addPortal = (p: React.ReactPortal) => { const addPortal = (p: React.ReactPortal) => {
setPortals((portals) => [...portals, p]) setPortals((portals) => [...portals, p]);
return { return {
dispose: () => { dispose: () => {
setPortals((portals) => setPortals((portals) =>
portals.filter((portal) => portal !== p) portals.filter((portal) => portal !== p)
) );
}, },
} };
} };
const factory: GroupPanelFrameworkComponentFactory = { const factory: GroupPanelFrameworkComponentFactory = {
content: { content: {
@ -70,7 +70,7 @@ export const ReactGrid = (props: IReactGridProps) => {
) => { ) => {
return new ReactPanelContentPart(id, component, { return new ReactPanelContentPart(id, component, {
addPortal, addPortal,
}) });
}, },
}, },
tab: { tab: {
@ -80,53 +80,57 @@ export const ReactGrid = (props: IReactGridProps) => {
) => { ) => {
return new ReactPanelHeaderPart(id, component, { return new ReactPanelHeaderPart(id, component, {
addPortal, addPortal,
}) });
}, },
}, },
} };
const layout = new Layout({ const element = document.createElement('div');
const layout = new Layout(element, {
frameworkComponentFactory: factory, frameworkComponentFactory: factory,
frameworkComponents: props.components, frameworkComponents: props.components,
frameworkTabComponents: props.tabComponents, frameworkTabComponents: props.tabComponents,
tabHeight: props.tabHeight, tabHeight: props.tabHeight,
debug: props.debug, debug: props.debug,
enableExternalDragEvents: props.enableExternalDragEvents, enableExternalDragEvents: props.enableExternalDragEvents,
}) });
layoutReference.current = layout layoutReference.current = layout;
domReference.current.appendChild(layoutReference.current.element) domReference.current.appendChild(layoutReference.current.element);
layout.deserializer = new ReactPanelDeserialzier(layout) layout.deserializer = new ReactPanelDeserialzier(layout);
layout.resizeToFit() layout.resizeToFit();
if (props.serializedLayout) { if (props.serializedLayout) {
layout.deserialize(props.serializedLayout) layout.deserialize(props.serializedLayout);
} }
if (props.onReady) { if (props.onReady) {
props.onReady({ api: layout }) props.onReady({ api: layout });
} }
return () => { return () => {
layout.dispose() layout.dispose();
} };
}, []) }, []);
React.useEffect(() => { React.useEffect(() => {
const disposable = layoutReference.current.onTabContextMenu((event) => { const disposable = layoutReference.current.onTabContextMenu((event) => {
props.onTabContextMenu(event) props.onTabContextMenu(event);
}) });
return () => { return () => {
disposable.dispose() disposable.dispose();
} };
}, [props.onTabContextMenu]) }, [props.onTabContextMenu]);
React.useEffect(() => { React.useEffect(() => {
layoutReference.current.setAutoResizeToFit(props.autoSizeToFitContainer) layoutReference.current.setAutoResizeToFit(
}, [props.autoSizeToFitContainer]) props.autoSizeToFitContainer
);
}, [props.autoSizeToFitContainer]);
return ( return (
<div <div
@ -138,5 +142,5 @@ export const ReactGrid = (props: IReactGridProps) => {
> >
{portals} {portals}
</div> </div>
) );
} };

View File

@ -1,61 +1,61 @@
import * as React from 'react' import * as React from 'react';
import * as ReactDOM from 'react-dom' import * as ReactDOM from 'react-dom';
import { IDisposable } from '../lifecycle' import { IDisposable } from '../lifecycle';
import { IGroupPanelApi } from '../groupview/panel/api' import { IGroupPanelApi } from '../groupview/panel/api';
import { sequentialNumberGenerator } from '../math' import { sequentialNumberGenerator } from '../math';
import { IBaseViewApi } from '../panel/api' import { IBaseViewApi } from '../panel/api';
export interface IPanelProps { export interface IPanelProps {
api: IGroupPanelApi api: IGroupPanelApi;
} }
interface IPanelWrapperProps { interface IPanelWrapperProps {
component: React.FunctionComponent<IPanelProps> component: React.FunctionComponent<IPanelProps>;
componentProps: any componentProps: any;
} }
interface IPanelWrapperRef { interface IPanelWrapperRef {
update: (props: { [key: string]: any }) => void update: (props: { [key: string]: any }) => void;
} }
const PanelWrapper = React.forwardRef( const PanelWrapper = React.forwardRef(
(props: IPanelWrapperProps, ref: React.RefObject<IPanelWrapperRef>) => { (props: IPanelWrapperProps, ref: React.RefObject<IPanelWrapperRef>) => {
const [_, triggerRender] = React.useState<number>() const [_, triggerRender] = React.useState<number>();
const _props = React.useRef<{ [key: string]: any }>( const _props = React.useRef<{ [key: string]: any }>(
props.componentProps props.componentProps
) );
React.useImperativeHandle( React.useImperativeHandle(
ref, ref,
() => ({ () => ({
update: (props: { [key: string]: any }) => { update: (props: { [key: string]: any }) => {
_props.current = { ..._props.current, ...props } _props.current = { ..._props.current, ...props };
triggerRender(Date.now()) triggerRender(Date.now());
}, },
}), }),
[] []
) );
React.useEffect(() => { React.useEffect(() => {
console.debug('[reactwrapper] component mounted ') console.debug('[reactwrapper] component mounted ');
return () => { return () => {
console.debug('[reactwrapper] component unmounted ') console.debug('[reactwrapper] component unmounted ');
} };
}, []) }, []);
return React.createElement( return React.createElement(
props.component, props.component,
_props.current as IPanelProps _props.current as IPanelProps
) );
} }
) );
const counter = sequentialNumberGenerator() const counter = sequentialNumberGenerator();
export class ReactPart implements IDisposable { export class ReactPart implements IDisposable {
private componentInstance: IPanelWrapperRef private componentInstance: IPanelWrapperRef;
private ref: { portal: React.ReactPortal; disposable: IDisposable } private ref: { portal: React.ReactPortal; disposable: IDisposable };
private disposed: boolean private disposed: boolean;
constructor( constructor(
private readonly parent: HTMLElement, private readonly parent: HTMLElement,
@ -64,49 +64,49 @@ export class ReactPart implements IDisposable {
private readonly component: React.FunctionComponent<{}>, private readonly component: React.FunctionComponent<{}>,
private readonly parameters: { [key: string]: any } private readonly parameters: { [key: string]: any }
) { ) {
this.createPortal() this.createPortal();
} }
public update(props: {}) { public update(props: {}) {
if (this.disposed) { if (this.disposed) {
throw new Error('invalid operation') throw new Error('invalid operation');
} }
this.componentInstance?.update(props) this.componentInstance?.update(props);
} }
private createPortal() { private createPortal() {
if (this.disposed) { if (this.disposed) {
throw new Error('invalid operation') throw new Error('invalid operation');
} }
let props = { let props = {
api: this.api, api: this.api,
...this.parameters, ...this.parameters,
} as any } as any;
const wrapper = React.createElement(PanelWrapper, { const wrapper = React.createElement(PanelWrapper, {
component: this.component, component: this.component,
componentProps: props, componentProps: props,
ref: (element: any) => { ref: (element: any) => {
this.componentInstance = element this.componentInstance = element;
}, },
}) });
const portal = ReactDOM.createPortal( const portal = ReactDOM.createPortal(
wrapper, wrapper,
this.parent, this.parent,
counter.next() counter.next()
) );
this.ref = { this.ref = {
portal, portal,
disposable: this.addPortal(portal), disposable: this.addPortal(portal),
} };
} }
public dispose() { public dispose() {
this.ref?.disposable?.dispose() this.ref?.disposable?.dispose();
this.ref = undefined this.ref = undefined;
this.disposed = true this.disposed = true;
} }
} }

View File

@ -1,55 +1,55 @@
import { trackFocus } from '../dom' import { trackFocus } from '../dom';
import { Emitter } from '../events' import { Emitter } from '../events';
import { GridApi } from '../panel/api' import { GridApi } from '../panel/api';
import { CompositeDisposable } from '../lifecycle' import { CompositeDisposable } from '../lifecycle';
import { ReactLayout } from './layout' import { ReactLayout } from './layout';
import { ReactPart } from './react' import { ReactPart } from './react';
import { ISplitviewPanelProps } from './splitview' import { ISplitviewPanelProps } from './splitview';
import { PanelUpdateEvent, InitParameters, IPanel } from '../panel/types' import { PanelUpdateEvent, InitParameters, IPanel } from '../panel/types';
import { IComponentGridview } from '../layout/componentGridview' import { IComponentGridview } from '../layout/componentGridview';
import { FunctionOrValue } from '../types' import { FunctionOrValue } from '../types';
export class ReactComponentGridView export class ReactComponentGridView
extends CompositeDisposable extends CompositeDisposable
implements IComponentGridview, IPanel { implements IComponentGridview, IPanel {
private _element: HTMLElement private _element: HTMLElement;
private part: ReactPart private part: ReactPart;
private params: { params: any } private params: { params: any };
private api: GridApi private api: GridApi;
private _onDidChange: Emitter<number | undefined> = new Emitter< private _onDidChange: Emitter<number | undefined> = new Emitter<
number | undefined number | undefined
>() >();
public onDidChange = this._onDidChange.event public onDidChange = this._onDidChange.event;
get element() { get element() {
return this._element return this._element;
} }
private _minimumWidth: FunctionOrValue<number> = 200 private _minimumWidth: FunctionOrValue<number> = 200;
private _minimumHeight: FunctionOrValue<number> = 200 private _minimumHeight: FunctionOrValue<number> = 200;
private _maximumWidth: FunctionOrValue<number> = Number.MAX_SAFE_INTEGER private _maximumWidth: FunctionOrValue<number> = Number.MAX_SAFE_INTEGER;
private _maximumHeight: FunctionOrValue<number> = Number.MAX_SAFE_INTEGER private _maximumHeight: FunctionOrValue<number> = Number.MAX_SAFE_INTEGER;
get minimumWidth() { get minimumWidth() {
return typeof this._minimumWidth === 'function' return typeof this._minimumWidth === 'function'
? this._minimumWidth() ? this._minimumWidth()
: this._minimumWidth : this._minimumWidth;
} }
get minimumHeight() { get minimumHeight() {
return typeof this._minimumHeight === 'function' return typeof this._minimumHeight === 'function'
? this._minimumHeight() ? this._minimumHeight()
: this._minimumHeight : this._minimumHeight;
} }
get maximumHeight() { get maximumHeight() {
return typeof this._maximumHeight === 'function' return typeof this._maximumHeight === 'function'
? this._maximumHeight() ? this._maximumHeight()
: this._maximumHeight : this._maximumHeight;
} }
get maximumWidth() { get maximumWidth() {
return typeof this._maximumWidth === 'function' return typeof this._maximumWidth === 'function'
? this._maximumWidth() ? this._maximumWidth()
: this._maximumWidth : this._maximumWidth;
} }
constructor( constructor(
@ -60,17 +60,17 @@ export class ReactComponentGridView
>, >,
private readonly parent: ReactLayout private readonly parent: ReactLayout
) { ) {
super() super();
this.api = new GridApi() this.api = new GridApi();
if (!this.component) { if (!this.component) {
throw new Error('React.FunctionalComponent cannot be undefined') throw new Error('React.FunctionalComponent cannot be undefined');
} }
this._element = document.createElement('div') this._element = document.createElement('div');
this._element.tabIndex = -1 this._element.tabIndex = -1;
this._element.style.outline = 'none' this._element.style.outline = 'none';
const { onDidFocus, onDidBlur } = trackFocus(this._element) const { onDidFocus, onDidBlur } = trackFocus(this._element);
this.addDisposables( this.addDisposables(
this.api.onDidConstraintsChange((event) => { this.api.onDidConstraintsChange((event) => {
@ -78,54 +78,58 @@ export class ReactComponentGridView
typeof event.minimumWidth === 'number' || typeof event.minimumWidth === 'number' ||
typeof event.minimumWidth === 'function' typeof event.minimumWidth === 'function'
) { ) {
this._minimumWidth = event.minimumWidth this._minimumWidth = event.minimumWidth;
} }
if ( if (
typeof event.minimumHeight === 'number' || typeof event.minimumHeight === 'number' ||
typeof event.minimumHeight === 'function' typeof event.minimumHeight === 'function'
) { ) {
this._minimumHeight = event.minimumHeight this._minimumHeight = event.minimumHeight;
} }
if ( if (
typeof event.maximumWidth === 'number' || typeof event.maximumWidth === 'number' ||
typeof event.maximumWidth === 'function' typeof event.maximumWidth === 'function'
) { ) {
this._maximumWidth = event.maximumWidth this._maximumWidth = event.maximumWidth;
} }
if ( if (
typeof event.maximumHeight === 'number' || typeof event.maximumHeight === 'number' ||
typeof event.maximumHeight === 'function' typeof event.maximumHeight === 'function'
) { ) {
this._maximumHeight = event.maximumHeight this._maximumHeight = event.maximumHeight;
} }
}), }),
onDidFocus(() => { onDidFocus(() => {
this.api._onDidChangeFocus.fire({ isFocused: true }) this.api._onDidChangeFocus.fire({ isFocused: true });
}), }),
onDidBlur(() => { onDidBlur(() => {
this.api._onDidChangeFocus.fire({ isFocused: false }) this.api._onDidChangeFocus.fire({ isFocused: false });
}) })
) );
}
setActive(isActive: boolean) {
// noop
} }
layout(width: number, height: number) { layout(width: number, height: number) {
this.api._onDidPanelDimensionChange.fire({ width, height }) this.api._onDidPanelDimensionChange.fire({ width, height });
} }
init(parameters: InitParameters): void { init(parameters: InitParameters): void {
this.params = parameters this.params = parameters;
this.part = new ReactPart( this.part = new ReactPart(
this.element, this.element,
this.api, this.api,
this.parent.addPortal, this.parent.addPortal,
this.component, this.component,
parameters.params parameters.params
) );
} }
update(params: PanelUpdateEvent) { update(params: PanelUpdateEvent) {
this.params = { ...this.params.params, ...params } this.params = { ...this.params.params, ...params };
this.part.update(params) this.part.update(params);
} }
toJSON(): object { toJSON(): object {
@ -134,11 +138,11 @@ export class ReactComponentGridView
component: this.componentName, component: this.componentName,
props: this.params.params, props: this.params.params,
state: this.api.getState(), state: this.api.getState(),
} };
} }
dispose() { dispose() {
super.dispose() super.dispose();
this.api.dispose() this.api.dispose();
} }
} }

View File

@ -1,13 +1,13 @@
import { trackFocus } from '../dom' import { trackFocus } from '../dom';
import { Emitter } from '../events' import { Emitter } from '../events';
import { PanelApi } from '../panel/api' import { PanelApi } from '../panel/api';
import { PanelDimensionChangeEvent } from '../panel/types' import { PanelDimensionChangeEvent } from '../panel/types';
import { CompositeDisposable } from '../lifecycle' import { CompositeDisposable } from '../lifecycle';
import { IView } from '../splitview/splitview' import { IView } from '../splitview/splitview';
import { ReactLayout } from './layout' import { ReactLayout } from './layout';
import { ReactPart } from './react' import { ReactPart } from './react';
import { ISplitviewPanelProps } from './splitview' import { ISplitviewPanelProps } from './splitview';
import { PanelUpdateEvent, InitParameters, IPanel } from '../panel/types' import { PanelUpdateEvent, InitParameters, IPanel } from '../panel/types';
/** /**
* A no-thrills implementation of IView that renders a React component * A no-thrills implementation of IView that renders a React component
@ -15,43 +15,43 @@ import { PanelUpdateEvent, InitParameters, IPanel } from '../panel/types'
export class ReactComponentView export class ReactComponentView
extends CompositeDisposable extends CompositeDisposable
implements IView, IPanel { implements IView, IPanel {
private _element: HTMLElement private _element: HTMLElement;
private part: ReactPart private part: ReactPart;
private params: { params: any } private params: { params: any };
private api: PanelApi private api: PanelApi;
private _onDidChange: Emitter<number | undefined> = new Emitter< private _onDidChange: Emitter<number | undefined> = new Emitter<
number | undefined number | undefined
>() >();
public onDidChange = this._onDidChange.event public onDidChange = this._onDidChange.event;
get element() { get element() {
return this._element return this._element;
} }
private _minimumSize: number = 200 private _minimumSize: number = 200;
private _maximumSize: number = Number.MAX_SAFE_INTEGER private _maximumSize: number = Number.MAX_SAFE_INTEGER;
private _snapSize: number private _snapSize: number;
get minimumSize() { get minimumSize() {
return this._minimumSize return this._minimumSize;
} }
set minimumSize(value: number) { set minimumSize(value: number) {
this._minimumSize = value this._minimumSize = value;
} }
get snapSize() { get snapSize() {
return this._snapSize return this._snapSize;
} }
set snapSize(value: number) { set snapSize(value: number) {
this._snapSize = value this._snapSize = value;
} }
get maximumSize() { get maximumSize() {
return this._maximumSize return this._maximumSize;
} }
set maximumSize(value: number) { set maximumSize(value: number) {
this._maximumSize = value this._maximumSize = value;
} }
constructor( constructor(
@ -62,46 +62,46 @@ export class ReactComponentView
>, >,
private readonly parent: ReactLayout private readonly parent: ReactLayout
) { ) {
super() super();
this.api = new PanelApi() this.api = new PanelApi();
if (!this.component) { if (!this.component) {
throw new Error('React.FunctionalComponent cannot be undefined') throw new Error('React.FunctionalComponent cannot be undefined');
} }
this._element = document.createElement('div') this._element = document.createElement('div');
this._element.tabIndex = -1 this._element.tabIndex = -1;
this._element.style.outline = 'none' this._element.style.outline = 'none';
const { onDidFocus, onDidBlur } = trackFocus(this._element) const { onDidFocus, onDidBlur } = trackFocus(this._element);
this.addDisposables( this.addDisposables(
onDidFocus(() => { onDidFocus(() => {
this.api._onDidChangeFocus.fire({ isFocused: true }) this.api._onDidChangeFocus.fire({ isFocused: true });
}), }),
onDidBlur(() => { onDidBlur(() => {
this.api._onDidChangeFocus.fire({ isFocused: false }) this.api._onDidChangeFocus.fire({ isFocused: false });
}) })
) );
} }
layout(width: number, height: number) { layout(width: number, height: number) {
this.api._onDidPanelDimensionChange.fire({ width, height }) this.api._onDidPanelDimensionChange.fire({ width, height });
} }
init(parameters: InitParameters): void { init(parameters: InitParameters): void {
this.params = parameters this.params = parameters;
this.part = new ReactPart( this.part = new ReactPart(
this.element, this.element,
this.api, this.api,
this.parent.addPortal, this.parent.addPortal,
this.component, this.component,
parameters.params parameters.params
) );
} }
update(params: PanelUpdateEvent) { update(params: PanelUpdateEvent) {
this.params = { ...this.params.params, ...params } this.params = { ...this.params.params, ...params };
this.part.update(params) this.part.update(params);
} }
toJSON(): object { toJSON(): object {
@ -110,11 +110,11 @@ export class ReactComponentView
component: this.componentName, component: this.componentName,
props: this.params.params, props: this.params.params,
state: this.api.getState(), state: this.api.getState(),
} };
} }
dispose() { dispose() {
super.dispose() super.dispose();
this.api.dispose() this.api.dispose();
} }
} }

View File

@ -1,18 +1,18 @@
import * as React from 'react' import * as React from 'react';
import { import {
PanelContentPart, PanelContentPart,
PartInitParameters, PartInitParameters,
ClosePanelResult, ClosePanelResult,
} from '../groupview/panel/parts' } from '../groupview/panel/parts';
import { ReactPart, IPanelProps } from './react' import { ReactPart, IPanelProps } from './react';
import { ReactLayout } from './layout' import { ReactLayout } from './layout';
export class ReactPanelContentPart implements PanelContentPart { export class ReactPanelContentPart implements PanelContentPart {
private _element: HTMLElement private _element: HTMLElement;
private part: ReactPart private part: ReactPart;
get element() { get element() {
return this._element return this._element;
} }
constructor( constructor(
@ -20,7 +20,7 @@ export class ReactPanelContentPart implements PanelContentPart {
private readonly component: React.FunctionComponent<IPanelProps>, private readonly component: React.FunctionComponent<IPanelProps>,
private readonly parent: ReactLayout private readonly parent: ReactLayout
) { ) {
this._element = document.createElement('div') this._element = document.createElement('div');
} }
public init(parameters: PartInitParameters): void { public init(parameters: PartInitParameters): void {
@ -30,17 +30,17 @@ export class ReactPanelContentPart implements PanelContentPart {
this.parent.addPortal, this.parent.addPortal,
this.component, this.component,
parameters.params parameters.params
) );
} }
public toJSON() { public toJSON() {
return { return {
id: this.id, id: this.id,
} };
} }
public update(params: {}) { public update(params: {}) {
this.part.update(params) this.part.update(params);
} }
public setVisible(isPanelVisible: boolean, isGroupVisible: boolean): void { public setVisible(isPanelVisible: boolean, isGroupVisible: boolean): void {
@ -50,13 +50,13 @@ export class ReactPanelContentPart implements PanelContentPart {
public layout(width: number, height: number): void {} public layout(width: number, height: number): void {}
public close(): Promise<ClosePanelResult> { public close(): Promise<ClosePanelResult> {
return Promise.resolve(ClosePanelResult.CLOSE) return Promise.resolve(ClosePanelResult.CLOSE);
} }
public focus(): void {} public focus(): void {}
public onHide(): void {} public onHide(): void {}
public dispose() { public dispose() {
this.part?.dispose() this.part?.dispose();
} }
} }

View File

@ -1,14 +1,14 @@
import * as React from 'react' import * as React from 'react';
import { PanelHeaderPart, PartInitParameters } from '../groupview/panel/parts' import { PanelHeaderPart, PartInitParameters } from '../groupview/panel/parts';
import { ReactPart, IPanelProps } from './react' import { ReactPart, IPanelProps } from './react';
import { ReactLayout } from './layout' import { ReactLayout } from './layout';
export class ReactPanelHeaderPart implements PanelHeaderPart { export class ReactPanelHeaderPart implements PanelHeaderPart {
private _element: HTMLElement private _element: HTMLElement;
private part: ReactPart private part: ReactPart;
get element() { get element() {
return this._element return this._element;
} }
constructor( constructor(
@ -16,7 +16,7 @@ export class ReactPanelHeaderPart implements PanelHeaderPart {
private readonly component: React.FunctionComponent<IPanelProps>, private readonly component: React.FunctionComponent<IPanelProps>,
private readonly parent: ReactLayout private readonly parent: ReactLayout
) { ) {
this._element = document.createElement('div') this._element = document.createElement('div');
} }
public init(parameters: PartInitParameters): void { public init(parameters: PartInitParameters): void {
@ -26,13 +26,13 @@ export class ReactPanelHeaderPart implements PanelHeaderPart {
this.parent.addPortal, this.parent.addPortal,
this.component, this.component,
parameters.params parameters.params
) );
} }
public toJSON() { public toJSON() {
return { return {
id: this.id, id: this.id,
} };
} }
public layout(height: string) { public layout(height: string) {
@ -44,6 +44,6 @@ export class ReactPanelHeaderPart implements PanelHeaderPart {
} }
public dispose() { public dispose() {
this.part?.dispose() this.part?.dispose();
} }
} }

View File

@ -1,57 +1,57 @@
import * as React from 'react' import * as React from 'react';
import { IPanelApi } from '../panel/api' import { IPanelApi } from '../panel/api';
import { IDisposable } from '../lifecycle' import { IDisposable } from '../lifecycle';
import { import {
IComponentSplitview, IComponentSplitview,
ComponentSplitview, ComponentSplitview,
} from '../splitview/componentSplitview' } from '../splitview/componentSplitview';
import { Orientation } from '../splitview/splitview' import { Orientation } from '../splitview/splitview';
import { ReactComponentView } from './reactComponentView' import { ReactComponentView } from './reactComponentView';
export interface SplitviewFacade { export interface SplitviewFacade {
addFromComponent(options: { addFromComponent(options: {
id: string id: string;
component: string component: string;
params?: { [index: string]: any } params?: { [index: string]: any };
}): void }): void;
layout(size: number, orthogonalSize: number): void layout(size: number, orthogonalSize: number): void;
onChange: (cb: (event: { proportions: number[] }) => void) => IDisposable onChange: (cb: (event: { proportions: number[] }) => void) => IDisposable;
toJSON: () => any toJSON: () => any;
deserialize: (data: any) => void deserialize: (data: any) => void;
minimumSize: number minimumSize: number;
} }
export interface SplitviewReadyEvent { export interface SplitviewReadyEvent {
api: IComponentSplitview api: IComponentSplitview;
} }
export interface ISplitviewPanelProps { export interface ISplitviewPanelProps {
api: IPanelApi api: IPanelApi;
} }
export interface ISplitviewComponentProps { export interface ISplitviewComponentProps {
orientation: Orientation orientation: Orientation;
onReady?: (event: SplitviewReadyEvent) => void onReady?: (event: SplitviewReadyEvent) => void;
components: { components: {
[index: string]: React.FunctionComponent<ISplitviewPanelProps> [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 splitpanel = React.useRef<IComponentSplitview>() 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) => {
setPortals((portals) => [...portals, p]) setPortals((portals) => [...portals, p]);
return { return {
dispose: () => { dispose: () => {
setPortals((portals) => setPortals((portals) =>
portals.filter((portal) => portal !== p) portals.filter((portal) => portal !== p)
) );
}, },
} };
}, []) }, []);
React.useEffect(() => { React.useEffect(() => {
splitpanel.current = new ComponentSplitview(domReference.current, { splitpanel.current = new ComponentSplitview(domReference.current, {
@ -61,27 +61,27 @@ export const SplitViewComponent = (props: ISplitviewComponentProps) => {
createComponent: (id: string, component: any) => { createComponent: (id: string, component: any) => {
return new ReactComponentView(id, id, component, { return new ReactComponentView(id, id, component, {
addPortal, addPortal,
}) });
}, },
}, },
proportionalLayout: false, proportionalLayout: false,
}) });
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];
splitpanel.current.layout(size, orthogonalSize) splitpanel.current.layout(size, orthogonalSize);
if (props.onReady) { if (props.onReady) {
props.onReady({ api: splitpanel.current }) props.onReady({ api: splitpanel.current });
} }
return () => { return () => {
splitpanel.current.dispose() splitpanel.current.dispose();
} };
}, []) }, []);
return ( return (
<div <div
@ -93,5 +93,5 @@ export const SplitViewComponent = (props: ISplitviewComponentProps) => {
> >
{portals} {portals}
</div> </div>
) );
} };

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,11 +1,11 @@
const gulp = require('gulp') const gulp = require('gulp');
const gulpClean = require('gulp-clean') const gulpClean = require('gulp-clean');
const gulpTypescript = require('gulp-typescript') const gulpTypescript = require('gulp-typescript');
const merge = require('merge2') 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 sourcemaps = require('gulp-sourcemaps');
const headerTemplate = [ const headerTemplate = [
'/**', '/**',
@ -14,25 +14,25 @@ const headerTemplate = [
' * @link <%= pkg.homepage %>', ' * @link <%= pkg.homepage %>',
' * @licence <%= pkg.licence %>', ' * @licence <%= pkg.licence %>',
' */\n', ' */\n',
].join('\n') ].join('\n');
const dtsHeaderTemplate = [ const dtsHeaderTemplate = [
'// Type definitions for <%= pkg.name %> v <%= pkg.version %>', '// Type definitions for <%= pkg.name %> v <%= pkg.version %>',
'// Project <%= pkg.homepage %>\n', '// Project <%= pkg.homepage %>\n',
].join('\n') ].join('\n');
const build = (options) => { const build = (options) => {
const { tsconfig, package } = options const { tsconfig, package } = options;
gulp.task('clean', () => gulp.task('clean', () =>
gulp.src('dist', { read: false, allowEmpty: true }).pipe(gulpClean()) gulp.src('dist', { read: false, allowEmpty: true }).pipe(gulpClean())
) );
gulp.task('esm', () => { gulp.task('esm', () => {
const ts = gulpTypescript.createProject(tsconfig) const ts = gulpTypescript.createProject(tsconfig);
const tsResult = gulp const tsResult = gulp
.src(['src/**/*.ts', 'src/**/*.tsx']) .src(['src/**/*.ts', 'src/**/*.tsx'])
.pipe(sourcemaps.init()) .pipe(sourcemaps.init())
.pipe(ts()) .pipe(ts());
return merge([ return merge([
tsResult.dts tsResult.dts
.pipe(header(dtsHeaderTemplate, { pkg: package })) .pipe(header(dtsHeaderTemplate, { pkg: package }))
@ -43,8 +43,8 @@ const build = (options) => {
tsResult tsResult
.pipe(sourcemaps.write('.', { includeContent: false })) .pipe(sourcemaps.write('.', { includeContent: false }))
.pipe(gulp.dest('./dist/esm')), .pipe(gulp.dest('./dist/esm')),
]) ]);
}) });
gulp.task('sass', () => { gulp.task('sass', () => {
return ( return (
@ -56,8 +56,8 @@ const build = (options) => {
// ) // )
.pipe(concat('styles.css')) .pipe(concat('styles.css'))
.pipe(gulp.dest('./dist')) .pipe(gulp.dest('./dist'))
) );
}) });
} };
module.exports = { build } module.exports = { build };