import { cloneDeep } from 'lodash-es'; /* * @Author: 疯狂秀才(lucas huang) * @Date: 2018-10-27 10:55:02 * @LastEditors: 疯狂秀才(Lucas Huang) * @LastEditTime: 2019-10-16 14:14:14 * @Company: Inspur * @Version: v0.0.1 */ import { Component, OnInit, Input, ViewChild, Output, EventEmitter, TemplateRef, ContentChildren, QueryList, AfterContentInit, AfterViewInit, OnChanges, SimpleChanges, Injector, ChangeDetectorRef, ElementRef, Renderer2 } from '@angular/core'; import { MessagerService } from '@farris/ui-messager'; import { TreeTableComponent, TreeNode } from '@farris/ui-treetable'; import { CommonUtils, FarrisTemplateDirective } from '@farris/ui-common'; import { LocaleService } from '@farris/ui-locale'; @Component({ selector: 'multi-select', templateUrl: './multi-select.component.html', styleUrls: [ './multi-select.css' ] }) export class MultiSelectComponent implements OnInit, OnChanges, AfterViewInit, AfterContentInit { private allTreeNodes = []; private _ds: any = []; get dataSource() { return this._ds; } @Input() set dataSource(val: any) { val = val || []; this._ds = val; if (!this.searchValue) { if (val.length) { if (val[0].toJSON) { this.copyDataSource = val.map(n => n.toJSON()); } else { this.copyDataSource = cloneDeep(val); } } this.initRightData(this.selectedId); } } @Input() displayType: 'List' | 'Tree' = 'List'; /** 显示右侧上移下移按钮 */ @Input() enableTargetSort = true; @Input() idField: string; @Input() textField: string; @Input() valueField: string; @Input() height = 445; @Input() fit = true; @Input() noDataMoveMessage = '请选择要移动的数据。'; @Input() leftTitle = '未选择'; @Input() rightTitle = '已选择'; @Input() selectedId: any; @Input() enableSearch = true; /** 目标数据源显示checkbox 允许多选 */ @Input() showCheckbox = false; @Output() dataChange = new EventEmitter(); @Output() selected = new EventEmitter(); @Output() selectedIdChange = new EventEmitter(); @Output() beforeMove = new EventEmitter(); @Output() afterMove = new EventEmitter(); @Output() sortMove = new EventEmitter(); @ContentChildren(FarrisTemplateDirective) templates: QueryList; @ViewChild('tt') tt: TreeTableComponent; // 右侧选中的数据 { [keyVal]: obj, [keyVal]: obj ... } _rightCheckeds: any; // 左侧选中的数据 { [keyVal]: obj, [keyVal]: obj ... } _leftCheckeds: any = {}; // 右侧数据 _rightData = []; get selections() { return this._rightData; } set selections(val: any) { this._rightData = val; this.cd.detectChanges(); } get searchValue() { return this._searchValue; } set searchValue(value: string) { this._searchValue = value; // this.copyDataSource = [...this.dataSource]; if (value === undefined || value === null || value === '') { this.dataSource = this.copyDataSource; return; } if (this.displayType === 'Tree') { const newDataSource = this.getTreeDataBySearch(this._searchValue, this.allTreeNodes); this.tt.loadData(newDataSource); } else { this.dataSource = this.getListDataBySearch(this._searchValue, this.copyDataSource); } } canCheckLength = 0; leftCount = 0; _searchValue = ''; // 复制一份数据源 private copyDataSource: any; // 树形结构的数据length treeDataLength: number; displayTemplate: TemplateRef; emptyData; el: ElementRef; render: Renderer2; localeService: LocaleService; commonUtils: CommonUtils; constructor(private mesagerSer: MessagerService, private injector: Injector, private cd: ChangeDetectorRef) { if (!Object.values) { Object.values = function (obj) { return Object.keys(obj).map(function (e) { return obj[e]; }); }; } this.el = this.injector.get(ElementRef); this.render = this.injector.get(Renderer2); this.localeService = this.injector.get(LocaleService); this.commonUtils = this.injector.get(CommonUtils, null); } ngOnInit() { // this.copyDataSource = this.dataSource ? [...this.dataSource] : []; this.emptyData = this.localeService.getValue('multiSelect.emptyData') || '暂无数据'; this.initRightData(this.selectedId); this.moveItem(); } ngAfterViewInit() { const elt = this.el.nativeElement; this.render.addClass(elt, 'row'); this.render.addClass(elt, 'f-multi-select'); if (this.fit === true) { this.render.addClass(elt, 'f-utils-fill'); } if (!!this.height) { this.render.setStyle(elt, 'height', this.height + 'px'); } this.noDataMoveMessage = this.noDataMoveMessage === '请选择要移动的数据。' ? this.localeService.getValue('multiSelect.noDataMoveMessage') : this.noDataMoveMessage; if (this.tt && this.selectedId) { this.tt.checkedNodes(this.selectedId); } } ngAfterContentInit() { this.templates.forEach((item) => { switch (item.getType()) { case 'text': this.displayTemplate = item.template; break; } }); } ngOnChanges(changes: SimpleChanges) { if (changes.dataSource && !changes.dataSource.isFirstChange()) { // this.copyDataSource = [...(this.dataSource || [])]; this._leftCheckeds = {}; this._rightData = []; } if (changes.selectedId && !changes.selectedId.isFirstChange()) { this.initRightData(changes.selectedId.currentValue); } } getCanCheckLength() { let n = 0; let total = 0; if (this.isTree()) { this.tt.serializedValue.forEach(rn => { if (rn.node.selectable) { if (this.canChecked(rn.node)) { n++; } } }); total = this.tt.serializedValue.length; } else { this.dataSource.forEach(d => { if (this.canChecked(d)) { n++; } }); total = this.dataSource.length; } this.leftCount = total; return n; } /** * 获取搜索的数据,List结构 * @param value 搜索的字符 * @param data 数据 */ private getListDataBySearch(value: string, data: any[]): any[] { const result = []; value = '' + value; if (data instanceof Array) { data.forEach(item => { let txt = ''; let val = ''; if (this.textField) { txt = '' + this.getText(item); } if (this.valueField) { val = '' + this.getValue(item); } if (this.textField && this.valueField) { if (txt.indexOf(value) !== -1 || val.indexOf(value) !== -1) { result.push(item); } } else if (this.textField && !this.valueField) { if (txt.indexOf(value) !== -1) { result.push(item); } } else if (!this.textField && this.valueField) { if (val.indexOf(value) !== -1) { result.push(item); } } }); } return result; } /** * 获取包含value值得节点 */ private getTreeDataByRecursive(value: string, data: any[], newData: any[]) { value = '' + value; data.forEach(item => { let txt = ''; let val = ''; if (this.textField) { txt = '' + this.getText(item.data); } if (this.valueField) { val = '' + this.getValue(item.data); } if (this.textField && this.valueField) { if (txt.indexOf(value) !== -1 || val.indexOf(value) !== -1) { newData.push(item); } } else if (this.textField && !this.valueField) { if (txt.indexOf(value) !== -1) { newData.push(item); } } else if (!this.textField && this.valueField) { if (val.indexOf(value) !== -1) { newData.push(item); } } if (item.children && item.children.length) { this.getTreeDataByRecursive(value, item.children, newData); } }); } /** * 获取根节点 */ private getTreeRootNode(data: any[], rootData: any[]) { data.forEach(item => { if (item.parent === null) { rootData.push(item); } else { this.getTreeRootNode(new Array(item.parent), rootData); } }); } /** * 获取搜索的数据,Tree结构 * @param value 搜索的字符 * @param data 数据 */ private getTreeDataBySearch(value: string, data: any[]): any[] { const rootNodes = []; const nodes = []; const finalRes = []; const uniqueId = []; // 获取包含value值得节点 this.getTreeDataByRecursive(value, data, nodes); // 获取根节点 this.getTreeRootNode(nodes, rootNodes); // 去重 rootNodes.forEach(node => { if (!uniqueId.some(item => item == node.id)) { finalRes.push(node); uniqueId.push(node.id); } }); return finalRes; } private initRightData(ids) { let dataSource: any = []; if (ids && ids.length) { if (this.isTree()) { this.convertTreeDataToPlainData(this.copyDataSource, dataSource); this._rightData = this.getTreeSelectionsById(ids, dataSource); } else { this._rightData = this.getListSelectionsById(ids, this.copyDataSource); } this._leftCheckeds = ids.reduce((r, id) => { r[id] = this._rightData.find(n => n[this.idField] == id); return r; }, {}); } setTimeout(() => { this.canCheckLength = this.getCanCheckLength(); if (this.tt) { this.tt.checkedNodes(ids, true, false, true); this.allTreeNodes = cloneDeep(this.tt.state.rowNodes.map(n => n.node)); } }); } /** * @param ids 选中数据的id * @param dataSource 原始数据 */ private getListSelectionsById(ids: any, dataSource: any) { let result = []; const _this = this; if ((typeof ids === 'string' && !!ids) || typeof ids === 'number') { const reusltObj = dataSource.find(item => { return item[_this.idField] === ids; }); if (reusltObj) { result.push(reusltObj); } } else if (Array.isArray(ids)) { if (Array.isArray(dataSource)) { result = ids.map(id => dataSource.find(item => item[this.idField] === id)).filter(n => n); } else { result = []; } } else { result = []; } return result; } /** * * @param ids 选中数据的id * @param dataSource 原始数据 */ private getTreeSelectionsById(ids: any, dataSource: any) { let result = []; const _this = this; if ((typeof ids === 'string' && !!ids) || typeof ids === 'number') { const reusltObj = dataSource.find(item => { return item.data[_this.idField] === ids; }); if (reusltObj && reusltObj.data) { result.push(reusltObj.data); } } else if (Array.isArray(ids)) { if (Array.isArray(dataSource)) { result = ids.map(id => { return dataSource.find(item => item.data[this.idField] === id); }).filter(n => n).map(n => n.data); } else { result = []; } } else { result = []; } return result; } /** * 判断是否选中 * @param data 要验证的数据 * @param direction 验证的数据所在的位置 L: 左侧,R: 右侧 */ isChecked(data: any, direction: 'L' | 'R'): boolean { const keyVal = data[this.idField]; if (direction === 'L') { return (this._leftCheckeds && this._leftCheckeds[keyVal] !== undefined) || !this.canChecked(data); } else { if (this._rightCheckeds) { return this._rightCheckeds[keyVal] !== undefined; } else { return false; } } } canChecked(item: any) { let data = item; if (this.isTree()) { data = item.data; if (item.selectable !== undefined && !item.selectable) { return false; } } if (!this.selections || this.selections.length === 0) { return true; } return this.selections.findIndex(c => c[this.idField] === data[this.idField]) === -1; } isTree(): any { return this.displayType === 'Tree'; } leftDataChecked(data: any[]) { if (data && data.length) { data.forEach(node => { let d = node; if (this.isTree()) { d = node.data; } if (this.canChecked(node)) { if (this.isChecked(d, 'L')) { delete this._leftCheckeds[d[this.idField]]; } else { if (!this._leftCheckeds) { this._leftCheckeds = {}; } this._leftCheckeds[d[this.idField]] = d; } } }); } } hasCheckeds(dir: 'L' | 'R') { if (dir === 'L') { if (this.isTree()) { return this.tt.checkeds && this.tt.checkeds.length; } if (this._leftCheckeds) { return Object.keys(this._leftCheckeds).length > 0; } else { return false; } } else { if (this._rightCheckeds) { return Object.keys(this._rightCheckeds).length > 0; } else { return false; } } } rightDataChecked(item, emit = true) { const isCheck = this.isChecked(item, 'R'); if (isCheck) { if (this.showCheckbox) { delete this._rightCheckeds[item[this.idField]]; } else { this._rightCheckeds = {}; } return; } else { if (this.showCheckbox) { this._rightCheckeds = this._rightCheckeds || {}; } else { this._rightCheckeds = {}; } this._rightCheckeds[item[this.idField]] = item; } this.cd.detectChanges(); this.selected.emit({ selected: !isCheck, data: item }); } selectItem(id, emit = true) { if (this.selections && this.selections.length) { const item = this.selections.find(n => n[this.idField] === id); if (item) { this.rightDataChecked(item, emit); } } } private clearSelections() { } /** * 将树结构数据转换为扁平数组 * @param nodes 树节点数组 * @param r 新的空数组 */ private convertTreeDataToPlainData(nodes: TreeNode[], r: any[] = []) { if (nodes) { nodes.forEach(n => { r.push(n); if (n.children) { this.convertTreeDataToPlainData(n.children, r); } }); } return r; } //#region 左右移动 private moveItem() { this.beforeMove.subscribe((obj: any) => { const items = obj.items; const ids = Object.keys(items); let direction = 1; switch (obj.target) { case 'right': Object.keys(items).forEach(id => { if (!this._rightData.find(n => n[this.idField].toString() === id)) { this._rightData.push(items[id]); } }); this._rightData = [...this._rightData]; if (this.isTree()) { this.tt.clearAll(); } direction = 1; break; case 'rightAll': items.forEach(data => { if (this._rightData.findIndex(n => n[this.idField] === data[this.idField]) === -1) { this._rightData.push(data); } }); if (this.isTree()) { this.tt.clearAll(); } direction = 1; break; case 'left': this._rightData = this._rightData.filter(v => { return ids.indexOf('' + v[this.idField]) === -1; }); this._rightCheckeds = {}; ids.forEach(id => { delete this._leftCheckeds[id]; }); direction = -1; if (this.isTree()) { this.tt.findRowNodes(ids).forEach(n => { n.node.selectable = true; }); this.tt.unCheckedNodes(ids, true); } break; case 'leftAll': this._rightCheckeds = {}; this._rightData = []; direction = -1; this._leftCheckeds = {}; if (this.isTree()) { this.tt.clearAll(); this.tt.findRowNodes(ids).forEach(n => { n.node.selectable = true; }); } break; } this.selectionsChange(); this.changeSelectIds(direction); this.afterMove.emit(Object.values(items)); }); } /** 右移 */ moveToRight(moveData?: any) { if (moveData) { this.beforeMove.emit({ items: { [moveData[this.idField]]: moveData }, target: 'right' }); } else { if (this.hasCheckeds('L')) { if (this.isTree()) { this._leftCheckeds = {}; this.tt.checkeds.forEach(n => { this._leftCheckeds[n.data[this.idField]] = n.data; }); } this.beforeMove.emit({ items: this._leftCheckeds, target: 'right' }); } else { this.mesagerSer.warning(this.noDataMoveMessage); } } } /** 全部右移 */ moveAllToRight() { if (this.searchValue === '') { this._rightData = []; } if (this.displayType === 'List') { this.beforeMove.emit({ items: this.dataSource, target: 'rightAll' }); } else { let _dataSource = []; this.convertTreeDataToPlainData(this.dataSource, _dataSource); _dataSource = _dataSource.filter(n => n.selectable).map(n => n.data); this.beforeMove.emit({ items: _dataSource, target: 'rightAll' }); } } /** 左移 */ moveToLeft(moveData?: any) { if (moveData) { this.beforeMove.emit({ items: { [moveData[this.idField]]: moveData }, target: 'left' }); } else { if (this.hasCheckeds('R')) { this.beforeMove.emit({ items: this._rightCheckeds, target: 'left' }); } else { this.mesagerSer.warning(this.noDataMoveMessage); } } } /** 全部左移 */ moveAllToLeft() { this.beforeMove.emit({ items: this._rightData, target: 'leftAll' }); } //#endregion //#region 上下移动显示列顺序 _getSelectInfo() { if (this.hasCheckeds('R')) { const key = Object.keys(this._rightCheckeds)[0]; const index = this._rightData.findIndex(c => '' + c[this.idField] === key); return { key, index }; } return null; } /** 置顶 */ moveTop() { const obj = this._getSelectInfo(); if (obj) { this._rightData.unshift(this._rightData[obj.index]); this._rightData.splice(obj.index + 1, 1); this.selectionsChange(); this.changeSelectIds(0); } else { this.mesagerSer.warning(this.noDataMoveMessage); } } /** 上移 */ moveUp() { if (this.hasCheckeds('R')) { const obj = this._getSelectInfo(); if (obj) { const index = obj.index; if (index !== 0) { const tempArr = this._rightData.splice(index, 1); this._rightData.splice(index - 1, 0, ...tempArr); } this.selectionsChange(); this.changeSelectIds(0); } } else { this.mesagerSer.warning(this.noDataMoveMessage); } } /** 下移 */ moveDown() { if (this.hasCheckeds('R')) { const obj = this._getSelectInfo(); if (obj) { const len = this._rightData.length; const index = obj.index; if (index === 0 || index !== len - 1) { const tempArr = this._rightData.splice(index, 1); this._rightData.splice(index + 1, 0, ...tempArr); } this.changeSelectIds(0); this.selectionsChange(); } } else { this.mesagerSer.warning(this.noDataMoveMessage); } } /** 置底 */ moveBottom() { const obj = this._getSelectInfo(); if (obj) { this._rightData.push(this._rightCheckeds[obj.key]); this._rightData.splice(obj.index, 1); this.changeSelectIds(0); this.selectionsChange(); } else { this.mesagerSer.warning(this.noDataMoveMessage); } } /** * 触发选择中id * @param number 1代表左侧数据右移,-1,代表数据左移,0代表上移下移 */ private changeSelectIds(number: number) { const ids = this._rightData.map(item => { return item[this.idField]; }); if (number === 0) { if (typeof this.selectedId === 'string') { this.selectedId = ids.join(','); } else if (Array.isArray(this.selectedId)) { this.selectedId = ids; } this.selectedIdChange.emit(this.selectedId); return; } if (!this.selectedId) { this.selectedId = []; } if (number === 1 || number === -1) { if (!ids.length) { this.selectedId = []; } else { this.selectedId = ids; } if (this.isTree()) { if (ids && ids.length) { this.tt.checkedNodes(ids, true, false, true); } } } this.canCheckLength = this.getCanCheckLength(); this.selectedIdChange.emit(this.selectedId); } //#endregion private selectionsChange() { this.dataChange.emit(this._rightData); this.cd.detectChanges(); } getText(item, textField = this.textField) { if (item) { if (this.commonUtils && this.commonUtils.getValue && textField) { return this.commonUtils.getValue(textField, item); } else { return item[textField]; } } else { return ''; } } getValue(item, valueField = this.valueField) { if (item) { if (this.commonUtils && this.commonUtils.getValue && valueField) { return this.commonUtils.getValue(valueField, item); } else { return item[valueField]; } } else { return ''; } } trackByItems = (index, item) => { return index; } } @Component({ selector: 'display-text', template: ` {{ _getText(data) }} ` }) export class DisplayTextComponent implements OnInit { @Input() template: TemplateRef; @Input() data: any; @Input() side = 'L'; constructor(public ms: MultiSelectComponent) { } ngOnInit() { // console.log(this.data); } _getText(item) { return this.ms.getText(item); } }