diff options
Diffstat (limited to 'lib/epub.js/src/managers/helpers')
-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 |
3 files changed, 868 insertions, 0 deletions
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; |