@onehat/ui
Version:
Base UI for OneHat apps
299 lines (268 loc) • 7.46 kB
JavaScript
import { forwardRef, useState, useRef, } from 'react';
import {
HORIZONTAL,
VERTICAL,
} from '../../Constants/Directions.js';
import {
UI_MODE_WEB,
UI_MODE_NATIVE,
CURRENT_MODE,
} from '../../Constants/UiModes.js';
import useBlocking from '../../Hooks/useBlocking.js';
import {
v4 as uuid,
} from 'uuid';
import getComponentFromType from '../../Functions/getComponentFromType.js';
// Note on modes:
// HORIZONTAL means the component moves along the X axis.
// VERTICAL means the component moves along the Y axis.
export default function withDraggable(WrappedComponent) {
return forwardRef((props, ref) => {
if (!props.isDraggable || props.alreadyHasDraggable) {
return <WrappedComponent {...props} ref={ref} />;
}
const {
// extract and pass
onDragStart,
onDrag,
onDragStop,
onChangeIsDragging,
getDraggableNodeFromNode = (node) => node,
getParentNode = (node) => node.parentElement.parentElement,
getProxy,
proxyParent,
proxyPositionRelativeToParent = false,
handle,
draggableProps = {},
...propsToPass
} = props,
{
// for local use
mode = HORIZONTAL, // HORIZONTAL, VERTICAL
} = props,
Draggable = getComponentFromType('Draggable'),
[isDragging, setIsDraggingRaw] = useState(false),
[node, setNode] = useState(false),
[bounds, setBounds] = useState(null),
nodeRef = useRef(null), // to get around React Draggable bug // https://stackoverflow.com/a/63603903
{ block } = useBlocking(),
setIsDragging = (value) => {
setIsDraggingRaw(value);
if (onChangeIsDragging) {
onChangeIsDragging(value);
}
},
handleStart = (e, info) => {
if (isDragging) {
return;
}
const
node = getDraggableNodeFromNode(info.node),
parentContainer = getParentNode && getParentNode(node);
setNode(node);
if (parentContainer && !parentContainer.id) {
parentContainer.id = 'a' + uuid().replace(/-/g, '');
setBounds(parentContainer.getBoundingClientRect());
}
// clone node for proxy, append to DOM
let proxy;
if (getProxy) {
proxy = getProxy(node);
} else {
proxy = node.cloneNode(true);
const nodeRect = node.getBoundingClientRect();
proxy.style.top = nodeRect.top + 'px';
proxy.style.left = nodeRect.left + 'px';
proxy.style.height = nodeRect.height + 'px';
proxy.style.width = nodeRect.width + 'px';
}
proxy = proxyParent ? proxyParent.appendChild(proxy) : document.body.appendChild(proxy);
proxy.style.zIndex = 10000;
proxy.style.position = 'absolute';
proxy.style.visibility = 'visible';
proxy.style.backgroundColor = '#fff';
proxy.id = 'dragproxy';
proxy.className = '';
node.style.visibility = 'hidden';
setIsDragging(true);
if (onDragStart) {
onDragStart(info, e, proxy, node)
}
},
isWithinBounds = ({ pageX, pageY }) => {
if (!bounds) {
return true;
}
const {
left,
right,
top,
bottom,
} = bounds;
if (mode === HORIZONTAL) {
return pageX >= left && pageX <= right;
} else if (mode === VERTICAL) {
return pageY <= bottom && pageX >= top;
} else {
return pageX >= left && pageX <= right && pageY <= bottom && pageX >= top;
}
},
handleDrag = (e, info) => {
// Move proxy to new page coords
if (!isWithinBounds(e)) {
return;
}
const {
deltaX,
deltaY,
} = info;
// Move the proxy to where it should be
const
proxy = document.getElementById('dragproxy'),
currentLeft = parseInt(proxy.style.left),
currentTop = parseInt(proxy.style.top);
if (mode === HORIZONTAL) {
const left = proxyPositionRelativeToParent ? e.pageX - proxyParent.getBoundingClientRect().left : e.pageX;
proxy.style.left = left + 'px';
} else if (mode === VERTICAL) {
const top = proxyPositionRelativeToParent ? e.pageY - proxyParent.getBoundingClientRect().top : e.pageY;
proxy.style.top = top + 'px';
} else {
proxy.style.left = currentLeft + deltaX + 'px';
proxy.style.top = currentTop + deltaY + 'px';
}
if (onDrag) {
onDrag(info, e, proxy, node);
}
},
handleStop = (e, info) => {
if (!isDragging) {
return;
}
// console.log('end', info);
// remove proxy
const proxy = document.getElementById('dragproxy');
proxy.remove();
// constrain node to bounds
if (!isWithinBounds(e)) {
const {
left,
right,
top,
bottom,
} = bounds,
{ pageX, pageY } = e;
let newX = pageX,
newY = pageY;
if (mode === HORIZONTAL) {
if (left > pageX) {
newX = left;
} else if (pageX > right) {
newX = right;
}
node.style.left = newX + 'px';
} else if (mode === VERTICAL) {
if (top > pageY) {
newX = top;
} else if (pageY > bottom) {
newX = bottom;
}
node.style.top = newY + 'px';
} else {
if (left > pageX) {
newX = left;
} else if (pageX > right) {
newX = right;
}
node.style.left = newX + 'px';
if (top > pageY) {
newX = top;
} else if (pageY > bottom) {
newX = bottom;
}
node.style.top = newY + 'px';
}
}
// show original node
node.style.visibility = 'visible';
block();
if (onDragStop) {
if (mode === HORIZONTAL) {
onDragStop(info.x, e, node);
} else if (mode === VERTICAL) {
onDragStop(info.y, e, node);
} else {
onDragStop(info, e, node);
}
}
setIsDragging(false);
};
propsToPass.isDragging = isDragging;
if (CURRENT_MODE === UI_MODE_WEB) {
if (mode === VERTICAL) {
return <Draggable
axis="x"
onStart={handleStart}
onDrag={handleDrag}
onStop={handleStop}
position={{ x: 0, y: 0, /* reset to dropped position */ }}
nodeRef={nodeRef}
// bounds={bounds}
{...draggableProps}
>
<div ref={nodeRef} className="nsResize">
<WrappedComponent
{...propsToPass}
alreadyHasDraggable={true}
ref={ref}
/>
</div>
</Draggable>;
} else if (mode === HORIZONTAL) {
return <Draggable
axis="y"
onStart={handleStart}
onDrag={handleDrag}
onStop={handleStop}
position={{ x: 0, y: 0, /* reset to dropped position */ }}
// bounds={bounds}
nodeRef={nodeRef}
{...draggableProps}
>
<div ref={nodeRef} className="ewResize" style={{ height: '100%', }}>
<WrappedComponent
{...propsToPass}
alreadyHasDraggable={true}
ref={ref}
/>
</div>
</Draggable>;
}
// can drag in all directions
return <Draggable
axis="both"
onStart={handleStart}
onDrag={handleDrag}
onStop={handleStop}
position={{ x: 0, y: 0, /* reset to dropped position */ }}
handle={handle}
nodeRef={nodeRef}
{...draggableProps}
>
<WrappedComponent
{...propsToPass}
alreadyHasDraggable={true}
ref={nodeRef}
/>
</Draggable>;
} else if (CURRENT_MODE === UI_MODE_NATIVE) {
// NOT YET IMPLEMENTED
// Really need to replace most of this, as much of it is web-centric.
return <WrappedComponent
{...propsToPass}
alreadyHasDraggable={true}
ref={ref}
/>;
}
});
}