@kubohiroya/turbowarp-plus-editor-layout 0.1.4 → 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.
@@ -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
+ }
@@ -2,6 +2,7 @@ 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
4
  import type { DockviewStagePane } from './dockview-stage.ts';
5
+ export { getDocumentWorkspace } from './document-workspace.ts';
5
6
  export { useDockviewStage } from './dockview-stage.ts';
6
7
  interface CostumeAsset {
7
8
  encodeDataURI(): string;
@@ -4,6 +4,8 @@ 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";
7
9
  export { useDockviewStage } from "./dockview-stage.js";
8
10
  const variableSection = spriteEditorSections.indexOf('variables');
9
11
  function createAssetSelections(target, source) {
@@ -41,6 +43,7 @@ function createAssetSelections(target, source) {
41
43
  return { selected, ports, layouts, selectors, scroll };
42
44
  }
43
45
  const stagePanelId = 'tw-plus-stage-display';
46
+ const documentComponent = 'tw-plus-document';
44
47
  const renameLabels = {
45
48
  en: { title: 'Rename sprite', name: 'Sprite name', cancel: 'Cancel', save: 'Rename' },
46
49
  ja: { title: 'スプライト名の変更', name: 'スプライト名', cancel: 'キャンセル', save: '変更' }
@@ -76,6 +79,7 @@ export function mountSpriteTabs(root, content, options) {
76
79
  const editLabel = /^ja(?:-|$)/i.test(locale) ? 'クリックしてこのビューで編集' : 'Activate this view to edit';
77
80
  const slots = new Map();
78
81
  const views = new Map();
82
+ const documents = new Map();
79
83
  const costumeIcons = new WeakMap();
80
84
  const remembered = new WeakMap();
81
85
  const lastUsed = new WeakMap();
@@ -98,8 +102,17 @@ export function mountSpriteTabs(root, content, options) {
98
102
  return [];
99
103
  return [{ label: text.title, action: () => openRenameDialog(target) }];
100
104
  },
101
- createComponent: ({ name }) => {
105
+ createComponent: ({ name, id: componentId }) => {
102
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
+ }
103
116
  if (name === stagePanelId) {
104
117
  element.className = 'tw-plus-dockview-stage-slot';
105
118
  return { element, init() { stagePane.attach(element); } };
@@ -189,7 +202,7 @@ export function mountSpriteTabs(root, content, options) {
189
202
  split('below', group.activePanel.id); },
190
203
  close: () => { if (group.activePanel)
191
204
  closeSplit(group.activePanel.id); },
192
- canClose: () => dockview.groups.length > 1,
205
+ canClose: () => canCloseGroup(group),
193
206
  canSplit: () => views.has(group.activePanel?.id)
194
207
  });
195
208
  return { ...menu, init() { } };
@@ -328,8 +341,8 @@ export function mountSpriteTabs(root, content, options) {
328
341
  view.snapshots.set(view.section, { snapshot, view: descriptor });
329
342
  }
330
343
  function show(panel) {
331
- if (panel?.id === stagePanelId) {
332
- // Displaying the Stage does not select the Stage target or interrupt the sprite editor.
344
+ if (panel?.id === stagePanelId || documents.has(panel?.id)) {
345
+ // Utility panels do not change the VM editing target.
333
346
  refreshSurfaces();
334
347
  return;
335
348
  }
@@ -368,7 +381,7 @@ export function mountSpriteTabs(root, content, options) {
368
381
  }
369
382
  function add(target, splitPosition, section) {
370
383
  let id = target.id;
371
- if (splitPosition || views.has(id) || id === stagePanelId) {
384
+ if (splitPosition || views.has(id) || id === stagePanelId || documents.has(id)) {
372
385
  do {
373
386
  id = 'sprite-view-' + ++nextView;
374
387
  } while (views.has(id) || vm.runtime.targets.some(item => item.id === id));
@@ -407,6 +420,46 @@ export function mountSpriteTabs(root, content, options) {
407
420
  [...views.values()].find(item => item.target === target);
408
421
  activateView(view?.id ?? add(target).id);
409
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
+ }
410
463
  function openStage() {
411
464
  if (!stagePane)
412
465
  return;
@@ -433,7 +486,7 @@ export function mountSpriteTabs(root, content, options) {
433
486
  }
434
487
  function closeSplit(id = active?.id) {
435
488
  const panel = dockview.getPanel(id);
436
- if (!multiView || !panel || dockview.groups.length <= 1)
489
+ if (!multiView || !panel || !canCloseGroup(panel.api.group))
437
490
  return false;
438
491
  syncing = true;
439
492
  try {
@@ -526,6 +579,12 @@ export function mountSpriteTabs(root, content, options) {
526
579
  }
527
580
  });
528
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
+ }
529
588
  if (panel.id === stagePanelId) {
530
589
  stagePane?.attach(null);
531
590
  return;
@@ -540,6 +599,9 @@ export function mountSpriteTabs(root, content, options) {
540
599
  closed.add(view.target);
541
600
  });
542
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);
543
605
  vm.on('targetsUpdate', refresh);
544
606
  refresh();
545
607
  return {
@@ -552,6 +614,13 @@ export function mountSpriteTabs(root, content, options) {
552
614
  currentView: () => active && describe(active),
553
615
  views: () => [...views.values()].map(view => describe(view)),
554
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();
555
624
  stagePane?.attach(null);
556
625
  renameDialog?.remove();
557
626
  renameDialog = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubohiroya/turbowarp-plus-editor-layout",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Resizable panes and layout for TurboWarp Plus Editor",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -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
@@ -774,3 +774,10 @@ details.layout-menu button {
774
774
  .floating-stage:has([data-stage-in-workspace]:empty) {
775
775
  pointer-events: none;
776
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
+ }
@@ -8,6 +8,10 @@ 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
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';
11
15
 
12
16
  export {useDockviewStage} from './dockview-stage.ts';
13
17
 
@@ -115,6 +119,7 @@ function createAssetSelections(target: Target, source?: {
115
119
  export type SpriteSplitDirection = 'right' | 'below';
116
120
 
117
121
  const stagePanelId = 'tw-plus-stage-display';
122
+ const documentComponent = 'tw-plus-document';
118
123
 
119
124
  interface TabOptions<Snapshot = unknown> {
120
125
  vm: Project;
@@ -210,6 +215,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
210
215
  const editLabel = /^ja(?:-|$)/i.test(locale) ? 'クリックしてこのビューで編集' : 'Activate this view to edit';
211
216
  const slots = new Map<string, TabSlot>();
212
217
  const views = new Map<string, View<Snapshot>>();
218
+ const documents = new Map<string, {request: DocumentPanelRequest; session?: ReturnType<DocumentPanelRequest['mount']>}>();
213
219
  const costumeIcons = new WeakMap<CostumeAsset, string>();
214
220
  const remembered = new WeakMap<Target, number>();
215
221
  const lastUsed = new WeakMap<Target, View<Snapshot>>();
@@ -232,8 +238,17 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
232
238
  if (!target || target.isStage || !vm.renameSprite) return [];
233
239
  return [{label: text.title, action: () => openRenameDialog(target)}];
234
240
  },
235
- createComponent: ({name}) => {
241
+ createComponent: ({name, id: componentId}) => {
236
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
+ }
237
252
  if (name === stagePanelId) {
238
253
  element.className = 'tw-plus-dockview-stage-slot';
239
254
  return {element, init() { stagePane.attach(element); }};
@@ -312,7 +327,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
312
327
  splitRight: () => { if (group.activePanel) split('right', group.activePanel.id); },
313
328
  splitBelow: () => { if (group.activePanel) split('below', group.activePanel.id); },
314
329
  close: () => { if (group.activePanel) closeSplit(group.activePanel.id); },
315
- canClose: () => dockview.groups.length > 1,
330
+ canClose: () => canCloseGroup(group),
316
331
  canSplit: () => views.has(group.activePanel?.id)
317
332
  });
318
333
  return {...menu, init() {}};
@@ -444,8 +459,8 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
444
459
  }
445
460
 
446
461
  function show(panel: IDockviewPanel | undefined) {
447
- if (panel?.id === stagePanelId) {
448
- // Displaying the Stage does not select the Stage target or interrupt the sprite editor.
462
+ if (panel?.id === stagePanelId || documents.has(panel?.id)) {
463
+ // Utility panels do not change the VM editing target.
449
464
  refreshSurfaces();
450
465
  return;
451
466
  }
@@ -478,7 +493,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
478
493
 
479
494
  function add(target: Target, splitPosition?: {referencePanel: string; direction: SpriteSplitDirection}, section?: number) {
480
495
  let id = target.id;
481
- if (splitPosition || views.has(id) || id === stagePanelId) {
496
+ if (splitPosition || views.has(id) || id === stagePanelId || documents.has(id)) {
482
497
  do {
483
498
  id = 'sprite-view-' + ++nextView;
484
499
  } while (views.has(id) || vm.runtime.targets.some(item => item.id === id));
@@ -517,6 +532,46 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
517
532
  activateView(view?.id ?? add(target).id);
518
533
  }
519
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
+
520
575
  function openStage() {
521
576
  if (!stagePane) return;
522
577
  const existing = dockview.getPanel(stagePanelId);
@@ -544,7 +599,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
544
599
 
545
600
  function closeSplit(id = active?.id) {
546
601
  const panel = dockview.getPanel(id);
547
- if (!multiView || !panel || dockview.groups.length <= 1) return false;
602
+ if (!multiView || !panel || !canCloseGroup(panel.api.group)) return false;
548
603
  syncing = true;
549
604
  try {
550
605
  dockview.removeGroup(panel.api.group);
@@ -628,6 +683,12 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
628
683
  }
629
684
  });
630
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
+ }
631
692
  if (panel.id === stagePanelId) {
632
693
  stagePane?.attach(null);
633
694
  return;
@@ -639,6 +700,9 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
639
700
  if (![...views.values()].some(item => item.target === view.target)) closed.add(view.target);
640
701
  });
641
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);
642
706
  vm.on('targetsUpdate', refresh);
643
707
  refresh();
644
708
  return {
@@ -651,6 +715,11 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
651
715
  currentView: () => active && describe(active),
652
716
  views: () => [...views.values()].map(view => describe(view)),
653
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();
654
723
  stagePane?.attach(null);
655
724
  renameDialog?.remove();
656
725
  renameDialog = undefined;