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,307 @@
|
|
|
1
|
+
<script lang="ts" generics="const W extends PaneviewWidgets">
|
|
2
|
+
import type {
|
|
3
|
+
IPaneviewPanel,
|
|
4
|
+
PaneviewApi,
|
|
5
|
+
PaneviewComponentOptions,
|
|
6
|
+
PaneviewDidDropEvent,
|
|
7
|
+
PaneviewDndOverlayEvent,
|
|
8
|
+
SerializedPaneview,
|
|
9
|
+
} from 'dockview'
|
|
10
|
+
import { createPaneview } from 'dockview'
|
|
11
|
+
import { onMount, type Snippet, setContext } from 'svelte'
|
|
12
|
+
import { DOCKVIEW_CONTEXT_KEY, type PaneviewContext } from '../core/context.js'
|
|
13
|
+
import { createPaneviewFactory } from '../core/paneview.svelte.js'
|
|
14
|
+
import { PaneviewWidgetRegistry } from '../core/registry.js'
|
|
15
|
+
import type {
|
|
16
|
+
PaneviewHandle,
|
|
17
|
+
PaneviewOpenPanelOptions,
|
|
18
|
+
PaneviewPanelHandle,
|
|
19
|
+
PaneviewParamsOf,
|
|
20
|
+
PaneviewWidgetDefinition,
|
|
21
|
+
PaneviewWidgets,
|
|
22
|
+
} from '../core/types.js'
|
|
23
|
+
import { deepEqual } from '../core/utils.js'
|
|
24
|
+
|
|
25
|
+
interface Props<W extends PaneviewWidgets> {
|
|
26
|
+
/** Widget registry: `{ [key]: { component, header?, title? } }`. */
|
|
27
|
+
widgets?: W
|
|
28
|
+
/**
|
|
29
|
+
* `PaneviewComponentOptions` passthrough (disableDnd, …).
|
|
30
|
+
*
|
|
31
|
+
* `createComponent` / `createHeaderComponent` are owned by the library
|
|
32
|
+
* (Svelte `mount` factories backed by the `widgets` registry) and cannot
|
|
33
|
+
* be overridden.
|
|
34
|
+
*/
|
|
35
|
+
options?: Omit<PaneviewComponentOptions, 'createComponent' | 'createHeaderComponent'>
|
|
36
|
+
/** `bind:layout` — paneview JSON (`toJSON`/`fromJSON` shape). */
|
|
37
|
+
layout?: SerializedPaneview
|
|
38
|
+
/** `bind:handle` — `{ api, openPanel, registerWidget, removePanel, movePanel, setVisible, setExpanded }`. */
|
|
39
|
+
handle?: PaneviewHandle<W>
|
|
40
|
+
/**
|
|
41
|
+
* `bind:panels` — reactive list of current panes (`api.panels` snapshot).
|
|
42
|
+
* Refreshed on layout/add/remove/FromJSON, so `panels.length` is the
|
|
43
|
+
* sveltish pane count — no manual `onDidAddView` counting needed.
|
|
44
|
+
*/
|
|
45
|
+
panels?: IPaneviewPanel[]
|
|
46
|
+
/** Extra classes for the root container. */
|
|
47
|
+
class?: string
|
|
48
|
+
/** Declarative widgets (`<DvWidget>` children) — alternative to the `widgets` prop. */
|
|
49
|
+
children?: Snippet
|
|
50
|
+
/** Fired once the component is mounted and `api` is ready. */
|
|
51
|
+
onReady?: (event: { api: PaneviewApi; handle: PaneviewHandle<W> }) => void
|
|
52
|
+
onDidLayoutChange?: () => void
|
|
53
|
+
onDidLayoutFromJSON?: () => void
|
|
54
|
+
onDidAddView?: (panel: IPaneviewPanel) => void
|
|
55
|
+
onDidRemoveView?: (panel: IPaneviewPanel) => void
|
|
56
|
+
onDidDrop?: (event: PaneviewDidDropEvent) => void
|
|
57
|
+
onUnhandledDragOver?: (event: PaneviewDndOverlayEvent) => void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let {
|
|
61
|
+
widgets = {} as W,
|
|
62
|
+
options = {},
|
|
63
|
+
layout = $bindable(),
|
|
64
|
+
handle = $bindable(),
|
|
65
|
+
panels = $bindable([]),
|
|
66
|
+
class: className = '',
|
|
67
|
+
children,
|
|
68
|
+
onReady,
|
|
69
|
+
onDidLayoutChange,
|
|
70
|
+
onDidLayoutFromJSON,
|
|
71
|
+
onDidAddView,
|
|
72
|
+
onDidRemoveView,
|
|
73
|
+
onDidDrop,
|
|
74
|
+
onUnhandledDragOver,
|
|
75
|
+
}: Props<W> = $props()
|
|
76
|
+
|
|
77
|
+
let container: HTMLElement
|
|
78
|
+
let api = $state<PaneviewApi | undefined>(undefined)
|
|
79
|
+
|
|
80
|
+
const registry = new PaneviewWidgetRegistry()
|
|
81
|
+
const counters = new Map<string, number>()
|
|
82
|
+
|
|
83
|
+
// Stable `registerWidget`, shared by context and handle.
|
|
84
|
+
function registerWidget(key: string, def: PaneviewWidgetDefinition): void {
|
|
85
|
+
registry.register(key, def)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function unregisterWidget(key: string): void {
|
|
89
|
+
registry.unregister(key)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Context for descendant widgets (api is populated after mount).
|
|
93
|
+
const context = $state<PaneviewContext>({
|
|
94
|
+
api: undefined,
|
|
95
|
+
registerWidget,
|
|
96
|
+
unregisterWidget,
|
|
97
|
+
kind: 'paneview',
|
|
98
|
+
})
|
|
99
|
+
setContext(DOCKVIEW_CONTEXT_KEY, context)
|
|
100
|
+
|
|
101
|
+
// Seed the registry from the `widgets` prop (additive).
|
|
102
|
+
$effect(() => {
|
|
103
|
+
registry.seed(widgets)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
function nextId(key: string): string {
|
|
107
|
+
const n = (counters.get(key) ?? 0) + 1
|
|
108
|
+
counters.set(key, n)
|
|
109
|
+
return `${key}-${n}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `bind:layout` loop-break: the JSON we last emitted to the parent. */
|
|
113
|
+
let lastEmitted: SerializedPaneview | undefined
|
|
114
|
+
|
|
115
|
+
/** Serialize the current layout and push it to the bound `layout`. */
|
|
116
|
+
function emitLayout(): void {
|
|
117
|
+
if (!api) return
|
|
118
|
+
const json = api.toJSON()
|
|
119
|
+
lastEmitted = json
|
|
120
|
+
layout = json
|
|
121
|
+
refreshPanels()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Refresh `bind:panels` from the live panes. */
|
|
125
|
+
function refreshPanels(): void {
|
|
126
|
+
if (!api) return
|
|
127
|
+
panels = [...api.panels]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getPanelOrThrow(id: string): import('dockview').PaneviewPanel {
|
|
131
|
+
const panel = api!.getPanel(id)
|
|
132
|
+
if (!panel) {
|
|
133
|
+
throw new Error(`dockview-svelte: unknown panel "${id}"`)
|
|
134
|
+
}
|
|
135
|
+
return panel as import('dockview').PaneviewPanel
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function openPanel<K extends keyof W & string>(
|
|
139
|
+
key: K,
|
|
140
|
+
opts?: PaneviewOpenPanelOptions<PaneviewParamsOf<W[K]['component']>>
|
|
141
|
+
): PaneviewPanelHandle<PaneviewParamsOf<W[K]['component']>> {
|
|
142
|
+
if (!api) {
|
|
143
|
+
throw new Error('dockview-svelte: Paneview is not mounted yet')
|
|
144
|
+
}
|
|
145
|
+
const def = registry.get(key)
|
|
146
|
+
if (!def) {
|
|
147
|
+
throw new Error(`dockview-svelte: unknown widget "${key}"`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const id = opts?.id ?? nextId(key)
|
|
151
|
+
// Title resolves through the same three-tier chain as Dockview
|
|
152
|
+
// (explicit → per-widget → widget key), evaluated once at open time.
|
|
153
|
+
// `headerComponent` reuses the widget key so `createHeaderComponent`
|
|
154
|
+
// can resolve the header off the same registry entry; omit it when the
|
|
155
|
+
// widget defines no header so dockview falls back to `DefaultHeader`.
|
|
156
|
+
const panel = api.addPanel({
|
|
157
|
+
...(opts ?? {}),
|
|
158
|
+
id,
|
|
159
|
+
component: key,
|
|
160
|
+
headerComponent: def.header ? key : undefined,
|
|
161
|
+
title: opts?.title ?? def.title ?? key,
|
|
162
|
+
params: opts?.params ?? {},
|
|
163
|
+
})
|
|
164
|
+
// `addPanel` fires no observed event for the open path itself — refresh the
|
|
165
|
+
// bound panels/layout directly so `bind:panels` reflects the new pane.
|
|
166
|
+
emitLayout()
|
|
167
|
+
|
|
168
|
+
const state = factory?.getState(id)
|
|
169
|
+
// `state` is `PaneviewState<Record<string, unknown>>` at the factory
|
|
170
|
+
// boundary; `PaneviewState<P>` is invariant in `P` (params is read/write),
|
|
171
|
+
// so the runtime-known `P` requires an `unknown` bridge.
|
|
172
|
+
return { id, panel, api: panel.api, state: state! } as unknown as PaneviewPanelHandle<
|
|
173
|
+
PaneviewParamsOf<W[K]['component']>
|
|
174
|
+
>
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Remove a pane by panel id. */
|
|
178
|
+
function removePanel(id: string): void {
|
|
179
|
+
if (!api) {
|
|
180
|
+
throw new Error('dockview-svelte: Paneview is not mounted yet')
|
|
181
|
+
}
|
|
182
|
+
api.removePanel(getPanelOrThrow(id))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Move a pane from one index to another. */
|
|
186
|
+
function movePanel(from: number, to: number): void {
|
|
187
|
+
if (!api) {
|
|
188
|
+
throw new Error('dockview-svelte: Paneview is not mounted yet')
|
|
189
|
+
}
|
|
190
|
+
api.movePanel(from, to)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Show or hide a pane by id (via the panel api — `panel.setVisible` only fires `onDidVisibilityChange` without touching the layout). */
|
|
194
|
+
function setVisible(id: string, visible: boolean): void {
|
|
195
|
+
if (!api) {
|
|
196
|
+
throw new Error('dockview-svelte: Paneview is not mounted yet')
|
|
197
|
+
}
|
|
198
|
+
getPanelOrThrow(id).api.setVisible(visible)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Expand or collapse a pane by id. */
|
|
202
|
+
function setExpanded(id: string, expanded: boolean): void {
|
|
203
|
+
if (!api) {
|
|
204
|
+
throw new Error('dockview-svelte: Paneview is not mounted yet')
|
|
205
|
+
}
|
|
206
|
+
getPanelOrThrow(id).setExpanded(expanded)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let factory: ReturnType<typeof createPaneviewFactory> | undefined
|
|
210
|
+
|
|
211
|
+
// External `layout` changes → apply via `fromJSON` (loop-break via `lastEmitted`).
|
|
212
|
+
$effect(() => {
|
|
213
|
+
const current = api
|
|
214
|
+
if (!current || !layout) return
|
|
215
|
+
if (deepEqual(lastEmitted, layout)) return
|
|
216
|
+
current.fromJSON(layout)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// Live `options` changes → apply via `updateOptions`.
|
|
220
|
+
// NOTE: `opts` must be read before the `api` guard — otherwise the
|
|
221
|
+
// initial run returns early without tracking `options`, and later changes
|
|
222
|
+
// never re-trigger the effect.
|
|
223
|
+
$effect(() => {
|
|
224
|
+
const opts = options
|
|
225
|
+
if (!api) return
|
|
226
|
+
api.updateOptions(opts)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
onMount(() => {
|
|
230
|
+
factory = createPaneviewFactory(registry, context)
|
|
231
|
+
const createdApi = createPaneview(container, {
|
|
232
|
+
...options,
|
|
233
|
+
createComponent: factory.createComponent,
|
|
234
|
+
createHeaderComponent: factory.createHeaderComponent,
|
|
235
|
+
})
|
|
236
|
+
// Apply a pre-existing `bind:layout` before exposing the api.
|
|
237
|
+
if (layout) {
|
|
238
|
+
createdApi.fromJSON(layout)
|
|
239
|
+
lastEmitted = createdApi.toJSON()
|
|
240
|
+
layout = lastEmitted
|
|
241
|
+
}
|
|
242
|
+
api = createdApi
|
|
243
|
+
context.api = createdApi
|
|
244
|
+
|
|
245
|
+
const createdHandle: PaneviewHandle<W> = {
|
|
246
|
+
api: createdApi,
|
|
247
|
+
openPanel,
|
|
248
|
+
registerWidget,
|
|
249
|
+
removePanel,
|
|
250
|
+
movePanel,
|
|
251
|
+
setVisible,
|
|
252
|
+
setExpanded,
|
|
253
|
+
}
|
|
254
|
+
handle = createdHandle
|
|
255
|
+
|
|
256
|
+
// Bridge paneview events to callbacks, disposing on teardown.
|
|
257
|
+
const subscriptions = [
|
|
258
|
+
createdApi.onDidLayoutChange(() => {
|
|
259
|
+
emitLayout()
|
|
260
|
+
onDidLayoutChange?.()
|
|
261
|
+
}),
|
|
262
|
+
createdApi.onDidLayoutFromJSON(() => {
|
|
263
|
+
refreshPanels()
|
|
264
|
+
onDidLayoutFromJSON?.()
|
|
265
|
+
}),
|
|
266
|
+
createdApi.onDidAddView((panel: IPaneviewPanel) => {
|
|
267
|
+
refreshPanels()
|
|
268
|
+
onDidAddView?.(panel)
|
|
269
|
+
}),
|
|
270
|
+
createdApi.onDidRemoveView((panel: IPaneviewPanel) => {
|
|
271
|
+
refreshPanels()
|
|
272
|
+
onDidRemoveView?.(panel)
|
|
273
|
+
}),
|
|
274
|
+
createdApi.onDidDrop((event: PaneviewDidDropEvent) => {
|
|
275
|
+
onDidDrop?.(event)
|
|
276
|
+
}),
|
|
277
|
+
createdApi.onUnhandledDragOver((event: PaneviewDndOverlayEvent) => {
|
|
278
|
+
onUnhandledDragOver?.(event)
|
|
279
|
+
}),
|
|
280
|
+
]
|
|
281
|
+
// Seed the initial panels before exposing the handle.
|
|
282
|
+
refreshPanels()
|
|
283
|
+
onReady?.({ api: createdApi, handle: createdHandle })
|
|
284
|
+
|
|
285
|
+
return () => {
|
|
286
|
+
for (const disposable of subscriptions) disposable.dispose()
|
|
287
|
+
createdApi.dispose()
|
|
288
|
+
api = undefined
|
|
289
|
+
factory = undefined
|
|
290
|
+
lastEmitted = undefined
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
</script>
|
|
294
|
+
|
|
295
|
+
<div class="dv-svelte-paneview-root {className}" bind:this={container}>
|
|
296
|
+
{#if children}
|
|
297
|
+
{@render children()}
|
|
298
|
+
{/if}
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<style>
|
|
302
|
+
.dv-svelte-paneview-root {
|
|
303
|
+
width: 100%;
|
|
304
|
+
height: 100%;
|
|
305
|
+
position: relative;
|
|
306
|
+
}
|
|
307
|
+
</style>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { IPaneviewPanel, PaneviewApi, PaneviewComponentOptions, PaneviewDidDropEvent, PaneviewDndOverlayEvent, SerializedPaneview } from 'dockview';
|
|
2
|
+
import { type Snippet } from 'svelte';
|
|
3
|
+
import type { PaneviewHandle, PaneviewWidgets } from '../core/types.js';
|
|
4
|
+
interface Props<W extends PaneviewWidgets> {
|
|
5
|
+
/** Widget registry: `{ [key]: { component, header?, title? } }`. */
|
|
6
|
+
widgets?: W;
|
|
7
|
+
/**
|
|
8
|
+
* `PaneviewComponentOptions` passthrough (disableDnd, …).
|
|
9
|
+
*
|
|
10
|
+
* `createComponent` / `createHeaderComponent` are owned by the library
|
|
11
|
+
* (Svelte `mount` factories backed by the `widgets` registry) and cannot
|
|
12
|
+
* be overridden.
|
|
13
|
+
*/
|
|
14
|
+
options?: Omit<PaneviewComponentOptions, 'createComponent' | 'createHeaderComponent'>;
|
|
15
|
+
/** `bind:layout` — paneview JSON (`toJSON`/`fromJSON` shape). */
|
|
16
|
+
layout?: SerializedPaneview;
|
|
17
|
+
/** `bind:handle` — `{ api, openPanel, registerWidget, removePanel, movePanel, setVisible, setExpanded }`. */
|
|
18
|
+
handle?: PaneviewHandle<W>;
|
|
19
|
+
/**
|
|
20
|
+
* `bind:panels` — reactive list of current panes (`api.panels` snapshot).
|
|
21
|
+
* Refreshed on layout/add/remove/FromJSON, so `panels.length` is the
|
|
22
|
+
* sveltish pane count — no manual `onDidAddView` counting needed.
|
|
23
|
+
*/
|
|
24
|
+
panels?: IPaneviewPanel[];
|
|
25
|
+
/** Extra classes for the root container. */
|
|
26
|
+
class?: string;
|
|
27
|
+
/** Declarative widgets (`<DvWidget>` children) — alternative to the `widgets` prop. */
|
|
28
|
+
children?: Snippet;
|
|
29
|
+
/** Fired once the component is mounted and `api` is ready. */
|
|
30
|
+
onReady?: (event: {
|
|
31
|
+
api: PaneviewApi;
|
|
32
|
+
handle: PaneviewHandle<W>;
|
|
33
|
+
}) => void;
|
|
34
|
+
onDidLayoutChange?: () => void;
|
|
35
|
+
onDidLayoutFromJSON?: () => void;
|
|
36
|
+
onDidAddView?: (panel: IPaneviewPanel) => void;
|
|
37
|
+
onDidRemoveView?: (panel: IPaneviewPanel) => void;
|
|
38
|
+
onDidDrop?: (event: PaneviewDidDropEvent) => void;
|
|
39
|
+
onUnhandledDragOver?: (event: PaneviewDndOverlayEvent) => void;
|
|
40
|
+
}
|
|
41
|
+
declare function $$render<const W extends PaneviewWidgets>(): {
|
|
42
|
+
props: Props<W>;
|
|
43
|
+
exports: {};
|
|
44
|
+
bindings: "layout" | "handle" | "panels";
|
|
45
|
+
slots: {};
|
|
46
|
+
events: {};
|
|
47
|
+
};
|
|
48
|
+
declare class __sveltets_Render<const W extends PaneviewWidgets> {
|
|
49
|
+
props(): ReturnType<typeof $$render<W>>['props'];
|
|
50
|
+
events(): ReturnType<typeof $$render<W>>['events'];
|
|
51
|
+
slots(): ReturnType<typeof $$render<W>>['slots'];
|
|
52
|
+
bindings(): "layout" | "handle" | "panels";
|
|
53
|
+
exports(): {};
|
|
54
|
+
}
|
|
55
|
+
interface $$IsomorphicComponent {
|
|
56
|
+
new <const W extends PaneviewWidgets>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<W>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<W>['props']>, ReturnType<__sveltets_Render<W>['events']>, ReturnType<__sveltets_Render<W>['slots']>> & {
|
|
57
|
+
$$bindings?: ReturnType<__sveltets_Render<W>['bindings']>;
|
|
58
|
+
} & ReturnType<__sveltets_Render<W>['exports']>;
|
|
59
|
+
<const W extends PaneviewWidgets>(internal: unknown, props: ReturnType<__sveltets_Render<W>['props']> & {}): ReturnType<__sveltets_Render<W>['exports']>;
|
|
60
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
61
|
+
}
|
|
62
|
+
declare const Paneview: $$IsomorphicComponent;
|
|
63
|
+
type Paneview<const W extends PaneviewWidgets> = InstanceType<typeof Paneview<W>>;
|
|
64
|
+
export default Paneview;
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
<script lang="ts" generics="const W extends SplitviewWidgets">
|
|
2
|
+
import type {
|
|
3
|
+
ISplitviewPanel,
|
|
4
|
+
IView,
|
|
5
|
+
SerializedSplitview,
|
|
6
|
+
SplitviewApi,
|
|
7
|
+
SplitviewComponentOptions,
|
|
8
|
+
} from 'dockview'
|
|
9
|
+
import { createSplitview } from 'dockview'
|
|
10
|
+
import { onMount, type Snippet, setContext } from 'svelte'
|
|
11
|
+
import { DOCKVIEW_CONTEXT_KEY, type SplitviewContext } from '../core/context.js'
|
|
12
|
+
import { SplitviewWidgetRegistry } from '../core/registry.js'
|
|
13
|
+
import { createSplitviewFactory } from '../core/splitview.svelte.js'
|
|
14
|
+
import type {
|
|
15
|
+
SplitviewHandle,
|
|
16
|
+
SplitviewOpenPanelOptions,
|
|
17
|
+
SplitviewPanelHandle,
|
|
18
|
+
SplitviewParamsOf,
|
|
19
|
+
SplitviewWidgetDefinition,
|
|
20
|
+
SplitviewWidgets,
|
|
21
|
+
} from '../core/types.js'
|
|
22
|
+
import { deepEqual } from '../core/utils.js'
|
|
23
|
+
|
|
24
|
+
interface Props<W extends SplitviewWidgets> {
|
|
25
|
+
/** Widget registry: `{ [key]: { component } }` — no tabs or titles, splitviews have no headers. */
|
|
26
|
+
widgets?: W
|
|
27
|
+
/**
|
|
28
|
+
* `SplitviewComponentOptions` passthrough (orientation, proportionalLayout, …).
|
|
29
|
+
*
|
|
30
|
+
* `createComponent` is owned by the library (Svelte `mount` factory
|
|
31
|
+
* backed by the `widgets` registry) and cannot be overridden.
|
|
32
|
+
*/
|
|
33
|
+
options?: Omit<SplitviewComponentOptions, 'createComponent'>
|
|
34
|
+
/** `bind:layout` — splitview JSON (`toJSON`/`fromJSON` shape). */
|
|
35
|
+
layout?: SerializedSplitview
|
|
36
|
+
/** `bind:handle` — `{ api, openPanel, registerWidget, removePanel, movePanel, setVisible, setActive }`. */
|
|
37
|
+
handle?: SplitviewHandle<W>
|
|
38
|
+
/**
|
|
39
|
+
* `bind:views` — reactive list of current views (`api.panels` snapshot).
|
|
40
|
+
* Refreshed on layout/add/remove/FromJSON, so `views.length` is the
|
|
41
|
+
* sveltish view count — no manual `onDidAddView` counting needed.
|
|
42
|
+
*/
|
|
43
|
+
views?: ISplitviewPanel[]
|
|
44
|
+
/**
|
|
45
|
+
* `bind:activeView` — the currently active pane. `SplitviewApi` exposes
|
|
46
|
+
* no active event (activation only fires per-panel
|
|
47
|
+
* `api.onDidActiveChange`), so this derives from those events plus
|
|
48
|
+
* explicit sync on the open/remove paths (dockview activates the new
|
|
49
|
+
* pane on `addPanel` and the last pane on `removePanel`).
|
|
50
|
+
*/
|
|
51
|
+
activeView?: ISplitviewPanel | undefined
|
|
52
|
+
/** Extra classes for the root container. */
|
|
53
|
+
class?: string
|
|
54
|
+
/** Declarative widgets (`<DvWidget>` children) — alternative to the `widgets` prop. */
|
|
55
|
+
children?: Snippet
|
|
56
|
+
/** Fired once the component is mounted and `api` is ready. */
|
|
57
|
+
onReady?: (event: { api: SplitviewApi; handle: SplitviewHandle<W> }) => void
|
|
58
|
+
onDidLayoutChange?: () => void
|
|
59
|
+
onDidLayoutFromJSON?: () => void
|
|
60
|
+
onDidAddView?: (view: IView) => void
|
|
61
|
+
onDidRemoveView?: (view: IView) => void
|
|
62
|
+
onDidActiveViewChange?: (view: ISplitviewPanel | undefined) => void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let {
|
|
66
|
+
widgets = {} as W,
|
|
67
|
+
options = {},
|
|
68
|
+
layout = $bindable(),
|
|
69
|
+
handle = $bindable(),
|
|
70
|
+
views = $bindable([]),
|
|
71
|
+
activeView = $bindable(undefined),
|
|
72
|
+
class: className = '',
|
|
73
|
+
children,
|
|
74
|
+
onReady,
|
|
75
|
+
onDidLayoutChange,
|
|
76
|
+
onDidLayoutFromJSON,
|
|
77
|
+
onDidAddView,
|
|
78
|
+
onDidRemoveView,
|
|
79
|
+
onDidActiveViewChange,
|
|
80
|
+
}: Props<W> = $props()
|
|
81
|
+
|
|
82
|
+
let container: HTMLElement
|
|
83
|
+
let api = $state<SplitviewApi | undefined>(undefined)
|
|
84
|
+
|
|
85
|
+
const registry = new SplitviewWidgetRegistry()
|
|
86
|
+
const counters = new Map<string, number>()
|
|
87
|
+
|
|
88
|
+
// Stable `registerWidget`, shared by context and handle.
|
|
89
|
+
function registerWidget(key: string, def: SplitviewWidgetDefinition): void {
|
|
90
|
+
registry.register(key, def)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function unregisterWidget(key: string): void {
|
|
94
|
+
registry.unregister(key)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Context for descendant widgets (api is populated after mount).
|
|
98
|
+
const context = $state<SplitviewContext>({
|
|
99
|
+
api: undefined,
|
|
100
|
+
registerWidget,
|
|
101
|
+
unregisterWidget,
|
|
102
|
+
kind: 'splitview',
|
|
103
|
+
})
|
|
104
|
+
setContext(DOCKVIEW_CONTEXT_KEY, context)
|
|
105
|
+
|
|
106
|
+
// Seed the registry from the `widgets` prop (additive).
|
|
107
|
+
$effect(() => {
|
|
108
|
+
registry.seed(widgets)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
function nextId(key: string): string {
|
|
112
|
+
const n = (counters.get(key) ?? 0) + 1
|
|
113
|
+
counters.set(key, n)
|
|
114
|
+
return `${key}-${n}`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** `bind:layout` loop-break: the JSON we last emitted to the parent. */
|
|
118
|
+
let lastEmitted: SerializedSplitview | undefined
|
|
119
|
+
|
|
120
|
+
/** Serialize the current layout and push it to the bound `layout`. */
|
|
121
|
+
function emitLayout(): void {
|
|
122
|
+
if (!api) return
|
|
123
|
+
const json = api.toJSON()
|
|
124
|
+
lastEmitted = json
|
|
125
|
+
layout = json
|
|
126
|
+
refreshViews()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Refresh `bind:views` from the live panels. */
|
|
130
|
+
function refreshViews(): void {
|
|
131
|
+
if (!api) return
|
|
132
|
+
views = [...api.panels]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Set `bind:activeView` + fire `onDidActiveViewChange`, deduped on panel id.
|
|
137
|
+
* Per-panel `onDidActiveChange` fires on both the deactivated and the
|
|
138
|
+
* activated pane, so the guard keeps a single update per activation.
|
|
139
|
+
*/
|
|
140
|
+
function setActiveView(view: ISplitviewPanel | undefined): void {
|
|
141
|
+
if (activeView?.id === view?.id) return
|
|
142
|
+
activeView = view
|
|
143
|
+
onDidActiveViewChange?.(view)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function openPanel<K extends keyof W & string>(
|
|
147
|
+
key: K,
|
|
148
|
+
opts?: SplitviewOpenPanelOptions<SplitviewParamsOf<W[K]['component']>>
|
|
149
|
+
): SplitviewPanelHandle<SplitviewParamsOf<W[K]['component']>> {
|
|
150
|
+
if (!api) {
|
|
151
|
+
throw new Error('dockview-svelte: Splitview is not mounted yet')
|
|
152
|
+
}
|
|
153
|
+
const def = registry.get(key)
|
|
154
|
+
if (!def) {
|
|
155
|
+
throw new Error(`dockview-svelte: unknown widget "${key}"`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const id = opts?.id ?? nextId(key)
|
|
159
|
+
const panel = api.addPanel({
|
|
160
|
+
...(opts ?? {}),
|
|
161
|
+
id,
|
|
162
|
+
component: key,
|
|
163
|
+
params: opts?.params ?? {},
|
|
164
|
+
})
|
|
165
|
+
// `addPanel` is synchronous but fires no event the component observes for
|
|
166
|
+
// the open path itself (events cover external mutations) — refresh the
|
|
167
|
+
// bound views/layout directly so `bind:views` reflects the new panel.
|
|
168
|
+
// `setActive` inside `addPanel` fires per-panel `onDidActiveChange`,
|
|
169
|
+
// which the factory mirrors to `state.active` but the component does
|
|
170
|
+
// not observe — so sync `bind:activeView` explicitly.
|
|
171
|
+
emitLayout()
|
|
172
|
+
setActiveView(panel)
|
|
173
|
+
|
|
174
|
+
const state = factory?.getState(id)
|
|
175
|
+
// `state` is `SplitviewState<Record<string, unknown>>` at the factory
|
|
176
|
+
// boundary; `SplitviewState<P>` is invariant in `P` (params is read/write),
|
|
177
|
+
// so the runtime-known `P` requires an `unknown` bridge.
|
|
178
|
+
return { id, panel, api: panel.api, state: state! } as unknown as SplitviewPanelHandle<
|
|
179
|
+
SplitviewParamsOf<W[K]['component']>
|
|
180
|
+
>
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Remove a view by panel id. */
|
|
184
|
+
function removePanel(id: string): void {
|
|
185
|
+
if (!api) {
|
|
186
|
+
throw new Error('dockview-svelte: Splitview is not mounted yet')
|
|
187
|
+
}
|
|
188
|
+
const panel = api.getPanel(id)
|
|
189
|
+
if (!panel) {
|
|
190
|
+
throw new Error(`dockview-svelte: unknown panel "${id}"`)
|
|
191
|
+
}
|
|
192
|
+
const wasActive = activeView?.id === panel.id
|
|
193
|
+
api.removePanel(panel)
|
|
194
|
+
// `removePanel` activates the last pane without any active event —
|
|
195
|
+
// fall back to it when the active one was removed.
|
|
196
|
+
if (wasActive) setActiveView(api.panels.at(-1))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Move a view from one index to another. */
|
|
200
|
+
function movePanel(from: number, to: number): void {
|
|
201
|
+
if (!api) {
|
|
202
|
+
throw new Error('dockview-svelte: Splitview is not mounted yet')
|
|
203
|
+
}
|
|
204
|
+
api.movePanel(from, to)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getPanelOrThrow(id: string): import('dockview').SplitviewPanel {
|
|
208
|
+
const panel = api!.getPanel(id)
|
|
209
|
+
if (!panel) {
|
|
210
|
+
throw new Error(`dockview-svelte: unknown panel "${id}"`)
|
|
211
|
+
}
|
|
212
|
+
return panel as import('dockview').SplitviewPanel
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Show or hide a split pane by id (via the panel api — `panel.setVisible` only fires `onDidVisibilityChange` without touching the layout). */
|
|
216
|
+
function setVisible(id: string, visible: boolean): void {
|
|
217
|
+
if (!api) {
|
|
218
|
+
throw new Error('dockview-svelte: Splitview is not mounted yet')
|
|
219
|
+
}
|
|
220
|
+
getPanelOrThrow(id).api.setVisible(visible)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Activate a split pane by id (via the panel api — `panel.setActive` only fires `onDidActiveChange`). */
|
|
224
|
+
function setActive(id: string): void {
|
|
225
|
+
if (!api) {
|
|
226
|
+
throw new Error('dockview-svelte: Splitview is not mounted yet')
|
|
227
|
+
}
|
|
228
|
+
getPanelOrThrow(id).api.setActive()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let factory: ReturnType<typeof createSplitviewFactory> | undefined
|
|
232
|
+
|
|
233
|
+
// External `layout` changes → apply via `fromJSON` (loop-break via `lastEmitted`).
|
|
234
|
+
$effect(() => {
|
|
235
|
+
const current = api
|
|
236
|
+
if (!current || !layout) return
|
|
237
|
+
if (deepEqual(lastEmitted, layout)) return
|
|
238
|
+
current.fromJSON(layout)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// Live `options` changes (orientation, etc.) → apply via `updateOptions`.
|
|
242
|
+
// NOTE: `opts` must be read before the `api` guard — otherwise the
|
|
243
|
+
// initial run returns early without tracking `options`, and later changes
|
|
244
|
+
// never re-trigger the effect.
|
|
245
|
+
$effect(() => {
|
|
246
|
+
const opts = options
|
|
247
|
+
if (!api) return
|
|
248
|
+
api.updateOptions(opts)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
onMount(() => {
|
|
252
|
+
factory = createSplitviewFactory(registry, context, {
|
|
253
|
+
onDidActiveViewChange: (view) => setActiveView(view as unknown as ISplitviewPanel),
|
|
254
|
+
})
|
|
255
|
+
const createdApi = createSplitview(container, {
|
|
256
|
+
...options,
|
|
257
|
+
createComponent: factory.createComponent,
|
|
258
|
+
})
|
|
259
|
+
// Apply a pre-existing `bind:layout` before exposing the api.
|
|
260
|
+
if (layout) {
|
|
261
|
+
createdApi.fromJSON(layout)
|
|
262
|
+
lastEmitted = createdApi.toJSON()
|
|
263
|
+
layout = lastEmitted
|
|
264
|
+
}
|
|
265
|
+
api = createdApi
|
|
266
|
+
context.api = createdApi
|
|
267
|
+
|
|
268
|
+
const createdHandle: SplitviewHandle<W> = {
|
|
269
|
+
api: createdApi,
|
|
270
|
+
openPanel,
|
|
271
|
+
registerWidget,
|
|
272
|
+
removePanel,
|
|
273
|
+
movePanel,
|
|
274
|
+
setVisible,
|
|
275
|
+
setActive,
|
|
276
|
+
}
|
|
277
|
+
handle = createdHandle
|
|
278
|
+
|
|
279
|
+
// Bridge splitview events to callbacks, disposing on teardown.
|
|
280
|
+
const subscriptions = [
|
|
281
|
+
createdApi.onDidLayoutChange(() => {
|
|
282
|
+
emitLayout()
|
|
283
|
+
onDidLayoutChange?.()
|
|
284
|
+
}),
|
|
285
|
+
createdApi.onDidLayoutFromJSON(() => {
|
|
286
|
+
refreshViews()
|
|
287
|
+
onDidLayoutFromJSON?.()
|
|
288
|
+
}),
|
|
289
|
+
createdApi.onDidAddView((view: IView) => {
|
|
290
|
+
refreshViews()
|
|
291
|
+
// External adds (e.g. `api.addPanel` escape hatch) activate the
|
|
292
|
+
// new pane without a component-level event — same sync as the
|
|
293
|
+
// `openPanel` path.
|
|
294
|
+
setActiveView(view as unknown as ISplitviewPanel)
|
|
295
|
+
onDidAddView?.(view)
|
|
296
|
+
}),
|
|
297
|
+
createdApi.onDidRemoveView((view: IView) => {
|
|
298
|
+
const removedId = (view as unknown as ISplitviewPanel | undefined)?.id
|
|
299
|
+
refreshViews()
|
|
300
|
+
if (removedId !== undefined && activeView?.id === removedId) {
|
|
301
|
+
setActiveView(createdApi.panels.at(-1))
|
|
302
|
+
}
|
|
303
|
+
onDidRemoveView?.(view)
|
|
304
|
+
}),
|
|
305
|
+
]
|
|
306
|
+
// Seed the initial views before exposing the handle.
|
|
307
|
+
refreshViews()
|
|
308
|
+
onReady?.({ api: createdApi, handle: createdHandle })
|
|
309
|
+
|
|
310
|
+
return () => {
|
|
311
|
+
for (const disposable of subscriptions) disposable.dispose()
|
|
312
|
+
createdApi.dispose()
|
|
313
|
+
api = undefined
|
|
314
|
+
factory = undefined
|
|
315
|
+
lastEmitted = undefined
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
</script>
|
|
319
|
+
|
|
320
|
+
<div class="dv-svelte-splitview-root {className}" bind:this={container}>
|
|
321
|
+
{#if children}
|
|
322
|
+
{@render children()}
|
|
323
|
+
{/if}
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<style>
|
|
327
|
+
.dv-svelte-splitview-root {
|
|
328
|
+
width: 100%;
|
|
329
|
+
height: 100%;
|
|
330
|
+
position: relative;
|
|
331
|
+
}
|
|
332
|
+
</style>
|