@eigenpal/docx-editor-core/layout-painter/renderPage
Page Renderer
Renders a single page from Layout data to DOM elements. Each page contains positioned fragments within a content area.
This file owns the single-page orchestrator (renderPage) plus page-level styling (background, borders, content area) and floating-image extraction from paragraphs. Header/footer rendering lives in ./renderPage/headerFooter.ts, footnote area rendering in ./renderPage/footnotes.ts, and the multi-page virtualization / IntersectionObserver layer in ./renderPage/virtualization.ts.
Functions (28)
applyPageStylesfunctionSource ↗
Apply page styles to an element. Exported because virtualization.ts uses it to size lightweight shells before content lands in them.
declare function applyPageStyles(element: HTMLElement, width: number, height: number, options: RenderPageOptions): void;floatingImageIsBehindDocfunctionSource ↗
declare function floatingImageIsBehindDoc(img: {
wrapType?: string;
}): boolean;floatingImageWrapsTextfunctionSource ↗
Whether a floating image record reserves space in the text-wrap calculation. Records reaching this predicate have already passed isFloatingImageRun, so wrapType=undefined implies a cssFloat-driven float that wraps text.
declare function floatingImageWrapsText(img: {
wrapType?: string;
}): boolean;hfunction
Toggle the .is-focused reveal class on the painted boundary boxes whose control encloses the caret. Kept separate from the hover-driven .is-active class so the two reveal paths never clear each other.
declare function applySdtFocus(container: HTMLElement, focusedIds: Set<string>): void;ifunction
Build the painter's block.id → { block, measure } lookup from the parallel blocks/measures arrays. Shared by both adapters' paint step.
declare function buildBlockLookup(blocks: FlowBlock[], measures: Measure[]): BlockLookup;isFloatingImageRunfunctionSource ↗
Check if an image run is a floating image positioned at page/cell level.
declare function isFloatingImageRun(run: ImageRun): boolean;isTextWrappingFloatingImageRunfunctionSource ↗
Check if a floating image should create text wrapping exclusion zones. wrapNone images (behind / inFront) are positioned floats but do not shrink line widths; text paints over or under them.
declare function isTextWrappingFloatingImageRun(run: ImageRun): boolean;jfunction
Capture the rendered position of an inline image as EMUs, normalised to unzoomed coordinates. Returns horizontal offset relative to the page content area (column origin) and vertical offset relative to the containing paragraph — matches the OOXML attrs the resolver writes (relativeFrom: 'column' / relativeFrom: 'paragraph').
zoom defaults to 1; pass the editor's current zoom factor when the pages container has a CSS scale applied so getBoundingClientRect deltas are converted back to authored-pixel space before going to EMU.
Returns undefined for non-inline images or detached DOM.
declare function captureInlinePositionEmu(imageEl: HTMLElement, zoom?: number): {
horizontalEmu: number;
verticalEmu: number;
} | undefined;kfunction
Create a new LayoutPainter instance
declare function createPainter(options?: PainterOptions): LayoutPainter;lfunction
Map an image's current OOXML attrs onto the menu's choice vocabulary so the menu can highlight the active option. Returns null for topAndBottom and any unknown wrap type — those don't have a directional menu entry.
cssFloat accepts both null and undefined so framework adapters can use either as their "unset" sentinel without an extra normalisation step.
declare function deriveLayoutChoice(wrapType: WrapType, cssFloat?: ImageAttrs['cssFloat'] | null): ImageLayoutTarget | null;mfunction
Group ids of every block-level content control that encloses the given selection — used to keep a control's boundary visible while the caret is inside it (Word-style focus), independent of mouse hover. The id matches toFlowBlocks' sdt@${pos} scheme, where pos is the SDT node's position; $pos.before(d) yields exactly that for an ancestor at depth d, so the ids line up with the painted boxes' data-sdt-group-id. Both ends of a range are collected so a selection straddling a control still lights it up.
declare function enclosingSdtGroupIds(doc: Node, from: number, to: number): Set<string>;nfunction
Walk up from a click target to the nearest rendered image element, returning just the element (no PM position parsing). Used by the left-click selection path in both adapters — data-pm-start is read separately by callers that need the position. Returns the element when the click was directly on an inline <img.layout-run-image> OR inside one of the container classes registered in LAYOUT_IMAGE_CLASSES.
declare function findImageElement(target: EventTarget | null): HTMLElement | null;ofunction
Walk up from an event target looking for any rendered image element. Returns the PM position embedded in data-pm-start, or null if the target isn't on an image.
declare function hitTestImage(target: EventTarget | null): ImageHitTestResult | null;pfunction
Whether a given option is enabled for an image with the given current wrap type. Every option stays clickable — picking the option that matches the current state is a no-op (the PM command early-returns), which matches Word's behavior. We don't grey out the current option, so the menu reads consistently regardless of whether the image is inline or anchored.
The flag is kept around for forward-compatibility (e.g. future read-only mode), but currently always returns true.
declare function isImageLayoutOptionEnabled(_option: ImageLayoutOptionDef, _currentWrapType: WrapType): boolean;qfunction
Render an image fragment to DOM
declare function renderImageFragment(fragment: ImageFragment, block: ImageBlock, _measure: ImageMeasure, _context: RenderContext, options?: RenderImageFragmentOptions): HTMLElement;rfunction
Render a fragment to DOM
declare function renderFragment(fragment: Fragment, context: RenderContext, options?: RenderFragmentOptions): HTMLElement;renderAllPagesNowfunctionSource ↗
Force every virtualized page shell in container to be fully rendered.
Virtualization keeps off-screen pages as empty shells so cloning the pages container for print (or any DOM snapshot) yields blank pages past the visible band. Callers that need every page populated — print, export-to-HTML, pdf snapshot — should call this first.
No-op for small documents (rendered eagerly) or containers that were never managed by renderPages. Returns the number of shells populated by this call (useful for tests).
declare function renderAllPagesNow(container: HTMLElement): number;renderFloatingImagesLayerfunctionSource ↗
Render a layer of positioned floating images. Used at both page level and inside table cells; the variant differs only in class names and sizing.
declare function renderFloatingImagesLayer(floatingImages: FloatingImagePaintRecord[], doc: Document, options: FloatingImagesLayerOptions): HTMLElement;renderPagefunctionSource ↗
Render a single page to DOM
declare function renderPage(page: Page, context: RenderContext, options?: RenderPageOptions): HTMLElement;renderPagesfunctionSource ↗
Render multiple pages to a container with virtualization for large documents.
For documents with fewer than VIRTUALIZATION_THRESHOLD pages, all pages are rendered eagerly. For larger documents, only pages near the visible viewport are fully rendered — off-screen pages are lightweight shells with correct dimensions to preserve scroll position.
An IntersectionObserver watches page elements and populates/clears content as pages scroll into and out of view.
declare function renderPages(pages: Page[], container: HTMLElement, options?: RenderPageOptions & {
pageGap?: number;
footnotesByPage?: Map<number, FootnoteRenderItem[]>;
}): RenderPagesUpdateKind;resolveHeaderFooterFloatingTablePositionfunctionSource ↗
Resolve the (left, top) position for a floating table inside a header/ footer container, per ECMA-376 §17.4.57. The table's floating.tblpX/tblpY are already in pixels (parser converted from twips); horzAnchor/ vertAnchor decide whether the offset is relative to the page, the margins, or the surrounding text/column. Coordinates returned are relative to the HF container's flow origin (layout.flowTop / layout.flowLeft) so the caller can drop them straight into style.top / style.left.
declare function resolveHeaderFooterFloatingTablePosition(floating: NonNullable<TableBlock['floating']>, layout: HeaderFooterLayoutInfo): {
left: number;
top: number;
};resolveHeaderFooterFloatLeftfunctionSource ↗
Resolve the CSS left (px) for an anchored object (image or text box) in a header/footer, honoring wp:positionH (relativeTo page/margin, align left/center/right, or posOffset). Shared by floating images and text boxes so a page-centered text box in the header lands centered like Word, not pinned to the left.
declare function resolveHeaderFooterFloatLeft(width: number, h: {
relativeTo?: string;
posOffset?: number;
align?: string;
alignment?: string;
} | undefined, layout: HeaderFooterLayoutInfo): string;sfunction
Render a single line
declare function renderLine(block: ParagraphBlock, line: MeasuredLine, alignment: 'left' | 'center' | 'right' | 'justify' | undefined, doc: Document, options?: RenderLineOptions): HTMLElement;tfunction
Render a paragraph fragment
declare function renderParagraphFragment(fragment: ParagraphFragment, block: ParagraphBlock, measure: ParagraphMeasure, context: RenderContext, options?: RenderParagraphOptions): HTMLElement;ufunction
Render a table fragment to DOM
declare function renderTableFragment(fragment: TableFragment, block: TableBlock, measure: TableMeasure, context: RenderContext, options?: RenderTableFragmentOptions): HTMLElement;vfunction
Render a text box fragment to DOM
declare function renderTextBoxFragment(fragment: TextBoxFragment, block: TextBoxBlock, measure: TextBoxMeasure, context: RenderContext, options?: RenderTextBoxFragmentOptions): HTMLElement;wfunction
Slice runs for a specific line
declare function sliceRunsForLine(block: ParagraphBlock, line: MeasuredLine): Run[];xfunction
Translate the legacy toolbar wrap-type vocabulary (wrapLeft / wrapRight / square / tight / through / topAndBottom / behind / inFront / inline) into a ImageLayoutTarget so toolbar dispatch shares the same PM command path as the right-click menu.
Returns undefined for unknown values; callers should treat that as "no-op".
declare function toolbarValueToLayoutTarget(value: string): ImageLayoutTarget | undefined;Classes (1)
fclass
Layout Painter class
Renders Layout data to DOM with efficient reconciliation. Only updates changed pages and fragments for better performance.
declare class LayoutPainter| Member | Type | Summary |
|---|---|---|
| (constructor) | | Constructs a new instance of the `LayoutPainter` class |
| getPageCount | | Get the current page count |
| getPageElement | | Get a page element by index |
| mount | | Mount the painter to a container element |
| paint | | Paint a layout to the container |
| resolvedCommentIds | Set<number> | |
| scrollToPage | | Scroll to a specific page |
| setBlockLookup | | Set the block lookup map for rendering fragments |
| unmount | | Unmount the painter |
Interfaces (11)
ainterface
Block lookup entry for painter
interface BlockLookupEntry| Member | Type | Summary |
|---|---|---|
| block | FlowBlock | |
| measure | Measure | |
| version? | string |
cinterface
interface ImageHitTestResult| Member | Type | Summary |
|---|---|---|
| imageEl | HTMLElement | The matched element — pass to `captureInlinePositionEmu` if it's inline. |
| pos | number | PM doc position of the image node, read from `data-pm-start`. |
einterface
interface ImageLayoutOptionDef| Member | Type | Summary |
|---|---|---|
| choice | ImageLayoutTarget | Choice value — what gets dispatched on click. |
| i18nDescKey | string | i18n key under `imageWrap.menuDesc.*`. |
| i18nLabelKey | string | i18n key under `imageWrap.menu.*`. |
| iconHint | ImageLayoutIconHint | Hint for the framework's icon registry. |
FloatingImagePaintRecordinterfaceSource ↗
Minimum fields the floating-image painter needs. Page-level and cell-level float records both satisfy this shape.
interface FloatingImagePaintRecord| Member | Type | Summary |
|---|---|---|
| alt? | string | |
| cropBottom? | number | |
| cropLeft? | number | |
| cropRight? | number | |
| cropTop? | number | wp:srcRect crop fractions in [0, 1]. |
| height | number | |
| opacity? | number | a:alphaModFix - CSS opacity. |
| pmEnd? | number | |
| pmStart? | number | |
| src | string | |
| transform? | string | |
| width | number | |
| x | number | |
| y | number |
FloatingImagesLayerOptionsinterfaceSource ↗
interface FloatingImagesLayerOptions| Member | Type | Summary |
|---|---|---|
| itemClass | string | |
| layerClass | string | |
| layerMode | 'front' | 'behind' | `behind` skips z-index so DOM order keeps the layer below body fragments. |
| sizing | 'inset0' | 'fullSize' | `inset0` sizes the layer with `top/right/bottom/left = 0` (used at page level). `fullSize` uses `width/height = 100%` and adds `overflow: hidden` (used inside table cells). |
FootnoteRenderIteminterfaceSource ↗
A single footnote item ready for rendering at page bottom.
interface FootnoteRenderItem| Member | Type | Summary |
|---|---|---|
| content? | FootnoteContent | Measured body-pipeline content used for WYSIWYG painting. |
| displayNumber | string | Display number (e.g. "1", "2") |
| text | string | Plain text content |
HeaderFooterContentinterfaceSource ↗
Header/footer content for rendering
interface HeaderFooterContent| Member | Type | Summary |
|---|---|---|
| blocks | FlowBlock[] | Flow blocks for the header/footer content. |
| flowHeight? | number | In-flow band height: the height of strictly in-flow content (paragraphs, tables, inline images/text boxes), EXCLUDING anchored / floating objects. This is what grows the header/footer band and pushes the body margin, mirroring Word: a page/margin-anchored shape (e.g. a full-page letterhead in a header) is positioned independently and does NOT push body text down. Use this — not `height`/`visualBottom` — for margin extension. Falls back to `height` when undefined. |
| height | number | Total height of the content (in-flow stack incl. floating blocks). |
| measures | Measure[] | Measurements for the blocks. |
| visualBottom? | number | Bottom-most visual extent relative to the nominal flow origin. |
| visualTop? | number | Top-most visual extent relative to the nominal flow origin. |
HeaderFooterLayoutInfointerfaceSource ↗
interface HeaderFooterLayoutInfo| Member | Type | Summary |
|---|---|---|
| contentWidth | number | |
| flowLeft | number | |
| flowTop | number | |
| margins | {
top: number;
right: number;
bottom: number;
left: number;
} | |
| pageHeight | number | |
| pageWidth | number |
Pinterface
Painter options
interface PainterOptions| Member | Type | Summary |
|---|---|---|
| containerBackground? | string | Container background color |
| document? | Document | Document to create elements in |
| pageBackground? | string | Background color for pages |
| pageGap? | number | Gap between pages in pixels |
| showShadow? | boolean | Show page shadows |
RenderContextinterfaceSource ↗
Context passed to fragment renderers
interface RenderContext| Member | Type | Summary |
|---|---|---|
| contentWidth? | number | Content width in pixels (page width minus margins) - used for justify |
| insideTableCell? | boolean | When true, floating images render in-flow instead of being skipped (for table cells) |
| pageNumber | number | Current page number (1-indexed) |
| positioning? | 'absolute' | 'flow' | How the renderer should position its outer element. The body lays fragments at absolute (x, y) on the page (`'absolute'`, the default), while headers/footers and text boxes flow blocks vertically and let normal document flow handle placement (`'flow'`). The caller passes 'flow' instead of overwriting the renderer's inline styles after the fact (#379). |
| resolvedCommentIds? | Set<number> | Comment IDs that are resolved — skip highlight for these |
| section | 'body' | 'header' | 'footer' | Which section is being rendered |
| totalPages | number | Total number of pages |
RenderPageOptionsinterfaceSource ↗
Options for rendering a page
interface RenderPageOptions| Member | Type | Summary |
|---|---|---|
| backgroundColor? | string | Background color for pages |
| blockLookup? | BlockLookup | Block lookup for rendering actual content. |
| document? | Document | Document to create elements in (default: window.document) |
| firstPageFooterContent? | HeaderFooterContent | Footer content for the first page only (when titlePg is set). |
| firstPageHeaderContent? | HeaderFooterContent | Header content for the first page only (when titlePg is set). |
| footerContent? | HeaderFooterContent | Footer content to render (used for all pages, or pages 2+ when titlePg is set). |
| footerDistance? | number | Distance from page bottom to footer content. |
| footnoteArea? | FootnoteRenderItem[] | Footnotes to render at the bottom of this page. |
| headerContent? | HeaderFooterContent | Header content to render (used for all pages, or pages 2+ when titlePg is set). |
| headerDistance? | number | Distance from page top to header content. |
| pageBorders? | {
top?: BorderSpec;
bottom?: BorderSpec;
left?: BorderSpec;
right?: BorderSpec;
display?: 'allPages' | 'firstPage' | 'notFirstPage';
offsetFrom?: 'page' | 'text';
zOrder?: 'front' | 'back';
} | OOXML page borders from section properties. |
| pageClassName? | string | Custom page class name |
| resolvedCommentIds? | Set<number> | Comment IDs that are resolved — skip highlight for these |
| showBorders? | boolean | Show page borders (for debugging) |
| showShadow? | boolean | Drop shadow on pages |
| theme? | Theme | null | Theme for resolving border colors. |
| titlePg? | boolean | Whether different first page headers/footers are enabled (w:titlePg). |
| watermark? | Watermark | Watermark to paint behind body content (resolved from the page's section header). |
Type aliases (3)
Btype
Block lookup map type
type BlockLookup = Map<string, BlockLookupEntry>;dtype
Hint to the framework's icon registry for which Material Symbol — or equivalent — to render alongside each option. Bindings own the icon component itself.
type ImageLayoutIconHint = 'inline' | 'squareLeft' | 'squareRight' | 'behind' | 'inFront';RenderPagesUpdateKindtypeSource ↗
Multi-page rendering with virtualization.
For documents under VIRTUALIZATION_THRESHOLD pages, all pages render eagerly. Larger documents render only pages near the viewport — off-screen pages are lightweight shells (correct dimensions, no fragment content) so scroll position is preserved. An IntersectionObserver populates and clears page content as the user scrolls. Incremental updates (re-rendering only fingerprint-changed pages) avoid blink when the document model shifts.
type RenderPagesUpdateKind = 'incremental' | 'full';Variables (7)
bconst
Mirrors Word's Wrap Text menu — five directional options.
IMAGE_LAYOUT_OPTIONS: readonly ImageLayoutOptionDef[]Fconst
CSS class names for fragment elements
FRAGMENT_CLASS_NAMES: {
fragment: string;
paragraph: string;
table: string;
image: string;
line: string;
run: string;
}gconst
CSS class names for text box elements
TEXTBOX_CLASS_NAMES: {
textBox: string;
}Iconst
CSS class names for image elements
IMAGE_CLASS_NAMES: {
image: string;
imageAnchored: string;
}Lconst
Image layout helpers shared between framework adapters (React, Vue, ...).
Everything here is framework-agnostic: pure DOM math + pure functions over OOXML wrap-type vocabulary. The corresponding UI bindings (right-click menu, toolbar dropdown) live in each framework adapter and call into these.
LAYOUT_IMAGE_CLASSES: {
readonly runImage: "layout-run-image";
readonly blockImage: "layout-block-image";
readonly pageFloatingImage: "layout-page-floating-image";
readonly cellFloatingImage: "layout-cell-floating-image";
readonly pageContent: "layout-page-content";
readonly paragraph: "layout-paragraph";
}PAGE_CLASS_NAMESconstSource ↗
CSS class names for page elements
PAGE_CLASS_NAMES: {
page: string;
content: string;
header: string;
footer: string;
}Tconst
CSS class names for table elements
TABLE_CLASS_NAMES: {
table: string;
row: string;
cell: string;
cellContent: string;
resizeHandle: string;
rowResizeHandle: string;
tableEdgeHandleBottom: string;
tableEdgeHandleRight: string;
}