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