UNPKG

react-resizable-panels

Version:

React components for resizable panel groups/layouts

264 lines (212 loc) 7.31 kB
import { Direction, ResizeEvent } from "./types"; import { resetGlobalCursorStyle, setGlobalCursorStyle } from "./utils/cursor"; import { getResizeEventCoordinates } from "./utils/events/getResizeEventCoordinates"; import { getInputType } from "./utils/getInputType"; export type ResizeHandlerAction = "down" | "move" | "up"; export type ResizeHandlerState = "drag" | "hover" | "inactive"; export type SetResizeHandlerState = ( action: ResizeHandlerAction, state: ResizeHandlerState, event: ResizeEvent ) => void; export type PointerHitAreaMargins = { coarse: number; fine: number; }; export type ResizeHandlerData = { direction: Direction; element: HTMLElement; hitAreaMargins: PointerHitAreaMargins; setResizeHandlerState: SetResizeHandlerState; }; export const EXCEEDED_HORIZONTAL_MIN = 0b0001; export const EXCEEDED_HORIZONTAL_MAX = 0b0010; export const EXCEEDED_VERTICAL_MIN = 0b0100; export const EXCEEDED_VERTICAL_MAX = 0b1000; const isCoarsePointer = getInputType() === "coarse"; let intersectingHandles: ResizeHandlerData[] = []; let isPointerDown = false; let ownerDocumentCounts: Map<Document, number> = new Map(); let panelConstraintFlags: Map<string, number> = new Map(); const registeredResizeHandlers = new Set<ResizeHandlerData>(); export function registerResizeHandle( resizeHandleId: string, element: HTMLElement, direction: Direction, hitAreaMargins: PointerHitAreaMargins, setResizeHandlerState: SetResizeHandlerState ) { const { ownerDocument } = element; const data: ResizeHandlerData = { direction, element, hitAreaMargins, setResizeHandlerState, }; const count = ownerDocumentCounts.get(ownerDocument) ?? 0; ownerDocumentCounts.set(ownerDocument, count + 1); registeredResizeHandlers.add(data); updateListeners(); return function unregisterResizeHandle() { panelConstraintFlags.delete(resizeHandleId); registeredResizeHandlers.delete(data); const count = ownerDocumentCounts.get(ownerDocument) ?? 1; ownerDocumentCounts.set(ownerDocument, count - 1); updateListeners(); if (count === 1) { ownerDocumentCounts.delete(ownerDocument); } }; } function handlePointerDown(event: ResizeEvent) { const { x, y } = getResizeEventCoordinates(event); isPointerDown = true; recalculateIntersectingHandles({ x, y }); updateListeners(); if (intersectingHandles.length > 0) { updateResizeHandlerStates("down", event); event.preventDefault(); } } function handlePointerMove(event: ResizeEvent) { const { x, y } = getResizeEventCoordinates(event); if (isPointerDown) { intersectingHandles.forEach((data) => { const { setResizeHandlerState } = data; setResizeHandlerState("move", "drag", event); }); // Update cursor based on return value(s) from active handles updateCursor(); } else { recalculateIntersectingHandles({ x, y }); updateResizeHandlerStates("move", event); updateCursor(); } if (intersectingHandles.length > 0) { event.preventDefault(); } } function handlePointerUp(event: ResizeEvent) { const { x, y } = getResizeEventCoordinates(event); panelConstraintFlags.clear(); isPointerDown = false; if (intersectingHandles.length > 0) { event.preventDefault(); } recalculateIntersectingHandles({ x, y }); updateResizeHandlerStates("up", event); updateCursor(); updateListeners(); } function recalculateIntersectingHandles({ x, y }: { x: number; y: number }) { intersectingHandles.splice(0); registeredResizeHandlers.forEach((data) => { const { element, hitAreaMargins } = data; const { bottom, left, right, top } = element.getBoundingClientRect(); const margin = isCoarsePointer ? hitAreaMargins.coarse : hitAreaMargins.fine; const intersects = x >= left - margin && x <= right + margin && y >= top - margin && y <= bottom + margin; if (intersects) { intersectingHandles.push(data); } }); } export function reportConstraintsViolation( resizeHandleId: string, flag: number ) { panelConstraintFlags.set(resizeHandleId, flag); } function updateCursor() { let intersectsHorizontal = false; let intersectsVertical = false; intersectingHandles.forEach((data) => { const { direction } = data; if (direction === "horizontal") { intersectsHorizontal = true; } else { intersectsVertical = true; } }); let constraintFlags = 0; panelConstraintFlags.forEach((flag) => { constraintFlags |= flag; }); if (intersectsHorizontal && intersectsVertical) { setGlobalCursorStyle("intersection", constraintFlags); } else if (intersectsHorizontal) { setGlobalCursorStyle("horizontal", constraintFlags); } else if (intersectsVertical) { setGlobalCursorStyle("vertical", constraintFlags); } else { resetGlobalCursorStyle(); } } function updateListeners() { ownerDocumentCounts.forEach((_, ownerDocument) => { const { body } = ownerDocument; body.removeEventListener("contextmenu", handlePointerUp); body.removeEventListener("mousedown", handlePointerDown); body.removeEventListener("mouseleave", handlePointerMove); body.removeEventListener("mousemove", handlePointerMove); body.removeEventListener("touchmove", handlePointerMove); body.removeEventListener("touchstart", handlePointerDown); }); window.removeEventListener("mouseup", handlePointerUp); window.removeEventListener("touchcancel", handlePointerUp); window.removeEventListener("touchend", handlePointerUp); if (registerResizeHandle.length > 0) { if (isPointerDown) { if (intersectingHandles.length > 0) { ownerDocumentCounts.forEach((count, ownerDocument) => { const { body } = ownerDocument; if (count > 0) { body.addEventListener("contextmenu", handlePointerUp); body.addEventListener("mouseleave", handlePointerMove); body.addEventListener("mousemove", handlePointerMove); body.addEventListener("touchmove", handlePointerMove, { passive: false, }); } }); } window.addEventListener("mouseup", handlePointerUp); window.addEventListener("touchcancel", handlePointerUp); window.addEventListener("touchend", handlePointerUp); } else { ownerDocumentCounts.forEach((count, ownerDocument) => { const { body } = ownerDocument; if (count > 0) { body.addEventListener("mousedown", handlePointerDown); body.addEventListener("mousemove", handlePointerMove); body.addEventListener("touchmove", handlePointerMove, { passive: false, }); body.addEventListener("touchstart", handlePointerDown); } }); } } } function updateResizeHandlerStates( action: ResizeHandlerAction, event: ResizeEvent ) { registeredResizeHandlers.forEach((data) => { const { setResizeHandlerState } = data; if (intersectingHandles.includes(data)) { if (isPointerDown) { setResizeHandlerState(action, "drag", event); } else { setResizeHandlerState(action, "hover", event); } } else { setResizeHandlerState(action, "inactive", event); } }); }