完善tabs组件

master
yutent 2023-04-14 19:08:29 +08:00
parent 341f096b76
commit b0196b23d9
1 changed files with 310 additions and 38 deletions

View File

@ -4,7 +4,15 @@
* @date 2023/03/06 15:17:25
*/
import { css, html, bind, Component, nextTick } from '@bd/core'
import {
css,
html,
bind,
Component,
nextTick,
styleMap,
classMap
} from '@bd/core'
class Tabs extends Component {
static props = {
@ -18,51 +26,263 @@ class Tabs extends Component {
css`
:host {
display: flex;
font-size: 14px;
}
:host([tab-position='top']),
:host([tab-position='bottom']) {
:host([tab-position='top']) {
flex-direction: column;
}
:host([tab-position='left']),
:host([tab-position='bottom']) {
flex-direction: column-reverse;
}
:host([tab-position='right']) {
flex-direction: row-reverse;
}
.ell {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.header {
position: relative;
display: flex;
width: 100%;
height: 38px;
color: var(--color-dark-1);
user-select: none;
}
.content {
padding: 16px 0;
}
.label {
position: relative;
display: inline-flex;
align-items: center;
max-width: 120px;
margin: 0 16px;
--size: 16px;
cursor: pointer;
[slot='label'] {
display: inline-flex;
align-items: center;
}
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
&:hover,
&.active {
color: var(--color-teal-1);
}
}
`,
// type default
css`
.header {
user-select: none;
:host([type='default']) {
.header {
border-bottom: 2px solid var(--color-plain-3);
}
.active-bar {
position: absolute;
left: 0;
bottom: -2px;
height: 2px;
background: var(--color-teal-1);
}
.active-bar {
transition: width 0.2s ease-out, left 0.2s ease-out, top 0.2s ease-out;
}
&:host([tab-position='bottom']) {
.header {
border-top: 2px solid var(--color-plain-3);
border-bottom: 0;
}
.active-bar {
top: -2px;
bottom: auto;
}
}
&:host([tab-position='left']),
&:host([tab-position='right']) {
.header {
flex-direction: column;
width: auto;
height: auto;
max-width: 120px;
border-bottom: 0;
}
.content {
padding: 0 16px;
}
.label {
margin: 0;
height: 38px;
padding: 0 16px;
}
.active-bar {
width: 2px;
height: 38px;
bottom: auto;
}
}
&:host([tab-position='left']) {
.header {
border-right: 2px solid var(--color-plain-3);
}
.label {
justify-content: flex-end;
}
.active-bar {
left: unset;
right: -2px;
}
}
&:host([tab-position='right']) {
.header {
border-left: 2px solid var(--color-plain-3);
}
.active-bar {
left: -2px;
}
}
}
`,
// type folder && card
css`
:host([type='folder']),
:host([type='card']) {
.content {
padding: 16px;
}
.active-bar {
display: none;
}
.label {
margin: 0;
padding: 0 16px;
background: var(--color-plain-1);
}
}
:host([type='folder']) {
.header {
overflow: hidden;
padding-left: 6px;
}
.content {
background: var(--tab-folder-color, #fff);
box-shadow: 0 0 6px rgba(0, 0, 0, 0.1);
}
.label {
padding-top: 2px;
background: none;
> span,
> [slot='label'] {
position: relative;
z-index: 1;
}
&::before {
position: absolute;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 36px;
background: var(--color-plain-3);
border-radius: 6px 6px 0 0;
transform: perspective(9px) rotateX(3deg);
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
content: '';
}
&.active::before {
z-index: 0;
background: #fff;
}
}
&:host([tab-position='bottom']) {
.label {
padding-top: 0;
&::before {
border-radius: 0 0 6px 6px;
transform: perspective(9px) rotateX(-3deg);
box-shadow: 0 -1px 6px rgba(0, 0, 0, 0.1);
}
}
}
}
:host([type='card']) {
border: 1px solid var(--color-plain-3);
background: #fff;
.header {
background: var(--color-plain-1);
box-shadow: inset 0 -1px 0 0px var(--color-plain-3);
}
.label {
border-right: 1px solid var(--color-plain-3);
border-bottom: 1px solid var(--color-plain-3);
&.active {
border-bottom-color: transparent;
background: #fff;
}
}
}
`
]
#cache = {}
render() {
return html`
<div class="tabs">
<header class="header" ref="tabs" @click=${this.selectTab}>
${this.labels.map(
(it, i) =>
html`<label class="label" data-key=${it.name}>${it.label}</label>`
)}
</header>
<div class="content">
<slot></slot>
</div>
</div>
`
}
#tabWidth = 0 // 选项卡的宽度
#tabLeft = 0 // 选项卡的左侧距离
#tabTop = 0 // 选项卡的顶部距离
// 处理子组件的slot,穿透到父组件来
#parseSlot() {
let left = 0
let gaps = this.type === 'default' ? 32 : 0
let top = 0
this.labels.forEach((it, i) => {
let tab = this.$refs.tabs.children[i]
if (tab.lastElementChild) {
tab.replaceChild(it.el, tab.lastElementChild)
} else {
tab.append(it.el)
let nav = this.$refs.navs.children[i]
if (it.el) {
nav.replaceChild(it.el, nav.firstElementChild)
}
if (this.tab === it.name) {
if (gaps) {
this.#tabWidth = nav.clientWidth
this.#tabLeft = left
this.#tabTop = top
}
this.$requestUpdate()
it.panel.$animate()
}
top += 38
left += nav.clientWidth + gaps
delete it.el
})
}
@ -70,6 +290,9 @@ class Tabs extends Component {
selectTab(ev) {
let elem = ev.target
let key
let left = 0
let gaps = this.type === 'default' ? 32 : 0
let top = 0
if (elem === ev.currentTarget) {
return
}
@ -78,15 +301,31 @@ class Tabs extends Component {
elem = elem.parentNode
}
key = elem.dataset.key
key = this.labels[+elem.dataset.i].name
if (key === this.tab) {
return
}
this.#cache[this.tab].tab
// 非默认类型, 不显示active-bar
if (gaps) {
this.#tabWidth = elem.clientWidth
for (let i = -1, it; (it = this.labels[++i]); ) {
let nav = this.$refs.navs.children[i]
if (key === it.name) {
this.#tabLeft = left
this.#tabTop = top
break
}
top += 38
left += nav.clientWidth + gaps
}
}
this.#cache[this.tab].panel
.$animate(true)
.then(_ => this.#cache[key].tab.$animate())
.then(_ => this.#cache[key].panel.$animate())
this.tab = key
}
@ -98,9 +337,11 @@ class Tabs extends Component {
this.labels = children.map((it, i) => {
let tmp = {
label: it.label,
name: it.name || i,
name: (it.name || i).toString(),
el: null,
tab: null
panel: null,
disabled: it.disabled,
closable: it.closable
}
this.#cache[tmp.name] = tmp
return tmp
@ -112,21 +353,52 @@ class Tabs extends Component {
this.labels[i].el = slot.cloneNode(true)
slot.remove()
}
this.labels[i].tab = it
this.labels[i].panel = it
})
nextTick(_ => this.#parseSlot())
this.tab = this.tab || this.labels[0].name
if (!this.tab) {
this.tab = this.labels[0].name
this.labels[0].tab.$animate()
}
nextTick(_ => this.#parseSlot())
})
}
mounted() {
//
}
render() {
let tabStyle = {}
if (this['tab-position'] === 'top' || this['tab-position'] === 'bottom') {
tabStyle = {
width: this.#tabWidth + 'px',
left: this.#tabLeft + 'px'
}
} else {
tabStyle = { top: this.#tabTop + 'px' }
}
return html`
<header class="header" ref="navs" @click=${this.selectTab}>
${this.labels.map(
(it, i) =>
html`<label
class=${classMap({
label: true,
active: it.name === this.tab
})}
title=${it.label}
data-i=${i}
>
<span class="ell">${it.label}</span>
</label>`
)}
<span class="active-bar" style=${styleMap(tabStyle)}></span>
</header>
<div class="content">
<slot></slot>
</div>
`
}
}
class Tab extends Component {