工具函数
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 获取 Observable
const 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 相关属性则为 true
export 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 replacement
Object.assign({}, event, { target: document.body }) as Event
)));
let observables = [windowScroll];
// walk the parents and subscribe to all the scroll events we can
while (node.parentElement && node !== document.body) {
if (isScrollableElement(node)) {
observables.push(fromEvent(node, "scroll", { passive: true }));
}
node = node.parentElement;
}
return merge(...observables);
};