feat : wizard multi-étapes, migrations contenu, versionnage semver (v1.2.1) #60

Merged
cedricAbonnel merged 2 commits from feat/wizard-multi-step into main 2026-05-14 21:17:20 +00:00
7 changed files with 29 additions and 27 deletions
Showing only changes of commit 72cb7acae4 - Show all commits
+11
View File
@@ -9,6 +9,17 @@ Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) — versionnag
---
## [1.2.1] - 2026-05-14
### Corrigé
- Cache article invalidé si `index.md` est plus récent que `meta.json` (migration de contenu ne se reflétait pas)
- Migration 001 : `touch(meta.json)` après écriture de `index.md` pour invalider le cache
- `post_view.php` : le `# Titre` Markdown est retiré du rendu (déjà affiché par le template)
- Wizard étape 1 : en-tête affiche « Modifier » sans répéter le titre de l'article
- `wizard.js` : suppression de `scrollToCursor` (calcul erroné sur textarea auto-resize) ; Ctrl+Home / Ctrl+End scrollent correctement via `scrollIntoView`
---
## [1.2.0] - 2026-05-14
### Ajouté
+7 -20
View File
@@ -13,20 +13,13 @@ document.addEventListener('DOMContentLoaded', function () {
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+Home / Ctrl+End : scroller la fenêtre vers le début/fin du textarea
ta.addEventListener('keydown', function (e) {
if (!(e.ctrlKey || e.metaKey) || (e.key !== 'Home' && e.key !== 'End')) return;
requestAnimationFrame(function () {
ta.scrollIntoView({ block: e.key === 'Home' ? 'start' : 'end', behavior: 'smooth' });
});
});
}
// ─── Ctrl+Enter soumet le formulaire ────────────────────────────────────
@@ -61,12 +54,6 @@ document.addEventListener('DOMContentLoaded', function () {
var data = await res.json();
if (data.ok) {
indicator.textContent = 'Sauvegardé à ' + data.time;
// Mettre à jour le titre de la page si le serveur l'a extrait
var pageTitle = document.getElementById('wz-page-title');
if (pageTitle && data.title) {
var prefix = pageTitle.dataset.prefix || '';
pageTitle.textContent = prefix ? prefix + data.title : data.title;
}
} else {
indicator.textContent = 'Erreur de sauvegarde';
}
+1 -1
View File
@@ -1 +1 @@
1.2.0
1.2.1
@@ -43,6 +43,7 @@ foreach (glob($dataDir . '/*/meta.json') as $metaPath) {
}
file_put_contents($mdPath, '# ' . $title . "\n\n" . ltrim($content));
touch($metaPath);
$updated++;
}
+3 -2
View File
@@ -1164,8 +1164,9 @@ class ArticleManager
$uuid = basename($dir);
$cachePath = $this->articleCachePath($uuid);
// Utiliser le cache si plus récent que meta.json
if (file_exists($cachePath) && filemtime($cachePath) >= filemtime($metaPath)) {
// Utiliser le cache si plus récent que meta.json ET index.md
$contentMtime = file_exists($dir . '/index.md') ? filemtime($dir . '/index.md') : 0;
if (file_exists($cachePath) && filemtime($cachePath) >= filemtime($metaPath) && filemtime($cachePath) >= $contentMtime) {
$cached = json_decode((string) file_get_contents($cachePath), true);
if (is_array($cached) && !empty($cached['uuid'])) {
return $cached;
+3 -1
View File
@@ -9,6 +9,8 @@ $_accentMap = [
];
$_tocItems = [];
$_tocSeen = [];
// Le titre H1 est déjà affiché par le template ; on le retire du rendu.
$_rawForRender = preg_replace('/^\s*# [^\n]*\n*/u', '', $rawContent);
$_renderedContent = preg_replace_callback(
'/<(h[23])>(.+?)<\/h[23]>/i',
function ($m) use (&$_tocItems, &$_tocSeen, $_accentMap) {
@@ -31,7 +33,7 @@ $_renderedContent = preg_replace_callback(
$_tocItems[] = ['level' => $level, 'text' => $plain, 'id' => $id];
return "<{$tag} id=\"" . htmlspecialchars($id) . "\">{$inner}</{$tag}>";
},
$Parsedown->text($rawContent)
$Parsedown->text($_rawForRender)
);
ob_start();
+3 -3
View File
@@ -19,8 +19,7 @@ $_hasUuid = $_wizUuid !== '';
<!-- En-tête avec boutons ────────────────────────────────────────────────── -->
<div class="d-flex align-items-center justify-content-between gap-3 mb-4 flex-wrap">
<div>
<h1 class="h4 mb-0" id="wz-page-title"
data-prefix="<?= $mode === 'edit' ? 'Modifier — ' : '' ?>"><?= $mode === 'create' ? 'Nouvel article' : htmlspecialchars('Modifier — ' . ($article['title'] ?? '')) ?></h1>
<h1 class="h4 mb-0" id="wz-page-title"><?= $mode === 'create' ? 'Nouvel article' : 'Modifier' ?></h1>
<?php if ($_hasUuid): ?>
<span id="autosave-indicator" class="text-muted small"></span>
<?php endif; ?>
@@ -44,7 +43,8 @@ $_hasUuid = $_wizUuid !== '';
<div class="mb-3">
<label for="wz-content" class="form-label fw-semibold">Contenu <small class="text-muted fw-normal">(Markdown)</small></label>
<textarea class="form-control font-monospace" id="wz-content" name="content" rows="18"
style="min-height:320px"><?= htmlspecialchars($content ?? '') ?></textarea>
style="min-height:320px"
placeholder="# Titre de l'article&#10;&#10;Votre contenu ici…"><?= htmlspecialchars($content ?? '') ?></textarea>
</div>
</div><!-- /col-lg-9 -->