import { EventBase } from "UnityEngine/UIElements" import { DomStyleWrapper } from "./dom-style" export class DomWrapper { public get _dom(): CS.OneJS.Dom.Dom { return this.dom } public get ve(): CS.UnityEngine.UIElements.VisualElement { return this.dom.ve } public get childNodes(): DomWrapper[] { if (this.cachedChildNodes) return this.cachedChildNodes this.cachedChildNodes = new Array(this.dom.childNodes.Length) as DomWrapper[] var i = this.dom.childNodes.Length while (i--) { this.cachedChildNodes[i] = new DomWrapper(this.dom.childNodes.get_Item(i)) } return this.cachedChildNodes } public get firstChild(): DomWrapper | null { return this.dom.firstChild ? new DomWrapper(this.dom.firstChild) : null } public get parentNode(): DomWrapper | null { return this.dom.parentNode ? new DomWrapper(this.dom.parentNode) : null } public get nextSibling(): DomWrapper | null { return this.dom.nextSibling ? new DomWrapper(this.dom.nextSibling) : null } public get nodeType(): number { return this.dom.nodeType } public get style(): DomStyleWrapper { return this.domStyleWrapper } public get Id(): string { return this.dom.Id } public set Id(value: string) { this.dom.Id = value } public get key(): string { return this.dom.key } public set key(value: string) { this.dom.key = value } public get value(): any { return this.dom.value } public get checked(): boolean { return this.dom.checked } public get data(): any { return this.dom.data } public set data(value: any) { this.dom.data = value } public get className(): string { return this.dom.className } public set className(value: string) { this.dom.className = value } public get classList(): DomTokenList { return this.domTokenList } /** * Not using private fields because of issues with the `#private;` line * generated by tsc */ dom: CS.OneJS.Dom.Dom domStyleWrapper: DomStyleWrapper domTokenList: DomTokenList cachedChildNodes: DomWrapper[] | null = null boundListeners = new WeakMap(); constructor(dom: CS.OneJS.Dom.Dom) { this.dom = dom this.domStyleWrapper = new DomStyleWrapper(dom.style) this.domTokenList = new DomTokenList(dom) } // MARK: Manipulation appendChild(child: DomWrapper) { if (!child) return this.dom.appendChild(child.dom) this.cachedChildNodes = null return child } removeChild(child: DomWrapper) { if (!child) return this.dom.removeChild(child.dom) this.cachedChildNodes = null } insertBefore(a: DomWrapper, b: DomWrapper) { this.dom.insertBefore(a?._dom, b?._dom) this.cachedChildNodes = null } insertAfter(a: DomWrapper, b: DomWrapper) { this.dom.insertAfter(a?._dom, b?._dom) this.cachedChildNodes = null } before(other: DomWrapper) { // TODO: variable length args if (this.parentNode) { this.parentNode.insertBefore(other, this) } } clearChildren() { this.dom.clearChildren() this.cachedChildNodes = null } setAttribute(name: string, value: any) { this.dom.setAttribute(name, value) } removeAttribute(name: string) { this.dom.removeAttribute(name) } // MARK: Node.prototype append(child: DomWrapper) { if (!child) return this.dom.appendChild(child.dom) this.cachedChildNodes = null } cloneNode(deep: boolean = false): DomWrapper { return this } remove() { if (this.parentNode) { this.parentNode.removeChild(this) } } // MARK: Misc contains(child: DomWrapper) { if (!child) return false return this.dom.contains(child._dom) } focus() { this.dom.focus() } // MARK: Event addEventListener(type: string, listener: (event: EventBase) => void, options?: boolean | { once?: boolean }) { let boundListener = this.boundListeners.get(listener) if (!boundListener) { boundListener = listener.bind(this) this.boundListeners.set(listener, boundListener) } if (typeof options === 'object' && options.once) { const onceWrapper = (event: EventBase) => { boundListener(event) this.dom.removeEventListener(type, onceWrapper, false) } this.dom.addEventListener(type, onceWrapper, false) } else { this.dom.addEventListener(type, boundListener, options ? true : false) } } removeEventListener(type: string, listener: (event: EventBase) => void, useCapture?: boolean) { const boundListener = this.boundListeners.get(listener) if (boundListener) { this.dom.removeEventListener(type, boundListener, useCapture ? true : false) this.boundListeners.delete(listener) // isn't strictly necessary for WeakMap, but still good practice } } /** * Returns all elements matching the specified selector. * Supports basic selectors: * - Tag names: 'div' * - IDs: '#myId' * - Classes: '.myClass' * - Combinations: 'div.myClass#myId' */ querySelectorAll(selector: string): DomWrapper[] { const selectorInfo = parseSelector(selector) const results: DomWrapper[] = [] function traverse(element: DomWrapper) { if (elementMatchesSelector(element, selectorInfo)) { results.push(element) } for (const child of element.childNodes) { traverse(child) } } traverse(this) return results } /** * Returns the first element matching the specified selector. * Supports the same basic selectors as querySelectorAll. */ querySelector(selector: string): DomWrapper | null { const selectorInfo = parseSelector(selector) function traverse(element: DomWrapper): DomWrapper | null { if (elementMatchesSelector(element, selectorInfo)) { return element } for (const child of element.childNodes) { const match = traverse(child) if (match) { return match } } return null } return traverse(this) } } interface SelectorInfo { tag?: string id?: string classes: string[] } function parseSelector(selector: string): SelectorInfo { const selectorInfo: SelectorInfo = { classes: [] } // Handle ID const idMatch = selector.match(/#([^.#\s]+)/) if (idMatch) { selectorInfo.id = idMatch[1] selector = selector.replace(idMatch[0], '') } // Handle classes const classMatches = selector.match(/\.([^.#\s]+)/g) if (classMatches) { selectorInfo.classes = classMatches.map(c => c.substring(1)) selector = selector.replace(/\.[^.#\s]+/g, '') } // Handle tag name (what's left after removing id and classes) const tagName = selector.trim() if (tagName) { selectorInfo.tag = tagName.toLowerCase() } return selectorInfo } function elementMatchesSelector(element: DomWrapper, selectorInfo: SelectorInfo): boolean { // Check tag name if (selectorInfo.tag && element.ve.GetType().Name.toLowerCase() !== selectorInfo.tag) { return false } // Check ID if (selectorInfo.id && element.Id !== selectorInfo.id) { return false } // Check classes if (selectorInfo.classes.length > 0) { const elementClasses = element.className.split(' ').filter(c => c) for (const className of selectorInfo.classes) { if (!elementClasses.includes(className)) { return false } } } return true } export function querySelectorAll(root: DomWrapper, selector: string): DomWrapper[] { const results: DomWrapper[] = [] const selectorInfo = parseSelector(selector) function traverse(element: DomWrapper) { // Check if current element matches if (elementMatchesSelector(element, selectorInfo)) { results.push(element) } // Recursively check children for (const child of element.childNodes) { traverse(child) } } traverse(root) return results } export function querySelector(root: DomWrapper, selector: string): DomWrapper | null { const selectorInfo = parseSelector(selector) function traverse(element: DomWrapper): DomWrapper | null { // Check if current element matches if (elementMatchesSelector(element, selectorInfo)) { return element } // Recursively check children for (const child of element.childNodes) { const match = traverse(child) if (match) { return match } } return null } return traverse(root) } class DomTokenList { dom: CS.OneJS.Dom.Dom constructor(dom: CS.OneJS.Dom.Dom) { this.dom = dom } _tokens(): string[] { return this.dom.className.trim().split(/\s+/).filter(Boolean) } _update(tokens: string[]) { this.dom.className = tokens.join(' ') } add(...tokens: string[]) { const set = new Set(this._tokens()) tokens.forEach(t => t && set.add(t)) this._update(Array.from(set)) } remove(...tokens: string[]) { const set = new Set(this._tokens()) tokens.forEach(t => set.delete(t)) this._update(Array.from(set)) } toggle(token: string, force?: boolean): boolean { if (!token) return false const has = this.contains(token) if (force === true || (!has && force !== false)) { this.add(token) return true } if (has && (force === false || force === undefined)) { this.remove(token) return false } return has } contains(token: string): boolean { return this._tokens().includes(token) } replace(oldToken: string, newToken: string): boolean { if (!this.contains(oldToken)) return false const tokens = this._tokens().map(t => (t === oldToken ? newToken : t)) this._update(tokens) return true } toString(): string { return this.dom.className } get length(): number { return this._tokens().length } item(index: number): string | null { const t = this._tokens() return index >= 0 && index < t.length ? t[index] : null } [Symbol.iterator](): Iterator { return this._tokens()[Symbol.iterator]() } }