UNPKG

pagedjs

Version:

Chunks up a document into paged media flows and applies print styles

791 lines (641 loc) 20.3 kB
import { getBoundingClientRect, getClientRects } from "../utils/utils.js"; import { breakInsideAvoidParentNode, child, cloneNode, findElement, hasContent, indexOf, indexOfTextNode, isContainer, isElement, isText, letters, needsBreakBefore, needsPageBreak, needsPreviousBreakAfter, nodeAfter, nodeBefore, parentOf, prevValidNode, rebuildAncestors, validNode, walk, words } from "../utils/dom.js"; import BreakToken from "./breaktoken.js"; import RenderResult, { OverflowContentError } from "./renderresult.js"; import EventEmitter from "event-emitter"; import Hook from "../utils/hook.js"; const MAX_CHARS_PER_BREAK = 1500; /** * Layout * @class */ class Layout { constructor(element, hooks, options) { this.element = element; this.bounds = this.element.getBoundingClientRect(); if (hooks) { this.hooks = hooks; } else { this.hooks = {}; this.hooks.layout = new Hook(); this.hooks.renderNode = new Hook(); this.hooks.layoutNode = new Hook(); this.hooks.beforeOverflow = new Hook(); this.hooks.onOverflow = new Hook(); this.hooks.afterOverflowRemoved = new Hook(); this.hooks.onBreakToken = new Hook(); } this.settings = options || {}; this.maxChars = this.settings.maxChars || MAX_CHARS_PER_BREAK; this.forceRenderBreak = false; } async renderTo(wrapper, source, breakToken, bounds = this.bounds) { let start = this.getStart(source, breakToken); let walker = walk(start, source); let node; let prevNode; let done; let next; let hasRenderedContent = false; let newBreakToken; let length = 0; let prevBreakToken = breakToken || new BreakToken(start); while (!done && !newBreakToken) { next = walker.next(); prevNode = node; node = next.value; done = next.done; if (!node) { this.hooks && this.hooks.layout.trigger(wrapper, this); let imgs = wrapper.querySelectorAll("img"); if (imgs.length) { await this.waitForImages(imgs); } newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken); if (newBreakToken && newBreakToken.equals(prevBreakToken)) { console.warn("Unable to layout item: ", prevNode); return new RenderResult(undefined, new OverflowContentError("Unable to layout item", [prevNode])); } this.rebuildTableFromBreakToken(newBreakToken, wrapper); return new RenderResult(newBreakToken); } this.hooks && this.hooks.layoutNode.trigger(node); // Check if the rendered element has a break set if (hasRenderedContent && this.shouldBreak(node, start)) { this.hooks && this.hooks.layout.trigger(wrapper, this); let imgs = wrapper.querySelectorAll("img"); if (imgs.length) { await this.waitForImages(imgs); } newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken); if (!newBreakToken) { newBreakToken = this.breakAt(node); } if (newBreakToken && newBreakToken.equals(prevBreakToken)) { console.warn("Unable to layout item: ", node); return new RenderResult(undefined, new OverflowContentError("Unable to layout item", [node])); } this.rebuildTableFromBreakToken(newBreakToken, wrapper); length = 0; break; } if (node.dataset && node.dataset.page) { let named = node.dataset.page; let page = this.element.closest(".pagedjs_page"); page.classList.add("pagedjs_named_page"); page.classList.add("pagedjs_" + named + "_page"); if (!node.dataset.splitFrom) { page.classList.add("pagedjs_" + named + "_first_page"); } } // Should the Node be a shallow or deep clone let shallow = isContainer(node); let rendered = this.append(node, wrapper, breakToken, shallow); length += rendered.textContent.length; // Check if layout has content yet if (!hasRenderedContent) { hasRenderedContent = hasContent(node); } // Skip to the next node if a deep clone was rendered if (!shallow) { walker = walk(nodeAfter(node, source), source); } if (this.forceRenderBreak) { this.hooks && this.hooks.layout.trigger(wrapper, this); newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken); if (!newBreakToken) { newBreakToken = this.breakAt(node); } this.rebuildTableFromBreakToken(newBreakToken, wrapper); length = 0; this.forceRenderBreak = false; break; } // Only check x characters if (length >= this.maxChars) { this.hooks && this.hooks.layout.trigger(wrapper, this); let imgs = wrapper.querySelectorAll("img"); if (imgs.length) { await this.waitForImages(imgs); } newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken); if (newBreakToken && newBreakToken.equals(prevBreakToken)) { console.warn("Unable to layout item: ", node); return new RenderResult(undefined, new OverflowContentError("Unable to layout item", [node])); } if (newBreakToken) { length = 0; this.rebuildTableFromBreakToken(newBreakToken, wrapper); } } } return new RenderResult(newBreakToken); } breakAt(node, offset = 0) { let newBreakToken = new BreakToken( node, offset ); let breakHooks = this.hooks.onBreakToken.triggerSync(newBreakToken, undefined, node, this); breakHooks.forEach((newToken) => { if (typeof newToken != "undefined") { newBreakToken = newToken; } }); return newBreakToken; } shouldBreak(node, limiter) { let previousNode = nodeBefore(node, limiter); let parentNode = node.parentNode; let parentBreakBefore = needsBreakBefore(node) && parentNode && !previousNode && needsBreakBefore(parentNode); let doubleBreakBefore; if (parentBreakBefore) { doubleBreakBefore = node.dataset.breakBefore === parentNode.dataset.breakBefore; } return !doubleBreakBefore && needsBreakBefore(node) || needsPreviousBreakAfter(node) || needsPageBreak(node, previousNode); } forceBreak() { this.forceRenderBreak = true; } getStart(source, breakToken) { let start; let node = breakToken && breakToken.node; if (node) { start = node; } else { start = source.firstChild; } return start; } append(node, dest, breakToken, shallow = true, rebuild = true) { let clone = cloneNode(node, !shallow); if (node.parentNode && isElement(node.parentNode)) { let parent = findElement(node.parentNode, dest); // Rebuild chain if (parent) { parent.appendChild(clone); } else if (rebuild) { let fragment = rebuildAncestors(node); parent = findElement(node.parentNode, fragment); if (!parent) { dest.appendChild(clone); } else if (breakToken && isText(breakToken.node) && breakToken.offset > 0) { clone.textContent = clone.textContent.substring(breakToken.offset); parent.appendChild(clone); } else { parent.appendChild(clone); } dest.appendChild(fragment); } else { dest.appendChild(clone); } } else { dest.appendChild(clone); } if (clone.dataset && clone.dataset.ref) { if (!dest.indexOfRefs) { dest.indexOfRefs = {}; } dest.indexOfRefs[clone.dataset.ref] = clone; } let nodeHooks = this.hooks.renderNode.triggerSync(clone, node, this); nodeHooks.forEach((newNode) => { if (typeof newNode != "undefined") { clone = newNode; } }); return clone; } rebuildTableFromBreakToken(breakToken, dest) { if (!breakToken || !breakToken.node) { return; } let node = breakToken.node; let td = isElement(node) ? node.closest("td") : node.parentElement.closest("td"); if (td) { let rendered = findElement(td, dest, true); if (!rendered) { return; } while ((td = td.nextElementSibling)) { this.append(td, dest, null, true); } } } async waitForImages(imgs) { let results = Array.from(imgs).map(async (img) => { return this.awaitImageLoaded(img); }); await Promise.all(results); } async awaitImageLoaded(image) { return new Promise(resolve => { if (image.complete !== true) { image.onload = function () { let {width, height} = window.getComputedStyle(image); resolve(width, height); }; image.onerror = function (e) { let {width, height} = window.getComputedStyle(image); resolve(width, height, e); }; } else { let {width, height} = window.getComputedStyle(image); resolve(width, height); } }); } avoidBreakInside(node, limiter) { let breakNode; if (node === limiter) { return; } while (node.parentNode) { node = node.parentNode; if (node === limiter) { break; } if (window.getComputedStyle(node)["break-inside"] === "avoid") { breakNode = node; break; } } return breakNode; } createBreakToken(overflow, rendered, source) { let container = overflow.startContainer; let offset = overflow.startOffset; let node, renderedNode, parent, index, temp; if (isElement(container)) { temp = child(container, offset); if (isElement(temp)) { renderedNode = findElement(temp, rendered); if (!renderedNode) { // Find closest element with data-ref let prevNode = prevValidNode(temp); if (!isElement(prevNode)) { prevNode = prevNode.parentElement; } renderedNode = findElement(prevNode, rendered); // Check if temp is the last rendered node at its level. if (!temp.nextSibling) { // We need to ensure that the previous sibling of temp is fully rendered. const renderedNodeFromSource = findElement(renderedNode, source); const walker = document.createTreeWalker(renderedNodeFromSource, NodeFilter.SHOW_ELEMENT); const lastChildOfRenderedNodeFromSource = walker.lastChild(); const lastChildOfRenderedNodeMatchingFromRendered = findElement(lastChildOfRenderedNodeFromSource, rendered); // Check if we found that the last child in source if (!lastChildOfRenderedNodeMatchingFromRendered) { // Pending content to be rendered before virtual break token return; } // Otherwise we will return a break token as per below } // renderedNode is actually the last unbroken box that does not overflow. // Break Token is therefore the next sibling of renderedNode within source node. node = findElement(renderedNode, source).nextSibling; offset = 0; } else { node = findElement(renderedNode, source); offset = 0; } } else { renderedNode = findElement(container, rendered); if (!renderedNode) { renderedNode = findElement(prevValidNode(container), rendered); } parent = findElement(renderedNode, source); index = indexOfTextNode(temp, parent); // No seperatation for the first textNode of an element if(index === 0) { node = parent; offset = 0; } else { node = child(parent, index); offset = 0; } } } else { renderedNode = findElement(container.parentNode, rendered); if (!renderedNode) { renderedNode = findElement(prevValidNode(container.parentNode), rendered); } parent = findElement(renderedNode, source); index = indexOfTextNode(container, parent); if (index === -1) { return; } node = child(parent, index); offset += node.textContent.indexOf(container.textContent); } if (!node) { return; } return new BreakToken( node, offset ); } findBreakToken(rendered, source, bounds = this.bounds, prevBreakToken, extract = true) { let overflow = this.findOverflow(rendered, bounds); let breakToken, breakLetter; let overflowHooks = this.hooks.onOverflow.triggerSync(overflow, rendered, bounds, this); overflowHooks.forEach((newOverflow) => { if (typeof newOverflow != "undefined") { overflow = newOverflow; } }); if (overflow) { breakToken = this.createBreakToken(overflow, rendered, source); // breakToken is nullable let breakHooks = this.hooks.onBreakToken.triggerSync(breakToken, overflow, rendered, this); breakHooks.forEach((newToken) => { if (typeof newToken != "undefined") { breakToken = newToken; } }); // Stop removal if we are in a loop if (breakToken && breakToken.equals(prevBreakToken)) { return breakToken; } if (breakToken && breakToken["node"] && breakToken["offset"] && breakToken["node"].textContent) { breakLetter = breakToken["node"].textContent.charAt(breakToken["offset"]); } else { breakLetter = undefined; } if (breakToken && breakToken.node && extract) { let removed = this.removeOverflow(overflow, breakLetter); this.hooks && this.hooks.afterOverflowRemoved.trigger(removed, rendered, this); } } return breakToken; } hasOverflow(element, bounds = this.bounds) { let constrainingElement = element && element.parentNode; // this gets the element, instead of the wrapper for the width workaround let {width} = element.getBoundingClientRect(); let scrollWidth = constrainingElement ? constrainingElement.scrollWidth : 0; return Math.max(Math.floor(width), scrollWidth) > Math.round(bounds.width); } findOverflow(rendered, bounds = this.bounds) { if (!this.hasOverflow(rendered, bounds)) return; let start = Math.round(bounds.left); let end = Math.round(bounds.right); let range; let walker = walk(rendered.firstChild, rendered); // Find Start let next, done, node, offset, skip, breakAvoid, prev, br; while (!done) { next = walker.next(); done = next.done; node = next.value; skip = false; breakAvoid = false; prev = undefined; br = undefined; if (node) { let pos = getBoundingClientRect(node); let left = Math.round(pos.left); let right = Math.floor(pos.right); if (!range && left >= end) { // Check if it is a float let isFloat = false; // Check if the node is inside a break-inside: avoid table cell const insideTableCell = parentOf(node, "TD", rendered); if (insideTableCell && window.getComputedStyle(insideTableCell)["break-inside"] === "avoid") { // breaking inside a table cell produces unexpected result, as a workaround, we forcibly avoid break inside in a cell. prev = insideTableCell; } else if (isElement(node)) { let styles = window.getComputedStyle(node); isFloat = styles.getPropertyValue("float") !== "none"; skip = styles.getPropertyValue("break-inside") === "avoid"; breakAvoid = node.dataset.breakBefore === "avoid" || node.dataset.previousBreakAfter === "avoid"; prev = breakAvoid && nodeBefore(node, rendered); br = node.tagName === "BR" || node.tagName === "WBR"; } let tableRow; if (node.nodeName === "TR") { tableRow = node; } else { tableRow = parentOf(node, "TR", rendered); } if (tableRow) { // honor break-inside="avoid" in parent tbody/thead let container = tableRow.parentElement; if (["TBODY", "THEAD"].includes(container.nodeName)) { let styles = window.getComputedStyle(container); if (styles.getPropertyValue("break-inside") === "avoid") prev = container; } // Check if the node is inside a row with a rowspan const table = parentOf(tableRow, "TABLE", rendered); const rowspan = table.querySelector("[colspan]"); if (table && rowspan) { let columnCount = 0; for (const cell of Array.from(table.rows[0].cells)) { columnCount += parseInt(cell.getAttribute("colspan") || "1"); } if (tableRow.cells.length !== columnCount) { let previousRow = tableRow.previousElementSibling; let previousRowColumnCount; while (previousRow !== null) { previousRowColumnCount = 0; for (const cell of Array.from(previousRow.cells)) { previousRowColumnCount += parseInt(cell.getAttribute("colspan") || "1"); } if (previousRowColumnCount === columnCount) { break; } previousRow = previousRow.previousElementSibling; } if (previousRowColumnCount === columnCount) { prev = previousRow; } } } } if (prev) { range = document.createRange(); range.selectNode(prev); break; } if (!br && !isFloat && isElement(node)) { range = document.createRange(); range.selectNode(node); break; } if (isText(node) && node.textContent.trim().length) { range = document.createRange(); range.selectNode(node); break; } } if (!range && isText(node) && node.textContent.trim().length && !breakInsideAvoidParentNode(node.parentNode)) { let rects = getClientRects(node); let rect; left = 0; for (var i = 0; i != rects.length; i++) { rect = rects[i]; if (rect.width > 0 && (!left || rect.left > left)) { left = rect.left; } } if (left >= end) { range = document.createRange(); offset = this.textBreak(node, start, end); if (!offset) { range = undefined; } else { range.setStart(node, offset); } break; } } // Skip children if (skip || right <= end) { next = nodeAfter(node, rendered); if (next) { walker = walk(next, rendered); } } } } // Find End if (range) { range.setEndAfter(rendered.lastChild); return range; } } findEndToken(rendered, source, bounds = this.bounds) { if (rendered.childNodes.length === 0) { return; } let lastChild = rendered.lastChild; let lastNodeIndex; while (lastChild && lastChild.lastChild) { if (!validNode(lastChild)) { // Only get elements with refs lastChild = lastChild.previousSibling; } else if (!validNode(lastChild.lastChild)) { // Deal with invalid dom items lastChild = prevValidNode(lastChild.lastChild); break; } else { lastChild = lastChild.lastChild; } } if (isText(lastChild)) { if (lastChild.parentNode.dataset.ref) { lastNodeIndex = indexOf(lastChild); lastChild = lastChild.parentNode; } else { lastChild = lastChild.previousSibling; } } let original = findElement(lastChild, source); if (lastNodeIndex) { original = original.childNodes[lastNodeIndex]; } let after = nodeAfter(original); return this.breakAt(after); } textBreak(node, start, end) { let wordwalker = words(node); let left = 0; let right = 0; let word, next, done, pos; let offset; while (!done) { next = wordwalker.next(); word = next.value; done = next.done; if (!word) { break; } pos = getBoundingClientRect(word); left = Math.floor(pos.left); right = Math.floor(pos.right); if (left >= end) { offset = word.startOffset; break; } if (right > end) { let letterwalker = letters(word); let letter, nextLetter, doneLetter; while (!doneLetter) { nextLetter = letterwalker.next(); letter = nextLetter.value; doneLetter = nextLetter.done; if (!letter) { break; } pos = getBoundingClientRect(letter); left = Math.floor(pos.left); if (left >= end) { offset = letter.startOffset; done = true; break; } } } } return offset; } removeOverflow(overflow, breakLetter) { let {startContainer} = overflow; let extracted = overflow.extractContents(); this.hyphenateAtBreak(startContainer, breakLetter); return extracted; } hyphenateAtBreak(startContainer, breakLetter) { if (isText(startContainer)) { let startText = startContainer.textContent; let prevLetter = startText[startText.length - 1]; // Add a hyphen if previous character is a letter or soft hyphen if ( (breakLetter && /^\w|\u00AD$/.test(prevLetter) && /^\w|\u00AD$/.test(breakLetter)) || (!breakLetter && /^\w|\u00AD$/.test(prevLetter)) ) { startContainer.parentNode.classList.add("pagedjs_hyphen"); startContainer.textContent += this.settings.hyphenGlyph || "\u2011"; } } } equalTokens(a, b) { if (!a || !b) { return false; } if (a["node"] && b["node"] && a["node"] !== b["node"]) { return false; } if (a["offset"] && b["offset"] && a["offset"] !== b["offset"]) { return false; } return true; } } EventEmitter(Layout.prototype); export default Layout;