inventory/inventory/templates/update_list.html
2025-10-21 10:19:26 -05:00

335 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% set items = (field.template_ctx.instance.updates or []) %}
<div class="mt-3">
<label class="form-label">Add update</label>
<textarea id="newUpdateInput" class="form-control auto-md" rows="3" placeholder="Write a new update..."></textarea>
<div class="mt-2 d-flex gap-2">
<button type="button" class="btn btn-primary btn-sm" onclick="addNewDraft()">Add</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="clearNewDraft()">Clear</button>
</div>
<div id="newDraftsList" class="mt-3 d-none">
<div class="fw-semibold mb-1">Pending new updates</div>
<ul class="list-group" id="newDraftsUl"></ul>
</div>
</div>
<ul class="list-group mt-3">
{% for n in items %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div class="me-3 w-100 markdown-body" id="editContainer{{ n.id }}"></div>
<script type="application/json" id="md-{{ n.id }}">{{ n.content | tojson }}</script>
<div class="d-flex flex-column align-items-end">
<div id="editView{{ n.id }}">
<small class="text-muted text-nowrap">
{{ n.timestamp.strftime("%Y-%m-%d %H:%M") if n.timestamp else "" }}
</small>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="editSwitch{{ n.id }}" onchange="changeMode({{ n.id }})" role="switch" aria-label="Edit mode for update {{ n.id }}">
<label for="editSwitch{{ n.id }}" class="form-check-label">Edit</label>
</div>
<div class="mt-2">
<button type="button" class="btn btn-outline-danger btn-sm" id="deleteBtn{{ n.id }}" onclick="toggleDelete({{ n.id }})">Delete</button>
</div>
</div>
</div>
</li>
{% else %}
<li id="noUpdatesRow" class="list-group-item text-muted">No updates yet.</li>
{% endfor %}
</ul>
<!-- link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css" -->
<style>
textarea.auto-md {
box-sizing: border-box;
width: 100%;
max-height: 60vh;
overflow: hidden;
resize: vertical;
}
@supports (field-sizing: content) {
textarea.auto-md {
field-sizing: content;
}
}
</style>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
<script>
// State (kept global for compatibility with your form serialization)
window.newDrafts = window.newDrafts || [];
window.deletedIds = window.deletedIds || [];
// ---------- DRY UTILITIES ----------
function renderMarkdown(md) {
// One place to parse + sanitize
const raw = marked.parse(md || "");
return DOMPurify.sanitize(raw, { ADD_ATTR: ['target', 'rel'] });
}
function enhanceLinks(root) {
if (!root) return;
for (const a of root.querySelectorAll('a[href]')) {
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'noopener noreferrer nofollow');
a.classList.add('link-success', 'link-underline', 'link-underline-opacity-0', 'fw-semibold');
}
}
function enhanceTables(root) {
if (!root) return;
for (const t of root.querySelectorAll('table')) {
t.classList.add('table', 'table-sm', 'table-striped', 'table-bordered');
}
}
function enhanceBlockquotes(root) {
if (!root) return;
for (const t of root.querySelectorAll('blockquote')) {
t.classList.add('blockquote', 'border-start', 'border-5', 'border-success', 'mt-3', 'ps-3');
}
}
function renderHTML(el, md) {
if (!el) return;
el.innerHTML = renderMarkdown(md);
enhanceLinks(el);
enhanceTables(el);
enhanceBlockquotes(el);
}
function getMarkdown(id) {
const el = document.getElementById(`md-${id}`);
return el ? JSON.parse(el.textContent || '""') : "";
}
function setMarkdown(id, md) {
const el = document.getElementById(`md-${id}`);
if (el) el.textContent = JSON.stringify(md ?? "");
}
function escapeForTextarea(s) {
return (s ?? "").replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function autoGrow(ta) {
if (!ta) return;
ta.style.height = 'auto';
ta.style.height = (ta.scrollHeight + 5) + 'px';
}
// ---------- RENDERERS ----------
function renderExistingView(id) {
const container = document.getElementById(`editContainer${id}`);
renderHTML(container, getMarkdown(id));
}
function renderEditor(id) {
const container = document.getElementById(`editContainer${id}`);
if (!container) return;
const current = getMarkdown(id);
container.innerHTML = `
<textarea class="form-control w-100 overflow-auto auto-md" id="editor${id}">${escapeForTextarea(current)}</textarea>
<div class="mt-2 d-flex gap-2">
<button type="button" class="btn btn-primary btn-sm" data-action="save-edit" data-id="${id}">Save</button>
<button type="button" class="btn btn-secondary btn-sm" data-action="cancel-edit" data-id="${id}">Cancel</button>
<button type="button" class="btn btn-outline-secondary btn-sm" data-action="toggle-preview" data-id="${id}">Preview</button>
</div>
<div class="mt-2 border rounded p-2 d-none" id="preview${id}" aria-live="polite"></div>
`;
const ta = document.getElementById(`editor${id}`);
autoGrow(ta);
ta.addEventListener('input', () => autoGrow(ta));
}
function renderDraftsList() {
const wrap = document.getElementById('newDraftsList');
const ul = document.getElementById('newDraftsUl');
if (!wrap || !ul) return;
ul.innerHTML = '';
const drafts = window.newDrafts || [];
if (!drafts.length) {
wrap.classList.add('d-none');
} else {
wrap.classList.remove('d-none');
drafts.forEach((md, idx) => {
const li = document.createElement('li');
li.className = 'list-group-item d-flex justify-content-between align-items-start';
const left = document.createElement('div');
left.className = 'w-100 markdown-body';
left.innerHTML = renderMarkdown(md || '');
enhanceLinks(left);
const right = document.createElement('div');
right.className = 'ms-3 d-flex flex-column align-items-end';
right.innerHTML = `
<button type="button" class="btn btn-outline-danger btn-sm" data-action="remove-draft" data-index="${idx}">
Remove
</button>
`;
li.appendChild(left);
li.appendChild(right);
ul.appendChild(li);
});
}
const noRow = document.getElementById('noUpdatesRow');
if (noRow) {
if (drafts.length) noRow.classList.add('d-none');
else noRow.classList.remove('d-none');
}
}
// ---------- STATE MUTATORS ----------
function addNewDraft() {
const ta = document.getElementById('newUpdateInput');
const text = (ta?.value || '').trim();
if (!text) return;
window.newDrafts.push(text);
ta.value = '';
renderDraftsList();
}
function clearNewDraft() {
const ta = document.getElementById('newUpdateInput');
if (ta) ta.value = '';
}
function deleteDraft(idx) {
if (!Array.isArray(window.newDrafts)) return;
window.newDrafts.splice(idx, 1);
renderDraftsList();
}
function toggleDelete(id) {
window.deletedIds = window.deletedIds || [];
const idx = window.deletedIds.indexOf(id);
const btn = document.getElementById(`deleteBtn${id}`);
const container = document.getElementById(`editContainer${id}`);
const markDeleted = (on) => {
if (!btn || !container) return;
if (on) {
btn.classList.replace('btn-outline-danger', 'btn-danger');
btn.textContent = 'Undelete';
container.style.opacity = '0.5';
} else {
btn.classList.replace('btn-danger', 'btn-outline-danger');
btn.textContent = 'Delete';
container.style.opacity = '1';
}
};
if (idx === -1) {
window.deletedIds.push(id);
markDeleted(true);
} else {
window.deletedIds.splice(idx, 1);
markDeleted(false);
}
}
function changeMode(id) {
const toggle = document.getElementById(`editSwitch${id}`);
if (!toggle) return;
if (!toggle.checked) {
renderExistingView(id);
return;
}
renderEditor(id);
}
function saveEdit(id) {
const ta = document.getElementById(`editor${id}`);
const value = ta ? ta.value : "";
setMarkdown(id, value);
renderExistingView(id);
const toggle = document.getElementById(`editSwitch${id}`);
if (toggle) toggle.checked = false;
}
function cancelEdit(id) {
const toggle = document.getElementById(`editSwitch${id}`);
if (toggle) toggle.checked = false;
renderExistingView(id);
}
function togglePreview(id) {
const ta = document.getElementById(`editor${id}`);
const preview = document.getElementById(`preview${id}`);
if (!preview) return;
preview.classList.toggle('d-none');
if (!preview.classList.contains('d-none')) {
preview.innerHTML = renderMarkdown(ta ? ta.value : "");
}
}
// ---------- EVENT DELEGATION ----------
document.addEventListener('click', (e) => {
const t = e.target;
if (!(t instanceof Element)) return;
const action = t.getAttribute('data-action');
if (!action) return;
switch (action) {
case 'add-draft': addNewDraft(); break;
case 'clear-draft': clearNewDraft(); break;
case 'remove-draft': deleteDraft(parseInt(t.getAttribute('data-index') || '-1', 10)); break;
case 'toggle-delete': toggleDelete(parseInt(t.getAttribute('data-id') || '0', 10)); break;
case 'save-edit': saveEdit(parseInt(t.getAttribute('data-id') || '0', 10)); break;
case 'cancel-edit': cancelEdit(parseInt(t.getAttribute('data-id') || '0', 10)); break;
case 'toggle-preview': togglePreview(parseInt(t.getAttribute('data-id') || '0', 10)); break;
}
});
document.addEventListener('change', (e) => {
const t = e.target;
if (!(t instanceof Element)) return;
if (t.matches('[id^="editSwitch"]')) {
// id form: editSwitch{n.id}
const id = parseInt(t.id.replace('editSwitch', ''), 10);
changeMode(id);
}
});
// ---------- BOOT ----------
document.addEventListener('DOMContentLoaded', () => {
// If you insist on leaving inline onclick attributes in HTML, fine, it wont break.
// Still render all existing updates:
const ids = [ {% for n in items %}{{ n.id }}{% if not loop.last %}, {% endif %}{% endfor %} ];
for (const id of ids) renderExistingView(id);
// Wire the top buttons to delegation without changing your HTML:
// Just add data-action attributes at runtime so you can keep your markup unchanged.
const addBtn = document.querySelector('button.btn.btn-primary.btn-sm[onclick="addNewDraft()"]');
if (addBtn) addBtn.setAttribute('data-action', 'add-draft');
const clearBtn = document.querySelector('button.btn.btn-outline-secondary.btn-sm[onclick="clearNewDraft()"]');
if (clearBtn) clearBtn.setAttribute('data-action', 'clear-draft');
// For delete buttons, add data-action so delegation can handle them too
{% for n in items %}
(function(id){
const del = document.getElementById('deleteBtn' + id);
if (del) {
del.setAttribute('data-action', 'toggle-delete');
del.setAttribute('data-id', String(id));
}
})({{ n.id }});
{% endfor %}
renderDraftsList();
});
</script>