@jbrowse/app-core 5.0.0-beta.11 → 5.0.0-beta.13
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/JBrowseConfig/RootConfiguration.d.ts +1 -8
- package/esm/JBrowseConfig/RootConfiguration.js +2 -6
- package/esm/JBrowseConfig/index.d.ts +1 -8
- package/esm/JBrowseModel/index.d.ts +1 -8
- package/esm/JbApi/jbApi.d.ts +2 -2
- package/esm/JbApi/jbApi.js +12 -10
- package/esm/SessionSpec/loadSessionSpec.js +2 -12
- package/esm/WorkspaceLayout/LayoutRenderer.d.ts +0 -10
- package/esm/WorkspaceLayout/LayoutRenderer.js +34 -101
- package/esm/WorkspaceLayout/PanelView.js +19 -45
- package/esm/WorkspaceLayout/TabStrip.d.ts +2 -2
- package/esm/WorkspaceLayout/TabStrip.js +36 -104
- package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +0 -9
- package/esm/WorkspaceLayout/WorkspaceContainer.js +9 -34
- package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +1 -16
- package/esm/WorkspaceLayout/WorkspacePanelActions.js +45 -50
- package/esm/WorkspaceLayout/WorkspaceTab.d.ts +1 -16
- package/esm/WorkspaceLayout/WorkspaceTab.js +11 -47
- package/esm/WorkspaceLayout/dropZone.d.ts +7 -37
- package/esm/WorkspaceLayout/dropZone.js +7 -44
- package/esm/WorkspaceLayout/model.d.ts +42 -112
- package/esm/WorkspaceLayout/model.js +139 -268
- package/esm/WorkspaceLayout/panelChrome.d.ts +4 -7
- package/esm/WorkspaceLayout/panelChrome.js +0 -2
- package/esm/WorkspaceLayout/spec.d.ts +10 -51
- package/esm/WorkspaceLayout/spec.js +12 -80
- package/esm/WorkspaceLayout/splitter.d.ts +3 -19
- package/esm/WorkspaceLayout/splitter.js +5 -21
- package/esm/WorkspaceLayout/tabName.d.ts +8 -0
- package/esm/WorkspaceLayout/tabName.js +14 -0
- package/esm/WorkspaceLayout/tree.d.ts +19 -76
- package/esm/WorkspaceLayout/tree.js +29 -122
- package/esm/WorkspaceLayout/useLayoutDrag.d.ts +4 -22
- package/esm/WorkspaceLayout/useLayoutDrag.js +46 -174
- package/esm/WorkspaceLayout/usePointerGesture.d.ts +24 -0
- package/esm/WorkspaceLayout/usePointerGesture.js +83 -0
- package/esm/WorkspaceLayout/workspaceTheme.d.ts +27 -0
- package/esm/WorkspaceLayout/workspaceTheme.js +37 -0
- package/esm/ui/App/App.js +3 -0
- package/esm/ui/App/JBrowseTabMenu.js +1 -1
- package/esm/ui/App/ThemeModeButton.js +5 -4
- package/esm/ui/App/ViewMenu.js +47 -29
- package/esm/ui/App/ViewsContainer.js +3 -4
- package/esm/ui/App/lazyParts.d.ts +0 -5
- package/esm/ui/App/lazyParts.js +0 -1
- package/esm/ui/App/preloadAppShell.js +3 -6
- package/esm/ui/App/types.d.ts +3 -2
- package/package.json +4 -4
- package/esm/WorkspaceLayout/dockviewTheme.d.ts +0 -69
- package/esm/WorkspaceLayout/dockviewTheme.js +0 -82
- package/esm/ui/App/ClassicViewsContainer.d.ts +0 -6
- package/esm/ui/App/ClassicViewsContainer.js +0 -22
|
@@ -123,11 +123,6 @@ export default function RootConfiguration({ pluginManager, extraConfigSlots, }:
|
|
|
123
123
|
} & {
|
|
124
124
|
defaultValue: boolean;
|
|
125
125
|
};
|
|
126
|
-
readonly useWorkspaces: {
|
|
127
|
-
type: "boolean";
|
|
128
|
-
} & {
|
|
129
|
-
defaultValue: boolean;
|
|
130
|
-
};
|
|
131
126
|
readonly developerMode: {
|
|
132
127
|
type: "boolean";
|
|
133
128
|
} & {
|
|
@@ -145,8 +140,6 @@ export default function RootConfiguration({ pluginManager, extraConfigSlots, }:
|
|
|
145
140
|
defaultValue: unknown;
|
|
146
141
|
};
|
|
147
142
|
readonly logoPath: {
|
|
148
|
-
type: "
|
|
149
|
-
} & {
|
|
150
|
-
defaultValue: unknown;
|
|
143
|
+
type: "maybeFileLocation";
|
|
151
144
|
};
|
|
152
145
|
}, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, undefined, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
|
|
@@ -90,12 +90,8 @@ export default function RootConfiguration({ pluginManager, extraConfigSlots = {}
|
|
|
90
90
|
* #slot configuration.logoPath
|
|
91
91
|
*/
|
|
92
92
|
logoPath: {
|
|
93
|
-
type: '
|
|
94
|
-
description: '
|
|
95
|
-
defaultValue: {
|
|
96
|
-
uri: '',
|
|
97
|
-
locationType: 'UriLocation',
|
|
98
|
-
},
|
|
93
|
+
type: 'maybeFileLocation',
|
|
94
|
+
description: 'a custom logo image displayed in the app header; unset, the JBrowse logo',
|
|
99
95
|
advanced: true,
|
|
100
96
|
},
|
|
101
97
|
/**
|
|
@@ -126,11 +126,6 @@ export declare function JBrowseConfigF({ pluginManager, assemblyConfigSchema, ex
|
|
|
126
126
|
} & {
|
|
127
127
|
defaultValue: boolean;
|
|
128
128
|
};
|
|
129
|
-
readonly useWorkspaces: {
|
|
130
|
-
type: "boolean";
|
|
131
|
-
} & {
|
|
132
|
-
defaultValue: boolean;
|
|
133
|
-
};
|
|
134
129
|
readonly developerMode: {
|
|
135
130
|
type: "boolean";
|
|
136
131
|
} & {
|
|
@@ -148,9 +143,7 @@ export declare function JBrowseConfigF({ pluginManager, assemblyConfigSchema, ex
|
|
|
148
143
|
defaultValue: unknown;
|
|
149
144
|
};
|
|
150
145
|
readonly logoPath: {
|
|
151
|
-
type: "
|
|
152
|
-
} & {
|
|
153
|
-
defaultValue: unknown;
|
|
146
|
+
type: "maybeFileLocation";
|
|
154
147
|
};
|
|
155
148
|
}, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, undefined, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
|
|
156
149
|
/**
|
|
@@ -115,11 +115,6 @@ export declare function JBrowseModelF({ pluginManager, assemblyConfigSchema, ext
|
|
|
115
115
|
} & {
|
|
116
116
|
defaultValue: boolean;
|
|
117
117
|
};
|
|
118
|
-
readonly useWorkspaces: {
|
|
119
|
-
type: "boolean";
|
|
120
|
-
} & {
|
|
121
|
-
defaultValue: boolean;
|
|
122
|
-
};
|
|
123
118
|
readonly developerMode: {
|
|
124
119
|
type: "boolean";
|
|
125
120
|
} & {
|
|
@@ -137,9 +132,7 @@ export declare function JBrowseModelF({ pluginManager, assemblyConfigSchema, ext
|
|
|
137
132
|
defaultValue: unknown;
|
|
138
133
|
};
|
|
139
134
|
readonly logoPath: {
|
|
140
|
-
type: "
|
|
141
|
-
} & {
|
|
142
|
-
defaultValue: unknown;
|
|
135
|
+
type: "maybeFileLocation";
|
|
143
136
|
};
|
|
144
137
|
}, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, undefined, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
|
|
145
138
|
plugins: import("@jbrowse/mobx-state-tree").IArrayType<import("@jbrowse/mobx-state-tree").IType<PluginDefinition, PluginDefinition, PluginDefinition>>;
|
package/esm/JbApi/jbApi.d.ts
CHANGED
|
@@ -1018,9 +1018,9 @@ export declare function createJbApi(pluginManager: PluginManager, options?: JbAp
|
|
|
1018
1018
|
total: number;
|
|
1019
1019
|
note?: string | undefined;
|
|
1020
1020
|
tracks: {
|
|
1021
|
-
trackId:
|
|
1021
|
+
trackId: any;
|
|
1022
1022
|
name: string;
|
|
1023
|
-
type:
|
|
1023
|
+
type: any;
|
|
1024
1024
|
assemblyNames: string[];
|
|
1025
1025
|
}[];
|
|
1026
1026
|
};
|
package/esm/JbApi/jbApi.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getReExportRegistry, setReExportRegistry, } from '@jbrowse/core/ReExports/registry';
|
|
2
|
-
import { getConf, getConfigurationSchemaDefinition, getConfigurationSchemaMetadata, isSlotDefinitionEntry, readConfObject, shorthandTargets, } from '@jbrowse/core/configuration';
|
|
2
|
+
import { getConf, getConfigurationSchemaDefinition, getConfigurationSchemaMetadata, hydrateTrackConfig, isSlotDefinitionEntry, readConfObject, shorthandTargets, } from '@jbrowse/core/configuration';
|
|
3
3
|
import { releaseAdapterSession, retainAdapterSession, } from '@jbrowse/core/data_adapters/adapterSessionRefcount';
|
|
4
4
|
import { adapterByteLimit } from '@jbrowse/core/rpc/byteBudget';
|
|
5
5
|
import { isElectron, isSessionWithAddSessionTrack, objectHash, parseLocString, sum, } from '@jbrowse/core/util';
|
|
@@ -594,7 +594,7 @@ async function fitToWindow(session, settleMs, root = document) {
|
|
|
594
594
|
function trackEntry(conf) {
|
|
595
595
|
return {
|
|
596
596
|
trackId: conf.trackId,
|
|
597
|
-
name:
|
|
597
|
+
name: conf.name ?? '',
|
|
598
598
|
type: conf.type,
|
|
599
599
|
// getConfAssemblyNamesOrNone, not the slot: an assembly's sequence track
|
|
600
600
|
// has no assemblyNames slot and answers through its parent assembly
|
|
@@ -826,8 +826,13 @@ async function visibleRegionsOf(session, viewId, preferTrackId, trackAssemblies
|
|
|
826
826
|
// file's own refNames during serialization (RpcMethodTypeWithRenameRegions),
|
|
827
827
|
// and the features come back rebuilt as SimpleFeature. What crosses the worker
|
|
828
828
|
// boundary is bounded by the byte gate that runs first.
|
|
829
|
-
async function fetchFeatures(session, trackId, regions, requestedByteLimit) {
|
|
830
|
-
const
|
|
829
|
+
async function fetchFeatures(pluginManager, session, trackId, regions, requestedByteLimit) {
|
|
830
|
+
const entry = session.getTrackById(trackId);
|
|
831
|
+
// the node a shown track reads: its adapter is in the hydrated form, which is
|
|
832
|
+
// what the shown track's session id hashes
|
|
833
|
+
const conf = isStateTreeNode(entry)
|
|
834
|
+
? entry
|
|
835
|
+
: entry && hydrateTrackConfig(pluginManager, entry);
|
|
831
836
|
if (!conf) {
|
|
832
837
|
throw new Error(`No track with trackId "${trackId}" — jb.listTracks() shows what is available`);
|
|
833
838
|
}
|
|
@@ -1019,7 +1024,7 @@ const JB_HELP = `jb drives this JBrowse app programmatically (window.jb in a bro
|
|
|
1019
1024
|
|
|
1020
1025
|
Orient first: jb.sessionSummary(). Introspect, never guess: jb.listTracks(search?, limit?) answers { total, tracks } with the trackIds; jb.describeSlots(jb.trackModel('someTrackId').activeDisplay.configuration) for the settings keys a display accepts — an unknown settings key is not an error, it lands in applyDisplaySettings' "unapplied" list as { key, reason }, so read the report; jb.inspect(node) — jb.view(), jb.trackModel('someTrackId'), session.views[0] — for its getters, actions and modelType.
|
|
1021
1026
|
|
|
1022
|
-
The model is mobx-state-tree: mutate only through actions (raw assignment throws), and write display settings with track.applyDisplaySettings(settings). A display's grammar settings are its plot, activeDisplay.plot, by slot name: facet ('strand' or { field, domain? }), color (a CSS color or { field: 'type', domain?, range? }), rows, scales, filter (['jexl:feature.score > 10']), and on a mark display marks and transform. Edit structuredClone(activeDisplay.plot), check it with activeDisplay.plotProblems(draft), which throws what a config refuses, and write it with activeDisplay.applyPlot(draft): each setting replaced whole, null resetting one. An alignments track takes the same facet, its field a read dimension ('pairOrientation', 'mapq', ...) or a tag ('tags.HP'). LinearMarkDisplay plots a track's features as ggplot2 does, on a feature, alignments, variant or quantitative track, in seven settings: marks, a list drawn in order of { mark: 'bar' | 'point' | 'rule' | 'line' | 'span' | 'text' | 'link', encoding: { y: 'score', x?, x2?, row?, color?, shape?, text?, size? }, each a feature field or a jexl: expression, a constant written { value: 'red' }, transform?: steps such as { type: 'coverage' } or a bin then an aggregate, which fill the y they write, minBpPerPx?, maxBpPerPx? }, beside transform, facet, rows, rowColor, scales ({ y: { type, domainMin, domainMax, autoscaleGroup, title } }) and filter. await view.launchTrack(trackId, {}, { type: 'LinearMarkDisplay', marks: [...] }) draws a track with it, shown already or not (on a GWASTrack, type 'LinearManhattanDisplay'). activeDisplay.notices names a mark the config cannot draw, or one whose field no loaded feature holds. Build views declaratively with jb.loadSessionSpec({ views: [{ type: 'LinearGenomeView', assembly, loc, tracks: [...] }] }), which replaces the session; jb.addView(oneViewSpec) opens one more view beside what is open; jb.setSession(document) rewrites the session as a document — what jb.mst.getSnapshot(jb.session) answers, edited: a view keeps its id and is patched in place, loc on it navigates, a { trackId } entry in its tracks opens that track. Act on a view with await view.navToLocString('BRCA1' or 'chr1:1-1000'), which also SHOWS the track whose search index answered a gene name unless a 4th arg { showHitTrack: false } says not to; await view.launchTrack(trackId, {}, settings) shows a track with settings, view.hideTrack(trackId) hides it. Arrange open views into panels with session.layoutViews({ direction: 'horizontal', children: [{ views: [viewId] }, ...] }), a leaf naming view ids or session.views indexes; await jb.fitToWindow() shrinks what is open until the session fits the window; add data with jb.addTrack({ location }) (an absolute path or a URL), or show a track the catalog already holds with jb.addTrack({ trackId, settings? }); read data with await jb.getFeatures({ trackId, loc?, assembly?, viewId?, regions?, byteLimit? }), which renames refNames ("chr1" vs "1") so the file answers and reads on the worker the track's display uses. Anything lower level is jb.require('@jbrowse/core/util') and friends, the module registry plugins link against. After changing anything, await jb.waitReady(ms) and read its notifications and notReady lists before trusting the screen.
|
|
1027
|
+
The model is mobx-state-tree: mutate only through actions (raw assignment throws), and write display settings with track.applyDisplaySettings(settings). A display's grammar settings are its plot, activeDisplay.plot, by slot name: facet ('strand' or { field, domain? }), color (a CSS color or { field: 'type', domain?, range? }), rows, unit (what a row stands for: 'read' | 'chain' on alignments, 'sample' | 'haplotype' on multi-sample variants), scales, filter (['jexl:feature.score > 10']), and on a mark display marks and transform. Edit structuredClone(activeDisplay.plot), check it with activeDisplay.plotProblems(draft), which throws what a config refuses, and write it with activeDisplay.applyPlot(draft): each setting replaced whole, null resetting one. An alignments track takes the same facet, its field a read dimension ('pairOrientation', 'mapq', ...) or a tag ('tags.HP'). LinearMarkDisplay plots a track's features as ggplot2 does, on a feature, alignments, variant or quantitative track, in seven settings: marks, a list drawn in order of { mark: 'bar' | 'point' | 'rule' | 'line' | 'span' | 'text' | 'link', encoding: { y: 'score', x?, x2?, row?, color?, shape?, text?, size? }, each a feature field or a jexl: expression, a constant written { value: 'red' }, transform?: steps such as { type: 'coverage' } or a bin then an aggregate, which fill the y they write, minBpPerPx?, maxBpPerPx? }, beside transform, facet, rows, rowColor, scales ({ y: { type, domainMin, domainMax, autoscaleGroup, title } }) and filter. await view.launchTrack(trackId, {}, { type: 'LinearMarkDisplay', marks: [...] }) draws a track with it, shown already or not (on a GWASTrack, type 'LinearManhattanDisplay'). activeDisplay.notices names a mark the config cannot draw, or one whose field no loaded feature holds. Build views declaratively with jb.loadSessionSpec({ views: [{ type: 'LinearGenomeView', assembly, loc, tracks: [...] }] }), which replaces the session; jb.addView(oneViewSpec) opens one more view beside what is open; jb.setSession(document) rewrites the session as a document — what jb.mst.getSnapshot(jb.session) answers, edited: a view keeps its id and is patched in place, loc on it navigates, a { trackId } entry in its tracks opens that track. Act on a view with await view.navToLocString('BRCA1' or 'chr1:1-1000'), which also SHOWS the track whose search index answered a gene name unless a 4th arg { showHitTrack: false } says not to; await view.launchTrack(trackId, {}, settings) shows a track with settings, view.hideTrack(trackId) hides it. Arrange open views into panels with session.layoutViews({ direction: 'horizontal', children: [{ views: [viewId] }, ...] }), a leaf naming view ids or session.views indexes; await jb.fitToWindow() shrinks what is open until the session fits the window; add data with jb.addTrack({ location }) (an absolute path or a URL), or show a track the catalog already holds with jb.addTrack({ trackId, settings? }); read data with await jb.getFeatures({ trackId, loc?, assembly?, viewId?, regions?, byteLimit? }), which renames refNames ("chr1" vs "1") so the file answers and reads on the worker the track's display uses. Anything lower level is jb.require('@jbrowse/core/util') and friends, the module registry plugins link against. After changing anything, await jb.waitReady(ms) and read its notifications and notReady lists before trusting the screen.
|
|
1023
1028
|
|
|
1024
1029
|
Views nest and several can be open. jb.view(viewId?) is the open view, and jb.view(), jb.trackModel(trackId), jb.visibleRegions(), jb.addTrack and jb.getFeatures over a visible region throw naming the candidates rather than picking one when more than one view could answer — pass viewId (from jb.sessionSummary()) to say which. A synteny or breakpoint view's viewId also covers its rows: the named view answers when it can, and otherwise its rows do.
|
|
1025
1030
|
|
|
@@ -1107,7 +1112,7 @@ export function createJbApi(pluginManager, options = {}) {
|
|
|
1107
1112
|
await locToRegion(session, conf, fetchArgs.loc, fetchArgs.assembly),
|
|
1108
1113
|
]
|
|
1109
1114
|
: await visibleRegionsOf(session, fetchArgs.viewId, fetchArgs.trackId, getConfAssemblyNamesOrNone(conf));
|
|
1110
|
-
return fetchFeatures(session, fetchArgs.trackId, regions, fetchArgs.byteLimit);
|
|
1115
|
+
return fetchFeatures(pluginManager, session, fetchArgs.trackId, regions, fetchArgs.byteLimit);
|
|
1111
1116
|
},
|
|
1112
1117
|
};
|
|
1113
1118
|
return jb;
|
|
@@ -1192,9 +1197,6 @@ async function addTrack(pluginManager, session, args) {
|
|
|
1192
1197
|
}
|
|
1193
1198
|
// A track already in the catalog, shown: the same view choice and the same
|
|
1194
1199
|
// settle the file route ends in.
|
|
1195
|
-
// `conf` is whatever `session.getTrackById` answered, which is a track config
|
|
1196
|
-
// but not necessarily a `BaseTrackConfig` — an assembly's ReferenceSequenceTrack
|
|
1197
|
-
// declares a subset of those slots. The four reads below are all ones it has.
|
|
1198
1200
|
async function showCatalogTrack(pluginManager, session, conf, args) {
|
|
1199
1201
|
// index, assembly and name describe the file being added, and the catalog's
|
|
1200
1202
|
// copy already has all three — taking them here would drop them in silence
|
|
@@ -1207,7 +1209,7 @@ async function showCatalogTrack(pluginManager, session, conf, args) {
|
|
|
1207
1209
|
trackType: conf.type,
|
|
1208
1210
|
assembly: getConfAssemblyNamesOrNone(conf)[0],
|
|
1209
1211
|
};
|
|
1210
|
-
const adapterType =
|
|
1212
|
+
const adapterType = conf.adapter?.type;
|
|
1211
1213
|
const summary = { ...track, ...(adapterType ? { adapterType } : {}) };
|
|
1212
1214
|
// show:false over a catalog trackId asks for nothing — there is no file to
|
|
1213
1215
|
// add — and showing it anyway would contradict the flag
|
|
@@ -132,18 +132,8 @@ function unresolvedLayoutRefs(node, viewCount, knownIds) {
|
|
|
132
132
|
: []),
|
|
133
133
|
];
|
|
134
134
|
}
|
|
135
|
-
// A `tabs` node shares one cell between its children
|
|
136
|
-
//
|
|
137
|
-
// left where a stated size is dropped.
|
|
138
|
-
//
|
|
139
|
-
// It used to be far wider — dockview forces its branches to alternate
|
|
140
|
-
// orientation by depth, so a nested container had no branch to size against and
|
|
141
|
-
// the whole sizing pass bailed, top-level numbers included. That is gone
|
|
142
|
-
// (ADR-068): the spec's nesting is the tree's nesting, `size` applies wherever
|
|
143
|
-
// it is written, and a bare sibling takes an equal share of the remainder. The
|
|
144
|
-
// wider check outlived the limitation it reported and told an author their
|
|
145
|
-
// nested or partial sizes had been ignored while the layout honoured them —
|
|
146
|
-
// which is also what website/docs/urlparams.md promises.
|
|
135
|
+
// A `tabs` node shares one cell between its children, so a `size` on one of
|
|
136
|
+
// them describes nothing; it is the only stated size the layout drops.
|
|
147
137
|
function unsizeableLayoutNodes(layout) {
|
|
148
138
|
const children = Array.isArray(layout.children) ? layout.children : [];
|
|
149
139
|
const sizedTabsChild = layout.direction === 'tabs' && children.some(c => c.size !== undefined);
|
|
@@ -2,16 +2,6 @@ import type { WorkspaceLayout } from './model.ts';
|
|
|
2
2
|
import type { PanelChrome } from './panelChrome.ts';
|
|
3
3
|
import type { LayoutTree } from './tree.ts';
|
|
4
4
|
import type { DragState } from './useLayoutDrag.ts';
|
|
5
|
-
/**
|
|
6
|
-
* The layout, rendered. There is no imperative api and no event to listen to:
|
|
7
|
-
* the tree is state, this is a function of it, and a gesture is an action.
|
|
8
|
-
*
|
|
9
|
-
* Sizes are `flex-grow`, which is why there is no resize handling anywhere in
|
|
10
|
-
* here. A branch's children divide its space in proportion to their `size`
|
|
11
|
-
* whatever that space becomes, so a window resize is the browser's problem —
|
|
12
|
-
* this is the layout maths dockview does in pixels, and it is the part of a
|
|
13
|
-
* grid engine most likely to be got subtly wrong.
|
|
14
|
-
*/
|
|
15
5
|
interface Props {
|
|
16
6
|
node: LayoutTree;
|
|
17
7
|
layout: WorkspaceLayout;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment
|
|
2
|
+
import { Fragment } from 'react';
|
|
3
3
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
4
4
|
import { observer } from 'mobx-react';
|
|
5
5
|
import { PanelView } from './PanelView.js';
|
|
6
|
-
import { dv } from './dockviewTheme.js';
|
|
7
6
|
import { pairSpan, withBoundaryAt } from './splitter.js';
|
|
8
7
|
import { isBranch } from './tree.js';
|
|
8
|
+
import { usePointerGesture } from './usePointerGesture.js';
|
|
9
|
+
import { workspaceTheme } from './workspaceTheme.js';
|
|
9
10
|
/**
|
|
10
|
-
* A node's share of its parent's space
|
|
11
|
-
*
|
|
12
|
-
* a wide view pushes its own cell past its share.
|
|
11
|
+
* A node's share of its parent's space, as `flex-grow`, so window resizes need
|
|
12
|
+
* no code. `min*: 0` stops a wide view pushing its cell past its share.
|
|
13
13
|
*/
|
|
14
14
|
function paneStyle(size) {
|
|
15
15
|
return {
|
|
@@ -27,12 +27,7 @@ export const LayoutRenderer = observer(function LayoutRenderer(props) {
|
|
|
27
27
|
}
|
|
28
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
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
|
-
*/
|
|
30
|
+
/** The pixels the two panes either side of a handle occupy. */
|
|
36
31
|
function measurePairPx(handle, index, horizontal) {
|
|
37
32
|
const panes = [...(handle.parentElement?.children ?? [])].filter(el => !Object.hasOwn(el.dataset, 'splitter'));
|
|
38
33
|
const before = panes[index - 1]?.getBoundingClientRect();
|
|
@@ -42,25 +37,21 @@ function measurePairPx(handle, index, horizontal) {
|
|
|
42
37
|
}
|
|
43
38
|
return horizontal ? before.width + after.width : before.height + after.height;
|
|
44
39
|
}
|
|
45
|
-
//
|
|
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.
|
|
40
|
+
// a transparent grab strip with a 1px line drawn down its middle
|
|
48
41
|
const useSplitterStyles = makeStyles()({
|
|
49
42
|
splitter: {
|
|
50
|
-
flex: `0 0 ${
|
|
43
|
+
flex: `0 0 ${workspaceTheme.splitterSize}px`,
|
|
51
44
|
position: 'relative',
|
|
52
45
|
background: 'transparent',
|
|
53
46
|
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
47
|
'&:focus-visible': {
|
|
57
|
-
outline: `2px solid ${
|
|
48
|
+
outline: `2px solid ${workspaceTheme.accent}`,
|
|
58
49
|
outlineOffset: -1,
|
|
59
50
|
},
|
|
60
51
|
'&::before': {
|
|
61
52
|
content: '""',
|
|
62
53
|
position: 'absolute',
|
|
63
|
-
background:
|
|
54
|
+
background: workspaceTheme.splitterLine,
|
|
64
55
|
},
|
|
65
56
|
},
|
|
66
57
|
horizontal: {
|
|
@@ -71,86 +62,35 @@ const useSplitterStyles = makeStyles()({
|
|
|
71
62
|
},
|
|
72
63
|
});
|
|
73
64
|
/**
|
|
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.
|
|
65
|
+
* Drags the boundary between children `index - 1` and `index`; `splitter.ts`
|
|
66
|
+
* decides where it may land.
|
|
79
67
|
*/
|
|
80
68
|
const Splitter = observer(function Splitter({ branch, index, layout, }) {
|
|
81
69
|
const { classes, cx } = useSplitterStyles();
|
|
82
|
-
const dragRef = useRef(undefined);
|
|
83
70
|
const horizontal = branch.direction === 'row';
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
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) => {
|
|
71
|
+
const gesture = usePointerGesture({
|
|
72
|
+
start(event) {
|
|
73
|
+
const pairPx = measurePairPx(event.currentTarget, index, horizontal);
|
|
74
|
+
return pairPx > 0
|
|
75
|
+
? {
|
|
76
|
+
axis: horizontal ? 'clientX' : 'clientY',
|
|
77
|
+
start: horizontal ? event.clientX : event.clientY,
|
|
78
|
+
pairPx,
|
|
79
|
+
startSizes: branch.children.map(c => c.size),
|
|
80
|
+
}
|
|
81
|
+
: undefined;
|
|
82
|
+
},
|
|
83
|
+
move(drag, event) {
|
|
84
|
+
const delta = (event[drag.axis] - drag.start) / drag.pairPx;
|
|
85
|
+
const sizes = drag.startSizes;
|
|
86
|
+
layout.setSizes(branch.id, withBoundaryAt(sizes, index, sizes[index - 1] + delta * pairSpan(sizes, index), drag.pairPx));
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const onKeyDown = (event) => {
|
|
148
90
|
const sizes = branch.children.map(c => c.size);
|
|
149
91
|
const pair = pairSpan(sizes, index);
|
|
150
92
|
const decrease = horizontal ? 'ArrowLeft' : 'ArrowUp';
|
|
151
93
|
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
94
|
const step = pair * 0.02;
|
|
155
95
|
let moved;
|
|
156
96
|
if (event.key === decrease) {
|
|
@@ -169,17 +109,10 @@ const Splitter = observer(function Splitter({ branch, index, layout, }) {
|
|
|
169
109
|
return;
|
|
170
110
|
}
|
|
171
111
|
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
112
|
layout.setSizes(branch.id, withBoundaryAt(sizes, index, moved, measurePairPx(event.currentTarget, index, horizontal)));
|
|
175
|
-
}
|
|
176
|
-
// the pane
|
|
177
|
-
// is what the handle actually moves
|
|
113
|
+
};
|
|
114
|
+
// the pane before the handle, as a percentage of the pair it divides
|
|
178
115
|
const sizes = branch.children.map(c => c.size);
|
|
179
116
|
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' } }));
|
|
117
|
+
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: gesture.onPointerDown, onPointerMove: gesture.onPointerMove, onPointerUp: gesture.onPointerUp, onPointerCancel: gesture.onPointerCancel, onLostPointerCapture: gesture.onLostPointerCapture, className: cx(classes.splitter, horizontal ? classes.horizontal : classes.vertical), style: { cursor: horizontal ? 'ew-resize' : 'ns-resize' } }));
|
|
185
118
|
});
|
|
@@ -4,28 +4,13 @@ import { useScrollPortHeightVar } from '@jbrowse/core/util/hooks';
|
|
|
4
4
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
5
5
|
import { observer } from 'mobx-react';
|
|
6
6
|
import { TabStrip } from './TabStrip.js';
|
|
7
|
-
import { dv } from './dockviewTheme.js';
|
|
8
7
|
import { indicatorRect } from './dropZone.js';
|
|
9
8
|
import { tabDomId, tabPanelDomId } from './panelChrome.js';
|
|
10
9
|
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
|
-
*/
|
|
10
|
+
import { workspaceTheme } from './workspaceTheme.js';
|
|
23
11
|
const useStyles = makeStyles()(theme => ({
|
|
24
|
-
// `flex: 1` and `minWidth: 0`
|
|
25
|
-
//
|
|
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.
|
|
12
|
+
// without `flex: 1` and `minWidth: 0` the cell takes its content's width, and
|
|
13
|
+
// a view sizes itself to its container, so both settle too narrow
|
|
29
14
|
panel: {
|
|
30
15
|
position: 'relative',
|
|
31
16
|
display: 'flex',
|
|
@@ -33,9 +18,7 @@ const useStyles = makeStyles()(theme => ({
|
|
|
33
18
|
flex: 1,
|
|
34
19
|
minWidth: 0,
|
|
35
20
|
minHeight: 0,
|
|
36
|
-
// the
|
|
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.
|
|
21
|
+
// below the strip the cell follows the app theme, not `workspaceTheme`
|
|
39
22
|
background: theme.palette.background.default,
|
|
40
23
|
},
|
|
41
24
|
content: {
|
|
@@ -48,51 +31,42 @@ const useStyles = makeStyles()(theme => ({
|
|
|
48
31
|
indicator: {
|
|
49
32
|
position: 'absolute',
|
|
50
33
|
pointerEvents: 'none',
|
|
51
|
-
//
|
|
52
|
-
// position:relative, so an app-wide z-index would let it cover menus
|
|
34
|
+
// local to the position:relative panel, so it cannot cover menus
|
|
53
35
|
zIndex: 1,
|
|
54
|
-
background:
|
|
55
|
-
outline: `1px solid ${
|
|
36
|
+
background: workspaceTheme.dropWash,
|
|
37
|
+
outline: `1px solid ${workspaceTheme.accent}`,
|
|
56
38
|
},
|
|
57
|
-
// where a tab dragged onto the strip would land
|
|
58
39
|
caret: {
|
|
59
40
|
position: 'absolute',
|
|
60
41
|
pointerEvents: 'none',
|
|
61
42
|
zIndex: 1,
|
|
62
43
|
top: 0,
|
|
63
44
|
width: 2,
|
|
64
|
-
height:
|
|
65
|
-
background:
|
|
45
|
+
height: workspaceTheme.stripHeight,
|
|
46
|
+
background: workspaceTheme.accent,
|
|
66
47
|
},
|
|
67
48
|
}));
|
|
68
49
|
export const PanelView = observer(function PanelView({ panel, layout, chrome, drop, }) {
|
|
69
50
|
const { classes } = useStyles();
|
|
70
51
|
const active = activeTabIn(panel);
|
|
71
|
-
const contentRef = useScrollPortHeightVar();
|
|
72
52
|
return (_jsxs("div", { "data-panel-id": panel.id, className: classes.panel, onPointerDownCapture: () => {
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// control inside the view.
|
|
53
|
+
// capture phase, so a control inside the view that consumes the click
|
|
54
|
+
// still activates the cell
|
|
76
55
|
if (layout.activePanelId !== panel.id) {
|
|
77
56
|
layout.setActivePanelId(panel.id);
|
|
78
57
|
}
|
|
79
|
-
}, children: [_jsx(TabStrip, { panel: panel, layout: layout, chrome: chrome, active: active,
|
|
58
|
+
}, children: [layout.panels.length > 1 || panel.tabs.length > 1 ? (_jsx(TabStrip, { panel: panel, layout: layout, chrome: chrome, active: active, cellActive: layout.activePanelId === panel.id })) : null, active ? (_jsx(TabPanel, { panelId: panel.id, tab: active, chrome: chrome, className: classes.content }, active.id)) : (_jsx("div", { role: "tabpanel", className: classes.content })), _jsx(DropIndicator, { drop: drop, classes: classes })] }));
|
|
80
59
|
});
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*/
|
|
60
|
+
// keyed by tab, so a switch gets a fresh ViewStack and scroll port
|
|
61
|
+
const TabPanel = observer(function TabPanel({ panelId, tab, chrome, className, }) {
|
|
62
|
+
const ref = useScrollPortHeightVar();
|
|
63
|
+
return (_jsx("div", { role: "tabpanel", id: tabPanelDomId(panelId), "aria-labelledby": tabDomId(tab.id), className: className, ref: ref, children: chrome.renderTabContent(tab) }));
|
|
64
|
+
});
|
|
65
|
+
/** Where a drop would land: a caret at a strip gap, or a wash over a zone. */
|
|
88
66
|
const DropIndicator = observer(function DropIndicator({ drop, classes, }) {
|
|
89
67
|
if (drop?.strip) {
|
|
90
68
|
return (_jsx("div", { "data-drop-caret": drop.strip.index, className: classes.caret,
|
|
91
|
-
//
|
|
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.
|
|
69
|
+
// a scrolled strip's first gap is left of the panel, which does not clip
|
|
96
70
|
style: { left: Math.max(0, drop.strip.left) } }));
|
|
97
71
|
}
|
|
98
72
|
return drop ? (_jsx("div", { "data-drop-indicator": drop.zone, className: classes.indicator, style: indicatorRect(drop.zone) })) : null;
|
|
@@ -8,7 +8,7 @@ interface TabStripProps {
|
|
|
8
8
|
/** the tab this panel is showing — the strip does not decide it */
|
|
9
9
|
active: TabNode | undefined;
|
|
10
10
|
/** whether this is the cell a newly launched view lands in */
|
|
11
|
-
|
|
11
|
+
cellActive: boolean;
|
|
12
12
|
}
|
|
13
|
-
export declare const TabStrip: ({ panel, layout, chrome, active,
|
|
13
|
+
export declare const TabStrip: ({ panel, layout, chrome, active, cellActive, }: TabStripProps) => import("react").JSX.Element;
|
|
14
14
|
export {};
|