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

257 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export type DocsGraphicType = "image" | "shape" | "gradient"
export type DocsGraphicPlacement = "inline" | "block" | "absolute"
/** Text wrap / layering relative to body text */
export type DocsGraphicWrap =
| "inline"
| "square"
| "tight"
| "through"
| "top-bottom"
| "behind"
| "in-front"
export type DocsGraphicFloatSide = "left" | "right" | "center"
export type DocsShapeType = "rect" | "ellipse" | "line" | "arrow"
export type DocsCropShape = "rect" | "ellipse"
export type DocsGraphicAttrs = {
graphicType: DocsGraphicType
src: string | null
alt: string
assetId: string | null
shapeType: DocsShapeType
fill: string
stroke: string
strokeWidth: number
gradientCss: string
gradientAngle: number
gradientColor1: string
gradientColor2: string
width: number
height: number
placement: DocsGraphicPlacement
wrap: DocsGraphicWrap
floatSide: DocsGraphicFloatSide
x: number
y: number
rotationDeg: number
zIndex: number
/** Normalized crop region 01 relative to source image */
cropX: number
cropY: number
cropWidth: number
cropHeight: number
cropShape: DocsCropShape
opacity: number
shadow: string
}
export const DOCS_GRAPHIC_DEFAULTS: DocsGraphicAttrs = {
graphicType: "image",
src: null,
alt: "",
assetId: null,
shapeType: "rect",
fill: "#4285f4",
stroke: "#1a73e8",
strokeWidth: 2,
gradientCss: "",
gradientAngle: 180,
gradientColor1: "#4285f4",
gradientColor2: "#34a853",
width: 240,
height: 160,
placement: "block",
wrap: "square",
floatSide: "left",
x: 0,
y: 0,
rotationDeg: 0,
zIndex: 0,
cropX: 0,
cropY: 0,
cropWidth: 1,
cropHeight: 1,
cropShape: "rect",
opacity: 1,
shadow: "",
}
export const DOCS_GRAPHIC_WRAP_LABELS: Record<DocsGraphicWrap, string> = {
inline: "En ligne avec le texte",
square: "Carré",
tight: "Rapproché",
through: "À travers",
"top-bottom": "Haut et bas",
behind: "Derrière le texte",
"in-front": "Devant le texte",
}
export const DOCS_GRAPHIC_PLACEMENT_LABELS: Record<DocsGraphicPlacement, string> = {
inline: "En ligne",
block: "Bloc",
absolute: "Position absolue",
}
export function buildGradientCss(
angle: number,
color1: string,
color2: string
): string {
return `linear-gradient(${angle}deg, ${color1}, ${color2})`
}
export function parseGraphicAttrs(raw: Record<string, unknown>): DocsGraphicAttrs {
const num = (key: keyof DocsGraphicAttrs, fallback: number) => {
const value = raw[key]
return typeof value === "number" && Number.isFinite(value) ? value : fallback
}
const str = (key: keyof DocsGraphicAttrs, fallback: string) => {
const value = raw[key]
return typeof value === "string" ? value : fallback
}
const gradientAngle = num("gradientAngle", DOCS_GRAPHIC_DEFAULTS.gradientAngle)
const gradientColor1 = str("gradientColor1", DOCS_GRAPHIC_DEFAULTS.gradientColor1)
const gradientColor2 = str("gradientColor2", DOCS_GRAPHIC_DEFAULTS.gradientColor2)
const gradientCss =
str("gradientCss", "") ||
(raw.graphicType === "gradient"
? buildGradientCss(gradientAngle, gradientColor1, gradientColor2)
: "")
return {
graphicType:
raw.graphicType === "shape" || raw.graphicType === "gradient" || raw.graphicType === "image"
? raw.graphicType
: DOCS_GRAPHIC_DEFAULTS.graphicType,
src: typeof raw.src === "string" ? raw.src : null,
alt: str("alt", ""),
shapeType:
raw.shapeType === "ellipse" ||
raw.shapeType === "line" ||
raw.shapeType === "arrow" ||
raw.shapeType === "rect"
? raw.shapeType
: DOCS_GRAPHIC_DEFAULTS.shapeType,
fill: str("fill", DOCS_GRAPHIC_DEFAULTS.fill),
stroke: str("stroke", DOCS_GRAPHIC_DEFAULTS.stroke),
strokeWidth: num("strokeWidth", DOCS_GRAPHIC_DEFAULTS.strokeWidth),
gradientCss,
gradientAngle,
gradientColor1,
gradientColor2,
width: Math.max(24, num("width", DOCS_GRAPHIC_DEFAULTS.width)),
height: Math.max(24, num("height", DOCS_GRAPHIC_DEFAULTS.height)),
placement:
raw.placement === "inline" || raw.placement === "block" || raw.placement === "absolute"
? raw.placement
: DOCS_GRAPHIC_DEFAULTS.placement,
wrap:
raw.wrap === "inline" ||
raw.wrap === "square" ||
raw.wrap === "tight" ||
raw.wrap === "through" ||
raw.wrap === "top-bottom" ||
raw.wrap === "behind" ||
raw.wrap === "in-front"
? raw.wrap
: DOCS_GRAPHIC_DEFAULTS.wrap,
floatSide:
raw.floatSide === "left" || raw.floatSide === "right" || raw.floatSide === "center"
? raw.floatSide
: DOCS_GRAPHIC_DEFAULTS.floatSide,
assetId: typeof raw.assetId === "string" && raw.assetId ? raw.assetId : null,
x: num("x", 0),
y: num("y", 0),
rotationDeg: num("rotationDeg", 0),
zIndex: num("zIndex", 0),
cropX: clamp01(num("cropX", 0)),
cropY: clamp01(num("cropY", 0)),
cropWidth: clamp01(num("cropWidth", 1), 1),
cropHeight: clamp01(num("cropHeight", 1), 1),
cropShape: raw.cropShape === "ellipse" ? "ellipse" : "rect",
opacity: clamp01(num("opacity", 1), 1),
shadow: str("shadow", ""),
}
}
function clamp01(value: number, fallback = 0): number {
if (!Number.isFinite(value)) return fallback
return Math.min(1, Math.max(0, value))
}
/** CSS styles to render a cropped image inside its frame. */
export function computeCropImageStyle(attrs: Pick<
DocsGraphicAttrs,
"cropX" | "cropY" | "cropWidth" | "cropHeight" | "cropShape"
>): { img: Record<string, string | number>; clipPath?: string } {
const hasCrop =
attrs.cropX > 0 ||
attrs.cropY > 0 ||
attrs.cropWidth < 1 ||
attrs.cropHeight < 1
if (!hasCrop) return { img: {} }
const scaleX = 1 / Math.max(attrs.cropWidth, 0.01)
const scaleY = 1 / Math.max(attrs.cropHeight, 0.01)
const offsetX = -(attrs.cropX * scaleX * 100)
const offsetY = -(attrs.cropY * scaleY * 100)
const img: Record<string, string | number> = {
width: `${scaleX * 100}%`,
height: `${scaleY * 100}%`,
maxWidth: "none",
objectFit: "cover",
objectPosition: `${offsetX}% ${offsetY}%`,
}
const clipPath =
attrs.cropShape === "ellipse" ? "ellipse(50% 50% at 50% 50%)" : undefined
return { img, clipPath }
}
export function imageAttrsToGraphic(raw: Record<string, unknown>): Partial<DocsGraphicAttrs> {
const width =
typeof raw.width === "number"
? raw.width
: typeof raw.width === "string"
? Number(raw.width) || DOCS_GRAPHIC_DEFAULTS.width
: DOCS_GRAPHIC_DEFAULTS.width
const height =
typeof raw.height === "number"
? raw.height
: typeof raw.height === "string"
? Number(raw.height) || DOCS_GRAPHIC_DEFAULTS.height
: DOCS_GRAPHIC_DEFAULTS.height
return parseGraphicAttrs({
graphicType: "image",
src: raw.src,
alt: raw.alt ?? "",
width,
height,
placement: raw.placement ?? "inline",
wrap: raw.wrap ?? "inline",
floatSide: raw.floatSide ?? "left",
x: raw.x ?? 0,
y: raw.y ?? 0,
rotationDeg: raw.rotationDeg ?? 0,
zIndex: raw.zIndex ?? 0,
cropX: raw.cropX ?? 0,
cropY: raw.cropY ?? 0,
cropWidth: raw.cropWidth ?? 1,
cropHeight: raw.cropHeight ?? 1,
cropShape: raw.cropShape ?? "rect",
assetId: raw.assetId ?? null,
opacity: raw.opacity ?? 1,
shadow: raw.shadow ?? "",
})
}