First commit.
This commit is contained in:
@@ -0,0 +1,922 @@
|
||||
import React, { MouseEvent, PointerEvent, Component } from 'react';
|
||||
import { createStyles, Theme } from '@material-ui/core/styles';
|
||||
import { Property } from "csstype";
|
||||
import { nullCast} from './Utility'
|
||||
|
||||
const SELECT_SCALE = 1.0;
|
||||
|
||||
const AUTOSCROLL_TICK_DELAY = 30;
|
||||
const AUTOSCROLL_THRESHOLD = 48;
|
||||
const AUTOSCROLL_SCROLL_PER_SECOND = 300;
|
||||
const AUTOSCROLL_SCROLL_RATE = AUTOSCROLL_SCROLL_PER_SECOND * AUTOSCROLL_TICK_DELAY / 1000;
|
||||
|
||||
const LONG_PRESS_TIME_MS = 250;
|
||||
|
||||
|
||||
const ANIMATION_DURATION_MS = 100;
|
||||
|
||||
|
||||
|
||||
class AnimationData {
|
||||
constructor(element: HTMLElement, originalPosition: DOMRect) {
|
||||
this.element = element;
|
||||
this.originalPosition = originalPosition;
|
||||
this.fromY = this.toY = this.currentY = this.originalPosition.y;
|
||||
this.key = (element as any)["key"];
|
||||
}
|
||||
key: any;
|
||||
element: HTMLElement;
|
||||
originalPosition: DOMRect;
|
||||
fromY: number;
|
||||
toY: number;
|
||||
currentY: number;
|
||||
|
||||
animating: boolean = false;
|
||||
|
||||
setPosition(y: number)
|
||||
{
|
||||
this.fromY = this.toY;
|
||||
this.toY = y;
|
||||
this.animating = true;
|
||||
}
|
||||
snapToPosition() {
|
||||
this.fromY = this.toY;
|
||||
this.currentY = this.toY;
|
||||
this.animating = true;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: {
|
||||
position: "relative",
|
||||
margin: "12px",
|
||||
background: theme.palette.background.paper
|
||||
}
|
||||
});
|
||||
|
||||
type Point = { x: number, y: number };
|
||||
|
||||
export enum ScrollDirection {
|
||||
None, X, Y
|
||||
}
|
||||
export interface DraggableGridProps
|
||||
{
|
||||
fullWidth?: boolean; // defaults to true
|
||||
fullHeight?: boolean; // defaults to true.
|
||||
onDragStart?: (itemIndex: number, clientX: number, clientY: number) => void;
|
||||
onDragMove?: (currentItemIndex: number, clientX: number, clientY: number) => void;
|
||||
onDragEnd?: (fromIndex: number, toIndex: number, clientX: number, clientY: number) => void;
|
||||
onDragCancel?: (clientX: number, clientY: number) => void;
|
||||
|
||||
defaultSelectedIndex?: number;
|
||||
|
||||
onLongPress?: (itemIndex: number) => void;
|
||||
|
||||
canDrag?: boolean;
|
||||
|
||||
children: React.ReactNode[];
|
||||
|
||||
moveElement: (from: number, to: number) => void;
|
||||
scroll: ScrollDirection;
|
||||
|
||||
};
|
||||
|
||||
type DraggableGridState = {
|
||||
indexToSelect?: number;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const DraggableGrid =
|
||||
(
|
||||
class extends Component<DraggableGridProps, DraggableGridState>
|
||||
{
|
||||
refScrollContainer: React.RefObject<HTMLDivElement>;
|
||||
refGrid: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: DraggableGridProps) {
|
||||
super(props);
|
||||
this.refScrollContainer = React.createRef();
|
||||
this.refGrid = React.createRef();
|
||||
|
||||
this.state = {
|
||||
indexToSelect: props.defaultSelectedIndex
|
||||
};
|
||||
|
||||
this.handlePointerDownCapture = this.handlePointerDownCapture.bind(this);
|
||||
this.handlePointerDown = this.handlePointerDown.bind(this);
|
||||
this.handlePointerCancel = this.handlePointerCancel.bind(this);
|
||||
this.handlePointerMove = this.handlePointerMove.bind(this);
|
||||
this.handlePointerUp = this.handlePointerUp.bind(this);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.handleAutoScrollTick = this.handleAutoScrollTick.bind(this);
|
||||
this.animationTick = this.animationTick.bind(this);
|
||||
this.longPressTimerTick = this.longPressTimerTick.bind(this);
|
||||
this.handleTouchMove = this.handleTouchMove.bind(this);
|
||||
this.handleTouchEnd = this.handleTouchEnd.bind(this);
|
||||
}
|
||||
|
||||
animationData: AnimationData[] = [];
|
||||
|
||||
|
||||
handleTouchMove(e: any)
|
||||
{
|
||||
if (this.dragStarted)
|
||||
{
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
handleTouchEnd(e: any)
|
||||
{
|
||||
if (this.dragStarted)
|
||||
{
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
animationStartTime: number = 0;
|
||||
timerHandle: number| null = null;
|
||||
|
||||
animationTick() {
|
||||
let t = (new Date().getTime() - this.animationStartTime) / ANIMATION_DURATION_MS;
|
||||
if (t > 1) t = 1;
|
||||
|
||||
// easing function.
|
||||
//t = 1-(1-t)*(1-t);
|
||||
let didAnimate: boolean = false;
|
||||
for (let i = 0; i < this.animationData.length; ++i) {
|
||||
let animationData = this.animationData[i];
|
||||
if (animationData.animating)
|
||||
{
|
||||
if (i !== this.startIndex) {
|
||||
//if ((animationData.xFrom !== animationData.xTo) || (animationData.yFrom !== animationData.yTo))
|
||||
let y = t * (animationData.toY - animationData.fromY) + animationData.fromY;
|
||||
let style = animationData.element.style;
|
||||
style.left = "0px";
|
||||
style.top = y-animationData.originalPosition.y + "px";
|
||||
animationData.currentY = y;
|
||||
animationData.animating = animationData.toY !== animationData.fromY;
|
||||
if (animationData.animating) didAnimate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (t < 1 && didAnimate) {
|
||||
this.timerHandle = requestAnimationFrame(this.animationTick)
|
||||
} else {
|
||||
this.timerHandle = null;
|
||||
}
|
||||
|
||||
}
|
||||
startAnimation() {
|
||||
this.animationStartTime = new Date().getTime();
|
||||
|
||||
if (this.timerHandle === null) {
|
||||
|
||||
this.timerHandle = requestAnimationFrame(this.animationTick);
|
||||
this.animationTick();
|
||||
}
|
||||
}
|
||||
stopAnimation() {
|
||||
if (this.timerHandle !== null) {
|
||||
cancelAnimationFrame(this.timerHandle);
|
||||
}
|
||||
this.timerHandle = null;
|
||||
}
|
||||
|
||||
isValidPointer(e: PointerEvent<HTMLDivElement>): boolean
|
||||
{
|
||||
if (e.pointerType === "mouse") {
|
||||
return e.button === 0;
|
||||
} else if (e.pointerType === "pen") {
|
||||
return true;
|
||||
} else if (e.pointerType === "touch") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bringSelectedItemIntoView() {
|
||||
if (this.state.indexToSelect && this.state.indexToSelect >= 0)
|
||||
{
|
||||
let grid = this.refGrid.current;
|
||||
if (grid)
|
||||
{
|
||||
if (this.state.indexToSelect >= 0 && this.state.indexToSelect < grid.childNodes.length)
|
||||
{
|
||||
let element = grid.childNodes[this.state.indexToSelect] as HTMLElement;
|
||||
element.scrollIntoView({
|
||||
block: "nearest",
|
||||
inline: "nearest"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
this.bringSelectedItemIntoView();
|
||||
// this.refGrid.current!.addEventListener("touchstart",(e)=> this.handleTouchStart(e),{passive: false});
|
||||
}
|
||||
componentWillUnmount()
|
||||
{
|
||||
//this.refGrid.current!.removeEventListener("touchstart",(e) => this.handleTouchStart(e));
|
||||
this.stopAnimation();
|
||||
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (!this.mouseDown)
|
||||
{
|
||||
this.bringSelectedItemIntoView();
|
||||
}
|
||||
if (this.mouseDown)
|
||||
{
|
||||
this.reprepareChildren();
|
||||
this.reflowChildren();
|
||||
this.stopAnimation();
|
||||
this.animationTick();
|
||||
}
|
||||
}
|
||||
|
||||
mouseDown: boolean = false;
|
||||
pointerId: number = 0;
|
||||
pointerType: string = "";
|
||||
|
||||
lastOnDragTime: number = 0;
|
||||
|
||||
onClick(e: MouseEvent<HTMLDivElement>) {
|
||||
// if the click event immediately follows drag end, suppress it.
|
||||
let dt = new Date().getTime() - this.lastOnDragTime;
|
||||
if (dt < 150) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
}
|
||||
hasClass(element: HTMLElement, className: string) {
|
||||
let classes = element.classList;
|
||||
for (let i = 0; i < classes.length; ++i) {
|
||||
if (classes[i] === className) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
findGridItem(element: any | null): HTMLDivElement | null {
|
||||
let grid = this.refGrid.current;
|
||||
if(!grid) return null;
|
||||
while (element !== null && element !== grid) {
|
||||
if (element.parentElement === grid) {
|
||||
return element as HTMLDivElement;
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
isCapturedPointer(e: PointerEvent<HTMLDivElement>): boolean {
|
||||
return this.mouseDown
|
||||
&& e.pointerId === this.pointerId
|
||||
&& e.pointerType === this.pointerType;
|
||||
|
||||
}
|
||||
|
||||
startX: number = 0;
|
||||
startY: number = 0;
|
||||
startClientX: number = 0;
|
||||
startClientY: number = 0;
|
||||
dragStarted: boolean = false;
|
||||
savedIndex: string = "";
|
||||
savedBackground: string = "";
|
||||
lastPointerDown: number = 0;
|
||||
|
||||
ptItemStart: DOMPoint = new DOMPoint(0, 0);
|
||||
captureElement?: HTMLDivElement;
|
||||
|
||||
handlePointerDownCapture(e: any) {
|
||||
// a new pointer down of any type cancels capture
|
||||
this.cancelDrag();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
|
||||
|
||||
cancelDrag() {
|
||||
//window.removeEventListener("touchstart",this.handleTouchStart);
|
||||
window.removeEventListener("touchmove",this.handleTouchMove);
|
||||
window.removeEventListener("touchend",this.handleTouchEnd);
|
||||
|
||||
if (this.originalOverscrollBehaviourY)
|
||||
{
|
||||
this.getScrollContainer()!.style.setProperty("overscroll-behaviour-y",this.originalOverscrollBehaviourY);
|
||||
this.originalOverscrollBehaviourY = undefined;
|
||||
}
|
||||
|
||||
if (this.longPressTimer) {
|
||||
clearTimeout(this.longPressTimer);
|
||||
this.longPressTimer = undefined;
|
||||
}
|
||||
this.stopAnimation();
|
||||
this.stopAutoScroll();
|
||||
if (this.dragStarted && this.captureElement) {
|
||||
try {
|
||||
this.captureElement.releasePointerCapture(this.pointerId);
|
||||
} catch (error)
|
||||
{
|
||||
// throws if we've already lost it.
|
||||
}
|
||||
|
||||
}
|
||||
this.dragStarted = false;
|
||||
if (this.dragTarget) {
|
||||
this.clearDragTransform(this.dragTarget);
|
||||
this.dragTarget = null;
|
||||
}
|
||||
this.captureElement = undefined;
|
||||
this.mouseDown = false;
|
||||
this.unprepareChildren();
|
||||
|
||||
document.body.removeEventListener("pointerdown", this.handlePointerDownCapture, true);
|
||||
}
|
||||
|
||||
|
||||
prepareChildren() {
|
||||
if (this.refGrid.current === null) return;
|
||||
let grid = this.refGrid.current;
|
||||
let nodes = grid.childNodes;
|
||||
let childRects = [];
|
||||
|
||||
let gridClientRect = grid.getBoundingClientRect();
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
let child = nodes[i] as HTMLDivElement;
|
||||
let rect = child.getBoundingClientRect();
|
||||
rect.x -= gridClientRect.left;
|
||||
rect.y -= gridClientRect.top;
|
||||
childRects[i] = rect;
|
||||
}
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
let child = nodes[i] as HTMLDivElement;
|
||||
child.style.position = "relative";
|
||||
child.style.left = "0px";
|
||||
child.style.top = "0px";
|
||||
}
|
||||
this.animationData = [];
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
this.animationData[i] = new AnimationData(nodes[i] as HTMLElement, childRects[i]);
|
||||
}
|
||||
grid.style.height = this.gridBounds.height + "px";
|
||||
grid.style.width = this.gridBounds.width + "px";
|
||||
|
||||
}
|
||||
reprepareChildren()
|
||||
{
|
||||
if (this.refGrid.current === null) return;
|
||||
let grid = this.refGrid.current;
|
||||
let nodes = grid.childNodes;
|
||||
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
let child = nodes[i] as HTMLDivElement;
|
||||
child.style.position = "relative";
|
||||
child.style.left = "0px";
|
||||
child.style.top = "0px";
|
||||
}
|
||||
if (nodes.length !== this.animationData.length)
|
||||
{
|
||||
this.cancelDrag();
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
let key: any = (nodes[i] as any).key;
|
||||
if (nodes[i] as HTMLElement !== this.animationData[i].element) {
|
||||
console.log("Animation failed: Element " + i + " changed.");
|
||||
}
|
||||
if (key !== this.animationData[i].key) {
|
||||
console.log("Animation failed: Element " + i + " key has changed.");
|
||||
}
|
||||
this.animationData[i].element = nodes[i] as HTMLElement;
|
||||
if (i !== this.startIndex)
|
||||
{
|
||||
this.animationData[i].animating = true;
|
||||
}
|
||||
}
|
||||
grid.style.height = this.gridBounds.height + "px";
|
||||
grid.style.width = this.gridBounds.width + "px";
|
||||
this.animationTick();
|
||||
|
||||
|
||||
}
|
||||
|
||||
getIndexFromPosition(clientX: number, clientY: number)
|
||||
{
|
||||
let grid = nullCast(this.refGrid.current);
|
||||
let gridBounds = grid.getBoundingClientRect();
|
||||
clientX -= gridBounds.left;
|
||||
clientY -= gridBounds.top;
|
||||
|
||||
if (clientY < 0) return -1;
|
||||
for (let i = 0; i < this.animationData.length; ++i)
|
||||
{
|
||||
let item = this.animationData[i];
|
||||
if (clientY >= item.originalPosition.x && clientY < item.originalPosition.bottom)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
|
||||
|
||||
}
|
||||
getDragIndexFromPosition(clientX: number, clientY: number): number {
|
||||
|
||||
let grid = nullCast(this.refGrid.current);
|
||||
|
||||
let originalBounds = this.animationData[this.startIndex].originalPosition;
|
||||
// Base test on the CENTER point of the currently dragged item.
|
||||
clientX += originalBounds.width / 2 - this.ptItemStart.x;
|
||||
clientY += originalBounds.height / 2 - this.ptItemStart.y;
|
||||
|
||||
let gridBounds = grid.getBoundingClientRect();
|
||||
|
||||
clientX -= gridBounds.x;
|
||||
clientY -= gridBounds.top;
|
||||
|
||||
// clientX/Y is now the position of the center of the currently-dragged item, in the coordinates of the contenst of the scroll view (scroll applied)
|
||||
|
||||
|
||||
if (clientY < 0) return 0;
|
||||
for (let i = 0; i < this.animationData.length; ++i) {
|
||||
let rect = this.animationData[i].originalPosition;
|
||||
|
||||
if (clientY >= rect.top && clientY < rect.bottom)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return this.animationData.length-1;
|
||||
}
|
||||
|
||||
|
||||
unprepareChildren() {
|
||||
if (this.refGrid.current === null) return;
|
||||
let grid = this.refGrid.current;
|
||||
let nodes = grid.childNodes;
|
||||
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
let child = nodes[i] as HTMLDivElement;
|
||||
child.style.position = "";
|
||||
child.style.top = ""
|
||||
child.style.left = "";
|
||||
child.style.width = "";
|
||||
child.style.height = "";
|
||||
}
|
||||
|
||||
grid.style.height = "";
|
||||
grid.style.width = "100%";
|
||||
|
||||
// releast refs and storage.
|
||||
this.dragTarget = null;
|
||||
this.animationData = [];
|
||||
|
||||
}
|
||||
|
||||
dragTarget: HTMLDivElement | null = null;
|
||||
|
||||
elementKeys: any[] = [];
|
||||
|
||||
|
||||
|
||||
clearDragTransform(element: HTMLDivElement): void {
|
||||
element.style.transform = "";
|
||||
element.style.zIndex = this.savedIndex;
|
||||
element.style.background = this.savedBackground;
|
||||
element.style.opacity = "1";
|
||||
|
||||
}
|
||||
|
||||
handlePointerCancel(e: PointerEvent<HTMLDivElement>) {
|
||||
if (this.isCapturedPointer(e)) {
|
||||
this.cancelDrag();
|
||||
if (this.props.onDragCancel) {
|
||||
this.props.onDragCancel(e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handlePointerUp(e: PointerEvent<HTMLDivElement>) {
|
||||
if (this.isCapturedPointer(e)) {
|
||||
|
||||
this.assertKeysNotChanged(this.elementKeys); // make sure that a late render hasn't swapped elements while we're dragging.
|
||||
|
||||
this.handlePointerMove(e);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (this.dragStarted) {
|
||||
this.lastOnDragTime = new Date().getTime();
|
||||
if (this.props.onDragEnd) {
|
||||
this.props.onDragEnd(
|
||||
this.startIndex, this.currentIndex,
|
||||
e.clientX, e.clientY);
|
||||
}
|
||||
if (this.startIndex !== this.currentIndex) {
|
||||
this.props.moveElement(this.startIndex, this.currentIndex);
|
||||
}
|
||||
}
|
||||
this.cancelDrag();
|
||||
}
|
||||
}
|
||||
|
||||
dragThresholdExceeded(e: PointerEvent<HTMLDivElement>) {
|
||||
if (this.pointerType === "touch")
|
||||
{
|
||||
let timeMs = new Date().getTime();
|
||||
let dt = timeMs -this.lastPointerDown;
|
||||
if (dt < LONG_PRESS_TIME_MS) return false;
|
||||
}
|
||||
|
||||
let dx = e.clientX - this.startX;
|
||||
let dy = e.clientY - this.startY;
|
||||
let d2 = dx * dx + dy * dy;
|
||||
|
||||
let DRAG_THRESHOLD = 5;
|
||||
if (this.pointerType === "touch")
|
||||
{
|
||||
DRAG_THRESHOLD = 15;
|
||||
}
|
||||
return (d2 > DRAG_THRESHOLD * DRAG_THRESHOLD);
|
||||
}
|
||||
|
||||
lastX: number = 0;
|
||||
lastY: number = 0;
|
||||
|
||||
translateViewPortPoint(fromElement: HTMLElement, toElement: HTMLElement, pt: Point): Point {
|
||||
let rcFrom = fromElement.getBoundingClientRect();
|
||||
let rcTo = toElement.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
x: pt.x + rcFrom.left + fromElement.scrollLeft - rcTo.left - toElement.scrollLeft,
|
||||
y: pt.y + rcFrom.top + fromElement.scrollTop - rcTo.top - toElement.scrollTop
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
makeTransform(targetElement: HTMLElement, clientX: number, clientY: number): string {
|
||||
|
||||
// the bounds of the translated element must COMPLETELY fit within the scroll range of the container.
|
||||
let originalBounds = this.animationData[this.startIndex].originalPosition;
|
||||
|
||||
let newLocation = new DOMRect(
|
||||
originalBounds.x + clientX - this.startX,
|
||||
originalBounds.y + clientY - this.startY,
|
||||
originalBounds.width,
|
||||
originalBounds.height);
|
||||
|
||||
|
||||
if (newLocation.x < 0) {
|
||||
newLocation.x = 0;
|
||||
} else if (newLocation.right >= this.gridBounds.width) {
|
||||
newLocation.x -= newLocation.right - this.gridBounds.width;
|
||||
}
|
||||
|
||||
// chrome: inconsistent scroll height calculations.
|
||||
let maxY = this.gridBounds.height;
|
||||
if (newLocation.top < 0) {
|
||||
newLocation.y = 0;
|
||||
} else if (newLocation.bottom > maxY) {
|
||||
newLocation.y -= newLocation.bottom - maxY;
|
||||
}
|
||||
|
||||
return "translate(" + (newLocation.x - originalBounds.x) + "px,"
|
||||
+ (newLocation.y - originalBounds.y) + "px) scale(" + SELECT_SCALE + ")";
|
||||
}
|
||||
|
||||
createOriginalBounds(): DOMRect {
|
||||
// bounds of the target element, in ScrollContainer coordinates.
|
||||
let rcTarget = nullCast(this.dragTarget).getBoundingClientRect();
|
||||
|
||||
let rcScroll = nullCast(this.getScrollContainer()).getBoundingClientRect();
|
||||
rcTarget.x -= rcScroll.x;
|
||||
rcTarget.y -= rcScroll.y;
|
||||
return rcTarget;
|
||||
}
|
||||
|
||||
startIndex: number = 0;
|
||||
currentIndex: number = 0;
|
||||
|
||||
|
||||
reflowChildren(): void {
|
||||
|
||||
let y = 0;
|
||||
for (let i = 0; i < this.animationData.length; ++i) {
|
||||
if (i === this.startIndex) {
|
||||
if (i === this.currentIndex) {
|
||||
y += this.animationData[this.startIndex].originalPosition.height;
|
||||
}
|
||||
|
||||
} else {
|
||||
if (i === this.currentIndex && this.currentIndex < this.startIndex)
|
||||
{
|
||||
y += this.animationData[this.startIndex].originalPosition.height;
|
||||
}
|
||||
let animationItem = this.animationData[i];
|
||||
animationItem.setPosition(y);
|
||||
y += animationItem.originalPosition.height;
|
||||
if (i === this.currentIndex && this.currentIndex > this.startIndex)
|
||||
{
|
||||
y += this.animationData[this.startIndex].originalPosition.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.stopAnimation();
|
||||
this.animationStartTime = new Date().getTime();
|
||||
this.animationTick();
|
||||
}
|
||||
|
||||
itemOffsetX: number = 0;
|
||||
itemOffsetY: number = 0;
|
||||
longPressTimer?: NodeJS.Timeout = undefined;
|
||||
|
||||
|
||||
longPressTimerTick() {
|
||||
if (navigator.vibrate)
|
||||
{
|
||||
navigator.vibrate([5]);
|
||||
}
|
||||
if (!this.dragStarted)
|
||||
{
|
||||
this.startDrag();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handlePointerDown(e: PointerEvent<HTMLDivElement>): void {
|
||||
|
||||
if (this.props.canDrag !== undefined && !this.props.canDrag) return;
|
||||
|
||||
// any new pointer down cancels a drag in progress.
|
||||
this.cancelDrag();
|
||||
|
||||
// avoid 2nd click of a double click (which is difficult to cancel)
|
||||
let timeMs = new Date().getTime();
|
||||
if (timeMs - this.lastPointerDown < 300) {
|
||||
this.lastPointerDown = timeMs;
|
||||
return;
|
||||
}
|
||||
this.lastPointerDown = timeMs;
|
||||
|
||||
|
||||
let gridElement = this.findGridItem(e.target);
|
||||
if (gridElement === null) return;
|
||||
|
||||
|
||||
|
||||
if (!this.mouseDown && this.isValidPointer(e)) {
|
||||
|
||||
this.dragTarget = gridElement;
|
||||
|
||||
|
||||
document.body.addEventListener("pointerdown", this.handlePointerDownCapture, true);
|
||||
this.captureElement = e.currentTarget;
|
||||
|
||||
this.elementKeys = this.getElementKeys();
|
||||
this.longPressTimer = setTimeout(this.longPressTimerTick,LONG_PRESS_TIME_MS);
|
||||
this.mouseDown = true;
|
||||
|
||||
///window.addEventListener("touchstart",this.handleTouchStart, {passive: false});
|
||||
|
||||
this.dragStarted = false;
|
||||
this.startX = e.clientX;
|
||||
this.startY = e.clientY;
|
||||
this.startClientX = e.clientX;
|
||||
this.startClientY = e.clientY;
|
||||
|
||||
this.pointerId = e.pointerId;
|
||||
this.pointerType = e.pointerType;
|
||||
|
||||
this.savedIndex = gridElement.style.zIndex;
|
||||
gridElement.style.zIndex = "3";
|
||||
this.savedBackground = gridElement.style.background;
|
||||
gridElement.style.background = "#EEE";
|
||||
gridElement.style.opacity = "0.8";
|
||||
}
|
||||
}
|
||||
|
||||
originalOverscrollBehaviourY?: string;
|
||||
|
||||
gridBounds: DOMRect = new DOMRect();
|
||||
|
||||
startDrag() {
|
||||
this.dragStarted = true;
|
||||
let grid = this.refGrid.current!;
|
||||
this.gridBounds = grid.getBoundingClientRect();
|
||||
|
||||
window.addEventListener("touchmove",this.handleTouchMove, {passive: false});
|
||||
window.addEventListener("touchend",this.handleTouchEnd, {passive: false});
|
||||
|
||||
this.prepareChildren();
|
||||
|
||||
this.startIndex = this.getIndexFromPosition(this.startX, this.startY);
|
||||
if (this.startIndex === -1)
|
||||
{
|
||||
this.cancelDrag();
|
||||
return;
|
||||
}
|
||||
let t = grid.childNodes[this.startIndex];
|
||||
if (t !== this.dragTarget) {
|
||||
console.count("dragTarget mismatch!");
|
||||
this.startIndex = this.getIndexFromPosition(this.startX,this.startY);
|
||||
this.dragTarget = grid.childNodes[this.startIndex] as HTMLDivElement;
|
||||
}
|
||||
this.currentIndex = this.startIndex;
|
||||
if (this.props.onDragStart) {
|
||||
this.props.onDragStart(this.startIndex, this.startX, this.startY);
|
||||
}
|
||||
this.dragTarget!.setPointerCapture(this.pointerId);
|
||||
|
||||
let rcItem = this.dragTarget!.getBoundingClientRect();
|
||||
this.ptItemStart = new DOMPoint(this.startX - rcItem.x, this.startY - rcItem.y);
|
||||
|
||||
this.originalOverscrollBehaviourY = this.getScrollContainer()!.style.getPropertyValue("overscroll-behaviour-y");
|
||||
this.getScrollContainer()!.style.setProperty("overscroll-behaviour-y","none");
|
||||
|
||||
if (this.pointerType === "touch")
|
||||
{
|
||||
this.startY -= 4; // causes the selected item to "pop"
|
||||
}
|
||||
}
|
||||
|
||||
handlePointerMove(e: PointerEvent<HTMLDivElement>): void {
|
||||
if (this.isCapturedPointer(e)) {
|
||||
if (!this.dragStarted && this.dragThresholdExceeded(e)) {
|
||||
this.startDrag();
|
||||
}
|
||||
if (this.dragStarted) {
|
||||
this.lastX = e.clientX;
|
||||
this.lastY = e.clientY;
|
||||
if (this.dragTarget !== null) {
|
||||
this.dragTarget.style.transform = this.makeTransform(this.dragTarget, e.clientX, e.clientY);
|
||||
|
||||
let index = this.getDragIndexFromPosition(e.clientX, e.clientY);
|
||||
if (index !== this.currentIndex) {
|
||||
this.currentIndex = index;
|
||||
this.reflowChildren();
|
||||
}
|
||||
}
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.onDragMove) {
|
||||
this.props.onDragMove(this.currentIndex, e.clientX, e.clientY);
|
||||
}
|
||||
|
||||
this.checkForAutoScroll(e.currentTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
autoScrollTimer?: NodeJS.Timeout;
|
||||
|
||||
|
||||
getElementKeys(): any[] {
|
||||
let grid = nullCast(this.refGrid.current);
|
||||
let children = grid.childNodes;
|
||||
let result: any[] = [];
|
||||
for (let i = 0; i < children.length; ++i)
|
||||
{
|
||||
result[i] = (children[i] as any).key;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
assertKeysNotChanged(oldKeys: any[])
|
||||
{
|
||||
let newKeys = this.getElementKeys();
|
||||
for (let i = 0; i < newKeys.length; ++i)
|
||||
{
|
||||
if (newKeys[i] !== oldKeys[i])
|
||||
{
|
||||
throw new Error("Keys have changed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleAutoScrollTick(currentTarget: HTMLElement, dx: number, dy: number) {
|
||||
if (!this.mouseDown) return;
|
||||
let scrollContainer = this.getScrollContainer();
|
||||
if (scrollContainer) {
|
||||
let tx = scrollContainer.scrollLeft;
|
||||
let ty = scrollContainer.scrollTop;
|
||||
|
||||
let x = scrollContainer.scrollLeft + dx;
|
||||
let y = scrollContainer.scrollTop + dy;
|
||||
x = Math.min(x,scrollContainer.scrollWidth-scrollContainer.clientWidth);
|
||||
y = Math.min(y,scrollContainer.scrollHeight-scrollContainer.clientHeight);
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
|
||||
scrollContainer.scrollTo(x, y);
|
||||
this.startX -= (scrollContainer.scrollLeft - tx);
|
||||
this.startY -= (scrollContainer.scrollTop - ty);
|
||||
|
||||
if (this.dragTarget) {
|
||||
this.dragTarget.style.transform = this.makeTransform(this.dragTarget, this.lastX, this.lastY);
|
||||
}
|
||||
|
||||
this.checkForAutoScroll(currentTarget);
|
||||
}
|
||||
}
|
||||
stopAutoScroll() {
|
||||
if (this.autoScrollTimer !== undefined) {
|
||||
clearTimeout(this.autoScrollTimer);
|
||||
this.autoScrollTimer = undefined;
|
||||
}
|
||||
}
|
||||
startAutoScroll(scrollContainer: HTMLElement, dx: number, dy: number) {
|
||||
this.stopAutoScroll();
|
||||
|
||||
this.autoScrollTimer = setTimeout(
|
||||
() => { this.handleAutoScrollTick(scrollContainer, dx, dy) }, AUTOSCROLL_TICK_DELAY);
|
||||
}
|
||||
|
||||
checkForAutoScroll(currentTarget: HTMLElement) {
|
||||
let scrollContainer = this.getScrollContainer();
|
||||
if (!scrollContainer) {
|
||||
this.stopAutoScroll();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let scrollContainerRect = scrollContainer.getBoundingClientRect();
|
||||
|
||||
let dy: number = 0;
|
||||
let dx: number = 0;
|
||||
|
||||
if (this.lastX < scrollContainerRect.x + AUTOSCROLL_THRESHOLD) {
|
||||
dx = -AUTOSCROLL_SCROLL_RATE;
|
||||
if (dx < -scrollContainer.scrollLeft) {
|
||||
dx = -scrollContainer.scrollLeft;
|
||||
}
|
||||
} else if (this.lastX >= scrollContainerRect.right - AUTOSCROLL_THRESHOLD) {
|
||||
dx = AUTOSCROLL_SCROLL_RATE;
|
||||
let maxScroll = Math.max(scrollContainer.scrollWidth - scrollContainer.clientWidth, 0);
|
||||
if (dx > maxScroll) dx = maxScroll;
|
||||
}
|
||||
if (this.lastY < scrollContainerRect.top) {
|
||||
dy = -AUTOSCROLL_SCROLL_RATE;
|
||||
if (dy < -scrollContainer.scrollTop) dy = -scrollContainer.scrollTop;
|
||||
} else if (this.lastY >= scrollContainerRect.bottom - AUTOSCROLL_THRESHOLD) {
|
||||
dy = AUTOSCROLL_SCROLL_RATE;
|
||||
let maxScroll = Math.max(scrollContainer.scrollHeight - scrollContainer.clientHeight);
|
||||
if (dy > maxScroll) dy = maxScroll;
|
||||
}
|
||||
|
||||
if (dx === 0 && dy === 0) {
|
||||
this.stopAutoScroll();
|
||||
return;
|
||||
} else {
|
||||
this.startAutoScroll(currentTarget, dx, dy);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getScrollContainer(): HTMLDivElement | null {
|
||||
return this.refScrollContainer.current;
|
||||
}
|
||||
|
||||
overflowX(): Property.OverflowX {
|
||||
return this.props.scroll === ScrollDirection.X ? "auto" : "hidden";
|
||||
}
|
||||
overflowY(): Property.OverflowX {
|
||||
return this.props.scroll === ScrollDirection.Y ? "auto" : "hidden";
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
transform: "",
|
||||
overflowX: this.overflowX(),
|
||||
overflowY: this.overflowY(),
|
||||
width: this.props.fullWidth??true? "100%": "auto",
|
||||
height: this.props.fullHeight??true? "100%": "auto",
|
||||
}}
|
||||
onPointerDown={this.handlePointerDown}
|
||||
onPointerMove={this.handlePointerMove}
|
||||
onPointerCancel={this.handlePointerCancel}
|
||||
onPointerCancelCapture={this.handlePointerCancel}
|
||||
onPointerUp={this.handlePointerUp}
|
||||
onDragStart={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||
onClick={this.onClick}
|
||||
ref={this.refScrollContainer}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", position: "relative" }}
|
||||
ref={this.refGrid}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
export default DraggableGrid;
|
||||
Reference in New Issue
Block a user