index.js 9.0 KB

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