index.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. import baseComponent from '../helpers/baseComponent'
  2. import classNames from '../helpers/libs/classNames'
  3. import shallowEqual from '../helpers/libs/shallowEqual'
  4. import styleToCssString from '../helpers/libs/styleToCssString'
  5. import fieldNamesBehavior from '../helpers/mixins/fieldNamesBehavior'
  6. import { getTouchPoints, getPointsNumber } from '../helpers/shared/gestures'
  7. import { vibrateShort } from '../helpers/hooks/useNativeAPI'
  8. import { props } from './props'
  9. import {
  10. getRealCol,
  11. getRealValue,
  12. getIndexFromValue,
  13. getLabelFromIndex,
  14. } from './utils'
  15. function getStyles(value) {
  16. return Array.isArray(value) ? value.map((n) => styleToCssString(n)) : styleToCssString(value)
  17. }
  18. baseComponent({
  19. behaviors: [fieldNamesBehavior],
  20. properties: props,
  21. data: {
  22. inputValue: null,
  23. selectedIndex: null,
  24. selectedValue: null,
  25. cols: [],
  26. extIndicatorStyle: '',
  27. extItemStyle: '',
  28. extMaskStyle: '',
  29. contentStyle: '',
  30. itemCount: 7, // 默认显示的子元素个数
  31. styles: {},
  32. },
  33. computed: {
  34. classes: ['prefixCls, labelAlign', function(prefixCls, labelAlign) {
  35. const wrap = classNames(prefixCls, {
  36. [`${prefixCls}--${labelAlign}`]: labelAlign,
  37. })
  38. const mask = `${prefixCls}__mask`
  39. const indicator = `${prefixCls}__indicator`
  40. const content = `${prefixCls}__content`
  41. const item = `${prefixCls}__item`
  42. return {
  43. wrap,
  44. mask,
  45. indicator,
  46. content,
  47. item,
  48. }
  49. }],
  50. },
  51. observers: {
  52. itemHeight(newVal) {
  53. this.updatedStyles(newVal)
  54. },
  55. itemStyle(newVal) {
  56. this.setData({
  57. extItemStyle: getStyles(newVal),
  58. })
  59. },
  60. indicatorStyle(newVal) {
  61. this.setData({
  62. extIndicatorStyle: getStyles(newVal),
  63. })
  64. },
  65. maskStyle(newVal) {
  66. this.setData({
  67. extMaskStyle: getStyles(newVal),
  68. })
  69. },
  70. ['value, options'](value, options) {
  71. const { controlled } = this.data
  72. const fieldNames = this.getFieldNames()
  73. const cols = getRealCol(options, fieldNames)
  74. if (!shallowEqual(this.data.cols, cols)) {
  75. this.setData({ cols })
  76. }
  77. if (controlled) {
  78. this.setValue(value, true)
  79. }
  80. },
  81. inputValue(newVal) {
  82. const {
  83. selectedIndex,
  84. selectedValue,
  85. } = this.getValue(newVal)
  86. this.setData({
  87. selectedIndex,
  88. selectedValue,
  89. })
  90. },
  91. },
  92. methods: {
  93. updatedStyles(itemHeight) {
  94. let num = this.data.itemCount
  95. if (num % 2 === 0) {
  96. num--
  97. }
  98. num--
  99. num /= 2
  100. const wrap = `height: ${itemHeight * this.data.itemCount}px;`
  101. const item = `line-height: ${itemHeight}px; height: ${itemHeight}px;`
  102. const content = `padding: ${itemHeight * num}px 0;`
  103. const indicator = `top: ${itemHeight * num}px; height: ${itemHeight}px;`
  104. const mask = `background-size: 100% ${itemHeight * num}px;`
  105. const styles = {
  106. wrap,
  107. item,
  108. content,
  109. indicator,
  110. mask,
  111. }
  112. this.setData({ styles })
  113. },
  114. updated(inputValue, isForce) {
  115. if (this.data.inputValue !== inputValue || isForce) {
  116. this.setData({
  117. inputValue,
  118. })
  119. }
  120. // 设置选择器位置
  121. if (isForce) {
  122. this.select(inputValue, this.data.itemHeight, (y) => this.scrollTo(y, 0, false))
  123. }
  124. },
  125. setValue(value, isForce) {
  126. const { value: inputValue } = this.getValue(value)
  127. this.updated(inputValue, isForce)
  128. },
  129. getValue(value = this.data.inputValue, cols = this.data.cols) {
  130. const fieldNames = this.getFieldNames()
  131. const inputValue = getRealValue(value, cols, fieldNames) || null
  132. const selectedValue = inputValue
  133. const selectedIndex = getIndexFromValue(value, cols, fieldNames)
  134. const displayValue = getLabelFromIndex(selectedIndex, cols, fieldNames.label)
  135. return {
  136. value: inputValue,
  137. displayValue,
  138. selectedIndex,
  139. selectedValue,
  140. cols,
  141. }
  142. },
  143. /**
  144. * 设置选择器的位置信息
  145. */
  146. scrollTo(y, time = .3, runCallbacks = true) {
  147. if (this.scrollY !== y) {
  148. if (this.runCallbacks) {
  149. clearTimeout(this.runCallbacks)
  150. this.runCallbacks = null
  151. }
  152. this.scrollY = y
  153. this.setTransform(-y, time, () => {
  154. runCallbacks && (this.runCallbacks = setTimeout(() => {
  155. this.setTransform(-y, 0, this.scrollingComplete)
  156. }, +time * 1000))
  157. })
  158. }
  159. },
  160. /**
  161. * 滚动结束时的回调函数
  162. */
  163. onFinish() {
  164. this.isMoving = false
  165. let targetY = this.scrollY
  166. const { cols, itemHeight } = this.data
  167. const height = (cols.length - 1) * itemHeight
  168. let time = .3
  169. // const velocity = this.Velocity.getVelocity(targetY) * 4
  170. // if (velocity) {
  171. // targetY = velocity * 40 + targetY
  172. // time = Math.abs(velocity) * .1
  173. // time = parseFloat(time.toFixed(2))
  174. // }
  175. if (targetY % itemHeight !== 0) {
  176. targetY = Math.round(targetY / itemHeight) * itemHeight
  177. }
  178. if (targetY < 0) {
  179. targetY = 0
  180. } else if (targetY > height) {
  181. targetY = height
  182. }
  183. // check disabled & reset
  184. const child = this.getChildMeta(targetY, itemHeight)
  185. const disabledName = this.getFieldName('disabled')
  186. if (child && !child[disabledName]) {
  187. this.scrollTo(targetY, time < .3 ? .3 : time)
  188. } else {
  189. this.select(this.data.inputValue, itemHeight, (y) => this.scrollTo(y, 0, false))
  190. }
  191. this.onScrollChange()
  192. },
  193. /**
  194. * 手指触摸动作开始
  195. */
  196. onTouchStart(e) {
  197. if (getPointsNumber(e) > 1) return
  198. this.isMoving = true
  199. this.startY = getTouchPoints(e).y
  200. this.lastY = this.scrollY
  201. this.triggerEvent('beforeChange', this.getValue())
  202. },
  203. /**
  204. * 手指触摸后移动
  205. */
  206. onTouchMove(e) {
  207. if (!this.isMoving || getPointsNumber(e) > 1) return
  208. this.scrollY = this.lastY - getTouchPoints(e).y + this.startY
  209. this.setTransform(-this.scrollY, false, this.onScrollChange)
  210. // this.Velocity.record(this.scrollY)
  211. },
  212. /**
  213. * 手指触摸动作结束
  214. */
  215. onTouchEnd(e) {
  216. if (getPointsNumber(e) > 1) return
  217. this.onFinish()
  218. },
  219. /**
  220. * 手指触摸后马上离开
  221. */
  222. onItemClick(e) {
  223. const { index, disabled } = e.currentTarget.dataset
  224. if (!disabled) {
  225. this.scrollTo(index * this.data.itemHeight)
  226. }
  227. },
  228. /**
  229. * 设置滚动样式
  230. */
  231. setTransform(y, time, cb) {
  232. const contentStyle = {
  233. transform: `translate3d(0,${y}px,0)`,
  234. transition: time ? `cubic-bezier(0, 0, 0.2, 1.15) ${time}s` : 'none',
  235. }
  236. this.setData({ contentStyle: styleToCssString(contentStyle) }, cb)
  237. },
  238. /**
  239. * 设置选择器
  240. */
  241. select(value, itemHeight, scrollTo) {
  242. const fieldNames = this.getFieldNames()
  243. const { cols: children } = this.data
  244. const index = getIndexFromValue(value, children, fieldNames)
  245. this.selectByIndex(index, itemHeight, scrollTo)
  246. },
  247. /**
  248. * 通过元素的索引值设置选择器
  249. */
  250. selectByIndex(index, itemHeight, zscrollTo) {
  251. if (index < 0 || index >= this.data.cols.length || !itemHeight) return
  252. zscrollTo.call(this, index * itemHeight)
  253. },
  254. /**
  255. * 计算子元素的索引值
  256. */
  257. computeChildIndex(top, itemHeight, childrenLength) {
  258. const index = Math.round(top / itemHeight)
  259. return Math.min(index, childrenLength - 1)
  260. },
  261. /**
  262. * 获取子元素的属性
  263. */
  264. getChildMeta(top, itemHeight) {
  265. const { cols: children } = this.data
  266. const index = this.computeChildIndex(top, itemHeight, children.length)
  267. const child = children[index]
  268. return child
  269. },
  270. /**
  271. * 滚动完成的回调函数
  272. */
  273. scrollingComplete() {
  274. const top = this.scrollY
  275. if (top >= 0) {
  276. const valueName = this.getFieldName('value')
  277. const { itemHeight } = this.data
  278. const child = this.getChildMeta(top, itemHeight)
  279. if (child) {
  280. const inputValue = child[valueName]
  281. if (this.data.inputValue !== inputValue) {
  282. this.fireValueChange(inputValue)
  283. }
  284. }
  285. }
  286. },
  287. /**
  288. * 滚动数据选择变化后的回调函数
  289. */
  290. onScrollChange() {
  291. const top = this.scrollY
  292. if (top >= 0) {
  293. const valueName = this.getFieldName('value')
  294. const { cols: children, itemHeight } = this.data
  295. const index = this.computeChildIndex(top, itemHeight, children.length)
  296. if (this.scrollValue !== index) {
  297. this.scrollValue = index
  298. const child = children[index]
  299. if (child) {
  300. const values = this.getValue(child[valueName])
  301. this.triggerEvent('scrollChange', values)
  302. }
  303. // 振动反馈
  304. vibrateShort()
  305. }
  306. }
  307. },
  308. /**
  309. * 数据选择变化后的回调函数
  310. */
  311. fireValueChange(value) {
  312. if (!this.data.controlled) {
  313. this.updated(value)
  314. }
  315. this.triggerEvent('valueChange', this.getValue(value))
  316. // 振动反馈
  317. vibrateShort()
  318. },
  319. },
  320. created() {
  321. this.scrollValue = undefined
  322. this.scrollY = -1
  323. this.lastY = 0
  324. this.startY = 0
  325. this.isMoving = false
  326. // this.Velocity = ((minInterval = 30, maxInterval = 100) => {
  327. // let _time = 0
  328. // let _y = 0
  329. // let _velocity = 0
  330. // const recorder = {
  331. // record: (y) => {
  332. // const now = +new Date()
  333. // _velocity = (y - _y) / (now - _time)
  334. // if (now - _time >= minInterval) {
  335. // _velocity = now - _time <= maxInterval ? _velocity : 0
  336. // _y = y
  337. // _time = now
  338. // }
  339. // },
  340. // getVelocity: (y) => {
  341. // if (y !== _y) {
  342. // recorder.record(y)
  343. // }
  344. // return _velocity
  345. // },
  346. // }
  347. // return recorder
  348. // })()
  349. },
  350. attached() {
  351. const { defaultValue, value, controlled, options, itemHeight } = this.data
  352. const inputValue = controlled ? value : defaultValue
  353. const fieldNames = this.getFieldNames()
  354. const cols = getRealCol(options, fieldNames)
  355. this.updatedStyles(itemHeight)
  356. this.setData({ cols })
  357. this.setValue(inputValue, true)
  358. },
  359. })