import debounce from 'lodash/debounce'; import ResizeObserver from 'resize-observer-polyfill'; import PropTypes from '../../_util/vue-types'; import BaseMixin from '../../_util/BaseMixin'; import { getComponent, getSlot } from '../../_util/props-util'; import { setTransform, isTransform3dSupported } from './utils'; export default { name: 'ScrollableTabBarNode', mixins: [BaseMixin], inheritAttrs: false, props: { activeKey: PropTypes.any, getRef: PropTypes.func.def(() => {}), saveRef: PropTypes.func.def(() => {}), tabBarPosition: PropTypes.oneOf(['left', 'right', 'top', 'bottom']).def('left'), prefixCls: PropTypes.string.def(''), scrollAnimated: PropTypes.bool.def(true), navWrapper: PropTypes.func.def(arg => arg), prevIcon: PropTypes.any, nextIcon: PropTypes.any, direction: PropTypes.string, }, data() { this.offset = 0; this.prevProps = { ...this.$props }; return { next: false, prev: false, }; }, watch: { tabBarPosition() { this.tabBarPositionChange = true; this.$nextTick(() => { this.setOffset(0); }); }, }, mounted() { this.$nextTick(() => { this.updatedCal(); this.debouncedResize = debounce(() => { this.setNextPrev(); this.scrollToActiveTab(); }, 200); this.resizeObserver = new ResizeObserver(this.debouncedResize); this.resizeObserver.observe(this.$props.getRef('container')); }); }, updated() { this.$nextTick(() => { this.updatedCal(this.prevProps); this.prevProps = { ...this.$props }; }); }, beforeUnmount() { if (this.resizeObserver) { this.resizeObserver.disconnect(); } if (this.debouncedResize && this.debouncedResize.cancel) { this.debouncedResize.cancel(); } }, methods: { updatedCal(prevProps) { const props = this.$props; if (prevProps && prevProps.tabBarPosition !== props.tabBarPosition) { this.setOffset(0); return; } // wait next, prev show hide if (this.isNextPrevShown(this.$data) !== this.isNextPrevShown(this.setNextPrev())) { this.$forceUpdate(); this.$nextTick(() => { this.scrollToActiveTab(); }); } else if (!prevProps || props.activeKey !== prevProps.activeKey) { // can not use props.activeKey this.scrollToActiveTab(); } }, setNextPrev() { const navNode = this.$props.getRef('nav'); const navTabsContainer = this.$props.getRef('navTabsContainer'); const navNodeWH = this.getScrollWH(navTabsContainer || navNode); // Add 1px to fix `offsetWidth` with decimal in Chrome not correct handle // https://github.com/ant-design/ant-design/issues/13423 const containerWH = this.getOffsetWH(this.$props.getRef('container')) + 1; const navWrapNodeWH = this.getOffsetWH(this.$props.getRef('navWrap')); let { offset } = this; const minOffset = containerWH - navNodeWH; let { next, prev } = this; if (minOffset >= 0) { next = false; this.setOffset(0, false); offset = 0; } else if (minOffset < offset) { next = true; } else { next = false; // Fix https://github.com/ant-design/ant-design/issues/8861 // Test with container offset which is stable // and set the offset of the nav wrap node const realOffset = navWrapNodeWH - navNodeWH; this.setOffset(realOffset, false); offset = realOffset; } if (offset < 0) { prev = true; } else { prev = false; } this.setNext(next); this.setPrev(prev); return { next, prev, }; }, getOffsetWH(node) { const tabBarPosition = this.$props.tabBarPosition; let prop = 'offsetWidth'; if (tabBarPosition === 'left' || tabBarPosition === 'right') { prop = 'offsetHeight'; } return node[prop]; }, getScrollWH(node) { const tabBarPosition = this.tabBarPosition; let prop = 'scrollWidth'; if (tabBarPosition === 'left' || tabBarPosition === 'right') { prop = 'scrollHeight'; } return node[prop]; }, getOffsetLT(node) { const tabBarPosition = this.$props.tabBarPosition; let prop = 'left'; if (tabBarPosition === 'left' || tabBarPosition === 'right') { prop = 'top'; } return node.getBoundingClientRect()[prop]; }, setOffset(offset, checkNextPrev = true) { let target = Math.min(0, offset); if (this.offset !== target) { this.offset = target; let navOffset = {}; const tabBarPosition = this.$props.tabBarPosition; const navStyle = this.$props.getRef('nav').style; const transformSupported = isTransform3dSupported(navStyle); if (tabBarPosition === 'left' || tabBarPosition === 'right') { if (transformSupported) { navOffset = { value: `translate3d(0,${target}px,0)`, }; } else { navOffset = { name: 'top', value: `${target}px`, }; } } else if (transformSupported) { if (this.$props.direction === 'rtl') { target = -target; } navOffset = { value: `translate3d(${target}px,0,0)`, }; } else { navOffset = { name: 'left', value: `${target}px`, }; } if (transformSupported) { setTransform(navStyle, navOffset.value); } else { navStyle[navOffset.name] = navOffset.value; } if (checkNextPrev) { this.setNextPrev(); } } }, setPrev(v) { if (this.prev !== v) { this.prev = v; } }, setNext(v) { if (this.next !== v) { this.next = v; } }, isNextPrevShown(state) { if (state) { return state.next || state.prev; } return this.next || this.prev; }, prevTransitionEnd(e) { if (e.propertyName !== 'opacity') { return; } const container = this.$props.getRef('container'); this.scrollToActiveTab({ target: container, currentTarget: container, }); }, scrollToActiveTab(e) { const activeTab = this.$props.getRef('activeTab'); const navWrap = this.$props.getRef('navWrap'); if ((e && e.target !== e.currentTarget) || !activeTab) { return; } // when not scrollable or enter scrollable first time, don't emit scrolling const needToSroll = this.isNextPrevShown() && this.lastNextPrevShown; this.lastNextPrevShown = this.isNextPrevShown(); if (!needToSroll) { return; } const activeTabWH = this.getScrollWH(activeTab); const navWrapNodeWH = this.getOffsetWH(navWrap); let { offset } = this; const wrapOffset = this.getOffsetLT(navWrap); const activeTabOffset = this.getOffsetLT(activeTab); if (wrapOffset > activeTabOffset) { offset += wrapOffset - activeTabOffset; this.setOffset(offset); } else if (wrapOffset + navWrapNodeWH < activeTabOffset + activeTabWH) { offset -= activeTabOffset + activeTabWH - (wrapOffset + navWrapNodeWH); this.setOffset(offset); } }, prevClick(e) { this.__emit('prevClick', e); const navWrapNode = this.$props.getRef('navWrap'); const navWrapNodeWH = this.getOffsetWH(navWrapNode); const { offset } = this; this.setOffset(offset + navWrapNodeWH); }, nextClick(e) { this.__emit('nextClick', e); const navWrapNode = this.$props.getRef('navWrap'); const navWrapNodeWH = this.getOffsetWH(navWrapNode); const { offset } = this; this.setOffset(offset - navWrapNodeWH); }, }, render() { const { next, prev } = this; const { prefixCls, scrollAnimated, navWrapper } = this.$props; const prevIcon = getComponent(this, 'prevIcon'); const nextIcon = getComponent(this, 'nextIcon'); const showNextPrev = prev || next; const prevButton = ( {prevIcon || } ); const nextButton = ( {nextIcon || } ); const navClassName = `${prefixCls}-nav`; const navClasses = { [navClassName]: true, [scrollAnimated ? `${navClassName}-animated` : `${navClassName}-no-animated`]: true, }; return (
{prevButton} {nextButton}
{navWrapper(getSlot(this))}
); }, };