diff --git a/components/tabs/InkTabBar.vue b/components/tabs/InkTabBar.vue new file mode 100644 index 000000000..501bf6c3e --- /dev/null +++ b/components/tabs/InkTabBar.vue @@ -0,0 +1,14 @@ + diff --git a/components/tabs/InkTabBarMixin.js b/components/tabs/InkTabBarMixin.js new file mode 100644 index 000000000..fa27ce527 --- /dev/null +++ b/components/tabs/InkTabBarMixin.js @@ -0,0 +1,146 @@ +import { setTransform, isTransformSupported } from './utils' + +export function getScroll (w, top) { + let ret = w[`page${top ? 'Y' : 'X'}Offset`] + const method = `scroll${top ? 'Top' : 'Left'}` + if (typeof ret !== 'number') { + const d = w.document + // ie6,7,8 standard mode + ret = d.documentElement[method] + if (typeof ret !== 'number') { + // quirks mode + ret = d.body[method] + } + } + return ret +} + +function offset (elem) { + let x + let y + const doc = elem.ownerDocument + const body = doc.body + const docElem = doc && doc.documentElement + const box = elem.getBoundingClientRect() + x = box.left + y = box.top + x -= docElem.clientLeft || body.clientLeft || 0 + y -= docElem.clientTop || body.clientTop || 0 + const w = doc.defaultView || doc.parentWindow + x += getScroll(w) + y += getScroll(w, true) + return { + left: x, top: y, + } +} + +function componentDidUpdate (component, init) { + const { styles } = component.$props + const wrapNode = component.$refs.nav || component.$refs.root + const containerOffset = offset(wrapNode) + const inkBarNode = component.$refs.inkBar + const activeTab = component.$refs.activeTab + const inkBarNodeStyle = inkBarNode.style + const tabBarPosition = component.$props.tabBarPosition + if (init) { + // prevent mount animation + inkBarNodeStyle.display = 'none' + } + if (activeTab) { + const tabNode = activeTab + const tabOffset = offset(tabNode) + const transformSupported = isTransformSupported(inkBarNodeStyle) + if (tabBarPosition === 'top' || tabBarPosition === 'bottom') { + let left = tabOffset.left - containerOffset.left + let width = tabNode.offsetWidth + + // If tabNode'width width equal to wrapNode'width when tabBarPosition is top or bottom + // It means no css working, then ink bar should not have width until css is loaded + // Fix https://github.com/ant-design/ant-design/issues/7564 + if (width === wrapNode.offsetWidth) { + width = 0 + } else if (styles.inkBar && styles.inkBar.width !== undefined) { + width = parseFloat(styles.inkBar.width, 10) + if (width) { + left = left + (tabNode.offsetWidth - width) / 2 + } + } + // use 3d gpu to optimize render + if (transformSupported) { + setTransform(inkBarNodeStyle, `translate3d(${left}px,0,0)`) + inkBarNodeStyle.width = `${width}px` + inkBarNodeStyle.height = '' + } else { + inkBarNodeStyle.left = `${left}px` + inkBarNodeStyle.top = '' + inkBarNodeStyle.bottom = '' + inkBarNodeStyle.right = `${wrapNode.offsetWidth - left - width}px` + } + } else { + let top = tabOffset.top - containerOffset.top + let height = tabNode.offsetHeight + if (styles.inkBar && styles.inkBar.height !== undefined) { + height = parseFloat(styles.inkBar.height, 10) + if (height) { + top = top + (tabNode.offsetHeight - height) / 2 + } + } + if (transformSupported) { + setTransform(inkBarNodeStyle, `translate3d(0,${top}px,0)`) + inkBarNodeStyle.height = `${height}px` + inkBarNodeStyle.width = '' + } else { + inkBarNodeStyle.left = '' + inkBarNodeStyle.right = '' + inkBarNodeStyle.top = `${top}px` + inkBarNodeStyle.bottom = `${wrapNode.offsetHeight - top - height}px` + } + } + } + inkBarNodeStyle.display = activeTab ? 'block' : 'none' +} + +export default { + props: { + inkBarAnimated: { + type: Boolean, + default: true, + }, + prefixCls: String, + styles: Object, + }, + updated () { + this.$nextTick(function () { + componentDidUpdate(this, true) + }) + componentDidUpdate(this) + }, + + mounted () { + this.$nextTick(function () { + componentDidUpdate(this, true) + }) + }, + methods: { + getInkBarNode () { + const { prefixCls, styles = {}, inkBarAnimated } = this + const className = `${prefixCls}-ink-bar` + const classes = { + [className]: true, + [ + inkBarAnimated + ? `${className}-animated` + : `${className}-no-animated` + ]: true, + } + return ( +
+ ) + }, + }, +} diff --git a/components/tabs/ScrollableInkTabBar.vue b/components/tabs/ScrollableInkTabBar.vue new file mode 100644 index 000000000..e3a729727 --- /dev/null +++ b/components/tabs/ScrollableInkTabBar.vue @@ -0,0 +1,17 @@ + + diff --git a/components/tabs/ScrollableTabBar.vue b/components/tabs/ScrollableTabBar.vue new file mode 100644 index 000000000..d041190e9 --- /dev/null +++ b/components/tabs/ScrollableTabBar.vue @@ -0,0 +1,16 @@ + + diff --git a/components/tabs/ScrollableTabBarMixin.js b/components/tabs/ScrollableTabBarMixin.js new file mode 100644 index 000000000..e2371a41b --- /dev/null +++ b/components/tabs/ScrollableTabBarMixin.js @@ -0,0 +1,288 @@ +import { setTransform, isTransformSupported } from './utils' +import addDOMEventListener from 'add-dom-event-listener' +import debounce from 'lodash.debounce' +function noop () { +} +export default { + props: { + scrollAnimated: { type: Boolean, default: true }, + onPrevClick: { type: Function, default: noop }, + onNextClick: { type: Function, default: noop }, + }, + + data () { + return { + next: false, + prev: false, + offset: 0, + } + }, + + mounted () { + this.updatedCal() + const debouncedResize = debounce(() => { + this.setNextPrev() + this.scrollToActiveTab() + }, 200) + this.resizeEvent = addDOMEventListener(window, 'resize', debouncedResize) + }, + + updated (prevProps) { + const props = this.$props + if (prevProps && prevProps.tabBarPosition !== props.tabBarPosition) { + this.setOffset(0) + return + } + const nextPrev = this.setNextPrev() + // wait next, prev show hide + if (this.isNextPrevShown(this) !== this.isNextPrevShown(nextPrev)) { + Object.assign(this, this.scrollToActiveTab) + } else if (!prevProps || props.activeKey !== prevProps.activeKey) { + // can not use props.activeKey + this.scrollToActiveTab() + } + }, + + beforeDestroy () { + if (this.resizeEvent) { + this.resizeEvent.remove() + } + }, + methods: { + updatedCal () { + + }, + setNextPrev () { + const navNode = this.nav + const navNodeWH = this.getOffsetWH(navNode) + const navWrapNode = this.navWrap + const navWrapNodeWH = this.getOffsetWH(navWrapNode) + let { offset } = this + const minOffset = navWrapNodeWH - 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) + this.setOffset(minOffset, false) + offset = minOffset + } + + 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] + }, + + 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) { + const target = Math.min(0, offset) + if (this.offset !== target) { + this.offset = target + let navOffset = {} + const tabBarPosition = this.$props.tabBarPosition + const navStyle = this.nav.style + const transformSupported = isTransformSupported(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) { + 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 + this.scrollToActiveTab({ + target: container, + currentTarget: container, + }) + }, + + scrollToActiveTab (e) { + const { activeTab, navWrap } = this + 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.getOffsetWH(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.$props.onPrevClick(e) + const navWrapNode = this.navWrap + const navWrapNodeWH = this.getOffsetWH(navWrapNode) + const { offset } = this + this.setOffset(offset + navWrapNodeWH) + }, + + nextClick (e) { + this.$props.onNextClick(e) + const navWrapNode = this.navWrap + const navWrapNodeWH = this.getOffsetWH(navWrapNode) + const { offset } = this + this.setOffset(offset - navWrapNodeWH) + }, + + getScrollBarNode (content) { + const { next, prev } = this + const { prefixCls, scrollAnimated } = this.$props + const showNextPrev = prev || next + + const prevButton = ( + + + + ) + + const nextButton = ( + + + + ) + + const navClassName = `${prefixCls}-nav` + const navClasses = { + [navClassName]: true, + [ + scrollAnimated + ? `${navClassName}-animated` + : `${navClassName}-no-animated` + ]: true, + } + + return ( +