Tabs.mjs 11 KB

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