@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.global.js
CHANGED
|
@@ -4434,7 +4434,7 @@ var zx = (() => {
|
|
|
4434
4434
|
if (matchWidth) floating.style.minWidth = `${anchorRect.width}px`;
|
|
4435
4435
|
const floatingRect = floating.getBoundingClientRect();
|
|
4436
4436
|
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
4437
|
-
const
|
|
4437
|
+
const viewportHeight2 = document.documentElement.clientHeight || window.innerHeight;
|
|
4438
4438
|
let actualPlacement = placement;
|
|
4439
4439
|
const side = placementSide(placement);
|
|
4440
4440
|
const bottomY = anchorRect.bottom + offset;
|
|
@@ -4442,9 +4442,9 @@ var zx = (() => {
|
|
|
4442
4442
|
const rightX = anchorRect.right + offset;
|
|
4443
4443
|
const leftX = anchorRect.left - offset - floatingRect.width;
|
|
4444
4444
|
if (flip) {
|
|
4445
|
-
if (side === "bottom" && bottomY + floatingRect.height >
|
|
4445
|
+
if (side === "bottom" && bottomY + floatingRect.height > viewportHeight2 && topY >= 0) {
|
|
4446
4446
|
actualPlacement = flipPlacement(placement, "bottom", "top");
|
|
4447
|
-
} else if (side === "top" && topY < 0 && bottomY + floatingRect.height <=
|
|
4447
|
+
} else if (side === "top" && topY < 0 && bottomY + floatingRect.height <= viewportHeight2) {
|
|
4448
4448
|
actualPlacement = flipPlacement(placement, "top", "bottom");
|
|
4449
4449
|
} else if (side === "right" && rightX + floatingRect.width > viewportWidth && leftX >= 0) {
|
|
4450
4450
|
actualPlacement = flipPlacement(placement, "right", "left");
|
|
@@ -4470,7 +4470,7 @@ var zx = (() => {
|
|
|
4470
4470
|
else top = anchorRect.top + (anchorRect.height - floatingRect.height) / 2;
|
|
4471
4471
|
}
|
|
4472
4472
|
floating.style.left = `${Math.round(clamp(left, 0, Math.max(0, viewportWidth - floatingRect.width)))}px`;
|
|
4473
|
-
floating.style.top = `${Math.round(clamp(top, 0, Math.max(0,
|
|
4473
|
+
floating.style.top = `${Math.round(clamp(top, 0, Math.max(0, viewportHeight2 - floatingRect.height)))}px`;
|
|
4474
4474
|
}
|
|
4475
4475
|
function placementSide(placement) {
|
|
4476
4476
|
return (
|
|
@@ -35471,7 +35471,11 @@ var zx = (() => {
|
|
|
35471
35471
|
variant: "outlined",
|
|
35472
35472
|
loadingCount: 6,
|
|
35473
35473
|
headingLevel: 3,
|
|
35474
|
-
label: null
|
|
35474
|
+
label: null,
|
|
35475
|
+
keyboard: "tabs",
|
|
35476
|
+
collapsibleGroups: false,
|
|
35477
|
+
collapsedGroups: [],
|
|
35478
|
+
renderGroupHeading: null
|
|
35475
35479
|
};
|
|
35476
35480
|
/**
|
|
35477
35481
|
* Creates or enhances a card view.
|
|
@@ -35500,6 +35504,19 @@ var zx = (() => {
|
|
|
35500
35504
|
this._cardVariant = normalizeCardVariant(options.variant);
|
|
35501
35505
|
this._cardLoadingCount = normalizeLoadingCount3(options.loadingCount);
|
|
35502
35506
|
this._cardHeadingLevel = normalizeHeadingLevel5(options.headingLevel);
|
|
35507
|
+
this._cardRoving = options.keyboard === "roving";
|
|
35508
|
+
this._cardIdPrefix = uid("zx-card-view");
|
|
35509
|
+
this._cardCollapsed = new Set(normalizeGroupOrder(options.collapsedGroups));
|
|
35510
|
+
this._cardCurrent = null;
|
|
35511
|
+
this._cardCurrentIndex = 0;
|
|
35512
|
+
this._cardTabStop = null;
|
|
35513
|
+
this._cardStickyX = null;
|
|
35514
|
+
this._cardMoving = false;
|
|
35515
|
+
this._cardHeadersById = /* @__PURE__ */ new Map();
|
|
35516
|
+
this._cardGroupOfId = /* @__PURE__ */ new Map();
|
|
35517
|
+
this._cardSectionIds = /* @__PURE__ */ new Map();
|
|
35518
|
+
this._cardIdOfNode = /* @__PURE__ */ new WeakMap();
|
|
35519
|
+
if (this._cardRoving) root.dataset.keyboard = "roving";
|
|
35503
35520
|
try {
|
|
35504
35521
|
this._initRecordView(root);
|
|
35505
35522
|
root.setAttribute("aria-label", String(options.label || this.msg("cardView.label")));
|
|
@@ -35524,10 +35541,22 @@ var zx = (() => {
|
|
|
35524
35541
|
/** @type {MouseEvent} */
|
|
35525
35542
|
event
|
|
35526
35543
|
));
|
|
35527
|
-
this.listen(content, "keydown", (event) =>
|
|
35528
|
-
|
|
35529
|
-
|
|
35530
|
-
|
|
35544
|
+
this.listen(content, "keydown", (event) => {
|
|
35545
|
+
if (this._cardRoving) this._cardRovingKeydown(
|
|
35546
|
+
/** @type {KeyboardEvent} */
|
|
35547
|
+
event
|
|
35548
|
+
);
|
|
35549
|
+
else this._cardKeydown(
|
|
35550
|
+
/** @type {KeyboardEvent} */
|
|
35551
|
+
event
|
|
35552
|
+
);
|
|
35553
|
+
});
|
|
35554
|
+
if (this._cardRoving) {
|
|
35555
|
+
this.listen(content, "focusin", (event) => this._cardFocusIn(
|
|
35556
|
+
/** @type {FocusEvent} */
|
|
35557
|
+
event
|
|
35558
|
+
));
|
|
35559
|
+
}
|
|
35531
35560
|
this.listen(content, "error", (event) => this._cardPreviewError(event), true);
|
|
35532
35561
|
this._applyCardLayout();
|
|
35533
35562
|
this._renderCardSort();
|
|
@@ -35546,7 +35575,8 @@ var zx = (() => {
|
|
|
35546
35575
|
groupOrder: [...this._cardGroupOrder],
|
|
35547
35576
|
minCardWidth: this._cardMinWidth,
|
|
35548
35577
|
maxColumns: this._cardMaxColumns,
|
|
35549
|
-
variant: this._cardVariant
|
|
35578
|
+
variant: this._cardVariant,
|
|
35579
|
+
collapsedGroups: [...this._cardCollapsed]
|
|
35550
35580
|
};
|
|
35551
35581
|
}
|
|
35552
35582
|
/**
|
|
@@ -35569,9 +35599,36 @@ var zx = (() => {
|
|
|
35569
35599
|
if (Object.prototype.hasOwnProperty.call(source, "variant")) {
|
|
35570
35600
|
this._cardVariant = normalizeCardVariant(source.variant);
|
|
35571
35601
|
}
|
|
35602
|
+
if (Object.prototype.hasOwnProperty.call(source, "collapsedGroups")) {
|
|
35603
|
+
this._cardCollapsed = new Set(normalizeGroupOrder(source.collapsedGroups));
|
|
35604
|
+
}
|
|
35572
35605
|
this._applyCardLayout();
|
|
35573
35606
|
return super.setViewState(source, options);
|
|
35574
35607
|
}
|
|
35608
|
+
/**
|
|
35609
|
+
* Collapses or expands one group. A collapsed group keeps its heading and hides its cards.
|
|
35610
|
+
* @param {unknown} id Group id, as `groupBy` produces it.
|
|
35611
|
+
* @param {boolean} [collapsed=true] Next state.
|
|
35612
|
+
* @param {{silent?:boolean}} [options={}] Set `silent` to suppress `groupcollapse` and `statechange`.
|
|
35613
|
+
* @returns {this}
|
|
35614
|
+
* @fires CardView#groupcollapse
|
|
35615
|
+
*/
|
|
35616
|
+
setGroupCollapsed(id, collapsed = true, options = {}) {
|
|
35617
|
+
const groupId = normalizeGroupId(id);
|
|
35618
|
+
if (this._cardCollapsed.has(groupId) === Boolean(collapsed)) return this;
|
|
35619
|
+
if (collapsed) this._cardCollapsed.add(groupId);
|
|
35620
|
+
else this._cardCollapsed.delete(groupId);
|
|
35621
|
+
this._renderCards();
|
|
35622
|
+
if (!options.silent) {
|
|
35623
|
+
this.emit("groupcollapse", { id: groupId, collapsed: Boolean(collapsed), collapsedGroups: [...this._cardCollapsed] });
|
|
35624
|
+
this.emit("statechange", { state: this.getViewState(), reason: "groupcollapse" });
|
|
35625
|
+
}
|
|
35626
|
+
return this;
|
|
35627
|
+
}
|
|
35628
|
+
/** Returns the collapsed group ids. @returns {string[]} */
|
|
35629
|
+
getCollapsedGroups() {
|
|
35630
|
+
return [...this._cardCollapsed];
|
|
35631
|
+
}
|
|
35575
35632
|
/** Restores an enhanced target exactly, or removes an owned root. @returns {void} */
|
|
35576
35633
|
destroy() {
|
|
35577
35634
|
if (this._cardCleaned) return;
|
|
@@ -35641,22 +35698,30 @@ var zx = (() => {
|
|
|
35641
35698
|
/** @type {HTMLElement} */
|
|
35642
35699
|
this.refs.content
|
|
35643
35700
|
);
|
|
35701
|
+
const documentRef = content.ownerDocument;
|
|
35702
|
+
const activeElement = documentRef?.activeElement ?? null;
|
|
35703
|
+
const hadFocus = Boolean(activeElement && activeElement !== documentRef.body && content.contains(activeElement));
|
|
35644
35704
|
const active = (
|
|
35645
35705
|
/** @type {Element|null} */
|
|
35646
35706
|
content.querySelector(".zx-record-card:focus")
|
|
35647
35707
|
);
|
|
35648
|
-
const
|
|
35649
|
-
/** @type {HTMLElement} */
|
|
35650
|
-
active.dataset.recordIndex
|
|
35651
|
-
) : -1;
|
|
35652
|
-
const activeRecord = activeIndex >= 0 ? this._viewData[activeIndex] : null;
|
|
35653
|
-
const activeId = activeRecord ? this._viewRecordId(activeRecord) : null;
|
|
35708
|
+
const activeId = active && this._cardIdOfNode.has(active) ? this._cardIdOfNode.get(active) : null;
|
|
35654
35709
|
this._cardNodesById = /* @__PURE__ */ new Map();
|
|
35710
|
+
this._cardHeadersById = /* @__PURE__ */ new Map();
|
|
35711
|
+
this._cardGroupOfId = /* @__PURE__ */ new Map();
|
|
35712
|
+
this._cardSectionIds = /* @__PURE__ */ new Map();
|
|
35713
|
+
this._cardTabStop = null;
|
|
35714
|
+
if (this._cardRoving) {
|
|
35715
|
+
content.removeAttribute("role");
|
|
35716
|
+
content.removeAttribute("aria-label");
|
|
35717
|
+
content.removeAttribute("aria-multiselectable");
|
|
35718
|
+
}
|
|
35655
35719
|
if (
|
|
35656
35720
|
/** @type {HTMLElement} */
|
|
35657
35721
|
this.el.dataset.loading === "true"
|
|
35658
35722
|
) {
|
|
35659
35723
|
content.replaceChildren(this._createCardSkeletons());
|
|
35724
|
+
this._syncCardEmptyFocus(hadFocus);
|
|
35660
35725
|
return;
|
|
35661
35726
|
}
|
|
35662
35727
|
const groups = groupCardRecords(
|
|
@@ -35668,12 +35733,25 @@ var zx = (() => {
|
|
|
35668
35733
|
);
|
|
35669
35734
|
if (this._viewData.length === 0 && groups.length === 0) {
|
|
35670
35735
|
content.replaceChildren(this._createCardEmpty());
|
|
35736
|
+
this._syncCardEmptyFocus(hadFocus);
|
|
35671
35737
|
return;
|
|
35672
35738
|
}
|
|
35673
35739
|
const fields = this.getFields();
|
|
35674
35740
|
const visibleFields = this.getVisibleFields();
|
|
35675
35741
|
const sections = groups.map((group, groupIndex) => this._createCardGroup(group, fields, visibleFields, groupIndex));
|
|
35676
35742
|
content.replaceChildren(...sections);
|
|
35743
|
+
if (this._cardRoving) {
|
|
35744
|
+
content.setAttribute("role", "grid");
|
|
35745
|
+
content.setAttribute(
|
|
35746
|
+
"aria-label",
|
|
35747
|
+
/** @type {HTMLElement} */
|
|
35748
|
+
this.el.getAttribute("aria-label") ?? ""
|
|
35749
|
+
);
|
|
35750
|
+
if (this.options.selectable === "multi") content.setAttribute("aria-multiselectable", "true");
|
|
35751
|
+
this._syncCardEmptyFocus(false);
|
|
35752
|
+
this._syncCardRoving(hadFocus);
|
|
35753
|
+
return;
|
|
35754
|
+
}
|
|
35677
35755
|
if (activeId !== null) {
|
|
35678
35756
|
const next = this._cardNodesById.get(activeId);
|
|
35679
35757
|
if (next) queueMicrotask(() => next.isConnected && next.focus());
|
|
@@ -35682,14 +35760,25 @@ var zx = (() => {
|
|
|
35682
35760
|
/** @private @param {CardRecordGroup} group @param {ViewField[]} fields @param {ViewField[]} visibleFields @param {number} groupIndex @returns {HTMLElement} */
|
|
35683
35761
|
_createCardGroup(group, fields, visibleFields, groupIndex) {
|
|
35684
35762
|
const options = this._cardOptions();
|
|
35685
|
-
const
|
|
35763
|
+
const roving = this._cardRoving;
|
|
35764
|
+
const headingId = group.id === null ? null : `${this._cardIdPrefix}-group-${safeDomPart(group.id)}-${groupIndex}`;
|
|
35765
|
+
const collapsible = group.id !== null && Boolean(options.collapsibleGroups);
|
|
35766
|
+
const collapsed = collapsible && this._cardCollapsed.has(
|
|
35767
|
+
/** @type {string} */
|
|
35768
|
+
group.id
|
|
35769
|
+
);
|
|
35686
35770
|
const list = h("ul", {
|
|
35687
35771
|
class: "zx-card-view__list",
|
|
35688
|
-
|
|
35772
|
+
id: headingId ? `${headingId}-list` : void 0,
|
|
35773
|
+
role: roving ? group.id === null ? "rowgroup" : "none" : void 0,
|
|
35774
|
+
ariaLabelledby: headingId ?? void 0,
|
|
35775
|
+
hidden: collapsed
|
|
35689
35776
|
});
|
|
35690
35777
|
for (const record of group.records) {
|
|
35691
|
-
const index = this._viewData.indexOf(record);
|
|
35692
35778
|
const id = this._viewRecordId(record);
|
|
35779
|
+
if (group.id !== null) this._cardGroupOfId.set(id, group.id);
|
|
35780
|
+
if (collapsed) continue;
|
|
35781
|
+
const index = this._viewData.indexOf(record);
|
|
35693
35782
|
const card = createRecordCard(record, index, {
|
|
35694
35783
|
fields,
|
|
35695
35784
|
visibleFields,
|
|
@@ -35706,19 +35795,287 @@ var zx = (() => {
|
|
|
35706
35795
|
headingLevel: this._cardHeadingLevel,
|
|
35707
35796
|
message: (key, ...args) => this.msg(key, ...args)
|
|
35708
35797
|
});
|
|
35798
|
+
if (roving) this._makeCardRow(card, id);
|
|
35709
35799
|
this._cardNodesById.set(id, card);
|
|
35800
|
+
this._cardIdOfNode.set(card, id);
|
|
35710
35801
|
list.append(card);
|
|
35711
35802
|
}
|
|
35712
|
-
if (group.records.length === 0) {
|
|
35713
|
-
|
|
35803
|
+
if (group.records.length === 0 && !collapsed) {
|
|
35804
|
+
const text4 = this.msg("cardView.groupEmpty");
|
|
35805
|
+
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));
|
|
35714
35806
|
}
|
|
35715
35807
|
if (group.id === null) return list;
|
|
35716
|
-
|
|
35717
|
-
|
|
35718
|
-
|
|
35719
|
-
|
|
35720
|
-
|
|
35808
|
+
const header = this._createGroupHeader(
|
|
35809
|
+
group,
|
|
35810
|
+
/** @type {string} */
|
|
35811
|
+
headingId,
|
|
35812
|
+
collapsible,
|
|
35813
|
+
collapsed
|
|
35814
|
+
);
|
|
35815
|
+
const section = h("section", {
|
|
35816
|
+
class: "zx-card-view__group",
|
|
35817
|
+
role: roving ? "rowgroup" : void 0,
|
|
35818
|
+
ariaLabelledby: headingId,
|
|
35819
|
+
dataset: { collapsed: collapsible ? String(collapsed) : null }
|
|
35820
|
+
}, header, list);
|
|
35821
|
+
this._cardSectionIds.set(section, group.id);
|
|
35822
|
+
return section;
|
|
35823
|
+
}
|
|
35824
|
+
/**
|
|
35825
|
+
* Builds a group heading: the label (or `renderGroupHeading` content), inside an APG
|
|
35826
|
+
* disclosure button when the group can collapse.
|
|
35827
|
+
* @private
|
|
35828
|
+
* @param {CardRecordGroup} group Group.
|
|
35829
|
+
* @param {string} headingId Heading id.
|
|
35830
|
+
* @param {boolean} collapsible Whether the group can collapse.
|
|
35831
|
+
* @param {boolean} collapsed Whether it is collapsed.
|
|
35832
|
+
* @returns {HTMLElement}
|
|
35833
|
+
*/
|
|
35834
|
+
_createGroupHeader(group, headingId, collapsible, collapsed) {
|
|
35835
|
+
const id = (
|
|
35836
|
+
/** @type {string} */
|
|
35837
|
+
group.id
|
|
35838
|
+
);
|
|
35839
|
+
const render = this._cardOptions().renderGroupHeading;
|
|
35840
|
+
const custom = typeof render === "function" ? render({
|
|
35841
|
+
id,
|
|
35842
|
+
label: group.label,
|
|
35843
|
+
count: group.records.length,
|
|
35844
|
+
records: [...group.records],
|
|
35845
|
+
collapsed,
|
|
35846
|
+
toggle: () => this.setGroupCollapsed(id, !this._cardCollapsed.has(id))
|
|
35847
|
+
}) : null;
|
|
35848
|
+
const label = custom == null ? group.label : isNode5(custom) ? custom : String(custom);
|
|
35849
|
+
const labelNode = h("span", { class: "zx-card-view__group-label" }, label);
|
|
35850
|
+
const roving = this._cardRoving;
|
|
35851
|
+
const content = collapsible ? h("button", {
|
|
35852
|
+
class: "zx-card-view__group-toggle",
|
|
35853
|
+
type: "button",
|
|
35854
|
+
tabindex: roving ? -1 : void 0,
|
|
35855
|
+
ariaExpanded: String(!collapsed),
|
|
35856
|
+
ariaControls: `${headingId}-list`,
|
|
35857
|
+
dataset: { groupToggle: "" }
|
|
35858
|
+
}, icon("chevron-down", { size: 14 }), labelNode) : labelNode;
|
|
35859
|
+
const heading = h("h2", { class: "zx-card-view__group-heading", id: headingId }, content);
|
|
35860
|
+
const header = h("div", {
|
|
35861
|
+
class: "zx-card-view__group-header",
|
|
35862
|
+
role: roving ? "row" : void 0,
|
|
35863
|
+
ariaExpanded: roving && collapsible ? String(!collapsed) : void 0,
|
|
35864
|
+
ariaLabelledby: roving ? headingId : void 0,
|
|
35865
|
+
tabindex: roving && collapsible ? -1 : void 0,
|
|
35866
|
+
dataset: { cardItem: roving && collapsible ? "group" : null }
|
|
35867
|
+
}, h("div", { class: "zx-card-view__group-cell", role: roving ? "rowheader" : void 0 }, heading));
|
|
35868
|
+
if (roving && collapsible) this._cardHeadersById.set(id, header);
|
|
35869
|
+
return header;
|
|
35870
|
+
}
|
|
35871
|
+
/**
|
|
35872
|
+
* Turns a rendered card into a grid row for the roving model: row focus, `aria-selected` when
|
|
35873
|
+
* selectable, and the controls inside it out of the Tab order.
|
|
35874
|
+
* @private
|
|
35875
|
+
* @param {HTMLElement} card Card element.
|
|
35876
|
+
* @param {unknown} id Record id.
|
|
35877
|
+
* @returns {void}
|
|
35878
|
+
*/
|
|
35879
|
+
_makeCardRow(card, id) {
|
|
35880
|
+
card.setAttribute("role", "row");
|
|
35881
|
+
card.tabIndex = -1;
|
|
35882
|
+
card.dataset.cardItem = "card";
|
|
35883
|
+
card.removeAttribute("aria-description");
|
|
35884
|
+
card.removeAttribute("aria-keyshortcuts");
|
|
35885
|
+
if (this.options.selectable) card.setAttribute("aria-selected", String(this._isViewSelected(id)));
|
|
35886
|
+
for (const child of card.children) child.setAttribute("role", "gridcell");
|
|
35887
|
+
for (const control of card.querySelectorAll(FOCUSABLE3)) {
|
|
35888
|
+
if (control !== card) control.tabIndex = -1;
|
|
35889
|
+
}
|
|
35890
|
+
}
|
|
35891
|
+
/**
|
|
35892
|
+
* Moves the single Tab stop to the current item after a render — the same record or group by
|
|
35893
|
+
* id; else the heading of the collapsed group that now hides the card; else the item now at its
|
|
35894
|
+
* old position — and focuses it again, without scrolling, when the collection had focus.
|
|
35895
|
+
* @private
|
|
35896
|
+
* @param {boolean} hadFocus Whether focus was inside the collection before the render.
|
|
35897
|
+
* @returns {void}
|
|
35898
|
+
*/
|
|
35899
|
+
_syncCardRoving(hadFocus) {
|
|
35900
|
+
const items = this._cardItems();
|
|
35901
|
+
if (items.length === 0) {
|
|
35902
|
+
this._syncCardEmptyFocus(hadFocus);
|
|
35903
|
+
return;
|
|
35904
|
+
}
|
|
35905
|
+
let item = this._cardItemFor(this._cardCurrent);
|
|
35906
|
+
if (!item && this._cardCurrent?.kind === "card") {
|
|
35907
|
+
const groupId = this._cardGroupOfId.get(this._cardCurrent.id);
|
|
35908
|
+
item = groupId === void 0 ? null : this._cardHeadersById.get(groupId) ?? null;
|
|
35909
|
+
}
|
|
35910
|
+
if (!item) item = items[Math.min(Math.max(this._cardCurrentIndex, 0), items.length - 1)];
|
|
35911
|
+
this._setCardTabStop(item, items);
|
|
35912
|
+
if (hadFocus) item.focus({ preventScroll: true });
|
|
35913
|
+
}
|
|
35914
|
+
/**
|
|
35915
|
+
* @private
|
|
35916
|
+
* @param {HTMLElement} item New Tab stop.
|
|
35917
|
+
* @param {HTMLElement[]} [items] Every item, when already collected.
|
|
35918
|
+
* @returns {void}
|
|
35919
|
+
*/
|
|
35920
|
+
_setCardTabStop(item, items = this._cardItems()) {
|
|
35921
|
+
for (const candidate of items) if (candidate !== item) candidate.tabIndex = -1;
|
|
35922
|
+
item.tabIndex = 0;
|
|
35923
|
+
this._cardTabStop = item;
|
|
35924
|
+
const key = this._cardItemKey(item);
|
|
35925
|
+
if (key) this._cardCurrent = key;
|
|
35926
|
+
this._cardCurrentIndex = Math.max(0, items.indexOf(item));
|
|
35927
|
+
}
|
|
35928
|
+
/**
|
|
35929
|
+
* While loading or empty there is no card to hold focus: the content takes it rather than
|
|
35930
|
+
* letting it fall to the document, and gives the Tab order its old shape back afterwards.
|
|
35931
|
+
* @private
|
|
35932
|
+
* @param {boolean} hold Whether focus was inside and there is nothing to put it on.
|
|
35933
|
+
* @returns {void}
|
|
35934
|
+
*/
|
|
35935
|
+
_syncCardEmptyFocus(hold) {
|
|
35936
|
+
if (!this._cardRoving) return;
|
|
35937
|
+
const content = (
|
|
35938
|
+
/** @type {HTMLElement} */
|
|
35939
|
+
this.refs.content
|
|
35940
|
+
);
|
|
35941
|
+
if (hold) {
|
|
35942
|
+
if (!content.hasAttribute("tabindex")) {
|
|
35943
|
+
content.tabIndex = -1;
|
|
35944
|
+
content.setAttribute("data-empty-focus", "");
|
|
35945
|
+
}
|
|
35946
|
+
content.focus({ preventScroll: true });
|
|
35947
|
+
} else if (content.hasAttribute("data-empty-focus")) {
|
|
35948
|
+
content.removeAttribute("tabindex");
|
|
35949
|
+
content.removeAttribute("data-empty-focus");
|
|
35950
|
+
}
|
|
35951
|
+
}
|
|
35952
|
+
/** @private @returns {HTMLElement[]} Roving items in display order. */
|
|
35953
|
+
_cardItems() {
|
|
35954
|
+
return (
|
|
35955
|
+
/** @type {HTMLElement[]} */
|
|
35956
|
+
[...this.refs.content.querySelectorAll("[data-card-item]")]
|
|
35957
|
+
);
|
|
35958
|
+
}
|
|
35959
|
+
/** @private @param {HTMLElement} item @returns {{kind:'card'|'group', id:unknown}|null} */
|
|
35960
|
+
_cardItemKey(item) {
|
|
35961
|
+
if (item.dataset.cardItem === "group") {
|
|
35962
|
+
for (const [id, header] of this._cardHeadersById) if (header === item) return { kind: "group", id };
|
|
35963
|
+
return null;
|
|
35964
|
+
}
|
|
35965
|
+
return this._cardIdOfNode.has(item) ? { kind: "card", id: this._cardIdOfNode.get(item) } : null;
|
|
35966
|
+
}
|
|
35967
|
+
/** @private @param {{kind:'card'|'group', id:unknown}|null} key @returns {HTMLElement|null} */
|
|
35968
|
+
_cardItemFor(key) {
|
|
35969
|
+
if (!key) return null;
|
|
35970
|
+
if (key.kind === "group") return this._cardHeadersById.get(
|
|
35971
|
+
/** @type {string} */
|
|
35972
|
+
key.id
|
|
35973
|
+
) ?? null;
|
|
35974
|
+
return (
|
|
35975
|
+
/** @type {HTMLElement|undefined} */
|
|
35976
|
+
this._cardNodesById.get(key.id) ?? null
|
|
35977
|
+
);
|
|
35978
|
+
}
|
|
35979
|
+
/** @private @param {FocusEvent} event @returns {void} */
|
|
35980
|
+
_cardFocusIn(event) {
|
|
35981
|
+
const target = (
|
|
35982
|
+
/** @type {Element|null} */
|
|
35983
|
+
event.target
|
|
35984
|
+
);
|
|
35985
|
+
const item = (
|
|
35986
|
+
/** @type {HTMLElement|null} */
|
|
35987
|
+
target?.closest?.("[data-card-item]") ?? null
|
|
35721
35988
|
);
|
|
35989
|
+
if (!item || !this.refs.content.contains(item)) return;
|
|
35990
|
+
if (this._cardTabStop !== item) this._setCardTabStop(item);
|
|
35991
|
+
if (!this._cardMoving) this._cardStickyX = item.getBoundingClientRect().left;
|
|
35992
|
+
}
|
|
35993
|
+
/**
|
|
35994
|
+
* The roving keyboard model; see the class description.
|
|
35995
|
+
* @private
|
|
35996
|
+
* @param {KeyboardEvent} event Key event.
|
|
35997
|
+
* @returns {void}
|
|
35998
|
+
*/
|
|
35999
|
+
_cardRovingKeydown(event) {
|
|
36000
|
+
const target = (
|
|
36001
|
+
/** @type {Element|null} */
|
|
36002
|
+
event.target
|
|
36003
|
+
);
|
|
36004
|
+
const item = (
|
|
36005
|
+
/** @type {HTMLElement|null} */
|
|
36006
|
+
target?.closest?.("[data-card-item]") ?? null
|
|
36007
|
+
);
|
|
36008
|
+
if (!item || !this.refs.content.contains(item)) return;
|
|
36009
|
+
const onItem = target === item;
|
|
36010
|
+
if (!onItem && target?.matches?.(TEXT_ENTRY)) return;
|
|
36011
|
+
const items = this._cardItems();
|
|
36012
|
+
const index = items.indexOf(item);
|
|
36013
|
+
const kind = item.dataset.cardItem;
|
|
36014
|
+
const key = event.key;
|
|
36015
|
+
const multi = this.options.selectable === "multi";
|
|
36016
|
+
if ((event.ctrlKey || event.metaKey) && (key === "a" || key === "A")) {
|
|
36017
|
+
if (!multi) return;
|
|
36018
|
+
event.preventDefault();
|
|
36019
|
+
this.setSelection(this._cardIdsOf(items));
|
|
36020
|
+
return;
|
|
36021
|
+
}
|
|
36022
|
+
if (onItem && (key === "Enter" || key === " ")) {
|
|
36023
|
+
event.preventDefault();
|
|
36024
|
+
if (kind === "group") {
|
|
36025
|
+
const groupId = (
|
|
36026
|
+
/** @type {string} */
|
|
36027
|
+
this._cardItemKey(item)?.id
|
|
36028
|
+
);
|
|
36029
|
+
this.setGroupCollapsed(groupId, !this._cardCollapsed.has(groupId));
|
|
36030
|
+
return;
|
|
36031
|
+
}
|
|
36032
|
+
const record = this._viewData[Number(item.dataset.recordIndex)];
|
|
36033
|
+
if (!record) return;
|
|
36034
|
+
const id = this._viewRecordId(record);
|
|
36035
|
+
if (key === " " && this.options.selectable) {
|
|
36036
|
+
this.toggleSelection(id);
|
|
36037
|
+
return;
|
|
36038
|
+
}
|
|
36039
|
+
this._activateCard(record, id, Number(item.dataset.recordIndex), event);
|
|
36040
|
+
if (this.options.selectable === "single") this.toggleSelection(id, { selected: true });
|
|
36041
|
+
return;
|
|
36042
|
+
}
|
|
36043
|
+
if (kind === "group" && onItem && (key === "ArrowLeft" || key === "ArrowRight")) {
|
|
36044
|
+
event.preventDefault();
|
|
36045
|
+
const groupId = (
|
|
36046
|
+
/** @type {string} */
|
|
36047
|
+
this._cardItemKey(item)?.id
|
|
36048
|
+
);
|
|
36049
|
+
this.setGroupCollapsed(groupId, key === "ArrowLeft");
|
|
36050
|
+
return;
|
|
36051
|
+
}
|
|
36052
|
+
if (!NAVIGATION_KEYS.has(key)) return;
|
|
36053
|
+
const rects = items.map((candidate) => candidate.getBoundingClientRect());
|
|
36054
|
+
const vertical = key === "ArrowUp" || key === "ArrowDown" || key === "PageUp" || key === "PageDown";
|
|
36055
|
+
const x = vertical ? this._cardStickyX ?? rects[index].left : rects[index].left;
|
|
36056
|
+
const page = cardPageSize(viewportHeight(this.refs.content), rects[index].height);
|
|
36057
|
+
const next = cardGridTarget(rects, index, key, { page, x });
|
|
36058
|
+
if (next === null) return;
|
|
36059
|
+
event.preventDefault();
|
|
36060
|
+
if (next === index) return;
|
|
36061
|
+
const destination = items[next];
|
|
36062
|
+
if (!vertical) this._cardStickyX = rects[next].left;
|
|
36063
|
+
this._cardMoving = true;
|
|
36064
|
+
try {
|
|
36065
|
+
this._setCardTabStop(destination, items);
|
|
36066
|
+
destination.focus({ preventScroll: true });
|
|
36067
|
+
destination.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
36068
|
+
} finally {
|
|
36069
|
+
this._cardMoving = false;
|
|
36070
|
+
}
|
|
36071
|
+
if (event.shiftKey && multi) {
|
|
36072
|
+
const span = items.slice(Math.min(index, next), Math.max(index, next) + 1);
|
|
36073
|
+
this.setSelection([...this.getSelectionIds(), ...this._cardIdsOf(span)]);
|
|
36074
|
+
}
|
|
36075
|
+
}
|
|
36076
|
+
/** @private @param {HTMLElement[]} items @returns {unknown[]} Record ids of the card items. */
|
|
36077
|
+
_cardIdsOf(items) {
|
|
36078
|
+
return items.filter((item) => item.dataset.cardItem === "card").map((item) => this._viewData[Number(item.dataset.recordIndex)]).filter(Boolean).map((record) => this._viewRecordId(record));
|
|
35722
36079
|
}
|
|
35723
36080
|
/** @private @returns {HTMLElement} */
|
|
35724
36081
|
_createCardSkeletons() {
|
|
@@ -35749,10 +36106,14 @@ var zx = (() => {
|
|
|
35749
36106
|
for (const [id, card] of this._cardNodesById) {
|
|
35750
36107
|
const selected = this._isViewSelected(id);
|
|
35751
36108
|
card.dataset.selected = String(selected);
|
|
35752
|
-
if (this.
|
|
35753
|
-
"aria-
|
|
35754
|
-
|
|
35755
|
-
|
|
36109
|
+
if (this._cardRoving) {
|
|
36110
|
+
if (this.options.selectable) card.setAttribute("aria-selected", String(selected));
|
|
36111
|
+
} else if (this.options.selectable) {
|
|
36112
|
+
card.setAttribute(
|
|
36113
|
+
"aria-description",
|
|
36114
|
+
this.msg(selected ? "recordCard.selected" : "recordCard.notSelected")
|
|
36115
|
+
);
|
|
36116
|
+
}
|
|
35756
36117
|
const checkbox = (
|
|
35757
36118
|
/** @type {HTMLInputElement|null} */
|
|
35758
36119
|
card.querySelector("[data-record-selection]")
|
|
@@ -35766,6 +36127,16 @@ var zx = (() => {
|
|
|
35766
36127
|
/** @type {Element|null} */
|
|
35767
36128
|
event.target
|
|
35768
36129
|
);
|
|
36130
|
+
const toggle = (
|
|
36131
|
+
/** @type {HTMLElement|null} */
|
|
36132
|
+
target?.closest?.("[data-group-toggle]")
|
|
36133
|
+
);
|
|
36134
|
+
if (toggle && this.refs.content.contains(toggle)) {
|
|
36135
|
+
const section = toggle.closest(".zx-card-view__group");
|
|
36136
|
+
const id2 = section ? this._cardSectionIds.get(section) : void 0;
|
|
36137
|
+
if (id2 !== void 0) this.setGroupCollapsed(id2, !this._cardCollapsed.has(id2));
|
|
36138
|
+
return;
|
|
36139
|
+
}
|
|
35769
36140
|
const card = (
|
|
35770
36141
|
/** @type {HTMLElement|null} */
|
|
35771
36142
|
target?.closest?.(".zx-record-card")
|
|
@@ -35873,6 +36244,58 @@ var zx = (() => {
|
|
|
35873
36244
|
);
|
|
35874
36245
|
}
|
|
35875
36246
|
};
|
|
36247
|
+
var FOCUSABLE3 = 'a[href],button,input,select,textarea,summary,[tabindex]:not([tabindex="-1"]),[contenteditable="true"]';
|
|
36248
|
+
var TEXT_ENTRY = 'input:not([type="checkbox"]):not([type="radio"]),select,textarea,[contenteditable="true"]';
|
|
36249
|
+
var NAVIGATION_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
|
|
36250
|
+
function cardGridRows(rects, tolerance = 4) {
|
|
36251
|
+
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);
|
|
36252
|
+
const rows = [];
|
|
36253
|
+
for (const entry of order) {
|
|
36254
|
+
const row2 = rows.at(-1);
|
|
36255
|
+
if (row2 && Math.abs(entry.top - row2.top) <= tolerance) row2.items.push(entry);
|
|
36256
|
+
else rows.push({ top: entry.top, items: [entry] });
|
|
36257
|
+
}
|
|
36258
|
+
return rows.map((row2) => row2.items.sort((a, b) => a.left - b.left).map((entry) => entry.index));
|
|
36259
|
+
}
|
|
36260
|
+
function cardGridTarget(rects, index, key, options = {}) {
|
|
36261
|
+
if (rects.length === 0) return null;
|
|
36262
|
+
const current = Math.min(Math.max(Math.trunc(Number(index) || 0), 0), rects.length - 1);
|
|
36263
|
+
if (key === "Home") return 0;
|
|
36264
|
+
if (key === "End") return rects.length - 1;
|
|
36265
|
+
const rows = cardGridRows(rects);
|
|
36266
|
+
const rowIndex = rows.findIndex((row3) => row3.includes(current));
|
|
36267
|
+
const row2 = rows[rowIndex];
|
|
36268
|
+
if (key === "ArrowLeft" || key === "ArrowRight") {
|
|
36269
|
+
const position2 = row2.indexOf(current) + (key === "ArrowLeft" ? -1 : 1);
|
|
36270
|
+
return position2 < 0 || position2 >= row2.length ? current : row2[position2];
|
|
36271
|
+
}
|
|
36272
|
+
const page = Math.max(1, Math.trunc(Number(options.page) || 10));
|
|
36273
|
+
const step = { ArrowUp: -1, ArrowDown: 1, PageUp: -page, PageDown: page }[key];
|
|
36274
|
+
if (step === void 0) return null;
|
|
36275
|
+
const targetRow = rows[Math.min(Math.max(rowIndex + step, 0), rows.length - 1)];
|
|
36276
|
+
if (targetRow === row2) return current;
|
|
36277
|
+
const x = Number.isFinite(Number(options.x)) ? Number(options.x) : Number(rects[current].left) || 0;
|
|
36278
|
+
let best = targetRow[0];
|
|
36279
|
+
for (const candidate of targetRow) {
|
|
36280
|
+
if (Math.abs((Number(rects[candidate].left) || 0) - x) < Math.abs((Number(rects[best].left) || 0) - x)) best = candidate;
|
|
36281
|
+
}
|
|
36282
|
+
return best;
|
|
36283
|
+
}
|
|
36284
|
+
function cardPageSize(viewport, rowHeight, fallback = 10) {
|
|
36285
|
+
const visible = Number(viewport);
|
|
36286
|
+
const height = Number(rowHeight);
|
|
36287
|
+
if (!(visible > 0) || !(height > 0)) return Math.max(1, Math.trunc(fallback) || 10);
|
|
36288
|
+
return Math.max(1, Math.floor(visible / height) - 1);
|
|
36289
|
+
}
|
|
36290
|
+
function viewportHeight(element) {
|
|
36291
|
+
let node = element.parentElement;
|
|
36292
|
+
while (node && node !== element.ownerDocument.body) {
|
|
36293
|
+
const overflow = getComputedStyle(node).overflowY;
|
|
36294
|
+
if (/(auto|scroll|overlay)/.test(overflow) && node.scrollHeight > node.clientHeight) return node.clientHeight;
|
|
36295
|
+
node = node.parentElement;
|
|
36296
|
+
}
|
|
36297
|
+
return element.ownerDocument.defaultView?.innerHeight ?? 0;
|
|
36298
|
+
}
|
|
35876
36299
|
function groupCardRecords(records, groupBy2, groupOrder = [], fields = [], ungroupedLabel = "Ungrouped") {
|
|
35877
36300
|
if (!Array.isArray(records)) throw new TypeError("Card view records must be an array");
|
|
35878
36301
|
if (groupBy2 == null) return records.length ? [{ id: null, label: "", records: [...records] }] : [];
|