@teselagen/ove 0.7.6 → 0.7.8

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.
@@ -4,7 +4,13 @@ import {
4
4
  Droppable,
5
5
  Draggable as DndDraggable
6
6
  } from "@hello-pangea/dnd";
7
- import React from "react";
7
+ import React, {
8
+ useCallback,
9
+ useEffect,
10
+ useMemo,
11
+ useRef,
12
+ useState
13
+ } from "react";
8
14
  import { connect } from "react-redux";
9
15
  import {
10
16
  Button,
@@ -19,17 +25,8 @@ import {
19
25
  EditableText
20
26
  } from "@blueprintjs/core";
21
27
  import { InfoHelper, Loading, showContextMenu, withStore } from "@teselagen/ui";
22
- import { store } from "@risingstack/react-easy-state";
23
- import {
24
- throttle,
25
- // cloneDeep,
26
- map,
27
- some,
28
- forEach,
29
- isFunction,
30
- unset,
31
- omit
32
- } from "lodash-es";
28
+ import { store as _store } from "@risingstack/react-easy-state";
29
+ import { throttle, map, some, isFunction, unset, omit } from "lodash-es";
33
30
  import { getSequenceDataBetweenRange } from "@teselagen/sequence-utils";
34
31
  import ReactList from "@teselagen/react-list";
35
32
  import ReactDOM from "react-dom";
@@ -39,7 +36,7 @@ import Minimap, { getTrimmedRangesToDisplay } from "./Minimap";
39
36
  import { compose, branch, renderComponent } from "recompose";
40
37
  import AlignmentVisibilityTool from "./AlignmentVisibilityTool";
41
38
  import * as alignmentActions from "../redux/alignments";
42
- import estimateRowHeight from "../RowView/estimateRowHeight";
39
+ import _estimateRowHeight from "../RowView/estimateRowHeight";
43
40
  import prepareRowData from "../utils/prepareRowData";
44
41
  import withEditorProps from "../withEditorProps";
45
42
 
@@ -48,7 +45,7 @@ import {
48
45
  editorDragged,
49
46
  editorClicked,
50
47
  editorDragStarted,
51
- updateSelectionOrCaret,
48
+ updateSelectionOrCaret as _updateSelectionOrCaret,
52
49
  editorDragStopped
53
50
  } from "../withEditorInteractions/clickAndDragUtils";
54
51
  import { ResizeSensor } from "@blueprintjs/core";
@@ -91,119 +88,95 @@ try {
91
88
  );
92
89
  }
93
90
 
94
- export class AlignmentView extends React.Component {
95
- bindOutsideChangeHelper = {};
96
- constructor(props) {
97
- super(props);
98
- window.scrollAlignmentToPercent = this.scrollAlignmentToPercent;
99
- if (window.Cypress)
100
- window.Cypress.scrollAlignmentToPercent = this.scrollAlignmentToPercent;
101
- this.onShortcutCopy = document.addEventListener(
102
- "keydown",
103
- this.handleAlignmentCopy
104
- );
105
- }
106
-
107
- getMaxLength = () => {
108
- const { alignmentTracks } = this.props;
109
- const { sequenceData = { sequence: "" }, alignmentData } =
110
- alignmentTracks[0];
111
- const data = alignmentData || sequenceData;
112
- return data.noSequence ? data.size : data.sequence.length;
113
- };
114
-
115
- getNearestCursorPositionToMouseEvent(rowData, event, callback) {
116
- this.charWidth = this.getCharWidthInLinearView();
117
- //loop through all the rendered rows to see if the click event lands in one of them
118
- let nearestCaretPos = 0;
119
- const rowDomNode = this.veTracksAndAlignmentHolder;
120
- const boundingRowRect = rowDomNode.getBoundingClientRect();
121
- const maxEnd = this.getMaxLength();
122
- if (
123
- getClientX(event) - boundingRowRect.left - this.state.nameDivWidth <
124
- 0
125
- ) {
126
- nearestCaretPos = 0;
127
- } else {
128
- const clickXPositionRelativeToRowContainer =
129
- getClientX(event) - boundingRowRect.left - this.state.nameDivWidth;
130
- const numberOfBPsInFromRowStart = Math.floor(
131
- (clickXPositionRelativeToRowContainer + this.charWidth / 2) /
132
- this.charWidth
133
- );
134
- nearestCaretPos = numberOfBPsInFromRowStart + 0;
135
- if (nearestCaretPos > maxEnd + 1) {
136
- nearestCaretPos = maxEnd + 1;
137
- }
138
- }
139
- if (this.props.sequenceData && this.props.sequenceData.isProtein) {
140
- nearestCaretPos = Math.round(nearestCaretPos / 3) * 3;
141
- }
142
- if (this.props.sequenceLength === 0) nearestCaretPos = 0;
143
- const callbackVals = {
144
- updateSelectionOrCaret: this.updateSelectionOrCaret,
145
- nearestCaretPos,
146
- sequenceLength: this.getSequenceLength(),
147
- caretPosition: this.easyStore.caretPosition,
148
- selectionLayer: this.easyStore.selectionLayer,
149
- easyStore: this.easyStore,
150
- caretPositionUpdate: this.caretPositionUpdate,
151
- selectionLayerUpdate: this.selectionLayerUpdate,
152
- event,
153
- doNotWrapOrigin: true,
154
- shiftHeld: event.shiftKey,
155
- // caretGrabbed: event.target.className === "cursor",
156
- selectionStartGrabbed: event.target.classList.contains(
157
- draggableClassnames.selectionStart
158
- ),
159
- selectionEndGrabbed: event.target.classList.contains(
160
- draggableClassnames.selectionEnd
161
- )
162
- };
163
- callback(callbackVals);
164
- }
165
-
166
- componentWillUnmount() {
167
- if (window.Cypress) {
168
- delete window.scrollAlignmentToPercent;
169
- delete window.Cypress.scrollAlignmentToPercent;
170
- delete window.updateAlignmentSelection;
171
- delete window.Cypress.updateAlignmentSelection;
172
- }
173
- // const { removeAlignmentFromRedux, id } = this.props;
174
- // removeAlignmentFromRedux({ id });
175
- this.onShortcutCopy &&
176
- document.removeEventListener("keydown", this.handleAlignmentCopy);
177
- }
91
+ export const AlignmentView = props => {
92
+ const {
93
+ alignmentType,
94
+ alignmentRunUpdate,
95
+ alignmentName: _alignmentName,
96
+ caretPosition,
97
+ handleAlignmentSave,
98
+ id,
99
+ isFullyZoomedOut,
100
+ scrollPercentageToJumpTo,
101
+ selectionLayer,
102
+ sequenceData,
103
+ sequenceLength,
104
+ shouldAutosave,
105
+ store,
106
+ height,
107
+ minimapLaneHeight,
108
+ minimapLaneSpacing,
109
+ isInPairwiseOverviewView,
110
+ noVisibilityOptions,
111
+ updateAlignmentSortOrder,
112
+ alignmentSortOrder,
113
+ handleBackButtonClicked,
114
+ allowTrimming,
115
+ additionalSelectionLayerRightClickedOptions,
116
+ selectionLayerRightClicked,
117
+ additionalTopEl,
118
+ additionalTopLeftEl,
119
+ handleAlignmentRename,
120
+ stateTrackingId,
121
+ style,
122
+ unmappedSeqs
123
+ } = props;
124
+
125
+ const {
126
+ alignmentId,
127
+ alignmentTrackIndex,
128
+ alignmentTracks = [],
129
+ alignmentVisibilityToolOptions,
130
+ allowTrackNameEdit,
131
+ allowTrackRearrange,
132
+ currentPairwiseAlignmentIndex,
133
+ handleSelectTrack,
134
+ hasTemplate,
135
+ isPairwise,
136
+ linearViewOptions,
137
+ noClickDragHandlers,
138
+ pairwiseAlignments,
139
+ pairwiseOverviewAlignmentTracks,
140
+ upsertAlignmentRun,
141
+ ...rest
142
+ } = props;
178
143
 
179
- handleAlignmentCopy = event => {
180
- if (
181
- event.key === "c" &&
182
- !event.shiftKey &&
183
- (event.metaKey === true || event.ctrlKey === true)
184
- ) {
185
- const input = document.createElement("textarea");
186
- document.body.appendChild(input);
187
- const seqDataToCopy = this.getAllAlignmentsFastaText();
188
- input.value = seqDataToCopy;
189
- input.select();
190
- const copySuccess = document.execCommand("copy");
191
- if (!copySuccess) {
192
- window.toastr.error("Selection Not Copied");
193
- } else {
194
- window.toastr.success("Selection Copied");
195
- }
196
- document.body.removeChild(input);
197
- event.preventDefault();
198
- }
199
- };
144
+ const [width, setWidth] = useState(0);
145
+ const [nameDivWidth, setNameDivWidth] = useState(140);
146
+ const [charWidthInLinearView, _setCharWidthInLinearView] = useState(
147
+ charWidthInLinearViewDefault
148
+ );
149
+ const [alignmentName, setAlignmentName] = useState(_alignmentName);
150
+ const [isTrackDragging, setIsTrackDragging] = useState(false);
151
+ const [saveMessage, setSaveMessage] = useState();
152
+ const [saveMessageLoading, setSaveMessageLoading] = useState(false);
153
+ const [tempTrimBefore, setTempTrimBefore] = useState({});
154
+ const [tempTrimAfter, setTempTrimAfter] = useState({});
155
+ const [tempTrimmingCaret, setTempTrimmingCaret] = useState({});
156
+ const bindOutsideChangeHelper = useRef({});
157
+ const alignmentHolder = useRef(null);
158
+ const alignmentHolderTop = useRef(null);
159
+ const veTracksAndAlignmentHolder = useRef(null);
160
+ const InfiniteScroller = useRef(null);
161
+ const oldAlignmentHolderScrollTop = useRef(0);
162
+ const blockScroll = useRef(false);
163
+ const isZooming = useRef(false);
164
+ const rowData = useRef({});
165
+ const latestMouseY = useRef();
166
+ const easyStore = useRef(
167
+ _store({
168
+ selectionLayer: { start: -1, end: -1 },
169
+ caretPosition: -1,
170
+ percentScrolled: 0,
171
+ viewportWidth: 400,
172
+ verticalVisibleRange: { start: 0, end: 0 }
173
+ })
174
+ );
200
175
 
201
- getAllAlignmentsFastaText = () => {
176
+ const getAllAlignmentsFastaText = useCallback(() => {
202
177
  const selectionLayer =
203
- this.props.store.getState().VectorEditor.__allEditorsOptions.alignments[
204
- this.props.id
205
- ].selectionLayer || {};
206
- const { alignmentTracks } = this.props;
178
+ store.getState().VectorEditor.__allEditorsOptions.alignments[id]
179
+ .selectionLayer || {};
207
180
  const seqDataOfAllTracksToCopy = [];
208
181
  alignmentTracks.forEach(track => {
209
182
  const seqDataToCopy = getSequenceDataBetweenRange(
@@ -215,94 +188,307 @@ export class AlignmentView extends React.Component {
215
188
  );
216
189
  });
217
190
  return seqDataOfAllTracksToCopy.join("");
218
- };
191
+ }, [alignmentTracks, id, store]);
219
192
 
220
- state = {
221
- alignmentName: this.props.alignmentName,
222
- isTrackDragging: false,
223
- charWidthInLinearView: charWidthInLinearViewDefault,
224
- scrollAlignmentView: false,
225
- width: 0,
226
- nameDivWidth: 140
227
- };
193
+ useEffect(() => {
194
+ const handleAlignmentCopy = event => {
195
+ if (
196
+ event.key === "c" &&
197
+ !event.shiftKey &&
198
+ (event.metaKey === true || event.ctrlKey === true)
199
+ ) {
200
+ const input = document.createElement("textarea");
201
+ document.body.appendChild(input);
202
+ const seqDataToCopy = getAllAlignmentsFastaText();
203
+ input.value = seqDataToCopy;
204
+ input.select();
205
+ const copySuccess = document.execCommand("copy");
206
+ if (!copySuccess) {
207
+ window.toastr.error("Selection Not Copied");
208
+ } else {
209
+ window.toastr.success("Selection Copied");
210
+ }
211
+ document.body.removeChild(input);
212
+ event.preventDefault();
213
+ }
214
+ };
228
215
 
229
- easyStore = store({
230
- selectionLayer: { start: -1, end: -1 },
231
- caretPosition: -1,
232
- percentScrolled: 0,
233
- viewportWidth: 400,
234
- verticalVisibleRange: { start: 0, end: 0 }
235
- });
216
+ document.addEventListener("keydown", handleAlignmentCopy);
217
+ return () => {
218
+ document.removeEventListener("keydown", handleAlignmentCopy);
219
+ };
220
+ }, [getAllAlignmentsFastaText]);
236
221
 
237
- getMinCharWidth = noNameDiv => {
238
- const toReturn = Math.min(
239
- Math.max(
240
- this.state.width - (noNameDiv ? 0 : this.state.nameDivWidth) - 5,
241
- 1
242
- ) / this.getSequenceLength(),
243
- 10
244
- );
245
- if (isNaN(toReturn)) return 10;
246
- return toReturn;
222
+ const scrollAlignmentToPercent = scrollPercentage => {
223
+ const scrollPercentageToUse = Math.min(Math.max(scrollPercentage, 0), 1);
224
+ easyStore.current.percentScrolled = scrollPercentageToUse;
225
+ alignmentHolder.current.scrollLeft =
226
+ scrollPercentageToUse *
227
+ (alignmentHolder.current.scrollWidth -
228
+ alignmentHolder.current.clientWidth);
229
+ if (alignmentHolderTop.current) {
230
+ alignmentHolderTop.current.scrollLeft =
231
+ scrollPercentageToUse *
232
+ (alignmentHolderTop.current.scrollWidth -
233
+ alignmentHolderTop.current.clientWidth);
234
+ }
247
235
  };
248
236
 
249
- getSequenceLength = () => {
250
- const { alignmentTracks: [template] = [] } = this.props;
251
- return template.alignmentData.sequence.length || 1;
252
- };
253
- async componentDidUpdate(prevProps) {
254
- if (
255
- prevProps.scrollPercentageToJumpTo !==
256
- this.props.scrollPercentageToJumpTo &&
257
- this.props.scrollPercentageToJumpTo !== undefined
258
- ) {
259
- this.scrollAlignmentToPercent(this.props.scrollPercentageToJumpTo);
237
+ useEffect(() => {
238
+ window.scrollAlignmentToPercent = scrollAlignmentToPercent;
239
+ if (window.Cypress)
240
+ window.Cypress.scrollAlignmentToPercent = scrollAlignmentToPercent;
241
+
242
+ return () => {
243
+ delete window.scrollAlignmentToPercent;
244
+ if (window.Cypress) delete window.Cypress.scrollAlignmentToPercent;
245
+ };
246
+ }, []);
247
+
248
+ const maxLength = useMemo(() => {
249
+ const { sequenceData = { sequence: "" }, alignmentData } =
250
+ alignmentTracks[0];
251
+ const data = alignmentData || sequenceData;
252
+ return data.noSequence ? data.size : data.sequence.length;
253
+ }, [alignmentTracks]);
254
+
255
+ const getSequenceLength = useCallback(() => {
256
+ return alignmentTracks?.[0]?.alignmentData?.sequence?.length || 1;
257
+ }, [alignmentTracks]);
258
+
259
+ const getMinCharWidth = useCallback(
260
+ noNameDiv => {
261
+ const toReturn = Math.min(
262
+ Math.max(width - (noNameDiv ? 0 : nameDivWidth) - 5, 1) /
263
+ getSequenceLength(),
264
+ 10
265
+ );
266
+ if (isNaN(toReturn)) return 10;
267
+ return toReturn;
268
+ },
269
+ [getSequenceLength, nameDivWidth, width]
270
+ );
271
+
272
+ const charWidth = useMemo(() => {
273
+ if (isFullyZoomedOut) {
274
+ return getMinCharWidth();
275
+ } else {
276
+ return Math.max(getMinCharWidth(), charWidthInLinearView);
260
277
  }
261
- //autosave if necessary!
262
- if (
263
- this.props.shouldAutosave &&
264
- prevProps &&
265
- prevProps.stateTrackingId &&
266
- this.props.stateTrackingId !== prevProps.stateTrackingId
267
- ) {
268
- this.setState({ saveMessage: "Alignment Saving.." });
269
- this.setState({ saveMessageLoading: true });
278
+ }, [charWidthInLinearView, getMinCharWidth, isFullyZoomedOut]);
270
279
 
271
- let cleanedTracks;
272
- if (this.props.pairwiseAlignments) {
273
- cleanedTracks = this.props.pairwiseAlignments.map(cleanTracks);
274
- } else {
275
- cleanedTracks = cleanTracks(this.props.alignmentTracks);
280
+ const debouncedAlignmentRunUpdate = debounce(alignmentRunUpdate, 1000);
281
+
282
+ const caretPositionUpdate = useCallback(
283
+ position => {
284
+ if ((caretPosition || -1) === position) {
285
+ return;
276
286
  }
287
+ easyStore.current.caretPosition = position;
288
+ easyStore.current.selectionLayer = { start: -1, end: -1 };
289
+ debouncedAlignmentRunUpdate({
290
+ alignmentId,
291
+ selectionLayer: { start: -1, end: -1 },
292
+ caretPosition: position
293
+ });
294
+ },
295
+ [alignmentId, caretPosition, debouncedAlignmentRunUpdate]
296
+ );
277
297
 
278
- await this.props.handleAlignmentSave(cleanedTracks, this.props);
279
- this.setState({ saveMessage: "Alignment Saved" });
280
- this.setState({ saveMessageLoading: false });
281
- setTimeout(() => {
282
- this.setState({ saveMessage: undefined });
283
- this.setState({ saveMessageLoading: false });
284
- }, 5000);
285
- }
286
- }
298
+ const selectionLayerUpdate = useCallback(
299
+ (newSelection, { forceReduxUpdate } = {}) => {
300
+ const usableSelectionLayer = selectionLayer || { start: -1, end: -1 };
301
+ if (!newSelection) return;
302
+ const { start, end } = newSelection;
287
303
 
288
- componentDidMount() {
304
+ if (
305
+ usableSelectionLayer.start === start &&
306
+ usableSelectionLayer.end === end
307
+ ) {
308
+ return;
309
+ }
310
+ easyStore.current.caretPosition = -1;
311
+ easyStore.current.selectionLayer = newSelection;
312
+
313
+ (forceReduxUpdate ? alignmentRunUpdate : debouncedAlignmentRunUpdate)({
314
+ alignmentId,
315
+ selectionLayer: newSelection,
316
+ caretPosition: -1
317
+ });
318
+ },
319
+ [
320
+ alignmentId,
321
+ alignmentRunUpdate,
322
+ debouncedAlignmentRunUpdate,
323
+ selectionLayer
324
+ ]
325
+ );
326
+
327
+ const updateSelectionOrCaret = useCallback(
328
+ (shiftHeld, newRangeOrCaret, { forceReduxUpdate } = {}) => {
329
+ const forceReduxSelectionLayerUpdate = newSelection => {
330
+ selectionLayerUpdate(newSelection, { forceReduxUpdate: true });
331
+ };
332
+
333
+ const sequenceLength = getSequenceLength();
334
+
335
+ _updateSelectionOrCaret({
336
+ doNotWrapOrigin: true,
337
+ shiftHeld,
338
+ sequenceLength,
339
+ newRangeOrCaret,
340
+ caretPosition: easyStore.current.caretPosition,
341
+ selectionLayer: easyStore.current.selectionLayer,
342
+ selectionLayerUpdate: forceReduxUpdate
343
+ ? forceReduxSelectionLayerUpdate
344
+ : selectionLayerUpdate,
345
+ caretPositionUpdate
346
+ });
347
+ },
348
+ [caretPositionUpdate, getSequenceLength, selectionLayerUpdate]
349
+ );
350
+
351
+ const getNearestCursorPositionToMouseEvent = useCallback(
352
+ (_, event, callback) => {
353
+ //loop through all the rendered rows to see if the click event lands in one of them
354
+ let nearestCaretPos = 0;
355
+ const rowDomNode = veTracksAndAlignmentHolder.current;
356
+ const boundingRowRect = rowDomNode.getBoundingClientRect();
357
+ if (getClientX(event) - boundingRowRect.left - nameDivWidth < 0) {
358
+ nearestCaretPos = 0;
359
+ } else {
360
+ const clickXPositionRelativeToRowContainer =
361
+ getClientX(event) - boundingRowRect.left - nameDivWidth;
362
+ const numberOfBPsInFromRowStart = Math.floor(
363
+ (clickXPositionRelativeToRowContainer + charWidth / 2) / charWidth
364
+ );
365
+ nearestCaretPos = numberOfBPsInFromRowStart + 0;
366
+ if (nearestCaretPos > maxLength + 1) {
367
+ nearestCaretPos = maxLength + 1;
368
+ }
369
+ }
370
+ if (sequenceData?.isProtein) {
371
+ nearestCaretPos = Math.round(nearestCaretPos / 3) * 3;
372
+ }
373
+ if (sequenceLength === 0) nearestCaretPos = 0;
374
+ const callbackVals = {
375
+ updateSelectionOrCaret,
376
+ nearestCaretPos,
377
+ sequenceLength: getSequenceLength(),
378
+ caretPosition: easyStore.current.caretPosition,
379
+ selectionLayer: easyStore.current.selectionLayer,
380
+ easyStore: easyStore.current,
381
+ caretPositionUpdate,
382
+ selectionLayerUpdate,
383
+ event,
384
+ doNotWrapOrigin: true,
385
+ shiftHeld: event.shiftKey,
386
+ // caretGrabbed: event.target.className === "cursor",
387
+ selectionStartGrabbed: event.target.classList.contains(
388
+ draggableClassnames.selectionStart
389
+ ),
390
+ selectionEndGrabbed: event.target.classList.contains(
391
+ draggableClassnames.selectionEnd
392
+ )
393
+ };
394
+ callback(callbackVals);
395
+ },
396
+ [
397
+ caretPositionUpdate,
398
+ charWidth,
399
+ getSequenceLength,
400
+ maxLength,
401
+ nameDivWidth,
402
+ selectionLayerUpdate,
403
+ sequenceData?.isProtein,
404
+ sequenceLength,
405
+ updateSelectionOrCaret
406
+ ]
407
+ );
408
+
409
+ useEffect(() => {
289
410
  const updateAlignmentSelection = newRangeOrCaret => {
290
- this.updateSelectionOrCaret(false, newRangeOrCaret, {
411
+ updateSelectionOrCaret(false, newRangeOrCaret, {
291
412
  forceReduxUpdate: true
292
413
  });
293
414
  };
294
415
  window.updateAlignmentSelection = updateAlignmentSelection;
295
416
  if (window.Cypress)
296
417
  window.Cypress.updateAlignmentSelection = updateAlignmentSelection;
418
+ return () => {
419
+ delete window.updateAlignmentSelection;
420
+ if (window.Cypress) {
421
+ delete window.Cypress.updateAlignmentSelection;
422
+ }
423
+ };
424
+ }, [updateSelectionOrCaret]);
425
+
426
+ const setVerticalScrollRange = throttle(() => {
427
+ if (
428
+ InfiniteScroller.current &&
429
+ InfiniteScroller.current.getFractionalVisibleRange &&
430
+ easyStore.current
431
+ ) {
432
+ let [start, end] = InfiniteScroller.current.getFractionalVisibleRange();
433
+ if (hasTemplate) {
434
+ end = end + 1;
435
+ }
436
+ if (
437
+ easyStore.current.verticalVisibleRange.start !== start ||
438
+ easyStore.current.verticalVisibleRange.end !== end
439
+ )
440
+ easyStore.current.verticalVisibleRange = { start, end };
441
+ }
442
+ }, 100);
443
+
444
+ useEffect(() => {
297
445
  setTimeout(() => {
298
446
  updateLabelsForInViewFeatures({ rectElement: ".alignmentHolder" });
299
447
  }, 0);
300
448
  setTimeout(() => {
301
- this.setVerticalScrollRange();
449
+ setVerticalScrollRange();
302
450
  }, 500);
303
- }
451
+ });
452
+
453
+ useEffect(() => {
454
+ if (scrollPercentageToJumpTo !== undefined) {
455
+ scrollAlignmentToPercent(scrollPercentageToJumpTo);
456
+ }
457
+ }, [scrollPercentageToJumpTo]);
458
+
459
+ useEffect(() => {
460
+ const saveAlignment = async () => {
461
+ //autosave if necessary!
462
+ if (shouldAutosave && stateTrackingId) {
463
+ setSaveMessage("Alignment Saving..");
464
+ setSaveMessageLoading(true);
465
+
466
+ let cleanedTracks;
467
+ if (pairwiseAlignments) {
468
+ cleanedTracks = pairwiseAlignments.map(cleanTracks);
469
+ } else {
470
+ cleanedTracks = cleanTracks(alignmentTracks);
471
+ }
304
472
 
305
- annotationClicked = ({
473
+ await handleAlignmentSave(cleanedTracks);
474
+ setSaveMessage("Alignment Saved");
475
+ setSaveMessageLoading(false);
476
+ setTimeout(() => {
477
+ setSaveMessage(undefined);
478
+ setSaveMessageLoading(false);
479
+ }, 5000);
480
+ }
481
+ };
482
+ saveAlignment();
483
+ }, [
484
+ alignmentTracks,
485
+ handleAlignmentSave,
486
+ pairwiseAlignments,
487
+ shouldAutosave,
488
+ stateTrackingId
489
+ ]);
490
+
491
+ const annotationClicked = ({
306
492
  event,
307
493
  annotation,
308
494
  gapsBefore = 0,
@@ -310,172 +496,83 @@ export class AlignmentView extends React.Component {
310
496
  }) => {
311
497
  event.preventDefault && event.preventDefault();
312
498
  event.stopPropagation && event.stopPropagation();
313
- this.updateSelectionOrCaret(event.shiftKey, {
499
+ updateSelectionOrCaret(event.shiftKey, {
314
500
  ...annotation,
315
501
  start: annotation.start + gapsBefore,
316
502
  end: annotation.end + gapsBefore + gapsInside
317
503
  });
318
504
  };
319
505
 
320
- updateSelectionOrCaret = (
321
- shiftHeld,
322
- newRangeOrCaret,
323
- { forceReduxUpdate } = {}
324
- ) => {
325
- const sequenceLength = this.getSequenceLength();
326
-
327
- updateSelectionOrCaret({
328
- doNotWrapOrigin: true,
329
- shiftHeld,
330
- sequenceLength,
331
- newRangeOrCaret,
332
- caretPosition: this.easyStore.caretPosition,
333
- selectionLayer: this.easyStore.selectionLayer,
334
- selectionLayerUpdate: forceReduxUpdate
335
- ? this.forceReduxSelectionLayerUpdate
336
- : this.selectionLayerUpdate,
337
- caretPositionUpdate: this.caretPositionUpdate
338
- });
339
- };
340
-
341
- caretPositionUpdate = position => {
342
- const { caretPosition = -1, alignmentId } = this.props;
343
- if (caretPosition === position) {
344
- return;
345
- }
346
- this.easyStore.caretPosition = position;
347
- this.easyStore.selectionLayer = { start: -1, end: -1 };
348
- this.debouncedAlignmentRunUpdate({
349
- alignmentId,
350
- selectionLayer: { start: -1, end: -1 },
351
- caretPosition: position
352
- });
353
- };
354
-
355
- debouncedAlignmentRunUpdate = debounce(this.props.alignmentRunUpdate, 1000);
356
-
357
- forceReduxSelectionLayerUpdate = newSelection => {
358
- this.selectionLayerUpdate(newSelection, { forceReduxUpdate: true });
359
- };
360
-
361
- selectionLayerUpdate = (newSelection, { forceReduxUpdate } = {}) => {
362
- const { selectionLayer = { start: -1, end: -1 }, alignmentId } = this.props;
363
- if (!newSelection) return;
364
- const { start, end } = newSelection;
365
-
366
- if (selectionLayer.start === start && selectionLayer.end === end) {
367
- return;
368
- }
369
- this.easyStore.caretPosition = -1;
370
- this.easyStore.selectionLayer = newSelection;
371
-
372
- (forceReduxUpdate
373
- ? this.props.alignmentRunUpdate
374
- : this.debouncedAlignmentRunUpdate)({
375
- alignmentId,
376
- selectionLayer: newSelection,
377
- caretPosition: -1
378
- });
379
- };
380
-
381
- getCharWidthInLinearView = () => {
382
- if (this.props.isFullyZoomedOut) {
383
- return this.getMinCharWidth();
384
- } else {
385
- return Math.max(this.getMinCharWidth(), this.state.charWidthInLinearView);
386
- }
387
- };
388
- getNumBpsShownInLinearView = () => {
389
- const toReturn =
390
- (this.state.width - this.state.nameDivWidth) /
391
- this.getCharWidthInLinearView();
506
+ const getNumBpsShownInLinearView = () => {
507
+ const toReturn = (width - nameDivWidth) / charWidth;
392
508
  return toReturn || 0;
393
509
  };
394
- setVerticalScrollRange = throttle(() => {
510
+
511
+ const handleScroll = () => {
395
512
  if (
396
- this &&
397
- this.InfiniteScroller &&
398
- this.InfiniteScroller.getFractionalVisibleRange &&
399
- this.easyStore
513
+ alignmentHolder.current.scrollTop !== oldAlignmentHolderScrollTop.current
400
514
  ) {
401
- let [start, end] = this.InfiniteScroller.getFractionalVisibleRange();
402
- if (this.props.hasTemplate) {
403
- end = end + 1;
404
- }
405
- if (
406
- this.easyStore.verticalVisibleRange.start !== start ||
407
- this.easyStore.verticalVisibleRange.end !== end
408
- )
409
- this.easyStore.verticalVisibleRange = { start, end };
410
- }
411
- }, 100);
412
- handleScroll = () => {
413
- // tnr: maybe add this in at some point
414
- // this.updateMinimapHighlightForScroll(
415
- // this.oldMinimapScrollTracker || this.alignmentHolder.scrollTop,
416
- // this.alignmentHolder.scrollTop
417
- // );
418
- // this.oldMinimapScrollTracker = this.alignmentHolder.scrollTop;
419
- // if (this.alignmentHolder.scrollTop !== this.oldMinimapScrollTracker) {
420
- // }
421
- if (this.alignmentHolder.scrollTop !== this.oldAlignmentHolderScrollTop) {
422
515
  setTimeout(() => {
423
- this.setVerticalScrollRange();
424
- this.oldAlignmentHolderScrollTop = this.alignmentHolder.scrollTop;
516
+ setVerticalScrollRange();
517
+ oldAlignmentHolderScrollTop.current = alignmentHolder.current.scrollTop;
425
518
  }, 100);
426
519
  }
427
- if (this.blockScroll) {
520
+ if (blockScroll.current) {
428
521
  //we have to block the scroll sometimes when adjusting the minimap so things aren't too jumpy
429
522
  return;
430
523
  }
431
524
 
432
525
  const scrollPercentage =
433
- this.alignmentHolder.scrollLeft /
434
- (this.alignmentHolder.scrollWidth - this.alignmentHolder.clientWidth);
435
- this.easyStore.percentScrolled = scrollPercentage || 0;
436
- if (!this.isZooming) {
437
- this.easyStore.percentScrolledPreZoom = this.easyStore.percentScrolled;
526
+ alignmentHolder.current.scrollLeft /
527
+ (alignmentHolder.current.scrollWidth -
528
+ alignmentHolder.current.clientWidth);
529
+ easyStore.current.percentScrolled = scrollPercentage || 0;
530
+ if (!isZooming.current) {
531
+ easyStore.current.percentScrolledPreZoom =
532
+ easyStore.current.percentScrolled;
438
533
  }
439
- if (this.alignmentHolderTop) {
440
- this.alignmentHolderTop.scrollLeft = this.alignmentHolder.scrollLeft;
534
+ if (alignmentHolderTop.current) {
535
+ alignmentHolderTop.current.scrollLeft =
536
+ alignmentHolder.current.scrollLeft;
441
537
  }
442
538
  updateLabelsForInViewFeatures({ rectElement: ".alignmentHolder" });
443
539
  };
444
- handleTopScroll = () => {
445
- this.alignmentHolder.scrollLeft = this.alignmentHolderTop.scrollLeft;
540
+
541
+ const handleTopScroll = () => {
542
+ alignmentHolder.current.scrollLeft = alignmentHolderTop.current.scrollLeft;
446
543
  };
544
+
447
545
  /**
448
546
  * Responsible for handling resizing the highlighted region of the minimap
449
547
  * @param {*} newSliderSize
450
548
  * @param {*} newPercent
451
549
  */
452
- onMinimapSizeAdjust = (newSliderSize, newPercent) => {
453
- const percentageOfSpace = newSliderSize / this.state.width;
454
- const seqLength = this.getSequenceLength();
550
+ const onMinimapSizeAdjust = (newSliderSize, newPercent) => {
551
+ const percentageOfSpace = newSliderSize / width;
552
+ const seqLength = getSequenceLength();
455
553
  const numBpsInView = seqLength * percentageOfSpace;
456
- const newCharWidth =
457
- (this.state.width - this.state.nameDivWidth) / numBpsInView;
458
- this.blockScroll = true;
459
- this.setCharWidthInLinearView({ charWidthInLinearView: newCharWidth });
554
+ const newCharWidth = (width - nameDivWidth) / numBpsInView;
555
+ blockScroll.current = true;
556
+ setCharWidthInLinearView({ charWidthInLinearView: newCharWidth });
460
557
  setTimeout(() => {
461
- this.scrollAlignmentToPercent(newPercent);
462
- this.blockScroll = false;
558
+ scrollAlignmentToPercent(newPercent);
559
+ blockScroll.current = false;
463
560
  updateLabelsForInViewFeatures({ rectElement: ".alignmentHolder" });
464
561
  });
465
562
  };
466
563
 
467
- setCharWidthInLinearView = ({ charWidthInLinearView }) => {
564
+ const setCharWidthInLinearView = ({ charWidthInLinearView }) => {
468
565
  window.localStorage.setItem(
469
566
  "charWidthInLinearViewDefault",
470
567
  charWidthInLinearView
471
568
  );
472
- this.setState({ charWidthInLinearView });
569
+ _setCharWidthInLinearView(charWidthInLinearView);
473
570
  charWidthInLinearViewDefault = JSON.parse(
474
571
  window.localStorage.getItem("charWidthInLinearViewDefault")
475
572
  );
476
573
  };
477
574
 
478
- scrollToCaret = () => {
575
+ const scrollToCaret = () => {
479
576
  let el = window.document.querySelector(".veCaret:not(.zoomSelection)"); //adding .veRowViewCaret breaks this for some reason
480
577
  if (!el) {
481
578
  el = window.document.querySelector(".veCaret"); //adding .veRowViewCaret breaks this for some reason
@@ -486,34 +583,22 @@ export class AlignmentView extends React.Component {
486
583
  el.scrollIntoView({ inline: "center", block: "nearest" });
487
584
  };
488
585
 
489
- scrollAlignmentToPercent = scrollPercentage => {
490
- const scrollPercentageToUse = Math.min(Math.max(scrollPercentage, 0), 1);
491
- this.easyStore.percentScrolled = scrollPercentageToUse;
492
- this.alignmentHolder.scrollLeft =
493
- scrollPercentageToUse *
494
- (this.alignmentHolder.scrollWidth - this.alignmentHolder.clientWidth);
495
- if (this.alignmentHolderTop) {
496
- this.alignmentHolderTop.scrollLeft =
497
- scrollPercentageToUse *
498
- (this.alignmentHolderTop.scrollWidth -
499
- this.alignmentHolderTop.clientWidth);
500
- }
501
- };
502
- scrollYToTrack = trackIndex => {
503
- this.InfiniteScroller.scrollTo(trackIndex);
586
+ const scrollYToTrack = trackIndex => {
587
+ InfiniteScroller.current.scrollTo(trackIndex);
504
588
  };
505
589
 
506
- estimateRowHeight = (index, cache) => {
507
- const { alignmentVisibilityToolOptions, alignmentTracks } = this.props;
590
+ const estimateRowHeight = (index, cache) => {
508
591
  const track = alignmentTracks[index];
509
592
  if (!track) return 100;
510
593
  const { sequenceData } = track;
511
- this.rowData = prepareRowData(sequenceData, sequenceData.sequence.length);
512
- return estimateRowHeight({
594
+ rowData.current = prepareRowData(
595
+ sequenceData,
596
+ sequenceData.sequence.length
597
+ );
598
+ return _estimateRowHeight({
513
599
  index,
514
600
  cache,
515
- // clearCache: this.clearCache,
516
- row: this.rowData[index],
601
+ row: rowData.current[index],
517
602
  annotationVisibility:
518
603
  alignmentVisibilityToolOptions.alignmentAnnotationVisibility,
519
604
  annotationLabelVisibility:
@@ -521,36 +606,7 @@ export class AlignmentView extends React.Component {
521
606
  });
522
607
  };
523
608
 
524
- getMaxLinearViewWidth = () => {
525
- let maxWidth = 0;
526
- const charWidthInLinearView = this.getCharWidthInLinearView();
527
- forEach(this.props.alignmentTracks, t => {
528
- const w = (t.alignmentData || t.sequenceData).sequence.length;
529
- if (w > maxWidth) maxWidth = w;
530
- });
531
- return maxWidth * charWidthInLinearView;
532
- };
533
-
534
- renderItem = (_i, key, isTemplate, cloneProps) => {
535
- const charWidthInLinearView = this.getCharWidthInLinearView();
536
- const {
537
- alignmentTrackIndex,
538
- pairwiseAlignments,
539
- currentPairwiseAlignmentIndex,
540
- pairwiseOverviewAlignmentTracks,
541
- alignmentTracks = [],
542
- upsertAlignmentRun,
543
- alignmentId,
544
- noClickDragHandlers,
545
- handleSelectTrack,
546
- allowTrackRearrange,
547
- isPairwise,
548
- linearViewOptions,
549
- alignmentVisibilityToolOptions,
550
- hasTemplate,
551
- allowTrackNameEdit,
552
- ...rest
553
- } = this.props;
609
+ const renderItem = (_i, key, isTemplate, cloneProps) => {
554
610
  const isDragDisabled = !allowTrackRearrange || isPairwise;
555
611
  let i;
556
612
  if (isTemplate) {
@@ -561,7 +617,7 @@ export class AlignmentView extends React.Component {
561
617
  i = _i;
562
618
  }
563
619
 
564
- const track = alignmentTracks[i];
620
+ const track = alignmentTracks?.[i];
565
621
  if (!track) return null;
566
622
  const {
567
623
  sequenceData,
@@ -572,132 +628,23 @@ export class AlignmentView extends React.Component {
572
628
  chromatogramData
573
629
  // mismatches
574
630
  } = track;
575
- const seqLen = this.getMaxLength();
631
+ const seqLen = maxLength;
576
632
 
577
633
  const trimmedRangesToDisplay = getTrimmedRangesToDisplay({
578
634
  seqLen,
579
635
  trimmedRange: alignmentData?.trimmedRange
580
636
  });
581
637
  const linearViewWidth =
582
- (alignmentData || sequenceData).sequence.length * charWidthInLinearView;
638
+ (alignmentData || sequenceData).sequence.length * charWidth;
583
639
  const name = sequenceData.name || sequenceData.id;
584
640
 
585
- //tnw: commenting this out for now since it can significantly slow down the alignment visualization
586
- /**
587
- * for alignment of sanger seq reads to a ref seq, have translations show up at the bp pos of ref seq's CDS features across all seq reads
588
- **/
589
- // let sequenceDataWithRefSeqCdsFeatures;
590
- // if (this.props.alignmentType === "SANGER SEQUENCING") {
591
- // if (i !== 0) {
592
- // sequenceDataWithRefSeqCdsFeatures = cloneDeep(sequenceData);
593
- // const refSeqCdsFeaturesBpPos = [];
594
- // alignmentTracks[0].sequenceData.features.forEach((feature) => {
595
- // if (feature.type === "CDS") {
596
- // const editedFeature = cloneDeep(feature);
597
- // // in seq reads, ref seq's CDS feature translations need to show up at the bp pos of alignment, not the original bp pos
598
- // // actual position in the track
599
- // const absoluteFeatureStart =
600
- // getGaps(feature.start, alignmentTracks[0].alignmentData.sequence)
601
- // .gapsBefore + feature.start;
602
- // const gapsBeforeSeqRead = getGaps(
603
- // 0,
604
- // alignmentData.sequence
605
- // ).gapsBefore;
606
- // const bpsFromSeqReadStartToFeatureStartIncludingGaps =
607
- // absoluteFeatureStart - gapsBeforeSeqRead;
608
- // const absoluteFeatureEnd =
609
- // getGaps(feature.end, alignmentTracks[0].alignmentData.sequence)
610
- // .gapsBefore + feature.end;
611
- // // const gapsBeforeFeatureInSeqRead = getGaps(feature.start - gapsBeforeSeqRead, alignmentData.sequence).gapsBefore
612
- // const gapsAfterSeqRead =
613
- // alignmentData.sequence.length -
614
- // alignmentData.sequence.replace(/-+$/g, "").length;
615
- // const seqReadLengthWithoutGapsBeforeAfter =
616
- // alignmentData.sequence.length -
617
- // gapsBeforeSeqRead -
618
- // gapsAfterSeqRead;
619
- // const absoluteSeqReadStart = gapsBeforeSeqRead;
620
- // const absoluteSeqReadEnd =
621
- // absoluteSeqReadStart + seqReadLengthWithoutGapsBeforeAfter;
622
- // let featureStartInSeqRead;
623
- // if (absoluteFeatureEnd < absoluteSeqReadStart) {
624
- // // if the feature ends before the seq read starts, do nothing
625
- // } else if (absoluteFeatureStart > absoluteSeqReadEnd) {
626
- // // if the feature starts after the seq read ends, do nothing
627
- // } else if (
628
- // absoluteFeatureStart < absoluteSeqReadStart &&
629
- // absoluteFeatureEnd > absoluteSeqReadStart
630
- // ) {
631
- // // if the feature starts before the seq read starts but doesn't end before the seq read starts
632
- // const arrayOfCodonStartPos = [];
633
- // for (
634
- // let i = absoluteFeatureStart;
635
- // i < absoluteSeqReadStart + 6;
636
- // i += 3
637
- // ) {
638
- // arrayOfCodonStartPos.push(i);
639
- // }
640
- // // want to start translation at the codon start pos closest to seq read start
641
- // const absoluteTranslationStartInFrame =
642
- // arrayOfCodonStartPos.reduce((prev, curr) =>
643
- // Math.abs(curr - absoluteSeqReadStart) <
644
- // Math.abs(prev - absoluteSeqReadStart) &&
645
- // curr >= absoluteSeqReadStart
646
- // ? curr
647
- // : prev
648
- // );
649
- // const seqReadTranslationStartInFrame =
650
- // absoluteTranslationStartInFrame - gapsBeforeSeqRead;
651
- // editedFeature.start = seqReadTranslationStartInFrame;
652
- // const shortenedFeatureLength =
653
- // Math.abs(absoluteFeatureEnd - absoluteFeatureStart) -
654
- // (absoluteTranslationStartInFrame - absoluteFeatureStart);
655
- // editedFeature.end = editedFeature.start + shortenedFeatureLength;
656
- // refSeqCdsFeaturesBpPos.push(editedFeature);
657
- // } else {
658
- // // if the feature is fully contained within the seq read start/end
659
- // const seqReadStartToFeatureStartIncludingGaps =
660
- // alignmentData.sequence
661
- // .replace(/^-+/g, "")
662
- // .replace(/-+$/g, "")
663
- // .slice(0, bpsFromSeqReadStartToFeatureStartIncludingGaps);
664
- // const arrayOfGaps = seqReadStartToFeatureStartIncludingGaps.match(
665
- // new RegExp("-", "g")
666
- // );
667
- // let numOfGapsFromSeqReadStartToFeatureStart = 0;
668
- // if (arrayOfGaps !== null) {
669
- // numOfGapsFromSeqReadStartToFeatureStart = arrayOfGaps.length;
670
- // }
671
- // featureStartInSeqRead =
672
- // bpsFromSeqReadStartToFeatureStartIncludingGaps -
673
- // numOfGapsFromSeqReadStartToFeatureStart;
674
- // editedFeature.start = featureStartInSeqRead;
675
- // const featureLength = Math.abs(feature.end - feature.start);
676
- // editedFeature.end = editedFeature.start + featureLength;
677
- // refSeqCdsFeaturesBpPos.push(editedFeature);
678
- // }
679
- // }
680
- // });
681
- // // add ref seq's CDS features to seq reads (not the actual sequenceData) to generate translations at those bp pos
682
- // if (refSeqCdsFeaturesBpPos.length !== 0) {
683
- // sequenceDataWithRefSeqCdsFeatures.features.push(
684
- // ...refSeqCdsFeaturesBpPos
685
- // );
686
- // // use returned aligned sequence rather than original sequence because after bowtie2, may be reverse complement or have soft-clipped ends
687
- // sequenceDataWithRefSeqCdsFeatures.sequence =
688
- // alignmentData.sequence.replace(/-/g, "");
689
- // }
690
- // }
691
- // }
692
- const tickSpacing = massageTickSpacing(
693
- Math.ceil(120 / charWidthInLinearView)
694
- );
641
+ const tickSpacing = massageTickSpacing(Math.ceil(120 / charWidth));
695
642
 
696
643
  const { compactNames } =
697
644
  alignmentVisibilityToolOptions.alignmentAnnotationVisibility;
698
645
  const selectionLayer = [
699
- this.state[`tempTrimBefore${i}`] || trimmedRangesToDisplay[0],
700
- this.state[`tempTrimAfter${i}`] || trimmedRangesToDisplay[1]
646
+ tempTrimBefore[i] || trimmedRangesToDisplay[0],
647
+ tempTrimAfter[i] || trimmedRangesToDisplay[1]
701
648
  ]
702
649
  .filter(i => i)
703
650
  .map(i => ({
@@ -733,11 +680,11 @@ export class AlignmentView extends React.Component {
733
680
  zIndex: 10,
734
681
  borderBottom: `1px solid ${isTemplate ? "red" : "lightgray"}`,
735
682
  borderRight: `1px solid ${isTemplate ? "red" : "lightgray"}`,
736
- width: this.state.nameDivWidth - 3,
683
+ width: nameDivWidth - 3,
737
684
  padding: 2,
738
685
  marginRight: 3,
739
686
  paddingBottom: 0,
740
- minWidth: this.state.nameDivWidth - 3,
687
+ minWidth: nameDivWidth - 3,
741
688
  overflow: "hidden",
742
689
  scrollbarWidth: "none",
743
690
  whiteSpace: "nowrap"
@@ -784,7 +731,7 @@ export class AlignmentView extends React.Component {
784
731
  pairwiseAlignments,
785
732
  upsertAlignmentRun,
786
733
  alignmentId,
787
- alignmentTracks,
734
+ alignmentTracks: alignmentTracks || [],
788
735
  alignmentTrackIndex: i,
789
736
  update: { name: newName }
790
737
  });
@@ -795,9 +742,9 @@ export class AlignmentView extends React.Component {
795
742
  small
796
743
  data-tip="Edit Track Name"
797
744
  className="edit-track-name-btn"
798
- icon={<Icon size={12} color="lightgrey" icon="edit"></Icon>}
745
+ icon={<Icon size={12} color="lightgrey" icon="edit" />}
799
746
  minimal
800
- ></Button>
747
+ />
801
748
  )}
802
749
  {sequenceData.seqLink && (
803
750
  <AnchorButton
@@ -805,20 +752,13 @@ export class AlignmentView extends React.Component {
805
752
  data-tip={sequenceData.seqLinkTooltip}
806
753
  target="_blank"
807
754
  small
808
- icon={
809
- <Icon size={12} color="white" icon="document-open"></Icon>
810
- }
755
+ icon={<Icon size={12} color="white" icon="document-open" />}
811
756
  minimal
812
- ></AnchorButton>
757
+ />
813
758
  )}
814
759
  {name}
815
760
  </div>
816
761
  <div style={{ fontSize: 10, marginTop: 2, marginBottom: 2 }}>
817
- {/* <Icon //tnr: add this once we support forward/reverse for each track
818
- color="darkgrey"
819
- style={{ marginRight: 10 }}
820
- icon="arrow-right"
821
- ></Icon> */}
822
762
  {isReversed && (
823
763
  <span
824
764
  style={{
@@ -858,12 +798,7 @@ export class AlignmentView extends React.Component {
858
798
  </div>
859
799
  <HorizontalPanelDragHandle
860
800
  onDrag={({ dx }) => {
861
- this.setState({
862
- nameDivWidth: Math.min(
863
- this.state.nameDivWidth - dx,
864
- this.state.width - 20
865
- )
866
- });
801
+ setNameDivWidth(Math.min(nameDivWidth - dx, width - 20));
867
802
  }}
868
803
  />
869
804
  </div>
@@ -877,7 +812,7 @@ export class AlignmentView extends React.Component {
877
812
  position: "absolute",
878
813
  opacity: 0,
879
814
  height: "100%",
880
- left: this.state.nameDivWidth,
815
+ left: nameDivWidth,
881
816
  width: linearViewWidth,
882
817
  fontWeight: "bolder",
883
818
  cursor: "pointer",
@@ -891,65 +826,63 @@ export class AlignmentView extends React.Component {
891
826
  </div>
892
827
  )}
893
828
  <NonReduxEnhancedLinearView
894
- {...{
895
- ...rest,
896
- caretPosition: this.state[`tempTrimmingCaret${i}`] || -1,
897
- selectionLayer,
898
- isInAlignment: true,
899
- // : { start: -1, end: -1 },
900
- annotationVisibilityOverrides:
901
- alignmentVisibilityToolOptions.alignmentAnnotationVisibility,
902
- linearViewAnnotationLabelVisibilityOverrides:
903
- alignmentVisibilityToolOptions.alignmentAnnotationLabelVisibility,
904
- marginWith: 0,
905
- orfClicked: this.annotationClicked,
906
- primerClicked: this.annotationClicked,
907
- translationClicked: this.annotationClicked,
908
- cutsiteClicked: this.annotationClicked,
909
- translationDoubleClicked: this.annotationClicked,
910
- deletionLayerClicked: this.annotationClicked,
911
- replacementLayerClicked: this.annotationClicked,
912
- featureClicked: this.annotationClicked,
913
- partClicked: this.annotationClicked,
914
- searchLayerClicked: this.annotationClicked,
915
- editorDragStarted: noop, //override these since we're defining the handlers above
916
- editorDragStopped: noop, //override these since we're defining the handlers above
917
- editorDragged: noop, //override these since we're defining the handlers above
918
- hideName: true,
919
- sequenceData,
920
- // sequenceDataWithRefSeqCdsFeatures,
921
- tickSpacing,
922
- allowSeqDataOverride: true, //override the sequence data stored in redux so we can track the caret position/selection layer in redux but not have to update the redux editor
923
- editorName: `${isTemplate ? "template_" : ""}alignmentView${i}`,
924
- alignmentData,
925
- chromatogramData,
926
- height: "100%",
927
- vectorInteractionWrapperStyle: {
928
- overflowY: "hidden"
929
- },
930
- withZoomLinearView: false,
931
- marginWidth: 0,
932
- linearViewCharWidth: charWidthInLinearView,
933
- ignoreGapsOnHighlight: true,
934
- ...(linearViewOptions &&
935
- (isFunction(linearViewOptions)
936
- ? linearViewOptions({
937
- index: i,
938
- isTemplate,
939
- alignmentVisibilityToolOptions,
940
- sequenceData,
941
- alignmentData,
942
- chromatogramData
943
- })
944
- : linearViewOptions)),
945
- additionalSelectionLayers,
946
- dimensions: {
947
- width: linearViewWidth
948
- },
949
- width: linearViewWidth,
950
- paddingBottom: 5,
951
- scrollDataPassed: this.easyStore
829
+ {...rest}
830
+ caretPosition={tempTrimmingCaret[i] || -1}
831
+ selectionLayer={selectionLayer}
832
+ isInAlignment
833
+ annotationVisibilityOverrides={
834
+ alignmentVisibilityToolOptions.alignmentAnnotationVisibility
835
+ }
836
+ linearViewAnnotationLabelVisibilityOverrides={
837
+ alignmentVisibilityToolOptions.alignmentAnnotationLabelVisibility
838
+ }
839
+ marginWith={0}
840
+ orfClicked={annotationClicked}
841
+ primerClicked={annotationClicked}
842
+ translationClicked={annotationClicked}
843
+ cutsiteClicked={annotationClicked}
844
+ translationDoubleClicked={annotationClicked}
845
+ deletionLayerClicked={annotationClicked}
846
+ replacementLayerClicked={annotationClicked}
847
+ featureClicked={annotationClicked}
848
+ partClicked={annotationClicked}
849
+ searchLayerClicked={annotationClicked}
850
+ editorDragStarted={noop} //override these since we're defining the handlers above
851
+ editorDragStopped={noop} //override these since we're defining the handlers above
852
+ editorDragged={noop} //override these since we're defining the handlers above
853
+ hideName
854
+ sequenceData={sequenceData}
855
+ tickSpacing={tickSpacing}
856
+ allowSeqDataOverride //override the sequence data stored in redux so we can track the caret position/selection layer in redux but not have to update the redux editor
857
+ editorName={`${isTemplate ? "template_" : ""}alignmentView${i}`}
858
+ alignmentData={alignmentData}
859
+ chromatogramData={chromatogramData}
860
+ height={"100%"}
861
+ vectorInteractionWrapperStyle={{
862
+ overflowY: "hidden"
863
+ }}
864
+ withZoomLinearView={false}
865
+ marginWidth={0}
866
+ linearViewCharWidth={charWidth}
867
+ ignoreGapsOnHighlight
868
+ {...(linearViewOptions &&
869
+ (isFunction(linearViewOptions)
870
+ ? linearViewOptions({
871
+ index: i,
872
+ isTemplate,
873
+ alignmentVisibilityToolOptions,
874
+ sequenceData,
875
+ alignmentData,
876
+ chromatogramData
877
+ })
878
+ : linearViewOptions))}
879
+ additionalSelectionLayers={additionalSelectionLayers}
880
+ dimensions={{
881
+ width: linearViewWidth
952
882
  }}
883
+ width={linearViewWidth}
884
+ paddingBottom={5}
885
+ scrollDataPassed={easyStore.current}
953
886
  />
954
887
  </div>
955
888
  );
@@ -968,22 +901,24 @@ export class AlignmentView extends React.Component {
968
901
  </DndDraggable>
969
902
  );
970
903
  };
971
- handleResize = throttle(([e]) => {
972
- this.easyStore.viewportWidth =
973
- e.contentRect.width - this.state.nameDivWidth || 400;
974
- this.setState({ width: e.contentRect.width });
904
+
905
+ const handleResize = throttle(([e]) => {
906
+ easyStore.current.viewportWidth = e.contentRect.width - nameDivWidth || 400;
907
+ setWidth(e.contentRect.width);
975
908
  }, 200);
976
909
 
977
- removeMinimapHighlightForMouseLeave = () => {
910
+ const removeMinimapHighlightForMouseLeave = () => {
978
911
  const minimapLaneEl = document.querySelector(`.minimapLane.lane-hovered`);
979
912
  if (!minimapLaneEl) return;
980
913
  minimapLaneEl.classList.remove("lane-hovered");
981
914
  };
982
- updateMinimapHighlightForMouseMove = event => {
983
- this.latestMouseY = getClientY(event); //we use this variable later
984
- this.updateMinimapHighlight();
915
+
916
+ const updateMinimapHighlightForMouseMove = event => {
917
+ latestMouseY.current = getClientY(event); //we use this variable later
918
+ updateMinimapHighlight();
985
919
  };
986
- updateMinimapHighlight = () => {
920
+
921
+ const updateMinimapHighlight = () => {
987
922
  const rows = document.querySelectorAll(`.alignmentViewTrackContainer`);
988
923
  const rowsLength = document.querySelectorAll(`.minimapLane`).length;
989
924
  if (rowsLength <= 4) {
@@ -992,8 +927,8 @@ export class AlignmentView extends React.Component {
992
927
  some(rows, rowDomNode => {
993
928
  const boundingRowRect = rowDomNode.getBoundingClientRect();
994
929
  if (
995
- this.latestMouseY > boundingRowRect.top &&
996
- this.latestMouseY < boundingRowRect.top + boundingRowRect.height
930
+ latestMouseY.current > boundingRowRect.top &&
931
+ latestMouseY.current < boundingRowRect.top + boundingRowRect.height
997
932
  ) {
998
933
  const prevMinimapLaneEl = document.querySelector(
999
934
  `.minimapLane.lane-hovered`
@@ -1013,15 +948,16 @@ export class AlignmentView extends React.Component {
1013
948
  }
1014
949
  });
1015
950
  };
1016
- onTrackDragStart = () => {
1017
- this.setState({ isTrackDragging: true });
951
+
952
+ const onTrackDragStart = () => {
953
+ setIsTrackDragging(true);
1018
954
  };
1019
- onTrackDragEnd = ({ destination, source }) => {
1020
- this.setState({ isTrackDragging: false });
955
+
956
+ const onTrackDragEnd = ({ destination, source }) => {
957
+ setIsTrackDragging(false);
1021
958
  if (!destination) {
1022
959
  return;
1023
960
  }
1024
- const { upsertAlignmentRun, alignmentId, alignmentTracks } = this.props;
1025
961
  upsertAlignmentRun({
1026
962
  id: alignmentId,
1027
963
  alignmentTracks: array_move(
@@ -1032,691 +968,23 @@ export class AlignmentView extends React.Component {
1032
968
  });
1033
969
  };
1034
970
 
1035
- render() {
1036
- const charWidthInLinearView = this.getCharWidthInLinearView();
1037
- const {
1038
- alignmentTracks = [],
1039
- height,
1040
- minimapLaneHeight,
1041
- minimapLaneSpacing,
1042
- isInPairwiseOverviewView,
1043
- isPairwise,
1044
- currentPairwiseAlignmentIndex,
1045
- hasTemplate,
1046
- noVisibilityOptions,
1047
- updateAlignmentSortOrder,
1048
- alignmentSortOrder,
1049
- handleBackButtonClicked,
1050
- noClickDragHandlers,
1051
- upsertAlignmentRun,
1052
- alignmentId,
1053
- allowTrimming,
1054
- additionalSelectionLayerRightClickedOptions,
1055
- selectionLayerRightClicked,
1056
- additionalTopEl,
1057
- additionalTopLeftEl,
1058
- handleAlignmentRename,
1059
- alignmentVisibilityToolOptions
1060
- } = this.props;
1061
- const sequenceLength = this.getMaxLength();
1062
- if (
1063
- !alignmentTracks ||
1064
- !alignmentTracks[0] ||
1065
- !alignmentTracks[0].alignmentData
1066
- ) {
1067
- console.error("corrupted data!", this.props);
1068
- return "corrupted data!";
1069
- }
1070
-
1071
- const getTrackVis = (alignmentTracks, isTemplate, allTracks) => {
1072
- const rowData = {};
1073
- const innerTrackVis = (drop_provided, drop_snapshot) => {
1074
- return (
1075
- <div
1076
- className="alignmentTracks "
1077
- style={{
1078
- overflowY: "auto",
1079
- display: "flex",
1080
- zIndex: 10
1081
- }}
1082
- >
1083
- <div
1084
- style={{
1085
- overflowX: "auto",
1086
- width: this.state.width
1087
- }}
1088
- ref={ref => {
1089
- this[isTemplate ? "alignmentHolderTop" : "alignmentHolder"] =
1090
- ref;
1091
- }}
1092
- onContextMenu={e => {
1093
- if (
1094
- !allowTrimming ||
1095
- isTargetWithinEl(e, ".alignmentTrackName")
1096
- ) {
1097
- return;
1098
- }
1099
-
1100
- this.getTrackTrimmingOptions({
1101
- e,
1102
- allTracks,
1103
- upsertAlignmentRun,
1104
- alignmentId,
1105
- currentPairwiseAlignmentIndex
1106
- });
1107
- }}
1108
- onMouseLeave={this.removeMinimapHighlightForMouseLeave}
1109
- onMouseMove={this.updateMinimapHighlightForMouseMove}
1110
- dataname="scrollGroup"
1111
- className="alignmentHolder"
1112
- onScroll={isTemplate ? this.handleTopScroll : this.handleScroll}
1113
- >
1114
- <ReactDraggable
1115
- disabled={this.state.isTrackDragging}
1116
- bounds={{ top: 0, left: 0, right: 0, bottom: 0 }}
1117
- onDrag={
1118
- noClickDragHandlers
1119
- ? noop
1120
- : event => {
1121
- if (this.state.isTrackDragging) return;
1122
- this.getNearestCursorPositionToMouseEvent(
1123
- rowData,
1124
- event,
1125
- editorDragged
1126
- );
1127
- }
1128
- }
1129
- onStart={
1130
- noClickDragHandlers
1131
- ? noop
1132
- : event => {
1133
- if (isTargetWithinEl(event, ".alignmentTrackName")) {
1134
- return this.setState({ isTrackDragging: true });
1135
- }
1136
- if (this.state.isTrackDragging) return;
1137
- this.getNearestCursorPositionToMouseEvent(
1138
- rowData,
1139
- event,
1140
- editorDragStarted
1141
- );
1142
- }
1143
- }
1144
- onStop={
1145
- noClickDragHandlers
1146
- ? noop
1147
- : (...args) => {
1148
- setTimeout(() => {
1149
- this.setState({ isTrackDragging: false });
1150
- }, 0);
1151
- editorDragStopped(...args);
1152
- }
1153
- }
1154
- >
1155
- <div
1156
- ref={ref => (this.veTracksAndAlignmentHolder = ref)}
1157
- className={classNames("veTracksAndAlignmentHolder", {
1158
- isTrackDragging: this.state.isTrackDragging
1159
- })}
1160
- // onContextMenu={
1161
- //tnrtodo add copy single track/all tracks logic here
1162
- // (event) => {
1163
- // this.getNearestCursorPositionToMouseEvent(
1164
- // rowData,
1165
- // event,
1166
- // () => {
1167
- // }
1168
- // );
1169
- // }
1170
- // }
1171
- onClick={
1172
- noClickDragHandlers
1173
- ? noop
1174
- : event => {
1175
- if (this.state.isTrackDragging) return;
1176
- if (isTargetWithinEl(event, ".alignmentTrackName")) {
1177
- return;
1178
- }
1179
- this.getNearestCursorPositionToMouseEvent(
1180
- rowData,
1181
- event,
1182
- editorClicked
1183
- );
1184
- }
1185
- }
1186
- >
1187
- <PerformantSelectionLayer
1188
- leftMargin={this.state.nameDivWidth}
1189
- className="veAlignmentSelectionLayer"
1190
- isDraggable
1191
- selectionLayerRightClicked={
1192
- selectionLayerRightClicked
1193
- ? (...args) => {
1194
- selectionLayerRightClicked(...args, this.props);
1195
- }
1196
- : (...args) => {
1197
- const { event } = args[0];
1198
- const track = getTrackFromEvent(event, allTracks);
1199
-
1200
- const alignmentData = track.alignmentData;
1201
- const { name } = alignmentData;
1202
-
1203
- const copySpecificAlignmentFasta = async () => {
1204
- const { selectionLayer } =
1205
- this.props.store.getState().VectorEditor
1206
- .__allEditorsOptions.alignments[
1207
- this.props.id
1208
- ] || {};
1209
- const seqDataToCopy = getSequenceDataBetweenRange(
1210
- alignmentData,
1211
- selectionLayer
1212
- ).sequence;
1213
- const seqDataToCopyAsFasta = `>${name}\r\n${seqDataToCopy}\r\n`;
1214
- await navigator.clipboard.writeText(
1215
- seqDataToCopyAsFasta
1216
- );
1217
- };
1218
-
1219
- const copySpecificAlignment = async () => {
1220
- const { selectionLayer } =
1221
- this.props.store.getState().VectorEditor
1222
- .__allEditorsOptions.alignments[
1223
- this.props.id
1224
- ] || {};
1225
- const seqDataToCopy = getSequenceDataBetweenRange(
1226
- alignmentData,
1227
- selectionLayer
1228
- ).sequence;
1229
- await navigator.clipboard.writeText(
1230
- seqDataToCopy
1231
- );
1232
- };
1233
-
1234
- const getAllAlignmentsFastaText = async () => {
1235
- await navigator.clipboard.writeText(
1236
- this.getAllAlignmentsFastaText()
1237
- );
1238
- };
1239
-
1240
- showContextMenu(
1241
- [
1242
- ...(additionalSelectionLayerRightClickedOptions
1243
- ? additionalSelectionLayerRightClickedOptions(
1244
- ...args,
1245
- this.props
1246
- )
1247
- : []),
1248
- {
1249
- text: "Copy Selection of All Alignments as Fasta",
1250
- className:
1251
- "copyAllAlignmentsFastaClipboardHelper",
1252
- hotkey: "cmd+c",
1253
- onClick: () => {
1254
- getAllAlignmentsFastaText();
1255
- window.toastr.success("Selection Copied");
1256
- }
1257
- },
1258
- {
1259
- text: `Copy Selection of ${name} as Fasta`,
1260
- className:
1261
- "copySpecificAlignmentFastaClipboardHelper",
1262
- onClick: () => {
1263
- copySpecificAlignmentFasta();
1264
- window.toastr.success(
1265
- "Selection Copied As Fasta"
1266
- );
1267
- }
1268
- },
1269
- {
1270
- text: `Copy Selection of ${name}`,
1271
- className:
1272
- "copySpecificAlignmentAsPlainClipboardHelper",
1273
- onClick: () => {
1274
- copySpecificAlignment();
1275
- window.toastr.success("Selection Copied");
1276
- }
1277
- }
1278
- ],
1279
- undefined,
1280
- event
1281
- );
1282
- }
1283
- }
1284
- easyStore={this.easyStore}
1285
- sequenceLength={sequenceLength}
1286
- charWidth={this.getCharWidthInLinearView()}
1287
- row={{ start: 0, end: sequenceLength - 1 }}
1288
- />
1289
- <PerformantCaret
1290
- leftMargin={this.state.nameDivWidth}
1291
- className="veAlignmentSelectionLayer"
1292
- isDraggable
1293
- sequenceLength={sequenceLength}
1294
- charWidth={this.getCharWidthInLinearView()}
1295
- row={{ start: 0, end: sequenceLength - 1 }}
1296
- easyStore={this.easyStore}
1297
- />
1298
- {isTemplate ? (
1299
- this.renderItem(0, 0, isTemplate)
1300
- ) : (
1301
- <ReactList
1302
- ref={c => {
1303
- this.InfiniteScroller = c;
1304
- const domNode = ReactDOM.findDOMNode(c);
1305
- if (domNode instanceof HTMLElement) {
1306
- drop_provided.innerRef(domNode);
1307
- }
1308
- }}
1309
- type="variable"
1310
- itemSizeEstimator={this.estimateRowHeight}
1311
- itemRenderer={this.renderItem}
1312
- length={
1313
- alignmentTracks.length +
1314
- (drop_snapshot.isUsingPlaceholder ? 1 : 0)
1315
- }
1316
- />
1317
- )}
1318
- </div>
1319
- </ReactDraggable>
1320
- </div>
1321
- </div>
1322
- );
1323
- };
1324
- if (isTemplate) return innerTrackVis();
1325
- else
1326
- return (
1327
- <Droppable
1328
- mode="virtual"
1329
- renderClone={(provided, snapshot, { source: { index } }) => {
1330
- return this.renderItem(index, index, false, {
1331
- provided,
1332
- snapshot
1333
- });
1334
- }}
1335
- direction="vertical"
1336
- droppableId={"droppable" + isTemplate ? "_no_drop" : ""}
1337
- >
1338
- {innerTrackVis}
1339
- </Droppable>
1340
- );
1341
- };
1342
-
1343
- const [firstTrack, ...otherTracks] = alignmentTracks;
1344
- const totalWidthOfMinimap = this.state.width;
1345
- const totalWidthInAlignmentView = 14 * this.getSequenceLength();
1346
- const minSliderSize = Math.min(
1347
- totalWidthOfMinimap * (totalWidthOfMinimap / totalWidthInAlignmentView),
1348
- totalWidthOfMinimap
1349
- );
1350
- const viewportHeight = Math.max(
1351
- document.documentElement.clientHeight,
1352
- window.innerHeight || 0
1353
- );
1354
-
1355
- /**
1356
- * Parameters to be passed to our Pinch Handler component
1357
- * OnPinch is the method to be executed when the pinch gesture is registered
1358
- * Pinch Handler for minimap
1359
- */
1360
- const pinchHandler = {
1361
- onPinch: ({ delta: [d] }) => {
1362
- this.bindOutsideChangeHelper.triggerChange(({ value, changeValue }) => {
1363
- // changeValue(d);
1364
- if (d > 0) {
1365
- if (value > 8) {
1366
- changeValue(value + 0.4);
1367
- } else {
1368
- changeValue(value + 0.2);
1369
- }
1370
- } else if (d < 0) {
1371
- if (value > 8) {
1372
- changeValue(value - 0.4);
1373
- } else {
1374
- changeValue(value - 0.2);
1375
- }
1376
- }
1377
- });
1378
- updateLabelsForInViewFeatures();
1379
- }
1380
- };
1381
-
1382
- return (
1383
- <PinchHelper {...pinchHandler}>
1384
- <ResizeSensor onResize={this.handleResize}>
1385
- <div
1386
- style={{
1387
- height: height || (isPairwise ? "auto" : viewportHeight * 0.88),
1388
- display: "flex",
1389
- flexDirection: "column",
1390
- justifyContent: "space-between",
1391
- position: "relative",
1392
- overflowY: "auto",
1393
- ...this.props.style
1394
- // borderTop: "1px solid black"
1395
- }}
1396
- className="alignmentView"
1397
- >
1398
- <DragDropContext
1399
- onDragStart={this.onTrackDragStart}
1400
- onDragEnd={this.onTrackDragEnd}
1401
- >
1402
- <div
1403
- style={{
1404
- display: "flex",
1405
- flexDirection: "column",
1406
- position: "relative",
1407
- overflowY: "auto"
1408
- }}
1409
- className="alignmentView-top-container"
1410
- >
1411
- <div
1412
- style={{
1413
- paddingTop: "3px",
1414
- paddingBottom: "5px",
1415
- borderBottom: "1px solid",
1416
- display: "flex",
1417
- minHeight: "32px",
1418
- width: "100%",
1419
- flexWrap: "nowrap",
1420
- flexDirection: "row",
1421
- flex: "0 0 auto"
1422
- }}
1423
- className="ve-alignment-top-bar"
1424
- >
1425
- {additionalTopLeftEl}
1426
- {handleBackButtonClicked && (
1427
- <Tooltip content="Back to Pairwise Alignment Overview">
1428
- <Button
1429
- icon="arrow-left"
1430
- onClick={() => {
1431
- // this.setState({
1432
- // charWidthInLinearView: charWidthInLinearViewDefault
1433
- // });
1434
- handleBackButtonClicked();
1435
- this.caretPositionUpdate(-1);
1436
- }}
1437
- small
1438
- intent={Intent.PRIMARY}
1439
- minimal
1440
- style={{ marginRight: 10 }}
1441
- className="alignmentViewBackButton"
1442
- />
1443
- </Tooltip>
1444
- )}
1445
-
1446
- <div style={{ display: "flex" }}>
1447
- <EditableText
1448
- disabled={!handleAlignmentRename}
1449
- onChange={v => {
1450
- this.setState({
1451
- alignmentName: v
1452
- });
1453
- }}
1454
- maxLength={399} //stop the name from being tooo long
1455
- value={this.state.alignmentName}
1456
- onConfirm={async v => {
1457
- if (!v) {
1458
- this.setState({
1459
- alignmentName: this.props.alignmentName
1460
- });
1461
- return;
1462
- }
1463
- if (v === this.props.alignmentName) {
1464
- return; //already saved this name
1465
- }
1466
- this.setState({ saveMessage: "Alignment Renaming.." });
1467
- this.setState({ saveMessageLoading: true });
1468
- await handleAlignmentRename(v, this.props);
1469
- this.setState({ saveMessage: "Rename Successful" });
1470
- this.setState({ saveMessageLoading: false });
1471
- setTimeout(() => {
1472
- this.setState({ saveMessage: undefined });
1473
- this.setState({ saveMessageLoading: false });
1474
- }, 5000);
1475
- }}
1476
- selectAllOnFocus={true}
1477
- className="veAlignmentName"
1478
- ></EditableText>
1479
- &nbsp;&nbsp;&nbsp;
1480
- <div
1481
- className="veAlignmentType"
1482
- style={{
1483
- paddingTop: "3px",
1484
- fontSize: "14px",
1485
- color: "grey",
1486
- maxWidth: "300px",
1487
- overflow: "hidden",
1488
- textOverflow: "ellipsis",
1489
- whiteSpace: "nowrap"
1490
- }}
1491
- data-title={
1492
- this.props.alignmentType || "Unknown Alignment Type"
1493
- }
1494
- >
1495
- {this.props.alignmentType || "Unknown Alignment Type"}
1496
- </div>
1497
- </div>
1498
-
1499
- {this.props.unmappedSeqs && (
1500
- <InfoHelper
1501
- size={20}
1502
- content={
1503
- <div>
1504
- This alignment had sequences that did not map to the
1505
- template sequence:
1506
- {this.props.unmappedSeqs.map(
1507
- ({ sequenceData }, i) => (
1508
- <div key={i}>{sequenceData.name}</div>
1509
- )
1510
- )}
1511
- </div>
1512
- }
1513
- intent="warning"
1514
- icon="warning-sign"
1515
- ></InfoHelper>
1516
- )}
1517
- {!isInPairwiseOverviewView && (
1518
- <UncontrolledSliderWithPlusMinusBtns
1519
- noWraparound
1520
- bindOutsideChangeHelper={this.bindOutsideChangeHelper}
1521
- onClick={() => {
1522
- setTimeout(this.scrollToCaret, 0);
1523
- }}
1524
- minCharWidth={this.getMinCharWidth()}
1525
- onChange={async zoomLvl => {
1526
- this.isZooming = true;
1527
- setTimeout(() => {
1528
- this.isZooming = false;
1529
- }, 10);
1530
- // zoomLvl is in the range of 0 to 10
1531
- const minCharWidth = this.getMinCharWidth();
1532
- const scaleFactor = Math.pow(12 / minCharWidth, 1 / 10);
1533
- const newCharWidth =
1534
- minCharWidth * Math.pow(scaleFactor, zoomLvl);
1535
- await this.setCharWidthInLinearView({
1536
- charWidthInLinearView: newCharWidth
1537
- });
1538
- await this.scrollToCaret();
1539
- await updateLabelsForInViewFeatures({
1540
- rectElement: ".alignmentHolder"
1541
- });
1542
- }}
1543
- coerceInitialValue={coerceInitialValue}
1544
- title="Adjust Zoom Level"
1545
- style={{ paddingTop: "4px", width: 100 }}
1546
- className="veZoomAlignmentSlider ove-slider"
1547
- labelRenderer={false}
1548
- initialValue={charWidthInLinearView}
1549
- stepSize={0.05} //was 0.01
1550
- max={10}
1551
- min={0}
1552
- clickStepSize={0.5}
1553
- />
1554
- )}
1555
- {!noVisibilityOptions && !isInPairwiseOverviewView && (
1556
- <AlignmentVisibilityTool
1557
- currentPairwiseAlignmentIndex={
1558
- currentPairwiseAlignmentIndex
1559
- }
1560
- {...alignmentVisibilityToolOptions}
1561
- />
1562
- )}
1563
- {updateAlignmentSortOrder && !isInPairwiseOverviewView && (
1564
- <Popover
1565
- minimal
1566
- content={
1567
- <Menu>
1568
- <MenuItem
1569
- active={true || alignmentSortOrder}
1570
- onClick={() => {
1571
- updateAlignmentSortOrder("Position");
1572
- }}
1573
- text="Position"
1574
- />
1575
- <MenuItem
1576
- active={false || alignmentSortOrder}
1577
- onClick={() => {
1578
- updateAlignmentSortOrder("Alphabetical");
1579
- }}
1580
- text="Alphabetical"
1581
- />
1582
- </Menu>
1583
- }
1584
- target={
1585
- <Button
1586
- small
1587
- text="Sort Order"
1588
- rightIcon="caret-down"
1589
- icon="sort"
1590
- />
1591
- }
1592
- />
1593
- )}
1594
- {additionalTopEl}
1595
- {this.state.saveMessage && (
1596
- <div
1597
- className="ove-menu-toast"
1598
- style={{
1599
- display: "flex",
1600
- alignItems: "center",
1601
- marginLeft: "auto",
1602
- marginRight: 10
1603
- }}
1604
- >
1605
- {this.state.saveMessageLoading ? (
1606
- <div>
1607
- <Spinner size={15}></Spinner>
1608
- </div>
1609
- ) : (
1610
- <Icon icon="tick-circle" intent="success"></Icon>
1611
- )}{" "}
1612
- &nbsp;
1613
- {this.state.saveMessage}
1614
- </div>
1615
- )}
1616
- </div>
1617
- {hasTemplate ? (
1618
- <React.Fragment>
1619
- <div className="alignmentTrackFixedToTop">
1620
- {getTrackVis([firstTrack], true, alignmentTracks)}
1621
- </div>
1622
- {getTrackVis(otherTracks, false, alignmentTracks)}
1623
- </React.Fragment>
1624
- ) : (
1625
- getTrackVis(alignmentTracks, false, alignmentTracks)
1626
- )}
1627
- </div>
1628
- </DragDropContext>
1629
- {!isInPairwiseOverviewView && (
1630
- <div
1631
- className="alignmentViewBottomBar"
1632
- style={{
1633
- // flexGrow: 1,
1634
- // minHeight: "-webkit-min-content", //https://stackoverflow.com/questions/28029736/how-to-prevent-a-flex-item-from-shrinking-smaller-than-its-content
1635
- maxHeight: 210,
1636
- marginTop: 4,
1637
- paddingTop: 4,
1638
- borderTop: "1px solid lightgrey",
1639
- display: "flex"
1640
- }}
1641
- >
1642
- <Minimap
1643
- {...{
1644
- selectionLayerComp: (
1645
- <React.Fragment>
1646
- <PerformantSelectionLayer
1647
- is
1648
- hideCarets
1649
- className="veAlignmentSelectionLayer veMinimapSelectionLayer"
1650
- easyStore={this.easyStore}
1651
- sequenceLength={sequenceLength}
1652
- charWidth={this.getMinCharWidth(true)}
1653
- row={{ start: 0, end: sequenceLength - 1 }}
1654
- />
1655
- <PerformantCaret
1656
- style={{
1657
- opacity: 0.2
1658
- }}
1659
- className="veAlignmentSelectionLayer veMinimapSelectionLayer"
1660
- sequenceLength={sequenceLength}
1661
- charWidth={this.getMinCharWidth(true)}
1662
- row={{ start: 0, end: sequenceLength - 1 }}
1663
- easyStore={this.easyStore}
1664
- />
1665
- </React.Fragment>
1666
- ),
1667
- alignmentTracks,
1668
- dimensions: {
1669
- width: Math.max(this.state.width, 10) || 10
1670
- },
1671
- nameDivOffsetPercent: 0,
1672
- // this.state.nameDivWidth / this.getMaxLinearViewWidth(),
1673
- scrollYToTrack: this.scrollYToTrack,
1674
- onSizeAdjust: this.onMinimapSizeAdjust,
1675
- minSliderSize,
1676
- laneHeight:
1677
- minimapLaneHeight ||
1678
- (alignmentTracks.length > 5 ? 10 : 17),
1679
- laneSpacing:
1680
- minimapLaneSpacing ||
1681
- (alignmentTracks.length > 5 ? 2 : 1),
1682
- easyStore: this.easyStore,
1683
- numBpsShownInLinearView: this.getNumBpsShownInLinearView(),
1684
- scrollAlignmentView: this.state.scrollAlignmentView
1685
- }}
1686
- onMinimapScrollX={this.scrollAlignmentToPercent}
1687
- />
1688
- </div>
1689
- )}
1690
- <GlobalDialog
1691
- // {...pickedUserDefinedHandlersAndOpts}
1692
- // dialogOverrides={pick(this.props, [
1693
- // "AddOrEditFeatureDialogOverride",
1694
- // "AddOrEditPartDialogOverride",
1695
- // "AddOrEditPrimerDialogOverride"
1696
- // ])}
1697
- />
1698
- </div>
1699
- </ResizeSensor>
1700
- </PinchHelper>
1701
- );
1702
- }
1703
-
1704
- getTrackTrimmingOptions({
971
+ const getTrackTrimmingOptions = ({
1705
972
  e,
1706
973
  allTracks,
1707
974
  upsertAlignmentRun,
1708
975
  currentPairwiseAlignmentIndex,
1709
976
  alignmentId
1710
- }) {
977
+ }) => {
1711
978
  const track = getTrackFromEvent(e, allTracks);
1712
979
 
1713
- this.getNearestCursorPositionToMouseEvent(
1714
- this.rowData,
980
+ getNearestCursorPositionToMouseEvent(
981
+ rowData.current,
1715
982
  e,
1716
983
  ({ nearestCaretPos }) => {
1717
- this.setState({
1718
- [`tempTrimmingCaret${track.index}`]: nearestCaretPos
1719
- });
984
+ setTempTrimmingCaret(prev => ({
985
+ ...prev,
986
+ [track.index]: nearestCaretPos
987
+ }));
1720
988
  const afterDisabled =
1721
989
  nearestCaretPos <= track.alignmentData.trimmedRange?.start;
1722
990
  const beforeDisabled =
@@ -1742,16 +1010,15 @@ export class AlignmentView extends React.Component {
1742
1010
  icon: "drawer-left-filled",
1743
1011
  onMouseOver: () =>
1744
1012
  !beforeDisabled &&
1745
- this.setState({
1746
- [`tempTrimBefore${track.index}`]: {
1747
- start: 0,
1748
- end: nearestCaretPos - 1
1749
- }
1750
- }),
1013
+ setTempTrimBefore(prev => ({
1014
+ ...prev,
1015
+ [track.index]: { start: 0, end: nearestCaretPos - 1 }
1016
+ })),
1751
1017
  onMouseLeave: () =>
1752
- this.setState({
1753
- [`tempTrimBefore${track.index}`]: undefined
1754
- }),
1018
+ setTempTrimBefore(prev => ({
1019
+ ...prev,
1020
+ [track.index]: undefined
1021
+ })),
1755
1022
  onClick: () => {
1756
1023
  updateTrackHelper({
1757
1024
  currentPairwiseAlignmentIndex,
@@ -1764,8 +1031,7 @@ export class AlignmentView extends React.Component {
1764
1031
  trimmedRange: {
1765
1032
  start: nearestCaretPos,
1766
1033
  end:
1767
- track.alignmentData.trimmedRange?.end ||
1768
- this.getMaxLength() - 1
1034
+ track.alignmentData.trimmedRange?.end || maxLength - 1
1769
1035
  }
1770
1036
  }
1771
1037
  });
@@ -1777,16 +1043,15 @@ export class AlignmentView extends React.Component {
1777
1043
  icon: "drawer-right-filled",
1778
1044
  onMouseOver: () =>
1779
1045
  !afterDisabled &&
1780
- this.setState({
1781
- [`tempTrimAfter${track.index}`]: {
1782
- start: nearestCaretPos,
1783
- end: this.getMaxLength() - 1
1784
- }
1785
- }),
1046
+ setTempTrimAfter(prev => ({
1047
+ ...prev,
1048
+ [track.index]: { start: nearestCaretPos, end: maxLength - 1 }
1049
+ })),
1786
1050
  onMouseLeave: () =>
1787
- this.setState({
1788
- [`tempTrimAfter${track.index}`]: undefined
1789
- }),
1051
+ setTempTrimAfter(prev => ({
1052
+ ...prev,
1053
+ [track.index]: undefined
1054
+ })),
1790
1055
  onClick: () => {
1791
1056
  updateTrackHelper({
1792
1057
  currentPairwiseAlignmentIndex,
@@ -1829,17 +1094,630 @@ export class AlignmentView extends React.Component {
1829
1094
  undefined,
1830
1095
  e,
1831
1096
  () => {
1832
- this.setState({
1833
- [`tempTrimmingCaret${track.index}`]: undefined
1834
- });
1097
+ setTempTrimAfter(prev => ({ ...prev, [track.index]: undefined }));
1835
1098
  }
1836
1099
  );
1837
1100
  }
1838
1101
  );
1839
1102
  e.preventDefault();
1840
1103
  e.stopPropagation();
1104
+ };
1105
+
1106
+ if (
1107
+ !alignmentTracks ||
1108
+ !alignmentTracks[0] ||
1109
+ !alignmentTracks[0].alignmentData
1110
+ ) {
1111
+ console.error("corrupted data!", props);
1112
+ return "corrupted data!";
1841
1113
  }
1842
- }
1114
+
1115
+ const getTrackVis = (alignmentTracks, isTemplate, allTracks) => {
1116
+ const rowData = {};
1117
+ const innerTrackVis = (drop_provided, drop_snapshot) => {
1118
+ return (
1119
+ <div
1120
+ className="alignmentTracks "
1121
+ style={{
1122
+ overflowY: "auto",
1123
+ display: "flex",
1124
+ zIndex: 10
1125
+ }}
1126
+ >
1127
+ <div
1128
+ style={{ overflowX: "auto", width }}
1129
+ ref={ref => {
1130
+ if (isTemplate) {
1131
+ alignmentHolderTop.current = ref;
1132
+ } else {
1133
+ alignmentHolder.current = ref;
1134
+ }
1135
+ }}
1136
+ onContextMenu={e => {
1137
+ if (
1138
+ !allowTrimming ||
1139
+ isTargetWithinEl(e, ".alignmentTrackName")
1140
+ ) {
1141
+ return;
1142
+ }
1143
+
1144
+ getTrackTrimmingOptions({
1145
+ e,
1146
+ allTracks,
1147
+ upsertAlignmentRun,
1148
+ alignmentId,
1149
+ currentPairwiseAlignmentIndex
1150
+ });
1151
+ }}
1152
+ onMouseLeave={removeMinimapHighlightForMouseLeave}
1153
+ onMouseMove={updateMinimapHighlightForMouseMove}
1154
+ dataname="scrollGroup"
1155
+ className="alignmentHolder"
1156
+ onScroll={isTemplate ? handleTopScroll : handleScroll}
1157
+ >
1158
+ <ReactDraggable
1159
+ disabled={isTrackDragging}
1160
+ bounds={{ top: 0, left: 0, right: 0, bottom: 0 }}
1161
+ onDrag={
1162
+ noClickDragHandlers
1163
+ ? noop
1164
+ : event => {
1165
+ if (isTrackDragging) return;
1166
+ getNearestCursorPositionToMouseEvent(
1167
+ rowData,
1168
+ event,
1169
+ editorDragged
1170
+ );
1171
+ }
1172
+ }
1173
+ onStart={
1174
+ noClickDragHandlers
1175
+ ? noop
1176
+ : event => {
1177
+ if (isTargetWithinEl(event, ".alignmentTrackName")) {
1178
+ setIsTrackDragging(true);
1179
+ return;
1180
+ }
1181
+ if (isTrackDragging) return;
1182
+ getNearestCursorPositionToMouseEvent(
1183
+ rowData,
1184
+ event,
1185
+ editorDragStarted
1186
+ );
1187
+ }
1188
+ }
1189
+ onStop={
1190
+ noClickDragHandlers
1191
+ ? noop
1192
+ : (...args) => {
1193
+ setTimeout(() => {
1194
+ setIsTrackDragging(false);
1195
+ }, 0);
1196
+ editorDragStopped(...args);
1197
+ }
1198
+ }
1199
+ >
1200
+ <div
1201
+ ref={veTracksAndAlignmentHolder}
1202
+ className={classNames("veTracksAndAlignmentHolder", {
1203
+ isTrackDragging
1204
+ })}
1205
+ onClick={
1206
+ noClickDragHandlers
1207
+ ? noop
1208
+ : event => {
1209
+ if (isTrackDragging) return;
1210
+ if (isTargetWithinEl(event, ".alignmentTrackName")) {
1211
+ return;
1212
+ }
1213
+ getNearestCursorPositionToMouseEvent(
1214
+ rowData,
1215
+ event,
1216
+ editorClicked
1217
+ );
1218
+ }
1219
+ }
1220
+ >
1221
+ <PerformantSelectionLayer
1222
+ leftMargin={nameDivWidth}
1223
+ className="veAlignmentSelectionLayer"
1224
+ isDraggable
1225
+ selectionLayerRightClicked={
1226
+ selectionLayerRightClicked
1227
+ ? (...args) => {
1228
+ // It's necessary to take the props out of the function
1229
+ // arguments
1230
+ selectionLayerRightClicked(...args, props);
1231
+ }
1232
+ : (...args) => {
1233
+ const { event } = args[0];
1234
+ const track = getTrackFromEvent(event, allTracks);
1235
+
1236
+ const alignmentData = track.alignmentData;
1237
+ const { name } = alignmentData;
1238
+
1239
+ const copySpecificAlignmentFasta = async () => {
1240
+ const { selectionLayer } =
1241
+ store.getState().VectorEditor.__allEditorsOptions
1242
+ .alignments[id] || {};
1243
+ const seqDataToCopy = getSequenceDataBetweenRange(
1244
+ alignmentData,
1245
+ selectionLayer
1246
+ ).sequence;
1247
+ const seqDataToCopyAsFasta = `>${name}\r\n${seqDataToCopy}\r\n`;
1248
+ await navigator.clipboard.writeText(
1249
+ seqDataToCopyAsFasta
1250
+ );
1251
+ };
1252
+
1253
+ const copySpecificAlignment = async () => {
1254
+ const { selectionLayer } =
1255
+ store.getState().VectorEditor.__allEditorsOptions
1256
+ .alignments[id] || {};
1257
+ const seqDataToCopy = getSequenceDataBetweenRange(
1258
+ alignmentData,
1259
+ selectionLayer
1260
+ ).sequence;
1261
+ await navigator.clipboard.writeText(seqDataToCopy);
1262
+ };
1263
+
1264
+ const getAllAlignmentsFastaText = async () => {
1265
+ await navigator.clipboard.writeText(
1266
+ getAllAlignmentsFastaText()
1267
+ );
1268
+ };
1269
+
1270
+ showContextMenu(
1271
+ [
1272
+ ...(additionalSelectionLayerRightClickedOptions
1273
+ ? additionalSelectionLayerRightClickedOptions(
1274
+ ...args,
1275
+ props
1276
+ )
1277
+ : []),
1278
+ {
1279
+ text: "Copy Selection of All Alignments as Fasta",
1280
+ className:
1281
+ "copyAllAlignmentsFastaClipboardHelper",
1282
+ hotkey: "cmd+c",
1283
+ onClick: () => {
1284
+ getAllAlignmentsFastaText();
1285
+ window.toastr.success("Selection Copied");
1286
+ }
1287
+ },
1288
+ {
1289
+ text: `Copy Selection of ${name} as Fasta`,
1290
+ className:
1291
+ "copySpecificAlignmentFastaClipboardHelper",
1292
+ onClick: () => {
1293
+ copySpecificAlignmentFasta();
1294
+ window.toastr.success(
1295
+ "Selection Copied As Fasta"
1296
+ );
1297
+ }
1298
+ },
1299
+ {
1300
+ text: `Copy Selection of ${name}`,
1301
+ className:
1302
+ "copySpecificAlignmentAsPlainClipboardHelper",
1303
+ onClick: () => {
1304
+ copySpecificAlignment();
1305
+ window.toastr.success("Selection Copied");
1306
+ }
1307
+ }
1308
+ ],
1309
+ undefined,
1310
+ event
1311
+ );
1312
+ }
1313
+ }
1314
+ easyStore={easyStore.current}
1315
+ sequenceLength={maxLength}
1316
+ charWidth={charWidth}
1317
+ row={{ start: 0, end: maxLength - 1 }}
1318
+ />
1319
+ <PerformantCaret
1320
+ leftMargin={nameDivWidth}
1321
+ className="veAlignmentSelectionLayer"
1322
+ isDraggable
1323
+ sequenceLength={maxLength}
1324
+ charWidth={charWidth}
1325
+ row={{ start: 0, end: maxLength - 1 }}
1326
+ easyStore={easyStore.current}
1327
+ />
1328
+ {isTemplate ? (
1329
+ renderItem(0, 0, isTemplate)
1330
+ ) : (
1331
+ <ReactList
1332
+ ref={c => {
1333
+ InfiniteScroller.current = c;
1334
+ const domNode = ReactDOM.findDOMNode(c);
1335
+ if (domNode instanceof HTMLElement) {
1336
+ drop_provided.innerRef(domNode);
1337
+ }
1338
+ }}
1339
+ type="variable"
1340
+ itemSizeEstimator={estimateRowHeight}
1341
+ itemRenderer={renderItem}
1342
+ length={
1343
+ alignmentTracks.length +
1344
+ (drop_snapshot.isUsingPlaceholder ? 1 : 0)
1345
+ }
1346
+ />
1347
+ )}
1348
+ </div>
1349
+ </ReactDraggable>
1350
+ </div>
1351
+ </div>
1352
+ );
1353
+ };
1354
+ if (isTemplate) return innerTrackVis();
1355
+ else
1356
+ return (
1357
+ <Droppable
1358
+ mode="virtual"
1359
+ renderClone={(provided, snapshot, { source: { index } }) => {
1360
+ return renderItem(index, index, false, {
1361
+ provided,
1362
+ snapshot
1363
+ });
1364
+ }}
1365
+ direction="vertical"
1366
+ droppableId={"droppable" + isTemplate ? "_no_drop" : ""}
1367
+ >
1368
+ {innerTrackVis}
1369
+ </Droppable>
1370
+ );
1371
+ };
1372
+
1373
+ const [firstTrack, ...otherTracks] = alignmentTracks;
1374
+ const totalWidthOfMinimap = width;
1375
+ const totalWidthInAlignmentView = 14 * getSequenceLength();
1376
+ const minSliderSize = Math.min(
1377
+ totalWidthOfMinimap * (totalWidthOfMinimap / totalWidthInAlignmentView),
1378
+ totalWidthOfMinimap
1379
+ );
1380
+ const viewportHeight = Math.max(
1381
+ document.documentElement.clientHeight,
1382
+ window.innerHeight || 0
1383
+ );
1384
+
1385
+ /**
1386
+ * Parameters to be passed to our Pinch Handler component
1387
+ * OnPinch is the method to be executed when the pinch gesture is registered
1388
+ * Pinch Handler for minimap
1389
+ */
1390
+ const pinchHandler = {
1391
+ onPinch: ({ delta: [d] }) => {
1392
+ bindOutsideChangeHelper.current.triggerChange(
1393
+ ({ value, changeValue }) => {
1394
+ // changeValue(d);
1395
+ if (d > 0) {
1396
+ if (value > 8) {
1397
+ changeValue(value + 0.4);
1398
+ } else {
1399
+ changeValue(value + 0.2);
1400
+ }
1401
+ } else if (d < 0) {
1402
+ if (value > 8) {
1403
+ changeValue(value - 0.4);
1404
+ } else {
1405
+ changeValue(value - 0.2);
1406
+ }
1407
+ }
1408
+ }
1409
+ );
1410
+ updateLabelsForInViewFeatures();
1411
+ }
1412
+ };
1413
+
1414
+ return (
1415
+ <PinchHelper {...pinchHandler}>
1416
+ <ResizeSensor onResize={handleResize}>
1417
+ <div
1418
+ style={{
1419
+ height: height || (isPairwise ? "auto" : viewportHeight * 0.88),
1420
+ display: "flex",
1421
+ flexDirection: "column",
1422
+ justifyContent: "space-between",
1423
+ position: "relative",
1424
+ overflowY: "auto",
1425
+ ...style
1426
+ // borderTop: "1px solid black"
1427
+ }}
1428
+ className="alignmentView"
1429
+ >
1430
+ <DragDropContext
1431
+ onDragStart={onTrackDragStart}
1432
+ onDragEnd={onTrackDragEnd}
1433
+ >
1434
+ <div
1435
+ style={{
1436
+ display: "flex",
1437
+ flexDirection: "column",
1438
+ position: "relative",
1439
+ overflowY: "auto"
1440
+ }}
1441
+ className="alignmentView-top-container"
1442
+ >
1443
+ <div
1444
+ style={{
1445
+ paddingTop: "3px",
1446
+ paddingBottom: "5px",
1447
+ borderBottom: "1px solid",
1448
+ display: "flex",
1449
+ minHeight: "32px",
1450
+ width: "100%",
1451
+ flexWrap: "nowrap",
1452
+ flexDirection: "row",
1453
+ flex: "0 0 auto"
1454
+ }}
1455
+ className="ve-alignment-top-bar"
1456
+ >
1457
+ {additionalTopLeftEl}
1458
+ {handleBackButtonClicked && (
1459
+ <Tooltip content="Back to Pairwise Alignment Overview">
1460
+ <Button
1461
+ icon="arrow-left"
1462
+ onClick={() => {
1463
+ handleBackButtonClicked();
1464
+ caretPositionUpdate(-1);
1465
+ }}
1466
+ small
1467
+ intent={Intent.PRIMARY}
1468
+ minimal
1469
+ style={{ marginRight: 10 }}
1470
+ className="alignmentViewBackButton"
1471
+ />
1472
+ </Tooltip>
1473
+ )}
1474
+
1475
+ <div style={{ display: "flex" }}>
1476
+ <EditableText
1477
+ disabled={!handleAlignmentRename}
1478
+ onChange={v => {
1479
+ setAlignmentName(v);
1480
+ }}
1481
+ maxLength={399} //stop the name from being tooo long
1482
+ value={alignmentName}
1483
+ onConfirm={async v => {
1484
+ if (!v) {
1485
+ setAlignmentName(_alignmentName);
1486
+ return;
1487
+ }
1488
+ if (v === _alignmentName) {
1489
+ return; //already saved this name
1490
+ }
1491
+ setSaveMessage("Alignment Renaming..");
1492
+ setSaveMessageLoading(true);
1493
+ await handleAlignmentRename(v, props);
1494
+ setSaveMessage("Rename Successful");
1495
+ setSaveMessageLoading(false);
1496
+ setTimeout(() => {
1497
+ setSaveMessage(undefined);
1498
+ setSaveMessageLoading(false);
1499
+ }, 5000);
1500
+ }}
1501
+ selectAllOnFocus={true}
1502
+ className="veAlignmentName"
1503
+ />
1504
+ &nbsp;&nbsp;&nbsp;
1505
+ <div
1506
+ className="veAlignmentType"
1507
+ style={{
1508
+ paddingTop: "3px",
1509
+ fontSize: "14px",
1510
+ color: "grey",
1511
+ maxWidth: "300px",
1512
+ overflow: "hidden",
1513
+ textOverflow: "ellipsis",
1514
+ whiteSpace: "nowrap"
1515
+ }}
1516
+ data-title={alignmentType || "Unknown Alignment Type"}
1517
+ >
1518
+ {alignmentType || "Unknown Alignment Type"}
1519
+ </div>
1520
+ </div>
1521
+
1522
+ {unmappedSeqs && (
1523
+ <InfoHelper
1524
+ size={20}
1525
+ content={
1526
+ <div>
1527
+ This alignment had sequences that did not map to the
1528
+ template sequence:
1529
+ {unmappedSeqs.map(({ sequenceData }, i) => (
1530
+ <div key={i}>{sequenceData.name}</div>
1531
+ ))}
1532
+ </div>
1533
+ }
1534
+ intent="warning"
1535
+ icon="warning-sign"
1536
+ />
1537
+ )}
1538
+ {!isInPairwiseOverviewView && (
1539
+ <UncontrolledSliderWithPlusMinusBtns
1540
+ noWraparound
1541
+ bindOutsideChangeHelper={bindOutsideChangeHelper.current}
1542
+ onClick={() => {
1543
+ setTimeout(scrollToCaret, 0);
1544
+ }}
1545
+ minCharWidth={getMinCharWidth()}
1546
+ onChange={async zoomLvl => {
1547
+ isZooming.current = true;
1548
+ setTimeout(() => {
1549
+ isZooming.current = false;
1550
+ }, 10);
1551
+ // zoomLvl is in the range of 0 to 10
1552
+ const minCharWidth = getMinCharWidth();
1553
+ const scaleFactor = Math.pow(12 / minCharWidth, 1 / 10);
1554
+ const newCharWidth =
1555
+ minCharWidth * Math.pow(scaleFactor, zoomLvl);
1556
+ await setCharWidthInLinearView({
1557
+ charWidthInLinearView: newCharWidth
1558
+ });
1559
+ await scrollToCaret();
1560
+ await updateLabelsForInViewFeatures({
1561
+ rectElement: ".alignmentHolder"
1562
+ });
1563
+ }}
1564
+ coerceInitialValue={coerceInitialValue}
1565
+ title="Adjust Zoom Level"
1566
+ style={{ paddingTop: "4px", width: 100 }}
1567
+ className="veZoomAlignmentSlider ove-slider"
1568
+ labelRenderer={false}
1569
+ initialValue={charWidth}
1570
+ stepSize={0.05} //was 0.01
1571
+ max={10}
1572
+ min={0}
1573
+ clickStepSize={0.5}
1574
+ />
1575
+ )}
1576
+ {!noVisibilityOptions && !isInPairwiseOverviewView && (
1577
+ <AlignmentVisibilityTool
1578
+ currentPairwiseAlignmentIndex={
1579
+ currentPairwiseAlignmentIndex
1580
+ }
1581
+ {...alignmentVisibilityToolOptions}
1582
+ />
1583
+ )}
1584
+ {updateAlignmentSortOrder && !isInPairwiseOverviewView && (
1585
+ <Popover
1586
+ minimal
1587
+ content={
1588
+ <Menu>
1589
+ <MenuItem
1590
+ active={true || alignmentSortOrder}
1591
+ onClick={() => {
1592
+ updateAlignmentSortOrder("Position");
1593
+ }}
1594
+ text="Position"
1595
+ />
1596
+ <MenuItem
1597
+ active={false || alignmentSortOrder}
1598
+ onClick={() => {
1599
+ updateAlignmentSortOrder("Alphabetical");
1600
+ }}
1601
+ text="Alphabetical"
1602
+ />
1603
+ </Menu>
1604
+ }
1605
+ target={
1606
+ <Button
1607
+ small
1608
+ text="Sort Order"
1609
+ rightIcon="caret-down"
1610
+ icon="sort"
1611
+ />
1612
+ }
1613
+ />
1614
+ )}
1615
+ {additionalTopEl}
1616
+ {saveMessage && (
1617
+ <div
1618
+ className="ove-menu-toast"
1619
+ style={{
1620
+ display: "flex",
1621
+ alignItems: "center",
1622
+ marginLeft: "auto",
1623
+ marginRight: 10
1624
+ }}
1625
+ >
1626
+ {saveMessageLoading ? (
1627
+ <div>
1628
+ <Spinner size={15}></Spinner>
1629
+ </div>
1630
+ ) : (
1631
+ <Icon icon="tick-circle" intent="success"></Icon>
1632
+ )}{" "}
1633
+ &nbsp;
1634
+ {saveMessage}
1635
+ </div>
1636
+ )}
1637
+ </div>
1638
+ {hasTemplate ? (
1639
+ <>
1640
+ <div className="alignmentTrackFixedToTop">
1641
+ {getTrackVis([firstTrack], true, alignmentTracks)}
1642
+ </div>
1643
+ {getTrackVis(otherTracks, false, alignmentTracks)}
1644
+ </>
1645
+ ) : (
1646
+ getTrackVis(alignmentTracks, false, alignmentTracks)
1647
+ )}
1648
+ </div>
1649
+ </DragDropContext>
1650
+ {!isInPairwiseOverviewView && (
1651
+ <div
1652
+ className="alignmentViewBottomBar"
1653
+ style={{
1654
+ // flexGrow: 1,
1655
+ // minHeight: "-webkit-min-content", //https://stackoverflow.com/questions/28029736/how-to-prevent-a-flex-item-from-shrinking-smaller-than-its-content
1656
+ maxHeight: 210,
1657
+ marginTop: 4,
1658
+ paddingTop: 4,
1659
+ borderTop: "1px solid lightgrey",
1660
+ display: "flex"
1661
+ }}
1662
+ >
1663
+ <Minimap
1664
+ selectionLayerComp={
1665
+ <>
1666
+ <PerformantSelectionLayer
1667
+ is
1668
+ hideCarets
1669
+ className="veAlignmentSelectionLayer veMinimapSelectionLayer"
1670
+ easyStore={easyStore.current}
1671
+ sequenceLength={maxLength}
1672
+ charWidth={getMinCharWidth(true)}
1673
+ row={{ start: 0, end: maxLength - 1 }}
1674
+ />
1675
+ <PerformantCaret
1676
+ style={{
1677
+ opacity: 0.2
1678
+ }}
1679
+ className="veAlignmentSelectionLayer veMinimapSelectionLayer"
1680
+ sequenceLength={maxLength}
1681
+ charWidth={getMinCharWidth(true)}
1682
+ row={{ start: 0, end: maxLength - 1 }}
1683
+ easyStore={easyStore.current}
1684
+ />
1685
+ </>
1686
+ }
1687
+ alignmentTracks={alignmentTracks}
1688
+ dimensions={{
1689
+ width: Math.max(width, 10) || 10
1690
+ }}
1691
+ nameDivOffsetPercent={0}
1692
+ scrollYToTrack={scrollYToTrack}
1693
+ onSizeAdjust={onMinimapSizeAdjust}
1694
+ minSliderSize={minSliderSize}
1695
+ laneHeight={
1696
+ minimapLaneHeight || (alignmentTracks.length > 5 ? 10 : 17)
1697
+ }
1698
+ laneSpacing={
1699
+ minimapLaneSpacing || (alignmentTracks.length > 5 ? 2 : 1)
1700
+ }
1701
+ easyStore={easyStore.current}
1702
+ numBpsShownInLinearView={getNumBpsShownInLinearView()}
1703
+ scrollAlignmentView={false}
1704
+ onMinimapScrollX={scrollAlignmentToPercent}
1705
+ />
1706
+ </div>
1707
+ )}
1708
+ <GlobalDialog
1709
+ // {...pickedUserDefinedHandlersAndOpts}
1710
+ // dialogOverrides={pick(this.props, [
1711
+ // "AddOrEditFeatureDialogOverride",
1712
+ // "AddOrEditPartDialogOverride",
1713
+ // "AddOrEditPrimerDialogOverride"
1714
+ // ])}
1715
+ />
1716
+ </div>
1717
+ </ResizeSensor>
1718
+ </PinchHelper>
1719
+ );
1720
+ };
1843
1721
 
1844
1722
  export default compose(
1845
1723
  withStore,