@jbrowse/plugin-linear-genome-view 5.0.0-beta.19 → 5.0.0-beta.20

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.
Files changed (47) hide show
  1. package/esm/LinearGenomeView/afterAttach.d.ts +17 -0
  2. package/esm/LinearGenomeView/afterAttach.js +71 -43
  3. package/esm/LinearGenomeView/components/CenterLine.js +6 -3
  4. package/esm/LinearGenomeView/components/ConnectedHoverHighlight.js +2 -3
  5. package/esm/LinearGenomeView/components/ExportSvgDialog.js +12 -3
  6. package/esm/LinearGenomeView/components/Header.js +1 -1
  7. package/esm/LinearGenomeView/components/HeaderZoomControls.js +3 -2
  8. package/esm/LinearGenomeView/components/HighlightBand.js +7 -7
  9. package/esm/LinearGenomeView/components/ImportForm.js +3 -3
  10. package/esm/LinearGenomeView/components/MiniControls.js +1 -1
  11. package/esm/LinearGenomeView/components/OverviewRubberband.js +5 -3
  12. package/esm/LinearGenomeView/components/ResizeAllTracksHandle.js +7 -4
  13. package/esm/LinearGenomeView/components/SVGHighlightBand.d.ts +10 -1
  14. package/esm/LinearGenomeView/components/SVGHighlightBand.js +15 -3
  15. package/esm/LinearGenomeView/components/SearchBox.js +3 -2
  16. package/esm/LinearGenomeView/components/SearchResultsTable.js +2 -2
  17. package/esm/LinearGenomeView/components/TrackContainer.js +1 -1
  18. package/esm/LinearGenomeView/components/TrackLabelDragHandle.js +2 -3
  19. package/esm/LinearGenomeView/components/fetchSequence.js +15 -7
  20. package/esm/LinearGenomeView/components/useRangeSelect.js +5 -2
  21. package/esm/LinearGenomeView/menuItems.js +5 -5
  22. package/esm/LinearGenomeView/model.d.ts +52 -40
  23. package/esm/LinearGenomeView/model.js +71 -50
  24. package/esm/LinearGenomeView/rExport/exportLinearView.d.ts +4 -0
  25. package/esm/LinearGenomeView/rExport/exportLinearView.js +26 -6
  26. package/esm/LinearGenomeView/rExport/rScript.d.ts +19 -9
  27. package/esm/LinearGenomeView/rExport/rScript.js +91 -53
  28. package/esm/LinearGenomeView/showRegionsWithUndo.js +5 -3
  29. package/esm/LinearGenomeView/svgcomponents/SVGHighlights.d.ts +3 -1
  30. package/esm/LinearGenomeView/svgcomponents/SVGHighlights.js +2 -2
  31. package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.d.ts +3 -1
  32. package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.js +2 -2
  33. package/esm/LinearGenomeView/svgcomponents/SVGView.js +15 -1
  34. package/esm/LinearGenomeView/svgcomponents/util.d.ts +1 -0
  35. package/esm/LinearGenomeView/types.d.ts +6 -0
  36. package/esm/LinearGenomeView/types.js +0 -1
  37. package/esm/LinearGenomeView/util.js +9 -9
  38. package/esm/MultiLevelRubberband/useRangeSelect.js +14 -12
  39. package/esm/TrackModifiers/index.d.ts +4 -1
  40. package/esm/TrackModifiers/index.js +26 -7
  41. package/esm/displayKitTests/perRegionTestEnv.d.ts +2 -0
  42. package/esm/index.d.ts +2 -2
  43. package/esm/index.js +1 -1
  44. package/esm/searchUtils.d.ts +1 -3
  45. package/esm/searchUtils.js +33 -44
  46. package/esm/shared/useWindowDrag.js +11 -9
  47. package/package.json +7 -7
@@ -1,6 +1,23 @@
1
1
  import type { LinearGenomeViewModel } from './model.ts';
2
2
  import type { InitState } from './types.ts';
3
3
  import type { AbstractSessionModel, AssemblyHost, NotificationSink } from '@jbrowse/core/util';
4
+ /**
5
+ * Navigate a view to an init blob's `loc`, reporting a bad one as a warning
6
+ * (a gene that matched nothing) or an error rather than throwing. Exported
7
+ * because a LinearSyntenyView row is a LinearGenomeView that does not go
8
+ * through this file's init autorun, so it navigates each row itself.
9
+ */
10
+ export declare function navToInitLoc(view: Pick<LinearGenomeViewModel, 'navToLocString'>, session: NotificationSink, init: Pick<InitState, 'assembly' | 'grow' | 'showHitTrack' | 'tracks'> & {
11
+ loc: string;
12
+ }): Promise<void>;
13
+ /**
14
+ * Open an init blob's `tracks`, one at a time, a bare string taken as a list
15
+ * of one. launchTrack funnels through showTrackGeneric, which surfaces any
16
+ * failure (unresolved id, bad config) as its own snackbar, and loads the
17
+ * picked display's state model first when it is registered lazily. Exported
18
+ * for the LinearSyntenyView rows, as `navToInitLoc` is.
19
+ */
20
+ export declare function showInitTracks(self: LinearGenomeViewModel, init: Pick<InitState, 'tracks'>): Promise<void>;
4
21
  /**
5
22
  * Add an init blob's `highlight` entries to the session, coercing each
6
23
  * locstring or wire-format object and reporting a bad one without taking out
@@ -2,10 +2,9 @@ import { getSession, isSessionModelWithWidgets, resolveNamedRegions, } from '@jb
2
2
  import { coerceHighlight } from '@jbrowse/core/util/highlights';
3
3
  import { installInitAutorun } from '@jbrowse/core/util/installInitAutorun';
4
4
  import { normalizeTrackInit, warmTrackDisplayGeneric, } from '@jbrowse/core/util/tracks';
5
- import { requestedDisplayType } from '@jbrowse/launch-args/displaySettings';
5
+ import { requestedDisplayType } from '@jbrowse/launch-args/displayAlias';
6
6
  import { addDisposer } from '@jbrowse/mobx-state-tree';
7
7
  import { autorun, untracked, when } from 'mobx';
8
- import { launchTrackInit } from '../TrackModifiers/index.js';
9
8
  import { SearchResultsNotFoundError } from '../searchUtils.js';
10
9
  // `init` is a frozen blob that often comes from hand-authored JSON, where a
11
10
  // lone entry gets written bare (`tracks: 'genes'`). A string is iterable, so
@@ -83,53 +82,82 @@ function showNamedRegions(self, session, assemblyName, names) {
83
82
  }
84
83
  }
85
84
  }
86
- async function navigateInit(self, session, init) {
85
+ function reportInitNavError(session, e) {
86
+ console.error(e);
87
+ if (e instanceof SearchResultsNotFoundError) {
88
+ // a gene name that matched nothing is a soft miss, not an app error
89
+ session.notify(e.message, 'warning');
90
+ }
91
+ else {
92
+ session.notifyError(`${e}`, e);
93
+ }
94
+ }
95
+ /**
96
+ * Navigate a view to an init blob's `loc`, reporting a bad one as a warning
97
+ * (a gene that matched nothing) or an error rather than throwing. Exported
98
+ * because a LinearSyntenyView row is a LinearGenomeView that does not go
99
+ * through this file's init autorun, so it navigates each row itself.
100
+ */
101
+ export async function navToInitLoc(view, session, init) {
87
102
  try {
88
- if (init.loc) {
89
- // navToLocString waits for the assembly itself, and this autorun only
90
- // runs once `initialized` confirms init.assembly has loaded regions, so
91
- // no explicit waitForAssembly is needed here
92
- // a spec that listed its tracks gets those tracks; one that only named
93
- // a gene gets the track the gene was found in, as the search box would
94
- await self.navToLocString(init.loc, init.assembly, init.grow, {
95
- showHitTrack: init.showHitTrack ?? asArray(init.tracks).length === 0,
96
- });
97
- }
98
- else if (init.displayedRegionNames?.length) {
99
- // an explicit region list is a navigation request just like `loc`, so it
100
- // applies even when regions already exist (URL params layered onto a
101
- // defaultSession that already navigated)
102
- //
103
- // `?.length`, not the bare key: an empty array is truthy, so a spec
104
- // writing `displayedRegionNames: []` took this path with nothing to name
105
- // and got told its list of no names had matched no regions. That is why
106
- // LinearSyntenyView's doSubmit omits the key rather than passing [].
107
- showNamedRegions(self, session, init.assembly, init.displayedRegionNames);
108
- }
109
- else if (!self.hasDisplayedRegions) {
110
- // a highlight-only init (nothing to navigate to) must not clobber a
111
- // defaultSession's existing navigation, so only auto-navigate when
112
- // nothing is shown yet
113
- self.showAllRegionsInAssembly(init.assembly);
114
- }
103
+ // navToLocString waits for the assembly itself. A spec that listed its
104
+ // tracks gets those tracks; one that only named a gene gets the track the
105
+ // gene was found in, as the search box would
106
+ await view.navToLocString(init.loc, init.assembly, init.grow, {
107
+ showHitTrack: init.showHitTrack ?? asArray(init.tracks).length === 0,
108
+ });
115
109
  }
116
110
  catch (e) {
117
- console.error(init, e);
118
- if (e instanceof SearchResultsNotFoundError) {
119
- // a &loc= gene name that matched nothing is a soft miss, not an app error
120
- session.notify(e.message, 'warning');
111
+ reportInitNavError(session, e);
112
+ }
113
+ }
114
+ async function navigateInit(self, session, init) {
115
+ if (init.loc) {
116
+ await navToInitLoc(self, session, { ...init, loc: init.loc });
117
+ }
118
+ else {
119
+ try {
120
+ navigateWithoutLoc(self, session, init);
121
121
  }
122
- else {
123
- session.notifyError(`${e}`, e);
122
+ catch (e) {
123
+ reportInitNavError(session, e);
124
124
  }
125
125
  }
126
126
  }
127
- // launchTrack funnels through showTrackGeneric, which surfaces any failure
128
- // (unresolved id, bad config, etc) as its own snackbar, and loads the picked
129
- // display's state model first when it is registered lazily
130
- async function showInitTracks(self, init) {
131
- for (const t of asArray(init.tracks)) {
132
- await launchTrackInit(self, t);
127
+ function navigateWithoutLoc(self, session, init) {
128
+ if (init.displayedRegionNames?.length) {
129
+ // an explicit region list is a navigation request just like `loc`, so it
130
+ // applies even when regions already exist (URL params layered onto a
131
+ // defaultSession that already navigated)
132
+ //
133
+ // `?.length`, not the bare key: an empty array is truthy, so a spec
134
+ // writing `displayedRegionNames: []` took this path with nothing to name
135
+ // and got told its list of no names had matched no regions. That is why
136
+ // LinearSyntenyView's doSubmit omits the key rather than passing [].
137
+ showNamedRegions(self, session, init.assembly, init.displayedRegionNames);
138
+ }
139
+ else if (!self.hasDisplayedRegions) {
140
+ // a highlight-only init (nothing to navigate to) must not clobber a
141
+ // defaultSession's existing navigation, so only auto-navigate when
142
+ // nothing is shown yet
143
+ self.showAllRegionsInAssembly(init.assembly);
144
+ }
145
+ }
146
+ /**
147
+ * Open an init blob's `tracks`, one at a time, a bare string taken as a list
148
+ * of one. launchTrack funnels through showTrackGeneric, which surfaces any
149
+ * failure (unresolved id, bad config) as its own snackbar, and loads the
150
+ * picked display's state model first when it is registered lazily. Exported
151
+ * for the LinearSyntenyView rows, as `navToInitLoc` is.
152
+ */
153
+ export async function showInitTracks(self, init) {
154
+ const tracks = asArray(init.tracks);
155
+ if (tracks.length) {
156
+ // lazy, so the modifier and slot tables stay out of every page's first load
157
+ const { launchTrackInit } = await import('../TrackModifiers/index.js');
158
+ for (const t of tracks) {
159
+ await launchTrackInit(self, t);
160
+ }
133
161
  }
134
162
  }
135
163
  /**
@@ -147,7 +175,7 @@ export function applyInitHighlights(session, init) {
147
175
  // parseLocString throws on an unknown refName or a malformed locstring, and
148
176
  // one bad entry must take out neither its siblings nor the rest of init
149
177
  try {
150
- const highlight = coerceHighlight(h, init.assembly, refName => session.assemblyManager.isValidRefName(refName, init.assembly));
178
+ const highlight = coerceHighlight(h, init.assembly, (refName, asm) => session.assemblyManager.isValidRefName(refName, asm ?? init.assembly));
151
179
  if (highlight) {
152
180
  session.addHighlight(highlight);
153
181
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { clamp } from '@jbrowse/core/util';
3
- import { basePaintedAt } from '@jbrowse/core/util/Base1DUtils';
3
+ import { basePaintedAt, bpIntoPaintedBase, } from '@jbrowse/core/util/Base1DUtils';
4
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
5
  import CloseIcon from '@mui/icons-material/Close';
6
6
  import { IconButton, Tooltip } from '@mui/material';
@@ -36,9 +36,12 @@ const useStyles = makeStyles()(theme => ({
36
36
  const CenterLine = observer(function CenterLine({ model }) {
37
37
  const { bpPerPx, centerLineInfo, tracks, width } = model;
38
38
  const { classes } = useStyles();
39
- const startingPosition = width / 2;
39
+ const left = centerLineInfo && bpPerPx < 1
40
+ ? width / 2 -
41
+ bpIntoPaintedBase(centerLineInfo, centerLineInfo.offset) / bpPerPx
42
+ : width / 2;
40
43
  return tracks.length ? (_jsx("div", { "data-testid": "centerline_container", className: classes.centerLineContainer, role: "presentation", style: {
41
- transform: `translateX(${startingPosition}px)`,
44
+ transform: `translateX(${left}px)`,
42
45
  width: Math.max(1 / bpPerPx, 1),
43
46
  }, children: centerLineInfo && (_jsxs("div", { "data-testid": "centerline_text", className: classes.centerLineText, role: "presentation", style: {
44
47
  transform: `translateX(${Math.max(1 / bpPerPx, 1) + 5}px)`,
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- import { getSession } from '@jbrowse/core/util';
2
+ import { getSession, isSessionModelWithWidgets } from '@jbrowse/core/util';
3
3
  import { observer } from 'mobx-react';
4
4
  import HoverPositionHighlight from './HoverPositionHighlight.js';
5
5
  // Draws a crosshair for every session widget "connected" to this view that is
@@ -9,8 +9,7 @@ import HoverPositionHighlight from './HoverPositionHighlight.js';
9
9
  // hovered interval or undefined; the widget iteration and drawing are shared.
10
10
  const ConnectedHoverHighlight = observer(function ConnectedHoverHighlight({ model, getPosition, }) {
11
11
  const session = getSession(model);
12
- const widgets = 'widgets' in session ? session.widgets : undefined;
13
- return !widgets ? null : (_jsx(_Fragment, { children: [...widgets].flatMap(([id, widget]) => {
12
+ return !isSessionModelWithWidgets(session) ? null : (_jsx(_Fragment, { children: [...session.widgets].flatMap(([id, widget]) => {
14
13
  const position = getPosition(widget, model.id);
15
14
  return position
16
15
  ? [
@@ -5,6 +5,12 @@ import { MenuItem, TextField } from '@mui/material';
5
5
  import { observer } from 'mobx-react';
6
6
  const MIN_WIDTH = 100;
7
7
  const MAX_WIDTH = 10000;
8
+ const TRACK_LABEL_MODES = [
9
+ { mode: 'offset', label: 'Offset' },
10
+ { mode: 'overlapping', label: 'Overlapping' },
11
+ { mode: 'left', label: 'Left' },
12
+ { mode: 'hidden', label: 'Hidden' },
13
+ ];
8
14
  // Shared track-label + gridlines export dialog. Used by LGV, linear-synteny and
9
15
  // breakpoint-split views (their lazyDialogs re-export this). Only a caller
10
16
  // passing `chooseWidth` offers the width field, whose model lays itself out at
@@ -29,9 +35,12 @@ const ExportSvgDialog = observer(function ExportSvgDialog({ model, handleClose,
29
35
  viewWidth: chooseWidth && widthValid ? typedWidth : undefined,
30
36
  }), checkboxes: _jsx(LabeledCheckbox, { checked: showGridlines, onChange: val => {
31
37
  setShowGridlines(val);
32
- }, label: "Show gridlines" }), children: [_jsxs(TextField, { select: true, label: "Track label positioning", variant: "outlined", style: { minWidth: 200 }, value: trackLabels, onChange: event => {
33
- setTrackLabels(event.target.value);
34
- }, children: [_jsx(MenuItem, { value: "offset", children: "Offset" }), _jsx(MenuItem, { value: "overlapping", children: "Overlapping" }), _jsx(MenuItem, { value: "left", children: "Left" }), _jsx(MenuItem, { value: "hidden", children: "Hidden" })] }), chooseWidth ? (_jsx(TextField, { label: "Width (px)", variant: "outlined", type: "number", value: widthText, error: !widthValid, helperText: widthValid
38
+ }, label: "Show gridlines" }), children: [_jsx(TextField, { select: true, label: "Track label positioning", variant: "outlined", style: { minWidth: 200 }, value: trackLabels, onChange: event => {
39
+ const picked = TRACK_LABEL_MODES.find(m => m.mode === event.target.value);
40
+ if (picked) {
41
+ setTrackLabels(picked.mode);
42
+ }
43
+ }, children: TRACK_LABEL_MODES.map(({ mode, label }) => (_jsx(MenuItem, { value: mode, children: label }, mode))) }), chooseWidth ? (_jsx(TextField, { label: "Width (px)", variant: "outlined", type: "number", value: widthText, error: !widthValid, helperText: widthValid
35
44
  ? 'Lays the tracks out as a window this wide would; text keeps its size'
36
45
  : `A whole number from ${MIN_WIDTH} to ${MAX_WIDTH}`, slotProps: {
37
46
  htmlInput: { min: MIN_WIDTH, max: MAX_WIDTH, step: 1 },
@@ -50,7 +50,7 @@ const Controls = observer(function Controls({ model, }) {
50
50
  viewMenu,
51
51
  touch,
52
52
  });
53
- return (_jsxs("div", { className: classes.headerBar, ref: ref, children: [viewMenu ? (_jsx(CascadingMenuButton, { menuItems: () => model.menuItems(), "data-testid": "view_menu_icon", children: _jsx(MenuIcon, {}) })) : null, _jsx(HeaderTrackSelectorButton, { model: model, indent: fit.trackSelectorIndent }), touch ? null : (_jsx(ScrollZoomToggle, { model: model, iconOnly: !fit.scrollZoomLabel })), _jsx("div", { className: classes.spacer }), touch ? null : (_jsx(HeaderPanControls, { model: model, compact: !fit.panButtonSpacing })), _jsx(SearchBox, { model: model }), _jsx(HeaderClearHighlightButton, { highlighted: highlighted }), fit.regionWidth ? _jsx(HeaderRegionWidth, { model: model }) : null, _jsx(HeaderZoomControls, { model: model, showSlider: fit.zoomSlider }), _jsx("div", { className: classes.spacer })] }));
53
+ return (_jsxs("div", { className: classes.headerBar, ref: ref, children: [viewMenu ? (_jsx(CascadingMenuButton, { tooltip: "View menu", menuItems: () => model.menuItems(), "data-testid": "view_menu_icon", children: _jsx(MenuIcon, {}) })) : null, _jsx(HeaderTrackSelectorButton, { model: model, indent: fit.trackSelectorIndent }), touch ? null : (_jsx(ScrollZoomToggle, { model: model, iconOnly: !fit.scrollZoomLabel })), _jsx("div", { className: classes.spacer }), touch ? null : (_jsx(HeaderPanControls, { model: model, compact: !fit.panButtonSpacing })), _jsx(SearchBox, { model: model }), _jsx(HeaderClearHighlightButton, { highlighted: highlighted }), fit.regionWidth ? _jsx(HeaderRegionWidth, { model: model }) : null, _jsx(HeaderZoomControls, { model: model, showSlider: fit.zoomSlider }), _jsx("div", { className: classes.spacer })] }));
54
54
  });
55
55
  const LinearGenomeViewHeader = observer(function LinearGenomeViewHeader({ model, }) {
56
56
  const { hideHeader, hideHeaderOverview } = model;
@@ -37,11 +37,12 @@ const ZoomSlider = observer(function ZoomSlider({ model, }) {
37
37
  const { width, maxBpPerPx, minBpPerPx, bpPerPx } = model;
38
38
  const [dragValue, setDragValue] = useState(null);
39
39
  const value = dragValue ?? -Math.log2(bpPerPx) * 100;
40
+ const windowSize = (sliderValue) => `Window size: ${getBpDisplayStr(2 ** (-sliderValue / 100) * width)}`;
40
41
  return (_jsx(SingleSlider, { size: "small", className: classes.slider, value: value, min: -Math.log2(maxBpPerPx) * 100, max: -Math.log2(minBpPerPx) * 100, onChangeCommitted: val => {
41
42
  setDragValue(null);
42
43
  model.cancelZoomAnimation();
43
44
  model.zoomTo(2 ** (-val / 100));
44
- }, valueLabelDisplay: "auto", valueLabelFormat: newValue => `Window size: ${getBpDisplayStr(2 ** (-newValue / 100) * width)}`, onChange: val => {
45
+ }, valueLabelDisplay: "auto", "aria-label": "Zoom level", valueLabelFormat: newValue => windowSize(newValue), getAriaValueText: newValue => windowSize(newValue), onChange: val => {
45
46
  // Take over from any in-flight animated zoom as soon as the user grabs
46
47
  // the thumb, so the view stops lurching underneath the drag.
47
48
  if (dragValue === null) {
@@ -52,6 +53,6 @@ const ZoomSlider = observer(function ZoomSlider({ model, }) {
52
53
  });
53
54
  const HeaderZoomControls = observer(function HeaderZoomControls({ model, showSlider = true, }) {
54
55
  const { classes } = useStyles();
55
- return (_jsxs("div", { className: classes.container, children: [_jsx(ZoomButton, { model: model, direction: "out" }), showSlider ? _jsx(ZoomSlider, { model: model }) : null, _jsx(ZoomButton, { model: model, direction: "in" }), _jsx(CascadingMenuButton, { menuItems: () => zoomMenuItems(model), children: _jsx(MoreVert, {}) })] }));
56
+ return (_jsxs("div", { className: classes.container, children: [_jsx(ZoomButton, { model: model, direction: "out" }), showSlider ? _jsx(ZoomSlider, { model: model }) : null, _jsx(ZoomButton, { model: model, direction: "in" }), _jsx(CascadingMenuButton, { tooltip: "Zoom options", menuItems: () => zoomMenuItems(model), children: _jsx(MoreVert, {}) })] }));
56
57
  });
57
58
  export default HeaderZoomControls;
@@ -39,21 +39,21 @@ export function gapMask(gaps) {
39
39
  ]);
40
40
  return `linear-gradient(to bottom, ${stops.join(', ')})`;
41
41
  }
42
- // OverviewHighlight stays separate (different bpToPx API). A non-interactive
43
- // colored band with an optional top label; when `children` (the highlight
44
- // chip) is passed it renders that instead of the plain label. Visibility is
45
- // toggled from the view menu
42
+ // A colored band with an optional top label; when `children` (the highlight
43
+ // chip) is passed it renders that instead of the plain label. Clicks fall
44
+ // through it to the tracks, which is what keeps it apart from the hoverable
45
+ // OverviewHighlightBand. Visibility is toggled from the view menu
46
46
  const HighlightBand = observer(function HighlightBand({ coords, background, label, gaps, children, }) {
47
47
  const { classes } = useStyles();
48
+ const mask = gaps?.length ? gapMask(gaps) : undefined;
48
49
  const content = children ??
49
50
  (label ? (_jsx(Typography, { variant: "caption", className: classes.label, noWrap: true, children: label })) : null);
50
51
  return (_jsxs(_Fragment, { children: [_jsx("div", { "data-testid": "highlight-band", className: classes.highlight, style: {
51
52
  transform: `translateX(${coords.left}px)`,
52
53
  width: coords.width,
53
54
  background,
54
- ...(gaps?.length
55
- ? { maskImage: gapMask(gaps), WebkitMaskImage: gapMask(gaps) }
56
- : undefined),
55
+ maskImage: mask,
56
+ WebkitMaskImage: mask,
57
57
  } }), content ? (_jsx("div", { className: classes.overlay, style: { transform: `translateX(${coords.left}px)` }, children: content })) : null] }));
58
58
  });
59
59
  export default HighlightBand;
@@ -44,12 +44,12 @@ const LinearGenomeViewImportForm = observer(function LinearGenomeViewImportForm(
44
44
  model.setError(undefined);
45
45
  if (selectedAsm) {
46
46
  try {
47
- await navigateToSelectedOption({
47
+ const moved = await navigateToSelectedOption({
48
48
  option,
49
49
  model,
50
50
  assemblyName: selectedAsm,
51
51
  });
52
- if (record) {
52
+ if (record && moved) {
53
53
  addRecentLocation(recentLocationOf(option));
54
54
  }
55
55
  }
@@ -61,7 +61,7 @@ const LinearGenomeViewImportForm = observer(function LinearGenomeViewImportForm(
61
61
  const recentMenuItems = recentLocationsMenu({
62
62
  recentLocations,
63
63
  onNavigate: option => {
64
- navigate({ option, record: true }).catch(() => { });
64
+ void navigate({ option, record: true });
65
65
  },
66
66
  onClear: clearRecentLocations,
67
67
  });
@@ -33,7 +33,7 @@ const MiniControls = observer(function MiniControls({ model, }) {
33
33
  ? { top: model.scalebarHeight, transform: 'translateY(-100%)' }
34
34
  : undefined, children: _jsxs(Paper, { className: focusedViewId === id ? classes.focusedBackground : undefined, children: [_jsx(Tooltip, { title: "Open track selector", children: _jsx(IconButton, { size: "small", onClick: () => {
35
35
  model.activateTrackSelector();
36
- }, children: _jsx(TrackSelectorIcon, { fontSize: "small" }) }) }), _jsx(CascadingMenuButton, { menuItems: () => model.menuItems(), children: _jsx(MoreVertIcon, { fontSize: "small" }) }), _jsx(Tooltip, { title: scalebarOnly ? 'Show tracks' : 'Show ruler only', children: _jsx(IconButton, { size: "small", onClick: () => {
36
+ }, children: _jsx(TrackSelectorIcon, { fontSize: "small" }) }) }), _jsx(CascadingMenuButton, { tooltip: "View menu", menuItems: () => model.menuItems(), children: _jsx(MoreVertIcon, { fontSize: "small" }) }), _jsx(Tooltip, { title: scalebarOnly ? 'Show tracks' : 'Show ruler only', children: _jsx(IconButton, { size: "small", onClick: () => {
37
37
  model.setScalebarOnly(!scalebarOnly);
38
38
  }, children: scalebarOnly ? (_jsx(AddIcon, { fontSize: "small" })) : (_jsx(RemoveIcon, { fontSize: "small" })) }) }), _jsx(ZoomButton, { model: model, direction: "out", small: true }), _jsx(ZoomButton, { model: model, direction: "in", small: true })] }) }));
39
39
  });
@@ -52,9 +52,11 @@ const OverviewRubberband = observer(function OverviewRubberband({ model, overvie
52
52
  },
53
53
  });
54
54
  function mouseDown(event) {
55
- event.preventDefault();
56
- event.stopPropagation();
57
- setStartX(getRelativeX(event, controlsRef.current));
55
+ if (event.button === 0) {
56
+ event.preventDefault();
57
+ event.stopPropagation();
58
+ setStartX(getRelativeX(event, controlsRef.current));
59
+ }
58
60
  }
59
61
  function mouseMove(event) {
60
62
  setGuideX(getRelativeX(event, controlsRef.current));
@@ -40,7 +40,7 @@ function useScrollPortExcess(ref, model, enabled) {
40
40
  }
41
41
  const ResizeAllTracksHandle = observer(function ResizeAllTracksHandle({ model, }) {
42
42
  const ref = useRef(null);
43
- const drag = useRef({ from: new Map(), moved: 0 });
43
+ const drag = useRef({ from: new Map() });
44
44
  const { isTopLevelView } = model;
45
45
  useScrollPortExcess(ref, model, isTopLevelView);
46
46
  return (_jsx("div", { ref: ref, style: isTopLevelView
@@ -54,12 +54,15 @@ const ResizeAllTracksHandle = observer(function ResizeAllTracksHandle({ model, }
54
54
  position: 'absolute',
55
55
  bottom: 0,
56
56
  }, onDragStart: () => {
57
- drag.current = { from: model.resizableTrackHeights, moved: 0 };
57
+ drag.current = { from: model.resizableTrackHeights };
58
58
  for (const track of model.tracks) {
59
59
  track.setResizing(true);
60
60
  }
61
- }, onDrag: distance => {
62
- drag.current.moved = model.resizeTracks(drag.current.moved + distance, drag.current.from);
61
+ },
62
+ // no value, so the distance from the press, every track scaled from
63
+ // where it was
64
+ onChange: distance => {
65
+ model.resizeTracks(distance, drag.current.from);
63
66
  }, onDragEnd: () => {
64
67
  for (const track of model.tracks) {
65
68
  track.setResizing(false);
@@ -1,9 +1,18 @@
1
- export default function SVGHighlightBand({ coords, height, color, label, labelColor, }: {
1
+ export interface BandGap {
2
+ top: number;
3
+ height: number;
4
+ }
5
+ export declare function spansAround(gaps: BandGap[], height: number): {
6
+ top: number;
7
+ height: number;
8
+ }[];
9
+ export default function SVGHighlightBand({ coords, height, gaps, color, label, labelColor, }: {
2
10
  coords: {
3
11
  left: number;
4
12
  width: number;
5
13
  };
6
14
  height: number;
15
+ gaps: BandGap[];
7
16
  color: string;
8
17
  label?: string;
9
18
  labelColor: string;
@@ -1,8 +1,20 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { getFillProps } from '@jbrowse/core/util';
3
+ // The stretches of a band `height` tall left once the sorted, disjoint `gaps`
4
+ // are cut out of it.
5
+ export function spansAround(gaps, height) {
6
+ const edges = [0, ...gaps.flatMap(g => [g.top, g.top + g.height]), height];
7
+ return edges.flatMap((top, i) => {
8
+ const bottom = edges[i + 1];
9
+ return i % 2 === 0 && bottom !== undefined && bottom > top
10
+ ? [{ top, height: bottom - top }]
11
+ : [];
12
+ });
13
+ }
3
14
  // One full-height SVG highlight band with an optional label at the top,
4
15
  // mirroring the on-screen HighlightBand div: a label wider than its band runs
5
- // past the band's right edge.
6
- export default function SVGHighlightBand({ coords, height, color, label, labelColor, }) {
7
- return (_jsxs(_Fragment, { children: [_jsx("rect", { x: coords.left, y: 0, width: coords.width, height: height, ...getFillProps(color) }), label ? (_jsx("text", { x: coords.left + 3, y: 2, fontSize: 11, dominantBaseline: "hanging", ...getFillProps(labelColor), children: label })) : null] }));
16
+ // past the band's right edge, and the band leaves out `gaps`, the tracks that
17
+ // do not draw in genomic coordinates.
18
+ export default function SVGHighlightBand({ coords, height, gaps, color, label, labelColor, }) {
19
+ return (_jsxs(_Fragment, { children: [spansAround(gaps, height).map(span => (_jsx("rect", { x: coords.left, y: span.top, width: coords.width, height: span.height, ...getFillProps(color) }, span.top))), label ? (_jsx("text", { x: coords.left + 3, y: 2, fontSize: 11, dominantBaseline: "hanging", ...getFillProps(labelColor), children: label })) : null] }));
8
20
  }
@@ -39,8 +39,9 @@ const SearchBox = observer(function SearchBox({ model, showHelp = true, minWidth
39
39
  const { recentLocations, addRecentLocation, clearRecentLocations } = useRecentLocations(assemblyName);
40
40
  async function navigate(option) {
41
41
  try {
42
- await navigateToSelectedOption({ model, assemblyName, option });
43
- addRecentLocation(recentLocationOf(option));
42
+ if (await navigateToSelectedOption({ model, assemblyName, option })) {
43
+ addRecentLocation(recentLocationOf(option));
44
+ }
44
45
  }
45
46
  catch (e) {
46
47
  notifySearchFailure(session, e);
@@ -15,7 +15,7 @@ export function filterRows(rows, text) {
15
15
  const SearchResultsTable = observer(function SearchResultsTable({ picker, }) {
16
16
  const [filter, setFilter] = useState('');
17
17
  const rows = filterRows(picker.rows, filter);
18
- return (_jsxs(_Fragment, { children: [picker.rows.length > FILTER_AT ? (_jsx(TextField, { autoFocus: true, fullWidth: true, size: "small", margin: "dense", placeholder: "Filter by name, location or track", value: filter, onChange: event => {
18
+ return (_jsxs(_Fragment, { children: [picker.rows.length > FILTER_AT ? (_jsx(TextField, { autoFocus: true, fullWidth: true, size: "small", margin: "dense", placeholder: "Filter by name, location or track", slotProps: { htmlInput: { 'aria-label': 'Filter search results' } }, value: filter, onChange: event => {
19
19
  setFilter(event.target.value);
20
20
  }, onKeyDown: event => {
21
21
  if (event.key === 'Enter' && rows.length === 1) {
@@ -23,6 +23,6 @@ const SearchResultsTable = observer(function SearchResultsTable({ picker, }) {
23
23
  }
24
24
  }, helperText: filter ? `${rows.length} of ${picker.rows.length}` : undefined })) : null, _jsx(TableContainer, { component: Paper, children: _jsxs(Table, { children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: "Name" }), _jsx(TableCell, { align: "right", children: "Location" }), _jsx(TableCell, { align: "right", children: "Track" }), _jsx(TableCell, { align: "right" })] }) }), _jsx(TableBody, { children: rows.map(row => (_jsxs(TableRow, { children: [_jsx(TableCell, { component: "th", scope: "row", children: row.label }), _jsx(TableCell, { align: "right", children: row.location }), _jsx(TableCell, { align: "right", children: _jsx(SanitizedHTML, { html: row.trackName || 'N/A' }) }), _jsx(TableCell, { align: "right", children: _jsx(Button, { onClick: () => {
25
25
  void picker.pick(row.id);
26
- }, color: "primary", variant: "contained", children: "Go" }) })] }, row.id))) })] }) })] }));
26
+ }, color: "primary", variant: "contained", "aria-label": `Go to ${row.label} at ${row.location}`, children: "Go" }) })] }, row.id))) })] }) })] }));
27
27
  });
28
28
  export default SearchResultsTable;
@@ -134,7 +134,7 @@ const TrackContainer = observer(function TrackContainer({ model, track, }) {
134
134
  const { setPaper, setContainer } = useTrackLabelBand(model, track.trackId);
135
135
  return (_jsxs(Paper, { ref: setPaper, className: cx(classes.root, track.pinned ? null : classes.unpinnedTrack), variant: showTrackOutlines ? 'outlined' : undefined, elevation: showTrackOutlines ? undefined : 0, onDragOver: event => {
136
136
  model.onTrackDragOver(track.id, event.clientY);
137
- }, children: [track.pinned ? _jsx(Gridlines, { model: model, offset: outlineOffset }) : null, model.effectiveTrackLabels !== 'hidden' ? (_jsx(TrackLabel, { track: track, className: trackLabelStyle })) : null, _jsx(TrackOverlaySlot, { zIndex: 100, overlayStyle: { left: -outlineOffset }, labelBand: model.trackLabelBand(track), children: _jsx(ErrorBoundary, { FallbackComponent: e => (_jsx("div", { className: classes.crashBanner, "data-gesture-owner": "true", children: _jsx(ErrorBanner, { error: e.error, onReset: e.resetErrorBoundary }) })), children: _jsx(TrackRenderingContainer, { model: model, track: track, display: display, ref: setContainer }) }, display.id) }), _jsx(PaddingBlocks, { model: model, offset: outlineOffset }), _jsx(ResizeHandle, { style: { height: RESIZE_HANDLE_HEIGHT }, onDrag: distance => display.resizeHeight(distance),
137
+ }, children: [track.pinned ? _jsx(Gridlines, { model: model, offset: outlineOffset }) : null, model.effectiveTrackLabels !== 'hidden' ? (_jsx(TrackLabel, { track: track, className: trackLabelStyle })) : null, _jsx(TrackOverlaySlot, { zIndex: 100, overlayStyle: { left: -outlineOffset }, labelBand: model.trackLabelBand(track), children: _jsx(ErrorBoundary, { FallbackComponent: e => (_jsx("div", { className: classes.crashBanner, "data-gesture-owner": "true", children: _jsx(ErrorBanner, { error: e.error, onReset: e.resetErrorBoundary }) })), children: _jsx(TrackRenderingContainer, { model: model, track: track, display: display, ref: setContainer }) }, display.id) }), _jsx(PaddingBlocks, { model: model, offset: outlineOffset }), _jsx(ResizeHandle, { style: { height: RESIZE_HANDLE_HEIGHT }, value: display.height, onChange: height => display.resizeHeight(height - display.height),
138
138
  // the drag's shortcut: grow the track by whatever it is scrolled over,
139
139
  // so a pileup showing three rows of forty opens to all forty
140
140
  // Optional: only a display that scrolls its own content has anything
@@ -18,9 +18,8 @@ const TrackLabelDragHandle = observer(function TrackLabelDragHandle({ track, })
18
18
  const view = containingLgv(track);
19
19
  const trackId = track.trackId;
20
20
  return (_jsx("span", { draggable: true, className: classes.dragHandle, onDragStart: event => {
21
- const target = event.currentTarget;
22
- if (target.parentNode) {
23
- const parent = target.parentNode;
21
+ const parent = event.currentTarget.parentElement;
22
+ if (parent) {
24
23
  event.dataTransfer.setDragImage(parent, 20, 20);
25
24
  view.setDraggingTrackId(track.id);
26
25
  }
@@ -1,5 +1,5 @@
1
1
  import { getSequenceAdapterConfig } from '@jbrowse/core/assemblyManager/assembly';
2
- import { getSession } from '@jbrowse/core/util';
2
+ import { assembleLocString, getSession } from '@jbrowse/core/util';
3
3
  export async function fetchSequence(model, regions, opts = {}) {
4
4
  const session = getSession(model);
5
5
  const assemblyNames = new Set(regions.map(r => r.assemblyName));
@@ -25,10 +25,18 @@ export async function fetchSequence(model, regions, opts = {}) {
25
25
  if (!adapterConfig) {
26
26
  throw new Error(`assembly ${assemblyName} has no resolved configuration, so its sequence adapter cannot be read`);
27
27
  }
28
- const sessionId = 'getSequence';
29
- return rpcManager.call(sessionId, 'CoreGetFeatures', {
30
- adapterConfig,
31
- regions,
32
- ...opts,
33
- });
28
+ // one call per region: a multi-region fetch merges its regions' streams in
29
+ // the order they finish, and a region the sequence lacks yields nothing,
30
+ // where the reader wants one entry per selected region, in selection order
31
+ return Promise.all(regions.map(async (region) => {
32
+ const [feature] = await rpcManager.call('getSequence', 'CoreGetFeatures', {
33
+ adapterConfig,
34
+ regions: [region],
35
+ ...opts,
36
+ });
37
+ if (!feature) {
38
+ throw new Error(`no sequence found for ${assembleLocString(region)} in assembly ${assemblyName}`);
39
+ }
40
+ return feature;
41
+ }));
34
42
  }
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useState } from 'react';
2
2
  import { getRelativeX } from '@jbrowse/core/util/getRelativeX';
3
- import { isFromPortal } from '@jbrowse/core/util/pressOwner';
3
+ import { isClaimedPress, isFromPortal } from '@jbrowse/core/util/pressOwner';
4
4
  import { useWindowDrag } from '../../shared/useWindowDrag.js';
5
5
  import { isOnScalebarRefLabel } from './util.js';
6
6
  export function useRangeSelect(ref, model, shiftOnly) {
@@ -45,7 +45,10 @@ export function useRangeSelect(ref, model, shiftOnly) {
45
45
  },
46
46
  });
47
47
  function mouseDown(event) {
48
- if (isFromPortal(event) || (shiftOnly && !event.shiftKey)) {
48
+ if (event.button !== 0 ||
49
+ (shiftOnly
50
+ ? !event.shiftKey || isClaimedPress(event)
51
+ : isFromPortal(event))) {
49
52
  return;
50
53
  }
51
54
  // a press with the refName menu already open reopens it on the label under
@@ -320,7 +320,8 @@ export function buildRubberBandMenuItems(self, launchItems) {
320
320
  // reversed region it names neither the base painted at the pixel nor the ends
321
321
  // in ascending order, and a `leftRef === rightRef` test calls a selection
322
322
  // crossing a collapsed intron one range when it is two.
323
- const rangeString = assembleLocStrings(self.getSelectedRegions(leftOffset, rightOffset));
323
+ const regions = self.getSelectedRegions(leftOffset, rightOffset);
324
+ const rangeString = assembleLocStrings(regions);
324
325
  const launch = [
325
326
  ...(self.isTopLevelView
326
327
  ? [
@@ -348,7 +349,6 @@ export function buildRubberBandMenuItems(self, launchItems) {
348
349
  label: 'Get sequence...',
349
350
  icon: MenuOpenIcon,
350
351
  onClick: () => {
351
- const regions = self.getSelectedRegions(leftOffset, rightOffset);
352
352
  getDialogHost(self).queueDialog(handleClose => [
353
353
  GetSequenceDialog,
354
354
  {
@@ -373,9 +373,9 @@ export function buildRubberBandMenuItems(self, launchItems) {
373
373
  label: 'Highlight region',
374
374
  icon: Highlighter,
375
375
  onClick: () => {
376
- const [region] = self.getSelectedRegions(leftOffset, rightOffset);
377
- if (region) {
378
- getSession(self).addHighlight(region);
376
+ const session = getSession(self);
377
+ for (const region of regions) {
378
+ session.addHighlight(region);
379
379
  }
380
380
  },
381
381
  },