Source: modules/paged-media/position-fixed.js

import Handler from "../handler.js";
import csstree from "css-tree";

/**
 * Handles `position: fixed` elements in paged media.
 *
 * Since fixed positioning does not behave as expected in paged outputs,
 * this handler collects all `position: fixed` elements during parsing,
 * removes them from the flow, and re-inserts them absolutely positioned
 * at the top of every page during layout.
 */
class PositionFixed extends Handler {
	/**
	 * Creates an instance of PositionFixed.
	 *
	 * @param {Object} chunker - The chunker instance used for pagination.
	 * @param {Object} polisher - The polisher instance responsible for styles.
	 * @param {Object} caller - The object coordinating the handlers.
	 */
	constructor(chunker, polisher, caller) {
		super(chunker, polisher, caller);

		/** @type {CSSStyleSheet} */
		this.styleSheet = polisher.styleSheet;

		/**
		 * Array of selectors that match `position: fixed` elements.
		 * @type {string[]}
		 */
		this.fixedElementsSelector = [];

		/**
		 * Collected DOM elements that matched fixed position rules.
		 * These are inserted back on each page during layout.
		 * @type {HTMLElement[]}
		 */
		this.fixedElements = [];
	}

	/**
	 * Intercepts `position: fixed` declarations, saves their selector,
	 * and removes the declaration from the stylesheet.
	 *
	 * @param {Object} declaration - The CSS AST node for the declaration.
	 * @param {Object} dItem - The item in the declaration list.
	 * @param {Object} dList - The full declaration list.
	 * @param {Object} rule - The rule containing this declaration.
	 */
	onDeclaration(declaration, dItem, dList, rule) {
		if (
			declaration.property === "position" &&
			declaration.value.children.first().name === "fixed"
		) {
			let selector = csstree.generate(rule.ruleNode.prelude);
			this.fixedElementsSelector.push(selector);
			dList.remove(dItem);
		}
	}

	/**
	 * After parsing the full document, this method finds all elements
	 * that matched `position: fixed`, applies `position: absolute` instead,
	 * removes them from the DOM flow, and stores them for re-insertion.
	 *
	 * @param {DocumentFragment} fragment - The parsed document fragment.
	 */
	afterParsed(fragment) {
		this.fixedElementsSelector.forEach((fixedEl) => {
			fragment.querySelectorAll(fixedEl).forEach((el) => {
				el.style.setProperty("position", "absolute");
				this.fixedElements.push(el);
				el.remove(); // remove from flow
			});
		});
	}

	/**
	 * After each page layout, this method re-inserts all collected fixed
	 * elements into the top of the page, effectively simulating `position: fixed`.
	 *
	 * @param {HTMLElement} pageElement - The DOM element for the current page.
	 * @param {Object} page - Metadata or state for the current page.
	 * @param {Object} breakToken - Information about content breaks (unused here).
	 */
	afterPageLayout(pageElement, page, breakToken) {
		this.fixedElements.forEach((el) => {
			const clone = el.cloneNode(true);
			pageElement
				.querySelector(".pagedjs_pagebox")
				.insertAdjacentElement("afterbegin", clone);
		});
	}
}

export default PositionFixed;