1/* 2 * Copyright (C) 2022 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import {element} from "../BaseElement.js"; 17import {LitTabpane} from "./lit-tabpane.js"; 18 19@element('lit-tabs') 20export class LitTabs extends HTMLElement { 21 private tabPos: any 22 private nav: HTMLDivElement | undefined | null 23 private line: HTMLDivElement | undefined | null 24 private slots: HTMLSlotElement | undefined | null 25 26 constructor() { 27 super(); 28 const shadowRoot = this.attachShadow({mode: 'open'}); 29 shadowRoot.innerHTML = ` 30 <style> 31 :host{ 32 display: block; 33 text-align: unset; 34 color: var(--dark-color1,#252525); 35 background-color: var(--dark-background,#FFFFFF); 36 box-shadow: #00000033 0 0 10px ; 37 } 38 ::slotted(lit-tabpane){ 39 box-sizing:border-box; 40 width:100%; 41 height:100%; 42 flex-shrink:0; 43 overflow:auto; 44 } 45 .nav-item{ 46 display: inline-flex; 47 justify-content: center; 48 align-items: center; 49 /*padding: 6px 0px 6px 12px;*/ 50 padding-left: 12px; 51 font-size: .9rem; 52 font-weight: normal; 53 cursor: pointer; 54 transition: all 0.3s; 55 flex-shrink: 0; 56 } 57 .nav-item lit-icon{ 58 margin-right: 2px; 59 font-size: inherit; 60 } 61 62 .nav-item[data-disabled]{ 63 pointer-events: all; 64 cursor: not-allowed; 65 color: #bfbfbf; 66 } 67 .nav-item[data-hidden]{ 68 pointer-events: all; 69 cursor: not-allowed; 70 color: #bfbfbf; 71 display: none; 72 } 73 74 .tab-content{ 75 display: block; 76 background-color: #fff; 77 flex:1; 78 } 79 80 /* 81 * top top-left top-center top-right 82 */ 83 :host(:not([position])) .nav-root, 84 :host([position^='top']) .nav-root{ 85 display: flex; 86 position: relative; 87 height: 38px; 88 /*justify-content: center;*/ 89 /*align-items: center;*/ 90 } 91 :host(:not([mode]):not([position])) .tab-line,/*移动的线条*/ 92 :host([mode='flat'][position^='top']) .tab-line{ 93 position:absolute; 94 } 95 96 :host(:not([position])) .tab-nav-container, 97 :host([position^='top']) .tab-nav-container{ 98 display: flex; 99 /*position: relative;*/ 100 /*flex-direction: column;*/ 101 /*overflow-y: hidden;*/ 102 /*overflow-x: auto;*/ 103 /*overflow: -moz-scrollbars-none; */ 104 /*-ms-overflow-style: none;*/ 105 /*transition: all 0.3s;*/ 106 107 position: absolute; 108 overflow: auto; 109 height: 850px; 110 transform: rotateZ(-90deg) rotateY(180deg); 111 transform-origin: left top; 112 overflow-x: hidden; 113 width: 38px; 114 115 cursor: row-resize; 116 user-select: none; 117 } 118 :host([position='top']) .tab-nav, 119 :host([position='top-left']) .tab-nav{ 120 display: flex; 121 position: relative; 122 justify-content: flex-start; 123 cursor: row-resize; 124 user-select: none; 125 margin-top: 6px; 126 margin-left: 5px; 127 128 transform: translateY(-38px) rotateZ(90deg) rotateX(180deg) translateY(38px); 129 transform-origin: left bottom; 130 flex-wrap: nowrap; 131 height: 38px; 132 } 133 :host([position='top-center']) .tab-nav{ 134 display: flex; 135 justify-content: center; 136 } 137 :host([position='top-right']) .tab-nav{ 138 display: flex; 139 justify-content: flex-end; 140 } 141 142 :host([position^='top'][mode='card']) .nav-item{ 143 border-top: 1px solid var(--dark-border1,#f0f0f0); 144 border-left: 1px solid var(--dark-border1,#f0f0f0); 145 border-right: 1px solid var(--dark-border1,#f0f0f0); 146 bottom: 0px; 147 margin-right: 2px; 148 position: relative; 149 height: 100%; 150 } 151 :host([position^='top']) .tab-nav-bg-line{ 152 position: absolute;bottom: 0;height: 1px; 153 width: 100% 154 } 155 :host([position^='top'][mode='card']) .nav-item:not([data-selected]){ 156 border-top-left-radius: 5px; 157 border-top-right-radius: 5px; 158 background-color: var(--dark-border1,#D8D8D8); 159 color: var(--dark-color1,#212121); 160 } 161 :host([position^='top'][mode='card']) .nav-item[data-selected]{ 162 background-color: var(--dark-background,#ffffff); 163 bottom: 0px; 164 color: var(--dark-color1,#212121); 165 border-top: 1px solid var(--dark-border1,#bababa); 166 border-top-left-radius: 5px; 167 border-top-right-radius: 5px; 168 border-left: 1px solid var(--dark-border1,#bababa); 169 border-right: 1px solid var(--dark-border1,#bababa); 170 } 171 /* 172 bottom bottom-left bottom-center bottom-right 173 */ 174 :host([position^='bottom']) .tab{ 175 display: flex; 176 flex-direction: column-reverse; 177 } 178 :host([mode='flat'][position^='bottom']) .tab-line{ 179 position:absolute; 180 top: -3px; 181 background-color: #42b983; 182 height: 2px; 183 transform: translateY(-100%); 184 transition: all 0.3s; 185 } 186 :host([position^='bottom']) .tab-nav-container{ 187 display: flex; 188 position: relative; 189 flex-direction: column; 190 overflow-x: auto; 191 overflow-y: visible; 192 overflow: -moz-scrollbars-none; 193 -ms-overflow-style: none; 194 transition: all 0.3s; 195 flex: 1; 196 border-top: #f0f0f0 1px solid; 197 } 198 :host([position^='bottom']) .nav-root{ 199 display: flex; 200 justify-content: center; 201 align-items: center; 202 } 203 :host([position='bottom']) .tab-nav, 204 :host([position='bottom-left']) .tab-nav{ 205 display: flex; 206 position: relative; 207 justify-content: flex-start; 208 } 209 :host([position='bottom-center']) .tab-nav{ 210 display: flex; 211 justify-content: center; 212 } 213 :host([position='bottom-right']) .tab-nav{ 214 display: flex; 215 justify-content: flex-end; 216 } 217 :host([position^='bottom'][mode='card']) .nav-item{ 218 border-top: 1px solid #ffffff; 219 border-left: 1px solid #f0f0f0; 220 border-right: 1px solid #f0f0f0; 221 border-bottom: 1px solid #f0f0f0; 222 top: -1px; 223 margin-right: 2px; 224 position: relative; 225 } 226 :host([position^='bottom']) .tab-nav-bg-line{ 227 position: absolute;top: 0;height: 1px;background-color: #f0f0f0;width: 100% 228 } 229 :host([position^='bottom'][mode='card']) .nav-item:not([data-selected]){ 230 background-color: #f5f5f5; 231 border-top: 1px solid #f0f0f0; 232 } 233 :host([position^='bottom'][mode='card']) .nav-item[data-selected]{ 234 background-color: #ffffff; 235 border-top: 1px solid #fff; 236 top: -1px; 237 } 238 /* 239 left left-top left-center left-bottom 240 */ 241 :host([position^='left']) .tab{ 242 display: flex; 243 flex-direction: row; 244 } 245 :host([mode='flat'][position^='left']) .tab-line{ 246 position:absolute; 247 right: 1px; 248 background-color: #42b983; 249 width: 3px; 250 transform: translateX(100%); 251 transition: all 0.3s; 252 } 253 :host([position^='left']) .tab-nav-container{ 254 display: flex; 255 position: relative; 256 flex-direction: row; 257 overflow-x: auto; 258 overflow-y: visible; 259 overflow: -moz-scrollbars-none; 260 -ms-overflow-style: none; 261 transition: all 0.3s; 262 flex: 1; 263 border-right: #f0f0f0 1px solid; 264 } 265 :host([position^='left']) .nav-root{ 266 display: flex; 267 flex-direction: column; 268 justify-content: center; 269 align-items: center; 270 } 271 :host([position='left']) .tab-nav, 272 :host([position='left-top']) .tab-nav{ 273 display: flex; 274 position: relative; 275 flex-direction: column; 276 justify-content: flex-start; 277 } 278 :host([position='left-center']) .tab-nav{ 279 display: flex; 280 position: relative; 281 flex-direction: column; 282 justify-content: center; 283 } 284 :host([position='left-bottom']) .tab-nav{ 285 display: flex; 286 position: relative; 287 flex-direction: column; 288 justify-content: flex-end; 289 } 290 :host([position^='left'][mode='card']) .nav-item{ 291 border-top: 1px solid #f0f0f0; 292 border-left: 1px solid #f0f0f0; 293 border-right: 1px solid #ffffff; 294 border-bottom: 1px solid #f0f0f0; 295 right: -1px; 296 margin-bottom: 2px; 297 position: relative; 298 } 299 :host([position^='left']) .tab-nav-bg-line{ 300 position: absolute;right: 0;width: 1px;background-color: #f0f0f0;width: 100% 301 } 302 :host([position^='left'][mode='card']) .nav-item:not([data-selected]){ 303 background-color: #f5f5f5; 304 border-right: 1px solid #f0f0f0; 305 } 306 :host([position^='left'][mode='card']) .nav-item[data-selected]{ 307 background-color: #ffffff; 308 border-bottom: 1px solid #fff; 309 right: -1px; 310 } 311 /* 312 right right-top right-center right-bottom 313 */ 314 :host([position^='right']) .tab{ 315 display: flex; 316 flex-direction: row-reverse; 317 } 318 :host([mode='flat'][position^='right']) .tab-line{ 319 position:absolute; 320 left: 1px; 321 background-color: #42b983; 322 width: 3px; 323 transform: translateX(-100%); 324 transition: all 0.3s; 325 } 326 :host([position^='right']) .tab-nav-container{ 327 display: flex; 328 position: relative; 329 flex-direction: row-reverse; 330 overflow-x: auto; 331 overflow-y: visible; 332 overflow: -moz-scrollbars-none; 333 -ms-overflow-style: none; 334 transition: all 0.3s; 335 flex: 1; 336 border-left: #f0f0f0 1px solid; 337 } 338 :host([position^='right']) .nav-root{ 339 display: flex; 340 flex-direction: column; 341 justify-content: center; 342 align-items: center; 343 } 344 :host([position='right']) .tab-nav, 345 :host([position='right-top']) .tab-nav{ 346 display: flex; 347 position: relative; 348 flex-direction: column; 349 justify-content: flex-start; 350 } 351 :host([position='right-center']) .tab-nav{ 352 display: flex; 353 position: relative; 354 flex-direction: column; 355 justify-content: center; 356 } 357 :host([position='right-bottom']) .tab-nav{ 358 display: flex; 359 position: relative; 360 flex-direction: column; 361 justify-content: flex-end; 362 } 363 :host([position^='right'][mode='card']) .nav-item{ 364 border-top: 1px solid #f0f0f0; 365 border-left: 1px solid #ffffff; 366 border-right: 1px solid #f0f0f0; 367 border-bottom: 1px solid #f0f0f0; 368 left: -1px; 369 margin-top: 2px; 370 position: relative; 371 } 372 :host([position^='right']) .tab-nav-bg-line{ 373 position: absolute;left: 0;width: 1px;background-color: #f0f0f0;width: 100% 374 } 375 :host([position^='right'][mode='card']) .nav-item:not([data-selected]){ 376 background-color: #f5f5f5; 377 border-left: 1px solid #f0f0f0; 378 } 379 :host([position^='right'][mode='card']) .nav-item[data-selected]{ 380 background-color: #ffffff; 381 left: -1px; 382 } 383 384 385 .tab-nav-container::-webkit-scrollbar { 386 display: none; 387 } 388 389 .close-icon:hover{ 390 color: #000; 391 } 392 .nav-item[data-closeable] .close-icon{ 393 display: block; 394 padding: 2px 5px; 395 color: var(--dark-icon,#606060) 396 } 397 .nav-item[data-closeable] .no-close-icon{ 398 display: none; 399 } 400 .nav-item:not([data-closeable]) .no-close-icon{ 401 display: block; 402 } 403 .nav-item:not([data-closeable]) .close-icon{ 404 display: none; 405 } 406 407 </style> 408 <style id="filter"></style> 409 <div class="tab" > 410 <div class="nav-root" style="background-color: var(--dark-background4,#f2f2f2);"> 411 <slot name="left" style="flex:1"></slot> 412 <div class="tab-nav-container" > 413 <div class="tab-nav-bg-line"></div> 414 <div class="tab-nav" id="nav" ></div> 415 <div class="tab-line" id="tab-line"></div> 416 </div> 417 <div id="tab-filling" style="flex: 1"></div> 418 <slot name="right" style="flex:1"></slot> 419 </div> 420 <div class="tab-content"> 421 <slot id="slot">NEED CONTENT</slot> 422 </div> 423 </div> 424 ` 425 } 426 427 static get observedAttributes() { 428 return ['activekey', 'mode', 'position'] 429 } 430 431 get position() { 432 return this.getAttribute('position') || 'top'; 433 } 434 435 set position(value) { 436 this.setAttribute('position', value); 437 } 438 439 get mode() { 440 return this.getAttribute('mode') || 'flat'; 441 } 442 443 set mode(value) { 444 this.setAttribute('mode', value); 445 } 446 447 get activekey() { 448 return this.getAttribute("activekey") || ''; 449 } 450 451 set activekey(value: string) { 452 this.setAttribute('activekey', value); 453 } 454 455 set onTabClick(fn: any) { 456 this.addEventListener('onTabClick', fn); 457 } 458 459 updateLabel(key: string, value: string) { 460 if (this.nav) { 461 let item = this.nav.querySelector(`.nav-item[data-key='${key}']`); 462 if (item) { 463 item.querySelector<HTMLSpanElement>("span")!.innerHTML = value; 464 this.initTabPos() 465 } 466 } 467 } 468 469 updateDisabled(key: string, value: string) { 470 if (this.nav) { 471 let item = this.nav.querySelector(`.nav-item[data-key='${key}']`); 472 if (item) { 473 if (value) { 474 item.setAttribute('data-disabled', '') 475 } else { 476 item.removeAttribute('data-disabled'); 477 } 478 this.initTabPos() 479 } 480 } 481 } 482 483 updateCloseable(key: string, value: string) { 484 if (this.nav) { 485 let item = this.nav.querySelector(`.nav-item[data-key='${key}']`); 486 if (item) { 487 if (value) { 488 item.setAttribute('data-closeable', '') 489 } else { 490 item.removeAttribute('data-closeable'); 491 } 492 this.initTabPos() 493 } 494 } 495 } 496 497 updateHidden(key: string, value: string) { 498 if (this.nav) { 499 let item = this.nav.querySelector(`.nav-item[data-key='${key}']`); 500 if (item) { 501 if (value === "true") { 502 item.setAttribute('data-hidden', '') 503 } else { 504 item.removeAttribute('data-hidden'); 505 } 506 this.initTabPos() 507 } 508 } 509 } 510 511 initTabPos() { 512 const items = this.nav!.querySelectorAll<HTMLDivElement>(".nav-item"); 513 Array.from(items).forEach((a, index) => { 514 // @ts-ignore 515 this.tabPos[a.dataset.key] = { 516 index: index, 517 width: a.offsetWidth, 518 height: a.offsetHeight, 519 left: a.offsetLeft, 520 top: a.offsetTop, 521 label: a.textContent 522 } 523 }) 524 if (this.activekey) { 525 if (this.position.startsWith('left')) { 526 this.line?.setAttribute('style', `height:${this.tabPos[this.activekey].height}px;transform:translate(100%,${this.tabPos[this.activekey].top}px)`); 527 } else if (this.position.startsWith('top')) { 528 if (this.tabPos[this.activekey]) { 529 this.line?.setAttribute('style', `width:${this.tabPos[this.activekey].width}px;transform:translate(${this.tabPos[this.activekey].left}px,100%)`); 530 } 531 } else if (this.position.startsWith('right')) { 532 this.line?.setAttribute('style', `height:${this.tabPos[this.activekey].height}px;transform:translate(-100%,${this.tabPos[this.activekey].top}px)`); 533 } else if (this.position.startsWith('bottom')) { 534 this.line?.setAttribute('style', `width:${this.tabPos[this.activekey].width}px;transform:translate(${this.tabPos[this.activekey].left}px,100%)`); 535 } 536 } 537 } 538 539 connectedCallback() { 540 let that = this; 541 this.tabPos = {} 542 this.nav = this.shadowRoot?.querySelector('#nav') 543 this.line = this.shadowRoot?.querySelector('#tab-line') 544 this.slots = this.shadowRoot?.querySelector('#slot'); 545 this.slots?.addEventListener('slotchange', () => { 546 const elements: Element[] | undefined = this.slots?.assignedElements(); 547 let panes = this.querySelectorAll<LitTabpane>('lit-tabpane'); 548 if (this.activekey) { 549 panes.forEach(a => { 550 if (a.key === this.activekey) { 551 a.style.display = 'block' 552 } else { 553 a.style.display = 'none'; 554 } 555 }) 556 } else { 557 panes.forEach((a, index) => { 558 if (index === 0) { 559 a.style.display = 'block' 560 this.activekey = a.key || '' 561 } else { 562 a.style.display = 'none'; 563 } 564 }) 565 } 566 let navHtml = ""; 567 elements?.map(it => it as LitTabpane).forEach(a => { 568 if (a.disabled) { 569 navHtml += `<div class="nav-item" data-key="${a.key}" data-disabled ${a.closeable ? 'data-closeable' : ''}> 570 ${a.icon ? `<lit-icon name='${a.icon}'></lit-icon>` : ``} 571 <span>${a.tab}</span> 572 <lit-icon class="close-icon" name='close' size="12"></lit-icon><div class="no-close-icon" style="margin-right: 12px"></div> 573 </div>`; 574 } else if (a.hidden) { 575 navHtml += `<div class="nav-item" data-key="${a.key}" data-hidden ${a.closeable ? 'data-closeable' : ''}> 576 ${a.icon ? `<lit-icon name='${a.icon}'></lit-icon>` : ``} 577 <span>${a.tab}</span> 578 <lit-icon class="close-icon" name='close' size="12"></lit-icon><div class="no-close-icon" style="margin-right: 12px"></div> 579 </div>`; 580 } else { 581 if (a.key === this.activekey) { 582 navHtml += `<div class="nav-item" data-key="${a.key}" data-selected ${a.closeable ? 'data-closeable' : ''}> 583 ${a.icon ? `<lit-icon name='${a.icon}'></lit-icon>` : ``} 584 <span>${a.tab}</span> 585 <lit-icon class="close-icon" name='close' size="12"></lit-icon><div class="no-close-icon" style="margin-right: 12px"></div> 586 </div>`; 587 } else { 588 navHtml += `<div class="nav-item" data-key="${a.key}" ${a.closeable ? 'data-closeable' : ''}> 589 ${a.icon ? `<lit-icon name='${a.icon}'></lit-icon>` : ``} 590 <span>${a.tab}</span> 591 <lit-icon class="close-icon" name='close' size="12"></lit-icon><div class="no-close-icon" style="margin-right: 12px"></div> 592 </div>`; 593 } 594 595 } 596 }) 597 this.nav!.innerHTML = navHtml; 598 this.initTabPos() 599 this.nav!.querySelectorAll<HTMLElement>('.close-icon').forEach(a => { 600 a.onclick = (e) => { 601 e.stopPropagation(); 602 const closeKey = (e.target! as HTMLElement).parentElement!.dataset.key; 603 this.dispatchEvent(new CustomEvent('close-handler', {detail: {key: closeKey}, composed: true})); 604 } 605 }); 606 }) 607 this.nav!.onclick = (e) => { 608 if ((e.target! as HTMLElement).closest('div')!.hasAttribute('data-disabled')) return; 609 let key = (e.target! as HTMLElement).closest('div')!.dataset.key; 610 if (key) { 611 this.activeByKey(key) 612 } 613 let label = (e.target! as HTMLElement).closest('div')!.querySelector('span')!.textContent; 614 this.dispatchEvent(new CustomEvent('onTabClick', {detail: {key: key, tab: label}})) 615 }; 616 617 new ResizeObserver((entries) => { 618 let filling = this.shadowRoot!.querySelector<HTMLDivElement>("#tab-filling") 619 620 this.shadowRoot!.querySelector<HTMLDivElement>(".tab-nav-container")!.style.height = filling!.offsetWidth+"px" 621 622 }).observe(this.shadowRoot!.querySelector("#tab-filling")!); 623 } 624 625 activeByKey(key: string) { 626 if (key === null || key === undefined) return; //如果没有key 不做相应 627 this.nav!.querySelectorAll('.nav-item').forEach(a => { 628 if (a.getAttribute('data-key') === key) { 629 a.setAttribute('data-selected', 'true'); 630 } else { 631 a.removeAttribute('data-selected'); 632 } 633 }) 634 let tbp = this.querySelector(`lit-tabpane[key='${key}']`); 635 let panes = this.querySelectorAll<LitTabpane>('lit-tabpane'); 636 panes.forEach(a => { 637 if (a.key === key) { 638 a.style.display = 'block'; 639 this.activekey = a.key; 640 this.initTabPos() 641 } else { 642 a.style.display = 'none'; 643 } 644 }) 645 } 646 647 activePane(key: string) { 648 if (key === null || key === undefined) return false; 649 let tbp = this.querySelector(`lit-tabpane[key='${key}']`); 650 if (tbp) { 651 this.activeByKey(key) 652 return true; 653 } else { 654 return false; 655 } 656 } 657 658 disconnectedCallback() { 659 660 } 661 662 adoptedCallback() { 663 } 664 665 attributeChangedCallback(name: string, oldValue: string, newValue: string) { 666 if (name === 'activekey' && this.nav && oldValue !== newValue && newValue != '') { 667 this.activeByKey(newValue) 668 } 669 } 670} 671 672