6895a3bf65
Remplace le formulaire unique par un wizard 5 étapes (création) et
6 étapes (édition) avec auto-sauvegarde en brouillon, détection de
tags depuis le texte (TagSuggester), aperçu SEO, diff avant validation
et plan Markdown dynamique dans l'éditeur.
Détail des changements :
- ArticleManager : +6 méthodes (updatePartialMeta, saveDraftOverlay,
getDraftOverlay, hasDraftOverlay, discardDraftOverlay, commitDraftOverlay)
- .htaccess : routes /new/{uuid}/{1-5} et /edit/{uuid}/{1-6}
- index.php : cases create et edit réécrits en switch($step),
nouveau case autosave_draft et edit_discard_draft
- assets/js/wizard.js : autosave debounce, auto-resize textarea,
scroll curseur, plan TOC dynamique, toggle pills tags
- templates/wizard/ : nav.php + step1..6.php
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
299 lines
14 KiB
JavaScript
299 lines
14 KiB
JavaScript
// wizard.js — autosave, insertions, couleur catégorie, génération slug
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
|
|
var page = document.getElementById('vl-page');
|
|
var uuid = page ? page.dataset.uuid : '';
|
|
var autosaveUrl = page ? page.dataset.autosaveUrl : '';
|
|
|
|
// ─── Auto-resize textarea + scroll curseur ──────────────────────────────
|
|
var ta = document.getElementById('wz-content');
|
|
if (ta) {
|
|
function resizeTa() { ta.style.height = 'auto'; ta.style.height = ta.scrollHeight + 'px'; }
|
|
ta.addEventListener('input', resizeTa);
|
|
resizeTa();
|
|
|
|
function scrollToCursor() {
|
|
var lineH = parseFloat(getComputedStyle(ta).lineHeight) || 20;
|
|
var padT = parseFloat(getComputedStyle(ta).paddingTop) || 8;
|
|
var lines = ta.value.substr(0, ta.selectionStart).split('\n').length;
|
|
var cursorY = ta.getBoundingClientRect().top + padT + lines * lineH;
|
|
var margin = lineH * 3;
|
|
if (cursorY > window.innerHeight - margin) {
|
|
window.scrollBy({ top: cursorY - window.innerHeight + margin, behavior: 'instant' });
|
|
} else if (cursorY < margin) {
|
|
window.scrollBy({ top: cursorY - margin, behavior: 'instant' });
|
|
}
|
|
}
|
|
ta.addEventListener('keyup', scrollToCursor);
|
|
ta.addEventListener('click', scrollToCursor);
|
|
}
|
|
|
|
// ─── Ctrl+Enter soumet le formulaire ────────────────────────────────────
|
|
var form = document.querySelector('form[method="POST"]');
|
|
if (form) {
|
|
form.addEventListener('keydown', function (e) {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { form.submit(); }
|
|
});
|
|
}
|
|
|
|
// ─── Génération slug automatique (étape 1 / création) ───────────────────
|
|
var titleInput = document.getElementById('wz-title');
|
|
var slugField = document.getElementById('slug');
|
|
var slugPreview = document.getElementById('slug-preview');
|
|
|
|
function slugify(s) {
|
|
var map = {'à':'a','â':'a','ä':'a','é':'e','è':'e','ê':'e','ë':'e','î':'i','ï':'i','ô':'o','ö':'o','ù':'u','û':'u','ü':'u','ç':'c','æ':'ae','œ':'oe'};
|
|
return s.toLowerCase()
|
|
.replace(/[àâäéèêëîïôöùûüçæœ]/g, function(c) { return map[c] || c; })
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '');
|
|
}
|
|
|
|
if (titleInput && slugField) {
|
|
if (slugField.value !== '') slugField._auto = false;
|
|
titleInput.addEventListener('input', function () {
|
|
if (slugField._auto !== false) {
|
|
var gen = slugify(this.value);
|
|
slugField.value = gen;
|
|
if (slugPreview) slugPreview.textContent = gen;
|
|
}
|
|
});
|
|
slugField.addEventListener('input', function () {
|
|
this._auto = (this.value === '');
|
|
if (slugPreview) slugPreview.textContent = this.value;
|
|
});
|
|
}
|
|
|
|
// ─── Autosave ────────────────────────────────────────────────────────────
|
|
var indicator = document.getElementById('autosave-indicator');
|
|
if (indicator && uuid && autosaveUrl) {
|
|
var timer = null;
|
|
var titleEl = document.getElementById('wz-title');
|
|
var contentEl = document.getElementById('wz-content');
|
|
|
|
function scheduleAutosave() {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(doAutosave, 3000);
|
|
}
|
|
|
|
async function doAutosave() {
|
|
if (!titleEl || !contentEl) return;
|
|
indicator.textContent = 'Sauvegarde…';
|
|
try {
|
|
var res = await fetch(autosaveUrl, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
|
body: new URLSearchParams({
|
|
title: titleEl.value,
|
|
content: contentEl.value,
|
|
slug: slugField ? slugField.value : '',
|
|
}),
|
|
});
|
|
var data = await res.json();
|
|
indicator.textContent = data.ok ? 'Brouillon sauvegardé à ' + data.time : 'Erreur de sauvegarde';
|
|
} catch (err) {
|
|
indicator.textContent = 'Erreur de sauvegarde';
|
|
}
|
|
}
|
|
|
|
if (titleEl) titleEl.addEventListener('input', scheduleAutosave);
|
|
if (ta) ta.addEventListener('input', scheduleAutosave);
|
|
}
|
|
|
|
// ─── Insertion Markdown depuis miniatures ────────────────────────────────
|
|
var insertUrl = page ? page.dataset.insertUrl : '';
|
|
|
|
document.querySelectorAll('[data-insert-ref]').forEach(function (el) {
|
|
el.addEventListener('click', function () {
|
|
if (!ta) return;
|
|
var ref = this.dataset.insertRef;
|
|
var isImage = /\.(jpe?g|png|gif|webp|svg|avif)(\?.*)?$/i.test(ref);
|
|
var md = isImage ? '' : '[' + ref + '](' + ref + ')';
|
|
var sep = ta.value.length > 0 && !ta.value.endsWith('\n') ? '\n' : '';
|
|
ta.value += sep + md;
|
|
ta.focus();
|
|
ta.selectionStart = ta.selectionEnd = ta.value.length;
|
|
ta.dispatchEvent(new Event('input'));
|
|
});
|
|
});
|
|
|
|
if (insertUrl) {
|
|
var isImg = /\.(jpe?g|png|gif|webp|svg|avif)(\?.*)?$/i.test(insertUrl);
|
|
var name = decodeURIComponent(insertUrl.split('/').pop().split('?')[0]) || 'fichier';
|
|
var ref = isImg ? '' : '[' + name + '](' + insertUrl + ')';
|
|
if (ta) {
|
|
var sep = ta.value.length > 0 && !ta.value.endsWith('\n') ? '\n' : '';
|
|
ta.value += sep + ref;
|
|
ta.dispatchEvent(new Event('input'));
|
|
}
|
|
}
|
|
|
|
// ─── Copier référence Markdown (bouton MD dans la liste des fichiers) ────
|
|
document.querySelectorAll('[data-copy-md-name]').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
if (!ta) return;
|
|
var name = this.dataset.copyMdName;
|
|
var isImage = this.dataset.copyMdIsImage === '1';
|
|
var md = isImage ? '' : '[' + name + '](' + name + ')';
|
|
var sep = ta.value.length > 0 && !ta.value.endsWith('\n') ? '\n' : '';
|
|
ta.value += sep + md;
|
|
ta.focus();
|
|
ta.dispatchEvent(new Event('input'));
|
|
});
|
|
});
|
|
|
|
// ─── Aperçu couleur catégorie (étape 3) ─────────────────────────────────
|
|
var KNOWN_CATS = {
|
|
'actualité': 10, 'travaux': 35, 'scolaire': 55,
|
|
'linux': 120, 'domotique': 160, 'télécom': 190,
|
|
'blog': 220, 'informatique': 255, 'réflexion': 285,
|
|
'loisirs': 320, 'perso': 345,
|
|
};
|
|
var FREE_HUES = [87, 140, 205, 237, 302];
|
|
|
|
var catInput = document.getElementById('category');
|
|
var catSwatch = document.getElementById('cat-swatch');
|
|
var catHint = document.getElementById('cat-hint');
|
|
var catSwatches = document.getElementById('cat-free-swatches');
|
|
|
|
function catHue(name) {
|
|
var key = name.toLowerCase().trim();
|
|
if (KNOWN_CATS[key] !== undefined) return KNOWN_CATS[key];
|
|
var h = 0;
|
|
for (var i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) & 0xffff;
|
|
return h % 360;
|
|
}
|
|
|
|
function updateCatSwatch() {
|
|
if (!catInput || !catSwatch) return;
|
|
var v = catInput.value.trim();
|
|
if (v === '') {
|
|
catSwatch.style.background = '#e5e7eb';
|
|
catSwatch.title = '';
|
|
if (catHint) catHint.textContent = '';
|
|
} else {
|
|
var hue = catHue(v);
|
|
catSwatch.style.background = 'hsl(' + hue + ',55%,52%)';
|
|
catSwatch.title = 'hsl(' + hue + ', 55%, 52%)';
|
|
if (catHint) {
|
|
var known = KNOWN_CATS[v.toLowerCase()] !== undefined;
|
|
catHint.textContent = known ? 'Couleur fixe' : 'Nouvelle catégorie (couleur générée)';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (catInput) {
|
|
catInput.addEventListener('input', updateCatSwatch);
|
|
updateCatSwatch();
|
|
|
|
if (catSwatches) {
|
|
FREE_HUES.forEach(function (h) {
|
|
var sw = document.createElement('span');
|
|
sw.style.cssText = 'display:inline-block;width:20px;height:20px;border-radius:4px;cursor:pointer;background:hsl(' + h + ',55%,52%)';
|
|
sw.title = 'hsl(' + h + ', 55%, 52%)';
|
|
sw.addEventListener('click', function () {
|
|
// trouver ou créer le nom correspondant
|
|
catInput.dispatchEvent(new Event('input'));
|
|
});
|
|
catSwatches.appendChild(sw);
|
|
});
|
|
}
|
|
}
|
|
|
|
// ─── Plan (TOC dynamique) ────────────────────────────────────────────────
|
|
var tocList = document.getElementById('wz-toc-list');
|
|
if (tocList && ta) {
|
|
function buildToc() {
|
|
var lines = ta.value.split('\n');
|
|
var items = [];
|
|
lines.forEach(function (line) {
|
|
var m = line.match(/^(#{1,6})\s+(.+)/);
|
|
if (m) { items.push({ level: m[1].length, text: m[2].trim() }); }
|
|
});
|
|
if (items.length === 0) {
|
|
tocList.innerHTML = '<li class="small text-muted fst-italic px-1">Aucun titre</li>';
|
|
return;
|
|
}
|
|
var minLevel = Math.min.apply(null, items.map(function (i) { return i.level; }));
|
|
tocList.innerHTML = items.map(function (item) {
|
|
var indent = (item.level - minLevel) * 12;
|
|
var escaped = item.text.replace(/&/g,'&').replace(/</g,'<');
|
|
return '<li class="small text-truncate py-0" style="padding-left:' + indent + 'px" title="' + escaped + '">'
|
|
+ '<span class="text-muted me-1" style="font-size:.65rem">H' + item.level + '</span>'
|
|
+ escaped + '</li>';
|
|
}).join('');
|
|
}
|
|
ta.addEventListener('input', buildToc);
|
|
buildToc();
|
|
}
|
|
|
|
// ─── Sélection catégorie — pills .wz-cat-pick (étape 3) ─────────────────
|
|
document.querySelectorAll('.wz-cat-pick').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var catInp = document.getElementById('category');
|
|
if (catInp) {
|
|
catInp.value = this.dataset.cat;
|
|
catInp.dispatchEvent(new Event('input'));
|
|
}
|
|
document.querySelectorAll('.wz-cat-pick').forEach(function (b) { b.classList.remove('active'); });
|
|
this.classList.add('active');
|
|
});
|
|
});
|
|
|
|
// ─── Toggle tags — pills .wz-tag-pill (étape 4) ──────────────────────────
|
|
document.querySelectorAll('.wz-tag-pills').forEach(function (container) {
|
|
var targetId = container.dataset.target;
|
|
var inp = document.getElementById(targetId);
|
|
if (!inp) return;
|
|
container.querySelectorAll('.wz-tag-pill').forEach(function (pill) {
|
|
pill.addEventListener('click', function () {
|
|
var val = this.dataset.value;
|
|
var parts = inp.value.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
|
var idx = parts.indexOf(val);
|
|
if (idx >= 0) {
|
|
parts.splice(idx, 1);
|
|
this.classList.remove('btn-secondary', 'btn-info');
|
|
this.classList.add(this.classList.contains('btn-outline-info') ? 'btn-outline-info' : 'btn-outline-secondary');
|
|
} else {
|
|
parts.push(val);
|
|
var isDetected = this.classList.contains('btn-outline-info') || this.classList.contains('btn-info');
|
|
this.classList.remove('btn-outline-secondary', 'btn-outline-info');
|
|
this.classList.add(isDetected ? 'btn-info' : 'btn-secondary');
|
|
}
|
|
inp.value = parts.join(', ');
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── Image de couverture .wz-cover-thumb (étape 5) ───────────────────────
|
|
document.querySelectorAll('.wz-cover-thumb').forEach(function (img) {
|
|
img.addEventListener('click', function () {
|
|
document.querySelectorAll('.wz-cover-thumb').forEach(function (i) { i.classList.remove('wz-cover-selected'); });
|
|
this.classList.add('wz-cover-selected');
|
|
});
|
|
});
|
|
|
|
// ─── Compteurs SEO (étape 5) ──────────────────────────────────────────────
|
|
(function () {
|
|
function counter(inputId, counterId, warn) {
|
|
var el = document.getElementById(inputId);
|
|
var ct = document.getElementById(counterId);
|
|
if (!el || !ct) return;
|
|
function upd() { var l = el.value.length; ct.textContent = l + ' / ' + warn; ct.className = 'small ' + (l > warn ? 'text-danger' : (l < warn * 0.5 ? 'text-muted' : 'text-success')); }
|
|
el.addEventListener('input', upd);
|
|
upd();
|
|
}
|
|
counter('seo_title', 'seo_title_counter', 60);
|
|
counter('seo_description', 'seo_desc_counter', 155);
|
|
}());
|
|
|
|
// ─── Confirmation data-confirm ────────────────────────────────────────────
|
|
document.querySelectorAll('[data-confirm]').forEach(function (el) {
|
|
el.addEventListener('click', function (e) {
|
|
if (!confirm(this.dataset.confirm)) e.preventDefault();
|
|
});
|
|
});
|
|
|
|
});
|