ultisuite-client/lib/drive/docs-graphic-layout.ts
R3D347HR4Y 303b2b1074
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
wow
2026-06-11 01:22:40 +02:00

190 lines
5.7 KiB
TypeScript

import type { CSSProperties } from "react"
import { mmToPx } from "./doc-page-setup.ts"
import { pageTopPx, usesPageLayer } from "./docs-graphic-position.ts"
import {
type DocsGraphicAttrs,
parseGraphicAttrs,
} from "./docs-graphic-types.ts"
export type DocsGraphicLayoutStyle = {
wrapper: CSSProperties
inner: CSSProperties
content: CSSProperties
behindText: boolean
inFrontText: boolean
usePageLayer: boolean
}
export type DocsGraphicLayoutOptions = {
pageHeight?: number
}
export function computeGraphicLayoutStyle(
raw: Record<string, unknown> | DocsGraphicAttrs,
options: DocsGraphicLayoutOptions = {}
): DocsGraphicLayoutStyle {
const attrs = "graphicType" in raw && typeof raw.graphicType === "string"
? (raw as DocsGraphicAttrs)
: parseGraphicAttrs(raw)
const width = attrs.width
const height = attrs.height
const rotation = attrs.rotationDeg
const behindText = attrs.wrap === "behind"
const inFrontText = attrs.wrap === "in-front"
const onPageLayer = usesPageLayer(attrs)
const isAbsolute = onPageLayer || attrs.placement === "absolute"
const wrapMarginPx = mmToPx(attrs.wrapMarginMm)
const blockMarginPx = Math.max(4, Math.round(wrapMarginPx / 2))
const wrapper: CSSProperties = {}
const inner: CSSProperties = {
width,
height,
transform: rotation ? `rotate(${rotation}deg)` : undefined,
transformOrigin: "center center",
position: isAbsolute ? "absolute" : "relative",
zIndex: behindText ? 1 : inFrontText ? 2 : attrs.zIndex > 0 ? attrs.zIndex : undefined,
pointerEvents: "auto",
opacity: attrs.opacity < 1 ? attrs.opacity : undefined,
boxShadow: attrs.shadow || undefined,
}
if (onPageLayer) {
if (options.pageHeight != null && options.pageHeight > 0) {
inner.left = attrs.pageX
inner.top = pageTopPx(attrs.pageIndex, options.pageHeight) + attrs.pageY
} else {
inner.left = attrs.pageX
inner.top = attrs.pageY
}
} else if (isAbsolute) {
// Legacy anchored-absolute coords (kept for raw/unmigrated attrs).
inner.left = attrs.x
inner.top = attrs.y
} else {
// In-flow: layout (float, margins, display) must live on the wrapper so
// surrounding text actually wraps around the node.
wrapper.width = width
wrapper.height = height
wrapper.maxWidth = "100%"
if (attrs.placement === "inline" || attrs.wrap === "inline") {
wrapper.display = "inline-block"
wrapper.verticalAlign = "baseline"
} else if (attrs.wrap === "top-bottom") {
wrapper.display = "block"
wrapper.clear = "both"
wrapper.marginBlock = `${wrapMarginPx}px`
if (attrs.floatSide === "center") {
wrapper.marginInline = "auto"
}
} else if (attrs.wrap === "square" || attrs.wrap === "tight" || attrs.wrap === "through") {
wrapper.display = "block"
wrapper.marginBlock = `${blockMarginPx}px`
if (attrs.floatSide === "center") {
wrapper.float = "none"
wrapper.marginInline = "auto"
} else {
const sideMargin = attrs.wrap === "tight" ? Math.max(2, blockMarginPx / 2) : wrapMarginPx
wrapper.float = attrs.floatSide === "right" ? "right" : "left"
// Gap goes on the text side of the float.
wrapper.marginInlineStart = attrs.floatSide === "right" ? `${sideMargin}px` : undefined
wrapper.marginInlineEnd = attrs.floatSide === "right" ? undefined : `${sideMargin}px`
if (attrs.wrap === "tight") {
wrapper.shapeOutside = "margin-box"
}
}
if (attrs.wrap === "through") {
inner.opacity = 0.85
inner.mixBlendMode = "multiply"
}
} else {
wrapper.display = "block"
wrapper.marginBlock = `${wrapMarginPx}px`
if (attrs.floatSide === "center") {
wrapper.marginInline = "auto"
}
}
// Resize-from-left/top keeps the opposite edge fixed via x/y offset.
if (attrs.x || attrs.y) {
inner.left = attrs.x
inner.top = attrs.y
}
}
const content: CSSProperties = {
width: "100%",
height: "100%",
overflow: "hidden",
position: "relative",
}
return { wrapper, inner, content, behindText, inFrontText, usePageLayer: onPageLayer }
}
export const RESIZE_HANDLES = [
"nw",
"n",
"ne",
"e",
"se",
"s",
"sw",
"w",
] as const
export type ResizeHandle = (typeof RESIZE_HANDLES)[number]
export function resizeWithHandle(
handle: ResizeHandle,
startWidth: number,
startHeight: number,
dx: number,
dy: number,
minSize = 24,
lockAspect = false
): { width: number; height: number; xOffset: number; yOffset: number } {
let width = startWidth
let height = startHeight
let xOffset = 0
let yOffset = 0
if (lockAspect) {
const aspect = startWidth / Math.max(startHeight, 1)
if (Math.abs(dx) >= Math.abs(dy)) {
width = startWidth + (handle.includes("w") ? -dx : dx)
height = width / aspect
} else {
height = startHeight + (handle.includes("n") ? -dy : dy)
width = height * aspect
}
if (handle.includes("w")) xOffset = startWidth - width
if (handle.includes("n")) yOffset = startHeight - height
} else {
if (handle.includes("e")) width = startWidth + dx
if (handle.includes("w")) {
width = startWidth - dx
xOffset = dx
}
if (handle.includes("s")) height = startHeight + dy
if (handle.includes("n")) {
height = startHeight - dy
yOffset = dy
}
}
width = Math.max(minSize, width)
height = Math.max(minSize, height)
if (handle.includes("w") && width === minSize) xOffset = startWidth - minSize
if (handle.includes("n") && height === minSize) yOffset = startHeight - minSize
return { width: Math.round(width), height: Math.round(height), xOffset, yOffset }
}
export const DOCS_GRAPHIC_WRAP_MARGIN_PRESETS_MM = [0, 3, 6, 12] as const