diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 10a0ad555..d4a057c08 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -13,8 +13,11 @@ "/packages/docs/sandboxes/externaldnd-dockview", "/packages/docs/sandboxes/floatinggroup-dockview", "/packages/docs/sandboxes/fullwidthtab-dockview", + "/packages/docs/sandboxes/headeractions-dockview", + "/packages/docs/sandboxes/ide-example", "/packages/docs/sandboxes/groupcontol-dockview", "/packages/docs/sandboxes/iframe-dockview", + "/packages/docs/sandboxes/keyboard-dockview", "/packages/docs/sandboxes/layout-dockview", "/packages/docs/sandboxes/lockedgroup-dockview", "/packages/docs/sandboxes/nativeapp-dockview", @@ -33,4 +36,4 @@ "/packages/docs/sandboxes/javascript/vanilla-dockview" ], "node": "16" -} +} \ No newline at end of file diff --git a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts index 664057572..711d2ee0b 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts @@ -638,4 +638,214 @@ describe('tabsContainer', () => { expect(preventDefaultSpy).toBeCalledTimes(1); expect(accessor.addFloatingGroup).toBeCalledTimes(1); }); + + test('pre header actions', () => { + const accessorMock = jest.fn(() => { + return (>{ + options: {}, + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + element: document.createElement('div'), + addFloatingGroup: jest.fn(), + getGroupPanel: jest.fn(), + }) as DockviewComponent; + }); + + const groupPanelMock = jest.fn(() => { + return (>{ + api: { isFloating: true } as any, + model: {} as any, + }) as DockviewGroupPanel; + }); + + const accessor = new accessorMock(); + const groupPanel = new groupPanelMock(); + + const cut = new TabsContainer(accessor, groupPanel); + + const panelMock = jest.fn((id: string) => { + const partial: Partial = { + id, + + view: { + tab: { + element: document.createElement('div'), + } as any, + content: { + element: document.createElement('div'), + } as any, + } as any, + }; + return partial as IDockviewPanel; + }); + + const panel = new panelMock('test_id'); + cut.openPanel(panel); + + let result = cut.element.querySelector('.pre-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(0); + + const actions = document.createElement('div'); + cut.setPrefixActionsElement(actions); + + result = cut.element.querySelector('.pre-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(1); + expect(result!.childNodes.item(0)).toBe(actions); + + const updatedActions = document.createElement('div'); + cut.setPrefixActionsElement(updatedActions); + + result = cut.element.querySelector('.pre-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(1); + expect(result!.childNodes.item(0)).toBe(updatedActions); + + cut.setPrefixActionsElement(undefined); + + result = cut.element.querySelector('.pre-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(0); + }); + + test('left header actions', () => { + const accessorMock = jest.fn(() => { + return (>{ + options: {}, + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + element: document.createElement('div'), + addFloatingGroup: jest.fn(), + getGroupPanel: jest.fn(), + }) as DockviewComponent; + }); + + const groupPanelMock = jest.fn(() => { + return (>{ + api: { isFloating: true } as any, + model: {} as any, + }) as DockviewGroupPanel; + }); + + const accessor = new accessorMock(); + const groupPanel = new groupPanelMock(); + + const cut = new TabsContainer(accessor, groupPanel); + + const panelMock = jest.fn((id: string) => { + const partial: Partial = { + id, + + view: { + tab: { + element: document.createElement('div'), + } as any, + content: { + element: document.createElement('div'), + } as any, + } as any, + }; + return partial as IDockviewPanel; + }); + + const panel = new panelMock('test_id'); + cut.openPanel(panel); + + let result = cut.element.querySelector('.left-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(0); + + const actions = document.createElement('div'); + cut.setLeftActionsElement(actions); + + result = cut.element.querySelector('.left-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(1); + expect(result!.childNodes.item(0)).toBe(actions); + + const updatedActions = document.createElement('div'); + cut.setLeftActionsElement(updatedActions); + + result = cut.element.querySelector('.left-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(1); + expect(result!.childNodes.item(0)).toBe(updatedActions); + + cut.setLeftActionsElement(undefined); + + result = cut.element.querySelector('.left-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(0); + }); + + test('right header actions', () => { + const accessorMock = jest.fn(() => { + return (>{ + options: {}, + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + element: document.createElement('div'), + addFloatingGroup: jest.fn(), + getGroupPanel: jest.fn(), + }) as DockviewComponent; + }); + + const groupPanelMock = jest.fn(() => { + return (>{ + api: { isFloating: true } as any, + model: {} as any, + }) as DockviewGroupPanel; + }); + + const accessor = new accessorMock(); + const groupPanel = new groupPanelMock(); + + const cut = new TabsContainer(accessor, groupPanel); + + const panelMock = jest.fn((id: string) => { + const partial: Partial = { + id, + + view: { + tab: { + element: document.createElement('div'), + } as any, + content: { + element: document.createElement('div'), + } as any, + } as any, + }; + return partial as IDockviewPanel; + }); + + const panel = new panelMock('test_id'); + cut.openPanel(panel); + + let result = cut.element.querySelector('.right-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(0); + + const actions = document.createElement('div'); + cut.setRightActionsElement(actions); + + result = cut.element.querySelector('.right-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(1); + expect(result!.childNodes.item(0)).toBe(actions); + + const updatedActions = document.createElement('div'); + cut.setRightActionsElement(updatedActions); + + result = cut.element.querySelector('.right-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(1); + expect(result!.childNodes.item(0)).toBe(updatedActions); + + cut.setRightActionsElement(undefined); + + result = cut.element.querySelector('.right-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(0); + }); }); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 1c99baabc..1dfac7ed4 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -4063,4 +4063,81 @@ describe('dockviewComponent', () => { expect(tabDragEvents.length).toBe(0); expect(groupDragEvents.length).toBe(1); }); + + test('that loading a corrupt layout throws an error and leaves a clean dockview behind', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + expect(dockview.groups.length).toBe(1); + expect(dockview.panels.length).toBe(1); + + expect(() => { + dockview.fromJSON({ + grid: { + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panelA'], + activeView: 'panelA', + id: '1', + }, + size: 841, + }, + { + type: 'leaf', + data: { + views: ['panelB'], + activeView: 'panelB', + id: '2', + }, + size: 842, + }, + ], + size: 530, + }, + width: 1683, + height: 530, + orientation: Orientation.HORIZONTAL, + }, + panels: { + panelA: { + id: 'panelA', + contentComponent: 'somethingBad', + title: 'Panel A', + }, + panelB: { + id: 'panelB', + contentComponent: 'panelB', + title: 'Panel B', + }, + }, + activeGroup: '1', + }); + }).toThrow( + "Cannot create 'panelA', no component 'somethingBad' provided" + ); + + expect(dockview.groups.length).toBe(0); + expect(dockview.panels.length).toBe(0); + }); }); diff --git a/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts b/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts index ec7654393..ed02acf4c 100644 --- a/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts +++ b/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts @@ -676,4 +676,100 @@ describe('splitview', () => { expect(addEventListenerSpy).toBeCalledTimes(3); expect(removeEventListenerSpy).toBeCalledTimes(3); }); + + test('setViewVisible', () => { + const splitview = new Splitview(container, { + orientation: Orientation.HORIZONTAL, + proportionalLayout: false, + }); + splitview.layout(900, 500); + + const view1 = new Testview(0, 1000); + const view2 = new Testview(0, 1000); + const view3 = new Testview(0, 1000); + + splitview.addView(view1); + splitview.addView(view2); + splitview.addView(view3); + + expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]); + + splitview.setViewVisible(0, false); + expect([view1.size, view2.size, view3.size]).toEqual([0, 300, 600]); + + splitview.setViewVisible(0, true); + expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]); + }); + + test('setViewVisible with one view having high layout priority', () => { + const splitview = new Splitview(container, { + orientation: Orientation.HORIZONTAL, + proportionalLayout: false, + }); + splitview.layout(900, 500); + + const view1 = new Testview(0, 1000); + const view2 = new Testview(0, 1000, LayoutPriority.High); + const view3 = new Testview(0, 1000); + + splitview.addView(view1); + splitview.addView(view2); + splitview.addView(view3); + + expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]); + + splitview.setViewVisible(0, false); + expect([view1.size, view2.size, view3.size]).toEqual([0, 600, 300]); + + splitview.setViewVisible(0, true); + expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]); + }); + + test('set view size', () => { + const splitview = new Splitview(container, { + orientation: Orientation.HORIZONTAL, + proportionalLayout: false, + }); + splitview.layout(900, 500); + + const view1 = new Testview(0, 1000); + const view2 = new Testview(0, 1000); + const view3 = new Testview(0, 1000); + + splitview.addView(view1); + splitview.addView(view2); + splitview.addView(view3); + + expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]); + + view1.fireChangeEvent({ size: 0 }); + expect([view1.size, view2.size, view3.size]).toEqual([0, 300, 600]); + + view1.fireChangeEvent({ size: 300 }); + expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]); + }); + + test('set view size with one view having high layout priority', () => { + const splitview = new Splitview(container, { + orientation: Orientation.HORIZONTAL, + proportionalLayout: false, + }); + splitview.layout(900, 500); + + const view1 = new Testview(0, 1000); + const view2 = new Testview(0, 1000, LayoutPriority.High); + const view3 = new Testview(0, 1000); + + splitview.addView(view1); + splitview.addView(view2); + splitview.addView(view3); + + expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]); + + view1.fireChangeEvent({ size: 0 }); + expect([view1.size, view2.size, view3.size]).toEqual([0, 600, 300]); + + view1.fireChangeEvent({ size: 300 }); + expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]); + }); }); diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 4a3931cf2..77a3f7182 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -43,6 +43,7 @@ export interface ITabsContainer extends IDisposable { openPanel: (panel: IDockviewPanel, index?: number) => void; setRightActionsElement(element: HTMLElement | undefined): void; setLeftActionsElement(element: HTMLElement | undefined): void; + setPrefixActionsElement(element: HTMLElement | undefined): void; show(): void; hide(): void; } @@ -55,12 +56,14 @@ export class TabsContainer private readonly tabContainer: HTMLElement; private readonly rightActionsContainer: HTMLElement; private readonly leftActionsContainer: HTMLElement; + private readonly preActionsContainer: HTMLElement; private readonly voidContainer: VoidContainer; private tabs: IValueDisposable[] = []; private selectedIndex = -1; private rightActions: HTMLElement | undefined; private leftActions: HTMLElement | undefined; + private preActions: HTMLElement | undefined; private _hidden = false; @@ -129,6 +132,20 @@ export class TabsContainer } } + setPrefixActionsElement(element: HTMLElement | undefined): void { + if (this.preActions === element) { + return; + } + if (this.preActions) { + this.preActions.remove(); + this.preActions = undefined; + } + if (element) { + this.preActionsContainer.appendChild(element); + this.preActions = element; + } + } + get element(): HTMLElement { return this._element; } @@ -192,11 +209,15 @@ export class TabsContainer this.leftActionsContainer = document.createElement('div'); this.leftActionsContainer.className = 'left-actions-container'; + this.preActionsContainer = document.createElement('div'); + this.preActionsContainer.className = 'pre-actions-container'; + this.tabContainer = document.createElement('div'); this.tabContainer.className = 'tabs-container'; this.voidContainer = new VoidContainer(this.accessor, this.group); + this._element.appendChild(this.preActionsContainer); this._element.appendChild(this.tabContainer); this._element.appendChild(this.leftActionsContainer); this._element.appendChild(this.voidContainer.element); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 04a0eb280..bb8452f20 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -92,6 +92,7 @@ export type DockviewComponentUpdateOptions = Pick< | 'defaultTabComponent' | 'createLeftHeaderActionsElement' | 'createRightHeaderActionsElement' + | 'createPrefixHeaderActionsElement' | 'disableFloatingGroups' | 'floatingGroupBounds' >; @@ -649,33 +650,65 @@ export class DockviewComponent const createGroupFromSerializedState = (data: GroupPanelViewState) => { const { id, locked, hideHeader, views, activeView } = data; - const group = this.createGroup({ - id, - locked: !!locked, - hideHeader: !!hideHeader, - }); - - this._onDidAddGroup.fire(group); - - for (const child of views) { - const panel = this._deserializer.fromJSON(panels[child], group); - - const isActive = - typeof activeView === 'string' && activeView === panel.id; - - group.model.openPanel(panel, { - skipSetPanelActive: !isActive, - skipSetGroupActive: true, - }); + if (typeof id !== 'string') { + throw new Error('group id must be of type string'); } - if (!group.activePanel && group.panels.length > 0) { - group.model.openPanel(group.panels[group.panels.length - 1], { - skipSetGroupActive: true, - }); - } + let group: DockviewGroupPanel | undefined; - return group; + try { + group = this.createGroup({ + id, + locked: !!locked, + hideHeader: !!hideHeader, + }); + + this._onDidAddGroup.fire(group); + + for (const child of views) { + const panel = this._deserializer.fromJSON( + panels[child], + group + ); + + const isActive = + typeof activeView === 'string' && + activeView === panel.id; + + group.model.openPanel(panel, { + skipSetPanelActive: !isActive, + skipSetGroupActive: true, + }); + } + + if (!group.activePanel && group.panels.length > 0) { + group.model.openPanel( + group.panels[group.panels.length - 1], + { + skipSetGroupActive: true, + } + ); + } + + return group; + } catch (err) { + /** + * This is an odd case... we have failed to deserialize a view but we have already created a group, + * but we havn't registered that group with the gridview. + * We cannot use the removeGroup method because the group has only been partially added, we must + * manually dipose() of the view and remove it from being stored in the map. + */ + if (group) { + group.dispose(); + this._groups.delete(group.id); + } + + /** + * re-throw the error becasue we don't actually want to catch it, we just + * needed to do some clean-up before continuing. + */ + throw err; + } }; this.gridview.deserialize(grid, { diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index d05aa7308..0f3ec17ab 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -12,7 +12,7 @@ import { IContentContainer, } from './components/panel/content'; import { - GroupDragEvent, + GroupDragEvent, ITabsContainer, TabDragEvent, TabsContainer, @@ -144,6 +144,7 @@ export class DockviewGroupPanelModel private _isFloating = false; private _rightHeaderActions: IHeaderActionsRenderer | undefined; private _leftHeaderActions: IHeaderActionsRenderer | undefined; + private _prefixHeaderActions: IHeaderActionsRenderer | undefined; private mostRecentlyUsed: IDockviewPanel[] = []; @@ -398,6 +399,21 @@ export class DockviewGroupPanelModel this._leftHeaderActions.element ); } + + if (this.accessor.options.createPrefixHeaderActionsElement) { + this._prefixHeaderActions = + this.accessor.options.createPrefixHeaderActionsElement( + this.groupPanel + ); + this.addDisposables(this._prefixHeaderActions); + this._prefixHeaderActions.init({ + containerApi: new DockviewApi(this.accessor), + api: this.groupPanel.api, + }); + this.tabsContainer.setPrefixActionsElement( + this._prefixHeaderActions.element + ); + } } public indexOf(panel: IDockviewPanel): number { diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 920737a06..f9d2cc1c5 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -84,6 +84,9 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions { createLeftHeaderActionsElement?: ( group: DockviewGroupPanel ) => IHeaderActionsRenderer; + createPrefixHeaderActionsElement?: ( + group: DockviewGroupPanel + ) => IHeaderActionsRenderer; singleTabMode?: 'fullwidth' | 'default'; parentElement?: HTMLElement; disableFloatingGroups?: boolean; diff --git a/packages/dockview-core/src/resizable.ts b/packages/dockview-core/src/resizable.ts index 499ce6615..4df6366ec 100644 --- a/packages/dockview-core/src/resizable.ts +++ b/packages/dockview-core/src/resizable.ts @@ -29,6 +29,16 @@ export abstract class Resizable extends CompositeDisposable { */ return; } + + if (!document.body.contains(this._element)) { + /** + * since the event is dispatched through requestAnimationFrame there is a small chance + * the component is no longer attached to the DOM, if that is the case the dimensions + * are mostly likely all zero and meaningless. we should skip this case. + */ + return; + } + const { width, height } = entry.contentRect; this.layout(width, height); }) diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index 007a8e60a..0d56cce24 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -37,10 +37,11 @@ export interface SplitViewOptions { readonly proportionalLayout?: boolean; readonly styles?: ISplitviewStyles; } + export enum LayoutPriority { - Low = 'low', - High = 'high', - Normal = 'normal', + Low = 'low', // view is offered space last + High = 'high', // view is offered space first + Normal = 'normal', // view is offered space in view order } export interface IBaseView extends IDisposable { @@ -340,7 +341,22 @@ export class Splitview { item.size = size; - this.relayout([index]); + const indexes = range(this.viewItems.length).filter((i) => i !== index); + const lowPriorityIndexes = [ + ...indexes.filter( + (i) => this.viewItems[i].priority === LayoutPriority.Low + ), + index, + ]; + const highPriorityIndexes = indexes.filter( + (i) => this.viewItems[i].priority === LayoutPriority.High + ); + + /** + * add this view we are changing to the low-index list since we have determined the size + * here and don't want it changed + */ + this.relayout([...lowPriorityIndexes, index], highPriorityIndexes); } public addView( diff --git a/packages/dockview/src/dockview/dockview.tsx b/packages/dockview/src/dockview/dockview.tsx index 7c6bcc435..2485e6d81 100644 --- a/packages/dockview/src/dockview/dockview.tsx +++ b/packages/dockview/src/dockview/dockview.tsx @@ -67,6 +67,7 @@ export interface IDockviewReactProps { defaultTabComponent?: React.FunctionComponent; rightHeaderActionsComponent?: React.FunctionComponent; leftHeaderActionsComponent?: React.FunctionComponent; + prefixHeaderActionsComponent?: React.FunctionComponent; singleTabMode?: 'fullwidth' | 'default'; disableFloatingGroups?: boolean; floatingGroupBounds?: @@ -166,6 +167,10 @@ export const DockviewReact = React.forwardRef( props.rightHeaderActionsComponent, { addPortal } ), + createPrefixHeaderActionsElement: createGroupControlElement( + props.prefixHeaderActionsComponent, + { addPortal } + ), singleTabMode: props.singleTabMode, disableFloatingGroups: props.disableFloatingGroups, floatingGroupBounds: props.floatingGroupBounds, @@ -301,6 +306,18 @@ export const DockviewReact = React.forwardRef( }); }, [props.leftHeaderActionsComponent]); + React.useEffect(() => { + if (!dockviewRef.current) { + return; + } + dockviewRef.current.updateOptions({ + createPrefixHeaderActionsElement: createGroupControlElement( + props.prefixHeaderActionsComponent, + { addPortal } + ), + }); + }, [props.prefixHeaderActionsComponent]); + return (
= () => { @@ -850,7 +854,23 @@ A simple example showing events fired by `dockviewz that can be interacted with. react={EventsDockview} /> -## Advanced Examples +## Keyboard Navigation + +Keyboard shortcuts + + + +## Application with sidebars + + ### Nested Dockviews diff --git a/packages/docs/sandboxes/demo-dockview/src/app.tsx b/packages/docs/sandboxes/demo-dockview/src/app.tsx index 7e84a0747..0e5b2ff96 100644 --- a/packages/docs/sandboxes/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/demo-dockview/src/app.tsx @@ -191,6 +191,23 @@ const LeftControls = (props: IDockviewHeaderActionsProps) => { ); }; +const PrefixHeaderControls = (props: IDockviewHeaderActionsProps) => { + return ( +
+ +
+ ); +}; + const DockviewDemo = (props: { theme?: string }) => { const onReady = (event: DockviewReadyEvent) => { event.api.addPanel({ @@ -262,6 +279,7 @@ const DockviewDemo = (props: { theme?: string }) => { defaultTabComponent={headerComponents.default} rightHeaderActionsComponent={RightControls} leftHeaderActionsComponent={LeftControls} + prefixHeaderActionsComponent={PrefixHeaderControls} onReady={onReady} className={props.theme || 'dockview-theme-abyss'} /> diff --git a/packages/docs/sandboxes/ide-example/package.json b/packages/docs/sandboxes/ide-example/package.json new file mode 100644 index 000000000..5de7b1222 --- /dev/null +++ b/packages/docs/sandboxes/ide-example/package.json @@ -0,0 +1,32 @@ +{ + "name": "ide-example", + "description": "", + "keywords": [ + "dockview" + ], + "version": "1.0.0", + "main": "src/index.tsx", + "dependencies": { + "dockview": "*", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.11", + "typescript": "^4.9.5", + "react-scripts": "*" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] +} diff --git a/packages/docs/sandboxes/ide-example/public/index.html b/packages/docs/sandboxes/ide-example/public/index.html new file mode 100644 index 000000000..1f8a52426 --- /dev/null +++ b/packages/docs/sandboxes/ide-example/public/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + React App + + + + +
+ + + + diff --git a/packages/docs/sandboxes/ide-example/src/app.tsx b/packages/docs/sandboxes/ide-example/src/app.tsx new file mode 100644 index 000000000..d63b035f8 --- /dev/null +++ b/packages/docs/sandboxes/ide-example/src/app.tsx @@ -0,0 +1,154 @@ +import { + GridviewReact, + GridviewReadyEvent, + IGridviewPanelProps, + GridviewComponent, + Orientation, + GridviewApi, + LayoutPriority, +} from 'dockview'; +import * as React from 'react'; + +const components = { + 'left-sidebar': (props: IGridviewPanelProps<{ title: string }>) => { + return ( +
+ {props.params.title} +
+ ); + }, + 'middle-content': (props: IGridviewPanelProps<{ title: string }>) => { + return ( +
+ {props.params.title} +
+ ); + }, + 'right-sidebar': (props: IGridviewPanelProps<{ title: string }>) => { + return ( +
+ {props.params.title} +
+ ); + }, +}; + +const App = (props: { theme?: string }) => { + const [api, setApi] = React.useState(); + + const onReady = (event: GridviewReadyEvent) => { + event.api.fromJSON({ + grid: { + height: 1000, + width: 1000, + orientation: Orientation.HORIZONTAL, + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + id: 'left-sidebar-id', + component: 'left-sidebar', + snap: true, + minimumWidth: 100, + }, + size: 200, + }, + { + type: 'leaf', + data: { + id: 'middle-content-id', + component: 'middle-content', + priority: LayoutPriority.High, + minimumWidth: 100, + }, + size: 600, + }, + { + type: 'leaf', + data: { + id: 'right-sidebar-id', + component: 'right-sidebar', + snap: true, + minimumWidth: 100, + }, + size: 200, + }, + ], + }, + }, + }); + + setApi(event.api); + }; + + const onKeyPress = (event: React.KeyboardEvent) => { + if (!api) { + return; + } + + if (event.ctrlKey) { + if (event.code === 'ArrowLeft') { + const leftSidebarPanel = api.getPanel('left-sidebar-id'); + + if (leftSidebarPanel) { + leftSidebarPanel.api.setVisible(false); + } + } + } + + if (event.code === 'ArrowRight') { + const leftSidebarPanel = api.getPanel('left-sidebar-id'); + + if (leftSidebarPanel) { + leftSidebarPanel.api.setVisible(true); + } + } + }; + + return ( +
+
+ {'Use '} + {'Ctrl+ArrowLeft'} + {' and '} + {'Ctrl+ArrowRight'} + { + ' to show and hide the left sidebar. The right sidebar can be hidden by dragging it to the right.' + } +
+
+ +
+
+ ); +}; + +export default App; diff --git a/packages/docs/sandboxes/ide-example/src/index.tsx b/packages/docs/sandboxes/ide-example/src/index.tsx new file mode 100644 index 000000000..2fe1be232 --- /dev/null +++ b/packages/docs/sandboxes/ide-example/src/index.tsx @@ -0,0 +1,20 @@ +import { StrictMode } from 'react'; +import * as ReactDOMClient from 'react-dom/client'; +import './styles.css'; +import 'dockview/dist/styles/dockview.css'; + +import App from './app'; + +const rootElement = document.getElementById('root'); + +if (rootElement) { + const root = ReactDOMClient.createRoot(rootElement); + + root.render( + +
+ +
+
+ ); +} diff --git a/packages/docs/sandboxes/ide-example/src/styles.css b/packages/docs/sandboxes/ide-example/src/styles.css new file mode 100644 index 000000000..2198f8a37 --- /dev/null +++ b/packages/docs/sandboxes/ide-example/src/styles.css @@ -0,0 +1,15 @@ +body { + margin: 0px; + font-family: sans-serif; + text-align: center; +} + +#root { + height: 100vh; + width: 100vw; +} + +.app { + height: 100%; + +} diff --git a/packages/docs/sandboxes/ide-example/tsconfig.json b/packages/docs/sandboxes/ide-example/tsconfig.json new file mode 100644 index 000000000..cdc4fb5f5 --- /dev/null +++ b/packages/docs/sandboxes/ide-example/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "outDir": "build/dist", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true + } +} diff --git a/packages/docs/sandboxes/keyboard-dockview/package.json b/packages/docs/sandboxes/keyboard-dockview/package.json new file mode 100644 index 000000000..a5766dcba --- /dev/null +++ b/packages/docs/sandboxes/keyboard-dockview/package.json @@ -0,0 +1,34 @@ +{ + "name": "keyboard-dockview", + "description": "", + "keywords": [ + "dockview" + ], + "version": "1.0.0", + "main": "src/index.tsx", + "dependencies": { + "dockview": "*", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.11", + "@types/uuid": "^9.0.0", + "typescript": "^4.9.5", + "react-scripts": "*" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] +} diff --git a/packages/docs/sandboxes/keyboard-dockview/public/index.html b/packages/docs/sandboxes/keyboard-dockview/public/index.html new file mode 100644 index 000000000..5a4850c1d --- /dev/null +++ b/packages/docs/sandboxes/keyboard-dockview/public/index.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + React App + + + + +
+ + + + diff --git a/packages/docs/sandboxes/keyboard-dockview/src/app.scss b/packages/docs/sandboxes/keyboard-dockview/src/app.scss new file mode 100644 index 000000000..53fd8f0be --- /dev/null +++ b/packages/docs/sandboxes/keyboard-dockview/src/app.scss @@ -0,0 +1,21 @@ +.keyboard-example-panel { + padding: 20px; + color: white; + font-size: 13px; + + input { + &:focus { + outline: 1px solid dodgerblue; + } + } + + .keyboard-example-description { + padding: 10px 0px; + .keyboard-example-shortcut { + background-color: lightblue; + color: black; + padding: 2px 4px; + border-radius: 4px; + } + } +} diff --git a/packages/docs/sandboxes/keyboard-dockview/src/app.tsx b/packages/docs/sandboxes/keyboard-dockview/src/app.tsx new file mode 100644 index 000000000..021491507 --- /dev/null +++ b/packages/docs/sandboxes/keyboard-dockview/src/app.tsx @@ -0,0 +1,137 @@ +import { + DockviewApi, + DockviewReact, + DockviewReadyEvent, + IDockviewPanelProps, +} from 'dockview'; +import './app.scss'; +import * as React from 'react'; + +const components = { + default: (props: IDockviewPanelProps<{ title: string }>) => { + const [active, setActive] = React.useState(props.api.isActive); + const ref = React.useRef(null); + + React.useEffect(() => { + const disposable = props.api.onDidActiveChange((event) => { + setActive(props.api.isActive); + }); + + return () => { + disposable.dispose(); + }; + }, [props.api]); + + React.useEffect(() => { + if (!active) { + return; + } + + requestAnimationFrame(() => { + ref.current?.focus(); + }); + }, [active]); + + return ( +
+
+ {props.api.title} +
+
+ {'Use '} + + {'Ctrl+ArrowLeft'} + + {' and '} + + {'Ctrl+ArrowRight'} + + {' to nativgate between tabs.'} +
+ +
+
+ { + 'This input box should take focus when the panel is active to demonsrate managed focus' + } + +
+
+ +
+ {'isPanelActive: '} + {active ? 'true' : 'false'} +
+
+ ); + }, +}; + +const DockviewDemo = (props: { theme?: string }) => { + const [api, setApi] = React.useState(); + + const onReady = (event: DockviewReadyEvent) => { + event.api.addPanel({ + id: 'panel_1', + component: 'default', + title: 'Panel 1', + }); + event.api.addPanel({ + id: 'panel_2', + component: 'default', + title: 'Panel 2', + }); + event.api.addPanel({ + id: 'panel_3', + component: 'default', + title: 'Panel 3', + }); + event.api.addPanel({ + id: 'panel_4', + component: 'default', + title: 'Panel 4', + position: { referencePanel: 'panel_3', direction: 'right' }, + }); + event.api.addPanel({ + id: 'panel_5', + component: 'default', + title: 'Panel 5', + position: { referencePanel: 'panel_4', direction: 'within' }, + }); + + event.api.getPanel('panel_1')!.api.setActive(); + + setApi(event.api); + }; + + const onKeyDown = (event: React.KeyboardEvent) => { + if (!api) { + return; + } + + if (event.ctrlKey && event.code === 'ArrowLeft') { + // move backwards + api.moveToPrevious({ includePanel: true }); + } + + if (event.ctrlKey && event.code === 'ArrowRight') { + // move backwards + api.moveToNext({ includePanel: true }); + } + }; + + return ( +
+ +
+ ); +}; + +export default DockviewDemo; diff --git a/packages/docs/sandboxes/keyboard-dockview/src/index.tsx b/packages/docs/sandboxes/keyboard-dockview/src/index.tsx new file mode 100644 index 000000000..2fe1be232 --- /dev/null +++ b/packages/docs/sandboxes/keyboard-dockview/src/index.tsx @@ -0,0 +1,20 @@ +import { StrictMode } from 'react'; +import * as ReactDOMClient from 'react-dom/client'; +import './styles.css'; +import 'dockview/dist/styles/dockview.css'; + +import App from './app'; + +const rootElement = document.getElementById('root'); + +if (rootElement) { + const root = ReactDOMClient.createRoot(rootElement); + + root.render( + +
+ +
+
+ ); +} diff --git a/packages/docs/sandboxes/keyboard-dockview/src/styles.css b/packages/docs/sandboxes/keyboard-dockview/src/styles.css new file mode 100644 index 000000000..92b6a1b36 --- /dev/null +++ b/packages/docs/sandboxes/keyboard-dockview/src/styles.css @@ -0,0 +1,16 @@ +body { + margin: 0px; + color: white; + font-family: sans-serif; + text-align: center; +} + +#root { + height: 100vh; + width: 100vw; +} + +.app { + height: 100%; + +} diff --git a/packages/docs/sandboxes/keyboard-dockview/tsconfig.json b/packages/docs/sandboxes/keyboard-dockview/tsconfig.json new file mode 100644 index 000000000..cdc4fb5f5 --- /dev/null +++ b/packages/docs/sandboxes/keyboard-dockview/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "outDir": "build/dist", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true + } +}