index.js 11 KB

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