/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
extensions/amp-analytics/0.1/scroll-manager.js
203 строки
6 KB
Jake Fried
♻️ amp lint --fix: sort/order (#37353)
12 янв 2022, 22:24
Не верифицирован
12 янв 2022, 22:24
8c34f2d
Код
Авторство
О чём код?
import {Observable} from '#core/data-structures/observable'; import {Services} from '#service'; import {devAssert} from '#utils/log'; /** * @typedef {{ * top: number, * left: number, * width: number, * height: number, * scrollHeight: number, * scrollWidth: number, * initialSize: { * scrollHeight: number, * scrollWidth: number * } * }} */ export let ScrollEventDef; /** * A manager for handling multiple Scroll Event Trackers. * The instance of this class corresponds 1:1 to `AnalyticsRoot`. It represents * a collection of all scroll triggers declared within the `AnalyticsRoot`. * @implements {../../../src/service.Disposable} */ export class ScrollManager { /** * @param {!./analytics-root.AnalyticsRoot} root */ constructor(root) { /** @const @private {!../../../src/service/viewport/viewport-interface.ViewportInterface} */ this.viewport_ = Services.viewportForDoc(root.ampdoc); /** @const @private {!../../../src/service/mutator-interface.MutatorInterface} */ this.mutator_ = Services.mutatorForDoc(root.ampdoc); /** @private {?UnlistenDef} */ this.viewportOnChangedUnlistener_ = null; /** @private {!Observable<!./scroll-manager.ScrollEventDef>} */ this.scrollObservable_ = new Observable(); /** @const @private {!Element} */ this.root_ = root.getRootElement(); /** @private {?Promise} */ this.initialRootRectPromise_ = null; } /** * Function to dispose of all handlers on the scroll observable */ dispose() { this.scrollObservable_.removeAll(); this.removeViewportOnChangedListener_(); } /** * @param {function(!Object)} handler */ removeScrollHandler(handler) { this.scrollObservable_.remove(handler); if (this.scrollObservable_.getHandlerCount() <= 0) { this.removeViewportOnChangedListener_(); } } /** * @param {function(!Object)} handler * @return {!UnlistenDef} */ addScrollHandler(handler) { // Trigger an event to fire events that might have already happened. const size = this.viewport_.getSize(); this.getInitRootElementRect_().then((initRootElementRect) => { // In the case of shadow/embedded documents, the root element's // layoutRect is relative to the parent doc's origin const { height: scrollHeight, left: scrollLeft, top: scrollTop, width: scrollWidth, } = initRootElementRect; /** {./scroll-manager.ScrollEventDef} */ const scrollEvent = { // In the case of shadow documents (e.g. amp-next-page), we offset // the event's top and left coordinates by the top/left position of // the document's container element (so that scroll triggers become relative to // container instead of the top-level host page). In the case of a top-level // page, the container/root is the document body so scrollTop and scrollLeft // are both 0 and the measurements are not affected top: this.viewport_.getScrollTop() - scrollTop, left: this.viewport_.getScrollLeft() - scrollLeft, width: size.width, height: size.height, scrollHeight, scrollWidth, initialSize: {scrollHeight, scrollWidth}, }; handler(scrollEvent); }); if (this.scrollObservable_.getHandlerCount() === 0) { this.addViewportOnChangedListener_(); } return this.scrollObservable_.add(handler); } /** * @param {!../../../src/service/viewport/viewport-interface.ViewportChangedEventDef} e * @return {!Promise} * @private */ onScroll_(e) { return Promise.all([ // Initial root layout rectangle this.getInitRootElementRect_(), // Current root layout rectangle this.measureRootElement_(), ]).then((rects) => { // Initial root layout rectangle const {height: initialScrollHeight, width: initialScrollWidth} = rects[0]; // Current root layout rectangle const { height: scrollHeight, left: scrollLeft, top: scrollTop, width: scrollWidth, } = rects[1]; /** {./scroll-manager.ScrollEventDef} */ const scrollEvent = { // In the case of shadow documents (e.g. amp-next-page), we offset // the event's top and left coordinates by the top/left position of // the document's container element (so that scroll triggers become relative to // container instead of the top-level host page). In the case of a top-level // page, the container/root is the document body so scrollTop and scrollLeft // are both 0 and the measurements are not affected top: e.top - scrollTop, left: e.left - scrollLeft, width: e.width, height: e.height, scrollWidth, scrollHeight, initialSize: { scrollHeight: initialScrollHeight, scrollWidth: initialScrollWidth, }, }; // Fire all of our children scroll observables this.scrollObservable_.fire(scrollEvent); }); } /** * Function to remove the viewport onChanged listener * @private */ removeViewportOnChangedListener_() { if (this.viewportOnChangedUnlistener_) { this.viewportOnChangedUnlistener_(); this.viewportOnChangedUnlistener_ = null; } } /** * Function to add the viewport onChanged listener * @private */ addViewportOnChangedListener_() { this.viewportOnChangedUnlistener_ = this.viewport_.onChanged( this.onScroll_.bind(this) ); } /** * Gets the cached layout rectangle of the root element * @return {!Promise<!../../../src/layout-rect.LayoutRectDef>} */ getInitRootElementRect_() { return devAssert( this.initialRootRectPromise_ || this.measureRootElement_() ); } /** * Gets the layout rectangle of the root element * @return {!Promise<!../../../src/layout-rect.LayoutRectDef>} */ measureRootElement_() { const rectPromise = this.mutator_.measureElement(() => this.viewport_.getLayoutRect(this.root_) ); this.initialRootRectPromise_ = this.initialRootRectPromise_ || rectPromise; return rectPromise; } }