Enhance local viewer: implement centralized hide functionality and improve close handling for resource overlays

This commit is contained in:
Ross
2025-12-15 10:08:14 +00:00
parent 18dbb27a3e
commit 654d0ae686
2 changed files with 188 additions and 181 deletions
+144 -142
View File
@@ -1,8 +1,8 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Viewer</title> <title>Viewer</title>
<style type="text/css"> <style type="text/css">
body, html body, html
{ {
@@ -11,177 +11,179 @@
#viewer #viewer
{ {
position:absolute; left: 0; right: 0; bottom: 0; top: 0px; position:absolute; left: 0; right: 0; bottom: 0; top: 0px;
} }
</style> </style>
</head> </head>
<body> <body>
<p>Awating load request</p> <p>Awating load request</p>
This page will display series when loaded from other pages. This page will display series when loaded from other pages.
<div id="viewerContainer" style="width:100%; height:100%; position:relative;"> <div id="viewerContainer" style="width:100%; height:100%; position:relative;">
<iframe id="viewer" style="width: 100%; height: 100%; border: none; padding: 0px;" ></iframe> <iframe id="viewer" style="width: 100%; height: 100%; border: none; padding: 0px;" ></iframe>
<div id="resourceDisplay" style="display:none; position:absolute; inset:0; background:#000; align-items:center; justify-content:center; z-index:100000;"> <div id="resourceDisplay" style="display:none; position:absolute; inset:0; background:#000; align-items:center; justify-content:center; z-index:100000;">
<div id="resourceInner" style="position:relative; width:100%; height:100%; display:flex; align-items:center; justify-content:center;"> <div id="resourceInner" style="position:relative; width:100%; height:100%; display:flex; align-items:center; justify-content:center;">
</div>
<button id="resourceClose" style="position:absolute; top:8px; right:8px; z-index:9999;" class="btn btn-sm btn-light">Close</button>
</div> </div>
<button id="resourceClose" style="position:absolute; top:8px; right:8px; z-index:9999;" class="btn btn-sm btn-light">Close</button>
</div> </div>
</div>
<script type="text/javascript"> <script type="text/javascript">
var win2 = false; var win2 = false;
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
document.title = "Case: {{current_case.title}}"; document.title = "Case: {{current_case.title}}";
console.log('ready'); console.log('ready');
const bc = new BroadcastChannel("viva_load"); const bc = new BroadcastChannel("viva_load");
function isMediaUrl(url) { function isMediaUrl(url) {
if (!url) return false; if (!url) return false;
const lower = url.toLowerCase(); const lower = url.toLowerCase();
if (lower.match(/\.(jpe?g|png|gif|webp|svg)(?:$|\?|#)/)) return 'image'; if (lower.match(/\.(jpe?g|png|gif|webp|svg)(?:$|\?|#)/)) return 'image';
if (lower.match(/\.(mp4|webm|ogg)(?:$|\?|#)/)) return 'video'; if (lower.match(/\.(mp4|webm|ogg)(?:$|\?|#)/)) return 'video';
if (lower.indexOf('youtube.com') !== -1 || lower.indexOf('youtu.be') !== -1) return 'embed'; if (lower.indexOf('youtube.com') !== -1 || lower.indexOf('youtu.be') !== -1) return 'embed';
if (lower.indexOf('vimeo.com') !== -1) return 'embed'; if (lower.indexOf('vimeo.com') !== -1) return 'embed';
return false; return false;
} }
function showResource(url) { function showResource(url) {
const type = isMediaUrl(url); const type = isMediaUrl(url);
const disp = document.getElementById('resourceDisplay'); const disp = document.getElementById('resourceDisplay');
const inner = document.getElementById('resourceInner'); const inner = document.getElementById('resourceInner');
inner.innerHTML = ''; inner.innerHTML = '';
if (!type) { if (!type) {
// fallback: open in iframe // fallback: open in iframe
document.getElementById('viewer').src = url; document.getElementById('viewer').src = url;
disp.style.display = 'none'; disp.style.display = 'none';
return; return;
} }
// create a transform wrapper so we can pan/zoom the content uniformly // create a transform wrapper so we can pan/zoom the content uniformly
const transformWrap = document.createElement('div'); const transformWrap = document.createElement('div');
transformWrap.style.display = 'flex'; transformWrap.style.display = 'flex';
transformWrap.style.alignItems = 'center'; transformWrap.style.alignItems = 'center';
transformWrap.style.justifyContent = 'center'; transformWrap.style.justifyContent = 'center';
transformWrap.style.width = '100%'; transformWrap.style.width = '100%';
transformWrap.style.height = '100%'; transformWrap.style.height = '100%';
transformWrap.style.overflow = 'hidden'; transformWrap.style.overflow = 'hidden';
transformWrap.style.touchAction = 'none'; transformWrap.style.touchAction = 'none';
transformWrap.style.position = 'relative'; transformWrap.style.position = 'relative';
if (type === 'image') { if (type === 'image') {
const img = document.createElement('img'); const img = document.createElement('img');
img.src = url; img.src = url;
img.style.maxWidth = '95%'; img.style.maxWidth = '95%';
img.style.maxHeight = '95%'; img.style.maxHeight = '95%';
img.style.objectFit = 'contain'; img.style.objectFit = 'contain';
img.style.cursor = 'grab'; img.style.cursor = 'grab';
img.draggable = false; img.draggable = false;
transformWrap.appendChild(img); transformWrap.appendChild(img);
} else if (type === 'video') { } else if (type === 'video') {
const v = document.createElement('video'); const v = document.createElement('video');
v.controls = true; v.autoplay = false; v.style.maxWidth = '95%'; v.style.maxHeight = '95%'; v.style.objectFit = 'contain'; v.controls = true; v.autoplay = false; v.style.maxWidth = '95%'; v.style.maxHeight = '95%'; v.style.objectFit = 'contain';
v.style.cursor = 'grab'; v.style.cursor = 'grab';
const s = document.createElement('source'); s.src = url; v.appendChild(s); transformWrap.appendChild(v); const s = document.createElement('source'); s.src = url; v.appendChild(s); transformWrap.appendChild(v);
} else if (type === 'embed') { } else if (type === 'embed') {
let embed = url; let embed = url;
try { try {
const m = url.match(/(?:v=|v\/|embed\/|youtu\.be\/)([A-Za-z0-9_-]{6,})/); const m = url.match(/(?:v=|v\/|embed\/|youtu\.be\/)([A-Za-z0-9_-]{6,})/);
if (m && m[1]) embed = 'https://www.youtube.com/embed/' + m[1]; if (m && m[1]) embed = 'https://www.youtube.com/embed/' + m[1];
else if (url.indexOf('vimeo.com') !== -1) { else if (url.indexOf('vimeo.com') !== -1) {
const m2 = url.match(/vimeo\.com\/(\d+)/); const m2 = url.match(/vimeo\.com\/(\d+)/);
if (m2 && m2[1]) embed = 'https://player.vimeo.com/video/' + m2[1]; if (m2 && m2[1]) embed = 'https://player.vimeo.com/video/' + m2[1];
} }
} catch(e){} } catch(e){}
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.src = embed; iframe.setAttribute('frameborder','0'); iframe.setAttribute('allowfullscreen',''); iframe.src = embed; iframe.setAttribute('frameborder','0'); iframe.setAttribute('allowfullscreen','');
iframe.style.width = '95%'; iframe.style.height = '85%'; iframe.style.maxWidth = '95%'; iframe.style.maxHeight = '95%'; iframe.style.width = '95%'; iframe.style.height = '85%'; iframe.style.maxWidth = '95%'; iframe.style.maxHeight = '95%';
transformWrap.appendChild(iframe); transformWrap.appendChild(iframe);
}
inner.appendChild(transformWrap);
// Setup zoom/pan controls for the transformWrap
(function enablePanZoom(target){
let scale = 1; let minScale = 0.5; let maxScale = 6; let startX=0, startY=0; let translateX=0, translateY=0; let dragging=false;
const content = target.firstElementChild;
function updateTransform(){
if (content) content.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
} }
inner.appendChild(transformWrap);
// Setup zoom/pan controls for the transformWrap
(function enablePanZoom(target){
let scale = 1; let minScale = 0.5; let maxScale = 6; let startX=0, startY=0; let translateX=0, translateY=0; let dragging=false;
const content = target.firstElementChild;
function updateTransform(){
if (content) content.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
}
// Wheel to zoom (allow wheel-only zoom while overlay is shown) // Wheel to zoom (allow wheel-only zoom while overlay is shown)
target.addEventListener('wheel', function(e){ target.addEventListener('wheel', function(e){
// require overlay active (safety) - if not active, ignore // require overlay active (safety) - if not active, ignore
const dispEl = document.getElementById('resourceDisplay'); const dispEl = document.getElementById('resourceDisplay');
if (!dispEl || dispEl.style.display === 'none') return; if (!dispEl || dispEl.style.display === 'none') return;
e.preventDefault(); e.preventDefault();
const delta = -e.deltaY || e.wheelDelta; const delta = -e.deltaY || e.wheelDelta;
const zoomFactor = delta > 0 ? 1.12 : 0.9; const zoomFactor = delta > 0 ? 1.12 : 0.9;
const newScale = Math.min(maxScale, Math.max(minScale, scale * zoomFactor)); const newScale = Math.min(maxScale, Math.max(minScale, scale * zoomFactor));
// adjust translate to zoom to mouse position // adjust translate to zoom to mouse position
const rect = content.getBoundingClientRect(); const rect = content.getBoundingClientRect();
const mx = e.clientX - rect.left; const my = e.clientY - rect.top; const mx = e.clientX - rect.left; const my = e.clientY - rect.top;
const dx = (mx - rect.width/2); const dy = (my - rect.height/2); const dx = (mx - rect.width/2); const dy = (my - rect.height/2);
// compute scale ratio // compute scale ratio
const ratio = newScale/scale - 1; const ratio = newScale/scale - 1;
translateX -= dx * ratio; translateX -= dx * ratio;
translateY -= dy * ratio; translateY -= dy * ratio;
scale = newScale; updateTransform(); scale = newScale; updateTransform();
}, {passive:false}); }, {passive:false});
// Pointer drag to pan // Pointer drag to pan
target.addEventListener('pointerdown', function(e){ target.addEventListener('pointerdown', function(e){
dragging = true; startX = e.clientX; startY = e.clientY; target.setPointerCapture(e.pointerId); content.style.cursor = 'grabbing'; dragging = true; startX = e.clientX; startY = e.clientY; target.setPointerCapture(e.pointerId); content.style.cursor = 'grabbing';
}); });
target.addEventListener('pointermove', function(e){ target.addEventListener('pointermove', function(e){
if (!dragging) return; e.preventDefault(); const dx = e.clientX - startX; const dy = e.clientY - startY; startX = e.clientX; startY = e.clientY; translateX += dx; translateY += dy; updateTransform(); if (!dragging) return; e.preventDefault(); const dx = e.clientX - startX; const dy = e.clientY - startY; startX = e.clientX; startY = e.clientY; translateX += dx; translateY += dy; updateTransform();
}); });
target.addEventListener('pointerup', function(e){ dragging = false; try{ target.releasePointerCapture(e.pointerId);}catch(_){} if (content) content.style.cursor = 'grab'; }); target.addEventListener('pointerup', function(e){ dragging = false; try{ target.releasePointerCapture(e.pointerId);}catch(_){} if (content) content.style.cursor = 'grab'; });
target.addEventListener('pointercancel', function(e){ dragging=false; if (content) content.style.cursor='grab'; }); target.addEventListener('pointercancel', function(e){ dragging=false; if (content) content.style.cursor='grab'; });
// double click to reset // double click to reset
target.addEventListener('dblclick', function(e){ scale = 1; translateX = 0; translateY = 0; updateTransform(); }); target.addEventListener('dblclick', function(e){ scale = 1; translateX = 0; translateY = 0; updateTransform(); });
// Escape to close // Escape to close
document.addEventListener('keydown', function(ev){ if (ev.key === 'Escape'){ document.getElementById('resourceDisplay').style.display='none'; document.getElementById('resourceInner').innerHTML=''; } }); document.addEventListener('keydown', function(ev){ if (ev.key === 'Escape'){ document.getElementById('resourceDisplay').style.display='none'; document.getElementById('resourceInner').innerHTML=''; } });
})(transformWrap); })(transformWrap);
} }
disp.style.display = 'flex'; disp.style.display = 'flex';
} }
// Centralized hide helper // Centralized hide helper
function hideResource() { function hideResource() {
const disp = document.getElementById('resourceDisplay'); const disp = document.getElementById('resourceDisplay');
const inner = document.getElementById('resourceInner'); const inner = document.getElementById('resourceInner');
if (disp) disp.style.display = 'none'; if (disp) disp.style.display = 'none';
if (inner) inner.innerHTML = ''; if (inner) inner.innerHTML = '';
} }
// Robust close: delegate clicks on the overlay so pointer capture doesn't block the button // Robust close: delegate clicks on the overlay so pointer capture doesn't block the button
(function(){ (function(){
const disp = document.getElementById('resourceDisplay'); const disp = document.getElementById('resourceDisplay');
if (!disp) return; if (!disp) return;
disp.addEventListener('click', function(e){ disp.addEventListener('click', function(e){
// if click was on the close button (or inside it), hide // if click was on the close button (or inside it), hide
const close = e.target.closest && e.target.closest('#resourceClose'); const close = e.target.closest && e.target.closest('#resourceClose');
if (close) { hideResource(); } if (close) { hideResource(); }
}); });
})(); })();
bc.onmessage = (event) => { bc.onmessage = (event) => {
console.log("message received"); console.log("message received");
console.log(event); console.log(event);
if (event.data.type == "open") { // Always hide any existing resource overlay on a new load
// If URL looks like an image/video/embed, render in the resource panel try { hideResource(); } catch (e) { /* ignore if not available */ }
if (event.data.url && isMediaUrl(event.data.url)) { if (event.data.type == "open") {
showResource(event.data.url); // If URL looks like an image/video/embed, render in the resource panel
if (event.data.url && isMediaUrl(event.data.url)) {
showResource(event.data.url);
bc.postMessage({"type": "open-complete", "url" : event.data.url});
return;
}
document.getElementById("viewer").src = event.data.url;
bc.postMessage({"type": "open-complete", "url" : event.data.url}); bc.postMessage({"type": "open-complete", "url" : event.data.url});
return;
} }
document.getElementById("viewer").src = event.data.url; };
bc.postMessage({"type": "open-complete", "url" : event.data.url}); });
} </script>
};
});
</script>
</body> </body>
</html> </html>
+44 -39
View File
@@ -86,7 +86,14 @@
document.title = "Case: {{current_case.title}}"; document.title = "Case: {{current_case.title}}";
console.log('ready'); console.log('ready');
const bc = new BroadcastChannel("viva_load"); const bc = new BroadcastChannel("viva_load");
// Centralized hide helper and delegated close handling for local viewer
function hideResourceLocal(){ const disp=document.getElementById('resourceDisplay'); const inner=document.getElementById('resourceInner'); if(disp) disp.style.display='none'; if(inner) inner.innerHTML=''; }
(function(){ const disp=document.getElementById('resourceDisplay'); if(!disp) return; disp.addEventListener('click', function(e){ const close = e.target.closest && e.target.closest('#resourceClose'); if (close) hideResourceLocal(); }); })();
bc.onmessage = (event) => { bc.onmessage = (event) => {
// Ensure any existing overlay is closed on new load
try { hideResourceLocal(); } catch(_) {}
console.log("message received"); console.log("message received");
console.log(event); console.log(event);
if (event.data.type == "open") { if (event.data.type == "open") {
@@ -141,46 +148,44 @@
document.addEventListener('keydown', function(ev){ if(ev.key==='Escape'){ document.getElementById('resourceDisplay').style.display='none'; document.getElementById('resourceInner').innerHTML=''; } }); document.addEventListener('keydown', function(ev){ if(ev.key==='Escape'){ document.getElementById('resourceDisplay').style.display='none'; document.getElementById('resourceInner').innerHTML=''; } });
})(transformWrap); })(transformWrap);
disp.style.display='flex'; return true; disp.style.display='flex'; return true;
}
// Centralized hide helper and delegated close handling
function hideResourceLocal(){ const disp=document.getElementById('resourceDisplay'); const inner=document.getElementById('resourceInner'); if(disp) disp.style.display='none'; if(inner) inner.innerHTML=''; }
(function(){ const disp=document.getElementById('resourceDisplay'); if(!disp) return; disp.addEventListener('click', function(e){ const close = e.target.closest && e.target.closest('#resourceClose'); if (close) hideResourceLocal(); }); })();
if (event.data.url && isMediaUrl(event.data.url)) {
// show resource overlay and do not attempt to mount DICOM viewer
if (showResource(event.data.url)) {
bc.postMessage({"type": "open-complete-local"});
return;
}
}
// Hide modal when data is loaded
document.getElementById("awaiting-modal").style.display = "none";
document.getElementById("root").dataset.images = event.data.images;
if (
event.data.viewerstate &&
Object.keys(event.data.viewerstate).length > 0
) {
document.getElementById("root").dataset.viewerstate = event.data.viewerstate;
} else {
delete document.getElementById("root").dataset.viewerstate;
}
console.log("Annotations:", event.data.annotations);
if (
event.data.annotations &&
Object.keys(event.data.annotations).length > 0
) {
document.getElementById("root").dataset.annotationjson = event.data.annotations;
} else {
delete document.getElementById("root").dataset.annotationjson;
}
window.mountDicomViewers();
bc.postMessage({"type": "open-complete-local"});
} }
};
if (event.data.url && isMediaUrl(event.data.url)) {
// show resource overlay and do not attempt to mount DICOM viewer
if (showResource(event.data.url)) {
bc.postMessage({"type": "open-complete-local"});
return;
}
}
// Hide modal when data is loaded
document.getElementById("awaiting-modal").style.display = "none";
document.getElementById("root").dataset.images = event.data.images;
if (
event.data.viewerstate &&
Object.keys(event.data.viewerstate).length > 0
) {
document.getElementById("root").dataset.viewerstate = event.data.viewerstate;
} else {
delete document.getElementById("root").dataset.viewerstate;
}
console.log("Annotations:", event.data.annotations);
if (
event.data.annotations &&
Object.keys(event.data.annotations).length > 0
) {
document.getElementById("root").dataset.annotationjson = event.data.annotations;
} else {
delete document.getElementById("root").dataset.annotationjson;
}
window.mountDicomViewers();
bc.postMessage({"type": "open-complete-local"});
}
};
}); });
</script> </script>