diff --git a/SwarselSystems.org b/SwarselSystems.org index 4383750..23924af 100644 --- a/SwarselSystems.org +++ b/SwarselSystems.org @@ -61,7 +61,12 @@ That is the reason why I keep this configuration as a literate one: so that I am For a beginner, I recommend to read this file like a book, from start to finish. I will try to explain concepts whenever they first come up, and will regularly link to [[#h:8ea35dcc-ef94-4c10-9112-8be8efd6f424][Appendix C: Explanations to nix functions and operators]] when more context is needed. For the first few times that I am using a new function, I will place such a link again. However, to keep the writing of this file manageable, I will generally only do this no more than three times. -This page offers some utilities to you: you can pin specific headings to the right "pinned" bar by hovering over the heading and clicking =[pin]=. If a section seems uninteresting to you, you can press the =↓= button to skip to the next one. And if you want to send a section to somebody else, you can click the =#= in order to copy its link to the clipboard. Your pinned headings will be saved locally, so you can continue reading in case you take a break. +This page offers some utilities to you: +- you can pin specific headings to the right "pinned" bar by hovering over the heading and clicking =[pin]= +- If a section seems uninteresting to you, you can press the =↓= button to skip to the next one. +- If a sections upcoming subsections seem uninteresting to you, you can press the =⇣= button to skip to the next heading of at least same or higher level. +- If you are currently in a level of subsections that all seem uninteresting to you, you can press the =⇑= button to skip to the next heading that is at least one level higher. +- And if you want to send a section to somebody else, you can click the =#= in order to copy its link to the clipboard. Your pinned headings will be saved locally, so you can continue reading in case you take a break. ** Structure of this file :PROPERTIES: @@ -183,7 +188,7 @@ I add a javascript bit to the file in order to have a darkmode toggle when expor I also add this javascript to add header pinning functionality to the site, using the same trick as above (this is defined in [[#h:e5f900a0-9d68-4269-a663-53c52434c342][HTML Export: Docs QoL]]): -#+begin_src elisp :noweb yes :exports both :results html +#+begin_src elisp :tangle no :noweb yes :exports both :results html " #+end_src -** HTML Export: Docs QoL +** HTML export javascript: Docs QoL :PROPERTIES: :CUSTOM_ID: h:e5f900a0-9d68-4269-a663-53c52434c342 :END: @@ -33448,502 +33748,562 @@ This adds the following functionalities to the [[#h:12880c64-229c-4063-9eea-387a - Section pinning with persistent pins - Copy section links to clipboard button - Searching in table of contents -- Skip to next section button +- Skip to next section buttons (one for simply next section, one for same level, one for at least higher level) #+begin_src javascript :noweb-ref js-docs-qol :exports code (function() { - function ready(fn) { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', fn); - } else { - fn(); - } - } - - ready(function initPinned() { - const STORAGE_KEY = 'org-pinned-items-v2'; - - let pinnedPanel = document.getElementById('pinned-panel'); - if (!pinnedPanel) { - pinnedPanel = document.createElement('aside'); - pinnedPanel.id = 'pinned-panel'; - pinnedPanel.innerHTML = ` -
-

Pinned

- -
- - - `; - document.body.appendChild(pinnedPanel); + function ready(fn) { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', fn); + } else { + fn(); + } } - let showBtn = document.getElementById('show-pinned-btn'); - if (!showBtn) { - showBtn = document.createElement('button'); - showBtn.id = 'show-pinned-btn'; - showBtn.type = 'button'; - showBtn.textContent = 'Pinned'; - document.body.appendChild(showBtn); - } + ready(function initPinned() { + const STORAGE_KEY = 'org-pinned-items-v2'; - const content = document.getElementById('content'); - const pinnedList = document.getElementById('pinned-list'); - const toggleBtn = document.getElementById('toggle-pinned-btn'); - const clearAllBtn = document.getElementById('clear-all-pins-btn'); - const toc = document.getElementById('table-of-contents'); - const body = document.body; + let pinnedPanel = document.getElementById('pinned-panel'); + if (!pinnedPanel) { + pinnedPanel = document.createElement('aside'); + pinnedPanel.id = 'pinned-panel'; + pinnedPanel.innerHTML = ` +
+

Pinned

+ +
+ + + `; + document.body.appendChild(pinnedPanel); + } - if (!content || !pinnedList || !toggleBtn || !clearAllBtn || !toc) return; + let showBtn = document.getElementById('show-pinned-btn'); + if (!showBtn) { + showBtn = document.createElement('button'); + showBtn.id = 'show-pinned-btn'; + showBtn.type = 'button'; + showBtn.textContent = 'Pinned'; + document.body.appendChild(showBtn); + } - function injectSearch() { - // Check if already injected - if (document.getElementById('toc-search-input')) return; + const content = document.getElementById('content'); + const pinnedList = document.getElementById('pinned-list'); + const toggleBtn = document.getElementById('toggle-pinned-btn'); + const clearAllBtn = document.getElementById('clear-all-pins-btn'); + const toc = document.getElementById('table-of-contents'); + const body = document.body; - const searchContainer = document.createElement('div'); - searchContainer.id = 'toc-search-container'; + if (!content || !pinnedList || !toggleBtn || !clearAllBtn || !toc) return; - const searchInput = document.createElement('input'); - searchInput.id = 'toc-search-input'; - searchInput.type = 'text'; - searchInput.placeholder = 'Search TOC...'; - searchInput.autocomplete = 'off'; + function injectSearch() { + if (document.getElementById('toc-search-input')) return; - const clearBtn = document.createElement('button'); - clearBtn.id = 'toc-search-clear'; - clearBtn.type = 'button'; - clearBtn.textContent = 'Clear'; + const searchContainer = document.createElement('div'); + searchContainer.id = 'toc-search-container'; - searchContainer.appendChild(searchInput); - searchContainer.appendChild(clearBtn); + const searchInput = document.createElement('input'); + searchInput.id = 'toc-search-input'; + searchInput.type = 'text'; + searchInput.placeholder = 'Search TOC...'; + searchInput.autocomplete = 'off'; - toc.insertBefore(searchContainer, toc.firstChild); + const clearBtn = document.createElement('button'); + clearBtn.id = 'toc-search-clear'; + clearBtn.type = 'button'; + clearBtn.textContent = 'Clear'; - function filterTOC(term) { - const allLinks = toc.querySelectorAll('a'); + searchContainer.appendChild(searchInput); + searchContainer.appendChild(clearBtn); - allLinks.forEach(link => { - const li = link.closest('li'); - if (!li) return; + toc.insertBefore(searchContainer, toc.firstChild); - const text = link.textContent.toLowerCase(); - const matches = text.includes(term); + function filterTOC(term) { + const allLinks = toc.querySelectorAll('a'); - if (matches) { - li.classList.remove('hidden-by-search'); - let parent = li.parentElement; - while (parent && parent !== toc) { - if (parent.tagName === 'UL') { - parent.style.display = ''; - } - if (parent.tagName === 'LI') { - parent.classList.remove('hidden-by-search'); - } - parent = parent.parentElement; + allLinks.forEach(link => { + const li = link.closest('li'); + if (!li) return; + + const text = link.textContent.toLowerCase(); + const matches = text.includes(term); + + if (matches) { + li.classList.remove('hidden-by-search'); + let parent = li.parentElement; + while (parent && parent !== toc) { + if (parent.tagName === 'UL') { + parent.style.display = ''; + } + if (parent.tagName === 'LI') { + parent.classList.remove('hidden-by-search'); + } + parent = parent.parentElement; + } + } else { + li.classList.add('hidden-by-search'); + } + }); + + if (term === '') { + const allLis = toc.querySelectorAll('li'); + allLis.forEach(li => li.classList.remove('hidden-by-search')); + } } - } else { - li.classList.add('hidden-by-search'); - } - }); - if (term === '') { - const allLis = toc.querySelectorAll('li'); - allLis.forEach(li => li.classList.remove('hidden-by-search')); + searchInput.addEventListener('input', function(e) { + const term = e.target.value.toLowerCase(); + filterTOC(term); + }); + + clearBtn.addEventListener('click', function() { + searchInput.value = ''; + filterTOC(''); + searchInput.focus(); + }); } - } + injectSearch(); - searchInput.addEventListener('input', function(e) { - const term = e.target.value.toLowerCase(); - filterTOC(term); - }); + function addHeadingLinks() { + const headers = content.querySelectorAll('h1, h2, h3, h4, h5, h6, h7, h8, h9'); + headers.forEach(header => { + const id = header.getAttribute('id'); + if (!id) return; - clearBtn.addEventListener('click', function() { - searchInput.value = ''; - filterTOC(''); - searchInput.focus(); - }); - } - injectSearch(); + if (header.querySelector('.heading-link')) return; - function addHeadingLinks() { - const headers = content.querySelectorAll('h1, h2, h3, h4, h5, h6, h7, h8, h9'); - headers.forEach(header => { - const id = header.getAttribute('id'); - if (!id) return; + const link = document.createElement('a'); + link.className = 'heading-link'; + link.href = '#' + id; + link.textContent = '#'; + link.title = 'Copy link to this heading'; - if (header.querySelector('.heading-link')) return; + const pinBtn = header.querySelector('.toc-pin-btn'); + if (pinBtn) { + header.insertBefore(link, pinBtn); + } else { + header.appendChild(link); + } - const link = document.createElement('a'); - link.className = 'heading-link'; - link.href = '#' + id; - link.textContent = '#'; - link.title = 'Copy link to this heading'; + link.addEventListener('click', function(e) { + e.preventDefault(); + const url = window.location.origin + window.location.pathname + '#' + id; - const pinBtn = header.querySelector('.toc-pin-btn'); - if (pinBtn) { - header.insertBefore(link, pinBtn); - } else { - header.appendChild(link); + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(url) + .then(() => { + const originalText = link.textContent; + link.textContent = '✓'; + setTimeout(() => { + link.textContent = originalText; + }, 1000); + }) + .catch(err => { + console.warn('Failed to copy to clipboard', err); + window.location.hash = id; + }); + } else { + window.location.hash = id; + } + }); + }); } + addHeadingLinks(); - link.addEventListener('click', function(e) { - e.preventDefault(); - const url = window.location.origin + window.location.pathname + '#' + id; + function addNextHeadingButtons() { + const headers = Array.from(content.querySelectorAll('h1, h2, h3, h4, h5, h6, h7, h8, h9')); - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(url) - .then(() => { - const originalText = link.textContent; - link.textContent = '✓'; - setTimeout(() => { - link.textContent = originalText; - }, 1000); - }) - .catch(err => { - console.warn('Failed to copy to clipboard', err); - window.location.hash = id; - }); - } else { - window.location.hash = id; - } - }); - }); - } - addHeadingLinks(); - - function addNextHeadingButtons() { - const headers = Array.from(content.querySelectorAll('h1, h2, h3, h4, h5, h6, h7, h8, h9')); - - headers.forEach((header, index) => { - // Skip if button already exists - if (header.querySelector('.heading-next')) return; - - // Find next heading - const nextHeader = headers[index + 1]; - if (!nextHeader) return; // No next heading - - const nextId = nextHeader.getAttribute('id'); - if (!nextId) return; - - const nextBtn = document.createElement('button'); - nextBtn.className = 'heading-next'; - nextBtn.type = 'button'; - nextBtn.textContent = '↓'; - nextBtn.title = 'Jump to next heading'; - - // Insert after the heading link, before the pin button - const headingLink = header.querySelector('.heading-link'); - const pinBtn = header.querySelector('.toc-pin-btn'); - - if (pinBtn) { - header.insertBefore(nextBtn, pinBtn); - } else if (headingLink) { - headingLink.after(nextBtn); - } else { - header.appendChild(nextBtn); - } - - nextBtn.addEventListener('click', function(e) { - e.preventDefault(); - nextHeader.scrollIntoView({ - behavior: 'smooth', - block: 'start' - }); - // Update URL hash - history.pushState(null, null, '#' + nextId); - }); - }); - } - addNextHeadingButtons(); - - let mobileTocBtn = document.getElementById('mobile-toc-toggle'); - if (!mobileTocBtn) { - mobileTocBtn = document.createElement('button'); - mobileTocBtn.id = 'mobile-toc-toggle'; - mobileTocBtn.type = 'button'; - mobileTocBtn.textContent = 'TOC'; - document.body.appendChild(mobileTocBtn); - } - - let mobilePinnedBtn = document.getElementById('mobile-pinned-toggle'); - if (!mobilePinnedBtn) { - mobilePinnedBtn = document.createElement('button'); - mobilePinnedBtn.id = 'mobile-pinned-toggle'; - mobilePinnedBtn.type = 'button'; - mobilePinnedBtn.textContent = 'Pinned'; - document.body.appendChild(mobilePinnedBtn); - } - - function anyMobilePanelOpen() { - return toc.classList.contains('mobile-visible') || - pinnedPanel.classList.contains('mobile-visible'); - } - - function updateBodyMobilePanelState() { - if (anyMobilePanelOpen()) body.classList.add('mobile-panel-open'); - else body.classList.remove('mobile-panel-open'); - } - - document.addEventListener('click', function(e) { - if (window.innerWidth > 1000) return; - - if (!anyMobilePanelOpen()) return; - - const clickedInsideToc = toc.contains(e.target); - const clickedInsidePinned = pinnedPanel.contains(e.target); - const clickedTocBtn = mobileTocBtn.contains(e.target); - const clickedPinnedBtn = mobilePinnedBtn.contains(e.target); - - const clickedInteractive = - e.target.tagName === 'A' || - e.target.tagName === 'BUTTON' || - e.target.tagName === 'INPUT' || - e.target.tagName === 'TEXTAREA' || - e.target.closest('a') || - e.target.closest('button'); - - if (!clickedInsideToc && !clickedInsidePinned && !clickedTocBtn && !clickedPinnedBtn && !clickedInteractive) { - toc.classList.remove('mobile-visible'); - pinnedPanel.classList.remove('mobile-visible'); - updateBodyMobilePanelState(); - } - }); - - mobileTocBtn.addEventListener('click', function() { - const isOpen = toc.classList.toggle('mobile-visible'); - if (isOpen) { - pinnedPanel.classList.remove('mobile-visible'); - } - updateBodyMobilePanelState(); - }); - - mobilePinnedBtn.addEventListener('click', function() { - const isOpen = pinnedPanel.classList.toggle('mobile-visible'); - if (isOpen) { - toc.classList.remove('mobile-visible'); - } - updateBodyMobilePanelState(); - }); - - const pinnedItems = new Map(); - let initiallyPinnedHrefs = new Set(); - - function loadFromStorage() { - try { - const raw = window.localStorage && localStorage.getItem(STORAGE_KEY); - if (!raw) return; - const arr = JSON.parse(raw); - if (!Array.isArray(arr)) return; - initiallyPinnedHrefs = new Set(arr); - } catch (e) { - console.warn('Pinned: failed to load from localStorage', e); - } - } - - function saveToStorage() { - try { - if (!window.localStorage) return; - const arr = []; - pinnedItems.forEach((entry, href) => { - if (entry.li) arr.push(href); - }); - localStorage.setItem(STORAGE_KEY, JSON.stringify(arr)); - } catch (e) { - console.warn('Pinned: failed to save to localStorage', e); - } - } - - function sortPinnedList() { - const items = Array.from(pinnedList.children) - .map(li => { - const link = li.querySelector('a'); - return { - li: li, - text: link ? link.textContent.trim() - .toLowerCase() : '' - }; - }); - items.sort((a, b) => a.text.localeCompare(b.text)); - items.forEach(item => pinnedList.appendChild(item.li)); - } - - function hidePinnedPanel() { - pinnedPanel.classList.add('hidden'); - content.classList.add('pinned-hidden'); - showBtn.classList.add('visible'); - } - - function showPinnedPanel() { - pinnedPanel.classList.remove('hidden'); - content.classList.remove('pinned-hidden'); - showBtn.classList.remove('visible'); - } - - toggleBtn.addEventListener('click', hidePinnedPanel); - showBtn.addEventListener('click', showPinnedPanel); - - clearAllBtn.addEventListener('click', function() { - if (pinnedItems.size === 0) return; - - const confirmed = confirm('Are you sure you want to clear all pinned items?'); - if (!confirmed) return; - - pinnedItems.forEach((entry, href) => { - if (entry.li && entry.li.parentElement) { - entry.li.parentElement.removeChild(entry.li); - } - entry.li = null; - entry.btns.forEach(b => b.textContent = '[pin]'); - }); - - saveToStorage(); - }); - - function attachPinBehavior(pinBtn, href, text) { - if (!href) return; - - if (!pinnedItems.has(href)) { - pinnedItems.set(href, { - li: null, - btns: new Set(), - text: text - }); - } - const entry = pinnedItems.get(href); - entry.btns.add(pinBtn); - - pinBtn.textContent = entry.li ? '[unpin]' : '[pin]'; - - pinBtn.addEventListener('click', function(e) { - e.preventDefault(); - e.stopPropagation(); - const current = pinnedItems.get(href); - if (!current) return; - - if (current.li) { - if (current.li.parentElement) { - current.li.parentElement.removeChild(current.li); - } - current.li = null; - current.btns.forEach(b => b.textContent = '[pin]'); - saveToStorage(); - } else { - const li = document.createElement('li'); - - const a = document.createElement('a'); - a.href = href; - a.textContent = current.text; - - const removeBtn = document.createElement('button'); - removeBtn.className = 'pin-remove'; - removeBtn.type = 'button'; - removeBtn.textContent = '✕'; - removeBtn.addEventListener('click', () => { - const cur = pinnedItems.get(href); - if (!cur) return; - if (cur.li && cur.li.parentElement) { - cur.li.parentElement.removeChild(cur.li); + function getLevel(header) { + return parseInt(header.tagName.substring(1), 10); } - cur.li = null; - cur.btns.forEach(b => b.textContent = '[pin]'); + + headers.forEach((header, index) => { + const currentLevel = getLevel(header); + + const headingLink = header.querySelector('.heading-link'); + const pinBtn = header.querySelector('.toc-pin-btn'); + + function insertActionBtn(btn) { + if (pinBtn) { + header.insertBefore(btn, pinBtn); + return; + } + if (headingLink && headingLink.parentNode === header) { + headingLink.after(btn); + return; + } + header.appendChild(btn); + } + + const nextHeader = headers[index + 1]; + if (nextHeader && !header.querySelector('.heading-next')) { + const nextId = nextHeader.getAttribute('id'); + if (nextId) { + const nextBtn = document.createElement('button'); + nextBtn.className = 'heading-next'; + nextBtn.type = 'button'; + nextBtn.textContent = '↓'; + nextBtn.title = 'Jump to next heading'; + + insertActionBtn(nextBtn); + + nextBtn.addEventListener('click', function(e) { + e.preventDefault(); + nextHeader.scrollIntoView({ behavior: 'smooth', block: 'start' }); + history.pushState(null, null, '#' + nextId); + }); + } + } + + if (!header.querySelector('.heading-skip')) { + let skipHeader = null; + for (let i = index + 1; i < headers.length; i++) { + if (getLevel(headers[i]) <= currentLevel) { + skipHeader = headers[i]; + break; + } + } + + if (skipHeader) { + const skipId = skipHeader.getAttribute('id'); + if (skipId) { + const skipBtn = document.createElement('button'); + skipBtn.className = 'heading-skip'; + skipBtn.type = 'button'; + skipBtn.textContent = '⇣'; + skipBtn.title = 'Skip to next section (same level)'; + + insertActionBtn(skipBtn); + + skipBtn.addEventListener('click', function(e) { + e.preventDefault(); + skipHeader.scrollIntoView({ behavior: 'smooth', block: 'start' }); + history.pushState(null, null, '#' + skipId); + }); + } + } + } + + if (!header.querySelector('.heading-skip-up')) { + let upHeader = null; + for (let i = index + 1; i < headers.length; i++) { + if (getLevel(headers[i]) < currentLevel) { + upHeader = headers[i]; + break; + } + } + + if (upHeader) { + const upId = upHeader.getAttribute('id'); + if (upId) { + const upBtn = document.createElement('button'); + upBtn.className = 'heading-skip-up'; + upBtn.type = 'button'; + upBtn.textContent = '⇑'; + upBtn.title = 'Jump to next higher-level section (escape subsections)'; + + insertActionBtn(upBtn); + + upBtn.addEventListener('click', function(e) { + e.preventDefault(); + upHeader.scrollIntoView({ behavior: 'smooth', block: 'start' }); + history.pushState(null, null, '#' + upId); + }); + } + } + } + }); + } + addNextHeadingButtons(); + + let mobileTocBtn = document.getElementById('mobile-toc-toggle'); + if (!mobileTocBtn) { + mobileTocBtn = document.createElement('button'); + mobileTocBtn.id = 'mobile-toc-toggle'; + mobileTocBtn.type = 'button'; + mobileTocBtn.textContent = 'TOC'; + document.body.appendChild(mobileTocBtn); + } + + let mobilePinnedBtn = document.getElementById('mobile-pinned-toggle'); + if (!mobilePinnedBtn) { + mobilePinnedBtn = document.createElement('button'); + mobilePinnedBtn.id = 'mobile-pinned-toggle'; + mobilePinnedBtn.type = 'button'; + mobilePinnedBtn.textContent = 'Pinned'; + document.body.appendChild(mobilePinnedBtn); + } + + function anyMobilePanelOpen() { + return toc.classList.contains('mobile-visible') || + pinnedPanel.classList.contains('mobile-visible'); + } + + function updateBodyMobilePanelState() { + if (anyMobilePanelOpen()) body.classList.add('mobile-panel-open'); + else body.classList.remove('mobile-panel-open'); + } + + document.addEventListener('click', function(e) { + if (window.innerWidth > 1000) return; + + if (!anyMobilePanelOpen()) return; + + const clickedInsideToc = toc.contains(e.target); + const clickedInsidePinned = pinnedPanel.contains(e.target); + const clickedTocBtn = mobileTocBtn.contains(e.target); + const clickedPinnedBtn = mobilePinnedBtn.contains(e.target); + + const clickedInteractive = + e.target.tagName === 'A' || + e.target.tagName === 'BUTTON' || + e.target.tagName === 'INPUT' || + e.target.tagName === 'TEXTAREA' || + e.target.closest('a') || + e.target.closest('button'); + + if (!clickedInsideToc && !clickedInsidePinned && !clickedTocBtn && !clickedPinnedBtn && !clickedInteractive) { + toc.classList.remove('mobile-visible'); + pinnedPanel.classList.remove('mobile-visible'); + updateBodyMobilePanelState(); + } + }); + + mobileTocBtn.addEventListener('click', function() { + const isOpen = toc.classList.toggle('mobile-visible'); + if (isOpen) { + pinnedPanel.classList.remove('mobile-visible'); + } + updateBodyMobilePanelState(); + }); + + mobilePinnedBtn.addEventListener('click', function() { + const isOpen = pinnedPanel.classList.toggle('mobile-visible'); + if (isOpen) { + toc.classList.remove('mobile-visible'); + } + updateBodyMobilePanelState(); + }); + + const pinnedItems = new Map(); + let initiallyPinnedHrefs = new Set(); + + function loadFromStorage() { + try { + const raw = window.localStorage && localStorage.getItem(STORAGE_KEY); + if (!raw) return; + const arr = JSON.parse(raw); + if (!Array.isArray(arr)) return; + initiallyPinnedHrefs = new Set(arr); + } catch (e) { + console.warn('Pinned: failed to load from localStorage', e); + } + } + + function saveToStorage() { + try { + if (!window.localStorage) return; + const arr = []; + pinnedItems.forEach((entry, href) => { + if (entry.li) arr.push(href); + }); + localStorage.setItem(STORAGE_KEY, JSON.stringify(arr)); + } catch (e) { + console.warn('Pinned: failed to save to localStorage', e); + } + } + + function sortPinnedList() { + const items = Array.from(pinnedList.children) + .map(li => { + const link = li.querySelector('a'); + return { + li: li, + text: link ? link.textContent.trim() + .toLowerCase() : '' + }; + }); + items.sort((a, b) => a.text.localeCompare(b.text)); + items.forEach(item => pinnedList.appendChild(item.li)); + } + + function hidePinnedPanel() { + pinnedPanel.classList.add('hidden'); + content.classList.add('pinned-hidden'); + showBtn.classList.add('visible'); + } + + function showPinnedPanel() { + pinnedPanel.classList.remove('hidden'); + content.classList.remove('pinned-hidden'); + showBtn.classList.remove('visible'); + } + + toggleBtn.addEventListener('click', hidePinnedPanel); + showBtn.addEventListener('click', showPinnedPanel); + + clearAllBtn.addEventListener('click', function() { + if (pinnedItems.size === 0) return; + + const confirmed = confirm('Are you sure you want to clear all pinned items?'); + if (!confirmed) return; + + pinnedItems.forEach((entry, href) => { + if (entry.li && entry.li.parentElement) { + entry.li.parentElement.removeChild(entry.li); + } + entry.li = null; + entry.btns.forEach(b => b.textContent = '[pin]'); + }); + saveToStorage(); - }); + }); - li.appendChild(a); - li.appendChild(removeBtn); - pinnedList.appendChild(li); + function attachPinBehavior(pinBtn, href, text) { + if (!href) return; - current.li = li; - current.btns.forEach(b => b.textContent = '[unpin]'); + if (!pinnedItems.has(href)) { + pinnedItems.set(href, { + li: null, + btns: new Set(), + text: text + }); + } + const entry = pinnedItems.get(href); + entry.btns.add(pinBtn); - sortPinnedList(); - saveToStorage(); + pinBtn.textContent = entry.li ? '[unpin]' : '[pin]'; + + pinBtn.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + const current = pinnedItems.get(href); + if (!current) return; + + if (current.li) { + if (current.li.parentElement) { + current.li.parentElement.removeChild(current.li); + } + current.li = null; + current.btns.forEach(b => b.textContent = '[pin]'); + saveToStorage(); + } else { + const li = document.createElement('li'); + + const a = document.createElement('a'); + a.href = href; + a.textContent = current.text; + + const removeBtn = document.createElement('button'); + removeBtn.className = 'pin-remove'; + removeBtn.type = 'button'; + removeBtn.textContent = '✕'; + removeBtn.addEventListener('click', () => { + const cur = pinnedItems.get(href); + if (!cur) return; + if (cur.li && cur.li.parentElement) { + cur.li.parentElement.removeChild(cur.li); + } + cur.li = null; + cur.btns.forEach(b => b.textContent = '[pin]'); + saveToStorage(); + }); + + li.appendChild(a); + li.appendChild(removeBtn); + pinnedList.appendChild(li); + + current.li = li; + current.btns.forEach(b => b.textContent = '[unpin]'); + + sortPinnedList(); + saveToStorage(); + } + }); } - }); - } - loadFromStorage(); + loadFromStorage(); - const tocLinks = document.querySelectorAll('#text-table-of-contents a'); - tocLinks.forEach(link => { - if (link.parentElement && link.parentElement.classList.contains('toc-entry')) { - return; - } - const li = link.closest('li'); - if (!li) return; + const tocLinks = document.querySelectorAll('#text-table-of-contents a'); + tocLinks.forEach(link => { + if (link.parentElement && link.parentElement.classList.contains('toc-entry')) { + return; + } + const li = link.closest('li'); + if (!li) return; - const wrapper = document.createElement('span'); - wrapper.className = 'toc-entry'; - li.insertBefore(wrapper, link); - wrapper.appendChild(link); + const wrapper = document.createElement('span'); + wrapper.className = 'toc-entry'; + li.insertBefore(wrapper, link); + wrapper.appendChild(link); - const pinBtn = document.createElement('button'); - pinBtn.className = 'toc-pin-btn'; - pinBtn.type = 'button'; - pinBtn.textContent = '[pin]'; - wrapper.appendChild(pinBtn); + const pinBtn = document.createElement('button'); + pinBtn.className = 'toc-pin-btn'; + pinBtn.type = 'button'; + pinBtn.textContent = '[pin]'; + wrapper.appendChild(pinBtn); - const href = link.getAttribute('href'); - const text = link.textContent.trim(); - attachPinBehavior(pinBtn, href, text); - }); + const href = link.getAttribute('href'); + const text = link.textContent.trim(); + attachPinBehavior(pinBtn, href, text); + }); - const headers = content.querySelectorAll('h2, h3, h4, h5, h6, h7, h8, h9'); - headers.forEach(header => { - const id = header.getAttribute('id'); - if (!id) return; - if (header.querySelector('.toc-pin-btn')) return; + const headers = content.querySelectorAll('h2, h3, h4, h5, h6, h7, h8, h9'); + headers.forEach(header => { + const id = header.getAttribute('id'); + if (!id) return; + if (header.querySelector('.toc-pin-btn')) return; - const href = '#' + id; - const text = header.textContent.trim(); + const href = '#' + id; + const text = header.textContent.trim(); - const pinBtn = document.createElement('button'); - pinBtn.className = 'toc-pin-btn'; - pinBtn.type = 'button'; - pinBtn.textContent = '[pin]'; - pinBtn.style.marginLeft = '0.8rem'; - pinBtn.style.fontSize = '0.75em'; + const pinBtn = document.createElement('button'); + pinBtn.className = 'toc-pin-btn'; + pinBtn.type = 'button'; + pinBtn.textContent = '[pin]'; + pinBtn.style.marginLeft = '0.8rem'; + pinBtn.style.fontSize = '0.75em'; - header.appendChild(pinBtn); - attachPinBehavior(pinBtn, href, text); - }); + header.appendChild(pinBtn); + attachPinBehavior(pinBtn, href, text); + }); - initiallyPinnedHrefs.forEach(href => { - const entry = pinnedItems.get(href); - if (!entry) return; + initiallyPinnedHrefs.forEach(href => { + const entry = pinnedItems.get(href); + if (!entry) return; - const li = document.createElement('li'); + const li = document.createElement('li'); - const a = document.createElement('a'); - a.href = href; - a.textContent = entry.text; + const a = document.createElement('a'); + a.href = href; + a.textContent = entry.text; - const removeBtn = document.createElement('button'); - removeBtn.className = 'pin-remove'; - removeBtn.type = 'button'; - removeBtn.textContent = '✕'; - removeBtn.addEventListener('click', () => { - const cur = pinnedItems.get(href); - if (!cur) return; - if (cur.li && cur.li.parentElement) { - cur.li.parentElement.removeChild(cur.li); - } - cur.li = null; - cur.btns.forEach(b => b.textContent = '[pin]'); + const removeBtn = document.createElement('button'); + removeBtn.className = 'pin-remove'; + removeBtn.type = 'button'; + removeBtn.textContent = '✕'; + removeBtn.addEventListener('click', () => { + const cur = pinnedItems.get(href); + if (!cur) return; + if (cur.li && cur.li.parentElement) { + cur.li.parentElement.removeChild(cur.li); + } + cur.li = null; + cur.btns.forEach(b => b.textContent = '[pin]'); + saveToStorage(); + }); + + li.appendChild(a); + li.appendChild(removeBtn); + pinnedList.appendChild(li); + + entry.li = li; + entry.btns.forEach(b => b.textContent = '[unpin]'); + }); + + sortPinnedList(); saveToStorage(); - }); - - li.appendChild(a); - li.appendChild(removeBtn); - pinnedList.appendChild(li); - - entry.li = li; - entry.btns.forEach(b => b.textContent = '[unpin]'); }); - - sortPinnedList(); - saveToStorage(); - }); })(); #+end_src @@ -35050,6 +35410,72 @@ This is the stylesheet used by the [[#h:12880c64-229c-4063-9eea-387a97490676][HT visibility: visible; } + .heading-skip { + opacity: 0; + visibility: hidden; + transition: opacity 0.2s, visibility 0.2s; + margin-left: 0.5rem; + color: #718ca1; + text-decoration: none; + font-size: 0.8em; + vertical-align: middle; + cursor: pointer; + background: none; + border: none; + padding: 0; + } + + .heading-skip:hover { + color: #5ec4ff; + text-decoration: none; + } + + h1:hover .heading-skip, + h2:hover .heading-skip, + h3:hover .heading-skip, + h4:hover .heading-skip, + h5:hover .heading-skip, + h6:hover .heading-skip, + h7:hover .heading-skip, + h8:hover .heading-skip, + h9:hover .heading-skip { + opacity: 1; + visibility: visible; + } + + .heading-skip-up { + opacity: 0; + visibility: hidden; + transition: opacity 0.2s, visibility 0.2s; + margin-left: 0.5rem; + color: #718ca1; + text-decoration: none; + font-size: 0.8em; + vertical-align: middle; + cursor: pointer; + background: none; + border: none; + padding: 0; + } + + .heading-skip-up:hover { + color: #5ec4ff; + text-decoration: none; + } + + h1:hover .heading-skip-up, + h2:hover .heading-skip-up, + h3:hover .heading-skip-up, + h4:hover .heading-skip-up, + h5:hover .heading-skip-up, + h6:hover .heading-skip-up, + h7:hover .heading-skip-up, + h8:hover .heading-skip-up, + h9:hover .heading-skip-up { + opacity: 1; + visibility: visible; + } + @media (max-width: 1600px) { #content { max-width: 100%;