UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

189 lines (188 loc) 9.54 kB
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)))); });