Tabs.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. import { createVNode as _createVNode, mergeProps as _mergeProps } from "vue";
  2. import { ref, watch, computed, reactive, nextTick, onActivated, defineComponent, getCurrentInstance } from "vue";
  3. import { pick, isDef, addUnit, isHidden, unitToPx, truthProp, numericProp, windowWidth, getElementTop, makeStringProp, callInterceptor, createNamespace, makeNumericProp, setRootScrollTop, BORDER_TOP_BOTTOM } from "../utils/index.mjs";
  4. import { scrollLeftTo, scrollTopTo } from "./utils.mjs";
  5. import { useRect, useChildren, useScrollParent, useEventListener, onMountedOrActivated } from "@vant/use";
  6. import { useId } from "../composables/use-id.mjs";
  7. import { route } from "../composables/use-route.mjs";
  8. import { useRefs } from "../composables/use-refs.mjs";
  9. import { useExpose } from "../composables/use-expose.mjs";
  10. import { onPopupReopen } from "../composables/on-popup-reopen.mjs";
  11. import { Sticky } from "../sticky/index.mjs";
  12. import TabsTitle from "./TabsTitle.mjs";
  13. import TabsContent from "./TabsContent.mjs";
  14. const [name, bem] = createNamespace("tabs");
  15. const tabsProps = {
  16. type: makeStringProp("line"),
  17. color: String,
  18. border: Boolean,
  19. sticky: Boolean,
  20. shrink: Boolean,
  21. active: makeNumericProp(0),
  22. duration: makeNumericProp(0.3),
  23. animated: Boolean,
  24. ellipsis: truthProp,
  25. swipeable: Boolean,
  26. scrollspy: Boolean,
  27. offsetTop: makeNumericProp(0),
  28. background: String,
  29. lazyRender: truthProp,
  30. lineWidth: numericProp,
  31. lineHeight: numericProp,
  32. beforeChange: Function,
  33. swipeThreshold: makeNumericProp(5),
  34. titleActiveColor: String,
  35. titleInactiveColor: String
  36. };
  37. const TABS_KEY = Symbol(name);
  38. var stdin_default = defineComponent({
  39. name,
  40. props: tabsProps,
  41. emits: ["click", "change", "scroll", "disabled", "rendered", "click-tab", "update:active"],
  42. setup(props, {
  43. emit,
  44. slots
  45. }) {
  46. var _a, _b;
  47. if (process.env.NODE_ENV !== "production") {
  48. const props2 = (_b = (_a = getCurrentInstance()) == null ? void 0 : _a.vnode) == null ? void 0 : _b.props;
  49. if (props2 && "onClick" in props2) {
  50. console.warn('[Vant] Tabs: "click" event is deprecated, using "click-tab" instead.');
  51. }
  52. if (props2 && "onDisabled" in props2) {
  53. console.warn('[Vant] Tabs: "disabled" event is deprecated, using "click-tab" instead.');
  54. }
  55. }
  56. let tabHeight;
  57. let lockScroll;
  58. let stickyFixed;
  59. const root = ref();
  60. const navRef = ref();
  61. const wrapRef = ref();
  62. const contentRef = ref();
  63. const id = useId();
  64. const scroller = useScrollParent(root);
  65. const [titleRefs, setTitleRefs] = useRefs();
  66. const {
  67. children,
  68. linkChildren
  69. } = useChildren(TABS_KEY);
  70. const state = reactive({
  71. inited: false,
  72. position: "",
  73. lineStyle: {},
  74. currentIndex: -1
  75. });
  76. const scrollable = computed(() => children.length > props.swipeThreshold || !props.ellipsis || props.shrink);
  77. const navStyle = computed(() => ({
  78. borderColor: props.color,
  79. background: props.background
  80. }));
  81. const getTabName = (tab, index) => {
  82. var _a2;
  83. return (_a2 = tab.name) != null ? _a2 : index;
  84. };
  85. const currentName = computed(() => {
  86. const activeTab = children[state.currentIndex];
  87. if (activeTab) {
  88. return getTabName(activeTab, state.currentIndex);
  89. }
  90. });
  91. const offsetTopPx = computed(() => unitToPx(props.offsetTop));
  92. const scrollOffset = computed(() => {
  93. if (props.sticky) {
  94. return offsetTopPx.value + tabHeight;
  95. }
  96. return 0;
  97. });
  98. const scrollIntoView = (immediate) => {
  99. const nav = navRef.value;
  100. const titles = titleRefs.value;
  101. if (!scrollable.value || !nav || !titles || !titles[state.currentIndex]) {
  102. return;
  103. }
  104. const title = titles[state.currentIndex].$el;
  105. const to = title.offsetLeft - (nav.offsetWidth - title.offsetWidth) / 2;
  106. scrollLeftTo(nav, to, immediate ? 0 : +props.duration);
  107. };
  108. const setLine = () => {
  109. const shouldAnimate = state.inited;
  110. nextTick(() => {
  111. const titles = titleRefs.value;
  112. if (!titles || !titles[state.currentIndex] || props.type !== "line" || isHidden(root.value)) {
  113. return;
  114. }
  115. const title = titles[state.currentIndex].$el;
  116. const {
  117. lineWidth,
  118. lineHeight
  119. } = props;
  120. const left = title.offsetLeft + title.offsetWidth / 2;
  121. const lineStyle = {
  122. width: addUnit(lineWidth),
  123. backgroundColor: props.color,
  124. transform: `translateX(${left}px) translateX(-50%)`
  125. };
  126. if (shouldAnimate) {
  127. lineStyle.transitionDuration = `${props.duration}s`;
  128. }
  129. if (isDef(lineHeight)) {
  130. const height = addUnit(lineHeight);
  131. lineStyle.height = height;
  132. lineStyle.borderRadius = height;
  133. }
  134. state.lineStyle = lineStyle;
  135. });
  136. };
  137. const findAvailableTab = (index) => {
  138. const diff = index < state.currentIndex ? -1 : 1;
  139. while (index >= 0 && index < children.length) {
  140. if (!children[index].disabled) {
  141. return index;
  142. }
  143. index += diff;
  144. }
  145. };
  146. const setCurrentIndex = (currentIndex, skipScrollIntoView) => {
  147. const newIndex = findAvailableTab(currentIndex);
  148. if (!isDef(newIndex)) {
  149. return;
  150. }
  151. const newTab = children[newIndex];
  152. const newName = getTabName(newTab, newIndex);
  153. const shouldEmitChange = state.currentIndex !== null;
  154. if (state.currentIndex !== newIndex) {
  155. state.currentIndex = newIndex;
  156. if (!skipScrollIntoView) {
  157. scrollIntoView();
  158. }
  159. setLine();
  160. }
  161. if (newName !== props.active) {
  162. emit("update:active", newName);
  163. if (shouldEmitChange) {
  164. emit("change", newName, newTab.title);
  165. }
  166. }
  167. if (stickyFixed && !props.scrollspy) {
  168. setRootScrollTop(Math.ceil(getElementTop(root.value) - offsetTopPx.value));
  169. }
  170. };
  171. const setCurrentIndexByName = (name2, skipScrollIntoView) => {
  172. const matched = children.find((tab, index2) => getTabName(tab, index2) === name2);
  173. const index = matched ? children.indexOf(matched) : 0;
  174. setCurrentIndex(index, skipScrollIntoView);
  175. };
  176. const scrollToCurrentContent = (immediate = false) => {
  177. if (props.scrollspy) {
  178. const target = children[state.currentIndex].$el;
  179. if (target && scroller.value) {
  180. const to = getElementTop(target, scroller.value) - scrollOffset.value;
  181. lockScroll = true;
  182. scrollTopTo(scroller.value, to, immediate ? 0 : +props.duration, () => {
  183. lockScroll = false;
  184. });
  185. }
  186. }
  187. };
  188. const onClickTab = (item, index, event) => {
  189. const {
  190. title,
  191. disabled
  192. } = children[index];
  193. const name2 = getTabName(children[index], index);
  194. if (disabled) {
  195. emit("disabled", name2, title);
  196. } else {
  197. callInterceptor(props.beforeChange, {
  198. args: [name2],
  199. done: () => {
  200. setCurrentIndex(index);
  201. scrollToCurrentContent();
  202. }
  203. });
  204. emit("click", name2, title);
  205. route(item);
  206. }
  207. emit("click-tab", {
  208. name: name2,
  209. title,
  210. event,
  211. disabled
  212. });
  213. };
  214. const onStickyScroll = (params) => {
  215. stickyFixed = params.isFixed;
  216. emit("scroll", params);
  217. };
  218. const scrollTo = (name2) => {
  219. nextTick(() => {
  220. setCurrentIndexByName(name2);
  221. scrollToCurrentContent(true);
  222. });
  223. };
  224. const getCurrentIndexOnScroll = () => {
  225. for (let index = 0; index < children.length; index++) {
  226. const {
  227. top
  228. } = useRect(children[index].$el);
  229. if (top > scrollOffset.value) {
  230. return index === 0 ? 0 : index - 1;
  231. }
  232. }
  233. return children.length - 1;
  234. };
  235. const onScroll = () => {
  236. if (props.scrollspy && !lockScroll) {
  237. const index = getCurrentIndexOnScroll();
  238. setCurrentIndex(index);
  239. }
  240. };
  241. const renderNav = () => children.map((item, index) => _createVNode(TabsTitle, _mergeProps({
  242. "key": item.id,
  243. "id": `${id}-${index}`,
  244. "ref": setTitleRefs(index),
  245. "type": props.type,
  246. "color": props.color,
  247. "style": item.titleStyle,
  248. "class": item.titleClass,
  249. "shrink": props.shrink,
  250. "isActive": index === state.currentIndex,
  251. "controls": item.id,
  252. "scrollable": scrollable.value,
  253. "activeColor": props.titleActiveColor,
  254. "inactiveColor": props.titleInactiveColor,
  255. "onClick": (event) => onClickTab(item, index, event)
  256. }, pick(item, ["dot", "badge", "title", "disabled", "showZeroBadge"])), {
  257. title: item.$slots.title
  258. }));
  259. const renderLine = () => {
  260. if (props.type === "line" && children.length) {
  261. return _createVNode("div", {
  262. "class": bem("line"),
  263. "style": state.lineStyle
  264. }, null);
  265. }
  266. };
  267. const renderHeader = () => {
  268. var _a2, _b2, _c;
  269. const {
  270. type,
  271. border,
  272. sticky
  273. } = props;
  274. const Header = [_createVNode("div", {
  275. "ref": sticky ? void 0 : wrapRef,
  276. "class": [bem("wrap"), {
  277. [BORDER_TOP_BOTTOM]: type === "line" && border
  278. }]
  279. }, [_createVNode("div", {
  280. "ref": navRef,
  281. "role": "tablist",
  282. "class": bem("nav", [type, {
  283. shrink: props.shrink,
  284. complete: scrollable.value
  285. }]),
  286. "style": navStyle.value,
  287. "aria-orientation": "horizontal"
  288. }, [(_a2 = slots["nav-left"]) == null ? void 0 : _a2.call(slots), renderNav(), renderLine(), (_b2 = slots["nav-right"]) == null ? void 0 : _b2.call(slots)])]), (_c = slots["nav-bottom"]) == null ? void 0 : _c.call(slots)];
  289. if (sticky) {
  290. return _createVNode("div", {
  291. "ref": wrapRef
  292. }, [Header]);
  293. }
  294. return Header;
  295. };
  296. watch([() => props.color, windowWidth], setLine);
  297. watch(() => props.active, (value) => {
  298. if (value !== currentName.value) {
  299. setCurrentIndexByName(value);
  300. }
  301. });
  302. watch(() => children.length, () => {
  303. if (state.inited) {
  304. setCurrentIndexByName(props.active);
  305. setLine();
  306. nextTick(() => {
  307. scrollIntoView(true);
  308. });
  309. }
  310. });
  311. const init = () => {
  312. setCurrentIndexByName(props.active, true);
  313. nextTick(() => {
  314. state.inited = true;
  315. if (wrapRef.value) {
  316. tabHeight = useRect(wrapRef.value).height;
  317. }
  318. scrollIntoView(true);
  319. });
  320. };
  321. const onRendered = (name2, title) => emit("rendered", name2, title);
  322. const resize = () => {
  323. setLine();
  324. nextTick(() => {
  325. var _a2, _b2;
  326. return (_b2 = (_a2 = contentRef.value) == null ? void 0 : _a2.swipeRef.value) == null ? void 0 : _b2.resize();
  327. });
  328. };
  329. useExpose({
  330. resize,
  331. scrollTo
  332. });
  333. onActivated(setLine);
  334. onPopupReopen(setLine);
  335. onMountedOrActivated(init);
  336. useEventListener("scroll", onScroll, {
  337. target: scroller,
  338. passive: true
  339. });
  340. linkChildren({
  341. id,
  342. props,
  343. setLine,
  344. onRendered,
  345. currentName,
  346. scrollIntoView
  347. });
  348. return () => _createVNode("div", {
  349. "ref": root,
  350. "class": bem([props.type])
  351. }, [props.sticky ? _createVNode(Sticky, {
  352. "container": root.value,
  353. "offsetTop": offsetTopPx.value,
  354. "onScroll": onStickyScroll
  355. }, {
  356. default: () => [renderHeader()]
  357. }) : renderHeader(), _createVNode(TabsContent, {
  358. "ref": contentRef,
  359. "count": children.length,
  360. "inited": state.inited,
  361. "animated": props.animated,
  362. "duration": props.duration,
  363. "swipeable": props.swipeable,
  364. "lazyRender": props.lazyRender,
  365. "currentIndex": state.currentIndex,
  366. "onChange": setCurrentIndex
  367. }, {
  368. default: () => {
  369. var _a2;
  370. return [(_a2 = slots.default) == null ? void 0 : _a2.call(slots)];
  371. }
  372. })]);
  373. }
  374. });
  375. export {
  376. TABS_KEY,
  377. stdin_default as default
  378. };