@jbrowse/app-core 4.3.0 → 5.0.0-beta.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/esm/AppSession/AppRootModel.d.ts +30 -0
- package/esm/AppSession/AppRootModel.js +1 -0
- package/esm/AppSession/AppSessionMixin.d.ts +1598 -0
- package/esm/AppSession/AppSessionMixin.js +62 -0
- package/esm/AppSession/index.d.ts +2 -0
- package/esm/AppSession/index.js +1 -0
- package/esm/Assemblies/AssembliesMixin.d.ts +225 -0
- package/esm/Assemblies/AssembliesMixin.js +149 -0
- package/esm/Assemblies/index.d.ts +1 -2
- package/esm/Assemblies/index.js +1 -2
- package/esm/HistoryManagement/index.d.ts +19 -0
- package/esm/HistoryManagement/index.js +42 -5
- package/esm/JBrowseConfig/RootConfiguration.d.ts +167 -78
- package/esm/JBrowseConfig/RootConfiguration.js +85 -7
- package/esm/JBrowseConfig/index.d.ts +169 -84
- package/esm/JBrowseConfig/index.js +62 -2
- package/esm/JBrowseModel/index.d.ts +176 -95
- package/esm/JBrowseModel/index.js +122 -21
- package/esm/RootMenu/index.d.ts +77 -2
- package/esm/RootMenu/index.js +107 -61
- package/esm/SessionSpec/index.d.ts +6 -0
- package/esm/SessionSpec/index.js +3 -0
- package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
- package/esm/SessionSpec/lgvUrlInit.js +158 -0
- package/esm/SessionSpec/loadSessionSpec.d.ts +21 -0
- package/esm/SessionSpec/loadSessionSpec.js +311 -0
- package/esm/SessionSpec/parseSessionSpecUrl.d.ts +13 -0
- package/esm/SessionSpec/parseSessionSpecUrl.js +196 -0
- package/esm/SessionSpec/types.d.ts +33 -0
- package/esm/SessionSpec/types.js +1 -0
- package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
- package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
- package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
- package/esm/WorkspaceLayout/PanelView.js +99 -0
- package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
- package/esm/WorkspaceLayout/TabStrip.js +226 -0
- package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
- package/esm/WorkspaceLayout/WorkspaceContainer.js +105 -0
- package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
- package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
- package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
- package/esm/WorkspaceLayout/WorkspaceTab.js +109 -0
- package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
- package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
- package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
- package/esm/WorkspaceLayout/dropZone.js +113 -0
- package/esm/WorkspaceLayout/index.d.ts +8 -0
- package/esm/WorkspaceLayout/index.js +4 -0
- package/esm/WorkspaceLayout/model.d.ts +240 -0
- package/esm/WorkspaceLayout/model.js +527 -0
- package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
- package/esm/WorkspaceLayout/panelChrome.js +4 -0
- package/esm/WorkspaceLayout/spec.d.ts +58 -0
- package/esm/WorkspaceLayout/spec.js +153 -0
- package/esm/WorkspaceLayout/splitter.d.ts +25 -0
- package/esm/WorkspaceLayout/splitter.js +37 -0
- package/esm/WorkspaceLayout/tree.d.ts +158 -0
- package/esm/WorkspaceLayout/tree.js +379 -0
- package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
- package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
- package/esm/index.d.ts +16 -9
- package/esm/index.js +13 -9
- package/esm/menus.d.ts +63 -62
- package/esm/menus.js +131 -84
- package/esm/queryParams.d.ts +9 -0
- package/esm/queryParams.js +92 -0
- package/esm/sessionModelFactory.d.ts +18 -0
- package/esm/sessionModelFactory.js +1 -0
- package/esm/ui/App/App.d.ts +3 -14
- package/esm/ui/App/App.js +30 -17
- package/esm/ui/App/AppFab.d.ts +12 -3
- package/esm/ui/App/AppFab.js +15 -8
- package/esm/ui/App/AppReadyMarker.d.ts +34 -0
- package/esm/ui/App/AppReadyMarker.js +56 -0
- package/esm/ui/App/AppToolbar.d.ts +2 -14
- package/esm/ui/App/AppToolbar.js +23 -24
- package/esm/ui/App/ClassicViewsContainer.d.ts +3 -9
- package/esm/ui/App/ClassicViewsContainer.js +9 -8
- package/esm/ui/App/DialogQueue.d.ts +17 -1
- package/esm/ui/App/DialogQueue.js +30 -1
- package/esm/ui/App/Drawer.d.ts +1 -1
- package/esm/ui/App/Drawer.js +10 -39
- package/esm/ui/App/DrawerControls.d.ts +1 -5
- package/esm/ui/App/DrawerControls.js +1 -25
- package/esm/ui/App/DrawerHeader.d.ts +2 -3
- package/esm/ui/App/DrawerHeader.js +24 -10
- package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
- package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
- package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
- package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
- package/esm/ui/App/DrawerWidget.d.ts +1 -1
- package/esm/ui/App/DrawerWidget.js +9 -21
- package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
- package/esm/ui/App/JBrowseTabMenu.js +24 -5
- package/esm/ui/App/ViewButtons.d.ts +2 -4
- package/esm/ui/App/ViewButtons.js +15 -3
- package/esm/ui/App/ViewContainer.d.ts +3 -2
- package/esm/ui/App/ViewContainer.js +144 -24
- package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
- package/esm/ui/App/ViewContainerTitle.js +9 -2
- package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
- package/esm/ui/App/ViewErrorFallback.js +42 -0
- package/esm/ui/App/ViewHeader.d.ts +3 -4
- package/esm/ui/App/ViewHeader.js +28 -17
- package/esm/ui/App/ViewLauncher.d.ts +2 -8
- package/esm/ui/App/ViewLauncher.js +7 -5
- package/esm/ui/App/ViewMenu.d.ts +3 -5
- package/esm/ui/App/ViewMenu.js +87 -92
- package/esm/ui/App/ViewStack.d.ts +7 -0
- package/esm/ui/App/ViewStack.js +42 -0
- package/esm/ui/App/ViewWrapper.d.ts +15 -1
- package/esm/ui/App/ViewWrapper.js +26 -10
- package/esm/ui/App/ViewsContainer.d.ts +3 -8
- package/esm/ui/App/ViewsContainer.js +9 -6
- package/esm/ui/App/copyView.js +1 -1
- package/esm/ui/App/index.d.ts +2 -1
- package/esm/ui/App/index.js +3 -2
- package/esm/ui/App/lazyChunk.d.ts +13 -0
- package/esm/ui/App/lazyChunk.js +91 -0
- package/esm/ui/App/types.d.ts +12 -3
- package/esm/ui/App/types.js +1 -1
- package/esm/ui/App/useViewVisibility.d.ts +34 -0
- package/esm/ui/App/useViewVisibility.js +87 -0
- package/esm/ui/App/viewTitle.d.ts +5 -0
- package/esm/ui/App/viewTitle.js +9 -0
- package/esm/ui/index.d.ts +1 -1
- package/esm/ui/index.js +1 -1
- package/package.json +12 -17
- package/esm/AppFocus/index.d.ts +0 -5
- package/esm/AppFocus/index.js +0 -12
- package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
- package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
- package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
- package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
- package/esm/DockviewLayout/index.d.ts +0 -35
- package/esm/DockviewLayout/index.js +0 -111
- package/esm/ui/App/DockviewContext.d.ts +0 -22
- package/esm/ui/App/DockviewContext.js +0 -29
- package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
- package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
- package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
- package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
- package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
- package/esm/ui/App/DrawerWidgetSelector.js +0 -47
- package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
- package/esm/ui/App/JBrowseViewPanel.js +0 -40
- package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
- package/esm/ui/App/JBrowseViewTab.js +0 -97
- package/esm/ui/App/ModalWidget.d.ts +0 -6
- package/esm/ui/App/ModalWidget.js +0 -48
- package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
- package/esm/ui/App/TiledViewsContainer.js +0 -366
- package/esm/ui/App/dockviewUtils.d.ts +0 -46
- package/esm/ui/App/dockviewUtils.js +0 -55
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useCallback, useRef } from 'react';
|
|
3
|
+
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
4
|
+
import { observer } from 'mobx-react';
|
|
5
|
+
import { PanelView } from './PanelView.js';
|
|
6
|
+
import { dv } from './dockviewTheme.js';
|
|
7
|
+
import { pairSpan, withBoundaryAt } from './splitter.js';
|
|
8
|
+
import { isBranch } from './tree.js';
|
|
9
|
+
/**
|
|
10
|
+
* A node's share of its parent's space — the whole grid engine. The `min*: 0`
|
|
11
|
+
* are what stop a flex item refusing to shrink below its content; without them
|
|
12
|
+
* a wide view pushes its own cell past its share.
|
|
13
|
+
*/
|
|
14
|
+
function paneStyle(size) {
|
|
15
|
+
return {
|
|
16
|
+
display: 'flex',
|
|
17
|
+
flexGrow: size,
|
|
18
|
+
flexBasis: 0,
|
|
19
|
+
minWidth: 0,
|
|
20
|
+
minHeight: 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export const LayoutRenderer = observer(function LayoutRenderer(props) {
|
|
24
|
+
const { node, layout, drag } = props;
|
|
25
|
+
if (!isBranch(node)) {
|
|
26
|
+
return (_jsx("div", { style: paneStyle(node.size), children: _jsx(PanelView, { panel: node, layout: layout, chrome: props.chrome, drop: drag?.panelId === node.id ? drag : undefined }) }));
|
|
27
|
+
}
|
|
28
|
+
return (_jsx("div", { style: { ...paneStyle(node.size), flexDirection: node.direction }, children: node.children.map((child, i) => (_jsxs(Fragment, { children: [i > 0 && _jsx(Splitter, { branch: node, index: i, layout: layout }), _jsx(LayoutRenderer, { ...props, node: child })] }, child.id))) }));
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* The pixels the two panes either side of a handle occupy. Read from the DOM
|
|
32
|
+
* because sizes are shares of a container whose width only the browser knows.
|
|
33
|
+
* The panes are the handle's siblings minus the other handles, which is what
|
|
34
|
+
* `data-splitter` marks.
|
|
35
|
+
*/
|
|
36
|
+
function measurePairPx(handle, index, horizontal) {
|
|
37
|
+
const panes = [...(handle.parentElement?.children ?? [])].filter(el => !Object.hasOwn(el.dataset, 'splitter'));
|
|
38
|
+
const before = panes[index - 1]?.getBoundingClientRect();
|
|
39
|
+
const after = panes[index]?.getBoundingClientRect();
|
|
40
|
+
if (!before || !after) {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
return horizontal ? before.width + after.width : before.height + after.height;
|
|
44
|
+
}
|
|
45
|
+
// dockview's sash is a transparent grab strip with a 1px separator line drawn
|
|
46
|
+
// down the middle of it — the line is what you see, the 4px is what you can
|
|
47
|
+
// hit. Its dark theme deliberately gives the sash no hover colour at all.
|
|
48
|
+
const useSplitterStyles = makeStyles()({
|
|
49
|
+
splitter: {
|
|
50
|
+
flex: `0 0 ${dv.sashSize}px`,
|
|
51
|
+
position: 'relative',
|
|
52
|
+
background: 'transparent',
|
|
53
|
+
touchAction: 'none',
|
|
54
|
+
// it is focusable, so it has to show focus — dockview's sash deliberately
|
|
55
|
+
// has no HOVER colour, which is a different thing and still holds
|
|
56
|
+
'&:focus-visible': {
|
|
57
|
+
outline: `2px solid ${dv.edgeDockIndicatorColor}`,
|
|
58
|
+
outlineOffset: -1,
|
|
59
|
+
},
|
|
60
|
+
'&::before': {
|
|
61
|
+
content: '""',
|
|
62
|
+
position: 'absolute',
|
|
63
|
+
background: dv.separatorBorder,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
horizontal: {
|
|
67
|
+
'&::before': { top: 0, bottom: 0, left: '50%', width: 1 },
|
|
68
|
+
},
|
|
69
|
+
vertical: {
|
|
70
|
+
'&::before': { left: 0, right: 0, top: '50%', height: 1 },
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* Drags the boundary between children `index - 1` and `index`.
|
|
75
|
+
*
|
|
76
|
+
* The DOM half only: it measures, and hands position and pair span to
|
|
77
|
+
* `splitter.ts`, which decides where the boundary is allowed to land. Same
|
|
78
|
+
* split as the drag — geometry pure, wiring dumb.
|
|
79
|
+
*/
|
|
80
|
+
const Splitter = observer(function Splitter({ branch, index, layout, }) {
|
|
81
|
+
const { classes, cx } = useSplitterStyles();
|
|
82
|
+
const dragRef = useRef(undefined);
|
|
83
|
+
const horizontal = branch.direction === 'row';
|
|
84
|
+
/**
|
|
85
|
+
* The same three rules the tab drag owns (see `useLayoutDrag`), on the handle
|
|
86
|
+
* that was written before they were written down.
|
|
87
|
+
*
|
|
88
|
+
* A resize starts on the PRIMARY button of the primary pointer. A right-press
|
|
89
|
+
* armed the drag and then lost its `pointerup` to the native context menu,
|
|
90
|
+
* and capture routes every later move back here — so a button-less pointer
|
|
91
|
+
* went on resizing from the start position the right-press recorded.
|
|
92
|
+
*/
|
|
93
|
+
const onPointerDown = useCallback((event) => {
|
|
94
|
+
if (event.button !== 0 || !event.isPrimary) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const pairPx = measurePairPx(event.currentTarget, index, horizontal);
|
|
98
|
+
if (pairPx <= 0) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
dragRef.current = {
|
|
102
|
+
pointerId: event.pointerId,
|
|
103
|
+
axis: horizontal ? 'clientX' : 'clientY',
|
|
104
|
+
start: horizontal ? event.clientX : event.clientY,
|
|
105
|
+
pairPx,
|
|
106
|
+
startSizes: branch.children.map(c => c.size),
|
|
107
|
+
};
|
|
108
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
109
|
+
}, [branch, index, horizontal]);
|
|
110
|
+
// One `pointerId` per gesture: a second finger landing on the handle steered
|
|
111
|
+
// the first one's resize from the first one's start position, and its release
|
|
112
|
+
// ended the gesture the first one was still holding.
|
|
113
|
+
const onPointerMove = useCallback((event) => {
|
|
114
|
+
const drag = dragRef.current;
|
|
115
|
+
if (drag?.pointerId !== event.pointerId) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// the pointer's travel as a fraction of the pair's pixels, applied to the
|
|
119
|
+
// pair's share — so the handle tracks the pointer whatever the units are
|
|
120
|
+
const delta = (event[drag.axis] - drag.start) / drag.pairPx;
|
|
121
|
+
const sizes = drag.startSizes;
|
|
122
|
+
layout.setSizes(branch.id, withBoundaryAt(sizes, index, sizes[index - 1] + delta * pairSpan(sizes, index), drag.pairPx));
|
|
123
|
+
}, [branch.id, index, layout]);
|
|
124
|
+
const endDrag = useCallback(() => {
|
|
125
|
+
dragRef.current = undefined;
|
|
126
|
+
}, []);
|
|
127
|
+
const onPointerUp = useCallback((event) => {
|
|
128
|
+
if (dragRef.current && dragRef.current.pointerId !== event.pointerId) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
endDrag();
|
|
132
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
133
|
+
}, [endDrag]);
|
|
134
|
+
// `pointercancel` ends the gesture — a touch long-press opens the platform's
|
|
135
|
+
// context menu and cancels the pointer with no `pointerup` behind it. A
|
|
136
|
+
// browser also fires `lostpointercapture` for that, so this is the rule
|
|
137
|
+
// stated where it belongs rather than inferred from a second event.
|
|
138
|
+
const onPointerCancel = useCallback((event) => {
|
|
139
|
+
if (dragRef.current?.pointerId === event.pointerId) {
|
|
140
|
+
endDrag();
|
|
141
|
+
}
|
|
142
|
+
}, [endDrag]);
|
|
143
|
+
// A `separator` that can be moved is focusable and takes the arrow keys; one
|
|
144
|
+
// that cannot is just decoration and should not claim the role. This one
|
|
145
|
+
// claimed it while being neither focusable nor operable, which announces an
|
|
146
|
+
// affordance to a screen reader that its user then cannot find.
|
|
147
|
+
const onKeyDown = useCallback((event) => {
|
|
148
|
+
const sizes = branch.children.map(c => c.size);
|
|
149
|
+
const pair = pairSpan(sizes, index);
|
|
150
|
+
const decrease = horizontal ? 'ArrowLeft' : 'ArrowUp';
|
|
151
|
+
const increase = horizontal ? 'ArrowRight' : 'ArrowDown';
|
|
152
|
+
// 2% of the PAIR per press, matching the drag: the boundary moves within
|
|
153
|
+
// the two panes either side of it and every other pane holds still
|
|
154
|
+
const step = pair * 0.02;
|
|
155
|
+
let moved;
|
|
156
|
+
if (event.key === decrease) {
|
|
157
|
+
moved = sizes[index - 1] - step;
|
|
158
|
+
}
|
|
159
|
+
else if (event.key === increase) {
|
|
160
|
+
moved = sizes[index - 1] + step;
|
|
161
|
+
}
|
|
162
|
+
else if (event.key === 'Home') {
|
|
163
|
+
moved = 0;
|
|
164
|
+
}
|
|
165
|
+
else if (event.key === 'End') {
|
|
166
|
+
moved = pair;
|
|
167
|
+
}
|
|
168
|
+
if (moved === undefined) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
event.preventDefault();
|
|
172
|
+
// Home/End mean "as far as this goes", which is the minimum rather than
|
|
173
|
+
// zero — the same stop the drag hits
|
|
174
|
+
layout.setSizes(branch.id, withBoundaryAt(sizes, index, moved, measurePairPx(event.currentTarget, index, horizontal)));
|
|
175
|
+
}, [branch, index, horizontal, layout]);
|
|
176
|
+
// the pane BEFORE the handle, as a percentage of the pair it divides — which
|
|
177
|
+
// is what the handle actually moves
|
|
178
|
+
const sizes = branch.children.map(c => c.size);
|
|
179
|
+
const valueNow = Math.round((sizes[index - 1] / pairSpan(sizes, index)) * 100);
|
|
180
|
+
return (_jsx("div", { "data-splitter": true, role: "separator", tabIndex: 0, "aria-orientation": horizontal ? 'vertical' : 'horizontal', "aria-label": horizontal ? 'Resize panels' : 'Resize rows', "aria-valuenow": valueNow, "aria-valuemin": 0, "aria-valuemax": 100, onKeyDown: onKeyDown, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerCancel,
|
|
181
|
+
// capture can end without a pointerup — the browser drops it if the
|
|
182
|
+
// element is removed, and then the next pointer that merely passes over
|
|
183
|
+
// the handle would go on resizing from the stale start sizes
|
|
184
|
+
onLostPointerCapture: endDrag, className: cx(classes.splitter, horizontal ? classes.horizontal : classes.vertical), style: { cursor: horizontal ? 'ew-resize' : 'ns-resize' } }));
|
|
185
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DropTarget } from './dropZone.ts';
|
|
2
|
+
import type { WorkspaceLayout } from './model.ts';
|
|
3
|
+
import type { PanelChrome } from './panelChrome.ts';
|
|
4
|
+
import type { PanelNode } from './tree.ts';
|
|
5
|
+
interface PanelViewProps {
|
|
6
|
+
panel: PanelNode;
|
|
7
|
+
layout: WorkspaceLayout;
|
|
8
|
+
chrome: PanelChrome;
|
|
9
|
+
/** where an in-flight drag would land in THIS cell, if it is over this one */
|
|
10
|
+
drop?: DropTarget;
|
|
11
|
+
}
|
|
12
|
+
export declare const PanelView: ({ panel, layout, chrome, drop, }: PanelViewProps) => import("react").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { persistentScrollbarStyle } from '@jbrowse/core/ui/persistentScrollbarStyle';
|
|
3
|
+
import { useScrollPortHeightVar } from '@jbrowse/core/util/hooks';
|
|
4
|
+
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
5
|
+
import { observer } from 'mobx-react';
|
|
6
|
+
import { TabStrip } from './TabStrip.js';
|
|
7
|
+
import { dv } from './dockviewTheme.js';
|
|
8
|
+
import { indicatorRect } from './dropZone.js';
|
|
9
|
+
import { tabDomId, tabPanelDomId } from './panelChrome.js';
|
|
10
|
+
import { activeTabIn } from './tree.js';
|
|
11
|
+
/**
|
|
12
|
+
* One cell of the grid: a tab strip, and the content of whichever tab is
|
|
13
|
+
* showing.
|
|
14
|
+
*
|
|
15
|
+
* The strip and its state are `TabStrip`; this is the frame around them, and
|
|
16
|
+
* has no state of its own — it is a function of its node, which is the whole
|
|
17
|
+
* point of the layout being one MST tree.
|
|
18
|
+
*
|
|
19
|
+
* The line between chrome and content is the strip's lower edge: everything
|
|
20
|
+
* above it is dockview's dark theme in either JBrowse theme (see
|
|
21
|
+
* `dockviewTheme.ts`), everything below it follows the app's.
|
|
22
|
+
*/
|
|
23
|
+
const useStyles = makeStyles()(theme => ({
|
|
24
|
+
// `flex: 1` and `minWidth: 0` are load-bearing, not tidiness. This is a child
|
|
25
|
+
// of a `display: flex` row, so without them its width is its CONTENT's width
|
|
26
|
+
// — and a view measures its container to decide how wide to draw
|
|
27
|
+
// (useWidthSetter), so the two settle at the view's intrinsic width and the
|
|
28
|
+
// panel renders at half the window with dead space beside it.
|
|
29
|
+
panel: {
|
|
30
|
+
position: 'relative',
|
|
31
|
+
display: 'flex',
|
|
32
|
+
flexDirection: 'column',
|
|
33
|
+
flex: 1,
|
|
34
|
+
minWidth: 0,
|
|
35
|
+
minHeight: 0,
|
|
36
|
+
// the surface the views sit on, so it is the app's background and not
|
|
37
|
+
// dockview's — see `dockviewTheme.ts`. A cell is taller than its views
|
|
38
|
+
// whenever they don't fill it, so this is most of what a panel shows.
|
|
39
|
+
background: theme.palette.background.default,
|
|
40
|
+
},
|
|
41
|
+
content: {
|
|
42
|
+
flex: 1,
|
|
43
|
+
minWidth: 0,
|
|
44
|
+
minHeight: 0,
|
|
45
|
+
overflow: 'auto',
|
|
46
|
+
...persistentScrollbarStyle(theme),
|
|
47
|
+
},
|
|
48
|
+
indicator: {
|
|
49
|
+
position: 'absolute',
|
|
50
|
+
pointerEvents: 'none',
|
|
51
|
+
// above this panel's own content and nothing else — the panel is
|
|
52
|
+
// position:relative, so an app-wide z-index would let it cover menus
|
|
53
|
+
zIndex: 1,
|
|
54
|
+
background: dv.dragOverBackground,
|
|
55
|
+
outline: `1px solid ${dv.edgeDockIndicatorColor}`,
|
|
56
|
+
},
|
|
57
|
+
// where a tab dragged onto the strip would land
|
|
58
|
+
caret: {
|
|
59
|
+
position: 'absolute',
|
|
60
|
+
pointerEvents: 'none',
|
|
61
|
+
zIndex: 1,
|
|
62
|
+
top: 0,
|
|
63
|
+
width: 2,
|
|
64
|
+
height: dv.tabsHeight,
|
|
65
|
+
background: dv.edgeDockIndicatorColor,
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
export const PanelView = observer(function PanelView({ panel, layout, chrome, drop, }) {
|
|
69
|
+
const { classes } = useStyles();
|
|
70
|
+
const active = activeTabIn(panel);
|
|
71
|
+
const contentRef = useScrollPortHeightVar();
|
|
72
|
+
return (_jsxs("div", { "data-panel-id": panel.id, className: classes.panel, onPointerDownCapture: () => {
|
|
73
|
+
// clicking anywhere in a cell makes it the one a new view lands in.
|
|
74
|
+
// Capture, so it still registers when the click is consumed by a
|
|
75
|
+
// control inside the view.
|
|
76
|
+
if (layout.activePanelId !== panel.id) {
|
|
77
|
+
layout.setActivePanelId(panel.id);
|
|
78
|
+
}
|
|
79
|
+
}, children: [_jsx(TabStrip, { panel: panel, layout: layout, chrome: chrome, active: active, groupActive: layout.activePanelId === panel.id }), _jsx("div", { role: "tabpanel", id: tabPanelDomId(panel.id), "aria-labelledby": active ? tabDomId(active.id) : undefined, className: classes.content, ref: contentRef, children: active ? chrome.renderTabContent(active) : null }), _jsx(DropIndicator, { drop: drop, classes: classes })] }));
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* Where a drop would land, drawn over the cell.
|
|
83
|
+
*
|
|
84
|
+
* A strip drop gets a caret at the gap rather than a wash over half the cell:
|
|
85
|
+
* it is a position in the tab order, and shading half the panel would say
|
|
86
|
+
* "split this cell", which is the wrong thing.
|
|
87
|
+
*/
|
|
88
|
+
const DropIndicator = observer(function DropIndicator({ drop, classes, }) {
|
|
89
|
+
if (drop?.strip) {
|
|
90
|
+
return (_jsx("div", { "data-drop-caret": drop.strip.index, className: classes.caret,
|
|
91
|
+
// A scrolled strip's left-most gap has a negative panel-relative x, and
|
|
92
|
+
// the panel has no `overflow: hidden` — the caret drew into the
|
|
93
|
+
// neighbour. Clamped here because `stripDropAt` is deliberately
|
|
94
|
+
// ignorant of which coordinate space it was handed, so it cannot know
|
|
95
|
+
// where zero is.
|
|
96
|
+
style: { left: Math.max(0, drop.strip.left) } }));
|
|
97
|
+
}
|
|
98
|
+
return drop ? (_jsx("div", { "data-drop-indicator": drop.zone, className: classes.indicator, style: indicatorRect(drop.zone) })) : null;
|
|
99
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WorkspaceLayout } from './model.ts';
|
|
2
|
+
import type { PanelChrome } from './panelChrome.ts';
|
|
3
|
+
import type { PanelNode, TabNode } from './tree.ts';
|
|
4
|
+
interface TabStripProps {
|
|
5
|
+
panel: PanelNode;
|
|
6
|
+
layout: WorkspaceLayout;
|
|
7
|
+
chrome: PanelChrome;
|
|
8
|
+
/** the tab this panel is showing — the strip does not decide it */
|
|
9
|
+
active: TabNode | undefined;
|
|
10
|
+
/** whether this is the cell a newly launched view lands in */
|
|
11
|
+
groupActive: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare const TabStrip: ({ panel, layout, chrome, active, groupActive, }: TabStripProps) => import("react").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
4
|
+
import { observer } from 'mobx-react';
|
|
5
|
+
import { dv, tabColors } from './dockviewTheme.js';
|
|
6
|
+
import { tabDomId, tabPanelDomId } from './panelChrome.js';
|
|
7
|
+
/**
|
|
8
|
+
* A panel's tab strip: the tabs, and the panel's own buttons beside them.
|
|
9
|
+
*
|
|
10
|
+
* Everything with state in a cell lives here — the roving tabindex, the wheel
|
|
11
|
+
* translation, the scroll-into-view — which leaves `PanelView` a pure function
|
|
12
|
+
* of its node. The strip is dockview's dark theme, transcribed in
|
|
13
|
+
* `dockviewTheme.ts`; see there for why it is fixed rather than derived from
|
|
14
|
+
* the MUI theme.
|
|
15
|
+
*/
|
|
16
|
+
const useStyles = makeStyles()({
|
|
17
|
+
strip: {
|
|
18
|
+
display: 'flex',
|
|
19
|
+
flexShrink: 0,
|
|
20
|
+
boxSizing: 'border-box',
|
|
21
|
+
height: dv.tabsHeight,
|
|
22
|
+
fontSize: dv.tabsFontSize,
|
|
23
|
+
background: dv.tabsBackground,
|
|
24
|
+
},
|
|
25
|
+
tabs: {
|
|
26
|
+
display: 'flex',
|
|
27
|
+
overflowX: 'auto',
|
|
28
|
+
// shrink but never GROW: the panel actions are the next child of the strip,
|
|
29
|
+
// so a tab list that takes the leftover space pushes the `+` to the far
|
|
30
|
+
// right edge, arbitrarily far from the tab it acts on. `0 1 auto` sizes the
|
|
31
|
+
// list to its tabs and leaves the `+` beside the last one, and the shrink
|
|
32
|
+
// half is what makes it scroll here rather than push the `+` off the strip.
|
|
33
|
+
flex: '0 1 auto',
|
|
34
|
+
minWidth: 0,
|
|
35
|
+
// the strip is chrome, so a scrollbar across it would be noise
|
|
36
|
+
scrollbarWidth: 'none',
|
|
37
|
+
'&::-webkit-scrollbar': { display: 'none' },
|
|
38
|
+
},
|
|
39
|
+
tab: {
|
|
40
|
+
display: 'flex',
|
|
41
|
+
alignItems: 'center',
|
|
42
|
+
flexShrink: 0,
|
|
43
|
+
boxSizing: 'border-box',
|
|
44
|
+
maxWidth: 240,
|
|
45
|
+
padding: '0.25rem 0.5rem',
|
|
46
|
+
cursor: 'pointer',
|
|
47
|
+
userSelect: 'none',
|
|
48
|
+
touchAction: 'none',
|
|
49
|
+
borderRight: `1px solid ${dv.tabDividerColor}`,
|
|
50
|
+
'&:hover .jbrowse-tab-menu': { visibility: 'visible' },
|
|
51
|
+
// a keyboard user needs the ⋮ too, and hover is not a thing they can do
|
|
52
|
+
'&:focus-within .jbrowse-tab-menu': { visibility: 'visible' },
|
|
53
|
+
// the strip is dark in either theme, so the focus ring is the drop
|
|
54
|
+
// indicator's blue rather than the UA default black-on-dark
|
|
55
|
+
'&:focus-visible': {
|
|
56
|
+
outline: `2px solid ${dv.edgeDockIndicatorColor}`,
|
|
57
|
+
outlineOffset: -2,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
/**
|
|
62
|
+
* A wheel event's delta in PIXELS.
|
|
63
|
+
*
|
|
64
|
+
* `deltaY` is only pixels when `deltaMode` says so. Firefox reports whole lines
|
|
65
|
+
* for a mouse wheel — `deltaMode: 1`, `deltaY: ±3` — where Chrome reports
|
|
66
|
+
* `deltaMode: 0`, `deltaY: ±100`. Using the raw number moves the strip three
|
|
67
|
+
* pixels per notch there: scrolling, technically, and unusable in practice.
|
|
68
|
+
*
|
|
69
|
+
* The line height is nominal rather than measured. `getComputedStyle` per wheel
|
|
70
|
+
* event to resolve a `line-height: normal` that is itself font-dependent buys
|
|
71
|
+
* accuracy nobody can perceive in a scroll gesture, and the strip is a fixed
|
|
72
|
+
* 35px of chrome in one size.
|
|
73
|
+
*/
|
|
74
|
+
const WHEEL_LINE_PX = 16;
|
|
75
|
+
function wheelDeltaPixels(event, pageSize) {
|
|
76
|
+
switch (event.deltaMode) {
|
|
77
|
+
case 1:
|
|
78
|
+
return event.deltaY * WHEEL_LINE_PX;
|
|
79
|
+
case 2:
|
|
80
|
+
return event.deltaY * pageSize;
|
|
81
|
+
default:
|
|
82
|
+
return event.deltaY;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export const TabStrip = observer(function TabStrip({ panel, layout, chrome, active, groupActive, }) {
|
|
86
|
+
const { classes } = useStyles();
|
|
87
|
+
const { renderPanelActions } = chrome;
|
|
88
|
+
// Roving tabindex: the strip is ONE tab stop, and the arrow keys move within
|
|
89
|
+
// it. Focus is tracked separately from selection because activation here is
|
|
90
|
+
// manual (see `onKeyDown`), so the two genuinely differ while arrowing.
|
|
91
|
+
const stripRef = useRef(null);
|
|
92
|
+
const [focusedTabId, setFocusedTabId] = useState(undefined);
|
|
93
|
+
const roving = panel.tabs.find(t => t.id === focusedTabId)?.id ?? active?.id;
|
|
94
|
+
// by dataset rather than an interpolated attribute selector: a tab id is
|
|
95
|
+
// nanoid output and would need CSS.escape, which jsdom does not have
|
|
96
|
+
function tabElement(tabId) {
|
|
97
|
+
return [...(stripRef.current?.children ?? [])].find(child => child.dataset.tabId === tabId);
|
|
98
|
+
}
|
|
99
|
+
function focusTab(tabId) {
|
|
100
|
+
setFocusedTabId(tabId);
|
|
101
|
+
// focusing scrolls it into view on its own, which is the whole reason the
|
|
102
|
+
// keyboard could always reach an overflowing strip when the mouse could not
|
|
103
|
+
tabElement(tabId)?.focus();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Keep the shown tab in view.
|
|
107
|
+
*
|
|
108
|
+
* The strip scrolls, so a tab can become current while sitting outside it —
|
|
109
|
+
* `+` on a full strip appends a tab, makes it active, and leaves the user
|
|
110
|
+
* looking at the tabs it scrolled past. Clicking never needs this (you
|
|
111
|
+
* clicked something visible) and arrowing gets it from `focus()`, so this is
|
|
112
|
+
* for the tab that becomes current without being touched.
|
|
113
|
+
*
|
|
114
|
+
* `block: 'nearest'` so it cannot scroll an ancestor vertically. jsdom has no
|
|
115
|
+
* scrollIntoView at all, but `config/jest/scrollIntoView.js` already no-ops it
|
|
116
|
+
* for the whole suite — don't add an optional call for that, it reads as a
|
|
117
|
+
* browser that might not have the method.
|
|
118
|
+
*/
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
tabElement(active?.id)?.scrollIntoView({
|
|
121
|
+
block: 'nearest',
|
|
122
|
+
inline: 'nearest',
|
|
123
|
+
});
|
|
124
|
+
}, [active?.id]);
|
|
125
|
+
/**
|
|
126
|
+
* MANUAL activation: arrows move focus, Enter/Space selects.
|
|
127
|
+
*
|
|
128
|
+
* The automatic form (arrowing selects as it goes) is the more common reading
|
|
129
|
+
* of the tabs pattern, and is wrong here for the reason WAI-ARIA names as its
|
|
130
|
+
* exception — showing a tab is expensive. Only the selected tab's views are
|
|
131
|
+
* mounted, and a JBrowse view costs a WebGL2 context per display against a
|
|
132
|
+
* ceiling of 16, so arrowing across five tabs would build and tear down five
|
|
133
|
+
* sets of them to pass through.
|
|
134
|
+
*/
|
|
135
|
+
function onKeyDown(event, tabId) {
|
|
136
|
+
const ids = panel.tabs.map(t => t.id);
|
|
137
|
+
const i = ids.indexOf(tabId);
|
|
138
|
+
const step = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
|
|
139
|
+
let next;
|
|
140
|
+
if (step !== 0) {
|
|
141
|
+
next = ids[(i + step + ids.length) % ids.length];
|
|
142
|
+
}
|
|
143
|
+
else if (event.key === 'Home') {
|
|
144
|
+
next = ids[0];
|
|
145
|
+
}
|
|
146
|
+
else if (event.key === 'End') {
|
|
147
|
+
next = ids.at(-1);
|
|
148
|
+
}
|
|
149
|
+
else if (event.key === 'Enter' || event.key === ' ') {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
layout.setActiveTab(panel.id, tabId);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (next !== undefined) {
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
focusTab(next);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return (_jsxs("div", { "data-tab-strip": true, className: classes.strip,
|
|
160
|
+
// Double-clicking the strip's empty space maximizes the cell and
|
|
161
|
+
// restores it — the IDE convention, and free here because the strip's
|
|
162
|
+
// only other `onDoubleClick` is the one on a tab's own label, for rename.
|
|
163
|
+
//
|
|
164
|
+
// `target === currentTarget` is what keeps those two apart, and it is the
|
|
165
|
+
// test rather than a `stopPropagation` in `WorkspaceTab` because it holds
|
|
166
|
+
// for anything ever put on the strip: a rename double-click bubbles out
|
|
167
|
+
// of the label to here, and so would a double-click on the `+`.
|
|
168
|
+
onDoubleClick: event => {
|
|
169
|
+
if (event.target === event.currentTarget) {
|
|
170
|
+
layout.toggleMaximizedPanel(panel.id);
|
|
171
|
+
}
|
|
172
|
+
}, children: [_jsx("div", { role: "tablist", ref: stripRef, className: classes.tabs,
|
|
173
|
+
// A mouse wheel only has a vertical axis, and this scrolls
|
|
174
|
+
// horizontally — so without translating it, a strip with more tabs
|
|
175
|
+
// than fit is reachable by trackpad swipe and by keyboard and NOT AT
|
|
176
|
+
// ALL by mouse. The scrollbar is hidden on purpose (the strip is
|
|
177
|
+
// chrome) which removes the other way of noticing there is more.
|
|
178
|
+
//
|
|
179
|
+
// `deltaX` is a trackpad's own horizontal axis and the browser has
|
|
180
|
+
// already applied it; taking the larger axis leaves that gesture alone
|
|
181
|
+
// rather than doubling it.
|
|
182
|
+
onWheel: event => {
|
|
183
|
+
const el = stripRef.current;
|
|
184
|
+
if (!el || Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
el.scrollLeft += wheelDeltaPixels(event, el.clientWidth);
|
|
188
|
+
}, children: panel.tabs.map(tab => (_jsx(Tab, { tab: tab, panel: panel, layout: layout, chrome: chrome, className: classes.tab, selected: tab.id === active?.id, groupActive: groupActive, tabIndex: tab.id === roving ? 0 : -1, onKeyDown: onKeyDown, onFocus: setFocusedTabId }, tab.id))) }), renderPanelActions?.(panel)] }));
|
|
189
|
+
});
|
|
190
|
+
const Tab = observer(function Tab({ tab, panel, layout, chrome, className, selected, groupActive, tabIndex, onKeyDown, onFocus, }) {
|
|
191
|
+
const { renderTabLabel, dragHandlers, onTabClose } = chrome;
|
|
192
|
+
return (_jsx("div", { role: "tab", id: tabDomId(tab.id), "data-tab-id": tab.id, "aria-selected": selected, "aria-controls": selected ? tabPanelDomId(panel.id) : undefined, tabIndex: tabIndex, onKeyDown: event => {
|
|
193
|
+
onKeyDown(event, tab.id);
|
|
194
|
+
}, onFocus: () => {
|
|
195
|
+
onFocus(tab.id);
|
|
196
|
+
}, onPointerDown: event => {
|
|
197
|
+
// The middle button closes rather than drags, and its default action is
|
|
198
|
+
// the browser's autoscroll — which would otherwise start the moment a
|
|
199
|
+
// tab is middle-pressed.
|
|
200
|
+
//
|
|
201
|
+
// Cancelling pointerdown suppresses the compatibility mouse events
|
|
202
|
+
// (which is what stops the autoscroll) and NOT `click` / `auxclick`:
|
|
203
|
+
// Pointer Events L3 dispatches those two independently of that mapping.
|
|
204
|
+
// So the close below still fires, which is the thing this looks like it
|
|
205
|
+
// would break.
|
|
206
|
+
if (event.button === 1) {
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Showing a tab is expensive — it mounts a stack of views, each display
|
|
211
|
+
// costing a WebGL2 context — so it is the LEFT button that asks for it,
|
|
212
|
+
// the same gate dockview puts on `_activateOnPointerDown`. A right-press
|
|
213
|
+
// is on its way to a context menu and is not a request to look at
|
|
214
|
+
// anything.
|
|
215
|
+
if (event.button !== 0) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
layout.setActiveTab(panel.id, tab.id);
|
|
219
|
+
dragHandlers.onTabPointerDown(tab.id, event);
|
|
220
|
+
}, onAuxClick: event => {
|
|
221
|
+
if (event.button === 1) {
|
|
222
|
+
event.preventDefault();
|
|
223
|
+
onTabClose?.(tab.id);
|
|
224
|
+
}
|
|
225
|
+
}, onPointerMove: dragHandlers.onTabPointerMove, onPointerUp: dragHandlers.onTabPointerUp, onPointerCancel: dragHandlers.onTabPointerCancel, className: className, style: tabColors(groupActive, selected), children: renderTabLabel(tab) }));
|
|
226
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WorkspaceSessionType } from '../ui/App/types.ts';
|
|
2
|
+
import type { WorkspaceLayout } from './model.ts';
|
|
3
|
+
type WorkspaceSession = WorkspaceSessionType & WorkspaceLayout;
|
|
4
|
+
/**
|
|
5
|
+
* The workspace. Compare `TiledViewsContainer` + `useDockviewController`, which
|
|
6
|
+
* this replaces: there is no api to hold, no `onReady`, no event to subscribe
|
|
7
|
+
* to, and no reconciliation — the layout is session state and this renders it.
|
|
8
|
+
*
|
|
9
|
+
* The one reaction left is homing: `session.views` is owned by the session, so
|
|
10
|
+
* a view launched from a menu arrives belonging to no tab and has to land
|
|
11
|
+
* somewhere. That is one-directional and idempotent, and nothing reads back.
|
|
12
|
+
*/
|
|
13
|
+
export declare const WorkspaceContainer: ({ session, }: {
|
|
14
|
+
session: WorkspaceSession;
|
|
15
|
+
}) => import("react").JSX.Element;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, lazy, useCallback, useEffect, useMemo } from 'react';
|
|
3
|
+
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
4
|
+
import { autorun } from 'mobx';
|
|
5
|
+
import { observer } from 'mobx-react';
|
|
6
|
+
import ViewStack from '../ui/App/ViewStack.js';
|
|
7
|
+
import { LayoutRenderer } from './LayoutRenderer.js';
|
|
8
|
+
import { WorkspacePanelActions } from './WorkspacePanelActions.js';
|
|
9
|
+
import { WorkspaceTab } from './WorkspaceTab.js';
|
|
10
|
+
import { useLayoutDrag } from './useLayoutDrag.js';
|
|
11
|
+
const ViewLauncher = lazy(() => import('../ui/App/ViewLauncher.js'));
|
|
12
|
+
const useStyles = makeStyles()(theme => ({
|
|
13
|
+
container: {
|
|
14
|
+
height: '100%',
|
|
15
|
+
width: '100%',
|
|
16
|
+
display: 'flex',
|
|
17
|
+
gridRow: 'components',
|
|
18
|
+
background: theme.palette.background.default,
|
|
19
|
+
},
|
|
20
|
+
stack: {
|
|
21
|
+
display: 'flex',
|
|
22
|
+
flexDirection: 'column',
|
|
23
|
+
minWidth: 0,
|
|
24
|
+
},
|
|
25
|
+
empty: {
|
|
26
|
+
display: 'flex',
|
|
27
|
+
alignItems: 'center',
|
|
28
|
+
justifyContent: 'center',
|
|
29
|
+
height: '100%',
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
/**
|
|
33
|
+
* The workspace. Compare `TiledViewsContainer` + `useDockviewController`, which
|
|
34
|
+
* this replaces: there is no api to hold, no `onReady`, no event to subscribe
|
|
35
|
+
* to, and no reconciliation — the layout is session state and this renders it.
|
|
36
|
+
*
|
|
37
|
+
* The one reaction left is homing: `session.views` is owned by the session, so
|
|
38
|
+
* a view launched from a menu arrives belonging to no tab and has to land
|
|
39
|
+
* somewhere. That is one-directional and idempotent, and nothing reads back.
|
|
40
|
+
*/
|
|
41
|
+
export const WorkspaceContainer = observer(function WorkspaceContainer({ session, }) {
|
|
42
|
+
const { classes } = useStyles();
|
|
43
|
+
const { drag, handlers } = useLayoutDrag(session);
|
|
44
|
+
useEffect(() => autorun(() => {
|
|
45
|
+
// reads session.views itself, so it re-runs when the view set changes;
|
|
46
|
+
// homeUnassignedViews is an action and would not be tracked from inside
|
|
47
|
+
session.homeUnassignedViews(session.views.map(v => v.id));
|
|
48
|
+
}), [session]);
|
|
49
|
+
// The layout does not own views, so closing anything that holds them is
|
|
50
|
+
// explicitly the pair. Stated ONCE, here, because every spelling of it is a
|
|
51
|
+
// chance to drop the node and leave its views in the session forever — and
|
|
52
|
+
// three gestures want it now: the tab's ⋮ menu, middle-clicking the tab, and
|
|
53
|
+
// the cell's ×.
|
|
54
|
+
const closeViews = useCallback((viewIds) => {
|
|
55
|
+
for (const view of viewsOf(session, viewIds)) {
|
|
56
|
+
session.removeView(view);
|
|
57
|
+
}
|
|
58
|
+
}, [session]);
|
|
59
|
+
const closeTab = useCallback((tabId) => {
|
|
60
|
+
const tab = session.findTab(tabId)?.tab;
|
|
61
|
+
if (!tab) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
closeViews(tab.viewIds);
|
|
65
|
+
session.closeTab(tabId);
|
|
66
|
+
}, [session, closeViews]);
|
|
67
|
+
const closePanel = useCallback((panelId) => {
|
|
68
|
+
const panel = session.panels.find(p => p.id === panelId);
|
|
69
|
+
if (!panel) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
closeViews(panel.tabs.flatMap(t => t.viewIds));
|
|
73
|
+
session.closePanel(panelId);
|
|
74
|
+
}, [session, closeViews]);
|
|
75
|
+
/**
|
|
76
|
+
* Memoised because this reaches every panel, so a fresh one per render
|
|
77
|
+
* defeats `observer`'s memo for all of them and each re-render rebuilds a
|
|
78
|
+
* `ViewStack`. `drag` is deliberately NOT in here — it goes down its own prop
|
|
79
|
+
* so it reaches only the cell it describes.
|
|
80
|
+
*
|
|
81
|
+
* Nothing does this for us: `observer(function(){})` is not compiled by the
|
|
82
|
+
* React Compiler.
|
|
83
|
+
*/
|
|
84
|
+
const chrome = useMemo(() => ({
|
|
85
|
+
dragHandlers: handlers,
|
|
86
|
+
onTabClose: closeTab,
|
|
87
|
+
renderPanelActions: panel => (_jsx(WorkspacePanelActions, { panel: panel, session: session, onClose: () => {
|
|
88
|
+
closePanel(panel.id);
|
|
89
|
+
} })),
|
|
90
|
+
renderTabLabel: tab => (_jsx(WorkspaceTab, { tab: tab, views: viewsOf(session, tab.viewIds), session: session, layout: session, onClose: () => {
|
|
91
|
+
closeTab(tab.id);
|
|
92
|
+
} })),
|
|
93
|
+
renderTabContent: tab => {
|
|
94
|
+
const views = viewsOf(session, tab.viewIds);
|
|
95
|
+
return views.length > 0 ? (_jsx(ViewStack, { views: views, session: session, className: classes.stack })) : (_jsx("div", { className: classes.empty, children: _jsx(Suspense, { fallback: null, children: _jsx(ViewLauncher, { session: session }) }) }));
|
|
96
|
+
},
|
|
97
|
+
}), [session, handlers, closeTab, closePanel, classes.stack, classes.empty]);
|
|
98
|
+
return (_jsx("div", { className: classes.container, "data-testid": "workspace", children: _jsx(LayoutRenderer, { node: session.visibleTree, layout: session, drag: drag, chrome: chrome }) }));
|
|
99
|
+
});
|
|
100
|
+
// `session.views` is the one ordering of views, in both layout modes, so a
|
|
101
|
+
// tab's membership list is filtered through it rather than read as an order.
|
|
102
|
+
function viewsOf(session, viewIds) {
|
|
103
|
+
const members = new Set(viewIds);
|
|
104
|
+
return session.views.filter(v => members.has(v.id));
|
|
105
|
+
}
|