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 { BaseElement, element } from '../BaseElement';
17import { selectHtmlStr } from './LitSelectHtml';
18import { LitSelectOption } from './LitSelectOption';
19import { SpSystemTrace } from '../../trace/component/SpSystemTrace';
20
21@element('lit-select')
22export class LitSelect extends BaseElement {
23  private focused: unknown;
24  private selectInputEl: unknown;
25  private selectSearchInputEl: HTMLInputElement | null | undefined;
26  private selectVInputEl: HTMLInputElement | null | undefined;
27  private selectOptions: HTMLDivElement | null | undefined;
28  private selectItem: string = '';
29  private selectClearEl: unknown;
30  private selectIconEl: unknown;
31  private bodyEl: unknown;
32  private selectSearchEl: unknown;
33  private selectMultipleRootEl: unknown;
34  private currentSelectedValue: string = '';
35
36  static get observedAttributes(): string[] {
37    return [
38      'value',
39      'default-value',
40      'placeholder',
41      'disabled',
42      'loading',
43      'allow-clear',
44      'show-search',
45      'list-height',
46      'border',
47      'mode',
48      'showSearchInput',
49      'tabSelect'
50    ];
51  }
52
53  get value(): string {
54    return this.getAttribute('value') || this.defaultValue;
55  }
56
57  set value(selectValue) {
58    this.setAttribute('value', selectValue);
59  }
60
61  get rounded(): boolean {
62    return this.hasAttribute('rounded');
63  }
64
65  set rounded(selectRounded: boolean) {
66    if (selectRounded) {
67      this.setAttribute('rounded', '');
68    } else {
69      this.removeAttribute('rounded');
70    }
71  }
72
73  get placement(): string {
74    return this.getAttribute('placement') || '';
75  }
76
77  set placement(selectPlacement: string) {
78    if (selectPlacement) {
79      this.setAttribute('placement', selectPlacement);
80    } else {
81      this.removeAttribute('placement');
82    }
83  }
84
85  get border(): string {
86    return this.getAttribute('border') || 'true';
87  }
88
89  set border(selectBorder) {
90    if (selectBorder) {
91      this.setAttribute('border', 'true');
92    } else {
93      this.setAttribute('border', 'false');
94    }
95  }
96
97  get listHeight(): string {
98    return this.getAttribute('list-height') || '256px';
99  }
100
101  set listHeight(selectListHeight) {
102    this.setAttribute('list-height', selectListHeight);
103  }
104
105  get defaultPlaceholder(): string {
106    return this.getAttribute('placeholder') || '请选择';
107  }
108
109  set canInsert(can: boolean) {
110    if (can) {
111      this.setAttribute('canInsert', '');
112    } else {
113      this.removeAttribute('canInsert');
114    }
115  }
116
117  get canInsert(): boolean {
118    return this.hasAttribute('canInsert');
119  }
120  get showSearch(): boolean {
121    return this.hasAttribute('show-search');
122  }
123
124  get defaultValue(): string {
125    return this.getAttribute('default-value') || '';
126  }
127
128  set defaultValue(selectDefaultValue) {
129    this.setAttribute('default-value', selectDefaultValue);
130  }
131
132  get placeholder(): string {
133    return this.getAttribute('placeholder') || this.defaultPlaceholder;
134  }
135
136  set placeholder(selectPlaceHolder) {
137    this.setAttribute('placeholder', selectPlaceHolder);
138  }
139
140  get loading(): boolean {
141    return this.hasAttribute('loading');
142  }
143
144  set loading(selectLoading) {
145    if (selectLoading) {
146      this.setAttribute('loading', '');
147    } else {
148      this.removeAttribute('loading');
149    }
150  }
151
152  get showSearchInput(): boolean {
153    return this.hasAttribute('showSearchInput');
154  }
155
156  set showSearchInput(isHide: boolean) {
157    if (isHide) {
158      this.setAttribute('showSearchInput', '');
159    } else {
160      this.removeAttribute('showSearchInput');
161    }
162  }
163
164  set showItem(item: string) {
165    this.selectItem = item;
166  }
167
168  set dataSource(selectDataSource: unknown) {
169    this.innerHTML = '<slot></slot><slot name="footer"></slot>'; // @ts-ignore
170    if (selectDataSource.length > 0) {
171      // @ts-ignore
172      this.bodyEl!.style.display = 'flex';
173      this.querySelectorAll('lit-select-option').forEach((a) => {
174        this.removeChild(a);
175      });
176      let valuesSet = new Set();
177      let flag = true; // 假设所有 value 都是唯一的  
178      // @ts-ignore
179      selectDataSource.forEach(item => {
180        if (valuesSet.has(item.value)) {
181          flag = false; // 如果value有重复,就设置flag为false  
182          return;
183        }
184        valuesSet.add(item.value);
185      });
186      // @ts-ignore
187      selectDataSource.forEach((dateSourceBean: unknown) => {
188        if (dateSourceBean) {
189          let selectOption = document.createElement('lit-select-option');
190          let optionData = {
191            // @ts-ignore
192            value: dateSourceBean.value ? dateSourceBean.value : dateSourceBean.name || dateSourceBean, // @ts-ignore
193            name: dateSourceBean.name ? dateSourceBean.name : dateSourceBean,
194          };
195          if (!flag) { // 如果数组的value值不是唯一的,就用name做为value值,避免多个选项被选中
196            optionData = {
197              // @ts-ignore
198              value: dateSourceBean.name ? dateSourceBean.name : dateSourceBean, // @ts-ignore
199              name: dateSourceBean.name ? dateSourceBean.name : dateSourceBean,
200            };
201          }
202          selectOption.textContent = optionData.name;
203          selectOption.setAttribute('value', optionData.value);
204          if (this.currentSelectedValue === optionData.value) {
205            selectOption.setAttribute('selected', '');
206          }
207          // @ts-ignore
208          this.selectInputEl!.value = '';
209          this.append(selectOption);
210        }
211      });
212      this.initOptions();
213    } else {
214      // @ts-ignore
215      this.bodyEl!.style.display = 'none';
216    }
217  }
218
219  initElements(): void {
220    this.selectVInputEl = this.shadowRoot!.querySelector('#select-input') as HTMLInputElement;
221    this.selectVInputEl?.addEventListener('keyup', (e) => {
222      if (e.code === 'Enter' || e.code === 'NumpadEnter') {// @ts-ignore
223        this.selectVInputEl.blur();// @ts-ignore
224        this.selectInputEl.value = this.selectVInputEl.value;
225      }
226    });
227    if (this.showSearchInput) {
228      this.shadowRoot!.querySelector<HTMLDivElement>('.body-select')!.style.display = 'block';
229      this.selectSearchInputEl = this.shadowRoot!.querySelector('#search-input') as HTMLInputElement;
230      this.selectSearchInputEl?.addEventListener('keyup', (evt) => {
231        let options = [];
232        options = [...this.querySelectorAll<LitSelectOption>('lit-select-option')];
233        options.filter((a: LitSelectOption) => {
234          if (a.textContent!.indexOf(this.selectSearchInputEl!.value) <= -1) {
235            a.style.display = 'none';
236          } else {
237            a.style.display = 'flex';
238          }
239        });
240        evt.preventDefault();
241        evt.stopPropagation();
242      });
243    }
244  }
245
246  initHtml(): string {
247    return `
248        ${selectHtmlStr(this.listHeight)}
249        <div class="root noSelect" tabindex="0" hidefocus="true">
250            <div class="multipleRoot">
251            <input id="select-input" placeholder="${this.placeholder}" autocomplete="off" ${this.showSearch || this.canInsert ? '' : 'readonly'} tabindex="0">
252            </div>
253            <lit-loading class="loading" size="12"></lit-loading>
254            <lit-icon class="icon" name='down' color="#c3c3c3"></lit-icon>
255            <lit-icon class="clear" name='close-circle-fill'></lit-icon>
256            <lit-icon class="search" name='search'></lit-icon>
257        </div>
258        <div class="body">
259            <div class="body-select" style="display: none;">
260                <input id="search-input" placeholder="Search">
261            </div>
262            <div class="body-opt">
263                <slot></slot>
264                <slot name="footer"></slot>
265            </div>
266        </div>
267        `;
268  }
269
270  isMultiple(): boolean {
271    return this.hasAttribute('mode') && this.getAttribute('mode') === 'multiple';
272  }
273
274  newTag(value: unknown, text: unknown): HTMLDivElement {
275    let tag: unknown = document.createElement('div');
276    let icon: unknown = document.createElement('lit-icon'); // @ts-ignore
277    icon.classList.add('tag-close'); // @ts-ignore
278    icon.name = 'close';
279    let span = document.createElement('span'); // @ts-ignore
280    tag.classList.add('tag'); // @ts-ignore
281    span.dataset.value = value; // @ts-ignore
282    span.textContent = text; // @ts-ignore
283    tag.append(span); // @ts-ignore
284    tag.append(icon); // @ts-ignore
285    icon.onclick = (ev: unknown): void => {
286      // @ts-ignore
287      tag.parentElement.removeChild(tag);
288      this.querySelector(`lit-select-option[value=${value}]`)!.removeAttribute('selected');
289      if (this.shadowRoot!.querySelectorAll('.tag').length === 0) {
290        // @ts-ignore
291        this.selectInputEl.style.width = 'auto'; // @ts-ignore
292        this.selectInputEl.placeholder = this.defaultPlaceholder;
293      } // @ts-ignore
294      ev.stopPropagation();
295    }; // @ts-ignore
296    tag.value = value; // @ts-ignore
297    tag.dataset.value = value; // @ts-ignore
298    tag.text = text; // @ts-ignore
299    tag.dataset.text = text; // @ts-ignore
300    return tag;
301  }
302
303  connectedCallback(): void {
304    this.tabIndex = 0;
305    this.focused = false;
306    this.bodyEl = this.shadowRoot!.querySelector('.body');
307    this.selectInputEl = this.shadowRoot!.querySelector('input');
308    this.selectClearEl = this.shadowRoot!.querySelector('.clear');
309    this.selectIconEl = this.shadowRoot!.querySelector('.icon');
310    this.selectSearchEl = this.shadowRoot!.querySelector('.search');
311    this.selectMultipleRootEl = this.shadowRoot!.querySelector('.multipleRoot');
312    this.selectOptions = this.shadowRoot!.querySelector('.body-opt') as HTMLDivElement;
313    this.setEventClick();
314    this.setEvent(); // @ts-ignore
315    this.selectInputEl.onblur = (ev: unknown): void => {
316      if (this.hasAttribute('disabled')) {
317        return;
318      }
319      if (this.isMultiple()) {
320        if (this.hasAttribute('show-search')) {
321          // @ts-ignore
322          this.selectSearchEl.style.display = 'none'; // @ts-ignore
323          this.selectIconEl.style.display = 'flex';
324        }
325      } else {
326        // @ts-ignore
327        if (this.selectInputEl.placeholder !== this.defaultPlaceholder) {
328          // @ts-ignore
329          this.selectInputEl.value = this.selectInputEl.placeholder; // @ts-ignore
330          this.selectInputEl.placeholder = this.defaultPlaceholder;
331        }
332        if (this.hasAttribute('show-search')) {
333          // @ts-ignore
334          this.selectSearchEl.style.display = 'none'; // @ts-ignore
335          this.selectIconEl.style.display = 'flex';
336        }
337      }
338    };
339    this.setOninput();
340    this.setOnkeydown();
341  }
342
343  setOninput(): void {
344    // @ts-ignore
345    this.selectInputEl.oninput = (ev: unknown): void => {
346      let els: Element[] = [...this.querySelectorAll('lit-select-option')];
347      if (this.hasAttribute('show-search')) {
348        // @ts-ignore
349        if (!ev.target.value) {
350          // @ts-ignore
351          els.forEach((a: unknown) => (a.style.display = 'flex'));
352        } else {
353          this.setSelectItem(els, ev);
354        }
355      } else {
356        // @ts-ignore
357        this.value = ev.target.value;
358      }
359    };
360  }
361
362  setSelectItem(els: Element[], ev: unknown): void {
363    els.forEach((a: unknown) => {
364      // @ts-ignore
365      let value = a.getAttribute('value');
366      if (
367        // @ts-ignore
368        value.toLowerCase().indexOf(ev.target.value.toLowerCase()) !== -1 || // @ts-ignore
369        a.textContent.toLowerCase().indexOf(ev.target.value.toLowerCase()) !== -1
370      ) {
371        // @ts-ignore
372        a.style.display = 'flex';
373      } else {
374        // @ts-ignore
375        a.style.display = 'none';
376      }
377    });
378  }
379
380  setEventClick(): void {
381    // @ts-ignore
382    this.selectClearEl.onclick = (ev: unknown): void => {
383      if (this.isMultiple()) {
384        let delNodes: Array<unknown> = []; // @ts-ignore
385        this.selectMultipleRootEl.childNodes.forEach((a: unknown) => {
386          // @ts-ignore
387          if (a.tagName === 'DIV') {
388            delNodes.push(a);
389          }
390        });
391        for (let i = 0; i < delNodes.length; i++) {
392          // @ts-ignore
393          delNodes[i].remove();
394        }
395        if (this.shadowRoot!.querySelectorAll('.tag').length === 0) {
396          // @ts-ignore
397          this.selectInputEl.style.width = 'auto'; // @ts-ignore
398          this.selectInputEl.placeholder = this.defaultPlaceholder;
399        }
400      }
401      this.querySelectorAll('lit-select-option').forEach((a) => a.removeAttribute('selected')); // @ts-ignore
402      this.selectInputEl.value = ''; // @ts-ignore
403      this.selectClearEl.style.display = 'none'; // @ts-ignore
404      this.selectIconEl.style.display = 'flex';
405      this.blur(); // @ts-ignore
406      ev.stopPropagation();
407      this.dispatchEvent(new CustomEvent('onClear', { detail: ev }));
408    };
409    this.initOptions();
410    this.onclick = (ev: unknown): void => {
411      // @ts-ignore
412      if (ev.target.tagName === 'LIT-SELECT') {
413        if (this.focused === false) {
414          // @ts-ignore
415          this.selectInputEl.focus();
416          this.focused = true; // @ts-ignore
417          this.bodyEl!.style.display = 'flex';
418        } else {
419          this.focused = false;
420        }
421      }
422    };
423  }
424
425  setEvent(): void {
426    this.onmouseover = this.onfocus = (ev): void => {
427      if (this.focused === false && this.hasAttribute('adaptive-expansion')) {
428        // @ts-ignore
429        if (this.parentElement!.offsetTop < this.bodyEl!.clientHeight) {
430          // @ts-ignore
431          this.bodyEl!.classList.add('body-bottom');
432        } else {
433          // @ts-ignore
434          this.bodyEl!.classList.remove('body-bottom');
435        }
436      }
437      if (this.hasAttribute('allow-clear')) {
438        // @ts-ignore
439        if (this.selectInputEl.value.length > 0 || this.selectInputEl.placeholder !== this.defaultPlaceholder) {
440          // @ts-ignore
441          this.selectClearEl.style.display = 'flex'; // @ts-ignore
442          this.selectIconEl.style.display = 'none';
443        } else {
444          // @ts-ignore
445          this.selectClearEl.style.display = 'none'; // @ts-ignore
446          this.selectIconEl.style.display = 'flex';
447        }
448      }
449    };
450    this.onmouseout = this.onblur = (ev): void => {
451      if (this.hasAttribute('allow-clear')) {
452        // @ts-ignore
453        this.selectClearEl.style.display = 'none'; // @ts-ignore
454        this.selectIconEl.style.display = 'flex';
455      }
456      this.focused = false;
457    }; // @ts-ignore
458    this.selectInputEl.onfocus = (ev: unknown): void => {
459      if (this.hasAttribute('disabled')) {
460        return;
461      }
462      if (this.hasAttribute('show-search')) {
463        // @ts-ignore
464        this.selectSearchEl.style.display = 'flex'; // @ts-ignore
465        this.selectIconEl.style.display = 'none';
466      }
467      this.querySelectorAll('lit-select-option').forEach((a) => {
468        // @ts-ignore
469        a.style.display = 'flex';
470      });
471    };
472  }
473
474  setOnkeydown(): void {
475    // @ts-ignore
476    this.selectInputEl.onkeydown = (ev: KeyboardEvent): void => {
477      ev.stopPropagation();
478    if (this.hasAttribute('tabselect')) {
479      // @ts-ignore
480      this.selectInputEl.readOnly = true;
481    } else {
482      // @ts-ignore
483      if (ev.key === 'Backspace') {
484        if (this.isMultiple()) {
485          // @ts-ignore
486          let tag = this.selectMultipleRootEl.lastElementChild.previousElementSibling;
487          if (tag) {
488            this.querySelector(`lit-select-option[value=${tag.value}]`)?.removeAttribute('selected');
489            tag.remove();
490            if (this.shadowRoot!.querySelectorAll('.tag').length === 0) {
491              // @ts-ignore
492              this.selectInputEl.style.width = 'auto'; // @ts-ignore
493              this.selectInputEl.placeholder = this.defaultPlaceholder;
494            }
495          }
496        } else {
497          this.clear();
498          this.dispatchEvent(new CustomEvent('onClear', { detail: ev })); //向外派发清理事件
499        } // @ts-ignore
500      } else if (ev.key === 'Enter') {
501        if (!this.canInsert) {
502          let filter = [...this.querySelectorAll('lit-select-option')].filter(
503            // @ts-ignore
504            (a: unknown) => a.style.display !== 'none'
505          );
506          if (filter.length > 0) {
507            // @ts-ignore
508            this.selectInputEl.value = filter[0].textContent; // @ts-ignore
509            this.selectInputEl.placeholder = filter[0].textContent;
510            this.blur();
511            // @ts-ignore
512            this.value = filter[0].getAttribute('value');
513            this.dispatchEvent(
514              new CustomEvent('change', {
515                detail: {
516                  selected: true,
517                  value: filter[0].getAttribute('value'),
518                  text: filter[0].textContent,
519                },
520              })
521            );
522          }
523        }// @ts-ignore
524      } else if (ev.key === '0' && ev.target.value.length === 1 && ev.target.value === '0') {
525        // @ts-ignore
526        ev.preventDefault();
527      }
528    }
529    };
530  }
531
532  initOptions(): void {
533    this.querySelectorAll('lit-select-option').forEach((a) => {
534      if (this.isMultiple()) {
535        a.setAttribute('check', '');
536        if (a.getAttribute('value') === this.defaultValue) {
537          let tag = this.newTag(a.getAttribute('value'), a.textContent); // @ts-ignore
538          this.selectMultipleRootEl.insertBefore(tag, this.selectInputEl); // @ts-ignore
539          this.selectInputEl.placeholder = ''; // @ts-ignore
540          this.selectInputEl.value = ''; // @ts-ignore
541          this.selectInputEl.style.width = '1px';
542          a.setAttribute('selected', '');
543        }
544      } else {
545        if (a.hasAttribute('selected')) {
546          a.removeAttribute('selected');
547        }
548        if (a.getAttribute('value') === this.defaultValue) {
549          // @ts-ignore
550          this.selectInputEl.value = a.textContent;
551          a.setAttribute('selected', '');
552        }
553      }
554      a.addEventListener('mouseup', (e) => {
555        e.stopPropagation();
556      });
557      a.addEventListener('mousedown', (e) => {
558        e.stopPropagation();
559      });
560      this.onSelectedEvent(a);
561    });
562  }
563
564  onSelectedEvent(a: Element): void {
565    a.addEventListener('onSelected', (e: unknown) => {
566      if (this.isMultiple()) {
567        if (a.hasAttribute('selected')) {
568          // @ts-ignore
569          let tag = this.shadowRoot!.querySelector(`div[data-value=${e.detail.value}]`) as HTMLElement;
570          if (tag) {
571            tag.parentElement!.removeChild(tag);
572          } // @ts-ignore
573          e.detail.selected = false;
574        } else {
575          // @ts-ignore
576          let tag = this.newTag(e.detail.value, e.detail.text); // @ts-ignore
577          this.selectMultipleRootEl.insertBefore(tag, this.selectInputEl); // @ts-ignore
578          this.selectInputEl.placeholder = ''; // @ts-ignore
579          this.selectInputEl.value = ''; // @ts-ignore
580          this.selectInputEl.style.width = '1px';
581        }
582        if (this.shadowRoot!.querySelectorAll('.tag').length === 0) {
583          // @ts-ignore
584          this.selectInputEl.style.width = 'auto'; // @ts-ignore
585          this.selectInputEl.placeholder = this.defaultPlaceholder;
586        } // @ts-ignore
587        this.selectInputEl.focus();
588      } else {
589        [...this.querySelectorAll('lit-select-option')].forEach((item) => {
590          if (item.hasAttribute('selected')) {
591            this.currentSelectedValue = item.getAttribute('value') || '';
592          }
593          item.removeAttribute('selected');
594        });
595        this.blur(); // @ts-ignore
596        this.bodyEl!.style.display = 'none';
597        // @ts-ignore
598        this.selectInputEl.value = e.detail.text;
599      }
600      if (a.getAttribute('value') === this.currentSelectedValue) {
601        a.removeAttribute('selected');
602        this.currentSelectedValue = '';
603        // @ts-ignore
604        this.selectInputEl.value = '';
605        // @ts-ignore
606        this.selectInputEl.placeholder = this.defaultPlaceholder;
607      } else {
608        this.currentSelectedValue = a.getAttribute('value') || '';
609        a.setAttribute('selected', '');
610      }
611      this.value = this.currentSelectedValue;
612      // @ts-ignore
613      this.dispatchEvent(new CustomEvent('change', { detail: { selectValue: this.currentSelectedValue, text: e.detail.text } })); //向外层派发change事件,返回当前选中项
614    });
615  }
616
617  clear(): void {
618    // @ts-ignore
619    this.selectInputEl.value = ''; // @ts-ignore
620    this.selectInputEl.placeholder = this.defaultPlaceholder;
621  }
622
623  reset(): void {
624    this.querySelectorAll('lit-select-option').forEach((a) => {
625      [...this.querySelectorAll('lit-select-option')].forEach((a) => a.removeAttribute('selected'));
626      if (a.getAttribute('value') === this.defaultValue) {
627        // @ts-ignore
628        this.selectInputEl.value = a.textContent;
629        a.setAttribute('selected', '');
630      }
631    });
632  }
633
634  disconnectedCallback(): void { }
635
636  adoptedCallback(): void { }
637
638  attributeChangedCallback(name: unknown, oldValue: unknown, newValue: unknown): void {
639    if (name === 'value' && this.selectInputEl) {
640      if (newValue) {
641        [...this.querySelectorAll('lit-select-option')].forEach((a) => {
642          if (a.getAttribute('value') === newValue) {
643            this.currentSelectedValue = a.getAttribute('value') || '';
644            a.setAttribute('selected', ''); // @ts-ignore
645            this.selectInputEl.value = a.textContent;
646          } else {
647            a.removeAttribute('selected');
648          }
649        });
650      } else {
651        this.clear();
652      }
653    }
654  }
655}
656