react-msaview 5.9.0 → 5.10.0

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 (71) hide show
  1. package/bundle/index.js +76 -76
  2. package/bundle/index.js.map +3 -3
  3. package/dist/components/ConservationTrack.js +21 -16
  4. package/dist/components/ConservationTrack.js.map +1 -1
  5. package/dist/components/MSAView.js +17 -23
  6. package/dist/components/MSAView.js.map +1 -1
  7. package/dist/components/TextTrack.js +26 -21
  8. package/dist/components/TextTrack.js.map +1 -1
  9. package/dist/components/header/settingsMenuItems.js +39 -72
  10. package/dist/components/header/settingsMenuItems.js.map +1 -1
  11. package/dist/components/msa/MSACanvasBlock.js +19 -16
  12. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  13. package/dist/components/msa/MSAMouseoverCanvas.js +13 -6
  14. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  15. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +9 -6
  16. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  17. package/dist/components/msa/renderMSAMouseover.js +31 -31
  18. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  19. package/dist/components/overlayColors.d.ts +2 -0
  20. package/dist/components/overlayColors.js +5 -0
  21. package/dist/components/overlayColors.js.map +1 -1
  22. package/dist/components/tree/TreeCanvas.js +29 -24
  23. package/dist/components/tree/TreeCanvas.js.map +1 -1
  24. package/dist/components/tree/TreeCanvasBlock.js +19 -12
  25. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  26. package/dist/components/tree/renderTreeCanvas.d.ts +0 -38
  27. package/dist/components/tree/renderTreeCanvas.js +17 -17
  28. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  29. package/dist/model.js +82 -113
  30. package/dist/model.js.map +1 -1
  31. package/dist/neighborJoining.js +12 -19
  32. package/dist/neighborJoining.js.map +1 -1
  33. package/dist/parseAsn1.js +32 -43
  34. package/dist/parseAsn1.js.map +1 -1
  35. package/dist/renderToSvg.js +3 -0
  36. package/dist/renderToSvg.js.map +1 -1
  37. package/dist/seqPosToGlobalCol.d.ts +6 -19
  38. package/dist/seqPosToGlobalCol.js +6 -33
  39. package/dist/seqPosToGlobalCol.js.map +1 -1
  40. package/dist/useCanvasAutorun.d.ts +23 -1
  41. package/dist/useCanvasAutorun.js +20 -8
  42. package/dist/useCanvasAutorun.js.map +1 -1
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/dist/version.js.map +1 -1
  46. package/package.json +3 -3
  47. package/src/components/ConservationTrack.tsx +7 -5
  48. package/src/components/MSAView.tsx +24 -30
  49. package/src/components/TextTrack.tsx +7 -12
  50. package/src/components/header/settingsMenuItems.ts +40 -72
  51. package/src/components/msa/MSACanvasBlock.tsx +16 -21
  52. package/src/components/msa/MSAMouseoverCanvas.tsx +10 -6
  53. package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +51 -0
  54. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +13 -8
  55. package/src/components/msa/renderMSAMouseover.ts +37 -32
  56. package/src/components/overlayColors.ts +6 -0
  57. package/src/components/tree/TreeCanvas.tsx +6 -4
  58. package/src/components/tree/TreeCanvasBlock.tsx +11 -12
  59. package/src/components/tree/renderTreeCanvas.ts +20 -17
  60. package/src/model.ts +97 -130
  61. package/src/modelFilehandleLoaders.test.ts +137 -0
  62. package/src/neighborJoining.test.ts +12 -0
  63. package/src/neighborJoining.ts +12 -20
  64. package/src/parseAsn1.ts +32 -40
  65. package/src/{renderDomainLegendSvg.test.tsx → renderDomainsSvg.test.tsx} +44 -20
  66. package/src/renderToSvg.tsx +3 -0
  67. package/src/seqPosToGlobalCol.test.ts +39 -51
  68. package/src/seqPosToGlobalCol.ts +6 -41
  69. package/src/useCanvasAutorun.test.tsx +98 -0
  70. package/src/useCanvasAutorun.ts +30 -11
  71. package/src/version.ts +1 -1
@@ -1,9 +1,12 @@
1
- import { multiRowHoverColor, referenceColor } from '../overlayColors.ts'
1
+ import {
2
+ clickColor,
3
+ hoverColor,
4
+ multiRowHoverColor,
5
+ referenceColor,
6
+ } from '../overlayColors.ts'
2
7
 
3
8
  import type { MsaViewModel } from '../../model.ts'
4
9
 
5
- const hoverColor = 'rgba(0,0,0,0.15)'
6
- const highlightColor = 'rgba(128,128,0,0.2)'
7
10
  // the persistent highlightColumns overlay: a stronger fill plus a solid border
8
11
  // so a domain/motif band reads clearly over the colored alignment cells (the
9
12
  // faint hover-style wash alone is invisible against clustalx coloring)
@@ -39,48 +42,50 @@ export function renderMouseover({
39
42
  ctx.clearRect(0, 0, width * highResScaleFactor, height * highResScaleFactor)
40
43
  ctx.scale(highResScaleFactor, highResScaleFactor)
41
44
 
42
- // Highlight reference row (relativeTo) persistently
45
+ // rows and columns sit at a fixed pitch, so an index is all it takes to place
46
+ // a full-width or full-height band (matching fillRow in TreeCanvas)
47
+ const rowBand = (index: number) => {
48
+ ctx.fillRect(0, index * rowHeight + scrollY, width, rowHeight)
49
+ }
50
+ const colBand = (index: number) => {
51
+ ctx.fillRect(index * colWidth + scrollX, 0, colWidth, height)
52
+ }
53
+
54
+ // the reference row (relativeTo) stays lit, as does every tip under a hovered
55
+ // tree node
43
56
  if (referenceRowIndex !== undefined) {
44
57
  ctx.fillStyle = referenceColor
45
- ctx.fillRect(0, referenceRowIndex * rowHeight + scrollY, width, rowHeight)
58
+ rowBand(referenceRowIndex)
46
59
  }
47
-
48
- // Highlight multiple rows when hovering over tree nodes with children
49
- if (hoveredRowIndices.length > 0) {
50
- ctx.fillStyle = multiRowHoverColor
51
- for (const rowIndex of hoveredRowIndices) {
52
- ctx.fillRect(0, rowIndex * rowHeight + scrollY, width, rowHeight)
53
- }
60
+ ctx.fillStyle = multiRowHoverColor
61
+ for (const rowIndex of hoveredRowIndices) {
62
+ rowBand(rowIndex)
54
63
  }
55
64
 
56
- // Highlight multiple columns draw each contiguous run as a filled, bordered
57
- // band so a domain/motif highlight is clearly visible over the alignment
58
- if (highlightedColumnRuns.length > 0) {
59
- ctx.lineWidth = 2
60
- for (const { start, end } of highlightedColumnRuns) {
61
- const x = start * colWidth + scrollX
62
- const w = (end - start + 1) * colWidth
63
- ctx.fillStyle = highlightColumnsFill
64
- ctx.fillRect(x, 0, w, height)
65
- ctx.strokeStyle = highlightColumnsBorder
66
- ctx.strokeRect(x, 0, w, height)
67
- }
65
+ // each contiguous run of highlighted columns draws as one filled, bordered
66
+ // band, so a domain/motif highlight stays visible over the alignment
67
+ ctx.lineWidth = 2
68
+ for (const { start, end } of highlightedColumnRuns) {
69
+ const x = start * colWidth + scrollX
70
+ const w = (end - start + 1) * colWidth
71
+ ctx.fillStyle = highlightColumnsFill
72
+ ctx.fillRect(x, 0, w, height)
73
+ ctx.strokeStyle = highlightColumnsBorder
74
+ ctx.strokeRect(x, 0, w, height)
68
75
  }
69
76
 
77
+ ctx.fillStyle = hoverColor
70
78
  if (mouseCol !== undefined) {
71
- ctx.fillStyle = hoverColor
72
- ctx.fillRect(mouseCol * colWidth + scrollX, 0, colWidth, height)
79
+ colBand(mouseCol)
73
80
  }
74
81
  if (mouseRow !== undefined) {
75
- ctx.fillStyle = hoverColor
76
- ctx.fillRect(0, mouseRow * rowHeight + scrollY, width, rowHeight)
82
+ rowBand(mouseRow)
77
83
  }
84
+ ctx.fillStyle = clickColor
78
85
  if (mouseClickCol !== undefined) {
79
- ctx.fillStyle = highlightColor
80
- ctx.fillRect(mouseClickCol * colWidth + scrollX, 0, colWidth, height)
86
+ colBand(mouseClickCol)
81
87
  }
82
88
  if (mouseClickRow !== undefined) {
83
- ctx.fillStyle = highlightColor
84
- ctx.fillRect(0, mouseClickRow * rowHeight + scrollY, width, rowHeight)
89
+ rowBand(mouseClickRow)
85
90
  }
86
91
  }
@@ -10,3 +10,9 @@ export const referenceColor = 'rgba(0,128,255,0.3)'
10
10
  // while the alignment cells are already colored so a lighter wash reads better.
11
11
  export const treeHoverColor = 'rgba(255,165,0,0.2)'
12
12
  export const multiRowHoverColor = 'rgba(255,165,0,0.15)'
13
+
14
+ // the hovered column/row, and the one pinned by a click. Drawn on the alignment
15
+ // overlay canvas and again as plain divs over the track area, which have to
16
+ // match or the crosshair appears to change color as it crosses the tracks.
17
+ export const hoverColor = 'rgba(0,0,0,0.15)'
18
+ export const clickColor = 'rgba(128,128,0,0.2)'
@@ -22,8 +22,8 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
22
22
  )
23
23
  const { onMouseDown, onMouseUp } = useWheelScroll({ ref, onScrollY })
24
24
 
25
- const mouseoverRef = useCanvasAutorun(
26
- ctx => {
25
+ const mouseoverRef = useCanvasAutorun({
26
+ draw: ctx => {
27
27
  if (isAlive(model)) {
28
28
  const {
29
29
  rowHeight,
@@ -61,8 +61,10 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
61
61
  }
62
62
  }
63
63
  },
64
- [model],
65
- )
64
+ width: treeAreaWidth,
65
+ height,
66
+ deps: [model],
67
+ })
66
68
 
67
69
  return (
68
70
  <div
@@ -60,15 +60,12 @@ const TreeCanvasBlock = observer(function ({
60
60
  const width = treeAreaWidth + padding
61
61
  const height = blockSize
62
62
 
63
- const ref = useCanvasAutorun(
64
- ctx => {
63
+ const canvasWidth = width * highResScaleFactor
64
+ const canvasHeight = height * highResScaleFactor
65
+ const ref = useCanvasAutorun({
66
+ draw: ctx => {
65
67
  ctx.resetTransform()
66
- ctx.clearRect(
67
- 0,
68
- 0,
69
- width * highResScaleFactor,
70
- height * highResScaleFactor,
71
- )
68
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight)
72
69
  renderTreeCanvas({
73
70
  ctx,
74
71
  model,
@@ -77,8 +74,10 @@ const TreeCanvasBlock = observer(function ({
77
74
  theme,
78
75
  })
79
76
  },
80
- [model, clickMap, offsetY, theme],
81
- )
77
+ width: canvasWidth,
78
+ height: canvasHeight,
79
+ deps: [model, clickMap, offsetY, theme],
80
+ })
82
81
 
83
82
  const style = {
84
83
  width,
@@ -112,8 +111,8 @@ const TreeCanvasBlock = observer(function ({
112
111
 
113
112
  <canvas
114
113
  ref={ref}
115
- width={width * highResScaleFactor}
116
- height={height * highResScaleFactor}
114
+ width={canvasWidth}
115
+ height={canvasHeight}
117
116
  style={{ ...style, cursor: hovered ? 'pointer' : 'default' }}
118
117
  onMouseMove={event => {
119
118
  onMouseMove(event)
@@ -45,7 +45,7 @@ export function getNodeX(
45
45
  return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * tipX
46
46
  }
47
47
 
48
- export function renderTree({
48
+ function renderTree({
49
49
  offsetY,
50
50
  ctx,
51
51
  model,
@@ -89,7 +89,7 @@ export function renderTree({
89
89
  })
90
90
  }
91
91
 
92
- export function renderCollapsedTriangles({
92
+ function renderCollapsedTriangles({
93
93
  ctx,
94
94
  clickMap,
95
95
  offsetY,
@@ -98,6 +98,7 @@ export function renderCollapsedTriangles({
98
98
  tipX,
99
99
  maxDepthToLeaf,
100
100
  blockSizeYOverride,
101
+ collapsedSet,
101
102
  }: {
102
103
  ctx: RenderCtx
103
104
  clickMap?: ClickMapIndex
@@ -107,11 +108,11 @@ export function renderCollapsedTriangles({
107
108
  tipX: number
108
109
  maxDepthToLeaf: number
109
110
  blockSizeYOverride?: number
111
+ collapsedSet: Set<string>
110
112
  }) {
111
113
  const {
112
114
  hierarchy,
113
115
  showBranchLenEffective: showBranchLen,
114
- collapsed,
115
116
  blockSize,
116
117
  rowHeight,
117
118
  fontSize,
@@ -119,10 +120,9 @@ export function renderCollapsedTriangles({
119
120
  } = model
120
121
  // nothing collapsed is the common case, and the traversal below visits every
121
122
  // node in the tree on every block of every redraw
122
- if (collapsed.length === 0) {
123
+ if (collapsedSet.size === 0) {
123
124
  return
124
125
  }
125
- const collapsedSet = new Set(collapsed)
126
126
  const by = blockSizeYOverride ?? blockSize
127
127
  const halfHeight = Math.max(2, rowHeight * 0.42)
128
128
  forEachDescendant(hierarchy, node => {
@@ -171,7 +171,7 @@ export function renderCollapsedTriangles({
171
171
  })
172
172
  }
173
173
 
174
- export function renderNodeBubbles({
174
+ function renderNodeBubbles({
175
175
  ctx,
176
176
  clickMap,
177
177
  offsetY,
@@ -179,6 +179,7 @@ export function renderNodeBubbles({
179
179
  tipX,
180
180
  maxDepthToLeaf,
181
181
  blockSizeYOverride,
182
+ collapsedSet,
182
183
  }: {
183
184
  ctx: RenderCtx
184
185
  clickMap?: ClickMapIndex
@@ -187,16 +188,15 @@ export function renderNodeBubbles({
187
188
  tipX: number
188
189
  maxDepthToLeaf: number
189
190
  blockSizeYOverride?: number
191
+ collapsedSet: Set<string>
190
192
  }) {
191
193
  const {
192
194
  hierarchy,
193
195
  showBranchLenEffective: showBranchLen,
194
- collapsed,
195
196
  blockSize,
196
197
  marginLeft: ml,
197
198
  } = model
198
199
  const by = blockSizeYOverride ?? blockSize
199
- const collapsedSet = new Set(collapsed)
200
200
  forEachDescendant(hierarchy, node => {
201
201
  const x = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf)
202
202
  if (x === undefined) {
@@ -231,7 +231,7 @@ export function renderNodeBubbles({
231
231
  })
232
232
  }
233
233
 
234
- export function renderTreeLabels({
234
+ function renderTreeLabels({
235
235
  theme,
236
236
  model,
237
237
  offsetY,
@@ -240,6 +240,7 @@ export function renderTreeLabels({
240
240
  tipX,
241
241
  maxDepthToLeaf,
242
242
  blockSizeYOverride,
243
+ collapsedSet,
243
244
  }: {
244
245
  model: MsaViewModel
245
246
  offsetY: number
@@ -249,6 +250,7 @@ export function renderTreeLabels({
249
250
  tipX: number
250
251
  maxDepthToLeaf: number
251
252
  blockSizeYOverride?: number
253
+ collapsedSet: Set<string>
252
254
  }) {
253
255
  const {
254
256
  fontSize,
@@ -262,16 +264,11 @@ export function renderTreeLabels({
262
264
  marginLeft,
263
265
  noTree,
264
266
  labelWidthMap,
265
- collapsed,
266
267
  } = model
268
+ const by = blockSizeYOverride ?? blockSize
267
269
  // labels only exist for leaves, which are laid out top to bottom, so take the
268
270
  // same slice the MSA renderer uses instead of walking every tip per block
269
- const visibleLeaves = getVisibleLeaves({
270
- model,
271
- offsetY,
272
- blockSizeY: blockSizeYOverride ?? model.blockSize,
273
- })
274
- const by = blockSizeYOverride ?? blockSize
271
+ const visibleLeaves = getVisibleLeaves({ model, offsetY, blockSizeY: by })
275
272
  const emHeight = ctx.measureText('M').width
276
273
  if (labelsAlignRight) {
277
274
  ctx.textAlign = 'right'
@@ -289,7 +286,7 @@ export function renderTreeLabels({
289
286
  const displayName = treeMetadata[name]?.genome || name
290
287
  // a collapsed clade is drawn as a triangle + tip count; suppress its leaf
291
288
  // label when the "name" is just the auto-generated internal-node id
292
- const isAnonymousCollapsed = collapsed.includes(id) && name === id
289
+ const isAnonymousCollapsed = collapsedSet.has(id) && name === id
293
290
  if (!isAnonymousCollapsed && inYBlock(y, offsetY, by)) {
294
291
  // note: +rowHeight/4 matches with -rowHeight/4 in msa
295
292
  const yp = y + fontSize / 4
@@ -389,6 +386,9 @@ export function renderTreeCanvas({
389
386
  // maxBranchLength for the cladogram would pin every node to x=0 for a tree
390
387
  // with no branch lengths -- which is exactly what forces cladogram mode.
391
388
  const tipX = showBranchLenEffective ? maxBranchLength : treeWidth
389
+ // built once for all three passes: each of them asks about a different node on
390
+ // every block of every redraw, and `collapsed` is a list
391
+ const collapsedSet = new Set(model.collapsed)
392
392
 
393
393
  if (!noTree && drawTree) {
394
394
  renderTree({
@@ -410,6 +410,7 @@ export function renderTreeCanvas({
410
410
  tipX,
411
411
  maxDepthToLeaf,
412
412
  blockSizeYOverride,
413
+ collapsedSet,
413
414
  })
414
415
 
415
416
  if (drawNodeBubbles) {
@@ -421,6 +422,7 @@ export function renderTreeCanvas({
421
422
  tipX,
422
423
  maxDepthToLeaf,
423
424
  blockSizeYOverride,
425
+ collapsedSet,
424
426
  })
425
427
  }
426
428
  }
@@ -435,6 +437,7 @@ export function renderTreeCanvas({
435
437
  tipX,
436
438
  maxDepthToLeaf,
437
439
  blockSizeYOverride,
440
+ collapsedSet,
438
441
  })
439
442
  }
440
443
 
package/src/model.ts CHANGED
@@ -1635,8 +1635,8 @@ function stateModelFactory() {
1635
1635
  return 0
1636
1636
  }
1637
1637
  const col = self.seqPosGlobalColIndex.get(rowName)?.[seqPos]
1638
- // past the end of the ungapped sequence: 0 for the degenerate empty-row
1639
- // case, otherwise one past the last column, as the scan version returned
1638
+ // past the end of the ungapped sequence: 0 for the degenerate all-gap
1639
+ // row, otherwise one past the last column
1640
1640
  return col ?? (seqPos === 0 ? 0 : seq.length)
1641
1641
  },
1642
1642
 
@@ -2084,79 +2084,100 @@ function stateModelFactory() {
2084
2084
  })
2085
2085
  }
2086
2086
 
2087
- // autorun opens treeFilehandle. generation guard: if the filehandle
2088
- // changes mid-fetch, a slower earlier request must not clobber the data
2089
- // (or loading flag) set by the newer one
2090
- let treeGeneration = 0
2091
- addDisposer(
2092
- self,
2093
- autorun(async () => {
2094
- const { treeFilehandle } = self
2095
- if (treeFilehandle) {
2096
- const generation = ++treeGeneration
2087
+ /**
2088
+ * Fetch a filehandle whenever it changes, and hand the text to
2089
+ * `onLoad`.
2090
+ *
2091
+ * Every loader carries a generation guard: the filehandle can change
2092
+ * mid-fetch (a second file picked while the first is still in flight),
2093
+ * and the slower earlier request must not clobber the data, status, or
2094
+ * loading flag belonging to the newer one.
2095
+ *
2096
+ * `clearFilehandle` serves two purposes for the loaders that pass it.
2097
+ * A local file has no URL to refetch from, so the handle is dropped
2098
+ * once its bytes are in the model; and a fetch the user cancels drops
2099
+ * it too, returning the view to the import form rather than leaving a
2100
+ * stuck spinner.
2101
+ */
2102
+ const loadOnFilehandleChange = ({
2103
+ getFilehandle,
2104
+ onLoad,
2105
+ setLoading,
2106
+ clearFilehandle,
2107
+ }: {
2108
+ getFilehandle: () => FileLocationType | undefined
2109
+ onLoad: (text: string) => void
2110
+ setLoading?: (arg: boolean) => void
2111
+ clearFilehandle?: () => void
2112
+ }) => {
2113
+ let generation = 0
2114
+ addDisposer(
2115
+ self,
2116
+ autorun(async () => {
2117
+ const filehandle = getFilehandle()
2118
+ if (!filehandle) {
2119
+ return
2120
+ }
2121
+ const current = ++generation
2122
+ const isCurrent = () => current === generation
2097
2123
  try {
2098
- self.setLoadingTree(true)
2124
+ setLoading?.(true)
2125
+ self.setError(undefined)
2099
2126
  const text = await fetchTextWithProgress(
2100
- openLocation(treeFilehandle),
2127
+ openLocation(filehandle),
2101
2128
  status => {
2102
- if (generation === treeGeneration) {
2129
+ if (isCurrent()) {
2103
2130
  self.setStatus(status)
2104
2131
  }
2105
2132
  },
2106
2133
  )
2107
- if (generation === treeGeneration) {
2134
+ if (isCurrent()) {
2108
2135
  transaction(() => {
2109
- self.setTree(text)
2110
- if (treeFilehandle.locationType === 'BlobLocation') {
2111
- // clear filehandle after loading if from a local file
2112
- self.setTreeFilehandle(undefined)
2136
+ onLoad(text)
2137
+ if (filehandle.locationType === 'BlobLocation') {
2138
+ clearFilehandle?.()
2113
2139
  }
2114
2140
  })
2115
2141
  }
2116
2142
  } catch (e) {
2117
- if (generation === treeGeneration) {
2143
+ if (isCurrent()) {
2118
2144
  if (isAbortError(e)) {
2119
- // cancelled by the user: drop the filehandle so the view
2120
- // returns to the import form instead of a stuck spinner
2121
- self.setTreeFilehandle(undefined)
2145
+ clearFilehandle?.()
2122
2146
  } else {
2123
2147
  console.error(e)
2124
2148
  self.setError(e)
2125
2149
  }
2126
2150
  }
2127
2151
  } finally {
2128
- if (generation === treeGeneration) {
2129
- self.setLoadingTree(false)
2130
- }
2131
- }
2132
- }
2133
- }),
2134
- )
2135
- // autorun opens treeMetadataFilehandle
2136
- addDisposer(
2137
- self,
2138
- autorun(async () => {
2139
- const { treeMetadataFilehandle } = self
2140
- if (treeMetadataFilehandle) {
2141
- try {
2142
- self.setTreeMetadata(
2143
- await fetchTextWithProgress(
2144
- openLocation(treeMetadataFilehandle),
2145
- status => {
2146
- self.setStatus(status)
2147
- },
2148
- ),
2149
- )
2150
- } catch (e) {
2151
- // ignore user cancel (isAbortError); treeMetadata is optional
2152
- if (!isAbortError(e)) {
2153
- console.error(e)
2154
- self.setError(e)
2152
+ if (isCurrent()) {
2153
+ setLoading?.(false)
2155
2154
  }
2156
2155
  }
2157
- }
2158
- }),
2159
- )
2156
+ }),
2157
+ )
2158
+ }
2159
+
2160
+ loadOnFilehandleChange({
2161
+ getFilehandle: () => self.treeFilehandle,
2162
+ onLoad: text => {
2163
+ self.setTree(text)
2164
+ },
2165
+ setLoading: arg => {
2166
+ self.setLoadingTree(arg)
2167
+ },
2168
+ clearFilehandle: () => {
2169
+ self.setTreeFilehandle(undefined)
2170
+ },
2171
+ })
2172
+
2173
+ // treeMetadata is decorative and has no import-form step of its own, so
2174
+ // it keeps no loading flag and nothing to return to on cancel
2175
+ loadOnFilehandleChange({
2176
+ getFilehandle: () => self.treeMetadataFilehandle,
2177
+ onLoad: text => {
2178
+ self.setTreeMetadata(text)
2179
+ },
2180
+ })
2160
2181
 
2161
2182
  // autorun parses inline gff text from data.gff
2162
2183
  addDisposer(
@@ -2174,83 +2195,29 @@ function stateModelFactory() {
2174
2195
  }),
2175
2196
  )
2176
2197
 
2177
- // autorun opens gffFilehandle for InterProScan domains
2178
- addDisposer(
2179
- self,
2180
- autorun(async () => {
2181
- const { gffFilehandle } = self
2182
- if (gffFilehandle) {
2183
- try {
2184
- const gffText = await fetchTextWithProgress(
2185
- openLocation(gffFilehandle),
2186
- status => {
2187
- self.setStatus(status)
2188
- },
2189
- )
2190
- self.applyGFFText(gffText)
2191
- if (gffFilehandle.locationType === 'BlobLocation') {
2192
- self.setGFFFilehandle(undefined)
2193
- }
2194
- } catch (e) {
2195
- // on user cancel (isAbortError) drop the filehandle
2196
- if (isAbortError(e)) {
2197
- self.setGFFFilehandle(undefined)
2198
- } else {
2199
- console.error(e)
2200
- self.setError(e)
2201
- }
2202
- }
2203
- }
2204
- }),
2205
- )
2198
+ // gffFilehandle carries InterProScan domains
2199
+ loadOnFilehandleChange({
2200
+ getFilehandle: () => self.gffFilehandle,
2201
+ onLoad: text => {
2202
+ self.applyGFFText(text)
2203
+ },
2204
+ clearFilehandle: () => {
2205
+ self.setGFFFilehandle(undefined)
2206
+ },
2207
+ })
2206
2208
 
2207
- // autorun opens msaFilehandle. generation guard: see treeFilehandle
2208
- let msaGeneration = 0
2209
- addDisposer(
2210
- self,
2211
- autorun(async () => {
2212
- const { msaFilehandle } = self
2213
- if (msaFilehandle) {
2214
- const generation = ++msaGeneration
2215
- try {
2216
- self.setLoadingMSA(true)
2217
- self.setError(undefined)
2218
- const txt = await fetchTextWithProgress(
2219
- openLocation(msaFilehandle),
2220
- status => {
2221
- if (generation === msaGeneration) {
2222
- self.setStatus(status)
2223
- }
2224
- },
2225
- )
2226
- if (generation === msaGeneration) {
2227
- transaction(() => {
2228
- self.setMSA(txt)
2229
- if (msaFilehandle.locationType === 'BlobLocation') {
2230
- // clear filehandle after loading if from a local file
2231
- self.setMSAFilehandle(undefined)
2232
- }
2233
- })
2234
- }
2235
- } catch (e) {
2236
- if (generation === msaGeneration) {
2237
- if (isAbortError(e)) {
2238
- // cancelled by the user: drop the filehandle so the view
2239
- // returns to the import form instead of a stuck spinner
2240
- self.setMSAFilehandle(undefined)
2241
- } else {
2242
- console.error(e)
2243
- self.setError(e)
2244
- }
2245
- }
2246
- } finally {
2247
- if (generation === msaGeneration) {
2248
- self.setLoadingMSA(false)
2249
- }
2250
- }
2251
- }
2252
- }),
2253
- )
2209
+ loadOnFilehandleChange({
2210
+ getFilehandle: () => self.msaFilehandle,
2211
+ onLoad: text => {
2212
+ self.setMSA(text)
2213
+ },
2214
+ setLoading: arg => {
2215
+ self.setLoadingMSA(arg)
2216
+ },
2217
+ clearFilehandle: () => {
2218
+ self.setMSAFilehandle(undefined)
2219
+ },
2220
+ })
2254
2221
 
2255
2222
  // Keep the parse chain warm: reading self.columns transitively holds
2256
2223
  // self.MSA (parseMSA) computed alive, so it is parsed once per data