nz-affix (NG-ZORRO 8.5.x)

github 源码
NG-ZORRO 手册
NgZone

fixed & scroll

*
父节点:fixed
子节点: 内容超过父节点宽度,想出现横向滚动条,并且可以滚动
引起了冲突,子节点的横向滚动条失效了

*
让div在不显示滚动条的情况下响应scroll事件:
爷爷爷爷节点: fixed
爷爷节点: overflow: hidden
父节点: overflow-x: scroll; width: 100%; height: 100% + 滚动条高度
子节点: 内容超过父节点宽度

<style>
* {
    margin: 0;
    padding: 0;
}
#fixed {
    position:fixed;
    left:200px;
    top:100px;
    height: 50px;
    border:solid 1px #000;
    width:360px;
    background:#fff
}
.a1 {
    height: 100%;
    overflow: hidden;
}
.a2 {
    height: calc(100% + 20px);
    overflow-x: auto;
}
.ctx {
    width: max-content;
}
</style>
<div id="fixed">
    <!-- fixed定位容器 -->
    <div class="a1">
        <div class="a2">
            <div class="ctx">内容a内容b内容c内容d内容e内容f内容g内容h内容j内容k内容内容内容内容</div>
        </div>
    </div>
</div>

nz-affix & scroll

父节点: 采用 nz-affix (fixed)
子节点:内容超过父节点宽度,想出现横向滚动条,并且可以滚动

可以达到预期目的,
既能固定,又能在固定基础上进行滚动

NgZone

一种用于在Angular区域内部或外部执行工作的可注射服务。

在启动由一个或多个不需要Angular处理UI更新或错误处理的异步任务的工作时优化性能。
可以通过runOutsideAngular启动此类任务,如果需要,这些任务可以通过run重新进入Angular区域。

<div #fixedEl>
  <ng-content></ng-content>
</div>
/**
 * @license
 * Copyright Alibaba.com All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
 */

import { Platform } from '@angular/cdk/platform';
import { DOCUMENT } from '@angular/common';
import {
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  EventEmitter,
  Inject,
  Input,
  NgZone,
  OnChanges,
  OnDestroy,
  Output,
  SimpleChanges,
  ViewChild,
  ViewEncapsulation
} from '@angular/core';
import { fromEvent, merge, Subscription } from 'rxjs';
import { auditTime } from 'rxjs/operators';

import {
  getStyleAsText,
  shallowEqual,
  InputNumber,
  NgStyleInterface,
  NzConfigService,
  NzScrollService,
  WithConfig
} from 'ng-zorro-antd/core';
import { isTargetWindow } from './utils';

interface SimpleRect {
  top: number;
  left: number;
  width?: number;
  height?: number;
  bottom?: number;
}

const NZ_CONFIG_COMPONENT_NAME = 'affix';
const NZ_AFFIX_CLS_PREFIX = 'ant-affix';
const NZ_AFFIX_DEFAULT_SCROLL_TIME = 20;
const NZ_AFFIX_RESPOND_EVENTS = ['resize', 'scroll', 'touchstart', 'touchmove', 'touchend', 'pageshow', 'load'];

@Component({
  selector: 'nz-affix',
  exportAs: 'nzAffix',
  templateUrl: './nz-affix.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
  styles: [
    `
      nz-affix {
        display: block;
      }
    `
  ],
  encapsulation: ViewEncapsulation.None
})
export class NzAffixComponent implements AfterViewInit, OnChanges, OnDestroy {
  @ViewChild('fixedEl', { static: true }) private fixedEl: ElementRef<HTMLDivElement>;

  @Input() nzTarget: string | Element | Window;

  @Input()
  @WithConfig<number | null>(NZ_CONFIG_COMPONENT_NAME, 0)
  @InputNumber()
  nzOffsetTop: null | number;

  @Input()
  @WithConfig<number | null>(NZ_CONFIG_COMPONENT_NAME, null)
  @InputNumber()
  nzOffsetBottom: null | number;

  @Output() readonly nzChange = new EventEmitter<boolean>();

  private readonly placeholderNode: HTMLElement;

  private affixStyle?: NgStyleInterface;
  private placeholderStyle?: NgStyleInterface;
  private scroll$: Subscription = Subscription.EMPTY;
  private timeout?: number;
  private document: Document;

  private get target(): Element | Window {
    const el = this.nzTarget;
    return (typeof el === 'string' ? this.document.querySelector(el) : el) || window;
  }

  constructor(
    el: ElementRef,
    @Inject(DOCUMENT) doc: any, // tslint:disable-line no-any
    public nzConfigService: NzConfigService,
    private scrollSrv: NzScrollService,
    private ngZone: NgZone,
    private platform: Platform
  ) {
    // The wrapper would stay at the original position as a placeholder.
    this.placeholderNode = el.nativeElement;
    this.document = doc;
  }

  ngOnChanges(changes: SimpleChanges): void {
    const { nzOffsetBottom, nzOffsetTop, nzTarget } = changes;

    if (nzOffsetBottom || nzOffsetTop) {
      this.updatePosition({} as Event);
    }
    if (nzTarget) {
      this.registerListeners();
    }
  }

  ngAfterViewInit(): void {
    this.registerListeners();
  }

  ngOnDestroy(): void {
    this.removeListeners();
  }

  private registerListeners(): void {
    this.removeListeners();
    this.scroll$ = this.ngZone.runOutsideAngular(() => {
      return merge(...NZ_AFFIX_RESPOND_EVENTS.map(evName => fromEvent(this.target, evName)))
        .pipe(auditTime(NZ_AFFIX_DEFAULT_SCROLL_TIME))
        .subscribe(e => this.updatePosition(e));
    });
    this.timeout = setTimeout(() => this.updatePosition({} as Event));
  }

  private removeListeners(): void {
    clearTimeout(this.timeout);
    this.scroll$.unsubscribe();
  }

  getOffset(element: Element, target: Element | Window | undefined): SimpleRect {
    const elemRect = element.getBoundingClientRect();
    const targetRect = this.getTargetRect(target!);

    const scrollTop = this.scrollSrv.getScroll(target, true);
    const scrollLeft = this.scrollSrv.getScroll(target, false);

    const docElem = this.document.body;
    const clientTop = docElem.clientTop || 0;
    const clientLeft = docElem.clientLeft || 0;

    return {
      top: elemRect.top - targetRect.top + scrollTop - clientTop,
      left: elemRect.left - targetRect.left + scrollLeft - clientLeft,
      width: elemRect.width,
      height: elemRect.height
    };
  }

  private getTargetRect(target: Element | Window): SimpleRect {
    return !isTargetWindow(target)
      ? target.getBoundingClientRect()
      : {
          top: 0,
          left: 0,
          bottom: 0
        };
  }

  private setAffixStyle(e: Event, affixStyle?: NgStyleInterface): void {
    const originalAffixStyle = this.affixStyle;
    const isWindow = this.target === window;
    if (e.type === 'scroll' && originalAffixStyle && affixStyle && isWindow) {
      return;
    }
    if (shallowEqual(originalAffixStyle, affixStyle)) {
      return;
    }

    const fixed = !!affixStyle;
    const wrapEl = this.fixedEl.nativeElement;
    wrapEl.style.cssText = getStyleAsText(affixStyle);
    this.affixStyle = affixStyle;
    if (fixed) {
      wrapEl.classList.add(NZ_AFFIX_CLS_PREFIX);
    } else {
      wrapEl.classList.remove(NZ_AFFIX_CLS_PREFIX);
    }

    if ((affixStyle && !originalAffixStyle) || (!affixStyle && originalAffixStyle)) {
      this.nzChange.emit(fixed);
    }
  }

  private setPlaceholderStyle(placeholderStyle?: NgStyleInterface): void {
    const originalPlaceholderStyle = this.placeholderStyle;
    if (shallowEqual(placeholderStyle, originalPlaceholderStyle)) {
      return;
    }
    this.placeholderNode.style.cssText = getStyleAsText(placeholderStyle);
    this.placeholderStyle = placeholderStyle;
  }

  private syncPlaceholderStyle(e: Event): void {
    if (!this.affixStyle) {
      return;
    }
    this.placeholderNode.style.cssText = '';
    this.placeholderStyle = undefined;
    const styleObj = {
      width: this.placeholderNode.offsetWidth,
      height: this.fixedEl.nativeElement.offsetHeight
    };
    this.setAffixStyle(e, {
      ...this.affixStyle,
      ...styleObj
    });
    this.setPlaceholderStyle(styleObj);
  }

  updatePosition(e: Event): void {
    if (!this.platform.isBrowser) {
      return;
    }

    const targetNode = this.target;
    let offsetTop = this.nzOffsetTop;
    const scrollTop = this.scrollSrv.getScroll(targetNode, true);
    const elemOffset = this.getOffset(this.placeholderNode, targetNode!);
    const fixedNode = this.fixedEl.nativeElement;
    const elemSize = {
      width: fixedNode.offsetWidth,
      height: fixedNode.offsetHeight
    };
    const offsetMode = {
      top: false,
      bottom: false
    };
    // Default to `offsetTop=0`.
    if (typeof offsetTop !== 'number' && typeof this.nzOffsetBottom !== 'number') {
      offsetMode.top = true;
      offsetTop = 0;
    } else {
      offsetMode.top = typeof offsetTop === 'number';
      offsetMode.bottom = typeof this.nzOffsetBottom === 'number';
    }
    const targetRect = this.getTargetRect(targetNode as Window);
    const targetInnerHeight = (targetNode as Window).innerHeight || (targetNode as HTMLElement).clientHeight;
    if (scrollTop >= elemOffset.top - (offsetTop as number) && offsetMode.top) {
      const width = elemOffset.width;
      const top = targetRect.top + (offsetTop as number);
      this.setAffixStyle(e, {
        position: 'fixed',
        top,
        left: targetRect.left + elemOffset.left,
        maxHeight: `calc(100vh - ${top}px)`,
        width
      });
      this.setPlaceholderStyle({
        width,
        height: elemSize.height
      });
    } else if (
      scrollTop <= elemOffset.top + elemSize.height + (this.nzOffsetBottom as number) - targetInnerHeight &&
      offsetMode.bottom
    ) {
      const targetBottomOffet = targetNode === window ? 0 : window.innerHeight - targetRect.bottom!;
      const width = elemOffset.width;
      this.setAffixStyle(e, {
        position: 'fixed',
        bottom: targetBottomOffet + (this.nzOffsetBottom as number),
        left: targetRect.left + elemOffset.left,
        width
      });
      this.setPlaceholderStyle({
        width,
        height: elemOffset.height
      });
    } else {
      if (
        e.type === 'resize' &&
        this.affixStyle &&
        this.affixStyle.position === 'fixed' &&
        this.placeholderNode.offsetWidth
      ) {
        this.setAffixStyle(e, {
          ...this.affixStyle,
          width: this.placeholderNode.offsetWidth
        });
      } else {
        this.setAffixStyle(e);
      }
      this.setPlaceholderStyle();
    }

    if (e.type === 'resize') {
      this.syncPlaceholderStyle(e);
    }
  }
}