index.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import baseComponent from '../helpers/baseComponent'
  2. import classNames from '../helpers/libs/classNames'
  3. import warning from '../helpers/libs/warning'
  4. import styleToCssString from '../helpers/libs/styleToCssString'
  5. import { vibrateShort } from '../helpers/hooks/useNativeAPI'
  6. import { useRectAll } from '../helpers/hooks/useDOM'
  7. const findActiveByIndex = (current, currentName, children) => {
  8. return children.filter((child) => (
  9. child.index === current &&
  10. child.name === currentName
  11. ))[0]
  12. }
  13. const findActiveByPosition = (scrollTop, offsetY, children) => {
  14. return children.filter((child) => (
  15. scrollTop < (child.top + child.height - offsetY) &&
  16. scrollTop >= (child.top - offsetY)
  17. ))[0]
  18. }
  19. baseComponent({
  20. useExport: true,
  21. relations: {
  22. '../index-item/index': {
  23. type: 'child',
  24. observer() {
  25. this.callDebounceFn(this.updated)
  26. },
  27. },
  28. },
  29. properties: {
  30. prefixCls: {
  31. type: String,
  32. value: 'wux-index',
  33. },
  34. height: {
  35. type: [String, Number],
  36. value: 300,
  37. observer: 'updateStyle',
  38. },
  39. showIndicator: {
  40. type: Boolean,
  41. value: true,
  42. },
  43. indicatorPosition: {
  44. type: String,
  45. value: 'center',
  46. },
  47. parentOffsetTop: {
  48. type: Number,
  49. value: 0,
  50. },
  51. },
  52. data: {
  53. colHight: 0,
  54. points: [],
  55. scrollTop: 0,
  56. children: [],
  57. moving: false,
  58. current: 0,
  59. currentName: '',
  60. currentBrief: '',
  61. extStyle: '',
  62. indicatorStyle: '',
  63. },
  64. computed: {
  65. classes: ['prefixCls, indicatorPosition', function(prefixCls, indicatorPosition) {
  66. const wrap = classNames(prefixCls)
  67. const nav = `${prefixCls}__nav`
  68. const navRow = `${prefixCls}__nav-row`
  69. const navCol = `${prefixCls}__nav-col`
  70. const navItem = `${prefixCls}__nav-item`
  71. const indicator = classNames(`${prefixCls}__indicator`, {
  72. [`${prefixCls}__indicator--${indicatorPosition}`]: indicatorPosition,
  73. })
  74. return {
  75. wrap,
  76. nav,
  77. navRow,
  78. navCol,
  79. navItem,
  80. indicator,
  81. }
  82. }],
  83. },
  84. methods: {
  85. /**
  86. * 更新样式
  87. */
  88. updateStyle(height = this.data.height) {
  89. const extStyle = styleToCssString({ height })
  90. if (extStyle !== this.data.extStyle) {
  91. this.setData({
  92. extStyle,
  93. })
  94. }
  95. },
  96. /**
  97. * 更新元素
  98. */
  99. updated() {
  100. const elements = this.getRelationsByName('../index-item/index')
  101. if (elements.length > 0) {
  102. elements.forEach((element, index) => {
  103. element.updated(index)
  104. })
  105. // HACK: https://github.com/wux-weapp/wux-weapp/issues/224
  106. setTimeout(this.getNavPoints.bind(this))
  107. }
  108. this.updateChildren()
  109. },
  110. /**
  111. * 设置当前激活的元素
  112. */
  113. setActive(current, currentName) {
  114. if (current !== this.data.current || currentName !== this.data.currentName) {
  115. const target = findActiveByIndex(current, currentName, this.data.children)
  116. const currentBrief = target !== undefined ? target.brief : currentName.charAt(0)
  117. if (target !== undefined) {
  118. const { colHight, indicatorPosition } = this.data
  119. const indicatorStyle = indicatorPosition === 'right'
  120. ? styleToCssString({ top: current * colHight + colHight / 2 })
  121. : ''
  122. this.setData({
  123. current,
  124. currentName,
  125. currentBrief,
  126. scrollTop: target.top - this.data.parentOffsetTop,
  127. indicatorStyle,
  128. })
  129. }
  130. // 振动反馈
  131. vibrateShort()
  132. this.triggerEvent('change', { index: current, name: currentName, brief: currentBrief })
  133. }
  134. },
  135. /**
  136. * 手指触摸动作开始
  137. */
  138. onTouchStart(e) {
  139. if (this.data.moving) return
  140. const { index, name } = e.target.dataset
  141. this.setActive(index, name)
  142. this.setData({ moving: true })
  143. },
  144. /**
  145. * 手指触摸后移动
  146. */
  147. onTouchMove(e) {
  148. const target = this.getTargetFromPoint(e.changedTouches[0].pageY)
  149. if (target !== undefined) {
  150. const { index, name } = target.dataset
  151. this.setActive(index, name)
  152. }
  153. },
  154. /**
  155. * 手指触摸动作结束
  156. */
  157. onTouchEnd() {
  158. if (!this.data.moving) return
  159. setTimeout(() => this.setData({ moving: false }), 300)
  160. },
  161. /**
  162. * 滚动事件的回调函数
  163. */
  164. onScroll(e) {
  165. if (this.data.moving) return
  166. this.checkActiveIndex.call(this, this.data, e)
  167. },
  168. /**
  169. * 获取右侧导航对应的坐标
  170. */
  171. getNavPoints() {
  172. const navColCls = `.${this.data.prefixCls}__nav-col`
  173. const navItemCls = `.${this.data.prefixCls}__nav-item`
  174. useRectAll([navColCls, navItemCls], this)
  175. .then(([cols, items]) => {
  176. if (!cols.length && !items.length) return
  177. this.setData({
  178. colHight: cols[0].height,
  179. points: items.map((n) => ({ ...n, offsets: [n.top, n.top + n.height] })),
  180. })
  181. })
  182. },
  183. /**
  184. * 根据坐标获得对应的元素
  185. */
  186. getTargetFromPoint(y) {
  187. const { points } = this.data
  188. let target
  189. for (let i = points.length - 1; i >= 0; i--) {
  190. const [a, b] = points[i].offsets
  191. // 1.判断是否为第一个元素且大于最大坐标点
  192. // 2.判断是否为最后一个元素且小于最小坐标点
  193. // 3.判断是否包含于某个坐标系内
  194. if ((i === points.length - 1 && y > b) || (i === 0 && y < a) || (y >= a && y <= b)) {
  195. target = points[i]
  196. break
  197. }
  198. }
  199. return target
  200. },
  201. getInternalHooks(key) {
  202. if (key === 'INDEX_HOOK_MARK') {
  203. return {
  204. updateChildren: this.updateChildren.bind(this),
  205. }
  206. }
  207. warning(
  208. false,
  209. '`getInternalHooks` is internal usage of the <index />. Should not call directly.'
  210. )
  211. return null
  212. },
  213. expose() {
  214. const scrollTo = (index) => {
  215. const { children } = this.data
  216. const child = typeof index === 'number'
  217. ? children.filter((child) => (child.index === index))[0]
  218. : children.filter((child) => (child.name === index))[0]
  219. if (child) {
  220. this.setData({ moving: true })
  221. this.setActive(child.index, child.name)
  222. setTimeout(() => this.setData({ moving: false }), 300)
  223. }
  224. }
  225. return {
  226. scrollTo,
  227. getInternalHooks: this.getInternalHooks.bind(this),
  228. }
  229. },
  230. },
  231. created() {
  232. const { run: checkActiveIndex } = this.useThrottleFn((data, event) => {
  233. const target = findActiveByPosition(event.detail.scrollTop, data.parentOffsetTop, data.children)
  234. if (target !== undefined) {
  235. const current = target.index
  236. const currentName = target.name
  237. const currentBrief = target.brief
  238. if (current !== data.current || currentName !== data.currentName) {
  239. this.setData({
  240. current,
  241. currentName,
  242. currentBrief,
  243. })
  244. this.triggerEvent('change', { index: current, name: currentName, brief: currentBrief })
  245. }
  246. }
  247. }, 50, { trailing: true, leading: true })
  248. this.checkActiveIndex = checkActiveIndex
  249. const propsFilter = (props) => ({
  250. name: props.name,
  251. index: props.index,
  252. top: props.top,
  253. height: props.height,
  254. brief: props.brief,
  255. })
  256. const { run: updateChildren } = this.useThrottleFn(() => {
  257. const nodeList = this.getRelationsByName('../index-item/index')
  258. const children = nodeList.map((node) => propsFilter(node.data))
  259. this.setData({
  260. children,
  261. })
  262. }, 50, { trailing: true, leading: true })
  263. this.updateChildren = updateChildren
  264. },
  265. ready() {
  266. this.updateStyle()
  267. this.getNavPoints()
  268. this.updateChildren()
  269. },
  270. })