import { Component, OnInit, Input, ViewChild, ElementRef, Renderer2, AfterViewInit, DoCheck, AfterViewChecked, ChangeDetectionStrategy, ChangeDetectorRef, HostBinding, Output, EventEmitter, NgZone, OnDestroy, ViewRef } from "@angular/core"; import { ResponseToolbarDropDown, ResponseToolbarItem, ResponseToolbarGroup, ResponseToolbarClickEvent } from "../model/response-toolbar.model"; import { FResizedDirective } from "../resized/f-resized.directive"; import { Subject, Observable, Subscription, BehaviorSubject } from "rxjs"; import { throttleTime, debounceTime } from "rxjs/operators"; import { LocaleService } from "@farris/ui-locale"; import ResizeObserver from "resize-observer-polyfill"; @Component({ selector: "f-response-toolbar", templateUrl: "./response-toolbar.component.html", styleUrls: ["./response-toolbar.component.css"], // changeDetection: ChangeDetectionStrategy.OnPush }) export class ResponseToolbarComponent implements OnInit, AfterViewInit, DoCheck, AfterViewChecked, OnDestroy { @HostBinding("class.f-response-toolbar") frtCls = true; @HostBinding("class.position-relative") prCls = true; // 响应式容器 @ViewChild("resizedContainer") resizedContainerEl; // 辅助计算容器 @ViewChild("auxiliaryContainer") auxiliaryContainerEl; // 响应式内容容器 @ViewChild("resizedContent") resizedContentEl; // @ViewChild("resizedContainer", { read: FResizedDirective }) resizedContainerDirective: FResizedDirective; // 传递点击事件 @Output() rtClickEvent: EventEmitter = new EventEmitter< ResponseToolbarClickEvent >(); // 辅助计算宽度数据 auxiliaryDatas: Array = []; // 控制父元素滚动的样式 // private _dpState = false; // private ro: ResizeObserver | null = null; // 记录容器宽度 private containerWidthRecord = 0; // 记录变化 private needResponseChanges = 0; referEl = null; dpState = [false]; // 用来内部记录 private _auxiliary = -1; private _selfEl: HTMLElement; resizeGroups: Array = []; // 记录格式化后的数据 toolbarDatas: Array = []; // 内部记录格式化后的初始数据-------------------------暂时不用 // _toolbarDatas: Array = []; @Input() set groups(values: Array) { if (values) { this.resizeGroups = []; values.forEach(group => { this.resizeGroups.push( new ResponseToolbarGroup(group.id, group.name) ); // 还没有计算预置排序 }); } } private distanceThreshold = 20; private isUpdateTBView = false; @Input() set datas(values: Array) { if (values) { this.toolbarDatas = this._formatData(values); this.needResponseChanges++; // 处理分组数据 this._reOrganizeResizeGroups(); } } _stateDatas: Observable> = new Subject(); // 按钮的禁用状态 @Input() set btnState(values: Observable>) { this._stateDatas = values; } _btnVisibleDatas: Observable> = new Subject(); // 按钮的查看状态 @Input() set btnVisible(values: Observable>) { this._btnVisibleDatas = values; } @Input() clickThrottleTime = 350; private clickItems = new Subject(); private clickSubscription: Subscription; private rtSize = ""; /*尺寸:有sm,lg,空值,默认是空值 */ @Input() set buttonSize(btnSize: string) { if (btnSize != this.rtSize) { this.rtSize && this.render.removeClass( this._selfEl, "f-response-toolbar-" + this.rtSize ); this.rtSize = btnSize; this.rtSize && this.render.addClass( this._selfEl, "f-response-toolbar-" + this.rtSize ); } } get buttonSize(): string { return this.rtSize; } /*强制方向,默认为空值,可以有top、bottom,朝上、朝下。 * 使用场景:工具栏内部的下拉,需要朝上或者需要朝下展开,统一控制 */ @Input() popDirection = ""; // right按钮靠右 left按钮靠左 @Input() btnAlign = "right"; private ngZone = null; constructor( el: ElementRef, private render: Renderer2, private cd: ChangeDetectorRef, public localeService: LocaleService ) // private ngZone: NgZone { this._selfEl = el.nativeElement; if (el.nativeElement.parentElement) { this.referEl = el.nativeElement.parentElement; } } // 因为旧模板带有这个方法 ngAfterViewInit() {} // 因为旧模板带有这个方法 ngDoCheck() {} ngOnInit() { // 去掉overflow之后 this.render.addClass( this.auxiliaryContainerEl.nativeElement, "response-toolbar-hidden-element" ); // 按钮的禁用状态 this._stateDatas.subscribe(data => { this.changeState(data); }); // 按钮的可见状态 this._btnVisibleDatas.subscribe(data => { this.changeVisible(data); }); this.observerElement(); //处理表单加载后的按钮变化 this.ngZone.runOutsideAngular(() => { let selfObj=this; setTimeout(()=>{ selfObj.responseResize(); },0); }); // 拦截点击事件只传递第一次点击事件的处理操作交给parent来处理 this.clickSubscription = this.clickItems .pipe(debounceTime(this.clickThrottleTime)) .subscribe((eventDatas: ResponseToolbarClickEvent) => { this.rtClickEvent.emit(eventDatas); }); } ngOnDestroy() { if (this.ro) { this.ro.unobserve(this.resizedContainerEl.nativeElement); this.ro = null; } if (this.clickSubscription) { this.clickSubscription.unsubscribe(); } } ngAfterViewChecked() { if (this.needResponseChanges > 0) { this.needResponseChanges--; this.responseResize(); }else if (this.auxiliaryDatas.length>0) { this._calculate( true, this.auxiliaryContainerEl.nativeElement.offsetWidth ); } } // 因为旧模板带有这个方法 onResize(event) {} /** * 兼容旧模板旧方法 * 在不存在ngZone的情况下,创建ngZone,然后绑定事件 */ private observerElement() { if (!this.ngZone) { const moduleInjector = this.cd["_view"].root.ngModule.injector; this.ngZone = moduleInjector.get(NgZone, null); } this.ngZone.runOutsideAngular(() => { this.ro = new ResizeObserver((entries, observer) => { const tempWidth = entries[0].contentRect.width; if ( Math.abs(tempWidth - this.containerWidthRecord) > this.distanceThreshold ) { this.responseResize(); this.containerWidthRecord = tempWidth; } }); this.ro.observe(this.resizedContainerEl.nativeElement); }); } /** * 强制转换方向 * @param selfDefinePL * @param defaultPL */ getPlacement(selfDefinePL: string, defaultPL: string): string { let oldDirection = selfDefinePL ? selfDefinePL : defaultPL; if (!this.popDirection) { return oldDirection; } var plRelation = { bottom: "top", top: "bottom" }; oldDirection = oldDirection == "left" ? "left-bottom" : oldDirection; oldDirection = oldDirection == "right" ? "right-bottom" : oldDirection; let newDirection = oldDirection.replace( plRelation[this.popDirection], this.popDirection ); return newDirection; } /** * 调用此方法的场景 * 1、界面拖拽自动触发 * 2、显示调用 */ responseResize() { const _distance = this._getDistance(); if (_distance > 0) { // 如果分组都已经处理完,再出现滚动条不管 const _tempIndex = this._getFirstUnResponsedIndex(); if (_tempIndex < 0) { return; } // 所有第一层下拉都収折 this.dpState = [false]; for (let k = _tempIndex; k < this.resizeGroups.length; k++) { const _tempGroup = this.resizeGroups[k]; this._auxiliary = k; if (_tempGroup.isResponsing()) { this._calculate(); } else { // 如果还未开始处理响应式 const _tempDP = new ResponseToolbarDropDown({ text: _tempGroup.name, id: _tempGroup.id, placement: this.popDirection ? this.popDirection + "-left" : "bottom-left", width: _tempGroup.getWidth() }); // 已经处理过宽度 if (_tempDP.getWidth()) { this.auxiliaryDatas = [_tempDP]; this._calculate(true, _tempDP.getWidth()); } else { // 更新辅助数据 let auxiliaryIndex=this.auxiliaryDatas.findIndex((item)=>{ return item.id==_tempDP.id }); if(auxiliaryIndex==-1){ this.auxiliaryDatas.push(_tempDP); } if(this.cd&&!(this.cd as ViewRef).destroyed){ this.cd.detectChanges(); } } } } } else { // 寻找最后一个处理响应式的元素 const _tempIndex = this._getLastResponseIndex(); if (_tempIndex < 0) { return; } // 临时存储数据 let _tempToolbarDatas = [].concat(this.toolbarDatas); // 如果已经开始处理响应式 for (let k = _tempIndex; k >= 0; k--) { const result = this._restitute(_tempToolbarDatas, k); _tempToolbarDatas = [].concat(result["data"]); if (!result["continueTo"]) { break; } } // 更新数据 this.toolbarDatas = [].concat(_tempToolbarDatas); // 所有第一层下拉都収折 this.dpState = [false]; if(this.cd&&!(this.cd as ViewRef).destroyed){ this.cd.detectChanges(); } } } // 下拉的展开收起状态 dpSectionState(state: boolean, id) { if (state) { // 展开状态下 let dropdowns = this.resizedContentEl.nativeElement.querySelectorAll( "[fDropdown]" ); if (dropdowns && dropdowns.length > 1) { for (var k = 0; k < dropdowns.length; k++) { if ( dropdowns[k].className.indexOf("show") && dropdowns[k].id != id ) { // dropdowns[k].dispatchEvent(new Event('selfClose')); this.compatibleDispatchEvent(dropdowns[k], "selfClose"); } } } } } private compatibleDispatchEvent(eventEl, eventName) { var event; if (typeof Event === "function") { event = new Event(eventName); } else { event = document.createEvent("Event"); event.initEvent(eventName, false, false); } eventEl.dispatchEvent(event); } // 修改启用禁用状态 changeState(values) { const idArray = Object.keys(values); idArray.forEach(id => { const state = values[id]; const item = this._findItemByID(id, this.toolbarDatas); if(item){ item["disabled"] = state; } }); if(this.cd&&!(this.cd as ViewRef).destroyed){ this.cd.detectChanges(); } } /** * 修改显示状态 * 如果是下拉按钮 * 判断下面的子是否都隐藏 * @param values */ changeVisible(values) { const idArray = Object.keys(values); idArray.forEach(id => { const visible = values[id]; const item = this._findItemByID(id, this.toolbarDatas); if(item){ item["visible"] = visible; } }); // 遍历循环,更新数据 this.toolbarDatas.forEach(item => { // 如果是下拉 if (item["isDP"]) { this._checkDropdownVisible(values, item); } }); this.needResponseChanges++; // 更新 //this.cd.markForCheck(); if(this.cd&&!(this.cd as ViewRef).destroyed){ this.cd.detectChanges(); } } /** * 如果是下拉按钮 * 判断下面的子是否都隐藏 * A. 如果子都隐藏,那么整个下拉都被隐藏; * B. 如果存在子没有隐藏,而且下拉没有被预置隐藏,下拉按钮显示 * @param outsideVisibleArrays 从组件外传入的可见状态对象 * @param dropdownItem 下拉元素 */ private _checkDropdownVisible(outsideVisibleArrays, dropdownItem): void { //如果没有被强制设置状态 if (!outsideVisibleArrays.hasOwnProperty(dropdownItem["id"])) { // 下拉元素不可见,判断是否有可见子元素 if ( !dropdownItem["visible"] && this._hasVisibleItem(dropdownItem["children"], true) ) { dropdownItem["visible"] = true; } // 下拉元素是可见,判断是否有子元素都不可见 if ( dropdownItem["visible"] && !this._hasVisibleItem(dropdownItem["children"], true) ) { dropdownItem["visible"] = false; } } dropdownItem.children.forEach(item => { if (item["isDP"]) { this._checkDropdownVisible(outsideVisibleArrays, item); } }); } /** * 在待检查数据中心,检查是否存在该可见状态的数据。 * 如果有返回true,否则返回false * @param datas 待检查数据 * @param visible 可见状态 */ private _hasVisibleItem(datas, visible): boolean { const findIndex = datas.findIndex(childItem => { if (childItem["visible"] == visible) { return true; } return false; }); if (findIndex > -1) { return true; } return false; } /** * 捕获点击 * @param ev */ clickItem(ev: MouseEvent) { // 如果是分离的下拉按钮 if (this.elhasSpecialCls(ev.target, "dropdown-toggle-split")) { ev.stopImmediatePropagation(); } // 判断是否点击到需要处理事件的元素上,按钮、下拉按钮 var clickEvEl = this.findBtnOrTogglerItemFromClick(ev.target); if (clickEvEl) { let tempID = clickEvEl["id"]; // 修改模板,id调整到下拉整体上 if ( !tempID && this.elhasSpecialCls(clickEvEl["parentNode"], "f-rt-dropdown") ) { tempID = clickEvEl["parentNode"].id; } if (tempID) { const tempItem = this._findItemByID(tempID, this.toolbarDatas); // 如果元素禁用 if (tempItem["disabled"]) { // 禁止向上传递 ev.stopImmediatePropagation(); } else { this.clickItems.next({ id: tempID, text: tempItem["text"], hidden: tempItem["hidden"] }); } } } else { ev.stopImmediatePropagation(); } } private findBtnOrTogglerItemFromClick(clickTarget) { // 父级标签是否是body,是着停止返回集合,反之继续 if (this.elhasSpecialCls(clickTarget)) { return clickTarget; } else if ( clickTarget.parentNode["className"]&&clickTarget.parentNode["className"].indexOf("f-response-content") < 0 ) { var parentN = clickTarget.parentNode; if (this.elhasSpecialCls(parentN)) { return parentN; } else { return this.findBtnOrTogglerItemFromClick(parentN); } } // 返回集合,结束 return null; } private elhasSpecialCls(el, clsName = "") { if (el) { var classNameList = el.classList; var findCls = false; for (var k = 0; k < classNameList.length; k++) { if (clsName) { if (classNameList[k] == clsName) { findCls = true; break; } } else { if ( classNameList[k] == "f-rt-btn" || classNameList[k] == "f-rt-toggle" ) { findCls = true; break; } } } return findCls; } return false; } /** * 根据传递的数据重新组建分组 * ToDo 暂时没有更复杂的分组,否则需要记录分组 */ private _reOrganizeResizeGroups() { // 处理分组数据 // if (this.resizeGroups.length === 0) { let moreText = "更多"; if (this.localeService) { moreText = this.localeService.getValue("responseToolbar.more"); } const defaultResizeGroup = new ResponseToolbarGroup( "toolbar-group-1", moreText ); for (let i = this.toolbarDatas.length - 1; i >= 0; i--) { defaultResizeGroup.setPreset(this.toolbarDatas[i]["id"]); } this.resizeGroups = [].concat(defaultResizeGroup); // this.cd.markForCheck(); if(this.cd&&!(this.cd as ViewRef).destroyed){ this.cd.detectChanges(); } } /** * 内容与容器的差值 */ private _getDistance() { return ( this.resizedContentEl.nativeElement.offsetWidth - this.resizedContainerEl.nativeElement.offsetWidth ); } /** * 根据返回结果判断是否继续 * @param groupIndex */ private _restitute(originalData, groupIndex) { const tempGroup = this.resizeGroups[groupIndex]; // 滚动条和内容的距离 let tempDistance = this._getDistance(); // 计算最后一个下拉的宽度 const tempGroupData = originalData[this._findIndexByID(originalData, tempGroup.id)]; // 找到还原的位置 const tempPresetIndex = [].concat(tempGroup.responsedIndex); // 排除原下拉子元素被计算 for (let j = tempPresetIndex.length - 1; j >= 0; j--) { if (tempDistance >= 0) { break; } // 从下拉中移除 const tempChildData = tempGroupData.children[0]; const specialDistance = j === 0 ? tempGroupData.getWidth() : 0; tempDistance = tempDistance + (tempChildData.getWidth() ? tempChildData.getWidth() : 0) - specialDistance; if (tempDistance >= 0) { break; } else { // 更新下拉 tempGroupData.removeChild(); // 插入到原位置 originalData.splice(tempPresetIndex[j], 0, tempChildData); // 更新位置 tempGroup.removeResponsed(j); if (j === 0) { originalData.splice( this._findIndexByID(originalData, tempGroup.id), 1 ); } } } return { continueTo: tempDistance > 0 ? true : false, data: originalData }; } /** * 开始计算 * @param datas */ private _calculate(auxiliary: boolean = false, tGroupWidth: number = 0) { if (this._auxiliary < 0) { return; } // 记录需要修改的下拉 let tempDP; // 滚动条和内容的距离 let tempDistance = this._getDistance() + tGroupWidth; // 临时存储数据 const tempToolbarDatas = [].concat(this.toolbarDatas); const tempGroup = this.resizeGroups[this._auxiliary]; if (auxiliary) { tempDP = this.auxiliaryDatas.pop(); tempDP.setWidth(tGroupWidth); tempGroup.setWidth(tGroupWidth); } else { tempDP = tempToolbarDatas[ this._findIndexByID(tempToolbarDatas, tempGroup.id) ]; } // 当前剩余隐藏 for ( let j = tempGroup.responsedIndex.length; j < tempGroup.presetId.length; j++ ) { if (tempDistance <= 0) { break; } // 计算在当前数据中的位置,根据这个位置可以找宽度 const tempOldIndex = this._findIndexByID( this.toolbarDatas, tempGroup.presetId[j] ) as any; const tempNewIndex = this._findIndexByID( tempToolbarDatas, tempGroup.presetId[j] ) as any; // 计算按钮宽度 let tempWidth = this.toolbarDatas[tempOldIndex].getWidth() as any; if (tempWidth === 0) { // 按钮宽度未曾被计算过 tempWidth = this._getWidthFromElementsById( tempGroup.presetId[j] ); tempToolbarDatas[tempNewIndex].setWidth(tempWidth); } else if (tempWidth === false) { // 按钮被隐藏 tempWidth = 0; } tempDP.addChild(tempToolbarDatas[tempNewIndex]); // 移除 tempToolbarDatas.splice(tempNewIndex, 1); // 保存位置用于还原 tempGroup.setResponsed(tempNewIndex); tempDistance = tempDistance - tempWidth; } if (auxiliary && tempDP.hasChild()) { // 插入 tempToolbarDatas.push(tempDP); } this._auxiliary = -1; this.toolbarDatas = [].concat(tempToolbarDatas); if(this.cd&&!(this.cd as ViewRef).destroyed){ this.cd.detectChanges(); } } /** * 根据id找到实际显示元素的宽度 * @param findId */ private _getWidthFromElementsById(findId): number { const parentEl = this.resizedContentEl.nativeElement; const childLen = parentEl.children.length; let result = 0; for (var k = 0; k < childLen; k++) { if (parentEl.children[k].id === findId) { result = parentEl.children[k].offsetWidth; break; } } return result; } /** * 格式化数据 */ private _formatData(datas, idPrefix: string = "response") { const result = []; datas.forEach((item, index) => { if (item.isDP) { // 如果是下拉 if ( item.hasOwnProperty("children") && item["children"].length ) { const children = [].concat(item["children"]); item.children = []; const dpItem = new ResponseToolbarDropDown( Object.assign({ id: idPrefix + "_" + index }, item) ); result.push(dpItem); dpItem.children = this._formatData( children, idPrefix + "_" + index ); } else { result.push( new ResponseToolbarDropDown( Object.assign({ id: idPrefix + "_" + index }, item) ) ); } } else { // 非下拉 result.push( new ResponseToolbarItem( Object.assign({ id: idPrefix + "_" + index }, item) ) ); } }); return result; } /** * 找到第一个未处理完响应式的元素 */ private _getFirstUnResponsedIndex() { const tempUnResponsedIndex = this.resizeGroups.findIndex(group => { if (group.isResponsed()) { return false; } return true; }); return tempUnResponsedIndex; } /** * 寻找最后一个处理响应式的元素 */ private _getLastResponseIndex() { const tempResponsingIndex = this.resizeGroups.findIndex(group => { if (!group.isResponsing()) { return true; } return false; }); // 所有组元素都已处理完响应式 if (tempResponsingIndex === -1) { return this.resizeGroups.length - 1; } // 所有组元素都未开始处理响应式 return tempResponsingIndex > 1 ? tempResponsingIndex - 1 : -1; } /** * 找到group形成的下拉,在数据中的位置 */ private _findIndexByID( datas: Array, id: string ): any { const result = datas.findIndex((item, index) => { if (item["id"] === id) { return true; } return false; }); return result; } /** * 根据ID寻找Item */ private _findItemByID( id: string, data: Array ): ResponseToolbarDropDown | ResponseToolbarItem { let tempResult = null; const tempIndex = data.findIndex((item, index) => { if (item["id"] === id) { tempResult = item; return true; } // 如果是下拉,层级下拉 if (item["isDP"]) { const tempItem = this._findItemByID(id, item["children"]); if (tempItem) { tempResult = tempItem; return true; } } return false; }); return tempResult; } }