Commit 6607d914 authored by Fred Chasen's avatar Fred Chasen

Switch layout to be an async function

parent 27fe900a
......@@ -2,10 +2,16 @@ import Page from "./page";
import ContentParser from "./parser";
import EventEmitter from "event-emitter";
import Hook from "../utils/hook";
import Queue from "../utils/queue";
import {
needsBreakBefore,
needsBreakAfter
} from "../utils/dom";
import {
requestIdleCallback,
defer
} from "../utils/utils";
const MAX_PAGES = false;
const TEMPLATE = `<div class="pagedjs_page">
......@@ -48,8 +54,6 @@ const TEMPLATE = `<div class="pagedjs_page">
</div>
</div>`;
const _requestIdleCallback = 'requestIdleCallback' in window ? requestIdleCallback : requestAnimationFrame;
/**
* Chop up text into flows
* @class
......@@ -72,6 +76,9 @@ class Chunker {
this.pages = [];
this._total = 0;
this.q = new Queue(this);
this.stopped = false;
this.content = content;
if (content) {
......@@ -102,18 +109,28 @@ class Chunker {
parsed = new ContentParser(content);
this.source = parsed;
this.breakToken = undefined;
this.setup(renderTo);
if (this.pagesArea && this.pageTemplate) {
this.q.clear();
this.removePages();
} else {
this.setup(renderTo);
}
this.emit("rendering", content);
await this.hooks.afterParsed.trigger(parsed, this);
if (typeof document.fonts.ready !== "undefined") {
await document.fonts.ready;
await this.loadFonts();
let rendered = await this.render(parsed, this.breakToken);
while (rendered.canceled) {
this.start();
rendered = await this.render(parsed, this.breakToken);
}
await this.render(parsed, renderTo);
this.rendered = true;
await this.hooks.afterRendered.trigger(this.pages, this);
......@@ -122,25 +139,70 @@ class Chunker {
return this;
}
async render(parsed, renderTo) {
let renderer = this.layout(parsed);
// oversetPages() {
// let overset = [];
// for (let i = 0; i < this.pages.length; i++) {
// let page = this.pages[i];
// if (page.overset) {
// overset.push(page);
// // page.overset = false;
// }
// }
// return overset;
// }
//
// async handleOverset(parsed) {
// let overset = this.oversetPages();
// if (overset.length) {
// console.log("overset", overset);
// let index = this.pages.indexOf(overset[0]) + 1;
// console.log("INDEX", index);
//
// // Remove pages
// // this.removePages(index);
//
// // await this.render(parsed, overset[0].overset);
//
// // return this.handleOverset(parsed);
// }
// }
async render(parsed, startAt) {
let renderer = this.layout(parsed, startAt);
let done = false;
let result;
while (!done) {
result = await this.renderOnIdle(renderer);
result = await this.q.enqueue(async () => { return this.renderOnIdle(renderer) });
done = result.done;
}
return this;
return result;
}
start() {
this.rendered = false;
this.stopped = false;
}
stop() {
this.stopped = true;
this.q.clear();
}
renderOnIdle(renderer) {
return new Promise(resolve => {
_requestIdleCallback(() => {
let result = renderer.next();
resolve(result);
requestIdleCallback(async () => {
if (this.stopped) {
return resolve({ done: true, canceled: true });
}
let result = await renderer.next();
if (this.stopped) {
resolve({ done: true, canceled: true });
} else {
resolve(result);
}
});
});
}
......@@ -197,8 +259,8 @@ class Chunker {
}
}
async *layout(content) {
let breakToken = false;
async *layout(content, startAt) {
let breakToken = startAt || false;
while (breakToken !== undefined && (MAX_PAGES ? this.total < MAX_PAGES : true)) {
......@@ -214,9 +276,7 @@ class Chunker {
this.emit("page", page);
// Layout content in the page, starting from the breakToken
breakToken = page.layout(content, breakToken);
// await this.hooks.layout.trigger(page.element, page, breakToken, this);
breakToken = await page.layout(content, breakToken);
await this.hooks.afterPageLayout.trigger(page.element, page, breakToken, this);
this.emit("renderedPage", page);
......@@ -225,8 +285,24 @@ class Chunker {
// Stop if we get undefined, showing we have reached the end of the content
}
}
this.rendered = true;
removePages(fromIndex=0) {
if (fromIndex >= this.pages.length) {
return;
}
// Remove pages
for (let i = fromIndex; i < this.pages.length; i++) {
this.pages[i].destroy();
}
if (fromIndex > 0) {
this.pages.splice(fromIndex);
} else {
this.pages = [];
}
}
addPage(blank) {
......@@ -245,16 +321,24 @@ class Chunker {
page.onOverflow((overflowToken) => {
// console.log("overflow on", page.id, overflowToken);
let index = this.pages.indexOf(page) + 1;
if (index < this.pages.length &&
(this.pages[index].breakBefore || this.pages[index].previousBreakAfter)) {
let newPage = this.insertPage(index - 1);
newPage.layout(this.source, overflowToken);
} else if (index < this.pages.length) {
this.pages[index].layout(this.source, overflowToken);
} else {
let newPage = this.addPage();
newPage.layout(this.source, overflowToken);
}
// Stop the rendering
this.stop();
// Set the breakToken to resume at
this.breakToken = overflowToken;
// Remove pages
this.removePages(index);
this.q.enqueue(async () => {
if (this.rendered) {
this.start();
this.render(this.source, this.breakToken);
}
});
});
page.onUnderflow((overflowToken) => {
......@@ -265,11 +349,11 @@ class Chunker {
});
}
this.total += 1;
this.total = this.pages.length;
return page;
}
/*
insertPage(index, blank) {
let lastPage = this.pages[index];
// Create a new page from the template
......@@ -306,6 +390,7 @@ class Chunker {
return page;
}
*/
get total() {
return this._total;
......@@ -316,6 +401,16 @@ class Chunker {
this._total = num;
}
loadFonts() {
let fontPromises = [];
for (let fontFace of document.fonts.values()) {
if (fontFace.status !== "loaded") {
fontPromises.push(fontFace.load());
}
}
return Promise.all(fontPromises);
}
destroy() {
this.pagesArea.remove()
this.pageTemplate.remove();
......
......@@ -55,7 +55,7 @@ class Layout {
}
renderTo(wrapper, source, breakToken, bounds=this.bounds) {
async renderTo(wrapper, source, breakToken, bounds=this.bounds) {
let start = this.getStart(source, breakToken);
let walker = walk(start, source);
......@@ -78,24 +78,18 @@ class Layout {
return newBreakToken;
}
/*
let exists;
if (isElement(node)) {
exists = findElement(node, wrapper);
} else {
exists = false;
}
if (exists) {
console.log("found", exists);
break;
}
*/
this.hooks && this.hooks.layoutNode.trigger(node);
// Check if the rendered element has a break set
if (hasRenderedContent && this.shouldBreak(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);
if (!newBreakToken) {
......@@ -126,6 +120,11 @@ class Layout {
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);
}
......@@ -204,6 +203,31 @@ class Layout {
return clone;
}
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;
......@@ -297,9 +321,9 @@ class Layout {
}
hasOverflow(element, bounds=this.bounds) {
let constrainingElement = element.parentNode; // this gets the element, instead of the wrapper for the width workaround
let constrainingElement = element && element.parentNode; // this gets the element, instead of the wrapper for the width workaround
let { width } = element.getBoundingClientRect();
let { scrollWidth } = constrainingElement;
let scrollWidth = constrainingElement ? constrainingElement.scrollWidth : 0;
return Math.max(Math.floor(width), scrollWidth) > Math.round(bounds.width);
}
......
......@@ -11,8 +11,6 @@ class Page {
this.pageTemplate = pageTemplate;
this.blank = blank;
// this.mapper = new Mapping(undefined, undefined, undefined, true);
this.width = undefined;
this.height = undefined;
......@@ -111,28 +109,34 @@ class Page {
}
*/
layout(contents, breakToken) {
// console.log("layout page", this.id);
async layout(contents, breakToken) {
this.clear();
this.startToken = breakToken;
this.layoutMethod = new Layout(this.area, this.hooks);
breakToken = this.layoutMethod.renderTo(this.wrapper, contents, breakToken);
let newBreakToken = await this.layoutMethod.renderTo(this.wrapper, contents, breakToken);
this.addListeners(contents);
return breakToken;
this.endToken = newBreakToken;
return newBreakToken;
}
append(contents, breakToken) {
async append(contents, breakToken) {
if (!this.layoutMethod) {
return this.layout(contents, breakToken);
}
breakToken = this.layoutMethod.renderTo(this.wrapper, contents, breakToken);
let newBreakToken = await this.layoutMethod.renderTo(this.wrapper, contents, breakToken);
return breakToken;
this.endToken = newBreakToken;
return newBreakToken;
}
getByParent(ref, entries) {
......@@ -170,7 +174,7 @@ class Page {
// TODO: fall back to mutation observer?
// Key scroll width from changing
// Keep scroll left from changing
this.element.addEventListener("scroll", () => {
if(this.listening) {
this.element.scrollLeft = 0;
......@@ -229,6 +233,7 @@ class Page {
let newBreakToken = this.layoutMethod.findBreakToken(this.wrapper, contents);
if (newBreakToken) {
this.endToken = newBreakToken;
this._onOverflow && this._onOverflow(newBreakToken);
}
}
......@@ -247,9 +252,12 @@ class Page {
}
}
destroy() {
this.removeListeners();
this.element.remove();
this.element = undefined;
this.wrapper = undefined;
}
......
import {defer} from "./utils";
/**
* Queue for handling tasks one at a time
* @class
* @param {scope} context what this will resolve to in the tasks
*/
class Queue {
constructor(context){
this._q = [];
this.context = context;
this.tick = requestAnimationFrame;
this.running = false;
this.paused = false;
}
/**
* Add an item to the queue
* @return {Promise}
*/
enqueue() {
var deferred, promise;
var queued;
var task = [].shift.call(arguments);
var args = arguments;
// Handle single args without context
// if(args && !Array.isArray(args)) {
// args = [args];
// }
if(!task) {
throw new Error("No Task Provided");
}
if(typeof task === "function"){
deferred = new defer();
promise = deferred.promise;
queued = {
"task" : task,
"args" : args,
//"context" : context,
"deferred" : deferred,
"promise" : promise
};
} else {
// Task is a promise
queued = {
"promise" : task
};
}
this._q.push(queued);
// Wait to start queue flush
if (this.paused == false && !this.running) {
this.run();
}
return queued.promise;
}
/**
* Run one item
* @return {Promise}
*/
dequeue(){
var inwait, task, result;
if(this._q.length && !this.paused) {
inwait = this._q.shift();
task = inwait.task;
if(task){
// console.log(task)
result = task.apply(this.context, inwait.args);
if(result && typeof result["then"] === "function") {
// Task is a function that returns a promise
return result.then(function(){
inwait.deferred.resolve.apply(this.context, arguments);
}.bind(this), function() {
inwait.deferred.reject.apply(this.context, arguments);
}.bind(this));
} else {
// Task resolves immediately
inwait.deferred.resolve.apply(this.context, result);
return inwait.promise;
}
} else if(inwait.promise) {
// Task is a promise
return inwait.promise;
}
} else {
inwait = new defer();
inwait.deferred.resolve();
return inwait.promise;
}
}
// Run All Immediately
dump(){
while(this._q.length) {
this.dequeue();
}
}
/**
* Run all tasks sequentially, at convince
* @return {Promise}
*/
run(){
if(!this.running){
this.running = true;
this.defered = new defer();
}
this.tick.call(window, () => {
if(this._q.length) {
this.dequeue()
.then(function(){
this.run();
}.bind(this));
} else {
this.defered.resolve();
this.running = undefined;
}
});
// Unpause
if(this.paused == true) {
this.paused = false;
}
return this.defered.promise;
}
/**
* Flush all, as quickly as possible
* @return {Promise}
*/
flush(){
if(this.running){
return this.running;
}
if(this._q.length) {
this.running = this.dequeue()
.then(function(){
this.running = undefined;
return this.flush();
}.bind(this));
return this.running;
}
}
/**
* Clear all items in wait
*/
clear(){
this._q = [];
}
/**
* Get the number of tasks in the queue
* @return {number} tasks
*/
length(){
return this._q.length;
}
/**
* Pause a running queue
*/
pause(){
this.paused = true;
}
/**
* End the queue
*/
stop(){
this._q = [];
this.running = false;
this.paused = true;
}
}
/**
* Create a new task from a callback
* @class
* @private
* @param {function} task
* @param {array} args
* @param {scope} context
* @return {function} task
*/
class Task {
constructor(task, args, context){
return function(){
var toApply = arguments || [];
return new Promise( (resolve, reject) => {
var callback = function(value, err){
if (!value && err) {
reject(err);
} else {
resolve(value);
}
};
// Add the callback to the arguments list
toApply.push(callback);
// Apply all arguments to the functions
task.apply(context || this, toApply);
});
};
}
}
export default Queue;
export { Task };
......@@ -219,3 +219,23 @@ export function querySelectorEscape(value) {
}
return result;
}
/**
* Creates a new pending promise and provides methods to resolve or reject it.
* From: https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred#backwards_forwards_compatible
*/
export function defer() {
this.resolve = null;
this.reject = null;
this.id = UUID();
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
Object.freeze(this);
}
export const requestIdleCallback = 'requestIdleCallback' in window ? window.requestIdleCallback : window.requestAnimationFrame;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment