pull/9/head
tangjinzhou 2017-11-30 19:11:42 +08:00
parent eb021ff40c
commit b1f8fd739f
10 changed files with 696 additions and 6 deletions

View File

@ -0,0 +1,14 @@
<script>
import InkTabBarMixin from './InkTabBarMixin'
import TabBarMixin from './TabBarMixin'
export default {
name: 'InkTabBar',
mixins: [TabBarMixin, InkTabBarMixin],
render () {
const inkBarNode = this.getInkBarNode()
const tabs = this.getTabs()
return this.getRootNode([inkBarNode, tabs])
},
}
</script>

View File

@ -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 (
<div
style={styles.inkBar}
class={classes}
key='inkBar'
ref='inkBar'
/>
)
},
},
}

View File

@ -0,0 +1,17 @@
<script>
import InkTabBarMixin from './InkTabBarMixin'
import ScrollableTabBarMixin from './ScrollableTabBarMixin'
import TabBarMixin from './TabBarMixin'
export default {
name: 'ScrollableInkTabBar',
mixins: [TabBarMixin, InkTabBarMixin, ScrollableTabBarMixin],
render () {
const inkBarNode = this.getInkBarNode()
const tabs = this.getTabs()
const scrollbarNode = this.getScrollBarNode([inkBarNode, tabs])
return this.getRootNode(scrollbarNode)
},
}
</script>

View File

@ -0,0 +1,16 @@
<script>
import ScrollableTabBarMixin from './ScrollableTabBarMixin'
import TabBarMixin from './TabBarMixin'
export default {
name: 'ScrollableTabBar',
mixins: [TabBarMixin, ScrollableTabBarMixin],
render () {
const inkBarNode = this.getInkBarNode()
const tabs = this.getTabs()
const scrollbarNode = this.getScrollBarNode([inkBarNode, tabs])
return this.getRootNode(scrollbarNode)
},
}
</script>

View File

@ -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 = (
<span
onClick={prev ? this.prevClick : noop}
unselectable='unselectable'
class={{
[`${prefixCls}-tab-prev`]: 1,
[`${prefixCls}-tab-btn-disabled`]: !prev,
[`${prefixCls}-tab-arrow-show`]: showNextPrev,
}}
onTransitionEnd={this.prevTransitionEnd}
>
<span class={`${prefixCls}-tab-prev-icon`} />
</span>
)
const nextButton = (
<span
onClick={next ? this.nextClick : noop}
unselectable='unselectable'
class={{
[`${prefixCls}-tab-next`]: 1,
[`${prefixCls}-tab-btn-disabled`]: !next,
[`${prefixCls}-tab-arrow-show`]: showNextPrev,
}}
>
<span class={`${prefixCls}-tab-next-icon`} />
</span>
)
const navClassName = `${prefixCls}-nav`
const navClasses = {
[navClassName]: true,
[
scrollAnimated
? `${navClassName}-animated`
: `${navClassName}-no-animated`
]: true,
}
return (
<div
class={{
[`${prefixCls}-nav-container`]: 1,
[`${prefixCls}-nav-container-scrolling`]: showNextPrev,
}}
key='container'
ref='container'
>
{prevButton}
{nextButton}
<div class={`${prefixCls}-nav-wrap`} ref='navWrap'>
<div class={`${prefixCls}-nav-scroll`}>
<div class={navClasses} ref='nav'>
{content}
</div>
</div>
</div>
</div>
)
},
},
}

View File

@ -1,4 +1,25 @@
export default {
props: {
prefixCls: {
default: 'ant-tabs',
type: String,
},
tabBarPosition: {
default: 'top',
type: String,
},
disabled: Boolean,
onKeyDown: {
default: () => {},
type: Function,
},
onTabClick: {
default: () => {},
type: Function,
},
activeKey: String,
panels: Array,
},
methods: {
getTabs () {
const { panels: children, activeKey, prefixCls } = this

View File

@ -3,6 +3,7 @@ import Icon from '../icon'
import KeyCode from './KeyCode'
import TabBar from './TabBar'
import TabContent from './TabContent'
import ScrollableInkTabBar from './ScrollableInkTabBar'
function getDefaultActiveKey (t) {
let activeKey
t.$slot.default.forEach((child) => {
@ -34,6 +35,11 @@ export default {
return ['top', 'bottom', 'left', 'right'].includes(value)
},
},
renderTabBar: {
type: Function,
default: () => {},
},
tabBarProps: Object,
destroyInactiveTabPane: Boolean,
activeKey: String,
defaultActiveKey: String,
@ -162,6 +168,7 @@ export default {
}
const tabBarProps = {
props: {
...tabBarProps,
panels: panels,
prefixCls: prefixCls,
onKeyDown: onNavKeyDown,
@ -172,9 +179,9 @@ export default {
},
}
const contents = [
<TabBar {...tabBarProps}>
<ScrollableInkTabBar {...tabBarProps}>
{this.$slots.tabBarExtraContent}
</TabBar>,
</ScrollableInkTabBar>,
<TabContent {...tabContentProps}>
{$slots.default}
</TabContent>,

View File

@ -1,6 +1,6 @@
import Tabs from './Tabs'
import Tabs from './index.vue'
import TabPane from './TabPane'
// import TabContent from './TabContent'
import TabContent from './TabContent'
Tabs.TabPane = TabPane
export default Tabs
export { TabPane }
export { TabPane, TabContent }

179
components/tabs/index.vue Normal file
View File

@ -0,0 +1,179 @@
<script>
import Tabs from './Tabs'
// import TabPane from './TabPane'
import ScrollableInkTabBar from './ScrollableInkTabBar'
import TabBar from './TabBar'
import TabContent from './TabContent'
import Icon from '../icon'
import isFlexSupported from '../_util/isFlexSupported'
export default {
props: {
prefixCls: { type: String, default: 'ant-tabs' },
activeKey: String,
defaultActiveKey: String,
hideAdd: { type: Boolean, default: false },
onChange: { type: Function, default: () => {} },
onTabClick: { type: Function, default: () => {} },
onPrevClick: { type: Function, default: () => {} },
onNextClick: { type: Function, default: () => {} },
tabBarStyle: Object,
type: {
validator (value) {
return ['line', 'card', 'editable-card'].includes(value)
},
},
tabPosition: {
validator (value) {
return ['top', 'right', 'bottom', 'left'].includes(value)
},
},
onEdit: { type: Function, default: () => {} },
size: {
validator (value) {
return ['default', 'small'].includes(value)
},
},
animated: Boolean | Object,
},
methods: {
createNewTab (targetKey) {
const onEdit = this.$props.onEdit
if (onEdit) {
onEdit(targetKey, 'add')
}
},
removeTab (targetKey, e) {
e.stopPropagation()
if (!targetKey) {
return
}
const onEdit = this.$props.onEdit
if (onEdit) {
onEdit(targetKey, 'remove')
}
},
handleChange (activeKey) {
const onChange = this.$props.onChange
if (onChange) {
onChange(activeKey)
}
},
},
mounted () {
const NO_FLEX = ' no-flex'
const tabNode = this.$el
if (tabNode && !isFlexSupported() && tabNode.className.indexOf(NO_FLEX) === -1) {
tabNode.className += NO_FLEX
}
},
render () {
const {
prefixCls,
size,
type = 'line',
tabPosition,
children,
tabBarStyle,
// hideAdd,
onTabClick,
onPrevClick,
onNextClick,
animated = true,
} = this.$props
let { tabBarExtraContent } = this.$props
let { inkBarAnimated, tabPaneAnimated } = typeof animated === 'object' ? { // eslint-disable-line
inkBarAnimated: animated.inkBar, tabPaneAnimated: animated.tabPane,
} : {
inkBarAnimated: animated, tabPaneAnimated: animated,
}
// card tabs should not have animation
if (type !== 'line') {
tabPaneAnimated = 'animated' in this.$props ? tabPaneAnimated : false
}
const cls = {
[`${prefixCls}-mini`]: size === 'small' || size,
[`${prefixCls}-vertical`]: tabPosition === 'left' || tabPosition === 'right',
[`${prefixCls}-card`]: type.indexOf('card') >= 0,
[`${prefixCls}-${type}`]: true,
[`${prefixCls}-no-animation`]: !tabPaneAnimated,
}
// only card type tabs can be added and closed
let childrenWithClose
// if (type === 'editable-card') {
// childrenWithClose = []
// React.Children.forEach(children, (child, index) => {
// let closable = child.props.closable
// closable = typeof closable === 'undefined' ? true : closable
// const closeIcon = closable ? (
// <Icon
// type='close'
// onClick={e => this.removeTab(child.key, e)}
// />
// ) : null
// childrenWithClose.push(cloneElement(child, {
// tab: (
// <div className={closable ? undefined : `${prefixCls}-tab-unclosable`}>
// {child.props.tab}
// {closeIcon}
// </div>
// ),
// key: child.key || index,
// }))
// })
// // Add new tab handler
// if (!hideAdd) {
// tabBarExtraContent = (
// <span>
// <Icon type='plus' className={`${prefixCls}-new-tab`} onClick={this.createNewTab} />
// {tabBarExtraContent}
// </span>
// )
// }
// }
tabBarExtraContent = tabBarExtraContent ? (
<div class={`${prefixCls}-extra-content`}>
{tabBarExtraContent}
</div>
) : null
const renderTabBar = () => (
<ScrollableInkTabBar
inkBarAnimated={inkBarAnimated}
extraContent={tabBarExtraContent}
onTabClick={onTabClick}
onPrevClick={onPrevClick}
onNextClick={onNextClick}
style={tabBarStyle}
/>
)
const tabBarProps = {
inkBarAnimated,
extraContent: tabBarExtraContent,
onTabClick,
onPrevClick,
onNextClick,
style: tabBarStyle,
}
return (
<Tabs
{...this.$props}
class={cls}
tabBarPosition={tabPosition}
renderTabBar={renderTabBar}
// renderTabContent={() => <TabContent animated={tabPaneAnimated} animatedWithMargin />}
onChange={this.handleChange}
tabBarProps={tabBarProps}
>
{childrenWithClose || this.$slots.default}
</Tabs>
)
},
}
</script>

View File

@ -74,6 +74,8 @@
"webpack-dev-server": "^2.8.2"
},
"dependencies": {
"eslint-plugin-vue": "^3.13.0"
"add-dom-event-listener": "^1.0.2",
"eslint-plugin-vue": "^3.13.0",
"lodash.debounce": "^4.0.8"
}
}