@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.global.js
CHANGED
|
@@ -91,9 +91,12 @@ var zx = (() => {
|
|
|
91
91
|
TreeView: () => TreeView,
|
|
92
92
|
ValueList: () => ValueList,
|
|
93
93
|
__: () => __,
|
|
94
|
+
absoluteKanbanIndex: () => absoluteKanbanIndex,
|
|
94
95
|
addCalendarDays: () => addCalendarDays,
|
|
96
|
+
addCalendarMonths: () => addCalendarMonths,
|
|
95
97
|
addDays: () => addDays,
|
|
96
98
|
addMonths: () => addMonths,
|
|
99
|
+
allowsKanbanTransition: () => allowsKanbanTransition,
|
|
97
100
|
appIcon: () => appIcon,
|
|
98
101
|
appendCompact: () => appendCompact,
|
|
99
102
|
appendCompactChild: () => appendCompactChild,
|
|
@@ -112,6 +115,7 @@ var zx = (() => {
|
|
|
112
115
|
calendarDayKey: () => calendarDayKey,
|
|
113
116
|
calendarEventIntersects: () => calendarEventIntersects,
|
|
114
117
|
calendarEventSpansDays: () => calendarEventSpansDays,
|
|
118
|
+
calendarPageDate: () => calendarPageDate,
|
|
115
119
|
calendarRange: () => calendarRange,
|
|
116
120
|
chartSummaryTable: () => chartSummaryTable,
|
|
117
121
|
clamp: () => clamp,
|
|
@@ -125,6 +129,7 @@ var zx = (() => {
|
|
|
125
129
|
configureIcons: () => configureIcons,
|
|
126
130
|
copyButton: () => copyButton,
|
|
127
131
|
copyToClipboard: () => copyToClipboard,
|
|
132
|
+
createKanbanHistory: () => createKanbanHistory,
|
|
128
133
|
debounce: () => debounce,
|
|
129
134
|
deepMerge: () => deepMerge,
|
|
130
135
|
defaultDateRangePresets: () => defaultDateRangePresets,
|
|
@@ -134,6 +139,7 @@ var zx = (() => {
|
|
|
134
139
|
emptyFilterAst: () => emptyFilterAst,
|
|
135
140
|
emptyState: () => emptyState,
|
|
136
141
|
escapeRegExp: () => escapeRegExp,
|
|
142
|
+
evaluateKanbanMove: () => evaluateKanbanMove,
|
|
137
143
|
faFamilies: () => faFamilies,
|
|
138
144
|
faIconClasses: () => faIconClasses,
|
|
139
145
|
faNames: () => faNames,
|
|
@@ -168,15 +174,19 @@ var zx = (() => {
|
|
|
168
174
|
isSameDay: () => isSameDay,
|
|
169
175
|
isTruncated: () => isTruncated,
|
|
170
176
|
joinTime: () => joinTime,
|
|
177
|
+
kanbanSearchTerms: () => kanbanSearchTerms,
|
|
171
178
|
kitUrl: () => kitUrl,
|
|
172
179
|
layoutCalendarSpans: () => layoutCalendarSpans,
|
|
173
180
|
layoutTimedCalendarEvents: () => layoutTimedCalendarEvents,
|
|
174
181
|
loadFontAwesome: () => loadFontAwesome,
|
|
175
182
|
loadFontAwesomeKit: () => loadFontAwesomeKit,
|
|
183
|
+
locateKanbanRecord: () => locateKanbanRecord,
|
|
176
184
|
matchBreakpoint: () => matchBreakpoint,
|
|
185
|
+
matchesKanbanSearch: () => matchesKanbanSearch,
|
|
177
186
|
moveViewField: () => moveViewField,
|
|
178
187
|
normalizeAppItems: () => normalizeAppItems,
|
|
179
188
|
normalizeCalendarEvents: () => normalizeCalendarEvents,
|
|
189
|
+
normalizeKanbanRules: () => normalizeKanbanRules,
|
|
180
190
|
normalizeRange: () => normalizeRange,
|
|
181
191
|
normalizeViewFields: () => normalizeViewFields,
|
|
182
192
|
normalizeViewState: () => normalizeViewState,
|
|
@@ -201,8 +211,10 @@ var zx = (() => {
|
|
|
201
211
|
registerFieldAdapters: () => registerFieldAdapters,
|
|
202
212
|
registerIcons: () => registerIcons,
|
|
203
213
|
renderViewField: () => renderViewField,
|
|
214
|
+
reorderKanbanRecords: () => reorderKanbanRecords,
|
|
204
215
|
reorderTableRows: () => reorderTableRows,
|
|
205
216
|
resolveElement: () => resolveElement,
|
|
217
|
+
resolveKanbanRules: () => resolveKanbanRules,
|
|
206
218
|
resolveSize: () => resolveSize,
|
|
207
219
|
rovingTabindex: () => rovingTabindex,
|
|
208
220
|
setLanguage: () => setLanguage,
|
|
@@ -1039,21 +1051,26 @@ var zx = (() => {
|
|
|
1039
1051
|
* Dispatches a component event and a bubbling, composed `zx-*` DOM event.
|
|
1040
1052
|
* @param {string} type Lowercase event name.
|
|
1041
1053
|
* @param {Record<string, unknown>} [detail={}] Single event detail object.
|
|
1054
|
+
* @param {{honorDomCancellation?:boolean}} [options={}] Mirrored DOM-event behavior. Components
|
|
1055
|
+
* with a cancelable mutation boundary may opt into treating `preventDefault()` on the bubbling
|
|
1056
|
+
* `zx-*` event exactly like cancellation of the component event.
|
|
1042
1057
|
* @returns {Event & {detail: Record<string, unknown>}}
|
|
1043
1058
|
* @fires Component#event
|
|
1044
1059
|
*/
|
|
1045
|
-
emit(type, detail = {}) {
|
|
1060
|
+
emit(type, detail = {}, options = {}) {
|
|
1046
1061
|
if (detail === null || typeof detail !== "object" || Array.isArray(detail)) {
|
|
1047
1062
|
throw new TypeError("Event detail must be an object");
|
|
1048
1063
|
}
|
|
1049
1064
|
const componentEvent = createCustomEvent(type, detail, { cancelable: true });
|
|
1050
1065
|
this.dispatchEvent(componentEvent);
|
|
1051
1066
|
if (this.el) {
|
|
1052
|
-
|
|
1067
|
+
const domEvent = createCustomEvent(`zx-${type}`, detail, {
|
|
1053
1068
|
bubbles: true,
|
|
1054
1069
|
composed: true,
|
|
1055
1070
|
cancelable: true
|
|
1056
|
-
})
|
|
1071
|
+
});
|
|
1072
|
+
this.el.dispatchEvent(domEvent);
|
|
1073
|
+
if (options.honorDomCancellation && domEvent.defaultPrevented) componentEvent.preventDefault();
|
|
1057
1074
|
}
|
|
1058
1075
|
return componentEvent;
|
|
1059
1076
|
}
|
|
@@ -24250,6 +24267,29 @@ var zx = (() => {
|
|
|
24250
24267
|
}
|
|
24251
24268
|
return { start, end };
|
|
24252
24269
|
}
|
|
24270
|
+
function calendarPageDate(view, anchor, direction, options = {}) {
|
|
24271
|
+
const date = validDate5(anchor, "Calendar date");
|
|
24272
|
+
const step = integer(direction, "direction");
|
|
24273
|
+
if (view === "day") date.setDate(date.getDate() + step);
|
|
24274
|
+
else if (view === "agenda") {
|
|
24275
|
+
date.setDate(date.getDate() + step * positiveInteger(options.agendaDays ?? 14, "agendaDays"));
|
|
24276
|
+
} else if (view === "week") date.setDate(date.getDate() + step * 7);
|
|
24277
|
+
else if (view === "month") return addCalendarMonths(date, step);
|
|
24278
|
+
else if (view === "year") return addCalendarMonths(date, step * 12);
|
|
24279
|
+
else throw new RangeError(`Unknown calendar view: ${view}`);
|
|
24280
|
+
return date;
|
|
24281
|
+
}
|
|
24282
|
+
function addCalendarMonths(value, months) {
|
|
24283
|
+
const date = validDate5(value, "Calendar date");
|
|
24284
|
+
const day = date.getDate();
|
|
24285
|
+
date.setDate(1);
|
|
24286
|
+
date.setMonth(date.getMonth() + integer(months, "month amount"));
|
|
24287
|
+
date.setDate(Math.min(day, daysInCalendarMonth(date.getFullYear(), date.getMonth())));
|
|
24288
|
+
return date;
|
|
24289
|
+
}
|
|
24290
|
+
function daysInCalendarMonth(year, month) {
|
|
24291
|
+
return new Date(year, month + 1, 0).getDate();
|
|
24292
|
+
}
|
|
24253
24293
|
function normalizeCalendarEvents(records, options = {}) {
|
|
24254
24294
|
if (!Array.isArray(records)) throw new TypeError("Calendar events must be an array");
|
|
24255
24295
|
const readers = {
|
|
@@ -24693,7 +24733,7 @@ var zx = (() => {
|
|
|
24693
24733
|
durationEditable: Object.prototype.hasOwnProperty.call(changes, "durationEditable") ? changes.durationEditable : current.durationEditable,
|
|
24694
24734
|
data: changes.data ?? current.data
|
|
24695
24735
|
};
|
|
24696
|
-
const [next] = normalizeCalendarEvents([source]);
|
|
24736
|
+
const [next] = normalizeCalendarEvents([source], { dateUnit: this.options.dateUnit });
|
|
24697
24737
|
next.data = source.data;
|
|
24698
24738
|
this._events = this._events.map((event, eventIndex) => eventIndex === index ? next : event);
|
|
24699
24739
|
this._mutationVersions.delete(String(id));
|
|
@@ -25719,13 +25759,9 @@ var zx = (() => {
|
|
|
25719
25759
|
}
|
|
25720
25760
|
/** @param {number} direction @returns {this} */
|
|
25721
25761
|
_page(direction) {
|
|
25722
|
-
|
|
25723
|
-
|
|
25724
|
-
|
|
25725
|
-
else if (this._view === "week") date.setDate(date.getDate() + direction * 7);
|
|
25726
|
-
else if (this._view === "month") date.setMonth(date.getMonth() + direction);
|
|
25727
|
-
else date.setFullYear(date.getFullYear() + direction);
|
|
25728
|
-
return this.setDate(date);
|
|
25762
|
+
return this.setDate(calendarPageDate(this._view, this._date, direction, {
|
|
25763
|
+
agendaDays: this.options.agendaDays
|
|
25764
|
+
}));
|
|
25729
25765
|
}
|
|
25730
25766
|
/** @param {{start: Date, end: Date}} range @returns {string} */
|
|
25731
25767
|
_rangeTitle(range) {
|
|
@@ -26882,6 +26918,16 @@ var zx = (() => {
|
|
|
26882
26918
|
class: "zx-record-card__heading",
|
|
26883
26919
|
id: headingId
|
|
26884
26920
|
}, headingContent);
|
|
26921
|
+
const fieldGroups = partitionRecordCardFields(visibleFields, titleSource, subtitleSource);
|
|
26922
|
+
const eyebrow = createEyebrow(record, index, fieldGroups.eyebrowStart, fieldGroups.eyebrowEnd);
|
|
26923
|
+
const titlePrefix = createFieldList(
|
|
26924
|
+
record,
|
|
26925
|
+
index,
|
|
26926
|
+
fieldGroups.titlePrefix,
|
|
26927
|
+
"zx-record-card__title-prefix",
|
|
26928
|
+
"title-prefix"
|
|
26929
|
+
);
|
|
26930
|
+
const titleRow = titlePrefix ? h("div", { class: "zx-record-card__title-row" }, titlePrefix, heading) : heading;
|
|
26885
26931
|
const selection = selectable === "multi" ? h("label", {
|
|
26886
26932
|
class: "zx-record-card__selection"
|
|
26887
26933
|
}, h("input", {
|
|
@@ -26903,19 +26949,19 @@ var zx = (() => {
|
|
|
26903
26949
|
h(
|
|
26904
26950
|
"div",
|
|
26905
26951
|
{ class: "zx-record-card__titles" },
|
|
26906
|
-
|
|
26952
|
+
titleRow,
|
|
26907
26953
|
isEmptyContent(subtitle) ? null : h("p", { class: "zx-record-card__subtitle" }, contentNode(subtitle))
|
|
26908
26954
|
),
|
|
26909
26955
|
actionArea
|
|
26910
26956
|
);
|
|
26911
|
-
const metadata =
|
|
26912
|
-
const body = h("div", { class: "zx-record-card__body" }, header, metadata);
|
|
26957
|
+
const metadata = createFieldList(record, index, fieldGroups.metadata, "zx-record-card__metadata");
|
|
26958
|
+
const body = h("div", { class: "zx-record-card__body" }, eyebrow, header, metadata);
|
|
26913
26959
|
const card = (
|
|
26914
26960
|
/** @type {HTMLLIElement} */
|
|
26915
26961
|
h("li", {
|
|
26916
26962
|
class: "zx-record-card",
|
|
26917
26963
|
tabindex: 0,
|
|
26918
|
-
|
|
26964
|
+
ariaDescription: selectable ? selected ? "Selected" : "Not selected" : void 0,
|
|
26919
26965
|
ariaLabelledby: headingId,
|
|
26920
26966
|
dataset: {
|
|
26921
26967
|
recordIndex: String(index),
|
|
@@ -26926,6 +26972,20 @@ var zx = (() => {
|
|
|
26926
26972
|
);
|
|
26927
26973
|
return card;
|
|
26928
26974
|
}
|
|
26975
|
+
function partitionRecordCardFields(fields, titleSource = null, subtitleSource = null) {
|
|
26976
|
+
const groups = { eyebrowStart: [], eyebrowEnd: [], titlePrefix: [], metadata: [] };
|
|
26977
|
+
const titleId = typeof titleSource === "string" ? titleSource : null;
|
|
26978
|
+
const subtitleId = typeof subtitleSource === "string" ? subtitleSource : null;
|
|
26979
|
+
for (const field of Array.isArray(fields) ? fields : []) {
|
|
26980
|
+
if (!field.duplicate && (field.id === titleId || field.id === subtitleId)) continue;
|
|
26981
|
+
const slot = resolveRecordCardFieldSlot(field);
|
|
26982
|
+
if (slot === "eyebrow-start") groups.eyebrowStart.push(field);
|
|
26983
|
+
else if (slot === "eyebrow-end") groups.eyebrowEnd.push(field);
|
|
26984
|
+
else if (slot === "title-prefix") groups.titlePrefix.push(field);
|
|
26985
|
+
else groups.metadata.push(field);
|
|
26986
|
+
}
|
|
26987
|
+
return groups;
|
|
26988
|
+
}
|
|
26929
26989
|
function resolveRecordPreview(record, index, preview, previewAlt, fields = []) {
|
|
26930
26990
|
if (preview == null) return null;
|
|
26931
26991
|
const candidate = typeof preview === "function" ? preview(record, index) : readRecordSource(record, index, preview, fields, false);
|
|
@@ -26994,21 +27054,49 @@ var zx = (() => {
|
|
|
26994
27054
|
dataset: { failed: String(preview.rejected) }
|
|
26995
27055
|
}, fallback, image);
|
|
26996
27056
|
}
|
|
26997
|
-
function
|
|
26998
|
-
const
|
|
26999
|
-
|
|
27000
|
-
|
|
27001
|
-
|
|
27002
|
-
|
|
27057
|
+
function createEyebrow(record, index, eyebrowStart, eyebrowEnd) {
|
|
27058
|
+
const start = createFieldList(
|
|
27059
|
+
record,
|
|
27060
|
+
index,
|
|
27061
|
+
eyebrowStart,
|
|
27062
|
+
"zx-record-card__eyebrow-group",
|
|
27063
|
+
"eyebrow-start"
|
|
27064
|
+
);
|
|
27065
|
+
const end = createFieldList(
|
|
27066
|
+
record,
|
|
27067
|
+
index,
|
|
27068
|
+
eyebrowEnd,
|
|
27069
|
+
"zx-record-card__eyebrow-group",
|
|
27070
|
+
"eyebrow-end"
|
|
27071
|
+
);
|
|
27072
|
+
return start || end ? h("div", { class: "zx-record-card__eyebrow" }, start, end) : null;
|
|
27073
|
+
}
|
|
27074
|
+
function createFieldList(record, index, fields, className, slot = null) {
|
|
27075
|
+
if (fields.length === 0) return null;
|
|
27076
|
+
return h("dl", {
|
|
27077
|
+
class: className,
|
|
27078
|
+
dataset: slot ? { cardSlot: slot } : void 0
|
|
27079
|
+
}, fields.map((field) => h(
|
|
27003
27080
|
"div",
|
|
27004
27081
|
{
|
|
27005
27082
|
class: "zx-record-card__field",
|
|
27006
|
-
dataset: { fieldId: field.id }
|
|
27083
|
+
dataset: { fieldId: field.id, ...slot ? { cardSlot: slot } : {} }
|
|
27007
27084
|
},
|
|
27008
27085
|
h("dt", { class: "zx-record-card__label" }, field.label),
|
|
27009
27086
|
h("dd", { class: "zx-record-card__value" }, contentNode(renderViewField(field, record, index)))
|
|
27010
27087
|
)));
|
|
27011
27088
|
}
|
|
27089
|
+
function resolveRecordCardFieldSlot(field) {
|
|
27090
|
+
const view = field?.view;
|
|
27091
|
+
if (!view || typeof view !== "object" || Array.isArray(view)) return null;
|
|
27092
|
+
const card = view.card;
|
|
27093
|
+
if (!card || typeof card !== "object" || Array.isArray(card)) return null;
|
|
27094
|
+
const slot = (
|
|
27095
|
+
/** @type {{slot?:unknown}} */
|
|
27096
|
+
card.slot
|
|
27097
|
+
);
|
|
27098
|
+
return slot === "eyebrow-start" || slot === "eyebrow-end" || slot === "title-prefix" ? slot : null;
|
|
27099
|
+
}
|
|
27012
27100
|
function createAction(action) {
|
|
27013
27101
|
const label = String(action.label ?? "");
|
|
27014
27102
|
const title = String(action.title ?? (label || action.id));
|
|
@@ -27078,7 +27166,7 @@ var zx = (() => {
|
|
|
27078
27166
|
try {
|
|
27079
27167
|
const base = globalThis.location?.href ?? "https://zx.invalid/";
|
|
27080
27168
|
const protocol = new URL(href, base).protocol.toLowerCase();
|
|
27081
|
-
return ["http:", "https:", "blob:"
|
|
27169
|
+
return ["http:", "https:", "blob:"].includes(protocol) ? href : null;
|
|
27082
27170
|
} catch {
|
|
27083
27171
|
return null;
|
|
27084
27172
|
}
|
|
@@ -27122,7 +27210,7 @@ var zx = (() => {
|
|
|
27122
27210
|
actions: [],
|
|
27123
27211
|
groupBy: null,
|
|
27124
27212
|
groupOrder: [],
|
|
27125
|
-
minCardWidth: "
|
|
27213
|
+
minCardWidth: "15rem",
|
|
27126
27214
|
maxColumns: null,
|
|
27127
27215
|
variant: "outlined",
|
|
27128
27216
|
loadingCount: 6,
|
|
@@ -27396,7 +27484,7 @@ var zx = (() => {
|
|
|
27396
27484
|
for (const [id, card] of this._cardNodesById) {
|
|
27397
27485
|
const selected = this._isViewSelected(id);
|
|
27398
27486
|
card.dataset.selected = String(selected);
|
|
27399
|
-
if (this.options.selectable) card.setAttribute("aria-
|
|
27487
|
+
if (this.options.selectable) card.setAttribute("aria-description", selected ? "Selected" : "Not selected");
|
|
27400
27488
|
const checkbox = (
|
|
27401
27489
|
/** @type {HTMLInputElement|null} */
|
|
27402
27490
|
card.querySelector("[data-record-selection]")
|
|
@@ -27540,11 +27628,11 @@ var zx = (() => {
|
|
|
27540
27628
|
return value == null || value === "" ? "" : String(value);
|
|
27541
27629
|
}
|
|
27542
27630
|
function normalizeCardWidth(value) {
|
|
27543
|
-
if (typeof value === "number") return Number.isFinite(value) && value > 0 ? `${value}px` : "
|
|
27631
|
+
if (typeof value === "number") return Number.isFinite(value) && value > 0 ? `${value}px` : "15rem";
|
|
27544
27632
|
const text4 = String(value ?? "").trim();
|
|
27545
|
-
if (!text4 || text4.length > 128 || /[;{}\u0000-\u001f\u007f]/.test(text4)) return "
|
|
27546
|
-
if (/(?:url|image|image-set|cross-fade|element|expression)\s*\(/i.test(text4)) return "
|
|
27547
|
-
if (typeof globalThis.CSS?.supports === "function" && !globalThis.CSS.supports("width", text4)) return "
|
|
27633
|
+
if (!text4 || text4.length > 128 || /[;{}\u0000-\u001f\u007f]/.test(text4)) return "15rem";
|
|
27634
|
+
if (/(?:url|image|image-set|cross-fade|element|expression)\s*\(/i.test(text4)) return "15rem";
|
|
27635
|
+
if (typeof globalThis.CSS?.supports === "function" && !globalThis.CSS.supports("width", text4)) return "15rem";
|
|
27548
27636
|
return text4;
|
|
27549
27637
|
}
|
|
27550
27638
|
function normalizeMaxColumns(value) {
|
|
@@ -27582,8 +27670,284 @@ var zx = (() => {
|
|
|
27582
27670
|
return value.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "empty";
|
|
27583
27671
|
}
|
|
27584
27672
|
|
|
27673
|
+
// src/components/kanban-view/kanban-policy.js
|
|
27674
|
+
var RULE_TONES = Object.freeze(["neutral", "accent", "success", "warning", "danger", "info"]);
|
|
27675
|
+
function normalizeKanbanRules(rules) {
|
|
27676
|
+
if (rules == null) return [];
|
|
27677
|
+
if (!Array.isArray(rules)) throw new TypeError("Kanban rules must be an array or null");
|
|
27678
|
+
const seen = /* @__PURE__ */ new Set();
|
|
27679
|
+
return rules.map((rule, index) => {
|
|
27680
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
|
27681
|
+
throw new TypeError(`Kanban rule at index ${index} must be an object`);
|
|
27682
|
+
}
|
|
27683
|
+
const source = (
|
|
27684
|
+
/** @type {Record<string, any>} */
|
|
27685
|
+
rule
|
|
27686
|
+
);
|
|
27687
|
+
const id = String(source.id ?? "").trim();
|
|
27688
|
+
if (!id) throw new TypeError(`Kanban rule at index ${index} requires an id`);
|
|
27689
|
+
if (seen.has(id)) throw new TypeError(`Duplicate Kanban rule: ${id}`);
|
|
27690
|
+
if (typeof source.when !== "function") {
|
|
27691
|
+
throw new TypeError(`Kanban rule ${id} requires a when predicate`);
|
|
27692
|
+
}
|
|
27693
|
+
seen.add(id);
|
|
27694
|
+
return {
|
|
27695
|
+
id,
|
|
27696
|
+
when: source.when,
|
|
27697
|
+
tone: RULE_TONES.includes(source.tone) ? source.tone : "accent",
|
|
27698
|
+
label: source.label == null ? "" : String(source.label),
|
|
27699
|
+
icon: source.icon == null ? null : String(source.icon),
|
|
27700
|
+
description: source.description == null ? "" : String(source.description)
|
|
27701
|
+
};
|
|
27702
|
+
});
|
|
27703
|
+
}
|
|
27704
|
+
function resolveKanbanRules(rules, record, context) {
|
|
27705
|
+
if (!Array.isArray(rules) || !rules.length) return [];
|
|
27706
|
+
return rules.filter((rule) => {
|
|
27707
|
+
try {
|
|
27708
|
+
return rule.when(record, context) === true;
|
|
27709
|
+
} catch {
|
|
27710
|
+
return false;
|
|
27711
|
+
}
|
|
27712
|
+
});
|
|
27713
|
+
}
|
|
27714
|
+
function evaluateKanbanMove(input) {
|
|
27715
|
+
const { records, column, swimlane, context, from } = input;
|
|
27716
|
+
const moving = Math.max(1, records.length);
|
|
27717
|
+
const policy = column?.wipPolicy === "block" || column?.wipPolicy === "warn" ? column.wipPolicy : input.policy === "block" ? "block" : "warn";
|
|
27718
|
+
const refuse = (reason, extra = {}) => ({
|
|
27719
|
+
allowed: false,
|
|
27720
|
+
reason,
|
|
27721
|
+
limit: null,
|
|
27722
|
+
count: 0,
|
|
27723
|
+
limitExceeded: false,
|
|
27724
|
+
limitReached: false,
|
|
27725
|
+
policy,
|
|
27726
|
+
...extra
|
|
27727
|
+
});
|
|
27728
|
+
const blockedTransition = allowsKanbanTransition(column, swimlane, from);
|
|
27729
|
+
if (blockedTransition) return refuse(blockedTransition);
|
|
27730
|
+
for (const record of records) {
|
|
27731
|
+
if (column.accept && column.accept(record, context) === false) return refuse("accept");
|
|
27732
|
+
if (swimlane?.accept && swimlane.accept(record, context) === false) return refuse("lane-accept");
|
|
27733
|
+
}
|
|
27734
|
+
const capacities = [];
|
|
27735
|
+
if (column.limit != null) {
|
|
27736
|
+
capacities.push({ reason: "wip", limit: column.limit, count: input.columnCount + moving });
|
|
27737
|
+
}
|
|
27738
|
+
const cellLimit = swimlane ? readLaneLimit(column.laneLimits, swimlane.id) : null;
|
|
27739
|
+
if (cellLimit != null) {
|
|
27740
|
+
capacities.push({ reason: "wip", limit: cellLimit, count: input.cellCount + moving });
|
|
27741
|
+
}
|
|
27742
|
+
if (swimlane?.limit != null) {
|
|
27743
|
+
capacities.push({ reason: "lane-wip", limit: swimlane.limit, count: input.laneCount + moving });
|
|
27744
|
+
}
|
|
27745
|
+
const violated = capacities.find((capacity) => capacity.count > capacity.limit);
|
|
27746
|
+
const reported = violated ?? capacities.find((capacity) => capacity.count === capacity.limit) ?? capacities[0];
|
|
27747
|
+
if (violated && policy === "block") {
|
|
27748
|
+
return refuse(violated.reason, {
|
|
27749
|
+
limit: violated.limit,
|
|
27750
|
+
count: violated.count,
|
|
27751
|
+
limitExceeded: true
|
|
27752
|
+
});
|
|
27753
|
+
}
|
|
27754
|
+
return {
|
|
27755
|
+
allowed: true,
|
|
27756
|
+
reason: null,
|
|
27757
|
+
limit: reported ? reported.limit : null,
|
|
27758
|
+
count: reported ? reported.count : 0,
|
|
27759
|
+
limitExceeded: Boolean(violated),
|
|
27760
|
+
limitReached: Boolean(reported && !violated && reported.count === reported.limit),
|
|
27761
|
+
policy
|
|
27762
|
+
};
|
|
27763
|
+
}
|
|
27764
|
+
function allowsKanbanTransition(column, swimlane, from) {
|
|
27765
|
+
if (column.id !== from.column && !allowsOrigin(column.from, from.column)) return "transition";
|
|
27766
|
+
if (swimlane && swimlane.id !== from.lane && !allowsOrigin(swimlane.from, from.lane)) {
|
|
27767
|
+
return "lane-transition";
|
|
27768
|
+
}
|
|
27769
|
+
return null;
|
|
27770
|
+
}
|
|
27771
|
+
function kanbanSearchTerms(search) {
|
|
27772
|
+
const text4 = search == null ? "" : String(search).trim().toLowerCase();
|
|
27773
|
+
if (!text4) return [];
|
|
27774
|
+
return [...text4.matchAll(/"([^"]*)"|(\S+)/g)].map((match) => (match[1] ?? match[2] ?? "").trim()).filter(Boolean);
|
|
27775
|
+
}
|
|
27776
|
+
function matchesKanbanSearch(text4, terms) {
|
|
27777
|
+
if (!terms.length) return true;
|
|
27778
|
+
const haystack = text4.toLowerCase();
|
|
27779
|
+
return terms.every((term) => haystack.includes(term));
|
|
27780
|
+
}
|
|
27781
|
+
function createKanbanHistory(limit = 50) {
|
|
27782
|
+
const max = Number.isFinite(Number(limit)) && Number(limit) > 0 ? Math.floor(Number(limit)) : 50;
|
|
27783
|
+
let undoStack = [];
|
|
27784
|
+
let redoStack = [];
|
|
27785
|
+
return {
|
|
27786
|
+
push(entry) {
|
|
27787
|
+
undoStack.push(entry);
|
|
27788
|
+
if (undoStack.length > max) undoStack = undoStack.slice(-max);
|
|
27789
|
+
redoStack = [];
|
|
27790
|
+
},
|
|
27791
|
+
undo(current) {
|
|
27792
|
+
const entry = undoStack.pop();
|
|
27793
|
+
if (!entry) return null;
|
|
27794
|
+
redoStack.push(current);
|
|
27795
|
+
if (redoStack.length > max) redoStack = redoStack.slice(-max);
|
|
27796
|
+
return entry;
|
|
27797
|
+
},
|
|
27798
|
+
redo(current) {
|
|
27799
|
+
const entry = redoStack.pop();
|
|
27800
|
+
if (!entry) return null;
|
|
27801
|
+
undoStack.push(current);
|
|
27802
|
+
if (undoStack.length > max) undoStack = undoStack.slice(-max);
|
|
27803
|
+
return entry;
|
|
27804
|
+
},
|
|
27805
|
+
canUndo: () => undoStack.length > 0,
|
|
27806
|
+
canRedo: () => redoStack.length > 0,
|
|
27807
|
+
clear() {
|
|
27808
|
+
undoStack = [];
|
|
27809
|
+
redoStack = [];
|
|
27810
|
+
},
|
|
27811
|
+
depth: () => ({ undo: undoStack.length, redo: redoStack.length })
|
|
27812
|
+
};
|
|
27813
|
+
}
|
|
27814
|
+
function reorderKanbanRecords(records, entries, options) {
|
|
27815
|
+
if (!Array.isArray(records) || !Array.isArray(entries) || !entries.length) return null;
|
|
27816
|
+
const sources = entries.map((entry) => ({
|
|
27817
|
+
...entry,
|
|
27818
|
+
index: records.findIndex((record) => Object.is(viewRecordId(record, options.recordId), entry.id))
|
|
27819
|
+
}));
|
|
27820
|
+
if (sources.some((source) => source.index < 0 || !source.record || typeof source.record !== "object")) {
|
|
27821
|
+
return null;
|
|
27822
|
+
}
|
|
27823
|
+
sources.sort((left, right) => left.index - right.index);
|
|
27824
|
+
const moves = sources.map((source) => ({
|
|
27825
|
+
id: source.id,
|
|
27826
|
+
from: locateKanbanRecord(records, source.index, options)
|
|
27827
|
+
}));
|
|
27828
|
+
const movingIndices = new Set(sources.map((source) => source.index));
|
|
27829
|
+
const next = records.filter((_, index) => !movingIndices.has(index));
|
|
27830
|
+
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));
|
|
27831
|
+
const targetIndex = clampKanbanIndex(options.destination.index, destinationRecords.length);
|
|
27832
|
+
let insertion = next.length;
|
|
27833
|
+
if (destinationRecords[targetIndex]) insertion = next.indexOf(destinationRecords[targetIndex]);
|
|
27834
|
+
else if (destinationRecords.length) insertion = next.indexOf(destinationRecords.at(-1)) + 1;
|
|
27835
|
+
next.splice(insertion, 0, ...sources.map((source) => source.record));
|
|
27836
|
+
return {
|
|
27837
|
+
records: next,
|
|
27838
|
+
moves,
|
|
27839
|
+
to: {
|
|
27840
|
+
column: options.destination.column,
|
|
27841
|
+
lane: options.swimlaneBy == null ? null : options.destination.lane,
|
|
27842
|
+
index: targetIndex
|
|
27843
|
+
}
|
|
27844
|
+
};
|
|
27845
|
+
}
|
|
27846
|
+
function locateKanbanRecord(records, index, options) {
|
|
27847
|
+
const record = records[index];
|
|
27848
|
+
const column = normalizeAxisId(readKanbanAccessor(record, index, options.columnBy));
|
|
27849
|
+
const lane = options.swimlaneBy == null ? null : normalizeAxisId(readKanbanAccessor(record, index, options.swimlaneBy));
|
|
27850
|
+
const group = records.filter((candidate, candidateIndex) => normalizeAxisId(readKanbanAccessor(candidate, candidateIndex, options.columnBy)) === column && (options.swimlaneBy == null || normalizeAxisId(readKanbanAccessor(candidate, candidateIndex, options.swimlaneBy)) === lane));
|
|
27851
|
+
const id = viewRecordId(record, options.recordId);
|
|
27852
|
+
return {
|
|
27853
|
+
column,
|
|
27854
|
+
lane,
|
|
27855
|
+
index: group.findIndex((candidate) => Object.is(viewRecordId(candidate, options.recordId), id))
|
|
27856
|
+
};
|
|
27857
|
+
}
|
|
27858
|
+
function absoluteKanbanIndex(visibility, visibleIndex) {
|
|
27859
|
+
const target = clampKanbanIndex(visibleIndex, visibility.filter(Boolean).length);
|
|
27860
|
+
let seen = 0;
|
|
27861
|
+
for (let index = 0; index < visibility.length; index += 1) {
|
|
27862
|
+
if (!visibility[index]) continue;
|
|
27863
|
+
if (seen === target) return index;
|
|
27864
|
+
seen += 1;
|
|
27865
|
+
}
|
|
27866
|
+
return visibility.length;
|
|
27867
|
+
}
|
|
27868
|
+
function readKanbanAccessor(record, index, accessor) {
|
|
27869
|
+
return typeof accessor === "function" ? accessor(record, index) : accessor == null ? null : record?.[accessor];
|
|
27870
|
+
}
|
|
27871
|
+
function normalizeAxisId(value) {
|
|
27872
|
+
return value == null ? "" : String(value);
|
|
27873
|
+
}
|
|
27874
|
+
function clampKanbanIndex(value, maximum) {
|
|
27875
|
+
if (value == null || !Number.isFinite(Number(value))) return maximum;
|
|
27876
|
+
return Math.max(0, Math.min(maximum, Math.trunc(Number(value))));
|
|
27877
|
+
}
|
|
27878
|
+
function allowsOrigin(allowed, origin) {
|
|
27879
|
+
if (allowed == null) return true;
|
|
27880
|
+
if (!Array.isArray(allowed)) return true;
|
|
27881
|
+
if (!allowed.length) return false;
|
|
27882
|
+
return allowed.some((entry) => String(entry) === "*" || String(entry) === normalizeAxisId(origin));
|
|
27883
|
+
}
|
|
27884
|
+
function readLaneLimit(laneLimits, lane) {
|
|
27885
|
+
if (!laneLimits || typeof laneLimits !== "object") return null;
|
|
27886
|
+
const value = (
|
|
27887
|
+
/** @type {Record<string, unknown>} */
|
|
27888
|
+
laneLimits[lane]
|
|
27889
|
+
);
|
|
27890
|
+
if (value == null || value === "") return null;
|
|
27891
|
+
const number2 = Number(value);
|
|
27892
|
+
return Number.isFinite(number2) && number2 >= 0 ? Math.floor(number2) : null;
|
|
27893
|
+
}
|
|
27894
|
+
|
|
27585
27895
|
// src/components/kanban-view/kanban-view.js
|
|
27586
27896
|
var INTERACTIVE = 'a, button, input, select, textarea, summary, [contenteditable="true"]';
|
|
27897
|
+
var DRAG_THRESHOLD = 4;
|
|
27898
|
+
var TOUCH_HOLD = 350;
|
|
27899
|
+
var SEARCH_DEBOUNCE = 120;
|
|
27900
|
+
var SCROLL_EDGE = 56;
|
|
27901
|
+
var SCROLL_SPEED = 18;
|
|
27902
|
+
var DEFAULT_LABELS = Object.freeze({
|
|
27903
|
+
fields: "Fields",
|
|
27904
|
+
search: "Search board",
|
|
27905
|
+
searchPlaceholder: "Search cards",
|
|
27906
|
+
clearSearch: "Clear search",
|
|
27907
|
+
searchSummary: "%count% of %total% cards match.",
|
|
27908
|
+
undo: "Undo",
|
|
27909
|
+
redo: "Redo",
|
|
27910
|
+
add: "Add card",
|
|
27911
|
+
addTo: "Add card to %target%",
|
|
27912
|
+
move: "Move %name%",
|
|
27913
|
+
moveMultiple: "Move %count% selected cards",
|
|
27914
|
+
collapseColumn: "Collapse column %label%",
|
|
27915
|
+
expandColumn: "Expand column %label%",
|
|
27916
|
+
collapseLane: "Collapse swim lane %label%",
|
|
27917
|
+
expandLane: "Expand swim lane %label%",
|
|
27918
|
+
count: "%count% records",
|
|
27919
|
+
countFiltered: "%count% of %total% records",
|
|
27920
|
+
countLimit: "%count% records, work-in-progress limit %limit%",
|
|
27921
|
+
countLimitInLane: "%count% records in this lane, %total% of work-in-progress limit %limit% in the column",
|
|
27922
|
+
empty: "No records",
|
|
27923
|
+
columnEmpty: "",
|
|
27924
|
+
selected: "Selected",
|
|
27925
|
+
notSelected: "Not selected",
|
|
27926
|
+
noMatches: "No cards match the current search.",
|
|
27927
|
+
record: "record",
|
|
27928
|
+
grabbed: "Grabbed %name%. Use left and right for columns, up and down for position, and Alt plus up or down for swim lanes.",
|
|
27929
|
+
grabbedMultiple: "Grabbed %count% cards. Use left and right for columns, up and down for position, and Alt plus up or down for swim lanes.",
|
|
27930
|
+
target: "Target %target%, position %position% of %total%.",
|
|
27931
|
+
moved: "Moved %name% to %target%.%wip%",
|
|
27932
|
+
movedMultiple: "Moved %count% cards to %target%.%wip%",
|
|
27933
|
+
moveRequested: "Move requested for %name% to %target%.%wip%",
|
|
27934
|
+
moveCanceled: "Move canceled for %name%.",
|
|
27935
|
+
moveCanceledPlain: "Move canceled.",
|
|
27936
|
+
limitReached: " Work-in-progress limit %limit% reached.",
|
|
27937
|
+
limitExceeded: " Work-in-progress limit %limit% exceeded.",
|
|
27938
|
+
rejectAccept: "Move rejected: %target% does not accept %name%.",
|
|
27939
|
+
rejectTransition: "Move rejected: %target% does not accept cards from %origin%.",
|
|
27940
|
+
rejectWip: "Move rejected: %target% is at its work-in-progress limit of %limit%.",
|
|
27941
|
+
rejectDestination: "Move rejected: the destination is not available.",
|
|
27942
|
+
rejectGrouping: "Move rejected: callback grouping requires external move mode.",
|
|
27943
|
+
undone: "Move undone.",
|
|
27944
|
+
redone: "Move redone.",
|
|
27945
|
+
nothingToUndo: "Nothing to undo.",
|
|
27946
|
+
nothingToRedo: "Nothing to redo.",
|
|
27947
|
+
menuMoveTo: "Move to %target%",
|
|
27948
|
+
menuMoveToLane: "Move to lane %target%",
|
|
27949
|
+
dragPreview: "%count% cards"
|
|
27950
|
+
});
|
|
27587
27951
|
var KanbanView = class extends RecordView {
|
|
27588
27952
|
static cssName = "kanban-view";
|
|
27589
27953
|
/** @type {Readonly<KanbanViewOptions & RecordViewOptions>} */
|
|
@@ -27600,6 +27964,27 @@ var zx = (() => {
|
|
|
27600
27964
|
link: null,
|
|
27601
27965
|
actions: [],
|
|
27602
27966
|
moveMode: "local",
|
|
27967
|
+
dragFrom: "card",
|
|
27968
|
+
multiMove: true,
|
|
27969
|
+
autoScroll: true,
|
|
27970
|
+
wipPolicy: "warn",
|
|
27971
|
+
rules: [],
|
|
27972
|
+
search: "",
|
|
27973
|
+
searchFields: null,
|
|
27974
|
+
filter: null,
|
|
27975
|
+
searchControl: false,
|
|
27976
|
+
history: true,
|
|
27977
|
+
historyLimit: 50,
|
|
27978
|
+
historyControls: false,
|
|
27979
|
+
columnHeight: null,
|
|
27980
|
+
allowAdd: false,
|
|
27981
|
+
contextMenu: false,
|
|
27982
|
+
renderCard: null,
|
|
27983
|
+
renderColumnHeader: null,
|
|
27984
|
+
renderSwimlaneHeader: null,
|
|
27985
|
+
renderColumnEmpty: null,
|
|
27986
|
+
renderDragPreview: null,
|
|
27987
|
+
labels: {},
|
|
27603
27988
|
columnOrder: [],
|
|
27604
27989
|
swimlaneOrder: [],
|
|
27605
27990
|
collapsedColumns: [],
|
|
@@ -27635,17 +28020,45 @@ var zx = (() => {
|
|
|
27635
28020
|
this._kanbanSwimlaneOrder = normalizeOrder(options.swimlaneOrder);
|
|
27636
28021
|
this._collapsedColumns = new Set(normalizeOrder(options.collapsedColumns));
|
|
27637
28022
|
this._collapsedSwimlanes = new Set(normalizeOrder(options.collapsedSwimlanes));
|
|
28023
|
+
this._kanbanRules = normalizeKanbanRules(options.rules);
|
|
28024
|
+
this._kanbanSearch = options.search == null ? "" : String(options.search);
|
|
28025
|
+
this._kanbanFilter = typeof options.filter === "function" ? options.filter : null;
|
|
28026
|
+
this._kanbanHistory = createKanbanHistory(options.historyLimit);
|
|
27638
28027
|
this._keyboardMove = /** @type {KanbanKeyboardMove|null} */
|
|
27639
28028
|
null;
|
|
27640
|
-
this.
|
|
28029
|
+
this._dragIds = /** @type {unknown[]} */
|
|
28030
|
+
[];
|
|
27641
28031
|
this._dragDestination = /** @type {KanbanMovePoint|null} */
|
|
27642
28032
|
null;
|
|
28033
|
+
this._pointerStart = /** @type {{x:number,y:number,id:number,ids:unknown[],touch:boolean}|null} */
|
|
28034
|
+
null;
|
|
28035
|
+
this._pointerHold = 0;
|
|
28036
|
+
this._dragFrame = 0;
|
|
28037
|
+
this._dragPreview = /** @type {HTMLElement|null} */
|
|
28038
|
+
null;
|
|
28039
|
+
this._dragPointer = /** @type {number|null} */
|
|
28040
|
+
null;
|
|
28041
|
+
this._dragCaptured = false;
|
|
28042
|
+
this._searchTimer = 0;
|
|
28043
|
+
this._dragPoint = { x: 0, y: 0 };
|
|
28044
|
+
this._suppressClick = false;
|
|
27643
28045
|
this._announcementToken = 0;
|
|
27644
28046
|
this._cardMeta = /* @__PURE__ */ new WeakMap();
|
|
27645
28047
|
this._sectionMeta = /* @__PURE__ */ new WeakMap();
|
|
27646
28048
|
this._cardsById = /* @__PURE__ */ new Map();
|
|
28049
|
+
this._visibleIds = /* @__PURE__ */ new Set();
|
|
28050
|
+
this._kanbanMenu = /** @type {ContextMenu|null} */
|
|
28051
|
+
null;
|
|
28052
|
+
this._searchInput = /** @type {HTMLInputElement|null} */
|
|
28053
|
+
null;
|
|
28054
|
+
this._undoButton = /** @type {HTMLButtonElement|null} */
|
|
28055
|
+
null;
|
|
28056
|
+
this._redoButton = /** @type {HTMLButtonElement|null} */
|
|
28057
|
+
null;
|
|
27647
28058
|
this._toolbar = h("div", { class: "zx-record-view__toolbar zx-kanban-view__toolbar" });
|
|
27648
|
-
if (options.fieldControls) this._toolbar.append(this._createViewFieldControls());
|
|
28059
|
+
if (options.fieldControls) this._toolbar.append(this._createViewFieldControls(this._text("fields")));
|
|
28060
|
+
if (options.searchControl) this._toolbar.append(this._createSearchControl());
|
|
28061
|
+
if (options.historyControls) this._toolbar.append(this._createHistoryControls());
|
|
27649
28062
|
this._board = h("div", {
|
|
27650
28063
|
class: "zx-kanban-view__board",
|
|
27651
28064
|
role: "region",
|
|
@@ -27658,6 +28071,10 @@ var zx = (() => {
|
|
|
27658
28071
|
ariaAtomic: "true"
|
|
27659
28072
|
});
|
|
27660
28073
|
root.replaceChildren(this._toolbar, this._board, this._live);
|
|
28074
|
+
if (options.columnHeight) {
|
|
28075
|
+
root.style.setProperty("--zx-kanban-column-height", String(options.columnHeight));
|
|
28076
|
+
root.dataset.columnScroll = "true";
|
|
28077
|
+
}
|
|
27661
28078
|
this.listen(this._board, "click", (event) => this._handleBoardClick(
|
|
27662
28079
|
/** @type {MouseEvent} */
|
|
27663
28080
|
event
|
|
@@ -27671,20 +28088,22 @@ var zx = (() => {
|
|
|
27671
28088
|
event
|
|
27672
28089
|
));
|
|
27673
28090
|
this.listen(this._board, "error", (event) => this._handlePreviewError(event), { capture: true });
|
|
27674
|
-
this.listen(this._board, "
|
|
27675
|
-
/** @type {
|
|
28091
|
+
this.listen(this._board, "pointerdown", (event) => this._handlePointerDown(
|
|
28092
|
+
/** @type {PointerEvent} */
|
|
27676
28093
|
event
|
|
27677
28094
|
));
|
|
27678
|
-
this.listen(this._board, "
|
|
27679
|
-
/** @type {
|
|
28095
|
+
this.listen(this._board, "pointermove", (event) => this._handlePointerMove(
|
|
28096
|
+
/** @type {PointerEvent} */
|
|
27680
28097
|
event
|
|
27681
28098
|
));
|
|
27682
|
-
this.listen(this._board, "
|
|
27683
|
-
/** @type {
|
|
28099
|
+
this.listen(this._board, "pointerup", (event) => this._handlePointerUp(
|
|
28100
|
+
/** @type {PointerEvent} */
|
|
27684
28101
|
event
|
|
27685
28102
|
));
|
|
27686
|
-
this.listen(this._board, "
|
|
28103
|
+
this.listen(this._board, "pointercancel", () => this._clearDrag());
|
|
28104
|
+
this.listen(this._board, "dragstart", (event) => event.preventDefault());
|
|
27687
28105
|
this._refreshView("init");
|
|
28106
|
+
if (options.contextMenu) this._createContextMenu();
|
|
27688
28107
|
return root;
|
|
27689
28108
|
} catch (error2) {
|
|
27690
28109
|
if (this._kanbanSnapshot) restoreTarget(root, this._kanbanSnapshot);
|
|
@@ -27702,7 +28121,16 @@ var zx = (() => {
|
|
|
27702
28121
|
*/
|
|
27703
28122
|
setColumns(columns) {
|
|
27704
28123
|
this._kanbanColumns = columns === null ? null : normalizeKanbanDescriptors(columns, "column");
|
|
27705
|
-
|
|
28124
|
+
const descriptors = this._rawColumns();
|
|
28125
|
+
this._kanbanColumnOrder = reconcileKanbanPreference(
|
|
28126
|
+
descriptors,
|
|
28127
|
+
this._kanbanColumnOrder,
|
|
28128
|
+
this._kanbanColumns === null
|
|
28129
|
+
);
|
|
28130
|
+
if (this._kanbanColumns !== null) {
|
|
28131
|
+
const ids = new Set(descriptors.map((column) => column.id));
|
|
28132
|
+
this._collapsedColumns = new Set([...this._collapsedColumns].filter((id) => ids.has(id)));
|
|
28133
|
+
}
|
|
27706
28134
|
this._refreshView("columns");
|
|
27707
28135
|
this._emitKanbanState("columns");
|
|
27708
28136
|
return this;
|
|
@@ -27718,7 +28146,17 @@ var zx = (() => {
|
|
|
27718
28146
|
*/
|
|
27719
28147
|
setSwimlanes(lanes) {
|
|
27720
28148
|
this._kanbanSwimlanes = lanes === null ? null : normalizeKanbanDescriptors(lanes, "swimlane");
|
|
27721
|
-
|
|
28149
|
+
const descriptors = this._rawSwimlanes();
|
|
28150
|
+
const preserveUnknown = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
28151
|
+
this._kanbanSwimlaneOrder = reconcileKanbanPreference(
|
|
28152
|
+
descriptors,
|
|
28153
|
+
this._kanbanSwimlaneOrder,
|
|
28154
|
+
preserveUnknown
|
|
28155
|
+
);
|
|
28156
|
+
if (!preserveUnknown) {
|
|
28157
|
+
const ids = new Set(descriptors.map((lane) => lane.id));
|
|
28158
|
+
this._collapsedSwimlanes = new Set([...this._collapsedSwimlanes].filter((id) => ids.has(id)));
|
|
28159
|
+
}
|
|
27722
28160
|
this._refreshView("swimlanes");
|
|
27723
28161
|
this._emitKanbanState("swimlanes");
|
|
27724
28162
|
return this;
|
|
@@ -27734,9 +28172,16 @@ var zx = (() => {
|
|
|
27734
28172
|
* @returns {this}
|
|
27735
28173
|
*/
|
|
27736
28174
|
setColumnOrder(order, options = {}) {
|
|
27737
|
-
const
|
|
27738
|
-
|
|
27739
|
-
|
|
28175
|
+
const descriptors = this._rawColumns();
|
|
28176
|
+
const current = this.getColumnOrder();
|
|
28177
|
+
const next = reconcileKanbanOrder(descriptors, order);
|
|
28178
|
+
if (sameArray3(next, current)) return this;
|
|
28179
|
+
this._kanbanColumnOrder = mergeKanbanResolvedOrder(
|
|
28180
|
+
this._kanbanColumnOrder,
|
|
28181
|
+
descriptors,
|
|
28182
|
+
next,
|
|
28183
|
+
this._kanbanColumns === null
|
|
28184
|
+
);
|
|
27740
28185
|
this._refreshView("columnorder");
|
|
27741
28186
|
if (!options.silent) this._emitKanbanState("columnorder");
|
|
27742
28187
|
return this;
|
|
@@ -27762,9 +28207,17 @@ var zx = (() => {
|
|
|
27762
28207
|
* @returns {this}
|
|
27763
28208
|
*/
|
|
27764
28209
|
setSwimlaneOrder(order, options = {}) {
|
|
27765
|
-
const
|
|
27766
|
-
|
|
27767
|
-
|
|
28210
|
+
const descriptors = this._rawSwimlanes();
|
|
28211
|
+
const current = this.getSwimlaneOrder();
|
|
28212
|
+
const next = reconcileKanbanOrder(descriptors, order);
|
|
28213
|
+
if (sameArray3(next, current)) return this;
|
|
28214
|
+
const preserveUnknown = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
28215
|
+
this._kanbanSwimlaneOrder = mergeKanbanResolvedOrder(
|
|
28216
|
+
this._kanbanSwimlaneOrder,
|
|
28217
|
+
descriptors,
|
|
28218
|
+
next,
|
|
28219
|
+
preserveUnknown
|
|
28220
|
+
);
|
|
27768
28221
|
this._refreshView("swimlaneorder");
|
|
27769
28222
|
if (!options.silent) this._emitKanbanState("swimlaneorder");
|
|
27770
28223
|
return this;
|
|
@@ -27832,91 +28285,244 @@ var zx = (() => {
|
|
|
27832
28285
|
return this.setSwimlaneCollapsed(id, !this._collapsedSwimlanes.has(String(id)));
|
|
27833
28286
|
}
|
|
27834
28287
|
/**
|
|
27835
|
-
* Proposes and, in local mode, commits a record move.
|
|
27836
|
-
* a cancelable `recordmove` event. WIP limits are reported but remain advisory.
|
|
28288
|
+
* Proposes and, in local mode, commits a record move. Equivalent to `moveRecords([id], …)`.
|
|
27837
28289
|
* @param {unknown} id Record id.
|
|
27838
28290
|
* @param {KanbanDestination} destination Destination.
|
|
27839
28291
|
* @returns {this}
|
|
27840
28292
|
* @fires KanbanView#recordmove
|
|
28293
|
+
* @fires KanbanView#movereject
|
|
27841
28294
|
* @fires RecordView#datachange
|
|
27842
28295
|
*/
|
|
27843
28296
|
moveRecord(id, destination) {
|
|
28297
|
+
return this.moveRecords([id], destination);
|
|
28298
|
+
}
|
|
28299
|
+
/**
|
|
28300
|
+
* Proposes and, in local mode, commits an atomic move of one or more records. Destination
|
|
28301
|
+
* eligibility, transition rules, and work-in-progress policy are all checked before a single
|
|
28302
|
+
* cancelable `recordmove` event; the move commits completely or not at all, and the moving
|
|
28303
|
+
* records keep their relative order at the destination.
|
|
28304
|
+
* @param {unknown[]} ids Record ids, in any order.
|
|
28305
|
+
* @param {KanbanDestination} destination Destination.
|
|
28306
|
+
* @returns {this}
|
|
28307
|
+
* @fires KanbanView#recordmove
|
|
28308
|
+
* @fires KanbanView#movereject
|
|
28309
|
+
* @fires RecordView#datachange
|
|
28310
|
+
*/
|
|
28311
|
+
moveRecords(ids, destination) {
|
|
27844
28312
|
const options = this._kanbanOptions();
|
|
27845
|
-
|
|
27846
|
-
const
|
|
27847
|
-
if (!
|
|
28313
|
+
if (!Array.isArray(ids) || !destination || typeof destination !== "object") return this;
|
|
28314
|
+
const moving = this._orderedIds(ids);
|
|
28315
|
+
if (!moving.length) return this;
|
|
28316
|
+
const records = moving.map((id) => (
|
|
28317
|
+
/** @type {KanbanRecord} */
|
|
28318
|
+
this.getRecord(id)
|
|
28319
|
+
));
|
|
28320
|
+
const origins = moving.map((id) => (
|
|
28321
|
+
/** @type {KanbanMovePoint} */
|
|
28322
|
+
this._recordLocation(id)
|
|
28323
|
+
));
|
|
28324
|
+
const from = origins[0];
|
|
27848
28325
|
const columns = this._resolvedColumns();
|
|
27849
28326
|
const lanes = this._resolvedSwimlanes();
|
|
27850
28327
|
const columnId = destination.column == null ? from.column : String(destination.column);
|
|
27851
28328
|
const laneId = options.swimlaneBy == null ? null : destination.lane === void 0 ? from.lane : normalizeAxisId(destination.lane);
|
|
27852
28329
|
const column = columns.find((candidate) => candidate.id === columnId);
|
|
27853
|
-
const lane = options.swimlaneBy == null ? null : lanes.find((candidate) => candidate.id === laneId);
|
|
28330
|
+
const lane = options.swimlaneBy == null ? null : lanes.find((candidate) => candidate.id === laneId) ?? null;
|
|
27854
28331
|
if (!column || options.swimlaneBy != null && !lane) {
|
|
27855
|
-
this.
|
|
28332
|
+
this._rejectMove("destination", { ids: moving, records, from, to: null, column: null, swimlane: null });
|
|
27856
28333
|
return this;
|
|
27857
28334
|
}
|
|
27858
|
-
const
|
|
27859
|
-
const
|
|
27860
|
-
const index =
|
|
28335
|
+
const excluded = new Set(moving);
|
|
28336
|
+
const cellCount = this._recordsIn(columnId, laneId).filter((entry) => !excluded.has(this._viewRecordId(entry.record))).length;
|
|
28337
|
+
const index = clampKanbanIndex(destination.index, cellCount);
|
|
27861
28338
|
const to = { column: columnId, lane: laneId, index };
|
|
27862
|
-
if (sameMovePoint(from, to)) return this;
|
|
27863
|
-
const context = {
|
|
27864
|
-
|
|
27865
|
-
|
|
28339
|
+
if (moving.length === 1 && sameMovePoint(from, to)) return this;
|
|
28340
|
+
const context = {
|
|
28341
|
+
id: moving[0],
|
|
28342
|
+
from: { ...from },
|
|
28343
|
+
to: { ...to },
|
|
28344
|
+
column: { ...column },
|
|
28345
|
+
swimlane: lane ? { ...lane } : null
|
|
28346
|
+
};
|
|
28347
|
+
const evaluation = this._evaluateMove(records, origins, column, lane, to, context, excluded);
|
|
28348
|
+
if (!evaluation.allowed) {
|
|
28349
|
+
this._rejectMove(
|
|
28350
|
+
/** @type {KanbanRejectReason} */
|
|
28351
|
+
evaluation.reason,
|
|
28352
|
+
{
|
|
28353
|
+
ids: moving,
|
|
28354
|
+
records,
|
|
28355
|
+
from,
|
|
28356
|
+
to,
|
|
28357
|
+
column,
|
|
28358
|
+
swimlane: lane,
|
|
28359
|
+
evaluation
|
|
28360
|
+
}
|
|
28361
|
+
);
|
|
27866
28362
|
return this;
|
|
27867
28363
|
}
|
|
27868
|
-
|
|
27869
|
-
|
|
27870
|
-
|
|
27871
|
-
|
|
27872
|
-
|
|
28364
|
+
const entries = [];
|
|
28365
|
+
for (let position2 = 0; position2 < moving.length; position2 += 1) {
|
|
28366
|
+
const record = records[position2];
|
|
28367
|
+
const moved = options.moveMode === "local" ? this._localMovedRecord(record, origins[position2], to, column, lane) : record;
|
|
28368
|
+
if (!moved) {
|
|
28369
|
+
this._rejectMove("grouping", { ids: moving, records, from, to, column, swimlane: lane });
|
|
27873
28370
|
return this;
|
|
27874
28371
|
}
|
|
28372
|
+
entries.push({ id: moving[position2], record: moved });
|
|
27875
28373
|
}
|
|
27876
|
-
const next =
|
|
28374
|
+
const next = reorderKanbanRecords(this._viewData, entries, {
|
|
27877
28375
|
recordId: options.recordId,
|
|
27878
28376
|
columnBy: (candidate, candidateIndex) => this._readAxis(options.columnBy, candidate, candidateIndex),
|
|
27879
28377
|
swimlaneBy: options.swimlaneBy == null ? null : (candidate, candidateIndex) => this._readAxis(options.swimlaneBy, candidate, candidateIndex),
|
|
27880
28378
|
destination: to
|
|
27881
28379
|
});
|
|
27882
28380
|
if (!next) {
|
|
27883
|
-
this.
|
|
28381
|
+
this._rejectMove("destination", { ids: moving, records, from, to, column, swimlane: lane });
|
|
27884
28382
|
return this;
|
|
27885
28383
|
}
|
|
27886
|
-
const
|
|
27887
|
-
const limitExceeded = column.limit != null && finalCount > column.limit;
|
|
28384
|
+
const wip = this._wipAnnouncement(evaluation);
|
|
27888
28385
|
const moveEvent = this.emit("recordmove", {
|
|
27889
|
-
record,
|
|
27890
|
-
id,
|
|
28386
|
+
record: records[0],
|
|
28387
|
+
id: moving[0],
|
|
28388
|
+
records: [...records],
|
|
28389
|
+
ids: [...moving],
|
|
28390
|
+
moves: next.moves.map((move) => ({ id: move.id, from: { ...move.from } })),
|
|
27891
28391
|
from: { ...from },
|
|
27892
28392
|
to: { ...next.to },
|
|
27893
28393
|
column: { ...column },
|
|
27894
28394
|
swimlane: lane ? { ...lane } : null,
|
|
27895
|
-
limitExceeded
|
|
27896
|
-
});
|
|
28395
|
+
limitExceeded: evaluation.limitExceeded
|
|
28396
|
+
}, { honorDomCancellation: true });
|
|
27897
28397
|
if (moveEvent.defaultPrevented) {
|
|
27898
|
-
this._announce(
|
|
28398
|
+
this._announce(this._text("moveCanceled", { name: this._movingName(records) }));
|
|
27899
28399
|
return this;
|
|
27900
28400
|
}
|
|
27901
28401
|
if (options.moveMode === "external") {
|
|
27902
|
-
this._announce(
|
|
28402
|
+
this._announce(this._text("moveRequested", {
|
|
28403
|
+
name: this._movingName(records),
|
|
28404
|
+
target: this._targetName(next.to),
|
|
28405
|
+
wip
|
|
28406
|
+
}));
|
|
27903
28407
|
return this;
|
|
27904
28408
|
}
|
|
27905
|
-
this.
|
|
27906
|
-
this.
|
|
27907
|
-
|
|
27908
|
-
|
|
27909
|
-
|
|
28409
|
+
this._commitLocalRecords(next.records);
|
|
28410
|
+
this._announce(moving.length > 1 ? this._text("movedMultiple", { count: moving.length, target: this._targetName(next.to), wip }) : this._text("moved", {
|
|
28411
|
+
name: this._recordName(entries[0].record, next.to.index),
|
|
28412
|
+
target: this._targetName(next.to),
|
|
28413
|
+
wip
|
|
28414
|
+
}));
|
|
28415
|
+
queueMicrotask(() => this._moveHandle(moving[0])?.focus());
|
|
28416
|
+
return this;
|
|
28417
|
+
}
|
|
28418
|
+
/** Whether a committed local move can be reverted. @returns {boolean} */
|
|
28419
|
+
canUndo() {
|
|
28420
|
+
return this._kanbanOptions().history !== false && this._kanbanHistory.canUndo();
|
|
28421
|
+
}
|
|
28422
|
+
/** Whether a reverted local move can be reapplied. @returns {boolean} */
|
|
28423
|
+
canRedo() {
|
|
28424
|
+
return this._kanbanOptions().history !== false && this._kanbanHistory.canRedo();
|
|
28425
|
+
}
|
|
28426
|
+
/**
|
|
28427
|
+
* Reverts the last committed local move, restoring the exact record order and selection.
|
|
28428
|
+
* @returns {this}
|
|
28429
|
+
* @fires RecordView#datachange
|
|
28430
|
+
* @fires KanbanView#historychange
|
|
28431
|
+
*/
|
|
28432
|
+
undo() {
|
|
28433
|
+
return this._travelHistory("undo");
|
|
28434
|
+
}
|
|
28435
|
+
/**
|
|
28436
|
+
* Reapplies the last reverted local move.
|
|
28437
|
+
* @returns {this}
|
|
28438
|
+
* @fires RecordView#datachange
|
|
28439
|
+
* @fires KanbanView#historychange
|
|
28440
|
+
*/
|
|
28441
|
+
redo() {
|
|
28442
|
+
return this._travelHistory("redo");
|
|
28443
|
+
}
|
|
28444
|
+
/** Empties both history stacks. @returns {this} @fires KanbanView#historychange */
|
|
28445
|
+
clearHistory() {
|
|
28446
|
+
this._kanbanHistory.clear();
|
|
28447
|
+
this._syncHistoryControls();
|
|
28448
|
+
this._emitHistory();
|
|
28449
|
+
return this;
|
|
28450
|
+
}
|
|
28451
|
+
/** Returns the current board search text. @returns {string} */
|
|
28452
|
+
getSearch() {
|
|
28453
|
+
return this._kanbanSearch;
|
|
28454
|
+
}
|
|
28455
|
+
/**
|
|
28456
|
+
* Narrows visible cards to those matching a search. Hidden records keep their position in the
|
|
28457
|
+
* data, so a card dropped between two visible cards still lands between them.
|
|
28458
|
+
* @param {string} search Search text.
|
|
28459
|
+
* @param {{silent?:boolean}} [options={}] Event behavior.
|
|
28460
|
+
* @returns {this}
|
|
28461
|
+
* @fires KanbanView#searchchange
|
|
28462
|
+
*/
|
|
28463
|
+
setSearch(search, options = {}) {
|
|
28464
|
+
const next = search == null ? "" : String(search);
|
|
28465
|
+
if (next === this._kanbanSearch) return this;
|
|
28466
|
+
this._kanbanSearch = next;
|
|
28467
|
+
if (this._searchInput && this._searchInput.value !== next) this._searchInput.value = next;
|
|
28468
|
+
this._refreshView("search");
|
|
28469
|
+
if (!options.silent) this.emit("searchchange", { search: next });
|
|
28470
|
+
return this;
|
|
28471
|
+
}
|
|
28472
|
+
/**
|
|
28473
|
+
* Replaces the extra card predicate. Pass null to clear it.
|
|
28474
|
+
* @param {((record:KanbanRecord,index:number)=>boolean)|null} filter Predicate.
|
|
28475
|
+
* @returns {this}
|
|
28476
|
+
*/
|
|
28477
|
+
setFilter(filter) {
|
|
28478
|
+
if (filter != null && typeof filter !== "function") {
|
|
28479
|
+
throw new TypeError("Kanban filter must be a function or null");
|
|
28480
|
+
}
|
|
28481
|
+
this._kanbanFilter = filter ?? null;
|
|
28482
|
+
this._refreshView("filter");
|
|
27910
28483
|
return this;
|
|
27911
28484
|
}
|
|
28485
|
+
/** Returns the records currently rendered, in board order. @returns {KanbanRecord[]} */
|
|
28486
|
+
getVisibleRecords() {
|
|
28487
|
+
return this._viewData.filter((record) => this._visibleIds.has(this._viewRecordId(record)));
|
|
28488
|
+
}
|
|
28489
|
+
/**
|
|
28490
|
+
* Replaces every record. History is dropped because its snapshots describe records the host has
|
|
28491
|
+
* just replaced, and reinstating them would resurrect data the application no longer owns.
|
|
28492
|
+
* @param {KanbanRecord[]} records New records.
|
|
28493
|
+
* @returns {this}
|
|
28494
|
+
*/
|
|
28495
|
+
setData(records) {
|
|
28496
|
+
this._resetHistory();
|
|
28497
|
+
return super.setData(records);
|
|
28498
|
+
}
|
|
28499
|
+
/** @param {KanbanRecord[]} records Records to append. @returns {this} */
|
|
28500
|
+
addData(records) {
|
|
28501
|
+
this._resetHistory();
|
|
28502
|
+
return super.addData(records);
|
|
28503
|
+
}
|
|
28504
|
+
/** @param {unknown} id Existing record id. @param {KanbanRecord} record Replacement. @returns {this} */
|
|
28505
|
+
updateRecord(id, record) {
|
|
28506
|
+
this._resetHistory();
|
|
28507
|
+
return super.updateRecord(id, record);
|
|
28508
|
+
}
|
|
28509
|
+
/** @param {unknown} id Record id. @returns {this} */
|
|
28510
|
+
removeRecord(id) {
|
|
28511
|
+
this._resetHistory();
|
|
28512
|
+
return super.removeRecord(id);
|
|
28513
|
+
}
|
|
27912
28514
|
/** Returns common plus board-specific serializable configuration. @returns {KanbanViewState} */
|
|
27913
28515
|
getViewState() {
|
|
28516
|
+
this.getColumnOrder();
|
|
28517
|
+
this.getSwimlaneOrder();
|
|
28518
|
+
const derivedColumns = this._kanbanColumns === null;
|
|
28519
|
+
const derivedSwimlanes = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
27914
28520
|
return {
|
|
27915
28521
|
...super.getViewState(),
|
|
27916
|
-
columnOrder: this.getColumnOrder(),
|
|
27917
|
-
swimlaneOrder: this.getSwimlaneOrder(),
|
|
27918
|
-
collapsedColumns: this.getCollapsedColumns(),
|
|
27919
|
-
collapsedSwimlanes: this.getCollapsedSwimlanes()
|
|
28522
|
+
columnOrder: derivedColumns ? [...this._kanbanColumnOrder] : this.getColumnOrder(),
|
|
28523
|
+
swimlaneOrder: derivedSwimlanes ? [...this._kanbanSwimlaneOrder] : this.getSwimlaneOrder(),
|
|
28524
|
+
collapsedColumns: derivedColumns ? [...this._collapsedColumns] : this.getCollapsedColumns(),
|
|
28525
|
+
collapsedSwimlanes: derivedSwimlanes ? [...this._collapsedSwimlanes] : this.getCollapsedSwimlanes()
|
|
27920
28526
|
};
|
|
27921
28527
|
}
|
|
27922
28528
|
/**
|
|
@@ -27928,21 +28534,29 @@ var zx = (() => {
|
|
|
27928
28534
|
setViewState(state, options = {}) {
|
|
27929
28535
|
const source = state && typeof state === "object" ? state : {};
|
|
27930
28536
|
super.setViewState(source, { silent: true });
|
|
27931
|
-
|
|
27932
|
-
|
|
27933
|
-
|
|
27934
|
-
);
|
|
27935
|
-
this.
|
|
27936
|
-
|
|
27937
|
-
Array.isArray(source.
|
|
27938
|
-
|
|
27939
|
-
|
|
27940
|
-
|
|
28537
|
+
const columns = this._rawColumns();
|
|
28538
|
+
const lanes = this._rawSwimlanes();
|
|
28539
|
+
const derivedColumns = this._kanbanColumns === null;
|
|
28540
|
+
const derivedSwimlanes = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
28541
|
+
this._kanbanColumnOrder = reconcileKanbanPreference(
|
|
28542
|
+
columns,
|
|
28543
|
+
Array.isArray(source.columnOrder) ? source.columnOrder : this._kanbanColumnOrder,
|
|
28544
|
+
derivedColumns
|
|
28545
|
+
);
|
|
28546
|
+
this._kanbanSwimlaneOrder = reconcileKanbanPreference(
|
|
28547
|
+
lanes,
|
|
28548
|
+
Array.isArray(source.swimlaneOrder) ? source.swimlaneOrder : this._kanbanSwimlaneOrder,
|
|
28549
|
+
derivedSwimlanes
|
|
28550
|
+
);
|
|
28551
|
+
const columnIds = new Set(columns.map((column) => column.id));
|
|
28552
|
+
const laneIds = new Set(lanes.map((lane) => lane.id));
|
|
27941
28553
|
if (Array.isArray(source.collapsedColumns)) {
|
|
27942
|
-
|
|
28554
|
+
const collapsed = normalizeOrder(source.collapsedColumns);
|
|
28555
|
+
this._collapsedColumns = new Set(derivedColumns ? collapsed : collapsed.filter((id) => columnIds.has(id)));
|
|
27943
28556
|
}
|
|
27944
28557
|
if (Array.isArray(source.collapsedSwimlanes)) {
|
|
27945
|
-
|
|
28558
|
+
const collapsed = normalizeOrder(source.collapsedSwimlanes);
|
|
28559
|
+
this._collapsedSwimlanes = new Set(derivedSwimlanes ? collapsed : collapsed.filter((id) => laneIds.has(id)));
|
|
27946
28560
|
}
|
|
27947
28561
|
this._refreshView("state");
|
|
27948
28562
|
if (!options.silent) this._emitKanbanState("restore");
|
|
@@ -27952,6 +28566,11 @@ var zx = (() => {
|
|
|
27952
28566
|
destroy() {
|
|
27953
28567
|
if (this._kanbanDestroyed) return;
|
|
27954
28568
|
this._kanbanDestroyed = true;
|
|
28569
|
+
this._clearDrag();
|
|
28570
|
+
clearTimeout(this._pointerHold);
|
|
28571
|
+
clearTimeout(this._searchTimer);
|
|
28572
|
+
this._kanbanMenu?.destroy();
|
|
28573
|
+
this._kanbanMenu = null;
|
|
27955
28574
|
const root = this.el;
|
|
27956
28575
|
const snapshot10 = this._kanbanSnapshot;
|
|
27957
28576
|
super.destroy();
|
|
@@ -27966,25 +28585,61 @@ var zx = (() => {
|
|
|
27966
28585
|
}
|
|
27967
28586
|
if (_reason === "loading") return;
|
|
27968
28587
|
const options = this._kanbanOptions();
|
|
28588
|
+
const activeElement = document.activeElement;
|
|
27969
28589
|
const activeMeta = this._metaForTarget(document.activeElement);
|
|
27970
28590
|
const restoreHandle = Boolean(document.activeElement?.classList?.contains("zx-kanban-view__move-handle"));
|
|
28591
|
+
const activeCollapse = activeElement instanceof Element ? activeElement.closest("[data-kanban-collapse]") : null;
|
|
28592
|
+
const activeSection = activeCollapse?.closest(".zx-kanban-view__column");
|
|
28593
|
+
const activeCollapseFocus = activeCollapse && this._board.contains(activeCollapse) ? {
|
|
28594
|
+
axis: (
|
|
28595
|
+
/** @type {'column'|'swimlane'} */
|
|
28596
|
+
/** @type {HTMLElement} */
|
|
28597
|
+
activeCollapse.dataset.kanbanCollapse
|
|
28598
|
+
),
|
|
28599
|
+
id: String(
|
|
28600
|
+
/** @type {HTMLElement} */
|
|
28601
|
+
activeCollapse.dataset.kanbanId
|
|
28602
|
+
),
|
|
28603
|
+
lane: activeSection ? this._sectionMeta.get(activeSection)?.lane ?? null : null
|
|
28604
|
+
} : null;
|
|
27971
28605
|
const columns = this._resolvedColumns();
|
|
27972
28606
|
const lanes = this._resolvedSwimlanes();
|
|
27973
28607
|
this._cardMeta = /* @__PURE__ */ new WeakMap();
|
|
27974
28608
|
this._sectionMeta = /* @__PURE__ */ new WeakMap();
|
|
27975
28609
|
this._cardsById = /* @__PURE__ */ new Map();
|
|
28610
|
+
this._resolveVisibility();
|
|
27976
28611
|
const content = [];
|
|
27977
28612
|
if (options.swimlaneBy == null) {
|
|
27978
28613
|
content.push(this._renderColumns(columns, null, false));
|
|
27979
28614
|
} else {
|
|
27980
28615
|
lanes.forEach((lane) => content.push(this._renderLane(lane, columns)));
|
|
27981
28616
|
}
|
|
27982
|
-
|
|
27983
|
-
if (
|
|
28617
|
+
if (!this._viewData.length) content.push(this._emptyContent(this._text("empty")));
|
|
28618
|
+
else if (!this._visibleIds.size) content.push(this._emptyContent(this._text("noMatches")));
|
|
27984
28619
|
this._board.replaceChildren(...content);
|
|
27985
|
-
if (this._keyboardMove
|
|
28620
|
+
if (this._keyboardMove) {
|
|
28621
|
+
this._keyboardMove.ids = this._keyboardMove.ids.filter((id) => this.getRecord(id));
|
|
28622
|
+
if (!this._keyboardMove.ids.length) this._keyboardMove = null;
|
|
28623
|
+
}
|
|
27986
28624
|
this._syncKeyboardTarget();
|
|
27987
|
-
if (activeMeta) queueMicrotask(() => {
|
|
28625
|
+
if (activeCollapseFocus || activeMeta) queueMicrotask(() => {
|
|
28626
|
+
if (activeCollapseFocus) {
|
|
28627
|
+
this._collapseControl(
|
|
28628
|
+
activeCollapseFocus.axis,
|
|
28629
|
+
activeCollapseFocus.id,
|
|
28630
|
+
activeCollapseFocus.lane
|
|
28631
|
+
)?.focus();
|
|
28632
|
+
return;
|
|
28633
|
+
}
|
|
28634
|
+
if (!activeMeta) return;
|
|
28635
|
+
if (activeMeta.lane != null && this._collapsedSwimlanes.has(activeMeta.lane)) {
|
|
28636
|
+
this._collapseControl("swimlane", activeMeta.lane)?.focus();
|
|
28637
|
+
return;
|
|
28638
|
+
}
|
|
28639
|
+
if (this._collapsedColumns.has(activeMeta.column)) {
|
|
28640
|
+
this._collapseControl("column", activeMeta.column, activeMeta.lane)?.focus();
|
|
28641
|
+
return;
|
|
28642
|
+
}
|
|
27988
28643
|
const card = this._cardsById.get(activeMeta.id);
|
|
27989
28644
|
const target = restoreHandle ? card?.querySelector(".zx-kanban-view__move-handle") : card;
|
|
27990
28645
|
if (target?.isConnected) target.focus();
|
|
@@ -27996,21 +28651,36 @@ var zx = (() => {
|
|
|
27996
28651
|
const collapsed = this._collapsedSwimlanes.has(lane.id);
|
|
27997
28652
|
const headingId = uid("zx-kanban-lane");
|
|
27998
28653
|
const bodyId = uid("zx-kanban-lane-body");
|
|
27999
|
-
const
|
|
28654
|
+
const entries = this._viewData.filter((record, index) => normalizeAxisId(this._readAxis(options.swimlaneBy, record, index)) === lane.id);
|
|
28655
|
+
const total = entries.length;
|
|
28656
|
+
const count2 = entries.filter((record) => this._visibleIds.has(this._viewRecordId(record))).length;
|
|
28000
28657
|
const toggle = h("button", {
|
|
28001
28658
|
class: "zx-kanban-view__collapse",
|
|
28002
28659
|
type: "button",
|
|
28003
28660
|
ariaExpanded: String(!collapsed),
|
|
28004
28661
|
ariaControls: bodyId,
|
|
28005
|
-
ariaLabel:
|
|
28662
|
+
ariaLabel: this._text(collapsed ? "expandLane" : "collapseLane", { label: lane.label }),
|
|
28006
28663
|
dataset: { kanbanCollapse: "swimlane", kanbanId: lane.id }
|
|
28007
28664
|
}, icon(collapsed ? "chevron-right" : "chevron-down", { size: 13 }));
|
|
28665
|
+
const limit = lane.limit ?? null;
|
|
28666
|
+
const wip = wipState(total, limit);
|
|
28667
|
+
const custom = this._renderHook("renderSwimlaneHeader", {
|
|
28668
|
+
swimlane: { ...lane },
|
|
28669
|
+
count: count2,
|
|
28670
|
+
total,
|
|
28671
|
+
columnTotal: total,
|
|
28672
|
+
limit,
|
|
28673
|
+
wip,
|
|
28674
|
+
collapsed
|
|
28675
|
+
});
|
|
28008
28676
|
const heading = h(
|
|
28009
28677
|
"h2",
|
|
28010
28678
|
{ class: "zx-kanban-view__lane-heading", id: headingId },
|
|
28011
28679
|
toggle,
|
|
28012
|
-
|
|
28013
|
-
|
|
28680
|
+
custom ?? [
|
|
28681
|
+
h("span", {}, lane.label),
|
|
28682
|
+
options.showCounts || limit != null ? this._countIndicator(count2, total, limit) : null
|
|
28683
|
+
]
|
|
28014
28684
|
);
|
|
28015
28685
|
const body = this._renderColumns(columns, lane, true);
|
|
28016
28686
|
body.id = bodyId;
|
|
@@ -28018,7 +28688,7 @@ var zx = (() => {
|
|
|
28018
28688
|
return h("section", {
|
|
28019
28689
|
class: "zx-kanban-view__lane",
|
|
28020
28690
|
ariaLabelledby: headingId,
|
|
28021
|
-
dataset: { collapsed: String(collapsed) }
|
|
28691
|
+
dataset: { collapsed: String(collapsed), wip }
|
|
28022
28692
|
}, heading, body);
|
|
28023
28693
|
}
|
|
28024
28694
|
/** @param {KanbanColumn[]} columns @param {KanbanSwimlane|null} lane @param {boolean} hasLanes @returns {HTMLElement} */
|
|
@@ -28044,36 +28714,67 @@ var zx = (() => {
|
|
|
28044
28714
|
const collapsed = this._collapsedColumns.has(column.id);
|
|
28045
28715
|
const headingId = uid("zx-kanban-column");
|
|
28046
28716
|
const listId = uid("zx-kanban-column-list");
|
|
28047
|
-
const
|
|
28048
|
-
const
|
|
28717
|
+
const columnEntries = this._viewData.filter((record, index) => normalizeAxisId(this._readAxis(options.columnBy, record, index)) === column.id);
|
|
28718
|
+
const columnTotal = columnEntries.length;
|
|
28719
|
+
const visible = entries.filter((entry) => this._visibleIds.has(this._viewRecordId(entry.record)));
|
|
28720
|
+
const laneLimit = lane ? readColumnLaneLimit(column, lane.id) : null;
|
|
28721
|
+
const limit = laneLimit ?? column.limit ?? null;
|
|
28722
|
+
const wip = wipState(laneLimit == null ? columnTotal : entries.length, limit);
|
|
28049
28723
|
const toggle = h("button", {
|
|
28050
28724
|
class: "zx-kanban-view__collapse",
|
|
28051
28725
|
type: "button",
|
|
28052
28726
|
ariaExpanded: String(!collapsed),
|
|
28053
28727
|
ariaControls: listId,
|
|
28054
|
-
ariaLabel:
|
|
28728
|
+
ariaLabel: this._text(collapsed ? "expandColumn" : "collapseColumn", { label: column.label }),
|
|
28055
28729
|
dataset: { kanbanCollapse: "column", kanbanId: column.id }
|
|
28056
28730
|
}, icon(collapsed ? "chevron-right" : "chevron-down", { size: 13 }));
|
|
28057
|
-
const
|
|
28058
|
-
|
|
28059
|
-
|
|
28060
|
-
|
|
28731
|
+
const custom = this._renderHook("renderColumnHeader", {
|
|
28732
|
+
column: { ...column },
|
|
28733
|
+
swimlane: lane ? { ...lane } : null,
|
|
28734
|
+
count: visible.length,
|
|
28735
|
+
total: entries.length,
|
|
28736
|
+
columnTotal,
|
|
28737
|
+
limit,
|
|
28738
|
+
wip,
|
|
28739
|
+
collapsed
|
|
28740
|
+
});
|
|
28741
|
+
const indicator = options.showCounts || limit != null ? this._columnIndicator(visible.length, entries.length, columnTotal, limit, hasLanes && laneLimit == null) : null;
|
|
28061
28742
|
const heading = h(hasLanes ? "h3" : "h2", {
|
|
28062
28743
|
class: "zx-kanban-view__column-heading",
|
|
28063
28744
|
id: headingId
|
|
28064
|
-
}, toggle, h("span", { class: "zx-kanban-view__column-label" }, column.label), indicator);
|
|
28745
|
+
}, toggle, custom ?? [h("span", { class: "zx-kanban-view__column-label" }, column.label), indicator]);
|
|
28065
28746
|
const list = h("ul", {
|
|
28066
28747
|
class: "zx-kanban-view__cards",
|
|
28067
28748
|
id: listId,
|
|
28068
28749
|
ariaLabel: `${column.label}${lane ? `, ${lane.label}` : ""}`
|
|
28069
28750
|
});
|
|
28070
|
-
|
|
28751
|
+
visible.forEach((entry) => list.append(this._renderCard(entry.record, entry.index, column.id, lane?.id ?? null, hasLanes)));
|
|
28752
|
+
if (!visible.length) {
|
|
28753
|
+
const placeholder = this._renderHook("renderColumnEmpty", {
|
|
28754
|
+
column: { ...column },
|
|
28755
|
+
swimlane: lane ? { ...lane } : null,
|
|
28756
|
+
count: 0,
|
|
28757
|
+
total: entries.length,
|
|
28758
|
+
columnTotal,
|
|
28759
|
+
limit,
|
|
28760
|
+
wip,
|
|
28761
|
+
collapsed
|
|
28762
|
+
}) ?? this._placeholderText();
|
|
28763
|
+
if (placeholder) list.append(h("li", { class: "zx-kanban-view__column-empty", role: "presentation" }, placeholder));
|
|
28764
|
+
}
|
|
28071
28765
|
list.hidden = collapsed;
|
|
28766
|
+
const add = this._allowsAdd(column, lane) ? h("button", {
|
|
28767
|
+
class: "zx-kanban-view__add",
|
|
28768
|
+
type: "button",
|
|
28769
|
+
ariaLabel: this._text("addTo", { target: `${column.label}${lane ? `, ${lane.label}` : ""}` }),
|
|
28770
|
+
dataset: { kanbanAdd: column.id, kanbanLane: lane?.id ?? "" }
|
|
28771
|
+
}, icon("plus", { size: 13 }), h("span", {}, this._text("add"))) : null;
|
|
28772
|
+
if (add) add.hidden = collapsed;
|
|
28072
28773
|
const section = h("section", {
|
|
28073
28774
|
class: "zx-kanban-view__column",
|
|
28074
28775
|
ariaLabelledby: headingId,
|
|
28075
28776
|
dataset: { column: column.id, collapsed: String(collapsed), wip }
|
|
28076
|
-
}, heading, list);
|
|
28777
|
+
}, heading, list, add);
|
|
28077
28778
|
this._sectionMeta.set(section, { column: column.id, lane: lane?.id ?? null, list });
|
|
28078
28779
|
return section;
|
|
28079
28780
|
}
|
|
@@ -28081,13 +28782,14 @@ var zx = (() => {
|
|
|
28081
28782
|
_renderCard(record, index, column, lane, hasLanes) {
|
|
28082
28783
|
const options = this._kanbanOptions();
|
|
28083
28784
|
const id = this._viewRecordId(record);
|
|
28084
|
-
const grabbed = Boolean(this._keyboardMove
|
|
28785
|
+
const grabbed = Boolean(this._keyboardMove?.ids.some((candidate) => Object.is(candidate, id)));
|
|
28786
|
+
const rules = resolveKanbanRules(this._kanbanRules, record, { index, column, lane });
|
|
28787
|
+
const multiple = grabbed && this._keyboardMove ? this._keyboardMove.ids.length : 1;
|
|
28085
28788
|
const handle = h("button", {
|
|
28086
28789
|
class: "zx-kanban-view__move-handle",
|
|
28087
28790
|
type: "button",
|
|
28088
|
-
draggable: true,
|
|
28089
28791
|
ariaPressed: String(grabbed),
|
|
28090
|
-
ariaLabel:
|
|
28792
|
+
ariaLabel: multiple > 1 ? this._text("moveMultiple", { count: multiple }) : this._text("move", { name: this._recordName(record, index) })
|
|
28091
28793
|
}, icon("drag", { size: 13 }));
|
|
28092
28794
|
const card = createRecordCard(record, index, {
|
|
28093
28795
|
fields: this.getFields(),
|
|
@@ -28106,15 +28808,81 @@ var zx = (() => {
|
|
|
28106
28808
|
});
|
|
28107
28809
|
card.tabIndex = 0;
|
|
28108
28810
|
if (grabbed) card.dataset.grabbed = "true";
|
|
28811
|
+
this._applyCardContent(card, { record, id, index, column, lane, rules, selected: this._isViewSelected(id) });
|
|
28812
|
+
this._applyCardRules(card, rules);
|
|
28813
|
+
this._applyCardDescription(card, id);
|
|
28109
28814
|
const meta = { record, id, index, column, lane };
|
|
28110
28815
|
this._cardMeta.set(card, meta);
|
|
28111
28816
|
this._cardsById.set(id, card);
|
|
28112
28817
|
return card;
|
|
28113
28818
|
}
|
|
28114
|
-
/**
|
|
28115
|
-
|
|
28819
|
+
/**
|
|
28820
|
+
* Hands card content to `renderCard` while keeping the managed shell: the list item, its
|
|
28821
|
+
* selection control, the action group, and the move handle are never given away, because they
|
|
28822
|
+
* carry the drop, focus, and movement semantics the board is responsible for.
|
|
28823
|
+
* @param {HTMLElement} card Card built from the shared anatomy.
|
|
28824
|
+
* @param {KanbanCardContext} context Card context.
|
|
28825
|
+
* @returns {void}
|
|
28826
|
+
*/
|
|
28827
|
+
_applyCardContent(card, context) {
|
|
28828
|
+
const custom = this._renderHook("renderCard", context);
|
|
28829
|
+
if (!custom) return;
|
|
28830
|
+
const body = card.querySelector(".zx-record-card__body");
|
|
28831
|
+
const titles = card.querySelector(".zx-record-card__titles");
|
|
28832
|
+
if (!body || !titles) return;
|
|
28833
|
+
titles.replaceChildren(custom);
|
|
28834
|
+
body.querySelector(".zx-record-card__eyebrow")?.remove();
|
|
28835
|
+
body.querySelector(".zx-record-card__metadata")?.remove();
|
|
28836
|
+
card.removeAttribute("aria-labelledby");
|
|
28837
|
+
card.setAttribute("aria-label", this._recordName(context.record, context.index));
|
|
28838
|
+
card.dataset.customCard = "true";
|
|
28839
|
+
}
|
|
28840
|
+
/**
|
|
28841
|
+
* Applies ordered card rules. The first match owns the marker tone; every match with a label adds
|
|
28842
|
+
* a badge, and rule descriptions join the accessible description so the marker is never the only
|
|
28843
|
+
* carrier of the meaning.
|
|
28844
|
+
* @param {HTMLElement} card Card element.
|
|
28845
|
+
* @param {KanbanRule[]} rules Matching rules.
|
|
28846
|
+
* @returns {void}
|
|
28847
|
+
*/
|
|
28848
|
+
_applyCardRules(card, rules) {
|
|
28849
|
+
if (!rules.length) return;
|
|
28850
|
+
card.dataset.rule = rules.map((rule) => rule.id).join(" ");
|
|
28851
|
+
card.dataset.ruleTone = rules[0].tone;
|
|
28852
|
+
const badges = rules.filter((rule) => rule.label);
|
|
28853
|
+
if (badges.length) {
|
|
28854
|
+
const list = h("p", { class: "zx-kanban-view__rules" }, badges.map((rule) => badge({
|
|
28855
|
+
label: rule.label,
|
|
28856
|
+
icon: rule.icon ?? void 0,
|
|
28857
|
+
kind: rule.tone,
|
|
28858
|
+
size: "sm",
|
|
28859
|
+
variant: "soft"
|
|
28860
|
+
})));
|
|
28861
|
+
card.querySelector(".zx-record-card__body")?.append(list);
|
|
28862
|
+
}
|
|
28863
|
+
const descriptions = rules.map((rule) => rule.description).filter(Boolean).join(". ");
|
|
28864
|
+
if (descriptions) card.dataset.ruleDescription = descriptions;
|
|
28865
|
+
}
|
|
28866
|
+
/**
|
|
28867
|
+
* Composes one card's accessible description from its selection state and its rules, so that
|
|
28868
|
+
* refreshing selection cannot quietly drop what a rule contributed.
|
|
28869
|
+
* @param {HTMLElement} card Card element.
|
|
28870
|
+
* @param {unknown} id Record id.
|
|
28871
|
+
* @returns {void}
|
|
28872
|
+
*/
|
|
28873
|
+
_applyCardDescription(card, id) {
|
|
28874
|
+
const parts = [];
|
|
28875
|
+
if (this.options.selectable) {
|
|
28876
|
+
parts.push(this._text(this._isViewSelected(id) ? "selected" : "notSelected"));
|
|
28877
|
+
}
|
|
28878
|
+
if (card.dataset.ruleDescription) parts.push(card.dataset.ruleDescription);
|
|
28879
|
+
if (parts.length) card.setAttribute("aria-description", parts.join(". "));
|
|
28880
|
+
else card.removeAttribute("aria-description");
|
|
28881
|
+
}
|
|
28882
|
+
/** @param {string} text Board-level empty message. @returns {HTMLElement} */
|
|
28883
|
+
_emptyContent(text4) {
|
|
28116
28884
|
const supplied = typeof this.options.emptyText === "function" ? this.options.emptyText() : this.options.emptyText;
|
|
28117
|
-
const content = supplied ??
|
|
28885
|
+
const content = supplied ?? text4;
|
|
28118
28886
|
return h(
|
|
28119
28887
|
"div",
|
|
28120
28888
|
{ class: "zx-kanban-view__empty", role: "status" },
|
|
@@ -28124,6 +28892,37 @@ var zx = (() => {
|
|
|
28124
28892
|
) : String(content)
|
|
28125
28893
|
);
|
|
28126
28894
|
}
|
|
28895
|
+
/** @returns {string} Per-column placeholder text, or an empty string for none. */
|
|
28896
|
+
_placeholderText() {
|
|
28897
|
+
return this._text("columnEmpty");
|
|
28898
|
+
}
|
|
28899
|
+
/**
|
|
28900
|
+
* @param {number} count Visible records.
|
|
28901
|
+
* @param {number} total Records before search and filters.
|
|
28902
|
+
* @param {number|null} limit Applicable limit.
|
|
28903
|
+
* @returns {HTMLElement}
|
|
28904
|
+
*/
|
|
28905
|
+
_countIndicator(count2, total, limit) {
|
|
28906
|
+
const filtered = count2 !== total;
|
|
28907
|
+
return h("span", {
|
|
28908
|
+
class: "zx-kanban-view__count",
|
|
28909
|
+
ariaLabel: limit != null ? this._text("countLimit", { count: total, limit }) : filtered ? this._text("countFiltered", { count: count2, total }) : this._text("count", { count: count2 })
|
|
28910
|
+
}, limit != null ? `${total} / ${limit}` : filtered ? `${count2} / ${total}` : String(count2));
|
|
28911
|
+
}
|
|
28912
|
+
/**
|
|
28913
|
+
* @param {number} count Visible records in this column and lane.
|
|
28914
|
+
* @param {number} total Records in this column and lane before search and filters.
|
|
28915
|
+
* @param {number} columnTotal Records across the whole column.
|
|
28916
|
+
* @param {number|null} limit Applicable limit.
|
|
28917
|
+
* @param {boolean} columnScoped Whether the limit counts the column rather than this lane.
|
|
28918
|
+
* @returns {HTMLElement}
|
|
28919
|
+
*/
|
|
28920
|
+
_columnIndicator(count2, total, columnTotal, limit, columnScoped) {
|
|
28921
|
+
const filtered = count2 !== total;
|
|
28922
|
+
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 });
|
|
28923
|
+
const text4 = limit == null ? filtered ? `${count2} / ${total}` : String(count2) : columnScoped ? `${total} \xB7 ${columnTotal} / ${limit}` : `${total} / ${limit}`;
|
|
28924
|
+
return h("span", { class: "zx-kanban-view__count", ariaLabel: label }, text4);
|
|
28925
|
+
}
|
|
28127
28926
|
/** @returns {KanbanColumn[]} */
|
|
28128
28927
|
_rawColumns() {
|
|
28129
28928
|
const options = this._kanbanOptions();
|
|
@@ -28132,7 +28931,11 @@ var zx = (() => {
|
|
|
28132
28931
|
/** @returns {KanbanColumn[]} */
|
|
28133
28932
|
_resolvedColumns() {
|
|
28134
28933
|
const columns = this._rawColumns();
|
|
28135
|
-
this._kanbanColumnOrder =
|
|
28934
|
+
this._kanbanColumnOrder = reconcileKanbanPreference(
|
|
28935
|
+
columns,
|
|
28936
|
+
this._kanbanColumnOrder,
|
|
28937
|
+
this._kanbanColumns === null
|
|
28938
|
+
);
|
|
28136
28939
|
return orderDescriptors(columns, this._kanbanColumnOrder);
|
|
28137
28940
|
}
|
|
28138
28941
|
/** @returns {KanbanSwimlane[]} */
|
|
@@ -28144,7 +28947,12 @@ var zx = (() => {
|
|
|
28144
28947
|
/** @returns {KanbanSwimlane[]} */
|
|
28145
28948
|
_resolvedSwimlanes() {
|
|
28146
28949
|
const lanes = this._rawSwimlanes();
|
|
28147
|
-
this.
|
|
28950
|
+
const preserveUnknown = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
28951
|
+
this._kanbanSwimlaneOrder = reconcileKanbanPreference(
|
|
28952
|
+
lanes,
|
|
28953
|
+
this._kanbanSwimlaneOrder,
|
|
28954
|
+
preserveUnknown
|
|
28955
|
+
);
|
|
28148
28956
|
return orderDescriptors(lanes, this._kanbanSwimlaneOrder);
|
|
28149
28957
|
}
|
|
28150
28958
|
/** @param {KanbanAccessor} accessor @param {KanbanRecord} record @param {number} index @returns {unknown} */
|
|
@@ -28168,13 +28976,84 @@ var zx = (() => {
|
|
|
28168
28976
|
/** @param {unknown} id @returns {KanbanMovePoint|null} */
|
|
28169
28977
|
_recordLocation(id) {
|
|
28170
28978
|
const options = this._kanbanOptions();
|
|
28171
|
-
const index = this._viewData.findIndex((
|
|
28979
|
+
const index = this._viewData.findIndex((record) => Object.is(this._viewRecordId(record), id));
|
|
28172
28980
|
if (index < 0) return null;
|
|
28173
|
-
|
|
28174
|
-
|
|
28175
|
-
|
|
28176
|
-
|
|
28177
|
-
|
|
28981
|
+
return locateKanbanRecord(this._viewData, index, {
|
|
28982
|
+
recordId: options.recordId,
|
|
28983
|
+
columnBy: (record, recordIndex) => this._readAxis(options.columnBy, record, recordIndex),
|
|
28984
|
+
swimlaneBy: options.swimlaneBy == null ? null : (record, recordIndex) => this._readAxis(options.swimlaneBy, record, recordIndex)
|
|
28985
|
+
});
|
|
28986
|
+
}
|
|
28987
|
+
/**
|
|
28988
|
+
* Recomputes which records survive the search and the filter predicate.
|
|
28989
|
+
* @returns {void}
|
|
28990
|
+
*/
|
|
28991
|
+
_resolveVisibility() {
|
|
28992
|
+
const terms = kanbanSearchTerms(this._kanbanSearch);
|
|
28993
|
+
const filter = this._kanbanFilter;
|
|
28994
|
+
const sources = terms.length ? this._searchSources() : null;
|
|
28995
|
+
this._visibleIds = /* @__PURE__ */ new Set();
|
|
28996
|
+
this._viewData.forEach((record, index) => {
|
|
28997
|
+
if (filter && filter(record, index) !== true) return;
|
|
28998
|
+
if (sources && !matchesKanbanSearch(this._searchableText(record, index, sources), terms)) return;
|
|
28999
|
+
this._visibleIds.add(this._viewRecordId(record));
|
|
29000
|
+
});
|
|
29001
|
+
}
|
|
29002
|
+
/**
|
|
29003
|
+
* Resolves what a search looks at, once. `searchFields` names them explicitly; otherwise the
|
|
29004
|
+
* visible fields are searched, plus the title and subtitle sources — a card is findable by the
|
|
29005
|
+
* name it shows even when that field is hidden from the metadata list.
|
|
29006
|
+
* @returns {{fields:import('../view/record-view.js').ViewField[],readers:((record:KanbanRecord,index:number)=>unknown)[]}}
|
|
29007
|
+
*/
|
|
29008
|
+
_searchSources() {
|
|
29009
|
+
const options = this._kanbanOptions();
|
|
29010
|
+
const configured = Array.isArray(options.searchFields) ? options.searchFields.map(String) : null;
|
|
29011
|
+
const all = this.getFields();
|
|
29012
|
+
if (configured) return { fields: all.filter((field) => configured.includes(field.id)), readers: [] };
|
|
29013
|
+
const fields = this.getVisibleFields();
|
|
29014
|
+
const known = new Set(fields.map((field) => field.id));
|
|
29015
|
+
const readers = [];
|
|
29016
|
+
for (const source of [options.titleField, options.subtitleField]) {
|
|
29017
|
+
if (source == null) continue;
|
|
29018
|
+
if (typeof source === "function") {
|
|
29019
|
+
readers.push(source);
|
|
29020
|
+
continue;
|
|
29021
|
+
}
|
|
29022
|
+
if (known.has(source)) continue;
|
|
29023
|
+
const field = all.find((candidate) => candidate.id === source);
|
|
29024
|
+
if (field) fields.push(field);
|
|
29025
|
+
else readers.push((record) => record?.[source]);
|
|
29026
|
+
}
|
|
29027
|
+
return { fields, readers };
|
|
29028
|
+
}
|
|
29029
|
+
/**
|
|
29030
|
+
* Builds the text one record is searched through. Field `render` output is deliberately ignored:
|
|
29031
|
+
* it may be a DOM node, and matching what a formatter produced rather than what the record holds
|
|
29032
|
+
* would make the same query behave differently in two views over the same data.
|
|
29033
|
+
* @param {KanbanRecord} record Record.
|
|
29034
|
+
* @param {number} index Display index.
|
|
29035
|
+
* @param {{fields:import('../view/record-view.js').ViewField[],readers:((record:KanbanRecord,index:number)=>unknown)[]}} sources Resolved sources.
|
|
29036
|
+
* @returns {string} Searchable text.
|
|
29037
|
+
*/
|
|
29038
|
+
_searchableText(record, index, sources) {
|
|
29039
|
+
const parts = sources.fields.map((field) => stringifyValue(readViewField(field, record, index)));
|
|
29040
|
+
for (const reader of sources.readers) parts.push(stringifyValue(reader(record, index)));
|
|
29041
|
+
return parts.filter(Boolean).join(" ");
|
|
29042
|
+
}
|
|
29043
|
+
/**
|
|
29044
|
+
* Translates a position among rendered cards into a position among all records in one bucket.
|
|
29045
|
+
* @param {string} column Column id.
|
|
29046
|
+
* @param {string|null} lane Lane id.
|
|
29047
|
+
* @param {number} visibleIndex Position among visible cards.
|
|
29048
|
+
* @param {Set<unknown>} excluded Ids being moved, which the visible list already omits.
|
|
29049
|
+
* @returns {number} Absolute position.
|
|
29050
|
+
*/
|
|
29051
|
+
_absoluteIndex(column, lane, visibleIndex, excluded) {
|
|
29052
|
+
const entries = this._recordsIn(column, lane).filter((entry) => !excluded.has(this._viewRecordId(entry.record)));
|
|
29053
|
+
return absoluteKanbanIndex(
|
|
29054
|
+
entries.map((entry) => this._visibleIds.has(this._viewRecordId(entry.record))),
|
|
29055
|
+
visibleIndex
|
|
29056
|
+
);
|
|
28178
29057
|
}
|
|
28179
29058
|
/**
|
|
28180
29059
|
* @param {KanbanRecord} record @param {KanbanMovePoint} from @param {KanbanMovePoint} to
|
|
@@ -28196,8 +29075,327 @@ var zx = (() => {
|
|
|
28196
29075
|
if (options.swimlaneBy != null && normalizeAxisId(this._readAxis(options.swimlaneBy, clone, currentIndex)) !== to.lane) return null;
|
|
28197
29076
|
return clone;
|
|
28198
29077
|
}
|
|
29078
|
+
/**
|
|
29079
|
+
* Runs transition, eligibility, and capacity policy for a whole move. Every distinct origin is
|
|
29080
|
+
* checked, so a multi-card move drawn from two columns cannot slip past a transition rule that
|
|
29081
|
+
* only the second origin violates.
|
|
29082
|
+
* @param {KanbanRecord[]} records Moving records.
|
|
29083
|
+
* @param {KanbanMovePoint[]} origins Their origins.
|
|
29084
|
+
* @param {KanbanColumn} column Destination column.
|
|
29085
|
+
* @param {KanbanSwimlane|null} lane Destination lane.
|
|
29086
|
+
* @param {KanbanMovePoint} to Destination point.
|
|
29087
|
+
* @param {KanbanMoveContext} context Predicate context.
|
|
29088
|
+
* @param {Set<unknown>} excluded Moving ids, excluded from destination counts.
|
|
29089
|
+
* @returns {KanbanMoveEvaluation} Decision.
|
|
29090
|
+
*/
|
|
29091
|
+
_evaluateMove(records, origins, column, lane, to, context, excluded) {
|
|
29092
|
+
const options = this._kanbanOptions();
|
|
29093
|
+
for (const origin of origins) {
|
|
29094
|
+
const blocked = allowsKanbanTransition(column, lane, origin);
|
|
29095
|
+
if (blocked) {
|
|
29096
|
+
return {
|
|
29097
|
+
allowed: false,
|
|
29098
|
+
reason: blocked,
|
|
29099
|
+
limit: null,
|
|
29100
|
+
count: 0,
|
|
29101
|
+
limitExceeded: false,
|
|
29102
|
+
limitReached: false,
|
|
29103
|
+
policy: column.wipPolicy ?? (options.wipPolicy === "block" ? "block" : "warn")
|
|
29104
|
+
};
|
|
29105
|
+
}
|
|
29106
|
+
}
|
|
29107
|
+
const excludes = (record) => !excluded.has(this._viewRecordId(record));
|
|
29108
|
+
const columnCount = this._viewData.filter((record, index) => normalizeAxisId(this._readAxis(options.columnBy, record, index)) === to.column && excludes(record)).length;
|
|
29109
|
+
const laneCount = options.swimlaneBy == null ? 0 : this._viewData.filter((record, index) => normalizeAxisId(this._readAxis(options.swimlaneBy, record, index)) === to.lane && excludes(record)).length;
|
|
29110
|
+
const cellCount = this._recordsIn(to.column, to.lane).filter((entry) => excludes(entry.record)).length;
|
|
29111
|
+
return evaluateKanbanMove({
|
|
29112
|
+
records,
|
|
29113
|
+
column,
|
|
29114
|
+
swimlane: lane,
|
|
29115
|
+
context,
|
|
29116
|
+
from: origins[0],
|
|
29117
|
+
columnCount,
|
|
29118
|
+
laneCount,
|
|
29119
|
+
cellCount,
|
|
29120
|
+
policy: options.wipPolicy === "block" ? "block" : "warn"
|
|
29121
|
+
});
|
|
29122
|
+
}
|
|
29123
|
+
/**
|
|
29124
|
+
* Announces and reports a refused move. Nothing is written and no `recordmove` is emitted, so a
|
|
29125
|
+
* listener can neither observe nor undo a move that never happened.
|
|
29126
|
+
* @param {KanbanRejectReason} reason Refusal reason.
|
|
29127
|
+
* @param {{ids:unknown[],records:KanbanRecord[],from:KanbanMovePoint|null,to:KanbanMovePoint|null,column:KanbanColumn|null,swimlane:KanbanSwimlane|null,evaluation?:KanbanMoveEvaluation}} detail Context.
|
|
29128
|
+
* @returns {void}
|
|
29129
|
+
* @fires KanbanView#movereject
|
|
29130
|
+
*/
|
|
29131
|
+
_rejectMove(reason, detail) {
|
|
29132
|
+
const target = detail.to ? this._targetName(detail.to) : "";
|
|
29133
|
+
const name = this._movingName(detail.records);
|
|
29134
|
+
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 });
|
|
29135
|
+
this._announce(message);
|
|
29136
|
+
this.emit("movereject", {
|
|
29137
|
+
reason,
|
|
29138
|
+
ids: [...detail.ids],
|
|
29139
|
+
records: [...detail.records],
|
|
29140
|
+
record: detail.records[0] ?? null,
|
|
29141
|
+
id: detail.ids[0],
|
|
29142
|
+
from: detail.from ? { ...detail.from } : null,
|
|
29143
|
+
to: detail.to ? { ...detail.to } : null,
|
|
29144
|
+
column: detail.column ? { ...detail.column } : null,
|
|
29145
|
+
swimlane: detail.swimlane ? { ...detail.swimlane } : null,
|
|
29146
|
+
limit: detail.evaluation?.limit ?? null,
|
|
29147
|
+
count: detail.evaluation?.count ?? 0,
|
|
29148
|
+
message
|
|
29149
|
+
});
|
|
29150
|
+
}
|
|
29151
|
+
/**
|
|
29152
|
+
* Installs a committed local record order, remembering the previous one for undo.
|
|
29153
|
+
* @param {KanbanRecord[]} records Next record order.
|
|
29154
|
+
* @returns {void}
|
|
29155
|
+
*/
|
|
29156
|
+
_commitLocalRecords(records) {
|
|
29157
|
+
const options = this._kanbanOptions();
|
|
29158
|
+
if (options.history !== false) {
|
|
29159
|
+
this._kanbanHistory.push({ records: [...this._viewData], selection: this.getSelectionIds() });
|
|
29160
|
+
this._syncHistoryControls();
|
|
29161
|
+
}
|
|
29162
|
+
this._viewData = records;
|
|
29163
|
+
if (options.sortMode === "local" && this.getSort()) this.setSort(null);
|
|
29164
|
+
else this._refreshView("move");
|
|
29165
|
+
this.emit("datachange", { records: this.getData() });
|
|
29166
|
+
if (options.history !== false) this._emitHistory();
|
|
29167
|
+
}
|
|
29168
|
+
/**
|
|
29169
|
+
* @param {'undo'|'redo'} direction Travel direction.
|
|
29170
|
+
* @returns {this}
|
|
29171
|
+
*/
|
|
29172
|
+
_travelHistory(direction) {
|
|
29173
|
+
if (this._kanbanOptions().history === false) return this;
|
|
29174
|
+
const current = { records: [...this._viewData], selection: this.getSelectionIds() };
|
|
29175
|
+
const entry = direction === "undo" ? this._kanbanHistory.undo(current) : this._kanbanHistory.redo(current);
|
|
29176
|
+
if (!entry) {
|
|
29177
|
+
this._announce(this._text(direction === "undo" ? "nothingToUndo" : "nothingToRedo"));
|
|
29178
|
+
return this;
|
|
29179
|
+
}
|
|
29180
|
+
this._viewData = [...entry.records];
|
|
29181
|
+
this._refreshView(direction);
|
|
29182
|
+
this.setSelection(entry.selection);
|
|
29183
|
+
this._syncHistoryControls();
|
|
29184
|
+
this.emit("datachange", { records: this.getData() });
|
|
29185
|
+
this._emitHistory();
|
|
29186
|
+
this._announce(this._text(direction === "undo" ? "undone" : "redone"));
|
|
29187
|
+
return this;
|
|
29188
|
+
}
|
|
29189
|
+
/** Drops both stacks without announcing, for host-driven data replacement. @returns {void} */
|
|
29190
|
+
_resetHistory() {
|
|
29191
|
+
if (!this._kanbanHistory) return;
|
|
29192
|
+
const had = this._kanbanHistory.canUndo() || this._kanbanHistory.canRedo();
|
|
29193
|
+
this._kanbanHistory.clear();
|
|
29194
|
+
this._syncHistoryControls();
|
|
29195
|
+
if (had) this._emitHistory();
|
|
29196
|
+
}
|
|
29197
|
+
/** @returns {void} */
|
|
29198
|
+
_emitHistory() {
|
|
29199
|
+
this.emit("historychange", {
|
|
29200
|
+
canUndo: this.canUndo(),
|
|
29201
|
+
canRedo: this.canRedo(),
|
|
29202
|
+
depth: this._kanbanHistory.depth()
|
|
29203
|
+
});
|
|
29204
|
+
}
|
|
29205
|
+
/** @returns {void} */
|
|
29206
|
+
_syncHistoryControls() {
|
|
29207
|
+
if (this._undoButton) this._undoButton.disabled = !this.canUndo();
|
|
29208
|
+
if (this._redoButton) this._redoButton.disabled = !this.canRedo();
|
|
29209
|
+
}
|
|
29210
|
+
/** @returns {HTMLElement} Built-in search control. */
|
|
29211
|
+
_createSearchControl() {
|
|
29212
|
+
const input = (
|
|
29213
|
+
/** @type {HTMLInputElement} */
|
|
29214
|
+
h("input", {
|
|
29215
|
+
class: "zx-kanban-view__search-input",
|
|
29216
|
+
type: "search",
|
|
29217
|
+
value: this._kanbanSearch,
|
|
29218
|
+
ariaLabel: this._text("search"),
|
|
29219
|
+
placeholder: this._text("searchPlaceholder")
|
|
29220
|
+
})
|
|
29221
|
+
);
|
|
29222
|
+
this._searchInput = input;
|
|
29223
|
+
this.listen(input, "input", () => {
|
|
29224
|
+
clearTimeout(this._searchTimer);
|
|
29225
|
+
this._searchTimer = setTimeout(() => {
|
|
29226
|
+
this._searchTimer = 0;
|
|
29227
|
+
if (!this._kanbanDestroyed) this.setSearch(input.value);
|
|
29228
|
+
}, SEARCH_DEBOUNCE);
|
|
29229
|
+
});
|
|
29230
|
+
this.listen(input, "keydown", (event) => {
|
|
29231
|
+
if (
|
|
29232
|
+
/** @type {KeyboardEvent} */
|
|
29233
|
+
event.key !== "Escape" || !input.value
|
|
29234
|
+
) return;
|
|
29235
|
+
event.stopPropagation();
|
|
29236
|
+
clearTimeout(this._searchTimer);
|
|
29237
|
+
this._searchTimer = 0;
|
|
29238
|
+
this.setSearch("");
|
|
29239
|
+
});
|
|
29240
|
+
return h("div", { class: "zx-kanban-view__search" }, icon("search", { size: 13 }), input);
|
|
29241
|
+
}
|
|
29242
|
+
/** @returns {HTMLElement} Built-in history controls. */
|
|
29243
|
+
_createHistoryControls() {
|
|
29244
|
+
this._undoButton = /** @type {HTMLButtonElement} */
|
|
29245
|
+
h("button", {
|
|
29246
|
+
class: "zx-button zx-button--sm",
|
|
29247
|
+
type: "button",
|
|
29248
|
+
disabled: true,
|
|
29249
|
+
dataset: { kanbanHistory: "undo" }
|
|
29250
|
+
}, this._text("undo"));
|
|
29251
|
+
this._redoButton = /** @type {HTMLButtonElement} */
|
|
29252
|
+
h("button", {
|
|
29253
|
+
class: "zx-button zx-button--sm",
|
|
29254
|
+
type: "button",
|
|
29255
|
+
disabled: true,
|
|
29256
|
+
dataset: { kanbanHistory: "redo" }
|
|
29257
|
+
}, this._text("redo"));
|
|
29258
|
+
const group = h(
|
|
29259
|
+
"div",
|
|
29260
|
+
{ class: "zx-kanban-view__history", role: "group" },
|
|
29261
|
+
this._undoButton,
|
|
29262
|
+
this._redoButton
|
|
29263
|
+
);
|
|
29264
|
+
this.listen(group, "click", (event) => {
|
|
29265
|
+
const control = (
|
|
29266
|
+
/** @type {Element|null} */
|
|
29267
|
+
event.target?.closest?.("[data-kanban-history]")
|
|
29268
|
+
);
|
|
29269
|
+
if (!control) return;
|
|
29270
|
+
if (
|
|
29271
|
+
/** @type {HTMLElement} */
|
|
29272
|
+
control.dataset.kanbanHistory === "undo"
|
|
29273
|
+
) this.undo();
|
|
29274
|
+
else this.redo();
|
|
29275
|
+
});
|
|
29276
|
+
return group;
|
|
29277
|
+
}
|
|
29278
|
+
/** Attaches the card context menu. @returns {void} */
|
|
29279
|
+
_createContextMenu() {
|
|
29280
|
+
this._kanbanMenu = new ContextMenu(this._board, {
|
|
29281
|
+
selector: ".zx-record-card",
|
|
29282
|
+
items: (context) => this._menuItems(context),
|
|
29283
|
+
onselect: ({ detail }) => this._handleMenuSelect(detail)
|
|
29284
|
+
});
|
|
29285
|
+
}
|
|
29286
|
+
/**
|
|
29287
|
+
* Builds the menu for one card: every destination the same policy path would accept, plus any
|
|
29288
|
+
* application items. Destinations the board would refuse are shown disabled rather than hidden,
|
|
29289
|
+
* so a rule is discoverable instead of silently removing a command.
|
|
29290
|
+
* @param {Element|null} context Card element the menu opened on.
|
|
29291
|
+
* @returns {(MenuItem|'-')[]} Menu entries.
|
|
29292
|
+
*/
|
|
29293
|
+
_menuItems(context) {
|
|
29294
|
+
const meta = context ? this._cardMeta.get(context) : null;
|
|
29295
|
+
if (!meta) return [];
|
|
29296
|
+
const options = this._kanbanOptions();
|
|
29297
|
+
const ids = this._movingIds(meta.id);
|
|
29298
|
+
const items = [];
|
|
29299
|
+
for (const column of this._resolvedColumns()) {
|
|
29300
|
+
if (column.id === meta.column) continue;
|
|
29301
|
+
items.push({
|
|
29302
|
+
label: this._text("menuMoveTo", { target: column.label }),
|
|
29303
|
+
value: { kind: "column", id: column.id, ids },
|
|
29304
|
+
disabled: !this._canMove(ids, { column: column.id })
|
|
29305
|
+
});
|
|
29306
|
+
}
|
|
29307
|
+
if (options.swimlaneBy != null) {
|
|
29308
|
+
for (const lane of this._resolvedSwimlanes()) {
|
|
29309
|
+
if (lane.id === meta.lane) continue;
|
|
29310
|
+
items.push({
|
|
29311
|
+
label: this._text("menuMoveToLane", { target: lane.label }),
|
|
29312
|
+
value: { kind: "lane", id: lane.id, ids },
|
|
29313
|
+
disabled: !this._canMove(ids, { lane: lane.id })
|
|
29314
|
+
});
|
|
29315
|
+
}
|
|
29316
|
+
}
|
|
29317
|
+
const extra = typeof options.contextMenu === "function" ? options.contextMenu({
|
|
29318
|
+
record: meta.record,
|
|
29319
|
+
id: meta.id,
|
|
29320
|
+
index: meta.index,
|
|
29321
|
+
column: meta.column,
|
|
29322
|
+
lane: meta.lane,
|
|
29323
|
+
rules: resolveKanbanRules(this._kanbanRules, meta.record, meta),
|
|
29324
|
+
selected: this._isViewSelected(meta.id)
|
|
29325
|
+
}) : Array.isArray(options.contextMenu) ? options.contextMenu : [];
|
|
29326
|
+
if (Array.isArray(extra) && extra.length) items.push("-", ...extra);
|
|
29327
|
+
return items;
|
|
29328
|
+
}
|
|
29329
|
+
/**
|
|
29330
|
+
* @param {{value:unknown,item:MenuItem,context:Element|null}} detail Selection detail.
|
|
29331
|
+
* @returns {void}
|
|
29332
|
+
*/
|
|
29333
|
+
_handleMenuSelect(detail) {
|
|
29334
|
+
const value = (
|
|
29335
|
+
/** @type {{kind?:string,id?:string,ids?:unknown[]}|null} */
|
|
29336
|
+
detail.value
|
|
29337
|
+
);
|
|
29338
|
+
if (!value || typeof value !== "object" || !value.kind || !Array.isArray(value.ids)) return;
|
|
29339
|
+
this.moveRecords(value.ids, value.kind === "lane" ? { lane: value.id } : { column: value.id });
|
|
29340
|
+
}
|
|
29341
|
+
/**
|
|
29342
|
+
* Dry-runs the policy path for a destination without emitting or committing anything.
|
|
29343
|
+
* @param {unknown[]} ids Moving ids.
|
|
29344
|
+
* @param {KanbanDestination} destination Destination.
|
|
29345
|
+
* @returns {boolean} Whether the move would be allowed.
|
|
29346
|
+
*/
|
|
29347
|
+
_canMove(ids, destination) {
|
|
29348
|
+
const options = this._kanbanOptions();
|
|
29349
|
+
const moving = this._orderedIds(ids);
|
|
29350
|
+
if (!moving.length) return false;
|
|
29351
|
+
const records = moving.map((id) => (
|
|
29352
|
+
/** @type {KanbanRecord} */
|
|
29353
|
+
this.getRecord(id)
|
|
29354
|
+
));
|
|
29355
|
+
const origins = moving.map((id) => (
|
|
29356
|
+
/** @type {KanbanMovePoint} */
|
|
29357
|
+
this._recordLocation(id)
|
|
29358
|
+
));
|
|
29359
|
+
const from = origins[0];
|
|
29360
|
+
const columnId = destination.column == null ? from.column : String(destination.column);
|
|
29361
|
+
const laneId = options.swimlaneBy == null ? null : destination.lane === void 0 ? from.lane : normalizeAxisId(destination.lane);
|
|
29362
|
+
const column = this._resolvedColumns().find((candidate) => candidate.id === columnId);
|
|
29363
|
+
const lane = options.swimlaneBy == null ? null : this._resolvedSwimlanes().find((candidate) => candidate.id === laneId) ?? null;
|
|
29364
|
+
if (!column || options.swimlaneBy != null && !lane) return false;
|
|
29365
|
+
const excluded = new Set(moving);
|
|
29366
|
+
const to = { column: columnId, lane: laneId, index: 0 };
|
|
29367
|
+
const context = {
|
|
29368
|
+
id: moving[0],
|
|
29369
|
+
from: { ...from },
|
|
29370
|
+
to: { ...to },
|
|
29371
|
+
column: { ...column },
|
|
29372
|
+
swimlane: lane ? { ...lane } : null
|
|
29373
|
+
};
|
|
29374
|
+
return this._evaluateMove(records, origins, column, lane, to, context, excluded).allowed;
|
|
29375
|
+
}
|
|
29376
|
+
/**
|
|
29377
|
+
* Resolves the ids one gesture moves: the whole selection when the grabbed card belongs to it,
|
|
29378
|
+
* otherwise just that card. Picking up an unselected card never drags the selection along.
|
|
29379
|
+
* @param {unknown} id Grabbed record id.
|
|
29380
|
+
* @returns {unknown[]} Ids to move, in board order.
|
|
29381
|
+
*/
|
|
29382
|
+
_movingIds(id) {
|
|
29383
|
+
const options = this._kanbanOptions();
|
|
29384
|
+
if (options.multiMove === false || options.selectable !== "multi" || !this._isViewSelected(id)) return [id];
|
|
29385
|
+
const selection = this.getSelectionIds();
|
|
29386
|
+
return selection.length > 1 ? selection : [id];
|
|
29387
|
+
}
|
|
29388
|
+
/** @param {unknown[]} ids Requested ids. @returns {unknown[]} Existing ids in board order. */
|
|
29389
|
+
_orderedIds(ids) {
|
|
29390
|
+
const requested = new Set(ids);
|
|
29391
|
+
return this._viewData.map((record) => this._viewRecordId(record)).filter((id) => requested.has(id));
|
|
29392
|
+
}
|
|
28199
29393
|
/** @param {MouseEvent} event @returns {void} */
|
|
28200
29394
|
_handleBoardClick(event) {
|
|
29395
|
+
if (this._suppressClick) {
|
|
29396
|
+
this._suppressClick = false;
|
|
29397
|
+
return;
|
|
29398
|
+
}
|
|
28201
29399
|
const target = elementTarget(event);
|
|
28202
29400
|
const collapse = target?.closest("[data-kanban-collapse]");
|
|
28203
29401
|
if (collapse) {
|
|
@@ -28209,6 +29407,23 @@ var zx = (() => {
|
|
|
28209
29407
|
else this.toggleSwimlane(String(element.dataset.kanbanId));
|
|
28210
29408
|
return;
|
|
28211
29409
|
}
|
|
29410
|
+
const add = target?.closest("[data-kanban-add]");
|
|
29411
|
+
if (add) {
|
|
29412
|
+
const element = (
|
|
29413
|
+
/** @type {HTMLElement} */
|
|
29414
|
+
add
|
|
29415
|
+
);
|
|
29416
|
+
const columnId = String(element.dataset.kanbanAdd);
|
|
29417
|
+
const laneId = element.dataset.kanbanLane || null;
|
|
29418
|
+
this.emit("recordadd", {
|
|
29419
|
+
column: this._resolvedColumns().find((candidate) => candidate.id === columnId) ?? null,
|
|
29420
|
+
swimlane: laneId == null ? null : this._resolvedSwimlanes().find((candidate) => candidate.id === laneId) ?? null,
|
|
29421
|
+
columnId,
|
|
29422
|
+
laneId,
|
|
29423
|
+
event
|
|
29424
|
+
});
|
|
29425
|
+
return;
|
|
29426
|
+
}
|
|
28212
29427
|
const meta = this._metaForTarget(target);
|
|
28213
29428
|
if (!meta) return;
|
|
28214
29429
|
const selection = (
|
|
@@ -28257,7 +29472,14 @@ var zx = (() => {
|
|
|
28257
29472
|
}
|
|
28258
29473
|
/** @param {KeyboardEvent} event @returns {void} */
|
|
28259
29474
|
_handleBoardKeydown(event) {
|
|
29475
|
+
if (this._dragIds.length && event.key === "Escape") {
|
|
29476
|
+
event.preventDefault();
|
|
29477
|
+
this._clearDrag();
|
|
29478
|
+
this._announce(this._text("moveCanceledPlain"));
|
|
29479
|
+
return;
|
|
29480
|
+
}
|
|
28260
29481
|
const target = elementTarget(event);
|
|
29482
|
+
if (this._handleHistoryKeydown(event, target)) return;
|
|
28261
29483
|
const handle = target?.closest(".zx-kanban-view__move-handle");
|
|
28262
29484
|
const meta = this._metaForTarget(handle);
|
|
28263
29485
|
if (handle && meta) {
|
|
@@ -28281,12 +29503,33 @@ var zx = (() => {
|
|
|
28281
29503
|
this.toggleSelection(cardMeta.id);
|
|
28282
29504
|
}
|
|
28283
29505
|
}
|
|
29506
|
+
/**
|
|
29507
|
+
* @param {KeyboardEvent} event Keyboard event.
|
|
29508
|
+
* @param {Element|null} target Event target.
|
|
29509
|
+
* @returns {boolean} Whether the event was handled as a history command.
|
|
29510
|
+
*/
|
|
29511
|
+
_handleHistoryKeydown(event, target) {
|
|
29512
|
+
if (this._kanbanOptions().history === false || !(event.ctrlKey || event.metaKey)) return false;
|
|
29513
|
+
if (target?.closest('input, textarea, [contenteditable="true"]')) return false;
|
|
29514
|
+
const key = event.key.toLowerCase();
|
|
29515
|
+
if (key === "z" && !event.shiftKey && this.canUndo()) {
|
|
29516
|
+
event.preventDefault();
|
|
29517
|
+
this.undo();
|
|
29518
|
+
return true;
|
|
29519
|
+
}
|
|
29520
|
+
if ((key === "y" || key === "z" && event.shiftKey) && this.canRedo()) {
|
|
29521
|
+
event.preventDefault();
|
|
29522
|
+
this.redo();
|
|
29523
|
+
return true;
|
|
29524
|
+
}
|
|
29525
|
+
return false;
|
|
29526
|
+
}
|
|
28284
29527
|
/** @param {KeyboardEvent} event @param {HTMLElement} handle @param {KanbanCardMeta} meta @returns {void} */
|
|
28285
29528
|
_handleMoveKeydown(event, handle, meta) {
|
|
28286
29529
|
if (event.key === " " || event.key === "Enter") {
|
|
28287
29530
|
event.preventDefault();
|
|
28288
|
-
if (this._keyboardMove
|
|
28289
|
-
const
|
|
29531
|
+
if (this._keyboardMove?.ids.some((candidate) => Object.is(candidate, meta.id))) {
|
|
29532
|
+
const move2 = this._keyboardMove;
|
|
28290
29533
|
this._keyboardMove = null;
|
|
28291
29534
|
handle.setAttribute("aria-pressed", "false");
|
|
28292
29535
|
const card = (
|
|
@@ -28295,22 +29538,24 @@ var zx = (() => {
|
|
|
28295
29538
|
);
|
|
28296
29539
|
if (card) delete card.dataset.grabbed;
|
|
28297
29540
|
this._syncKeyboardTarget();
|
|
28298
|
-
this.
|
|
29541
|
+
this._commitInteractionMove(move2.ids, move2.to);
|
|
28299
29542
|
} else {
|
|
28300
29543
|
const from = this._recordLocation(meta.id);
|
|
28301
29544
|
if (!from) return;
|
|
28302
|
-
|
|
29545
|
+
const ids = this._movingIds(meta.id);
|
|
29546
|
+
const start = { ...from, index: this._visibleIndex(meta.id) };
|
|
29547
|
+
this._keyboardMove = { ids, from: { ...from }, to: start };
|
|
28303
29548
|
handle.setAttribute("aria-pressed", "true");
|
|
28304
29549
|
const card = handle.closest(".zx-record-card");
|
|
28305
29550
|
if (card) card.dataset.grabbed = "true";
|
|
28306
29551
|
this._syncKeyboardTarget();
|
|
28307
|
-
this._announce(
|
|
29552
|
+
this._announce(ids.length > 1 ? this._text("grabbedMultiple", { count: ids.length }) : this._text("grabbed", { name: this._recordName(meta.record, meta.index) }));
|
|
28308
29553
|
}
|
|
28309
29554
|
return;
|
|
28310
29555
|
}
|
|
28311
29556
|
if (event.key === "Escape" && this._keyboardMove) {
|
|
28312
29557
|
event.preventDefault();
|
|
28313
|
-
const
|
|
29558
|
+
const records = this._keyboardMove.ids.map((id) => this.getRecord(id)).filter(Boolean);
|
|
28314
29559
|
this._keyboardMove = null;
|
|
28315
29560
|
handle.setAttribute("aria-pressed", "false");
|
|
28316
29561
|
const card = (
|
|
@@ -28319,10 +29564,13 @@ var zx = (() => {
|
|
|
28319
29564
|
);
|
|
28320
29565
|
if (card) delete card.dataset.grabbed;
|
|
28321
29566
|
this._syncKeyboardTarget();
|
|
28322
|
-
this._announce(
|
|
29567
|
+
this._announce(records.length ? this._text("moveCanceled", { name: this._movingName(
|
|
29568
|
+
/** @type {KanbanRecord[]} */
|
|
29569
|
+
records
|
|
29570
|
+
) }) : this._text("moveCanceledPlain"));
|
|
28323
29571
|
return;
|
|
28324
29572
|
}
|
|
28325
|
-
if (!this._keyboardMove || !Object.is(
|
|
29573
|
+
if (!this._keyboardMove || !this._keyboardMove.ids.some((candidate) => Object.is(candidate, meta.id)) || !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return;
|
|
28326
29574
|
event.preventDefault();
|
|
28327
29575
|
const move = this._keyboardMove;
|
|
28328
29576
|
const laneModifier = event.altKey || event.ctrlKey || event.metaKey;
|
|
@@ -28339,10 +29587,14 @@ var zx = (() => {
|
|
|
28339
29587
|
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
28340
29588
|
move.to.index += event.key === "ArrowUp" ? -1 : 1;
|
|
28341
29589
|
}
|
|
28342
|
-
const targetCount = this.
|
|
28343
|
-
move.to.index =
|
|
29590
|
+
const targetCount = this._visibleCount(move.to.column, move.to.lane, new Set(move.ids));
|
|
29591
|
+
move.to.index = clampKanbanIndex(move.to.index, targetCount);
|
|
28344
29592
|
this._syncKeyboardTarget();
|
|
28345
|
-
this._announce(
|
|
29593
|
+
this._announce(this._text("target", {
|
|
29594
|
+
target: this._targetName(move.to),
|
|
29595
|
+
position: move.to.index + 1,
|
|
29596
|
+
total: targetCount + 1
|
|
29597
|
+
}));
|
|
28346
29598
|
}
|
|
28347
29599
|
/** @param {Event} event @returns {void} */
|
|
28348
29600
|
_handlePreviewError(event) {
|
|
@@ -28358,80 +29610,246 @@ var zx = (() => {
|
|
|
28358
29610
|
if (preview) preview.dataset.failed = "true";
|
|
28359
29611
|
image.hidden = true;
|
|
28360
29612
|
}
|
|
28361
|
-
/**
|
|
28362
|
-
|
|
28363
|
-
|
|
28364
|
-
|
|
28365
|
-
|
|
28366
|
-
|
|
28367
|
-
|
|
29613
|
+
/**
|
|
29614
|
+
* Records a candidate drag. Nothing moves yet: a press that never travels stays a click, and a
|
|
29615
|
+
* touch on the card body stays a scroll until the long press elapses.
|
|
29616
|
+
* @param {PointerEvent} event Pointer event.
|
|
29617
|
+
* @returns {void}
|
|
29618
|
+
*/
|
|
29619
|
+
_handlePointerDown(event) {
|
|
29620
|
+
this._suppressClick = false;
|
|
29621
|
+
if (event.button != null && event.button !== 0) return;
|
|
29622
|
+
const target = elementTarget(event);
|
|
29623
|
+
const handle = target?.closest(".zx-kanban-view__move-handle");
|
|
29624
|
+
const meta = this._metaForTarget(handle ?? target);
|
|
29625
|
+
if (!meta) return;
|
|
29626
|
+
if (!handle) {
|
|
29627
|
+
if (this._kanbanOptions().dragFrom !== "card") return;
|
|
29628
|
+
if (target?.closest(INTERACTIVE) || target?.closest(".zx-record-card__selection")) return;
|
|
29629
|
+
}
|
|
29630
|
+
const ids = this._movingIds(meta.id);
|
|
29631
|
+
this._pointerStart = {
|
|
29632
|
+
x: event.clientX,
|
|
29633
|
+
y: event.clientY,
|
|
29634
|
+
id: event.pointerId,
|
|
29635
|
+
ids,
|
|
29636
|
+
touch: event.pointerType === "touch"
|
|
29637
|
+
};
|
|
29638
|
+
this._dragPoint = { x: event.clientX, y: event.clientY };
|
|
29639
|
+
clearTimeout(this._pointerHold);
|
|
29640
|
+
if (event.pointerType !== "touch" || handle) return;
|
|
29641
|
+
this._pointerHold = setTimeout(() => this._beginDrag(this._dragPoint.x, this._dragPoint.y), TOUCH_HOLD);
|
|
29642
|
+
}
|
|
29643
|
+
/** @param {PointerEvent} event @returns {void} */
|
|
29644
|
+
_handlePointerMove(event) {
|
|
29645
|
+
const start = this._pointerStart;
|
|
29646
|
+
if (!this._dragIds.length) {
|
|
29647
|
+
if (!start || event.pointerId !== start.id) return;
|
|
29648
|
+
const distance = Math.hypot(event.clientX - start.x, event.clientY - start.y);
|
|
29649
|
+
this._dragPoint = { x: event.clientX, y: event.clientY };
|
|
29650
|
+
if (distance <= DRAG_THRESHOLD) return;
|
|
29651
|
+
if (start.touch && this._pointerHold) {
|
|
29652
|
+
clearTimeout(this._pointerHold);
|
|
29653
|
+
this._pointerHold = 0;
|
|
29654
|
+
this._pointerStart = null;
|
|
29655
|
+
return;
|
|
29656
|
+
}
|
|
29657
|
+
this._beginDrag(event.clientX, event.clientY);
|
|
29658
|
+
if (!this._dragIds.length) return;
|
|
28368
29659
|
}
|
|
28369
|
-
this.
|
|
28370
|
-
|
|
28371
|
-
|
|
28372
|
-
|
|
28373
|
-
|
|
29660
|
+
if (this._dragPointer != null && event.pointerId !== this._dragPointer) return;
|
|
29661
|
+
event.preventDefault();
|
|
29662
|
+
this._dragPoint = { x: event.clientX, y: event.clientY };
|
|
29663
|
+
this._positionDragPreview();
|
|
29664
|
+
this._updateDropTarget(event.clientX, event.clientY);
|
|
28374
29665
|
}
|
|
28375
|
-
/** @param {
|
|
28376
|
-
|
|
28377
|
-
if (this.
|
|
28378
|
-
|
|
28379
|
-
|
|
28380
|
-
|
|
29666
|
+
/** @param {PointerEvent} event @returns {void} */
|
|
29667
|
+
_handlePointerUp(event) {
|
|
29668
|
+
if (this._dragIds.length && this._dragPointer != null && event.pointerId !== this._dragPointer) return;
|
|
29669
|
+
clearTimeout(this._pointerHold);
|
|
29670
|
+
this._pointerHold = 0;
|
|
29671
|
+
this._pointerStart = null;
|
|
29672
|
+
if (!this._dragIds.length) return;
|
|
28381
29673
|
event.preventDefault();
|
|
28382
|
-
const
|
|
28383
|
-
this._dragDestination
|
|
29674
|
+
const ids = [...this._dragIds];
|
|
29675
|
+
const destination = this._dragDestination ? { ...this._dragDestination } : null;
|
|
29676
|
+
this._clearDrag();
|
|
29677
|
+
this._suppressClick = true;
|
|
29678
|
+
if (destination) this._commitInteractionMove(ids, destination);
|
|
29679
|
+
}
|
|
29680
|
+
/**
|
|
29681
|
+
* Starts a pointer drag: the floating preview appears, the board stops selecting text, and the
|
|
29682
|
+
* auto-scroll loop begins.
|
|
29683
|
+
* @param {number} x Pointer client x.
|
|
29684
|
+
* @param {number} y Pointer client y.
|
|
29685
|
+
* @returns {void}
|
|
29686
|
+
*/
|
|
29687
|
+
_beginDrag(x, y) {
|
|
29688
|
+
const start = this._pointerStart;
|
|
29689
|
+
if (!start || this._dragIds.length) return;
|
|
29690
|
+
this._pointerHold = 0;
|
|
29691
|
+
this._dragIds = start.ids.filter((id) => this.getRecord(id));
|
|
29692
|
+
if (!this._dragIds.length) return;
|
|
29693
|
+
this._dragPoint = { x, y };
|
|
29694
|
+
this._board.dataset.dragging = "true";
|
|
29695
|
+
for (const id of this._dragIds) {
|
|
29696
|
+
const card = this._cardsById.get(id);
|
|
29697
|
+
if (card) card.dataset.dragging = "true";
|
|
29698
|
+
}
|
|
29699
|
+
this._dragPointer = start.id;
|
|
29700
|
+
try {
|
|
29701
|
+
this._board.setPointerCapture(start.id);
|
|
29702
|
+
this._dragCaptured = true;
|
|
29703
|
+
} catch {
|
|
29704
|
+
}
|
|
29705
|
+
globalThis.getSelection?.()?.removeAllRanges();
|
|
29706
|
+
this._dragPreview = this._createDragPreview();
|
|
29707
|
+
this.el.append(this._dragPreview);
|
|
29708
|
+
this._positionDragPreview();
|
|
29709
|
+
this._updateDropTarget(x, y);
|
|
29710
|
+
if (this._kanbanOptions().autoScroll !== false) this._scheduleAutoScroll();
|
|
29711
|
+
}
|
|
29712
|
+
/** @returns {HTMLElement} Floating drag preview. */
|
|
29713
|
+
_createDragPreview() {
|
|
29714
|
+
const records = this._dragIds.map((id) => (
|
|
29715
|
+
/** @type {KanbanRecord} */
|
|
29716
|
+
this.getRecord(id)
|
|
29717
|
+
));
|
|
29718
|
+
const custom = this._renderHook("renderDragPreview", {
|
|
29719
|
+
records,
|
|
29720
|
+
ids: [...this._dragIds],
|
|
29721
|
+
count: this._dragIds.length
|
|
29722
|
+
});
|
|
29723
|
+
const content = custom ?? (this._dragIds.length > 1 ? this._text("dragPreview", { count: this._dragIds.length }) : this._movingName(records));
|
|
29724
|
+
return h("div", {
|
|
29725
|
+
class: "zx-kanban-view__drag-preview",
|
|
29726
|
+
ariaHidden: "true",
|
|
29727
|
+
dataset: { count: String(this._dragIds.length) }
|
|
29728
|
+
}, content);
|
|
29729
|
+
}
|
|
29730
|
+
/** @returns {void} */
|
|
29731
|
+
_positionDragPreview() {
|
|
29732
|
+
if (!this._dragPreview) return;
|
|
29733
|
+
this._dragPreview.style.transform = `translate3d(${Math.round(this._dragPoint.x)}px, ${Math.round(this._dragPoint.y)}px, 0)`;
|
|
29734
|
+
}
|
|
29735
|
+
/**
|
|
29736
|
+
* Resolves and marks the drop destination under the pointer. Pointer capture retargets events to
|
|
29737
|
+
* the board, so the element under the pointer is looked up rather than read off the event.
|
|
29738
|
+
* @param {number} x Pointer client x.
|
|
29739
|
+
* @param {number} y Pointer client y.
|
|
29740
|
+
* @returns {void}
|
|
29741
|
+
*/
|
|
29742
|
+
_updateDropTarget(x, y) {
|
|
29743
|
+
const element = document.elementFromPoint(x, y);
|
|
29744
|
+
const section = element?.closest(".zx-kanban-view__column");
|
|
29745
|
+
const sectionMeta = section && this._board.contains(section) ? this._sectionMeta.get(section) : null;
|
|
28384
29746
|
this._clearDropMarkers();
|
|
28385
|
-
|
|
28386
|
-
|
|
28387
|
-
|
|
28388
|
-
const rectangle = card.getBoundingClientRect();
|
|
28389
|
-
card.dataset.drop = event.clientY >= rectangle.top + rectangle.height / 2 ? "after" : "before";
|
|
28390
|
-
} else {
|
|
28391
|
-
sectionMeta.list.dataset.drop = "append";
|
|
29747
|
+
if (!sectionMeta || this._collapsedColumns.has(sectionMeta.column)) {
|
|
29748
|
+
this._dragDestination = null;
|
|
29749
|
+
return;
|
|
28392
29750
|
}
|
|
28393
|
-
|
|
29751
|
+
const cards = this._draggableCards(sectionMeta.list);
|
|
29752
|
+
const index = cards.findIndex((card) => {
|
|
29753
|
+
const rectangle = card.getBoundingClientRect();
|
|
29754
|
+
return y < rectangle.top + rectangle.height / 2;
|
|
29755
|
+
});
|
|
29756
|
+
const position2 = index < 0 ? cards.length : index;
|
|
29757
|
+
if (cards[position2]) cards[position2].dataset.drop = "before";
|
|
29758
|
+
else if (cards.length) cards[cards.length - 1].dataset.drop = "after";
|
|
29759
|
+
else sectionMeta.list.dataset.drop = "append";
|
|
29760
|
+
this._dragDestination = { column: sectionMeta.column, lane: sectionMeta.lane, index: position2 };
|
|
28394
29761
|
}
|
|
28395
|
-
/** @
|
|
28396
|
-
|
|
28397
|
-
if (this.
|
|
28398
|
-
|
|
28399
|
-
|
|
28400
|
-
|
|
28401
|
-
|
|
28402
|
-
|
|
28403
|
-
|
|
28404
|
-
|
|
28405
|
-
|
|
28406
|
-
|
|
28407
|
-
const
|
|
28408
|
-
const
|
|
28409
|
-
if (
|
|
28410
|
-
|
|
28411
|
-
|
|
28412
|
-
|
|
28413
|
-
const
|
|
28414
|
-
if (
|
|
28415
|
-
|
|
28416
|
-
|
|
28417
|
-
|
|
28418
|
-
|
|
28419
|
-
|
|
28420
|
-
|
|
29762
|
+
/** @returns {void} */
|
|
29763
|
+
_scheduleAutoScroll() {
|
|
29764
|
+
if (this._dragFrame) return;
|
|
29765
|
+
this._dragFrame = requestAnimationFrame(() => {
|
|
29766
|
+
this._dragFrame = 0;
|
|
29767
|
+
if (!this._dragIds.length) return;
|
|
29768
|
+
this._autoScroll();
|
|
29769
|
+
this._scheduleAutoScroll();
|
|
29770
|
+
});
|
|
29771
|
+
}
|
|
29772
|
+
/** Scrolls the board horizontally and the hovered column vertically near their edges. @returns {void} */
|
|
29773
|
+
_autoScroll() {
|
|
29774
|
+
const { x, y } = this._dragPoint;
|
|
29775
|
+
const board = this._board.getBoundingClientRect();
|
|
29776
|
+
if (x < board.left + SCROLL_EDGE) this._board.scrollLeft -= SCROLL_SPEED;
|
|
29777
|
+
else if (x > board.right - SCROLL_EDGE) this._board.scrollLeft += SCROLL_SPEED;
|
|
29778
|
+
const list = document.elementFromPoint(x, y)?.closest(".zx-kanban-view__cards");
|
|
29779
|
+
if (!list || !this._board.contains(list) || list.scrollHeight <= list.clientHeight) return;
|
|
29780
|
+
const bounds = list.getBoundingClientRect();
|
|
29781
|
+
if (y < bounds.top + SCROLL_EDGE) list.scrollTop -= SCROLL_SPEED;
|
|
29782
|
+
else if (y > bounds.bottom - SCROLL_EDGE) list.scrollTop += SCROLL_SPEED;
|
|
29783
|
+
}
|
|
29784
|
+
/**
|
|
29785
|
+
* Commits a move whose index came from rendered cards. Search and filters hide records without
|
|
29786
|
+
* removing them, so the visible position is translated back before anything is proposed.
|
|
29787
|
+
* @param {unknown[]} ids Moving ids.
|
|
29788
|
+
* @param {KanbanMovePoint} point Destination in visible-card space.
|
|
29789
|
+
* @returns {void}
|
|
29790
|
+
*/
|
|
29791
|
+
_commitInteractionMove(ids, point) {
|
|
29792
|
+
const excluded = new Set(ids);
|
|
29793
|
+
this.moveRecords(ids, {
|
|
29794
|
+
column: point.column,
|
|
29795
|
+
lane: point.lane,
|
|
29796
|
+
index: this._absoluteIndex(point.column, point.lane, point.index, excluded)
|
|
29797
|
+
});
|
|
28421
29798
|
}
|
|
28422
29799
|
/** @returns {void} */
|
|
28423
29800
|
_clearDrag() {
|
|
28424
|
-
this.
|
|
28425
|
-
|
|
28426
|
-
|
|
28427
|
-
this.
|
|
29801
|
+
if (this._dragFrame) cancelAnimationFrame(this._dragFrame);
|
|
29802
|
+
this._dragFrame = 0;
|
|
29803
|
+
clearTimeout(this._pointerHold);
|
|
29804
|
+
this._pointerHold = 0;
|
|
29805
|
+
this._pointerStart = null;
|
|
29806
|
+
this._dragPreview?.remove();
|
|
29807
|
+
this._dragPreview = null;
|
|
29808
|
+
this._dragIds = [];
|
|
28428
29809
|
this._dragDestination = null;
|
|
29810
|
+
if (this._board) {
|
|
29811
|
+
if (this._dragCaptured && this._dragPointer != null) {
|
|
29812
|
+
try {
|
|
29813
|
+
this._board.releasePointerCapture(this._dragPointer);
|
|
29814
|
+
} catch {
|
|
29815
|
+
}
|
|
29816
|
+
}
|
|
29817
|
+
this._clearDropMarkers();
|
|
29818
|
+
delete this._board.dataset.dragging;
|
|
29819
|
+
for (const card of this._board.querySelectorAll("[data-dragging]")) delete /** @type {HTMLElement} */
|
|
29820
|
+
card.dataset.dragging;
|
|
29821
|
+
}
|
|
29822
|
+
this._dragPointer = null;
|
|
29823
|
+
this._dragCaptured = false;
|
|
28429
29824
|
}
|
|
28430
29825
|
/** @returns {void} */
|
|
28431
29826
|
_clearDropMarkers() {
|
|
28432
29827
|
for (const element of this._board.querySelectorAll("[data-drop]")) delete /** @type {HTMLElement} */
|
|
28433
29828
|
element.dataset.drop;
|
|
28434
29829
|
}
|
|
29830
|
+
/** @param {HTMLElement} list Card list. @returns {HTMLElement[]} Cards excluding the ones in flight. */
|
|
29831
|
+
_draggableCards(list) {
|
|
29832
|
+
const moving = new Set(this._dragIds.length ? this._dragIds : this._keyboardMove?.ids ?? []);
|
|
29833
|
+
return (
|
|
29834
|
+
/** @type {HTMLElement[]} */
|
|
29835
|
+
[...list.querySelectorAll(":scope > .zx-record-card")].filter((card) => !hasId(moving, this._cardMeta.get(card)?.id))
|
|
29836
|
+
);
|
|
29837
|
+
}
|
|
29838
|
+
/** @param {string} column @param {string|null} lane @param {Set<unknown>} excluded @returns {number} */
|
|
29839
|
+
_visibleCount(column, lane, excluded) {
|
|
29840
|
+
return this._recordsIn(column, lane).filter((entry) => {
|
|
29841
|
+
const id = this._viewRecordId(entry.record);
|
|
29842
|
+
return !hasId(excluded, id) && this._visibleIds.has(id);
|
|
29843
|
+
}).length;
|
|
29844
|
+
}
|
|
29845
|
+
/** @param {unknown} id Record id. @returns {number} Position among visible cards in its bucket. */
|
|
29846
|
+
_visibleIndex(id) {
|
|
29847
|
+
const location = this._recordLocation(id);
|
|
29848
|
+
if (!location) return 0;
|
|
29849
|
+
const visible = this._recordsIn(location.column, location.lane).filter((entry) => this._visibleIds.has(this._viewRecordId(entry.record)));
|
|
29850
|
+
const position2 = visible.findIndex((entry) => Object.is(this._viewRecordId(entry.record), id));
|
|
29851
|
+
return position2 < 0 ? visible.length : position2;
|
|
29852
|
+
}
|
|
28435
29853
|
/** @returns {void} */
|
|
28436
29854
|
_syncKeyboardTarget() {
|
|
28437
29855
|
for (const element of this._board.querySelectorAll("[data-keyboard-target]")) {
|
|
@@ -28446,7 +29864,7 @@ var zx = (() => {
|
|
|
28446
29864
|
});
|
|
28447
29865
|
const sectionMeta = section ? this._sectionMeta.get(section) : null;
|
|
28448
29866
|
if (!sectionMeta) return;
|
|
28449
|
-
const cards =
|
|
29867
|
+
const cards = this._draggableCards(sectionMeta.list);
|
|
28450
29868
|
const target = cards[move.to.index];
|
|
28451
29869
|
if (target) target.dataset.keyboardTarget = "before";
|
|
28452
29870
|
else sectionMeta.list.dataset.keyboardTarget = "append";
|
|
@@ -28456,7 +29874,7 @@ var zx = (() => {
|
|
|
28456
29874
|
for (const [id, card] of this._cardsById) {
|
|
28457
29875
|
const selected = this._isViewSelected(id);
|
|
28458
29876
|
card.dataset.selected = String(selected);
|
|
28459
|
-
|
|
29877
|
+
this._applyCardDescription(card, id);
|
|
28460
29878
|
const input = (
|
|
28461
29879
|
/** @type {HTMLInputElement|null} */
|
|
28462
29880
|
card.querySelector("[data-record-selection]")
|
|
@@ -28476,6 +29894,50 @@ var zx = (() => {
|
|
|
28476
29894
|
this._cardsById.get(id)?.querySelector(".zx-kanban-view__move-handle") ?? null
|
|
28477
29895
|
);
|
|
28478
29896
|
}
|
|
29897
|
+
/**
|
|
29898
|
+
* Resolves a collapse control after a board refresh, retaining the same lane copy for columns.
|
|
29899
|
+
* @param {'column'|'swimlane'} axis Collapse axis.
|
|
29900
|
+
* @param {string} id Descriptor id.
|
|
29901
|
+
* @param {string|null} [lane=null] Column copy's lane.
|
|
29902
|
+
* @returns {HTMLElement|null}
|
|
29903
|
+
*/
|
|
29904
|
+
_collapseControl(axis, id, lane = null) {
|
|
29905
|
+
return (
|
|
29906
|
+
/** @type {HTMLElement|null} */
|
|
29907
|
+
[...this._board.querySelectorAll("[data-kanban-collapse]")].find((candidate) => {
|
|
29908
|
+
const control = (
|
|
29909
|
+
/** @type {HTMLElement} */
|
|
29910
|
+
candidate
|
|
29911
|
+
);
|
|
29912
|
+
if (control.dataset.kanbanCollapse !== axis || control.dataset.kanbanId !== id) return false;
|
|
29913
|
+
if (axis !== "column") return true;
|
|
29914
|
+
const section = control.closest(".zx-kanban-view__column");
|
|
29915
|
+
return section ? this._sectionMeta.get(section)?.lane === lane : false;
|
|
29916
|
+
}) ?? null
|
|
29917
|
+
);
|
|
29918
|
+
}
|
|
29919
|
+
/**
|
|
29920
|
+
* Runs one presentation hook, ignoring anything that is not a node so a hook cannot smuggle
|
|
29921
|
+
* markup in as a string.
|
|
29922
|
+
* @param {'renderCard'|'renderColumnHeader'|'renderSwimlaneHeader'|'renderColumnEmpty'|'renderDragPreview'} name Hook name.
|
|
29923
|
+
* @param {any} context Hook context.
|
|
29924
|
+
* @returns {Node|null} Node to adopt, or null for the default presentation.
|
|
29925
|
+
*/
|
|
29926
|
+
_renderHook(name, context) {
|
|
29927
|
+
const hook = this._kanbanOptions()[name];
|
|
29928
|
+
if (typeof hook !== "function") return null;
|
|
29929
|
+
const result = hook(context);
|
|
29930
|
+
return result && typeof result === "object" && "nodeType" in result ? (
|
|
29931
|
+
/** @type {Node} */
|
|
29932
|
+
result
|
|
29933
|
+
) : null;
|
|
29934
|
+
}
|
|
29935
|
+
/** @param {KanbanColumn} column @param {KanbanSwimlane|null} lane @returns {boolean} */
|
|
29936
|
+
_allowsAdd(column, lane) {
|
|
29937
|
+
const allow = this._kanbanOptions().allowAdd;
|
|
29938
|
+
if (typeof allow === "function") return allow(column, lane) === true;
|
|
29939
|
+
return allow === true;
|
|
29940
|
+
}
|
|
28479
29941
|
/** @param {KanbanRecord} record @param {number} index @returns {string} */
|
|
28480
29942
|
_recordName(record, index) {
|
|
28481
29943
|
const source = this._kanbanOptions().titleField;
|
|
@@ -28483,10 +29945,16 @@ var zx = (() => {
|
|
|
28483
29945
|
if (value && typeof value === "object" && "nodeType" in value) {
|
|
28484
29946
|
return (
|
|
28485
29947
|
/** @type {Node} */
|
|
28486
|
-
value.textContent?.trim() || "record"
|
|
29948
|
+
value.textContent?.trim() || this._text("record")
|
|
28487
29949
|
);
|
|
28488
29950
|
}
|
|
28489
|
-
return String(value ?? "record");
|
|
29951
|
+
return String(value ?? this._text("record"));
|
|
29952
|
+
}
|
|
29953
|
+
/** @param {KanbanRecord[]} records Moving records. @returns {string} */
|
|
29954
|
+
_movingName(records) {
|
|
29955
|
+
if (!records.length) return this._text("record");
|
|
29956
|
+
if (records.length === 1) return this._recordName(records[0], this._viewData.indexOf(records[0]));
|
|
29957
|
+
return this._text("dragPreview", { count: records.length });
|
|
28490
29958
|
}
|
|
28491
29959
|
/** @param {KanbanMovePoint} point @returns {string} */
|
|
28492
29960
|
_targetName(point) {
|
|
@@ -28494,6 +29962,26 @@ var zx = (() => {
|
|
|
28494
29962
|
const lane = point.lane == null ? null : this._resolvedSwimlanes().find((candidate) => candidate.id === point.lane);
|
|
28495
29963
|
return `${column?.label ?? point.column}${lane ? `, ${lane.label}` : ""}`;
|
|
28496
29964
|
}
|
|
29965
|
+
/** @param {KanbanMoveEvaluation} evaluation Capacity report. @returns {string} */
|
|
29966
|
+
_wipAnnouncement(evaluation) {
|
|
29967
|
+
if (evaluation.limit == null) return "";
|
|
29968
|
+
if (evaluation.limitExceeded) return this._text("limitExceeded", { limit: evaluation.limit });
|
|
29969
|
+
return evaluation.limitReached ? this._text("limitReached", { limit: evaluation.limit }) : "";
|
|
29970
|
+
}
|
|
29971
|
+
/**
|
|
29972
|
+
* Resolves one localizable string.
|
|
29973
|
+
* @param {keyof typeof DEFAULT_LABELS|string} key Label key.
|
|
29974
|
+
* @param {Record<string, unknown>} [args] Placeholder values.
|
|
29975
|
+
* @returns {string} Resolved text.
|
|
29976
|
+
*/
|
|
29977
|
+
_text(key, args) {
|
|
29978
|
+
const overrides = this._kanbanOptions().labels;
|
|
29979
|
+
const source = overrides && typeof overrides === "object" && key in overrides ? overrides[key] : (
|
|
29980
|
+
/** @type {Record<string,string>} */
|
|
29981
|
+
DEFAULT_LABELS[key]
|
|
29982
|
+
);
|
|
29983
|
+
return printf(source == null ? String(key) : String(source), args);
|
|
29984
|
+
}
|
|
28497
29985
|
/** @param {string} message @returns {void} */
|
|
28498
29986
|
_announce(message) {
|
|
28499
29987
|
const token = ++this._announcementToken;
|
|
@@ -28533,6 +30021,9 @@ var zx = (() => {
|
|
|
28533
30021
|
if (source.accept != null && typeof source.accept !== "function") {
|
|
28534
30022
|
throw new TypeError(`Kanban ${axis} ${id} accept must be a function`);
|
|
28535
30023
|
}
|
|
30024
|
+
if (source.from != null && !Array.isArray(source.from)) {
|
|
30025
|
+
throw new TypeError(`Kanban ${axis} ${id} from must be an array or null`);
|
|
30026
|
+
}
|
|
28536
30027
|
seen.add(id);
|
|
28537
30028
|
const normalized = (
|
|
28538
30029
|
/** @type {Record<string, any>} */
|
|
@@ -28540,11 +30031,21 @@ var zx = (() => {
|
|
|
28540
30031
|
...source,
|
|
28541
30032
|
id,
|
|
28542
30033
|
label: String(source.label ?? axisLabel(source.id)),
|
|
30034
|
+
limit: normalizeLimit2(source.limit),
|
|
30035
|
+
from: Array.isArray(source.from) ? source.from.map(String) : null,
|
|
28543
30036
|
...Object.prototype.hasOwnProperty.call(source, "value") ? {} : { value: source.id }
|
|
28544
30037
|
}
|
|
28545
30038
|
);
|
|
28546
|
-
if (axis === "column")
|
|
28547
|
-
|
|
30039
|
+
if (axis === "column") {
|
|
30040
|
+
const laneLimits = normalizeLaneLimits(source.laneLimits);
|
|
30041
|
+
if (laneLimits) normalized.laneLimits = laneLimits;
|
|
30042
|
+
if (source.wipPolicy === "block" || source.wipPolicy === "warn") {
|
|
30043
|
+
normalized.wipPolicy = source.wipPolicy;
|
|
30044
|
+
}
|
|
30045
|
+
} else {
|
|
30046
|
+
delete normalized.laneLimits;
|
|
30047
|
+
delete normalized.wipPolicy;
|
|
30048
|
+
}
|
|
28548
30049
|
return normalized;
|
|
28549
30050
|
})
|
|
28550
30051
|
);
|
|
@@ -28567,37 +30068,11 @@ var zx = (() => {
|
|
|
28567
30068
|
const preferred = normalizeOrder(order).filter((id) => allowed.has(id));
|
|
28568
30069
|
return [...new Set(preferred), ...available.filter((id) => !preferred.includes(id))];
|
|
28569
30070
|
}
|
|
28570
|
-
function
|
|
28571
|
-
if (!
|
|
28572
|
-
const
|
|
28573
|
-
|
|
28574
|
-
|
|
28575
|
-
const sourceColumn = normalizeAxisId(readKanbanAccessor(source, sourceIndex, options.columnBy));
|
|
28576
|
-
const sourceLane = options.swimlaneBy == null ? null : normalizeAxisId(readKanbanAccessor(source, sourceIndex, options.swimlaneBy));
|
|
28577
|
-
const sourceGroup = records.filter((record, index) => normalizeAxisId(readKanbanAccessor(record, index, options.columnBy)) === sourceColumn && (options.swimlaneBy == null || normalizeAxisId(readKanbanAccessor(record, index, options.swimlaneBy)) === sourceLane));
|
|
28578
|
-
const from = {
|
|
28579
|
-
column: sourceColumn,
|
|
28580
|
-
lane: sourceLane,
|
|
28581
|
-
index: sourceGroup.findIndex((record) => Object.is(viewRecordId(record, options.recordId), id))
|
|
28582
|
-
};
|
|
28583
|
-
const next = records.filter((_, index) => index !== sourceIndex);
|
|
28584
|
-
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));
|
|
28585
|
-
const targetIndex = normalizeMoveIndex(options.destination.index, destinationRecords.length);
|
|
28586
|
-
let insertion = next.length;
|
|
28587
|
-
if (destinationRecords[targetIndex]) insertion = next.indexOf(destinationRecords[targetIndex]);
|
|
28588
|
-
else if (destinationRecords.length) insertion = next.indexOf(destinationRecords.at(-1)) + 1;
|
|
28589
|
-
next.splice(insertion, 0, movedRecord);
|
|
28590
|
-
return {
|
|
28591
|
-
records: next,
|
|
28592
|
-
from,
|
|
28593
|
-
to: { column: options.destination.column, lane: options.swimlaneBy == null ? null : options.destination.lane, index: targetIndex }
|
|
28594
|
-
};
|
|
28595
|
-
}
|
|
28596
|
-
function readKanbanAccessor(record, index, accessor) {
|
|
28597
|
-
return typeof accessor === "function" ? accessor(record, index) : accessor == null ? null : record?.[accessor];
|
|
28598
|
-
}
|
|
28599
|
-
function normalizeAxisId(value) {
|
|
28600
|
-
return value == null ? "" : String(value);
|
|
30071
|
+
function reconcileKanbanPreference(descriptors, order, preserveUnknown = false) {
|
|
30072
|
+
if (!preserveUnknown) return reconcileKanbanOrder(descriptors, order);
|
|
30073
|
+
const preferred = normalizeOrder(order);
|
|
30074
|
+
const available = descriptors.map((descriptor) => descriptor.id);
|
|
30075
|
+
return [...preferred, ...available.filter((id) => !preferred.includes(id))];
|
|
28601
30076
|
}
|
|
28602
30077
|
function axisLabel(value) {
|
|
28603
30078
|
if (value == null || String(value).trim() === "") return "Unassigned";
|
|
@@ -28609,13 +30084,42 @@ var zx = (() => {
|
|
|
28609
30084
|
if (!Number.isFinite(number2) || number2 < 0) throw new TypeError("Kanban column limit must be a non-negative number");
|
|
28610
30085
|
return Math.floor(number2);
|
|
28611
30086
|
}
|
|
28612
|
-
function
|
|
28613
|
-
if (value == null
|
|
28614
|
-
|
|
30087
|
+
function normalizeLaneLimits(value) {
|
|
30088
|
+
if (value == null) return void 0;
|
|
30089
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
30090
|
+
throw new TypeError("Kanban column laneLimits must be an object");
|
|
30091
|
+
}
|
|
30092
|
+
const result = {};
|
|
30093
|
+
for (const [lane, limit] of Object.entries(
|
|
30094
|
+
/** @type {Record<string, unknown>} */
|
|
30095
|
+
value
|
|
30096
|
+
)) {
|
|
30097
|
+
const normalized = normalizeLimit2(limit);
|
|
30098
|
+
if (normalized != null) result[lane] = normalized;
|
|
30099
|
+
}
|
|
30100
|
+
return result;
|
|
30101
|
+
}
|
|
30102
|
+
function readColumnLaneLimit(column, lane) {
|
|
30103
|
+
const limits = column.laneLimits;
|
|
30104
|
+
if (!limits || typeof limits !== "object") return null;
|
|
30105
|
+
const value = limits[lane];
|
|
30106
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
30107
|
+
}
|
|
30108
|
+
function wipState(count2, limit) {
|
|
30109
|
+
if (limit == null) return "none";
|
|
30110
|
+
return count2 > limit ? "exceeded" : count2 === limit ? "limit" : "within";
|
|
28615
30111
|
}
|
|
28616
30112
|
function normalizeOrder(value) {
|
|
28617
30113
|
return Array.isArray(value) ? [...new Set(value.map(String))] : [];
|
|
28618
30114
|
}
|
|
30115
|
+
function stringifyValue(value) {
|
|
30116
|
+
if (value == null) return "";
|
|
30117
|
+
if (typeof value === "object") return "nodeType" in value ? String(
|
|
30118
|
+
/** @type {Node} */
|
|
30119
|
+
value.textContent ?? ""
|
|
30120
|
+
) : "";
|
|
30121
|
+
return String(value);
|
|
30122
|
+
}
|
|
28619
30123
|
function orderDescriptors(descriptors, order) {
|
|
28620
30124
|
const byId = new Map(descriptors.map((descriptor) => [descriptor.id, descriptor]));
|
|
28621
30125
|
return order.map((id) => byId.get(id)).filter(Boolean);
|
|
@@ -28629,12 +30133,28 @@ var zx = (() => {
|
|
|
28629
30133
|
next.splice(target + (position2 === "after" ? 1 : 0), 0, String(id));
|
|
28630
30134
|
return next;
|
|
28631
30135
|
}
|
|
30136
|
+
function mergeKanbanResolvedOrder(preference, descriptors, resolvedOrder, preserveUnknown) {
|
|
30137
|
+
if (!preserveUnknown) return [...resolvedOrder];
|
|
30138
|
+
const available = new Set(descriptors.map((descriptor) => descriptor.id));
|
|
30139
|
+
const replacements = [...resolvedOrder];
|
|
30140
|
+
let cursor = 0;
|
|
30141
|
+
const merged = normalizeOrder(preference).flatMap((id) => {
|
|
30142
|
+
if (!available.has(id)) return [id];
|
|
30143
|
+
return cursor < replacements.length ? [replacements[cursor++]] : [];
|
|
30144
|
+
});
|
|
30145
|
+
merged.push(...replacements.slice(cursor));
|
|
30146
|
+
return normalizeOrder(merged);
|
|
30147
|
+
}
|
|
28632
30148
|
function sameMovePoint(left, right) {
|
|
28633
30149
|
return left.column === right.column && left.lane === right.lane && left.index === right.index;
|
|
28634
30150
|
}
|
|
28635
30151
|
function sameArray3(left, right) {
|
|
28636
30152
|
return left.length === right.length && left.every((value, index) => Object.is(value, right[index]));
|
|
28637
30153
|
}
|
|
30154
|
+
function hasId(haystack, needle) {
|
|
30155
|
+
for (const candidate of haystack) if (Object.is(candidate, needle)) return true;
|
|
30156
|
+
return false;
|
|
30157
|
+
}
|
|
28638
30158
|
function elementTarget(event) {
|
|
28639
30159
|
const target = event.target;
|
|
28640
30160
|
if (!target) return null;
|