react-native-get-pixel_edit/node_modules/react-native/Libraries/IntersectionObserver/IntersectionObserver.js
2025-07-09 11:41:52 +09:00

253 lines
8.0 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
// flowlint unsafe-getters-setters:off
import type IntersectionObserverEntry from './IntersectionObserverEntry';
import type {IntersectionObserverId} from './IntersectionObserverManager';
import ReactNativeElement from '../DOM/Nodes/ReactNativeElement';
import * as IntersectionObserverManager from './IntersectionObserverManager';
export type IntersectionObserverCallback = (
entries: Array<IntersectionObserverEntry>,
observer: IntersectionObserver,
) => mixed;
type IntersectionObserverInit = {
// root?: ReactNativeElement, // This option exists on the Web but it's not currently supported in React Native.
// rootMargin?: string, // This option exists on the Web but it's not currently supported in React Native.
threshold?: number | $ReadOnlyArray<number>,
};
/**
* The [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
* provides a way to asynchronously observe changes in the intersection of a
* target element with an ancestor element or with a top-level document's
* viewport.
*
* The ancestor element or viewport is referred to as the root.
*
* When an `IntersectionObserver` is created, it's configured to watch for given
* ratios of visibility within the root.
*
* The configuration cannot be changed once the `IntersectionObserver` is
* created, so a given observer object is only useful for watching for specific
* changes in degree of visibility; however, you can watch multiple target
* elements with the same observer.
*
* This implementation only supports the `threshold` option at the moment
* (`root` and `rootMargin` are not supported).
*/
export default class IntersectionObserver {
_callback: IntersectionObserverCallback;
_thresholds: $ReadOnlyArray<number>;
_observationTargets: Set<ReactNativeElement> = new Set();
_intersectionObserverId: ?IntersectionObserverId;
constructor(
callback: IntersectionObserverCallback,
options?: IntersectionObserverInit,
): void {
if (callback == null) {
throw new TypeError(
"Failed to construct 'IntersectionObserver': 1 argument required, but only 0 present.",
);
}
if (typeof callback !== 'function') {
throw new TypeError(
"Failed to construct 'IntersectionObserver': parameter 1 is not of type 'Function'.",
);
}
// $FlowExpectedError[prop-missing] it's not typed in React Native but exists on Web.
if (options?.root != null) {
throw new TypeError(
"Failed to construct 'IntersectionObserver': root is not supported",
);
}
// $FlowExpectedError[prop-missing] it's not typed in React Native but exists on Web.
if (options?.rootMargin != null) {
throw new TypeError(
"Failed to construct 'IntersectionObserver': rootMargin is not supported",
);
}
this._callback = callback;
this._thresholds = normalizeThresholds(options?.threshold);
}
/**
* The `ReactNativeElement` whose bounds are used as the bounding box when
* testing for intersection.
* If no `root` value was passed to the constructor or its value is `null`,
* the root view is used.
*
* NOTE: This cannot currently be configured and `root` is always `null`.
*/
get root(): ReactNativeElement | null {
return null;
}
/**
* String with syntax similar to that of the CSS `margin` property.
* Each side of the rectangle represented by `rootMargin` is added to the
* corresponding side in the root element's bounding box before the
* intersection test is performed.
*
* NOTE: This cannot currently be configured and `rootMargin` is always
* `null`.
*/
get rootMargin(): string {
return '0px 0px 0px 0px';
}
/**
* A list of thresholds, sorted in increasing numeric order, where each
* threshold is a ratio of intersection area to bounding box area of an
* observed target.
* Notifications for a target are generated when any of the thresholds are
* crossed for that target.
* If no value was passed to the constructor, `0` is used.
*/
get thresholds(): $ReadOnlyArray<number> {
return this._thresholds;
}
/**
* Adds an element to the set of target elements being watched by the
* `IntersectionObserver`.
* One observer has one set of thresholds and one root, but can watch multiple
* target elements for visibility changes.
* To stop observing the element, call `IntersectionObserver.unobserve()`.
*/
observe(target: ReactNativeElement): void {
if (!(target instanceof ReactNativeElement)) {
throw new TypeError(
"Failed to execute 'observe' on 'IntersectionObserver': parameter 1 is not of type 'ReactNativeElement'.",
);
}
if (this._observationTargets.has(target)) {
return;
}
IntersectionObserverManager.observe({
intersectionObserverId: this._getOrCreateIntersectionObserverId(),
target,
});
this._observationTargets.add(target);
}
/**
* Instructs the `IntersectionObserver` to stop observing the specified target
* element.
*/
unobserve(target: ReactNativeElement): void {
if (!(target instanceof ReactNativeElement)) {
throw new TypeError(
"Failed to execute 'unobserve' on 'IntersectionObserver': parameter 1 is not of type 'ReactNativeElement'.",
);
}
if (!this._observationTargets.has(target)) {
return;
}
const intersectionObserverId = this._intersectionObserverId;
if (intersectionObserverId == null) {
// This is unexpected if the target is in `_observationTargets`.
console.error(
"Unexpected state in 'IntersectionObserver': could not find observer ID to unobserve target.",
);
return;
}
IntersectionObserverManager.unobserve(intersectionObserverId, target);
this._observationTargets.delete(target);
if (this._observationTargets.size === 0) {
IntersectionObserverManager.unregisterObserver(intersectionObserverId);
this._intersectionObserverId = null;
}
}
/**
* Stops watching all of its target elements for visibility changes.
*/
disconnect(): void {
for (const target of this._observationTargets.keys()) {
this.unobserve(target);
}
}
_getOrCreateIntersectionObserverId(): IntersectionObserverId {
let intersectionObserverId = this._intersectionObserverId;
if (intersectionObserverId == null) {
intersectionObserverId = IntersectionObserverManager.registerObserver(
this,
this._callback,
);
this._intersectionObserverId = intersectionObserverId;
}
return intersectionObserverId;
}
// Only for tests
__getObserverID(): ?IntersectionObserverId {
return this._intersectionObserverId;
}
}
/**
* Converts the user defined `threshold` value into an array of sorted valid
* threshold options for `IntersectionObserver` (double ∈ [0, 1]).
*
* @example
* normalizeThresholds(0.5); // → [0.5]
* normalizeThresholds([1, 0.5, 0]); // → [0, 0.5, 1]
* normalizeThresholds(['1', '0.5', '0']); // → [0, 0.5, 1]
*/
function normalizeThresholds(threshold: mixed): $ReadOnlyArray<number> {
if (Array.isArray(threshold)) {
if (threshold.length > 0) {
return threshold.map(normalizeThresholdValue).sort();
} else {
return [0];
}
}
return [normalizeThresholdValue(threshold)];
}
function normalizeThresholdValue(threshold: mixed): number {
if (threshold == null) {
return 0;
}
const thresholdAsNumber = Number(threshold);
if (!Number.isFinite(thresholdAsNumber)) {
throw new TypeError(
"Failed to read the 'threshold' property from 'IntersectionObserverInit': The provided double value is non-finite.",
);
}
if (thresholdAsNumber < 0 || thresholdAsNumber > 1) {
throw new RangeError(
"Failed to construct 'IntersectionObserver': Threshold values must be numbers between 0 and 1",
);
}
return thresholdAsNumber;
}