工具函数

Code

  1. import { Injectable, OnDestroy } from "@angular/core";
  2. import { Observable, Subscription } from "rxjs";
  3. import { EventHandler } from "./types";
  4. import { getEventObservable } from "./event-observable";
  5. @Injectable()
  6. export class DocumentService implements OnDestroy {
  7. // 这里声明了全局事件的变量
  8. protected globalEvents = new Map<string, Observable<Event>>();
  9. protected documentRef = document;
  10. // 订阅集合
  11. protected subscriptions = new Subscription();
  12. handleEvent(eventType: string, callback: EventHandler) {
  13. // 当 globalEvents 中没有该事件时再添加
  14. if (!this.globalEvents.has(eventType)) {
  15. if (this.documentRef) {
  16. this.globalEvents.set(eventType, getEventObservable(this.documentRef as any, eventType));
  17. } else {
  18. this.globalEvents.set(eventType, new Observable());
  19. }
  20. }
  21. // 根据 eventType 获取 Observable
  22. const observable = this.globalEvents.get(eventType);
  23. // 向 订阅集合中 添加该事件订阅
  24. this.subscriptions.add(observable.subscribe(callback));
  25. }
  26. // 这个方法,相当于绑定 click 事件的快捷方式
  27. handleClick(callback: EventHandler) {
  28. this.handleEvent("click", callback);
  29. }
  30. ngOnDestroy() {
  31. this.subscriptions.unsubscribe();
  32. this.globalEvents = null;
  33. }
  34. }
  1. import { Observable, fromEvent } from "rxjs";
  2. // 这里的事件监听用的是 rxjs 的 fromEvent,和 rxjs 深度结合
  3. export const getEventObservable = (targetElement: HTMLElement | Element, eventType: string): Observable<Event> => {
  4. switch (eventType) {
  5. case "scroll":
  6. case "resize":
  7. case "touchstart":
  8. case "touchmove":
  9. case "touchend":
  10. // https://stackoverflow.com/questions/37721782/what-are-passive-event-listeners
  11. // https://github.com/ReactiveX/rxjs/pull/1845/files#diff-b66649012c6589d424eccfd864157c59R57
  12. // 这里的 passive 是优化性能的操作
  13. return fromEvent(targetElement, eventType, { passive: true });
  14. default:
  15. return fromEvent(targetElement, eventType);
  16. }
  17. };
  1. import { map } from "rxjs/operators";
  2. import { fromEvent, merge, Observable } from "rxjs";
  3. /**
  4. * Checks if a given element is scrollable.
  5. * If the element has an overflow set as part of its computed style it can scroll.
  6. * @param element the element to check scrollability
  7. */
  8. // 判断传入的元素是否为滚动元素,如果他的属性中包含overflow 相关属性则为 true
  9. export const isScrollableElement = (element: HTMLElement) => {
  10. const computedStyle = getComputedStyle(element);
  11. return (
  12. computedStyle.overflow === "auto" ||
  13. computedStyle.overflow === "scroll" ||
  14. computedStyle["overflow-y"] === "auto" ||
  15. computedStyle["overflow-y"] === "scroll" ||
  16. computedStyle["overflow-x"] === "auto" ||
  17. computedStyle["overflow-x"] === "scroll"
  18. );
  19. };
  20. /**
  21. * Checks if an element is visible within a container
  22. * @param element the element to check
  23. * @param container the container to check
  24. */
  25. export const isVisibleInContainer = (element: HTMLElement, container: HTMLElement) => {
  26. const elementRect = element.getBoundingClientRect();
  27. const containerRect = container.getBoundingClientRect();
  28. // If there exists `height: 100%` on the `html` or `body` tag of an application,
  29. // it causes the calculation to return true if you need to scroll before the element is seen.
  30. // In that case we calculate its visibility based on the window viewport.
  31. if (container.tagName === "BODY" || container.tagName === "HTML") {
  32. // This checks if element is within the top, bottom, left and right of viewport, ie. if the element is visible in
  33. // the screen. This also takes into account partial visibility of an element.
  34. const isAboveViewport = elementRect.top < 0 && (elementRect.top + element.clientHeight) < 0;
  35. const isLeftOfViewport = elementRect.left < 0;
  36. const isBelowViewport =
  37. (elementRect.bottom - element.clientHeight) > (window.innerHeight || document.documentElement.clientHeight);
  38. const isRightOfViewport = elementRect.right > (window.innerWidth || document.documentElement.clientWidth);
  39. const isVisibleInViewport = !(isAboveViewport || isBelowViewport || isLeftOfViewport || isRightOfViewport);
  40. return isVisibleInViewport;
  41. }
  42. return (
  43. // This also accounts for partial visibility. It will still return true if the element is partially visible inside the container.
  44. (elementRect.bottom - element.clientHeight) <= (containerRect.bottom + (container.offsetHeight - container.clientHeight) / 2) &&
  45. elementRect.top >= (- element.clientHeight)
  46. );
  47. };
  48. // 获得滚动的父元素
  49. export const getScrollableParents = (node: HTMLElement) => {
  50. const elements = [document.body];
  51. // 这里只要有父级,切父级不是 body 元素则继续循环
  52. while (node.parentElement && node !== document.body) {
  53. if (isScrollableElement(node)) {
  54. elements.push(node);
  55. }
  56. // 将当前的 node 设置为父元素,层层向上遍历
  57. node = node.parentElement;
  58. }
  59. return elements;
  60. };
  61. // 判断是否有可滚动的父元素
  62. export const hasScrollableParents = (node: HTMLElement) => {
  63. while (node.parentElement && node !== document.body) {
  64. if (isScrollableElement(node)) {
  65. return true;
  66. }
  67. node = node.parentElement;
  68. }
  69. return false;
  70. };
  71. /**
  72. * Returns an observable that emits whenever any scrollable parent element scrolls
  73. *
  74. * @param node root element to start finding scrolling parents from
  75. */
  76. // 获取传入元素的可滚动的父元素的订阅
  77. export const scrollableParentsObservable = (node: HTMLElement): Observable<Event> => {
  78. const windowScroll = fromEvent(window, "scroll", { passive: true }).pipe(map(event => (
  79. // update the event target to be something useful. In this case `body` is a sensible replacement
  80. Object.assign({}, event, { target: document.body }) as Event
  81. )));
  82. let observables = [windowScroll];
  83. // walk the parents and subscribe to all the scroll events we can
  84. while (node.parentElement && node !== document.body) {
  85. if (isScrollableElement(node)) {
  86. observables.push(fromEvent(node, "scroll", { passive: true }));
  87. }
  88. node = node.parentElement;
  89. }
  90. return merge(...observables);
  91. };