@@ -23,7 +23,14 @@ const footnoteHTML = (el: HTMLDivElement, e: HTMLAnchorElement): string | null =
2323 return elem . firstElementChild ?. innerHTML || null
2424}
2525
26- const addPopup = ( e : HTMLElement , id : string , contents : string , mobile : boolean ) : HTMLElement => {
26+ const addPopup = (
27+ e : HTMLElement ,
28+ id : string ,
29+ contents : string ,
30+ mobile : boolean ,
31+ hasImage : boolean = false ,
32+ imageHtml : string = ''
33+ ) : HTMLElement => {
2734 const preexisting = document . getElementById ( id )
2835 if ( preexisting ) return preexisting
2936
@@ -34,15 +41,175 @@ const addPopup = (e: HTMLElement, id: string, contents: string, mobile: boolean)
3441
3542 e . insertAdjacentElement ( 'afterend' , popup )
3643
44+ // Create image popup if needed
45+ let imagePopup : HTMLElement | null = null
46+ if ( hasImage && imageHtml ) {
47+ imagePopup = document . createElement ( 'div' )
48+ imagePopup . className = 'link-popup image-popup'
49+ imagePopup . innerHTML = `<div class="glossary-image-container">${ imageHtml } </div>`
50+ imagePopup . id = `${ id } -image`
51+ imagePopup . style . display = 'none'
52+ e . insertAdjacentElement ( 'afterend' , imagePopup )
53+ }
54+
55+ const positionPopup = ( ) => {
56+ if ( ! mobile ) {
57+ const article = e . closest ( 'article' )
58+ if ( article ) {
59+ const articleRect = article . getBoundingClientRect ( )
60+ const viewportWidth = window . innerWidth
61+ const availableRight = viewportWidth - articleRect . right
62+
63+ // Only position to the right if there's actually enough space in the viewport
64+ // Reduced minimum to allow more cases to display on the right
65+ if ( availableRight >= 200 ) {
66+ // Position to the right of the article
67+ popup . classList . add ( 'positioned-right' )
68+ const elementRect = e . getBoundingClientRect ( )
69+ popup . style . position = 'fixed'
70+
71+ // Calculate left position ensuring popup stays within viewport
72+ const popupWidth = Math . min ( 350 , availableRight - 40 )
73+ let leftPosition = articleRect . right + 20
74+
75+ // Check if popup would go off the right edge
76+ if ( leftPosition + popupWidth > viewportWidth - 20 ) {
77+ leftPosition = viewportWidth - popupWidth - 20
78+ }
79+
80+ popup . style . left = `${ leftPosition } px`
81+ popup . style . width = `${ popupWidth } px`
82+ popup . style . transform = 'none'
83+
84+ // Calculate vertical position ensuring popup stays in viewport
85+ const viewportHeight = window . innerHeight
86+ let topPosition = elementRect . top
87+
88+ // Estimate popup height for initial positioning
89+ const estimatedPopupHeight = 350
90+
91+ // Check if popup would go below viewport
92+ if ( topPosition + estimatedPopupHeight > viewportHeight - 20 ) {
93+ topPosition = Math . max ( 10 , viewportHeight - estimatedPopupHeight - 20 )
94+ }
95+
96+ popup . style . top = `${ topPosition } px`
97+
98+ // Position image popup above if there's space
99+ if ( imagePopup ) {
100+ const estimatedImageHeight = 250 // Estimate for image container
101+ const gap = 10 // Gap between image and text popup
102+ const spaceAbove = topPosition // Use adjusted position
103+
104+ if ( spaceAbove > estimatedImageHeight + gap + 10 ) {
105+ imagePopup . style . position = 'fixed'
106+ imagePopup . style . left = `${ leftPosition } px`
107+ imagePopup . style . width = `${ popupWidth } px`
108+ imagePopup . style . transform = 'none'
109+ imagePopup . style . display = 'block'
110+ imagePopup . classList . add ( 'shown' )
111+
112+ // Wait for image to load to get actual height and reposition
113+ requestAnimationFrame ( ( ) => {
114+ if ( imagePopup ) {
115+ const actualHeight = imagePopup . offsetHeight || estimatedImageHeight
116+ // Position above the text popup (which might have been adjusted)
117+ imagePopup . style . top = `${ Math . max ( 10 , topPosition - actualHeight - gap ) } px`
118+ }
119+ } )
120+ } else {
121+ imagePopup . classList . remove ( 'shown' )
122+ imagePopup . style . display = 'none'
123+ }
124+ }
125+ }
126+ }
127+ }
128+ }
129+
130+ // Shared hover state for this popup group
131+ let isHovered = false
132+ let hideTimeout : number | null = null
133+
134+ const clearHideTimeout = ( ) => {
135+ if ( hideTimeout ) {
136+ clearTimeout ( hideTimeout )
137+ hideTimeout = null
138+ }
139+ }
140+
141+ const actuallyShow = ( ) => {
142+ // Hide any other visible popups immediately (both text and image)
143+ const visiblePopups = document . querySelectorAll ( '.link-popup.shown' )
144+ const otherPopups = Array . from ( visiblePopups ) . filter ( ( p ) => p !== popup && p !== imagePopup )
145+ otherPopups . forEach ( ( p ) => {
146+ // Force instant hide by temporarily disabling transitions
147+ const popupElement = p as HTMLElement
148+ popupElement . style . transition = 'none'
149+ p . classList . remove ( 'shown' )
150+
151+ // Reset transition after next frame
152+ requestAnimationFrame ( ( ) => {
153+ const popupElement = p as HTMLElement
154+ popupElement . style . transition = ''
155+ } )
156+
157+ // Also hide associated image popups
158+ const associatedImageId = p . id + '-image'
159+ const associatedImage = document . getElementById ( associatedImageId )
160+ if ( associatedImage ) {
161+ const imageElement = associatedImage as HTMLElement
162+ imageElement . style . transition = 'none'
163+ associatedImage . classList . remove ( 'shown' )
164+ imageElement . style . display = 'none'
165+ requestAnimationFrame ( ( ) => {
166+ imageElement . style . transition = ''
167+ } )
168+ }
169+ } )
170+
171+ // Only add shown and reposition if not already shown
172+ if ( ! popup . classList . contains ( 'shown' ) ) {
173+ popup . classList . add ( 'shown' )
174+ positionPopup ( )
175+ }
176+ }
177+
178+ const actuallyHide = ( ) => {
179+ popup . classList . remove ( 'shown' )
180+ if ( imagePopup ) {
181+ imagePopup . classList . remove ( 'shown' )
182+ }
183+ }
184+
37185 const toggle = ( ) => popup . classList . toggle ( 'shown' )
38- const show = ( ) => popup . classList . add ( 'shown' )
39- const hide = ( ) => popup . classList . remove ( 'shown' )
186+ const show = ( ) => {
187+ isHovered = true
188+ clearHideTimeout ( )
189+ // Always call actuallyShow to handle hiding other popups immediately
190+ actuallyShow ( )
191+ }
192+
193+ const hide = ( ) => {
194+ isHovered = false
195+ clearHideTimeout ( )
196+ // Use a small delay to allow moving between term and popup
197+ hideTimeout = setTimeout ( ( ) => {
198+ if ( ! isHovered ) {
199+ actuallyHide ( )
200+ }
201+ } , 100 ) as unknown as number
202+ }
40203
41204 if ( ! mobile ) {
42205 e . addEventListener ( 'mouseover' , show )
43206 e . addEventListener ( 'mouseout' , hide )
44207 popup . addEventListener ( 'mouseover' , show )
45208 popup . addEventListener ( 'mouseout' , hide )
209+ if ( imagePopup ) {
210+ imagePopup . addEventListener ( 'mouseover' , show )
211+ imagePopup . addEventListener ( 'mouseout' , hide )
212+ }
46213 } else {
47214 popup . addEventListener ( 'click' , togglePopup ( toggle , e ) )
48215 e . addEventListener ( 'click' , togglePopup ( toggle , e ) )
@@ -167,23 +334,24 @@ const insertGlossary = (pageid: string, glossary: Glossary, mobile: boolean) =>
167334 entry . pageid &&
168335 `<a href="${ questionUrl ( entry ) } " target="_blank" rel="noopener noreferrer" class="button secondary">View full definition</a>`
169336 const isGoogleDrive = entry . image && entry . image . includes ( 'drive.google.com/file/d/' )
170- const image = entry . image
337+ const imageHtml = entry . image
171338 ? isGoogleDrive
172339 ? `<iframe src="${ entry . image . replace ( / \/ v i e w $ / , '/preview' ) } " style="width:100%; border:none;" allowFullScreen></iframe>`
173340 : `<img src="${ entry . image } "/>`
174341 : ''
175342 addPopup (
176343 e as HTMLSpanElement ,
177344 `glossary-${ entry . term } -${ randomId } ` ,
178- `<div class="glossary-popup flex-container black small">
179- <div class="contents ${ image ? '' : ' full-width' } ">
345+ `<div class="glossary-popup black small">
346+ <div class="contents full-width">
180347 <div class="small-bold text-no-wrap">${ entry . term } </div>
181348 <div class="definition small">${ entry . contents } </div>
182349 ${ link || '' }
183350 </div>
184- ${ image || '' }
185351 </div>` ,
186- mobile
352+ mobile ,
353+ ! ! imageHtml ,
354+ imageHtml
187355 )
188356 } )
189357
@@ -241,7 +409,9 @@ const Contents = ({
241409 e as HTMLAnchorElement ,
242410 `footnote-${ footnoteId } ` ,
243411 `<div class="footnote">${ footnote } </div>` ,
244- mobile
412+ mobile ,
413+ false ,
414+ ''
245415 )
246416 }
247417 } )
0 commit comments