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'; const cloneDeep = require('lodash.clonedeep'); 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; } export interface FilterOption { value?: string; label?: string; title?: string; advanced?: boolean; disabled?: boolean; sort?: FilterOption; items?: FilterOption[]; [key: string]: any; } @Component({ encapsulation: ViewEncapsulation.None, selector: 'cm-advancedfilter', templateUrl: './advancedfilter.component.html', styleUrls: ['./advancedfilter.component.less'], providers: [ { provide : NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AdvancedFilterComponent), multi : true } ] }) export class AdvancedFilterComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit, ControlValueAccessor { _el: HTMLElement; _prefixCls = 'cm-filter'; _popupVisible = false; // selection will trigger value change _selectedOptions: any; // activaction will not trigger value change _activatedOptions: any; _options: FilterOption[] = []; // 点击Document的事件(一般用于点击后隐藏菜单) private _clickOutsideHandler: () => void; private _touchOutsideHandler: () => void; private _delayTimer: any; // ngModel Access onChange: any = Function.prototype; onTouched: any = Function.prototype; /** 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: FilterOption[]; /** Change value on selection only if this function returns `true` */ @Input() cmChangeOn: (option: FilterOption, level: number) => boolean; /** 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 option selected, event data:{option: any, index: number} */ @Output() cmSelect = new EventEmitter<{ option: FilterOption, index: number }>(); /** * Event: emit on option unselected, event data:{option: any, index: number} */ @Output() cmUnSelect = new EventEmitter<{ option: FilterOption, index: number }>(); /** Event: emit on the clear button clicked */ @Output() cmClear = new EventEmitter(); getArrowCls(node: FilterOption): any { return { [`${this._prefixCls}-picker-arrow`] : true, [`${this._prefixCls}-picker-arrow-expand`]: (node.expand || node.visible) }; } /** 获取菜单中分类列表的样式 */ get _classifyCls(): any { return { [`${this._prefixCls}-classify`] : true, [`${this.cmColumnClassName}`]: this.cmColumnClassName, [`${this._prefixCls}-edge`] : true, [`clearfix`] : true }; } getPanelCls(node: FilterOption): any { return { [`${this._prefixCls}-menus`] : true, [`${this._prefixCls}-menus-hidden`]: !node.visible, [`${this.cmColumnClassName}`]: this.cmColumnClassName, [`dropdownlist-content`] : true, [`${this._prefixCls}-tagwrap`] : !node.advanced, [`multi-mode`] : false, }; } getMenuCls(node: FilterOption): any { return { [`${this._prefixCls}-menuwrap`] : true, [`${this.cmPopupClassName}`] : this.cmPopupClassName, [`line1height`] : node.showOneLine && !node.expand && !node.multi, [`line2height`] : !node.showOneLine && !node.expand && !node.multi, [`autoheight`] : node.advanced || node.expand || node.multi, [`multi-mode`] : node.multi }; } getButtonCls(childNode?: FilterOption): any { return { [`${this._prefixCls}-button`] : true, [`button-group `] : true, [`visible`] : (!childNode || childNode.visible) }; } getSaveButtonCls(node: FilterOption): any { return { [`saveMultiMode`] : true, [`btn `] : true, [`save`] : true, [`active`] : node.checked }; } get _operationCls(): any { return { [`${this._prefixCls}-operational`] : true, [`morewrap`] : true, }; } getItemCls(node: FilterOption): any { return { [`${this._prefixCls}-menus`] : true, [`${this.cmPopupClassName}`] : this.cmPopupClassName, [`brands`] : !node.advanced, [`all`] : true, [`brands-cate`] : !node.showOneLine && !node.advanced, [`attr-items`] : node.advanced, }; } /** 获取列中Option的样式 */ getOptionCls(node: FilterOption, option: FilterOption, index: number): any { return { [`${this._prefixCls}-menu-item`] : true, [`${this._prefixCls}-menu-item-expand`] : !node.isLeaf, [`${this._prefixCls}-menu-item-active`] : node.visible, [`${this._prefixCls}-menu-item-seleted`] : !node.advanced && option.checked, [`${this._prefixCls}-multiItem`] : true, [`list-item `] : node.visible, [`dditem`] : node.advanced, [`active`] : option.visible, }; } /** 获取Option的值,例如,可以指定labelProperty="name"来取Name */ getOptionLabel(option: FilterOption): any { return option[this.cmLabelProperty || 'label']; } /** 获取Option的值,例如,可以指定valueProperty="id"来取ID */ getOptionValue(option: FilterOption): any { return option[this.cmValueProperty || 'value']; } /** 由用户来定义点击后是否变更 */ _isChangeOn(option: FilterOption, index: number): boolean { if (typeof this.cmChangeOn === 'function') { return this.cmChangeOn(option, index) === true; } return false; } @HostListener('click', ['$event']) _onTriggerClick(event: MouseEvent): void { this.onTouched(); // set your control to 'touched' const popupEl = this.menu && this.menu.nativeElement as HTMLElement; if (popupEl && popupEl.contains(event.target as Node)) { return; // 还在菜单内部 } } @HostListener('mouseleave', ['$event']) _onTriggerMouseLeave(event: MouseEvent): void { //const currEl = this._el; const popupEl = this.menu && this.menu.nativeElement as HTMLElement; if (popupEl && popupEl.contains(event.target as Node)) { return; // 还在菜单内部 } } closeMenu(): void { this._clearDelayTimer(); this.setPopupVisible(false); } setPopupVisible(popupVisible: boolean): void { if (this._popupVisible !== popupVisible) { this._popupVisible = popupVisible; 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(); } this.cmVisibleChange.emit(popupVisible); } } _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; } } onExpandClick(option: FilterOption, index: number, event: Event): void { event.preventDefault(); option.expand = !option.expand; } onMultiClick(option: FilterOption, index: number, event: Event): void { event.preventDefault(); event.stopPropagation(); option.multi = !option.multi; } /** * 设置某列的激活的菜单选项 * * @param option 菜单选项 * @param index 选项所在的列组的索引 */ setActiveOption(node: FilterOption, option: FilterOption, index: number): void { if (!node || !option) { return; } if (!this._activatedOptions) { this._activatedOptions = {}; } if (option.checked) { this.cmSelect.emit({option, index}); if (node.advanced && node.items && node.items[index]) { node.items[index].checked = true; if (!this._activatedOptions[node.items[index].value]) { this._activatedOptions[node.items[index].value] = []; } if (node.multi) { this._activatedOptions[node.items[index].value].push(option.value); } else { for (let i = 0; i < node.items[index].items.length; i++) { if (node.items[index].items[i].value != option.value) { node.items[index].items[i].checked = false; } } this._activatedOptions[node.items[index].value] = [option.value]; } } else { node.checked = true; if (!this._activatedOptions[node.value]) { this._activatedOptions[node.value] = []; } if (node.multi) { this._activatedOptions[node.value].push(option.value); } else { for (let i = 0; i < node.items.length; i++) { if (node.items[i].value != option.value) { node.items[i].checked = false; } } this._activatedOptions[node.value] = [option.value]; } } } else { this.cmUnSelect.emit({option, index}); if (node.advanced && node.items && node.items[index]) { if (node.multi) { if (this._activatedOptions[node.items[index].value]) { let len = this._activatedOptions[node.items[index].value].length; for (let i = 0; i < len; i++) { if (option.value == this._activatedOptions[node.items[index].value][i]) { this._activatedOptions[node.items[index].value].splice(i, 1); break; } } if (this._activatedOptions[node.items[index].value] && !this._activatedOptions[node.items[index].value].length) { node.items[index].checked = false; delete this._activatedOptions[node.items[index].value]; } } } else { node.items[index].checked = false; delete this._activatedOptions[node.items[index].value]; } } else { if (node.multi) { if (this._activatedOptions[node.value]) { let len = this._activatedOptions[node.value].length; for (let i = 0; i < len; i++) { if (option.value == this._activatedOptions[node.value][i]) { this._activatedOptions[node.value].splice(i, 1); break; } } if (!this._activatedOptions[node.value].length) { node.checked = false; delete this._activatedOptions[node.value]; } } } else { node.checked = false; delete this._activatedOptions[node.value]; } } } this._isChangeOn(option, index); if (!node.multi) { this._selectedOptions = cloneDeep(this._activatedOptions); // 触发变更事件 this.onValueChange(); } } /** * 鼠标点击选项 * * @param option 菜单选项 * @param index 选项所在的列组的索引 * @param event 鼠标事件 */ onOptionClick(node: FilterOption, option: FilterOption, index: number, event: Event): void { event.preventDefault(); event.stopPropagation(); if (option && option.disabled) { return; } option.checked = !option.checked; this.setActiveOption(node, option, index); } onSelectClick(node: FilterOption, index: number, event: Event): void { event.preventDefault(); event.stopPropagation(); if (!this._activatedOptions) { return; } if (node.advanced && node.items && node.items[index]) { if (this._activatedOptions[node.items[index].value]) { this._selectedOptions[node.items[index].value] = this._activatedOptions[node.items[index].value]; // 触发变更事件 this.onValueChange(); } } else { if (this._activatedOptions[node.value]) { this._selectedOptions[node.value] = this._activatedOptions[node.value]; // 触发变更事件 this.onValueChange(); } } } onCancelClick(node: FilterOption, index: number, event: Event): void { event.preventDefault(); event.stopPropagation(); node.multi = !node.multi; if (node.advanced && node.items && node.items[index]) { let activatedOptions = this._selectedOptions && this._selectedOptions[node.items[index].value] || []; let len = node.items[index].items && node.items[index].items.length || 0; for (let i = 0; i < len; i++) { let tag = false; for (let j = 0; j < activatedOptions.length; j++) { if (node.items[index].items[i].value == activatedOptions[j]) { tag = true; break; } } node.items[index].items[i].checked = tag; } } else { let activatedOptions = this._selectedOptions && this._selectedOptions[node.value] || []; for (let i = 0; i < node.items.length; i++) { let tag = false; for (let j = 0; j < activatedOptions.length; j++) { if (node.items[i].value == activatedOptions[j]) { tag = true; break; } } node.items[i].checked = tag; } } } delayInvisible(option: FilterOption, index: number): void { this._clearDelayTimer(); this._delayTimer = setTimeout(() => { option.visible = false; this._delayTimer = null; }, 500); } /** * 鼠标划入选项 * * @param option 菜单选项 * @param index 选项所在的列组的索引 * @param event 鼠标事件 */ onOptionMouseEnter(node: FilterOption, option: FilterOption, index: number, event: Event): void { event.preventDefault(); if (node.advanced) { for (let i = 0; i < node.items.length; i++) { node.items[i].visible = false; } option.visible = true; } } /** * 鼠标划出选项 * * @param option 菜单选项 * @param index 选项所在的列组的索引 * @param event 鼠标事件 */ onOptionMouseLeave(node: FilterOption, option: FilterOption, index: number, event: Event): void { event.preventDefault(); if (node.advanced) { this.delayInvisible(option, index); } } /** * 鼠标划入选项 * * @param panel 菜单选项 * @param index 选项所在的列组的索引 * @param event 鼠标事件 */ onPanelMouseEnter(option: FilterOption, index: number, event: Event): void { event.preventDefault(); this._clearDelayTimer(); option.visible = true; } /** * 鼠标划出选项 * * @param panel 菜单选项 * @param index 选项所在的列组的索引 * @param event 鼠标事件 */ onPanelMouseLeave(option: FilterOption, index: number, event: Event): void { event.preventDefault(); this.delayInvisible(option, index); } onValueChange(): void { this.onChange(this._selectedOptions); // Angular need this this.cmChange.emit(this._selectedOptions); } 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); } /** * 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._activatedOptions = this._selectedOptions; } registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; } registerOnTouched(fn: () => {}): void { this.onTouched = fn; } ngOnInit(): void { if (this.cmOptions && this.cmOptions.length) { this._options = this.cmOptions; } } ngOnDestroy(): void { this._clearDelayTimer(); } ngOnChanges(changes: SimpleChanges): void { const cmOptions = changes.cmOptions; if (cmOptions && !cmOptions.isFirstChange()) { this._options = []; const newOptions: FilterOption[] = cmOptions.currentValue; if (newOptions && newOptions.length) { this._options.push(newOptions); } } } ngAfterViewInit(): void { this._addHostClass(this._prefixCls); this._addHostClass(`${this._prefixCls}-picker`); } }