index.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import { VantComponent } from '../common/component';
  2. import { touch } from '../mixins/touch';
  3. import { getAllRect, getRect, groupSetData, nextTick, requestAnimationFrame, } from '../common/utils';
  4. import { isDef } from '../common/validator';
  5. import { useChildren } from '../common/relation';
  6. VantComponent({
  7. mixins: [touch],
  8. classes: ['nav-class', 'tab-class', 'tab-active-class', 'line-class'],
  9. relation: useChildren('tab', function () {
  10. this.updateTabs();
  11. }),
  12. props: {
  13. sticky: Boolean,
  14. border: Boolean,
  15. swipeable: Boolean,
  16. titleActiveColor: String,
  17. titleInactiveColor: String,
  18. color: String,
  19. animated: {
  20. type: Boolean,
  21. observer() {
  22. this.children.forEach((child, index) => child.updateRender(index === this.data.currentIndex, this));
  23. },
  24. },
  25. lineWidth: {
  26. type: null,
  27. value: 40,
  28. observer: 'resize',
  29. },
  30. lineHeight: {
  31. type: null,
  32. value: -1,
  33. },
  34. active: {
  35. type: null,
  36. value: 0,
  37. observer(name) {
  38. if (name !== this.getCurrentName()) {
  39. this.setCurrentIndexByName(name);
  40. }
  41. },
  42. },
  43. type: {
  44. type: String,
  45. value: 'line',
  46. },
  47. ellipsis: {
  48. type: Boolean,
  49. value: true,
  50. },
  51. duration: {
  52. type: Number,
  53. value: 0.3,
  54. },
  55. zIndex: {
  56. type: Number,
  57. value: 1,
  58. },
  59. swipeThreshold: {
  60. type: Number,
  61. value: 5,
  62. observer(value) {
  63. this.setData({
  64. scrollable: this.children.length > value || !this.data.ellipsis,
  65. });
  66. },
  67. },
  68. offsetTop: {
  69. type: Number,
  70. value: 0,
  71. },
  72. lazyRender: {
  73. type: Boolean,
  74. value: true,
  75. },
  76. },
  77. data: {
  78. tabs: [],
  79. scrollLeft: 0,
  80. scrollable: false,
  81. currentIndex: 0,
  82. container: null,
  83. skipTransition: true,
  84. scrollWithAnimation: false,
  85. lineOffsetLeft: 0,
  86. },
  87. mounted() {
  88. requestAnimationFrame(() => {
  89. this.swiping = true;
  90. this.setData({
  91. container: () => this.createSelectorQuery().select('.van-tabs'),
  92. });
  93. this.resize();
  94. this.scrollIntoView();
  95. });
  96. },
  97. methods: {
  98. updateTabs() {
  99. const { children = [], data } = this;
  100. this.setData({
  101. tabs: children.map((child) => child.data),
  102. scrollable: this.children.length > data.swipeThreshold || !data.ellipsis,
  103. });
  104. this.setCurrentIndexByName(data.active || this.getCurrentName());
  105. },
  106. trigger(eventName, child) {
  107. const { currentIndex } = this.data;
  108. const currentChild = child || this.children[currentIndex];
  109. if (!isDef(currentChild)) {
  110. return;
  111. }
  112. this.$emit(eventName, {
  113. index: currentChild.index,
  114. name: currentChild.getComputedName(),
  115. title: currentChild.data.title,
  116. });
  117. },
  118. onTap(event) {
  119. const { index } = event.currentTarget.dataset;
  120. const child = this.children[index];
  121. if (child.data.disabled) {
  122. this.trigger('disabled', child);
  123. }
  124. else {
  125. this.setCurrentIndex(index);
  126. nextTick(() => {
  127. this.trigger('click');
  128. });
  129. }
  130. },
  131. // correct the index of active tab
  132. setCurrentIndexByName(name) {
  133. const { children = [] } = this;
  134. const matched = children.filter((child) => child.getComputedName() === name);
  135. if (matched.length) {
  136. this.setCurrentIndex(matched[0].index);
  137. }
  138. },
  139. setCurrentIndex(currentIndex) {
  140. const { data, children = [] } = this;
  141. if (!isDef(currentIndex) ||
  142. currentIndex >= children.length ||
  143. currentIndex < 0) {
  144. return;
  145. }
  146. groupSetData(this, () => {
  147. children.forEach((item, index) => {
  148. const active = index === currentIndex;
  149. if (active !== item.data.active || !item.inited) {
  150. item.updateRender(active, this);
  151. }
  152. });
  153. });
  154. if (currentIndex === data.currentIndex) {
  155. return;
  156. }
  157. const shouldEmitChange = data.currentIndex !== null;
  158. this.setData({ currentIndex });
  159. requestAnimationFrame(() => {
  160. this.resize();
  161. this.scrollIntoView();
  162. });
  163. nextTick(() => {
  164. this.trigger('input');
  165. if (shouldEmitChange) {
  166. this.trigger('change');
  167. }
  168. });
  169. },
  170. getCurrentName() {
  171. const activeTab = this.children[this.data.currentIndex];
  172. if (activeTab) {
  173. return activeTab.getComputedName();
  174. }
  175. },
  176. resize() {
  177. if (this.data.type !== 'line') {
  178. return;
  179. }
  180. const { currentIndex, ellipsis, skipTransition } = this.data;
  181. Promise.all([
  182. getAllRect(this, '.van-tab'),
  183. getRect(this, '.van-tabs__line'),
  184. ]).then(([rects = [], lineRect]) => {
  185. const rect = rects[currentIndex];
  186. if (rect == null) {
  187. return;
  188. }
  189. let lineOffsetLeft = rects
  190. .slice(0, currentIndex)
  191. .reduce((prev, curr) => prev + curr.width, 0);
  192. lineOffsetLeft +=
  193. (rect.width - lineRect.width) / 2 + (ellipsis ? 0 : 8);
  194. this.setData({ lineOffsetLeft });
  195. this.swiping = true;
  196. if (skipTransition) {
  197. nextTick(() => {
  198. this.setData({ skipTransition: false });
  199. });
  200. }
  201. });
  202. },
  203. // scroll active tab into view
  204. scrollIntoView() {
  205. const { currentIndex, scrollable, scrollWithAnimation } = this.data;
  206. if (!scrollable) {
  207. return;
  208. }
  209. Promise.all([
  210. getAllRect(this, '.van-tab'),
  211. getRect(this, '.van-tabs__nav'),
  212. ]).then(([tabRects, navRect]) => {
  213. const tabRect = tabRects[currentIndex];
  214. const offsetLeft = tabRects
  215. .slice(0, currentIndex)
  216. .reduce((prev, curr) => prev + curr.width, 0);
  217. this.setData({
  218. scrollLeft: offsetLeft - (navRect.width - tabRect.width) / 2,
  219. });
  220. if (!scrollWithAnimation) {
  221. nextTick(() => {
  222. this.setData({ scrollWithAnimation: true });
  223. });
  224. }
  225. });
  226. },
  227. onTouchScroll(event) {
  228. this.$emit('scroll', event.detail);
  229. },
  230. onTouchStart(event) {
  231. if (!this.data.swipeable)
  232. return;
  233. this.touchStart(event);
  234. },
  235. onTouchMove(event) {
  236. if (!this.data.swipeable || !this.swiping)
  237. return;
  238. this.touchMove(event);
  239. },
  240. // watch swipe touch end
  241. onTouchEnd() {
  242. if (!this.data.swipeable || !this.swiping)
  243. return;
  244. const { direction, deltaX, offsetX } = this;
  245. const minSwipeDistance = 50;
  246. if (direction === 'horizontal' && offsetX >= minSwipeDistance) {
  247. const index = this.getAvaiableTab(deltaX);
  248. if (index !== -1) {
  249. this.setCurrentIndex(index);
  250. }
  251. }
  252. this.swiping = false;
  253. },
  254. getAvaiableTab(direction) {
  255. const { tabs, currentIndex } = this.data;
  256. const step = direction > 0 ? -1 : 1;
  257. for (let i = step; currentIndex + i < tabs.length && currentIndex + i >= 0; i += step) {
  258. const index = currentIndex + i;
  259. if (index >= 0 &&
  260. index < tabs.length &&
  261. tabs[index] &&
  262. !tabs[index].disabled) {
  263. return index;
  264. }
  265. }
  266. return -1;
  267. },
  268. },
  269. });