index.js 10 KB

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