1111 * @module presentation-editor/header-footer/HeaderFooterSessionManager
1212 */
1313
14- import type { Layout , FlowBlock , Measure , Page , SectionMetadata , Fragment } from '@superdoc/contracts' ;
14+ import type {
15+ Layout ,
16+ FlowBlock ,
17+ Measure ,
18+ Page ,
19+ SectionMetadata ,
20+ Fragment ,
21+ ResolvedHeaderFooterLayout ,
22+ ResolvedPaintItem ,
23+ } from '@superdoc/contracts' ;
1524import type { PageDecorationProvider } from '@superdoc/painter-dom' ;
25+ import { resolveHeaderFooterLayout } from '@superdoc/layout-resolved' ;
1626import type { HeaderFooterPartStoryLocator } from '@superdoc/document-api' ;
1727import { DOM_CLASS_NAMES } from '@superdoc/dom-contract' ;
1828
@@ -339,6 +349,55 @@ type HeaderFooterActivationOptions = {
339349 initialSelection ?: 'end' | 'defer' ;
340350} ;
341351
352+ // =============================================================================
353+ // Helpers
354+ // =============================================================================
355+
356+ /**
357+ * Resolve a `HeaderFooterLayoutResult` into a `ResolvedHeaderFooterLayout`.
358+ * Paired with the originals so the decoration provider can deliver aligned
359+ * `items` alongside `fragments`.
360+ */
361+ function resolveResult ( result : HeaderFooterLayoutResult ) : ResolvedHeaderFooterLayout {
362+ return resolveHeaderFooterLayout ( result . layout , result . blocks , result . measures ) ;
363+ }
364+
365+ function shiftResolvedPaintItemY ( item : ResolvedPaintItem , yOffset : number ) : ResolvedPaintItem {
366+ if ( item . kind === 'group' ) {
367+ return {
368+ ...item ,
369+ y : item . y + yOffset ,
370+ children : item . children . map ( ( child ) => shiftResolvedPaintItemY ( child , yOffset ) ) ,
371+ } ;
372+ }
373+
374+ return {
375+ ...item ,
376+ y : item . y + yOffset ,
377+ } ;
378+ }
379+
380+ function normalizeDecorationFragments ( fragments : Fragment [ ] , layoutMinY : number ) : Fragment [ ] {
381+ if ( layoutMinY >= 0 ) {
382+ return fragments ;
383+ }
384+
385+ const yOffset = - layoutMinY ;
386+ return fragments . map ( ( fragment ) => ( { ...fragment , y : fragment . y + yOffset } ) ) ;
387+ }
388+
389+ function normalizeDecorationItems (
390+ items : ResolvedPaintItem [ ] | undefined ,
391+ layoutMinY : number ,
392+ ) : ResolvedPaintItem [ ] | undefined {
393+ if ( ! items || layoutMinY >= 0 ) {
394+ return items ;
395+ }
396+
397+ const yOffset = - layoutMinY ;
398+ return items . map ( ( item ) => shiftResolvedPaintItemY ( item , yOffset ) ) ;
399+ }
400+
342401// =============================================================================
343402// HeaderFooterSessionManager
344403// =============================================================================
@@ -365,6 +424,12 @@ export class HeaderFooterSessionManager {
365424 #headerLayoutsByRId: Map < string , HeaderFooterLayoutResult > = new Map ( ) ;
366425 #footerLayoutsByRId: Map < string , HeaderFooterLayoutResult > = new Map ( ) ;
367426
427+ // Resolved layouts (aligned 1:1 with the results above)
428+ #resolvedHeaderLayouts: ResolvedHeaderFooterLayout [ ] | null = null ;
429+ #resolvedFooterLayouts: ResolvedHeaderFooterLayout [ ] | null = null ;
430+ #resolvedHeaderByRId: Map < string , ResolvedHeaderFooterLayout > = new Map ( ) ;
431+ #resolvedFooterByRId: Map < string , ResolvedHeaderFooterLayout > = new Map ( ) ;
432+
368433 // Decoration providers
369434 #headerDecorationProvider: PageDecorationProvider | undefined ;
370435 #footerDecorationProvider: PageDecorationProvider | undefined ;
@@ -492,6 +557,7 @@ export class HeaderFooterSessionManager {
492557 /** Set header layout results */
493558 set headerLayoutResults ( results : HeaderFooterLayoutResult [ ] | null ) {
494559 this . #headerLayoutResults = results ;
560+ this . #resolvedHeaderLayouts = results ? results . map ( resolveResult ) : null ;
495561 }
496562
497563 /** Footer layout results */
@@ -502,6 +568,7 @@ export class HeaderFooterSessionManager {
502568 /** Set footer layout results */
503569 set footerLayoutResults ( results : HeaderFooterLayoutResult [ ] | null ) {
504570 this . #footerLayoutResults = results ;
571+ this . #resolvedFooterLayouts = results ? results . map ( resolveResult ) : null ;
505572 }
506573
507574 /** Header layouts by rId */
@@ -612,6 +679,8 @@ export class HeaderFooterSessionManager {
612679 ) : void {
613680 this . #headerLayoutResults = headerResults ;
614681 this . #footerLayoutResults = footerResults ;
682+ this . #resolvedHeaderLayouts = headerResults ? headerResults . map ( resolveResult ) : null ;
683+ this . #resolvedFooterLayouts = footerResults ? footerResults . map ( resolveResult ) : null ;
615684 }
616685
617686 /**
@@ -1449,10 +1518,20 @@ export class HeaderFooterSessionManager {
14491518 layout : Layout ,
14501519 sectionMetadata : SectionMetadata [ ] ,
14511520 ) : Promise < void > {
1452- return await layoutPerRIdHeaderFooters ( headerFooterInput , layout , sectionMetadata , {
1521+ await layoutPerRIdHeaderFooters ( headerFooterInput , layout , sectionMetadata , {
14531522 headerLayoutsByRId : this . #headerLayoutsByRId,
14541523 footerLayoutsByRId : this . #footerLayoutsByRId,
14551524 } ) ;
1525+
1526+ // Rebuild resolved maps aligned 1:1 with the raw rId maps.
1527+ this . #resolvedHeaderByRId. clear ( ) ;
1528+ for ( const [ key , result ] of this . #headerLayoutsByRId) {
1529+ this . #resolvedHeaderByRId. set ( key , resolveResult ( result ) ) ;
1530+ }
1531+ this . #resolvedFooterByRId. clear ( ) ;
1532+ for ( const [ key , result ] of this . #footerLayoutsByRId) {
1533+ this . #resolvedFooterByRId. set ( key , resolveResult ( result ) ) ;
1534+ }
14561535 }
14571536
14581537 #computeMetrics(
@@ -2092,6 +2171,8 @@ export class HeaderFooterSessionManager {
20922171 createDecorationProvider ( kind : 'header' | 'footer' , layout : Layout ) : PageDecorationProvider | undefined {
20932172 const results = kind === 'header' ? this . #headerLayoutResults : this . #footerLayoutResults;
20942173 const layoutsByRId = kind === 'header' ? this . #headerLayoutsByRId : this . #footerLayoutsByRId;
2174+ const resolvedResults = kind === 'header' ? this . #resolvedHeaderLayouts : this . #resolvedFooterLayouts;
2175+ const resolvedByRId = kind === 'header' ? this . #resolvedHeaderByRId : this . #resolvedFooterByRId;
20952176
20962177 if ( ( ! results || results . length === 0 ) && ( ! layoutsByRId || layoutsByRId . size === 0 ) ) {
20972178 return undefined ;
@@ -2166,6 +2247,15 @@ export class HeaderFooterSessionManager {
21662247 const slotPage = this . #findPageForNumber( rIdLayout . layout . pages , pageNumber ) ;
21672248 if ( slotPage ) {
21682249 const fragments = slotPage . fragments ?? [ ] ;
2250+ const resolvedLayout = resolvedByRId . get ( rIdLayoutKey ) ;
2251+ const resolvedSlotPage = resolvedLayout ?. pages . find ( ( p ) => p . number === slotPage . number ) ;
2252+ const resolvedItems = resolvedSlotPage ?. items ;
2253+ if ( resolvedItems && resolvedItems . length !== fragments . length ) {
2254+ console . warn (
2255+ `[HeaderFooterSessionManager] Resolved items length (${ resolvedItems . length } ) does not match fragments length (${ fragments . length } ) for rId '${ rIdLayoutKey } ' page ${ pageNumber } . Dropping items.` ,
2256+ ) ;
2257+ }
2258+ const alignedItems = resolvedItems && resolvedItems . length === fragments . length ? resolvedItems : undefined ;
21692259 const pageHeight = page ?. size ?. h ?? layout . pageSize ?. h ?? layoutOptions . pageSize ?. h ?? defaultPageSize . h ;
21702260 const margins = pageMargins ?? layout . pages [ 0 ] ?. margins ?? layoutOptions . margins ?? defaultMargins ;
21712261 const decorationMargins =
@@ -2180,11 +2270,12 @@ export class HeaderFooterSessionManager {
21802270 const metrics = this . #computeMetrics( kind , rawLayoutHeight , box , pageHeight , margins ?. footer ?? 0 ) ;
21812271
21822272 const layoutMinY = rIdLayout . layout . minY ?? 0 ;
2183- const normalizedFragments =
2184- layoutMinY < 0 ? fragments . map ( ( f ) => ( { ... f , y : f . y - layoutMinY } ) ) : fragments ;
2273+ const normalizedFragments = normalizeDecorationFragments ( fragments , layoutMinY ) ;
2274+ const normalizedItems = normalizeDecorationItems ( alignedItems , layoutMinY ) ;
21852275
21862276 return {
21872277 fragments : normalizedFragments ,
2278+ items : normalizedItems ,
21882279 height : metrics . containerHeight ,
21892280 contentHeight : metrics . layoutHeight > 0 ? metrics . layoutHeight : metrics . containerHeight ,
21902281 offset : metrics . offset ,
@@ -2205,7 +2296,8 @@ export class HeaderFooterSessionManager {
22052296 return null ;
22062297 }
22072298
2208- const variant = results . find ( ( entry ) => entry . type === headerFooterType ) ;
2299+ const variantIndex = results . findIndex ( ( entry ) => entry . type === headerFooterType ) ;
2300+ const variant = variantIndex >= 0 ? results [ variantIndex ] : undefined ;
22092301 if ( ! variant || ! variant . layout ?. pages ?. length ) {
22102302 return null ;
22112303 }
@@ -2216,6 +2308,17 @@ export class HeaderFooterSessionManager {
22162308 }
22172309 const fragments = slotPage . fragments ?? [ ] ;
22182310
2311+ const resolvedVariant = resolvedResults ?. [ variantIndex ] ;
2312+ const resolvedVariantPage = resolvedVariant ?. pages . find ( ( p ) => p . number === slotPage . number ) ;
2313+ const resolvedVariantItems = resolvedVariantPage ?. items ;
2314+ if ( resolvedVariantItems && resolvedVariantItems . length !== fragments . length ) {
2315+ console . warn (
2316+ `[HeaderFooterSessionManager] Resolved items length (${ resolvedVariantItems . length } ) does not match fragments length (${ fragments . length } ) for variant '${ headerFooterType } ' page ${ pageNumber } . Dropping items.` ,
2317+ ) ;
2318+ }
2319+ const alignedVariantItems =
2320+ resolvedVariantItems && resolvedVariantItems . length === fragments . length ? resolvedVariantItems : undefined ;
2321+
22192322 const pageHeight = page ?. size ?. h ?? layout . pageSize ?. h ?? layoutOptions . pageSize ?. h ?? defaultPageSize . h ;
22202323 const margins = pageMargins ?? layout . pages [ 0 ] ?. margins ?? layoutOptions . margins ?? defaultMargins ;
22212324 const decorationMargins =
@@ -2228,10 +2331,12 @@ export class HeaderFooterSessionManager {
22282331 const finalHeaderId = sectionRId ?? fallbackId ?? undefined ;
22292332
22302333 const layoutMinY = variant . layout . minY ?? 0 ;
2231- const normalizedFragments = layoutMinY < 0 ? fragments . map ( ( f ) => ( { ...f , y : f . y - layoutMinY } ) ) : fragments ;
2334+ const normalizedFragments = normalizeDecorationFragments ( fragments , layoutMinY ) ;
2335+ const normalizedItems = normalizeDecorationItems ( alignedVariantItems , layoutMinY ) ;
22322336
22332337 return {
22342338 fragments : normalizedFragments ,
2339+ items : normalizedItems ,
22352340 height : metrics . containerHeight ,
22362341 contentHeight : metrics . layoutHeight > 0 ? metrics . layoutHeight : metrics . containerHeight ,
22372342 offset : metrics . offset ,
@@ -2312,6 +2417,10 @@ export class HeaderFooterSessionManager {
23122417 this . #footerLayoutResults = null ;
23132418 this . #headerLayoutsByRId. clear ( ) ;
23142419 this . #footerLayoutsByRId. clear ( ) ;
2420+ this . #resolvedHeaderLayouts = null ;
2421+ this . #resolvedFooterLayouts = null ;
2422+ this . #resolvedHeaderByRId. clear ( ) ;
2423+ this . #resolvedFooterByRId. clear ( ) ;
23152424
23162425 // Clear decoration providers
23172426 this . #headerDecorationProvider = undefined ;
0 commit comments