Enhance resource handling: add support for loading and displaying non-DICOM resources (images, videos, embeds) in viewer overlays
This commit is contained in:
@@ -108,51 +108,51 @@
|
|||||||
|
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="btn-group link-buttons" role="group">
|
<div class="btn-group link-buttons" role="group">
|
||||||
<button type="button" class="btn btn-primary" onclick="window.location.href='/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}'">View case in OHIF</button>
|
<button type="button" class="btn btn-primary" onclick="window.location.href='/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}'">View case in OHIF</button>
|
||||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<span class="visually-hidden">Toggle dropdown</span>
|
<span class="visually-hidden">Toggle dropdown</span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" target="_blank" href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">Open in new tab</a>
|
<a class="dropdown-item" target="_blank" href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">Open in new tab</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="clearfix mb-2">
|
<div class="clearfix mb-2">
|
||||||
<div class="float-start">
|
<div class="float-start">
|
||||||
<span class="id">ID: {{ case.id }}</span>
|
<span class="id">ID: {{ case.id }}</span>
|
||||||
<span class="mx-1">/</span>
|
<span class="mx-1">/</span>
|
||||||
<span class="date">{{ case.created_date|date:"d/m/Y" }}</span>
|
<span class="date">{{ case.created_date|date:"d/m/Y" }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="float-end text-end">
|
<div class="float-end text-end">
|
||||||
{# Subspecialty badges (preserve any anchor returned by get_link) #}
|
{# Subspecialty badges (preserve any anchor returned by get_link) #}
|
||||||
{% if case.subspecialty.all %}
|
{% if case.subspecialty.all %}
|
||||||
{% for sub in case.subspecialty.all %}
|
{% for sub in case.subspecialty.all %}
|
||||||
<span class="badge bg-secondary ms-1">{{ sub.get_link|safe }}</span>
|
<span class="badge bg-secondary ms-1">{{ sub.get_link|safe }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted me-2">No subspecialty</span>
|
<span class="text-muted me-2">No subspecialty</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Pathological process badges #}
|
{# Pathological process badges #}
|
||||||
{% if case.pathological_process.all %}
|
{% if case.pathological_process.all %}
|
||||||
{% for p in case.pathological_process.all %}
|
{% for p in case.pathological_process.all %}
|
||||||
<span class="badge bg-secondary ms-1">{{ p.get_link|safe }}</span>
|
<span class="badge bg-secondary ms-1">{{ p.get_link|safe }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Diagnostic certainty as a badge #}
|
{# Diagnostic certainty as a badge #}
|
||||||
Diagnostic certainty: <span class="badge bg-info text-dark ms-2">{{ case.get_diagnostic_certainty_display }}</span>
|
Diagnostic certainty: <span class="badge bg-info text-dark ms-2">{{ case.get_diagnostic_certainty_display }}</span>
|
||||||
{# Normal toggle HTMX block #}
|
{# Normal toggle HTMX block #}
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
{% include "atlas/partials/_normal_toggle.html" %}
|
{% include "atlas/partials/_normal_toggle.html" %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<details id="dicom-viewer-details">
|
<details id="dicom-viewer-details">
|
||||||
<summary>Viewer</summary>
|
<summary>Viewer</summary>
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
<div class="col-12 col-md-12">
|
<div class="col-12 col-md-12">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header">History</div>
|
<div class="card-header">History</div>
|
||||||
@@ -492,12 +492,12 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
{% if casedetail %}
|
{% if casedetail %}
|
||||||
{% include 'atlas/partials/collection_question_block.html' with can_edit=can_edit %}
|
{% include 'atlas/partials/collection_question_block.html' with can_edit=can_edit %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,22 @@
|
|||||||
<div class="question-block-snippet" style="display:none;">
|
<div class="question-block-snippet" style="display:none;">
|
||||||
{% include 'atlas/partials/collection_question_block.html' with casedetail=casedetail %}
|
{% include 'atlas/partials/collection_question_block.html' with casedetail=casedetail %}
|
||||||
</div>
|
</div>
|
||||||
|
{# Attached resources for this case: allow loading into the shared viewer #}
|
||||||
|
{% if case.caseresource_set.exists %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong>Resources:</strong>
|
||||||
|
<div class="d-flex flex-column gap-1">
|
||||||
|
{% for cr in case.caseresource_set.all %}
|
||||||
|
{% with res=cr.resource %}
|
||||||
|
<div>
|
||||||
|
<span class="me-2">{{ res.name }}</span>
|
||||||
|
<button class="open-resource btn btn-sm btn-outline-primary ms-1" data-src="{% if res.file %}{{ res.file.url }}{% else %}{{ res.url }}{% endif %}" data-title="{{ res.name|escapejs }}">Load</button>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if case.display_sets.all %}
|
{% if case.display_sets.all %}
|
||||||
<details class="displayset-detail"><summary>Display Sets:</summary>
|
<details class="displayset-detail"><summary>Display Sets:</summary>
|
||||||
{% for ds in case.display_sets.all %}
|
{% for ds in case.display_sets.all %}
|
||||||
@@ -335,6 +351,19 @@
|
|||||||
//}
|
//}
|
||||||
//win2 = openSecondaryWindow(url);
|
//win2 = openSecondaryWindow(url);
|
||||||
})
|
})
|
||||||
|
// Load resource into linked viewer
|
||||||
|
$('.open-resource').click(function() {
|
||||||
|
var src = $(this).data('src');
|
||||||
|
var title = $(this).data('title') || 'Resource';
|
||||||
|
$('#loading-case').show();
|
||||||
|
$("#open-viewer-local").addClass("flash-button");
|
||||||
|
$('#current-case-title').html("<span class='title'>Resource:</span> "+title);
|
||||||
|
$('#current-case-history').html('');
|
||||||
|
$('#current-case-discussion').html('');
|
||||||
|
$('#current-case-report').html('');
|
||||||
|
|
||||||
|
bc.postMessage({"type": "open", "url": src});
|
||||||
|
});
|
||||||
$('.send-message').click(function() {
|
$('.send-message').click(function() {
|
||||||
var url = $(this).data('target');
|
var url = $(this).data('target');
|
||||||
bc.postMessage(url);
|
bc.postMessage(url);
|
||||||
|
|||||||
@@ -18,7 +18,14 @@
|
|||||||
<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.
|
||||||
<iframe id="viewer" style="width: 100%; height: 100%; border: none; padding: 0px;" ></iframe>
|
<div id="viewerContainer" style="width:100%; height:100%; position:relative;">
|
||||||
|
<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="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>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var win2 = false;
|
var win2 = false;
|
||||||
@@ -27,12 +34,149 @@
|
|||||||
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) {
|
||||||
|
if (!url) return false;
|
||||||
|
const lower = url.toLowerCase();
|
||||||
|
if (lower.match(/\.(jpe?g|png|gif|webp|svg)(?:$|\?|#)/)) return 'image';
|
||||||
|
if (lower.match(/\.(mp4|webm|ogg)(?:$|\?|#)/)) return 'video';
|
||||||
|
if (lower.indexOf('youtube.com') !== -1 || lower.indexOf('youtu.be') !== -1) return 'embed';
|
||||||
|
if (lower.indexOf('vimeo.com') !== -1) return 'embed';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResource(url) {
|
||||||
|
const type = isMediaUrl(url);
|
||||||
|
const disp = document.getElementById('resourceDisplay');
|
||||||
|
const inner = document.getElementById('resourceInner');
|
||||||
|
inner.innerHTML = '';
|
||||||
|
if (!type) {
|
||||||
|
// fallback: open in iframe
|
||||||
|
document.getElementById('viewer').src = url;
|
||||||
|
disp.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// create a transform wrapper so we can pan/zoom the content uniformly
|
||||||
|
const transformWrap = document.createElement('div');
|
||||||
|
transformWrap.style.display = 'flex';
|
||||||
|
transformWrap.style.alignItems = 'center';
|
||||||
|
transformWrap.style.justifyContent = 'center';
|
||||||
|
transformWrap.style.width = '100%';
|
||||||
|
transformWrap.style.height = '100%';
|
||||||
|
transformWrap.style.overflow = 'hidden';
|
||||||
|
transformWrap.style.touchAction = 'none';
|
||||||
|
transformWrap.style.position = 'relative';
|
||||||
|
|
||||||
|
if (type === 'image') {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = url;
|
||||||
|
img.style.maxWidth = '95%';
|
||||||
|
img.style.maxHeight = '95%';
|
||||||
|
img.style.objectFit = 'contain';
|
||||||
|
img.style.cursor = 'grab';
|
||||||
|
img.draggable = false;
|
||||||
|
transformWrap.appendChild(img);
|
||||||
|
} else if (type === '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.style.cursor = 'grab';
|
||||||
|
const s = document.createElement('source'); s.src = url; v.appendChild(s); transformWrap.appendChild(v);
|
||||||
|
} else if (type === 'embed') {
|
||||||
|
let embed = url;
|
||||||
|
try {
|
||||||
|
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];
|
||||||
|
else if (url.indexOf('vimeo.com') !== -1) {
|
||||||
|
const m2 = url.match(/vimeo\.com\/(\d+)/);
|
||||||
|
if (m2 && m2[1]) embed = 'https://player.vimeo.com/video/' + m2[1];
|
||||||
|
}
|
||||||
|
} catch(e){}
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
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%';
|
||||||
|
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})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wheel to zoom (allow wheel-only zoom while overlay is shown)
|
||||||
|
target.addEventListener('wheel', function(e){
|
||||||
|
// require overlay active (safety) - if not active, ignore
|
||||||
|
const dispEl = document.getElementById('resourceDisplay');
|
||||||
|
if (!dispEl || dispEl.style.display === 'none') return;
|
||||||
|
e.preventDefault();
|
||||||
|
const delta = -e.deltaY || e.wheelDelta;
|
||||||
|
const zoomFactor = delta > 0 ? 1.12 : 0.9;
|
||||||
|
const newScale = Math.min(maxScale, Math.max(minScale, scale * zoomFactor));
|
||||||
|
// adjust translate to zoom to mouse position
|
||||||
|
const rect = content.getBoundingClientRect();
|
||||||
|
const mx = e.clientX - rect.left; const my = e.clientY - rect.top;
|
||||||
|
const dx = (mx - rect.width/2); const dy = (my - rect.height/2);
|
||||||
|
// compute scale ratio
|
||||||
|
const ratio = newScale/scale - 1;
|
||||||
|
translateX -= dx * ratio;
|
||||||
|
translateY -= dy * ratio;
|
||||||
|
scale = newScale; updateTransform();
|
||||||
|
}, {passive:false});
|
||||||
|
|
||||||
|
// Pointer drag to pan
|
||||||
|
target.addEventListener('pointerdown', function(e){
|
||||||
|
dragging = true; startX = e.clientX; startY = e.clientY; target.setPointerCapture(e.pointerId); content.style.cursor = 'grabbing';
|
||||||
|
});
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
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'; });
|
||||||
|
|
||||||
|
// double click to reset
|
||||||
|
target.addEventListener('dblclick', function(e){ scale = 1; translateX = 0; translateY = 0; updateTransform(); });
|
||||||
|
|
||||||
|
// Escape to close
|
||||||
|
document.addEventListener('keydown', function(ev){ if (ev.key === 'Escape'){ document.getElementById('resourceDisplay').style.display='none'; document.getElementById('resourceInner').innerHTML=''; } });
|
||||||
|
})(transformWrap);
|
||||||
|
}
|
||||||
|
disp.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centralized hide helper
|
||||||
|
function hideResource() {
|
||||||
|
const disp = document.getElementById('resourceDisplay');
|
||||||
|
const inner = document.getElementById('resourceInner');
|
||||||
|
if (disp) disp.style.display = 'none';
|
||||||
|
if (inner) inner.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Robust close: delegate clicks on the overlay so pointer capture doesn't block the button
|
||||||
|
(function(){
|
||||||
|
const disp = document.getElementById('resourceDisplay');
|
||||||
|
if (!disp) return;
|
||||||
|
disp.addEventListener('click', function(e){
|
||||||
|
// if click was on the close button (or inside it), hide
|
||||||
|
const close = e.target.closest && e.target.closest('#resourceClose');
|
||||||
|
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") {
|
if (event.data.type == "open") {
|
||||||
|
// 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;
|
document.getElementById("viewer").src = event.data.url;
|
||||||
//document.querySelectorAll("[data-cy='seriesList-btn']")[0].click()
|
|
||||||
bc.postMessage({"type": "open-complete", "url" : event.data.url});
|
bc.postMessage({"type": "open-complete", "url" : event.data.url});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,6 +73,12 @@
|
|||||||
data-auto-cache-stack="true"
|
data-auto-cache-stack="true"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
<!-- Overlay for non-DICOM resources (images/videos/embeds) -->
|
||||||
|
<div id="resourceDisplay" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.96); z-index:100000; align-items:center; justify-content:center;">
|
||||||
|
<div id="resourceInner" style="max-width:95vw; max-height:95vh; display:flex; align-items:center; justify-content:center;"></div>
|
||||||
|
<button id="resourceClose" style="position:fixed; top:12px; right:12px; z-index:2001;" class="btn btn-sm btn-light">Close</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var win2 = false;
|
var win2 = false;
|
||||||
|
|
||||||
@@ -84,6 +90,71 @@
|
|||||||
console.log("message received");
|
console.log("message received");
|
||||||
console.log(event);
|
console.log(event);
|
||||||
if (event.data.type == "open") {
|
if (event.data.type == "open") {
|
||||||
|
// If a resource URL is provided (image/video/embed), show it in overlay
|
||||||
|
function isMediaUrl(url) {
|
||||||
|
if (!url) return false;
|
||||||
|
const lower = url.toLowerCase();
|
||||||
|
if (lower.match(/\.(jpe?g|png|gif|webp|svg)(?:$|\?|#)/)) return 'image';
|
||||||
|
if (lower.match(/\.(mp4|webm|ogg)(?:$|\?|#)/)) return 'video';
|
||||||
|
if (lower.indexOf('youtube.com') !== -1 || lower.indexOf('youtu.be') !== -1) return 'embed';
|
||||||
|
if (lower.indexOf('vimeo.com') !== -1) return 'embed';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResource(url) {
|
||||||
|
const type = isMediaUrl(url);
|
||||||
|
const disp = document.getElementById('resourceDisplay');
|
||||||
|
const inner = document.getElementById('resourceInner');
|
||||||
|
inner.innerHTML = '';
|
||||||
|
if (!type) return false;
|
||||||
|
|
||||||
|
const transformWrap = document.createElement('div');
|
||||||
|
transformWrap.style.display = 'flex'; transformWrap.style.alignItems = 'center'; transformWrap.style.justifyContent = 'center';
|
||||||
|
transformWrap.style.width = '100%'; transformWrap.style.height = '100%'; transformWrap.style.overflow = 'hidden'; transformWrap.style.touchAction = 'none';
|
||||||
|
|
||||||
|
if (type === 'image') {
|
||||||
|
const img = document.createElement('img'); img.src = url; img.style.maxWidth = '95%'; img.style.maxHeight = '95%'; img.style.objectFit = 'contain'; img.style.cursor='grab'; img.draggable=false; transformWrap.appendChild(img);
|
||||||
|
} else if (type === 'video') {
|
||||||
|
const v = document.createElement('video'); v.controls=true; v.autoplay=false; v.style.maxWidth='95%'; v.style.maxHeight='95%'; v.style.objectFit='contain'; const s=document.createElement('source'); s.src=url; v.appendChild(s); transformWrap.appendChild(v);
|
||||||
|
} else if (type === 'embed') {
|
||||||
|
let embed = url; try{ 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]; else if (url.indexOf('vimeo.com')!==-1){ const m2=url.match(/vimeo\.com\/(\d+)/); if(m2&&m2[1]) embed='https://player.vimeo.com/video/'+m2[1]; } }catch(e){}
|
||||||
|
const iframe=document.createElement('iframe'); iframe.src=embed; iframe.setAttribute('frameborder','0'); iframe.setAttribute('allowfullscreen',''); iframe.style.width='95%'; iframe.style.height='85%'; transformWrap.appendChild(iframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
inner.appendChild(transformWrap);
|
||||||
|
|
||||||
|
// pan/zoom
|
||||||
|
(function enablePanZoom(target){
|
||||||
|
let scale=1, minScale=0.5, maxScale=6, startX=0, startY=0, translateX=0, translateY=0, dragging=false;
|
||||||
|
const content=target.firstElementChild;
|
||||||
|
function update(){ if(content) content.style.transform=`translate(${translateX}px, ${translateY}px) scale(${scale})`; }
|
||||||
|
target.addEventListener('wheel', function(e){
|
||||||
|
const dispEl = document.getElementById('resourceDisplay');
|
||||||
|
if (!dispEl || dispEl.style.display === 'none') return;
|
||||||
|
e.preventDefault();
|
||||||
|
const delta=-e.deltaY||e.wheelDelta; const zoom=delta>0?1.12:0.9; const newScale=Math.min(maxScale, Math.max(minScale, scale*zoom)); const rect=content.getBoundingClientRect(); const mx=e.clientX-rect.left, my=e.clientY-rect.top; const ratio=newScale/scale-1; translateX -= (mx-rect.width/2)*ratio; translateY -= (my-rect.height/2)*ratio; scale=newScale; update();
|
||||||
|
}, {passive:false});
|
||||||
|
target.addEventListener('pointerdown', function(e){ dragging=true; startX=e.clientX; startY=e.clientY; target.setPointerCapture(e.pointerId); if(content) content.style.cursor='grabbing'; });
|
||||||
|
target.addEventListener('pointermove', function(e){ if(!dragging) return; e.preventDefault(); const dx=e.clientX-startX, dy=e.clientY-startY; startX=e.clientX; startY=e.clientY; translateX+=dx; translateY+=dy; update(); });
|
||||||
|
target.addEventListener('pointerup', function(e){ dragging=false; try{target.releasePointerCapture(e.pointerId)}catch(_){} if(content) content.style.cursor='grab'; });
|
||||||
|
target.addEventListener('dblclick', function(){ scale=1; translateX=0; translateY=0; update(); });
|
||||||
|
document.addEventListener('keydown', function(ev){ if(ev.key==='Escape'){ document.getElementById('resourceDisplay').style.display='none'; document.getElementById('resourceInner').innerHTML=''; } });
|
||||||
|
})(transformWrap);
|
||||||
|
|
||||||
|
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
|
// Hide modal when data is loaded
|
||||||
document.getElementById("awaiting-modal").style.display = "none";
|
document.getElementById("awaiting-modal").style.display = "none";
|
||||||
document.getElementById("root").dataset.images = event.data.images;
|
document.getElementById("root").dataset.images = event.data.images;
|
||||||
|
|||||||
Reference in New Issue
Block a user