|
| 1 | +/** |
| 2 | + * Generic Vue Element Tracking System |
| 3 | + * |
| 4 | + * Automatically tracks DOM size and position changes for Vue-rendered elements |
| 5 | + * and syncs them to the layout store. Uses a single shared ResizeObserver for |
| 6 | + * performance, with elements identified by configurable data attributes. |
| 7 | + * |
| 8 | + * Supports different element types (nodes, slots, widgets, etc.) with |
| 9 | + * customizable data attributes and update handlers. |
| 10 | + */ |
| 11 | +import { getCurrentInstance, onMounted, onUnmounted } from 'vue' |
| 12 | + |
| 13 | +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' |
| 14 | +import type { Bounds, NodeId } from '@/renderer/core/layout/types' |
| 15 | + |
| 16 | +/** |
| 17 | + * Generic update item for element bounds tracking |
| 18 | + */ |
| 19 | +interface ElementBoundsUpdate { |
| 20 | + /** Element identifier (could be nodeId, widgetId, slotId, etc.) */ |
| 21 | + id: string |
| 22 | + /** Updated bounds */ |
| 23 | + bounds: Bounds |
| 24 | +} |
| 25 | + |
| 26 | +/** |
| 27 | + * Configuration for different types of tracked elements |
| 28 | + */ |
| 29 | +interface ElementTrackingConfig { |
| 30 | + /** Data attribute name (e.g., 'nodeId') */ |
| 31 | + dataAttribute: string |
| 32 | + /** Handler for processing bounds updates */ |
| 33 | + updateHandler: (updates: ElementBoundsUpdate[]) => void |
| 34 | +} |
| 35 | + |
| 36 | +/** |
| 37 | + * Registry of tracking configurations by element type |
| 38 | + */ |
| 39 | +const trackingConfigs: Map<string, ElementTrackingConfig> = new Map([ |
| 40 | + [ |
| 41 | + 'node', |
| 42 | + { |
| 43 | + dataAttribute: 'nodeId', |
| 44 | + updateHandler: (updates) => { |
| 45 | + const nodeUpdates = updates.map(({ id, bounds }) => ({ |
| 46 | + nodeId: id as NodeId, |
| 47 | + bounds |
| 48 | + })) |
| 49 | + layoutStore.batchUpdateNodeBounds(nodeUpdates) |
| 50 | + } |
| 51 | + } |
| 52 | + ] |
| 53 | +]) |
| 54 | + |
| 55 | +// Single ResizeObserver instance for all Vue elements |
| 56 | +const resizeObserver = new ResizeObserver((entries) => { |
| 57 | + // Group updates by element type |
| 58 | + const updatesByType = new Map<string, ElementBoundsUpdate[]>() |
| 59 | + |
| 60 | + for (const entry of entries) { |
| 61 | + if (!(entry.target instanceof HTMLElement)) continue |
| 62 | + const element = entry.target |
| 63 | + |
| 64 | + // Find which type this element belongs to |
| 65 | + let elementType: string | undefined |
| 66 | + let elementId: string | undefined |
| 67 | + |
| 68 | + for (const [type, config] of trackingConfigs) { |
| 69 | + const id = element.dataset[config.dataAttribute] |
| 70 | + if (id) { |
| 71 | + elementType = type |
| 72 | + elementId = id |
| 73 | + break |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + if (!elementType || !elementId) continue |
| 78 | + |
| 79 | + const { inlineSize: width, blockSize: height } = entry.contentBoxSize[0] |
| 80 | + const rect = element.getBoundingClientRect() |
| 81 | + |
| 82 | + const bounds: Bounds = { |
| 83 | + x: rect.left, |
| 84 | + y: rect.top, |
| 85 | + width, |
| 86 | + height: height |
| 87 | + } |
| 88 | + |
| 89 | + if (!updatesByType.has(elementType)) { |
| 90 | + updatesByType.set(elementType, []) |
| 91 | + } |
| 92 | + const updates = updatesByType.get(elementType) |
| 93 | + if (updates) { |
| 94 | + updates.push({ id: elementId, bounds }) |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + // Process updates by type |
| 99 | + for (const [type, updates] of updatesByType) { |
| 100 | + const config = trackingConfigs.get(type) |
| 101 | + if (config && updates.length > 0) { |
| 102 | + config.updateHandler(updates) |
| 103 | + } |
| 104 | + } |
| 105 | +}) |
| 106 | + |
| 107 | +/** |
| 108 | + * Tracks DOM element size/position changes for a Vue component and syncs to layout store |
| 109 | + * |
| 110 | + * Sets up automatic ResizeObserver tracking when the component mounts and cleans up |
| 111 | + * when unmounted. The tracked element is identified by a data attribute set on the |
| 112 | + * component's root DOM element. |
| 113 | + * |
| 114 | + * @param appIdentifier - Application-level identifier for this tracked element (not a DOM ID) |
| 115 | + * Example: node ID like 'node-123', widget ID like 'widget-456' |
| 116 | + * @param trackingType - Type of element being tracked, determines which tracking config to use |
| 117 | + * Example: 'node' for Vue nodes, 'widget' for UI widgets |
| 118 | + * |
| 119 | + * @example |
| 120 | + * ```ts |
| 121 | + * // Track a Vue node component with ID 'my-node-123' |
| 122 | + * useVueElementTracking('my-node-123', 'node') |
| 123 | + * |
| 124 | + * // Would set data-node-id="my-node-123" on the component's root element |
| 125 | + * // and sync size changes to layoutStore.batchUpdateNodeBounds() |
| 126 | + * ``` |
| 127 | + */ |
| 128 | +export function useVueElementTracking( |
| 129 | + appIdentifier: string, |
| 130 | + trackingType: string |
| 131 | +) { |
| 132 | + onMounted(() => { |
| 133 | + const element = getCurrentInstance()?.proxy?.$el |
| 134 | + if (!(element instanceof HTMLElement) || !appIdentifier) return |
| 135 | + |
| 136 | + const config = trackingConfigs.get(trackingType) |
| 137 | + if (config) { |
| 138 | + // Set the appropriate data attribute |
| 139 | + element.dataset[config.dataAttribute] = appIdentifier |
| 140 | + resizeObserver.observe(element) |
| 141 | + } |
| 142 | + }) |
| 143 | + |
| 144 | + onUnmounted(() => { |
| 145 | + const element = getCurrentInstance()?.proxy?.$el |
| 146 | + if (!(element instanceof HTMLElement)) return |
| 147 | + |
| 148 | + const config = trackingConfigs.get(trackingType) |
| 149 | + if (config) { |
| 150 | + // Remove the data attribute |
| 151 | + delete element.dataset[config.dataAttribute] |
| 152 | + resizeObserver.unobserve(element) |
| 153 | + } |
| 154 | + }) |
| 155 | +} |
0 commit comments