Refactoring MarkDown behavior.
This commit is contained in:
parent
6357e5794f
commit
38bae34247
4 changed files with 108 additions and 122 deletions
30
inventory/static/js/components/markdown.js
Normal file
30
inventory/static/js/components/markdown.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
const MarkDown = {
|
||||||
|
parseOptions: { gfm: true, breaks: false },
|
||||||
|
sanitizeOptions: { ADD_ATTR: ['target', 'rel'] },
|
||||||
|
|
||||||
|
toHTML(md) {
|
||||||
|
const raw = marked.parse(md || "", this.parseOptions);
|
||||||
|
return DOMPurify.sanitize(raw, this.sanitizeOptions);
|
||||||
|
},
|
||||||
|
|
||||||
|
enhance(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');
|
||||||
|
}
|
||||||
|
for (const t of root.querySelectorAll('table')) {
|
||||||
|
t.classList.add('table', 'table-sm', 'table-striped', 'table-bordered');
|
||||||
|
}
|
||||||
|
for (const q of root.querySelectorAll('blockquote')) {
|
||||||
|
q.classList.add('blockquote', 'border-start', 'border-5', 'border-success', 'mt-3', 'ps-3');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderInto(el, md) {
|
||||||
|
if (!el) return;
|
||||||
|
el.innerHTML = this.toHTML(md);
|
||||||
|
this.enhance(el);
|
||||||
|
}
|
||||||
|
};
|
||||||
7
inventory/static/js/utils/json.js
Normal file
7
inventory/static/js/utils/json.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
function readJSONScript(id, fallback = "") {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el) return fallback;
|
||||||
|
const txt = el.textContent?.trim();
|
||||||
|
if (!txt) return fallback;
|
||||||
|
try { return JSON.parse(txt); } catch { return fallback; }
|
||||||
|
}
|
||||||
|
|
@ -28,89 +28,76 @@
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<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 src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/components/markdown.js') }}" defer></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/utils/json.js') }}" defer></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
renderView(getMarkdown());
|
MarkDown.renderInto(document.getElementById('editContainer'), getMarkdown());
|
||||||
});
|
});
|
||||||
|
|
||||||
function getMarkdown() {
|
// used by entry_buttons submit
|
||||||
const el = document.getElementById('noteContent');
|
window.getMarkdown = function () {
|
||||||
return el ? (JSON.parse(el.textContent) || "") : "";
|
return readJSONScript('noteContent', "");
|
||||||
|
};
|
||||||
|
|
||||||
|
function setMarkdown(md) {
|
||||||
|
const el = document.getElementById('noteContent');
|
||||||
|
if (el) el.textContent = JSON.stringify(md ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeMode() {
|
||||||
|
const container = document.getElementById('editContainer');
|
||||||
|
const toggle = document.getElementById('editSwitch');
|
||||||
|
if (!toggle.checked) {
|
||||||
|
MarkDown.renderInto(container, getMarkdown());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMarkdown(md) {
|
const current = getMarkdown();
|
||||||
const el = document.getElementById('noteContent');
|
container.innerHTML = `
|
||||||
if (el) el.textContent = JSON.stringify(md ?? "");
|
<textarea class="form-control w-100 auto-md" id="editor" name="notes">${escapeForTextarea(current)}</textarea>
|
||||||
}
|
<div class="mt-2 d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" onclick="saveEdit()">Save</button>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm" onclick="cancelEdit()">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="togglePreview()">Preview</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 border rounded p-2 d-none" id="preview" aria-live="polite"></div>
|
||||||
|
`;
|
||||||
|
const ta = document.getElementById('editor');
|
||||||
|
autoGrow(ta);
|
||||||
|
ta.addEventListener('input', () => autoGrow(ta));
|
||||||
|
}
|
||||||
|
|
||||||
function renderView(md) {
|
function saveEdit() {
|
||||||
const container = document.getElementById('editContainer');
|
const ta = document.getElementById('editor');
|
||||||
if (!container) return;
|
const value = ta ? ta.value : "";
|
||||||
const html = marked.parse(md || "", {gfm: true});
|
setMarkdown(value);
|
||||||
container.innerHTML = DOMPurify.sanitize(html, { ADD_ATTR: ['target', 'rel'] });
|
MarkDown.renderInto(document.getElementById('editContainer'), value);
|
||||||
for (const a of container.querySelectorAll('a[href]')) {
|
document.getElementById('editSwitch').checked = false;
|
||||||
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');
|
|
||||||
}
|
|
||||||
for (const t of container.querySelectorAll('blockquote')) {
|
|
||||||
t.classList.add('blockquote', 'border-start', 'border-5', 'border-success', 'mt-3', 'ps-3');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeMode() {
|
function cancelEdit() {
|
||||||
const container = document.getElementById('editContainer');
|
document.getElementById('editSwitch').checked = false;
|
||||||
const toggle = document.getElementById('editSwitch');
|
MarkDown.renderInto(document.getElementById('editContainer'), getMarkdown());
|
||||||
if (!toggle.checked) return renderView(getMarkdown());
|
}
|
||||||
|
|
||||||
const current = getMarkdown();
|
function togglePreview() {
|
||||||
container.innerHTML = `
|
const ta = document.getElementById('editor');
|
||||||
<textarea class="form-control w-100 auto-md" id="editor" name="notes">${escapeForTextarea(current)}</textarea>
|
const preview = document.getElementById('preview');
|
||||||
<div class="mt-2 d-flex gap-2">
|
preview.classList.toggle('d-none');
|
||||||
<button type="button" class="btn btn-primary btn-sm" onclick="saveEdit()">Save</button>
|
if (!preview.classList.contains('d-none')) {
|
||||||
<button type="button" class="btn btn-secondary btn-sm" onclick="cancelEdit()">Cancel</button>
|
MarkDown.renderInto(preview, ta ? ta.value : "");
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="togglePreview()">Preview</button>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 border rounded p-2 d-none" id="preview" aria-live="polite"></div>
|
|
||||||
`;
|
|
||||||
const ta = document.getElementById('editor');
|
|
||||||
autoGrow(ta);
|
|
||||||
ta.addEventListener('input', () => autoGrow(ta));
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function saveEdit() {
|
function autoGrow(ta) {
|
||||||
const ta = document.getElementById('editor');
|
if (!ta) return;
|
||||||
const value = ta ? ta.value : "";
|
if (CSS?.supports?.('field-sizing: content')) return;
|
||||||
setMarkdown(value);
|
ta.style.height = 'auto';
|
||||||
renderView(value);
|
ta.style.height = ta.scrollHeight + 'px';
|
||||||
document.getElementById('editSwitch').checked = false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function cancelEdit() {
|
function escapeForTextarea(s) {
|
||||||
document.getElementById('editSwitch').checked = false;
|
return (s ?? "").replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
renderView(getMarkdown());
|
}
|
||||||
}
|
</script>
|
||||||
|
|
||||||
function togglePreview() {
|
|
||||||
const ta = document.getElementById('editor');
|
|
||||||
const preview = document.getElementById('preview');
|
|
||||||
preview.classList.toggle('d-none');
|
|
||||||
if (!preview.classList.contains('d-none')) {
|
|
||||||
const html = marked.parse(ta ? ta.value : "");
|
|
||||||
preview.innerHTML = DOMPurify.sanitize(html);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function autoGrow(ta) {
|
|
||||||
if (!ta) return;
|
|
||||||
ta.style.height = 'auto';
|
|
||||||
ta.style.height = ta.scrollHeight + 'px';
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeForTextarea(s) {
|
|
||||||
return (s ?? "").replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<ul class="list-group mt-3">
|
<ul class="list-group mt-3">
|
||||||
{% for n in items %}
|
{% for n in items %}
|
||||||
<li class="list-group-item">
|
<li class="list-group-item" id="note-{{ n.id }}">
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
<div class="me-3 w-100 markdown-body" id="editContainer{{ n.id }}"></div>
|
<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>
|
<script type="application/json" id="md-{{ n.id }}">{{ n.content | tojson }}</script>
|
||||||
|
|
@ -63,52 +63,15 @@
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<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 src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/components/markdown.js') }}" defer></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/utils/json.js') }}" defer></script>
|
||||||
<script>
|
<script>
|
||||||
// State (kept global for compatibility with your form serialization)
|
// State (kept global for compatibility with your form serialization)
|
||||||
window.newDrafts = window.newDrafts || [];
|
window.newDrafts = window.newDrafts || [];
|
||||||
window.deletedIds = window.deletedIds || [];
|
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) {
|
function getMarkdown(id) {
|
||||||
const el = document.getElementById(`md-${id}`);
|
return readJSONScript(`md-${id}`, "");
|
||||||
return el ? JSON.parse(el.textContent || '""') : "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMarkdown(id, md) {
|
function setMarkdown(id, md) {
|
||||||
|
|
@ -122,14 +85,14 @@
|
||||||
|
|
||||||
function autoGrow(ta) {
|
function autoGrow(ta) {
|
||||||
if (!ta) return;
|
if (!ta) return;
|
||||||
|
if (CSS?.supports?.('field-sizing: content')) return;
|
||||||
ta.style.height = 'auto';
|
ta.style.height = 'auto';
|
||||||
ta.style.height = (ta.scrollHeight + 5) + 'px';
|
ta.style.height = (ta.scrollHeight + 5) + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- RENDERERS ----------
|
// ---------- RENDERERS ----------
|
||||||
function renderExistingView(id) {
|
function renderExistingView(id) {
|
||||||
const container = document.getElementById(`editContainer${id}`);
|
MarkDown.renderInto(document.getElementById(`editContainer${id}`), getMarkdown(id));
|
||||||
renderHTML(container, getMarkdown(id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEditor(id) {
|
function renderEditor(id) {
|
||||||
|
|
@ -167,8 +130,7 @@
|
||||||
|
|
||||||
const left = document.createElement('div');
|
const left = document.createElement('div');
|
||||||
left.className = 'w-100 markdown-body';
|
left.className = 'w-100 markdown-body';
|
||||||
left.innerHTML = renderMarkdown(md || '');
|
MarkDown.renderInto(left, md || '');
|
||||||
enhanceLinks(left);
|
|
||||||
|
|
||||||
const right = document.createElement('div');
|
const right = document.createElement('div');
|
||||||
right.className = 'ms-3 d-flex flex-column align-items-end';
|
right.className = 'ms-3 d-flex flex-column align-items-end';
|
||||||
|
|
@ -271,7 +233,7 @@
|
||||||
if (!preview) return;
|
if (!preview) return;
|
||||||
preview.classList.toggle('d-none');
|
preview.classList.toggle('d-none');
|
||||||
if (!preview.classList.contains('d-none')) {
|
if (!preview.classList.contains('d-none')) {
|
||||||
preview.innerHTML = renderMarkdown(ta ? ta.value : "");
|
MarkDown.renderInto(preview, ta ? ta.value : "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue