tab-nav.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. 'use strict';
  2. Object.defineProperty(exports, '__esModule', { value: true });
  3. var vue = require('vue');
  4. var core = require('@vueuse/core');
  5. var index$1 = require('../../icon/index.js');
  6. var iconsVue = require('@element-plus/icons-vue');
  7. var useWheel = require('../../virtual-list/src/hooks/use-wheel.js');
  8. var lodashUnified = require('lodash-unified');
  9. var tabBar = require('./tab-bar2.js');
  10. var constants = require('./constants.js');
  11. var runtime = require('../../../utils/vue/props/runtime.js');
  12. var typescript = require('../../../utils/typescript.js');
  13. var error = require('../../../utils/error.js');
  14. var index = require('../../../hooks/use-namespace/index.js');
  15. var raf = require('../../../utils/raf.js');
  16. var event = require('../../../utils/dom/event.js');
  17. var aria = require('../../../constants/aria.js');
  18. var strings = require('../../../utils/strings.js');
  19. const tabNavProps = runtime.buildProps({
  20. panes: {
  21. type: runtime.definePropType(Array),
  22. default: () => typescript.mutable([])
  23. },
  24. currentName: {
  25. type: [String, Number],
  26. default: ""
  27. },
  28. editable: Boolean,
  29. type: {
  30. type: String,
  31. values: ["card", "border-card", ""],
  32. default: ""
  33. },
  34. stretch: Boolean,
  35. tabindex: {
  36. type: [String, Number],
  37. default: void 0
  38. }
  39. });
  40. const tabNavEmits = {
  41. tabClick: (tab, tabName, ev) => ev instanceof Event,
  42. tabRemove: (tab, ev) => ev instanceof Event
  43. };
  44. const COMPONENT_NAME = "ElTabNav";
  45. const TabNav = vue.defineComponent({
  46. name: COMPONENT_NAME,
  47. props: tabNavProps,
  48. emits: tabNavEmits,
  49. setup(props, {
  50. expose,
  51. emit
  52. }) {
  53. const rootTabs = vue.inject(constants.tabsRootContextKey);
  54. if (!rootTabs)
  55. error.throwError(COMPONENT_NAME, `<el-tabs><tab-nav /></el-tabs>`);
  56. const ns = index.useNamespace("tabs");
  57. const visibility = core.useDocumentVisibility();
  58. const focused = core.useWindowFocus();
  59. const navScroll$ = vue.ref();
  60. const nav$ = vue.ref();
  61. const el$ = vue.ref();
  62. const tabRefsMap = vue.ref({});
  63. const tabBarRef = vue.ref();
  64. const scrollable = vue.ref(false);
  65. const navOffset = vue.ref(0);
  66. const isFocus = vue.ref(false);
  67. const focusable = vue.ref(true);
  68. const tracker = vue.shallowRef();
  69. const isHorizontal = vue.computed(() => ["top", "bottom"].includes(rootTabs.props.tabPosition));
  70. const sizeName = vue.computed(() => isHorizontal.value ? "width" : "height");
  71. const navStyle = vue.computed(() => {
  72. const dir = sizeName.value === "width" ? "X" : "Y";
  73. return {
  74. transform: `translate${dir}(-${navOffset.value}px)`
  75. };
  76. });
  77. const {
  78. width: navContainerWidth,
  79. height: navContainerHeight
  80. } = core.useElementSize(navScroll$);
  81. const {
  82. width: navWidth,
  83. height: navHeight
  84. } = core.useElementSize(nav$, {
  85. width: 0,
  86. height: 0
  87. }, {
  88. box: "border-box"
  89. });
  90. const navContainerSize = vue.computed(() => isHorizontal.value ? navContainerWidth.value : navContainerHeight.value);
  91. const navSize = vue.computed(() => isHorizontal.value ? navWidth.value : navHeight.value);
  92. const {
  93. onWheel
  94. } = useWheel["default"]({
  95. atStartEdge: vue.computed(() => navOffset.value <= 0),
  96. atEndEdge: vue.computed(() => navSize.value - navOffset.value <= navContainerSize.value),
  97. layout: vue.computed(() => isHorizontal.value ? "horizontal" : "vertical")
  98. }, (offset) => {
  99. navOffset.value = lodashUnified.clamp(navOffset.value + offset, 0, navSize.value - navContainerSize.value);
  100. });
  101. const scrollPrev = () => {
  102. if (!navScroll$.value)
  103. return;
  104. const containerSize = navScroll$.value[`offset${strings.capitalize(sizeName.value)}`];
  105. const currentOffset = navOffset.value;
  106. if (!currentOffset)
  107. return;
  108. const newOffset = currentOffset > containerSize ? currentOffset - containerSize : 0;
  109. navOffset.value = newOffset;
  110. };
  111. const scrollNext = () => {
  112. if (!navScroll$.value || !nav$.value)
  113. return;
  114. const navSize2 = nav$.value[`offset${strings.capitalize(sizeName.value)}`];
  115. const containerSize = navScroll$.value[`offset${strings.capitalize(sizeName.value)}`];
  116. const currentOffset = navOffset.value;
  117. if (navSize2 - currentOffset <= containerSize)
  118. return;
  119. const newOffset = navSize2 - currentOffset > containerSize * 2 ? currentOffset + containerSize : navSize2 - containerSize;
  120. navOffset.value = newOffset;
  121. };
  122. const scrollToActiveTab = async () => {
  123. const nav = nav$.value;
  124. if (!scrollable.value || !el$.value || !navScroll$.value || !nav)
  125. return;
  126. await vue.nextTick();
  127. const activeTab = tabRefsMap.value[props.currentName];
  128. if (!activeTab)
  129. return;
  130. const navScroll = navScroll$.value;
  131. const activeTabBounding = activeTab.getBoundingClientRect();
  132. const navScrollBounding = navScroll.getBoundingClientRect();
  133. const maxOffset = isHorizontal.value ? nav.offsetWidth - navScrollBounding.width : nav.offsetHeight - navScrollBounding.height;
  134. const currentOffset = navOffset.value;
  135. let newOffset = currentOffset;
  136. if (isHorizontal.value) {
  137. if (activeTabBounding.left < navScrollBounding.left) {
  138. newOffset = currentOffset - (navScrollBounding.left - activeTabBounding.left);
  139. }
  140. if (activeTabBounding.right > navScrollBounding.right) {
  141. newOffset = currentOffset + activeTabBounding.right - navScrollBounding.right;
  142. }
  143. } else {
  144. if (activeTabBounding.top < navScrollBounding.top) {
  145. newOffset = currentOffset - (navScrollBounding.top - activeTabBounding.top);
  146. }
  147. if (activeTabBounding.bottom > navScrollBounding.bottom) {
  148. newOffset = currentOffset + (activeTabBounding.bottom - navScrollBounding.bottom);
  149. }
  150. }
  151. newOffset = Math.max(newOffset, 0);
  152. navOffset.value = Math.min(newOffset, maxOffset);
  153. };
  154. const update = () => {
  155. var _a;
  156. if (!nav$.value || !navScroll$.value)
  157. return;
  158. props.stretch && ((_a = tabBarRef.value) == null ? void 0 : _a.update());
  159. const navSize2 = nav$.value[`offset${strings.capitalize(sizeName.value)}`];
  160. const containerSize = navScroll$.value[`offset${strings.capitalize(sizeName.value)}`];
  161. const currentOffset = navOffset.value;
  162. if (containerSize < navSize2) {
  163. scrollable.value = scrollable.value || {};
  164. scrollable.value.prev = currentOffset;
  165. scrollable.value.next = currentOffset + containerSize < navSize2;
  166. if (navSize2 - currentOffset < containerSize) {
  167. navOffset.value = navSize2 - containerSize;
  168. }
  169. } else {
  170. scrollable.value = false;
  171. if (currentOffset > 0) {
  172. navOffset.value = 0;
  173. }
  174. }
  175. };
  176. const changeTab = (event$1) => {
  177. const code = event.getEventCode(event$1);
  178. let step = 0;
  179. switch (code) {
  180. case aria.EVENT_CODE.left:
  181. case aria.EVENT_CODE.up:
  182. step = -1;
  183. break;
  184. case aria.EVENT_CODE.right:
  185. case aria.EVENT_CODE.down:
  186. step = 1;
  187. break;
  188. default:
  189. return;
  190. }
  191. const tabList = Array.from(event$1.currentTarget.querySelectorAll("[role=tab]:not(.is-disabled)"));
  192. const currentIndex = tabList.indexOf(event$1.target);
  193. let nextIndex = currentIndex + step;
  194. if (nextIndex < 0) {
  195. nextIndex = tabList.length - 1;
  196. } else if (nextIndex >= tabList.length) {
  197. nextIndex = 0;
  198. }
  199. tabList[nextIndex].focus({
  200. preventScroll: true
  201. });
  202. tabList[nextIndex].click();
  203. setFocus();
  204. };
  205. const setFocus = () => {
  206. if (focusable.value)
  207. isFocus.value = true;
  208. };
  209. const removeFocus = () => isFocus.value = false;
  210. const setRefs = (el, key) => {
  211. tabRefsMap.value[key] = el;
  212. };
  213. const focusActiveTab = async () => {
  214. await vue.nextTick();
  215. const activeTab = tabRefsMap.value[props.currentName];
  216. activeTab == null ? void 0 : activeTab.focus({
  217. preventScroll: true
  218. });
  219. };
  220. vue.watch(visibility, (visibility2) => {
  221. if (visibility2 === "hidden") {
  222. focusable.value = false;
  223. } else if (visibility2 === "visible") {
  224. setTimeout(() => focusable.value = true, 50);
  225. }
  226. });
  227. vue.watch(focused, (focused2) => {
  228. if (focused2) {
  229. setTimeout(() => focusable.value = true, 50);
  230. } else {
  231. focusable.value = false;
  232. }
  233. });
  234. core.useResizeObserver(el$, () => {
  235. raf.rAF(update);
  236. });
  237. vue.onMounted(() => setTimeout(() => scrollToActiveTab(), 0));
  238. vue.onUpdated(() => update());
  239. expose({
  240. scrollToActiveTab,
  241. removeFocus,
  242. focusActiveTab,
  243. tabListRef: nav$,
  244. tabBarRef,
  245. scheduleRender: () => vue.triggerRef(tracker)
  246. });
  247. return () => {
  248. const scrollBtn = scrollable.value ? [vue.createVNode("span", {
  249. "class": [ns.e("nav-prev"), ns.is("disabled", !scrollable.value.prev)],
  250. "onClick": scrollPrev
  251. }, [vue.createVNode(index$1.ElIcon, null, {
  252. default: () => [vue.createVNode(iconsVue.ArrowLeft, null, null)]
  253. })]), vue.createVNode("span", {
  254. "class": [ns.e("nav-next"), ns.is("disabled", !scrollable.value.next)],
  255. "onClick": scrollNext
  256. }, [vue.createVNode(index$1.ElIcon, null, {
  257. default: () => [vue.createVNode(iconsVue.ArrowRight, null, null)]
  258. })])] : null;
  259. const tabs = props.panes.map((pane, index) => {
  260. var _a, _b, _c, _d, _e;
  261. const uid = pane.uid;
  262. const disabled = pane.props.disabled;
  263. const tabName = (_b = (_a = pane.props.name) != null ? _a : pane.index) != null ? _b : `${index}`;
  264. const closable = !disabled && (pane.isClosable || pane.props.closable !== false && props.editable);
  265. pane.index = `${index}`;
  266. const btnClose = closable ? vue.createVNode(index$1.ElIcon, {
  267. "class": "is-icon-close",
  268. "onClick": (ev) => emit("tabRemove", pane, ev)
  269. }, {
  270. default: () => [vue.createVNode(iconsVue.Close, null, null)]
  271. }) : null;
  272. const tabLabelContent = ((_d = (_c = pane.slots).label) == null ? void 0 : _d.call(_c)) || pane.props.label;
  273. const tabindex = !disabled && pane.active ? (_e = props.tabindex) != null ? _e : rootTabs.props.tabindex : -1;
  274. return vue.createVNode("div", {
  275. "ref": (el) => setRefs(el, tabName),
  276. "class": [ns.e("item"), ns.is(rootTabs.props.tabPosition), ns.is("active", pane.active), ns.is("disabled", disabled), ns.is("closable", closable), ns.is("focus", isFocus.value)],
  277. "id": `tab-${tabName}`,
  278. "key": `tab-${uid}`,
  279. "aria-controls": `pane-${tabName}`,
  280. "role": "tab",
  281. "aria-selected": pane.active,
  282. "tabindex": tabindex,
  283. "onFocus": () => setFocus(),
  284. "onBlur": () => removeFocus(),
  285. "onClick": (ev) => {
  286. removeFocus();
  287. emit("tabClick", pane, tabName, ev);
  288. },
  289. "onKeydown": (ev) => {
  290. const code = event.getEventCode(ev);
  291. if (closable && (code === aria.EVENT_CODE.delete || code === aria.EVENT_CODE.backspace)) {
  292. emit("tabRemove", pane, ev);
  293. }
  294. }
  295. }, [...[tabLabelContent, btnClose]]);
  296. });
  297. tracker.value;
  298. return vue.createVNode("div", {
  299. "ref": el$,
  300. "class": [ns.e("nav-wrap"), ns.is("scrollable", !!scrollable.value), ns.is(rootTabs.props.tabPosition)]
  301. }, [scrollBtn, vue.createVNode("div", {
  302. "class": ns.e("nav-scroll"),
  303. "ref": navScroll$
  304. }, [props.panes.length > 0 ? vue.createVNode("div", {
  305. "class": [ns.e("nav"), ns.is(rootTabs.props.tabPosition), ns.is("stretch", props.stretch && ["top", "bottom"].includes(rootTabs.props.tabPosition))],
  306. "ref": nav$,
  307. "style": navStyle.value,
  308. "role": "tablist",
  309. "onKeydown": changeTab,
  310. "onWheel": onWheel
  311. }, [...[!props.type ? vue.createVNode(tabBar["default"], {
  312. "ref": tabBarRef,
  313. "tabs": [...props.panes],
  314. "tabRefs": tabRefsMap.value
  315. }, null) : null, tabs]]) : null])]);
  316. };
  317. }
  318. });
  319. exports["default"] = TabNav;
  320. exports.tabNavEmits = tabNavEmits;
  321. exports.tabNavProps = tabNavProps;
  322. //# sourceMappingURL=tab-nav.js.map