| 1/1 | A shelf's link followed on its own owner page opens that shelf | +36 | -15 | ✓ |
No comments yet: forge comment 26 adds one.
1/1 A shelf's link followed on its own owner page opens that shelf
| M | site/ | 15 | ++++++++++++--- |
| M | test/ | 25 | ++++++++++++++++--------- |
| M | test/ | 9 | +++++++-- |
| M | test/ | 2 | +- |
@@ -139,9 +139,18 @@
139139 else b.setAttribute('aria-pressed', String(b.getAttribute('aria-pressed') !== 'true'));140140 });141141 142- // A link to one shelf (/cat/#games, from the home page's topics) opens on it.143- const at = decodeURIComponent(location.hash.slice(1));144- if (at && document.querySelector(`[data-shelves] a[data-shelf="${CSS.escape(at)}"]`)) pick.shelf = at;142+ // A link to one shelf (/cat/#games, from the home page's topics) opens on143+ // it, and so does following one, or going back, on the page itself: the144+ // address changes but the page doesn't load again. Any other address is "all".145+ const atHash = (): void => {146+ const at = decodeURIComponent(location.hash.slice(1));147+ pick.shelf = at && document.querySelector(`[data-shelves] a[data-shelf="${CSS.escape(at)}"]`) ? at : '';148+ };149+ atHash();150+ window.addEventListener('hashchange', () => {151+ atHash();152+ shelves();153+ });145154 146155 const content = (): void => {147156 wrap(wrapped());@@ -4,10 +4,10 @@
44 // hidden, text, moving children, and selectors made of tags, classes and55 // attributes, descendants and lists.66 //7-// node test/page.ts shelves <page.html> [#fragment] [click <selector>]...7+// node test/page.ts shelves <page.html> [#fragment] [click <selector> | go <#fragment>]...88 //9-// After loading the page at the fragment and clicking each selector in10-// turn, prints the shelf panels that show, a line each:9+// After loading the page at the fragment, then clicking each selector or10+// going to each fragment on the page (hashchange) in turn, prints the shelf panels that show, a line each:1111 // `<head strip>: <repo> <repo>...`, the repos as their rows show them.1212 import fs from 'node:fs';1313 import vm from 'node:vm';@@ -197,9 +197,11 @@ function load(file: string, hash: string) {
197197 },198198 });199199 const noop = (): void => {};200+ const location = { hash };200201 const page = vm.createContext({201202 document: doc,202- location: { hash },203+ window: { addEventListener: (type: string, f: (e: object) => void) => (handlers['window ' + type] ??= []).push(f) },204+ location,203205 localStorage: { getItem: () => null, setItem: noop },204206 matchMedia: () => ({ matches: false }),205207 setInterval: noop,@@ -221,7 +223,11 @@ function load(file: string, hash: string) {
221223 if (!target) throw new Error(`test/page.ts: nothing on the page matches ${selector}`);222224 for (const f of handlers.click ?? []) f({ type: 'click', target, preventDefault: noop });223225 };224- return { doc, click };226+ const go = (to: string): void => {227+ location.hash = to;228+ for (const f of handlers['window hashchange'] ?? []) f({ type: 'hashchange' });229+ };230+ return { doc, click, go };225231 }226232 227233 const shown = (e: El): boolean => !e.closest('[hidden]');@@ -229,14 +235,15 @@ const words = (s: string): string => s.replace(/\s+/g, ' ').trim();
229235 230236 const [mode, file, ...rest] = process.argv.slice(2);231237 if (mode !== 'shelves' || !file) {232- console.error('usage: node test/page.ts shelves <page.html> [#fragment] [click <selector>]...');238+ console.error('usage: node test/page.ts shelves <page.html> [#fragment] [click <selector> | go <#fragment>]...');233239 process.exit(2);234240 }235241 const hash = rest[0]?.startsWith('#') ? rest.shift()! : '';236-const { doc, click } = load(file, hash);242+const { doc, click, go } = load(file, hash);237243 for (let i = 0; i < rest.length; i += 2) {238- if (rest[i] !== 'click' || !rest[i + 1]) throw new Error(`test/page.ts: expected click <selector>, got ${rest.slice(i).join(' ')}`);239- click(rest[i + 1]);244+ if (rest[i] === 'click' && rest[i + 1]) click(rest[i + 1]);245+ else if (rest[i] === 'go' && rest[i + 1]) go(rest[i + 1]);246+ else throw new Error(`test/page.ts: expected click <selector> or go <#fragment>, got ${rest.slice(i).join(' ')}`);240247 }241248 for (const sec of doc.querySelectorAll('section.shelf').filter(shown)) {242249 const names = sec.querySelectorAll('.card').filter(shown).map((c) => words(c.querySelector('.card-name')!.textContent).replace(' / ', '/'));@@ -1,6 +1,6 @@
11 # A link to one shelf (/dami/#games, from the home page's topics) opens the2-# owner's page on that shelf's tab; an address naming no shelf opens on3-# "all". Without JS the address picks the panel too (:target, in the2+# owner's page on that shelf's tab, also when followed on the page itself;3+# an address naming no shelf opens on "all". Without JS the address picks the panel too (:target, in the44 # shelves macro's CSS).55 . "$(dirname "$0")/../lib.sh"66 user dami@@ -22,6 +22,11 @@ has "$FORGE_WWW/index.html" '<a href="/dami/#games">games'
2222 eq "$(page '#games')" "games 2: go chess"2323 eq "$(page '#tools')" "tools 1: notes"2424 eq "$(page '#nothing')" "all 4: blog notes go chess"25+# Following such a link on the page itself, or going back: the address26+# changes, the page stays.27+eq "$(page go '#games')" "games 2: go chess"28+eq "$(page click '[data-shelves] a[data-shelf="tools"]' go '#games')" "games 2: go chess"29+eq "$(page '#games' go '#')" "all 4: blog notes go chess"2530 has "$i" '<section class="panel shelf" data-shelf="games" id="games">'2631 css=$FORGE_WWW$(grep -o '/assets/[A-Za-z0-9_-]*\.css' "$i" | while read -r f; do grep -q shelves-js "$FORGE_WWW$f" && echo "$f"; done | head -n 1)2732 has "$css" '\.shelves:not\(\.shelves-js\) > \.shelf\[data-shelf\]:not\(\[data-shelf=""\]\):not\(:target\) \{ display: none; \}'@@ -13,7 +13,7 @@ got=$(cd "$T_HOME" && TZ=UTC LC_ALL=en_US.UTF-8 node -e '
1313 for (const [name, iso] of cases) for (const ago of [false, true]) els.push({ name: name + (ago ? ", in a sentence" : ""), dateTime: iso, textContent: "", title: "", hasAttribute: (a) => ago && a === "data-ago" });1414 const noop = () => {};1515 const document = { addEventListener: noop, querySelector: () => null, querySelectorAll: (s) => (s === "time[datetime]" ? els : []), body: { classList: { toggle: noop } } };16- const page = vm.createContext({ document, localStorage: { getItem: () => null, setItem: noop }, matchMedia: () => ({ matches: false }), location: { hash: "" }, setInterval: noop, setTimeout: noop, navigator: {} });16+ const page = vm.createContext({ document, window: { addEventListener: noop }, localStorage: { getItem: () => null, setItem: noop }, matchMedia: () => ({ matches: false }), location: { hash: "" }, setInterval: noop, setTimeout: noop, navigator: {} });1717 vm.runInContext("Date.now = () => " + now, page);1818 vm.runInContext(stripTypeScriptTypes(fs.readFileSync("site/assets/ui.ts", "utf8")), page);1919 for (const e of els) console.log(e.name + ": " + e.textContent);