react-dnd-html5-backend
Version:
HTML5 backend for React DnD
124 lines (110 loc) • 3.6 kB
text/typescript
import type { XYCoord } from 'dnd-core'
import { isFirefox, isSafari } from './BrowserDetector.js'
import { MonotonicInterpolant } from './MonotonicInterpolant.js'
const ELEMENT_NODE = 1
export function getNodeClientOffset(node: Node): XYCoord | null {
const el = node.nodeType === ELEMENT_NODE ? node : node.parentElement
if (!el) {
return null
}
const { top, left } = (el as HTMLElement).getBoundingClientRect()
return { x: left, y: top }
}
export function getEventClientOffset(e: MouseEvent): XYCoord {
return {
x: e.clientX,
y: e.clientY,
}
}
function isImageNode(node: any) {
return (
node.nodeName === 'IMG' &&
(isFirefox() || !document.documentElement?.contains(node))
)
}
function getDragPreviewSize(
isImage: boolean,
dragPreview: any,
sourceWidth: number,
sourceHeight: number,
) {
let dragPreviewWidth = isImage ? dragPreview.width : sourceWidth
let dragPreviewHeight = isImage ? dragPreview.height : sourceHeight
// Work around @2x coordinate discrepancies in browsers
if (isSafari() && isImage) {
dragPreviewHeight /= window.devicePixelRatio
dragPreviewWidth /= window.devicePixelRatio
}
return { dragPreviewWidth, dragPreviewHeight }
}
export function getDragPreviewOffset(
sourceNode: HTMLElement,
dragPreview: HTMLElement,
clientOffset: XYCoord,
anchorPoint: { anchorX: number; anchorY: number },
offsetPoint: { offsetX: number; offsetY: number },
): XYCoord {
// The browsers will use the image intrinsic size under different conditions.
// Firefox only cares if it's an image, but WebKit also wants it to be detached.
const isImage = isImageNode(dragPreview)
const dragPreviewNode = isImage ? sourceNode : dragPreview
const dragPreviewNodeOffsetFromClient = getNodeClientOffset(
dragPreviewNode,
) as XYCoord
const offsetFromDragPreview = {
x: clientOffset.x - dragPreviewNodeOffsetFromClient.x,
y: clientOffset.y - dragPreviewNodeOffsetFromClient.y,
}
const { offsetWidth: sourceWidth, offsetHeight: sourceHeight } = sourceNode
const { anchorX, anchorY } = anchorPoint
const { dragPreviewWidth, dragPreviewHeight } = getDragPreviewSize(
isImage,
dragPreview,
sourceWidth,
sourceHeight,
)
const calculateYOffset = () => {
const interpolantY = new MonotonicInterpolant(
[0, 0.5, 1],
[
// Dock to the top
offsetFromDragPreview.y,
// Align at the center
(offsetFromDragPreview.y / sourceHeight) * dragPreviewHeight,
// Dock to the bottom
offsetFromDragPreview.y + dragPreviewHeight - sourceHeight,
],
)
let y = interpolantY.interpolate(anchorY)
// Work around Safari 8 positioning bug
if (isSafari() && isImage) {
// We'll have to wait for @3x to see if this is entirely correct
y += (window.devicePixelRatio - 1) * dragPreviewHeight
}
return y
}
const calculateXOffset = () => {
// Interpolate coordinates depending on anchor point
// If you know a simpler way to do this, let me know
const interpolantX = new MonotonicInterpolant(
[0, 0.5, 1],
[
// Dock to the left
offsetFromDragPreview.x,
// Align at the center
(offsetFromDragPreview.x / sourceWidth) * dragPreviewWidth,
// Dock to the right
offsetFromDragPreview.x + dragPreviewWidth - sourceWidth,
],
)
return interpolantX.interpolate(anchorX)
}
// Force offsets if specified in the options.
const { offsetX, offsetY } = offsetPoint
const isManualOffsetX = offsetX === 0 || offsetX
const isManualOffsetY = offsetY === 0 || offsetY
return {
x: isManualOffsetX ? offsetX : calculateXOffset(),
y: isManualOffsetY ? offsetY : calculateYOffset(),
}
}