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