@paperbits/common
Version:
Paperbits common components.
253 lines (216 loc) • 11.1 kB
text/typescript
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 };
}
}