first
This commit is contained in:
8
packages/tabs/index.js
Normal file
8
packages/tabs/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import ElTabs from './src/tabs';
|
||||
|
||||
/* istanbul ignore next */
|
||||
ElTabs.install = function(Vue) {
|
||||
Vue.component(ElTabs.name, ElTabs);
|
||||
};
|
||||
|
||||
export default ElTabs;
|
57
packages/tabs/src/tab-bar.vue
Normal file
57
packages/tabs/src/tab-bar.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="el-tabs__active-bar" :class="`is-${ rootTabs.tabPosition }`" :style="barStyle"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { arrayFind } from 'element-ui/src/utils/util';
|
||||
export default {
|
||||
name: 'TabBar',
|
||||
|
||||
props: {
|
||||
tabs: Array
|
||||
},
|
||||
|
||||
inject: ['rootTabs'],
|
||||
|
||||
computed: {
|
||||
barStyle: {
|
||||
get() {
|
||||
let style = {};
|
||||
let offset = 0;
|
||||
let tabSize = 0;
|
||||
const sizeName = ['top', 'bottom'].indexOf(this.rootTabs.tabPosition) !== -1 ? 'width' : 'height';
|
||||
const sizeDir = sizeName === 'width' ? 'x' : 'y';
|
||||
const firstUpperCase = str => {
|
||||
return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase());
|
||||
};
|
||||
this.tabs.every((tab, index) => {
|
||||
let $el = arrayFind(this.$parent.$refs.tabs || [], t => t.id.replace('tab-', '') === tab.paneName);
|
||||
if (!$el) { return false; }
|
||||
|
||||
if (!tab.active) {
|
||||
offset += $el[`client${firstUpperCase(sizeName)}`];
|
||||
return true;
|
||||
} else {
|
||||
tabSize = $el[`client${firstUpperCase(sizeName)}`];
|
||||
const tabStyles = window.getComputedStyle($el);
|
||||
if (sizeName === 'width' && this.tabs.length > 1) {
|
||||
tabSize -= parseFloat(tabStyles.paddingLeft) + parseFloat(tabStyles.paddingRight);
|
||||
}
|
||||
if (sizeName === 'width') {
|
||||
offset += parseFloat(tabStyles.paddingLeft);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const transform = `translate${firstUpperCase(sizeDir)}(${offset}px)`;
|
||||
style[sizeName] = tabSize + 'px';
|
||||
style.transform = transform;
|
||||
style.msTransform = transform;
|
||||
style.webkitTransform = transform;
|
||||
|
||||
return style;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
294
packages/tabs/src/tab-nav.vue
Normal file
294
packages/tabs/src/tab-nav.vue
Normal file
@@ -0,0 +1,294 @@
|
||||
<script>
|
||||
import TabBar from './tab-bar';
|
||||
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
|
||||
|
||||
function noop() {}
|
||||
const firstUpperCase = str => {
|
||||
return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase());
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'TabNav',
|
||||
|
||||
components: {
|
||||
TabBar
|
||||
},
|
||||
|
||||
inject: ['rootTabs'],
|
||||
|
||||
props: {
|
||||
panes: Array,
|
||||
currentName: String,
|
||||
editable: Boolean,
|
||||
onTabClick: {
|
||||
type: Function,
|
||||
default: noop
|
||||
},
|
||||
onTabRemove: {
|
||||
type: Function,
|
||||
default: noop
|
||||
},
|
||||
type: String,
|
||||
stretch: Boolean
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
scrollable: false,
|
||||
navOffset: 0,
|
||||
isFocus: false,
|
||||
focusable: true
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
navStyle() {
|
||||
const dir = ['top', 'bottom'].indexOf(this.rootTabs.tabPosition) !== -1 ? 'X' : 'Y';
|
||||
return {
|
||||
transform: `translate${dir}(-${this.navOffset}px)`
|
||||
};
|
||||
},
|
||||
sizeName() {
|
||||
return ['top', 'bottom'].indexOf(this.rootTabs.tabPosition) !== -1 ? 'width' : 'height';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
scrollPrev() {
|
||||
const containerSize = this.$refs.navScroll[`offset${firstUpperCase(this.sizeName)}`];
|
||||
const currentOffset = this.navOffset;
|
||||
|
||||
if (!currentOffset) return;
|
||||
|
||||
let newOffset = currentOffset > containerSize
|
||||
? currentOffset - containerSize
|
||||
: 0;
|
||||
|
||||
this.navOffset = newOffset;
|
||||
},
|
||||
scrollNext() {
|
||||
const navSize = this.$refs.nav[`offset${firstUpperCase(this.sizeName)}`];
|
||||
const containerSize = this.$refs.navScroll[`offset${firstUpperCase(this.sizeName)}`];
|
||||
const currentOffset = this.navOffset;
|
||||
|
||||
if (navSize - currentOffset <= containerSize) return;
|
||||
|
||||
let newOffset = navSize - currentOffset > containerSize * 2
|
||||
? currentOffset + containerSize
|
||||
: (navSize - containerSize);
|
||||
|
||||
this.navOffset = newOffset;
|
||||
},
|
||||
scrollToActiveTab() {
|
||||
if (!this.scrollable) return;
|
||||
const nav = this.$refs.nav;
|
||||
const activeTab = this.$el.querySelector('.is-active');
|
||||
if (!activeTab) return;
|
||||
const navScroll = this.$refs.navScroll;
|
||||
const isHorizontal = ['top', 'bottom'].indexOf(this.rootTabs.tabPosition) !== -1;
|
||||
const activeTabBounding = activeTab.getBoundingClientRect();
|
||||
const navScrollBounding = navScroll.getBoundingClientRect();
|
||||
const maxOffset = isHorizontal
|
||||
? nav.offsetWidth - navScrollBounding.width
|
||||
: nav.offsetHeight - navScrollBounding.height;
|
||||
const currentOffset = this.navOffset;
|
||||
let newOffset = currentOffset;
|
||||
|
||||
if (isHorizontal) {
|
||||
if (activeTabBounding.left < navScrollBounding.left) {
|
||||
newOffset = currentOffset - (navScrollBounding.left - activeTabBounding.left);
|
||||
}
|
||||
if (activeTabBounding.right > navScrollBounding.right) {
|
||||
newOffset = currentOffset + activeTabBounding.right - navScrollBounding.right;
|
||||
}
|
||||
} else {
|
||||
if (activeTabBounding.top < navScrollBounding.top) {
|
||||
newOffset = currentOffset - (navScrollBounding.top - activeTabBounding.top);
|
||||
}
|
||||
if (activeTabBounding.bottom > navScrollBounding.bottom) {
|
||||
newOffset = currentOffset + (activeTabBounding.bottom - navScrollBounding.bottom);
|
||||
}
|
||||
}
|
||||
newOffset = Math.max(newOffset, 0);
|
||||
this.navOffset = Math.min(newOffset, maxOffset);
|
||||
},
|
||||
update() {
|
||||
if (!this.$refs.nav) return;
|
||||
const sizeName = this.sizeName;
|
||||
const navSize = this.$refs.nav[`offset${firstUpperCase(sizeName)}`];
|
||||
const containerSize = this.$refs.navScroll[`offset${firstUpperCase(sizeName)}`];
|
||||
const currentOffset = this.navOffset;
|
||||
|
||||
if (containerSize < navSize) {
|
||||
const currentOffset = this.navOffset;
|
||||
this.scrollable = this.scrollable || {};
|
||||
this.scrollable.prev = currentOffset;
|
||||
this.scrollable.next = currentOffset + containerSize < navSize;
|
||||
if (navSize - currentOffset < containerSize) {
|
||||
this.navOffset = navSize - containerSize;
|
||||
}
|
||||
} else {
|
||||
this.scrollable = false;
|
||||
if (currentOffset > 0) {
|
||||
this.navOffset = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
changeTab(e) {
|
||||
const keyCode = e.keyCode;
|
||||
let nextIndex;
|
||||
let currentIndex, tabList;
|
||||
if ([37, 38, 39, 40].indexOf(keyCode) !== -1) { // 左右上下键更换tab
|
||||
tabList = e.currentTarget.querySelectorAll('[role=tab]');
|
||||
currentIndex = Array.prototype.indexOf.call(tabList, e.target);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (keyCode === 37 || keyCode === 38) { // left
|
||||
if (currentIndex === 0) { // first
|
||||
nextIndex = tabList.length - 1;
|
||||
} else {
|
||||
nextIndex = currentIndex - 1;
|
||||
}
|
||||
} else { // right
|
||||
if (currentIndex < tabList.length - 1) { // not last
|
||||
nextIndex = currentIndex + 1;
|
||||
} else {
|
||||
nextIndex = 0;
|
||||
}
|
||||
}
|
||||
tabList[nextIndex].focus(); // 改变焦点元素
|
||||
tabList[nextIndex].click(); // 选中下一个tab
|
||||
this.setFocus();
|
||||
},
|
||||
setFocus() {
|
||||
if (this.focusable) {
|
||||
this.isFocus = true;
|
||||
}
|
||||
},
|
||||
removeFocus() {
|
||||
this.isFocus = false;
|
||||
},
|
||||
visibilityChangeHandler() {
|
||||
const visibility = document.visibilityState;
|
||||
if (visibility === 'hidden') {
|
||||
this.focusable = false;
|
||||
} else if (visibility === 'visible') {
|
||||
setTimeout(() => {
|
||||
this.focusable = true;
|
||||
}, 50);
|
||||
}
|
||||
},
|
||||
windowBlurHandler() {
|
||||
this.focusable = false;
|
||||
},
|
||||
windowFocusHandler() {
|
||||
setTimeout(() => {
|
||||
this.focusable = true;
|
||||
}, 50);
|
||||
}
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.update();
|
||||
},
|
||||
|
||||
render(h) {
|
||||
const {
|
||||
type,
|
||||
panes,
|
||||
editable,
|
||||
stretch,
|
||||
onTabClick,
|
||||
onTabRemove,
|
||||
navStyle,
|
||||
scrollable,
|
||||
scrollNext,
|
||||
scrollPrev,
|
||||
changeTab,
|
||||
setFocus,
|
||||
removeFocus
|
||||
} = this;
|
||||
const scrollBtn = scrollable
|
||||
? [
|
||||
<span class={['el-tabs__nav-prev', scrollable.prev ? '' : 'is-disabled']} on-click={scrollPrev}><i class="el-icon-arrow-left"></i></span>,
|
||||
<span class={['el-tabs__nav-next', scrollable.next ? '' : 'is-disabled']} on-click={scrollNext}><i class="el-icon-arrow-right"></i></span>
|
||||
] : null;
|
||||
|
||||
const tabs = this._l(panes, (pane, index) => {
|
||||
let tabName = pane.name || pane.index || index;
|
||||
const closable = pane.isClosable || editable;
|
||||
|
||||
pane.index = `${index}`;
|
||||
|
||||
const btnClose = closable
|
||||
? <span class="el-icon-close" on-click={(ev) => { onTabRemove(pane, ev); }}></span>
|
||||
: null;
|
||||
|
||||
const tabLabelContent = pane.$slots.label || pane.label;
|
||||
const tabindex = pane.active ? 0 : -1;
|
||||
return (
|
||||
<div
|
||||
class={{
|
||||
'el-tabs__item': true,
|
||||
[`is-${ this.rootTabs.tabPosition }`]: true,
|
||||
'is-active': pane.active,
|
||||
'is-disabled': pane.disabled,
|
||||
'is-closable': closable,
|
||||
'is-focus': this.isFocus
|
||||
}}
|
||||
id={`tab-${tabName}`}
|
||||
key={`tab-${tabName}`}
|
||||
aria-controls={`pane-${tabName}`}
|
||||
role="tab"
|
||||
aria-selected={ pane.active }
|
||||
ref="tabs"
|
||||
tabindex={tabindex}
|
||||
refInFor
|
||||
on-focus={ ()=> { setFocus(); }}
|
||||
on-blur ={ ()=> { removeFocus(); }}
|
||||
on-click={(ev) => { removeFocus(); onTabClick(pane, tabName, ev); }}
|
||||
on-keydown={(ev) => { if (closable && (ev.keyCode === 46 || ev.keyCode === 8)) { onTabRemove(pane, ev);} }}
|
||||
>
|
||||
{tabLabelContent}
|
||||
{btnClose}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div class={['el-tabs__nav-wrap', scrollable ? 'is-scrollable' : '', `is-${ this.rootTabs.tabPosition }`]}>
|
||||
{scrollBtn}
|
||||
<div class={['el-tabs__nav-scroll']} ref="navScroll">
|
||||
<div
|
||||
class={['el-tabs__nav', `is-${ this.rootTabs.tabPosition }`, stretch && ['top', 'bottom'].indexOf(this.rootTabs.tabPosition) !== -1 ? 'is-stretch' : '']}
|
||||
ref="nav"
|
||||
style={navStyle}
|
||||
role="tablist"
|
||||
on-keydown={ changeTab }
|
||||
>
|
||||
{!type ? <tab-bar tabs={panes}></tab-bar> : null}
|
||||
{tabs}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
addResizeListener(this.$el, this.update);
|
||||
document.addEventListener('visibilitychange', this.visibilityChangeHandler);
|
||||
window.addEventListener('blur', this.windowBlurHandler);
|
||||
window.addEventListener('focus', this.windowFocusHandler);
|
||||
setTimeout(() => {
|
||||
this.scrollToActiveTab();
|
||||
}, 0);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if (this.$el && this.update) removeResizeListener(this.$el, this.update);
|
||||
document.removeEventListener('visibilitychange', this.visibilityChangeHandler);
|
||||
window.removeEventListener('blur', this.windowBlurHandler);
|
||||
window.removeEventListener('focus', this.windowFocusHandler);
|
||||
}
|
||||
};
|
||||
</script>
|
56
packages/tabs/src/tab-pane.vue
Normal file
56
packages/tabs/src/tab-pane.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div
|
||||
class="el-tab-pane"
|
||||
v-if="(!lazy || loaded) || active"
|
||||
v-show="active"
|
||||
role="tabpanel"
|
||||
:aria-hidden="!active"
|
||||
:id="`pane-${paneName}`"
|
||||
:aria-labelledby="`tab-${paneName}`"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'ElTabPane',
|
||||
|
||||
componentName: 'ElTabPane',
|
||||
|
||||
props: {
|
||||
label: String,
|
||||
labelContent: Function,
|
||||
name: String,
|
||||
closable: Boolean,
|
||||
disabled: Boolean,
|
||||
lazy: Boolean
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
index: null,
|
||||
loaded: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isClosable() {
|
||||
return this.closable || this.$parent.closable;
|
||||
},
|
||||
active() {
|
||||
const active = this.$parent.currentName === (this.name || this.index);
|
||||
if (active) {
|
||||
this.loaded = true;
|
||||
}
|
||||
return active;
|
||||
},
|
||||
paneName() {
|
||||
return this.name || this.index;
|
||||
}
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.$parent.$emit('tab-nav-update');
|
||||
}
|
||||
};
|
||||
</script>
|
191
packages/tabs/src/tabs.vue
Normal file
191
packages/tabs/src/tabs.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<script>
|
||||
import TabNav from './tab-nav';
|
||||
|
||||
export default {
|
||||
name: 'ElTabs',
|
||||
|
||||
components: {
|
||||
TabNav
|
||||
},
|
||||
|
||||
props: {
|
||||
type: String,
|
||||
activeName: String,
|
||||
closable: Boolean,
|
||||
addable: Boolean,
|
||||
value: {},
|
||||
editable: Boolean,
|
||||
tabPosition: {
|
||||
type: String,
|
||||
default: 'top'
|
||||
},
|
||||
beforeLeave: Function,
|
||||
stretch: Boolean
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
rootTabs: this
|
||||
};
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
currentName: this.value || this.activeName,
|
||||
panes: []
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
activeName(value) {
|
||||
this.setCurrentName(value);
|
||||
},
|
||||
value(value) {
|
||||
this.setCurrentName(value);
|
||||
},
|
||||
currentName(value) {
|
||||
if (this.$refs.nav) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.nav.$nextTick(_ => {
|
||||
this.$refs.nav.scrollToActiveTab();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
calcPaneInstances(isForceUpdate = false) {
|
||||
if (this.$slots.default) {
|
||||
const paneSlots = this.$slots.default.filter(vnode => vnode.tag &&
|
||||
vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'ElTabPane');
|
||||
// update indeed
|
||||
const panes = paneSlots.map(({ componentInstance }) => componentInstance);
|
||||
const panesChanged = !(panes.length === this.panes.length && panes.every((pane, index) => pane === this.panes[index]));
|
||||
if (isForceUpdate || panesChanged) {
|
||||
this.panes = panes;
|
||||
}
|
||||
} else if (this.panes.length !== 0) {
|
||||
this.panes = [];
|
||||
}
|
||||
},
|
||||
handleTabClick(tab, tabName, event) {
|
||||
if (tab.disabled) return;
|
||||
this.setCurrentName(tabName);
|
||||
this.$emit('tab-click', tab, event);
|
||||
},
|
||||
handleTabRemove(pane, ev) {
|
||||
if (pane.disabled) return;
|
||||
ev.stopPropagation();
|
||||
this.$emit('edit', pane.name, 'remove');
|
||||
this.$emit('tab-remove', pane.name);
|
||||
},
|
||||
handleTabAdd() {
|
||||
this.$emit('edit', null, 'add');
|
||||
this.$emit('tab-add');
|
||||
},
|
||||
setCurrentName(value) {
|
||||
const changeCurrentName = () => {
|
||||
this.currentName = value;
|
||||
this.$emit('input', value);
|
||||
};
|
||||
if (this.currentName !== value && this.beforeLeave) {
|
||||
const before = this.beforeLeave(value, this.currentName);
|
||||
if (before && before.then) {
|
||||
before
|
||||
.then(() => {
|
||||
changeCurrentName();
|
||||
this.$refs.nav && this.$refs.nav.removeFocus();
|
||||
}, () => {
|
||||
// https://github.com/ElemeFE/element/pull/14816
|
||||
// ignore promise rejection in `before-leave` hook
|
||||
});
|
||||
} else if (before !== false) {
|
||||
changeCurrentName();
|
||||
}
|
||||
} else {
|
||||
changeCurrentName();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render(h) {
|
||||
let {
|
||||
type,
|
||||
handleTabClick,
|
||||
handleTabRemove,
|
||||
handleTabAdd,
|
||||
currentName,
|
||||
panes,
|
||||
editable,
|
||||
addable,
|
||||
tabPosition,
|
||||
stretch
|
||||
} = this;
|
||||
|
||||
const newButton = editable || addable
|
||||
? (
|
||||
<span
|
||||
class="el-tabs__new-tab"
|
||||
on-click={ handleTabAdd }
|
||||
tabindex="0"
|
||||
on-keydown={ (ev) => { if (ev.keyCode === 13) { handleTabAdd(); }} }
|
||||
>
|
||||
<i class="el-icon-plus"></i>
|
||||
</span>
|
||||
)
|
||||
: null;
|
||||
|
||||
const navData = {
|
||||
props: {
|
||||
currentName,
|
||||
onTabClick: handleTabClick,
|
||||
onTabRemove: handleTabRemove,
|
||||
editable,
|
||||
type,
|
||||
panes,
|
||||
stretch
|
||||
},
|
||||
ref: 'nav'
|
||||
};
|
||||
const header = (
|
||||
<div class={['el-tabs__header', `is-${tabPosition}`]}>
|
||||
{newButton}
|
||||
<tab-nav { ...navData }></tab-nav>
|
||||
</div>
|
||||
);
|
||||
const panels = (
|
||||
<div class="el-tabs__content">
|
||||
{this.$slots.default}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<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] }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
created() {
|
||||
if (!this.currentName) {
|
||||
this.setCurrentName('0');
|
||||
}
|
||||
|
||||
this.$on('tab-nav-update', this.calcPaneInstances.bind(null, true));
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.calcPaneInstances();
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.calcPaneInstances();
|
||||
}
|
||||
};
|
||||
</script>
|
Reference in New Issue
Block a user