UNPKG

ng-ytl-zorro-antd

Version:

An enterprise-class UI components based on Ant Design and Angular

809 lines (735 loc) 25.6 kB
/** * complex but work well * TODO: rebuild latter */ import { DOWN_ARROW, ENTER, TAB } from '@angular/cdk/keycodes'; import { CdkConnectedOverlay, ConnectedOverlayPositionChange } from '@angular/cdk/overlay'; import { forwardRef, AfterContentChecked, AfterContentInit, Component, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, Renderer2, ViewChild, ViewEncapsulation, } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { dropDownAnimation } from '../core/animation/dropdown-animations'; import { tagAnimation } from '../core/animation/tag-animations'; import { NzLocaleService } from '../locale/index'; import { toBoolean } from '../util/convert'; import { NzOptionComponent } from './nz-option.component'; import { NzOptionPipe } from './nz-option.pipe'; @Component({ selector : 'nz-select', encapsulation: ViewEncapsulation.None, providers : [ { provide : NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NzSelectComponent), multi : true } ], animations : [ dropDownAnimation, tagAnimation ], template : ` <div tabindex="0" #trigger cdkOverlayOrigin #origin="cdkOverlayOrigin" [ngClass]="_selectionClassMap" (keydown.Enter)="handleKeyEnterEvent($event)" (keydown.Backspace)="handleKeyBackspaceEvent($event)" (keydown.ArrowUp)="handleKeyUpEvent($event)" (keydown.ArrowDown)="handleKeyDownEvent($event)"> <div class="ant-select-selection__rendered" *ngIf="!nzShowSearch"> <div class="ant-select-selection__placeholder"> <ng-template [ngIf]="!_selectedOption"> {{ nzPlaceHolder }} </ng-template> </div> <div class="ant-select-selection-selected-value"> {{ _selectedOption?.nzLabel }} </div> </div> <div class="ant-select-selection__rendered" *ngIf="nzShowSearch"> <div [hidden]="_searchText||(!nzOpen&&_selectedOption)||_selectedOptions.size" class="ant-select-selection__placeholder"> <ng-template [ngIf]="(!_composing)&&_selectedOption"> {{ _selectedOption.nzLabel }} </ng-template> <ng-template [ngIf]="(!_composing)&&(!_selectedOption)"> {{ nzPlaceHolder }} </ng-template> </div> <ul *ngIf="nzMultiple"> <li *ngFor="let option of _selectedOptions" [@tagAnimation] [attr.title]="option?.nzLabel || option?.nzValue" class="ant-select-selection__choice" style="-webkit-user-select: none;"> <div class="ant-select-selection__choice__content">{{ option?.nzLabel }}</div><!----><span class="ant-select-selection__choice__remove" (click)="unSelectMultipleOption(option,$event)"></span> </li> <li class="ant-select-search ant-select-search--inline"> <div class="ant-select-search__field__wrap"> <input class="ant-select-search__field" (compositionstart)="compositionStart()" (compositionend)="compositionEnd();updateWidth(searchInput,_searchText);" [(ngModel)]="_searchText" (ngModelChange)="updateFilterOption();onSearchChange($event);" (keydown)="updateWidth(searchInput,_searchText)" (input)="updateWidth(searchInput,_searchText)" (blur)="onTouched()" #searchInput> <span class="ant-select-search__field__mirror"></span></div> </li> </ul> <div *ngIf="!nzMultiple" class="ant-select-selection-selected-value" [hidden]="!(_selectedOption?.nzLabel)||nzOpen"> {{ _selectedOption?.nzLabel }} </div> <div *ngIf="!nzMultiple" [hidden]="!nzOpen" class="ant-select-search ant-select-search--inline"> <div class="ant-select-search__field__wrap"> <input class="ant-select-search__field" (blur)="onTouched()" (compositionstart)="compositionStart()" (compositionend)="compositionEnd()" [(ngModel)]="_searchText" (ngModelChange)="updateFilterOption();onSearchChange($event);" #searchInput> <span class="ant-select-search__field__mirror"></span> </div> </div> </div> <span (click)="onTouched();clearSelect($event)" class="ant-select-selection__clear" style="-webkit-user-select: none;" *ngIf="_selectedOption?.nzLabel&&nzAllowClear&&!nzMultiple"> </span> <span class="ant-select-arrow"><b></b></span></div> <ng-template cdkConnectedOverlay cdkConnectedOverlayHasBackdrop [cdkConnectedOverlayOrigin]="origin" (backdropClick)="closeDropDown()" (detach)="closeDropDown();" (positionChange)="onPositionChange($event)" [cdkConnectedOverlayWidth]="_triggerWidth" [cdkConnectedOverlayOpen]="_isOpen" > <div [ngClass]="_dropDownClassMap" [@dropDownAnimation]="_dropDownPosition"> <div style="overflow: auto;"> <ul class="ant-select-dropdown-menu ant-select-dropdown-menu-vertical ant-select-dropdown-menu-root" #dropdownUl (scroll)="dropDownScroll(dropdownUl)"> <li *ngFor="let option of _filterOptions" [class.ant-select-dropdown-menu-item-disabled]="option.nzDisabled" [class.ant-select-dropdown-menu-item-active]="option.nzValue == _activeFilterOption?.nzValue" [class.ant-select-dropdown-menu-item-selected]="(option.nzValue==(_selectedOption?.nzValue))||(isInSet(_selectedOptions,option))" class="ant-select-dropdown-menu-item" (click)="clickOption(option,$event)"> <ng-template *ngIf="option.nzOptionTemplate" [ngTemplateOutlet]="option.nzOptionTemplate"> </ng-template> <ng-template [ngIf]="!option.nzOptionTemplate"> {{ option.nzLabel }} </ng-template> </li> </ul> </div> </div> </ng-template>`, styleUrls : [ './style/index.less', './style/patch.less' ] }) export class NzSelectComponent implements OnInit, AfterContentInit, AfterContentChecked, ControlValueAccessor { private _allowClear = false; private _disabled = false; private _isTags = false; private _isMultiple = false; private _keepUnListOptions = false; private _showSearch = false; _el: HTMLElement; _isOpen = false; _prefixCls = 'ant-select'; _classList: string[] = []; _dropDownClassMap; _dropDownPrefixCls = `${this._prefixCls}-dropdown`; _selectionClassMap; _selectionPrefixCls = `${this._prefixCls}-selection`; _size: string; _value: string[] | string; _placeholder = 'placeholder'; _notFoundContent = this._locale.translate('Select.notFoundContent'); _searchText = ''; _triggerWidth = 0; _selectedOption: NzOptionComponent; _operatingMultipleOption: NzOptionComponent; _selectedOptions: Set<NzOptionComponent> = new Set(); _options: NzOptionComponent[] = []; _cacheOptions: NzOptionComponent[] = []; _filterOptions: NzOptionComponent[] = []; _tagsOptions: NzOptionComponent[] = []; _activeFilterOption: NzOptionComponent; _isMultiInit = false; _dropDownPosition: 'top' | 'center' | 'bottom' = 'bottom'; _composing = false; _mode; // ngModel Access onChange: (value: string | string[]) => void = () => null; onTouched: () => void = () => null; @ViewChild('searchInput') searchInputElementRef; @ViewChild('trigger') trigger: ElementRef; @ViewChild('dropdownUl') dropdownUl: ElementRef; @Output() nzSearchChange: EventEmitter<string> = new EventEmitter(); @Output() nzOpenChange: EventEmitter<boolean> = new EventEmitter(); @Output() nzScrollToBottom: EventEmitter<boolean> = new EventEmitter(); @Input() nzFilter = true; @Input() nzMaxMultiple = Infinity; @ViewChild(CdkConnectedOverlay) _cdkOverlay: CdkConnectedOverlay; @Input() set nzAllowClear(value: boolean) { this._allowClear = toBoolean(value); } get nzAllowClear(): boolean { return this._allowClear; } @Input() set nzKeepUnListOptions(value: boolean) { this._keepUnListOptions = toBoolean(value); } get nzKeepUnListOptions(): boolean { return this._keepUnListOptions; } @Input() set nzMode(value: string) { this._mode = value; if (this._mode === 'multiple') { this.nzMultiple = true; } else if (this._mode === 'tags') { this.nzTags = true; } else if (this._mode === 'combobox') { this.nzShowSearch = true; } } @Input() set nzMultiple(value: boolean) { this._isMultiple = toBoolean(value); if (this._isMultiple) { this.nzShowSearch = true; } } get nzMultiple(): boolean { return this._isMultiple; } @Input() set nzPlaceHolder(value: string) { this._placeholder = value; } get nzPlaceHolder(): string { return this._placeholder; } @Input() set nzNotFoundContent(value: string) { this._notFoundContent = value; } get nzNotFoundContent(): string { return this._notFoundContent; } @Input() set nzSize(value: string) { this._size = { large: 'lg', small: 'sm' }[ value ]; this.setClassMap(); } get nzSize(): string { return this._size; } @Input() set nzShowSearch(value: boolean) { this._showSearch = toBoolean(value); } get nzShowSearch(): boolean { return this._showSearch; } @Input() set nzTags(value: boolean) { const isTags = toBoolean(value); this._isTags = isTags; this.nzMultiple = isTags; } get nzTags(): boolean { return this._isTags; } @Input() set nzDisabled(value: boolean) { this._disabled = toBoolean(value); this.closeDropDown(); this.setClassMap(); } get nzDisabled(): boolean { return this._disabled; } @Input() set nzOpen(value: boolean) { const isOpen = toBoolean(value); if (this._isOpen === isOpen) { return; } if (isOpen) { this.scrollToActive(); this._setTriggerWidth(); } this._isOpen = isOpen; this.nzOpenChange.emit(this._isOpen); this.setClassMap(); if (this._isOpen) { setTimeout(() => { this.checkDropDownScroll(); }); } } get nzOpen(): boolean { return this._isOpen; } /** new nz-option insert or new tags insert */ addOption = (option) => { this._options.push(option); if (!this._isTags) { if (option.nzValue) { this.updateSelectedOption(this._value); } else { this.forceUpdateSelectedOption(this._value); } } } /** nz-option remove or tags remove */ removeOption(option: NzOptionComponent): void { this._options.splice(this._options.indexOf(option), 1); if (!this._isTags) { this.forceUpdateSelectedOption(this._value); } } /** dropdown position changed */ onPositionChange(position: ConnectedOverlayPositionChange): void { this._dropDownPosition = position.connectionPair.originY; } compositionStart(): void { this._composing = true; } compositionEnd(): void { this._composing = false; } /** clear single selected option */ clearSelect($event?: MouseEvent): void { if ($event) { $event.preventDefault(); $event.stopPropagation(); } this._selectedOption = null; this.nzValue = null; this.onChange(null); } /** click dropdown option by user */ clickOption(option: NzOptionComponent, $event?: MouseEvent): void { if (!option) { return; } this.chooseOption(option, true, $event); this.clearSearchText(); if (!this._isMultiple) { this.nzOpen = false; } } /** choose option */ chooseOption(option: NzOptionComponent, isUserClick: boolean = false, $event?: MouseEvent): void { if ($event) { $event.preventDefault(); $event.stopPropagation(); } this._activeFilterOption = option; if (option && !option.nzDisabled) { if (!this.nzMultiple) { this._selectedOption = option; this._value = option.nzValue; if (isUserClick) { this.onChange(option.nzValue); } } else { if (isUserClick) { this.isInSet(this._selectedOptions, option) ? this.unSelectMultipleOption(option) : this.selectMultipleOption(option); } } } } updateWidth(element: HTMLInputElement, text: string): void { if (text) { /** wait for scroll width change */ setTimeout(_ => { this._renderer.setStyle(element, 'width', `${element.scrollWidth}px`); }); } else { this._renderer.removeStyle(element, 'width'); } } /** determine if option in set */ isInSet(set: Set<NzOptionComponent>, option: NzOptionComponent): NzOptionComponent { return ((Array.from(set) as NzOptionComponent[]).find((data: NzOptionComponent) => data.nzValue === option.nzValue)); } /** cancel select multiple option */ unSelectMultipleOption = (option, $event?, emitChange = true) => { this._operatingMultipleOption = option; this._selectedOptions.delete(option); if (emitChange) { this.emitMultipleOptions(); } // 对Tag进行特殊处理 if (this._isTags && (this._options.indexOf(option) !== -1) && (this._tagsOptions.indexOf(option) !== -1)) { this.removeOption(option); this._tagsOptions.splice(this._tagsOptions.indexOf(option), 1); } if ($event) { $event.preventDefault(); $event.stopPropagation(); } } /** select multiple option */ selectMultipleOption(option: NzOptionComponent, $event?: MouseEvent): void { /** if tags do push to tag option */ if (this._isTags && (this._options.indexOf(option) === -1) && (this._tagsOptions.indexOf(option) === -1)) { this.addOption(option); this._tagsOptions.push(option); } this._operatingMultipleOption = option; if (this._selectedOptions.size < this.nzMaxMultiple) { this._selectedOptions.add(option); } this.emitMultipleOptions(); if ($event) { $event.preventDefault(); $event.stopPropagation(); } } /** emit multiple options */ emitMultipleOptions(): void { if (this._isMultiInit) { return; } const arrayOptions = Array.from(this._selectedOptions); this._value = arrayOptions.map(item => item.nzValue); this.onChange(this._value); } /** update selected option when add remove option etc */ updateSelectedOption(currentModelValue: string | string[], triggerByNgModel: boolean = false): void { if (currentModelValue == null) { return; } if (this.nzMultiple) { const selectedOptions = this._options.filter((item) => { return (item != null) && (currentModelValue.indexOf(item.nzValue) !== -1); }); if ((this.nzKeepUnListOptions || this.nzTags) && (!triggerByNgModel)) { const _selectedOptions = Array.from(this._selectedOptions); selectedOptions.forEach(option => { const _exist = _selectedOptions.some(item => item._value === option._value); if (!_exist) { this._selectedOptions.add(option); } }); } else { this._selectedOptions = new Set(); selectedOptions.forEach(option => { this._selectedOptions.add(option); }); } } else { const selectedOption = this._options.filter((item) => { return (item != null) && (item.nzValue === currentModelValue); }); this.chooseOption(selectedOption[ 0 ]); } } forceUpdateSelectedOption(value: string | string[]): void { /** trigger dirty check */ setTimeout(_ => { this.updateSelectedOption(value); }); } get nzValue(): string | string[] { return this._value; } set nzValue(value: string | string[]) { this._updateValue(value); } clearAllSelectedOption(emitChange: boolean = true): void { this._selectedOptions.forEach(item => { this.unSelectMultipleOption(item, null, emitChange); }); } handleKeyEnterEvent(event: KeyboardEvent): void { /** when composing end */ if (!this._composing && this._isOpen) { event.preventDefault(); event.stopPropagation(); this.updateFilterOption(false); this.clickOption(this._activeFilterOption); } } handleKeyBackspaceEvent(event: KeyboardEvent): void { if ((!this._searchText) && (!this._composing) && (this._isMultiple)) { event.preventDefault(); const lastOption = Array.from(this._selectedOptions).pop(); this.unSelectMultipleOption(lastOption); } } handleKeyDownEvent($event: MouseEvent): void { if (this._isOpen) { $event.preventDefault(); $event.stopPropagation(); this._activeFilterOption = this.nextOption(this._activeFilterOption, this._filterOptions.filter(w => !w.nzDisabled)); this.scrollToActive(); } } handleKeyUpEvent($event: MouseEvent): void { if (this._isOpen) { $event.preventDefault(); $event.stopPropagation(); this._activeFilterOption = this.preOption(this._activeFilterOption, this._filterOptions.filter(w => !w.nzDisabled)); this.scrollToActive(); } } preOption(option: NzOptionComponent, options: NzOptionComponent[]): NzOptionComponent { return options[ options.indexOf(option) - 1 ] || options[ options.length - 1 ]; } nextOption(option: NzOptionComponent, options: NzOptionComponent[]): NzOptionComponent { return options[ options.indexOf(option) + 1 ] || options[ 0 ]; } clearSearchText(): void { this._searchText = ''; this.updateFilterOption(); } updateFilterOption(updateActiveFilter: boolean = true): void { if (this.nzFilter) { this._filterOptions = new NzOptionPipe().transform(this._options, { 'searchText' : this._searchText, 'tags' : this._isTags, 'notFoundContent': this._isTags ? this._searchText : this._notFoundContent, 'disabled' : !this._isTags, 'value' : this._isTags ? this._searchText : 'disabled' }); } else { this._filterOptions = this._options; } /** TODO: cause pre & next key selection not work */ if (updateActiveFilter && !this._selectedOption) { this._activeFilterOption = this._filterOptions[ 0 ]; } } onSearchChange(searchValue: string): void { this.nzSearchChange.emit(searchValue); } @HostListener('click', [ '$event' ]) onClick(e: MouseEvent): void { e.preventDefault(); if (!this._disabled) { this.nzOpen = !this.nzOpen; if (this.nzShowSearch) { /** wait for search display */ setTimeout(_ => { this.searchInputElementRef.nativeElement.focus(); }); } } } @HostListener('keydown', [ '$event' ]) onKeyDown(e: KeyboardEvent): void { const keyCode = e.keyCode; if (keyCode === TAB && this.nzOpen) { this.nzOpen = false; return; } if ((keyCode !== DOWN_ARROW && keyCode !== ENTER) || this.nzOpen) { return; } e.preventDefault(); if (!this._disabled) { this.nzOpen = true; if (this.nzShowSearch) { /** wait for search display */ setTimeout(_ => { this.searchInputElementRef.nativeElement.focus(); }); } } } closeDropDown(): void { if (!this.nzOpen) { return; } this.onTouched(); if (this.nzMultiple) { this._renderer.removeStyle(this.searchInputElementRef.nativeElement, 'width'); } this.clearSearchText(); this.nzOpen = false; } setClassMap(): void { this._classList.forEach(_className => { this._renderer.removeClass(this._el, _className); }); this._classList = [ this._prefixCls, (this._mode === 'combobox') && `${this._prefixCls}-combobox`, (!this._disabled) && `${this._prefixCls}-enabled`, (this._disabled) && `${this._prefixCls}-disabled`, this._isOpen && `${this._prefixCls}-open`, this._showSearch && `${this._prefixCls}-show-search`, this._size && `${this._prefixCls}-${this._size}` ].filter((item) => { return !!item; }); this._classList.forEach(_className => { this._renderer.addClass(this._el, _className); }); this._selectionClassMap = { [this._selectionPrefixCls] : true, [`${this._selectionPrefixCls}--single`] : !this.nzMultiple, [`${this._selectionPrefixCls}--multiple`]: this.nzMultiple }; } setDropDownClassMap(): void { this._dropDownClassMap = { [this._dropDownPrefixCls] : true, ['component-select'] : this._mode === 'combobox', [`${this._dropDownPrefixCls}--single`] : !this.nzMultiple, [`${this._dropDownPrefixCls}--multiple`] : this.nzMultiple, [`${this._dropDownPrefixCls}-placement-bottomLeft`]: this._dropDownPosition === 'bottom', [`${this._dropDownPrefixCls}-placement-topLeft`] : this._dropDownPosition === 'top' }; } scrollToActive(): void { /** wait for dropdown display */ setTimeout(_ => { if (this._activeFilterOption && this._activeFilterOption.nzValue) { const index = this._filterOptions.findIndex(option => option.nzValue === this._activeFilterOption.nzValue); try { const scrollPane = this.dropdownUl.nativeElement.children[ index ] as HTMLLIElement; // TODO: scrollIntoViewIfNeeded is not a standard API, why doing so? /* tslint:disable-next-line:no-any */ (scrollPane as any).scrollIntoViewIfNeeded(false); } catch (e) { } } }); } flushComponentState(): void { this.updateFilterOption(); if (!this.nzMultiple) { this.updateSelectedOption(this._value); } else { if (this._value) { this.updateSelectedOption(this._value); } } } _setTriggerWidth(): void { this._triggerWidth = this._getTriggerRect().width; /** should remove after after https://github.com/angular/material2/pull/8765 merged **/ if (this._cdkOverlay && this._cdkOverlay.overlayRef) { this._cdkOverlay.overlayRef.updateSize({ width: this._triggerWidth }); } } _getTriggerRect(): ClientRect { return this.trigger.nativeElement.getBoundingClientRect(); } writeValue(value: string | string[]): void { this._updateValue(value, false); } registerOnChange(fn: (value: string | string[]) => void): void { this.onChange = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; } setDisabledState(isDisabled: boolean): void { this.nzDisabled = isDisabled; } dropDownScroll(ul: HTMLUListElement): void { if (ul && (ul.scrollHeight - ul.scrollTop === ul.clientHeight)) { this.nzScrollToBottom.emit(true); } } checkDropDownScroll(): void { if (this.dropdownUl && (this.dropdownUl.nativeElement.scrollHeight === this.dropdownUl.nativeElement.clientHeight)) { this.nzScrollToBottom.emit(true); } } constructor(private _elementRef: ElementRef, private _renderer: Renderer2, private _locale: NzLocaleService) { this._el = this._elementRef.nativeElement; } ngAfterContentInit(): void { if (this._value != null) { this.flushComponentState(); } } ngOnInit(): void { this.updateFilterOption(); this.setClassMap(); this.setDropDownClassMap(); } ngAfterContentChecked(): void { if (this._cacheOptions !== this._options) { /** update filter option after every content check cycle */ this.updateFilterOption(); this._cacheOptions = this._options; } else { this.updateFilterOption(false); } } private _updateValue(value: string[] | string, emitChange: boolean = true): void { if (this._value === value) { return; } if ((value == null) && this.nzMultiple) { this._value = []; } else { this._value = value; } if (!this.nzMultiple) { if (value == null) { this._selectedOption = null; } else { this.updateSelectedOption(value); } } else { if (value) { if (value.length === 0) { this.clearAllSelectedOption(emitChange); } else { this.updateSelectedOption(value, true); } } else if (value == null) { this.clearAllSelectedOption(emitChange); } } } }