import {Component, OnInit, OnChanges, OnDestroy, Input, Output, SimpleChanges, HostBinding, HostListener, AfterViewInit, forwardRef, ElementRef, EventEmitter, Renderer2, TemplateRef, ViewChild, ViewEncapsulation, ChangeDetectorRef} 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 { CdkConnectedOverlay, ConnectedOverlayPositionChange, ConnectionPositionPair} from '@angular/cdk/overlay'; const cloneDeep = require('lodash.clonedeep'); const ESC = 27; function toBoolean(value: boolean | string): boolean { return value === '' || (value && value !== 'false'); } 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; } 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; } const defaultDisplayRender = (label: any) => label.join(' ; '); const defaultIntervalRender = (label: any) => label.join('、'); export type CascaderExpandTrigger = 'click' | 'hover'; export type CascaderTriggerType = 'click' | 'hover'; export interface CascaderOption { value?: string; label?: string; title?: string; disabled?: boolean; loading?: boolean; isLeaf?: boolean; parent?: CascaderOption; children?: CascaderOption[]; [key: string]: any; } export interface DisplayLabelContext { labels?: string[]; selectedOptions?: CascaderOption[][]; } @Component({ encapsulation: ViewEncapsulation.None, selector: 'cm-checkcascader', templateUrl: './checkcascader.component.html', styleUrls: ['./checkcascader.component.less'], providers: [ { provide : NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CheckCascaderComponent), multi : true } ] }) export class CheckCascaderComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit, ControlValueAccessor { private _allowClear = true; private _disabled = false; private _enableCache = true; private _showArrow = true; private _showInput = true; private _showSearch = false; private _changeOnSelect = false; _cmPlaceHolder: string = "Please select"; _dropDownPosition: 'top' | 'center' | 'bottom' = 'bottom'; _el: HTMLElement; _prefixCls = 'ant-cascader'; _inputPrefixCls = 'ant-input'; _focused = false; _popupVisible = false; _displayLabel: string | TemplateRef; _displayLabelIsTemplate = false; _displayLabelContext: DisplayLabelContext = {}; /* tslint:disable-next-line:variable-name */ __inputValue = ''; get _inputValue(): string { return this.__inputValue; } set _inputValue(inputValue: string) { this.__inputValue = inputValue; if (inputValue.length) { this._addHostClass(`${this._prefixCls}-picker-with-value`); } else { this._removeHostClass(`${this._prefixCls}-picker-with-value`); } } _searchValue: any[] = []; // check if change happened _lastValue: any[]; // selection will trigger value change _selectedOptions: CascaderOption[][] = []; // activaction will not trigger value change _activatedOptions: CascaderOption[][] = []; // all data columns _cmColumns: CascaderOption[][] = []; _filterColumns: CascaderOption[][] = []; // 点击Document的事件(一般用于点击后隐藏菜单) private _clickOutsideHandler: () => void; private _touchOutsideHandler: () => void; private _delayTimer: any; // ngModel Access onChange: any = Function.prototype; onTouched: any = Function.prototype; /** Whether is disabled */ @Input() set cmDisabled(value: boolean) { this._disabled = toBoolean(value); } get cmDisabled(): boolean { return this._disabled; } /** Input size, one of `large` `default` `small` */ @Input() cmSize: 'large' | 'default' | 'small' = 'default'; /** Input placeholder */ @Input() set cmPlaceHolder(placeHolder: string) { this._cmPlaceHolder = placeHolder; } /** Whether show input box. Defaults to `true`. */ @Input() set cmShowInput(value: boolean) { this._showInput = toBoolean(value); } get cmShowInput(): boolean { return this._showInput; } /** Whether can search. Defaults to `false`. */ @Input() set cmShowSearch(value: boolean) { this._showSearch = toBoolean(value); } get cmShowSearch(): boolean { return this._showSearch; } /** Whether allow clear. Defaults to `true`. */ @Input() set cmAllowClear(value: boolean) { this._allowClear = toBoolean(value); } get cmAllowClear(): boolean { return this._allowClear; } /** Hover text for the clear icon */ @Input() cmClearText = 'Clear'; /** Whether to show arrow */ @Input() set cmShowArrow(value: boolean) { this._showArrow = toBoolean(value); } get cmShowArrow(): boolean { return this._showArrow; } /** Specify content to show when no result matches. */ @Input() cmNotFoundContent = 'Not Found'; /** Additional className of popup overlay */ @Input() cmPopupClassName: string; /** Additional className of popup overlay column */ @Input() cmColumnClassName: string; /** Options for first column, sub column will be load async */ @Input() cmOptions: CascaderOption[]; /** Whether cache children when they were loaded asych */ @Input() set cmEnableCache(value: boolean) { this._enableCache = toBoolean(value); } get cmEnableCache(): boolean { return this._enableCache; } /** Expand column item when click or hover, one of 'click' 'hover' */ @Input() cmExpandTrigger: CascaderExpandTrigger = 'click'; /** Change value on each selection if set to true */ @Input() set cmChangeOnSelect(value: boolean) { this._changeOnSelect = toBoolean(value); } get cmChangeOnSelect(): boolean { return this._changeOnSelect; } /** Change value on selection only if this function returns `true` */ @Input() cmChangeOn: (option: CascaderOption, level: number) => boolean; /** Delay time to show when mouse enter, when `cmExpandTrigger` is `hover`. */ @Input() cmMouseEnterDelay = 150; // ms /** Delay time to hide when mouse enter, when `cmExpandTrigger` is `hover`. */ @Input() cmMouseLeaveDelay = 150; // ms /** Triggering mode: can be Array<'click'|'hover'> */ @Input() cmTriggerAction: CascaderTriggerType | CascaderTriggerType[] = ['click']; /** Render function of displaying selected options */ @Input() cmDisplayRender: (label: string[], selectedOptions: CascaderOption[]) => string | TemplateRef; /** Render function of displaying selected options */ @Input() cmIntervalRender: (label: string[], selectedOptions: CascaderOption[]) => string | TemplateRef; /** Property name for getting `value` in the option */ @Input() cmValueProperty = 'value'; /** Property name for getting `label` in the option */ @Input() cmLabelProperty = 'label'; @ViewChild('menu') menu: ElementRef; @HostBinding('attr.tabIndex') tabIndex = '0'; /** Event: emit on popup show or hide */ @Output() cmVisibleChange = new EventEmitter(); /** Event: emit on values changed */ @Output() cmChange = new EventEmitter(); /** Event: emit on values and selection changed */ @Output() cmSelectionChange = new EventEmitter(); /** * Event: emit on option selected, event data:{option: any, index: number} */ @Output() cmSelect = new EventEmitter<{ option: CascaderOption, index: number }>(); /** * Event: emit on option unselected, event data:{option: any, index: number} */ @Output() cmUnSelect = new EventEmitter<{ option: CascaderOption, index: number }>(); /** * Event: emit before loading children. event data:{option: any|null, index: number, resolve, reject} */ @Output() cmLoad = new EventEmitter<{ option: CascaderOption, index: number, resolve(children: CascaderOption[]): void, reject(): void }>(); /** Event: emit on the clear button clicked */ @Output() cmClear = new EventEmitter(); onPositionChange(position: ConnectedOverlayPositionChange): void { const _position = position.connectionPair.originY === 'bottom' ? 'bottom' : 'top'; if (this._dropDownPosition !== _position) { this._dropDownPosition = _position; this._cdr.detectChanges(); } } cmFocus(): void { this._focused = true; this._addHostClass(`${this._prefixCls}-focused`); } cmBlur(): void { this._focused = false; this._removeHostClass(`${this._prefixCls}-focused`); } get _pickerLabelCls(): any { return { [`${this._prefixCls}-picker-label`]: true }; } get _arrowCls(): any { return { [`${this._prefixCls}-picker-arrow`] : true, [`${this._prefixCls}-picker-arrow-expand`]: this._popupVisible }; } get _clearCls(): any { return { [`${this._prefixCls}-picker-clear`]: true }; } get _inputCls(): any { return { [`${this._prefixCls}-input`] : 1, [`${this._inputPrefixCls}-disabled`]: this.cmDisabled, [`${this._inputPrefixCls}-lg`] : this.cmSize === 'large', [`${this._inputPrefixCls}-sm`] : this.cmSize === 'small', }; } get _menuCls(): any { return { [`${this._prefixCls}-menus`] : true, [`${this._prefixCls}-menus-hidden`]: !this._popupVisible, [`${this.cmPopupClassName}`] : this.cmPopupClassName }; } /** 获取菜单中列的样式 */ get _columnCls(): any { return { [`${this._prefixCls}-menu`] : true, [`${this.cmColumnClassName}`]: this.cmColumnClassName }; } /** 获取列中Option的样式 */ 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.isActiveOption(option, index), [`${this._prefixCls}-menu-item-disabled`]: option.disabled, [`${this._prefixCls}-menu-item-loading`] : option.loading }; } _getLabel(): string | TemplateRef { return this._displayLabelIsTemplate ? '' : this._displayLabel; } /** prevent input change event */ _handlerInputChange(event: Event): void { event.stopPropagation(); } /** input blur */ _handleInputBlur(event: Event): void { if (!this.cmShowSearch) { return; } if (this._popupVisible) { this.cmFocus(); } else { this.cmBlur(); } } /** input focus */ _handleInputFocus(event: Event): void { if (!this.cmShowSearch) { return; } this.cmFocus(); } /** input key down */ _handleInputKeyDown(event: KeyboardEvent): void { } setInputValue(inputValue: any, fireSearch: boolean = true): void { if (inputValue !== this._inputValue) { this._inputValue = inputValue; } } _hasInput(): boolean { return this._inputValue.length > 0; } _hasSelection(): boolean { return this._selectedOptions.length > 0; } /** Whether the clear button is visible */ get _showClearIcon(): boolean { const isSelected = this._hasSelection(); const isHasInput = this._hasInput(); return this.cmAllowClear && !this.cmDisabled && (isSelected || isHasInput); } get _displayRender(): (label: string[], selectedOptions: CascaderOption[]) => string | TemplateRef { return this.cmDisplayRender || defaultDisplayRender; } get _intervalRender(): (label: string[], selectedOptions: CascaderOption[]) => string | TemplateRef { return this.cmIntervalRender || defaultIntervalRender; } _buildDisplayLabel(): void { let labels: string[] = []; const selectedOptions = this._selectedOptions; let len = selectedOptions && selectedOptions.length || 0; for (let i = 0; i < len; i++) { let label = selectedOptions[i].map(o => o[this.cmLabelProperty || 'label']); labels[i] = this._intervalRender.call(this, label, selectedOptions[i]); } // 设置当前控件的显示值 this._displayLabel = this._displayRender.call(this, labels, selectedOptions); this._displayLabelIsTemplate = !(typeof this._displayLabel === 'string'); this._displayLabelContext = {labels, selectedOptions}; } /** 由用户来定义点击后是否变更 */ _isChangeOn(option: CascaderOption, index: number): boolean { if (typeof this.cmChangeOn === 'function') { return this.cmChangeOn(option, index) === true; } return false; } @HostListener('keydown', ['$event']) 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 !== ESC) { return; } // Press any keys above to reopen menu if (!this._isPopupVisible() && keyCode !== BACKSPACE && keyCode !== ESC) { this.setPopupVisible(true); return; } // Press ESC to close menu if (keyCode === ESC) { this.setPopupVisible(false); return; } if (this._isPopupVisible()) { event.preventDefault(); if (keyCode === DOWN_ARROW) { this._moveDown(); } if (keyCode === UP_ARROW) { this._moveUp(); } if (keyCode === LEFT_ARROW) { this._moveLeft(); } if (keyCode === RIGHT_ARROW) { this._moveRight(); } } } @HostListener('click', ['$event']) _onTriggerClick(event: MouseEvent): void { if (this.cmDisabled) { return; } this.onTouched(); // set your control to 'touched' if (this._isClickTiggerAction()) { this._delaySetPopupVisible(!this._popupVisible, 100); } } @HostListener('mouseenter', ['$event']) _onTriggerMouseEnter(event: MouseEvent): void { if (this.cmDisabled) { return; } if (this._isPointerTiggerAction()) { this._delaySetPopupVisible(true, this.cmMouseEnterDelay); } } @HostListener('mouseleave', ['$event']) _onTriggerMouseLeave(event: MouseEvent): void { if (this.cmDisabled) { return; } if (!this._isPopupVisible()) { return; } if (this._isPointerTiggerAction()) { const currEl = this._el; const popupEl = this.menu && this.menu.nativeElement as HTMLElement; if (currEl.contains(event.target as Node) || (popupEl && popupEl.contains(event.target as Node))) { return; // 还在菜单内部 } this._delaySetPopupVisible(false, this.cmMouseLeaveDelay); } } _isClickTiggerAction(): boolean { if (typeof this.cmTriggerAction === 'string') { return this.cmTriggerAction === 'click'; } return this.cmTriggerAction.indexOf('click') !== -1; } _isPointerTiggerAction(): boolean { if (typeof this.cmTriggerAction === 'string') { return this.cmTriggerAction === 'hover'; } return this.cmTriggerAction.indexOf('hover') !== -1; } closeMenu(): void { this._clearDelayTimer(); this.setPopupVisible(false); } /** * 显示或者隐藏菜单 * * @param visible true-显示,false-隐藏 * @param delay 延迟时间 */ _delaySetPopupVisible(visible: boolean, delay: number): void { this._clearDelayTimer(); if (delay) { this._delayTimer = setTimeout(() => { this.setPopupVisible(visible); this._clearDelayTimer(); }, delay); } else { this.setPopupVisible(visible); } } _isPopupVisible(): boolean { return this._popupVisible; } setPopupVisible(popupVisible: boolean): void { if (this.cmDisabled) { return; } if (this._popupVisible !== popupVisible) { this._popupVisible = popupVisible; // We must listen to `mousedown` or `touchstart`, edge case: // https://github.com/ant-design/ant-design/issues/5804 // https://github.com/react-component/calendar/issues/250 // https://github.com/react-component/trigger/issues/50 if (popupVisible) { if (!this._clickOutsideHandler) { this._clickOutsideHandler = this._render.listen('document', 'mousedown', this._onDocumentClick.bind(this)); } // always hide on mobile if (!this._touchOutsideHandler) { this._touchOutsideHandler = this._render.listen('document', 'touchstart', this._onDocumentClick.bind(this)); } } if (!popupVisible) { this._clearOutsideHandler(); } if (popupVisible) { this._beforeVisible(); } this.cmVisibleChange.emit(popupVisible); } } /** load init data if necessary */ _beforeVisible(): void { if (!this._cmColumns.length) { new Promise((resolve, reject) => { this.cmLoad.emit({option: null, index: -1, resolve, reject}); }).then((children: CascaderOption[]) => { this.setColumnData(children, 0); this.filterColumnData(); }, (reason: any) => { // should not be here }); } } _onDocumentClick(event: MouseEvent): void { const target = event.target as Node; const popupEl = this.menu && this.menu.nativeElement as HTMLElement; if (!this._el.contains(target) && !popupEl.contains(target)) { this.setPopupVisible(false); } } _clearOutsideHandler(): void { if (this._clickOutsideHandler) { this._clickOutsideHandler(); // Removes "listen" listener this._clickOutsideHandler = null; } if (this._touchOutsideHandler) { this._touchOutsideHandler(); // Removes "listen" listener this._touchOutsideHandler = null; } } _clearDelayTimer(): void { if (this._delayTimer) { clearTimeout(this._delayTimer); this._delayTimer = null; } } /** * press `up` or `down` arrow to select the sibling option. */ _moveUpOrDown(isUp: boolean): void { const columnIndex = Math.max(this._activatedOptions.length - 1, 0); // 该组中已经被激活的选项 const activeOption = this._activatedOptions[columnIndex]; // 该组所有的选项,用于遍历获取下一个被激活的选项 const options = this._cmColumns[columnIndex]; if (!options || !options.length) { return; } const length = options.length; let nextOptIndex = -1; if (!activeOption) { // 该列还没有选中的选项 nextOptIndex = isUp ? length : -1; } else { nextOptIndex = options.indexOf(activeOption); } while (true) { nextOptIndex = isUp ? nextOptIndex - 1 : nextOptIndex + 1; if (nextOptIndex < 0 || nextOptIndex >= length) { break; } const nextOption = options[nextOptIndex]; if (!nextOption || nextOption.disabled) { continue; } this.setActiveOption(nextOption, columnIndex); break; } } _moveUp(): void { this._moveUpOrDown(true); } _moveDown(): void { this._moveUpOrDown(false); } /** * press `left` arrow to remove the last selected option. * If there is no option selected, emit `cmClear` event. */ _moveLeft(): void { const options = this._selectedOptions; if (options.length) { options.pop(); // Remove the last one const len = options.length; if (len) { this.setActiveOption(options[len - 1], len - 1); } else { this.cmClear.emit(); } } } /** * press `right` arrow to select the next column option. */ _moveRight(): void { const columns = this._cmColumns; const length = this._selectedOptions.length; if (length === 0) { return; } const nextColIndex = length; const options = columns.length > nextColIndex ? columns[nextColIndex] : null; if (options) { // 存在`下级选项` const len = options.length; for (let i = 0; i < len; i++) { const activeOpt = options[i]; if (activeOpt && !activeOpt.disabled) { this.setActiveOption(activeOpt, nextColIndex); return; } } } } /** 获取Option的值,例如,可以指定labelProperty="name"来取Name */ getOptionLabel(option: CascaderOption): any { return option[this.cmLabelProperty || 'label']; } /** 获取Option的值,例如,可以指定valueProperty="id"来取ID */ getOptionValue(option: CascaderOption): any { return option[this.cmValueProperty || 'value']; } checkChange(event: any[], option: any) { } /** clear the input box and selected options */ clearSelection(event: Event): void { if (event) { event.preventDefault(); event.stopPropagation(); } this._displayLabel = ''; this._displayLabelIsTemplate = false; this._searchValue = []; this._selectedOptions = []; this._activatedOptions = []; this._cmColumns = this._filterColumns = []; this.setInputValue('', false); this.setPopupVisible(false); if (this.cmOptions && this.cmOptions.length) { this._cmColumns.push(cloneDeep(this.cmOptions)); } this._cmColumns.forEach((item: CascaderOption[], index: number) => { this._filterColumns[index] = item; }); // trigger change event this.onValueChange(); } onSearch(event: any, index: number) { if (!this._cmColumns || !this._cmColumns.length) { return; } this._filterColumns[index] = []; let len = this._cmColumns[index].length; for (let i = 0; i < len; i++) { if (-1 == this._cmColumns[index][i].label.indexOf(event)) { continue; } this._filterColumns[index].push(this._cmColumns[index][i]); } } filterColumnData() { this._filterColumns = []; for (let i = 0; i < this._cmColumns.length; i++) { if (this._searchValue[i]) { this.onSearch(this._searchValue[i], i); } else { this._filterColumns[i] = this._cmColumns[i]; } } } isActiveOption(option: CascaderOption, index: number): boolean { const activeOpt = this._activatedOptions[index]; if (activeOpt === option) { return true; } if (activeOpt && this.getOptionValue(activeOpt) === this.getOptionValue(option)) { return true; } return false; } setColumnData(options: CascaderOption[], index: number): void { if (!arrayEquals(this._cmColumns[index], options)) { if (!this._cmColumns[index]) { this._cmColumns[index] = [].concat(options); } else { this._cmColumns[index] = this._cmColumns[index].concat(options); } } } selectOption(option: CascaderOption, index: number): void { // trigger `cmSelect` event this.cmSelect.emit({option, index}); // load children directly if (option.children && option.children.length) { option.isLeaf = false; option.children.forEach(child => { child.parent = option; child.checked = false; }); this.setColumnData(option.children, index + 1); } else if (!option.isLeaf) { // load children async new Promise((resolve, reject) => { this.cmLoad.emit({option, index, resolve, reject}); }).then((children: CascaderOption[]) => { children.forEach(child => { child.parent = option; child.checked = false; }); this.setColumnData(children, index + 1); if (this.cmEnableCache) { option.children = children; // next time we load children directly } }, (reason: any) => { option.isLeaf = true; }); } else { // clicking leaf node will't remove any children columns /*if (index < this._cmColumns.length - 1) { this._cmColumns = this._cmColumns.slice(0, index + 1); }*/ } } addActiveOption(option: CascaderOption, index: number): void { if (!option || option.disabled) { return; } let node = cloneDeep(option); delete node.checked; if (!this._activatedOptions[index]) { this._activatedOptions[index] = []; } this._activatedOptions[index].push(node); // 当直接选择最后一级时,前面的选项要补全。例如,选择“城市”,则自动补全“国家”、“省份” node = option; for (let i = index - 1; i >= 0; i--) { if (!this._activatedOptions[i]) { this._activatedOptions[i] = []; } if (!this._activatedOptions[i].length) { node.parent.checked = true; let pnode = cloneDeep(node.parent); delete pnode.checked; this._activatedOptions[i].push(pnode); } else { let tag = false; let currentlen = this._activatedOptions[i].length; for (let j = 0; j < currentlen; j++) { if (this._activatedOptions[i][j].value == node.parent.value) { tag = true; break; } } if (!tag) { node.parent.checked = true; let pnode = cloneDeep(node.parent); delete pnode.checked; this._activatedOptions[i].push(pnode); } } node = node.parent; } } resetColumnData(option: CascaderOption[], index: number): void { if (!option) { return; } let len = this._cmColumns[index] && this._cmColumns[index].length || 0; for (let i = 0; i < option.length; i++) { for (let j = 0; j < len; j++) { if (option[i].value == this._cmColumns[index][j].value) { this.resetColumnData(option[i].children, index + 1); this._cmColumns[index].splice(j, 1); break; } } } if (this._cmColumns[index] && !this._cmColumns[index].length) { this._cmColumns.splice(index, 1); } } unselectOption(option: CascaderOption, index: number): void { // trigger `cmUnSelect` event this.cmUnSelect.emit({option, index}); if (option.children && option.children.length) { this.resetColumnData(option.children, index + 1); } } delActiveOption(option: CascaderOption, index: number): void { if (!option || option.disabled) { return; } let len = this._activatedOptions[index] && this._activatedOptions[index].length || 0; // 截断多余的选项,如选择“省份”,则只会有“国家”、“省份”,去掉“城市”、“区县” if (index < this._activatedOptions.length - 1) { if (1 == len) { this._activatedOptions = this._activatedOptions.slice(0, index); } else { for (let i = 0; i < len; i++) { if (option.value == this._activatedOptions[index][i].value) { if (option.children && option.children.length) { for (let j = 0; j < option.children.length; j++) { this.delActiveOption(option.children[j], index + 1); } } this._activatedOptions[index].splice(i, 1); break; } } } } else { for (let i = 0; i < len; i++) { if (option.value == this._activatedOptions[index][i].value) { this._activatedOptions[index].splice(i, 1); break; } } } if (this._activatedOptions[index] && !this._activatedOptions[index].length) { this._activatedOptions.splice(index, 1); } } /** * 设置某列的激活的菜单选项 * * @param option 菜单选项 * @param index 选项所在的列组的索引 */ setActiveOption(option: CascaderOption, index: number): void { if (!option || option.disabled) { return; } if (!this._activatedOptions[index]) { this._activatedOptions[index] = []; } if (option.checked) { // trigger select event, and display label this.selectOption(option, index); this.addActiveOption(option, index); } else { // trigger select event, and display label this.unselectOption(option, index); this.delActiveOption(option, index); } this.filterColumnData(); // 生成显示 if (option.isLeaf || this.cmChangeOnSelect || this._isChangeOn(option, index)) { this._selectedOptions = this._activatedOptions; // 设置当前控件的显示值 this._buildDisplayLabel(); // 触发变更事件 this.onValueChange(); } } /** * 鼠标点击选项 * * @param option 菜单选项 * @param index 选项所在的列组的索引 * @param event 鼠标事件 */ onOptionClick(option: CascaderOption, index: number, event: Event): void { event.preventDefault(); // Keep focused state for keyboard support this._el.focus(); if (option && option.disabled) { return; } option.checked = !option.checked; this.setActiveOption(option, index); } /** * 鼠标划入选项 * * @param option 菜单选项 * @param index 选项所在的列组的索引 * @param event 鼠标事件 */ onOptionMouseEnter(option: CascaderOption, index: number, event: Event): void { event.preventDefault(); if (this.cmExpandTrigger === 'hover' && !option.isLeaf) { this.delaySelect(option, index, true); } } /** * 鼠标划出选项 * * @param option 菜单选项 * @param index 选项所在的列组的索引 * @param event 鼠标事件 */ onOptionMouseLeave(option: CascaderOption, index: number, event: Event): void { event.preventDefault(); if (this.cmExpandTrigger === 'hover' && !option.isLeaf) { this.delaySelect(option, index, false); } } delaySelect(option: CascaderOption, index: number, doSelect: boolean): void { if (this._delayTimer) { clearTimeout(this._delayTimer); this._delayTimer = null; } if (doSelect) { this._delayTimer = setTimeout(() => { this.setActiveOption(option, index); this._delayTimer = null; }, 150); } } handleTreeData(option: CascaderOption, index: number) { delete option.checked; if (!option.children || !option.children.length) { return; } let child = this._selectedOptions[index+1]; if (child && child.length) { if (option.children && option.children.length) { for (let i = 0; i < option.children.length; i++) { let find = false; for (let j = 0; j < child.length; j++) { if (child[j].value == option.children[i].value) { delete option.children[i].checked; delete option.children[i].parent; this.handleTreeData(option.children[i], index+1); find = true; break; } } if (!find) { option.children.splice(i, 1); i--; } } } } else { option.children = []; } } getSubmitValue(): any[] { let index: number = 0; if (!this._selectedOptions || !this._selectedOptions.length) { return []; } const values: CascaderOption[] = cloneDeep(this._selectedOptions[index]); let len = this._selectedOptions[index].length; for (let i = 0; i < len; i++) { let node = values[i]; this.handleTreeData(node, index); } return values; } onValueChange(): void { const value = this.getSubmitValue(); if (!arrayEquals(this._lastValue, value)) { this._lastValue = value; this.onChange(value); // Angular need this if (value.length === 0) { this.cmClear.emit(); // first trigger `clear` and then `change` } this.cmSelectionChange.emit(this._selectedOptions); this.cmChange.emit(value); } } constructor( private _elementRef: ElementRef, private _render: Renderer2, private _cdr: ChangeDetectorRef ) { this._el = this._elementRef.nativeElement; } _addHostClass(classname: string): void { this._render.addClass(this._el, classname); } _removeHostClass(classname: string): void { this._render.removeClass(this._el, classname); } writeColumns(array: CascaderOption[], index: number) { if (!array || !array.length) { return; } for (let i = 0; i < array.length; i++) { let find = false; let len = this._cmColumns[index] && this._cmColumns[index].length || 0; for (let j = 0; j < len; j++) { if (array[i].value == this._cmColumns[index][j].value) { this._cmColumns[index][j].checked = true; this.writeColumns(array[i].children, index+1); find = true; break; } } if (!find) { if (!this._cmColumns[index]) { this._cmColumns[index] = []; } let node = cloneDeep(array[i]); node.checked = true; this._cmColumns[index].push(node); this.writeColumns(array[i].children, index+1); } } } writeSelected(array: CascaderOption[], index: number) { if (!array || !array.length) { return; } if (!this._selectedOptions[index]) { this._selectedOptions[index] = []; } this._selectedOptions[index] = this._selectedOptions[index].concat(array); for (let i = 0; i < array.length; i++) { if (array[i].children && array[i].children.length) { this.writeSelected(array[i].children, index+1) } } } /** * Write a new value to the element. * * @Override (From ControlValueAccessor interface) */ writeValue(value: any): void { const array: any[] = []; toArray(value).forEach((v: any, index: number) => { if (typeof v !== 'object') { const obj = {}; obj[this.cmValueProperty] = v; obj[this.cmLabelProperty] = v; array[index] = obj; } else { array[index] = v; } }); this.writeSelected(array, 0); this._activatedOptions = this._selectedOptions; this.writeColumns(array, 0); this.filterColumnData(); this._buildDisplayLabel(); } registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; } registerOnTouched(fn: () => {}): void { this.onTouched = fn; } setDisabledState(isDisabled: boolean): void { if (isDisabled) { this.closeMenu(); this._addHostClass(`${this._prefixCls}-picker-disabled`); } else { this._removeHostClass(`${this._prefixCls}-picker-disabled`); } this.cmDisabled = isDisabled; } ngOnInit(): void { // 设置第一列 if (this.cmOptions && this.cmOptions.length) { this._cmColumns.push(cloneDeep(this.cmOptions)); } this._cmColumns.forEach((item: CascaderOption[], index: number) => { this._filterColumns[index] = item; }); } ngOnDestroy(): void { if (this._delayTimer) { clearTimeout(this._delayTimer); this._delayTimer = null; } } ngOnChanges(changes: SimpleChanges): void { const cmDisabled = changes.cmDisabled; if (cmDisabled) { if (cmDisabled.currentValue) { this._addHostClass(`${this._prefixCls}-picker-disabled`); } else { this._removeHostClass(`${this._prefixCls}-picker-disabled`); } } const cmOptions = changes.cmOptions; if (cmOptions && !cmOptions.isFirstChange()) { this._cmColumns.splice(0); const newOptions: CascaderOption[] = cmOptions.currentValue; if (newOptions && newOptions.length) { this._cmColumns.push(newOptions); this.clearSelection(null); } } this.filterColumnData(); } ngAfterViewInit(): void { this._addHostClass(this._prefixCls); this._addHostClass(`${this._prefixCls}-picker`); } }