Commit 27d439fc authored by Fred Chasen's avatar Fred Chasen

Merge branch 'async_layout' into 'master'

Update to Async Layout

See merge request tools/pagedjs!31
parents aeb0e48b 6607d914
......@@ -115,3 +115,4 @@
<p>Nulla dignissim pellentesque magna ac maximus. Integer id tincidunt erat. Sed elementum posuere augue, quis pharetra mi vehicula in. Nullam rhoncus mi quis lectus gravida dignissim. Pellentesque a tortor ut leo pretium auctor non in massa. Nunc efficitur vestibulum mi, id mattis quam aliquet id. Ut semper tortor sit amet molestie mattis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec laoreet eleifend purus ut sagittis. Nunc consequat vel sapien at convallis. Maecenas sollicitudin quis justo non varius.</p>
</section>
</body>
</html>
......@@ -112,3 +112,4 @@
<p>Nulla dignissim pellentesque magna ac maximus. Integer id tincidunt erat. Sed elementum posuere augue, quis pharetra mi vehicula in. Nullam rhoncus mi quis lectus gravida dignissim. Pellentesque a tortor ut leo pretium auctor non in massa. Nunc efficitur vestibulum mi, id mattis quam aliquet id. Ut semper tortor sit amet molestie mattis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec laoreet eleifend purus ut sagittis. Nunc consequat vel sapien at convallis. Maecenas sollicitudin quis justo non varius.</p>
</section>
</body>
</html>
......@@ -95,3 +95,4 @@
</section>
</body>
</html>
......@@ -95,3 +95,4 @@
</section>
</body>
</html>
......@@ -240,3 +240,4 @@
</section>
</body>
</html>
......@@ -72,3 +72,4 @@
</p>
</body>
</html>
......@@ -191,3 +191,4 @@
</section>
</body>
</html>
......@@ -127,4 +127,5 @@
</p>
</section>
</body>
</body>
</html>
......@@ -198,3 +198,4 @@
</section>
</body>
</html>
......@@ -111,3 +111,4 @@
</section>
</body>
</html>
......@@ -128,3 +128,4 @@
</section>
</body>
</html>
......@@ -160,3 +160,4 @@
</section>
</body>
</html>
......@@ -130,3 +130,4 @@
</section>
</body>
</html>
......@@ -173,3 +173,4 @@
</section>
</body>
</html>
......@@ -42,3 +42,4 @@
</section>
</body>
</html>
......@@ -42,3 +42,4 @@
</section>
</body>
</html>
......@@ -42,3 +42,4 @@
</section>
</body>
</html>
......@@ -115,3 +115,4 @@
</section>
</body>
</html>
......@@ -107,3 +107,4 @@
</section>
</body>
</html>
......@@ -113,3 +113,4 @@
</section>
</body>
</html>
......@@ -140,3 +140,4 @@
</section>
</body>
</html>
......@@ -104,3 +104,4 @@
</p>
</section>
</body>
</html>
......@@ -142,3 +142,4 @@
</section>
</body>
</html>
......@@ -101,3 +101,4 @@
</section>
</body>
</html>
......@@ -168,3 +168,4 @@
</section>
</body>
</html>
......@@ -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) {
......@@ -91,6 +98,7 @@ class Chunker {
this.pageTemplate = document.createElement("template");
this.pageTemplate.innerHTML = TEMPLATE;
}
async flow(content, renderTo) {
......@@ -101,14 +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);
await this.render(parsed, renderTo);
await this.loadFonts();
let rendered = await this.render(parsed, this.breakToken);
while (rendered.canceled) {
this.start();
rendered = await this.render(parsed, this.breakToken);
}
this.rendered = true;
await this.hooks.afterRendered.trigger(this.pages, this);
......@@ -117,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);
}
});
});
}
......@@ -192,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)) {
......@@ -209,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);
......@@ -220,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) {
......@@ -240,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) => {
......@@ -260,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
......@@ -301,6 +390,7 @@ class Chunker {
return page;
}
*/
get total() {
return this._total;
......@@ -311,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;
}
......
......@@ -136,7 +136,7 @@ class Sheet {
csstree.walk(ast, {
visit: 'Url',
enter: (node, item, list) => {
let href = node.value.value.replace(/["|']/g, '');
let href = node.value.value.replace(/["']/g, '');
let url = new URL(href, this.url)
node.value.value = url.toString();
}
......
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(){