pridaná generovaná navigácia z H1–H3 s unikátnymi ID,
pridaný responzívny panel a ukladanie stavu podľa URL, pridané verzovanie CSS a JS pomocou MD5 hashov, aktualizovaná dokumentácia
This commit is contained in:
+156
-1
@@ -9,6 +9,7 @@
|
||||
--quote-bg: #f4f8fa;
|
||||
--code-bg: #1f2328;
|
||||
--code-text: #f4f6f8;
|
||||
--toc-width: 19rem;
|
||||
}
|
||||
|
||||
*,
|
||||
@@ -201,10 +202,164 @@ body {
|
||||
}
|
||||
|
||||
.markdown-body :target {
|
||||
scroll-margin-top: 1rem;
|
||||
scroll-margin-top: 4.5rem;
|
||||
animation: mark-target 1.2s ease;
|
||||
}
|
||||
|
||||
/* Navigation lives outside the rendered Markdown. */
|
||||
.md2html-document {
|
||||
transition: margin-left 200ms ease;
|
||||
}
|
||||
|
||||
.markdown-body :is(h1, h2, h3) {
|
||||
scroll-margin-top: 4.5rem;
|
||||
}
|
||||
|
||||
html:has(.md2html-toc-ready) {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.md2html-toc-toggle,
|
||||
.md2html-toc-close {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
padding: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
color: var(--accent);
|
||||
background: var(--paper-bg);
|
||||
font: 1.25rem/1 sans-serif;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.md2html-toc-toggle {
|
||||
position: fixed;
|
||||
top: 0.75rem;
|
||||
left: 0.75rem;
|
||||
z-index: 30;
|
||||
box-shadow: 0 2px 6px #40312118;
|
||||
}
|
||||
|
||||
.md2html-toc-toggle[hidden],
|
||||
.md2html-toc[hidden],
|
||||
.md2html-toc-backdrop[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.md2html-toc-toggle[aria-expanded="true"] {
|
||||
color: var(--paper-bg);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.md2html-toc :is(a, button):focus-visible,
|
||||
.md2html-toc-toggle:focus-visible {
|
||||
outline: 3px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.md2html-toc {
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
top: 4rem;
|
||||
bottom: 1rem;
|
||||
left: 0.75rem;
|
||||
width: var(--toc-width);
|
||||
max-width: calc(100% - 4.5rem);
|
||||
max-height: calc(100vh - 5rem);
|
||||
max-height: calc(100dvh - 5rem);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--paper-bg);
|
||||
box-shadow: 0 8px 24px #40312120;
|
||||
font: 0.95rem/1.5 "Segoe UI", sans-serif;
|
||||
overflow-wrap: anywhere;
|
||||
visibility: hidden;
|
||||
transform: translateX(calc(-100% - 1rem));
|
||||
transition: transform 200ms ease, visibility 200ms;
|
||||
}
|
||||
|
||||
.md2html-toc-open .md2html-toc {
|
||||
visibility: visible;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.md2html-toc-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.md2html-toc-close {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.md2html-toc-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.md2html-toc-list a {
|
||||
display: block;
|
||||
padding: 0.4rem 0.25rem;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.md2html-toc-list a:hover {
|
||||
background: var(--accent-soft);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.md2html-toc-level-2 { padding-left: 1rem; }
|
||||
.md2html-toc-level-3 { padding-left: 2rem; }
|
||||
|
||||
.md2html-toc-backdrop { display: none; }
|
||||
|
||||
@media (min-width: 761px) {
|
||||
.md2html-toc-open .md2html-document {
|
||||
margin-left: calc(var(--toc-width) + 1.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.md2html-toc-ready .markdown-body {
|
||||
margin-top: 4.25rem;
|
||||
}
|
||||
|
||||
.md2html-toc-backdrop {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
background: #24201c55;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease, visibility 200ms;
|
||||
}
|
||||
|
||||
.md2html-toc-open .md2html-toc-backdrop {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html:has(.md2html-toc-ready) { scroll-behavior: auto; }
|
||||
.md2html-document,
|
||||
.md2html-toc,
|
||||
.md2html-toc-backdrop { transition: none; }
|
||||
.markdown-body :target { animation: none; }
|
||||
}
|
||||
|
||||
@keyframes mark-target {
|
||||
0% {
|
||||
background: #fff3c9;
|
||||
|
||||
+11
-1
@@ -14,7 +14,17 @@ $parser = new Parsedown();
|
||||
|
||||
echo '<!doctype html>';
|
||||
echo '<meta charset="utf-8">';
|
||||
echo '<link rel="stylesheet" href="//localhost/md2html/src/markdown.css">';
|
||||
echo '<meta name="viewport" content="width=device-width, initial-scale=1">';
|
||||
echo '<link rel="stylesheet" href="//localhost/md2html/src/markdown.css?h=' . md5_file(__DIR__ . '/markdown.css') . '">';
|
||||
echo '<script src="//localhost/md2html/src/toc.js?h=' . md5_file(__DIR__ . '/toc.js') . '" defer></script>';
|
||||
echo '<button class="md2html-toc-toggle" type="button" aria-expanded="false" aria-label="Zobraziť obsah dokumentu" hidden><span aria-hidden="true">☰</span></button>';
|
||||
echo '<div class="md2html-toc-backdrop" aria-hidden="true" hidden></div>';
|
||||
echo '<nav class="md2html-toc" aria-label="Obsah dokumentu" hidden>';
|
||||
echo '<div class="md2html-toc-header"><strong>Obsah dokumentu</strong><button class="md2html-toc-close" type="button" aria-label="Zatvoriť obsah dokumentu">×</button></div>';
|
||||
echo '<ol class="md2html-toc-list"></ol>';
|
||||
echo '</nav>';
|
||||
echo '<div class="md2html-document">';
|
||||
echo '<div class="markdown-body">';
|
||||
echo $parser->text($md);
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const content = document.querySelector('.markdown-body');
|
||||
const panel = document.querySelector('.md2html-toc');
|
||||
const toggle = document.querySelector('.md2html-toc-toggle');
|
||||
const close = document.querySelector('.md2html-toc-close');
|
||||
const backdrop = document.querySelector('.md2html-toc-backdrop');
|
||||
const list = document.querySelector('.md2html-toc-list');
|
||||
if (!content || !panel || !toggle || !close || !backdrop || !list) return;
|
||||
|
||||
// Reserve all existing anchors, including anchors outside headings.
|
||||
const usedIds = new Set(Array.from(document.querySelectorAll('[id]'), node => node.id));
|
||||
function uniqueId(base) {
|
||||
let id = base;
|
||||
let suffix = 2;
|
||||
while (usedIds.has(id)) id = `${base}-${suffix++}`;
|
||||
usedIds.add(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
panel.id = uniqueId('md2html-toc-panel');
|
||||
toggle.setAttribute('aria-controls', panel.id);
|
||||
const headings = content.querySelectorAll('h1, h2, h3');
|
||||
const seenHeadingIds = new Set();
|
||||
for (const heading of headings) {
|
||||
const label = heading.textContent.trim() || 'Bez názvu';
|
||||
if (!heading.id || seenHeadingIds.has(heading.id) || document.getElementById(heading.id) !== heading) {
|
||||
const slug = label.normalize('NFC').toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}_-]+/gu, '-').replace(/^-+|-+$/g, '') || 'nadpis';
|
||||
heading.id = uniqueId(slug);
|
||||
}
|
||||
seenHeadingIds.add(heading.id);
|
||||
|
||||
const item = document.createElement('li');
|
||||
item.className = `md2html-toc-level-${heading.tagName.slice(1)}`;
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', `#${encodeURIComponent(heading.id)}`);
|
||||
link.textContent = label;
|
||||
link.addEventListener('click', () => {
|
||||
if (window.matchMedia('(max-width: 760px)').matches) setOpen(false);
|
||||
if (!heading.hasAttribute('tabindex')) heading.setAttribute('tabindex', '-1');
|
||||
heading.focus({ preventScroll: true });
|
||||
// Native anchor navigation updates the fragment and supports Back/Forward.
|
||||
});
|
||||
item.append(link);
|
||||
list.append(item);
|
||||
}
|
||||
if (!headings.length) {
|
||||
const message = document.createElement('p');
|
||||
message.textContent = 'Obsah dokumentu nie je dostupný.';
|
||||
list.replaceWith(message);
|
||||
}
|
||||
|
||||
const storageKey = `md2html:toc:${window.location.pathname}${window.location.search}`;
|
||||
let open = false;
|
||||
function setOpen(value, persist = true) {
|
||||
open = value;
|
||||
document.body.classList.toggle('md2html-toc-open', open);
|
||||
toggle.setAttribute('aria-expanded', String(open));
|
||||
toggle.setAttribute('aria-label', open ? 'Zatvoriť obsah dokumentu' : 'Zobraziť obsah dokumentu');
|
||||
panel.setAttribute('aria-hidden', String(!open));
|
||||
panel.inert = !open;
|
||||
if (!open && panel.contains(document.activeElement)) toggle.focus();
|
||||
if (persist) {
|
||||
try { window.localStorage.setItem(storageKey, String(open)); } catch { /* Storage is optional. */ }
|
||||
}
|
||||
}
|
||||
|
||||
let savedOpen = false;
|
||||
try { savedOpen = window.localStorage.getItem(storageKey) === 'true'; } catch { /* Default to closed. */ }
|
||||
setOpen(savedOpen, false);
|
||||
panel.hidden = false;
|
||||
toggle.hidden = false;
|
||||
backdrop.hidden = false;
|
||||
document.body.classList.add('md2html-toc-ready');
|
||||
toggle.addEventListener('click', () => setOpen(!open));
|
||||
close.addEventListener('click', () => { setOpen(false); toggle.focus(); });
|
||||
backdrop.addEventListener('click', () => setOpen(false));
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'Escape' && open) {
|
||||
setOpen(false);
|
||||
toggle.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// The browser may have tried to resolve the fragment before generated IDs existed.
|
||||
if (window.location.hash) {
|
||||
try {
|
||||
const target = document.getElementById(decodeURIComponent(window.location.hash.slice(1)));
|
||||
if (target && content.contains(target)) target.scrollIntoView();
|
||||
} catch { /* Ignore malformed URL fragments. */ }
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user