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