@@ -2,7 +2,7 @@ import type { App, Directive } from 'vue'
22import type { TooltipProps } from '@/components/tooltip/Tooltip.vue'
33import type { TooltipDirectiveModifiers } from '@/types/tooltip-modifiers'
44
5- import { createApp , h } from 'vue'
5+ import { createApp , h , reactive } from 'vue'
66import Tooltip from '@/components/tooltip/Tooltip.vue'
77import { getReactiveGlobalConfig } from '@/config/globalConfig'
88
@@ -20,15 +20,29 @@ interface TooltipDirectiveBinding {
2020 modifiers ?: TooltipDirectiveModifiers
2121}
2222
23- interface TooltipDirectiveInstance {
24- app : App
25- wrapper : HTMLElement
26- originalElement : HTMLElement
27- cleanup : ( ) => void
23+ interface TooltipInstance {
24+ id : string
25+ element : HTMLElement
26+ props : TooltipProps
2827}
2928
30- // Store tooltip instances
31- const TOOLTIP_INSTANCES = new WeakMap < HTMLElement , TooltipDirectiveInstance > ( )
29+ // Shared state for all tooltip instances
30+ const tooltipStore = reactive ( {
31+ tooltips : new Map < string , TooltipInstance > ( ) ,
32+ } )
33+
34+ // Single shared Vue app instance
35+ let sharedApp : App | null = null
36+ let appContainer : HTMLElement | null = null
37+ let instanceCounter = 0
38+
39+ // Generate unique ID for each tooltip
40+ function generateTooltipInstanceId ( ) : string {
41+ return `tooltip-directive-${ ++ instanceCounter } `
42+ }
43+
44+ // WeakMap to track element to tooltip ID mapping
45+ const elementTooltipMap = new WeakMap < HTMLElement , string > ( )
3246
3347function getTooltipProps ( binding : TooltipDirectiveBinding ) : TooltipProps {
3448 const {
@@ -92,59 +106,105 @@ function getTooltipProps(binding: TooltipDirectiveBinding): TooltipProps {
92106 return props
93107}
94108
95- function createTooltipInstance (
96- element : HTMLElement ,
97- binding : TooltipDirectiveBinding ,
98- ) : TooltipDirectiveInstance {
99- const tooltipProps = getTooltipProps ( binding )
100-
101- // Create container for tooltip (element stays in place - non-invasive)
102- const tooltipContainer = document . createElement ( 'div' )
103- tooltipContainer . style . display = 'contents' // Makes the container invisible in layout
104- tooltipContainer . setAttribute ( 'data-tooltip-container' , '' ) // For debugging
105-
106- // Store reference to the original element
107- const originalElement = element
108-
109- // Insert container AFTER the element (element stays in original position)
110- const parent = element . parentNode
111- if ( parent ) {
112- parent . insertBefore ( tooltipContainer , element . nextSibling )
113- }
114-
115- // Create Vue app instance for the tooltip with external trigger
116- const tooltipApp = createApp ( {
109+ /**
110+ * Initialize the shared Vue app for all directive tooltips
111+ * This is called lazily on first directive mount
112+ */
113+ function initializeSharedApp ( ) {
114+ if ( sharedApp )
115+ return
116+
117+ // Create a container for all directive tooltips
118+ appContainer = document . createElement ( 'div' )
119+ appContainer . setAttribute ( 'data-tooltip-directive-container' , '' )
120+ appContainer . style . display = 'contents' // Invisible in layout
121+ document . body . appendChild ( appContainer )
122+
123+ // Create single Vue app that renders all tooltips
124+ sharedApp = createApp ( {
125+ setup ( ) {
126+ return { tooltips : tooltipStore . tooltips }
127+ } ,
117128 render ( ) {
118- // Pass the original element as externalTrigger prop
119- return h ( Tooltip , {
120- ...tooltipProps ,
121- externalTrigger : originalElement ,
129+ // Render all active tooltips
130+ const tooltipComponents = Array . from ( this . tooltips . values ( ) as IterableIterator < TooltipInstance > ) . map ( ( instance ) => {
131+ return h ( Tooltip , {
132+ key : instance . id ,
133+ ...instance . props ,
134+ externalTrigger : instance . element ,
135+ } )
122136 } )
137+
138+ return tooltipComponents
123139 } ,
124140 } )
125141
126- // Mount the tooltip app to the container (not replacing the element )
127- tooltipApp . mount ( tooltipContainer )
142+ sharedApp . mount ( appContainer )
143+ }
128144
129- const cleanup = ( ) => {
130- try {
131- tooltipApp . unmount ( )
132- }
133- catch ( error ) {
134- console . warn ( 'Error unmounting tooltip app:' , error )
135- }
145+ /**
146+ * Add a tooltip instance to the shared app
147+ */
148+ function addTooltipInstance ( element : HTMLElement , binding : TooltipDirectiveBinding ) : string {
149+ // Initialize shared app if needed
150+ initializeSharedApp ( )
151+
152+ const id = generateTooltipInstanceId ( )
153+ const props = getTooltipProps ( binding )
154+
155+ // Add to reactive store (triggers re-render)
156+ tooltipStore . tooltips . set ( id , {
157+ id,
158+ element,
159+ props,
160+ } )
136161
137- // Remove the tooltip container (original element stays untouched)
138- if ( tooltipContainer . parentNode ) {
139- tooltipContainer . parentNode . removeChild ( tooltipContainer )
140- }
162+ // Track mapping
163+ elementTooltipMap . set ( element , id )
164+
165+ return id
166+ }
167+
168+ /**
169+ * Update a tooltip instance in the shared app
170+ */
171+ function updateTooltipInstance ( element : HTMLElement , binding : TooltipDirectiveBinding ) {
172+ const id = elementTooltipMap . get ( element )
173+ if ( ! id )
174+ return
175+
176+ const props = getTooltipProps ( binding )
177+
178+ // Update in reactive store (triggers re-render)
179+ const existing = tooltipStore . tooltips . get ( id )
180+ if ( existing ) {
181+ tooltipStore . tooltips . set ( id , {
182+ ...existing ,
183+ props,
184+ } )
141185 }
186+ }
142187
143- return {
144- app : tooltipApp ,
145- wrapper : tooltipContainer ,
146- originalElement,
147- cleanup,
188+ /**
189+ * Remove a tooltip instance from the shared app
190+ */
191+ function removeTooltipInstance ( element : HTMLElement ) {
192+ const id = elementTooltipMap . get ( element )
193+ if ( ! id )
194+ return
195+
196+ // Remove from reactive store (triggers re-render)
197+ tooltipStore . tooltips . delete ( id )
198+ elementTooltipMap . delete ( element )
199+
200+ // Clean up shared app if no tooltips remain
201+ if ( tooltipStore . tooltips . size === 0 && sharedApp && appContainer ) {
202+ sharedApp . unmount ( )
203+ if ( appContainer . parentNode ) {
204+ appContainer . parentNode . removeChild ( appContainer )
205+ }
206+ sharedApp = null
207+ appContainer = null
148208 }
149209}
150210
@@ -153,29 +213,19 @@ export const vTooltip: Directive<HTMLElement, string | TooltipProps> = {
153213 if ( ! binding . value )
154214 return
155215
156- const instance = createTooltipInstance ( element , binding )
157- TOOLTIP_INSTANCES . set ( element , instance )
216+ addTooltipInstance ( element , binding )
158217 } ,
159218
160219 updated ( element : HTMLElement , binding ) {
161- const instance = TOOLTIP_INSTANCES . get ( element )
162- if ( instance ) {
163- instance . cleanup ( )
164- TOOLTIP_INSTANCES . delete ( element )
165- }
166-
167- if ( ! binding . value )
220+ if ( ! binding . value ) {
221+ removeTooltipInstance ( element )
168222 return
223+ }
169224
170- const newInstance = createTooltipInstance ( element , binding )
171- TOOLTIP_INSTANCES . set ( element , newInstance )
225+ updateTooltipInstance ( element , binding )
172226 } ,
173227
174228 unmounted ( element : HTMLElement ) {
175- const instance = TOOLTIP_INSTANCES . get ( element )
176- if ( instance ) {
177- instance . cleanup ( )
178- TOOLTIP_INSTANCES . delete ( element )
179- }
229+ removeTooltipInstance ( element )
180230 } ,
181231}
0 commit comments