mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-31 04:45:13 +08:00
🎄 Sync 2026-09-02 23:53:04
This commit is contained in:
1 parent
e27699fe9a
commit
351528cefe
53 files changed
+1219
-1072
No files matched your search
@@ -261,6 +261,22 @@ function fitChrome() {
|
||||
* fitShell's data-narrow, and is untouched here.) */
|
||||
const topBar = !!bar && !!menu && prefs.isTopLayout();
|
||||
|
||||
/* THE BAR MAY NOT GET SHORTER WHILE IT IS BEING MEASURED. The three classes below are taken off
|
||||
* so the menu can be asked whether it fits on one row (fs-fit rule 1), and on a narrow bar that
|
||||
* makes the whole chrome one row instead of two for that layout — every pixel of it above the
|
||||
* reader, who is moved by exactly as much and moved back a moment later. Chromium and Firefox
|
||||
* hide it behind their scroll anchoring; Safari implements none, on any platform, so on an
|
||||
* iPhone this is the Overview creeping up once per poll tick. Reported from one, and bisected to
|
||||
* this pass on the reporter's own router: `?off=chromefit` stopped it, `?off=measure` (the
|
||||
* tables' own re-measure) did not.
|
||||
*
|
||||
* `min-height`, not `height`: the pass may legitimately need MORE room a moment later — that is
|
||||
* what `fs-bar-stack` is for — and a floor lets it grow while refusing the shrink. It comes off
|
||||
* before `publishBarHeight()`, which must measure the bar the reader actually gets. */
|
||||
const pinned = bar ? Math.round(bar.getBoundingClientRect().height) : 0;
|
||||
const hadMinH = bar ? bar.style.minHeight : '';
|
||||
if (pinned > 0) bar.style.minHeight = pinned + 'px';
|
||||
|
||||
if (bar) bar.classList.remove('fs-bar-stack', 'fs-ind-compact', 'fs-bar-actrow');
|
||||
fitTabStrips();
|
||||
/* ---- does the main menu fit on the brand's row? ----
|
||||
@@ -291,6 +307,7 @@ function fitChrome() {
|
||||
if (bar && (topBar || document.documentElement.hasAttribute('data-narrow')))
|
||||
fitCluster(bar, menu);
|
||||
|
||||
if (pinned > 0) bar.style.minHeight = hadMinH;
|
||||
publishBarHeight(bar);
|
||||
}
|
||||
|
||||
@@ -470,8 +487,30 @@ function wireIndicatorCounts() {
|
||||
|
||||
/* ui.showIndicator replaces the label's text node on an update and appends the span on the
|
||||
* first change of the session, so childList, subtree and characterData all matter. Our own
|
||||
* attribute writes do not re-enter: attributes are not observed. */
|
||||
new MutationObserver(stamp).observe(box, { childList: true, subtree: true, characterData: true });
|
||||
* attribute writes do not re-enter: attributes are not observed.
|
||||
*
|
||||
* AND THE BAR IS RE-FITTED HERE, because a pill arriving is a layout change the fit engine
|
||||
* cannot see: fs-fit watches `#view` and the dialog, and `#indicators` is in the chrome. The
|
||||
* cluster then wraps — flexbox answers first — and the compact form only follows when something
|
||||
* else happens to wake the pass. Measured on WebKit at 390px in the narrow sidebar bar, a second
|
||||
* indicator beside the poll pill pushed the whole page down 91px and held it there for 708 ms,
|
||||
* until `fs-ind-compact` landed at 771 ms and it snapped back: a lurch down and up, once per
|
||||
* appearance, which is what "the Overview twitches, as if an invisible loading bar came and
|
||||
* went" is from the reader's side. Called from the same callback as `stamp()`, so the decision
|
||||
* is taken in the microtask before paint and the wrapped frame is never drawn. It cannot
|
||||
* re-enter: `fitChrome()` writes classes on the BAR and attributes here, and neither is what
|
||||
* this observer watches.
|
||||
*
|
||||
* ON A PILL ARRIVING OR LEAVING, not on its text: `ui.showIndicator` rewrites the poll pill's
|
||||
* label on every tick, and a fit is a handful of forced layouts — running one per tick to answer
|
||||
* a label that did not change width is the cost this file spends its measurements avoiding. A
|
||||
* childList record is the cluster gaining or losing a member, which is the layout change that
|
||||
* wraps it. */
|
||||
new MutationObserver((recs) => {
|
||||
stamp();
|
||||
if (recs.some((r) => r.type === 'childList' && r.target === box)) fitChrome();
|
||||
})
|
||||
.observe(box, { childList: true, subtree: true, characterData: true });
|
||||
stamp();
|
||||
}
|
||||
|
||||
|
||||
@@ -99,133 +99,50 @@ function run() {
|
||||
*
|
||||
* Wrapping `dom.content()` itself also works, at the price of patching a luci-base API every app
|
||||
* shares and up to seven read/write pairs per call. */
|
||||
/* A section body and a table: what `dom.content()` is called on and this can hold. Its third target,
|
||||
* a table's BODY, is deliberately absent — a floor there holds nothing. `min-height` is undefined on
|
||||
* a table box (CSS 2.1 §10.7) and WebKit acts on that: a `.table.cbi-section-table` carrying a 313px
|
||||
* floor still collapsed to 30px and the document lost 284px (webkit, /admin/network/firewall, 24.10
|
||||
* and 25.12 alike; Chromium holds the 313px), and writing the floor on the `.tbody` instead loses
|
||||
* the same 284px. 24.10 has no `.tbody` at all: its Overview renders `<table class="table">` with the
|
||||
* rows directly inside, and the container a poll empties there is `.cbi-section > div`.
|
||||
* tools/scroll-anchor.mjs looks for all three when it picks a box to collapse, which is a different
|
||||
* question from which box can carry a floor. */
|
||||
/* The three things `dom.content()` is called on: a section body, a table, and a TABLE'S BODY. The
|
||||
* third was missing and cost a release: on 24.10's Overview the section is a table, so nothing here
|
||||
* matched, the floor held nothing, and a poll emptying it took 58px off the document under the
|
||||
* reader — on ImmortalWrt 24.10 with a webkit engine, where no CI job looks. tools/scroll-anchor.mjs
|
||||
* looks for the same three and says why. */
|
||||
const SHRINKS = '.cbi-section > div, .table';
|
||||
|
||||
/* A box qualifies for a floor through its CHILDREN, so emptying it takes it out of `SHRINKS` — and
|
||||
* a floor nothing sweeps any more is a floor nothing can take off: measured on Network ->
|
||||
* Interfaces, a section emptied of its table kept 1299px of `min-height` for the life of the page.
|
||||
* Every floor this writes is marked, so the sweep finds its own work again whatever became of the
|
||||
* markup underneath. The attribute is in the theme's own namespace and says nothing to CSS. */
|
||||
/* A floor is swept off the box that wears it, and the climb below can put one on a box that is not
|
||||
* in SHRINKS itself — so emptying the table under it takes the whole section out of the sweep and
|
||||
* the floor stays for the life of the page: 927px of blank on /admin/network/network, 13 s after
|
||||
* the section emptied (tools/floor-contract.mjs). Every floor is marked, so the sweep finds its own
|
||||
* work again whatever became of the markup underneath. The attribute is in the theme's own
|
||||
* namespace and says nothing to CSS; matching on inline `min-height` instead would sweep off the
|
||||
* ones an app wrote for itself. */
|
||||
const FLOORED = '[data-fs-floor]';
|
||||
|
||||
/* What the container would stand at with no floor under it, asked of the CONTENT rather than by
|
||||
* taking the floor off and re-measuring.
|
||||
/* The floor is the height the next tick may not go below, one per container. Cleared before the
|
||||
* read, or each floor measures itself and never comes down; batched into one clear, one read pass
|
||||
* and one write pass, so the whole sweep costs a single forced layout rather than one per element.
|
||||
*
|
||||
* Clearing `min-height` to re-measure was the obvious way and is the expensive one: `min-height` is
|
||||
* a scroll-anchoring suppression trigger on the path from the anchor to the scroller
|
||||
* (css-scroll-anchoring-1 §3.2), so a clear, a forced layout and a write back tell the engine to
|
||||
* drop its own compensation for that frame — the theme switching off the very thing it relies on,
|
||||
* once per tick.
|
||||
* Not while the reader scrolls: clearing to re-measure is a layout read, and a floor staying where
|
||||
* it was is still a floor.
|
||||
*
|
||||
* The span of the children answers the same question with reads alone. It is the box's content
|
||||
* height plus what the box adds below it; a collapsed margin on the last child can put it a few
|
||||
* pixels out, which a floor can afford — the floor is a lower bound on a height that is about to be
|
||||
* replaced, not a layout the page is drawn to.
|
||||
* AND NOT ON A TABLE BOX, WHICH CANNOT HOLD IT. `min-height` is undefined on a table box (CSS 2.1
|
||||
* §10.7) and WebKit acts on that: a `.table` wearing a 313px floor still collapsed to 30px when its
|
||||
* rows went, and the document lost 284px on /admin/network/firewall — Chromium held the 313px. So
|
||||
* the floor climbs to the first box that is not a table, where the same emptied table costs 0px on
|
||||
* both engines. `getComputedStyle` resolves style, not layout, so the climb adds no forced layout.
|
||||
*
|
||||
* An empty container answers 0, and the caller keeps the standing floor for exactly that reason:
|
||||
* `dom.content()` empties before it refills, and 0 is the number the floor exists to refuse. */
|
||||
function naturalHeight(el) {
|
||||
const last = el.lastElementChild;
|
||||
if (!last) return 0;
|
||||
const box = el.getBoundingClientRect();
|
||||
/* A BOX WITH NO HEIGHT OF ITS OWN HOLDS NOTHING UP, and asking its content how tall it would be
|
||||
* gets an answer about a page that is not on screen. `visibility: hidden` leaves children in the
|
||||
* layout with rects of their own, so an inactive tab pane — collapsed to height 0 by
|
||||
* theme/30-tables.css — measures its full content here and the floor then pins that collapse
|
||||
* open: on Network -> Interfaces the hidden `device` pane held 893px, and the active pane's
|
||||
* content sat that much further down the page (issue #41, reported against a third-party page
|
||||
* and reproduced on the stock one). The clear-and-remeasure shape this replaced read the
|
||||
* collapsed height and wrote nothing, which is the behaviour restored here.
|
||||
*
|
||||
* A container a tick has just emptied also measures 0, and that is the same answer for the same
|
||||
* reason: the floor it already wears is what holds it up, and holdFloor() leaves it alone. */
|
||||
if (!box.height) return 0;
|
||||
const end = last.getBoundingClientRect();
|
||||
if (!end.height && !end.width) return 0; /* a last child out of the flow says nothing */
|
||||
let bottom = end.bottom;
|
||||
/* AND THE TEXT AFTER IT. A box whose content ends in text ends below its last ELEMENT, and
|
||||
* measuring to that element writes a floor shorter than the box: `.cbi-section-descr` on
|
||||
* /admin/network/dhcp measured 115px against the 156px it stands at, and a floor 41px short lets
|
||||
* the document shrink under the reader during the very tick the floor is there to hold.
|
||||
*
|
||||
* Only the tail, not `selectNodeContents(el)`: a Range over the whole box takes in whatever is
|
||||
* out of the flow inside it and writes a floor TALLER than the box instead — 205px against 107
|
||||
* on the Overview, which is the same blank page seen from the other side. */
|
||||
if (last.nextSibling) {
|
||||
const tail = document.createRange();
|
||||
tail.setStartAfter(last);
|
||||
tail.setEnd(el, el.childNodes.length);
|
||||
const box2 = tail.getBoundingClientRect();
|
||||
if (box2.height || box2.width) bottom = Math.max(bottom, box2.bottom);
|
||||
}
|
||||
const cs = window.getComputedStyle(el);
|
||||
const below = parseFloat(cs.paddingBottom) + parseFloat(cs.borderBottomWidth);
|
||||
return Math.round(bottom - box.top + (below || 0));
|
||||
}
|
||||
|
||||
/* The floor is the height the next tick may not go below, one per box. Read in one pass and written
|
||||
* in another, so the sweep costs a single forced layout rather than one per element.
|
||||
* Reported from an iPhone as the Overview sinking a little every five seconds: LuCI's poll takes
|
||||
* the whole `table.table` out of the first card and puts a new one back — measured on the stand,
|
||||
* once per `pollinterval`, 482px — and between the two the section is empty. On WebKit the floor on
|
||||
* the table held nothing, the offset was clamped into a document that short, and the reader was
|
||||
* left further down than they had been.
|
||||
*
|
||||
* WRITTEN ONLY WHERE THE VALUE CHANGES, which is the difference between a floor and a page the
|
||||
* engine refuses to anchor: see naturalHeight() above. Measured over 25 s of real polling on the
|
||||
* Overview at 390px, the clear-and-rewrite shape wrote style 1550 times on 25.12 and 170 times on
|
||||
* ImmortalWrt 24.10, of which 75 and 62 carried a value that had actually moved — the rest were
|
||||
* suppression bought for nothing. It is 45 writes, all of them real, on both.
|
||||
*
|
||||
* AND NOT ON A TABLE BOX, which cannot hold it: `min-height` is undefined there (CSS 2.1 §10.7) and
|
||||
* WebKit acts on that — a `.table.cbi-section-table` wearing a 313px floor still collapsed to 30px
|
||||
* when its rows went, and the document lost 284px on /admin/network/firewall, the same on 24.10 and
|
||||
* 25.12, while Chromium held the 313px. The `.tbody` inside it is a table box too and loses the same
|
||||
* 284px. So the floor climbs to the first box that is not one — the section — where the same
|
||||
* emptied `.tbody` costs the document 0px on both engines. `getComputedStyle` resolves style, not
|
||||
* layout, so the climb adds no forced layout of its own.
|
||||
*
|
||||
* Not while the reader scrolls: a rect read is a forced layout, and a floor staying where it was is
|
||||
* still a floor. */
|
||||
/* Boxes that measured empty on the LAST pass, so a floor that is holding nothing up can be told
|
||||
* from one that is holding the page still while `dom.content()` refills. WeakSet: a box the router
|
||||
* has replaced is garbage, and this must not be what keeps it alive. */
|
||||
const emptied = new WeakSet();
|
||||
|
||||
/* Off in one place, so the mark and the style can never disagree about who wears a floor. */
|
||||
function dropFloor(box) {
|
||||
if (box.style.minHeight) box.style.minHeight = '';
|
||||
box.removeAttribute('data-fs-floor');
|
||||
}
|
||||
|
||||
/* THE SECOND LOOK HAS TO BE SCHEDULED, not waited for. Every other pass here is driven by the
|
||||
* MutationObserver on `#view`, and a container that empties and stays empty produces no further
|
||||
* mutation — so "clear it if it is still empty next pass" never gets a next pass, and the floor
|
||||
* stands for the life of the page (measured: 1299px on Network -> Interfaces).
|
||||
*
|
||||
* One poll interval, because that is how long a container that is genuinely refilling may take: the
|
||||
* router's own `pollinterval` when the page will say it, and its shipped default of 5 s otherwise.
|
||||
* Shorter risks taking the floor away from a tick still in flight, which is the 568px clamp this
|
||||
* whole mechanism exists to stop. */
|
||||
let _emptyCheck = null;
|
||||
|
||||
function checkEmptyLater() {
|
||||
if (_emptyCheck) return;
|
||||
let secs = 5;
|
||||
try { secs = (window.L && L.env && L.env.pollinterval) || 5; } catch (e) { /* not on a LuCI page */ }
|
||||
_emptyCheck = setTimeout(() => { _emptyCheck = null; run(); }, secs * 1000);
|
||||
}
|
||||
|
||||
* The climb is the ONE part of the 0.14.4 floor kept: this pass still clears and re-measures, so a
|
||||
* box that cannot hold anything up measures 0 with its floor off and gets none — which is why the
|
||||
* collapsed tab pane of issue #41 cannot come back with it. */
|
||||
function holdFloor() {
|
||||
if (scrolling()) return;
|
||||
const host = document.getElementById('view');
|
||||
if (!host) return; /* the login page has no view */
|
||||
const boxes = [], hs = [], gone = [];
|
||||
host.querySelectorAll(SHRINKS + ', ' + FLOORED).forEach((el) => {
|
||||
const boxes = [], hs = [];
|
||||
host.querySelectorAll(SHRINKS).forEach((el) => {
|
||||
let box = el, cs = window.getComputedStyle(el);
|
||||
while (box && box !== host && cs.display.startsWith('table')) {
|
||||
box = box.parentElement;
|
||||
@@ -234,42 +151,13 @@ function holdFloor() {
|
||||
/* several tables in one section climb to the same box; it needs one floor, not one each */
|
||||
if (!box || box === host || boxes.indexOf(box) !== -1) return;
|
||||
boxes.push(box);
|
||||
/* `visibility` inherits, so this is equally true of every box inside a collapsed pane */
|
||||
const hidden = cs.visibility === 'hidden';
|
||||
gone.push(hidden);
|
||||
hs.push(hidden ? 0 : naturalHeight(box));
|
||||
});
|
||||
host.querySelectorAll(FLOORED).forEach((box) => { if (boxes.indexOf(box) === -1) boxes.push(box); });
|
||||
boxes.forEach((box) => { box.style.minHeight = ''; });
|
||||
boxes.forEach((box) => hs.push(box.offsetHeight));
|
||||
boxes.forEach((box, i) => {
|
||||
/* A BOX THE READER CANNOT SEE GIVES ITS FLOOR BACK. `min-height` beats the `height: 0` an
|
||||
* inactive tab pane is collapsed with (theme/30-tables.css), so a floor written while the pane
|
||||
* was open pins the collapse open once it closes: on Network -> Interfaces the `interface`
|
||||
* pane held its 1265px after the reader left it, and the tab they were looking at started
|
||||
* that far down the page (issue #41). The clear-and-remeasure shape this replaced in 0.14.4
|
||||
* cleared every floor each tick, so a pane going inactive lost its own on the next one; only
|
||||
* a box that cannot hold anything up is cleared here, and it is re-measured the tick after it
|
||||
* comes back. A collapsed pane whose floor is still standing also measures a height of its
|
||||
* own, which is why refusing to write is not enough. */
|
||||
if (gone[i]) { dropFloor(box); return; }
|
||||
/* ZERO IS AN EMPTY BOX, and the floor it already wears is what holds it up — the moment this
|
||||
* whole mechanism exists for, since `dom.content()` empties before it refills.
|
||||
*
|
||||
* But only for the one pass. A box that is still empty on the next one is not refilling, and
|
||||
* its floor is then blank page that nothing takes back: measured on Network -> Interfaces,
|
||||
* a section emptied and left alone keeps 1299px of `min-height` for the life of the page,
|
||||
* with the document standing at 1720px around no content at all. That is what 0.14.4 changed
|
||||
* by replacing clear-and-remeasure, which read the collapsed height and wrote nothing. */
|
||||
if (hs[i] <= 0) {
|
||||
if (!box.style.minHeight) return;
|
||||
if (emptied.has(box)) { dropFloor(box); emptied.delete(box); }
|
||||
else { emptied.add(box); checkEmptyLater(); }
|
||||
return;
|
||||
}
|
||||
emptied.delete(box);
|
||||
const px = hs[i] + 'px';
|
||||
if (box.style.minHeight !== px) {
|
||||
box.style.minHeight = px;
|
||||
box.setAttribute('data-fs-floor', '');
|
||||
}
|
||||
if (hs[i] > 0) { box.style.minHeight = hs[i] + 'px'; box.setAttribute('data-fs-floor', ''); }
|
||||
else box.removeAttribute('data-fs-floor');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -353,11 +241,6 @@ function sampleMotion() {
|
||||
/* the page has held still for SCROLL_IDLE: whatever was put off may run now */
|
||||
if (_deferred) {
|
||||
_deferred = false;
|
||||
/* where the reference stands BEFORE the put-off pass re-lays the page — see settleDrift() */
|
||||
const settled = _rest;
|
||||
const before = (settled && settled.el && settled.el.isConnected)
|
||||
? settled.el.getBoundingClientRect().top
|
||||
: ((settled && settled.sec && settled.sec.isConnected) ? settled.sec.getBoundingClientRect().top : null);
|
||||
/* No correction for this batch. Both available references are wrong for a page the reader
|
||||
* has just scrolled through: a fresh one is read against an offset WebKit may not have laid
|
||||
* out yet (the theme then undoes the reader's own move), and the one from the last still
|
||||
@@ -366,77 +249,6 @@ function sampleMotion() {
|
||||
* the fitters re-measure what the scroll already showed rather than growing the page — and
|
||||
* the next mutation corrects against a reference taken while the page was still. */
|
||||
run();
|
||||
if (ENGINE_ANCHORS) settleDrift(settled, before);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- the put-off pass moves the page too, and nothing was looking ----
|
||||
*
|
||||
* A tick landing while the offset is in motion leaves its measurements to the block above, and that
|
||||
* pass then re-lays the tables it could not measure. An anchoring engine answers that layout change
|
||||
* the way it answers any other — and `min-height`, which the floor writes on every container, is
|
||||
* itself a suppression trigger on the path to the anchor (css-scroll-anchoring-1 §3.2), so the
|
||||
* engine's compensation can be switched off by the very pass that needs it. Measured on
|
||||
* ImmortalWrt 24.10/WebKit: this pass's 88 `min-height` writes and a 58px jump of the offset land in
|
||||
* the SAME frame, 429 ms after the mutation (@390, top layout, large density, Overview).
|
||||
*
|
||||
* `lateDrift()` cannot see it: it is scheduled from the mutation on the same SCROLL_IDLE, so it
|
||||
* measures ALONGSIDE this pass rather than after it — it read a drift of zero three milliseconds
|
||||
* before the page moved, and scroll-anchor reported those 58px on that cell alone out of 48.
|
||||
*
|
||||
* MEASURED SYNCHRONOUSLY AROUND THE PASS, not a frame or an idle window later. Two reasons, and the
|
||||
* second cost a run: the reader cannot scroll between two statements, so what this sees is the
|
||||
* pass's doing and nothing else — a version that looked two frames later corrected inside a flick on
|
||||
* three cells of the same sweep. And `getBoundingClientRect()` is exactly the operation the spec
|
||||
* makes the engine flush a pending adjustment before, so the read after the pass sees the engine's
|
||||
* answer rather than racing it (§2.2: the suppression window ends at the end of the event loop
|
||||
* iteration, or before the next operation whose result would differ, whichever is sooner). */
|
||||
function settleDrift(ref, before) {
|
||||
if (before == null || !ref) return;
|
||||
if (!anchorEnabled() || Date.now() < _userUntil) return;
|
||||
if (_restPage !== pageStamp()) return;
|
||||
const el = (ref.el && ref.el.isConnected) ? ref.el : ((ref.sec && ref.sec.isConnected) ? ref.sec : null);
|
||||
if (el) putBack(el, before);
|
||||
}
|
||||
|
||||
/* Give the reader back what moved under them: the one write both corrections make, and the rules
|
||||
* that write obeys.
|
||||
*
|
||||
* A drift under a pixel is rounding, and an engine that answered for it reads the same. One
|
||||
* viewport is the ceiling, a drift that size being a view that replaced its whole subtree rather
|
||||
* than a tick — anchorFor() raises it for the one drift that big with a receipt, a measured clamp.
|
||||
*
|
||||
* The write moves the page by exactly the drift measured, so the reference is back at the top it
|
||||
* was remembered at and the next tick measures zero. Only `_restAt` moves, and the write may have
|
||||
* been clamped short, so it is re-read rather than assumed; `rememberRest()` cannot do it, since
|
||||
* the write starts the motion sampler and that function returns early while the page moves. */
|
||||
function putBack(el, was) {
|
||||
const drift = el.getBoundingClientRect().top - was;
|
||||
if (Math.abs(drift) < 1 || Math.abs(drift) > (window.innerHeight || 800)) return;
|
||||
const sc = scroller();
|
||||
const at = sc ? sc.scrollTop : window.scrollY;
|
||||
if (sc) sc.scrollTop = at + drift; else window.scrollTo(0, at + drift);
|
||||
_restAt = scrollTop();
|
||||
/* AND WHERE THE ELEMENT ACTUALLY LANDED, which is not always `was`. The line above used to be
|
||||
* the whole of it, on the reasoning this comment states — the write is exactly the drift, so the
|
||||
* element is back at the top it was remembered at. It is not whenever the write was CLAMPED
|
||||
* SHORT, a case the line above already allows for by re-reading the offset: the page had less
|
||||
* room than the drift asked for, the element stops wherever the clamp left it, and `_rest` goes
|
||||
* on naming a top nothing can reach. Every later tick then measures that unreachable difference
|
||||
* and spends it on the reader: measured on webkit/Overview @390 top, `_rest` claiming 93.02 for
|
||||
* an element standing at 105, and the reader 12px off on the next tick (52px at normal density).
|
||||
*
|
||||
* Two rects on a layout the write has already forced. The section half is re-read for the same
|
||||
* reason and cannot be measured by the same probe — the sweep's swap returns the very nodes it
|
||||
* took out, so `_rest.el` survives it and the fallback is never reached, while a real
|
||||
* `dom.content()` puts NEW nodes in and lateDrift() corrects against `sec`/`secTop` instead.
|
||||
*
|
||||
* `_rest.at` is deliberately NOT touched: it belongs to anchorFor(), on the path where the
|
||||
* engine does no anchoring, and this fault is on the other one — measured, the sweep is green
|
||||
* on all 18 cells of the failing axis without it. */
|
||||
if (_rest) {
|
||||
if (_rest.el && _rest.el.isConnected) _rest.top = _rest.el.getBoundingClientRect().top;
|
||||
if (_rest.sec && _rest.sec.isConnected) _rest.secTop = _rest.sec.getBoundingClientRect().top;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,8 +602,23 @@ function lateDrift(ref) {
|
||||
if (scrollTop() !== seen) return;
|
||||
/* the tick usually replaces the element this was taken on, so without the section
|
||||
* fallback the correction does nothing on the tick it exists for */
|
||||
if (ref.el && ref.el.isConnected) putBack(ref.el, ref.top);
|
||||
else if (ref.sec && ref.sec.isConnected && ref.secTop != null) putBack(ref.sec, ref.secTop);
|
||||
let el = ref.el, was = ref.top;
|
||||
if (!el || !el.isConnected) {
|
||||
if (!ref.sec || !ref.sec.isConnected || ref.secTop == null) return;
|
||||
el = ref.sec; was = ref.secTop;
|
||||
}
|
||||
const drift = el.getBoundingClientRect().top - was;
|
||||
if (Math.abs(drift) < 1) return; /* the engine put it back */
|
||||
if (Math.abs(drift) > (window.innerHeight || 800)) return;
|
||||
const sc = scroller();
|
||||
const at = sc ? sc.scrollTop : window.scrollY;
|
||||
if (sc) sc.scrollTop = at + drift; else window.scrollTo(0, at + drift);
|
||||
/* The write moves the page by exactly the drift measured, which puts the reference back
|
||||
* at the top it was remembered at, so `_rest.top` still holds and the next tick
|
||||
* measures zero. Only `_restAt` changes, and the write may have been clamped short, so
|
||||
* it is re-read rather than assumed; `rememberRest()` cannot do it, since the write
|
||||
* starts the motion sampler and that function returns early while the page moves. */
|
||||
_restAt = scrollTop();
|
||||
}, SCROLL_IDLE);
|
||||
});
|
||||
}
|
||||
@@ -898,17 +725,13 @@ function observeContent() {
|
||||
_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 TAB SWITCH MUTATES NO NODE. ui.tabs writes `data-tab-active` on the panes, so the
|
||||
* {childList} registration above never wakes and the floor the pane wore while it was open
|
||||
* stays on it — and `min-height` beats the `height: 0` an inactive pane is collapsed with
|
||||
* (theme/30-tables.css), so that floor IS blank page above the tab the reader just opened.
|
||||
* Measured on 25.12, /admin/network/network, Interfaces -> Devices: 1299px left standing, the
|
||||
* document at 2647px against 1720 and the content the reader came for 1559px down, still there
|
||||
* 13 s later on a page whose poll never mutates #view (tools/floor-contract.mjs, issue #75).
|
||||
*
|
||||
* 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
|
||||
@@ -916,6 +739,7 @@ function observeContent() {
|
||||
_moTabs = new MutationObserver(run);
|
||||
for (const host of hosts)
|
||||
_moTabs.observe(host, { attributes: true, attributeFilter: [ 'data-tab-active' ], subtree: true });
|
||||
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
|
||||
Reference in new issue
Block a user