Skip to content

Commit f306684

Browse files
committed
Move glossary popup to the right
1 parent 1fe6c48 commit f306684

File tree

4 files changed

+243
-18
lines changed

4 files changed

+243
-18
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
/public/build
77
/.mf
88
/.idea
9+
.venv/
910

1011
.DS_store
1112
wrangler.toml

app/components/Article/Contents.tsx

Lines changed: 179 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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(/\/view$/, '/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
})

app/components/Article/article.css

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,24 +169,71 @@ article .footnote-ref {
169169
article .glossary-popup {
170170
border-radius: var(--border-radius);
171171
overflow: hidden;
172-
height: 304px;
172+
height: auto;
173+
max-height: 400px;
174+
}
175+
176+
article .link-popup.positioned-right .glossary-popup {
177+
height: auto;
178+
max-height: 80vh;
179+
overflow-y: auto;
180+
}
181+
182+
article .link-popup.image-popup {
183+
padding: 0;
184+
background: transparent;
185+
border: none;
186+
visibility: hidden;
187+
opacity: 0;
188+
transition:
189+
visibility 0s 600ms,
190+
opacity cubic-bezier(1, 0, 1, 1) 600ms;
191+
}
192+
193+
article .link-popup.image-popup.shown {
194+
visibility: visible;
195+
opacity: 1;
196+
transition:
197+
visibility 0s,
198+
opacity 100ms;
199+
}
200+
201+
article .glossary-image-container {
202+
background: transparent;
203+
border: none;
204+
padding: 0;
205+
box-shadow: none;
206+
text-align: center;
207+
}
208+
209+
article .glossary-image-container img {
210+
width: 300px;
211+
max-width: 100%;
212+
height: auto;
213+
display: inline-block;
214+
border-radius: var(--border-radius);
215+
}
216+
217+
article .link-popup.positioned-right .definition {
218+
height: auto;
219+
max-height: none;
220+
-webkit-line-clamp: unset;
173221
}
174222

175223
article .contents a.button {
176224
font-weight: normal;
177225
}
178226
article .link-popup .glossary-popup > .contents {
179-
padding: var(--spacing-24) var(--spacing-40) var(--spacing-24);
227+
padding: var(--spacing-16) var(--spacing-32) var(--spacing-16);
180228
}
181229
article .definition {
182-
height: 140px;
183230
display: -webkit-box;
184231
/* These are webkit specific things, so might not work in all browsers (firefox handles them fine) */
185232
-webkit-box-orient: vertical;
186-
-webkit-line-clamp: 5; /* Number of lines you want to display */
233+
-webkit-line-clamp: 8; /* Increased to show more lines if needed */
187234
overflow: hidden;
188235
text-overflow: ellipsis;
189-
margin-bottom: var(--spacing-32);
236+
margin-bottom: var(--spacing-16);
190237
}
191238
article .link-popup .glossary-popup > img {
192239
border-top-right-radius: var(--border-radius);
@@ -276,8 +323,15 @@ article blockquote + p {
276323
left: calc(50% - 200px);
277324
transform: translateY(var(--spacing-40));
278325
transition:
279-
visibility 0s 300ms,
280-
opacity cubic-bezier(1, 0, 1, 1) 300ms;
326+
visibility 0s 600ms,
327+
opacity cubic-bezier(1, 0, 1, 1) 600ms;
328+
}
329+
330+
article .link-popup.positioned-right {
331+
position: fixed !important;
332+
max-width: none;
333+
left: auto;
334+
transform: none !important;
281335
}
282336
}
283337

app/components/icons-generated/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export {default as StampyLarge} from './StampyLarge'
2+
export {default as StampySmall} from './StampySmall'
13
export {default as Aisafety} from './Aisafety'
24
export {default as ArrowRight} from './ArrowRight'
35
export {default as ArrowUpRight} from './ArrowUpRight'
@@ -61,8 +63,6 @@ export {default as Share} from './Share'
6163
export {default as Speaker} from './Speaker'
6264
export {default as Stamp} from './Stamp'
6365
export {default as Stampy} from './Stampy'
64-
export {default as StampyLarge} from './StampyLarge'
65-
export {default as StampySmall} from './StampySmall'
6666
export {default as Tag} from './Tag'
6767
export {default as ThumbDownLarge} from './ThumbDownLarge'
6868
export {default as ThumbDown} from './ThumbDown'

0 commit comments

Comments
 (0)