@lynx-js/web-core
Version:
This is an internal experimental package, do not use
225 lines • 10.7 kB
JavaScript
/*
* 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