@zeyos/zx 4.3.0 → 4.3.4
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 +111 -1
- package/README.md +0 -3
- package/dist/types/components/calendar/calendar-model.d.ts +22 -0
- package/dist/types/components/card-view/card-view.d.ts +1 -1
- package/dist/types/components/card-view/record-card.d.ts +47 -5
- package/dist/types/components/kanban-view/kanban-policy.d.ts +268 -0
- package/dist/types/components/kanban-view/kanban-view.d.ts +669 -24
- package/dist/types/core/component.d.ts +6 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/zx-zeyos.esm.js +35 -11
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +597 -78
- package/dist/zx.esm.js +1758 -238
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +1758 -238
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +214 -2
- package/docs/llms.md +30 -30
- package/docs/llms.txt +0 -2
- package/package.json +1 -2
- package/src/components/calendar/calendar-model.js +45 -0
- package/src/components/calendar/calendar.css +30 -10
- package/src/components/calendar/calendar.js +8 -10
- package/src/components/card-view/card-view.css +211 -9
- package/src/components/card-view/card-view.js +7 -7
- package/src/components/card-view/record-card.js +92 -19
- package/src/components/kanban-view/kanban-policy.js +412 -0
- package/src/components/kanban-view/kanban-view.css +480 -36
- package/src/components/kanban-view/kanban-view.js +1514 -235
- package/src/components/message/message.css +9 -10
- package/src/core/component.js +8 -3
- package/src/index.js +8 -3
- package/DESIGN-SYSTEM.md +0 -182
package/dist/zx.esm.js
CHANGED
|
@@ -798,21 +798,26 @@ var Component = class extends EventTarget {
|
|
|
798
798
|
* Dispatches a component event and a bubbling, composed `zx-*` DOM event.
|
|
799
799
|
* @param {string} type Lowercase event name.
|
|
800
800
|
* @param {Record<string, unknown>} [detail={}] Single event detail object.
|
|
801
|
+
* @param {{honorDomCancellation?:boolean}} [options={}] Mirrored DOM-event behavior. Components
|
|
802
|
+
* with a cancelable mutation boundary may opt into treating `preventDefault()` on the bubbling
|
|
803
|
+
* `zx-*` event exactly like cancellation of the component event.
|
|
801
804
|
* @returns {Event & {detail: Record<string, unknown>}}
|
|
802
805
|
* @fires Component#event
|
|
803
806
|
*/
|
|
804
|
-
emit(type, detail = {}) {
|
|
807
|
+
emit(type, detail = {}, options = {}) {
|
|
805
808
|
if (detail === null || typeof detail !== "object" || Array.isArray(detail)) {
|
|
806
809
|
throw new TypeError("Event detail must be an object");
|
|
807
810
|
}
|
|
808
811
|
const componentEvent = createCustomEvent(type, detail, { cancelable: true });
|
|
809
812
|
this.dispatchEvent(componentEvent);
|
|
810
813
|
if (this.el) {
|
|
811
|
-
|
|
814
|
+
const domEvent = createCustomEvent(`zx-${type}`, detail, {
|
|
812
815
|
bubbles: true,
|
|
813
816
|
composed: true,
|
|
814
817
|
cancelable: true
|
|
815
|
-
})
|
|
818
|
+
});
|
|
819
|
+
this.el.dispatchEvent(domEvent);
|
|
820
|
+
if (options.honorDomCancellation && domEvent.defaultPrevented) componentEvent.preventDefault();
|
|
816
821
|
}
|
|
817
822
|
return componentEvent;
|
|
818
823
|
}
|
|
@@ -24009,6 +24014,29 @@ function calendarRange(view, anchor, options = {}) {
|
|
|
24009
24014
|
}
|
|
24010
24015
|
return { start, end };
|
|
24011
24016
|
}
|
|
24017
|
+
function calendarPageDate(view, anchor, direction, options = {}) {
|
|
24018
|
+
const date = validDate5(anchor, "Calendar date");
|
|
24019
|
+
const step = integer(direction, "direction");
|
|
24020
|
+
if (view === "day") date.setDate(date.getDate() + step);
|
|
24021
|
+
else if (view === "agenda") {
|
|
24022
|
+
date.setDate(date.getDate() + step * positiveInteger(options.agendaDays ?? 14, "agendaDays"));
|
|
24023
|
+
} else if (view === "week") date.setDate(date.getDate() + step * 7);
|
|
24024
|
+
else if (view === "month") return addCalendarMonths(date, step);
|
|
24025
|
+
else if (view === "year") return addCalendarMonths(date, step * 12);
|
|
24026
|
+
else throw new RangeError(`Unknown calendar view: ${view}`);
|
|
24027
|
+
return date;
|
|
24028
|
+
}
|
|
24029
|
+
function addCalendarMonths(value, months) {
|
|
24030
|
+
const date = validDate5(value, "Calendar date");
|
|
24031
|
+
const day = date.getDate();
|
|
24032
|
+
date.setDate(1);
|
|
24033
|
+
date.setMonth(date.getMonth() + integer(months, "month amount"));
|
|
24034
|
+
date.setDate(Math.min(day, daysInCalendarMonth(date.getFullYear(), date.getMonth())));
|
|
24035
|
+
return date;
|
|
24036
|
+
}
|
|
24037
|
+
function daysInCalendarMonth(year, month) {
|
|
24038
|
+
return new Date(year, month + 1, 0).getDate();
|
|
24039
|
+
}
|
|
24012
24040
|
function normalizeCalendarEvents(records, options = {}) {
|
|
24013
24041
|
if (!Array.isArray(records)) throw new TypeError("Calendar events must be an array");
|
|
24014
24042
|
const readers = {
|
|
@@ -24452,7 +24480,7 @@ var Calendar = class extends Component {
|
|
|
24452
24480
|
durationEditable: Object.prototype.hasOwnProperty.call(changes, "durationEditable") ? changes.durationEditable : current.durationEditable,
|
|
24453
24481
|
data: changes.data ?? current.data
|
|
24454
24482
|
};
|
|
24455
|
-
const [next] = normalizeCalendarEvents([source]);
|
|
24483
|
+
const [next] = normalizeCalendarEvents([source], { dateUnit: this.options.dateUnit });
|
|
24456
24484
|
next.data = source.data;
|
|
24457
24485
|
this._events = this._events.map((event, eventIndex) => eventIndex === index ? next : event);
|
|
24458
24486
|
this._mutationVersions.delete(String(id));
|
|
@@ -25478,13 +25506,9 @@ var Calendar = class extends Component {
|
|
|
25478
25506
|
}
|
|
25479
25507
|
/** @param {number} direction @returns {this} */
|
|
25480
25508
|
_page(direction) {
|
|
25481
|
-
|
|
25482
|
-
|
|
25483
|
-
|
|
25484
|
-
else if (this._view === "week") date.setDate(date.getDate() + direction * 7);
|
|
25485
|
-
else if (this._view === "month") date.setMonth(date.getMonth() + direction);
|
|
25486
|
-
else date.setFullYear(date.getFullYear() + direction);
|
|
25487
|
-
return this.setDate(date);
|
|
25509
|
+
return this.setDate(calendarPageDate(this._view, this._date, direction, {
|
|
25510
|
+
agendaDays: this.options.agendaDays
|
|
25511
|
+
}));
|
|
25488
25512
|
}
|
|
25489
25513
|
/** @param {{start: Date, end: Date}} range @returns {string} */
|
|
25490
25514
|
_rangeTitle(range) {
|
|
@@ -26641,6 +26665,16 @@ function createRecordCard(record, index, options = {}) {
|
|
|
26641
26665
|
class: "zx-record-card__heading",
|
|
26642
26666
|
id: headingId
|
|
26643
26667
|
}, headingContent);
|
|
26668
|
+
const fieldGroups = partitionRecordCardFields(visibleFields, titleSource, subtitleSource);
|
|
26669
|
+
const eyebrow = createEyebrow(record, index, fieldGroups.eyebrowStart, fieldGroups.eyebrowEnd);
|
|
26670
|
+
const titlePrefix = createFieldList(
|
|
26671
|
+
record,
|
|
26672
|
+
index,
|
|
26673
|
+
fieldGroups.titlePrefix,
|
|
26674
|
+
"zx-record-card__title-prefix",
|
|
26675
|
+
"title-prefix"
|
|
26676
|
+
);
|
|
26677
|
+
const titleRow = titlePrefix ? h("div", { class: "zx-record-card__title-row" }, titlePrefix, heading) : heading;
|
|
26644
26678
|
const selection = selectable === "multi" ? h("label", {
|
|
26645
26679
|
class: "zx-record-card__selection"
|
|
26646
26680
|
}, h("input", {
|
|
@@ -26662,19 +26696,19 @@ function createRecordCard(record, index, options = {}) {
|
|
|
26662
26696
|
h(
|
|
26663
26697
|
"div",
|
|
26664
26698
|
{ class: "zx-record-card__titles" },
|
|
26665
|
-
|
|
26699
|
+
titleRow,
|
|
26666
26700
|
isEmptyContent(subtitle) ? null : h("p", { class: "zx-record-card__subtitle" }, contentNode(subtitle))
|
|
26667
26701
|
),
|
|
26668
26702
|
actionArea
|
|
26669
26703
|
);
|
|
26670
|
-
const metadata =
|
|
26671
|
-
const body = h("div", { class: "zx-record-card__body" }, header, metadata);
|
|
26704
|
+
const metadata = createFieldList(record, index, fieldGroups.metadata, "zx-record-card__metadata");
|
|
26705
|
+
const body = h("div", { class: "zx-record-card__body" }, eyebrow, header, metadata);
|
|
26672
26706
|
const card = (
|
|
26673
26707
|
/** @type {HTMLLIElement} */
|
|
26674
26708
|
h("li", {
|
|
26675
26709
|
class: "zx-record-card",
|
|
26676
26710
|
tabindex: 0,
|
|
26677
|
-
|
|
26711
|
+
ariaDescription: selectable ? selected ? "Selected" : "Not selected" : void 0,
|
|
26678
26712
|
ariaLabelledby: headingId,
|
|
26679
26713
|
dataset: {
|
|
26680
26714
|
recordIndex: String(index),
|
|
@@ -26685,6 +26719,20 @@ function createRecordCard(record, index, options = {}) {
|
|
|
26685
26719
|
);
|
|
26686
26720
|
return card;
|
|
26687
26721
|
}
|
|
26722
|
+
function partitionRecordCardFields(fields, titleSource = null, subtitleSource = null) {
|
|
26723
|
+
const groups = { eyebrowStart: [], eyebrowEnd: [], titlePrefix: [], metadata: [] };
|
|
26724
|
+
const titleId = typeof titleSource === "string" ? titleSource : null;
|
|
26725
|
+
const subtitleId = typeof subtitleSource === "string" ? subtitleSource : null;
|
|
26726
|
+
for (const field of Array.isArray(fields) ? fields : []) {
|
|
26727
|
+
if (!field.duplicate && (field.id === titleId || field.id === subtitleId)) continue;
|
|
26728
|
+
const slot = resolveRecordCardFieldSlot(field);
|
|
26729
|
+
if (slot === "eyebrow-start") groups.eyebrowStart.push(field);
|
|
26730
|
+
else if (slot === "eyebrow-end") groups.eyebrowEnd.push(field);
|
|
26731
|
+
else if (slot === "title-prefix") groups.titlePrefix.push(field);
|
|
26732
|
+
else groups.metadata.push(field);
|
|
26733
|
+
}
|
|
26734
|
+
return groups;
|
|
26735
|
+
}
|
|
26688
26736
|
function resolveRecordPreview(record, index, preview, previewAlt, fields = []) {
|
|
26689
26737
|
if (preview == null) return null;
|
|
26690
26738
|
const candidate = typeof preview === "function" ? preview(record, index) : readRecordSource(record, index, preview, fields, false);
|
|
@@ -26753,21 +26801,49 @@ function createPreview(preview) {
|
|
|
26753
26801
|
dataset: { failed: String(preview.rejected) }
|
|
26754
26802
|
}, fallback, image);
|
|
26755
26803
|
}
|
|
26756
|
-
function
|
|
26757
|
-
const
|
|
26758
|
-
|
|
26759
|
-
|
|
26760
|
-
|
|
26761
|
-
|
|
26804
|
+
function createEyebrow(record, index, eyebrowStart, eyebrowEnd) {
|
|
26805
|
+
const start = createFieldList(
|
|
26806
|
+
record,
|
|
26807
|
+
index,
|
|
26808
|
+
eyebrowStart,
|
|
26809
|
+
"zx-record-card__eyebrow-group",
|
|
26810
|
+
"eyebrow-start"
|
|
26811
|
+
);
|
|
26812
|
+
const end = createFieldList(
|
|
26813
|
+
record,
|
|
26814
|
+
index,
|
|
26815
|
+
eyebrowEnd,
|
|
26816
|
+
"zx-record-card__eyebrow-group",
|
|
26817
|
+
"eyebrow-end"
|
|
26818
|
+
);
|
|
26819
|
+
return start || end ? h("div", { class: "zx-record-card__eyebrow" }, start, end) : null;
|
|
26820
|
+
}
|
|
26821
|
+
function createFieldList(record, index, fields, className, slot = null) {
|
|
26822
|
+
if (fields.length === 0) return null;
|
|
26823
|
+
return h("dl", {
|
|
26824
|
+
class: className,
|
|
26825
|
+
dataset: slot ? { cardSlot: slot } : void 0
|
|
26826
|
+
}, fields.map((field) => h(
|
|
26762
26827
|
"div",
|
|
26763
26828
|
{
|
|
26764
26829
|
class: "zx-record-card__field",
|
|
26765
|
-
dataset: { fieldId: field.id }
|
|
26830
|
+
dataset: { fieldId: field.id, ...slot ? { cardSlot: slot } : {} }
|
|
26766
26831
|
},
|
|
26767
26832
|
h("dt", { class: "zx-record-card__label" }, field.label),
|
|
26768
26833
|
h("dd", { class: "zx-record-card__value" }, contentNode(renderViewField(field, record, index)))
|
|
26769
26834
|
)));
|
|
26770
26835
|
}
|
|
26836
|
+
function resolveRecordCardFieldSlot(field) {
|
|
26837
|
+
const view = field?.view;
|
|
26838
|
+
if (!view || typeof view !== "object" || Array.isArray(view)) return null;
|
|
26839
|
+
const card = view.card;
|
|
26840
|
+
if (!card || typeof card !== "object" || Array.isArray(card)) return null;
|
|
26841
|
+
const slot = (
|
|
26842
|
+
/** @type {{slot?:unknown}} */
|
|
26843
|
+
card.slot
|
|
26844
|
+
);
|
|
26845
|
+
return slot === "eyebrow-start" || slot === "eyebrow-end" || slot === "title-prefix" ? slot : null;
|
|
26846
|
+
}
|
|
26771
26847
|
function createAction(action) {
|
|
26772
26848
|
const label = String(action.label ?? "");
|
|
26773
26849
|
const title = String(action.title ?? (label || action.id));
|
|
@@ -26837,7 +26913,7 @@ function safePreviewUrl(value) {
|
|
|
26837
26913
|
try {
|
|
26838
26914
|
const base = globalThis.location?.href ?? "https://zx.invalid/";
|
|
26839
26915
|
const protocol = new URL(href, base).protocol.toLowerCase();
|
|
26840
|
-
return ["http:", "https:", "blob:"
|
|
26916
|
+
return ["http:", "https:", "blob:"].includes(protocol) ? href : null;
|
|
26841
26917
|
} catch {
|
|
26842
26918
|
return null;
|
|
26843
26919
|
}
|
|
@@ -26881,7 +26957,7 @@ var CardView = class extends RecordView {
|
|
|
26881
26957
|
actions: [],
|
|
26882
26958
|
groupBy: null,
|
|
26883
26959
|
groupOrder: [],
|
|
26884
|
-
minCardWidth: "
|
|
26960
|
+
minCardWidth: "15rem",
|
|
26885
26961
|
maxColumns: null,
|
|
26886
26962
|
variant: "outlined",
|
|
26887
26963
|
loadingCount: 6,
|
|
@@ -27155,7 +27231,7 @@ var CardView = class extends RecordView {
|
|
|
27155
27231
|
for (const [id, card] of this._cardNodesById) {
|
|
27156
27232
|
const selected = this._isViewSelected(id);
|
|
27157
27233
|
card.dataset.selected = String(selected);
|
|
27158
|
-
if (this.options.selectable) card.setAttribute("aria-
|
|
27234
|
+
if (this.options.selectable) card.setAttribute("aria-description", selected ? "Selected" : "Not selected");
|
|
27159
27235
|
const checkbox = (
|
|
27160
27236
|
/** @type {HTMLInputElement|null} */
|
|
27161
27237
|
card.querySelector("[data-record-selection]")
|
|
@@ -27299,11 +27375,11 @@ function normalizeGroupId(value) {
|
|
|
27299
27375
|
return value == null || value === "" ? "" : String(value);
|
|
27300
27376
|
}
|
|
27301
27377
|
function normalizeCardWidth(value) {
|
|
27302
|
-
if (typeof value === "number") return Number.isFinite(value) && value > 0 ? `${value}px` : "
|
|
27378
|
+
if (typeof value === "number") return Number.isFinite(value) && value > 0 ? `${value}px` : "15rem";
|
|
27303
27379
|
const text4 = String(value ?? "").trim();
|
|
27304
|
-
if (!text4 || text4.length > 128 || /[;{}\u0000-\u001f\u007f]/.test(text4)) return "
|
|
27305
|
-
if (/(?:url|image|image-set|cross-fade|element|expression)\s*\(/i.test(text4)) return "
|
|
27306
|
-
if (typeof globalThis.CSS?.supports === "function" && !globalThis.CSS.supports("width", text4)) return "
|
|
27380
|
+
if (!text4 || text4.length > 128 || /[;{}\u0000-\u001f\u007f]/.test(text4)) return "15rem";
|
|
27381
|
+
if (/(?:url|image|image-set|cross-fade|element|expression)\s*\(/i.test(text4)) return "15rem";
|
|
27382
|
+
if (typeof globalThis.CSS?.supports === "function" && !globalThis.CSS.supports("width", text4)) return "15rem";
|
|
27307
27383
|
return text4;
|
|
27308
27384
|
}
|
|
27309
27385
|
function normalizeMaxColumns(value) {
|
|
@@ -27341,8 +27417,284 @@ function safeDomPart(value) {
|
|
|
27341
27417
|
return value.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "empty";
|
|
27342
27418
|
}
|
|
27343
27419
|
|
|
27420
|
+
// src/components/kanban-view/kanban-policy.js
|
|
27421
|
+
var RULE_TONES = Object.freeze(["neutral", "accent", "success", "warning", "danger", "info"]);
|
|
27422
|
+
function normalizeKanbanRules(rules) {
|
|
27423
|
+
if (rules == null) return [];
|
|
27424
|
+
if (!Array.isArray(rules)) throw new TypeError("Kanban rules must be an array or null");
|
|
27425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
27426
|
+
return rules.map((rule, index) => {
|
|
27427
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
|
27428
|
+
throw new TypeError(`Kanban rule at index ${index} must be an object`);
|
|
27429
|
+
}
|
|
27430
|
+
const source = (
|
|
27431
|
+
/** @type {Record<string, any>} */
|
|
27432
|
+
rule
|
|
27433
|
+
);
|
|
27434
|
+
const id = String(source.id ?? "").trim();
|
|
27435
|
+
if (!id) throw new TypeError(`Kanban rule at index ${index} requires an id`);
|
|
27436
|
+
if (seen.has(id)) throw new TypeError(`Duplicate Kanban rule: ${id}`);
|
|
27437
|
+
if (typeof source.when !== "function") {
|
|
27438
|
+
throw new TypeError(`Kanban rule ${id} requires a when predicate`);
|
|
27439
|
+
}
|
|
27440
|
+
seen.add(id);
|
|
27441
|
+
return {
|
|
27442
|
+
id,
|
|
27443
|
+
when: source.when,
|
|
27444
|
+
tone: RULE_TONES.includes(source.tone) ? source.tone : "accent",
|
|
27445
|
+
label: source.label == null ? "" : String(source.label),
|
|
27446
|
+
icon: source.icon == null ? null : String(source.icon),
|
|
27447
|
+
description: source.description == null ? "" : String(source.description)
|
|
27448
|
+
};
|
|
27449
|
+
});
|
|
27450
|
+
}
|
|
27451
|
+
function resolveKanbanRules(rules, record, context) {
|
|
27452
|
+
if (!Array.isArray(rules) || !rules.length) return [];
|
|
27453
|
+
return rules.filter((rule) => {
|
|
27454
|
+
try {
|
|
27455
|
+
return rule.when(record, context) === true;
|
|
27456
|
+
} catch {
|
|
27457
|
+
return false;
|
|
27458
|
+
}
|
|
27459
|
+
});
|
|
27460
|
+
}
|
|
27461
|
+
function evaluateKanbanMove(input) {
|
|
27462
|
+
const { records, column, swimlane, context, from } = input;
|
|
27463
|
+
const moving = Math.max(1, records.length);
|
|
27464
|
+
const policy = column?.wipPolicy === "block" || column?.wipPolicy === "warn" ? column.wipPolicy : input.policy === "block" ? "block" : "warn";
|
|
27465
|
+
const refuse = (reason, extra = {}) => ({
|
|
27466
|
+
allowed: false,
|
|
27467
|
+
reason,
|
|
27468
|
+
limit: null,
|
|
27469
|
+
count: 0,
|
|
27470
|
+
limitExceeded: false,
|
|
27471
|
+
limitReached: false,
|
|
27472
|
+
policy,
|
|
27473
|
+
...extra
|
|
27474
|
+
});
|
|
27475
|
+
const blockedTransition = allowsKanbanTransition(column, swimlane, from);
|
|
27476
|
+
if (blockedTransition) return refuse(blockedTransition);
|
|
27477
|
+
for (const record of records) {
|
|
27478
|
+
if (column.accept && column.accept(record, context) === false) return refuse("accept");
|
|
27479
|
+
if (swimlane?.accept && swimlane.accept(record, context) === false) return refuse("lane-accept");
|
|
27480
|
+
}
|
|
27481
|
+
const capacities = [];
|
|
27482
|
+
if (column.limit != null) {
|
|
27483
|
+
capacities.push({ reason: "wip", limit: column.limit, count: input.columnCount + moving });
|
|
27484
|
+
}
|
|
27485
|
+
const cellLimit = swimlane ? readLaneLimit(column.laneLimits, swimlane.id) : null;
|
|
27486
|
+
if (cellLimit != null) {
|
|
27487
|
+
capacities.push({ reason: "wip", limit: cellLimit, count: input.cellCount + moving });
|
|
27488
|
+
}
|
|
27489
|
+
if (swimlane?.limit != null) {
|
|
27490
|
+
capacities.push({ reason: "lane-wip", limit: swimlane.limit, count: input.laneCount + moving });
|
|
27491
|
+
}
|
|
27492
|
+
const violated = capacities.find((capacity) => capacity.count > capacity.limit);
|
|
27493
|
+
const reported = violated ?? capacities.find((capacity) => capacity.count === capacity.limit) ?? capacities[0];
|
|
27494
|
+
if (violated && policy === "block") {
|
|
27495
|
+
return refuse(violated.reason, {
|
|
27496
|
+
limit: violated.limit,
|
|
27497
|
+
count: violated.count,
|
|
27498
|
+
limitExceeded: true
|
|
27499
|
+
});
|
|
27500
|
+
}
|
|
27501
|
+
return {
|
|
27502
|
+
allowed: true,
|
|
27503
|
+
reason: null,
|
|
27504
|
+
limit: reported ? reported.limit : null,
|
|
27505
|
+
count: reported ? reported.count : 0,
|
|
27506
|
+
limitExceeded: Boolean(violated),
|
|
27507
|
+
limitReached: Boolean(reported && !violated && reported.count === reported.limit),
|
|
27508
|
+
policy
|
|
27509
|
+
};
|
|
27510
|
+
}
|
|
27511
|
+
function allowsKanbanTransition(column, swimlane, from) {
|
|
27512
|
+
if (column.id !== from.column && !allowsOrigin(column.from, from.column)) return "transition";
|
|
27513
|
+
if (swimlane && swimlane.id !== from.lane && !allowsOrigin(swimlane.from, from.lane)) {
|
|
27514
|
+
return "lane-transition";
|
|
27515
|
+
}
|
|
27516
|
+
return null;
|
|
27517
|
+
}
|
|
27518
|
+
function kanbanSearchTerms(search) {
|
|
27519
|
+
const text4 = search == null ? "" : String(search).trim().toLowerCase();
|
|
27520
|
+
if (!text4) return [];
|
|
27521
|
+
return [...text4.matchAll(/"([^"]*)"|(\S+)/g)].map((match) => (match[1] ?? match[2] ?? "").trim()).filter(Boolean);
|
|
27522
|
+
}
|
|
27523
|
+
function matchesKanbanSearch(text4, terms) {
|
|
27524
|
+
if (!terms.length) return true;
|
|
27525
|
+
const haystack = text4.toLowerCase();
|
|
27526
|
+
return terms.every((term) => haystack.includes(term));
|
|
27527
|
+
}
|
|
27528
|
+
function createKanbanHistory(limit = 50) {
|
|
27529
|
+
const max = Number.isFinite(Number(limit)) && Number(limit) > 0 ? Math.floor(Number(limit)) : 50;
|
|
27530
|
+
let undoStack = [];
|
|
27531
|
+
let redoStack = [];
|
|
27532
|
+
return {
|
|
27533
|
+
push(entry) {
|
|
27534
|
+
undoStack.push(entry);
|
|
27535
|
+
if (undoStack.length > max) undoStack = undoStack.slice(-max);
|
|
27536
|
+
redoStack = [];
|
|
27537
|
+
},
|
|
27538
|
+
undo(current) {
|
|
27539
|
+
const entry = undoStack.pop();
|
|
27540
|
+
if (!entry) return null;
|
|
27541
|
+
redoStack.push(current);
|
|
27542
|
+
if (redoStack.length > max) redoStack = redoStack.slice(-max);
|
|
27543
|
+
return entry;
|
|
27544
|
+
},
|
|
27545
|
+
redo(current) {
|
|
27546
|
+
const entry = redoStack.pop();
|
|
27547
|
+
if (!entry) return null;
|
|
27548
|
+
undoStack.push(current);
|
|
27549
|
+
if (undoStack.length > max) undoStack = undoStack.slice(-max);
|
|
27550
|
+
return entry;
|
|
27551
|
+
},
|
|
27552
|
+
canUndo: () => undoStack.length > 0,
|
|
27553
|
+
canRedo: () => redoStack.length > 0,
|
|
27554
|
+
clear() {
|
|
27555
|
+
undoStack = [];
|
|
27556
|
+
redoStack = [];
|
|
27557
|
+
},
|
|
27558
|
+
depth: () => ({ undo: undoStack.length, redo: redoStack.length })
|
|
27559
|
+
};
|
|
27560
|
+
}
|
|
27561
|
+
function reorderKanbanRecords(records, entries, options) {
|
|
27562
|
+
if (!Array.isArray(records) || !Array.isArray(entries) || !entries.length) return null;
|
|
27563
|
+
const sources = entries.map((entry) => ({
|
|
27564
|
+
...entry,
|
|
27565
|
+
index: records.findIndex((record) => Object.is(viewRecordId(record, options.recordId), entry.id))
|
|
27566
|
+
}));
|
|
27567
|
+
if (sources.some((source) => source.index < 0 || !source.record || typeof source.record !== "object")) {
|
|
27568
|
+
return null;
|
|
27569
|
+
}
|
|
27570
|
+
sources.sort((left, right) => left.index - right.index);
|
|
27571
|
+
const moves = sources.map((source) => ({
|
|
27572
|
+
id: source.id,
|
|
27573
|
+
from: locateKanbanRecord(records, source.index, options)
|
|
27574
|
+
}));
|
|
27575
|
+
const movingIndices = new Set(sources.map((source) => source.index));
|
|
27576
|
+
const next = records.filter((_, index) => !movingIndices.has(index));
|
|
27577
|
+
const destinationRecords = next.filter((record, index) => normalizeAxisId(readKanbanAccessor(record, index, options.columnBy)) === options.destination.column && (options.swimlaneBy == null || normalizeAxisId(readKanbanAccessor(record, index, options.swimlaneBy)) === options.destination.lane));
|
|
27578
|
+
const targetIndex = clampKanbanIndex(options.destination.index, destinationRecords.length);
|
|
27579
|
+
let insertion = next.length;
|
|
27580
|
+
if (destinationRecords[targetIndex]) insertion = next.indexOf(destinationRecords[targetIndex]);
|
|
27581
|
+
else if (destinationRecords.length) insertion = next.indexOf(destinationRecords.at(-1)) + 1;
|
|
27582
|
+
next.splice(insertion, 0, ...sources.map((source) => source.record));
|
|
27583
|
+
return {
|
|
27584
|
+
records: next,
|
|
27585
|
+
moves,
|
|
27586
|
+
to: {
|
|
27587
|
+
column: options.destination.column,
|
|
27588
|
+
lane: options.swimlaneBy == null ? null : options.destination.lane,
|
|
27589
|
+
index: targetIndex
|
|
27590
|
+
}
|
|
27591
|
+
};
|
|
27592
|
+
}
|
|
27593
|
+
function locateKanbanRecord(records, index, options) {
|
|
27594
|
+
const record = records[index];
|
|
27595
|
+
const column = normalizeAxisId(readKanbanAccessor(record, index, options.columnBy));
|
|
27596
|
+
const lane = options.swimlaneBy == null ? null : normalizeAxisId(readKanbanAccessor(record, index, options.swimlaneBy));
|
|
27597
|
+
const group = records.filter((candidate, candidateIndex) => normalizeAxisId(readKanbanAccessor(candidate, candidateIndex, options.columnBy)) === column && (options.swimlaneBy == null || normalizeAxisId(readKanbanAccessor(candidate, candidateIndex, options.swimlaneBy)) === lane));
|
|
27598
|
+
const id = viewRecordId(record, options.recordId);
|
|
27599
|
+
return {
|
|
27600
|
+
column,
|
|
27601
|
+
lane,
|
|
27602
|
+
index: group.findIndex((candidate) => Object.is(viewRecordId(candidate, options.recordId), id))
|
|
27603
|
+
};
|
|
27604
|
+
}
|
|
27605
|
+
function absoluteKanbanIndex(visibility, visibleIndex) {
|
|
27606
|
+
const target = clampKanbanIndex(visibleIndex, visibility.filter(Boolean).length);
|
|
27607
|
+
let seen = 0;
|
|
27608
|
+
for (let index = 0; index < visibility.length; index += 1) {
|
|
27609
|
+
if (!visibility[index]) continue;
|
|
27610
|
+
if (seen === target) return index;
|
|
27611
|
+
seen += 1;
|
|
27612
|
+
}
|
|
27613
|
+
return visibility.length;
|
|
27614
|
+
}
|
|
27615
|
+
function readKanbanAccessor(record, index, accessor) {
|
|
27616
|
+
return typeof accessor === "function" ? accessor(record, index) : accessor == null ? null : record?.[accessor];
|
|
27617
|
+
}
|
|
27618
|
+
function normalizeAxisId(value) {
|
|
27619
|
+
return value == null ? "" : String(value);
|
|
27620
|
+
}
|
|
27621
|
+
function clampKanbanIndex(value, maximum) {
|
|
27622
|
+
if (value == null || !Number.isFinite(Number(value))) return maximum;
|
|
27623
|
+
return Math.max(0, Math.min(maximum, Math.trunc(Number(value))));
|
|
27624
|
+
}
|
|
27625
|
+
function allowsOrigin(allowed, origin) {
|
|
27626
|
+
if (allowed == null) return true;
|
|
27627
|
+
if (!Array.isArray(allowed)) return true;
|
|
27628
|
+
if (!allowed.length) return false;
|
|
27629
|
+
return allowed.some((entry) => String(entry) === "*" || String(entry) === normalizeAxisId(origin));
|
|
27630
|
+
}
|
|
27631
|
+
function readLaneLimit(laneLimits, lane) {
|
|
27632
|
+
if (!laneLimits || typeof laneLimits !== "object") return null;
|
|
27633
|
+
const value = (
|
|
27634
|
+
/** @type {Record<string, unknown>} */
|
|
27635
|
+
laneLimits[lane]
|
|
27636
|
+
);
|
|
27637
|
+
if (value == null || value === "") return null;
|
|
27638
|
+
const number2 = Number(value);
|
|
27639
|
+
return Number.isFinite(number2) && number2 >= 0 ? Math.floor(number2) : null;
|
|
27640
|
+
}
|
|
27641
|
+
|
|
27344
27642
|
// src/components/kanban-view/kanban-view.js
|
|
27345
27643
|
var INTERACTIVE = 'a, button, input, select, textarea, summary, [contenteditable="true"]';
|
|
27644
|
+
var DRAG_THRESHOLD = 4;
|
|
27645
|
+
var TOUCH_HOLD = 350;
|
|
27646
|
+
var SEARCH_DEBOUNCE = 120;
|
|
27647
|
+
var SCROLL_EDGE = 56;
|
|
27648
|
+
var SCROLL_SPEED = 18;
|
|
27649
|
+
var DEFAULT_LABELS = Object.freeze({
|
|
27650
|
+
fields: "Fields",
|
|
27651
|
+
search: "Search board",
|
|
27652
|
+
searchPlaceholder: "Search cards",
|
|
27653
|
+
clearSearch: "Clear search",
|
|
27654
|
+
searchSummary: "%count% of %total% cards match.",
|
|
27655
|
+
undo: "Undo",
|
|
27656
|
+
redo: "Redo",
|
|
27657
|
+
add: "Add card",
|
|
27658
|
+
addTo: "Add card to %target%",
|
|
27659
|
+
move: "Move %name%",
|
|
27660
|
+
moveMultiple: "Move %count% selected cards",
|
|
27661
|
+
collapseColumn: "Collapse column %label%",
|
|
27662
|
+
expandColumn: "Expand column %label%",
|
|
27663
|
+
collapseLane: "Collapse swim lane %label%",
|
|
27664
|
+
expandLane: "Expand swim lane %label%",
|
|
27665
|
+
count: "%count% records",
|
|
27666
|
+
countFiltered: "%count% of %total% records",
|
|
27667
|
+
countLimit: "%count% records, work-in-progress limit %limit%",
|
|
27668
|
+
countLimitInLane: "%count% records in this lane, %total% of work-in-progress limit %limit% in the column",
|
|
27669
|
+
empty: "No records",
|
|
27670
|
+
columnEmpty: "",
|
|
27671
|
+
selected: "Selected",
|
|
27672
|
+
notSelected: "Not selected",
|
|
27673
|
+
noMatches: "No cards match the current search.",
|
|
27674
|
+
record: "record",
|
|
27675
|
+
grabbed: "Grabbed %name%. Use left and right for columns, up and down for position, and Alt plus up or down for swim lanes.",
|
|
27676
|
+
grabbedMultiple: "Grabbed %count% cards. Use left and right for columns, up and down for position, and Alt plus up or down for swim lanes.",
|
|
27677
|
+
target: "Target %target%, position %position% of %total%.",
|
|
27678
|
+
moved: "Moved %name% to %target%.%wip%",
|
|
27679
|
+
movedMultiple: "Moved %count% cards to %target%.%wip%",
|
|
27680
|
+
moveRequested: "Move requested for %name% to %target%.%wip%",
|
|
27681
|
+
moveCanceled: "Move canceled for %name%.",
|
|
27682
|
+
moveCanceledPlain: "Move canceled.",
|
|
27683
|
+
limitReached: " Work-in-progress limit %limit% reached.",
|
|
27684
|
+
limitExceeded: " Work-in-progress limit %limit% exceeded.",
|
|
27685
|
+
rejectAccept: "Move rejected: %target% does not accept %name%.",
|
|
27686
|
+
rejectTransition: "Move rejected: %target% does not accept cards from %origin%.",
|
|
27687
|
+
rejectWip: "Move rejected: %target% is at its work-in-progress limit of %limit%.",
|
|
27688
|
+
rejectDestination: "Move rejected: the destination is not available.",
|
|
27689
|
+
rejectGrouping: "Move rejected: callback grouping requires external move mode.",
|
|
27690
|
+
undone: "Move undone.",
|
|
27691
|
+
redone: "Move redone.",
|
|
27692
|
+
nothingToUndo: "Nothing to undo.",
|
|
27693
|
+
nothingToRedo: "Nothing to redo.",
|
|
27694
|
+
menuMoveTo: "Move to %target%",
|
|
27695
|
+
menuMoveToLane: "Move to lane %target%",
|
|
27696
|
+
dragPreview: "%count% cards"
|
|
27697
|
+
});
|
|
27346
27698
|
var KanbanView = class extends RecordView {
|
|
27347
27699
|
static cssName = "kanban-view";
|
|
27348
27700
|
/** @type {Readonly<KanbanViewOptions & RecordViewOptions>} */
|
|
@@ -27359,6 +27711,27 @@ var KanbanView = class extends RecordView {
|
|
|
27359
27711
|
link: null,
|
|
27360
27712
|
actions: [],
|
|
27361
27713
|
moveMode: "local",
|
|
27714
|
+
dragFrom: "card",
|
|
27715
|
+
multiMove: true,
|
|
27716
|
+
autoScroll: true,
|
|
27717
|
+
wipPolicy: "warn",
|
|
27718
|
+
rules: [],
|
|
27719
|
+
search: "",
|
|
27720
|
+
searchFields: null,
|
|
27721
|
+
filter: null,
|
|
27722
|
+
searchControl: false,
|
|
27723
|
+
history: true,
|
|
27724
|
+
historyLimit: 50,
|
|
27725
|
+
historyControls: false,
|
|
27726
|
+
columnHeight: null,
|
|
27727
|
+
allowAdd: false,
|
|
27728
|
+
contextMenu: false,
|
|
27729
|
+
renderCard: null,
|
|
27730
|
+
renderColumnHeader: null,
|
|
27731
|
+
renderSwimlaneHeader: null,
|
|
27732
|
+
renderColumnEmpty: null,
|
|
27733
|
+
renderDragPreview: null,
|
|
27734
|
+
labels: {},
|
|
27362
27735
|
columnOrder: [],
|
|
27363
27736
|
swimlaneOrder: [],
|
|
27364
27737
|
collapsedColumns: [],
|
|
@@ -27394,17 +27767,45 @@ var KanbanView = class extends RecordView {
|
|
|
27394
27767
|
this._kanbanSwimlaneOrder = normalizeOrder(options.swimlaneOrder);
|
|
27395
27768
|
this._collapsedColumns = new Set(normalizeOrder(options.collapsedColumns));
|
|
27396
27769
|
this._collapsedSwimlanes = new Set(normalizeOrder(options.collapsedSwimlanes));
|
|
27770
|
+
this._kanbanRules = normalizeKanbanRules(options.rules);
|
|
27771
|
+
this._kanbanSearch = options.search == null ? "" : String(options.search);
|
|
27772
|
+
this._kanbanFilter = typeof options.filter === "function" ? options.filter : null;
|
|
27773
|
+
this._kanbanHistory = createKanbanHistory(options.historyLimit);
|
|
27397
27774
|
this._keyboardMove = /** @type {KanbanKeyboardMove|null} */
|
|
27398
27775
|
null;
|
|
27399
|
-
this.
|
|
27776
|
+
this._dragIds = /** @type {unknown[]} */
|
|
27777
|
+
[];
|
|
27400
27778
|
this._dragDestination = /** @type {KanbanMovePoint|null} */
|
|
27401
27779
|
null;
|
|
27780
|
+
this._pointerStart = /** @type {{x:number,y:number,id:number,ids:unknown[],touch:boolean}|null} */
|
|
27781
|
+
null;
|
|
27782
|
+
this._pointerHold = 0;
|
|
27783
|
+
this._dragFrame = 0;
|
|
27784
|
+
this._dragPreview = /** @type {HTMLElement|null} */
|
|
27785
|
+
null;
|
|
27786
|
+
this._dragPointer = /** @type {number|null} */
|
|
27787
|
+
null;
|
|
27788
|
+
this._dragCaptured = false;
|
|
27789
|
+
this._searchTimer = 0;
|
|
27790
|
+
this._dragPoint = { x: 0, y: 0 };
|
|
27791
|
+
this._suppressClick = false;
|
|
27402
27792
|
this._announcementToken = 0;
|
|
27403
27793
|
this._cardMeta = /* @__PURE__ */ new WeakMap();
|
|
27404
27794
|
this._sectionMeta = /* @__PURE__ */ new WeakMap();
|
|
27405
27795
|
this._cardsById = /* @__PURE__ */ new Map();
|
|
27796
|
+
this._visibleIds = /* @__PURE__ */ new Set();
|
|
27797
|
+
this._kanbanMenu = /** @type {ContextMenu|null} */
|
|
27798
|
+
null;
|
|
27799
|
+
this._searchInput = /** @type {HTMLInputElement|null} */
|
|
27800
|
+
null;
|
|
27801
|
+
this._undoButton = /** @type {HTMLButtonElement|null} */
|
|
27802
|
+
null;
|
|
27803
|
+
this._redoButton = /** @type {HTMLButtonElement|null} */
|
|
27804
|
+
null;
|
|
27406
27805
|
this._toolbar = h("div", { class: "zx-record-view__toolbar zx-kanban-view__toolbar" });
|
|
27407
|
-
if (options.fieldControls) this._toolbar.append(this._createViewFieldControls());
|
|
27806
|
+
if (options.fieldControls) this._toolbar.append(this._createViewFieldControls(this._text("fields")));
|
|
27807
|
+
if (options.searchControl) this._toolbar.append(this._createSearchControl());
|
|
27808
|
+
if (options.historyControls) this._toolbar.append(this._createHistoryControls());
|
|
27408
27809
|
this._board = h("div", {
|
|
27409
27810
|
class: "zx-kanban-view__board",
|
|
27410
27811
|
role: "region",
|
|
@@ -27417,6 +27818,10 @@ var KanbanView = class extends RecordView {
|
|
|
27417
27818
|
ariaAtomic: "true"
|
|
27418
27819
|
});
|
|
27419
27820
|
root.replaceChildren(this._toolbar, this._board, this._live);
|
|
27821
|
+
if (options.columnHeight) {
|
|
27822
|
+
root.style.setProperty("--zx-kanban-column-height", String(options.columnHeight));
|
|
27823
|
+
root.dataset.columnScroll = "true";
|
|
27824
|
+
}
|
|
27420
27825
|
this.listen(this._board, "click", (event) => this._handleBoardClick(
|
|
27421
27826
|
/** @type {MouseEvent} */
|
|
27422
27827
|
event
|
|
@@ -27430,20 +27835,22 @@ var KanbanView = class extends RecordView {
|
|
|
27430
27835
|
event
|
|
27431
27836
|
));
|
|
27432
27837
|
this.listen(this._board, "error", (event) => this._handlePreviewError(event), { capture: true });
|
|
27433
|
-
this.listen(this._board, "
|
|
27434
|
-
/** @type {
|
|
27838
|
+
this.listen(this._board, "pointerdown", (event) => this._handlePointerDown(
|
|
27839
|
+
/** @type {PointerEvent} */
|
|
27435
27840
|
event
|
|
27436
27841
|
));
|
|
27437
|
-
this.listen(this._board, "
|
|
27438
|
-
/** @type {
|
|
27842
|
+
this.listen(this._board, "pointermove", (event) => this._handlePointerMove(
|
|
27843
|
+
/** @type {PointerEvent} */
|
|
27439
27844
|
event
|
|
27440
27845
|
));
|
|
27441
|
-
this.listen(this._board, "
|
|
27442
|
-
/** @type {
|
|
27846
|
+
this.listen(this._board, "pointerup", (event) => this._handlePointerUp(
|
|
27847
|
+
/** @type {PointerEvent} */
|
|
27443
27848
|
event
|
|
27444
27849
|
));
|
|
27445
|
-
this.listen(this._board, "
|
|
27850
|
+
this.listen(this._board, "pointercancel", () => this._clearDrag());
|
|
27851
|
+
this.listen(this._board, "dragstart", (event) => event.preventDefault());
|
|
27446
27852
|
this._refreshView("init");
|
|
27853
|
+
if (options.contextMenu) this._createContextMenu();
|
|
27447
27854
|
return root;
|
|
27448
27855
|
} catch (error2) {
|
|
27449
27856
|
if (this._kanbanSnapshot) restoreTarget(root, this._kanbanSnapshot);
|
|
@@ -27461,7 +27868,16 @@ var KanbanView = class extends RecordView {
|
|
|
27461
27868
|
*/
|
|
27462
27869
|
setColumns(columns) {
|
|
27463
27870
|
this._kanbanColumns = columns === null ? null : normalizeKanbanDescriptors(columns, "column");
|
|
27464
|
-
|
|
27871
|
+
const descriptors = this._rawColumns();
|
|
27872
|
+
this._kanbanColumnOrder = reconcileKanbanPreference(
|
|
27873
|
+
descriptors,
|
|
27874
|
+
this._kanbanColumnOrder,
|
|
27875
|
+
this._kanbanColumns === null
|
|
27876
|
+
);
|
|
27877
|
+
if (this._kanbanColumns !== null) {
|
|
27878
|
+
const ids = new Set(descriptors.map((column) => column.id));
|
|
27879
|
+
this._collapsedColumns = new Set([...this._collapsedColumns].filter((id) => ids.has(id)));
|
|
27880
|
+
}
|
|
27465
27881
|
this._refreshView("columns");
|
|
27466
27882
|
this._emitKanbanState("columns");
|
|
27467
27883
|
return this;
|
|
@@ -27477,7 +27893,17 @@ var KanbanView = class extends RecordView {
|
|
|
27477
27893
|
*/
|
|
27478
27894
|
setSwimlanes(lanes) {
|
|
27479
27895
|
this._kanbanSwimlanes = lanes === null ? null : normalizeKanbanDescriptors(lanes, "swimlane");
|
|
27480
|
-
|
|
27896
|
+
const descriptors = this._rawSwimlanes();
|
|
27897
|
+
const preserveUnknown = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
27898
|
+
this._kanbanSwimlaneOrder = reconcileKanbanPreference(
|
|
27899
|
+
descriptors,
|
|
27900
|
+
this._kanbanSwimlaneOrder,
|
|
27901
|
+
preserveUnknown
|
|
27902
|
+
);
|
|
27903
|
+
if (!preserveUnknown) {
|
|
27904
|
+
const ids = new Set(descriptors.map((lane) => lane.id));
|
|
27905
|
+
this._collapsedSwimlanes = new Set([...this._collapsedSwimlanes].filter((id) => ids.has(id)));
|
|
27906
|
+
}
|
|
27481
27907
|
this._refreshView("swimlanes");
|
|
27482
27908
|
this._emitKanbanState("swimlanes");
|
|
27483
27909
|
return this;
|
|
@@ -27493,9 +27919,16 @@ var KanbanView = class extends RecordView {
|
|
|
27493
27919
|
* @returns {this}
|
|
27494
27920
|
*/
|
|
27495
27921
|
setColumnOrder(order, options = {}) {
|
|
27496
|
-
const
|
|
27497
|
-
|
|
27498
|
-
|
|
27922
|
+
const descriptors = this._rawColumns();
|
|
27923
|
+
const current = this.getColumnOrder();
|
|
27924
|
+
const next = reconcileKanbanOrder(descriptors, order);
|
|
27925
|
+
if (sameArray3(next, current)) return this;
|
|
27926
|
+
this._kanbanColumnOrder = mergeKanbanResolvedOrder(
|
|
27927
|
+
this._kanbanColumnOrder,
|
|
27928
|
+
descriptors,
|
|
27929
|
+
next,
|
|
27930
|
+
this._kanbanColumns === null
|
|
27931
|
+
);
|
|
27499
27932
|
this._refreshView("columnorder");
|
|
27500
27933
|
if (!options.silent) this._emitKanbanState("columnorder");
|
|
27501
27934
|
return this;
|
|
@@ -27521,9 +27954,17 @@ var KanbanView = class extends RecordView {
|
|
|
27521
27954
|
* @returns {this}
|
|
27522
27955
|
*/
|
|
27523
27956
|
setSwimlaneOrder(order, options = {}) {
|
|
27524
|
-
const
|
|
27525
|
-
|
|
27526
|
-
|
|
27957
|
+
const descriptors = this._rawSwimlanes();
|
|
27958
|
+
const current = this.getSwimlaneOrder();
|
|
27959
|
+
const next = reconcileKanbanOrder(descriptors, order);
|
|
27960
|
+
if (sameArray3(next, current)) return this;
|
|
27961
|
+
const preserveUnknown = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
27962
|
+
this._kanbanSwimlaneOrder = mergeKanbanResolvedOrder(
|
|
27963
|
+
this._kanbanSwimlaneOrder,
|
|
27964
|
+
descriptors,
|
|
27965
|
+
next,
|
|
27966
|
+
preserveUnknown
|
|
27967
|
+
);
|
|
27527
27968
|
this._refreshView("swimlaneorder");
|
|
27528
27969
|
if (!options.silent) this._emitKanbanState("swimlaneorder");
|
|
27529
27970
|
return this;
|
|
@@ -27591,91 +28032,244 @@ var KanbanView = class extends RecordView {
|
|
|
27591
28032
|
return this.setSwimlaneCollapsed(id, !this._collapsedSwimlanes.has(String(id)));
|
|
27592
28033
|
}
|
|
27593
28034
|
/**
|
|
27594
|
-
* Proposes and, in local mode, commits a record move.
|
|
27595
|
-
* a cancelable `recordmove` event. WIP limits are reported but remain advisory.
|
|
28035
|
+
* Proposes and, in local mode, commits a record move. Equivalent to `moveRecords([id], …)`.
|
|
27596
28036
|
* @param {unknown} id Record id.
|
|
27597
28037
|
* @param {KanbanDestination} destination Destination.
|
|
27598
28038
|
* @returns {this}
|
|
27599
28039
|
* @fires KanbanView#recordmove
|
|
28040
|
+
* @fires KanbanView#movereject
|
|
27600
28041
|
* @fires RecordView#datachange
|
|
27601
28042
|
*/
|
|
27602
28043
|
moveRecord(id, destination) {
|
|
28044
|
+
return this.moveRecords([id], destination);
|
|
28045
|
+
}
|
|
28046
|
+
/**
|
|
28047
|
+
* Proposes and, in local mode, commits an atomic move of one or more records. Destination
|
|
28048
|
+
* eligibility, transition rules, and work-in-progress policy are all checked before a single
|
|
28049
|
+
* cancelable `recordmove` event; the move commits completely or not at all, and the moving
|
|
28050
|
+
* records keep their relative order at the destination.
|
|
28051
|
+
* @param {unknown[]} ids Record ids, in any order.
|
|
28052
|
+
* @param {KanbanDestination} destination Destination.
|
|
28053
|
+
* @returns {this}
|
|
28054
|
+
* @fires KanbanView#recordmove
|
|
28055
|
+
* @fires KanbanView#movereject
|
|
28056
|
+
* @fires RecordView#datachange
|
|
28057
|
+
*/
|
|
28058
|
+
moveRecords(ids, destination) {
|
|
27603
28059
|
const options = this._kanbanOptions();
|
|
27604
|
-
|
|
27605
|
-
const
|
|
27606
|
-
if (!
|
|
28060
|
+
if (!Array.isArray(ids) || !destination || typeof destination !== "object") return this;
|
|
28061
|
+
const moving = this._orderedIds(ids);
|
|
28062
|
+
if (!moving.length) return this;
|
|
28063
|
+
const records = moving.map((id) => (
|
|
28064
|
+
/** @type {KanbanRecord} */
|
|
28065
|
+
this.getRecord(id)
|
|
28066
|
+
));
|
|
28067
|
+
const origins = moving.map((id) => (
|
|
28068
|
+
/** @type {KanbanMovePoint} */
|
|
28069
|
+
this._recordLocation(id)
|
|
28070
|
+
));
|
|
28071
|
+
const from = origins[0];
|
|
27607
28072
|
const columns = this._resolvedColumns();
|
|
27608
28073
|
const lanes = this._resolvedSwimlanes();
|
|
27609
28074
|
const columnId = destination.column == null ? from.column : String(destination.column);
|
|
27610
28075
|
const laneId = options.swimlaneBy == null ? null : destination.lane === void 0 ? from.lane : normalizeAxisId(destination.lane);
|
|
27611
28076
|
const column = columns.find((candidate) => candidate.id === columnId);
|
|
27612
|
-
const lane = options.swimlaneBy == null ? null : lanes.find((candidate) => candidate.id === laneId);
|
|
28077
|
+
const lane = options.swimlaneBy == null ? null : lanes.find((candidate) => candidate.id === laneId) ?? null;
|
|
27613
28078
|
if (!column || options.swimlaneBy != null && !lane) {
|
|
27614
|
-
this.
|
|
28079
|
+
this._rejectMove("destination", { ids: moving, records, from, to: null, column: null, swimlane: null });
|
|
27615
28080
|
return this;
|
|
27616
28081
|
}
|
|
27617
|
-
const
|
|
27618
|
-
const
|
|
27619
|
-
const index =
|
|
28082
|
+
const excluded = new Set(moving);
|
|
28083
|
+
const cellCount = this._recordsIn(columnId, laneId).filter((entry) => !excluded.has(this._viewRecordId(entry.record))).length;
|
|
28084
|
+
const index = clampKanbanIndex(destination.index, cellCount);
|
|
27620
28085
|
const to = { column: columnId, lane: laneId, index };
|
|
27621
|
-
if (sameMovePoint(from, to)) return this;
|
|
27622
|
-
const context = {
|
|
27623
|
-
|
|
27624
|
-
|
|
28086
|
+
if (moving.length === 1 && sameMovePoint(from, to)) return this;
|
|
28087
|
+
const context = {
|
|
28088
|
+
id: moving[0],
|
|
28089
|
+
from: { ...from },
|
|
28090
|
+
to: { ...to },
|
|
28091
|
+
column: { ...column },
|
|
28092
|
+
swimlane: lane ? { ...lane } : null
|
|
28093
|
+
};
|
|
28094
|
+
const evaluation = this._evaluateMove(records, origins, column, lane, to, context, excluded);
|
|
28095
|
+
if (!evaluation.allowed) {
|
|
28096
|
+
this._rejectMove(
|
|
28097
|
+
/** @type {KanbanRejectReason} */
|
|
28098
|
+
evaluation.reason,
|
|
28099
|
+
{
|
|
28100
|
+
ids: moving,
|
|
28101
|
+
records,
|
|
28102
|
+
from,
|
|
28103
|
+
to,
|
|
28104
|
+
column,
|
|
28105
|
+
swimlane: lane,
|
|
28106
|
+
evaluation
|
|
28107
|
+
}
|
|
28108
|
+
);
|
|
27625
28109
|
return this;
|
|
27626
28110
|
}
|
|
27627
|
-
|
|
27628
|
-
|
|
27629
|
-
|
|
27630
|
-
|
|
27631
|
-
|
|
28111
|
+
const entries = [];
|
|
28112
|
+
for (let position2 = 0; position2 < moving.length; position2 += 1) {
|
|
28113
|
+
const record = records[position2];
|
|
28114
|
+
const moved = options.moveMode === "local" ? this._localMovedRecord(record, origins[position2], to, column, lane) : record;
|
|
28115
|
+
if (!moved) {
|
|
28116
|
+
this._rejectMove("grouping", { ids: moving, records, from, to, column, swimlane: lane });
|
|
27632
28117
|
return this;
|
|
27633
28118
|
}
|
|
28119
|
+
entries.push({ id: moving[position2], record: moved });
|
|
27634
28120
|
}
|
|
27635
|
-
const next =
|
|
28121
|
+
const next = reorderKanbanRecords(this._viewData, entries, {
|
|
27636
28122
|
recordId: options.recordId,
|
|
27637
28123
|
columnBy: (candidate, candidateIndex) => this._readAxis(options.columnBy, candidate, candidateIndex),
|
|
27638
28124
|
swimlaneBy: options.swimlaneBy == null ? null : (candidate, candidateIndex) => this._readAxis(options.swimlaneBy, candidate, candidateIndex),
|
|
27639
28125
|
destination: to
|
|
27640
28126
|
});
|
|
27641
28127
|
if (!next) {
|
|
27642
|
-
this.
|
|
28128
|
+
this._rejectMove("destination", { ids: moving, records, from, to, column, swimlane: lane });
|
|
27643
28129
|
return this;
|
|
27644
28130
|
}
|
|
27645
|
-
const
|
|
27646
|
-
const limitExceeded = column.limit != null && finalCount > column.limit;
|
|
28131
|
+
const wip = this._wipAnnouncement(evaluation);
|
|
27647
28132
|
const moveEvent = this.emit("recordmove", {
|
|
27648
|
-
record,
|
|
27649
|
-
id,
|
|
28133
|
+
record: records[0],
|
|
28134
|
+
id: moving[0],
|
|
28135
|
+
records: [...records],
|
|
28136
|
+
ids: [...moving],
|
|
28137
|
+
moves: next.moves.map((move) => ({ id: move.id, from: { ...move.from } })),
|
|
27650
28138
|
from: { ...from },
|
|
27651
28139
|
to: { ...next.to },
|
|
27652
28140
|
column: { ...column },
|
|
27653
28141
|
swimlane: lane ? { ...lane } : null,
|
|
27654
|
-
limitExceeded
|
|
27655
|
-
});
|
|
28142
|
+
limitExceeded: evaluation.limitExceeded
|
|
28143
|
+
}, { honorDomCancellation: true });
|
|
27656
28144
|
if (moveEvent.defaultPrevented) {
|
|
27657
|
-
this._announce(
|
|
28145
|
+
this._announce(this._text("moveCanceled", { name: this._movingName(records) }));
|
|
27658
28146
|
return this;
|
|
27659
28147
|
}
|
|
27660
28148
|
if (options.moveMode === "external") {
|
|
27661
|
-
this._announce(
|
|
28149
|
+
this._announce(this._text("moveRequested", {
|
|
28150
|
+
name: this._movingName(records),
|
|
28151
|
+
target: this._targetName(next.to),
|
|
28152
|
+
wip
|
|
28153
|
+
}));
|
|
27662
28154
|
return this;
|
|
27663
28155
|
}
|
|
27664
|
-
this.
|
|
27665
|
-
this.
|
|
27666
|
-
|
|
27667
|
-
|
|
27668
|
-
|
|
28156
|
+
this._commitLocalRecords(next.records);
|
|
28157
|
+
this._announce(moving.length > 1 ? this._text("movedMultiple", { count: moving.length, target: this._targetName(next.to), wip }) : this._text("moved", {
|
|
28158
|
+
name: this._recordName(entries[0].record, next.to.index),
|
|
28159
|
+
target: this._targetName(next.to),
|
|
28160
|
+
wip
|
|
28161
|
+
}));
|
|
28162
|
+
queueMicrotask(() => this._moveHandle(moving[0])?.focus());
|
|
28163
|
+
return this;
|
|
28164
|
+
}
|
|
28165
|
+
/** Whether a committed local move can be reverted. @returns {boolean} */
|
|
28166
|
+
canUndo() {
|
|
28167
|
+
return this._kanbanOptions().history !== false && this._kanbanHistory.canUndo();
|
|
28168
|
+
}
|
|
28169
|
+
/** Whether a reverted local move can be reapplied. @returns {boolean} */
|
|
28170
|
+
canRedo() {
|
|
28171
|
+
return this._kanbanOptions().history !== false && this._kanbanHistory.canRedo();
|
|
28172
|
+
}
|
|
28173
|
+
/**
|
|
28174
|
+
* Reverts the last committed local move, restoring the exact record order and selection.
|
|
28175
|
+
* @returns {this}
|
|
28176
|
+
* @fires RecordView#datachange
|
|
28177
|
+
* @fires KanbanView#historychange
|
|
28178
|
+
*/
|
|
28179
|
+
undo() {
|
|
28180
|
+
return this._travelHistory("undo");
|
|
28181
|
+
}
|
|
28182
|
+
/**
|
|
28183
|
+
* Reapplies the last reverted local move.
|
|
28184
|
+
* @returns {this}
|
|
28185
|
+
* @fires RecordView#datachange
|
|
28186
|
+
* @fires KanbanView#historychange
|
|
28187
|
+
*/
|
|
28188
|
+
redo() {
|
|
28189
|
+
return this._travelHistory("redo");
|
|
28190
|
+
}
|
|
28191
|
+
/** Empties both history stacks. @returns {this} @fires KanbanView#historychange */
|
|
28192
|
+
clearHistory() {
|
|
28193
|
+
this._kanbanHistory.clear();
|
|
28194
|
+
this._syncHistoryControls();
|
|
28195
|
+
this._emitHistory();
|
|
28196
|
+
return this;
|
|
28197
|
+
}
|
|
28198
|
+
/** Returns the current board search text. @returns {string} */
|
|
28199
|
+
getSearch() {
|
|
28200
|
+
return this._kanbanSearch;
|
|
28201
|
+
}
|
|
28202
|
+
/**
|
|
28203
|
+
* Narrows visible cards to those matching a search. Hidden records keep their position in the
|
|
28204
|
+
* data, so a card dropped between two visible cards still lands between them.
|
|
28205
|
+
* @param {string} search Search text.
|
|
28206
|
+
* @param {{silent?:boolean}} [options={}] Event behavior.
|
|
28207
|
+
* @returns {this}
|
|
28208
|
+
* @fires KanbanView#searchchange
|
|
28209
|
+
*/
|
|
28210
|
+
setSearch(search, options = {}) {
|
|
28211
|
+
const next = search == null ? "" : String(search);
|
|
28212
|
+
if (next === this._kanbanSearch) return this;
|
|
28213
|
+
this._kanbanSearch = next;
|
|
28214
|
+
if (this._searchInput && this._searchInput.value !== next) this._searchInput.value = next;
|
|
28215
|
+
this._refreshView("search");
|
|
28216
|
+
if (!options.silent) this.emit("searchchange", { search: next });
|
|
28217
|
+
return this;
|
|
28218
|
+
}
|
|
28219
|
+
/**
|
|
28220
|
+
* Replaces the extra card predicate. Pass null to clear it.
|
|
28221
|
+
* @param {((record:KanbanRecord,index:number)=>boolean)|null} filter Predicate.
|
|
28222
|
+
* @returns {this}
|
|
28223
|
+
*/
|
|
28224
|
+
setFilter(filter) {
|
|
28225
|
+
if (filter != null && typeof filter !== "function") {
|
|
28226
|
+
throw new TypeError("Kanban filter must be a function or null");
|
|
28227
|
+
}
|
|
28228
|
+
this._kanbanFilter = filter ?? null;
|
|
28229
|
+
this._refreshView("filter");
|
|
27669
28230
|
return this;
|
|
27670
28231
|
}
|
|
28232
|
+
/** Returns the records currently rendered, in board order. @returns {KanbanRecord[]} */
|
|
28233
|
+
getVisibleRecords() {
|
|
28234
|
+
return this._viewData.filter((record) => this._visibleIds.has(this._viewRecordId(record)));
|
|
28235
|
+
}
|
|
28236
|
+
/**
|
|
28237
|
+
* Replaces every record. History is dropped because its snapshots describe records the host has
|
|
28238
|
+
* just replaced, and reinstating them would resurrect data the application no longer owns.
|
|
28239
|
+
* @param {KanbanRecord[]} records New records.
|
|
28240
|
+
* @returns {this}
|
|
28241
|
+
*/
|
|
28242
|
+
setData(records) {
|
|
28243
|
+
this._resetHistory();
|
|
28244
|
+
return super.setData(records);
|
|
28245
|
+
}
|
|
28246
|
+
/** @param {KanbanRecord[]} records Records to append. @returns {this} */
|
|
28247
|
+
addData(records) {
|
|
28248
|
+
this._resetHistory();
|
|
28249
|
+
return super.addData(records);
|
|
28250
|
+
}
|
|
28251
|
+
/** @param {unknown} id Existing record id. @param {KanbanRecord} record Replacement. @returns {this} */
|
|
28252
|
+
updateRecord(id, record) {
|
|
28253
|
+
this._resetHistory();
|
|
28254
|
+
return super.updateRecord(id, record);
|
|
28255
|
+
}
|
|
28256
|
+
/** @param {unknown} id Record id. @returns {this} */
|
|
28257
|
+
removeRecord(id) {
|
|
28258
|
+
this._resetHistory();
|
|
28259
|
+
return super.removeRecord(id);
|
|
28260
|
+
}
|
|
27671
28261
|
/** Returns common plus board-specific serializable configuration. @returns {KanbanViewState} */
|
|
27672
28262
|
getViewState() {
|
|
28263
|
+
this.getColumnOrder();
|
|
28264
|
+
this.getSwimlaneOrder();
|
|
28265
|
+
const derivedColumns = this._kanbanColumns === null;
|
|
28266
|
+
const derivedSwimlanes = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
27673
28267
|
return {
|
|
27674
28268
|
...super.getViewState(),
|
|
27675
|
-
columnOrder: this.getColumnOrder(),
|
|
27676
|
-
swimlaneOrder: this.getSwimlaneOrder(),
|
|
27677
|
-
collapsedColumns: this.getCollapsedColumns(),
|
|
27678
|
-
collapsedSwimlanes: this.getCollapsedSwimlanes()
|
|
28269
|
+
columnOrder: derivedColumns ? [...this._kanbanColumnOrder] : this.getColumnOrder(),
|
|
28270
|
+
swimlaneOrder: derivedSwimlanes ? [...this._kanbanSwimlaneOrder] : this.getSwimlaneOrder(),
|
|
28271
|
+
collapsedColumns: derivedColumns ? [...this._collapsedColumns] : this.getCollapsedColumns(),
|
|
28272
|
+
collapsedSwimlanes: derivedSwimlanes ? [...this._collapsedSwimlanes] : this.getCollapsedSwimlanes()
|
|
27679
28273
|
};
|
|
27680
28274
|
}
|
|
27681
28275
|
/**
|
|
@@ -27687,21 +28281,29 @@ var KanbanView = class extends RecordView {
|
|
|
27687
28281
|
setViewState(state, options = {}) {
|
|
27688
28282
|
const source = state && typeof state === "object" ? state : {};
|
|
27689
28283
|
super.setViewState(source, { silent: true });
|
|
27690
|
-
|
|
27691
|
-
|
|
27692
|
-
|
|
27693
|
-
);
|
|
27694
|
-
this.
|
|
27695
|
-
|
|
27696
|
-
Array.isArray(source.
|
|
27697
|
-
|
|
27698
|
-
|
|
27699
|
-
|
|
28284
|
+
const columns = this._rawColumns();
|
|
28285
|
+
const lanes = this._rawSwimlanes();
|
|
28286
|
+
const derivedColumns = this._kanbanColumns === null;
|
|
28287
|
+
const derivedSwimlanes = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
28288
|
+
this._kanbanColumnOrder = reconcileKanbanPreference(
|
|
28289
|
+
columns,
|
|
28290
|
+
Array.isArray(source.columnOrder) ? source.columnOrder : this._kanbanColumnOrder,
|
|
28291
|
+
derivedColumns
|
|
28292
|
+
);
|
|
28293
|
+
this._kanbanSwimlaneOrder = reconcileKanbanPreference(
|
|
28294
|
+
lanes,
|
|
28295
|
+
Array.isArray(source.swimlaneOrder) ? source.swimlaneOrder : this._kanbanSwimlaneOrder,
|
|
28296
|
+
derivedSwimlanes
|
|
28297
|
+
);
|
|
28298
|
+
const columnIds = new Set(columns.map((column) => column.id));
|
|
28299
|
+
const laneIds = new Set(lanes.map((lane) => lane.id));
|
|
27700
28300
|
if (Array.isArray(source.collapsedColumns)) {
|
|
27701
|
-
|
|
28301
|
+
const collapsed = normalizeOrder(source.collapsedColumns);
|
|
28302
|
+
this._collapsedColumns = new Set(derivedColumns ? collapsed : collapsed.filter((id) => columnIds.has(id)));
|
|
27702
28303
|
}
|
|
27703
28304
|
if (Array.isArray(source.collapsedSwimlanes)) {
|
|
27704
|
-
|
|
28305
|
+
const collapsed = normalizeOrder(source.collapsedSwimlanes);
|
|
28306
|
+
this._collapsedSwimlanes = new Set(derivedSwimlanes ? collapsed : collapsed.filter((id) => laneIds.has(id)));
|
|
27705
28307
|
}
|
|
27706
28308
|
this._refreshView("state");
|
|
27707
28309
|
if (!options.silent) this._emitKanbanState("restore");
|
|
@@ -27711,6 +28313,11 @@ var KanbanView = class extends RecordView {
|
|
|
27711
28313
|
destroy() {
|
|
27712
28314
|
if (this._kanbanDestroyed) return;
|
|
27713
28315
|
this._kanbanDestroyed = true;
|
|
28316
|
+
this._clearDrag();
|
|
28317
|
+
clearTimeout(this._pointerHold);
|
|
28318
|
+
clearTimeout(this._searchTimer);
|
|
28319
|
+
this._kanbanMenu?.destroy();
|
|
28320
|
+
this._kanbanMenu = null;
|
|
27714
28321
|
const root = this.el;
|
|
27715
28322
|
const snapshot10 = this._kanbanSnapshot;
|
|
27716
28323
|
super.destroy();
|
|
@@ -27725,25 +28332,61 @@ var KanbanView = class extends RecordView {
|
|
|
27725
28332
|
}
|
|
27726
28333
|
if (_reason === "loading") return;
|
|
27727
28334
|
const options = this._kanbanOptions();
|
|
28335
|
+
const activeElement = document.activeElement;
|
|
27728
28336
|
const activeMeta = this._metaForTarget(document.activeElement);
|
|
27729
28337
|
const restoreHandle = Boolean(document.activeElement?.classList?.contains("zx-kanban-view__move-handle"));
|
|
28338
|
+
const activeCollapse = activeElement instanceof Element ? activeElement.closest("[data-kanban-collapse]") : null;
|
|
28339
|
+
const activeSection = activeCollapse?.closest(".zx-kanban-view__column");
|
|
28340
|
+
const activeCollapseFocus = activeCollapse && this._board.contains(activeCollapse) ? {
|
|
28341
|
+
axis: (
|
|
28342
|
+
/** @type {'column'|'swimlane'} */
|
|
28343
|
+
/** @type {HTMLElement} */
|
|
28344
|
+
activeCollapse.dataset.kanbanCollapse
|
|
28345
|
+
),
|
|
28346
|
+
id: String(
|
|
28347
|
+
/** @type {HTMLElement} */
|
|
28348
|
+
activeCollapse.dataset.kanbanId
|
|
28349
|
+
),
|
|
28350
|
+
lane: activeSection ? this._sectionMeta.get(activeSection)?.lane ?? null : null
|
|
28351
|
+
} : null;
|
|
27730
28352
|
const columns = this._resolvedColumns();
|
|
27731
28353
|
const lanes = this._resolvedSwimlanes();
|
|
27732
28354
|
this._cardMeta = /* @__PURE__ */ new WeakMap();
|
|
27733
28355
|
this._sectionMeta = /* @__PURE__ */ new WeakMap();
|
|
27734
28356
|
this._cardsById = /* @__PURE__ */ new Map();
|
|
28357
|
+
this._resolveVisibility();
|
|
27735
28358
|
const content = [];
|
|
27736
28359
|
if (options.swimlaneBy == null) {
|
|
27737
28360
|
content.push(this._renderColumns(columns, null, false));
|
|
27738
28361
|
} else {
|
|
27739
28362
|
lanes.forEach((lane) => content.push(this._renderLane(lane, columns)));
|
|
27740
28363
|
}
|
|
27741
|
-
|
|
27742
|
-
if (
|
|
28364
|
+
if (!this._viewData.length) content.push(this._emptyContent(this._text("empty")));
|
|
28365
|
+
else if (!this._visibleIds.size) content.push(this._emptyContent(this._text("noMatches")));
|
|
27743
28366
|
this._board.replaceChildren(...content);
|
|
27744
|
-
if (this._keyboardMove
|
|
28367
|
+
if (this._keyboardMove) {
|
|
28368
|
+
this._keyboardMove.ids = this._keyboardMove.ids.filter((id) => this.getRecord(id));
|
|
28369
|
+
if (!this._keyboardMove.ids.length) this._keyboardMove = null;
|
|
28370
|
+
}
|
|
27745
28371
|
this._syncKeyboardTarget();
|
|
27746
|
-
if (activeMeta) queueMicrotask(() => {
|
|
28372
|
+
if (activeCollapseFocus || activeMeta) queueMicrotask(() => {
|
|
28373
|
+
if (activeCollapseFocus) {
|
|
28374
|
+
this._collapseControl(
|
|
28375
|
+
activeCollapseFocus.axis,
|
|
28376
|
+
activeCollapseFocus.id,
|
|
28377
|
+
activeCollapseFocus.lane
|
|
28378
|
+
)?.focus();
|
|
28379
|
+
return;
|
|
28380
|
+
}
|
|
28381
|
+
if (!activeMeta) return;
|
|
28382
|
+
if (activeMeta.lane != null && this._collapsedSwimlanes.has(activeMeta.lane)) {
|
|
28383
|
+
this._collapseControl("swimlane", activeMeta.lane)?.focus();
|
|
28384
|
+
return;
|
|
28385
|
+
}
|
|
28386
|
+
if (this._collapsedColumns.has(activeMeta.column)) {
|
|
28387
|
+
this._collapseControl("column", activeMeta.column, activeMeta.lane)?.focus();
|
|
28388
|
+
return;
|
|
28389
|
+
}
|
|
27747
28390
|
const card = this._cardsById.get(activeMeta.id);
|
|
27748
28391
|
const target = restoreHandle ? card?.querySelector(".zx-kanban-view__move-handle") : card;
|
|
27749
28392
|
if (target?.isConnected) target.focus();
|
|
@@ -27755,21 +28398,36 @@ var KanbanView = class extends RecordView {
|
|
|
27755
28398
|
const collapsed = this._collapsedSwimlanes.has(lane.id);
|
|
27756
28399
|
const headingId = uid("zx-kanban-lane");
|
|
27757
28400
|
const bodyId = uid("zx-kanban-lane-body");
|
|
27758
|
-
const
|
|
28401
|
+
const entries = this._viewData.filter((record, index) => normalizeAxisId(this._readAxis(options.swimlaneBy, record, index)) === lane.id);
|
|
28402
|
+
const total = entries.length;
|
|
28403
|
+
const count2 = entries.filter((record) => this._visibleIds.has(this._viewRecordId(record))).length;
|
|
27759
28404
|
const toggle = h("button", {
|
|
27760
28405
|
class: "zx-kanban-view__collapse",
|
|
27761
28406
|
type: "button",
|
|
27762
28407
|
ariaExpanded: String(!collapsed),
|
|
27763
28408
|
ariaControls: bodyId,
|
|
27764
|
-
ariaLabel:
|
|
28409
|
+
ariaLabel: this._text(collapsed ? "expandLane" : "collapseLane", { label: lane.label }),
|
|
27765
28410
|
dataset: { kanbanCollapse: "swimlane", kanbanId: lane.id }
|
|
27766
28411
|
}, icon(collapsed ? "chevron-right" : "chevron-down", { size: 13 }));
|
|
28412
|
+
const limit = lane.limit ?? null;
|
|
28413
|
+
const wip = wipState(total, limit);
|
|
28414
|
+
const custom = this._renderHook("renderSwimlaneHeader", {
|
|
28415
|
+
swimlane: { ...lane },
|
|
28416
|
+
count: count2,
|
|
28417
|
+
total,
|
|
28418
|
+
columnTotal: total,
|
|
28419
|
+
limit,
|
|
28420
|
+
wip,
|
|
28421
|
+
collapsed
|
|
28422
|
+
});
|
|
27767
28423
|
const heading = h(
|
|
27768
28424
|
"h2",
|
|
27769
28425
|
{ class: "zx-kanban-view__lane-heading", id: headingId },
|
|
27770
28426
|
toggle,
|
|
27771
|
-
|
|
27772
|
-
|
|
28427
|
+
custom ?? [
|
|
28428
|
+
h("span", {}, lane.label),
|
|
28429
|
+
options.showCounts || limit != null ? this._countIndicator(count2, total, limit) : null
|
|
28430
|
+
]
|
|
27773
28431
|
);
|
|
27774
28432
|
const body = this._renderColumns(columns, lane, true);
|
|
27775
28433
|
body.id = bodyId;
|
|
@@ -27777,7 +28435,7 @@ var KanbanView = class extends RecordView {
|
|
|
27777
28435
|
return h("section", {
|
|
27778
28436
|
class: "zx-kanban-view__lane",
|
|
27779
28437
|
ariaLabelledby: headingId,
|
|
27780
|
-
dataset: { collapsed: String(collapsed) }
|
|
28438
|
+
dataset: { collapsed: String(collapsed), wip }
|
|
27781
28439
|
}, heading, body);
|
|
27782
28440
|
}
|
|
27783
28441
|
/** @param {KanbanColumn[]} columns @param {KanbanSwimlane|null} lane @param {boolean} hasLanes @returns {HTMLElement} */
|
|
@@ -27803,36 +28461,67 @@ var KanbanView = class extends RecordView {
|
|
|
27803
28461
|
const collapsed = this._collapsedColumns.has(column.id);
|
|
27804
28462
|
const headingId = uid("zx-kanban-column");
|
|
27805
28463
|
const listId = uid("zx-kanban-column-list");
|
|
27806
|
-
const
|
|
27807
|
-
const
|
|
28464
|
+
const columnEntries = this._viewData.filter((record, index) => normalizeAxisId(this._readAxis(options.columnBy, record, index)) === column.id);
|
|
28465
|
+
const columnTotal = columnEntries.length;
|
|
28466
|
+
const visible = entries.filter((entry) => this._visibleIds.has(this._viewRecordId(entry.record)));
|
|
28467
|
+
const laneLimit = lane ? readColumnLaneLimit(column, lane.id) : null;
|
|
28468
|
+
const limit = laneLimit ?? column.limit ?? null;
|
|
28469
|
+
const wip = wipState(laneLimit == null ? columnTotal : entries.length, limit);
|
|
27808
28470
|
const toggle = h("button", {
|
|
27809
28471
|
class: "zx-kanban-view__collapse",
|
|
27810
28472
|
type: "button",
|
|
27811
28473
|
ariaExpanded: String(!collapsed),
|
|
27812
28474
|
ariaControls: listId,
|
|
27813
|
-
ariaLabel:
|
|
28475
|
+
ariaLabel: this._text(collapsed ? "expandColumn" : "collapseColumn", { label: column.label }),
|
|
27814
28476
|
dataset: { kanbanCollapse: "column", kanbanId: column.id }
|
|
27815
28477
|
}, icon(collapsed ? "chevron-right" : "chevron-down", { size: 13 }));
|
|
27816
|
-
const
|
|
27817
|
-
|
|
27818
|
-
|
|
27819
|
-
|
|
28478
|
+
const custom = this._renderHook("renderColumnHeader", {
|
|
28479
|
+
column: { ...column },
|
|
28480
|
+
swimlane: lane ? { ...lane } : null,
|
|
28481
|
+
count: visible.length,
|
|
28482
|
+
total: entries.length,
|
|
28483
|
+
columnTotal,
|
|
28484
|
+
limit,
|
|
28485
|
+
wip,
|
|
28486
|
+
collapsed
|
|
28487
|
+
});
|
|
28488
|
+
const indicator = options.showCounts || limit != null ? this._columnIndicator(visible.length, entries.length, columnTotal, limit, hasLanes && laneLimit == null) : null;
|
|
27820
28489
|
const heading = h(hasLanes ? "h3" : "h2", {
|
|
27821
28490
|
class: "zx-kanban-view__column-heading",
|
|
27822
28491
|
id: headingId
|
|
27823
|
-
}, toggle, h("span", { class: "zx-kanban-view__column-label" }, column.label), indicator);
|
|
28492
|
+
}, toggle, custom ?? [h("span", { class: "zx-kanban-view__column-label" }, column.label), indicator]);
|
|
27824
28493
|
const list = h("ul", {
|
|
27825
28494
|
class: "zx-kanban-view__cards",
|
|
27826
28495
|
id: listId,
|
|
27827
28496
|
ariaLabel: `${column.label}${lane ? `, ${lane.label}` : ""}`
|
|
27828
28497
|
});
|
|
27829
|
-
|
|
28498
|
+
visible.forEach((entry) => list.append(this._renderCard(entry.record, entry.index, column.id, lane?.id ?? null, hasLanes)));
|
|
28499
|
+
if (!visible.length) {
|
|
28500
|
+
const placeholder = this._renderHook("renderColumnEmpty", {
|
|
28501
|
+
column: { ...column },
|
|
28502
|
+
swimlane: lane ? { ...lane } : null,
|
|
28503
|
+
count: 0,
|
|
28504
|
+
total: entries.length,
|
|
28505
|
+
columnTotal,
|
|
28506
|
+
limit,
|
|
28507
|
+
wip,
|
|
28508
|
+
collapsed
|
|
28509
|
+
}) ?? this._placeholderText();
|
|
28510
|
+
if (placeholder) list.append(h("li", { class: "zx-kanban-view__column-empty", role: "presentation" }, placeholder));
|
|
28511
|
+
}
|
|
27830
28512
|
list.hidden = collapsed;
|
|
28513
|
+
const add = this._allowsAdd(column, lane) ? h("button", {
|
|
28514
|
+
class: "zx-kanban-view__add",
|
|
28515
|
+
type: "button",
|
|
28516
|
+
ariaLabel: this._text("addTo", { target: `${column.label}${lane ? `, ${lane.label}` : ""}` }),
|
|
28517
|
+
dataset: { kanbanAdd: column.id, kanbanLane: lane?.id ?? "" }
|
|
28518
|
+
}, icon("plus", { size: 13 }), h("span", {}, this._text("add"))) : null;
|
|
28519
|
+
if (add) add.hidden = collapsed;
|
|
27831
28520
|
const section = h("section", {
|
|
27832
28521
|
class: "zx-kanban-view__column",
|
|
27833
28522
|
ariaLabelledby: headingId,
|
|
27834
28523
|
dataset: { column: column.id, collapsed: String(collapsed), wip }
|
|
27835
|
-
}, heading, list);
|
|
28524
|
+
}, heading, list, add);
|
|
27836
28525
|
this._sectionMeta.set(section, { column: column.id, lane: lane?.id ?? null, list });
|
|
27837
28526
|
return section;
|
|
27838
28527
|
}
|
|
@@ -27840,13 +28529,14 @@ var KanbanView = class extends RecordView {
|
|
|
27840
28529
|
_renderCard(record, index, column, lane, hasLanes) {
|
|
27841
28530
|
const options = this._kanbanOptions();
|
|
27842
28531
|
const id = this._viewRecordId(record);
|
|
27843
|
-
const grabbed = Boolean(this._keyboardMove
|
|
28532
|
+
const grabbed = Boolean(this._keyboardMove?.ids.some((candidate) => Object.is(candidate, id)));
|
|
28533
|
+
const rules = resolveKanbanRules(this._kanbanRules, record, { index, column, lane });
|
|
28534
|
+
const multiple = grabbed && this._keyboardMove ? this._keyboardMove.ids.length : 1;
|
|
27844
28535
|
const handle = h("button", {
|
|
27845
28536
|
class: "zx-kanban-view__move-handle",
|
|
27846
28537
|
type: "button",
|
|
27847
|
-
draggable: true,
|
|
27848
28538
|
ariaPressed: String(grabbed),
|
|
27849
|
-
ariaLabel:
|
|
28539
|
+
ariaLabel: multiple > 1 ? this._text("moveMultiple", { count: multiple }) : this._text("move", { name: this._recordName(record, index) })
|
|
27850
28540
|
}, icon("drag", { size: 13 }));
|
|
27851
28541
|
const card = createRecordCard(record, index, {
|
|
27852
28542
|
fields: this.getFields(),
|
|
@@ -27865,15 +28555,81 @@ var KanbanView = class extends RecordView {
|
|
|
27865
28555
|
});
|
|
27866
28556
|
card.tabIndex = 0;
|
|
27867
28557
|
if (grabbed) card.dataset.grabbed = "true";
|
|
28558
|
+
this._applyCardContent(card, { record, id, index, column, lane, rules, selected: this._isViewSelected(id) });
|
|
28559
|
+
this._applyCardRules(card, rules);
|
|
28560
|
+
this._applyCardDescription(card, id);
|
|
27868
28561
|
const meta = { record, id, index, column, lane };
|
|
27869
28562
|
this._cardMeta.set(card, meta);
|
|
27870
28563
|
this._cardsById.set(id, card);
|
|
27871
28564
|
return card;
|
|
27872
28565
|
}
|
|
27873
|
-
/**
|
|
27874
|
-
|
|
28566
|
+
/**
|
|
28567
|
+
* Hands card content to `renderCard` while keeping the managed shell: the list item, its
|
|
28568
|
+
* selection control, the action group, and the move handle are never given away, because they
|
|
28569
|
+
* carry the drop, focus, and movement semantics the board is responsible for.
|
|
28570
|
+
* @param {HTMLElement} card Card built from the shared anatomy.
|
|
28571
|
+
* @param {KanbanCardContext} context Card context.
|
|
28572
|
+
* @returns {void}
|
|
28573
|
+
*/
|
|
28574
|
+
_applyCardContent(card, context) {
|
|
28575
|
+
const custom = this._renderHook("renderCard", context);
|
|
28576
|
+
if (!custom) return;
|
|
28577
|
+
const body = card.querySelector(".zx-record-card__body");
|
|
28578
|
+
const titles = card.querySelector(".zx-record-card__titles");
|
|
28579
|
+
if (!body || !titles) return;
|
|
28580
|
+
titles.replaceChildren(custom);
|
|
28581
|
+
body.querySelector(".zx-record-card__eyebrow")?.remove();
|
|
28582
|
+
body.querySelector(".zx-record-card__metadata")?.remove();
|
|
28583
|
+
card.removeAttribute("aria-labelledby");
|
|
28584
|
+
card.setAttribute("aria-label", this._recordName(context.record, context.index));
|
|
28585
|
+
card.dataset.customCard = "true";
|
|
28586
|
+
}
|
|
28587
|
+
/**
|
|
28588
|
+
* Applies ordered card rules. The first match owns the marker tone; every match with a label adds
|
|
28589
|
+
* a badge, and rule descriptions join the accessible description so the marker is never the only
|
|
28590
|
+
* carrier of the meaning.
|
|
28591
|
+
* @param {HTMLElement} card Card element.
|
|
28592
|
+
* @param {KanbanRule[]} rules Matching rules.
|
|
28593
|
+
* @returns {void}
|
|
28594
|
+
*/
|
|
28595
|
+
_applyCardRules(card, rules) {
|
|
28596
|
+
if (!rules.length) return;
|
|
28597
|
+
card.dataset.rule = rules.map((rule) => rule.id).join(" ");
|
|
28598
|
+
card.dataset.ruleTone = rules[0].tone;
|
|
28599
|
+
const badges = rules.filter((rule) => rule.label);
|
|
28600
|
+
if (badges.length) {
|
|
28601
|
+
const list = h("p", { class: "zx-kanban-view__rules" }, badges.map((rule) => badge({
|
|
28602
|
+
label: rule.label,
|
|
28603
|
+
icon: rule.icon ?? void 0,
|
|
28604
|
+
kind: rule.tone,
|
|
28605
|
+
size: "sm",
|
|
28606
|
+
variant: "soft"
|
|
28607
|
+
})));
|
|
28608
|
+
card.querySelector(".zx-record-card__body")?.append(list);
|
|
28609
|
+
}
|
|
28610
|
+
const descriptions = rules.map((rule) => rule.description).filter(Boolean).join(". ");
|
|
28611
|
+
if (descriptions) card.dataset.ruleDescription = descriptions;
|
|
28612
|
+
}
|
|
28613
|
+
/**
|
|
28614
|
+
* Composes one card's accessible description from its selection state and its rules, so that
|
|
28615
|
+
* refreshing selection cannot quietly drop what a rule contributed.
|
|
28616
|
+
* @param {HTMLElement} card Card element.
|
|
28617
|
+
* @param {unknown} id Record id.
|
|
28618
|
+
* @returns {void}
|
|
28619
|
+
*/
|
|
28620
|
+
_applyCardDescription(card, id) {
|
|
28621
|
+
const parts = [];
|
|
28622
|
+
if (this.options.selectable) {
|
|
28623
|
+
parts.push(this._text(this._isViewSelected(id) ? "selected" : "notSelected"));
|
|
28624
|
+
}
|
|
28625
|
+
if (card.dataset.ruleDescription) parts.push(card.dataset.ruleDescription);
|
|
28626
|
+
if (parts.length) card.setAttribute("aria-description", parts.join(". "));
|
|
28627
|
+
else card.removeAttribute("aria-description");
|
|
28628
|
+
}
|
|
28629
|
+
/** @param {string} text Board-level empty message. @returns {HTMLElement} */
|
|
28630
|
+
_emptyContent(text4) {
|
|
27875
28631
|
const supplied = typeof this.options.emptyText === "function" ? this.options.emptyText() : this.options.emptyText;
|
|
27876
|
-
const content = supplied ??
|
|
28632
|
+
const content = supplied ?? text4;
|
|
27877
28633
|
return h(
|
|
27878
28634
|
"div",
|
|
27879
28635
|
{ class: "zx-kanban-view__empty", role: "status" },
|
|
@@ -27883,6 +28639,37 @@ var KanbanView = class extends RecordView {
|
|
|
27883
28639
|
) : String(content)
|
|
27884
28640
|
);
|
|
27885
28641
|
}
|
|
28642
|
+
/** @returns {string} Per-column placeholder text, or an empty string for none. */
|
|
28643
|
+
_placeholderText() {
|
|
28644
|
+
return this._text("columnEmpty");
|
|
28645
|
+
}
|
|
28646
|
+
/**
|
|
28647
|
+
* @param {number} count Visible records.
|
|
28648
|
+
* @param {number} total Records before search and filters.
|
|
28649
|
+
* @param {number|null} limit Applicable limit.
|
|
28650
|
+
* @returns {HTMLElement}
|
|
28651
|
+
*/
|
|
28652
|
+
_countIndicator(count2, total, limit) {
|
|
28653
|
+
const filtered = count2 !== total;
|
|
28654
|
+
return h("span", {
|
|
28655
|
+
class: "zx-kanban-view__count",
|
|
28656
|
+
ariaLabel: limit != null ? this._text("countLimit", { count: total, limit }) : filtered ? this._text("countFiltered", { count: count2, total }) : this._text("count", { count: count2 })
|
|
28657
|
+
}, limit != null ? `${total} / ${limit}` : filtered ? `${count2} / ${total}` : String(count2));
|
|
28658
|
+
}
|
|
28659
|
+
/**
|
|
28660
|
+
* @param {number} count Visible records in this column and lane.
|
|
28661
|
+
* @param {number} total Records in this column and lane before search and filters.
|
|
28662
|
+
* @param {number} columnTotal Records across the whole column.
|
|
28663
|
+
* @param {number|null} limit Applicable limit.
|
|
28664
|
+
* @param {boolean} columnScoped Whether the limit counts the column rather than this lane.
|
|
28665
|
+
* @returns {HTMLElement}
|
|
28666
|
+
*/
|
|
28667
|
+
_columnIndicator(count2, total, columnTotal, limit, columnScoped) {
|
|
28668
|
+
const filtered = count2 !== total;
|
|
28669
|
+
const label = limit == null ? filtered ? this._text("countFiltered", { count: count2, total }) : this._text("count", { count: count2 }) : columnScoped ? this._text("countLimitInLane", { count: total, total: columnTotal, limit }) : this._text("countLimit", { count: total, limit });
|
|
28670
|
+
const text4 = limit == null ? filtered ? `${count2} / ${total}` : String(count2) : columnScoped ? `${total} \xB7 ${columnTotal} / ${limit}` : `${total} / ${limit}`;
|
|
28671
|
+
return h("span", { class: "zx-kanban-view__count", ariaLabel: label }, text4);
|
|
28672
|
+
}
|
|
27886
28673
|
/** @returns {KanbanColumn[]} */
|
|
27887
28674
|
_rawColumns() {
|
|
27888
28675
|
const options = this._kanbanOptions();
|
|
@@ -27891,7 +28678,11 @@ var KanbanView = class extends RecordView {
|
|
|
27891
28678
|
/** @returns {KanbanColumn[]} */
|
|
27892
28679
|
_resolvedColumns() {
|
|
27893
28680
|
const columns = this._rawColumns();
|
|
27894
|
-
this._kanbanColumnOrder =
|
|
28681
|
+
this._kanbanColumnOrder = reconcileKanbanPreference(
|
|
28682
|
+
columns,
|
|
28683
|
+
this._kanbanColumnOrder,
|
|
28684
|
+
this._kanbanColumns === null
|
|
28685
|
+
);
|
|
27895
28686
|
return orderDescriptors(columns, this._kanbanColumnOrder);
|
|
27896
28687
|
}
|
|
27897
28688
|
/** @returns {KanbanSwimlane[]} */
|
|
@@ -27903,7 +28694,12 @@ var KanbanView = class extends RecordView {
|
|
|
27903
28694
|
/** @returns {KanbanSwimlane[]} */
|
|
27904
28695
|
_resolvedSwimlanes() {
|
|
27905
28696
|
const lanes = this._rawSwimlanes();
|
|
27906
|
-
this.
|
|
28697
|
+
const preserveUnknown = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
28698
|
+
this._kanbanSwimlaneOrder = reconcileKanbanPreference(
|
|
28699
|
+
lanes,
|
|
28700
|
+
this._kanbanSwimlaneOrder,
|
|
28701
|
+
preserveUnknown
|
|
28702
|
+
);
|
|
27907
28703
|
return orderDescriptors(lanes, this._kanbanSwimlaneOrder);
|
|
27908
28704
|
}
|
|
27909
28705
|
/** @param {KanbanAccessor} accessor @param {KanbanRecord} record @param {number} index @returns {unknown} */
|
|
@@ -27927,13 +28723,84 @@ var KanbanView = class extends RecordView {
|
|
|
27927
28723
|
/** @param {unknown} id @returns {KanbanMovePoint|null} */
|
|
27928
28724
|
_recordLocation(id) {
|
|
27929
28725
|
const options = this._kanbanOptions();
|
|
27930
|
-
const index = this._viewData.findIndex((
|
|
28726
|
+
const index = this._viewData.findIndex((record) => Object.is(this._viewRecordId(record), id));
|
|
27931
28727
|
if (index < 0) return null;
|
|
27932
|
-
|
|
27933
|
-
|
|
27934
|
-
|
|
27935
|
-
|
|
27936
|
-
|
|
28728
|
+
return locateKanbanRecord(this._viewData, index, {
|
|
28729
|
+
recordId: options.recordId,
|
|
28730
|
+
columnBy: (record, recordIndex) => this._readAxis(options.columnBy, record, recordIndex),
|
|
28731
|
+
swimlaneBy: options.swimlaneBy == null ? null : (record, recordIndex) => this._readAxis(options.swimlaneBy, record, recordIndex)
|
|
28732
|
+
});
|
|
28733
|
+
}
|
|
28734
|
+
/**
|
|
28735
|
+
* Recomputes which records survive the search and the filter predicate.
|
|
28736
|
+
* @returns {void}
|
|
28737
|
+
*/
|
|
28738
|
+
_resolveVisibility() {
|
|
28739
|
+
const terms = kanbanSearchTerms(this._kanbanSearch);
|
|
28740
|
+
const filter = this._kanbanFilter;
|
|
28741
|
+
const sources = terms.length ? this._searchSources() : null;
|
|
28742
|
+
this._visibleIds = /* @__PURE__ */ new Set();
|
|
28743
|
+
this._viewData.forEach((record, index) => {
|
|
28744
|
+
if (filter && filter(record, index) !== true) return;
|
|
28745
|
+
if (sources && !matchesKanbanSearch(this._searchableText(record, index, sources), terms)) return;
|
|
28746
|
+
this._visibleIds.add(this._viewRecordId(record));
|
|
28747
|
+
});
|
|
28748
|
+
}
|
|
28749
|
+
/**
|
|
28750
|
+
* Resolves what a search looks at, once. `searchFields` names them explicitly; otherwise the
|
|
28751
|
+
* visible fields are searched, plus the title and subtitle sources — a card is findable by the
|
|
28752
|
+
* name it shows even when that field is hidden from the metadata list.
|
|
28753
|
+
* @returns {{fields:import('../view/record-view.js').ViewField[],readers:((record:KanbanRecord,index:number)=>unknown)[]}}
|
|
28754
|
+
*/
|
|
28755
|
+
_searchSources() {
|
|
28756
|
+
const options = this._kanbanOptions();
|
|
28757
|
+
const configured = Array.isArray(options.searchFields) ? options.searchFields.map(String) : null;
|
|
28758
|
+
const all = this.getFields();
|
|
28759
|
+
if (configured) return { fields: all.filter((field) => configured.includes(field.id)), readers: [] };
|
|
28760
|
+
const fields = this.getVisibleFields();
|
|
28761
|
+
const known = new Set(fields.map((field) => field.id));
|
|
28762
|
+
const readers = [];
|
|
28763
|
+
for (const source of [options.titleField, options.subtitleField]) {
|
|
28764
|
+
if (source == null) continue;
|
|
28765
|
+
if (typeof source === "function") {
|
|
28766
|
+
readers.push(source);
|
|
28767
|
+
continue;
|
|
28768
|
+
}
|
|
28769
|
+
if (known.has(source)) continue;
|
|
28770
|
+
const field = all.find((candidate) => candidate.id === source);
|
|
28771
|
+
if (field) fields.push(field);
|
|
28772
|
+
else readers.push((record) => record?.[source]);
|
|
28773
|
+
}
|
|
28774
|
+
return { fields, readers };
|
|
28775
|
+
}
|
|
28776
|
+
/**
|
|
28777
|
+
* Builds the text one record is searched through. Field `render` output is deliberately ignored:
|
|
28778
|
+
* it may be a DOM node, and matching what a formatter produced rather than what the record holds
|
|
28779
|
+
* would make the same query behave differently in two views over the same data.
|
|
28780
|
+
* @param {KanbanRecord} record Record.
|
|
28781
|
+
* @param {number} index Display index.
|
|
28782
|
+
* @param {{fields:import('../view/record-view.js').ViewField[],readers:((record:KanbanRecord,index:number)=>unknown)[]}} sources Resolved sources.
|
|
28783
|
+
* @returns {string} Searchable text.
|
|
28784
|
+
*/
|
|
28785
|
+
_searchableText(record, index, sources) {
|
|
28786
|
+
const parts = sources.fields.map((field) => stringifyValue(readViewField(field, record, index)));
|
|
28787
|
+
for (const reader of sources.readers) parts.push(stringifyValue(reader(record, index)));
|
|
28788
|
+
return parts.filter(Boolean).join(" ");
|
|
28789
|
+
}
|
|
28790
|
+
/**
|
|
28791
|
+
* Translates a position among rendered cards into a position among all records in one bucket.
|
|
28792
|
+
* @param {string} column Column id.
|
|
28793
|
+
* @param {string|null} lane Lane id.
|
|
28794
|
+
* @param {number} visibleIndex Position among visible cards.
|
|
28795
|
+
* @param {Set<unknown>} excluded Ids being moved, which the visible list already omits.
|
|
28796
|
+
* @returns {number} Absolute position.
|
|
28797
|
+
*/
|
|
28798
|
+
_absoluteIndex(column, lane, visibleIndex, excluded) {
|
|
28799
|
+
const entries = this._recordsIn(column, lane).filter((entry) => !excluded.has(this._viewRecordId(entry.record)));
|
|
28800
|
+
return absoluteKanbanIndex(
|
|
28801
|
+
entries.map((entry) => this._visibleIds.has(this._viewRecordId(entry.record))),
|
|
28802
|
+
visibleIndex
|
|
28803
|
+
);
|
|
27937
28804
|
}
|
|
27938
28805
|
/**
|
|
27939
28806
|
* @param {KanbanRecord} record @param {KanbanMovePoint} from @param {KanbanMovePoint} to
|
|
@@ -27955,8 +28822,327 @@ var KanbanView = class extends RecordView {
|
|
|
27955
28822
|
if (options.swimlaneBy != null && normalizeAxisId(this._readAxis(options.swimlaneBy, clone, currentIndex)) !== to.lane) return null;
|
|
27956
28823
|
return clone;
|
|
27957
28824
|
}
|
|
28825
|
+
/**
|
|
28826
|
+
* Runs transition, eligibility, and capacity policy for a whole move. Every distinct origin is
|
|
28827
|
+
* checked, so a multi-card move drawn from two columns cannot slip past a transition rule that
|
|
28828
|
+
* only the second origin violates.
|
|
28829
|
+
* @param {KanbanRecord[]} records Moving records.
|
|
28830
|
+
* @param {KanbanMovePoint[]} origins Their origins.
|
|
28831
|
+
* @param {KanbanColumn} column Destination column.
|
|
28832
|
+
* @param {KanbanSwimlane|null} lane Destination lane.
|
|
28833
|
+
* @param {KanbanMovePoint} to Destination point.
|
|
28834
|
+
* @param {KanbanMoveContext} context Predicate context.
|
|
28835
|
+
* @param {Set<unknown>} excluded Moving ids, excluded from destination counts.
|
|
28836
|
+
* @returns {KanbanMoveEvaluation} Decision.
|
|
28837
|
+
*/
|
|
28838
|
+
_evaluateMove(records, origins, column, lane, to, context, excluded) {
|
|
28839
|
+
const options = this._kanbanOptions();
|
|
28840
|
+
for (const origin of origins) {
|
|
28841
|
+
const blocked = allowsKanbanTransition(column, lane, origin);
|
|
28842
|
+
if (blocked) {
|
|
28843
|
+
return {
|
|
28844
|
+
allowed: false,
|
|
28845
|
+
reason: blocked,
|
|
28846
|
+
limit: null,
|
|
28847
|
+
count: 0,
|
|
28848
|
+
limitExceeded: false,
|
|
28849
|
+
limitReached: false,
|
|
28850
|
+
policy: column.wipPolicy ?? (options.wipPolicy === "block" ? "block" : "warn")
|
|
28851
|
+
};
|
|
28852
|
+
}
|
|
28853
|
+
}
|
|
28854
|
+
const excludes = (record) => !excluded.has(this._viewRecordId(record));
|
|
28855
|
+
const columnCount = this._viewData.filter((record, index) => normalizeAxisId(this._readAxis(options.columnBy, record, index)) === to.column && excludes(record)).length;
|
|
28856
|
+
const laneCount = options.swimlaneBy == null ? 0 : this._viewData.filter((record, index) => normalizeAxisId(this._readAxis(options.swimlaneBy, record, index)) === to.lane && excludes(record)).length;
|
|
28857
|
+
const cellCount = this._recordsIn(to.column, to.lane).filter((entry) => excludes(entry.record)).length;
|
|
28858
|
+
return evaluateKanbanMove({
|
|
28859
|
+
records,
|
|
28860
|
+
column,
|
|
28861
|
+
swimlane: lane,
|
|
28862
|
+
context,
|
|
28863
|
+
from: origins[0],
|
|
28864
|
+
columnCount,
|
|
28865
|
+
laneCount,
|
|
28866
|
+
cellCount,
|
|
28867
|
+
policy: options.wipPolicy === "block" ? "block" : "warn"
|
|
28868
|
+
});
|
|
28869
|
+
}
|
|
28870
|
+
/**
|
|
28871
|
+
* Announces and reports a refused move. Nothing is written and no `recordmove` is emitted, so a
|
|
28872
|
+
* listener can neither observe nor undo a move that never happened.
|
|
28873
|
+
* @param {KanbanRejectReason} reason Refusal reason.
|
|
28874
|
+
* @param {{ids:unknown[],records:KanbanRecord[],from:KanbanMovePoint|null,to:KanbanMovePoint|null,column:KanbanColumn|null,swimlane:KanbanSwimlane|null,evaluation?:KanbanMoveEvaluation}} detail Context.
|
|
28875
|
+
* @returns {void}
|
|
28876
|
+
* @fires KanbanView#movereject
|
|
28877
|
+
*/
|
|
28878
|
+
_rejectMove(reason, detail) {
|
|
28879
|
+
const target = detail.to ? this._targetName(detail.to) : "";
|
|
28880
|
+
const name = this._movingName(detail.records);
|
|
28881
|
+
const message = reason === "destination" ? this._text("rejectDestination") : reason === "grouping" ? this._text("rejectGrouping") : reason === "transition" || reason === "lane-transition" ? this._text("rejectTransition", { target, origin: detail.from ? this._targetName(detail.from) : "" }) : reason === "wip" || reason === "lane-wip" ? this._text("rejectWip", { target, limit: detail.evaluation?.limit ?? "" }) : this._text("rejectAccept", { target, name });
|
|
28882
|
+
this._announce(message);
|
|
28883
|
+
this.emit("movereject", {
|
|
28884
|
+
reason,
|
|
28885
|
+
ids: [...detail.ids],
|
|
28886
|
+
records: [...detail.records],
|
|
28887
|
+
record: detail.records[0] ?? null,
|
|
28888
|
+
id: detail.ids[0],
|
|
28889
|
+
from: detail.from ? { ...detail.from } : null,
|
|
28890
|
+
to: detail.to ? { ...detail.to } : null,
|
|
28891
|
+
column: detail.column ? { ...detail.column } : null,
|
|
28892
|
+
swimlane: detail.swimlane ? { ...detail.swimlane } : null,
|
|
28893
|
+
limit: detail.evaluation?.limit ?? null,
|
|
28894
|
+
count: detail.evaluation?.count ?? 0,
|
|
28895
|
+
message
|
|
28896
|
+
});
|
|
28897
|
+
}
|
|
28898
|
+
/**
|
|
28899
|
+
* Installs a committed local record order, remembering the previous one for undo.
|
|
28900
|
+
* @param {KanbanRecord[]} records Next record order.
|
|
28901
|
+
* @returns {void}
|
|
28902
|
+
*/
|
|
28903
|
+
_commitLocalRecords(records) {
|
|
28904
|
+
const options = this._kanbanOptions();
|
|
28905
|
+
if (options.history !== false) {
|
|
28906
|
+
this._kanbanHistory.push({ records: [...this._viewData], selection: this.getSelectionIds() });
|
|
28907
|
+
this._syncHistoryControls();
|
|
28908
|
+
}
|
|
28909
|
+
this._viewData = records;
|
|
28910
|
+
if (options.sortMode === "local" && this.getSort()) this.setSort(null);
|
|
28911
|
+
else this._refreshView("move");
|
|
28912
|
+
this.emit("datachange", { records: this.getData() });
|
|
28913
|
+
if (options.history !== false) this._emitHistory();
|
|
28914
|
+
}
|
|
28915
|
+
/**
|
|
28916
|
+
* @param {'undo'|'redo'} direction Travel direction.
|
|
28917
|
+
* @returns {this}
|
|
28918
|
+
*/
|
|
28919
|
+
_travelHistory(direction) {
|
|
28920
|
+
if (this._kanbanOptions().history === false) return this;
|
|
28921
|
+
const current = { records: [...this._viewData], selection: this.getSelectionIds() };
|
|
28922
|
+
const entry = direction === "undo" ? this._kanbanHistory.undo(current) : this._kanbanHistory.redo(current);
|
|
28923
|
+
if (!entry) {
|
|
28924
|
+
this._announce(this._text(direction === "undo" ? "nothingToUndo" : "nothingToRedo"));
|
|
28925
|
+
return this;
|
|
28926
|
+
}
|
|
28927
|
+
this._viewData = [...entry.records];
|
|
28928
|
+
this._refreshView(direction);
|
|
28929
|
+
this.setSelection(entry.selection);
|
|
28930
|
+
this._syncHistoryControls();
|
|
28931
|
+
this.emit("datachange", { records: this.getData() });
|
|
28932
|
+
this._emitHistory();
|
|
28933
|
+
this._announce(this._text(direction === "undo" ? "undone" : "redone"));
|
|
28934
|
+
return this;
|
|
28935
|
+
}
|
|
28936
|
+
/** Drops both stacks without announcing, for host-driven data replacement. @returns {void} */
|
|
28937
|
+
_resetHistory() {
|
|
28938
|
+
if (!this._kanbanHistory) return;
|
|
28939
|
+
const had = this._kanbanHistory.canUndo() || this._kanbanHistory.canRedo();
|
|
28940
|
+
this._kanbanHistory.clear();
|
|
28941
|
+
this._syncHistoryControls();
|
|
28942
|
+
if (had) this._emitHistory();
|
|
28943
|
+
}
|
|
28944
|
+
/** @returns {void} */
|
|
28945
|
+
_emitHistory() {
|
|
28946
|
+
this.emit("historychange", {
|
|
28947
|
+
canUndo: this.canUndo(),
|
|
28948
|
+
canRedo: this.canRedo(),
|
|
28949
|
+
depth: this._kanbanHistory.depth()
|
|
28950
|
+
});
|
|
28951
|
+
}
|
|
28952
|
+
/** @returns {void} */
|
|
28953
|
+
_syncHistoryControls() {
|
|
28954
|
+
if (this._undoButton) this._undoButton.disabled = !this.canUndo();
|
|
28955
|
+
if (this._redoButton) this._redoButton.disabled = !this.canRedo();
|
|
28956
|
+
}
|
|
28957
|
+
/** @returns {HTMLElement} Built-in search control. */
|
|
28958
|
+
_createSearchControl() {
|
|
28959
|
+
const input = (
|
|
28960
|
+
/** @type {HTMLInputElement} */
|
|
28961
|
+
h("input", {
|
|
28962
|
+
class: "zx-kanban-view__search-input",
|
|
28963
|
+
type: "search",
|
|
28964
|
+
value: this._kanbanSearch,
|
|
28965
|
+
ariaLabel: this._text("search"),
|
|
28966
|
+
placeholder: this._text("searchPlaceholder")
|
|
28967
|
+
})
|
|
28968
|
+
);
|
|
28969
|
+
this._searchInput = input;
|
|
28970
|
+
this.listen(input, "input", () => {
|
|
28971
|
+
clearTimeout(this._searchTimer);
|
|
28972
|
+
this._searchTimer = setTimeout(() => {
|
|
28973
|
+
this._searchTimer = 0;
|
|
28974
|
+
if (!this._kanbanDestroyed) this.setSearch(input.value);
|
|
28975
|
+
}, SEARCH_DEBOUNCE);
|
|
28976
|
+
});
|
|
28977
|
+
this.listen(input, "keydown", (event) => {
|
|
28978
|
+
if (
|
|
28979
|
+
/** @type {KeyboardEvent} */
|
|
28980
|
+
event.key !== "Escape" || !input.value
|
|
28981
|
+
) return;
|
|
28982
|
+
event.stopPropagation();
|
|
28983
|
+
clearTimeout(this._searchTimer);
|
|
28984
|
+
this._searchTimer = 0;
|
|
28985
|
+
this.setSearch("");
|
|
28986
|
+
});
|
|
28987
|
+
return h("div", { class: "zx-kanban-view__search" }, icon("search", { size: 13 }), input);
|
|
28988
|
+
}
|
|
28989
|
+
/** @returns {HTMLElement} Built-in history controls. */
|
|
28990
|
+
_createHistoryControls() {
|
|
28991
|
+
this._undoButton = /** @type {HTMLButtonElement} */
|
|
28992
|
+
h("button", {
|
|
28993
|
+
class: "zx-button zx-button--sm",
|
|
28994
|
+
type: "button",
|
|
28995
|
+
disabled: true,
|
|
28996
|
+
dataset: { kanbanHistory: "undo" }
|
|
28997
|
+
}, this._text("undo"));
|
|
28998
|
+
this._redoButton = /** @type {HTMLButtonElement} */
|
|
28999
|
+
h("button", {
|
|
29000
|
+
class: "zx-button zx-button--sm",
|
|
29001
|
+
type: "button",
|
|
29002
|
+
disabled: true,
|
|
29003
|
+
dataset: { kanbanHistory: "redo" }
|
|
29004
|
+
}, this._text("redo"));
|
|
29005
|
+
const group = h(
|
|
29006
|
+
"div",
|
|
29007
|
+
{ class: "zx-kanban-view__history", role: "group" },
|
|
29008
|
+
this._undoButton,
|
|
29009
|
+
this._redoButton
|
|
29010
|
+
);
|
|
29011
|
+
this.listen(group, "click", (event) => {
|
|
29012
|
+
const control = (
|
|
29013
|
+
/** @type {Element|null} */
|
|
29014
|
+
event.target?.closest?.("[data-kanban-history]")
|
|
29015
|
+
);
|
|
29016
|
+
if (!control) return;
|
|
29017
|
+
if (
|
|
29018
|
+
/** @type {HTMLElement} */
|
|
29019
|
+
control.dataset.kanbanHistory === "undo"
|
|
29020
|
+
) this.undo();
|
|
29021
|
+
else this.redo();
|
|
29022
|
+
});
|
|
29023
|
+
return group;
|
|
29024
|
+
}
|
|
29025
|
+
/** Attaches the card context menu. @returns {void} */
|
|
29026
|
+
_createContextMenu() {
|
|
29027
|
+
this._kanbanMenu = new ContextMenu(this._board, {
|
|
29028
|
+
selector: ".zx-record-card",
|
|
29029
|
+
items: (context) => this._menuItems(context),
|
|
29030
|
+
onselect: ({ detail }) => this._handleMenuSelect(detail)
|
|
29031
|
+
});
|
|
29032
|
+
}
|
|
29033
|
+
/**
|
|
29034
|
+
* Builds the menu for one card: every destination the same policy path would accept, plus any
|
|
29035
|
+
* application items. Destinations the board would refuse are shown disabled rather than hidden,
|
|
29036
|
+
* so a rule is discoverable instead of silently removing a command.
|
|
29037
|
+
* @param {Element|null} context Card element the menu opened on.
|
|
29038
|
+
* @returns {(MenuItem|'-')[]} Menu entries.
|
|
29039
|
+
*/
|
|
29040
|
+
_menuItems(context) {
|
|
29041
|
+
const meta = context ? this._cardMeta.get(context) : null;
|
|
29042
|
+
if (!meta) return [];
|
|
29043
|
+
const options = this._kanbanOptions();
|
|
29044
|
+
const ids = this._movingIds(meta.id);
|
|
29045
|
+
const items = [];
|
|
29046
|
+
for (const column of this._resolvedColumns()) {
|
|
29047
|
+
if (column.id === meta.column) continue;
|
|
29048
|
+
items.push({
|
|
29049
|
+
label: this._text("menuMoveTo", { target: column.label }),
|
|
29050
|
+
value: { kind: "column", id: column.id, ids },
|
|
29051
|
+
disabled: !this._canMove(ids, { column: column.id })
|
|
29052
|
+
});
|
|
29053
|
+
}
|
|
29054
|
+
if (options.swimlaneBy != null) {
|
|
29055
|
+
for (const lane of this._resolvedSwimlanes()) {
|
|
29056
|
+
if (lane.id === meta.lane) continue;
|
|
29057
|
+
items.push({
|
|
29058
|
+
label: this._text("menuMoveToLane", { target: lane.label }),
|
|
29059
|
+
value: { kind: "lane", id: lane.id, ids },
|
|
29060
|
+
disabled: !this._canMove(ids, { lane: lane.id })
|
|
29061
|
+
});
|
|
29062
|
+
}
|
|
29063
|
+
}
|
|
29064
|
+
const extra = typeof options.contextMenu === "function" ? options.contextMenu({
|
|
29065
|
+
record: meta.record,
|
|
29066
|
+
id: meta.id,
|
|
29067
|
+
index: meta.index,
|
|
29068
|
+
column: meta.column,
|
|
29069
|
+
lane: meta.lane,
|
|
29070
|
+
rules: resolveKanbanRules(this._kanbanRules, meta.record, meta),
|
|
29071
|
+
selected: this._isViewSelected(meta.id)
|
|
29072
|
+
}) : Array.isArray(options.contextMenu) ? options.contextMenu : [];
|
|
29073
|
+
if (Array.isArray(extra) && extra.length) items.push("-", ...extra);
|
|
29074
|
+
return items;
|
|
29075
|
+
}
|
|
29076
|
+
/**
|
|
29077
|
+
* @param {{value:unknown,item:MenuItem,context:Element|null}} detail Selection detail.
|
|
29078
|
+
* @returns {void}
|
|
29079
|
+
*/
|
|
29080
|
+
_handleMenuSelect(detail) {
|
|
29081
|
+
const value = (
|
|
29082
|
+
/** @type {{kind?:string,id?:string,ids?:unknown[]}|null} */
|
|
29083
|
+
detail.value
|
|
29084
|
+
);
|
|
29085
|
+
if (!value || typeof value !== "object" || !value.kind || !Array.isArray(value.ids)) return;
|
|
29086
|
+
this.moveRecords(value.ids, value.kind === "lane" ? { lane: value.id } : { column: value.id });
|
|
29087
|
+
}
|
|
29088
|
+
/**
|
|
29089
|
+
* Dry-runs the policy path for a destination without emitting or committing anything.
|
|
29090
|
+
* @param {unknown[]} ids Moving ids.
|
|
29091
|
+
* @param {KanbanDestination} destination Destination.
|
|
29092
|
+
* @returns {boolean} Whether the move would be allowed.
|
|
29093
|
+
*/
|
|
29094
|
+
_canMove(ids, destination) {
|
|
29095
|
+
const options = this._kanbanOptions();
|
|
29096
|
+
const moving = this._orderedIds(ids);
|
|
29097
|
+
if (!moving.length) return false;
|
|
29098
|
+
const records = moving.map((id) => (
|
|
29099
|
+
/** @type {KanbanRecord} */
|
|
29100
|
+
this.getRecord(id)
|
|
29101
|
+
));
|
|
29102
|
+
const origins = moving.map((id) => (
|
|
29103
|
+
/** @type {KanbanMovePoint} */
|
|
29104
|
+
this._recordLocation(id)
|
|
29105
|
+
));
|
|
29106
|
+
const from = origins[0];
|
|
29107
|
+
const columnId = destination.column == null ? from.column : String(destination.column);
|
|
29108
|
+
const laneId = options.swimlaneBy == null ? null : destination.lane === void 0 ? from.lane : normalizeAxisId(destination.lane);
|
|
29109
|
+
const column = this._resolvedColumns().find((candidate) => candidate.id === columnId);
|
|
29110
|
+
const lane = options.swimlaneBy == null ? null : this._resolvedSwimlanes().find((candidate) => candidate.id === laneId) ?? null;
|
|
29111
|
+
if (!column || options.swimlaneBy != null && !lane) return false;
|
|
29112
|
+
const excluded = new Set(moving);
|
|
29113
|
+
const to = { column: columnId, lane: laneId, index: 0 };
|
|
29114
|
+
const context = {
|
|
29115
|
+
id: moving[0],
|
|
29116
|
+
from: { ...from },
|
|
29117
|
+
to: { ...to },
|
|
29118
|
+
column: { ...column },
|
|
29119
|
+
swimlane: lane ? { ...lane } : null
|
|
29120
|
+
};
|
|
29121
|
+
return this._evaluateMove(records, origins, column, lane, to, context, excluded).allowed;
|
|
29122
|
+
}
|
|
29123
|
+
/**
|
|
29124
|
+
* Resolves the ids one gesture moves: the whole selection when the grabbed card belongs to it,
|
|
29125
|
+
* otherwise just that card. Picking up an unselected card never drags the selection along.
|
|
29126
|
+
* @param {unknown} id Grabbed record id.
|
|
29127
|
+
* @returns {unknown[]} Ids to move, in board order.
|
|
29128
|
+
*/
|
|
29129
|
+
_movingIds(id) {
|
|
29130
|
+
const options = this._kanbanOptions();
|
|
29131
|
+
if (options.multiMove === false || options.selectable !== "multi" || !this._isViewSelected(id)) return [id];
|
|
29132
|
+
const selection = this.getSelectionIds();
|
|
29133
|
+
return selection.length > 1 ? selection : [id];
|
|
29134
|
+
}
|
|
29135
|
+
/** @param {unknown[]} ids Requested ids. @returns {unknown[]} Existing ids in board order. */
|
|
29136
|
+
_orderedIds(ids) {
|
|
29137
|
+
const requested = new Set(ids);
|
|
29138
|
+
return this._viewData.map((record) => this._viewRecordId(record)).filter((id) => requested.has(id));
|
|
29139
|
+
}
|
|
27958
29140
|
/** @param {MouseEvent} event @returns {void} */
|
|
27959
29141
|
_handleBoardClick(event) {
|
|
29142
|
+
if (this._suppressClick) {
|
|
29143
|
+
this._suppressClick = false;
|
|
29144
|
+
return;
|
|
29145
|
+
}
|
|
27960
29146
|
const target = elementTarget(event);
|
|
27961
29147
|
const collapse = target?.closest("[data-kanban-collapse]");
|
|
27962
29148
|
if (collapse) {
|
|
@@ -27968,6 +29154,23 @@ var KanbanView = class extends RecordView {
|
|
|
27968
29154
|
else this.toggleSwimlane(String(element.dataset.kanbanId));
|
|
27969
29155
|
return;
|
|
27970
29156
|
}
|
|
29157
|
+
const add = target?.closest("[data-kanban-add]");
|
|
29158
|
+
if (add) {
|
|
29159
|
+
const element = (
|
|
29160
|
+
/** @type {HTMLElement} */
|
|
29161
|
+
add
|
|
29162
|
+
);
|
|
29163
|
+
const columnId = String(element.dataset.kanbanAdd);
|
|
29164
|
+
const laneId = element.dataset.kanbanLane || null;
|
|
29165
|
+
this.emit("recordadd", {
|
|
29166
|
+
column: this._resolvedColumns().find((candidate) => candidate.id === columnId) ?? null,
|
|
29167
|
+
swimlane: laneId == null ? null : this._resolvedSwimlanes().find((candidate) => candidate.id === laneId) ?? null,
|
|
29168
|
+
columnId,
|
|
29169
|
+
laneId,
|
|
29170
|
+
event
|
|
29171
|
+
});
|
|
29172
|
+
return;
|
|
29173
|
+
}
|
|
27971
29174
|
const meta = this._metaForTarget(target);
|
|
27972
29175
|
if (!meta) return;
|
|
27973
29176
|
const selection = (
|
|
@@ -28016,7 +29219,14 @@ var KanbanView = class extends RecordView {
|
|
|
28016
29219
|
}
|
|
28017
29220
|
/** @param {KeyboardEvent} event @returns {void} */
|
|
28018
29221
|
_handleBoardKeydown(event) {
|
|
29222
|
+
if (this._dragIds.length && event.key === "Escape") {
|
|
29223
|
+
event.preventDefault();
|
|
29224
|
+
this._clearDrag();
|
|
29225
|
+
this._announce(this._text("moveCanceledPlain"));
|
|
29226
|
+
return;
|
|
29227
|
+
}
|
|
28019
29228
|
const target = elementTarget(event);
|
|
29229
|
+
if (this._handleHistoryKeydown(event, target)) return;
|
|
28020
29230
|
const handle = target?.closest(".zx-kanban-view__move-handle");
|
|
28021
29231
|
const meta = this._metaForTarget(handle);
|
|
28022
29232
|
if (handle && meta) {
|
|
@@ -28040,12 +29250,33 @@ var KanbanView = class extends RecordView {
|
|
|
28040
29250
|
this.toggleSelection(cardMeta.id);
|
|
28041
29251
|
}
|
|
28042
29252
|
}
|
|
29253
|
+
/**
|
|
29254
|
+
* @param {KeyboardEvent} event Keyboard event.
|
|
29255
|
+
* @param {Element|null} target Event target.
|
|
29256
|
+
* @returns {boolean} Whether the event was handled as a history command.
|
|
29257
|
+
*/
|
|
29258
|
+
_handleHistoryKeydown(event, target) {
|
|
29259
|
+
if (this._kanbanOptions().history === false || !(event.ctrlKey || event.metaKey)) return false;
|
|
29260
|
+
if (target?.closest('input, textarea, [contenteditable="true"]')) return false;
|
|
29261
|
+
const key = event.key.toLowerCase();
|
|
29262
|
+
if (key === "z" && !event.shiftKey && this.canUndo()) {
|
|
29263
|
+
event.preventDefault();
|
|
29264
|
+
this.undo();
|
|
29265
|
+
return true;
|
|
29266
|
+
}
|
|
29267
|
+
if ((key === "y" || key === "z" && event.shiftKey) && this.canRedo()) {
|
|
29268
|
+
event.preventDefault();
|
|
29269
|
+
this.redo();
|
|
29270
|
+
return true;
|
|
29271
|
+
}
|
|
29272
|
+
return false;
|
|
29273
|
+
}
|
|
28043
29274
|
/** @param {KeyboardEvent} event @param {HTMLElement} handle @param {KanbanCardMeta} meta @returns {void} */
|
|
28044
29275
|
_handleMoveKeydown(event, handle, meta) {
|
|
28045
29276
|
if (event.key === " " || event.key === "Enter") {
|
|
28046
29277
|
event.preventDefault();
|
|
28047
|
-
if (this._keyboardMove
|
|
28048
|
-
const
|
|
29278
|
+
if (this._keyboardMove?.ids.some((candidate) => Object.is(candidate, meta.id))) {
|
|
29279
|
+
const move2 = this._keyboardMove;
|
|
28049
29280
|
this._keyboardMove = null;
|
|
28050
29281
|
handle.setAttribute("aria-pressed", "false");
|
|
28051
29282
|
const card = (
|
|
@@ -28054,22 +29285,24 @@ var KanbanView = class extends RecordView {
|
|
|
28054
29285
|
);
|
|
28055
29286
|
if (card) delete card.dataset.grabbed;
|
|
28056
29287
|
this._syncKeyboardTarget();
|
|
28057
|
-
this.
|
|
29288
|
+
this._commitInteractionMove(move2.ids, move2.to);
|
|
28058
29289
|
} else {
|
|
28059
29290
|
const from = this._recordLocation(meta.id);
|
|
28060
29291
|
if (!from) return;
|
|
28061
|
-
|
|
29292
|
+
const ids = this._movingIds(meta.id);
|
|
29293
|
+
const start = { ...from, index: this._visibleIndex(meta.id) };
|
|
29294
|
+
this._keyboardMove = { ids, from: { ...from }, to: start };
|
|
28062
29295
|
handle.setAttribute("aria-pressed", "true");
|
|
28063
29296
|
const card = handle.closest(".zx-record-card");
|
|
28064
29297
|
if (card) card.dataset.grabbed = "true";
|
|
28065
29298
|
this._syncKeyboardTarget();
|
|
28066
|
-
this._announce(
|
|
29299
|
+
this._announce(ids.length > 1 ? this._text("grabbedMultiple", { count: ids.length }) : this._text("grabbed", { name: this._recordName(meta.record, meta.index) }));
|
|
28067
29300
|
}
|
|
28068
29301
|
return;
|
|
28069
29302
|
}
|
|
28070
29303
|
if (event.key === "Escape" && this._keyboardMove) {
|
|
28071
29304
|
event.preventDefault();
|
|
28072
|
-
const
|
|
29305
|
+
const records = this._keyboardMove.ids.map((id) => this.getRecord(id)).filter(Boolean);
|
|
28073
29306
|
this._keyboardMove = null;
|
|
28074
29307
|
handle.setAttribute("aria-pressed", "false");
|
|
28075
29308
|
const card = (
|
|
@@ -28078,10 +29311,13 @@ var KanbanView = class extends RecordView {
|
|
|
28078
29311
|
);
|
|
28079
29312
|
if (card) delete card.dataset.grabbed;
|
|
28080
29313
|
this._syncKeyboardTarget();
|
|
28081
|
-
this._announce(
|
|
29314
|
+
this._announce(records.length ? this._text("moveCanceled", { name: this._movingName(
|
|
29315
|
+
/** @type {KanbanRecord[]} */
|
|
29316
|
+
records
|
|
29317
|
+
) }) : this._text("moveCanceledPlain"));
|
|
28082
29318
|
return;
|
|
28083
29319
|
}
|
|
28084
|
-
if (!this._keyboardMove || !Object.is(
|
|
29320
|
+
if (!this._keyboardMove || !this._keyboardMove.ids.some((candidate) => Object.is(candidate, meta.id)) || !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return;
|
|
28085
29321
|
event.preventDefault();
|
|
28086
29322
|
const move = this._keyboardMove;
|
|
28087
29323
|
const laneModifier = event.altKey || event.ctrlKey || event.metaKey;
|
|
@@ -28098,10 +29334,14 @@ var KanbanView = class extends RecordView {
|
|
|
28098
29334
|
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
28099
29335
|
move.to.index += event.key === "ArrowUp" ? -1 : 1;
|
|
28100
29336
|
}
|
|
28101
|
-
const targetCount = this.
|
|
28102
|
-
move.to.index =
|
|
29337
|
+
const targetCount = this._visibleCount(move.to.column, move.to.lane, new Set(move.ids));
|
|
29338
|
+
move.to.index = clampKanbanIndex(move.to.index, targetCount);
|
|
28103
29339
|
this._syncKeyboardTarget();
|
|
28104
|
-
this._announce(
|
|
29340
|
+
this._announce(this._text("target", {
|
|
29341
|
+
target: this._targetName(move.to),
|
|
29342
|
+
position: move.to.index + 1,
|
|
29343
|
+
total: targetCount + 1
|
|
29344
|
+
}));
|
|
28105
29345
|
}
|
|
28106
29346
|
/** @param {Event} event @returns {void} */
|
|
28107
29347
|
_handlePreviewError(event) {
|
|
@@ -28117,80 +29357,246 @@ var KanbanView = class extends RecordView {
|
|
|
28117
29357
|
if (preview) preview.dataset.failed = "true";
|
|
28118
29358
|
image.hidden = true;
|
|
28119
29359
|
}
|
|
28120
|
-
/**
|
|
28121
|
-
|
|
28122
|
-
|
|
28123
|
-
|
|
28124
|
-
|
|
28125
|
-
|
|
28126
|
-
|
|
29360
|
+
/**
|
|
29361
|
+
* Records a candidate drag. Nothing moves yet: a press that never travels stays a click, and a
|
|
29362
|
+
* touch on the card body stays a scroll until the long press elapses.
|
|
29363
|
+
* @param {PointerEvent} event Pointer event.
|
|
29364
|
+
* @returns {void}
|
|
29365
|
+
*/
|
|
29366
|
+
_handlePointerDown(event) {
|
|
29367
|
+
this._suppressClick = false;
|
|
29368
|
+
if (event.button != null && event.button !== 0) return;
|
|
29369
|
+
const target = elementTarget(event);
|
|
29370
|
+
const handle = target?.closest(".zx-kanban-view__move-handle");
|
|
29371
|
+
const meta = this._metaForTarget(handle ?? target);
|
|
29372
|
+
if (!meta) return;
|
|
29373
|
+
if (!handle) {
|
|
29374
|
+
if (this._kanbanOptions().dragFrom !== "card") return;
|
|
29375
|
+
if (target?.closest(INTERACTIVE) || target?.closest(".zx-record-card__selection")) return;
|
|
29376
|
+
}
|
|
29377
|
+
const ids = this._movingIds(meta.id);
|
|
29378
|
+
this._pointerStart = {
|
|
29379
|
+
x: event.clientX,
|
|
29380
|
+
y: event.clientY,
|
|
29381
|
+
id: event.pointerId,
|
|
29382
|
+
ids,
|
|
29383
|
+
touch: event.pointerType === "touch"
|
|
29384
|
+
};
|
|
29385
|
+
this._dragPoint = { x: event.clientX, y: event.clientY };
|
|
29386
|
+
clearTimeout(this._pointerHold);
|
|
29387
|
+
if (event.pointerType !== "touch" || handle) return;
|
|
29388
|
+
this._pointerHold = setTimeout(() => this._beginDrag(this._dragPoint.x, this._dragPoint.y), TOUCH_HOLD);
|
|
29389
|
+
}
|
|
29390
|
+
/** @param {PointerEvent} event @returns {void} */
|
|
29391
|
+
_handlePointerMove(event) {
|
|
29392
|
+
const start = this._pointerStart;
|
|
29393
|
+
if (!this._dragIds.length) {
|
|
29394
|
+
if (!start || event.pointerId !== start.id) return;
|
|
29395
|
+
const distance = Math.hypot(event.clientX - start.x, event.clientY - start.y);
|
|
29396
|
+
this._dragPoint = { x: event.clientX, y: event.clientY };
|
|
29397
|
+
if (distance <= DRAG_THRESHOLD) return;
|
|
29398
|
+
if (start.touch && this._pointerHold) {
|
|
29399
|
+
clearTimeout(this._pointerHold);
|
|
29400
|
+
this._pointerHold = 0;
|
|
29401
|
+
this._pointerStart = null;
|
|
29402
|
+
return;
|
|
29403
|
+
}
|
|
29404
|
+
this._beginDrag(event.clientX, event.clientY);
|
|
29405
|
+
if (!this._dragIds.length) return;
|
|
28127
29406
|
}
|
|
28128
|
-
this.
|
|
28129
|
-
|
|
28130
|
-
|
|
28131
|
-
|
|
28132
|
-
|
|
29407
|
+
if (this._dragPointer != null && event.pointerId !== this._dragPointer) return;
|
|
29408
|
+
event.preventDefault();
|
|
29409
|
+
this._dragPoint = { x: event.clientX, y: event.clientY };
|
|
29410
|
+
this._positionDragPreview();
|
|
29411
|
+
this._updateDropTarget(event.clientX, event.clientY);
|
|
28133
29412
|
}
|
|
28134
|
-
/** @param {
|
|
28135
|
-
|
|
28136
|
-
if (this.
|
|
28137
|
-
|
|
28138
|
-
|
|
28139
|
-
|
|
29413
|
+
/** @param {PointerEvent} event @returns {void} */
|
|
29414
|
+
_handlePointerUp(event) {
|
|
29415
|
+
if (this._dragIds.length && this._dragPointer != null && event.pointerId !== this._dragPointer) return;
|
|
29416
|
+
clearTimeout(this._pointerHold);
|
|
29417
|
+
this._pointerHold = 0;
|
|
29418
|
+
this._pointerStart = null;
|
|
29419
|
+
if (!this._dragIds.length) return;
|
|
28140
29420
|
event.preventDefault();
|
|
28141
|
-
const
|
|
28142
|
-
this._dragDestination
|
|
29421
|
+
const ids = [...this._dragIds];
|
|
29422
|
+
const destination = this._dragDestination ? { ...this._dragDestination } : null;
|
|
29423
|
+
this._clearDrag();
|
|
29424
|
+
this._suppressClick = true;
|
|
29425
|
+
if (destination) this._commitInteractionMove(ids, destination);
|
|
29426
|
+
}
|
|
29427
|
+
/**
|
|
29428
|
+
* Starts a pointer drag: the floating preview appears, the board stops selecting text, and the
|
|
29429
|
+
* auto-scroll loop begins.
|
|
29430
|
+
* @param {number} x Pointer client x.
|
|
29431
|
+
* @param {number} y Pointer client y.
|
|
29432
|
+
* @returns {void}
|
|
29433
|
+
*/
|
|
29434
|
+
_beginDrag(x, y) {
|
|
29435
|
+
const start = this._pointerStart;
|
|
29436
|
+
if (!start || this._dragIds.length) return;
|
|
29437
|
+
this._pointerHold = 0;
|
|
29438
|
+
this._dragIds = start.ids.filter((id) => this.getRecord(id));
|
|
29439
|
+
if (!this._dragIds.length) return;
|
|
29440
|
+
this._dragPoint = { x, y };
|
|
29441
|
+
this._board.dataset.dragging = "true";
|
|
29442
|
+
for (const id of this._dragIds) {
|
|
29443
|
+
const card = this._cardsById.get(id);
|
|
29444
|
+
if (card) card.dataset.dragging = "true";
|
|
29445
|
+
}
|
|
29446
|
+
this._dragPointer = start.id;
|
|
29447
|
+
try {
|
|
29448
|
+
this._board.setPointerCapture(start.id);
|
|
29449
|
+
this._dragCaptured = true;
|
|
29450
|
+
} catch {
|
|
29451
|
+
}
|
|
29452
|
+
globalThis.getSelection?.()?.removeAllRanges();
|
|
29453
|
+
this._dragPreview = this._createDragPreview();
|
|
29454
|
+
this.el.append(this._dragPreview);
|
|
29455
|
+
this._positionDragPreview();
|
|
29456
|
+
this._updateDropTarget(x, y);
|
|
29457
|
+
if (this._kanbanOptions().autoScroll !== false) this._scheduleAutoScroll();
|
|
29458
|
+
}
|
|
29459
|
+
/** @returns {HTMLElement} Floating drag preview. */
|
|
29460
|
+
_createDragPreview() {
|
|
29461
|
+
const records = this._dragIds.map((id) => (
|
|
29462
|
+
/** @type {KanbanRecord} */
|
|
29463
|
+
this.getRecord(id)
|
|
29464
|
+
));
|
|
29465
|
+
const custom = this._renderHook("renderDragPreview", {
|
|
29466
|
+
records,
|
|
29467
|
+
ids: [...this._dragIds],
|
|
29468
|
+
count: this._dragIds.length
|
|
29469
|
+
});
|
|
29470
|
+
const content = custom ?? (this._dragIds.length > 1 ? this._text("dragPreview", { count: this._dragIds.length }) : this._movingName(records));
|
|
29471
|
+
return h("div", {
|
|
29472
|
+
class: "zx-kanban-view__drag-preview",
|
|
29473
|
+
ariaHidden: "true",
|
|
29474
|
+
dataset: { count: String(this._dragIds.length) }
|
|
29475
|
+
}, content);
|
|
29476
|
+
}
|
|
29477
|
+
/** @returns {void} */
|
|
29478
|
+
_positionDragPreview() {
|
|
29479
|
+
if (!this._dragPreview) return;
|
|
29480
|
+
this._dragPreview.style.transform = `translate3d(${Math.round(this._dragPoint.x)}px, ${Math.round(this._dragPoint.y)}px, 0)`;
|
|
29481
|
+
}
|
|
29482
|
+
/**
|
|
29483
|
+
* Resolves and marks the drop destination under the pointer. Pointer capture retargets events to
|
|
29484
|
+
* the board, so the element under the pointer is looked up rather than read off the event.
|
|
29485
|
+
* @param {number} x Pointer client x.
|
|
29486
|
+
* @param {number} y Pointer client y.
|
|
29487
|
+
* @returns {void}
|
|
29488
|
+
*/
|
|
29489
|
+
_updateDropTarget(x, y) {
|
|
29490
|
+
const element = document.elementFromPoint(x, y);
|
|
29491
|
+
const section = element?.closest(".zx-kanban-view__column");
|
|
29492
|
+
const sectionMeta = section && this._board.contains(section) ? this._sectionMeta.get(section) : null;
|
|
28143
29493
|
this._clearDropMarkers();
|
|
28144
|
-
|
|
28145
|
-
|
|
28146
|
-
|
|
28147
|
-
const rectangle = card.getBoundingClientRect();
|
|
28148
|
-
card.dataset.drop = event.clientY >= rectangle.top + rectangle.height / 2 ? "after" : "before";
|
|
28149
|
-
} else {
|
|
28150
|
-
sectionMeta.list.dataset.drop = "append";
|
|
29494
|
+
if (!sectionMeta || this._collapsedColumns.has(sectionMeta.column)) {
|
|
29495
|
+
this._dragDestination = null;
|
|
29496
|
+
return;
|
|
28151
29497
|
}
|
|
28152
|
-
|
|
29498
|
+
const cards = this._draggableCards(sectionMeta.list);
|
|
29499
|
+
const index = cards.findIndex((card) => {
|
|
29500
|
+
const rectangle = card.getBoundingClientRect();
|
|
29501
|
+
return y < rectangle.top + rectangle.height / 2;
|
|
29502
|
+
});
|
|
29503
|
+
const position2 = index < 0 ? cards.length : index;
|
|
29504
|
+
if (cards[position2]) cards[position2].dataset.drop = "before";
|
|
29505
|
+
else if (cards.length) cards[cards.length - 1].dataset.drop = "after";
|
|
29506
|
+
else sectionMeta.list.dataset.drop = "append";
|
|
29507
|
+
this._dragDestination = { column: sectionMeta.column, lane: sectionMeta.lane, index: position2 };
|
|
28153
29508
|
}
|
|
28154
|
-
/** @
|
|
28155
|
-
|
|
28156
|
-
if (this.
|
|
28157
|
-
|
|
28158
|
-
|
|
28159
|
-
|
|
28160
|
-
|
|
28161
|
-
|
|
28162
|
-
|
|
28163
|
-
|
|
28164
|
-
|
|
28165
|
-
|
|
28166
|
-
const
|
|
28167
|
-
const
|
|
28168
|
-
if (
|
|
28169
|
-
|
|
28170
|
-
|
|
28171
|
-
|
|
28172
|
-
const
|
|
28173
|
-
if (
|
|
28174
|
-
|
|
28175
|
-
|
|
28176
|
-
|
|
28177
|
-
|
|
28178
|
-
|
|
28179
|
-
|
|
29509
|
+
/** @returns {void} */
|
|
29510
|
+
_scheduleAutoScroll() {
|
|
29511
|
+
if (this._dragFrame) return;
|
|
29512
|
+
this._dragFrame = requestAnimationFrame(() => {
|
|
29513
|
+
this._dragFrame = 0;
|
|
29514
|
+
if (!this._dragIds.length) return;
|
|
29515
|
+
this._autoScroll();
|
|
29516
|
+
this._scheduleAutoScroll();
|
|
29517
|
+
});
|
|
29518
|
+
}
|
|
29519
|
+
/** Scrolls the board horizontally and the hovered column vertically near their edges. @returns {void} */
|
|
29520
|
+
_autoScroll() {
|
|
29521
|
+
const { x, y } = this._dragPoint;
|
|
29522
|
+
const board = this._board.getBoundingClientRect();
|
|
29523
|
+
if (x < board.left + SCROLL_EDGE) this._board.scrollLeft -= SCROLL_SPEED;
|
|
29524
|
+
else if (x > board.right - SCROLL_EDGE) this._board.scrollLeft += SCROLL_SPEED;
|
|
29525
|
+
const list = document.elementFromPoint(x, y)?.closest(".zx-kanban-view__cards");
|
|
29526
|
+
if (!list || !this._board.contains(list) || list.scrollHeight <= list.clientHeight) return;
|
|
29527
|
+
const bounds = list.getBoundingClientRect();
|
|
29528
|
+
if (y < bounds.top + SCROLL_EDGE) list.scrollTop -= SCROLL_SPEED;
|
|
29529
|
+
else if (y > bounds.bottom - SCROLL_EDGE) list.scrollTop += SCROLL_SPEED;
|
|
29530
|
+
}
|
|
29531
|
+
/**
|
|
29532
|
+
* Commits a move whose index came from rendered cards. Search and filters hide records without
|
|
29533
|
+
* removing them, so the visible position is translated back before anything is proposed.
|
|
29534
|
+
* @param {unknown[]} ids Moving ids.
|
|
29535
|
+
* @param {KanbanMovePoint} point Destination in visible-card space.
|
|
29536
|
+
* @returns {void}
|
|
29537
|
+
*/
|
|
29538
|
+
_commitInteractionMove(ids, point) {
|
|
29539
|
+
const excluded = new Set(ids);
|
|
29540
|
+
this.moveRecords(ids, {
|
|
29541
|
+
column: point.column,
|
|
29542
|
+
lane: point.lane,
|
|
29543
|
+
index: this._absoluteIndex(point.column, point.lane, point.index, excluded)
|
|
29544
|
+
});
|
|
28180
29545
|
}
|
|
28181
29546
|
/** @returns {void} */
|
|
28182
29547
|
_clearDrag() {
|
|
28183
|
-
this.
|
|
28184
|
-
|
|
28185
|
-
|
|
28186
|
-
this.
|
|
29548
|
+
if (this._dragFrame) cancelAnimationFrame(this._dragFrame);
|
|
29549
|
+
this._dragFrame = 0;
|
|
29550
|
+
clearTimeout(this._pointerHold);
|
|
29551
|
+
this._pointerHold = 0;
|
|
29552
|
+
this._pointerStart = null;
|
|
29553
|
+
this._dragPreview?.remove();
|
|
29554
|
+
this._dragPreview = null;
|
|
29555
|
+
this._dragIds = [];
|
|
28187
29556
|
this._dragDestination = null;
|
|
29557
|
+
if (this._board) {
|
|
29558
|
+
if (this._dragCaptured && this._dragPointer != null) {
|
|
29559
|
+
try {
|
|
29560
|
+
this._board.releasePointerCapture(this._dragPointer);
|
|
29561
|
+
} catch {
|
|
29562
|
+
}
|
|
29563
|
+
}
|
|
29564
|
+
this._clearDropMarkers();
|
|
29565
|
+
delete this._board.dataset.dragging;
|
|
29566
|
+
for (const card of this._board.querySelectorAll("[data-dragging]")) delete /** @type {HTMLElement} */
|
|
29567
|
+
card.dataset.dragging;
|
|
29568
|
+
}
|
|
29569
|
+
this._dragPointer = null;
|
|
29570
|
+
this._dragCaptured = false;
|
|
28188
29571
|
}
|
|
28189
29572
|
/** @returns {void} */
|
|
28190
29573
|
_clearDropMarkers() {
|
|
28191
29574
|
for (const element of this._board.querySelectorAll("[data-drop]")) delete /** @type {HTMLElement} */
|
|
28192
29575
|
element.dataset.drop;
|
|
28193
29576
|
}
|
|
29577
|
+
/** @param {HTMLElement} list Card list. @returns {HTMLElement[]} Cards excluding the ones in flight. */
|
|
29578
|
+
_draggableCards(list) {
|
|
29579
|
+
const moving = new Set(this._dragIds.length ? this._dragIds : this._keyboardMove?.ids ?? []);
|
|
29580
|
+
return (
|
|
29581
|
+
/** @type {HTMLElement[]} */
|
|
29582
|
+
[...list.querySelectorAll(":scope > .zx-record-card")].filter((card) => !hasId(moving, this._cardMeta.get(card)?.id))
|
|
29583
|
+
);
|
|
29584
|
+
}
|
|
29585
|
+
/** @param {string} column @param {string|null} lane @param {Set<unknown>} excluded @returns {number} */
|
|
29586
|
+
_visibleCount(column, lane, excluded) {
|
|
29587
|
+
return this._recordsIn(column, lane).filter((entry) => {
|
|
29588
|
+
const id = this._viewRecordId(entry.record);
|
|
29589
|
+
return !hasId(excluded, id) && this._visibleIds.has(id);
|
|
29590
|
+
}).length;
|
|
29591
|
+
}
|
|
29592
|
+
/** @param {unknown} id Record id. @returns {number} Position among visible cards in its bucket. */
|
|
29593
|
+
_visibleIndex(id) {
|
|
29594
|
+
const location = this._recordLocation(id);
|
|
29595
|
+
if (!location) return 0;
|
|
29596
|
+
const visible = this._recordsIn(location.column, location.lane).filter((entry) => this._visibleIds.has(this._viewRecordId(entry.record)));
|
|
29597
|
+
const position2 = visible.findIndex((entry) => Object.is(this._viewRecordId(entry.record), id));
|
|
29598
|
+
return position2 < 0 ? visible.length : position2;
|
|
29599
|
+
}
|
|
28194
29600
|
/** @returns {void} */
|
|
28195
29601
|
_syncKeyboardTarget() {
|
|
28196
29602
|
for (const element of this._board.querySelectorAll("[data-keyboard-target]")) {
|
|
@@ -28205,7 +29611,7 @@ var KanbanView = class extends RecordView {
|
|
|
28205
29611
|
});
|
|
28206
29612
|
const sectionMeta = section ? this._sectionMeta.get(section) : null;
|
|
28207
29613
|
if (!sectionMeta) return;
|
|
28208
|
-
const cards =
|
|
29614
|
+
const cards = this._draggableCards(sectionMeta.list);
|
|
28209
29615
|
const target = cards[move.to.index];
|
|
28210
29616
|
if (target) target.dataset.keyboardTarget = "before";
|
|
28211
29617
|
else sectionMeta.list.dataset.keyboardTarget = "append";
|
|
@@ -28215,7 +29621,7 @@ var KanbanView = class extends RecordView {
|
|
|
28215
29621
|
for (const [id, card] of this._cardsById) {
|
|
28216
29622
|
const selected = this._isViewSelected(id);
|
|
28217
29623
|
card.dataset.selected = String(selected);
|
|
28218
|
-
|
|
29624
|
+
this._applyCardDescription(card, id);
|
|
28219
29625
|
const input = (
|
|
28220
29626
|
/** @type {HTMLInputElement|null} */
|
|
28221
29627
|
card.querySelector("[data-record-selection]")
|
|
@@ -28235,6 +29641,50 @@ var KanbanView = class extends RecordView {
|
|
|
28235
29641
|
this._cardsById.get(id)?.querySelector(".zx-kanban-view__move-handle") ?? null
|
|
28236
29642
|
);
|
|
28237
29643
|
}
|
|
29644
|
+
/**
|
|
29645
|
+
* Resolves a collapse control after a board refresh, retaining the same lane copy for columns.
|
|
29646
|
+
* @param {'column'|'swimlane'} axis Collapse axis.
|
|
29647
|
+
* @param {string} id Descriptor id.
|
|
29648
|
+
* @param {string|null} [lane=null] Column copy's lane.
|
|
29649
|
+
* @returns {HTMLElement|null}
|
|
29650
|
+
*/
|
|
29651
|
+
_collapseControl(axis, id, lane = null) {
|
|
29652
|
+
return (
|
|
29653
|
+
/** @type {HTMLElement|null} */
|
|
29654
|
+
[...this._board.querySelectorAll("[data-kanban-collapse]")].find((candidate) => {
|
|
29655
|
+
const control = (
|
|
29656
|
+
/** @type {HTMLElement} */
|
|
29657
|
+
candidate
|
|
29658
|
+
);
|
|
29659
|
+
if (control.dataset.kanbanCollapse !== axis || control.dataset.kanbanId !== id) return false;
|
|
29660
|
+
if (axis !== "column") return true;
|
|
29661
|
+
const section = control.closest(".zx-kanban-view__column");
|
|
29662
|
+
return section ? this._sectionMeta.get(section)?.lane === lane : false;
|
|
29663
|
+
}) ?? null
|
|
29664
|
+
);
|
|
29665
|
+
}
|
|
29666
|
+
/**
|
|
29667
|
+
* Runs one presentation hook, ignoring anything that is not a node so a hook cannot smuggle
|
|
29668
|
+
* markup in as a string.
|
|
29669
|
+
* @param {'renderCard'|'renderColumnHeader'|'renderSwimlaneHeader'|'renderColumnEmpty'|'renderDragPreview'} name Hook name.
|
|
29670
|
+
* @param {any} context Hook context.
|
|
29671
|
+
* @returns {Node|null} Node to adopt, or null for the default presentation.
|
|
29672
|
+
*/
|
|
29673
|
+
_renderHook(name, context) {
|
|
29674
|
+
const hook = this._kanbanOptions()[name];
|
|
29675
|
+
if (typeof hook !== "function") return null;
|
|
29676
|
+
const result = hook(context);
|
|
29677
|
+
return result && typeof result === "object" && "nodeType" in result ? (
|
|
29678
|
+
/** @type {Node} */
|
|
29679
|
+
result
|
|
29680
|
+
) : null;
|
|
29681
|
+
}
|
|
29682
|
+
/** @param {KanbanColumn} column @param {KanbanSwimlane|null} lane @returns {boolean} */
|
|
29683
|
+
_allowsAdd(column, lane) {
|
|
29684
|
+
const allow = this._kanbanOptions().allowAdd;
|
|
29685
|
+
if (typeof allow === "function") return allow(column, lane) === true;
|
|
29686
|
+
return allow === true;
|
|
29687
|
+
}
|
|
28238
29688
|
/** @param {KanbanRecord} record @param {number} index @returns {string} */
|
|
28239
29689
|
_recordName(record, index) {
|
|
28240
29690
|
const source = this._kanbanOptions().titleField;
|
|
@@ -28242,10 +29692,16 @@ var KanbanView = class extends RecordView {
|
|
|
28242
29692
|
if (value && typeof value === "object" && "nodeType" in value) {
|
|
28243
29693
|
return (
|
|
28244
29694
|
/** @type {Node} */
|
|
28245
|
-
value.textContent?.trim() || "record"
|
|
29695
|
+
value.textContent?.trim() || this._text("record")
|
|
28246
29696
|
);
|
|
28247
29697
|
}
|
|
28248
|
-
return String(value ?? "record");
|
|
29698
|
+
return String(value ?? this._text("record"));
|
|
29699
|
+
}
|
|
29700
|
+
/** @param {KanbanRecord[]} records Moving records. @returns {string} */
|
|
29701
|
+
_movingName(records) {
|
|
29702
|
+
if (!records.length) return this._text("record");
|
|
29703
|
+
if (records.length === 1) return this._recordName(records[0], this._viewData.indexOf(records[0]));
|
|
29704
|
+
return this._text("dragPreview", { count: records.length });
|
|
28249
29705
|
}
|
|
28250
29706
|
/** @param {KanbanMovePoint} point @returns {string} */
|
|
28251
29707
|
_targetName(point) {
|
|
@@ -28253,6 +29709,26 @@ var KanbanView = class extends RecordView {
|
|
|
28253
29709
|
const lane = point.lane == null ? null : this._resolvedSwimlanes().find((candidate) => candidate.id === point.lane);
|
|
28254
29710
|
return `${column?.label ?? point.column}${lane ? `, ${lane.label}` : ""}`;
|
|
28255
29711
|
}
|
|
29712
|
+
/** @param {KanbanMoveEvaluation} evaluation Capacity report. @returns {string} */
|
|
29713
|
+
_wipAnnouncement(evaluation) {
|
|
29714
|
+
if (evaluation.limit == null) return "";
|
|
29715
|
+
if (evaluation.limitExceeded) return this._text("limitExceeded", { limit: evaluation.limit });
|
|
29716
|
+
return evaluation.limitReached ? this._text("limitReached", { limit: evaluation.limit }) : "";
|
|
29717
|
+
}
|
|
29718
|
+
/**
|
|
29719
|
+
* Resolves one localizable string.
|
|
29720
|
+
* @param {keyof typeof DEFAULT_LABELS|string} key Label key.
|
|
29721
|
+
* @param {Record<string, unknown>} [args] Placeholder values.
|
|
29722
|
+
* @returns {string} Resolved text.
|
|
29723
|
+
*/
|
|
29724
|
+
_text(key, args) {
|
|
29725
|
+
const overrides = this._kanbanOptions().labels;
|
|
29726
|
+
const source = overrides && typeof overrides === "object" && key in overrides ? overrides[key] : (
|
|
29727
|
+
/** @type {Record<string,string>} */
|
|
29728
|
+
DEFAULT_LABELS[key]
|
|
29729
|
+
);
|
|
29730
|
+
return printf(source == null ? String(key) : String(source), args);
|
|
29731
|
+
}
|
|
28256
29732
|
/** @param {string} message @returns {void} */
|
|
28257
29733
|
_announce(message) {
|
|
28258
29734
|
const token = ++this._announcementToken;
|
|
@@ -28292,6 +29768,9 @@ function normalizeKanbanDescriptors(descriptors, axis = "column") {
|
|
|
28292
29768
|
if (source.accept != null && typeof source.accept !== "function") {
|
|
28293
29769
|
throw new TypeError(`Kanban ${axis} ${id} accept must be a function`);
|
|
28294
29770
|
}
|
|
29771
|
+
if (source.from != null && !Array.isArray(source.from)) {
|
|
29772
|
+
throw new TypeError(`Kanban ${axis} ${id} from must be an array or null`);
|
|
29773
|
+
}
|
|
28295
29774
|
seen.add(id);
|
|
28296
29775
|
const normalized = (
|
|
28297
29776
|
/** @type {Record<string, any>} */
|
|
@@ -28299,11 +29778,21 @@ function normalizeKanbanDescriptors(descriptors, axis = "column") {
|
|
|
28299
29778
|
...source,
|
|
28300
29779
|
id,
|
|
28301
29780
|
label: String(source.label ?? axisLabel(source.id)),
|
|
29781
|
+
limit: normalizeLimit2(source.limit),
|
|
29782
|
+
from: Array.isArray(source.from) ? source.from.map(String) : null,
|
|
28302
29783
|
...Object.prototype.hasOwnProperty.call(source, "value") ? {} : { value: source.id }
|
|
28303
29784
|
}
|
|
28304
29785
|
);
|
|
28305
|
-
if (axis === "column")
|
|
28306
|
-
|
|
29786
|
+
if (axis === "column") {
|
|
29787
|
+
const laneLimits = normalizeLaneLimits(source.laneLimits);
|
|
29788
|
+
if (laneLimits) normalized.laneLimits = laneLimits;
|
|
29789
|
+
if (source.wipPolicy === "block" || source.wipPolicy === "warn") {
|
|
29790
|
+
normalized.wipPolicy = source.wipPolicy;
|
|
29791
|
+
}
|
|
29792
|
+
} else {
|
|
29793
|
+
delete normalized.laneLimits;
|
|
29794
|
+
delete normalized.wipPolicy;
|
|
29795
|
+
}
|
|
28307
29796
|
return normalized;
|
|
28308
29797
|
})
|
|
28309
29798
|
);
|
|
@@ -28326,37 +29815,11 @@ function reconcileKanbanOrder(descriptors, order) {
|
|
|
28326
29815
|
const preferred = normalizeOrder(order).filter((id) => allowed.has(id));
|
|
28327
29816
|
return [...new Set(preferred), ...available.filter((id) => !preferred.includes(id))];
|
|
28328
29817
|
}
|
|
28329
|
-
function
|
|
28330
|
-
if (!
|
|
28331
|
-
const
|
|
28332
|
-
|
|
28333
|
-
|
|
28334
|
-
const sourceColumn = normalizeAxisId(readKanbanAccessor(source, sourceIndex, options.columnBy));
|
|
28335
|
-
const sourceLane = options.swimlaneBy == null ? null : normalizeAxisId(readKanbanAccessor(source, sourceIndex, options.swimlaneBy));
|
|
28336
|
-
const sourceGroup = records.filter((record, index) => normalizeAxisId(readKanbanAccessor(record, index, options.columnBy)) === sourceColumn && (options.swimlaneBy == null || normalizeAxisId(readKanbanAccessor(record, index, options.swimlaneBy)) === sourceLane));
|
|
28337
|
-
const from = {
|
|
28338
|
-
column: sourceColumn,
|
|
28339
|
-
lane: sourceLane,
|
|
28340
|
-
index: sourceGroup.findIndex((record) => Object.is(viewRecordId(record, options.recordId), id))
|
|
28341
|
-
};
|
|
28342
|
-
const next = records.filter((_, index) => index !== sourceIndex);
|
|
28343
|
-
const destinationRecords = next.filter((record, index) => normalizeAxisId(readKanbanAccessor(record, index, options.columnBy)) === options.destination.column && (options.swimlaneBy == null || normalizeAxisId(readKanbanAccessor(record, index, options.swimlaneBy)) === options.destination.lane));
|
|
28344
|
-
const targetIndex = normalizeMoveIndex(options.destination.index, destinationRecords.length);
|
|
28345
|
-
let insertion = next.length;
|
|
28346
|
-
if (destinationRecords[targetIndex]) insertion = next.indexOf(destinationRecords[targetIndex]);
|
|
28347
|
-
else if (destinationRecords.length) insertion = next.indexOf(destinationRecords.at(-1)) + 1;
|
|
28348
|
-
next.splice(insertion, 0, movedRecord);
|
|
28349
|
-
return {
|
|
28350
|
-
records: next,
|
|
28351
|
-
from,
|
|
28352
|
-
to: { column: options.destination.column, lane: options.swimlaneBy == null ? null : options.destination.lane, index: targetIndex }
|
|
28353
|
-
};
|
|
28354
|
-
}
|
|
28355
|
-
function readKanbanAccessor(record, index, accessor) {
|
|
28356
|
-
return typeof accessor === "function" ? accessor(record, index) : accessor == null ? null : record?.[accessor];
|
|
28357
|
-
}
|
|
28358
|
-
function normalizeAxisId(value) {
|
|
28359
|
-
return value == null ? "" : String(value);
|
|
29818
|
+
function reconcileKanbanPreference(descriptors, order, preserveUnknown = false) {
|
|
29819
|
+
if (!preserveUnknown) return reconcileKanbanOrder(descriptors, order);
|
|
29820
|
+
const preferred = normalizeOrder(order);
|
|
29821
|
+
const available = descriptors.map((descriptor) => descriptor.id);
|
|
29822
|
+
return [...preferred, ...available.filter((id) => !preferred.includes(id))];
|
|
28360
29823
|
}
|
|
28361
29824
|
function axisLabel(value) {
|
|
28362
29825
|
if (value == null || String(value).trim() === "") return "Unassigned";
|
|
@@ -28368,13 +29831,42 @@ function normalizeLimit2(value) {
|
|
|
28368
29831
|
if (!Number.isFinite(number2) || number2 < 0) throw new TypeError("Kanban column limit must be a non-negative number");
|
|
28369
29832
|
return Math.floor(number2);
|
|
28370
29833
|
}
|
|
28371
|
-
function
|
|
28372
|
-
if (value == null
|
|
28373
|
-
|
|
29834
|
+
function normalizeLaneLimits(value) {
|
|
29835
|
+
if (value == null) return void 0;
|
|
29836
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
29837
|
+
throw new TypeError("Kanban column laneLimits must be an object");
|
|
29838
|
+
}
|
|
29839
|
+
const result = {};
|
|
29840
|
+
for (const [lane, limit] of Object.entries(
|
|
29841
|
+
/** @type {Record<string, unknown>} */
|
|
29842
|
+
value
|
|
29843
|
+
)) {
|
|
29844
|
+
const normalized = normalizeLimit2(limit);
|
|
29845
|
+
if (normalized != null) result[lane] = normalized;
|
|
29846
|
+
}
|
|
29847
|
+
return result;
|
|
29848
|
+
}
|
|
29849
|
+
function readColumnLaneLimit(column, lane) {
|
|
29850
|
+
const limits = column.laneLimits;
|
|
29851
|
+
if (!limits || typeof limits !== "object") return null;
|
|
29852
|
+
const value = limits[lane];
|
|
29853
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
29854
|
+
}
|
|
29855
|
+
function wipState(count2, limit) {
|
|
29856
|
+
if (limit == null) return "none";
|
|
29857
|
+
return count2 > limit ? "exceeded" : count2 === limit ? "limit" : "within";
|
|
28374
29858
|
}
|
|
28375
29859
|
function normalizeOrder(value) {
|
|
28376
29860
|
return Array.isArray(value) ? [...new Set(value.map(String))] : [];
|
|
28377
29861
|
}
|
|
29862
|
+
function stringifyValue(value) {
|
|
29863
|
+
if (value == null) return "";
|
|
29864
|
+
if (typeof value === "object") return "nodeType" in value ? String(
|
|
29865
|
+
/** @type {Node} */
|
|
29866
|
+
value.textContent ?? ""
|
|
29867
|
+
) : "";
|
|
29868
|
+
return String(value);
|
|
29869
|
+
}
|
|
28378
29870
|
function orderDescriptors(descriptors, order) {
|
|
28379
29871
|
const byId = new Map(descriptors.map((descriptor) => [descriptor.id, descriptor]));
|
|
28380
29872
|
return order.map((id) => byId.get(id)).filter(Boolean);
|
|
@@ -28388,12 +29880,28 @@ function moveOrderedId(order, id, targetId, position2) {
|
|
|
28388
29880
|
next.splice(target + (position2 === "after" ? 1 : 0), 0, String(id));
|
|
28389
29881
|
return next;
|
|
28390
29882
|
}
|
|
29883
|
+
function mergeKanbanResolvedOrder(preference, descriptors, resolvedOrder, preserveUnknown) {
|
|
29884
|
+
if (!preserveUnknown) return [...resolvedOrder];
|
|
29885
|
+
const available = new Set(descriptors.map((descriptor) => descriptor.id));
|
|
29886
|
+
const replacements = [...resolvedOrder];
|
|
29887
|
+
let cursor = 0;
|
|
29888
|
+
const merged = normalizeOrder(preference).flatMap((id) => {
|
|
29889
|
+
if (!available.has(id)) return [id];
|
|
29890
|
+
return cursor < replacements.length ? [replacements[cursor++]] : [];
|
|
29891
|
+
});
|
|
29892
|
+
merged.push(...replacements.slice(cursor));
|
|
29893
|
+
return normalizeOrder(merged);
|
|
29894
|
+
}
|
|
28391
29895
|
function sameMovePoint(left, right) {
|
|
28392
29896
|
return left.column === right.column && left.lane === right.lane && left.index === right.index;
|
|
28393
29897
|
}
|
|
28394
29898
|
function sameArray3(left, right) {
|
|
28395
29899
|
return left.length === right.length && left.every((value, index) => Object.is(value, right[index]));
|
|
28396
29900
|
}
|
|
29901
|
+
function hasId(haystack, needle) {
|
|
29902
|
+
for (const candidate of haystack) if (Object.is(candidate, needle)) return true;
|
|
29903
|
+
return false;
|
|
29904
|
+
}
|
|
28397
29905
|
function elementTarget(event) {
|
|
28398
29906
|
const target = event.target;
|
|
28399
29907
|
if (!target) return null;
|
|
@@ -32993,9 +34501,12 @@ export {
|
|
|
32993
34501
|
TreeView,
|
|
32994
34502
|
ValueList,
|
|
32995
34503
|
__,
|
|
34504
|
+
absoluteKanbanIndex,
|
|
32996
34505
|
addCalendarDays,
|
|
34506
|
+
addCalendarMonths,
|
|
32997
34507
|
addDays,
|
|
32998
34508
|
addMonths,
|
|
34509
|
+
allowsKanbanTransition,
|
|
32999
34510
|
appIcon,
|
|
33000
34511
|
appendCompact,
|
|
33001
34512
|
appendCompactChild,
|
|
@@ -33014,6 +34525,7 @@ export {
|
|
|
33014
34525
|
calendarDayKey,
|
|
33015
34526
|
calendarEventIntersects,
|
|
33016
34527
|
calendarEventSpansDays,
|
|
34528
|
+
calendarPageDate,
|
|
33017
34529
|
calendarRange,
|
|
33018
34530
|
chartSummaryTable,
|
|
33019
34531
|
clamp,
|
|
@@ -33027,6 +34539,7 @@ export {
|
|
|
33027
34539
|
configureIcons,
|
|
33028
34540
|
copyButton,
|
|
33029
34541
|
copyToClipboard,
|
|
34542
|
+
createKanbanHistory,
|
|
33030
34543
|
debounce,
|
|
33031
34544
|
deepMerge,
|
|
33032
34545
|
defaultDateRangePresets,
|
|
@@ -33036,6 +34549,7 @@ export {
|
|
|
33036
34549
|
emptyFilterAst,
|
|
33037
34550
|
emptyState,
|
|
33038
34551
|
escapeRegExp,
|
|
34552
|
+
evaluateKanbanMove,
|
|
33039
34553
|
faFamilies,
|
|
33040
34554
|
faIconClasses,
|
|
33041
34555
|
faNames,
|
|
@@ -33070,15 +34584,19 @@ export {
|
|
|
33070
34584
|
isSameDay,
|
|
33071
34585
|
isTruncated,
|
|
33072
34586
|
joinTime,
|
|
34587
|
+
kanbanSearchTerms,
|
|
33073
34588
|
kitUrl,
|
|
33074
34589
|
layoutCalendarSpans,
|
|
33075
34590
|
layoutTimedCalendarEvents,
|
|
33076
34591
|
loadFontAwesome,
|
|
33077
34592
|
loadFontAwesomeKit,
|
|
34593
|
+
locateKanbanRecord,
|
|
33078
34594
|
matchBreakpoint,
|
|
34595
|
+
matchesKanbanSearch,
|
|
33079
34596
|
moveViewField,
|
|
33080
34597
|
normalizeAppItems,
|
|
33081
34598
|
normalizeCalendarEvents,
|
|
34599
|
+
normalizeKanbanRules,
|
|
33082
34600
|
normalizeRange,
|
|
33083
34601
|
normalizeViewFields,
|
|
33084
34602
|
normalizeViewState,
|
|
@@ -33103,8 +34621,10 @@ export {
|
|
|
33103
34621
|
registerFieldAdapters,
|
|
33104
34622
|
registerIcons,
|
|
33105
34623
|
renderViewField,
|
|
34624
|
+
reorderKanbanRecords,
|
|
33106
34625
|
reorderTableRows,
|
|
33107
34626
|
resolveElement,
|
|
34627
|
+
resolveKanbanRules,
|
|
33108
34628
|
resolveSize,
|
|
33109
34629
|
rovingTabindex,
|
|
33110
34630
|
setLanguage,
|