diff --git a/atlas/static/atlas/js/markup_inserter.js b/atlas/static/atlas/js/markup_inserter.js
index a6787c3e..d58db45c 100644
--- a/atlas/static/atlas/js/markup_inserter.js
+++ b/atlas/static/atlas/js/markup_inserter.js
@@ -283,4 +283,153 @@
// Simplified detection: rely on explicit dataset attributes set by the widget
// When a textarea includes `data-current-case` and optionally `data-current-case-title`, that will be used as the default filter.
+ // --- Render inserted markup tokens as styled items with popup/modal actions ---
+ function injectMarkupStyles(){
+ if(document.getElementById('markup-inserter-styles')) return;
+ var s = document.createElement('style'); s.id = 'markup-inserter-styles';
+ s.textContent = '\n.markup-token{cursor:pointer;display:inline-block;margin:0 .25rem .25rem 0;padding:.25rem .5rem;border-radius:.375rem;font-size:0.85em;}\n.markup-popup{position:fixed;z-index:1060;background:#fff;border:1px solid rgba(0,0,0,.12);box-shadow:0 6px 18px rgba(0,0,0,.12);padding:.5rem;border-radius:.25rem;}\n.markup-modal-backdrop{position:fixed;left:0;right:0;top:0;bottom:0;background:rgba(0,0,0,.45);z-index:1080;display:flex;align-items:flex-start;justify-content:center;padding-top:4vh;}\n.markup-modal{background:#fff;border-radius:.375rem;max-width:95%;max-height:85vh;overflow:auto;}\n';
+ document.head.appendChild(s);
+ }
+
+ function getDetailUrl(type, pk){
+ if(!type || !pk) return null;
+ if(type === 'seriesfinding') return window.location.origin + '/atlas/series_finding/' + pk + '/details/';
+ if(type === 'finding') return window.location.origin + '/atlas/finding/' + pk;
+ if(type === 'displayset') return window.location.origin + '/' + 'atlas/' + pk + '/display_sets/detail';
+ if(type === 'case') return window.location.origin + '/atlas/case/' + pk + '/';
+ return null;
+ }
+
+ // Reusable modal element
+ var __markup_modal = null;
+ function ensureModal(){
+ if(__markup_modal) return __markup_modal;
+ var backdrop = document.createElement('div'); backdrop.className = 'markup-modal-backdrop'; backdrop.style.display='none';
+ var modal = document.createElement('div'); modal.className = 'markup-modal card'; modal.style.marginTop='1rem'; modal.style.maxHeight='80vh'; modal.style.overflow='auto';
+ var header = document.createElement('div'); header.className='card-header d-flex justify-content-between align-items-center';
+ var h = document.createElement('strong'); h.textContent = 'Detail';
+ var closeBtn = document.createElement('button'); closeBtn.type='button'; closeBtn.className='btn-close'; closeBtn.addEventListener('click', function(){ backdrop.style.display='none'; modal.querySelector('.card-body') && (modal.querySelector('.card-body').innerHTML=''); });
+ header.appendChild(h); header.appendChild(closeBtn);
+ var body = document.createElement('div'); body.className='card-body'; body.style.maxHeight='70vh'; body.style.overflow='auto';
+ modal.appendChild(header); modal.appendChild(body); backdrop.appendChild(modal); document.body.appendChild(backdrop);
+ __markup_modal = { backdrop: backdrop, modal: modal, body: body };
+ return __markup_modal;
+ }
+
+ function showModalForUrl(url, title){
+ var m = ensureModal();
+ m.backdrop.style.display = 'flex';
+ if(title){ var h = m.modal.querySelector('strong'); if(h) h.textContent = title; }
+ m.body.innerHTML = '
Loading…
';
+ fetch(url, {credentials:'same-origin'}).then(function(r){ return r.text(); }).then(function(html){ m.body.innerHTML = html; }).catch(function(e){ m.body.innerHTML = 'Failed to load
'; console.error('modal fetch error', e); });
+ }
+
+ function showPopupFor(element){
+ // remove existing
+ var existing = document.getElementById('markup-popup'); if(existing) existing.remove();
+ var type = element.getAttribute('data-type'); var pk = element.getAttribute('data-pk'); var label = element.getAttribute('data-label') || element.textContent || '';
+ var popup = document.createElement('div'); popup.id = 'markup-popup'; popup.className = 'markup-popup';
+ popup.innerHTML = ''+ (label||type+':'+pk) +'
';
+ var openBtn = document.createElement('button'); openBtn.type='button'; openBtn.className='btn btn-sm btn-outline-primary me-2'; openBtn.textContent='Open';
+ openBtn.addEventListener('click', function(){ var url = getDetailUrl(type, pk); if(url) showModalForUrl(url, label); popup.remove(); });
+ var closeBtn = document.createElement('button'); closeBtn.type='button'; closeBtn.className='btn btn-sm btn-outline-secondary'; closeBtn.textContent='Close'; closeBtn.addEventListener('click', function(){ popup.remove(); });
+ popup.appendChild(openBtn); popup.appendChild(closeBtn);
+ document.body.appendChild(popup);
+ // position near element
+ try{
+ var r = element.getBoundingClientRect();
+ popup.style.top = (window.scrollY + r.bottom + 6) + 'px';
+ popup.style.left = Math.min(window.innerWidth - 240, window.scrollX + r.left) + 'px';
+ popup.style.minWidth = '160px';
+ }catch(e){}
+ // clicking outside closes it
+ setTimeout(function(){
+ var onDoc = function(ev){ if(!popup.contains(ev.target) && ev.target !== element) { popup.remove(); document.removeEventListener('click', onDoc); } };
+ document.addEventListener('click', onDoc);
+ }, 10);
+ }
+
+ // Replace tokens in text nodes with styled spans
+ function renderMarkupTokens(root){
+ injectMarkupStyles();
+ root = root || document.body;
+ // Convert server-rendered anchor tokens into our interactive spans
+ try{
+ if(root.querySelectorAll){
+ root.querySelectorAll('a.markup-link').forEach(function(a){
+ try{
+ var type = a.getAttribute('data-type') || '';
+ var args = [];
+ try{ args = JSON.parse(a.getAttribute('data-args') || '[]'); }catch(e){ args = []; }
+ var pk = (args && args.length) ? (args[0] + '') : (a.getAttribute('data-pk') || '');
+ var label = a.textContent ? a.textContent.trim() : null;
+ var span = document.createElement('span');
+ span.className = 'markup-token';
+ if(type) span.setAttribute('data-type', type);
+ if(pk) span.setAttribute('data-pk', pk);
+ if(label) span.setAttribute('data-label', label);
+ if(type === 'case') span.className += ' badge bg-primary text-white';
+ else if(type === 'displayset') span.className += ' badge bg-success text-white';
+ else span.className += ' badge bg-info text-dark';
+ span.textContent = label ? label : (type + ':' + pk);
+ span.addEventListener('click', function(e){ e.stopPropagation(); showPopupFor(span); });
+ a.parentNode.replaceChild(span, a);
+ }catch(e){}
+ });
+ }
+ }catch(e){}
+ var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
+ var nodes = [];
+ while(walker.nextNode()) nodes.push(walker.currentNode);
+ var re = /\[\[(seriesfinding|finding|displayset|case):(\d+)(?:\|([^\]]+))?\]\]/gi;
+ nodes.forEach(function(textNode){
+ if(!textNode.nodeValue || !re.test(textNode.nodeValue)) return;
+ var parentTag = textNode.parentElement && textNode.parentElement.tagName ? textNode.parentElement.tagName.toLowerCase() : '';
+ if(['script','style','textarea','code','pre','a','button'].includes(parentTag)) return;
+ // rebuild the fragment
+ var frag = document.createDocumentFragment();
+ var txt = textNode.nodeValue;
+ var lastIndex = 0; var m;
+ re.lastIndex = 0;
+ while((m = re.exec(txt)) !== null){
+ var before = txt.slice(lastIndex, m.index);
+ if(before) frag.appendChild(document.createTextNode(before));
+ var type = m[1].toLowerCase(); var pk = m[2]; var label = m[3] ? m[3] : null;
+ var span = document.createElement('span'); span.className = 'markup-token'; span.setAttribute('data-type', type); span.setAttribute('data-pk', pk); if(label) span.setAttribute('data-label', label);
+ // style by type
+ if(type === 'case') span.className += ' badge bg-primary text-white';
+ else if(type === 'displayset') span.className += ' badge bg-success text-white';
+ else span.className += ' badge bg-info text-dark';
+ span.textContent = label ? label : (type + ':' + pk);
+ span.addEventListener('click', function(e){ e.stopPropagation(); showPopupFor(span); });
+ frag.appendChild(span);
+ lastIndex = re.lastIndex;
+ }
+ var rest = txt.slice(lastIndex); if(rest) frag.appendChild(document.createTextNode(rest));
+ textNode.parentNode.replaceChild(frag, textNode);
+ });
+
+ // Ensure any server-rendered span.markup-token elements have click handlers
+ try{
+ if(root.querySelectorAll){
+ root.querySelectorAll('span.markup-token').forEach(function(span){
+ try{
+ if(span._markupBound) return;
+ span._markupBound = true;
+ // ensure data attributes are present
+ var type = span.getAttribute('data-type') || '';
+ var pk = span.getAttribute('data-pk') || '';
+ var label = span.getAttribute('data-label') || span.textContent || '';
+ span.addEventListener('click', function(e){ e.stopPropagation(); showPopupFor(span); });
+ }catch(e){}
+ });
+ }
+ }catch(e){}
+ }
+
+ // Run rendering on DOMContentLoaded and after HTMX swaps
+ document.addEventListener('DOMContentLoaded', function(){ try{ renderMarkupTokens(document.body); }catch(e){console.error(e);} });
+ document.addEventListener('htmx:afterSwap', function(evt){ try{ var target = evt.detail && evt.detail.target ? evt.detail.target : document; renderMarkupTokens(target); }catch(e){/*ignore*/} });
+
})(window);
+
diff --git a/atlas/templatetags/markup_links.py b/atlas/templatetags/markup_links.py
index 59674ab4..cfc6a4d9 100644
--- a/atlas/templatetags/markup_links.py
+++ b/atlas/templatetags/markup_links.py
@@ -26,28 +26,26 @@ def render_markup(value):
# Default display text
display = text_override or (" ".join([typ.capitalize()] + args))
- # Build href when possible
- href = "#"
+ # Provide data attributes and render as a non-clickable span badge so
+ # client-side JS can upgrade it to an interactive token. Keep
+ # `data-args` for backward compatibility.
data_args = json.dumps(args)
-
- try:
- if typ == "case":
- # args[0] expected to be case PK
- case_id = int(args[0])
- href = reverse("atlas:collection_detail", args=[case_id])
- elif typ == "finding":
- # finding view not implemented here; provide data attrs for JS
- href = "#"
- elif typ == "displayset":
- href = "#"
- except Exception:
- href = "#"
+ pk = args[0] if args else ''
+ cls = 'markup-token'
+ if typ == 'case':
+ cls += ' badge bg-primary text-white'
+ elif typ == 'displayset':
+ cls += ' badge bg-success text-white'
+ else:
+ cls += ' badge bg-info text-dark'
return format_html(
- '{}',
- href,
+ '{}',
+ cls,
typ,
data_args,
+ pk,
+ display,
display,
)