@jbrowse/plugin-linear-genome-view 5.0.0-beta.2 → 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.
- package/esm/LinearGenomeView/afterAttach.d.ts +22 -5
- package/esm/LinearGenomeView/afterAttach.js +129 -106
- package/esm/LinearGenomeView/axisConfigSchema.d.ts +18 -0
- package/esm/LinearGenomeView/axisConfigSchema.js +22 -0
- package/esm/LinearGenomeView/components/CenterLine.js +10 -5
- package/esm/LinearGenomeView/components/ConnectedHoverHighlight.js +2 -3
- package/esm/LinearGenomeView/components/CrisprGuidePanel.js +3 -5
- package/esm/LinearGenomeView/components/ExportSvgDialog.d.ts +9 -4
- package/esm/LinearGenomeView/components/ExportSvgDialog.js +45 -8
- package/esm/LinearGenomeView/components/GetSequenceDialog.js +2 -2
- package/esm/LinearGenomeView/components/Header.js +11 -1
- package/esm/LinearGenomeView/components/HeaderClearHighlightButton.js +2 -2
- package/esm/LinearGenomeView/components/HeaderPanControls.d.ts +3 -3
- package/esm/LinearGenomeView/components/HeaderPanControls.js +6 -3
- package/esm/LinearGenomeView/components/HeaderSearchBoxes.js +4 -1
- package/esm/LinearGenomeView/components/HeaderZoomControls.js +6 -35
- package/esm/LinearGenomeView/components/Highlight.js +27 -11
- package/esm/LinearGenomeView/components/HighlightBand.d.ts +11 -2
- package/esm/LinearGenomeView/components/HighlightBand.js +32 -13
- package/esm/LinearGenomeView/components/HighlightChip.d.ts +3 -3
- package/esm/LinearGenomeView/components/HighlightChip.js +13 -8
- package/esm/LinearGenomeView/components/HighlightLabelDialog.d.ts +6 -0
- package/esm/LinearGenomeView/components/HighlightLabelDialog.js +18 -0
- package/esm/LinearGenomeView/components/HoverPositionHighlight.js +3 -3
- package/esm/LinearGenomeView/components/ImportForm.js +7 -9
- package/esm/LinearGenomeView/components/LinearGenomeView.js +5 -12
- package/esm/LinearGenomeView/components/LinearGenomeViewContainer.js +13 -14
- package/esm/LinearGenomeView/components/MiniControls.js +11 -5
- package/esm/LinearGenomeView/components/OverviewHighlight.js +3 -3
- package/esm/LinearGenomeView/components/OverviewHighlightBand.d.ts +3 -2
- package/esm/LinearGenomeView/components/OverviewHighlightBand.js +6 -5
- package/esm/LinearGenomeView/components/OverviewRubberband.js +25 -48
- package/esm/LinearGenomeView/components/OverviewScalebar.js +4 -5
- package/esm/LinearGenomeView/components/OverviewScalebarPolygon.js +5 -3
- package/esm/LinearGenomeView/components/PaddingBlocks.js +3 -8
- package/esm/LinearGenomeView/components/RangeSelectOverlay.js +5 -6
- package/esm/LinearGenomeView/components/RegionWidthEditorDialog.d.ts +3 -2
- package/esm/LinearGenomeView/components/RegionWidthEditorDialog.js +4 -4
- package/esm/LinearGenomeView/components/ResizeAllTracksHandle.d.ts +5 -0
- package/esm/LinearGenomeView/components/ResizeAllTracksHandle.js +74 -0
- package/esm/LinearGenomeView/components/Rubberband.d.ts +1 -2
- package/esm/LinearGenomeView/components/Rubberband.js +3 -2
- package/esm/LinearGenomeView/components/SVGHighlightBand.d.ts +10 -1
- package/esm/LinearGenomeView/components/SVGHighlightBand.js +16 -5
- package/esm/LinearGenomeView/components/Scalebar.d.ts +2 -6
- package/esm/LinearGenomeView/components/Scalebar.js +8 -5
- package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +51 -36
- package/esm/LinearGenomeView/components/SearchBox.js +10 -16
- package/esm/LinearGenomeView/components/SearchPanelForm.d.ts +3 -2
- package/esm/LinearGenomeView/components/SearchPanelForm.js +4 -2
- package/esm/LinearGenomeView/components/SearchResultsDialog.d.ts +4 -8
- package/esm/LinearGenomeView/components/SearchResultsDialog.js +7 -4
- package/esm/LinearGenomeView/components/SearchResultsTable.d.ts +4 -7
- package/esm/LinearGenomeView/components/SearchResultsTable.js +25 -53
- package/esm/LinearGenomeView/components/SequenceFeatureHoverHighlight.js +2 -3
- package/esm/LinearGenomeView/components/SequenceSearchDialog.d.ts +2 -4
- package/esm/LinearGenomeView/components/StrandCheckboxes.d.ts +3 -2
- package/esm/LinearGenomeView/components/StrandCheckboxes.js +4 -2
- package/esm/LinearGenomeView/components/TrackContainer.d.ts +2 -3
- package/esm/LinearGenomeView/components/TrackContainer.js +63 -10
- package/esm/LinearGenomeView/components/TrackLabel.js +14 -4
- package/esm/LinearGenomeView/components/TrackLabelButtons.js +2 -2
- package/esm/LinearGenomeView/components/TrackLabelDragHandle.js +4 -5
- package/esm/LinearGenomeView/components/TrackLabelMenu.js +11 -14
- package/esm/LinearGenomeView/components/TrackLabelRefNameWarning.d.ts +3 -4
- package/esm/LinearGenomeView/components/TrackLabelRefNameWarning.js +23 -10
- package/esm/LinearGenomeView/components/TrackRenderingContainer.d.ts +6 -4
- package/esm/LinearGenomeView/components/TrackRenderingContainer.js +12 -24
- package/esm/LinearGenomeView/components/TracksContainer.js +11 -23
- package/esm/LinearGenomeView/components/VerticalGuide.d.ts +1 -2
- package/esm/LinearGenomeView/components/VerticalGuide.js +3 -16
- package/esm/LinearGenomeView/components/fetchSequence.d.ts +1 -2
- package/esm/LinearGenomeView/components/fetchSequence.js +15 -7
- package/esm/LinearGenomeView/components/recentLocationsMenu.js +10 -0
- package/esm/LinearGenomeView/components/useRangeSelect.d.ts +0 -3
- package/esm/LinearGenomeView/components/useRangeSelect.js +26 -61
- package/esm/LinearGenomeView/components/useSearchBoxPrefs.d.ts +3 -1
- package/esm/LinearGenomeView/components/useSearchBoxPrefs.js +4 -2
- package/esm/LinearGenomeView/components/useSideScroll.d.ts +17 -2
- package/esm/LinearGenomeView/components/useSideScroll.js +156 -102
- package/esm/LinearGenomeView/components/util.d.ts +4 -0
- package/esm/LinearGenomeView/components/util.js +4 -2
- package/esm/LinearGenomeView/consts.d.ts +2 -0
- package/esm/LinearGenomeView/consts.js +3 -1
- package/esm/LinearGenomeView/containingLgv.d.ts +21 -0
- package/esm/LinearGenomeView/containingLgv.js +23 -0
- package/esm/LinearGenomeView/headerFit.d.ts +4 -2
- package/esm/LinearGenomeView/headerFit.js +20 -7
- package/esm/LinearGenomeView/index.d.ts +2 -7
- package/esm/LinearGenomeView/index.js +5 -7
- package/esm/LinearGenomeView/keyboardHandler.d.ts +8 -7
- package/esm/LinearGenomeView/keyboardHandler.js +61 -23
- package/esm/LinearGenomeView/launchKeys.d.ts +1 -1
- package/esm/LinearGenomeView/launchKeys.js +11 -6
- package/esm/LinearGenomeView/lazyDialogs.d.ts +16 -3
- package/esm/LinearGenomeView/lazyDialogs.js +1 -0
- package/esm/LinearGenomeView/linkedViewSync.d.ts +3 -11
- package/esm/LinearGenomeView/linkedViewSync.js +27 -49
- package/esm/LinearGenomeView/menuItems.d.ts +16 -18
- package/esm/LinearGenomeView/menuItems.js +264 -164
- package/esm/LinearGenomeView/model.d.ts +1192 -560
- package/esm/LinearGenomeView/model.js +805 -590
- package/esm/LinearGenomeView/rExport/exportLinearView.d.ts +17 -0
- package/esm/LinearGenomeView/rExport/exportLinearView.js +117 -0
- package/esm/LinearGenomeView/rExport/rScript.d.ts +110 -0
- package/esm/LinearGenomeView/rExport/rScript.js +524 -0
- package/esm/LinearGenomeView/sharedScaleContainer.d.ts +1 -1
- package/esm/LinearGenomeView/sharedScaleContainer.js +2 -12
- package/esm/LinearGenomeView/showFiles.d.ts +2 -0
- package/esm/LinearGenomeView/showFiles.js +19 -0
- package/esm/LinearGenomeView/showRegionsWithUndo.d.ts +1 -1
- package/esm/LinearGenomeView/showRegionsWithUndo.js +13 -5
- package/esm/LinearGenomeView/stickyChrome.d.ts +5 -9
- package/esm/LinearGenomeView/stickyChrome.js +5 -9
- package/esm/LinearGenomeView/svgcomponents/SVGGridlines.js +4 -4
- package/esm/LinearGenomeView/svgcomponents/SVGHeader.d.ts +4 -1
- package/esm/LinearGenomeView/svgcomponents/SVGHeader.js +8 -21
- package/esm/LinearGenomeView/svgcomponents/SVGHighlights.d.ts +3 -1
- package/esm/LinearGenomeView/svgcomponents/SVGHighlights.js +9 -9
- package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.d.ts +3 -1
- package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.js +5 -5
- package/esm/LinearGenomeView/svgcomponents/SVGLinearGenomeView.js +13 -18
- package/esm/LinearGenomeView/svgcomponents/SVGRegionSeparators.js +2 -5
- package/esm/LinearGenomeView/svgcomponents/SVGRowHeader.js +5 -6
- package/esm/LinearGenomeView/svgcomponents/SVGRuler.js +9 -10
- package/esm/LinearGenomeView/svgcomponents/SVGScalebar.js +3 -3
- package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.d.ts +26 -0
- package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.js +12 -0
- package/esm/LinearGenomeView/svgcomponents/SVGTrackLabel.js +21 -13
- package/esm/LinearGenomeView/svgcomponents/SVGTracks.d.ts +4 -5
- package/esm/LinearGenomeView/svgcomponents/SVGTracks.js +7 -6
- package/esm/LinearGenomeView/svgcomponents/SVGView.d.ts +2 -3
- package/esm/LinearGenomeView/svgcomponents/SVGView.js +17 -3
- package/esm/LinearGenomeView/svgcomponents/renderViewTracks.d.ts +24 -20
- package/esm/LinearGenomeView/svgcomponents/renderViewTracks.js +42 -29
- package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +74 -77
- package/esm/LinearGenomeView/svgcomponents/util.d.ts +28 -12
- package/esm/LinearGenomeView/svgcomponents/util.js +41 -29
- package/esm/LinearGenomeView/types.d.ts +13 -10
- package/esm/LinearGenomeView/types.js +0 -1
- package/esm/LinearGenomeView/util.d.ts +103 -46
- package/esm/LinearGenomeView/util.js +222 -170
- package/esm/MultiLevelRubberband/MultiLevelRubberband.js +9 -6
- package/esm/MultiLevelRubberband/index.d.ts +2 -0
- package/esm/MultiLevelRubberband/index.js +2 -0
- package/esm/MultiLevelRubberband/menuItems.d.ts +34 -0
- package/esm/MultiLevelRubberband/menuItems.js +52 -0
- package/esm/MultiLevelRubberband/rowLabel.d.ts +22 -0
- package/esm/MultiLevelRubberband/rowLabel.js +27 -0
- package/esm/MultiLevelRubberband/types.d.ts +1 -0
- package/esm/MultiLevelRubberband/useRangeSelect.d.ts +2 -1
- package/esm/MultiLevelRubberband/useRangeSelect.js +45 -59
- package/esm/TrackModifiers/index.d.ts +83 -0
- package/esm/TrackModifiers/index.js +231 -0
- package/esm/displayKitTests/perRegionTestEnv.d.ts +244 -68
- package/esm/displayKitTests/perRegionTestEnv.js +30 -26
- package/esm/index.d.ts +21 -99
- package/esm/index.js +17 -38
- package/esm/searchUtils.d.ts +26 -10
- package/esm/searchUtils.js +137 -137
- package/esm/shared/RubberbandSpan.d.ts +3 -2
- package/esm/shared/RubberbandSpan.js +6 -3
- package/esm/shared/coordLabels.d.ts +11 -4
- package/esm/shared/coordLabels.js +20 -5
- package/esm/shared/useWindowDrag.d.ts +18 -0
- package/esm/shared/useWindowDrag.js +47 -0
- package/package.json +12 -11
- package/esm/FeatureTrack/configSchema.d.ts +0 -124
- package/esm/FeatureTrack/configSchema.js +0 -54
- package/esm/FeatureTrack/index.d.ts +0 -2
- package/esm/FeatureTrack/index.js +0 -13
- package/esm/LinearGenomeView/initKeys.d.ts +0 -12
- package/esm/LinearGenomeView/initKeys.js +0 -68
- package/esm/MultiLevelRubberband/VerticalGuide.d.ts +0 -6
- package/esm/MultiLevelRubberband/VerticalGuide.js +0 -21
- package/esm/lazyPluginExports.d.ts +0 -23
- package/esm/lazyPluginExports.js +0 -36
package/esm/searchUtils.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import { RefSequenceResult } from '@jbrowse/core/TextSearch/BaseResults';
|
|
2
|
-
import {
|
|
2
|
+
import { groupByPlace, landsInAssembly } from '@jbrowse/core/TextSearch/places';
|
|
3
|
+
import { dedupe, getEnv, getNotificationSink, getSession, matchRefNames, MAX_GLOB_REGIONS, rankRefNames, UnknownRefNameError, } from '@jbrowse/core/util';
|
|
3
4
|
import { isAlive } from '@jbrowse/mobx-state-tree';
|
|
4
5
|
import { parseLocStrings } from './LinearGenomeView/util.js';
|
|
5
6
|
// Budget for refName suggestions from a query. Deliberately far below the
|
|
6
|
-
// autocomplete's ~100-row display cap:
|
|
7
|
-
// search hits, so a scaffold-heavy assembly would otherwise fill every row with
|
|
7
|
+
// autocomplete's ~100-row display cap: exact and prefix refName hits are
|
|
8
|
+
// returned ahead of the text search hits, so a scaffold-heavy assembly would otherwise fill every row with
|
|
8
9
|
// scaffolds and push the gene hits out of the list entirely. Raising this to
|
|
9
10
|
// match the display cap looks like a consistency fix and is not one.
|
|
10
11
|
const MAX_REFNAME_HITS = 10;
|
|
11
12
|
// shared dispatch used by SearchBox.onSelect and the LGV ImportForm submit:
|
|
12
13
|
// route a chosen result to a direct nav, a multi-result dialog, or a generic
|
|
13
14
|
// locstring/refname resolution
|
|
15
|
+
// Resolves whether the view moved: a hit that raised the picker, or a glob
|
|
16
|
+
// refused as too wide, did not.
|
|
14
17
|
export async function navigateToSelectedOption({ option, model, assemblyName, }) {
|
|
15
18
|
if (option.hasLocation()) {
|
|
16
19
|
await navToOption({ option, model, assemblyName });
|
|
20
|
+
return true;
|
|
17
21
|
}
|
|
18
22
|
else if (option.results?.length) {
|
|
19
|
-
|
|
23
|
+
return showSearchResults({
|
|
20
24
|
results: option.results,
|
|
21
25
|
query: option.getLabel(),
|
|
22
26
|
model,
|
|
@@ -24,7 +28,7 @@ export async function navigateToSelectedOption({ option, model, assemblyName, })
|
|
|
24
28
|
});
|
|
25
29
|
}
|
|
26
30
|
else {
|
|
27
|
-
|
|
31
|
+
return handleSelectedRegion({
|
|
28
32
|
input: option.getLabel(),
|
|
29
33
|
assemblyName,
|
|
30
34
|
model,
|
|
@@ -36,37 +40,6 @@ export function isOpenInView(result, model) {
|
|
|
36
40
|
const trackId = result.getTrackId();
|
|
37
41
|
return trackId !== undefined && !!model.getTrack(trackId);
|
|
38
42
|
}
|
|
39
|
-
// One spelling of a locstring, so two indexes that answer `chr1:1-100` and
|
|
40
|
-
// `1:1..100` are recognised as one place. An unparseable string is compared
|
|
41
|
-
// raw, which can only ever split a group that would otherwise have merged —
|
|
42
|
-
// the safe direction, since the picker is what an unprovable match falls back
|
|
43
|
-
// to.
|
|
44
|
-
function canonicalLocString(locString, assembly) {
|
|
45
|
-
try {
|
|
46
|
-
const loc = parseLocString(locString, refName => assembly.isValidRefName(refName));
|
|
47
|
-
return assembleLocString({
|
|
48
|
-
...loc,
|
|
49
|
-
refName: assembly.getCanonicalRefName2(loc.refName),
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
catch (e) {
|
|
53
|
-
console.warn('failed to parse location string', locString, e);
|
|
54
|
-
return locString;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
// What a hit means as a destination: the name it shows and the place it goes.
|
|
58
|
-
// A hit with no location has no destination and can never join a group.
|
|
59
|
-
function destination(result, assembly) {
|
|
60
|
-
const locString = result.getLocation();
|
|
61
|
-
return locString
|
|
62
|
-
? [
|
|
63
|
-
result.getDisplayString(),
|
|
64
|
-
assembly?.initialized
|
|
65
|
-
? canonicalLocString(locString, assembly)
|
|
66
|
-
: locString,
|
|
67
|
-
].join('\u0000')
|
|
68
|
-
: undefined;
|
|
69
|
-
}
|
|
70
43
|
// Which hit of an agreeing group to travel through, best rung first: a track
|
|
71
44
|
// the view already has on screen, then one the session could open if asked,
|
|
72
45
|
// then whatever the ranking put first. The middle rung is not hypothetical —
|
|
@@ -86,12 +59,10 @@ function trackRank(result, model, session) {
|
|
|
86
59
|
return session.getTrackById(trackId) ? 1 : 3;
|
|
87
60
|
}
|
|
88
61
|
}
|
|
89
|
-
// The pick is a stable minimum, so a linear scan rather than a sort:
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// `trackRank` reaches `session.getTrackById`, and a comparator asks O(n log n)
|
|
94
|
-
// times for an answer that does not vary.
|
|
62
|
+
// The pick is a stable minimum, so a linear scan rather than a sort: keeping
|
|
63
|
+
// the first strictly-lowest rung leaves a group with nothing to choose between
|
|
64
|
+
// in the order the ranking handed it. A comparator would ask `trackRank`, and
|
|
65
|
+
// through it `session.getTrackById`, O(n log n) times.
|
|
95
66
|
function bestRanked(results, model, session) {
|
|
96
67
|
let best = results[0];
|
|
97
68
|
let bestRank = trackRank(best, model, session);
|
|
@@ -104,42 +75,32 @@ function bestRanked(results, model, session) {
|
|
|
104
75
|
}
|
|
105
76
|
return best;
|
|
106
77
|
}
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
export function
|
|
113
|
-
|
|
114
|
-
// carry a hundred results, and each one costs a locstring parse
|
|
115
|
-
const destinations = results.map(r => destination(r, assembly));
|
|
116
|
-
const agree = destinations[0] !== undefined &&
|
|
117
|
-
destinations.every(d => d === destinations[0]);
|
|
118
|
-
return agree ? bestRanked(results, model, session) : undefined;
|
|
78
|
+
// One hit per place, in the order the ranking first reached each. Hits that
|
|
79
|
+
// name one feature at one place are several indexes having found it, not a
|
|
80
|
+
// choice to put to the user: a handful of gene tracks turned every gene search
|
|
81
|
+
// into a picker whose only varying column was Track (issues #4302 and #5068),
|
|
82
|
+
// and two whose gene models end a few bases apart still did.
|
|
83
|
+
export function distinctDestinations({ results, model, session, assembly, }) {
|
|
84
|
+
return groupByPlace(results, assembly).map(group => bestRanked(group, model, session));
|
|
119
85
|
}
|
|
120
86
|
// Every multi-hit result set reaches the picker through here, so the two
|
|
121
|
-
// search surfaces cannot disagree about when one is worth raising
|
|
122
|
-
// whether the view moved.
|
|
87
|
+
// search surfaces cannot disagree about when one is worth raising: only when
|
|
88
|
+
// the hits name more than one place. Returns whether the view moved.
|
|
123
89
|
export async function showSearchResults({ results, query, model, assemblyName, grow, showHitTrack, }) {
|
|
124
90
|
const session = getSession(model);
|
|
125
|
-
const
|
|
91
|
+
const places = distinctDestinations({
|
|
126
92
|
results,
|
|
127
93
|
model,
|
|
128
94
|
session,
|
|
129
95
|
assembly: session.assemblyManager.get(assemblyName),
|
|
130
96
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
model,
|
|
135
|
-
assemblyName,
|
|
136
|
-
grow,
|
|
137
|
-
showHitTrack,
|
|
138
|
-
});
|
|
97
|
+
const land = (option) => navToOption({ option, model, assemblyName, grow, showHitTrack });
|
|
98
|
+
if (places.length === 1) {
|
|
99
|
+
await land(places[0]);
|
|
139
100
|
return true;
|
|
140
101
|
}
|
|
141
102
|
else {
|
|
142
|
-
model.setSearchResults(
|
|
103
|
+
model.setSearchResults(places, query, assemblyName, land);
|
|
143
104
|
return false;
|
|
144
105
|
}
|
|
145
106
|
}
|
|
@@ -147,6 +108,18 @@ export async function showSearchResults({ results, query, model, assemblyName, g
|
|
|
147
108
|
// own edges, which is what this default is; a caller that asked for a specific
|
|
148
109
|
// padding gets that instead.
|
|
149
110
|
const SEARCH_HIT_GROW = 0.2;
|
|
111
|
+
// The track to show beside a hit: the one its index came from, when this
|
|
112
|
+
// session has it. A shared index names tracks of whichever config it was built
|
|
113
|
+
// for — the UCSC hg38 names index says `hg38-ncbiRefSeqGff`, which the HPRC
|
|
114
|
+
// pangenome config reuses without carrying — and launching a track the session
|
|
115
|
+
// lacks turned every gene search there into a "Could not resolve identifier"
|
|
116
|
+
// error over a view that had navigated fine.
|
|
117
|
+
export function hitTrackToShow(result, session) {
|
|
118
|
+
const trackId = result.getTrackId();
|
|
119
|
+
return trackId !== undefined && session.getTrackById(trackId)
|
|
120
|
+
? trackId
|
|
121
|
+
: undefined;
|
|
122
|
+
}
|
|
150
123
|
// A hit lands with the track its index came from shown, which is what a name
|
|
151
124
|
// typed into the search box wants. A session spec that named its own tracks
|
|
152
125
|
// passes false: it asked for those tracks, and a hosted config's full RefSeq
|
|
@@ -156,11 +129,11 @@ export async function navToOption({ option, model, assemblyName, grow, showHitTr
|
|
|
156
129
|
// but carries no coordinates; treat that as "no location" and fall back to
|
|
157
130
|
// the label rather than forwarding '' into an empty, view-blanking parse
|
|
158
131
|
const location = option.getLocation() || option.getLabel();
|
|
159
|
-
const trackId = option.getTrackId();
|
|
160
132
|
const session = getSession(model);
|
|
161
133
|
const { assemblyManager } = session;
|
|
162
134
|
await model.navToLocations(parseLocStrings(location, assemblyName, (ref, asm) => assemblyManager.isValidRefName(ref, asm)), assemblyName, grow ?? SEARCH_HIT_GROW);
|
|
163
|
-
|
|
135
|
+
const trackId = showHitTrack ? hitTrackToShow(option, session) : undefined;
|
|
136
|
+
if (trackId && isAlive(model)) {
|
|
164
137
|
await model.launchTrack(trackId);
|
|
165
138
|
}
|
|
166
139
|
// same detach hazard as showTrack above: navToLocations awaits, and handlers
|
|
@@ -189,9 +162,9 @@ export function notifySearchFailure(session, e) {
|
|
|
189
162
|
console.error(e);
|
|
190
163
|
session.notify(e instanceof SearchResultsNotFoundError ? e.message : `${e}`, 'warning');
|
|
191
164
|
}
|
|
192
|
-
// if input
|
|
193
|
-
//
|
|
194
|
-
//
|
|
165
|
+
// if input parses as one or more locstrings, navigate directly; otherwise
|
|
166
|
+
// search and: pop a dialog for multiple results, navigate for one, or report
|
|
167
|
+
// the miss
|
|
195
168
|
//
|
|
196
169
|
// Returns whether the view actually moved. Three branches deliberately do not
|
|
197
170
|
// navigate — a glob too wide to open, a multi-hit search whose hits disagree
|
|
@@ -200,6 +173,7 @@ export function notifySearchFailure(session, e) {
|
|
|
200
173
|
// otherwise tell "showing it" from "asked you which one".
|
|
201
174
|
export async function handleSelectedRegion({ input, model, assemblyName, grow, showHitTrack, }) {
|
|
202
175
|
const { assemblyManager, textSearchManager } = getSession(model);
|
|
176
|
+
model.closeSearchPicker();
|
|
203
177
|
// resolves only once regions/aliases are loaded, which isValidRefName needs
|
|
204
178
|
// (it throws otherwise). A load failure is not this function's to report:
|
|
205
179
|
// swallow it and let the input fall through to the text-search path below
|
|
@@ -207,16 +181,11 @@ export async function handleSelectedRegion({ input, model, assemblyName, grow, s
|
|
|
207
181
|
const assembly = await assemblyManager
|
|
208
182
|
.waitForAssembly(assemblyName)
|
|
209
183
|
.catch(() => undefined);
|
|
210
|
-
|
|
211
|
-
// that passes here is one the parse below will accept
|
|
184
|
+
await Promise.all(assembliesNamedIn(input).map(name => assemblyManager.requireAssembly(name)));
|
|
212
185
|
const isRef = (ref) => !!assembly && assemblyManager.isValidRefName(ref, assemblyName);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
.split(/\s+/)
|
|
217
|
-
.filter(Boolean)
|
|
218
|
-
.every(entry => checkRef(entry, isRef))) {
|
|
219
|
-
await navToLocstrings();
|
|
186
|
+
const parsed = parseOrError(() => parseLocStrings(input, assemblyName, (ref, asm) => assemblyManager.isValidRefName(ref, asm)));
|
|
187
|
+
if (parsed.locations) {
|
|
188
|
+
await model.navToLocations(parsed.locations, assemblyName, grow);
|
|
220
189
|
return true;
|
|
221
190
|
}
|
|
222
191
|
else if (input.includes('*') && !!assembly) {
|
|
@@ -265,14 +234,14 @@ export async function handleSelectedRegion({ input, model, assemblyName, grow, s
|
|
|
265
234
|
// adapters answer 'exact' by filtering exactly this list. An adapter that
|
|
266
235
|
// tags nothing simply never wins the exact pass, which is the behaviour it
|
|
267
236
|
// had when it returned nothing for searchType: 'exact'.
|
|
268
|
-
const
|
|
237
|
+
const found = await searchNames({
|
|
269
238
|
queryString: input,
|
|
270
239
|
assemblyName,
|
|
271
240
|
textSearchManager,
|
|
272
241
|
assembly,
|
|
273
242
|
});
|
|
274
|
-
const exactResults =
|
|
275
|
-
const results = exactResults.length ? exactResults :
|
|
243
|
+
const exactResults = found.results.filter(r => r.isExact());
|
|
244
|
+
const results = exactResults.length ? exactResults : found.results;
|
|
276
245
|
// the view may have been closed/detached while the text-search RPC ran
|
|
277
246
|
if (!isAlive(model)) {
|
|
278
247
|
return false;
|
|
@@ -302,73 +271,104 @@ export async function handleSelectedRegion({ input, model, assemblyName, grow, s
|
|
|
302
271
|
return true;
|
|
303
272
|
}
|
|
304
273
|
else {
|
|
305
|
-
// no search hits
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (e instanceof UnknownRefNameError && isPlainName) {
|
|
317
|
-
throw new SearchResultsNotFoundError(`No results found for "${input}"`);
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
throw e;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
274
|
+
// no search hits and no locstring either. An input naming no sequence
|
|
275
|
+
// and no coordinate — a gene name, or a phrase like "breast cancer" — is
|
|
276
|
+
// reported as a name miss; coordinate-shaped input keeps its ref error
|
|
277
|
+
const isPlainName = !input.includes(':') &&
|
|
278
|
+
input
|
|
279
|
+
.trim()
|
|
280
|
+
.split(/\s+/)
|
|
281
|
+
.every(token => !isRef(token) && !/^\d[\d,]*$/.test(token));
|
|
282
|
+
throw parsed.error instanceof UnknownRefNameError && isPlainName
|
|
283
|
+
? nameNotFound(input, assemblyName, found)
|
|
284
|
+
: parsed.error;
|
|
323
285
|
}
|
|
324
286
|
}
|
|
325
287
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
288
|
+
function parseOrError(parse) {
|
|
289
|
+
try {
|
|
290
|
+
return { locations: parse() };
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
return { error };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function assembliesNamedIn(input) {
|
|
297
|
+
return [...input.matchAll(/(?:^|\s)\{([^}]+)\}/g)].map(([, name]) => name);
|
|
329
298
|
}
|
|
330
|
-
|
|
331
|
-
const
|
|
299
|
+
async function searchNames({ queryString, searchType, assemblyName, textSearchManager, assembly, signal, }) {
|
|
300
|
+
const report = await textSearchManager?.searchIndexes({
|
|
332
301
|
queryString,
|
|
333
302
|
searchType,
|
|
334
|
-
|
|
303
|
+
signal,
|
|
335
304
|
}, assemblyName);
|
|
336
305
|
// ensure aliases are loaded: allRefNames is a pure getter, so reading it does
|
|
337
306
|
// not itself kick off the lazy load. A load failure just leaves allRefNames
|
|
338
307
|
// undefined, so this still returns whatever the text search turned up
|
|
339
308
|
await assembly?.load().catch(() => { });
|
|
340
|
-
const
|
|
309
|
+
const { strong, weak } = assembly
|
|
341
310
|
? searchRefNames(assembly, queryString, searchType)
|
|
342
|
-
: [];
|
|
343
|
-
return
|
|
311
|
+
: { strong: [], weak: [] };
|
|
312
|
+
return {
|
|
313
|
+
results: dedupe([
|
|
314
|
+
...strong,
|
|
315
|
+
...(report?.results ?? []).filter(r => landsInAssembly(r, assembly)),
|
|
316
|
+
...weak,
|
|
317
|
+
], elt => elt.getId()),
|
|
318
|
+
indexCount: report?.indexCount ?? 0,
|
|
319
|
+
failures: report?.failures ?? [],
|
|
320
|
+
};
|
|
344
321
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
break;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
322
|
+
/**
|
|
323
|
+
* The search box's hits for a query: refNames and text-index results, merged.
|
|
324
|
+
* Pass the session and the function finds the index manager and the assembly
|
|
325
|
+
* itself; passing `textSearchManager` and `assembly` is the older form.
|
|
326
|
+
*/
|
|
327
|
+
export async function fetchResults(args) {
|
|
328
|
+
if ('session' in args) {
|
|
329
|
+
const { session, ...rest } = args;
|
|
330
|
+
const assembly = await session.assemblyManager
|
|
331
|
+
.waitForAssembly(args.assemblyName)
|
|
332
|
+
.catch(() => undefined);
|
|
333
|
+
return (await searchNames({
|
|
334
|
+
...rest,
|
|
335
|
+
textSearchManager: session.textSearchManager,
|
|
336
|
+
assembly,
|
|
337
|
+
})).results;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
return (await searchNames(args)).results;
|
|
367
341
|
}
|
|
368
|
-
return [...canonicalHits].map(([r, exact]) => new RefSequenceResult({ label: r, refName: r, exact }));
|
|
369
342
|
}
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
343
|
+
// A miss says when an index could not answer: with none answering it is a
|
|
344
|
+
// failure, and with some answering it is still a miss, naming what failed
|
|
345
|
+
export function nameNotFound(input, assemblyName, { indexCount, failures }) {
|
|
346
|
+
const [failure] = failures;
|
|
347
|
+
const miss = `No results found for "${input}"`;
|
|
348
|
+
return failure === undefined
|
|
349
|
+
? new SearchResultsNotFoundError(indexCount ? miss : `${miss}: ${assemblyName} has no text search index`)
|
|
350
|
+
: failures.length === indexCount
|
|
351
|
+
? new Error(`Searching for "${input}" failed: ${failure}`, {
|
|
352
|
+
cause: failure,
|
|
353
|
+
})
|
|
354
|
+
: new SearchResultsNotFoundError(`${miss} (a search index failed: ${failure})`, { cause: failure });
|
|
355
|
+
}
|
|
356
|
+
// The refNames a query names, through the same ranking the dropdown's fallback
|
|
357
|
+
// list uses. A refName the query is only a substring of is the weakest kind of
|
|
358
|
+
// hit, so it goes behind the text-search results rather than ahead of them
|
|
359
|
+
function searchRefNames(assembly, queryString, searchType) {
|
|
360
|
+
const hits = rankRefNames(assembly, queryString, MAX_REFNAME_HITS)
|
|
361
|
+
.filter(({ tier }) => searchType !== 'exact' || tier === 'exact')
|
|
362
|
+
.map(({ refName, tier }) => ({
|
|
363
|
+
tier,
|
|
364
|
+
result: new RefSequenceResult({
|
|
365
|
+
label: refName,
|
|
366
|
+
refName,
|
|
367
|
+
exact: tier === 'exact',
|
|
368
|
+
}),
|
|
369
|
+
}));
|
|
370
|
+
return {
|
|
371
|
+
strong: hits.filter(h => h.tier !== 'substring').map(h => h.result),
|
|
372
|
+
weak: hits.filter(h => h.tier === 'substring').map(h => h.result),
|
|
373
|
+
};
|
|
374
374
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* single view reads one coordinate per edge and the multi-level rubberband reads
|
|
5
5
|
* one per level.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
declare const RubberbandSpan: ({ left, width, viewWidth, stickyTop, leftLabel, rightLabel, size, }: {
|
|
8
8
|
left: number;
|
|
9
9
|
width: number;
|
|
10
10
|
viewWidth: number;
|
|
@@ -12,4 +12,5 @@ export default function RubberbandSpan({ left, width, viewWidth, stickyTop, left
|
|
|
12
12
|
leftLabel: React.ReactNode;
|
|
13
13
|
rightLabel: React.ReactNode;
|
|
14
14
|
size?: React.ReactNode;
|
|
15
|
-
})
|
|
15
|
+
}) => import("react").JSX.Element;
|
|
16
|
+
export default RubberbandSpan;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { alpha } from '@jbrowse/core/ui/palette';
|
|
2
3
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
3
|
-
import { Typography
|
|
4
|
+
import { Typography } from '@mui/material';
|
|
5
|
+
import { observer } from 'mobx-react';
|
|
4
6
|
import { LABEL_WIDTH, SpanEdgeLabels } from './coordLabels.js';
|
|
5
7
|
const useStyles = makeStyles()(theme => ({
|
|
6
8
|
rubberband: {
|
|
@@ -22,7 +24,7 @@ const useStyles = makeStyles()(theme => ({
|
|
|
22
24
|
* single view reads one coordinate per edge and the multi-level rubberband reads
|
|
23
25
|
* one per level.
|
|
24
26
|
*/
|
|
25
|
-
|
|
27
|
+
const RubberbandSpan = observer(function RubberbandSpan({ left, width, viewWidth, stickyTop, leftLabel, rightLabel, size, }) {
|
|
26
28
|
const { classes } = useStyles();
|
|
27
29
|
// a label only moves inside its edge when the span can hold it, so a sliver of
|
|
28
30
|
// a selection at a view border keeps both labels outside and takes the clip
|
|
@@ -30,4 +32,5 @@ export default function RubberbandSpan({ left, width, viewWidth, stickyTop, left
|
|
|
30
32
|
return (_jsxs("div", { className: classes.rubberband, style: { transform: `translateX(${left}px)`, width }, children: [_jsx(SpanEdgeLabels, { stickyTop: stickyTop, left: leftLabel, right: rightLabel, insideLeft: fitsInside && left < LABEL_WIDTH, insideRight: fitsInside && left + width > viewWidth - LABEL_WIDTH }), size ? (_jsx(Typography, { variant: "h6", className: classes.rubberbandText, style: stickyTop === undefined
|
|
31
33
|
? undefined
|
|
32
34
|
: { position: 'sticky', top: stickyTop }, children: size })) : null] }));
|
|
33
|
-
}
|
|
35
|
+
});
|
|
36
|
+
export default RubberbandSpan;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export declare const LABEL_WIDTH = 100;
|
|
2
|
+
/** A red vertical line at `coordX` with `children` labelled above it. */
|
|
3
|
+
export declare const VerticalGuideLine: ({ coordX, viewWidth, stickyTop, children, }: {
|
|
4
|
+
coordX: number;
|
|
5
|
+
viewWidth: number;
|
|
6
|
+
stickyTop: string | undefined;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}) => import("react").JSX.Element;
|
|
2
9
|
/**
|
|
3
10
|
* The bp coordinate under a vertical guide, drawn just above the top of the
|
|
4
11
|
* guide line so it covers neither the line nor the cursor hovering the scalebar
|
|
@@ -19,22 +26,22 @@ export declare const LABEL_WIDTH = 100;
|
|
|
19
26
|
* measured once per hover, not per mousemove — only `coordX` changes as the
|
|
20
27
|
* mouse moves, and that needs no measurement.
|
|
21
28
|
*/
|
|
22
|
-
export declare
|
|
29
|
+
export declare const GuideLabel: ({ coordX, viewWidth, stickyTop, children, }: {
|
|
23
30
|
coordX: number;
|
|
24
31
|
viewWidth: number;
|
|
25
32
|
stickyTop: string | undefined;
|
|
26
33
|
children: React.ReactNode;
|
|
27
|
-
})
|
|
34
|
+
}) => import("react").JSX.Element;
|
|
28
35
|
/**
|
|
29
36
|
* The bp coordinates of a rubberband selection, one label just outside each edge
|
|
30
37
|
* of the span. Renders inside the span, so the edges are its own box. A label
|
|
31
38
|
* with no room outside its edge flips inside instead, which is why the span it
|
|
32
39
|
* has to fit in is a prop.
|
|
33
40
|
*/
|
|
34
|
-
export declare
|
|
41
|
+
export declare const SpanEdgeLabels: ({ stickyTop, left, right, insideLeft, insideRight, }: {
|
|
35
42
|
stickyTop: string | undefined;
|
|
36
43
|
left: React.ReactNode;
|
|
37
44
|
right: React.ReactNode;
|
|
38
45
|
insideLeft: boolean;
|
|
39
46
|
insideRight: boolean;
|
|
40
|
-
})
|
|
47
|
+
}) => import("react").JSX.Element;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { alpha } from '@jbrowse/core/ui/palette';
|
|
3
4
|
import { MUI_TOOLTIP_Z_INDEX } from '@jbrowse/core/ui/zIndexes';
|
|
4
5
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
5
|
-
import {
|
|
6
|
+
import { observer } from 'mobx-react';
|
|
6
7
|
import { createPortal } from 'react-dom';
|
|
7
8
|
// roughly how wide a coordinate label is. the guide clamps itself half of this
|
|
8
9
|
// from either border, and a rubberband label that would hang off a border goes
|
|
@@ -51,7 +52,21 @@ const useStyles = makeStyles()(theme => ({
|
|
|
51
52
|
insideRightEdge: {
|
|
52
53
|
right: 0,
|
|
53
54
|
},
|
|
55
|
+
guideLine: {
|
|
56
|
+
pointerEvents: 'none',
|
|
57
|
+
height: '100%',
|
|
58
|
+
width: 1,
|
|
59
|
+
position: 'absolute',
|
|
60
|
+
left: 0,
|
|
61
|
+
background: 'red',
|
|
62
|
+
zIndex: 1001,
|
|
63
|
+
},
|
|
54
64
|
}));
|
|
65
|
+
/** A red vertical line at `coordX` with `children` labelled above it. */
|
|
66
|
+
export const VerticalGuideLine = observer(function VerticalGuideLine({ coordX, viewWidth, stickyTop, children, }) {
|
|
67
|
+
const { classes } = useStyles();
|
|
68
|
+
return (_jsxs(_Fragment, { children: [_jsx(GuideLabel, { coordX: coordX, viewWidth: viewWidth, stickyTop: stickyTop, children: children }), _jsx("div", { className: classes.guideLine, style: { transform: `translateX(${coordX}px)` } })] }));
|
|
69
|
+
});
|
|
55
70
|
function anchorStyle(stickyTop) {
|
|
56
71
|
return stickyTop === undefined
|
|
57
72
|
? { position: 'relative' }
|
|
@@ -77,7 +92,7 @@ function anchorStyle(stickyTop) {
|
|
|
77
92
|
* measured once per hover, not per mousemove — only `coordX` changes as the
|
|
78
93
|
* mouse moves, and that needs no measurement.
|
|
79
94
|
*/
|
|
80
|
-
export function GuideLabel({ coordX, viewWidth, stickyTop, children, }) {
|
|
95
|
+
export const GuideLabel = observer(function GuideLabel({ coordX, viewWidth, stickyTop, children, }) {
|
|
81
96
|
const { classes, cx } = useStyles();
|
|
82
97
|
const anchorRef = useRef(null);
|
|
83
98
|
const [anchorPos, setAnchorPos] = useState();
|
|
@@ -106,14 +121,14 @@ export function GuideLabel({ coordX, viewWidth, stickyTop, children, }) {
|
|
|
106
121
|
bottom: anchorPos.viewportHeight - anchorPos.top + LABEL_GAP,
|
|
107
122
|
}, children: children }), document.body)
|
|
108
123
|
: null] }));
|
|
109
|
-
}
|
|
124
|
+
});
|
|
110
125
|
/**
|
|
111
126
|
* The bp coordinates of a rubberband selection, one label just outside each edge
|
|
112
127
|
* of the span. Renders inside the span, so the edges are its own box. A label
|
|
113
128
|
* with no room outside its edge flips inside instead, which is why the span it
|
|
114
129
|
* has to fit in is a prop.
|
|
115
130
|
*/
|
|
116
|
-
export function SpanEdgeLabels({ stickyTop, left, right, insideLeft, insideRight, }) {
|
|
131
|
+
export const SpanEdgeLabels = observer(function SpanEdgeLabels({ stickyTop, left, right, insideLeft, insideRight, }) {
|
|
117
132
|
const { classes, cx } = useStyles();
|
|
118
133
|
return (_jsxs("div", { className: classes.anchor, style: anchorStyle(stickyTop), children: [_jsx("div", { className: cx(classes.label, classes.spanLabel, insideLeft ? classes.insideLeftEdge : classes.leftOfEdge), children: left }), _jsx("div", { className: cx(classes.label, classes.spanLabel, insideRight ? classes.insideRightEdge : classes.rightOfEdge), children: right })] }));
|
|
119
|
-
}
|
|
134
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
export interface DragEnd {
|
|
3
|
+
startX: number;
|
|
4
|
+
endX: number;
|
|
5
|
+
isClick: boolean;
|
|
6
|
+
clientX: number;
|
|
7
|
+
clientY: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Follow a press that began at `startX` (px from `ref`'s left edge) through
|
|
11
|
+
* window events until it ends, so the pointer can leave the element mid-drag.
|
|
12
|
+
* A release within 3px of the press is a click. Undefined `startX` is no drag.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useWindowDrag(ref: React.RefObject<HTMLElement | null>, startX: number | undefined, handlers: {
|
|
15
|
+
onMove: (x: number) => void;
|
|
16
|
+
onEnd: (end: DragEnd) => void;
|
|
17
|
+
onCancel: () => void;
|
|
18
|
+
}): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useEventCallback } from '@jbrowse/core/util/useEventCallback';
|
|
3
|
+
/**
|
|
4
|
+
* Follow a press that began at `startX` (px from `ref`'s left edge) through
|
|
5
|
+
* window events until it ends, so the pointer can leave the element mid-drag.
|
|
6
|
+
* A release within 3px of the press is a click. Undefined `startX` is no drag.
|
|
7
|
+
*/
|
|
8
|
+
export function useWindowDrag(ref, startX, handlers) {
|
|
9
|
+
const onMove = useEventCallback(handlers.onMove);
|
|
10
|
+
const onEnd = useEventCallback(handlers.onEnd);
|
|
11
|
+
const onCancel = useEventCallback(handlers.onCancel);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const el = ref.current;
|
|
14
|
+
if (startX === undefined || !el) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const { left } = el.getBoundingClientRect();
|
|
18
|
+
const mouseMove = (event) => {
|
|
19
|
+
onMove(event.clientX - left);
|
|
20
|
+
};
|
|
21
|
+
const mouseUp = ({ button, clientX, clientY }) => {
|
|
22
|
+
if (button === 0) {
|
|
23
|
+
const endX = clientX - left;
|
|
24
|
+
onEnd({
|
|
25
|
+
startX,
|
|
26
|
+
endX,
|
|
27
|
+
isClick: Math.abs(endX - startX) <= 3,
|
|
28
|
+
clientX,
|
|
29
|
+
clientY,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const keyDown = (event) => {
|
|
34
|
+
if (event.key === 'Escape') {
|
|
35
|
+
onCancel();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
window.addEventListener('mousemove', mouseMove);
|
|
39
|
+
window.addEventListener('mouseup', mouseUp);
|
|
40
|
+
window.addEventListener('keydown', keyDown);
|
|
41
|
+
return () => {
|
|
42
|
+
window.removeEventListener('mousemove', mouseMove);
|
|
43
|
+
window.removeEventListener('mouseup', mouseUp);
|
|
44
|
+
window.removeEventListener('keydown', keyDown);
|
|
45
|
+
};
|
|
46
|
+
}, [ref, startX, onMove, onEnd, onCancel]);
|
|
47
|
+
}
|