工具函数
Code
import { Injectable, OnDestroy } from "@angular/core";import { Observable, Subscription } from "rxjs";import { EventHandler } from "./types";import { getEventObservable } from "./event-observable";@Injectable()export class DocumentService implements OnDestroy {// 这里声明了全局事件的变量protected globalEvents = new Map<string, Observable<Event>>();protected documentRef = document;// 订阅集合protected subscriptions = new Subscription();handleEvent(eventType: string, callback: EventHandler) {// 当 globalEvents 中没有该事件时再添加if (!this.globalEvents.has(eventType)) {if (this.documentRef) {this.globalEvents.set(eventType, getEventObservable(this.documentRef as any, eventType));} else {this.globalEvents.set(eventType, new Observable());}}// 根据 eventType 获取 Observableconst observable = this.globalEvents.get(eventType);// 向 订阅集合中 添加该事件订阅this.subscriptions.add(observable.subscribe(callback));}// 这个方法,相当于绑定 click 事件的快捷方式handleClick(callback: EventHandler) {this.handleEvent("click", callback);}ngOnDestroy() {this.subscriptions.unsubscribe();this.globalEvents = null;}}
import { Observable, fromEvent } from "rxjs";// 这里的事件监听用的是 rxjs 的 fromEvent,和 rxjs 深度结合export const getEventObservable = (targetElement: HTMLElement | Element, eventType: string): Observable<Event> => {switch (eventType) {case "scroll":case "resize":case "touchstart":case "touchmove":case "touchend":// https://stackoverflow.com/questions/37721782/what-are-passive-event-listeners// https://github.com/ReactiveX/rxjs/pull/1845/files#diff-b66649012c6589d424eccfd864157c59R57// 这里的 passive 是优化性能的操作return fromEvent(targetElement, eventType, { passive: true });default:return fromEvent(targetElement, eventType);}};
import { map } from "rxjs/operators";import { fromEvent, merge, Observable } from "rxjs";/*** Checks if a given element is scrollable.* If the element has an overflow set as part of its computed style it can scroll.* @param element the element to check scrollability*/// 判断传入的元素是否为滚动元素,如果他的属性中包含overflow 相关属性则为 trueexport const isScrollableElement = (element: HTMLElement) => {const computedStyle = getComputedStyle(element);return (computedStyle.overflow === "auto" ||computedStyle.overflow === "scroll" ||computedStyle["overflow-y"] === "auto" ||computedStyle["overflow-y"] === "scroll" ||computedStyle["overflow-x"] === "auto" ||computedStyle["overflow-x"] === "scroll");};/*** Checks if an element is visible within a container* @param element the element to check* @param container the container to check*/export const isVisibleInContainer = (element: HTMLElement, container: HTMLElement) => {const elementRect = element.getBoundingClientRect();const containerRect = container.getBoundingClientRect();// If there exists `height: 100%` on the `html` or `body` tag of an application,// it causes the calculation to return true if you need to scroll before the element is seen.// In that case we calculate its visibility based on the window viewport.if (container.tagName === "BODY" || container.tagName === "HTML") {// This checks if element is within the top, bottom, left and right of viewport, ie. if the element is visible in// the screen. This also takes into account partial visibility of an element.const isAboveViewport = elementRect.top < 0 && (elementRect.top + element.clientHeight) < 0;const isLeftOfViewport = elementRect.left < 0;const isBelowViewport =(elementRect.bottom - element.clientHeight) > (window.innerHeight || document.documentElement.clientHeight);const isRightOfViewport = elementRect.right > (window.innerWidth || document.documentElement.clientWidth);const isVisibleInViewport = !(isAboveViewport || isBelowViewport || isLeftOfViewport || isRightOfViewport);return isVisibleInViewport;}return (// This also accounts for partial visibility. It will still return true if the element is partially visible inside the container.(elementRect.bottom - element.clientHeight) <= (containerRect.bottom + (container.offsetHeight - container.clientHeight) / 2) &&elementRect.top >= (- element.clientHeight));};// 获得滚动的父元素export const getScrollableParents = (node: HTMLElement) => {const elements = [document.body];// 这里只要有父级,切父级不是 body 元素则继续循环while (node.parentElement && node !== document.body) {if (isScrollableElement(node)) {elements.push(node);}// 将当前的 node 设置为父元素,层层向上遍历node = node.parentElement;}return elements;};// 判断是否有可滚动的父元素export const hasScrollableParents = (node: HTMLElement) => {while (node.parentElement && node !== document.body) {if (isScrollableElement(node)) {return true;}node = node.parentElement;}return false;};/*** Returns an observable that emits whenever any scrollable parent element scrolls** @param node root element to start finding scrolling parents from*/// 获取传入元素的可滚动的父元素的订阅export const scrollableParentsObservable = (node: HTMLElement): Observable<Event> => {const windowScroll = fromEvent(window, "scroll", { passive: true }).pipe(map(event => (// update the event target to be something useful. In this case `body` is a sensible replacementObject.assign({}, event, { target: document.body }) as Event)));let observables = [windowScroll];// walk the parents and subscribe to all the scroll events we canwhile (node.parentElement && node !== document.body) {if (isScrollableElement(node)) {observables.push(fromEvent(node, "scroll", { passive: true }));}node = node.parentElement;}return merge(...observables);};
