// Simple markup inserter module (function(global){ console.log('markup_inserter.js loaded'); function insertAtCursor(el, text){ if(!el) return; const start = el.selectionStart || 0; const end = el.selectionEnd || 0; const val = el.value || ''; el.value = val.slice(0, start) + text + val.slice(end); el.selectionStart = el.selectionEnd = start + text.length; el.focus(); } function createToolbar(){ const wrap = document.createElement('div'); wrap.className = 'd-flex gap-2 mb-2 markup-inserter-toolbar'; const group = document.createElement('div'); group.className = 'btn-group'; const makeBtn = function(type, text){ const b = document.createElement('button'); b.type = 'button'; b.className = 'btn btn-sm btn-outline-secondary markup-insert-button'; b.dataset.type = type; b.textContent = text; return b; }; group.appendChild(makeBtn('case','Insert case')); group.appendChild(makeBtn('finding','Insert finding')); group.appendChild(makeBtn('displayset','Insert displayset')); const hint = document.createElement('div'); hint.className='flex-fill text-muted small align-self-center'; hint.textContent='Use the buttons to insert markup like [[case:123]]'; wrap.appendChild(group); wrap.appendChild(hint); return wrap; } function attachToolbarToTextarea(textarea){ if(!textarea) return null; // avoid double attaching if(textarea._markupToolbarAttached) return null; console.log('Attaching markup toolbar to textarea', textarea); const toolbar = createToolbar(); textarea.parentNode.insertBefore(toolbar, textarea); toolbar.querySelectorAll('.markup-insert-button').forEach(function(btn){ btn.addEventListener('click', function(){ const t = btn.dataset.type; // Respect explicit dataset provided by the widget or page meta only var detectedCase = textarea && textarea.dataset && textarea.dataset.currentCase ? textarea.dataset.currentCase : (document.getElementById('current-case-meta') ? document.getElementById('current-case-meta').getAttribute('data-current-case') : null); if(t === 'case'){ openCasePicker(textarea); } else if(t === 'finding'){ openFindingPicker(textarea, detectedCase); } else if(t === 'displayset'){ openDisplaysetPicker(textarea, detectedCase); } }); }); textarea._markupToolbarAttached = true; return toolbar; } function initForNames(names){ if(!Array.isArray(names)) return; document.addEventListener('DOMContentLoaded', function(){ console.log('markupInserter.initForNames DOMContentLoaded handler running for', names); names.forEach(function(name){ const el = document.querySelector('textarea[name="'+name+'"]'); if(el) attachToolbarToTextarea(el); }); }); } function attachToSelector(selector){ document.addEventListener('DOMContentLoaded', function(){ document.querySelectorAll(selector).forEach(function(el){ if(el.tagName === 'TEXTAREA') attachToolbarToTextarea(el); }); }); } // Auto-attach to placeholders rendered in HTMX responses or server-side includes. document.addEventListener('DOMContentLoaded', function(){ console.log('markupInserter DOMContentLoaded auto-attach running'); // Attach to explicit placeholders document.querySelectorAll('.markup-toolbar-placeholder').forEach(function(ph){ let ta = ph.nextElementSibling; if(ta && ta.tagName === 'TEXTAREA') attachToolbarToTextarea(ta); }); // Also auto-init for common case textarea names to cover full-page forms try{ initForNames(['description','history','discussion','report','notes']); }catch(e){} }); document.addEventListener('htmx:afterSwap', function(evt){ console.log('markupInserter htmx:afterSwap event', evt && evt.detail && evt.detail.target); try{ const target = evt.detail && evt.detail.target ? evt.detail.target : document; (target.querySelectorAll ? target : document).querySelectorAll('.markup-toolbar-placeholder').forEach(function(ph){ let ta = ph.nextElementSibling; if(ta && ta.tagName === 'TEXTAREA') attachToolbarToTextarea(ta); }); // Also attach to any textareas within the swapped target that match our common names var ctx = evt.detail && evt.detail.target ? evt.detail.target : document; ['description','history','discussion','report','notes'].forEach(function(name){ ctx.querySelectorAll && ctx.querySelectorAll('textarea[name="'+name+'"]') && ctx.querySelectorAll('textarea[name="'+name+'"]').forEach(function(ta){ attachToolbarToTextarea(ta); }); }); }catch(e){/* ignore */} }); // Fallback: attach toolbar when a textarea receives focus (covers dynamically created editors) document.addEventListener('focusin', function(e){ console.log('markupInserter focusin event for', e && e.target); try{ var el = e.target; if(!el || el.tagName !== 'TEXTAREA') return; // Attach for common names or if a placeholder is present var name = el.getAttribute('name') || ''; if(['description','history','discussion','report','notes'].includes(name) || (el.previousElementSibling && el.previousElementSibling.classList && el.previousElementSibling.classList.contains('markup-toolbar-placeholder'))){ attachToolbarToTextarea(el); } }catch(err){/* ignore */} }, true); // Expose API global.markupInserter = { attachToolbarToTextarea: attachToolbarToTextarea, initForNames: initForNames, attachToSelector: attachToSelector }; // --- Case picker modal --- function openCasePicker(textarea){ try{ console.log('Opening case picker for', textarea); // Create overlay const overlay = document.createElement('div'); overlay.className = 'markup-case-picker-overlay'; Object.assign(overlay.style, {position:'fixed',left:0,top:0,right:0,bottom:0,background:'rgba(0,0,0,0.4)',zIndex:1050,display:'flex',alignItems:'center',justifyContent:'center'}); const dialog = document.createElement('div'); dialog.className = 'markup-case-picker-dialog card'; Object.assign(dialog.style, {width:'min(900px,95%)',maxHeight:'80vh',overflow:'auto'}); const header = document.createElement('div'); header.className='card-header d-flex justify-content-between align-items-center'; header.innerHTML = 'Select case'; const closeBtn = document.createElement('button'); closeBtn.type='button'; closeBtn.className='btn-close'; closeBtn.setAttribute('aria-label','Close'); closeBtn.addEventListener('click', function(){ document.body.removeChild(overlay); }); header.appendChild(closeBtn); const body = document.createElement('div'); body.className='card-body'; const input = document.createElement('input'); input.type='search'; input.className='form-control mb-2'; input.placeholder='Type to search cases...'; const results = document.createElement('div'); results.className='case-search-results'; body.appendChild(input); body.appendChild(results); dialog.appendChild(header); dialog.appendChild(body); overlay.appendChild(dialog); document.body.appendChild(overlay); input.focus(); var timer = null; input.addEventListener('input', function(){ if(timer) clearTimeout(timer); timer = setTimeout(function(){ var q = encodeURIComponent(input.value || ''); // Fetch server-side partial that renders result list-group var url = window.location.origin + '/atlas/search/cases/?q=' + q; fetch(url, {credentials:'same-origin'}).then(function(r){ return r.text(); }).then(function(html){ results.innerHTML = html; }).catch(function(e){ console.error('case picker fetch error', e); results.innerHTML = '
Search failed
'; }); }, 300); }); // Delegate click on result items results.addEventListener('click', function(e){ var item = e.target.closest('.list-group-item'); if(!item) return; var pk = item.getAttribute('data-case-pk'); if(!pk){ var a = item.querySelector('a'); if(a){ var m = a.getAttribute('href').match(/case\/(\d+)\/?/); if(m) pk = m[1]; } } if(!pk) return; var defaultLabel = item.getAttribute('data-label') || (item.querySelector('h6') ? item.querySelector('h6').textContent.trim() : ''); var custom = window.prompt('Custom text (leave empty to use default)', defaultLabel || ''); if(custom === null){ if(overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); return; } var labelToUse = custom && custom.length ? custom : (defaultLabel || null); var token = '[[' + 'case:' + pk + (labelToUse ? '|' + labelToUse : '') + ']]'; insertAtCursor(textarea, token); // remove modal if(overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); }); // Allow ESC to close overlay.addEventListener('keydown', function(ev){ if(ev.key === 'Escape'){ if(overlay.parentNode) overlay.parentNode.removeChild(overlay); } }); // make overlay focusable so keydown works overlay.tabIndex = -1; overlay.focus(); // load initial recent results (empty query) fetch(window.location.origin + '/atlas/search/cases/').then(function(r){ return r.text(); }).then(function(html){ results.innerHTML = html; }).catch(function(){}); }catch(err){ console.error('openCasePicker error', err); } } function openFindingPicker(textarea, caseId){ try{ console.log('Opening finding picker for', textarea); const overlay = document.createElement('div'); overlay.className = 'markup-case-picker-overlay'; Object.assign(overlay.style, {position:'fixed',left:0,top:0,right:0,bottom:0,background:'rgba(0,0,0,0.4)',zIndex:1050,display:'flex',alignItems:'center',justifyContent:'center'}); const dialog = document.createElement('div'); dialog.className='markup-case-picker-dialog card'; Object.assign(dialog.style, {width:'min(900px,95%)',maxHeight:'80vh',overflow:'auto'}); const header = document.createElement('div'); header.className='card-header d-flex justify-content-between align-items-center'; header.innerHTML = 'Select finding'; // Determine case filter from explicit dataset on the textarea or provided caseId var detectedF = (textarea && textarea.dataset && textarea.dataset.currentCase) ? { id: textarea.dataset.currentCase, title: textarea.dataset.currentCaseTitle } : {}; var currentCaseF = caseId || detectedF.id || null; const closeBtn = document.createElement('button'); closeBtn.type='button'; closeBtn.className='btn-close'; closeBtn.setAttribute('aria-label','Close'); closeBtn.addEventListener('click', function(){ document.body.removeChild(overlay); }); header.appendChild(closeBtn); const body = document.createElement('div'); body.className='card-body'; const input = document.createElement('input'); input.type='search'; input.className='form-control mb-2'; input.placeholder='Type to search findings...'; const results = document.createElement('div'); results.className='finding-search-results'; body.appendChild(input); body.appendChild(results); dialog.appendChild(header); dialog.appendChild(body); overlay.appendChild(dialog); document.body.appendChild(overlay); input.focus(); var timer = null; input.addEventListener('input', function(){ if(timer) clearTimeout(timer); timer = setTimeout(function(){ var q = encodeURIComponent(input.value || ''); var url = window.location.origin + '/atlas/search/findings/?q=' + q + (currentCaseF ? '&case=' + currentCaseF : ''); fetch(url, {credentials:'same-origin'}).then(function(r){ return r.text(); }).then(function(html){ results.innerHTML = html; }).catch(function(e){ console.error('finding picker fetch error', e); results.innerHTML = '
Search failed
'; }); }, 300); }); results.addEventListener('click', function(e){ var item = e.target.closest('.list-group-item'); if(!item) return; var sfpk = item.getAttribute('data-seriesfinding-pk'); var fpk = item.getAttribute('data-finding-pk'); var defaultLabel = item.getAttribute('data-label') || (item.querySelector('strong') ? item.querySelector('strong').textContent.trim() : ''); var custom = window.prompt('Custom text (leave empty to use default)', defaultLabel || ''); if(custom === null){ if(overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); return; } var labelToUse = custom && custom.length ? custom : (defaultLabel || null); if(sfpk){ var token = '[['+'seriesfinding:'+sfpk + (labelToUse ? '|' + labelToUse : '') + ']]'; insertAtCursor(textarea, token); } else if(fpk){ var token = '[['+'finding:'+fpk + (labelToUse ? '|' + labelToUse : '') + ']]'; insertAtCursor(textarea, token); } else { return; } if(overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); }); overlay.tabIndex = -1; overlay.focus(); function fetchInitialFindings(){ var initFindingsUrl = window.location.origin + '/atlas/search/findings/' + (currentCaseF ? '?case=' + currentCaseF : ''); fetch(initFindingsUrl).then(function(r){ return r.text(); }).then(function(html){ results.innerHTML = html; }).catch(function(){}); } fetchInitialFindings(); }catch(err){ console.error('openFindingPicker error', err); } } function openDisplaysetPicker(textarea, caseId){ try{ console.log('Opening displayset picker for', textarea); const overlay = document.createElement('div'); overlay.className = 'markup-case-picker-overlay'; Object.assign(overlay.style, {position:'fixed',left:0,top:0,right:0,bottom:0,background:'rgba(0,0,0,0.4)',zIndex:1050,display:'flex',alignItems:'center',justifyContent:'center'}); const dialog = document.createElement('div'); dialog.className='markup-case-picker-dialog card'; Object.assign(dialog.style, {width:'min(900px,95%)',maxHeight:'80vh',overflow:'auto'}); const header = document.createElement('div'); header.className='card-header d-flex justify-content-between align-items-center'; header.innerHTML = 'Select display set'; // Determine case filter from explicit dataset on the textarea or provided caseId var detectedD = (textarea && textarea.dataset && textarea.dataset.currentCase) ? { id: textarea.dataset.currentCase, title: textarea.dataset.currentCaseTitle } : {}; var currentCaseD = caseId || detectedD.id || null; const closeBtn = document.createElement('button'); closeBtn.type='button'; closeBtn.className='btn-close'; closeBtn.setAttribute('aria-label','Close'); closeBtn.addEventListener('click', function(){ document.body.removeChild(overlay); }); header.appendChild(closeBtn); const body = document.createElement('div'); body.className='card-body'; const input = document.createElement('input'); input.type='search'; input.className='form-control mb-2'; input.placeholder='Type to search display sets...'; const results = document.createElement('div'); results.className='displayset-search-results'; body.appendChild(input); body.appendChild(results); dialog.appendChild(header); dialog.appendChild(body); overlay.appendChild(dialog); document.body.appendChild(overlay); input.focus(); var timer = null; input.addEventListener('input', function(){ if(timer) clearTimeout(timer); timer = setTimeout(function(){ var q = encodeURIComponent(input.value || ''); var url = window.location.origin + '/atlas/search/displaysets/?q=' + q + (currentCaseD ? '&case=' + currentCaseD : ''); fetch(url, {credentials:'same-origin'}).then(function(r){ return r.text(); }).then(function(html){ results.innerHTML = html; }).catch(function(e){ console.error('displayset picker fetch error', e); results.innerHTML = '
Search failed
'; }); }, 300); }); results.addEventListener('click', function(e){ var item = e.target.closest('.list-group-item'); if(!item) return; var pk = item.getAttribute('data-ds-pk'); if(!pk) return; var defaultLabel = item.getAttribute('data-label') || (item.querySelector('strong') ? item.querySelector('strong').textContent.trim() : ''); var custom = window.prompt('Custom text (leave empty to use default)', defaultLabel || ''); if(custom === null){ if(overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); return; } var labelToUse = custom && custom.length ? custom : (defaultLabel || null); var token = '[['+'displayset:'+pk + (labelToUse ? '|' + labelToUse : '') + ']]'; insertAtCursor(textarea, token); if(overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); }); overlay.tabIndex = -1; overlay.focus(); function fetchInitialDisplaysets(){ var initDisplaysetsUrl = window.location.origin + '/atlas/search/displaysets/' + (currentCaseD ? '?case=' + currentCaseD : ''); fetch(initDisplaysetsUrl).then(function(r){ return r.text(); }).then(function(html){ results.innerHTML = html; }).catch(function(){}); } fetchInitialDisplaysets(); }catch(err){ console.error('openDisplaysetPicker error', err); } } // 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 + '/preview/'; if(type === 'finding') return window.location.origin + '/atlas/finding/' + pk; if(type === 'displayset') return window.location.origin + '/atlas/displayset/' + pk + '/preview/'; 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); try{ // Prefer page-specific modal loaders if present so we reuse existing Bootstrap modals/viewers if(type === 'displayset'){ if(typeof window.loadDisplaysetModal === 'function'){ window.loadDisplaysetModal(url, pk); popup.remove(); return; } var dsBtn = document.querySelector('.view-displayset-modal[data-ds-id="'+pk+'"]'); if(dsBtn){ dsBtn.click(); popup.remove(); return; } } if(type === 'seriesfinding' || type === 'finding'){ if(typeof window.loadFindingModal === 'function'){ window.loadFindingModal(pk, url); popup.remove(); return; } var fBtn = document.querySelector('.view-finding-modal[data-finding-id="'+pk+'"]'); if(fBtn){ fBtn.click(); popup.remove(); return; } } }catch(e){ console.warn('error invoking page modal loader', e); } 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); } // Try to open a modal for a token (prefer page-specific modal loaders) function openModalForToken(element){ var type = element.getAttribute('data-type'); var pk = element.getAttribute('data-pk'); var label = element.getAttribute('data-label') || element.textContent || ''; var url = getDetailUrl(type, pk); try{ if(type === 'displayset'){ if(typeof window.loadDisplaysetModal === 'function'){ window.loadDisplaysetModal(url, pk); return; } var dsBtn = document.querySelector('.view-displayset-modal[data-ds-id="'+pk+'"]'); if(dsBtn){ dsBtn.click(); return; } } if(type === 'seriesfinding' || type === 'finding'){ if(typeof window.loadFindingModal === 'function'){ window.loadFindingModal(pk, url); return; } var fBtn = document.querySelector('.view-finding-modal[data-finding-id="'+pk+'"]'); if(fBtn){ fBtn.click(); return; } } }catch(e){ console.warn('error invoking page modal loader', e); } if(url) showModalForUrl(url, label); } // 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(); openModalForToken(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(); openModalForToken(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(); openModalForToken(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);