mirror of
https://github.com/mathuo/dockview
synced 2025-09-14 13:17:58 +00:00
feat: angular
This commit is contained in:
parent
839027391d
commit
3374dc9400
@ -1,7 +1,7 @@
|
||||
import { JestConfigWithTsJest } from 'ts-jest';
|
||||
|
||||
const config: JestConfigWithTsJest = {
|
||||
preset: 'ts-jest',
|
||||
preset: 'jest-preset-angular',
|
||||
roots: ['<rootDir>/packages/dockview-angular'],
|
||||
modulePaths: ['<rootDir>/packages/dockview-angular/src'],
|
||||
displayName: { name: 'dockview-angular', color: 'blue' },
|
||||
@ -12,11 +12,9 @@ const config: JestConfigWithTsJest = {
|
||||
'!<rootDir>/packages/dockview-angular/src/**/index.ts',
|
||||
'!<rootDir>/packages/dockview-angular/src/public-api.ts',
|
||||
],
|
||||
setupFiles: [
|
||||
'<rootDir>/packages/dockview-angular/src/__tests__/__mocks__/resizeObserver.js',
|
||||
'<rootDir>/packages/dockview-angular/src/__tests__/__mocks__/angular-testing.js',
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/packages/dockview-angular/src/__tests__/setup-jest.ts'
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
|
||||
coveragePathIgnorePatterns: ['/node_modules/'],
|
||||
modulePathIgnorePatterns: [
|
||||
'<rootDir>/packages/dockview-angular/src/__tests__/__mocks__',
|
||||
@ -25,20 +23,19 @@ const config: JestConfigWithTsJest = {
|
||||
coverageDirectory: '<rootDir>/packages/dockview-angular/coverage/',
|
||||
testResultsProcessor: 'jest-sonar-reporter',
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.test.json',
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@angular/(.*)$': '<rootDir>/../../node_modules/@angular/$1',
|
||||
},
|
||||
testMatch: [
|
||||
'<rootDir>/packages/dockview-angular/src/**/*.spec.ts',
|
||||
'<rootDir>/packages/dockview-angular/src/**/*.test.ts'
|
||||
],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(.*\\.mjs$|@angular|rxjs))'
|
||||
],
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@ -56,11 +56,13 @@
|
||||
"dockview-core": "^4.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": ">=14.0.0",
|
||||
"@angular/common": ">=14.0.0",
|
||||
"@angular/core": ">=14.0.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ng-packagr": "^17.0.0"
|
||||
"jest-preset-angular": "^14.6.1",
|
||||
"ng-packagr": "^17.0.0",
|
||||
"zone.js": "^0.15.1"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,33 @@
|
||||
// NOTE: These tests require Angular testing dependencies to be installed in the root node_modules
|
||||
// For now they are commented out to demonstrate the build works
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Component, Injector, EnvironmentInjector } from '@angular/core';
|
||||
import { AngularRenderer } from '../lib/utils/angular-renderer';
|
||||
|
||||
@Component({
|
||||
selector: 'test-component',
|
||||
template: '<div class="test-component">{{ title || "Test" }} - {{ value || "default" }}</div>',
|
||||
})
|
||||
class TestComponent {
|
||||
title?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
describe('AngularRenderer', () => {
|
||||
let injector: Injector;
|
||||
let environmentInjector: EnvironmentInjector;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [TestComponent]
|
||||
}).compileComponents();
|
||||
|
||||
injector = TestBed.inject(Injector);
|
||||
environmentInjector = TestBed.inject(EnvironmentInjector);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
});
|
||||
|
||||
it('should be testable when Angular dependencies are available', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
@ -17,8 +43,7 @@ describe('AngularRenderer', () => {
|
||||
renderer.init(parameters);
|
||||
|
||||
expect(renderer.element).toBeTruthy();
|
||||
expect(renderer.element.tagName).toBe('DIV');
|
||||
expect(renderer.element.classList.contains('test-component')).toBe(true);
|
||||
expect(renderer.element.tagName).toBe('TEST-COMPONENT');
|
||||
});
|
||||
|
||||
it('should update component properties', () => {
|
||||
@ -28,13 +53,9 @@ describe('AngularRenderer', () => {
|
||||
environmentInjector
|
||||
});
|
||||
|
||||
// Initialize with initial parameters
|
||||
renderer.init({ title: 'Initial Title' });
|
||||
|
||||
// Update properties
|
||||
renderer.update({ title: 'Updated Title', value: 'new-value' });
|
||||
|
||||
// The component should have updated properties
|
||||
expect(renderer.element).toBeTruthy();
|
||||
});
|
||||
|
||||
@ -52,12 +73,10 @@ describe('AngularRenderer', () => {
|
||||
|
||||
renderer.dispose();
|
||||
|
||||
// After dispose, accessing element should throw
|
||||
expect(() => renderer.element).toThrow('Angular renderer not initialized');
|
||||
});
|
||||
|
||||
it('should handle component creation errors gracefully', () => {
|
||||
// Use an invalid component to trigger error
|
||||
const renderer = new AngularRenderer({
|
||||
component: null as any,
|
||||
injector,
|
||||
@ -79,7 +98,6 @@ describe('AngularRenderer', () => {
|
||||
renderer.init({ title: 'Test Title' });
|
||||
renderer.dispose();
|
||||
|
||||
// Should not throw
|
||||
expect(() => {
|
||||
renderer.update({ title: 'Updated Title' });
|
||||
}).not.toThrow();
|
||||
@ -94,7 +112,6 @@ describe('AngularRenderer', () => {
|
||||
|
||||
renderer.init({ title: 'Test Title' });
|
||||
|
||||
// Multiple dispose calls should not throw
|
||||
expect(() => {
|
||||
renderer.dispose();
|
||||
renderer.dispose();
|
||||
|
@ -4,11 +4,9 @@ import { By } from '@angular/platform-browser';
|
||||
|
||||
import { DockviewAngularComponent } from '../lib/dockview/dockview-angular.component';
|
||||
import { DockviewApi } from 'dockview-core';
|
||||
import { setupTestBed, getTestComponents, TestPanelComponent } from './__test_utils__/test-helpers';
|
||||
import { setupTestBed, getTestComponents } from './__test_utils__/test-helpers';
|
||||
|
||||
// NOTE: These tests require Angular testing dependencies to be installed in the root node_modules
|
||||
// For now they are commented out to demonstrate the build works
|
||||
describe.skip('DockviewAngularComponent', () => {
|
||||
describe('DockviewAngularComponent', () => {
|
||||
let component: DockviewAngularComponent;
|
||||
let fixture: ComponentFixture<DockviewAngularComponent>;
|
||||
let debugElement: DebugElement;
|
||||
@ -21,7 +19,6 @@ describe.skip('DockviewAngularComponent', () => {
|
||||
component = fixture.componentInstance;
|
||||
debugElement = fixture.debugElement;
|
||||
|
||||
// Set required inputs
|
||||
component.components = getTestComponents();
|
||||
});
|
||||
|
||||
@ -84,7 +81,6 @@ describe.skip('DockviewAngularComponent', () => {
|
||||
const api = component.getDockviewApi();
|
||||
const updateOptionsSpy = jest.spyOn(api!, 'updateOptions');
|
||||
|
||||
// Simulate input change
|
||||
component.className = 'test-class';
|
||||
component.ngOnChanges({
|
||||
className: {
|
||||
@ -105,12 +101,10 @@ describe.skip('DockviewAngularComponent', () => {
|
||||
|
||||
component.ngOnInit();
|
||||
|
||||
// API should be initialized without throwing
|
||||
expect(component.getDockviewApi()).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// Integration test with template
|
||||
@Component({
|
||||
template: `
|
||||
<dv-dockview
|
||||
@ -132,7 +126,7 @@ class TestHostComponent {
|
||||
}
|
||||
}
|
||||
|
||||
describe.skip('DockviewAngularComponent Integration', () => {
|
||||
describe('DockviewAngularComponent Integration', () => {
|
||||
let hostComponent: TestHostComponent;
|
||||
let fixture: ComponentFixture<TestHostComponent>;
|
||||
|
||||
@ -168,7 +162,5 @@ describe.skip('DockviewAngularComponent Integration', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(hostComponent.api).toBeDefined();
|
||||
// Additional assertions could be added here to verify the properties
|
||||
// were passed to the core dockview component
|
||||
});
|
||||
});
|
56
packages/dockview-angular/src/__tests__/setup-jest.ts
Normal file
56
packages/dockview-angular/src/__tests__/setup-jest.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv();
|
||||
|
||||
// Global mocks for browser APIs
|
||||
Object.defineProperty(window, 'CSS', {value: null});
|
||||
Object.defineProperty(window, 'getComputedStyle', {
|
||||
value: () => {
|
||||
return {
|
||||
display: 'none',
|
||||
appearance: ['-webkit-appearance']
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(document, 'doctype', {
|
||||
value: '<!DOCTYPE html>'
|
||||
});
|
||||
|
||||
Object.defineProperty(document.body.style, 'transform', {
|
||||
value: () => {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Mock ResizeObserver
|
||||
global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock getBoundingClientRect
|
||||
Element.prototype.getBoundingClientRect = jest.fn(() => ({
|
||||
width: 100,
|
||||
height: 100,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 100,
|
||||
right: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock scrollIntoView
|
||||
HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
HTMLElement.prototype.focus = jest.fn();
|
||||
HTMLElement.prototype.blur = jest.fn();
|
||||
|
||||
// Mock requestAnimationFrame
|
||||
global.requestAnimationFrame = jest.fn((cb) => setTimeout(cb, 16));
|
||||
global.cancelAnimationFrame = jest.fn((id) => clearTimeout(id));
|
35
packages/docs/src/components/demo/Button.tsx
Normal file
35
packages/docs/src/components/demo/Button.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import * as React from 'react';
|
||||
import { Button as ChakraButton, ChakraProvider, defaultSystem } from '@chakra-ui/react';
|
||||
|
||||
interface ButtonProps {
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
variant?: 'primary' | 'secondary' | 'ghost';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
children,
|
||||
onClick,
|
||||
variant = 'secondary',
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
className = '',
|
||||
}) => {
|
||||
const chakraVariant = variant === 'primary' ? 'solid' : variant === 'ghost' ? 'ghost' : 'outline';
|
||||
const colorPalette = variant === 'primary' ? 'blue' : 'gray';
|
||||
|
||||
return (
|
||||
<ChakraButton
|
||||
onClick={onClick}
|
||||
variant={chakraVariant}
|
||||
colorPalette={colorPalette}
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</ChakraButton>
|
||||
);
|
||||
};
|
15
packages/docs/src/components/demo/ButtonGroup.tsx
Normal file
15
packages/docs/src/components/demo/ButtonGroup.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { ButtonGroup as ChakraButtonGroup } from '@chakra-ui/react';
|
||||
|
||||
interface ButtonGroupProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ButtonGroup: React.FC<ButtonGroupProps> = ({ children, className = '' }) => {
|
||||
return (
|
||||
<ChakraButtonGroup isAttached variant="outline" className={className}>
|
||||
{children}
|
||||
</ChakraButtonGroup>
|
||||
);
|
||||
};
|
215
packages/docs/src/components/demo/DockviewDemo.scss
Normal file
215
packages/docs/src/components/demo/DockviewDemo.scss
Normal file
@ -0,0 +1,215 @@
|
||||
// Modern Button Styles
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid transparent;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease-in-out;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid var(--ifm-color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: white;
|
||||
background-color: var(--ifm-color-primary);
|
||||
border-color: var(--ifm-color-primary);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--ifm-color-primary-dark);
|
||||
border-color: var(--ifm-color-primary-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
color: var(--ifm-color-emphasis-800);
|
||||
background-color: var(--ifm-background-surface-color);
|
||||
border-color: var(--ifm-color-emphasis-300);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--ifm-color-emphasis-100);
|
||||
border-color: var(--ifm-color-emphasis-400);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
color: var(--ifm-color-emphasis-700);
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--ifm-color-emphasis-100);
|
||||
color: var(--ifm-color-emphasis-800);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
// Button Group Styles
|
||||
.btn-group {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
|
||||
.btn {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dockview-demo {
|
||||
.group-control {
|
||||
.action {
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-radius: 2px;
|
||||
color: var(--dv-activegroup-visiblepanel-tab-color);
|
||||
background-color: var(--dv-icon-hover-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-table {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
table {
|
||||
font-size: 11px;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
th, td {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
word-break: break-word;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
th:nth-child(3), td:nth-child(3) {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
input {
|
||||
outline: 1px solid #4c65d4;
|
||||
border: none;
|
||||
margin: 0px;
|
||||
height: 25px;
|
||||
|
||||
&:focus {
|
||||
outline: 1px solid #4c65d4 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
overflow: auto;
|
||||
|
||||
.text-button {
|
||||
margin: 0px 4px;
|
||||
}
|
||||
|
||||
.button-action {
|
||||
margin: 0px 4px;
|
||||
|
||||
.selected {
|
||||
background-color: #4864dc;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
button {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
min-width: 50px;
|
||||
padding: 0px 2px;
|
||||
border-radius: 0px;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
outline: 1px solid #4c65d4;
|
||||
}
|
||||
|
||||
.demo-icon-button {
|
||||
outline: 1px solid #4c65d4;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 0px;
|
||||
padding: 0px 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
color: gray;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
650
packages/docs/src/components/demo/DockviewDemo.tsx
Normal file
650
packages/docs/src/components/demo/DockviewDemo.tsx
Normal file
@ -0,0 +1,650 @@
|
||||
import {
|
||||
DockviewDefaultTab,
|
||||
DockviewReact,
|
||||
DockviewReadyEvent,
|
||||
IDockviewPanelHeaderProps,
|
||||
IDockviewPanelProps,
|
||||
DockviewApi,
|
||||
DockviewTheme,
|
||||
} from 'dockview';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom/client';
|
||||
import './DockviewDemo.scss';
|
||||
import { defaultConfig } from './defaultLayout';
|
||||
import { GridActions } from './gridActions';
|
||||
import { PanelActions } from './panelActions';
|
||||
import { GroupActions } from './groupActions';
|
||||
import { LeftControls, PrefixHeaderControls, RightControls } from './controls';
|
||||
import { Table, usePanelApiMetadata } from './debugPanel';
|
||||
import { Button } from './Button';
|
||||
import {
|
||||
ChakraProvider,
|
||||
createSystem,
|
||||
defaultConfig as chakraDefaultConfig,
|
||||
} from '@chakra-ui/react';
|
||||
import { useColorMode } from '@docusaurus/theme-common';
|
||||
|
||||
const DebugContext = React.createContext<boolean>(false);
|
||||
|
||||
const Option = (props: {
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
value: string;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<span>{`${props.title}: `}</span>
|
||||
<button onClick={props.onClick}>{props.value}</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ShadowIframe = (props: IDockviewPanelProps) => {
|
||||
return (
|
||||
<iframe
|
||||
onMouseDown={() => {
|
||||
if (!props.api.isActive) {
|
||||
props.api.setActive();
|
||||
}
|
||||
}}
|
||||
style={{ border: 'none', width: '100%', height: '100%' }}
|
||||
src="https://dockview.dev"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const components = {
|
||||
default: (props: IDockviewPanelProps) => {
|
||||
const api = usePanelApiMetadata(props.api);
|
||||
|
||||
const [count, setCount] = React.useState<number>(0);
|
||||
|
||||
const isDebug = React.useContext(DebugContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '10px',
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--dv-activegroup-visiblepanel-tab-color)',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{isDebug && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '10px',
|
||||
}}
|
||||
>
|
||||
<span>{props.api.title}</span>
|
||||
<Button
|
||||
onClick={() => setCount(count + 1)}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
>
|
||||
count: {count}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isDebug && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
pointerEvents: 'none',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
border: '2px dashed red',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '2px',
|
||||
left: '2px',
|
||||
backgroundColor: 'red',
|
||||
color: 'white',
|
||||
fontSize: '10px',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
>
|
||||
{props.api.id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
minHeight: 0,
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{isDebug ? (
|
||||
<Table api={api} />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
color: 'var(--dv-activegroup-visiblepanel-tab-color)',
|
||||
fontSize: '14px',
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '10px',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{props.api.title}
|
||||
</div>
|
||||
<div style={{ opacity: 0.7 }}>
|
||||
Click the engineering button to see debug
|
||||
info
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
iframe: ShadowIframe,
|
||||
shadow: (props: IDockviewPanelProps) => {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return () => {
|
||||
//
|
||||
};
|
||||
}
|
||||
|
||||
const element = ref.current;
|
||||
|
||||
const shadow = element.attachShadow({ mode: 'open' });
|
||||
|
||||
const shadowRoot = document.createElement('div');
|
||||
shadowRoot.style.height = '100%';
|
||||
shadow.appendChild(shadowRoot);
|
||||
|
||||
const root = ReactDOM.createRoot(shadowRoot);
|
||||
|
||||
root.render(<ShadowIframe {...props} />);
|
||||
|
||||
return () => {
|
||||
root.unmount();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div style={{ height: '100%' }} ref={ref}></div>;
|
||||
},
|
||||
};
|
||||
|
||||
const headerComponents = {
|
||||
default: (props: IDockviewPanelHeaderProps) => {
|
||||
const onContextMenu = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
alert('context menu');
|
||||
};
|
||||
return <DockviewDefaultTab onContextMenu={onContextMenu} {...props} />;
|
||||
},
|
||||
};
|
||||
|
||||
const colors = [
|
||||
'rgba(255,0,0,0.2)',
|
||||
'rgba(0,255,0,0.2)',
|
||||
'rgba(0,0,255,0.2)',
|
||||
'rgba(255,255,0,0.2)',
|
||||
'rgba(0,255,255,0.2)',
|
||||
'rgba(255,0,255,0.2)',
|
||||
];
|
||||
let count = 0;
|
||||
|
||||
const WatermarkComponent = () => {
|
||||
return <div>custom watermark</div>;
|
||||
};
|
||||
|
||||
const ThemeContext = React.createContext<DockviewTheme | undefined>(undefined);
|
||||
|
||||
const DockviewDemo = (props: { theme?: DockviewTheme }) => {
|
||||
const [logLines, setLogLines] = React.useState<
|
||||
{ text: string; timestamp?: Date; backgroundColor?: string }[]
|
||||
>([]);
|
||||
|
||||
const [panels, setPanels] = React.useState<string[]>([]);
|
||||
const [groups, setGroups] = React.useState<string[]>([]);
|
||||
const [api, setApi] = React.useState<DockviewApi>();
|
||||
const [activePanel, setActivePanel] = React.useState<string>();
|
||||
const [activeGroup, setActiveGroup] = React.useState<string>();
|
||||
|
||||
const addLogLine = (text: string, backgroundColor?: string) => {
|
||||
setLogLines((_) => [
|
||||
..._,
|
||||
{ text, timestamp: new Date(), backgroundColor },
|
||||
]);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return () => {
|
||||
//
|
||||
};
|
||||
}
|
||||
|
||||
const disposables = [
|
||||
api.onDidAddPanel((event) => {
|
||||
setPanels((_) => [..._, event.id]);
|
||||
addLogLine(`Panel Added ${event.id}`);
|
||||
}),
|
||||
|
||||
api.onDidActiveGroupChange((event) => {
|
||||
setActiveGroup(event?.id);
|
||||
addLogLine(`Group Activated ${event?.id}`);
|
||||
}),
|
||||
|
||||
api.onDidActivePanelChange((event) => {
|
||||
setActivePanel(event?.id);
|
||||
addLogLine(
|
||||
`Panel Activated ${event?.id}`,
|
||||
colors[count++ % colors.length]
|
||||
);
|
||||
}),
|
||||
|
||||
api.onDidRemovePanel((event) => {
|
||||
setPanels((_) => {
|
||||
const next = [..._];
|
||||
next.splice(
|
||||
next.findIndex((x) => x === event.id),
|
||||
1
|
||||
);
|
||||
|
||||
return next;
|
||||
});
|
||||
addLogLine(`Panel Removed ${event.id}`);
|
||||
}),
|
||||
|
||||
api.onDidAddGroup((event) => {
|
||||
setGroups((_) => [..._, event.id]);
|
||||
addLogLine(`Group Added ${event.id}`);
|
||||
}),
|
||||
|
||||
api.onDidMovePanel((event) => {
|
||||
addLogLine(`Panel Moved ${event.panel.id}`);
|
||||
}),
|
||||
|
||||
api.onDidMaximizedGroupChange((event) => {
|
||||
addLogLine(
|
||||
`Group Maximized Changed ${event.group.api.id} [${event.isMaximized}]`
|
||||
);
|
||||
}),
|
||||
|
||||
api.onDidRemoveGroup((event) => {
|
||||
setGroups((_) => {
|
||||
const next = [..._];
|
||||
next.splice(
|
||||
next.findIndex((x) => x === event.id),
|
||||
1
|
||||
);
|
||||
|
||||
return next;
|
||||
});
|
||||
addLogLine(`Group Removed ${event.id}`);
|
||||
}),
|
||||
|
||||
api.onDidActiveGroupChange((event) => {
|
||||
setActiveGroup(event?.id);
|
||||
addLogLine(`Group Activated ${event?.id}`);
|
||||
}),
|
||||
];
|
||||
|
||||
const loadLayout = () => {
|
||||
const state = localStorage.getItem('dv-demo-state');
|
||||
|
||||
if (state) {
|
||||
try {
|
||||
api.fromJSON(JSON.parse(state));
|
||||
return;
|
||||
} catch {
|
||||
localStorage.removeItem('dv-demo-state');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
defaultConfig(api);
|
||||
};
|
||||
|
||||
loadLayout();
|
||||
|
||||
return () => {
|
||||
disposables.forEach((disposable) => disposable.dispose());
|
||||
};
|
||||
}, [api]);
|
||||
|
||||
const onReady = (event: DockviewReadyEvent) => {
|
||||
setApi(event.api);
|
||||
};
|
||||
|
||||
const [watermark, setWatermark] = React.useState<boolean>(false);
|
||||
|
||||
const [gapCheck, setGapCheck] = React.useState<boolean>(false);
|
||||
|
||||
const css = React.useMemo(() => {
|
||||
if (!gapCheck) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
'--dv-group-gap-size': '0.5rem',
|
||||
'--demo-border': '5px dashed purple',
|
||||
} as React.CSSProperties;
|
||||
}, [gapCheck]);
|
||||
|
||||
const [showLogs, setShowLogs] = React.useState<boolean>(false);
|
||||
const [debug, setDebug] = React.useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="dockview-demo"
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
padding: '8px',
|
||||
position: 'relative',
|
||||
...css,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<GridActions
|
||||
api={api}
|
||||
toggleCustomWatermark={() => setWatermark(!watermark)}
|
||||
hasCustomWatermark={watermark}
|
||||
/>
|
||||
{api && (
|
||||
<PanelActions
|
||||
api={api}
|
||||
panels={panels}
|
||||
activePanel={activePanel}
|
||||
/>
|
||||
)}
|
||||
{api && (
|
||||
<GroupActions
|
||||
api={api}
|
||||
groups={groups}
|
||||
activeGroup={activeGroup}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="action-container"
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
padding: '4px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDebug(!debug);
|
||||
}}
|
||||
variant={debug ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
>
|
||||
<span className="material-symbols-outlined">
|
||||
engineering
|
||||
</span>
|
||||
</Button>
|
||||
{showLogs && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setLogLines([]);
|
||||
}}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
<span className="material-symbols-outlined">undo</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowLogs(!showLogs);
|
||||
}}
|
||||
variant={showLogs ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
>
|
||||
<span style={{ paddingRight: '4px' }}>
|
||||
{`${showLogs ? 'Hide' : 'Show'} Events Log`}
|
||||
</span>
|
||||
<span className="material-symbols-outlined">terminal</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
height: 0,
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<DebugContext.Provider value={debug}>
|
||||
<ThemeContext.Provider value={props.theme}>
|
||||
<DockviewReact
|
||||
components={components}
|
||||
defaultTabComponent={headerComponents.default}
|
||||
rightHeaderActionsComponent={RightControls}
|
||||
leftHeaderActionsComponent={LeftControls}
|
||||
prefixHeaderActionsComponent={
|
||||
PrefixHeaderControls
|
||||
}
|
||||
watermarkComponent={
|
||||
watermark ? WatermarkComponent : undefined
|
||||
}
|
||||
onReady={onReady}
|
||||
theme={props.theme}
|
||||
/>
|
||||
</ThemeContext.Provider>
|
||||
</DebugContext.Provider>
|
||||
</div>
|
||||
|
||||
{showLogs && (
|
||||
<div
|
||||
style={{
|
||||
width: '400px',
|
||||
backgroundColor: 'black',
|
||||
color: 'white',
|
||||
overflow: 'hidden',
|
||||
fontFamily: 'monospace',
|
||||
marginLeft: '10px',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<div style={{ flexGrow: 1, overflow: 'auto' }}>
|
||||
{logLines.map((line, i) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '30px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: '13px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
||||
backgroundColor:
|
||||
line.backgroundColor,
|
||||
}}
|
||||
key={i}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minWidth: '20px',
|
||||
maxWidth: '20px',
|
||||
color: 'gray',
|
||||
borderRight: '1px solid gray',
|
||||
marginRight: '4px',
|
||||
paddingLeft: '4px',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
{logLines.length - i}
|
||||
</span>
|
||||
<span>
|
||||
{line.timestamp && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: '0.7em',
|
||||
padding: '0px 2px',
|
||||
}}
|
||||
>
|
||||
{line.timestamp
|
||||
.toISOString()
|
||||
.substring(11, 23)}
|
||||
</span>
|
||||
)}
|
||||
<span>{line.text}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: '4px',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => setLogLines([])}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DockviewDemoWithChakra = (props: { theme?: DockviewTheme }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const system = React.useMemo(
|
||||
() =>
|
||||
createSystem(chakraDefaultConfig, {
|
||||
theme: {
|
||||
tokens: {
|
||||
colors: {
|
||||
colorPalette: {
|
||||
fg: {
|
||||
value:
|
||||
colorMode === 'dark'
|
||||
? 'white'
|
||||
: 'black',
|
||||
},
|
||||
solid: {
|
||||
value:
|
||||
colorMode === 'dark'
|
||||
? 'white'
|
||||
: 'black',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
recipes: {
|
||||
button: {
|
||||
variants: {
|
||||
variant: {
|
||||
outline: {
|
||||
bg: 'transparent',
|
||||
border: '1px solid gray',
|
||||
borderColor:
|
||||
colorMode === 'dark'
|
||||
? 'gray.700'
|
||||
: 'gray.200',
|
||||
color:
|
||||
colorMode === 'dark'
|
||||
? 'white'
|
||||
: 'black',
|
||||
borderRadius: 'md',
|
||||
transition: 'all 0.25s ease-in-out',
|
||||
px: '8px',
|
||||
py: '2px',
|
||||
height: '28px',
|
||||
mr: '4px',
|
||||
_hover: {
|
||||
bg:
|
||||
colorMode === 'dark'
|
||||
? 'whiteAlpha.100'
|
||||
: 'blackAlpha.100',
|
||||
borderColor:
|
||||
colorMode === 'dark'
|
||||
? 'gray.600'
|
||||
: 'gray.300',
|
||||
color:
|
||||
colorMode === 'dark'
|
||||
? 'white'
|
||||
: 'black',
|
||||
},
|
||||
_focus: {
|
||||
boxShadow: 'outline',
|
||||
bg: 'transparent',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[colorMode]
|
||||
);
|
||||
|
||||
return (
|
||||
<ChakraProvider value={system}>
|
||||
<DockviewDemo {...props} />
|
||||
</ChakraProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default DockviewDemoWithChakra;
|
148
packages/docs/src/components/demo/controls.tsx
Normal file
148
packages/docs/src/components/demo/controls.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import { IDockviewHeaderActionsProps } from 'dockview';
|
||||
import * as React from 'react';
|
||||
import { nextId } from './defaultLayout';
|
||||
|
||||
const Icon = (props: {
|
||||
icon: string;
|
||||
title?: string;
|
||||
onClick?: (event: React.MouseEvent) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div title={props.title} className="action" onClick={props.onClick}>
|
||||
<span
|
||||
style={{ fontSize: 'inherit' }}
|
||||
className="material-symbols-outlined"
|
||||
>
|
||||
{props.icon}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const groupControlsComponents: Record<string, React.FC> = {
|
||||
panel_1: () => {
|
||||
return <Icon icon="file_download" />;
|
||||
},
|
||||
};
|
||||
|
||||
export const RightControls = (props: IDockviewHeaderActionsProps) => {
|
||||
const Component = React.useMemo(() => {
|
||||
if (!props.isGroupActive || !props.activePanel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return groupControlsComponents[props.activePanel.id];
|
||||
}, [props.isGroupActive, props.activePanel]);
|
||||
|
||||
const [isMaximized, setIsMaximized] = React.useState<boolean>(
|
||||
props.containerApi.hasMaximizedGroup()
|
||||
);
|
||||
|
||||
const [isPopout, setIsPopout] = React.useState<boolean>(
|
||||
props.api.location.type === 'popout'
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const disposable = props.containerApi.onDidMaximizedGroupChange(() => {
|
||||
setIsMaximized(props.containerApi.hasMaximizedGroup());
|
||||
});
|
||||
|
||||
const disposable2 = props.api.onDidLocationChange(() => {
|
||||
setIsPopout(props.api.location.type === 'popout');
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
disposable2.dispose();
|
||||
};
|
||||
}, [props.containerApi]);
|
||||
|
||||
const onClick = () => {
|
||||
if (props.containerApi.hasMaximizedGroup()) {
|
||||
props.containerApi.exitMaximizedGroup();
|
||||
} else {
|
||||
props.activePanel?.api.maximize();
|
||||
}
|
||||
};
|
||||
|
||||
const onClick2 = () => {
|
||||
if (props.api.location.type !== 'popout') {
|
||||
props.containerApi.addPopoutGroup(props.group);
|
||||
} else {
|
||||
props.api.moveTo({ position: 'right' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group-control"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0px 8px',
|
||||
height: '100%',
|
||||
color: 'var(--dv-activegroup-hiddenpanel-tab-color)',
|
||||
}}
|
||||
>
|
||||
{props.isGroupActive && <Icon icon="star" />}
|
||||
{Component && <Component />}
|
||||
<Icon
|
||||
title={isPopout ? 'Close Window' : 'Open In New Window'}
|
||||
icon={isPopout ? 'close_fullscreen' : 'open_in_new'}
|
||||
onClick={onClick2}
|
||||
/>
|
||||
{!isPopout && (
|
||||
<Icon
|
||||
title={isMaximized ? 'Minimize View' : 'Maximize View'}
|
||||
icon={isMaximized ? 'collapse_content' : 'expand_content'}
|
||||
onClick={onClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LeftControls = (props: IDockviewHeaderActionsProps) => {
|
||||
const onClick = () => {
|
||||
props.containerApi.addPanel({
|
||||
id: `id_${Date.now().toString()}`,
|
||||
component: 'default',
|
||||
title: `Tab ${nextId()}`,
|
||||
position: {
|
||||
referenceGroup: props.group,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group-control"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0px 8px',
|
||||
height: '100%',
|
||||
color: 'var(--dv-activegroup-visiblepanel-tab-color)',
|
||||
}}
|
||||
>
|
||||
<Icon onClick={onClick} icon="add" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PrefixHeaderControls = (props: IDockviewHeaderActionsProps) => {
|
||||
return (
|
||||
<div
|
||||
className="group-control"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0px 8px',
|
||||
height: '100%',
|
||||
color: 'var(--dv-activegroup-visiblepanel-tab-color)',
|
||||
}}
|
||||
>
|
||||
<Icon icon="Menu" />
|
||||
</div>
|
||||
);
|
||||
};
|
164
packages/docs/src/components/demo/debugPanel.tsx
Normal file
164
packages/docs/src/components/demo/debugPanel.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import {
|
||||
DockviewGroupLocation,
|
||||
DockviewPanelApi,
|
||||
DockviewPanelRenderer,
|
||||
} from 'dockview';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface PanelApiMetadata {
|
||||
isActive: {
|
||||
value: boolean;
|
||||
count: number;
|
||||
};
|
||||
isVisible: {
|
||||
value: boolean;
|
||||
count: number;
|
||||
};
|
||||
renderer: {
|
||||
value: DockviewPanelRenderer;
|
||||
count: number;
|
||||
};
|
||||
isGroupActive: {
|
||||
value: boolean;
|
||||
count: number;
|
||||
};
|
||||
groupChanged: {
|
||||
count: number;
|
||||
};
|
||||
location: {
|
||||
value: DockviewGroupLocation;
|
||||
count: number;
|
||||
};
|
||||
didFocus: {
|
||||
count: number;
|
||||
};
|
||||
dimensions: {
|
||||
count: number;
|
||||
value: { height: number; width: number };
|
||||
};
|
||||
}
|
||||
|
||||
export const Table = (props: { api: PanelApiMetadata }) => {
|
||||
return (
|
||||
<div className="data-table">
|
||||
<table>
|
||||
<tr>
|
||||
<th>{'Key'}</th>
|
||||
<th>{'Count'}</th>
|
||||
<th>{'Value'}</th>
|
||||
</tr>
|
||||
{Object.entries(props.api).map(([key, value]) => {
|
||||
return (
|
||||
<tr key={key}>
|
||||
<th>{key}</th>
|
||||
<th>{value.count}</th>
|
||||
<th>{JSON.stringify(value.value, null, 4)}</th>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function usePanelApiMetadata(api: DockviewPanelApi): PanelApiMetadata {
|
||||
const [state, setState] = React.useState<PanelApiMetadata>({
|
||||
isActive: { value: api.isActive, count: 0 },
|
||||
isVisible: { value: api.isVisible, count: 0 },
|
||||
renderer: { value: api.renderer, count: 0 },
|
||||
isGroupActive: { value: api.isGroupActive, count: 0 },
|
||||
groupChanged: { count: 0 },
|
||||
location: { value: api.location, count: 0 },
|
||||
didFocus: { count: 0 },
|
||||
dimensions: {
|
||||
count: 0,
|
||||
value: { height: api.height, width: api.width },
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const d1 = api.onDidActiveChange((event) => {
|
||||
setState((_) => ({
|
||||
..._,
|
||||
isActive: {
|
||||
value: event.isActive,
|
||||
count: _.isActive.count + 1,
|
||||
},
|
||||
}));
|
||||
});
|
||||
const d2 = api.onDidActiveGroupChange((event) => {
|
||||
setState((_) => ({
|
||||
..._,
|
||||
isGroupActive: {
|
||||
value: event.isActive,
|
||||
count: _.isGroupActive.count + 1,
|
||||
},
|
||||
}));
|
||||
});
|
||||
const d3 = api.onDidDimensionsChange((event) => {
|
||||
setState((_) => ({
|
||||
..._,
|
||||
dimensions: {
|
||||
count: _.dimensions.count + 1,
|
||||
value: { height: event.height, width: event.width },
|
||||
},
|
||||
}));
|
||||
});
|
||||
const d4 = api.onDidFocusChange((event) => {
|
||||
setState((_) => ({
|
||||
..._,
|
||||
didFocus: {
|
||||
count: _.didFocus.count + 1,
|
||||
},
|
||||
}));
|
||||
});
|
||||
const d5 = api.onDidGroupChange((event) => {
|
||||
setState((_) => ({
|
||||
..._,
|
||||
groupChanged: {
|
||||
count: _.groupChanged.count + 1,
|
||||
},
|
||||
}));
|
||||
});
|
||||
const d7 = api.onDidLocationChange((event) => {
|
||||
setState((_) => ({
|
||||
..._,
|
||||
location: {
|
||||
value: event.location,
|
||||
count: _.location.count + 1,
|
||||
},
|
||||
}));
|
||||
});
|
||||
const d8 = api.onDidRendererChange((event) => {
|
||||
setState((_) => ({
|
||||
..._,
|
||||
renderer: {
|
||||
value: event.renderer,
|
||||
count: _.renderer.count + 1,
|
||||
},
|
||||
}));
|
||||
});
|
||||
const d9 = api.onDidVisibilityChange((event) => {
|
||||
setState((_) => ({
|
||||
..._,
|
||||
isVisible: {
|
||||
value: event.isVisible,
|
||||
count: _.isVisible.count + 1,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
return () => {
|
||||
d1.dispose();
|
||||
d2.dispose();
|
||||
d3.dispose();
|
||||
d4.dispose();
|
||||
d5.dispose();
|
||||
d7.dispose();
|
||||
d8.dispose();
|
||||
d9.dispose();
|
||||
};
|
||||
}, [api]);
|
||||
|
||||
return state;
|
||||
}
|
67
packages/docs/src/components/demo/defaultLayout.ts
Normal file
67
packages/docs/src/components/demo/defaultLayout.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { DockviewApi } from 'dockview';
|
||||
|
||||
export const nextId = (() => {
|
||||
let counter = 0;
|
||||
|
||||
return () => counter++;
|
||||
})();
|
||||
|
||||
export function defaultConfig(api: DockviewApi) {
|
||||
const panel1 = api.addPanel({
|
||||
id: 'panel_1',
|
||||
component: 'default',
|
||||
renderer: 'always',
|
||||
title: 'Panel 1',
|
||||
});
|
||||
|
||||
api.addPanel({
|
||||
id: 'panel_2',
|
||||
component: 'default',
|
||||
title: 'Panel 2',
|
||||
position: { referencePanel: panel1 },
|
||||
});
|
||||
|
||||
api.addPanel({
|
||||
id: 'panel_3',
|
||||
component: 'default',
|
||||
title: 'Panel 3',
|
||||
position: { referencePanel: panel1 },
|
||||
});
|
||||
|
||||
const panel4 = api.addPanel({
|
||||
id: 'panel_4',
|
||||
component: 'default',
|
||||
title: 'Panel 4',
|
||||
position: { referencePanel: panel1, direction: 'right' },
|
||||
});
|
||||
|
||||
const panel5 = api.addPanel({
|
||||
id: 'panel_5',
|
||||
component: 'default',
|
||||
title: 'Panel 5',
|
||||
position: { referencePanel: panel4 },
|
||||
});
|
||||
|
||||
const panel6 = api.addPanel({
|
||||
id: 'panel_6',
|
||||
component: 'default',
|
||||
title: 'Panel 6',
|
||||
position: { referencePanel: panel5, direction: 'below' },
|
||||
});
|
||||
|
||||
const panel7 = api.addPanel({
|
||||
id: 'panel_7',
|
||||
component: 'default',
|
||||
title: 'Panel 7',
|
||||
position: { referencePanel: panel6, direction: 'left' },
|
||||
});
|
||||
|
||||
api.addPanel({
|
||||
id: 'panel8',
|
||||
component: 'default',
|
||||
title: 'Panel 8',
|
||||
position: { referencePanel: panel7, direction: 'below' },
|
||||
});
|
||||
|
||||
panel1.api.setActive();
|
||||
}
|
270
packages/docs/src/components/demo/gridActions.tsx
Normal file
270
packages/docs/src/components/demo/gridActions.tsx
Normal file
@ -0,0 +1,270 @@
|
||||
import { DockviewApi } from 'dockview';
|
||||
import * as React from 'react';
|
||||
import { defaultConfig, nextId } from './defaultLayout';
|
||||
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { PanelBuilder } from './panelBuilder';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { ButtonGroup } from './ButtonGroup';
|
||||
|
||||
let mount = document.querySelector('.popover-anchor') as HTMLElement | null;
|
||||
|
||||
if (!mount) {
|
||||
mount = document.createElement('div');
|
||||
mount.className = 'popover-anchor';
|
||||
document.body.insertBefore(mount, document.body.firstChild);
|
||||
}
|
||||
|
||||
const PopoverComponent = (props: {
|
||||
close: () => void;
|
||||
component: React.FC<{ close: () => void }>;
|
||||
}) => {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handler = (ev: MouseEvent) => {
|
||||
let target = ev.target as HTMLElement;
|
||||
|
||||
while (target.parentElement) {
|
||||
if (target === ref.current) {
|
||||
return;
|
||||
}
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
props.close();
|
||||
};
|
||||
|
||||
window.addEventListener('mousedown', handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousedown', handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 9999,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%,-50%)',
|
||||
backgroundColor: 'black',
|
||||
color: 'white',
|
||||
padding: 10,
|
||||
}}
|
||||
>
|
||||
<props.component close={props.close} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function usePopover() {
|
||||
return {
|
||||
open: (Component: React.FC<{ close: () => void }>) => {
|
||||
const el = document.createElement('div');
|
||||
mount!.appendChild(el);
|
||||
const root = createRoot(el);
|
||||
|
||||
root.render(
|
||||
<PopoverComponent
|
||||
component={Component}
|
||||
close={() => {
|
||||
root.unmount();
|
||||
el.remove();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const GridActions = (props: {
|
||||
api?: DockviewApi;
|
||||
hasCustomWatermark: boolean;
|
||||
toggleCustomWatermark: () => void;
|
||||
}) => {
|
||||
const onClear = () => {
|
||||
props.api?.clear();
|
||||
};
|
||||
|
||||
const onLoad = () => {
|
||||
const state = localStorage.getItem('dv-demo-state');
|
||||
if (state) {
|
||||
try {
|
||||
props.api?.fromJSON(JSON.parse(state));
|
||||
} catch (err) {
|
||||
console.error('failed to load state', err);
|
||||
localStorage.removeItem('dv-demo-state');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
if (props.api) {
|
||||
const state = props.api.toJSON();
|
||||
console.log(state);
|
||||
|
||||
localStorage.setItem('dv-demo-state', JSON.stringify(state));
|
||||
}
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
if (props.api) {
|
||||
try {
|
||||
props.api.clear();
|
||||
defaultConfig(props.api);
|
||||
} catch (err) {
|
||||
localStorage.removeItem('dv-demo-state');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const popover = usePopover();
|
||||
|
||||
const onAddPanel = (options?: { advanced?: boolean; nested?: boolean }) => {
|
||||
if (options?.advanced) {
|
||||
popover.open(({ close }) => {
|
||||
return <PanelBuilder api={props.api!} done={close} />;
|
||||
});
|
||||
} else {
|
||||
props.api?.addPanel({
|
||||
id: `id_${Date.now().toString()}`,
|
||||
component: options?.nested ? 'nested' : 'default',
|
||||
title: `Tab ${nextId()}`,
|
||||
renderer: 'always',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onAddGroup = () => {
|
||||
props.api?.addGroup();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="action-container">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
css={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: 0
|
||||
}}
|
||||
>
|
||||
<span
|
||||
onClick={() => onAddPanel()}
|
||||
style={{
|
||||
padding: '2px 8px',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.15s',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '100%'
|
||||
}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = 'rgba(255,255,255,0.1)'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
|
||||
>
|
||||
Add Panel
|
||||
</span>
|
||||
<span
|
||||
onClick={() => onAddPanel({ advanced: true })}
|
||||
className="material-symbols-outlined"
|
||||
style={{
|
||||
padding: '4px',
|
||||
cursor: 'pointer',
|
||||
borderLeft: '1px solid rgba(255,255,255,0.2)',
|
||||
borderRadius: '0 4px 4px 0',
|
||||
transition: 'background-color 0.15s',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = 'rgba(255,255,255,0.1)'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
|
||||
>
|
||||
tune
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onAddPanel({ nested: true })}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
Add Nested Panel
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={onAddGroup}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
Add Group
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Button
|
||||
onClick={props.toggleCustomWatermark}
|
||||
size="sm"
|
||||
variant={props.hasCustomWatermark ? "solid" : "outline"}
|
||||
colorPalette="blue"
|
||||
>
|
||||
Use Custom Watermark
|
||||
</Button>
|
||||
<span style={{ flexGrow: 1 }} />
|
||||
<ButtonGroup>
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--chakra-colors-fg)',
|
||||
alignSelf: 'center',
|
||||
marginRight: '8px',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
Layout:
|
||||
</span>
|
||||
<Button
|
||||
onClick={onClear}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorPalette="red"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onLoad}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
Load
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onSave}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onReset}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
175
packages/docs/src/components/demo/groupActions.tsx
Normal file
175
packages/docs/src/components/demo/groupActions.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import {
|
||||
DockviewApi,
|
||||
DockviewGroupLocation,
|
||||
DockviewGroupPanel,
|
||||
} from 'dockview';
|
||||
import * as React from 'react';
|
||||
import { Button, ButtonGroup } from '@chakra-ui/react';
|
||||
|
||||
const GroupAction = (props: {
|
||||
groupId: string;
|
||||
groups: string[];
|
||||
api: DockviewApi;
|
||||
activeGroup?: string;
|
||||
}) => {
|
||||
const onClick = () => {
|
||||
props.api?.getGroup(props.groupId)?.focus();
|
||||
};
|
||||
|
||||
const isActive = props.activeGroup === props.groupId;
|
||||
|
||||
const [group, setGroup] = React.useState<DockviewGroupPanel | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const disposable = props.api.onDidLayoutFromJSON(() => {
|
||||
setGroup(props.api.getGroup(props.groupId));
|
||||
});
|
||||
|
||||
setGroup(props.api.getGroup(props.groupId));
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [props.api, props.groupId]);
|
||||
|
||||
const [location, setLocation] =
|
||||
React.useState<DockviewGroupLocation | null>(null);
|
||||
const [isMaximized, setIsMaximized] = React.useState<boolean>(false);
|
||||
const [isVisible, setIsVisible] = React.useState<boolean>(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!group) {
|
||||
setLocation(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const disposable = group.api.onDidLocationChange((event) => {
|
||||
setLocation(event.location);
|
||||
});
|
||||
|
||||
const disposable2 = props.api.onDidMaximizedGroupChange(() => {
|
||||
setIsMaximized(group.api.isMaximized());
|
||||
});
|
||||
|
||||
const disposable3 = group.api.onDidVisibilityChange(() => {
|
||||
setIsVisible(group.api.isVisible);
|
||||
});
|
||||
|
||||
setLocation(group.api.location);
|
||||
setIsMaximized(group.api.isMaximized());
|
||||
setIsVisible(group.api.isVisible);
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
disposable2.dispose();
|
||||
disposable3.dispose();
|
||||
};
|
||||
}, [group]);
|
||||
|
||||
return (
|
||||
<div className="button-action">
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Button
|
||||
onClick={onClick}
|
||||
variant={isActive ? "solid" : "outline"}
|
||||
colorPalette={isActive ? "blue" : undefined}
|
||||
size="sm"
|
||||
>
|
||||
{props.groupId}
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<ButtonGroup size="sm" variant="outline">
|
||||
<Button
|
||||
variant={location?.type === 'floating' ? "solid" : "outline"}
|
||||
colorPalette={location?.type === 'floating' ? "blue" : undefined}
|
||||
onClick={() => {
|
||||
if (group) {
|
||||
props.api.addFloatingGroup(group, {
|
||||
width: 400,
|
||||
height: 300,
|
||||
x: 50,
|
||||
y: 50,
|
||||
position: {
|
||||
bottom: 50,
|
||||
right: 50,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined">ad_group</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={location?.type === 'popout' ? "solid" : "outline"}
|
||||
colorPalette={location?.type === 'popout' ? "blue" : undefined}
|
||||
onClick={() => {
|
||||
if (group) {
|
||||
props.api.addPopoutGroup(group);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined">open_in_new</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={isMaximized ? "solid" : "outline"}
|
||||
colorPalette={isMaximized ? "blue" : undefined}
|
||||
onClick={() => {
|
||||
if (group) {
|
||||
if (group.api.isMaximized()) {
|
||||
group.api.exitMaximized();
|
||||
} else {
|
||||
group.api.maximize();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined">fullscreen</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
console.log(group);
|
||||
if (group) {
|
||||
if (group.api.isVisible) {
|
||||
group.api.setVisible(false);
|
||||
} else {
|
||||
group.api.setVisible(true);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined">
|
||||
{isVisible ? 'visibility' : 'visibility_off'}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const panel = props.api?.getGroup(props.groupId);
|
||||
panel?.api.close();
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined">close</span>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GroupActions = (props: {
|
||||
groups: string[];
|
||||
api: DockviewApi;
|
||||
activeGroup?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="action-container">
|
||||
{props.groups.map((groupId) => {
|
||||
return (
|
||||
<GroupAction key={groupId} {...props} groupId={groupId} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
126
packages/docs/src/components/demo/panelActions.tsx
Normal file
126
packages/docs/src/components/demo/panelActions.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
import { DockviewApi, IDockviewPanel } from 'dockview';
|
||||
import * as React from 'react';
|
||||
import { Button, ButtonGroup } from '@chakra-ui/react';
|
||||
|
||||
const PanelAction = (props: {
|
||||
panels: string[];
|
||||
api: DockviewApi;
|
||||
activePanel?: string;
|
||||
panelId: string;
|
||||
}) => {
|
||||
const onClick = () => {
|
||||
props.api.getPanel(props.panelId)?.focus();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const panel = props.api.getPanel(props.panelId);
|
||||
if (panel) {
|
||||
const disposable = panel.api.onDidVisibilityChange((event) => {
|
||||
setVisible(event.isVisible);
|
||||
});
|
||||
setVisible(panel.api.isVisible);
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}
|
||||
}, [props.api, props.panelId]);
|
||||
|
||||
const [panel, setPanel] = React.useState<IDockviewPanel | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const list = [
|
||||
props.api.onDidLayoutFromJSON(() => {
|
||||
setPanel(props.api.getPanel(props.panelId));
|
||||
}),
|
||||
];
|
||||
|
||||
if (panel) {
|
||||
const disposable = panel.api.onDidVisibilityChange((event) => {
|
||||
setVisible(event.isVisible);
|
||||
});
|
||||
setVisible(panel.api.isVisible);
|
||||
|
||||
list.push(disposable);
|
||||
}
|
||||
|
||||
setPanel(props.api.getPanel(props.panelId));
|
||||
|
||||
return () => {
|
||||
list.forEach((l) => l.dispose());
|
||||
};
|
||||
}, [props.api, props.panelId]);
|
||||
|
||||
const [visible, setVisible] = React.useState<boolean>(true);
|
||||
|
||||
return (
|
||||
<div className="button-action">
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Button
|
||||
variant={props.activePanel === props.panelId ? "solid" : "outline"}
|
||||
colorPalette={props.activePanel === props.panelId ? "blue" : undefined}
|
||||
onClick={onClick}
|
||||
size="sm"
|
||||
>
|
||||
{props.panelId}
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<ButtonGroup size="sm" variant="outline">
|
||||
<Button
|
||||
onClick={() => {
|
||||
const panel = props.api.getPanel(props.panelId);
|
||||
if (panel) {
|
||||
props.api.addFloatingGroup(panel);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined">ad_group</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const panel = props.api.getPanel(props.panelId);
|
||||
if (panel) {
|
||||
props.api.addPopoutGroup(panel);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined">open_in_new</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const panel = props.api.getPanel(props.panelId);
|
||||
panel?.api.close();
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined">close</span>
|
||||
</Button>
|
||||
<Button
|
||||
title="Panel visiblity cannot be edited manually."
|
||||
disabled={true}
|
||||
>
|
||||
<span className="material-symbols-outlined">
|
||||
{visible ? 'visibility' : 'visibility_off'}
|
||||
</span>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PanelActions = (props: {
|
||||
panels: string[];
|
||||
api: DockviewApi;
|
||||
activePanel?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="action-container">
|
||||
{props.panels.map((id) => {
|
||||
return <PanelAction key={id} {...props} panelId={id} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
115
packages/docs/src/components/demo/panelBuilder.tsx
Normal file
115
packages/docs/src/components/demo/panelBuilder.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import { DockviewApi } from 'dockview';
|
||||
import * as React from 'react';
|
||||
import { nextId } from './defaultLayout';
|
||||
|
||||
export const PanelBuilder = (props: { api: DockviewApi; done: () => void }) => {
|
||||
const [parameters, setParameters] = React.useState<{
|
||||
initialWidth?: number;
|
||||
initialHeight?: number;
|
||||
maximumHeight?: number;
|
||||
maximumWidth?: number;
|
||||
minimumHeight?: number;
|
||||
minimumWidth?: number;
|
||||
}>({});
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
}}
|
||||
>
|
||||
<div>{'Initial Width'}</div>
|
||||
<input
|
||||
type="number"
|
||||
value={parameters.initialWidth}
|
||||
onChange={(event) =>
|
||||
setParameters((_) => ({
|
||||
..._,
|
||||
initialWidth: Number(event.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<div>{'Initial Height'}</div>
|
||||
<input
|
||||
type="number"
|
||||
value={parameters.initialHeight}
|
||||
onChange={(event) =>
|
||||
setParameters((_) => ({
|
||||
..._,
|
||||
initialHeight: Number(event.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<div>{'Maximum Width'}</div>
|
||||
<input
|
||||
type="number"
|
||||
value={parameters.maximumWidth}
|
||||
onChange={(event) =>
|
||||
setParameters((_) => ({
|
||||
..._,
|
||||
maximumWidth: Number(event.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<div>{'Maximum Height'}</div>
|
||||
<input
|
||||
type="number"
|
||||
value={parameters.maximumHeight}
|
||||
onChange={(event) =>
|
||||
setParameters((_) => ({
|
||||
..._,
|
||||
maximumHeight: Number(event.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<div>{'Minimum Width'}</div>
|
||||
<input
|
||||
type="number"
|
||||
value={parameters.minimumWidth}
|
||||
onChange={(event) =>
|
||||
setParameters((_) => ({
|
||||
..._,
|
||||
minimumWidth: Number(event.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<div>{'Minimum Height'}</div>
|
||||
<input
|
||||
type="number"
|
||||
value={parameters.minimumHeight}
|
||||
onChange={(event) =>
|
||||
setParameters((_) => ({
|
||||
..._,
|
||||
minimumHeight: Number(event.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
props.done();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
props.api?.addPanel({
|
||||
id: `id_${Date.now().toString()}`,
|
||||
component: 'default',
|
||||
title: `Tab ${nextId()}`,
|
||||
renderer: 'always',
|
||||
...parameters,
|
||||
});
|
||||
|
||||
props.done();
|
||||
}}
|
||||
>
|
||||
Go
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
9
tsconfig.spec.json
Normal file
9
tsconfig.spec.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.test.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"noImplicitAny": false,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["**/*.spec.ts", "**/*.test.ts", "**/__tests__/**/*", "./jest-setup.ts"]
|
||||
}
|
73
yarn.lock
73
yarn.lock
@ -6099,7 +6099,7 @@ browserslist@^4.22.2:
|
||||
node-releases "^2.0.14"
|
||||
update-browserslist-db "^1.0.13"
|
||||
|
||||
bs-logger@0.x:
|
||||
bs-logger@^0.2.6:
|
||||
version "0.2.6"
|
||||
resolved "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz"
|
||||
integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==
|
||||
@ -8109,12 +8109,12 @@ es6-weak-map@^2.0.1:
|
||||
es6-iterator "^2.0.3"
|
||||
es6-symbol "^3.1.1"
|
||||
|
||||
esbuild-wasm@^0.19.5:
|
||||
esbuild-wasm@^0.19.5, esbuild-wasm@>=0.15.13:
|
||||
version "0.19.12"
|
||||
resolved "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.19.12.tgz"
|
||||
integrity sha512-Zmc4hk6FibJZBcTx5/8K/4jT3/oG1vkGTEeKJUQFCUQKimD6Q7+adp/bdVQyYJFolMKaXkQnVZdV4O5ZaTYmyQ==
|
||||
|
||||
esbuild@^0.19.0, esbuild@^0.19.3:
|
||||
esbuild@^0.19.0, esbuild@^0.19.3, esbuild@>=0.15.13:
|
||||
version "0.19.12"
|
||||
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz"
|
||||
integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==
|
||||
@ -9513,7 +9513,7 @@ handle-thing@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz"
|
||||
integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==
|
||||
|
||||
handlebars@^4.7.7:
|
||||
handlebars@^4.7.7, handlebars@^4.7.8:
|
||||
version "4.7.8"
|
||||
resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz"
|
||||
integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==
|
||||
@ -11041,6 +11041,20 @@ jest-pnp-resolver@^1.2.2:
|
||||
resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz"
|
||||
integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==
|
||||
|
||||
jest-preset-angular@^14.6.1:
|
||||
version "14.6.1"
|
||||
resolved "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.6.1.tgz"
|
||||
integrity sha512-7q5x42wKrsF2ykOwGVzcXpr9p1X4FQJMU/DnH1tpvCmeOm5XqENdwD/xDZug+nP6G8SJPdioauwdsK/PMY/MpQ==
|
||||
dependencies:
|
||||
bs-logger "^0.2.6"
|
||||
esbuild-wasm ">=0.15.13"
|
||||
jest-environment-jsdom "^29.7.0"
|
||||
jest-util "^29.7.0"
|
||||
pretty-format "^29.7.0"
|
||||
ts-jest "^29.3.0"
|
||||
optionalDependencies:
|
||||
esbuild ">=0.15.13"
|
||||
|
||||
jest-regex-util@^29.6.3:
|
||||
version "29.6.3"
|
||||
resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz"
|
||||
@ -11157,7 +11171,7 @@ jest-sonar-reporter@^2.0.0:
|
||||
dependencies:
|
||||
xml "^1.0.1"
|
||||
|
||||
jest-util@^29.0.0, jest-util@^29.7.0:
|
||||
jest-util@^29.7.0:
|
||||
version "29.7.0"
|
||||
resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz"
|
||||
integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==
|
||||
@ -11817,7 +11831,7 @@ lodash.ismatch@^4.4.0:
|
||||
resolved "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz"
|
||||
integrity sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==
|
||||
|
||||
lodash.memoize@^4.1.2, lodash.memoize@4.x:
|
||||
lodash.memoize@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz"
|
||||
integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==
|
||||
@ -11944,7 +11958,7 @@ make-dir@~3.1.0:
|
||||
dependencies:
|
||||
semver "^6.0.0"
|
||||
|
||||
make-error@^1.1.1, make-error@1.x:
|
||||
make-error@^1.1.1, make-error@^1.3.6:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz"
|
||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||
@ -15953,12 +15967,10 @@ semver@^6.3.1:
|
||||
resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz"
|
||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||
|
||||
semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4:
|
||||
version "7.5.4"
|
||||
resolved "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz"
|
||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.7.2:
|
||||
version "7.7.2"
|
||||
resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
|
||||
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
||||
|
||||
"semver@2 || 3 || 4 || 5":
|
||||
version "5.7.2"
|
||||
@ -17084,19 +17096,20 @@ ts-api-utils@^1.0.1:
|
||||
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz"
|
||||
integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==
|
||||
|
||||
ts-jest@^29.1.1:
|
||||
version "29.1.1"
|
||||
resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz"
|
||||
integrity sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==
|
||||
ts-jest@^29.1.1, ts-jest@^29.3.0:
|
||||
version "29.4.1"
|
||||
resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz"
|
||||
integrity sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==
|
||||
dependencies:
|
||||
bs-logger "0.x"
|
||||
fast-json-stable-stringify "2.x"
|
||||
jest-util "^29.0.0"
|
||||
bs-logger "^0.2.6"
|
||||
fast-json-stable-stringify "^2.1.0"
|
||||
handlebars "^4.7.8"
|
||||
json5 "^2.2.3"
|
||||
lodash.memoize "4.x"
|
||||
make-error "1.x"
|
||||
semver "^7.5.3"
|
||||
yargs-parser "^21.0.1"
|
||||
lodash.memoize "^4.1.2"
|
||||
make-error "^1.3.6"
|
||||
semver "^7.7.2"
|
||||
type-fest "^4.41.0"
|
||||
yargs-parser "^21.1.1"
|
||||
|
||||
ts-loader@^9.5.1:
|
||||
version "9.5.1"
|
||||
@ -17208,6 +17221,11 @@ type-fest@^2.5.0:
|
||||
resolved "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz"
|
||||
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
|
||||
|
||||
type-fest@^4.41.0:
|
||||
version "4.41.0"
|
||||
resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz"
|
||||
integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==
|
||||
|
||||
type-is@~1.6.18:
|
||||
version "1.6.18"
|
||||
resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz"
|
||||
@ -18305,7 +18323,7 @@ yargs-parser@^20.2.3:
|
||||
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz"
|
||||
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
|
||||
|
||||
yargs-parser@^21.0.1, yargs-parser@^21.1.1, yargs-parser@21.1.1:
|
||||
yargs-parser@^21.1.1, yargs-parser@21.1.1:
|
||||
version "21.1.1"
|
||||
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
@ -18378,6 +18396,11 @@ yocto-queue@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz"
|
||||
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
|
||||
|
||||
zone.js@^0.15.1:
|
||||
version "0.15.1"
|
||||
resolved "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz"
|
||||
integrity sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==
|
||||
|
||||
zwitch@^2.0.0:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz"
|
||||
|
Loading…
x
Reference in New Issue
Block a user