feat: table des matières auto-générée avec ancres et suivi de défilement
- Génération côté PHP depuis les h2/h3 du contenu rendu (slug, accents, déduplication) - Injection d'id sur chaque titre pour les ancres - Affichage dans la sidebar si >= 3 titres, H3 indenté - Sidebar défilable (max-height 100vh) pour que la TOC reste visible - IntersectionObserver JS : surlignage du chapitre courant au défilement
This commit is contained in:
@@ -597,6 +597,10 @@ textarea.form-control {
|
|||||||
.related-sidebar {
|
.related-sidebar {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 1.5rem;
|
top: 1.5rem;
|
||||||
|
max-height: calc(100vh - 3rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--vl-border) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Post layout: colonnes sidebar fixe + article flexible ── */
|
/* ─── Post layout: colonnes sidebar fixe + article flexible ── */
|
||||||
@@ -617,6 +621,40 @@ textarea.form-control {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toc-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-list a {
|
||||||
|
display: block;
|
||||||
|
font-size: .8rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--vl-muted);
|
||||||
|
padding: .25rem .5rem;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
transition: color .15s, border-color .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-list a:hover,
|
||||||
|
.toc-list a.toc-active {
|
||||||
|
color: var(--vl-accent);
|
||||||
|
border-left-color: var(--vl-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-list a.toc-active {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-h3 a {
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
font-size: .775rem;
|
||||||
|
}
|
||||||
|
|
||||||
.related-card {
|
.related-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|||||||
-1
File diff suppressed because one or more lines are too long
+72
-1
@@ -2,6 +2,38 @@
|
|||||||
require_once __DIR__ . '/../src/Parsedown.php';
|
require_once __DIR__ . '/../src/Parsedown.php';
|
||||||
$Parsedown = new Parsedown();
|
$Parsedown = new Parsedown();
|
||||||
|
|
||||||
|
$_accentMap = [
|
||||||
|
'à' => 'a','â' => 'a','ä' => 'a','á' => 'a','é' => 'e','è' => 'e','ê' => 'e','ë' => 'e',
|
||||||
|
'î' => 'i','ï' => 'i','í' => 'i','ô' => 'o','ö' => 'o','ó' => 'o','ù' => 'u','û' => 'u',
|
||||||
|
'ü' => 'u','ú' => 'u','ç' => 'c','ñ' => 'n','æ' => 'ae','œ' => 'oe',
|
||||||
|
];
|
||||||
|
$_tocItems = [];
|
||||||
|
$_tocSeen = [];
|
||||||
|
$_renderedContent = preg_replace_callback(
|
||||||
|
'/<(h[23])>(.+?)<\/h[23]>/i',
|
||||||
|
function ($m) use (&$_tocItems, &$_tocSeen, $_accentMap) {
|
||||||
|
$tag = $m[1];
|
||||||
|
$inner = $m[2];
|
||||||
|
$level = (int) substr($tag, 1);
|
||||||
|
$plain = strip_tags($inner);
|
||||||
|
$slug = trim(preg_replace(
|
||||||
|
'/[^a-z0-9]+/',
|
||||||
|
'-',
|
||||||
|
mb_strtolower(strtr($plain, $_accentMap), 'UTF-8')
|
||||||
|
), '-') ?: 'section';
|
||||||
|
if (isset($_tocSeen[$slug])) {
|
||||||
|
$_tocSeen[$slug]++;
|
||||||
|
$id = $slug . '-' . $_tocSeen[$slug];
|
||||||
|
} else {
|
||||||
|
$_tocSeen[$slug] = 0;
|
||||||
|
$id = $slug;
|
||||||
|
}
|
||||||
|
$_tocItems[] = ['level' => $level, 'text' => $plain, 'id' => $id];
|
||||||
|
return "<{$tag} id=\"" . htmlspecialchars($id) . "\">{$inner}</{$tag}>";
|
||||||
|
},
|
||||||
|
$Parsedown->text($rawContent)
|
||||||
|
);
|
||||||
|
|
||||||
ob_start();
|
ob_start();
|
||||||
|
|
||||||
$coverFile = $article['cover'] ?? '';
|
$coverFile = $article['cover'] ?? '';
|
||||||
@@ -126,7 +158,7 @@ $hasSources = (!empty($externalLinks) || !empty($files))
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-text post-content">
|
<div class="card-text post-content">
|
||||||
<?= $Parsedown->text($rawContent) ?>
|
<?= $_renderedContent ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,6 +175,17 @@ $hasSources = (!empty($externalLinks) || !empty($files))
|
|||||||
<div class="post-sidebar-col order-3">
|
<div class="post-sidebar-col order-3">
|
||||||
<aside class="related-sidebar">
|
<aside class="related-sidebar">
|
||||||
|
|
||||||
|
<?php if (count($_tocItems) >= 3): ?>
|
||||||
|
<h6 class="related-sidebar-title">Table des matières</h6>
|
||||||
|
<ul class="toc-list">
|
||||||
|
<?php foreach ($_tocItems as $_ti): ?>
|
||||||
|
<li class="toc-h<?= (int) $_ti['level'] ?>">
|
||||||
|
<a href="#<?= htmlspecialchars($_ti['id']) ?>"><?= htmlspecialchars($_ti['text']) ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (!empty($backlinks ?? [])): ?>
|
<?php if (!empty($backlinks ?? [])): ?>
|
||||||
<h6 class="related-sidebar-title">Rétroliens</h6>
|
<h6 class="related-sidebar-title">Rétroliens</h6>
|
||||||
<?php foreach ($backlinks as $_bl):
|
<?php foreach ($backlinks as $_bl):
|
||||||
@@ -264,6 +307,34 @@ $hasSources = (!empty($externalLinks) || !empty($files))
|
|||||||
|
|
||||||
</div><!-- /row -->
|
</div><!-- /row -->
|
||||||
|
|
||||||
|
<?php if (count($_tocItems) >= 3): ?>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var headings = document.querySelectorAll('.post-content h2, .post-content h3');
|
||||||
|
var links = document.querySelectorAll('.toc-list a');
|
||||||
|
if (!headings.length || !links.length) return;
|
||||||
|
|
||||||
|
var map = {};
|
||||||
|
links.forEach(function (a) {
|
||||||
|
map[decodeURIComponent(a.getAttribute('href').slice(1))] = a;
|
||||||
|
});
|
||||||
|
|
||||||
|
var active = null;
|
||||||
|
var observer = new IntersectionObserver(function (entries) {
|
||||||
|
entries.forEach(function (entry) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
if (active) active.classList.remove('toc-active');
|
||||||
|
active = map[entry.target.id] || null;
|
||||||
|
if (active) active.classList.add('toc-active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { rootMargin: '-8% 0px -82% 0px', threshold: 0 });
|
||||||
|
|
||||||
|
headings.forEach(function (h) { observer.observe(h); });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$content = ob_get_clean();
|
$content = ob_get_clean();
|
||||||
$title = htmlspecialchars($article['title']);
|
$title = htmlspecialchars($article['title']);
|
||||||
|
|||||||
Reference in New Issue
Block a user