ng-ytl-zorro-antd
Version:
An enterprise-class UI components based on Ant Design and Angular
809 lines (735 loc) • 25.6 kB
text/typescript
/**
* 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';
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;
searchInputElementRef;
trigger: ElementRef;
dropdownUl: ElementRef;
nzSearchChange: EventEmitter<string> = new EventEmitter();
nzOpenChange: EventEmitter<boolean> = new EventEmitter();
nzScrollToBottom: EventEmitter<boolean> = new EventEmitter();
nzFilter = true;
nzMaxMultiple = Infinity;
_cdkOverlay: CdkConnectedOverlay;
set nzAllowClear(value: boolean) {
this._allowClear = toBoolean(value);
}
get nzAllowClear(): boolean {
return this._allowClear;
}
set nzKeepUnListOptions(value: boolean) {
this._keepUnListOptions = toBoolean(value);
}
get nzKeepUnListOptions(): boolean {
return this._keepUnListOptions;
}
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;
}
}
set nzMultiple(value: boolean) {
this._isMultiple = toBoolean(value);
if (this._isMultiple) {
this.nzShowSearch = true;
}
}
get nzMultiple(): boolean {
return this._isMultiple;
}
set nzPlaceHolder(value: string) {
this._placeholder = value;
}
get nzPlaceHolder(): string {
return this._placeholder;
}
set nzNotFoundContent(value: string) {
this._notFoundContent = value;
}
get nzNotFoundContent(): string {
return this._notFoundContent;
}
set nzSize(value: string) {
this._size = { large: 'lg', small: 'sm' }[ value ];
this.setClassMap();
}
get nzSize(): string {
return this._size;
}
set nzShowSearch(value: boolean) {
this._showSearch = toBoolean(value);
}
get nzShowSearch(): boolean {
return this._showSearch;
}
set nzTags(value: boolean) {
const isTags = toBoolean(value);
this._isTags = isTags;
this.nzMultiple = isTags;
}
get nzTags(): boolean {
return this._isTags;
}
set nzDisabled(value: boolean) {
this._disabled = toBoolean(value);
this.closeDropDown();
this.setClassMap();
}
get nzDisabled(): boolean {
return this._disabled;
}
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);
}
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();
});
}
}
}
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);
}
}
}
}