Skip to content

Commit a79892c

Browse files
fix(header-footer): preserve negative minY from page-relative behindDoc media
Stop shifting normal footer/header fragments when the layout's minY is negative purely because of explicit behindDoc anchored drawings/images (e.g. page-relative background shapes). Decoration normalization now computes its own minY that ignores those explicit behindDoc media, so in-flow content stays at its original coordinates while the negative minY is preserved on the payload for downstream painters.
1 parent 6a945da commit a79892c

2 files changed

Lines changed: 136 additions & 10 deletions

File tree

packages/super-editor/src/editors/v1/core/presentation-editor/header-footer/HeaderFooterSessionManager.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -394,21 +394,43 @@ function shiftResolvedPaintItemY(item: ResolvedPaintItem, yOffset: number): Reso
394394
};
395395
}
396396

397-
function normalizeDecorationFragments(fragments: Fragment[], layoutMinY: number): Fragment[] {
398-
if (layoutMinY >= 0) {
397+
function isExplicitBehindDocMediaFragment(fragment: Fragment): boolean {
398+
return (fragment.kind === 'image' || fragment.kind === 'drawing') && fragment.behindDoc === true;
399+
}
400+
401+
function getDecorationNormalizationMinY(fragments: Fragment[], layoutMinY: number): number {
402+
if (!Number.isFinite(layoutMinY) || layoutMinY >= 0) {
403+
return 0;
404+
}
405+
406+
let minY = Infinity;
407+
for (const fragment of fragments) {
408+
if (isExplicitBehindDocMediaFragment(fragment)) {
409+
continue;
410+
}
411+
if (Number.isFinite(fragment.y)) {
412+
minY = Math.min(minY, fragment.y);
413+
}
414+
}
415+
416+
return minY < 0 ? minY : 0;
417+
}
418+
419+
function normalizeDecorationFragments(fragments: Fragment[], normalizationMinY: number): Fragment[] {
420+
if (normalizationMinY >= 0) {
399421
return fragments;
400422
}
401423

402-
const yOffset = -layoutMinY;
424+
const yOffset = -normalizationMinY;
403425
return fragments.map((fragment) => ({ ...fragment, y: fragment.y + yOffset }));
404426
}
405427

406-
function normalizeDecorationItems(items: ResolvedPaintItem[], layoutMinY: number): ResolvedPaintItem[] {
407-
if (layoutMinY >= 0) {
428+
function normalizeDecorationItems(items: ResolvedPaintItem[], normalizationMinY: number): ResolvedPaintItem[] {
429+
if (normalizationMinY >= 0) {
408430
return items;
409431
}
410432

411-
const yOffset = -layoutMinY;
433+
const yOffset = -normalizationMinY;
412434
return items.map((item) => shiftResolvedPaintItemY(item, yOffset));
413435
}
414436

@@ -2446,8 +2468,9 @@ export class HeaderFooterSessionManager {
24462468
const metrics = this.#computeMetrics(kind, rawLayoutHeight, box, pageHeight, margins?.footer ?? 0);
24472469

24482470
const layoutMinY = rIdLayout.layout.minY ?? 0;
2449-
const normalizedFragments = normalizeDecorationFragments(fragments, layoutMinY);
2450-
const normalizedItems = normalizeDecorationItems(alignedItems, layoutMinY);
2471+
const normalizationMinY = getDecorationNormalizationMinY(fragments, layoutMinY);
2472+
const normalizedFragments = normalizeDecorationFragments(fragments, normalizationMinY);
2473+
const normalizedItems = normalizeDecorationItems(alignedItems, normalizationMinY);
24512474
const isActiveHeaderFooter = this.#isActiveDecoration(kind, sectionRId, pageNumber);
24522475

24532476
return {
@@ -2512,8 +2535,9 @@ export class HeaderFooterSessionManager {
25122535
const metrics = this.#computeMetrics(kind, rawLayoutHeight, box, pageHeight, margins?.footer ?? 0);
25132536

25142537
const layoutMinY = variant.layout.minY ?? 0;
2515-
const normalizedFragments = normalizeDecorationFragments(fragments, layoutMinY);
2516-
const normalizedItems = normalizeDecorationItems(alignedVariantItems, layoutMinY);
2538+
const normalizationMinY = getDecorationNormalizationMinY(fragments, layoutMinY);
2539+
const normalizedFragments = normalizeDecorationFragments(fragments, normalizationMinY);
2540+
const normalizedItems = normalizeDecorationItems(alignedVariantItems, normalizationMinY);
25172541
const isActiveHeaderFooter = this.#isActiveDecoration(kind, finalHeaderId, pageNumber);
25182542

25192543
return {

packages/super-editor/src/editors/v1/core/presentation-editor/tests/HeaderFooterSessionManager.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import type {
2222
ParaFragment,
2323
ResolvedLayout,
2424
ResolvedPage,
25+
TableFragment,
26+
DrawingFragment,
2527
} from '@superdoc/contracts';
2628
import { buildMultiSectionIdentifier, type HeaderFooterLayoutResult } from '@superdoc/layout-bridge';
2729
import {
@@ -779,6 +781,106 @@ describe('HeaderFooterSessionManager', () => {
779781
expect(payload!.items![0]).toMatchObject({ blockId: 'p1', x: 72, y: 0 });
780782
});
781783

784+
it('does not shift normal rId footer fragments for negative minY from page-relative behindDoc drawings', () => {
785+
const deps: SessionManagerDependencies = {
786+
getLayoutOptions: vi.fn(() => ({})),
787+
getPageElement: vi.fn(() => null),
788+
scrollPageIntoView: vi.fn(),
789+
waitForPageMount: vi.fn(async () => true),
790+
convertPageLocalToOverlayCoords: vi.fn(() => ({ x: 0, y: 0 })),
791+
isViewLocked: vi.fn(() => false),
792+
getBodyPageHeight: vi.fn(() => 800),
793+
notifyInputBridgeTargetChanged: vi.fn(),
794+
scheduleRerender: vi.fn(),
795+
setPendingDocChange: vi.fn(),
796+
getBodyPageCount: vi.fn(() => 1),
797+
};
798+
const tableFragment: TableFragment = {
799+
kind: 'table',
800+
blockId: 'footer-table',
801+
fromRow: 0,
802+
toRow: 1,
803+
x: 72,
804+
y: 0,
805+
width: 468,
806+
height: 24,
807+
};
808+
const behindDocFragment: DrawingFragment = {
809+
kind: 'drawing',
810+
blockId: 'footer-bg',
811+
drawingKind: 'vectorShape',
812+
x: 0,
813+
y: -36,
814+
width: 612,
815+
height: 120,
816+
isAnchored: true,
817+
behindDoc: true,
818+
zIndex: 0,
819+
geometry: { width: 612, height: 120 },
820+
scale: 1,
821+
sourceAnchor: { vRelativeFrom: 'page' },
822+
} as DrawingFragment;
823+
const footerResult: HeaderFooterLayoutResult = {
824+
kind: 'footer',
825+
type: 'default',
826+
layout: {
827+
height: 48,
828+
minY: -36,
829+
pages: [{ number: 1, fragments: [tableFragment, behindDocFragment] }],
830+
},
831+
blocks: [
832+
{ kind: 'table', id: 'footer-table', rows: [{ id: 'row-1', cells: [] }] },
833+
{
834+
kind: 'drawing',
835+
id: 'footer-bg',
836+
drawingKind: 'vectorShape',
837+
anchor: { isAnchored: true, vRelativeFrom: 'page', behindDoc: true },
838+
geometry: { width: 612, height: 120 },
839+
},
840+
] as FlowBlock[],
841+
measures: [
842+
{ kind: 'table', rowHeights: [24], columnWidths: [468], cells: [], rows: [] },
843+
{ kind: 'drawing', width: 612, height: 120 },
844+
] as unknown as Measure[],
845+
};
846+
847+
manager = new HeaderFooterSessionManager({
848+
painterHost,
849+
visibleHost,
850+
selectionOverlay,
851+
editor: createMainEditorStub(),
852+
defaultPageSize: { w: 612, h: 792 },
853+
defaultMargins: { top: 72, right: 72, bottom: 72, left: 72, header: 36, footer: 36 },
854+
});
855+
manager.setDependencies(deps);
856+
manager.headerFooterIdentifier = {
857+
headerIds: { default: null, first: null, even: null, odd: null },
858+
footerIds: { default: 'rId-footer-default', first: null, even: null, odd: null },
859+
titlePg: false,
860+
alternateHeaders: false,
861+
};
862+
manager.footerLayoutsByRId.set('rId-footer-default', footerResult);
863+
864+
const layout: Layout = {
865+
version: 1,
866+
flowMode: 'paginated',
867+
pageGap: 0,
868+
pageSize: { w: 612, h: 792 },
869+
pages: [{ number: 1, margins: { top: 72, right: 72, bottom: 72, left: 72, header: 36, footer: 36 } } as never],
870+
} as unknown as Layout;
871+
const provider = manager.createDecorationProvider('footer', layout as unknown as ResolvedLayout);
872+
const payload = provider!(1, layout.pages[0]!.margins, layout.pages[0] as unknown as ResolvedPage);
873+
874+
expect(payload).not.toBeNull();
875+
expect(payload!.minY).toBe(-36);
876+
expect(payload!.fragments).toHaveLength(2);
877+
expect(payload!.fragments[0]).toMatchObject({ kind: 'table', blockId: 'footer-table', y: 0 });
878+
expect(payload!.fragments[1]).toMatchObject({ kind: 'drawing', blockId: 'footer-bg', y: -36, behindDoc: true });
879+
expect(payload!.items).toHaveLength(2);
880+
expect(payload!.items![0]).toMatchObject({ fragmentKind: 'table', blockId: 'footer-table', y: 0 });
881+
expect(payload!.items![1]).toMatchObject({ fragmentKind: 'drawing', blockId: 'footer-bg', y: -36 });
882+
});
883+
782884
it('uses section titlePg state when selecting decoration-provider variants', () => {
783885
const deps: SessionManagerDependencies = {
784886
getLayoutOptions: vi.fn(() => ({})),

0 commit comments

Comments
 (0)