"use client" import { useCallback, useEffect, useRef, useState } from "react" import { ChevronDown } from "lucide-react" import type { Editor } from "@tiptap/react" import type { DocPageLayout, DocPageSetup } from "@/lib/drive/doc-page-setup" import { pxToMm } from "@/lib/drive/doc-page-setup" import { DOCS_HF_CHROME_BAR_PX, defaultFooterZoneHeightPx, defaultHeaderZoneHeightPx, pageFooterGeometry, pageHeaderGeometry, resolveRegionForPage, toggleDifferentFirstPage, type DocsHeaderFooterRegion, } from "@/lib/drive/docs-header-footer-layout" import { DocsRegionEditor } from "@/components/drive/richtext/docs-region-editor" import { DocsHeaderFooterFormatDialog, DocsPageNumbersDialog, } from "@/components/drive/richtext/docs-header-footer-dialogs" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Checkbox } from "@/components/ui/checkbox" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" export function extractRegionPlainText(content: Record | undefined): string { if (!content) return "" const parts: string[] = [] const walk = (node: unknown) => { if (!node || typeof node !== "object") return const record = node as Record if (typeof record.text === "string") parts.push(record.text) if (Array.isArray(record.content)) record.content.forEach(walk) } walk(content) return parts.join(" ").trim() } export type { DocsHeaderFooterRegion } export type DocsHeaderFooterEditTarget = { region: DocsHeaderFooterRegion pageIndex: number } | null function DocsHeaderFooterChrome({ label, pageWidth, barTop, placement, showFirstPageCheckbox, differentFirstPage, onDifferentFirstPageChange, onFormatOpen, onPageNumOpen, onRemove, }: { label: string pageWidth: number barTop: number placement: DocsHeaderFooterRegion showFirstPageCheckbox: boolean differentFirstPage: boolean onDifferentFirstPageChange: (checked: boolean) => void onFormatOpen: () => void onPageNumOpen: () => void onRemove: () => void }) { return (
event.preventDefault()} > {label}
{showFirstPageCheckbox ? ( ) : null} {label === "En-tête" ? "Format de l'en-tête" : "Format du pied de page"} Numéros de page {label === "En-tête" ? "Supprimer l'en-tête" : "Supprimer le pied de page"}
) } export function DocsHeaderFooterBand({ region, pageLayout, pageIndex, pageTop, pageWidth, pageHeight, margins, editable, editingTarget, onStartEdit, onStopEdit, onContentChange, onPageSetupChange, onRegionEditorReady, onRegionHeightMeasure, }: { region: DocsHeaderFooterRegion pageLayout: DocPageLayout pageIndex: number pageTop: number pageWidth: number pageHeight: number margins: { top: number; right: number; bottom: number; left: number } editable: boolean editingTarget: DocsHeaderFooterEditTarget onStartEdit: (region: DocsHeaderFooterRegion, pageIndex: number) => void onStopEdit: () => void onContentChange: ( region: DocsHeaderFooterRegion, content: Record, meta: { pageIndex: number; contentHeightPx: number }, options?: { immediate?: boolean } ) => void onPageSetupChange: (patch: Partial) => void onRegionEditorReady?: (editor: Editor | null) => void onRegionHeightMeasure?: (payload: { region: DocsHeaderFooterRegion pageIndex: number heightPx: number }) => void }) { const isHeader = region === "header" const isEditing = editingTarget?.region === region && editingTarget.pageIndex === pageIndex const canEdit = editable const regionData = resolveRegionForPage(pageLayout, region, pageIndex) const differentFirstPage = pageLayout.headerFooterDifferentFirstPage ?? false const headerGeom = pageHeaderGeometry(pageLayout, pageTop, pageIndex) const footerGeom = pageFooterGeometry(pageLayout, pageTop, pageHeight, pageIndex) const bandLeft = margins.left const bandWidth = pageWidth - margins.left - margins.right const zoneHeight = isHeader ? headerGeom.zoneHeight : footerGeom.zoneHeight const minZoneHeight = isHeader ? defaultHeaderZoneHeightPx(pageLayout) : defaultFooterZoneHeightPx(pageLayout) const [liveHeight, setLiveHeight] = useState(zoneHeight) const liveHeightRef = useRef(zoneHeight) liveHeightRef.current = liveHeight useEffect(() => { if (isEditing) return setLiveHeight((height) => Math.min(height, zoneHeight)) }, [isEditing, zoneHeight]) /** View: content-sized. Edit: at least default header/footer margin band. */ const contentHeight = isEditing ? Math.max(minZoneHeight, liveHeight) : Math.max(20, liveHeight) const zoneTop = isHeader ? headerGeom.zoneTop : footerGeom.zoneBottom - contentHeight const headerChromeTop = headerGeom.zoneTop + contentHeight const footerChromeTop = footerGeom.zoneBottom - contentHeight - DOCS_HF_CHROME_BAR_PX const chromeBarTop = isHeader ? headerChromeTop : footerChromeTop const editLateralTop = isHeader ? zoneTop : footerChromeTop const editLateralHeight = contentHeight + DOCS_HF_CHROME_BAR_PX const [formatOpen, setFormatOpen] = useState(false) const [pageNumOpen, setPageNumOpen] = useState(false) const contentPersistTimer = useRef | null>(null) const latestContentRef = useRef | null>(null) const persistRegionContent = useCallback( (content: Record, immediate = false) => { latestContentRef.current = content onContentChange( region, content, { pageIndex, contentHeightPx: Math.max(minZoneHeight, liveHeightRef.current), }, immediate ? { immediate: true } : undefined ) }, [minZoneHeight, onContentChange, pageIndex, region] ) const scheduleRegionContentPersist = useCallback( (content: Record) => { latestContentRef.current = content if (contentPersistTimer.current) clearTimeout(contentPersistTimer.current) contentPersistTimer.current = setTimeout(() => { persistRegionContent(content) }, 800) }, [persistRegionContent] ) const pageNumber = pageLayout.pageNumbers?.enabled && pageLayout.pageNumbers.placement === region && (pageLayout.pageNumbers.showOnFirstPage || pageIndex > 0) ? (pageLayout.pageNumbers.startAt ?? 1) + pageIndex : null const handleRemove = useCallback(() => { onContentChange( region, { type: "doc", content: [{ type: "paragraph" }] }, { pageIndex, contentHeightPx: minZoneHeight }, { immediate: true } ) onPageSetupChange(isHeader ? { header: null, headerFirstPage: null } : { footer: null, footerFirstPage: null }) onStopEdit() }, [isHeader, minZoneHeight, onContentChange, onPageSetupChange, onStopEdit, pageIndex, region]) const setupPatch: DocPageSetup = { widthMm: pageLayout.format.widthMm, heightMm: pageLayout.format.heightMm, marginsMm: { top: pxToMm(pageLayout.marginsPx.top), right: pxToMm(pageLayout.marginsPx.right), bottom: pxToMm(pageLayout.marginsPx.bottom), left: pxToMm(pageLayout.marginsPx.left), }, headerMarginMm: pageLayout.headerMarginMm, footerMarginMm: pageLayout.footerMarginMm, headerFooterDifferentFirstPage: differentFirstPage, headerFooterDifferentOddEven: pageLayout.headerFooterDifferentOddEven, pageNumbers: pageLayout.pageNumbers, header: pageLayout.header, footer: pageLayout.footer, headerFirstPage: pageLayout.headerFirstPage, footerFirstPage: pageLayout.footerFirstPage, } const hasContent = regionData?.content && extractRegionPlainText(regionData.content).length > 0 const regionLabel = isHeader ? "En-tête" : "Pied de page" const handleDoubleClick = (event: React.MouseEvent) => { if (!canEdit) return event.preventDefault() event.stopPropagation() onStartEdit(region, pageIndex) } const persistHeight = useCallback( (heightPx: number) => { const measured = Math.max(20, heightPx) setLiveHeight(measured) onRegionHeightMeasure?.({ region, pageIndex, heightPx: measured }) }, [onRegionHeightMeasure, pageIndex, region] ) useEffect(() => { return () => { if (contentPersistTimer.current) clearTimeout(contentPersistTimer.current) } }, []) const wasEditingRef = useRef(false) useEffect(() => { if (wasEditingRef.current && !isEditing) { if (contentPersistTimer.current) { clearTimeout(contentPersistTimer.current) contentPersistTimer.current = null } persistRegionContent( (latestContentRef.current ?? regionData?.content ?? ({ type: "doc", content: [{ type: "paragraph" }] } as Record)), true ) } wasEditingRef.current = isEditing }, [isEditing, persistRegionContent, regionData?.content]) return ( <> {isEditing ? ( <>
{isHeader ? (
) : (
)} { onPageSetupChange(toggleDifferentFirstPage(setupPatch, checked)) }} onFormatOpen={() => setFormatOpen(true)} onPageNumOpen={() => setPageNumOpen(true)} onRemove={handleRemove} /> ) : null} {!isEditing && !hasContent && canEdit ? (
) : null} {isEditing || hasContent ? (
{ requestAnimationFrame(() => { const active = document.activeElement if ( active?.closest(".docs-hf-chrome") || active?.closest('[role="dialog"]') || active?.closest("[data-radix-popper-content-wrapper]") ) { return } onStopEdit() }) } : undefined } onEditorReady={isEditing ? onRegionEditorReady : undefined} onContentHeightChange={persistHeight} /> {pageNumber != null && !isEditing ? (
{pageNumber}
) : null}
) : null} {isEditing ? ( <> onPageSetupChange({ pageNumbers })} /> ) : null} ) }