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'; 17import { LitTabpane } from './lit-tabpane'; 18import { SpStatisticsHttpUtil } from '../../statistics/util/SpStatisticsHttpUtil'; 19import { LitTabsHtml } from './lit-tabs.html'; 20import { shadowRootInput } from '../../trace/component/trace/base/shadowRootInput'; 21 22@element('lit-tabs') 23export class LitTabs extends HTMLElement { 24 private tabPos: unknown; 25 private nav: HTMLDivElement | undefined | null; 26 private line: HTMLDivElement | undefined | null; 27 private slots: HTMLSlotElement | undefined | null; 28 29 constructor() { 30 super(); 31 const shadowRoot = this.attachShadow({ mode: 'open' }); 32 shadowRoot.innerHTML = LitTabsHtml; 33 } 34 35 static get observedAttributes(): string[] { 36 return ['activekey', 'mode', 'position']; 37 } 38 39 get position(): string { 40 return this.getAttribute('position') || 'top'; 41 } 42 43 set position(value) { 44 this.setAttribute('position', value); 45 } 46 47 get mode(): string { 48 return this.getAttribute('mode') || 'flat'; 49 } 50 51 set mode(value) { 52 this.setAttribute('mode', value); 53 } 54 55 get activekey(): string { 56 return this.getAttribute('activekey') || ''; 57 } 58 59 set activekey(value: string) { 60 this.setAttribute('activekey', value); 61 } 62 63 set onTabClick(fn: unknown) { 64 //@ts-ignore 65 this.addEventListener('onTabClick', fn); 66 } 67 68 updateLabel(key: string, value: string): void { 69 if (this.nav) { 70 let item = this.nav.querySelector(`.nav-item[data-key='${key}']`); 71 if (item) { 72 item.querySelector<HTMLSpanElement>('span')!.innerHTML = value; 73 this.initTabPos(); 74 } 75 } 76 } 77 78 updateDisabled(key: string, value: string): void { 79 if (this.nav) { 80 let item = this.nav.querySelector(`.nav-item[data-key='${key}']`); 81 if (item) { 82 if (value) { 83 item.setAttribute('data-disabled', ''); 84 } else { 85 item.removeAttribute('data-disabled'); 86 } 87 this.initTabPos(); 88 } 89 } 90 } 91 92 updateCloseable(key: string, value: string): void { 93 if (this.nav) { 94 let item = this.nav.querySelector(`.nav-item[data-key='${key}']`); 95 if (item) { 96 if (value) { 97 item.setAttribute('data-closeable', ''); 98 } else { 99 item.removeAttribute('data-closeable'); 100 } 101 this.initTabPos(); 102 } 103 } 104 } 105 106 updateHidden(key: string, value: string): void { 107 if (this.nav) { 108 let item = this.nav.querySelector(`.nav-item[data-key='${key}']`); 109 if (item) { 110 if (value === 'true') { 111 item.setAttribute('data-hidden', ''); 112 } else { 113 item.removeAttribute('data-hidden'); 114 } 115 this.initTabPos(); 116 } 117 } 118 } 119 120 initTabPos(): void { 121 const items = this.nav!.querySelectorAll<HTMLDivElement>('.nav-item'); 122 Array.from(items).forEach((a, index) => { 123 // @ts-ignore 124 this.tabPos[a.dataset.key] = { 125 index: index, 126 width: a.offsetWidth, 127 height: a.offsetHeight, 128 left: a.offsetLeft, 129 top: a.offsetTop, 130 label: a.textContent, 131 }; 132 }); 133 if (this.activekey) { 134 if (this.position.startsWith('left')) { 135 this.line?.setAttribute( 136 'style', //@ts-ignore 137 `height:${this.tabPos[this.activekey].height}px;transform:translate(100%,${ 138 //@ts-ignore 139 this.tabPos[this.activekey].top 140 }px)` 141 ); 142 } else if (this.position.startsWith('top')) { 143 //@ts-ignore 144 if (this.tabPos[this.activekey]) { 145 this.line?.setAttribute( 146 'style', //@ts-ignore 147 `width:${this.tabPos[this.activekey].width}px;transform:translate(${ 148 //@ts-ignore 149 this.tabPos[this.activekey].left 150 }px,100%)` 151 ); 152 } 153 } else if (this.position.startsWith('right')) { 154 this.line?.setAttribute( 155 'style', //@ts-ignore 156 `height:${this.tabPos[this.activekey].height}px;transform:translate(-100%,${ 157 //@ts-ignore 158 this.tabPos[this.activekey].top 159 }px)` 160 ); 161 } else if (this.position.startsWith('bottom')) { 162 this.line?.setAttribute( 163 'style', //@ts-ignore 164 `width:${this.tabPos[this.activekey].width}px;transform:translate(${this.tabPos[this.activekey].left}px,100%)` 165 ); 166 } 167 } 168 } 169 170 connectedCallback(): void { 171 this.tabPos = {}; 172 this.nav = this.shadowRoot?.querySelector('#nav'); 173 this.line = this.shadowRoot?.querySelector('#tab-line'); 174 this.slots = this.shadowRoot?.querySelector('#slot'); 175 this.slots?.addEventListener('slotchange', () => { 176 const elements: Element[] | undefined = this.slots?.assignedElements(); 177 let panes = this.querySelectorAll<LitTabpane>('lit-tabpane'); 178 if (this.activekey) { 179 panes.forEach((a) => { 180 if (a.key === this.activekey) { 181 a.style.display = 'block'; 182 } else { 183 a.style.display = 'none'; 184 } 185 }); 186 } else { 187 panes.forEach((a, index) => { 188 if (index === 0) { 189 a.style.display = 'block'; 190 this.activekey = a.key || ''; 191 } else { 192 a.style.display = 'none'; 193 } 194 }); 195 } 196 this.setItemNode(elements); 197 }); 198 this.nav!.onclick = (e): void => { 199 if ((e.target! as HTMLElement).closest('div')!.hasAttribute('data-disabled')) { 200 return; 201 } 202 let key = (e.target! as HTMLElement).closest('div')!.dataset.key; 203 if (key) { 204 this.activeByKey(key); 205 } 206 let label = (e.target! as HTMLElement).closest('div')!.querySelector('span')!.textContent; 207 this.dispatchEvent( 208 new CustomEvent('onTabClick', { 209 detail: { key: key, tab: label }, 210 }) 211 ); 212 }; 213 214 new ResizeObserver((entries) => { 215 let filling = this.shadowRoot!.querySelector<HTMLDivElement>('#tab-filling'); 216 217 this.shadowRoot!.querySelector<HTMLDivElement>('.tab-nav-vessel')!.style.height = filling!.offsetWidth + 'px'; 218 }).observe(this.shadowRoot!.querySelector('#tab-filling')!); 219 } 220 221 setItemNode(elements: Element[] | undefined): void { 222 let navHtml: string = ''; 223 elements 224 ?.map((it) => it as LitTabpane) 225 .forEach((a) => { 226 if (a.disabled) { 227 navHtml += `<div class="nav-item" data-key="${a.key}" data-disabled ${a.closeable ? 'data-closeable' : ''}> 228 ${a.icon ? `<lit-icon name='${a.icon}'></lit-icon>` : ''} 229 <span>${a.tab}</span> 230 <lit-icon class="close-icon" name='close' size="16"></lit-icon><div class="no-close-icon" style="margin-right: 12px"></div> 231 </div>`; 232 } else if (a.hidden) { 233 navHtml += `<div class="nav-item" data-key="${a.key}" data-hidden ${a.closeable ? 'data-closeable' : ''}> 234 ${a.icon ? `<lit-icon name='${a.icon}'></lit-icon>` : ''} 235 <span>${a.tab}</span> 236 <lit-icon class="close-icon" name='close' size="16"></lit-icon><div class="no-close-icon" style="margin-right: 12px"></div> 237 </div>`; 238 } else { 239 if (a.key === this.activekey) { 240 navHtml += `<div class="nav-item" data-key="${a.key}" data-selected ${a.closeable ? 'data-closeable' : ''}> 241 ${a.icon ? `<lit-icon name='${a.icon}'></lit-icon>` : ''} 242 <span>${a.tab}</span> 243 <lit-icon class="close-icon" name='close' size="16"></lit-icon><div class="no-close-icon" style="margin-right: 12px"></div> 244 </div>`; 245 } else { 246 navHtml += `<div class="nav-item" data-key="${a.key}" ${a.closeable ? 'data-closeable' : ''}> 247 ${a.icon ? `<lit-icon name='${a.icon}'></lit-icon>` : ''} 248 <span>${a.tab}</span> 249 <lit-icon class="close-icon" name='close' size="16"></lit-icon><div class="no-close-icon" style="margin-right: 12px"></div> 250 </div>`; 251 } 252 } 253 }); 254 this.nav!.innerHTML = navHtml; 255 this.initTabPos(); 256 this.nav!.querySelectorAll<HTMLElement>('.close-icon').forEach((a) => { 257 a.onclick = (e): void => { 258 e.stopPropagation(); 259 const closeKey = (e.target! as HTMLElement).parentElement!.dataset.key; 260 this.dispatchEvent( 261 new CustomEvent('close-handler', { 262 detail: { key: closeKey }, 263 composed: true, 264 }) 265 ); 266 }; 267 }); 268 } 269 270 activeByKey(key: string, isValid: boolean = true): void { 271 if (key === null || key === undefined) { 272 return; 273 } //如果没有key 不做相应 274 this.nav!.querySelectorAll('.nav-item').forEach((a) => { 275 if (a.querySelector('span')?.innerText === 'Comparison') { 276 a.setAttribute('id', 'nav-comparison'); 277 } 278 if (a.getAttribute('data-key') === key) { 279 a.setAttribute('data-selected', 'true'); 280 this.byKeyIsValid(isValid, a); 281 } else { 282 a.removeAttribute('data-selected'); 283 } 284 }); 285 let panes = this.querySelectorAll<LitTabpane>('lit-tabpane'); 286 panes.forEach((a) => { 287 if (a.key === key) { 288 a.style.display = 'block'; 289 this.activekey = a.key; 290 this.initTabPos(); 291 } else { 292 a.style.display = 'none'; 293 } 294 }); 295 let tbp = this.querySelector(`lit-tabpane[key='${key}']`); 296 if (tbp) { 297 setTimeout(() => { 298 shadowRootInput.preventBubbling(tbp); 299 }, 500); 300 } 301 } 302 303 byKeyIsValid(isValid: boolean, a: Element): void { 304 if (isValid) { 305 let span = a.querySelector('span') as HTMLSpanElement; 306 let title = span.innerText; 307 let rowType = document 308 .querySelector<HTMLElement>('sp-application')! 309 .shadowRoot?.querySelector<HTMLElement>('sp-system-trace')! 310 .getAttribute('clickRow'); 311 if (title === 'Counters' || title === 'Thread States') { 312 title += `(${rowType})`; 313 } 314 if (title === 'Analysis') { 315 let rowId = document 316 .querySelector<HTMLElement>('sp-application')! 317 .shadowRoot?.querySelector<HTMLElement>('sp-system-trace')! 318 .getAttribute('rowId'); 319 if (rowId!.indexOf('DiskIOLatency') > -1) { 320 title += '(disk-io)'; 321 } else if (rowId!.indexOf('VirtualMemory') > -1) { 322 title += '(virtual-memory-cell)'; 323 } else { 324 title += `(${rowType})`; 325 } 326 } 327 if (title === 'Slices' || title === 'Current Selection') { 328 let rowName = document 329 .querySelector<HTMLElement>('sp-application')! 330 .shadowRoot?.querySelector<HTMLElement>('sp-system-trace')! 331 .getAttribute('rowName'); 332 if (rowName && rowName!.indexOf('deliverInputEvent') > -1) { 333 title += '(deliverInputEvent)'; 334 } else { 335 let rowType = document 336 .querySelector<HTMLElement>('sp-application')! 337 .shadowRoot?.querySelector<HTMLElement>('sp-system-trace')! 338 .getAttribute('clickRow'); 339 title += `(${rowType})`; 340 } 341 } 342 SpStatisticsHttpUtil.addOrdinaryVisitAction({ 343 event: title, 344 action: 'trace_tab', 345 }); 346 } 347 } 348 349 activePane(key: string): boolean { 350 if (key === null || key === undefined) { 351 return false; 352 } 353 let tbp = this.querySelector(`lit-tabpane[key='${key}']`); 354 if (tbp) { 355 this.activeByKey(key); 356 return true; 357 } else { 358 return false; 359 } 360 } 361 362 disconnectedCallback(): void {} 363 364 adoptedCallback(): void {} 365 366 attributeChangedCallback(name: string, oldValue: string, newValue: string): void { 367 if (name === 'activekey' && this.nav && oldValue !== newValue && newValue !== '') { 368 this.activeByKey(newValue, false); 369 } 370 } 371} 372