From 5a5eaa04a2382fbdb3aa1cb9420fb0367fe2b6ae Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 23 Mar 2026 11:46:21 +0000 Subject: [PATCH] Add markup inserter functionality and render markup filter for enhanced text editing --- atlas/static/atlas/js/markup_inserter.js | 97 +++++++++++++++++++ atlas/templates/atlas/case_form.html | 7 ++ atlas/templates/atlas/collection_detail.html | 59 +++++++++++ .../atlas/partials/case_inline_field.html | 4 +- atlas/templatetags/markup_links.py | 57 +++++++++++ templates/base.html | 1 + 6 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 atlas/static/atlas/js/markup_inserter.js create mode 100644 atlas/templatetags/markup_links.py diff --git a/atlas/static/atlas/js/markup_inserter.js b/atlas/static/atlas/js/markup_inserter.js new file mode 100644 index 00000000..7ab331bd --- /dev/null +++ b/atlas/static/atlas/js/markup_inserter.js @@ -0,0 +1,97 @@ +// Simple markup inserter module +(function(global){ + 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; + 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; + if(t === 'case'){ + const id = prompt('Case ID to link to:'); if(!id) return; insertAtCursor(textarea, '[['+t+':'+id+']]' ); + } else if(t === 'finding'){ + const id = prompt('Finding ID to link to:'); if(!id) return; insertAtCursor(textarea, '[['+t+':'+id+']]' ); + } else if(t === 'displayset'){ + const caseId = prompt('Case ID containing the display set:'); if(!caseId) return; const setName = prompt('Display set name:'); if(!setName) return; insertAtCursor(textarea, '[['+t+':'+caseId+':'+setName+']]' ); + } + }); + }); + textarea._markupToolbarAttached = true; + return toolbar; + } + + function initForNames(names){ + if(!Array.isArray(names)) return; + document.addEventListener('DOMContentLoaded', function(){ + 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(){ + document.querySelectorAll('.markup-toolbar-placeholder').forEach(function(ph){ + // find next textarea sibling + let ta = ph.nextElementSibling; + if(ta && ta.tagName === 'TEXTAREA') attachToolbarToTextarea(ta); + }); + }); + + document.addEventListener('htmx:afterSwap', function(evt){ + 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); + }); + }catch(e){/* ignore */} + }); + + // Expose API + global.markupInserter = { + attachToolbarToTextarea: attachToolbarToTextarea, + initForNames: initForNames, + attachToSelector: attachToSelector + }; + +})(window); diff --git a/atlas/templates/atlas/case_form.html b/atlas/templates/atlas/case_form.html index f34c7b43..343659cb 100755 --- a/atlas/templates/atlas/case_form.html +++ b/atlas/templates/atlas/case_form.html @@ -216,4 +216,11 @@ {{ caseresource_formset.empty_form | crispy}} + {% endblock %} diff --git a/atlas/templates/atlas/collection_detail.html b/atlas/templates/atlas/collection_detail.html index b4ce7d6d..7fa35f4b 100644 --- a/atlas/templates/atlas/collection_detail.html +++ b/atlas/templates/atlas/collection_detail.html @@ -219,4 +219,63 @@ } }); + {% endblock %} diff --git a/atlas/templates/atlas/partials/case_inline_field.html b/atlas/templates/atlas/partials/case_inline_field.html index 86c6c2db..2db2e443 100644 --- a/atlas/templates/atlas/partials/case_inline_field.html +++ b/atlas/templates/atlas/partials/case_inline_field.html @@ -25,6 +25,7 @@ class="mt-2"> {% csrf_token %} {% if field_kind == "textarea" %} +
{% elif field_kind == "select" %}