Skip to content
Open
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
69 changes: 47 additions & 22 deletions app/assets/javascript/image-streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@ if (configEl) {
console.warn('image-streaming: could not parse config', e)
}

if (config && config.enabled && !config.alreadyReceived && config.images && config.images.length) {
if (
config &&
config.enabled &&
!config.alreadyReceived &&
config.images &&
config.images.length
) {
initStreaming(config)
}
}

function initStreaming (config) {
function initStreaming(config) {
const { intervalMs = 3000, images = [], totalImages = images.length } = config

// Find all view slots rendered by mammogram-image-display.njk
Expand All @@ -26,15 +32,15 @@ function initStreaming (config) {

// Build a map of view code → { el, revealedCount }
const slotMap = {}
slots.forEach(slot => {
slots.forEach((slot) => {
const view = slot.dataset.view
if (view) {
slotMap[view] = { el: slot, revealedCount: 0 }
}
})

// Set all slots to awaiting state before any images arrive
slots.forEach(slot => setSlotAwaiting(slot))
slots.forEach((slot) => setSlotAwaiting(slot))

// Counter element in the inset text
const countEl = document.getElementById('streaming-received-count')
Expand Down Expand Up @@ -68,7 +74,7 @@ function initStreaming (config) {
scheduleNext()

// Right arrow key immediately advances to the next image (useful for demos)
document.addEventListener('keydown', e => {
document.addEventListener('keydown', (e) => {
if (e.key !== 'ArrowRight') return
if (e.target.matches('input, textarea, select')) return
if (timer) {
Expand All @@ -82,8 +88,10 @@ function initStreaming (config) {

// Replace a slot's content with a placeholder, collapsing to a single wrapper.
// Uses the existing missing-image markup so styles and sizing are consistent.
function setSlotAwaiting (slot) {
const wrappers = slot.querySelectorAll('.app-mammogram-thumbnail__image-wrapper')
function setSlotAwaiting(slot) {
const wrappers = slot.querySelectorAll(
'.app-mammogram-thumbnail__image-wrapper'
)
if (!wrappers.length) return

const firstWrapper = wrappers[0]
Expand All @@ -95,12 +103,17 @@ function setSlotAwaiting (slot) {

// Capture label text before replacing inner content
const labelEl = firstWrapper.querySelector('.app-mammogram-thumbnail__label')
const labelText = labelEl ? labelEl.textContent.trim() : (slot.dataset.view || '')

// Set an explicit width so the wrapper doesn't collapse when there's no image content.
// (width: 100% on the wrapper resolves to 0 when the flex parent has no intrinsic width)
const isLarge = firstWrapper.classList.contains('app-mammogram-thumbnail__image-wrapper--large')
firstWrapper.style.width = isLarge ? '200px' : '120px'
const labelText = labelEl
? labelEl.textContent.trim()
: slot.dataset.view || ''

// Give the slot a preferred width so the wrapper's width: 100% can resolve.
// Keep max-width at 100% so placeholders still shrink on smaller screens.
const isLarge = firstWrapper.classList.contains(
'app-mammogram-thumbnail__image-wrapper--large'
)
slot.style.width = isLarge ? '200px' : '120px'
slot.style.maxWidth = '100%'

// Show a spinner on every placeholder from the start — images may load too fast
// for a spinner added only at reveal time to be visible
Expand All @@ -113,15 +126,17 @@ function setSlotAwaiting (slot) {

// Reveal an image in the appropriate slot.
// immediate=true skips the spinner delay (used for the first image, already received).
function revealImage (imageData, slotMap, immediate = false) {
function revealImage(imageData, slotMap, immediate = false) {
const slotInfo = slotMap[imageData.view]
if (!slotInfo) return

const { el: slot } = slotInfo

if (slotInfo.revealedCount === 0) {
// First image for this view — replace placeholder content in the existing wrapper
const wrapper = slot.querySelector('.app-mammogram-thumbnail__image-wrapper')
const wrapper = slot.querySelector(
'.app-mammogram-thumbnail__image-wrapper'
)
if (!wrapper) return

// Re-read the label before wiping innerHTML
Expand All @@ -132,11 +147,13 @@ function revealImage (imageData, slotMap, immediate = false) {
// Skip the spinner for the first image (already received when we arrived on this page).
const showImage = () => {
wrapper.innerHTML = `<span class="app-mammogram-thumbnail__label">${labelText}</span>`
// Reset the explicit width we set in setSlotAwaiting
wrapper.style.width = ''
// Reset placeholder-only sizing once a real image is shown.
slot.style.width = ''
slot.style.maxWidth = ''

const img = document.createElement('img')
img.className = 'app-mammogram-thumbnail__image app-mammogram-thumbnail__image--diagram'
img.className =
'app-mammogram-thumbnail__image app-mammogram-thumbnail__image--diagram'
img.src = imageData.src
img.alt = `${imageData.view} view`
wrapper.appendChild(img)
Expand All @@ -154,19 +171,27 @@ function revealImage (imageData, slotMap, immediate = false) {
}
} else {
// Additional image for this view (repeat or extra) — append a new wrapper
const existingWrapper = slot.querySelector('.app-mammogram-thumbnail__image-wrapper')
const isLarge = existingWrapper && existingWrapper.classList.contains('app-mammogram-thumbnail__image-wrapper--large')
const existingWrapper = slot.querySelector(
'.app-mammogram-thumbnail__image-wrapper'
)
const isLarge =
existingWrapper &&
existingWrapper.classList.contains(
'app-mammogram-thumbnail__image-wrapper--large'
)

const newWrapper = document.createElement('div')
newWrapper.className = 'app-mammogram-thumbnail__image-wrapper' +
newWrapper.className =
'app-mammogram-thumbnail__image-wrapper' +
(isLarge ? ' app-mammogram-thumbnail__image-wrapper--large' : '')

const label = document.createElement('span')
label.className = 'app-mammogram-thumbnail__label'
label.textContent = slot.dataset.view

const img = document.createElement('img')
img.className = 'app-mammogram-thumbnail__image app-mammogram-thumbnail__image--diagram'
img.className =
'app-mammogram-thumbnail__image app-mammogram-thumbnail__image--diagram'
img.src = imageData.src
img.alt = `${imageData.view} view`

Expand Down
12 changes: 12 additions & 0 deletions app/assets/javascript/reading-scroll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// app/assets/javascript/reading-scroll.js

// Reading workflow: scroll the status bar into view on page load.
// Uses getBoundingClientRect for an exact pixel position rather than
// scrollIntoView, which can behave oddly near sticky/fixed elements.

document.addEventListener('DOMContentLoaded', () => {
const statusBar = document.querySelector('.app-status-bar')
if (!statusBar) return
const top = statusBar.getBoundingClientRect().top + window.scrollY
window.scrollTo({ top, behavior: 'instant' })
})
2 changes: 1 addition & 1 deletion app/assets/sass/_misc.scss
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ body.js-enabled .app-no-js-only {
// }

.app-page-width--wide .nhsuk-width-container {
max-width: 1200px;
@include nhsuk-width-container(1200px);
}

.event-row:focus {
Expand Down
5 changes: 5 additions & 0 deletions app/assets/sass/components/_compact.scss
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@
}
}

.nhsuk-back-link,
.nhsuk-forward-link {
@include nhsuk-responsive-margin(3, "top");
}

// Reduce spacing after buttons
.nhsuk-button {
@include nhsuk-responsive-margin(4, "bottom", $adjustment: $nhsuk-button-shadow-size);
Expand Down
2 changes: 2 additions & 0 deletions app/assets/sass/components/_reading.scss
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,5 @@
padding-top: nhsuk-spacing(4);
}
}


6 changes: 3 additions & 3 deletions app/views/_includes/reading/image-warnings.njk
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
{% endset %}
{% set warningsHtml %}
{{ warningsHtml | safe }}
<h3 class="nhsuk-heading-s nhsuk-u-margin-bottom-2 {{ 'nhsuk-u-margin-top-4' if warningsHtml }}">Additional images</h3>
<h3 class="nhsuk-heading-s nhsuk-u-margin-bottom-2 {{ 'nhsuk-u-margin-top-0' if warningsHtml }}">Additional images</h3>
<p>{{ additionalImagesDetail | trim }}</p>
{% endset %}
{% endif %}
Expand Down Expand Up @@ -73,7 +73,7 @@

{% set warningsHtml %}
{{ warningsHtml | safe }}
<h3 class="nhsuk-heading-s nhsuk-u-margin-bottom-2 {{ 'nhsuk-u-margin-top-4' if warningsHtml }}">Not all mammograms taken</h3>
<h3 class="nhsuk-heading-s nhsuk-u-margin-bottom-2 {{ 'nhsuk-u-margin-top-0' if warningsHtml }}">Not all mammograms taken</h3>
{{ incompleteDetails | safe }}
{% endset %}
{% endif %}
Expand All @@ -82,7 +82,7 @@
{% if event.mammogramData.notesForReader %}
{% set warningsHtml %}
{{ warningsHtml | safe }}
<h3 class="nhsuk-heading-s nhsuk-u-margin-bottom-2 {{ 'nhsuk-u-margin-top-4' if warningsHtml }}">Notes for reader</h3>
<h3 class="nhsuk-heading-s nhsuk-u-margin-bottom-2 {{ 'nhsuk-u-margin-top-0' if warningsHtml }}">Notes for reader</h3>
<p>{{ event.mammogramData.notesForReader }}</p>
{% endset %}
{% endif %}
Expand Down
5 changes: 5 additions & 0 deletions app/views/_templates/layout-reading.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
{# Script to show loading state in PACS viewer when navigating away, and inspect panel (press I) #}
{% block bodyEnd %}
{{ super() }}

{# Fullscreen toggle and auto-scroll to status bar on page load #}
{% if isReadingWorkflow %}
<script type="module" src="/assets/javascript/reading-scroll.js"></script>
{% endif %}
{% if isReadingWorkflow and participant %}
<script>
(function() {
Expand Down
Loading