@zeyos/zx 5.2.0 → 5.3.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/CHANGELOG.md +34 -0
- package/dist/types/components/card-view/card-view.d.ts +236 -0
- package/dist/zx.css +56 -0
- package/dist/zx.esm.js +453 -30
- package/dist/zx.esm.min.js +6 -6
- package/dist/zx.global.js +453 -30
- package/dist/zx.global.min.js +6 -6
- package/dist/zx.min.css +1 -1
- package/docs/api.json +41 -0
- package/docs/llms.md +30 -2
- package/package.json +1 -1
- package/src/components/card-view/card-view.css +71 -0
- package/src/components/card-view/card-view.js +505 -15
- package/src/components/search/search.css +2 -0
package/dist/zx.esm.js
CHANGED
|
@@ -4130,7 +4130,7 @@ function updateFallback(anchor, floating, { placement, offset, flip, matchWidth
|
|
|
4130
4130
|
if (matchWidth) floating.style.minWidth = `${anchorRect.width}px`;
|
|
4131
4131
|
const floatingRect = floating.getBoundingClientRect();
|
|
4132
4132
|
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
4133
|
-
const
|
|
4133
|
+
const viewportHeight2 = document.documentElement.clientHeight || window.innerHeight;
|
|
4134
4134
|
let actualPlacement = placement;
|
|
4135
4135
|
const side = placementSide(placement);
|
|
4136
4136
|
const bottomY = anchorRect.bottom + offset;
|
|
@@ -4138,9 +4138,9 @@ function updateFallback(anchor, floating, { placement, offset, flip, matchWidth
|
|
|
4138
4138
|
const rightX = anchorRect.right + offset;
|
|
4139
4139
|
const leftX = anchorRect.left - offset - floatingRect.width;
|
|
4140
4140
|
if (flip) {
|
|
4141
|
-
if (side === "bottom" && bottomY + floatingRect.height >
|
|
4141
|
+
if (side === "bottom" && bottomY + floatingRect.height > viewportHeight2 && topY >= 0) {
|
|
4142
4142
|
actualPlacement = flipPlacement(placement, "bottom", "top");
|
|
4143
|
-
} else if (side === "top" && topY < 0 && bottomY + floatingRect.height <=
|
|
4143
|
+
} else if (side === "top" && topY < 0 && bottomY + floatingRect.height <= viewportHeight2) {
|
|
4144
4144
|
actualPlacement = flipPlacement(placement, "top", "bottom");
|
|
4145
4145
|
} else if (side === "right" && rightX + floatingRect.width > viewportWidth && leftX >= 0) {
|
|
4146
4146
|
actualPlacement = flipPlacement(placement, "right", "left");
|
|
@@ -4166,7 +4166,7 @@ function updateFallback(anchor, floating, { placement, offset, flip, matchWidth
|
|
|
4166
4166
|
else top = anchorRect.top + (anchorRect.height - floatingRect.height) / 2;
|
|
4167
4167
|
}
|
|
4168
4168
|
floating.style.left = `${Math.round(clamp(left, 0, Math.max(0, viewportWidth - floatingRect.width)))}px`;
|
|
4169
|
-
floating.style.top = `${Math.round(clamp(top, 0, Math.max(0,
|
|
4169
|
+
floating.style.top = `${Math.round(clamp(top, 0, Math.max(0, viewportHeight2 - floatingRect.height)))}px`;
|
|
4170
4170
|
}
|
|
4171
4171
|
function placementSide(placement) {
|
|
4172
4172
|
return (
|
|
@@ -35167,7 +35167,11 @@ var CardView = class extends RecordView {
|
|
|
35167
35167
|
variant: "outlined",
|
|
35168
35168
|
loadingCount: 6,
|
|
35169
35169
|
headingLevel: 3,
|
|
35170
|
-
label: null
|
|
35170
|
+
label: null,
|
|
35171
|
+
keyboard: "tabs",
|
|
35172
|
+
collapsibleGroups: false,
|
|
35173
|
+
collapsedGroups: [],
|
|
35174
|
+
renderGroupHeading: null
|
|
35171
35175
|
};
|
|
35172
35176
|
/**
|
|
35173
35177
|
* Creates or enhances a card view.
|
|
@@ -35196,6 +35200,19 @@ var CardView = class extends RecordView {
|
|
|
35196
35200
|
this._cardVariant = normalizeCardVariant(options.variant);
|
|
35197
35201
|
this._cardLoadingCount = normalizeLoadingCount3(options.loadingCount);
|
|
35198
35202
|
this._cardHeadingLevel = normalizeHeadingLevel5(options.headingLevel);
|
|
35203
|
+
this._cardRoving = options.keyboard === "roving";
|
|
35204
|
+
this._cardIdPrefix = uid("zx-card-view");
|
|
35205
|
+
this._cardCollapsed = new Set(normalizeGroupOrder(options.collapsedGroups));
|
|
35206
|
+
this._cardCurrent = null;
|
|
35207
|
+
this._cardCurrentIndex = 0;
|
|
35208
|
+
this._cardTabStop = null;
|
|
35209
|
+
this._cardStickyX = null;
|
|
35210
|
+
this._cardMoving = false;
|
|
35211
|
+
this._cardHeadersById = /* @__PURE__ */ new Map();
|
|
35212
|
+
this._cardGroupOfId = /* @__PURE__ */ new Map();
|
|
35213
|
+
this._cardSectionIds = /* @__PURE__ */ new Map();
|
|
35214
|
+
this._cardIdOfNode = /* @__PURE__ */ new WeakMap();
|
|
35215
|
+
if (this._cardRoving) root.dataset.keyboard = "roving";
|
|
35199
35216
|
try {
|
|
35200
35217
|
this._initRecordView(root);
|
|
35201
35218
|
root.setAttribute("aria-label", String(options.label || this.msg("cardView.label")));
|
|
@@ -35220,10 +35237,22 @@ var CardView = class extends RecordView {
|
|
|
35220
35237
|
/** @type {MouseEvent} */
|
|
35221
35238
|
event
|
|
35222
35239
|
));
|
|
35223
|
-
this.listen(content, "keydown", (event) =>
|
|
35224
|
-
|
|
35225
|
-
|
|
35226
|
-
|
|
35240
|
+
this.listen(content, "keydown", (event) => {
|
|
35241
|
+
if (this._cardRoving) this._cardRovingKeydown(
|
|
35242
|
+
/** @type {KeyboardEvent} */
|
|
35243
|
+
event
|
|
35244
|
+
);
|
|
35245
|
+
else this._cardKeydown(
|
|
35246
|
+
/** @type {KeyboardEvent} */
|
|
35247
|
+
event
|
|
35248
|
+
);
|
|
35249
|
+
});
|
|
35250
|
+
if (this._cardRoving) {
|
|
35251
|
+
this.listen(content, "focusin", (event) => this._cardFocusIn(
|
|
35252
|
+
/** @type {FocusEvent} */
|
|
35253
|
+
event
|
|
35254
|
+
));
|
|
35255
|
+
}
|
|
35227
35256
|
this.listen(content, "error", (event) => this._cardPreviewError(event), true);
|
|
35228
35257
|
this._applyCardLayout();
|
|
35229
35258
|
this._renderCardSort();
|
|
@@ -35242,7 +35271,8 @@ var CardView = class extends RecordView {
|
|
|
35242
35271
|
groupOrder: [...this._cardGroupOrder],
|
|
35243
35272
|
minCardWidth: this._cardMinWidth,
|
|
35244
35273
|
maxColumns: this._cardMaxColumns,
|
|
35245
|
-
variant: this._cardVariant
|
|
35274
|
+
variant: this._cardVariant,
|
|
35275
|
+
collapsedGroups: [...this._cardCollapsed]
|
|
35246
35276
|
};
|
|
35247
35277
|
}
|
|
35248
35278
|
/**
|
|
@@ -35265,9 +35295,36 @@ var CardView = class extends RecordView {
|
|
|
35265
35295
|
if (Object.prototype.hasOwnProperty.call(source, "variant")) {
|
|
35266
35296
|
this._cardVariant = normalizeCardVariant(source.variant);
|
|
35267
35297
|
}
|
|
35298
|
+
if (Object.prototype.hasOwnProperty.call(source, "collapsedGroups")) {
|
|
35299
|
+
this._cardCollapsed = new Set(normalizeGroupOrder(source.collapsedGroups));
|
|
35300
|
+
}
|
|
35268
35301
|
this._applyCardLayout();
|
|
35269
35302
|
return super.setViewState(source, options);
|
|
35270
35303
|
}
|
|
35304
|
+
/**
|
|
35305
|
+
* Collapses or expands one group. A collapsed group keeps its heading and hides its cards.
|
|
35306
|
+
* @param {unknown} id Group id, as `groupBy` produces it.
|
|
35307
|
+
* @param {boolean} [collapsed=true] Next state.
|
|
35308
|
+
* @param {{silent?:boolean}} [options={}] Set `silent` to suppress `groupcollapse` and `statechange`.
|
|
35309
|
+
* @returns {this}
|
|
35310
|
+
* @fires CardView#groupcollapse
|
|
35311
|
+
*/
|
|
35312
|
+
setGroupCollapsed(id, collapsed = true, options = {}) {
|
|
35313
|
+
const groupId = normalizeGroupId(id);
|
|
35314
|
+
if (this._cardCollapsed.has(groupId) === Boolean(collapsed)) return this;
|
|
35315
|
+
if (collapsed) this._cardCollapsed.add(groupId);
|
|
35316
|
+
else this._cardCollapsed.delete(groupId);
|
|
35317
|
+
this._renderCards();
|
|
35318
|
+
if (!options.silent) {
|
|
35319
|
+
this.emit("groupcollapse", { id: groupId, collapsed: Boolean(collapsed), collapsedGroups: [...this._cardCollapsed] });
|
|
35320
|
+
this.emit("statechange", { state: this.getViewState(), reason: "groupcollapse" });
|
|
35321
|
+
}
|
|
35322
|
+
return this;
|
|
35323
|
+
}
|
|
35324
|
+
/** Returns the collapsed group ids. @returns {string[]} */
|
|
35325
|
+
getCollapsedGroups() {
|
|
35326
|
+
return [...this._cardCollapsed];
|
|
35327
|
+
}
|
|
35271
35328
|
/** Restores an enhanced target exactly, or removes an owned root. @returns {void} */
|
|
35272
35329
|
destroy() {
|
|
35273
35330
|
if (this._cardCleaned) return;
|
|
@@ -35337,22 +35394,30 @@ var CardView = class extends RecordView {
|
|
|
35337
35394
|
/** @type {HTMLElement} */
|
|
35338
35395
|
this.refs.content
|
|
35339
35396
|
);
|
|
35397
|
+
const documentRef = content.ownerDocument;
|
|
35398
|
+
const activeElement = documentRef?.activeElement ?? null;
|
|
35399
|
+
const hadFocus = Boolean(activeElement && activeElement !== documentRef.body && content.contains(activeElement));
|
|
35340
35400
|
const active = (
|
|
35341
35401
|
/** @type {Element|null} */
|
|
35342
35402
|
content.querySelector(".zx-record-card:focus")
|
|
35343
35403
|
);
|
|
35344
|
-
const
|
|
35345
|
-
/** @type {HTMLElement} */
|
|
35346
|
-
active.dataset.recordIndex
|
|
35347
|
-
) : -1;
|
|
35348
|
-
const activeRecord = activeIndex >= 0 ? this._viewData[activeIndex] : null;
|
|
35349
|
-
const activeId = activeRecord ? this._viewRecordId(activeRecord) : null;
|
|
35404
|
+
const activeId = active && this._cardIdOfNode.has(active) ? this._cardIdOfNode.get(active) : null;
|
|
35350
35405
|
this._cardNodesById = /* @__PURE__ */ new Map();
|
|
35406
|
+
this._cardHeadersById = /* @__PURE__ */ new Map();
|
|
35407
|
+
this._cardGroupOfId = /* @__PURE__ */ new Map();
|
|
35408
|
+
this._cardSectionIds = /* @__PURE__ */ new Map();
|
|
35409
|
+
this._cardTabStop = null;
|
|
35410
|
+
if (this._cardRoving) {
|
|
35411
|
+
content.removeAttribute("role");
|
|
35412
|
+
content.removeAttribute("aria-label");
|
|
35413
|
+
content.removeAttribute("aria-multiselectable");
|
|
35414
|
+
}
|
|
35351
35415
|
if (
|
|
35352
35416
|
/** @type {HTMLElement} */
|
|
35353
35417
|
this.el.dataset.loading === "true"
|
|
35354
35418
|
) {
|
|
35355
35419
|
content.replaceChildren(this._createCardSkeletons());
|
|
35420
|
+
this._syncCardEmptyFocus(hadFocus);
|
|
35356
35421
|
return;
|
|
35357
35422
|
}
|
|
35358
35423
|
const groups = groupCardRecords(
|
|
@@ -35364,12 +35429,25 @@ var CardView = class extends RecordView {
|
|
|
35364
35429
|
);
|
|
35365
35430
|
if (this._viewData.length === 0 && groups.length === 0) {
|
|
35366
35431
|
content.replaceChildren(this._createCardEmpty());
|
|
35432
|
+
this._syncCardEmptyFocus(hadFocus);
|
|
35367
35433
|
return;
|
|
35368
35434
|
}
|
|
35369
35435
|
const fields = this.getFields();
|
|
35370
35436
|
const visibleFields = this.getVisibleFields();
|
|
35371
35437
|
const sections = groups.map((group, groupIndex) => this._createCardGroup(group, fields, visibleFields, groupIndex));
|
|
35372
35438
|
content.replaceChildren(...sections);
|
|
35439
|
+
if (this._cardRoving) {
|
|
35440
|
+
content.setAttribute("role", "grid");
|
|
35441
|
+
content.setAttribute(
|
|
35442
|
+
"aria-label",
|
|
35443
|
+
/** @type {HTMLElement} */
|
|
35444
|
+
this.el.getAttribute("aria-label") ?? ""
|
|
35445
|
+
);
|
|
35446
|
+
if (this.options.selectable === "multi") content.setAttribute("aria-multiselectable", "true");
|
|
35447
|
+
this._syncCardEmptyFocus(false);
|
|
35448
|
+
this._syncCardRoving(hadFocus);
|
|
35449
|
+
return;
|
|
35450
|
+
}
|
|
35373
35451
|
if (activeId !== null) {
|
|
35374
35452
|
const next = this._cardNodesById.get(activeId);
|
|
35375
35453
|
if (next) queueMicrotask(() => next.isConnected && next.focus());
|
|
@@ -35378,14 +35456,25 @@ var CardView = class extends RecordView {
|
|
|
35378
35456
|
/** @private @param {CardRecordGroup} group @param {ViewField[]} fields @param {ViewField[]} visibleFields @param {number} groupIndex @returns {HTMLElement} */
|
|
35379
35457
|
_createCardGroup(group, fields, visibleFields, groupIndex) {
|
|
35380
35458
|
const options = this._cardOptions();
|
|
35381
|
-
const
|
|
35459
|
+
const roving = this._cardRoving;
|
|
35460
|
+
const headingId = group.id === null ? null : `${this._cardIdPrefix}-group-${safeDomPart(group.id)}-${groupIndex}`;
|
|
35461
|
+
const collapsible = group.id !== null && Boolean(options.collapsibleGroups);
|
|
35462
|
+
const collapsed = collapsible && this._cardCollapsed.has(
|
|
35463
|
+
/** @type {string} */
|
|
35464
|
+
group.id
|
|
35465
|
+
);
|
|
35382
35466
|
const list = h("ul", {
|
|
35383
35467
|
class: "zx-card-view__list",
|
|
35384
|
-
|
|
35468
|
+
id: headingId ? `${headingId}-list` : void 0,
|
|
35469
|
+
role: roving ? group.id === null ? "rowgroup" : "none" : void 0,
|
|
35470
|
+
ariaLabelledby: headingId ?? void 0,
|
|
35471
|
+
hidden: collapsed
|
|
35385
35472
|
});
|
|
35386
35473
|
for (const record of group.records) {
|
|
35387
|
-
const index = this._viewData.indexOf(record);
|
|
35388
35474
|
const id = this._viewRecordId(record);
|
|
35475
|
+
if (group.id !== null) this._cardGroupOfId.set(id, group.id);
|
|
35476
|
+
if (collapsed) continue;
|
|
35477
|
+
const index = this._viewData.indexOf(record);
|
|
35389
35478
|
const card = createRecordCard(record, index, {
|
|
35390
35479
|
fields,
|
|
35391
35480
|
visibleFields,
|
|
@@ -35402,19 +35491,287 @@ var CardView = class extends RecordView {
|
|
|
35402
35491
|
headingLevel: this._cardHeadingLevel,
|
|
35403
35492
|
message: (key, ...args) => this.msg(key, ...args)
|
|
35404
35493
|
});
|
|
35494
|
+
if (roving) this._makeCardRow(card, id);
|
|
35405
35495
|
this._cardNodesById.set(id, card);
|
|
35496
|
+
this._cardIdOfNode.set(card, id);
|
|
35406
35497
|
list.append(card);
|
|
35407
35498
|
}
|
|
35408
|
-
if (group.records.length === 0) {
|
|
35409
|
-
|
|
35499
|
+
if (group.records.length === 0 && !collapsed) {
|
|
35500
|
+
const text4 = this.msg("cardView.groupEmpty");
|
|
35501
|
+
list.append(roving ? h("li", { class: "zx-card-view__group-empty", role: "row" }, h("span", { role: "gridcell" }, text4)) : h("li", { class: "zx-card-view__group-empty" }, text4));
|
|
35410
35502
|
}
|
|
35411
35503
|
if (group.id === null) return list;
|
|
35412
|
-
|
|
35413
|
-
|
|
35414
|
-
|
|
35415
|
-
|
|
35416
|
-
|
|
35504
|
+
const header = this._createGroupHeader(
|
|
35505
|
+
group,
|
|
35506
|
+
/** @type {string} */
|
|
35507
|
+
headingId,
|
|
35508
|
+
collapsible,
|
|
35509
|
+
collapsed
|
|
35510
|
+
);
|
|
35511
|
+
const section = h("section", {
|
|
35512
|
+
class: "zx-card-view__group",
|
|
35513
|
+
role: roving ? "rowgroup" : void 0,
|
|
35514
|
+
ariaLabelledby: headingId,
|
|
35515
|
+
dataset: { collapsed: collapsible ? String(collapsed) : null }
|
|
35516
|
+
}, header, list);
|
|
35517
|
+
this._cardSectionIds.set(section, group.id);
|
|
35518
|
+
return section;
|
|
35519
|
+
}
|
|
35520
|
+
/**
|
|
35521
|
+
* Builds a group heading: the label (or `renderGroupHeading` content), inside an APG
|
|
35522
|
+
* disclosure button when the group can collapse.
|
|
35523
|
+
* @private
|
|
35524
|
+
* @param {CardRecordGroup} group Group.
|
|
35525
|
+
* @param {string} headingId Heading id.
|
|
35526
|
+
* @param {boolean} collapsible Whether the group can collapse.
|
|
35527
|
+
* @param {boolean} collapsed Whether it is collapsed.
|
|
35528
|
+
* @returns {HTMLElement}
|
|
35529
|
+
*/
|
|
35530
|
+
_createGroupHeader(group, headingId, collapsible, collapsed) {
|
|
35531
|
+
const id = (
|
|
35532
|
+
/** @type {string} */
|
|
35533
|
+
group.id
|
|
35534
|
+
);
|
|
35535
|
+
const render = this._cardOptions().renderGroupHeading;
|
|
35536
|
+
const custom = typeof render === "function" ? render({
|
|
35537
|
+
id,
|
|
35538
|
+
label: group.label,
|
|
35539
|
+
count: group.records.length,
|
|
35540
|
+
records: [...group.records],
|
|
35541
|
+
collapsed,
|
|
35542
|
+
toggle: () => this.setGroupCollapsed(id, !this._cardCollapsed.has(id))
|
|
35543
|
+
}) : null;
|
|
35544
|
+
const label = custom == null ? group.label : isNode5(custom) ? custom : String(custom);
|
|
35545
|
+
const labelNode = h("span", { class: "zx-card-view__group-label" }, label);
|
|
35546
|
+
const roving = this._cardRoving;
|
|
35547
|
+
const content = collapsible ? h("button", {
|
|
35548
|
+
class: "zx-card-view__group-toggle",
|
|
35549
|
+
type: "button",
|
|
35550
|
+
tabindex: roving ? -1 : void 0,
|
|
35551
|
+
ariaExpanded: String(!collapsed),
|
|
35552
|
+
ariaControls: `${headingId}-list`,
|
|
35553
|
+
dataset: { groupToggle: "" }
|
|
35554
|
+
}, icon("chevron-down", { size: 14 }), labelNode) : labelNode;
|
|
35555
|
+
const heading = h("h2", { class: "zx-card-view__group-heading", id: headingId }, content);
|
|
35556
|
+
const header = h("div", {
|
|
35557
|
+
class: "zx-card-view__group-header",
|
|
35558
|
+
role: roving ? "row" : void 0,
|
|
35559
|
+
ariaExpanded: roving && collapsible ? String(!collapsed) : void 0,
|
|
35560
|
+
ariaLabelledby: roving ? headingId : void 0,
|
|
35561
|
+
tabindex: roving && collapsible ? -1 : void 0,
|
|
35562
|
+
dataset: { cardItem: roving && collapsible ? "group" : null }
|
|
35563
|
+
}, h("div", { class: "zx-card-view__group-cell", role: roving ? "rowheader" : void 0 }, heading));
|
|
35564
|
+
if (roving && collapsible) this._cardHeadersById.set(id, header);
|
|
35565
|
+
return header;
|
|
35566
|
+
}
|
|
35567
|
+
/**
|
|
35568
|
+
* Turns a rendered card into a grid row for the roving model: row focus, `aria-selected` when
|
|
35569
|
+
* selectable, and the controls inside it out of the Tab order.
|
|
35570
|
+
* @private
|
|
35571
|
+
* @param {HTMLElement} card Card element.
|
|
35572
|
+
* @param {unknown} id Record id.
|
|
35573
|
+
* @returns {void}
|
|
35574
|
+
*/
|
|
35575
|
+
_makeCardRow(card, id) {
|
|
35576
|
+
card.setAttribute("role", "row");
|
|
35577
|
+
card.tabIndex = -1;
|
|
35578
|
+
card.dataset.cardItem = "card";
|
|
35579
|
+
card.removeAttribute("aria-description");
|
|
35580
|
+
card.removeAttribute("aria-keyshortcuts");
|
|
35581
|
+
if (this.options.selectable) card.setAttribute("aria-selected", String(this._isViewSelected(id)));
|
|
35582
|
+
for (const child of card.children) child.setAttribute("role", "gridcell");
|
|
35583
|
+
for (const control of card.querySelectorAll(FOCUSABLE3)) {
|
|
35584
|
+
if (control !== card) control.tabIndex = -1;
|
|
35585
|
+
}
|
|
35586
|
+
}
|
|
35587
|
+
/**
|
|
35588
|
+
* Moves the single Tab stop to the current item after a render — the same record or group by
|
|
35589
|
+
* id; else the heading of the collapsed group that now hides the card; else the item now at its
|
|
35590
|
+
* old position — and focuses it again, without scrolling, when the collection had focus.
|
|
35591
|
+
* @private
|
|
35592
|
+
* @param {boolean} hadFocus Whether focus was inside the collection before the render.
|
|
35593
|
+
* @returns {void}
|
|
35594
|
+
*/
|
|
35595
|
+
_syncCardRoving(hadFocus) {
|
|
35596
|
+
const items = this._cardItems();
|
|
35597
|
+
if (items.length === 0) {
|
|
35598
|
+
this._syncCardEmptyFocus(hadFocus);
|
|
35599
|
+
return;
|
|
35600
|
+
}
|
|
35601
|
+
let item = this._cardItemFor(this._cardCurrent);
|
|
35602
|
+
if (!item && this._cardCurrent?.kind === "card") {
|
|
35603
|
+
const groupId = this._cardGroupOfId.get(this._cardCurrent.id);
|
|
35604
|
+
item = groupId === void 0 ? null : this._cardHeadersById.get(groupId) ?? null;
|
|
35605
|
+
}
|
|
35606
|
+
if (!item) item = items[Math.min(Math.max(this._cardCurrentIndex, 0), items.length - 1)];
|
|
35607
|
+
this._setCardTabStop(item, items);
|
|
35608
|
+
if (hadFocus) item.focus({ preventScroll: true });
|
|
35609
|
+
}
|
|
35610
|
+
/**
|
|
35611
|
+
* @private
|
|
35612
|
+
* @param {HTMLElement} item New Tab stop.
|
|
35613
|
+
* @param {HTMLElement[]} [items] Every item, when already collected.
|
|
35614
|
+
* @returns {void}
|
|
35615
|
+
*/
|
|
35616
|
+
_setCardTabStop(item, items = this._cardItems()) {
|
|
35617
|
+
for (const candidate of items) if (candidate !== item) candidate.tabIndex = -1;
|
|
35618
|
+
item.tabIndex = 0;
|
|
35619
|
+
this._cardTabStop = item;
|
|
35620
|
+
const key = this._cardItemKey(item);
|
|
35621
|
+
if (key) this._cardCurrent = key;
|
|
35622
|
+
this._cardCurrentIndex = Math.max(0, items.indexOf(item));
|
|
35623
|
+
}
|
|
35624
|
+
/**
|
|
35625
|
+
* While loading or empty there is no card to hold focus: the content takes it rather than
|
|
35626
|
+
* letting it fall to the document, and gives the Tab order its old shape back afterwards.
|
|
35627
|
+
* @private
|
|
35628
|
+
* @param {boolean} hold Whether focus was inside and there is nothing to put it on.
|
|
35629
|
+
* @returns {void}
|
|
35630
|
+
*/
|
|
35631
|
+
_syncCardEmptyFocus(hold) {
|
|
35632
|
+
if (!this._cardRoving) return;
|
|
35633
|
+
const content = (
|
|
35634
|
+
/** @type {HTMLElement} */
|
|
35635
|
+
this.refs.content
|
|
35636
|
+
);
|
|
35637
|
+
if (hold) {
|
|
35638
|
+
if (!content.hasAttribute("tabindex")) {
|
|
35639
|
+
content.tabIndex = -1;
|
|
35640
|
+
content.setAttribute("data-empty-focus", "");
|
|
35641
|
+
}
|
|
35642
|
+
content.focus({ preventScroll: true });
|
|
35643
|
+
} else if (content.hasAttribute("data-empty-focus")) {
|
|
35644
|
+
content.removeAttribute("tabindex");
|
|
35645
|
+
content.removeAttribute("data-empty-focus");
|
|
35646
|
+
}
|
|
35647
|
+
}
|
|
35648
|
+
/** @private @returns {HTMLElement[]} Roving items in display order. */
|
|
35649
|
+
_cardItems() {
|
|
35650
|
+
return (
|
|
35651
|
+
/** @type {HTMLElement[]} */
|
|
35652
|
+
[...this.refs.content.querySelectorAll("[data-card-item]")]
|
|
35653
|
+
);
|
|
35654
|
+
}
|
|
35655
|
+
/** @private @param {HTMLElement} item @returns {{kind:'card'|'group', id:unknown}|null} */
|
|
35656
|
+
_cardItemKey(item) {
|
|
35657
|
+
if (item.dataset.cardItem === "group") {
|
|
35658
|
+
for (const [id, header] of this._cardHeadersById) if (header === item) return { kind: "group", id };
|
|
35659
|
+
return null;
|
|
35660
|
+
}
|
|
35661
|
+
return this._cardIdOfNode.has(item) ? { kind: "card", id: this._cardIdOfNode.get(item) } : null;
|
|
35662
|
+
}
|
|
35663
|
+
/** @private @param {{kind:'card'|'group', id:unknown}|null} key @returns {HTMLElement|null} */
|
|
35664
|
+
_cardItemFor(key) {
|
|
35665
|
+
if (!key) return null;
|
|
35666
|
+
if (key.kind === "group") return this._cardHeadersById.get(
|
|
35667
|
+
/** @type {string} */
|
|
35668
|
+
key.id
|
|
35669
|
+
) ?? null;
|
|
35670
|
+
return (
|
|
35671
|
+
/** @type {HTMLElement|undefined} */
|
|
35672
|
+
this._cardNodesById.get(key.id) ?? null
|
|
35673
|
+
);
|
|
35674
|
+
}
|
|
35675
|
+
/** @private @param {FocusEvent} event @returns {void} */
|
|
35676
|
+
_cardFocusIn(event) {
|
|
35677
|
+
const target = (
|
|
35678
|
+
/** @type {Element|null} */
|
|
35679
|
+
event.target
|
|
35680
|
+
);
|
|
35681
|
+
const item = (
|
|
35682
|
+
/** @type {HTMLElement|null} */
|
|
35683
|
+
target?.closest?.("[data-card-item]") ?? null
|
|
35417
35684
|
);
|
|
35685
|
+
if (!item || !this.refs.content.contains(item)) return;
|
|
35686
|
+
if (this._cardTabStop !== item) this._setCardTabStop(item);
|
|
35687
|
+
if (!this._cardMoving) this._cardStickyX = item.getBoundingClientRect().left;
|
|
35688
|
+
}
|
|
35689
|
+
/**
|
|
35690
|
+
* The roving keyboard model; see the class description.
|
|
35691
|
+
* @private
|
|
35692
|
+
* @param {KeyboardEvent} event Key event.
|
|
35693
|
+
* @returns {void}
|
|
35694
|
+
*/
|
|
35695
|
+
_cardRovingKeydown(event) {
|
|
35696
|
+
const target = (
|
|
35697
|
+
/** @type {Element|null} */
|
|
35698
|
+
event.target
|
|
35699
|
+
);
|
|
35700
|
+
const item = (
|
|
35701
|
+
/** @type {HTMLElement|null} */
|
|
35702
|
+
target?.closest?.("[data-card-item]") ?? null
|
|
35703
|
+
);
|
|
35704
|
+
if (!item || !this.refs.content.contains(item)) return;
|
|
35705
|
+
const onItem = target === item;
|
|
35706
|
+
if (!onItem && target?.matches?.(TEXT_ENTRY)) return;
|
|
35707
|
+
const items = this._cardItems();
|
|
35708
|
+
const index = items.indexOf(item);
|
|
35709
|
+
const kind = item.dataset.cardItem;
|
|
35710
|
+
const key = event.key;
|
|
35711
|
+
const multi = this.options.selectable === "multi";
|
|
35712
|
+
if ((event.ctrlKey || event.metaKey) && (key === "a" || key === "A")) {
|
|
35713
|
+
if (!multi) return;
|
|
35714
|
+
event.preventDefault();
|
|
35715
|
+
this.setSelection(this._cardIdsOf(items));
|
|
35716
|
+
return;
|
|
35717
|
+
}
|
|
35718
|
+
if (onItem && (key === "Enter" || key === " ")) {
|
|
35719
|
+
event.preventDefault();
|
|
35720
|
+
if (kind === "group") {
|
|
35721
|
+
const groupId = (
|
|
35722
|
+
/** @type {string} */
|
|
35723
|
+
this._cardItemKey(item)?.id
|
|
35724
|
+
);
|
|
35725
|
+
this.setGroupCollapsed(groupId, !this._cardCollapsed.has(groupId));
|
|
35726
|
+
return;
|
|
35727
|
+
}
|
|
35728
|
+
const record = this._viewData[Number(item.dataset.recordIndex)];
|
|
35729
|
+
if (!record) return;
|
|
35730
|
+
const id = this._viewRecordId(record);
|
|
35731
|
+
if (key === " " && this.options.selectable) {
|
|
35732
|
+
this.toggleSelection(id);
|
|
35733
|
+
return;
|
|
35734
|
+
}
|
|
35735
|
+
this._activateCard(record, id, Number(item.dataset.recordIndex), event);
|
|
35736
|
+
if (this.options.selectable === "single") this.toggleSelection(id, { selected: true });
|
|
35737
|
+
return;
|
|
35738
|
+
}
|
|
35739
|
+
if (kind === "group" && onItem && (key === "ArrowLeft" || key === "ArrowRight")) {
|
|
35740
|
+
event.preventDefault();
|
|
35741
|
+
const groupId = (
|
|
35742
|
+
/** @type {string} */
|
|
35743
|
+
this._cardItemKey(item)?.id
|
|
35744
|
+
);
|
|
35745
|
+
this.setGroupCollapsed(groupId, key === "ArrowLeft");
|
|
35746
|
+
return;
|
|
35747
|
+
}
|
|
35748
|
+
if (!NAVIGATION_KEYS.has(key)) return;
|
|
35749
|
+
const rects = items.map((candidate) => candidate.getBoundingClientRect());
|
|
35750
|
+
const vertical = key === "ArrowUp" || key === "ArrowDown" || key === "PageUp" || key === "PageDown";
|
|
35751
|
+
const x = vertical ? this._cardStickyX ?? rects[index].left : rects[index].left;
|
|
35752
|
+
const page = cardPageSize(viewportHeight(this.refs.content), rects[index].height);
|
|
35753
|
+
const next = cardGridTarget(rects, index, key, { page, x });
|
|
35754
|
+
if (next === null) return;
|
|
35755
|
+
event.preventDefault();
|
|
35756
|
+
if (next === index) return;
|
|
35757
|
+
const destination = items[next];
|
|
35758
|
+
if (!vertical) this._cardStickyX = rects[next].left;
|
|
35759
|
+
this._cardMoving = true;
|
|
35760
|
+
try {
|
|
35761
|
+
this._setCardTabStop(destination, items);
|
|
35762
|
+
destination.focus({ preventScroll: true });
|
|
35763
|
+
destination.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
35764
|
+
} finally {
|
|
35765
|
+
this._cardMoving = false;
|
|
35766
|
+
}
|
|
35767
|
+
if (event.shiftKey && multi) {
|
|
35768
|
+
const span = items.slice(Math.min(index, next), Math.max(index, next) + 1);
|
|
35769
|
+
this.setSelection([...this.getSelectionIds(), ...this._cardIdsOf(span)]);
|
|
35770
|
+
}
|
|
35771
|
+
}
|
|
35772
|
+
/** @private @param {HTMLElement[]} items @returns {unknown[]} Record ids of the card items. */
|
|
35773
|
+
_cardIdsOf(items) {
|
|
35774
|
+
return items.filter((item) => item.dataset.cardItem === "card").map((item) => this._viewData[Number(item.dataset.recordIndex)]).filter(Boolean).map((record) => this._viewRecordId(record));
|
|
35418
35775
|
}
|
|
35419
35776
|
/** @private @returns {HTMLElement} */
|
|
35420
35777
|
_createCardSkeletons() {
|
|
@@ -35445,10 +35802,14 @@ var CardView = class extends RecordView {
|
|
|
35445
35802
|
for (const [id, card] of this._cardNodesById) {
|
|
35446
35803
|
const selected = this._isViewSelected(id);
|
|
35447
35804
|
card.dataset.selected = String(selected);
|
|
35448
|
-
if (this.
|
|
35449
|
-
"aria-
|
|
35450
|
-
|
|
35451
|
-
|
|
35805
|
+
if (this._cardRoving) {
|
|
35806
|
+
if (this.options.selectable) card.setAttribute("aria-selected", String(selected));
|
|
35807
|
+
} else if (this.options.selectable) {
|
|
35808
|
+
card.setAttribute(
|
|
35809
|
+
"aria-description",
|
|
35810
|
+
this.msg(selected ? "recordCard.selected" : "recordCard.notSelected")
|
|
35811
|
+
);
|
|
35812
|
+
}
|
|
35452
35813
|
const checkbox = (
|
|
35453
35814
|
/** @type {HTMLInputElement|null} */
|
|
35454
35815
|
card.querySelector("[data-record-selection]")
|
|
@@ -35462,6 +35823,16 @@ var CardView = class extends RecordView {
|
|
|
35462
35823
|
/** @type {Element|null} */
|
|
35463
35824
|
event.target
|
|
35464
35825
|
);
|
|
35826
|
+
const toggle = (
|
|
35827
|
+
/** @type {HTMLElement|null} */
|
|
35828
|
+
target?.closest?.("[data-group-toggle]")
|
|
35829
|
+
);
|
|
35830
|
+
if (toggle && this.refs.content.contains(toggle)) {
|
|
35831
|
+
const section = toggle.closest(".zx-card-view__group");
|
|
35832
|
+
const id2 = section ? this._cardSectionIds.get(section) : void 0;
|
|
35833
|
+
if (id2 !== void 0) this.setGroupCollapsed(id2, !this._cardCollapsed.has(id2));
|
|
35834
|
+
return;
|
|
35835
|
+
}
|
|
35465
35836
|
const card = (
|
|
35466
35837
|
/** @type {HTMLElement|null} */
|
|
35467
35838
|
target?.closest?.(".zx-record-card")
|
|
@@ -35569,6 +35940,58 @@ var CardView = class extends RecordView {
|
|
|
35569
35940
|
);
|
|
35570
35941
|
}
|
|
35571
35942
|
};
|
|
35943
|
+
var FOCUSABLE3 = 'a[href],button,input,select,textarea,summary,[tabindex]:not([tabindex="-1"]),[contenteditable="true"]';
|
|
35944
|
+
var TEXT_ENTRY = 'input:not([type="checkbox"]):not([type="radio"]),select,textarea,[contenteditable="true"]';
|
|
35945
|
+
var NAVIGATION_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
|
|
35946
|
+
function cardGridRows(rects, tolerance = 4) {
|
|
35947
|
+
const order = rects.map((rect, index) => ({ index, top: Number(rect.top) || 0, left: Number(rect.left) || 0 })).sort((a, b) => a.top - b.top || a.left - b.left);
|
|
35948
|
+
const rows = [];
|
|
35949
|
+
for (const entry of order) {
|
|
35950
|
+
const row2 = rows.at(-1);
|
|
35951
|
+
if (row2 && Math.abs(entry.top - row2.top) <= tolerance) row2.items.push(entry);
|
|
35952
|
+
else rows.push({ top: entry.top, items: [entry] });
|
|
35953
|
+
}
|
|
35954
|
+
return rows.map((row2) => row2.items.sort((a, b) => a.left - b.left).map((entry) => entry.index));
|
|
35955
|
+
}
|
|
35956
|
+
function cardGridTarget(rects, index, key, options = {}) {
|
|
35957
|
+
if (rects.length === 0) return null;
|
|
35958
|
+
const current = Math.min(Math.max(Math.trunc(Number(index) || 0), 0), rects.length - 1);
|
|
35959
|
+
if (key === "Home") return 0;
|
|
35960
|
+
if (key === "End") return rects.length - 1;
|
|
35961
|
+
const rows = cardGridRows(rects);
|
|
35962
|
+
const rowIndex = rows.findIndex((row3) => row3.includes(current));
|
|
35963
|
+
const row2 = rows[rowIndex];
|
|
35964
|
+
if (key === "ArrowLeft" || key === "ArrowRight") {
|
|
35965
|
+
const position2 = row2.indexOf(current) + (key === "ArrowLeft" ? -1 : 1);
|
|
35966
|
+
return position2 < 0 || position2 >= row2.length ? current : row2[position2];
|
|
35967
|
+
}
|
|
35968
|
+
const page = Math.max(1, Math.trunc(Number(options.page) || 10));
|
|
35969
|
+
const step = { ArrowUp: -1, ArrowDown: 1, PageUp: -page, PageDown: page }[key];
|
|
35970
|
+
if (step === void 0) return null;
|
|
35971
|
+
const targetRow = rows[Math.min(Math.max(rowIndex + step, 0), rows.length - 1)];
|
|
35972
|
+
if (targetRow === row2) return current;
|
|
35973
|
+
const x = Number.isFinite(Number(options.x)) ? Number(options.x) : Number(rects[current].left) || 0;
|
|
35974
|
+
let best = targetRow[0];
|
|
35975
|
+
for (const candidate of targetRow) {
|
|
35976
|
+
if (Math.abs((Number(rects[candidate].left) || 0) - x) < Math.abs((Number(rects[best].left) || 0) - x)) best = candidate;
|
|
35977
|
+
}
|
|
35978
|
+
return best;
|
|
35979
|
+
}
|
|
35980
|
+
function cardPageSize(viewport, rowHeight, fallback = 10) {
|
|
35981
|
+
const visible = Number(viewport);
|
|
35982
|
+
const height = Number(rowHeight);
|
|
35983
|
+
if (!(visible > 0) || !(height > 0)) return Math.max(1, Math.trunc(fallback) || 10);
|
|
35984
|
+
return Math.max(1, Math.floor(visible / height) - 1);
|
|
35985
|
+
}
|
|
35986
|
+
function viewportHeight(element) {
|
|
35987
|
+
let node = element.parentElement;
|
|
35988
|
+
while (node && node !== element.ownerDocument.body) {
|
|
35989
|
+
const overflow = getComputedStyle(node).overflowY;
|
|
35990
|
+
if (/(auto|scroll|overlay)/.test(overflow) && node.scrollHeight > node.clientHeight) return node.clientHeight;
|
|
35991
|
+
node = node.parentElement;
|
|
35992
|
+
}
|
|
35993
|
+
return element.ownerDocument.defaultView?.innerHeight ?? 0;
|
|
35994
|
+
}
|
|
35572
35995
|
function groupCardRecords(records, groupBy2, groupOrder = [], fields = [], ungroupedLabel = "Ungrouped") {
|
|
35573
35996
|
if (!Array.isArray(records)) throw new TypeError("Card view records must be an array");
|
|
35574
35997
|
if (groupBy2 == null) return records.length ? [{ id: null, label: "", records: [...records] }] : [];
|