UNPKG

@aplus-frontend/antdv

Version:

Vue basic component library maintained based on ant-design-vue

549 lines 19.1 kB
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2"; import _extends from "@babel/runtime/helpers/esm/extends"; import { createVNode as _createVNode } from "vue"; import { useRafState } from '../hooks/useRaf'; import TabNode from './TabNode'; import useOffsets from '../hooks/useOffsets'; import OperationNode from './OperationNode'; import { useInjectTabs } from '../TabContext'; import useTouchMove from '../hooks/useTouchMove'; import AddButton from './AddButton'; import { objectType, functionType } from '../../../_util/type'; import { shallowRef, onBeforeUnmount, defineComponent, watch, watchEffect, computed } from 'vue'; import PropTypes from '../../../_util/vue-types'; import useSyncState from '../hooks/useSyncState'; import useState from '../../../_util/hooks/useState'; import raf from '../../../_util/raf'; import classNames from '../../../_util/classNames'; import ResizeObserver from '../../../vc-resize-observer'; import { toPx } from '../../../_util/util'; import useRefs from '../../../_util/hooks/useRefs'; import pick from 'lodash-es/pick'; const DEFAULT_SIZE = { width: 0, height: 0, left: 0, top: 0, right: 0 }; export const tabNavListProps = () => { return { id: { type: String }, tabPosition: { type: String }, activeKey: { type: [String, Number] }, rtl: { type: Boolean }, animated: objectType(), editable: objectType(), moreIcon: PropTypes.any, moreTransitionName: { type: String }, mobile: { type: Boolean }, tabBarGutter: { type: Number }, renderTabBar: { type: Function }, locale: objectType(), popupClassName: String, getPopupContainer: functionType(), onTabClick: { type: Function }, onTabScroll: { type: Function } }; }; const getTabSize = (tab, containerRect) => { // tabListRef const { offsetWidth, offsetHeight, offsetTop, offsetLeft } = tab; const { width, height, x, y } = tab.getBoundingClientRect(); // Use getBoundingClientRect to avoid decimal inaccuracy if (Math.abs(width - offsetWidth) < 1) { return [width, height, x - containerRect.x, y - containerRect.y]; } return [offsetWidth, offsetHeight, offsetLeft, offsetTop]; }; // const getSize = (refObj: ShallowRef<HTMLElement>) => { // const { offsetWidth = 0, offsetHeight = 0 } = refObj.value || {}; // // Use getBoundingClientRect to avoid decimal inaccuracy // if (refObj.value) { // const { width, height } = refObj.value.getBoundingClientRect(); // if (Math.abs(width - offsetWidth) < 1) { // return [width, height]; // } // } // return [offsetWidth, offsetHeight]; // }; export default defineComponent({ compatConfig: { MODE: 3 }, name: 'TabNavList', inheritAttrs: false, props: tabNavListProps(), slots: Object, emits: ['tabClick', 'tabScroll'], setup(props, _ref) { let { attrs, slots } = _ref; const { tabs, prefixCls } = useInjectTabs(); const tabsWrapperRef = shallowRef(); const tabListRef = shallowRef(); const operationsRef = shallowRef(); const innerAddButtonRef = shallowRef(); const [setRef, btnRefs] = useRefs(); const tabPositionTopOrBottom = computed(() => props.tabPosition === 'top' || props.tabPosition === 'bottom'); const [transformLeft, setTransformLeft] = useSyncState(0, (next, prev) => { if (tabPositionTopOrBottom.value && props.onTabScroll) { props.onTabScroll({ direction: next > prev ? 'left' : 'right' }); } }); const [transformTop, setTransformTop] = useSyncState(0, (next, prev) => { if (!tabPositionTopOrBottom.value && props.onTabScroll) { props.onTabScroll({ direction: next > prev ? 'top' : 'bottom' }); } }); const [wrapperScrollWidth, setWrapperScrollWidth] = useState(0); const [wrapperScrollHeight, setWrapperScrollHeight] = useState(0); const [wrapperWidth, setWrapperWidth] = useState(null); const [wrapperHeight, setWrapperHeight] = useState(null); const [addWidth, setAddWidth] = useState(0); const [addHeight, setAddHeight] = useState(0); const [tabSizes, setTabSizes] = useRafState(new Map()); const tabOffsets = useOffsets(tabs, tabSizes); // ========================== Util ========================= const operationsHiddenClassName = computed(() => `${prefixCls.value}-nav-operations-hidden`); const transformMin = shallowRef(0); const transformMax = shallowRef(0); watchEffect(() => { if (!tabPositionTopOrBottom.value) { transformMin.value = Math.min(0, wrapperHeight.value - wrapperScrollHeight.value); transformMax.value = 0; } else if (props.rtl) { transformMin.value = 0; transformMax.value = Math.max(0, wrapperScrollWidth.value - wrapperWidth.value); } else { transformMin.value = Math.min(0, wrapperWidth.value - wrapperScrollWidth.value); transformMax.value = 0; } }); const alignInRange = value => { if (value < transformMin.value) { return transformMin.value; } if (value > transformMax.value) { return transformMax.value; } return value; }; // ========================= Mobile ======================== const touchMovingRef = shallowRef(); const [lockAnimation, setLockAnimation] = useState(); const doLockAnimation = () => { setLockAnimation(Date.now()); }; const clearTouchMoving = () => { clearTimeout(touchMovingRef.value); }; const doMove = (setState, offset) => { setState(value => { const newValue = alignInRange(value + offset); return newValue; }); }; useTouchMove(tabsWrapperRef, (offsetX, offsetY) => { if (tabPositionTopOrBottom.value) { // Skip scroll if place is enough if (wrapperWidth.value >= wrapperScrollWidth.value) { return false; } doMove(setTransformLeft, offsetX); } else { if (wrapperHeight.value >= wrapperScrollHeight.value) { return false; } doMove(setTransformTop, offsetY); } clearTouchMoving(); doLockAnimation(); return true; }); watch(lockAnimation, () => { clearTouchMoving(); if (lockAnimation.value) { touchMovingRef.value = setTimeout(() => { setLockAnimation(0); }, 100); } }); // ========================= Scroll ======================== const scrollToTab = function () { let key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : props.activeKey; const tabOffset = tabOffsets.value.get(key) || { width: 0, height: 0, left: 0, right: 0, top: 0 }; if (tabPositionTopOrBottom.value) { // ============ Align with top & bottom ============ let newTransform = transformLeft.value; // RTL if (props.rtl) { if (tabOffset.right < transformLeft.value) { newTransform = tabOffset.right; } else if (tabOffset.right + tabOffset.width > transformLeft.value + wrapperWidth.value) { newTransform = tabOffset.right + tabOffset.width - wrapperWidth.value; } } // LTR else if (tabOffset.left < -transformLeft.value) { newTransform = -tabOffset.left; } else if (tabOffset.left + tabOffset.width > -transformLeft.value + wrapperWidth.value) { newTransform = -(tabOffset.left + tabOffset.width - wrapperWidth.value); } setTransformTop(0); setTransformLeft(alignInRange(newTransform)); } else { // ============ Align with left & right ============ let newTransform = transformTop.value; if (tabOffset.top < -transformTop.value) { newTransform = -tabOffset.top; } else if (tabOffset.top + tabOffset.height > -transformTop.value + wrapperHeight.value) { newTransform = -(tabOffset.top + tabOffset.height - wrapperHeight.value); } setTransformLeft(0); setTransformTop(alignInRange(newTransform)); } }; const visibleStart = shallowRef(0); const visibleEnd = shallowRef(0); watchEffect(() => { let unit; let position; let transformSize; let basicSize; let tabContentSize; let addSize; const tabOffsetsValue = tabOffsets.value; if (['top', 'bottom'].includes(props.tabPosition)) { unit = 'width'; basicSize = wrapperWidth.value; tabContentSize = wrapperScrollWidth.value; addSize = addWidth.value; position = props.rtl ? 'right' : 'left'; transformSize = Math.abs(transformLeft.value); } else { unit = 'height'; basicSize = wrapperHeight.value; tabContentSize = wrapperScrollWidth.value; addSize = addHeight.value; position = 'top'; transformSize = -transformTop.value; } let mergedBasicSize = basicSize; if (tabContentSize + addSize > basicSize && tabContentSize < basicSize) { mergedBasicSize = basicSize - addSize; } const tabsVal = tabs.value; if (!tabsVal.length) { return [visibleStart.value, visibleEnd.value] = [0, 0]; } const len = tabsVal.length; let endIndex = len; for (let i = 0; i < len; i += 1) { const offset = tabOffsetsValue.get(tabsVal[i].key) || DEFAULT_SIZE; if (offset[position] + offset[unit] > transformSize + mergedBasicSize) { endIndex = i - 1; break; } } let startIndex = 0; for (let i = len - 1; i >= 0; i -= 1) { const offset = tabOffsetsValue.get(tabsVal[i].key) || DEFAULT_SIZE; if (offset[position] < transformSize) { startIndex = i + 1; break; } } return [visibleStart.value, visibleEnd.value] = [startIndex, endIndex]; }); const updateTabSizes = () => { setTabSizes(() => { var _a; const newSizes = new Map(); const listRect = (_a = tabListRef.value) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect(); tabs.value.forEach(_ref2 => { let { key } = _ref2; const btnRef = btnRefs.value.get(key); const btnNode = (btnRef === null || btnRef === void 0 ? void 0 : btnRef.$el) || btnRef; if (btnNode) { const [width, height, left, top] = getTabSize(btnNode, listRect); newSizes.set(key, { width, height, left, top }); } }); return newSizes; }); }; watch(() => tabs.value.map(tab => tab.key).join('%%'), () => { updateTabSizes(); }, { flush: 'post' }); const onListHolderResize = () => { var _a, _b, _c, _d, _e; // Update wrapper records const offsetWidth = ((_a = tabsWrapperRef.value) === null || _a === void 0 ? void 0 : _a.offsetWidth) || 0; const offsetHeight = ((_b = tabsWrapperRef.value) === null || _b === void 0 ? void 0 : _b.offsetHeight) || 0; const addDom = ((_c = innerAddButtonRef.value) === null || _c === void 0 ? void 0 : _c.$el) || {}; const newAddWidth = addDom.offsetWidth || 0; const newAddHeight = addDom.offsetHeight || 0; setWrapperWidth(offsetWidth); setWrapperHeight(offsetHeight); setAddWidth(newAddWidth); setAddHeight(newAddHeight); const newWrapperScrollWidth = (((_d = tabListRef.value) === null || _d === void 0 ? void 0 : _d.offsetWidth) || 0) - newAddWidth; const newWrapperScrollHeight = (((_e = tabListRef.value) === null || _e === void 0 ? void 0 : _e.offsetHeight) || 0) - newAddHeight; setWrapperScrollWidth(newWrapperScrollWidth); setWrapperScrollHeight(newWrapperScrollHeight); // Update buttons records updateTabSizes(); }; // ======================== Dropdown ======================= const hiddenTabs = computed(() => [...tabs.value.slice(0, visibleStart.value), ...tabs.value.slice(visibleEnd.value + 1)]); // =================== Link & Operations =================== const [inkStyle, setInkStyle] = useState(); const activeTabOffset = computed(() => tabOffsets.value.get(props.activeKey)); // Delay set ink style to avoid remove tab blink const inkBarRafRef = shallowRef(); const cleanInkBarRaf = () => { raf.cancel(inkBarRafRef.value); }; watch([activeTabOffset, tabPositionTopOrBottom, () => props.rtl], () => { const newInkStyle = {}; if (activeTabOffset.value) { if (tabPositionTopOrBottom.value) { if (props.rtl) { newInkStyle.right = toPx(activeTabOffset.value.right); } else { newInkStyle.left = toPx(activeTabOffset.value.left); } newInkStyle.width = toPx(activeTabOffset.value.width); } else { newInkStyle.top = toPx(activeTabOffset.value.top); newInkStyle.height = toPx(activeTabOffset.value.height); } } cleanInkBarRaf(); inkBarRafRef.value = raf(() => { setInkStyle(newInkStyle); }); }); watch([() => props.activeKey, activeTabOffset, tabOffsets, tabPositionTopOrBottom], () => { scrollToTab(); }, { flush: 'post' }); watch([() => props.rtl, () => props.tabBarGutter, () => props.activeKey, () => tabs.value], () => { onListHolderResize(); }, { flush: 'post' }); const ExtraContent = _ref3 => { let { position, prefixCls, extra } = _ref3; if (!extra) return null; const content = extra === null || extra === void 0 ? void 0 : extra({ position }); return content ? _createVNode("div", { "class": `${prefixCls}-extra-content` }, [content]) : null; }; onBeforeUnmount(() => { clearTouchMoving(); cleanInkBarRaf(); }); return () => { const { id, animated, activeKey, rtl, editable, locale, tabPosition, tabBarGutter, onTabClick } = props; const { class: className, style } = attrs; const pre = prefixCls.value; // ========================= Render ======================== const hasDropdown = !!hiddenTabs.value.length; const wrapPrefix = `${pre}-nav-wrap`; let pingLeft; let pingRight; let pingTop; let pingBottom; if (tabPositionTopOrBottom.value) { if (rtl) { pingRight = transformLeft.value > 0; pingLeft = transformLeft.value + wrapperWidth.value < wrapperScrollWidth.value; } else { pingLeft = transformLeft.value < 0; pingRight = -transformLeft.value + wrapperWidth.value < wrapperScrollWidth.value; } } else { pingTop = transformTop.value < 0; pingBottom = -transformTop.value + wrapperHeight.value < wrapperScrollHeight.value; } const tabNodeStyle = {}; if (tabPosition === 'top' || tabPosition === 'bottom') { tabNodeStyle[rtl ? 'marginRight' : 'marginLeft'] = typeof tabBarGutter === 'number' ? `${tabBarGutter}px` : tabBarGutter; } else { tabNodeStyle.marginTop = typeof tabBarGutter === 'number' ? `${tabBarGutter}px` : tabBarGutter; } const tabNodes = tabs.value.map((tab, i) => { const { key } = tab; return _createVNode(TabNode, { "id": id, "prefixCls": pre, "key": key, "tab": tab, "style": i === 0 ? undefined : tabNodeStyle, "closable": tab.closable, "editable": editable, "active": key === activeKey, "removeAriaLabel": locale === null || locale === void 0 ? void 0 : locale.removeAriaLabel, "ref": setRef(key), "onClick": e => { onTabClick(key, e); }, "onFocus": () => { scrollToTab(key); doLockAnimation(); if (!tabsWrapperRef.value) { return; } // Focus element will make scrollLeft change which we should reset back if (!rtl) { tabsWrapperRef.value.scrollLeft = 0; } tabsWrapperRef.value.scrollTop = 0; } }, slots); }); return _createVNode("div", { "role": "tablist", "class": classNames(`${pre}-nav`, className), "style": style, "onKeydown": () => { // No need animation when use keyboard doLockAnimation(); } }, [_createVNode(ExtraContent, { "position": "left", "prefixCls": pre, "extra": slots.leftExtra }, null), _createVNode(ResizeObserver, { "onResize": onListHolderResize }, { default: () => [_createVNode("div", { "class": classNames(wrapPrefix, { [`${wrapPrefix}-ping-left`]: pingLeft, [`${wrapPrefix}-ping-right`]: pingRight, [`${wrapPrefix}-ping-top`]: pingTop, [`${wrapPrefix}-ping-bottom`]: pingBottom }), "ref": tabsWrapperRef }, [_createVNode(ResizeObserver, { "onResize": onListHolderResize }, { default: () => [_createVNode("div", { "ref": tabListRef, "class": `${pre}-nav-list`, "style": { transform: `translate(${transformLeft.value}px, ${transformTop.value}px)`, transition: lockAnimation.value ? 'none' : undefined } }, [tabNodes, _createVNode(AddButton, { "ref": innerAddButtonRef, "prefixCls": pre, "locale": locale, "editable": editable, "style": _extends(_extends({}, tabNodes.length === 0 ? undefined : tabNodeStyle), { visibility: hasDropdown ? 'hidden' : null }) }, null), _createVNode("div", { "class": classNames(`${pre}-ink-bar`, { [`${pre}-ink-bar-animated`]: animated.inkBar }), "style": inkStyle.value }, null)])] })])] }), _createVNode(OperationNode, _objectSpread(_objectSpread({}, props), {}, { "removeAriaLabel": locale === null || locale === void 0 ? void 0 : locale.removeAriaLabel, "ref": operationsRef, "prefixCls": pre, "tabs": hiddenTabs.value, "class": !hasDropdown && operationsHiddenClassName.value }), pick(slots, ['moreIcon'])), _createVNode(ExtraContent, { "position": "right", "prefixCls": pre, "extra": slots.rightExtra }, null), _createVNode(ExtraContent, { "position": "right", "prefixCls": pre, "extra": slots.tabBarExtraContent }, null)]); }; } });