mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-30 18:35:13 +08:00
⛅ Sync 2026-09-02 10:34:10
This commit is contained in:
1 parent
e9d23393c1
commit
eb2b4e956d
23 files changed
+805
-642
No files matched your search
@@ -32,7 +32,7 @@ function armGate() {
|
||||
|
||||
const _fitters = [];
|
||||
let _rafPending = false;
|
||||
let _ro = null, _mo = null, _moFlag = null;
|
||||
let _ro = null, _mo = null, _moFlag = null, _moTabs = null;
|
||||
|
||||
/* ---- a pass that reads layout may not run while the reader scrolls ----
|
||||
*
|
||||
@@ -879,8 +879,9 @@ function observeContent() {
|
||||
if (ENGINE_ANCHORS) lateDrift(settled);
|
||||
else scheduleAnchor(ref);
|
||||
});
|
||||
for (const host of [ document.getElementById('view') || document.body, document.getElementById('modal_overlay') ]) {
|
||||
if (!host) continue;
|
||||
const hosts = [ document.getElementById('view') || document.body, document.getElementById('modal_overlay') ]
|
||||
.filter(Boolean);
|
||||
for (const host of hosts) {
|
||||
_mo.observe(host, { childList: true, subtree: true });
|
||||
watch(host);
|
||||
}
|
||||
@@ -896,6 +897,25 @@ function observeContent() {
|
||||
* change in the document, and the poll rewrites row classes on every tick. */
|
||||
_moFlag = new MutationObserver(run);
|
||||
_moFlag.observe(document.body, { attributes: true, attributeFilter: [ 'class' ] });
|
||||
|
||||
/* A TAB SWITCH IS A LAYOUT CHANGE WITH NO MUTATION IN IT. `ui.tabs` moves no node — it writes
|
||||
* `data-tab-active` on the panes — so the {childList} registration above never wakes, and the
|
||||
* floor the outgoing pane wears stands until something else sweeps. `min-height` beats the
|
||||
* `height: 0` an inactive pane is collapsed with (theme/30-tables.css), so that floor is blank
|
||||
* page above whatever the reader just opened: measured on 25.12, System -> Startup left 2432px
|
||||
* of it and the "Local Startup" textarea read as missing (#75), Network -> Interfaces 1299px
|
||||
* until the next poll tick, i.e. one `pollinterval`. A page that does not poll never gets that
|
||||
* tick and keeps the blank for the life of the page.
|
||||
*
|
||||
* `run()` direct, not the observer above: the anchoring corrections answer a poll tick that
|
||||
* moved the page under a still reader, and a tab the reader clicked is neither.
|
||||
*
|
||||
* A THIRD observer for the reason the second one exists — observe() replaces the options of a
|
||||
* registration for the same node. The filter keeps it to the one attribute: `subtree: true` on
|
||||
* `class` would wake run() on every row the poll rewrites. */
|
||||
_moTabs = new MutationObserver(run);
|
||||
for (const host of hosts)
|
||||
_moTabs.observe(host, { attributes: true, attributeFilter: [ 'data-tab-active' ], subtree: true });
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
|
||||
@@ -720,34 +720,25 @@ function commitStage(stage, contentHost) {
|
||||
dropStage(stage);
|
||||
}
|
||||
|
||||
/* ---- the commit is the one frame of a navigation worth animating ----
|
||||
/* ---- the swap is a DOM move, and nothing wraps it ----
|
||||
*
|
||||
* `commitStage()` is a synchronous DOM move, so a view transition here wraps a FRAME, not a render:
|
||||
* the update callback settles in the same tick and the API's rendering suppression cannot outlive
|
||||
* it. Wrapping navigate() instead would freeze the page for the whole chain — 136-196 ms median
|
||||
* (docs/spa-router.md) and up to RENDER_TIMEOUT on a cold route.
|
||||
* 0.14.4 wrapped `commitStage()` in `document.startViewTransition()` to cross-fade the swap, on the
|
||||
* reasoning that the commit is one synchronous DOM move and so the API would be animating a frame
|
||||
* rather than a render. What that reasoning did not cover is the CAPTURE, which the engine performs
|
||||
* before it enters the update callback and which the page cannot bound: measured on WebKit at
|
||||
* 390px, a client navigation to /admin/system/system spent 2,143 ms in a single main-thread task
|
||||
* with the transition and 213 ms without it, and the swap itself landed at 3,728 ms against 206 ms.
|
||||
* The reader spends that time looking at the page they navigated away from, under the new URL —
|
||||
* which is what a report of "the section is not where I expect it, F5 fixes it" looks like from the
|
||||
* outside (issue #42), F5 being a full load and starting no transition.
|
||||
*
|
||||
* The scroll restore runs INSIDE the callback: `::view-transition` is a fixed overlay of the old
|
||||
* pixels, and a scroll that lands after the snapshot slides the live page under a still image.
|
||||
*
|
||||
* Reduced motion is answered by not starting a transition at all, rather than by a zeroed animation:
|
||||
* the `*` rule in theme/95-a11y-media.css does not reach a pseudo tree, and skipping also saves the
|
||||
* snapshot. Read per navigation, so an OS change needs no listener.
|
||||
*
|
||||
* `ready` rejects when the transition is skipped — a hidden document, a duplicate name — which is a
|
||||
* normal outcome and not an error to report; the swap itself has happened either way.
|
||||
*
|
||||
* BOTH promises are taken, and `finished` is not decoration: it rejects with whatever the callback
|
||||
* threw, so leaving it alone turns one fault into two console lines — the throw itself and an
|
||||
* unhandled rejection behind it. Measured on a page whose callback throws: two `pageerror`s with
|
||||
* only `ready` handled, one with both. The extra line is noise in a log the live gates read. */
|
||||
function swapIn(commit) {
|
||||
const mq = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
if ((mq && mq.matches) || typeof document.startViewTransition !== 'function') { commit(); return; }
|
||||
const t = document.startViewTransition(commit);
|
||||
t.ready.catch(() => {});
|
||||
t.finished.catch(() => {});
|
||||
}
|
||||
* A deadline was tried and does not hold: `skipTransition()` runs the update callback at once
|
||||
* (3-5 ms on WebKit, Chromium and Firefox alike), but it has to be called from a timer, and the
|
||||
* capture is holding the thread that timer needs — the 150 ms deadline fired at 1,944 ms, after the
|
||||
* callback it was there to pre-empt. Chromium captures the same navigations in 15-40 ms and pays
|
||||
* none of this; an effect that is free on one engine and seconds on another, with no way to tell
|
||||
* them apart before spending them, is not worth a reader's navigation. It can come back when a
|
||||
* capture can be measured without one. */
|
||||
|
||||
/* The `#view` the document keeps between navigations, i.e. the one the observers are bound to:
|
||||
* whichever `#view` is not the stage. A document that has none gets one, once. */
|
||||
@@ -1101,11 +1092,9 @@ function navigate(pathname, push, kbd) {
|
||||
/* superseded while rendering: the chain painted into its own stage, so drop it and
|
||||
* leave the live page to the newer navigation */
|
||||
if (gen !== _navGen) { dropStage(stage); return; }
|
||||
swapIn(() => {
|
||||
commitStage(stage, contentHost);
|
||||
/* now, and only now, is there one height to read: the incoming page's */
|
||||
if (restoreTo) restoreScroll(restoreTo, gen);
|
||||
});
|
||||
commitStage(stage, contentHost);
|
||||
/* now, and only now, is there one height to read: the incoming page's */
|
||||
if (restoreTo) restoreScroll(restoreTo, gen);
|
||||
})
|
||||
.catch((e) => { dropStage(stage); throw e; });
|
||||
}).catch((e) => {
|
||||
|
||||
Reference in new issue
Block a user