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