204 lines
6.4 KiB
TypeScript
204 lines
6.4 KiB
TypeScript
import type { DocPageHeaderFooter, DocPageLayout, DocPageSetup } from "./doc-page-setup.ts"
|
||
import { mmToPx, pxToMm } from "./doc-page-setup.ts"
|
||
|
||
export type DocsHeaderFooterRegion = "header" | "footer"
|
||
|
||
/** Height of the Google Docs–style header/footer chrome bar (px). */
|
||
export const DOCS_HF_CHROME_BAR_PX = 28
|
||
|
||
export function headerOffsetPx(pageLayout: DocPageLayout): number {
|
||
return pageLayout.headerMarginMm != null
|
||
? mmToPx(pageLayout.headerMarginMm)
|
||
: 0
|
||
}
|
||
|
||
export function footerOffsetPx(pageLayout: DocPageLayout): number {
|
||
return pageLayout.footerMarginMm != null
|
||
? mmToPx(pageLayout.footerMarginMm)
|
||
: 0
|
||
}
|
||
|
||
export function defaultHeaderZoneHeightPx(pageLayout: DocPageLayout): number {
|
||
return Math.max(24, pageLayout.marginsPx.top - headerOffsetPx(pageLayout))
|
||
}
|
||
|
||
export function defaultFooterZoneHeightPx(pageLayout: DocPageLayout): number {
|
||
return Math.max(24, pageLayout.marginsPx.bottom - footerOffsetPx(pageLayout))
|
||
}
|
||
|
||
export function regionZoneHeightPx(
|
||
region: DocPageHeaderFooter | null | undefined,
|
||
defaultPx: number
|
||
): number {
|
||
if (region?.heightMm != null && region.heightMm > 0) {
|
||
return Math.max(defaultPx, mmToPx(region.heightMm))
|
||
}
|
||
return defaultPx
|
||
}
|
||
|
||
/** Header/footer content shown on a given page index. */
|
||
export function resolveRegionForPage(
|
||
pageLayout: DocPageLayout,
|
||
region: DocsHeaderFooterRegion,
|
||
pageIndex: number
|
||
): DocPageHeaderFooter | null | undefined {
|
||
const differentFirst = pageLayout.headerFooterDifferentFirstPage ?? false
|
||
if (region === "header") {
|
||
if (pageIndex === 0 && differentFirst) return pageLayout.headerFirstPage ?? null
|
||
return pageLayout.header
|
||
}
|
||
if (pageIndex === 0 && differentFirst) return pageLayout.footerFirstPage ?? null
|
||
return pageLayout.footer
|
||
}
|
||
|
||
export function effectiveTopMarginPx(pageLayout: DocPageLayout): number {
|
||
const offset = headerOffsetPx(pageLayout)
|
||
const defaultZone = defaultHeaderZoneHeightPx(pageLayout)
|
||
const sharedH = regionZoneHeightPx(pageLayout.header, defaultZone)
|
||
const firstH = pageLayout.headerFooterDifferentFirstPage
|
||
? regionZoneHeightPx(pageLayout.headerFirstPage, defaultZone)
|
||
: sharedH
|
||
return Math.max(pageLayout.marginsPx.top, offset + Math.max(sharedH, firstH))
|
||
}
|
||
|
||
export function effectiveBottomMarginPx(pageLayout: DocPageLayout): number {
|
||
const offset = footerOffsetPx(pageLayout)
|
||
const defaultZone = defaultFooterZoneHeightPx(pageLayout)
|
||
const sharedH = regionZoneHeightPx(pageLayout.footer, defaultZone)
|
||
const firstH = pageLayout.headerFooterDifferentFirstPage
|
||
? regionZoneHeightPx(pageLayout.footerFirstPage, defaultZone)
|
||
: sharedH
|
||
return Math.max(pageLayout.marginsPx.bottom, offset + Math.max(sharedH, firstH))
|
||
}
|
||
|
||
export function effectiveMarginsPx(
|
||
pageLayout: DocPageLayout,
|
||
livePreview?: { region: DocsHeaderFooterRegion; heightPx: number } | null,
|
||
measuredHeights?: Partial<Record<DocsHeaderFooterRegion, number>>
|
||
) {
|
||
let top = effectiveTopMarginPx(pageLayout)
|
||
let bottom = effectiveBottomMarginPx(pageLayout)
|
||
|
||
if (measuredHeights?.header != null) {
|
||
const offset = headerOffsetPx(pageLayout)
|
||
top = Math.max(pageLayout.marginsPx.top, offset + measuredHeights.header)
|
||
}
|
||
if (measuredHeights?.footer != null) {
|
||
const offset = footerOffsetPx(pageLayout)
|
||
bottom = Math.max(pageLayout.marginsPx.bottom, offset + measuredHeights.footer)
|
||
}
|
||
|
||
if (livePreview) {
|
||
const { region, heightPx } = livePreview
|
||
if (region === "header") {
|
||
const offset = headerOffsetPx(pageLayout)
|
||
top = Math.max(pageLayout.marginsPx.top, offset + heightPx)
|
||
} else {
|
||
const offset = footerOffsetPx(pageLayout)
|
||
bottom = Math.max(pageLayout.marginsPx.bottom, offset + heightPx)
|
||
}
|
||
}
|
||
|
||
return {
|
||
top,
|
||
right: pageLayout.marginsPx.right,
|
||
bottom,
|
||
left: pageLayout.marginsPx.left,
|
||
}
|
||
}
|
||
|
||
export function pageRegionZoneHeightPx(
|
||
pageLayout: DocPageLayout,
|
||
region: DocsHeaderFooterRegion,
|
||
pageIndex: number
|
||
): number {
|
||
const data = resolveRegionForPage(pageLayout, region, pageIndex)
|
||
const defaultPx =
|
||
region === "header"
|
||
? defaultHeaderZoneHeightPx(pageLayout)
|
||
: defaultFooterZoneHeightPx(pageLayout)
|
||
return regionZoneHeightPx(data, defaultPx)
|
||
}
|
||
|
||
export function pageHeaderGeometry(
|
||
pageLayout: DocPageLayout,
|
||
pageTop: number,
|
||
pageIndex: number
|
||
) {
|
||
const offset = headerOffsetPx(pageLayout)
|
||
const zoneHeight = pageRegionZoneHeightPx(pageLayout, "header", pageIndex)
|
||
const zoneTop = pageTop + offset
|
||
return {
|
||
zoneTop,
|
||
zoneHeight,
|
||
bottomLine: zoneTop + zoneHeight,
|
||
offset,
|
||
}
|
||
}
|
||
|
||
export function pageFooterGeometry(
|
||
pageLayout: DocPageLayout,
|
||
pageTop: number,
|
||
pageHeight: number,
|
||
pageIndex: number
|
||
) {
|
||
const offset = footerOffsetPx(pageLayout)
|
||
const zoneHeight = pageRegionZoneHeightPx(pageLayout, "footer", pageIndex)
|
||
const zoneBottom = pageTop + pageHeight - offset
|
||
return {
|
||
zoneTop: zoneBottom - zoneHeight,
|
||
zoneHeight,
|
||
zoneBottom,
|
||
topLine: zoneBottom - zoneHeight,
|
||
offset,
|
||
}
|
||
}
|
||
|
||
export function regionStorageKey(
|
||
region: DocsHeaderFooterRegion,
|
||
pageIndex: number,
|
||
differentFirstPage: boolean
|
||
): keyof DocPageSetup {
|
||
if (pageIndex === 0 && differentFirstPage) {
|
||
return region === "header" ? "headerFirstPage" : "footerFirstPage"
|
||
}
|
||
return region
|
||
}
|
||
|
||
export function buildRegionPatch(
|
||
setup: DocPageSetup,
|
||
region: DocsHeaderFooterRegion,
|
||
pageIndex: number,
|
||
content: Record<string, unknown>,
|
||
contentHeightPx: number
|
||
): Partial<DocPageSetup> {
|
||
const differentFirst = setup.headerFooterDifferentFirstPage ?? false
|
||
const key = regionStorageKey(region, pageIndex, differentFirst)
|
||
const topPx = mmToPx(setup.marginsMm.top)
|
||
const bottomPx = mmToPx(setup.marginsMm.bottom)
|
||
const headerOff =
|
||
setup.headerMarginMm != null ? mmToPx(setup.headerMarginMm) : 0
|
||
const footerOff =
|
||
setup.footerMarginMm != null ? mmToPx(setup.footerMarginMm) : 0
|
||
const defaultZonePx =
|
||
region === "header" ? Math.max(24, topPx - headerOff) : Math.max(24, bottomPx - footerOff)
|
||
const heightMm = pxToMm(Math.max(defaultZonePx, contentHeightPx))
|
||
return {
|
||
[key]: { content, heightMm },
|
||
}
|
||
}
|
||
|
||
export function toggleDifferentFirstPage(
|
||
setup: DocPageSetup,
|
||
enabled: boolean
|
||
): Partial<DocPageSetup> {
|
||
if (enabled) {
|
||
return {
|
||
headerFooterDifferentFirstPage: true,
|
||
headerFirstPage: setup.headerFirstPage ?? setup.header ?? null,
|
||
footerFirstPage: setup.footerFirstPage ?? setup.footer ?? null,
|
||
}
|
||
}
|
||
return { headerFooterDifferentFirstPage: false }
|
||
}
|