Shifting markdown to use bootstrap.
This commit is contained in:
parent
668924ba10
commit
3bca03e1bb
2 changed files with 237 additions and 126 deletions
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<!-- 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;
|
||||
|
|
@ -51,6 +51,10 @@
|
|||
for (const a of container.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');
|
||||
}
|
||||
for (const t of container.querySelectorAll('table')) {
|
||||
t.classList.add('table', 'table-sm', 'table-striped', 'table-bordered');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css">
|
||||
<!-- 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;
|
||||
|
|
@ -64,148 +64,52 @@
|
|||
<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 || [];
|
||||
|
||||
function addNewDraft() {
|
||||
const ta = document.getElementById('newUpdateInput');
|
||||
const text = (ta.value || '').trim()
|
||||
if (!text) return;
|
||||
window.newDrafts.push(text);
|
||||
ta.value = '';
|
||||
renderNewDrafts();
|
||||
// ---------- DRY UTILITIES ----------
|
||||
function renderMarkdown(md) {
|
||||
// One place to parse + sanitize
|
||||
const raw = marked.parse(md || "");
|
||||
return DOMPurify.sanitize(raw, { ADD_ATTR: ['target', 'rel'] });
|
||||
}
|
||||
|
||||
function clearNewDraft() {
|
||||
const ta = document.getElementById('newUpdateInput');
|
||||
ta.value = '';
|
||||
}
|
||||
|
||||
function renderNewDrafts() {
|
||||
const wrap = document.getElementById('newDraftsList');
|
||||
const ul = document.getElementById('newDraftsUl');
|
||||
ul.innerHTML = '';
|
||||
const drafts = window.newDrafts || [];
|
||||
if (!drafts.length) {
|
||||
wrap.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
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 = DOMPurify.sanitize(marked.parse(md || ''));
|
||||
for (const a of left.querySelectorAll('a[href]')) {
|
||||
function enhanceLinks(root) {
|
||||
if (!root) return;
|
||||
for (const a of root.querySelectorAll('a[href]')) {
|
||||
a.setAttribute('target', '_blank');
|
||||
a.setAttribute('rel', 'noopener noreferrer nofollow');
|
||||
}
|
||||
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" onclick="deleteDraft(${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');
|
||||
a.classList.add('link-success', 'link-underline', 'link-underline-opacity-0', 'fw-semibold');
|
||||
}
|
||||
}
|
||||
|
||||
function deleteDraft(idx) {
|
||||
if (!Array.isArray(window.newDrafts)) return;
|
||||
window.newDrafts.splice(idx, 1);
|
||||
renderNewDrafts();
|
||||
}
|
||||
|
||||
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}`);
|
||||
if (idx === -1) {
|
||||
window.deletedIds.push(id);
|
||||
if (btn) btn.classList.replace('btn-outline-danger', 'btn-danger');
|
||||
if (btn) btn.textContent = 'Undelete';
|
||||
if (container) container.style.opacity = '0.5';
|
||||
} else {
|
||||
window.deletedIds.splice(idx, 1);
|
||||
if (btn) btn.classList.replace('btn-danger', 'btn-outline-danger');
|
||||
if (btn) btn.textContent = 'Delete';
|
||||
if (container) container.style.opacity = '1';
|
||||
function enhanceTables(root) {
|
||||
if (!root) return;
|
||||
for (const t of root.querySelectorAll('table')) {
|
||||
t.classList.add('table', 'table-sm', 'table-striped', 'table-bordered');
|
||||
}
|
||||
}
|
||||
|
||||
// Initial render
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const ids = [{% for n in items %} {{ n.id }}{% if not loop.last %}, {% endif %} {% endfor %} ];
|
||||
for (const id of ids) renderView(id, getMarkdown(id));
|
||||
});
|
||||
function renderHTML(el, md) {
|
||||
if (!el) return;
|
||||
el.innerHTML = renderMarkdown(md);
|
||||
enhanceLinks(el);
|
||||
enhanceTables(el);
|
||||
}
|
||||
|
||||
function getMarkdown(id) {
|
||||
const el = document.getElementById(`md-${id}`);
|
||||
return el ? JSON.parse(el.textContent) : "";
|
||||
return el ? JSON.parse(el.textContent || '""') : "";
|
||||
}
|
||||
|
||||
function setMarkdown(id, md) {
|
||||
const el = document.getElementById(`md-${id}`);
|
||||
if (el) el.textContent = JSON.stringify(md ?? "");
|
||||
}
|
||||
|
||||
function renderView(id, md) {
|
||||
const container = document.getElementById(`editContainer${id}`);
|
||||
if (!container) return;
|
||||
const html = marked.parse(md || "");
|
||||
container.innerHTML = DOMPurify.sanitize(html, { ADD_ATTR: ['target', 'rel'] });
|
||||
for (const a of container.querySelectorAll('a[href]')) {
|
||||
a.setAttribute('target', '_blank');
|
||||
a.setAttribute('rel', 'noopener noreferrer nofollow');
|
||||
}
|
||||
}
|
||||
|
||||
function changeMode(id) {
|
||||
const container = document.getElementById(`editContainer${id}`);
|
||||
const toggle = document.getElementById(`editSwitch${id}`);
|
||||
if (!toggle.checked) return renderView(id, getMarkdown(id));
|
||||
|
||||
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" onclick="saveEdit(${id})">Save</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="cancelEdit(${id})">Cancel</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="togglePreview(${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 saveEdit(id) {
|
||||
const ta = document.getElementById(`editor${id}`);
|
||||
const value = ta ? ta.value : "";
|
||||
setMarkdown(id, value); // persist into the JSON tag
|
||||
renderView(id, value); // show it
|
||||
document.getElementById(`editSwitch${id}`).checked = false;
|
||||
}
|
||||
|
||||
function cancelEdit(id) {
|
||||
document.getElementById(`editSwitch${id}`).checked = false;
|
||||
renderView(id, getMarkdown(id));
|
||||
}
|
||||
|
||||
function togglePreview(id) {
|
||||
const ta = document.getElementById(`editor${id}`);
|
||||
const preview = document.getElementById(`preview${id}`);
|
||||
preview.classList.toggle('d-none');
|
||||
if (!preview.classList.contains('d-none')) {
|
||||
const html = marked.parse(ta ? ta.value : "");
|
||||
preview.innerHTML = DOMPurify.sanitize(html);
|
||||
}
|
||||
function escapeForTextarea(s) {
|
||||
return (s ?? "").replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function autoGrow(ta) {
|
||||
|
|
@ -214,7 +118,210 @@
|
|||
ta.style.height = (ta.scrollHeight + 5) + 'px';
|
||||
}
|
||||
|
||||
function escapeForTextarea(s) {
|
||||
return (s ?? "").replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
// ---------- 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 won’t 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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue