epubjs
Version:
Parse and Render Epubs
972 lines (791 loc) • 27.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _eventEmitter = _interopRequireDefault(require("event-emitter"));
var _core = require("../../utils/core");
var _scrolltype = _interopRequireDefault(require("../../utils/scrolltype"));
var _mapping = _interopRequireDefault(require("../../mapping"));
var _queue = _interopRequireDefault(require("../../utils/queue"));
var _stage = _interopRequireDefault(require("../helpers/stage"));
var _views = _interopRequireDefault(require("../helpers/views"));
var _constants = require("../../utils/constants");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
class DefaultViewManager {
constructor(options) {
this.name = "default";
this.optsSettings = options.settings;
this.View = options.view;
this.request = options.request;
this.renditionQueue = options.queue;
this.q = new _queue.default(this);
this.settings = (0, _core.extend)(this.settings || {}, {
infinite: true,
hidden: false,
width: undefined,
height: undefined,
axis: undefined,
writingMode: undefined,
flow: "scrolled",
ignoreClass: "",
fullsize: undefined,
allowScriptedContent: false,
allowPopups: false
});
(0, _core.extend)(this.settings, options.settings || {});
this.viewSettings = {
ignoreClass: this.settings.ignoreClass,
axis: this.settings.axis,
flow: this.settings.flow,
layout: this.layout,
method: this.settings.method,
// srcdoc, blobUrl, write
width: 0,
height: 0,
forceEvenPages: true,
allowScriptedContent: this.settings.allowScriptedContent,
allowPopups: this.settings.allowPopups
};
this.rendered = false;
}
render(element, size) {
let tag = element.tagName;
if (typeof this.settings.fullsize === "undefined" && tag && (tag.toLowerCase() == "body" || tag.toLowerCase() == "html")) {
this.settings.fullsize = true;
}
if (this.settings.fullsize) {
this.settings.overflow = "visible";
this.overflow = this.settings.overflow;
}
this.settings.size = size;
this.settings.rtlScrollType = (0, _scrolltype.default)(); // Save the stage
this.stage = new _stage.default({
width: size.width,
height: size.height,
overflow: this.overflow,
hidden: this.settings.hidden,
axis: this.settings.axis,
fullsize: this.settings.fullsize,
direction: this.settings.direction
});
this.stage.attachTo(element); // Get this stage container div
this.container = this.stage.getContainer(); // Views array methods
this.views = new _views.default(this.container); // Calculate Stage Size
this._bounds = this.bounds();
this._stageSize = this.stage.size(); // Set the dimensions for views
this.viewSettings.width = this._stageSize.width;
this.viewSettings.height = this._stageSize.height; // Function to handle a resize event.
// Will only attach if width and height are both fixed.
this.stage.onResize(this.onResized.bind(this));
this.stage.onOrientationChange(this.onOrientationChange.bind(this)); // Add Event Listeners
this.addEventListeners(); // Add Layout method
// this.applyLayoutMethod();
if (this.layout) {
this.updateLayout();
}
this.rendered = true;
}
addEventListeners() {
var scroller;
window.addEventListener("unload", function (e) {
this.destroy();
}.bind(this));
if (!this.settings.fullsize) {
scroller = this.container;
} else {
scroller = window;
}
this._onScroll = this.onScroll.bind(this);
scroller.addEventListener("scroll", this._onScroll);
}
removeEventListeners() {
var scroller;
if (!this.settings.fullsize) {
scroller = this.container;
} else {
scroller = window;
}
scroller.removeEventListener("scroll", this._onScroll);
this._onScroll = undefined;
}
destroy() {
clearTimeout(this.orientationTimeout);
clearTimeout(this.resizeTimeout);
clearTimeout(this.afterScrolled);
this.clear();
this.removeEventListeners();
this.stage.destroy();
this.rendered = false;
/*
clearTimeout(this.trimTimeout);
if(this.settings.hidden) {
this.element.removeChild(this.wrapper);
} else {
this.element.removeChild(this.container);
}
*/
}
onOrientationChange(e) {
let {
orientation
} = window;
if (this.optsSettings.resizeOnOrientationChange) {
this.resize();
} // Per ampproject:
// In IOS 10.3, the measured size of an element is incorrect if the
// element size depends on window size directly and the measurement
// happens in window.resize event. Adding a timeout for correct
// measurement. See https://github.com/ampproject/amphtml/issues/8479
clearTimeout(this.orientationTimeout);
this.orientationTimeout = setTimeout(function () {
this.orientationTimeout = undefined;
if (this.optsSettings.resizeOnOrientationChange) {
this.resize();
}
this.emit(_constants.EVENTS.MANAGERS.ORIENTATION_CHANGE, orientation);
}.bind(this), 500);
}
onResized(e) {
this.resize();
}
resize(width, height, epubcfi) {
let stageSize = this.stage.size(width, height); // For Safari, wait for orientation to catch up
// if the window is a square
this.winBounds = (0, _core.windowBounds)();
if (this.orientationTimeout && this.winBounds.width === this.winBounds.height) {
// reset the stage size for next resize
this._stageSize = undefined;
return;
}
if (this._stageSize && this._stageSize.width === stageSize.width && this._stageSize.height === stageSize.height) {
// Size is the same, no need to resize
return;
}
this._stageSize = stageSize;
this._bounds = this.bounds(); // Clear current views
this.clear(); // Update for new views
this.viewSettings.width = this._stageSize.width;
this.viewSettings.height = this._stageSize.height;
this.updateLayout();
this.emit(_constants.EVENTS.MANAGERS.RESIZED, {
width: this._stageSize.width,
height: this._stageSize.height
}, epubcfi);
}
createView(section, forceRight) {
return new this.View(section, (0, _core.extend)(this.viewSettings, {
forceRight
}));
}
handleNextPrePaginated(forceRight, section, action) {
let next;
if (this.layout.name === "pre-paginated" && this.layout.divisor > 1) {
if (forceRight || section.index === 0) {
// First page (cover) should stand alone for pre-paginated books
return;
}
next = section.next();
if (next && !next.properties.includes("page-spread-left")) {
return action.call(this, next);
}
}
}
display(section, target) {
var displaying = new _core.defer();
var displayed = displaying.promise; // Check if moving to target is needed
if (target === section.href || (0, _core.isNumber)(target)) {
target = undefined;
} // Check to make sure the section we want isn't already shown
var visible = this.views.find(section); // View is already shown, just move to correct location in view
if (visible && section && this.layout.name !== "pre-paginated") {
let offset = visible.offset();
if (this.settings.direction === "ltr") {
this.scrollTo(offset.left, offset.top, true);
} else {
let width = visible.width();
this.scrollTo(offset.left + width, offset.top, true);
}
if (target) {
let offset = visible.locationOf(target);
let width = visible.width();
this.moveTo(offset, width);
}
displaying.resolve();
return displayed;
} // Hide all current views
this.clear();
let forceRight = false;
if (this.layout.name === "pre-paginated" && this.layout.divisor === 2 && section.properties.includes("page-spread-right")) {
forceRight = true;
}
this.add(section, forceRight).then(function (view) {
// Move to correct place within the section, if needed
if (target) {
let offset = view.locationOf(target);
let width = view.width();
this.moveTo(offset, width);
}
}.bind(this), err => {
displaying.reject(err);
}).then(function () {
return this.handleNextPrePaginated(forceRight, section, this.add);
}.bind(this)).then(function () {
this.views.show();
displaying.resolve();
}.bind(this)); // .then(function(){
// return this.hooks.display.trigger(view);
// }.bind(this))
// .then(function(){
// this.views.show();
// }.bind(this));
return displayed;
}
afterDisplayed(view) {
this.emit(_constants.EVENTS.MANAGERS.ADDED, view);
}
afterResized(view) {
this.emit(_constants.EVENTS.MANAGERS.RESIZE, view.section);
}
moveTo(offset, width) {
var distX = 0,
distY = 0;
if (!this.isPaginated) {
distY = offset.top;
} else {
distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta;
if (distX + this.layout.delta > this.container.scrollWidth) {
distX = this.container.scrollWidth - this.layout.delta;
}
distY = Math.floor(offset.top / this.layout.delta) * this.layout.delta;
if (distY + this.layout.delta > this.container.scrollHeight) {
distY = this.container.scrollHeight - this.layout.delta;
}
}
if (this.settings.direction === 'rtl') {
/***
the `floor` function above (L343) is on positive values, so we should add one `layout.delta`
to distX or use `Math.ceil` function, or multiply offset.left by -1
before `Math.floor`
*/
distX = distX + this.layout.delta;
distX = distX - width;
}
this.scrollTo(distX, distY, true);
}
add(section, forceRight) {
var view = this.createView(section, forceRight);
this.views.append(view); // view.on(EVENTS.VIEWS.SHOWN, this.afterDisplayed.bind(this));
view.onDisplayed = this.afterDisplayed.bind(this);
view.onResize = this.afterResized.bind(this);
view.on(_constants.EVENTS.VIEWS.AXIS, axis => {
this.updateAxis(axis);
});
view.on(_constants.EVENTS.VIEWS.WRITING_MODE, mode => {
this.updateWritingMode(mode);
});
return view.display(this.request);
}
append(section, forceRight) {
var view = this.createView(section, forceRight);
this.views.append(view);
view.onDisplayed = this.afterDisplayed.bind(this);
view.onResize = this.afterResized.bind(this);
view.on(_constants.EVENTS.VIEWS.AXIS, axis => {
this.updateAxis(axis);
});
view.on(_constants.EVENTS.VIEWS.WRITING_MODE, mode => {
this.updateWritingMode(mode);
});
return view.display(this.request);
}
prepend(section, forceRight) {
var view = this.createView(section, forceRight);
view.on(_constants.EVENTS.VIEWS.RESIZED, bounds => {
this.counter(bounds);
});
this.views.prepend(view);
view.onDisplayed = this.afterDisplayed.bind(this);
view.onResize = this.afterResized.bind(this);
view.on(_constants.EVENTS.VIEWS.AXIS, axis => {
this.updateAxis(axis);
});
view.on(_constants.EVENTS.VIEWS.WRITING_MODE, mode => {
this.updateWritingMode(mode);
});
return view.display(this.request);
}
counter(bounds) {
if (this.settings.axis === "vertical") {
this.scrollBy(0, bounds.heightDelta, true);
} else {
this.scrollBy(bounds.widthDelta, 0, true);
}
} // resizeView(view) {
//
// if(this.settings.globalLayoutProperties.layout === "pre-paginated") {
// view.lock("both", this.bounds.width, this.bounds.height);
// } else {
// view.lock("width", this.bounds.width, this.bounds.height);
// }
//
// };
next() {
var next;
var left;
let dir = this.settings.direction;
if (!this.views.length) return;
if (this.isPaginated && this.settings.axis === "horizontal" && (!dir || dir === "ltr")) {
this.scrollLeft = this.container.scrollLeft;
left = this.container.scrollLeft + this.container.offsetWidth + this.layout.delta;
if (left <= this.container.scrollWidth) {
this.scrollBy(this.layout.delta, 0, true);
} else {
next = this.views.last().section.next();
}
} else if (this.isPaginated && this.settings.axis === "horizontal" && dir === "rtl") {
this.scrollLeft = this.container.scrollLeft;
if (this.settings.rtlScrollType === "default") {
left = this.container.scrollLeft;
if (left > 0) {
this.scrollBy(this.layout.delta, 0, true);
} else {
next = this.views.last().section.next();
}
} else {
left = this.container.scrollLeft + this.layout.delta * -1;
if (left > this.container.scrollWidth * -1) {
this.scrollBy(this.layout.delta, 0, true);
} else {
next = this.views.last().section.next();
}
}
} else if (this.isPaginated && this.settings.axis === "vertical") {
this.scrollTop = this.container.scrollTop;
let top = this.container.scrollTop + this.container.offsetHeight;
if (top < this.container.scrollHeight) {
this.scrollBy(0, this.layout.height, true);
} else {
next = this.views.last().section.next();
}
} else {
next = this.views.last().section.next();
}
if (next) {
this.clear(); // The new section may have a different writing-mode from the old section. Thus, we need to update layout.
this.updateLayout();
let forceRight = false;
if (this.layout.name === "pre-paginated" && this.layout.divisor === 2 && next.properties.includes("page-spread-right")) {
forceRight = true;
}
return this.append(next, forceRight).then(function () {
return this.handleNextPrePaginated(forceRight, next, this.append);
}.bind(this), err => {
return err;
}).then(function () {
// Reset position to start for scrolled-doc vertical-rl in default mode
if (!this.isPaginated && this.settings.axis === "horizontal" && this.settings.direction === "rtl" && this.settings.rtlScrollType === "default") {
this.scrollTo(this.container.scrollWidth, 0, true);
}
this.views.show();
}.bind(this));
}
}
prev() {
var prev;
var left;
let dir = this.settings.direction;
if (!this.views.length) return;
if (this.isPaginated && this.settings.axis === "horizontal" && (!dir || dir === "ltr")) {
this.scrollLeft = this.container.scrollLeft;
left = this.container.scrollLeft;
if (left > 0) {
this.scrollBy(-this.layout.delta, 0, true);
} else {
prev = this.views.first().section.prev();
}
} else if (this.isPaginated && this.settings.axis === "horizontal" && dir === "rtl") {
this.scrollLeft = this.container.scrollLeft;
if (this.settings.rtlScrollType === "default") {
left = this.container.scrollLeft + this.container.offsetWidth;
if (left < this.container.scrollWidth) {
this.scrollBy(-this.layout.delta, 0, true);
} else {
prev = this.views.first().section.prev();
}
} else {
left = this.container.scrollLeft;
if (left < 0) {
this.scrollBy(-this.layout.delta, 0, true);
} else {
prev = this.views.first().section.prev();
}
}
} else if (this.isPaginated && this.settings.axis === "vertical") {
this.scrollTop = this.container.scrollTop;
let top = this.container.scrollTop;
if (top > 0) {
this.scrollBy(0, -this.layout.height, true);
} else {
prev = this.views.first().section.prev();
}
} else {
prev = this.views.first().section.prev();
}
if (prev) {
this.clear(); // The new section may have a different writing-mode from the old section. Thus, we need to update layout.
this.updateLayout();
let forceRight = false;
if (this.layout.name === "pre-paginated" && this.layout.divisor === 2 && typeof prev.prev() !== "object") {
forceRight = true;
}
return this.prepend(prev, forceRight).then(function () {
var left;
if (this.layout.name === "pre-paginated" && this.layout.divisor > 1) {
left = prev.prev();
if (left) {
return this.prepend(left);
}
}
}.bind(this), err => {
return err;
}).then(function () {
if (this.isPaginated && this.settings.axis === "horizontal") {
if (this.settings.direction === "rtl") {
if (this.settings.rtlScrollType === "default") {
this.scrollTo(0, 0, true);
} else {
this.scrollTo(this.container.scrollWidth * -1 + this.layout.delta, 0, true);
}
} else {
this.scrollTo(this.container.scrollWidth - this.layout.delta, 0, true);
}
}
this.views.show();
}.bind(this));
}
}
current() {
var visible = this.visible();
if (visible.length) {
// Current is the last visible view
return visible[visible.length - 1];
}
return null;
}
clear() {
// this.q.clear();
if (this.views) {
this.views.hide();
this.scrollTo(0, 0, true);
this.views.clear();
}
}
currentLocation() {
this.updateLayout();
if (this.isPaginated && this.settings.axis === "horizontal") {
this.location = this.paginatedLocation();
} else {
this.location = this.scrolledLocation();
}
return this.location;
}
scrolledLocation() {
let visible = this.visible();
let container = this.container.getBoundingClientRect();
let pageHeight = container.height < window.innerHeight ? container.height : window.innerHeight;
let pageWidth = container.width < window.innerWidth ? container.width : window.innerWidth;
let vertical = this.settings.axis === "vertical";
let rtl = this.settings.direction === "rtl";
let offset = 0;
let used = 0;
if (this.settings.fullsize) {
offset = vertical ? window.scrollY : window.scrollX;
}
let sections = visible.map(view => {
let {
index,
href
} = view.section;
let position = view.position();
let width = view.width();
let height = view.height();
let startPos;
let endPos;
let stopPos;
let totalPages;
if (vertical) {
startPos = offset + container.top - position.top + used;
endPos = startPos + pageHeight - used;
totalPages = this.layout.count(height, pageHeight).pages;
stopPos = pageHeight;
} else {
startPos = offset + container.left - position.left + used;
endPos = startPos + pageWidth - used;
totalPages = this.layout.count(width, pageWidth).pages;
stopPos = pageWidth;
}
let currPage = Math.ceil(startPos / stopPos);
let pages = [];
let endPage = Math.ceil(endPos / stopPos); // Reverse page counts for horizontal rtl
if (this.settings.direction === "rtl" && !vertical) {
let tempStartPage = currPage;
currPage = totalPages - endPage;
endPage = totalPages - tempStartPage;
}
pages = [];
for (var i = currPage; i <= endPage; i++) {
let pg = i + 1;
pages.push(pg);
}
let mapping = this.mapping.page(view.contents, view.section.cfiBase, startPos, endPos);
return {
index,
href,
pages,
totalPages,
mapping
};
});
return sections;
}
paginatedLocation() {
let visible = this.visible();
let container = this.container.getBoundingClientRect();
let left = 0;
let used = 0;
if (this.settings.fullsize) {
left = window.scrollX;
}
let sections = visible.map(view => {
let {
index,
href
} = view.section;
let offset;
let position = view.position();
let width = view.width(); // Find mapping
let start;
let end;
let pageWidth;
if (this.settings.direction === "rtl") {
offset = container.right - left;
pageWidth = Math.min(Math.abs(offset - position.left), this.layout.width) - used;
end = position.width - (position.right - offset) - used;
start = end - pageWidth;
} else {
offset = container.left + left;
pageWidth = Math.min(position.right - offset, this.layout.width) - used;
start = offset - position.left + used;
end = start + pageWidth;
}
used += pageWidth;
let mapping = this.mapping.page(view.contents, view.section.cfiBase, start, end);
let totalPages = this.layout.count(width).pages;
let startPage = Math.floor(start / this.layout.pageWidth);
let pages = [];
let endPage = Math.floor(end / this.layout.pageWidth); // start page should not be negative
if (startPage < 0) {
startPage = 0;
endPage = endPage + 1;
} // Reverse page counts for rtl
if (this.settings.direction === "rtl") {
let tempStartPage = startPage;
startPage = totalPages - endPage;
endPage = totalPages - tempStartPage;
}
for (var i = startPage + 1; i <= endPage; i++) {
let pg = i;
pages.push(pg);
}
return {
index,
href,
pages,
totalPages,
mapping
};
});
return sections;
}
isVisible(view, offsetPrev, offsetNext, _container) {
var position = view.position();
var container = _container || this.bounds();
if (this.settings.axis === "horizontal" && position.right > container.left - offsetPrev && position.left < container.right + offsetNext) {
return true;
} else if (this.settings.axis === "vertical" && position.bottom > container.top - offsetPrev && position.top < container.bottom + offsetNext) {
return true;
}
return false;
}
visible() {
var container = this.bounds();
var views = this.views.displayed();
var viewsLength = views.length;
var visible = [];
var isVisible;
var view;
for (var i = 0; i < viewsLength; i++) {
view = views[i];
isVisible = this.isVisible(view, 0, 0, container);
if (isVisible === true) {
visible.push(view);
}
}
return visible;
}
scrollBy(x, y, silent) {
let dir = this.settings.direction === "rtl" ? -1 : 1;
if (silent) {
this.ignore = true;
}
if (!this.settings.fullsize) {
if (x) this.container.scrollLeft += x * dir;
if (y) this.container.scrollTop += y;
} else {
window.scrollBy(x * dir, y * dir);
}
this.scrolled = true;
}
scrollTo(x, y, silent) {
if (silent) {
this.ignore = true;
}
if (!this.settings.fullsize) {
this.container.scrollLeft = x;
this.container.scrollTop = y;
} else {
window.scrollTo(x, y);
}
this.scrolled = true;
}
onScroll() {
let scrollTop;
let scrollLeft;
if (!this.settings.fullsize) {
scrollTop = this.container.scrollTop;
scrollLeft = this.container.scrollLeft;
} else {
scrollTop = window.scrollY;
scrollLeft = window.scrollX;
}
this.scrollTop = scrollTop;
this.scrollLeft = scrollLeft;
if (!this.ignore) {
this.emit(_constants.EVENTS.MANAGERS.SCROLL, {
top: scrollTop,
left: scrollLeft
});
clearTimeout(this.afterScrolled);
this.afterScrolled = setTimeout(function () {
this.emit(_constants.EVENTS.MANAGERS.SCROLLED, {
top: this.scrollTop,
left: this.scrollLeft
});
}.bind(this), 20);
} else {
this.ignore = false;
}
}
bounds() {
var bounds;
bounds = this.stage.bounds();
return bounds;
}
applyLayout(layout) {
this.layout = layout;
this.updateLayout();
if (this.views && this.views.length > 0 && this.layout.name === "pre-paginated") {
this.display(this.views.first().section);
} // this.manager.layout(this.layout.format);
}
updateLayout() {
if (!this.stage) {
return;
}
this._stageSize = this.stage.size();
if (!this.isPaginated) {
this.layout.calculate(this._stageSize.width, this._stageSize.height);
} else {
this.layout.calculate(this._stageSize.width, this._stageSize.height, this.settings.gap); // Set the look ahead offset for what is visible
this.settings.offset = this.layout.delta / this.layout.divisor; // this.stage.addStyleRules("iframe", [{"margin-right" : this.layout.gap + "px"}]);
} // Set the dimensions for views
this.viewSettings.width = this.layout.width;
this.viewSettings.height = this.layout.height;
this.setLayout(this.layout);
}
setLayout(layout) {
this.viewSettings.layout = layout;
this.mapping = new _mapping.default(layout.props, this.settings.direction, this.settings.axis);
if (this.views) {
this.views.forEach(function (view) {
if (view) {
view.setLayout(layout);
}
});
}
}
updateWritingMode(mode) {
this.writingMode = mode;
}
updateAxis(axis, forceUpdate) {
if (!forceUpdate && axis === this.settings.axis) {
return;
}
this.settings.axis = axis;
this.stage && this.stage.axis(axis);
this.viewSettings.axis = axis;
if (this.mapping) {
this.mapping = new _mapping.default(this.layout.props, this.settings.direction, this.settings.axis);
}
if (this.layout) {
if (axis === "vertical") {
this.layout.spread("none");
} else {
this.layout.spread(this.layout.settings.spread);
}
}
}
updateFlow(flow, defaultScrolledOverflow = "auto") {
let isPaginated = flow === "paginated" || flow === "auto";
this.isPaginated = isPaginated;
if (flow === "scrolled-doc" || flow === "scrolled-continuous" || flow === "scrolled") {
this.updateAxis("vertical");
} else {
this.updateAxis("horizontal");
}
this.viewSettings.flow = flow;
if (!this.settings.overflow) {
this.overflow = isPaginated ? "hidden" : defaultScrolledOverflow;
} else {
this.overflow = this.settings.overflow;
}
this.stage && this.stage.overflow(this.overflow);
this.updateLayout();
}
getContents() {
var contents = [];
if (!this.views) {
return contents;
}
this.views.forEach(function (view) {
const viewContents = view && view.contents;
if (viewContents) {
contents.push(viewContents);
}
});
return contents;
}
direction(dir = "ltr") {
this.settings.direction = dir;
this.stage && this.stage.direction(dir);
this.viewSettings.direction = dir;
this.updateLayout();
}
isRendered() {
return this.rendered;
}
} //-- Enable binding events to Manager
(0, _eventEmitter.default)(DefaultViewManager.prototype);
var _default = DefaultViewManager;
exports.default = _default;