@eyeear/mswipe
Version:
轮播、焦点图、跑马灯
449 lines (369 loc) • 14 kB
JavaScript
/**
* LBS MSwipe
* Date: 2018-9-8
**/
import utils from './utils'
let _transition_ = utils.prefix('transition')
let _transform_ = utils.prefix('transform')
class MSwipe {
constructor(el, options = {}) {
const opts = Object.assign({}, options)
this.wrapper = utils.query(el)
if (!this.wrapper) return
this.scroller = this.wrapper.children[0]
this.loop = opts.loop == false ? false : true // 循环切换
this.vertical = opts.vertical || false // 默认水平方向
this.showIndicator = opts.showIndicator == false ? false : true
this.indicatorClass = opts.indicatorClass || 'swipe-indicator'
this.indicatorTag = opts.indicatorTag || 'span'
this.activeClass = opts.activeClass || 'active'
this.autoplay = opts.autoplay == 0 ? 0 : 5000
this.scrollTime = opts.duration || 500
this.lazyload = opts.lazyload || false
this.attribute = opts.attribute || 'data-src'
this.index = opts.index || 0
this.step = opts.step || 0 // 手动设置每次滚动的步长
this.locked = opts.locked || false // 锁定手指触发滚动 此时只有autoplay prev next能触发滚动
this.disable = opts.disable || false // 禁止跟随手指移动
this.setup = opts.setup || function() {}
this.scrollStart = opts.scrollStart || function() {}
this.scrollEnd = opts.scrollEnd || function() {}
this.$events = {}
this._init()
}
_init() {
this._initObserve()
this._initIndicator()
this._initLazyLoad()
this._initEvent()
this.refresh()
}
_setup() {
if (!this._hasInstall) {
this._hasInstall = true
if (utils.css(this.wrapper, 'position') === 'static') {
this.wrapper.style.position = 'relative'
}
if (utils.css(this.wrapper, 'overflow') !== 'hidden') {
this.wrapper.style.overflow = 'hidden'
}
this.styleProp = this.vertical ? 'height' : 'width'
}
this.items = Array.from(this.scroller.children)
this.count = this.items.length
this.items.forEach(item => item.style[this.styleProp] = '')
this.wrapperSize = this.vertical ? utils.height(this.wrapper) : utils.width(this.wrapper)
this.itemSize = this.vertical ? utils.height(this.items[0]) : utils.width(this.items[0])
this.items.forEach(item => item.style[this.styleProp] = this.itemSize + 'px')
this.scale = this.wrapperSize / this.itemSize
if (this.scale > 1) {
this.size = this.step > 0 ? this.step : (this.vertical ? utils.outterHeight(this.items[0]) : utils.outterWidth(this.items[0]))
} else {
this.size = this.wrapperSize
}
this.totalSize = this.items.reduce((total, item) => total + (this.vertical ? utils.outterHeight(item) : utils.outterWidth(item)), 0)
this.maxOffset = this.wrapperSize - this.totalSize
this.maxCount = this.scale > 1 ? Math.ceil(Math.abs(this.maxOffset) / this.size) : this.count - 1
this.scroller.style[this.styleProp] = this.totalSize + 'px'
this.offset = 0
this.duration = 0
this._scroll()
this._autoPlay()
this.$emit('setup', this.index)
}
_start(e) {
this.startX = e.touches[0].pageX
this.startY = e.touches[0].pageY
this.startTime = Date.now()
this._resetTouch()
this._resetPosition()
this._clearPlay()
}
_move(e) {
e.stopPropagation()
if (this.vertical) e.preventDefault()
if (this.locked) return
const deltaX = e.touches[0].pageX - this.startX
const deltaY = e.touches[0].pageY - this.startY
this.delta = this.vertical ? deltaY : deltaX
if (this.direction == '') {
this.direction = utils.getDirection(Math.abs(deltaX), Math.abs(deltaY))
}
if (!this.vertical) {
if (this.direction === 'vertical') return
if (this.direction === 'horizontal') e.preventDefault()
}
if (!this.moving) {
this.moving = true
}
if (!this.loop) {
if ((this.index == 0 && this.delta > 0) || (this.index == this.maxCount && this.delta < 0)) this.delta /= 4
}
if (this.disable) return
this._scroll({ offset: this.delta })
}
_end(e) {
e.stopPropagation()
if (!this.moving) {
this._autoPlay()
return
}
const duration = Date.now() - this.startTime
const delta = Math.abs(this.delta)
let pace = 0
if (delta / duration > 0.25 || delta > this.size / 2) {
pace = this.delta > 0 ? -1 : 1
}
this._scroll({
pace: pace,
duration: this.scrollTime
})
}
_resetTouch() {
this.delta = 0
this.direction = ''
this.moving = false
this.duration = 0
}
_resetPosition() {
if (!this.loop) return
if (this.index == -1) {
this._scroll({ pace: this.count })
}
if (this.index == this.count) {
this._scroll({ pace: -this.count })
}
}
_resize() {
this._resizeTimer && clearTimeout(this._resizeTimer)
this._resizeTimer = setTimeout(() => {
this._clearPlay()
this.refresh()
}, 100)
}
_visibility() {
if (document.hidden) {
this._clearPlay()
} else {
this._autoPlay()
}
}
_scroll(ref = {}) {
const offset = ref.offset || 0
const pace = ref.pace || 0
const duration = ref.duration
const targetIndex = this._getTargetIndex(pace)
const targetOffset = this._getTargetOffset(targetIndex, offset)
const oldIndex = this.index
if (this.loop) {
this._setItemTransform(this.items[this.count - 1], targetIndex <= 0 ? -this.totalSize : '')
this._setItemTransform(this.items[0], targetIndex >= this.count - 1 ? this.totalSize : '')
}
if (duration) {
this.duration = duration
if (pace != 0) {
this.scrolling = true
this.$emit('scrollStart', this.oIndex = (oldIndex + this.count) % this.count)
}
}
this.index = targetIndex
this.offset = targetOffset
}
_scrollEnd() {
this.duration = 0
if (this.scrolling) {
this.scrolling = false
this.$emit('scrollEnd', (this.index + this.count) % this.count, this.oIndex)
}
this._autoPlay()
}
_getTargetIndex(pace) {
if (pace == 0) return this.index
const index = this.index + pace
if (this.loop) {
return utils.range(index, -1, this.count)
}
if (this.autoplay) {
if (index > this.maxCount) return 0
if (index < 0) return this.maxCount
return index
}
return utils.range(index, 0, this.maxCount)
}
_getTargetOffset(index, offset) {
let currentPosition = index * this.size
if (!this.loop) {
currentPosition = Math.min(currentPosition, -this.maxOffset)
}
let targetOffset = offset - currentPosition
// if (!this.loop) {
// targetOffset = utils.range(targetOffset, this.maxOffset, 0)
// }
return targetOffset
}
_setTransition(duration = 0) {
this.scroller.style[_transition_] = duration != 0 ? ('all ' + duration + 'ms') : ''
}
_setTransform(offset) {
this._setItemTransform(this.scroller, offset)
this.$emit('scrollMove', offset)
}
_setItemTransform(item, offset) {
if (offset != '') {
item.style[_transform_] = this.vertical ? 'translate3d(0px, ' + offset + 'px, 0px)' : 'translate3d(' + offset + 'px, 0px, 0px)'
} else {
item.style[_transform_] = ''
}
}
_initEvent() {
utils.on(this.wrapper, 'touchstart', this.start = this._start.bind(this))
utils.on(this.wrapper, 'touchmove', this.move = this._move.bind(this))
utils.on(this.wrapper, 'touchend', this.end = this._end.bind(this))
utils.on(this.scroller, ['transitionend', 'webkitTransitionEnd'], this.transitionEnd = this._scrollEnd.bind(this))
utils.on(window, 'resize', this.resize = this._resize.bind(this))
utils.on(window, 'visibilitychange', this.visibility = this._visibility.bind(this))
this.$on('setup', this.setup)
this.$on('scrollStart', this.scrollStart)
this.$on('scrollEnd', this.scrollEnd)
}
_initObserve() {
utils.observe(this, ['offset', 'duration'], () => {
this._setTransition(this.duration)
this._setTransform(this.offset)
})
}
_initIndicator() {
if (!this.showIndicator || this.count < 2) return
let indicatorItems = []
let indicator = document.createElement('div')
let setup = () => {
indicatorItems = []
indicator.innerHTML = ''
for (let i = 0; i < this.count; i++) {
let span = document.createElement(this.indicatorTag)
i === this.index && (span.className = this.activeClass)
indicator.appendChild(span)
indicatorItems.push(span)
}
}
indicator.className = this.indicatorClass
this.wrapper.appendChild(indicator)
this.$on('setup', () => {
setup()
})
this.$on('scrollEnd', (index, oIndex) => {
indicatorItems[oIndex].className = ''
indicatorItems[index].className = this.activeClass
})
}
_initLazyLoad() {
if (!this.lazyload) return
const loadImage = (item) => {
if (!item) return
if (item.__loaded) return
const imgs = item.getElementsByTagName('img')
const n = imgs.length
if (!n) return
item.__loaded = true
for (let i = 0; i < n; i++) {
let img = imgs[i]
if (img.getAttribute(this.attribute)) {
img.classList.add('loaded')
img.setAttribute('src', img.getAttribute(this.attribute))
img.removeAttribute(this.attribute)
}
}
}
const lazyLoad = (index) => {
if (!(this.scale > 1)) return loadImage(this.items[index])
for (let i = 0; i < this.count; i++) {
let item = this.items[i]
if (item.__loaded) continue
const rect = item.getBoundingClientRect()
const wrapperRect = this.wrapper.getBoundingClientRect()
const visible = this.vertical ? (rect.top < wrapperRect.bottom && rect.bottom > wrapperRect.top) : (rect.left < wrapperRect.right && rect.right > wrapperRect.left)
if (visible) {
loadImage(item)
}
}
}
this.$on('setup', index => {
lazyLoad(index)
})
this.$on('scrollEnd', index => {
lazyLoad(index)
})
}
_autoPlay() {
if (this.autoplay < 1 || this.count < 2) return
this._clearPlay()
this._autoTimer = setTimeout(() => {
this.next()
this._autoPlay()
}, this.autoplay)
}
_clearPlay() {
clearTimeout(this._autoTimer)
this._autoTimer = null
}
$emit(type) {
if (!this.$events[type]) return
let n = this.$events[type].length
if (!n) return
for (let i = 0; i < n; i++) {
this.$events[type][i].apply(this, [].slice.call(arguments, 1))
}
}
$on(type, fn) {
if (!this.$events[type]) this.$events[type] = []
this.$events[type].push(fn)
}
$off(type, fn) {
if (!this.$events[type]) return
let index = this.$events[type].indexOf(fn)
if (index > -1) {
this.$events[type].splice(index, 1)
}
}
refresh() {
this._setup()
this.$emit('refresh')
}
prev() {
if (this.scrolling) return
if (this.destroyed) return
this._clearPlay()
this._resetPosition()
setTimeout(() => {
this._scroll({
pace: -1,
duration: this.scrollTime
})
}, 100)
}
next() {
if (this.scrolling) return
if (this.destroyed) return
this._clearPlay()
this._resetPosition()
setTimeout(() => {
this._scroll({
pace: 1,
duration: this.scrollTime
})
}, 100)
}
destroy() {
this.destroyed = true
this.$emit('destroy')
this.$events = {}
this._clearPlay()
utils.off(this.wrapper, 'touchstart', this.start)
utils.off(this.wrapper, 'touchmove', this.move)
utils.off(this.wrapper, 'touchend', this.end)
utils.off(this.scroller, ['transitionend', 'webkitTransitionEnd'], this.transitionEnd)
utils.off(window, 'resize', this.resize)
utils.off(window, 'visibilitychange', this.visibility)
}
}
export default MSwipe