index.js 9.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import { VantComponent } from '../common/component';
  2. import { touch } from '../mixins/touch';
  3. import { nextTick } from '../common/utils';
  4. VantComponent({
  5. mixins: [touch],
  6. classes: ['nav-class', 'tab-class', 'tab-active-class', 'line-class'],
  7. relation: {
  8. name: 'tab',
  9. type: 'descendant',
  10. linked(child) {
  11. this.child.push(child);
  12. this.updateTabs(this.data.tabs.concat(child.data));
  13. },
  14. unlinked(child) {
  15. const index = this.child.indexOf(child);
  16. const { tabs } = this.data;
  17. tabs.splice(index, 1);
  18. this.child.splice(index, 1);
  19. this.updateTabs(tabs);
  20. }
  21. },
  22. props: {
  23. color: String,
  24. sticky: Boolean,
  25. animated: Boolean,
  26. swipeable: Boolean,
  27. lineWidth: {
  28. type: Number,
  29. value: -1
  30. },
  31. lineHeight: {
  32. type: Number,
  33. value: -1
  34. },
  35. active: {
  36. type: Number,
  37. value: 0
  38. },
  39. type: {
  40. type: String,
  41. value: 'line'
  42. },
  43. border: {
  44. type: Boolean,
  45. value: true
  46. },
  47. duration: {
  48. type: Number,
  49. value: 0.3
  50. },
  51. zIndex: {
  52. type: Number,
  53. value: 1
  54. },
  55. swipeThreshold: {
  56. type: Number,
  57. value: 4
  58. },
  59. offsetTop: {
  60. type: Number,
  61. value: 0
  62. }
  63. },
  64. data: {
  65. tabs: [],
  66. lineStyle: '',
  67. scrollLeft: 0,
  68. scrollable: false,
  69. trackStyle: '',
  70. wrapStyle: '',
  71. position: ''
  72. },
  73. watch: {
  74. swipeThreshold() {
  75. this.set({
  76. scrollable: this.child.length > this.data.swipeThreshold
  77. });
  78. },
  79. color: 'setLine',
  80. lineWidth: 'setLine',
  81. lineHeight: 'setLine',
  82. active: 'setActiveTab',
  83. animated: 'setTrack',
  84. offsetTop: 'setWrapStyle'
  85. },
  86. beforeCreate() {
  87. this.child = [];
  88. },
  89. mounted() {
  90. this.setLine(true);
  91. this.setTrack();
  92. this.scrollIntoView();
  93. this.getRect('.van-tabs__wrap').then((rect) => {
  94. this.navHeight = rect.height;
  95. this.observerContentScroll();
  96. });
  97. },
  98. destroyed() {
  99. // @ts-ignore
  100. this.createIntersectionObserver().disconnect();
  101. },
  102. methods: {
  103. updateTabs(tabs) {
  104. tabs = tabs || this.data.tabs;
  105. this.set({
  106. tabs,
  107. scrollable: tabs.length > this.data.swipeThreshold
  108. });
  109. this.setActiveTab();
  110. },
  111. trigger(eventName, index) {
  112. this.$emit(eventName, {
  113. index,
  114. title: this.data.tabs[index].title
  115. });
  116. },
  117. onTap(event) {
  118. const { index } = event.currentTarget.dataset;
  119. if (this.data.tabs[index].disabled) {
  120. this.trigger('disabled', index);
  121. }
  122. else {
  123. this.trigger('click', index);
  124. this.setActive(index);
  125. }
  126. },
  127. setActive(active) {
  128. if (active !== this.data.active) {
  129. this.trigger('change', active);
  130. this.set({ active });
  131. this.setActiveTab();
  132. }
  133. },
  134. setLine(skipTransition) {
  135. if (this.data.type !== 'line') {
  136. return;
  137. }
  138. const { color, active, duration, lineWidth, lineHeight } = this.data;
  139. this.getRect('.van-tab', true).then((rects) => {
  140. const rect = rects[active];
  141. const width = lineWidth !== -1 ? lineWidth : rect.width / 2;
  142. const height = lineHeight !== -1 ? `height: ${lineHeight}px;` : '';
  143. let left = rects
  144. .slice(0, active)
  145. .reduce((prev, curr) => prev + curr.width, 0);
  146. left += (rect.width - width) / 2;
  147. const transition = skipTransition
  148. ? ''
  149. : `transition-duration: ${duration}s; -webkit-transition-duration: ${duration}s;`;
  150. this.set({
  151. lineStyle: `
  152. ${height}
  153. width: ${width}px;
  154. background-color: ${color};
  155. -webkit-transform: translateX(${left}px);
  156. transform: translateX(${left}px);
  157. ${transition}
  158. `
  159. });
  160. });
  161. },
  162. setTrack() {
  163. const { animated, active, duration } = this.data;
  164. if (!animated)
  165. return '';
  166. this.getRect('.van-tabs__content').then((rect) => {
  167. const { width } = rect;
  168. this.set({
  169. trackStyle: `
  170. width: ${width * this.child.length}px;
  171. left: ${-1 * active * width}px;
  172. transition: left ${duration}s;
  173. display: -webkit-box;
  174. display: flex;
  175. `
  176. });
  177. const props = { width, animated };
  178. this.child.forEach((item) => {
  179. item.set(props);
  180. });
  181. });
  182. },
  183. setActiveTab() {
  184. this.child.forEach((item, index) => {
  185. const data = {
  186. active: index === this.data.active
  187. };
  188. if (data.active) {
  189. data.inited = true;
  190. }
  191. if (data.active !== item.data.active) {
  192. item.set(data);
  193. }
  194. });
  195. nextTick(() => {
  196. this.setLine();
  197. this.setTrack();
  198. this.scrollIntoView();
  199. });
  200. },
  201. // scroll active tab into view
  202. scrollIntoView() {
  203. const { active, scrollable } = this.data;
  204. if (!scrollable) {
  205. return;
  206. }
  207. Promise.all([
  208. this.getRect('.van-tab', true),
  209. this.getRect('.van-tabs__nav')
  210. ]).then(([tabRects, navRect]) => {
  211. const tabRect = tabRects[active];
  212. const offsetLeft = tabRects
  213. .slice(0, active)
  214. .reduce((prev, curr) => prev + curr.width, 0);
  215. this.set({
  216. scrollLeft: offsetLeft - (navRect.width - tabRect.width) / 2
  217. });
  218. });
  219. },
  220. onTouchStart(event) {
  221. if (!this.data.swipeable)
  222. return;
  223. this.touchStart(event);
  224. },
  225. onTouchMove(event) {
  226. if (!this.data.swipeable)
  227. return;
  228. this.touchMove(event);
  229. },
  230. // watch swipe touch end
  231. onTouchEnd() {
  232. if (!this.data.swipeable)
  233. return;
  234. const { active, tabs } = this.data;
  235. const { direction, deltaX, offsetX } = this;
  236. const minSwipeDistance = 50;
  237. if (direction === 'horizontal' && offsetX >= minSwipeDistance) {
  238. if (deltaX > 0 && active !== 0) {
  239. this.setActive(active - 1);
  240. }
  241. else if (deltaX < 0 && active !== tabs.length - 1) {
  242. this.setActive(active + 1);
  243. }
  244. }
  245. },
  246. setWrapStyle() {
  247. const { offsetTop, position } = this.data;
  248. let wrapStyle;
  249. switch (position) {
  250. case 'top':
  251. wrapStyle = `
  252. top: ${offsetTop}px;
  253. position: fixed;
  254. `;
  255. break;
  256. case 'bottom':
  257. wrapStyle = `
  258. top: auto;
  259. bottom: 0;
  260. `;
  261. break;
  262. default:
  263. wrapStyle = '';
  264. }
  265. // cut down `set`
  266. if (wrapStyle === this.data.wrapStyle)
  267. return;
  268. this.set({ wrapStyle });
  269. },
  270. observerContentScroll() {
  271. if (!this.data.sticky) {
  272. return;
  273. }
  274. const { offsetTop } = this.data;
  275. const { windowHeight } = wx.getSystemInfoSync();
  276. // @ts-ignore
  277. this.createIntersectionObserver().disconnect();
  278. // @ts-ignore
  279. this.createIntersectionObserver()
  280. .relativeToViewport({ top: -(this.navHeight + offsetTop) })
  281. .observe('.van-tabs', (res) => {
  282. const { top } = res.boundingClientRect;
  283. if (top > offsetTop) {
  284. return;
  285. }
  286. const position = res.intersectionRatio > 0 ? 'top' : 'bottom';
  287. this.$emit('scroll', {
  288. scrollTop: top + offsetTop,
  289. isFixed: position === 'top'
  290. });
  291. this.setPosition(position);
  292. });
  293. // @ts-ignore
  294. this.createIntersectionObserver()
  295. .relativeToViewport({ bottom: -(windowHeight - 1 - offsetTop) })
  296. .observe('.van-tabs', (res) => {
  297. const { top, bottom } = res.boundingClientRect;
  298. if (bottom < this.navHeight) {
  299. return;
  300. }
  301. const position = res.intersectionRatio > 0 ? 'top' : '';
  302. this.$emit('scroll', {
  303. scrollTop: top + offsetTop,
  304. isFixed: position === 'top'
  305. });
  306. this.setPosition(position);
  307. });
  308. },
  309. setPosition(position) {
  310. if (position !== this.data.position) {
  311. this.set({ position }).then(() => {
  312. this.setWrapStyle();
  313. });
  314. }
  315. }
  316. }
  317. });