@faqir-ui/core 1.1.1 → 1.2.0
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/README.md +9 -8
- package/cdn.json +41 -36
- package/dist/faqir-core.js +568 -394
- package/dist/faqir-core.min.js +3 -3
- package/dist/faqir-core.min.js.map +3 -3
- package/dist/faqir-core.mjs +651 -532
- package/dist/faqir.aurora.css +6436 -1401
- package/dist/faqir.brutalist.css +6436 -1401
- package/dist/faqir.candy.css +6436 -1401
- package/dist/faqir.clay.css +19164 -0
- package/dist/faqir.clinical.css +6436 -1401
- package/dist/faqir.contrast.css +6436 -1401
- package/dist/faqir.deco.css +19247 -0
- package/dist/faqir.default.css +6436 -1401
- package/dist/faqir.document-serif.css +6436 -1401
- package/dist/faqir.document.css +6437 -1401
- package/dist/faqir.editorial-document.css +6436 -1401
- package/dist/faqir.editorial.css +6436 -1401
- package/dist/faqir.fintech.css +6436 -1401
- package/dist/faqir.glass.css +6436 -1401
- package/dist/faqir.ink-document.css +6436 -1401
- package/dist/faqir.ink.css +6436 -1401
- package/dist/faqir.luxe.css +6436 -1401
- package/dist/faqir.memphis.css +19163 -0
- package/dist/faqir.midnight.css +6436 -1401
- package/dist/faqir.monolith.css +19162 -0
- package/dist/faqir.neo.css +6436 -1401
- package/dist/faqir.neumorph.css +6436 -1401
- package/dist/faqir.nordic.css +6436 -1401
- package/dist/faqir.organic.css +6436 -1401
- package/dist/faqir.paper.css +6436 -1401
- package/dist/faqir.slate.css +6436 -1401
- package/dist/faqir.soft.css +6436 -1401
- package/dist/faqir.sunset.css +6436 -1401
- package/dist/faqir.swiss-document.css +6436 -1401
- package/dist/faqir.swiss.css +6436 -1401
- package/dist/faqir.terminal.css +6436 -1401
- package/dist/plugins/faqir-mask.js +1 -2
- package/dist/plugins/faqir-rules.js +1 -1
- package/dist/plugins/faqir-tweak.js +304 -0
- package/dist/plugins/faqir-validate.js +12 -4
- package/dist/sri.json +39 -34
- package/faqir-core.d.ts +52 -2
- package/package.json +1 -1
package/dist/faqir-core.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// PRODUCTION BUILD — dev-only diagnostics stripped at assembly time.
|
|
11
11
|
// Load faqir-core.dev.js instead while developing for full warnings.
|
|
12
12
|
// Regenerate with: bun run build:core
|
|
13
|
-
// Package version: 1.
|
|
13
|
+
// Package version: 1.2.0
|
|
14
14
|
// ============================================================================
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
function evaluate(expression, scope, el) {
|
|
228
228
|
try {
|
|
229
229
|
const fn = compileExpression(expression);
|
|
230
|
-
return fn.call(scope, scope, el);
|
|
230
|
+
return fn.call(scope, scope, el, el);
|
|
231
231
|
} catch (e) {
|
|
232
232
|
console.warn('[Faqir] Expression error: "' + expression + '"', e);
|
|
233
233
|
return undefined;
|
|
@@ -237,7 +237,7 @@
|
|
|
237
237
|
function evaluateAssignment(expression, scope, el) {
|
|
238
238
|
try {
|
|
239
239
|
const fn = compileStatement(expression);
|
|
240
|
-
fn.call(scope, scope, el);
|
|
240
|
+
fn.call(scope, scope, el, el);
|
|
241
241
|
} catch (e) {
|
|
242
242
|
console.warn('[Faqir] Statement error: "' + expression + '"', e);
|
|
243
243
|
}
|
|
@@ -261,12 +261,17 @@
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// `$el` and `$this` are both the element the directive is written on. Inside
|
|
265
|
+
// `with($scope)` the scope's own `$el` magic (the root) wins, so `$el` only
|
|
266
|
+
// reaches this parameter where nothing shadows it: a top-level `l-data`,
|
|
267
|
+
// evaluated against `{}`. `$this` is never a scope key, so it always reaches
|
|
268
|
+
// it. [1.1F-06]
|
|
264
269
|
function compileExpression(expr) {
|
|
265
270
|
var key = 'expr:' + expr;
|
|
266
271
|
if (expressionCache.has(key)) return expressionCache.get(key);
|
|
267
272
|
|
|
268
273
|
var fn = new Function(
|
|
269
|
-
'$scope', '$el',
|
|
274
|
+
'$scope', '$el', '$this',
|
|
270
275
|
'with($scope) { return (' + expr + ') }'
|
|
271
276
|
);
|
|
272
277
|
expressionCache.set(key, fn);
|
|
@@ -278,7 +283,7 @@
|
|
|
278
283
|
if (expressionCache.has(key)) return expressionCache.get(key);
|
|
279
284
|
|
|
280
285
|
var fn = new Function(
|
|
281
|
-
'$scope', '$el',
|
|
286
|
+
'$scope', '$el', '$this',
|
|
282
287
|
'with($scope) { ' + expr + ' }'
|
|
283
288
|
);
|
|
284
289
|
expressionCache.set(key, fn);
|
|
@@ -316,21 +321,21 @@
|
|
|
316
321
|
// (§3.9) and MOTION_PRESETS (§3.14) — are read from the code itself, not
|
|
317
322
|
// repeated here.
|
|
318
323
|
//
|
|
319
|
-
// @ui:directive l-data | — | any element, which becomes the scope root | l-data="{ count: 0 }" | Declares a reactive scope from an object literal, or from a name registered with `Faqir.data()`. `data-prop-*` attributes are JSON-parsed and merged over it. Descendants share the scope until the next `l-data`.
|
|
320
|
-
// @ui:directive l-init | — | a scope root, beside `l-data` | l-init="load()" | Runs once, after the scope exists and its `l-source` bindings are injected.
|
|
324
|
+
// @ui:directive l-data | — | any element, which becomes the scope root | l-data="{ count: 0 }" | Declares a reactive scope from an object literal, or from a name registered with `Faqir.data()`. `data-prop-*` attributes are JSON-parsed and merged over it. Descendants share the scope until the next `l-data`. Its methods are bound to the scope, so `this` is the component even when an `l-for` row calls them; a function assigned later is not bound.
|
|
325
|
+
// @ui:directive l-init | — | a scope root, beside `l-data` | l-init="load()" | Runs once, after the scope exists and its `l-source` bindings are injected, with the subtree's controllers already started. In `l-if` / `l-for` content it runs after insertion: bindings, insert, controllers, then `l-init` / first `l-effect`.
|
|
321
326
|
// @ui:directive l-source:<name> | — | a scope root, beside `l-data` | l-source:tasks="/api/tasks" | Binds a REST collection into the scope as `<name>`, `<name>Loading`, `<name>Error` and the `$<name>` CRUD controller. See below.
|
|
322
327
|
// @ui:directive l-text | — | any element | l-text="count" | Writes the value to `textContent`; `null` and `undefined` write an empty string.
|
|
323
328
|
// @ui:directive l-html | — | any element | l-html="body" | Writes the value to `innerHTML`, unsanitized — never pass user input. The dev engine reports every use.
|
|
324
|
-
// @ui:directive l-bind:<attr> | :<attr> | any element | :disabled="loading" | Binds one attribute. `class` and `style` take a string, an array or an object
|
|
329
|
+
// @ui:directive l-bind:<attr> | :<attr> | any element | :disabled="loading" | Binds one attribute. `class` and `style` take a string, an array or an object and merge with what is there: each run removes only what its previous run set, so static markup, `l-show` and controllers keep theirs. Boolean attributes are added or removed; `null`, `undefined` and `false` remove the attribute.
|
|
325
330
|
// @ui:directive l-on:<event> | @<event> | any element | @click="count++" | Adds a listener for `<event>`. Inside the expression `$event` is the DOM event.
|
|
326
331
|
// @ui:directive l-model | — | an input, textarea or select, or `[data-ui="switch"]` | l-model="name" | Two-way binding. A checkbox bound to an array is a checkbox group; radios bind by value; `<select multiple>` is not handled. The modifiers below apply to the text-like branch (input, textarea, number) only.
|
|
327
332
|
// @ui:directive l-show | — | any element | l-show="open" | Toggles `display: none`, leaving the element in the DOM. Runs `l-transition` on each flip.
|
|
328
333
|
// @ui:directive l-if | — | a `<template>` element | <template l-if="open"> | Inserts and removes the template's content. Removal tears the subtree down: cleanups run and in-flight `l-source` requests abort.
|
|
329
334
|
// @ui:directive l-for | — | a `<template>` element | <template l-for="(task, i) in tasks"> | Repeats the template's content once per item, with the item and index names bound in a child scope.
|
|
330
335
|
// @ui:directive l-key | — | the same `<template>` as `l-for` | l-key="task.id" | The reconciliation key. Without it items are matched by position, so a reorder re-renders rather than moves; the dev engine reports that case.
|
|
331
|
-
// @ui:directive l-ref | — | any element | l-ref="field" | Registers the element on the scope's `$refs` under that name.
|
|
336
|
+
// @ui:directive l-ref | — | any element | l-ref="field" | Registers the element on the scope's `$refs` under that name — also from inside `l-if`, `l-for` and `l-teleport`, where the scope is the enclosing one. In an `l-for` the last row rendered wins, and the ref is cleared when that row goes. Effects that read it re-run when it appears or goes.
|
|
332
337
|
// @ui:directive l-effect | — | any element | l-effect="document.title = title" | Runs the expression immediately, then again whenever a value it read changes.
|
|
333
|
-
// @ui:directive l-cloak | — | any element | l-cloak | Removed from every element once the tree is initialized.
|
|
338
|
+
// @ui:directive l-cloak | — | any element | l-cloak | Removed from every element once the tree is initialized. Faqir's CSS (`base/reset.css`) hides `[l-cloak]` until then; the engine injects no style, so a page without that CSS needs its own `[l-cloak] { display: none }` rule.
|
|
334
339
|
// @ui:directive l-transition | — | an `l-show` element, or a top-level element inside a `<template l-if>` | l-transition="slide-up" | Names the motion preset for that element's enter/leave cycle. The engine only stamps `data-motion`; the CSS animates.
|
|
335
340
|
// @ui:directive l-teleport | — | any element | l-teleport="body" | Moves the element into the first element matching the value, which is a plain CSS selector rather than an expression.
|
|
336
341
|
//
|
|
@@ -354,6 +359,7 @@
|
|
|
354
359
|
// @ui:modifier l-source .key | Names the identity property, `id` by default: `.key.uuid`.
|
|
355
360
|
//
|
|
356
361
|
// @ui:magic $el | every expression | The scope ROOT — the element carrying `l-data`, not the element the expression is written on. Every magic that walks the DOM starts from here.
|
|
362
|
+
// @ui:magic $this | every expression | The element the directive is written on — the `<input>` carrying `l-init`, the button carrying `@click`. In `l-data` and a root's `l-init` it is the root; in `l-for`'s list and `l-key` expressions, the `<template>`. Not a scope key, so `Faqir.magic()` cannot shadow it.
|
|
357
363
|
// @ui:magic $refs | every expression | The scope's `l-ref` elements, keyed by name. Cleared entry by entry as elements are destroyed.
|
|
358
364
|
// @ui:magic $store | every expression | Every store registered with `Faqir.store()`.
|
|
359
365
|
// @ui:magic $state | every expression | `data-state` of the `[data-ui]` closest to the scope root. Writable — assigning sets the attribute — and reads re-run when a controller changes it.
|
|
@@ -478,6 +484,37 @@
|
|
|
478
484
|
// point authors at ("a scope root, beside `l-data`"). [W2-1]
|
|
479
485
|
var ROOT_CONSUMED = { data: 1, source: 1, init: 1 };
|
|
480
486
|
|
|
487
|
+
// `l-init` and each `l-effect`'s first run, held back while a structural
|
|
488
|
+
// render builds content that is not in the document yet. [1.1F-03]
|
|
489
|
+
var pendingInits = null;
|
|
490
|
+
|
|
491
|
+
function runInit(fn) {
|
|
492
|
+
if (pendingInits) pendingInits.push(fn); else fn();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// `l-if` / `l-for` content ran its `l-init` and its effects' first pass while
|
|
496
|
+
// still detached — before insertion, and before any controller inside it had
|
|
497
|
+
// started — so `$ui('#x')` there was null, and an effect that read nothing
|
|
498
|
+
// reactive never ran again. `render` binds and inserts, and returns the nodes
|
|
499
|
+
// it inserted; their controllers start next, and only then does the held work
|
|
500
|
+
// run: bindings → insertion → controllers → `l-init` / `l-effect`. A render
|
|
501
|
+
// nested inside another (an `l-if` in an `l-for` row) joins the outer one,
|
|
502
|
+
// which alone knows when the content is attached. Controllers are not started
|
|
503
|
+
// on content that is still detached. Untracked, so nothing the held work reads
|
|
504
|
+
// subscribes the structural effect that rendered it. [1.1F-03]
|
|
505
|
+
function renderThenInit(render) {
|
|
506
|
+
if (pendingInits) return render();
|
|
507
|
+
var queue = pendingInits = [];
|
|
508
|
+
var nodes;
|
|
509
|
+
try { nodes = render(); } finally { pendingInits = null; }
|
|
510
|
+
untrack(function() {
|
|
511
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
512
|
+
if (nodes[i].nodeType === 1 && nodes[i].isConnected) startControllers(nodes[i]);
|
|
513
|
+
}
|
|
514
|
+
for (var q = 0; q < queue.length; q++) queue[q]();
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
481
518
|
function initTree(root, parentScope) {
|
|
482
519
|
var scope = initScope(root, parentScope);
|
|
483
520
|
|
|
@@ -504,8 +541,10 @@
|
|
|
504
541
|
for (var i = 0; i < children.length; i++) {
|
|
505
542
|
var child = children[i];
|
|
506
543
|
|
|
507
|
-
// Skip elements already initialized by l-for or l-if
|
|
508
|
-
|
|
544
|
+
// Skip elements already initialized by l-for or l-if — or teleported here
|
|
545
|
+
// by an earlier sibling's l-teleport, which bound them (and, with an
|
|
546
|
+
// l-data of their own, scoped them) on the way out.
|
|
547
|
+
if (child.__faqirStray || (child.__faqirScope && !child.hasAttribute('l-data'))) {
|
|
509
548
|
continue;
|
|
510
549
|
}
|
|
511
550
|
|
|
@@ -558,6 +597,17 @@
|
|
|
558
597
|
// at `<body>`. Created once, on first use, so a page with no stray directive
|
|
559
598
|
// pays nothing for it.
|
|
560
599
|
var strayScope = null;
|
|
600
|
+
|
|
601
|
+
// The element a scope's `$refs` live on, keyed by the scope object. Not
|
|
602
|
+
// `findScopeRoot`: `__faqirScope` also marks "already initialised", so an
|
|
603
|
+
// `l-if` clone or an `l-for` row carries it too, and a ref resolved by walking
|
|
604
|
+
// up landed on the clone while `$refs` read the real root — invisible. A
|
|
605
|
+
// teleported element has left the root's subtree, and a stray element has no
|
|
606
|
+
// stamped ancestor at all. A WeakMap rather than a scope property, because a
|
|
607
|
+
// read through the reactive scope inside a structural effect would subscribe
|
|
608
|
+
// it. [1.1F-01]
|
|
609
|
+
var scopeRoots = new WeakMap();
|
|
610
|
+
|
|
561
611
|
function getStrayScope() {
|
|
562
612
|
if (!strayScope) {
|
|
563
613
|
if (!document.body.__scopeId) document.body.__scopeId = ++scopeCounter;
|
|
@@ -567,6 +617,7 @@
|
|
|
567
617
|
// MutationObserver would stop giving standalone `[data-ui]` islands their
|
|
568
618
|
// own scope.
|
|
569
619
|
strayScope = createScopeWithMagics({}, document.body, document.body);
|
|
620
|
+
scopeRoots.set(strayScope, document.body);
|
|
570
621
|
}
|
|
571
622
|
return strayScope;
|
|
572
623
|
}
|
|
@@ -619,6 +670,7 @@
|
|
|
619
670
|
|
|
620
671
|
var scopeId = ++scopeCounter;
|
|
621
672
|
var scope = createScopeWithMagics(userData, root, root);
|
|
673
|
+
scopeRoots.set(scope, root);
|
|
622
674
|
root.__faqirScope = scope;
|
|
623
675
|
root.__scopeId = scopeId;
|
|
624
676
|
root.__faqirCleanups = [];
|
|
@@ -643,7 +695,7 @@
|
|
|
643
695
|
|
|
644
696
|
var initExpr = root.getAttribute('l-init');
|
|
645
697
|
if (initExpr) {
|
|
646
|
-
evaluateAssignment(initExpr, scope, root);
|
|
698
|
+
runInit(function() { evaluateAssignment(initExpr, scope, root); });
|
|
647
699
|
}
|
|
648
700
|
|
|
649
701
|
return scope;
|
|
@@ -733,6 +785,15 @@
|
|
|
733
785
|
Object.defineProperties(target, descriptors);
|
|
734
786
|
|
|
735
787
|
var scope = reactive(target);
|
|
788
|
+
// A bare call `pick()` inside `with(rowScope)` gets the l-for row scope as
|
|
789
|
+
// `this`, and that scope owns the loop variable and index: `this.item = x`
|
|
790
|
+
// wrote the row instead of the component. Bind each own method to the
|
|
791
|
+
// scope. Getters keep their receiver; functions assigned later stay
|
|
792
|
+
// unbound. Written to the target, so nothing is triggered. [1.1F-05]
|
|
793
|
+
for (var k in descriptors) {
|
|
794
|
+
var d = descriptors[k];
|
|
795
|
+
if (d.writable && typeof d.value === 'function') target[k] = d.value.bind(scope);
|
|
796
|
+
}
|
|
736
797
|
return scope;
|
|
737
798
|
}
|
|
738
799
|
|
|
@@ -751,8 +812,10 @@
|
|
|
751
812
|
|
|
752
813
|
// --- Scope utilities ---
|
|
753
814
|
|
|
815
|
+
// Created on first read, so an effect that reads a ref before it exists
|
|
816
|
+
// subscribes to the same object `handleRef` later writes through.
|
|
754
817
|
function getScopeRefs(root) {
|
|
755
|
-
return root.__faqirRefs || {};
|
|
818
|
+
return root.__faqirRefs || (root.__faqirRefs = {});
|
|
756
819
|
}
|
|
757
820
|
|
|
758
821
|
function findScopeRoot(el) {
|
|
@@ -1185,16 +1248,38 @@
|
|
|
1185
1248
|
'open', 'novalidate', 'formnovalidate', 'inert'
|
|
1186
1249
|
]);
|
|
1187
1250
|
|
|
1251
|
+
// Properties whose live state stops following the attribute once the user
|
|
1252
|
+
// has touched the control (the "dirty" flag), so an attribute-only write
|
|
1253
|
+
// leaves a stale control. `muted` and `indeterminate` have no attribute at all.
|
|
1254
|
+
var PROPS = new Set(['checked', 'selected', 'muted', 'indeterminate', 'value']);
|
|
1255
|
+
|
|
1256
|
+
// Write a DOM property only when it differs. `checked`/`selected` writes
|
|
1257
|
+
// notify with a bubbling `faqir:model` — never a native `change`, which would
|
|
1258
|
+
// re-enter l-model's own listener and fire user `@change` handlers on a
|
|
1259
|
+
// store-driven write.
|
|
1260
|
+
function writeProp(el, name, value) {
|
|
1261
|
+
if (!(name in el) || el[name] === value) return;
|
|
1262
|
+
el[name] = value;
|
|
1263
|
+
if (name === 'checked' || name === 'selected') {
|
|
1264
|
+
el.dispatchEvent(new CustomEvent('faqir:model', { bubbles: true }));
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1188
1268
|
function handleBind(el, dir, scope) {
|
|
1189
1269
|
var attrName = dir.arg;
|
|
1270
|
+
// What this binding's last run set on `class`/`style`. Both merge: a run
|
|
1271
|
+
// applies its own classes/declarations and removes only the ones its
|
|
1272
|
+
// previous run set and this one dropped, so static markup, `l-show` and
|
|
1273
|
+
// controller writes survive a re-run. [1.1F-04]
|
|
1274
|
+
var prev = {};
|
|
1190
1275
|
|
|
1191
1276
|
var cl = effect(function() {
|
|
1192
1277
|
var value = evaluate(dir.expression, scope, el);
|
|
1193
1278
|
|
|
1194
1279
|
if (attrName === 'class') {
|
|
1195
|
-
applyClassBinding(el, value);
|
|
1280
|
+
prev = applyClassBinding(el, value, prev);
|
|
1196
1281
|
} else if (attrName === 'style') {
|
|
1197
|
-
applyStyleBinding(el, value);
|
|
1282
|
+
prev = applyStyleBinding(el, value, prev);
|
|
1198
1283
|
} else if (BOOLEAN_ATTRS.has(attrName)) {
|
|
1199
1284
|
if (value) {
|
|
1200
1285
|
el.setAttribute(attrName, '');
|
|
@@ -1208,41 +1293,66 @@
|
|
|
1208
1293
|
el.setAttribute(attrName, String(value));
|
|
1209
1294
|
}
|
|
1210
1295
|
}
|
|
1296
|
+
if (PROPS.has(attrName)) {
|
|
1297
|
+
writeProp(el, attrName, attrName === 'value'
|
|
1298
|
+
? (value == null || value === false ? '' : String(value))
|
|
1299
|
+
: !!value);
|
|
1300
|
+
}
|
|
1211
1301
|
});
|
|
1212
1302
|
|
|
1213
1303
|
addCleanup(el, cl);
|
|
1214
1304
|
}
|
|
1215
1305
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
} else if (
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1306
|
+
// A `:class` value as { class: on }. Strings and arrays turn classes on; an
|
|
1307
|
+
// object entry sets its own, so a falsy one removes a class markup set.
|
|
1308
|
+
function classMap(v, out, on) {
|
|
1309
|
+
if (typeof v === 'string') {
|
|
1310
|
+
v.split(/\s+/).forEach(function(c) { if (c) out[c] = on; });
|
|
1311
|
+
} else if (Array.isArray(v)) {
|
|
1312
|
+
v.forEach(function(x) { if (x) classMap(x, out, true); });
|
|
1313
|
+
} else if (v && typeof v === 'object') {
|
|
1314
|
+
for (var k in v) classMap(k, out, !!v[k]);
|
|
1227
1315
|
}
|
|
1316
|
+
return out;
|
|
1228
1317
|
}
|
|
1229
1318
|
|
|
1230
|
-
function
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1319
|
+
function applyClassBinding(el, value, prev) {
|
|
1320
|
+
var next = classMap(value, {}, true), c;
|
|
1321
|
+
for (c in prev) if (prev[c] && !(c in next)) el.classList.remove(c);
|
|
1322
|
+
for (c in next) el.classList.toggle(c, next[c]);
|
|
1323
|
+
return next;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// A `:style` value as { property: [value, priority] }, or null to remove it.
|
|
1327
|
+
// A string is parsed by a detached element's CSSOM, which handles custom
|
|
1328
|
+
// properties, `!important`, shorthands and `;` inside `url()`.
|
|
1329
|
+
var styleScratch;
|
|
1330
|
+
function styleMap(v, out) {
|
|
1331
|
+
if (typeof v === 'string') {
|
|
1332
|
+
var s = (styleScratch || (styleScratch = document.createElement('div'))).style;
|
|
1333
|
+
s.cssText = v;
|
|
1334
|
+
for (var i = 0; i < s.length; i++) {
|
|
1335
|
+
out[s[i]] = [s.getPropertyValue(s[i]), s.getPropertyPriority(s[i])];
|
|
1244
1336
|
}
|
|
1337
|
+
} else if (Array.isArray(v)) {
|
|
1338
|
+
v.forEach(function(x) { styleMap(x, out); });
|
|
1339
|
+
} else if (v && typeof v === 'object') {
|
|
1340
|
+
for (var k in v) {
|
|
1341
|
+
out[/^--/.test(k) ? k : k.replace(/[A-Z]/g, '-$&').toLowerCase()] =
|
|
1342
|
+
v[k] == null || v[k] === false ? null : [String(v[k]), ''];
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return out;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function applyStyleBinding(el, value, prev) {
|
|
1349
|
+
var next = styleMap(value, {}), p;
|
|
1350
|
+
for (p in prev) if (prev[p] && !(p in next)) el.style.removeProperty(p);
|
|
1351
|
+
for (p in next) {
|
|
1352
|
+
if (next[p]) el.style.setProperty(p, next[p][0], next[p][1]);
|
|
1353
|
+
else el.style.removeProperty(p);
|
|
1245
1354
|
}
|
|
1355
|
+
return next;
|
|
1246
1356
|
}
|
|
1247
1357
|
|
|
1248
1358
|
// --- 3.9 l-on / @event ---
|
|
@@ -1344,13 +1454,17 @@
|
|
|
1344
1454
|
|
|
1345
1455
|
function handleRef(el, dir, scope) {
|
|
1346
1456
|
var name = dir.expression;
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1457
|
+
// Written through the reactive proxy, so an effect that read `$refs.name`
|
|
1458
|
+
// re-runs when the ref appears or goes. [1.1F-01]
|
|
1459
|
+
var refs = reactive(getScopeRefs(scopeRoots.get(scope) || findScopeRoot(el)));
|
|
1460
|
+
refs[name] = el;
|
|
1350
1461
|
|
|
1351
1462
|
addCleanup(el, function() {
|
|
1352
|
-
|
|
1353
|
-
|
|
1463
|
+
// `__target`: a tracked read here would subscribe whichever structural
|
|
1464
|
+
// effect is tearing this element down.
|
|
1465
|
+
if (refs.__target[name] === el) {
|
|
1466
|
+
refs[name] = undefined;
|
|
1467
|
+
delete refs.__target[name];
|
|
1354
1468
|
}
|
|
1355
1469
|
});
|
|
1356
1470
|
}
|
|
@@ -1358,29 +1472,24 @@
|
|
|
1358
1472
|
// --- 3.11 l-init ---
|
|
1359
1473
|
|
|
1360
1474
|
function handleInit(el, dir, scope) {
|
|
1361
|
-
evaluateAssignment(dir.expression, scope, el);
|
|
1475
|
+
runInit(function() { evaluateAssignment(dir.expression, scope, el); });
|
|
1362
1476
|
}
|
|
1363
1477
|
|
|
1364
1478
|
// --- 3.12 l-effect ---
|
|
1365
1479
|
|
|
1366
1480
|
function handleEffect(el, dir, scope) {
|
|
1367
|
-
|
|
1368
|
-
|
|
1481
|
+
runInit(function() {
|
|
1482
|
+
addCleanup(el, effect(function() {
|
|
1483
|
+
evaluateAssignment(dir.expression, scope, el);
|
|
1484
|
+
}));
|
|
1369
1485
|
});
|
|
1370
|
-
addCleanup(el, cl);
|
|
1371
1486
|
}
|
|
1372
1487
|
|
|
1373
1488
|
// --- 3.13 l-cloak ---
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
if (document.querySelector('style[data-faqir-cloak]')) return;
|
|
1379
|
-
var style = document.createElement('style');
|
|
1380
|
-
style.setAttribute('data-faqir-cloak', '');
|
|
1381
|
-
style.textContent = '[l-cloak] { display: none !important; }';
|
|
1382
|
-
document.head.appendChild(style);
|
|
1383
|
-
}
|
|
1489
|
+
//
|
|
1490
|
+
// The engine injects no stylesheet. The `[l-cloak]` rule ships in
|
|
1491
|
+
// `base/reset.css` (so in every bundle); the injected copy it used to append
|
|
1492
|
+
// at boot was redundant, and the only inline style a CSP had to allow. [1.1F-07]
|
|
1384
1493
|
|
|
1385
1494
|
/**
|
|
1386
1495
|
* Strip `l-cloak` from `within` and everything under it (the whole document
|
|
@@ -1389,7 +1498,7 @@
|
|
|
1389
1498
|
* The sweep used to run exactly once, at the end of bootstrap, against a
|
|
1390
1499
|
* document-wide selector. Anything inserted afterwards — an `l-if` branch, an
|
|
1391
1500
|
* `l-for` row, a fragment an application appended — kept the attribute, and
|
|
1392
|
-
* the
|
|
1501
|
+
* the `[l-cloak] { display: none !important }` rule then hid it
|
|
1393
1502
|
* permanently: content that had bound correctly and could never be seen. The
|
|
1394
1503
|
* MutationObserver runs this over every node it is handed. [W3-1]
|
|
1395
1504
|
*/
|
|
@@ -1415,7 +1524,7 @@
|
|
|
1415
1524
|
|
|
1416
1525
|
// Named presets shipped by motion-presets.css. Unknown names still animate
|
|
1417
1526
|
// (the attribute is stamped) but have no styling, so we warn to catch typos.
|
|
1418
|
-
var MOTION_PRESETS = { fade: 1, 'slide-up': 1, scale: 1 };
|
|
1527
|
+
var MOTION_PRESETS = { fade: 1, 'slide-up': 1, 'slide-down': 1, 'slide-start': 1, 'slide-end': 1, scale: 1, blur: 1, flip: 1 };
|
|
1419
1528
|
|
|
1420
1529
|
function prefersReducedMotion() {
|
|
1421
1530
|
return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
@@ -1469,7 +1578,7 @@
|
|
|
1469
1578
|
var preset = el.getAttribute('l-transition') || 'fade';
|
|
1470
1579
|
if (!MOTION_PRESETS[preset]) {
|
|
1471
1580
|
console.warn('[Faqir] l-transition: unknown preset "' + preset +
|
|
1472
|
-
'" — expected one of:
|
|
1581
|
+
'" — expected one of: ' + Object.keys(MOTION_PRESETS).join(', '));
|
|
1473
1582
|
}
|
|
1474
1583
|
|
|
1475
1584
|
el.setAttribute('data-motion', phase); // from-state (no transition yet)
|
|
@@ -1546,23 +1655,22 @@
|
|
|
1546
1655
|
if (isFaqirSwitch) {
|
|
1547
1656
|
var cl = effect(function() {
|
|
1548
1657
|
var value = evaluate(prop, scope, el);
|
|
1549
|
-
el
|
|
1658
|
+
writeProp(el, 'checked', !!value);
|
|
1550
1659
|
el.dataset.state = value ? 'on' : 'off';
|
|
1551
1660
|
el.setAttribute('aria-checked', value ? 'true' : 'false');
|
|
1552
1661
|
});
|
|
1553
|
-
|
|
1554
|
-
|
|
1662
|
+
// A switch is a <button role="switch">: no `checked`, and no `change`.
|
|
1663
|
+
bind('click', function() {
|
|
1664
|
+
writeModel(prop, !evaluate(prop, scope, el), scope, el);
|
|
1555
1665
|
});
|
|
1556
1666
|
addCleanup(el, cl);
|
|
1557
1667
|
|
|
1558
1668
|
} else if (tag === 'input' && type === 'checkbox') {
|
|
1559
1669
|
var cl = effect(function() {
|
|
1560
1670
|
var current = evaluate(prop, scope, el);
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
el.checked = !!current;
|
|
1565
|
-
}
|
|
1671
|
+
writeProp(el, 'checked', Array.isArray(current)
|
|
1672
|
+
? current.indexOf(el.value) >= 0
|
|
1673
|
+
: !!current);
|
|
1566
1674
|
});
|
|
1567
1675
|
bind('change', function() {
|
|
1568
1676
|
var current = evaluate(prop, scope, el);
|
|
@@ -1580,7 +1688,7 @@
|
|
|
1580
1688
|
|
|
1581
1689
|
} else if (tag === 'input' && type === 'radio') {
|
|
1582
1690
|
var cl = effect(function() {
|
|
1583
|
-
el
|
|
1691
|
+
writeProp(el, 'checked', evaluate(prop, scope, el) === el.value);
|
|
1584
1692
|
});
|
|
1585
1693
|
bind('change', function() {
|
|
1586
1694
|
if (el.checked) {
|
|
@@ -1591,7 +1699,7 @@
|
|
|
1591
1699
|
|
|
1592
1700
|
} else if (tag === 'select') {
|
|
1593
1701
|
var cl = effect(function() {
|
|
1594
|
-
el
|
|
1702
|
+
writeProp(el, 'value', evaluate(prop, scope, el) || '');
|
|
1595
1703
|
});
|
|
1596
1704
|
bind('change', function() {
|
|
1597
1705
|
writeModel(prop, el.value, scope, el);
|
|
@@ -1684,7 +1792,7 @@
|
|
|
1684
1792
|
var value = evaluate(dir.expression, scope, el);
|
|
1685
1793
|
|
|
1686
1794
|
if (value) {
|
|
1687
|
-
if (insertedNodes.length === 0) {
|
|
1795
|
+
if (insertedNodes.length === 0) renderThenInit(function() {
|
|
1688
1796
|
var fragment = el.content.cloneNode(true);
|
|
1689
1797
|
var nodes = [].slice.call(fragment.childNodes);
|
|
1690
1798
|
|
|
@@ -1727,7 +1835,8 @@
|
|
|
1727
1835
|
runEnterTransition(insertedNodes[i]);
|
|
1728
1836
|
}
|
|
1729
1837
|
}
|
|
1730
|
-
|
|
1838
|
+
return insertedNodes;
|
|
1839
|
+
});
|
|
1731
1840
|
} else {
|
|
1732
1841
|
for (var i = 0; i < insertedNodes.length; i++) {
|
|
1733
1842
|
var node = insertedNodes[i];
|
|
@@ -1843,6 +1952,8 @@
|
|
|
1843
1952
|
var indexName = match[2] || 'index';
|
|
1844
1953
|
var listExpr = match[3];
|
|
1845
1954
|
var keyExpr = el.getAttribute('l-key');
|
|
1955
|
+
// Dev hint: a loop variable that shadows a name the scope already has. Only
|
|
1956
|
+
// names the author wrote; `in` reads no reactive dependency. [1.1F-05]
|
|
1846
1957
|
|
|
1847
1958
|
var anchor = document.createComment('l-for');
|
|
1848
1959
|
el.parentNode.insertBefore(anchor, el);
|
|
@@ -1877,6 +1988,7 @@
|
|
|
1877
1988
|
own[itemName] = item;
|
|
1878
1989
|
own[indexName] = i;
|
|
1879
1990
|
var childScope = createForItemScope(own, scope);
|
|
1991
|
+
scopeRoots.set(childScope, scopeRoots.get(scope));
|
|
1880
1992
|
for (var j = 0; j < nodes.length; j++) {
|
|
1881
1993
|
if (nodes[j].nodeType !== 1) continue;
|
|
1882
1994
|
// A row whose top node declares `l-data` is a scope root: build it with
|
|
@@ -1901,7 +2013,9 @@
|
|
|
1901
2013
|
|
|
1902
2014
|
var currentEntries = [];
|
|
1903
2015
|
|
|
1904
|
-
|
|
2016
|
+
// One reconciliation. Returns the rows it built, for `renderThenInit` to
|
|
2017
|
+
// start their controllers and run their held inits. [1.1F-03]
|
|
2018
|
+
function renderList() {
|
|
1905
2019
|
var list = evaluate(listExpr, scope, el);
|
|
1906
2020
|
var items = Array.isArray(list) ? list :
|
|
1907
2021
|
typeof list === 'number' ? Array.from({ length: list }, function(_, i) { return i + 1; }) :
|
|
@@ -1974,7 +2088,12 @@
|
|
|
1974
2088
|
}
|
|
1975
2089
|
|
|
1976
2090
|
currentEntries = newEntries;
|
|
1977
|
-
|
|
2091
|
+
var fresh = [];
|
|
2092
|
+
for (var i = 0; i < n; i++) if (!source[i]) fresh.push.apply(fresh, newEntries[i].nodes);
|
|
2093
|
+
return fresh;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
var cl = effect(function() { renderThenInit(renderList); });
|
|
1978
2097
|
|
|
1979
2098
|
addCleanup(el, cl, anchor);
|
|
1980
2099
|
}
|
|
@@ -2113,41 +2232,20 @@
|
|
|
2113
2232
|
// ═══════════════════════════════════════════════════════
|
|
2114
2233
|
|
|
2115
2234
|
// --- From dom.js ---
|
|
2116
|
-
var $ = function(selector, scope) { return (scope || document).querySelector(selector); };
|
|
2117
2235
|
var $$ = function(selector, scope) { return [].slice.call((scope || document).querySelectorAll(selector)); };
|
|
2118
2236
|
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
for (var i = 0; i < keys.length; i++) {
|
|
2124
|
-
el.setAttribute(keys[i], attrs[keys[i]]);
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
for (var j = 2; j < arguments.length; j++) {
|
|
2128
|
-
var child = arguments[j];
|
|
2129
|
-
el.append(typeof child === 'string' ? document.createTextNode(child) : child);
|
|
2130
|
-
}
|
|
2131
|
-
return el;
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
// --- From events.js ---
|
|
2135
|
-
function delegate(root, event, selector, handler) {
|
|
2136
|
-
function listener(e) {
|
|
2137
|
-
var target = e.target.closest(selector);
|
|
2138
|
-
if (target && root.contains(target)) handler(e, target);
|
|
2139
|
-
}
|
|
2140
|
-
root.addEventListener(event, listener);
|
|
2141
|
-
return function() { root.removeEventListener(event, listener); };
|
|
2142
|
-
}
|
|
2237
|
+
// A part's owner is its nearest [data-ui] above it that is not a layout
|
|
2238
|
+
// primitive, starting from the parent since a part may carry data-ui itself.
|
|
2239
|
+
// Same selector as registry/core/dom.js `owns`.
|
|
2240
|
+
var OWNER = '[data-ui]:not([data-ui=stack],[data-ui=cluster],[data-ui=grid],[data-ui=container],[data-ui=surface],[data-ui=switcher],[data-ui=aspect-ratio])';
|
|
2143
2241
|
|
|
2144
|
-
function
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
return cleanupOnce;
|
|
2242
|
+
function ownParts(root, part) {
|
|
2243
|
+
return $$("[data-part='" + part + "']", root).filter(function(el) {
|
|
2244
|
+
return el.parentElement.closest(OWNER) === root;
|
|
2245
|
+
});
|
|
2149
2246
|
}
|
|
2150
2247
|
|
|
2248
|
+
// --- From events.js ---
|
|
2151
2249
|
function onOutsideClick(el, handler) {
|
|
2152
2250
|
function listener(e) { if (!el.contains(e.target)) handler(e); }
|
|
2153
2251
|
document.addEventListener('pointerdown', listener);
|
|
@@ -2192,25 +2290,14 @@
|
|
|
2192
2290
|
return function() { container.removeEventListener('keydown', onKeyDown); };
|
|
2193
2291
|
}
|
|
2194
2292
|
|
|
2195
|
-
//
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
(style.animationName !== 'none' && parseFloat(style.animationDuration) > 0);
|
|
2201
|
-
if (!hasDuration) return Promise.resolve();
|
|
2202
|
-
return new Promise(function(resolve) {
|
|
2203
|
-
function done(e) {
|
|
2204
|
-
if (e.target !== el) return;
|
|
2205
|
-
el.removeEventListener('transitionend', done);
|
|
2206
|
-
el.removeEventListener('animationend', done);
|
|
2207
|
-
resolve();
|
|
2208
|
-
}
|
|
2209
|
-
el.addEventListener('transitionend', done);
|
|
2210
|
-
el.addEventListener('animationend', done);
|
|
2211
|
-
});
|
|
2293
|
+
// Back to the opener once an overlay has closed, unless focus moved on while
|
|
2294
|
+
// it closed (`active` was read before the panel hid) or the opener is gone.
|
|
2295
|
+
function returnFocus(root, target, active) {
|
|
2296
|
+
if ((!active || active === document.body || root.contains(active)) &&
|
|
2297
|
+
target && target.isConnected && !target.closest('[hidden]')) target.focus();
|
|
2212
2298
|
}
|
|
2213
2299
|
|
|
2300
|
+
// --- From motion.js (prefersReducedMotion already defined in Section 3.14) ---
|
|
2214
2301
|
// The four events an exit can end with, in one list: bound and unbound
|
|
2215
2302
|
// together, so neither half can drift from the other.
|
|
2216
2303
|
var EXIT_EVENTS = ['transitionend', 'transitioncancel', 'animationend', 'animationcancel'];
|
|
@@ -2513,7 +2600,8 @@ function createAccordion(root) {
|
|
|
2513
2600
|
// Prevent double-init
|
|
2514
2601
|
if (root._faqirAccordion) return root._faqirAccordion;
|
|
2515
2602
|
|
|
2516
|
-
|
|
2603
|
+
// Direct children only: a nested accordion's items are its own.
|
|
2604
|
+
const getItems = () => [...root.querySelectorAll(":scope > [data-part='item']")];
|
|
2517
2605
|
const isSingle = () => root.dataset.variant === "single";
|
|
2518
2606
|
|
|
2519
2607
|
function expand(index) {
|
|
@@ -2573,26 +2661,30 @@ function createAccordion(root) {
|
|
|
2573
2661
|
if (content) content.hidden = true;
|
|
2574
2662
|
}
|
|
2575
2663
|
|
|
2576
|
-
|
|
2664
|
+
/**
|
|
2665
|
+
* The index of the item whose own trigger `e` came from, or -1. A trigger of
|
|
2666
|
+
* a component nested in the content (a dropdown, another accordion) is not
|
|
2667
|
+
* the item's: an item's own trigger is the first one in it, since it comes
|
|
2668
|
+
* before the content.
|
|
2669
|
+
*/
|
|
2670
|
+
function triggerIndex(e) {
|
|
2577
2671
|
const trigger = e.target.closest("[data-part='trigger']");
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2672
|
+
const item = trigger?.closest("[data-part='item']");
|
|
2673
|
+
if (!item || item.querySelector("[data-part='trigger']") !== trigger) return -1;
|
|
2674
|
+
return getItems().indexOf(item);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
function onTriggerClick(e) {
|
|
2678
|
+
const index = triggerIndex(e);
|
|
2583
2679
|
if (index >= 0) toggle(index);
|
|
2584
2680
|
}
|
|
2585
2681
|
|
|
2586
2682
|
function onKeyDown(e) {
|
|
2587
2683
|
if (e.key === "Enter" || e.key === " ") {
|
|
2588
|
-
const
|
|
2589
|
-
if (
|
|
2684
|
+
const index = triggerIndex(e);
|
|
2685
|
+
if (index >= 0) {
|
|
2590
2686
|
e.preventDefault();
|
|
2591
|
-
|
|
2592
|
-
if (!item) return;
|
|
2593
|
-
const items = getItems();
|
|
2594
|
-
const index = items.indexOf(item);
|
|
2595
|
-
if (index >= 0) toggle(index);
|
|
2687
|
+
toggle(index);
|
|
2596
2688
|
}
|
|
2597
2689
|
}
|
|
2598
2690
|
}
|
|
@@ -3367,12 +3459,14 @@ function createCarousel(root) {
|
|
|
3367
3459
|
if (!viewport) return null;
|
|
3368
3460
|
|
|
3369
3461
|
const loop = root.hasAttribute("data-loop");
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
const
|
|
3373
|
-
const
|
|
3374
|
-
const
|
|
3375
|
-
const
|
|
3462
|
+
// A carousel nested in a slide brings its own slides, buttons and dots:
|
|
3463
|
+
// slides are the viewport's children, the rest go through the owner guard.
|
|
3464
|
+
const slides = () => [...viewport.querySelectorAll(":scope > [data-part='slide']")];
|
|
3465
|
+
const dots = () => ownParts(root, "dot");
|
|
3466
|
+
const [prevBtn] = ownParts(root, "prev");
|
|
3467
|
+
const [nextBtn] = ownParts(root, "next");
|
|
3468
|
+
const [status] = ownParts(root, "status");
|
|
3469
|
+
const enhanced = [...ownParts(root, "controls"), ...ownParts(root, "dots")];
|
|
3376
3470
|
|
|
3377
3471
|
let index = 0;
|
|
3378
3472
|
|
|
@@ -3468,12 +3562,13 @@ function createCarousel(root) {
|
|
|
3468
3562
|
|
|
3469
3563
|
// ── events ──────────────────────────────────────────────────────────────────
|
|
3470
3564
|
function onClick(e) {
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
const
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
else if (
|
|
3565
|
+
// Matched by identity, so a nested carousel's buttons are not these. A
|
|
3566
|
+
// missing button is `undefined`, never equal to a `null` miss here.
|
|
3567
|
+
const btn = e.target.closest("[data-part]");
|
|
3568
|
+
const dot = dots().indexOf(btn);
|
|
3569
|
+
if (btn === nextBtn) next();
|
|
3570
|
+
else if (btn === prevBtn) prev();
|
|
3571
|
+
else if (dot >= 0) goTo(dot);
|
|
3477
3572
|
}
|
|
3478
3573
|
|
|
3479
3574
|
// One rAF-throttled measurement per scroll burst keeps tracking cheap.
|
|
@@ -3824,8 +3919,11 @@ function createCommandPalette(root) {
|
|
|
3824
3919
|
}
|
|
3825
3920
|
|
|
3826
3921
|
function close() {
|
|
3922
|
+
const active = document.activeElement;
|
|
3827
3923
|
dismiss();
|
|
3828
|
-
|
|
3924
|
+
// The opener may have left with another overlay — the downstream palette
|
|
3925
|
+
// was opened from inside a dialog that closed underneath it.
|
|
3926
|
+
returnFocus(root, previouslyFocused, active);
|
|
3829
3927
|
}
|
|
3830
3928
|
|
|
3831
3929
|
/**
|
|
@@ -4019,14 +4117,29 @@ function createCommandPalette(root) {
|
|
|
4019
4117
|
}
|
|
4020
4118
|
}
|
|
4021
4119
|
|
|
4120
|
+
/**
|
|
4121
|
+
* Cmd/Ctrl+K — one owner per press.
|
|
4122
|
+
*
|
|
4123
|
+
* Every instance listens on `document`, so a page with three palettes used to
|
|
4124
|
+
* open three stacked focus traps. The first instance to act prevents default
|
|
4125
|
+
* and every later one (or one an app handler pre-empted) stands down;
|
|
4126
|
+
* `data-no-shortcut` opts an instance out entirely, read live, and a palette
|
|
4127
|
+
* whose root has left the document without `destroy()` (an SPA route change)
|
|
4128
|
+
* no longer claims the press from the live one. The key is
|
|
4129
|
+
* matched case-insensitively (Caps Lock sends "K"), falling back to the
|
|
4130
|
+
* physical key on a non-Latin layout; Shift is left alone because Cmd+Shift+K
|
|
4131
|
+
* is a browser and app chord of its own.
|
|
4132
|
+
*/
|
|
4022
4133
|
function onGlobalKeyDown(e) {
|
|
4023
|
-
if (
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4134
|
+
if (e.defaultPrevented || !root.isConnected || root.hasAttribute("data-no-shortcut")) return;
|
|
4135
|
+
if (!(e.metaKey || e.ctrlKey) || e.altKey || e.shiftKey || e.repeat || e.isComposing) return;
|
|
4136
|
+
const k = (e.key || "").toLowerCase();
|
|
4137
|
+
if (k !== "k" && (/^[a-z]$/.test(k) || e.code !== "KeyK")) return;
|
|
4138
|
+
e.preventDefault();
|
|
4139
|
+
if (root.dataset.state === "open") {
|
|
4140
|
+
close();
|
|
4141
|
+
} else {
|
|
4142
|
+
open();
|
|
4030
4143
|
}
|
|
4031
4144
|
}
|
|
4032
4145
|
|
|
@@ -4062,8 +4175,10 @@ function createCommandPalette(root) {
|
|
|
4062
4175
|
function createContextMenu(root) {
|
|
4063
4176
|
if (root._faqirContextMenu) return root._faqirContextMenu;
|
|
4064
4177
|
|
|
4065
|
-
|
|
4066
|
-
|
|
4178
|
+
// Matched by owner: a dropdown inside the target has a `menu` part too, and
|
|
4179
|
+
// it comes first in document order.
|
|
4180
|
+
const [target] = ownParts(root, "target");
|
|
4181
|
+
const [menu] = ownParts(root, "menu");
|
|
4067
4182
|
|
|
4068
4183
|
// Same as `dropdown`: a missing part must name itself rather than throwing a
|
|
4069
4184
|
// TypeError out of `createMenuNavigation(null)`. [W3-2]
|
|
@@ -4367,17 +4482,24 @@ function createDialog(root) {
|
|
|
4367
4482
|
// Prevent double-init
|
|
4368
4483
|
if (root._faqirDialog) return root._faqirDialog;
|
|
4369
4484
|
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
const
|
|
4374
|
-
const
|
|
4375
|
-
const
|
|
4485
|
+
// Overlay and panel are direct children. Trigger and actions may be wrapped
|
|
4486
|
+
// in layout primitives, so they are matched by owner: a popover's close
|
|
4487
|
+
// button inside the panel belongs to the popover and must not close this.
|
|
4488
|
+
const [trigger] = ownParts(root, "trigger");
|
|
4489
|
+
const overlay = root.querySelector(":scope > [data-part='overlay']");
|
|
4490
|
+
const panel = root.querySelector(":scope > [data-part='panel']");
|
|
4491
|
+
const closeButtons = ownParts(root, "close");
|
|
4492
|
+
const confirmButtons = ownParts(root, "confirm");
|
|
4493
|
+
const cancelButtons = ownParts(root, "cancel");
|
|
4376
4494
|
|
|
4377
4495
|
// The role is the seam between `dialog` and `alert-dialog` — read it from the
|
|
4378
4496
|
// markup so a single controller serves both recipes.
|
|
4379
4497
|
const isAlert = !!panel && panel.getAttribute("role") === "alertdialog";
|
|
4380
4498
|
|
|
4499
|
+
// `panel.focus()` on open does nothing without a tabindex: focus stayed on
|
|
4500
|
+
// `body`, out of reach of the trap and the root's Escape listener.
|
|
4501
|
+
if (panel && !panel.hasAttribute("tabindex")) panel.setAttribute("tabindex", "-1");
|
|
4502
|
+
|
|
4381
4503
|
let focusCleanup = null;
|
|
4382
4504
|
let previouslyFocused = null;
|
|
4383
4505
|
|
|
@@ -4400,10 +4522,7 @@ function createDialog(root) {
|
|
|
4400
4522
|
/** On open, focus the least-destructive action for an alert, else the panel. */
|
|
4401
4523
|
function focusInitial() {
|
|
4402
4524
|
if (isAlert) {
|
|
4403
|
-
const target =
|
|
4404
|
-
root.querySelector("[data-part='cancel']") ||
|
|
4405
|
-
root.querySelector("[data-part='close']") ||
|
|
4406
|
-
panel;
|
|
4525
|
+
const target = cancelButtons[0] || closeButtons[0] || panel;
|
|
4407
4526
|
target?.focus?.();
|
|
4408
4527
|
} else {
|
|
4409
4528
|
panel?.focus?.();
|
|
@@ -4437,12 +4556,13 @@ function createDialog(root) {
|
|
|
4437
4556
|
|
|
4438
4557
|
const onEnd = () => {
|
|
4439
4558
|
cancelExitWait = null;
|
|
4559
|
+
const active = document.activeElement;
|
|
4440
4560
|
root.dataset.state = "closed";
|
|
4441
4561
|
overlay.hidden = true;
|
|
4442
4562
|
panel.hidden = true;
|
|
4443
4563
|
if (focusCleanup) focusCleanup();
|
|
4444
4564
|
focusCleanup = null;
|
|
4445
|
-
previouslyFocused
|
|
4565
|
+
returnFocus(root, previouslyFocused, active);
|
|
4446
4566
|
};
|
|
4447
4567
|
|
|
4448
4568
|
// The panel's own exit motion. A footer button's `background` transitionend
|
|
@@ -4575,10 +4695,16 @@ function createDrawer(root) {
|
|
|
4575
4695
|
// Prevent double-init
|
|
4576
4696
|
if (root._faqirDrawer) return root._faqirDrawer;
|
|
4577
4697
|
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
const
|
|
4581
|
-
const
|
|
4698
|
+
// Overlay and panel are direct children; trigger and close buttons are
|
|
4699
|
+
// matched by owner, so a nested component's close leaves this one alone.
|
|
4700
|
+
const [trigger] = ownParts(root, "trigger");
|
|
4701
|
+
const overlay = root.querySelector(":scope > [data-part='overlay']");
|
|
4702
|
+
const panel = root.querySelector(":scope > [data-part='panel']");
|
|
4703
|
+
const closeButtons = ownParts(root, "close");
|
|
4704
|
+
|
|
4705
|
+
// `panel.focus()` on open does nothing without a tabindex: focus stayed on
|
|
4706
|
+
// `body`, out of reach of the trap and the root's Escape listener.
|
|
4707
|
+
if (panel && !panel.hasAttribute("tabindex")) panel.setAttribute("tabindex", "-1");
|
|
4582
4708
|
|
|
4583
4709
|
let focusCleanup = null;
|
|
4584
4710
|
let previouslyFocused = null;
|
|
@@ -4622,13 +4748,14 @@ function createDrawer(root) {
|
|
|
4622
4748
|
|
|
4623
4749
|
const onEnd = () => {
|
|
4624
4750
|
cancelExitWait = null;
|
|
4751
|
+
const active = document.activeElement;
|
|
4625
4752
|
root.dataset.state = "closed";
|
|
4626
4753
|
overlay.hidden = true;
|
|
4627
4754
|
panel.hidden = true;
|
|
4628
4755
|
unlockScroll();
|
|
4629
4756
|
if (focusCleanup) focusCleanup();
|
|
4630
4757
|
focusCleanup = null;
|
|
4631
|
-
previouslyFocused
|
|
4758
|
+
returnFocus(root, previouslyFocused, active);
|
|
4632
4759
|
};
|
|
4633
4760
|
|
|
4634
4761
|
// The panel's OWN `transform` — not the close button's `background`, which
|
|
@@ -6103,7 +6230,8 @@ function createPopover(root) {
|
|
|
6103
6230
|
|
|
6104
6231
|
const trigger = root.querySelector("[data-part='trigger']");
|
|
6105
6232
|
const content = root.querySelector("[data-part='content']");
|
|
6106
|
-
|
|
6233
|
+
// Not a close button belonging to a dialog or popover inside the content.
|
|
6234
|
+
const [closeBtn] = ownParts(root, "close");
|
|
6107
6235
|
|
|
6108
6236
|
let outsideClickCleanup = null;
|
|
6109
6237
|
|
|
@@ -6238,41 +6366,37 @@ function rsEncode(data, ecLen) {
|
|
|
6238
6366
|
|
|
6239
6367
|
const ECL_MAP = { L: 0, M: 1, Q: 2, H: 3 };
|
|
6240
6368
|
|
|
6241
|
-
// [version
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
[{ dc: 34, ec: 10, b: 1 }, { dc: 28, ec: 16, b: 1 }, { dc: 22, ec: 22, b: 1 }, { dc: 16, ec: 28, b: 1 }],
|
|
6248
|
-
// v3
|
|
6249
|
-
[{ dc: 55, ec: 15, b: 1 }, { dc: 44, ec: 26, b: 1 }, { dc: 34, ec: 18, b: 2 }, { dc: 26, ec: 22, b: 2 }],
|
|
6250
|
-
// v4
|
|
6251
|
-
[{ dc: 80, ec: 20, b: 1 }, { dc: 64, ec: 18, b: 2 }, { dc: 48, ec: 26, b: 2 }, { dc: 36, ec: 16, b: 4 }],
|
|
6252
|
-
// v5
|
|
6253
|
-
[{ dc: 108, ec: 26, b: 1 }, { dc: 86, ec: 24, b: 2 }, { dc: 62, ec: 18, b: 4 }, { dc: 46, ec: 22, b: 4 }],
|
|
6254
|
-
// v6
|
|
6255
|
-
[{ dc: 136, ec: 18, b: 2 }, { dc: 108, ec: 16, b: 4 }, { dc: 76, ec: 24, b: 4 }, { dc: 60, ec: 28, b: 4 }],
|
|
6256
|
-
// v7
|
|
6257
|
-
[{ dc: 156, ec: 20, b: 2 }, { dc: 124, ec: 18, b: 4 }, { dc: 88, ec: 18, b: 6 }, { dc: 66, ec: 26, b: 5 }],
|
|
6258
|
-
// v8
|
|
6259
|
-
[{ dc: 194, ec: 24, b: 2 }, { dc: 154, ec: 22, b: 4 }, { dc: 110, ec: 22, b: 6 }, { dc: 86, ec: 26, b: 6 }],
|
|
6260
|
-
// v9
|
|
6261
|
-
[{ dc: 232, ec: 30, b: 2 }, { dc: 182, ec: 22, b: 5 }, { dc: 132, ec: 20, b: 8 }, { dc: 98, ec: 24, b: 8 }],
|
|
6262
|
-
// v10
|
|
6263
|
-
[{ dc: 274, ec: 18, b: 4 }, { dc: 216, ec: 26, b: 5 }, { dc: 154, ec: 24, b: 8 }, { dc: 119, ec: 28, b: 8 }],
|
|
6369
|
+
// ISO/IEC 18004 Table 9, flattened as [(version - 1) * 4 + ecl]: EC codewords
|
|
6370
|
+
// per block, and the block count. Data codewords are what is left of the
|
|
6371
|
+
// version's total once the EC blocks are taken out.
|
|
6372
|
+
const EC_PER_BLOCK = [
|
|
6373
|
+
7, 10, 13, 17, 10, 16, 22, 28, 15, 26, 18, 22, 20, 18, 26, 16, 26, 24, 18, 22,
|
|
6374
|
+
18, 16, 24, 28, 20, 18, 18, 26, 24, 22, 22, 26, 30, 22, 20, 24, 18, 26, 24, 28,
|
|
6264
6375
|
];
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
[6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50],
|
|
6376
|
+
const BLOCKS = [
|
|
6377
|
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 2, 2, 4, 1, 2, 4, 4,
|
|
6378
|
+
2, 4, 4, 4, 2, 4, 6, 5, 2, 4, 6, 6, 2, 5, 8, 8, 4, 5, 8, 8,
|
|
6269
6379
|
];
|
|
6380
|
+
const TOTAL_CODEWORDS = [26, 44, 70, 100, 134, 172, 196, 242, 292, 346];
|
|
6381
|
+
|
|
6382
|
+
function versionInfo(version, eclIdx) {
|
|
6383
|
+
const i = (version - 1) * 4 + eclIdx;
|
|
6384
|
+
const ec = EC_PER_BLOCK[i];
|
|
6385
|
+
const b = BLOCKS[i];
|
|
6386
|
+
return { dc: TOTAL_CODEWORDS[version - 1] - ec * b, ec, b };
|
|
6387
|
+
}
|
|
6388
|
+
|
|
6389
|
+
// Alignment centres: none for v1, two up to v6, three from v7 (evenly spaced).
|
|
6390
|
+
function alignmentPositions(v) {
|
|
6391
|
+
return v < 2 ? [] : v < 7 ? [6, 4 * v + 10] : [6, 2 * v + 8, 4 * v + 10];
|
|
6392
|
+
}
|
|
6270
6393
|
|
|
6271
6394
|
function selectVersion(dataLen, ecl) {
|
|
6272
6395
|
const eclIdx = ECL_MAP[ecl] ?? 1;
|
|
6273
6396
|
for (let v = 1; v <= 10; v++) {
|
|
6274
|
-
const info =
|
|
6275
|
-
|
|
6397
|
+
const info = versionInfo(v, eclIdx);
|
|
6398
|
+
// + 4-bit mode and 8-bit (v1–9) or 16-bit (v10) character count, rounded up.
|
|
6399
|
+
if (dataLen + (v < 10 ? 2 : 3) <= info.dc) return v;
|
|
6276
6400
|
}
|
|
6277
6401
|
return -1;
|
|
6278
6402
|
}
|
|
@@ -6291,16 +6415,14 @@ function setModule(matrix, row, col, val, reserved) {
|
|
|
6291
6415
|
}
|
|
6292
6416
|
}
|
|
6293
6417
|
|
|
6418
|
+
// Both patterns are concentric squares, coloured by their ring (Chebyshev
|
|
6419
|
+
// distance from the centre): a finder is dark at rings 0, 1 and 3, light at 2 and
|
|
6420
|
+
// at its ring-4 separator; an alignment pattern is dark at 0 and 2, light at 1.
|
|
6294
6421
|
function placeFinderPattern(matrix, reserved, row, col) {
|
|
6295
6422
|
for (let r = -1; r <= 7; r++) {
|
|
6296
6423
|
for (let c = -1; c <= 7; c++) {
|
|
6297
|
-
const
|
|
6298
|
-
|
|
6299
|
-
(c >= 0 && c <= 6 && (r === 0 || r === 6)) ||
|
|
6300
|
-
(r >= 2 && r <= 4 && c >= 2 && c <= 4)
|
|
6301
|
-
? 1
|
|
6302
|
-
: 0;
|
|
6303
|
-
setModule(matrix, row + r, col + c, val, reserved);
|
|
6424
|
+
const d = Math.max(Math.abs(r - 3), Math.abs(c - 3));
|
|
6425
|
+
setModule(matrix, row + r, col + c, d !== 2 && d !== 4 ? 1 : 0, reserved);
|
|
6304
6426
|
}
|
|
6305
6427
|
}
|
|
6306
6428
|
}
|
|
@@ -6308,9 +6430,8 @@ function placeFinderPattern(matrix, reserved, row, col) {
|
|
|
6308
6430
|
function placeAlignmentPattern(matrix, reserved, row, col) {
|
|
6309
6431
|
for (let r = -2; r <= 2; r++) {
|
|
6310
6432
|
for (let c = -2; c <= 2; c++) {
|
|
6311
|
-
const
|
|
6312
|
-
|
|
6313
|
-
setModule(matrix, row + r, col + c, val, reserved);
|
|
6433
|
+
const d = Math.max(Math.abs(r), Math.abs(c));
|
|
6434
|
+
setModule(matrix, row + r, col + c, d !== 1 ? 1 : 0, reserved);
|
|
6314
6435
|
}
|
|
6315
6436
|
}
|
|
6316
6437
|
}
|
|
@@ -6336,23 +6457,34 @@ function reserveFormatArea(matrix, reserved) {
|
|
|
6336
6457
|
setModule(matrix, s - 8, 8, 1, reserved); // dark module
|
|
6337
6458
|
}
|
|
6338
6459
|
|
|
6460
|
+
// Versions 7+ carry an 18-bit version word (BCH 18,6) in two 6×3 blocks.
|
|
6461
|
+
function placeVersionInfo(matrix, reserved, version) {
|
|
6462
|
+
if (version < 7) return;
|
|
6463
|
+
const s = matrix.length;
|
|
6464
|
+
let rem = version;
|
|
6465
|
+
for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >> 11) * 0x1f25);
|
|
6466
|
+
const bits = (version << 12) | rem;
|
|
6467
|
+
for (let i = 0; i < 18; i++) {
|
|
6468
|
+
const bit = (bits >> i) & 1;
|
|
6469
|
+
const a = s - 11 + (i % 3);
|
|
6470
|
+
const b = (i / 3) | 0;
|
|
6471
|
+
setModule(matrix, b, a, bit, reserved);
|
|
6472
|
+
setModule(matrix, a, b, bit, reserved);
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
|
|
6339
6476
|
function placeData(matrix, reserved, bits) {
|
|
6340
6477
|
const s = matrix.length;
|
|
6341
6478
|
let bitIdx = 0;
|
|
6342
|
-
let upward = true;
|
|
6343
|
-
for (let col = s - 1; col >= 1; col -= 2) {
|
|
6479
|
+
for (let col = s - 1, upward = true; col >= 1; col -= 2, upward = !upward) {
|
|
6344
6480
|
if (col === 6) col = 5; // skip timing column
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
const cc = col - c;
|
|
6351
|
-
if (reserved[row][cc]) continue;
|
|
6352
|
-
matrix[row][cc] = bitIdx < bits.length ? bits[bitIdx++] : 0;
|
|
6481
|
+
for (let i = 0; i < s; i++) {
|
|
6482
|
+
const row = upward ? s - 1 - i : i;
|
|
6483
|
+
for (let cc = col; cc > col - 2; cc--) {
|
|
6484
|
+
// Past the last bit the remainder modules are 0.
|
|
6485
|
+
if (!reserved[row][cc]) matrix[row][cc] = bits[bitIdx++] | 0;
|
|
6353
6486
|
}
|
|
6354
6487
|
}
|
|
6355
|
-
upward = !upward;
|
|
6356
6488
|
}
|
|
6357
6489
|
}
|
|
6358
6490
|
|
|
@@ -6394,8 +6526,9 @@ function placeFormatInfo(matrix, ecl, maskId) {
|
|
|
6394
6526
|
else if (i === 7) matrix[8][8] = bit;
|
|
6395
6527
|
else if (i === 8) matrix[7][8] = bit;
|
|
6396
6528
|
else matrix[14 - i][8] = bit;
|
|
6397
|
-
//
|
|
6398
|
-
|
|
6529
|
+
// The second copy: bits 0–6 up column 8 from the bottom, 7–14 along row 8
|
|
6530
|
+
// to the right edge. (s - 8, 8) between them is the always-dark module.
|
|
6531
|
+
if (i < 7) matrix[s - 1 - i][8] = bit;
|
|
6399
6532
|
else matrix[8][s - 15 + i] = bit;
|
|
6400
6533
|
}
|
|
6401
6534
|
}
|
|
@@ -6437,7 +6570,7 @@ function encodeQR(text, ecl = "M") {
|
|
|
6437
6570
|
if (version < 0) throw new Error("Data too long for QR versions 1–10");
|
|
6438
6571
|
|
|
6439
6572
|
const eclIdx = ECL_MAP[ecl] ?? 1;
|
|
6440
|
-
const info =
|
|
6573
|
+
const info = versionInfo(version, eclIdx);
|
|
6441
6574
|
const size = version * 4 + 17;
|
|
6442
6575
|
|
|
6443
6576
|
// Build data codewords
|
|
@@ -6477,7 +6610,6 @@ function encodeQR(text, ecl = "M") {
|
|
|
6477
6610
|
// Split into blocks and compute EC
|
|
6478
6611
|
const blocks = [];
|
|
6479
6612
|
const ecBlocks = [];
|
|
6480
|
-
const ecPerBlock = info.ec / info.b;
|
|
6481
6613
|
const dcPerBlock = Math.floor(info.dc / info.b);
|
|
6482
6614
|
const remainder = info.dc % info.b;
|
|
6483
6615
|
let offset = 0;
|
|
@@ -6487,7 +6619,7 @@ function encodeQR(text, ecl = "M") {
|
|
|
6487
6619
|
const blockData = codewords.slice(offset, offset + blockDc);
|
|
6488
6620
|
offset += blockDc;
|
|
6489
6621
|
blocks.push(blockData);
|
|
6490
|
-
ecBlocks.push(rsEncode(blockData,
|
|
6622
|
+
ecBlocks.push(rsEncode(blockData, info.ec));
|
|
6491
6623
|
}
|
|
6492
6624
|
|
|
6493
6625
|
// Interleave
|
|
@@ -6498,10 +6630,8 @@ function encodeQR(text, ecl = "M") {
|
|
|
6498
6630
|
if (i < block.length) interleaved.push(block[i]);
|
|
6499
6631
|
}
|
|
6500
6632
|
}
|
|
6501
|
-
for (let i = 0; i <
|
|
6502
|
-
for (const ec of ecBlocks)
|
|
6503
|
-
if (i < ec.length) interleaved.push(ec[i]);
|
|
6504
|
-
}
|
|
6633
|
+
for (let i = 0; i < info.ec; i++) {
|
|
6634
|
+
for (const ec of ecBlocks) interleaved.push(ec[i]);
|
|
6505
6635
|
}
|
|
6506
6636
|
|
|
6507
6637
|
// Convert to bit array
|
|
@@ -6518,7 +6648,7 @@ function encodeQR(text, ecl = "M") {
|
|
|
6518
6648
|
placeFinderPattern(matrix, reserved, 0, size - 7);
|
|
6519
6649
|
placeFinderPattern(matrix, reserved, size - 7, 0);
|
|
6520
6650
|
|
|
6521
|
-
const alignPos =
|
|
6651
|
+
const alignPos = alignmentPositions(version);
|
|
6522
6652
|
for (const r of alignPos) {
|
|
6523
6653
|
for (const c of alignPos) {
|
|
6524
6654
|
if (reserved[r]?.[c]) continue;
|
|
@@ -6528,6 +6658,7 @@ function encodeQR(text, ecl = "M") {
|
|
|
6528
6658
|
|
|
6529
6659
|
placeTimingPatterns(matrix, reserved);
|
|
6530
6660
|
reserveFormatArea(matrix, reserved);
|
|
6661
|
+
placeVersionInfo(matrix, reserved, version);
|
|
6531
6662
|
|
|
6532
6663
|
// Try all masks, pick lowest penalty
|
|
6533
6664
|
let bestMask = 0;
|
|
@@ -6995,10 +7126,16 @@ function createSheet(root) {
|
|
|
6995
7126
|
// Prevent double-init
|
|
6996
7127
|
if (root._faqirSheet) return root._faqirSheet;
|
|
6997
7128
|
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
const
|
|
7001
|
-
const
|
|
7129
|
+
// Overlay and panel are direct children; trigger and close buttons are
|
|
7130
|
+
// matched by owner, so a nested component's close leaves this one alone.
|
|
7131
|
+
const [trigger] = ownParts(root, "trigger");
|
|
7132
|
+
const overlay = root.querySelector(":scope > [data-part='overlay']");
|
|
7133
|
+
const panel = root.querySelector(":scope > [data-part='panel']");
|
|
7134
|
+
const closeButtons = ownParts(root, "close");
|
|
7135
|
+
|
|
7136
|
+
// `panel.focus()` on open does nothing without a tabindex: focus stayed on
|
|
7137
|
+
// `body`, out of reach of the trap and the root's Escape listener.
|
|
7138
|
+
if (panel && !panel.hasAttribute("tabindex")) panel.setAttribute("tabindex", "-1");
|
|
7002
7139
|
|
|
7003
7140
|
let focusCleanup = null;
|
|
7004
7141
|
let previouslyFocused = null;
|
|
@@ -7042,13 +7179,14 @@ function createSheet(root) {
|
|
|
7042
7179
|
|
|
7043
7180
|
const onEnd = () => {
|
|
7044
7181
|
cancelExitWait = null;
|
|
7182
|
+
const active = document.activeElement;
|
|
7045
7183
|
root.dataset.state = "closed";
|
|
7046
7184
|
overlay.hidden = true;
|
|
7047
7185
|
panel.hidden = true;
|
|
7048
7186
|
unlockScroll();
|
|
7049
7187
|
if (focusCleanup) focusCleanup();
|
|
7050
7188
|
focusCleanup = null;
|
|
7051
|
-
previouslyFocused
|
|
7189
|
+
returnFocus(root, previouslyFocused, active);
|
|
7052
7190
|
};
|
|
7053
7191
|
|
|
7054
7192
|
// The panel's OWN `transform` — not a close button's `background`, which
|
|
@@ -7158,12 +7296,14 @@ function createSidebar(root) {
|
|
|
7158
7296
|
// Prevent double-init.
|
|
7159
7297
|
if (root._faqirSidebar) return root._faqirSidebar;
|
|
7160
7298
|
|
|
7161
|
-
const overlay = root.querySelector("[data-part='overlay']");
|
|
7162
|
-
const panel = root.querySelector("[data-part='panel']") || root;
|
|
7299
|
+
const overlay = root.querySelector(":scope > [data-part='overlay']");
|
|
7300
|
+
const panel = root.querySelector(":scope > [data-part='panel']") || root;
|
|
7163
7301
|
|
|
7164
|
-
// Toggle buttons:
|
|
7165
|
-
// triggers in the app shell that point at this sidebar by id. Both call
|
|
7166
|
-
|
|
7302
|
+
// Toggle buttons: the sidebar's own `[data-part='trigger']`s, plus external
|
|
7303
|
+
// triggers in the app shell that point at this sidebar by id. Both call
|
|
7304
|
+
// toggle(). A dropdown's trigger in the footer belongs to the dropdown: it
|
|
7305
|
+
// used to have its aria-expanded forced and its click rail the sidebar.
|
|
7306
|
+
const internalTriggers = ownParts(root, "trigger");
|
|
7167
7307
|
const externalTriggers = root.id
|
|
7168
7308
|
? [...document.querySelectorAll(`[data-sidebar-toggle="${root.id}"]`)]
|
|
7169
7309
|
: [];
|
|
@@ -7773,10 +7913,13 @@ function createTable(root) {
|
|
|
7773
7913
|
// Prevent double-init
|
|
7774
7914
|
if (root._faqirTable) return root._faqirTable;
|
|
7775
7915
|
|
|
7916
|
+
// A table nested in a detail row keeps its own parts and events: `tbody`
|
|
7917
|
+
// contains it, so every deep lookup and delegated handler checks `mine`.
|
|
7918
|
+
const mine = (el) => el.closest("[data-ui='table']") === root;
|
|
7776
7919
|
const table = root.querySelector("[data-part='table']");
|
|
7777
7920
|
const thead = root.querySelector("[data-part='thead']");
|
|
7778
7921
|
const tbody = root.querySelector("[data-part='tbody']");
|
|
7779
|
-
const tfoot = root.
|
|
7922
|
+
const tfoot = [...root.querySelectorAll("[data-part='tfoot']")].find(mine);
|
|
7780
7923
|
const headerCheckbox = thead?.querySelector("[data-part='checkbox']");
|
|
7781
7924
|
|
|
7782
7925
|
// ── Options (read once from the root's opt-in attributes) ──
|
|
@@ -7996,23 +8139,14 @@ function createTable(root) {
|
|
|
7996
8139
|
|
|
7997
8140
|
// ── Intl formatting ──
|
|
7998
8141
|
function formatValue(n, fmt) {
|
|
8142
|
+
const o =
|
|
8143
|
+
fmt === "currency"
|
|
8144
|
+
? { style: fmt, currency: opts.currency, currencySign: accounting ? "accounting" : "standard" }
|
|
8145
|
+
: fmt === "percent"
|
|
8146
|
+
? { style: fmt, maximumFractionDigits: 2 }
|
|
8147
|
+
: fmt === "number" && { maximumFractionDigits: 2 };
|
|
7999
8148
|
try {
|
|
8000
|
-
if (fmt === "
|
|
8001
|
-
return new Intl.NumberFormat(opts.locale, {
|
|
8002
|
-
style: "currency",
|
|
8003
|
-
currency: opts.currency,
|
|
8004
|
-
currencySign: accounting ? "accounting" : "standard",
|
|
8005
|
-
}).format(n);
|
|
8006
|
-
}
|
|
8007
|
-
if (fmt === "percent") {
|
|
8008
|
-
return new Intl.NumberFormat(opts.locale, {
|
|
8009
|
-
style: "percent",
|
|
8010
|
-
maximumFractionDigits: 2,
|
|
8011
|
-
}).format(n / 100);
|
|
8012
|
-
}
|
|
8013
|
-
if (fmt === "number") {
|
|
8014
|
-
return new Intl.NumberFormat(opts.locale, { maximumFractionDigits: 2 }).format(n);
|
|
8015
|
-
}
|
|
8149
|
+
if (o) return new Intl.NumberFormat(opts.locale, o).format(fmt === "percent" ? n / 100 : n);
|
|
8016
8150
|
} catch {
|
|
8017
8151
|
/* unknown locale/currency — fall through to raw */
|
|
8018
8152
|
}
|
|
@@ -8253,13 +8387,17 @@ function createTable(root) {
|
|
|
8253
8387
|
refreshStripes();
|
|
8254
8388
|
}
|
|
8255
8389
|
|
|
8390
|
+
function sortChanged() {
|
|
8391
|
+
applySorts();
|
|
8392
|
+
emit("sort", { sorts: sorts.map((s) => ({ column: s.index, direction: s.dir })) });
|
|
8393
|
+
scheduleStateSave();
|
|
8394
|
+
}
|
|
8395
|
+
|
|
8256
8396
|
/** Legacy API: sort one column with an explicit direction. */
|
|
8257
8397
|
function sort(columnIndex, direction) {
|
|
8258
8398
|
if (!tbody || bodyRows().length === 0) return;
|
|
8259
8399
|
sorts = [{ index: columnIndex, dir: direction }];
|
|
8260
|
-
|
|
8261
|
-
emit("sort", { sorts: sorts.map((s) => ({ column: s.index, direction: s.dir })) });
|
|
8262
|
-
scheduleStateSave();
|
|
8400
|
+
sortChanged();
|
|
8263
8401
|
}
|
|
8264
8402
|
|
|
8265
8403
|
/** Multi-sort API: sortBy([{ column, direction }...]). */
|
|
@@ -8271,17 +8409,13 @@ function createTable(root) {
|
|
|
8271
8409
|
dir: s.direction || s.dir || "ascending",
|
|
8272
8410
|
}))
|
|
8273
8411
|
.filter((s) => Number.isInteger(s.index) && s.index >= 0 && s.index < columnCount());
|
|
8274
|
-
|
|
8275
|
-
emit("sort", { sorts: sorts.map((s) => ({ column: s.index, direction: s.dir })) });
|
|
8276
|
-
scheduleStateSave();
|
|
8412
|
+
sortChanged();
|
|
8277
8413
|
}
|
|
8278
8414
|
|
|
8279
8415
|
function clearSort() {
|
|
8280
8416
|
if (!sorts.length) return;
|
|
8281
8417
|
sorts = [];
|
|
8282
|
-
|
|
8283
|
-
emit("sort", { sorts: [] });
|
|
8284
|
-
scheduleStateSave();
|
|
8418
|
+
sortChanged();
|
|
8285
8419
|
}
|
|
8286
8420
|
|
|
8287
8421
|
function handleSortRequest(idx, additive) {
|
|
@@ -8298,9 +8432,7 @@ function createTable(root) {
|
|
|
8298
8432
|
} else {
|
|
8299
8433
|
sorts = dir === null ? [] : [{ index: idx, dir }];
|
|
8300
8434
|
}
|
|
8301
|
-
|
|
8302
|
-
emit("sort", { sorts: sorts.map((s) => ({ column: s.index, direction: s.dir })) });
|
|
8303
|
-
scheduleStateSave();
|
|
8435
|
+
sortChanged();
|
|
8304
8436
|
}
|
|
8305
8437
|
|
|
8306
8438
|
// ── Selection ──
|
|
@@ -8316,8 +8448,7 @@ function createTable(root) {
|
|
|
8316
8448
|
const row = rows[index];
|
|
8317
8449
|
setRowSelected(row, !row.hasAttribute("data-selected"));
|
|
8318
8450
|
lastSelectedIndex = index;
|
|
8319
|
-
|
|
8320
|
-
emit("selection-change", { selected: getSelected() });
|
|
8451
|
+
selectionChanged();
|
|
8321
8452
|
}
|
|
8322
8453
|
|
|
8323
8454
|
function selectAll() {
|
|
@@ -8326,15 +8457,13 @@ function createTable(root) {
|
|
|
8326
8457
|
if (filtered && row.hasAttribute("data-filtered")) continue;
|
|
8327
8458
|
setRowSelected(row, true);
|
|
8328
8459
|
}
|
|
8329
|
-
|
|
8330
|
-
emit("selection-change", { selected: getSelected() });
|
|
8460
|
+
selectionChanged();
|
|
8331
8461
|
}
|
|
8332
8462
|
|
|
8333
8463
|
function deselectAll() {
|
|
8334
8464
|
for (const row of bodyRows()) setRowSelected(row, false);
|
|
8335
8465
|
lastSelectedIndex = -1;
|
|
8336
|
-
|
|
8337
|
-
emit("selection-change", { selected: getSelected() });
|
|
8466
|
+
selectionChanged();
|
|
8338
8467
|
}
|
|
8339
8468
|
|
|
8340
8469
|
function getSelected() {
|
|
@@ -8351,23 +8480,19 @@ function createTable(root) {
|
|
|
8351
8480
|
for (let i = start; i <= end; i++) {
|
|
8352
8481
|
if (i >= 0 && i < rows.length && isRowVisible(rows[i])) setRowSelected(rows[i], true);
|
|
8353
8482
|
}
|
|
8483
|
+
}
|
|
8484
|
+
|
|
8485
|
+
function selectionChanged() {
|
|
8354
8486
|
updateHeaderCheckbox();
|
|
8487
|
+
emit("selection-change", { selected: getSelected() });
|
|
8355
8488
|
}
|
|
8356
8489
|
|
|
8357
8490
|
function updateHeaderCheckbox() {
|
|
8358
8491
|
if (!headerCheckbox) return;
|
|
8359
8492
|
const rows = visibleBodyRows();
|
|
8360
|
-
const
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
headerCheckbox.indeterminate = false;
|
|
8364
|
-
} else if (selected.length === rows.length) {
|
|
8365
|
-
headerCheckbox.checked = true;
|
|
8366
|
-
headerCheckbox.indeterminate = false;
|
|
8367
|
-
} else {
|
|
8368
|
-
headerCheckbox.checked = false;
|
|
8369
|
-
headerCheckbox.indeterminate = true;
|
|
8370
|
-
}
|
|
8493
|
+
const n = rows.filter((r) => r.hasAttribute("data-selected")).length;
|
|
8494
|
+
headerCheckbox.checked = n > 0 && n === rows.length;
|
|
8495
|
+
headerCheckbox.indeterminate = n > 0 && n < rows.length;
|
|
8371
8496
|
}
|
|
8372
8497
|
|
|
8373
8498
|
// ── Filtering ──
|
|
@@ -8456,6 +8581,7 @@ function createTable(root) {
|
|
|
8456
8581
|
refreshStripes();
|
|
8457
8582
|
updateHeaderCheckbox();
|
|
8458
8583
|
computeAggregates();
|
|
8584
|
+
setupNavigability();
|
|
8459
8585
|
emit("filter", { query: globalFilter, visible, total: rows.length });
|
|
8460
8586
|
scheduleStateSave();
|
|
8461
8587
|
}
|
|
@@ -8476,7 +8602,7 @@ function createTable(root) {
|
|
|
8476
8602
|
applyFilters();
|
|
8477
8603
|
}
|
|
8478
8604
|
function syncFilterInputs() {
|
|
8479
|
-
const g = root.
|
|
8605
|
+
const g = [...root.querySelectorAll("[data-part='filter']")].find(mine);
|
|
8480
8606
|
if (g && g.value !== globalFilter) g.value = globalFilter;
|
|
8481
8607
|
const fr = filterRowEl();
|
|
8482
8608
|
if (!fr) return;
|
|
@@ -8499,6 +8625,7 @@ function createTable(root) {
|
|
|
8499
8625
|
for (const m of groupMembers(gh)) toggleAttr(m, "data-collapsed", !expand);
|
|
8500
8626
|
refreshStripes();
|
|
8501
8627
|
updateHeaderCheckbox();
|
|
8628
|
+
setupNavigability();
|
|
8502
8629
|
emit("group-toggle", { group: gh, expanded: expand });
|
|
8503
8630
|
}
|
|
8504
8631
|
|
|
@@ -8527,6 +8654,7 @@ function createTable(root) {
|
|
|
8527
8654
|
}
|
|
8528
8655
|
refreshStripes();
|
|
8529
8656
|
updateHeaderCheckbox();
|
|
8657
|
+
setupNavigability();
|
|
8530
8658
|
emit("tree-toggle", { row, expanded: expand });
|
|
8531
8659
|
}
|
|
8532
8660
|
|
|
@@ -8541,20 +8669,15 @@ function createTable(root) {
|
|
|
8541
8669
|
emit("row-expand", { row, detail, expanded: expand });
|
|
8542
8670
|
}
|
|
8543
8671
|
|
|
8544
|
-
function
|
|
8672
|
+
function setAllExpanded(expand) {
|
|
8545
8673
|
mutedRun(() => {
|
|
8546
|
-
for (const gh of groupHeaders()) toggleGroup(gh,
|
|
8547
|
-
if (opts.tree) for (const r of bodyRows()) if (isTreeParent(r)) toggleRow(r,
|
|
8548
|
-
});
|
|
8549
|
-
refreshStripes();
|
|
8550
|
-
}
|
|
8551
|
-
function collapseAll() {
|
|
8552
|
-
mutedRun(() => {
|
|
8553
|
-
for (const gh of groupHeaders()) toggleGroup(gh, false);
|
|
8554
|
-
if (opts.tree) for (const r of bodyRows()) if (isTreeParent(r)) toggleRow(r, false);
|
|
8674
|
+
for (const gh of groupHeaders()) toggleGroup(gh, expand);
|
|
8675
|
+
if (opts.tree) for (const r of bodyRows()) if (isTreeParent(r)) toggleRow(r, expand);
|
|
8555
8676
|
});
|
|
8556
8677
|
refreshStripes();
|
|
8557
8678
|
}
|
|
8679
|
+
const expandAll = () => setAllExpanded(true);
|
|
8680
|
+
const collapseAll = () => setAllExpanded(false);
|
|
8558
8681
|
|
|
8559
8682
|
// ── Aggregates (tfoot, group headers, tree parents) ──
|
|
8560
8683
|
function writeAggregate(cell, rows, ownIndex) {
|
|
@@ -8589,7 +8712,7 @@ function createTable(root) {
|
|
|
8589
8712
|
const fmt =
|
|
8590
8713
|
fn === "count"
|
|
8591
8714
|
? cell.getAttribute("data-format")
|
|
8592
|
-
: cell
|
|
8715
|
+
: cellFormatOf(cell, col) || "number";
|
|
8593
8716
|
cell.textContent = fmt ? formatValue(out, fmt) : String(out);
|
|
8594
8717
|
applyNegativeFlag(cell);
|
|
8595
8718
|
}
|
|
@@ -8773,17 +8896,15 @@ function createTable(root) {
|
|
|
8773
8896
|
|
|
8774
8897
|
function onEditorKeydown(e) {
|
|
8775
8898
|
if (!editing) return;
|
|
8899
|
+
e.stopPropagation(); // keep grid navigation away from typing
|
|
8776
8900
|
if (e.key === "Enter") {
|
|
8777
8901
|
e.preventDefault();
|
|
8778
|
-
e.stopPropagation();
|
|
8779
8902
|
commitEdit(true);
|
|
8780
8903
|
} else if (e.key === "Escape") {
|
|
8781
8904
|
e.preventDefault();
|
|
8782
|
-
e.stopPropagation();
|
|
8783
8905
|
cancelEdit(true);
|
|
8784
8906
|
} else if (e.key === "Tab") {
|
|
8785
8907
|
e.preventDefault();
|
|
8786
|
-
e.stopPropagation();
|
|
8787
8908
|
const from = editing.cell;
|
|
8788
8909
|
if (commitEdit()) {
|
|
8789
8910
|
const next = nextEditableCell(from, e.shiftKey ? -1 : 1);
|
|
@@ -8791,7 +8912,6 @@ function createTable(root) {
|
|
|
8791
8912
|
}
|
|
8792
8913
|
} else {
|
|
8793
8914
|
e.target.removeAttribute("data-invalid");
|
|
8794
|
-
e.stopPropagation(); // keep grid navigation away from typing
|
|
8795
8915
|
}
|
|
8796
8916
|
}
|
|
8797
8917
|
function onEditorBlur(e) {
|
|
@@ -8832,6 +8952,7 @@ function createTable(root) {
|
|
|
8832
8952
|
if (!th) return;
|
|
8833
8953
|
forEachColumnCell(i, (cell) => toggleAttr(cell, "data-col-hidden", hidden));
|
|
8834
8954
|
refreshPins();
|
|
8955
|
+
setupNavigability();
|
|
8835
8956
|
emit("col-visibility", { column: i, hidden: !!hidden });
|
|
8836
8957
|
scheduleStateSave();
|
|
8837
8958
|
}
|
|
@@ -8943,14 +9064,11 @@ function createTable(root) {
|
|
|
8943
9064
|
}
|
|
8944
9065
|
|
|
8945
9066
|
function elementAt(e) {
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
return null;
|
|
8951
|
-
}
|
|
9067
|
+
try {
|
|
9068
|
+
return document.elementFromPoint(e.clientX, e.clientY);
|
|
9069
|
+
} catch {
|
|
9070
|
+
return null; // elementFromPoint missing or throwing (headless)
|
|
8952
9071
|
}
|
|
8953
|
-
return null;
|
|
8954
9072
|
}
|
|
8955
9073
|
|
|
8956
9074
|
function resolveRowAt(e) {
|
|
@@ -8959,16 +9077,13 @@ function createTable(root) {
|
|
|
8959
9077
|
return row && tbody?.contains(row) ? row : null;
|
|
8960
9078
|
}
|
|
8961
9079
|
|
|
8962
|
-
|
|
9080
|
+
/** "before"/"after" the pointer's half of el: vertical for rows, horizontal (RTL-aware) for columns. */
|
|
9081
|
+
function dropPos(e, el, horizontal) {
|
|
8963
9082
|
const rect = el.getBoundingClientRect?.();
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
const rect = el.getBoundingClientRect?.();
|
|
8969
|
-
if (!rect || !rect.width) return "after";
|
|
8970
|
-
const before = e.clientX < rect.left + rect.width / 2;
|
|
8971
|
-
return before !== isRTL ? "before" : "after";
|
|
9083
|
+
const size = rect && (horizontal ? rect.width : rect.height);
|
|
9084
|
+
if (!size) return "after";
|
|
9085
|
+
const before = horizontal ? e.clientX < rect.left + size / 2 !== isRTL : e.clientY < rect.top + size / 2;
|
|
9086
|
+
return before ? "before" : "after";
|
|
8972
9087
|
}
|
|
8973
9088
|
|
|
8974
9089
|
function bindDocDrag() {
|
|
@@ -9068,7 +9183,7 @@ function createTable(root) {
|
|
|
9068
9183
|
if (!th) th = e.target?.closest?.("[data-part='th']");
|
|
9069
9184
|
if (!th || th === drag.th || !headerRowEl()?.contains(th)) return;
|
|
9070
9185
|
th.setAttribute("data-drop-target", "");
|
|
9071
|
-
th.setAttribute("data-drop-pos",
|
|
9186
|
+
th.setAttribute("data-drop-pos", dropPos(e, th, true));
|
|
9072
9187
|
}
|
|
9073
9188
|
}
|
|
9074
9189
|
|
|
@@ -9290,15 +9405,11 @@ function createTable(root) {
|
|
|
9290
9405
|
existing.remove();
|
|
9291
9406
|
existing = null;
|
|
9292
9407
|
}
|
|
9293
|
-
|
|
9294
|
-
if (isParent)
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
el.setAttribute("aria-label", "Toggle");
|
|
9299
|
-
} else {
|
|
9300
|
-
el = document.createElement("span");
|
|
9301
|
-
el.setAttribute("data-part", "expander");
|
|
9408
|
+
const el = document.createElement(isParent ? "button" : "span");
|
|
9409
|
+
if (isParent) el.type = "button";
|
|
9410
|
+
el.setAttribute("data-part", "expander");
|
|
9411
|
+
if (isParent) el.setAttribute("aria-label", "Toggle");
|
|
9412
|
+
else {
|
|
9302
9413
|
el.setAttribute("data-leaf", "");
|
|
9303
9414
|
el.setAttribute("aria-hidden", "true");
|
|
9304
9415
|
}
|
|
@@ -9394,14 +9505,35 @@ function createTable(root) {
|
|
|
9394
9505
|
return rows;
|
|
9395
9506
|
}
|
|
9396
9507
|
|
|
9508
|
+
// APG grid: links, buttons and fields inside a navigable grid's cells leave the
|
|
9509
|
+
// Tab order (Enter/F2 reaches them, Escape comes back). The table's own controls
|
|
9510
|
+
// stay focusable. destroy() puts the original tabindex back.
|
|
9511
|
+
const demoted = new WeakMap();
|
|
9512
|
+
const cellControls = (cell) =>
|
|
9513
|
+
[...cell.querySelectorAll("input,select,textarea,button,a[href],[tabindex]")].filter(
|
|
9514
|
+
(el) => mine(el) && !el.closest("[data-part='checkbox'],[data-part='drag-handle'],[data-part='expander'],[data-part='row-toggle']")
|
|
9515
|
+
);
|
|
9516
|
+
|
|
9517
|
+
// Re-run after anything that can take the Tab stop away (refresh, filter,
|
|
9518
|
+
// collapse, column hide): only the Tab stop moves, never focus.
|
|
9397
9519
|
function setupNavigability() {
|
|
9398
9520
|
if (opts.navigable) {
|
|
9521
|
+
const rows = navMatrix();
|
|
9522
|
+
// A stale stop — its row removed, filtered or collapsed, or its column hidden —
|
|
9523
|
+
// is dropped so the first visible cell takes over.
|
|
9524
|
+
if (activeCell && (!rows.includes(activeCell.parentElement) || activeCell.hasAttribute("data-col-hidden"))) {
|
|
9525
|
+
activeCell.setAttribute("tabindex", "-1");
|
|
9526
|
+
activeCell = null;
|
|
9527
|
+
}
|
|
9399
9528
|
let first = null;
|
|
9400
|
-
for (const row of
|
|
9529
|
+
for (const row of rows) {
|
|
9401
9530
|
for (const cell of cellsOf(row)) {
|
|
9402
|
-
if (cell.hasAttribute("data-col-hidden"))
|
|
9403
|
-
if (!first && !activeCell) first = cell;
|
|
9531
|
+
if (!first && !activeCell && !cell.hasAttribute("data-col-hidden")) first = cell;
|
|
9404
9532
|
cell.setAttribute("tabindex", cell === (activeCell || first) ? "0" : "-1");
|
|
9533
|
+
for (const el of cellControls(cell)) {
|
|
9534
|
+
if (!demoted.has(el)) demoted.set(el, el.getAttribute("tabindex"));
|
|
9535
|
+
el.setAttribute("tabindex", "-1");
|
|
9536
|
+
}
|
|
9405
9537
|
}
|
|
9406
9538
|
}
|
|
9407
9539
|
if (!activeCell) activeCell = first;
|
|
@@ -9416,11 +9548,7 @@ function createTable(root) {
|
|
|
9416
9548
|
if (activeCell && activeCell !== cell) activeCell.setAttribute("tabindex", "-1");
|
|
9417
9549
|
activeCell = cell;
|
|
9418
9550
|
cell.setAttribute("tabindex", "0");
|
|
9419
|
-
|
|
9420
|
-
cell.focus();
|
|
9421
|
-
} catch {
|
|
9422
|
-
/* unfocusable */
|
|
9423
|
-
}
|
|
9551
|
+
cell.focus();
|
|
9424
9552
|
}
|
|
9425
9553
|
|
|
9426
9554
|
function moveFocus(rowDelta, colDelta, edge) {
|
|
@@ -9460,7 +9588,7 @@ function createTable(root) {
|
|
|
9460
9588
|
if (!opts.navigable) return;
|
|
9461
9589
|
const cell = e.target.closest("[data-part='td'],[data-part='th']");
|
|
9462
9590
|
if (!cell || cell === activeCell || !root.contains(cell)) return;
|
|
9463
|
-
if (e.target !== cell) return; //
|
|
9591
|
+
if (e.target !== cell && !demoted.has(e.target)) return; // the table's own control, not the cell
|
|
9464
9592
|
if (activeCell) activeCell.setAttribute("tabindex", "-1");
|
|
9465
9593
|
activeCell = cell;
|
|
9466
9594
|
cell.setAttribute("tabindex", "0");
|
|
@@ -9495,13 +9623,12 @@ function createTable(root) {
|
|
|
9495
9623
|
if (e.shiftKey && lastSelectedIndex >= 0) {
|
|
9496
9624
|
e.preventDefault();
|
|
9497
9625
|
selectRange(lastSelectedIndex, index);
|
|
9498
|
-
|
|
9626
|
+
selectionChanged();
|
|
9499
9627
|
return;
|
|
9500
9628
|
}
|
|
9501
9629
|
toggleAttr(row, "data-selected", checkbox.checked);
|
|
9502
9630
|
lastSelectedIndex = index;
|
|
9503
|
-
|
|
9504
|
-
emit("selection-change", { selected: getSelected() });
|
|
9631
|
+
selectionChanged();
|
|
9505
9632
|
}
|
|
9506
9633
|
|
|
9507
9634
|
function onRootClick(e) {
|
|
@@ -9541,7 +9668,7 @@ function createTable(root) {
|
|
|
9541
9668
|
if (index < 0) return;
|
|
9542
9669
|
if (e.shiftKey && lastSelectedIndex >= 0) {
|
|
9543
9670
|
selectRange(lastSelectedIndex, index);
|
|
9544
|
-
|
|
9671
|
+
selectionChanged();
|
|
9545
9672
|
return;
|
|
9546
9673
|
}
|
|
9547
9674
|
if (opts.selectable != null) {
|
|
@@ -9553,8 +9680,7 @@ function createTable(root) {
|
|
|
9553
9680
|
setRowSelected(row, !row.hasAttribute("data-selected"));
|
|
9554
9681
|
}
|
|
9555
9682
|
lastSelectedIndex = index;
|
|
9556
|
-
|
|
9557
|
-
emit("selection-change", { selected: getSelected() });
|
|
9683
|
+
selectionChanged();
|
|
9558
9684
|
}
|
|
9559
9685
|
}
|
|
9560
9686
|
|
|
@@ -9614,10 +9740,18 @@ function createTable(root) {
|
|
|
9614
9740
|
return;
|
|
9615
9741
|
}
|
|
9616
9742
|
|
|
9743
|
+
const cell = target.closest?.("[data-part='td'],[data-part='th']");
|
|
9744
|
+
|
|
9745
|
+
// Escape in a cell's control returns to the cell.
|
|
9746
|
+
if (e.key === "Escape" && demoted.has(target)) {
|
|
9747
|
+
e.preventDefault();
|
|
9748
|
+
focusCell(cell);
|
|
9749
|
+
return;
|
|
9750
|
+
}
|
|
9751
|
+
|
|
9617
9752
|
// Never hijack typing or native control activation (buttons own Enter/Space via click).
|
|
9618
9753
|
if (target.closest?.("input,select,textarea,button,a")) return;
|
|
9619
9754
|
|
|
9620
|
-
const cell = target.closest?.("[data-part='td'],[data-part='th']");
|
|
9621
9755
|
const inHeader = cell && thead?.contains(cell);
|
|
9622
9756
|
|
|
9623
9757
|
// Sortable header keyboard activation.
|
|
@@ -9708,6 +9842,12 @@ function createTable(root) {
|
|
|
9708
9842
|
if (isCellEditable(cell)) {
|
|
9709
9843
|
e.preventDefault();
|
|
9710
9844
|
startEdit(cell);
|
|
9845
|
+
return;
|
|
9846
|
+
}
|
|
9847
|
+
const ctl = cellControls(cell)[0];
|
|
9848
|
+
if (ctl) {
|
|
9849
|
+
e.preventDefault();
|
|
9850
|
+
ctl.focus();
|
|
9711
9851
|
}
|
|
9712
9852
|
return;
|
|
9713
9853
|
}
|
|
@@ -9755,7 +9895,7 @@ function createTable(root) {
|
|
|
9755
9895
|
|
|
9756
9896
|
function measureSticky() {
|
|
9757
9897
|
const needsHeaderVar =
|
|
9758
|
-
root.hasAttribute("data-sticky-header") ||
|
|
9898
|
+
root.hasAttribute("data-sticky-header") || bodyRows().some((r) => r.dataset.pin === "top");
|
|
9759
9899
|
if (needsHeaderVar && thead && thead.offsetHeight) {
|
|
9760
9900
|
root.style.setProperty("--table-thead-h", thead.offsetHeight + "px");
|
|
9761
9901
|
}
|
|
@@ -9794,6 +9934,12 @@ function createTable(root) {
|
|
|
9794
9934
|
cleanups.length = 0;
|
|
9795
9935
|
observer?.disconnect();
|
|
9796
9936
|
observer = null;
|
|
9937
|
+
for (const el of root.querySelectorAll("[tabindex]")) {
|
|
9938
|
+
if (demoted.has(el)) {
|
|
9939
|
+
const t = demoted.get(el);
|
|
9940
|
+
t == null ? el.removeAttribute("tabindex") : el.setAttribute("tabindex", t);
|
|
9941
|
+
}
|
|
9942
|
+
}
|
|
9797
9943
|
for (const t of timers) clearTimeout(t);
|
|
9798
9944
|
timers.clear();
|
|
9799
9945
|
clearTimeout(saveTimer);
|
|
@@ -9803,13 +9949,14 @@ function createTable(root) {
|
|
|
9803
9949
|
// ── Wire up ──
|
|
9804
9950
|
if (thead) on(thead, "click", onHeaderClick);
|
|
9805
9951
|
if (headerCheckbox) on(headerCheckbox, "change", onHeaderCheckboxChange);
|
|
9806
|
-
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9952
|
+
const onOwn = (target, ev, fn) => on(target, ev, (e) => mine(e.target) && fn(e));
|
|
9953
|
+
if (tbody) onOwn(tbody, "change", onRowCheckboxChange);
|
|
9954
|
+
onOwn(root, "click", onRootClick);
|
|
9955
|
+
onOwn(root, "dblclick", onDblClick);
|
|
9956
|
+
onOwn(root, "keydown", onKeydown);
|
|
9957
|
+
onOwn(root, "input", onFilterInput);
|
|
9958
|
+
onOwn(root, "pointerdown", onPointerDown);
|
|
9959
|
+
onOwn(root, "focusin", onFocusIn);
|
|
9813
9960
|
if (typeof window !== "undefined") {
|
|
9814
9961
|
let resizeTimer = null;
|
|
9815
9962
|
const onWinResize = () => {
|
|
@@ -9894,9 +10041,14 @@ function createTabs(root) {
|
|
|
9894
10041
|
// Prevent double-init
|
|
9895
10042
|
if (root._faqirTabs) return root._faqirTabs;
|
|
9896
10043
|
|
|
9897
|
-
|
|
9898
|
-
|
|
9899
|
-
|
|
10044
|
+
// Direct children only: a component nested in a panel (a collapsible's
|
|
10045
|
+
// summary, another tabs) has parts of the same names, and a deep query took
|
|
10046
|
+
// them for this component's tabs and shifted the trigger↔panel pairing.
|
|
10047
|
+
const list = root.querySelector(":scope > [data-part='list']");
|
|
10048
|
+
const triggers = () => [
|
|
10049
|
+
...root.querySelectorAll(":scope > [data-part='list'] > [data-part='trigger']"),
|
|
10050
|
+
];
|
|
10051
|
+
const panels = () => [...root.querySelectorAll(":scope > [data-part='panel']")];
|
|
9900
10052
|
|
|
9901
10053
|
function activate(index) {
|
|
9902
10054
|
const allTriggers = triggers();
|
|
@@ -10565,8 +10717,11 @@ function createToastContainer(root) {
|
|
|
10565
10717
|
* exclusivity when the controls are used standalone (no `l-model`), the
|
|
10566
10718
|
* `[data-state]` styling hook, and a `faqir:change` notification.
|
|
10567
10719
|
*
|
|
10568
|
-
* Every
|
|
10720
|
+
* Every user mutation funnels through a native `change` event on the affected
|
|
10569
10721
|
* input, so the engine's `l-model` listener and this controller stay in lockstep.
|
|
10722
|
+
* Store-driven `l-model` writes arrive as the engine's `faqir:model` event (and
|
|
10723
|
+
* a form `reset`); both re-sync state without a `faqir:change`. A plain script
|
|
10724
|
+
* write (`input.checked = x`) is silent and is not observed — use `setValue()`.
|
|
10570
10725
|
*/
|
|
10571
10726
|
function createToggleGroup(root) {
|
|
10572
10727
|
// Prevent double-init.
|
|
@@ -10727,13 +10882,23 @@ function createToggleGroup(root) {
|
|
|
10727
10882
|
if (el) select(el);
|
|
10728
10883
|
}
|
|
10729
10884
|
|
|
10885
|
+
// A store-driven `l-model` write sets `.checked` silently and announces it
|
|
10886
|
+
// with `faqir:model`; follow it without re-emitting `faqir:change` (the user
|
|
10887
|
+
// did not act). A form reset restores defaults silently the same way.
|
|
10888
|
+
const form = root.closest("form");
|
|
10889
|
+
const onReset = () => setTimeout(syncState, 0);
|
|
10890
|
+
|
|
10730
10891
|
root.addEventListener("change", onChange);
|
|
10892
|
+
root.addEventListener("faqir:model", syncState);
|
|
10731
10893
|
root.addEventListener("keydown", onKeyDown);
|
|
10894
|
+
if (form) form.addEventListener("reset", onReset);
|
|
10732
10895
|
syncState();
|
|
10733
10896
|
|
|
10734
10897
|
function destroy() {
|
|
10735
10898
|
root.removeEventListener("change", onChange);
|
|
10899
|
+
root.removeEventListener("faqir:model", syncState);
|
|
10736
10900
|
root.removeEventListener("keydown", onKeyDown);
|
|
10901
|
+
if (form) form.removeEventListener("reset", onReset);
|
|
10737
10902
|
delete root._faqirToggleGroup;
|
|
10738
10903
|
}
|
|
10739
10904
|
|
|
@@ -10754,7 +10919,8 @@ function createTooltip(root) {
|
|
|
10754
10919
|
if (root._faqirTooltip) return root._faqirTooltip;
|
|
10755
10920
|
|
|
10756
10921
|
const trigger = root.querySelector("[data-part='trigger']");
|
|
10757
|
-
|
|
10922
|
+
// Not the content of a collapsible or popover used as the trigger.
|
|
10923
|
+
const [content] = ownParts(root, "content");
|
|
10758
10924
|
|
|
10759
10925
|
let showTimer = null;
|
|
10760
10926
|
let hideTimer = null;
|
|
@@ -11352,7 +11518,9 @@ function createTreeView(root) {
|
|
|
11352
11518
|
/** One value, deep-copied into something plain, finite and printable. */
|
|
11353
11519
|
function snapshotValue(value, stack, depth) {
|
|
11354
11520
|
if (typeof value === 'function') {
|
|
11355
|
-
|
|
11521
|
+
// Scope methods are bound [1.1F-05]; report the name the author wrote.
|
|
11522
|
+
var name = String(value.name || '').replace(/^bound /, '');
|
|
11523
|
+
return '[Function' + (name ? ' ' + name : '') + ']';
|
|
11356
11524
|
}
|
|
11357
11525
|
if (value === null || typeof value !== 'object') return value;
|
|
11358
11526
|
if (value.nodeType === 1 && value.tagName) {
|
|
@@ -11596,22 +11764,35 @@ function createTreeView(root) {
|
|
|
11596
11764
|
}
|
|
11597
11765
|
}
|
|
11598
11766
|
|
|
11767
|
+
// Every controller inside `root` (a document or an element), `root` included.
|
|
11768
|
+
// Controllers are double-init guarded, so a second call is a no-op.
|
|
11769
|
+
function startControllers(root) {
|
|
11770
|
+
var own = root.getAttribute && root.getAttribute('data-ui');
|
|
11771
|
+
if (own && controllerRegistry[own]) initController(own, root);
|
|
11772
|
+
var names = Object.keys(controllerRegistry);
|
|
11773
|
+
for (var n = 0; n < names.length; n++) {
|
|
11774
|
+
var els = root.querySelectorAll('[data-ui="' + names[n] + '"]');
|
|
11775
|
+
for (var e = 0; e < els.length; e++) initController(names[n], els[e]);
|
|
11776
|
+
}
|
|
11777
|
+
}
|
|
11778
|
+
|
|
11779
|
+
// `Faqir.initTree` — the same order bootstrap uses: a connected subtree's
|
|
11780
|
+
// controllers start first, so its `l-init` / first `l-effect` reach them
|
|
11781
|
+
// through `$ui`. A detached subtree's controllers wait for the
|
|
11782
|
+
// MutationObserver, as before. [1.1F-03]
|
|
11783
|
+
function publicInitTree(root, parentScope) {
|
|
11784
|
+
if (root.isConnected) startControllers(root);
|
|
11785
|
+
initTree(root, parentScope);
|
|
11786
|
+
}
|
|
11787
|
+
|
|
11599
11788
|
// The document's one MutationObserver, or null before the first bootstrap,
|
|
11600
11789
|
// and the <body> it is currently watching.
|
|
11601
11790
|
var mutationObserver = null;
|
|
11602
11791
|
var observedBody = null;
|
|
11603
11792
|
|
|
11604
11793
|
function bootstrap() {
|
|
11605
|
-
injectCloakStyle();
|
|
11606
|
-
|
|
11607
11794
|
// Auto-init controllers for all [data-ui] elements
|
|
11608
|
-
|
|
11609
|
-
for (var n = 0; n < names.length; n++) {
|
|
11610
|
-
var els = document.querySelectorAll('[data-ui="' + names[n] + '"]');
|
|
11611
|
-
for (var e = 0; e < els.length; e++) {
|
|
11612
|
-
initController(names[n], els[e]);
|
|
11613
|
-
}
|
|
11614
|
-
}
|
|
11795
|
+
startControllers(document);
|
|
11615
11796
|
|
|
11616
11797
|
// Find all scope roots.
|
|
11617
11798
|
// l-data elements always create a scope.
|
|
@@ -11684,12 +11865,7 @@ function createTreeView(root) {
|
|
|
11684
11865
|
// after the first sweep ran and before the MutationObserver below starts.
|
|
11685
11866
|
// Controllers are double-init guarded, so re-invoking is a no-op for
|
|
11686
11867
|
// elements initialized by the first sweep.
|
|
11687
|
-
|
|
11688
|
-
var els2 = document.querySelectorAll('[data-ui="' + names[n2] + '"]');
|
|
11689
|
-
for (var e2 = 0; e2 < els2.length; e2++) {
|
|
11690
|
-
initController(names[n2], els2[e2]);
|
|
11691
|
-
}
|
|
11692
|
-
}
|
|
11868
|
+
startControllers(document);
|
|
11693
11869
|
|
|
11694
11870
|
removeCloaks();
|
|
11695
11871
|
|
|
@@ -11715,14 +11891,14 @@ function createTreeView(root) {
|
|
|
11715
11891
|
if (node.nodeType !== 1) continue;
|
|
11716
11892
|
|
|
11717
11893
|
// Content that arrives after bootstrap has already bound by the time
|
|
11718
|
-
// it is here; leaving `l-cloak` on it means the
|
|
11719
|
-
//
|
|
11894
|
+
// it is here; leaving `l-cloak` on it means the cloak rule hides it
|
|
11895
|
+
// forever. [W3-1]
|
|
11720
11896
|
removeCloaks(node);
|
|
11721
11897
|
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11898
|
+
// Controllers first — the node's own and every one inside it — so
|
|
11899
|
+
// the `l-init` / first `l-effect` that `initTree` runs below reach
|
|
11900
|
+
// them through `$ui`. They used to start after it. [1.1F-03]
|
|
11901
|
+
startControllers(node);
|
|
11726
11902
|
|
|
11727
11903
|
// Already initialized — by `Faqir.start()` / `Faqir.initTree()` in the
|
|
11728
11904
|
// same task that inserted it (the record is only delivered on the
|
|
@@ -11739,11 +11915,6 @@ function createTreeView(root) {
|
|
|
11739
11915
|
}
|
|
11740
11916
|
|
|
11741
11917
|
if (node.querySelectorAll) {
|
|
11742
|
-
var cNames = Object.keys(controllerRegistry);
|
|
11743
|
-
for (var cn = 0; cn < cNames.length; cn++) {
|
|
11744
|
-
var cEls = node.querySelectorAll('[data-ui="' + cNames[cn] + '"]');
|
|
11745
|
-
for (var ce = 0; ce < cEls.length; ce++) initController(cNames[cn], cEls[ce]);
|
|
11746
|
-
}
|
|
11747
11918
|
var scopeEls = node.querySelectorAll('[l-data]');
|
|
11748
11919
|
for (var se = 0; se < scopeEls.length; se++) {
|
|
11749
11920
|
if (!scopeEls[se].__faqirScope) {
|
|
@@ -11814,7 +11985,7 @@ function createTreeView(root) {
|
|
|
11814
11985
|
// one time it was hand-maintained (0.1.0 in a package versioned 0.2.4), so
|
|
11815
11986
|
// the built artifact is now the only place the number is authoritative and
|
|
11816
11987
|
// the build fails if this marker goes missing.
|
|
11817
|
-
version: '1.
|
|
11988
|
+
version: '1.2.0', // @faqir:version
|
|
11818
11989
|
reactive: reactive,
|
|
11819
11990
|
effect: effect,
|
|
11820
11991
|
batch: batch,
|
|
@@ -11825,11 +11996,14 @@ function createTreeView(root) {
|
|
|
11825
11996
|
data: function(name, factory) { dataRegistry.set(name, factory); },
|
|
11826
11997
|
store: function(name, obj) { globalStores[name] = reactive(obj); },
|
|
11827
11998
|
directive: function(name, handler) { customDirectives.set(name, handler); },
|
|
11828
|
-
magic: function(name, callback) {
|
|
11999
|
+
magic: function(name, callback) {
|
|
12000
|
+
if (name === 'this') throw new Error('[Faqir] "this" is reserved: $this is the directive element');
|
|
12001
|
+
customMagics.set(name, callback);
|
|
12002
|
+
},
|
|
11829
12003
|
plugin: function(fn) { fn(Faqir); },
|
|
11830
12004
|
controller: function(name, factory) { controllerRegistry[name] = factory; },
|
|
11831
12005
|
start: bootstrap,
|
|
11832
|
-
initTree:
|
|
12006
|
+
initTree: publicInitTree,
|
|
11833
12007
|
// Snapshot of Faqir's view of one element — scope, directives, controller,
|
|
11834
12008
|
// protocol attributes. [0.7-12]
|
|
11835
12009
|
inspect: inspect,
|