refactor(web): extract timeline live preview helpers
This commit is contained in:
@@ -4,6 +4,14 @@ import { useCallback, useEffect, useRef, useState, type MutableRefObject } from
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
import { useInvalidateTimeline } from "./useInvalidatePlanningViews.js";
|
||||
import { pixelsToDays, computeDragDates } from "~/components/timeline/dragMath.js";
|
||||
import {
|
||||
captureLivePreviewTargets,
|
||||
clearLivePreview,
|
||||
datesMatch,
|
||||
preserveLivePreview,
|
||||
scheduleLivePreview,
|
||||
type LivePreviewSession,
|
||||
} from "./timelineLivePreview.js";
|
||||
|
||||
const DRAG_CLICK_THRESHOLD_PX = 5;
|
||||
|
||||
@@ -104,133 +112,6 @@ const INITIAL_ALLOC_DRAG: AllocDragState = {
|
||||
daysDelta: 0,
|
||||
};
|
||||
|
||||
type LivePreviewMode = AllocDragMode;
|
||||
|
||||
type LivePreviewTarget = {
|
||||
element: HTMLElement;
|
||||
baseLeft: number;
|
||||
baseWidth: number;
|
||||
baseTransform: string;
|
||||
};
|
||||
|
||||
type LivePreviewSession = {
|
||||
mode: LivePreviewMode;
|
||||
cellWidth: number;
|
||||
targets: LivePreviewTarget[];
|
||||
pointerDeltaX: number;
|
||||
daysDelta: number;
|
||||
frame: number | null;
|
||||
};
|
||||
|
||||
function toPxValue(value: string): number {
|
||||
const parsed = Number.parseFloat(value);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function joinTransforms(...parts: Array<string | undefined>): string {
|
||||
return parts
|
||||
.map((part) => part?.trim())
|
||||
.filter((part): part is string => Boolean(part) && part !== "none")
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function captureLivePreviewTargets(elements: Iterable<HTMLElement>): LivePreviewTarget[] {
|
||||
const seen = new Set<HTMLElement>();
|
||||
const targets: LivePreviewTarget[] = [];
|
||||
|
||||
for (const element of elements) {
|
||||
if (seen.has(element)) continue;
|
||||
seen.add(element);
|
||||
targets.push({
|
||||
element,
|
||||
baseLeft: toPxValue(element.style.left),
|
||||
baseWidth: toPxValue(element.style.width),
|
||||
baseTransform: element.style.transform,
|
||||
});
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
function renderLivePreview(session: LivePreviewSession) {
|
||||
const pointerOffsetX = session.pointerDeltaX - session.daysDelta * session.cellWidth;
|
||||
|
||||
for (const target of session.targets) {
|
||||
let left = target.baseLeft;
|
||||
let width = target.baseWidth;
|
||||
|
||||
if (session.mode === "move") {
|
||||
left += session.daysDelta * session.cellWidth;
|
||||
} else if (session.mode === "resize-start") {
|
||||
left += session.daysDelta * session.cellWidth;
|
||||
width = Math.max(session.cellWidth, width - session.daysDelta * session.cellWidth);
|
||||
} else {
|
||||
width = Math.max(session.cellWidth, width + session.daysDelta * session.cellWidth);
|
||||
}
|
||||
|
||||
if (session.mode === "resize-start") {
|
||||
const nextWidth = width - pointerOffsetX;
|
||||
if (nextWidth < session.cellWidth) {
|
||||
left += width - session.cellWidth;
|
||||
width = session.cellWidth;
|
||||
} else {
|
||||
left += pointerOffsetX;
|
||||
width = nextWidth;
|
||||
}
|
||||
} else if (session.mode === "resize-end") {
|
||||
width = Math.max(session.cellWidth, width + pointerOffsetX);
|
||||
}
|
||||
|
||||
target.element.style.left = `${left}px`;
|
||||
target.element.style.width = `${Math.max(session.cellWidth, width)}px`;
|
||||
target.element.style.transform = joinTransforms(
|
||||
target.baseTransform,
|
||||
session.mode === "move" && pointerOffsetX !== 0
|
||||
? `translateX(${pointerOffsetX}px)`
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleLivePreview(session: LivePreviewSession) {
|
||||
if (session.frame !== null) return;
|
||||
session.frame = requestAnimationFrame(() => {
|
||||
session.frame = null;
|
||||
renderLivePreview(session);
|
||||
});
|
||||
}
|
||||
|
||||
function clearLivePreview(session: LivePreviewSession | null) {
|
||||
if (!session) return;
|
||||
if (session.frame !== null) {
|
||||
cancelAnimationFrame(session.frame);
|
||||
}
|
||||
|
||||
for (const target of session.targets) {
|
||||
target.element.style.left = `${target.baseLeft}px`;
|
||||
target.element.style.width = `${target.baseWidth}px`;
|
||||
target.element.style.transform = target.baseTransform;
|
||||
}
|
||||
}
|
||||
|
||||
function datesMatch(a: Date | null, b: Date | null) {
|
||||
return Boolean(a && b) && a!.getTime() === b!.getTime();
|
||||
}
|
||||
|
||||
function preserveLivePreview(session: LivePreviewSession | null) {
|
||||
if (!session) return;
|
||||
if (session.frame !== null) {
|
||||
cancelAnimationFrame(session.frame);
|
||||
session.frame = null;
|
||||
}
|
||||
|
||||
for (const target of session.targets) {
|
||||
target.baseLeft = toPxValue(target.element.style.left);
|
||||
target.baseWidth = toPxValue(target.element.style.width);
|
||||
target.baseTransform = target.element.style.transform;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Range-select state ─────────────────────────────────────────────────────
|
||||
|
||||
export interface RangeState {
|
||||
|
||||
Reference in New Issue
Block a user