summaryrefslogtreecommitdiff
path: root/lib/epub.js/src/contents.js
diff options
context:
space:
mode:
Diffstat (limited to 'lib/epub.js/src/contents.js')
-rw-r--r--lib/epub.js/src/contents.js1264
1 files changed, 1264 insertions, 0 deletions
diff --git a/lib/epub.js/src/contents.js b/lib/epub.js/src/contents.js
new file mode 100644
index 0000000..3effe72
--- /dev/null
+++ b/lib/epub.js/src/contents.js
@@ -0,0 +1,1264 @@
+import EventEmitter from "event-emitter";
+import {isNumber, prefixed, borders, defaults} from "./utils/core";
+import EpubCFI from "./epubcfi";
+import Mapping from "./mapping";
+import {replaceLinks} from "./utils/replacements";
+import { EPUBJS_VERSION, EVENTS, DOM_EVENTS } from "./utils/constants";
+
+const hasNavigator = typeof (navigator) !== "undefined";
+
+const isChrome = hasNavigator && /Chrome/.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
+ */
+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
+ * <meta name="viewport" content="width=1024,height=697" />
+ */
+ 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();
+
+ }
+}
+
+EventEmitter(Contents.prototype);
+
+export default Contents;