dockview-svelte 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +480 -0
- package/dist/components/DefaultTab.svelte +55 -0
- package/dist/components/DefaultTab.svelte.d.ts +7 -0
- package/dist/components/Dockview.svelte +517 -0
- package/dist/components/Dockview.svelte.d.ts +113 -0
- package/dist/components/DvWidget.svelte +120 -0
- package/dist/components/DvWidget.svelte.d.ts +41 -0
- package/dist/components/Gridview.svelte +341 -0
- package/dist/components/Gridview.svelte.d.ts +73 -0
- package/dist/components/Paneview.svelte +307 -0
- package/dist/components/Paneview.svelte.d.ts +64 -0
- package/dist/components/Splitview.svelte +332 -0
- package/dist/components/Splitview.svelte.d.ts +70 -0
- package/dist/core/context.d.ts +62 -0
- package/dist/core/context.js +1 -0
- package/dist/core/effect-helpers.d.ts +7 -0
- package/dist/core/effect-helpers.js +23 -0
- package/dist/core/factory.svelte.d.ts +16 -0
- package/dist/core/factory.svelte.js +248 -0
- package/dist/core/gridview.svelte.d.ts +31 -0
- package/dist/core/gridview.svelte.js +177 -0
- package/dist/core/paneview-test-helpers.js +28 -0
- package/dist/core/paneview.svelte.d.ts +29 -0
- package/dist/core/paneview.svelte.js +186 -0
- package/dist/core/registry.d.ts +88 -0
- package/dist/core/registry.js +143 -0
- package/dist/core/splitview.svelte.d.ts +32 -0
- package/dist/core/splitview.svelte.js +179 -0
- package/dist/core/types.d.ts +424 -0
- package/dist/core/types.js +1 -0
- package/dist/core/utils.d.ts +12 -0
- package/dist/core/utils.js +59 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +11 -0
- package/package.json +66 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CreateComponentOptions, IPanePart } from 'dockview';
|
|
2
|
+
import { type PaneviewContext } from './context.js';
|
|
3
|
+
import type { PaneviewWidgetRegistry } from './registry.js';
|
|
4
|
+
import type { PaneviewState } from './types.js';
|
|
5
|
+
/** The renderer factories returned by {@link createPaneviewFactory}. */
|
|
6
|
+
export interface PaneviewFactory {
|
|
7
|
+
createComponent: (options: CreateComponentOptions) => IPanePart;
|
|
8
|
+
createHeaderComponent: (options: CreateComponentOptions) => IPanePart | undefined;
|
|
9
|
+
/** Access a pane's shared state by id (used by `openPanel` to build the handle). */
|
|
10
|
+
getState: (id: string) => PaneviewState | undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates the paneview body/header renderer factories backed by a shared widget registry.
|
|
14
|
+
*
|
|
15
|
+
* dockview calls `createComponent({ id, name })` for the pane body and
|
|
16
|
+
* `createHeaderComponent({ id, name })` for the pane header, where `name` is
|
|
17
|
+
* the `component` / `headerComponent` string passed to `addPanel`. The body
|
|
18
|
+
* owns the {@link PaneviewState} (created here as `$state`); the header only
|
|
19
|
+
* mounts its component against the same state object. `getOrCreate` covers the
|
|
20
|
+
* construction order either way.
|
|
21
|
+
*
|
|
22
|
+
* When a widget defines no `header`, `createHeaderComponent` returns
|
|
23
|
+
* `undefined` so dockview falls back to its built-in `DefaultHeader` (plain
|
|
24
|
+
* title text) — the same fallback as the Dockview `tab`.
|
|
25
|
+
*
|
|
26
|
+
* Both mounts forward the parent `PaneviewContext` via Svelte's `mount`
|
|
27
|
+
* `context` option, so widgets can `getContext(DOCKVIEW_CONTEXT_KEY)`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createPaneviewFactory(registry: PaneviewWidgetRegistry, context?: PaneviewContext): PaneviewFactory;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { mount, unmount } from 'svelte';
|
|
2
|
+
import { DOCKVIEW_CONTEXT_KEY } from './context.js';
|
|
3
|
+
import { deepEqual, mergeInto } from './utils.js';
|
|
4
|
+
/** Create a `$state`-wrapped paneview state. `$state` must be a declaration initializer. */
|
|
5
|
+
function createPaneviewState(api, params, title) {
|
|
6
|
+
const state = $state({
|
|
7
|
+
params: params ?? {},
|
|
8
|
+
size: { width: 0, height: 0 },
|
|
9
|
+
visible: api.isVisible,
|
|
10
|
+
active: api.isActive,
|
|
11
|
+
focused: api.isFocused,
|
|
12
|
+
expanded: api.isExpanded,
|
|
13
|
+
api,
|
|
14
|
+
title,
|
|
15
|
+
custom: {},
|
|
16
|
+
});
|
|
17
|
+
return state;
|
|
18
|
+
}
|
|
19
|
+
/** Shared mount helper: one `IPanePart` (body or header) bound to the shared state. */
|
|
20
|
+
function createPanePart(widget, context, getOrCreateState, releaseEntry, side) {
|
|
21
|
+
const element = document.createElement('div');
|
|
22
|
+
element.className = side === 'body' ? 'dv-svelte-pane-body' : 'dv-svelte-pane-header';
|
|
23
|
+
let instance;
|
|
24
|
+
let entry;
|
|
25
|
+
let state;
|
|
26
|
+
let raf = 0;
|
|
27
|
+
let disposables = [];
|
|
28
|
+
let effectDestroy;
|
|
29
|
+
let lastParams;
|
|
30
|
+
let lastActive = false;
|
|
31
|
+
let lastVisible = true;
|
|
32
|
+
let lastExpanded = false;
|
|
33
|
+
return {
|
|
34
|
+
element,
|
|
35
|
+
init(parameters) {
|
|
36
|
+
entry = getOrCreateState(parameters.api, parameters.params, parameters.title);
|
|
37
|
+
state = entry.state;
|
|
38
|
+
instance = mount(widget, {
|
|
39
|
+
target: element,
|
|
40
|
+
props: { state },
|
|
41
|
+
...(context ? { context: new Map([[DOCKVIEW_CONTEXT_KEY, context]]) } : {}),
|
|
42
|
+
});
|
|
43
|
+
lastParams = $state.snapshot(state.params);
|
|
44
|
+
lastActive = state.active;
|
|
45
|
+
lastVisible = state.visible;
|
|
46
|
+
lastExpanded = state.expanded;
|
|
47
|
+
// The body owns the api mirrors + widget→dockview effects; the header
|
|
48
|
+
// only mounts against the same state (it must not double-subscribe).
|
|
49
|
+
if (side === 'body') {
|
|
50
|
+
disposables.push(parameters.api.onDidActiveChange((event) => {
|
|
51
|
+
state.active = event.isActive;
|
|
52
|
+
lastActive = event.isActive;
|
|
53
|
+
}), parameters.api.onDidFocusChange((event) => {
|
|
54
|
+
state.focused = event.isFocused;
|
|
55
|
+
}), parameters.api.onDidVisibilityChange((event) => {
|
|
56
|
+
state.visible = event.isVisible;
|
|
57
|
+
lastVisible = event.isVisible;
|
|
58
|
+
}), parameters.api.onDidDimensionsChange((event) => {
|
|
59
|
+
cancelAnimationFrame(raf);
|
|
60
|
+
raf = requestAnimationFrame(() => {
|
|
61
|
+
state.size.width = event.width;
|
|
62
|
+
state.size.height = event.height;
|
|
63
|
+
});
|
|
64
|
+
}), parameters.api.onDidExpansionChange((event) => {
|
|
65
|
+
state.expanded = event.isExpanded;
|
|
66
|
+
lastExpanded = event.isExpanded;
|
|
67
|
+
}));
|
|
68
|
+
// Widget → dockview (params double-bind, activation, expand toggle).
|
|
69
|
+
effectDestroy = $effect.root(() => {
|
|
70
|
+
$effect(() => {
|
|
71
|
+
const snap = $state.snapshot(state.params);
|
|
72
|
+
if (deepEqual(snap, lastParams))
|
|
73
|
+
return;
|
|
74
|
+
lastParams = snap;
|
|
75
|
+
state.api.updateParameters(snap);
|
|
76
|
+
});
|
|
77
|
+
$effect(() => {
|
|
78
|
+
// Only activation is meaningful from the widget side.
|
|
79
|
+
if (state.active && !lastActive) {
|
|
80
|
+
state.api.setActive();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
$effect(() => {
|
|
84
|
+
if (state.visible !== lastVisible) {
|
|
85
|
+
lastVisible = state.visible;
|
|
86
|
+
state.api.setVisible(state.visible);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
$effect(() => {
|
|
90
|
+
if (state.expanded !== lastExpanded) {
|
|
91
|
+
lastExpanded = state.expanded;
|
|
92
|
+
state.api.setExpanded(state.expanded);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
update(event) {
|
|
99
|
+
// dockview → widget (body owns the merge; header shares the state).
|
|
100
|
+
if (side !== 'body' || !state)
|
|
101
|
+
return;
|
|
102
|
+
mergeInto(state.params, event.params);
|
|
103
|
+
lastParams = $state.snapshot(state.params);
|
|
104
|
+
},
|
|
105
|
+
dispose() {
|
|
106
|
+
cancelAnimationFrame(raf);
|
|
107
|
+
effectDestroy?.();
|
|
108
|
+
effectDestroy = undefined;
|
|
109
|
+
for (const disposable of disposables)
|
|
110
|
+
disposable.dispose();
|
|
111
|
+
disposables = [];
|
|
112
|
+
if (instance) {
|
|
113
|
+
unmount(instance);
|
|
114
|
+
instance = undefined;
|
|
115
|
+
}
|
|
116
|
+
if (entry) {
|
|
117
|
+
// Entries are keyed by `api.id` — read it back from the shared state.
|
|
118
|
+
releaseEntry(entry.state.api.id, side);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Creates the paneview body/header renderer factories backed by a shared widget registry.
|
|
125
|
+
*
|
|
126
|
+
* dockview calls `createComponent({ id, name })` for the pane body and
|
|
127
|
+
* `createHeaderComponent({ id, name })` for the pane header, where `name` is
|
|
128
|
+
* the `component` / `headerComponent` string passed to `addPanel`. The body
|
|
129
|
+
* owns the {@link PaneviewState} (created here as `$state`); the header only
|
|
130
|
+
* mounts its component against the same state object. `getOrCreate` covers the
|
|
131
|
+
* construction order either way.
|
|
132
|
+
*
|
|
133
|
+
* When a widget defines no `header`, `createHeaderComponent` returns
|
|
134
|
+
* `undefined` so dockview falls back to its built-in `DefaultHeader` (plain
|
|
135
|
+
* title text) — the same fallback as the Dockview `tab`.
|
|
136
|
+
*
|
|
137
|
+
* Both mounts forward the parent `PaneviewContext` via Svelte's `mount`
|
|
138
|
+
* `context` option, so widgets can `getContext(DOCKVIEW_CONTEXT_KEY)`.
|
|
139
|
+
*/
|
|
140
|
+
export function createPaneviewFactory(registry, context) {
|
|
141
|
+
const panes = new Map();
|
|
142
|
+
function getOrCreateState(api, params, title) {
|
|
143
|
+
let entry = panes.get(api.id);
|
|
144
|
+
if (!entry) {
|
|
145
|
+
entry = {
|
|
146
|
+
state: createPaneviewState(api, params, title),
|
|
147
|
+
bodyDisposed: false,
|
|
148
|
+
headerDisposed: false,
|
|
149
|
+
};
|
|
150
|
+
panes.set(api.id, entry);
|
|
151
|
+
}
|
|
152
|
+
return entry;
|
|
153
|
+
}
|
|
154
|
+
function releaseEntry(id, side) {
|
|
155
|
+
const entry = panes.get(id);
|
|
156
|
+
if (!entry)
|
|
157
|
+
return;
|
|
158
|
+
if (side === 'body')
|
|
159
|
+
entry.bodyDisposed = true;
|
|
160
|
+
else
|
|
161
|
+
entry.headerDisposed = true;
|
|
162
|
+
if (entry.bodyDisposed && entry.headerDisposed)
|
|
163
|
+
panes.delete(id);
|
|
164
|
+
}
|
|
165
|
+
function createComponent(options) {
|
|
166
|
+
const def = registry.get(options.name);
|
|
167
|
+
if (!def) {
|
|
168
|
+
throw new Error(`dockview-svelte: unknown widget "${options.name}"`);
|
|
169
|
+
}
|
|
170
|
+
return createPanePart(def.component, context, getOrCreateState, releaseEntry, 'body');
|
|
171
|
+
}
|
|
172
|
+
function createHeaderComponent(options) {
|
|
173
|
+
const def = registry.get(options.name);
|
|
174
|
+
if (!def) {
|
|
175
|
+
throw new Error(`dockview-svelte: unknown widget "${options.name}"`);
|
|
176
|
+
}
|
|
177
|
+
if (!def.header)
|
|
178
|
+
return undefined;
|
|
179
|
+
return createPanePart(def.header, context, getOrCreateState, releaseEntry, 'header');
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
createComponent,
|
|
183
|
+
createHeaderComponent,
|
|
184
|
+
getState: (id) => panes.get(id)?.state,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { GridviewWidgetDefinition, GridviewWidgets, PaneviewWidgetDefinition, PaneviewWidgets, SplitviewWidgetDefinition, SplitviewWidgets, WidgetDefinition, Widgets } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Mutable widget registry consulted by the renderer factories and `openPanel`.
|
|
4
|
+
* Seeded from the `widgets` prop, extended at runtime via `registerWidget`
|
|
5
|
+
* (including `<DvWidget>` declarative children).
|
|
6
|
+
*/
|
|
7
|
+
export declare class WidgetRegistry {
|
|
8
|
+
private map;
|
|
9
|
+
/** Register or replace a widget definition. */
|
|
10
|
+
register(key: string, def: WidgetDefinition): void;
|
|
11
|
+
/** Remove a widget definition (used when a `<DvWidget>` child is destroyed). */
|
|
12
|
+
unregister(key: string): void;
|
|
13
|
+
/** Get a widget definition by key, or `undefined` if unknown. */
|
|
14
|
+
get(key: string): WidgetDefinition | undefined;
|
|
15
|
+
/** True if a widget with the given key is registered. */
|
|
16
|
+
has(key: string): boolean;
|
|
17
|
+
/** Seed the registry from a `{ [key]: definition }` object. */
|
|
18
|
+
seed(widgets: Record<string, WidgetDefinition>): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Identity helper that preserves literal keys and per-widget param types via
|
|
22
|
+
* a `const` type param. No longer needed for inference — the view components
|
|
23
|
+
* declare `const W` generics, so a plain object literal passed to `widgets`
|
|
24
|
+
* infers the same types. Kept as a deprecated alias for existing code.
|
|
25
|
+
*
|
|
26
|
+
* @deprecated Pass a plain object to `widgets` instead — inference is automatic.
|
|
27
|
+
*/
|
|
28
|
+
export declare function defineWidgets<const W extends Widgets>(w: W): W;
|
|
29
|
+
/**
|
|
30
|
+
* Deprecated alias — pass a plain object to `Splitview`'s `widgets` instead.
|
|
31
|
+
*
|
|
32
|
+
* @deprecated Pass a plain object to `widgets` instead — inference is automatic.
|
|
33
|
+
*/
|
|
34
|
+
export declare function defineSplitviewWidgets<const W extends SplitviewWidgets>(w: W): W;
|
|
35
|
+
/**
|
|
36
|
+
* Deprecated alias — pass a plain object to `Gridview`'s `widgets` instead.
|
|
37
|
+
*
|
|
38
|
+
* @deprecated Pass a plain object to `widgets` instead — inference is automatic.
|
|
39
|
+
*/
|
|
40
|
+
export declare function defineGridviewWidgets<const W extends GridviewWidgets>(w: W): W;
|
|
41
|
+
/**
|
|
42
|
+
* Deprecated alias — pass a plain object to `Paneview`'s `widgets` instead.
|
|
43
|
+
*
|
|
44
|
+
* @deprecated Pass a plain object to `widgets` instead — inference is automatic.
|
|
45
|
+
*/
|
|
46
|
+
export declare function definePaneviewWidgets<const W extends PaneviewWidgets>(w: W): W;
|
|
47
|
+
/** Mutable splitview widget registry, mirroring {@link WidgetRegistry}. */
|
|
48
|
+
export declare class SplitviewWidgetRegistry {
|
|
49
|
+
private map;
|
|
50
|
+
/** Register or replace a widget definition. */
|
|
51
|
+
register(key: string, def: SplitviewWidgetDefinition): void;
|
|
52
|
+
/** Remove a widget definition (used when a `<DvWidget>` child is destroyed). */
|
|
53
|
+
unregister(key: string): void;
|
|
54
|
+
/** Get a widget definition by key, or `undefined` if unknown. */
|
|
55
|
+
get(key: string): SplitviewWidgetDefinition | undefined;
|
|
56
|
+
/** True if a widget with the given key is registered. */
|
|
57
|
+
has(key: string): boolean;
|
|
58
|
+
/** Seed the registry from a `{ [key]: definition }` object. */
|
|
59
|
+
seed(widgets: Record<string, SplitviewWidgetDefinition>): void;
|
|
60
|
+
}
|
|
61
|
+
/** Mutable gridview widget registry, mirroring {@link WidgetRegistry}. */
|
|
62
|
+
export declare class GridviewWidgetRegistry {
|
|
63
|
+
private map;
|
|
64
|
+
/** Register or replace a widget definition. */
|
|
65
|
+
register(key: string, def: GridviewWidgetDefinition): void;
|
|
66
|
+
/** Remove a widget definition (used when a `<DvWidget>` child is destroyed). */
|
|
67
|
+
unregister(key: string): void;
|
|
68
|
+
/** Get a widget definition by key, or `undefined` if unknown. */
|
|
69
|
+
get(key: string): GridviewWidgetDefinition | undefined;
|
|
70
|
+
/** True if a widget with the given key is registered. */
|
|
71
|
+
has(key: string): boolean;
|
|
72
|
+
/** Seed the registry from a `{ [key]: definition }` object. */
|
|
73
|
+
seed(widgets: Record<string, GridviewWidgetDefinition>): void;
|
|
74
|
+
}
|
|
75
|
+
/** Mutable paneview widget registry, mirroring {@link WidgetRegistry}. */
|
|
76
|
+
export declare class PaneviewWidgetRegistry {
|
|
77
|
+
private map;
|
|
78
|
+
/** Register or replace a widget definition. */
|
|
79
|
+
register(key: string, def: PaneviewWidgetDefinition): void;
|
|
80
|
+
/** Remove a widget definition (used when a `<DvWidget>` child is destroyed). */
|
|
81
|
+
unregister(key: string): void;
|
|
82
|
+
/** Get a widget definition by key, or `undefined` if unknown. */
|
|
83
|
+
get(key: string): PaneviewWidgetDefinition | undefined;
|
|
84
|
+
/** True if a widget with the given key is registered. */
|
|
85
|
+
has(key: string): boolean;
|
|
86
|
+
/** Seed the registry from a `{ [key]: definition }` object. */
|
|
87
|
+
seed(widgets: Record<string, PaneviewWidgetDefinition>): void;
|
|
88
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutable widget registry consulted by the renderer factories and `openPanel`.
|
|
3
|
+
* Seeded from the `widgets` prop, extended at runtime via `registerWidget`
|
|
4
|
+
* (including `<DvWidget>` declarative children).
|
|
5
|
+
*/
|
|
6
|
+
export class WidgetRegistry {
|
|
7
|
+
map = new Map();
|
|
8
|
+
/** Register or replace a widget definition. */
|
|
9
|
+
register(key, def) {
|
|
10
|
+
this.map.set(key, def);
|
|
11
|
+
}
|
|
12
|
+
/** Remove a widget definition (used when a `<DvWidget>` child is destroyed). */
|
|
13
|
+
unregister(key) {
|
|
14
|
+
this.map.delete(key);
|
|
15
|
+
}
|
|
16
|
+
/** Get a widget definition by key, or `undefined` if unknown. */
|
|
17
|
+
get(key) {
|
|
18
|
+
return this.map.get(key);
|
|
19
|
+
}
|
|
20
|
+
/** True if a widget with the given key is registered. */
|
|
21
|
+
has(key) {
|
|
22
|
+
return this.map.has(key);
|
|
23
|
+
}
|
|
24
|
+
/** Seed the registry from a `{ [key]: definition }` object. */
|
|
25
|
+
seed(widgets) {
|
|
26
|
+
for (const [key, def] of Object.entries(widgets)) {
|
|
27
|
+
this.map.set(key, def);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Identity helper that preserves literal keys and per-widget param types via
|
|
33
|
+
* a `const` type param. No longer needed for inference — the view components
|
|
34
|
+
* declare `const W` generics, so a plain object literal passed to `widgets`
|
|
35
|
+
* infers the same types. Kept as a deprecated alias for existing code.
|
|
36
|
+
*
|
|
37
|
+
* @deprecated Pass a plain object to `widgets` instead — inference is automatic.
|
|
38
|
+
*/
|
|
39
|
+
export function defineWidgets(w) {
|
|
40
|
+
return w;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Deprecated alias — pass a plain object to `Splitview`'s `widgets` instead.
|
|
44
|
+
*
|
|
45
|
+
* @deprecated Pass a plain object to `widgets` instead — inference is automatic.
|
|
46
|
+
*/
|
|
47
|
+
export function defineSplitviewWidgets(w) {
|
|
48
|
+
return w;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Deprecated alias — pass a plain object to `Gridview`'s `widgets` instead.
|
|
52
|
+
*
|
|
53
|
+
* @deprecated Pass a plain object to `widgets` instead — inference is automatic.
|
|
54
|
+
*/
|
|
55
|
+
export function defineGridviewWidgets(w) {
|
|
56
|
+
return w;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Deprecated alias — pass a plain object to `Paneview`'s `widgets` instead.
|
|
60
|
+
*
|
|
61
|
+
* @deprecated Pass a plain object to `widgets` instead — inference is automatic.
|
|
62
|
+
*/
|
|
63
|
+
export function definePaneviewWidgets(w) {
|
|
64
|
+
return w;
|
|
65
|
+
}
|
|
66
|
+
/** Mutable splitview widget registry, mirroring {@link WidgetRegistry}. */
|
|
67
|
+
export class SplitviewWidgetRegistry {
|
|
68
|
+
map = new Map();
|
|
69
|
+
/** Register or replace a widget definition. */
|
|
70
|
+
register(key, def) {
|
|
71
|
+
this.map.set(key, def);
|
|
72
|
+
}
|
|
73
|
+
/** Remove a widget definition (used when a `<DvWidget>` child is destroyed). */
|
|
74
|
+
unregister(key) {
|
|
75
|
+
this.map.delete(key);
|
|
76
|
+
}
|
|
77
|
+
/** Get a widget definition by key, or `undefined` if unknown. */
|
|
78
|
+
get(key) {
|
|
79
|
+
return this.map.get(key);
|
|
80
|
+
}
|
|
81
|
+
/** True if a widget with the given key is registered. */
|
|
82
|
+
has(key) {
|
|
83
|
+
return this.map.has(key);
|
|
84
|
+
}
|
|
85
|
+
/** Seed the registry from a `{ [key]: definition }` object. */
|
|
86
|
+
seed(widgets) {
|
|
87
|
+
for (const [key, def] of Object.entries(widgets)) {
|
|
88
|
+
this.map.set(key, def);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Mutable gridview widget registry, mirroring {@link WidgetRegistry}. */
|
|
93
|
+
export class GridviewWidgetRegistry {
|
|
94
|
+
map = new Map();
|
|
95
|
+
/** Register or replace a widget definition. */
|
|
96
|
+
register(key, def) {
|
|
97
|
+
this.map.set(key, def);
|
|
98
|
+
}
|
|
99
|
+
/** Remove a widget definition (used when a `<DvWidget>` child is destroyed). */
|
|
100
|
+
unregister(key) {
|
|
101
|
+
this.map.delete(key);
|
|
102
|
+
}
|
|
103
|
+
/** Get a widget definition by key, or `undefined` if unknown. */
|
|
104
|
+
get(key) {
|
|
105
|
+
return this.map.get(key);
|
|
106
|
+
}
|
|
107
|
+
/** True if a widget with the given key is registered. */
|
|
108
|
+
has(key) {
|
|
109
|
+
return this.map.has(key);
|
|
110
|
+
}
|
|
111
|
+
/** Seed the registry from a `{ [key]: definition }` object. */
|
|
112
|
+
seed(widgets) {
|
|
113
|
+
for (const [key, def] of Object.entries(widgets)) {
|
|
114
|
+
this.map.set(key, def);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Mutable paneview widget registry, mirroring {@link WidgetRegistry}. */
|
|
119
|
+
export class PaneviewWidgetRegistry {
|
|
120
|
+
map = new Map();
|
|
121
|
+
/** Register or replace a widget definition. */
|
|
122
|
+
register(key, def) {
|
|
123
|
+
this.map.set(key, def);
|
|
124
|
+
}
|
|
125
|
+
/** Remove a widget definition (used when a `<DvWidget>` child is destroyed). */
|
|
126
|
+
unregister(key) {
|
|
127
|
+
this.map.delete(key);
|
|
128
|
+
}
|
|
129
|
+
/** Get a widget definition by key, or `undefined` if unknown. */
|
|
130
|
+
get(key) {
|
|
131
|
+
return this.map.get(key);
|
|
132
|
+
}
|
|
133
|
+
/** True if a widget with the given key is registered. */
|
|
134
|
+
has(key) {
|
|
135
|
+
return this.map.has(key);
|
|
136
|
+
}
|
|
137
|
+
/** Seed the registry from a `{ [key]: definition }` object. */
|
|
138
|
+
seed(widgets) {
|
|
139
|
+
for (const [key, def] of Object.entries(widgets)) {
|
|
140
|
+
this.map.set(key, def);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CreateComponentOptions, SplitviewPanel } from 'dockview';
|
|
2
|
+
import { type SplitviewContext } from './context.js';
|
|
3
|
+
import type { SplitviewWidgetRegistry } from './registry.js';
|
|
4
|
+
import type { SplitviewState } from './types.js';
|
|
5
|
+
/** The renderer factory returned by {@link createSplitviewFactory}. */
|
|
6
|
+
export interface SplitviewFactory {
|
|
7
|
+
createComponent: (options: CreateComponentOptions) => SplitviewPanel;
|
|
8
|
+
/** Access a view's state by id (used by `openPanel` to build the handle). */
|
|
9
|
+
getState: (id: string) => SplitviewState | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Fired when a pane's `api.onDidActiveChange` reports `isActive: true`.
|
|
12
|
+
* The component derives `bind:activeView` from this — `SplitviewApi`
|
|
13
|
+
* exposes no active event of its own.
|
|
14
|
+
*/
|
|
15
|
+
onDidActiveViewChange: (view: SplitviewPanel) => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Creates the splitview renderer factory backed by a shared widget registry.
|
|
19
|
+
*
|
|
20
|
+
* Unlike dockview's content/tab pair, a splitview view is a single
|
|
21
|
+
* `SplitviewPanel` subclass instance: `getComponent()` returns the
|
|
22
|
+
* `IFrameworkPart` (`{ update, dispose }`) that mounts the Svelte widget into
|
|
23
|
+
* the panel element. `init()` runs before the panel is added to the splitview,
|
|
24
|
+
* so the mount happens there; `layout()` writes `state.size` via the api's
|
|
25
|
+
* `onDidDimensionsChange` mirror.
|
|
26
|
+
*
|
|
27
|
+
* The mount forwards the parent context via Svelte's `mount` `context` option,
|
|
28
|
+
* so widgets can `getContext(DOCKVIEW_CONTEXT_KEY)`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createSplitviewFactory(registry: SplitviewWidgetRegistry, context?: SplitviewContext, hooks?: {
|
|
31
|
+
onDidActiveViewChange?: (view: SplitviewPanel) => void;
|
|
32
|
+
}): SplitviewFactory;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { SplitviewPanel as SplitviewPanelBase } from 'dockview';
|
|
2
|
+
import { mount, unmount } from 'svelte';
|
|
3
|
+
import { DOCKVIEW_CONTEXT_KEY } from './context.js';
|
|
4
|
+
import { deepEqual, mergeInto } from './utils.js';
|
|
5
|
+
/** Create a `$state`-wrapped splitview state. `$state` must be a declaration initializer. */
|
|
6
|
+
function createSplitviewState(api, params) {
|
|
7
|
+
const state = $state({
|
|
8
|
+
params: params ?? {},
|
|
9
|
+
size: { width: 0, height: 0 },
|
|
10
|
+
visible: api.isVisible,
|
|
11
|
+
active: api.isActive,
|
|
12
|
+
focused: api.isFocused,
|
|
13
|
+
api,
|
|
14
|
+
custom: {},
|
|
15
|
+
});
|
|
16
|
+
return state;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A splitview panel that mounts a Svelte widget into `this.element` and wires
|
|
20
|
+
* the reactive {@link SplitviewState}.
|
|
21
|
+
*
|
|
22
|
+
* `SplitviewPanel` is abstract with a single abstract member
|
|
23
|
+
* `getComponent(): IFrameworkPart`. The subclass overrides `init()` to mount
|
|
24
|
+
* (once `this.api`/`this.element` exist and params arrive) and `update()` for
|
|
25
|
+
* the dockview → widget params merge.
|
|
26
|
+
*
|
|
27
|
+
* `getComponent().update` is deliberately a no-op: dockview calls
|
|
28
|
+
* `part.update(...)` with inconsistent shapes (raw `params` from `layout()`,
|
|
29
|
+
* `{ params }` from `update()`), so params flow through the `update(event)`
|
|
30
|
+
* override instead — a single, well-typed path.
|
|
31
|
+
*/
|
|
32
|
+
class SvelteSplitviewPanel extends SplitviewPanelBase {
|
|
33
|
+
deps;
|
|
34
|
+
instance;
|
|
35
|
+
entry;
|
|
36
|
+
state;
|
|
37
|
+
disposables = [];
|
|
38
|
+
effectDestroy;
|
|
39
|
+
lastParams;
|
|
40
|
+
lastActive = false;
|
|
41
|
+
lastVisible = true;
|
|
42
|
+
raf = 0;
|
|
43
|
+
constructor(id, name, deps) {
|
|
44
|
+
super(id, name);
|
|
45
|
+
this.deps = deps;
|
|
46
|
+
}
|
|
47
|
+
init(parameters) {
|
|
48
|
+
super.init(parameters);
|
|
49
|
+
const { widget, context, getOrCreateState } = this.deps;
|
|
50
|
+
this.entry = getOrCreateState(this.api, parameters.params);
|
|
51
|
+
this.state = this.entry.state;
|
|
52
|
+
this.instance = mount(widget, {
|
|
53
|
+
target: this.element,
|
|
54
|
+
props: { state: this.state },
|
|
55
|
+
...(context ? { context: new Map([[DOCKVIEW_CONTEXT_KEY, context]]) } : {}),
|
|
56
|
+
});
|
|
57
|
+
this.lastParams = $state.snapshot(this.state.params);
|
|
58
|
+
this.lastActive = this.state.active;
|
|
59
|
+
this.lastVisible = this.state.visible;
|
|
60
|
+
// Read-only mirrors of the api flags. `size` is rAF-throttled —
|
|
61
|
+
// `onDidDimensionsChange` fires per-pixel on sash drags, exactly like the
|
|
62
|
+
// Dockview `layout()` hook.
|
|
63
|
+
this.disposables.push(this.api.onDidActiveChange((event) => {
|
|
64
|
+
this.state.active = event.isActive;
|
|
65
|
+
this.lastActive = event.isActive;
|
|
66
|
+
// Activation (button, `setActive`, focus) lands here on the
|
|
67
|
+
// newly-active pane — the component derives `bind:activeView`
|
|
68
|
+
// from it. Deactivation (`false`) is ignored; the incoming
|
|
69
|
+
// pane's `true` wins without ordering hazards.
|
|
70
|
+
if (event.isActive)
|
|
71
|
+
this.deps.notifyActive(this);
|
|
72
|
+
}), this.api.onDidFocusChange((event) => {
|
|
73
|
+
this.state.focused = event.isFocused;
|
|
74
|
+
}), this.api.onDidVisibilityChange((event) => {
|
|
75
|
+
this.state.visible = event.isVisible;
|
|
76
|
+
this.lastVisible = event.isVisible;
|
|
77
|
+
}), this.api.onDidDimensionsChange((event) => {
|
|
78
|
+
cancelAnimationFrame(this.raf);
|
|
79
|
+
this.raf = requestAnimationFrame(() => {
|
|
80
|
+
this.state.size.width = event.width;
|
|
81
|
+
this.state.size.height = event.height;
|
|
82
|
+
});
|
|
83
|
+
}));
|
|
84
|
+
// Widget → dockview (params double-bind, activation).
|
|
85
|
+
this.effectDestroy = $effect.root(() => {
|
|
86
|
+
$effect(() => {
|
|
87
|
+
const snap = $state.snapshot(this.state.params);
|
|
88
|
+
if (deepEqual(snap, this.lastParams))
|
|
89
|
+
return;
|
|
90
|
+
this.lastParams = snap;
|
|
91
|
+
this.state.api.updateParameters(snap);
|
|
92
|
+
});
|
|
93
|
+
$effect(() => {
|
|
94
|
+
// Only activation is meaningful from the widget side.
|
|
95
|
+
if (this.state.active && !this.lastActive) {
|
|
96
|
+
this.state.api.setActive();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
$effect(() => {
|
|
100
|
+
if (this.state.visible !== this.lastVisible) {
|
|
101
|
+
this.lastVisible = this.state.visible;
|
|
102
|
+
this.state.api.setVisible(this.state.visible);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
update(event) {
|
|
108
|
+
super.update(event);
|
|
109
|
+
if (!this.state)
|
|
110
|
+
return;
|
|
111
|
+
mergeInto(this.state.params, event.params);
|
|
112
|
+
this.lastParams = $state.snapshot(this.state.params);
|
|
113
|
+
}
|
|
114
|
+
getComponent() {
|
|
115
|
+
return {
|
|
116
|
+
update: () => {
|
|
117
|
+
// Params flow through `update(event)` above, not here (see class doc).
|
|
118
|
+
},
|
|
119
|
+
dispose: () => {
|
|
120
|
+
cancelAnimationFrame(this.raf);
|
|
121
|
+
this.effectDestroy?.();
|
|
122
|
+
this.effectDestroy = undefined;
|
|
123
|
+
for (const disposable of this.disposables)
|
|
124
|
+
disposable.dispose();
|
|
125
|
+
this.disposables = [];
|
|
126
|
+
if (this.instance) {
|
|
127
|
+
unmount(this.instance);
|
|
128
|
+
this.instance = undefined;
|
|
129
|
+
}
|
|
130
|
+
if (this.entry && !this.entry.disposed) {
|
|
131
|
+
this.entry.disposed = true;
|
|
132
|
+
this.deps.releaseState(this.id);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Creates the splitview renderer factory backed by a shared widget registry.
|
|
140
|
+
*
|
|
141
|
+
* Unlike dockview's content/tab pair, a splitview view is a single
|
|
142
|
+
* `SplitviewPanel` subclass instance: `getComponent()` returns the
|
|
143
|
+
* `IFrameworkPart` (`{ update, dispose }`) that mounts the Svelte widget into
|
|
144
|
+
* the panel element. `init()` runs before the panel is added to the splitview,
|
|
145
|
+
* so the mount happens there; `layout()` writes `state.size` via the api's
|
|
146
|
+
* `onDidDimensionsChange` mirror.
|
|
147
|
+
*
|
|
148
|
+
* The mount forwards the parent context via Svelte's `mount` `context` option,
|
|
149
|
+
* so widgets can `getContext(DOCKVIEW_CONTEXT_KEY)`.
|
|
150
|
+
*/
|
|
151
|
+
export function createSplitviewFactory(registry, context, hooks) {
|
|
152
|
+
const panels = new Map();
|
|
153
|
+
function getOrCreateState(api, params) {
|
|
154
|
+
let entry = panels.get(api.id);
|
|
155
|
+
if (!entry) {
|
|
156
|
+
entry = { state: createSplitviewState(api, params), disposed: false };
|
|
157
|
+
panels.set(api.id, entry);
|
|
158
|
+
}
|
|
159
|
+
return entry;
|
|
160
|
+
}
|
|
161
|
+
function createComponent(options) {
|
|
162
|
+
const def = registry.get(options.name);
|
|
163
|
+
if (!def) {
|
|
164
|
+
throw new Error(`dockview-svelte: unknown widget "${options.name}"`);
|
|
165
|
+
}
|
|
166
|
+
return new SvelteSplitviewPanel(options.id, options.name, {
|
|
167
|
+
widget: def.component,
|
|
168
|
+
context,
|
|
169
|
+
getOrCreateState,
|
|
170
|
+
releaseState: (id) => panels.delete(id),
|
|
171
|
+
notifyActive: (view) => hooks?.onDidActiveViewChange?.(view),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
createComponent,
|
|
176
|
+
getState: (id) => panels.get(id)?.state,
|
|
177
|
+
onDidActiveViewChange: (view) => hooks?.onDidActiveViewChange?.(view),
|
|
178
|
+
};
|
|
179
|
+
}
|