diff options
Diffstat (limited to 'lib/epub.js/src/contents.js')
-rw-r--r-- | lib/epub.js/src/contents.js | 1264 |
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; |