import { Component, ComponentInternalInstance, ComputedRef, defineComponent, Fragment, getCurrentInstance, h, nextTick, onMounted, onUpdated, PropType, provide, Ref, ref, VNode, watch, } from 'vue' import TabNav from './tabs-nav.vue' export interface RTabsProps { [key: string]: any } export interface RPaneProps { label: string name: string closable: boolean disabled: boolean lazy: boolean } export interface RootTabs { props: RTabsProps currentName: Ref } type BeforeLeave = (newTabName: string, oldTabName: string) => void | Promise | boolean export type UpdatePaneStateCallback = (pane: Pane) => void export interface Pane { uid: number instance: ComponentInternalInstance props: RPaneProps paneName: ComputedRef active: ComputedRef index: Ref isClosable: ComputedRef } export type RTabsAlign = PropType<'centered' | 'right'> export type RTabsSize = PropType<'small' | 'medium' | 'large'> export type RTabsType = PropType<'boxed'> export default defineComponent({ name: 'RolTabs', components: { TabNav }, props: { activeName: { type: String, default: '', }, modelValue: { type: String, default: '', }, beforeLeave: { type: Function as PropType, default: null, }, align: { type: String as RTabsAlign, default: '', validator: (val: string) => { return ['centered', 'right', ''].includes(val) }, }, size: { type: String as RTabsSize, default: '', validator: (val: string) => { return ['small', 'medium', 'large', ''].includes(val) }, }, type: { type: String as RTabsType, default: '', validator: (val: string) => { return ['boxed', ''].includes(val) }, }, tabPosition: { type: String, default: 'top', }, fullwidth: Boolean, closable: Boolean, }, emits: ['update:modelValue', 'input', 'tab-click', 'edit', 'tab-remove'], setup(props, ctx) { const nav$ = ref(null) const currentName = ref(props.modelValue || props.activeName || '0') const panes = ref([]) const instance = getCurrentInstance() const paneStatesMap = {} provide('rootTabs', { props, currentName, }) provide('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).forEach(node => { let type = node.type type = (type as Component).name || type if (type === 'RolTabPane' && 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).find(({ props }) => { return props.class === 'rol-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: string) => { currentName.value = value ctx.emit('input', value) ctx.emit('update:modelValue', value) } const setCurrentName = (value: string) => { if (currentName.value !== value && props.beforeLeave) { const before = props.beforeLeave(value, currentName.value) if (before && (before as Promise).then) { ;(before as Promise).then( () => { changeCurrentName(value) nav$.value && nav$.value.removeFocus() }, () => { // ignore promise rejection in `before-leave` hook }, ) } else if (before !== false) { changeCurrentName(value) } } else { changeCurrentName(value) } } const handleTabClick = (tab: Pane, tabName: string, event: MouseEvent) => { 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) } onUpdated(() => { setPaneInstances() }) onMounted(() => { setPaneInstances() }) return { nav$, currentName, panes, handleTabClick, handleTabRemove, } }, render() { const { handleTabClick, panes, currentName, type, size, align, fullwidth, tabPosition, handleTabRemove } = this const header = h( 'div', { class: ['rol-tabs__header', , `is-${tabPosition}`], }, [ h(TabNav, { panes, currentName, onTabClick: handleTabClick, onTabRemove: handleTabRemove, type, size, align, fullwidth, ref: 'nav$', }), ], ) const panels = h( 'div', { class: 'rol-tabs__content', }, this.$slots?.default(), ) const tabs = h( 'div', { class: ['rol-tabs', `rol-tabs--${tabPosition}`], }, tabPosition !== 'bottom' ? [header, panels] : [panels, header], ) return tabs }, })