Source: dom/DOMContainer.ts

import { Point } from '../maths/point/Point';
import { ViewContainer, type ViewContainerOptions } from '../scene/view/ViewContainer';

import type { PointData } from '../maths/point/PointData';

/**
 * Options for the DOMContainer constructor.
 * @memberof scene
 */
export interface DOMContainerOptions extends ViewContainerOptions
{
    /** The DOM element to use for the container. */
    element?: HTMLElement;
    /** The anchor point of the container. */
    anchor?: PointData | number;
}

/**
 * The DOMContainer object is used to render DOM elements within the PixiJS scene graph.
 * It allows you to integrate HTML elements into your PixiJS application.
 *
 * DOMContainer is especially useful for rendering standard DOM elements
 * that handle user input, such as `<input>` or `<textarea>`.
 * This is often simpler and more flexible than trying to implement text input
 * directly in PixiJS. For instance, if you need text fields or text areas,
 * you can embed them through this container for native browser text handling.
 *
 * --------- EXPERIMENTAL ---------
 *
 * This is a new API, things may change and it may not work as expected.
 * We want to hear your feedback as we go!
 *
 * --------------------------------
 * @example
 * ```js
 * import { DOMContainer } from 'pixi.js';
 *
 * const element = document.createElement('div');
 * element.innerHTML = 'Hello World!';
 *
 * const domContainer = new DOMContainer({ element });
 * ```
 * @memberof scene
 * @extends scene.ViewContainer
 */
export class DOMContainer extends ViewContainer
{
    /** @private */
    public override readonly renderPipeId: string = 'dom';

    /** @private */
    public batched = false;
    /**
     * The anchor point of the container.
     * @private
     */
    public readonly _anchor: Point;

    /** The DOM element that this container is using. */
    private _element: HTMLElement;

    /**
     * @param options - The options for creating the DOM container.
     */
    constructor(options: DOMContainerOptions = {})
    {
        const { element, anchor, ...rest } = options;

        super({
            label: 'DOMContainer',
            ...rest
        });

        this._anchor = new Point(0, 0);

        if (anchor)
        {
            this.anchor = anchor;
        }

        this.element = options.element || document.createElement('div');
    }

    /**
     * The anchor sets the origin point of the container.
     * The default is `(0,0)`, this means the container's origin is the top left.
     *
     * Setting the anchor to `(0.5,0.5)` means the container's origin is centered.
     * Setting the anchor to `(1,1)` would mean the container's origin point will be the bottom right corner.
     *
     * If you pass only single parameter, it will set both x and y to the same value as shown in the example below.
     */
    get anchor(): Point
    {
        return this._anchor;
    }

    set anchor(value: PointData | number)
    {
        typeof value === 'number' ? this._anchor.set(value) : this._anchor.copyFrom(value);
    }

    set element(value: HTMLElement)
    {
        if (this._element === value) return;

        this._element = value;
        this.onViewUpdate();
    }

    /** The DOM element that this container is using. */
    get element(): HTMLElement
    {
        return this._element;
    }

    /** @private */
    protected updateBounds()
    {
        const bounds = this._bounds;
        const element = this._element;

        if (!element)
        {
            bounds.minX = 0;
            bounds.minY = 0;
            bounds.maxX = 0;
            bounds.maxY = 0;

            return;
        }

        const { offsetWidth, offsetHeight } = element;

        bounds.minX = 0;
        bounds.maxX = offsetWidth;
        bounds.minY = 0;
        bounds.maxY = offsetHeight;
    }

    /**
     * Destroys this DOM container.
     * @param options - Options parameter. A boolean will act as if all options
     *  have been set to that value
     */
    public override destroy(options: boolean = false)
    {
        super.destroy(options);

        this._element?.parentNode?.removeChild(this._element);
        this._element = null;
        (this._anchor as null) = null;
    }
}