react-native
Version:
A framework for building native apps using React
368 lines (306 loc) • 9.73 kB
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.
*
* @format
* @flow strict-local
*/
// flowlint unsafe-getters-setters:off
import type {
InternalInstanceHandle,
Node as ShadowNode,
} from '../../../../../Libraries/Renderer/shims/ReactNativeTypes';
import type NodeList from '../oldstylecollections/NodeList';
import type ReadOnlyElement from './ReadOnlyElement';
import {createNodeList} from '../oldstylecollections/NodeList';
import NativeDOM from './specs/NativeDOM';
// We initialize this lazily to avoid a require cycle
// (`ReadOnlyElement` also depends on `ReadOnlyNode`).
let ReadOnlyElementClass: Class<ReadOnlyElement>;
export default class ReadOnlyNode {
constructor(internalInstanceHandle: InternalInstanceHandle) {
// This constructor is inlined in `ReactNativeElement` so if you modify
// this make sure that their implementation stays in sync.
setInstanceHandle(this, internalInstanceHandle);
}
get childNodes(): NodeList<ReadOnlyNode> {
const childNodes = getChildNodes(this);
return createNodeList(childNodes);
}
get firstChild(): ReadOnlyNode | null {
const childNodes = getChildNodes(this);
if (childNodes.length === 0) {
return null;
}
return childNodes[0];
}
get isConnected(): boolean {
const shadowNode = getShadowNode(this);
if (shadowNode == null) {
return false;
}
return NativeDOM.isConnected(shadowNode);
}
get lastChild(): ReadOnlyNode | null {
const childNodes = getChildNodes(this);
if (childNodes.length === 0) {
return null;
}
return childNodes[childNodes.length - 1];
}
get nextSibling(): ReadOnlyNode | null {
const [siblings, position] = getNodeSiblingsAndPosition(this);
if (position === siblings.length - 1) {
// this node is the last child of its parent, so there is no next sibling.
return null;
}
return siblings[position + 1];
}
/**
* @abstract
*/
get nodeName(): string {
throw new TypeError(
'`nodeName` is abstract and must be implemented in a subclass of `ReadOnlyNode`',
);
}
/**
* @abstract
*/
get nodeType(): number {
throw new TypeError(
'`nodeType` is abstract and must be implemented in a subclass of `ReadOnlyNode`',
);
}
/**
* @abstract
*/
get nodeValue(): string | null {
throw new TypeError(
'`nodeValue` is abstract and must be implemented in a subclass of `ReadOnlyNode`',
);
}
get parentElement(): ReadOnlyElement | null {
const parentNode = this.parentNode;
if (ReadOnlyElementClass == null) {
// We initialize this lazily to avoid a require cycle.
ReadOnlyElementClass = require('./ReadOnlyElement').default;
}
if (parentNode instanceof ReadOnlyElementClass) {
return parentNode;
}
return null;
}
get parentNode(): ReadOnlyNode | null {
const shadowNode = getShadowNode(this);
if (shadowNode == null) {
return null;
}
const parentInstanceHandle = NativeDOM.getParentNode(shadowNode);
if (parentInstanceHandle == null) {
return null;
}
return (
getPublicInstanceFromInternalInstanceHandle(parentInstanceHandle) ?? null
);
}
get previousSibling(): ReadOnlyNode | null {
const [siblings, position] = getNodeSiblingsAndPosition(this);
if (position === 0) {
// this node is the first child of its parent, so there is no previous sibling.
return null;
}
return siblings[position - 1];
}
/**
* @abstract
*/
get textContent(): string | null {
throw new TypeError(
'`textContent` is abstract and must be implemented in a subclass of `ReadOnlyNode`',
);
}
compareDocumentPosition(otherNode: ReadOnlyNode): number {
// Quick check to avoid having to call into Fabric if the nodes are the same.
if (otherNode === this) {
return 0;
}
const shadowNode = getShadowNode(this);
const otherShadowNode = getShadowNode(otherNode);
if (shadowNode == null || otherShadowNode == null) {
return ReadOnlyNode.DOCUMENT_POSITION_DISCONNECTED;
}
return NativeDOM.compareDocumentPosition(shadowNode, otherShadowNode);
}
contains(otherNode: ReadOnlyNode): boolean {
if (otherNode === this) {
return true;
}
const position = this.compareDocumentPosition(otherNode);
// eslint-disable-next-line no-bitwise
return (position & ReadOnlyNode.DOCUMENT_POSITION_CONTAINED_BY) !== 0;
}
getRootNode(): ReadOnlyNode {
// eslint-disable-next-line consistent-this
let lastKnownParent: ReadOnlyNode = this;
let nextPossibleParent: ?ReadOnlyNode = this.parentNode;
while (nextPossibleParent != null) {
lastKnownParent = nextPossibleParent;
nextPossibleParent = nextPossibleParent.parentNode;
}
return lastKnownParent;
}
hasChildNodes(): boolean {
return getChildNodes(this).length > 0;
}
/*
* Node types, as returned by the `nodeType` property.
*/
/**
* Type of Element, HTMLElement and ReactNativeElement instances.
*/
static ELEMENT_NODE: number = 1;
/**
* Currently Unused in React Native.
*/
static ATTRIBUTE_NODE: number = 2;
/**
* Text nodes.
*/
static TEXT_NODE: number = 3;
/**
* @deprecated Unused in React Native.
*/
static CDATA_SECTION_NODE: number = 4;
/**
* @deprecated
*/
static ENTITY_REFERENCE_NODE: number = 5;
/**
* @deprecated
*/
static ENTITY_NODE: number = 6;
/**
* @deprecated Unused in React Native.
*/
static PROCESSING_INSTRUCTION_NODE: number = 7;
/**
* @deprecated Unused in React Native.
*/
static COMMENT_NODE: number = 8;
/**
* @deprecated Unused in React Native.
*/
static DOCUMENT_NODE: number = 9;
/**
* @deprecated Unused in React Native.
*/
static DOCUMENT_TYPE_NODE: number = 10;
/**
* @deprecated Unused in React Native.
*/
static DOCUMENT_FRAGMENT_NODE: number = 11;
/**
* @deprecated
*/
static NOTATION_NODE: number = 12;
/*
* Document position flags. Used to check the return value of
* `compareDocumentPosition()`.
*/
/**
* Both nodes are in different documents.
*/
static DOCUMENT_POSITION_DISCONNECTED: number = 1;
/**
* `otherNode` precedes the node in either a pre-order depth-first traversal of a tree containing both
* (e.g., as an ancestor or previous sibling or a descendant of a previous sibling or previous sibling of an ancestor)
* or (if they are disconnected) in an arbitrary but consistent ordering.
*/
static DOCUMENT_POSITION_PRECEDING: number = 2;
/**
* `otherNode` follows the node in either a pre-order depth-first traversal of a tree containing both
* (e.g., as a descendant or following sibling or a descendant of a following sibling or following sibling of an ancestor)
* or (if they are disconnected) in an arbitrary but consistent ordering.
*/
static DOCUMENT_POSITION_FOLLOWING: number = 4;
/**
* `otherNode` is an ancestor of the node.
*/
static DOCUMENT_POSITION_CONTAINS: number = 8;
/**
* `otherNode` is a descendant of the node.
*/
static DOCUMENT_POSITION_CONTAINED_BY: number = 16;
/**
* @deprecated Unused in React Native.
*/
static DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: number = 32;
}
const INSTANCE_HANDLE_KEY = Symbol('internalInstanceHandle');
export function getInstanceHandle(node: ReadOnlyNode): InternalInstanceHandle {
// $FlowExpectedError[prop-missing]
return node[INSTANCE_HANDLE_KEY];
}
export function setInstanceHandle(
node: ReadOnlyNode,
instanceHandle: InternalInstanceHandle,
): void {
// $FlowExpectedError[prop-missing]
node[INSTANCE_HANDLE_KEY] = instanceHandle;
}
let RendererProxy;
function getRendererProxy() {
if (RendererProxy == null) {
// Lazy import Fabric here to avoid DOM Node APIs classes from having side-effects.
// With a static import we can't use these classes for Paper-only variants.
RendererProxy = require('../../../../../Libraries/ReactNative/RendererProxy');
}
return RendererProxy;
}
export function getShadowNode(node: ReadOnlyNode): ?ShadowNode {
return getRendererProxy().getNodeFromInternalInstanceHandle(
getInstanceHandle(node),
);
}
export function getChildNodes(
node: ReadOnlyNode,
): $ReadOnlyArray<ReadOnlyNode> {
const shadowNode = getShadowNode(node);
if (shadowNode == null) {
return [];
}
const childNodeInstanceHandles = NativeDOM.getChildNodes(shadowNode);
return childNodeInstanceHandles
.map(instanceHandle =>
getPublicInstanceFromInternalInstanceHandle(instanceHandle),
)
.filter(Boolean);
}
function getNodeSiblingsAndPosition(
node: ReadOnlyNode,
): [$ReadOnlyArray<ReadOnlyNode>, number] {
const parent = node.parentNode;
if (parent == null) {
// This node is the root or it's disconnected.
return [[node], 0];
}
const siblings = getChildNodes(parent);
const position = siblings.indexOf(node);
if (position === -1) {
throw new TypeError("Missing node in parent's child node list");
}
return [siblings, position];
}
export function getPublicInstanceFromInternalInstanceHandle(
instanceHandle: InternalInstanceHandle,
): ?ReadOnlyNode {
const mixedPublicInstance =
getRendererProxy().getPublicInstanceFromInternalInstanceHandle(
instanceHandle,
);
// $FlowExpectedError[incompatible-return] React defines public instances as "mixed" because it can't access the definition from React Native.
return mixedPublicInstance;
}