• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      // @ts-ignore
463      if (this.selectInputEl.value.length > 0) {
464        // @ts-ignore
465        this.selectInputEl.placeholder = this.selectInputEl.value;
466        // @ts-ignore
467        this.selectInputEl.value = '';
468      }
469      if (this.hasAttribute('show-search')) {
470        // @ts-ignore
471        this.selectSearchEl.style.display = 'flex'; // @ts-ignore
472        this.selectIconEl.style.display = 'none';
473      }
474      this.querySelectorAll('lit-select-option').forEach((a) => {
475        // @ts-ignore
476        a.style.display = 'flex';
477      });
478    };
479  }
480
481  setOnkeydown(): void {
482    // @ts-ignore
483    this.selectInputEl.onkeydown = (ev: KeyboardEvent): void => {
484      ev.stopPropagation();
485      if (this.hasAttribute('tabselect')) {
486        // @ts-ignore
487        this.selectInputEl.readOnly = true;
488      } else {
489        // @ts-ignore
490        if (ev.key === 'Backspace') {
491          if (this.isMultiple()) {
492            // @ts-ignore
493            let tag = this.selectMultipleRootEl.lastElementChild.previousElementSibling;
494            if (tag) {
495              this.querySelector(`lit-select-option[value=${tag.value}]`)?.removeAttribute('selected');
496              tag.remove();
497              if (this.shadowRoot!.querySelectorAll('.tag').length === 0) {
498                // @ts-ignore
499                this.selectInputEl.style.width = 'auto'; // @ts-ignore
500                this.selectInputEl.placeholder = this.defaultPlaceholder;
501              }
502            }
503          } else {
504            this.clear();
505            this.dispatchEvent(new CustomEvent('onClear', { detail: ev })); //向外派发清理事件
506          } // @ts-ignore
507        } else if (ev.key === 'Enter') {
508          if (!this.canInsert) {
509            let filter = [...this.querySelectorAll('lit-select-option')].filter(
510              // @ts-ignore
511              (a: unknown) => a.style.display !== 'none'
512            );
513            if (filter.length > 0) {
514              // @ts-ignore
515              this.selectInputEl.value = filter[0].textContent; // @ts-ignore
516              this.selectInputEl.placeholder = filter[0].textContent;
517              this.blur();
518              // @ts-ignore
519              this.value = filter[0].getAttribute('value');
520              this.dispatchEvent(
521                new CustomEvent('change', {
522                  detail: {
523                    selected: true,
524                    value: filter[0].getAttribute('value'),
525                    text: filter[0].textContent,
526                  },
527                })
528              );
529            }
530          }// @ts-ignore
531        } else if (ev.key === '0' && ev.target.value.length === 1 && ev.target.value === '0') {
532          // @ts-ignore
533          ev.preventDefault();
534        }
535      }
536    };
537  }
538
539  initOptions(): void {
540    this.querySelectorAll('lit-select-option').forEach((a) => {
541      if (this.isMultiple()) {
542        a.setAttribute('check', '');
543        if (a.getAttribute('value') === this.defaultValue) {
544          let tag = this.newTag(a.getAttribute('value'), a.textContent); // @ts-ignore
545          this.selectMultipleRootEl.insertBefore(tag, this.selectInputEl); // @ts-ignore
546          this.selectInputEl.placeholder = ''; // @ts-ignore
547          this.selectInputEl.value = ''; // @ts-ignore
548          this.selectInputEl.style.width = '1px';
549          a.setAttribute('selected', '');
550        }
551      } else {
552        if (a.hasAttribute('selected')) {
553          a.removeAttribute('selected');
554        }
555        if (a.getAttribute('value') === this.defaultValue) {
556          // @ts-ignore
557          this.selectInputEl.value = a.textContent;
558          a.setAttribute('selected', '');
559        }
560      }
561      a.addEventListener('mouseup', (e) => {
562        e.stopPropagation();
563      });
564      a.addEventListener('mousedown', (e) => {
565        e.stopPropagation();
566      });
567      this.onSelectedEvent(a);
568    });
569  }
570
571  onSelectedEvent(a: Element): void {
572    a.addEventListener('onSelected', (e: unknown) => {
573      if (this.isMultiple()) {
574        if (a.hasAttribute('selected')) {
575          // @ts-ignore
576          let tag = this.shadowRoot!.querySelector(`div[data-value=${e.detail.value}]`) as HTMLElement;
577          if (tag) {
578            tag.parentElement!.removeChild(tag);
579          } // @ts-ignore
580          e.detail.selected = false;
581        } else {
582          // @ts-ignore
583          let tag = this.newTag(e.detail.value, e.detail.text); // @ts-ignore
584          this.selectMultipleRootEl.insertBefore(tag, this.selectInputEl); // @ts-ignore
585          this.selectInputEl.placeholder = ''; // @ts-ignore
586          this.selectInputEl.value = ''; // @ts-ignore
587          this.selectInputEl.style.width = '1px';
588        }
589        if (this.shadowRoot!.querySelectorAll('.tag').length === 0) {
590          // @ts-ignore
591          this.selectInputEl.style.width = 'auto'; // @ts-ignore
592          this.selectInputEl.placeholder = this.defaultPlaceholder;
593        } // @ts-ignore
594        this.selectInputEl.focus();
595      } else {
596        [...this.querySelectorAll('lit-select-option')].forEach((item) => {
597          if (item.hasAttribute('selected')) {
598            this.currentSelectedValue = item.getAttribute('value') || '';
599          }
600          item.removeAttribute('selected');
601        });
602        this.blur(); // @ts-ignore
603        this.bodyEl!.style.display = 'none';
604        // @ts-ignore
605        this.selectInputEl.value = e.detail.text;
606      }
607      if (a.getAttribute('value') === this.currentSelectedValue) {
608        a.removeAttribute('selected');
609        this.currentSelectedValue = '';
610        // @ts-ignore
611        this.selectInputEl.value = '';
612        // @ts-ignore
613        this.selectInputEl.placeholder = this.defaultPlaceholder;
614      } else {
615        this.currentSelectedValue = a.getAttribute('value') || '';
616        a.setAttribute('selected', '');
617      }
618      this.value = this.currentSelectedValue;
619      // @ts-ignore
620      this.dispatchEvent(new CustomEvent('change', { detail: { selectValue: this.currentSelectedValue, text: e.detail.text } })); //向外层派发change事件,返回当前选中项
621    });
622  }
623
624  clear(): void {
625    // @ts-ignore
626    this.selectInputEl.value = ''; // @ts-ignore
627    this.selectInputEl.placeholder = this.defaultPlaceholder;
628  }
629
630  reset(): void {
631    this.querySelectorAll('lit-select-option').forEach((a) => {
632      [...this.querySelectorAll('lit-select-option')].forEach((a) => a.removeAttribute('selected'));
633      if (a.getAttribute('value') === this.defaultValue) {
634        // @ts-ignore
635        this.selectInputEl.value = a.textContent;
636        a.setAttribute('selected', '');
637      }
638    });
639  }
640
641  disconnectedCallback(): void { }
642
643  adoptedCallback(): void { }
644
645  attributeChangedCallback(name: unknown, oldValue: unknown, newValue: unknown): void {
646    if (name === 'value' && this.selectInputEl) {
647      if (newValue) {
648        [...this.querySelectorAll('lit-select-option')].forEach((a) => {
649          if (a.getAttribute('value') === newValue) {
650            this.currentSelectedValue = a.getAttribute('value') || '';
651            a.setAttribute('selected', ''); // @ts-ignore
652            this.selectInputEl.value = a.textContent;
653          } else {
654            a.removeAttribute('selected');
655          }
656        });
657      } else {
658        this.clear();
659      }
660    }
661  }
662}
663