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 * */ 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;