diff options
Diffstat (limited to 'lib/epub.js/src/managers')
-rw-r--r-- | lib/epub.js/src/managers/continuous/index.js | 588 | ||||
-rw-r--r-- | lib/epub.js/src/managers/default/index.js | 1073 | ||||
-rw-r--r-- | lib/epub.js/src/managers/helpers/snap.js | 338 | ||||
-rw-r--r-- | lib/epub.js/src/managers/helpers/stage.js | 363 | ||||
-rw-r--r-- | lib/epub.js/src/managers/helpers/views.js | 167 | ||||
-rw-r--r-- | lib/epub.js/src/managers/views/iframe.js | 835 | ||||
-rw-r--r-- | lib/epub.js/src/managers/views/inline.js | 432 |
7 files changed, 3796 insertions, 0 deletions
diff --git a/lib/epub.js/src/managers/continuous/index.js b/lib/epub.js/src/managers/continuous/index.js new file mode 100644 index 0000000..e6a9e61 --- /dev/null +++ b/lib/epub.js/src/managers/continuous/index.js @@ -0,0 +1,588 @@ +import {extend, defer, requestAnimationFrame} from "../../utils/core"; +import DefaultViewManager from "../default"; +import Snap from "../helpers/snap"; +import { EVENTS } from "../../utils/constants"; +import debounce from "lodash/debounce"; + +class ContinuousViewManager extends DefaultViewManager { + constructor(options) { + super(options); + + this.name = "continuous"; + + this.settings = extend(this.settings || {}, { + infinite: true, + overflow: undefined, + axis: undefined, + writingMode: undefined, + flow: "scrolled", + offset: 500, + offsetDelta: 250, + width: undefined, + height: undefined, + snap: false, + afterScrolledTimeout: 10 + }); + + extend(this.settings, options.settings || {}); + + // Gap can be 0, but defaults doesn't handle that + if (options.settings.gap != "undefined" && options.settings.gap === 0) { + this.settings.gap = options.settings.gap; + } + + this.viewSettings = { + ignoreClass: this.settings.ignoreClass, + axis: this.settings.axis, + flow: this.settings.flow, + layout: this.layout, + width: 0, + height: 0, + forceEvenPages: false + }; + + this.scrollTop = 0; + this.scrollLeft = 0; + } + + display(section, target){ + return DefaultViewManager.prototype.display.call(this, section, target) + .then(function () { + return this.fill(); + }.bind(this)); + } + + fill(_full){ + var full = _full || new defer(); + + this.q.enqueue(() => { + return this.check(); + }).then((result) => { + if (result) { + this.fill(full); + } else { + full.resolve(); + } + }); + + return full.promise; + } + + moveTo(offset){ + // var bounds = this.stage.bounds(); + // var dist = Math.floor(offset.top / bounds.height) * bounds.height; + var distX = 0, + distY = 0; + + var offsetX = 0, + offsetY = 0; + + if(!this.isPaginated) { + distY = offset.top; + offsetY = offset.top+this.settings.offsetDelta; + } else { + distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta; + offsetX = distX+this.settings.offsetDelta; + } + + if (distX > 0 || distY > 0) { + this.scrollBy(distX, distY, true); + } + } + + afterResized(view){ + this.emit(EVENTS.MANAGERS.RESIZE, view.section); + } + + // Remove Previous Listeners if present + removeShownListeners(view){ + + // view.off("shown", this.afterDisplayed); + // view.off("shown", this.afterDisplayedAbove); + view.onDisplayed = function(){}; + + } + + add(section){ + var view = this.createView(section); + + this.views.append(view); + + view.on(EVENTS.VIEWS.RESIZED, (bounds) => { + view.expanded = true; + }); + + view.on(EVENTS.VIEWS.AXIS, (axis) => { + this.updateAxis(axis); + }); + + view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { + this.updateWritingMode(mode); + }); + + // view.on(EVENTS.VIEWS.SHOWN, this.afterDisplayed.bind(this)); + view.onDisplayed = this.afterDisplayed.bind(this); + view.onResize = this.afterResized.bind(this); + + return view.display(this.request); + } + + append(section){ + var view = this.createView(section); + + view.on(EVENTS.VIEWS.RESIZED, (bounds) => { + view.expanded = true; + }); + + view.on(EVENTS.VIEWS.AXIS, (axis) => { + this.updateAxis(axis); + }); + + view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { + this.updateWritingMode(mode); + }); + + this.views.append(view); + + view.onDisplayed = this.afterDisplayed.bind(this); + + return view; + } + + prepend(section){ + var view = this.createView(section); + + view.on(EVENTS.VIEWS.RESIZED, (bounds) => { + this.counter(bounds); + view.expanded = true; + }); + + view.on(EVENTS.VIEWS.AXIS, (axis) => { + this.updateAxis(axis); + }); + + view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { + this.updateWritingMode(mode); + }); + + this.views.prepend(view); + + view.onDisplayed = this.afterDisplayed.bind(this); + + return view; + } + + counter(bounds){ + if(this.settings.axis === "vertical") { + this.scrollBy(0, bounds.heightDelta, true); + } else { + this.scrollBy(bounds.widthDelta, 0, true); + } + } + + update(_offset){ + var container = this.bounds(); + var views = this.views.all(); + var viewsLength = views.length; + var visible = []; + var offset = typeof _offset != "undefined" ? _offset : (this.settings.offset || 0); + var isVisible; + var view; + + var updating = new defer(); + var promises = []; + for (var i = 0; i < viewsLength; i++) { + view = views[i]; + + isVisible = this.isVisible(view, offset, offset, container); + + if(isVisible === true) { + // console.log("visible " + view.index, view.displayed); + + if (!view.displayed) { + let displayed = view.display(this.request) + .then(function (view) { + view.show(); + }, (err) => { + view.hide(); + }); + promises.push(displayed); + } else { + view.show(); + } + visible.push(view); + } else { + this.q.enqueue(view.destroy.bind(view)); + // console.log("hidden " + view.index, view.displayed); + + clearTimeout(this.trimTimeout); + this.trimTimeout = setTimeout(function(){ + this.q.enqueue(this.trim.bind(this)); + }.bind(this), 250); + } + + } + + if(promises.length){ + return Promise.all(promises) + .catch((err) => { + updating.reject(err); + }); + } else { + updating.resolve(); + return updating.promise; + } + + } + + check(_offsetLeft, _offsetTop){ + var checking = new defer(); + var newViews = []; + + var horizontal = (this.settings.axis === "horizontal"); + var delta = this.settings.offset || 0; + + if (_offsetLeft && horizontal) { + delta = _offsetLeft; + } + + if (_offsetTop && !horizontal) { + delta = _offsetTop; + } + + var bounds = this._bounds; // bounds saved this until resize + + let offset = horizontal ? this.scrollLeft : this.scrollTop; + let visibleLength = horizontal ? Math.floor(bounds.width) : bounds.height; + let contentLength = horizontal ? this.container.scrollWidth : this.container.scrollHeight; + let writingMode = (this.writingMode && this.writingMode.indexOf("vertical") === 0) ? "vertical" : "horizontal"; + let rtlScrollType = this.settings.rtlScrollType; + let rtl = this.settings.direction === "rtl"; + + if (!this.settings.fullsize) { + // Scroll offset starts at width of element + if (rtl && rtlScrollType === "default" && writingMode === "horizontal") { + offset = contentLength - visibleLength - offset; + } + // Scroll offset starts at 0 and goes negative + if (rtl && rtlScrollType === "negative" && writingMode === "horizontal") { + offset = offset * -1; + } + } else { + // Scroll offset starts at 0 and goes negative + if ((horizontal && rtl && rtlScrollType === "negative") || + (!horizontal && rtl && rtlScrollType === "default")) { + offset = offset * -1; + } + } + + let prepend = () => { + let first = this.views.first(); + let prev = first && first.section.prev(); + + if(prev) { + newViews.push(this.prepend(prev)); + } + }; + + let append = () => { + let last = this.views.last(); + let next = last && last.section.next(); + + if(next) { + newViews.push(this.append(next)); + } + + }; + + let end = offset + visibleLength + delta; + let start = offset - delta; + + if (end >= contentLength) { + append(); + } + + if (start < 0) { + prepend(); + } + + + let promises = newViews.map((view) => { + return view.display(this.request); + }); + + if(newViews.length){ + return Promise.all(promises) + .then(() => { + return this.check(); + }) + .then(() => { + // Check to see if anything new is on screen after rendering + return this.update(delta); + }, (err) => { + return err; + }); + } else { + this.q.enqueue(function(){ + this.update(); + }.bind(this)); + checking.resolve(false); + return checking.promise; + } + + + } + + trim(){ + var task = new defer(); + var displayed = this.views.displayed(); + var first = displayed[0]; + var last = displayed[displayed.length-1]; + var firstIndex = this.views.indexOf(first); + var lastIndex = this.views.indexOf(last); + var above = this.views.slice(0, firstIndex); + var below = this.views.slice(lastIndex+1); + + // Erase all but last above + for (var i = 0; i < above.length-1; i++) { + this.erase(above[i], above); + } + + // Erase all except first below + for (var j = 1; j < below.length; j++) { + this.erase(below[j]); + } + + task.resolve(); + return task.promise; + } + + erase(view, above){ //Trim + + var prevTop; + var prevLeft; + + if(!this.settings.fullsize) { + prevTop = this.container.scrollTop; + prevLeft = this.container.scrollLeft; + } else { + prevTop = window.scrollY; + prevLeft = window.scrollX; + } + + var bounds = view.bounds(); + + this.views.remove(view); + + if(above) { + if (this.settings.axis === "vertical") { + this.scrollTo(0, prevTop - bounds.height, true); + } else { + if(this.settings.direction === 'rtl') { + if (!this.settings.fullsize) { + this.scrollTo(prevLeft, 0, true); + } else { + this.scrollTo(prevLeft + Math.floor(bounds.width), 0, true); + } + } else { + this.scrollTo(prevLeft - Math.floor(bounds.width), 0, true); + } + } + } + + } + + addEventListeners(stage){ + + window.addEventListener("unload", function(e){ + this.ignore = true; + // this.scrollTo(0,0); + this.destroy(); + }.bind(this)); + + this.addScrollListeners(); + + if (this.isPaginated && this.settings.snap) { + this.snapper = new Snap(this, this.settings.snap && (typeof this.settings.snap === "object") && this.settings.snap); + } + } + + addScrollListeners() { + var scroller; + + this.tick = requestAnimationFrame; + + let dir = this.settings.direction === "rtl" && this.settings.rtlScrollType === "default" ? -1 : 1; + + this.scrollDeltaVert = 0; + this.scrollDeltaHorz = 0; + + if(!this.settings.fullsize) { + scroller = this.container; + this.scrollTop = this.container.scrollTop; + this.scrollLeft = this.container.scrollLeft; + } else { + scroller = window; + this.scrollTop = window.scrollY * dir; + this.scrollLeft = window.scrollX * dir; + } + + this._onScroll = this.onScroll.bind(this); + scroller.addEventListener("scroll", this._onScroll); + this._scrolled = debounce(this.scrolled.bind(this), 30); + // this.tick.call(window, this.onScroll.bind(this)); + + this.didScroll = false; + + } + + removeEventListeners(){ + var scroller; + + if(!this.settings.fullsize) { + scroller = this.container; + } else { + scroller = window; + } + + scroller.removeEventListener("scroll", this._onScroll); + this._onScroll = undefined; + } + + onScroll(){ + let scrollTop; + let scrollLeft; + let dir = this.settings.direction === "rtl" && this.settings.rtlScrollType === "default" ? -1 : 1; + + if(!this.settings.fullsize) { + scrollTop = this.container.scrollTop; + scrollLeft = this.container.scrollLeft; + } else { + scrollTop = window.scrollY * dir; + scrollLeft = window.scrollX * dir; + } + + this.scrollTop = scrollTop; + this.scrollLeft = scrollLeft; + + if(!this.ignore) { + + this._scrolled(); + + } else { + this.ignore = false; + } + + this.scrollDeltaVert += Math.abs(scrollTop-this.prevScrollTop); + this.scrollDeltaHorz += Math.abs(scrollLeft-this.prevScrollLeft); + + this.prevScrollTop = scrollTop; + this.prevScrollLeft = scrollLeft; + + clearTimeout(this.scrollTimeout); + this.scrollTimeout = setTimeout(function(){ + this.scrollDeltaVert = 0; + this.scrollDeltaHorz = 0; + }.bind(this), 150); + + clearTimeout(this.afterScrolled); + + this.didScroll = false; + + } + + scrolled() { + + this.q.enqueue(function() { + return this.check(); + }.bind(this)); + + this.emit(EVENTS.MANAGERS.SCROLL, { + top: this.scrollTop, + left: this.scrollLeft + }); + + clearTimeout(this.afterScrolled); + this.afterScrolled = setTimeout(function () { + + // Don't report scroll if we are about the snap + if (this.snapper && this.snapper.supportsTouch && this.snapper.needsSnap()) { + return; + } + + this.emit(EVENTS.MANAGERS.SCROLLED, { + top: this.scrollTop, + left: this.scrollLeft + }); + + }.bind(this), this.settings.afterScrolledTimeout); + } + + next(){ + + let delta = this.layout.props.name === "pre-paginated" && + this.layout.props.spread ? this.layout.props.delta * 2 : this.layout.props.delta; + + if(!this.views.length) return; + + if(this.isPaginated && this.settings.axis === "horizontal") { + + this.scrollBy(delta, 0, true); + + } else { + + this.scrollBy(0, this.layout.height, true); + + } + + this.q.enqueue(function() { + return this.check(); + }.bind(this)); + } + + prev(){ + + let delta = this.layout.props.name === "pre-paginated" && + this.layout.props.spread ? this.layout.props.delta * 2 : this.layout.props.delta; + + if(!this.views.length) return; + + if(this.isPaginated && this.settings.axis === "horizontal") { + + this.scrollBy(-delta, 0, true); + + } else { + + this.scrollBy(0, -this.layout.height, true); + + } + + this.q.enqueue(function() { + return this.check(); + }.bind(this)); + } + + updateFlow(flow){ + if (this.rendered && this.snapper) { + this.snapper.destroy(); + this.snapper = undefined; + } + + super.updateFlow(flow, "scroll"); + + if (this.rendered && this.isPaginated && this.settings.snap) { + this.snapper = new Snap(this, this.settings.snap && (typeof this.settings.snap === "object") && this.settings.snap); + } + } + + destroy(){ + super.destroy(); + + if (this.snapper) { + this.snapper.destroy(); + } + } + +} + +export default ContinuousViewManager; diff --git a/lib/epub.js/src/managers/default/index.js b/lib/epub.js/src/managers/default/index.js new file mode 100644 index 0000000..b24a4f6 --- /dev/null +++ b/lib/epub.js/src/managers/default/index.js @@ -0,0 +1,1073 @@ +import EventEmitter from "event-emitter"; +import {extend, defer, windowBounds, isNumber} from "../../utils/core"; +import scrollType from "../../utils/scrolltype"; +import Mapping from "../../mapping"; +import Queue from "../../utils/queue"; +import Stage from "../helpers/stage"; +import Views from "../helpers/views"; +import { EVENTS } from "../../utils/constants"; + +class DefaultViewManager { + constructor(options) { + + this.name = "default"; + this.optsSettings = options.settings; + this.View = options.view; + this.request = options.request; + this.renditionQueue = options.queue; + this.q = new Queue(this); + + this.settings = extend(this.settings || {}, { + infinite: true, + hidden: false, + width: undefined, + height: undefined, + axis: undefined, + writingMode: undefined, + flow: "scrolled", + ignoreClass: "", + fullsize: undefined + }); + + extend(this.settings, options.settings || {}); + + this.viewSettings = { + ignoreClass: this.settings.ignoreClass, + axis: this.settings.axis, + flow: this.settings.flow, + layout: this.layout, + method: this.settings.method, // srcdoc, blobUrl, write + width: 0, + height: 0, + forceEvenPages: true + }; + + this.rendered = false; + + } + + render(element, size){ + let tag = element.tagName; + + if (typeof this.settings.fullsize === "undefined" && + tag && (tag.toLowerCase() == "body" || + tag.toLowerCase() == "html")) { + this.settings.fullsize = true; + } + + if (this.settings.fullsize) { + this.settings.overflow = "visible"; + this.overflow = this.settings.overflow; + } + + this.settings.size = size; + + this.settings.rtlScrollType = scrollType(); + + // Save the stage + this.stage = new Stage({ + width: size.width, + height: size.height, + overflow: this.overflow, + hidden: this.settings.hidden, + axis: this.settings.axis, + fullsize: this.settings.fullsize, + direction: this.settings.direction + }); + + this.stage.attachTo(element); + + // Get this stage container div + this.container = this.stage.getContainer(); + + // Views array methods + this.views = new Views(this.container); + + // Calculate Stage Size + this._bounds = this.bounds(); + this._stageSize = this.stage.size(); + + // Set the dimensions for views + this.viewSettings.width = this._stageSize.width; + this.viewSettings.height = this._stageSize.height; + + // Function to handle a resize event. + // Will only attach if width and height are both fixed. + this.stage.onResize(this.onResized.bind(this)); + + this.stage.onOrientationChange(this.onOrientationChange.bind(this)); + + // Add Event Listeners + this.addEventListeners(); + + // Add Layout method + // this.applyLayoutMethod(); + if (this.layout) { + this.updateLayout(); + } + + this.rendered = true; + + } + + addEventListeners(){ + var scroller; + + window.addEventListener("unload", function(e){ + this.destroy(); + }.bind(this)); + + if(!this.settings.fullsize) { + scroller = this.container; + } else { + scroller = window; + } + + this._onScroll = this.onScroll.bind(this); + scroller.addEventListener("scroll", this._onScroll); + } + + removeEventListeners(){ + var scroller; + + if(!this.settings.fullsize) { + scroller = this.container; + } else { + scroller = window; + } + + scroller.removeEventListener("scroll", this._onScroll); + this._onScroll = undefined; + } + + destroy(){ + clearTimeout(this.orientationTimeout); + clearTimeout(this.resizeTimeout); + clearTimeout(this.afterScrolled); + + this.clear(); + + this.removeEventListeners(); + + this.stage.destroy(); + + this.rendered = false; + + /* + + clearTimeout(this.trimTimeout); + if(this.settings.hidden) { + this.element.removeChild(this.wrapper); + } else { + this.element.removeChild(this.container); + } + */ + } + + onOrientationChange(e) { + let {orientation} = window; + + if(this.optsSettings.resizeOnOrientationChange) { + this.resize(); + } + + // Per ampproject: + // In IOS 10.3, the measured size of an element is incorrect if the + // element size depends on window size directly and the measurement + // happens in window.resize event. Adding a timeout for correct + // measurement. See https://github.com/ampproject/amphtml/issues/8479 + clearTimeout(this.orientationTimeout); + this.orientationTimeout = setTimeout(function(){ + this.orientationTimeout = undefined; + + if(this.optsSettings.resizeOnOrientationChange) { + this.resize(); + } + + this.emit(EVENTS.MANAGERS.ORIENTATION_CHANGE, orientation); + }.bind(this), 500); + + } + + onResized(e) { + this.resize(); + } + + resize(width, height, epubcfi){ + let stageSize = this.stage.size(width, height); + + // For Safari, wait for orientation to catch up + // if the window is a square + this.winBounds = windowBounds(); + if (this.orientationTimeout && + this.winBounds.width === this.winBounds.height) { + // reset the stage size for next resize + this._stageSize = undefined; + return; + } + + if (this._stageSize && + this._stageSize.width === stageSize.width && + this._stageSize.height === stageSize.height ) { + // Size is the same, no need to resize + return; + } + + this._stageSize = stageSize; + + this._bounds = this.bounds(); + + // Clear current views + this.clear(); + + // Update for new views + this.viewSettings.width = this._stageSize.width; + this.viewSettings.height = this._stageSize.height; + + this.updateLayout(); + + this.emit(EVENTS.MANAGERS.RESIZED, { + width: this._stageSize.width, + height: this._stageSize.height + }, epubcfi); + } + + createView(section, forceRight) { + return new this.View(section, extend(this.viewSettings, { forceRight }) ); + } + + handleNextPrePaginated(forceRight, section, action) { + let next; + + if (this.layout.name === "pre-paginated" && this.layout.divisor > 1) { + if (forceRight || section.index === 0) { + // First page (cover) should stand alone for pre-paginated books + return; + } + next = section.next(); + if (next && !next.properties.includes("page-spread-left")) { + return action.call(this, next); + } + } + } + + display(section, target){ + + var displaying = new defer(); + var displayed = displaying.promise; + + // Check if moving to target is needed + if (target === section.href || isNumber(target)) { + target = undefined; + } + + // Check to make sure the section we want isn't already shown + var visible = this.views.find(section); + + // View is already shown, just move to correct location in view + if(visible && section && this.layout.name !== "pre-paginated") { + let offset = visible.offset(); + + if (this.settings.direction === "ltr") { + this.scrollTo(offset.left, offset.top, true); + } else { + let width = visible.width(); + this.scrollTo(offset.left + width, offset.top, true); + } + + if(target) { + let offset = visible.locationOf(target); + let width = visible.width(); + this.moveTo(offset, width); + } + + displaying.resolve(); + return displayed; + } + + // Hide all current views + this.clear(); + + let forceRight = false; + if (this.layout.name === "pre-paginated" && this.layout.divisor === 2 && section.properties.includes("page-spread-right")) { + forceRight = true; + } + + this.add(section, forceRight) + .then(function(view){ + + // Move to correct place within the section, if needed + if(target) { + let offset = view.locationOf(target); + let width = view.width(); + this.moveTo(offset, width); + } + + }.bind(this), (err) => { + displaying.reject(err); + }) + .then(function(){ + return this.handleNextPrePaginated(forceRight, section, this.add); + }.bind(this)) + .then(function(){ + + this.views.show(); + + displaying.resolve(); + + }.bind(this)); + // .then(function(){ + // return this.hooks.display.trigger(view); + // }.bind(this)) + // .then(function(){ + // this.views.show(); + // }.bind(this)); + return displayed; + } + + afterDisplayed(view){ + this.emit(EVENTS.MANAGERS.ADDED, view); + } + + afterResized(view){ + this.emit(EVENTS.MANAGERS.RESIZE, view.section); + } + + moveTo(offset, width){ + var distX = 0, + distY = 0; + + if(!this.isPaginated) { + distY = offset.top; + } else { + distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta; + + if (distX + this.layout.delta > this.container.scrollWidth) { + distX = this.container.scrollWidth - this.layout.delta; + } + + distY = Math.floor(offset.top / this.layout.delta) * this.layout.delta; + + if (distY + this.layout.delta > this.container.scrollHeight) { + distY = this.container.scrollHeight - this.layout.delta; + } + } + if(this.settings.direction === 'rtl'){ + /*** + the `floor` function above (L343) is on positive values, so we should add one `layout.delta` + to distX or use `Math.ceil` function, or multiply offset.left by -1 + before `Math.floor` + */ + distX = distX + this.layout.delta + distX = distX - width + } + this.scrollTo(distX, distY, true); + } + + add(section, forceRight){ + var view = this.createView(section, forceRight); + + this.views.append(view); + + // view.on(EVENTS.VIEWS.SHOWN, this.afterDisplayed.bind(this)); + view.onDisplayed = this.afterDisplayed.bind(this); + view.onResize = this.afterResized.bind(this); + + view.on(EVENTS.VIEWS.AXIS, (axis) => { + this.updateAxis(axis); + }); + + view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { + this.updateWritingMode(mode); + }); + + return view.display(this.request); + } + + append(section, forceRight){ + var view = this.createView(section, forceRight); + this.views.append(view); + + view.onDisplayed = this.afterDisplayed.bind(this); + view.onResize = this.afterResized.bind(this); + + view.on(EVENTS.VIEWS.AXIS, (axis) => { + this.updateAxis(axis); + }); + + view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { + this.updateWritingMode(mode); + }); + + return view.display(this.request); + } + + prepend(section, forceRight){ + var view = this.createView(section, forceRight); + + view.on(EVENTS.VIEWS.RESIZED, (bounds) => { + this.counter(bounds); + }); + + this.views.prepend(view); + + view.onDisplayed = this.afterDisplayed.bind(this); + view.onResize = this.afterResized.bind(this); + + view.on(EVENTS.VIEWS.AXIS, (axis) => { + this.updateAxis(axis); + }); + + view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => { + this.updateWritingMode(mode); + }); + + return view.display(this.request); + } + + counter(bounds){ + if(this.settings.axis === "vertical") { + this.scrollBy(0, bounds.heightDelta, true); + } else { + this.scrollBy(bounds.widthDelta, 0, true); + } + + } + + // resizeView(view) { + // + // if(this.settings.globalLayoutProperties.layout === "pre-paginated") { + // view.lock("both", this.bounds.width, this.bounds.height); + // } else { + // view.lock("width", this.bounds.width, this.bounds.height); + // } + // + // }; + + next(){ + var next; + var left; + + let dir = this.settings.direction; + + if(!this.views.length) return; + + if(this.isPaginated && this.settings.axis === "horizontal" && (!dir || dir === "ltr")) { + + this.scrollLeft = this.container.scrollLeft; + + left = this.container.scrollLeft + this.container.offsetWidth + this.layout.delta; + + if(left <= this.container.scrollWidth) { + this.scrollBy(this.layout.delta, 0, true); + } else { + next = this.views.last().section.next(); + } + } else if (this.isPaginated && this.settings.axis === "horizontal" && dir === "rtl") { + + this.scrollLeft = this.container.scrollLeft; + + if (this.settings.rtlScrollType === "default"){ + left = this.container.scrollLeft; + + if (left > 0) { + this.scrollBy(this.layout.delta, 0, true); + } else { + next = this.views.last().section.next(); + } + } else { + left = this.container.scrollLeft + ( this.layout.delta * -1 ); + + if (left > this.container.scrollWidth * -1){ + this.scrollBy(this.layout.delta, 0, true); + } else { + next = this.views.last().section.next(); + } + } + + } else if (this.isPaginated && this.settings.axis === "vertical") { + + this.scrollTop = this.container.scrollTop; + + let top = this.container.scrollTop + this.container.offsetHeight; + + if(top < this.container.scrollHeight) { + this.scrollBy(0, this.layout.height, true); + } else { + next = this.views.last().section.next(); + } + + } else { + next = this.views.last().section.next(); + } + + if(next) { + this.clear(); + // The new section may have a different writing-mode from the old section. Thus, we need to update layout. + this.updateLayout(); + + let forceRight = false; + if (this.layout.name === "pre-paginated" && this.layout.divisor === 2 && next.properties.includes("page-spread-right")) { + forceRight = true; + } + + return this.append(next, forceRight) + .then(function(){ + return this.handleNextPrePaginated(forceRight, next, this.append); + }.bind(this), (err) => { + return err; + }) + .then(function(){ + + // Reset position to start for scrolled-doc vertical-rl in default mode + if (!this.isPaginated && + this.settings.axis === "horizontal" && + this.settings.direction === "rtl" && + this.settings.rtlScrollType === "default") { + + this.scrollTo(this.container.scrollWidth, 0, true); + } + this.views.show(); + }.bind(this)); + } + + + } + + prev(){ + var prev; + var left; + let dir = this.settings.direction; + + if(!this.views.length) return; + + if(this.isPaginated && this.settings.axis === "horizontal" && (!dir || dir === "ltr")) { + + this.scrollLeft = this.container.scrollLeft; + + left = this.container.scrollLeft; + + if(left > 0) { + this.scrollBy(-this.layout.delta, 0, true); + } else { + prev = this.views.first().section.prev(); + } + + } else if (this.isPaginated && this.settings.axis === "horizontal" && dir === "rtl") { + + this.scrollLeft = this.container.scrollLeft; + + if (this.settings.rtlScrollType === "default"){ + left = this.container.scrollLeft + this.container.offsetWidth; + + if (left < this.container.scrollWidth) { + this.scrollBy(-this.layout.delta, 0, true); + } else { + prev = this.views.first().section.prev(); + } + } + else{ + left = this.container.scrollLeft; + + if (left < 0) { + this.scrollBy(-this.layout.delta, 0, true); + } else { + prev = this.views.first().section.prev(); + } + } + + } else if (this.isPaginated && this.settings.axis === "vertical") { + + this.scrollTop = this.container.scrollTop; + + let top = this.container.scrollTop; + + if(top > 0) { + this.scrollBy(0, -(this.layout.height), true); + } else { + prev = this.views.first().section.prev(); + } + + } else { + + prev = this.views.first().section.prev(); + + } + + if(prev) { + this.clear(); + // The new section may have a different writing-mode from the old section. Thus, we need to update layout. + this.updateLayout(); + + let forceRight = false; + if (this.layout.name === "pre-paginated" && this.layout.divisor === 2 && typeof prev.prev() !== "object") { + forceRight = true; + } + + return this.prepend(prev, forceRight) + .then(function(){ + var left; + if (this.layout.name === "pre-paginated" && this.layout.divisor > 1) { + left = prev.prev(); + if (left) { + return this.prepend(left); + } + } + }.bind(this), (err) => { + return err; + }) + .then(function(){ + if(this.isPaginated && this.settings.axis === "horizontal") { + if (this.settings.direction === "rtl") { + if (this.settings.rtlScrollType === "default"){ + this.scrollTo(0, 0, true); + } + else{ + this.scrollTo((this.container.scrollWidth * -1) + this.layout.delta, 0, true); + } + } else { + this.scrollTo(this.container.scrollWidth - this.layout.delta, 0, true); + } + } + this.views.show(); + }.bind(this)); + } + } + + current(){ + var visible = this.visible(); + if(visible.length){ + // Current is the last visible view + return visible[visible.length-1]; + } + return null; + } + + clear () { + + // this.q.clear(); + + if (this.views) { + this.views.hide(); + this.scrollTo(0,0, true); + this.views.clear(); + } + } + + currentLocation(){ + this.updateLayout(); + if (this.isPaginated && this.settings.axis === "horizontal") { + this.location = this.paginatedLocation(); + } else { + this.location = this.scrolledLocation(); + } + return this.location; + } + + scrolledLocation() { + let visible = this.visible(); + let container = this.container.getBoundingClientRect(); + let pageHeight = (container.height < window.innerHeight) ? container.height : window.innerHeight; + let pageWidth = (container.width < window.innerWidth) ? container.width : window.innerWidth; + let vertical = (this.settings.axis === "vertical"); + let rtl = (this.settings.direction === "rtl"); + + let offset = 0; + let used = 0; + + if(this.settings.fullsize) { + offset = vertical ? window.scrollY : window.scrollX; + } + + let sections = visible.map((view) => { + let {index, href} = view.section; + let position = view.position(); + let width = view.width(); + let height = view.height(); + + let startPos; + let endPos; + let stopPos; + let totalPages; + + if (vertical) { + startPos = offset + container.top - position.top + used; + endPos = startPos + pageHeight - used; + totalPages = this.layout.count(height, pageHeight).pages; + stopPos = pageHeight; + } else { + startPos = offset + container.left - position.left + used; + endPos = startPos + pageWidth - used; + totalPages = this.layout.count(width, pageWidth).pages; + stopPos = pageWidth; + } + + let currPage = Math.ceil(startPos / stopPos); + let pages = []; + let endPage = Math.ceil(endPos / stopPos); + + // Reverse page counts for horizontal rtl + if (this.settings.direction === "rtl" && !vertical) { + let tempStartPage = currPage; + currPage = totalPages - endPage; + endPage = totalPages - tempStartPage; + } + + pages = []; + for (var i = currPage; i <= endPage; i++) { + let pg = i + 1; + pages.push(pg); + } + + let mapping = this.mapping.page(view.contents, view.section.cfiBase, startPos, endPos); + + return { + index, + href, + pages, + totalPages, + mapping + }; + }); + + return sections; + } + + paginatedLocation(){ + let visible = this.visible(); + let container = this.container.getBoundingClientRect(); + + let left = 0; + let used = 0; + + if(this.settings.fullsize) { + left = window.scrollX; + } + + let sections = visible.map((view) => { + let {index, href} = view.section; + let offset; + let position = view.position(); + let width = view.width(); + + // Find mapping + let start; + let end; + let pageWidth; + + if (this.settings.direction === "rtl") { + offset = container.right - left; + pageWidth = Math.min(Math.abs(offset - position.left), this.layout.width) - used; + end = position.width - (position.right - offset) - used; + start = end - pageWidth; + } else { + offset = container.left + left; + pageWidth = Math.min(position.right - offset, this.layout.width) - used; + start = offset - position.left + used; + end = start + pageWidth; + } + + used += pageWidth; + + let mapping = this.mapping.page(view.contents, view.section.cfiBase, start, end); + + let totalPages = this.layout.count(width).pages; + let startPage = Math.floor(start / this.layout.pageWidth); + let pages = []; + let endPage = Math.floor(end / this.layout.pageWidth); + + // start page should not be negative + if (startPage < 0) { + startPage = 0; + endPage = endPage + 1; + } + + // Reverse page counts for rtl + if (this.settings.direction === "rtl") { + let tempStartPage = startPage; + startPage = totalPages - endPage; + endPage = totalPages - tempStartPage; + } + + + for (var i = startPage + 1; i <= endPage; i++) { + let pg = i; + pages.push(pg); + } + + return { + index, + href, + pages, + totalPages, + mapping + }; + }); + + return sections; + } + + isVisible(view, offsetPrev, offsetNext, _container){ + var position = view.position(); + var container = _container || this.bounds(); + + if(this.settings.axis === "horizontal" && + position.right > container.left - offsetPrev && + position.left < container.right + offsetNext) { + + return true; + + } else if(this.settings.axis === "vertical" && + position.bottom > container.top - offsetPrev && + position.top < container.bottom + offsetNext) { + + return true; + } + + return false; + + } + + visible(){ + var container = this.bounds(); + var views = this.views.displayed(); + var viewsLength = views.length; + var visible = []; + var isVisible; + var view; + + for (var i = 0; i < viewsLength; i++) { + view = views[i]; + isVisible = this.isVisible(view, 0, 0, container); + + if(isVisible === true) { + visible.push(view); + } + + } + return visible; + } + + scrollBy(x, y, silent){ + let dir = this.settings.direction === "rtl" ? -1 : 1; + + if(silent) { + this.ignore = true; + } + + if(!this.settings.fullsize) { + if(x) this.container.scrollLeft += x * dir; + if(y) this.container.scrollTop += y; + } else { + window.scrollBy(x * dir, y * dir); + } + this.scrolled = true; + } + + scrollTo(x, y, silent){ + if(silent) { + this.ignore = true; + } + + if(!this.settings.fullsize) { + this.container.scrollLeft = x; + this.container.scrollTop = y; + } else { + window.scrollTo(x,y); + } + this.scrolled = true; + } + + onScroll(){ + let scrollTop; + let scrollLeft; + + if(!this.settings.fullsize) { + scrollTop = this.container.scrollTop; + scrollLeft = this.container.scrollLeft; + } else { + scrollTop = window.scrollY; + scrollLeft = window.scrollX; + } + + this.scrollTop = scrollTop; + this.scrollLeft = scrollLeft; + + if(!this.ignore) { + this.emit(EVENTS.MANAGERS.SCROLL, { + top: scrollTop, + left: scrollLeft + }); + + clearTimeout(this.afterScrolled); + this.afterScrolled = setTimeout(function () { + this.emit(EVENTS.MANAGERS.SCROLLED, { + top: this.scrollTop, + left: this.scrollLeft + }); + }.bind(this), 20); + + + + } else { + this.ignore = false; + } + + } + + bounds() { + var bounds; + + bounds = this.stage.bounds(); + + return bounds; + } + + applyLayout(layout) { + + this.layout = layout; + this.updateLayout(); + if (this.views && this.views.length > 0 && this.layout.name === "pre-paginated") { + this.display(this.views.first().section); + } + // this.manager.layout(this.layout.format); + } + + updateLayout() { + + if (!this.stage) { + return; + } + + this._stageSize = this.stage.size(); + + if(!this.isPaginated) { + this.layout.calculate(this._stageSize.width, this._stageSize.height); + } else { + this.layout.calculate( + this._stageSize.width, + this._stageSize.height, + this.settings.gap + ); + + // Set the look ahead offset for what is visible + this.settings.offset = this.layout.delta / this.layout.divisor; + + // this.stage.addStyleRules("iframe", [{"margin-right" : this.layout.gap + "px"}]); + + } + + // Set the dimensions for views + this.viewSettings.width = this.layout.width; + this.viewSettings.height = this.layout.height; + + this.setLayout(this.layout); + } + + setLayout(layout){ + + this.viewSettings.layout = layout; + + this.mapping = new Mapping(layout.props, this.settings.direction, this.settings.axis); + + if(this.views) { + + this.views.forEach(function(view){ + if (view) { + view.setLayout(layout); + } + }); + + } + + } + + updateWritingMode(mode) { + this.writingMode = mode; + } + + updateAxis(axis, forceUpdate){ + + if (!forceUpdate && axis === this.settings.axis) { + return; + } + + this.settings.axis = axis; + + this.stage && this.stage.axis(axis); + + this.viewSettings.axis = axis; + + if (this.mapping) { + this.mapping = new Mapping(this.layout.props, this.settings.direction, this.settings.axis); + } + + if (this.layout) { + if (axis === "vertical") { + this.layout.spread("none"); + } else { + this.layout.spread(this.layout.settings.spread); + } + } + } + + updateFlow(flow, defaultScrolledOverflow="auto"){ + let isPaginated = (flow === "paginated" || flow === "auto"); + + this.isPaginated = isPaginated; + + if (flow === "scrolled-doc" || + flow === "scrolled-continuous" || + flow === "scrolled") { + this.updateAxis("vertical"); + } else { + this.updateAxis("horizontal"); + } + + this.viewSettings.flow = flow; + + if (!this.settings.overflow) { + this.overflow = isPaginated ? "hidden" : defaultScrolledOverflow; + } else { + this.overflow = this.settings.overflow; + } + + this.stage && this.stage.overflow(this.overflow); + + this.updateLayout(); + + } + + getContents(){ + var contents = []; + if (!this.views) { + return contents; + } + this.views.forEach(function(view){ + const viewContents = view && view.contents; + if (viewContents) { + contents.push(viewContents); + } + }); + return contents; + } + + direction(dir="ltr") { + this.settings.direction = dir; + + this.stage && this.stage.direction(dir); + + this.viewSettings.direction = dir; + + this.updateLayout(); + } + + isRendered() { + return this.rendered; + } +} + +//-- Enable binding events to Manager +EventEmitter(DefaultViewManager.prototype); + +export default DefaultViewManager; diff --git a/lib/epub.js/src/managers/helpers/snap.js b/lib/epub.js/src/managers/helpers/snap.js new file mode 100644 index 0000000..db0aaff --- /dev/null +++ b/lib/epub.js/src/managers/helpers/snap.js @@ -0,0 +1,338 @@ +import {extend, defer, requestAnimationFrame, prefixed} from "../../utils/core"; +import { EVENTS, DOM_EVENTS } from "../../utils/constants"; +import EventEmitter from "event-emitter"; + +// easing equations from https://github.com/danro/easing-js/blob/master/easing.js +const PI_D2 = (Math.PI / 2); +const EASING_EQUATIONS = { + easeOutSine: function (pos) { + return Math.sin(pos * PI_D2); + }, + easeInOutSine: function (pos) { + return (-0.5 * (Math.cos(Math.PI * pos) - 1)); + }, + easeInOutQuint: function (pos) { + if ((pos /= 0.5) < 1) { + return 0.5 * Math.pow(pos, 5); + } + return 0.5 * (Math.pow((pos - 2), 5) + 2); + }, + easeInCubic: function(pos) { + return Math.pow(pos, 3); + } +}; + +class Snap { + constructor(manager, options) { + + this.settings = extend({ + duration: 80, + minVelocity: 0.2, + minDistance: 10, + easing: EASING_EQUATIONS['easeInCubic'] + }, options || {}); + + this.supportsTouch = this.supportsTouch(); + + if (this.supportsTouch) { + this.setup(manager); + } + } + + setup(manager) { + this.manager = manager; + + this.layout = this.manager.layout; + + this.fullsize = this.manager.settings.fullsize; + if (this.fullsize) { + this.element = this.manager.stage.element; + this.scroller = window; + this.disableScroll(); + } else { + this.element = this.manager.stage.container; + this.scroller = this.element; + this.element.style["WebkitOverflowScrolling"] = "touch"; + } + + // this.overflow = this.manager.overflow; + + // set lookahead offset to page width + this.manager.settings.offset = this.layout.width; + this.manager.settings.afterScrolledTimeout = this.settings.duration * 2; + + this.isVertical = this.manager.settings.axis === "vertical"; + + // disable snapping if not paginated or axis in not horizontal + if (!this.manager.isPaginated || this.isVertical) { + return; + } + + this.touchCanceler = false; + this.resizeCanceler = false; + this.snapping = false; + + + this.scrollLeft; + this.scrollTop; + + this.startTouchX = undefined; + this.startTouchY = undefined; + this.startTime = undefined; + this.endTouchX = undefined; + this.endTouchY = undefined; + this.endTime = undefined; + + this.addListeners(); + } + + supportsTouch() { + if (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) { + return true; + } + + return false; + } + + disableScroll() { + this.element.style.overflow = "hidden"; + } + + enableScroll() { + this.element.style.overflow = ""; + } + + addListeners() { + this._onResize = this.onResize.bind(this); + window.addEventListener('resize', this._onResize); + + this._onScroll = this.onScroll.bind(this); + this.scroller.addEventListener('scroll', this._onScroll); + + this._onTouchStart = this.onTouchStart.bind(this); + this.scroller.addEventListener('touchstart', this._onTouchStart, { passive: true }); + this.on('touchstart', this._onTouchStart); + + this._onTouchMove = this.onTouchMove.bind(this); + this.scroller.addEventListener('touchmove', this._onTouchMove, { passive: true }); + this.on('touchmove', this._onTouchMove); + + this._onTouchEnd = this.onTouchEnd.bind(this); + this.scroller.addEventListener('touchend', this._onTouchEnd, { passive: true }); + this.on('touchend', this._onTouchEnd); + + this._afterDisplayed = this.afterDisplayed.bind(this); + this.manager.on(EVENTS.MANAGERS.ADDED, this._afterDisplayed); + } + + removeListeners() { + window.removeEventListener('resize', this._onResize); + this._onResize = undefined; + + this.scroller.removeEventListener('scroll', this._onScroll); + this._onScroll = undefined; + + this.scroller.removeEventListener('touchstart', this._onTouchStart, { passive: true }); + this.off('touchstart', this._onTouchStart); + this._onTouchStart = undefined; + + this.scroller.removeEventListener('touchmove', this._onTouchMove, { passive: true }); + this.off('touchmove', this._onTouchMove); + this._onTouchMove = undefined; + + this.scroller.removeEventListener('touchend', this._onTouchEnd, { passive: true }); + this.off('touchend', this._onTouchEnd); + this._onTouchEnd = undefined; + + this.manager.off(EVENTS.MANAGERS.ADDED, this._afterDisplayed); + this._afterDisplayed = undefined; + } + + afterDisplayed(view) { + let contents = view.contents; + ["touchstart", "touchmove", "touchend"].forEach((e) => { + contents.on(e, (ev) => this.triggerViewEvent(ev, contents)); + }); + } + + triggerViewEvent(e, contents){ + this.emit(e.type, e, contents); + } + + onScroll(e) { + this.scrollLeft = this.fullsize ? window.scrollX : this.scroller.scrollLeft; + this.scrollTop = this.fullsize ? window.scrollY : this.scroller.scrollTop; + } + + onResize(e) { + this.resizeCanceler = true; + } + + onTouchStart(e) { + let { screenX, screenY } = e.touches[0]; + + if (this.fullsize) { + this.enableScroll(); + } + + this.touchCanceler = true; + + if (!this.startTouchX) { + this.startTouchX = screenX; + this.startTouchY = screenY; + this.startTime = this.now(); + } + + this.endTouchX = screenX; + this.endTouchY = screenY; + this.endTime = this.now(); + } + + onTouchMove(e) { + let { screenX, screenY } = e.touches[0]; + let deltaY = Math.abs(screenY - this.endTouchY); + + this.touchCanceler = true; + + + if (!this.fullsize && deltaY < 10) { + this.element.scrollLeft -= screenX - this.endTouchX; + } + + this.endTouchX = screenX; + this.endTouchY = screenY; + this.endTime = this.now(); + } + + onTouchEnd(e) { + if (this.fullsize) { + this.disableScroll(); + } + + this.touchCanceler = false; + + let swipped = this.wasSwiped(); + + if (swipped !== 0) { + this.snap(swipped); + } else { + this.snap(); + } + + this.startTouchX = undefined; + this.startTouchY = undefined; + this.startTime = undefined; + this.endTouchX = undefined; + this.endTouchY = undefined; + this.endTime = undefined; + } + + wasSwiped() { + let snapWidth = this.layout.pageWidth * this.layout.divisor; + let distance = (this.endTouchX - this.startTouchX); + let absolute = Math.abs(distance); + let time = this.endTime - this.startTime; + let velocity = (distance / time); + let minVelocity = this.settings.minVelocity; + + if (absolute <= this.settings.minDistance || absolute >= snapWidth) { + return 0; + } + + if (velocity > minVelocity) { + // previous + return -1; + } else if (velocity < -minVelocity) { + // next + return 1; + } + } + + needsSnap() { + let left = this.scrollLeft; + let snapWidth = this.layout.pageWidth * this.layout.divisor; + return (left % snapWidth) !== 0; + } + + snap(howMany=0) { + let left = this.scrollLeft; + let snapWidth = this.layout.pageWidth * this.layout.divisor; + let snapTo = Math.round(left / snapWidth) * snapWidth; + + if (howMany) { + snapTo += (howMany * snapWidth); + } + + return this.smoothScrollTo(snapTo); + } + + smoothScrollTo(destination) { + const deferred = new defer(); + const start = this.scrollLeft; + const startTime = this.now(); + + const duration = this.settings.duration; + const easing = this.settings.easing; + + this.snapping = true; + + // add animation loop + function tick() { + const now = this.now(); + const time = Math.min(1, ((now - startTime) / duration)); + const timeFunction = easing(time); + + + if (this.touchCanceler || this.resizeCanceler) { + this.resizeCanceler = false; + this.snapping = false; + deferred.resolve(); + return; + } + + if (time < 1) { + window.requestAnimationFrame(tick.bind(this)); + this.scrollTo(start + ((destination - start) * time), 0); + } else { + this.scrollTo(destination, 0); + this.snapping = false; + deferred.resolve(); + } + } + + tick.call(this); + + return deferred.promise; + } + + scrollTo(left=0, top=0) { + if (this.fullsize) { + window.scroll(left, top); + } else { + this.scroller.scrollLeft = left; + this.scroller.scrollTop = top; + } + } + + now() { + return ('now' in window.performance) ? performance.now() : new Date().getTime(); + } + + destroy() { + if (!this.scroller) { + return; + } + + if (this.fullsize) { + this.enableScroll(); + } + + this.removeListeners(); + + this.scroller = undefined; + } +} + +EventEmitter(Snap.prototype); + +export default Snap; diff --git a/lib/epub.js/src/managers/helpers/stage.js b/lib/epub.js/src/managers/helpers/stage.js new file mode 100644 index 0000000..d0f67e6 --- /dev/null +++ b/lib/epub.js/src/managers/helpers/stage.js @@ -0,0 +1,363 @@ +import {uuid, isNumber, isElement, windowBounds, extend} from "../../utils/core"; +import throttle from 'lodash/throttle' + +class Stage { + constructor(_options) { + this.settings = _options || {}; + this.id = "epubjs-container-" + uuid(); + + this.container = this.create(this.settings); + + if(this.settings.hidden) { + this.wrapper = this.wrap(this.container); + } + + } + + /* + * Creates an element to render to. + * Resizes to passed width and height or to the elements size + */ + create(options){ + let height = options.height;// !== false ? options.height : "100%"; + let width = options.width;// !== false ? options.width : "100%"; + let overflow = options.overflow || false; + let axis = options.axis || "vertical"; + let direction = options.direction; + + extend(this.settings, options); + + if(options.height && isNumber(options.height)) { + height = options.height + "px"; + } + + if(options.width && isNumber(options.width)) { + width = options.width + "px"; + } + + // Create new container element + let container = document.createElement("div"); + + container.id = this.id; + container.classList.add("epub-container"); + + // Style Element + // container.style.fontSize = "0"; + container.style.wordSpacing = "0"; + container.style.lineHeight = "0"; + container.style.verticalAlign = "top"; + container.style.position = "relative"; + + if(axis === "horizontal") { + // container.style.whiteSpace = "nowrap"; + container.style.display = "flex"; + container.style.flexDirection = "row"; + container.style.flexWrap = "nowrap"; + } + + if(width){ + container.style.width = width; + } + + if(height){ + container.style.height = height; + } + + if (overflow) { + if (overflow === "scroll" && axis === "vertical") { + container.style["overflow-y"] = overflow; + container.style["overflow-x"] = "hidden"; + } else if (overflow === "scroll" && axis === "horizontal") { + container.style["overflow-y"] = "hidden"; + container.style["overflow-x"] = overflow; + } else { + container.style["overflow"] = overflow; + } + } + + if (direction) { + container.dir = direction; + container.style["direction"] = direction; + } + + if (direction && this.settings.fullsize) { + document.body.style["direction"] = direction; + } + + return container; + } + + wrap(container) { + var wrapper = document.createElement("div"); + + wrapper.style.visibility = "hidden"; + wrapper.style.overflow = "hidden"; + wrapper.style.width = "0"; + wrapper.style.height = "0"; + + wrapper.appendChild(container); + return wrapper; + } + + + getElement(_element){ + var element; + + if(isElement(_element)) { + element = _element; + } else if (typeof _element === "string") { + element = document.getElementById(_element); + } + + if(!element){ + throw new Error("Not an Element"); + } + + return element; + } + + attachTo(what){ + + var element = this.getElement(what); + var base; + + if(!element){ + return; + } + + if(this.settings.hidden) { + base = this.wrapper; + } else { + base = this.container; + } + + element.appendChild(base); + + this.element = element; + + return element; + + } + + getContainer() { + return this.container; + } + + onResize(func){ + // Only listen to window for resize event if width and height are not fixed. + // This applies if it is set to a percent or auto. + if(!isNumber(this.settings.width) || + !isNumber(this.settings.height) ) { + this.resizeFunc = throttle(func, 50); + window.addEventListener("resize", this.resizeFunc, false); + } + + } + + onOrientationChange(func){ + this.orientationChangeFunc = func; + window.addEventListener("orientationchange", this.orientationChangeFunc, false); + } + + size(width, height){ + var bounds; + let _width = width || this.settings.width; + let _height = height || this.settings.height; + + // If width or height are set to false, inherit them from containing element + if(width === null) { + bounds = this.element.getBoundingClientRect(); + + if(bounds.width) { + width = Math.floor(bounds.width); + this.container.style.width = width + "px"; + } + } else { + if (isNumber(width)) { + this.container.style.width = width + "px"; + } else { + this.container.style.width = width; + } + } + + if(height === null) { + bounds = bounds || this.element.getBoundingClientRect(); + + if(bounds.height) { + height = bounds.height; + this.container.style.height = height + "px"; + } + + } else { + if (isNumber(height)) { + this.container.style.height = height + "px"; + } else { + this.container.style.height = height; + } + } + + if(!isNumber(width)) { + width = this.container.clientWidth; + } + + if(!isNumber(height)) { + height = this.container.clientHeight; + } + + this.containerStyles = window.getComputedStyle(this.container); + + this.containerPadding = { + left: parseFloat(this.containerStyles["padding-left"]) || 0, + right: parseFloat(this.containerStyles["padding-right"]) || 0, + top: parseFloat(this.containerStyles["padding-top"]) || 0, + bottom: parseFloat(this.containerStyles["padding-bottom"]) || 0 + }; + + // Bounds not set, get them from window + let _windowBounds = windowBounds(); + let bodyStyles = window.getComputedStyle(document.body); + let bodyPadding = { + left: parseFloat(bodyStyles["padding-left"]) || 0, + right: parseFloat(bodyStyles["padding-right"]) || 0, + top: parseFloat(bodyStyles["padding-top"]) || 0, + bottom: parseFloat(bodyStyles["padding-bottom"]) || 0 + }; + + if (!_width) { + width = _windowBounds.width - + bodyPadding.left - + bodyPadding.right; + } + + if ((this.settings.fullsize && !_height) || !_height) { + height = _windowBounds.height - + bodyPadding.top - + bodyPadding.bottom; + } + + return { + width: width - + this.containerPadding.left - + this.containerPadding.right, + height: height - + this.containerPadding.top - + this.containerPadding.bottom + }; + + } + + bounds(){ + let box; + if (this.container.style.overflow !== "visible") { + box = this.container && this.container.getBoundingClientRect(); + } + + if(!box || !box.width || !box.height) { + return windowBounds(); + } else { + return box; + } + + } + + getSheet(){ + var style = document.createElement("style"); + + // WebKit hack --> https://davidwalsh.name/add-rules-stylesheets + style.appendChild(document.createTextNode("")); + + document.head.appendChild(style); + + return style.sheet; + } + + addStyleRules(selector, rulesArray){ + var scope = "#" + this.id + " "; + var rules = ""; + + if(!this.sheet){ + this.sheet = this.getSheet(); + } + + rulesArray.forEach(function(set) { + for (var prop in set) { + if(set.hasOwnProperty(prop)) { + rules += prop + ":" + set[prop] + ";"; + } + } + }); + + this.sheet.insertRule(scope + selector + " {" + rules + "}", 0); + } + + axis(axis) { + if(axis === "horizontal") { + this.container.style.display = "flex"; + this.container.style.flexDirection = "row"; + this.container.style.flexWrap = "nowrap"; + } else { + this.container.style.display = "block"; + } + this.settings.axis = axis; + } + + // orientation(orientation) { + // if (orientation === "landscape") { + // + // } else { + // + // } + // + // this.orientation = orientation; + // } + + direction(dir) { + if (this.container) { + this.container.dir = dir; + this.container.style["direction"] = dir; + } + + if (this.settings.fullsize) { + document.body.style["direction"] = dir; + } + this.settings.dir = dir; + } + + overflow(overflow) { + if (this.container) { + if (overflow === "scroll" && this.settings.axis === "vertical") { + this.container.style["overflow-y"] = overflow; + this.container.style["overflow-x"] = "hidden"; + } else if (overflow === "scroll" && this.settings.axis === "horizontal") { + this.container.style["overflow-y"] = "hidden"; + this.container.style["overflow-x"] = overflow; + } else { + this.container.style["overflow"] = overflow; + } + } + this.settings.overflow = overflow; + } + + destroy() { + var base; + + if (this.element) { + + if(this.settings.hidden) { + base = this.wrapper; + } else { + base = this.container; + } + + if(this.element.contains(this.container)) { + this.element.removeChild(this.container); + } + + window.removeEventListener("resize", this.resizeFunc); + window.removeEventListener("orientationChange", this.orientationChangeFunc); + + } + } +} + +export default Stage; diff --git a/lib/epub.js/src/managers/helpers/views.js b/lib/epub.js/src/managers/helpers/views.js new file mode 100644 index 0000000..4368da2 --- /dev/null +++ b/lib/epub.js/src/managers/helpers/views.js @@ -0,0 +1,167 @@ +class Views { + constructor(container) { + this.container = container; + this._views = []; + this.length = 0; + this.hidden = false; + } + + all() { + return this._views; + } + + first() { + return this._views[0]; + } + + last() { + return this._views[this._views.length-1]; + } + + indexOf(view) { + return this._views.indexOf(view); + } + + slice() { + return this._views.slice.apply(this._views, arguments); + } + + get(i) { + return this._views[i]; + } + + append(view){ + this._views.push(view); + if(this.container){ + this.container.appendChild(view.element); + } + this.length++; + return view; + } + + prepend(view){ + this._views.unshift(view); + if(this.container){ + this.container.insertBefore(view.element, this.container.firstChild); + } + this.length++; + return view; + } + + insert(view, index) { + this._views.splice(index, 0, view); + + if(this.container){ + if(index < this.container.children.length){ + this.container.insertBefore(view.element, this.container.children[index]); + } else { + this.container.appendChild(view.element); + } + } + + this.length++; + return view; + } + + remove(view) { + var index = this._views.indexOf(view); + + if(index > -1) { + this._views.splice(index, 1); + } + + + this.destroy(view); + + this.length--; + } + + destroy(view) { + if(view.displayed){ + view.destroy(); + } + + if(this.container){ + this.container.removeChild(view.element); + } + view = null; + } + + // Iterators + + forEach() { + return this._views.forEach.apply(this._views, arguments); + } + + clear(){ + // Remove all views + var view; + var len = this.length; + + if(!this.length) return; + + for (var i = 0; i < len; i++) { + view = this._views[i]; + this.destroy(view); + } + + this._views = []; + this.length = 0; + } + + find(section){ + + var view; + var len = this.length; + + for (var i = 0; i < len; i++) { + view = this._views[i]; + if(view.displayed && view.section.index == section.index) { + return view; + } + } + + } + + displayed(){ + var displayed = []; + var view; + var len = this.length; + + for (var i = 0; i < len; i++) { + view = this._views[i]; + if(view.displayed){ + displayed.push(view); + } + } + return displayed; + } + + show(){ + var view; + var len = this.length; + + for (var i = 0; i < len; i++) { + view = this._views[i]; + if(view.displayed){ + view.show(); + } + } + this.hidden = false; + } + + hide(){ + var view; + var len = this.length; + + for (var i = 0; i < len; i++) { + view = this._views[i]; + if(view.displayed){ + view.hide(); + } + } + this.hidden = true; + } +} + +export default Views; diff --git a/lib/epub.js/src/managers/views/iframe.js b/lib/epub.js/src/managers/views/iframe.js new file mode 100644 index 0000000..76b2c1d --- /dev/null +++ b/lib/epub.js/src/managers/views/iframe.js @@ -0,0 +1,835 @@ +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; diff --git a/lib/epub.js/src/managers/views/inline.js b/lib/epub.js/src/managers/views/inline.js new file mode 100644 index 0000000..072b586 --- /dev/null +++ b/lib/epub.js/src/managers/views/inline.js @@ -0,0 +1,432 @@ +import EventEmitter from "event-emitter"; +import {extend, borders, uuid, isNumber, bounds, defer, qs, parse} from "../../utils/core"; +import EpubCFI from "../../epubcfi"; +import Contents from "../../contents"; +import { EVENTS } from "../../utils/constants"; + +class InlineView { + constructor(section, options) { + this.settings = extend({ + ignoreClass : "", + axis: "vertical", + width: 0, + height: 0, + layout: undefined, + globalLayoutProperties: {}, + }, 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"]; + + } + + container(axis) { + var element = document.createElement("div"); + + element.classList.add("epub-view"); + + // if(this.settings.axis === "horizontal") { + // element.style.width = "auto"; + // element.style.height = "0"; + // } else { + // element.style.width = "0"; + // element.style.height = "auto"; + // } + + element.style.overflow = "hidden"; + + if(axis && axis == "horizontal"){ + element.style.display = "inline-block"; + } else { + element.style.display = "block"; + } + + return element; + } + + create() { + + if(this.frame) { + return this.frame; + } + + if(!this.element) { + this.element = this.createContainer(); + } + + this.frame = document.createElement("div"); + this.frame.id = this.id; + this.frame.style.overflow = "hidden"; + this.frame.style.wordSpacing = "initial"; + this.frame.style.lineHeight = "initial"; + + this.resizing = true; + + // this.frame.style.display = "none"; + this.element.style.visibility = "hidden"; + this.frame.style.visibility = "hidden"; + + if(this.settings.axis === "horizontal") { + this.frame.style.width = "auto"; + this.frame.style.height = "0"; + } else { + this.frame.style.width = "0"; + this.frame.style.height = "auto"; + } + + this._width = 0; + this._height = 0; + + this.element.appendChild(this.frame); + this.added = true; + + this.elementBounds = bounds(this.element); + + return this.frame; + } + + render(request, show) { + + // view.onLayout = this.layout.format.bind(this.layout); + this.create(); + + // Fit to size of the container, apply padding + this.size(); + + // Render Chain + return this.section.render(request) + .then(function(contents){ + return this.load(contents); + }.bind(this)) + // .then(function(doc){ + // return this.hooks.content.trigger(view, this); + // }.bind(this)) + .then(function(){ + // this.settings.layout.format(view.contents); + // return this.hooks.layout.trigger(view, this); + }.bind(this)) + // .then(function(){ + // return this.display(); + // }.bind(this)) + // .then(function(){ + // return this.hooks.render.trigger(view, this); + // }.bind(this)) + .then(function(){ + + // apply the layout function to the contents + this.settings.layout.format(this.contents); + + // Expand the iframe to the full size of the content + // this.expand(); + + // Listen for events that require an expansion of the iframe + this.addListeners(); + + if(show !== false) { + //this.q.enqueue(function(view){ + this.show(); + //}, view); + } + // this.map = new Map(view, this.layout); + //this.hooks.show.trigger(view, this); + this.emit(EVENTS.VIEWS.RENDERED, this.section); + + }.bind(this)) + .catch(function(e){ + this.emit(EVENTS.VIEWS.LOAD_ERROR, e); + }.bind(this)); + + } + + // 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") { + // TODO: check if these are different than the size set in chapter + this.lock("both", width, height); + } else if(this.settings.axis === "horizontal") { + this.lock("height", width, height); + } else { + this.lock("width", width, height); + } + + } + + // Lock an axis to element dimensions, taking borders into account + lock(what, width, height) { + var elBorders = borders(this.element); + var iframeBorders; + + if(this.frame) { + iframeBorders = borders(this.frame); + } else { + iframeBorders = {width: 0, height: 0}; + } + + if(what == "width" && isNumber(width)){ + this.lockedWidth = width - elBorders.width - iframeBorders.width; + this.resize(this.lockedWidth, false); // width keeps ratio correct + } + + if(what == "height" && isNumber(height)){ + this.lockedHeight = height - elBorders.height - iframeBorders.height; + this.resize(false, 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); + } + + } + + // Resize a single axis based on content dimensions + expand(force) { + var width = this.lockedWidth; + var height = this.lockedHeight; + + var textWidth, textHeight; + + if(!this.frame || this._expanding) return; + + this._expanding = true; + + // Expand Horizontally + if(this.settings.axis === "horizontal") { + width = this.contentWidth(textWidth); + } // Expand Vertically + else if(this.settings.axis === "vertical") { + height = this.contentHeight(textHeight); + } + + // 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.resize(width, height); + } + + this._expanding = false; + } + + contentWidth(min) { + return this.frame.scrollWidth; + } + + contentHeight(min) { + return this.frame.scrollHeight; + } + + + resize(width, height) { + + if(!this.frame) return; + + if(isNumber(width)){ + this.frame.style.width = width + "px"; + this._width = width; + } + + if(isNumber(height)){ + this.frame.style.height = height + "px"; + this._height = height; + } + + this.prevBounds = this.elementBounds; + + this.elementBounds = bounds(this.element); + + let size = { + width: this.elementBounds.width, + height: this.elementBounds.height, + widthDelta: this.elementBounds.width - this.prevBounds.width, + heightDelta: this.elementBounds.height - this.prevBounds.height, + }; + + this.onResize(this, size); + + this.emit(EVENTS.VIEWS.RESIZED, size); + + } + + + load(contents) { + var loading = new defer(); + var loaded = loading.promise; + var doc = parse(contents, "text/html"); + var body = qs(doc, "body"); + + /* + var srcs = doc.querySelectorAll("[src]"); + + Array.prototype.slice.call(srcs) + .forEach(function(item) { + var src = item.getAttribute("src"); + var assetUri = URI(src); + var origin = assetUri.origin(); + var absoluteUri; + + if (!origin) { + absoluteUri = assetUri.absoluteTo(this.section.url); + item.src = absoluteUri; + } + }.bind(this)); + */ + this.frame.innerHTML = body.innerHTML; + + this.document = this.frame.ownerDocument; + this.window = this.document.defaultView; + + this.contents = new Contents(this.document, this.frame); + + this.rendering = false; + + loading.resolve(this.contents); + + + return loaded; + } + + setLayout(layout) { + this.layout = layout; + } + + + resizeListenters() { + // Test size again + // clearTimeout(this.expanding); + // this.expanding = setTimeout(this.expand.bind(this), 350); + } + + 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)); + + } else { + displayed.resolve(this); + } + + + return displayed.promise; + } + + show() { + + this.element.style.visibility = "visible"; + + if(this.frame){ + this.frame.style.visibility = "visible"; + } + + this.emit(EVENTS.VIEWS.SHOWN, this); + } + + hide() { + // this.frame.style.display = "none"; + this.element.style.visibility = "hidden"; + this.frame.style.visibility = "hidden"; + + this.stopExpanding = true; + this.emit(EVENTS.VIEWS.HIDDEN, this); + } + + position() { + return this.element.getBoundingClientRect(); + } + + locationOf(target) { + var parentPos = this.frame.getBoundingClientRect(); + var targetPos = this.contents.locationOf(target, this.settings.ignoreClass); + + return { + "left": window.scrollX + parentPos.left + targetPos.left, + "top": window.scrollY + parentPos.top + targetPos.top + }; + } + + onDisplayed(view) { + // Stub, override with a custom functions + } + + onResize(view, e) { + // Stub, override with a custom functions + } + + bounds() { + if(!this.elementBounds) { + this.elementBounds = bounds(this.element); + } + return this.elementBounds; + } + + destroy() { + + if(this.displayed){ + this.displayed = false; + + this.removeListeners(); + + this.stopExpanding = true; + this.element.removeChild(this.frame); + this.displayed = false; + this.frame = null; + + this._textWidth = null; + this._textHeight = null; + this._width = null; + this._height = null; + } + // this.element.style.height = "0px"; + // this.element.style.width = "0px"; + } +} + +EventEmitter(InlineView.prototype); + +export default InlineView; |