mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Rework contenus-edit: auto-save, OverType toolbar, dynamic sidebar links
- Auto-save: new autosave.js with 1.5s debounce, watches all forms with
data-autosave, POSTs to form action with Accept: application/json, shows
saving/saved/error status indicator
- All action handlers (page.php, apropos.php, form-help.php) now detect
JSON Accept header and return {success, csrf_token} or {error} responses
- OverType toolbar enabled (toolbar:true) on all three markdown editors
(page, about_page, form_help)
- Sidebar links: replaced fixed erg_site_url / source_code_url rows with
dynamic sidebar_links array of {label, url} objects. Add/remove via JS.
Fallback migration reads legacy keys if sidebar_links is empty.
- Updated AboutController and about.php template to render dynamic links
- Updated apropos.css: unified .apropos-toc-link replacing .apropos-toc-erg
and .apropos-toc-source
- New CSS: autosave-status states, sidebar-link-row layout
- Removed all Enregistrer + Annuler buttons — auto-save and h1 back-arrow
make them redundant
This commit is contained in:
@@ -2140,3 +2140,63 @@ th.admin-ap-col {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ── Sidebar links editor ──────────────────────────────────────────────── */
|
||||
|
||||
.sidebar-link-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-xs);
|
||||
margin-bottom: var(--space-s);
|
||||
}
|
||||
|
||||
.sidebar-link-fields {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: var(--space-s);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar-link-fields > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-link-fields label {
|
||||
font-size: var(--step--1);
|
||||
margin-bottom: var(--space-3xs);
|
||||
}
|
||||
|
||||
.sidebar-link-fields input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sidebar-link-row .admin-icon-btn--delete {
|
||||
margin-top: calc(var(--step--1) + var(--space-3xs) + 0.3em);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Auto-save status indicator ────────────────────────────────────────── */
|
||||
|
||||
.autosave-status {
|
||||
font-size: var(--step--2);
|
||||
min-height: 1.5em;
|
||||
margin-top: var(--space-2xs);
|
||||
color: var(--text-tertiary);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.autosave-status--saving {
|
||||
color: var(--accent-yellow);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.autosave-status--saved {
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.autosave-status--error {
|
||||
color: var(--error);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -71,35 +71,23 @@
|
||||
border-left-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.apropos-toc-erg {
|
||||
.apropos-toc-link:first-of-type {
|
||||
padding-top: var(--space-s);
|
||||
border-top: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.apropos-toc-erg a {
|
||||
font-size: var(--step--2);
|
||||
color: var(--accent-primary);
|
||||
text-decoration: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.apropos-toc-erg a:hover {
|
||||
color: var(--accent-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.apropos-toc-source {
|
||||
.apropos-toc-link + .apropos-toc-link {
|
||||
padding-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.apropos-toc-source a {
|
||||
.apropos-toc-link a {
|
||||
font-size: var(--step--2);
|
||||
color: var(--accent-primary);
|
||||
text-decoration: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.apropos-toc-source a:hover {
|
||||
.apropos-toc-link a:hover {
|
||||
color: var(--accent-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -326,7 +314,7 @@
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.apropos-toc-erg {
|
||||
.apropos-toc-link {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
margin-left: auto;
|
||||
|
||||
76
app/public/assets/js/app/autosave.js
Normal file
76
app/public/assets/js/app/autosave.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Auto-save for admin content edit forms.
|
||||
*
|
||||
* Watches forms with [data-autosave] attribute. Debounces 1.5s after the
|
||||
* last change, POSTs to the form's action, and shows a small status bar.
|
||||
*
|
||||
* The status indicator lives in a sibling element with [data-autosave-status].
|
||||
* States: idle → saving… → saved ✓ / error !
|
||||
*/
|
||||
(() => {
|
||||
const forms = document.querySelectorAll('form[data-autosave]');
|
||||
if (!forms.length) return;
|
||||
|
||||
const DEBOUNCE_MS = 1500;
|
||||
|
||||
for (const form of forms) {
|
||||
const statusEl = document.querySelector(form.dataset.autosaveStatus || '[data-autosave-status]');
|
||||
let timer = null;
|
||||
let dirty = false;
|
||||
|
||||
const setStatus = (text, cls) => {
|
||||
if (!statusEl) return;
|
||||
statusEl.textContent = text;
|
||||
statusEl.className = 'autosave-status ' + (cls || '');
|
||||
};
|
||||
|
||||
const doSave = () => {
|
||||
if (!dirty) return;
|
||||
dirty = false;
|
||||
setStatus('Enregistrement…', 'autosave-status--saving');
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'Accept': 'application/json' },
|
||||
})
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
setStatus('Enregistré ✓', 'autosave-status--saved');
|
||||
// Refresh CSRF token from response if provided
|
||||
r.json().then((data) => {
|
||||
if (data && data.csrf_token) {
|
||||
const csrfInput = form.querySelector('input[name="csrf_token"]');
|
||||
if (csrfInput) csrfInput.value = data.csrf_token;
|
||||
const meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (meta) meta.setAttribute('content', data.csrf_token);
|
||||
}
|
||||
}).catch(() => {});
|
||||
})
|
||||
.catch(() => {
|
||||
setStatus('Erreur !', 'autosave-status--error');
|
||||
dirty = true;
|
||||
});
|
||||
};
|
||||
|
||||
const schedule = () => {
|
||||
dirty = true;
|
||||
setStatus('', '');
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(doSave, DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
// Watch all inputs inside the form
|
||||
form.addEventListener('input', schedule);
|
||||
form.addEventListener('change', schedule);
|
||||
|
||||
// Clear dirty on manual submit
|
||||
form.addEventListener('submit', () => {
|
||||
clearTimeout(timer);
|
||||
dirty = false;
|
||||
setStatus('', '');
|
||||
});
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user