diff --git a/packages/splitview-demo/src/app.tsx b/packages/splitview-demo/src/app.tsx index ca67bbaee..d243ce849 100644 --- a/packages/splitview-demo/src/app.tsx +++ b/packages/splitview-demo/src/app.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { LoadFromConfig } from "./loadFromConfig"; import { FromApi } from "./fromApi"; import { PaneDemo } from "./pane"; -import { TestGrid } from "./reactgrid"; +import { TestGrid } from "./layout-grid/reactgrid"; const options = [ { id: "config", component: LoadFromConfig }, diff --git a/packages/splitview-demo/src/layout-grid/customTab.tsx b/packages/splitview-demo/src/layout-grid/customTab.tsx new file mode 100644 index 000000000..9cca1901c --- /dev/null +++ b/packages/splitview-demo/src/layout-grid/customTab.tsx @@ -0,0 +1,6 @@ +import * as React from "react"; +import { IPanelProps } from "splitview"; + +export const CustomTab = (props: IPanelProps) => { + return
hello
; +}; diff --git a/packages/splitview-demo/src/reactgrid.tsx b/packages/splitview-demo/src/layout-grid/reactgrid.tsx similarity index 81% rename from packages/splitview-demo/src/reactgrid.tsx rename to packages/splitview-demo/src/layout-grid/reactgrid.tsx index fc9d0c73a..59e396e12 100644 --- a/packages/splitview-demo/src/reactgrid.tsx +++ b/packages/splitview-demo/src/layout-grid/reactgrid.tsx @@ -9,6 +9,39 @@ import { CompositeDisposable, GroupChangeKind, } from "splitview"; +import { CustomTab } from "./customTab"; +import { SplitPanel } from "./splitPanel"; + +const Editor = (props: IPanelProps & { layoutApi: Api }) => { + const [tabHeight, setTabHeight] = React.useState(0); + + React.useEffect(() => { + if (props.layoutApi) { + setTabHeight(props.layoutApi.getTabHeight()); + } + }, [props.layoutApi]); + + const onTabHeightChange = (event: React.ChangeEvent) => { + const value = Number(event.target.value); + if (!Number.isNaN(value)) { + setTabHeight(value); + } + }; + + const onClick = () => { + props.layoutApi.setTabHeight(tabHeight); + }; + + return ( +
+ +
+ ); +}; const components = { inner_component: (props: IPanelProps) => { @@ -122,6 +155,7 @@ const components = { const backgroundColor = React.useMemo( () => + // "#1e1e1e", `rgb(${Math.floor(Math.random() * 256)},${Math.floor( Math.random() * 256 )},${Math.floor(Math.random() * 256)})`, @@ -143,6 +177,12 @@ const components = { ); }, + editor: Editor, + split_panel: SplitPanel, +}; + +const tabComponents = { + default: CustomTab, }; const nextGuid = (() => { @@ -177,7 +217,7 @@ export const TestGrid = () => { title: "Item 2", }); api.addPanelFromComponent({ - componentName: "test_component", + componentName: "split_panel", id: nextGuid(), title: "Item 3 with a long title", }); @@ -202,6 +242,18 @@ export const TestGrid = () => { 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]); const onAdd = () => { @@ -224,6 +276,7 @@ export const TestGrid = () => { _api.current?.layout(width, height); }; window.addEventListener("resize", callback); + callback(undefined); const dis = _api.current.onDidLayoutChange((event) => { console.log(event.kind); @@ -280,7 +333,6 @@ export const TestGrid = () => { if (!api) { return; } - console.log("create drag refs"); api.createDragTarget( { element: dragRef.current, content: "drag me" }, () => ({ @@ -294,10 +346,25 @@ export const TestGrid = () => { event.dataTransfer.setData("text/plain", "Panel2"); }; + const onAddEditor = () => { + api.addPanelFromComponent({ + id: "editor", + componentName: "editor", + tabComponentName: "default", + params: { layoutApi: api }, + }); + }; + + const onTabContextMenu = (event: MouseEvent) => {}; + return ( -
+
+ @@ -335,9 +402,12 @@ export const TestGrid = () => { // autoSizeToFitContainer={true} onReady={onReady} components={components} + tabComponents={tabComponents} debug={true} + // tabHeight={30} enableExternalDragEvents={true} // serializedLayout={data} + // onTabContextMenu={onTabContextMenu} />
); diff --git a/packages/splitview-demo/src/layout-grid/splitPanel.tsx b/packages/splitview-demo/src/layout-grid/splitPanel.tsx new file mode 100644 index 000000000..55948381c --- /dev/null +++ b/packages/splitview-demo/src/layout-grid/splitPanel.tsx @@ -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
hiya
; + }, +}; + +export const SplitPanel = (props: IPanelProps) => { + const api = React.useRef(); + + 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 ( + + ); +}; diff --git a/packages/splitview-react/src/bridge/pane.tsx b/packages/splitview-react/src/bridge/pane.tsx index 427509f03..e045fbd46 100644 --- a/packages/splitview-react/src/bridge/pane.tsx +++ b/packages/splitview-react/src/bridge/pane.tsx @@ -16,7 +16,7 @@ export interface IPaneComponentRef { layout: (size: number, orthogonalSize: number) => void; } -export type PaneComponent = React.RefForwardingComponent< +export type PaneComponent = React.ForwardRefRenderFunction< IPaneComponentRef, IPaneComponentProps >; @@ -27,7 +27,7 @@ export interface IPaneHeaderComponentProps extends IViewWithReactComponent { userprops?: { [index: string]: any }; } -export type PaneHeaderComponent = React.RefForwardingComponent< +export type PaneHeaderComponent = React.ForwardRefRenderFunction< {}, IPaneHeaderComponentProps >; diff --git a/packages/splitview-react/src/bridge/view.tsx b/packages/splitview-react/src/bridge/view.tsx index c6fbd242c..62bc25e5c 100644 --- a/packages/splitview-react/src/bridge/view.tsx +++ b/packages/splitview-react/src/bridge/view.tsx @@ -12,7 +12,7 @@ export interface IViewComponentRef { layout: (size: number, orthogonalSize: number) => void; } -export type ViewComponent = React.RefForwardingComponent< +export type ViewComponent = React.ForwardRefRenderFunction< IViewComponentRef, IViewComponentProps >; diff --git a/packages/splitview-react/src/paneview.tsx b/packages/splitview-react/src/paneview.tsx index a43730427..f260e0662 100644 --- a/packages/splitview-react/src/paneview.tsx +++ b/packages/splitview-react/src/paneview.tsx @@ -24,19 +24,19 @@ export interface IPaneViewReactProps { initialLayout?: PaneViewSerializedConfig; } -export type PaneViewReadyEvent = { +export interface PaneViewReadyEvent { api: PaneviewApi; -}; +} -export type PaneViewSerializedConfig = { +export interface PaneViewSerializedConfig { views: Array< Omit & { size?: number; } >; -}; +} -export type PaneviewApi = { +export interface PaneviewApi { add: ( options: Omit & { size?: number; @@ -45,7 +45,7 @@ export type PaneviewApi = { ) => void; moveView: (from: number, to: number) => void; toJSON: () => {}; -}; +} export interface IPaneViewComponentRef { layout: (size: number, orthogonalSize: number) => void; diff --git a/packages/splitview-react/src/splitview.tsx b/packages/splitview-react/src/splitview.tsx index 404235ba5..935007dd9 100644 --- a/packages/splitview-react/src/splitview.tsx +++ b/packages/splitview-react/src/splitview.tsx @@ -9,15 +9,15 @@ export interface IViewWithReactComponent extends IBaseView { component: ViewComponent; } -export type OnReadyEvent = { +export interface OnReadyEvent { api: SplitviewApi; -}; +} -export type SerializedConfig = { +export interface SerializedConfig { views: Array & { size?: number }>; -}; +} -export type SplitviewApi = { +export interface SplitviewApi { add: ( options: Omit & { size?: number; @@ -26,7 +26,7 @@ export type SplitviewApi = { ) => void; moveView: (from: number, to: number) => void; toJSON: () => {}; -}; +} export interface ISplitViewReactProps { orientation: Orientation; diff --git a/packages/splitview/package.json b/packages/splitview/package.json index a40b3b281..f58f468c5 100644 --- a/packages/splitview/package.json +++ b/packages/splitview/package.json @@ -6,7 +6,7 @@ "types": "dist/esm/index.d.ts", "scripts": { "build": "gulp run", - "docs": "typedoc --excludeNotExported --excludePrivate true --mode file --out typedocs/ src/" + "docs": "typedoc" }, "author": "", "license": "ISC", diff --git a/packages/splitview/src/gridview/gridview.ts b/packages/splitview/src/gridview/gridview.ts index ec423c0cd..7e8fc1921 100644 --- a/packages/splitview/src/gridview/gridview.ts +++ b/packages/splitview/src/gridview/gridview.ts @@ -1,5 +1,5 @@ import { Orientation, Sizing } from "../splitview/splitview"; -import { Target } from "../groupview/droptarget/droptarget"; +import { Position } from "../groupview/droptarget/droptarget"; import { tail } from "../array"; import { LeafNode } from "./leafNode"; import { BranchNode } from "./branchNode"; @@ -91,7 +91,7 @@ export function getGridLocation(element: HTMLElement): number[] { export function getRelativeLocation( rootOrientation: Orientation, location: number[], - direction: Target + direction: Position ): number[] { const orientation = getLocationOrientation(rootOrientation, location); const directionOrientation = getDirectionOrientation(direction); @@ -99,20 +99,20 @@ export function getRelativeLocation( if (orientation === directionOrientation) { let [rest, index] = tail(location); - if (direction === Target.Right || direction === Target.Bottom) { + if (direction === Position.Right || direction === Position.Bottom) { index += 1; } return [...rest, index]; } else { const index = - direction === Target.Right || direction === Target.Bottom ? 1 : 0; + direction === Position.Right || direction === Position.Bottom ? 1 : 0; return [...location, index]; } } -export function getDirectionOrientation(direction: Target): Orientation { - return direction === Target.Top || direction === Target.Bottom +export function getDirectionOrientation(direction: Position): Orientation { + return direction === Position.Top || direction === Position.Bottom ? Orientation.VERTICAL : Orientation.HORIZONTAL; } @@ -193,9 +193,9 @@ export interface INodeDescriptor { visible?: boolean; } -export type IViewDeserializer = { +export interface IViewDeserializer { fromJSON: (data: {}) => IGridView; -}; +} export class Gridview { private _root: BranchNode; diff --git a/packages/splitview/src/groupview/droptarget/dataTransfer.ts b/packages/splitview/src/groupview/droptarget/dataTransfer.ts index bd44330fb..50360e46f 100644 --- a/packages/splitview/src/groupview/droptarget/dataTransfer.ts +++ b/packages/splitview/src/groupview/droptarget/dataTransfer.ts @@ -11,12 +11,12 @@ export enum DragType { EXTERNAL = "external_group_drag", } -export type DragItem = { +export interface DragItem { itemId: string; groupId: string; -}; +} -export type ExternalDragItem = PanelOptions; +export interface ExternalDragItem extends PanelOptions {} export type DataObject = DragItem | ExternalDragItem; diff --git a/packages/splitview/src/groupview/droptarget/droptarget.scss b/packages/splitview/src/groupview/droptarget/droptarget.scss index ef35e94a3..45028835a 100644 --- a/packages/splitview/src/groupview/droptarget/droptarget.scss +++ b/packages/splitview/src/groupview/droptarget/droptarget.scss @@ -20,18 +20,18 @@ transition-duration: 0.15s; transition-timing-function: ease-out; - &.left { + &.left, + &.right { width: 50%; } &.right { - left: 50%; - width: 50%; + transform: translate(100%, 0%); } &.bottom { - top: 50%; - height: 50%; + transform: translate(0%, 100%); } - &.top { + &.top, + &.bottom { height: 50%; } } diff --git a/packages/splitview/src/groupview/droptarget/droptarget.ts b/packages/splitview/src/groupview/droptarget/droptarget.ts index 8f279e666..d100a546e 100644 --- a/packages/splitview/src/groupview/droptarget/droptarget.ts +++ b/packages/splitview/src/groupview/droptarget/droptarget.ts @@ -1,7 +1,7 @@ import { Emitter, Event } from "../../events"; import { DataTransferSingleton } from "./dataTransfer"; -export enum Target { +export enum Position { Top = "Top", Left = "Left", Bottom = "Bottom", @@ -9,10 +9,10 @@ export enum Target { Center = "Center", } -export type DroptargetEvent = { - target: Target; +export interface DroptargetEvent { + position: Position; event: DragEvent; -}; +} const HAS_PROCESSED_KEY = "__drop_target_processed__"; @@ -39,7 +39,7 @@ const toggleClassName = ( export class Droptarget { private target: HTMLElement; private overlay: HTMLElement; - private state: Target; + private state: Position | undefined; private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; @@ -106,7 +106,7 @@ export class Droptarget { this.removeDropTarget(); if (!hasProcessed(event)) { - this._onDidChange.fire({ target: this.state, event }); + this._onDidChange.fire({ position: this.state, event }); } else { console.debug("[dragtarget] already processed"); } @@ -140,15 +140,15 @@ export class Droptarget { toggleClassName(this.overlay, "bottom", isBottom); if (isRight) { - this.state = Target.Right; + this.state = Position.Right; } else if (isLeft) { - this.state = Target.Left; + this.state = Position.Left; } else if (isTop) { - this.state = Target.Top; + this.state = Position.Top; } else if (isBottom) { - this.state = Target.Bottom; + this.state = Position.Bottom; } else { - this.state = Target.Center; + this.state = Position.Center; } }; diff --git a/packages/splitview/src/groupview/events.ts b/packages/splitview/src/groupview/events.ts index eb5931222..0c0e0522c 100644 --- a/packages/splitview/src/groupview/events.ts +++ b/packages/splitview/src/groupview/events.ts @@ -1,11 +1,6 @@ import { DroptargetEvent } from "./droptarget/droptarget"; -export enum TabChangedEventType { - CLICK, -} - -export type TabChangedEvent = { type: TabChangedEventType }; -export type TabDropEvent = { +export interface TabDropEvent { event: DroptargetEvent; index?: number; -}; +} diff --git a/packages/splitview/src/groupview/groupview.ts b/packages/splitview/src/groupview/groupview.ts index b40a937e4..dae75bd01 100644 --- a/packages/splitview/src/groupview/groupview.ts +++ b/packages/splitview/src/groupview/groupview.ts @@ -1,8 +1,8 @@ import { IDisposable, CompositeDisposable, Disposable } from "../lifecycle"; -import { ITabContainer, TabContainer } from "./tabs/tabContainer"; -import { IContentContainer, ContentContainer } from "./content"; +import { ITabContainer, TabContainer } from "./titlebar/tabContainer"; +import { IContentContainer, ContentContainer } from "./panel/content/content"; 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 { IGroupAccessor, Layout } from "../layout"; import { toggleClass } from "../dom"; @@ -44,12 +44,12 @@ export interface IGroupItem { body: { element: HTMLElement }; } -type GroupMoveEvent = { +interface GroupMoveEvent { groupId: string; itemId: string; - target: Target; + target: Position; index?: number; -}; +} export interface GroupOptions { panels: IPanel[]; @@ -88,11 +88,11 @@ export interface IGroupview extends IDisposable, IGridView { moveToPrevious(options?: { panel?: IPanel; suppressRoll?: boolean }): void; } -export type GroupDropEvent = { +export interface GroupDropEvent { event: DragEvent; - target: Target; + target: Position; index?: number; -}; +} export class Groupview extends CompositeDisposable implements IGroupview { private _element: HTMLElement; @@ -129,6 +129,7 @@ export class Groupview extends CompositeDisposable implements IGroupview { set tabHeight(height: number) { this.tabContainer.height = height; + this.layout(this._width, this._height); } get isActive() { @@ -290,7 +291,7 @@ export class Groupview extends CompositeDisposable implements IGroupview { this.dropTarget.onDidChange((event) => { // if we've center dropped on ourself then ignore if ( - event.target === Target.Center && + event.position === Position.Center && this.tabContainer.hasActiveDragEvent ) { return; @@ -531,18 +532,18 @@ export class Groupview extends CompositeDisposable implements IGroupview { private handleDropEvent(event: DroptargetEvent, index?: number) { if (isPanelTransferEvent(event.event)) { - this.handlePanelDropEvent(event.event, event.target, index); + this.handlePanelDropEvent(event.event, event.position, index); return; } - this._onDrop.fire({ event: event.event, target: event.target, index }); + this._onDrop.fire({ event: event.event, target: event.position, index }); console.debug("[customDropEvent]"); } private handlePanelDropEvent( event: DragEvent, - target: Target, + target: Position, index?: number ) { const dataObject = extractData(event); @@ -550,10 +551,10 @@ export class Groupview extends CompositeDisposable implements IGroupview { if (isTabDragEvent(dataObject)) { const { groupId, itemId } = dataObject; const isSameGroup = this.id === groupId; - if (isSameGroup) { - const index = this.tabContainer.indexOf(itemId); - if (index > -1 && index === this.panels.length - 1) { - console.debug("[tabs] dropped in empty space"); + if (isSameGroup && !target) { + const oldIndex = this.tabContainer.indexOf(itemId); + if (oldIndex === index) { + console.debug("[tabs] drop indicates no change in position"); return; } } diff --git a/packages/splitview/src/groupview/panel/api.ts b/packages/splitview/src/groupview/panel/api.ts index 62b261f32..65563508e 100644 --- a/packages/splitview/src/groupview/panel/api.ts +++ b/packages/splitview/src/groupview/panel/api.ts @@ -4,15 +4,15 @@ import { ClosePanelResult } from "./parts"; import { IPanel } from "./types"; import { CompositeDisposable, IDisposable } from "../../lifecycle"; -export type PanelStateChangeEvent = { +export interface PanelStateChangeEvent { isPanelVisible: boolean; isGroupActive: boolean; -}; +} -export type PanelDimensionChangeEvent = { +export interface PanelDimensionChangeEvent { width: number; height: number; -}; +} export interface PanelApi extends IDisposable { onDidPanelStateChange: Event; diff --git a/packages/splitview/src/groupview/content.ts b/packages/splitview/src/groupview/panel/content/content.ts similarity index 88% rename from packages/splitview/src/groupview/content.ts rename to packages/splitview/src/groupview/panel/content/content.ts index 88f0637d4..3405585f4 100644 --- a/packages/splitview/src/groupview/content.ts +++ b/packages/splitview/src/groupview/panel/content/content.ts @@ -1,6 +1,6 @@ -import { CompositeDisposable, IDisposable } from "../lifecycle"; -import { Emitter, Event } from "../events"; -import { trackFocus } from "../dom"; +import { CompositeDisposable, IDisposable } from "../../../lifecycle"; +import { Emitter, Event } from "../../../events"; +import { trackFocus } from "../../../dom"; export interface IContentContainer extends IDisposable { onDidFocus: Event; diff --git a/packages/splitview/src/groupview/panel/panel.ts b/packages/splitview/src/groupview/panel/panel.ts index 2dd7cdaf2..a0214376b 100644 --- a/packages/splitview/src/groupview/panel/panel.ts +++ b/packages/splitview/src/groupview/panel/panel.ts @@ -149,7 +149,11 @@ export class DefaultPanel extends CompositeDisposable implements IPanel { } 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() { diff --git a/packages/splitview/src/groupview/panel/parts.ts b/packages/splitview/src/groupview/panel/parts.ts index b68d271cc..f52bcf778 100644 --- a/packages/splitview/src/groupview/panel/parts.ts +++ b/packages/splitview/src/groupview/panel/parts.ts @@ -14,13 +14,13 @@ interface Methods extends IDisposable { setVisible(isPanelVisible: boolean, isGroupVisible: boolean): void; } -export type WatermarkPartInitParameters = { +export interface WatermarkPartInitParameters { accessor: IGroupAccessor; -}; +} -export type PartInitParameters = { +export interface PartInitParameters extends PanelInitParameters { api: PanelApi; -} & PanelInitParameters; +} export interface PanelHeaderPart extends Methods { id: string; diff --git a/packages/splitview/src/groupview/tabs/tab.ts b/packages/splitview/src/groupview/panel/tab/tab.ts similarity index 80% rename from packages/splitview/src/groupview/tabs/tab.ts rename to packages/splitview/src/groupview/panel/tab/tab.ts index 2971e1d19..cbbf51535 100644 --- a/packages/splitview/src/groupview/tabs/tab.ts +++ b/packages/splitview/src/groupview/panel/tab/tab.ts @@ -1,23 +1,31 @@ -import { addDisposableListener, Emitter, Event } from "../../events"; -import { Droptarget, DroptargetEvent } from "../droptarget/droptarget"; -import { CompositeDisposable } from "../../lifecycle"; -import { TabChangedEvent, TabDropEvent, TabChangedEventType } from "../events"; -import { IGroupview } from "../groupview"; +import { addDisposableListener, Emitter, Event } from "../../../events"; +import { Droptarget, DroptargetEvent } from "../../droptarget/droptarget"; +import { CompositeDisposable } from "../../../lifecycle"; +import { IGroupview } from "../../groupview"; import { DataTransferSingleton, DATA_KEY, DragType, - extractData, -} from "../droptarget/dataTransfer"; -import { IGroupAccessor } from "../../layout"; -import { toggleClass } from "../../dom"; +} from "../../droptarget/dataTransfer"; +// import { IGroupAccessor } from "../../layout"; +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 { id: string; element: HTMLElement; hasActiveDragEvent: boolean; setContent: (element: HTMLElement) => void; - onChanged: Event; + onChanged: Event; onDropped: Event; setActive(isActive: boolean): void; startDragEvent(): void; @@ -32,8 +40,8 @@ export class Tab extends CompositeDisposable implements ITab { private droptarget: Droptarget; private content: HTMLElement; - private readonly _onChanged = new Emitter(); - readonly onChanged: Event = this._onChanged.event; + private readonly _onChanged = new Emitter(); + readonly onChanged: Event = this._onChanged.event; private readonly _onDropped = new Emitter(); readonly onDropped: Event = this._onDropped.event; @@ -72,7 +80,10 @@ export class Tab extends CompositeDisposable implements ITab { if (ev.defaultPrevented) { 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) => { this.dragInPlayDetails = { isDragging: true, id: this.accessor.id }; diff --git a/packages/splitview/src/groupview/panel/types.ts b/packages/splitview/src/groupview/panel/types.ts index 3b1100199..00574d5bd 100644 --- a/packages/splitview/src/groupview/panel/types.ts +++ b/packages/splitview/src/groupview/panel/types.ts @@ -5,18 +5,18 @@ import { PanelHeaderPart, PanelContentPart, ClosePanelResult } from "./parts"; // objects -export type PanelUpdateEvent = { +export interface PanelUpdateEvent { params: { [key: string]: any }; -}; +} // init parameters -export type PanelInitParameters = { +export interface PanelInitParameters { title: string; suppressClosable?: boolean; params: { [index: string]: any }; state?: { [index: string]: any }; -}; +} // constructors diff --git a/packages/splitview/src/groupview/tabs/tabContainer.scss b/packages/splitview/src/groupview/titlebar/tabContainer.scss similarity index 100% rename from packages/splitview/src/groupview/tabs/tabContainer.scss rename to packages/splitview/src/groupview/titlebar/tabContainer.scss diff --git a/packages/splitview/src/groupview/tabs/tabContainer.ts b/packages/splitview/src/groupview/titlebar/tabContainer.ts similarity index 93% rename from packages/splitview/src/groupview/tabs/tabContainer.ts rename to packages/splitview/src/groupview/titlebar/tabContainer.ts index e9b638465..380ebf5b3 100644 --- a/packages/splitview/src/groupview/tabs/tabContainer.ts +++ b/packages/splitview/src/groupview/titlebar/tabContainer.ts @@ -1,8 +1,8 @@ import { IDisposable, CompositeDisposable } from "../../lifecycle"; 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 { hasProcessed } from "../droptarget/droptarget"; +import { hasProcessed, Position } from "../droptarget/droptarget"; import { TabDropEvent } from "../events"; import { IGroupview } from "../groupview"; @@ -144,7 +144,8 @@ export class TabContainer extends CompositeDisposable implements ITabContainer { } 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 const disposables = CompositeDisposable.from( tab.onChanged((event) => { - this.group.openPanel(panel); + switch (event.kind) { + case TabInteractionKind.CLICK: + this.group.openPanel(panel); + break; + case TabInteractionKind.CONTEXT_MENU: + // TODO finish + } }), tab.onDropped((event) => { this._onDropped.fire({ event, index: this.indexOf(tab) }); diff --git a/packages/splitview/src/index.ts b/packages/splitview/src/index.ts index c042303c3..929e0ecde 100644 --- a/packages/splitview/src/index.ts +++ b/packages/splitview/src/index.ts @@ -2,8 +2,8 @@ export * from "./splitview/splitview"; export * from "./splitview/paneview"; export * from "./gridview/gridview"; export * from "./groupview/groupview"; -export * from "./groupview/content"; -export * from "./groupview/tabs/tab"; +export * from "./groupview/panel/content/content"; +export * from "./groupview/panel/tab/tab"; export * from "./events"; export * from "./lifecycle"; export * from "./groupview/panel/panel"; @@ -12,6 +12,7 @@ export * from "./react/react"; export * from "./groupview/panel/types"; export * from "./groupview/panel/parts"; export * from "./react/layout"; +export * from "./react/splitview"; export * from "./react/reactContentPart"; export * from "./react/reactHeaderPart"; diff --git a/packages/splitview/src/layout/components/tab/defaultTab.scss b/packages/splitview/src/layout/components/tab/defaultTab.scss index 86e11f341..b21b8831d 100644 --- a/packages/splitview/src/layout/components/tab/defaultTab.scss +++ b/packages/splitview/src/layout/components/tab/defaultTab.scss @@ -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 { &.dragging { - color: var(--active-group-visible-panel-color); .tab-action { background-color: var(--active-group-visible-panel-color); } } &.active-tab > .default-tab { - background-color: var(--tab-background-visible); .tab-action { visibility: visible; } } &.inactive-tab > .default-tab { - background-color: var(--tab-background-hidden); .tab-action:not(.dirty) { visibility: hidden; } diff --git a/packages/splitview/src/layout/layout.scss b/packages/splitview/src/layout/layout.scss index 2406c0279..29f09a4f5 100644 --- a/packages/splitview/src/layout/layout.scss +++ b/packages/splitview/src/layout/layout.scss @@ -9,3 +9,55 @@ position: absolute; 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); + } +} diff --git a/packages/splitview/src/layout/layout.ts b/packages/splitview/src/layout/layout.ts index 0510aab0c..50bfdf5cc 100644 --- a/packages/splitview/src/layout/layout.ts +++ b/packages/splitview/src/layout/layout.ts @@ -1,5 +1,5 @@ import { Gridview, getRelativeLocation } from "../gridview/gridview"; -import { Target } from "../groupview/droptarget/droptarget"; +import { Position } from "../groupview/droptarget/droptarget"; import { getGridLocation } from "../gridview/gridview"; import { tail, sequenceEquals } from "../array"; import { @@ -45,10 +45,10 @@ import { const nextGroupId = sequentialNumberGenerator(); const nextLayoutId = sequentialNumberGenerator(); -export type PanelReference = { +export interface PanelReference { update: (event: { params: { [key: string]: any } }) => void; remove: () => void; -}; +} export interface Api { layout(width: number, height: number): void; @@ -56,6 +56,7 @@ export interface Api { setAutoResizeToFit(enabled: boolean): void; resizeToFit(): void; setTabHeight(height: number): void; + getTabHeight(): number; size: number; totalPanels: number; // lifecycle @@ -90,7 +91,7 @@ export interface IGroupAccessor { referenceGroup: IGroupview, groupId: string, itemId: string, - target: Target, + target: Position, index?: number ): void; doSetGroupActive: (group: IGroupview) => void; @@ -399,11 +400,16 @@ export class Layout extends CompositeDisposable implements ILayout { } public setTabHeight(height: number) { + this.options.tabHeight = height; this.groups.forEach((value) => { value.value.tabHeight = height; }); } + public getTabHeight() { + return this.options.tabHeight; + } + public setAutoResizeToFit(enabled: boolean) { if (this.resizeTimer) { clearInterval(this.resizeTimer); @@ -435,7 +441,7 @@ export class Layout extends CompositeDisposable implements ILayout { const referenceGroup = this.findGroup(referencePanel); const target = this.toTarget(options.position.direction); - if (target === Target.Center) { + if (target === Position.Center) { referenceGroup.openPanel(panel); } else { const location = getGridLocation(referenceGroup.element); @@ -597,13 +603,13 @@ export class Layout extends CompositeDisposable implements ILayout { referenceGroup: IGroupview, groupId: string, itemId: string, - target: Target, + target: Position, index?: number ) { const sourceGroup = groupId ? this.groups.get(groupId).value : undefined; switch (target) { - case Target.Center: + case Position.Center: case undefined: const groupItem = sourceGroup?.removePanel(itemId) || this.panels.get(itemId).value; @@ -699,7 +705,7 @@ export class Layout extends CompositeDisposable implements ILayout { } this.moveGroup( group, - panel?.group.id, + panel?.group?.id, panel.id, event.target, event.index @@ -774,16 +780,16 @@ export class Layout extends CompositeDisposable implements ILayout { private toTarget(direction: "left" | "right" | "above" | "below" | "within") { switch (direction) { case "left": - return Target.Left; + return Position.Left; case "right": - return Target.Right; + return Position.Right; case "above": - return Target.Top; + return Position.Top; case "below": - return Target.Bottom; + return Position.Bottom; case "within": default: - return Target.Center; + return Position.Center; } } diff --git a/packages/splitview/src/layout/options.ts b/packages/splitview/src/layout/options.ts index 93abab529..60d19b7f3 100644 --- a/packages/splitview/src/layout/options.ts +++ b/packages/splitview/src/layout/options.ts @@ -1,4 +1,5 @@ import { IGroupview } from "../groupview/groupview"; +import { PanelApi } from "../groupview/panel/api"; import { PanelContentPart, PanelContentPartConstructor, @@ -6,11 +7,20 @@ import { PanelHeaderPartConstructor, WatermarkConstructor, } 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; createTabWrapper: (id: string, component: any) => PanelHeaderPart; -}; +} + +export interface TabContextMenuEvent { + event: MouseEvent; + api: Api; + panelApi: PanelApi; + panel: IPanel; +} export interface LayoutOptions { tabComponents?: { @@ -31,6 +41,7 @@ export interface LayoutOptions { tabHeight?: number; debug?: boolean; enableExternalDragEvents?: boolean; + onTabContextMenu?: (event: TabContextMenuEvent) => void; } export interface PanelOptions { diff --git a/packages/splitview/src/react/layout.tsx b/packages/splitview/src/react/layout.tsx index 54f176812..eb83b572d 100644 --- a/packages/splitview/src/react/layout.tsx +++ b/packages/splitview/src/react/layout.tsx @@ -6,9 +6,9 @@ import { ReactPanelHeaderPart } from "./reactHeaderPart"; import { IPanelProps } from "./react"; import { ReactPanelDeserialzier } from "./deserializer"; -export type OnReadyEvent = { +export interface OnReadyEvent { api: Api; -}; +} export interface ReactLayout { addPortal: (portal: React.ReactPortal) => IDisposable; diff --git a/packages/splitview/src/react/reactView.ts b/packages/splitview/src/react/reactView.ts new file mode 100644 index 000000000..db91aa68d --- /dev/null +++ b/packages/splitview/src/react/reactView.ts @@ -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 = 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); + } +} diff --git a/packages/splitview/src/react/splitview.tsx b/packages/splitview/src/react/splitview.tsx new file mode 100644 index 000000000..54e2ff18c --- /dev/null +++ b/packages/splitview/src/react/splitview.tsx @@ -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(); + const splitview = React.useRef(); + const [portals, setPortals] = React.useState([]); + + 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 ( +
+ {portals} +
+ ); +}; diff --git a/packages/splitview/src/theme.scss b/packages/splitview/src/theme.scss index e30a6b092..7646fef0f 100644 --- a/packages/splitview/src/theme.scss +++ b/packages/splitview/src/theme.scss @@ -4,11 +4,13 @@ --title-bar-background-color: #252526; --title-bar-scroll-bar-color: #888; // - --tab-background-visible: #1e1e1e; - --tab-background-hidden: #2d2d2d; + --active-tab-background-visible: #1e1e1e; + --active-tab-background-hidden: #2d2d2d; + --inactive-tab-background-visible: #1e1e1e; + --inactive-tab-background-hidden: #2d2d2d; --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-hidden-panel-color: #969696; @@ -20,3 +22,20 @@ // --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); + } + } + } +} diff --git a/packages/splitview/typedoc.json b/packages/splitview/typedoc.json new file mode 100644 index 000000000..a8ab0cde3 --- /dev/null +++ b/packages/splitview/typedoc.json @@ -0,0 +1,11 @@ +{ + "out": "typedocs", + "mode": "file", + "inputFiles": ["./src"], + "exclude": ["**/_test/**/*.*", "**/index.ts"], + "ignoreCompilerErrors": true, + "disableOutputCheck": true, + "excludeExternals": true, + "excludePrivate": true, + "excludeNotExported": true +}