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
155 changes: 0 additions & 155 deletions .eslintrc.js

This file was deleted.

58 changes: 31 additions & 27 deletions app/assets/javascript/keyboard-shortcuts.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
// app/assets/javascript/keyboard-shortcuts.js
// Keyboard shortcut handling for reading workflow

const CHANNEL_NAME = "mammogram-viewer"
const CHANNEL_NAME = 'mammogram-viewer'

/**
* Play an alert sound when shortcut is blocked (e.g., during lockout)
*/
const playAlertSound = () => {
// Create a short beep using Web Audio API
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)()
const audioContext = new (
window.AudioContext || window.webkitAudioContext
)()
const oscillator = audioContext.createOscillator()
const gainNode = audioContext.createGain()

oscillator.connect(gainNode)
gainNode.connect(audioContext.destination)

oscillator.frequency.value = 440 // A4 note
oscillator.type = "sine"
oscillator.type = 'sine'
gainNode.gain.value = 0.3

oscillator.start()
oscillator.stop(audioContext.currentTime + 0.15) // Short beep
} catch (e) {
Expand All @@ -34,9 +36,9 @@ const isInFormField = (element) => {
if (!element) return false
const tagName = element.tagName.toLowerCase()
return (
tagName === "input" ||
tagName === "textarea" ||
tagName === "select" ||
tagName === 'input' ||
tagName === 'textarea' ||
tagName === 'select' ||
element.isContentEditable
)
}
Expand All @@ -45,8 +47,8 @@ const isInFormField = (element) => {
* Check if the opinion form is locked (during initial delay)
*/
const isOpinionLocked = () => {
const form = document.querySelector("[data-reading-opinion-locked]")
return form && form.getAttribute("data-reading-opinion-locked") === "true"
const form = document.querySelector('[data-reading-opinion-locked]')
return form && form.getAttribute('data-reading-opinion-locked') === 'true'
}

/**
Expand All @@ -58,28 +60,28 @@ const isOpinionLocked = () => {
const triggerShortcut = (key) => {
// Check if opinion is locked - play alert sound if so
if (isOpinionLocked()) {
console.log("[Shortcut] Blocked - opinion is locked")
console.log('[Shortcut] Blocked - opinion is locked')
playAlertSound()
return false
}

// First, try button/submit elements with data-shortcut
const button = document.querySelector(`[data-shortcut="${key}"]`)
if (button && !button.disabled) {
console.log("[Shortcut] Triggering:", key)
console.log('[Shortcut] Triggering:', key)
button.click()
return true
}

// Then, try radio inputs with data-shortcut-radio (attribute is on the input)
const radio = document.querySelector(`input[data-shortcut-radio="${key}"]`)
if (radio && !radio.disabled) {
console.log("[Shortcut] Triggering:", key)
console.log('[Shortcut] Triggering:', key)
radio.checked = true
radio.dispatchEvent(new Event("change", { bubbles: true }))
radio.dispatchEvent(new Event('change', { bubbles: true }))

// Submit the form
const form = radio.closest("form")
const form = radio.closest('form')
if (form) {
form.submit()
}
Expand All @@ -95,12 +97,12 @@ const triggerShortcut = (key) => {
const initReadingShortcuts = () => {
// Get the broadcast channel for cross-window communication
let channel = null
if (typeof BroadcastChannel !== "undefined") {
if (typeof BroadcastChannel !== 'undefined') {
channel = new BroadcastChannel(CHANNEL_NAME)
}

// Handle local keyboard events
document.addEventListener("keydown", (e) => {
document.addEventListener('keydown', (e) => {
// Ignore if in a form field
if (isInFormField(e.target)) return

Expand All @@ -113,8 +115,8 @@ const initReadingShortcuts = () => {

// Listen for shortcut messages from PACS viewer
if (channel) {
channel.addEventListener("message", (event) => {
if (event.data.type === "shortcut") {
channel.addEventListener('message', (event) => {
if (event.data.type === 'shortcut') {
triggerShortcut(event.data.key)
}
})
Expand All @@ -127,16 +129,16 @@ const initReadingShortcuts = () => {
*/
const initViewerShortcutForwarding = () => {
let channel = null
if (typeof BroadcastChannel !== "undefined") {
if (typeof BroadcastChannel !== 'undefined') {
channel = new BroadcastChannel(CHANNEL_NAME)
}

if (!channel) return

// Define which keys to forward to the reading page
const forwardKeys = ["n", "t", "r"]
const forwardKeys = ['n', 't', 'r']

document.addEventListener("keydown", (e) => {
document.addEventListener('keydown', (e) => {
// Ignore if modifier keys are pressed
if (e.ctrlKey || e.altKey || e.metaKey) return

Expand All @@ -145,7 +147,7 @@ const initViewerShortcutForwarding = () => {
// Forward reading shortcuts to the reading page
if (forwardKeys.includes(key)) {
channel.postMessage({
type: "shortcut",
type: 'shortcut',
key: key,
timestamp: Date.now()
})
Expand All @@ -154,9 +156,11 @@ const initViewerShortcutForwarding = () => {
}

// Auto-initialise on reading pages (pages with shortcut elements)
document.addEventListener("DOMContentLoaded", () => {
document.addEventListener('DOMContentLoaded', () => {
// Check if this page has any shortcut elements (buttons or radios)
const hasShortcuts = document.querySelector("[data-shortcut], [data-shortcut-radio]")
const hasShortcuts = document.querySelector(
'[data-shortcut], [data-shortcut-radio]'
)
if (hasShortcuts) {
initReadingShortcuts()
}
Expand Down
10 changes: 7 additions & 3 deletions app/assets/javascript/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,13 @@ document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
opinionBanner.classList.add('app-reading-opinion-banner--fade-out')
// Remove from DOM after the CSS transition (0.2s) completes
opinionBanner.addEventListener('transitionend', () => {
opinionBanner.remove()
}, { once: true })
opinionBanner.addEventListener(
'transitionend',
() => {
opinionBanner.remove()
},
{ once: true }
)
}, delay)
}

Expand Down
Loading