From 40daee9bc801943290976abed599e5fa8de677cf Mon Sep 17 00:00:00 2001 From: vthinkxie Date: Tue, 18 Feb 2020 11:50:02 +0800 Subject: [PATCH] feat(module:select): refactor the select to support virutal scroll close #4585 close #3497 --- .../calendar/nz-calendar-header.spec.ts | 4 +- components/checkbox/checkbox.module.ts | 4 +- components/select/demo/basic.ts | 33 +- components/select/demo/big-data.md | 15 + components/select/demo/big-data.ts | 30 + components/select/demo/border-less.md | 14 + components/select/demo/border-less.ts | 24 + components/select/demo/custom-content.md | 2 +- .../select/demo/custom-dropdown-menu.ts | 37 +- components/select/demo/custom-template.md | 6 +- components/select/demo/custom-template.ts | 26 +- components/select/demo/frontend-search.md | 15 - components/select/demo/frontend-search.ts | 46 -- components/select/demo/module | 3 +- components/select/demo/multiple.ts | 8 +- components/select/demo/optgroup.ts | 3 +- .../select/nz-option-container.component.html | 90 --- .../select/nz-option-container.component.ts | 114 ---- components/select/nz-option-container.spec.ts | 126 ---- .../select/nz-option-group.component.html | 1 - .../select/nz-option-group.component.ts | 33 - components/select/nz-option-li.component.html | 11 - components/select/nz-option-li.component.ts | 85 --- components/select/nz-option-li.spec.ts | 107 ---- components/select/nz-option.component.html | 3 - components/select/nz-option.component.ts | 34 - components/select/nz-option.pipe.spec.ts | 102 --- components/select/nz-option.pipe.ts | 55 -- .../nz-select-top-control.component.html | 149 ----- .../select/nz-select-top-control.component.ts | 161 ----- .../select/nz-select-top-control.spec.ts | 192 ------ .../nz-select-unselectable.directive.ts | 19 - .../select/nz-select-unselectable.spec.ts | 35 -- components/select/nz-select.component.html | 64 -- components/select/nz-select.component.spec.ts | 592 ------------------ components/select/nz-select.component.ts | 344 ---------- components/select/nz-select.module.ts | 53 -- components/select/nz-select.service.spec.ts | 125 ---- components/select/nz-select.service.ts | 389 ------------ .../select/option-container.component.ts | 119 ++++ components/select/option-group.component.ts | 28 + .../select/option-item-group.component.ts | 26 + components/select/option-item.component.ts | 80 +++ components/select/option.component.ts | 68 ++ components/select/public-api.ts | 23 +- components/select/select-arrow.component.ts | 34 + components/select/select-clear.component.ts | 32 + components/select/select-item.component.ts | 46 ++ .../select/select-placeholder.component.ts | 27 + components/select/select-search.component.ts | 114 ++++ .../select/select-top-control.component.ts | 244 ++++++++ components/select/select.component.ts | 532 ++++++++++++++++ components/select/select.module.ts | 72 +++ components/select/select.types.ts | 30 + components/select/style/entry.less | 1 + components/select/style/patch.less | 9 + 56 files changed, 1633 insertions(+), 3006 deletions(-) create mode 100644 components/select/demo/big-data.md create mode 100644 components/select/demo/big-data.ts create mode 100644 components/select/demo/border-less.md create mode 100644 components/select/demo/border-less.ts delete mode 100644 components/select/demo/frontend-search.md delete mode 100644 components/select/demo/frontend-search.ts delete mode 100644 components/select/nz-option-container.component.html delete mode 100644 components/select/nz-option-container.component.ts delete mode 100644 components/select/nz-option-container.spec.ts delete mode 100644 components/select/nz-option-group.component.html delete mode 100644 components/select/nz-option-group.component.ts delete mode 100644 components/select/nz-option-li.component.html delete mode 100644 components/select/nz-option-li.component.ts delete mode 100644 components/select/nz-option-li.spec.ts delete mode 100644 components/select/nz-option.component.html delete mode 100644 components/select/nz-option.component.ts delete mode 100644 components/select/nz-option.pipe.spec.ts delete mode 100644 components/select/nz-option.pipe.ts delete mode 100644 components/select/nz-select-top-control.component.html delete mode 100644 components/select/nz-select-top-control.component.ts delete mode 100644 components/select/nz-select-top-control.spec.ts delete mode 100644 components/select/nz-select-unselectable.directive.ts delete mode 100644 components/select/nz-select-unselectable.spec.ts delete mode 100644 components/select/nz-select.component.html delete mode 100644 components/select/nz-select.component.spec.ts delete mode 100644 components/select/nz-select.component.ts delete mode 100644 components/select/nz-select.module.ts delete mode 100644 components/select/nz-select.service.spec.ts delete mode 100644 components/select/nz-select.service.ts create mode 100644 components/select/option-container.component.ts create mode 100644 components/select/option-group.component.ts create mode 100644 components/select/option-item-group.component.ts create mode 100644 components/select/option-item.component.ts create mode 100644 components/select/option.component.ts create mode 100644 components/select/select-arrow.component.ts create mode 100644 components/select/select-clear.component.ts create mode 100644 components/select/select-item.component.ts create mode 100644 components/select/select-placeholder.component.ts create mode 100644 components/select/select-search.component.ts create mode 100644 components/select/select-top-control.component.ts create mode 100644 components/select/select.component.ts create mode 100644 components/select/select.module.ts create mode 100644 components/select/select.types.ts create mode 100644 components/select/style/patch.less diff --git a/components/calendar/nz-calendar-header.spec.ts b/components/calendar/nz-calendar-header.spec.ts index e183ad41001..1974a8ee81e 100644 --- a/components/calendar/nz-calendar-header.spec.ts +++ b/components/calendar/nz-calendar-header.spec.ts @@ -8,8 +8,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { CandyDate } from '../core'; import { NzI18nModule } from '../i18n/nz-i18n.module'; import { NzRadioGroupComponent as RadioGroup, NzRadioModule } from '../radio/index'; -import { NzSelectComponent as Select } from '../select/nz-select.component'; -import { NzSelectModule } from '../select/nz-select.module'; +import { NzSelectComponent as Select } from '../select/select.component'; +import { NzSelectModule } from '../select/select.module'; import { NzCalendarHeaderComponent, NzCalendarHeaderComponent as CalendarHeader } from './nz-calendar-header.component'; registerLocaleData(zh); diff --git a/components/checkbox/checkbox.module.ts b/components/checkbox/checkbox.module.ts index ef3b3ee20a3..570bfe30fdf 100644 --- a/components/checkbox/checkbox.module.ts +++ b/components/checkbox/checkbox.module.ts @@ -6,16 +6,16 @@ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE */ +import { A11yModule } from '@angular/cdk/a11y'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; - import { NzCheckboxGroupComponent } from './checkbox-group.component'; import { NzCheckboxWrapperComponent } from './checkbox-wrapper.component'; import { NzCheckboxComponent } from './checkbox.component'; @NgModule({ - imports: [CommonModule, FormsModule], + imports: [CommonModule, FormsModule, A11yModule], declarations: [NzCheckboxComponent, NzCheckboxGroupComponent, NzCheckboxWrapperComponent], exports: [NzCheckboxComponent, NzCheckboxGroupComponent, NzCheckboxWrapperComponent] }) diff --git a/components/select/demo/basic.ts b/components/select/demo/basic.ts index 629beb9bcac..f2b484d2a07 100644 --- a/components/select/demo/basic.ts +++ b/components/select/demo/basic.ts @@ -3,29 +3,28 @@ import { Component } from '@angular/core'; @Component({ selector: 'nz-demo-select-basic', template: ` -
- - - - - - - - - - - -
+ + + + + + + + + + + + + + `, styles: [ ` nz-select { - margin-right: 8px; + margin: 0 8px 10px 0; width: 120px; } ` ] }) -export class NzDemoSelectBasicComponent { - selectedValue = 'lucy'; -} +export class NzDemoSelectBasicComponent {} diff --git a/components/select/demo/big-data.md b/components/select/demo/big-data.md new file mode 100644 index 00000000000..19dad36d886 --- /dev/null +++ b/components/select/demo/big-data.md @@ -0,0 +1,15 @@ +--- +order: 20 +title: + zh-CN: 大量数据 + en-US: Large amounts of data +--- + +## zh-CN + +组件使用了虚拟滚动技术,可以同时处理大量数据。 + +## en-US + +With the help of virtual scroll, select component can deal with Large amounts of data. + diff --git a/components/select/demo/big-data.ts b/components/select/demo/big-data.ts new file mode 100644 index 00000000000..48f55d1f0e6 --- /dev/null +++ b/components/select/demo/big-data.ts @@ -0,0 +1,30 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'nz-demo-select-big-data', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + `, + styles: [ + ` + nz-select { + width: 100%; + } + ` + ] +}) +export class NzDemoSelectBigDataComponent implements OnInit { + listOfOption: string[] = []; + listOfSelectedValue = ['a10', 'c12']; + + ngOnInit(): void { + const children: string[] = []; + for (let i = 10; i < 100000; i++) { + children.push(`${i.toString(36)}${i}`); + } + this.listOfOption = children; + } +} diff --git a/components/select/demo/border-less.md b/components/select/demo/border-less.md new file mode 100644 index 00000000000..314ca6cfce9 --- /dev/null +++ b/components/select/demo/border-less.md @@ -0,0 +1,14 @@ +--- +order: 21 +title: + zh-CN: 无边框 + en-US: Bordered-less +--- + +## zh-CN + +无边框样式。 + +## en-US + +Bordered-less style component. diff --git a/components/select/demo/border-less.ts b/components/select/demo/border-less.ts new file mode 100644 index 00000000000..db2139cbf57 --- /dev/null +++ b/components/select/demo/border-less.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-select-border-less', + template: ` + + + + + + + + + `, + styles: [ + ` + nz-select { + margin: 0 8px 10px 0; + width: 120px; + } + ` + ] +}) +export class NzDemoSelectBorderLessComponent {} diff --git a/components/select/demo/custom-content.md b/components/select/demo/custom-content.md index c120f6fe44e..93a73bfac4d 100644 --- a/components/select/demo/custom-content.md +++ b/components/select/demo/custom-content.md @@ -7,7 +7,7 @@ title: ## zh-CN -通过 `nzCustomContent` 自定义 nz-option 显示的内容。 +通过 `nzCustomContent` 自定义下拉菜单选项显示的内容。 ## en-US diff --git a/components/select/demo/custom-dropdown-menu.ts b/components/select/demo/custom-dropdown-menu.ts index 26034eac93d..9cdedd78da3 100644 --- a/components/select/demo/custom-dropdown-menu.ts +++ b/components/select/demo/custom-dropdown-menu.ts @@ -3,30 +3,47 @@ import { Component } from '@angular/core'; @Component({ selector: 'nz-demo-select-custom-dropdown-menu', template: ` - - - + + - + -
Add item
+
+ + Add item +
`, styles: [ ` nz-select { - width: 120px; + width: 240px; } - nz-divider { margin: 4px 0; } - + .container { + display: flex; + flex-wrap: nowrap; + padding: 8px; + } + input { + } .add-item { + flex: 0 0 auto; padding: 8px; - cursor: pointer; + display: block; } ` ] }) -export class NzDemoSelectCustomDropdownMenuComponent {} +export class NzDemoSelectCustomDropdownMenuComponent { + listOfItem = ['jack', 'lucy']; + index = 0; + addItem(input: HTMLInputElement): void { + const value = input.value; + if (this.listOfItem.indexOf(value) === -1) { + this.listOfItem = [...this.listOfItem, input.value || `New item ${this.index++}`]; + } + } +} diff --git a/components/select/demo/custom-template.md b/components/select/demo/custom-template.md index 19f0fc79b58..cb6f146bd66 100644 --- a/components/select/demo/custom-template.md +++ b/components/select/demo/custom-template.md @@ -1,8 +1,8 @@ --- -order: 18 +order: 22 title: - zh-CN: 自定义选择器内容 - en-US: Custom Select Template + zh-CN: 自定义选择标签 + en-US: Custom Top Render --- ## zh-CN diff --git a/components/select/demo/custom-template.ts b/components/select/demo/custom-template.ts index c8b9023c184..c1c45e1b3c0 100644 --- a/components/select/demo/custom-template.ts +++ b/components/select/demo/custom-template.ts @@ -3,23 +3,29 @@ import { Component } from '@angular/core'; @Component({ selector: 'nz-demo-select-custom-template', template: ` - - Windows - Mac - Android + + + + - - Label: {{ selected.nzLabel }} Value: {{ selected.nzValue }} + {{ selected.nzLabel }} +
+
+ + + + + + +
{{ selected.nzLabel }}
`, styles: [ ` nz-select { - width: 200px; + width: 100%; } ` ] }) -export class NzDemoSelectCustomTemplateComponent { - selectedOS = null; -} +export class NzDemoSelectCustomTemplateComponent {} diff --git a/components/select/demo/frontend-search.md b/components/select/demo/frontend-search.md deleted file mode 100644 index a2623c38ef0..00000000000 --- a/components/select/demo/frontend-search.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -order: 20 -title: - zh-CN: 前端搜索 - en-US: Select with front-end search ---- - -## zh-CN - -当数据量过大时,在前端根据搜索关键字对数据进行处理后二次展示 - -## en-US - -Search the options in the frontend. - diff --git a/components/select/demo/frontend-search.ts b/components/select/demo/frontend-search.ts deleted file mode 100644 index 03c72e29c37..00000000000 --- a/components/select/demo/frontend-search.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'nz-demo-select-frontend-search', - template: ` - - - - Type at lease 2 letters to Search - - - `, - styles: [ - ` - nz-select { - width: 100%; - } - ` - ] -}) -export class NzDemoSelectFrontendSearchComponent { - bigList: string[] = new Array(10000).fill(0).map((_, i) => i.toString(36) + i); - optionList: string[] = []; - selectedUser: string; - displayTips = true; - - onSearch(value: string): void { - if (value && value.length > 1) { - this.optionList = this.bigList.filter(item => item.indexOf(value) > -1); - this.displayTips = false; - } else { - this.optionList = []; - this.displayTips = true; - } - } - - constructor() {} -} diff --git a/components/select/demo/module b/components/select/demo/module index 500ad811341..b993db63f77 100644 --- a/components/select/demo/module +++ b/components/select/demo/module @@ -2,7 +2,8 @@ import { NzSelectModule } from 'ng-zorro-antd/select'; import { NzDividerModule } from 'ng-zorro-antd/divider'; import { NzRadioModule } from 'ng-zorro-antd/radio'; import { NzIconModule } from 'ng-zorro-antd/icon'; +import { NzInputModule } from 'ng-zorro-antd/input'; import { FormsModule } from '@angular/forms'; import { HttpClientJsonpModule } from '@angular/common/http'; -export const moduleList = [ FormsModule, HttpClientJsonpModule, NzSelectModule, NzDividerModule, NzRadioModule, NzIconModule ]; +export const moduleList = [ FormsModule, HttpClientJsonpModule, NzSelectModule, NzDividerModule, NzRadioModule, NzIconModule, NzInputModule ]; diff --git a/components/select/demo/multiple.ts b/components/select/demo/multiple.ts index 9f259c5fe42..7fc7943e369 100644 --- a/components/select/demo/multiple.ts +++ b/components/select/demo/multiple.ts @@ -10,7 +10,7 @@ import { Component, OnInit } from '@angular/core'; nzPlaceHolder="Please select" [(ngModel)]="listOfSelectedValue" > - +
and {{ selectedList.length }} more selected `, @@ -23,13 +23,13 @@ import { Component, OnInit } from '@angular/core'; ] }) export class NzDemoSelectMultipleComponent implements OnInit { - listOfOption: Array<{ label: string; value: string }> = []; + listOfOption: string[] = []; listOfSelectedValue = ['a10', 'c12']; ngOnInit(): void { - const children: Array<{ label: string; value: string }> = []; + const children: string[] = []; for (let i = 10; i < 36; i++) { - children.push({ label: i.toString(36) + i, value: i.toString(36) + i }); + children.push(`${i.toString(36)}${i}`); } this.listOfOption = children; } diff --git a/components/select/demo/optgroup.ts b/components/select/demo/optgroup.ts index 4c2cb571c9f..43cb60b219b 100644 --- a/components/select/demo/optgroup.ts +++ b/components/select/demo/optgroup.ts @@ -9,8 +9,9 @@ import { Component } from '@angular/core'; - + +
`, styles: [ diff --git a/components/select/nz-option-container.component.html b/components/select/nz-option-container.component.html deleted file mode 100644 index e8ef7b7957c..00000000000 --- a/components/select/nz-option-container.component.html +++ /dev/null @@ -1,90 +0,0 @@ - diff --git a/components/select/nz-option-container.component.ts b/components/select/nz-option-container.component.ts deleted file mode 100644 index 7791709e021..00000000000 --- a/components/select/nz-option-container.component.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * @license - * Copyright Alibaba.com All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE - */ - -import { - AfterViewInit, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ElementRef, - EventEmitter, - Input, - NgZone, - OnDestroy, - OnInit, - Output, - QueryList, - TemplateRef, - ViewChild, - ViewChildren, - ViewEncapsulation -} from '@angular/core'; -import { fromEvent, Subject } from 'rxjs'; -import { filter, map, pairwise, takeUntil } from 'rxjs/operators'; -import { NzOptionGroupComponent } from './nz-option-group.component'; -import { NzOptionLiComponent } from './nz-option-li.component'; -import { NzOptionComponent } from './nz-option.component'; -import { NzSelectService } from './nz-select.service'; - -@Component({ - selector: '[nz-option-container]', - exportAs: 'nzOptionContainer', - changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None, - preserveWhitespaces: false, - templateUrl: './nz-option-container.component.html' -}) -export class NzOptionContainerComponent implements OnDestroy, OnInit, AfterViewInit { - private destroy$ = new Subject(); - private lastScrollTop = 0; - @ViewChildren(NzOptionLiComponent) listOfNzOptionLiComponent: QueryList; - @ViewChild('dropdownUl', { static: true }) dropdownUl: ElementRef; - @Input() nzNotFoundContent: string; - @Input() nzMenuItemSelectedIcon: TemplateRef; - @Output() readonly nzScrollToBottom = new EventEmitter(); - - scrollIntoViewIfNeeded(option: NzOptionComponent): void { - // delay after open - setTimeout(() => { - if (this.listOfNzOptionLiComponent && this.listOfNzOptionLiComponent.length && option) { - const targetOption = this.listOfNzOptionLiComponent.find(o => this.nzSelectService.compareWith(o.nzOption.nzValue, option.nzValue)); - // tslint:disable:no-any - if (targetOption && targetOption.el && (targetOption.el as any).scrollIntoViewIfNeeded) { - (targetOption.el as any).scrollIntoViewIfNeeded(false); - } - } - }); - } - - trackLabel(_index: number, option: NzOptionGroupComponent): string | TemplateRef { - return option.nzLabel; - } - - // tslint:disable-next-line:no-any - trackValue(_index: number, option: NzOptionComponent): any { - return option.nzValue; - } - - constructor(public nzSelectService: NzSelectService, public cdr: ChangeDetectorRef, private ngZone: NgZone) {} - - ngOnInit(): void { - this.nzSelectService.activatedOption$.pipe(takeUntil(this.destroy$)).subscribe(option => { - this.scrollIntoViewIfNeeded(option!); - }); - this.nzSelectService.check$.pipe(takeUntil(this.destroy$)).subscribe(() => { - this.cdr.markForCheck(); - }); - this.ngZone.runOutsideAngular(() => { - const ul = this.dropdownUl.nativeElement; - fromEvent(ul, 'scroll') - .pipe(takeUntil(this.destroy$)) - .subscribe(e => { - e.preventDefault(); - e.stopPropagation(); - if (ul && ul.scrollTop > this.lastScrollTop && ul.scrollHeight < ul.clientHeight + ul.scrollTop + 10) { - this.lastScrollTop = ul.scrollTop; - this.ngZone.run(() => { - this.nzScrollToBottom.emit(); - }); - } - }); - }); - } - - ngAfterViewInit(): void { - this.listOfNzOptionLiComponent.changes - .pipe( - map(list => list.length), - pairwise(), - filter(([before, after]) => after < before), - takeUntil(this.destroy$) - ) - .subscribe(() => (this.lastScrollTop = 0)); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } -} diff --git a/components/select/nz-option-container.spec.ts b/components/select/nz-option-container.spec.ts deleted file mode 100644 index 8561e847ec3..00000000000 --- a/components/select/nz-option-container.spec.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Component, DebugElement, QueryList, ViewEncapsulation } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ReplaySubject, Subject } from 'rxjs'; - -import { NzOptionContainerComponent } from './nz-option-container.component'; -import { NzOptionGroupComponent } from './nz-option-group.component'; -import { NzOptionComponent } from './nz-option.component'; -import { defaultFilterOption } from './nz-option.pipe'; -import { NzSelectModule } from './nz-select.module'; -import { NzSelectService } from './nz-select.service'; - -export const createListOfOption = (count: number, prefix = 'option') => { - const list: NzOptionComponent[] = []; - for (let i = 0; i < count; i++) { - const option = new NzOptionComponent(); - option.nzValue = `${prefix}_value_${i}`; - option.nzLabel = `${prefix}_label_${i}`; - list.push(option); - } - return list; -}; - -export const createListOfGroupOption = (groupCount: number, optionCount: number) => { - const list: NzOptionGroupComponent[] = []; - for (let i = 0; i < groupCount; i++) { - const queryList = new QueryList(); - queryList.reset(createListOfOption(optionCount, `${i}_inner_option`)); - const option = new NzOptionGroupComponent(); - option.nzLabel = `group_label_${i}`; - option.listOfNzOptionComponent = queryList; - list.push(option); - } - return list; -}; - -describe('nz-select option container', () => { - beforeEach(fakeAsync(() => { - let nzSelectServiceStub: Partial; - nzSelectServiceStub = { - searchValue: '', - filterOption: defaultFilterOption, - serverSearch: false, - listOfNzOptionComponent: createListOfOption(20), - check$: new Subject(), - activatedOption$: new ReplaySubject(1), - listOfNzOptionGroupComponent: createListOfGroupOption(10, 10), - listOfSelectedValue$: new Subject(), - compareWith: (o1, o2) => o1 === o2 - }; - TestBed.configureTestingModule({ - imports: [NzSelectModule, NoopAnimationsModule], - providers: [{ provide: NzSelectService, useValue: nzSelectServiceStub }], - declarations: [NzOptionContainerSpecComponent] - }); - TestBed.compileComponents(); - })); - describe('default', () => { - let fixture: ComponentFixture; - let testComponent: NzOptionContainerSpecComponent; - let oc: DebugElement; - - beforeEach(() => { - fixture = TestBed.createComponent(NzOptionContainerSpecComponent); - fixture.detectChanges(); - testComponent = fixture.debugElement.componentInstance; - oc = fixture.debugElement.query(By.directive(NzOptionContainerComponent)); - }); - // it('should scrollToBottom emit', () => { - // fixture.detectChanges(); - // expect(testComponent.scrollToBottom).toHaveBeenCalledTimes(0); - // const ul = oc.injector.get(NzOptionContainerComponent).dropdownUl.nativeElement; - // ul.scrollTop = ul.scrollHeight - ul.clientHeight; - // dispatchFakeEvent(ul, 'scroll'); - // fixture.detectChanges(); - // expect(testComponent.scrollToBottom).toHaveBeenCalledTimes(1); - // }); - // it('should scrollIntoViewIfNeeded', fakeAsync(() => { - // fixture.detectChanges(); - // const nzSelectService = fixture.debugElement.injector.get(NzSelectService); - // nzSelectService.activatedOption$.next(nzSelectService.listOfNzOptionComponent[nzSelectService.listOfNzOptionComponent.length - 1]); - // fixture.detectChanges(); - // tick(); - // fixture.detectChanges(); - // const ul = oc.injector.get(NzOptionContainerComponent).dropdownUl.nativeElement; - // expect(ul.scrollTop).toBeGreaterThan(0); - // })); - it('should destroy piped', () => { - fixture.detectChanges(); - const checkSpy = spyOn(oc.injector.get(NzOptionContainerComponent).cdr, 'markForCheck'); - fixture.detectChanges(); - expect(checkSpy).toHaveBeenCalledTimes(0); - const nzSelectService = fixture.debugElement.injector.get(NzSelectService); - // TODO: observable does not have next method. - (nzSelectService.check$ as any).next(); // tslint:disable-line:no-any - fixture.detectChanges(); - expect(checkSpy).toHaveBeenCalledTimes(1); - testComponent.destroy = true; - fixture.detectChanges(); - (nzSelectService.check$ as any).next(); // tslint:disable-line:no-any - fixture.detectChanges(); - expect(checkSpy).toHaveBeenCalledTimes(1); - }); - }); -}); - -@Component({ - template: ` -
- icon - `, - encapsulation: ViewEncapsulation.None, - styleUrls: ['../style/index.less', './style/index.less'] -}) -export class NzOptionContainerSpecComponent { - destroy = false; - scrollToBottom = jasmine.createSpy('scrollToBottom callback'); - notFoundContent = 'not found'; -} diff --git a/components/select/nz-option-group.component.html b/components/select/nz-option-group.component.html deleted file mode 100644 index 6dbc7430638..00000000000 --- a/components/select/nz-option-group.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/components/select/nz-option-group.component.ts b/components/select/nz-option-group.component.ts deleted file mode 100644 index 30a5ade3ca3..00000000000 --- a/components/select/nz-option-group.component.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @license - * Copyright Alibaba.com All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE - */ - -import { ChangeDetectionStrategy, Component, ContentChildren, Input, QueryList, TemplateRef, ViewEncapsulation } from '@angular/core'; -import { NzOptionComponent } from './nz-option.component'; - -@Component({ - selector: 'nz-option-group', - exportAs: 'nzOptionGroup', - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: './nz-option-group.component.html' -}) -export class NzOptionGroupComponent { - isLabelString = false; - label: string | TemplateRef; - @ContentChildren(NzOptionComponent) listOfNzOptionComponent: QueryList; - - @Input() - set nzLabel(value: string | TemplateRef) { - this.label = value; - this.isLabelString = !(this.nzLabel instanceof TemplateRef); - } - - get nzLabel(): string | TemplateRef { - return this.label; - } -} diff --git a/components/select/nz-option-li.component.html b/components/select/nz-option-li.component.html deleted file mode 100644 index da491e202d1..00000000000 --- a/components/select/nz-option-li.component.html +++ /dev/null @@ -1,11 +0,0 @@ - - {{ nzOption.nzLabel }} - - - - diff --git a/components/select/nz-option-li.component.ts b/components/select/nz-option-li.component.ts deleted file mode 100644 index eb7ed3ac838..00000000000 --- a/components/select/nz-option-li.component.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @license - * Copyright Alibaba.com All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE - */ - -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ElementRef, - Input, - OnDestroy, - OnInit, - Renderer2, - TemplateRef, - ViewEncapsulation -} from '@angular/core'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; - -import { isNotNil } from 'ng-zorro-antd/core'; - -import { NzOptionComponent } from './nz-option.component'; -import { NzSelectService } from './nz-select.service'; - -@Component({ - selector: '[nz-option-li]', - exportAs: 'nzOptionLi', - templateUrl: './nz-option-li.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None, - host: { - '[class.ant-select-dropdown-menu-item-selected]': 'selected && !nzOption.nzDisabled', - '[class.ant-select-dropdown-menu-item-disabled]': 'nzOption.nzDisabled', - '[class.ant-select-dropdown-menu-item-active]': 'active && !nzOption.nzDisabled', - '[attr.unselectable]': '"unselectable"', - '[style.user-select]': '"none"', - '(click)': 'clickOption()', - '(mousedown)': '$event.preventDefault()' - } -}) -export class NzOptionLiComponent implements OnInit, OnDestroy { - el: HTMLElement = this.elementRef.nativeElement; - selected = false; - active = false; - destroy$ = new Subject(); - @Input() nzOption: NzOptionComponent; - @Input() nzMenuItemSelectedIcon: TemplateRef; - - clickOption(): void { - this.nzSelectService.clickOption(this.nzOption); - } - - constructor( - private elementRef: ElementRef, - public nzSelectService: NzSelectService, - private cdr: ChangeDetectorRef, - renderer: Renderer2 - ) { - renderer.addClass(elementRef.nativeElement, 'ant-select-dropdown-menu-item'); - } - - ngOnInit(): void { - this.nzSelectService.listOfSelectedValue$.pipe(takeUntil(this.destroy$)).subscribe(list => { - this.selected = isNotNil(list.find(v => this.nzSelectService.compareWith(v, this.nzOption.nzValue))); - this.cdr.markForCheck(); - }); - this.nzSelectService.activatedOption$.pipe(takeUntil(this.destroy$)).subscribe(option => { - if (option) { - this.active = this.nzSelectService.compareWith(option.nzValue, this.nzOption.nzValue); - } else { - this.active = false; - } - this.cdr.markForCheck(); - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } -} diff --git a/components/select/nz-option-li.spec.ts b/components/select/nz-option-li.spec.ts deleted file mode 100644 index 78f786e3fe4..00000000000 --- a/components/select/nz-option-li.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Component, DebugElement } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { ReplaySubject, Subject } from 'rxjs'; - -import { dispatchFakeEvent } from 'ng-zorro-antd/core'; - -import { NzOptionLiComponent } from './nz-option-li.component'; -import { NzOptionComponent } from './nz-option.component'; -import { NzSelectService } from './nz-select.service'; - -describe('select option li', () => { - beforeEach(fakeAsync(() => { - let nzSelectServiceStub: Partial; - nzSelectServiceStub = { - activatedOption$: new ReplaySubject(1), - listOfSelectedValue$: new Subject(), - compareWith: (o1, o2) => o1 === o2, - clickOption: () => {} - }; - TestBed.configureTestingModule({ - providers: [{ provide: NzSelectService, useValue: nzSelectServiceStub }], - declarations: [NzTestSelectOptionLiComponent, NzOptionLiComponent] - }); - TestBed.compileComponents(); - })); - describe('default', () => { - let fixture: ComponentFixture; - let testComponent: NzTestSelectOptionLiComponent; - let li: DebugElement; - let liComponent: NzOptionLiComponent; - let nzSelectService: NzSelectService; - beforeEach(() => { - fixture = TestBed.createComponent(NzTestSelectOptionLiComponent); - fixture.detectChanges(); - testComponent = fixture.debugElement.componentInstance; - li = fixture.debugElement.query(By.directive(NzOptionLiComponent)); - liComponent = li.injector.get(NzOptionLiComponent); - nzSelectService = fixture.debugElement.injector.get(NzSelectService); - }); - it('should selected work', () => { - fixture.detectChanges(); - expect(liComponent.selected).toBe(false); - // @ts-ignore - nzSelectService.listOfSelectedValue$.next(['01_value']); - fixture.detectChanges(); - expect(liComponent.selected).toBe(true); - // @ts-ignore - nzSelectService.listOfSelectedValue$.next(['01_label']); - fixture.detectChanges(); - expect(liComponent.selected).toBe(false); - }); - it('should active work', () => { - fixture.detectChanges(); - expect(liComponent.active).toBe(false); - const option01 = new NzOptionComponent(); - option01.nzLabel = '01_label'; - option01.nzValue = '01_value'; - nzSelectService.activatedOption$.next(option01); - fixture.detectChanges(); - expect(liComponent.active).toBe(true); - nzSelectService.activatedOption$.next(null); - fixture.detectChanges(); - expect(liComponent.active).toBe(false); - }); - it('should destroy piped', () => { - fixture.detectChanges(); - // @ts-ignore - const checkSpy = spyOn(liComponent.cdr, 'markForCheck'); - expect(checkSpy).toHaveBeenCalledTimes(0); - // @ts-ignore - nzSelectService.listOfSelectedValue$.next(['01_value']); - fixture.detectChanges(); - expect(checkSpy).toHaveBeenCalledTimes(1); - testComponent.destroy = true; - fixture.detectChanges(); - // @ts-ignore - nzSelectService.listOfSelectedValue$.next(['01_value']); - fixture.detectChanges(); - expect(checkSpy).toHaveBeenCalledTimes(1); - }); - it('should host click trigger', () => { - fixture.detectChanges(); - const clickSpy = spyOn(nzSelectService, 'clickOption'); - fixture.detectChanges(); - expect(clickSpy).toHaveBeenCalledTimes(0); - dispatchFakeEvent(li.nativeElement, 'click'); - expect(clickSpy).toHaveBeenCalledTimes(1); - }); - }); -}); - -@Component({ - template: ` -
  • - icon - ` -}) -export class NzTestSelectOptionLiComponent { - option = new NzOptionComponent(); - destroy = false; - - constructor() { - this.option.nzLabel = '01_label'; - this.option.nzValue = '01_value'; - } -} diff --git a/components/select/nz-option.component.html b/components/select/nz-option.component.html deleted file mode 100644 index a4bd3d8f63e..00000000000 --- a/components/select/nz-option.component.html +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/components/select/nz-option.component.ts b/components/select/nz-option.component.ts deleted file mode 100644 index 4ceece0bf7d..00000000000 --- a/components/select/nz-option.component.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright Alibaba.com All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE - */ - -import { ChangeDetectionStrategy, Component, Input, OnChanges, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core'; - -import { InputBoolean } from 'ng-zorro-antd/core'; -import { Subject } from 'rxjs'; - -@Component({ - selector: 'nz-option', - exportAs: 'nzOption', - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: './nz-option.component.html' -}) -export class NzOptionComponent implements OnChanges { - changes = new Subject(); - @ViewChild(TemplateRef, { static: false }) template: TemplateRef; - @Input() nzLabel: string; - // tslint:disable-next-line:no-any - @Input() nzValue: any; - @Input() @InputBoolean() nzDisabled = false; - @Input() @InputBoolean() nzHide = false; - @Input() @InputBoolean() nzCustomContent = false; - - ngOnChanges(): void { - this.changes.next(); - } -} diff --git a/components/select/nz-option.pipe.spec.ts b/components/select/nz-option.pipe.spec.ts deleted file mode 100644 index fe3b9ab4577..00000000000 --- a/components/select/nz-option.pipe.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { QueryList } from '@angular/core'; -import { NzOptionGroupComponent } from './nz-option-group.component'; -import { NzOptionComponent } from './nz-option.component'; -import { defaultFilterOption, NzFilterGroupOptionPipe, NzFilterOptionPipe } from './nz-option.pipe'; - -// tslint:disable-next-line:no-any -function generateOption(value: any, label: string): NzOptionComponent { - const option = new NzOptionComponent(); - option.nzValue = value; - option.nzLabel = label; - return option; -} - -function generateGroupOption(label: string, value: NzOptionComponent[]): NzOptionGroupComponent { - const optionGroup = new NzOptionGroupComponent(); - const queryList = new QueryList(); - queryList.reset(value); - optionGroup.listOfNzOptionComponent = queryList; - optionGroup.nzLabel = label; - return optionGroup; -} - -describe('nz-option pipe', () => { - describe('NzFilterOptionPipe', () => { - let pipe: NzFilterOptionPipe; - let listOfOption: NzOptionComponent[]; - beforeEach(() => { - pipe = new NzFilterOptionPipe(); - listOfOption = []; - for (let i = 0; i < 10; i++) { - listOfOption.push(generateOption(`value${i}`, `label${i}`)); - } - }); - it('should return correct value with inputValue', () => { - const result = pipe.transform(listOfOption, '9', defaultFilterOption, false); - expect(result[0].nzLabel).toBe('label9'); - expect(result.length).toBe(1); - }); - it('should return correct value with null option', () => { - const result = pipe.transform([new NzOptionComponent()], 'a', defaultFilterOption, false); - expect(result.length).toBe(0); - }); - it('should return correct value with filterOption', () => { - const filterOption = (input: string, option: NzOptionComponent) => { - if (option && option.nzLabel) { - return option.nzLabel.toLowerCase().indexOf(input.toLowerCase().replace('9', '8')) > -1; - } else { - return false; - } - }; - const result = pipe.transform(listOfOption, '9', filterOption, false); - expect(result[0].nzLabel).toBe('label8'); - expect(result.length).toBe(1); - }); - it('should return correct value without inputValue', () => { - const result = pipe.transform(listOfOption, '', defaultFilterOption, false); - expect(result.length).toBe(10); - }); - it('should return correct value with serverSearch', () => { - const result = pipe.transform(listOfOption, 'absd', defaultFilterOption, true); - expect(result.length).toBe(10); - }); - }); - describe('NzFilterGroupOptionPipe', () => { - let pipe: NzFilterGroupOptionPipe; - let listOfGroupOption: NzOptionGroupComponent[]; - beforeEach(() => { - pipe = new NzFilterGroupOptionPipe(); - listOfGroupOption = [ - generateGroupOption('g1', [generateOption('a', 'a'), generateOption('b', 'b')]), - generateGroupOption('g2', [generateOption('b', 'b'), generateOption('c', 'c')]) - ]; - }); - it('should return correct value with inputValue', () => { - const result01 = pipe.transform(listOfGroupOption, 'a', defaultFilterOption, false); - expect(result01[0].nzLabel).toBe('g1'); - expect(result01.length).toBe(1); - const result02 = pipe.transform(listOfGroupOption, 'b', defaultFilterOption, false); - expect(result02.length).toBe(2); - }); - it('should return correct value with filterOption', () => { - const filterOption = (input: string, option: NzOptionComponent) => { - if (option && option.nzLabel) { - return option.nzLabel.toLowerCase().indexOf(input.toLowerCase().replace('a', 'c')) > -1; - } else { - return false; - } - }; - const result = pipe.transform(listOfGroupOption, 'a', filterOption, false); - expect(result[0].nzLabel).toBe('g2'); - expect(result.length).toBe(1); - }); - it('should return correct value without inputValue', () => { - const result = pipe.transform(listOfGroupOption, '', defaultFilterOption, false); - expect(result.length).toBe(2); - }); - it('should return correct value with serverSearch', () => { - const result = pipe.transform(listOfGroupOption, 'absd', defaultFilterOption, true); - expect(result.length).toBe(2); - }); - }); -}); diff --git a/components/select/nz-option.pipe.ts b/components/select/nz-option.pipe.ts deleted file mode 100644 index 577ae772091..00000000000 --- a/components/select/nz-option.pipe.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @license - * Copyright Alibaba.com All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE - */ - -import { Pipe, PipeTransform, QueryList } from '@angular/core'; -import { NzOptionGroupComponent } from './nz-option-group.component'; -import { NzOptionComponent } from './nz-option.component'; - -export type TFilterOption = (input: string, option: NzOptionComponent) => boolean; - -@Pipe({ name: 'nzFilterOption' }) -export class NzFilterOptionPipe implements PipeTransform { - transform( - options: NzOptionComponent[] | QueryList, - searchValue: string, - filterOption: TFilterOption, - serverSearch: boolean - ): NzOptionComponent[] { - if (serverSearch || !searchValue) { - return options as NzOptionComponent[]; - } else { - return (options as NzOptionComponent[]).filter(o => filterOption(searchValue, o)); - } - } -} - -@Pipe({ name: 'nzFilterGroupOption' }) -export class NzFilterGroupOptionPipe implements PipeTransform { - transform( - groups: NzOptionGroupComponent[], - searchValue: string, - filterOption: TFilterOption, - serverSearch: boolean - ): NzOptionGroupComponent[] { - if (serverSearch || !searchValue) { - return groups; - } else { - return (groups as NzOptionGroupComponent[]).filter(g => { - return g.listOfNzOptionComponent.some(o => filterOption(searchValue, o)); - }); - } - } -} - -export function defaultFilterOption(searchValue: string, option: NzOptionComponent): boolean { - if (option && option.nzLabel) { - return option.nzLabel.toLowerCase().indexOf(searchValue.toLowerCase()) > -1; - } else { - return false; - } -} diff --git a/components/select/nz-select-top-control.component.html b/components/select/nz-select-top-control.component.html deleted file mode 100644 index 6a61932108e..00000000000 --- a/components/select/nz-select-top-control.component.html +++ /dev/null @@ -1,149 +0,0 @@ - - - - -
    -
    - {{ nzPlaceHolder }} -
    - - - -
    - - {{ nzSelectService.listOfCachedSelectedOption[0]?.nzLabel }} - -
    - - -
    - -
      - -
    • - -
      {{ option.nzLabel }}
      -
      - - - -
    • -
      -
    • -
      - - - - - - + {{ nzSelectService.listOfCachedSelectedOption.length - nzMaxTagCount }} ... - -
      -
    • - -
    -
    - - - - - - - - - diff --git a/components/select/nz-select-top-control.component.ts b/components/select/nz-select-top-control.component.ts deleted file mode 100644 index 9331770ac79..00000000000 --- a/components/select/nz-select-top-control.component.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * @license - * Copyright Alibaba.com All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE - */ - -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ElementRef, - Host, - Input, - OnDestroy, - OnInit, - Optional, - Renderer2, - TemplateRef, - ViewChild, - ViewEncapsulation -} from '@angular/core'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; - -import { NzNoAnimationDirective, zoomMotion } from 'ng-zorro-antd/core'; - -import { NzOptionComponent } from './nz-option.component'; -import { NzSelectService } from './nz-select.service'; - -@Component({ - selector: '[nz-select-top-control]', - exportAs: 'nzSelectTopControl', - preserveWhitespaces: false, - animations: [zoomMotion], - changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None, - templateUrl: './nz-select-top-control.component.html' -}) -export class NzSelectTopControlComponent implements OnInit, OnDestroy { - inputValue: string; - isComposing = false; - private destroy$ = new Subject(); - @ViewChild('inputElement', { static: false }) inputElement: ElementRef; - @ViewChild('mirrorElement', { static: false }) mirrorElement: ElementRef; - @Input() nzShowSearch = false; - @Input() nzPlaceHolder: string; - @Input() nzOpen = false; - @Input() nzMaxTagCount: number; - @Input() nzAllowClear = false; - @Input() nzShowArrow = true; - @Input() nzLoading = false; - @Input() nzCustomTemplate: TemplateRef<{ $implicit: NzOptionComponent }>; - @Input() nzSuffixIcon: TemplateRef; - @Input() nzClearIcon: TemplateRef; - @Input() nzRemoveIcon: TemplateRef; - // tslint:disable-next-line:no-any - @Input() nzMaxTagPlaceholder: TemplateRef<{ $implicit: any[] }>; - @Input() nzTokenSeparators: string[] = []; - - onClearSelection(e: MouseEvent): void { - e.stopPropagation(); - this.nzSelectService.updateListOfSelectedValue([], true); - } - - setInputValue(value: string): void { - /** fix clear value https://github.com/NG-ZORRO/ng-zorro-antd/issues/3825 **/ - if (this.inputDOM && !value) { - this.inputDOM.value = value; - } - this.inputValue = value; - this.updateWidth(); - this.nzSelectService.updateSearchValue(value); - this.nzSelectService.tokenSeparate(this.inputValue, this.nzTokenSeparators); - } - - get mirrorDOM(): HTMLElement { - return this.mirrorElement && this.mirrorElement.nativeElement; - } - - get inputDOM(): HTMLInputElement { - return this.inputElement && this.inputElement.nativeElement; - } - - get placeHolderDisplay(): string { - return this.inputValue || this.isComposing || this.nzSelectService.listOfSelectedValue.length ? 'none' : 'block'; - } - - get selectedValueStyle(): { [key: string]: string } { - let showSelectedValue = false; - let opacity = 1; - if (!this.nzShowSearch) { - showSelectedValue = true; - } else { - if (this.nzOpen) { - showSelectedValue = !(this.inputValue || this.isComposing); - if (showSelectedValue) { - opacity = 0.4; - } - } else { - showSelectedValue = true; - } - } - return { - display: showSelectedValue ? 'block' : 'none', - opacity: `${opacity}` - }; - } - - // tslint:disable-next-line:no-any - trackValue(_index: number, option: NzOptionComponent): any { - return option.nzValue; - } - - updateWidth(): void { - if (this.mirrorDOM && this.inputDOM && this.inputDOM.value) { - this.mirrorDOM.innerText = `${this.inputDOM.value} `; - this.renderer.removeStyle(this.inputDOM, 'width'); - this.renderer.setStyle(this.inputDOM, 'width', `${this.mirrorDOM.clientWidth}px`); - } else if (this.inputDOM) { - this.renderer.removeStyle(this.inputDOM, 'width'); - this.mirrorDOM.innerText = ''; - } - } - - removeSelectedValue(option: NzOptionComponent, e: MouseEvent): void { - this.nzSelectService.removeValueFormSelected(option); - e.stopPropagation(); - } - - animationEnd(): void { - this.nzSelectService.animationEvent$.next(); - } - - constructor( - private renderer: Renderer2, - public nzSelectService: NzSelectService, - private cdr: ChangeDetectorRef, - @Host() @Optional() public noAnimation?: NzNoAnimationDirective - ) {} - - ngOnInit(): void { - this.nzSelectService.open$.pipe(takeUntil(this.destroy$)).subscribe(open => { - if (this.inputElement && open) { - setTimeout(() => this.inputDOM.focus()); - } - }); - this.nzSelectService.clearInput$.pipe(takeUntil(this.destroy$)).subscribe(() => { - this.setInputValue(''); - }); - this.nzSelectService.check$.pipe(takeUntil(this.destroy$)).subscribe(() => { - this.cdr.markForCheck(); - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } -} diff --git a/components/select/nz-select-top-control.spec.ts b/components/select/nz-select-top-control.spec.ts deleted file mode 100644 index 3d424ab44a9..00000000000 --- a/components/select/nz-select-top-control.spec.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { Component, DebugElement } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { Subject } from 'rxjs'; - -import { dispatchFakeEvent } from 'ng-zorro-antd/core'; - -import { createListOfOption } from './nz-option-container.spec'; -import { NzSelectTopControlComponent } from './nz-select-top-control.component'; -import { NzSelectModule } from './nz-select.module'; -import { NzSelectService } from './nz-select.service'; - -describe('nz-select top control', () => { - beforeEach(fakeAsync(() => { - let nzSelectServiceStub: Partial; - nzSelectServiceStub = { - check$: new Subject(), - listOfSelectedValue$: new Subject(), - open$: new Subject(), - clearInput$: new Subject(), - listOfSelectedValue: [1, 2, 3], - listOfCachedSelectedOption: createListOfOption(10), - isMultipleOrTags: true, - removeValueFormSelected: () => {}, - tokenSeparate: () => {}, - updateSearchValue: () => {}, - updateListOfSelectedValue: () => {}, - compareWith: (o1, o2) => o1 === o2 - }; - TestBed.configureTestingModule({ - providers: [{ provide: NzSelectService, useValue: nzSelectServiceStub }], - imports: [NzSelectModule, NoopAnimationsModule], - declarations: [NzTestSelectTopControlComponent] - }); - TestBed.compileComponents(); - })); - describe('default', () => { - let fixture: ComponentFixture; - let testComponent: NzTestSelectTopControlComponent; - let tc: DebugElement; - let tcComponent: NzSelectTopControlComponent; - let nzSelectService: NzSelectService; - beforeEach(() => { - fixture = TestBed.createComponent(NzTestSelectTopControlComponent); - fixture.detectChanges(); - testComponent = fixture.debugElement.componentInstance; - tc = fixture.debugElement.query(By.directive(NzSelectTopControlComponent)); - tcComponent = tc.injector.get(NzSelectTopControlComponent); - nzSelectService = fixture.debugElement.injector.get(NzSelectService); - }); - it('should clear selection work', () => { - fixture.detectChanges(); - const clearSpy = spyOn(nzSelectService, 'updateListOfSelectedValue'); - fixture.detectChanges(); - expect(clearSpy).toHaveBeenCalledTimes(0); - dispatchFakeEvent(tc.nativeElement.querySelector('.ant-select-selection__clear'), 'click'); - fixture.detectChanges(); - expect(clearSpy).toHaveBeenCalledTimes(1); - }); - it('should setInputValue work', () => { - fixture.detectChanges(); - const setInputSpy = spyOn(tcComponent, 'setInputValue'); - fixture.detectChanges(); - expect(setInputSpy).toHaveBeenCalledTimes(0); - nzSelectService.clearInput$.next(); - fixture.detectChanges(); - expect(setInputSpy).toHaveBeenCalledTimes(1); - expect(setInputSpy).toHaveBeenCalledWith(''); - }); - it('should selectedValueDisplay', () => { - fixture.detectChanges(); - tcComponent.nzShowSearch = false; - fixture.detectChanges(); - expect(tcComponent.selectedValueStyle.display).toBe('block'); - expect(tcComponent.selectedValueStyle.opacity).toBe('1'); - tcComponent.nzShowSearch = true; - tcComponent.nzOpen = false; - fixture.detectChanges(); - expect(tcComponent.selectedValueStyle.display).toBe('block'); - expect(tcComponent.selectedValueStyle.opacity).toBe('1'); - tcComponent.nzShowSearch = true; - tcComponent.nzOpen = true; - // @ts-ignore - tcComponent.inputValue = true; - tcComponent.isComposing = true; - fixture.detectChanges(); - expect(tcComponent.selectedValueStyle.display).toBe('none'); - expect(tcComponent.selectedValueStyle.opacity).toBe('1'); - tcComponent.nzShowSearch = true; - tcComponent.nzOpen = true; - // @ts-ignore - tcComponent.inputValue = true; - tcComponent.isComposing = false; - fixture.detectChanges(); - expect(tcComponent.selectedValueStyle.display).toBe('none'); - expect(tcComponent.selectedValueStyle.opacity).toBe('1'); - tcComponent.nzShowSearch = true; - tcComponent.nzOpen = true; - // @ts-ignore - tcComponent.inputValue = false; - tcComponent.isComposing = true; - fixture.detectChanges(); - expect(tcComponent.selectedValueStyle.display).toBe('none'); - expect(tcComponent.selectedValueStyle.opacity).toBe('1'); - tcComponent.nzShowSearch = true; - tcComponent.nzOpen = true; - // @ts-ignore - tcComponent.inputValue = false; - tcComponent.isComposing = false; - fixture.detectChanges(); - expect(tcComponent.selectedValueStyle.display).toBe('block'); - expect(tcComponent.selectedValueStyle.opacity).toBe('0.4'); - }); - it('should open focus', fakeAsync(() => { - fixture.detectChanges(); - expect(tc.nativeElement.querySelector('.ant-select-search__field') === document.activeElement).toBeFalsy(); - // @ts-ignore - nzSelectService.open$.next(false); - fixture.detectChanges(); - expect(tc.nativeElement.querySelector('.ant-select-search__field') === document.activeElement).toBeFalsy(); - // @ts-ignore - nzSelectService.open$.next(true); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - expect(tc.nativeElement.querySelector('.ant-select-search__field') === document.activeElement).toBeTruthy(); - })); - it('should destroy piped', () => { - fixture.detectChanges(); - // @ts-ignore - const checkSpy = spyOn(tcComponent.cdr, 'markForCheck'); - fixture.detectChanges(); - expect(checkSpy).toHaveBeenCalledTimes(0); - // @ts-ignore - nzSelectService.check$.next(); - fixture.detectChanges(); - expect(checkSpy).toHaveBeenCalledTimes(1); - testComponent.destroy = true; - fixture.detectChanges(); - // @ts-ignore - nzSelectService.check$.next(); - fixture.detectChanges(); - expect(checkSpy).toHaveBeenCalledTimes(1); - }); - it('should remove option call', () => { - fixture.detectChanges(); - const removeSpy = spyOn(nzSelectService, 'removeValueFormSelected'); - fixture.detectChanges(); - expect(removeSpy).toHaveBeenCalledTimes(0); - dispatchFakeEvent(tc.nativeElement.querySelector('.ant-select-selection__choice__remove'), 'click'); - fixture.detectChanges(); - expect(removeSpy).toHaveBeenCalledTimes(1); - }); - }); -}); - -@Component({ - template: ` -
    - nzMaxTagPlaceholder - nzSuffixIcon - nzClearIcon - nzRemoveIcon - ` -}) -export class NzTestSelectTopControlComponent { - destroy = false; - open = false; - nzPlaceHolder = 'placeholder'; - nzAllowClear = true; - nzMaxTagCount = 3; - nzShowArrow = true; - nzLoading = false; - nzShowSearch = false; - nzTokenSeparators = [',']; -} diff --git a/components/select/nz-select-unselectable.directive.ts b/components/select/nz-select-unselectable.directive.ts deleted file mode 100644 index b79fd91bdd0..00000000000 --- a/components/select/nz-select-unselectable.directive.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @license - * Copyright Alibaba.com All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE - */ - -import { Directive } from '@angular/core'; - -@Directive({ - selector: '[nz-select-unselectable]', - exportAs: 'nzSelectUnselectable', - host: { - '[attr.unselectable]': '"unselectable"', - '[style.user-select]': '"none"' - } -}) -export class NzSelectUnselectableDirective {} diff --git a/components/select/nz-select-unselectable.spec.ts b/components/select/nz-select-unselectable.spec.ts deleted file mode 100644 index 1678751c91b..00000000000 --- a/components/select/nz-select-unselectable.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Component, DebugElement } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NzSelectUnselectableDirective } from './nz-select-unselectable.directive'; - -describe('select unselectable', () => { - beforeEach(fakeAsync(() => { - TestBed.configureTestingModule({ - imports: [], - declarations: [NzTestSelectUnselectableComponent, NzSelectUnselectableDirective] - }); - TestBed.compileComponents(); - })); - describe('basic select unselectable', () => { - let fixture: ComponentFixture; - let unselectable: DebugElement; - beforeEach(() => { - fixture = TestBed.createComponent(NzTestSelectUnselectableComponent); - fixture.detectChanges(); - unselectable = fixture.debugElement.query(By.directive(NzSelectUnselectableDirective)); - }); - it('should unselectable style work', () => { - fixture.detectChanges(); - expect(unselectable.nativeElement.attributes.getNamedItem('unselectable').name).toBe('unselectable'); - expect(unselectable.nativeElement.style.userSelect).toBe('none'); - }); - }); -}); - -@Component({ - template: ` -
    - ` -}) -export class NzTestSelectUnselectableComponent {} diff --git a/components/select/nz-select.component.html b/components/select/nz-select.component.html deleted file mode 100644 index 964431a6e7c..00000000000 --- a/components/select/nz-select.component.html +++ /dev/null @@ -1,64 +0,0 @@ -
    - -
    -
    - -
    -
    - - - - diff --git a/components/select/nz-select.component.spec.ts b/components/select/nz-select.component.spec.ts deleted file mode 100644 index 74e2201c930..00000000000 --- a/components/select/nz-select.component.spec.ts +++ /dev/null @@ -1,592 +0,0 @@ -import { DOWN_ARROW, ENTER, ESCAPE, SPACE, TAB, UP_ARROW } from '@angular/cdk/keycodes'; -import { Component, DebugElement, NgZone } from '@angular/core'; -import { async, ComponentFixture, fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing'; -import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { dispatchKeyboardEvent, MockNgZone } from 'ng-zorro-antd/core'; -import { NzOptionComponent } from './nz-option.component'; - -import { OverlayContainer } from '@angular/cdk/overlay'; -import { defaultFilterOption } from './nz-option.pipe'; -import { NzSelectComponent } from './nz-select.component'; -import { NzSelectModule } from './nz-select.module'; - -describe('nz-select component', () => { - let overlayContainer: OverlayContainer; - let overlayContainerElement: HTMLElement; - let zone: MockNgZone; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NzSelectModule, NoopAnimationsModule, FormsModule, ReactiveFormsModule], - declarations: [ - NzTestSelectDefaultComponent, - NzTestSelectTagsComponent, - NzTestSelectFormComponent, - NzTestOptionChangeComponent, - NzTestSelectFormDisabledTouchedComponent - ], - providers: [ - { - provide: NgZone, - useFactory: () => { - zone = new MockNgZone(); - return zone; - } - } - ] - }); - TestBed.compileComponents(); - inject([OverlayContainer], (oc: OverlayContainer) => { - overlayContainer = oc; - overlayContainerElement = oc.getContainerElement(); - })(); - })); - afterEach(inject([OverlayContainer], (currentOverlayContainer: OverlayContainer) => { - currentOverlayContainer.ngOnDestroy(); - overlayContainer.ngOnDestroy(); - })); - describe('default', () => { - let fixture: ComponentFixture; - let testComponent: NzTestSelectDefaultComponent; - let select: DebugElement; - let selectComponent: NzSelectComponent; - beforeEach(() => { - fixture = TestBed.createComponent(NzTestSelectDefaultComponent); - fixture.detectChanges(); - testComponent = fixture.debugElement.componentInstance; - select = fixture.debugElement.query(By.directive(NzSelectComponent)); - selectComponent = select.injector.get(NzSelectComponent); - }); - it('should className correct', () => { - fixture.detectChanges(); - expect(select.nativeElement.classList).toContain('ant-select'); - }); - it('should size work', () => { - testComponent.size = 'small'; - fixture.detectChanges(); - expect(select.nativeElement.classList).toContain('ant-select-sm'); - testComponent.size = 'large'; - fixture.detectChanges(); - expect(select.nativeElement.classList).toContain('ant-select-lg'); - }); - it('should allowClear work', () => { - fixture.detectChanges(); - expect(select.nativeElement.classList).not.toContain('ant-select-allow-clear'); - testComponent.allowClear = true; - fixture.detectChanges(); - expect(select.nativeElement.classList).toContain('ant-select-allow-clear'); - }); - it('should open work', () => { - fixture.detectChanges(); - expect(select.nativeElement.classList).not.toContain('ant-select-open'); - testComponent.open = true; - fixture.detectChanges(); - expect(select.nativeElement.classList).toContain('ant-select-open'); - expect(testComponent.openChange).toHaveBeenCalledTimes(0); - }); - it('should click toggle open', () => { - fixture.detectChanges(); - select.nativeElement.click(); - fixture.detectChanges(); - expect(testComponent.open).toBe(true); - expect(testComponent.openChange).toHaveBeenCalledTimes(1); - select.nativeElement.click(); - fixture.detectChanges(); - expect(testComponent.open).toBe(false); - expect(testComponent.openChange).toHaveBeenCalledTimes(2); - }); - it('should disabled work', fakeAsync(() => { - fixture.detectChanges(); - expect(select.nativeElement.classList).toContain('ant-select-enabled'); - testComponent.disabled = true; - fixture.detectChanges(); - expect(select.nativeElement.classList).not.toContain('ant-select-enabled'); - expect(select.nativeElement.classList).toContain('ant-select-disabled'); - expect(testComponent.openChange).toHaveBeenCalledTimes(0); - select.nativeElement.click(); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - expect(testComponent.open).toBe(false); - expect(testComponent.openChange).toHaveBeenCalledTimes(0); - })); - it('should focus and blur function work', () => { - testComponent.showSearch = true; - select.nativeElement.click(); - fixture.detectChanges(); - expect(select.nativeElement.querySelector('.ant-select-selection') === document.activeElement).toBe(true); - selectComponent.blur(); - fixture.detectChanges(); - expect(select.nativeElement.querySelector('.ant-select-selection') === document.activeElement).toBe(false); - selectComponent.focus(); - fixture.detectChanges(); - expect(select.nativeElement.querySelector('.ant-select-selection') === document.activeElement).toBe(true); - }); - it('should dropdown class work', () => { - fixture.detectChanges(); - select.nativeElement.click(); - fixture.detectChanges(); - expect(testComponent.open).toBe(true); - expect(overlayContainerElement.querySelector('.test-class')).toBeDefined(); - }); - it('should dropdown style work', () => { - fixture.detectChanges(); - select.nativeElement.click(); - fixture.detectChanges(); - expect(testComponent.open).toBe(true); - const targetElement = overlayContainerElement.querySelector('.test-class') as HTMLElement; - expect(targetElement.style.height).toBe('120px'); - }); - it('should dropdownMatchSelectWidth true work', fakeAsync(() => { - fixture.detectChanges(); - select.nativeElement.click(); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - zone.simulateZoneExit(); - fixture.detectChanges(); - expect(testComponent.open).toBe(true); - const targetElement = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; - expect(targetElement.style.width).toBe('10px'); - })); - it('should dropdownMatchSelectWidth false work', fakeAsync(() => { - testComponent.dropdownMatchSelectWidth = false; - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - select.nativeElement.click(); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - zone.simulateZoneExit(); - fixture.detectChanges(); - expect(testComponent.open).toBe(true); - const targetElement = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; - expect(targetElement.style.width).toBe(''); - expect(targetElement.style.minWidth).toBe('10px'); - })); - it('should custom template work', () => { - select.nativeElement.click(); - fixture.detectChanges(); - expect(testComponent.open).toBe(true); - fixture.detectChanges(); - overlayContainerElement.querySelector('li')!.click(); - fixture.detectChanges(); - zone.simulateZoneExit(); - fixture.detectChanges(); - const selection = select.nativeElement.querySelector('.ant-select-selection') as HTMLElement; - expect(selection.textContent).toContain('Label: JackValue: jack'); - }); - it('should click option close dropdown', () => { - testComponent.showSearch = true; - select.nativeElement.click(); - fixture.detectChanges(); - expect(testComponent.open).toBe(true); - fixture.detectChanges(); - overlayContainerElement.querySelector('li')!.click(); - fixture.detectChanges(); - expect(testComponent.open).toBe(false); - }); - it('should keep overlay open when press esc', fakeAsync(() => { - fixture.detectChanges(); - select.nativeElement.click(); - fixture.detectChanges(); - dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - select.nativeElement.click(); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.open).toBe(true); - expect(selectComponent.cdkConnectedOverlay.overlayRef.backdropElement).toBeDefined(); - })); - it('should keydown origin work', () => { - const keyDownSpy = spyOn(selectComponent, 'onKeyDown'); - dispatchKeyboardEvent(select.nativeElement.querySelector('.ant-select-selection'), 'keydown', UP_ARROW); - fixture.detectChanges(); - expect(keyDownSpy).toHaveBeenCalledTimes(1); - }); - it('should blur after user hits enter key in single mode', () => { - const spy = spyOn(selectComponent, 'blur'); - testComponent.showSearch = true; - select.nativeElement.click(); - fixture.detectChanges(); - dispatchKeyboardEvent(select.nativeElement.querySelector('.ant-select-selection'), 'keydown', DOWN_ARROW); - fixture.detectChanges(); - expect(spy).not.toHaveBeenCalled(); - dispatchKeyboardEvent(select.nativeElement.querySelector('.ant-select-selection'), 'keydown', ENTER); - fixture.detectChanges(); - expect(spy).toHaveBeenCalled(); - }); - it('should support keydown events to open and close select panel', fakeAsync(() => { - fixture.detectChanges(); - dispatchKeyboardEvent(select.nativeElement.querySelector('.ant-select-selection'), 'keydown', SPACE); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.open).toBe(true); - // #2201, space should not close select panel - dispatchKeyboardEvent(select.nativeElement.querySelector('.ant-select-selection'), 'keydown', TAB); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.open).toBe(false); - dispatchKeyboardEvent(select.nativeElement.querySelector('.ant-select-selection'), 'keydown', DOWN_ARROW); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.open).toBe(true); - dispatchKeyboardEvent(select.nativeElement.querySelector('.ant-select-selection'), 'keydown', TAB); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.open).toBe(false); - testComponent.disabled = true; - fixture.detectChanges(); - dispatchKeyboardEvent(select.nativeElement.querySelector('.ant-select-selection'), 'keydown', TAB); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.open).toBe(false); - })); - it('should skip disabled or hidden options on keydown events', fakeAsync(() => { - fixture.detectChanges(); - select.nativeElement.click(); - fixture.detectChanges(); - overlayContainerElement.querySelector('li')!.click(); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.selectedValue).toBe('jack'); - select.nativeElement.click(); - fixture.detectChanges(); - dispatchKeyboardEvent(select.nativeElement.querySelector('.ant-select-selection'), 'keydown', UP_ARROW); - fixture.detectChanges(); - dispatchKeyboardEvent(select.nativeElement.querySelector('.ant-select-selection'), 'keydown', ENTER); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.selectedValue).toBe('lucy'); - select.nativeElement.click(); - fixture.detectChanges(); - dispatchKeyboardEvent(select.nativeElement.querySelector('.ant-select-selection'), 'keydown', UP_ARROW); - fixture.detectChanges(); - dispatchKeyboardEvent(select.nativeElement.querySelector('.ant-select-selection'), 'keydown', DOWN_ARROW); - fixture.detectChanges(); - dispatchKeyboardEvent(select.nativeElement.querySelector('.ant-select-selection'), 'keydown', DOWN_ARROW); - fixture.detectChanges(); - dispatchKeyboardEvent(select.nativeElement.querySelector('.ant-select-selection'), 'keydown', ENTER); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.selectedValue).toBe('jack'); - })); - }); - describe('tags', () => { - let fixture: ComponentFixture; - let testComponent: NzTestSelectTagsComponent; - let select: DebugElement; - let selectComponent: NzSelectComponent; - - beforeEach(() => { - fixture = TestBed.createComponent(NzTestSelectTagsComponent); - fixture.detectChanges(); - testComponent = fixture.debugElement.componentInstance; - select = fixture.debugElement.query(By.directive(NzSelectComponent)); - selectComponent = select.injector.get(NzSelectComponent); - }); - it('should click option correct', fakeAsync(() => { - fixture.detectChanges(); - expect(select.nativeElement.classList).toContain('ant-select'); - select.nativeElement.click(); - fixture.detectChanges(); - overlayContainerElement.querySelector('li')!.click(); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.selectedValue.length).toBe(1); - expect(testComponent.selectedValue[0]).toBe('jack'); - })); - it('should remove from top control work', fakeAsync(() => { - fixture.detectChanges(); - selectComponent.nzSelectService.updateListOfSelectedValue(['jack'], true); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.selectedValue.length).toBe(1); - expect(testComponent.selectedValue[0]).toBe('jack'); - })); - it('should clear work', fakeAsync(() => { - fixture.detectChanges(); - selectComponent.nzSelectService.updateListOfSelectedValue([], true); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.selectedValue.length).toBe(0); - })); - it('should custom display template work', fakeAsync(() => { - fixture.detectChanges(); - selectComponent.nzSelectService.updateListOfSelectedValue(['jack'], true); - fixture.detectChanges(); - tick(1000); - flush(); - fixture.detectChanges(); - const selection = select.nativeElement.querySelector('.ant-select-selection') as HTMLElement; - expect(selection.innerText).toContain('Label: Jack\nValue: jack'); - })); - }); - - describe('form', () => { - let fixture: ComponentFixture; - let testComponent: NzTestSelectFormComponent; - let select: DebugElement; - - beforeEach(() => { - fixture = TestBed.createComponent(NzTestSelectFormComponent); - fixture.detectChanges(); - testComponent = fixture.debugElement.componentInstance; - select = fixture.debugElement.query(By.directive(NzSelectComponent)); - }); - it('should set disabled work', fakeAsync(() => { - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(select.nativeElement.classList).not.toContain('ant-select-disabled'); - testComponent.disable(); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(select.nativeElement.classList).toContain('ant-select-disabled'); - })); - /** https://github.com/NG-ZORRO/ng-zorro-antd/issues/3014 **/ - it('should reset form works', fakeAsync(() => { - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.formGroup.value.select).toBe('jack'); - testComponent.reset(); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.formGroup.value.select).toBe(null); - select.nativeElement.click(); - fixture.detectChanges(); - overlayContainerElement.querySelector('li')!.click(); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.formGroup.value.select).toBe('jack'); - testComponent.reset(); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.formGroup.value.select).toBe(null); - select.nativeElement.click(); - fixture.detectChanges(); - overlayContainerElement.querySelector('li')!.click(); - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.formGroup.value.select).toBe('jack'); - })); - }); - - describe('option change', () => { - let fixture: ComponentFixture; - let testComponent: NzTestOptionChangeComponent; - let select: DebugElement; - let selectComponent: NzSelectComponent; - - beforeEach(() => { - fixture = TestBed.createComponent(NzTestOptionChangeComponent); - fixture.detectChanges(); - testComponent = fixture.debugElement.componentInstance; - select = fixture.debugElement.query(By.directive(NzSelectComponent)); - selectComponent = select.injector.get(NzSelectComponent)!; - }); - - it('should option change work', () => { - fixture.detectChanges(); - const changeSpy = spyOn(selectComponent.nzSelectService, 'updateTemplateOption'); - fixture.detectChanges(); - expect(changeSpy).toHaveBeenCalledTimes(0); - testComponent.displaySingle = true; - fixture.detectChanges(); - expect(changeSpy).toHaveBeenCalledTimes(1); - testComponent.displayGroup = true; - fixture.detectChanges(); - expect(changeSpy).toHaveBeenCalledTimes(3); - testComponent.displayGroupInner = true; - fixture.detectChanges(); - expect(changeSpy).toHaveBeenCalledTimes(4); - }); - }); - - describe('form init state', () => { - let fixture: ComponentFixture; - let testComponent: NzTestSelectFormDisabledTouchedComponent; - beforeEach(() => { - fixture = TestBed.createComponent(NzTestSelectFormDisabledTouchedComponent); - fixture.detectChanges(); - testComponent = fixture.debugElement.componentInstance; - }); - /** https://github.com/NG-ZORRO/ng-zorro-antd/issues/3059 **/ - it('should init disabled state with touched false', fakeAsync(() => { - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - expect(testComponent.formGroup.controls.select.touched).toBe(false); - })); - }); -}); - -@Component({ - template: ` - - - - - - - - -
    Label: {{ selected.nzLabel }}
    -
    Value: {{ selected.nzValue }}
    -
    - ` -}) -export class NzTestSelectDefaultComponent { - selectedValue = 'lucy'; - allowClear = false; - open = false; - size = 'default'; - mode = 'default'; - autoFocus = false; - compareWith = (o1: any, o2: any) => o1 === o2; // tslint:disable-line:no-any - disabled = false; - onSearch = jasmine.createSpy('on search'); - showSearch = false; - placeholder = 'placeholder'; - filterOption = defaultFilterOption; - dropdownMatchSelectWidth = true; - openChange = jasmine.createSpy('open change'); - dropdownStyle = { height: '120px' }; - nzFilterOption = (input: string, option: NzOptionComponent) => { - if (option && option.nzLabel) { - return option.nzLabel.toLowerCase().indexOf(input.toLowerCase()) > -1; - } else { - return false; - } - }; -} - -@Component({ - template: ` - - - - Disabled - - -
    Label: {{ selected.nzLabel }}
    -
    Value: {{ selected.nzValue }}
    -
    - ` -}) -export class NzTestSelectTagsComponent { - selectedValue = ['lucy', 'jack']; - allowClear = false; -} - -@Component({ - template: ` -
    - - - - - -
    - ` -}) -export class NzTestSelectFormComponent { - formGroup: FormGroup; - - constructor(private formBuilder: FormBuilder) { - this.formGroup = this.formBuilder.group({ - select: ['jack'] - }); - } - - disable(): void { - this.formGroup.disable(); - } - - reset(): void { - this.formGroup.reset(); - } -} - -@Component({ - template: ` -
    - - - - -
    - ` -}) -export class NzTestSelectFormDisabledTouchedComponent { - formGroup: FormGroup; - - constructor() { - this.formGroup = new FormGroup({ select: new FormControl({ value: 'lucy', disabled: true }) }); - } -} - -@Component({ - template: ` - - - - - - - - - - - ` -}) -export class NzTestOptionChangeComponent { - displaySingle = false; - displayGroup = false; - displayGroupInner = false; -} diff --git a/components/select/nz-select.component.ts b/components/select/nz-select.component.ts deleted file mode 100644 index 6298461b5c4..00000000000 --- a/components/select/nz-select.component.ts +++ /dev/null @@ -1,344 +0,0 @@ -/** - * @license - * Copyright Alibaba.com All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE - */ - -import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedOverlayPositionChange } from '@angular/cdk/overlay'; -import { Platform } from '@angular/cdk/platform'; -import { - AfterContentInit, - AfterViewInit, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ContentChildren, - ElementRef, - EventEmitter, - forwardRef, - Host, - Input, - OnDestroy, - OnInit, - Optional, - Output, - QueryList, - Renderer2, - TemplateRef, - ViewChild, - ViewEncapsulation -} from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { EMPTY, merge, Subject } from 'rxjs'; -import { flatMap, startWith, takeUntil } from 'rxjs/operators'; - -import { InputBoolean, isNotNil, NzNoAnimationDirective, NzSizeLDSType, slideMotion, toBoolean } from 'ng-zorro-antd/core'; - -import { NzOptionGroupComponent } from './nz-option-group.component'; -import { NzOptionComponent } from './nz-option.component'; -import { TFilterOption } from './nz-option.pipe'; -import { NzSelectTopControlComponent } from './nz-select-top-control.component'; -import { NzSelectService } from './nz-select.service'; - -@Component({ - selector: 'nz-select', - exportAs: 'nzSelect', - preserveWhitespaces: false, - providers: [ - NzSelectService, - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => NzSelectComponent), - multi: true - } - ], - changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None, - animations: [slideMotion], - templateUrl: './nz-select.component.html', - host: { - '[class.ant-select-lg]': 'nzSize==="large"', - '[class.ant-select-sm]': 'nzSize==="small"', - '[class.ant-select-enabled]': '!nzDisabled', - '[class.ant-select-no-arrow]': '!nzShowArrow', - '[class.ant-select-disabled]': 'nzDisabled', - '[class.ant-select-allow-clear]': 'nzAllowClear', - '[class.ant-select-open]': 'open', - '(click)': 'toggleDropDown()' - }, - styles: [ - ` - .ant-select-dropdown { - top: 100%; - left: 0; - position: relative; - width: 100%; - margin-top: 4px; - margin-bottom: 4px; - } - ` - ] -}) -export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy, AfterContentInit { - open = false; - // tslint:disable-next-line:no-any - value: any | any[]; - onChange: (value: string | string[]) => void = () => null; - onTouched: () => void = () => null; - dropDownPosition: 'top' | 'center' | 'bottom' = 'bottom'; - triggerWidth: number; - private _disabled = false; - private isInit = false; - private destroy$ = new Subject(); - @ViewChild(CdkOverlayOrigin, { static: false }) cdkOverlayOrigin: CdkOverlayOrigin; - @ViewChild(CdkConnectedOverlay, { static: false }) cdkConnectedOverlay: CdkConnectedOverlay; - @ViewChild(NzSelectTopControlComponent, { static: true }) nzSelectTopControlComponent: NzSelectTopControlComponent; - @ViewChild(NzSelectTopControlComponent, { static: true, read: ElementRef }) nzSelectTopControlElement: ElementRef; - /** should move to nz-option-container when https://github.com/angular/angular/issues/20810 resolved **/ - @ContentChildren(NzOptionComponent) listOfNzOptionComponent: QueryList; - @ContentChildren(NzOptionGroupComponent) listOfNzOptionGroupComponent: QueryList; - @Output() readonly nzOnSearch = new EventEmitter(); - @Output() readonly nzScrollToBottom = new EventEmitter(); - @Output() readonly nzOpenChange = new EventEmitter(); - @Output() readonly nzBlur = new EventEmitter(); - @Output() readonly nzFocus = new EventEmitter(); - @Input() nzSize: NzSizeLDSType = 'default'; - @Input() nzDropdownClassName: string; - @Input() nzDropdownMatchSelectWidth = true; - @Input() nzDropdownStyle: { [key: string]: string }; - @Input() nzNotFoundContent: string; - @Input() @InputBoolean() nzAllowClear = false; - @Input() @InputBoolean() nzShowSearch = false; - @Input() @InputBoolean() nzLoading = false; - @Input() @InputBoolean() nzAutoFocus = false; - @Input() nzPlaceHolder: string; - @Input() nzMaxTagCount: number; - @Input() nzDropdownRender: TemplateRef; - @Input() nzCustomTemplate: TemplateRef<{ $implicit: NzOptionComponent }>; - @Input() nzSuffixIcon: TemplateRef; - @Input() nzClearIcon: TemplateRef; - @Input() nzRemoveIcon: TemplateRef; - @Input() nzMenuItemSelectedIcon: TemplateRef; - @Input() nzShowArrow = true; - @Input() nzTokenSeparators: string[] = []; - // tslint:disable-next-line:no-any - @Input() nzMaxTagPlaceholder: TemplateRef<{ $implicit: any[] }>; - - @Input() - set nzAutoClearSearchValue(value: boolean) { - this.nzSelectService.autoClearSearchValue = toBoolean(value); - } - - @Input() - set nzMaxMultipleCount(value: number) { - this.nzSelectService.maxMultipleCount = value; - } - - @Input() - set nzServerSearch(value: boolean) { - this.nzSelectService.serverSearch = toBoolean(value); - } - - @Input() - set nzMode(value: 'default' | 'multiple' | 'tags') { - this.nzSelectService.mode = value; - this.nzSelectService.check(); - } - - @Input() - set nzFilterOption(value: TFilterOption) { - this.nzSelectService.filterOption = value; - } - - @Input() - // tslint:disable-next-line:no-any - set compareWith(value: (o1: any, o2: any) => boolean) { - this.nzSelectService.compareWith = value; - } - - @Input() - set nzOpen(value: boolean) { - this.open = value; - this.nzSelectService.setOpenState(value); - } - - @Input() - set nzDisabled(value: boolean) { - this._disabled = toBoolean(value); - this.nzSelectService.disabled = this._disabled; - this.nzSelectService.check(); - if (this.nzDisabled && this.isInit) { - this.closeDropDown(); - } - } - - get nzDisabled(): boolean { - return this._disabled; - } - - get nzSelectTopControlDOM(): HTMLElement { - return this.nzSelectTopControlElement && this.nzSelectTopControlElement.nativeElement; - } - - updateAutoFocus(): void { - if (this.nzSelectTopControlDOM && this.nzAutoFocus) { - this.nzSelectTopControlDOM.focus(); - } - } - - focus(): void { - if (this.nzSelectTopControlDOM) { - this.nzSelectTopControlDOM.focus(); - } - } - - blur(): void { - if (this.nzSelectTopControlDOM) { - this.nzSelectTopControlDOM.blur(); - } - } - - onFocus(): void { - this.nzFocus.emit(); - } - - onBlur(): void { - this.nzBlur.emit(); - } - - onKeyDown(event: KeyboardEvent): void { - this.nzSelectService.onKeyDown(event); - } - - toggleDropDown(): void { - if (!this.nzDisabled) { - this.nzSelectService.setOpenState(!this.open); - } - } - - closeDropDown(): void { - this.nzSelectService.setOpenState(false); - } - - onPositionChange(position: ConnectedOverlayPositionChange): void { - this.dropDownPosition = position.connectionPair.originY; - } - - updateCdkConnectedOverlayStatus(): void { - if (this.platform.isBrowser) { - this.triggerWidth = this.cdkOverlayOrigin.elementRef.nativeElement.getBoundingClientRect().width; - } - } - - updateCdkConnectedOverlayPositions(): void { - setTimeout(() => { - if (this.cdkConnectedOverlay && this.cdkConnectedOverlay.overlayRef) { - this.cdkConnectedOverlay.overlayRef.updatePosition(); - } - }); - } - - constructor( - renderer: Renderer2, - public nzSelectService: NzSelectService, - private cdr: ChangeDetectorRef, - private platform: Platform, - elementRef: ElementRef, - @Host() @Optional() public noAnimation?: NzNoAnimationDirective - ) { - renderer.addClass(elementRef.nativeElement, 'ant-select'); - } - - /** update ngModel -> update listOfSelectedValue **/ - // tslint:disable-next-line:no-any - writeValue(value: any | any[]): void { - this.value = value; - let listValue: any[] = []; // tslint:disable-line:no-any - if (isNotNil(value)) { - if (this.nzSelectService.isMultipleOrTags) { - listValue = value; - } else { - listValue = [value]; - } - } - this.nzSelectService.updateListOfSelectedValue(listValue, false); - this.cdr.markForCheck(); - } - - registerOnChange(fn: (value: string | string[]) => void): void { - this.onChange = fn; - } - - registerOnTouched(fn: () => void): void { - this.onTouched = fn; - } - - setDisabledState(isDisabled: boolean): void { - this.nzDisabled = isDisabled; - this.cdr.markForCheck(); - } - - ngOnInit(): void { - this.nzSelectService.animationEvent$.pipe(takeUntil(this.destroy$)).subscribe(() => this.updateCdkConnectedOverlayPositions()); - this.nzSelectService.searchValue$.pipe(takeUntil(this.destroy$)).subscribe(data => { - this.nzOnSearch.emit(data); - this.updateCdkConnectedOverlayPositions(); - }); - this.nzSelectService.modelChange$.pipe(takeUntil(this.destroy$)).subscribe(modelValue => { - if (this.value !== modelValue) { - this.value = modelValue; - this.onChange(this.value); - } - }); - this.nzSelectService.open$.pipe(takeUntil(this.destroy$)).subscribe(value => { - if (this.open !== value) { - this.nzOpenChange.emit(value); - } - if (value) { - this.focus(); - this.updateCdkConnectedOverlayStatus(); - } else { - this.blur(); - this.onTouched(); - } - this.open = value; - this.nzSelectService.clearInput(); - }); - this.nzSelectService.check$.pipe(takeUntil(this.destroy$)).subscribe(() => { - this.cdr.markForCheck(); - }); - } - - ngAfterViewInit(): void { - this.updateCdkConnectedOverlayStatus(); - this.updateAutoFocus(); - this.isInit = true; - } - - ngAfterContentInit(): void { - this.listOfNzOptionGroupComponent.changes - .pipe( - startWith(true), - flatMap(() => - merge( - this.listOfNzOptionGroupComponent.changes, - this.listOfNzOptionComponent.changes, - ...this.listOfNzOptionComponent.map(option => option.changes), - ...this.listOfNzOptionGroupComponent.map(group => - group.listOfNzOptionComponent ? group.listOfNzOptionComponent.changes : EMPTY - ) - ).pipe(startWith(true)) - ) - ) - .subscribe(() => { - this.nzSelectService.updateTemplateOption(this.listOfNzOptionComponent.toArray(), this.listOfNzOptionGroupComponent.toArray()); - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } -} diff --git a/components/select/nz-select.module.ts b/components/select/nz-select.module.ts deleted file mode 100644 index c30e8465b95..00000000000 --- a/components/select/nz-select.module.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @license - * Copyright Alibaba.com All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE - */ -import { OverlayModule } from '@angular/cdk/overlay'; -import { PlatformModule } from '@angular/cdk/platform'; -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { NzNoAnimationModule, NzOutletModule, NzOverlayModule } from 'ng-zorro-antd/core'; -import { NzEmptyModule } from 'ng-zorro-antd/empty'; -import { NzI18nModule } from 'ng-zorro-antd/i18n'; -import { NzIconModule } from 'ng-zorro-antd/icon'; - -import { NzOptionContainerComponent } from './nz-option-container.component'; -import { NzOptionGroupComponent } from './nz-option-group.component'; -import { NzOptionLiComponent } from './nz-option-li.component'; -import { NzOptionComponent } from './nz-option.component'; -import { NzFilterGroupOptionPipe, NzFilterOptionPipe } from './nz-option.pipe'; -import { NzSelectTopControlComponent } from './nz-select-top-control.component'; -import { NzSelectUnselectableDirective } from './nz-select-unselectable.directive'; -import { NzSelectComponent } from './nz-select.component'; - -@NgModule({ - imports: [ - CommonModule, - NzI18nModule, - FormsModule, - PlatformModule, - OverlayModule, - NzIconModule, - NzOutletModule, - NzEmptyModule, - NzOverlayModule, - NzNoAnimationModule - ], - declarations: [ - NzFilterGroupOptionPipe, - NzFilterOptionPipe, - NzOptionComponent, - NzSelectComponent, - NzOptionContainerComponent, - NzOptionGroupComponent, - NzOptionLiComponent, - NzSelectTopControlComponent, - NzSelectUnselectableDirective - ], - exports: [NzOptionComponent, NzSelectComponent, NzOptionContainerComponent, NzOptionGroupComponent, NzSelectTopControlComponent] -}) -export class NzSelectModule {} diff --git a/components/select/nz-select.service.spec.ts b/components/select/nz-select.service.spec.ts deleted file mode 100644 index 9602e07d1ac..00000000000 --- a/components/select/nz-select.service.spec.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { createListOfOption } from './nz-option-container.spec'; -import { NzOptionComponent } from './nz-option.component'; -import { NzSelectService } from './nz-select.service'; - -describe('SelectService', () => { - let service: NzSelectService; - beforeEach(() => { - service = new NzSelectService(); - }); - describe('includesSeparators', () => { - const separators = [' ', ',']; - it('return true when given includes separators', () => { - expect(service.includesSeparators(',foo,bar', separators)).toBe(true); - }); - - it('return false when given do not include separators', () => { - expect(service.includesSeparators('foobar', separators)).toBe(false); - }); - - it('return false when string only has a leading separator', () => { - expect(service.includesSeparators(',foobar', separators)).toBe(false); - }); - }); - - describe('splitBySeparators', () => { - const separators = [' ', ',']; - it('split given string by separators', () => { - const str = 'foo bar,baz'; - expect(service.splitBySeparators(str, separators)).toEqual(['foo', 'bar', 'baz']); - }); - - it('split string with leading separator ', () => { - const str = ',foo'; - expect(service.splitBySeparators(str, separators)).toEqual(['foo']); - }); - - it('split string with trailling separator', () => { - const str = 'foo,'; - expect(service.splitBySeparators(str, separators)).toEqual(['foo']); - }); - - it('split a separator', () => { - const str = ','; - expect(service.splitBySeparators(str, separators)).toEqual([]); - }); - - it('split two separators', () => { - const str = ',,'; - expect(service.splitBySeparators(str, separators)).toEqual([]); - }); - - it('split two separators surrounded by valid input', () => { - const str = 'a,,b'; - expect(service.splitBySeparators(str, separators)).toEqual(['a', 'b']); - }); - - it('split repeating separators with valid input throughout', () => { - const str = ',,,a,b,,,c,d,,,e,'; - expect(service.splitBySeparators(str, separators)).toEqual(['a', 'b', 'c', 'd', 'e']); - }); - - it('split multiple repeating separators with valid input throughout', () => { - const str = ',,,a b, c,d, ,e ,f'; - expect(service.splitBySeparators(str, separators)).toEqual(['a', 'b', 'c', 'd', 'e', 'f']); - }); - it('split duplicated repeating separators with valid input throughout', () => { - const str = ',,,a b, c,d, ,e ,f,a,b,c'; - expect(service.splitBySeparators(str, separators)).toEqual(['a', 'b', 'c', 'd', 'e', 'f']); - }); - }); - describe('tag', () => { - beforeEach(() => { - service.mode = 'tags'; - }); - it('should updateListOfTagOption work', () => { - service.listOfCachedSelectedOption = [ - { nzValue: `option_value_0`, nzLabel: `option_label_0` }, - { nzValue: `option_value_miss`, nzLabel: `option_label_miss` } - // tslint:disable-next-line: no-any - ] as any; - service.listOfSelectedValue = [`option_value_1`, `option_value_miss_1`]; - service.listOfTemplateOption = createListOfOption(3); - service.updateListOfTagOption(); - expect(service.listOfTagAndTemplateOption.length).toEqual(4); - expect(service.listOfTagAndTemplateOption[0].nzValue).toEqual('option_value_0'); - expect(service.listOfTagAndTemplateOption[0].nzLabel).toEqual('option_label_0'); - expect(service.listOfTagAndTemplateOption[1].nzValue).toEqual('option_value_1'); - expect(service.listOfTagAndTemplateOption[1].nzLabel).toEqual('option_label_1'); - }); - it('should updateAddTagOption work', () => { - service.listOfSelectedValue = [`option_value_0`, `option_value_1`]; - service.listOfTemplateOption = createListOfOption(3); - service.searchValue = 'abc'; - service.updateAddTagOption(); - expect(service.addedTagOption!.nzValue).toEqual('abc'); - expect(service.addedTagOption!.nzLabel).toEqual('abc'); - }); - }); - describe('token', () => { - it('should multiple work', () => { - service.mode = 'multiple'; - const selectedValueSpy = spyOn(service, 'updateListOfSelectedValue'); - service.listOfTagAndTemplateOption = createListOfOption(3); - service.tokenSeparate('option_label_0,b,c', [',']); - expect(selectedValueSpy).toHaveBeenCalledWith(['option_value_0'], true); - }); - it('should tags work', () => { - service.mode = 'tags'; - const selectedValueSpy = spyOn(service, 'updateListOfSelectedValue'); - service.listOfTagAndTemplateOption = createListOfOption(3); - service.tokenSeparate('option_label_0,b,c', [',']); - expect(selectedValueSpy).toHaveBeenCalledWith(['option_value_0', 'b', 'c'], true); - }); - }); - describe('remove', () => { - it('should removeValueFormSelected work', () => { - service.listOfSelectedValue = ['a', 'b', 'c']; - const selectedValueSpy = spyOn(service, 'updateListOfSelectedValue'); - const option = new NzOptionComponent(); - option.nzValue = 'a'; - service.removeValueFormSelected(option); - expect(selectedValueSpy).toHaveBeenCalledWith(['b', 'c'], true); - }); - }); -}); diff --git a/components/select/nz-select.service.ts b/components/select/nz-select.service.ts deleted file mode 100644 index 5b253a62d1f..00000000000 --- a/components/select/nz-select.service.ts +++ /dev/null @@ -1,389 +0,0 @@ -/** - * @license - * Copyright Alibaba.com All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE - */ - -import { BACKSPACE, DOWN_ARROW, ENTER, SPACE, TAB, UP_ARROW } from '@angular/cdk/keycodes'; -import { Injectable } from '@angular/core'; -import { BehaviorSubject, combineLatest, merge, ReplaySubject, Subject } from 'rxjs'; -import { distinctUntilChanged, filter, map, share, skip, tap } from 'rxjs/operators'; - -import { isNil, isNotNil } from 'ng-zorro-antd/core'; - -import { NzOptionGroupComponent } from './nz-option-group.component'; -import { NzOptionComponent } from './nz-option.component'; -import { defaultFilterOption, NzFilterOptionPipe, TFilterOption } from './nz-option.pipe'; - -@Injectable() -export class NzSelectService { - /** Input params **/ - autoClearSearchValue = true; - serverSearch = false; - filterOption: TFilterOption = defaultFilterOption; - mode: 'default' | 'multiple' | 'tags' = 'default'; - maxMultipleCount = Infinity; - disabled = false; - // tslint:disable-next-line:no-any - compareWith = (o1: any, o2: any) => o1 === o2; - /** selectedValueChanged should emit ngModelChange or not **/ - // tslint:disable-next-line:no-any - private listOfSelectedValueWithEmit$ = new BehaviorSubject<{ value: any[]; emit: boolean }>({ - value: [], - emit: false - }); - /** ContentChildren Change **/ - private mapOfTemplateOption$ = new BehaviorSubject<{ - listOfNzOptionComponent: NzOptionComponent[]; - listOfNzOptionGroupComponent: NzOptionGroupComponent[]; - }>({ - listOfNzOptionComponent: [], - listOfNzOptionGroupComponent: [] - }); - /** searchValue Change **/ - private searchValueRaw$ = new BehaviorSubject(''); - private listOfFilteredOption: NzOptionComponent[] = []; - private openRaw$ = new Subject(); - private checkRaw$ = new Subject(); - private open = false; - clearInput$ = new Subject(); - searchValue = ''; - isShowNotFound = false; - /** animation event **/ - animationEvent$ = new Subject(); - /** open event **/ - open$ = this.openRaw$.pipe(distinctUntilChanged()); - activatedOption: NzOptionComponent | null; - activatedOption$ = new ReplaySubject(1); - listOfSelectedValue$ = this.listOfSelectedValueWithEmit$.pipe(map(data => data.value)); - modelChange$ = this.listOfSelectedValueWithEmit$.pipe( - filter(item => item.emit), - map(data => { - const selectedList = data.value; - let modelValue: any[] | null = null; // tslint:disable-line:no-any - if (this.isSingleMode) { - if (selectedList.length) { - modelValue = selectedList[0]; - } - } else { - modelValue = selectedList; - } - return modelValue; - }) - ); - searchValue$ = this.searchValueRaw$.pipe( - distinctUntilChanged(), - skip(1), - share(), - tap(value => { - this.searchValue = value; - if (value) { - this.updateActivatedOption(this.listOfFilteredOption[0]); - } - this.updateListOfFilteredOption(); - }) - ); - // tslint:disable-next-line:no-any - listOfSelectedValue: any[] = []; - /** flat ViewChildren **/ - listOfTemplateOption: NzOptionComponent[] = []; - /** tag option **/ - listOfTagOption: NzOptionComponent[] = []; - /** tag option concat template option **/ - listOfTagAndTemplateOption: NzOptionComponent[] = []; - /** ViewChildren **/ - listOfNzOptionComponent: NzOptionComponent[] = []; - listOfNzOptionGroupComponent: NzOptionGroupComponent[] = []; - /** click or enter add tag option **/ - addedTagOption: NzOptionComponent | null; - /** display in top control **/ - listOfCachedSelectedOption: NzOptionComponent[] = []; - /** selected value or ViewChildren change **/ - valueOrOption$ = combineLatest([this.listOfSelectedValue$, this.mapOfTemplateOption$]).pipe( - tap(data => { - const [listOfSelectedValue, mapOfTemplateOption] = data; - this.listOfSelectedValue = listOfSelectedValue; - this.listOfNzOptionComponent = mapOfTemplateOption.listOfNzOptionComponent; - this.listOfNzOptionGroupComponent = mapOfTemplateOption.listOfNzOptionGroupComponent; - this.listOfTemplateOption = this.listOfNzOptionComponent.concat( - this.listOfNzOptionGroupComponent.reduce( - (pre, cur) => [...pre, ...cur.listOfNzOptionComponent.toArray()], - [] as NzOptionComponent[] - ) - ); - this.updateListOfTagOption(); - this.updateListOfFilteredOption(); - this.resetActivatedOptionIfNeeded(); - this.updateListOfCachedOption(); - }), - share() - ); - check$ = merge(this.checkRaw$, this.valueOrOption$, this.searchValue$, this.activatedOption$, this.open$, this.modelChange$).pipe( - share() - ); - - clickOption(option: NzOptionComponent): void { - /** update listOfSelectedOption -> update listOfSelectedValue -> next listOfSelectedValue$ **/ - if (!option.nzDisabled) { - this.updateActivatedOption(option); - let listOfSelectedValue = [...this.listOfSelectedValue]; - if (this.isMultipleOrTags) { - const targetValue = listOfSelectedValue.find(o => this.compareWith(o, option.nzValue)); - if (isNotNil(targetValue)) { - listOfSelectedValue.splice(listOfSelectedValue.indexOf(targetValue), 1); - this.updateListOfSelectedValue(listOfSelectedValue, true); - } else if (listOfSelectedValue.length < this.maxMultipleCount) { - listOfSelectedValue.push(option.nzValue); - this.updateListOfSelectedValue(listOfSelectedValue, true); - } - } else if (!this.compareWith(listOfSelectedValue[0], option.nzValue)) { - listOfSelectedValue = [option.nzValue]; - this.updateListOfSelectedValue(listOfSelectedValue, true); - } - if (this.isSingleMode) { - this.setOpenState(false); - } else if (this.autoClearSearchValue) { - this.clearInput(); - } - } - } - - updateListOfCachedOption(): void { - if (this.isSingleMode) { - const selectedOption = this.listOfTemplateOption.find(o => this.compareWith(o.nzValue, this.listOfSelectedValue[0])); - if (!isNil(selectedOption)) { - this.listOfCachedSelectedOption = [selectedOption]; - } - } else { - const listOfCachedSelectedOption: NzOptionComponent[] = []; - this.listOfSelectedValue.forEach(v => { - const listOfMixedOption = [...this.listOfTagAndTemplateOption, ...this.listOfCachedSelectedOption]; - const option = listOfMixedOption.find(o => this.compareWith(o.nzValue, v)); - if (option) { - listOfCachedSelectedOption.push(option); - } - }); - this.listOfCachedSelectedOption = listOfCachedSelectedOption; - } - } - - updateListOfTagOption(): void { - if (this.isTagsMode) { - const listOfMissValue = this.listOfSelectedValue.filter( - value => !this.listOfTemplateOption.find(o => this.compareWith(o.nzValue, value)) - ); - this.listOfTagOption = listOfMissValue.map(value => { - const cachedOption = this.listOfCachedSelectedOption.find(o => this.compareWith(o.nzValue, value)); - if (cachedOption) { - return cachedOption; - } else { - const nzOptionComponent = new NzOptionComponent(); - nzOptionComponent.nzValue = value; - nzOptionComponent.nzLabel = value; - return nzOptionComponent; - } - }); - this.listOfTagAndTemplateOption = [...this.listOfTemplateOption.concat(this.listOfTagOption)]; - } else { - this.listOfTagAndTemplateOption = [...this.listOfTemplateOption]; - } - } - - updateAddTagOption(): void { - const isMatch = this.listOfTagAndTemplateOption.find(item => item.nzLabel === this.searchValue); - if (this.isTagsMode && this.searchValue && !isMatch) { - const option = new NzOptionComponent(); - option.nzValue = this.searchValue; - option.nzLabel = this.searchValue; - this.addedTagOption = option; - this.updateActivatedOption(option); - } else { - this.addedTagOption = null; - } - } - - updateListOfFilteredOption(): void { - this.updateAddTagOption(); - const listOfFilteredOption = new NzFilterOptionPipe().transform( - this.listOfTagAndTemplateOption, - this.searchValue, - this.filterOption, - this.serverSearch - ); - this.listOfFilteredOption = this.addedTagOption ? [this.addedTagOption, ...listOfFilteredOption] : [...listOfFilteredOption]; - this.isShowNotFound = !this.isTagsMode && !this.listOfFilteredOption.length; - } - - clearInput(): void { - this.clearInput$.next(); - } - - // tslint:disable-next-line:no-any - updateListOfSelectedValue(value: any[], emit: boolean): void { - this.listOfSelectedValueWithEmit$.next({ value, emit }); - } - - updateActivatedOption(option: NzOptionComponent | null): void { - this.activatedOption$.next(option); - this.activatedOption = option; - } - - tokenSeparate(inputValue: string, tokenSeparators: string[]): void { - /** auto tokenSeparators **/ - if ( - inputValue && - inputValue.length && - tokenSeparators.length && - this.isMultipleOrTags && - this.includesSeparators(inputValue, tokenSeparators) - ) { - const listOfLabel = this.splitBySeparators(inputValue, tokenSeparators); - this.updateSelectedValueByLabelList(listOfLabel); - this.clearInput(); - } - } - - includesSeparators(str: string | string[], separators: string[]): boolean { - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < separators.length; ++i) { - if (str.lastIndexOf(separators[i]) > 0) { - return true; - } - } - return false; - } - - splitBySeparators(str: string | string[], separators: string[]): string[] { - const reg = new RegExp(`[${separators.join()}]`); - const array = (str as string).split(reg).filter(token => token); - return Array.from(new Set(array)); - } - - resetActivatedOptionIfNeeded(): void { - const resetActivatedOption = () => { - const activatedOption = this.listOfFilteredOption.find(item => this.compareWith(item.nzValue, this.listOfSelectedValue[0])); - this.updateActivatedOption(activatedOption || null); - }; - if (this.activatedOption) { - if ( - !this.listOfFilteredOption.find(item => this.compareWith(item.nzValue, this.activatedOption!.nzValue)) || - !this.listOfSelectedValue.find(item => this.compareWith(item, this.activatedOption!.nzValue)) - ) { - resetActivatedOption(); - } - } else { - resetActivatedOption(); - } - } - - updateTemplateOption(listOfNzOptionComponent: NzOptionComponent[], listOfNzOptionGroupComponent: NzOptionGroupComponent[]): void { - this.mapOfTemplateOption$.next({ listOfNzOptionComponent, listOfNzOptionGroupComponent }); - } - - updateSearchValue(value: string): void { - this.searchValueRaw$.next(value); - } - - updateSelectedValueByLabelList(listOfLabel: string[]): void { - const listOfSelectedValue = [...this.listOfSelectedValue]; - const listOfMatchOptionValue = this.listOfTagAndTemplateOption - .filter(item => listOfLabel.indexOf(item.nzLabel) !== -1) - .map(item => item.nzValue) - .filter(item => !isNotNil(this.listOfSelectedValue.find(v => this.compareWith(v, item)))); - if (this.isMultipleMode) { - this.updateListOfSelectedValue([...listOfSelectedValue, ...listOfMatchOptionValue], true); - } else { - const listOfUnMatchOptionValue = listOfLabel.filter( - label => this.listOfTagAndTemplateOption.map(item => item.nzLabel).indexOf(label) === -1 - ); - this.updateListOfSelectedValue([...listOfSelectedValue, ...listOfMatchOptionValue, ...listOfUnMatchOptionValue], true); - } - } - - onKeyDown(e: KeyboardEvent): void { - if (this.disabled) { - return; - } - const keyCode = e.keyCode; - const eventTarget = e.target as HTMLInputElement; - const listOfFilteredOptionWithoutDisabledOrHidden = this.listOfFilteredOption.filter(item => !item.nzDisabled && !item.nzHide); - const activatedIndex = listOfFilteredOptionWithoutDisabledOrHidden.findIndex(item => item === this.activatedOption); - switch (keyCode) { - case UP_ARROW: - e.preventDefault(); - const preIndex = activatedIndex > 0 ? activatedIndex - 1 : listOfFilteredOptionWithoutDisabledOrHidden.length - 1; - this.updateActivatedOption(listOfFilteredOptionWithoutDisabledOrHidden[preIndex]); - break; - case DOWN_ARROW: - e.preventDefault(); - const nextIndex = activatedIndex < listOfFilteredOptionWithoutDisabledOrHidden.length - 1 ? activatedIndex + 1 : 0; - this.updateActivatedOption(listOfFilteredOptionWithoutDisabledOrHidden[nextIndex]); - if (!this.disabled && !this.open) { - this.setOpenState(true); - } - break; - case ENTER: - e.preventDefault(); - if (this.open) { - if (this.activatedOption && !this.activatedOption.nzDisabled) { - this.clickOption(this.activatedOption); - } - } else { - this.setOpenState(true); - } - break; - case BACKSPACE: - if (this.isMultipleOrTags && !eventTarget.value && this.listOfCachedSelectedOption.length) { - e.preventDefault(); - this.removeValueFormSelected(this.listOfCachedSelectedOption[this.listOfCachedSelectedOption.length - 1]); - } - break; - case SPACE: - if (!this.disabled && !this.open) { - this.setOpenState(true); - e.preventDefault(); - } - break; - case TAB: - this.setOpenState(false); - break; - } - } - - // tslint:disable-next-line:no-any - removeValueFormSelected(option: NzOptionComponent): void { - if (this.disabled || option.nzDisabled) { - return; - } - const listOfSelectedValue = this.listOfSelectedValue.filter(item => !this.compareWith(item, option.nzValue)); - this.updateListOfSelectedValue(listOfSelectedValue, true); - this.clearInput(); - } - - setOpenState(value: boolean): void { - this.openRaw$.next(value); - this.open = value; - } - - check(): void { - this.checkRaw$.next(); - } - - get isSingleMode(): boolean { - return this.mode === 'default'; - } - - get isTagsMode(): boolean { - return this.mode === 'tags'; - } - - get isMultipleMode(): boolean { - return this.mode === 'multiple'; - } - - get isMultipleOrTags(): boolean { - return this.mode === 'tags' || this.mode === 'multiple'; - } -} diff --git a/components/select/option-container.component.ts b/components/select/option-container.component.ts new file mode 100644 index 00000000000..e9dbff99aff --- /dev/null +++ b/components/select/option-container.component.ts @@ -0,0 +1,119 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { NzSafeAny } from 'ng-zorro-antd/core/types'; +import { NzSelectItemInterface, NzSelectModeType } from './select.types'; + +@Component({ + selector: 'nz-option-container', + exportAs: 'nzOptionContainer', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + preserveWhitespaces: false, + template: ` +
    +
    + +
    + +
    + + + + +
    +
    + +
    + `, + host: { + '[class.ant-select-dropdown]': 'true' + } +}) +export class NzOptionContainerComponent implements OnChanges { + @Input() notFoundContent: string | null = null; + @Input() menuItemSelectedIcon: TemplateRef | null = null; + @Input() dropdownRender: TemplateRef | null = null; + @Input() activatedValue: NzSafeAny | null = null; + @Input() listOfSelectedValue: NzSafeAny[] = []; + @Input() compareWith: (o1: NzSafeAny, o2: NzSafeAny) => boolean; + @Input() mode: NzSelectModeType = 'default'; + @Input() listOfContainerItem: NzSelectItemInterface[] = []; + @Output() readonly itemClick = new EventEmitter(); + @Output() readonly itemHover = new EventEmitter(); + @Output() readonly scrollToBottom = new EventEmitter(); + @ViewChild(CdkVirtualScrollViewport, { static: true }) cdkVirtualScrollViewport: CdkVirtualScrollViewport; + private scrolledIndex = 0; + readonly itemSize = 32; + readonly maxItemLength = 8; + + onItemClick(value: NzSafeAny): void { + this.itemClick.emit(value); + } + + onItemHover(value: NzSafeAny): void { + // TODO: bug when mouse inside the option container & keydown + this.itemHover.emit(value); + } + + trackValue(_index: number, option: NzSelectItemInterface): NzSafeAny { + return option.key; + } + + onScrolledIndexChange(index: number): void { + this.scrolledIndex = index; + if (index === this.listOfContainerItem.length - this.maxItemLength) { + this.scrollToBottom.emit(); + } + } + + ngOnChanges(changes: SimpleChanges): void { + const { listOfContainerItem, activatedValue } = changes; + if (listOfContainerItem || activatedValue) { + const index = this.listOfContainerItem.findIndex(item => this.compareWith(item.key, this.activatedValue)); + if (index < this.scrolledIndex || index >= this.scrolledIndex + this.maxItemLength) { + this.cdkVirtualScrollViewport.scrollToIndex(index || 0); + } + } + } +} diff --git a/components/select/option-group.component.ts b/components/select/option-group.component.ts new file mode 100644 index 00000000000..3927bf0530b --- /dev/null +++ b/components/select/option-group.component.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { ChangeDetectionStrategy, Component, Input, OnChanges, TemplateRef, ViewEncapsulation } from '@angular/core'; +import { NzSafeAny } from 'ng-zorro-antd/core/types'; +import { Subject } from 'rxjs'; + +@Component({ + selector: 'nz-option-group', + exportAs: 'nzOptionGroup', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + ` +}) +export class NzOptionGroupComponent implements OnChanges { + @Input() nzLabel: string | TemplateRef | null = null; + changes = new Subject(); + ngOnChanges(): void { + this.changes.next(); + } +} diff --git a/components/select/option-item-group.component.ts b/components/select/option-item-group.component.ts new file mode 100644 index 00000000000..6c198b77964 --- /dev/null +++ b/components/select/option-item-group.component.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { ChangeDetectionStrategy, Component, Input, TemplateRef, ViewEncapsulation } from '@angular/core'; +import { NzSafeAny } from 'ng-zorro-antd/core/types'; + +@Component({ + selector: 'nz-option-item-group', + template: ` + {{ nzLabel }} + `, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + host: { + '[class.ant-select-item]': 'true', + '[class.ant-select-item-group]': 'true' + } +}) +export class NzOptionItemGroupComponent { + @Input() nzLabel: string | TemplateRef | null = null; +} diff --git a/components/select/option-item.component.ts b/components/select/option-item.component.ts new file mode 100644 index 00000000000..48f5fb1d1d3 --- /dev/null +++ b/components/select/option-item.component.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, + TemplateRef, + ViewEncapsulation +} from '@angular/core'; +import { NzSafeAny } from 'ng-zorro-antd/core/types'; + +@Component({ + selector: 'nz-option-item', + template: ` +
    + {{ label }} +
    +
    + +
    + `, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + host: { + '[class.ant-select-item]': 'true', + '[class.ant-select-item-option]': 'true', + '[class.ant-select-item-option-grouped]': 'grouped', + '[class.ant-select-item-option-selected]': 'selected && !disabled', + '[class.ant-select-item-option-disabled]': 'disabled', + '[class.ant-select-item-option-active]': 'activated && !disabled', + '(mouseenter)': 'onHostMouseEnter()', + '(click)': 'onHostClick()' + } +}) +export class NzOptionItemComponent implements OnChanges { + selected = false; + activated = false; + @Input() grouped = false; + @Input() customContent = false; + @Input() template: TemplateRef | null = null; + @Input() disabled = false; + @Input() showState = false; + @Input() label: string | null = null; + @Input() value: NzSafeAny | null = null; + @Input() activatedValue: NzSafeAny | null = null; + @Input() listOfSelectedValue: NzSafeAny[] = []; + @Input() icon: TemplateRef | null = null; + @Input() compareWith: (o1: NzSafeAny, o2: NzSafeAny) => boolean; + @Output() readonly itemClick = new EventEmitter(); + @Output() readonly itemHover = new EventEmitter(); + onHostMouseEnter(): void { + if (!this.disabled) { + this.itemHover.next(this.value); + } + } + onHostClick(): void { + if (!this.disabled) { + this.itemClick.next(this.value); + } + } + ngOnChanges(changes: SimpleChanges): void { + const { value, activatedValue, listOfSelectedValue } = changes; + if (value || listOfSelectedValue) { + this.selected = this.listOfSelectedValue.find(v => this.compareWith(v, this.value)); + } + if (value || activatedValue) { + this.activated = this.compareWith(this.activatedValue, this.value); + } + } +} diff --git a/components/select/option.component.ts b/components/select/option.component.ts new file mode 100644 index 00000000000..cfab90e983e --- /dev/null +++ b/components/select/option.component.ts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, + OnDestroy, + OnInit, + Optional, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; + +import { InputBoolean } from 'ng-zorro-antd/core'; +import { NzSafeAny } from 'ng-zorro-antd/core/types'; +import { Subject } from 'rxjs'; +import { startWith, takeUntil } from 'rxjs/operators'; +import { NzOptionGroupComponent } from './option-group.component'; + +@Component({ + selector: 'nz-option', + exportAs: 'nzOption', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + ` +}) +export class NzOptionComponent implements OnChanges, OnInit, OnDestroy { + private destroy$ = new Subject(); + changes = new Subject(); + groupLabel: string | TemplateRef | null = null; + @ViewChild(TemplateRef, { static: true }) template: TemplateRef; + @Input() nzLabel: string | null = null; + @Input() nzValue: NzSafeAny | null = null; + @Input() @InputBoolean() nzDisabled = false; + @Input() @InputBoolean() nzHide = false; + @Input() @InputBoolean() nzCustomContent = false; + + constructor(@Optional() private nzOptionGroupComponent: NzOptionGroupComponent) {} + + ngOnInit(): void { + if (this.nzOptionGroupComponent) { + this.nzOptionGroupComponent.changes.pipe(startWith(true), takeUntil(this.destroy$)).subscribe(() => { + this.changes.next(); + this.groupLabel = this.nzOptionGroupComponent.nzLabel; + }); + } + } + + ngOnChanges(): void { + this.changes.next(); + } + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/components/select/public-api.ts b/components/select/public-api.ts index 112d6dc9b91..d3a5af37f58 100644 --- a/components/select/public-api.ts +++ b/components/select/public-api.ts @@ -6,13 +6,16 @@ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE */ -export * from './nz-option-group.component'; -export * from './nz-option-container.component'; -export * from './nz-option.component'; -export * from './nz-select.component'; -export * from './nz-select.module'; -export * from './nz-option-li.component'; -export * from './nz-option.pipe'; -export * from './nz-select-top-control.component'; -export * from './nz-select-unselectable.directive'; -export * from './nz-select.service'; +export * from './option-group.component'; +export * from './option-container.component'; +export * from './option.component'; +export * from './select.component'; +export * from './select.module'; +export * from './option-item.component'; +export * from './select-top-control.component'; +export * from './select-search.component'; +export * from './select-item.component'; +export * from './select-clear.component'; +export * from './select-arrow.component'; +export * from './select-placeholder.component'; +export * from './select.types'; diff --git a/components/select/select-arrow.component.ts b/components/select/select-arrow.component.ts new file mode 100644 index 00000000000..ff0ae8876f8 --- /dev/null +++ b/components/select/select-arrow.component.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { ChangeDetectionStrategy, Component, Input, TemplateRef, ViewEncapsulation } from '@angular/core'; +import { NzSafeAny } from 'ng-zorro-antd/core/types'; + +@Component({ + selector: 'nz-select-arrow', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + + + + + `, + host: { + '[class.ant-select-arrow]': 'true', + '[class.ant-select-arrow-loading]': 'loading' + } +}) +export class NzSelectArrowComponent { + @Input() loading = false; + @Input() search = false; + @Input() suffixIcon: TemplateRef | null = null; +} diff --git a/components/select/select-clear.component.ts b/components/select/select-clear.component.ts new file mode 100644 index 00000000000..14bb4691dae --- /dev/null +++ b/components/select/select-clear.component.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, TemplateRef, ViewEncapsulation } from '@angular/core'; +import { NzSafeAny } from 'ng-zorro-antd/core/types'; + +@Component({ + selector: 'nz-select-clear', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + `, + host: { + '(click)': 'onClick($event)', + '[class.ant-select-clear]': 'true' + } +}) +export class NzSelectClearComponent { + @Input() clearIcon: TemplateRef | null = null; + @Output() readonly clear = new EventEmitter(); + onClick(e: MouseEvent): void { + e.preventDefault(); + e.stopPropagation(); + this.clear.emit(e); + } +} diff --git a/components/select/select-item.component.ts b/components/select/select-item.component.ts new file mode 100644 index 00000000000..7cf337b0756 --- /dev/null +++ b/components/select/select-item.component.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, TemplateRef, ViewEncapsulation } from '@angular/core'; +import { NzSafeAny } from 'ng-zorro-antd/core/types'; + +@Component({ + selector: 'nz-select-item', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + +
    {{ label }}
    + {{ label }} +
    + + + + `, + host: { + '[attr.title]': 'label', + '[class.ant-select-selection-item]': 'true', + '[class.ant-select-selection-item-disabled]': 'disabled' + } +}) +export class NzSelectItemComponent { + @Input() disabled = false; + @Input() label: string | null = null; + @Input() deletable = false; + @Input() removeIcon: TemplateRef | null = null; + @Input() contentTemplateOutletContext: NzSafeAny | null = null; + @Input() contentTemplateOutlet: string | TemplateRef | null = null; + @Output() readonly delete = new EventEmitter(); + onDelete(e: MouseEvent): void { + e.preventDefault(); + e.stopPropagation(); + if (!this.disabled) { + this.delete.next(e); + } + } +} diff --git a/components/select/select-placeholder.component.ts b/components/select/select-placeholder.component.ts new file mode 100644 index 00000000000..85ebaa870e3 --- /dev/null +++ b/components/select/select-placeholder.component.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { ChangeDetectionStrategy, Component, Input, TemplateRef, ViewEncapsulation } from '@angular/core'; +import { NzSafeAny } from 'ng-zorro-antd/core/types'; + +@Component({ + selector: 'nz-select-placeholder', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + {{ placeholder }} + + `, + host: { + '[class.ant-select-selection-placeholder]': 'true' + } +}) +export class NzSelectPlaceholderComponent { + @Input() placeholder: TemplateRef | string | null = null; +} diff --git a/components/select/select-search.component.ts b/components/select/select-search.component.ts new file mode 100644 index 00000000000..405c5176019 --- /dev/null +++ b/components/select/select-search.component.ts @@ -0,0 +1,114 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { FocusMonitor } from '@angular/cdk/a11y'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + Output, + Renderer2, + SimpleChanges, + ViewChild, + ViewEncapsulation +} from '@angular/core'; + +@Component({ + selector: 'nz-select-search', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + `, + host: { + '[class.ant-select-selection-search]': 'true' + } +}) +export class NzSelectSearchComponent implements AfterViewInit, OnChanges { + @Input() disabled = false; + @Input() mirrorSync = false; + @Input() showInput = true; + @Input() focusTrigger = false; + @Input() value = ''; + @Input() autofocus = false; + @Output() readonly valueChange = new EventEmitter(); + @Output() readonly isComposingChange = new EventEmitter(); + @ViewChild('inputElement', { static: true }) inputElement: ElementRef; + @ViewChild('mirrorElement', { static: false }) mirrorElement: ElementRef; + + setCompositionState(isComposing: boolean): void { + this.isComposingChange.next(isComposing); + } + + onValueChange(value: string): void { + const inputDOM = this.inputElement.nativeElement; + inputDOM.value = value; + this.value = value; + this.valueChange.next(value); + if (this.mirrorSync) { + this.syncMirrorWidth(); + } + } + + clearInputValue(): void { + this.onValueChange(''); + } + + syncMirrorWidth(): void { + const mirrorDOM = this.mirrorElement.nativeElement; + const hostDOM = this.elementRef.nativeElement; + const inputDOM = this.inputElement.nativeElement; + this.renderer.removeStyle(hostDOM, 'width'); + mirrorDOM.innerHTML = `${inputDOM.value} `; + this.renderer.setStyle(hostDOM, 'width', `${mirrorDOM.scrollWidth}px`); + } + + focus(): void { + this.focusMonitor.focusVia(this.inputElement, 'keyboard'); + } + + blur(): void { + this.inputElement.nativeElement.blur(); + } + + constructor(private elementRef: ElementRef, private renderer: Renderer2, private focusMonitor: FocusMonitor) {} + + ngOnChanges(changes: SimpleChanges): void { + const inputDOM = this.inputElement.nativeElement; + const { focusTrigger } = changes; + if (focusTrigger && focusTrigger.currentValue === true && focusTrigger.previousValue === false) { + inputDOM.focus(); + } + } + + ngAfterViewInit(): void { + if (this.mirrorSync) { + this.syncMirrorWidth(); + } + if (this.autofocus) { + this.focus(); + } + } +} diff --git a/components/select/select-top-control.component.ts b/components/select/select-top-control.component.ts new file mode 100644 index 00000000000..15c4a0c190e --- /dev/null +++ b/components/select/select-top-control.component.ts @@ -0,0 +1,244 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { BACKSPACE } from '@angular/cdk/keycodes'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Host, + Input, + OnChanges, + Optional, + Output, + SimpleChanges, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { NzNoAnimationDirective, zoomMotion } from 'ng-zorro-antd/core'; +import { NzSafeAny } from 'ng-zorro-antd/core/types'; +import { NzSelectSearchComponent } from './select-search.component'; +import { NzSelectItemInterface, NzSelectModeType, NzSelectTopControlItemType } from './select.types'; + +@Component({ + selector: 'nz-select-top-control', + exportAs: 'nzSelectTopControl', + preserveWhitespaces: false, + animations: [zoomMotion], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + template: ` + + + + + + + + + + + + + + + `, + host: { + '[class.ant-select-selector]': 'true', + '(click)': 'onHostClick()', + '(keydown)': 'onHostKeydown($event)' + } +}) +export class NzSelectTopControlComponent implements OnChanges { + @Input() showSearch = false; + @Input() placeHolder: string | TemplateRef | null = null; + @Input() open = false; + @Input() maxTagCount: number = Infinity; + @Input() autofocus = false; + @Input() disabled = false; + @Input() mode: NzSelectModeType = 'default'; + @Input() customTemplate: TemplateRef<{ $implicit: NzSelectItemInterface }> | null = null; + @Input() maxTagPlaceholder: TemplateRef<{ $implicit: NzSafeAny[] }> | null = null; + @Input() listOfTopItem: NzSelectItemInterface[] = []; + @Input() tokenSeparators: string[] = []; + @Output() readonly tokenize = new EventEmitter(); + @Output() readonly inputValueChange = new EventEmitter(); + @Output() readonly animationEnd = new EventEmitter(); + @Output() readonly deleteItem = new EventEmitter(); + @Output() readonly openChange = new EventEmitter(); + @ViewChild(NzSelectSearchComponent) nzSelectSearchComponent: NzSelectSearchComponent; + listOfSlicedItem: NzSelectTopControlItemType[] = []; + isShowPlaceholder = true; + isShowSingleLabel = false; + isComposing = false; + inputValue: string | null = null; + + onHostClick(): void { + if (!this.disabled) { + this.openChange.next(!this.open); + } + } + + onHostKeydown(e: KeyboardEvent): void { + const inputValue = (e.target as HTMLInputElement).value; + if (e.keyCode === BACKSPACE && this.mode !== 'default' && !inputValue && this.listOfTopItem.length > 0) { + e.preventDefault(); + this.onDeleteItem(this.listOfTopItem[this.listOfTopItem.length - 1]); + } + } + + updateTemplateVariable(): void { + const isSelectedValueEmpty = this.listOfTopItem.length === 0; + this.isShowPlaceholder = isSelectedValueEmpty && !this.isComposing && !this.inputValue; + this.isShowSingleLabel = !isSelectedValueEmpty && !this.isComposing && !this.inputValue; + } + + isComposingChange(isComposing: boolean): void { + this.isComposing = isComposing; + this.updateTemplateVariable(); + } + + onInputValueChange(value: string): void { + if (value !== this.inputValue) { + this.inputValue = value; + this.updateTemplateVariable(); + this.inputValueChange.emit(value); + this.tokenSeparate(value, this.tokenSeparators); + } + } + + tokenSeparate(inputValue: string, tokenSeparators: string[]): void { + const includesSeparators = (str: string | string[], separators: string[]): boolean => { + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < separators.length; ++i) { + if (str.lastIndexOf(separators[i]) > 0) { + return true; + } + } + return false; + }; + const splitBySeparators = (str: string | string[], separators: string[]): string[] => { + const reg = new RegExp(`[${separators.join()}]`); + const array = (str as string).split(reg).filter(token => token); + return [...new Set(array)]; + }; + if ( + inputValue && + inputValue.length && + tokenSeparators.length && + this.mode !== 'default' && + includesSeparators(inputValue, tokenSeparators) + ) { + const listOfLabel = splitBySeparators(inputValue, tokenSeparators); + this.tokenize.next(listOfLabel); + } + } + + clearInputValue(): void { + if (this.nzSelectSearchComponent) { + this.nzSelectSearchComponent.clearInputValue(); + } + } + + focus(): void { + if (this.nzSelectSearchComponent) { + this.nzSelectSearchComponent.focus(); + } + } + + blur(): void { + if (this.nzSelectSearchComponent) { + this.nzSelectSearchComponent.blur(); + } + } + + trackValue(_index: number, option: NzSelectTopControlItemType): NzSafeAny { + return option.nzValue; + } + + onDeleteItem(item: NzSelectItemInterface): void { + if (!this.disabled && !item.nzDisabled) { + this.deleteItem.next(item); + } + } + + onAnimationEnd(): void { + this.animationEnd.next(); + } + + constructor(@Host() @Optional() public noAnimation?: NzNoAnimationDirective) {} + + ngOnChanges(changes: SimpleChanges): void { + const { listOfTopItem, maxTagCount, customTemplate, maxTagPlaceholder } = changes; + if (listOfTopItem) { + this.updateTemplateVariable(); + } + if (listOfTopItem || maxTagCount || customTemplate || maxTagPlaceholder) { + const listOfSlicedItem: NzSelectTopControlItemType[] = this.listOfTopItem.slice(0, this.maxTagCount).map(o => { + return { + nzLabel: o.nzLabel, + nzValue: o.nzValue, + nzDisabled: o.nzDisabled, + contentTemplateOutlet: this.customTemplate, + contentTemplateOutletContext: o + }; + }); + if (this.listOfTopItem.length > this.maxTagCount) { + const exceededLabel = `+ ${this.listOfTopItem.length - this.maxTagCount} ...'`; + const listOfSelectedValue = this.listOfTopItem.map(item => item.nzValue); + const exceededItem = { + nzLabel: exceededLabel, + nzValue: '$$__nz_exceeded_item', + nzDisabled: true, + contentTemplateOutlet: this.maxTagPlaceholder, + contentTemplateOutletContext: listOfSelectedValue.slice(this.maxTagCount) + }; + listOfSlicedItem.push(exceededItem); + } + this.listOfSlicedItem = listOfSlicedItem; + } + } +} diff --git a/components/select/select.component.ts b/components/select/select.component.ts new file mode 100644 index 00000000000..d501cfdbd99 --- /dev/null +++ b/components/select/select.component.ts @@ -0,0 +1,532 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { FocusMonitor } from '@angular/cdk/a11y'; +import { DOWN_ARROW, ENTER, SPACE, TAB, UP_ARROW } from '@angular/cdk/keycodes'; +import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedOverlayPositionChange } from '@angular/cdk/overlay'; +import { Platform } from '@angular/cdk/platform'; +import { + AfterContentInit, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChildren, + ElementRef, + EventEmitter, + forwardRef, + Host, + Input, + OnChanges, + OnDestroy, + OnInit, + Optional, + Output, + QueryList, + SimpleChanges, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { InputBoolean, NzNoAnimationDirective, slideMotion } from 'ng-zorro-antd/core'; +import { NzSafeAny, NzSizeLDSType, OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types'; +import { BehaviorSubject, combineLatest, merge, Subject } from 'rxjs'; +import { startWith, switchMap, takeUntil } from 'rxjs/operators'; +import { NzOptionGroupComponent } from './option-group.component'; +import { NzOptionComponent } from './option.component'; +import { NzSelectTopControlComponent } from './select-top-control.component'; +import { NzFilterOptionType, NzSelectItemInterface, NzSelectModeType } from './select.types'; + +const defaultFilterOption: NzFilterOptionType = (searchValue: string, item: NzSelectItemInterface): boolean => { + if (item && item.nzLabel) { + return item.nzLabel.toLowerCase().indexOf(searchValue.toLowerCase()) > -1; + } else { + return false; + } +}; + +@Component({ + selector: 'nz-select', + exportAs: 'nzSelect', + preserveWhitespaces: false, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NzSelectComponent), + multi: true + } + ], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + animations: [slideMotion], + template: ` + + + + + + + `, + host: { + '[class.ant-select]': 'true', + '[class.ant-select-lg]': 'nzSize === "large"', + '[class.ant-select-sm]': 'nzSize === "small"', + '[class.ant-select-show-arrow]': `nzShowArrow && nzMode === 'default'`, + '[class.ant-select-disabled]': 'nzDisabled', + '[class.ant-select-show-search]': `nzShowSearch || nzMode !== 'default'`, + '[class.ant-select-allow-clear]': 'nzAllowClear', + '[class.ant-select-borderless]': 'nzBorderless', + '[class.ant-select-open]': 'nzOpen', + '[class.ant-select-focused]': 'nzOpen', + '[class.ant-select-single]': `nzMode === 'default'`, + '[class.ant-select-multiple]': `nzMode !== 'default'` + } +}) +export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy, AfterContentInit, OnChanges { + @Input() nzSize: NzSizeLDSType = 'default'; + @Input() nzDropdownClassName: string; + @Input() nzDropdownMatchSelectWidth = true; + @Input() nzDropdownStyle: { [key: string]: string }; + @Input() nzNotFoundContent: string; + @Input() nzPlaceHolder: string; + @Input() nzMaxTagCount: number; + @Input() nzDropdownRender: TemplateRef; + @Input() nzCustomTemplate: TemplateRef<{ $implicit: NzSelectItemInterface }>; + @Input() nzSuffixIcon: TemplateRef; + @Input() nzClearIcon: TemplateRef; + @Input() nzRemoveIcon: TemplateRef; + @Input() nzMenuItemSelectedIcon: TemplateRef; + @Input() nzShowArrow = true; + @Input() nzTokenSeparators: string[] = []; + @Input() nzMaxTagPlaceholder: TemplateRef<{ $implicit: NzSafeAny[] }>; + @Input() nzMaxMultipleCount = Infinity; + @Input() nzMode: NzSelectModeType = 'default'; + @Input() nzFilterOption: NzFilterOptionType = defaultFilterOption; + @Input() compareWith: (o1: NzSafeAny, o2: NzSafeAny) => boolean = (o1: NzSafeAny, o2: NzSafeAny) => o1 === o2; + @Input() @InputBoolean() nzAllowClear = false; + @Input() @InputBoolean() nzBorderless = false; + @Input() @InputBoolean() nzShowSearch = false; + @Input() @InputBoolean() nzLoading = false; + @Input() @InputBoolean() nzAutoFocus = false; + @Input() @InputBoolean() nzAutoClearSearchValue = true; + @Input() @InputBoolean() nzServerSearch = false; + @Input() @InputBoolean() nzDisabled = false; + @Input() @InputBoolean() nzOpen = false; + @Output() readonly nzOnSearch = new EventEmitter(); + @Output() readonly nzScrollToBottom = new EventEmitter(); + @Output() readonly nzOpenChange = new EventEmitter(); + @Output() readonly nzBlur = new EventEmitter(); + @Output() readonly nzFocus = new EventEmitter(); + @ViewChild(CdkOverlayOrigin, { static: true, read: ElementRef }) originElement: ElementRef; + @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay; + @ViewChild(NzSelectTopControlComponent, { static: true }) nzSelectTopControlComponent: NzSelectTopControlComponent; + @ContentChildren(NzOptionComponent, { descendants: true }) listOfNzOptionComponent: QueryList; + @ContentChildren(NzOptionGroupComponent, { descendants: true }) listOfNzOptionGroupComponent: QueryList; + @ViewChild(NzOptionGroupComponent, { static: true, read: ElementRef }) nzOptionGroupComponentElement: ElementRef; + @ViewChild(NzSelectTopControlComponent, { static: true, read: ElementRef }) nzSelectTopControlComponentElement: ElementRef; + private listOfValue$ = new BehaviorSubject([]); + private listOfTemplateItem$ = new BehaviorSubject([]); + private listOfTagAndTemplateItem: NzSelectItemInterface[] = []; + private searchValue: string = ''; + private value: NzSafeAny | NzSafeAny[]; + private destroy$ = new Subject(); + onChange: OnChangeType = () => {}; + onTouched: OnTouchedType = () => {}; + dropDownPosition: 'top' | 'center' | 'bottom' = 'bottom'; + triggerWidth: number | null = null; + listOfContainerItem: NzSelectItemInterface[] = []; + listOfTopItem: NzSelectItemInterface[] = []; + activatedValue: NzSafeAny | null = null; + listOfValue: NzSafeAny[] = []; + + generateTagItem(value: string): NzSelectItemInterface { + return { + nzValue: value, + nzLabel: value, + type: 'item' + }; + } + + onItemClick(value: NzSafeAny): void { + this.activatedValue = value; + if (this.nzMode === 'default') { + if (this.listOfValue.length === 0 || !this.compareWith(this.listOfValue[0], value)) { + this.updateListOfValue([value]); + } + this.setOpenState(false); + } else { + const targetIndex = this.listOfValue.findIndex(o => this.compareWith(o, value)); + if (targetIndex !== -1) { + const listOfValueAfterRemoved = this.listOfValue.filter((_, i) => i !== targetIndex); + this.updateListOfValue(listOfValueAfterRemoved); + } else if (this.listOfValue.length < this.nzMaxMultipleCount) { + const listOfValueAfterAdded = [...this.listOfValue, value]; + this.updateListOfValue(listOfValueAfterAdded); + } + this.focus(); + if (this.nzAutoClearSearchValue) { + this.clearInput(); + } + } + } + + onItemDelete(item: NzSelectItemInterface): void { + const listOfSelectedValue = this.listOfValue.filter(v => !this.compareWith(v, item.nzValue)); + this.updateListOfValue(listOfSelectedValue); + this.clearInput(); + } + + onItemHover(value: NzSafeAny): void { + this.activatedValue = value; + } + + updateListOfContainerItem(): void { + let listOfContainerItem = this.listOfTagAndTemplateItem + .filter(item => !item.nzHide) + .filter(item => { + if (!this.nzServerSearch && this.searchValue) { + return this.nzFilterOption(this.searchValue, item); + } else { + return true; + } + }); + if ( + this.nzMode === 'tags' && + this.searchValue && + this.listOfTagAndTemplateItem.findIndex(item => item.nzLabel === this.searchValue) === -1 + ) { + const tagItem = this.generateTagItem(this.searchValue); + listOfContainerItem = [tagItem, ...listOfContainerItem]; + this.activatedValue = tagItem.nzValue; + } + if (listOfContainerItem.findIndex(item => this.compareWith(item.nzValue, this.activatedValue)) === -1) { + const activatedItem = listOfContainerItem.find(item => this.compareWith(item.nzValue, this.listOfValue[0])) || listOfContainerItem[0]; + this.activatedValue = (activatedItem && activatedItem.nzValue) || null; + } + /** insert group item **/ + if (this.listOfNzOptionGroupComponent) { + this.listOfNzOptionGroupComponent.forEach(o => { + const groupItem = { groupLabel: o.nzLabel, type: 'group', key: o.nzLabel } as NzSelectItemInterface; + const index = this.listOfContainerItem.findIndex(item => groupItem.groupLabel === item.groupLabel); + listOfContainerItem.splice(index, 0, groupItem); + }); + } + this.listOfContainerItem = listOfContainerItem; + this.updateCdkConnectedOverlayPositions(); + } + + clearInput(): void { + this.nzSelectTopControlComponent.clearInputValue(); + } + + updateListOfValue(listOfValue: NzSafeAny[]): void { + const covertListToModel = (list: NzSafeAny[], mode: NzSelectModeType): NzSafeAny[] | NzSafeAny => { + if (mode === 'default') { + if (list.length > 0) { + return list[0]; + } else { + return null; + } + } else { + return list; + } + }; + const model = covertListToModel(listOfValue, this.nzMode); + if (this.value !== model) { + this.listOfValue = listOfValue; + this.listOfValue$.next(listOfValue); + this.value = model; + this.onChange(this.value); + } + } + + onTokenSeparate(listOfLabel: string[]): void { + const listOfMatchedValue = this.listOfTagAndTemplateItem + .filter(item => listOfLabel.findIndex(label => label === item.nzLabel) !== -1) + .map(item => item.nzValue) + .filter(item => this.listOfValue.findIndex(v => this.compareWith(v, item)) === -1); + if (this.nzMode === 'multiple') { + this.updateListOfValue([...this.listOfValue, ...listOfMatchedValue]); + } else if (this.nzMode === 'tags') { + const listOfUnMatchedLabel = listOfLabel.filter( + label => this.listOfTagAndTemplateItem.findIndex(item => item.nzLabel === label) === -1 + ); + this.updateListOfValue([...this.listOfValue, ...listOfMatchedValue, ...listOfUnMatchedLabel]); + } + this.clearInput(); + } + + onKeyDown(e: KeyboardEvent): void { + if (this.nzDisabled) { + return; + } + const listOfFilteredOptionNotDisabled = this.listOfContainerItem.filter(item => item.type === 'item').filter(item => !item.nzDisabled); + const activatedIndex = listOfFilteredOptionNotDisabled.findIndex(item => this.compareWith(item.nzValue, this.activatedValue)); + switch (e.keyCode) { + case UP_ARROW: + e.preventDefault(); + if (this.nzOpen) { + const preIndex = activatedIndex > 0 ? activatedIndex - 1 : listOfFilteredOptionNotDisabled.length - 1; + this.activatedValue = listOfFilteredOptionNotDisabled[preIndex].nzValue; + } + break; + case DOWN_ARROW: + e.preventDefault(); + if (this.nzOpen) { + const nextIndex = activatedIndex < listOfFilteredOptionNotDisabled.length - 1 ? activatedIndex + 1 : 0; + this.activatedValue = listOfFilteredOptionNotDisabled[nextIndex].nzValue; + } else { + this.setOpenState(true); + } + break; + case ENTER: + e.preventDefault(); + if (this.nzOpen) { + if (this.activatedValue) { + this.onItemClick(this.activatedValue); + } + } else { + this.setOpenState(true); + } + break; + case SPACE: + if (!this.nzOpen) { + this.setOpenState(true); + e.preventDefault(); + } + break; + case TAB: + this.setOpenState(false); + break; + } + } + + setOpenState(value: boolean): void { + if (this.nzOpen !== value) { + this.nzOpen = value; + this.nzOpenChange.emit(value); + this.onOpenChange(); + this.cdr.markForCheck(); + } + } + + onOpenChange(): void { + this.updateCdkConnectedOverlayStatus(); + this.clearInput(); + } + + onInputValueChange(value: string): void { + this.searchValue = value; + this.updateListOfContainerItem(); + this.nzOnSearch.emit(value); + this.updateCdkConnectedOverlayPositions(); + } + + onClearSelection(): void { + this.updateListOfValue([]); + } + + focus(): void { + this.nzSelectTopControlComponent.focus(); + } + + blur(): void { + this.nzSelectTopControlComponent.blur(); + } + + onPositionChange(position: ConnectedOverlayPositionChange): void { + this.dropDownPosition = position.connectionPair.originY; + } + + updateCdkConnectedOverlayStatus(): void { + if (this.platform.isBrowser && this.originElement.nativeElement) { + this.triggerWidth = this.originElement.nativeElement.getBoundingClientRect().width; + } + } + + updateCdkConnectedOverlayPositions(): void { + if (this.cdkConnectedOverlay.overlayRef) { + this.cdkConnectedOverlay.overlayRef.updatePosition(); + } + } + + constructor( + private cdr: ChangeDetectorRef, + private elementRef: ElementRef, + private platform: Platform, + private focusMonitor: FocusMonitor, + @Host() @Optional() public noAnimation?: NzNoAnimationDirective + ) {} + + writeValue(modelValue: NzSafeAny | NzSafeAny[]): void { + /** https://github.com/angular/angular/issues/14988 **/ + if (this.value !== modelValue) { + this.value = modelValue; + const covertModelToList = (model: NzSafeAny[] | NzSafeAny, mode: NzSelectModeType): NzSafeAny[] => { + if (model === null || model === undefined) { + return []; + } else if (mode === 'default') { + return [model]; + } else { + return model; + } + }; + const listOfValue = covertModelToList(modelValue, this.nzMode); + this.listOfValue = listOfValue; + this.listOfValue$.next(listOfValue); + this.cdr.markForCheck(); + } + } + + registerOnChange(fn: OnChangeType): void { + this.onChange = fn; + } + + registerOnTouched(fn: OnTouchedType): void { + this.onTouched = fn; + } + + setDisabledState(disabled: boolean): void { + this.nzDisabled = disabled; + if (disabled) { + this.setOpenState(false); + } + this.cdr.markForCheck(); + } + + ngOnChanges(changes: SimpleChanges): void { + const { nzOpen, nzDisabled } = changes; + if (nzOpen) { + this.onOpenChange(); + } + if (nzDisabled && this.nzDisabled) { + this.setOpenState(false); + } + } + + ngOnInit(): void { + this.focusMonitor + .monitor(this.elementRef, true) + .pipe(takeUntil(this.destroy$)) + .subscribe(focusOrigin => { + if (!focusOrigin) { + this.nzBlur.emit(); + Promise.resolve().then(() => { + this.onTouched(); + }); + } else { + this.nzFocus.emit(); + } + }); + combineLatest([this.listOfValue$, this.listOfTemplateItem$]) + .pipe(takeUntil(this.destroy$)) + .subscribe(([listOfSelectedValue, listOfTemplateItem]) => { + const listOfTagItem = listOfSelectedValue + .filter(() => this.nzMode === 'tags') + .filter(value => listOfTemplateItem.findIndex(o => this.compareWith(o.nzValue, value)) === -1) + .map(value => this.listOfTopItem.find(o => this.compareWith(o.nzValue, value)) || this.generateTagItem(value)); + this.listOfTagAndTemplateItem = [...listOfTemplateItem, ...listOfTagItem]; + this.listOfTopItem = this.listOfValue + .map(v => [...this.listOfTagAndTemplateItem, ...this.listOfTopItem].find(item => this.compareWith(v, item.nzValue))!) + .filter(item => !!item); + this.activatedValue = listOfSelectedValue[0] || null; + this.updateListOfContainerItem(); + }); + } + + ngAfterViewInit(): void { + this.updateCdkConnectedOverlayStatus(); + } + + ngAfterContentInit(): void { + this.listOfNzOptionComponent.changes + .pipe( + startWith(true), + switchMap(() => + merge(...[this.listOfNzOptionComponent.changes, ...this.listOfNzOptionComponent.map(option => option.changes)]).pipe( + startWith(true) + ) + ) + ) + .subscribe(() => { + const listOfOptionInterface = this.listOfNzOptionComponent.toArray().map(item => { + const { template, nzLabel, nzValue, nzDisabled, nzHide, nzCustomContent, groupLabel } = item; + return { template, nzLabel, nzValue, nzDisabled, nzHide, nzCustomContent, groupLabel, type: 'item', key: nzValue }; + }); + this.listOfTemplateItem$.next(listOfOptionInterface); + this.cdr.markForCheck(); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/components/select/select.module.ts b/components/select/select.module.ts new file mode 100644 index 00000000000..f11855949b4 --- /dev/null +++ b/components/select/select.module.ts @@ -0,0 +1,72 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ +import { A11yModule } from '@angular/cdk/a11y'; +import { OverlayModule } from '@angular/cdk/overlay'; +import { PlatformModule } from '@angular/cdk/platform'; +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NzNoAnimationModule, NzOutletModule, NzOverlayModule } from 'ng-zorro-antd/core'; +import { NzEmptyModule } from 'ng-zorro-antd/empty'; +import { NzI18nModule } from 'ng-zorro-antd/i18n'; +import { NzIconModule } from 'ng-zorro-antd/icon'; +import { NzOptionContainerComponent } from './option-container.component'; +import { NzOptionGroupComponent } from './option-group.component'; +import { NzOptionItemGroupComponent } from './option-item-group.component'; +import { NzOptionItemComponent } from './option-item.component'; +import { NzOptionComponent } from './option.component'; +import { NzSelectArrowComponent } from './select-arrow.component'; +import { NzSelectClearComponent } from './select-clear.component'; +import { NzSelectItemComponent } from './select-item.component'; +import { NzSelectPlaceholderComponent } from './select-placeholder.component'; +import { NzSelectSearchComponent } from './select-search.component'; +import { NzSelectTopControlComponent } from './select-top-control.component'; +import { NzSelectComponent } from './select.component'; + +@NgModule({ + imports: [ + CommonModule, + NzI18nModule, + FormsModule, + PlatformModule, + OverlayModule, + NzIconModule, + NzOutletModule, + NzEmptyModule, + NzOverlayModule, + NzNoAnimationModule, + ScrollingModule, + A11yModule + ], + declarations: [ + NzOptionComponent, + NzSelectComponent, + NzOptionContainerComponent, + NzOptionGroupComponent, + NzOptionItemComponent, + NzSelectTopControlComponent, + NzSelectSearchComponent, + NzSelectItemComponent, + NzSelectClearComponent, + NzSelectArrowComponent, + NzSelectPlaceholderComponent, + NzOptionItemGroupComponent + ], + exports: [ + NzOptionComponent, + NzSelectComponent, + NzOptionGroupComponent, + NzSelectArrowComponent, + NzSelectClearComponent, + NzSelectItemComponent, + NzSelectPlaceholderComponent, + NzSelectSearchComponent + ] +}) +export class NzSelectModule {} diff --git a/components/select/select.types.ts b/components/select/select.types.ts new file mode 100644 index 00000000000..7d83178a89a --- /dev/null +++ b/components/select/select.types.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { TemplateRef } from '@angular/core'; +import { NzSafeAny } from 'ng-zorro-antd/core/types'; + +export type NzSelectModeType = 'default' | 'multiple' | 'tags'; +export interface NzSelectItemInterface { + template?: TemplateRef | null; + nzLabel: string | null; + nzValue: NzSafeAny | null; + nzDisabled?: boolean; + nzHide?: boolean; + nzCustomContent?: boolean; + groupLabel?: string | TemplateRef | null; + type?: string; + key?: NzSafeAny; +} + +export type NzSelectTopControlItemType = Partial & { + contentTemplateOutlet: TemplateRef | null; + contentTemplateOutletContext: NzSafeAny; +}; + +export type NzFilterOptionType = (input: string, option: NzSelectItemInterface) => boolean; diff --git a/components/select/style/entry.less b/components/select/style/entry.less index 517e66a923a..4bee3bde774 100644 --- a/components/select/style/entry.less +++ b/components/select/style/entry.less @@ -1,3 +1,4 @@ @import './index.less'; // style dependencies @import '../../empty/style/index.less'; +@import './patch.less'; diff --git a/components/select/style/patch.less b/components/select/style/patch.less new file mode 100644 index 00000000000..045d2265a34 --- /dev/null +++ b/components/select/style/patch.less @@ -0,0 +1,9 @@ +.ant-select-dropdown { + top: 100%; + left: 0; + position: relative; + width: 100%; + margin-top: 4px; + margin-bottom: 4px; + display: block; +}