import EventEmitter from "event-emitter"; 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 }, 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"; 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); 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); export default IframeView;