angular - 多次发出使用 RXJS 主题的窗口调整大小事件
问题描述
我的要求是在垫子菜单中显示额外的标签,这些标签不适合内部使用的屏幕,使用垫子分页处理溢出项目。在调整大小屏幕上,我正在计算溢出项目,这些项目将显示在“my-overflow-menu”中,其余的“visibleItems”将由 mat 选项卡显示。
在调整窗口大小时,我需要调用 _getOverflowItems(),但我认为事件被多次发出,因此“visibleItems”和“overflowItems”的值被多次更改。因此,可见项目未正确显示在垫选项卡内。HTML
<div class="flex-container">
<div class="flex-item-main">
<mat-tab-group [backgroundColor]="backgroundColor"
[selectedIndex]="selectedIndex" (selectedTabChange)="onSelect($event)" (focusChange)="onFocusChange($event)"
(keydown.enter)="OnSelectFocusedIndex($event)" (keydown.space)="OnSelectFocusedIndex($event)">
<ng-container *ngFor="let item of visibleItems; let currentIndex=index">
{{visibleItems.length}}
<mat-tab label="item.label">
{{item.label}}
</mat-tab>
</ng-container>
</mat-tab-group>
</div>
<div class="flex-item-overflow" *ngIf="overflowItems.length > 0">
<my-overflow-menu [items]="overflowItems" (select)="onSelectOverflowItem($event);"></my-overflow-menu>
</div>
</div>
CSS
.flex-container {
display: flex;
overflow: hidden;
width: 100%;
align-items: baseline;
}
.flex-item-main {
flex: 1;
width: calc(100% - 120px);\\ 120px is the width of view more button which is inside my-overflow-menu component
}
.flex-item-overflow {
flex: 1;
flex-basis: auto;
justify-content: flex-end;
}
JS
import {
AfterViewInit,
Input,
Output,
EventEmitter,
Component,
ChangeDetectorRef,
ViewEncapsulation,
ChangeDetectionStrategy,
ElementRef,
Renderer2,
OnInit,
OnChanges,
OnDestroy,
SimpleChanges,
TemplateRef,
ContentChild,
ViewChildren,
QueryList,
HostBinding,
HostListener,
ViewChild
} from '@angular/core';
import { ENTER, SPACE, ESCAPE } from '@angular/cdk/keycodes';
import { Directionality } from '@angular/cdk/bidi';
import { Subscription, async, AsyncSubject } from 'rxjs';
import { Subject } from 'rxjs';
import { last, takeLast, distinctUntilChanged, debounceTime } from 'rxjs/operators';
export enum DynamicNavType {
Nav = 'nav',
Tabs = 'tabs'
}
@Component({
selector: 'my-dynamic-menubar',
templateUrl: './my-menubar.component.html',
styleUrls: ['./my-menubar.component.scss'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class myDynamicMenubarComponent
implements AfterViewInit, OnInit, OnChanges, OnDestroy {
private _isMenuItemClosed = false;
private _intlChanges: Subscription;
private _closeAriaLabel: string;
private _type;
@Input() items = [];
overflowItems= [];
visibleItems = [];
private _selectedPath: string;
tabLabels = [];
private tabSubscription = Subscription.EMPTY;
@Input() verticalOverflow = false;
/* tslint:disable:no-input-rename */
@Input('activeColor') color;
/* tslint:disable:no-input-rename */
@Input() backgroundColor: string;
selectedIndex = 0;
focusedIndex = 0;
closedItemIndexLessThanSelectedIndex = false;
private resizeEvent$ = new Subject();
@Output() select = new EventEmitter<myDynamicMenubarItem>();
/* tslint:disable:no-output-rename */
@Output('close') closeEmitter = new EventEmitter<myDynamicMenubarItem>();
@ContentChild(TemplateRef, { static: false }) navActions;
@Input()
get closeAriaLabel() {
return this._closeAriaLabel || this._intl.dynamicmenubar.aria.closeIconLabel;
}
set closeAriaLabel(value: string) {
this._closeAriaLabel = value;
}
@Input()
get type() {
return this._type;
}
set type(value: myDynamicNavType) {
this._renderer.removeClass(this._elementRef.nativeElement, `my-dynamic-${this._type}`);
this._type = value || myDynamicNavType.Nav;
this._renderer.addClass(this._elementRef.nativeElement, `my-dynamic-${this._type}`)
}
constructor(public _elementRef: ElementRef,
protected _renderer: Renderer2,
changeDetectorRef: ChangeDetectorRef,
private direction: Directionality) {
super(_elementRef);
this.type = myDynamicNavType.Nav;
}
ngOnInit() {
let matTabGroupElem;
// remove the body element from matTabGroup since we are never going to need it.
matTabGroupElem = this._elementRef.nativeElement.querySelector('.mat-tab-group');
matTabGroupElem.removeChild(matTabGroupElem.children[1]);
this.visibleItems = this.items;
if (this.verticalOverflow) {
this.resizeEvent$.subscribe((ev) => {
console.log(ev);
this._getOverflowItems();
});
}
}
ngAfterViewInit() {
this._removeAriaControlFromTab();
this.tabLabels = this.menuLabels.toArray();
this.tabSubscription = this.menuLabels.changes.subscribe((value) => {
this.tabLabels = value.toArray();
this._removeAriaControlFromTab();
});
if (this.verticalOverflow) {
this.removePagination ();
this._getOverflowItems();
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes.items && this.selectedIndex > -1 && this.items[this.selectedIndex]) {
// Defer emitting the value in order to avoid the "Expression
// has changed after it was checked" errors from Angular.
Promise.resolve(null).then(() => {
// used indexToSet inside to get latest value of this.selectedIndex if changed by
// selectedPath setter.
const indexToSet = this.closedItemIndexLessThanSelectedIndex ? this.selectedIndex - 1 : this.selectedIndex;
this.onSelect({ index: indexToSet });
});
this.closedItemIndexLessThanSelectedIndex = false;
}
}
@HostListener('window:resize')
onResize() {
if (this.verticalOverflow) {
this.resizeEvent$.next();
}
}
removePagination() {
const matTabHeader = this._elementRef.nativeElement.querySelector('.mat-tab-header');
if (matTabHeader && matTabHeader.children[0] && matTabHeader.children[2]) {
matTabHeader.removeChild(matTabHeader.children[2]);
matTabHeader.removeChild(matTabHeader.children[0]);
}
}
_getOverflowItems() {
console.log('overflow items', this.overflowItems);
console.log('visible', this.visibleItems);
if (this.verticalOverflow) {
const matTabLabelContainer = this._elementRef.nativeElement.querySelectorAll('.mat-tab-label-container')[0];
const matTabLabels = this._elementRef.nativeElement.querySelectorAll('.mat-tab-label');
const matTabHeader = this._elementRef.nativeElement.querySelector('.mat-tab-header');
const tabsOffsetWidth = matTabLabelContainer.offsetWidth;
const tabsScrollWidth = matTabLabelContainer.scrollWidth;
this.visibleItems = JSON.parse(JSON.stringify(this.items));
if (tabsOffsetWidth < tabsScrollWidth) {
for (let i = 0; i < matTabLabels.length; i++) {
const matTab = matTabLabels[i];
if (matTab.offsetLeft + matTab.offsetWidth > tabsOffsetWidth) {
this.overflowItems = JSON.parse(JSON.stringify(this.items.slice(i-1)));
this.visibleItems = JSON.parse(JSON.stringify(this.items.slice(0, i-1)));
break;
}
}
}
}
}
findSelectedItem(selectedItem) {
let index;
this.items.forEach((item)=>{
if(item.path === selectedItem.path) {
index = item;
}
});
return this.items.indexOf(index);
}
onSelectOverflowItem(item) {
let overflowItem = item.item;
let overflowSubItem = item.subItem;
let selectedItemIndex;
const lastVisibleItem = this.visibleItems.pop();
this.overflowItems = this.overflowItems.filter(item => item.path !== overflowItem.path);
this.visibleItems.push(overflowItem);
this.overflowItems.push(lastVisibleItem);
console.log(this.items);
selectedItemIndex = this.findSelectedItem(overflowItem);
if(selectedItemIndex) {
let itemToRemoveIndex;
let itemToRemove;
itemToRemove = {...lastVisibleItem};
itemToRemoveIndex = this.findSelectedItem(itemToRemove)
this.items.splice(selectedItemIndex,1); //removed item selected from overflow menu, from original items.
this.items.splice(itemToRemoveIndex, 1, overflowItem);
this.items.push(itemToRemove); // push earlier removed item back to the end of original item.
}
console.log(this.items);
this.onSelect({ index: this.visibleItems.length - 1});
}
@Input()
set selectedPath(path: string) {
if (this.selectedPath === path) {
return;
}
this._selectedPath = path;
// Check if selectedPath match with any first level items
let selected = path ? this.items.filter(item => this.matches(path, item)) : null;
// If selectedPath matches with first level items, first matched item will be selected (and select event emitter will be emitted).
if (selected && selected.length > 0) {
if (this.selectedIndex === this.items.indexOf(selected[0])) {
this.onSelect({ index: this.selectedIndex })
} else {
this.selectedIndex = this.items.indexOf(selected[0]);
}
return;
} else {
// If selectedPath does not match with any first level item, check if matches with children of each first level item
for (const item of this.items) {
if (item.children && item.children.length > 0) {
selected = path ? item.children.filter(citem => this.matches(path, citem)) : null;
if (selected && selected.length > 0) {
// If selectedPath match with child menu, set selectedIndex to Parent and selectedPath to child menu
this.onChildSelect(selected[0], item);
return;
}
}
}
}
// If selectedPath does not match with first level items as well as child menu,
// select first item.
// FIXME: The `ngOnChange` event should take care of this. Currently it only checks for `items` changes.
this.onSelect({ index: 0 });
}
get selectedPath() {
return this._selectedPath;
}
private isChild(parent: myDynamicMenubarItem, childPath: string): boolean {
if (!childPath || parent.path === childPath) {
return false;
}
const selected = parent.children.filter(citem => this.matches(childPath, citem));
if (selected && selected.length > 0) {
return true;
}
return false;
}
onSelect($event) {
if (this.items.length > 0) {
this.selectedIndex = $event.index;
let selectedItem = this.items[$event.index];
// Focus on the next element after menuitem is closed.
// We dont need to handle focus on click and enter because focus for those events will be handled by mat-tabs.
if (this._isMenuItemClosed) {
this.focusTabOnIndex(this.selectedIndex);
this._isMenuItemClosed = false;
}
// This condition added to avoid select emitter for parent item on selecting child item
if (selectedItem.children && selectedItem.children.length > 0) {
if (this.isChild(selectedItem, this._selectedPath)) {
return;
} else {
selectedItem = selectedItem.children[0];
}
}
this._selectedPath = selectedItem.path;
this.select.emit(selectedItem);
}
}
onFocusChange($event) {
this.focusedIndex = $event.index;
}
OnSelectFocusedIndex(event) {
this.tabLabels[this.focusedIndex].handleKeydown(event);
}
onChildMenuToggle(event) {
if (!event) {
// this.selectedIndex is used to ensure that it gives focus to parent element
// when child is selected using mouse click.
this.focusTabOnIndex(this.selectedIndex);
}
}
onChildSelect($event, item) {
this.selectedIndex = this.items.indexOf(item);
this._selectedPath = $event.path;
this.select.emit($event);
}
private matches(path: string, item: myDynamicMenubarItem) {
if (path && item.path) {
if (path === item.path) {
return true;
}
}
return false;
}
close(item) {
// check if the index of the item being closed is smaller than the item that is currently selected
// if it is, and if the user actually closes the tab - we'll have to set the selectedIndex to selectedIndex - 1
// otherwise the selectedItem will change
this.closedItemIndexLessThanSelectedIndex = false;
if (this.items.indexOf(item) < this.selectedIndex) {
this.closedItemIndexLessThanSelectedIndex = true;
}
this._isMenuItemClosed = true;
this.closeEmitter.emit(item);
}
focusTabOnIndex(index) {
this._elementRef.nativeElement.getElementsByClassName('mat-tab-label')[index].focus();
}
_handleKeydown(event: KeyboardEvent, item: any) {
switch (event.keyCode) {
case ENTER:
case SPACE:
this.close(item);
event.stopPropagation();
break;
case ESCAPE:
this.focusTabOnIndex(this.focusedIndex);
event.stopPropagation();
break;
default:
}
}
/**
* Remove aria-controls from each tab because it has a reference to mat-content which is
*/
_removeAriaControlFromTab() {
const tabs = this._elementRef.nativeElement.querySelectorAll('.mat-tab-header .mat-tab-label');
tabs.forEach(tab => {
this._renderer.removeAttribute(tab, 'aria-controls');
});
}
ngOnDestroy() {
this.tabSubscription.unsubscribe();
this.resizeEvent$.complete();
}
}
解决方案
推荐阅读
- rust - 在不使用 libtest 的情况下使用 `test::TestDescAndFn` 的最佳方法是什么?
- django - Docker 在 Gitlab CI/CD 中提取的图像无法识别 django 测试
- javascript - 有没有办法在“while”事件中等待 5 秒然后执行下一行 javascript?
- powershell - 在 Powershell 中检索 CPU 详细信息
- javascript - 使用参数将 onlick 事件添加到多个元素
- angularjs - AngularJS范围变量值的变化没有反映在控制器之间
- python - 如果不允许从 Python 直接连接,有哪些选项可以连接到 SharePoint 列表
- rust - 根据目标平台生成 Rust 可执行文件或库
- php - PHP循环遍历没有索引名称的数组
- reactjs - 获取使用在 IE11 中工作的 create-react-app 创建的应用程序