Last active
March 11, 2023 13:14
-
-
Save leejunhui/8b4f6d149798eb98bf765bcc324f20f2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: options.afterScrolledTimeout, | |
allowScriptedContent: false, | |
allowPopups: false, | |
capableScrollPage: undefined, | |
}); | |
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, | |
allowScriptedContent: this.settings.allowScriptedContent, | |
allowPopups: this.settings.allowPopups, | |
}; | |
this.scrollTop = 0; | |
this.scrollLeft = 0; | |
this.waitForTrimViewList = []; | |
} | |
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) { | |
if (this.snapper.snapping && this.scrollLeft !== this.snapper.snapTarget) { | |
// console.log( | |
// 'AAA - append 被过滤了', | |
// section, | |
// this.scrollLeft, | |
// this.snapper.snapTarget | |
// ); | |
// return; | |
} | |
// console.log('AAA - 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) { | |
// if (this.snapper.snapping && this.scrollLeft !== this.snapper.snapTarget) { | |
// console.log( | |
// 'AAA - prepend 被过滤了', | |
// section, | |
// this.scrollLeft, | |
// this.snapper.snapTarget, | |
// this.snapper.snapTo | |
// ); | |
// return; | |
// } | |
// console.log('AAA - prepend 没有被过滤', section); | |
var view = this.createView(section); | |
view.on(EVENTS.VIEWS.RESIZED, (bounds) => { | |
// console.log('EVENTS.VIEWS.RESIZED 来了', view.id, 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 { | |
// !!! 非常关键,确保 snapper 在 snapping 过程中不会再有额外的 scrollBy 影响 container 的偏移 | |
// if ( | |
// this.snapper.snapping && | |
// this.scrollLeft !== this.snapper.snapTarget | |
// ) { | |
// console.log( | |
// 'epubjs - continious - counter - 被过滤掉了', | |
// this.snapper.snapping, | |
// this.scrollLeft | |
// ); | |
// return; | |
// } | |
// console.log( | |
// 'epubjs - continious - counter - 没有过滤掉', | |
// this.snapper.snapping, | |
// this.scrollLeft | |
// ); | |
this.scrollBy(bounds.widthDelta, 0, true); | |
} | |
} | |
trimWaitingView() { | |
setTimeout(() => { | |
while (this.waitForTrimViewList.length > 0) { | |
const view = this.waitForTrimViewList.shift(); | |
this.q.enqueue(view.destroy.bind(view)); | |
clearTimeout(this.trimTimeout); | |
this.trimTimeout = setTimeout( | |
function () { | |
this.q.enqueue(this.trim.bind(this)); | |
}.bind(this), | |
250 | |
); | |
} | |
}, 500); | |
} | |
update(_offset) { | |
// console.log('epubjs - continuious - update 来了'); | |
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); | |
// console.log( | |
// `epubjs - continious - update - isVisible = ${isVisible}, view=${view.id}, offset = ${offset}` | |
// ); | |
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 { | |
if (this.snapper.scrolling) { | |
// 正在滑动中,不执行 trim | |
// 将 view 加入队列中,在 snapper 滚动完成后执行对 view 的 trim 任务 | |
// 先检查是否已经在队列中存在 | |
const alreadyInList = this.waitForTrimViewList.some( | |
(v) => view.id === v.id | |
); | |
if (!alreadyInList) { | |
this.waitForTrimViewList.push(view); | |
} | |
console.log( | |
'epubjs - continuous - update - 因为仍然在 scrolling,所以 trim 加入队列', | |
this.waitForTrimViewList.map((v) => v.id) | |
); | |
return; | |
} | |
this.q.enqueue(view.destroy.bind(view)); | |
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) { | |
// if (this.snapper.snapping) { | |
// console.log( | |
// 'check 被跳过了', | |
// _offsetLeft, | |
// this.snapper.snapping, | |
// this.snapper.snapTarget, | |
// this.snapper.snapTo | |
// ); | |
// return; | |
// } | |
if (this.snapper.scrolling) { | |
return; | |
} | |
console.log( | |
`check 来了, this.scrollLeft=${this.scrollLeft}, this.views.length=${ | |
this.views.length | |
}, this.snapper.snapping=${this.snapper.snapping}, this.snapper.snapTo=${ | |
this.snapper.snapTo | |
}, this.snapper.snapTarget=${ | |
this.snapper.snapTarget | |
}, Date.now()=${Date.now()}` | |
); | |
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) { | |
const result = this.prepend(prev); | |
if (result) { | |
newViews.push(result); | |
} | |
} | |
}; | |
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) { | |
console.log( | |
`start < 0 了,需要 prepend 了 | |
this.settings.offset = ${this.settings.offset} | |
offset = ${offset} | |
delta = ${delta} | |
start = ${start} | |
this.views._views.length = ${this.views._views.length} | |
this.scrollLeft = ${this.scrollLeft} | |
` | |
); | |
// this.views.forEach(function (view) { | |
// if (view) { | |
// console.log('start < 0 了,需要 prepend 了 -- view', view.id); | |
// } | |
// }); | |
prepend(); | |
} | |
newViews = newViews.filter((view) => view != null); | |
// iframe display => render => expand | |
let promises = newViews.map((view) => { | |
if (view) { | |
return view.display(this.request); | |
} else { | |
console.log('wtf - view 居然为空'); | |
return undefined; | |
} | |
// 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 { | |
const eraseScrollTo = prevLeft - Math.floor(bounds.width); | |
// console.log( | |
// 'epubjs - continious - eraseScrollTo', | |
// eraseScrollTo, | |
// view.id | |
// ); | |
this.scrollTo(eraseScrollTo, 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, { | |
capableScrollPage: this.settings.capableScrollPage, | |
}); | |
} | |
} | |
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.prevScrollLeft !== this.scrollLeft) { | |
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() { | |
// console.log('epubjs - continuious - scrolled 来了', Date.now()); | |
// TODO: cenfeng - | |
this.q.enqueue( | |
function () { | |
return this.check(); | |
}.bind(this) | |
); | |
// console.log('epubjs - continuious - check 加入队列了'); | |
this.emit(EVENTS.MANAGERS.SCROLL, { | |
top: this.scrollTop, | |
left: this.scrollLeft, | |
}); | |
clearTimeout(this.afterScrolled); | |
this.afterScrolled = setTimeout( | |
function () { | |
// console.log('epubjs - continuious - afterScrolled 开始执行了'); | |
// Don't report scroll if we are about the snap | |
if ( | |
this.snapper && | |
this.snapper.supportsTouch && | |
this.snapper.needsSnap() | |
) { | |
// console.log('epubjs - continuious - snapper return 了'); | |
return; | |
} | |
// console.log('epubjs - continuious - EVENTS.MANAGERS.SCROLLED 来了'); | |
this.emit(EVENTS.MANAGERS.SCROLLED, { | |
top: this.scrollTop, | |
left: this.scrollLeft, | |
}); | |
}.bind(this), | |
50 | |
); | |
// console.log( | |
// 'epubjs - continuious - this.afterScrolled 被赋值了', | |
// this.afterScrolled | |
// ); | |
} | |
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
allowScriptedContent: false, | |
allowPopups: false, | |
baseUrl: 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, | |
allowScriptedContent: this.settings.allowScriptedContent, | |
allowPopups: this.settings.allowPopups, | |
}; | |
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) { | |
try { | |
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); | |
}); | |
// console.log('epubjs - defaultManager - 418'); | |
return view.display(this.request); | |
} catch (error) { | |
// console.log('epubjs - defaultManager - 421', error); | |
} | |
} | |
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; | |
// 之所以要去掉等于的时候 scrollBy,是因为在 epub.js 中插入 easy-marker 的节点后,会导致每一章后面都会多一个空白页 | |
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) { | |
if (this.settings.baseUrl && !next.baseUrl) { | |
next.baseUrl = this.settings.baseUrl; | |
} | |
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 { | |
// 之所以要减去 layout.delta * 2,是因为在 epub.js 中插入 easy-marker 的节点后,会导致每一章后面都会多一个空白页 | |
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(); | |
const isVisibleResult = | |
position.right > container.left - offsetPrev && | |
position.left < container.right + offsetNext; | |
// console.log( | |
// `=== isVisible isVisibleResult=${isVisibleResult} view=${ | |
// view.id | |
// } offsetPrev=${offsetPrev} offsetNext=${offsetNext} position=${JSON.stringify( | |
// position | |
// )} container=${JSON.stringify(container)}` | |
// ); | |
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; | |
// console.log( | |
// 'xxx epubjs - scrollBy - manager', | |
// x, | |
// Date.now(), | |
// this.snapper.swipped, | |
// this.container.scrollLeft | |
// ); | |
// console.trace(); | |
if (silent) { | |
this.ignore = true; | |
} | |
if (!this.settings.fullsize) { | |
if (x) { | |
this.container.scrollLeft += x * dir; | |
// console.log( | |
// 'xxx epubjs - scrollBy - manager - after', | |
// x, | |
// Date.now(), | |
// this.snapper.swipped, | |
// this.container.scrollLeft | |
// ); | |
} | |
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; | |
} | |
// console.log('xxx epubjs - scrollTo - manager', x); | |
// console.trace(); | |
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: 160, | |
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.scrolling = false; | |
this.scrollLeft; | |
this.scrollTop; | |
this.startTouchX = undefined; | |
this.startTouchY = undefined; | |
this.startTime = undefined; | |
this.endTouchX = undefined; | |
this.endTouchY = undefined; | |
this.endTime = undefined; | |
this.swipped = false; | |
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; | |
this.scrolling = 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.settings.capableScrollPage) { | |
const capableScrollPage = this.settings.capableScrollPage(); | |
if (capableScrollPage) { | |
// 非全屏并且 y 轴方向偏移量小于 10 则同步用户手势的偏移量到 this.element 上 | |
if (!this.fullsize && deltaY < 10) { | |
// screenX 是当前时间点用户手指所在的坐标点的 x 坐标 | |
// this.endTouchX 来源有两处,一是 touchStart 中即用户手指刚接触屏幕时的坐标的 x | |
// 而是上一次手指移动的位置的坐标点的 x | |
const targetScrollLeft = screenX - this.endTouchX; | |
// console.log( | |
// 'epubjs - snap - onTouchMove - before', | |
// targetScrollLeft, | |
// this.element.scrollLeft | |
// ); | |
// 用户从左往右滑动的时候,x 坐标是递减的,所以 targetScrollLeft 为负值,而为了让 this.element 产生向右偏移的效果,就需要减去负值,进而让 scrollLeft 有递增的趋势 | |
// 用户从右往左滑动的时候,x 坐标是递增的,所以 targetScrollLeft 为正值,而为了让 this.element 产生向左偏移的效果,就需要减去正值,进而让 scrollLeft 有递减的趋势 | |
this.element.scrollLeft -= targetScrollLeft; | |
// console.log( | |
// 'epubjs - snap - onTouchMove - after', | |
// targetScrollLeft, | |
// this.element.scrollLeft | |
// ); | |
} | |
} | |
} | |
this.endTouchX = screenX; | |
this.endTouchY = screenY; | |
this.endTime = this.now(); | |
} | |
onTouchEnd(e, contents) { | |
if (this.lastTouchEndTime) { | |
const timeDelta = Date.now() - this.lastTouchEndTime; | |
if (timeDelta < 10) { | |
// console.log('got you!!'); | |
return; | |
} | |
} | |
this.lastTouchEndTime = Date.now(); | |
// console.log('epubjs - snap - onTouchEnd 来了哦', e, contents, Date.now()); | |
// console.trace(); | |
if (this.fullsize) { | |
this.disableScroll(); | |
} | |
this.touchCanceler = false; | |
if (this.settings.capableScrollPage) { | |
const capableScrollPage = this.settings.capableScrollPage(); | |
if (capableScrollPage) { | |
let swipped = this.wasSwiped(); | |
this.swipped = swipped; | |
if (swipped !== 0) { | |
this.snap(swipped); | |
} else { | |
this.scrolling = false; | |
// TODO: cenfeng - 解决点击 epub 左右两侧没有反应的问题,因为这里 this.snap() 执行后会导致 | |
// smoothScrollTo() 的执行,进而导致 scrollTo() 的执行,进而导致翻页已经成功后,又被重置回老的偏移量 | |
// 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; | |
const result = left % snapWidth !== 0; | |
return result; | |
} | |
snap(howMany = 0) { | |
let left = this.scrollLeft; | |
let snapWidth = this.layout.pageWidth * this.layout.divisor; | |
let snapTo = Math.round(left / snapWidth) * snapWidth; | |
if (howMany) { | |
// console.log('epubjs - snap - snap - 来了老弟之前', howMany, snapTo); | |
snapTo += howMany * snapWidth; | |
// console.log('epubjs - snap - snap - 来了老弟之后', howMany, snapTo); | |
} | |
this.snapTarget = snapTo; | |
// console.log('epubjs - snap - snap - howMany', howMany); | |
// console.log('epubjs - snap - snap - left', left); | |
// console.log('epubjs - snap - snap - snapWidth', snapWidth); | |
// console.log('epubjs - snap - snap - snapTo', snapTo); | |
// console.trace(); | |
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); | |
// 如果用户 touch 事件或 resize 事件还未结束,则直接返回 | |
if (this.touchCanceler || this.resizeCanceler) { | |
this.resizeCanceler = false; | |
this.snapping = false; | |
deferred.resolve(); | |
return; | |
} | |
if (time < 1) { | |
window.requestAnimationFrame(tick.bind(this)); | |
const scrollValue = start + (destination - start) * time; | |
this.snapTo = scrollValue; | |
// console.log('epubjs - snap - smoothScrollTo - time < 1', scrollValue); | |
this.scrollTo(scrollValue, 0); | |
} else { | |
// console.log('epubjs - snap - smoothScrollTo - time >= 1', destination); | |
this.snapTo = destination; | |
this.scrollTo(destination, 0); | |
this.snapping = false; | |
this.scrolling = false; | |
deferred.resolve(); | |
this.manager.trimWaitingView(); | |
} | |
} | |
tick.call(this); | |
return deferred.promise; | |
} | |
scrollTo(left = 0, top = 0) { | |
// console.log('xxx epubjs - scrollTo - snap', left); | |
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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment