|
1 |
+ |
// Folding a run's overflow into a menu, because only a browser can measure it.
|
|
2 |
+ |
//
|
|
3 |
+ |
// `makeover_layout::Fallback::Menu` says the members a `Shed` would drop move
|
|
4 |
+ |
// into one overflow control instead. That needs to know the run is out of room,
|
|
5 |
+ |
// and CSS cannot ask: `@container` compares against a `<length>`, and there is
|
|
6 |
+ |
// no `@container (inline-size < min-content)`. Every honest CSS spelling needs
|
|
7 |
+ |
// an authored breakpoint, which is the thing the room ruling exists to forbid.
|
|
8 |
+ |
// So the measuring is here, where the browser already knows.
|
|
9 |
+ |
//
|
|
10 |
+ |
// It reads what makeover-webview emits and nothing else: `.run-menu` for a run
|
|
11 |
+ |
// that asked, and `.cell-drops-first` / `.cell-drops-next` for what a member is
|
|
12 |
+ |
// worth. The control it builds carries that crate's `.run-overflow` classes, so
|
|
13 |
+ |
// the surface and the shadow are the design system's and only the measuring is
|
|
14 |
+ |
// this file's.
|
|
15 |
+ |
//
|
|
16 |
+ |
// Degrading is the point of the mark. A page that never runs this keeps
|
|
17 |
+ |
// `.run-menu`'s own `flex-wrap: wrap` and every member stays reachable, which is
|
|
18 |
+ |
// rule 1; `data-menu` is what switches the run back to `nowrap`, and it is
|
|
19 |
+ |
// written from here, so the fallback is the default rather than the failure.
|
|
20 |
+ |
//
|
|
21 |
+ |
// The MNW server carried a copy of this for its four Askama tab strips
|
|
22 |
+ |
// (`core/tabs.ts`'s `tabOverflow`), which looked for `.tabs` and never saw the
|
|
23 |
+ |
// described strips at all.
|
|
24 |
+ |
(() => {
|
|
25 |
+ |
"use strict";
|
|
26 |
+ |
|
|
27 |
+ |
// What a member is worth when room runs out, lowest first. Read off the
|
|
28 |
+ |
// classes rather than off an attribute of our own: these are what
|
|
29 |
+ |
// `Ranked::priority` already reaches the markup as, and inventing a second
|
|
30 |
+ |
// spelling is how two encodings of one fact stop agreeing.
|
|
31 |
+ |
const TIERS = ["cell-drops-first", "cell-drops-next"];
|
|
32 |
+ |
|
|
33 |
+ |
// A tab strip is one member of its run and its tabs are what fold. The five
|
|
34 |
+ |
// described MNW strips are all this shape: `across(Fallback::Menu)` on a
|
|
35 |
+ |
// tab group whose run holds no members of its own, so a reading that only
|
|
36 |
+ |
// looked at the run's children would find one strip that always fits and
|
|
37 |
+ |
// fold nothing.
|
|
38 |
+ |
const STRIP = '[role="tablist"]';
|
|
39 |
+ |
|
|
40 |
+ |
/**
|
|
41 |
+ |
* The element whose children fold, and the children in shedding order.
|
|
42 |
+ |
*
|
|
43 |
+ |
* Order is by priority tier and then last-first within a tier. Last-first
|
|
44 |
+ |
* is what "shed by priority, not by document order" leaves once priority is
|
|
45 |
+ |
* spent: among members worth the same, the ones at the end are the ones a
|
|
46 |
+ |
* reader reaches for least. For a strip, where no tab carries a priority at
|
|
47 |
+ |
* all, that is the whole rule and it is the one the description means --
|
|
48 |
+ |
* the last tabs fold away and the first stay.
|
|
49 |
+ |
*/
|
|
50 |
+ |
const shedding = (run) => {
|
|
51 |
+ |
const strip = run.querySelector(STRIP);
|
|
52 |
+ |
const host = strip ?? run;
|
|
53 |
+ |
const members = [...host.children].filter(
|
|
54 |
+ |
(el) => !el.classList.contains("run-overflow"),
|
|
55 |
+ |
);
|
|
56 |
+ |
const order = [];
|
|
57 |
+ |
for (const tier of TIERS) {
|
|
58 |
+ |
order.push(...members.filter((el) => el.classList.contains(tier)).reverse());
|
|
59 |
+ |
}
|
|
60 |
+ |
order.push(
|
|
61 |
+ |
...members
|
|
62 |
+ |
.filter((el) => !TIERS.some((tier) => el.classList.contains(tier)))
|
|
63 |
+ |
.reverse(),
|
|
64 |
+ |
);
|
|
65 |
+ |
return { host, order };
|
|
66 |
+ |
};
|
|
67 |
+ |
|
|
68 |
+ |
/** The control the shed members move into, built once per host. */
|
|
69 |
+ |
const control = (host) => {
|
|
70 |
+ |
const existing = host.querySelector(":scope > .run-overflow");
|
|
71 |
+ |
if (existing) return existing;
|
|
72 |
+ |
|
|
73 |
+ |
const wrap = document.createElement("div");
|
|
74 |
+ |
wrap.className = "run-overflow";
|
|
75 |
+ |
|
|
76 |
+ |
const button = document.createElement("button");
|
|
77 |
+ |
button.type = "button";
|
|
78 |
+ |
button.className = "button";
|
|
79 |
+ |
button.setAttribute("aria-expanded", "false");
|
|
80 |
+ |
button.setAttribute("aria-haspopup", "true");
|
|
81 |
+ |
// A word rather than a glyph. The strips this replaces said "More" and
|
|
82 |
+ |
// a screen reader has something to read either way.
|
|
83 |
+ |
button.textContent = "More";
|
|
84 |
+ |
|
|
85 |
+ |
const items = document.createElement("div");
|
|
86 |
+ |
items.className = "run-overflow-items";
|
|
87 |
+ |
items.hidden = true;
|
|
88 |
+ |
|
|
89 |
+ |
button.addEventListener("click", () => {
|
|
90 |
+ |
const open = items.hidden;
|
|
91 |
+ |
items.hidden = !open;
|
|
92 |
+ |
button.setAttribute("aria-expanded", String(open));
|
|
93 |
+ |
});
|
|
94 |
+ |
|
|
95 |
+ |
wrap.append(button, items);
|
|
96 |
+ |
host.append(wrap);
|
|
97 |
+ |
return wrap;
|
|
98 |
+ |
};
|
|
99 |
+ |
|
|
100 |
+ |
/** Put everything back, so a measurement starts from the whole run. */
|
|
101 |
+ |
const restore = (host, wrap) => {
|
|
102 |
+ |
const items = wrap.querySelector(".run-overflow-items");
|
|
103 |
+ |
while (items.firstChild) host.insertBefore(items.firstChild, wrap);
|
|
104 |
+ |
wrap.hidden = true;
|
|
105 |
+ |
items.hidden = true;
|
|
106 |
+ |
wrap.querySelector("button")?.setAttribute("aria-expanded", "false");
|
|
107 |
+ |
};
|
|
108 |
+ |
|
|
109 |
+ |
// The measurement. `scrollWidth > clientWidth` is the browser answering
|
|
110 |
+ |
// "does this overflow" directly, which is the question CSS could not be
|
|
111 |
+ |
// asked. It is read after a restore, so what is compared is always the
|
|
112 |
+ |
// whole run rather than a run already missing what a previous pass took.
|
|
113 |
+ |
const tight = (host) => host.scrollWidth > host.clientWidth + 1;
|
|
114 |
+ |
|
|
115 |
+ |
/** Fold one run until what is left fits. */
|
|
116 |
+ |
const fold = (run) => {
|
|
117 |
+ |
const { host, order } = shedding(run);
|
|
118 |
+ |
const wrap = control(host);
|
|
119 |
+ |
restore(host, wrap);
|
|
120 |
+ |
|
|
121 |
+ |
// Marked before measuring: the mark is what turns wrapping off, and a
|
|
122 |
+ |
// wrapped run always fits, so measuring first would always find room.
|
|
123 |
+ |
run.setAttribute("data-menu", "");
|
|
124 |
+ |
|
|
125 |
+ |
const items = wrap.querySelector(".run-overflow-items");
|
|
126 |
+ |
let shed = 0;
|
|
127 |
+ |
// Never the last one. A control that folded everything into itself
|
|
128 |
+ |
// would leave a strip with nothing on it, which is worse than an
|
|
129 |
+ |
// overflowing strip and is not what the description asks for.
|
|
130 |
+ |
while (tight(host) && shed < order.length - 1) {
|
|
131 |
+ |
items.prepend(order[shed]);
|
|
132 |
+ |
shed += 1;
|
|
133 |
+ |
}
|
|
134 |
+ |
wrap.hidden = shed === 0;
|
|
135 |
+ |
};
|
|
136 |
+ |
|
|
137 |
+ |
const all = () => {
|
|
138 |
+ |
for (const run of document.querySelectorAll(".run-menu")) fold(run);
|
|
139 |
+ |
};
|
|
140 |
+ |
|
|
141 |
+ |
// Three occasions, and each is a different way the answer changes: the room
|
|
142 |
+ |
// changed, the members changed, or the page just arrived. The walk is over
|
|
143 |
+ |
// the document every time rather than over a list captured at load, which is
|
|
144 |
+ |
// the selection script's reason -- a swap brings new runs and takes old ones
|
|
145 |
+ |
// away, and both are found by the next pass.
|
|
146 |
+ |
if (typeof ResizeObserver === "function") {
|
|
147 |
+ |
const observer = new ResizeObserver(() => all());
|
|
148 |
+ |
observer.observe(document.documentElement);
|
|
149 |
+ |
} else {
|
|
150 |
+ |
window.addEventListener("resize", all);
|
|
151 |
+ |
}
|
|
152 |
+ |
document.addEventListener("htmx:after:settle", all);
|
|
153 |
+ |
if (document.readyState === "loading") {
|
|
154 |
+ |
document.addEventListener("DOMContentLoaded", all);
|
|
155 |
+ |
} else {
|
|
156 |
+ |
all();
|
|
157 |
+ |
}
|
|
158 |
+ |
})();
|