index.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. import { use, isDef } from '../utils';
  2. import { raf } from '../utils/raf';
  3. import { on, off } from '../utils/event';
  4. import { TouchMixin } from '../mixins/touch';
  5. import { setScrollTop, getScrollTop, getElementTop, getScrollEventTarget } from '../utils/scroll';
  6. var _use = use('tabs'),
  7. sfc = _use[0],
  8. bem = _use[1];
  9. var tabBem = use('tab')[1];
  10. export default sfc({
  11. mixins: [TouchMixin],
  12. model: {
  13. prop: 'active'
  14. },
  15. props: {
  16. color: String,
  17. sticky: Boolean,
  18. animated: Boolean,
  19. offsetTop: Number,
  20. swipeable: Boolean,
  21. background: String,
  22. titleActiveColor: String,
  23. titleInactiveColor: String,
  24. ellipsis: {
  25. type: Boolean,
  26. "default": true
  27. },
  28. lazyRender: {
  29. type: Boolean,
  30. "default": true
  31. },
  32. lineWidth: {
  33. type: Number,
  34. "default": null
  35. },
  36. lineHeight: {
  37. type: Number,
  38. "default": null
  39. },
  40. active: {
  41. type: [Number, String],
  42. "default": 0
  43. },
  44. type: {
  45. type: String,
  46. "default": 'line'
  47. },
  48. duration: {
  49. type: Number,
  50. "default": 0.3
  51. },
  52. swipeThreshold: {
  53. type: Number,
  54. "default": 4
  55. }
  56. },
  57. data: function data() {
  58. return {
  59. tabs: [],
  60. position: '',
  61. curActive: null,
  62. lineStyle: {
  63. backgroundColor: this.color
  64. },
  65. events: {
  66. resize: false,
  67. sticky: false,
  68. swipeable: false
  69. }
  70. };
  71. },
  72. computed: {
  73. // whether the nav is scrollable
  74. scrollable: function scrollable() {
  75. return this.tabs.length > this.swipeThreshold || !this.ellipsis;
  76. },
  77. wrapStyle: function wrapStyle() {
  78. switch (this.position) {
  79. case 'top':
  80. return {
  81. top: this.offsetTop + 'px',
  82. position: 'fixed'
  83. };
  84. case 'bottom':
  85. return {
  86. top: 'auto',
  87. bottom: 0
  88. };
  89. default:
  90. return null;
  91. }
  92. },
  93. navStyle: function navStyle() {
  94. return {
  95. borderColor: this.color,
  96. background: this.background
  97. };
  98. },
  99. trackStyle: function trackStyle() {
  100. if (this.animated) {
  101. return {
  102. left: -1 * this.curActive * 100 + "%",
  103. transitionDuration: this.duration + "s"
  104. };
  105. }
  106. }
  107. },
  108. watch: {
  109. active: function active(val) {
  110. if (val !== this.curActive) {
  111. this.correctActive(val);
  112. }
  113. },
  114. color: function color() {
  115. this.setLine();
  116. },
  117. tabs: function tabs() {
  118. this.correctActive(this.curActive || this.active);
  119. this.scrollIntoView();
  120. this.setLine();
  121. },
  122. curActive: function curActive() {
  123. this.scrollIntoView();
  124. this.setLine(); // scroll to correct position
  125. if (this.position === 'top' || this.position === 'bottom') {
  126. setScrollTop(window, getElementTop(this.$el) - this.offsetTop);
  127. }
  128. },
  129. sticky: function sticky() {
  130. this.handlers(true);
  131. },
  132. swipeable: function swipeable() {
  133. this.handlers(true);
  134. }
  135. },
  136. mounted: function mounted() {
  137. this.onShow();
  138. },
  139. activated: function activated() {
  140. this.onShow();
  141. this.setLine();
  142. },
  143. deactivated: function deactivated() {
  144. this.handlers(false);
  145. },
  146. beforeDestroy: function beforeDestroy() {
  147. this.handlers(false);
  148. },
  149. methods: {
  150. onShow: function onShow() {
  151. var _this = this;
  152. this.$nextTick(function () {
  153. _this.inited = true;
  154. _this.handlers(true);
  155. _this.scrollIntoView(true);
  156. });
  157. },
  158. // whether to bind sticky listener
  159. handlers: function handlers(bind) {
  160. var events = this.events;
  161. var sticky = this.sticky && bind;
  162. var swipeable = this.swipeable && bind; // listen to window resize event
  163. if (events.resize !== bind) {
  164. events.resize = bind;
  165. (bind ? on : off)(window, 'resize', this.setLine, true);
  166. } // listen to scroll event
  167. if (events.sticky !== sticky) {
  168. events.sticky = sticky;
  169. this.scrollEl = this.scrollEl || getScrollEventTarget(this.$el);
  170. (sticky ? on : off)(this.scrollEl, 'scroll', this.onScroll, true);
  171. this.onScroll();
  172. } // listen to touch event
  173. if (events.swipeable !== swipeable) {
  174. events.swipeable = swipeable;
  175. var content = this.$refs.content;
  176. var action = swipeable ? on : off;
  177. action(content, 'touchstart', this.touchStart);
  178. action(content, 'touchmove', this.touchMove);
  179. action(content, 'touchend', this.onTouchEnd);
  180. action(content, 'touchcancel', this.onTouchEnd);
  181. }
  182. },
  183. // watch swipe touch end
  184. onTouchEnd: function onTouchEnd() {
  185. var direction = this.direction,
  186. deltaX = this.deltaX,
  187. curActive = this.curActive;
  188. var minSwipeDistance = 50;
  189. /* istanbul ignore else */
  190. if (direction === 'horizontal' && this.offsetX >= minSwipeDistance) {
  191. /* istanbul ignore else */
  192. if (deltaX > 0 && curActive !== 0) {
  193. this.setCurActive(curActive - 1);
  194. } else if (deltaX < 0 && curActive !== this.tabs.length - 1) {
  195. this.setCurActive(curActive + 1);
  196. }
  197. }
  198. },
  199. // adjust tab position
  200. onScroll: function onScroll() {
  201. var scrollTop = getScrollTop(window) + this.offsetTop;
  202. var elTopToPageTop = getElementTop(this.$el);
  203. var elBottomToPageTop = elTopToPageTop + this.$el.offsetHeight - this.$refs.wrap.offsetHeight;
  204. if (scrollTop > elBottomToPageTop) {
  205. this.position = 'bottom';
  206. } else if (scrollTop > elTopToPageTop) {
  207. this.position = 'top';
  208. } else {
  209. this.position = '';
  210. }
  211. var scrollParams = {
  212. scrollTop: scrollTop,
  213. isFixed: this.position === 'top'
  214. };
  215. this.$emit('scroll', scrollParams);
  216. },
  217. // update nav bar style
  218. setLine: function setLine() {
  219. var _this2 = this;
  220. var shouldAnimate = this.inited;
  221. this.$nextTick(function () {
  222. var tabs = _this2.$refs.tabs;
  223. if (!tabs || !tabs[_this2.curActive] || _this2.type !== 'line') {
  224. return;
  225. }
  226. var tab = tabs[_this2.curActive];
  227. var lineWidth = _this2.lineWidth,
  228. lineHeight = _this2.lineHeight;
  229. var width = isDef(lineWidth) ? lineWidth : tab.offsetWidth / 2;
  230. var left = tab.offsetLeft + (tab.offsetWidth - width) / 2;
  231. var lineStyle = {
  232. width: width + "px",
  233. backgroundColor: _this2.color,
  234. transform: "translateX(" + left + "px)"
  235. };
  236. if (shouldAnimate) {
  237. lineStyle.transitionDuration = _this2.duration + "s";
  238. }
  239. if (isDef(lineHeight)) {
  240. var height = lineHeight + "px";
  241. lineStyle.height = height;
  242. lineStyle.borderRadius = height;
  243. }
  244. _this2.lineStyle = lineStyle;
  245. });
  246. },
  247. // correct the value of active
  248. correctActive: function correctActive(active) {
  249. active = +active;
  250. var exist = this.tabs.some(function (tab) {
  251. return tab.index === active;
  252. });
  253. var defaultActive = (this.tabs[0] || {}).index || 0;
  254. this.setCurActive(exist ? active : defaultActive);
  255. },
  256. setCurActive: function setCurActive(active) {
  257. active = this.findAvailableTab(active, active < this.curActive);
  258. if (isDef(active) && active !== this.curActive) {
  259. this.$emit('input', active);
  260. if (this.curActive !== null) {
  261. this.$emit('change', active, this.tabs[active].title);
  262. }
  263. this.curActive = active;
  264. }
  265. },
  266. findAvailableTab: function findAvailableTab(active, reverse) {
  267. var diff = reverse ? -1 : 1;
  268. var index = active;
  269. while (index >= 0 && index < this.tabs.length) {
  270. if (!this.tabs[index].disabled) {
  271. return index;
  272. }
  273. index += diff;
  274. }
  275. },
  276. // emit event when clicked
  277. onClick: function onClick(index) {
  278. var _this$tabs$index = this.tabs[index],
  279. title = _this$tabs$index.title,
  280. disabled = _this$tabs$index.disabled;
  281. if (disabled) {
  282. this.$emit('disabled', index, title);
  283. } else {
  284. this.setCurActive(index);
  285. this.$emit('click', index, title);
  286. }
  287. },
  288. // scroll active tab into view
  289. scrollIntoView: function scrollIntoView(immediate) {
  290. var tabs = this.$refs.tabs;
  291. if (!this.scrollable || !tabs || !tabs[this.curActive]) {
  292. return;
  293. }
  294. var nav = this.$refs.nav;
  295. var scrollLeft = nav.scrollLeft,
  296. navWidth = nav.offsetWidth;
  297. var _tabs$this$curActive = tabs[this.curActive],
  298. offsetLeft = _tabs$this$curActive.offsetLeft,
  299. tabWidth = _tabs$this$curActive.offsetWidth;
  300. this.scrollTo(nav, scrollLeft, offsetLeft - (navWidth - tabWidth) / 2, immediate);
  301. },
  302. // animate the scrollLeft of nav
  303. scrollTo: function scrollTo(el, from, to, immediate) {
  304. if (immediate) {
  305. el.scrollLeft += to - from;
  306. return;
  307. }
  308. var count = 0;
  309. var frames = Math.round(this.duration * 1000 / 16);
  310. var animate = function animate() {
  311. el.scrollLeft += (to - from) / frames;
  312. /* istanbul ignore next */
  313. if (++count < frames) {
  314. raf(animate);
  315. }
  316. };
  317. animate();
  318. },
  319. // render title slot of child tab
  320. renderTitle: function renderTitle(el, index) {
  321. var _this3 = this;
  322. this.$nextTick(function () {
  323. var title = _this3.$refs.title[index];
  324. title.parentNode.replaceChild(el, title);
  325. });
  326. },
  327. getTabStyle: function getTabStyle(item, index) {
  328. var style = {};
  329. var color = this.color;
  330. var active = index === this.curActive;
  331. var isCard = this.type === 'card'; // theme color
  332. if (color) {
  333. if (!item.disabled && isCard && !active) {
  334. style.color = color;
  335. }
  336. if (!item.disabled && isCard && active) {
  337. style.backgroundColor = color;
  338. }
  339. if (isCard) {
  340. style.borderColor = color;
  341. }
  342. }
  343. var titleColor = active ? this.titleActiveColor : this.titleInactiveColor;
  344. if (titleColor) {
  345. style.color = titleColor;
  346. }
  347. if (this.scrollable && this.ellipsis) {
  348. style.flexBasis = 88 / this.swipeThreshold + '%';
  349. }
  350. return style;
  351. }
  352. },
  353. render: function render(h) {
  354. var _this4 = this;
  355. var type = this.type,
  356. ellipsis = this.ellipsis,
  357. animated = this.animated,
  358. scrollable = this.scrollable;
  359. var Nav = this.tabs.map(function (tab, index) {
  360. return h("div", {
  361. "ref": "tabs",
  362. "refInFor": true,
  363. "class": tabBem({
  364. active: index === _this4.curActive,
  365. disabled: tab.disabled,
  366. complete: !ellipsis
  367. }),
  368. "style": _this4.getTabStyle(tab, index),
  369. "on": {
  370. "click": function click() {
  371. _this4.onClick(index);
  372. }
  373. }
  374. }, [h("span", {
  375. "ref": "title",
  376. "refInFor": true,
  377. "class": {
  378. 'van-ellipsis': ellipsis
  379. }
  380. }, [tab.title])]);
  381. });
  382. return h("div", {
  383. "class": bem([type])
  384. }, [h("div", {
  385. "ref": "wrap",
  386. "style": this.wrapStyle,
  387. "class": [bem('wrap', {
  388. scrollable: scrollable
  389. }), {
  390. 'van-hairline--top-bottom': type === 'line'
  391. }]
  392. }, [h("div", {
  393. "ref": "nav",
  394. "class": bem('nav', [type]),
  395. "style": this.navStyle
  396. }, [this.slots('nav-left'), type === 'line' && h("div", {
  397. "class": bem('line'),
  398. "style": this.lineStyle
  399. }), Nav, this.slots('nav-right')])]), h("div", {
  400. "ref": "content",
  401. "class": bem('content', {
  402. animated: animated
  403. })
  404. }, [animated ? h("div", {
  405. "class": bem('track'),
  406. "style": this.trackStyle
  407. }, [this.slots()]) : this.slots()])]);
  408. }
  409. });