@zeyos/zx 4.2.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 +148 -1
- package/README.md +5 -3
- package/dist/types/components/calendar/calendar-model.d.ts +22 -0
- package/dist/types/components/calendar/calendar.d.ts +19 -2
- 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/dialog/dialog.d.ts +6 -1
- package/dist/types/components/dropdown/dropdown.d.ts +5 -0
- 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/components/launcher/launcher.d.ts +5 -0
- package/dist/types/components/message/message.d.ts +9 -1
- package/dist/types/components/modal/modal.d.ts +44 -7
- package/dist/types/components/sheet/sheet.d.ts +22 -12
- package/dist/types/core/component.d.ts +6 -1
- package/dist/types/core/overlay-host.d.ts +12 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/zx-zeyos.esm.js +173 -43
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +876 -230
- package/dist/zx.esm.js +2053 -338
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +2053 -338
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +250 -2
- package/docs/llms.md +67 -48
- package/docs/llms.txt +1 -3
- package/package.json +1 -2
- package/src/components/account-menu/account-menu.css +1 -1
- package/src/components/app-rail/app-rail.css +1 -1
- package/src/components/calendar/calendar-model.js +45 -0
- package/src/components/calendar/calendar.css +96 -34
- package/src/components/calendar/calendar.js +122 -29
- 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/context-menu/context-menu.css +2 -2
- package/src/components/context-menu/context-menu.js +5 -1
- package/src/components/date-picker/date-picker.css +18 -11
- package/src/components/datebox/date-range-box.css +4 -2
- package/src/components/datebox/datebox.css +4 -2
- package/src/components/dialog/dialog.css +4 -3
- package/src/components/dialog/dialog.js +2 -1
- package/src/components/dropdown/dropdown.css +4 -2
- package/src/components/dropdown/dropdown.js +7 -2
- 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/launcher/launcher.css +10 -8
- package/src/components/launcher/launcher.js +9 -2
- package/src/components/menu-button/menu-button.css +2 -2
- package/src/components/message/message.css +15 -25
- package/src/components/message/message.js +17 -11
- package/src/components/modal/modal.css +6 -4
- package/src/components/modal/modal.js +99 -25
- package/src/components/select/select.css +9 -7
- package/src/components/sheet/sheet.css +25 -7
- package/src/components/sheet/sheet.js +43 -23
- package/src/components/table/table.css +5 -3
- package/src/components/tag-picker/tag-picker.css +5 -3
- package/src/components/tooltip/tooltip.css +4 -2
- package/src/components/tooltip/tooltip.js +4 -1
- package/src/core/component.js +8 -3
- package/src/core/overlay-host.js +23 -0
- package/src/index.js +8 -3
- package/styles/tokens/dark.css +4 -0
- package/styles/tokens/semantic.css +83 -8
- 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
|
}
|
|
@@ -10527,6 +10544,15 @@ var zx = (() => {
|
|
|
10527
10544
|
element.replaceChildren(...state.children);
|
|
10528
10545
|
}
|
|
10529
10546
|
|
|
10547
|
+
// src/core/overlay-host.js
|
|
10548
|
+
var SCOPE_SELECTOR = ".zx-scope, [data-zx-theme], [data-zx-density], [data-zx-preset]";
|
|
10549
|
+
function overlayHost(source = null) {
|
|
10550
|
+
const context = source == null ? document.activeElement instanceof Element ? document.activeElement : null : resolveElement(source);
|
|
10551
|
+
const scope2 = context?.closest?.(SCOPE_SELECTOR) ?? null;
|
|
10552
|
+
if (!scope2 || scope2 === document.documentElement || scope2 === document.body) return document.body;
|
|
10553
|
+
return scope2;
|
|
10554
|
+
}
|
|
10555
|
+
|
|
10530
10556
|
// src/components/modal/modal.js
|
|
10531
10557
|
var Modal = class extends Component {
|
|
10532
10558
|
static cssName = "modal";
|
|
@@ -10536,13 +10562,17 @@ var zx = (() => {
|
|
|
10536
10562
|
width: "auto",
|
|
10537
10563
|
closable: true,
|
|
10538
10564
|
lightDismiss: false,
|
|
10539
|
-
destroyOnClose: false
|
|
10565
|
+
destroyOnClose: false,
|
|
10566
|
+
scope: null
|
|
10540
10567
|
};
|
|
10541
|
-
|
|
10542
|
-
#
|
|
10568
|
+
/** @type {Map<number, {result: unknown}>} */
|
|
10569
|
+
#pendingResults = /* @__PURE__ */ new Map();
|
|
10543
10570
|
#destroying = false;
|
|
10571
|
+
#presentation = 0;
|
|
10572
|
+
/** @type {number[]} */
|
|
10573
|
+
#nativeClosingPresentations = [];
|
|
10544
10574
|
/**
|
|
10545
|
-
* Creates a modal
|
|
10575
|
+
* Creates a modal in the configured theme scope, or at document level when none is present.
|
|
10546
10576
|
* @param {Element|string|null} [_target=null] Ignored; Modal always owns its root dialog.
|
|
10547
10577
|
* @param {ModalOptions} [options={}] Modal options.
|
|
10548
10578
|
*/
|
|
@@ -10555,14 +10585,7 @@ var zx = (() => {
|
|
|
10555
10585
|
if (!this.options.closable || cancelEvent.defaultPrevented) event.preventDefault();
|
|
10556
10586
|
});
|
|
10557
10587
|
this.listen(this.el, "close", () => {
|
|
10558
|
-
|
|
10559
|
-
this.el.dataset.state = "closed";
|
|
10560
|
-
delete this.el.dataset.zxOverlayOrder;
|
|
10561
|
-
const result = this.#hasPendingResult ? this.#pendingResult : this.el.returnValue || void 0;
|
|
10562
|
-
this.#pendingResult = void 0;
|
|
10563
|
-
this.#hasPendingResult = false;
|
|
10564
|
-
this.emit("close", { result });
|
|
10565
|
-
if (this.options.destroyOnClose && !this.#destroying) this.destroy();
|
|
10588
|
+
this._settleClose(this.#nativeClosingPresentations.shift());
|
|
10566
10589
|
});
|
|
10567
10590
|
this.listen(this.el, "click", (event) => {
|
|
10568
10591
|
if (!this.options.lightDismiss || event.target !== this.el || !isBackdropClick(this.el, event)) return;
|
|
@@ -10578,7 +10601,7 @@ var zx = (() => {
|
|
|
10578
10601
|
* @returns {Element}
|
|
10579
10602
|
*/
|
|
10580
10603
|
mountTarget() {
|
|
10581
|
-
return
|
|
10604
|
+
return overlayHost(this.options.scope);
|
|
10582
10605
|
}
|
|
10583
10606
|
/**
|
|
10584
10607
|
* Puts the dialog on screen. Overridden by subclasses that open non-modally.
|
|
@@ -10587,17 +10610,78 @@ var zx = (() => {
|
|
|
10587
10610
|
_show() {
|
|
10588
10611
|
this.el.showModal();
|
|
10589
10612
|
}
|
|
10613
|
+
/**
|
|
10614
|
+
* Removes the dialog from its current presentation layer.
|
|
10615
|
+
* Subclasses may replace native dialog presentation while preserving Modal lifecycle events.
|
|
10616
|
+
* @param {string} [result=''] Native string return value.
|
|
10617
|
+
* @param {number} [presentation=this.#presentation] Presentation generation being hidden.
|
|
10618
|
+
* @returns {void}
|
|
10619
|
+
*/
|
|
10620
|
+
_hide(result = "", presentation = this.#presentation) {
|
|
10621
|
+
this.#nativeClosingPresentations.push(presentation);
|
|
10622
|
+
try {
|
|
10623
|
+
this.el.close(result);
|
|
10624
|
+
} catch (error2) {
|
|
10625
|
+
this.#nativeClosingPresentations.pop();
|
|
10626
|
+
throw error2;
|
|
10627
|
+
}
|
|
10628
|
+
}
|
|
10629
|
+
/**
|
|
10630
|
+
* Shows a new presentation and gives it a generation of its own. Close events are queued by the
|
|
10631
|
+
* platform, so the generation lets an event from an earlier presentation expire harmlessly if
|
|
10632
|
+
* the same overlay has already reopened.
|
|
10633
|
+
* @returns {void}
|
|
10634
|
+
*/
|
|
10635
|
+
_present() {
|
|
10636
|
+
this._show();
|
|
10637
|
+
this.#presentation += 1;
|
|
10638
|
+
}
|
|
10639
|
+
/**
|
|
10640
|
+
* Hides the current presentation and records which generation its queued close event belongs to.
|
|
10641
|
+
* @param {string} [result=''] Native string return value.
|
|
10642
|
+
* @returns {void}
|
|
10643
|
+
*/
|
|
10644
|
+
_dismiss(result = "") {
|
|
10645
|
+
this._hide(result, this.#presentation);
|
|
10646
|
+
}
|
|
10647
|
+
/**
|
|
10648
|
+
* Reports whether the current presentation layer is visible.
|
|
10649
|
+
* @returns {boolean}
|
|
10650
|
+
*/
|
|
10651
|
+
_isShown() {
|
|
10652
|
+
return this.el.open;
|
|
10653
|
+
}
|
|
10590
10654
|
/**
|
|
10591
10655
|
* Whether an incoming `close` event is a real dismissal.
|
|
10592
10656
|
*
|
|
10593
|
-
*
|
|
10594
|
-
*
|
|
10595
|
-
*
|
|
10596
|
-
*
|
|
10597
|
-
* `destroyOnClose`. Returning false from an override drops it at the source instead.
|
|
10657
|
+
* `close` is dispatched in a queued task, not synchronously. By the time it arrives the overlay
|
|
10658
|
+
* may have reopened or crossed presentation layers, so only the event for the current hidden
|
|
10659
|
+
* generation may settle state, emit lifecycle events, or trigger `destroyOnClose`.
|
|
10660
|
+
* @param {number|undefined} closingPresentation Presentation that produced the event, if known.
|
|
10598
10661
|
* @returns {boolean}
|
|
10599
10662
|
*/
|
|
10600
|
-
_isRealClose() {
|
|
10663
|
+
_isRealClose(closingPresentation) {
|
|
10664
|
+
if (closingPresentation !== void 0 && closingPresentation !== this.#presentation) return false;
|
|
10665
|
+
return !this._isShown();
|
|
10666
|
+
}
|
|
10667
|
+
/**
|
|
10668
|
+
* Settles one presentation's close without allowing a queued event to invalidate a newer one.
|
|
10669
|
+
* Popover-based subclasses call this directly; native dialog events arrive through the listener.
|
|
10670
|
+
* @param {number|undefined} closingPresentation Presentation that was hidden, if known.
|
|
10671
|
+
* @returns {boolean} Whether this event closed the current presentation.
|
|
10672
|
+
*/
|
|
10673
|
+
_settleClose(closingPresentation) {
|
|
10674
|
+
if (!this._isRealClose(closingPresentation)) {
|
|
10675
|
+
if (closingPresentation !== void 0) this.#pendingResults.delete(closingPresentation);
|
|
10676
|
+
return false;
|
|
10677
|
+
}
|
|
10678
|
+
this.el.dataset.state = "closed";
|
|
10679
|
+
delete this.el.dataset.zxOverlayOrder;
|
|
10680
|
+
const pending = closingPresentation === void 0 ? void 0 : this.#pendingResults.get(closingPresentation);
|
|
10681
|
+
if (closingPresentation !== void 0) this.#pendingResults.delete(closingPresentation);
|
|
10682
|
+
const result = pending ? pending.result : this.el.returnValue || void 0;
|
|
10683
|
+
this.emit("close", { result });
|
|
10684
|
+
if (this.options.destroyOnClose && !this.#destroying) this.destroy();
|
|
10601
10685
|
return true;
|
|
10602
10686
|
}
|
|
10603
10687
|
/**
|
|
@@ -10622,10 +10706,12 @@ var zx = (() => {
|
|
|
10622
10706
|
*/
|
|
10623
10707
|
open() {
|
|
10624
10708
|
if (this.isOpen()) return this;
|
|
10709
|
+
const host = this.mountTarget();
|
|
10710
|
+
if (this.el.parentElement !== host) host.append(this.el);
|
|
10625
10711
|
this.el.dataset.zxOverlayOrder = String(nextOverlayOrder());
|
|
10626
10712
|
this.el.returnValue = "";
|
|
10627
10713
|
try {
|
|
10628
|
-
this.
|
|
10714
|
+
this._present();
|
|
10629
10715
|
} catch (error2) {
|
|
10630
10716
|
delete this.el.dataset.zxOverlayOrder;
|
|
10631
10717
|
throw error2;
|
|
@@ -10641,9 +10727,14 @@ var zx = (() => {
|
|
|
10641
10727
|
*/
|
|
10642
10728
|
close(result) {
|
|
10643
10729
|
if (!this.isOpen()) return this;
|
|
10644
|
-
|
|
10645
|
-
this.#
|
|
10646
|
-
|
|
10730
|
+
const presentation = this.#presentation;
|
|
10731
|
+
this.#pendingResults.set(presentation, { result });
|
|
10732
|
+
try {
|
|
10733
|
+
this._dismiss(typeof result === "string" ? result : "");
|
|
10734
|
+
} catch (error2) {
|
|
10735
|
+
this.#pendingResults.delete(presentation);
|
|
10736
|
+
throw error2;
|
|
10737
|
+
}
|
|
10647
10738
|
return this;
|
|
10648
10739
|
}
|
|
10649
10740
|
/**
|
|
@@ -10660,7 +10751,7 @@ var zx = (() => {
|
|
|
10660
10751
|
* @returns {boolean}
|
|
10661
10752
|
*/
|
|
10662
10753
|
isOpen() {
|
|
10663
|
-
return this.
|
|
10754
|
+
return this._isShown();
|
|
10664
10755
|
}
|
|
10665
10756
|
/**
|
|
10666
10757
|
* Closes and removes the owned dialog. Safe to call repeatedly.
|
|
@@ -10669,7 +10760,7 @@ var zx = (() => {
|
|
|
10669
10760
|
destroy() {
|
|
10670
10761
|
if (this.#destroying) return;
|
|
10671
10762
|
this.#destroying = true;
|
|
10672
|
-
if (this.isOpen()) this.
|
|
10763
|
+
if (this.isOpen()) this._dismiss();
|
|
10673
10764
|
super.destroy();
|
|
10674
10765
|
}
|
|
10675
10766
|
/** @param {string|number} width @returns {void} */
|
|
@@ -10734,7 +10825,7 @@ var zx = (() => {
|
|
|
10734
10825
|
/** @type {Element|null} */
|
|
10735
10826
|
#opener = null;
|
|
10736
10827
|
/**
|
|
10737
|
-
* Creates a structured dialog
|
|
10828
|
+
* Creates a structured dialog in the configured theme scope, or at document level when unscoped.
|
|
10738
10829
|
* @param {Element|string|null} [_target=null] Ignored; Dialog always owns its root.
|
|
10739
10830
|
* @param {DialogOptions} [options={}] Dialog options.
|
|
10740
10831
|
*/
|
|
@@ -15631,7 +15722,8 @@ var zx = (() => {
|
|
|
15631
15722
|
offset: 4,
|
|
15632
15723
|
matchWidth: false,
|
|
15633
15724
|
openOn: "click",
|
|
15634
|
-
closeOnSelect: false
|
|
15725
|
+
closeOnSelect: false,
|
|
15726
|
+
scope: null
|
|
15635
15727
|
};
|
|
15636
15728
|
/** @type {Element} */
|
|
15637
15729
|
#anchor;
|
|
@@ -15650,6 +15742,7 @@ var zx = (() => {
|
|
|
15650
15742
|
if (!resolved) throw new TypeError("Dropdown anchor could not be resolved");
|
|
15651
15743
|
super(null, options);
|
|
15652
15744
|
this.#anchor = resolved;
|
|
15745
|
+
overlayHost(this.options.scope ?? resolved).append(this.el);
|
|
15653
15746
|
this.#rememberAnchorAttribute("aria-expanded");
|
|
15654
15747
|
this.#rememberAnchorAttribute("aria-controls");
|
|
15655
15748
|
this.#anchor.setAttribute("aria-expanded", "false");
|
|
@@ -15672,7 +15765,6 @@ var zx = (() => {
|
|
|
15672
15765
|
popover: "manual"
|
|
15673
15766
|
});
|
|
15674
15767
|
panel.dataset.state = "closed";
|
|
15675
|
-
document.body.append(panel);
|
|
15676
15768
|
return panel;
|
|
15677
15769
|
}
|
|
15678
15770
|
/**
|
|
@@ -15682,6 +15774,8 @@ var zx = (() => {
|
|
|
15682
15774
|
*/
|
|
15683
15775
|
open() {
|
|
15684
15776
|
if (this.isOpen() || this.#destroyed) return this;
|
|
15777
|
+
const host = overlayHost(this.options.scope ?? this.#anchor);
|
|
15778
|
+
if (this.el.parentElement !== host) host.append(this.el);
|
|
15685
15779
|
this.#position = position(this.#anchor, this.el, {
|
|
15686
15780
|
placement: this.options.placement,
|
|
15687
15781
|
offset: Number(this.options.offset),
|
|
@@ -20341,7 +20435,8 @@ var zx = (() => {
|
|
|
20341
20435
|
emptyText: "No results",
|
|
20342
20436
|
loadingText: "Searching\u2026",
|
|
20343
20437
|
shortcut: "mod+k",
|
|
20344
|
-
hints: { move: "Move", open: "Open", close: "Close" }
|
|
20438
|
+
hints: { move: "Move", open: "Open", close: "Close" },
|
|
20439
|
+
scope: null
|
|
20345
20440
|
};
|
|
20346
20441
|
/**
|
|
20347
20442
|
* Creates an owned launcher dialog or enhances an existing `<dialog>` target.
|
|
@@ -20432,7 +20527,7 @@ var zx = (() => {
|
|
|
20432
20527
|
)
|
|
20433
20528
|
);
|
|
20434
20529
|
dialog.dataset.state = "closed";
|
|
20435
|
-
if (this._createdRoot)
|
|
20530
|
+
if (this._createdRoot) overlayHost(this.options.scope).append(dialog);
|
|
20436
20531
|
this.listen(input, "input", () => {
|
|
20437
20532
|
this._query = /** @type {HTMLInputElement} */
|
|
20438
20533
|
input.value;
|
|
@@ -20483,6 +20578,10 @@ var zx = (() => {
|
|
|
20483
20578
|
/** Opens the launcher and focuses/selects its query. @returns {this} @fires Launcher#open */
|
|
20484
20579
|
open() {
|
|
20485
20580
|
if (this.isOpen() || this._destroyed) return this;
|
|
20581
|
+
if (this._createdRoot) {
|
|
20582
|
+
const host = overlayHost(this.options.scope);
|
|
20583
|
+
if (this.el.parentElement !== host) host.append(this.el);
|
|
20584
|
+
}
|
|
20486
20585
|
this.el.showModal();
|
|
20487
20586
|
this.el.dataset.state = "open";
|
|
20488
20587
|
this.refs.input.setAttribute("aria-expanded", "true");
|
|
@@ -21361,6 +21460,7 @@ var zx = (() => {
|
|
|
21361
21460
|
if (!resolved) throw new TypeError("Tooltip anchor could not be resolved");
|
|
21362
21461
|
super(null, options);
|
|
21363
21462
|
this.#anchor = resolved;
|
|
21463
|
+
overlayHost(resolved).append(this.el);
|
|
21364
21464
|
this.#content = this.options.content;
|
|
21365
21465
|
this.#disabled = Boolean(this.options.disabled);
|
|
21366
21466
|
this.#describedBy = resolved.getAttribute("aria-describedby");
|
|
@@ -21421,7 +21521,6 @@ var zx = (() => {
|
|
|
21421
21521
|
popover: "manual"
|
|
21422
21522
|
});
|
|
21423
21523
|
panel.dataset.state = "closed";
|
|
21424
|
-
document.body.append(panel);
|
|
21425
21524
|
return panel;
|
|
21426
21525
|
}
|
|
21427
21526
|
/**
|
|
@@ -21433,6 +21532,8 @@ var zx = (() => {
|
|
|
21433
21532
|
show() {
|
|
21434
21533
|
if (this.#destroyed || this.#disabled || this.isOpen()) return this;
|
|
21435
21534
|
this.#cancelTimers();
|
|
21535
|
+
const host = overlayHost(this.#anchor);
|
|
21536
|
+
if (this.el.parentElement !== host) host.append(this.el);
|
|
21436
21537
|
const node = resolveContent(this.#content);
|
|
21437
21538
|
if (node === null) return this;
|
|
21438
21539
|
this.el.replaceChildren(node);
|
|
@@ -22879,8 +22980,8 @@ var zx = (() => {
|
|
|
22879
22980
|
closable: true,
|
|
22880
22981
|
maxVisible: 5
|
|
22881
22982
|
};
|
|
22882
|
-
/** @type {Message
|
|
22883
|
-
static #floating =
|
|
22983
|
+
/** @type {WeakMap<Element, Message>} */
|
|
22984
|
+
static #floating = /* @__PURE__ */ new WeakMap();
|
|
22884
22985
|
/** @returns {HTMLElement} */
|
|
22885
22986
|
render() {
|
|
22886
22987
|
const created = !this.el;
|
|
@@ -22959,15 +23060,16 @@ var zx = (() => {
|
|
|
22959
23060
|
* @returns {MessageHandle}
|
|
22960
23061
|
*/
|
|
22961
23062
|
static show(msg, options = {}) {
|
|
22962
|
-
return this.#floatingRegion().show(msg, options);
|
|
23063
|
+
return this.#floatingRegion(options.scope).show(msg, options);
|
|
22963
23064
|
}
|
|
22964
23065
|
/**
|
|
22965
23066
|
* Shows a floating progress status.
|
|
22966
23067
|
* @param {string} text Initial status text.
|
|
23068
|
+
* @param {{scope?: Element|string|null}} [options={}] Floating-region scope.
|
|
22967
23069
|
* @returns {ProgressHandle}
|
|
22968
23070
|
*/
|
|
22969
|
-
static progress(text4) {
|
|
22970
|
-
return this.#floatingRegion()._progress(text4);
|
|
23071
|
+
static progress(text4, options = {}) {
|
|
23072
|
+
return this.#floatingRegion(options.scope)._progress(text4);
|
|
22971
23073
|
}
|
|
22972
23074
|
/** @returns {void} */
|
|
22973
23075
|
destroy() {
|
|
@@ -23145,19 +23247,22 @@ var zx = (() => {
|
|
|
23145
23247
|
this._entries.delete(entry);
|
|
23146
23248
|
this._queue.remove(entry);
|
|
23147
23249
|
}
|
|
23148
|
-
/** @returns {Message} */
|
|
23149
|
-
static #floatingRegion() {
|
|
23150
|
-
|
|
23250
|
+
/** @param {Element|string|null|undefined} scope @returns {Message} */
|
|
23251
|
+
static #floatingRegion(scope2) {
|
|
23252
|
+
const host = overlayHost(scope2 ?? null);
|
|
23253
|
+
const existing = this.#floating.get(host);
|
|
23254
|
+
if (existing) return existing;
|
|
23151
23255
|
const region = h("div", {
|
|
23152
23256
|
class: "zx-message-region",
|
|
23153
23257
|
popover: "manual",
|
|
23154
23258
|
role: "status",
|
|
23155
23259
|
ariaLive: "polite"
|
|
23156
23260
|
});
|
|
23157
|
-
|
|
23158
|
-
|
|
23261
|
+
host.append(region);
|
|
23262
|
+
const floating = new _Message(region);
|
|
23263
|
+
this.#floating.set(host, floating);
|
|
23159
23264
|
if (typeof region.showPopover === "function") region.showPopover();
|
|
23160
|
-
return
|
|
23265
|
+
return floating;
|
|
23161
23266
|
}
|
|
23162
23267
|
};
|
|
23163
23268
|
function normalizeKind3(value) {
|
|
@@ -23245,7 +23350,6 @@ var zx = (() => {
|
|
|
23245
23350
|
constructor(_target = null, options = {}) {
|
|
23246
23351
|
super(null, options);
|
|
23247
23352
|
this._dock = null;
|
|
23248
|
-
this._handoff = false;
|
|
23249
23353
|
this.setSide(this.options.side);
|
|
23250
23354
|
this.el.dataset.backdrop = BACKDROPS.has(this.options.backdrop) ? this.options.backdrop : "dim";
|
|
23251
23355
|
this.el.dataset.modality = modality(this.options.modal);
|
|
@@ -23420,7 +23524,7 @@ var zx = (() => {
|
|
|
23420
23524
|
* @returns {Element}
|
|
23421
23525
|
*/
|
|
23422
23526
|
mountTarget() {
|
|
23423
|
-
return this._dock?.el ??
|
|
23527
|
+
return this._dock?.el ?? super.mountTarget();
|
|
23424
23528
|
}
|
|
23425
23529
|
/**
|
|
23426
23530
|
* Reports whether a dock currently owns this sheet's positioning.
|
|
@@ -23446,35 +23550,23 @@ var zx = (() => {
|
|
|
23446
23550
|
/**
|
|
23447
23551
|
* Moves the dialog to its current mount and, when it was open, reopens it there.
|
|
23448
23552
|
*
|
|
23449
|
-
* The close is unavoidable — a dialog cannot cross between
|
|
23450
|
-
*
|
|
23451
|
-
* `open()` so no lifecycle event is emitted for what is only a change of address.
|
|
23553
|
+
* The close is unavoidable — a dialog cannot cross between presentation layers while open —
|
|
23554
|
+
* but it is invisible: Modal's presentation generation drops the stale event, and reopening
|
|
23555
|
+
* bypasses `open()` so no lifecycle event is emitted for what is only a change of address.
|
|
23452
23556
|
* @returns {void}
|
|
23453
23557
|
*/
|
|
23454
23558
|
_rehost() {
|
|
23455
23559
|
const wasOpen = this.isOpen();
|
|
23456
|
-
if (wasOpen)
|
|
23457
|
-
this._handoff = true;
|
|
23458
|
-
this.el.close();
|
|
23459
|
-
}
|
|
23560
|
+
if (wasOpen) this._dismiss();
|
|
23460
23561
|
this.mountTarget().append(this.el);
|
|
23461
23562
|
if (!wasOpen) return;
|
|
23462
23563
|
this.el.returnValue = "";
|
|
23463
|
-
this.
|
|
23564
|
+
this._present();
|
|
23464
23565
|
this.el.dataset.state = "open";
|
|
23465
23566
|
if (this._dock) delete /** @type {HTMLElement} */
|
|
23466
23567
|
this.el.dataset.zxOverlayOrder;
|
|
23467
23568
|
else this.el.dataset.zxOverlayOrder = String(nextOverlayOrder3());
|
|
23468
23569
|
}
|
|
23469
|
-
/**
|
|
23470
|
-
* Drops the stale `close` a re-hosting handoff leaves behind.
|
|
23471
|
-
* @returns {boolean}
|
|
23472
|
-
*/
|
|
23473
|
-
_isRealClose() {
|
|
23474
|
-
if (!this._handoff) return true;
|
|
23475
|
-
this._handoff = false;
|
|
23476
|
-
return false;
|
|
23477
|
-
}
|
|
23478
23570
|
/**
|
|
23479
23571
|
* Opens the sheet, trapping focus first where the modality asks for it.
|
|
23480
23572
|
* @returns {this}
|
|
@@ -23555,10 +23647,38 @@ var zx = (() => {
|
|
|
23555
23647
|
*/
|
|
23556
23648
|
_show() {
|
|
23557
23649
|
if (this.options.modal === true && !this._dock) {
|
|
23650
|
+
this.el.removeAttribute("popover");
|
|
23651
|
+
this.el.removeAttribute("aria-modal");
|
|
23558
23652
|
super._show();
|
|
23559
23653
|
return;
|
|
23560
23654
|
}
|
|
23561
|
-
this.el.
|
|
23655
|
+
this.el.setAttribute("aria-modal", "false");
|
|
23656
|
+
if (this._dock) {
|
|
23657
|
+
this.el.removeAttribute("popover");
|
|
23658
|
+
this.el.show();
|
|
23659
|
+
return;
|
|
23660
|
+
}
|
|
23661
|
+
this.el.setAttribute("popover", "manual");
|
|
23662
|
+
this.el.showPopover();
|
|
23663
|
+
}
|
|
23664
|
+
/**
|
|
23665
|
+
* Hides a native dialog or a non-modal Sheet promoted through the popover top layer.
|
|
23666
|
+
* @param {string} [result=''] Native string return value.
|
|
23667
|
+
* @param {number} [presentation] Presentation generation being hidden.
|
|
23668
|
+
* @returns {void}
|
|
23669
|
+
*/
|
|
23670
|
+
_hide(result = "", presentation) {
|
|
23671
|
+
if (!this.el.matches(":popover-open")) {
|
|
23672
|
+
super._hide(result, presentation);
|
|
23673
|
+
return;
|
|
23674
|
+
}
|
|
23675
|
+
this.el.returnValue = result;
|
|
23676
|
+
this.el.hidePopover();
|
|
23677
|
+
queueMicrotask(() => this._settleClose(presentation));
|
|
23678
|
+
}
|
|
23679
|
+
/** @returns {boolean} */
|
|
23680
|
+
_isShown() {
|
|
23681
|
+
return this.el.matches(":popover-open") || super._isShown();
|
|
23562
23682
|
}
|
|
23563
23683
|
/**
|
|
23564
23684
|
* Releases the focus trap and the owned dialog.
|
|
@@ -23883,7 +24003,11 @@ var zx = (() => {
|
|
|
23883
24003
|
super(null, options);
|
|
23884
24004
|
this.#target = resolved;
|
|
23885
24005
|
document.body.append(this.el);
|
|
23886
|
-
this.#dropdown = new Dropdown(this.el, {
|
|
24006
|
+
this.#dropdown = new Dropdown(this.el, {
|
|
24007
|
+
openOn: "manual",
|
|
24008
|
+
placement: "bottom-start",
|
|
24009
|
+
scope: resolved
|
|
24010
|
+
});
|
|
23887
24011
|
this.#panel = this.#dropdown.getPanel();
|
|
23888
24012
|
this.#panel.classList.add("zx-context-menu");
|
|
23889
24013
|
this.#panel.setAttribute("role", "menu");
|
|
@@ -24143,6 +24267,29 @@ var zx = (() => {
|
|
|
24143
24267
|
}
|
|
24144
24268
|
return { start, end };
|
|
24145
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
|
+
}
|
|
24146
24293
|
function normalizeCalendarEvents(records, options = {}) {
|
|
24147
24294
|
if (!Array.isArray(records)) throw new TypeError("Calendar events must be an array");
|
|
24148
24295
|
const readers = {
|
|
@@ -24586,7 +24733,7 @@ var zx = (() => {
|
|
|
24586
24733
|
durationEditable: Object.prototype.hasOwnProperty.call(changes, "durationEditable") ? changes.durationEditable : current.durationEditable,
|
|
24587
24734
|
data: changes.data ?? current.data
|
|
24588
24735
|
};
|
|
24589
|
-
const [next] = normalizeCalendarEvents([source]);
|
|
24736
|
+
const [next] = normalizeCalendarEvents([source], { dateUnit: this.options.dateUnit });
|
|
24590
24737
|
next.data = source.data;
|
|
24591
24738
|
this._events = this._events.map((event, eventIndex) => eventIndex === index ? next : event);
|
|
24592
24739
|
this._mutationVersions.delete(String(id));
|
|
@@ -24737,7 +24884,11 @@ var zx = (() => {
|
|
|
24737
24884
|
}).sort(compareEvents);
|
|
24738
24885
|
const row2 = h(
|
|
24739
24886
|
"section",
|
|
24740
|
-
{
|
|
24887
|
+
{
|
|
24888
|
+
class: "zx-calendar__agenda-day",
|
|
24889
|
+
role: "row",
|
|
24890
|
+
dataset: { date: calendarDayKey(day) }
|
|
24891
|
+
},
|
|
24741
24892
|
h("button", {
|
|
24742
24893
|
type: "button",
|
|
24743
24894
|
class: "zx-calendar__agenda-heading",
|
|
@@ -24747,11 +24898,13 @@ var zx = (() => {
|
|
|
24747
24898
|
disabled: this.options.disabled
|
|
24748
24899
|
}, this._format(day, { weekday: "long", day: "numeric", month: "long" }))
|
|
24749
24900
|
);
|
|
24901
|
+
const entries = h("div", { class: "zx-calendar__agenda-events", role: "gridcell" });
|
|
24750
24902
|
if (!events.length) {
|
|
24751
|
-
|
|
24903
|
+
entries.append(h("p", { class: "zx-calendar__agenda-empty" }, this.msg("noEvents")));
|
|
24752
24904
|
} else {
|
|
24753
|
-
for (const event of events)
|
|
24905
|
+
for (const event of events) entries.append(this._eventElement(event, { variant: "agenda", day }));
|
|
24754
24906
|
}
|
|
24907
|
+
row2.append(entries);
|
|
24755
24908
|
grid2.append(row2);
|
|
24756
24909
|
}
|
|
24757
24910
|
return grid2;
|
|
@@ -25021,6 +25174,7 @@ var zx = (() => {
|
|
|
25021
25174
|
dataset: {
|
|
25022
25175
|
eventId: key,
|
|
25023
25176
|
variant: context.variant,
|
|
25177
|
+
allDay: event.allDay ? "true" : null,
|
|
25024
25178
|
draggable: this._canMove(event) ? "true" : null
|
|
25025
25179
|
},
|
|
25026
25180
|
style: event.color ? { "--zx-calendar-event-color": event.color } : null
|
|
@@ -25287,7 +25441,9 @@ var zx = (() => {
|
|
|
25287
25441
|
const item = this._eventById(control.dataset.eventId);
|
|
25288
25442
|
const action = control.dataset.eventAction === "resize" ? "resize" : "move";
|
|
25289
25443
|
if (!item || (action === "move" ? !this._canMove(item) : !this._canResize(item))) return;
|
|
25290
|
-
|
|
25444
|
+
event.preventDefault();
|
|
25445
|
+
this._capturePointer(event.pointerId);
|
|
25446
|
+
const offset = action === "move" ? this._dragOffsetForEvent(item, event.clientX, event.clientY) : null;
|
|
25291
25447
|
this._pointer = {
|
|
25292
25448
|
type: "event",
|
|
25293
25449
|
pointerId: event.pointerId,
|
|
@@ -25297,6 +25453,8 @@ var zx = (() => {
|
|
|
25297
25453
|
event: cloneCalendarEvent(item),
|
|
25298
25454
|
action,
|
|
25299
25455
|
control,
|
|
25456
|
+
timeOffsetMinutes: offset?.minutes ?? 0,
|
|
25457
|
+
dayOffset: offset?.days ?? 0,
|
|
25300
25458
|
moved: false
|
|
25301
25459
|
};
|
|
25302
25460
|
return;
|
|
@@ -25305,7 +25463,8 @@ var zx = (() => {
|
|
|
25305
25463
|
if (!slot || !this.options.selectable) return;
|
|
25306
25464
|
const date = this._dateFromTarget(slot);
|
|
25307
25465
|
if (!date) return;
|
|
25308
|
-
|
|
25466
|
+
event.preventDefault();
|
|
25467
|
+
this._capturePointer(event.pointerId);
|
|
25309
25468
|
this._pointer = {
|
|
25310
25469
|
type: "selection",
|
|
25311
25470
|
pointerId: event.pointerId,
|
|
@@ -25338,7 +25497,13 @@ var zx = (() => {
|
|
|
25338
25497
|
const wrapper = pointer.control.closest(".zx-calendar__event");
|
|
25339
25498
|
if (wrapper) {
|
|
25340
25499
|
wrapper.dataset.state = "dragging";
|
|
25341
|
-
|
|
25500
|
+
if (pointer.action === "resize") {
|
|
25501
|
+
wrapper.style.transform = "";
|
|
25502
|
+
const height = this._timedPreviewHeight(proposed);
|
|
25503
|
+
if (height) wrapper.style.blockSize = height;
|
|
25504
|
+
} else {
|
|
25505
|
+
wrapper.style.transform = `translate(${event.clientX - pointer.startX}px, ${event.clientY - pointer.startY}px)`;
|
|
25506
|
+
}
|
|
25342
25507
|
wrapper.dataset.preview = this._eventTime(proposed, proposed.start);
|
|
25343
25508
|
}
|
|
25344
25509
|
this._announce(this.msg(
|
|
@@ -25353,6 +25518,7 @@ var zx = (() => {
|
|
|
25353
25518
|
const pointer = this._pointer;
|
|
25354
25519
|
if (!pointer || pointer.pointerId !== event.pointerId) return;
|
|
25355
25520
|
this._pointer = null;
|
|
25521
|
+
this._releasePointerCapture(pointer.pointerId);
|
|
25356
25522
|
this._clearSelectionPreview();
|
|
25357
25523
|
if (!pointer.moved) return;
|
|
25358
25524
|
event.preventDefault();
|
|
@@ -25370,6 +25536,7 @@ var zx = (() => {
|
|
|
25370
25536
|
_cancelPointer(event) {
|
|
25371
25537
|
if (!this._pointer || this._pointer.pointerId !== event.pointerId) return;
|
|
25372
25538
|
const control = this._pointer.control;
|
|
25539
|
+
this._releasePointerCapture(this._pointer.pointerId);
|
|
25373
25540
|
this._pointer = null;
|
|
25374
25541
|
this._clearSelectionPreview();
|
|
25375
25542
|
control.closest?.(".zx-calendar__event")?.removeAttribute("data-state");
|
|
@@ -25377,13 +25544,14 @@ var zx = (() => {
|
|
|
25377
25544
|
}
|
|
25378
25545
|
/** @param {CalendarEvent} event @param {'move'|'resize'} action @param {number} x @param {number} y @returns {CalendarEvent|null} */
|
|
25379
25546
|
_proposalAtPoint(event, action, x, y) {
|
|
25380
|
-
const
|
|
25547
|
+
const resizeTimed = action === "resize" && !calendarEventSpansDays(event);
|
|
25548
|
+
const timed = this._timeAtPoint(x, y, resizeTimed ? event.start : null);
|
|
25381
25549
|
if (timed && !calendarEventSpansDays(event)) {
|
|
25382
25550
|
if (action === "move") {
|
|
25383
25551
|
const duration = event.end.getTime() - event.start.getTime();
|
|
25384
25552
|
const next3 = cloneCalendarEvent(event);
|
|
25385
|
-
next3.start = timed;
|
|
25386
|
-
next3.end = new Date(
|
|
25553
|
+
next3.start = addMinutes(timed, -Number(this._pointer?.timeOffsetMinutes ?? 0));
|
|
25554
|
+
next3.end = new Date(next3.start.getTime() + duration);
|
|
25387
25555
|
return next3;
|
|
25388
25556
|
}
|
|
25389
25557
|
const next2 = cloneCalendarEvent(event);
|
|
@@ -25392,33 +25560,36 @@ var zx = (() => {
|
|
|
25392
25560
|
}
|
|
25393
25561
|
const day = this._dayAtPoint(x, y);
|
|
25394
25562
|
if (!day) return null;
|
|
25395
|
-
const
|
|
25563
|
+
const targetDay = addCalendarDays(day, -Number(this._pointer?.dayOffset ?? 0));
|
|
25564
|
+
const difference = calendarDayDifference(startOfCalendarDay(event.start), targetDay);
|
|
25396
25565
|
if (action === "move") return shiftCalendarEvent(event, { days: difference, minutes: 0 });
|
|
25397
25566
|
const next = cloneCalendarEvent(event);
|
|
25398
25567
|
const endDay = addCalendarDays(day, 1);
|
|
25399
25568
|
next.end = new Date(endDay.getTime());
|
|
25400
25569
|
return next.end > next.start ? next : null;
|
|
25401
25570
|
}
|
|
25402
|
-
/** @param {number} x @param {number} y @returns {Date|null} */
|
|
25403
|
-
_timeAtPoint(x, y) {
|
|
25571
|
+
/** @param {number} x @param {number} y @param {Date|null} [fixedDay=null] @returns {Date|null} */
|
|
25572
|
+
_timeAtPoint(x, y, fixedDay = null) {
|
|
25404
25573
|
const slots = this.refs.body.querySelector(".zx-calendar__slots");
|
|
25405
25574
|
if (!slots) return null;
|
|
25406
25575
|
const rect = slots.getBoundingClientRect();
|
|
25407
|
-
|
|
25576
|
+
const scroller = slots.closest(".zx-calendar__time-scroller");
|
|
25577
|
+
const viewport = scroller?.getBoundingClientRect();
|
|
25578
|
+
const left = viewport ? Math.max(rect.left, viewport.left) : rect.left;
|
|
25579
|
+
const right = viewport ? Math.min(rect.right, viewport.right) : rect.right;
|
|
25580
|
+
const top = viewport ? Math.max(rect.top, viewport.top) : rect.top;
|
|
25581
|
+
const bottom = viewport ? Math.min(rect.bottom, viewport.bottom) : rect.bottom;
|
|
25582
|
+
if (x < left || x > right || y < top || y > bottom || rect.width <= 0 || rect.height <= 0) return null;
|
|
25408
25583
|
const columns = Number(slots.closest('[role="grid"]')?.getAttribute("aria-colcount")) || 1;
|
|
25409
25584
|
const rows = Number(slots.closest('[role="grid"]')?.getAttribute("aria-rowcount")) || 1;
|
|
25410
|
-
const
|
|
25585
|
+
const fixedColumn = fixedDay ? this._timeGridDayIndex(fixedDay) : -1;
|
|
25586
|
+
const column = fixedColumn >= 0 ? fixedColumn : Math.max(0, Math.min(columns - 1, Math.floor((x - rect.left) / rect.width * columns)));
|
|
25411
25587
|
const row2 = Math.max(0, Math.min(rows - 1, Math.floor((y - rect.top) / rect.height * rows)));
|
|
25412
25588
|
const target = slots.querySelector(`[data-row-index="${row2}"][data-day-index="${column}"]`);
|
|
25413
25589
|
return target ? this._dateFromTarget(target) : null;
|
|
25414
25590
|
}
|
|
25415
25591
|
/** @param {number} x @param {number} y @returns {Date|null} */
|
|
25416
25592
|
_dayAtPoint(x, y) {
|
|
25417
|
-
const elements = typeof document.elementsFromPoint === "function" ? document.elementsFromPoint(x, y) : [];
|
|
25418
|
-
for (const element of elements) {
|
|
25419
|
-
const date = element instanceof HTMLElement ? element.dataset.date : null;
|
|
25420
|
-
if (date) return parseDayKey(date);
|
|
25421
|
-
}
|
|
25422
25593
|
const allDay = this.refs.body.querySelector(".zx-calendar__all-day-grid");
|
|
25423
25594
|
if (allDay) {
|
|
25424
25595
|
const rect = allDay.getBoundingClientRect();
|
|
@@ -25428,8 +25599,72 @@ var zx = (() => {
|
|
|
25428
25599
|
return parseDayKey(cells[index]?.dataset.date);
|
|
25429
25600
|
}
|
|
25430
25601
|
}
|
|
25602
|
+
for (const monthEvents of this.refs.body.querySelectorAll(".zx-calendar__month-events")) {
|
|
25603
|
+
const rect = monthEvents.getBoundingClientRect();
|
|
25604
|
+
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom || rect.width <= 0) continue;
|
|
25605
|
+
const start = parseDayKey(monthEvents.closest(".zx-calendar__month-week")?.dataset.weekStart);
|
|
25606
|
+
if (!start) continue;
|
|
25607
|
+
const index = Math.max(0, Math.min(6, Math.floor((x - rect.left) / rect.width * 7)));
|
|
25608
|
+
return addCalendarDays(start, index);
|
|
25609
|
+
}
|
|
25610
|
+
for (const agendaDay of this.refs.body.querySelectorAll(".zx-calendar__agenda-day")) {
|
|
25611
|
+
const rect = agendaDay.getBoundingClientRect();
|
|
25612
|
+
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) continue;
|
|
25613
|
+
return parseDayKey(agendaDay.dataset.date);
|
|
25614
|
+
}
|
|
25615
|
+
const elements = typeof document.elementsFromPoint === "function" ? document.elementsFromPoint(x, y) : [];
|
|
25616
|
+
for (const element of elements) {
|
|
25617
|
+
const dateTarget = element instanceof HTMLElement ? element.closest("[data-date]") : null;
|
|
25618
|
+
const date = dateTarget instanceof HTMLElement ? dateTarget.dataset.date : null;
|
|
25619
|
+
if (date) return parseDayKey(date);
|
|
25620
|
+
}
|
|
25431
25621
|
return null;
|
|
25432
25622
|
}
|
|
25623
|
+
/** @param {CalendarEvent} event @param {number} x @param {number} y @returns {{minutes: number, days: number}} */
|
|
25624
|
+
_dragOffsetForEvent(event, x, y) {
|
|
25625
|
+
if (!calendarEventSpansDays(event)) {
|
|
25626
|
+
const timed = this._timeAtPoint(x, y);
|
|
25627
|
+
if (timed) {
|
|
25628
|
+
const slot = normalizeSlot(this.options.slotDuration);
|
|
25629
|
+
const duration = Math.max(0, Math.round((event.end.getTime() - event.start.getTime()) / 6e4));
|
|
25630
|
+
const maxOffset = Math.max(0, duration - slot);
|
|
25631
|
+
const minutes = Math.round((timed.getTime() - event.start.getTime()) / 6e4);
|
|
25632
|
+
return { minutes: Math.max(0, Math.min(maxOffset, minutes)), days: 0 };
|
|
25633
|
+
}
|
|
25634
|
+
}
|
|
25635
|
+
const day = this._dayAtPoint(x, y);
|
|
25636
|
+
const days = day ? calendarDayDifference(startOfCalendarDay(event.start), day) : 0;
|
|
25637
|
+
return { minutes: 0, days: Math.max(0, days) };
|
|
25638
|
+
}
|
|
25639
|
+
/** @param {Date} day @returns {number} */
|
|
25640
|
+
_timeGridDayIndex(day) {
|
|
25641
|
+
const key = calendarDayKey(day);
|
|
25642
|
+
const headings = [...this.refs.body.querySelectorAll(".zx-calendar__day-heading")];
|
|
25643
|
+
return headings.findIndex((heading) => heading.dataset.date === key);
|
|
25644
|
+
}
|
|
25645
|
+
/** @param {CalendarEvent} event @returns {string|null} */
|
|
25646
|
+
_timedPreviewHeight(event) {
|
|
25647
|
+
const grid2 = this.refs.body.querySelector(".zx-calendar__time-grid");
|
|
25648
|
+
if (!grid2 || calendarEventSpansDays(event)) return null;
|
|
25649
|
+
const min = normalizeMinute(this.options.slotMinTime, 0);
|
|
25650
|
+
const max = normalizeMinute(this.options.slotMaxTime, 1440, true);
|
|
25651
|
+
const slot = normalizeSlot(this.options.slotDuration);
|
|
25652
|
+
const start = event.start.getHours() * 60 + event.start.getMinutes();
|
|
25653
|
+
const end = event.end.getHours() * 60 + event.end.getMinutes();
|
|
25654
|
+
const minutes = Math.max(slot, Math.min(max, end) - Math.max(min, start));
|
|
25655
|
+
return `${minutes / (max - min) * 100}%`;
|
|
25656
|
+
}
|
|
25657
|
+
/** @param {number} pointerId @returns {void} */
|
|
25658
|
+
_capturePointer(pointerId) {
|
|
25659
|
+
try {
|
|
25660
|
+
this.el.setPointerCapture?.(pointerId);
|
|
25661
|
+
} catch {
|
|
25662
|
+
}
|
|
25663
|
+
}
|
|
25664
|
+
/** @param {number} pointerId @returns {void} */
|
|
25665
|
+
_releasePointerCapture(pointerId) {
|
|
25666
|
+
if (this.el.hasPointerCapture?.(pointerId)) this.el.releasePointerCapture?.(pointerId);
|
|
25667
|
+
}
|
|
25433
25668
|
/** @param {Date} start @param {Date} end @returns {void} */
|
|
25434
25669
|
_previewSelection(start, end) {
|
|
25435
25670
|
const low = Math.min(start.getTime(), end.getTime());
|
|
@@ -25524,13 +25759,9 @@ var zx = (() => {
|
|
|
25524
25759
|
}
|
|
25525
25760
|
/** @param {number} direction @returns {this} */
|
|
25526
25761
|
_page(direction) {
|
|
25527
|
-
|
|
25528
|
-
|
|
25529
|
-
|
|
25530
|
-
else if (this._view === "week") date.setDate(date.getDate() + direction * 7);
|
|
25531
|
-
else if (this._view === "month") date.setMonth(date.getMonth() + direction);
|
|
25532
|
-
else date.setFullYear(date.getFullYear() + direction);
|
|
25533
|
-
return this.setDate(date);
|
|
25762
|
+
return this.setDate(calendarPageDate(this._view, this._date, direction, {
|
|
25763
|
+
agendaDays: this.options.agendaDays
|
|
25764
|
+
}));
|
|
25534
25765
|
}
|
|
25535
25766
|
/** @param {{start: Date, end: Date}} range @returns {string} */
|
|
25536
25767
|
_rangeTitle(range) {
|
|
@@ -26687,6 +26918,16 @@ var zx = (() => {
|
|
|
26687
26918
|
class: "zx-record-card__heading",
|
|
26688
26919
|
id: headingId
|
|
26689
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;
|
|
26690
26931
|
const selection = selectable === "multi" ? h("label", {
|
|
26691
26932
|
class: "zx-record-card__selection"
|
|
26692
26933
|
}, h("input", {
|
|
@@ -26708,19 +26949,19 @@ var zx = (() => {
|
|
|
26708
26949
|
h(
|
|
26709
26950
|
"div",
|
|
26710
26951
|
{ class: "zx-record-card__titles" },
|
|
26711
|
-
|
|
26952
|
+
titleRow,
|
|
26712
26953
|
isEmptyContent(subtitle) ? null : h("p", { class: "zx-record-card__subtitle" }, contentNode(subtitle))
|
|
26713
26954
|
),
|
|
26714
26955
|
actionArea
|
|
26715
26956
|
);
|
|
26716
|
-
const metadata =
|
|
26717
|
-
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);
|
|
26718
26959
|
const card = (
|
|
26719
26960
|
/** @type {HTMLLIElement} */
|
|
26720
26961
|
h("li", {
|
|
26721
26962
|
class: "zx-record-card",
|
|
26722
26963
|
tabindex: 0,
|
|
26723
|
-
|
|
26964
|
+
ariaDescription: selectable ? selected ? "Selected" : "Not selected" : void 0,
|
|
26724
26965
|
ariaLabelledby: headingId,
|
|
26725
26966
|
dataset: {
|
|
26726
26967
|
recordIndex: String(index),
|
|
@@ -26731,6 +26972,20 @@ var zx = (() => {
|
|
|
26731
26972
|
);
|
|
26732
26973
|
return card;
|
|
26733
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
|
+
}
|
|
26734
26989
|
function resolveRecordPreview(record, index, preview, previewAlt, fields = []) {
|
|
26735
26990
|
if (preview == null) return null;
|
|
26736
26991
|
const candidate = typeof preview === "function" ? preview(record, index) : readRecordSource(record, index, preview, fields, false);
|
|
@@ -26799,21 +27054,49 @@ var zx = (() => {
|
|
|
26799
27054
|
dataset: { failed: String(preview.rejected) }
|
|
26800
27055
|
}, fallback, image);
|
|
26801
27056
|
}
|
|
26802
|
-
function
|
|
26803
|
-
const
|
|
26804
|
-
|
|
26805
|
-
|
|
26806
|
-
|
|
26807
|
-
|
|
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(
|
|
26808
27080
|
"div",
|
|
26809
27081
|
{
|
|
26810
27082
|
class: "zx-record-card__field",
|
|
26811
|
-
dataset: { fieldId: field.id }
|
|
27083
|
+
dataset: { fieldId: field.id, ...slot ? { cardSlot: slot } : {} }
|
|
26812
27084
|
},
|
|
26813
27085
|
h("dt", { class: "zx-record-card__label" }, field.label),
|
|
26814
27086
|
h("dd", { class: "zx-record-card__value" }, contentNode(renderViewField(field, record, index)))
|
|
26815
27087
|
)));
|
|
26816
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
|
+
}
|
|
26817
27100
|
function createAction(action) {
|
|
26818
27101
|
const label = String(action.label ?? "");
|
|
26819
27102
|
const title = String(action.title ?? (label || action.id));
|
|
@@ -26883,7 +27166,7 @@ var zx = (() => {
|
|
|
26883
27166
|
try {
|
|
26884
27167
|
const base = globalThis.location?.href ?? "https://zx.invalid/";
|
|
26885
27168
|
const protocol = new URL(href, base).protocol.toLowerCase();
|
|
26886
|
-
return ["http:", "https:", "blob:"
|
|
27169
|
+
return ["http:", "https:", "blob:"].includes(protocol) ? href : null;
|
|
26887
27170
|
} catch {
|
|
26888
27171
|
return null;
|
|
26889
27172
|
}
|
|
@@ -26927,7 +27210,7 @@ var zx = (() => {
|
|
|
26927
27210
|
actions: [],
|
|
26928
27211
|
groupBy: null,
|
|
26929
27212
|
groupOrder: [],
|
|
26930
|
-
minCardWidth: "
|
|
27213
|
+
minCardWidth: "15rem",
|
|
26931
27214
|
maxColumns: null,
|
|
26932
27215
|
variant: "outlined",
|
|
26933
27216
|
loadingCount: 6,
|
|
@@ -27201,7 +27484,7 @@ var zx = (() => {
|
|
|
27201
27484
|
for (const [id, card] of this._cardNodesById) {
|
|
27202
27485
|
const selected = this._isViewSelected(id);
|
|
27203
27486
|
card.dataset.selected = String(selected);
|
|
27204
|
-
if (this.options.selectable) card.setAttribute("aria-
|
|
27487
|
+
if (this.options.selectable) card.setAttribute("aria-description", selected ? "Selected" : "Not selected");
|
|
27205
27488
|
const checkbox = (
|
|
27206
27489
|
/** @type {HTMLInputElement|null} */
|
|
27207
27490
|
card.querySelector("[data-record-selection]")
|
|
@@ -27345,11 +27628,11 @@ var zx = (() => {
|
|
|
27345
27628
|
return value == null || value === "" ? "" : String(value);
|
|
27346
27629
|
}
|
|
27347
27630
|
function normalizeCardWidth(value) {
|
|
27348
|
-
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";
|
|
27349
27632
|
const text4 = String(value ?? "").trim();
|
|
27350
|
-
if (!text4 || text4.length > 128 || /[;{}\u0000-\u001f\u007f]/.test(text4)) return "
|
|
27351
|
-
if (/(?:url|image|image-set|cross-fade|element|expression)\s*\(/i.test(text4)) return "
|
|
27352
|
-
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";
|
|
27353
27636
|
return text4;
|
|
27354
27637
|
}
|
|
27355
27638
|
function normalizeMaxColumns(value) {
|
|
@@ -27387,8 +27670,284 @@ var zx = (() => {
|
|
|
27387
27670
|
return value.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "empty";
|
|
27388
27671
|
}
|
|
27389
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
|
+
|
|
27390
27895
|
// src/components/kanban-view/kanban-view.js
|
|
27391
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
|
+
});
|
|
27392
27951
|
var KanbanView = class extends RecordView {
|
|
27393
27952
|
static cssName = "kanban-view";
|
|
27394
27953
|
/** @type {Readonly<KanbanViewOptions & RecordViewOptions>} */
|
|
@@ -27405,6 +27964,27 @@ var zx = (() => {
|
|
|
27405
27964
|
link: null,
|
|
27406
27965
|
actions: [],
|
|
27407
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: {},
|
|
27408
27988
|
columnOrder: [],
|
|
27409
27989
|
swimlaneOrder: [],
|
|
27410
27990
|
collapsedColumns: [],
|
|
@@ -27440,17 +28020,45 @@ var zx = (() => {
|
|
|
27440
28020
|
this._kanbanSwimlaneOrder = normalizeOrder(options.swimlaneOrder);
|
|
27441
28021
|
this._collapsedColumns = new Set(normalizeOrder(options.collapsedColumns));
|
|
27442
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);
|
|
27443
28027
|
this._keyboardMove = /** @type {KanbanKeyboardMove|null} */
|
|
27444
28028
|
null;
|
|
27445
|
-
this.
|
|
28029
|
+
this._dragIds = /** @type {unknown[]} */
|
|
28030
|
+
[];
|
|
27446
28031
|
this._dragDestination = /** @type {KanbanMovePoint|null} */
|
|
27447
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;
|
|
27448
28045
|
this._announcementToken = 0;
|
|
27449
28046
|
this._cardMeta = /* @__PURE__ */ new WeakMap();
|
|
27450
28047
|
this._sectionMeta = /* @__PURE__ */ new WeakMap();
|
|
27451
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;
|
|
27452
28058
|
this._toolbar = h("div", { class: "zx-record-view__toolbar zx-kanban-view__toolbar" });
|
|
27453
|
-
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());
|
|
27454
28062
|
this._board = h("div", {
|
|
27455
28063
|
class: "zx-kanban-view__board",
|
|
27456
28064
|
role: "region",
|
|
@@ -27463,6 +28071,10 @@ var zx = (() => {
|
|
|
27463
28071
|
ariaAtomic: "true"
|
|
27464
28072
|
});
|
|
27465
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
|
+
}
|
|
27466
28078
|
this.listen(this._board, "click", (event) => this._handleBoardClick(
|
|
27467
28079
|
/** @type {MouseEvent} */
|
|
27468
28080
|
event
|
|
@@ -27476,20 +28088,22 @@ var zx = (() => {
|
|
|
27476
28088
|
event
|
|
27477
28089
|
));
|
|
27478
28090
|
this.listen(this._board, "error", (event) => this._handlePreviewError(event), { capture: true });
|
|
27479
|
-
this.listen(this._board, "
|
|
27480
|
-
/** @type {
|
|
28091
|
+
this.listen(this._board, "pointerdown", (event) => this._handlePointerDown(
|
|
28092
|
+
/** @type {PointerEvent} */
|
|
27481
28093
|
event
|
|
27482
28094
|
));
|
|
27483
|
-
this.listen(this._board, "
|
|
27484
|
-
/** @type {
|
|
28095
|
+
this.listen(this._board, "pointermove", (event) => this._handlePointerMove(
|
|
28096
|
+
/** @type {PointerEvent} */
|
|
27485
28097
|
event
|
|
27486
28098
|
));
|
|
27487
|
-
this.listen(this._board, "
|
|
27488
|
-
/** @type {
|
|
28099
|
+
this.listen(this._board, "pointerup", (event) => this._handlePointerUp(
|
|
28100
|
+
/** @type {PointerEvent} */
|
|
27489
28101
|
event
|
|
27490
28102
|
));
|
|
27491
|
-
this.listen(this._board, "
|
|
28103
|
+
this.listen(this._board, "pointercancel", () => this._clearDrag());
|
|
28104
|
+
this.listen(this._board, "dragstart", (event) => event.preventDefault());
|
|
27492
28105
|
this._refreshView("init");
|
|
28106
|
+
if (options.contextMenu) this._createContextMenu();
|
|
27493
28107
|
return root;
|
|
27494
28108
|
} catch (error2) {
|
|
27495
28109
|
if (this._kanbanSnapshot) restoreTarget(root, this._kanbanSnapshot);
|
|
@@ -27507,7 +28121,16 @@ var zx = (() => {
|
|
|
27507
28121
|
*/
|
|
27508
28122
|
setColumns(columns) {
|
|
27509
28123
|
this._kanbanColumns = columns === null ? null : normalizeKanbanDescriptors(columns, "column");
|
|
27510
|
-
|
|
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
|
+
}
|
|
27511
28134
|
this._refreshView("columns");
|
|
27512
28135
|
this._emitKanbanState("columns");
|
|
27513
28136
|
return this;
|
|
@@ -27523,7 +28146,17 @@ var zx = (() => {
|
|
|
27523
28146
|
*/
|
|
27524
28147
|
setSwimlanes(lanes) {
|
|
27525
28148
|
this._kanbanSwimlanes = lanes === null ? null : normalizeKanbanDescriptors(lanes, "swimlane");
|
|
27526
|
-
|
|
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
|
+
}
|
|
27527
28160
|
this._refreshView("swimlanes");
|
|
27528
28161
|
this._emitKanbanState("swimlanes");
|
|
27529
28162
|
return this;
|
|
@@ -27539,9 +28172,16 @@ var zx = (() => {
|
|
|
27539
28172
|
* @returns {this}
|
|
27540
28173
|
*/
|
|
27541
28174
|
setColumnOrder(order, options = {}) {
|
|
27542
|
-
const
|
|
27543
|
-
|
|
27544
|
-
|
|
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
|
+
);
|
|
27545
28185
|
this._refreshView("columnorder");
|
|
27546
28186
|
if (!options.silent) this._emitKanbanState("columnorder");
|
|
27547
28187
|
return this;
|
|
@@ -27567,9 +28207,17 @@ var zx = (() => {
|
|
|
27567
28207
|
* @returns {this}
|
|
27568
28208
|
*/
|
|
27569
28209
|
setSwimlaneOrder(order, options = {}) {
|
|
27570
|
-
const
|
|
27571
|
-
|
|
27572
|
-
|
|
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
|
+
);
|
|
27573
28221
|
this._refreshView("swimlaneorder");
|
|
27574
28222
|
if (!options.silent) this._emitKanbanState("swimlaneorder");
|
|
27575
28223
|
return this;
|
|
@@ -27637,126 +28285,292 @@ var zx = (() => {
|
|
|
27637
28285
|
return this.setSwimlaneCollapsed(id, !this._collapsedSwimlanes.has(String(id)));
|
|
27638
28286
|
}
|
|
27639
28287
|
/**
|
|
27640
|
-
* Proposes and, in local mode, commits a record move.
|
|
27641
|
-
* 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], …)`.
|
|
27642
28289
|
* @param {unknown} id Record id.
|
|
27643
28290
|
* @param {KanbanDestination} destination Destination.
|
|
27644
28291
|
* @returns {this}
|
|
27645
28292
|
* @fires KanbanView#recordmove
|
|
28293
|
+
* @fires KanbanView#movereject
|
|
27646
28294
|
* @fires RecordView#datachange
|
|
27647
28295
|
*/
|
|
27648
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) {
|
|
27649
28312
|
const options = this._kanbanOptions();
|
|
27650
|
-
|
|
27651
|
-
const
|
|
27652
|
-
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];
|
|
27653
28325
|
const columns = this._resolvedColumns();
|
|
27654
28326
|
const lanes = this._resolvedSwimlanes();
|
|
27655
28327
|
const columnId = destination.column == null ? from.column : String(destination.column);
|
|
27656
28328
|
const laneId = options.swimlaneBy == null ? null : destination.lane === void 0 ? from.lane : normalizeAxisId(destination.lane);
|
|
27657
28329
|
const column = columns.find((candidate) => candidate.id === columnId);
|
|
27658
|
-
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;
|
|
27659
28331
|
if (!column || options.swimlaneBy != null && !lane) {
|
|
27660
|
-
this.
|
|
28332
|
+
this._rejectMove("destination", { ids: moving, records, from, to: null, column: null, swimlane: null });
|
|
27661
28333
|
return this;
|
|
27662
28334
|
}
|
|
27663
|
-
const
|
|
27664
|
-
const
|
|
27665
|
-
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);
|
|
27666
28338
|
const to = { column: columnId, lane: laneId, index };
|
|
27667
|
-
if (sameMovePoint(from, to)) return this;
|
|
27668
|
-
const context = {
|
|
27669
|
-
|
|
27670
|
-
|
|
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
|
+
);
|
|
27671
28362
|
return this;
|
|
27672
28363
|
}
|
|
27673
|
-
|
|
27674
|
-
|
|
27675
|
-
|
|
27676
|
-
|
|
27677
|
-
|
|
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 });
|
|
27678
28370
|
return this;
|
|
27679
28371
|
}
|
|
28372
|
+
entries.push({ id: moving[position2], record: moved });
|
|
27680
28373
|
}
|
|
27681
|
-
const next =
|
|
28374
|
+
const next = reorderKanbanRecords(this._viewData, entries, {
|
|
27682
28375
|
recordId: options.recordId,
|
|
27683
28376
|
columnBy: (candidate, candidateIndex) => this._readAxis(options.columnBy, candidate, candidateIndex),
|
|
27684
28377
|
swimlaneBy: options.swimlaneBy == null ? null : (candidate, candidateIndex) => this._readAxis(options.swimlaneBy, candidate, candidateIndex),
|
|
27685
28378
|
destination: to
|
|
27686
28379
|
});
|
|
27687
28380
|
if (!next) {
|
|
27688
|
-
this.
|
|
28381
|
+
this._rejectMove("destination", { ids: moving, records, from, to, column, swimlane: lane });
|
|
27689
28382
|
return this;
|
|
27690
28383
|
}
|
|
27691
|
-
const
|
|
27692
|
-
const limitExceeded = column.limit != null && finalCount > column.limit;
|
|
28384
|
+
const wip = this._wipAnnouncement(evaluation);
|
|
27693
28385
|
const moveEvent = this.emit("recordmove", {
|
|
27694
|
-
record,
|
|
27695
|
-
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 } })),
|
|
27696
28391
|
from: { ...from },
|
|
27697
28392
|
to: { ...next.to },
|
|
27698
28393
|
column: { ...column },
|
|
27699
28394
|
swimlane: lane ? { ...lane } : null,
|
|
27700
|
-
limitExceeded
|
|
27701
|
-
});
|
|
28395
|
+
limitExceeded: evaluation.limitExceeded
|
|
28396
|
+
}, { honorDomCancellation: true });
|
|
27702
28397
|
if (moveEvent.defaultPrevented) {
|
|
27703
|
-
this._announce(
|
|
28398
|
+
this._announce(this._text("moveCanceled", { name: this._movingName(records) }));
|
|
27704
28399
|
return this;
|
|
27705
28400
|
}
|
|
27706
28401
|
if (options.moveMode === "external") {
|
|
27707
|
-
this._announce(
|
|
28402
|
+
this._announce(this._text("moveRequested", {
|
|
28403
|
+
name: this._movingName(records),
|
|
28404
|
+
target: this._targetName(next.to),
|
|
28405
|
+
wip
|
|
28406
|
+
}));
|
|
27708
28407
|
return this;
|
|
27709
28408
|
}
|
|
27710
|
-
this.
|
|
27711
|
-
this.
|
|
27712
|
-
|
|
27713
|
-
|
|
27714
|
-
|
|
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());
|
|
27715
28416
|
return this;
|
|
27716
28417
|
}
|
|
27717
|
-
/**
|
|
27718
|
-
|
|
27719
|
-
return
|
|
27720
|
-
|
|
27721
|
-
|
|
27722
|
-
|
|
27723
|
-
|
|
27724
|
-
collapsedSwimlanes: this.getCollapsedSwimlanes()
|
|
27725
|
-
};
|
|
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();
|
|
27726
28425
|
}
|
|
27727
28426
|
/**
|
|
27728
|
-
*
|
|
27729
|
-
* @param {Partial<KanbanViewState>|null} state Saved state.
|
|
27730
|
-
* @param {{silent?:boolean}} [options={}] Event behavior.
|
|
28427
|
+
* Reverts the last committed local move, restoring the exact record order and selection.
|
|
27731
28428
|
* @returns {this}
|
|
28429
|
+
* @fires RecordView#datachange
|
|
28430
|
+
* @fires KanbanView#historychange
|
|
27732
28431
|
*/
|
|
27733
|
-
|
|
27734
|
-
|
|
27735
|
-
|
|
27736
|
-
|
|
27737
|
-
|
|
27738
|
-
|
|
27739
|
-
|
|
27740
|
-
|
|
27741
|
-
|
|
27742
|
-
|
|
27743
|
-
);
|
|
27744
|
-
|
|
27745
|
-
|
|
27746
|
-
|
|
27747
|
-
|
|
27748
|
-
|
|
27749
|
-
|
|
27750
|
-
|
|
27751
|
-
|
|
27752
|
-
|
|
27753
|
-
|
|
27754
|
-
return this;
|
|
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");
|
|
28483
|
+
return this;
|
|
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
|
+
}
|
|
28514
|
+
/** Returns common plus board-specific serializable configuration. @returns {KanbanViewState} */
|
|
28515
|
+
getViewState() {
|
|
28516
|
+
this.getColumnOrder();
|
|
28517
|
+
this.getSwimlaneOrder();
|
|
28518
|
+
const derivedColumns = this._kanbanColumns === null;
|
|
28519
|
+
const derivedSwimlanes = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
28520
|
+
return {
|
|
28521
|
+
...super.getViewState(),
|
|
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()
|
|
28526
|
+
};
|
|
28527
|
+
}
|
|
28528
|
+
/**
|
|
28529
|
+
* Restores common and Kanban configuration. State from another record view is accepted safely.
|
|
28530
|
+
* @param {Partial<KanbanViewState>|null} state Saved state.
|
|
28531
|
+
* @param {{silent?:boolean}} [options={}] Event behavior.
|
|
28532
|
+
* @returns {this}
|
|
28533
|
+
*/
|
|
28534
|
+
setViewState(state, options = {}) {
|
|
28535
|
+
const source = state && typeof state === "object" ? state : {};
|
|
28536
|
+
super.setViewState(source, { silent: true });
|
|
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));
|
|
28553
|
+
if (Array.isArray(source.collapsedColumns)) {
|
|
28554
|
+
const collapsed = normalizeOrder(source.collapsedColumns);
|
|
28555
|
+
this._collapsedColumns = new Set(derivedColumns ? collapsed : collapsed.filter((id) => columnIds.has(id)));
|
|
28556
|
+
}
|
|
28557
|
+
if (Array.isArray(source.collapsedSwimlanes)) {
|
|
28558
|
+
const collapsed = normalizeOrder(source.collapsedSwimlanes);
|
|
28559
|
+
this._collapsedSwimlanes = new Set(derivedSwimlanes ? collapsed : collapsed.filter((id) => laneIds.has(id)));
|
|
28560
|
+
}
|
|
28561
|
+
this._refreshView("state");
|
|
28562
|
+
if (!options.silent) this._emitKanbanState("restore");
|
|
28563
|
+
return this;
|
|
27755
28564
|
}
|
|
27756
28565
|
/** Restores enhanced targets after base lifecycle cleanup. @returns {void} */
|
|
27757
28566
|
destroy() {
|
|
27758
28567
|
if (this._kanbanDestroyed) return;
|
|
27759
28568
|
this._kanbanDestroyed = true;
|
|
28569
|
+
this._clearDrag();
|
|
28570
|
+
clearTimeout(this._pointerHold);
|
|
28571
|
+
clearTimeout(this._searchTimer);
|
|
28572
|
+
this._kanbanMenu?.destroy();
|
|
28573
|
+
this._kanbanMenu = null;
|
|
27760
28574
|
const root = this.el;
|
|
27761
28575
|
const snapshot10 = this._kanbanSnapshot;
|
|
27762
28576
|
super.destroy();
|
|
@@ -27771,25 +28585,61 @@ var zx = (() => {
|
|
|
27771
28585
|
}
|
|
27772
28586
|
if (_reason === "loading") return;
|
|
27773
28587
|
const options = this._kanbanOptions();
|
|
28588
|
+
const activeElement = document.activeElement;
|
|
27774
28589
|
const activeMeta = this._metaForTarget(document.activeElement);
|
|
27775
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;
|
|
27776
28605
|
const columns = this._resolvedColumns();
|
|
27777
28606
|
const lanes = this._resolvedSwimlanes();
|
|
27778
28607
|
this._cardMeta = /* @__PURE__ */ new WeakMap();
|
|
27779
28608
|
this._sectionMeta = /* @__PURE__ */ new WeakMap();
|
|
27780
28609
|
this._cardsById = /* @__PURE__ */ new Map();
|
|
28610
|
+
this._resolveVisibility();
|
|
27781
28611
|
const content = [];
|
|
27782
28612
|
if (options.swimlaneBy == null) {
|
|
27783
28613
|
content.push(this._renderColumns(columns, null, false));
|
|
27784
28614
|
} else {
|
|
27785
28615
|
lanes.forEach((lane) => content.push(this._renderLane(lane, columns)));
|
|
27786
28616
|
}
|
|
27787
|
-
|
|
27788
|
-
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")));
|
|
27789
28619
|
this._board.replaceChildren(...content);
|
|
27790
|
-
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
|
+
}
|
|
27791
28624
|
this._syncKeyboardTarget();
|
|
27792
|
-
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
|
+
}
|
|
27793
28643
|
const card = this._cardsById.get(activeMeta.id);
|
|
27794
28644
|
const target = restoreHandle ? card?.querySelector(".zx-kanban-view__move-handle") : card;
|
|
27795
28645
|
if (target?.isConnected) target.focus();
|
|
@@ -27801,21 +28651,36 @@ var zx = (() => {
|
|
|
27801
28651
|
const collapsed = this._collapsedSwimlanes.has(lane.id);
|
|
27802
28652
|
const headingId = uid("zx-kanban-lane");
|
|
27803
28653
|
const bodyId = uid("zx-kanban-lane-body");
|
|
27804
|
-
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;
|
|
27805
28657
|
const toggle = h("button", {
|
|
27806
28658
|
class: "zx-kanban-view__collapse",
|
|
27807
28659
|
type: "button",
|
|
27808
28660
|
ariaExpanded: String(!collapsed),
|
|
27809
28661
|
ariaControls: bodyId,
|
|
27810
|
-
ariaLabel:
|
|
28662
|
+
ariaLabel: this._text(collapsed ? "expandLane" : "collapseLane", { label: lane.label }),
|
|
27811
28663
|
dataset: { kanbanCollapse: "swimlane", kanbanId: lane.id }
|
|
27812
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
|
+
});
|
|
27813
28676
|
const heading = h(
|
|
27814
28677
|
"h2",
|
|
27815
28678
|
{ class: "zx-kanban-view__lane-heading", id: headingId },
|
|
27816
28679
|
toggle,
|
|
27817
|
-
|
|
27818
|
-
|
|
28680
|
+
custom ?? [
|
|
28681
|
+
h("span", {}, lane.label),
|
|
28682
|
+
options.showCounts || limit != null ? this._countIndicator(count2, total, limit) : null
|
|
28683
|
+
]
|
|
27819
28684
|
);
|
|
27820
28685
|
const body = this._renderColumns(columns, lane, true);
|
|
27821
28686
|
body.id = bodyId;
|
|
@@ -27823,7 +28688,7 @@ var zx = (() => {
|
|
|
27823
28688
|
return h("section", {
|
|
27824
28689
|
class: "zx-kanban-view__lane",
|
|
27825
28690
|
ariaLabelledby: headingId,
|
|
27826
|
-
dataset: { collapsed: String(collapsed) }
|
|
28691
|
+
dataset: { collapsed: String(collapsed), wip }
|
|
27827
28692
|
}, heading, body);
|
|
27828
28693
|
}
|
|
27829
28694
|
/** @param {KanbanColumn[]} columns @param {KanbanSwimlane|null} lane @param {boolean} hasLanes @returns {HTMLElement} */
|
|
@@ -27849,36 +28714,67 @@ var zx = (() => {
|
|
|
27849
28714
|
const collapsed = this._collapsedColumns.has(column.id);
|
|
27850
28715
|
const headingId = uid("zx-kanban-column");
|
|
27851
28716
|
const listId = uid("zx-kanban-column-list");
|
|
27852
|
-
const
|
|
27853
|
-
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);
|
|
27854
28723
|
const toggle = h("button", {
|
|
27855
28724
|
class: "zx-kanban-view__collapse",
|
|
27856
28725
|
type: "button",
|
|
27857
28726
|
ariaExpanded: String(!collapsed),
|
|
27858
28727
|
ariaControls: listId,
|
|
27859
|
-
ariaLabel:
|
|
28728
|
+
ariaLabel: this._text(collapsed ? "expandColumn" : "collapseColumn", { label: column.label }),
|
|
27860
28729
|
dataset: { kanbanCollapse: "column", kanbanId: column.id }
|
|
27861
28730
|
}, icon(collapsed ? "chevron-right" : "chevron-down", { size: 13 }));
|
|
27862
|
-
const
|
|
27863
|
-
|
|
27864
|
-
|
|
27865
|
-
|
|
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;
|
|
27866
28742
|
const heading = h(hasLanes ? "h3" : "h2", {
|
|
27867
28743
|
class: "zx-kanban-view__column-heading",
|
|
27868
28744
|
id: headingId
|
|
27869
|
-
}, 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]);
|
|
27870
28746
|
const list = h("ul", {
|
|
27871
28747
|
class: "zx-kanban-view__cards",
|
|
27872
28748
|
id: listId,
|
|
27873
28749
|
ariaLabel: `${column.label}${lane ? `, ${lane.label}` : ""}`
|
|
27874
28750
|
});
|
|
27875
|
-
|
|
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
|
+
}
|
|
27876
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;
|
|
27877
28773
|
const section = h("section", {
|
|
27878
28774
|
class: "zx-kanban-view__column",
|
|
27879
28775
|
ariaLabelledby: headingId,
|
|
27880
28776
|
dataset: { column: column.id, collapsed: String(collapsed), wip }
|
|
27881
|
-
}, heading, list);
|
|
28777
|
+
}, heading, list, add);
|
|
27882
28778
|
this._sectionMeta.set(section, { column: column.id, lane: lane?.id ?? null, list });
|
|
27883
28779
|
return section;
|
|
27884
28780
|
}
|
|
@@ -27886,13 +28782,14 @@ var zx = (() => {
|
|
|
27886
28782
|
_renderCard(record, index, column, lane, hasLanes) {
|
|
27887
28783
|
const options = this._kanbanOptions();
|
|
27888
28784
|
const id = this._viewRecordId(record);
|
|
27889
|
-
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;
|
|
27890
28788
|
const handle = h("button", {
|
|
27891
28789
|
class: "zx-kanban-view__move-handle",
|
|
27892
28790
|
type: "button",
|
|
27893
|
-
draggable: true,
|
|
27894
28791
|
ariaPressed: String(grabbed),
|
|
27895
|
-
ariaLabel:
|
|
28792
|
+
ariaLabel: multiple > 1 ? this._text("moveMultiple", { count: multiple }) : this._text("move", { name: this._recordName(record, index) })
|
|
27896
28793
|
}, icon("drag", { size: 13 }));
|
|
27897
28794
|
const card = createRecordCard(record, index, {
|
|
27898
28795
|
fields: this.getFields(),
|
|
@@ -27911,15 +28808,81 @@ var zx = (() => {
|
|
|
27911
28808
|
});
|
|
27912
28809
|
card.tabIndex = 0;
|
|
27913
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);
|
|
27914
28814
|
const meta = { record, id, index, column, lane };
|
|
27915
28815
|
this._cardMeta.set(card, meta);
|
|
27916
28816
|
this._cardsById.set(id, card);
|
|
27917
28817
|
return card;
|
|
27918
28818
|
}
|
|
27919
|
-
/**
|
|
27920
|
-
|
|
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) {
|
|
27921
28884
|
const supplied = typeof this.options.emptyText === "function" ? this.options.emptyText() : this.options.emptyText;
|
|
27922
|
-
const content = supplied ??
|
|
28885
|
+
const content = supplied ?? text4;
|
|
27923
28886
|
return h(
|
|
27924
28887
|
"div",
|
|
27925
28888
|
{ class: "zx-kanban-view__empty", role: "status" },
|
|
@@ -27929,6 +28892,37 @@ var zx = (() => {
|
|
|
27929
28892
|
) : String(content)
|
|
27930
28893
|
);
|
|
27931
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
|
+
}
|
|
27932
28926
|
/** @returns {KanbanColumn[]} */
|
|
27933
28927
|
_rawColumns() {
|
|
27934
28928
|
const options = this._kanbanOptions();
|
|
@@ -27937,7 +28931,11 @@ var zx = (() => {
|
|
|
27937
28931
|
/** @returns {KanbanColumn[]} */
|
|
27938
28932
|
_resolvedColumns() {
|
|
27939
28933
|
const columns = this._rawColumns();
|
|
27940
|
-
this._kanbanColumnOrder =
|
|
28934
|
+
this._kanbanColumnOrder = reconcileKanbanPreference(
|
|
28935
|
+
columns,
|
|
28936
|
+
this._kanbanColumnOrder,
|
|
28937
|
+
this._kanbanColumns === null
|
|
28938
|
+
);
|
|
27941
28939
|
return orderDescriptors(columns, this._kanbanColumnOrder);
|
|
27942
28940
|
}
|
|
27943
28941
|
/** @returns {KanbanSwimlane[]} */
|
|
@@ -27949,7 +28947,12 @@ var zx = (() => {
|
|
|
27949
28947
|
/** @returns {KanbanSwimlane[]} */
|
|
27950
28948
|
_resolvedSwimlanes() {
|
|
27951
28949
|
const lanes = this._rawSwimlanes();
|
|
27952
|
-
this.
|
|
28950
|
+
const preserveUnknown = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
28951
|
+
this._kanbanSwimlaneOrder = reconcileKanbanPreference(
|
|
28952
|
+
lanes,
|
|
28953
|
+
this._kanbanSwimlaneOrder,
|
|
28954
|
+
preserveUnknown
|
|
28955
|
+
);
|
|
27953
28956
|
return orderDescriptors(lanes, this._kanbanSwimlaneOrder);
|
|
27954
28957
|
}
|
|
27955
28958
|
/** @param {KanbanAccessor} accessor @param {KanbanRecord} record @param {number} index @returns {unknown} */
|
|
@@ -27973,13 +28976,84 @@ var zx = (() => {
|
|
|
27973
28976
|
/** @param {unknown} id @returns {KanbanMovePoint|null} */
|
|
27974
28977
|
_recordLocation(id) {
|
|
27975
28978
|
const options = this._kanbanOptions();
|
|
27976
|
-
const index = this._viewData.findIndex((
|
|
28979
|
+
const index = this._viewData.findIndex((record) => Object.is(this._viewRecordId(record), id));
|
|
27977
28980
|
if (index < 0) return null;
|
|
27978
|
-
|
|
27979
|
-
|
|
27980
|
-
|
|
27981
|
-
|
|
27982
|
-
|
|
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
|
+
);
|
|
27983
29057
|
}
|
|
27984
29058
|
/**
|
|
27985
29059
|
* @param {KanbanRecord} record @param {KanbanMovePoint} from @param {KanbanMovePoint} to
|
|
@@ -28001,8 +29075,327 @@ var zx = (() => {
|
|
|
28001
29075
|
if (options.swimlaneBy != null && normalizeAxisId(this._readAxis(options.swimlaneBy, clone, currentIndex)) !== to.lane) return null;
|
|
28002
29076
|
return clone;
|
|
28003
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
|
+
}
|
|
28004
29393
|
/** @param {MouseEvent} event @returns {void} */
|
|
28005
29394
|
_handleBoardClick(event) {
|
|
29395
|
+
if (this._suppressClick) {
|
|
29396
|
+
this._suppressClick = false;
|
|
29397
|
+
return;
|
|
29398
|
+
}
|
|
28006
29399
|
const target = elementTarget(event);
|
|
28007
29400
|
const collapse = target?.closest("[data-kanban-collapse]");
|
|
28008
29401
|
if (collapse) {
|
|
@@ -28014,6 +29407,23 @@ var zx = (() => {
|
|
|
28014
29407
|
else this.toggleSwimlane(String(element.dataset.kanbanId));
|
|
28015
29408
|
return;
|
|
28016
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
|
+
}
|
|
28017
29427
|
const meta = this._metaForTarget(target);
|
|
28018
29428
|
if (!meta) return;
|
|
28019
29429
|
const selection = (
|
|
@@ -28062,7 +29472,14 @@ var zx = (() => {
|
|
|
28062
29472
|
}
|
|
28063
29473
|
/** @param {KeyboardEvent} event @returns {void} */
|
|
28064
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
|
+
}
|
|
28065
29481
|
const target = elementTarget(event);
|
|
29482
|
+
if (this._handleHistoryKeydown(event, target)) return;
|
|
28066
29483
|
const handle = target?.closest(".zx-kanban-view__move-handle");
|
|
28067
29484
|
const meta = this._metaForTarget(handle);
|
|
28068
29485
|
if (handle && meta) {
|
|
@@ -28086,12 +29503,33 @@ var zx = (() => {
|
|
|
28086
29503
|
this.toggleSelection(cardMeta.id);
|
|
28087
29504
|
}
|
|
28088
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
|
+
}
|
|
28089
29527
|
/** @param {KeyboardEvent} event @param {HTMLElement} handle @param {KanbanCardMeta} meta @returns {void} */
|
|
28090
29528
|
_handleMoveKeydown(event, handle, meta) {
|
|
28091
29529
|
if (event.key === " " || event.key === "Enter") {
|
|
28092
29530
|
event.preventDefault();
|
|
28093
|
-
if (this._keyboardMove
|
|
28094
|
-
const
|
|
29531
|
+
if (this._keyboardMove?.ids.some((candidate) => Object.is(candidate, meta.id))) {
|
|
29532
|
+
const move2 = this._keyboardMove;
|
|
28095
29533
|
this._keyboardMove = null;
|
|
28096
29534
|
handle.setAttribute("aria-pressed", "false");
|
|
28097
29535
|
const card = (
|
|
@@ -28100,22 +29538,24 @@ var zx = (() => {
|
|
|
28100
29538
|
);
|
|
28101
29539
|
if (card) delete card.dataset.grabbed;
|
|
28102
29540
|
this._syncKeyboardTarget();
|
|
28103
|
-
this.
|
|
29541
|
+
this._commitInteractionMove(move2.ids, move2.to);
|
|
28104
29542
|
} else {
|
|
28105
29543
|
const from = this._recordLocation(meta.id);
|
|
28106
29544
|
if (!from) return;
|
|
28107
|
-
|
|
29545
|
+
const ids = this._movingIds(meta.id);
|
|
29546
|
+
const start = { ...from, index: this._visibleIndex(meta.id) };
|
|
29547
|
+
this._keyboardMove = { ids, from: { ...from }, to: start };
|
|
28108
29548
|
handle.setAttribute("aria-pressed", "true");
|
|
28109
29549
|
const card = handle.closest(".zx-record-card");
|
|
28110
29550
|
if (card) card.dataset.grabbed = "true";
|
|
28111
29551
|
this._syncKeyboardTarget();
|
|
28112
|
-
this._announce(
|
|
29552
|
+
this._announce(ids.length > 1 ? this._text("grabbedMultiple", { count: ids.length }) : this._text("grabbed", { name: this._recordName(meta.record, meta.index) }));
|
|
28113
29553
|
}
|
|
28114
29554
|
return;
|
|
28115
29555
|
}
|
|
28116
29556
|
if (event.key === "Escape" && this._keyboardMove) {
|
|
28117
29557
|
event.preventDefault();
|
|
28118
|
-
const
|
|
29558
|
+
const records = this._keyboardMove.ids.map((id) => this.getRecord(id)).filter(Boolean);
|
|
28119
29559
|
this._keyboardMove = null;
|
|
28120
29560
|
handle.setAttribute("aria-pressed", "false");
|
|
28121
29561
|
const card = (
|
|
@@ -28124,10 +29564,13 @@ var zx = (() => {
|
|
|
28124
29564
|
);
|
|
28125
29565
|
if (card) delete card.dataset.grabbed;
|
|
28126
29566
|
this._syncKeyboardTarget();
|
|
28127
|
-
this._announce(
|
|
29567
|
+
this._announce(records.length ? this._text("moveCanceled", { name: this._movingName(
|
|
29568
|
+
/** @type {KanbanRecord[]} */
|
|
29569
|
+
records
|
|
29570
|
+
) }) : this._text("moveCanceledPlain"));
|
|
28128
29571
|
return;
|
|
28129
29572
|
}
|
|
28130
|
-
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;
|
|
28131
29574
|
event.preventDefault();
|
|
28132
29575
|
const move = this._keyboardMove;
|
|
28133
29576
|
const laneModifier = event.altKey || event.ctrlKey || event.metaKey;
|
|
@@ -28144,10 +29587,14 @@ var zx = (() => {
|
|
|
28144
29587
|
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
28145
29588
|
move.to.index += event.key === "ArrowUp" ? -1 : 1;
|
|
28146
29589
|
}
|
|
28147
|
-
const targetCount = this.
|
|
28148
|
-
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);
|
|
28149
29592
|
this._syncKeyboardTarget();
|
|
28150
|
-
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
|
+
}));
|
|
28151
29598
|
}
|
|
28152
29599
|
/** @param {Event} event @returns {void} */
|
|
28153
29600
|
_handlePreviewError(event) {
|
|
@@ -28163,80 +29610,246 @@ var zx = (() => {
|
|
|
28163
29610
|
if (preview) preview.dataset.failed = "true";
|
|
28164
29611
|
image.hidden = true;
|
|
28165
29612
|
}
|
|
28166
|
-
/**
|
|
28167
|
-
|
|
28168
|
-
|
|
28169
|
-
|
|
28170
|
-
|
|
28171
|
-
|
|
28172
|
-
|
|
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;
|
|
28173
29659
|
}
|
|
28174
|
-
this.
|
|
28175
|
-
|
|
28176
|
-
|
|
28177
|
-
|
|
28178
|
-
|
|
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);
|
|
28179
29665
|
}
|
|
28180
|
-
/** @param {
|
|
28181
|
-
|
|
28182
|
-
if (this.
|
|
28183
|
-
|
|
28184
|
-
|
|
28185
|
-
|
|
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;
|
|
28186
29673
|
event.preventDefault();
|
|
28187
|
-
const
|
|
28188
|
-
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;
|
|
28189
29746
|
this._clearDropMarkers();
|
|
28190
|
-
|
|
28191
|
-
|
|
28192
|
-
|
|
28193
|
-
const rectangle = card.getBoundingClientRect();
|
|
28194
|
-
card.dataset.drop = event.clientY >= rectangle.top + rectangle.height / 2 ? "after" : "before";
|
|
28195
|
-
} else {
|
|
28196
|
-
sectionMeta.list.dataset.drop = "append";
|
|
29747
|
+
if (!sectionMeta || this._collapsedColumns.has(sectionMeta.column)) {
|
|
29748
|
+
this._dragDestination = null;
|
|
29749
|
+
return;
|
|
28197
29750
|
}
|
|
28198
|
-
|
|
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 };
|
|
28199
29761
|
}
|
|
28200
|
-
/** @
|
|
28201
|
-
|
|
28202
|
-
if (this.
|
|
28203
|
-
|
|
28204
|
-
|
|
28205
|
-
|
|
28206
|
-
|
|
28207
|
-
|
|
28208
|
-
|
|
28209
|
-
|
|
28210
|
-
|
|
28211
|
-
|
|
28212
|
-
const
|
|
28213
|
-
const
|
|
28214
|
-
if (
|
|
28215
|
-
|
|
28216
|
-
|
|
28217
|
-
|
|
28218
|
-
const
|
|
28219
|
-
if (
|
|
28220
|
-
|
|
28221
|
-
|
|
28222
|
-
|
|
28223
|
-
|
|
28224
|
-
|
|
28225
|
-
|
|
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
|
+
});
|
|
28226
29798
|
}
|
|
28227
29799
|
/** @returns {void} */
|
|
28228
29800
|
_clearDrag() {
|
|
28229
|
-
this.
|
|
28230
|
-
|
|
28231
|
-
|
|
28232
|
-
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 = [];
|
|
28233
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;
|
|
28234
29824
|
}
|
|
28235
29825
|
/** @returns {void} */
|
|
28236
29826
|
_clearDropMarkers() {
|
|
28237
29827
|
for (const element of this._board.querySelectorAll("[data-drop]")) delete /** @type {HTMLElement} */
|
|
28238
29828
|
element.dataset.drop;
|
|
28239
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
|
+
}
|
|
28240
29853
|
/** @returns {void} */
|
|
28241
29854
|
_syncKeyboardTarget() {
|
|
28242
29855
|
for (const element of this._board.querySelectorAll("[data-keyboard-target]")) {
|
|
@@ -28251,7 +29864,7 @@ var zx = (() => {
|
|
|
28251
29864
|
});
|
|
28252
29865
|
const sectionMeta = section ? this._sectionMeta.get(section) : null;
|
|
28253
29866
|
if (!sectionMeta) return;
|
|
28254
|
-
const cards =
|
|
29867
|
+
const cards = this._draggableCards(sectionMeta.list);
|
|
28255
29868
|
const target = cards[move.to.index];
|
|
28256
29869
|
if (target) target.dataset.keyboardTarget = "before";
|
|
28257
29870
|
else sectionMeta.list.dataset.keyboardTarget = "append";
|
|
@@ -28261,7 +29874,7 @@ var zx = (() => {
|
|
|
28261
29874
|
for (const [id, card] of this._cardsById) {
|
|
28262
29875
|
const selected = this._isViewSelected(id);
|
|
28263
29876
|
card.dataset.selected = String(selected);
|
|
28264
|
-
|
|
29877
|
+
this._applyCardDescription(card, id);
|
|
28265
29878
|
const input = (
|
|
28266
29879
|
/** @type {HTMLInputElement|null} */
|
|
28267
29880
|
card.querySelector("[data-record-selection]")
|
|
@@ -28281,6 +29894,50 @@ var zx = (() => {
|
|
|
28281
29894
|
this._cardsById.get(id)?.querySelector(".zx-kanban-view__move-handle") ?? null
|
|
28282
29895
|
);
|
|
28283
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
|
+
}
|
|
28284
29941
|
/** @param {KanbanRecord} record @param {number} index @returns {string} */
|
|
28285
29942
|
_recordName(record, index) {
|
|
28286
29943
|
const source = this._kanbanOptions().titleField;
|
|
@@ -28288,10 +29945,16 @@ var zx = (() => {
|
|
|
28288
29945
|
if (value && typeof value === "object" && "nodeType" in value) {
|
|
28289
29946
|
return (
|
|
28290
29947
|
/** @type {Node} */
|
|
28291
|
-
value.textContent?.trim() || "record"
|
|
29948
|
+
value.textContent?.trim() || this._text("record")
|
|
28292
29949
|
);
|
|
28293
29950
|
}
|
|
28294
|
-
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 });
|
|
28295
29958
|
}
|
|
28296
29959
|
/** @param {KanbanMovePoint} point @returns {string} */
|
|
28297
29960
|
_targetName(point) {
|
|
@@ -28299,6 +29962,26 @@ var zx = (() => {
|
|
|
28299
29962
|
const lane = point.lane == null ? null : this._resolvedSwimlanes().find((candidate) => candidate.id === point.lane);
|
|
28300
29963
|
return `${column?.label ?? point.column}${lane ? `, ${lane.label}` : ""}`;
|
|
28301
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
|
+
}
|
|
28302
29985
|
/** @param {string} message @returns {void} */
|
|
28303
29986
|
_announce(message) {
|
|
28304
29987
|
const token = ++this._announcementToken;
|
|
@@ -28338,6 +30021,9 @@ var zx = (() => {
|
|
|
28338
30021
|
if (source.accept != null && typeof source.accept !== "function") {
|
|
28339
30022
|
throw new TypeError(`Kanban ${axis} ${id} accept must be a function`);
|
|
28340
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
|
+
}
|
|
28341
30027
|
seen.add(id);
|
|
28342
30028
|
const normalized = (
|
|
28343
30029
|
/** @type {Record<string, any>} */
|
|
@@ -28345,11 +30031,21 @@ var zx = (() => {
|
|
|
28345
30031
|
...source,
|
|
28346
30032
|
id,
|
|
28347
30033
|
label: String(source.label ?? axisLabel(source.id)),
|
|
30034
|
+
limit: normalizeLimit2(source.limit),
|
|
30035
|
+
from: Array.isArray(source.from) ? source.from.map(String) : null,
|
|
28348
30036
|
...Object.prototype.hasOwnProperty.call(source, "value") ? {} : { value: source.id }
|
|
28349
30037
|
}
|
|
28350
30038
|
);
|
|
28351
|
-
if (axis === "column")
|
|
28352
|
-
|
|
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
|
+
}
|
|
28353
30049
|
return normalized;
|
|
28354
30050
|
})
|
|
28355
30051
|
);
|
|
@@ -28372,37 +30068,11 @@ var zx = (() => {
|
|
|
28372
30068
|
const preferred = normalizeOrder(order).filter((id) => allowed.has(id));
|
|
28373
30069
|
return [...new Set(preferred), ...available.filter((id) => !preferred.includes(id))];
|
|
28374
30070
|
}
|
|
28375
|
-
function
|
|
28376
|
-
if (!
|
|
28377
|
-
const
|
|
28378
|
-
|
|
28379
|
-
|
|
28380
|
-
const sourceColumn = normalizeAxisId(readKanbanAccessor(source, sourceIndex, options.columnBy));
|
|
28381
|
-
const sourceLane = options.swimlaneBy == null ? null : normalizeAxisId(readKanbanAccessor(source, sourceIndex, options.swimlaneBy));
|
|
28382
|
-
const sourceGroup = records.filter((record, index) => normalizeAxisId(readKanbanAccessor(record, index, options.columnBy)) === sourceColumn && (options.swimlaneBy == null || normalizeAxisId(readKanbanAccessor(record, index, options.swimlaneBy)) === sourceLane));
|
|
28383
|
-
const from = {
|
|
28384
|
-
column: sourceColumn,
|
|
28385
|
-
lane: sourceLane,
|
|
28386
|
-
index: sourceGroup.findIndex((record) => Object.is(viewRecordId(record, options.recordId), id))
|
|
28387
|
-
};
|
|
28388
|
-
const next = records.filter((_, index) => index !== sourceIndex);
|
|
28389
|
-
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));
|
|
28390
|
-
const targetIndex = normalizeMoveIndex(options.destination.index, destinationRecords.length);
|
|
28391
|
-
let insertion = next.length;
|
|
28392
|
-
if (destinationRecords[targetIndex]) insertion = next.indexOf(destinationRecords[targetIndex]);
|
|
28393
|
-
else if (destinationRecords.length) insertion = next.indexOf(destinationRecords.at(-1)) + 1;
|
|
28394
|
-
next.splice(insertion, 0, movedRecord);
|
|
28395
|
-
return {
|
|
28396
|
-
records: next,
|
|
28397
|
-
from,
|
|
28398
|
-
to: { column: options.destination.column, lane: options.swimlaneBy == null ? null : options.destination.lane, index: targetIndex }
|
|
28399
|
-
};
|
|
28400
|
-
}
|
|
28401
|
-
function readKanbanAccessor(record, index, accessor) {
|
|
28402
|
-
return typeof accessor === "function" ? accessor(record, index) : accessor == null ? null : record?.[accessor];
|
|
28403
|
-
}
|
|
28404
|
-
function normalizeAxisId(value) {
|
|
28405
|
-
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))];
|
|
28406
30076
|
}
|
|
28407
30077
|
function axisLabel(value) {
|
|
28408
30078
|
if (value == null || String(value).trim() === "") return "Unassigned";
|
|
@@ -28414,13 +30084,42 @@ var zx = (() => {
|
|
|
28414
30084
|
if (!Number.isFinite(number2) || number2 < 0) throw new TypeError("Kanban column limit must be a non-negative number");
|
|
28415
30085
|
return Math.floor(number2);
|
|
28416
30086
|
}
|
|
28417
|
-
function
|
|
28418
|
-
if (value == null
|
|
28419
|
-
|
|
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";
|
|
28420
30111
|
}
|
|
28421
30112
|
function normalizeOrder(value) {
|
|
28422
30113
|
return Array.isArray(value) ? [...new Set(value.map(String))] : [];
|
|
28423
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
|
+
}
|
|
28424
30123
|
function orderDescriptors(descriptors, order) {
|
|
28425
30124
|
const byId = new Map(descriptors.map((descriptor) => [descriptor.id, descriptor]));
|
|
28426
30125
|
return order.map((id) => byId.get(id)).filter(Boolean);
|
|
@@ -28434,12 +30133,28 @@ var zx = (() => {
|
|
|
28434
30133
|
next.splice(target + (position2 === "after" ? 1 : 0), 0, String(id));
|
|
28435
30134
|
return next;
|
|
28436
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
|
+
}
|
|
28437
30148
|
function sameMovePoint(left, right) {
|
|
28438
30149
|
return left.column === right.column && left.lane === right.lane && left.index === right.index;
|
|
28439
30150
|
}
|
|
28440
30151
|
function sameArray3(left, right) {
|
|
28441
30152
|
return left.length === right.length && left.every((value, index) => Object.is(value, right[index]));
|
|
28442
30153
|
}
|
|
30154
|
+
function hasId(haystack, needle) {
|
|
30155
|
+
for (const candidate of haystack) if (Object.is(candidate, needle)) return true;
|
|
30156
|
+
return false;
|
|
30157
|
+
}
|
|
28443
30158
|
function elementTarget(event) {
|
|
28444
30159
|
const target = event.target;
|
|
28445
30160
|
if (!target) return null;
|