control/entityTree/EntityTreeView.tstypescript
import { createEvents, type EventsHandler } from "../../Events";
import type { Entity } from "../../entity/Entity";
import { View, type IViewParams, type ViewEventsList } from "../../ui/View";

type EntityTreeViewEvents = ["select"];

const EVENT_NAMES: EntityTreeViewEvents = ["select"];

const TEMPLATE = `<div class="og-entityTree"></div>`;

export interface IEntityTreeViewParams extends IViewParams {
    model: Entity[];
    expandedByDefault?: boolean;
    expandedEntityIds?: Set<number>;
}

type EntityTreeViewEventsExt = EventsHandler<EntityTreeViewEvents> & EventsHandler<ViewEventsList>;

export class EntityTreeView extends View<Entity[]> {
    public override events: EntityTreeViewEventsExt;

    protected _expandedByDefault: boolean;
    protected _expandedEntityIds: Set<number>;

    constructor(params: IEntityTreeViewParams) {
        super({
            ...params,
            template: TEMPLATE,
            model: params.model || []
        });

        this.events = createEvents<EntityTreeViewEvents>(EVENT_NAMES) as EntityTreeViewEventsExt;
        this._expandedByDefault = params.expandedByDefault ?? false;
        this._expandedEntityIds = params.expandedEntityIds || new Set<number>();
    }

    public override render(params?: any): this {
        super.render(params);
        this._renderTree();
        return this;
    }

    public setEntities(entities: Entity[]): void {
        this.model = entities;
        this._renderTree();
    }

    protected _renderTree(): void {
        if (!this.el) return;
        this.el.innerHTML = "";
        this._renderEntities(this.model, this.el);
    }

    protected _renderEntities(entities: Entity[], parentNode: HTMLElement): void {
        for (let i = 0; i < entities.length; i++) {
            parentNode.appendChild(this._createEntityNode(entities[i]));
        }
    }

    protected _createEntityNode(entity: Entity): HTMLElement {
        const nodeEl = document.createElement("div");
        nodeEl.classList.add("og-entityTree__node");

        const rowEl = document.createElement("div");
        rowEl.classList.add("og-entityTree__row");
        nodeEl.appendChild(rowEl);

        const hasChildren = entity.childEntities.length !== 0;
        let expanded = hasChildren && (this._expandedByDefault || this._expandedEntityIds.has(entity.id));

        const toggleBtn = document.createElement("button");
        toggleBtn.type = "button";
        toggleBtn.classList.add("og-entityTree__toggleBtn");
        toggleBtn.classList.add(
            hasChildren ? "og-entityTree__toggleBtn_hasChildren" : "og-entityTree__toggleBtn_empty"
        );
        toggleBtn.disabled = entity.childEntities.length === 0;
        toggleBtn.textContent = hasChildren ? this._getToggleText(expanded) : "";
        rowEl.appendChild(toggleBtn);

        const entityBtn = document.createElement("button");
        entityBtn.type = "button";
        entityBtn.classList.add("og-entityTree__entityBtn");
        entityBtn.title = entity.name || `entity:${entity.id.toString()}`;

        const nameEl = document.createElement("span");
        nameEl.classList.add("og-entityTree__name");
        nameEl.textContent = entity.name || `entity:${entity.id.toString()}`;
        entityBtn.appendChild(nameEl);

        const typeEl = document.createElement("span");
        typeEl.classList.add("og-entityTree__type");
        typeEl.textContent = this._getEntityTypes(entity);
        entityBtn.appendChild(typeEl);

        rowEl.appendChild(entityBtn);

        const childrenEl = document.createElement("div");
        childrenEl.classList.add("og-entityTree__children");
        if (hasChildren && !expanded) {
            childrenEl.classList.add("og-entityTree__children_hidden");
        }
        nodeEl.appendChild(childrenEl);

        if (hasChildren) {
            this._renderEntities(entity.childEntities, childrenEl);

            toggleBtn.addEventListener("click", (e: MouseEvent) => {
                e.stopPropagation();
                expanded = !expanded;
                toggleBtn.textContent = this._getToggleText(expanded);
                childrenEl.classList.toggle("og-entityTree__children_hidden", !expanded);
            });
        }

        entityBtn.addEventListener("click", (e: MouseEvent) => {
            e.stopPropagation();
            this.events.dispatch(this.events.select, entity, this);
        });

        return nodeEl;
    }

    protected _getToggleText(isExpanded: boolean): string {
        return isExpanded ? "▾" : "▸";
    }

    protected _getEntityTypes(entity: Entity): string {
        const entityTypes: string[] = [];

        entity.billboard && entityTypes.push("billboard");
        entity.label && entityTypes.push("label");
        entity.geoObject && entityTypes.push("geoObject");
        entity.geometry && entityTypes.push("geometry");
        entity.polyline && entityTypes.push("polyline");
        entity.ray && entityTypes.push("ray");
        entity.pointCloud && entityTypes.push("pointCloud");
        entity.strip && entityTypes.push("strip");

        return entityTypes.length ? entityTypes.join(", ") : "";
    }
}