Editor refactored to expose controls; Edit moved into the doc header as the orange primary action; new Revisions dropdown lists page_revisions with a modal preview + Restore. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
58 lines
2.4 KiB
JavaScript
58 lines
2.4 KiB
JavaScript
// Revisions dropdown for a page: lists page_revisions; clicking one opens a modal
|
|
// with the rendered old content + a Restore action (PATCH body_md).
|
|
import { el } from '../dom.js';
|
|
import { api } from '../api.js';
|
|
import { marked } from '../vendor/marked.esm.js';
|
|
import DOMPurify from '../vendor/purify.es.mjs';
|
|
|
|
export function revisionsMenu(pageId, onRestore) {
|
|
const wrap = el('div', { class: 'exp-menu' });
|
|
const list = el('div', { class: 'exp-list rev-list' });
|
|
|
|
const btn = el('button', {
|
|
class: 'ghost',
|
|
onclick: async (e) => { e.stopPropagation(); if (wrap.classList.toggle('open')) await load(); }
|
|
}, 'Revisions ▾');
|
|
|
|
async function load() {
|
|
list.replaceChildren(el('div', { class: 'muted', style: { padding: '7px 10px' } }, 'Loading…'));
|
|
let revs = [];
|
|
try { revs = await api.get('/api/pages/' + pageId + '/revisions'); } catch { /* */ }
|
|
list.replaceChildren();
|
|
if (!revs.length) { list.appendChild(el('div', { class: 'muted', style: { padding: '7px 10px' } }, 'No earlier revisions.')); return; }
|
|
revs.forEach(r => list.appendChild(el('button', { onclick: () => view(r) },
|
|
new Date(r.created_at).toLocaleString() + (r.edited_by ? ` · ${r.edited_by}` : ''))));
|
|
}
|
|
|
|
function view(rev) {
|
|
wrap.classList.remove('open');
|
|
const body = el('div', { class: 'md-preview' });
|
|
body.innerHTML = DOMPurify.sanitize(marked.parse(rev.body_md || ''));
|
|
const modal = el('div', {
|
|
class: 'rev-overlay',
|
|
onclick: (e) => { if (e.target === modal) modal.remove(); }
|
|
},
|
|
el('div', { class: 'rev-modal' },
|
|
el('div', { class: 'rev-modal-head' },
|
|
el('span', {}, 'Revision · ' + new Date(rev.created_at).toLocaleString()),
|
|
el('button', { class: 'ghost', onclick: () => modal.remove() }, 'Close')),
|
|
body,
|
|
el('div', { class: 'rev-modal-foot' },
|
|
el('button', {
|
|
class: 'primary',
|
|
onclick: async () => {
|
|
try {
|
|
await api.patch('/api/pages/' + pageId, { body_md: rev.body_md });
|
|
if (onRestore) onRestore(rev.body_md);
|
|
modal.remove();
|
|
} catch (e) { alert('Restore failed: ' + e.message); }
|
|
}
|
|
}, 'Restore this version'))));
|
|
document.body.appendChild(modal);
|
|
}
|
|
|
|
wrap.append(btn, list);
|
|
document.addEventListener('click', () => wrap.classList.remove('open'));
|
|
return wrap;
|
|
}
|