@keenmate/pure-css 1.0.0-rc03 → 1.0.0-rc05
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +195 -0
- package/README.md +69 -23
- package/dist/css/base.css +48 -204
- package/dist/css/grid.css +364 -364
- package/dist/css/pure-css.css +1726 -612
- package/dist/css/reboot.css +5 -5
- package/dist/css/scrollbars.css +9 -9
- package/dist/css/utilities.css +78 -30
- package/package.json +18 -3
- package/src/js/container-breakpoint.js +308 -0
- package/src/js/fit.js +890 -0
- package/src/js/navbar-dropdown.js +164 -0
- package/src/js/pure-css.js +305 -0
- package/src/js/sidebar-resize.js +210 -0
- package/src/scss/_base-css-variables.scss +360 -385
- package/src/scss/_fit-flyout.scss +76 -0
- package/src/scss/_fonts.scss +36 -36
- package/src/scss/_layout-container.scss +168 -0
- package/src/scss/_layout-responsive.scss +181 -0
- package/src/scss/_navbar-elements.scss +419 -0
- package/src/scss/_navbar.scss +120 -0
- package/src/scss/_pa-grid.scss +43 -43
- package/src/scss/_resize-handle.scss +76 -0
- package/src/scss/_sidebar-states.scss +243 -0
- package/src/scss/_sidebar.scss +420 -0
- package/src/scss/base.scss +2 -3
- package/src/scss/grid.scss +1 -1
- package/src/scss/pure-css.scss +20 -5
- package/src/scss/reboot.scss +5 -5
- package/src/scss/scrollbars.scss +10 -10
- package/src/scss/utilities.scss +791 -758
- package/src/scss/variables/_base.scss +232 -232
- package/src/scss/variables/_components.scss +840 -835
- package/src/scss/variables/_layout.scss +62 -62
- package/src/scss/variables/_system.scss +1 -1
- package/src/scss/variables/_typography.scss +37 -37
package/src/js/fit.js
ADDED
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure CSS — Fit (priority-driven container degradation)
|
|
3
|
+
*
|
|
4
|
+
* The one priority-driven degradation engine. Folds EVERY slot in a horizontal
|
|
5
|
+
* container: brand, version, page title, a search box, actions — or, on any flex
|
|
6
|
+
* row (a toolbar, a filter bar, a form's action row), whatever you put in it. It
|
|
7
|
+
* ALSO folds NAV MENU items (the former navbar-collapse.js, now merged here — see
|
|
8
|
+
* the NAV COLLAPSE section below). When the row can't fit all its content, slots
|
|
9
|
+
* degrade one at a time, LOWEST PRIORITY FIRST, each using its declared strategy,
|
|
10
|
+
* until the row fits. When space returns, everything restores.
|
|
11
|
+
*
|
|
12
|
+
* The navbar (`.pc-navbar__inner`) auto-inits; any other container opts in with
|
|
13
|
+
* `pureCss.components.fit.init(el)` (aliased `navFit` for back-compat).
|
|
14
|
+
*
|
|
15
|
+
* Declare participation with attributes on any element inside the container:
|
|
16
|
+
*
|
|
17
|
+
* data-pc-fit="hide" — remove the slot when it must yield.
|
|
18
|
+
* data-pc-fit="steps" — the slot holds ranked variants; show the LARGEST
|
|
19
|
+
* that fits, degrading step 0 → 1 → 2 … → hidden.
|
|
20
|
+
* The A/B/C idea: e.g. full logo → wordmark →
|
|
21
|
+
* monogram, or a search pill → just its icon.
|
|
22
|
+
* data-pc-fit="relocate" — move the slot OUT of the row into a named
|
|
23
|
+
* destination when it must yield, restored on widen.
|
|
24
|
+
* WHERE it goes is a registered sink named by
|
|
25
|
+
* data-pc-fit-target ("sidebar" | "floating-menu" |
|
|
26
|
+
* a custom sink you register). fit only DECIDES;
|
|
27
|
+
* the sink PLACES. `data-pc-fit="sidebar"` is sugar
|
|
28
|
+
* for relocate + target=sidebar.
|
|
29
|
+
*
|
|
30
|
+
* data-pc-fit-priority="N" — degrade order; LOWER degrades first. When omitted,
|
|
31
|
+
* resolves to the nearest ancestor's
|
|
32
|
+
* data-pc-fit-default-priority, else
|
|
33
|
+
* pureCss.config.fit.defaultPriority, else 0.
|
|
34
|
+
* data-pc-fit-step="0" — on a `steps` slot's DIRECT children; 0 = largest
|
|
35
|
+
* / the default. Numbers, ascending = smaller.
|
|
36
|
+
* data-pc-fit-target="sidebar" — (relocate) which sink places the slot.
|
|
37
|
+
* data-pc-fit-target-selector="#sel" — (relocate) destination element for sinks
|
|
38
|
+
* that need one (sidebar: the <ul> to move
|
|
39
|
+
* into; default: first `.pc-sidebar__nav > ul`).
|
|
40
|
+
* data-pc-fit-managed — (relocate) HANDS-OFF: fit fires the event
|
|
41
|
+
* + hides the slot in-row but does NOT move
|
|
42
|
+
* the DOM. A framework wrapper owns placement
|
|
43
|
+
* and re-renders the block from state (fresh
|
|
44
|
+
* data, never a stale moved node). Same effect
|
|
45
|
+
* as calling preventDefault() on the event.
|
|
46
|
+
*
|
|
47
|
+
* Group opt-in / opt-out (so you don't have to tag every child):
|
|
48
|
+
*
|
|
49
|
+
* data-pc-fit-auto — on a CONTAINER: fold ALL its direct children into
|
|
50
|
+
* the fit set. Children with their own data-pc-fit
|
|
51
|
+
* keep it; the rest become implicit `hide` slots at
|
|
52
|
+
* the default priority (so an un-ranked control drops
|
|
53
|
+
* first). Use for a toolbar/filter bar you want to
|
|
54
|
+
* shrink without ranking each button.
|
|
55
|
+
* data-pc-fit-ignore — PIN this element: never a slot, even next to
|
|
56
|
+
* declared siblings or inside a data-pc-fit-auto
|
|
57
|
+
* container. Use for a burger, a notification bell,
|
|
58
|
+
* a profile avatar, a form's submit button.
|
|
59
|
+
* data-pc-fit-default-priority="N" — on a container: the priority implicit /
|
|
60
|
+
* un-ranked descendants inherit (overrides the global
|
|
61
|
+
* config default for that subtree).
|
|
62
|
+
*
|
|
63
|
+
* Example:
|
|
64
|
+
* <div class="pc-app-header">
|
|
65
|
+
* <span data-pc-fit="steps" data-pc-fit-priority="30">
|
|
66
|
+
* <span data-pc-fit-step="0">Pure Admin</span> <!-- widest, default -->
|
|
67
|
+
* <span data-pc-fit-step="1">PA</span> <!-- narrowest -->
|
|
68
|
+
* </span>
|
|
69
|
+
* <span class="pc-app-header__version" data-pc-fit="hide" data-pc-fit-priority="10">v2.9.0</span>
|
|
70
|
+
* </div>
|
|
71
|
+
*
|
|
72
|
+
* Measurement: the row's "needed" width is the sum of the fit container's direct
|
|
73
|
+
* section scrollWidths (+ gaps). scrollWidth reports each section's CONTENT width
|
|
74
|
+
* even when a flex child is squeezed — so a title clipped behind the brand still
|
|
75
|
+
* counts its true width and triggers a degrade before anything overlaps. The
|
|
76
|
+
* algorithm is RESET-THEN-DEGRADE: every pass restores all slots to natural,
|
|
77
|
+
* then degrades to fit. That makes the result a pure function of the width (no
|
|
78
|
+
* hysteresis, no oscillation) and sidesteps the "freed space is absorbed by the
|
|
79
|
+
* flex:1 centre slot, so a restore is undetectable" trap.
|
|
80
|
+
*
|
|
81
|
+
* Nav folding is coordinated in-engine: the header relayout pre-folds every
|
|
82
|
+
* fit-managed nav (relayoutAllNav) before it measures, so it never over-degrades
|
|
83
|
+
* the header to fit items the nav sheds anyway.
|
|
84
|
+
*
|
|
85
|
+
* Relocation events (on the slot element, bubbling):
|
|
86
|
+
* pc:fit-relocate — CustomEvent, cancelable. detail = { action:'out'|'in',
|
|
87
|
+
* target, container }. Fires once per flip. preventDefault()
|
|
88
|
+
* (or data-pc-fit-managed) → fit performs NO DOM move; the
|
|
89
|
+
* listener owns placement.
|
|
90
|
+
*
|
|
91
|
+
* Public API (on window.pureCss.components.fit, alias .navFit):
|
|
92
|
+
* init(container) — wire one fit container (idempotent)
|
|
93
|
+
* initAll(scope) — wire every navbar fit container under scope
|
|
94
|
+
* relayoutAll() — force a re-measure (e.g. after markup changes)
|
|
95
|
+
* registerSink(name, sink) — add a relocation destination. sink =
|
|
96
|
+
* { out(el, ctx) -> mount|false, in(el, ctx) }.
|
|
97
|
+
* Built-ins: 'sidebar', 'floating-menu'.
|
|
98
|
+
*/
|
|
99
|
+
(function () {
|
|
100
|
+
'use strict';
|
|
101
|
+
|
|
102
|
+
var CONTAINER_SELECTOR = '.pc-navbar__inner';
|
|
103
|
+
// Toggled to hide a slot / an inactive step. A class (not inline display) so
|
|
104
|
+
// "show" reverts to the element's natural stylesheet display; `!important` so
|
|
105
|
+
// it beats component rules like `.pc-navbar-search { display: flex }`.
|
|
106
|
+
var HIDDEN_CLASS = 'pc-fit-hidden';
|
|
107
|
+
var containers = [];
|
|
108
|
+
|
|
109
|
+
function stepIndex(el) {
|
|
110
|
+
return parseFloat(el.getAttribute('data-pc-fit-step')) || 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isIgnored(el) {
|
|
114
|
+
return el.nodeType === 1 && el.hasAttribute('data-pc-fit-ignore');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// The priority a slot without its own data-pc-fit-priority falls back to.
|
|
118
|
+
function configDefaultPriority() {
|
|
119
|
+
var pa = window.pureCss;
|
|
120
|
+
var v = pa && pa.config && pa.config.fit && pa.config.fit.defaultPriority;
|
|
121
|
+
return typeof v === 'number' && !isNaN(v) ? v : 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Resolve a slot's priority: its own data-pc-fit-priority wins; else the
|
|
125
|
+
// nearest ancestor (up to and including the container) carrying
|
|
126
|
+
// data-pc-fit-default-priority; else the global config default; else 0.
|
|
127
|
+
function resolvePriority(el, container) {
|
|
128
|
+
if (el.hasAttribute('data-pc-fit-priority')) {
|
|
129
|
+
var own = parseFloat(el.getAttribute('data-pc-fit-priority'));
|
|
130
|
+
if (!isNaN(own)) return own;
|
|
131
|
+
}
|
|
132
|
+
var node = el.parentNode;
|
|
133
|
+
while (node && node.nodeType === 1) {
|
|
134
|
+
if (node.hasAttribute('data-pc-fit-default-priority')) {
|
|
135
|
+
var d = parseFloat(node.getAttribute('data-pc-fit-default-priority'));
|
|
136
|
+
if (!isNaN(d)) return d;
|
|
137
|
+
}
|
|
138
|
+
if (node === container) break;
|
|
139
|
+
node = node.parentNode;
|
|
140
|
+
}
|
|
141
|
+
return configDefaultPriority();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Build one slot descriptor. Declared slots read their strategy/steps from the
|
|
145
|
+
// markup; implicit slots (folded in by a data-pc-fit-auto container) are always
|
|
146
|
+
// a plain `hide`.
|
|
147
|
+
function describeSlot(el, container, implicit) {
|
|
148
|
+
var strategy = 'hide';
|
|
149
|
+
var steps = [];
|
|
150
|
+
var target = null;
|
|
151
|
+
if (!implicit) {
|
|
152
|
+
strategy = el.getAttribute('data-pc-fit') || 'hide';
|
|
153
|
+
// 'sidebar' is sugar for relocate → the built-in 'sidebar' sink.
|
|
154
|
+
if (strategy === 'sidebar') { strategy = 'relocate'; target = 'sidebar'; }
|
|
155
|
+
if (strategy !== 'hide' && strategy !== 'steps' && strategy !== 'relocate') strategy = 'hide';
|
|
156
|
+
if (strategy === 'relocate') {
|
|
157
|
+
target = el.getAttribute('data-pc-fit-target') || target || 'sidebar';
|
|
158
|
+
}
|
|
159
|
+
if (strategy === 'steps') {
|
|
160
|
+
var children = el.children;
|
|
161
|
+
for (var c = 0; c < children.length; c++) {
|
|
162
|
+
if (children[c].hasAttribute('data-pc-fit-step')) steps.push(children[c]);
|
|
163
|
+
}
|
|
164
|
+
steps.sort(function (a, b) { return stepIndex(a) - stepIndex(b); });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
el: el,
|
|
169
|
+
strategy: strategy,
|
|
170
|
+
target: target, // relocate: name of the destination sink
|
|
171
|
+
priority: resolvePriority(el, container),
|
|
172
|
+
steps: steps,
|
|
173
|
+
domIndex: 0, // assigned after the document-order sort below
|
|
174
|
+
state: 0, // current degradation level
|
|
175
|
+
// maxState: how far a slot can degrade.
|
|
176
|
+
maxState: strategy === 'steps' ? steps.length /* last step + then hidden */ : 1
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Collect participating slots under a container. Two sources:
|
|
181
|
+
// 1. DECLARED — any [data-pc-fit] element (its strategy/priority as written).
|
|
182
|
+
// 2. IMPLICIT — the direct children of any [data-pc-fit-auto] container that
|
|
183
|
+
// didn't declare their own data-pc-fit; folded in as `hide` @ default
|
|
184
|
+
// priority so an un-ranked control yields first.
|
|
185
|
+
// data-pc-fit-ignore excludes an element from both. domIndex is assigned in
|
|
186
|
+
// document order so equal-priority ties break "later element first" (trailing
|
|
187
|
+
// decoration yields before leading content).
|
|
188
|
+
function collectSlots(container) {
|
|
189
|
+
var i, k;
|
|
190
|
+
|
|
191
|
+
var declared = [];
|
|
192
|
+
var els = container.querySelectorAll('[data-pc-fit]');
|
|
193
|
+
for (i = 0; i < els.length; i++) {
|
|
194
|
+
if (!isIgnored(els[i])) declared.push(els[i]);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Armed containers: every [data-pc-fit-auto] inside, plus the container
|
|
198
|
+
// itself if it carries the attribute.
|
|
199
|
+
var autoParents = [];
|
|
200
|
+
var autos = container.querySelectorAll('[data-pc-fit-auto]');
|
|
201
|
+
for (i = 0; i < autos.length; i++) autoParents.push(autos[i]);
|
|
202
|
+
if (container.nodeType === 1 && container.hasAttribute('data-pc-fit-auto')) {
|
|
203
|
+
autoParents.push(container);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
var implicit = [];
|
|
207
|
+
for (i = 0; i < autoParents.length; i++) {
|
|
208
|
+
var kids = autoParents[i].children;
|
|
209
|
+
for (k = 0; k < kids.length; k++) {
|
|
210
|
+
var kid = kids[k];
|
|
211
|
+
if (kid.hasAttribute('data-pc-fit')) continue; // already a declared slot
|
|
212
|
+
if (isIgnored(kid)) continue; // pinned out
|
|
213
|
+
if (implicit.indexOf(kid) === -1) implicit.push(kid);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
var slots = [];
|
|
218
|
+
for (i = 0; i < declared.length; i++) slots.push(describeSlot(declared[i], container, false));
|
|
219
|
+
for (i = 0; i < implicit.length; i++) slots.push(describeSlot(implicit[i], container, true));
|
|
220
|
+
|
|
221
|
+
slots.sort(function (a, b) {
|
|
222
|
+
var pos = a.el.compareDocumentPosition(b.el);
|
|
223
|
+
if (pos & 4 /* DOCUMENT_POSITION_FOLLOWING */) return -1; // a precedes b
|
|
224
|
+
if (pos & 2 /* DOCUMENT_POSITION_PRECEDING */) return 1;
|
|
225
|
+
return 0;
|
|
226
|
+
});
|
|
227
|
+
for (i = 0; i < slots.length; i++) slots[i].domIndex = i;
|
|
228
|
+
|
|
229
|
+
return slots;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// The furthest-along degrade first: pick the lowest-priority slot not yet at
|
|
233
|
+
// its maxState. Tie-break: later DOM position degrades first.
|
|
234
|
+
function nextToDegrade(slots) {
|
|
235
|
+
var best = null;
|
|
236
|
+
for (var i = 0; i < slots.length; i++) {
|
|
237
|
+
var s = slots[i];
|
|
238
|
+
if (s.state >= s.maxState) continue;
|
|
239
|
+
if (!best ||
|
|
240
|
+
s.priority < best.priority ||
|
|
241
|
+
(s.priority === best.priority && s.domIndex > best.domIndex)) {
|
|
242
|
+
best = s;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return best;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function setHidden(el, hidden) {
|
|
249
|
+
if (hidden) el.classList.add(HIDDEN_CLASS);
|
|
250
|
+
else el.classList.remove(HIDDEN_CLASS);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Apply a slot's MEASUREMENT state — class toggles only, no DOM moves.
|
|
254
|
+
// (Sidebar relocation is reconciled once, after the loop settles.)
|
|
255
|
+
function applyMeasureState(slot) {
|
|
256
|
+
var s = slot.state;
|
|
257
|
+
if (slot.strategy === 'steps') {
|
|
258
|
+
for (var i = 0; i < slot.steps.length; i++) {
|
|
259
|
+
setHidden(slot.steps[i], i !== s);
|
|
260
|
+
}
|
|
261
|
+
// Past the last step → the whole slot is hidden.
|
|
262
|
+
setHidden(slot.el, s >= slot.steps.length);
|
|
263
|
+
} else if (slot.strategy === 'hide') {
|
|
264
|
+
setHidden(slot.el, s >= 1);
|
|
265
|
+
} else if (slot.strategy === 'relocate') {
|
|
266
|
+
// Every relayout starts by pulling relocated nodes home (resetAll), so a
|
|
267
|
+
// relocate slot is always in-row during measurement. Hidden here means
|
|
268
|
+
// "won't fit — will move to its sink (or a managed wrapper renders it
|
|
269
|
+
// elsewhere)". reconcileRelocate acts on the settled state afterwards.
|
|
270
|
+
setHidden(slot.el, s >= 1);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// --- Relocation: fit decides, a SINK places -----------------------------
|
|
275
|
+
// fit.js only decides a slot must yield; WHERE it goes is a registered sink
|
|
276
|
+
// keyed by data-pc-fit-target. A sink is:
|
|
277
|
+
// out(el, ctx) -> mount | false place `el`; return an opaque mount (passed
|
|
278
|
+
// back to in), or false to refuse (→ acts
|
|
279
|
+
// like `hide`). Do NOT manage `el`'s home —
|
|
280
|
+
// the engine leaves a placeholder and moves
|
|
281
|
+
// `el` back itself.
|
|
282
|
+
// in(el, ctx) tear down the destination wrapper; ctx.mount
|
|
283
|
+
// is what out() returned. The engine has
|
|
284
|
+
// already moved `el` back to its placeholder.
|
|
285
|
+
// Before invoking a sink, fit fires a cancelable `pc:fit-relocate` on the
|
|
286
|
+
// element. A framework (svelte/phoenix) preventDefault()s it — or the slot /
|
|
287
|
+
// container carries data-pc-fit-managed — to own placement itself: it hides
|
|
288
|
+
// in-row (already done) and re-renders the block from state, so restored
|
|
289
|
+
// content is always fresh, never a stale moved node.
|
|
290
|
+
var sinks = {};
|
|
291
|
+
function registerSink(name, sink) { if (name && sink) sinks[name] = sink; }
|
|
292
|
+
|
|
293
|
+
function isManaged(slot, container) {
|
|
294
|
+
return slot.el.hasAttribute('data-pc-fit-managed') ||
|
|
295
|
+
(container.nodeType === 1 && container.hasAttribute('data-pc-fit-managed'));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function dispatchRelocate(slot, action, container) {
|
|
299
|
+
var detail = { action: action, target: slot.target, container: container };
|
|
300
|
+
var ev;
|
|
301
|
+
try {
|
|
302
|
+
ev = new CustomEvent('pc:fit-relocate', { bubbles: true, cancelable: true, detail: detail });
|
|
303
|
+
} catch (e) {
|
|
304
|
+
ev = document.createEvent('CustomEvent');
|
|
305
|
+
ev.initCustomEvent('pc:fit-relocate', true, true, detail);
|
|
306
|
+
}
|
|
307
|
+
slot.el.dispatchEvent(ev);
|
|
308
|
+
return ev;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Physically move a slot out through its sink, leaving a placeholder comment
|
|
312
|
+
// at its home so it restores to the exact spot. Returns a record, or null if
|
|
313
|
+
// the sink refused / is unknown (slot then behaves like `hide`).
|
|
314
|
+
function relocateOut(slot, container) {
|
|
315
|
+
var el = slot.el, sink = sinks[slot.target];
|
|
316
|
+
if (!sink) return null;
|
|
317
|
+
var placeholder = document.createComment('pa-fit-slot');
|
|
318
|
+
el.parentNode.insertBefore(placeholder, el);
|
|
319
|
+
setHidden(el, false); // visible in its new home
|
|
320
|
+
var mount = sink.out(el, { container: container, target: slot.target });
|
|
321
|
+
if (mount === false || mount == null) {
|
|
322
|
+
// Refused (e.g. no sidebar present). Undo: pull el back, drop placeholder,
|
|
323
|
+
// hide in row.
|
|
324
|
+
if (el.parentNode !== placeholder.parentNode) placeholder.parentNode.insertBefore(el, placeholder);
|
|
325
|
+
placeholder.parentNode.removeChild(placeholder);
|
|
326
|
+
setHidden(el, true);
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
return { el: el, placeholder: placeholder, sink: sink, target: slot.target, mount: mount };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Move a relocated node home and tear down its sink wrapper.
|
|
333
|
+
function restoreRec(rec) {
|
|
334
|
+
if (rec.placeholder && rec.placeholder.parentNode) {
|
|
335
|
+
rec.placeholder.parentNode.insertBefore(rec.el, rec.placeholder);
|
|
336
|
+
rec.placeholder.parentNode.removeChild(rec.placeholder);
|
|
337
|
+
}
|
|
338
|
+
try { if (rec.sink && rec.sink.in) rec.sink.in(rec.el, { target: rec.target, mount: rec.mount }); } catch (e) {}
|
|
339
|
+
setHidden(rec.el, false);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Pull every relocated node back home so the next measure sees natural width
|
|
343
|
+
// and collectSlots finds the full set again. This is what makes relocation a
|
|
344
|
+
// pure function of width (restore-on-widen falls out for free).
|
|
345
|
+
function resetAll(entry) {
|
|
346
|
+
if (!entry.relocs || !entry.relocs.length) return;
|
|
347
|
+
var recs = entry.relocs; entry.relocs = [];
|
|
348
|
+
for (var i = 0; i < recs.length; i++) restoreRec(recs[i]);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// After the degrade loop settles, relocate slots whose state says "out".
|
|
352
|
+
// Events fire only on a real flip (deduped via el.__paFitOut) so a framework
|
|
353
|
+
// listener isn't spammed every resize frame.
|
|
354
|
+
function reconcileRelocate(entry, container, slots) {
|
|
355
|
+
slots.forEach(function (slot) {
|
|
356
|
+
if (slot.strategy !== 'relocate') return;
|
|
357
|
+
var wantOut = slot.state >= 1;
|
|
358
|
+
var was = slot.el.__paFitOut === true;
|
|
359
|
+
if (wantOut !== was) dispatchRelocateFlip(slot, wantOut, container, entry);
|
|
360
|
+
else if (wantOut && !isManaged(slot, container)) {
|
|
361
|
+
// No user-visible flip, but resetAll pulled it home this pass — put it
|
|
362
|
+
// back out (no event) so a stable narrow width keeps it relocated.
|
|
363
|
+
var rec = relocateOut(slot, container);
|
|
364
|
+
if (rec) entry.relocs.push(rec);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function dispatchRelocateFlip(slot, wantOut, container, entry) {
|
|
370
|
+
var ev = dispatchRelocate(slot, wantOut ? 'out' : 'in', container);
|
|
371
|
+
slot.el.__paFitOut = wantOut;
|
|
372
|
+
// Managed / prevented: the framework owns placement. Node stays hidden
|
|
373
|
+
// in-row (applyMeasureState); nothing to move. Restore ('in') is likewise
|
|
374
|
+
// the framework's job.
|
|
375
|
+
if (ev.defaultPrevented || isManaged(slot, container)) return;
|
|
376
|
+
if (wantOut) {
|
|
377
|
+
var rec = relocateOut(slot, container);
|
|
378
|
+
if (rec) entry.relocs.push(rec);
|
|
379
|
+
}
|
|
380
|
+
// Non-managed 'in' already happened in resetAll() at the top of relayout.
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// --- Built-in sink: sidebar --------------------------------------------
|
|
384
|
+
// Rebuilds the slot as a sidebar list item under the app sidebar's nav <ul>.
|
|
385
|
+
// Target <ul>: data-pc-fit-target-selector (or legacy data-pc-fit-sidebar-target),
|
|
386
|
+
// else the first `.pc-sidebar__nav > ul`.
|
|
387
|
+
registerSink('sidebar', {
|
|
388
|
+
out: function (el, ctx) {
|
|
389
|
+
var sel = el.getAttribute('data-pc-fit-target-selector') || el.getAttribute('data-pc-fit-sidebar-target');
|
|
390
|
+
var ul = sel ? document.querySelector(sel) : document.querySelector('.pc-sidebar__nav > ul');
|
|
391
|
+
if (!ul) return false;
|
|
392
|
+
var li = document.createElement('li');
|
|
393
|
+
li.className = 'pc-sidebar__item pc-fit-relocated';
|
|
394
|
+
li.appendChild(el);
|
|
395
|
+
ul.appendChild(li);
|
|
396
|
+
return { wrapper: li };
|
|
397
|
+
},
|
|
398
|
+
in: function (el, ctx) {
|
|
399
|
+
var li = ctx.mount && ctx.mount.wrapper; // el already moved home by the engine
|
|
400
|
+
if (li && li.parentNode) li.parentNode.removeChild(li);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// --- Built-in sink: floating-menu --------------------------------------
|
|
405
|
+
// Self-contained flyout — needs no sidebar, so it works on sidebar-less pages.
|
|
406
|
+
// One trigger (pinned in the container, ignored by fit) + one panel (on body)
|
|
407
|
+
// per container, created lazily and hidden when empty.
|
|
408
|
+
var flyouts = [];
|
|
409
|
+
function flyoutFor(container) {
|
|
410
|
+
for (var i = 0; i < flyouts.length; i++) if (flyouts[i].container === container) return flyouts[i];
|
|
411
|
+
var trigger = document.createElement('button');
|
|
412
|
+
trigger.type = 'button';
|
|
413
|
+
trigger.className = 'pc-fit-flyout__trigger';
|
|
414
|
+
trigger.setAttribute('data-pc-fit-ignore', '');
|
|
415
|
+
trigger.setAttribute('aria-label', 'More');
|
|
416
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
417
|
+
trigger.style.display = 'none';
|
|
418
|
+
trigger.innerHTML = '<span class="pc-fit-flyout__dots" aria-hidden="true"></span>';
|
|
419
|
+
var panel = document.createElement('div');
|
|
420
|
+
panel.className = 'pc-fit-flyout__panel';
|
|
421
|
+
var f = { container: container, trigger: trigger, panel: panel, count: 0 };
|
|
422
|
+
function reposition() {
|
|
423
|
+
var r = trigger.getBoundingClientRect();
|
|
424
|
+
panel.style.top = (r.bottom + 4) + 'px';
|
|
425
|
+
panel.style.left = 'auto';
|
|
426
|
+
panel.style.right = Math.max(4, window.innerWidth - r.right) + 'px';
|
|
427
|
+
}
|
|
428
|
+
function close() { panel.classList.remove('pc-fit-flyout__panel--open'); trigger.setAttribute('aria-expanded', 'false'); }
|
|
429
|
+
function open() { reposition(); panel.classList.add('pc-fit-flyout__panel--open'); trigger.setAttribute('aria-expanded', 'true'); }
|
|
430
|
+
trigger.addEventListener('click', function (e) {
|
|
431
|
+
e.stopPropagation();
|
|
432
|
+
if (panel.classList.contains('pc-fit-flyout__panel--open')) close(); else open();
|
|
433
|
+
});
|
|
434
|
+
document.addEventListener('click', function (e) {
|
|
435
|
+
if (e.target !== trigger && !trigger.contains(e.target) && !panel.contains(e.target)) close();
|
|
436
|
+
});
|
|
437
|
+
window.addEventListener('resize', function () {
|
|
438
|
+
if (panel.classList.contains('pc-fit-flyout__panel--open')) reposition();
|
|
439
|
+
});
|
|
440
|
+
f.close = close;
|
|
441
|
+
container.appendChild(trigger);
|
|
442
|
+
document.body.appendChild(panel);
|
|
443
|
+
flyouts.push(f);
|
|
444
|
+
return f;
|
|
445
|
+
}
|
|
446
|
+
registerSink('floating-menu', {
|
|
447
|
+
out: function (el, ctx) {
|
|
448
|
+
var f = flyoutFor(ctx.container);
|
|
449
|
+
var item = document.createElement('div');
|
|
450
|
+
item.className = 'pc-fit-flyout__item pc-fit-relocated';
|
|
451
|
+
item.appendChild(el);
|
|
452
|
+
f.panel.appendChild(item);
|
|
453
|
+
f.count++;
|
|
454
|
+
f.trigger.style.display = '';
|
|
455
|
+
return { wrapper: item, flyout: f };
|
|
456
|
+
},
|
|
457
|
+
in: function (el, ctx) {
|
|
458
|
+
var m = ctx.mount || {};
|
|
459
|
+
if (m.wrapper && m.wrapper.parentNode) m.wrapper.parentNode.removeChild(m.wrapper);
|
|
460
|
+
if (m.flyout) {
|
|
461
|
+
m.flyout.count = Math.max(0, m.flyout.count - 1);
|
|
462
|
+
if (m.flyout.count === 0) { m.flyout.trigger.style.display = 'none'; m.flyout.close && m.flyout.close(); }
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// A section's CONTENT width. For a flex-GROWING section (the centre slot,
|
|
468
|
+
// flex:1) scrollWidth reports the inflated box, not the content — so removing
|
|
469
|
+
// content makes it grow and read *wider*, which would make the engine fight
|
|
470
|
+
// itself and over-degrade. For those, sum the children instead; for fixed
|
|
471
|
+
// sections (flex-grow:0) scrollWidth already equals content.
|
|
472
|
+
function contentWidth(el) {
|
|
473
|
+
if (parseFloat(getComputedStyle(el).flexGrow) > 0) {
|
|
474
|
+
var w = 0, kids = el.children;
|
|
475
|
+
for (var i = 0; i < kids.length; i++) {
|
|
476
|
+
if (getComputedStyle(kids[i]).display !== 'none') w += kids[i].scrollWidth;
|
|
477
|
+
}
|
|
478
|
+
return w;
|
|
479
|
+
}
|
|
480
|
+
return el.scrollWidth;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function measure(container) {
|
|
484
|
+
var cs = getComputedStyle(container);
|
|
485
|
+
var gap = parseFloat(cs.columnGap) || parseFloat(cs.gap) || 0;
|
|
486
|
+
var padX = (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0);
|
|
487
|
+
var avail = container.clientWidth - padX;
|
|
488
|
+
var kids = container.children, need = 0, vis = 0;
|
|
489
|
+
for (var i = 0; i < kids.length; i++) {
|
|
490
|
+
if (getComputedStyle(kids[i]).display === 'none') continue;
|
|
491
|
+
need += contentWidth(kids[i]);
|
|
492
|
+
vis++;
|
|
493
|
+
}
|
|
494
|
+
need += gap * Math.max(0, vis - 1);
|
|
495
|
+
return { avail: avail, need: need };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function overflowing(container) {
|
|
499
|
+
var m = measure(container);
|
|
500
|
+
return m.need > m.avail + 1; // 1px tolerance for sub-pixel rounding
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function relayout(entry) {
|
|
504
|
+
var container = entry.container;
|
|
505
|
+
|
|
506
|
+
// Pull any relocated nodes home FIRST, so we measure natural width and
|
|
507
|
+
// collectSlots sees the full set again (relocation = pure function of width).
|
|
508
|
+
resetAll(entry);
|
|
509
|
+
|
|
510
|
+
var slots = entry.slots = collectSlots(container);
|
|
511
|
+
|
|
512
|
+
// Reset every slot to natural, then degrade until it fits. Deterministic:
|
|
513
|
+
// the settled state is a pure function of the current width.
|
|
514
|
+
slots.forEach(function (s) { s.state = 0; applyMeasureState(s); });
|
|
515
|
+
|
|
516
|
+
// Fold any collapsing nav FIRST (at the natural, widest slot state), so we
|
|
517
|
+
// measure against a nav that's already shed what it can — otherwise we'd
|
|
518
|
+
// over-degrade the header to fit items that fold into the sidebar anyway.
|
|
519
|
+
relayoutAllNav();
|
|
520
|
+
|
|
521
|
+
var guard = 0;
|
|
522
|
+
while (overflowing(container) && guard++ < 100) {
|
|
523
|
+
var s = nextToDegrade(slots);
|
|
524
|
+
if (!s) break; // nothing left to give
|
|
525
|
+
s.state += 1;
|
|
526
|
+
applyMeasureState(s);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
reconcileRelocate(entry, container, slots);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function schedule(entry) {
|
|
533
|
+
if (entry.raf) return;
|
|
534
|
+
entry.raf = (typeof requestAnimationFrame === 'function')
|
|
535
|
+
? requestAnimationFrame(function () { entry.raf = null; relayout(entry); })
|
|
536
|
+
: (relayout(entry), null);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function init(container) {
|
|
540
|
+
if (!container || container.__paFitInit) return;
|
|
541
|
+
// Nothing to manage unless there's a declared slot, an armed sub-container,
|
|
542
|
+
// or the container itself is armed (data-pc-fit-auto with no tagged child).
|
|
543
|
+
var armedSelf = container.nodeType === 1 && container.hasAttribute('data-pc-fit-auto');
|
|
544
|
+
if (!armedSelf && !container.querySelector('[data-pc-fit], [data-pc-fit-auto]')) return;
|
|
545
|
+
container.__paFitInit = true;
|
|
546
|
+
|
|
547
|
+
var entry = { container: container, slots: [], raf: null, relocs: [] };
|
|
548
|
+
containers.push(entry);
|
|
549
|
+
|
|
550
|
+
schedule(entry);
|
|
551
|
+
|
|
552
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
553
|
+
var ro = new ResizeObserver(function () { schedule(entry); });
|
|
554
|
+
ro.observe(container);
|
|
555
|
+
} else if (window.pureCss && window.pureCss.events) {
|
|
556
|
+
window.pureCss.events.on('viewport:resize', function () { schedule(entry); });
|
|
557
|
+
} else {
|
|
558
|
+
window.addEventListener('resize', function () { schedule(entry); });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function initAll(scope) {
|
|
563
|
+
var root = scope || document;
|
|
564
|
+
var els = root.querySelectorAll(CONTAINER_SELECTOR);
|
|
565
|
+
for (var i = 0; i < els.length; i++) init(els[i]);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function relayoutAll() {
|
|
569
|
+
containers.forEach(schedule);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ======================================================================
|
|
573
|
+
// NAV COLLAPSE — a nav is a fit container whose ITEMS degrade (Option A:
|
|
574
|
+
// ported from navbar-collapse.js into the one engine). A nav's <li>s fold,
|
|
575
|
+
// lowest-priority first, into a sink chosen by data-pc-fit-nav
|
|
576
|
+
// ("sidebar" | "menu" | "off"), restoring as space returns.
|
|
577
|
+
//
|
|
578
|
+
// <nav class="pc-navmenu" data-pc-fit-nav="sidebar"
|
|
579
|
+
// data-pc-fit-nav-label="Menu">…
|
|
580
|
+
//
|
|
581
|
+
// Per-<li>: data-pc-fit-nav-priority (lower drops first),
|
|
582
|
+
// data-pc-nav-icon (sidebar icon), data-pc-fit-nav="hide" (drop, don't
|
|
583
|
+
// relocate). Nav config: data-pc-fit-nav-target (sidebar <ul> selector),
|
|
584
|
+
// data-pc-fit-nav-label / -icon, data-pc-fit-nav-more-label (menu).
|
|
585
|
+
//
|
|
586
|
+
// Measurement is NAV-specific — sums the <li> bounding widths vs
|
|
587
|
+
// nav.clientWidth (the nav is overflow:visible so item hover-dropdowns aren't
|
|
588
|
+
// clipped), NOT scrollWidth. This is the key reason it lived in its own file;
|
|
589
|
+
// now it's one engine with a per-container measure.
|
|
590
|
+
// ======================================================================
|
|
591
|
+
var NAV_SELECTOR = '.pc-navmenu[data-pc-fit-nav]';
|
|
592
|
+
var navRelayouts = [];
|
|
593
|
+
|
|
594
|
+
function navDirectChild(parent, tag) {
|
|
595
|
+
for (var c = parent.firstElementChild; c; c = c.nextElementSibling) {
|
|
596
|
+
if (c.tagName && c.tagName.toLowerCase() === tag) return c;
|
|
597
|
+
}
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
function navEscapeHtml(s) {
|
|
601
|
+
return String(s).replace(/[&<>"']/g, function (c) {
|
|
602
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// MENU sink — fold items into a generated "More ▾" nav dropdown (on <body>).
|
|
607
|
+
function navMenuStrategy(nav, ul) {
|
|
608
|
+
var moreLabel = nav.getAttribute('data-pc-fit-nav-more-label') || 'More';
|
|
609
|
+
var moreLi = document.createElement('li');
|
|
610
|
+
moreLi.className = 'pc-navmenu__item pc-navmenu__item--more';
|
|
611
|
+
var moreLink = document.createElement('a');
|
|
612
|
+
moreLink.href = '#';
|
|
613
|
+
moreLink.className = 'pc-navmenu__link';
|
|
614
|
+
moreLink.setAttribute('aria-haspopup', 'true');
|
|
615
|
+
moreLink.setAttribute('aria-expanded', 'false');
|
|
616
|
+
moreLink.innerHTML = navEscapeHtml(moreLabel) +
|
|
617
|
+
' <span class="pc-navmenu__more-chevron" aria-hidden="true">›</span>';
|
|
618
|
+
moreLi.appendChild(moreLink);
|
|
619
|
+
ul.appendChild(moreLi);
|
|
620
|
+
moreLi.style.display = 'none';
|
|
621
|
+
|
|
622
|
+
var menu = document.createElement('ul');
|
|
623
|
+
menu.className = 'pc-navmenu__dropdown pc-navmenu__more-menu';
|
|
624
|
+
menu.setAttribute('role', 'menu');
|
|
625
|
+
document.body.appendChild(menu);
|
|
626
|
+
|
|
627
|
+
function position() {
|
|
628
|
+
var r = moreLink.getBoundingClientRect();
|
|
629
|
+
menu.style.position = 'fixed';
|
|
630
|
+
menu.style.top = (r.bottom + 4) + 'px';
|
|
631
|
+
menu.style.left = 'auto';
|
|
632
|
+
menu.style.right = (window.innerWidth - r.right) + 'px';
|
|
633
|
+
}
|
|
634
|
+
function isOpen() { return menu.classList.contains('pc-navmenu__more-menu--open'); }
|
|
635
|
+
function openMenu() {
|
|
636
|
+
if (menu.children.length === 0) return;
|
|
637
|
+
position();
|
|
638
|
+
menu.classList.add('pc-navmenu__more-menu--open');
|
|
639
|
+
moreLi.classList.add('is-open');
|
|
640
|
+
moreLink.setAttribute('aria-expanded', 'true');
|
|
641
|
+
window.addEventListener('scroll', position, true);
|
|
642
|
+
window.addEventListener('resize', position);
|
|
643
|
+
setTimeout(function () { document.addEventListener('mousedown', onDocClick); }, 0);
|
|
644
|
+
}
|
|
645
|
+
function closeMenu() {
|
|
646
|
+
menu.classList.remove('pc-navmenu__more-menu--open');
|
|
647
|
+
moreLi.classList.remove('is-open');
|
|
648
|
+
moreLink.setAttribute('aria-expanded', 'false');
|
|
649
|
+
window.removeEventListener('scroll', position, true);
|
|
650
|
+
window.removeEventListener('resize', position);
|
|
651
|
+
document.removeEventListener('mousedown', onDocClick);
|
|
652
|
+
}
|
|
653
|
+
function onDocClick(e) {
|
|
654
|
+
if (menu.contains(e.target) || moreLi.contains(e.target)) return;
|
|
655
|
+
closeMenu();
|
|
656
|
+
}
|
|
657
|
+
moreLink.addEventListener('click', function (e) {
|
|
658
|
+
e.preventDefault(); e.stopPropagation();
|
|
659
|
+
if (isOpen()) closeMenu(); else openMenu();
|
|
660
|
+
});
|
|
661
|
+
menu.addEventListener('click', function (e) {
|
|
662
|
+
if (e.target.closest('a')) setTimeout(closeMenu, 0);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
count: function () { return menu.children.length; },
|
|
667
|
+
restore: function (el) { if (el.parentNode === menu) ul.insertBefore(el, moreLi); },
|
|
668
|
+
afterRestore: function () { moreLi.style.display = 'none'; closeMenu(); },
|
|
669
|
+
onFits: function () {},
|
|
670
|
+
onOverflow: function () {},
|
|
671
|
+
collapse: function (el) { moreLi.style.display = ''; menu.appendChild(el); }
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// SIDEBAR sink — build genuine .pc-sidebar__* markup from each nav item under
|
|
676
|
+
// an injected section heading. Leaf → link item; dropdown parent → collapsible
|
|
677
|
+
// toggle group (chevron, starts closed, parent's own page first). The original
|
|
678
|
+
// nav <li> is kept detached and re-inserted on restore.
|
|
679
|
+
function navSidebarStrategy(nav, ul) {
|
|
680
|
+
var targetSel = nav.getAttribute('data-pc-fit-nav-target');
|
|
681
|
+
var target = targetSel ? document.querySelector(targetSel) : document.querySelector('.pc-sidebar__nav > ul');
|
|
682
|
+
var label = nav.getAttribute('data-pc-fit-nav-label') || 'Menu';
|
|
683
|
+
var defaultIcon = nav.getAttribute('data-pc-fit-nav-icon');
|
|
684
|
+
if (defaultIcon == null) defaultIcon = '•';
|
|
685
|
+
|
|
686
|
+
var section = null, divider = null;
|
|
687
|
+
function ensureSection() {
|
|
688
|
+
if (!target) return null;
|
|
689
|
+
if (!section) {
|
|
690
|
+
section = document.createElement('li');
|
|
691
|
+
section.className = 'pc-sidebar__section';
|
|
692
|
+
section.setAttribute('data-pc-nav-injected', '');
|
|
693
|
+
section.textContent = label;
|
|
694
|
+
}
|
|
695
|
+
if (!divider) {
|
|
696
|
+
divider = document.createElement('li');
|
|
697
|
+
divider.className = 'pc-sidebar__divider';
|
|
698
|
+
divider.setAttribute('data-pc-nav-injected', '');
|
|
699
|
+
divider.setAttribute('aria-hidden', 'true');
|
|
700
|
+
}
|
|
701
|
+
if (section.parentNode !== target) target.insertBefore(section, target.firstChild);
|
|
702
|
+
if (divider.parentNode !== target) target.insertBefore(divider, section.nextSibling);
|
|
703
|
+
return section;
|
|
704
|
+
}
|
|
705
|
+
function removeSection() {
|
|
706
|
+
if (section && section.parentNode) section.parentNode.removeChild(section);
|
|
707
|
+
if (divider && divider.parentNode) divider.parentNode.removeChild(divider);
|
|
708
|
+
}
|
|
709
|
+
function labelOf(a) { return (a ? a.textContent : '').replace(/\s*[›»>]+\s*$/, '').trim(); }
|
|
710
|
+
function iconHtml(icon) { return icon ? '<span class="pc-sidebar__icon">' + navEscapeHtml(icon) + '</span>' : ''; }
|
|
711
|
+
function labelHtml(text) { return '<span class="pc-sidebar__label">' + navEscapeHtml(text) + '</span>'; }
|
|
712
|
+
function buildLinkItem(text, href, icon, active) {
|
|
713
|
+
var li = document.createElement('li');
|
|
714
|
+
li.className = 'pc-sidebar__item';
|
|
715
|
+
var a = document.createElement('a');
|
|
716
|
+
a.className = 'pc-sidebar__link' + (active ? ' pc-sidebar__link--active' : '');
|
|
717
|
+
a.setAttribute('href', href == null ? '#' : href);
|
|
718
|
+
a.innerHTML = iconHtml(icon) + labelHtml(text);
|
|
719
|
+
li.appendChild(a);
|
|
720
|
+
return li;
|
|
721
|
+
}
|
|
722
|
+
function buildSidebarItem(navLi) {
|
|
723
|
+
var link = navDirectChild(navLi, 'a');
|
|
724
|
+
var sub = navDirectChild(navLi, 'ul');
|
|
725
|
+
var icon = navLi.getAttribute('data-pc-nav-icon');
|
|
726
|
+
if (icon == null) icon = defaultIcon;
|
|
727
|
+
var text = labelOf(link);
|
|
728
|
+
var href = link ? link.getAttribute('href') : null;
|
|
729
|
+
var active = navLi.classList.contains('pc-navmenu__item--active');
|
|
730
|
+
var isReal = href && href !== '#' && href.charAt(href.length - 1) !== '#';
|
|
731
|
+
if (!sub) return buildLinkItem(text, href, icon, active);
|
|
732
|
+
|
|
733
|
+
var li = document.createElement('li');
|
|
734
|
+
li.className = 'pc-sidebar__item' + (active ? ' pc-sidebar__item--open' : '');
|
|
735
|
+
var btn = document.createElement('button');
|
|
736
|
+
btn.type = 'button';
|
|
737
|
+
btn.className = 'pc-sidebar__toggle';
|
|
738
|
+
btn.setAttribute('aria-expanded', active ? 'true' : 'false');
|
|
739
|
+
btn.innerHTML = iconHtml(icon) + labelHtml(text) +
|
|
740
|
+
'<span class="pc-sidebar__chevron" aria-hidden="true">›</span>';
|
|
741
|
+
li.appendChild(btn);
|
|
742
|
+
var submenu = document.createElement('ul');
|
|
743
|
+
submenu.className = 'pc-sidebar__submenu' + (active ? ' pc-sidebar__submenu--open' : '');
|
|
744
|
+
if (isReal) submenu.appendChild(buildLinkItem(text, href, icon, active));
|
|
745
|
+
for (var c = sub.firstElementChild; c; c = c.nextElementSibling) {
|
|
746
|
+
if (c.tagName && c.tagName.toLowerCase() === 'li') submenu.appendChild(buildSidebarItem(c));
|
|
747
|
+
}
|
|
748
|
+
li.appendChild(submenu);
|
|
749
|
+
btn.addEventListener('click', function () {
|
|
750
|
+
var open = !li.classList.contains('pc-sidebar__item--open');
|
|
751
|
+
li.classList.toggle('pc-sidebar__item--open', open);
|
|
752
|
+
submenu.classList.toggle('pc-sidebar__submenu--open', open);
|
|
753
|
+
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
754
|
+
});
|
|
755
|
+
return li;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
count: function () { return target ? target.querySelectorAll('[data-pc-nav-collapsed]').length : 0; },
|
|
760
|
+
restore: function (el) {
|
|
761
|
+
if (el.__paCollapsed) {
|
|
762
|
+
var node = el.__paSidebarNode;
|
|
763
|
+
if (node && node.parentNode) node.parentNode.removeChild(node);
|
|
764
|
+
el.__paSidebarNode = null;
|
|
765
|
+
el.__paCollapsed = false;
|
|
766
|
+
}
|
|
767
|
+
ul.appendChild(el);
|
|
768
|
+
},
|
|
769
|
+
afterRestore: function () {
|
|
770
|
+
if (target && !target.querySelector('[data-pc-nav-collapsed]')) removeSection();
|
|
771
|
+
},
|
|
772
|
+
onFits: function () { removeSection(); },
|
|
773
|
+
onOverflow: function () {},
|
|
774
|
+
collapse: function (el) {
|
|
775
|
+
if (!target || el.__paCollapsed) return;
|
|
776
|
+
ensureSection();
|
|
777
|
+
var node = buildSidebarItem(el);
|
|
778
|
+
node.setAttribute('data-pc-nav-collapsed', '');
|
|
779
|
+
el.__paSidebarNode = node;
|
|
780
|
+
el.__paCollapsed = true;
|
|
781
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
782
|
+
target.insertBefore(node, section.nextSibling);
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function initNav(nav) {
|
|
788
|
+
if (!nav || nav.__paFitNavInit) return;
|
|
789
|
+
var mode = nav.getAttribute('data-pc-fit-nav') || 'menu';
|
|
790
|
+
if (mode === 'off') return;
|
|
791
|
+
if (mode !== 'menu' && mode !== 'sidebar') mode = 'menu';
|
|
792
|
+
nav.__paFitNavInit = true;
|
|
793
|
+
|
|
794
|
+
var ul = nav.querySelector(':scope > ul');
|
|
795
|
+
if (!ul) return;
|
|
796
|
+
|
|
797
|
+
var items = Array.prototype.slice.call(ul.children).filter(function (el) { return el.nodeType === 1; });
|
|
798
|
+
if (items.length === 0) return;
|
|
799
|
+
|
|
800
|
+
var ordered = items.map(function (el, idx) {
|
|
801
|
+
var raw = parseInt(el.getAttribute('data-pc-fit-nav-priority'), 10);
|
|
802
|
+
var priority = isNaN(raw) ? 0 : raw;
|
|
803
|
+
var hide = el.getAttribute('data-pc-fit-nav') === 'hide';
|
|
804
|
+
return { el: el, priority: priority, domIndex: idx, hide: hide };
|
|
805
|
+
});
|
|
806
|
+
var dropOrder = ordered.slice().sort(function (a, b) {
|
|
807
|
+
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
808
|
+
return b.domIndex - a.domIndex;
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
var strategy = (mode === 'sidebar') ? navSidebarStrategy(nav, ul) : navMenuStrategy(nav, ul);
|
|
812
|
+
|
|
813
|
+
function contentWidth() {
|
|
814
|
+
var cs = getComputedStyle(ul);
|
|
815
|
+
var gap = parseFloat(cs.columnGap) || parseFloat(cs.gap) || 0;
|
|
816
|
+
var total = 0, count = 0, kids = ul.children;
|
|
817
|
+
for (var i = 0; i < kids.length; i++) {
|
|
818
|
+
var k = kids[i];
|
|
819
|
+
if (k.nodeType !== 1) continue;
|
|
820
|
+
if (getComputedStyle(k).display === 'none') continue;
|
|
821
|
+
total += k.getBoundingClientRect().width;
|
|
822
|
+
count++;
|
|
823
|
+
}
|
|
824
|
+
if (count > 1) total += gap * (count - 1);
|
|
825
|
+
return total;
|
|
826
|
+
}
|
|
827
|
+
function overflowing() { return contentWidth() > nav.clientWidth + 1; }
|
|
828
|
+
|
|
829
|
+
function relayout() {
|
|
830
|
+
ordered.slice().sort(function (a, b) { return a.domIndex - b.domIndex; })
|
|
831
|
+
.forEach(function (item) {
|
|
832
|
+
if (item.hide) item.el.style.display = '';
|
|
833
|
+
strategy.restore(item.el);
|
|
834
|
+
});
|
|
835
|
+
strategy.afterRestore();
|
|
836
|
+
|
|
837
|
+
if (!overflowing()) { strategy.onFits(); return; }
|
|
838
|
+
|
|
839
|
+
strategy.onOverflow();
|
|
840
|
+
for (var i = 0; i < dropOrder.length; i++) {
|
|
841
|
+
if (!overflowing()) break;
|
|
842
|
+
if (dropOrder[i].hide) dropOrder[i].el.style.display = 'none';
|
|
843
|
+
else strategy.collapse(dropOrder[i].el);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
navRelayouts.push(relayout);
|
|
848
|
+
(typeof requestAnimationFrame === 'function' ? requestAnimationFrame : function (f) { f(); })(relayout);
|
|
849
|
+
|
|
850
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
851
|
+
var ro = new ResizeObserver(function () { relayout(); });
|
|
852
|
+
ro.observe(nav);
|
|
853
|
+
var inner = nav.closest('.pc-navbar__inner') || nav.parentNode;
|
|
854
|
+
if (inner) ro.observe(inner);
|
|
855
|
+
} else if (window.pureCss && window.pureCss.events) {
|
|
856
|
+
window.pureCss.events.on('viewport:resize', relayout);
|
|
857
|
+
} else {
|
|
858
|
+
window.addEventListener('resize', relayout);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function initAllNav(scope) {
|
|
863
|
+
var root = scope || document;
|
|
864
|
+
var els = root.querySelectorAll(NAV_SELECTOR);
|
|
865
|
+
for (var i = 0; i < els.length; i++) initNav(els[i]);
|
|
866
|
+
}
|
|
867
|
+
function relayoutAllNav() { navRelayouts.forEach(function (fn) { fn(); }); }
|
|
868
|
+
|
|
869
|
+
// Register the two nav sinks by name so a custom nav target is possible and
|
|
870
|
+
// the fit + nav paths share one registry.
|
|
871
|
+
registerSink('nav-sidebar', { nav: navSidebarStrategy });
|
|
872
|
+
registerSink('nav-menu', { nav: navMenuStrategy });
|
|
873
|
+
|
|
874
|
+
var pa = (window.pureCss = window.pureCss || {});
|
|
875
|
+
var api = {
|
|
876
|
+
init: init, initAll: initAll, relayoutAll: relayoutAll, registerSink: registerSink,
|
|
877
|
+
initNav: initNav, initAllNav: initAllNav, relayoutAllNav: relayoutAllNav
|
|
878
|
+
};
|
|
879
|
+
// `fit` is the canonical name (the engine is container-generic now); `navFit`
|
|
880
|
+
// stays as a back-compat alias for existing callers.
|
|
881
|
+
(pa.components = pa.components || {}).fit = api;
|
|
882
|
+
pa.components.navFit = api;
|
|
883
|
+
|
|
884
|
+
if (document.readyState === 'loading') {
|
|
885
|
+
document.addEventListener('DOMContentLoaded', function () { initAll(); initAllNav(); });
|
|
886
|
+
} else {
|
|
887
|
+
initAll();
|
|
888
|
+
initAllNav();
|
|
889
|
+
}
|
|
890
|
+
})();
|