Skip to content

Commit 0569ec8

Browse files
11 Feb updates for ur (#242)
* Add keyboard shortcuts * Move view order to user setting * Rename view order setting * Reset PACS viewer when advancing case * Make it clearer how to return to default view
1 parent ae0fbbb commit 0569ec8

12 files changed

Lines changed: 477 additions & 40 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// app/assets/javascript/keyboard-shortcuts.js
2+
// Keyboard shortcut handling for reading workflow
3+
4+
const CHANNEL_NAME = "mammogram-viewer"
5+
6+
/**
7+
* Play an alert sound when shortcut is blocked (e.g., during lockout)
8+
*/
9+
const playAlertSound = () => {
10+
// Create a short beep using Web Audio API
11+
try {
12+
const audioContext = new (window.AudioContext || window.webkitAudioContext)()
13+
const oscillator = audioContext.createOscillator()
14+
const gainNode = audioContext.createGain()
15+
16+
oscillator.connect(gainNode)
17+
gainNode.connect(audioContext.destination)
18+
19+
oscillator.frequency.value = 440 // A4 note
20+
oscillator.type = "sine"
21+
gainNode.gain.value = 0.3
22+
23+
oscillator.start()
24+
oscillator.stop(audioContext.currentTime + 0.15) // Short beep
25+
} catch (e) {
26+
// Audio not supported, fail silently
27+
}
28+
}
29+
30+
/**
31+
* Check if the active element is a form field where shortcuts should be disabled
32+
*/
33+
const isInFormField = (element) => {
34+
if (!element) return false
35+
const tagName = element.tagName.toLowerCase()
36+
return (
37+
tagName === "input" ||
38+
tagName === "textarea" ||
39+
tagName === "select" ||
40+
element.isContentEditable
41+
)
42+
}
43+
44+
/**
45+
* Check if the opinion form is locked (during initial delay)
46+
*/
47+
const isOpinionLocked = () => {
48+
const form = document.querySelector("[data-reading-opinion-locked]")
49+
return form && form.getAttribute("data-reading-opinion-locked") === "true"
50+
}
51+
52+
/**
53+
* Find and trigger an element with a matching data-shortcut attribute (buttons)
54+
* or data-shortcut-radio attribute (radio inputs)
55+
* @param {string} key - The shortcut key (lowercase)
56+
* @returns {boolean} - True if a shortcut was triggered
57+
*/
58+
const triggerShortcut = (key) => {
59+
// Check if opinion is locked - play alert sound if so
60+
if (isOpinionLocked()) {
61+
console.log("[Shortcut] Blocked - opinion is locked")
62+
playAlertSound()
63+
return false
64+
}
65+
66+
// First, try button/submit elements with data-shortcut
67+
const button = document.querySelector(`[data-shortcut="${key}"]`)
68+
if (button && !button.disabled) {
69+
console.log("[Shortcut] Triggering:", key)
70+
button.click()
71+
return true
72+
}
73+
74+
// Then, try radio inputs with data-shortcut-radio (attribute is on the input)
75+
const radio = document.querySelector(`input[data-shortcut-radio="${key}"]`)
76+
if (radio && !radio.disabled) {
77+
console.log("[Shortcut] Triggering:", key)
78+
radio.checked = true
79+
radio.dispatchEvent(new Event("change", { bubbles: true }))
80+
81+
// Submit the form
82+
const form = radio.closest("form")
83+
if (form) {
84+
form.submit()
85+
}
86+
return true
87+
}
88+
89+
return false
90+
}
91+
92+
/**
93+
* Initialise keyboard shortcut handling on the reading page
94+
*/
95+
const initReadingShortcuts = () => {
96+
// Get the broadcast channel for cross-window communication
97+
let channel = null
98+
if (typeof BroadcastChannel !== "undefined") {
99+
channel = new BroadcastChannel(CHANNEL_NAME)
100+
}
101+
102+
// Handle local keyboard events
103+
document.addEventListener("keydown", (e) => {
104+
// Ignore if in a form field
105+
if (isInFormField(e.target)) return
106+
107+
// Ignore if modifier keys are pressed (except shift for future use)
108+
if (e.ctrlKey || e.altKey || e.metaKey) return
109+
110+
const key = e.key.toLowerCase()
111+
triggerShortcut(key)
112+
})
113+
114+
// Listen for shortcut messages from PACS viewer
115+
if (channel) {
116+
channel.addEventListener("message", (event) => {
117+
if (event.data.type === "shortcut") {
118+
triggerShortcut(event.data.key)
119+
}
120+
})
121+
}
122+
}
123+
124+
/**
125+
* Initialise keyboard shortcut forwarding from the PACS viewer
126+
* Call this from the mammogram viewer page
127+
*/
128+
const initViewerShortcutForwarding = () => {
129+
let channel = null
130+
if (typeof BroadcastChannel !== "undefined") {
131+
channel = new BroadcastChannel(CHANNEL_NAME)
132+
}
133+
134+
if (!channel) return
135+
136+
// Define which keys to forward to the reading page
137+
const forwardKeys = ["n", "t", "r"]
138+
139+
document.addEventListener("keydown", (e) => {
140+
// Ignore if modifier keys are pressed
141+
if (e.ctrlKey || e.altKey || e.metaKey) return
142+
143+
const key = e.key.toLowerCase()
144+
145+
// Forward reading shortcuts to the reading page
146+
if (forwardKeys.includes(key)) {
147+
channel.postMessage({
148+
type: "shortcut",
149+
key: key,
150+
timestamp: Date.now()
151+
})
152+
}
153+
})
154+
}
155+
156+
// Auto-initialise on reading pages (pages with shortcut elements)
157+
document.addEventListener("DOMContentLoaded", () => {
158+
// Check if this page has any shortcut elements (buttons or radios)
159+
const hasShortcuts = document.querySelector("[data-shortcut], [data-shortcut-radio]")
160+
if (hasShortcuts) {
161+
initReadingShortcuts()
162+
}
163+
})
164+
165+
// Expose for manual initialisation (e.g., from PACS viewer)
166+
window.KeyboardShortcuts = {
167+
initReadingShortcuts,
168+
initViewerShortcutForwarding,
169+
triggerShortcut
170+
}

app/assets/sass/components/_annotation.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
height: 22px;
3636
border-radius: 4px;
3737
background-color: $nhsuk-focus-colour;
38-
// color: #ffffff;
38+
color: $nhsuk-text-colour;
3939
font-size: 14px;
4040
font-weight: bold;
4141
display: flex;

app/assets/sass/components/_reading.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,9 @@
285285
}
286286
}
287287
}
288+
289+
// Keyboard shortcut hint styling
290+
.app-shortcut-hint {
291+
font-weight: 400;
292+
opacity: 0.8;
293+
}

app/data/session-data-defaults.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const defaultSettings = {
6363
darkMode: 'false',
6464
debugMode: 'false',
6565
showEnvironmentBanner: 'true',
66+
mammogramViewOrder: 'cc-first', // 'cc-first' | 'mlo-first'
6667
screening: {
6768
confirmIdentityOnCheckIn: 'true',
6869
manualImageCollection: 'true',

app/routes/reading.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -918,9 +918,7 @@ module.exports = (router) => {
918918

919919
// Redirect to next unread event or batch view if all done
920920
if (nextUnreadEvent) {
921-
res.redirect(
922-
`/reading/batch/${batchId}/events/${nextUnreadEvent.id}`
923-
)
921+
res.redirect(`/reading/batch/${batchId}/events/${nextUnreadEvent.id}`)
924922
} else {
925923
res.redirect(`/reading/batch/${batchId}`)
926924
}

app/views/_includes/scripts.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<script type="module" src="/assets/javascript/expandable-sections.js"></script>
1111
<script type="module" src="/assets/javascript/stepper-input.js"></script>
1212
<script type="module" src="/assets/javascript/mammogram-channel.js"></script>
13+
<script type="module" src="/assets/javascript/keyboard-shortcuts.js"></script>
1314

1415
<script>
1516
document.addEventListener('DOMContentLoaded', () => {

app/views/events/images-manual-details.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ <h2 aria-hidden="true">{{ viewsQuestionText }}?</h2>
5858
{{ (viewsQuestionText + " for the") | asVisuallyHiddenText }} Right breast
5959
</legend>
6060

61-
{% if data.config.reading.mammogramViewOrder == 'mlo-first' %}
61+
{% if data.settings.mammogramViewOrder == 'mlo-first' %}
6262
{{ appStepperInput({
6363
label: {
6464
text: "RMLO"
@@ -138,7 +138,7 @@ <h2 aria-hidden="true">{{ viewsQuestionText }}?</h2>
138138
{{ (viewsQuestionText + " for the") | asVisuallyHiddenText }} Left breast
139139
</legend>
140140

141-
{% if data.config.reading.mammogramViewOrder == 'mlo-first' %}
141+
{% if data.settings.mammogramViewOrder == 'mlo-first' %}
142142
{{ appStepperInput({
143143
label: {
144144
text: "LMLO"

0 commit comments

Comments
 (0)