saven
Version:
535 lines (490 loc) • 15 kB
JavaScript
import Nerv from 'nervjs'
import classNames from 'classnames'
import './style/swiper.scss'
import _ from '../../../utils/parse-type'
/**
* props 类型检测
*
* @param {Object} props
*/
function parseType (props) {
const {
current
} = props
// 抛出错误信息
const throwErrorMsg = type => {
throw new TypeError(type + ' must be number')
}
if (current) _.isNumber(current) ? '' : throwErrorMsg('current')
}
class Swiper extends Nerv.Component {
constructor (props) {
super(props)
parseType(this.props)
this.state = {
currentIndex: parseInt(this.props.current),
containerWidth: 0,
containerHeight: 0,
// touch
touching: false,
og: 0,
ogTranslate: 0,
touchId: undefined,
translate: 0,
animating: false
}
this.SwiperTimer = null
this.handleTouchStart = this.handleTouchStart.bind(this)
this.handleTouchMove = this.handleTouchMove.bind(this)
this.handleTouchEnd = this.handleTouchEnd.bind(this)
this.pauseAutoPlay = this.pauseAutoPlay.bind(this)
this.autoplay = this.autoplay.bind(this)
this.computedChangeContainer = this.computedChangeContainer.bind(this)
this.updateContainerBox = this.updateContainerBox.bind(this)
}
componentDidMount () {
this.updateContainerBox(this.props.children)
if (this.props.autoplay) this.autoplay(this.props.interval)
}
componentWillReceiveProps (nextProps) {
this.updateContainerBox(nextProps.children)
const { interval, autoplay, circular } = nextProps
this.pauseAutoPlay()
// this.updateCurrentIndex(current)
if (!circular) {
this.computedChangeContainer()
}
if (!autoplay) return
this.autoplay(interval)
}
componentWillUnmount () {
this.pauseAutoPlay()
}
// 在开关衔接滑动的时候,计算当前下标
computedChangeContainer () {
if (this.props.children.length - 1 === this.state.currentIndex) {
this.pauseAutoPlay()
return true
}
return false
}
// 更新容器的宽高
updateContainerBox (children) {
let $container = Nerv.findDOMNode(this.SwiperWp)
let childLen = children.length || 1
let currentIndex = this.state.currentIndex
// 默认偏移量
let offsetVal = this.props.vertical
? -$container.offsetHeight * (currentIndex + 1)
: -$container.offsetWidth * (currentIndex + 1)
childLen = childLen + 2
this.setState({
containerWidth: $container.offsetWidth, // 外层容器宽
containerHeight: $container.offsetHeight, // 外层容器高
wrapperWidth: !this.props.vertical // 轮播容器 是否纵向滑动,不是的话计算总宽度
? $container.offsetWidth * childLen
: $container.offsetWidth,
wrapperHeight: this.props.vertical // 轮播容器 是否纵向滑动,是的话计算总高度
? $container.offsetHeight * childLen
: $container.offsetHeight,
// 计算指定下标位置
translate: offsetVal
})
}
// 更新下标
updateCurrentIndex (currentIndex) {
let cur = currentIndex === this.props.children.length - 1 ? 0 : currentIndex
let tr = this.state.translate
let slideVal // 纵向还是横向滚动长度
if (!this.props.vertical) {
slideVal = this.state.containerWidth * Math.abs(currentIndex - this.state.currentIndex)
} else {
slideVal = this.state.containerHeight * Math.abs(currentIndex - this.state.currentIndex)
}
this.setState(
{
animating: true,
translate: tr - slideVal,
currentIndex: cur
},
() => {
setTimeout(() => {
this.computedTranslate()
}, this.props.duration)
}
)
}
// 判断当前是否在最后一页
isLastIndex () {
return this.state.currentIndex === this.props.children.length - 1
}
// 判断当前是否在第一页
isFirstIndex () {
return this.state.currentIndex === 0
}
handleTouchStart (e) {
if (this.state.animating) return
if (this.state.touching || this.props.children.length <= 1) return
if (this.props.autoplay) this.pauseAutoPlay()
let og = 0
if (!this.props.vertical) {
og = e.targetTouches[0].pageX - this.state.translate
} else {
og = e.targetTouches[0].pageY - this.state.translate
}
this.touchStartX = e.touches[0].pageX
this.touchStartY = e.touches[0].pageY
this.setState({
touching: true,
ogTranslate: this.state.translate,
touchId: e.targetTouches[0].identifier,
og: og,
animating: false
})
}
handleTouchMove (e) {
// 非衔接滑动的时候 判断首尾页的时候禁止拖动
if (!this.props.circular) {
// 计算偏移量、 判断左滑右滑动
let touchEndX = e.touches[0].pageX
let touchEndY = e.touches[0].pageY
let offsetMoveX = touchEndX - this.touchStartX
let offsetMoveY = touchEndY - this.touchStartY
if (this.isFirstIndex()) {
if (
(this.props.vertical && offsetMoveY > 0) ||
(!this.props.vertical && offsetMoveX > 0)
) {
return
}
}
if (this.isLastIndex()) {
if (
(this.props.vertical && offsetMoveY < 0) ||
(!this.props.vertical && offsetMoveX < 0)
) {
return
}
}
}
if (!this.state.touching || this.props.children.length <= 1) return
if (e.targetTouches[0].identifier !== this.state.touchId) return
e.preventDefault()
let diff = this.state.translate
if (!this.props.vertical) {
const pageX = e.targetTouches[0].pageX
diff = pageX - this.state.og
} else {
// vertical
const pageY = e.targetTouches[0].pageY
diff = pageY - this.state.og
}
this.setState({
translate: diff
})
}
handleTouchEnd (e) {
if (!this.state.touching || this.props.children.length <= 1) return
let translate = this.state.translate
let max = !this.props.vertical
? this.state.wrapperWidth - this.state.containerWidth
: this.state.wrapperHeight - this.state.containerHeight
let currentIndex = this.state.currentIndex
let ogIndex = currentIndex
if (translate > 0) {
// start
translate = 0
} else if (translate < -max) {
translate = -max
} else {
// default case
let changeV = this.isChangeSlide(translate, currentIndex)
translate = changeV.translate
currentIndex = changeV.currentIndex
}
this.setState(
{
touching: false,
og: 0,
touchId: undefined,
ogTranslate: 0,
animating: true,
translate,
currentIndex
},
() =>
setTimeout(() => {
this.computedTranslate()
}, this.props.duration)
)
if (this.props.onChange) {
Object.defineProperty(e, 'detail', {
enumerable: true,
value: {
current: currentIndex,
source: 'touch'
}
})
this.props.onChange(e)
}
if (this.props.autoplay) this.pauseAutoPlay()
}
computedTranslate () {
// if (this.props.circular) {
let prvTranslate = this.state.translate
// 横纵向
if (!this.props.vertical) {
if (prvTranslate === 0) {
prvTranslate = -(
this.state.wrapperWidth -
this.state.containerWidth * 2
)
}
// 减一个宽度是因为最后的静默回滚到最前面 滑块本身还有一个 translate位移量
if (
prvTranslate === -(this.state.wrapperWidth - this.state.containerWidth)
) {
prvTranslate = -this.state.containerWidth
}
} else {
if (prvTranslate === 0) {
prvTranslate = -(
this.state.wrapperHeight -
this.state.containerHeight * 2
)
}
// 减一个宽度是因为最后的静默回滚到最前面 滑块本身还有一个 translate位移量
if (
prvTranslate ===
-(this.state.wrapperHeight - this.state.containerHeight)
) {
prvTranslate = -this.state.containerHeight
}
}
this.setState({
animating: false,
translate: prvTranslate
})
// } else {
// this.setState({animating: false})
// }
}
// 自增下标点计算
addCurrentIndex (currentIndex) {
return currentIndex === this.props.children.length - 1
? 0
: (currentIndex += 1)
}
autoplay (interval) {
this.SwiperTimer = setInterval(() => {
this.slideNext()
}, interval)
}
pauseAutoPlay () {
clearInterval(this.SwiperTimer)
}
slideNext () {
if (!this.props.circular) {
if (this.computedChangeContainer()) {
return
}
}
let cur = this.addCurrentIndex(this.state.currentIndex)
let tr = this.state.translate
let slideVal // 纵向还是横向滚动长度
if (!this.props.vertical) {
slideVal = this.state.containerWidth
} else {
slideVal = this.state.containerHeight
}
this.setState(
{
animating: true,
translate: tr - slideVal,
currentIndex: cur
},
() => {
setTimeout(() => {
this.computedTranslate()
}, this.props.duration)
}
)
if (this.props.onChange) {
let e = new TouchEvent('touchend')
Object.defineProperty(e, 'detail', {
enumerable: true,
value: {
current: cur,
source: 'autoplay'
}
})
this.props.onChange(e)
}
}
isChangeSlide (translate, currentIndex) {
// 判读滑动到大于一半才过去
let threshold = !this.props.vertical
? this.state.containerWidth / 2
: this.state.containerHeight / 2
let diff = Math.abs(translate - this.state.ogTranslate)
let isNext = translate - this.state.ogTranslate < 0
if (diff > threshold) {
if (isNext) {
// next slide
translate =
this.state.ogTranslate -
(!this.props.vertical
? this.state.containerWidth
: this.state.containerHeight)
currentIndex = this.addCurrentIndex(currentIndex)
} else {
// prev slide
translate =
this.state.ogTranslate +
(!this.props.vertical
? this.state.containerWidth
: this.state.containerHeight)
currentIndex =
currentIndex === 0
? (currentIndex = this.props.children.length - 1)
: (currentIndex -= 1)
}
} else {
// revert back
translate = this.state.ogTranslate
}
return { translate, currentIndex }
}
renderPagination (indicatorColor, indicatorActiveColor) {
if (Array.isArray(this.props.children)) {
const childs = this.props.children.map((child, i) => {
let clx = classNames('swiper__pagination-bullet', {
active: i === this.state.currentIndex
})
let indiStyle = {
background:
i === this.state.currentIndex ? indicatorActiveColor : indicatorColor
}
return <span className={clx} key={i} style={indiStyle} />
})
return childs
} else {
let indiStyle = {
background: indicatorActiveColor
}
return <span className={'swiper__pagination-bullet active'} key='1' style={indiStyle} />
}
}
render () {
const {
className,
indicatorDots,
vertical,
children,
circular,
duration
} = this.props
const cls = classNames('swiper__container', className, {
'swiper__container-vertical': vertical,
'swiper__container-horizontal': !vertical
})
let items = [].concat(children)
// 衔接滑动增加首尾
// if (circular) {
if (items.length !== 0) {
const firstItem = items[0]
const lastItem = items[items.length - 1]
items.push(firstItem)
items.unshift(lastItem)
}
// }
let wrapperStyle = {
width: this.state.wrapperWidth,
height: this.state.wrapperHeight,
transition: this.state.animating
? `transform ${duration}ms ease-in-out`
: 'none',
transform: `translate(${!vertical ? this.state.translate : 0}px, ${
vertical ? this.state.translate : 0
}px)`
}
return (
<div
className={cls}
ref={SwiperWp => {
this.SwiperWp = SwiperWp
}}
onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouchEnd={this.handleTouchEnd}
>
<div className='swiper__wrapper' style={wrapperStyle}>
{items.map((child, i) => {
const c = child.props.children
// 没有子元素直接返回
if (c.length === 0) return <div className='swiper__item' />
const cls = classNames('swiper__item', c.props.className)
// 样式继承追加。 有可能 Object 或者是 String
let sty
if (typeof c.props.style === 'string') {
let display = !vertical
? ';display: inline-block;'
: ';display: block;'
let verticalAlign = !vertical
? ';vertical-align: top;'
: ';vertical-align:bottom;'
let w = `;width: ${this.state.containerWidth}px;`
let h = `;height: ${this.state.containerHeight}px;`
sty = c.props.style + verticalAlign + display + w + h
} else {
sty = Object.assign({}, c.props.style, {
display: !vertical ? 'inline-block' : 'block',
verticalAlign: !vertical ? 'top' : 'bottom',
width: this.state.containerWidth,
height: this.state.containerHeight
})
}
if (circular) i = i - 1
return Nerv.cloneElement(c, {
key: i,
className: cls,
style: sty,
onClick: child.props.onClick
})
})}
</div>
{indicatorDots ? (
<div className='swiper__pagination'>
{this.renderPagination(
this.props.indicatorColor,
this.props.indicatorActiveColor
)}
</div>
) : (
false
)}
</div>
)
}
}
// 默认配置
Swiper.defaultProps = {
indicatorDots: false, // 是否显示面板指示点
indicatorColor: 'rgba(0, 0, 0, .3)', // 指示点颜色
indicatorActiveColor: '#000000', // 当前选中的指示点颜色
autoplay: false, // 自动播放
current: 0, // 当前滑块位置
interval: 5000, // 切换时间间隔
duration: 500, // 滑动动画时长
circular: false, // 是否采用衔接滑动
vertical: false // 滑动方向是否为纵向
}
class SwiperItem extends Nerv.Component {
constructor (props) {
super(props)
}
render () {
return ''
}
}
export { SwiperItem, Swiper }