higlass 2.2.3 → 2.3.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 (37) hide show
  1. package/app/scripts/Autocomplete.jsx +12 -3
  2. package/app/scripts/Button.jsx +20 -24
  3. package/app/scripts/ContextMenuContainer.jsx +1 -3
  4. package/app/scripts/ContextMenuItem.jsx +15 -16
  5. package/app/scripts/Dialog.jsx +30 -29
  6. package/app/scripts/HiGlassComponent.jsx +54 -36
  7. package/app/scripts/HorizontalItem.jsx +50 -21
  8. package/app/scripts/HorizontalTrack.jsx +1 -0
  9. package/app/scripts/ListWrapper.jsx +6 -17
  10. package/app/scripts/Modal.jsx +14 -13
  11. package/app/scripts/PopupMenu.jsx +6 -12
  12. package/app/scripts/SortableList.jsx +119 -64
  13. package/app/scripts/TrackArea.jsx +19 -24
  14. package/app/scripts/TrackControl.jsx +16 -27
  15. package/app/scripts/VerticalItem.jsx +53 -22
  16. package/app/scripts/VerticalTrack.jsx +1 -0
  17. package/app/scripts/api.js +5 -4
  18. package/app/scripts/hglib.jsx +34 -4
  19. package/app/scripts/test-helpers/test-helpers.jsx +2 -2
  20. package/app/scripts/utils/get-higlass-components.js +2 -2
  21. package/app/scripts/utils/react-dom-compat.js +100 -0
  22. package/app/styles/HiGlass.scss +1 -0
  23. package/dist/app/scripts/Autocomplete.d.ts +3 -1
  24. package/dist/app/scripts/ContextMenuContainer.d.ts +0 -1
  25. package/dist/app/scripts/ContextMenuItem.d.ts +9 -15
  26. package/dist/app/scripts/HiGlassComponent.d.ts +0 -3
  27. package/dist/app/scripts/HorizontalItem.d.ts +1 -1
  28. package/dist/app/scripts/ListWrapper.d.ts +2 -7
  29. package/dist/app/scripts/PopupMenu.d.ts +2 -4
  30. package/dist/app/scripts/SortableList.d.ts +6 -2
  31. package/dist/app/scripts/VerticalItem.d.ts +1 -1
  32. package/dist/app/scripts/utils/react-dom-compat.d.ts +27 -0
  33. package/dist/hglib.js +13135 -11155
  34. package/dist/hglib.min.js +89 -89
  35. package/dist/higlass.mjs +13143 -11163
  36. package/dist/package.json +17 -7
  37. package/package.json +17 -7
@@ -9,6 +9,7 @@ class PopupMenu extends React.Component {
9
9
  constructor(props) {
10
10
  super(props);
11
11
 
12
+ this.popup = null;
12
13
  this.clickHandlerBound = this.clickHandler.bind(this);
13
14
  this.contextMenuHandlerBound = this.contextMenuHandler.bind(this);
14
15
  this.resizeHandlerBound = this.resizeHandler.bind(this);
@@ -30,11 +31,8 @@ class PopupMenu extends React.Component {
30
31
  );
31
32
  window.addEventListener('resize', this.resizeHandlerBound, true);
32
33
 
33
- this._renderLayer();
34
- }
35
-
36
- componentDidUpdate() {
37
- this._renderLayer();
34
+ // Re-render now that this.popup is ready for the portal
35
+ this.forceUpdate();
38
36
  }
39
37
 
40
38
  componentWillUnmount() {
@@ -45,14 +43,10 @@ class PopupMenu extends React.Component {
45
43
  true,
46
44
  );
47
45
  window.removeEventListener('resize', this.resizeHandlerBound, true);
48
- ReactDOM.unmountComponentAtNode(this.popup);
46
+ // React automatically cleans up the portal contents
49
47
  document.body.removeChild(this.popup);
50
48
  }
51
49
 
52
- _renderLayer() {
53
- ReactDOM.render(this.props.children, this.popup);
54
- }
55
-
56
50
  clickHandler(event) {
57
51
  if (!this.popup.contains(event.target)) {
58
52
  if (this.props.onMenuClosed) this.props.onMenuClosed(event);
@@ -70,8 +64,8 @@ class PopupMenu extends React.Component {
70
64
  }
71
65
 
72
66
  render() {
73
- // Render a placeholder
74
- return <div />;
67
+ if (!this.popup) return null;
68
+ return ReactDOM.createPortal(this.props.children, this.popup);
75
69
  }
76
70
  }
77
71
 
@@ -1,71 +1,126 @@
1
1
  // @ts-nocheck
2
2
 
3
+ import {
4
+ DndContext,
5
+ PointerSensor,
6
+ closestCenter,
7
+ useSensor,
8
+ useSensors,
9
+ } from '@dnd-kit/core';
10
+ import {
11
+ SortableContext,
12
+ horizontalListSortingStrategy,
13
+ verticalListSortingStrategy,
14
+ } from '@dnd-kit/sortable';
3
15
  import React from 'react';
4
- import { SortableContainer } from 'react-sortable-hoc';
5
16
 
6
- const SortableList = SortableContainer(
7
- ({
8
- className,
9
- items,
10
- itemClass,
11
- itemControlAlignLeft,
12
- sortingIndex,
13
- useDragHandle,
14
- sortableHandlers,
15
- height,
16
- width,
17
- onCloseTrack,
18
- onCollapseTrack,
19
- onExpandTrack,
20
- onCloseTrackMenuOpened,
21
- onConfigTrackMenuOpened,
22
- onAddSeries,
23
- handleConfigTrack,
24
- editable,
25
- itemReactClass,
26
- handleResizeTrack,
27
- resizeHandles,
28
- }) => {
29
- const itemElements = items.map((item, index) =>
30
- React.createElement(itemReactClass, {
31
- key: `sci-${item.uid}`,
32
- className: itemClass,
33
- controlAlignLeft: itemControlAlignLeft,
34
- sortingIndex,
35
- index,
36
- uid: item.uid,
37
- height: item.height,
38
- width: item.width,
39
- isCollapsed: item.isCollapsed,
40
- item,
41
- useDragHandle,
42
- onCloseTrack,
43
- onCollapseTrack,
44
- onExpandTrack,
45
- onCloseTrackMenuOpened,
46
- onConfigTrackMenuOpened,
47
- onAddSeries,
48
- handleConfigTrack,
49
- editable,
50
- handleResizeTrack,
51
- resizeHandles,
52
- }),
53
- );
17
+ function SortableList({
18
+ className,
19
+ items,
20
+ itemClass,
21
+ itemControlAlignLeft,
22
+ sortingIndex,
23
+ useDragHandle,
24
+ sortableHandlers,
25
+ height,
26
+ width,
27
+ onCloseTrack,
28
+ onCollapseTrack,
29
+ onExpandTrack,
30
+ onCloseTrackMenuOpened,
31
+ onConfigTrackMenuOpened,
32
+ onAddSeries,
33
+ handleConfigTrack,
34
+ editable,
35
+ itemReactClass,
36
+ handleResizeTrack,
37
+ resizeHandles,
38
+ onSortEnd,
39
+ onSortStart,
40
+ onSortMove,
41
+ axis,
42
+ }) {
43
+ const sensors = useSensors(
44
+ useSensor(PointerSensor, {
45
+ activationConstraint: { distance: 5 },
46
+ }),
47
+ );
54
48
 
55
- return (
56
- <div
57
- className={className}
58
- style={{
59
- height,
60
- width,
61
- background: 'transparent',
62
- }}
63
- {...sortableHandlers}
64
- >
65
- {itemElements}
66
- </div>
67
- );
68
- },
69
- );
49
+ const handleDragStart = (event) => {
50
+ const { active } = event;
51
+ const index = items.findIndex((item) => item.uid === active.id);
52
+ if (onSortStart) {
53
+ onSortStart({ index });
54
+ }
55
+ };
56
+
57
+ const handleDragOver = (event) => {
58
+ const { active, over } = event;
59
+ if (!over || active.id === over.id) return;
60
+
61
+ const oldIndex = items.findIndex((item) => item.uid === active.id);
62
+ const newIndex = items.findIndex((item) => item.uid === over.id);
63
+
64
+ if (oldIndex !== -1 && newIndex !== -1) {
65
+ onSortEnd({ oldIndex, newIndex });
66
+ }
67
+ };
68
+
69
+ const isHorizontalAxis = axis === 'x';
70
+ const strategy = isHorizontalAxis
71
+ ? horizontalListSortingStrategy
72
+ : verticalListSortingStrategy;
73
+
74
+ const itemIds = items.map((item) => item.uid);
75
+
76
+ const itemElements = items.map((item, index) =>
77
+ React.createElement(itemReactClass, {
78
+ key: `sci-${item.uid}`,
79
+ className: itemClass,
80
+ controlAlignLeft: itemControlAlignLeft,
81
+ sortingIndex,
82
+ index,
83
+ uid: item.uid,
84
+ height: item.height,
85
+ width: item.width,
86
+ isCollapsed: item.isCollapsed,
87
+ item,
88
+ useDragHandle,
89
+ onCloseTrack,
90
+ onCollapseTrack,
91
+ onExpandTrack,
92
+ onCloseTrackMenuOpened,
93
+ onConfigTrackMenuOpened,
94
+ onAddSeries,
95
+ handleConfigTrack,
96
+ editable,
97
+ handleResizeTrack,
98
+ resizeHandles,
99
+ }),
100
+ );
101
+
102
+ return (
103
+ <DndContext
104
+ sensors={sensors}
105
+ collisionDetection={closestCenter}
106
+ onDragStart={handleDragStart}
107
+ onDragOver={handleDragOver}
108
+ onDragEnd={() => {}}
109
+ >
110
+ <SortableContext items={itemIds} strategy={strategy}>
111
+ <div
112
+ className={className}
113
+ style={{
114
+ height,
115
+ width,
116
+ background: 'transparent',
117
+ }}
118
+ >
119
+ {itemElements}
120
+ </div>
121
+ </SortableContext>
122
+ </DndContext>
123
+ );
124
+ }
70
125
 
71
126
  export default SortableList;
@@ -1,8 +1,6 @@
1
1
  // @ts-nocheck
2
2
  import PropTypes from 'prop-types';
3
3
  import React from 'react';
4
- import ReactDOM from 'react-dom';
5
- import { SortableHandle } from 'react-sortable-hoc';
6
4
 
7
5
  class TrackArea extends React.Component {
8
6
  constructor(props) {
@@ -36,24 +34,23 @@ class TrackArea extends React.Component {
36
34
  }
37
35
 
38
36
  getControls() {
39
- let Handle = null;
37
+ const { dragHandleProps } = this.props;
40
38
 
41
- if (this.moveable) {
42
- Handle = SortableHandle(() => (
43
- <svg
44
- className="no-zoom"
45
- height="10px"
46
- onClick={() => {}}
47
- style={this.getMoveImgStyle()}
48
- width="10px"
49
- >
50
- <title>Move</title>
51
- <use xlinkHref="#move" />
52
- </svg>
53
- ));
54
- } else {
55
- Handle = SortableHandle(() => <div />);
56
- }
39
+ const handle = this.moveable ? (
40
+ <svg
41
+ className="no-zoom"
42
+ height="10px"
43
+ onClick={() => {}}
44
+ style={this.getMoveImgStyle()}
45
+ width="10px"
46
+ {...dragHandleProps}
47
+ >
48
+ <title>Move</title>
49
+ <use xlinkHref="#move" />
50
+ </svg>
51
+ ) : (
52
+ <div />
53
+ );
57
54
 
58
55
  return (
59
56
  <div
@@ -69,7 +66,7 @@ class TrackArea extends React.Component {
69
66
  border: '1px solid #dddddd',
70
67
  }}
71
68
  >
72
- <Handle />
69
+ {handle}
73
70
 
74
71
  <svg
75
72
  ref={(c) => {
@@ -78,8 +75,7 @@ class TrackArea extends React.Component {
78
75
  className="no-zoom"
79
76
  height="10px"
80
77
  onClick={() => {
81
- const imgDom = ReactDOM.findDOMNode(this.imgConfig);
82
- const bbox = imgDom.getBoundingClientRect();
78
+ const bbox = this.imgConfig.getBoundingClientRect();
83
79
  this.props.onConfigTrackMenuOpened(this.props.uid, bbox);
84
80
  }}
85
81
  style={this.getSettingsImgStyle()}
@@ -110,8 +106,7 @@ class TrackArea extends React.Component {
110
106
  className="no-zoom"
111
107
  height="10px"
112
108
  onClick={() => {
113
- const imgDom = ReactDOM.findDOMNode(this.imgClose);
114
- const bbox = imgDom.getBoundingClientRect();
109
+ const bbox = this.imgClose.getBoundingClientRect();
115
110
  this.props.onCloseTrackMenuOpened(this.props.uid, bbox);
116
111
  }}
117
112
  style={this.getCloseImgStyle()}
@@ -2,7 +2,6 @@
2
2
  import clsx from 'clsx';
3
3
  import PropTypes from 'prop-types';
4
4
  import React from 'react';
5
- import { SortableHandle } from 'react-sortable-hoc';
6
5
 
7
6
  import { THEME_DARK } from './configs';
8
7
  import withTheme from './hocs/with-theme';
@@ -25,39 +24,29 @@ const getButtonClassName = (props) =>
25
24
  [classes['track-control-button-vertical']]: props.isVertical,
26
25
  });
27
26
 
28
- let oldProps = null;
29
- let DragHandle = null;
30
-
31
27
  function TrackControl(props) {
32
- // Avoid constant recreating that button when the props didn't change.
33
- // Damn React could be a little smarter here...
34
- if (
35
- !props ||
36
- !oldProps ||
37
- Object.keys(props).some((key) => oldProps[key] !== props[key])
38
- ) {
39
- oldProps = props;
40
- DragHandle = SortableHandle(() => (
41
- <svg
42
- className={getButtonClassName(props)}
43
- style={{
44
- height: '20px',
45
- width: '20px',
46
- ...props.imgStyleMove,
47
- }}
48
- >
49
- <title>Move track</title>
50
- <use xlinkHref="#move" />
51
- </svg>
52
- ));
53
- }
28
+ const { dragHandleProps } = props;
54
29
 
55
30
  let imgConfig;
56
31
  let imgClose;
57
32
 
58
33
  return (
59
34
  <div className={getClassName(props)}>
60
- {props.isMoveable && <DragHandle />}
35
+ {props.isMoveable && (
36
+ <svg
37
+ className={getButtonClassName(props)}
38
+ draggable={false}
39
+ style={{
40
+ height: '20px',
41
+ width: '20px',
42
+ ...props.imgStyleMove,
43
+ }}
44
+ {...dragHandleProps}
45
+ >
46
+ <title>Move track</title>
47
+ <use xlinkHref="#move" />
48
+ </svg>
49
+ )}
61
50
 
62
51
  {/* Show collapse button */}
63
52
  {props.expandCollapseAvailable && !props.isCollapsed && (
@@ -1,29 +1,60 @@
1
1
  // @ts-nocheck
2
+
3
+ import { useSortable } from '@dnd-kit/sortable';
4
+ import { CSS } from '@dnd-kit/utilities';
2
5
  import React from 'react';
3
- import { SortableElement } from 'react-sortable-hoc';
4
6
 
5
7
  import VerticalTrack from './VerticalTrack';
6
8
 
7
- const VerticalItem = SortableElement((props) => (
8
- <VerticalTrack
9
- className={props.className}
10
- controlAlignLeft={props.controlAlignLeft}
11
- editable={props.editable}
12
- handleConfigTrack={props.handleConfigTrack}
13
- handleResizeTrack={props.handleResizeTrack}
14
- height={props.height}
15
- isCollapsed={props.isCollapsed}
16
- item={props.item}
17
- onAddSeries={props.onAddSeries}
18
- onCollapseTrack={props.onCollapseTrack}
19
- onExpandTrack={props.onExpandTrack}
20
- onCloseTrack={props.onCloseTrack}
21
- onCloseTrackMenuOpened={props.onCloseTrackMenuOpened}
22
- onConfigTrackMenuOpened={props.onConfigTrackMenuOpened}
23
- resizeHandles={props.resizeHandles}
24
- uid={props.uid}
25
- width={props.width}
26
- />
27
- ));
9
+ function VerticalItem(props) {
10
+ const {
11
+ attributes,
12
+ listeners,
13
+ setNodeRef,
14
+ transform,
15
+ transition,
16
+ isDragging,
17
+ } = useSortable({ id: props.uid });
18
+
19
+ const adjustedTransform = transform ? { ...transform, y: 0 } : transform;
20
+
21
+ const style = {
22
+ transform: CSS.Transform.toString(adjustedTransform),
23
+ transition,
24
+ ...(isDragging
25
+ ? {
26
+ zIndex: 10,
27
+ background: 'rgba(0, 0, 0, 0.1)',
28
+ border: '2px dashed rgba(0, 0, 0, 0.3)',
29
+ borderRadius: 4,
30
+ }
31
+ : {}),
32
+ };
33
+
34
+ return (
35
+ <div ref={setNodeRef} style={style}>
36
+ <VerticalTrack
37
+ className={props.className}
38
+ controlAlignLeft={props.controlAlignLeft}
39
+ dragHandleProps={{ ...attributes, ...listeners }}
40
+ editable={props.editable}
41
+ handleConfigTrack={props.handleConfigTrack}
42
+ handleResizeTrack={props.handleResizeTrack}
43
+ height={props.height}
44
+ isCollapsed={props.isCollapsed}
45
+ item={props.item}
46
+ onAddSeries={props.onAddSeries}
47
+ onCollapseTrack={props.onCollapseTrack}
48
+ onExpandTrack={props.onExpandTrack}
49
+ onCloseTrack={props.onCloseTrack}
50
+ onCloseTrackMenuOpened={props.onCloseTrackMenuOpened}
51
+ onConfigTrackMenuOpened={props.onConfigTrackMenuOpened}
52
+ resizeHandles={props.resizeHandles}
53
+ uid={props.uid}
54
+ width={props.width}
55
+ />
56
+ </div>
57
+ );
58
+ }
28
59
 
29
60
  export default VerticalItem;
@@ -16,6 +16,7 @@ class VerticalTrack extends MoveableTrack {
16
16
  // for use. Only available in horizontal and vertical tracks and not
17
17
  // center.
18
18
  expandCollapseAvailable={true}
19
+ dragHandleProps={this.props.dragHandleProps}
19
20
  imgStyleAdd={STYLES}
20
21
  imgStyleClose={STYLES}
21
22
  imgStyleMove={STYLES}
@@ -1,10 +1,10 @@
1
1
  // @ts-nocheck
2
2
  import Ajv from 'ajv';
3
3
  import createPubSub from 'pub-sub-es';
4
- import ReactDOM from 'react-dom';
5
-
6
4
  import schema from '../schema.json';
7
5
 
6
+ import { unmountFromContainer } from './utils/react-dom-compat';
7
+
8
8
  import { getTileProxyAuthHeader, setTileProxyAuthHeader } from './services';
9
9
 
10
10
  import { getTrackObjectFromHGC } from './utils';
@@ -162,8 +162,9 @@ const createApi = function api(context, pubSub) {
162
162
  */
163
163
  destroy() {
164
164
  destroy();
165
- if (self.topDivRef.current) {
166
- ReactDOM.unmountComponentAtNode(self.topDivRef.current);
165
+ const container = self.topDivRef.current?.parentNode;
166
+ if (container) {
167
+ unmountFromContainer(container);
167
168
  }
168
169
  },
169
170
 
@@ -1,6 +1,10 @@
1
1
  import React from 'react';
2
- import ReactDOM from 'react-dom';
3
2
  import HiGlassComponent from './HiGlassComponent';
3
+ import {
4
+ ensureReady,
5
+ renderToContainer,
6
+ unmountFromContainer,
7
+ } from './utils/react-dom-compat';
4
8
 
5
9
  import HorizontalGeneAnnotationsTrack from './HorizontalGeneAnnotationsTrack';
6
10
  // these exports can be used to create new tracks in outside environments (e.g. Observable)
@@ -77,8 +81,10 @@ export { OPTIONS_INFO } from './options-info';
77
81
  * @returns {Promise<HiGlassComponent>}
78
82
  */
79
83
  const launch = async (element, config, options = {}) => {
80
- return new Promise((resolve) => {
81
- ReactDOM.render(
84
+ await ensureReady();
85
+ const hgc = await new Promise((resolve) => {
86
+ renderToContainer(
87
+ element,
82
88
  <HiGlassComponent
83
89
  ref={(/** @type {HiGlassComponent | null} */ ref) => {
84
90
  // Wait to resolve until React gives us a ref
@@ -87,9 +93,33 @@ const launch = async (element, config, options = {}) => {
87
93
  options={options}
88
94
  viewConfig={config}
89
95
  />,
90
- element,
91
96
  );
92
97
  });
98
+
99
+ // react-grid-layout v2 uses async useEffect for layout synchronization.
100
+ // Wait until TiledPlots are rendered (trackRenderer is populated) before
101
+ // returning, so the component is truly ready to use.
102
+ const viewUids = Object.keys(hgc.state.views);
103
+ if (viewUids.length > 0) {
104
+ await /** @type {Promise<void>} */ (
105
+ new Promise((resolve) => {
106
+ const start = performance.now();
107
+ const check = () => {
108
+ const allReady = viewUids.every(
109
+ (uid) => hgc.tiledPlots[uid]?.trackRenderer,
110
+ );
111
+ if (allReady || performance.now() - start > 5000) {
112
+ resolve();
113
+ } else {
114
+ setTimeout(check, 16);
115
+ }
116
+ };
117
+ check();
118
+ })
119
+ );
120
+ }
121
+
122
+ return hgc;
93
123
  };
94
124
 
95
125
  /**
@@ -1,9 +1,9 @@
1
1
  import React from 'react';
2
- import ReactDOM from 'react-dom';
3
2
 
4
3
  import { mount } from 'enzyme';
5
4
 
6
5
  import { requestsInFlight } from '../services';
6
+ import { unmountFromContainer } from '../utils/react-dom-compat';
7
7
 
8
8
  import { getTrackObjectFromHGC, getTrackRenderer } from '../utils';
9
9
 
@@ -323,7 +323,7 @@ export const waitForComponentReady = async (div) => {
323
323
  export const removeHGComponent = (div) => {
324
324
  if (!div) return;
325
325
 
326
- ReactDOM.unmountComponentAtNode(div);
326
+ unmountFromContainer(div);
327
327
  document.body.removeChild(div);
328
328
  };
329
329
 
@@ -24,7 +24,7 @@ export const getTrackObjectFromHGC = (hgc, viewUid, trackUid) => {
24
24
  newTrackUid = trackUid;
25
25
  }
26
26
 
27
- return hgc.tiledPlots[newViewUid].trackRenderer?.getTrackObject(newTrackUid);
27
+ return hgc.tiledPlots[newViewUid]?.trackRenderer?.getTrackObject(newTrackUid);
28
28
  };
29
29
 
30
30
  /**
@@ -33,7 +33,7 @@ export const getTrackObjectFromHGC = (hgc, viewUid, trackUid) => {
33
33
  * @returns {TrackRenderer | null}
34
34
  */
35
35
  export const getTrackRenderer = (hgc, viewUid) =>
36
- hgc.tiledPlots[viewUid].trackRenderer;
36
+ hgc.tiledPlots[viewUid]?.trackRenderer;
37
37
 
38
38
  /**
39
39
  * @param {HiGlassComponent} hgc