ultisuite-client/components/drive/richtext/use-docs-ruler-margin-drag.ts
R3D347HR4Y 2a7c153748
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
wrap page
2026-06-10 12:48:27 +02:00

156 lines
4.0 KiB
TypeScript

"use client"
import { useCallback, useMemo, useRef, useState } from "react"
import type { DocPageLayout, DocPageSetup } from "@/lib/drive/doc-page-setup"
import { pxToMm } from "@/lib/drive/doc-page-setup"
import {
clampPageMarginPx,
mergeMarginPreview,
type DocsPageMarginsPx,
type DocsRulerMarginSide,
} from "@/lib/drive/docs-ruler-margin-math"
import { formatMarginDistanceLabel } from "@/lib/drive/docs-ruler-units"
import type { DocsRulerDragTooltipState } from "@/components/drive/richtext/docs-ruler-markers"
function applyMarginDragPx(
side: DocsRulerMarginSide,
rawValuePx: number,
base: DocsPageMarginsPx,
preview: Partial<DocsPageMarginsPx> | null,
pageWidth: number,
pageHeight: number
): { nextPreview: Partial<DocsPageMarginsPx>; clamped: number } {
const current = mergeMarginPreview(base, preview)
let nextValue = rawValuePx
if (side === "right") {
nextValue = pageWidth - rawValuePx
} else if (side === "bottom") {
nextValue = pageHeight - rawValuePx
}
const clamped = clampPageMarginPx(
side,
nextValue,
current,
pageWidth,
pageHeight
)
return {
nextPreview: { ...(preview ?? {}), [side]: clamped },
clamped,
}
}
export function useDocsRulerMarginDrag({
pageLayout,
editable,
onPageSetupChange,
}: {
pageLayout: DocPageLayout
editable: boolean
onPageSetupChange?: (
patch: Partial<DocPageSetup>,
options?: { immediate?: boolean }
) => void
}) {
const [previewPx, setPreviewPx] = useState<Partial<DocsPageMarginsPx> | null>(null)
const [dragTooltip, setDragTooltip] = useState<DocsRulerDragTooltipState>(null)
const dragBaseRef = useRef<DocsPageMarginsPx>(pageLayout.marginsPx)
/** Sync preview during drag — window pointer events don't flush React state in time. */
const previewRef = useRef<Partial<DocsPageMarginsPx> | null>(null)
const pageLayoutRef = useRef(pageLayout)
pageLayoutRef.current = pageLayout
const marginsPx = useMemo(
() => mergeMarginPreview(pageLayout.marginsPx, previewPx),
[pageLayout.marginsPx, previewPx]
)
const pageLayoutWithMargins = useMemo(
(): DocPageLayout => ({
...pageLayout,
marginsPx,
}),
[pageLayout, marginsPx]
)
const beginMarginDrag = useCallback(
(_side: DocsRulerMarginSide) => {
if (!editable) return
const layout = pageLayoutRef.current
dragBaseRef.current = mergeMarginPreview(layout.marginsPx, previewRef.current)
previewRef.current = null
},
[editable]
)
const moveMarginDrag = useCallback(
(side: DocsRulerMarginSide, rawValuePx: number, clientX: number, clientY: number) => {
if (!editable) return
const layout = pageLayoutRef.current
const { nextPreview, clamped } = applyMarginDragPx(
side,
rawValuePx,
dragBaseRef.current,
previewRef.current,
layout.widthPx,
layout.heightPx
)
previewRef.current = nextPreview
setPreviewPx(nextPreview)
setDragTooltip({
label: formatMarginDistanceLabel(clamped, layout.format.id),
x: clientX,
y: clientY,
})
},
[editable]
)
const endMarginDrag = useCallback(() => {
setDragTooltip(null)
const preview = previewRef.current
previewRef.current = null
if (!editable || !preview) {
setPreviewPx(null)
return
}
const layout = pageLayoutRef.current
const merged = mergeMarginPreview(layout.marginsPx, preview)
setPreviewPx(null)
onPageSetupChange?.(
{
marginsMm: {
top: pxToMm(merged.top),
right: pxToMm(merged.right),
bottom: pxToMm(merged.bottom),
left: pxToMm(merged.left),
},
},
{ immediate: true }
)
}, [editable, onPageSetupChange])
const cancelMarginDrag = useCallback(() => {
previewRef.current = null
setPreviewPx(null)
setDragTooltip(null)
}, [])
return {
marginsPx,
pageLayoutWithMargins,
marginDragActive: previewPx != null,
dragTooltip,
beginMarginDrag,
moveMarginDrag,
endMarginDrag,
cancelMarginDrag,
}
}