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:
@@ -3,25 +3,35 @@
|
|||||||
## Purpose
|
## Purpose
|
||||||
This project is a small Markdown-to-HTML utility used behind NGINX.
|
This project is a small Markdown-to-HTML utility used behind NGINX.
|
||||||
When a `.md` file is requested, `src/md2html.php` loads the Markdown, converts it with Parsedown, and renders HTML styled by `src/markdown.css`.
|
When a `.md` file is requested, `src/md2html.php` loads the Markdown, converts it with Parsedown, and renders HTML styled by `src/markdown.css`.
|
||||||
|
`src/toc.js` builds an interactive table of contents from the rendered headings.
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
- `src/md2html.php`: entrypoint for conversion and HTML output.
|
- `src/md2html.php`: entrypoint for conversion and HTML output.
|
||||||
- `src/markdown.css`: visual styling for rendered Markdown (`.markdown-body`).
|
- `src/markdown.css`: visual styling for rendered Markdown (`.markdown-body`).
|
||||||
|
- `src/toc.js`: vanilla JavaScript for the table of contents and per-document panel state.
|
||||||
- `composer.json`: PHP dependency config (`erusev/parsedown`).
|
- `composer.json`: PHP dependency config (`erusev/parsedown`).
|
||||||
|
- `tests/fixtures/toc.md`: Markdown regression fixture.
|
||||||
|
- `tests/render.php`: checks PHP output and exports heading data for JS tests.
|
||||||
|
- `tests/toc.test.cjs`: Node.js tests using DOM doubles, without a browser.
|
||||||
|
- `tests/run.ps1`: PowerShell runner for PHP and JS integration checks.
|
||||||
|
|
||||||
## Environment Assumptions
|
## Environment Assumptions
|
||||||
- Project root: `D:\www\md2html`
|
- Project root: `D:\www\md2html`
|
||||||
- Markdown files are resolved from `D:\www\...`
|
- Markdown files are resolved from `D:\www\...`
|
||||||
- CSS is currently linked as `//localhost/md2html/src/markdown.css`
|
- CSS and JS are linked under `//localhost/md2html/src/` with `?h=<MD5 of file contents>`.
|
||||||
|
- NGINX forwards Markdown requests to `$document_root/md2html/src/md2html.php`; see README.md.
|
||||||
|
|
||||||
## Change Priorities
|
## Change Priorities
|
||||||
1. Keep output readable on desktop and mobile.
|
1. Keep output readable on desktop and mobile.
|
||||||
2. Preserve Markdown semantics generated by Parsedown.
|
2. Preserve Markdown semantics generated by Parsedown.
|
||||||
3. Avoid introducing JS unless explicitly required.
|
3. Keep JavaScript in `src/toc.js`, use vanilla JavaScript, and avoid unnecessary dependencies.
|
||||||
4. Keep the utility simple and fast.
|
4. Keep the utility simple and fast.
|
||||||
|
|
||||||
## Styling Rules (`src/markdown.css`)
|
## Styling Rules (`src/markdown.css`)
|
||||||
- Style only inside `.markdown-body` unless there is a strong reason to change global page behavior.
|
- Style only inside `.markdown-body` unless there is a strong reason to change global page behavior.
|
||||||
|
- Navigation is outside `.markdown-body`; scope its styles to `.md2html-toc*` and `.md2html-document` where possible.
|
||||||
|
- At widths above 760px, the open panel must leave space for the document; at smaller widths, use a dismissible overlay.
|
||||||
|
- Keep the toggle fixed and available, the panel independently scrollable, and reduced-motion preferences respected.
|
||||||
- Must cover: headings, links, lists, blockquotes, inline code, code blocks, tables, images, hr.
|
- Must cover: headings, links, lists, blockquotes, inline code, code blocks, tables, images, hr.
|
||||||
- Keep strong contrast and accessible font sizes.
|
- Keep strong contrast and accessible font sizes.
|
||||||
- Ensure long code lines and wide tables remain usable on small screens (`overflow-x`).
|
- Ensure long code lines and wide tables remain usable on small screens (`overflow-x`).
|
||||||
@@ -29,20 +39,32 @@ When a `.md` file is requested, `src/md2html.php` loads the Markdown, converts i
|
|||||||
|
|
||||||
## PHP Rules (`src/md2html.php`)
|
## PHP Rules (`src/md2html.php`)
|
||||||
- Keep dependency loading via the project root's `vendor/autoload.php`, resolved relative to the entrypoint directory.
|
- Keep dependency loading via the project root's `vendor/autoload.php`, resolved relative to the entrypoint directory.
|
||||||
- Keep response minimal: `<!doctype html>`, UTF-8 meta, stylesheet link, wrapped content.
|
- Keep response minimal: `<!doctype html>`, UTF-8 and viewport meta, stylesheet link, deferred script, navigation controls, and wrapped content.
|
||||||
|
- Keep CSS and JS URLs versioned using `md5_file()` on paths relative to `__DIR__`.
|
||||||
|
- Do not assign anonymous functions or closures to PHP variables; use named functions or class methods.
|
||||||
|
- Output helpers should return rendered content for callers to consume.
|
||||||
- Do not remove `.md` extension guard.
|
- Do not remove `.md` extension guard.
|
||||||
- Any path-handling changes must remain compatible with Windows paths.
|
- Any path-handling changes must remain compatible with Windows paths.
|
||||||
|
|
||||||
## Recommended Validation After Edits
|
## Table of Contents Rules (`src/toc.js`)
|
||||||
1. Open at least one Markdown file with:
|
- Build navigation from rendered H1–H3 in `.markdown-body`, preserving order and indicating levels with indentation. Do not add a Markdown parser.
|
||||||
- headings (`#`, `##`, `###`)
|
- Keep existing unique IDs, generate collision-free IDs for other headings, and retain native anchor navigation.
|
||||||
- lists
|
- Use DOM creation and `textContent` for heading labels; do not insert untrusted content through `innerHTML`.
|
||||||
- fenced code block
|
- Default to closed. Store panel state under `md2html:toc:` + pathname + query string, excluding the fragment.
|
||||||
- table
|
- Handle unavailable localStorage without disabling navigation.
|
||||||
- blockquote
|
- Maintain ARIA state, keyboard access, Escape dismissal, mobile close controls, and readable output without JavaScript.
|
||||||
- image
|
|
||||||
2. Verify layout on desktop and mobile width.
|
## Validation After Edits
|
||||||
3. Verify links and code blocks remain readable.
|
- Never use a browser or browser automation. Leave visual browser verification to Igor; use non-browser checks instead.
|
||||||
|
- Run relevant checks from the project root:
|
||||||
|
- `php -l src/md2html.php`
|
||||||
|
- `php -l tests/render.php` when changing the PHP test helper.
|
||||||
|
- `node --check src/toc.js`
|
||||||
|
- `./tests/run.ps1`
|
||||||
|
- `git diff --check`
|
||||||
|
- Tests require PHP with DOM, Composer dependencies, and Node.js supporting `node:test` and `--test-isolation=none`.
|
||||||
|
- Keep regression coverage for heading levels, formatting, code fences, duplicate and existing anchors, Unicode, empty and long contents, URL-specific state, blocked storage, mobile dismissal, asset hashes, and the `.md` guard.
|
||||||
|
- DOM doubles do not verify actual layout or scrolling. Igor manually checks desktop/mobile layout, long-panel scrolling, anchor navigation, and readability of Markdown elements.
|
||||||
|
|
||||||
## Non-Goals
|
## Non-Goals
|
||||||
- Building a full CMS.
|
- Building a full CMS.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Zdrojove subory su v adresari `src/`:
|
|||||||
|
|
||||||
- `src/md2html.php`: vstupny bod konverzie Markdown do HTML.
|
- `src/md2html.php`: vstupny bod konverzie Markdown do HTML.
|
||||||
- `src/markdown.css`: styly vystupneho HTML.
|
- `src/markdown.css`: styly vystupneho HTML.
|
||||||
|
- `src/toc.js`: interaktivny obsah dokumentu bez externych JS zavislosti.
|
||||||
|
|
||||||
Zavislosti sa instaluju cez `composer install` v koreni projektu do `vendor/`.
|
Zavislosti sa instaluju cez `composer install` v koreni projektu do `vendor/`.
|
||||||
Konfiguracia NGINX musi smerovat Markdown poziadavky na `src/md2html.php`.
|
Konfiguracia NGINX musi smerovat Markdown poziadavky na `src/md2html.php`.
|
||||||
@@ -29,3 +30,31 @@ Konfiguracia predpoklada projekt v adresari `md2html` pod `root` daneho servera
|
|||||||
Subor `fastcgi_params` musi byt dostupny v konfiguracii NGINX.
|
Subor `fastcgi_params` musi byt dostupny v konfiguracii NGINX.
|
||||||
|
|
||||||
Po uprave over konfiguraciu prikazom `nginx -t` a nacitaj ju prikazom `nginx -s reload`.
|
Po uprave over konfiguraciu prikazom `nginx -t` a nacitaj ju prikazom `nginx -s reload`.
|
||||||
|
|
||||||
|
## Obsah dokumentu
|
||||||
|
|
||||||
|
Tlacidlo v lavom hornom rohu otvara obsah vytvoreny z nadpisov H1 az H3.
|
||||||
|
Na desktope panel uvolni miesto vedla dokumentu, na mobile sa zobrazi ako overlay.
|
||||||
|
Zatvorit ho mozno tlacidlom, klavesom Escape alebo na mobile kliknutim mimo panelu.
|
||||||
|
Panel ma vlastne scrollovanie a odkazy smeruju na unikatne ID nadpisov.
|
||||||
|
Existujuce jednoznacne ID zostavaju zachovane.
|
||||||
|
|
||||||
|
Stav sa uklada do localStorage pod klucom `md2html:toc:` + pathname + query string,
|
||||||
|
bez URL fragmentu. Pri nedostupnom ulozisku ovladanie funguje dalej.
|
||||||
|
Bez JavaScriptu zostava dokument citatelny, navigacne ovladanie je skryte.
|
||||||
|
|
||||||
|
## Testovanie
|
||||||
|
|
||||||
|
Testy vyzaduju PHP s DOM rozsirenim, nainstalovane Composer zavislosti a Node.js
|
||||||
|
s podporou `node:test` a `--test-isolation=none`. Nepouzivaju prehliadac ani dalsie balicky.
|
||||||
|
V PowerShelli z korena projektu spusti:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
php -l src/md2html.php
|
||||||
|
node --check src/toc.js
|
||||||
|
./tests/run.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Testy overuju realny vystup Parsedownu a JS spravanie pomocou modelu DOM.
|
||||||
|
Vizualne rozlozenie, skutocne scrollovanie a ovladanie v prehliadaci treba overit
|
||||||
|
manualne na desktope aj mobile.
|
||||||
|
|||||||
+156
-1
@@ -9,6 +9,7 @@
|
|||||||
--quote-bg: #f4f8fa;
|
--quote-bg: #f4f8fa;
|
||||||
--code-bg: #1f2328;
|
--code-bg: #1f2328;
|
||||||
--code-text: #f4f6f8;
|
--code-text: #f4f6f8;
|
||||||
|
--toc-width: 19rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
@@ -201,10 +202,164 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body :target {
|
.markdown-body :target {
|
||||||
scroll-margin-top: 1rem;
|
scroll-margin-top: 4.5rem;
|
||||||
animation: mark-target 1.2s ease;
|
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 {
|
@keyframes mark-target {
|
||||||
0% {
|
0% {
|
||||||
background: #fff3c9;
|
background: #fff3c9;
|
||||||
|
|||||||
+11
-1
@@ -14,7 +14,17 @@ $parser = new Parsedown();
|
|||||||
|
|
||||||
echo '<!doctype html>';
|
echo '<!doctype html>';
|
||||||
echo '<meta charset="utf-8">';
|
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 '<div class="markdown-body">';
|
||||||
echo $parser->text($md);
|
echo $parser->text($md);
|
||||||
echo '</div>';
|
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