diff --git a/src/contents.js b/src/contents.js
index 3effe7252..b25a4370f 100644
--- a/src/contents.js
+++ b/src/contents.js
@@ -1,1262 +1,1296 @@
import EventEmitter from "event-emitter";
-import {isNumber, prefixed, borders, defaults} from "./utils/core";
+import { isNumber, prefixed, borders, defaults } from "./utils/core";
import EpubCFI from "./epubcfi";
import Mapping from "./mapping";
-import {replaceLinks} from "./utils/replacements";
+import { replaceLinks } from "./utils/replacements";
import { EPUBJS_VERSION, EVENTS, DOM_EVENTS } from "./utils/constants";
-const hasNavigator = typeof (navigator) !== "undefined";
+const hasNavigator = typeof navigator !== "undefined";
const isChrome = hasNavigator && /Chrome/.test(navigator.userAgent);
-const isWebkit = hasNavigator && !isChrome && /AppleWebKit/.test(navigator.userAgent);
+const isWebkit =
+ hasNavigator && !isChrome && /AppleWebKit/.test(navigator.userAgent);
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
/**
- * Handles DOM manipulation, queries and events for View contents
- * @class
- * @param {document} doc Document
- * @param {element} content Parent Element (typically Body)
- * @param {string} cfiBase Section component of CFIs
- * @param {number} sectionIndex Index in Spine of Conntent's Section
- */
+ * Handles DOM manipulation, queries and events for View contents
+ * @class
+ * @param {document} doc Document
+ * @param {element} content Parent Element (typically Body)
+ * @param {string} cfiBase Section component of CFIs
+ * @param {number} sectionIndex Index in Spine of Conntent's Section
+ */
class Contents {
- constructor(doc, content, cfiBase, sectionIndex) {
- // Blank Cfi for Parsing
- this.epubcfi = new EpubCFI();
-
- this.document = doc;
- this.documentElement = this.document.documentElement;
- this.content = content || this.document.body;
- this.window = this.document.defaultView;
-
- this._size = {
- width: 0,
- height: 0
- };
-
- this.sectionIndex = sectionIndex || 0;
- this.cfiBase = cfiBase || "";
-
- this.epubReadingSystem("epub.js", EPUBJS_VERSION);
- this.called = 0;
- this.active = true;
- this.listeners();
- }
-
- /**
- * Get DOM events that are listened for and passed along
- */
- static get listenedEvents() {
- return DOM_EVENTS;
- }
-
- /**
- * Get or Set width
- * @param {number} [w]
- * @returns {number} width
- */
- width(w) {
- // var frame = this.documentElement;
- var frame = this.content;
-
- if (w && isNumber(w)) {
- w = w + "px";
- }
-
- if (w) {
- frame.style.width = w;
- // this.content.style.width = w;
- }
-
- return parseInt(this.window.getComputedStyle(frame)["width"]);
-
-
- }
-
- /**
- * Get or Set height
- * @param {number} [h]
- * @returns {number} height
- */
- height(h) {
- // var frame = this.documentElement;
- var frame = this.content;
-
- if (h && isNumber(h)) {
- h = h + "px";
- }
-
- if (h) {
- frame.style.height = h;
- // this.content.style.height = h;
- }
-
- return parseInt(this.window.getComputedStyle(frame)["height"]);
-
- }
-
- /**
- * Get or Set width of the contents
- * @param {number} [w]
- * @returns {number} width
- */
- contentWidth(w) {
-
- var content = this.content || this.document.body;
-
- if (w && isNumber(w)) {
- w = w + "px";
- }
-
- if (w) {
- content.style.width = w;
- }
-
- return parseInt(this.window.getComputedStyle(content)["width"]);
-
-
- }
-
- /**
- * Get or Set height of the contents
- * @param {number} [h]
- * @returns {number} height
- */
- contentHeight(h) {
-
- var content = this.content || this.document.body;
-
- if (h && isNumber(h)) {
- h = h + "px";
- }
-
- if (h) {
- content.style.height = h;
- }
-
- return parseInt(this.window.getComputedStyle(content)["height"]);
-
- }
-
- /**
- * Get the width of the text using Range
- * @returns {number} width
- */
- textWidth() {
- let rect;
- let width;
- let range = this.document.createRange();
- let content = this.content || this.document.body;
- let border = borders(content);
-
- // Select the contents of frame
- range.selectNodeContents(content);
-
- // get the width of the text content
- rect = range.getBoundingClientRect();
- width = rect.width;
-
- if (border && border.width) {
- width += border.width;
- }
-
- return Math.round(width);
- }
-
- /**
- * Get the height of the text using Range
- * @returns {number} height
- */
- textHeight() {
- let rect;
- let height;
- let range = this.document.createRange();
- let content = this.content || this.document.body;
-
- range.selectNodeContents(content);
-
- rect = range.getBoundingClientRect();
- height = rect.bottom;
-
- return Math.round(height);
- }
-
- /**
- * Get documentElement scrollWidth
- * @returns {number} width
- */
- scrollWidth() {
- var width = this.documentElement.scrollWidth;
-
- return width;
- }
-
- /**
- * Get documentElement scrollHeight
- * @returns {number} height
- */
- scrollHeight() {
- var height = this.documentElement.scrollHeight;
-
- return height;
- }
-
- /**
- * Set overflow css style of the contents
- * @param {string} [overflow]
- */
- overflow(overflow) {
-
- if (overflow) {
- this.documentElement.style.overflow = overflow;
- }
-
- return this.window.getComputedStyle(this.documentElement)["overflow"];
- }
-
- /**
- * Set overflowX css style of the documentElement
- * @param {string} [overflow]
- */
- overflowX(overflow) {
-
- if (overflow) {
- this.documentElement.style.overflowX = overflow;
- }
-
- return this.window.getComputedStyle(this.documentElement)["overflowX"];
- }
-
- /**
- * Set overflowY css style of the documentElement
- * @param {string} [overflow]
- */
- overflowY(overflow) {
-
- if (overflow) {
- this.documentElement.style.overflowY = overflow;
- }
-
- return this.window.getComputedStyle(this.documentElement)["overflowY"];
- }
-
- /**
- * Set Css styles on the contents element (typically Body)
- * @param {string} property
- * @param {string} value
- * @param {boolean} [priority] set as "important"
- */
- css(property, value, priority) {
- var content = this.content || this.document.body;
-
- if (value) {
- content.style.setProperty(property, value, priority ? "important" : "");
- } else {
- content.style.removeProperty(property);
- }
-
- return this.window.getComputedStyle(content)[property];
- }
-
- /**
- * Get or Set the viewport element
- * @param {object} [options]
- * @param {string} [options.width]
- * @param {string} [options.height]
- * @param {string} [options.scale]
- * @param {string} [options.minimum]
- * @param {string} [options.maximum]
- * @param {string} [options.scalable]
- */
- viewport(options) {
- var _width, _height, _scale, _minimum, _maximum, _scalable;
- // var width, height, scale, minimum, maximum, scalable;
- var $viewport = this.document.querySelector("meta[name='viewport']");
- var parsed = {
- "width": undefined,
- "height": undefined,
- "scale": undefined,
- "minimum": undefined,
- "maximum": undefined,
- "scalable": undefined
- };
- var newContent = [];
- var settings = {};
-
- /*
- * check for the viewport size
- *
- */
- if($viewport && $viewport.hasAttribute("content")) {
- let content = $viewport.getAttribute("content");
- let _width = content.match(/width\s*=\s*([^,]*)/);
- let _height = content.match(/height\s*=\s*([^,]*)/);
- let _scale = content.match(/initial-scale\s*=\s*([^,]*)/);
- let _minimum = content.match(/minimum-scale\s*=\s*([^,]*)/);
- let _maximum = content.match(/maximum-scale\s*=\s*([^,]*)/);
- let _scalable = content.match(/user-scalable\s*=\s*([^,]*)/);
-
- if(_width && _width.length && typeof _width[1] !== "undefined"){
- parsed.width = _width[1];
- }
- if(_height && _height.length && typeof _height[1] !== "undefined"){
- parsed.height = _height[1];
- }
- if(_scale && _scale.length && typeof _scale[1] !== "undefined"){
- parsed.scale = _scale[1];
- }
- if(_minimum && _minimum.length && typeof _minimum[1] !== "undefined"){
- parsed.minimum = _minimum[1];
- }
- if(_maximum && _maximum.length && typeof _maximum[1] !== "undefined"){
- parsed.maximum = _maximum[1];
- }
- if(_scalable && _scalable.length && typeof _scalable[1] !== "undefined"){
- parsed.scalable = _scalable[1];
- }
- }
-
- settings = defaults(options || {}, parsed);
-
- if (options) {
- if (settings.width) {
- newContent.push("width=" + settings.width);
- }
-
- if (settings.height) {
- newContent.push("height=" + settings.height);
- }
-
- if (settings.scale) {
- newContent.push("initial-scale=" + settings.scale);
- }
-
- if (settings.scalable === "no") {
- newContent.push("minimum-scale=" + settings.scale);
- newContent.push("maximum-scale=" + settings.scale);
- newContent.push("user-scalable=" + settings.scalable);
- } else {
-
- if (settings.scalable) {
- newContent.push("user-scalable=" + settings.scalable);
- }
-
- if (settings.minimum) {
- newContent.push("minimum-scale=" + settings.minimum);
- }
-
- if (settings.maximum) {
- newContent.push("minimum-scale=" + settings.maximum);
- }
- }
-
- if (!$viewport) {
- $viewport = this.document.createElement("meta");
- $viewport.setAttribute("name", "viewport");
- this.document.querySelector("head").appendChild($viewport);
- }
-
- $viewport.setAttribute("content", newContent.join(", "));
-
- this.window.scrollTo(0, 0);
- }
-
-
- return settings;
- }
-
- /**
- * Event emitter for when the contents has expanded
- * @private
- */
- expand() {
- this.emit(EVENTS.CONTENTS.EXPAND);
- }
-
- /**
- * Add DOM listeners
- * @private
- */
- listeners() {
- this.imageLoadListeners();
-
- this.mediaQueryListeners();
-
- // this.fontLoadListeners();
-
- this.addEventListeners();
-
- this.addSelectionListeners();
-
- // this.transitionListeners();
-
- if (typeof ResizeObserver === "undefined") {
- this.resizeListeners();
- this.visibilityListeners();
- } else {
- this.resizeObservers();
- }
-
- // this.mutationObservers();
-
- this.linksHandler();
- }
-
- /**
- * Remove DOM listeners
- * @private
- */
- removeListeners() {
-
- this.removeEventListeners();
-
- this.removeSelectionListeners();
-
- if (this.observer) {
- this.observer.disconnect();
- }
-
- clearTimeout(this.expanding);
- }
-
- /**
- * Check if size of contents has changed and
- * emit 'resize' event if it has.
- * @private
- */
- resizeCheck() {
- let width = this.textWidth();
- let height = this.textHeight();
-
- if (width != this._size.width || height != this._size.height) {
-
- this._size = {
- width: width,
- height: height
- };
-
- this.onResize && this.onResize(this._size);
- this.emit(EVENTS.CONTENTS.RESIZE, this._size);
- }
- }
-
- /**
- * Poll for resize detection
- * @private
- */
- resizeListeners() {
- var width, height;
- // Test size again
- clearTimeout(this.expanding);
- requestAnimationFrame(this.resizeCheck.bind(this));
- this.expanding = setTimeout(this.resizeListeners.bind(this), 350);
- }
-
- /**
- * Listen for visibility of tab to change
- * @private
- */
- visibilityListeners() {
- document.addEventListener("visibilitychange", () => {
- if (document.visibilityState === "visible" && this.active === false) {
- this.active = true;
- this.resizeListeners();
- } else {
- this.active = false;
- clearTimeout(this.expanding);
- }
- });
- }
-
- /**
- * Use css transitions to detect resize
- * @private
- */
- transitionListeners() {
- let body = this.content;
-
- body.style['transitionProperty'] = "font, font-size, font-size-adjust, font-stretch, font-variation-settings, font-weight, width, height";
- body.style['transitionDuration'] = "0.001ms";
- body.style['transitionTimingFunction'] = "linear";
- body.style['transitionDelay'] = "0";
-
- this._resizeCheck = this.resizeCheck.bind(this);
- this.document.addEventListener('transitionend', this._resizeCheck);
- }
-
- /**
- * Listen for media query changes and emit 'expand' event
- * Adapted from: https://github.com/tylergaw/media-query-events/blob/master/js/mq-events.js
- * @private
- */
- mediaQueryListeners() {
- var sheets = this.document.styleSheets;
- var mediaChangeHandler = function(m){
- if(m.matches && !this._expanding) {
- setTimeout(this.expand.bind(this), 1);
- }
- }.bind(this);
-
- for (var i = 0; i < sheets.length; i += 1) {
- var rules;
- // Firefox errors if we access cssRules cross-domain
- try {
- rules = sheets[i].cssRules;
- } catch (e) {
- return;
- }
- if(!rules) return; // Stylesheets changed
- for (var j = 0; j < rules.length; j += 1) {
- //if (rules[j].constructor === CSSMediaRule) {
- if(rules[j].media){
- var mql = this.window.matchMedia(rules[j].media.mediaText);
- mql.addListener(mediaChangeHandler);
- //mql.onchange = mediaChangeHandler;
- }
- }
- }
- }
-
- /**
- * Use ResizeObserver to listen for changes in the DOM and check for resize
- * @private
- */
- resizeObservers() {
- // create an observer instance
- this.observer = new ResizeObserver((e) => {
- requestAnimationFrame(this.resizeCheck.bind(this));
- });
-
- // pass in the target node
- this.observer.observe(this.document.documentElement);
- }
-
- /**
- * Use MutationObserver to listen for changes in the DOM and check for resize
- * @private
- */
- mutationObservers() {
- // create an observer instance
- this.observer = new MutationObserver((mutations) => {
- this.resizeCheck();
- });
-
- // configuration of the observer:
- let config = { attributes: true, childList: true, characterData: true, subtree: true };
-
- // pass in the target node, as well as the observer options
- this.observer.observe(this.document, config);
- }
-
- /**
- * Test if images are loaded or add listener for when they load
- * @private
- */
- imageLoadListeners() {
- var images = this.document.querySelectorAll("img");
- var img;
- for (var i = 0; i < images.length; i++) {
- img = images[i];
-
- if (typeof img.naturalWidth !== "undefined" &&
- img.naturalWidth === 0) {
- img.onload = this.expand.bind(this);
- }
- }
- }
-
- /**
- * Listen for font load and check for resize when loaded
- * @private
- */
- fontLoadListeners() {
- if (!this.document || !this.document.fonts) {
- return;
- }
-
- this.document.fonts.ready.then(function () {
- this.resizeCheck();
- }.bind(this));
-
- }
-
- /**
- * Get the documentElement
- * @returns {element} documentElement
- */
- root() {
- if(!this.document) return null;
- return this.document.documentElement;
- }
-
- /**
- * Get the location offset of a EpubCFI or an #id
- * @param {string | EpubCFI} target
- * @param {string} [ignoreClass] for the cfi
- * @returns { {left: Number, top: Number }
- */
- locationOf(target, ignoreClass) {
- var position;
- var targetPos = {"left": 0, "top": 0};
-
- if(!this.document) return targetPos;
-
- if(this.epubcfi.isCfiString(target)) {
- let range = new EpubCFI(target).toRange(this.document, ignoreClass);
-
- if(range) {
- try {
- if (!range.endContainer ||
- (range.startContainer == range.endContainer
- && range.startOffset == range.endOffset)) {
- // If the end for the range is not set, it results in collapsed becoming
- // true. This in turn leads to inconsistent behaviour when calling
- // getBoundingRect. Wrong bounds lead to the wrong page being displayed.
- // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/15684911/
- let pos = range.startContainer.textContent.indexOf(" ", range.startOffset);
- if (pos == -1) {
- pos = range.startContainer.textContent.length;
- }
- range.setEnd(range.startContainer, pos);
- }
- } catch (e) {
- console.error("setting end offset to start container length failed", e);
- }
-
- if (range.startContainer.nodeType === Node.ELEMENT_NODE) {
- position = range.startContainer.getBoundingClientRect();
- targetPos.left = position.left;
- targetPos.top = position.top;
- } else {
- // Webkit does not handle collapsed range bounds correctly
- // https://bugs.webkit.org/show_bug.cgi?id=138949
-
- // Construct a new non-collapsed range
- if (isWebkit) {
- let container = range.startContainer;
- let newRange = new Range();
- try {
- if (container.nodeType === ELEMENT_NODE) {
- position = container.getBoundingClientRect();
- } else if (range.startOffset + 2 < container.length) {
- newRange.setStart(container, range.startOffset);
- newRange.setEnd(container, range.startOffset + 2);
- position = newRange.getBoundingClientRect();
- } else if (range.startOffset - 2 > 0) {
- newRange.setStart(container, range.startOffset - 2);
- newRange.setEnd(container, range.startOffset);
- position = newRange.getBoundingClientRect();
- } else { // empty, return the parent element
- position = container.parentNode.getBoundingClientRect();
- }
- } catch (e) {
- console.error(e, e.stack);
- }
- } else {
- position = range.getBoundingClientRect();
- }
- }
- }
-
- } else if(typeof target === "string" &&
- target.indexOf("#") > -1) {
-
- let id = target.substring(target.indexOf("#")+1);
- let el = this.document.getElementById(id);
- if(el) {
- if (isWebkit) {
- // Webkit reports incorrect bounding rects in Columns
- let newRange = new Range();
- newRange.selectNode(el);
- position = newRange.getBoundingClientRect();
- } else {
- position = el.getBoundingClientRect();
- }
- }
- }
-
- if (position) {
- targetPos.left = position.left;
- targetPos.top = position.top;
- }
-
- return targetPos;
- }
-
- /**
- * Append a stylesheet link to the document head
- * @param {string} src url
- */
- addStylesheet(src) {
- return new Promise(function(resolve, reject){
- var $stylesheet;
- var ready = false;
-
- if(!this.document) {
- resolve(false);
- return;
- }
-
- // Check if link already exists
- $stylesheet = this.document.querySelector("link[href='"+src+"']");
- if ($stylesheet) {
- resolve(true);
- return; // already present
- }
-
- $stylesheet = this.document.createElement("link");
- $stylesheet.type = "text/css";
- $stylesheet.rel = "stylesheet";
- $stylesheet.href = src;
- $stylesheet.onload = $stylesheet.onreadystatechange = function() {
- if ( !ready && (!this.readyState || this.readyState == "complete") ) {
- ready = true;
- // Let apply
- setTimeout(() => {
- resolve(true);
- }, 1);
- }
- };
-
- this.document.head.appendChild($stylesheet);
-
- }.bind(this));
- }
-
- _getStylesheetNode(key) {
- var styleEl;
- key = "epubjs-inserted-css-" + (key || '');
-
- if(!this.document) return false;
-
- // Check if link already exists
- styleEl = this.document.getElementById(key);
- if (!styleEl) {
- styleEl = this.document.createElement("style");
- styleEl.id = key;
- // Append style element to head
- this.document.head.appendChild(styleEl);
- }
- return styleEl;
- }
-
- /**
- * Append stylesheet css
- * @param {string} serializedCss
- * @param {string} key If the key is the same, the CSS will be replaced instead of inserted
- */
- addStylesheetCss(serializedCss, key) {
- if(!this.document || !serializedCss) return false;
-
- var styleEl;
- styleEl = this._getStylesheetNode(key);
- styleEl.innerHTML = serializedCss;
-
- return true;
- }
-
- /**
- * Append stylesheet rules to a generate stylesheet
- * Array: https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule
- * Object: https://github.com/desirable-objects/json-to-css
- * @param {array | object} rules
- * @param {string} key If the key is the same, the CSS will be replaced instead of inserted
- */
- addStylesheetRules(rules, key) {
- var styleSheet;
-
- if(!this.document || !rules || rules.length === 0) return;
-
- // Grab style sheet
- styleSheet = this._getStylesheetNode(key).sheet;
-
- if (Object.prototype.toString.call(rules) === "[object Array]") {
- for (var i = 0, rl = rules.length; i < rl; i++) {
- var j = 1, rule = rules[i], selector = rules[i][0], propStr = "";
- // If the second argument of a rule is an array of arrays, correct our variables.
- if (Object.prototype.toString.call(rule[1][0]) === "[object Array]") {
- rule = rule[1];
- j = 0;
- }
-
- for (var pl = rule.length; j < pl; j++) {
- var prop = rule[j];
- propStr += prop[0] + ":" + prop[1] + (prop[2] ? " !important" : "") + ";\n";
- }
-
- // Insert CSS Rule
- styleSheet.insertRule(selector + "{" + propStr + "}", styleSheet.cssRules.length);
- }
- } else {
- const selectors = Object.keys(rules);
- selectors.forEach((selector) => {
- const definition = rules[selector];
- if (Array.isArray(definition)) {
- definition.forEach((item) => {
- const _rules = Object.keys(item);
- const result = _rules.map((rule) => {
- return `${rule}:${item[rule]}`;
- }).join(';');
- styleSheet.insertRule(`${selector}{${result}}`, styleSheet.cssRules.length);
- });
- } else {
- const _rules = Object.keys(definition);
- const result = _rules.map((rule) => {
- return `${rule}:${definition[rule]}`;
- }).join(';');
- styleSheet.insertRule(`${selector}{${result}}`, styleSheet.cssRules.length);
- }
- });
- }
- }
-
- /**
- * Append a script tag to the document head
- * @param {string} src url
- * @returns {Promise} loaded
- */
- addScript(src) {
-
- return new Promise(function(resolve, reject){
- var $script;
- var ready = false;
-
- if(!this.document) {
- resolve(false);
- return;
- }
-
- $script = this.document.createElement("script");
- $script.type = "text/javascript";
- $script.async = true;
- $script.src = src;
- $script.onload = $script.onreadystatechange = function() {
- if ( !ready && (!this.readyState || this.readyState == "complete") ) {
- ready = true;
- setTimeout(function(){
- resolve(true);
- }, 1);
- }
- };
-
- this.document.head.appendChild($script);
-
- }.bind(this));
- }
-
- /**
- * Add a class to the contents container
- * @param {string} className
- */
- addClass(className) {
- var content;
-
- if(!this.document) return;
-
- content = this.content || this.document.body;
-
- if (content) {
- content.classList.add(className);
- }
-
- }
-
- /**
- * Remove a class from the contents container
- * @param {string} removeClass
- */
- removeClass(className) {
- var content;
-
- if(!this.document) return;
-
- content = this.content || this.document.body;
-
- if (content) {
- content.classList.remove(className);
- }
-
- }
-
- /**
- * Add DOM event listeners
- * @private
- */
- addEventListeners(){
- if(!this.document) {
- return;
- }
-
- this._triggerEvent = this.triggerEvent.bind(this);
-
- DOM_EVENTS.forEach(function(eventName){
- this.document.addEventListener(eventName, this._triggerEvent, { passive: true });
- }, this);
-
- }
-
- /**
- * Remove DOM event listeners
- * @private
- */
- removeEventListeners(){
- if(!this.document) {
- return;
- }
- DOM_EVENTS.forEach(function(eventName){
- this.document.removeEventListener(eventName, this._triggerEvent, { passive: true });
- }, this);
- this._triggerEvent = undefined;
- }
-
- /**
- * Emit passed browser events
- * @private
- */
- triggerEvent(e){
- this.emit(e.type, e);
- }
-
- /**
- * Add listener for text selection
- * @private
- */
- addSelectionListeners(){
- if(!this.document) {
- return;
- }
- this._onSelectionChange = this.onSelectionChange.bind(this);
- this.document.addEventListener("selectionchange", this._onSelectionChange, { passive: true });
- }
-
- /**
- * Remove listener for text selection
- * @private
- */
- removeSelectionListeners(){
- if(!this.document) {
- return;
- }
- this.document.removeEventListener("selectionchange", this._onSelectionChange, { passive: true });
- this._onSelectionChange = undefined;
- }
-
- /**
- * Handle getting text on selection
- * @private
- */
- onSelectionChange(e){
- if (this.selectionEndTimeout) {
- clearTimeout(this.selectionEndTimeout);
- }
- this.selectionEndTimeout = setTimeout(function() {
- var selection = this.window.getSelection();
- this.triggerSelectedEvent(selection);
- }.bind(this), 250);
- }
-
- /**
- * Emit event on text selection
- * @private
- */
- triggerSelectedEvent(selection){
- var range, cfirange;
-
- if (selection && selection.rangeCount > 0) {
- range = selection.getRangeAt(0);
- if(!range.collapsed) {
- // cfirange = this.section.cfiFromRange(range);
- cfirange = new EpubCFI(range, this.cfiBase).toString();
- this.emit(EVENTS.CONTENTS.SELECTED, cfirange);
- this.emit(EVENTS.CONTENTS.SELECTED_RANGE, range);
- }
- }
- }
-
- /**
- * Get a Dom Range from EpubCFI
- * @param {EpubCFI} _cfi
- * @param {string} [ignoreClass]
- * @returns {Range} range
- */
- range(_cfi, ignoreClass){
- var cfi = new EpubCFI(_cfi);
- return cfi.toRange(this.document, ignoreClass);
- }
-
- /**
- * Get an EpubCFI from a Dom Range
- * @param {Range} range
- * @param {string} [ignoreClass]
- * @returns {EpubCFI} cfi
- */
- cfiFromRange(range, ignoreClass){
- return new EpubCFI(range, this.cfiBase, ignoreClass).toString();
- }
-
- /**
- * Get an EpubCFI from a Dom node
- * @param {node} node
- * @param {string} [ignoreClass]
- * @returns {EpubCFI} cfi
- */
- cfiFromNode(node, ignoreClass){
- return new EpubCFI(node, this.cfiBase, ignoreClass).toString();
- }
-
- // TODO: find where this is used - remove?
- map(layout){
- var map = new Mapping(layout);
- return map.section();
- }
-
- /**
- * Size the contents to a given width and height
- * @param {number} [width]
- * @param {number} [height]
- */
- size(width, height){
- var viewport = { scale: 1.0, scalable: "no" };
-
- this.layoutStyle("scrolling");
-
- if (width >= 0) {
- this.width(width);
- viewport.width = width;
- this.css("padding", "0 "+(width/12)+"px");
- }
-
- if (height >= 0) {
- this.height(height);
- viewport.height = height;
- }
-
- this.css("margin", "0");
- this.css("box-sizing", "border-box");
-
-
- this.viewport(viewport);
- }
-
- /**
- * Apply columns to the contents for pagination
- * @param {number} width
- * @param {number} height
- * @param {number} columnWidth
- * @param {number} gap
- */
- columns(width, height, columnWidth, gap, dir){
- let COLUMN_AXIS = prefixed("column-axis");
- let COLUMN_GAP = prefixed("column-gap");
- let COLUMN_WIDTH = prefixed("column-width");
- let COLUMN_FILL = prefixed("column-fill");
-
- let writingMode = this.writingMode();
- let axis = (writingMode.indexOf("vertical") === 0) ? "vertical" : "horizontal";
-
- this.layoutStyle("paginated");
-
- if (dir === "rtl" && axis === "horizontal") {
- this.direction(dir);
- }
-
- this.width(width);
- this.height(height);
-
- // Deal with Mobile trying to scale to viewport
- this.viewport({ width: width, height: height, scale: 1.0, scalable: "no" });
-
- // TODO: inline-block needs more testing
- // Fixes Safari column cut offs, but causes RTL issues
- // this.css("display", "inline-block");
-
- this.css("overflow-y", "hidden");
- this.css("margin", "0", true);
-
- if (axis === "vertical") {
- this.css("padding-top", (gap / 2) + "px", true);
- this.css("padding-bottom", (gap / 2) + "px", true);
- this.css("padding-left", "20px");
- this.css("padding-right", "20px");
- this.css(COLUMN_AXIS, "vertical");
- } else {
- this.css("padding-top", "20px");
- this.css("padding-bottom", "20px");
- this.css("padding-left", (gap / 2) + "px", true);
- this.css("padding-right", (gap / 2) + "px", true);
- this.css(COLUMN_AXIS, "horizontal");
- }
-
- this.css("box-sizing", "border-box");
- this.css("max-width", "inherit");
-
- this.css(COLUMN_FILL, "auto");
-
- this.css(COLUMN_GAP, gap+"px");
- this.css(COLUMN_WIDTH, columnWidth+"px");
-
- // Fix glyph clipping in WebKit
- // https://github.com/futurepress/epub.js/issues/983
- this.css("-webkit-line-box-contain", "block glyphs replaced");
- }
-
- /**
- * Scale contents from center
- * @param {number} scale
- * @param {number} offsetX
- * @param {number} offsetY
- */
- scaler(scale, offsetX, offsetY){
- var scaleStr = "scale(" + scale + ")";
- var translateStr = "";
- // this.css("position", "absolute"));
- this.css("transform-origin", "top left");
-
- if (offsetX >= 0 || offsetY >= 0) {
- translateStr = " translate(" + (offsetX || 0 )+ "px, " + (offsetY || 0 )+ "px )";
- }
-
- this.css("transform", scaleStr + translateStr);
- }
-
- /**
- * Fit contents into a fixed width and height
- * @param {number} width
- * @param {number} height
- */
- fit(width, height, section){
- var viewport = this.viewport();
- var viewportWidth = parseInt(viewport.width);
- var viewportHeight = parseInt(viewport.height);
- var widthScale = width / viewportWidth;
- var heightScale = height / viewportHeight;
- var scale = widthScale < heightScale ? widthScale : heightScale;
-
- // the translate does not work as intended, elements can end up unaligned
- // var offsetY = (height - (viewportHeight * scale)) / 2;
- // var offsetX = 0;
- // if (this.sectionIndex % 2 === 1) {
- // offsetX = width - (viewportWidth * scale);
- // }
-
- this.layoutStyle("paginated");
-
- // scale needs width and height to be set
- this.width(viewportWidth);
- this.height(viewportHeight);
- this.overflow("hidden");
-
- // Scale to the correct size
- this.scaler(scale, 0, 0);
- // this.scaler(scale, offsetX > 0 ? offsetX : 0, offsetY);
-
- // background images are not scaled by transform
- this.css("background-size", viewportWidth * scale + "px " + viewportHeight * scale + "px");
-
- this.css("background-color", "transparent");
- if (section && section.properties.includes("page-spread-left")) {
- // set margin since scale is weird
- var marginLeft = width - (viewportWidth * scale);
- this.css("margin-left", marginLeft + "px");
- }
- }
-
- /**
- * Set the direction of the text
- * @param {string} [dir="ltr"] "rtl" | "ltr"
- */
- direction(dir) {
- if (this.documentElement) {
- this.documentElement.style["direction"] = dir;
- }
- }
-
- mapPage(cfiBase, layout, start, end, dev) {
- var mapping = new Mapping(layout, dev);
-
- return mapping.page(this, cfiBase, start, end);
- }
-
- /**
- * Emit event when link in content is clicked
- * @private
- */
- linksHandler() {
- replaceLinks(this.content, (href) => {
- this.emit(EVENTS.CONTENTS.LINK_CLICKED, href);
- });
- }
-
- /**
- * Set the writingMode of the text
- * @param {string} [mode="horizontal-tb"] "horizontal-tb" | "vertical-rl" | "vertical-lr"
- */
- writingMode(mode) {
- let WRITING_MODE = prefixed("writing-mode");
-
- if (mode && this.documentElement) {
- this.documentElement.style[WRITING_MODE] = mode;
- }
-
- return this.window.getComputedStyle(this.documentElement)[WRITING_MODE] || '';
- }
-
- /**
- * Set the layoutStyle of the content
- * @param {string} [style="paginated"] "scrolling" | "paginated"
- * @private
- */
- layoutStyle(style) {
-
- if (style) {
- this._layoutStyle = style;
- navigator.epubReadingSystem.layoutStyle = this._layoutStyle;
- }
-
- return this._layoutStyle || "paginated";
- }
-
- /**
- * Add the epubReadingSystem object to the navigator
- * @param {string} name
- * @param {string} version
- * @private
- */
- epubReadingSystem(name, version) {
- navigator.epubReadingSystem = {
- name: name,
- version: version,
- layoutStyle: this.layoutStyle(),
- hasFeature: function (feature) {
- switch (feature) {
- case "dom-manipulation":
- return true;
- case "layout-changes":
- return true;
- case "touch-events":
- return true;
- case "mouse-events":
- return true;
- case "keyboard-events":
- return true;
- case "spine-scripting":
- return false;
- default:
- return false;
- }
- }
- };
- return navigator.epubReadingSystem;
- }
-
- destroy() {
- // this.document.removeEventListener('transitionend', this._resizeCheck);
-
- this.removeListeners();
-
- }
+ constructor(doc, content, cfiBase, sectionIndex, settings) {
+ // Blank Cfi for Parsing
+ this.epubcfi = new EpubCFI();
+ this.settings = settings;
+ this.document = doc;
+ this.documentElement = this.document.documentElement;
+ this.content = content || this.document.body;
+ this.window = this.document.defaultView;
+
+ this._size = {
+ width: 0,
+ height: 0,
+ };
+
+ this.sectionIndex = sectionIndex || 0;
+ this.cfiBase = cfiBase || "";
+
+ this.epubReadingSystem("epub.js", EPUBJS_VERSION);
+ this.called = 0;
+ this.active = true;
+ this.listeners();
+ }
+
+ /**
+ * Get DOM events that are listened for and passed along
+ */
+ static get listenedEvents() {
+ return DOM_EVENTS;
+ }
+
+ /**
+ * Get or Set width
+ * @param {number} [w]
+ * @returns {number} width
+ */
+ width(w) {
+ // var frame = this.documentElement;
+ var frame = this.content;
+
+ if (w && isNumber(w)) {
+ w = w + "px";
+ }
+
+ if (w) {
+ frame.style.width = w;
+ // this.content.style.width = w;
+ }
+
+ return parseInt(this.window.getComputedStyle(frame)["width"]);
+ }
+
+ /**
+ * Get or Set height
+ * @param {number} [h]
+ * @returns {number} height
+ */
+ height(h) {
+ // var frame = this.documentElement;
+ var frame = this.content;
+
+ if (h && isNumber(h)) {
+ h = h + "px";
+ }
+
+ if (h) {
+ frame.style.height = h;
+ // this.content.style.height = h;
+ }
+
+ return parseInt(this.window.getComputedStyle(frame)["height"]);
+ }
+
+ /**
+ * Get or Set width of the contents
+ * @param {number} [w]
+ * @returns {number} width
+ */
+ contentWidth(w) {
+ var content = this.content || this.document.body;
+
+ if (w && isNumber(w)) {
+ w = w + "px";
+ }
+
+ if (w) {
+ content.style.width = w;
+ }
+
+ return parseInt(this.window.getComputedStyle(content)["width"]);
+ }
+
+ /**
+ * Get or Set height of the contents
+ * @param {number} [h]
+ * @returns {number} height
+ */
+ contentHeight(h) {
+ var content = this.content || this.document.body;
+
+ if (h && isNumber(h)) {
+ h = h + "px";
+ }
+
+ if (h) {
+ content.style.height = h;
+ }
+
+ return parseInt(this.window.getComputedStyle(content)["height"]);
+ }
+
+ /**
+ * Get the width of the text using Range
+ * @returns {number} width
+ */
+ textWidth() {
+ let rect;
+ let width;
+ let range = this.document.createRange();
+ let content = this.content || this.document.body;
+ let border = borders(content);
+
+ // Select the contents of frame
+ range.selectNodeContents(content);
+
+ // get the width of the text content
+ rect = range.getBoundingClientRect();
+ width = rect.width;
+
+ if (border && border.width) {
+ width += border.width;
+ }
+
+ return Math.round(width);
+ }
+
+ /**
+ * Get the height of the text using Range
+ * @returns {number} height
+ */
+ textHeight() {
+ let rect;
+ let height;
+ let range = this.document.createRange();
+ let content = this.content || this.document.body;
+
+ range.selectNodeContents(content);
+
+ rect = range.getBoundingClientRect();
+ height = rect.bottom;
+
+ return Math.round(height);
+ }
+
+ /**
+ * Get documentElement scrollWidth
+ * @returns {number} width
+ */
+ scrollWidth() {
+ var width = this.documentElement.scrollWidth;
+
+ return width;
+ }
+
+ /**
+ * Get documentElement scrollHeight
+ * @returns {number} height
+ */
+ scrollHeight() {
+ var height = this.documentElement.scrollHeight;
+
+ return height;
+ }
+
+ /**
+ * Set overflow css style of the contents
+ * @param {string} [overflow]
+ */
+ overflow(overflow) {
+ if (overflow) {
+ this.documentElement.style.overflow = overflow;
+ }
+
+ return this.window.getComputedStyle(this.documentElement)["overflow"];
+ }
+
+ /**
+ * Set overflowX css style of the documentElement
+ * @param {string} [overflow]
+ */
+ overflowX(overflow) {
+ if (overflow) {
+ this.documentElement.style.overflowX = overflow;
+ }
+
+ return this.window.getComputedStyle(this.documentElement)["overflowX"];
+ }
+
+ /**
+ * Set overflowY css style of the documentElement
+ * @param {string} [overflow]
+ */
+ overflowY(overflow) {
+ if (overflow) {
+ this.documentElement.style.overflowY = overflow;
+ }
+
+ return this.window.getComputedStyle(this.documentElement)["overflowY"];
+ }
+
+ /**
+ * Set Css styles on the contents element (typically Body)
+ * @param {string} property
+ * @param {string} value
+ * @param {boolean} [priority] set as "important"
+ */
+ css(property, value, priority) {
+ var content = this.content || this.document.body;
+
+ if (value) {
+ content.style.setProperty(property, value, priority ? "important" : "");
+ } else {
+ content.style.removeProperty(property);
+ }
+
+ return this.window.getComputedStyle(content)[property];
+ }
+
+ /**
+ * Get or Set the viewport element
+ * @param {object} [options]
+ * @param {string} [options.width]
+ * @param {string} [options.height]
+ * @param {string} [options.scale]
+ * @param {string} [options.minimum]
+ * @param {string} [options.maximum]
+ * @param {string} [options.scalable]
+ */
+ viewport(options) {
+ var _width, _height, _scale, _minimum, _maximum, _scalable;
+ // var width, height, scale, minimum, maximum, scalable;
+ var $viewport = this.document.querySelector("meta[name='viewport']");
+ var parsed = {
+ width: undefined,
+ height: undefined,
+ scale: undefined,
+ minimum: undefined,
+ maximum: undefined,
+ scalable: undefined,
+ };
+ var newContent = [];
+ var settings = {};
+
+ /*
+ * check for the viewport size
+ *
+ */
+ if ($viewport && $viewport.hasAttribute("content")) {
+ let content = $viewport.getAttribute("content");
+ let _width = content.match(/width\s*=\s*([^,]*)/);
+ let _height = content.match(/height\s*=\s*([^,]*)/);
+ let _scale = content.match(/initial-scale\s*=\s*([^,]*)/);
+ let _minimum = content.match(/minimum-scale\s*=\s*([^,]*)/);
+ let _maximum = content.match(/maximum-scale\s*=\s*([^,]*)/);
+ let _scalable = content.match(/user-scalable\s*=\s*([^,]*)/);
+
+ if (_width && _width.length && typeof _width[1] !== "undefined") {
+ parsed.width = _width[1];
+ }
+ if (_height && _height.length && typeof _height[1] !== "undefined") {
+ parsed.height = _height[1];
+ }
+ if (_scale && _scale.length && typeof _scale[1] !== "undefined") {
+ parsed.scale = _scale[1];
+ }
+ if (_minimum && _minimum.length && typeof _minimum[1] !== "undefined") {
+ parsed.minimum = _minimum[1];
+ }
+ if (_maximum && _maximum.length && typeof _maximum[1] !== "undefined") {
+ parsed.maximum = _maximum[1];
+ }
+ if (
+ _scalable &&
+ _scalable.length &&
+ typeof _scalable[1] !== "undefined"
+ ) {
+ parsed.scalable = _scalable[1];
+ }
+ }
+
+ settings = defaults(options || {}, parsed);
+
+ if (options) {
+ if (settings.width) {
+ newContent.push("width=" + settings.width);
+ }
+
+ if (settings.height) {
+ newContent.push("height=" + settings.height);
+ }
+
+ if (settings.scale) {
+ newContent.push("initial-scale=" + settings.scale);
+ }
+
+ if (settings.scalable === "no") {
+ newContent.push("minimum-scale=" + settings.scale);
+ newContent.push("maximum-scale=" + settings.scale);
+ newContent.push("user-scalable=" + settings.scalable);
+ } else {
+ if (settings.scalable) {
+ newContent.push("user-scalable=" + settings.scalable);
+ }
+
+ if (settings.minimum) {
+ newContent.push("minimum-scale=" + settings.minimum);
+ }
+
+ if (settings.maximum) {
+ newContent.push("minimum-scale=" + settings.maximum);
+ }
+ }
+
+ if (!$viewport) {
+ $viewport = this.document.createElement("meta");
+ $viewport.setAttribute("name", "viewport");
+ this.document.querySelector("head").appendChild($viewport);
+ }
+
+ $viewport.setAttribute("content", newContent.join(", "));
+
+ this.window.scrollTo(0, 0);
+ }
+
+ return settings;
+ }
+
+ /**
+ * Event emitter for when the contents has expanded
+ * @private
+ */
+ expand() {
+ this.emit(EVENTS.CONTENTS.EXPAND);
+ }
+
+ /**
+ * Add DOM listeners
+ * @private
+ */
+ listeners() {
+ this.imageLoadListeners();
+
+ this.mediaQueryListeners();
+
+ // this.fontLoadListeners();
+
+ this.addEventListeners();
+
+ this.addSelectionListeners();
+
+ // this.transitionListeners();
+
+ if (typeof ResizeObserver === "undefined") {
+ this.resizeListeners();
+ this.visibilityListeners();
+ } else {
+ this.resizeObservers();
+ }
+
+ // this.mutationObservers();
+
+ this.linksHandler();
+ }
+
+ /**
+ * Remove DOM listeners
+ * @private
+ */
+ removeListeners() {
+ this.removeEventListeners();
+
+ this.removeSelectionListeners();
+
+ if (this.observer) {
+ this.observer.disconnect();
+ }
+
+ clearTimeout(this.expanding);
+ }
+
+ /**
+ * Check if size of contents has changed and
+ * emit 'resize' event if it has.
+ * @private
+ */
+ resizeCheck() {
+ let width = this.textWidth();
+ let height = this.textHeight();
+
+ if (width != this._size.width || height != this._size.height) {
+ this._size = {
+ width: width,
+ height: height,
+ };
+
+ this.onResize && this.onResize(this._size);
+ this.emit(EVENTS.CONTENTS.RESIZE, this._size);
+ }
+ }
+
+ /**
+ * Poll for resize detection
+ * @private
+ */
+ resizeListeners() {
+ var width, height;
+ // Test size again
+ clearTimeout(this.expanding);
+ requestAnimationFrame(this.resizeCheck.bind(this));
+ this.expanding = setTimeout(this.resizeListeners.bind(this), 350);
+ }
+
+ /**
+ * Listen for visibility of tab to change
+ * @private
+ */
+ visibilityListeners() {
+ document.addEventListener("visibilitychange", () => {
+ if (document.visibilityState === "visible" && this.active === false) {
+ this.active = true;
+ this.resizeListeners();
+ } else {
+ this.active = false;
+ clearTimeout(this.expanding);
+ }
+ });
+ }
+
+ /**
+ * Use css transitions to detect resize
+ * @private
+ */
+ transitionListeners() {
+ let body = this.content;
+
+ body.style["transitionProperty"] =
+ "font, font-size, font-size-adjust, font-stretch, font-variation-settings, font-weight, width, height";
+ body.style["transitionDuration"] = "0.001ms";
+ body.style["transitionTimingFunction"] = "linear";
+ body.style["transitionDelay"] = "0";
+
+ this._resizeCheck = this.resizeCheck.bind(this);
+ this.document.addEventListener("transitionend", this._resizeCheck);
+ }
+
+ /**
+ * Listen for media query changes and emit 'expand' event
+ * Adapted from: https://github.com/tylergaw/media-query-events/blob/master/js/mq-events.js
+ * @private
+ */
+ mediaQueryListeners() {
+ var sheets = this.document.styleSheets;
+ var mediaChangeHandler = function (m) {
+ if (m.matches && !this._expanding) {
+ setTimeout(this.expand.bind(this), 1);
+ }
+ }.bind(this);
+
+ for (var i = 0; i < sheets.length; i += 1) {
+ var rules;
+ // Firefox errors if we access cssRules cross-domain
+ try {
+ rules = sheets[i].cssRules;
+ } catch (e) {
+ return;
+ }
+ if (!rules) return; // Stylesheets changed
+ for (var j = 0; j < rules.length; j += 1) {
+ //if (rules[j].constructor === CSSMediaRule) {
+ if (rules[j].media) {
+ var mql = this.window.matchMedia(rules[j].media.mediaText);
+ mql.addListener(mediaChangeHandler);
+ //mql.onchange = mediaChangeHandler;
+ }
+ }
+ }
+ }
+
+ /**
+ * Use ResizeObserver to listen for changes in the DOM and check for resize
+ * @private
+ */
+ resizeObservers() {
+ // create an observer instance
+ this.observer = new ResizeObserver((e) => {
+ requestAnimationFrame(this.resizeCheck.bind(this));
+ });
+
+ // pass in the target node
+ this.observer.observe(this.document.documentElement);
+ }
+
+ /**
+ * Use MutationObserver to listen for changes in the DOM and check for resize
+ * @private
+ */
+ mutationObservers() {
+ // create an observer instance
+ this.observer = new MutationObserver((mutations) => {
+ this.resizeCheck();
+ });
+
+ // configuration of the observer:
+ let config = {
+ attributes: true,
+ childList: true,
+ characterData: true,
+ subtree: true,
+ };
+
+ // pass in the target node, as well as the observer options
+ this.observer.observe(this.document, config);
+ }
+
+ /**
+ * Test if images are loaded or add listener for when they load
+ * @private
+ */
+ imageLoadListeners() {
+ var images = this.document.querySelectorAll("img");
+ var img;
+ for (var i = 0; i < images.length; i++) {
+ img = images[i];
+
+ if (typeof img.naturalWidth !== "undefined" && img.naturalWidth === 0) {
+ img.onload = this.expand.bind(this);
+ }
+ }
+ }
+
+ /**
+ * Listen for font load and check for resize when loaded
+ * @private
+ */
+ fontLoadListeners() {
+ if (!this.document || !this.document.fonts) {
+ return;
+ }
+
+ this.document.fonts.ready.then(
+ function () {
+ this.resizeCheck();
+ }.bind(this)
+ );
+ }
+
+ /**
+ * Get the documentElement
+ * @returns {element} documentElement
+ */
+ root() {
+ if (!this.document) return null;
+ return this.document.documentElement;
+ }
+
+ /**
+ * Get the location offset of a EpubCFI or an #id
+ * @param {string | EpubCFI} target
+ * @param {string} [ignoreClass] for the cfi
+ * @returns { {left: Number, top: Number }
+ */
+ locationOf(target, ignoreClass) {
+ var position;
+ var targetPos = { left: 0, top: 0 };
+
+ if (!this.document) return targetPos;
+
+ if (this.epubcfi.isCfiString(target)) {
+ let range = new EpubCFI(target).toRange(this.document, ignoreClass);
+
+ if (range) {
+ try {
+ if (
+ !range.endContainer ||
+ (range.startContainer == range.endContainer &&
+ range.startOffset == range.endOffset)
+ ) {
+ // If the end for the range is not set, it results in collapsed becoming
+ // true. This in turn leads to inconsistent behaviour when calling
+ // getBoundingRect. Wrong bounds lead to the wrong page being displayed.
+ // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/15684911/
+ let pos = range.startContainer.textContent.indexOf(
+ " ",
+ range.startOffset
+ );
+ if (pos == -1) {
+ pos = range.startContainer.textContent.length;
+ }
+ range.setEnd(range.startContainer, pos);
+ }
+ } catch (e) {
+ console.error(
+ "setting end offset to start container length failed",
+ e
+ );
+ }
+
+ if (range.startContainer.nodeType === Node.ELEMENT_NODE) {
+ position = range.startContainer.getBoundingClientRect();
+ targetPos.left = position.left;
+ targetPos.top = position.top;
+ } else {
+ // Webkit does not handle collapsed range bounds correctly
+ // https://bugs.webkit.org/show_bug.cgi?id=138949
+
+ // Construct a new non-collapsed range
+ if (isWebkit) {
+ let container = range.startContainer;
+ let newRange = new Range();
+ try {
+ if (container.nodeType === ELEMENT_NODE) {
+ position = container.getBoundingClientRect();
+ } else if (range.startOffset + 2 < container.length) {
+ newRange.setStart(container, range.startOffset);
+ newRange.setEnd(container, range.startOffset + 2);
+ position = newRange.getBoundingClientRect();
+ } else if (range.startOffset - 2 > 0) {
+ newRange.setStart(container, range.startOffset - 2);
+ newRange.setEnd(container, range.startOffset);
+ position = newRange.getBoundingClientRect();
+ } else {
+ // empty, return the parent element
+ position = container.parentNode.getBoundingClientRect();
+ }
+ } catch (e) {
+ console.error(e, e.stack);
+ }
+ } else {
+ position = range.getBoundingClientRect();
+ }
+ }
+ }
+ } else if (typeof target === "string" && target.indexOf("#") > -1) {
+ let id = target.substring(target.indexOf("#") + 1);
+ let el = this.document.getElementById(id);
+ if (el) {
+ if (isWebkit) {
+ // Webkit reports incorrect bounding rects in Columns
+ let newRange = new Range();
+ newRange.selectNode(el);
+ position = newRange.getBoundingClientRect();
+ } else {
+ position = el.getBoundingClientRect();
+ }
+ }
+ }
+
+ if (position) {
+ targetPos.left = position.left;
+ targetPos.top = position.top;
+ }
+
+ return targetPos;
+ }
+
+ /**
+ * Append a stylesheet link to the document head
+ * @param {string} src url
+ */
+ addStylesheet(src) {
+ return new Promise(
+ function (resolve, reject) {
+ var $stylesheet;
+ var ready = false;
+
+ if (!this.document) {
+ resolve(false);
+ return;
+ }
+
+ // Check if link already exists
+ $stylesheet = this.document.querySelector("link[href='" + src + "']");
+ if ($stylesheet) {
+ resolve(true);
+ return; // already present
+ }
+
+ $stylesheet = this.document.createElement("link");
+ $stylesheet.type = "text/css";
+ $stylesheet.rel = "stylesheet";
+ $stylesheet.href = src;
+ $stylesheet.onload = $stylesheet.onreadystatechange = function () {
+ if (!ready && (!this.readyState || this.readyState == "complete")) {
+ ready = true;
+ // Let apply
+ setTimeout(() => {
+ resolve(true);
+ }, 1);
+ }
+ };
+
+ this.document.head.appendChild($stylesheet);
+ }.bind(this)
+ );
+ }
+
+ _getStylesheetNode(key) {
+ var styleEl;
+ key = "epubjs-inserted-css-" + (key || "");
+
+ if (!this.document) return false;
+
+ // Check if link already exists
+ styleEl = this.document.getElementById(key);
+ if (!styleEl) {
+ styleEl = this.document.createElement("style");
+ styleEl.id = key;
+ // Append style element to head
+ this.document.head.appendChild(styleEl);
+ }
+ return styleEl;
+ }
+
+ /**
+ * Append stylesheet css
+ * @param {string} serializedCss
+ * @param {string} key If the key is the same, the CSS will be replaced instead of inserted
+ */
+ addStylesheetCss(serializedCss, key) {
+ if (!this.document || !serializedCss) return false;
+
+ var styleEl;
+ styleEl = this._getStylesheetNode(key);
+ styleEl.innerHTML = serializedCss;
+
+ return true;
+ }
+
+ /**
+ * Append stylesheet rules to a generate stylesheet
+ * Array: https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule
+ * Object: https://github.com/desirable-objects/json-to-css
+ * @param {array | object} rules
+ * @param {string} key If the key is the same, the CSS will be replaced instead of inserted
+ */
+ addStylesheetRules(rules, key) {
+ var styleSheet;
+
+ if (!this.document || !rules || rules.length === 0) return;
+
+ // Grab style sheet
+ styleSheet = this._getStylesheetNode(key).sheet;
+
+ if (Object.prototype.toString.call(rules) === "[object Array]") {
+ for (var i = 0, rl = rules.length; i < rl; i++) {
+ var j = 1,
+ rule = rules[i],
+ selector = rules[i][0],
+ propStr = "";
+ // If the second argument of a rule is an array of arrays, correct our variables.
+ if (Object.prototype.toString.call(rule[1][0]) === "[object Array]") {
+ rule = rule[1];
+ j = 0;
+ }
+
+ for (var pl = rule.length; j < pl; j++) {
+ var prop = rule[j];
+ propStr +=
+ prop[0] + ":" + prop[1] + (prop[2] ? " !important" : "") + ";\n";
+ }
+
+ // Insert CSS Rule
+ styleSheet.insertRule(
+ selector + "{" + propStr + "}",
+ styleSheet.cssRules.length
+ );
+ }
+ } else {
+ const selectors = Object.keys(rules);
+ selectors.forEach((selector) => {
+ const definition = rules[selector];
+ if (Array.isArray(definition)) {
+ definition.forEach((item) => {
+ const _rules = Object.keys(item);
+ const result = _rules
+ .map((rule) => {
+ return `${rule}:${item[rule]}`;
+ })
+ .join(";");
+ styleSheet.insertRule(
+ `${selector}{${result}}`,
+ styleSheet.cssRules.length
+ );
+ });
+ } else {
+ const _rules = Object.keys(definition);
+ const result = _rules
+ .map((rule) => {
+ return `${rule}:${definition[rule]}`;
+ })
+ .join(";");
+ styleSheet.insertRule(
+ `${selector}{${result}}`,
+ styleSheet.cssRules.length
+ );
+ }
+ });
+ }
+ }
+
+ /**
+ * Append a script tag to the document head
+ * @param {string} src url
+ * @returns {Promise} loaded
+ */
+ addScript(src) {
+ return new Promise(
+ function (resolve, reject) {
+ var $script;
+ var ready = false;
+
+ if (!this.document) {
+ resolve(false);
+ return;
+ }
+
+ $script = this.document.createElement("script");
+ $script.type = "text/javascript";
+ $script.async = true;
+ $script.src = src;
+ $script.onload = $script.onreadystatechange = function () {
+ if (!ready && (!this.readyState || this.readyState == "complete")) {
+ ready = true;
+ setTimeout(function () {
+ resolve(true);
+ }, 1);
+ }
+ };
+
+ this.document.head.appendChild($script);
+ }.bind(this)
+ );
+ }
+
+ /**
+ * Add a class to the contents container
+ * @param {string} className
+ */
+ addClass(className) {
+ var content;
+
+ if (!this.document) return;
+
+ content = this.content || this.document.body;
+
+ if (content) {
+ content.classList.add(className);
+ }
+ }
+
+ /**
+ * Remove a class from the contents container
+ * @param {string} removeClass
+ */
+ removeClass(className) {
+ var content;
+
+ if (!this.document) return;
+
+ content = this.content || this.document.body;
+
+ if (content) {
+ content.classList.remove(className);
+ }
+ }
+
+ /**
+ * Add DOM event listeners
+ * @private
+ */
+ addEventListeners() {
+ if (!this.document) {
+ return;
+ }
+
+ this._triggerEvent = this.triggerEvent.bind(this);
+
+ DOM_EVENTS.forEach(function (eventName) {
+ this.document.addEventListener(eventName, this._triggerEvent, {
+ passive: true,
+ });
+ }, this);
+ }
+
+ /**
+ * Remove DOM event listeners
+ * @private
+ */
+ removeEventListeners() {
+ if (!this.document) {
+ return;
+ }
+ DOM_EVENTS.forEach(function (eventName) {
+ this.document.removeEventListener(eventName, this._triggerEvent, {
+ passive: true,
+ });
+ }, this);
+ this._triggerEvent = undefined;
+ }
+
+ /**
+ * Emit passed browser events
+ * @private
+ */
+ triggerEvent(e) {
+ this.emit(e.type, e);
+ }
+
+ /**
+ * Add listener for text selection
+ * @private
+ */
+ addSelectionListeners() {
+ if (!this.document) {
+ return;
+ }
+ this._onSelectionChange = this.onSelectionChange.bind(this);
+ this.document.addEventListener("selectionchange", this._onSelectionChange, {
+ passive: true,
+ });
+ }
+
+ /**
+ * Remove listener for text selection
+ * @private
+ */
+ removeSelectionListeners() {
+ if (!this.document) {
+ return;
+ }
+ this.document.removeEventListener(
+ "selectionchange",
+ this._onSelectionChange,
+ { passive: true }
+ );
+ this._onSelectionChange = undefined;
+ }
+
+ /**
+ * Handle getting text on selection
+ * @private
+ */
+ onSelectionChange(e) {
+ if (this.selectionEndTimeout) {
+ clearTimeout(this.selectionEndTimeout);
+ }
+ this.selectionEndTimeout = setTimeout(
+ function () {
+ var selection = this.window.getSelection();
+ this.triggerSelectedEvent(selection);
+ }.bind(this),
+ this.settings ? this.settings.selectionStopDelay : 250
+ );
+ }
+
+ /**
+ * Emit event on text selection
+ * @private
+ */
+ triggerSelectedEvent(selection) {
+ var range, cfirange;
+
+ if (selection && selection.rangeCount > 0) {
+ range = selection.getRangeAt(0);
+ if (!range.collapsed) {
+ // cfirange = this.section.cfiFromRange(range);
+ cfirange = new EpubCFI(range, this.cfiBase).toString();
+ this.emit(EVENTS.CONTENTS.SELECTED, cfirange);
+ this.emit(EVENTS.CONTENTS.SELECTED_RANGE, range);
+ }
+ }
+ }
+
+ /**
+ * Get a Dom Range from EpubCFI
+ * @param {EpubCFI} _cfi
+ * @param {string} [ignoreClass]
+ * @returns {Range} range
+ */
+ range(_cfi, ignoreClass) {
+ var cfi = new EpubCFI(_cfi);
+ return cfi.toRange(this.document, ignoreClass);
+ }
+
+ /**
+ * Get an EpubCFI from a Dom Range
+ * @param {Range} range
+ * @param {string} [ignoreClass]
+ * @returns {EpubCFI} cfi
+ */
+ cfiFromRange(range, ignoreClass) {
+ return new EpubCFI(range, this.cfiBase, ignoreClass).toString();
+ }
+
+ /**
+ * Get an EpubCFI from a Dom node
+ * @param {node} node
+ * @param {string} [ignoreClass]
+ * @returns {EpubCFI} cfi
+ */
+ cfiFromNode(node, ignoreClass) {
+ return new EpubCFI(node, this.cfiBase, ignoreClass).toString();
+ }
+
+ // TODO: find where this is used - remove?
+ map(layout) {
+ var map = new Mapping(layout);
+ return map.section();
+ }
+
+ /**
+ * Size the contents to a given width and height
+ * @param {number} [width]
+ * @param {number} [height]
+ */
+ size(width, height) {
+ var viewport = { scale: 1.0, scalable: "no" };
+
+ this.layoutStyle("scrolling");
+
+ if (width >= 0) {
+ this.width(width);
+ viewport.width = width;
+ this.css("padding", "0 " + width / 12 + "px");
+ }
+
+ if (height >= 0) {
+ this.height(height);
+ viewport.height = height;
+ }
+
+ this.css("margin", "0");
+ this.css("box-sizing", "border-box");
+
+ this.viewport(viewport);
+ }
+
+ /**
+ * Apply columns to the contents for pagination
+ * @param {number} width
+ * @param {number} height
+ * @param {number} columnWidth
+ * @param {number} gap
+ */
+ columns(width, height, columnWidth, gap, dir) {
+ let COLUMN_AXIS = prefixed("column-axis");
+ let COLUMN_GAP = prefixed("column-gap");
+ let COLUMN_WIDTH = prefixed("column-width");
+ let COLUMN_FILL = prefixed("column-fill");
+
+ let writingMode = this.writingMode();
+ let axis =
+ writingMode.indexOf("vertical") === 0 ? "vertical" : "horizontal";
+
+ this.layoutStyle("paginated");
+
+ if (dir === "rtl" && axis === "horizontal") {
+ this.direction(dir);
+ }
+
+ this.width(width);
+ this.height(height);
+
+ // Deal with Mobile trying to scale to viewport
+ this.viewport({ width: width, height: height, scale: 1.0, scalable: "no" });
+
+ // TODO: inline-block needs more testing
+ // Fixes Safari column cut offs, but causes RTL issues
+ // this.css("display", "inline-block");
+
+ this.css("overflow-y", "hidden");
+ this.css("margin", "0", true);
+
+ if (axis === "vertical") {
+ this.css("padding-top", gap / 2 + "px", true);
+ this.css("padding-bottom", gap / 2 + "px", true);
+ this.css("padding-left", "20px");
+ this.css("padding-right", "20px");
+ this.css(COLUMN_AXIS, "vertical");
+ } else {
+ this.css("padding-top", "20px");
+ this.css("padding-bottom", "20px");
+ this.css("padding-left", gap / 2 + "px", true);
+ this.css("padding-right", gap / 2 + "px", true);
+ this.css(COLUMN_AXIS, "horizontal");
+ }
+
+ this.css("box-sizing", "border-box");
+ this.css("max-width", "inherit");
+
+ this.css(COLUMN_FILL, "auto");
+
+ this.css(COLUMN_GAP, gap + "px");
+ this.css(COLUMN_WIDTH, columnWidth + "px");
+
+ // Fix glyph clipping in WebKit
+ // https://github.com/futurepress/epub.js/issues/983
+ this.css("-webkit-line-box-contain", "block glyphs replaced");
+ }
+
+ /**
+ * Scale contents from center
+ * @param {number} scale
+ * @param {number} offsetX
+ * @param {number} offsetY
+ */
+ scaler(scale, offsetX, offsetY) {
+ var scaleStr = "scale(" + scale + ")";
+ var translateStr = "";
+ // this.css("position", "absolute"));
+ this.css("transform-origin", "top left");
+
+ if (offsetX >= 0 || offsetY >= 0) {
+ translateStr =
+ " translate(" + (offsetX || 0) + "px, " + (offsetY || 0) + "px )";
+ }
+
+ this.css("transform", scaleStr + translateStr);
+ }
+
+ /**
+ * Fit contents into a fixed width and height
+ * @param {number} width
+ * @param {number} height
+ */
+ fit(width, height, section) {
+ var viewport = this.viewport();
+ var viewportWidth = parseInt(viewport.width);
+ var viewportHeight = parseInt(viewport.height);
+ var widthScale = width / viewportWidth;
+ var heightScale = height / viewportHeight;
+ var scale = widthScale < heightScale ? widthScale : heightScale;
+
+ // the translate does not work as intended, elements can end up unaligned
+ // var offsetY = (height - (viewportHeight * scale)) / 2;
+ // var offsetX = 0;
+ // if (this.sectionIndex % 2 === 1) {
+ // offsetX = width - (viewportWidth * scale);
+ // }
+
+ this.layoutStyle("paginated");
+
+ // scale needs width and height to be set
+ this.width(viewportWidth);
+ this.height(viewportHeight);
+ this.overflow("hidden");
+
+ // Scale to the correct size
+ this.scaler(scale, 0, 0);
+ // this.scaler(scale, offsetX > 0 ? offsetX : 0, offsetY);
+
+ // background images are not scaled by transform
+ this.css(
+ "background-size",
+ viewportWidth * scale + "px " + viewportHeight * scale + "px"
+ );
+
+ this.css("background-color", "transparent");
+ if (section && section.properties.includes("page-spread-left")) {
+ // set margin since scale is weird
+ var marginLeft = width - viewportWidth * scale;
+ this.css("margin-left", marginLeft + "px");
+ }
+ }
+
+ /**
+ * Set the direction of the text
+ * @param {string} [dir="ltr"] "rtl" | "ltr"
+ */
+ direction(dir) {
+ if (this.documentElement) {
+ this.documentElement.style["direction"] = dir;
+ }
+ }
+
+ mapPage(cfiBase, layout, start, end, dev) {
+ var mapping = new Mapping(layout, dev);
+
+ return mapping.page(this, cfiBase, start, end);
+ }
+
+ /**
+ * Emit event when link in content is clicked
+ * @private
+ */
+ linksHandler() {
+ replaceLinks(this.content, (href) => {
+ this.emit(EVENTS.CONTENTS.LINK_CLICKED, href);
+ });
+ }
+
+ /**
+ * Set the writingMode of the text
+ * @param {string} [mode="horizontal-tb"] "horizontal-tb" | "vertical-rl" | "vertical-lr"
+ */
+ writingMode(mode) {
+ let WRITING_MODE = prefixed("writing-mode");
+
+ if (mode && this.documentElement) {
+ this.documentElement.style[WRITING_MODE] = mode;
+ }
+
+ return (
+ this.window.getComputedStyle(this.documentElement)[WRITING_MODE] || ""
+ );
+ }
+
+ /**
+ * Set the layoutStyle of the content
+ * @param {string} [style="paginated"] "scrolling" | "paginated"
+ * @private
+ */
+ layoutStyle(style) {
+ if (style) {
+ this._layoutStyle = style;
+ navigator.epubReadingSystem.layoutStyle = this._layoutStyle;
+ }
+
+ return this._layoutStyle || "paginated";
+ }
+
+ /**
+ * Add the epubReadingSystem object to the navigator
+ * @param {string} name
+ * @param {string} version
+ * @private
+ */
+ epubReadingSystem(name, version) {
+ navigator.epubReadingSystem = {
+ name: name,
+ version: version,
+ layoutStyle: this.layoutStyle(),
+ hasFeature: function (feature) {
+ switch (feature) {
+ case "dom-manipulation":
+ return true;
+ case "layout-changes":
+ return true;
+ case "touch-events":
+ return true;
+ case "mouse-events":
+ return true;
+ case "keyboard-events":
+ return true;
+ case "spine-scripting":
+ return false;
+ default:
+ return false;
+ }
+ },
+ };
+ return navigator.epubReadingSystem;
+ }
+
+ destroy() {
+ // this.document.removeEventListener('transitionend', this._resizeCheck);
+
+ this.removeListeners();
+ }
}
EventEmitter(Contents.prototype);
diff --git a/src/managers/continuous/index.js b/src/managers/continuous/index.js
index d61f8f76c..0c0c4bd75 100644
--- a/src/managers/continuous/index.js
+++ b/src/managers/continuous/index.js
@@ -1,592 +1,630 @@
-import {extend, defer, requestAnimationFrame} from "../../utils/core";
+import { extend, defer, requestAnimationFrame } from "../../utils/core";
import DefaultViewManager from "../default";
import Snap from "../helpers/snap";
import { EVENTS } from "../../utils/constants";
import debounce from "lodash/debounce";
class ContinuousViewManager extends DefaultViewManager {
- constructor(options) {
- super(options);
-
- this.name = "continuous";
-
- this.settings = extend(this.settings || {}, {
- infinite: true,
- overflow: undefined,
- axis: undefined,
- writingMode: undefined,
- flow: "scrolled",
- offset: 500,
- offsetDelta: 250,
- width: undefined,
- height: undefined,
- snap: false,
- afterScrolledTimeout: 10,
- allowScriptedContent: false,
- allowPopups: false
- });
-
- extend(this.settings, options.settings || {});
-
- // Gap can be 0, but defaults doesn't handle that
- if (options.settings.gap != "undefined" && options.settings.gap === 0) {
- this.settings.gap = options.settings.gap;
- }
-
- this.viewSettings = {
- ignoreClass: this.settings.ignoreClass,
- axis: this.settings.axis,
- flow: this.settings.flow,
- layout: this.layout,
- width: 0,
- height: 0,
- forceEvenPages: false,
- allowScriptedContent: this.settings.allowScriptedContent,
- allowPopups: this.settings.allowPopups
- };
-
- this.scrollTop = 0;
- this.scrollLeft = 0;
- }
-
- display(section, target){
- return DefaultViewManager.prototype.display.call(this, section, target)
- .then(function () {
- return this.fill();
- }.bind(this));
- }
-
- fill(_full){
- var full = _full || new defer();
-
- this.q.enqueue(() => {
- return this.check();
- }).then((result) => {
- if (result) {
- this.fill(full);
- } else {
- full.resolve();
- }
- });
-
- return full.promise;
- }
-
- moveTo(offset){
- // var bounds = this.stage.bounds();
- // var dist = Math.floor(offset.top / bounds.height) * bounds.height;
- var distX = 0,
- distY = 0;
-
- var offsetX = 0,
- offsetY = 0;
-
- if(!this.isPaginated) {
- distY = offset.top;
- offsetY = offset.top+this.settings.offsetDelta;
- } else {
- distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta;
- offsetX = distX+this.settings.offsetDelta;
- }
-
- if (distX > 0 || distY > 0) {
- this.scrollBy(distX, distY, true);
- }
- }
-
- afterResized(view){
- this.emit(EVENTS.MANAGERS.RESIZE, view.section);
- }
-
- // Remove Previous Listeners if present
- removeShownListeners(view){
-
- // view.off("shown", this.afterDisplayed);
- // view.off("shown", this.afterDisplayedAbove);
- view.onDisplayed = function(){};
-
- }
-
- add(section){
- var view = this.createView(section);
-
- this.views.append(view);
-
- view.on(EVENTS.VIEWS.RESIZED, (bounds) => {
- view.expanded = true;
- });
-
- view.on(EVENTS.VIEWS.AXIS, (axis) => {
- this.updateAxis(axis);
- });
-
- view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
- this.updateWritingMode(mode);
- });
-
- // view.on(EVENTS.VIEWS.SHOWN, this.afterDisplayed.bind(this));
- view.onDisplayed = this.afterDisplayed.bind(this);
- view.onResize = this.afterResized.bind(this);
-
- return view.display(this.request);
- }
-
- append(section){
- var view = this.createView(section);
-
- view.on(EVENTS.VIEWS.RESIZED, (bounds) => {
- view.expanded = true;
- });
-
- view.on(EVENTS.VIEWS.AXIS, (axis) => {
- this.updateAxis(axis);
- });
-
- view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
- this.updateWritingMode(mode);
- });
-
- this.views.append(view);
-
- view.onDisplayed = this.afterDisplayed.bind(this);
-
- return view;
- }
-
- prepend(section){
- var view = this.createView(section);
-
- view.on(EVENTS.VIEWS.RESIZED, (bounds) => {
- this.counter(bounds);
- view.expanded = true;
- });
-
- view.on(EVENTS.VIEWS.AXIS, (axis) => {
- this.updateAxis(axis);
- });
-
- view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
- this.updateWritingMode(mode);
- });
-
- this.views.prepend(view);
-
- view.onDisplayed = this.afterDisplayed.bind(this);
-
- return view;
- }
-
- counter(bounds){
- if(this.settings.axis === "vertical") {
- this.scrollBy(0, bounds.heightDelta, true);
- } else {
- this.scrollBy(bounds.widthDelta, 0, true);
- }
- }
-
- update(_offset){
- var container = this.bounds();
- var views = this.views.all();
- var viewsLength = views.length;
- var visible = [];
- var offset = typeof _offset != "undefined" ? _offset : (this.settings.offset || 0);
- var isVisible;
- var view;
-
- var updating = new defer();
- var promises = [];
- for (var i = 0; i < viewsLength; i++) {
- view = views[i];
-
- isVisible = this.isVisible(view, offset, offset, container);
-
- if(isVisible === true) {
- // console.log("visible " + view.index, view.displayed);
-
- if (!view.displayed) {
- let displayed = view.display(this.request)
- .then(function (view) {
- view.show();
- }, (err) => {
- view.hide();
- });
- promises.push(displayed);
- } else {
- view.show();
- }
- visible.push(view);
- } else {
- this.q.enqueue(view.destroy.bind(view));
- // console.log("hidden " + view.index, view.displayed);
-
- clearTimeout(this.trimTimeout);
- this.trimTimeout = setTimeout(function(){
- this.q.enqueue(this.trim.bind(this));
- }.bind(this), 250);
- }
-
- }
-
- if(promises.length){
- return Promise.all(promises)
- .catch((err) => {
- updating.reject(err);
- });
- } else {
- updating.resolve();
- return updating.promise;
- }
-
- }
-
- check(_offsetLeft, _offsetTop){
- var checking = new defer();
- var newViews = [];
-
- var horizontal = (this.settings.axis === "horizontal");
- var delta = this.settings.offset || 0;
-
- if (_offsetLeft && horizontal) {
- delta = _offsetLeft;
- }
-
- if (_offsetTop && !horizontal) {
- delta = _offsetTop;
- }
-
- var bounds = this._bounds; // bounds saved this until resize
-
- let offset = horizontal ? this.scrollLeft : this.scrollTop;
- let visibleLength = horizontal ? Math.floor(bounds.width) : bounds.height;
- let contentLength = horizontal ? this.container.scrollWidth : this.container.scrollHeight;
- let writingMode = (this.writingMode && this.writingMode.indexOf("vertical") === 0) ? "vertical" : "horizontal";
- let rtlScrollType = this.settings.rtlScrollType;
- let rtl = this.settings.direction === "rtl";
-
- if (!this.settings.fullsize) {
- // Scroll offset starts at width of element
- if (rtl && rtlScrollType === "default" && writingMode === "horizontal") {
- offset = contentLength - visibleLength - offset;
- }
- // Scroll offset starts at 0 and goes negative
- if (rtl && rtlScrollType === "negative" && writingMode === "horizontal") {
- offset = offset * -1;
- }
- } else {
- // Scroll offset starts at 0 and goes negative
- if ((horizontal && rtl && rtlScrollType === "negative") ||
- (!horizontal && rtl && rtlScrollType === "default")) {
- offset = offset * -1;
- }
- }
-
- let prepend = () => {
- let first = this.views.first();
- let prev = first && first.section.prev();
-
- if(prev) {
- newViews.push(this.prepend(prev));
- }
- };
-
- let append = () => {
- let last = this.views.last();
- let next = last && last.section.next();
-
- if(next) {
- newViews.push(this.append(next));
- }
-
- };
-
- let end = offset + visibleLength + delta;
- let start = offset - delta;
-
- if (end >= contentLength) {
- append();
- }
-
- if (start < 0) {
- prepend();
- }
-
-
- let promises = newViews.map((view) => {
- return view.display(this.request);
- });
-
- if(newViews.length){
- return Promise.all(promises)
- .then(() => {
- return this.check();
- })
- .then(() => {
- // Check to see if anything new is on screen after rendering
- return this.update(delta);
- }, (err) => {
- return err;
- });
- } else {
- this.q.enqueue(function(){
- this.update();
- }.bind(this));
- checking.resolve(false);
- return checking.promise;
- }
-
-
- }
-
- trim(){
- var task = new defer();
- var displayed = this.views.displayed();
- var first = displayed[0];
- var last = displayed[displayed.length-1];
- var firstIndex = this.views.indexOf(first);
- var lastIndex = this.views.indexOf(last);
- var above = this.views.slice(0, firstIndex);
- var below = this.views.slice(lastIndex+1);
-
- // Erase all but last above
- for (var i = 0; i < above.length-1; i++) {
- this.erase(above[i], above);
- }
-
- // Erase all except first below
- for (var j = 1; j < below.length; j++) {
- this.erase(below[j]);
- }
-
- task.resolve();
- return task.promise;
- }
-
- erase(view, above){ //Trim
-
- var prevTop;
- var prevLeft;
-
- if(!this.settings.fullsize) {
- prevTop = this.container.scrollTop;
- prevLeft = this.container.scrollLeft;
- } else {
- prevTop = window.scrollY;
- prevLeft = window.scrollX;
- }
-
- var bounds = view.bounds();
-
- this.views.remove(view);
-
- if(above) {
- if (this.settings.axis === "vertical") {
- this.scrollTo(0, prevTop - bounds.height, true);
- } else {
- if(this.settings.direction === 'rtl') {
- if (!this.settings.fullsize) {
- this.scrollTo(prevLeft, 0, true);
- } else {
- this.scrollTo(prevLeft + Math.floor(bounds.width), 0, true);
- }
- } else {
- this.scrollTo(prevLeft - Math.floor(bounds.width), 0, true);
- }
- }
- }
-
- }
-
- addEventListeners(stage){
-
- window.addEventListener("unload", function(e){
- this.ignore = true;
- // this.scrollTo(0,0);
- this.destroy();
- }.bind(this));
-
- this.addScrollListeners();
-
- if (this.isPaginated && this.settings.snap) {
- this.snapper = new Snap(this, this.settings.snap && (typeof this.settings.snap === "object") && this.settings.snap);
- }
- }
-
- addScrollListeners() {
- var scroller;
-
- this.tick = requestAnimationFrame;
-
- let dir = this.settings.direction === "rtl" && this.settings.rtlScrollType === "default" ? -1 : 1;
-
- this.scrollDeltaVert = 0;
- this.scrollDeltaHorz = 0;
-
- if(!this.settings.fullsize) {
- scroller = this.container;
- this.scrollTop = this.container.scrollTop;
- this.scrollLeft = this.container.scrollLeft;
- } else {
- scroller = window;
- this.scrollTop = window.scrollY * dir;
- this.scrollLeft = window.scrollX * dir;
- }
-
- this._onScroll = this.onScroll.bind(this);
- scroller.addEventListener("scroll", this._onScroll);
- this._scrolled = debounce(this.scrolled.bind(this), 30);
- // this.tick.call(window, this.onScroll.bind(this));
-
- this.didScroll = false;
-
- }
-
- removeEventListeners(){
- var scroller;
-
- if(!this.settings.fullsize) {
- scroller = this.container;
- } else {
- scroller = window;
- }
-
- scroller.removeEventListener("scroll", this._onScroll);
- this._onScroll = undefined;
- }
-
- onScroll(){
- let scrollTop;
- let scrollLeft;
- let dir = this.settings.direction === "rtl" && this.settings.rtlScrollType === "default" ? -1 : 1;
-
- if(!this.settings.fullsize) {
- scrollTop = this.container.scrollTop;
- scrollLeft = this.container.scrollLeft;
- } else {
- scrollTop = window.scrollY * dir;
- scrollLeft = window.scrollX * dir;
- }
-
- this.scrollTop = scrollTop;
- this.scrollLeft = scrollLeft;
-
- if(!this.ignore) {
-
- this._scrolled();
-
- } else {
- this.ignore = false;
- }
-
- this.scrollDeltaVert += Math.abs(scrollTop-this.prevScrollTop);
- this.scrollDeltaHorz += Math.abs(scrollLeft-this.prevScrollLeft);
-
- this.prevScrollTop = scrollTop;
- this.prevScrollLeft = scrollLeft;
-
- clearTimeout(this.scrollTimeout);
- this.scrollTimeout = setTimeout(function(){
- this.scrollDeltaVert = 0;
- this.scrollDeltaHorz = 0;
- }.bind(this), 150);
-
- clearTimeout(this.afterScrolled);
-
- this.didScroll = false;
-
- }
-
- scrolled() {
-
- this.q.enqueue(function() {
- return this.check();
- }.bind(this));
-
- this.emit(EVENTS.MANAGERS.SCROLL, {
- top: this.scrollTop,
- left: this.scrollLeft
- });
-
- clearTimeout(this.afterScrolled);
- this.afterScrolled = setTimeout(function () {
-
- // Don't report scroll if we are about the snap
- if (this.snapper && this.snapper.supportsTouch && this.snapper.needsSnap()) {
- return;
- }
-
- this.emit(EVENTS.MANAGERS.SCROLLED, {
- top: this.scrollTop,
- left: this.scrollLeft
- });
-
- }.bind(this), this.settings.afterScrolledTimeout);
- }
-
- next(){
-
- let delta = this.layout.props.name === "pre-paginated" &&
- this.layout.props.spread ? this.layout.props.delta * 2 : this.layout.props.delta;
-
- if(!this.views.length) return;
-
- if(this.isPaginated && this.settings.axis === "horizontal") {
-
- this.scrollBy(delta, 0, true);
-
- } else {
-
- this.scrollBy(0, this.layout.height, true);
-
- }
-
- this.q.enqueue(function() {
- return this.check();
- }.bind(this));
- }
-
- prev(){
-
- let delta = this.layout.props.name === "pre-paginated" &&
- this.layout.props.spread ? this.layout.props.delta * 2 : this.layout.props.delta;
-
- if(!this.views.length) return;
-
- if(this.isPaginated && this.settings.axis === "horizontal") {
-
- this.scrollBy(-delta, 0, true);
-
- } else {
-
- this.scrollBy(0, -this.layout.height, true);
-
- }
-
- this.q.enqueue(function() {
- return this.check();
- }.bind(this));
- }
-
- updateFlow(flow){
- if (this.rendered && this.snapper) {
- this.snapper.destroy();
- this.snapper = undefined;
- }
-
- super.updateFlow(flow, "scroll");
-
- if (this.rendered && this.isPaginated && this.settings.snap) {
- this.snapper = new Snap(this, this.settings.snap && (typeof this.settings.snap === "object") && this.settings.snap);
- }
- }
-
- destroy(){
- super.destroy();
-
- if (this.snapper) {
- this.snapper.destroy();
- }
- }
-
+ constructor(options) {
+ super(options);
+
+ this.name = "continuous";
+
+ this.settings = extend(this.settings || {}, {
+ infinite: true,
+ overflow: undefined,
+ axis: undefined,
+ writingMode: undefined,
+ flow: "scrolled",
+ offset: 500,
+ offsetDelta: 250,
+ width: undefined,
+ height: undefined,
+ snap: false,
+ afterScrolledTimeout: 10,
+ allowScriptedContent: false,
+ allowPopups: false,
+ selectionStopDelay: 250,
+ });
+
+ extend(this.settings, options.settings || {});
+
+ // Gap can be 0, but defaults doesn't handle that
+ if (options.settings.gap != "undefined" && options.settings.gap === 0) {
+ this.settings.gap = options.settings.gap;
+ }
+
+ this.viewSettings = {
+ ignoreClass: this.settings.ignoreClass,
+ axis: this.settings.axis,
+ flow: this.settings.flow,
+ layout: this.layout,
+ width: 0,
+ height: 0,
+ forceEvenPages: false,
+ allowScriptedContent: this.settings.allowScriptedContent,
+ allowPopups: this.settings.allowPopups,
+ selectionStopDelay: this.settings.selectionStopDelay,
+ };
+
+ this.scrollTop = 0;
+ this.scrollLeft = 0;
+ }
+
+ display(section, target) {
+ return DefaultViewManager.prototype.display
+ .call(this, section, target)
+ .then(
+ function () {
+ return this.fill();
+ }.bind(this)
+ );
+ }
+
+ fill(_full) {
+ var full = _full || new defer();
+
+ this.q
+ .enqueue(() => {
+ return this.check();
+ })
+ .then((result) => {
+ if (result) {
+ this.fill(full);
+ } else {
+ full.resolve();
+ }
+ });
+
+ return full.promise;
+ }
+
+ moveTo(offset) {
+ // var bounds = this.stage.bounds();
+ // var dist = Math.floor(offset.top / bounds.height) * bounds.height;
+ var distX = 0,
+ distY = 0;
+
+ var offsetX = 0,
+ offsetY = 0;
+
+ if (!this.isPaginated) {
+ distY = offset.top;
+ offsetY = offset.top + this.settings.offsetDelta;
+ } else {
+ distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta;
+ offsetX = distX + this.settings.offsetDelta;
+ }
+
+ if (distX > 0 || distY > 0) {
+ this.scrollBy(distX, distY, true);
+ }
+ }
+
+ afterResized(view) {
+ this.emit(EVENTS.MANAGERS.RESIZE, view.section);
+ }
+
+ // Remove Previous Listeners if present
+ removeShownListeners(view) {
+ // view.off("shown", this.afterDisplayed);
+ // view.off("shown", this.afterDisplayedAbove);
+ view.onDisplayed = function () {};
+ }
+
+ add(section) {
+ var view = this.createView(section);
+
+ this.views.append(view);
+
+ view.on(EVENTS.VIEWS.RESIZED, (bounds) => {
+ view.expanded = true;
+ });
+
+ view.on(EVENTS.VIEWS.AXIS, (axis) => {
+ this.updateAxis(axis);
+ });
+
+ view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
+ this.updateWritingMode(mode);
+ });
+
+ // view.on(EVENTS.VIEWS.SHOWN, this.afterDisplayed.bind(this));
+ view.onDisplayed = this.afterDisplayed.bind(this);
+ view.onResize = this.afterResized.bind(this);
+
+ return view.display(this.request);
+ }
+
+ append(section) {
+ var view = this.createView(section);
+
+ view.on(EVENTS.VIEWS.RESIZED, (bounds) => {
+ view.expanded = true;
+ });
+
+ view.on(EVENTS.VIEWS.AXIS, (axis) => {
+ this.updateAxis(axis);
+ });
+
+ view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
+ this.updateWritingMode(mode);
+ });
+
+ this.views.append(view);
+
+ view.onDisplayed = this.afterDisplayed.bind(this);
+
+ return view;
+ }
+
+ prepend(section) {
+ var view = this.createView(section);
+
+ view.on(EVENTS.VIEWS.RESIZED, (bounds) => {
+ this.counter(bounds);
+ view.expanded = true;
+ });
+
+ view.on(EVENTS.VIEWS.AXIS, (axis) => {
+ this.updateAxis(axis);
+ });
+
+ view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
+ this.updateWritingMode(mode);
+ });
+
+ this.views.prepend(view);
+
+ view.onDisplayed = this.afterDisplayed.bind(this);
+
+ return view;
+ }
+
+ counter(bounds) {
+ if (this.settings.axis === "vertical") {
+ this.scrollBy(0, bounds.heightDelta, true);
+ } else {
+ this.scrollBy(bounds.widthDelta, 0, true);
+ }
+ }
+
+ update(_offset) {
+ var container = this.bounds();
+ var views = this.views.all();
+ var viewsLength = views.length;
+ var visible = [];
+ var offset =
+ typeof _offset != "undefined" ? _offset : this.settings.offset || 0;
+ var isVisible;
+ var view;
+
+ var updating = new defer();
+ var promises = [];
+ for (var i = 0; i < viewsLength; i++) {
+ view = views[i];
+
+ isVisible = this.isVisible(view, offset, offset, container);
+
+ if (isVisible === true) {
+ // console.log("visible " + view.index, view.displayed);
+
+ if (!view.displayed) {
+ let displayed = view.display(this.request).then(
+ function (view) {
+ view.show();
+ },
+ (err) => {
+ view.hide();
+ }
+ );
+ promises.push(displayed);
+ } else {
+ view.show();
+ }
+ visible.push(view);
+ } else {
+ this.q.enqueue(view.destroy.bind(view));
+ // console.log("hidden " + view.index, view.displayed);
+
+ clearTimeout(this.trimTimeout);
+ this.trimTimeout = setTimeout(
+ function () {
+ this.q.enqueue(this.trim.bind(this));
+ }.bind(this),
+ 250
+ );
+ }
+ }
+
+ if (promises.length) {
+ return Promise.all(promises).catch((err) => {
+ updating.reject(err);
+ });
+ } else {
+ updating.resolve();
+ return updating.promise;
+ }
+ }
+
+ check(_offsetLeft, _offsetTop) {
+ var checking = new defer();
+ var newViews = [];
+
+ var horizontal = this.settings.axis === "horizontal";
+ var delta = this.settings.offset || 0;
+
+ if (_offsetLeft && horizontal) {
+ delta = _offsetLeft;
+ }
+
+ if (_offsetTop && !horizontal) {
+ delta = _offsetTop;
+ }
+
+ var bounds = this._bounds; // bounds saved this until resize
+
+ let offset = horizontal ? this.scrollLeft : this.scrollTop;
+ let visibleLength = horizontal ? Math.floor(bounds.width) : bounds.height;
+ let contentLength = horizontal
+ ? this.container.scrollWidth
+ : this.container.scrollHeight;
+ let writingMode =
+ this.writingMode && this.writingMode.indexOf("vertical") === 0
+ ? "vertical"
+ : "horizontal";
+ let rtlScrollType = this.settings.rtlScrollType;
+ let rtl = this.settings.direction === "rtl";
+
+ if (!this.settings.fullsize) {
+ // Scroll offset starts at width of element
+ if (rtl && rtlScrollType === "default" && writingMode === "horizontal") {
+ offset = contentLength - visibleLength - offset;
+ }
+ // Scroll offset starts at 0 and goes negative
+ if (rtl && rtlScrollType === "negative" && writingMode === "horizontal") {
+ offset = offset * -1;
+ }
+ } else {
+ // Scroll offset starts at 0 and goes negative
+ if (
+ (horizontal && rtl && rtlScrollType === "negative") ||
+ (!horizontal && rtl && rtlScrollType === "default")
+ ) {
+ offset = offset * -1;
+ }
+ }
+
+ let prepend = () => {
+ let first = this.views.first();
+ let prev = first && first.section.prev();
+
+ if (prev) {
+ newViews.push(this.prepend(prev));
+ }
+ };
+
+ let append = () => {
+ let last = this.views.last();
+ let next = last && last.section.next();
+
+ if (next) {
+ newViews.push(this.append(next));
+ }
+ };
+
+ let end = offset + visibleLength + delta;
+ let start = offset - delta;
+
+ if (end >= contentLength) {
+ append();
+ }
+
+ if (start < 0) {
+ prepend();
+ }
+
+ let promises = newViews.map((view) => {
+ return view.display(this.request);
+ });
+
+ if (newViews.length) {
+ return Promise.all(promises)
+ .then(() => {
+ return this.check();
+ })
+ .then(
+ () => {
+ // Check to see if anything new is on screen after rendering
+ return this.update(delta);
+ },
+ (err) => {
+ return err;
+ }
+ );
+ } else {
+ this.q.enqueue(
+ function () {
+ this.update();
+ }.bind(this)
+ );
+ checking.resolve(false);
+ return checking.promise;
+ }
+ }
+
+ trim() {
+ var task = new defer();
+ var displayed = this.views.displayed();
+ var first = displayed[0];
+ var last = displayed[displayed.length - 1];
+ var firstIndex = this.views.indexOf(first);
+ var lastIndex = this.views.indexOf(last);
+ var above = this.views.slice(0, firstIndex);
+ var below = this.views.slice(lastIndex + 1);
+
+ // Erase all but last above
+ for (var i = 0; i < above.length - 1; i++) {
+ this.erase(above[i], above);
+ }
+
+ // Erase all except first below
+ for (var j = 1; j < below.length; j++) {
+ this.erase(below[j]);
+ }
+
+ task.resolve();
+ return task.promise;
+ }
+
+ erase(view, above) {
+ //Trim
+
+ var prevTop;
+ var prevLeft;
+
+ if (!this.settings.fullsize) {
+ prevTop = this.container.scrollTop;
+ prevLeft = this.container.scrollLeft;
+ } else {
+ prevTop = window.scrollY;
+ prevLeft = window.scrollX;
+ }
+
+ var bounds = view.bounds();
+
+ this.views.remove(view);
+
+ if (above) {
+ if (this.settings.axis === "vertical") {
+ this.scrollTo(0, prevTop - bounds.height, true);
+ } else {
+ if (this.settings.direction === "rtl") {
+ if (!this.settings.fullsize) {
+ this.scrollTo(prevLeft, 0, true);
+ } else {
+ this.scrollTo(prevLeft + Math.floor(bounds.width), 0, true);
+ }
+ } else {
+ this.scrollTo(prevLeft - Math.floor(bounds.width), 0, true);
+ }
+ }
+ }
+ }
+
+ addEventListeners(stage) {
+ window.addEventListener(
+ "unload",
+ function (e) {
+ this.ignore = true;
+ // this.scrollTo(0,0);
+ this.destroy();
+ }.bind(this)
+ );
+
+ this.addScrollListeners();
+
+ if (this.isPaginated && this.settings.snap) {
+ this.snapper = new Snap(
+ this,
+ this.settings.snap &&
+ typeof this.settings.snap === "object" &&
+ this.settings.snap
+ );
+ }
+ }
+
+ addScrollListeners() {
+ var scroller;
+
+ this.tick = requestAnimationFrame;
+
+ let dir =
+ this.settings.direction === "rtl" &&
+ this.settings.rtlScrollType === "default"
+ ? -1
+ : 1;
+
+ this.scrollDeltaVert = 0;
+ this.scrollDeltaHorz = 0;
+
+ if (!this.settings.fullsize) {
+ scroller = this.container;
+ this.scrollTop = this.container.scrollTop;
+ this.scrollLeft = this.container.scrollLeft;
+ } else {
+ scroller = window;
+ this.scrollTop = window.scrollY * dir;
+ this.scrollLeft = window.scrollX * dir;
+ }
+
+ this._onScroll = this.onScroll.bind(this);
+ scroller.addEventListener("scroll", this._onScroll);
+ this._scrolled = debounce(this.scrolled.bind(this), 30);
+ // this.tick.call(window, this.onScroll.bind(this));
+
+ this.didScroll = false;
+ }
+
+ removeEventListeners() {
+ var scroller;
+
+ if (!this.settings.fullsize) {
+ scroller = this.container;
+ } else {
+ scroller = window;
+ }
+
+ scroller.removeEventListener("scroll", this._onScroll);
+ this._onScroll = undefined;
+ }
+
+ onScroll() {
+ let scrollTop;
+ let scrollLeft;
+ let dir =
+ this.settings.direction === "rtl" &&
+ this.settings.rtlScrollType === "default"
+ ? -1
+ : 1;
+
+ if (!this.settings.fullsize) {
+ scrollTop = this.container.scrollTop;
+ scrollLeft = this.container.scrollLeft;
+ } else {
+ scrollTop = window.scrollY * dir;
+ scrollLeft = window.scrollX * dir;
+ }
+
+ this.scrollTop = scrollTop;
+ this.scrollLeft = scrollLeft;
+
+ if (!this.ignore) {
+ this._scrolled();
+ } else {
+ this.ignore = false;
+ }
+
+ this.scrollDeltaVert += Math.abs(scrollTop - this.prevScrollTop);
+ this.scrollDeltaHorz += Math.abs(scrollLeft - this.prevScrollLeft);
+
+ this.prevScrollTop = scrollTop;
+ this.prevScrollLeft = scrollLeft;
+
+ clearTimeout(this.scrollTimeout);
+ this.scrollTimeout = setTimeout(
+ function () {
+ this.scrollDeltaVert = 0;
+ this.scrollDeltaHorz = 0;
+ }.bind(this),
+ 150
+ );
+
+ clearTimeout(this.afterScrolled);
+
+ this.didScroll = false;
+ }
+
+ scrolled() {
+ this.q.enqueue(
+ function () {
+ return this.check();
+ }.bind(this)
+ );
+
+ this.emit(EVENTS.MANAGERS.SCROLL, {
+ top: this.scrollTop,
+ left: this.scrollLeft,
+ });
+
+ clearTimeout(this.afterScrolled);
+ this.afterScrolled = setTimeout(
+ function () {
+ // Don't report scroll if we are about the snap
+ if (
+ this.snapper &&
+ this.snapper.supportsTouch &&
+ this.snapper.needsSnap()
+ ) {
+ return;
+ }
+
+ this.emit(EVENTS.MANAGERS.SCROLLED, {
+ top: this.scrollTop,
+ left: this.scrollLeft,
+ });
+ }.bind(this),
+ this.settings.afterScrolledTimeout
+ );
+ }
+
+ next() {
+ let delta =
+ this.layout.props.name === "pre-paginated" && this.layout.props.spread
+ ? this.layout.props.delta * 2
+ : this.layout.props.delta;
+
+ if (!this.views.length) return;
+
+ if (this.isPaginated && this.settings.axis === "horizontal") {
+ this.scrollBy(delta, 0, true);
+ } else {
+ this.scrollBy(0, this.layout.height, true);
+ }
+
+ this.q.enqueue(
+ function () {
+ return this.check();
+ }.bind(this)
+ );
+ }
+
+ prev() {
+ let delta =
+ this.layout.props.name === "pre-paginated" && this.layout.props.spread
+ ? this.layout.props.delta * 2
+ : this.layout.props.delta;
+
+ if (!this.views.length) return;
+
+ if (this.isPaginated && this.settings.axis === "horizontal") {
+ this.scrollBy(-delta, 0, true);
+ } else {
+ this.scrollBy(0, -this.layout.height, true);
+ }
+
+ this.q.enqueue(
+ function () {
+ return this.check();
+ }.bind(this)
+ );
+ }
+
+ updateFlow(flow) {
+ if (this.rendered && this.snapper) {
+ this.snapper.destroy();
+ this.snapper = undefined;
+ }
+
+ super.updateFlow(flow, "scroll");
+
+ if (this.rendered && this.isPaginated && this.settings.snap) {
+ this.snapper = new Snap(
+ this,
+ this.settings.snap &&
+ typeof this.settings.snap === "object" &&
+ this.settings.snap
+ );
+ }
+ }
+
+ destroy() {
+ super.destroy();
+
+ if (this.snapper) {
+ this.snapper.destroy();
+ }
+ }
}
export default ContinuousViewManager;
diff --git a/src/managers/default/index.js b/src/managers/default/index.js
index 2812d878a..ba8d35823 100644
--- a/src/managers/default/index.js
+++ b/src/managers/default/index.js
@@ -1,5 +1,5 @@
import EventEmitter from "event-emitter";
-import {extend, defer, windowBounds, isNumber} from "../../utils/core";
+import { extend, defer, windowBounds, isNumber } from "../../utils/core";
import scrollType from "../../utils/scrolltype";
import Mapping from "../../mapping";
import Queue from "../../utils/queue";
@@ -8,156 +8,160 @@ import Views from "../helpers/views";
import { EVENTS } from "../../utils/constants";
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(this);
-
- this.settings = extend(this.settings || {}, {
- infinite: true,
- hidden: false,
- width: undefined,
- height: undefined,
- axis: undefined,
- writingMode: undefined,
- flow: "scrolled",
- ignoreClass: "",
- fullsize: undefined,
- allowScriptedContent: false,
- allowPopups: false
- });
-
- 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;
- }
+ 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(this);
+
+ this.settings = extend(this.settings || {}, {
+ infinite: true,
+ hidden: false,
+ width: undefined,
+ height: undefined,
+ axis: undefined,
+ writingMode: undefined,
+ flow: "scrolled",
+ ignoreClass: "",
+ fullsize: undefined,
+ allowScriptedContent: true,
+ allowPopups: false,
+ selectionStopDelay: 250,
+ });
+
+ 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,
+ selectionStopDelay: this.settings.selectionStopDelay,
+ };
+
+ 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 = scrollType();
+
+ // Save the stage
+ this.stage = new Stage({
+ 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(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;
+ }
- if (this.settings.fullsize) {
- this.settings.overflow = "visible";
- this.overflow = this.settings.overflow;
- }
+ this._onScroll = this.onScroll.bind(this);
+ scroller.addEventListener("scroll", this._onScroll);
+ }
+
+ removeEventListeners() {
+ var scroller;
- this.settings.size = size;
+ if (!this.settings.fullsize) {
+ scroller = this.container;
+ } else {
+ scroller = window;
+ }
- this.settings.rtlScrollType = scrollType();
+ scroller.removeEventListener("scroll", this._onScroll);
+ this._onScroll = undefined;
+ }
- // Save the stage
- this.stage = new Stage({
- 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
- });
+ destroy() {
+ clearTimeout(this.orientationTimeout);
+ clearTimeout(this.resizeTimeout);
+ clearTimeout(this.afterScrolled);
- this.stage.attachTo(element);
+ this.clear();
- // Get this stage container div
- this.container = this.stage.getContainer();
+ this.removeEventListeners();
- // Views array methods
- this.views = new Views(this.container);
+ this.stage.destroy();
- // Calculate Stage Size
- this._bounds = this.bounds();
- this._stageSize = this.stage.size();
+ this.rendered = false;
- // 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) {
@@ -166,909 +170,971 @@ class DefaultViewManager {
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(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 = 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(EVENTS.MANAGERS.RESIZED, {
- width: this._stageSize.width,
- height: this._stageSize.height
- }, epubcfi);
- }
-
- createView(section, forceRight) {
- return new this.View(section, 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 defer();
- var displayed = displaying.promise;
-
- // Check if moving to target is needed
- if (target === section.href || 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(EVENTS.MANAGERS.ADDED, view);
- }
-
- afterResized(view){
- this.emit(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'){
- /***
+ }
+
+ 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(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 = 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(
+ EVENTS.MANAGERS.RESIZED,
+ {
+ width: this._stageSize.width,
+ height: this._stageSize.height,
+ },
+ epubcfi
+ );
+ }
+
+ createView(section, forceRight) {
+ return new this.View(section, 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 defer();
+ var displayed = displaying.promise;
+
+ // Check if moving to target is needed
+ if (target === section.href || 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(EVENTS.MANAGERS.ADDED, view);
+ }
+
+ afterResized(view) {
+ this.emit(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(EVENTS.VIEWS.AXIS, (axis) => {
- this.updateAxis(axis);
- });
-
- view.on(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(EVENTS.VIEWS.AXIS, (axis) => {
- this.updateAxis(axis);
- });
-
- view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
- this.updateWritingMode(mode);
- });
-
- return view.display(this.request);
- }
-
- prepend(section, forceRight){
- var view = this.createView(section, forceRight);
-
- view.on(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(EVENTS.VIEWS.AXIS, (axis) => {
- this.updateAxis(axis);
- });
-
- view.on(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(EVENTS.MANAGERS.SCROLL, {
- top: scrollTop,
- left: scrollLeft
- });
-
- clearTimeout(this.afterScrolled);
- this.afterScrolled = setTimeout(function () {
- this.emit(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(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(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();
- }
+ distX = distX + this.layout.delta;
+ distX = distX - width;
+ }
+ this.scrollTo(distX, distY, true);
+ }
- isRendered() {
- return this.rendered;
- }
+ 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(EVENTS.VIEWS.AXIS, (axis) => {
+ this.updateAxis(axis);
+ });
+
+ view.on(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(EVENTS.VIEWS.AXIS, (axis) => {
+ this.updateAxis(axis);
+ });
+
+ view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
+ this.updateWritingMode(mode);
+ });
+
+ return view.display(this.request);
+ }
+
+ prepend(section, forceRight) {
+ var view = this.createView(section, forceRight);
+
+ view.on(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(EVENTS.VIEWS.AXIS, (axis) => {
+ this.updateAxis(axis);
+ });
+
+ view.on(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(EVENTS.MANAGERS.SCROLL, {
+ top: scrollTop,
+ left: scrollLeft,
+ });
+
+ clearTimeout(this.afterScrolled);
+ this.afterScrolled = setTimeout(
+ function () {
+ this.emit(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(
+ 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(
+ 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
diff --git a/src/managers/views/iframe.js b/src/managers/views/iframe.js
index e20e54d13..d235ec3c1 100644
--- a/src/managers/views/iframe.js
+++ b/src/managers/views/iframe.js
@@ -1,849 +1,886 @@
import EventEmitter from "event-emitter";
-import {extend, borders, uuid, isNumber, bounds, defer, createBlobUrl, revokeBlobUrl} from "../../utils/core";
+import {
+ extend,
+ borders,
+ uuid,
+ isNumber,
+ bounds,
+ defer,
+ createBlobUrl,
+ revokeBlobUrl,
+} from "../../utils/core";
import EpubCFI from "../../epubcfi";
import Contents from "../../contents";
import { EVENTS } from "../../utils/constants";
import { Pane, Highlight, Underline } from "marks-pane";
class IframeView {
- constructor(section, options) {
- this.settings = extend({
- ignoreClass : "",
- axis: undefined, //options.layout && options.layout.props.flow === "scrolled" ? "vertical" : "horizontal",
- direction: undefined,
- width: 0,
- height: 0,
- layout: undefined,
- globalLayoutProperties: {},
- method: undefined,
- forceRight: false,
- allowScriptedContent: false,
- allowPopups: false
- }, options || {});
-
- this.id = "epubjs-view-" + uuid();
- this.section = section;
- this.index = section.index;
-
- this.element = this.container(this.settings.axis);
-
- this.added = false;
- this.displayed = false;
- this.rendered = false;
-
- // this.width = this.settings.width;
- // this.height = this.settings.height;
-
- this.fixedWidth = 0;
- this.fixedHeight = 0;
-
- // Blank Cfi for Parsing
- this.epubcfi = new EpubCFI();
-
- this.layout = this.settings.layout;
- // Dom events to listen for
- // this.listenedEvents = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart"];
-
- this.pane = undefined;
- this.highlights = {};
- this.underlines = {};
- this.marks = {};
-
- }
-
- container(axis) {
- var element = document.createElement("div");
-
- element.classList.add("epub-view");
-
- // this.element.style.minHeight = "100px";
- element.style.height = "0px";
- element.style.width = "0px";
- element.style.overflow = "hidden";
- element.style.position = "relative";
- element.style.display = "block";
-
- if(axis && axis == "horizontal"){
- element.style.flex = "none";
- } else {
- element.style.flex = "initial";
- }
-
- return element;
- }
-
- create() {
-
- if(this.iframe) {
- return this.iframe;
- }
-
- if(!this.element) {
- this.element = this.createContainer();
- }
-
- this.iframe = document.createElement("iframe");
- this.iframe.id = this.id;
- this.iframe.scrolling = "no"; // Might need to be removed: breaks ios width calculations
- this.iframe.style.overflow = "hidden";
- this.iframe.seamless = "seamless";
- // Back up if seamless isn't supported
- this.iframe.style.border = "none";
-
- // sandbox
- this.iframe.sandbox = "allow-same-origin";
- if (this.settings.allowScriptedContent) {
- this.iframe.sandbox += " allow-scripts";
- }
- if (this.settings.allowPopups) {
- this.iframe.sandbox += " allow-popups";
- }
-
- this.iframe.setAttribute("enable-annotation", "true");
-
- this.resizing = true;
-
- // this.iframe.style.display = "none";
- this.element.style.visibility = "hidden";
- this.iframe.style.visibility = "hidden";
-
- this.iframe.style.width = "0";
- this.iframe.style.height = "0";
- this._width = 0;
- this._height = 0;
-
- this.element.setAttribute("ref", this.index);
-
- this.added = true;
-
- this.elementBounds = bounds(this.element);
-
- // if(width || height){
- // this.resize(width, height);
- // } else if(this.width && this.height){
- // this.resize(this.width, this.height);
- // } else {
- // this.iframeBounds = bounds(this.iframe);
- // }
-
-
- if(("srcdoc" in this.iframe)) {
- this.supportsSrcdoc = true;
- } else {
- this.supportsSrcdoc = false;
- }
-
- if (!this.settings.method) {
- this.settings.method = this.supportsSrcdoc ? "srcdoc" : "write";
- }
-
- return this.iframe;
- }
-
- render(request, show) {
-
- // view.onLayout = this.layout.format.bind(this.layout);
- this.create();
-
- // Fit to size of the container, apply padding
- this.size();
-
- if(!this.sectionRender) {
- this.sectionRender = this.section.render(request);
- }
-
- // Render Chain
- return this.sectionRender
- .then(function(contents){
- return this.load(contents);
- }.bind(this))
- .then(function(){
-
- // find and report the writingMode axis
- let writingMode = this.contents.writingMode();
-
- // Set the axis based on the flow and writing mode
- let axis;
- if (this.settings.flow === "scrolled") {
- axis = (writingMode.indexOf("vertical") === 0) ? "horizontal" : "vertical";
- } else {
- axis = (writingMode.indexOf("vertical") === 0) ? "vertical" : "horizontal";
- }
-
- if (writingMode.indexOf("vertical") === 0 && this.settings.flow === "paginated") {
- this.layout.delta = this.layout.height;
- }
-
- this.setAxis(axis);
- this.emit(EVENTS.VIEWS.AXIS, axis);
-
- this.setWritingMode(writingMode);
- this.emit(EVENTS.VIEWS.WRITING_MODE, writingMode);
-
-
- // apply the layout function to the contents
- this.layout.format(this.contents, this.section, this.axis);
-
- // Listen for events that require an expansion of the iframe
- this.addListeners();
-
- return new Promise((resolve, reject) => {
- // Expand the iframe to the full size of the content
- this.expand();
-
- if (this.settings.forceRight) {
- this.element.style.marginLeft = this.width() + "px";
- }
- resolve();
- });
-
- }.bind(this), function(e){
- this.emit(EVENTS.VIEWS.LOAD_ERROR, e);
- return new Promise((resolve, reject) => {
- reject(e);
- });
- }.bind(this))
- .then(function() {
- this.emit(EVENTS.VIEWS.RENDERED, this.section);
- }.bind(this));
-
- }
-
- reset () {
- if (this.iframe) {
- this.iframe.style.width = "0";
- this.iframe.style.height = "0";
- this._width = 0;
- this._height = 0;
- this._textWidth = undefined;
- this._contentWidth = undefined;
- this._textHeight = undefined;
- this._contentHeight = undefined;
- }
- this._needsReframe = true;
- }
-
- // Determine locks base on settings
- size(_width, _height) {
- var width = _width || this.settings.width;
- var height = _height || this.settings.height;
-
- if(this.layout.name === "pre-paginated") {
- this.lock("both", width, height);
- } else if(this.settings.axis === "horizontal") {
- this.lock("height", width, height);
- } else {
- this.lock("width", width, height);
- }
-
- this.settings.width = width;
- this.settings.height = height;
- }
-
- // Lock an axis to element dimensions, taking borders into account
- lock(what, width, height) {
- var elBorders = borders(this.element);
- var iframeBorders;
-
- if(this.iframe) {
- iframeBorders = borders(this.iframe);
- } else {
- iframeBorders = {width: 0, height: 0};
- }
-
- if(what == "width" && isNumber(width)){
- this.lockedWidth = width - elBorders.width - iframeBorders.width;
- // this.resize(this.lockedWidth, width); // width keeps ratio correct
- }
-
- if(what == "height" && isNumber(height)){
- this.lockedHeight = height - elBorders.height - iframeBorders.height;
- // this.resize(width, this.lockedHeight);
- }
-
- if(what === "both" &&
- isNumber(width) &&
- isNumber(height)){
-
- this.lockedWidth = width - elBorders.width - iframeBorders.width;
- this.lockedHeight = height - elBorders.height - iframeBorders.height;
- // this.resize(this.lockedWidth, this.lockedHeight);
- }
-
- if(this.displayed && this.iframe) {
-
- // this.contents.layout();
- this.expand();
- }
-
-
-
- }
-
- // Resize a single axis based on content dimensions
- expand(force) {
- var width = this.lockedWidth;
- var height = this.lockedHeight;
- var columns;
-
- var textWidth, textHeight;
-
- if(!this.iframe || this._expanding) return;
-
- this._expanding = true;
-
- if(this.layout.name === "pre-paginated") {
- width = this.layout.columnWidth;
- height = this.layout.height;
- }
- // Expand Horizontally
- else if(this.settings.axis === "horizontal") {
- // Get the width of the text
- width = this.contents.textWidth();
-
- if (width % this.layout.pageWidth > 0) {
- width = Math.ceil(width / this.layout.pageWidth) * this.layout.pageWidth;
- }
-
- if (this.settings.forceEvenPages) {
- columns = (width / this.layout.pageWidth);
- if ( this.layout.divisor > 1 &&
- this.layout.name === "reflowable" &&
- (columns % 2 > 0)) {
- // add a blank page
- width += this.layout.pageWidth;
- }
- }
-
- } // Expand Vertically
- else if(this.settings.axis === "vertical") {
- height = this.contents.textHeight();
- if (this.settings.flow === "paginated" &&
- height % this.layout.height > 0) {
- height = Math.ceil(height / this.layout.height) * this.layout.height;
- }
- }
-
- // Only Resize if dimensions have changed or
- // if Frame is still hidden, so needs reframing
- if(this._needsReframe || width != this._width || height != this._height){
- this.reframe(width, height);
- }
-
- this._expanding = false;
- }
-
- reframe(width, height) {
- var size;
-
- if(isNumber(width)){
- this.element.style.width = width + "px";
- this.iframe.style.width = width + "px";
- this._width = width;
- }
-
- if(isNumber(height)){
- this.element.style.height = height + "px";
- this.iframe.style.height = height + "px";
- this._height = height;
- }
-
- let widthDelta = this.prevBounds ? width - this.prevBounds.width : width;
- let heightDelta = this.prevBounds ? height - this.prevBounds.height : height;
-
- size = {
- width: width,
- height: height,
- widthDelta: widthDelta,
- heightDelta: heightDelta,
- };
-
- this.pane && this.pane.render();
-
- requestAnimationFrame(() => {
- let mark;
- for (let m in this.marks) {
- if (this.marks.hasOwnProperty(m)) {
- mark = this.marks[m];
- this.placeMark(mark.element, mark.range);
- }
- }
- });
-
- this.onResize(this, size);
-
- this.emit(EVENTS.VIEWS.RESIZED, size);
-
- this.prevBounds = size;
-
- this.elementBounds = bounds(this.element);
-
- }
-
-
- load(contents) {
- var loading = new defer();
- var loaded = loading.promise;
-
- if(!this.iframe) {
- loading.reject(new Error("No Iframe Available"));
- return loaded;
- }
-
- this.iframe.onload = function(event) {
-
- this.onLoad(event, loading);
-
- }.bind(this);
-
- if (this.settings.method === "blobUrl") {
- this.blobUrl = createBlobUrl(contents, "application/xhtml+xml");
- this.iframe.src = this.blobUrl;
- this.element.appendChild(this.iframe);
- } else if(this.settings.method === "srcdoc"){
- this.iframe.srcdoc = contents;
- this.element.appendChild(this.iframe);
- } else {
-
- this.element.appendChild(this.iframe);
-
- this.document = this.iframe.contentDocument;
-
- if(!this.document) {
- loading.reject(new Error("No Document Available"));
- return loaded;
- }
-
- this.iframe.contentDocument.open();
- // For Cordova windows platform
- if(window.MSApp && MSApp.execUnsafeLocalFunction) {
- var outerThis = this;
- MSApp.execUnsafeLocalFunction(function () {
- outerThis.iframe.contentDocument.write(contents);
- });
- } else {
- this.iframe.contentDocument.write(contents);
- }
- this.iframe.contentDocument.close();
-
- }
-
- return loaded;
- }
-
- onLoad(event, promise) {
-
- this.window = this.iframe.contentWindow;
- this.document = this.iframe.contentDocument;
-
- this.contents = new Contents(this.document, this.document.body, this.section.cfiBase, this.section.index);
-
- this.rendering = false;
-
- var link = this.document.querySelector("link[rel='canonical']");
- if (link) {
- link.setAttribute("href", this.section.canonical);
- } else {
- link = this.document.createElement("link");
- link.setAttribute("rel", "canonical");
- link.setAttribute("href", this.section.canonical);
- this.document.querySelector("head").appendChild(link);
- }
-
- this.contents.on(EVENTS.CONTENTS.EXPAND, () => {
- if(this.displayed && this.iframe) {
- this.expand();
- if (this.contents) {
- this.layout.format(this.contents);
- }
- }
- });
-
- this.contents.on(EVENTS.CONTENTS.RESIZE, (e) => {
- if(this.displayed && this.iframe) {
- this.expand();
- if (this.contents) {
- this.layout.format(this.contents);
- }
- }
- });
-
- promise.resolve(this.contents);
- }
-
- setLayout(layout) {
- this.layout = layout;
-
- if (this.contents) {
- this.layout.format(this.contents);
- this.expand();
- }
- }
-
- setAxis(axis) {
-
- this.settings.axis = axis;
-
- if(axis == "horizontal"){
- this.element.style.flex = "none";
- } else {
- this.element.style.flex = "initial";
- }
-
- this.size();
-
- }
-
- setWritingMode(mode) {
- // this.element.style.writingMode = writingMode;
- this.writingMode = mode;
- }
-
- addListeners() {
- //TODO: Add content listeners for expanding
- }
-
- removeListeners(layoutFunc) {
- //TODO: remove content listeners for expanding
- }
-
- display(request) {
- var displayed = new defer();
-
- if (!this.displayed) {
-
- this.render(request)
- .then(function () {
-
- this.emit(EVENTS.VIEWS.DISPLAYED, this);
- this.onDisplayed(this);
-
- this.displayed = true;
- displayed.resolve(this);
-
- }.bind(this), function (err) {
- displayed.reject(err, this);
- });
-
- } else {
- displayed.resolve(this);
- }
-
-
- return displayed.promise;
- }
-
- show() {
-
- this.element.style.visibility = "visible";
-
- if(this.iframe){
- this.iframe.style.visibility = "visible";
-
- // Remind Safari to redraw the iframe
- this.iframe.style.transform = "translateZ(0)";
- this.iframe.offsetWidth;
- this.iframe.style.transform = null;
- }
-
- this.emit(EVENTS.VIEWS.SHOWN, this);
- }
-
- hide() {
- // this.iframe.style.display = "none";
- this.element.style.visibility = "hidden";
- this.iframe.style.visibility = "hidden";
-
- this.stopExpanding = true;
- this.emit(EVENTS.VIEWS.HIDDEN, this);
- }
-
- offset() {
- return {
- top: this.element.offsetTop,
- left: this.element.offsetLeft
- }
- }
-
- width() {
- return this._width;
- }
-
- height() {
- return this._height;
- }
-
- position() {
- return this.element.getBoundingClientRect();
- }
-
- locationOf(target) {
- var parentPos = this.iframe.getBoundingClientRect();
- var targetPos = this.contents.locationOf(target, this.settings.ignoreClass);
-
- return {
- "left": targetPos.left,
- "top": targetPos.top
- };
- }
-
- onDisplayed(view) {
- // Stub, override with a custom functions
- }
-
- onResize(view, e) {
- // Stub, override with a custom functions
- }
-
- bounds(force) {
- if(force || !this.elementBounds) {
- this.elementBounds = bounds(this.element);
- }
-
- return this.elementBounds;
- }
-
- highlight(cfiRange, data={}, cb, className = "epubjs-hl", styles = {}) {
- if (!this.contents) {
- return;
- }
- const attributes = Object.assign({"fill": "yellow", "fill-opacity": "0.3", "mix-blend-mode": "multiply"}, styles);
- let range = this.contents.range(cfiRange);
-
- let emitter = () => {
- this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data);
- };
-
- data["epubcfi"] = cfiRange;
-
- if (!this.pane) {
- this.pane = new Pane(this.iframe, this.element);
- }
-
- let m = new Highlight(range, className, data, attributes);
- let h = this.pane.addMark(m);
-
- this.highlights[cfiRange] = { "mark": h, "element": h.element, "listeners": [emitter, cb] };
-
- h.element.setAttribute("ref", className);
- h.element.addEventListener("click", emitter);
- h.element.addEventListener("touchstart", emitter);
-
- if (cb) {
- h.element.addEventListener("click", cb);
- h.element.addEventListener("touchstart", cb);
- }
- return h;
- }
-
- underline(cfiRange, data={}, cb, className = "epubjs-ul", styles = {}) {
- if (!this.contents) {
- return;
- }
- const attributes = Object.assign({"stroke": "black", "stroke-opacity": "0.3", "mix-blend-mode": "multiply"}, styles);
- let range = this.contents.range(cfiRange);
- let emitter = () => {
- this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data);
- };
-
- data["epubcfi"] = cfiRange;
-
- if (!this.pane) {
- this.pane = new Pane(this.iframe, this.element);
- }
-
- let m = new Underline(range, className, data, attributes);
- let h = this.pane.addMark(m);
-
- this.underlines[cfiRange] = { "mark": h, "element": h.element, "listeners": [emitter, cb] };
-
- h.element.setAttribute("ref", className);
- h.element.addEventListener("click", emitter);
- h.element.addEventListener("touchstart", emitter);
-
- if (cb) {
- h.element.addEventListener("click", cb);
- h.element.addEventListener("touchstart", cb);
- }
- return h;
- }
-
- mark(cfiRange, data={}, cb) {
- if (!this.contents) {
- return;
- }
-
- if (cfiRange in this.marks) {
- let item = this.marks[cfiRange];
- return item;
- }
-
- let range = this.contents.range(cfiRange);
- if (!range) {
- return;
- }
- let container = range.commonAncestorContainer;
- let parent = (container.nodeType === 1) ? container : container.parentNode;
-
- let emitter = (e) => {
- this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data);
- };
-
- if (range.collapsed && container.nodeType === 1) {
- range = new Range();
- range.selectNodeContents(container);
- } else if (range.collapsed) { // Webkit doesn't like collapsed ranges
- range = new Range();
- range.selectNodeContents(parent);
- }
-
- let mark = this.document.createElement("a");
- mark.setAttribute("ref", "epubjs-mk");
- mark.style.position = "absolute";
-
- mark.dataset["epubcfi"] = cfiRange;
-
- if (data) {
- Object.keys(data).forEach((key) => {
- mark.dataset[key] = data[key];
- });
- }
-
- if (cb) {
- mark.addEventListener("click", cb);
- mark.addEventListener("touchstart", cb);
- }
-
- mark.addEventListener("click", emitter);
- mark.addEventListener("touchstart", emitter);
-
- this.placeMark(mark, range);
-
- this.element.appendChild(mark);
-
- this.marks[cfiRange] = { "element": mark, "range": range, "listeners": [emitter, cb] };
-
- return parent;
- }
-
- placeMark(element, range) {
- let top, right, left;
-
- if(this.layout.name === "pre-paginated" ||
- this.settings.axis !== "horizontal") {
- let pos = range.getBoundingClientRect();
- top = pos.top;
- right = pos.right;
- } else {
- // Element might break columns, so find the left most element
- let rects = range.getClientRects();
-
- let rect;
- for (var i = 0; i != rects.length; i++) {
- rect = rects[i];
- if (!left || rect.left < left) {
- left = rect.left;
- // right = rect.right;
- right = Math.ceil(left / this.layout.props.pageWidth) * this.layout.props.pageWidth - (this.layout.gap / 2);
- top = rect.top;
- }
- }
- }
-
- element.style.top = `${top}px`;
- element.style.left = `${right}px`;
- }
-
- unhighlight(cfiRange) {
- let item;
- if (cfiRange in this.highlights) {
- item = this.highlights[cfiRange];
-
- this.pane.removeMark(item.mark);
- item.listeners.forEach((l) => {
- if (l) {
- item.element.removeEventListener("click", l);
- item.element.removeEventListener("touchstart", l);
- };
- });
- delete this.highlights[cfiRange];
- }
- }
-
- ununderline(cfiRange) {
- let item;
- if (cfiRange in this.underlines) {
- item = this.underlines[cfiRange];
- this.pane.removeMark(item.mark);
- item.listeners.forEach((l) => {
- if (l) {
- item.element.removeEventListener("click", l);
- item.element.removeEventListener("touchstart", l);
- };
- });
- delete this.underlines[cfiRange];
- }
- }
-
- unmark(cfiRange) {
- let item;
- if (cfiRange in this.marks) {
- item = this.marks[cfiRange];
- this.element.removeChild(item.element);
- item.listeners.forEach((l) => {
- if (l) {
- item.element.removeEventListener("click", l);
- item.element.removeEventListener("touchstart", l);
- };
- });
- delete this.marks[cfiRange];
- }
- }
-
- destroy() {
-
- for (let cfiRange in this.highlights) {
- this.unhighlight(cfiRange);
- }
-
- for (let cfiRange in this.underlines) {
- this.ununderline(cfiRange);
- }
-
- for (let cfiRange in this.marks) {
- this.unmark(cfiRange);
- }
-
- if (this.blobUrl) {
- revokeBlobUrl(this.blobUrl);
- }
-
- if(this.displayed){
- this.displayed = false;
-
- this.removeListeners();
- this.contents.destroy();
-
- this.stopExpanding = true;
- this.element.removeChild(this.iframe);
-
- if (this.pane) {
- this.pane.element.remove();
- this.pane = undefined;
- }
-
- this.iframe = undefined;
- this.contents = undefined;
-
- this._textWidth = null;
- this._textHeight = null;
- this._width = null;
- this._height = null;
- }
-
- // this.element.style.height = "0px";
- // this.element.style.width = "0px";
- }
+ constructor(section, options) {
+ this.settings = extend(
+ {
+ ignoreClass: "",
+ axis: undefined, //options.layout && options.layout.props.flow === "scrolled" ? "vertical" : "horizontal",
+ direction: undefined,
+ width: 0,
+ height: 0,
+ layout: undefined,
+ globalLayoutProperties: {},
+ method: undefined,
+ forceRight: false,
+ allowScriptedContent: false,
+ allowPopups: false,
+ selectionStopDelay: 250,
+ },
+ options || {}
+ );
+
+ this.id = "epubjs-view-" + uuid();
+ this.section = section;
+ this.index = section.index;
+
+ this.element = this.container(this.settings.axis);
+
+ this.added = false;
+ this.displayed = false;
+ this.rendered = false;
+
+ // this.width = this.settings.width;
+ // this.height = this.settings.height;
+
+ this.fixedWidth = 0;
+ this.fixedHeight = 0;
+
+ // Blank Cfi for Parsing
+ this.epubcfi = new EpubCFI();
+
+ this.layout = this.settings.layout;
+ // Dom events to listen for
+ // this.listenedEvents = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart"];
+
+ this.pane = undefined;
+ this.highlights = {};
+ this.underlines = {};
+ this.marks = {};
+ }
+
+ container(axis) {
+ var element = document.createElement("div");
+
+ element.classList.add("epub-view");
+
+ // this.element.style.minHeight = "100px";
+ element.style.height = "0px";
+ element.style.width = "0px";
+ element.style.overflow = "hidden";
+ element.style.position = "relative";
+ element.style.display = "block";
+
+ if (axis && axis == "horizontal") {
+ element.style.flex = "none";
+ } else {
+ element.style.flex = "initial";
+ }
+
+ return element;
+ }
+
+ create() {
+ if (this.iframe) {
+ return this.iframe;
+ }
+
+ if (!this.element) {
+ this.element = this.createContainer();
+ }
+
+ this.iframe = document.createElement("iframe");
+ this.iframe.id = this.id;
+ this.iframe.scrolling = "no"; // Might need to be removed: breaks ios width calculations
+ this.iframe.style.overflow = "hidden";
+ this.iframe.seamless = "seamless";
+ // Back up if seamless isn't supported
+ this.iframe.style.border = "none";
+
+ // sandbox
+ this.iframe.sandbox = "allow-same-origin";
+ if (this.settings.allowScriptedContent) {
+ this.iframe.sandbox += " allow-scripts";
+ }
+ if (this.settings.allowPopups) {
+ this.iframe.sandbox += " allow-popups";
+ }
+
+ this.iframe.setAttribute("enable-annotation", "true");
+
+ this.resizing = true;
+
+ // this.iframe.style.display = "none";
+ this.element.style.visibility = "hidden";
+ this.iframe.style.visibility = "hidden";
+
+ this.iframe.style.width = "0";
+ this.iframe.style.height = "0";
+ this._width = 0;
+ this._height = 0;
+
+ this.element.setAttribute("ref", this.index);
+
+ this.added = true;
+
+ this.elementBounds = bounds(this.element);
+
+ // if(width || height){
+ // this.resize(width, height);
+ // } else if(this.width && this.height){
+ // this.resize(this.width, this.height);
+ // } else {
+ // this.iframeBounds = bounds(this.iframe);
+ // }
+
+ if ("srcdoc" in this.iframe) {
+ this.supportsSrcdoc = true;
+ } else {
+ this.supportsSrcdoc = false;
+ }
+
+ if (!this.settings.method) {
+ this.settings.method = this.supportsSrcdoc ? "srcdoc" : "write";
+ }
+
+ return this.iframe;
+ }
+
+ render(request, show) {
+ // view.onLayout = this.layout.format.bind(this.layout);
+ this.create();
+
+ // Fit to size of the container, apply padding
+ this.size();
+
+ if (!this.sectionRender) {
+ this.sectionRender = this.section.render(request);
+ }
+
+ // Render Chain
+ return this.sectionRender
+ .then(
+ function (contents) {
+ return this.load(contents);
+ }.bind(this)
+ )
+ .then(
+ function () {
+ // find and report the writingMode axis
+ let writingMode = this.contents.writingMode();
+
+ // Set the axis based on the flow and writing mode
+ let axis;
+ if (this.settings.flow === "scrolled") {
+ axis =
+ writingMode.indexOf("vertical") === 0 ? "horizontal" : "vertical";
+ } else {
+ axis =
+ writingMode.indexOf("vertical") === 0 ? "vertical" : "horizontal";
+ }
+
+ if (
+ writingMode.indexOf("vertical") === 0 &&
+ this.settings.flow === "paginated"
+ ) {
+ this.layout.delta = this.layout.height;
+ }
+
+ this.setAxis(axis);
+ this.emit(EVENTS.VIEWS.AXIS, axis);
+
+ this.setWritingMode(writingMode);
+ this.emit(EVENTS.VIEWS.WRITING_MODE, writingMode);
+
+ // apply the layout function to the contents
+ this.layout.format(this.contents, this.section, this.axis);
+
+ // Listen for events that require an expansion of the iframe
+ this.addListeners();
+
+ return new Promise((resolve, reject) => {
+ // Expand the iframe to the full size of the content
+ this.expand();
+
+ if (this.settings.forceRight) {
+ this.element.style.marginLeft = this.width() + "px";
+ }
+ resolve();
+ });
+ }.bind(this),
+ function (e) {
+ this.emit(EVENTS.VIEWS.LOAD_ERROR, e);
+ return new Promise((resolve, reject) => {
+ reject(e);
+ });
+ }.bind(this)
+ )
+ .then(
+ function () {
+ this.emit(EVENTS.VIEWS.RENDERED, this.section);
+ }.bind(this)
+ );
+ }
+
+ reset() {
+ if (this.iframe) {
+ this.iframe.style.width = "0";
+ this.iframe.style.height = "0";
+ this._width = 0;
+ this._height = 0;
+ this._textWidth = undefined;
+ this._contentWidth = undefined;
+ this._textHeight = undefined;
+ this._contentHeight = undefined;
+ }
+ this._needsReframe = true;
+ }
+
+ // Determine locks base on settings
+ size(_width, _height) {
+ var width = _width || this.settings.width;
+ var height = _height || this.settings.height;
+
+ if (this.layout.name === "pre-paginated") {
+ this.lock("both", width, height);
+ } else if (this.settings.axis === "horizontal") {
+ this.lock("height", width, height);
+ } else {
+ this.lock("width", width, height);
+ }
+
+ this.settings.width = width;
+ this.settings.height = height;
+ }
+
+ // Lock an axis to element dimensions, taking borders into account
+ lock(what, width, height) {
+ var elBorders = borders(this.element);
+ var iframeBorders;
+
+ if (this.iframe) {
+ iframeBorders = borders(this.iframe);
+ } else {
+ iframeBorders = { width: 0, height: 0 };
+ }
+
+ if (what == "width" && isNumber(width)) {
+ this.lockedWidth = width - elBorders.width - iframeBorders.width;
+ // this.resize(this.lockedWidth, width); // width keeps ratio correct
+ }
+
+ if (what == "height" && isNumber(height)) {
+ this.lockedHeight = height - elBorders.height - iframeBorders.height;
+ // this.resize(width, this.lockedHeight);
+ }
+
+ if (what === "both" && isNumber(width) && isNumber(height)) {
+ this.lockedWidth = width - elBorders.width - iframeBorders.width;
+ this.lockedHeight = height - elBorders.height - iframeBorders.height;
+ // this.resize(this.lockedWidth, this.lockedHeight);
+ }
+
+ if (this.displayed && this.iframe) {
+ // this.contents.layout();
+ this.expand();
+ }
+ }
+
+ // Resize a single axis based on content dimensions
+ expand(force) {
+ var width = this.lockedWidth;
+ var height = this.lockedHeight;
+ var columns;
+
+ var textWidth, textHeight;
+
+ if (!this.iframe || this._expanding) return;
+
+ this._expanding = true;
+
+ if (this.layout.name === "pre-paginated") {
+ width = this.layout.columnWidth;
+ height = this.layout.height;
+ }
+ // Expand Horizontally
+ else if (this.settings.axis === "horizontal") {
+ // Get the width of the text
+ width = this.contents.textWidth();
+
+ if (width % this.layout.pageWidth > 0) {
+ width =
+ Math.ceil(width / this.layout.pageWidth) * this.layout.pageWidth;
+ }
+
+ if (this.settings.forceEvenPages) {
+ columns = width / this.layout.pageWidth;
+ if (
+ this.layout.divisor > 1 &&
+ this.layout.name === "reflowable" &&
+ columns % 2 > 0
+ ) {
+ // add a blank page
+ width += this.layout.pageWidth;
+ }
+ }
+ } // Expand Vertically
+ else if (this.settings.axis === "vertical") {
+ height = this.contents.textHeight();
+ if (
+ this.settings.flow === "paginated" &&
+ height % this.layout.height > 0
+ ) {
+ height = Math.ceil(height / this.layout.height) * this.layout.height;
+ }
+ }
+
+ // Only Resize if dimensions have changed or
+ // if Frame is still hidden, so needs reframing
+ if (this._needsReframe || width != this._width || height != this._height) {
+ this.reframe(width, height);
+ }
+
+ this._expanding = false;
+ }
+
+ reframe(width, height) {
+ var size;
+
+ if (isNumber(width)) {
+ this.element.style.width = width + "px";
+ this.iframe.style.width = width + "px";
+ this._width = width;
+ }
+
+ if (isNumber(height)) {
+ this.element.style.height = height + "px";
+ this.iframe.style.height = height + "px";
+ this._height = height;
+ }
+
+ let widthDelta = this.prevBounds ? width - this.prevBounds.width : width;
+ let heightDelta = this.prevBounds
+ ? height - this.prevBounds.height
+ : height;
+
+ size = {
+ width: width,
+ height: height,
+ widthDelta: widthDelta,
+ heightDelta: heightDelta,
+ };
+
+ this.pane && this.pane.render();
+
+ requestAnimationFrame(() => {
+ let mark;
+ for (let m in this.marks) {
+ if (this.marks.hasOwnProperty(m)) {
+ mark = this.marks[m];
+ this.placeMark(mark.element, mark.range);
+ }
+ }
+ });
+
+ this.onResize(this, size);
+
+ this.emit(EVENTS.VIEWS.RESIZED, size);
+
+ this.prevBounds = size;
+
+ this.elementBounds = bounds(this.element);
+ }
+
+ load(contents) {
+ var loading = new defer();
+ var loaded = loading.promise;
+
+ if (!this.iframe) {
+ loading.reject(new Error("No Iframe Available"));
+ return loaded;
+ }
+
+ this.iframe.onload = function (event) {
+ this.onLoad(event, loading);
+ }.bind(this);
+
+ if (this.settings.method === "blobUrl") {
+ this.blobUrl = createBlobUrl(contents, "application/xhtml+xml");
+ this.iframe.src = this.blobUrl;
+
+ this.element.appendChild(this.iframe);
+ } else if (this.settings.method === "srcdoc") {
+ this.iframe.srcdoc = contents;
+ this.element.appendChild(this.iframe);
+ } else {
+ this.element.appendChild(this.iframe);
+
+ this.document = this.iframe.contentDocument;
+
+ if (!this.document) {
+ loading.reject(new Error("No Document Available"));
+ return loaded;
+ }
+
+ this.iframe.contentDocument.open();
+ // For Cordova windows platform
+ if (window.MSApp && MSApp.execUnsafeLocalFunction) {
+ var outerThis = this;
+ MSApp.execUnsafeLocalFunction(function () {
+ outerThis.iframe.contentDocument.write(contents);
+ });
+ } else {
+ this.iframe.contentDocument.write(contents);
+ }
+ this.iframe.contentDocument.close();
+ }
+
+ return loaded;
+ }
+
+ onLoad(event, promise) {
+ this.window = this.iframe.contentWindow;
+ this.document = this.iframe.contentDocument;
+
+ this.contents = new Contents(
+ this.document,
+ this.document.body,
+ this.section.cfiBase,
+ this.section.index,
+ this.settings
+ );
+
+ this.rendering = false;
+
+ var link = this.document.querySelector("link[rel='canonical']");
+ if (link) {
+ link.setAttribute("href", this.section.canonical);
+ } else {
+ link = this.document.createElement("link");
+ link.setAttribute("rel", "canonical");
+ link.setAttribute("href", this.section.canonical);
+ this.document.querySelector("head").appendChild(link);
+ }
+
+ this.contents.on(EVENTS.CONTENTS.EXPAND, () => {
+ if (this.displayed && this.iframe) {
+ this.expand();
+ if (this.contents) {
+ this.layout.format(this.contents);
+ }
+ }
+ });
+
+ this.contents.on(EVENTS.CONTENTS.RESIZE, (e) => {
+ if (this.displayed && this.iframe) {
+ this.expand();
+ if (this.contents) {
+ this.layout.format(this.contents);
+ }
+ }
+ });
+
+ promise.resolve(this.contents);
+ }
+
+ setLayout(layout) {
+ this.layout = layout;
+
+ if (this.contents) {
+ this.layout.format(this.contents);
+ this.expand();
+ }
+ }
+
+ setAxis(axis) {
+ this.settings.axis = axis;
+
+ if (axis == "horizontal") {
+ this.element.style.flex = "none";
+ } else {
+ this.element.style.flex = "initial";
+ }
+
+ this.size();
+ }
+
+ setWritingMode(mode) {
+ // this.element.style.writingMode = writingMode;
+ this.writingMode = mode;
+ }
+
+ addListeners() {
+ //TODO: Add content listeners for expanding
+ }
+
+ removeListeners(layoutFunc) {
+ //TODO: remove content listeners for expanding
+ }
+
+ display(request) {
+ var displayed = new defer();
+
+ if (!this.displayed) {
+ this.render(request).then(
+ function () {
+ this.emit(EVENTS.VIEWS.DISPLAYED, this);
+ this.onDisplayed(this);
+
+ this.displayed = true;
+ displayed.resolve(this);
+ }.bind(this),
+ function (err) {
+ displayed.reject(err, this);
+ }
+ );
+ } else {
+ displayed.resolve(this);
+ }
+
+ return displayed.promise;
+ }
+
+ show() {
+ this.element.style.visibility = "visible";
+
+ if (this.iframe) {
+ this.iframe.style.visibility = "visible";
+
+ // Remind Safari to redraw the iframe
+ this.iframe.style.transform = "translateZ(0)";
+ this.iframe.offsetWidth;
+ this.iframe.style.transform = null;
+ }
+
+ this.emit(EVENTS.VIEWS.SHOWN, this);
+ }
+
+ hide() {
+ // this.iframe.style.display = "none";
+ this.element.style.visibility = "hidden";
+ this.iframe.style.visibility = "hidden";
+
+ this.stopExpanding = true;
+ this.emit(EVENTS.VIEWS.HIDDEN, this);
+ }
+
+ offset() {
+ return {
+ top: this.element.offsetTop,
+ left: this.element.offsetLeft,
+ };
+ }
+
+ width() {
+ return this._width;
+ }
+
+ height() {
+ return this._height;
+ }
+
+ position() {
+ return this.element.getBoundingClientRect();
+ }
+
+ locationOf(target) {
+ var parentPos = this.iframe.getBoundingClientRect();
+ var targetPos = this.contents.locationOf(target, this.settings.ignoreClass);
+
+ return {
+ left: targetPos.left,
+ top: targetPos.top,
+ };
+ }
+
+ onDisplayed(view) {
+ // Stub, override with a custom functions
+ }
+
+ onResize(view, e) {
+ // Stub, override with a custom functions
+ }
+
+ bounds(force) {
+ if (force || !this.elementBounds) {
+ this.elementBounds = bounds(this.element);
+ }
+
+ return this.elementBounds;
+ }
+
+ highlight(cfiRange, data = {}, cb, className = "epubjs-hl", styles = {}) {
+ if (!this.contents) {
+ return;
+ }
+ const attributes = Object.assign(
+ { fill: "yellow", "fill-opacity": "0.3", "mix-blend-mode": "multiply" },
+ styles
+ );
+ let range = this.contents.range(cfiRange);
+
+ let emitter = () => {
+ this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data);
+ };
+
+ data["epubcfi"] = cfiRange;
+
+ if (!this.pane) {
+ this.pane = new Pane(this.iframe, this.element);
+ }
+
+ let m = new Highlight(range, className, data, attributes);
+ let h = this.pane.addMark(m);
+
+ this.highlights[cfiRange] = {
+ mark: h,
+ element: h.element,
+ listeners: [emitter, cb],
+ };
+
+ h.element.setAttribute("ref", className);
+ h.element.addEventListener("click", emitter);
+ h.element.addEventListener("touchstart", emitter);
+
+ if (cb) {
+ h.element.addEventListener("click", cb);
+ h.element.addEventListener("touchstart", cb);
+ }
+ return h;
+ }
+
+ underline(cfiRange, data = {}, cb, className = "epubjs-ul", styles = {}) {
+ if (!this.contents) {
+ return;
+ }
+ const attributes = Object.assign(
+ {
+ stroke: "black",
+ "stroke-opacity": "0.3",
+ "mix-blend-mode": "multiply",
+ },
+ styles
+ );
+ let range = this.contents.range(cfiRange);
+ let emitter = () => {
+ this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data);
+ };
+
+ data["epubcfi"] = cfiRange;
+
+ if (!this.pane) {
+ this.pane = new Pane(this.iframe, this.element);
+ }
+
+ let m = new Underline(range, className, data, attributes);
+ let h = this.pane.addMark(m);
+
+ this.underlines[cfiRange] = {
+ mark: h,
+ element: h.element,
+ listeners: [emitter, cb],
+ };
+
+ h.element.setAttribute("ref", className);
+ h.element.addEventListener("click", emitter);
+ h.element.addEventListener("touchstart", emitter);
+
+ if (cb) {
+ h.element.addEventListener("click", cb);
+ h.element.addEventListener("touchstart", cb);
+ }
+ return h;
+ }
+
+ mark(cfiRange, data = {}, cb) {
+ if (!this.contents) {
+ return;
+ }
+
+ if (cfiRange in this.marks) {
+ let item = this.marks[cfiRange];
+ return item;
+ }
+
+ let range = this.contents.range(cfiRange);
+ if (!range) {
+ return;
+ }
+ let container = range.commonAncestorContainer;
+ let parent = container.nodeType === 1 ? container : container.parentNode;
+
+ let emitter = (e) => {
+ this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data);
+ };
+
+ if (range.collapsed && container.nodeType === 1) {
+ range = new Range();
+ range.selectNodeContents(container);
+ } else if (range.collapsed) {
+ // Webkit doesn't like collapsed ranges
+ range = new Range();
+ range.selectNodeContents(parent);
+ }
+
+ let mark = this.document.createElement("a");
+ mark.setAttribute("ref", "epubjs-mk");
+ mark.style.position = "absolute";
+
+ mark.dataset["epubcfi"] = cfiRange;
+
+ if (data) {
+ Object.keys(data).forEach((key) => {
+ mark.dataset[key] = data[key];
+ });
+ }
+
+ if (cb) {
+ mark.addEventListener("click", cb);
+ mark.addEventListener("touchstart", cb);
+ }
+
+ mark.addEventListener("click", emitter);
+ mark.addEventListener("touchstart", emitter);
+
+ this.placeMark(mark, range);
+
+ this.element.appendChild(mark);
+
+ this.marks[cfiRange] = {
+ element: mark,
+ range: range,
+ listeners: [emitter, cb],
+ };
+
+ return parent;
+ }
+
+ placeMark(element, range) {
+ let top, right, left;
+
+ if (
+ this.layout.name === "pre-paginated" ||
+ this.settings.axis !== "horizontal"
+ ) {
+ let pos = range.getBoundingClientRect();
+ top = pos.top;
+ right = pos.right;
+ } else {
+ // Element might break columns, so find the left most element
+ let rects = range.getClientRects();
+
+ let rect;
+ for (var i = 0; i != rects.length; i++) {
+ rect = rects[i];
+ if (!left || rect.left < left) {
+ left = rect.left;
+ // right = rect.right;
+ right =
+ Math.ceil(left / this.layout.props.pageWidth) *
+ this.layout.props.pageWidth -
+ this.layout.gap / 2;
+ top = rect.top;
+ }
+ }
+ }
+
+ element.style.top = `${top}px`;
+ element.style.left = `${right}px`;
+ }
+
+ unhighlight(cfiRange) {
+ let item;
+ if (cfiRange in this.highlights) {
+ item = this.highlights[cfiRange];
+
+ this.pane.removeMark(item.mark);
+ item.listeners.forEach((l) => {
+ if (l) {
+ item.element.removeEventListener("click", l);
+ item.element.removeEventListener("touchstart", l);
+ }
+ });
+ delete this.highlights[cfiRange];
+ }
+ }
+
+ ununderline(cfiRange) {
+ let item;
+ if (cfiRange in this.underlines) {
+ item = this.underlines[cfiRange];
+ this.pane.removeMark(item.mark);
+ item.listeners.forEach((l) => {
+ if (l) {
+ item.element.removeEventListener("click", l);
+ item.element.removeEventListener("touchstart", l);
+ }
+ });
+ delete this.underlines[cfiRange];
+ }
+ }
+
+ unmark(cfiRange) {
+ let item;
+ if (cfiRange in this.marks) {
+ item = this.marks[cfiRange];
+ this.element.removeChild(item.element);
+ item.listeners.forEach((l) => {
+ if (l) {
+ item.element.removeEventListener("click", l);
+ item.element.removeEventListener("touchstart", l);
+ }
+ });
+ delete this.marks[cfiRange];
+ }
+ }
+
+ destroy() {
+ for (let cfiRange in this.highlights) {
+ this.unhighlight(cfiRange);
+ }
+
+ for (let cfiRange in this.underlines) {
+ this.ununderline(cfiRange);
+ }
+
+ for (let cfiRange in this.marks) {
+ this.unmark(cfiRange);
+ }
+
+ if (this.blobUrl) {
+ revokeBlobUrl(this.blobUrl);
+ }
+
+ if (this.displayed) {
+ this.displayed = false;
+
+ this.removeListeners();
+ this.contents.destroy();
+
+ this.stopExpanding = true;
+ this.element.removeChild(this.iframe);
+
+ if (this.pane) {
+ this.pane.element.remove();
+ this.pane = undefined;
+ }
+
+ this.iframe = undefined;
+ this.contents = undefined;
+
+ this._textWidth = null;
+ this._textHeight = null;
+ this._width = null;
+ this._height = null;
+ }
+
+ // this.element.style.height = "0px";
+ // this.element.style.width = "0px";
+ }
}
EventEmitter(IframeView.prototype);
diff --git a/src/rendition.js b/src/rendition.js
index 14a21b450..a4ba31cc2 100644
--- a/src/rendition.js
+++ b/src/rendition.js
@@ -41,331 +41,342 @@ import ContinuousViewManager from "./managers/continuous/index";
* @param {boolean} [options.allowPopups=false] enable opening popup in content
*/
class Rendition {
- constructor(book, options) {
-
- this.settings = extend(this.settings || {}, {
- width: null,
- height: null,
- ignoreClass: "",
- manager: "default",
- view: "iframe",
- flow: null,
- layout: null,
- spread: null,
- minSpreadWidth: 800,
- stylesheet: null,
- resizeOnOrientationChange: true,
- script: null,
- snap: false,
- defaultDirection: "ltr",
- allowScriptedContent: false,
- allowPopups: false
- });
-
- extend(this.settings, options);
-
- if (typeof(this.settings.manager) === "object") {
- this.manager = this.settings.manager;
- }
-
- this.book = book;
-
- /**
- * Adds Hook methods to the Rendition prototype
- * @member {object} hooks
- * @property {Hook} hooks.content
- * @memberof Rendition
- */
- this.hooks = {};
- this.hooks.display = new Hook(this);
- this.hooks.serialize = new Hook(this);
- this.hooks.content = new Hook(this);
- this.hooks.unloaded = new Hook(this);
- this.hooks.layout = new Hook(this);
- this.hooks.render = new Hook(this);
- this.hooks.show = new Hook(this);
-
- this.hooks.content.register(this.handleLinks.bind(this));
- this.hooks.content.register(this.passEvents.bind(this));
- this.hooks.content.register(this.adjustImages.bind(this));
-
- this.book.spine.hooks.content.register(this.injectIdentifier.bind(this));
-
- if (this.settings.stylesheet) {
- this.book.spine.hooks.content.register(this.injectStylesheet.bind(this));
- }
-
- if (this.settings.script) {
- this.book.spine.hooks.content.register(this.injectScript.bind(this));
- }
-
- /**
- * @member {Themes} themes
- * @memberof Rendition
- */
- this.themes = new Themes(this);
-
- /**
- * @member {Annotations} annotations
- * @memberof Rendition
- */
- this.annotations = new Annotations(this);
-
- this.epubcfi = new EpubCFI();
-
- this.q = new Queue(this);
-
- /**
- * A Rendered Location Range
- * @typedef location
- * @type {Object}
- * @property {object} start
- * @property {string} start.index
- * @property {string} start.href
- * @property {object} start.displayed
- * @property {EpubCFI} start.cfi
- * @property {number} start.location
- * @property {number} start.percentage
- * @property {number} start.displayed.page
- * @property {number} start.displayed.total
- * @property {object} end
- * @property {string} end.index
- * @property {string} end.href
- * @property {object} end.displayed
- * @property {EpubCFI} end.cfi
- * @property {number} end.location
- * @property {number} end.percentage
- * @property {number} end.displayed.page
- * @property {number} end.displayed.total
- * @property {boolean} atStart
- * @property {boolean} atEnd
- * @memberof Rendition
- */
- this.location = undefined;
-
- // Hold queue until book is opened
- this.q.enqueue(this.book.opened);
-
- this.starting = new defer();
- /**
- * @member {promise} started returns after the rendition has started
- * @memberof Rendition
- */
- this.started = this.starting.promise;
-
- // Block the queue until rendering is started
- this.q.enqueue(this.start);
- }
-
- /**
- * Set the manager function
- * @param {function} manager
- */
- setManager(manager) {
- this.manager = manager;
- }
-
- /**
- * Require the manager from passed string, or as a class function
- * @param {string|object} manager [description]
- * @return {method}
- */
- requireManager(manager) {
- var viewManager;
-
- // If manager is a string, try to load from imported managers
- if (typeof manager === "string" && manager === "default") {
- viewManager = DefaultViewManager;
- } else if (typeof manager === "string" && manager === "continuous") {
- viewManager = ContinuousViewManager;
- } else {
- // otherwise, assume we were passed a class function
- viewManager = manager;
- }
-
- return viewManager;
- }
-
- /**
- * Require the view from passed string, or as a class function
- * @param {string|object} view
- * @return {view}
- */
- requireView(view) {
- var View;
-
- // If view is a string, try to load from imported views,
- if (typeof view == "string" && view === "iframe") {
- View = IframeView;
- } else {
- // otherwise, assume we were passed a class function
- View = view;
- }
-
- return View;
- }
-
- /**
- * Start the rendering
- * @return {Promise} rendering has started
- */
- start(){
- if (!this.settings.layout && (this.book.package.metadata.layout === "pre-paginated" || this.book.displayOptions.fixedLayout === "true")) {
- this.settings.layout = "pre-paginated";
- }
- switch(this.book.package.metadata.spread) {
- case 'none':
- this.settings.spread = 'none';
- break;
- case 'both':
- this.settings.spread = true;
- break;
- }
-
- if(!this.manager) {
- this.ViewManager = this.requireManager(this.settings.manager);
- this.View = this.requireView(this.settings.view);
-
- this.manager = new this.ViewManager({
- view: this.View,
- queue: this.q,
- request: this.book.load.bind(this.book),
- settings: this.settings
- });
- }
-
- this.direction(this.book.package.metadata.direction || this.settings.defaultDirection);
-
- // Parse metadata to get layout props
- this.settings.globalLayoutProperties = this.determineLayoutProperties(this.book.package.metadata);
-
- this.flow(this.settings.globalLayoutProperties.flow);
-
- this.layout(this.settings.globalLayoutProperties);
-
- // Listen for displayed views
- this.manager.on(EVENTS.MANAGERS.ADDED, this.afterDisplayed.bind(this));
- this.manager.on(EVENTS.MANAGERS.REMOVED, this.afterRemoved.bind(this));
-
- // Listen for resizing
- this.manager.on(EVENTS.MANAGERS.RESIZED, this.onResized.bind(this));
-
- // Listen for rotation
- this.manager.on(EVENTS.MANAGERS.ORIENTATION_CHANGE, this.onOrientationChange.bind(this));
-
- // Listen for scroll changes
- this.manager.on(EVENTS.MANAGERS.SCROLLED, this.reportLocation.bind(this));
-
- /**
- * Emit that rendering has started
- * @event started
- * @memberof Rendition
- */
- this.emit(EVENTS.RENDITION.STARTED);
-
- // Start processing queue
- this.starting.resolve();
- }
-
- /**
- * Call to attach the container to an element in the dom
- * Container must be attached before rendering can begin
- * @param {element} element to attach to
- * @return {Promise}
- */
- attachTo(element){
-
- return this.q.enqueue(function () {
-
- // Start rendering
- this.manager.render(element, {
- "width" : this.settings.width,
- "height" : this.settings.height
- });
-
- /**
- * Emit that rendering has attached to an element
- * @event attached
- * @memberof Rendition
- */
- this.emit(EVENTS.RENDITION.ATTACHED);
-
- }.bind(this));
-
- }
-
- /**
- * Display a point in the book
- * The request will be added to the rendering Queue,
- * so it will wait until book is opened, rendering started
- * and all other rendering tasks have finished to be called.
- * @param {string} target Url or EpubCFI
- * @return {Promise}
- */
- display(target){
- if (this.displaying) {
- this.displaying.resolve();
- }
- return this.q.enqueue(this._display, target);
- }
-
- /**
- * Tells the manager what to display immediately
- * @private
- * @param {string} target Url or EpubCFI
- * @return {Promise}
- */
- _display(target){
- if (!this.book) {
- return;
- }
- var isCfiString = this.epubcfi.isCfiString(target);
- var displaying = new defer();
- var displayed = displaying.promise;
- var section;
- var moveTo;
-
- this.displaying = displaying;
-
- // Check if this is a book percentage
- if (this.book.locations.length() && isFloat(target)) {
- target = this.book.locations.cfiFromPercentage(parseFloat(target));
- }
-
- section = this.book.spine.get(target);
-
- if(!section){
- displaying.reject(new Error("No Section Found"));
- return displayed;
- }
-
- this.manager.display(section, target)
- .then(() => {
- displaying.resolve(section);
- this.displaying = undefined;
-
- /**
- * Emit that a section has been displayed
- * @event displayed
- * @param {Section} section
- * @memberof Rendition
- */
- this.emit(EVENTS.RENDITION.DISPLAYED, section);
- this.reportLocation();
- }, (err) => {
- /**
- * Emit that has been an error displaying
- * @event displayError
- * @param {Section} section
- * @memberof Rendition
- */
- this.emit(EVENTS.RENDITION.DISPLAY_ERROR, err);
- });
-
- return displayed;
- }
-
- /*
+ constructor(book, options) {
+ this.settings = extend(this.settings || {}, {
+ width: null,
+ height: null,
+ ignoreClass: "",
+ manager: "default",
+ view: "iframe",
+ flow: null,
+ layout: null,
+ spread: null,
+ minSpreadWidth: 800,
+ stylesheet: null,
+ resizeOnOrientationChange: true,
+ script: null,
+ snap: false,
+ defaultDirection: "ltr",
+ allowScriptedContent: false,
+ allowPopups: false,
+ selectionStopDelay: 250,
+ });
+
+ extend(this.settings, options);
+
+ if (typeof this.settings.manager === "object") {
+ this.manager = this.settings.manager;
+ }
+
+ this.book = book;
+
+ /**
+ * Adds Hook methods to the Rendition prototype
+ * @member {object} hooks
+ * @property {Hook} hooks.content
+ * @memberof Rendition
+ */
+ this.hooks = {};
+ this.hooks.display = new Hook(this);
+ this.hooks.serialize = new Hook(this);
+ this.hooks.content = new Hook(this);
+ this.hooks.unloaded = new Hook(this);
+ this.hooks.layout = new Hook(this);
+ this.hooks.render = new Hook(this);
+ this.hooks.show = new Hook(this);
+
+ this.hooks.content.register(this.handleLinks.bind(this));
+ this.hooks.content.register(this.passEvents.bind(this));
+ this.hooks.content.register(this.adjustImages.bind(this));
+
+ this.book.spine.hooks.content.register(this.injectIdentifier.bind(this));
+
+ if (this.settings.stylesheet) {
+ this.book.spine.hooks.content.register(this.injectStylesheet.bind(this));
+ }
+
+ if (this.settings.script) {
+ this.book.spine.hooks.content.register(this.injectScript.bind(this));
+ }
+
+ /**
+ * @member {Themes} themes
+ * @memberof Rendition
+ */
+ this.themes = new Themes(this);
+
+ /**
+ * @member {Annotations} annotations
+ * @memberof Rendition
+ */
+ this.annotations = new Annotations(this);
+
+ this.epubcfi = new EpubCFI();
+
+ this.q = new Queue(this);
+
+ /**
+ * A Rendered Location Range
+ * @typedef location
+ * @type {Object}
+ * @property {object} start
+ * @property {string} start.index
+ * @property {string} start.href
+ * @property {object} start.displayed
+ * @property {EpubCFI} start.cfi
+ * @property {number} start.location
+ * @property {number} start.percentage
+ * @property {number} start.displayed.page
+ * @property {number} start.displayed.total
+ * @property {object} end
+ * @property {string} end.index
+ * @property {string} end.href
+ * @property {object} end.displayed
+ * @property {EpubCFI} end.cfi
+ * @property {number} end.location
+ * @property {number} end.percentage
+ * @property {number} end.displayed.page
+ * @property {number} end.displayed.total
+ * @property {boolean} atStart
+ * @property {boolean} atEnd
+ * @memberof Rendition
+ */
+ this.location = undefined;
+
+ // Hold queue until book is opened
+ this.q.enqueue(this.book.opened);
+
+ this.starting = new defer();
+ /**
+ * @member {promise} started returns after the rendition has started
+ * @memberof Rendition
+ */
+ this.started = this.starting.promise;
+
+ // Block the queue until rendering is started
+ this.q.enqueue(this.start);
+ }
+
+ /**
+ * Set the manager function
+ * @param {function} manager
+ */
+ setManager(manager) {
+ this.manager = manager;
+ }
+
+ /**
+ * Require the manager from passed string, or as a class function
+ * @param {string|object} manager [description]
+ * @return {method}
+ */
+ requireManager(manager) {
+ var viewManager;
+
+ // If manager is a string, try to load from imported managers
+ if (typeof manager === "string" && manager === "default") {
+ viewManager = DefaultViewManager;
+ } else if (typeof manager === "string" && manager === "continuous") {
+ viewManager = ContinuousViewManager;
+ } else {
+ // otherwise, assume we were passed a class function
+ viewManager = manager;
+ }
+
+ return viewManager;
+ }
+
+ /**
+ * Require the view from passed string, or as a class function
+ * @param {string|object} view
+ * @return {view}
+ */
+ requireView(view) {
+ var View;
+
+ // If view is a string, try to load from imported views,
+ if (typeof view == "string" && view === "iframe") {
+ View = IframeView;
+ } else {
+ // otherwise, assume we were passed a class function
+ View = view;
+ }
+
+ return View;
+ }
+
+ /**
+ * Start the rendering
+ * @return {Promise} rendering has started
+ */
+ start() {
+ if (
+ !this.settings.layout &&
+ (this.book.package.metadata.layout === "pre-paginated" ||
+ this.book.displayOptions.fixedLayout === "true")
+ ) {
+ this.settings.layout = "pre-paginated";
+ }
+ switch (this.book.package.metadata.spread) {
+ case "none":
+ this.settings.spread = "none";
+ break;
+ case "both":
+ this.settings.spread = true;
+ break;
+ }
+
+ if (!this.manager) {
+ this.ViewManager = this.requireManager(this.settings.manager);
+ this.View = this.requireView(this.settings.view);
+
+ this.manager = new this.ViewManager({
+ view: this.View,
+ queue: this.q,
+ request: this.book.load.bind(this.book),
+ settings: this.settings,
+ });
+ }
+
+ this.direction(
+ this.book.package.metadata.direction || this.settings.defaultDirection
+ );
+
+ // Parse metadata to get layout props
+ this.settings.globalLayoutProperties = this.determineLayoutProperties(
+ this.book.package.metadata
+ );
+
+ this.flow(this.settings.globalLayoutProperties.flow);
+
+ this.layout(this.settings.globalLayoutProperties);
+
+ // Listen for displayed views
+ this.manager.on(EVENTS.MANAGERS.ADDED, this.afterDisplayed.bind(this));
+ this.manager.on(EVENTS.MANAGERS.REMOVED, this.afterRemoved.bind(this));
+
+ // Listen for resizing
+ this.manager.on(EVENTS.MANAGERS.RESIZED, this.onResized.bind(this));
+
+ // Listen for rotation
+ this.manager.on(
+ EVENTS.MANAGERS.ORIENTATION_CHANGE,
+ this.onOrientationChange.bind(this)
+ );
+
+ // Listen for scroll changes
+ this.manager.on(EVENTS.MANAGERS.SCROLLED, this.reportLocation.bind(this));
+
+ /**
+ * Emit that rendering has started
+ * @event started
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.STARTED);
+
+ // Start processing queue
+ this.starting.resolve();
+ }
+
+ /**
+ * Call to attach the container to an element in the dom
+ * Container must be attached before rendering can begin
+ * @param {element} element to attach to
+ * @return {Promise}
+ */
+ attachTo(element) {
+ return this.q.enqueue(
+ function () {
+ // Start rendering
+ this.manager.render(element, {
+ width: this.settings.width,
+ height: this.settings.height,
+ });
+
+ /**
+ * Emit that rendering has attached to an element
+ * @event attached
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.ATTACHED);
+ }.bind(this)
+ );
+ }
+
+ /**
+ * Display a point in the book
+ * The request will be added to the rendering Queue,
+ * so it will wait until book is opened, rendering started
+ * and all other rendering tasks have finished to be called.
+ * @param {string} target Url or EpubCFI
+ * @return {Promise}
+ */
+ display(target) {
+ if (this.displaying) {
+ this.displaying.resolve();
+ }
+ return this.q.enqueue(this._display, target);
+ }
+
+ /**
+ * Tells the manager what to display immediately
+ * @private
+ * @param {string} target Url or EpubCFI
+ * @return {Promise}
+ */
+ _display(target) {
+ if (!this.book) {
+ return;
+ }
+ var isCfiString = this.epubcfi.isCfiString(target);
+ var displaying = new defer();
+ var displayed = displaying.promise;
+ var section;
+ var moveTo;
+
+ this.displaying = displaying;
+
+ // Check if this is a book percentage
+ if (this.book.locations.length() && isFloat(target)) {
+ target = this.book.locations.cfiFromPercentage(parseFloat(target));
+ }
+
+ section = this.book.spine.get(target);
+
+ if (!section) {
+ displaying.reject(new Error("No Section Found"));
+ return displayed;
+ }
+
+ this.manager.display(section, target).then(
+ () => {
+ displaying.resolve(section);
+ this.displaying = undefined;
+
+ /**
+ * Emit that a section has been displayed
+ * @event displayed
+ * @param {Section} section
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.DISPLAYED, section);
+ this.reportLocation();
+ },
+ (err) => {
+ /**
+ * Emit that has been an error displaying
+ * @event displayError
+ * @param {Section} section
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.DISPLAY_ERROR, err);
+ }
+ );
+
+ return displayed;
+ }
+
+ /*
render(view, show) {
// view.onLayout = this.layout.format.bind(this.layout);
@@ -410,657 +421,693 @@ class Rendition {
}
*/
- /**
- * Report what section has been displayed
- * @private
- * @param {*} view
- */
- afterDisplayed(view){
-
- view.on(EVENTS.VIEWS.MARK_CLICKED, (cfiRange, data) => this.triggerMarkEvent(cfiRange, data, view.contents));
-
- this.hooks.render.trigger(view, this)
- .then(() => {
- if (view.contents) {
- this.hooks.content.trigger(view.contents, this).then(() => {
- /**
- * Emit that a section has been rendered
- * @event rendered
- * @param {Section} section
- * @param {View} view
- * @memberof Rendition
- */
- this.emit(EVENTS.RENDITION.RENDERED, view.section, view);
- });
- } else {
- this.emit(EVENTS.RENDITION.RENDERED, view.section, view);
- }
- });
-
- }
-
- /**
- * Report what has been removed
- * @private
- * @param {*} view
- */
- afterRemoved(view){
- this.hooks.unloaded.trigger(view, this).then(() => {
- /**
- * Emit that a section has been removed
- * @event removed
- * @param {Section} section
- * @param {View} view
- * @memberof Rendition
- */
- this.emit(EVENTS.RENDITION.REMOVED, view.section, view);
- });
- }
-
- /**
- * Report resize events and display the last seen location
- * @private
- */
- onResized(size, epubcfi){
-
- /**
- * Emit that the rendition has been resized
- * @event resized
- * @param {number} width
- * @param {height} height
- * @param {string} epubcfi (optional)
- * @memberof Rendition
- */
- this.emit(EVENTS.RENDITION.RESIZED, {
- width: size.width,
- height: size.height
- }, epubcfi);
-
- if (this.location && this.location.start) {
- this.display(epubcfi || this.location.start.cfi);
- }
-
- }
-
- /**
- * Report orientation events and display the last seen location
- * @private
- */
- onOrientationChange(orientation){
- /**
- * Emit that the rendition has been rotated
- * @event orientationchange
- * @param {string} orientation
- * @memberof Rendition
- */
- this.emit(EVENTS.RENDITION.ORIENTATION_CHANGE, orientation);
- }
-
- /**
- * Move the Rendition to a specific offset
- * Usually you would be better off calling display()
- * @param {object} offset
- */
- moveTo(offset){
- this.manager.moveTo(offset);
- }
-
- /**
- * Trigger a resize of the views
- * @param {number} [width]
- * @param {number} [height]
- * @param {string} [epubcfi] (optional)
- */
- resize(width, height, epubcfi){
- if (width) {
- this.settings.width = width;
- }
- if (height) {
- this.settings.height = height;
- }
- this.manager.resize(width, height, epubcfi);
- }
-
- /**
- * Clear all rendered views
- */
- clear(){
- this.manager.clear();
- }
-
- /**
- * Go to the next "page" in the rendition
- * @return {Promise}
- */
- next(){
- return this.q.enqueue(this.manager.next.bind(this.manager))
- .then(this.reportLocation.bind(this));
- }
-
- /**
- * Go to the previous "page" in the rendition
- * @return {Promise}
- */
- prev(){
- return this.q.enqueue(this.manager.prev.bind(this.manager))
- .then(this.reportLocation.bind(this));
- }
-
- //-- http://www.idpf.org/epub/301/spec/epub-publications.html#meta-properties-rendering
- /**
- * Determine the Layout properties from metadata and settings
- * @private
- * @param {object} metadata
- * @return {object} properties
- */
- determineLayoutProperties(metadata){
- var properties;
- var layout = this.settings.layout || metadata.layout || "reflowable";
- var spread = this.settings.spread || metadata.spread || "auto";
- var orientation = this.settings.orientation || metadata.orientation || "auto";
- var flow = this.settings.flow || metadata.flow || "auto";
- var viewport = metadata.viewport || "";
- var minSpreadWidth = this.settings.minSpreadWidth || metadata.minSpreadWidth || 800;
- var direction = this.settings.direction || metadata.direction || "ltr";
-
- if ((this.settings.width === 0 || this.settings.width > 0) &&
- (this.settings.height === 0 || this.settings.height > 0)) {
- // viewport = "width="+this.settings.width+", height="+this.settings.height+"";
- }
-
- properties = {
- layout : layout,
- spread : spread,
- orientation : orientation,
- flow : flow,
- viewport : viewport,
- minSpreadWidth : minSpreadWidth,
- direction: direction
- };
-
- return properties;
- }
-
- /**
- * Adjust the flow of the rendition to paginated or scrolled
- * (scrolled-continuous vs scrolled-doc are handled by different view managers)
- * @param {string} flow
- */
- flow(flow){
- var _flow = flow;
- if (flow === "scrolled" ||
- flow === "scrolled-doc" ||
- flow === "scrolled-continuous") {
- _flow = "scrolled";
- }
-
- if (flow === "auto" || flow === "paginated") {
- _flow = "paginated";
- }
-
- this.settings.flow = flow;
-
- if (this._layout) {
- this._layout.flow(_flow);
- }
-
- if (this.manager && this._layout) {
- this.manager.applyLayout(this._layout);
- }
-
- if (this.manager) {
- this.manager.updateFlow(_flow);
- }
-
- if (this.manager && this.manager.isRendered() && this.location) {
- this.manager.clear();
- this.display(this.location.start.cfi);
- }
- }
-
- /**
- * Adjust the layout of the rendition to reflowable or pre-paginated
- * @param {object} settings
- */
- layout(settings){
- if (settings) {
- this._layout = new Layout(settings);
- this._layout.spread(settings.spread, this.settings.minSpreadWidth);
-
- // this.mapping = new Mapping(this._layout.props);
-
- this._layout.on(EVENTS.LAYOUT.UPDATED, (props, changed) => {
- this.emit(EVENTS.RENDITION.LAYOUT, props, changed);
- })
- }
-
- if (this.manager && this._layout) {
- this.manager.applyLayout(this._layout);
- }
-
- return this._layout;
- }
-
- /**
- * Adjust if the rendition uses spreads
- * @param {string} spread none | auto (TODO: implement landscape, portrait, both)
- * @param {int} [min] min width to use spreads at
- */
- spread(spread, min){
-
- this.settings.spread = spread;
-
- if (min) {
- this.settings.minSpreadWidth = min;
- }
-
- if (this._layout) {
- this._layout.spread(spread, min);
- }
-
- if (this.manager && this.manager.isRendered()) {
- this.manager.updateLayout();
- }
- }
-
- /**
- * Adjust the direction of the rendition
- * @param {string} dir
- */
- direction(dir){
-
- this.settings.direction = dir || "ltr";
-
- if (this.manager) {
- this.manager.direction(this.settings.direction);
- }
-
- if (this.manager && this.manager.isRendered() && this.location) {
- this.manager.clear();
- this.display(this.location.start.cfi);
- }
- }
-
- /**
- * Report the current location
- * @fires relocated
- * @fires locationChanged
- */
- reportLocation(){
- return this.q.enqueue(function reportedLocation(){
- requestAnimationFrame(function reportedLocationAfterRAF() {
- var location = this.manager.currentLocation();
- if (location && location.then && typeof location.then === "function") {
- location.then(function(result) {
- let located = this.located(result);
-
- if (!located || !located.start || !located.end) {
- return;
- }
-
- this.location = located;
-
- this.emit(EVENTS.RENDITION.LOCATION_CHANGED, {
- index: this.location.start.index,
- href: this.location.start.href,
- start: this.location.start.cfi,
- end: this.location.end.cfi,
- percentage: this.location.start.percentage
- });
-
- this.emit(EVENTS.RENDITION.RELOCATED, this.location);
- }.bind(this));
- } else if (location) {
- let located = this.located(location);
-
- if (!located || !located.start || !located.end) {
- return;
- }
-
- this.location = located;
-
- /**
- * @event locationChanged
- * @deprecated
- * @type {object}
- * @property {number} index
- * @property {string} href
- * @property {EpubCFI} start
- * @property {EpubCFI} end
- * @property {number} percentage
- * @memberof Rendition
- */
- this.emit(EVENTS.RENDITION.LOCATION_CHANGED, {
- index: this.location.start.index,
- href: this.location.start.href,
- start: this.location.start.cfi,
- end: this.location.end.cfi,
- percentage: this.location.start.percentage
- });
-
- /**
- * @event relocated
- * @type {displayedLocation}
- * @memberof Rendition
- */
- this.emit(EVENTS.RENDITION.RELOCATED, this.location);
- }
- }.bind(this));
- }.bind(this));
- }
-
- /**
- * Get the Current Location object
- * @return {displayedLocation | promise} location (may be a promise)
- */
- currentLocation(){
- var location = this.manager.currentLocation();
- if (location && location.then && typeof location.then === "function") {
- location.then(function(result) {
- let located = this.located(result);
- return located;
- }.bind(this));
- } else if (location) {
- let located = this.located(location);
- return located;
- }
- }
-
- /**
- * Creates a Rendition#locationRange from location
- * passed by the Manager
- * @returns {displayedLocation}
- * @private
- */
- located(location){
- if (!location.length) {
- return {};
- }
- let start = location[0];
- let end = location[location.length-1];
-
- let located = {
- start: {
- index: start.index,
- href: start.href,
- cfi: start.mapping.start,
- displayed: {
- page: start.pages[0] || 1,
- total: start.totalPages
- }
- },
- end: {
- index: end.index,
- href: end.href,
- cfi: end.mapping.end,
- displayed: {
- page: end.pages[end.pages.length-1] || 1,
- total: end.totalPages
- }
- }
- };
-
- let locationStart = this.book.locations.locationFromCfi(start.mapping.start);
- let locationEnd = this.book.locations.locationFromCfi(end.mapping.end);
-
- if (locationStart != null) {
- located.start.location = locationStart;
- located.start.percentage = this.book.locations.percentageFromLocation(locationStart);
- }
- if (locationEnd != null) {
- located.end.location = locationEnd;
- located.end.percentage = this.book.locations.percentageFromLocation(locationEnd);
- }
-
- let pageStart = this.book.pageList.pageFromCfi(start.mapping.start);
- let pageEnd = this.book.pageList.pageFromCfi(end.mapping.end);
-
- if (pageStart != -1) {
- located.start.page = pageStart;
- }
- if (pageEnd != -1) {
- located.end.page = pageEnd;
- }
-
- if (end.index === this.book.spine.last().index &&
- located.end.displayed.page >= located.end.displayed.total) {
- located.atEnd = true;
- }
-
- if (start.index === this.book.spine.first().index &&
- located.start.displayed.page === 1) {
- located.atStart = true;
- }
-
- return located;
- }
-
- /**
- * Remove and Clean Up the Rendition
- */
- destroy(){
- // Clear the queue
- // this.q.clear();
- // this.q = undefined;
-
- this.manager && this.manager.destroy();
-
- this.book = undefined;
-
- // this.views = null;
-
- // this.hooks.display.clear();
- // this.hooks.serialize.clear();
- // this.hooks.content.clear();
- // this.hooks.layout.clear();
- // this.hooks.render.clear();
- // this.hooks.show.clear();
- // this.hooks = {};
-
- // this.themes.destroy();
- // this.themes = undefined;
-
- // this.epubcfi = undefined;
-
- // this.starting = undefined;
- // this.started = undefined;
-
-
- }
-
- /**
- * Pass the events from a view's Contents
- * @private
- * @param {Contents} view contents
- */
- passEvents(contents){
- DOM_EVENTS.forEach((e) => {
- contents.on(e, (ev) => this.triggerViewEvent(ev, contents));
- });
-
- contents.on(EVENTS.CONTENTS.SELECTED, (e) => this.triggerSelectedEvent(e, contents));
- }
-
- /**
- * Emit events passed by a view
- * @private
- * @param {event} e
- */
- triggerViewEvent(e, contents){
- this.emit(e.type, e, contents);
- }
-
- /**
- * Emit a selection event's CFI Range passed from a a view
- * @private
- * @param {string} cfirange
- */
- triggerSelectedEvent(cfirange, contents){
- /**
- * Emit that a text selection has occurred
- * @event selected
- * @param {string} cfirange
- * @param {Contents} contents
- * @memberof Rendition
- */
- this.emit(EVENTS.RENDITION.SELECTED, cfirange, contents);
- }
-
- /**
- * Emit a markClicked event with the cfiRange and data from a mark
- * @private
- * @param {EpubCFI} cfirange
- */
- triggerMarkEvent(cfiRange, data, contents){
- /**
- * Emit that a mark was clicked
- * @event markClicked
- * @param {EpubCFI} cfirange
- * @param {object} data
- * @param {Contents} contents
- * @memberof Rendition
- */
- this.emit(EVENTS.RENDITION.MARK_CLICKED, cfiRange, data, contents);
- }
-
- /**
- * Get a Range from a Visible CFI
- * @param {string} cfi EpubCfi String
- * @param {string} ignoreClass
- * @return {range}
- */
- getRange(cfi, ignoreClass){
- var _cfi = new EpubCFI(cfi);
- var found = this.manager.visible().filter(function (view) {
- if(_cfi.spinePos === view.index) return true;
- });
-
- // Should only every return 1 item
- if (found.length) {
- return found[0].contents.range(_cfi, ignoreClass);
- }
- }
-
- /**
- * Hook to adjust images to fit in columns
- * @param {Contents} contents
- * @private
- */
- adjustImages(contents) {
-
- if (this._layout.name === "pre-paginated") {
- return new Promise(function(resolve){
- resolve();
- });
- }
-
- let computed = contents.window.getComputedStyle(contents.content, null);
- let height = (contents.content.offsetHeight - (parseFloat(computed.paddingTop) + parseFloat(computed.paddingBottom))) * .95;
- let horizontalPadding = parseFloat(computed.paddingLeft) + parseFloat(computed.paddingRight);
-
- contents.addStylesheetRules({
- "img" : {
- "max-width": (this._layout.columnWidth ? (this._layout.columnWidth - horizontalPadding) + "px" : "100%") + "!important",
- "max-height": height + "px" + "!important",
- "object-fit": "contain",
- "page-break-inside": "avoid",
- "break-inside": "avoid",
- "box-sizing": "border-box"
- },
- "svg" : {
- "max-width": (this._layout.columnWidth ? (this._layout.columnWidth - horizontalPadding) + "px" : "100%") + "!important",
- "max-height": height + "px" + "!important",
- "page-break-inside": "avoid",
- "break-inside": "avoid"
- }
- });
-
- return new Promise(function(resolve, reject){
- // Wait to apply
- setTimeout(function() {
- resolve();
- }, 1);
- });
- }
-
- /**
- * Get the Contents object of each rendered view
- * @returns {Contents[]}
- */
- getContents () {
- return this.manager ? this.manager.getContents() : [];
- }
-
- /**
- * Get the views member from the manager
- * @returns {Views}
- */
- views () {
- let views = this.manager ? this.manager.views : undefined;
- return views || [];
- }
-
- /**
- * Hook to handle link clicks in rendered content
- * @param {Contents} contents
- * @private
- */
- handleLinks(contents) {
- if (contents) {
- contents.on(EVENTS.CONTENTS.LINK_CLICKED, (href) => {
- let relative = this.book.path.relative(href);
- this.display(relative);
- });
- }
- }
-
- /**
- * Hook to handle injecting stylesheet before
- * a Section is serialized
- * @param {document} doc
- * @param {Section} section
- * @private
- */
- injectStylesheet(doc, section) {
- let style = doc.createElement("link");
- style.setAttribute("type", "text/css");
- style.setAttribute("rel", "stylesheet");
- style.setAttribute("href", this.settings.stylesheet);
- doc.getElementsByTagName("head")[0].appendChild(style);
- }
-
- /**
- * Hook to handle injecting scripts before
- * a Section is serialized
- * @param {document} doc
- * @param {Section} section
- * @private
- */
- injectScript(doc, section) {
- let script = doc.createElement("script");
- script.setAttribute("type", "text/javascript");
- script.setAttribute("src", this.settings.script);
- script.textContent = " "; // Needed to prevent self closing tag
- doc.getElementsByTagName("head")[0].appendChild(script);
- }
-
- /**
- * Hook to handle the document identifier before
- * a Section is serialized
- * @param {document} doc
- * @param {Section} section
- * @private
- */
- injectIdentifier(doc, section) {
- let ident = this.book.packaging.metadata.identifier;
- let meta = doc.createElement("meta");
- meta.setAttribute("name", "dc.relation.ispartof");
- if (ident) {
- meta.setAttribute("content", ident);
- }
- doc.getElementsByTagName("head")[0].appendChild(meta);
- }
-
+ /**
+ * Report what section has been displayed
+ * @private
+ * @param {*} view
+ */
+ afterDisplayed(view) {
+ view.on(EVENTS.VIEWS.MARK_CLICKED, (cfiRange, data) =>
+ this.triggerMarkEvent(cfiRange, data, view.contents)
+ );
+
+ this.hooks.render.trigger(view, this).then(() => {
+ if (view.contents) {
+ this.hooks.content.trigger(view.contents, this).then(() => {
+ /**
+ * Emit that a section has been rendered
+ * @event rendered
+ * @param {Section} section
+ * @param {View} view
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.RENDERED, view.section, view);
+ });
+ } else {
+ this.emit(EVENTS.RENDITION.RENDERED, view.section, view);
+ }
+ });
+ }
+
+ /**
+ * Report what has been removed
+ * @private
+ * @param {*} view
+ */
+ afterRemoved(view) {
+ this.hooks.unloaded.trigger(view, this).then(() => {
+ /**
+ * Emit that a section has been removed
+ * @event removed
+ * @param {Section} section
+ * @param {View} view
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.REMOVED, view.section, view);
+ });
+ }
+
+ /**
+ * Report resize events and display the last seen location
+ * @private
+ */
+ onResized(size, epubcfi) {
+ /**
+ * Emit that the rendition has been resized
+ * @event resized
+ * @param {number} width
+ * @param {height} height
+ * @param {string} epubcfi (optional)
+ * @memberof Rendition
+ */
+ this.emit(
+ EVENTS.RENDITION.RESIZED,
+ {
+ width: size.width,
+ height: size.height,
+ },
+ epubcfi
+ );
+
+ if (this.location && this.location.start) {
+ this.display(epubcfi || this.location.start.cfi);
+ }
+ }
+
+ /**
+ * Report orientation events and display the last seen location
+ * @private
+ */
+ onOrientationChange(orientation) {
+ /**
+ * Emit that the rendition has been rotated
+ * @event orientationchange
+ * @param {string} orientation
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.ORIENTATION_CHANGE, orientation);
+ }
+
+ /**
+ * Move the Rendition to a specific offset
+ * Usually you would be better off calling display()
+ * @param {object} offset
+ */
+ moveTo(offset) {
+ this.manager.moveTo(offset);
+ }
+
+ /**
+ * Trigger a resize of the views
+ * @param {number} [width]
+ * @param {number} [height]
+ * @param {string} [epubcfi] (optional)
+ */
+ resize(width, height, epubcfi) {
+ if (width) {
+ this.settings.width = width;
+ }
+ if (height) {
+ this.settings.height = height;
+ }
+ this.manager.resize(width, height, epubcfi);
+ }
+
+ /**
+ * Clear all rendered views
+ */
+ clear() {
+ this.manager.clear();
+ }
+
+ /**
+ * Go to the next "page" in the rendition
+ * @return {Promise}
+ */
+ next() {
+ return this.q
+ .enqueue(this.manager.next.bind(this.manager))
+ .then(this.reportLocation.bind(this));
+ }
+
+ /**
+ * Go to the previous "page" in the rendition
+ * @return {Promise}
+ */
+ prev() {
+ return this.q
+ .enqueue(this.manager.prev.bind(this.manager))
+ .then(this.reportLocation.bind(this));
+ }
+
+ //-- http://www.idpf.org/epub/301/spec/epub-publications.html#meta-properties-rendering
+ /**
+ * Determine the Layout properties from metadata and settings
+ * @private
+ * @param {object} metadata
+ * @return {object} properties
+ */
+ determineLayoutProperties(metadata) {
+ var properties;
+ var layout = this.settings.layout || metadata.layout || "reflowable";
+ var spread = this.settings.spread || metadata.spread || "auto";
+ var orientation =
+ this.settings.orientation || metadata.orientation || "auto";
+ var flow = this.settings.flow || metadata.flow || "auto";
+ var viewport = metadata.viewport || "";
+ var minSpreadWidth =
+ this.settings.minSpreadWidth || metadata.minSpreadWidth || 800;
+ var direction = this.settings.direction || metadata.direction || "ltr";
+
+ if (
+ (this.settings.width === 0 || this.settings.width > 0) &&
+ (this.settings.height === 0 || this.settings.height > 0)
+ ) {
+ // viewport = "width="+this.settings.width+", height="+this.settings.height+"";
+ }
+
+ properties = {
+ layout: layout,
+ spread: spread,
+ orientation: orientation,
+ flow: flow,
+ viewport: viewport,
+ minSpreadWidth: minSpreadWidth,
+ direction: direction,
+ };
+
+ return properties;
+ }
+
+ /**
+ * Adjust the flow of the rendition to paginated or scrolled
+ * (scrolled-continuous vs scrolled-doc are handled by different view managers)
+ * @param {string} flow
+ */
+ flow(flow) {
+ var _flow = flow;
+ if (
+ flow === "scrolled" ||
+ flow === "scrolled-doc" ||
+ flow === "scrolled-continuous"
+ ) {
+ _flow = "scrolled";
+ }
+
+ if (flow === "auto" || flow === "paginated") {
+ _flow = "paginated";
+ }
+
+ this.settings.flow = flow;
+
+ if (this._layout) {
+ this._layout.flow(_flow);
+ }
+
+ if (this.manager && this._layout) {
+ this.manager.applyLayout(this._layout);
+ }
+
+ if (this.manager) {
+ this.manager.updateFlow(_flow);
+ }
+
+ if (this.manager && this.manager.isRendered() && this.location) {
+ this.manager.clear();
+ this.display(this.location.start.cfi);
+ }
+ }
+
+ /**
+ * Adjust the layout of the rendition to reflowable or pre-paginated
+ * @param {object} settings
+ */
+ layout(settings) {
+ if (settings) {
+ this._layout = new Layout(settings);
+ this._layout.spread(settings.spread, this.settings.minSpreadWidth);
+
+ // this.mapping = new Mapping(this._layout.props);
+
+ this._layout.on(EVENTS.LAYOUT.UPDATED, (props, changed) => {
+ this.emit(EVENTS.RENDITION.LAYOUT, props, changed);
+ });
+ }
+
+ if (this.manager && this._layout) {
+ this.manager.applyLayout(this._layout);
+ }
+
+ return this._layout;
+ }
+
+ /**
+ * Adjust if the rendition uses spreads
+ * @param {string} spread none | auto (TODO: implement landscape, portrait, both)
+ * @param {int} [min] min width to use spreads at
+ */
+ spread(spread, min) {
+ this.settings.spread = spread;
+
+ if (min) {
+ this.settings.minSpreadWidth = min;
+ }
+
+ if (this._layout) {
+ this._layout.spread(spread, min);
+ }
+
+ if (this.manager && this.manager.isRendered()) {
+ this.manager.updateLayout();
+ }
+ }
+
+ /**
+ * Adjust the direction of the rendition
+ * @param {string} dir
+ */
+ direction(dir) {
+ this.settings.direction = dir || "ltr";
+
+ if (this.manager) {
+ this.manager.direction(this.settings.direction);
+ }
+
+ if (this.manager && this.manager.isRendered() && this.location) {
+ this.manager.clear();
+ this.display(this.location.start.cfi);
+ }
+ }
+
+ /**
+ * Report the current location
+ * @fires relocated
+ * @fires locationChanged
+ */
+ reportLocation() {
+ return this.q.enqueue(
+ function reportedLocation() {
+ requestAnimationFrame(
+ function reportedLocationAfterRAF() {
+ var location = this.manager.currentLocation();
+ if (
+ location &&
+ location.then &&
+ typeof location.then === "function"
+ ) {
+ location.then(
+ function (result) {
+ let located = this.located(result);
+
+ if (!located || !located.start || !located.end) {
+ return;
+ }
+
+ this.location = located;
+
+ this.emit(EVENTS.RENDITION.LOCATION_CHANGED, {
+ index: this.location.start.index,
+ href: this.location.start.href,
+ start: this.location.start.cfi,
+ end: this.location.end.cfi,
+ percentage: this.location.start.percentage,
+ });
+
+ this.emit(EVENTS.RENDITION.RELOCATED, this.location);
+ }.bind(this)
+ );
+ } else if (location) {
+ let located = this.located(location);
+
+ if (!located || !located.start || !located.end) {
+ return;
+ }
+
+ this.location = located;
+
+ /**
+ * @event locationChanged
+ * @deprecated
+ * @type {object}
+ * @property {number} index
+ * @property {string} href
+ * @property {EpubCFI} start
+ * @property {EpubCFI} end
+ * @property {number} percentage
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.LOCATION_CHANGED, {
+ index: this.location.start.index,
+ href: this.location.start.href,
+ start: this.location.start.cfi,
+ end: this.location.end.cfi,
+ percentage: this.location.start.percentage,
+ });
+
+ /**
+ * @event relocated
+ * @type {displayedLocation}
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.RELOCATED, this.location);
+ }
+ }.bind(this)
+ );
+ }.bind(this)
+ );
+ }
+
+ /**
+ * Get the Current Location object
+ * @return {displayedLocation | promise} location (may be a promise)
+ */
+ currentLocation() {
+ var location = this.manager.currentLocation();
+ if (location && location.then && typeof location.then === "function") {
+ location.then(
+ function (result) {
+ let located = this.located(result);
+ return located;
+ }.bind(this)
+ );
+ } else if (location) {
+ let located = this.located(location);
+ return located;
+ }
+ }
+
+ /**
+ * Creates a Rendition#locationRange from location
+ * passed by the Manager
+ * @returns {displayedLocation}
+ * @private
+ */
+ located(location) {
+ if (!location.length) {
+ return {};
+ }
+ let start = location[0];
+ let end = location[location.length - 1];
+
+ let located = {
+ start: {
+ index: start.index,
+ href: start.href,
+ cfi: start.mapping.start,
+ displayed: {
+ page: start.pages[0] || 1,
+ total: start.totalPages,
+ },
+ },
+ end: {
+ index: end.index,
+ href: end.href,
+ cfi: end.mapping.end,
+ displayed: {
+ page: end.pages[end.pages.length - 1] || 1,
+ total: end.totalPages,
+ },
+ },
+ };
+
+ let locationStart = this.book.locations.locationFromCfi(
+ start.mapping.start
+ );
+ let locationEnd = this.book.locations.locationFromCfi(end.mapping.end);
+
+ if (locationStart != null) {
+ located.start.location = locationStart;
+ located.start.percentage =
+ this.book.locations.percentageFromLocation(locationStart);
+ }
+ if (locationEnd != null) {
+ located.end.location = locationEnd;
+ located.end.percentage =
+ this.book.locations.percentageFromLocation(locationEnd);
+ }
+
+ let pageStart = this.book.pageList.pageFromCfi(start.mapping.start);
+ let pageEnd = this.book.pageList.pageFromCfi(end.mapping.end);
+
+ if (pageStart != -1) {
+ located.start.page = pageStart;
+ }
+ if (pageEnd != -1) {
+ located.end.page = pageEnd;
+ }
+
+ if (
+ end.index === this.book.spine.last().index &&
+ located.end.displayed.page >= located.end.displayed.total
+ ) {
+ located.atEnd = true;
+ }
+
+ if (
+ start.index === this.book.spine.first().index &&
+ located.start.displayed.page === 1
+ ) {
+ located.atStart = true;
+ }
+
+ return located;
+ }
+
+ /**
+ * Remove and Clean Up the Rendition
+ */
+ destroy() {
+ // Clear the queue
+ // this.q.clear();
+ // this.q = undefined;
+
+ this.manager && this.manager.destroy();
+
+ this.book = undefined;
+
+ // this.views = null;
+
+ // this.hooks.display.clear();
+ // this.hooks.serialize.clear();
+ // this.hooks.content.clear();
+ // this.hooks.layout.clear();
+ // this.hooks.render.clear();
+ // this.hooks.show.clear();
+ // this.hooks = {};
+
+ // this.themes.destroy();
+ // this.themes = undefined;
+
+ // this.epubcfi = undefined;
+
+ // this.starting = undefined;
+ // this.started = undefined;
+ }
+
+ /**
+ * Pass the events from a view's Contents
+ * @private
+ * @param {Contents} view contents
+ */
+ passEvents(contents) {
+ DOM_EVENTS.forEach((e) => {
+ contents.on(e, (ev) => this.triggerViewEvent(ev, contents));
+ });
+
+ contents.on(EVENTS.CONTENTS.SELECTED, (e) =>
+ this.triggerSelectedEvent(e, contents)
+ );
+ }
+
+ /**
+ * Emit events passed by a view
+ * @private
+ * @param {event} e
+ */
+ triggerViewEvent(e, contents) {
+ this.emit(e.type, e, contents);
+ }
+
+ /**
+ * Emit a selection event's CFI Range passed from a a view
+ * @private
+ * @param {string} cfirange
+ */
+ triggerSelectedEvent(cfirange, contents) {
+ /**
+ * Emit that a text selection has occurred
+ * @event selected
+ * @param {string} cfirange
+ * @param {Contents} contents
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.SELECTED, cfirange, contents);
+ }
+
+ /**
+ * Emit a markClicked event with the cfiRange and data from a mark
+ * @private
+ * @param {EpubCFI} cfirange
+ */
+ triggerMarkEvent(cfiRange, data, contents) {
+ /**
+ * Emit that a mark was clicked
+ * @event markClicked
+ * @param {EpubCFI} cfirange
+ * @param {object} data
+ * @param {Contents} contents
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.MARK_CLICKED, cfiRange, data, contents);
+ }
+
+ /**
+ * Get a Range from a Visible CFI
+ * @param {string} cfi EpubCfi String
+ * @param {string} ignoreClass
+ * @return {range}
+ */
+ getRange(cfi, ignoreClass) {
+ var _cfi = new EpubCFI(cfi);
+ var found = this.manager.visible().filter(function (view) {
+ if (_cfi.spinePos === view.index) return true;
+ });
+
+ // Should only every return 1 item
+ if (found.length) {
+ return found[0].contents.range(_cfi, ignoreClass);
+ }
+ }
+
+ /**
+ * Hook to adjust images to fit in columns
+ * @param {Contents} contents
+ * @private
+ */
+ adjustImages(contents) {
+ if (this._layout.name === "pre-paginated") {
+ return new Promise(function (resolve) {
+ resolve();
+ });
+ }
+
+ let computed = contents.window.getComputedStyle(contents.content, null);
+ let height =
+ (contents.content.offsetHeight -
+ (parseFloat(computed.paddingTop) +
+ parseFloat(computed.paddingBottom))) *
+ 0.95;
+ let horizontalPadding =
+ parseFloat(computed.paddingLeft) + parseFloat(computed.paddingRight);
+
+ contents.addStylesheetRules({
+ img: {
+ "max-width":
+ (this._layout.columnWidth
+ ? this._layout.columnWidth - horizontalPadding + "px"
+ : "100%") + "!important",
+ "max-height": height + "px" + "!important",
+ "object-fit": "contain",
+ "page-break-inside": "avoid",
+ "break-inside": "avoid",
+ "box-sizing": "border-box",
+ },
+ svg: {
+ "max-width":
+ (this._layout.columnWidth
+ ? this._layout.columnWidth - horizontalPadding + "px"
+ : "100%") + "!important",
+ "max-height": height + "px" + "!important",
+ "page-break-inside": "avoid",
+ "break-inside": "avoid",
+ },
+ });
+
+ return new Promise(function (resolve, reject) {
+ // Wait to apply
+ setTimeout(function () {
+ resolve();
+ }, 1);
+ });
+ }
+
+ /**
+ * Get the Contents object of each rendered view
+ * @returns {Contents[]}
+ */
+ getContents() {
+ return this.manager ? this.manager.getContents() : [];
+ }
+
+ /**
+ * Get the views member from the manager
+ * @returns {Views}
+ */
+ views() {
+ let views = this.manager ? this.manager.views : undefined;
+ return views || [];
+ }
+
+ /**
+ * Hook to handle link clicks in rendered content
+ * @param {Contents} contents
+ * @private
+ */
+ handleLinks(contents) {
+ if (contents) {
+ contents.on(EVENTS.CONTENTS.LINK_CLICKED, (href) => {
+ let relative = this.book.path.relative(href);
+ this.display(relative);
+ });
+ }
+ }
+
+ /**
+ * Hook to handle injecting stylesheet before
+ * a Section is serialized
+ * @param {document} doc
+ * @param {Section} section
+ * @private
+ */
+ injectStylesheet(doc, section) {
+ let style = doc.createElement("link");
+ style.setAttribute("type", "text/css");
+ style.setAttribute("rel", "stylesheet");
+ style.setAttribute("href", this.settings.stylesheet);
+ doc.getElementsByTagName("head")[0].appendChild(style);
+ }
+
+ /**
+ * Hook to handle injecting scripts before
+ * a Section is serialized
+ * @param {document} doc
+ * @param {Section} section
+ * @private
+ */
+ injectScript(doc, section) {
+ let script = doc.createElement("script");
+ script.setAttribute("type", "text/javascript");
+ script.setAttribute("src", this.settings.script);
+ script.textContent = " "; // Needed to prevent self closing tag
+ doc.getElementsByTagName("head")[0].appendChild(script);
+ }
+
+ /**
+ * Hook to handle the document identifier before
+ * a Section is serialized
+ * @param {document} doc
+ * @param {Section} section
+ * @private
+ */
+ injectIdentifier(doc, section) {
+ let ident = this.book.packaging.metadata.identifier;
+ let meta = doc.createElement("meta");
+ meta.setAttribute("name", "dc.relation.ispartof");
+ if (ident) {
+ meta.setAttribute("content", ident);
+ }
+ doc.getElementsByTagName("head")[0].appendChild(meta);
+ }
}
//-- Enable binding events to Renderer
diff --git a/types/managers/view.d.ts b/types/managers/view.d.ts
index b8f297fd8..9fbf98e5e 100644
--- a/types/managers/view.d.ts
+++ b/types/managers/view.d.ts
@@ -11,7 +11,9 @@ export interface ViewSettings {
width?: number,
height?: number,
forceEvenPages?: boolean,
- allowScriptedContent?: boolean
+ allowScriptedContent?: boolean,
+ selectionStopDelay?:number
+
}
export default class View {
diff --git a/types/rendition.d.ts b/types/rendition.d.ts
index 489f3faeb..db5ee1b89 100644
--- a/types/rendition.d.ts
+++ b/types/rendition.d.ts
@@ -26,6 +26,7 @@ export interface RenditionOptions {
snap?: boolean | object,
defaultDirection?: string,
allowScriptedContent?: boolean
+ selectionStopDelay?:number
}
export interface DisplayedLocation {