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 = `
-
- Clear All
-
- `;
- 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 = `
+
+ Clear All
+
+ `;
+ 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%;