首页 > 解决方案 > 多次发出使用 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();
  }
}

标签: angular

解决方案


推荐阅读