vfrag
Version:
Paginate a document by breaking one or more containers vertically into multiple fragments.
167 lines (132 loc) • 3.76 kB
JavaScript
import { affectsLayout, getStyle } from "../util.js";
/**
* A class to manage stacks of nodes for pagination managing both strongly and weakly held nodes.
* Weakly held nodes that are not followed by a strongly held node are removed at the end.
* Not all array methods are implemented, avoid modifying the array with anything other than push, pop, and splice
*/
export default class NodeStack extends Array {
#lengthStrong;
constructor (container) {
if (typeof container === "number") {
super(container);
}
else {
super();
this.container = container;
}
if (this.container) {
this.range = this.container.ownerDocument.createRange();
this.#update();
}
this.#lengthStrong = 0;
// these shouldn't trigger anything by themselves, but should be taken along for the ride if a node after them gets moved
// and should never be the last node in the stack in the final state
// This is just an array of booleans describing the values in #all
this.weak = [];
}
get last () {
return this.at(-1);
}
get firstStrong () {
return this.at(this.weak.indexOf(false));
}
get lastStrong () {
return this.at(-1 - this.weak.slice().reverse().indexOf(false));
}
get lengthStrong () {
return this.#lengthStrong;
}
heights = [];
get height () {
if (this.length === 0) {
return 0;
}
return this.heightAt(-1);
}
heightAt (relativeIndex) {
let index = relativeIndex < 0 ? this.length + relativeIndex : relativeIndex < this.length ? relativeIndex : -1;
if (index === -1) {
throw new Error(`Cannot resolve index ${relativeIndex} in array of length ${this.length}`);
}
let node = this[index];
if (this.heights[index] === undefined) {
// We need to calculate it
this.range.setEndAfter(node);
this.heights[index] = this.range.getBoundingClientRect().height;
this.#update(); // restore range
}
return this.heights[index];
}
/**
* Find the last index that gives us a total height <= maxHeight
*/
indexOfHeight (maxHeight) {
for (let i = this.length - 1; i >= 0; i--) {
if (this.heightAt(i) <= maxHeight) {
return i;
}
}
return -1;
}
#update () {
if (this.length === 0) {
// Clear range start and end
this.range.setStart(this.container, 0);
this.range.setEnd(this.container, 0);
return;
}
this.range.setStartBefore(this[0]);
this.range.setEndAfter(this.last);
}
pushWeak (...nodes) {
this.weak.push(...nodes.map(_ => true));
this.heights.push(...nodes.map(_ => undefined));
let ret = super.push(...nodes);
this.#update();
return ret;
}
push (...nodes) {
this.#lengthStrong += nodes.length;
this.weak.push(...nodes.map(_ => false));
this.heights.push(...nodes.map(_ => undefined));
let ret = super.push(...nodes);
this.#update();
return ret;
}
popWeak () {
let ret = [];
while (this.length > 0 && this.weak.at(-1)) {
this.weak.pop();
ret.push(super.pop());
this.heights.pop();
}
this.#update();
return ret;
}
pop () {
let ret = super.pop();
let wasWeak = this.weak.pop();
this.heights.pop();
if (!wasWeak) {
this.#lengthStrong--;
}
this.#update();
return ret;
}
splice (start, deleteCount, ...nodes) {
let weak = this.weak.splice(start, deleteCount, ...nodes.map(_ => false));
this.heights.splice(start, deleteCount, ...nodes.map(_ => undefined));
let ret = super.splice(start, deleteCount, ...nodes);
ret.weak = weak;
this.#lengthStrong += nodes.length - weak.filter(w => !w).length;
this.#update();
return ret;
}
append (nodeStack) {
this.push(...nodeStack);
this.weak.push(...nodeStack.weak);
this.heights.push(...nodeStack.weak.map(_ => undefined));
this.#lengthStrong += nodeStack.lengthStrong;
this.#update();
}
}