UNPKG

@lynx-js/web-core

Version:

This is an internal experimental package, do not use

225 lines 10.7 kB
/* * Copyright 2021-2024 The Lynx Authors. All rights reserved. * Licensed under the Apache License Version 2.0 that can be found in the * LICENSE file in the root directory of this source tree. */ import { scrollContainerDom } from '../../constants.js'; import { convertLengthToPx } from './utils/convertLengthToPx.js'; export class ExposureServices { #exposureEnabledElementsToIntersectionObserver = new Map(); #exposureEnabledElementsToOldExposureIdAttributeValue = new Map(); #globalExposureEventCache = []; #globalDisexposureEventCache = []; #globalExposureEventBatchTimer = null; /** * The elements that are currently exposed * We only send the event when the element enters or leaves the exposed state */ #exposedElements = new Set(); /** * note that this flag only affects the global exposure events. * The uiappear/uidisappear events are always dispatched when the element enters or leaves the viewport */ #isExposureServiceOn = true; #lynxViewInstance; constructor(lynxViewInstance) { this.#lynxViewInstance = lynxViewInstance; } /** * diff the current exposure enabled elements with the previous ones, and start/stop IntersectionObserver accordingly * If an element's exposure-id attribute has changed, we also need to send a new disexposure event with the old one */ updateExposureStatus(elementsToBeEnabled, elementsToBeDisabled) { const elementsToBeEnabledSet = new Set(elementsToBeEnabled); // start observing newly enabled elements for (const element of elementsToBeEnabledSet.values()) { if (this.#exposureEnabledElementsToIntersectionObserver.has(element)) { this.#stopIntersectionObserver(element); } this.#startIntersectionObserver(element); } const elementsToBeDisabledSet = new Set(elementsToBeDisabled); // stop observing newly disabled elements for (const element of elementsToBeDisabledSet.values()) { this.#stopIntersectionObserver(element); } } #IntersectionObserverEventHandler = (entries) => { entries.forEach(({ target, isIntersecting }) => { if (isIntersecting && !this.#exposedElements.has(target)) { this.#sendExposureEvent(target, true, null, true); this.#exposedElements.add(target); } else if (!isIntersecting && this.#exposedElements.has(target)) { this.#sendExposureEvent(target, false, null, true); this.#exposedElements.delete(target); } }); }; #stopIntersectionObserver(element) { const intersectionObserver = this .#exposureEnabledElementsToIntersectionObserver.get(element); if (intersectionObserver) { const oldExposureId = this .#exposureEnabledElementsToOldExposureIdAttributeValue.get(element); intersectionObserver.unobserve(element); intersectionObserver.disconnect(); this.#exposureEnabledElementsToIntersectionObserver.delete(element); this.#exposureEnabledElementsToOldExposureIdAttributeValue.delete(element); const currentExposureId = element.getAttribute('exposure-id'); if (oldExposureId != null && currentExposureId !== oldExposureId) { this.#sendExposureEvent(element, false, oldExposureId, false); } } this.#exposedElements.delete(element); } #startIntersectionObserver(target) { const threshold = parseFloat(target.getAttribute('exposure-area') ?? '0') / 100; const screenMarginTop = convertLengthToPx(target, target.getAttribute('exposure-screen-margin-top')); const screenMarginRight = convertLengthToPx(target, target.getAttribute('exposure-screen-margin-right')); const screenMarginBottom = convertLengthToPx(target, target.getAttribute('exposure-screen-margin-bottom')); const screenMarginLeft = convertLengthToPx(target, target.getAttribute('exposure-screen-margin-left')); const uiMarginTop = convertLengthToPx(target, target.getAttribute('exposure-ui-margin-top')); const uiMarginRight = convertLengthToPx(target, target.getAttribute('exposure-ui-margin-right')); const uiMarginBottom = convertLengthToPx(target, target.getAttribute('exposure-ui-margin-bottom')); const uiMarginLeft = convertLengthToPx(target, target.getAttribute('exposure-ui-margin-left')); /** * TODO: @haoyang.wang support the switch `enableExposureUIMargin` */ const calcedRootMarginTop = (uiMarginBottom ? -1 : 1) * (screenMarginTop - uiMarginBottom); const calcedRootMarginRight = (uiMarginLeft ? -1 : 1) * (screenMarginRight - uiMarginLeft); const calcedRootMarginBottom = (uiMarginTop ? -1 : 1) * (screenMarginBottom - uiMarginTop); const calcedRootMarginLeft = (uiMarginRight ? -1 : 1) * (screenMarginLeft - uiMarginRight); // get the parent scroll container let root = target.parentElement; while (root) { // @ts-expect-error if (root[scrollContainerDom]) { // @ts-expect-error root = root[scrollContainerDom]; break; } else { root = root.parentElement; } } const rootContainer = root ?? this.#lynxViewInstance.rootDom.parentElement; const intersectionObserver = new IntersectionObserver(this.#IntersectionObserverEventHandler, { rootMargin: `${calcedRootMarginTop}px ${calcedRootMarginRight}px ${calcedRootMarginBottom}px ${calcedRootMarginLeft}px`, root: rootContainer, threshold, }); intersectionObserver.observe(target); this.#exposureEnabledElementsToIntersectionObserver.set(target, intersectionObserver); const currentExposureId = target.getAttribute('exposure-id'); if (currentExposureId != null) { this.#exposureEnabledElementsToOldExposureIdAttributeValue.set(target, currentExposureId); } } #sendExposureEvent(target, isIntersecting, exposureId, /** * Whether to send the uiappear/uidisappear event * If the exposure service is turned from off to on, we may not want to send the appear events for all currently exposed elements */ sendAppearEvent) { exposureId = exposureId ?? target.getAttribute('exposure-id'); const exposureScene = target.getAttribute('exposure-scene') ?? ''; const uniqueId = this.#lynxViewInstance.mainThreadGlobalThis .__GetElementUniqueID(target); const detail = { 'unique-id': uniqueId, exposureID: exposureId, exposureScene, 'exposure-id': exposureId, 'exposure-scene': exposureScene, }; if (sendAppearEvent) { const appearEvent = new CustomEvent(isIntersecting ? 'uiappear' : 'uidisappear', { bubbles: false, composed: false, cancelable: true, detail, }); target.dispatchEvent(appearEvent); } const serializedTargetInfo = this.#lynxViewInstance.mtsWasmBinding .generateTargetObject(target, this.#lynxViewInstance.mainThreadGlobalThis.__GetDataset(target) ?? {}); const globalEvent = { dataset: serializedTargetInfo.dataset, ...detail, type: isIntersecting ? 'exposure' : 'disexposure', target: serializedTargetInfo, currentTarget: serializedTargetInfo, detail: { ...detail, 'unique-id': 0, }, timestamp: Date.now(), }; if (isIntersecting) { this.#globalExposureEventCache.push(globalEvent); } else { this.#globalDisexposureEventCache.push(globalEvent); } if (!this.#globalExposureEventBatchTimer) { this.#globalExposureEventBatchTimer = setTimeout(() => { if (this.#globalExposureEventCache.length > 0 || this.#globalDisexposureEventCache.length > 0) { const currentExposureEvents = this.#globalExposureEventCache; const currentDisexposureEvents = this.#globalDisexposureEventCache; this.#globalExposureEventCache = []; this.#globalDisexposureEventCache = []; if (currentExposureEvents.length > 0) { this.#lynxViewInstance.backgroundThread?.sendGlobalEvent('exposure', [ currentExposureEvents, ]); } if (currentDisexposureEvents.length > 0) { this.#lynxViewInstance.backgroundThread?.sendGlobalEvent('disexposure', [ currentDisexposureEvents, ]); } } this.#globalExposureEventBatchTimer = null; }, 1000 / 20); } } switchExposureService(toEnable, sendEvent) { if (toEnable && !this.#isExposureServiceOn) { // send all onScreen info this.#exposedElements.forEach((element) => { this.#sendExposureEvent(element, true, element.getAttribute('exposure-id'), false); }); } else if (!toEnable && this.#isExposureServiceOn) { if (sendEvent) { this.#exposedElements.forEach((element) => { this.#sendExposureEvent(element, false, element.getAttribute('exposure-id'), false); }); } } this.#isExposureServiceOn = toEnable; } dispose() { this.#exposureEnabledElementsToIntersectionObserver.forEach((observer) => { observer.disconnect(); }); this.#exposureEnabledElementsToIntersectionObserver.clear(); this.#exposureEnabledElementsToOldExposureIdAttributeValue.clear(); this.#exposedElements.clear(); if (this.#globalExposureEventBatchTimer) { clearTimeout(this.#globalExposureEventBatchTimer); this.#globalExposureEventBatchTimer = null; } this.#globalExposureEventCache = []; this.#globalDisexposureEventCache = []; } } //# sourceMappingURL=ExposureServices.js.map