element-plus
Version:
> TODO: description
291 lines (263 loc) • 6.52 kB
text/typescript
import {
defineComponent,
Fragment,
getCurrentInstance,
h,
nextTick,
onMounted,
onUpdated,
provide,
ref,
watch,
} from 'vue'
import { isPromise } from '@vue/shared'
import { EVENT_CODE } from '@element-plus/utils/aria'
import TabNav from './tab-nav.vue'
import type { Component, ComponentInternalInstance, PropType, VNode } from 'vue'
import type {
BeforeLeave,
IElTabsProps,
ITabType,
ITabPosition,
Pane,
RootTabs,
UpdatePaneStateCallback,
} from './token'
export default defineComponent({
name: 'ElTabs',
components: { TabNav },
props: {
type: {
type: String as PropType<ITabType>,
default: '',
},
activeName: {
type: String,
default: '',
},
closable: Boolean,
addable: Boolean,
modelValue: {
type: String,
default: '',
},
editable: Boolean,
tabPosition: {
type: String as PropType<ITabPosition>,
default: 'top',
},
beforeLeave: {
type: Function as PropType<BeforeLeave>,
default: null,
},
stretch: Boolean,
},
emits: [
'tab-click',
'edit',
'tab-remove',
'tab-add',
'input',
'update:modelValue',
],
setup(props: IElTabsProps, ctx) {
const nav$ = ref<typeof TabNav>(null)
const currentName = ref(props.modelValue || props.activeName || '0')
const panes = ref([])
const instance = getCurrentInstance()
const paneStatesMap = {}
provide<RootTabs>('rootTabs', {
props,
currentName,
})
provide<UpdatePaneStateCallback>('updatePaneState', (pane: Pane) => {
paneStatesMap[pane.uid] = pane
})
watch(
() => props.activeName,
modelValue => {
setCurrentName(modelValue)
},
)
watch(
() => props.modelValue,
modelValue => {
setCurrentName(modelValue)
},
)
watch(currentName, () => {
if (nav$.value) {
nextTick(() => {
nav$.value.$nextTick(() => {
nav$.value.scrollToActiveTab()
})
})
}
setPaneInstances(true)
})
const getPaneInstanceFromSlot = (
vnode: VNode,
paneInstanceList: ComponentInternalInstance[] = [],
) => {
Array.from((vnode.children || []) as ArrayLike<VNode>).forEach(node => {
let type = node.type
type = (type as Component).name || type
if (type === 'ElTabPane' && node.component) {
paneInstanceList.push(node.component)
} else if (type === Fragment || type === 'template') {
getPaneInstanceFromSlot(node, paneInstanceList)
}
})
return paneInstanceList
}
const setPaneInstances = (isForceUpdate = false) => {
if (ctx.slots.default) {
const children = instance.subTree.children
const content = Array.from(children as ArrayLike<VNode>).find(
({ props }) => {
return props.class === 'el-tabs__content'
},
)
if (!content) return
const paneInstanceList: Pane[] = getPaneInstanceFromSlot(content).map(
paneComponent => {
return paneStatesMap[paneComponent.uid]
},
)
const panesChanged = !(
paneInstanceList.length === panes.value.length &&
paneInstanceList.every(
(pane, index) => pane.uid === panes.value[index].uid,
)
)
if (isForceUpdate || panesChanged) {
panes.value = paneInstanceList
}
} else if (panes.value.length !== 0) {
panes.value = []
}
}
const changeCurrentName = value => {
currentName.value = value
ctx.emit('input', value)
ctx.emit('update:modelValue', value)
}
const setCurrentName = value => {
// should do nothing.
if (currentName.value === value) return
const beforeLeave = props.beforeLeave
const before = beforeLeave && beforeLeave(value, currentName.value)
if (before && isPromise(before)) {
before.then(
() => {
changeCurrentName(value)
nav$.value.removeFocus?.()
},
() => {
// ignore promise rejection in `before-leave` hook
},
)
} else if (before !== false) {
changeCurrentName(value)
}
}
const handleTabClick = (tab, tabName, event) => {
if (tab.props.disabled) return
setCurrentName(tabName)
ctx.emit('tab-click', tab, event)
}
const handleTabRemove = (pane, ev) => {
if (pane.props.disabled) return
ev.stopPropagation()
ctx.emit('edit', pane.props.name, 'remove')
ctx.emit('tab-remove', pane.props.name)
}
const handleTabAdd = () => {
ctx.emit('edit', null, 'add')
ctx.emit('tab-add')
}
onUpdated(() => {
setPaneInstances()
})
onMounted(() => {
setPaneInstances()
})
return {
nav$,
handleTabClick,
handleTabRemove,
handleTabAdd,
currentName,
panes,
}
},
render() {
const {
type,
handleTabClick,
handleTabRemove,
handleTabAdd,
currentName,
panes,
editable,
addable,
tabPosition,
stretch,
} = this
const newButton =
editable || addable
? h(
'span',
{
class: 'el-tabs__new-tab',
tabindex: '0',
onClick: handleTabAdd,
onKeydown: ev => {
if (ev.code === EVENT_CODE.enter) {
handleTabAdd()
}
},
},
[h('i', { class: 'el-icon-plus' })],
)
: null
const header = h(
'div',
{
class: ['el-tabs__header', `is-${tabPosition}`],
},
[
newButton,
h(TabNav, {
currentName,
editable,
type,
panes,
stretch,
ref: 'nav$',
onTabClick: handleTabClick,
onTabRemove: handleTabRemove,
}),
],
)
const panels = h(
'div',
{
class: 'el-tabs__content',
},
this.$slots?.default(),
)
return h(
'div',
{
class: {
'el-tabs': true,
'el-tabs--card': type === 'card',
[`el-tabs--${tabPosition}`]: true,
'el-tabs--border-card': type === 'border-card',
},
},
tabPosition !== 'bottom' ? [header, panels] : [panels, header],
)
},
})