client/app/components/Resizable/index.jsx (144 lines of code) (raw):
import d3 from "d3";
import React, { useRef, useMemo, useCallback, useState, useEffect } from "react";
import PropTypes from "prop-types";
import { Resizable as ReactResizable } from "react-resizable";
import KeyboardShortcuts from "@/services/KeyboardShortcuts";
import "./index.less";
export default function Resizable({ toggleShortcut, direction, sizeAttribute, children }) {
const [size, setSize] = useState(0);
const elementRef = useRef();
const wasUsingTouchEventsRef = useRef(false);
const wasResizedRef = useRef(false);
const sizeProp = direction === "horizontal" ? "width" : "height";
sizeAttribute = sizeAttribute || sizeProp;
const getElementSize = useCallback(() => {
if (!elementRef.current) {
return 0;
}
return Math.floor(elementRef.current.getBoundingClientRect()[sizeProp]);
}, [sizeProp]);
const savedSize = useRef(null);
const toggle = useCallback(() => {
if (!elementRef.current) {
return;
}
const element = d3.select(elementRef.current);
let targetSize;
if (savedSize.current === null) {
targetSize = "0px";
savedSize.current = `${getElementSize()}px`;
} else {
targetSize = savedSize.current;
savedSize.current = null;
}
element
.style(sizeAttribute, savedSize.current || "0px")
.transition()
.duration(200)
.ease("swing")
.style(sizeAttribute, targetSize);
// update state to new element's size
setSize(parseInt(targetSize) || 0);
}, [getElementSize, sizeAttribute]);
const resizeHandle = useMemo(
() => (
<span
className={`react-resizable-handle react-resizable-handle-${direction}`}
onClick={() => {
// On desktops resize uses `mousedown`/`mousemove`/`mouseup` events, and there is a conflict
// with this `click` handler: after user releases mouse - this handler will be executed.
// So we use `wasResized` flag to check if there was actual resize or user just pressed and released
// left mouse button (see also resize event handlers where ths flag is set).
// On mobile devices `touchstart`/`touchend` events wll be used, so it's safe to just execute this handler.
// To detect which set of events was actually used during particular resize operation, we pass
// `onMouseDown` handler to draggable core and check event type there (see also that handler's code).
if (wasUsingTouchEventsRef.current || !wasResizedRef.current) {
toggle();
}
wasUsingTouchEventsRef.current = false;
wasResizedRef.current = false;
}}
/>
),
[direction, toggle]
);
useEffect(() => {
if (toggleShortcut) {
const shortcuts = {
[toggleShortcut]: toggle,
};
KeyboardShortcuts.bind(shortcuts);
return () => {
KeyboardShortcuts.unbind(shortcuts);
};
}
}, [toggleShortcut, toggle]);
const resizeEventHandlers = useMemo(
() => ({
onResizeStart: () => {
// use element's size as initial value (it will also check constraints set in CSS)
// updated here and in `draggableCore::onMouseDown` handler to ensure that right value will be used
setSize(getElementSize());
},
onResize: (unused, data) => {
// update element directly for better UI responsiveness
d3.select(elementRef.current).style(sizeAttribute, `${data.size[sizeProp]}px`);
setSize(data.size[sizeProp]);
wasResizedRef.current = true;
},
onResizeStop: () => {
if (wasResizedRef.current) {
savedSize.current = null;
}
},
}),
[sizeProp, getElementSize, sizeAttribute]
);
const draggableCoreOptions = useMemo(
() => ({
onMouseDown: e => {
// In some cases this handler is executed twice during the same resize operation - first time
// with `touchstart` event and second time with `mousedown` (probably emulated by browser).
// Therefore we set the flag only when we receive `touchstart` because in ths case it's definitely
// mobile browser (desktop browsers will also send `mousedown` but never `touchstart`).
if (e.type === "touchstart") {
wasUsingTouchEventsRef.current = true;
}
// use element's size as initial value (it will also check constraints set in CSS)
// updated here and in `onResizeStart` handler to ensure that right value will be used
setSize(getElementSize());
},
}),
[getElementSize]
);
if (!children) {
return null;
}
children = React.createElement(children.type, { ...children.props, ref: elementRef });
return (
<ReactResizable
className="resizable-component"
axis={direction === "horizontal" ? "x" : "y"}
resizeHandles={[direction === "horizontal" ? "e" : "s"]}
handle={resizeHandle}
width={direction === "horizontal" ? size : 0}
height={direction === "vertical" ? size : 0}
minConstraints={[0, 0]}
{...resizeEventHandlers}
draggableOpts={draggableCoreOptions}>
{children}
</ReactResizable>
);
}
Resizable.propTypes = {
direction: PropTypes.oneOf(["horizontal", "vertical"]),
sizeAttribute: PropTypes.string,
toggleShortcut: PropTypes.string,
children: PropTypes.element,
};
Resizable.defaultProps = {
direction: "horizontal",
sizeAttribute: null, // "width"/"height" - depending on `direction`
toggleShortcut: null,
children: null,
};