Skip to content

Commit dc48ae6

Browse files
Add simulated PACS image viewer window (#72)
1 parent da0a4ec commit dc48ae6

8 files changed

Lines changed: 673 additions & 3 deletions

File tree

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
// app/assets/javascript/mammogram-viewer.js
2+
3+
// Single global variable to store the window reference
4+
let mammogramWindow = null;
5+
6+
// Track what was the last event ID that had a viewer open
7+
let currentEventId = '';
8+
9+
// Global flag to indicate if we're in a reading context
10+
window.inReadingContext = false;
11+
12+
// Storage key for viewer state
13+
const VIEWER_STORAGE_KEY = 'mammogramViewerOpen';
14+
15+
// Flag to track if we're in the process of a page navigation/refresh
16+
let isNavigating = false;
17+
18+
// Check for viewer flag on page load
19+
document.addEventListener('DOMContentLoaded', function() {
20+
// Reset navigation flag
21+
isNavigating = false;
22+
23+
// Check if this page should show the viewer
24+
const viewerMeta = document.querySelector('meta[name="mammogram-viewer"]');
25+
const shouldShowViewer = viewerMeta?.getAttribute('content') === 'show';
26+
const participantName = document.querySelector('meta[name="participant-name"]')?.getAttribute('content');
27+
const eventId = document.querySelector('meta[name="event-id"]')?.getAttribute('content');
28+
29+
console.log("Mammogram viewer check:", shouldShowViewer ? "SHOW" : "HIDE", "Event ID:", eventId);
30+
31+
// Update our reading context flag
32+
window.inReadingContext = shouldShowViewer;
33+
34+
// Check for stored state (helps with page refreshes)
35+
try {
36+
const storedViewerState = localStorage.getItem(VIEWER_STORAGE_KEY);
37+
if (storedViewerState && !mammogramWindow) {
38+
// We have stored state but no window - this is likely a page refresh
39+
console.log("Found stored viewer state during page load");
40+
}
41+
} catch (e) {
42+
console.error("Error checking stored viewer state:", e);
43+
}
44+
45+
// If we're in a reading context with metadata present
46+
if (shouldShowViewer && participantName) {
47+
// Check if we need to update the viewer
48+
const isSameEvent = eventId && eventId === currentEventId;
49+
50+
if (!isSameEvent || !MammogramViewer.isOpen()) {
51+
// Update tracking variables
52+
currentEventId = eventId || '';
53+
54+
// Open or update viewer
55+
MammogramViewer.open(participantName);
56+
} else {
57+
console.log("Same event, not updating viewer");
58+
}
59+
} else if (!shouldShowViewer) {
60+
// We're on a page that should NOT show the viewer
61+
console.log("Page lacks viewer meta tag or has it set to hide - closing viewer");
62+
MammogramViewer.close();
63+
currentEventId = '';
64+
}
65+
});
66+
67+
// Message handler to track viewer status
68+
window.addEventListener('message', function(event) {
69+
if (event.data === 'viewer-alive') {
70+
console.log("Received alive signal from mammogram viewer");
71+
}
72+
});
73+
74+
// Simple viewer manager object
75+
const MammogramViewer = {
76+
// Open or update the mammogram viewer window
77+
open: function(participantName) {
78+
// Check if we already have a window reference that's valid
79+
try {
80+
if (mammogramWindow && mammogramWindow.closed) {
81+
mammogramWindow = null;
82+
localStorage.removeItem(VIEWER_STORAGE_KEY);
83+
}
84+
} catch (e) {
85+
mammogramWindow = null;
86+
localStorage.removeItem(VIEWER_STORAGE_KEY);
87+
}
88+
89+
// Create window if it doesn't exist
90+
if (!mammogramWindow) {
91+
this.openNew(participantName);
92+
} else {
93+
// Update existing window without reopening it
94+
this.updateExisting(participantName);
95+
}
96+
97+
// Force focus back to parent window to prevent focus stealing
98+
setTimeout(function() {
99+
window.focus();
100+
}, 100);
101+
102+
return mammogramWindow;
103+
},
104+
105+
// Open a new viewer window
106+
openNew: function(participantName) {
107+
const url = `/reading/mammogram-viewer?name=${encodeURIComponent(participantName)}&ts=${Date.now()}`;
108+
console.log("Opening new mammogram viewer");
109+
110+
// Force external window behavior with specific window features
111+
// Note: Using 'alwaysRaised=yes' and 'popup=yes' helps ensure it opens as a window
112+
mammogramWindow = window.open(
113+
url,
114+
'mammogramViewer',
115+
'width=700,height=900,resizable=yes,scrollbars=no,status=no,location=no,toolbar=no,menubar=no,alwaysRaised=yes,popup=yes'
116+
);
117+
118+
// Store window information
119+
if (mammogramWindow) {
120+
try {
121+
localStorage.setItem(VIEWER_STORAGE_KEY, 'true');
122+
console.log("Mammogram viewer opened");
123+
124+
// Ensure parent window keeps focus
125+
setTimeout(function() {
126+
window.focus();
127+
}, 50);
128+
} catch (e) {
129+
console.error('Error marking viewer as open:', e);
130+
}
131+
}
132+
},
133+
134+
// Update existing window without reopening it
135+
updateExisting: function(participantName) {
136+
let updatedSuccessfully = false;
137+
138+
try {
139+
// First try postMessage approach as it doesn't steal focus
140+
try {
141+
mammogramWindow.postMessage({
142+
type: 'update-participant',
143+
name: participantName
144+
}, '*');
145+
console.log("Sent postMessage update to viewer");
146+
updatedSuccessfully = true;
147+
} catch (postErr) {
148+
console.log("PostMessage update failed:", postErr);
149+
}
150+
151+
// Fall back to direct DOM manipulation only if postMessage failed
152+
if (!updatedSuccessfully) {
153+
console.log("Falling back to DOM update");
154+
// Remember current focus state - document.activeElement
155+
const previousFocus = document.activeElement;
156+
157+
const nameElement = mammogramWindow.document.getElementById('participant-name');
158+
if (nameElement) {
159+
nameElement.textContent = participantName;
160+
mammogramWindow.document.title = 'Mammogram: ' + participantName;
161+
console.log("Mammogram viewer updated for:", participantName);
162+
163+
// Re-randomize the images when switching participants
164+
if (typeof mammogramWindow.randomizeAllImages === 'function') {
165+
// Set a new random seed based on participant name
166+
const nameSeed = participantName.split('').reduce((sum, char, i) => sum + char.charCodeAt(0) * (i + 1), 0);
167+
mammogramWindow.Math.random = mammogramWindow.Math.seedrandom(nameSeed);
168+
169+
// Apply randomization
170+
mammogramWindow.randomizeAllImages();
171+
}
172+
173+
// Restore focus to the previous element
174+
if (previousFocus) {
175+
previousFocus.focus();
176+
} else {
177+
window.focus();
178+
}
179+
180+
updatedSuccessfully = true;
181+
}
182+
}
183+
} catch (e) {
184+
console.error('Could not update viewer window, opening new one', e);
185+
updatedSuccessfully = false;
186+
}
187+
188+
// If we couldn't update, create a new window
189+
if (!updatedSuccessfully) {
190+
try {
191+
if (mammogramWindow) {
192+
mammogramWindow.close();
193+
}
194+
} catch (err) {}
195+
196+
localStorage.removeItem(VIEWER_STORAGE_KEY);
197+
this.openNew(participantName);
198+
}
199+
200+
// Ensure parent window keeps focus
201+
window.focus();
202+
203+
return updatedSuccessfully;
204+
},
205+
206+
// Close the viewer window - but only if it actually exists
207+
close: function() {
208+
console.log("Attempting to close mammogram viewer");
209+
210+
// Only close if we're not in the middle of navigation
211+
if (isNavigating) {
212+
console.log("Navigation in progress, not closing viewer");
213+
return;
214+
}
215+
216+
// Only close from non-reading pages
217+
if (window.inReadingContext) {
218+
console.log("Still in reading context, not closing viewer");
219+
return;
220+
}
221+
222+
// Only try to close if we have a valid reference - don't search for windows
223+
if (mammogramWindow && !mammogramWindow.closed) {
224+
console.log("Closing viewer window");
225+
226+
// First try to signal a graceful close via message
227+
try {
228+
mammogramWindow.postMessage('please-close', '*');
229+
} catch (e) {
230+
console.error("Error sending close message:", e);
231+
}
232+
233+
// As a fallback, force close after a short delay
234+
setTimeout(function() {
235+
try {
236+
if (mammogramWindow && !mammogramWindow.closed) {
237+
mammogramWindow.close();
238+
}
239+
} catch (e) {
240+
console.error("Error force closing viewer:", e);
241+
}
242+
mammogramWindow = null;
243+
}, 300);
244+
}
245+
246+
// Clean up local storage
247+
try {
248+
localStorage.removeItem(VIEWER_STORAGE_KEY);
249+
} catch (e) {
250+
console.error("Error removing stored state:", e);
251+
}
252+
},
253+
254+
// Check if viewer is open
255+
isOpen: function() {
256+
// Only check our direct reference, don't try to find windows
257+
try {
258+
return mammogramWindow && !mammogramWindow.closed;
259+
} catch (e) {
260+
// If there's an error accessing the window property, it's likely closed
261+
return false;
262+
}
263+
}
264+
};
265+
266+
// Make it globally available
267+
window.MammogramViewer = MammogramViewer;
268+
269+
// Set the navigating flag on popstate events
270+
window.addEventListener('popstate', function() {
271+
isNavigating = true;
272+
console.log("Navigation detected, setting isNavigating flag");
273+
274+
// If we're leaving reading context, close the viewer
275+
if (window.inReadingContext && !window.location.pathname.includes('/reading/batch/')) {
276+
console.log("Navigation away from reading detected - flagging for closure");
277+
window.inReadingContext = false;
278+
}
279+
});
280+
281+
// Before unload handler - set navigating flag
282+
window.addEventListener('beforeunload', function() {
283+
isNavigating = true;
284+
console.log("Page unloading, setting isNavigating flag");
285+
286+
// Store the current reading context state for potential page refresh
287+
if (window.inReadingContext && mammogramWindow && !mammogramWindow.closed) {
288+
try {
289+
localStorage.setItem('wasInReadingContext', 'true');
290+
} catch (e) {}
291+
}
292+
});

app/data/session-data-defaults.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const defaultSettings = {
5959
confirmNormal: 'false',
6060
showRemaining: 'true',
6161
showRepeatsTag: 'false',
62+
showPacsViewer: 'true',
6263
},
6364
}
6465

app/views/_includes/scripts.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@
55
{% endif %}
66

77
<!-- Add any custom scripts -->
8+
{% if data.settings.reading.showPacsViewer | falsify %}
9+
<script src="/js/mammogram-viewer.js"></script>
10+
{% endif %}

app/views/_templates/layout-app.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,12 @@
157157
{% endif %}
158158

159159
{% endblock %}
160+
161+
162+
{% block head %}
163+
{{ super() }}
164+
{% block mammogramViewer %}
165+
<!-- Default: mammogram viewer should be hidden -->
166+
<meta name="mammogram-viewer" content="hide">
167+
{% endblock %}
168+
{% endblock head %}

app/views/_templates/layout-reading.html

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@
7171
<div class="app-reading-status__row">
7272
<div class="app-reading-status__participant">
7373
<div class="app-reading-status__detail">
74-
{# <strong>Participant:</strong> #}
7574
<strong>{{ participant | getFullName }}</strong>
7675
{% if participant | getAge %}
7776
<span>({{ participant | getAge }} years)</span>
@@ -178,4 +177,16 @@
178177

179178

180179
{% endif %}
181-
{% endblock pageNavigation %}
180+
{% endblock pageNavigation %}
181+
182+
{% block mammogramViewer %}
183+
{% if participant and data.settings.reading.showPacsViewer | falsify %}
184+
<!-- Meta tags to control mammogram viewer -->
185+
<meta name="mammogram-viewer" content="show">
186+
<meta name="participant-name" content="{{ participant | getFullName }}">
187+
<meta name="event-id" content="{{ eventId }}">
188+
{% else %}
189+
<!-- Default: mammogram viewer should be hidden -->
190+
<meta name="mammogram-viewer" content="hide">
191+
{% endif %}
192+
{% endblock %}

app/views/reading/batch.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,4 @@ <h2>Cases ({{ events | length }})</h2>
310310
{% endif %}
311311
</div>
312312
</div>
313-
{% endblock %}
313+
{% endblock %}

0 commit comments

Comments
 (0)