Greasy Fork is available in English.
Collapse/expand sections; auto-expands the section targeted by URL hash on SPA navigation
// ==UserScript==
// @name Grokipedia Collapsible Sections
// @namespace https://grokipedia.com/
// @version 2.3.0
// @description Collapse/expand sections; auto-expands the section targeted by URL hash on SPA navigation
// @author You
// @match https://grokipedia.com/*
// @match https://www.grokipedia.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
/* ─── Ref-anchor guard ───────────────────────────────────────── */
function isRefPage() {
return /#ref-/.test(window.location.hash);
}
if (isRefPage()) return;
/* ─── Styles ─────────────────────────────────────────────────── */
const style = document.createElement('style');
style.textContent = `
.gw-section-body {
overflow: hidden;
transition: max-height 0.32s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.28s ease;
max-height: 0;
opacity: 0;
}
.gw-section-body.gw-open {
max-height: 9999px;
opacity: 1;
}
.gw-section-header {
cursor: pointer;
user-select: none;
display: flex !important;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.gw-section-header:hover { opacity: 0.8; }
.gw-chevron {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.4em;
height: 1.4em;
border-radius: 50%;
background: rgba(128,128,128,0.12);
transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1),
background 0.15s ease;
font-size: 0.72em;
color: currentColor;
}
.gw-section-header:hover .gw-chevron { background: rgba(128,128,128,0.22); }
.gw-chevron.gw-open { transform: rotate(180deg); }
.gw-section-header:focus-visible {
outline: 2px solid currentColor;
outline-offset: 3px;
border-radius: 2px;
}
`;
document.head.appendChild(style);
/* ─── Core helpers ────────────────────────────────────────────── */
function isSectionHeader(el) {
return (
el.classList.contains('overflow-hidden') &&
el.classList.contains('border-b') &&
(el.className.includes('1.714286em') || el.className.includes('1\\.714286em'))
);
}
function containsSectionHeader(el) {
return Array.from(el.querySelectorAll('*')).some(child => isSectionHeader(child));
}
function collectSectionContent(headerEl) {
const siblings = [];
let node = headerEl.nextElementSibling;
while (node) {
if (isSectionHeader(node) || containsSectionHeader(node)) break;
siblings.push(node);
node = node.nextElementSibling;
}
return siblings;
}
function chevronSVG() {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '1em');
svg.setAttribute('height', '1em');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2.5');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
svg.setAttribute('aria-hidden', 'true');
const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
poly.setAttribute('points', '6 9 12 15 18 9');
svg.appendChild(poly);
return svg;
}
/* ─── header → wrapper registry ─────────────────────────────── */
const headerToWrapper = new WeakMap();
/* ─── Open a body wrapper, then scroll to target after transition ── */
function openSection(header, wrapper, scrollTarget) {
const alreadyOpen = wrapper.classList.contains('gw-open');
if (!alreadyOpen) {
wrapper.classList.add('gw-open');
header.setAttribute('aria-expanded', 'true');
const chevron = header.querySelector('.gw-chevron');
if (chevron) chevron.classList.add('gw-open');
}
if (!scrollTarget) return;
if (alreadyOpen) {
// Already open — content is visible, scroll immediately
scrollTarget.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else {
// Wait for the max-height transition (320ms) to finish before scrolling
// so the element has its real position in the document.
wrapper.addEventListener('transitionend', function onEnd(e) {
if (e.propertyName !== 'max-height') return;
wrapper.removeEventListener('transitionend', onEnd);
scrollTarget.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
}
}
/* ─── Expand whatever section the current hash targets ───────── */
function expandForCurrentHash() {
const hash = location.hash;
if (!hash || hash === '#' || isRefPage()) return;
const id = decodeURIComponent(hash.slice(1));
const target = document.getElementById(id);
if (!target) return;
// Walk up from the anchor target until we find a section header
// or land inside a collapsed body.
let el = target;
while (el && el !== document.body) {
// Case A: we landed directly on (or walked up to) a section header
if (isSectionHeader(el) && headerToWrapper.has(el)) {
openSection(el, headerToWrapper.get(el), target);
return;
}
// Case B: we're inside a collapsed body — its previous sibling is the header
if (el.classList.contains('gw-section-body')) {
const header = el.previousElementSibling;
if (header && headerToWrapper.has(header)) {
openSection(header, el, target);
}
return;
}
el = el.parentElement;
}
}
/* ─── Build collapsible section ──────────────────────────────── */
function wrapAndBind(header, contentEls) {
if (contentEls.length === 0 || header.dataset.gwProcessed === 'true') return;
const wrapper = document.createElement('div');
wrapper.className = 'gw-section-body';
wrapper.setAttribute('role', 'region');
contentEls[0].parentNode.insertBefore(wrapper, contentEls[0]);
contentEls.forEach(el => wrapper.appendChild(el));
header.dataset.gwProcessed = 'true';
headerToWrapper.set(header, wrapper);
header.classList.add('gw-section-header');
header.setAttribute('tabindex', '0');
header.setAttribute('role', 'button');
header.setAttribute('aria-expanded', 'false');
const chevron = document.createElement('span');
chevron.className = 'gw-chevron';
chevron.appendChild(chevronSVG());
header.appendChild(chevron);
function toggle(e) {
e.preventDefault();
const isOpen = wrapper.classList.contains('gw-open');
wrapper.classList.toggle('gw-open', !isOpen);
chevron.classList.toggle('gw-open', !isOpen);
header.setAttribute('aria-expanded', String(!isOpen));
}
header.addEventListener('click', toggle);
header.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') toggle(e);
});
}
/* ─── Cleanup (for ref pages) ────────────────────────────────── */
function cleanup() {
document.querySelectorAll('.gw-section-body').forEach(wrapper => {
while (wrapper.firstChild) wrapper.parentNode.insertBefore(wrapper.firstChild, wrapper);
wrapper.remove();
});
document.querySelectorAll('.gw-section-header').forEach(header => {
header.classList.remove('gw-section-header');
header.removeAttribute('tabindex');
header.removeAttribute('role');
header.removeAttribute('aria-expanded');
header.removeAttribute('data-gw-processed');
const chevron = header.querySelector('.gw-chevron');
if (chevron) chevron.remove();
});
}
/* ─── Section processing ─────────────────────────────────────── */
function processGenericSections(root) {
Array.from(root.querySelectorAll('*'))
.filter(isSectionHeader)
.forEach(header => wrapAndBind(header, collectSectionContent(header)));
}
function processReferences() {
const refContainer = document.getElementById('references');
if (!refContainer) return;
const heading = refContainer.querySelector(
'.clear-left.overflow-hidden.border-b[class*="1.714286em"]'
);
if (!heading || heading.dataset.gwProcessed === 'true') return;
const contentEls = collectSectionContent(heading);
if (contentEls.length === 0) {
const fallback = refContainer.querySelector('[class*="columns-1"]');
if (fallback && fallback.parentNode === refContainer) {
wrapAndBind(heading, [fallback]);
return;
}
}
wrapAndBind(heading, contentEls);
}
/* ─── SPA: intercept ALL ways the hash can change ───────────── */
window.addEventListener('hashchange', () => {
if (isRefPage()) { cleanup(); return; }
expandForCurrentHash();
});
window.addEventListener('popstate', () => {
if (isRefPage()) { cleanup(); return; }
expandForCurrentHash();
});
['pushState', 'replaceState'].forEach(method => {
const orig = history[method];
history[method] = function (...args) {
orig.apply(this, args);
const newUrl = args[2] ? String(args[2]) : location.href;
const newHash = newUrl.includes('#') ? '#' + newUrl.split('#')[1] : '';
if (newHash && !/#ref-/.test(newHash)) {
setTimeout(expandForCurrentHash, 0);
}
};
});
/* ─── Main init ──────────────────────────────────────────────── */
function init() {
if (isRefPage()) { cleanup(); return; }
processGenericSections(document.body);
processReferences();
if (location.hash) expandForCurrentHash();
}
let debounceTimer;
const observer = new MutationObserver(() => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
if (!isRefPage()) {
processGenericSections(document.body);
processReferences();
}
}, 400);
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
init();
observer.observe(document.body, { childList: true, subtree: true });
});
} else {
init();
observer.observe(document.body, { childList: true, subtree: true });
}
})();