azure-devops-ui
Version:
React components for building web UI in Azure DevOps
189 lines (188 loc) • 9.54 kB
JavaScript
import * as React from "react";
import { ObservableValue } from '../Core/Observable';
import { Portal } from '../Portal';
import { css, getPointByEventType, Pointer } from "../Util";
import { distance } from "./Position";
/**
* Represents the end result of a drag / drop operation.
*/
export var DragDropEffect;
(function (DragDropEffect) {
/**
* If the drop where to happen at this point, it would be a no-op.
*/
DragDropEffect["none"] = "none";
/**
* The data should be moved from the drag source to the drop target.
*/
DragDropEffect["move"] = "move";
/**
* The data should be copied from the drag source to the drop target.
*/
DragDropEffect["copy"] = "copy";
})(DragDropEffect || (DragDropEffect = {}));
class DragDropManager {
constructor() {
this.onEventCaptured = (event) => {
// Handle the pointerup and pointermove events
const { type } = event;
if (type === "pointermove") {
// For pointermove events, if there is no drag in progress, we need to check to see if the pointer
// has moved far enough to meet our threshold for triggering a drag/drop operation.
if (!this.dragInProgress) {
if (this.potentialDragInProgress) {
const coordinates = getPointByEventType(event);
if (distance(this.initialCoordinates, coordinates) > this.minimumPixelsForDrag) {
// The position of the pointer is far enough away from our threshold to trigger a drag event.
// Fire the dragstart event to give the drag source an opportunity to cancel the operation
dispatchCustomDragEvent("dragstart", this.dragSourceElement, event, this.dataTransfer);
if (this.dataTransfer.effectAllowed === DragDropEffect.none) {
this.potentialDragInProgress = false;
this.endDrag();
}
else {
this.dragInProgress = true;
if (this.operation) {
this.operation.value = { x: coordinates.x, y: coordinates.y };
}
}
event.preventDefault();
}
}
// If there isn't the potential for a drag, that means a consumer has already
// indicated that we should cancel this drag event, so there is no need to continue to
// check anything about this event.
}
else {
// If there is a drag in progress, treat this as a dragover event.
const target = this.getTargetFromEvent(event);
if (target) {
const coordinates = getPointByEventType(event);
if (this.operation) {
this.operation.value = { x: coordinates.x, y: coordinates.y };
}
dispatchCustomDragEvent("dragover", target, event, this.dataTransfer);
event.preventDefault();
}
}
}
else if (type === "pointerup") {
if (this.dragInProgress) {
const target = this.getTargetFromEvent(event);
// Always fire the dragend event when we get a pointerup, if there was a drag in progress.
dispatchCustomDragEvent("dragend", this.dragSourceElement, event, this.dataTransfer);
if (target && this.dataTransfer.dropEffect !== DragDropEffect.none) {
// Only fire a drop event if the dropEffect allows it.
dispatchCustomDragEvent("drop", target, event, this.dataTransfer);
}
}
this.endDrag();
}
};
this.onPointerLeave = (event) => {
// The pointer has left the bounds of the body element, so a drop is not
// viable at this point.
this.dataTransfer.dropEffect = DragDropEffect.none;
};
this.onPointerOut = (event) => {
if (event.target) {
// The pointer has left an element, so we need to set the dropEffect to none.
// The dragover event will fire, giving a new drop target the chance to
// reset the effect.
this.dataTransfer.dropEffect = DragDropEffect.none;
dispatchCustomDragEvent("dragexit", event.target, event, this.dataTransfer);
}
};
this.onPointerOver = (event) => {
if (event.target) {
// The pointer has entered an element, so we need to set the dropEffect to none.
// The dragover event will fire, giving a new drop target the chance to
// reset the effect.
this.dataTransfer.dropEffect = DragDropEffect.none;
dispatchCustomDragEvent("dragenter", event.target, event, this.dataTransfer);
}
};
}
beginDragOperation(event, dataTransfer, minimumPixelsForDrag = 4) {
this.operation = undefined;
// Something (typically a pointdown on a drag source) has indicated that there is the potential
// for a drag operation. If there is a drag operation already in progress, do nothing.
if (!this.dragInProgress) {
// If there is no drag operation in progress, we should set up the event handlers to detect pointer
// operations that could lead us to actually start the drag / drop operation.
if (event.type === "pointerdown") {
this.startDrag(event, minimumPixelsForDrag, dataTransfer);
this.initialCoordinates = {
x: event.clientX,
y: event.clientY
};
Pointer.setCapture(this.onEventCaptured);
document.body.addEventListener("pointerout", this.onPointerOut, true);
document.body.addEventListener("pointerover", this.onPointerOver, true);
document.body.addEventListener("pointerleave", this.onPointerLeave);
this.operation = new ObservableValue({ x: undefined, y: undefined });
}
}
return this.operation;
}
get isDragInProgress() {
return this.dragInProgress;
}
endDrag() {
document.body.removeEventListener("pointerout", this.onPointerOut);
document.body.removeEventListener("pointerover", this.onPointerOver);
document.body.removeEventListener("pointerleave", this.onPointerLeave);
this.dragInProgress = false;
}
getTargetFromEvent(event) {
return event.target;
}
startDrag(event, minimumPixelsForDrag, dataTransfer) {
this.potentialDragInProgress = true;
this.dragSourceElement = event.target;
this.minimumPixelsForDrag = minimumPixelsForDrag;
this.dataTransfer = dataTransfer;
}
}
const dragDropManager = new DragDropManager();
export function beginDragOperation(event, dataTransfer, minimumPixelsForDrag) {
return dragDropManager.beginDragOperation(event, dataTransfer, minimumPixelsForDrag);
}
export function dispatchCustomDragEvent(eventType, target, event, dataTransfer) {
const customEvent = new CustomEvent(eventType, {
bubbles: true,
detail: { dataTransfer: dataTransfer, nativeEvent: event }
});
target.dispatchEvent(customEvent);
return customEvent;
}
export function getDragInProgress() {
return dragDropManager.isDragInProgress;
}
export const DragImage = React.memo((props) => {
const { className, operation, xOffset = 5, yOffset = 5 } = props;
const dragImageRef = React.useRef(null);
const dragImageFrameId = React.useRef(0);
const updatePosition = () => {
cancelAnimationFrame(dragImageFrameId.current);
dragImageFrameId.current = requestAnimationFrame(() => {
var _a;
if (!((_a = dragImageRef.current) === null || _a === void 0 ? void 0 : _a.style) || !operation.value || !operation.value.x || !operation.value.y) {
// either the drag image is not mounted or no DnD coordinates are available => can't update position
return;
}
const xOffsetPx = operation.value.x + xOffset;
const yOffsetPx = operation.value.y + yOffset;
dragImageRef.current.style.transform = `translate3d(${xOffsetPx}px, ${yOffsetPx}px, 0)`;
});
};
React.useEffect(() => {
operation.subscribe(updatePosition);
return () => {
operation.unsubscribe(updatePosition);
};
}, []);
return (React.createElement(React.Fragment, null,
React.createElement(Portal, { className: "bolt-drag-image-portal" },
React.createElement("div", { className: css(className, "bolt-drag-image depth-16 absolute flex-row flex-center scroll-hidden justify-center"), ref: dragImageRef }, props.children))));
});