Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/public/build
/.mf
/.idea
.venv/
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love this, but my linter kept giving me warnings for some time


.DS_store
wrangler.toml
Expand Down
188 changes: 179 additions & 9 deletions app/components/Article/Contents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ const footnoteHTML = (el: HTMLDivElement, e: HTMLAnchorElement): string | null =
return elem.firstElementChild?.innerHTML || null
}

const addPopup = (e: HTMLElement, id: string, contents: string, mobile: boolean): HTMLElement => {
const addPopup = (
e: HTMLElement,
id: string,
contents: string,
mobile: boolean,
hasImage: boolean = false,
imageHtml: string = ''
): HTMLElement => {
const preexisting = document.getElementById(id)
if (preexisting) return preexisting

Expand All @@ -34,15 +41,175 @@ const addPopup = (e: HTMLElement, id: string, contents: string, mobile: boolean)

e.insertAdjacentElement('afterend', popup)

// Create image popup if needed
let imagePopup: HTMLElement | null = null
if (hasImage && imageHtml) {
imagePopup = document.createElement('div')
imagePopup.className = 'link-popup image-popup'
imagePopup.innerHTML = `<div class="glossary-image-container">${imageHtml}</div>`
imagePopup.id = `${id}-image`
imagePopup.style.display = 'none'
e.insertAdjacentElement('afterend', imagePopup)
}

const positionPopup = () => {
if (!mobile) {
const article = e.closest('article')
if (article) {
const articleRect = article.getBoundingClientRect()
const viewportWidth = window.innerWidth
const availableRight = viewportWidth - articleRect.right

// Only position to the right if there's actually enough space in the viewport
// Reduced minimum to allow more cases to display on the right
if (availableRight >= 200) {
// Position to the right of the article
popup.classList.add('positioned-right')
const elementRect = e.getBoundingClientRect()
popup.style.position = 'fixed'

// Calculate left position ensuring popup stays within viewport
const popupWidth = Math.min(350, availableRight - 40)
let leftPosition = articleRect.right + 20

// Check if popup would go off the right edge
if (leftPosition + popupWidth > viewportWidth - 20) {
leftPosition = viewportWidth - popupWidth - 20
}

popup.style.left = `${leftPosition}px`
popup.style.width = `${popupWidth}px`
popup.style.transform = 'none'

// Calculate vertical position ensuring popup stays in viewport
const viewportHeight = window.innerHeight
let topPosition = elementRect.top

// Estimate popup height for initial positioning
const estimatedPopupHeight = 350

// Check if popup would go below viewport
if (topPosition + estimatedPopupHeight > viewportHeight - 20) {
topPosition = Math.max(10, viewportHeight - estimatedPopupHeight - 20)
}

popup.style.top = `${topPosition}px`

// Position image popup above if there's space
if (imagePopup) {
const estimatedImageHeight = 250 // Estimate for image container
const gap = 10 // Gap between image and text popup
const spaceAbove = topPosition // Use adjusted position

if (spaceAbove > estimatedImageHeight + gap + 10) {
imagePopup.style.position = 'fixed'
imagePopup.style.left = `${leftPosition}px`
imagePopup.style.width = `${popupWidth}px`
imagePopup.style.transform = 'none'
imagePopup.style.display = 'block'
imagePopup.classList.add('shown')

// Wait for image to load to get actual height and reposition
requestAnimationFrame(() => {
if (imagePopup) {
const actualHeight = imagePopup.offsetHeight || estimatedImageHeight
// Position above the text popup (which might have been adjusted)
imagePopup.style.top = `${Math.max(10, topPosition - actualHeight - gap)}px`
}
})
} else {
imagePopup.classList.remove('shown')
imagePopup.style.display = 'none'
}
}
}
}
}
}

// Shared hover state for this popup group
let isHovered = false
let hideTimeout: number | null = null

const clearHideTimeout = () => {
if (hideTimeout) {
clearTimeout(hideTimeout)
hideTimeout = null
}
}

const actuallyShow = () => {
// Hide any other visible popups immediately (both text and image)
const visiblePopups = document.querySelectorAll('.link-popup.shown')
const otherPopups = Array.from(visiblePopups).filter((p) => p !== popup && p !== imagePopup)
otherPopups.forEach((p) => {
// Force instant hide by temporarily disabling transitions
const popupElement = p as HTMLElement
popupElement.style.transition = 'none'
p.classList.remove('shown')

// Reset transition after next frame
requestAnimationFrame(() => {
const popupElement = p as HTMLElement
popupElement.style.transition = ''
})

// Also hide associated image popups
const associatedImageId = p.id + '-image'
const associatedImage = document.getElementById(associatedImageId)
if (associatedImage) {
const imageElement = associatedImage as HTMLElement
imageElement.style.transition = 'none'
associatedImage.classList.remove('shown')
imageElement.style.display = 'none'
requestAnimationFrame(() => {
imageElement.style.transition = ''
})
}
})

// Only add shown and reposition if not already shown
if (!popup.classList.contains('shown')) {
popup.classList.add('shown')
positionPopup()
}
}

const actuallyHide = () => {
popup.classList.remove('shown')
if (imagePopup) {
imagePopup.classList.remove('shown')
}
}

const toggle = () => popup.classList.toggle('shown')
const show = () => popup.classList.add('shown')
const hide = () => popup.classList.remove('shown')
const show = () => {
isHovered = true
clearHideTimeout()
// Always call actuallyShow to handle hiding other popups immediately
actuallyShow()
}

const hide = () => {
isHovered = false
clearHideTimeout()
// Use a small delay to allow moving between term and popup
hideTimeout = setTimeout(() => {
if (!isHovered) {
actuallyHide()
}
}, 100) as unknown as number
}

if (!mobile) {
e.addEventListener('mouseover', show)
e.addEventListener('mouseout', hide)
popup.addEventListener('mouseover', show)
popup.addEventListener('mouseout', hide)
if (imagePopup) {
imagePopup.addEventListener('mouseover', show)
imagePopup.addEventListener('mouseout', hide)
}
} else {
popup.addEventListener('click', togglePopup(toggle, e))
e.addEventListener('click', togglePopup(toggle, e))
Expand Down Expand Up @@ -167,23 +334,24 @@ const insertGlossary = (pageid: string, glossary: Glossary, mobile: boolean) =>
entry.pageid &&
`<a href="${questionUrl(entry)}" target="_blank" rel="noopener noreferrer" class="button secondary">View full definition</a>`
const isGoogleDrive = entry.image && entry.image.includes('drive.google.com/file/d/')
const image = entry.image
const imageHtml = entry.image
? isGoogleDrive
? `<iframe src="${entry.image.replace(/\/view$/, '/preview')}" style="width:100%; border:none;" allowFullScreen></iframe>`
: `<img src="${entry.image}"/>`
: ''
addPopup(
e as HTMLSpanElement,
`glossary-${entry.term}-${randomId}`,
`<div class="glossary-popup flex-container black small">
<div class="contents ${image ? '' : 'full-width'}">
`<div class="glossary-popup black small">
<div class="contents full-width">
<div class="small-bold text-no-wrap">${entry.term}</div>
<div class="definition small">${entry.contents}</div>
${link || ''}
</div>
${image || ''}
</div>`,
mobile
mobile,
!!imageHtml,
imageHtml
)
})

Expand Down Expand Up @@ -241,7 +409,9 @@ const Contents = ({
e as HTMLAnchorElement,
`footnote-${footnoteId}`,
`<div class="footnote">${footnote}</div>`,
mobile
mobile,
false,
''
)
}
})
Expand Down
68 changes: 61 additions & 7 deletions app/components/Article/article.css
Original file line number Diff line number Diff line change
Expand Up @@ -169,24 +169,71 @@ article .footnote-ref {
article .glossary-popup {
border-radius: var(--border-radius);
overflow: hidden;
height: 304px;
height: auto;
max-height: 400px;
}

article .link-popup.positioned-right .glossary-popup {
height: auto;
max-height: 80vh;
overflow-y: auto;
}

article .link-popup.image-popup {
padding: 0;
background: transparent;
border: none;
visibility: hidden;
opacity: 0;
transition:
visibility 0s 600ms,
opacity cubic-bezier(1, 0, 1, 1) 600ms;
}

article .link-popup.image-popup.shown {
visibility: visible;
opacity: 1;
transition:
visibility 0s,
opacity 100ms;
}

article .glossary-image-container {
background: transparent;
border: none;
padding: 0;
box-shadow: none;
text-align: center;
}

article .glossary-image-container img {
width: 300px;
max-width: 100%;
height: auto;
display: inline-block;
border-radius: var(--border-radius);
}

article .link-popup.positioned-right .definition {
height: auto;
max-height: none;
-webkit-line-clamp: unset;
}

article .contents a.button {
font-weight: normal;
}
article .link-popup .glossary-popup > .contents {
padding: var(--spacing-24) var(--spacing-40) var(--spacing-24);
padding: var(--spacing-16) var(--spacing-32) var(--spacing-16);
}
article .definition {
height: 140px;
display: -webkit-box;
/* These are webkit specific things, so might not work in all browsers (firefox handles them fine) */
-webkit-box-orient: vertical;
-webkit-line-clamp: 5; /* Number of lines you want to display */
-webkit-line-clamp: 8; /* Increased to show more lines if needed */
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: var(--spacing-32);
margin-bottom: var(--spacing-16);
}
article .link-popup .glossary-popup > img {
border-top-right-radius: var(--border-radius);
Expand Down Expand Up @@ -276,8 +323,15 @@ article blockquote + p {
left: calc(50% - 200px);
transform: translateY(var(--spacing-40));
transition:
visibility 0s 300ms,
opacity cubic-bezier(1, 0, 1, 1) 300ms;
visibility 0s 600ms,
opacity cubic-bezier(1, 0, 1, 1) 600ms;
}

article .link-popup.positioned-right {
position: fixed !important;
max-width: none;
left: auto;
transform: none !important;
}
}

Expand Down
4 changes: 2 additions & 2 deletions app/components/icons-generated/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export {default as StampyLarge} from './StampyLarge'
export {default as StampySmall} from './StampySmall'
export {default as Aisafety} from './Aisafety'
export {default as ArrowRight} from './ArrowRight'
export {default as ArrowUpRight} from './ArrowUpRight'
Expand Down Expand Up @@ -61,8 +63,6 @@ export {default as Share} from './Share'
export {default as Speaker} from './Speaker'
export {default as Stamp} from './Stamp'
export {default as Stampy} from './Stampy'
export {default as StampyLarge} from './StampyLarge'
export {default as StampySmall} from './StampySmall'
export {default as Tag} from './Tag'
export {default as ThumbDownLarge} from './ThumbDownLarge'
export {default as ThumbDown} from './ThumbDown'
Expand Down