mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 11:05:13 +08:00
🛸 Sync 2026-08-19 20:38:21
This commit is contained in:
1 parent
7549efdb5e
commit
7cb3d8fcff
55 files changed
+403
-252
No files matched your search
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.1 KiB |
@@ -8,12 +8,6 @@
|
||||
"background_color": "#f6f8fa",
|
||||
"theme_color": "#f6f8fa",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/luci-static/footstrap/app-icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/luci-static/footstrap/app-icon-512.png",
|
||||
"sizes": "512x512",
|
||||
|
||||
@@ -149,7 +149,7 @@ function resolveLen(token, dflt) {
|
||||
* re-measured against the widths of the density before it. */
|
||||
/* The last resort, stated ONCE so the fallbacks and the sanity net below cannot restate the
|
||||
* stylesheet's widths in two different places. Reaching for these means the measurement failed. */
|
||||
const GEOM_DFLT = { contentMin: 500, sidebarW: 224, railW: 68, contentPad: 56 };
|
||||
const GEOM_DFLT = { contentMin: 500, sidebarW: 224, railW: 68, contentPad: 56, contentMax: 1280 };
|
||||
|
||||
let _geom = null, _geomDensity = null, _geomWarned = false;
|
||||
function shellGeometry() {
|
||||
@@ -162,10 +162,14 @@ function shellGeometry() {
|
||||
contentMin: px('--fs-content-min', GEOM_DFLT.contentMin),
|
||||
sidebarW: px('--fs-sidebar-w', GEOM_DFLT.sidebarW),
|
||||
railW: px('--fs-rail-w', GEOM_DFLT.railW),
|
||||
/* the token is ONE side's padding; the column loses it twice. Kept as `tokenPad` because it
|
||||
* is only the FALLBACK — the gutter that counts is measured, see measureShell() */
|
||||
tokenPad: px('--fs-content-pad', GEOM_DFLT.contentPad / 2) * 2,
|
||||
contentPad: px('--fs-content-pad', GEOM_DFLT.contentPad / 2) * 2
|
||||
/* the token is ONE side's padding; the column loses it twice. It is also only the FALLBACK:
|
||||
* measureShell() overwrites this field with the gutter the column actually got, and until it
|
||||
* has (the login page has no `.fs-content`, and nothing has measured before the first
|
||||
* fitter) the token is the honest answer. */
|
||||
contentPad: px('--fs-content-pad', GEOM_DFLT.contentPad / 2) * 2,
|
||||
/* the cap the column stops growing at, so the model knows where the surplus becomes margin
|
||||
* rather than gutter — see columnWidth() */
|
||||
contentMax: px('--fs-content-max', GEOM_DFLT.contentMax)
|
||||
};
|
||||
/* Plausibility, and it costs one comparison: the rail IS the sidebar collapsed, so
|
||||
* 0 < railW < sidebarW holds by construction. Both known ways this measurement fails destroy
|
||||
@@ -186,7 +190,6 @@ function shellGeometry() {
|
||||
+ 'renamed, or a foreign sheet is reaching the measurement probe.');
|
||||
}
|
||||
_geom = sane ? g : Object.assign({}, GEOM_DFLT);
|
||||
if (_geom.tokenPad === undefined) _geom.tokenPad = _geom.contentPad;
|
||||
/* the token is only the fallback; the gutter that counts is the one measureShell() read */
|
||||
if (_shellPad != null) _geom.contentPad = _shellPad;
|
||||
return _geom;
|
||||
@@ -210,21 +213,34 @@ function shellGeometry() {
|
||||
* under the reader's thumb). So the numbers are taken when a fitter runs — which is on every resize
|
||||
* and every content mutation, and never during a flick, because `fitChrome()` defers exactly like
|
||||
* every other measuring pass — and everything after that reads what was stored. Nothing they
|
||||
* describe can change without a resize, and a resize schedules a fit.
|
||||
* describe can change without a resize, and a resize schedules a fit; a resize that lands DURING a
|
||||
* scroll is deferred with the rest, so a mid-scroll answer is the geometry as of the last still
|
||||
* moment, which is the trade this whole file is built on.
|
||||
*
|
||||
* Unlike the token probe above, a hostile app's declaration is not a threat here: `.fs-content`
|
||||
* carries no chrome mark, so an app CAN restyle it, and if it did then that padding IS the column's
|
||||
* real gutter. What the probe must not report is a foreign answer for OUR layout; this reports the
|
||||
* layout as it stands. Before any fitter has run — the login page has no `.fs-content` at all — the
|
||||
* token stands in. */
|
||||
let _shellOuter = 0, _shellPad = null;
|
||||
let _shellOuter = 0, _shellPad = null, _padAt = null;
|
||||
function measureShell() {
|
||||
/* the window's own width, every time: fitShell read it before this function existed and the
|
||||
* value is what everything downstream is measured against */
|
||||
_shellOuter = document.documentElement.clientWidth;
|
||||
/* The GUTTER, though, is resolved style, and this runs on every mutation batch — once a second
|
||||
* on any polled page. The two things that move it are the width (a media query re-paddings the
|
||||
* column below 767px) and the density (the token is `calc(28px * var(--fs-density-space))`), so
|
||||
* both are the key and an unchanged page resolves nothing. Same trade as the token memo above,
|
||||
* for the same reason. */
|
||||
const key = (document.documentElement.getAttribute('data-density') || '') + '|' + _shellOuter;
|
||||
if (_padAt === key && _shellPad != null) return;
|
||||
const host = document.querySelector('.fs-content');
|
||||
const cs = host ? getComputedStyle(host) : null;
|
||||
const v = cs ? parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight) : NaN;
|
||||
/* a failed read leaves the key unset, so the next pass tries again rather than caching a miss */
|
||||
if (Number.isFinite(v) && v >= 0) {
|
||||
_shellPad = v;
|
||||
_padAt = key;
|
||||
if (_geom) _geom.contentPad = v;
|
||||
}
|
||||
}
|
||||
@@ -232,7 +248,15 @@ function measureShell() {
|
||||
function columnWidth(g, state) {
|
||||
/* narrow OR top: in both the chrome is above the content, not beside it */
|
||||
const cut = (state.narrow || state.top) ? 0 : (state.rail ? g.railW : g.sidebarW);
|
||||
return Math.max(0, state.outerW - cut - g.contentPad);
|
||||
/* AND THE COLUMN STOPS GROWING. `.fs-content` is `max-width: var(--fs-content-max); margin: 0
|
||||
* auto`, so past roughly a 1500px window the surplus becomes margin and not column: without the
|
||||
* cap this answered ~2280 on a 2560px sidebar layout for a column that is 1224 wide. No caller
|
||||
* can reach that today — both ask a LOWER bound (`< --fs-content-min` here, `< CRAMPED` in
|
||||
* fs-select) and the cap only binds where both are clear by a factor of two — but this is the
|
||||
* exported answer to "how wide is the content column", and an answer that is only true below
|
||||
* 1500px is the kind of thing the next caller inherits without being told. */
|
||||
const room = Math.min(state.outerW - cut, g.contentMax);
|
||||
return Math.max(0, room - g.contentPad);
|
||||
}
|
||||
|
||||
function fitShell() {
|
||||
@@ -546,9 +570,10 @@ return baseclass.extend({
|
||||
});
|
||||
},
|
||||
|
||||
/* exported for the unit suite (tests/chrome-geometry.test.mjs): the cut is pure arithmetic over
|
||||
* four measured numbers, and driving it directly is the only way to hold every combination of
|
||||
* layout, rail and width without a browser */
|
||||
/* exported for the unit suite in the theme's own repository (tests/chrome-geometry.test.mjs — no
|
||||
* tests ship in the package): the cut is pure arithmetic over a handful of measured numbers, and
|
||||
* driving it directly is the only way to hold every combination of layout, rail and width
|
||||
* without a browser */
|
||||
columnWidth,
|
||||
wireRail
|
||||
});
|
||||
@@ -86,6 +86,9 @@ function fittersEnabled() {
|
||||
function run() {
|
||||
if (!fittersEnabled()) return;
|
||||
runAll(_fitters, 'fitter');
|
||||
/* the page is settled now, so this is the position the next mutation must be measured against —
|
||||
* unless a correction is already on its way, which would make this reference the drifted one */
|
||||
if (!_anchorPending) rememberRest();
|
||||
}
|
||||
|
||||
/* ---- IS THE PAGE MOVING RIGHT NOW? — ASKED OF THE POSITION, NEVER OF THE EVENTS ----
|
||||
@@ -190,6 +193,8 @@ function sampleMotion() {
|
||||
}
|
||||
if (scrolling()) { requestAnimationFrame(sampleMotion); return; }
|
||||
_sampling = false;
|
||||
/* the reader has stopped: this is a still moment, and the reference for the next tick */
|
||||
rememberRest();
|
||||
/* the page has held still for SCROLL_IDLE: whatever was put off may run now */
|
||||
if (_deferred) {
|
||||
_deferred = false;
|
||||
@@ -295,53 +300,100 @@ function watch(el) {
|
||||
*
|
||||
* It never fights the user: a page at the very top has no offset to give back and is left alone, and
|
||||
* a drift under a pixel is rounding rather than movement. */
|
||||
/* DOES THE ENGINE ANCHOR AT ALL? Chromium and Firefox compensate a height change above the viewport
|
||||
* by themselves — the reader stays where they were and the scroll offset moves under them. WebKit
|
||||
* has never implemented it, so on Safari and on every iPhone the same poll tick moves the page:
|
||||
* measured with the engine's anchoring suppressed, a 120px growth above the fold moves the reader
|
||||
* 120px, and 0px with it on.
|
||||
*
|
||||
* The theme cannot do this job everywhere, only where nobody else is: correcting the offset in an
|
||||
* engine that also corrects it means two corrections and a page that jumps the other way. So it is
|
||||
* asked of the platform rather than of a browser name — `overflow-anchor` is the property that turns
|
||||
* the feature off, and an engine that does not know the property does not have the feature. */
|
||||
const ENGINE_ANCHORS = (() => {
|
||||
/* DEV SWITCH, and the reason the fallback can be gated at all: `localStorage.fsEngineAnchor =
|
||||
* 'off'` makes any engine take the path Safari takes. Without it the branch could only be
|
||||
* exercised on a machine with Safari on it, which is neither CI nor most of the people who work
|
||||
* on this — the same reason `fsFit` and `fsAnchor` exist. */
|
||||
try { if (localStorage.getItem('fsEngineAnchor') === 'off') return false; }
|
||||
catch (e) { /* no storage, no switch */ }
|
||||
try { return typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('overflow-anchor', 'auto'); }
|
||||
catch (e) { return true; } /* unreadable: assume it is handled rather than fight it */
|
||||
})();
|
||||
|
||||
/* WHAT THE READER WAS LOOKING AT, CAPTURED WHILE THE PAGE WAS STILL.
|
||||
*
|
||||
* `anchorRef()` below is called from the mutation observer, which runs AFTER the DOM changed: it
|
||||
* captures a position the poll has already moved, which is exactly right for the FITTERS (they have
|
||||
* not run yet) and blind to the mutation itself. On an engine that anchors, that split does not
|
||||
* matter — the engine covers the other half. On one that does not, this is the half nobody covers,
|
||||
* so the reference is kept from the last still moment instead. */
|
||||
let _rest = null;
|
||||
function rememberRest() {
|
||||
if (ENGINE_ANCHORS || scrolling()) return;
|
||||
const ref = anchorRef();
|
||||
/* the offset it was taken at travels with it: a reference is only about the page, and the page
|
||||
* moving under the reader is a different fact from the reader moving through it */
|
||||
_rest = ref ? { el: ref.el, top: ref.top, at: scrollTop() } : null;
|
||||
}
|
||||
|
||||
/* -> the reference to correct against: the pre-mutation one where the engine leaves that to us, the
|
||||
* post-mutation one everywhere else, which is what this file has always used. A remembered
|
||||
* reference is worth using only while it still describes the reader's position — an element that
|
||||
* left the document, or that the reader has since scrolled a screen away from, is not one. */
|
||||
function anchorFor() {
|
||||
if (ENGINE_ANCHORS) return anchorRef();
|
||||
if (!_rest || !_rest.el.isConnected) return anchorRef();
|
||||
/* THE READER MOVED, NOT THE PAGE. A reference taken at one offset says nothing about a document
|
||||
* seen from another: correcting against it would drag the page back to where the reader had
|
||||
* scrolled FROM. Anything but the offset it was captured at means take a fresh one — which, on
|
||||
* the mutation path, is the same as not correcting this tick. */
|
||||
if (_rest.at !== scrollTop()) return anchorRef();
|
||||
return _rest;
|
||||
}
|
||||
|
||||
function anchorRef() {
|
||||
/* NOT WHILE THE READER SCROLLS. Every rect read here is a forced layout, and this runs on every
|
||||
* content mutation — i.e. once a second, in the middle of a flick, which is the work iOS holds
|
||||
* the main thread back to avoid. The compensation exists for a page the reader is looking at,
|
||||
* not for one they are already moving; while they scroll there is nothing to keep still. */
|
||||
if (scrolling()) return null;
|
||||
const frames = document.querySelectorAll('#view .cbi-section, #view > .cbi-section-node, #view > div');
|
||||
for (const el of frames) {
|
||||
const r = el.getBoundingClientRect();
|
||||
/* the frame the viewport's top edge cuts through */
|
||||
if (r.top <= 0 && r.bottom > 0) return { el, top: r.top };
|
||||
|
||||
/* WHAT THE READER IS LOOKING AT, ASKED OF THE PAGE RATHER THAN OF A SELECTOR LIST.
|
||||
*
|
||||
* This walked a list of frames and took the one the fold cut through, which misses the case that
|
||||
* matters: a poll tick that grows something INSIDE that frame, above the viewport, leaves the
|
||||
* frame's own top exactly where it was — drift 0, measured — while everything after it moves.
|
||||
* Taking the deepest element AT the fold instead is both cheaper (one hit test, no rect walk)
|
||||
* and the same thing the engine's own anchoring picks, so the two agree about what "still" means.
|
||||
*
|
||||
* A data table is never the anchor: the fit pass deliberately falsifies its layout inside one
|
||||
* pass, so the theme excludes it from the engine's anchoring too (`overflow-anchor: none`,
|
||||
* theme/30-tables.css). Climbing to the nearest ancestor that is not one keeps a reference that
|
||||
* does not lie. */
|
||||
const host = document.getElementById('view');
|
||||
if (!host) return null;
|
||||
const box = host.getBoundingClientRect();
|
||||
const x = Math.round(box.left + (Math.min(box.width, window.innerWidth || box.width) / 2));
|
||||
/* BELOW THE CHROME, not at y=1: the bar is sticky and owns the first rows of the viewport, so a
|
||||
* hit test at the very top returns the chrome and the page gets no anchor at all (measured: at
|
||||
* 390px the hit was `nav.fs-sidebar` every time and no correction ever ran). The chrome says
|
||||
* where it ends — `[data-fs-chrome]` is the mark it already carries, so nothing here names a
|
||||
* height or a selector that a layout change could move. */
|
||||
let y = 1;
|
||||
let el = document.elementFromPoint(x, y);
|
||||
const chrome = el && el.closest ? el.closest('[data-fs-chrome]') : null;
|
||||
if (chrome) {
|
||||
y = Math.max(1, Math.round(chrome.getBoundingClientRect().bottom) + 1);
|
||||
el = document.elementFromPoint(x, y);
|
||||
}
|
||||
return null;
|
||||
if (!el || !host.contains(el)) return null;
|
||||
const table = el.closest('.table.fs-dt');
|
||||
if (table) el = table.parentElement;
|
||||
if (!el || !host.contains(el)) return null;
|
||||
return { el, top: el.getBoundingClientRect().top };
|
||||
}
|
||||
/* THE CORRECTION IS UNCONDITIONAL, AND IT IS A NO-OP WHERE THE ENGINE DOES THE JOB.
|
||||
*
|
||||
* There used to be a watching phase here: the first three mutations were only measured, and a
|
||||
* reference that had held still meant "this engine anchors, never touch the offset again". It was
|
||||
* wrong in the way that mattered. WebKit does not anchor, but the drift it leaves is often small —
|
||||
* a value grew a line, a row appeared — so three quiet samples were easy to come by, the
|
||||
* compensation switched itself off for the life of the document, and the page walked downwards a
|
||||
* few pixels at a time, once per poll tick. That is exactly how it was reported: "jerks a little
|
||||
* downwards, all the time".
|
||||
*
|
||||
* Correcting always is safe because the correction is computed from the REFERENCE, not from the
|
||||
* scroll offset: an engine that anchors has already put the reference back by the time this reads
|
||||
* it — the read forces layout, and the adjustment happens during layout — so the drift is zero and
|
||||
* nothing is written. Measuring the OFFSET instead reads an anchoring adjustment as a fault and
|
||||
* corrects a correction, which is how an earlier version made Chromium worse (10 movements up to
|
||||
* 1616px against 0 without it); that mistake is not in this shape.
|
||||
*
|
||||
* Two guards remain, and both are about not fighting the reader: a page at the very top has no
|
||||
* offset to give back, and a drift under a pixel is rounding rather than movement. */
|
||||
/* ONCE PER FRAME, AFTER EVERYTHING HAS LANDED — not once per mutation batch.
|
||||
*
|
||||
* A poll tick does not arrive as one mutation: luci-mod-status fills its sections one after another,
|
||||
* and each fill wakes this observer. Correcting on each of them measures a page that is still being
|
||||
* written: the reference is read between two batches, the drift is computed against a layout that
|
||||
* the next batch immediately invalidates, and the correction is wrong by whatever the rest of the
|
||||
* tick brought. The next tick corrects the other way. That is a page that jerks while nobody
|
||||
* touches it, and it is what was reported — with a telling detail: it went away whenever a
|
||||
* diagnostic probe that forced layout on EVERY frame was loaded, because then every reading this
|
||||
* code took happened to be against a settled layout.
|
||||
*
|
||||
* So the reference is taken at the first mutation of a batch and the correction is applied in a
|
||||
* requestAnimationFrame — after the batch, after style and layout have settled, once. A newer
|
||||
* reference is not taken while one is pending: the first one is where the reader actually was. */
|
||||
|
||||
let _anchorPending = null;
|
||||
let _anchorFrame = 0;
|
||||
/* DEV SWITCH, for the device that has the problem: `localStorage.fsAnchor = 'off'` stops the theme
|
||||
@@ -362,6 +414,10 @@ function scheduleAnchor(ref) {
|
||||
const pending = _anchorPending;
|
||||
_anchorPending = null;
|
||||
applyAnchor(pending);
|
||||
/* AFTER the correction, never before: the reference this file keeps for engines with no
|
||||
* anchoring of their own must describe the page as the reader now sees it, or the next tick
|
||||
* measures a drift that has already been paid and pays it twice. */
|
||||
rememberRest();
|
||||
});
|
||||
}
|
||||
function applyAnchor(ref) {
|
||||
@@ -373,6 +429,11 @@ function applyAnchor(ref) {
|
||||
if (at <= 0) return;
|
||||
const drift = ref.el.getBoundingClientRect().top - ref.top;
|
||||
if (Math.abs(drift) < 1) return;
|
||||
/* A CORRECTION IS A SCROLL THE READER DID NOT ASK FOR, so an absurd one is a bug rather than a
|
||||
* fix: a view that replaced its whole subtree can move a reference by thousands of pixels, and
|
||||
* jumping there is worse than leaving the page where it is. One viewport is the most a single
|
||||
* tick can honestly account for. */
|
||||
if (Math.abs(drift) > (window.innerHeight || 0) + 200) return;
|
||||
if (sc) sc.scrollTop = at + drift;
|
||||
else window.scrollTo(0, at + drift);
|
||||
}
|
||||
@@ -393,8 +454,11 @@ function applyAnchor(ref) {
|
||||
function observeContent() {
|
||||
if (_mo) return;
|
||||
_mo = new MutationObserver(() => {
|
||||
/* what the reader is looking at, before the poll's mutation and the fitters move anything */
|
||||
const ref = anchorRef();
|
||||
/* what the reader is looking at, before the poll's mutation and the fitters move anything —
|
||||
* see anchorFor(): where the engine does no anchoring of its own that reference comes from
|
||||
* the last still moment, because by the time this callback runs the poll has already moved
|
||||
* the page */
|
||||
const ref = anchorFor();
|
||||
run();
|
||||
scheduleAnchor(ref);
|
||||
});
|
||||
|
||||
@@ -22,7 +22,12 @@
|
||||
* what a full load does anyway. docs/spa-router.md.
|
||||
*
|
||||
* The path->node half lives in fs-menutree.js (the chrome needs it too); the "has a view poisoned
|
||||
* this document with its CSS?" half in fs-sheets.js. */
|
||||
* this document with its CSS?" half in fs-sheets.js.
|
||||
*
|
||||
* WHERE THE PATHS IN THESE COMMENTS RESOLVE: `docs/…`, `tools/…` and `tests/…` name the theme's own
|
||||
* repository (https://github.com/VizzleTF/luci-theme-footstrap), not the tree this file is read in —
|
||||
* the package ships the runtime and nothing else, so none of the three directories travels with it.
|
||||
* Anything a user is asked to quote carries a URL instead; a comment may name the path. */
|
||||
|
||||
/* --- stray-interval teardown for SPA nav ---
|
||||
* A full load kills every window.setInterval the outgoing page set; SPA nav does not, so a view's
|
||||
@@ -1393,9 +1398,13 @@ function wireRouter() {
|
||||
|
||||
const broken = contractBreaks();
|
||||
if (broken.length) {
|
||||
/* the URL, not a repository path: this package ships inside openwrt/luci as well, where a
|
||||
* `docs/` directory does not exist — and this is the one line a stranger is asked to quote,
|
||||
* on the branch where the theme has already switched half of itself off. */
|
||||
console.error('footstrap: this luci-base has no ' + broken.join(', ') +
|
||||
' — the client router stays off and every link is a full page load, which is what the ' +
|
||||
'theme did before it existed. Please report this line: docs/spa-router.md');
|
||||
'theme did before it existed. Please report this line: ' +
|
||||
'https://github.com/VizzleTF/luci-theme-footstrap/blob/main/docs/spa-router.md');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1546,12 +1555,14 @@ return baseclass.extend({
|
||||
wire: wireRouter,
|
||||
wireVisibility,
|
||||
onNavigate,
|
||||
/* exported for the unit suite (tests/router-contract.test.mjs), which drives it against a
|
||||
* hand-broken `L` — the one way to see the OFF branch without a router that ships one */
|
||||
/* exported for the unit suite in the theme's own repository (tests/router-contract.test.mjs —
|
||||
* it is not part of the package, which ships no tests), where it is driven against a
|
||||
* hand-broken `L`: the one way to see the OFF branch without a router that ships one */
|
||||
contractBreaks,
|
||||
/* likewise: tests/interval-pause.test.mjs drives the navigation sweep around a visibilitychange,
|
||||
* and tests/session-expiry.test.mjs reads the verdict the interceptors reached. navigate() is
|
||||
* the real caller of the first and `_expired` gates the second — nothing else may call either. */
|
||||
/* likewise, and likewise out-of-package: interval-pause.test.mjs drives the navigation sweep
|
||||
* around a visibilitychange and session-expiry.test.mjs reads the verdict the interceptors
|
||||
* reached. navigate() is the real caller of the first and `_expired` gates the second — nothing
|
||||
* else may call either. */
|
||||
clearViewIntervals,
|
||||
sessionExpired,
|
||||
/* fs-search warms the pages this admin actually uses (its recents) and the arrow-key-highlighted
|
||||
|
||||
@@ -612,8 +612,8 @@ function fitTables() {
|
||||
/* the column's width, not the window's: in the sidebar layout they differ by the sidebar,
|
||||
* and an 800px window whose column is 520px used to clear CRAMPED and leave the table
|
||||
* unstacked at full width, clipped by `.fs-main { overflow-x: clip }` until the reader
|
||||
* held still. `chrome.contentWidth()` answers from memoised tokens and attributes — no
|
||||
* layout is read here either. */
|
||||
* held still. `chrome.contentWidth()` answers from what the last fitter measured plus the
|
||||
* layout attributes — no layout is read here either. */
|
||||
applyDecision(t, known || { stack: chrome.contentWidth() < CRAMPED, drop: false, breakCol: -1 });
|
||||
fit.deferMeasurement();
|
||||
return;
|
||||
|
||||
@@ -5,12 +5,52 @@
|
||||
'require fs-menutree as tree';
|
||||
'require fs-chrome as chrome';
|
||||
'require fs-router as router';
|
||||
'require fs-appearance as appearance';
|
||||
'require fs-overview as overview';
|
||||
'require fs-prefs as prefs';
|
||||
'require fs-sheets as sheets';
|
||||
'require fs-search as search';
|
||||
|
||||
/* PAGE MODULES: TWO OF THESE MODULES ARE FOR ONE PAGE EACH, AND USED TO SHIP WITH EVERY PAGE.
|
||||
*
|
||||
* `fs-appearance` draws the Appearance controls on System -> System and `fs-overview` reshapes
|
||||
* Status -> Overview; each watches `body[data-page]` and does nothing anywhere else, which is the
|
||||
* right design — a theme may not register a dispatcher node, so it cannot own a route. But they
|
||||
* were `require`d in the directive prologue, and a pragma is a hard dependency: luci.js fetches and
|
||||
* evaluates them before this file's factory runs, on EVERY admin page. Measured after terser: 11.5
|
||||
* KB for the Appearance panel and 3.8 KB for the Overview one, on every cold visit to a page that
|
||||
* has neither.
|
||||
*
|
||||
* So the pragma is replaced by the same observation they were doing for themselves, once, here: on
|
||||
* the page they belong to — and only there — the module is required and wired. What each of them
|
||||
* then does is unchanged, including its own observer: `wire()` re-checks the page synchronously, so
|
||||
* a module that arrives after the stamp still starts watching. The dependency EDGE is what moved,
|
||||
* not the behaviour.
|
||||
*
|
||||
* The map duplicates a page name that also lives inside each module, and `npm run page-modules`
|
||||
* derives both sides and fails if they drift — the same trade as the Appearance axes, which are
|
||||
* implemented twice on purpose and held by a gate. */
|
||||
const PAGE_MODULES = {
|
||||
'admin-system-system': 'fs-appearance',
|
||||
'admin-status-overview': 'fs-overview'
|
||||
};
|
||||
const _pageModules = new Map();
|
||||
function wirePageModules() {
|
||||
/* through window.L, never the factory's `L`: that one carries no require() of its own */
|
||||
const RT = window.L;
|
||||
const load = () => {
|
||||
const name = PAGE_MODULES[document.body.getAttribute('data-page') || ''];
|
||||
if (!name || _pageModules.has(name)) return;
|
||||
_pageModules.set(name, RT.require(name).then((m) => m.wire()).catch((e) => {
|
||||
/* a page module that will not load costs its own page's extras and nothing else, so it
|
||||
* is dropped from the map and retried the next time that page comes up */
|
||||
_pageModules.delete(name);
|
||||
console.error('footstrap: ' + name + ' did not load', e);
|
||||
}));
|
||||
};
|
||||
/* the server's stamp is already in the DOM; every later one is the router's */
|
||||
new MutationObserver(load).observe(document.body, { attributes: true, attributeFilter: [ 'data-page' ] });
|
||||
load();
|
||||
}
|
||||
|
||||
/* The chrome BOOTSTRAP: load the menu tree once, hand it to the parts that need it, and wire them
|
||||
* in the right order. It renders nothing itself — every piece lives in its own module:
|
||||
*
|
||||
@@ -58,21 +98,17 @@ return baseclass.extend({
|
||||
fit.add(chrome.fitChrome);
|
||||
|
||||
chrome.renderChrome();
|
||||
/* Both of these ADD to a stock page rather than owning a route: each watches
|
||||
* body[data-page] and does nothing anywhere else. A theme may not register a
|
||||
* dispatcher node — it outlives the theme that registered it. */
|
||||
appearance.wire();
|
||||
/* after setTree(): the palette indexes that tree — lazily, on its first open, but its
|
||||
* recent-pages list is recorded from the first navigation onwards */
|
||||
search.wire();
|
||||
chrome.wireRail();
|
||||
chrome.wireIndicatorCounts();
|
||||
/* BEFORE router.wire(): the router restamps body[data-page] on every SPA navigation,
|
||||
* and that attribute is what fs-overview keys off. Wiring its observer after the
|
||||
* and that attribute is what the page modules key off. Wiring the observer after the
|
||||
* router's would still work today (the first stamp is the server's, already in the
|
||||
* DOM), but it would make a route change racy against a listener that is not attached
|
||||
* yet — cheap to order correctly, expensive to debug. */
|
||||
overview.wire();
|
||||
wirePageModules();
|
||||
router.wire();
|
||||
router.wireVisibility();
|
||||
/* fs-chrome's renderTabMenu warns about exactly this, and the root chain was left bare: a
|
||||
|
||||
Reference in new issue
Block a user