diff --git a/components/cascader/demo/custom-field-names.md b/components/cascader/demo/custom-field-names.md new file mode 100644 index 00000000000..6e70f37e193 --- /dev/null +++ b/components/cascader/demo/custom-field-names.md @@ -0,0 +1,15 @@ +--- +order: 16 +title: + zh-CN: 自定义字段名 + en-US: Custom Field Names +--- + +## zh-CN + +自定义字段名。 + +## en-US + +Custom field names. + diff --git a/components/cascader/demo/custom-field-names.ts b/components/cascader/demo/custom-field-names.ts new file mode 100644 index 00000000000..25952fd5fe4 --- /dev/null +++ b/components/cascader/demo/custom-field-names.ts @@ -0,0 +1,73 @@ +// tslint:disable:no-any +import { Component } from '@angular/core'; + +const options = [{ + code: 'zhejiang', + name: 'Zhejiang', + children: [{ + code: 'hangzhou', + name: 'Hangzhou', + children: [{ + code: 'xihu', + name: 'West Lake', + isLeaf: true + }] + }, { + code: 'ningbo', + name: 'Ningbo', + children: [{ + code: 'dongqianlake', + name: 'Dongqian Lake', + isLeaf: true + }] + }] +}, { + code: 'jiangsu', + name: 'Jiangsu', + children: [{ + code: 'nanjing', + name: 'Nanjing', + children: [{ + code: 'zhonghuamen', + name: 'Zhong Hua Men', + isLeaf: true + }] + }] +}]; + +@Component({ + selector: 'nz-demo-cascader-custom-field-names', + template: ` + + `, + styles : [ + ` + .ant-cascader-picker { + width: 300px; + } + ` + ] +}) +export class NzDemoCascaderCustomFieldNamesComponent { + /** init data */ + nzOptions = options; + + /** ngModel value */ + public values: any[] = null; + + public onChanges(values: any): void { + console.log(values, this.values); + } + + public validate(option: any, index: number): boolean { + const value = option.value; + return ['hangzhou', 'xihu', 'nanjing', 'zhonghuamen'].indexOf(value) >= 0; + } +} diff --git a/components/cascader/doc/index.en-US.md b/components/cascader/doc/index.en-US.md index 9e365980d80..c84bfacc406 100755 --- a/components/cascader/doc/index.en-US.md +++ b/components/cascader/doc/index.en-US.md @@ -32,6 +32,7 @@ Cascade selection box. | `[nzExpandTrigger]` | expand current item when click or hover, one of 'click' 'hover' | string | 'click' | | `[nzMenuClassName]` | additional className of popup overlay | string | - | | `[nzMenuStyle]` | additional css style of popup overlay | object | - | +| `[nzNotFoundContent]` | Specify content to show when no result matches. | string | - | | `[nzLabelProperty]` | the label property name of options | string | 'label' | | `[nzLabelRender]` | render template of displaying selected options | TemplateRef<any> | - | | `[nzLoadData]` | To load option lazily. If setting `ngModel` with an array value and `nzOptions` is not setting, lazy load will be call immediately | (option: any, index?: index) => PromiseLike<any> | - | diff --git a/components/cascader/doc/index.zh-CN.md b/components/cascader/doc/index.zh-CN.md index 1fb2a4699b6..e51aea67459 100755 --- a/components/cascader/doc/index.zh-CN.md +++ b/components/cascader/doc/index.zh-CN.md @@ -33,6 +33,7 @@ subtitle: 级联选择 | `[nzExpandTrigger]` | 次级菜单的展开方式,可选 'click' 和 'hover' | string | 'click' | | `[nzMenuClassName]` | 自定义浮层类名 | string | - | | `[nzMenuStyle]` | 自定义浮层样式 | object | - | +| `[nzNotFoundContent]` | 当下拉列表为空时显示的内容 | string | - | | `[nzLabelProperty]` | 选项的显示值的属性名 | string | 'label' | | `[nzLabelRender]` | 选择后展示的渲染模板 | TemplateRef<any> | - | | `[nzLoadData]` | 用于动态加载选项。如果提供了`ngModel`初始值,且未提供`nzOptions`值,则会立即触发动态加载。 | (option: any, index?: index) => PromiseLike<any> | - | diff --git a/components/cascader/nz-cascader-li.component.html b/components/cascader/nz-cascader-li.component.html new file mode 100644 index 00000000000..0b7e00d8288 --- /dev/null +++ b/components/cascader/nz-cascader-li.component.html @@ -0,0 +1,5 @@ + +{{ getOptionLabel() }} + + + \ No newline at end of file diff --git a/components/cascader/nz-cascader-li.component.ts b/components/cascader/nz-cascader-li.component.ts new file mode 100644 index 00000000000..e2d9be477a9 --- /dev/null +++ b/components/cascader/nz-cascader-li.component.ts @@ -0,0 +1,37 @@ +import { ChangeDetectionStrategy, Component, Input, SecurityContext, ViewEncapsulation } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { CascaderOption } from './types'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation : ViewEncapsulation.None, + selector : '[nz-cascader-option]', + templateUrl : './nz-cascader-li.component.html', + host : { + '[attr.title]' : 'option.title || getOptionLabel()', + '[class.ant-cascader-menu-item]' : 'true', + '[class.ant-cascader-menu-item-active]' : 'activated', + '[class.ant-cascader-menu-item-expand]' : '!option.isLeaf', + '[class.ant-cascader-menu-item-disabled]': 'option.disabled' + } +}) +export class NzCascaderOptionComponent { + @Input() option: CascaderOption; + @Input() activated = false; + @Input() highlightText: string; + @Input() nzLabelProperty = 'label'; + + constructor(private sanitizer: DomSanitizer) {} + + getOptionLabel(): string { + return this.option ? this.option[ this.nzLabelProperty ] : ''; + } + + renderHighlightString(str: string): string { + const safeHtml = this.sanitizer.sanitize(SecurityContext.HTML, `${this.highlightText}`); + if (!safeHtml) { + throw new Error(`[NG-ZORRO] Input value "${this.highlightText}" is not considered security.`); + } + return str.replace(new RegExp(this.highlightText, 'g'), safeHtml); + } +} diff --git a/components/cascader/nz-cascader.component.html b/components/cascader/nz-cascader.component.html index 6b48651d3bc..0d6bb8ce9c3 100644 --- a/components/cascader/nz-cascader.component.html +++ b/components/cascader/nz-cascader.component.html @@ -3,35 +3,40 @@ #origin="cdkOverlayOrigin" #trigger>
- - + (change)="$event.stopPropagation()"> + - - + nz-icon + type="down" + class="ant-cascader-picker-arrow" + [class.ant-cascader-picker-arrow-expand]="menuVisible"> + + + {{ labelRenderText }} @@ -49,30 +54,29 @@ (detach)="closeMenu()" (positionChange)="onPositionChange($event)" [cdkConnectedOverlayOpen]="menuVisible"> -
-
    -
  • +
  • - - - - - {{ getOptionLabel(option) }} - - - -
  • -
  • - Not Found +
  • + {{ nzNotFoundContent || ('Select.notFoundContent' | nzI18n) }}
diff --git a/components/cascader/nz-cascader.component.ts b/components/cascader/nz-cascader.component.ts index d755dccab11..6bfc7211e07 100644 --- a/components/cascader/nz-cascader.component.ts +++ b/components/cascader/nz-cascader.component.ts @@ -1,6 +1,8 @@ -// tslint:disable:no-any +import { BACKSPACE, DOWN_ARROW, ENTER, ESCAPE, LEFT_ARROW, RIGHT_ARROW, UP_ARROW } from '@angular/cdk/keycodes'; +import { ConnectedOverlayPositionChange, ConnectionPositionPair } from '@angular/cdk/overlay'; import { forwardRef, + ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, @@ -8,85 +10,31 @@ import { HostListener, Input, OnDestroy, - OnInit, Output, TemplateRef, - ViewChild + ViewChild, + ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { BACKSPACE, DOWN_ARROW, ENTER, ESCAPE, LEFT_ARROW, RIGHT_ARROW, UP_ARROW } from '@angular/cdk/keycodes'; -import { ConnectedOverlayPositionChange, ConnectionPositionPair } from '@angular/cdk/overlay'; -import { DEFAULT_DROPDOWN_POSITIONS } from '../core/overlay/overlay-position-map'; - import { dropDownAnimation } from '../core/animation/dropdown-animations'; -import { NzUpdateHostClassService } from '../core/services/update-host-class.service'; -import { toBoolean } from '../core/util/convert'; - -function toArray(value: T | T[]): T[] { - let ret: T[]; - if (value == null) { - ret = []; - } else if (!Array.isArray(value)) { - ret = [ value ]; - } else { - ret = value; - } - return ret; -} +import { ClassMap } from '../core/interface/interface'; +import { EXPANDED_DROPDOWN_POSITIONS } from '../core/overlay/overlay-position-map'; +import { arrayEquals, toArray } from '../core/util/array'; +import { InputBoolean } from '../core/util/convert'; -function arrayEquals(array1: T[], array2: T[]): boolean { - if (!array1 || !array2 || array1.length !== array2.length) { - return false; - } - - const len = array1.length; - for (let i = 0; i < len; i++) { - if (array1[ i ] !== array2[ i ]) { - return false; - } - } - return true; -} +import { CascaderOption, CascaderSearchOption, NzCascaderExpandTrigger, NzCascaderSize, NzCascaderTriggerType, NzShowSearchOptions } from './types'; const defaultDisplayRender = label => label.join(' / '); -export type NzCascaderExpandTrigger = 'click' | 'hover'; -export type NzCascaderTriggerType = 'click' | 'hover'; -export type NzCascaderSize = 'small' | 'large' | 'default' ; - -export interface CascaderOption { - value?: any; - label?: string; - title?: string; - disabled?: boolean; - loading?: boolean; - isLeaf?: boolean; - parent?: CascaderOption; - children?: CascaderOption[]; - - [ key: string ]: any; -} - -export interface CascaderSearchOption extends CascaderOption { - path: CascaderOption[]; -} - -export interface NzShowSearchOptions { - filter?(inputValue: string, path: CascaderOption[]): boolean; - - sorter?(a: CascaderOption[], b: CascaderOption[], inputValue: string): number; -} - @Component({ + changeDetection : ChangeDetectionStrategy.OnPush, + encapsulation : ViewEncapsulation.None, selector : 'nz-cascader,[nz-cascader]', preserveWhitespaces: false, - animations : [ - dropDownAnimation - ], templateUrl : './nz-cascader.component.html', + animations : [ dropDownAnimation ], providers : [ - NzUpdateHostClassService, { provide : NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NzCascaderComponent), @@ -94,910 +42,504 @@ export interface NzShowSearchOptions { } ], host : { - '[attr.tabIndex]': '"0"' + '[attr.tabIndex]' : '"0"', + '[class.ant-cascader]' : 'true', + '[class.ant-cascader-picker]' : 'true', + '[class.ant-cascader-lg]' : 'nzSize === "large"', + '[class.ant-cascader-sm]' : 'nzSize === "small"', + '[class.ant-cascader-picker-disabled]' : 'nzDisabled', + '[class.ant-cascader-picker-open]' : 'menuVisible', + '[class.ant-cascader-picker-with-value]': '!!inputValue', + '[class.ant-cascader-focused]' : 'isFocused' }, - styles : [ - `.ant-cascader-menus { + styles : [ ` + .ant-cascader-menus { margin-top: 4px; margin-bottom: 4px; top: 100%; left: 0; position: relative; width: 100%; - }` - ] + } + ` ] }) -export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAccessor { - private allowClear = true; - private autoFocus = false; - private disabled = false; - private enableCache = true; - private showArrow = true; - private showInput = true; - private size: NzCascaderSize = 'default'; - private prefixCls = 'ant-cascader'; - private inputPrefixCls = 'ant-input'; - private menuClassName; - private columnClassName; - private changeOnSelect = false; - private showSearch: boolean | NzShowSearchOptions; - private defaultValue: any[]; - - public dropDownPosition = 'bottom'; - public menuVisible = false; - public isLoading = false; - private isOpening = false; +export class NzCascaderComponent implements OnDestroy, ControlValueAccessor { + @ViewChild('input') input: ElementRef; + @ViewChild('menu') menu: ElementRef; - // 内部样式 - private _arrowCls: { [ name: string ]: any }; - private _clearCls: { [ name: string ]: any }; - private _inputCls: { [ name: string ]: any }; - private _labelCls: { [ name: string ]: any }; - private _loadingCls: { [ name: string ]: any }; - private _menuCls: { [ name: string ]: any }; - private _menuColumnCls: { [ name: string ]: any }; + @Input() @InputBoolean() nzShowInput = true; + @Input() @InputBoolean() nzShowArrow = true; + @Input() @InputBoolean() nzAllowClear = true; + @Input() @InputBoolean() nzAutoFocus = false; + @Input() @InputBoolean() nzChangeOnSelect = false; + @Input() @InputBoolean() nzDisabled = false; + @Input() nzColumnClassName: string; + @Input() nzExpandTrigger: NzCascaderExpandTrigger = 'click'; + @Input() nzValueProperty = 'value'; + @Input() nzLabelRender: TemplateRef; + @Input() nzLabelProperty = 'label'; + @Input() nzNotFoundContent: string; + @Input() nzSize: NzCascaderSize = 'default'; + @Input() nzShowSearch: boolean | NzShowSearchOptions; + @Input() nzPlaceHolder = 'Please select'; + @Input() nzMenuClassName: string; + @Input() nzMenuStyle: { [ key: string ]: string; }; + @Input() nzMouseEnterDelay: number = 150; // ms + @Input() nzMouseLeaveDelay: number = 150; // ms + @Input() nzTriggerAction: NzCascaderTriggerType | NzCascaderTriggerType[] = [ 'click' ] as NzCascaderTriggerType[]; + @Input() nzChangeOn: (option: CascaderOption, level: number) => boolean; - public el: HTMLElement = this.elementRef.nativeElement; + // tslint:disable-next-line:no-any + @Input() nzLoadData: (node: CascaderOption, index?: number) => PromiseLike; - private isFocused = false; + @Input() + get nzOptions(): CascaderOption[] { return this.columns[ 0 ]; } + set nzOptions(options: CascaderOption[] | null) { + this.columnsSnapshot = this.columns = options && options.length ? [ options ] : []; + if (!this.isSearching) { + if (this.defaultValue && this.columns.length) { + this.initOptions(0); + } + } else { + this.prepareSearchValue(); + } + } - /** 选择选项后,渲染显示文本 */ - private labelRenderTpl: TemplateRef; - public isLabelRenderTemplate = false; - public labelRenderText: string; - public labelRenderContext: any = {}; + @Output() readonly nzSelectionChange = new EventEmitter(); + @Output() readonly nzSelect = new EventEmitter<{ option: CascaderOption, index: number }>(); + @Output() readonly nzClear = new EventEmitter(); + @Output() readonly nzVisibleChange = new EventEmitter(); // Not exposed, only for test + @Output() readonly nzChange = new EventEmitter(); // Not exposed, only for test + + el: HTMLElement = this.elementRef.nativeElement; + dropDownPosition = 'bottom'; + menuVisible = false; + isLoading = false; + labelRenderText: string; + labelRenderContext = {}; + columns: CascaderOption[][] = []; + onChange = Function.prototype; + onTouched = Function.prototype; + positions: ConnectionPositionPair[] = [ ...EXPANDED_DROPDOWN_POSITIONS ]; + dropdownWidthStyle: string; + isSearching = false; + isFocused = false; - // 当前值 - private value: any[]; - // 已选择的选项表示当前已确认的选项:selection will trigger value change + private isOpening = false; + private defaultValue; // Default value written by `[ngModel]` + private value; private selectedOptions: CascaderOption[] = []; - // 已激活的选项表示通过键盘方向键选择的选项,并未最终确认(除非按ENTER键):activaction will not trigger value change private activatedOptions: CascaderOption[] = []; - // 表示当前菜单的数据列:all data columns - public nzColumns: CascaderOption[][] = []; - - // 显示或隐藏菜单计时器 - private delayTimer: any; - private delaySelectTimer: any; - - /** 搜索相关的输入值 */ - private _inputValue = ''; - get inputValue(): string { - return this._inputValue; - } + private columnsSnapshot: CascaderOption[][]; + private activatedOptionsSnapshot: CascaderOption[]; + private delayMenuTimer; + private delaySelectTimer; set inputValue(inputValue: string) { this._inputValue = inputValue; - const willBeInSearch = !!inputValue; - - // 搜索状态变动之前,如要进入则要保留之前激活选项的快照,退出搜索状态要还原该快照 - if (!this.inSearch && willBeInSearch) { - this.oldActivatedOptions = this.activatedOptions; - this.activatedOptions = []; - } else if (this.inSearch && !willBeInSearch) { - this.activatedOptions = this.oldActivatedOptions; - } - - // 搜索状态变更之后 - this.inSearch = !!willBeInSearch; - if (this.inSearch) { - this.labelRenderText = ''; - this.prepareSearchValue(); - } else { - if (this.showSearch) { - this.nzColumns = this.oldColumnsHolder; - } - this.buildDisplayLabel(); - this.searchWidthStyle = ''; - } - this.setClassMap(); - } - - // ngModel Access - onChange: any = Function.prototype; - onTouched: any = Function.prototype; - positions: ConnectionPositionPair[] = [ ...DEFAULT_DROPDOWN_POSITIONS ]; - - /** Display Render ngTemplate */ - @Input() - set nzLabelRender(value: TemplateRef) { - this.labelRenderTpl = value; - this.isLabelRenderTemplate = (value instanceof TemplateRef); - } - - get nzLabelRender(): TemplateRef { - return this.labelRenderTpl; - } - - /** prefixCls */ - @Input() - set nzPrefixCls(prefixCls: string) { - this.prefixCls = prefixCls; - this.setClassMap(); - this.setLabelClass(); - this.setArrowClass(); - this.setLoadingClass(); - this.setClearClass(); - this.setInputClass(); - this.setMenuClass(); - this.setMenuColumnClass(); - } - - get nzPrefixCls(): string { - return this.prefixCls; - } - - /** Whether is disabled */ - @Input() - set nzDisabled(value: boolean) { - this.disabled = toBoolean(value); - this.setClassMap(); - this.setInputClass(); - } - - get nzDisabled(): boolean { - return this.disabled; - } - - /** Input size, one of `large` `default` `small` */ - @Input() - set nzSize(value: NzCascaderSize) { - this.size = value; - this.setClassMap(); - this.setInputClass(); - } - - get nzSize(): NzCascaderSize { - return this.size; - } - - /** Whether show input box. Defaults to `true`. */ - @Input() - set nzShowInput(value: boolean) { - this.showInput = toBoolean(value); - } - - get nzShowInput(): boolean { - return this.showInput; - } - - /** Whether can search. Defaults to `false`. */ - @Input() - set nzShowSearch(value: boolean | NzShowSearchOptions) { - this.showSearch = value; - } - - get nzShowSearch(): boolean | NzShowSearchOptions { - return this.showSearch; - } - - public searchWidthStyle: string; - private oldColumnsHolder; - private oldActivatedOptions; - - /** If cascader is in search mode. */ - public inSearch = false; - - /** Whether allow clear. Defaults to `true`. */ - @Input() - set nzAllowClear(value: boolean) { - this.allowClear = toBoolean(value); - } - - get nzAllowClear(): boolean { - return this.allowClear; - } - - /** Whether auto focus. */ - @Input() - set nzAutoFocus(value: boolean) { - this.autoFocus = toBoolean(value); - } - - get nzAutoFocus(): boolean { - return this.autoFocus; - } - - /** Whether to show arrow */ - @Input() - set nzShowArrow(value: boolean) { - this.showArrow = toBoolean(value); - } - - get nzShowArrow(): boolean { - return this.showArrow; + this.toggleSearchMode(); } + get inputValue(): string { return this._inputValue; } + private _inputValue = ''; - /** Additional className of popup overlay */ - @Input() - set nzMenuClassName(value: string) { - this.menuClassName = value; - this.setMenuClass(); + get menuCls(): ClassMap { + return { + [ `${this.nzMenuClassName}` ]: !!this.nzMenuClassName + }; } - get nzMenuClassName(): string { - return this.menuClassName; + get menuColumnCls(): ClassMap { + return { + [ `${this.nzColumnClassName}` ]: !!this.nzColumnClassName + }; } - /** Additional className of popup overlay column */ - @Input() - set nzColumnClassName(value: string) { - this.columnClassName = value; - this.setMenuColumnClass(); - } + //#region Menu - get nzColumnClassName(): string { - return this.columnClassName; - } - - /** Options for first column, sub column will be load async */ - @Input() set nzOptions(options: CascaderOption[] | null) { - this.oldColumnsHolder = this.nzColumns = options && options.length ? [ options ] : []; - if (!this.inSearch) { - if (this.defaultValue && this.nzColumns.length) { - this.initOptions(0); + delaySetMenuVisible(visible: boolean, delay: number, setOpening: boolean = false): void { + this.clearDelayMenuTimer(); + if (delay) { + if (visible && setOpening) { + this.isOpening = true; } + this.delayMenuTimer = setTimeout(() => { + this.setMenuVisible(visible); + this.cdr.detectChanges(); + this.clearDelayMenuTimer(); + if (visible) { + setTimeout(() => { + this.isOpening = false; + }, 100); + } + }, delay); } else { - this.prepareSearchValue(); + this.setMenuVisible(visible); } } - get nzOptions(): CascaderOption[] { - return this.nzColumns[ 0 ]; - } - - /** Change value on each selection if set to true */ - @Input() - set nzChangeOnSelect(value: boolean) { - this.changeOnSelect = toBoolean(value); - } - - get nzChangeOnSelect(): boolean { - return this.changeOnSelect; - } - - /** Hover text for the clear icon */ - @Input() nzClearText = 'Clear'; - - /** Expand column item when click or hover, one of 'click' 'hover' */ - @Input() nzExpandTrigger: NzCascaderExpandTrigger = 'click'; - - /** Specify content to show when no result matches. */ - @Input() nzNotFoundContent = 'Not Found'; - - /** Input placeholder */ - @Input() nzPlaceHolder = 'Please select'; - - /** Additional style of popup overlay */ - @Input() nzMenuStyle: { [ key: string ]: string; }; - - /** Change value on selection only if this function returns `true` */ - @Input() nzChangeOn: (option: CascaderOption, level: number) => boolean; - - /** Delay time to show when mouse enter, when `nzExpandTrigger` is `hover`. */ - @Input() nzMouseEnterDelay = 150; // ms - - /** Delay time to hide when mouse enter, when `nzExpandTrigger` is `hover`. */ - @Input() nzMouseLeaveDelay = 150; // ms - - /** Triggering mode: can be Array<'click'|'hover'> */ - @Input() nzTriggerAction: NzCascaderTriggerType | NzCascaderTriggerType[] = [ 'click' ]; - - /** Property name for getting `value` in the option */ - @Input() nzValueProperty = 'value'; - - /** Property name for getting `label` in the option */ - @Input() nzLabelProperty = 'label'; - - /** 异步加载数据 */ - @Input() nzLoadData: (node: CascaderOption, index?: number) => PromiseLike; - - /** Event: emit on popup show or hide */ - @Output() readonly nzVisibleChange = new EventEmitter(); - - /** Event: emit on values changed */ - @Output() readonly nzChange = new EventEmitter(); - - /** Event: emit on values and selection changed */ - @Output() readonly nzSelectionChange = new EventEmitter(); - - /** - * Event: emit on option selected, event data:{option: any, index: number} - */ - @Output() readonly nzSelect = new EventEmitter<{ - option: CascaderOption, - index: number - }>(); - - /** Event: emit on the clear button clicked */ - @Output() readonly nzClear = new EventEmitter(); - - @ViewChild('input') input: ElementRef; - /** 浮层菜单 */ - @ViewChild('menu') menu: ElementRef; + setMenuVisible(visible: boolean): void { + if (this.nzDisabled) { + return; + } - public onPositionChange(position: ConnectedOverlayPositionChange): void { - const newValue = position.connectionPair.originY === 'bottom' ? 'bottom' : 'top'; - if (this.dropDownPosition !== newValue) { - this.dropDownPosition = newValue; + if (this.menuVisible !== visible) { + this.menuVisible = visible; this.cdr.detectChanges(); + if (visible) { + this.loadRootOptions(); + } + this.nzVisibleChange.emit(visible); } } - public focus(): void { - if (!this.isFocused) { - const input = this.el.querySelector(`.${this.prefixCls}-input`) as HTMLElement; - if (input && input.focus) { - input.focus(); - } else { - this.el.focus(); - } - this.isFocused = true; - this.setClassMap(); + private clearDelayMenuTimer(): void { + if (this.delayMenuTimer) { + clearTimeout(this.delayMenuTimer); + this.delayMenuTimer = null; } } - public blur(): void { - if (this.isFocused) { - const input = this.el.querySelector(`.${this.prefixCls}-input`) as HTMLElement; - if (input && input.blur) { - input.blur(); - } else { - this.el.blur(); - } - this.isFocused = false; - this.setClassMap(); - this.setLabelClass(); + private loadRootOptions(): void { + if (!this.columns.length) { + const root = {}; + this.loadChildrenAsync(root, -1); } } - private setClassMap(): void { - const classMap = { - [ `${this.prefixCls}` ] : 1, - [ `${this.prefixCls}-picker` ] : 1, - [ `${this.prefixCls}-lg` ] : this.nzSize === 'large', - [ `${this.prefixCls}-sm` ] : this.nzSize === 'small', - [ `${this.prefixCls}-picker-disabled` ] : this.disabled, - [ `${this.prefixCls}-focused` ] : this.isFocused, - [ `${this.prefixCls}-picker-open` ] : this.menuVisible, - [ `${this.prefixCls}-picker-with-value` ]: this.inputValue && this.inputValue.length - }; - this.nzUpdateHostClassService.updateHostClass(this.el, classMap); - } + //#endregion - /** 标签 样式 */ - public get labelCls(): any { - return this._labelCls; - } + //#region Init - private setLabelClass(): void { - this._labelCls = { - [ `${this.prefixCls}-picker-label` ]: true, - [ `${this.prefixCls}-show-search` ] : !!this.nzShowSearch, - [ `${this.prefixCls}-focused` ] : !!this.nzShowSearch && this.isFocused && !this._inputValue - }; + private isLoaded(index: number): boolean { + return this.columns[ index ] && this.columns[ index ].length > 0; } - /** 箭头 样式 */ - public get arrowCls(): any { - return this._arrowCls; + private findOption(option: CascaderOption, index: number): CascaderOption { + const options: CascaderOption[] = this.columns[ index ]; + if (options) { + const value = typeof option === 'object' ? this.getOptionValue(option) : option; + return options.find(o => value === this.getOptionValue(o)); + } + return null; } - private setArrowClass(): void { - this._arrowCls = { - [ `${this.prefixCls}-picker-arrow` ] : true, - [ `${this.prefixCls}-picker-arrow-expand` ]: this.menuVisible - }; + // tslint:disable-next-line:no-any + private activateOnInit(index: number, value: any): void { + let option = this.findOption(value, index); + if (!option) { + option = typeof value === 'object' ? value : { + [ `${this.nzValueProperty}` ]: value, + [ `${this.nzLabelProperty}` ]: value + }; + } + this.setOptionActivated(option, index, false, false); } - /** 加载中图标 样式 */ - public get loadingCls(): any { - return this._loadingCls; - } + private initOptions(index: number): void { + const vs = this.defaultValue; + const lastIndex = vs.length - 1; - private setLoadingClass(): void { - this._loadingCls = { - [ `${this.prefixCls}-picker-arrow` ]: true + const load = () => { + this.activateOnInit(index, vs[ index ]); + if (index < lastIndex) { + this.initOptions(index + 1); + } + if (index === lastIndex) { + this.afterWriteValue(); + } }; - } - /** 清除图标 样式 */ - public get clearCls(): any { - return this._clearCls; + if (this.isLoaded(index) || !this.nzLoadData) { + load(); + } else { + const node = this.activatedOptions[ index - 1 ] || {}; + this.loadChildrenAsync(node, index - 1, load, this.afterWriteValue); + } } - private setClearClass(): void { - this._clearCls = { - [ `${this.prefixCls}-picker-clear` ]: true - }; - } + //#endregion - /** 输入框 样式 */ - public get inputCls(): any { - return this._inputCls; - } + //#region Mutating data - private setInputClass(): void { - this._inputCls = { - [ `${this.prefixCls}-input` ] : 1, - [ `${this.inputPrefixCls}-disabled` ]: this.nzDisabled, - [ `${this.inputPrefixCls}-lg` ] : this.nzSize === 'large', - [ `${this.inputPrefixCls}-sm` ] : this.nzSize === 'small' - }; - } + private setOptionActivated(option: CascaderOption, columnIndex: number, select: boolean = false, loadChildren: boolean = true): void { + if (!option || option.disabled) { + return; + } - /** 浮层 样式 */ - public get menuCls(): any { - return this._menuCls; - } + this.activatedOptions[ columnIndex ] = option; - private setMenuClass(): void { - this._menuCls = { - [ `${this.prefixCls}-menus` ] : true, - [ `${this.prefixCls}-menus-hidden` ]: !this.menuVisible, - [ `${this.nzMenuClassName}` ] : this.nzMenuClassName - }; - } + // Set parent option and all ancestor options as active. + for (let i = columnIndex - 1; i >= 0; i--) { + if (!this.activatedOptions[ i ]) { + this.activatedOptions[ i ] = this.activatedOptions[ i + 1 ].parent; + } + } - /** 浮层列 样式 */ - public get menuColumnCls(): any { - return this._menuColumnCls; - } + // Set child options and all success options as inactive. + if (columnIndex < this.activatedOptions.length - 1) { + this.activatedOptions = this.activatedOptions.slice(0, columnIndex + 1); + } - private setMenuColumnClass(): void { - this._menuColumnCls = { - [ `${this.prefixCls}-menu` ] : true, - [ `${this.nzColumnClassName}` ]: this.nzColumnClassName - }; - } + // Load child options. + if (option.children && option.children.length && !option.isLeaf) { + option.children.forEach(child => child.parent = option); + this.setColumnData(option.children, columnIndex + 1); + } else if (!option.isLeaf && loadChildren) { + this.loadChildrenAsync(option, columnIndex); + } - /** 获取列中Option的样式 */ - public getOptionCls(option: CascaderOption, index: number): any { - return { - [ `${this.prefixCls}-menu-item` ] : true, - [ `${this.prefixCls}-menu-item-expand` ] : !option.isLeaf, - [ `${this.prefixCls}-menu-item-active` ] : this.isActivedOption(option, index), - [ `${this.prefixCls}-menu-item-disabled` ]: option.disabled - }; - } + if (select) { + this.setOptionSelected(option, columnIndex); + } - /** prevent input change event */ - public handlerInputChange(event: Event): void { - event.stopPropagation(); + this.cdr.detectChanges(); } - /** input element blur */ - public handleInputBlur(event: Event): void { - /* - if (!this.nzShowSearch) { - return; - } - */ - if (this.menuVisible) { - this.focus(); // keep input has focus when menu opened - } else { - this.blur(); + private loadChildrenAsync(option: CascaderOption, columnIndex: number, success?: () => void, failure?: () => void): void { + if (this.nzLoadData) { + this.isLoading = columnIndex < 0; + option.loading = true; + this.nzLoadData(option, columnIndex).then(() => { + option.loading = this.isLoading = false; + if (option.children) { + option.children.forEach(child => child.parent = columnIndex < 0 ? undefined : option); + this.setColumnData(option.children, columnIndex + 1); + this.cdr.detectChanges(); + } + if (success) { + success(); + } + }, () => { + option.loading = this.isLoading = false; + option.isLeaf = true; + this.cdr.detectChanges(); + if (failure) { + failure(); + } + }); } } - /** input element focus */ - public handleInputFocus(event: Event): void { - /* - if (!this.nzShowSearch) { - return; - } - */ - this.focus(); - this.setLabelClass(); - } + private setOptionSelected(option: CascaderOption, columnIndex: number): void { + const shouldPerformSelection = (o: CascaderOption, i: number): boolean => { + return typeof this.nzChangeOn === 'function' ? this.nzChangeOn(o, i) === true : false; + }; - private hasInput(): boolean { - return this.inputValue.length > 0; - } + this.nzSelect.emit({ option, index: columnIndex }); - private hasValue(): boolean { - return this.value && this.value.length > 0; - } + if (option.isLeaf || this.nzChangeOnSelect || shouldPerformSelection(option, columnIndex)) { + this.selectedOptions = this.activatedOptions; + this.buildDisplayLabel(); + this.onValueChange(); + } - /** Whether to show input element placeholder */ - public get showPlaceholder(): boolean { - return !(this.hasInput() || this.hasValue()); + if (option.isLeaf) { + this.delaySetMenuVisible(false, this.nzMouseLeaveDelay); + } } - /** Whether the clear button is visible */ - public get showClearIcon(): boolean { - const isHasValue = this.hasValue(); - const isHasInput = this.hasInput(); - return this.nzAllowClear && !this.nzDisabled && (isHasValue || isHasInput); + private setColumnData(options: CascaderOption[], columnIndex: number): void { + if (!arrayEquals(this.columns[ columnIndex ], options)) { + this.columns[ columnIndex ] = options; + if (columnIndex < this.columns.length - 1) { + this.columns = this.columns.slice(0, columnIndex + 1); + } + } } - /** clear the input box and selected options */ - public clearSelection(event?: Event): void { + clearSelection(event?: Event): void { if (event) { event.preventDefault(); event.stopPropagation(); } this.labelRenderText = ''; - // this.isLabelRenderTemplate = false; - // clear custom context this.labelRenderContext = {}; this.selectedOptions = []; this.activatedOptions = []; this.inputValue = ''; this.setMenuVisible(false); - // trigger change event this.onValueChange(); } - private buildDisplayLabel(): void { - const selectedOptions = this.selectedOptions; - const labels: string[] = selectedOptions.map(o => this.getOptionLabel(o)); - // 设置当前控件的显示值 - if (this.isLabelRenderTemplate) { - this.labelRenderContext = { labels, selectedOptions }; - } else { - this.labelRenderText = defaultDisplayRender.call(this, labels, selectedOptions); - } - } - - @HostListener('keydown', [ '$event' ]) - public onKeyDown(event: KeyboardEvent): void { - const keyCode = event.keyCode; - if (keyCode !== DOWN_ARROW && - keyCode !== UP_ARROW && - keyCode !== LEFT_ARROW && - keyCode !== RIGHT_ARROW && - keyCode !== ENTER && - keyCode !== BACKSPACE && - keyCode !== ESCAPE) { - return; - } - - if (this.inSearch && ( - keyCode === BACKSPACE || - keyCode === LEFT_ARROW || - keyCode === RIGHT_ARROW - )) { - return; - } - - // Press any keys above to reopen menu - if (!this.isMenuVisible() && - keyCode !== BACKSPACE && - keyCode !== ESCAPE) { - this.setMenuVisible(true); - return; - } - // Press ESC to close menu - if (keyCode === ESCAPE) { - // this.setMenuVisible(false); // already call by cdk-overlay detach - return; - } - - if (this.isMenuVisible()) { - event.preventDefault(); - if (keyCode === DOWN_ARROW) { - this.moveDown(); - } else if (keyCode === UP_ARROW) { - this.moveUp(); - } else if (keyCode === LEFT_ARROW) { - this.moveLeft(); - } else if (keyCode === RIGHT_ARROW) { - this.moveRight(); - } else if (keyCode === ENTER) { - this.onEnter(); - } - } - } - - @HostListener('click', [ '$event' ]) - public onTriggerClick(event: MouseEvent): void { - if (this.nzDisabled) { - return; - } - this.onTouched(); // set your control to 'touched' - if (this.nzShowSearch) { - this.focus(); - } - - if (this.isClickTiggerAction()) { - this.delaySetMenuVisible(!this.menuVisible, 100); - } - } - - @HostListener('mouseenter', [ '$event' ]) - public onTriggerMouseEnter(event: MouseEvent): void { - if (this.nzDisabled) { - return; - } - if (this.isPointerTiggerAction()) { - this.delaySetMenuVisible(true, this.nzMouseEnterDelay, true); - } - } - - @HostListener('mouseleave', [ '$event' ]) - public onTriggerMouseLeave(event: MouseEvent): void { - if (this.nzDisabled) { - return; - } - if (!this.isMenuVisible() || this.isOpening) { - event.preventDefault(); - return; - } - if (this.isPointerTiggerAction()) { - const mouseTarget = event.relatedTarget as HTMLElement; - const hostEl = this.el; - const menuEl = this.menu && this.menu.nativeElement as HTMLElement; - if (hostEl.contains(mouseTarget) || (menuEl && menuEl.contains(mouseTarget)) - /*|| mouseTarget.parentElement.contains(menuEl)*/) { - // 因为浮层的backdrop出现,暂时没有办法自动消失 - return; - } - this.delaySetMenuVisible(false, this.nzMouseLeaveDelay); - } - } - - private isClickTiggerAction(): boolean { - if (typeof this.nzTriggerAction === 'string') { - return this.nzTriggerAction === 'click'; - } - return this.nzTriggerAction.indexOf('click') !== -1; - } - - private isPointerTiggerAction(): boolean { - if (typeof this.nzTriggerAction === 'string') { - return this.nzTriggerAction === 'hover'; - } - return this.nzTriggerAction.indexOf('hover') !== -1; - } - - public closeMenu(): void { - this.blur(); - this.clearDelayTimer(); - this.setMenuVisible(false); - } - - private clearDelayTimer(): void { - if (this.delayTimer) { - clearTimeout(this.delayTimer); - this.delayTimer = null; - } + // tslint:disable-next-line:no-any + getSubmitValue(): any[] { + const values = []; + this.selectedOptions.forEach(option => { + values.push(this.getOptionValue(option)); + }); + return values; } - /** - * 显示或者隐藏菜单 - * - * @param visible true-显示,false-隐藏 - * @param delay 延迟时间 - */ - public delaySetMenuVisible(visible: boolean, delay: number, setOpening: boolean = false): void { - this.clearDelayTimer(); - if (delay) { - if (visible && setOpening) { - this.isOpening = true; + private onValueChange(): void { + const value = this.getSubmitValue(); + if (!arrayEquals(this.value, value)) { + this.defaultValue = null; + this.value = value; + this.onChange(value); + if (value.length === 0) { + this.nzClear.emit(); } - this.delayTimer = setTimeout(() => { - this.setMenuVisible(visible); - this.clearDelayTimer(); - if (visible) { - setTimeout(() => { - this.isOpening = false; - }, 100); - } - }, delay); - } else { - this.setMenuVisible(visible); + this.nzSelectionChange.emit(this.selectedOptions); + this.nzChange.emit(value); } } - public isMenuVisible(): boolean { - return this.menuVisible; + afterWriteValue(): void { + this.selectedOptions = this.activatedOptions; + this.value = this.getSubmitValue(); + this.buildDisplayLabel(); } - public setMenuVisible(menuVisible: boolean): void { - if (this.nzDisabled) { - return; - } - - if (this.menuVisible !== menuVisible) { - this.menuVisible = menuVisible; + //#endregion - // update class - this.setClassMap(); - this.setArrowClass(); - this.setMenuClass(); + //#region Mouse and keyboard event handlers, view children - if (menuVisible) { - this.beforeVisible(); - } - this.nzVisibleChange.emit(menuVisible); + focus(): void { + if (!this.isFocused) { + (this.input ? this.input.nativeElement : this.el).focus(); + this.isFocused = true; } } - /** load init data if necessary */ - private beforeVisible(): void { - this.loadRootOptions(); - } - - private loadRootOptions(): void { - if (!this.nzColumns.length) { - const root: any = {}; - this.loadChildren(root, -1); + blur(): void { + if (this.isFocused) { + (this.input ? this.input.nativeElement : this.el).blur(); + this.isFocused = false; } } - /** 获取Option的值,例如,可以指定labelProperty="name"来取Name */ - public getOptionLabel(option: CascaderOption): any { - return option[ this.nzLabelProperty || 'label' ]; + handleInputBlur(event: Event): void { + this.menuVisible ? this.focus() : this.blur(); } - /** 获取Option的值,例如,可以指定valueProperty="id"来取ID */ - public getOptionValue(option: CascaderOption): any { - return option[ this.nzValueProperty || 'value' ]; + handleInputFocus(event: Event): void { + this.focus(); } - private isActivedOption(option: CascaderOption, index: number): boolean { - const activeOpt = this.activatedOptions[ index ]; - return activeOpt === option; - } + @HostListener('keydown', [ '$event' ]) + onKeyDown(event: KeyboardEvent): void { + const keyCode = event.keyCode; - /** - * 设置某列的激活的菜单选项 - * - * @param option 菜单选项 - * @param index 选项所在的列组的索引 - * @param select 是否触发选择结点 - */ - private setActiveOption(option: CascaderOption, index: number, select: boolean = false, loadChildren: boolean = true): void { - if (!option || option.disabled) { + if (keyCode !== DOWN_ARROW && + keyCode !== UP_ARROW && + keyCode !== LEFT_ARROW && + keyCode !== RIGHT_ARROW && + keyCode !== ENTER && + keyCode !== BACKSPACE && + keyCode !== ESCAPE + ) { return; } - this.activatedOptions[ index ] = option; - - // 当直接选择最后一级时,前面的选项要补全。例如,选择“城市”,则自动补全“国家”、“省份” - for (let i = index - 1; i >= 0; i--) { - if (!this.activatedOptions[ i ]) { - this.activatedOptions[ i ] = this.activatedOptions[ i + 1 ].parent; - } - } - // 截断多余的选项,如选择“省份”,则只会有“国家”、“省份”,去掉“城市”、“区县” - if (index < this.activatedOptions.length - 1) { - this.activatedOptions = this.activatedOptions.slice(0, index + 1); + // Press any keys above to reopen menu. + if (!this.menuVisible && keyCode !== BACKSPACE && keyCode !== ESCAPE) { + return this.setMenuVisible(true); } - // load children - if (option.children && option.children.length) { - option.isLeaf = false; - option.children.forEach(child => child.parent = option); - this.setColumnData(option.children, index + 1); - } else if (!option.isLeaf && loadChildren) { - this.loadChildren(option, index); - } else { - // clicking leaf node will remove any children columns - if (index < this.nzColumns.length - 1) { - this.nzColumns = this.nzColumns.slice(0, index + 1); - } + // Make these keys work as default in searching mode. + if (this.isSearching && (keyCode === BACKSPACE || keyCode === LEFT_ARROW || keyCode === RIGHT_ARROW)) { + return; } - // trigger select event, and display label - if (select) { - this.onSelectOption(option, index); + // Interact with the component. + if (this.menuVisible) { + event.preventDefault(); + if (keyCode === DOWN_ARROW) { + this.moveUpOrDown(false); + } else if (keyCode === UP_ARROW) { + this.moveUpOrDown(true); + } else if (keyCode === LEFT_ARROW) { + this.moveLeft(); + } else if (keyCode === RIGHT_ARROW) { + this.moveRight(); + } else if (keyCode === ENTER) { + this.onEnter(); + } } } - private loadChildren(option: CascaderOption, index: number, success?: () => void, failure?: () => void): void { - if (this.nzLoadData) { - this.isLoading = index < 0; - option.loading = true; - this.nzLoadData(option, index).then(() => { - option.loading = this.isLoading = false; - if (option.children) { - option.children.forEach(child => child.parent = index < 0 ? undefined : option); - this.setColumnData(option.children, index + 1); - } - if (success) { - success(); - } - }, () => { - option.loading = this.isLoading = false; - option.isLeaf = true; - if (failure) { - failure(); - } - }); + @HostListener('click', [ '$event' ]) + onTriggerClick(event: MouseEvent): void { + if (this.nzDisabled) { + return; } - } - - private onSelectOption(option: CascaderOption, index: number): void { - // trigger `nzSelect` event - this.nzSelect.emit({ option, index }); - - // 生成显示 - if (option.isLeaf || this.nzChangeOnSelect || this.isChangeOn(option, index)) { - this.selectedOptions = this.activatedOptions; - // 设置当前控件的显示值 - this.buildDisplayLabel(); - // 触发变更事件 - this.onValueChange(); + if (this.nzShowSearch) { + this.focus(); } - - // close menu if click on leaf - if (option.isLeaf) { - this.delaySetMenuVisible(false, this.nzMouseLeaveDelay); + if (this.isActionTrigger('click')) { + this.delaySetMenuVisible(!this.menuVisible, 100); } + this.onTouched(); } - /** 由用户来定义点击后是否变更 */ - private isChangeOn(option: CascaderOption, index: number): boolean { - if (typeof this.nzChangeOn === 'function') { - return this.nzChangeOn(option, index) === true; + @HostListener('mouseenter', [ '$event' ]) + onTriggerMouseEnter(event: MouseEvent): void { + if (this.nzDisabled) { + return; + } + if (this.isActionTrigger('hover')) { + this.delaySetMenuVisible(true, this.nzMouseEnterDelay, true); } - return false; } - private setColumnData(options: CascaderOption[], index: number): void { - if (!arrayEquals(this.nzColumns[ index ], options)) { - this.nzColumns[ index ] = options; - if (index < this.nzColumns.length - 1) { - this.nzColumns = this.nzColumns.slice(0, index + 1); + @HostListener('mouseleave', [ '$event' ]) + onTriggerMouseLeave(event: MouseEvent): void { + if (this.nzDisabled) { + return; + } + if (!this.menuVisible || this.isOpening) { + event.preventDefault(); + return; + } + if (this.isActionTrigger('hover')) { + const mouseTarget = event.relatedTarget as HTMLElement; + const hostEl = this.el; + const menuEl = this.menu && this.menu.nativeElement as HTMLElement; + if (hostEl.contains(mouseTarget) || (menuEl && menuEl.contains(mouseTarget))) { + return; } + this.delaySetMenuVisible(false, this.nzMouseLeaveDelay); } } - /** - * 鼠标点击选项 - * - * @param option 菜单选项 - * @param index 选项所在的列组的索引 - * @param event 鼠标事件 - */ - onOptionClick(option: CascaderOption, index: number, event: Event): void { + private isActionTrigger(action: 'click' | 'hover'): boolean { + return typeof this.nzTriggerAction === 'string' + ? this.nzTriggerAction === action + : this.nzTriggerAction.indexOf(action) !== -1; + } + + onOptionClick(option: CascaderOption, columnIndex: number, event: Event): void { if (event) { event.preventDefault(); } - - // Keep focused state for keyboard support - this.el.focus(); - if (option && option.disabled) { return; } - - if (this.inSearch) { - this.setSearchActiveOption(option as CascaderSearchOption, event); - } else { - this.setActiveOption(option, index, true); - } + this.el.focus(); + this.isSearching + ? this.setSearchOptionActivated(option as CascaderSearchOption, event) + : this.setOptionActivated(option, columnIndex, true); } - /** 按下回车键时选择 */ private onEnter(): void { const columnIndex = Math.max(this.activatedOptions.length - 1, 0); - const activeOption = this.activatedOptions[ columnIndex ]; - if (activeOption && !activeOption.disabled) { - if (this.inSearch) { - this.setSearchActiveOption(activeOption as CascaderSearchOption, null); - } else { - this.onSelectOption(activeOption, columnIndex); - } + const option = this.activatedOptions[ columnIndex ]; + if (option && !option.disabled) { + this.isSearching + ? this.setSearchOptionActivated(option as CascaderSearchOption, null) + : this.setOptionSelected(option, columnIndex); } } - /** - * press `up` or `down` arrow to activate the sibling option. - */ private moveUpOrDown(isUp: boolean): void { const columnIndex = Math.max(this.activatedOptions.length - 1, 0); - // 该组中已经被激活的选项 const activeOption = this.activatedOptions[ columnIndex ]; - // 该组所有的选项,用于遍历获取下一个被激活的选项 - const options = this.nzColumns[ columnIndex ] || []; + const options = this.columns[ columnIndex ] || []; const length = options.length; let nextIndex = -1; - if (!activeOption) { // 该列还没有选中的选项 + if (!activeOption) { // Not selected options in this column nextIndex = isUp ? length : -1; } else { nextIndex = options.indexOf(activeOption); @@ -1012,22 +554,11 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces if (!nextOption || nextOption.disabled) { continue; } - this.setActiveOption(nextOption, columnIndex); + this.setOptionActivated(nextOption, columnIndex); break; } } - private moveUp(): void { - this.moveUpOrDown(true); - } - - private moveDown(): void { - this.moveUpOrDown(false); - } - - /** - * press `left` arrow to remove the last selected option. - */ private moveLeft(): void { const options = this.activatedOptions; if (options.length) { @@ -1035,45 +566,28 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces } } - /** - * press `right` arrow to select the next column option. - */ private moveRight(): void { const length = this.activatedOptions.length; - const options = this.nzColumns[ length ]; + const options = this.columns[ length ]; if (options && options.length) { const nextOpt = options.find(o => !o.disabled); if (nextOpt) { - this.setActiveOption(nextOpt, length); + this.setOptionActivated(nextOpt, length); } } } - /** - * 鼠标划入选项 - * - * @param option 菜单选项 - * @param index 选项所在的列组的索引 - * @param event 鼠标事件 - */ - onOptionMouseEnter(option: CascaderOption, index: number, event: Event): void { + onOptionMouseEnter(option: CascaderOption, columnIndex: number, event: Event): void { event.preventDefault(); if (this.nzExpandTrigger === 'hover' && !option.isLeaf) { - this.delaySelect(option, index, true); + this.delaySelectOption(option, columnIndex, true); } } - /** - * 鼠标划出选项 - * - * @param option 菜单选项 - * @param index 选项所在的列组的索引 - * @param event 鼠标事件 - */ - onOptionMouseLeave(option: CascaderOption, index: number, event: Event): void { + onOptionMouseLeave(option: CascaderOption, columnIndex: number, event: Event): void { event.preventDefault(); if (this.nzExpandTrigger === 'hover' && !option.isLeaf) { - this.delaySelect(option, index, false); + this.delaySelectOption(option, columnIndex, false); } } @@ -1084,162 +598,82 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces } } - private delaySelect(option: CascaderOption, index: number, doSelect: boolean): void { + private delaySelectOption(option: CascaderOption, index: number, doSelect: boolean): void { this.clearDelaySelectTimer(); if (doSelect) { this.delaySelectTimer = setTimeout(() => { - // 鼠标滑入只展开,不进行选中操作 - this.setActiveOption(option, index); + this.setOptionActivated(option, index); this.delaySelectTimer = null; }, 150); } } - public getSubmitValue(): any[] { - const values: any[] = []; - this.selectedOptions.forEach(option => { - values.push(this.getOptionValue(option)); - }); - return values; - } - - private onValueChange(): void { - const value = this.getSubmitValue(); - if (!arrayEquals(this.value, value)) { - this.defaultValue = null; // clear the init-value - this.value = value; - this.onChange(value); // Angular need this - if (value.length === 0) { - this.nzClear.emit(); // first trigger `clear` and then `change` - } - this.nzSelectionChange.emit(this.selectedOptions); - this.nzChange.emit(value); - } - } - - constructor(private elementRef: ElementRef, - private cdr: ChangeDetectorRef, - private nzUpdateHostClassService: NzUpdateHostClassService) { - } + //#endregion - private findOption(option: any, index: number): CascaderOption { - const options: CascaderOption[] = this.nzColumns[ index ]; - if (options) { - const value = typeof option === 'object' ? this.getOptionValue(option) : option; - return options.find(o => value === this.getOptionValue(o)); - } - return null; - } + //#region Search - private isLoaded(index: number): boolean { - return this.nzColumns[ index ] && this.nzColumns[ index ].length > 0; - } + private toggleSearchMode(): void { + const willBeInSearch = !!this._inputValue; - private activateOnInit(index: number, value: any): void { - let option = this.findOption(value, index); - if (!option) { - option = typeof value === 'object' ? value : { - [ `${this.nzValueProperty || 'value'}` ]: value, - [ `${this.nzLabelProperty || 'label'}` ]: value - }; - } - this.setActiveOption(option, index, false, false); - } + // Take a snapshot before entering search mode. + if (!this.isSearching && willBeInSearch) { + this.isSearching = true; + this.activatedOptionsSnapshot = this.activatedOptions; + this.activatedOptions = []; + this.labelRenderText = ''; - private initOptions(index: number): void { - const vs = this.defaultValue; - const load = () => { - this.activateOnInit(index, vs[ index ]); - if (index < vs.length - 1) { - this.initOptions(index + 1); - } - if (index === vs.length - 1) { - this.afterWriteValue(); + if (this.input) { + const width = this.input.nativeElement.offsetWidth; + this.dropdownWidthStyle = `${width}px`; } - }; - - if (this.isLoaded(index) || !this.nzLoadData) { - load(); - } else { - const node = this.activatedOptions[ index - 1 ] || {}; - this.loadChildren(node, index - 1, load, this.afterWriteValue); } - } - - afterWriteValue(): void { - this.selectedOptions = this.activatedOptions; - this.value = this.getSubmitValue(); - this.buildDisplayLabel(); - } - /** - * Write a new value to the element. - * - * @Override (From ControlValueAccessor interface) - */ - writeValue(value: any): void { - const vs = this.defaultValue = toArray(value); - if (vs.length) { - this.initOptions(0); - } else { - this.value = vs; - this.activatedOptions = []; - this.afterWriteValue(); + // Restore the snapshot after leaving search mode. + if (this.isSearching && !willBeInSearch) { + this.isSearching = false; + this.activatedOptions = this.activatedOptionsSnapshot; + this.columns = this.columnsSnapshot; + this.dropdownWidthStyle = ''; + if (this.activatedOptions) { + this.buildDisplayLabel(); + } } - } - - registerOnChange(fn: (_: any) => {}): void { - this.onChange = fn; - } - - registerOnTouched(fn: () => {}): void { - this.onTouched = fn; - } - setDisabledState(isDisabled: boolean): void { - if (isDisabled) { - this.closeMenu(); + if (this.isSearching) { + this.prepareSearchValue(); } - this.nzDisabled = isDisabled; } private prepareSearchValue(): void { const results: CascaderSearchOption[] = []; const path: CascaderOption[] = []; + const defaultFilter = (inputValue: string, p: CascaderOption[]): boolean => { - let flag = false; - p.forEach(n => { - const labelName = this.nzLabelProperty; - if (n[ labelName ] && n[ labelName ].indexOf(inputValue) > -1) { - flag = true; - } + return p.some(n => { + const label = this.getOptionLabel(n); + return label && label.indexOf(inputValue) !== -1; }); - return flag; }; const filter: (inputValue: string, p: CascaderOption[]) => boolean = this.nzShowSearch instanceof Object && (this.nzShowSearch as NzShowSearchOptions).filter ? (this.nzShowSearch as NzShowSearchOptions).filter : defaultFilter; + const sorter: (a: CascaderOption[], b: CascaderOption[], inputValue: string) => number = this.nzShowSearch instanceof Object && (this.nzShowSearch as NzShowSearchOptions).sorter; + const loopParent = (node: CascaderOption, forceDisabled = false) => { const disabled = forceDisabled || node.disabled; path.push(node); node.children.forEach((sNode) => { - if (!sNode.parent) { - sNode.parent = node; - } - /** 搜索的同时建立 parent 连接,因为用户直接搜索的话是没有建立连接的,会提升从叶子节点回溯的难度 */ - if (!sNode.isLeaf) { - loopParent(sNode, disabled); - } - if (sNode.isLeaf || !sNode.children || !sNode.children.length) { - loopChild(sNode, disabled); - } + if (!sNode.parent) { sNode.parent = node; } // Build parent reference when doing searching + if (!sNode.isLeaf) { loopParent(sNode, disabled); } + if (sNode.isLeaf || !sNode.children || !sNode.children.length) { loopChild(sNode, disabled); } }); path.pop(); }; + const loopChild = (node: CascaderOption, forceDisabled = false) => { path.push(node); const cPath = Array.from(path); @@ -1247,62 +681,142 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces const disabled = forceDisabled || node.disabled; const option: CascaderSearchOption = { disabled, - isLeaf : true, - path : cPath, - [ this.nzLabelProperty ]: cPath.map(p => p.label).join(' / ') + isLeaf : true, + path : cPath, + [ this.nzLabelProperty ]: cPath.map(p => this.getOptionLabel(p)).join(' / ') }; results.push(option); } path.pop(); }; - this.oldColumnsHolder[ 0 ].forEach(node => (node.isLeaf || !node.children || !node.children.length) + this.columnsSnapshot[ 0 ].forEach(node => (node.isLeaf || !node.children || !node.children.length) ? loopChild(node) : loopParent(node)); if (sorter) { results.sort((a, b) => sorter(a.path, b.path, this._inputValue)); } - this.nzColumns = [ results ]; - } - - renderSearchString(str: string): string { - return str.replace(new RegExp(this._inputValue, 'g'), - `${this._inputValue}`); + this.columns = [ results ]; } - setSearchActiveOption(result: CascaderSearchOption, event: Event): void { + setSearchOptionActivated(result: CascaderSearchOption, event: Event): void { this.activatedOptions = [ result ]; this.delaySetMenuVisible(false, 200); setTimeout(() => { - this.inputValue = ''; // Not only remove `inputValue` but also reverse `nzColumns` in the hook. + this.inputValue = ''; const index = result.path.length - 1; - const destiNode = result.path[ index ]; - const mockClickParent = (node: CascaderOption, cIndex: number) => { + const destinationNode = result.path[ index ]; + // NOTE: optimize this. + const mockClickParent = (node: CascaderOption, columnIndex: number) => { if (node && node.parent) { - mockClickParent(node.parent, cIndex - 1); + mockClickParent(node.parent, columnIndex - 1); } - this.onOptionClick(node, cIndex, event); + this.onOptionClick(node, columnIndex, event); }; - mockClickParent(destiNode, index); + mockClickParent(destinationNode, index); }, 300); } - ngOnInit(): void { - // 设置样式 - this.setClassMap(); - this.setLabelClass(); - this.setArrowClass(); - this.setLoadingClass(); - this.setClearClass(); - this.setInputClass(); - this.setMenuClass(); - this.setMenuColumnClass(); + //#endregion + + //#region Helpers + + private get hasInput(): boolean { + return !!this.inputValue; + } + + private get hasValue(): boolean { + return !!this.value && !!this.value.length; + } + + get showPlaceholder(): boolean { + return !(this.hasInput || this.hasValue); + } + + get clearIconVisible(): boolean { + return this.nzAllowClear && !this.nzDisabled && (this.hasValue || this.hasInput); + } + + get isLabelRenderTemplate(): boolean { + return !!this.nzLabelRender; + } + + // tslint:disable-next-line:no-any + getOptionLabel(option: CascaderOption): any { + return option[ this.nzLabelProperty || 'label' ]; + } + + // tslint:disable-next-line:no-any + getOptionValue(option: CascaderOption): any { + return option[ this.nzValueProperty || 'value' ]; + } + + isOptionActivated(option: CascaderOption, index: number): boolean { + const activeOpt = this.activatedOptions[ index ]; + return activeOpt === option; + } + + private buildDisplayLabel(): void { + const selectedOptions = this.selectedOptions; + const labels: string[] = selectedOptions.map(o => this.getOptionLabel(o)); + if (this.isLabelRenderTemplate) { + this.labelRenderContext = { labels, selectedOptions }; + } else { + this.labelRenderText = defaultDisplayRender.call(this, labels, selectedOptions); + } + // When components inits with default value, this would make display label appear correctly. + this.cdr.detectChanges(); + } + + //#endregion + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.closeMenu(); + } + this.nzDisabled = isDisabled; + } + + closeMenu(): void { + this.blur(); + this.clearDelayMenuTimer(); + this.setMenuVisible(false); + } + + constructor(private elementRef: ElementRef, private cdr: ChangeDetectorRef) { } ngOnDestroy(): void { - this.clearDelayTimer(); + this.clearDelayMenuTimer(); this.clearDelaySelectTimer(); } + registerOnChange(fn: () => {}): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => {}): void { + this.onTouched = fn; + } + + // tslint:disable-next-line:no-any + writeValue(value: any): void { + const vs = this.defaultValue = toArray(value); + if (vs.length) { + this.initOptions(0); + } else { + this.value = vs; + this.activatedOptions = []; + this.afterWriteValue(); + } + } + + onPositionChange(position: ConnectedOverlayPositionChange): void { + const newValue = position.connectionPair.originY === 'bottom' ? 'bottom' : 'top'; + if (this.dropDownPosition !== newValue) { + this.dropDownPosition = newValue; + this.cdr.detectChanges(); + } + } } diff --git a/components/cascader/nz-cascader.module.ts b/components/cascader/nz-cascader.module.ts index fbe49a6c409..d6035d1bcaf 100644 --- a/components/cascader/nz-cascader.module.ts +++ b/components/cascader/nz-cascader.module.ts @@ -2,16 +2,18 @@ import { OverlayModule } from '@angular/cdk/overlay'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { NzIconModule } from '../icon/nz-icon.module'; +import { NzI18nModule } from '../i18n/nz-i18n.module'; +import { NzIconModule } from '../icon/nz-icon.module'; import { NzInputModule } from '../input/nz-input.module'; - +import { NzCascaderOptionComponent } from './nz-cascader-li.component'; import { NzCascaderComponent } from './nz-cascader.component'; @NgModule({ - imports : [ CommonModule, FormsModule, OverlayModule, NzInputModule, NzIconModule ], + imports : [ CommonModule, FormsModule, OverlayModule, NzInputModule, NzIconModule, NzI18nModule ], declarations: [ - NzCascaderComponent + NzCascaderComponent, + NzCascaderOptionComponent ], exports : [ NzCascaderComponent diff --git a/components/cascader/nz-cascader.spec.ts b/components/cascader/nz-cascader.spec.ts index e1e0aa8b301..84987f942a6 100644 --- a/components/cascader/nz-cascader.spec.ts +++ b/components/cascader/nz-cascader.spec.ts @@ -13,8 +13,9 @@ import { dispatchMouseEvent } from '../core/testing'; -import { CascaderOption, NzCascaderComponent, NzShowSearchOptions } from './nz-cascader.component'; +import { NzCascaderComponent } from './nz-cascader.component'; import { NzCascaderModule } from './nz-cascader.module'; +import { CascaderOption, NzShowSearchOptions } from './types'; describe('cascader', () => { let fixture; @@ -79,12 +80,13 @@ describe('cascader', () => { const input: HTMLElement = cascader.nativeElement.querySelector('.ant-cascader-input'); expect(input.getAttribute('placeholder')).toBe(placeholder); }); - it('should prefixCls work', () => { - testComponent.nzPrefixCls = 'new-cascader'; - fixture.detectChanges(); - expect(testComponent.cascader.nzPrefixCls).toBe('new-cascader'); - expect(cascader.nativeElement.className).toContain('new-cascader new-cascader-picker'); - }); + // This API is redundant and should be removed. + // it('should prefixCls work', () => { + // testComponent.nzPrefixCls = 'new-cascader'; + // fixture.detectChanges(); + // expect(testComponent.cascader.nzPrefixCls).toBe('new-cascader'); + // expect(cascader.nativeElement.className).toContain('new-cascader new-cascader-picker'); + // }); it('should size work', () => { testComponent.nzSize = 'small'; fixture.detectChanges(); @@ -158,7 +160,7 @@ describe('cascader', () => { fixture.detectChanges(); tick(200); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(1); cascader.nativeElement.click(); @@ -167,7 +169,7 @@ describe('cascader', () => { fixture.detectChanges(); flush(); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(2); })); it('should mouse hover toggle open', fakeAsync(() => { @@ -175,12 +177,12 @@ describe('cascader', () => { testComponent.nzTriggerAction = 'hover'; fixture.detectChanges(); expect(testComponent.nzDisabled).toBe(false); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(0); dispatchMouseEvent(cascader.nativeElement, 'mouseenter'); tick(300); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(1); const mouseleave = createMouseEvent('mouseleave'); @@ -202,7 +204,7 @@ describe('cascader', () => { dispatchEvent(cascader.nativeElement, mouseleave); tick(300); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); mouseleave.initMouseEvent('mouseleave', false, /* canBubble */ @@ -222,7 +224,7 @@ describe('cascader', () => { dispatchEvent(cascader.nativeElement, mouseleave); tick(300); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); mouseleave.initMouseEvent('mouseleave', false, /* canBubble */ @@ -244,7 +246,7 @@ describe('cascader', () => { fixture.detectChanges(); flush(); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(2); })); it('should mouse hover toggle open immediately', fakeAsync(() => { @@ -253,18 +255,18 @@ describe('cascader', () => { testComponent.nzMouseEnterDelay = 0; testComponent.nzMouseLeaveDelay = 0; fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); dispatchMouseEvent(cascader.nativeElement, 'mouseenter'); fixture.detectChanges(); flush(); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(1); dispatchMouseEvent(cascader.nativeElement, 'mouseleave'); fixture.detectChanges(); flush(); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(2); })); it('should clear timer on option mouseenter and mouseleave', fakeAsync(() => { @@ -274,10 +276,10 @@ describe('cascader', () => { testComponent.nzExpandTrigger = 'hover'; fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); testComponent.cascader.setMenuVisible(true); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); flush(); fixture.detectChanges(); const optionEl = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; // 第1列第1个 @@ -311,13 +313,13 @@ describe('cascader', () => { fixture.detectChanges(); tick(); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(0); testComponent.cascader.setMenuVisible(true); fixture.detectChanges(); tick(); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(0); })); it('should disabled state work', fakeAsync(() => { @@ -331,7 +333,7 @@ describe('cascader', () => { fixture.detectChanges(); tick(); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(0); })); it('should disabled mouse hover open', fakeAsync(() => { @@ -339,12 +341,12 @@ describe('cascader', () => { testComponent.nzDisabled = true; fixture.detectChanges(); expect(testComponent.cascader.nzDisabled).toBe(true); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(0); dispatchMouseEvent(cascader.nativeElement, 'mouseenter'); tick(300); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(0); testComponent.nzDisabled = false; @@ -352,25 +354,25 @@ describe('cascader', () => { testComponent.cascader.setMenuVisible(true); fixture.detectChanges(); expect(testComponent.cascader.nzDisabled).toBe(false); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(1); testComponent.nzDisabled = true; fixture.detectChanges(); dispatchMouseEvent(cascader.nativeElement, 'mouseleave'); tick(300); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(1); })); it('should mouse leave not work when menu not open', fakeAsync(() => { testComponent.nzTriggerAction = [ 'hover' ]; fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); dispatchMouseEvent(cascader.nativeElement, 'mouseleave'); fixture.detectChanges(); tick(300); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(0); })); it('should clear value work', fakeAsync(() => { @@ -456,7 +458,7 @@ describe('cascader', () => { fixture.detectChanges(); tick(200); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); expect(overlayContainerElement.querySelector('.ant-cascader-menus').classList).toContain('menu-classA'); expect(overlayContainerElement.querySelector('.ant-cascader-menu').classList).toContain('column-classA'); })); @@ -466,7 +468,7 @@ describe('cascader', () => { fixture.detectChanges(); tick(200); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); const targetElement = overlayContainerElement.querySelector('.menu-classA') as HTMLElement; expect(targetElement.style.height).toBe('120px'); })); @@ -688,7 +690,7 @@ describe('cascader', () => { fixture.detectChanges(); flush(); // wait for cdk-overlay to open fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(3); // 3列 const itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; @@ -717,7 +719,7 @@ describe('cascader', () => { fixture.detectChanges(); flush(); // wait for cdk-overlay close fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.values.join(',')).toBe('zhejiang,ningbo'); })); it('should click option to change column count 3', () => { @@ -763,53 +765,53 @@ describe('cascader', () => { fixture.detectChanges(); testComponent.cascader.setMenuVisible(true); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); (overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement).click(); // 第1列第1个 fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); (overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(2) .ant-cascader-menu-item:nth-child(1)') as HTMLElement).click(); // 第2列第1个 fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); (overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(3) .ant-cascader-menu-item:nth-child(1)') as HTMLElement).click(); // 第3列第1个 fixture.detectChanges(); tick(200); fixture.detectChanges(); flush(); // wait for cdk-overlay to close fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(0); })); it('should open menu when press DOWN_ARROW', fakeAsync(() => { const DOWN_ARROW = 40; fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); dispatchKeyboardEvent(cascader.nativeElement, 'keydown', DOWN_ARROW); fixture.detectChanges(); tick(200); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); })); it('should open menu when press UP_ARROW', fakeAsync(() => { const UP_ARROW = 38; fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); dispatchKeyboardEvent(cascader.nativeElement, 'keydown', UP_ARROW); fixture.detectChanges(); tick(200); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); })); it('should close menu when press ESC', fakeAsync(() => { const ESC = 27; fixture.detectChanges(); testComponent.cascader.setMenuVisible(true); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); dispatchKeyboardEvent(cascader.nativeElement, 'keydown', ESC); fixture.detectChanges(); flush(); // wait for cdk-overlay to close fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); })); it('should navigate up when press UP_ARROW', fakeAsync(() => { const UP_ARROW = 38; @@ -930,7 +932,7 @@ describe('cascader', () => { expect(testComponent.values[ 2 ]).toBe('xihu'); flush(); // wait for cdk-overlay to close fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); })); it('should key nav disabled option correct', fakeAsync(() => { const DOWN_ARROW = 40; @@ -1009,10 +1011,10 @@ describe('cascader', () => { fixture.detectChanges(); keys.forEach(key => { - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); dispatchKeyboardEvent(cascader.nativeElement, 'keydown', key); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); }); })); it('should expand option on hover', fakeAsync(() => { @@ -1087,7 +1089,7 @@ describe('cascader', () => { flush(); // wait for cdk-overlay to close fixture.detectChanges(); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(0); // 0列 - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); })); it('should not expand disabled option on hover', fakeAsync(() => { testComponent.nzExpandTrigger = 'hover'; @@ -1135,7 +1137,7 @@ describe('cascader', () => { itemEl1.click(); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); expect(itemEl1.classList).toContain('ant-cascader-menu-item-active'); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(2); // 2列 expect(testComponent.values).toBeDefined(); @@ -1148,7 +1150,7 @@ describe('cascader', () => { itemEl2.click(); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); expect(itemEl1.classList).toContain('ant-cascader-menu-item-active'); expect(itemEl2.classList).toContain('ant-cascader-menu-item-active'); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(3); // 3列 @@ -1174,7 +1176,7 @@ describe('cascader', () => { flush(); // wait for cdk-overlay to close fixture.detectChanges(); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(0); // 0列 - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); })); it('should not change on hover work', fakeAsync(() => { testComponent.nzChangeOnSelect = true; @@ -1196,7 +1198,7 @@ describe('cascader', () => { tick(200); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); expect(itemEl1.classList).toContain('ant-cascader-menu-item-active'); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(2); // 2列 expect(testComponent.values).toBeNull(); // mouseenter does not trigger selection @@ -1209,7 +1211,7 @@ describe('cascader', () => { tick(200); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); expect(itemEl1.classList).toContain('ant-cascader-menu-item-active'); expect(itemEl2.classList).toContain('ant-cascader-menu-item-active'); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(3); // 3列 @@ -1239,7 +1241,7 @@ describe('cascader', () => { flush(); // wait for cdk-overlay to close fixture.detectChanges(); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(0); // 0列 - expect(testComponent.cascader.isMenuVisible()).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); })); it('should change on function work', fakeAsync(() => { testComponent.nzChangeOn = testComponent.fakeChangeOn; @@ -1264,7 +1266,7 @@ describe('cascader', () => { fixture.detectChanges(); tick(200); fixture.detectChanges(); - expect(testComponent.cascader.isMenuVisible()).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; // 第1列第1个 itemEl2 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(2)') as HTMLElement; // 第1列第2个 @@ -1300,32 +1302,30 @@ describe('cascader', () => { fixture.detectChanges(); expect(testComponent.cascader.dropDownPosition).toBe('bottom'); }); - it('should support search', (done) => { + it('should support search', fakeAsync(() => { fixture.detectChanges(); testComponent.nzShowSearch = true; fixture.detectChanges(); - // const input: HTMLElement = cascader.nativeElement.querySelector('.ant-cascader-input'); const spy = spyOn(testComponent.cascader, 'focus'); cascader.nativeElement.click(); fixture.detectChanges(); - // expect(document.activeElement).toBe(input); expect(spy).toHaveBeenCalled(); testComponent.cascader.inputValue = 'o'; testComponent.cascader.setMenuVisible(true); fixture.detectChanges(); const itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; - expect(testComponent.cascader.inSearch).toBe(true); + expect(testComponent.cascader.isSearching).toBe(true); expect(itemEl1.innerText).toBe('Zhejiang / Hangzhou / West Lake'); itemEl1.click(); - fixture.whenStable().then(() => { - expect(testComponent.cascader.inSearch).toBe(false); - expect(testComponent.cascader.menuVisible).toBe(false); - expect(testComponent.cascader.inputValue).toBe(''); - expect(testComponent.values.join(',')).toBe('zhejiang,hangzhou,xihu'); - done(); - }); - }); - it('should support nzLabelProperty', (done) => { + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expect(testComponent.cascader.isSearching).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); + expect(testComponent.cascader.inputValue).toBe(''); + expect(testComponent.values.join(',')).toBe('zhejiang,hangzhou,xihu'); + })); + it('should support nzLabelProperty', fakeAsync(() => { testComponent.nzShowSearch = true; testComponent.nzLabelProperty = 'l'; fixture.detectChanges(); @@ -1335,27 +1335,21 @@ describe('cascader', () => { testComponent.cascader.setMenuVisible(true); fixture.detectChanges(); const itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; - expect(testComponent.cascader.inSearch).toBe(true); + expect(testComponent.cascader.isSearching).toBe(true); expect(itemEl1.innerText).toBe('Zhejiang / Hangzhou / West Lake'); itemEl1.click(); - fixture.whenStable().then(() => { - expect(testComponent.cascader.inSearch).toBe(false); - expect(testComponent.cascader.menuVisible).toBe(false); - expect(testComponent.cascader.inputValue).toBe(''); - expect(testComponent.values.join(',')).toBe('zhejiang,hangzhou,xihu'); - done(); - }); - }); - it('should support custom filter', (done) => { + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expect(testComponent.cascader.isSearching).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); + expect(testComponent.cascader.inputValue).toBe(''); + expect(testComponent.values.join(',')).toBe('zhejiang,hangzhou,xihu'); + })); + it('should support custom filter', fakeAsync(() => { testComponent.nzShowSearch = { filter(inputValue: string, path: CascaderOption[]): boolean { - let flag = false; - path.forEach(p => { - if (p.label.indexOf(inputValue) > -1) { - flag = true; - } - }); - return flag; + return path.some(p => p.label.indexOf(inputValue) !== -1); } } as NzShowSearchOptions; fixture.detectChanges(); @@ -1363,18 +1357,18 @@ describe('cascader', () => { testComponent.cascader.setMenuVisible(true); fixture.detectChanges(); const itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; - expect(testComponent.cascader.inSearch).toBe(true); + expect(testComponent.cascader.isSearching).toBe(true); expect(itemEl1.innerText).toBe('Zhejiang / Hangzhou / West Lake'); itemEl1.click(); - fixture.whenStable().then(() => { - expect(testComponent.cascader.inSearch).toBe(false); - expect(testComponent.cascader.menuVisible).toBe(false); - expect(testComponent.cascader.inputValue).toBe(''); - expect(testComponent.values.join(',')).toBe('zhejiang,hangzhou,xihu'); - done(); - }); - }); - it('should support custom sorter', (done) => { + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expect(testComponent.cascader.isSearching).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); + expect(testComponent.cascader.inputValue).toBe(''); + expect(testComponent.values.join(',')).toBe('zhejiang,hangzhou,xihu'); + })); + it('should support custom sorter', fakeAsync(() => { testComponent.nzShowSearch = { sorter(a: CascaderOption[], b: CascaderOption[], inputValue: string): number { const l1 = a[ 0 ].label; @@ -1387,17 +1381,17 @@ describe('cascader', () => { testComponent.cascader.setMenuVisible(true); fixture.detectChanges(); const itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; - expect(testComponent.cascader.inSearch).toBe(true); + expect(testComponent.cascader.isSearching).toBe(true); expect(itemEl1.innerText).toBe('Jiangsu / Nanjing / Zhong Hua Men'); itemEl1.click(); - fixture.whenStable().then(() => { - expect(testComponent.cascader.inSearch).toBe(false); - expect(testComponent.cascader.menuVisible).toBe(false); - expect(testComponent.cascader.inputValue).toBe(''); - expect(testComponent.values.join(',')).toBe('jiangsu,nanjing,zhonghuamen'); - done(); - }); - }); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expect(testComponent.cascader.isSearching).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); + expect(testComponent.cascader.inputValue).toBe(''); + expect(testComponent.values.join(',')).toBe('jiangsu,nanjing,zhonghuamen'); + })); it('should forbid disabled search options to be clicked', fakeAsync(() => { testComponent.nzOptions = options4; fixture.detectChanges(); @@ -1406,11 +1400,11 @@ describe('cascader', () => { fixture.detectChanges(); const itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; expect(itemEl1.innerText).toBe('Zhejiang / Hangzhou / West Lake'); - expect(testComponent.cascader.nzColumns[ 0 ][ 0 ].disabled).toBe(true); + expect(testComponent.cascader.columns[ 0 ][ 0 ].disabled).toBe(true); itemEl1.click(); tick(300); fixture.detectChanges(); - expect(testComponent.cascader.inSearch).toBe(true); + expect(testComponent.cascader.isSearching).toBe(true); expect(testComponent.cascader.menuVisible).toBe(true); expect(testComponent.cascader.inputValue).toBe('o'); expect(testComponent.values).toBe(null); @@ -1421,9 +1415,9 @@ describe('cascader', () => { testComponent.cascader.inputValue = 'o'; testComponent.cascader.setMenuVisible(true); fixture.detectChanges(); - expect(testComponent.cascader.nzColumns[ 0 ][ 0 ].disabled).toBe(true); - expect(testComponent.cascader.nzColumns[ 0 ][ 1 ].disabled).toBe(undefined); - expect(testComponent.cascader.nzColumns[ 0 ][ 2 ].disabled).toBe(true); + expect(testComponent.cascader.columns[ 0 ][ 0 ].disabled).toBe(true); + expect(testComponent.cascader.columns[ 0 ][ 1 ].disabled).toBe(undefined); + expect(testComponent.cascader.columns[ 0 ][ 2 ].disabled).toBe(true); }); it('should support arrow in search mode', (done) => { const DOWN_ARROW = 40; @@ -1465,7 +1459,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(itemEl1.classList).not.toContain('ant-cascader-menu-item-active'); }); - it('should support search a root node have no children ', (done) => { + it('should support search a root node have no children ', fakeAsync(() => { fixture.detectChanges(); testComponent.nzShowSearch = true; testComponent.nzOptions = options5; @@ -1478,17 +1472,17 @@ describe('cascader', () => { testComponent.cascader.setMenuVisible(true); fixture.detectChanges(); const itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; - expect(testComponent.cascader.inSearch).toBe(true); + expect(testComponent.cascader.isSearching).toBe(true); expect(itemEl1.innerText).toBe('Root'); itemEl1.click(); - fixture.whenStable().then(() => { - expect(testComponent.cascader.inSearch).toBe(false); - expect(testComponent.cascader.menuVisible).toBe(false); - expect(testComponent.cascader.inputValue).toBe(''); - expect(testComponent.values.join(',')).toBe('root'); - done(); - }); - }); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expect(testComponent.cascader.isSearching).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); + expect(testComponent.cascader.inputValue).toBe(''); + expect(testComponent.values.join(',')).toBe('root'); + })); it('should re-prepare search results when nzOptions change', () => { fixture.detectChanges(); testComponent.nzShowSearch = true; @@ -1497,11 +1491,11 @@ describe('cascader', () => { testComponent.cascader.setMenuVisible(true); fixture.detectChanges(); let itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; - expect(testComponent.cascader.inSearch).toBe(true); + expect(testComponent.cascader.isSearching).toBe(true); expect(itemEl1.innerText).toBe('Zhejiang / Hangzhou / West Lake'); testComponent.nzOptions = options2; fixture.detectChanges(); - expect(testComponent.cascader.inSearch).toBe(true); + expect(testComponent.cascader.isSearching).toBe(true); itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; expect(itemEl1.innerText).toBe('Option1 / Option11'); }); @@ -1607,7 +1601,7 @@ describe('cascader', () => { tick(3000); fixture.detectChanges(); expect(testComponent.addCallTimes).toHaveBeenCalledTimes(3); - expect(testComponent.cascader.nzColumns.length).toBe(3); + expect(testComponent.cascader.columns.length).toBe(3); expect(testComponent.values.join(',')).toBe('zhejiang,hangzhou,xihu'); })); @@ -1854,7 +1848,6 @@ const options5 = [ { [nzLabelProperty]="nzLabelProperty" [nzValueProperty]="nzValueProperty" [nzPlaceHolder]="nzPlaceHolder" - [nzPrefixCls]="nzPrefixCls" [nzShowArrow]="nzShowArrow" [nzShowInput]="nzShowInput" [nzShowSearch]="nzShowSearch" @@ -1899,7 +1892,6 @@ export class NzDemoCascaderDefaultComponent { nzLabelProperty = 'label'; nzValueProperty = 'value'; nzPlaceHolder = 'please select'; - nzPrefixCls = 'ant-cascader'; nzShowArrow = true; nzShowInput = true; nzShowSearch: boolean | NzShowSearchOptions = false; diff --git a/components/cascader/public-api.ts b/components/cascader/public-api.ts index 3c6add083c3..234fabbcae6 100644 --- a/components/cascader/public-api.ts +++ b/components/cascader/public-api.ts @@ -1,2 +1,3 @@ export * from './nz-cascader.module'; export * from './nz-cascader.component'; +export * from './types'; diff --git a/components/cascader/types.ts b/components/cascader/types.ts new file mode 100644 index 00000000000..4431b586d67 --- /dev/null +++ b/components/cascader/types.ts @@ -0,0 +1,27 @@ +export type NzCascaderExpandTrigger = 'click' | 'hover'; +export type NzCascaderTriggerType = 'click' | 'hover'; +export type NzCascaderSize = 'small' | 'large' | 'default' ; + +// tslint:disable:no-any +export interface CascaderOption { + value?: any; + label?: string; + title?: string; + disabled?: boolean; + loading?: boolean; + isLeaf?: boolean; + parent?: CascaderOption; + children?: CascaderOption[]; + + [ key: string ]: any; +} +// tslint:enable:no-any + +export interface CascaderSearchOption extends CascaderOption { + path: CascaderOption[]; +} + +export interface NzShowSearchOptions { + filter?(inputValue: string, path: CascaderOption[]): boolean; + sorter?(a: CascaderOption[], b: CascaderOption[], inputValue: string): number; +} diff --git a/components/core/interface/interface.ts b/components/core/interface/interface.ts index 91c9b7af44a..cec0b74e495 100644 --- a/components/core/interface/interface.ts +++ b/components/core/interface/interface.ts @@ -1,3 +1,3 @@ export interface ClassMap { - [ key: string ]: boolean; + [ key: string ]: boolean; } diff --git a/components/core/overlay/overlay-position-map.ts b/components/core/overlay/overlay-position-map.ts index ac2797b8aa2..12364eb33e0 100644 --- a/components/core/overlay/overlay-position-map.ts +++ b/components/core/overlay/overlay-position-map.ts @@ -90,6 +90,7 @@ export const POSITION_MAP: { [key: string]: ConnectionPositionPair } = { // TODO: The whole logic does not make sense here, _objectValues just returns a copy of original array export const DEFAULT_4_POSITIONS = _objectValues([ POSITION_MAP.top, POSITION_MAP.right, POSITION_MAP.bottom, POSITION_MAP.left]); export const DEFAULT_DROPDOWN_POSITIONS = _objectValues([ POSITION_MAP.bottomLeft, POSITION_MAP.topLeft ]); +export const EXPANDED_DROPDOWN_POSITIONS = _objectValues([ POSITION_MAP.bottomLeft, POSITION_MAP.bottomRight, POSITION_MAP.topLeft, POSITION_MAP.topRight ]); // export const DEFAULT_DATEPICKER_POSITIONS = [ // { diff --git a/components/core/util/array.ts b/components/core/util/array.ts new file mode 100644 index 00000000000..a05e9ffff17 --- /dev/null +++ b/components/core/util/array.ts @@ -0,0 +1,25 @@ +export function toArray(value: T | T[]): T[] { + let ret: T[]; + if (value == null) { + ret = []; + } else if (!Array.isArray(value)) { + ret = [ value ]; + } else { + ret = value; + } + return ret; +} + +export function arrayEquals(array1: T[], array2: T[]): boolean { + if (!array1 || !array2 || array1.length !== array2.length) { + return false; + } + + const len = array1.length; + for (let i = 0; i < len; i++) { + if (array1[ i ] !== array2[ i ]) { + return false; + } + } + return true; +} diff --git a/components/core/util/logger/logger.module.ts b/components/core/util/logger/logger.module.ts index 1bf233207ce..0680598acdc 100644 --- a/components/core/util/logger/logger.module.ts +++ b/components/core/util/logger/logger.module.ts @@ -5,7 +5,7 @@ import { LOGGER_SERVICE_PROVIDER, NZ_LOGGER_STATE } from './logger.service'; @NgModule({ providers: [ { provide: NZ_LOGGER_STATE, useValue: false }, - LOGGER_SERVICE_PROVIDER, - ], + LOGGER_SERVICE_PROVIDER + ] }) export class LoggerModule { }