@kubohiroya/turbowarp-plus-editor-layout 0.1.3 → 0.1.5
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/dist/dockview-stage.d.ts +20 -0
- package/dist/dockview-stage.js +45 -0
- package/dist/document-workspace.d.ts +15 -0
- package/dist/document-workspace.js +25 -0
- package/dist/sprite-layout-menu.d.ts +1 -0
- package/dist/sprite-layout-menu.js +1 -0
- package/dist/sprite-tabs.d.ts +8 -1
- package/dist/sprite-tabs.js +112 -6
- package/dist/stage-surface.d.ts +5 -4
- package/dist/stage-surface.js +11 -4
- package/dist/standard-layout.d.ts +2 -1
- package/dist/standard-layout.js +33 -14
- package/package.json +1 -1
- package/src/dockview-stage.ts +53 -0
- package/src/document-workspace.ts +31 -0
- package/src/editor.css +27 -0
- package/src/sprite-layout-menu.ts +2 -0
- package/src/sprite-tabs.ts +116 -6
- package/src/stage-surface.ts +14 -4
- package/src/standard-layout.ts +31 -11
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type StageCommands = {
|
|
2
|
+
open(): void;
|
|
3
|
+
close(): void;
|
|
4
|
+
};
|
|
5
|
+
export interface DockviewStagePane {
|
|
6
|
+
title: string;
|
|
7
|
+
attach(destination: HTMLElement | null): void;
|
|
8
|
+
connect(commands: StageCommands | null): void;
|
|
9
|
+
}
|
|
10
|
+
export declare function useDockviewStage(enabled: boolean, locale: string): {
|
|
11
|
+
surface: HTMLDivElement;
|
|
12
|
+
pane: DockviewStagePane;
|
|
13
|
+
docked: boolean;
|
|
14
|
+
commands: StageCommands;
|
|
15
|
+
inlineRef: (element: HTMLElement | null) => void;
|
|
16
|
+
suspend: (value: boolean) => void;
|
|
17
|
+
openLabel: string;
|
|
18
|
+
closeLabel: string;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export function useDockviewStage(enabled, locale) {
|
|
3
|
+
const [surface] = React.useState(() => {
|
|
4
|
+
if (!enabled)
|
|
5
|
+
return null;
|
|
6
|
+
const node = document.createElement('div');
|
|
7
|
+
node.className = 'tw-plus-dockview-stage-surface';
|
|
8
|
+
return node;
|
|
9
|
+
});
|
|
10
|
+
const inline = React.useRef(null);
|
|
11
|
+
const dock = React.useRef(null);
|
|
12
|
+
const suspended = React.useRef(false);
|
|
13
|
+
const [docked, setDocked] = React.useState(false);
|
|
14
|
+
const [commands, setCommands] = React.useState(null);
|
|
15
|
+
const move = React.useCallback(() => {
|
|
16
|
+
const destination = suspended.current ? inline.current : dock.current ?? inline.current;
|
|
17
|
+
if (surface && destination && surface.parentElement !== destination)
|
|
18
|
+
destination.appendChild(surface);
|
|
19
|
+
}, [surface]);
|
|
20
|
+
const inlineRef = React.useCallback((element) => {
|
|
21
|
+
inline.current = element;
|
|
22
|
+
if (surface && element)
|
|
23
|
+
surface.dir = element.closest('[dir]')?.getAttribute('dir') ?? '';
|
|
24
|
+
move();
|
|
25
|
+
}, [move, surface]);
|
|
26
|
+
const suspend = React.useCallback((value) => {
|
|
27
|
+
suspended.current = value;
|
|
28
|
+
move();
|
|
29
|
+
}, [move]);
|
|
30
|
+
const japanese = /^ja(?:-|$)/i.test(locale);
|
|
31
|
+
const pane = React.useMemo(() => ({
|
|
32
|
+
title: japanese ? 'ステージ' : 'Stage',
|
|
33
|
+
attach(destination) {
|
|
34
|
+
dock.current = destination;
|
|
35
|
+
move();
|
|
36
|
+
setDocked(Boolean(destination));
|
|
37
|
+
},
|
|
38
|
+
connect: setCommands
|
|
39
|
+
}), [japanese, move]);
|
|
40
|
+
return surface && {
|
|
41
|
+
surface, pane, docked, commands, inlineRef, suspend,
|
|
42
|
+
openLabel: japanese ? '作業領域で開く' : 'Open stage in workspace',
|
|
43
|
+
closeLabel: japanese ? '作業領域から戻す' : 'Return stage from workspace'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface DocumentPanelRequest {
|
|
2
|
+
reference: {
|
|
3
|
+
providerId: string;
|
|
4
|
+
documentId: string;
|
|
5
|
+
};
|
|
6
|
+
filename: string;
|
|
7
|
+
mount(element: HTMLElement, close: () => void): {
|
|
8
|
+
requestClose(): void;
|
|
9
|
+
dispose(): void;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare function getDocumentWorkspace(vm: object): {
|
|
13
|
+
open(request: DocumentPanelRequest): boolean;
|
|
14
|
+
connect(handler: NonNullable<(request: DocumentPanelRequest) => void>): () => void;
|
|
15
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function createDocumentWorkspace() {
|
|
2
|
+
let open;
|
|
3
|
+
return {
|
|
4
|
+
open(request) {
|
|
5
|
+
if (!open)
|
|
6
|
+
return false;
|
|
7
|
+
open(request);
|
|
8
|
+
return true;
|
|
9
|
+
},
|
|
10
|
+
connect(handler) {
|
|
11
|
+
open = handler;
|
|
12
|
+
return () => { if (open === handler)
|
|
13
|
+
open = undefined; };
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const workspaces = new WeakMap();
|
|
18
|
+
export function getDocumentWorkspace(vm) {
|
|
19
|
+
let workspace = workspaces.get(vm);
|
|
20
|
+
if (!workspace) {
|
|
21
|
+
workspace = createDocumentWorkspace();
|
|
22
|
+
workspaces.set(vm, workspace);
|
|
23
|
+
}
|
|
24
|
+
return workspace;
|
|
25
|
+
}
|
|
@@ -64,6 +64,7 @@ export function createSpriteLayoutMenu(document, locale, commands) {
|
|
|
64
64
|
if (openedIn)
|
|
65
65
|
return;
|
|
66
66
|
openedIn = trigger.ownerDocument;
|
|
67
|
+
buttons[0].disabled = buttons[1].disabled = commands.canSplit?.() === false;
|
|
67
68
|
buttons[2].disabled = !commands.canClose();
|
|
68
69
|
topLayer = typeof popup.showPopover === 'function';
|
|
69
70
|
if (topLayer) {
|
package/dist/sprite-tabs.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { SpriteVariable } from './sprite-asset-pane.tsx';
|
|
3
3
|
import type { SpriteSearchResult } from './sprite-editor-toolbar.tsx';
|
|
4
|
+
import type { DockviewStagePane } from './dockview-stage.ts';
|
|
5
|
+
export { getDocumentWorkspace } from './document-workspace.ts';
|
|
6
|
+
export { useDockviewStage } from './dockview-stage.ts';
|
|
4
7
|
interface CostumeAsset {
|
|
5
8
|
encodeDataURI(): string;
|
|
6
9
|
}
|
|
@@ -71,6 +74,7 @@ interface TabOptions<Snapshot = unknown> {
|
|
|
71
74
|
resize(): void;
|
|
72
75
|
reopenLabel: string;
|
|
73
76
|
multiView?: boolean;
|
|
77
|
+
stagePane?: DockviewStagePane;
|
|
74
78
|
variables?: boolean;
|
|
75
79
|
locale?: string;
|
|
76
80
|
viewAdapter?: SpriteTabAdapter<Snapshot>;
|
|
@@ -114,6 +118,8 @@ declare const renameLabels: {
|
|
|
114
118
|
type RenameLabels = typeof renameLabels.en;
|
|
115
119
|
export declare function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElement, options: TabOptions<Snapshot>): {
|
|
116
120
|
select: (id: string) => void;
|
|
121
|
+
openStage: () => void;
|
|
122
|
+
closeStage: () => void;
|
|
117
123
|
split: (direction: SpriteSplitDirection, id?: string) => string;
|
|
118
124
|
closeSplit: (id?: string) => boolean;
|
|
119
125
|
activateView: (id: string) => boolean;
|
|
@@ -150,6 +156,8 @@ export declare function useSpriteTabs<Snapshot>({ available, enabled, locale, ..
|
|
|
150
156
|
}): {
|
|
151
157
|
controller: React.MutableRefObject<{
|
|
152
158
|
select: (id: string) => void;
|
|
159
|
+
openStage: () => void;
|
|
160
|
+
closeStage: () => void;
|
|
153
161
|
split: (direction: SpriteSplitDirection, id?: string) => string;
|
|
154
162
|
closeSplit: (id?: string) => boolean;
|
|
155
163
|
activateView: (id: string) => boolean;
|
|
@@ -171,4 +179,3 @@ export declare function useSpriteTabs<Snapshot>({ available, enabled, locale, ..
|
|
|
171
179
|
renderEditorTabRow: (children: React.ReactNode) => React.FunctionComponentElement<SpriteTabsProps<Snapshot>>;
|
|
172
180
|
renderEditorPanel: (index: number, children: React.ReactNode) => React.ReactPortal;
|
|
173
181
|
};
|
|
174
|
-
export {};
|
package/dist/sprite-tabs.js
CHANGED
|
@@ -4,6 +4,9 @@ import { createDockview, DefaultTab, themeLight } from 'dockview';
|
|
|
4
4
|
import { createSpriteLayoutMenu } from "./sprite-layout-menu.js";
|
|
5
5
|
import { SpriteAssetPane } from "./sprite-asset-pane.js";
|
|
6
6
|
import { SpriteEditorToolbar, spriteEditorSections } from "./sprite-editor-toolbar.js";
|
|
7
|
+
import { getDocumentWorkspace } from "./document-workspace.js";
|
|
8
|
+
export { getDocumentWorkspace } from "./document-workspace.js";
|
|
9
|
+
export { useDockviewStage } from "./dockview-stage.js";
|
|
7
10
|
const variableSection = spriteEditorSections.indexOf('variables');
|
|
8
11
|
function createAssetSelections(target, source) {
|
|
9
12
|
const selected = new Map(source?.selected);
|
|
@@ -39,6 +42,8 @@ function createAssetSelections(target, source) {
|
|
|
39
42
|
const ports = { costumes: forKind('costumes'), sounds: forKind('sounds') };
|
|
40
43
|
return { selected, ports, layouts, selectors, scroll };
|
|
41
44
|
}
|
|
45
|
+
const stagePanelId = 'tw-plus-stage-display';
|
|
46
|
+
const documentComponent = 'tw-plus-document';
|
|
42
47
|
const renameLabels = {
|
|
43
48
|
en: { title: 'Rename sprite', name: 'Sprite name', cancel: 'Cancel', save: 'Rename' },
|
|
44
49
|
ja: { title: 'スプライト名の変更', name: 'スプライト名', cancel: 'キャンセル', save: '変更' }
|
|
@@ -68,11 +73,13 @@ class SpriteTab extends DefaultTab {
|
|
|
68
73
|
export function mountSpriteTabs(root, content, options) {
|
|
69
74
|
const { vm, multiView = false, locale = 'en' } = options;
|
|
70
75
|
const viewAdapter = multiView ? options.viewAdapter : undefined;
|
|
76
|
+
const stagePane = multiView ? options.stagePane : undefined;
|
|
71
77
|
const usesNativeSection = (section) => section !== variableSection || !options.variables;
|
|
72
78
|
const document = root.ownerDocument;
|
|
73
79
|
const editLabel = /^ja(?:-|$)/i.test(locale) ? 'クリックしてこのビューで編集' : 'Activate this view to edit';
|
|
74
80
|
const slots = new Map();
|
|
75
81
|
const views = new Map();
|
|
82
|
+
const documents = new Map();
|
|
76
83
|
const costumeIcons = new WeakMap();
|
|
77
84
|
const remembered = new WeakMap();
|
|
78
85
|
const lastUsed = new WeakMap();
|
|
@@ -95,8 +102,21 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
95
102
|
return [];
|
|
96
103
|
return [{ label: text.title, action: () => openRenameDialog(target) }];
|
|
97
104
|
},
|
|
98
|
-
createComponent: () => {
|
|
105
|
+
createComponent: ({ name, id: componentId }) => {
|
|
99
106
|
const element = document.createElement('div');
|
|
107
|
+
if (name === documentComponent) {
|
|
108
|
+
element.className = 'tw-plus-document-slot';
|
|
109
|
+
return { element, init({ api }) {
|
|
110
|
+
const item = documents.get(componentId);
|
|
111
|
+
element.dataset.providerId = item.request.reference.providerId;
|
|
112
|
+
element.dataset.documentId = item.request.reference.documentId;
|
|
113
|
+
item.session = item.request.mount(element, () => api.close());
|
|
114
|
+
} };
|
|
115
|
+
}
|
|
116
|
+
if (name === stagePanelId) {
|
|
117
|
+
element.className = 'tw-plus-dockview-stage-slot';
|
|
118
|
+
return { element, init() { stagePane.attach(element); } };
|
|
119
|
+
}
|
|
100
120
|
element.className = 'tw-plus-sprite-tab-slot';
|
|
101
121
|
const body = multiView ? document.createElement('div') : element;
|
|
102
122
|
const toolbar = multiView && options.onToolbarSlots ? document.createElement('div') : undefined;
|
|
@@ -182,7 +202,8 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
182
202
|
split('below', group.activePanel.id); },
|
|
183
203
|
close: () => { if (group.activePanel)
|
|
184
204
|
closeSplit(group.activePanel.id); },
|
|
185
|
-
canClose: () =>
|
|
205
|
+
canClose: () => canCloseGroup(group),
|
|
206
|
+
canSplit: () => views.has(group.activePanel?.id)
|
|
186
207
|
});
|
|
187
208
|
return { ...menu, init() { } };
|
|
188
209
|
}
|
|
@@ -320,6 +341,11 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
320
341
|
view.snapshots.set(view.section, { snapshot, view: descriptor });
|
|
321
342
|
}
|
|
322
343
|
function show(panel) {
|
|
344
|
+
if (panel?.id === stagePanelId || documents.has(panel?.id)) {
|
|
345
|
+
// Utility panels do not change the VM editing target.
|
|
346
|
+
refreshSurfaces();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
323
349
|
const next = views.get(panel?.id);
|
|
324
350
|
if (active === next) {
|
|
325
351
|
refreshSurfaces();
|
|
@@ -355,7 +381,7 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
355
381
|
}
|
|
356
382
|
function add(target, splitPosition, section) {
|
|
357
383
|
let id = target.id;
|
|
358
|
-
if (splitPosition || views.has(id)) {
|
|
384
|
+
if (splitPosition || views.has(id) || id === stagePanelId || documents.has(id)) {
|
|
359
385
|
do {
|
|
360
386
|
id = 'sprite-view-' + ++nextView;
|
|
361
387
|
} while (views.has(id) || vm.runtime.targets.some(item => item.id === id));
|
|
@@ -394,6 +420,61 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
394
420
|
[...views.values()].find(item => item.target === target);
|
|
395
421
|
activateView(view?.id ?? add(target).id);
|
|
396
422
|
}
|
|
423
|
+
function openDocument(request) {
|
|
424
|
+
const { providerId, documentId } = request.reference;
|
|
425
|
+
for (const [id, { request: opened }] of documents) {
|
|
426
|
+
if (opened.reference.providerId === providerId && opened.reference.documentId === documentId) {
|
|
427
|
+
dockview.getPanel(id)?.api.setActive();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
let id;
|
|
432
|
+
do {
|
|
433
|
+
id = 'document-view-' + ++nextView;
|
|
434
|
+
} while (views.has(id) || documents.has(id) || vm.runtime.targets.some(target => target.id === id));
|
|
435
|
+
const title = request.filename + ' (' + providerId + ' / ' + documentId + ')';
|
|
436
|
+
documents.set(id, { request });
|
|
437
|
+
try {
|
|
438
|
+
dockview.addPanel({ id, title, component: documentComponent, renderer: 'always' });
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
documents.get(id)?.session?.dispose();
|
|
442
|
+
documents.delete(id);
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function guardDocumentClose(event) {
|
|
447
|
+
const element = event.target;
|
|
448
|
+
const tab = element.closest('[data-tab-panel-id]');
|
|
449
|
+
const item = documents.get(tab?.dataset.tabPanelId);
|
|
450
|
+
if (!item)
|
|
451
|
+
return;
|
|
452
|
+
const close = event.type === 'click' ? Boolean(element.closest('.dv-default-tab-action')) :
|
|
453
|
+
event.target === tab && ['Delete', 'Backspace'].includes(event.key);
|
|
454
|
+
if (!close)
|
|
455
|
+
return;
|
|
456
|
+
event.preventDefault();
|
|
457
|
+
event.stopPropagation();
|
|
458
|
+
item.session?.requestClose();
|
|
459
|
+
}
|
|
460
|
+
function canCloseGroup(group) {
|
|
461
|
+
return dockview.groups.length > 1 && group.panels.every(panel => !documents.has(panel.id));
|
|
462
|
+
}
|
|
463
|
+
function openStage() {
|
|
464
|
+
if (!stagePane)
|
|
465
|
+
return;
|
|
466
|
+
const existing = dockview.getPanel(stagePanelId);
|
|
467
|
+
if (existing) {
|
|
468
|
+
existing.api.setActive();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const reference = dockview.activePanel;
|
|
472
|
+
dockview.addPanel({ id: stagePanelId, title: stagePane.title, component: stagePanelId, renderer: 'always',
|
|
473
|
+
position: reference ? { referencePanel: reference.id, direction: 'right' } : undefined });
|
|
474
|
+
}
|
|
475
|
+
function closeStage() {
|
|
476
|
+
dockview.getPanel(stagePanelId)?.api.close();
|
|
477
|
+
}
|
|
397
478
|
function split(direction, id = active?.id) {
|
|
398
479
|
const reference = views.get(id);
|
|
399
480
|
if (!multiView || !reference)
|
|
@@ -405,7 +486,7 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
405
486
|
}
|
|
406
487
|
function closeSplit(id = active?.id) {
|
|
407
488
|
const panel = dockview.getPanel(id);
|
|
408
|
-
if (!multiView || !panel ||
|
|
489
|
+
if (!multiView || !panel || !canCloseGroup(panel.api.group))
|
|
409
490
|
return false;
|
|
410
491
|
syncing = true;
|
|
411
492
|
try {
|
|
@@ -498,6 +579,16 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
498
579
|
}
|
|
499
580
|
});
|
|
500
581
|
const removal = dockview.onDidRemovePanel(panel => {
|
|
582
|
+
const item = documents.get(panel.id);
|
|
583
|
+
if (item) {
|
|
584
|
+
documents.delete(panel.id);
|
|
585
|
+
item.session?.dispose();
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (panel.id === stagePanelId) {
|
|
589
|
+
stagePane?.attach(null);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
501
592
|
const view = views.get(panel.id);
|
|
502
593
|
if (!view)
|
|
503
594
|
return;
|
|
@@ -508,10 +599,13 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
508
599
|
closed.add(view.target);
|
|
509
600
|
});
|
|
510
601
|
const layout = multiView && dockview.onDidLayoutChange(refreshSurfaces);
|
|
602
|
+
const disconnectDocuments = multiView && getDocumentWorkspace(vm).connect(openDocument);
|
|
603
|
+
root.addEventListener('click', guardDocumentClose, true);
|
|
604
|
+
root.addEventListener('keydown', guardDocumentClose, true);
|
|
511
605
|
vm.on('targetsUpdate', refresh);
|
|
512
606
|
refresh();
|
|
513
607
|
return {
|
|
514
|
-
select, split, closeSplit, activateView, setViewSection, syncActiveSection, refreshSurfaces,
|
|
608
|
+
select, openStage, closeStage, split, closeSplit, activateView, setViewSection, syncActiveSection, refreshSurfaces,
|
|
515
609
|
selectAsset(id, kind, index) {
|
|
516
610
|
if (active?.id === id && active.section === (kind === 'costumes' ? 1 : 2)) {
|
|
517
611
|
active.assets.selectors.get(kind)?.(index);
|
|
@@ -520,6 +614,14 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
520
614
|
currentView: () => active && describe(active),
|
|
521
615
|
views: () => [...views.values()].map(view => describe(view)),
|
|
522
616
|
dispose() {
|
|
617
|
+
if (disconnectDocuments)
|
|
618
|
+
disconnectDocuments();
|
|
619
|
+
root.removeEventListener('click', guardDocumentClose, true);
|
|
620
|
+
root.removeEventListener('keydown', guardDocumentClose, true);
|
|
621
|
+
for (const item of documents.values())
|
|
622
|
+
item.session?.dispose();
|
|
623
|
+
documents.clear();
|
|
624
|
+
stagePane?.attach(null);
|
|
523
625
|
renameDialog?.remove();
|
|
524
626
|
renameDialog = undefined;
|
|
525
627
|
vm.removeListener('targetsUpdate', refresh);
|
|
@@ -628,6 +730,7 @@ function SpriteTabs(props) {
|
|
|
628
730
|
resize: () => latest.current.resize(),
|
|
629
731
|
reopenLabel: props.reopenLabel,
|
|
630
732
|
multiView: props.multiView,
|
|
733
|
+
stagePane: props.stagePane,
|
|
631
734
|
variables: hasVariables,
|
|
632
735
|
locale: props.locale,
|
|
633
736
|
viewAdapter: props.viewAdapter,
|
|
@@ -636,14 +739,17 @@ function SpriteTabs(props) {
|
|
|
636
739
|
});
|
|
637
740
|
controller.current = instance;
|
|
638
741
|
latest.current.connect(instance);
|
|
742
|
+
if (props.multiView)
|
|
743
|
+
props.stagePane?.connect({ open: instance.openStage, close: instance.closeStage });
|
|
639
744
|
return () => {
|
|
640
745
|
frame.cancelAnimationFrame(restoreFrame);
|
|
641
746
|
controller.current = null;
|
|
642
747
|
latest.current.connect(null);
|
|
748
|
+
props.stagePane?.connect(null);
|
|
643
749
|
instance.dispose();
|
|
644
750
|
};
|
|
645
751
|
}, [content, props.enabled, props.vm, props.reopenLabel, props.multiView, props.locale,
|
|
646
|
-
props.viewAdapter, props.renameLabels, hasVariables, hasToolbar, hasAssetPreview]);
|
|
752
|
+
props.viewAdapter, props.stagePane, props.renameLabels, hasVariables, hasToolbar, hasAssetPreview]);
|
|
647
753
|
React.useLayoutEffect(() => {
|
|
648
754
|
controller.current?.syncActiveSection(props.activeSection);
|
|
649
755
|
}, [props.activeSection]);
|
package/dist/stage-surface.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Context, ElementType } from 'react';
|
|
2
|
+
import type { useDockviewStage } from './dockview-stage.ts';
|
|
2
3
|
interface StageWindowConnection<Target> {
|
|
3
4
|
ready: Promise<Target>;
|
|
4
5
|
disconnected: AbortSignal;
|
|
@@ -15,6 +16,7 @@ export declare function stageDockActionLabel(locale: string, { drawer, detached,
|
|
|
15
16
|
detached: boolean;
|
|
16
17
|
external?: boolean;
|
|
17
18
|
}): "ドックに戻す" | "Dock stage" | "別窓で開く" | "別ウィンドウで開く" | "Open in a separate window" | "アンドック" | "Undock stage";
|
|
19
|
+
type StagePlacement = Pick<NonNullable<ReturnType<typeof useDockviewStage>>, 'surface' | 'inlineRef' | 'docked'>;
|
|
18
20
|
export declare function createStageSurface(React: typeof import('react'), { context, EdgeDrawer, Panel }: {
|
|
19
21
|
context: Context<{
|
|
20
22
|
locale: string;
|
|
@@ -23,9 +25,8 @@ export declare function createStageSurface(React: typeof import('react'), { cont
|
|
|
23
25
|
}>;
|
|
24
26
|
EdgeDrawer: ElementType;
|
|
25
27
|
Panel: ElementType;
|
|
26
|
-
}): ({ externalHost, onDockActionChange, ...props }: {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
onDockActionChange: any;
|
|
28
|
+
}): ({ externalHost, onDockActionChange, placement, ...props }: {
|
|
29
|
+
placement?: StagePlacement;
|
|
30
|
+
[key: string]: any;
|
|
30
31
|
}) => import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
|
|
31
32
|
export {};
|
package/dist/stage-surface.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createPortal } from 'react-dom';
|
|
1
2
|
import { createDockview, themeLight } from 'dockview';
|
|
2
3
|
export function createStagePopoutHost(host, title) {
|
|
3
4
|
return {
|
|
@@ -58,7 +59,7 @@ export function stageDockActionLabel(locale, { drawer, detached, external }) {
|
|
|
58
59
|
return japanese ? 'アンドック' : 'Undock stage';
|
|
59
60
|
}
|
|
60
61
|
export function createStageSurface(React, { context, EdgeDrawer, Panel }) {
|
|
61
|
-
return function StageSurface({ externalHost, onDockActionChange, ...props }) {
|
|
62
|
+
return function StageSurface({ externalHost, onDockActionChange, placement, ...props }) {
|
|
62
63
|
const { locale, stageDrawer, setStageDrawerOpen } = React.useContext(context);
|
|
63
64
|
const japanese = isJapanese(locale);
|
|
64
65
|
const title = japanese ? 'ステージ' : 'Stage';
|
|
@@ -72,10 +73,16 @@ export function createStageSurface(React, { context, EdgeDrawer, Panel }) {
|
|
|
72
73
|
externalOnly: stageDrawer,
|
|
73
74
|
onDockActionChange: stageDrawer && !host ? undefined : onDockActionChange
|
|
74
75
|
});
|
|
75
|
-
|
|
76
|
+
const inline = placement ? React.createElement('div', {
|
|
77
|
+
ref: placement.inlineRef,
|
|
78
|
+
'data-stage-in-workspace': placement.docked || undefined
|
|
79
|
+
}) : panel;
|
|
80
|
+
const dock = stageDrawer ? React.createElement(EdgeDrawer, {
|
|
76
81
|
edge: 'right',
|
|
77
82
|
label: japanese ? 'ステージを展開' : 'Reveal stage',
|
|
78
|
-
onOpenChange: setStageDrawerOpen
|
|
79
|
-
|
|
83
|
+
onOpenChange: setStageDrawerOpen,
|
|
84
|
+
portalContent: Boolean(placement)
|
|
85
|
+
}, inline) : inline;
|
|
86
|
+
return placement ? React.createElement(React.Fragment, null, dock, createPortal(panel, placement.surface)) : dock;
|
|
80
87
|
};
|
|
81
88
|
}
|
|
@@ -16,11 +16,12 @@ export declare function createStandardLayout(React: any, { styles, targetStyles,
|
|
|
16
16
|
Container: any;
|
|
17
17
|
render: any;
|
|
18
18
|
}) => any;
|
|
19
|
-
EdgeDrawer: ({ edge, label, pinned, onOpenChange, children }: {
|
|
19
|
+
EdgeDrawer: ({ edge, label, pinned, onOpenChange, portalContent, children }: {
|
|
20
20
|
edge: any;
|
|
21
21
|
label: any;
|
|
22
22
|
pinned?: boolean;
|
|
23
23
|
onOpenChange: any;
|
|
24
|
+
portalContent?: boolean;
|
|
24
25
|
children: any;
|
|
25
26
|
}) => any;
|
|
26
27
|
TargetLayout: ({ compact, render }: {
|
package/dist/standard-layout.js
CHANGED
|
@@ -136,30 +136,49 @@ export function createStandardLayout(React, { styles, targetStyles, context, Lay
|
|
|
136
136
|
stageDrawer: plusLayout && stageDrawer, setStageDrawerOpen, locale: props.locale
|
|
137
137
|
} }, Container ? element(Container, { locale: props.locale }, layout) : layout);
|
|
138
138
|
}
|
|
139
|
-
function EdgeDrawer({ edge, label, pinned = false, onOpenChange, children }) {
|
|
139
|
+
function EdgeDrawer({ edge, label, pinned = false, onOpenChange, portalContent = false, children }) {
|
|
140
140
|
const [hovered, setHovered] = useState(false);
|
|
141
141
|
const [engaged, setEngaged] = useState(false);
|
|
142
|
+
const drawer = useRef(null);
|
|
142
143
|
const trigger = useRef(null);
|
|
143
144
|
const panel = useRef(null);
|
|
144
145
|
const open = pinned || hovered || engaged;
|
|
145
146
|
useLayoutEffect(() => { onOpenChange?.(open); }, [open, onOpenChange]);
|
|
147
|
+
const enter = (event) => {
|
|
148
|
+
if (event.pointerType === 'mouse')
|
|
149
|
+
setHovered(true);
|
|
150
|
+
};
|
|
151
|
+
const leave = () => setHovered(false);
|
|
152
|
+
const focus = (event) => {
|
|
153
|
+
if (panel.current.contains(event.target))
|
|
154
|
+
setEngaged(true);
|
|
155
|
+
};
|
|
156
|
+
const blur = (event) => {
|
|
157
|
+
if (!drawer.current.contains(event.relatedTarget))
|
|
158
|
+
setEngaged(false);
|
|
159
|
+
};
|
|
160
|
+
useLayoutEffect(() => {
|
|
161
|
+
if (!portalContent)
|
|
162
|
+
return;
|
|
163
|
+
const node = drawer.current;
|
|
164
|
+
// Use the DOM ancestry for a surface rendered through a portal.
|
|
165
|
+
node.addEventListener('pointerenter', enter);
|
|
166
|
+
node.addEventListener('pointerleave', leave);
|
|
167
|
+
node.addEventListener('focusin', focus);
|
|
168
|
+
node.addEventListener('focusout', blur);
|
|
169
|
+
return () => {
|
|
170
|
+
node.removeEventListener('pointerenter', enter);
|
|
171
|
+
node.removeEventListener('pointerleave', leave);
|
|
172
|
+
node.removeEventListener('focusin', focus);
|
|
173
|
+
node.removeEventListener('focusout', blur);
|
|
174
|
+
};
|
|
175
|
+
}, [portalContent]);
|
|
146
176
|
return element('div', {
|
|
147
177
|
className: styles.edgeDrawer,
|
|
178
|
+
ref: drawer,
|
|
148
179
|
'data-edge': edge,
|
|
149
180
|
'data-pinned': pinned || undefined,
|
|
150
|
-
onPointerEnter:
|
|
151
|
-
if (event.pointerType === 'mouse')
|
|
152
|
-
setHovered(true);
|
|
153
|
-
},
|
|
154
|
-
onPointerLeave: () => setHovered(false),
|
|
155
|
-
onFocus: event => {
|
|
156
|
-
if (panel.current.contains(event.target))
|
|
157
|
-
setEngaged(true);
|
|
158
|
-
},
|
|
159
|
-
onBlur: event => {
|
|
160
|
-
if (!event.currentTarget.contains(event.relatedTarget))
|
|
161
|
-
setEngaged(false);
|
|
162
|
-
},
|
|
181
|
+
...(!portalContent && { onPointerEnter: enter, onPointerLeave: leave, onFocus: focus, onBlur: blur }),
|
|
163
182
|
onKeyDown: event => {
|
|
164
183
|
if (event.key !== 'Escape' || pinned || !open || event.defaultPrevented)
|
|
165
184
|
return;
|
package/package.json
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
type StageCommands = {open(): void; close(): void};
|
|
4
|
+
|
|
5
|
+
export interface DockviewStagePane {
|
|
6
|
+
title: string;
|
|
7
|
+
attach(destination: HTMLElement | null): void;
|
|
8
|
+
connect(commands: StageCommands | null): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useDockviewStage(enabled: boolean, locale: string) {
|
|
12
|
+
const [surface] = React.useState(() => {
|
|
13
|
+
if (!enabled) return null;
|
|
14
|
+
const node = document.createElement('div');
|
|
15
|
+
node.className = 'tw-plus-dockview-stage-surface';
|
|
16
|
+
return node;
|
|
17
|
+
});
|
|
18
|
+
const inline = React.useRef<HTMLElement>(null);
|
|
19
|
+
const dock = React.useRef<HTMLElement>(null);
|
|
20
|
+
const suspended = React.useRef(false);
|
|
21
|
+
const [docked, setDocked] = React.useState(false);
|
|
22
|
+
const [commands, setCommands] = React.useState<StageCommands>(null);
|
|
23
|
+
|
|
24
|
+
const move = React.useCallback(() => {
|
|
25
|
+
const destination = suspended.current ? inline.current : dock.current ?? inline.current;
|
|
26
|
+
if (surface && destination && surface.parentElement !== destination) destination.appendChild(surface);
|
|
27
|
+
}, [surface]);
|
|
28
|
+
const inlineRef = React.useCallback((element: HTMLElement | null) => {
|
|
29
|
+
inline.current = element;
|
|
30
|
+
if (surface && element) surface.dir = element.closest('[dir]')?.getAttribute('dir') ?? '';
|
|
31
|
+
move();
|
|
32
|
+
}, [move, surface]);
|
|
33
|
+
const suspend = React.useCallback((value: boolean) => {
|
|
34
|
+
suspended.current = value;
|
|
35
|
+
move();
|
|
36
|
+
}, [move]);
|
|
37
|
+
|
|
38
|
+
const japanese = /^ja(?:-|$)/i.test(locale);
|
|
39
|
+
const pane = React.useMemo<DockviewStagePane>(() => ({
|
|
40
|
+
title: japanese ? 'ステージ' : 'Stage',
|
|
41
|
+
attach(destination) {
|
|
42
|
+
dock.current = destination;
|
|
43
|
+
move();
|
|
44
|
+
setDocked(Boolean(destination));
|
|
45
|
+
},
|
|
46
|
+
connect: setCommands
|
|
47
|
+
}), [japanese, move]);
|
|
48
|
+
return surface && {
|
|
49
|
+
surface, pane, docked, commands, inlineRef, suspend,
|
|
50
|
+
openLabel: japanese ? '作業領域で開く' : 'Open stage in workspace',
|
|
51
|
+
closeLabel: japanese ? '作業領域から戻す' : 'Return stage from workspace'
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface DocumentPanelRequest {
|
|
2
|
+
reference: {providerId: string; documentId: string};
|
|
3
|
+
filename: string;
|
|
4
|
+
mount(element: HTMLElement, close: () => void): {requestClose(): void; dispose(): void};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function createDocumentWorkspace() {
|
|
8
|
+
let open: ((request: DocumentPanelRequest) => void) | undefined;
|
|
9
|
+
return {
|
|
10
|
+
open(request: DocumentPanelRequest) {
|
|
11
|
+
if (!open) return false;
|
|
12
|
+
open(request);
|
|
13
|
+
return true;
|
|
14
|
+
},
|
|
15
|
+
connect(handler: NonNullable<typeof open>) {
|
|
16
|
+
open = handler;
|
|
17
|
+
return () => { if (open === handler) open = undefined; };
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const workspaces = new WeakMap<object, ReturnType<typeof createDocumentWorkspace>>();
|
|
23
|
+
|
|
24
|
+
export function getDocumentWorkspace(vm: object) {
|
|
25
|
+
let workspace = workspaces.get(vm);
|
|
26
|
+
if (!workspace) {
|
|
27
|
+
workspace = createDocumentWorkspace();
|
|
28
|
+
workspaces.set(vm, workspace);
|
|
29
|
+
}
|
|
30
|
+
return workspace;
|
|
31
|
+
}
|
package/src/editor.css
CHANGED
|
@@ -649,6 +649,10 @@ details.layout-menu button {
|
|
|
649
649
|
height: fit-content;
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
+
.standard-body:has(.sprite-tabs[data-multi-view]) .stage-drawer {
|
|
653
|
+
z-index: var(--plus-docked-utility-layer);
|
|
654
|
+
}
|
|
655
|
+
|
|
652
656
|
.standard-body:has(.sprite-tabs[data-multi-view]):has(.retracted-targets) {
|
|
653
657
|
padding-left: 0.625rem;
|
|
654
658
|
}
|
|
@@ -754,3 +758,26 @@ details.layout-menu button {
|
|
|
754
758
|
min-height: 0;
|
|
755
759
|
min-width: 0;
|
|
756
760
|
}
|
|
761
|
+
|
|
762
|
+
:global(.tw-plus-dockview-stage-slot),
|
|
763
|
+
:global(.tw-plus-dockview-stage-slot) > :global(.tw-plus-dockview-stage-surface) {
|
|
764
|
+
inline-size: 100%;
|
|
765
|
+
block-size: 100%;
|
|
766
|
+
min-inline-size: 0;
|
|
767
|
+
min-block-size: 0;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
:global(.tw-plus-dockview-stage-slot) > :global(.tw-plus-dockview-stage-surface) > .basic-panel {
|
|
771
|
+
block-size: 100%;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.floating-stage:has([data-stage-in-workspace]:empty) {
|
|
775
|
+
pointer-events: none;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
:global(.tw-plus-document-slot) {
|
|
779
|
+
inline-size: 100%;
|
|
780
|
+
block-size: 100%;
|
|
781
|
+
min-inline-size: 0;
|
|
782
|
+
min-block-size: 0;
|
|
783
|
+
}
|
|
@@ -3,6 +3,7 @@ interface SpriteLayoutCommands {
|
|
|
3
3
|
splitBelow(): void;
|
|
4
4
|
close(): void;
|
|
5
5
|
canClose(): boolean;
|
|
6
|
+
canSplit?(): boolean;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
const labels = {
|
|
@@ -72,6 +73,7 @@ export function createSpriteLayoutMenu(document: Document, locale: string, comma
|
|
|
72
73
|
function open(last = false) {
|
|
73
74
|
if (openedIn) return;
|
|
74
75
|
openedIn = trigger.ownerDocument;
|
|
76
|
+
buttons[0].disabled = buttons[1].disabled = commands.canSplit?.() === false;
|
|
75
77
|
buttons[2].disabled = !commands.canClose();
|
|
76
78
|
topLayer = typeof popup.showPopover === 'function';
|
|
77
79
|
if (topLayer) {
|
package/src/sprite-tabs.ts
CHANGED
|
@@ -7,6 +7,13 @@ import {SpriteAssetPane} from './sprite-asset-pane.tsx';
|
|
|
7
7
|
import type {SpriteVariable} from './sprite-asset-pane.tsx';
|
|
8
8
|
import {SpriteEditorToolbar, spriteEditorSections} from './sprite-editor-toolbar.tsx';
|
|
9
9
|
import type {SpriteSearchResult} from './sprite-editor-toolbar.tsx';
|
|
10
|
+
import type {DockviewStagePane} from './dockview-stage.ts';
|
|
11
|
+
import {getDocumentWorkspace} from './document-workspace.ts';
|
|
12
|
+
import type {DocumentPanelRequest} from './document-workspace.ts';
|
|
13
|
+
|
|
14
|
+
export {getDocumentWorkspace} from './document-workspace.ts';
|
|
15
|
+
|
|
16
|
+
export {useDockviewStage} from './dockview-stage.ts';
|
|
10
17
|
|
|
11
18
|
interface CostumeAsset {
|
|
12
19
|
encodeDataURI(): string;
|
|
@@ -111,6 +118,9 @@ function createAssetSelections(target: Target, source?: {
|
|
|
111
118
|
|
|
112
119
|
export type SpriteSplitDirection = 'right' | 'below';
|
|
113
120
|
|
|
121
|
+
const stagePanelId = 'tw-plus-stage-display';
|
|
122
|
+
const documentComponent = 'tw-plus-document';
|
|
123
|
+
|
|
114
124
|
interface TabOptions<Snapshot = unknown> {
|
|
115
125
|
vm: Project;
|
|
116
126
|
getSection(): number;
|
|
@@ -119,6 +129,7 @@ interface TabOptions<Snapshot = unknown> {
|
|
|
119
129
|
resize(): void;
|
|
120
130
|
reopenLabel: string;
|
|
121
131
|
multiView?: boolean;
|
|
132
|
+
stagePane?: DockviewStagePane;
|
|
122
133
|
variables?: boolean;
|
|
123
134
|
locale?: string;
|
|
124
135
|
viewAdapter?: SpriteTabAdapter<Snapshot>;
|
|
@@ -198,11 +209,13 @@ class SpriteTab extends DefaultTab {
|
|
|
198
209
|
export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElement, options: TabOptions<Snapshot>) {
|
|
199
210
|
const {vm, multiView = false, locale = 'en'} = options;
|
|
200
211
|
const viewAdapter = multiView ? options.viewAdapter : undefined;
|
|
212
|
+
const stagePane = multiView ? options.stagePane : undefined;
|
|
201
213
|
const usesNativeSection = (section: number) => section !== variableSection || !options.variables;
|
|
202
214
|
const document = root.ownerDocument;
|
|
203
215
|
const editLabel = /^ja(?:-|$)/i.test(locale) ? 'クリックしてこのビューで編集' : 'Activate this view to edit';
|
|
204
216
|
const slots = new Map<string, TabSlot>();
|
|
205
217
|
const views = new Map<string, View<Snapshot>>();
|
|
218
|
+
const documents = new Map<string, {request: DocumentPanelRequest; session?: ReturnType<DocumentPanelRequest['mount']>}>();
|
|
206
219
|
const costumeIcons = new WeakMap<CostumeAsset, string>();
|
|
207
220
|
const remembered = new WeakMap<Target, number>();
|
|
208
221
|
const lastUsed = new WeakMap<Target, View<Snapshot>>();
|
|
@@ -225,8 +238,21 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
225
238
|
if (!target || target.isStage || !vm.renameSprite) return [];
|
|
226
239
|
return [{label: text.title, action: () => openRenameDialog(target)}];
|
|
227
240
|
},
|
|
228
|
-
createComponent: () => {
|
|
241
|
+
createComponent: ({name, id: componentId}) => {
|
|
229
242
|
const element = document.createElement('div');
|
|
243
|
+
if (name === documentComponent) {
|
|
244
|
+
element.className = 'tw-plus-document-slot';
|
|
245
|
+
return {element, init({api}) {
|
|
246
|
+
const item = documents.get(componentId);
|
|
247
|
+
element.dataset.providerId = item.request.reference.providerId;
|
|
248
|
+
element.dataset.documentId = item.request.reference.documentId;
|
|
249
|
+
item.session = item.request.mount(element, () => api.close());
|
|
250
|
+
}};
|
|
251
|
+
}
|
|
252
|
+
if (name === stagePanelId) {
|
|
253
|
+
element.className = 'tw-plus-dockview-stage-slot';
|
|
254
|
+
return {element, init() { stagePane.attach(element); }};
|
|
255
|
+
}
|
|
230
256
|
element.className = 'tw-plus-sprite-tab-slot';
|
|
231
257
|
const body = multiView ? document.createElement('div') : element;
|
|
232
258
|
const toolbar = multiView && options.onToolbarSlots ? document.createElement('div') : undefined;
|
|
@@ -301,7 +327,8 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
301
327
|
splitRight: () => { if (group.activePanel) split('right', group.activePanel.id); },
|
|
302
328
|
splitBelow: () => { if (group.activePanel) split('below', group.activePanel.id); },
|
|
303
329
|
close: () => { if (group.activePanel) closeSplit(group.activePanel.id); },
|
|
304
|
-
canClose: () =>
|
|
330
|
+
canClose: () => canCloseGroup(group),
|
|
331
|
+
canSplit: () => views.has(group.activePanel?.id)
|
|
305
332
|
});
|
|
306
333
|
return {...menu, init() {}};
|
|
307
334
|
}
|
|
@@ -432,6 +459,11 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
432
459
|
}
|
|
433
460
|
|
|
434
461
|
function show(panel: IDockviewPanel | undefined) {
|
|
462
|
+
if (panel?.id === stagePanelId || documents.has(panel?.id)) {
|
|
463
|
+
// Utility panels do not change the VM editing target.
|
|
464
|
+
refreshSurfaces();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
435
467
|
const next = views.get(panel?.id);
|
|
436
468
|
if (active === next) {
|
|
437
469
|
refreshSurfaces();
|
|
@@ -461,7 +493,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
461
493
|
|
|
462
494
|
function add(target: Target, splitPosition?: {referencePanel: string; direction: SpriteSplitDirection}, section?: number) {
|
|
463
495
|
let id = target.id;
|
|
464
|
-
if (splitPosition || views.has(id)) {
|
|
496
|
+
if (splitPosition || views.has(id) || id === stagePanelId || documents.has(id)) {
|
|
465
497
|
do {
|
|
466
498
|
id = 'sprite-view-' + ++nextView;
|
|
467
499
|
} while (views.has(id) || vm.runtime.targets.some(item => item.id === id));
|
|
@@ -500,6 +532,62 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
500
532
|
activateView(view?.id ?? add(target).id);
|
|
501
533
|
}
|
|
502
534
|
|
|
535
|
+
function openDocument(request: DocumentPanelRequest) {
|
|
536
|
+
const {providerId, documentId} = request.reference;
|
|
537
|
+
for (const [id, {request: opened}] of documents) {
|
|
538
|
+
if (opened.reference.providerId === providerId && opened.reference.documentId === documentId) {
|
|
539
|
+
dockview.getPanel(id)?.api.setActive();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
let id: string;
|
|
544
|
+
do {
|
|
545
|
+
id = 'document-view-' + ++nextView;
|
|
546
|
+
} while (views.has(id) || documents.has(id) || vm.runtime.targets.some(target => target.id === id));
|
|
547
|
+
const title = request.filename + ' (' + providerId + ' / ' + documentId + ')';
|
|
548
|
+
documents.set(id, {request});
|
|
549
|
+
try {
|
|
550
|
+
dockview.addPanel({id, title, component: documentComponent, renderer: 'always'});
|
|
551
|
+
} catch (error) {
|
|
552
|
+
documents.get(id)?.session?.dispose();
|
|
553
|
+
documents.delete(id);
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function guardDocumentClose(event: MouseEvent | KeyboardEvent) {
|
|
559
|
+
const element = event.target as HTMLElement;
|
|
560
|
+
const tab = element.closest<HTMLElement>('[data-tab-panel-id]');
|
|
561
|
+
const item = documents.get(tab?.dataset.tabPanelId);
|
|
562
|
+
if (!item) return;
|
|
563
|
+
const close = event.type === 'click' ? Boolean(element.closest('.dv-default-tab-action')) :
|
|
564
|
+
event.target === tab && ['Delete', 'Backspace'].includes((event as KeyboardEvent).key);
|
|
565
|
+
if (!close) return;
|
|
566
|
+
event.preventDefault();
|
|
567
|
+
event.stopPropagation();
|
|
568
|
+
item.session?.requestClose();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function canCloseGroup(group: DockviewGroupPanel) {
|
|
572
|
+
return dockview.groups.length > 1 && group.panels.every(panel => !documents.has(panel.id));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function openStage() {
|
|
576
|
+
if (!stagePane) return;
|
|
577
|
+
const existing = dockview.getPanel(stagePanelId);
|
|
578
|
+
if (existing) {
|
|
579
|
+
existing.api.setActive();
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const reference = dockview.activePanel;
|
|
583
|
+
dockview.addPanel({id: stagePanelId, title: stagePane.title, component: stagePanelId, renderer: 'always',
|
|
584
|
+
position: reference ? {referencePanel: reference.id, direction: 'right'} : undefined});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function closeStage() {
|
|
588
|
+
dockview.getPanel(stagePanelId)?.api.close();
|
|
589
|
+
}
|
|
590
|
+
|
|
503
591
|
function split(direction: SpriteSplitDirection, id = active?.id) {
|
|
504
592
|
const reference = views.get(id);
|
|
505
593
|
if (!multiView || !reference) return;
|
|
@@ -511,7 +599,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
511
599
|
|
|
512
600
|
function closeSplit(id = active?.id) {
|
|
513
601
|
const panel = dockview.getPanel(id);
|
|
514
|
-
if (!multiView || !panel ||
|
|
602
|
+
if (!multiView || !panel || !canCloseGroup(panel.api.group)) return false;
|
|
515
603
|
syncing = true;
|
|
516
604
|
try {
|
|
517
605
|
dockview.removeGroup(panel.api.group);
|
|
@@ -595,6 +683,16 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
595
683
|
}
|
|
596
684
|
});
|
|
597
685
|
const removal = dockview.onDidRemovePanel(panel => {
|
|
686
|
+
const item = documents.get(panel.id);
|
|
687
|
+
if (item) {
|
|
688
|
+
documents.delete(panel.id);
|
|
689
|
+
item.session?.dispose();
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (panel.id === stagePanelId) {
|
|
693
|
+
stagePane?.attach(null);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
598
696
|
const view = views.get(panel.id);
|
|
599
697
|
if (!view) return;
|
|
600
698
|
if (view === active) remembered.set(view.target, usesNativeSection(view.section) ? options.getSection() : view.section);
|
|
@@ -602,10 +700,13 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
602
700
|
if (![...views.values()].some(item => item.target === view.target)) closed.add(view.target);
|
|
603
701
|
});
|
|
604
702
|
const layout = multiView && dockview.onDidLayoutChange(refreshSurfaces);
|
|
703
|
+
const disconnectDocuments = multiView && getDocumentWorkspace(vm).connect(openDocument);
|
|
704
|
+
root.addEventListener('click', guardDocumentClose, true);
|
|
705
|
+
root.addEventListener('keydown', guardDocumentClose, true);
|
|
605
706
|
vm.on('targetsUpdate', refresh);
|
|
606
707
|
refresh();
|
|
607
708
|
return {
|
|
608
|
-
select, split, closeSplit, activateView, setViewSection, syncActiveSection, refreshSurfaces,
|
|
709
|
+
select, openStage, closeStage, split, closeSplit, activateView, setViewSection, syncActiveSection, refreshSurfaces,
|
|
609
710
|
selectAsset(id: string, kind: AssetKind, index: number) {
|
|
610
711
|
if (active?.id === id && active.section === (kind === 'costumes' ? 1 : 2)) {
|
|
611
712
|
active.assets.selectors.get(kind)?.(index);
|
|
@@ -614,6 +715,12 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
614
715
|
currentView: () => active && describe(active),
|
|
615
716
|
views: () => [...views.values()].map(view => describe(view)),
|
|
616
717
|
dispose() {
|
|
718
|
+
if (disconnectDocuments) disconnectDocuments();
|
|
719
|
+
root.removeEventListener('click', guardDocumentClose, true);
|
|
720
|
+
root.removeEventListener('keydown', guardDocumentClose, true);
|
|
721
|
+
for (const item of documents.values()) item.session?.dispose();
|
|
722
|
+
documents.clear();
|
|
723
|
+
stagePane?.attach(null);
|
|
617
724
|
renameDialog?.remove();
|
|
618
725
|
renameDialog = undefined;
|
|
619
726
|
vm.removeListener('targetsUpdate', refresh);
|
|
@@ -746,6 +853,7 @@ function SpriteTabs<Snapshot>(props: SpriteTabsProps<Snapshot>) {
|
|
|
746
853
|
resize: () => latest.current.resize(),
|
|
747
854
|
reopenLabel: props.reopenLabel,
|
|
748
855
|
multiView: props.multiView,
|
|
856
|
+
stagePane: props.stagePane,
|
|
749
857
|
variables: hasVariables,
|
|
750
858
|
locale: props.locale,
|
|
751
859
|
viewAdapter: props.viewAdapter,
|
|
@@ -754,14 +862,16 @@ function SpriteTabs<Snapshot>(props: SpriteTabsProps<Snapshot>) {
|
|
|
754
862
|
});
|
|
755
863
|
controller.current = instance;
|
|
756
864
|
latest.current.connect(instance);
|
|
865
|
+
if (props.multiView) props.stagePane?.connect({open: instance.openStage, close: instance.closeStage});
|
|
757
866
|
return () => {
|
|
758
867
|
frame.cancelAnimationFrame(restoreFrame);
|
|
759
868
|
controller.current = null;
|
|
760
869
|
latest.current.connect(null);
|
|
870
|
+
props.stagePane?.connect(null);
|
|
761
871
|
instance.dispose();
|
|
762
872
|
};
|
|
763
873
|
}, [content, props.enabled, props.vm, props.reopenLabel, props.multiView, props.locale,
|
|
764
|
-
props.viewAdapter, props.renameLabels, hasVariables, hasToolbar, hasAssetPreview]);
|
|
874
|
+
props.viewAdapter, props.stagePane, props.renameLabels, hasVariables, hasToolbar, hasAssetPreview]);
|
|
765
875
|
React.useLayoutEffect(() => {
|
|
766
876
|
controller.current?.syncActiveSection(props.activeSection);
|
|
767
877
|
}, [props.activeSection]);
|
package/src/stage-surface.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type {Context, ElementType} from 'react';
|
|
2
|
+
import {createPortal} from 'react-dom';
|
|
3
|
+
import type {useDockviewStage} from './dockview-stage.ts';
|
|
2
4
|
import {createDockview, themeLight} from 'dockview';
|
|
3
5
|
|
|
4
6
|
interface StageWindowConnection<Target> {
|
|
@@ -74,6 +76,8 @@ export function stageDockActionLabel(locale: string, {drawer, detached, external
|
|
|
74
76
|
return japanese ? 'アンドック' : 'Undock stage';
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
type StagePlacement = Pick<NonNullable<ReturnType<typeof useDockviewStage>>, 'surface' | 'inlineRef' | 'docked'>;
|
|
80
|
+
|
|
77
81
|
export function createStageSurface(React: typeof import('react'), {
|
|
78
82
|
context, EdgeDrawer, Panel
|
|
79
83
|
}: {
|
|
@@ -81,7 +85,7 @@ export function createStageSurface(React: typeof import('react'), {
|
|
|
81
85
|
EdgeDrawer: ElementType;
|
|
82
86
|
Panel: ElementType;
|
|
83
87
|
}) {
|
|
84
|
-
return function StageSurface({externalHost, onDockActionChange, ...props}) {
|
|
88
|
+
return function StageSurface({externalHost, onDockActionChange, placement, ...props}: {placement?: StagePlacement; [key: string]: any}) {
|
|
85
89
|
const {locale, stageDrawer, setStageDrawerOpen} = React.useContext(context);
|
|
86
90
|
const japanese = isJapanese(locale);
|
|
87
91
|
const title = japanese ? 'ステージ' : 'Stage';
|
|
@@ -95,10 +99,16 @@ export function createStageSurface(React: typeof import('react'), {
|
|
|
95
99
|
externalOnly: stageDrawer,
|
|
96
100
|
onDockActionChange: stageDrawer && !host ? undefined : onDockActionChange
|
|
97
101
|
});
|
|
98
|
-
|
|
102
|
+
const inline = placement ? React.createElement('div', {
|
|
103
|
+
ref: placement.inlineRef,
|
|
104
|
+
'data-stage-in-workspace': placement.docked || undefined
|
|
105
|
+
}) : panel;
|
|
106
|
+
const dock = stageDrawer ? React.createElement(EdgeDrawer, {
|
|
99
107
|
edge: 'right',
|
|
100
108
|
label: japanese ? 'ステージを展開' : 'Reveal stage',
|
|
101
|
-
onOpenChange: setStageDrawerOpen
|
|
102
|
-
|
|
109
|
+
onOpenChange: setStageDrawerOpen,
|
|
110
|
+
portalContent: Boolean(placement)
|
|
111
|
+
}, inline) : inline;
|
|
112
|
+
return placement ? React.createElement(React.Fragment, null, dock, createPortal(panel, placement.surface)) : dock;
|
|
103
113
|
};
|
|
104
114
|
}
|
package/src/standard-layout.ts
CHANGED
|
@@ -151,29 +151,49 @@ export function createStandardLayout(React, {styles, targetStyles, context, Layo
|
|
|
151
151
|
Container ? element(Container, {locale: props.locale}, layout) : layout);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
function EdgeDrawer({edge, label, pinned = false, onOpenChange, children}) {
|
|
154
|
+
function EdgeDrawer({edge, label, pinned = false, onOpenChange, portalContent = false, children}) {
|
|
155
155
|
const [hovered, setHovered] = useState(false);
|
|
156
156
|
const [engaged, setEngaged] = useState(false);
|
|
157
|
+
const drawer = useRef(null);
|
|
157
158
|
const trigger = useRef(null);
|
|
158
159
|
const panel = useRef(null);
|
|
159
160
|
const open = pinned || hovered || engaged;
|
|
160
161
|
|
|
161
162
|
useLayoutEffect(() => {onOpenChange?.(open);}, [open, onOpenChange]);
|
|
162
163
|
|
|
164
|
+
const enter = (event: PointerEvent) => {
|
|
165
|
+
if (event.pointerType === 'mouse') setHovered(true);
|
|
166
|
+
};
|
|
167
|
+
const leave = () => setHovered(false);
|
|
168
|
+
const focus = (event: FocusEvent) => {
|
|
169
|
+
if (panel.current.contains(event.target as Node)) setEngaged(true);
|
|
170
|
+
};
|
|
171
|
+
const blur = (event: FocusEvent) => {
|
|
172
|
+
if (!drawer.current.contains(event.relatedTarget as Node)) setEngaged(false);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
useLayoutEffect(() => {
|
|
176
|
+
if (!portalContent) return;
|
|
177
|
+
const node = drawer.current as HTMLElement;
|
|
178
|
+
// Use the DOM ancestry for a surface rendered through a portal.
|
|
179
|
+
node.addEventListener('pointerenter', enter);
|
|
180
|
+
node.addEventListener('pointerleave', leave);
|
|
181
|
+
node.addEventListener('focusin', focus);
|
|
182
|
+
node.addEventListener('focusout', blur);
|
|
183
|
+
return () => {
|
|
184
|
+
node.removeEventListener('pointerenter', enter);
|
|
185
|
+
node.removeEventListener('pointerleave', leave);
|
|
186
|
+
node.removeEventListener('focusin', focus);
|
|
187
|
+
node.removeEventListener('focusout', blur);
|
|
188
|
+
};
|
|
189
|
+
}, [portalContent]);
|
|
190
|
+
|
|
163
191
|
return element('div', {
|
|
164
192
|
className: styles.edgeDrawer,
|
|
193
|
+
ref: drawer,
|
|
165
194
|
'data-edge': edge,
|
|
166
195
|
'data-pinned': pinned || undefined,
|
|
167
|
-
onPointerEnter:
|
|
168
|
-
if (event.pointerType === 'mouse') setHovered(true);
|
|
169
|
-
},
|
|
170
|
-
onPointerLeave: () => setHovered(false),
|
|
171
|
-
onFocus: event => {
|
|
172
|
-
if (panel.current.contains(event.target)) setEngaged(true);
|
|
173
|
-
},
|
|
174
|
-
onBlur: event => {
|
|
175
|
-
if (!event.currentTarget.contains(event.relatedTarget)) setEngaged(false);
|
|
176
|
-
},
|
|
196
|
+
...(!portalContent && {onPointerEnter: enter, onPointerLeave: leave, onFocus: focus, onBlur: blur}),
|
|
177
197
|
onKeyDown: event => {
|
|
178
198
|
if (event.key !== 'Escape' || pinned || !open || event.defaultPrevented) return;
|
|
179
199
|
setHovered(false);
|