| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| import './slider.css' |
| |
| const TRANSITION_TIME = 400 |
| const NEIGHBOR_SCALE_TIME = 100 |
| const MAIN_SLIDE_OPACITY = 1 |
| const THROTTLE_SCROLL_TIME = 25 |
| const INTERVAL_MINIMUM = 200 |
| |
| export default { |
| created () { |
| this._clones = [] |
| this.innerOffset = 0 |
| this._indicator = null |
| }, |
| |
| beforeUpdate () { |
| this._getWrapperSize() |
| }, |
| |
| updated () { |
| const children = this.$children |
| const len = children && children.length |
| if (children && len > 0) { |
| for (let i = 0; i < len; i++) { |
| const vm = children[i] |
| if (vm.$options._componentTag === 'indicator' |
| || vm.$vnode.data.ref === 'indicator') { |
| vm._watcher.get() |
| break |
| } |
| } |
| } |
| weex.utils.fireLazyload(this.$el, true) |
| if (this._preIndex !== this.currentIndex) { |
| this._slideTo(this.currentIndex) |
| } |
| }, |
| |
| mounted () { |
| this._getWrapperSize() |
| this._slideTo(this.currentIndex) |
| weex.utils.fireLazyload(this.$el, true) |
| }, |
| |
| methods: { |
| _getWrapperSize () { |
| const wrapper = this.$refs.wrapper |
| if (wrapper) { |
| const rect = wrapper.getBoundingClientRect() |
| this._wrapperWidth = rect.width |
| this._wrapperHeight = rect.height |
| } |
| }, |
| |
| _formatChildren (createElement) { |
| const children = this.$slots.default || [] |
| let indicatorVnode |
| const cells = children.filter(vnode => { |
| if (!vnode.tag) return false |
| if (vnode.componentOptions && vnode.componentOptions.tag === 'indicator') { |
| indicatorVnode = vnode |
| return false |
| } |
| return true |
| }).map(vnode => { |
| return createElement('li', { |
| ref: 'cells', |
| staticClass: `weex-slider-cell weex-ct${this.isNeighbor ? ' neighbor-cell' : ''}` |
| }, [vnode]) |
| }) |
| if (indicatorVnode) { |
| indicatorVnode.data.attrs = indicatorVnode.data.attrs || {} |
| indicatorVnode.data.attrs.count = cells.length |
| indicatorVnode.data.attrs.active = this.currentIndex |
| this._indicator = indicatorVnode |
| } |
| return cells |
| }, |
| |
| _renderSlides (createElement) { |
| this._cells = this._formatChildren(createElement) |
| this.frameCount = this._cells.length |
| return createElement( |
| 'nav', |
| { |
| ref: 'wrapper', |
| attrs: { 'weex-type': this.isNeighbor ? 'slider-neighbor' : 'slider' }, |
| on: weex.createEventMap( |
| this, |
| ['scroll', 'scrollstart', 'scrollend'], |
| { |
| touchstart: this._handleTouchStart, |
| touchmove: weex.utils.throttle(weex.utils.bind(this._handleTouchMove, this), 25), |
| touchend: this._handleTouchEnd, |
| touchcancel: this._handleTouchCancel |
| } |
| ), |
| staticClass: 'weex-slider weex-slider-wrapper weex-ct', |
| staticStyle: weex.extractComponentStyle(this) |
| }, |
| [ |
| createElement('ul', { |
| ref: 'inner', |
| staticClass: 'weex-slider-inner weex-ct' |
| }, this._cells), |
| this._indicator |
| ] |
| ) |
| }, |
| |
| // get standard index |
| _normalizeIndex (index) { |
| const newIndex = (index + this.frameCount) % this.frameCount |
| return Math.min(Math.max(newIndex, 0), this.frameCount - 1) |
| }, |
| |
| _startAutoPlay () { |
| if (!this.autoPlay || this.autoPlay === 'false') { |
| return |
| } |
| if (this._autoPlayTimer) { |
| clearTimeout(this._autoPlayTimer) |
| this._autoPlayTimer = null |
| } |
| let interval = parseInt(this.interval - TRANSITION_TIME - NEIGHBOR_SCALE_TIME) |
| interval = interval > INTERVAL_MINIMUM ? interval : INTERVAL_MINIMUM |
| this._autoPlayTimer = setTimeout(weex.utils.bind(this._next, this), interval) |
| }, |
| |
| _stopAutoPlay () { |
| if (this._autoPlayTimer) { |
| clearTimeout(this._autoPlayTimer) |
| this._autoPlayTimer = null |
| } |
| }, |
| |
| _slideTo (index, isTouchScroll) { |
| if (this.frameCount <= 0) { |
| return |
| } |
| if (!this.infinite || this.infinite === 'false') { |
| if (index === -1 || index > (this.frameCount - 1)) { |
| this._slideTo(this.currentIndex) |
| return |
| } |
| } |
| |
| if (!this._preIndex && this._preIndex !== 0) { |
| if (this._showNodes && this._showNodes[0]) { |
| this._preIndex = this._showNodes[0].index |
| } |
| else { |
| this._preIndex = this.currentIndex |
| } |
| } |
| |
| if (this._sliding) { |
| return |
| } |
| this._sliding = true |
| |
| const newIndex = this._normalizeIndex(index) |
| const inner = this.$refs.inner |
| const step = this._step = this.frameCount <= 1 ? 0 : this._preIndex - index |
| |
| if (inner) { |
| this._prepareNodes() |
| const translate = weex.utils.getTransformObj(inner).translate |
| const match = translate && translate.match(/translate[^(]+\(([+-\d.]+)/) |
| const innerX = match && match[1] || 0 |
| const dist = innerX - this.innerOffset |
| this.innerOffset += step * this._wrapperWidth |
| // transform the whole slides group. |
| inner.style.webkitTransition = `-webkit-transform ${TRANSITION_TIME / 1000}s ease-in-out` |
| inner.style.mozTransition = `transform ${TRANSITION_TIME / 1000}s ease-in-out` |
| inner.style.transition = `transform ${TRANSITION_TIME / 1000}s ease-in-out` |
| inner.style.webkitTransform = `translate3d(${this.innerOffset}px, 0, 0)` |
| inner.style.mozTransform = `translate3d(${this.innerOffset}px, 0, 0)` |
| inner.style.transform = `translate3d(${this.innerOffset}px, 0, 0)` |
| |
| // emit scroll events. |
| if (!isTouchScroll) { |
| this._emitScrollEvent('scrollstart') |
| } |
| setTimeout(() => { |
| this._throttleEmitScroll(dist, () => { |
| this._emitScrollEvent('scrollend') |
| }) |
| }, THROTTLE_SCROLL_TIME) |
| |
| this._loopShowNodes(step) |
| |
| setTimeout(() => { |
| if (this.isNeighbor) { |
| this._setNeighbors() |
| } |
| |
| setTimeout(() => { |
| inner.style.webkitTransition = '' |
| inner.style.mozTransition = '' |
| inner.style.transition = '' |
| for (let i = this._showStartIdx; i <= this._showEndIdx; i++) { |
| const node = this._showNodes[i] |
| if (!node) { continue } |
| const elm = node.firstElementChild |
| elm.style.webkitTransition = '' |
| elm.style.mozTransition = '' |
| elm.style.transition = '' |
| } |
| // clean cloned nodes and rearrange slide cells. |
| this._rearrangeNodes(newIndex) |
| }, NEIGHBOR_SCALE_TIME) |
| }, TRANSITION_TIME) |
| } |
| |
| if (newIndex !== this._preIndex) { |
| this.$emit('change', weex.utils.createEvent(this.$el, 'change', { |
| index: newIndex |
| })) |
| } |
| }, |
| |
| _clearNodesOffset () { |
| const end = this._showEndIdx |
| for (let i = this._showStartIdx; i <= end; i++) { |
| let node = this._showNodes[i] |
| node = node && node.firstElementChild |
| if (!node) { continue } |
| weex.utils.addTransform(this._showNodes[i].firstElementChild, { |
| translate: 'translate3d(0px, 0px, 0px)' |
| }) |
| } |
| }, |
| |
| _loopShowNodes (step) { |
| if (!step || this.frameCount <= 1) { |
| return |
| } |
| const sign = step > 0 ? 1 : -1 |
| let i = step <= 0 ? this._showStartIdx : this._showEndIdx |
| const end = step <= 0 ? this._showEndIdx : this._showStartIdx |
| for (; i !== end - sign; i -= sign) { |
| const nextIdx = i + step |
| this._showNodes[nextIdx] = this._showNodes[i] |
| this._showNodes[nextIdx]._showIndex = nextIdx |
| delete this._showNodes[i] |
| } |
| this._showStartIdx += step |
| this._showEndIdx += step |
| }, |
| |
| _prepareNodes () { |
| // test if the next slide towards the direction exists. |
| // e.g. currentIndex 0 -> 1: should prepare 4 slides: -1, 0, 1, 2 |
| // if not, translate a node to here, or just clone it. |
| const step = this._step |
| if (!this._inited) { |
| this._initNodes() |
| this._inited = true |
| this._showNodes = {} |
| } |
| if (this.frameCount <= 1) { |
| this._showStartIdx = this._showEndIdx = 0 |
| const node = this._cells[0].elm |
| node.style.opacity = 1 |
| node.style.zIndex = 99 |
| node.index = 0 |
| this._showNodes[0] = node |
| node._inShow = true |
| node._showIndex = 0 |
| return |
| } |
| const showCount = this._showCount = Math.abs(step) + 3 |
| this._showStartIdx = step <= 0 ? -1 : 2 - showCount |
| this._showEndIdx = step <= 0 ? showCount - 2 : 1 |
| this._clearNodesOffset() |
| this._positionNodes(this._showStartIdx, this._showEndIdx, step) |
| }, |
| |
| _initNodes () { |
| const total = this.frameCount |
| const cells = this._cells |
| for (let i = 0; i < total; i++) { |
| const node = cells[i].elm |
| node.index = i |
| node._inShow = false |
| node.style.zIndex = 0 |
| node.style.opacity = 0 |
| } |
| }, |
| |
| _positionNodes (begin, end, step, anim) { |
| const cells = this._cells |
| const start = step <= 0 ? begin : end |
| const stop = step <= 0 ? end : begin |
| const sign = step <= 0 ? -1 : 1 |
| let cellIndex = this._preIndex + sign |
| for (let i = start; i !== stop - sign; i = i - sign) { |
| const node = cells[this._normalizeIndex(cellIndex)].elm |
| cellIndex = cellIndex - sign |
| this._positionNode(node, i) |
| } |
| }, |
| |
| /** |
| * index: position index in the showing cells' view. |
| */ |
| _positionNode (node, index) { |
| const holder = this._showNodes[index] |
| if (node._inShow && holder !== node) { |
| if (holder) { this._removeClone(holder) } |
| node = this._getClone(node.index) |
| } |
| else if (node._inShow) { |
| return |
| } |
| |
| node._inShow = true |
| const translateX = index * this._wrapperWidth - this.innerOffset |
| weex.utils.addTransform(node, { |
| translate: `translate3d(${translateX}px, 0px, 0px)` |
| }) |
| node.style.zIndex = 99 - Math.abs(index) |
| node.style.opacity = 1 |
| node._showIndex = index |
| this._showNodes[index] = node |
| }, |
| |
| _getClone (index) { |
| let arr = this._clones[index] |
| if (!arr) { |
| this._clones[index] = arr = [] |
| } |
| if (arr.length <= 0) { |
| const origNode = this._cells[index].elm |
| const clone = origNode.cloneNode(true) |
| clone._isClone = true |
| clone._inShow = origNode._inShow |
| clone.index = origNode.index |
| clone.style.opacity = 0 |
| clone.style.zIndex = 0 |
| const ct = this.$refs.inner |
| ct.appendChild(clone) |
| arr.push(clone) |
| } |
| return arr.pop() |
| }, |
| |
| _removeClone (node) { |
| const idx = node.index |
| this._hideNode(node) |
| const arr = this._clones[idx] |
| arr.push(node) |
| }, |
| |
| _hideNode (node) { |
| node._inShow = false |
| node.style.opacity = 0 |
| node.style.zIndex = 0 |
| }, |
| |
| /** |
| * hide nodes from begin to end in showArray. |
| * if it is clone node, just move the clone node to the buffer. |
| */ |
| _clearNodes (begin, end) { |
| for (let i = begin; i <= end; i++) { |
| const node = this._showNodes[i] |
| if (!node) { return } |
| if (node._isClone) { |
| this._removeClone(node) |
| } |
| else if (!node._inShow) { |
| this._hideNode(node) |
| } |
| delete this._showNodes[i] |
| } |
| }, |
| |
| /** |
| * copy node style props (opacity and zIndex) and transform status from |
| * one element to another. |
| */ |
| _copyStyle (from, to, styles = ['opacity', 'zIndex'], transformExtra = {}) { |
| weex.utils.extendKeys(to.style, from.style, styles) |
| const transObj = weex.utils.getTransformObj(from) |
| for (const k in transformExtra) { |
| transObj[k] = transformExtra[k] |
| } |
| weex.utils.addTransform(to, transObj) |
| const fromInner = from.firstElementChild |
| const toInner = to.firstElementChild |
| toInner.style.opacity = fromInner.style.opacity |
| weex.utils.copyTransform(fromInner, toInner) |
| }, |
| |
| /** |
| * replace a clone node with the original node if it's not in use. |
| */ |
| _replaceClone (clone, pos) { |
| const origNode = this._cells[clone.index].elm |
| if (origNode._inShow) { |
| return |
| } |
| const origShowIndex = origNode._showIndex |
| const styleProps = ['opacity', 'zIndex'] |
| let cl |
| if (Math.abs(origShowIndex) <= 1) { |
| // leave a clone to replace the origNode in the show zone(-1 ~ 1). |
| cl = this._getClone(origNode.index) |
| this._copyStyle(origNode, cl) |
| this._showNodes[origShowIndex] = cl |
| } |
| origNode._inShow = true |
| const transObj = weex.utils.getTransformObj(clone) |
| transObj.translate = transObj.translate.replace(/[+-\d.]+[pw]x/, ($0) => { |
| return pos * this._wrapperWidth - this.innerOffset + 'px' |
| }) |
| this._copyStyle(clone, origNode, styleProps, transObj) |
| this._removeClone(clone) |
| if (!cl) { |
| delete this._showNodes[origShowIndex] |
| } |
| this._showNodes[pos] = origNode |
| origNode._showIndex = pos |
| }, |
| |
| _rearrangeNodes (newIndex) { |
| if (this.frameCount <= 1) { |
| this._sliding = false |
| this.currentIndex = 0 |
| return |
| } |
| |
| // clear autoPlay timer (and restart after updated hook). |
| this._startAutoPlay() |
| |
| /** |
| * clean nodes. replace current node with non-cloned node. |
| * set current index to the new index. |
| */ |
| const shows = this._showNodes |
| for (let i = this._showStartIdx; i <= this._showEndIdx; i++) { |
| shows[i]._inShow = false |
| } |
| for (let i = -1; i <= 1; i++) { |
| const node = shows[i] |
| if (!node._isClone) { |
| node._inShow = true |
| } |
| else { |
| this._replaceClone(node, i) |
| } |
| } |
| |
| this._clearNodes(this._showStartIdx, -2) |
| this._showStartIdx = -1 |
| this._clearNodes(2, this._showEndIdx) |
| this._showEndIdx = 1 |
| this._sliding = false |
| |
| // set current index to the new index. |
| this.currentIndex = newIndex |
| this._preIndex = newIndex |
| }, |
| |
| /** |
| * according to the attrs: neighborScale, neighborAlpha, neighborSpace. |
| * 1. apply the main cell transform effects. |
| * 2. set the previous cell and the next cell's positon, scale and alpha. |
| * 3. set other cells' scale and alpha. |
| */ |
| _setNeighbors () { |
| for (let i = this._showStartIdx; i <= this._showEndIdx; i++) { |
| const elm = this._showNodes[i].firstElementChild |
| elm.style.webkitTransition = `all ${NEIGHBOR_SCALE_TIME / 1000}s ease` |
| elm.style.mozTransition = `all ${NEIGHBOR_SCALE_TIME / 1000}s ease` |
| elm.style.transition = `all ${NEIGHBOR_SCALE_TIME / 1000}s ease` |
| const transObj = { |
| scale: `scale(${i === 0 ? this.currentItemScale : this.neighborScale})` |
| } |
| let translateX |
| if (!this._neighborWidth) { |
| this._neighborWidth = parseFloat(elm.style.width) || elm.getBoundingClientRect().width |
| } |
| // calculate position offsets according to neighbor scales. |
| if (Math.abs(i) === 1) { |
| const dist = ((this._wrapperWidth - this._neighborWidth * this.neighborScale) / 2 |
| + this.neighborSpace * weex.config.env.scale) / this.neighborScale |
| translateX = -i * dist |
| } |
| else { |
| // clear position offsets. |
| translateX = 0 |
| } |
| transObj.translate = `translate3d(${translateX}px, 0px, 0px)` |
| weex.utils.addTransform(elm, transObj) |
| elm.style.opacity = i === 0 ? MAIN_SLIDE_OPACITY : this.neighborAlpha |
| } |
| }, |
| |
| _next () { |
| let next = this.currentIndex + 1 |
| if (this.frameCount <= 1) { |
| next-- |
| } |
| this._slideTo(next) |
| }, |
| |
| _prev () { |
| let prev = this.currentIndex - 1 |
| if (this.frameCount <= 1) { |
| prev++ |
| } |
| this._slideTo(prev) |
| }, |
| |
| _handleTouchStart (event) { |
| const touch = event.changedTouches[0] |
| this._stopAutoPlay() |
| const inner = this.$refs.inner |
| this._touchParams = { |
| originalTransform: inner.style.webkitTransform |
| || inner.style.mozTransform |
| || inner.style.transform, |
| startTouchEvent: touch, |
| startX: touch.pageX, |
| startY: touch.pageY, |
| timeStamp: event.timeStamp |
| } |
| }, |
| |
| _handleTouchMove (event) { |
| const tp = this._touchParams |
| if (!tp) { return } |
| if (this._sliding) { |
| if (process.env.NODE_ENV === 'development') { |
| console.warn(`[vue-render] warn: can't scroll the slider during sliding.`) |
| } |
| return |
| } |
| const { startX, startY } = this._touchParams |
| const touch = event.changedTouches[0] |
| const offsetX = touch.pageX - startX |
| const offsetY = touch.pageY - startY |
| tp.offsetX = offsetX |
| tp.offsetY = offsetY |
| let isV = tp.isVertical |
| if (typeof isV === 'undefined') { |
| isV = tp.isVertical = Math.abs(offsetX) < Math.abs(offsetY) |
| if (!isV) { |
| this._emitScrollEvent('scrollstart') |
| } |
| } |
| // vertical scroll. just ignore it. |
| if (isV) { |
| return |
| } |
| // horizontal scroll. trigger scroll event. |
| event.preventDefault() |
| const inner = this.$refs.inner |
| if (inner && offsetX) { |
| if (!this._nodesOffsetCleared) { |
| this._nodesOffsetCleared = true |
| this._clearNodesOffset() |
| } |
| this._emitScrollEvent('scroll', { |
| offsetXRatio: offsetX / this._wrapperWidth |
| }) |
| inner.style.webkitTransform = `translate3d(${this.innerOffset + offsetX}px, 0, 0)` |
| inner.style.mozTransform = `translate3d(${this.innerOffset + offsetX}px, 0, 0)` |
| inner.style.transform = `translate3d(${this.innerOffset + offsetX}px, 0, 0)` |
| } |
| }, |
| |
| _handleTouchEnd (event) { |
| this._startAutoPlay() |
| const tp = this._touchParams |
| if (!tp) { return } |
| const isV = tp.isVertical |
| if (typeof isV === 'undefined') { |
| return |
| } |
| const inner = this.$refs.inner |
| const { offsetX } = tp |
| if (inner) { |
| this._nodesOffsetCleared = false |
| // TODO: test the velocity if it's less than 0.2. |
| const reset = Math.abs(offsetX / this._wrapperWidth) < 0.2 |
| const direction = offsetX > 0 ? 1 : -1 |
| const newIndex = reset ? this.currentIndex : (this.currentIndex - direction) |
| this._slideTo(newIndex, true) |
| } |
| delete this._touchParams |
| }, |
| |
| _handleTouchCancel (event) { |
| return this._handleTouchEnd(event) |
| }, |
| |
| _emitScrollEvent (type, data = {}) { |
| this.$emit(type, weex.utils.createEvent(this.$el, type, data)) |
| }, |
| |
| _throttleEmitScroll (offset, callback) { |
| let i = 0 |
| const throttleTime = THROTTLE_SCROLL_TIME |
| const cnt = parseInt(TRANSITION_TIME / throttleTime) - 1 |
| const sign = offset > 0 ? 1 : -1 |
| const r = Math.abs(offset / this._wrapperWidth) |
| const throttledScroll = () => { |
| if (++i > cnt) { |
| return callback && callback.call(this) |
| } |
| const ratio = this._step === 0 |
| ? sign * r * (1 - i / cnt) |
| : sign * (r + (1 - r) * i / cnt) |
| this._emitScrollEvent('scroll', { |
| offsetXRatio: ratio |
| }) |
| setTimeout(throttledScroll, THROTTLE_SCROLL_TIME) |
| } |
| throttledScroll() |
| } |
| } |
| } |