diff options
| author | Adam Soltys <[email protected]> | 2026-09-14 13:46:58 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-14 13:46:58 -0700 |
| commit | 0c0ba1b0129a140dc540f0fb8588b3ffdd44350c (patch) | |
| tree | 26c5e00dc2a7fb6fc1ba13caa8015e5ee09f1e68 /src/app.js | |
| parent | d730ea49feb389b43362b1044964f5a9e03ae4ff (diff) | |
| download | coinosv3-0c0ba1b0129a140dc540f0fb8588b3ffdd44350c.tar.gz coinosv3-0c0ba1b0129a140dc540f0fb8588b3ffdd44350c.zip | |
A new post opens up instead of appearing.
A post let in at the top of an open feed was there on the next paint —
one frame nothing, the next a whole row, the page shoved down by that
much. Now it opens over a third of a second: height from nothing to its
own, fading in as it goes, and a repaint mid-way resumes where it was.
The same happens to the posts the pill lets in.
That needed the morph to know which row is which. Children were matched
by position, so a new first row was patched onto the old first row's
node and every row below onto its neighbour's — the node the new post
was built as never reached the page. A list whose every child carries a
data-key is now reconciled by key: new rows are inserted as themselves,
the rest keep their nodes and move. Feed rows and their hairlines carry
the post id. Partially keyed lists keep the positional rule.
feed-live-test catches the arriving row mid-animation: shorter than it
will be, one animation running, and settled clean afterwards.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR
Diffstat (limited to 'src/app.js')
| -rw-r--r-- | src/app.js | 31 |
1 files changed, 31 insertions, 0 deletions
@@ -229,6 +229,7 @@ function morph(a, b) { // children, by position; snapshot first (appending b's children moves them) const ac = [...a.childNodes]; const bc = [...b.childNodes]; + if (allKeyed(bc)) return morphKeyed(a, ac, bc); for (let i = 0; i < bc.length; i++) { if (!ac[i]) a.append(bc[i]); else if (ac[i] !== bc[i]) morph(ac[i], bc[i]); @@ -238,6 +239,7 @@ function morph(a, b) { function morphChildren(parent, next) { const cur = [...parent.childNodes]; + if (allKeyed(next)) return morphKeyed(parent, cur, next); for (let i = 0; i < next.length; i++) { if (!cur[i]) parent.append(next[i]); else if (cur[i] !== next[i]) morph(cur[i], next[i]); @@ -245,6 +247,35 @@ function morphChildren(parent, next) { for (let i = next.length; i < cur.length; i++) cur[i].remove(); } +// A list whose every child carries a data-key is reconciled BY KEY, not by +// position: a post let in at the top of the feed used to be patched onto +// the old first row's node, and every row below it onto its neighbour's — +// so the node the new post was built as never reached the page (nothing +// that animates its own arrival could run), and each row's live state slid +// one post down. Keyed, the new row is inserted as itself and the old rows +// keep their nodes, moved down. Partially keyed lists keep the positional +// rule above, where a keyed node still refuses to patch into another key. +const allKeyed = (nodes) => nodes.length > 0 + && nodes.every((n) => n.nodeType === 1 && n.hasAttribute('data-key')); +function morphKeyed(parent, cur, next) { + const byKey = new Map(); + for (const n of cur) if (n.nodeType === 1 && n.hasAttribute('data-key')) byKey.set(n.getAttribute('data-key'), n); + const kept = new Set(); + for (let i = 0; i < next.length; i++) { + const b = next[i]; + const have = byKey.get(b.getAttribute('data-key')); + let node = b; + if (have && !kept.has(have)) { + morph(have, b); // may itself swap the node out (data-fresh, tag change) + node = have.parentNode === parent ? have : b; + } + kept.add(node); + const at = parent.childNodes[i] || null; + if (at !== node) parent.insertBefore(node, at); + } + for (const n of cur) if (!kept.has(n) && n.parentNode === parent) n.remove(); +} + const root = document.getElementById('app'); function footer() { return h( |
