• documentation
  • examples
  • about
  • FAQ
  • journal
  • Gitlab

Handlers, Hooks and custom javascript

written on Jan 28, 2020

You’re now fluent in paged media specs, that quite awesome :). But you may want to do more with the scripts. Here is how you can update some of the script to do what you’d like to do.

Paged.js is built as a collection of module that run codes at different moments while creating the layout. This can be helpful if you want to transform long hyperlinks adresses to truncable ones before paged.js look at the content, or if you want to pass some script to balance the amount of lines your titles need once the title is put on the page.

To do that, we need to register a module, and define when its code should be processed.

Modules

Modules are groups of handlers for that apply the layout and styles of a CSS module, such as Generated Content.

New handlers can be registered from import { registerHandlers } from 'pagedjs' or by calling Paged.registerHandlers on an html page.

<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"></script>
<script>
class MyHandler extends Paged.Handler {
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
}

afterPageLayout(pageFragment, page) {
console.log(pageFragment);
}
}
Paged.registerHandlers(MyHandler);
</script>

Handlers have methods that correspond to the hooks for the parsing, layout and rendering of the Chunker and Polisher. Returning an promise or async function from a method in a handler will complete that task before continuing with the other registered methods for that hook.

// Previewer
beforePreview(content, renderTo);
afterPreview(pages);

// Chunker
beforeParsed(content);
afterParsed(parsed);
beforePageLayout(page);
afterPageLayout(pageElement, page, breakToken);
afterRendered(pages);

// Polisher
beforeTreeParse(text, sheet);
beforeTreeWalk(ast);
afterTreeWalk(ast, sheet);
onUrl(urlNode);
onAtPage(atPageNode);
onRule(ruleNode);
onDeclaration(declarationNode, ruleNode);
onContent(contentNode, declarationNode, ruleNode);

// Layout
layoutNode(node);
renderNode(node, sourceNode);
onOverflow(overflow, rendered, bounds);
onBreakToken(breakToken, overflow, rendered);

Some informations about the hooks

Here is some information about some hooks, the documentation remains to be completed. Their execution order is not quite the same as the one presented and corresponds rather to the different modules that constitute Paged.js

Previewer

Hook Hooks order Attributes
beforePreview(content, renderTo) Before pagedjs is launched [Warning : event does not trigger]
afterPreview(pages) After PagedJS finished running (check afterRendered(pages) pages → array that contains all page nodes

Chuncker

Hook Hooks order Attributes
beforeParsed(content) runs on content before it is parsed and given IDs content → document-fragment made from the original DOM
afterParsed(parsed) runs after the content has been parsed but before rendering has started parsed → content once parsed and given ids (data-ref and break rules from the css)
beforePageLayout(page) runs when a new page has been created page → node of the page that’s gonna be created
afterPageLayout(pageElement, page, breakToken) runs after a single page has gone through layout, and allows adjusting the breakToken pageElement → page element that just been rendered,
page → node of the page being rendered,
breakToken → location of the beginning of the overflow
afterRendered(pages) runs after all pages have finished rendering pages → array that contains all page nodes

Polisher

Hook Hooks order Attributes
beforeTreeParse(text, sheet) runs every time a style element or a link to a css file is called (worked also on @import) text → innerText of the css,
sheet → cssTree parsed object (AST object)
beforeTreeWalk(ast) runs before the stylesheet has been parsed by csstree ast → csstree parsed object
afterTreeWalk(ast, sheet) runs after the stylesheet has been parsed by csstree ast → csstree parsed object,
sheet → object that contains text and ast
onUrl(urlNode) runs any time a CSS URL is parsed urlNode → url origin
onAtPage(atPageNode) runs any time a CSS @page is parsed
onRule(ruleNode) runs any time a CSS rule is parsed
onDeclaration(declaration, dItem, dList, rule) runs any time a CSS declaration is parsed
onContent(contentNode, declarationNode, ruleNode) run every time a css content property is found

Layout

Hook Hooks order Attributes
layoutNode(node) When a node is layed out
(Tips: layoutNode is the original node from the content)
renderNode(node, sourceNode) When a node is rendered
(Tips: renderNode is the cloned node that is added to the DOM)
onOverflow(overflow, rendered, bounds) When an element overflows: when it’s bigger than the remaining space on the page
onBreakToken(breakToken, overflow, rendered) When a breakToken is defined

Some examples

cleaning hyperlinks for the print

HTML5 has this unknown element called wbr which represents a line break opportunity, which you can translate into “I don’t have enough space to put the line of text, but thanksfully, i have those small element that i can cut!” Using this, we wouldn’t have any problem with line flowing outside of the page, or arbitrary cuts in the text that we want to avoid. Exactly what we need.

So let’s say that we have some written down hyperlinks that we want to clean and make ready for printing. Let’s create a new Paged.Handler

class linkCleaning extends Paged.Handler {
// this let us call the methods from the the chunker, the polisher and the caller for the rest of the script
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
}

beforeParsed(content) {
// Before the content is parsed by Paged.js, please do the follwing:

// first, look for all the links <a> that are referencing a link started by http or www
const links = content.querySelectorAll('a[href^="http"], a[href^="www"]');
// for each of those links,
links.forEach((link) => {
// Break after a colon or a double slash (//) or before a single slash (/), a tilde (~), a period, a comma, a hyphen, an underline (_), a question mark, a number sign, or a percent symbol.
const content = link.textContent;
let printableUrl = content.replace(/\/\//g, "//\u003Cwbr\u003E");
printableUrl = printableUrl.replace(/\,/g, ",\u003Cwbr\u003E");
// put a <wbr> element around to define where to break the line.
printableUrl = printableUrl.replace(
/(\/|\~|\-|\.|\,|\_|\?|\#|\%)/g,
"\u003Cwbr\u003E$1"
);
// turn hyphen in non breaking hyphen
printableUrl = printableUrl.replace(/\-/g, "\u003Cwbr\u003E&#x2011;");
// add a data-print-url to keep track of the previous link
link.setAttribute("data-print-url", content);
// modify the inner text of the link
link.innerHTML = printableUrl;
});
}
}

// and we don’t forget to register the handler like this

Paged.registerHandlers(linkCleaning);

Calling this script right after paged.js will run it before the content is parsed.

Adding a class to an image based on its ratio

class imageRatio extends Paged.Handler {
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
}

afterParsed(parsed) {
// create an array that will store the images data later on
let imagePromises = [];
// find all images parsed by paged.js
let images = parsed.querySelectorAll("img");
// for each image
images.forEach((image) => {
// load the image as an object
let img = new Image();
// test if the image is loaded
let resolve, reject;
let imageLoaded = new Promise(function (r, x) {
resolve = r;
reject = x;
});
// when the image loads
img.onload = function () {
// find its height
let height = img.naturalHeight;

// find its width
let width = img.naturalWidth;

// calculate the ratio
let ratio = width / height;

// if the ratio is superior than 1.4, set it as a lanscape adn add a class to the image (and to the parent figure)
if (ratio >= 1.4) {
image.classList.add("landscape");
image.parentNode.classList.add("fig-landscape");
}
// if the ratio is inferior than 0.8, set it as a portrait adn add a class to the image (and to the parent figure)
else if (ratio <= 0.8) {
image.classList.add("portrait");
image.parentNode.classList.add("fig-portrait");
}
// else, if it’s between 1.39 and 0.8, add a “square” class.
else if (ratio < 1.39 || ratio > 0.8) {
image.classList.add("square");
image.parentNode.classList.add("fig-square");
}
// resolve the promise
resolve();
};
// if there is an error, reject the promise
img.onerror = function () {
reject();
};

img.src = image.src;

imagePromises.push(imageLoaded);
});

return Promise.all(imagePromises).catch((err) => {
console.warn(err);
});
}
}

// and we register the handler

Paged.registerHandlers(imageRatio);
Cross References

Paged.js is maintained by the nice folks at Coko.