UNPKG

@paperbits/common

Version:
253 lines (216 loc) 11.1 kB
import { EventManager, Events } from "@paperbits/common/events"; import { ResizableOptions } from "@paperbits/common/ui/resizableOptions"; import { BehaviorHandle } from "./behavior"; export class ResizableBehavior { public static attach(element: HTMLElement, config: string | ResizableOptions, eventManager: EventManager): BehaviorHandle { let directions: string; let onResizeCallback: (() => void) | undefined; let initialWidthOption: number | undefined; let initialHeightOption: number | undefined; let initialEdge: string; let initialOffsetX: number, initialOffsetY: number; let currentElementWidth: number, currentElementHeight: number; // Renamed from initialWidth/Height to avoid confusion let initialLeft: number, initialRight: number, initialTop: number, initialBottom: number; let resizing = false; const resizeHandles: HTMLElement[] = []; const handleEventListeners: { element: HTMLElement, type: string, listener: (e: any) => void }[] = []; // Store references to pointerMove and pointerUp handlers for add/removeEventListener let pointerMoveHandlerRef: ((event: MouseEvent) => void) | null = null; let pointerUpHandlerRef: ((event: MouseEvent) => void) | null = null; const setOptions = (currentConfig: string | ResizableOptions) => { if (typeof currentConfig === "string") { directions = currentConfig; onResizeCallback = undefined; initialWidthOption = undefined; initialHeightOption = undefined; } else { directions = currentConfig.directions; onResizeCallback = currentConfig.onresize; initialWidthOption = currentConfig.initialWidth; initialHeightOption = currentConfig.initialHeight; if (initialWidthOption !== undefined) { element.style.width = initialWidthOption + "px"; } if (initialHeightOption !== undefined) { element.style.height = initialHeightOption + "px"; } } if (directions.includes("suspended")) { element.classList.add("resize-suspended"); element.style.width = ""; element.style.height = ""; } else { element.classList.remove("resize-suspended"); } }; setOptions(config); const style = window.getComputedStyle(element); const minWidthNum = parseFloat(style.minWidth) || 0; const minHeightNum = parseFloat(style.minHeight) || 0; const onPointerMove = (event: MouseEvent): void => { if (!resizing) { return; } let newWidthPx: string | undefined; let newHeightPx: string | undefined; switch (initialEdge) { case "left": let newWidthLeft = currentElementWidth + (initialOffsetX - event.clientX); if (newWidthLeft < minWidthNum) { newWidthLeft = minWidthNum; } newWidthPx = newWidthLeft + "px"; element.style.width = newWidthPx; element.classList.add("resized-horizontally"); break; case "right": let newWidthRight = currentElementWidth + (event.clientX - initialOffsetX); if (newWidthRight < minWidthNum) { newWidthRight = minWidthNum; } newWidthPx = newWidthRight + "px"; element.style.width = newWidthPx; element.classList.add("resized-horizontally"); break; case "top": let newHeightTop = currentElementHeight + (initialOffsetY - event.clientY); if (newHeightTop < minHeightNum) { newHeightTop = minHeightNum; } newHeightPx = newHeightTop + "px"; element.style.height = newHeightPx; element.classList.add("resized-vertically"); break; case "bottom": let newHeightBottom = currentElementHeight + (event.clientY - initialOffsetY); if (newHeightBottom < minHeightNum) { newHeightBottom = minHeightNum; } newHeightPx = newHeightBottom + "px"; element.style.height = newHeightPx; element.classList.add("resized-vertically"); break; default: // Should not happen console.error(`Unexpected resizing initial edge: "${initialEdge}".`); return; } eventManager.dispatchEvent("onEditorResize"); }; const onPointerUp = (event: MouseEvent): void => { resizing = false; if (pointerMoveHandlerRef) { eventManager.removeEventListener("onPointerMove", pointerMoveHandlerRef); } if (pointerUpHandlerRef) { eventManager.removeEventListener("onPointerUp", pointerUpHandlerRef); } pointerMoveHandlerRef = null; pointerUpHandlerRef = null; // Note: Original position style (fixed) is not reverted here, matching original behavior. // Classes "resized-horizontally"/"resized-vertically" also remain. if (onResizeCallback) { onResizeCallback(); } }; pointerMoveHandlerRef = onPointerMove; pointerUpHandlerRef = onPointerUp; const onPointerDown = (event: MouseEvent, edge: string): void => { if (directions === "none" || directions.includes("suspended") || resizing) { return; } event.preventDefault(); event.stopImmediatePropagation(); if (pointerMoveHandlerRef && pointerUpHandlerRef) { eventManager.addEventListener("onPointerMove", pointerMoveHandlerRef); eventManager.addEventListener("onPointerUp", pointerUpHandlerRef); } resizing = true; const elementRect = element.getBoundingClientRect(); initialEdge = edge; initialOffsetX = event.clientX; initialOffsetY = event.clientY; currentElementWidth = elementRect.width; currentElementHeight = elementRect.height; initialLeft = elementRect.left; initialRight = elementRect.right; initialTop = elementRect.top; initialBottom = elementRect.bottom; const bodyWidth = element.ownerDocument.body.clientWidth; const bodyHeight = element.ownerDocument.body.clientHeight; element.style.position = "fixed"; // This is a key part of the original behavior switch (initialEdge) { case "left": element.style.left = "unset"; element.style.right = (bodyWidth - initialRight) + "px"; element.style.width = currentElementWidth + "px"; break; case "right": element.style.right = "unset"; element.style.left = initialLeft + "px"; element.style.width = currentElementWidth + "px"; break; case "top": element.style.top = "unset"; element.style.bottom = (bodyHeight - initialBottom) + "px"; element.style.height = currentElementHeight + "px"; break; case "bottom": element.style.bottom = "unset"; element.style.top = initialTop + "px"; element.style.height = currentElementHeight + "px"; break; } }; const createHandle = (edge: string, className: string): void => { const handle = element.ownerDocument.createElement("div"); handle.classList.add("resize-handle", className); element.appendChild(handle); resizeHandles.push(handle); const onMouseDownListener = (e: MouseEvent) => onPointerDown(e, edge); handle.addEventListener(Events.MouseDown, onMouseDownListener); // Events.MouseDown is typically "mousedown" handleEventListeners.push({ element: handle, type: Events.MouseDown, listener: onMouseDownListener }); }; if (!directions.includes("suspended")) { if (directions.includes("top") || directions.includes("vertically")) { createHandle("top", "resize-handle-top"); } if (directions.includes("bottom") || directions.includes("vertically")) { createHandle("bottom", "resize-handle-bottom"); } if (directions.includes("left") || directions.includes("horizontally")) { createHandle("left", "resize-handle-left"); } if (directions.includes("right") || directions.includes("horizontally")) { createHandle("right", "resize-handle-right"); } } const detach = () => { if (resizing) { // If dispose is called mid-resize if (pointerMoveHandlerRef) { eventManager.removeEventListener("onPointerMove", pointerMoveHandlerRef); } if (pointerUpHandlerRef) { eventManager.removeEventListener("onPointerUp", pointerUpHandlerRef); } } pointerMoveHandlerRef = null; pointerUpHandlerRef = null; handleEventListeners.forEach(entry => { entry.element.removeEventListener(entry.type, entry.listener); }); handleEventListeners.length = 0; resizeHandles.forEach(handle => { if (handle.parentElement) { handle.parentElement.removeChild(handle); } }); resizeHandles.length = 0; // Reset suspended class if it was added element.classList.remove("resize-suspended"); // Note: other styles like position:fixed, width/height are not reset by dispose, // matching the original binding's behavior where KO's node disposal handles element cleanup. }; return { detach: detach }; } }