diff --git a/src/core/clickInspector.ts b/src/core/clickInspector.ts new file mode 100644 index 0000000..9c8cc73 --- /dev/null +++ b/src/core/clickInspector.ts @@ -0,0 +1,76 @@ +import { Config, isDev } from './config.js'; +import { isElementNode } from './utils.js'; +import type { ElementNode } from './elementNode.js'; + +let installed = false; + +function findDeepestAtPosition( + root: ElementNode, + x: number, + y: number, +): ElementNode { + const precision = Config.rendererOptions?.deviceLogicalPixelRatio || 1; + const px = x / precision; + const py = y / precision; + + let current = root; + while (true) { + let best: ElementNode | undefined; + let bestZ = -Infinity; + for (const child of current.children) { + if (!isElementNode(child) || child.alpha === 0) continue; + const cx = (child.lng.absX as number) || 0; + const cy = (child.lng.absY as number) || 0; + const cw = child.width || 0; + const ch = child.height || 0; + if (px < cx || px > cx + cw || py < cy || py > cy + ch) continue; + const z = child.zIndex ?? -1; + if (z >= bestZ) { + bestZ = z; + best = child; + } + } + if (!best) return current; + current = best; + } +} + +function handleClick(event: MouseEvent) { + if (!event.altKey) return; + let target = event.target as HTMLElement | null; + while (target && !target.element) { + target = target.parentElement; + } + const hit = target?.element; + if (!hit) return; + let root = hit; + while (root.parent) root = root.parent; + const el = findDeepestAtPosition(root, event.clientX, event.clientY); + event.preventDefault(); + event.stopPropagation(); + const lng = el.lng as any; + const label = el.componentName || el._type; + const loc = el.componentLocation ? ` @ ${el.componentLocation}` : ''; + console.log( + `%c[SolidTV Inspector] %c${label}${loc}`, + 'color: magenta; font-weight: bold;', + 'color: inherit; font-weight: normal;', + { + element: el, + div: lng.div, + lng, + states: el._states ? Array.from(el._states) : [], + position: { x: lng?.x, y: lng?.y, w: lng?.w, h: lng?.h }, + parent: el.parent, + children: el.children, + }, + ); + (globalThis as any).$el = el; + console.log('Pinned to $el — try $el.parent, $el.setFocus()'); +} + +export function initClickInspector(): void { + if (installed || !isDev || typeof document === 'undefined') return; + installed = true; + document.addEventListener('click', handleClick, true); +} diff --git a/src/core/dom-renderer/domRenderer.ts b/src/core/dom-renderer/domRenderer.ts index 5fb1be0..bcb3d56 100644 --- a/src/core/dom-renderer/domRenderer.ts +++ b/src/core/dom-renderer/domRenderer.ts @@ -230,6 +230,8 @@ function updateNodeParent(node: DOMNode | DOMText) { const parent = node.props.parent; if (parent instanceof DOMNode) { elMap.get(parent)!.appendChild(node.div); + } else { + node.div.parentNode?.removeChild(node.div); } } @@ -1174,7 +1176,7 @@ export class DOMNode extends EventEmitter implements IRendererNode { if (parent instanceof DOMNode) { parent.children.delete(this); } - this.div.parentNode!.removeChild(this.div); + this.div.parentNode?.removeChild(this.div); } get parent() { diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index 4d4523c..b1e4557 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -52,6 +52,7 @@ import { setActiveElement, FocusNode, } from './focusManager.js'; +import { initClickInspector } from './clickInspector.js'; import { IRendererNode, @@ -279,6 +280,8 @@ declare global { } } +initClickInspector(); + export type RendererNode = AddColorString< Partial< NewOmit< @@ -1338,6 +1341,17 @@ export class ElementNode { _stateChanged() { isDev && log('State Changed: ', this, this.states); + if (isDev) { + const div = (this.lng as any)?.div as HTMLElement | undefined; + if (div) { + if (this.states.length > 0) { + div.dataset.states = this.states.join(' '); + } else { + delete div.dataset.states; + } + } + } + if (this.forwardStates) { // apply states to children first const states = this.states.slice() as States; @@ -1622,7 +1636,12 @@ export class ElementNode { // L3 Inspector adds div to the lng object const div: HTMLElement | undefined = (node.lng as any)?.div; - if (div) div.element = node; + if (isDev && div) { + div.element = node; + if (node._states && node._states.length > 0) { + div.dataset.states = node._states.join(' '); + } + } if (node._type === NodeType.Element) { // only element nodes will have children that need rendering diff --git a/src/render.ts b/src/render.ts index d9bdebc..6cafad3 100644 --- a/src/render.ts +++ b/src/render.ts @@ -134,6 +134,7 @@ export function Dynamic>( case 'string': { const el = createElement(component); + (el as { componentName?: string }).componentName = component; spread(el, others); return el; }