This commit is contained in:
mathuo 2020-08-29 10:20:12 +01:00
parent 4033489e38
commit 47f27633da
33 changed files with 516 additions and 177 deletions

View File

@ -2,7 +2,7 @@ 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 "./reactgrid"; import { TestGrid } from "./layout-grid/reactgrid";
const options = [ const options = [
{ id: "config", component: LoadFromConfig }, { id: "config", component: LoadFromConfig },

View File

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

View File

@ -9,6 +9,39 @@ import {
CompositeDisposable, CompositeDisposable,
GroupChangeKind, GroupChangeKind,
} from "splitview"; } from "splitview";
import { CustomTab } from "./customTab";
import { SplitPanel } from "./splitPanel";
const Editor = (props: IPanelProps & { layoutApi: Api }) => {
const [tabHeight, setTabHeight] = React.useState<number>(0);
React.useEffect(() => {
if (props.layoutApi) {
setTabHeight(props.layoutApi.getTabHeight());
}
}, [props.layoutApi]);
const onTabHeightChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = Number(event.target.value);
if (!Number.isNaN(value)) {
setTabHeight(value);
}
};
const onClick = () => {
props.layoutApi.setTabHeight(tabHeight);
};
return (
<div style={{ height: "100%", backgroundColor: "white", color: "black" }}>
<label>
Tab height
<input onChange={onTabHeightChange} value={tabHeight} type="number" />
<button onClick={onClick}>Apply</button>
</label>
</div>
);
};
const components = { const components = {
inner_component: (props: IPanelProps) => { inner_component: (props: IPanelProps) => {
@ -122,6 +155,7 @@ const components = {
const backgroundColor = React.useMemo( const backgroundColor = React.useMemo(
() => () =>
// "#1e1e1e",
`rgb(${Math.floor(Math.random() * 256)},${Math.floor( `rgb(${Math.floor(Math.random() * 256)},${Math.floor(
Math.random() * 256 Math.random() * 256
)},${Math.floor(Math.random() * 256)})`, )},${Math.floor(Math.random() * 256)})`,
@ -143,6 +177,12 @@ const components = {
</div> </div>
); );
}, },
editor: Editor,
split_panel: SplitPanel,
};
const tabComponents = {
default: CustomTab,
}; };
const nextGuid = (() => { const nextGuid = (() => {
@ -177,7 +217,7 @@ export const TestGrid = () => {
title: "Item 2", title: "Item 2",
}); });
api.addPanelFromComponent({ api.addPanelFromComponent({
componentName: "test_component", componentName: "split_panel",
id: nextGuid(), id: nextGuid(),
title: "Item 3 with a long title", title: "Item 3 with a long title",
}); });
@ -202,6 +242,18 @@ export const TestGrid = () => {
componentName: "test_component", componentName: "test_component",
}; };
}); });
api.addDndHandle("Files", (ev) => {
const { event } = ev;
ev.event.event.preventDefault();
return {
id: Date.now().toString(),
title: event.event.dataTransfer.files[0].name,
componentName: "test_component",
};
});
}, [api]); }, [api]);
const onAdd = () => { const onAdd = () => {
@ -224,6 +276,7 @@ export const TestGrid = () => {
_api.current?.layout(width, height); _api.current?.layout(width, height);
}; };
window.addEventListener("resize", callback); window.addEventListener("resize", callback);
callback(undefined);
const dis = _api.current.onDidLayoutChange((event) => { const dis = _api.current.onDidLayoutChange((event) => {
console.log(event.kind); console.log(event.kind);
@ -280,7 +333,6 @@ export const TestGrid = () => {
if (!api) { if (!api) {
return; return;
} }
console.log("create drag refs");
api.createDragTarget( api.createDragTarget(
{ element: dragRef.current, content: "drag me" }, { element: dragRef.current, content: "drag me" },
() => ({ () => ({
@ -294,10 +346,25 @@ export const TestGrid = () => {
event.dataTransfer.setData("text/plain", "Panel2"); event.dataTransfer.setData("text/plain", "Panel2");
}; };
const onAddEditor = () => {
api.addPanelFromComponent({
id: "editor",
componentName: "editor",
tabComponentName: "default",
params: { layoutApi: api },
});
};
const onTabContextMenu = (event: MouseEvent) => {};
return ( return (
<div style={{ width: "100%" }}> <div
// className="visual-studio-theme"
style={{ width: "100%" }}
>
<div style={{ height: "20px", display: "flex" }}> <div style={{ height: "20px", display: "flex" }}>
<button onClick={onAdd}>Add</button> <button onClick={onAdd}>Add</button>
<button onClick={onAddEditor}>Expr</button>
<button onClick={onAddEmpty}>Add empty</button> <button onClick={onAddEmpty}>Add empty</button>
<button onClick={onConfig}>Save</button> <button onClick={onConfig}>Save</button>
<button onClick={onLoad}>Load</button> <button onClick={onLoad}>Load</button>
@ -335,9 +402,12 @@ export const TestGrid = () => {
// autoSizeToFitContainer={true} // autoSizeToFitContainer={true}
onReady={onReady} onReady={onReady}
components={components} components={components}
tabComponents={tabComponents}
debug={true} debug={true}
// tabHeight={30}
enableExternalDragEvents={true} enableExternalDragEvents={true}
// serializedLayout={data} // serializedLayout={data}
// onTabContextMenu={onTabContextMenu}
/> />
</div> </div>
); );

View File

@ -0,0 +1,43 @@
import * as React from "react";
import {
IPanelProps,
Orientation,
SplitviewFacade,
SplitviewReadyEvent,
} from "splitview";
import { SplitViewComponent } from "splitview";
const components = {
default1: (props) => {
return <div style={{ height: "100%", width: "100%" }}>hiya</div>;
},
};
export const SplitPanel = (props: IPanelProps) => {
const api = React.useRef<SplitviewFacade>();
React.useEffect(() => {
props.api.onDidPanelDimensionChange((event) => {
// const [height,width] = [event.height, event.width]
// const [size, orthogonalSize] =
// props.orientation === Orientation.HORIZONTAL
// ? [width, height]
// : [height, width];
api.current?.layout(event.width, event.height);
});
}, []);
const onReady = (event: SplitviewReadyEvent) => {
event.api.addFromComponent({ id: "1", component: "default1" });
event.api.addFromComponent({ id: "2", component: "default1" });
api.current = event.api;
};
return (
<SplitViewComponent
components={components}
onReady={onReady}
orientation={Orientation.VERTICAL}
/>
);
};

View File

@ -16,7 +16,7 @@ export interface IPaneComponentRef {
layout: (size: number, orthogonalSize: number) => void; layout: (size: number, orthogonalSize: number) => void;
} }
export type PaneComponent = React.RefForwardingComponent< export type PaneComponent = React.ForwardRefRenderFunction<
IPaneComponentRef, IPaneComponentRef,
IPaneComponentProps IPaneComponentProps
>; >;
@ -27,7 +27,7 @@ export interface IPaneHeaderComponentProps extends IViewWithReactComponent {
userprops?: { [index: string]: any }; userprops?: { [index: string]: any };
} }
export type PaneHeaderComponent = React.RefForwardingComponent< export type PaneHeaderComponent = React.ForwardRefRenderFunction<
{}, {},
IPaneHeaderComponentProps IPaneHeaderComponentProps
>; >;

View File

@ -12,7 +12,7 @@ export interface IViewComponentRef {
layout: (size: number, orthogonalSize: number) => void; layout: (size: number, orthogonalSize: number) => void;
} }
export type ViewComponent = React.RefForwardingComponent< export type ViewComponent = React.ForwardRefRenderFunction<
IViewComponentRef, IViewComponentRef,
IViewComponentProps IViewComponentProps
>; >;

View File

@ -24,19 +24,19 @@ export interface IPaneViewReactProps {
initialLayout?: PaneViewSerializedConfig; initialLayout?: PaneViewSerializedConfig;
} }
export type PaneViewReadyEvent = { export interface PaneViewReadyEvent {
api: PaneviewApi; api: PaneviewApi;
}; }
export type PaneViewSerializedConfig = { export interface PaneViewSerializedConfig {
views: Array< views: Array<
Omit<IPaneWithReactComponent, "component" | "headerComponent"> & { Omit<IPaneWithReactComponent, "component" | "headerComponent"> & {
size?: number; size?: number;
} }
>; >;
}; }
export type PaneviewApi = { export interface PaneviewApi {
add: ( add: (
options: Omit<IPaneWithReactComponent, "component" | "headerComponent"> & { options: Omit<IPaneWithReactComponent, "component" | "headerComponent"> & {
size?: number; size?: number;
@ -45,7 +45,7 @@ export type PaneviewApi = {
) => 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;

View File

@ -9,15 +9,15 @@ export interface IViewWithReactComponent extends IBaseView {
component: ViewComponent; component: ViewComponent;
} }
export type OnReadyEvent = { export interface OnReadyEvent {
api: SplitviewApi; api: SplitviewApi;
}; }
export type SerializedConfig = { export interface SerializedConfig {
views: Array<Omit<IViewWithReactComponent, "component"> & { size?: number }>; views: Array<Omit<IViewWithReactComponent, "component"> & { size?: number }>;
}; }
export type SplitviewApi = { export interface SplitviewApi {
add: ( add: (
options: Omit<IViewWithReactComponent, "component"> & { options: Omit<IViewWithReactComponent, "component"> & {
size?: number; size?: number;
@ -26,7 +26,7 @@ export type SplitviewApi = {
) => 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;

View File

@ -6,7 +6,7 @@
"types": "dist/esm/index.d.ts", "types": "dist/esm/index.d.ts",
"scripts": { "scripts": {
"build": "gulp run", "build": "gulp run",
"docs": "typedoc --excludeNotExported --excludePrivate true --mode file --out typedocs/ src/" "docs": "typedoc"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",

View File

@ -1,5 +1,5 @@
import { Orientation, Sizing } from "../splitview/splitview"; import { Orientation, Sizing } from "../splitview/splitview";
import { Target } 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";
@ -91,7 +91,7 @@ export function getGridLocation(element: HTMLElement): number[] {
export function getRelativeLocation( export function getRelativeLocation(
rootOrientation: Orientation, rootOrientation: Orientation,
location: number[], location: number[],
direction: Target direction: Position
): number[] { ): number[] {
const orientation = getLocationOrientation(rootOrientation, location); const orientation = getLocationOrientation(rootOrientation, location);
const directionOrientation = getDirectionOrientation(direction); const directionOrientation = getDirectionOrientation(direction);
@ -99,20 +99,20 @@ export function getRelativeLocation(
if (orientation === directionOrientation) { if (orientation === directionOrientation) {
let [rest, index] = tail(location); let [rest, index] = tail(location);
if (direction === Target.Right || direction === Target.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 === Target.Right || direction === Target.Bottom ? 1 : 0; direction === Position.Right || direction === Position.Bottom ? 1 : 0;
return [...location, index]; return [...location, index];
} }
} }
export function getDirectionOrientation(direction: Target): Orientation { export function getDirectionOrientation(direction: Position): Orientation {
return direction === Target.Top || direction === Target.Bottom return direction === Position.Top || direction === Position.Bottom
? Orientation.VERTICAL ? Orientation.VERTICAL
: Orientation.HORIZONTAL; : Orientation.HORIZONTAL;
} }
@ -193,9 +193,9 @@ export interface INodeDescriptor {
visible?: boolean; visible?: boolean;
} }
export type IViewDeserializer = { export interface IViewDeserializer {
fromJSON: (data: {}) => IGridView; fromJSON: (data: {}) => IGridView;
}; }
export class Gridview { export class Gridview {
private _root: BranchNode; private _root: BranchNode;

View File

@ -11,12 +11,12 @@ export enum DragType {
EXTERNAL = "external_group_drag", EXTERNAL = "external_group_drag",
} }
export type DragItem = { export interface DragItem {
itemId: string; itemId: string;
groupId: string; groupId: string;
}; }
export type ExternalDragItem = PanelOptions; export interface ExternalDragItem extends PanelOptions {}
export type DataObject = DragItem | ExternalDragItem; export type DataObject = DragItem | ExternalDragItem;

View File

@ -20,18 +20,18 @@
transition-duration: 0.15s; transition-duration: 0.15s;
transition-timing-function: ease-out; transition-timing-function: ease-out;
&.left { &.left,
&.right {
width: 50%; width: 50%;
} }
&.right { &.right {
left: 50%; transform: translate(100%, 0%);
width: 50%;
} }
&.bottom { &.bottom {
top: 50%; transform: translate(0%, 100%);
height: 50%;
} }
&.top { &.top,
&.bottom {
height: 50%; height: 50%;
} }
} }

View File

@ -1,7 +1,7 @@
import { Emitter, Event } from "../../events"; import { Emitter, Event } from "../../events";
import { DataTransferSingleton } from "./dataTransfer"; import { DataTransferSingleton } from "./dataTransfer";
export enum Target { export enum Position {
Top = "Top", Top = "Top",
Left = "Left", Left = "Left",
Bottom = "Bottom", Bottom = "Bottom",
@ -9,10 +9,10 @@ export enum Target {
Center = "Center", Center = "Center",
} }
export type DroptargetEvent = { export interface DroptargetEvent {
target: Target; position: Position;
event: DragEvent; event: DragEvent;
}; }
const HAS_PROCESSED_KEY = "__drop_target_processed__"; const HAS_PROCESSED_KEY = "__drop_target_processed__";
@ -39,7 +39,7 @@ const toggleClassName = (
export class Droptarget { export class Droptarget {
private target: HTMLElement; private target: HTMLElement;
private overlay: HTMLElement; private overlay: HTMLElement;
private state: Target; 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;
@ -106,7 +106,7 @@ export class Droptarget {
this.removeDropTarget(); this.removeDropTarget();
if (!hasProcessed(event)) { if (!hasProcessed(event)) {
this._onDidChange.fire({ target: this.state, event }); this._onDidChange.fire({ position: this.state, event });
} else { } else {
console.debug("[dragtarget] already processed"); console.debug("[dragtarget] already processed");
} }
@ -140,15 +140,15 @@ export class Droptarget {
toggleClassName(this.overlay, "bottom", isBottom); toggleClassName(this.overlay, "bottom", isBottom);
if (isRight) { if (isRight) {
this.state = Target.Right; this.state = Position.Right;
} else if (isLeft) { } else if (isLeft) {
this.state = Target.Left; this.state = Position.Left;
} else if (isTop) { } else if (isTop) {
this.state = Target.Top; this.state = Position.Top;
} else if (isBottom) { } else if (isBottom) {
this.state = Target.Bottom; this.state = Position.Bottom;
} else { } else {
this.state = Target.Center; this.state = Position.Center;
} }
}; };

View File

@ -1,11 +1,6 @@
import { DroptargetEvent } from "./droptarget/droptarget"; import { DroptargetEvent } from "./droptarget/droptarget";
export enum TabChangedEventType { export interface TabDropEvent {
CLICK,
}
export type TabChangedEvent = { type: TabChangedEventType };
export type TabDropEvent = {
event: DroptargetEvent; event: DroptargetEvent;
index?: number; index?: number;
}; }

View File

@ -1,8 +1,8 @@
import { IDisposable, CompositeDisposable, Disposable } from "../lifecycle"; import { IDisposable, CompositeDisposable, Disposable } from "../lifecycle";
import { ITabContainer, TabContainer } from "./tabs/tabContainer"; import { ITabContainer, TabContainer } from "./titlebar/tabContainer";
import { IContentContainer, ContentContainer } from "./content"; import { IContentContainer, ContentContainer } from "./panel/content/content";
import { IGridView } from "../gridview/gridview"; import { IGridView } from "../gridview/gridview";
import { Target, 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 { IGroupAccessor, Layout } from "../layout"; import { IGroupAccessor, Layout } from "../layout";
import { toggleClass } from "../dom"; import { toggleClass } from "../dom";
@ -44,12 +44,12 @@ export interface IGroupItem {
body: { element: HTMLElement }; body: { element: HTMLElement };
} }
type GroupMoveEvent = { interface GroupMoveEvent {
groupId: string; groupId: string;
itemId: string; itemId: string;
target: Target; target: Position;
index?: number; index?: number;
}; }
export interface GroupOptions { export interface GroupOptions {
panels: IPanel[]; panels: IPanel[];
@ -88,11 +88,11 @@ export interface IGroupview extends IDisposable, IGridView {
moveToPrevious(options?: { panel?: IPanel; suppressRoll?: boolean }): void; moveToPrevious(options?: { panel?: IPanel; suppressRoll?: boolean }): void;
} }
export type GroupDropEvent = { export interface GroupDropEvent {
event: DragEvent; event: DragEvent;
target: Target; 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;
@ -129,6 +129,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
set tabHeight(height: number) { set tabHeight(height: number) {
this.tabContainer.height = height; this.tabContainer.height = height;
this.layout(this._width, this._height);
} }
get isActive() { get isActive() {
@ -290,7 +291,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
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
if ( if (
event.target === Target.Center && event.position === Position.Center &&
this.tabContainer.hasActiveDragEvent this.tabContainer.hasActiveDragEvent
) { ) {
return; return;
@ -531,18 +532,18 @@ export class Groupview extends CompositeDisposable implements IGroupview {
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.target, index); this.handlePanelDropEvent(event.event, event.position, index);
return; return;
} }
this._onDrop.fire({ event: event.event, target: event.target, index }); this._onDrop.fire({ event: event.event, target: event.position, index });
console.debug("[customDropEvent]"); console.debug("[customDropEvent]");
} }
private handlePanelDropEvent( private handlePanelDropEvent(
event: DragEvent, event: DragEvent,
target: Target, target: Position,
index?: number index?: number
) { ) {
const dataObject = extractData(event); const dataObject = extractData(event);
@ -550,10 +551,10 @@ export class Groupview extends CompositeDisposable implements IGroupview {
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) { if (isSameGroup && !target) {
const index = this.tabContainer.indexOf(itemId); const oldIndex = this.tabContainer.indexOf(itemId);
if (index > -1 && index === this.panels.length - 1) { if (oldIndex === index) {
console.debug("[tabs] dropped in empty space"); console.debug("[tabs] drop indicates no change in position");
return; return;
} }
} }

View File

@ -4,15 +4,15 @@ import { ClosePanelResult } from "./parts";
import { IPanel } from "./types"; import { IPanel } from "./types";
import { CompositeDisposable, IDisposable } from "../../lifecycle"; import { CompositeDisposable, IDisposable } from "../../lifecycle";
export type PanelStateChangeEvent = { export interface PanelStateChangeEvent {
isPanelVisible: boolean; isPanelVisible: boolean;
isGroupActive: boolean; isGroupActive: boolean;
}; }
export type PanelDimensionChangeEvent = { export interface PanelDimensionChangeEvent {
width: number; width: number;
height: number; height: number;
}; }
export interface PanelApi extends IDisposable { export interface PanelApi extends IDisposable {
onDidPanelStateChange: Event<PanelStateChangeEvent>; onDidPanelStateChange: Event<PanelStateChangeEvent>;

View File

@ -1,6 +1,6 @@
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>;

View File

@ -149,7 +149,11 @@ export class DefaultPanel extends CompositeDisposable implements IPanel {
} }
public layout(width: number, height: number) { public layout(width: number, height: number) {
this._onDidPanelDimensionsChange.fire({ width, height }); // thw height of the panel excluded the height of the title/tab
this._onDidPanelDimensionsChange.fire({
width,
height: height - (this.group?.tabHeight || 0),
});
} }
public dispose() { public dispose() {

View File

@ -14,13 +14,13 @@ interface Methods extends IDisposable {
setVisible(isPanelVisible: boolean, isGroupVisible: boolean): void; setVisible(isPanelVisible: boolean, isGroupVisible: boolean): void;
} }
export type WatermarkPartInitParameters = { export interface WatermarkPartInitParameters {
accessor: IGroupAccessor; accessor: IGroupAccessor;
}; }
export type PartInitParameters = { export interface PartInitParameters extends PanelInitParameters {
api: PanelApi; api: PanelApi;
} & PanelInitParameters; }
export interface PanelHeaderPart extends Methods { export interface PanelHeaderPart extends Methods {
id: string; id: string;

View File

@ -1,23 +1,31 @@
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 { TabChangedEvent, TabDropEvent, TabChangedEventType } from "../events"; import { IGroupview } from "../../groupview";
import { IGroupview } from "../groupview";
import { import {
DataTransferSingleton, DataTransferSingleton,
DATA_KEY, DATA_KEY,
DragType, DragType,
extractData, } from "../../droptarget/dataTransfer";
} from "../droptarget/dataTransfer"; // import { IGroupAccessor } from "../../layout";
import { IGroupAccessor } from "../../layout"; import { toggleClass } from "../../../dom";
import { toggleClass } from "../../dom"; import { IGroupAccessor } from "../../../layout";
export enum TabInteractionKind {
CLICK = "CLICK",
CONTEXT_MENU = "CONTEXT_MEU",
}
export interface TabInteractionEvent {
kind: TabInteractionKind;
}
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<TabChangedEvent>; onChanged: Event<TabInteractionEvent>;
onDropped: Event<DroptargetEvent>; onDropped: Event<DroptargetEvent>;
setActive(isActive: boolean): void; setActive(isActive: boolean): void;
startDragEvent(): void; startDragEvent(): void;
@ -32,8 +40,8 @@ export class Tab extends CompositeDisposable implements ITab {
private droptarget: Droptarget; private droptarget: Droptarget;
private content: HTMLElement; private content: HTMLElement;
private readonly _onChanged = new Emitter<TabChangedEvent>(); private readonly _onChanged = new Emitter<TabInteractionEvent>();
readonly onChanged: Event<TabChangedEvent> = this._onChanged.event; readonly onChanged: Event<TabInteractionEvent> = 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;
@ -72,7 +80,10 @@ export class Tab extends CompositeDisposable implements ITab {
if (ev.defaultPrevented) { if (ev.defaultPrevented) {
return; return;
} }
this._onChanged.fire({ type: TabChangedEventType.CLICK }); this._onChanged.fire({ kind: TabInteractionKind.CLICK });
}),
addDisposableListener(this._element, "contextmenu", (ev) => {
this._onChanged.fire({ kind: TabInteractionKind.CONTEXT_MENU });
}), }),
addDisposableListener(this._element, "dragstart", (event) => { addDisposableListener(this._element, "dragstart", (event) => {
this.dragInPlayDetails = { isDragging: true, id: this.accessor.id }; this.dragInPlayDetails = { isDragging: true, id: this.accessor.id };

View File

@ -5,18 +5,18 @@ import { PanelHeaderPart, PanelContentPart, ClosePanelResult } from "./parts";
// objects // objects
export type PanelUpdateEvent = { export interface PanelUpdateEvent {
params: { [key: string]: any }; params: { [key: string]: any };
}; }
// init parameters // init parameters
export type PanelInitParameters = { export interface PanelInitParameters {
title: string; title: string;
suppressClosable?: boolean; suppressClosable?: boolean;
params: { [index: string]: any }; params: { [index: string]: any };
state?: { [index: string]: any }; state?: { [index: string]: any };
}; }
// constructors // constructors

View File

@ -1,8 +1,8 @@
import { IDisposable, CompositeDisposable } from "../../lifecycle"; import { IDisposable, CompositeDisposable } from "../../lifecycle";
import { addDisposableListener, Emitter, Event } from "../../events"; import { addDisposableListener, Emitter, Event } from "../../events";
import { ITab, Tab } from "./tab"; import { ITab, Tab, TabInteractionKind } from "../panel/tab/tab";
import { removeClasses, addClasses, toggleClass } from "../../dom"; import { removeClasses, addClasses, toggleClass } from "../../dom";
import { hasProcessed } 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";
@ -144,7 +144,8 @@ export class TabContainer extends CompositeDisposable implements ITabContainer {
} }
this._onDropped.fire({ this._onDropped.fire({
event: { event, target: undefined }, event: { event, position: Position.Center },
index: this.tabs.length - 1,
}); });
}) })
); );
@ -195,7 +196,13 @@ export class TabContainer extends CompositeDisposable implements ITabContainer {
// TODO - dispose of resources // TODO - dispose of resources
const disposables = CompositeDisposable.from( const disposables = CompositeDisposable.from(
tab.onChanged((event) => { tab.onChanged((event) => {
switch (event.kind) {
case TabInteractionKind.CLICK:
this.group.openPanel(panel); this.group.openPanel(panel);
break;
case TabInteractionKind.CONTEXT_MENU:
// TODO finish
}
}), }),
tab.onDropped((event) => { tab.onDropped((event) => {
this._onDropped.fire({ event, index: this.indexOf(tab) }); this._onDropped.fire({ event, index: this.indexOf(tab) });

View File

@ -2,8 +2,8 @@ export * from "./splitview/splitview";
export * from "./splitview/paneview"; export * from "./splitview/paneview";
export * from "./gridview/gridview"; export * from "./gridview/gridview";
export * from "./groupview/groupview"; export * from "./groupview/groupview";
export * from "./groupview/content"; export * from "./groupview/panel/content/content";
export * from "./groupview/tabs/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";
@ -12,6 +12,7 @@ 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/reactContentPart"; export * from "./react/reactContentPart";
export * from "./react/reactHeaderPart"; export * from "./react/reactHeaderPart";

View File

@ -1,67 +1,17 @@
.groupview {
&.active-group {
> .title-container > .tab-container > .tab {
&.active-tab {
.default-tab {
background-color: var(--tab-background-visible);
color: var(--active-group-visible-panel-color);
}
.tab-action {
background-color: var(--active-group-visible-panel-color);
}
}
&.inactive-tab {
.default-tab {
background-color: var(--tab-background-hidden);
color: var(--active-group-hidden-panel-color);
}
.tab-action {
background-color: var(--active-group-hidden-panel-color);
}
}
}
}
&.inactive-group {
> .title-container > .tab-container > .tab {
&.active-tab {
.default-tab {
background-color: var(--tab-background-visible);
color: var(--inactive-group-visible-panel-color);
}
.tab-action {
background-color: var(--inactive-group-visible-panel-color);
}
}
&.inactive-tab {
.default-tab {
background-color: var(--tab-background-hidden);
color: var(--inactive-group-hidden-panel-color);
}
.tab-action {
background-color: var(--inactive-group-hidden-panel-color);
}
}
}
}
}
.tab { .tab {
&.dragging { &.dragging {
color: var(--active-group-visible-panel-color);
.tab-action { .tab-action {
background-color: var(--active-group-visible-panel-color); background-color: var(--active-group-visible-panel-color);
} }
} }
&.active-tab > .default-tab { &.active-tab > .default-tab {
background-color: var(--tab-background-visible);
.tab-action { .tab-action {
visibility: visible; visibility: visible;
} }
} }
&.inactive-tab > .default-tab { &.inactive-tab > .default-tab {
background-color: var(--tab-background-hidden);
.tab-action:not(.dirty) { .tab-action:not(.dirty) {
visibility: hidden; visibility: hidden;
} }

View File

@ -9,3 +9,55 @@
position: absolute; position: absolute;
padding-left: 10px; padding-left: 10px;
} }
.groupview {
&.active-group {
> .title-container > .tab-container > .tab {
&.active-tab {
background-color: var(--active-tab-background-visible);
color: var(--active-group-visible-panel-color);
.tab-action {
background-color: var(--active-group-visible-panel-color);
}
}
&.inactive-tab {
background-color: var(--active-tab-background-hidden);
color: var(--active-group-hidden-panel-color);
.tab-action {
background-color: var(--active-group-hidden-panel-color);
}
}
}
}
&.inactive-group {
> .title-container > .tab-container > .tab {
&.active-tab {
background-color: var(--inactive-tab-background-visible);
color: var(--inactive-group-visible-panel-color);
.tab-action {
background-color: var(--inactive-group-visible-panel-color);
}
}
&.inactive-tab {
background-color: var(--inactive-tab-background-hidden);
color: var(--inactive-group-hidden-panel-color);
.tab-action {
background-color: var(--inactive-group-hidden-panel-color);
}
}
}
}
}
// when a tab is dragged we loss the above stylings because they are conditional on parent elements
// there we also set some stylings for the dragging event
.tab {
&.dragging {
background-color: var(--active-tab-background-visible);
color: var(--active-group-visible-panel-color);
}
}

View File

@ -1,5 +1,5 @@
import { Gridview, getRelativeLocation } from "../gridview/gridview"; import { Gridview, getRelativeLocation } from "../gridview/gridview";
import { Target } 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 {
@ -45,10 +45,10 @@ import {
const nextGroupId = sequentialNumberGenerator(); const nextGroupId = sequentialNumberGenerator();
const nextLayoutId = sequentialNumberGenerator(); const nextLayoutId = sequentialNumberGenerator();
export type PanelReference = { export interface PanelReference {
update: (event: { params: { [key: string]: any } }) => void; update: (event: { params: { [key: string]: any } }) => void;
remove: () => void; remove: () => void;
}; }
export interface Api { export interface Api {
layout(width: number, height: number): void; layout(width: number, height: number): void;
@ -56,6 +56,7 @@ export interface Api {
setAutoResizeToFit(enabled: boolean): void; setAutoResizeToFit(enabled: boolean): void;
resizeToFit(): void; resizeToFit(): void;
setTabHeight(height: number): void; setTabHeight(height: number): void;
getTabHeight(): number;
size: number; size: number;
totalPanels: number; totalPanels: number;
// lifecycle // lifecycle
@ -90,7 +91,7 @@ export interface IGroupAccessor {
referenceGroup: IGroupview, referenceGroup: IGroupview,
groupId: string, groupId: string,
itemId: string, itemId: string,
target: Target, target: Position,
index?: number index?: number
): void; ): void;
doSetGroupActive: (group: IGroupview) => void; doSetGroupActive: (group: IGroupview) => void;
@ -399,11 +400,16 @@ export class Layout extends CompositeDisposable implements ILayout {
} }
public setTabHeight(height: number) { public setTabHeight(height: number) {
this.options.tabHeight = height;
this.groups.forEach((value) => { this.groups.forEach((value) => {
value.value.tabHeight = height; value.value.tabHeight = height;
}); });
} }
public getTabHeight() {
return this.options.tabHeight;
}
public setAutoResizeToFit(enabled: boolean) { public setAutoResizeToFit(enabled: boolean) {
if (this.resizeTimer) { if (this.resizeTimer) {
clearInterval(this.resizeTimer); clearInterval(this.resizeTimer);
@ -435,7 +441,7 @@ export class Layout extends CompositeDisposable implements ILayout {
const referenceGroup = this.findGroup(referencePanel); const referenceGroup = this.findGroup(referencePanel);
const target = this.toTarget(options.position.direction); const target = this.toTarget(options.position.direction);
if (target === Target.Center) { if (target === Position.Center) {
referenceGroup.openPanel(panel); referenceGroup.openPanel(panel);
} else { } else {
const location = getGridLocation(referenceGroup.element); const location = getGridLocation(referenceGroup.element);
@ -597,13 +603,13 @@ export class Layout extends CompositeDisposable implements ILayout {
referenceGroup: IGroupview, referenceGroup: IGroupview,
groupId: string, groupId: string,
itemId: string, itemId: string,
target: Target, target: Position,
index?: number index?: number
) { ) {
const sourceGroup = groupId ? this.groups.get(groupId).value : undefined; const sourceGroup = groupId ? this.groups.get(groupId).value : undefined;
switch (target) { switch (target) {
case Target.Center: case Position.Center:
case undefined: case undefined:
const groupItem = const groupItem =
sourceGroup?.removePanel(itemId) || this.panels.get(itemId).value; sourceGroup?.removePanel(itemId) || this.panels.get(itemId).value;
@ -699,7 +705,7 @@ export class Layout extends CompositeDisposable implements ILayout {
} }
this.moveGroup( this.moveGroup(
group, group,
panel?.group.id, panel?.group?.id,
panel.id, panel.id,
event.target, event.target,
event.index event.index
@ -774,16 +780,16 @@ export class Layout extends CompositeDisposable implements ILayout {
private toTarget(direction: "left" | "right" | "above" | "below" | "within") { private toTarget(direction: "left" | "right" | "above" | "below" | "within") {
switch (direction) { switch (direction) {
case "left": case "left":
return Target.Left; return Position.Left;
case "right": case "right":
return Target.Right; return Position.Right;
case "above": case "above":
return Target.Top; return Position.Top;
case "below": case "below":
return Target.Bottom; return Position.Bottom;
case "within": case "within":
default: default:
return Target.Center; return Position.Center;
} }
} }

View File

@ -1,4 +1,5 @@
import { IGroupview } from "../groupview/groupview"; import { IGroupview } from "../groupview/groupview";
import { PanelApi } from "../groupview/panel/api";
import { import {
PanelContentPart, PanelContentPart,
PanelContentPartConstructor, PanelContentPartConstructor,
@ -6,11 +7,20 @@ import {
PanelHeaderPartConstructor, PanelHeaderPartConstructor,
WatermarkConstructor, WatermarkConstructor,
} from "../groupview/panel/parts"; } from "../groupview/panel/parts";
import { IPanel } from "../groupview/panel/types";
import { Api } from "./layout";
export type FrameworkPanelWrapper = { export interface FrameworkPanelWrapper {
createContentWrapper: (id: string, component: any) => PanelContentPart; createContentWrapper: (id: string, component: any) => PanelContentPart;
createTabWrapper: (id: string, component: any) => PanelHeaderPart; createTabWrapper: (id: string, component: any) => PanelHeaderPart;
}; }
export interface TabContextMenuEvent {
event: MouseEvent;
api: Api;
panelApi: PanelApi;
panel: IPanel;
}
export interface LayoutOptions { export interface LayoutOptions {
tabComponents?: { tabComponents?: {
@ -31,6 +41,7 @@ export interface LayoutOptions {
tabHeight?: number; tabHeight?: number;
debug?: boolean; debug?: boolean;
enableExternalDragEvents?: boolean; enableExternalDragEvents?: boolean;
onTabContextMenu?: (event: TabContextMenuEvent) => void;
} }
export interface PanelOptions { export interface PanelOptions {

View File

@ -6,9 +6,9 @@ import { ReactPanelHeaderPart } from "./reactHeaderPart";
import { IPanelProps } from "./react"; import { IPanelProps } from "./react";
import { ReactPanelDeserialzier } from "./deserializer"; import { ReactPanelDeserialzier } from "./deserializer";
export type OnReadyEvent = { export interface OnReadyEvent {
api: Api; api: Api;
}; }
export interface ReactLayout { export interface ReactLayout {
addPortal: (portal: React.ReactPortal) => IDisposable; addPortal: (portal: React.ReactPortal) => IDisposable;

View File

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

View File

@ -0,0 +1,95 @@
import * as React from "react";
import { Orientation, SplitView } from "../splitview/splitview";
import { ReactView } from "./reactView";
export interface SplitviewFacade {
addFromComponent(options: { id: string; component: string }): void;
layout(size: number, orthogonalSize: number): void;
}
export interface SplitviewReadyEvent {
api: SplitviewFacade;
}
export interface ISplitviewComponentProps {
orientation: Orientation;
onReady?: (event: SplitviewReadyEvent) => void;
components: { [index: string]: React.FunctionComponent<{}> };
}
export const SplitViewComponent = (props: ISplitviewComponentProps) => {
const domReference = React.useRef<HTMLDivElement>();
const splitview = React.useRef<SplitView>();
const [portals, setPortals] = React.useState<React.ReactPortal[]>([]);
const addPortal = React.useCallback((p: React.ReactPortal) => {
setPortals((portals) => [...portals, p]);
return {
dispose: () => {
setPortals((portals) => portals.filter((portal) => portal !== p));
},
};
}, []);
React.useEffect(() => {
splitview.current = new SplitView(domReference.current, {
orientation: props.orientation,
});
const createViewWrapper = (
id: string,
component: React.FunctionComponent<{}>
) => {
return new ReactView(id, component, { addPortal });
};
const facade: SplitviewFacade = {
addFromComponent: (options) => {
const component = props.components[options.component];
const view = createViewWrapper(options.id, component);
splitview.current.addView(view, { type: "distribute" });
view.init({ params: {} });
return {
dispose: () => {
//
},
};
},
layout: (width, height) => {
const [size, orthogonalSize] =
props.orientation === Orientation.HORIZONTAL
? [width, height]
: [height, width];
splitview.current.layout(size, orthogonalSize);
},
};
const { width, height } = domReference.current.getBoundingClientRect();
const [size, orthogonalSize] =
props.orientation === Orientation.HORIZONTAL
? [width, height]
: [height, width];
splitview.current.layout(size, orthogonalSize);
if (props.onReady) {
props.onReady({ api: facade });
}
return () => {
splitview.current.dispose();
};
}, []);
return (
<div
style={{
height: "100%",
width: "100%",
}}
ref={domReference}
>
{portals}
</div>
);
};

View File

@ -4,11 +4,13 @@
--title-bar-background-color: #252526; --title-bar-background-color: #252526;
--title-bar-scroll-bar-color: #888; --title-bar-scroll-bar-color: #888;
// //
--tab-background-visible: #1e1e1e; --active-tab-background-visible: #1e1e1e;
--tab-background-hidden: #2d2d2d; --active-tab-background-hidden: #2d2d2d;
--inactive-tab-background-visible: #1e1e1e;
--inactive-tab-background-hidden: #2d2d2d;
--tab-divider-color: #1e1e1e; --tab-divider-color: #1e1e1e;
// //
--drag-over-background-color: rgba(255, 0, 0, 0.5); --drag-over-background-color: rgba(83, 89, 93, 0.5);
// //
--active-group-visible-panel-color: white; --active-group-visible-panel-color: white;
--active-group-hidden-panel-color: #969696; --active-group-hidden-panel-color: #969696;
@ -20,3 +22,20 @@
// //
--splitview-divider-color: rgb(68, 68, 68); --splitview-divider-color: rgb(68, 68, 68);
} }
.visual-studio-theme {
--active-tab-background-visible: dodgerblue;
.groupview {
&.active-group {
> .title-container {
border-bottom: 2px solid var(--active-tab-background-visible);
}
}
&.inactive-group {
> .title-container {
border-bottom: 2px solid var(--inactive-tab-background-visible);
}
}
}
}

View File

@ -0,0 +1,11 @@
{
"out": "typedocs",
"mode": "file",
"inputFiles": ["./src"],
"exclude": ["**/_test/**/*.*", "**/index.ts"],
"ignoreCompilerErrors": true,
"disableOutputCheck": true,
"excludeExternals": true,
"excludePrivate": true,
"excludeNotExported": true
}