diff --git a/packages/taro-cli/templates/wxcloud/index.js b/packages/taro-cli/templates/wxcloud/index.js
index a6eff62324e4..d3b9c233cc84 100644
--- a/packages/taro-cli/templates/wxcloud/index.js
+++ b/packages/taro-cli/templates/wxcloud/index.js
@@ -103,7 +103,7 @@ module.exports = function (creater, params, helper, cb) {
typescript: true
})
} else {
- creater.template(template, path.join(clientDirName, 'pagejs'), path.join(sourceDir, 'pages', 'index', 'index.weapp.js'), {
+ creater.template(template, path.join(clientDirName, 'pagejs'), path.join(sourceDir, 'pages', 'index', 'index.js'), {
css: currentStyleExt
})
creater.template(template, path.join(clientDirName, 'components', 'login', 'index'), path.join(sourceDir, 'components', 'login', 'index.weapp.js'), {
diff --git a/packages/taro-components/src/components/canvas/index.js b/packages/taro-components/src/components/canvas/index.js
index 858497719548..ff1574466215 100644
--- a/packages/taro-components/src/components/canvas/index.js
+++ b/packages/taro-components/src/components/canvas/index.js
@@ -5,22 +5,30 @@ import classnames from 'classnames'
import './style/index.css'
-// canvas-id String canvas 组件的唯一标识符
-// disable-scroll Boolean false 当在 canvas 中移动时且有绑定手势事件时,禁止屏幕滚动以及下拉刷新
-// bindtouchstart EventHandle 手指触摸动作开始
-// bindtouchmove EventHandle 手指触摸后移动
-// bindtouchend EventHandle 手指触摸动作结束
-// bindtouchcancel EventHandle 手指触摸动作被打断,如来电提醒,弹窗
-// bindlongtap EventHandle 手指长按 500ms 之后触发,触发了长按事件后进行移动不会触发屏幕的滚动
-// binderror EventHandle 当发生错误时触发 error 事件,detail = {errMsg: 'something wrong'}
+/**
+ * Canvas组件参数
+ * @typedef CanvasProps
+ * @param {String} [canvasId=canvas] 组件的唯一标识符
+ * @param {Boolean} [disableScroll=false] 当在 canvas 中移动时且有绑定手势事件时,禁止屏幕滚动以及下拉刷新
+ * @param {EventHandle} onTouchstart 手指触摸动作开始
+ * @param {EventHandle} onTouchmove 手指触摸后移动
+ * @param {EventHandle} onTouchend 手指触摸动作结束
+ * @param {EventHandle} onTouchcancel 手指触摸动作被打断,如来电提醒,弹窗
+ * @param {EventHandle} onLongtap 手指长按 500ms 之后触发,触发了长按事件后进行移动不会触发屏幕的滚动
+ * @param {EventHandle} onError 当发生错误时触发 error 事件,detail = {errMsg: 'something wrong'}
+ */
@touchable()
-export default class Canvas extends Taro.PureComponent {
+class Canvas extends Taro.PureComponent {
+ /** @type {CanvasProps} */
static defaultProps = {
canvasId: '',
disableScroll: false,
- bindError: null
+ onError: null
}
+
+ /** @type {CanvasProps} */
+ props
width = 300
height = 150
getWrapRef = ref => {
@@ -40,8 +48,8 @@ export default class Canvas extends Taro.PureComponent {
this.height = height
}
componentDidCatch (e) {
- const bindError = this.props.bindError
- bindError && bindError({
+ const onError = this.props.onError
+ onError && onError({
errMsg: e.message
})
}
@@ -68,3 +76,5 @@ export default class Canvas extends Taro.PureComponent {
)
}
}
+
+export default Canvas
diff --git a/packages/taro-components/src/components/navigator/index.js b/packages/taro-components/src/components/navigator/index.js
index 47735241be33..8a00d10d721d 100644
--- a/packages/taro-components/src/components/navigator/index.js
+++ b/packages/taro-components/src/components/navigator/index.js
@@ -8,19 +8,21 @@ import './navigator.css'
/* eslint-disable prefer-promise-reject-errors */
-/*
- *target String self 在哪个目标上发生跳转,默认当前小程序,可选值self/miniProgram
- *url String 当前小程序内的跳转链接
- *open-type String navigate 跳转方式
- *delta Number 当 open-type 为 'navigateBack' 时有效,表示回退的层数
- *app-id String 当target="miniProgram"时有效,要打开的小程序 appId
- *path String 当target="miniProgram"时有效,打开的页面路径,如果为空则打开首页
- *extra-data Object 当target="miniProgram"时有效,需要传递给目标小程序的数据,目标小程序可在 App.onLaunch(),App.onShow() 中获取到这份数据。详情
- version version release 当target="miniProgram"时有效,要打开的小程序版本,有效值 develop(开发版),trial(体验版),release(正式版),仅在当前小程序为开发版或体验版时此参数有效;如果当前小程序是 *式版,则打开的小程序必定是正式版。
- *bindsuccess String 当target="miniProgram"时有效,跳转小程序成功
- *bindfail String 当target="miniProgram"时有效,跳转小程序失败
- *bindcomplete String 当target="miniProgram"时有效,跳转小程序完成
- *aria-label String 无障碍访问,(属性)元素的额外描述
+/**
+ * Navigator组件参数
+ * @typedef NavigatorProps
+ * @property {String} appId 当target="miniProgram"时有效,要打开的小程序 appId
+ * @property {String} ariaLabel 无障碍访问,(属性)元素的额外描述
+ * @property {Number} delta 当 openType 为 'navigateBack' 时有效,表示回退的层数
+ * @property {Object} extraData 当target="miniProgram"时有效,需要传递给目标小程序的数据,目标小程序可在 App.onLaunch(),App.onShow() 中获取到这份数据。详情
+ * @property {String} [openType=navigate] 跳转方式
+ * @property {String} path 当target="miniProgram"时有效,打开的页面路径,如果为空则打开首页
+ * @property {String} [target=self] 在哪个目标上发生跳转,默认当前小程序,可选值self/miniProgram
+ * @property {String} url 当前小程序内的跳转链接
+ * @property {version} [version=release] 当target="miniProgram"时有效,要打开的小程序版本,有效值 develop(开发版),trial(体验版),release(正式版),仅在当前小程序为开发版或体验版时此参数有效;如果当前小程序是 *式版,则打开的小程序必定是正式版。
+ * @property {String} onFail 当target="miniProgram"时有效,跳转小程序失败
+ * @property {String} onComplete 当target="miniProgram"时有效,跳转小程序完成
+ * @property {String} onSuccess 当target="miniProgram"时有效,跳转小程序成功
*/
/**
@@ -29,7 +31,14 @@ import './navigator.css'
* https://developers.weixin.qq.com/miniprogram/dev/component/navigator.html
**/
+@hoverable({
+ hoverClass: 'navigator-hover',
+ hoverStopPropergation: false,
+ hoverStartTime: 50,
+ hoverStayTime: 600
+})
class Navigator extends Taro.Component {
+ /** @type {NavigatorProps} */
static defaultProps = {
target: 'self',
url: null,
@@ -39,13 +48,15 @@ class Navigator extends Taro.Component {
path: null,
extraData: {},
version: 'release',
- bindSuccess: null,
- bindFail: null,
- bindComplete: null,
+ onSuccess: null,
+ onFail: null,
+ onComplete: null,
isHover: false
}
+ /** @type {NavigationProps} */
+ props
onClick = () => {
- const { openType, bindSuccess, bindFail, bindComplete } = this.props
+ const { openType, onSuccess, onFail, onComplete } = this.props
let promise
switch (openType) {
case 'navigate':
@@ -81,11 +92,11 @@ class Navigator extends Taro.Component {
}
if (promise) {
promise.then(res => {
- bindSuccess && bindSuccess(res)
+ onSuccess && onSuccess(res)
}).catch(res => {
- bindFail && bindFail(res)
+ onFail && onFail(res)
}).finally(res => {
- bindComplete && bindComplete(res)
+ onComplete && onComplete(res)
})
}
}
@@ -105,9 +116,4 @@ class Navigator extends Taro.Component {
}
}
-export default hoverable({
- hoverClass: 'navigator-hover',
- hoverStopPropergation: false,
- hoverStartTime: 50,
- hoverStayTime: 600
-})(Navigator)
+export default Navigator
diff --git a/packages/taro-components/src/components/tabbar/index.js b/packages/taro-components/src/components/tabbar/index.js
index 069b4433ed50..93735fd4ba5f 100644
--- a/packages/taro-components/src/components/tabbar/index.js
+++ b/packages/taro-components/src/components/tabbar/index.js
@@ -39,9 +39,9 @@ class Tabbar extends Nerv.Component {
this.customRoutes.push([key, customRoutes[key]])
}
- list.forEach( item => {
- if (item.pagePath.indexOf('/') !== 0){
- item.pagePath = "/" + item.pagePath
+ list.forEach(item => {
+ if (item.pagePath.indexOf('/') !== 0) {
+ item.pagePath = '/' + item.pagePath
}
})
diff --git a/packages/taro-components/src/components/video/controls.js b/packages/taro-components/src/components/video/controls.js
new file mode 100644
index 000000000000..c3cfcf49e5c6
--- /dev/null
+++ b/packages/taro-components/src/components/video/controls.js
@@ -0,0 +1,171 @@
+import Nerv, { Component } from 'nervjs'
+import classnames from 'classnames'
+import { formatTime, calcDist } from './utils'
+
+/**
+ * @typedef {Object} ControlsProps
+ * @property {Boolean} controls={controls}
+ * @property {Number} currentTime={this.currentTime}
+ * @property {Number} duration={this.state.duration}
+ * @property {Boolean} isPlaying={this.state.isPlaying}
+ * @property {Function} pauseFunc={this.pause}
+ * @property {Function} playFunc={this.play}
+ * @property {Function} seekFunc={this.seek}
+ * @property {Boolean} showPlayBtn={showPlayBtn}
+ * @property {Boolean} showProgress={showProgress}
+ */
+class Controls extends Component {
+ visible = false
+ isDraggingProgressBall = false
+
+ /** @type {number} */
+ hideControlsTimer
+
+ /** @type {ControlsProps} */
+ props
+
+ progressDimentions = {
+ left: 0,
+ right: 0,
+ width: 0
+ }
+
+ calcPercentage = pageX => {
+ let pos = pageX - this.progressDimentions.left
+ pos = Math.max(pos, 0)
+ pos = Math.min(pos, this.progressDimentions.width)
+ return pos / this.progressDimentions.width
+ }
+
+ getControlsRef = ref => {
+ if (!ref) return
+ this.controlsRef = ref
+ }
+ getCurrentTimeRef = ref => {
+ if (!ref) return
+ this.currentTimeRef = ref
+ }
+ getProgressBallRef = ref => {
+ if (!ref) return
+ this.progressBallRef = ref
+ }
+
+ setCurrentTime (time) {
+ this.currentTimeRef.innerHTML = formatTime(time)
+ }
+ setProgressBall (percentage) {
+ this.progressBallRef.style.left = `${percentage * 100}%`
+ }
+
+ toggleVisible (nextVisible) {
+ const visible = nextVisible === undefined ? !this.visible : nextVisible
+ if (visible) {
+ this.hideControlsTimer && clearTimeout(this.hideControlsTimer)
+ if (this.props.isPlaying) {
+ this.hideControlsTimer = setTimeout(() => {
+ this.toggleVisible(false)
+ }, 2000)
+ }
+ this.controlsRef.style.visibility = 'visible'
+ } else {
+ this.controlsRef.style.visibility = 'hidden'
+ }
+ this.visible = !!visible
+ }
+
+ onDragProgressBallStart = () => {
+ this.isDraggingProgressBall = true
+ this.hideControlsTimer && clearTimeout(this.hideControlsTimer)
+ }
+ onClickProgress = e => {
+ e.stopPropagation()
+ const seekFunc = this.props.seekFunc
+ const percentage = this.calcPercentage(e.pageX)
+ seekFunc(percentage * this.props.duration)
+ this.toggleVisible(true)
+ }
+ bindTouchEvents = () => {
+ let percentage = 0
+ const touchMove = e => {
+ if (!this.isDraggingProgressBall) return
+ const touchX = e.touches[0].pageX
+ percentage = this.calcPercentage(touchX)
+ this.setProgressBall(percentage)
+ }
+ const touchEnd = e => {
+ if (!this.isDraggingProgressBall) return
+ const seekFunc = this.props.seekFunc
+ this.isDraggingProgressBall = false
+ seekFunc(percentage * this.props.duration)
+ this.toggleVisible(true)
+ }
+
+ document.body.addEventListener('touchmove', touchMove)
+ document.body.addEventListener('touchend', touchEnd)
+ document.body.addEventListener('touchcancel', touchEnd)
+ return () => {
+ document.body.removeEventListener('touchmove', touchMove)
+ document.body.removeEventListener('touchend', touchEnd)
+ document.body.removeEventListener('touchcancel', touchEnd)
+ }
+ }
+
+ componentDidMount () {
+ this.unbindTouchEvents = this.bindTouchEvents()
+ }
+ componentWillUnmount () {
+ this.unbindTouchEvents()
+ }
+
+ render () {
+ const { controls, currentTime, duration, isPlaying, pauseFunc, playFunc, showPlayBtn, showProgress } = this.props
+ const formattedDuration = formatTime(duration)
+ let playBtn
+
+ if (!showPlayBtn) {
+ return null
+ } else if (isPlaying) {
+ playBtn =
+ } else {
+ playBtn =
+ }
+
+ return (
+
+ {controls && (
+
+ {playBtn}
+ {showProgress && (
+
+ {formatTime(currentTime)}
+
+ )}
+ {showProgress && (
+
+
{
+ if (ref !== null) {
+ const rect = ref.getBoundingClientRect()
+ this.progressDimentions.left = rect.left
+ this.progressDimentions.right = rect.right
+ this.progressDimentions.width = rect.width
+ }
+ }}>
+
+
+
+
+ )}
+ {showProgress &&
{formattedDuration}
}
+
+ )}
+ {this.props.children}
+
+ )
+ }
+}
+
+export default Controls
diff --git a/packages/taro-components/src/components/video/danmu.js b/packages/taro-components/src/components/video/danmu.js
new file mode 100644
index 000000000000..29e685d518cd
--- /dev/null
+++ b/packages/taro-components/src/components/video/danmu.js
@@ -0,0 +1,107 @@
+import Nerv, { PureComponent } from 'nervjs'
+
+class Danmu extends PureComponent {
+ state = {
+ danmuList: []
+ }
+
+ danmuList = []
+ danmuElList = []
+ currentTime = 0
+
+ ensureProperties (danmu) {
+ const clonedDanmu = {...danmu}
+ if (!('time' in danmu)) {
+ clonedDanmu.time = this.currentTime
+ }
+ clonedDanmu.key = Math.random()
+ clonedDanmu.bottom = `${Math.random() * 90 + 5}%`
+ return clonedDanmu
+ }
+
+ sendDanmu (danmuList) {
+ if (Array.isArray(danmuList)) {
+ this.danmuList = [
+ ...this.danmuList,
+ ...danmuList.map(danmu => {
+ return this.ensureProperties(danmu)
+ })
+ ]
+ } else {
+ const danmu = danmuList
+ this.danmuList = [
+ ...this.danmuList,
+ { ...this.ensureProperties(danmu) }
+ ]
+ }
+ }
+
+ tick (currentTime) {
+ this.currentTime = currentTime
+ if (!this.props.enable) return
+
+ const danmuList = this.danmuList
+ /**
+ * @todo 这个判断对拖拽进度的处理不严谨
+ */
+ const newDanmuList = danmuList.filter(({ time }) => {
+ return currentTime - time < 4 && currentTime > time
+ })
+ let shouldUpdate = false
+ const oldDanmuList = this.state.danmuList
+
+ if (newDanmuList.length !== oldDanmuList.length) {
+ shouldUpdate = true
+ } else {
+ shouldUpdate = newDanmuList.some(({ key }) => {
+ return oldDanmuList.every((danmu) => {
+ return key !== danmu.key
+ })
+ })
+ }
+ if (shouldUpdate) {
+ this.setState({
+ danmuList: newDanmuList
+ })
+ }
+ }
+
+ componentDidUpdate () {
+ requestAnimationFrame(() => {
+ setTimeout(() => {
+ const danmuElList = this.danmuElList.splice(0)
+ danmuElList.forEach(danmu => {
+ danmu.style.left = 0
+ danmu.style.webkitTransform = 'translateX(-100%)'
+ danmu.style.transform = 'translateX(-100%)'
+ })
+ })
+ })
+ }
+
+ render () {
+ if (!this.props.enable) return ''
+ return
+ {this.state.danmuList.map(({ text, color, bottom, key }) => {
+ return (
+
{
+ if (ref) {
+ this.danmuElList.push(ref)
+ }
+ }}>
+ {text}
+
+ )
+ })}
+
+ }
+}
+
+export default Danmu
diff --git a/packages/taro-components/src/components/video/images/full.png b/packages/taro-components/src/components/video/images/full.png
new file mode 100644
index 000000000000..9d1ec07a6533
Binary files /dev/null and b/packages/taro-components/src/components/video/images/full.png differ
diff --git a/packages/taro-components/src/components/video/images/mute.png b/packages/taro-components/src/components/video/images/mute.png
new file mode 100644
index 000000000000..158b42685631
Binary files /dev/null and b/packages/taro-components/src/components/video/images/mute.png differ
diff --git a/packages/taro-components/src/components/video/images/pause.png b/packages/taro-components/src/components/video/images/pause.png
new file mode 100644
index 000000000000..8a2afc2ca759
Binary files /dev/null and b/packages/taro-components/src/components/video/images/pause.png differ
diff --git a/packages/taro-components/src/components/video/images/play.png b/packages/taro-components/src/components/video/images/play.png
new file mode 100644
index 000000000000..ceabd9faaa77
Binary files /dev/null and b/packages/taro-components/src/components/video/images/play.png differ
diff --git a/packages/taro-components/src/components/video/images/shrink.png b/packages/taro-components/src/components/video/images/shrink.png
new file mode 100644
index 000000000000..2cd56f85208f
Binary files /dev/null and b/packages/taro-components/src/components/video/images/shrink.png differ
diff --git a/packages/taro-components/src/components/video/images/unmute.png b/packages/taro-components/src/components/video/images/unmute.png
new file mode 100644
index 000000000000..572b201e1edc
Binary files /dev/null and b/packages/taro-components/src/components/video/images/unmute.png differ
diff --git a/packages/taro-components/src/components/video/images/volume.png b/packages/taro-components/src/components/video/images/volume.png
new file mode 100644
index 000000000000..e4fbbaeebbdb
Binary files /dev/null and b/packages/taro-components/src/components/video/images/volume.png differ
diff --git a/packages/taro-components/src/components/video/index.js b/packages/taro-components/src/components/video/index.js
index e9a85af16abc..b5d545cff400 100644
--- a/packages/taro-components/src/components/video/index.js
+++ b/packages/taro-components/src/components/video/index.js
@@ -1,88 +1,500 @@
-import 'weui'
-import Nerv from 'nervjs'
+import Nerv, { Component, createPortal } from 'nervjs'
+import classnames from 'classnames'
+import Danmu from './danmu'
+import Controls from './controls'
+import { formatTime, calcDist, normalizeNumber } from './utils'
+
import './style/index.scss'
+import 'weui'
+
+/**
+ * @typedef {Object} Danmu
+ * @property {string} text 弹幕文字
+ * @property {string} color 弹幕颜色
+ * @property {number} [time] 弹幕时间
+ */
-class Video extends Nerv.Component {
- constructor () {
- super(...arguments)
+/**
+ * Video组件参数
+ * @typedef {Object} VideoProps
+ * @property {string} src 要播放视频的资源地址
+ * @property {boolean} [autoPauseIfNavigate=true] 当跳转到其它小程序页面时,是否自动暂停本页面的视频
+ * @property {boolean} [autoPauseIfOpenNative=true] 当跳转到其它微信原生页面时,是否自动暂停本页面的视频
+ * @property {boolean} [autoplay=false] 是否自动播放
+ * @property {boolean} [controls=true] 是否显示默认播放控件(播放/暂停按钮、播放进度、时间)
+ * @property {boolean} [danmuBtn=false] 是否显示弹幕按钮,只在初始化时有效,不能动态变更
+ * @property {Array.} [danmuList=[]] 弹幕列表
+ * @property {boolean} [enableDanmu=false] 是否展示弹幕,只在初始化时有效,不能动态变更
+ * @property {boolean} [enablePlayGesture=false] 是否开启播放手势,即双击切换播放/暂停
+ * @property {boolean} [enableProgressGesture=true] 是否开启控制进度的手势
+ * @property {number} [initialTime=0] 指定视频初始播放位置
+ * @property {boolean} [loop=false] 是否循环播放
+ * @property {boolean} [muted=false] 是否静音播放
+ * @property {string} [objectFit=contain] 当视频大小与 video 容器大小不一致时,视频的表现形式
+ * @property {string} [playBtnPosition=bottom] 播放按钮的位置
+ * @property {boolean} [showCenterPlayBtn=true] 是否显示视频中间的播放按钮
+ * @property {boolean} [showFullscreenBtn=true] 是否显示全屏按钮
+ * @property {boolean} [showMuteBtn=false] 是否显示静音按钮
+ * @property {boolean} [showPlayBtn=true] 是否显示视频底部控制栏的播放按钮
+ * @property {boolean} [showProgress=true] 若不设置,宽度大于240时才会显示
+ * @property {boolean} [vslideGesture=false] 在非全屏模式下,是否开启亮度与音量调节手势(同 pageGesture)
+ * @property {boolean} [vslideGestureInFullscreen=true] 在全屏模式下,是否开启亮度与音量调节手势
+ * @property {number} [direction] 设置全屏时视频的方向,不指定则根据宽高比自动判断
+ * @property {number} [duration] 指定视频时长
+ * @property {string} [poster] 视频封面的图片网络资源地址。若 controls 属性值为 false 则设置 poster 无效
+ * @property {string} [title] 视频的标题,全屏时在顶部展示
+ * @property {Function} [onPlay] 当开始/继续播放时触发play事件
+ * @property {Function} [onPause] 当暂停播放时触发 pause 事件
+ * @property {Function} [onEnded] 当播放到末尾时触发 ended 事件
+ * @property {Function} [onTimeUpdate] 播放进度变化时触发,event.detail = {currentTime, duration} 。触发频率 250ms 一次
+ * @property {Function} [onFullscreenChange] 视频进入和退出全屏时触发,event.detail = {fullScreen, direction},direction 有效值为 vertical 或 horizontal
+ * @property {Function} [onWaiting] 视频出现缓冲时触发
+ * @property {Function} [onError] 视频播放出错时触发
+ * @property {Function} [onProgress] 加载进度变化时触发,只支持一段加载。event.detail = {buffered},百分比
+ */
+
+class Video extends Component {
+ /** @type {VideoProps} */
+ static defaultProps = {
+ autoPauseIfNavigate: true,
+ autoPauseIfOpenNative: true,
+ autoplay: false,
+ controls: true,
+ danmuBtn: false,
+ danmuList: [],
+ enableDanmu: false,
+ enablePlayGesture: false,
+ enableProgressGesture: true,
+ initialTime: 0,
+ loop: false,
+ muted: false,
+ objectFit: 'contain',
+ playBtnPosition: 'bottom',
+ showCenterPlayBtn: true,
+ showFullscreenBtn: true,
+ showMuteBtn: false,
+ showPlayBtn: true,
+ showProgress: true,
+ vslideGesture: false,
+ vslideGestureInFullscreen: true
}
- componentDidMount () {
- this.bindevent()
+
+ /** @type {VideoProps} */
+ props
+
+ /** @type {HTMLVideoElement} */
+ videoRef
+
+ /** @type {Contorls} */
+ controlsRef
+
+ /** @type {HTMLDivElement} */
+ currentTimeRef
+
+ /** @type {HTMLDivElement} */
+ danmuRef
+
+ /** @type {number} */
+ currentTime = 0
+
+ /** @type {number} */
+ lastClickedTime
+
+ /** @type {number} */
+ lastTouchScreenX
+
+ /** @type {number} */
+ lastTouchScreenY
+
+ progressDimentions = {
+ left: 0,
+ right: 0,
+ width: 0
}
- bindevent () {
- this.video.addEventListener('timeupdate', (e) => {
- Object.defineProperty(e, 'detail', {
- enumerable: true,
- value: {
- duration: e.srcElement.duration,
- currentTime: e.srcElement.currentTime
- }
+ constructor (props, context) {
+ super(props, context)
+ const stateObj = this.getInitialState(this.props)
+ this.state = Object.assign(
+ {
+ duration: null,
+ isPlaying: false,
+ isFirst: true,
+ enableDanmu: false,
+ isFullScreen: false,
+ isMute: false
+ },
+ stateObj
+ )
+ }
+
+ sendDanmu (danmu) {
+ this.danmuRef.sendDanmu(danmu)
+ }
+
+ onTimeUpdate = e => {
+ Object.defineProperty(e, 'detail', {
+ enumerable: true,
+ value: {
+ duration: e.srcElement.duration,
+ currentTime: e.srcElement.currentTime
+ }
+ })
+ this.currentTime = this.videoRef.currentTime
+ const duration = this.state.duration
+ if (!this.controlsRef.isDraggingProgressBall && !this.isDraggingProgress) {
+ this.controlsRef.setProgressBall(this.currentTime / duration)
+ }
+ this.controlsRef.setCurrentTime(this.currentTime)
+
+ this.danmuRef.tick(this.currentTime)
+ this.props.onTimeUpdate && this.props.onTimeUpdate(e)
+ }
+
+ onEnded = e => {
+ this.pause()
+ this.props.onEnded && this.props.onEnded(e)
+ }
+
+ onPlay = e => {
+ this.props.onPlay && this.props.onPlay(e)
+ this.controlsRef.toggleVisible(true)
+ if (!this.state.isPlaying) {
+ this.setState({
+ isPlaying: true
+ })
+ }
+ }
+
+ onPause = e => {
+ this.props.onPause && this.props.onPause(e)
+ this.controlsRef.toggleVisible(true)
+ if (this.state.isPlaying) {
+ this.setState({
+ isPlaying: false
})
- this.props.onTimeUpdate && this.props.onTimeUpdate(e)
+ }
+ }
+
+ onError = e => {
+ Object.defineProperty(e, 'detail', {
+ enumerable: true,
+ value: { errMsg: e.srcElement.error.code }
})
+ this.props.onError && this.props.onError(e)
+ }
- this.video.addEventListener('ended', (e) => {
- this.props.onEnded && this.props.onEnded(e)
+ onClickContainer = e => {
+ if (this.props.enablePlayGesture) {
+ const now = Date.now()
+ if (now - this.lastClickedTime < 300) {
+ // 双击
+ if (this.state.isPlaying) {
+ this.pause()
+ } else {
+ this.play()
+ }
+ }
+ this.lastClickedTime = now
+ }
+ this.controlsRef.toggleVisible()
+ }
+
+ onLoadedMetadata = e => {
+ this.setState({
+ duration: this.videoRef.duration
})
+ this.duration = this.videoRef.duration
+ if (this.state.isFirst) {
+ this.seek(this.props.initialTime)
+ }
+ }
- this.video.addEventListener('play', (e) => {
- this.props.onPlay && this.props.onPlay(e)
+ toggleDanmu = e => {
+ e.stopPropagation()
+ this.controlsRef.toggleVisible(true)
+ this.setState({
+ enableDanmu: !this.state.enableDanmu
})
+ }
+
+ toggleFullScreen = e => {
+ e.stopPropagation()
+ const currentTime = this.currentTime
+ const danmuList = this.danmuRef.danmuList
+ this.setState(
+ {
+ isFullScreen: !this.state.isFullScreen
+ },
+ () => {
+ const evt = new Event('fullscreenChange', {
+ fullScreen: this.state.isFullScreen,
+ direction: 'vertical'
+ })
+ this.props.onFullscreenChange && this.props.onFullscreenChange(evt)
+ this.danmuRef.danmuList = danmuList
+ this.seek(currentTime)
+ this.state.isPlaying && this.play()
+ this.controlsRef.toggleVisible(true)
+ }
+ )
+ }
- this.video.addEventListener('pause', (e) => {
- this.props.onPause && this.props.onPause(e)
+ toggleMute = e => {
+ e.stopPropagation()
+ this.setState(() => {
+ const nextMuteState = !this.state.isMute
+ this.videoRef.muted = nextMuteState
+ this.controlsRef.toggleVisible(true)
+ return { isMute: nextMuteState }
})
+ }
- // 网络错误
- this.video.addEventListener('error', (e) => {
- Object.defineProperty(e, 'detail', {
- enumerable: true,
- value: {errMsg: e.srcElement.error.code}
- })
- this.props.onError && this.props.onError(e)
+ play = () => {
+ this.videoRef.play()
+ this.setState({
+ isPlaying: true,
+ isFirst: false
})
}
+ pause = () => {
+ this.videoRef.pause()
+ this.setState({
+ isPlaying: false
+ })
+ }
+
+ seek = position => {
+ this.videoRef.currentTime = position
+ }
+
+ getInitialState (props) {
+ const stateObj = {
+ enableDanmu: props.enableDanmu
+ }
+ return stateObj
+ }
+
+ onTouchStartContainer = e => {
+ this.lastTouchScreenX = e.touches[0].screenX
+ this.lastTouchScreenY = e.touches[0].screenY
+ }
+
+ bindTouchEvents = () => {
+ let lastVolume
+ let lastPercentage
+ let nextPercentage
+ let gestureType = 'none'
+
+ const analyseGesture = e => {
+ const obj = {}
+ const nowX = e.touches[0].screenX
+ const nowY = e.touches[0].screenY
+ const distX = nowX - this.lastTouchScreenX
+ const distY = nowY - this.lastTouchScreenY
+ if (gestureType === 'none') {
+ const dist = calcDist(distX, distY)
+ if (dist < 10) {
+ obj.type = 'none'
+ return obj
+ }
+ if (distX === 0 || Math.abs(distY / distX) > 1) {
+ let enableVslideGesture = this.state.isFullScreen ? this.props.vslideGestureInFullscreen : this.props.vslideGesture
+ if (enableVslideGesture) {
+ gestureType = 'adjustVolume'
+ lastVolume = this.videoRef.volume
+ }
+ } else if (this.props.enableProgressGesture && Math.abs(distY / distX) <= 1) {
+ gestureType = 'adjustProgress'
+ lastPercentage = this.currentTime / this.state.duration
+ }
+ }
+ obj.type = gestureType
+ obj.dataX = normalizeNumber(distX / window.screen.width)
+ obj.dataY = normalizeNumber(distY / window.screen.height)
+ return obj
+ }
+ const touchMove = e => {
+ if (this.controlsRef.isDraggingProgressBall) return
+
+ const gestureObj = analyseGesture(e)
+ if (gestureObj.type === 'adjustVolume') {
+ this.toastVolumeRef.style.visibility = 'visible'
+ const nextVolume = Math.max(Math.min(lastVolume - gestureObj.dataY, 1), 0)
+ this.videoRef.volume = nextVolume
+ this.toastVolumeBarRef.style.width = `${nextVolume * 100}%`
+ } else if (gestureObj.type === 'adjustProgress') {
+ this.isDraggingProgress = true
+ nextPercentage = Math.max(Math.min(lastPercentage + gestureObj.dataX, 1), 0)
+ this.controlsRef.setProgressBall(nextPercentage)
+ this.controlsRef.toggleVisible(true)
+ this.toastProgressTitleRef.innerHTML = `${formatTime(nextPercentage * this.duration)} / ${formatTime(this.duration)}`
+ this.toastProgressRef.style.visibility = 'visible'
+ }
+ }
+ const touchEnd = e => {
+ if (gestureType === 'adjustVolume') {
+ this.toastVolumeRef.style.visibility = 'hidden'
+ } else if (gestureType === 'adjustProgress') {
+ this.toastProgressRef.style.visibility = 'hidden'
+ }
+ gestureType = 'none'
+ if (this.isDraggingProgress) {
+ this.isDraggingProgress = false
+ this.seek(nextPercentage * this.videoRef.duration)
+ }
+ }
+
+ document.body.addEventListener('touchmove', touchMove)
+ document.body.addEventListener('touchend', touchEnd)
+ document.body.addEventListener('touchcancel', touchEnd)
+ return () => {
+ document.body.removeEventListener('touchmove', touchMove)
+ document.body.removeEventListener('touchend', touchEnd)
+ document.body.removeEventListener('touchcancel', touchEnd)
+ }
+ }
+
+ componentWillMount () {
+ const getRef = refName => {
+ return ref => {
+ if (!ref) return
+ this[refName] = ref
+ }
+ }
+ this.getVideoRef = getRef('videoRef')
+ this.getControlsRef = getRef('controlsRef')
+ this.getDanmuRef = getRef('danmuRef')
+ this.getToastProgressRef = getRef('toastProgressRef')
+ this.getToastProgressTitleRef = getRef('toastProgressTitleRef')
+ this.getToastVolumeRef = getRef('toastVolumeRef')
+ this.getToastVolumeBarRef = getRef('toastVolumeBarRef')
+ }
+
+ componentDidMount () {
+ this.unbindTouchEvents = this.bindTouchEvents()
+ this.sendDanmu(this.props.danmuList)
+ }
+
+ componentWillReceiveProps (nProps) {
+ const nState = this.getInitialState(nProps)
+ this.setState(nState)
+ }
+
+ componentWillUnmount () {
+ this.unbindTouchEvents()
+ }
+
render () {
- let {
+ const {
src,
- controls,
autoplay,
- poster,
- initialTime,
+ className,
id,
+ initialTime,
loop,
muted,
- className
+ objectFit,
+ poster,
+
+ controls,
+ showFullscreenBtn,
+ showMuteBtn,
+ showPlayBtn,
+ showProgress,
+ showCenterPlayBtn,
+ danmuBtn
} = this.props
- if (!controls) {
- poster = ''
+ const { enableDanmu, isFirst, isMute, isFullScreen } = this.state
+ const duration = formatTime(this.state.duration)
+
+ const videoProps = {
+ id,
+ src,
+ autoplay,
+ poster: controls ? poster : null,
+ loop,
+ muted,
+ start: initialTime,
+ className: classnames('taro-video-video', className),
+ ref: this.getVideoRef,
+ playsinline: true,
+ 'webkit-playsinline': true,
+ 'object-fit': objectFit,
+
+ controls: false,
+ onTimeUpdate: this.onTimeUpdate,
+ onEnded: this.onEnded,
+ onPlay: this.onPlay,
+ onPause: this.onPause,
+ onError: this.onError,
+ onDurationChange: this.onLoadedMetadata
}
- return (
-
+
+ const videoNode = (
+
+
+
+ {showMuteBtn && (
+
+ )}
+ {danmuBtn && (
+
+ 弹幕
+
+ )}
+ {showFullscreenBtn && (
+
+ )}
+
+
+ {isFirst && showCenterPlayBtn && !this.state.isPlaying && (
+
+ )}
+
+
音量
+
+
+
+
+ {new Array(10).fill().map(v => (
+
+ ))}
+
+
+
+
+
+
)
+ return this.state.isFullScreen ? createPortal(videoNode, document.body) : {videoNode}
}
}
-// 默认配置
-Video.defaultProps = {
- autoplay: false,
- controls: true,
- loop: false,
- muted: false
-}
-
export default Video
diff --git a/packages/taro-components/src/components/video/index.md b/packages/taro-components/src/components/video/index.md
index c4132aec0d2f..b767442a6ce6 100644
--- a/packages/taro-components/src/components/video/index.md
+++ b/packages/taro-components/src/components/video/index.md
@@ -2,15 +2,37 @@
| 是否支持 | 属性 | 类型 | 默认值 | 说明 |
| -------- | -------------- | ----------- | ------ | ------------------------------------------------------------ |
-| √ | src | String | | 要播放视频的资源地址 |
-| √ | controls | Boolean | true | 是否显示默认播放控件(播放/暂停按钮、播放进度、时间) |
-| √ | autoplay | Boolean | false | 是否自动播放 |
-| √ | poster | String | | 视频封面的图片网络资源地址,如果 controls 属性值为 false 则设置 poster 无效 |
-| | initial-time | Number | | 指定视频初始播放位置 |
-| √ | loop | Boolean | false | 是否循环播放 |
-| √ | muted | Boolean | false | 是否静音播放 |
-| √ | bindplay | EventHandle | | 当开始/继续播放时触发play事件 |
-| √ | bindpause | EventHandle | | 当暂停播放时触发 pause 事件 |
-| √ | bindended | EventHandle | | 当播放到末尾时触发 ended 事件 |
-| √ | bindtimeupdate | EventHandle | | 播放进度变化时触发。触发频率 250ms 一次 |
-| √ | binderror | EventHandle | | 视频播放出错时触发 |
+| | autoPauseIfNavigate | boolean | true |当跳转到其它小程序页面时,是否自动暂停本页面的视频
+| | autoPauseIfOpenNative | boolean | true |当跳转到其它微信原生页面时,是否自动暂停本页面的视频
+| | direction | number | |设置全屏时视频的方向,不指定则根据宽高比自动判断
+| √ | autoplay | boolean | false |是否自动播放
+| √ | controls | boolean | true |是否显示默认播放控件(播放/暂停按钮、播放进度、时间)
+| √ | danmuBtn | boolean | false |是否显示弹幕按钮,只在初始化时有效,不能动态变更
+| √ | danmuList | Array. | [] |弹幕列表
+| √ | duration | number | |指定视频时长
+| √ | enableDanmu | boolean | false |是否展示弹幕,只在初始化时有效,不能动态变更
+| √ | enablePlayGesture | boolean | false |是否开启播放手势,即双击切换播放/暂停
+| √ | enableProgressGesture | boolean | true |是否开启控制进度的手势
+| √ | initialTime | number | 0 |指定视频初始播放位置
+| √ | loop | boolean | false |是否循环播放
+| √ | muted | boolean | false |是否静音播放
+| √ | objectFit | string | contain |当视频大小与 video 容器大小不一致时,视频的表现形式
+| √ | onEnded | Function | |当播放到末尾时触发 ended 事件
+| √ | onError | Function | |视频播放出错时触发
+| √ | onFullscreenChange | Function | |视频进入和退出全屏时触发,event.detail = {fullScreen, direction},direction 有效值为 vertical 或 horizontal
+| √ | onPause | Function | |当暂停播放时触发 pause 事件
+| √ | onPlay | Function | |当开始/继续播放时触发play事件
+| √ | onProgress | Function | |加载进度变化时触发,只支持一段加载。event.detail = {buffered},百分比
+| √ | onTimeUpdate | Function | |播放进度变化时触发,event.detail = {currentTime, duration}。触发频率 250ms 一次
+| √ | onWaiting | Function | |视频出现缓冲时触发
+| √ | playBtnPosition | string | bottom |播放按钮的位置
+| √ | poster | string | |视频封面的图片网络资源地址。若 controls 属性值为 false 则设置 poster 无效
+| √ | showCenterPlayBtn | boolean | true |是否显示视频中间的播放按钮
+| √ | showFullscreenBtn | boolean | true |是否显示全屏按钮
+| √ | showMuteBtn | boolean | false |是否显示静音按钮
+| √ | showPlayBtn | boolean | true |是否显示视频底部控制栏的播放按钮
+| √ | showProgress | boolean | true |若不设置,宽度大于240时才会显示
+| √ | src | string | |要播放视频的资源地址
+| √ | title | string | |视频的标题,全屏时在顶部展示
+| √ | vslideGesture | boolean | false |在非全屏模式下,是否开启亮度与音量调节手势(同 pageGesture)
+| √ | vslideGestureInFullscreen | boolean | true |在全屏模式下,是否开启亮度与音量调节手势
diff --git a/packages/taro-components/src/components/video/style/index.scss b/packages/taro-components/src/components/video/style/index.scss
index 016922c28efb..e2c048ba8c32 100644
--- a/packages/taro-components/src/components/video/style/index.scss
+++ b/packages/taro-components/src/components/video/style/index.scss
@@ -1,6 +1,349 @@
@charset "UTF-8";
-video {
- max-width:100%;
- height:auto;
+.taro-video {
+ // width: 300px;
+ width: 100%;
+ height: 225px;
+ display: inline-block;
+ line-height: 0;
+ overflow: hidden;
+ position: relative
}
+
+.taro-video[hidden] {
+ display: none
+}
+
+.taro-video-container {
+ width: 100%;
+ height: 100%;
+ background-color: #000;
+ display: inline-block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ overflow: hidden;
+ object-position: inherit
+}
+
+.taro-video-container.taro-video-type-fullscreen {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ -webkit-transform: translate(-50%,-50%);
+ transform: translate(-50%,-50%);
+ z-index: 999
+}
+
+.taro-video-container.taro-video-type-fullscreen.taro-video-type-rotate-left {
+ -webkit-transform: translate(-50%,-50%) rotate(-90deg);
+ transform: translate(-50%,-50%) rotate(-90deg)
+}
+
+.taro-video-container.taro-video-type-fullscreen.taro-video-type-rotate-right {
+ -webkit-transform: translate(-50%,-50%) rotate(90deg);
+ transform: translate(-50%,-50%) rotate(90deg)
+}
+
+.taro-video-video {
+ width: 100%;
+ height: 100%;
+ object-position: inherit
+}
+
+.taro-video-cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ justify-content: center;
+ -webkit-box-align: center;
+ -webkit-align-items: center;
+ align-items: center;
+ background-color: rgba(1,1,1,.5);
+ z-index: 1
+}
+
+.taro-video-cover-play-button {
+ width: 40px;
+ height: 40px;
+ background-size: 50%;
+ background-repeat: no-repeat;
+ background-position: 50% 50%
+}
+
+.taro-video-cover-duration {
+ color: #fff;
+ font-size: 16px;
+ line-height: 1;
+ margin-top: 10px
+}
+
+.taro-video-bar {
+ visibility: hidden;
+ height: 44px;
+ background-color: rgba(0,0,0,.5);
+ overflow: hidden;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-box-align: center;
+ -webkit-align-items: center;
+ align-items: center;
+ padding: 0 10px;
+ z-index: 0
+}
+
+.taro-video-bar.taro-video-bar-full {
+ left: 0
+}
+
+.taro-video-controls {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-box-flex: 1;
+ -webkit-flex-grow: 1;
+ flex-grow: 1;
+ margin: 0 8.5px
+}
+
+.taro-video-control-button {
+ width: 13px;
+ height: 15px;
+ padding: 14.5px 12.5px 14.5px 12.5px;
+ margin-left: -8.5px;
+ box-sizing: content-box
+}
+
+.taro-video-control-button:after {
+ content: "";
+ display: block;
+ width: 100%;
+ height: 100%;
+ background-size: 100%;
+ background-position: 50% 50%;
+ background-repeat: no-repeat
+}
+
+.taro-video-control-button.taro-video-control-button-play:after,.taro-video-cover-play-button {
+ background-image: url("../images/play.png")
+}
+
+.taro-video-control-button.taro-video-control-button-pause:after {
+ background-image: url("../images//pause.png")
+}
+
+.taro-video-current-time,.taro-video-duration {
+ height: 14.5px;
+ line-height: 14.5px;
+ margin-top: 15px;
+ margin-bottom: 14.5px;
+ font-size: 12px;
+ color: #cbcbcb
+}
+
+.taro-video-progress-container {
+ -webkit-box-flex: 2;
+ -webkit-flex-grow: 2;
+ flex-grow: 2;
+ position: relative
+}
+
+.taro-video-progress {
+ height: 2px;
+ margin: 21px 12px;
+ background-color: hsla(0,0%,100%,.4);
+ position: relative
+}
+
+.taro-video-progress-buffered {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 0;
+ height: 100%;
+ -webkit-transition: width .1s;
+ transition: width .1s;
+ background-color: hsla(0,0%,100%,.8)
+}
+
+.taro-video-ball {
+ width: 16px;
+ height: 16px;
+ padding: 14px;
+ position: absolute;
+ top: -21px;
+ box-sizing: content-box;
+ left: 0;
+ margin-left: -22px
+}
+
+.taro-video-inner {
+ width: 100%;
+ height: 100%;
+ background-color: #fff;
+ border-radius: 50%
+}
+
+.taro-video-danmu-button {
+ white-space: nowrap;
+ line-height: 1;
+ padding: 2px 10px;
+ border: 1px solid #fff;
+ border-radius: 5px;
+ font-size: 13px;
+ color: #fff;
+ margin: 0 8.5px
+}
+
+.taro-video-danmu-button.taro-video-danmu-button-active {
+ border-color: #48c23d;
+ color: #48c23d
+}
+
+.taro-video-fullscreen,
+.taro-video-mute {
+ width: 17px;
+ height: 17px;
+ padding: 8.5px;
+ box-sizing: content-box;
+ background-size: 50%;
+ background-position: 50% 50%;
+ background-repeat: no-repeat
+}
+.taro-video-fullscreen {
+ background-image: url("../images/full.png");
+}
+
+.taro-video-fullscreen.taro-video-type-fullscreen {
+ background-image: url("../images/shrink.png")
+}
+
+.taro-video-mute {
+ background-image: url("../images/unmute.png");
+}
+
+.taro-video-mute.taro-video-type-mute {
+ background-image: url("../images/mute.png")
+}
+
+.taro-video-danmu {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ margin-top: 14px;
+ margin-bottom: 44px;
+ font-size: 14px;
+ line-height: 14px;
+ overflow: visible
+}
+
+.taro-video-danmu-item {
+ line-height: 1;
+ position: absolute;
+ color: #fff;
+ white-space: nowrap;
+ left: 100%;
+ -webkit-transform: translatex(0);
+ transform: translatex(0);
+ -webkit-transition-property: left,-webkit-transform;
+ transition-property: left,-webkit-transform;
+ transition-property: left,transform;
+ transition-property: left,transform,-webkit-transform;
+ -webkit-transition-duration: 3s;
+ transition-duration: 3s;
+ -webkit-transition-timing-function: linear;
+ transition-timing-function: linear
+}
+
+.taro-video-toast {
+ pointer-events: none;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ -webkit-transform: translate(-50%,-50%);
+ transform: translate(-50%,-50%);
+ border-radius: 5px;
+ background-color: hsla(0,0%,100%,.8);
+ color: #000;
+ display: block;
+ visibility: hidden;
+}
+
+.taro-video-toast.taro-video-toast-volume {
+ width: 100px;
+ height: 100px;
+ display: block
+}
+
+.taro-video-toast-volume .taro-video-toast-title {
+ width: 100%;
+ font-size: 12px;
+ line-height: 16px;
+ text-align: center;
+ margin-top: 10px;
+ display: block
+}
+
+.taro-video-toast-volume .taro-video-toast-icon {
+ fill: #000;
+ width: 50%;
+ height: 50%;
+ margin-left: 25%;
+ display: block;
+ background-image: url('../images/volume.png');
+ background-size: 50%;
+ background-position: 50% 50%;
+ background-repeat: no-repeat
+}
+
+.taro-video-toast-volume .taro-video-toast-value {
+ width: 80px;
+ height: 5px;
+ margin-top: 5px;
+ margin-left: 10px
+}
+
+.taro-video-toast-volume .taro-video-toast-value>.taro-video-toast-value-content {
+ overflow: hidden
+}
+
+.taro-video-toast-volume-grids {
+ width: 80px;
+ height: 5px
+}
+
+.taro-video-toast-volume-grids-item {
+ float: left;
+ width: 7.1px;
+ height: 5px;
+ background-color: #000
+}
+
+.taro-video-toast-volume-grids-item:not(:first-child) {
+ margin-left: 1px
+}
+
+.taro-video-toast.taro-video-toast-progress {
+ background-color: rgba(0,0,0,.8);
+ color: #fff;
+ font-size: 14px;
+ line-height: 18px;
+ padding: 6px
+}
\ No newline at end of file
diff --git a/packages/taro-components/src/components/video/utils.js b/packages/taro-components/src/components/video/utils.js
new file mode 100644
index 000000000000..d1e52025fe6c
--- /dev/null
+++ b/packages/taro-components/src/components/video/utils.js
@@ -0,0 +1,14 @@
+export const formatTime = time => {
+ if (time === null) return ''
+ const sec = Math.round(time % 60)
+ const min = Math.round((time - sec) / 60)
+ return `${min < 10 ? `0${min}` : min}:${sec < 10 ? `0${sec}` : sec}`
+}
+
+export const calcDist = (x, y) => {
+ return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
+}
+
+export const normalizeNumber = number => {
+ return Math.max(-1, Math.min(number, 1))
+}
diff --git a/packages/taro-components/src/utils/touchable.js b/packages/taro-components/src/utils/touchable.js
index 2db90382e3cc..9bae3a73cc80 100644
--- a/packages/taro-components/src/utils/touchable.js
+++ b/packages/taro-components/src/utils/touchable.js
@@ -8,35 +8,35 @@ const touchable = (opt = {
return ComponentClass => {
return class TouchableComponent extends Taro.Component {
static defaultProps = {
- bindTouchStart: null,
- bindTouchMove: null,
- bindTouchEnd: null,
- bindTouchCancel: null,
- bindLongTap: null
+ onTouchStart: null,
+ onTouchMove: null,
+ onTouchEnd: null,
+ onTouchCancel: null,
+ onLongTap: null
}
timer = null
onTouchStart = e => {
- const { bindTouchStart, bindLongTap } = this.props
- bindTouchStart && bindTouchStart(e)
+ const { onTouchStart, onLongTap } = this.props
+ onTouchStart && onTouchStart(e)
this.timer = setTimeout(() => {
- bindLongTap && bindLongTap(e)
+ onLongTap && onLongTap(e)
}, opt.longTapTime)
}
onTouchMove = e => {
this.timer && clearTimeout(this.timer)
- const { bindTouchMove } = this.props
- bindTouchMove && bindTouchMove(e)
+ const { onTouchMove } = this.props
+ onTouchMove && onTouchMove(e)
}
onTouchEnd = e => {
this.timer && clearTimeout(this.timer)
- const { bindTouchEnd } = this.props
- bindTouchEnd && bindTouchEnd(e)
+ const { onTouchEnd } = this.props
+ onTouchEnd && onTouchEnd(e)
}
onTouchCancel = e => {
this.timer && clearTimeout(this.timer)
- const { bindTouchCancel } = this.props
- bindTouchCancel && bindTouchCancel(e)
+ const { onTouchCancel } = this.props
+ onTouchCancel && onTouchCancel(e)
}
render () {
const props = {
@@ -45,11 +45,11 @@ const touchable = (opt = {
onTouchEnd: this.onTouchEnd,
onTouchCancel: this.onTouchCancel,
...omit(this.props, [
- 'bindTouchStart',
- 'bindTouchMove',
- 'bindTouchEnd',
- 'bindTouchCancel',
- 'bindLongTap'
+ 'onTouchStart',
+ 'onTouchMove',
+ 'onTouchEnd',
+ 'onTouchCancel',
+ 'onLongTap'
])
}
return
diff --git a/packages/taro-h5/src/api/index.js b/packages/taro-h5/src/api/index.js
index b0f86fe6647d..ac1a638d160c 100644
--- a/packages/taro-h5/src/api/index.js
+++ b/packages/taro-h5/src/api/index.js
@@ -2,18 +2,19 @@
export * from './unsupportedApi'
/* 已实现api */
-export * from './request'
export * from './canvas'
export * from './createAnimation'
export * from './createSelectorQuery'
-export * from './webSocket'
-export * from './storage'
+export * from './imageUtils'
export * from './interactive'
-export * from './tabBar'
-export * from './system'
-export * from './others'
export * from './navigationBar'
-export * from './imageUtils'
+export * from './others'
export * from './pullDownRefresh'
+export * from './request'
+export * from './storage'
+export * from './system'
+export * from './tabBar'
+export * from './videoUtils'
+export * from './webSocket'
export * from './privateApis'
diff --git a/packages/taro-h5/src/api/videoUtils/index.js b/packages/taro-h5/src/api/videoUtils/index.js
new file mode 100644
index 000000000000..faffffacec55
--- /dev/null
+++ b/packages/taro-h5/src/api/videoUtils/index.js
@@ -0,0 +1,63 @@
+import { shouleBeObject, getParameterError } from '../utils'
+
+export function chooseImage (options) {
+ // options must be an Object
+ const isObject = shouleBeObject(options)
+ if (!isObject.res) {
+ const res = { errMsg: `chooseImage${isObject.msg}` }
+ console.error(res.errMsg)
+ return Promise.reject(res)
+ }
+
+ const { count = 1, success, fail, complete } = options
+ const res = {
+ errMsg: 'chooseImage:ok',
+ tempFilePaths: [],
+ tempFiles: []
+ }
+
+ if (count && typeof count !== 'number') {
+ res.errMsg = getParameterError({
+ name: 'chooseImage',
+ para: 'count',
+ correct: 'Number',
+ wrong: count
+ })
+ console.error(res.errMsg)
+ typeof fail === 'function' && fail(res)
+ typeof complete === 'function' && complete(res)
+ return Promise.reject(res)
+ }
+
+ let taroChooseImageId = document.getElementById('taroChooseImage')
+ if (!taroChooseImageId) {
+ let obj = document.createElement('input')
+ obj.setAttribute('type', 'file')
+ obj.setAttribute('id', 'taroChooseImage')
+ obj.setAttribute('multiple', 'multiple')
+ obj.setAttribute('accept', 'image/*')
+ obj.setAttribute('style', 'position: fixed; top: -4000px; left: -3000px; z-index: -300;')
+ document.body.appendChild(obj)
+ taroChooseImageId = document.getElementById('taroChooseImage')
+ }
+ let taroChooseImageCallback
+ const taroChooseImagePromise = new Promise(resolve => {
+ taroChooseImageCallback = resolve
+ })
+ let TaroMouseEvents = document.createEvent('MouseEvents')
+ TaroMouseEvents.initEvent('click', true, true)
+ taroChooseImageId.dispatchEvent(TaroMouseEvents)
+ taroChooseImageId.onchange = function (e) {
+ let arr = Array.from(e.target.files)
+ arr && arr.forEach(item => {
+ let blob = new Blob([item])
+ let url = URL.createObjectURL(blob)
+ res.tempFilePaths.push(url)
+ res.tempFiles.push({path: url, size: item.size, type: item.type})
+ })
+ typeof success === 'function' && success(res)
+ typeof complete === 'function' && complete(res)
+ taroChooseImageCallback(res)
+ }
+ return taroChooseImagePromise
+}
diff --git a/packages/taro-router/src/router/route.tsx b/packages/taro-router/src/router/route.tsx
index 7a7cfb4665c7..77b0a1bb3375 100644
--- a/packages/taro-router/src/router/route.tsx
+++ b/packages/taro-router/src/router/route.tsx
@@ -133,7 +133,10 @@ class Route extends Component {
const WrappedComponent = this.wrappedComponent
return (
-
+
)
diff --git a/packages/taro-router/src/router/router.tsx b/packages/taro-router/src/router/router.tsx
index b9551ed0010d..e5b928f3525d 100644
--- a/packages/taro-router/src/router/router.tsx
+++ b/packages/taro-router/src/router/router.tsx
@@ -142,7 +142,9 @@ class Router extends Component
{
const currentLocation = Taro._$router
router.currentPages.length = this.state.routeStack.length
return (
-
+
{this.state.routeStack.map(({ path, componentLoader, isIndex, key, isRedirect }, k) => {
return (
{