blob: da06d7990f0662fecfec26b0d15b1490df58a156 [file] [log] [blame]
/*
* 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.
*/
/* global lib */
'use strict'
import './neighbor.css'
const DEFAULT_INTERVAL = 3000
const DEFAULT_NEIGHBOR_SPACE = 20
const DEFAULT_NEIGHBOR_ALPHA = 0.6
const DEFAULT_NEIGHBOR_SCALE = 0.8
const TRANSITION_DURATION = 400
const MAIN_SLIDE_SCALE = 0.9
const MAIN_SLIDE_OPACITY = 1
let extend, Component
function idleWhenPageDisappear (slider) {
function handlePageShow () {
slider.isPageShow = true
slider.autoPlay && !slider.isDomRendering && slider.play()
}
function handlePageHide () {
slider.isPageShow = false
slider.stop()
}
global.addEventListener('pageshow', handlePageShow)
global.addEventListener('pagehide', handlePageHide)
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') {
handlePageShow()
}
else if (document.visibilityState === 'hidden') {
handlePageHide()
}
})
}
function idleWhenDomRendering (slider) {
global.addEventListener('renderend', function () {
slider.isDomRendering = false
slider.autoPlay && slider.isPageShow && slider.play()
})
global.addEventListener('renderbegin', function () {
slider.isDomRendering = true
slider.stop()
})
}
function _renderIndicators (slider, ct, data) {
data.extra.width = data.style.width || slider.width
data.extra.height = data.style.height || slider.height
const indicator = slider.getComponentManager().createElement(data)
indicator.parentRef = slider.data.ref
indicator.slider = slider
slider.indicator = indicator
ct.appendChild(indicator.node)
}
function renderIndicators (slider, ct, data) {
setTimeout(() => _renderIndicators(slider, ct, data), 0)
}
function updateIndicators (slider) {
slider.indicator && slider.indicator.setIndex(slider.currentIndex)
}
function transitionOnce (node, duration, timingFunc) {
const transitionStr = `all ${duration}ms ${timingFunc}`
node.style.webkitTransition = transitionStr
node.style.transition = transitionStr
setTimeout(function () {
node.style.webkitTransition = ''
node.style.transition = ''
}, duration)
}
function animateTransform (node, style, duration, timeFunction) {
transitionOnce(node, duration || TRANSITION_DURATION, timeFunction || 'ease')
for (const k in style) {
node.style[k] = style[k]
}
}
function transformSlide (slider, index) {
const node = slider.slides[index].node
node.style.opacity = slider.neighborAlpha
const transformStr = `scale(${slider.neighborScale})`
node.style.webkitTransform = transformStr
node.style.transform = transformStr
node.style.width = slider.width + 'px'
node.style.height = slider.height + 'px'
node.style.position = 'absolute'
node.style.top = '0px'
node.style.left = '0px'
}
function loadImg (slider) {
const imgs1 = slider.node.querySelectorAll('[img-src]') || []
const imgs2 = slider.node.querySelectorAll('[i-lazy-src]') || []
function load (node) {
const src = node.getAttribute('img-src') || node.getAttribute('i-lazy-src')
lib.img.applySrc(node, src, node.dataset.placeholder)
}
for (let i = 0; i < imgs1.length; i++) {
load(imgs1[i])
}
for (let i = 0; i < imgs2.length; i++) {
load(imgs2[i])
}
}
function _doRender (slider) {
loadImg(slider)
slider.total = slider.slides.length
slider.currentIndex = 0
const width = slider.data.style.width || 0
const height = slider.data.style.height || 0
slider.width = parseFloat(width) || slider.node.getBoundingClientRect().width
slider.height = parseFloat(height) || slider.node.getBoundingClientRect().height
const l = slider.slides.length
for (let i = 0; i < l; i++) {
transformSlide(slider, i)
}
setTimeout(() => slider.slideTo(0), 0)
}
function doRender (slider) {
setTimeout(_doRender.bind(null, slider), 0)
}
function loopIndex (idx, total) {
if (total === 0) {
return 0
}
return (total + idx) % total
}
function autoPlay (slider) {
const next = slider.currentIndex + 1
setTimeout(() => slider.slideTo(next), 0)
slider.playTimer = setTimeout(() => autoPlay(slider), slider.interval + TRANSITION_DURATION)
}
function useGesture (slider) {
const node = slider.node
let displacement, panning
node.addEventListener('panstart', function (e) {
if (!e.isVertical) {
e.preventDefault()
e.stopPropagation()
slider.stop()
panning = true
displacement = 0
}
})
node.addEventListener('panmove', function (e) {
if (!e.isVertical && panning) {
e.preventDefault()
e.stopPropagation()
const displacement = e.displacementX
moveSlides(slider, displacement)
}
})
node.addEventListener('panend', function (e) {
if (!e.isVertical && panning) {
e.preventDefault()
e.stopPropagation()
displacement = e.displacementX
if (e.isSwipe) {
if (displacement < 0) {
slider.slideToNext()
}
else {
slider.slideToPrev()
}
}
else {
if (Math.abs(displacement) < slider.width / 2) {
slider.slideTo(slider.currentIndex)
}
else if (displacement < 0) {
slider.slideToNext()
}
else {
slider.slideToPrev()
}
}
panning = false
slider.play()
}
})
node.addEventListener('swipe', function (e) {
if (!e.isVertical) {
e.preventDefault()
e.stopPropagation()
}
})
}
function moveSlides (slider, offset) {
const mainTransformStr = `translate(${offset}px, 0px) scale(${MAIN_SLIDE_SCALE})`
const mainNode = slider.mainSlide.node
mainNode.style.webkitTransform = mainTransformStr
mainNode.style.transform = mainTransformStr
const leftTransformStr = `translate(${slider.leftTranslate + offset}px, 0px) scale(${slider.neighborScale})`
const leftNode = slider.leftSlide.node
leftNode.style.webkitTransform = leftTransformStr
leftNode.style.transform = leftTransformStr
const rightTransformStr = `translate(${slider.rightTranslate + offset}px, 0px) scale(${slider.neighborScale})`
const rightNode = slider.rightSlide.node
rightNode.style.webkitTransform = rightTransformStr
rightNode.style.transform = rightTransformStr
}
function resetSideSlidePos (slider, side) {
const signMap = { left: '-', right: '' }
const transformStr = `translate(${signMap[side] + slider.width}px, 0px)`
const node = slider[side + 'Slide'].node
node.style.webkitTransform = transformStr
node.style.transform = transformStr
}
function resetOutsideSlides (slider, indexArr) {
indexArr = indexArr || []
const l = slider.slides.length
for (let i = 0; i < l; i++) {
if (indexArr.indexOf(i) <= -1) {
slider.slides[i].node.style.opacity = 0
}
}
}
const proto = {
create () {
const node = document.createElement('div')
this.node = node
node.classList.add('slider-neighbor')
node.classList.add('weex-container')
this.style.flexDirection.call(this, 'row')
node.style.position = 'relative'
node.style.overflow = 'hidden'
return node
},
createChildren () {
const componentManager = this.getComponentManager()
const children = this.data.children
const fragment = document.createDocumentFragment()
if (children && children.length) {
for (let i = 0; i < children.length; i++) {
let child
const data = children[i]
data.instanceId = this.data.instanceId
// 'indicator' maybe the last child of this component.
if (data.type !== 'indicator') {
child = componentManager.createElement(data)
child.node.classList.add('weex-neighbor-item')
const width = (data.style || {}).width || this.data.style.width
const height = (data.style || {}).height || this.data.style.height
child.node.style.marginTop = -(height / 2) + 'px'
child.node.style.marginLeft = -(width / 2) + 'px'
this.slides.push(child)
fragment.appendChild(child.node)
child.parentRef = this.data.ref
}
else {
renderIndicators(this, fragment, extend(data, {
extra: {
amount: children.length - 1,
index: 0
}
}))
}
}
resetOutsideSlides(this, [])
this.node.appendChild(fragment)
doRender(this)
}
},
appendChild (data) {
const children = this.data.children
const componentManager = this.getComponentManager()
let child
if (data.type === 'indicator') {
renderIndicators(this, this.node, extend(data, {
extra: {
amount: children.length,
index: this.currentIndex
}
}))
}
else {
child = componentManager.createElement(data)
child.node.classList.add('weex-neighbor-item')
const width = (data.style || {}).width || this.data.style.width
const height = (data.style || {}).height || this.data.style.height
child.node.style.marginTop = -(height / 2) + 'px'
child.node.style.marginLeft = -(width / 2) + 'px'
this.slides.push(child)
resetOutsideSlides(this, [])
this.node.appendChild(child.node)
}
doRender(this)
if (!children || !children.length) {
this.data.children = [data]
}
else {
children.push(data)
}
return child || this.indicator
},
insertBefore (child, before) {
const children = this.data.children
let i = 0
let slidesIdx = 0
let isAppend = false
if (!children || !children.length || !before) {
isAppend = true
}
else {
let l
for (l = children.length; i < l; i++) {
if (children[i].ref === before.data.ref) {
break
}
if (children[i].type !== 'indicator') {
slidesIdx++
}
}
if (i === l) {
isAppend = true
}
}
child.node.classList.add('weex-neighbor-item')
const data = child.data
const width = (data.style || {}).width || this.data.style.width
const height = (data.style || {}).height || this.data.style.height
child.node.style.marginTop = -(height / 2) + 'px'
child.node.style.marginLeft = -(width / 2) + 'px'
if (isAppend) {
this.node.appendChild(child.node)
this.slides.push(child)
resetOutsideSlides(this, [])
children.push(child.data)
}
else {
this.node.insertBefore(child.node, before.node)
this.slides.splice(slidesIdx, 0, child)
children.splice(i, 0, child.data)
}
doRender(this)
},
removeChild (child) {
const children = this.data.children
let i = 0
let slidesIdx = 0
if (children && children.length) {
let l
for (l = children.length; i < l; i++) {
if (children[i].ref === child.data.ref) {
break
}
if (children[i].type !== 'indicator') {
slidesIdx++
}
}
if (i < l) {
children.splice(i, 1)
this.slides.splice(slidesIdx, 1)
resetOutsideSlides(this, [])
}
}
this.getComponentManager().removeComponent(child.data.ref)
child.node.parentNode.removeChild(child.node)
doRender(this)
},
onAppend () {
this.slideTo(0)
useGesture(this)
Component.prototype.onAppend.call(this)
},
play () {
// start playing
this.playTimer && clearTimeout(this.playTimer)
if (this.playstatus/* && !this.toggleOff*/) {
this.playTimer = setTimeout(autoPlay.bind(null, this), this.interval)
}
},
stop () {
// stop playing
this.playTimer && clearTimeout(this.playTimer)
},
slideTo (index, restartAutoplay) {
const total = this.slides.length
if (total === 0) {
return
}
if (restartAutoplay) {
this.stop()
setTimeout(() => this.play(), 100)
}
const origIdx = index
index = loopIndex(origIdx, total)
const leftIndex = loopIndex(index - 1, total)
const rightIndex = loopIndex(index + 1, total)
this.mainSlide = this.slides[index]
this.leftSlide = this.slides[loopIndex(index - 1, total)]
this.rightSlide = this.slides[loopIndex(index + 1, total)]
const mainTransformStr = `translate(0px, 0px) scale(${MAIN_SLIDE_SCALE})`
setTimeout(() => animateTransform(this.mainSlide.node, {
webkitTransform: mainTransformStr,
transform: mainTransformStr,
opacity: MAIN_SLIDE_OPACITY,
zIndex: 99
}), 100)
const translateX = this.width
- this.width * (1 - this.neighborScale) / 2
- this.neighborSpace
this.leftTranslate = -translateX
this.rightTranslate = translateX
if (origIdx > this.currentIndex) {
resetSideSlidePos(this, 'right')
}
else if (origIdx < this.currentIndex) {
resetSideSlidePos(this, 'left')
}
const leftTransformStr = `translate(${-translateX + 'px'}, 0px) scale(${this.neighborScale})`
setTimeout(() => animateTransform(this.leftSlide.node, {
webkitTransform: leftTransformStr,
transform: leftTransformStr,
opacity: this.neighborAlpha,
zIndex: 1
}), 100)
const rightTransformStr = `translate(${translateX + 'px'}, 0px) scale(${this.neighborScale})`
setTimeout(() => animateTransform(this.rightSlide.node, {
webkitTransform: rightTransformStr,
transform: rightTransformStr,
opacity: this.neighborAlpha,
zIndex: 1
}), 100)
resetOutsideSlides(this, [index, leftIndex, rightIndex])
this.currentIndex = index
updateIndicators(this)
this.dispatchEvent('change', { index: this.currentIndex })
},
slideToPrev () {
this.slideTo(this.currentIndex - 1)
},
slideToNext () {
this.slideTo(this.currentIndex + 1)
}
}
const attr = {
interval: function (val) {
this.interval = parseInt(val) || DEFAULT_INTERVAL
},
index: function (val) {
const _this = this
function doSlide (index) {
index = parseInt(index)
if (index < 0 || isNaN(index)) {
return console.error('[h5-render] invalid index ', index)
}
_this.stop()
_this.slideTo(index)
_this.autoPlay && _this.isPageShow && _this.play()
if (_this._updateIndex) {
window.removeEventListener('renderend', _this._updateIndex)
}
}
if (this.isDomRendering) {
const pre = !!this._updateIndex
this._updateIndex = function () {
doSlide(val)
}
!pre && window.addEventListener('renderend', this._updateIndex)
}
else {
doSlide(val)
}
},
playstatus: function (val) {
const _this = this
this.playstatus = val && val !== 'false'
this.autoPlay = this.playstatus
function doPlay () {
_this.isPageShow && _this.play()
if (_this._updatePlaystatus) {
window.removeEventListener('renderend', _this._updatePlaystatus)
}
}
if (this.playstatus) {
if (this.isDomRendering) {
const pre = !!this._updatePlaystatus
this._updatePlaystatus = function () {
doPlay()
}
!pre && window.addEventListener('renderend', this._updatePlaystatus)
}
else {
doPlay()
}
}
else {
this.stop()
}
},
// support playstatus' alias auto-play for compatibility
autoPlay: function (val) {
this.attr.playstatus.call(this, val)
},
neighborSpace (val) {
const ns = parseFloat(val)
if (!isNaN(ns) && ns >= 0) {
this.neighborSpace = ns
}
else {
console.warn(`[h5-render] invalid value for 'neighbor-space' of slider-neighbor: ${val}.`)
}
},
neighborAlpha (val) {
const na = parseFloat(val)
if (!isNaN(na) && na >= 0 && na <= 1) {
this.neighborAlpha = na
}
else {
console.warn(`[h5-render] invalid value for 'neighbor-alpha' of slider-neighbor: ${val}.`)
}
},
neighborScale (val) {
const ns = parseFloat(val)
if (!isNaN(ns) && ns >= 0 && ns <= 1) {
this.neighborScale = ns
}
else {
console.warn(`[h5-render] invalid value for 'neighbor-scale' of slider-neighbor: ${val}.`)
}
}
}
const event = {
change: {
updator: function () {
return {
attrs: {
index: this.currentIndex
}
}
}
}
}
function init (Weex) {
Component = Weex.Component
extend = Weex.utils.extend
/**
* data.attr
* support slider's attributes and three
* @param {number} neighbor-space 0 - 375, the exposing width of slides on both other sides.
* @param {number} neighbor-alpha 0 - 1, opacity of both other sides of slides, default is 0.6.
* @param {number} neighbor-scale 0 - 1, the scale of both other sides of slides, default is 0.8.
*/
function SliderNeighbor (data) {
this.autoPlay = false // default value is false.
this.interval = DEFAULT_INTERVAL
this.direction = 'row' // 'column' is not temporarily supported.
this.slides = []
this.isPageShow = true
this.isDomRendering = true
this.currentIndex = 0
this.neighborSpace = DEFAULT_NEIGHBOR_SPACE
this.neighborAlpha = DEFAULT_NEIGHBOR_ALPHA
this.neighborScale = DEFAULT_NEIGHBOR_SCALE
// bind event 'pageshow', 'pagehide' and 'visibilitychange' on window.
idleWhenPageDisappear(this)
// bind event 'renderBegin' and 'renderEnd' on window.
idleWhenDomRendering(this)
Component.call(this, data)
}
SliderNeighbor.prototype = Object.create(Component.prototype)
extend(SliderNeighbor.prototype, proto)
extend(SliderNeighbor.prototype, { attr })
extend(SliderNeighbor.prototype, { event })
Weex.registerComponent('slider-neighbor', SliderNeighbor)
}
export default { init }