@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.esm.js
CHANGED
|
@@ -798,21 +798,26 @@ var Component = class extends EventTarget {
|
|
|
798
798
|
* Dispatches a component event and a bubbling, composed `zx-*` DOM event.
|
|
799
799
|
* @param {string} type Lowercase event name.
|
|
800
800
|
* @param {Record<string, unknown>} [detail={}] Single event detail object.
|
|
801
|
+
* @param {{honorDomCancellation?:boolean}} [options={}] Mirrored DOM-event behavior. Components
|
|
802
|
+
* with a cancelable mutation boundary may opt into treating `preventDefault()` on the bubbling
|
|
803
|
+
* `zx-*` event exactly like cancellation of the component event.
|
|
801
804
|
* @returns {Event & {detail: Record<string, unknown>}}
|
|
802
805
|
* @fires Component#event
|
|
803
806
|
*/
|
|
804
|
-
emit(type, detail = {}) {
|
|
807
|
+
emit(type, detail = {}, options = {}) {
|
|
805
808
|
if (detail === null || typeof detail !== "object" || Array.isArray(detail)) {
|
|
806
809
|
throw new TypeError("Event detail must be an object");
|
|
807
810
|
}
|
|
808
811
|
const componentEvent = createCustomEvent(type, detail, { cancelable: true });
|
|
809
812
|
this.dispatchEvent(componentEvent);
|
|
810
813
|
if (this.el) {
|
|
811
|
-
|
|
814
|
+
const domEvent = createCustomEvent(`zx-${type}`, detail, {
|
|
812
815
|
bubbles: true,
|
|
813
816
|
composed: true,
|
|
814
817
|
cancelable: true
|
|
815
|
-
})
|
|
818
|
+
});
|
|
819
|
+
this.el.dispatchEvent(domEvent);
|
|
820
|
+
if (options.honorDomCancellation && domEvent.defaultPrevented) componentEvent.preventDefault();
|
|
816
821
|
}
|
|
817
822
|
return componentEvent;
|
|
818
823
|
}
|
|
@@ -10286,6 +10291,15 @@ function restore2(element, state) {
|
|
|
10286
10291
|
element.replaceChildren(...state.children);
|
|
10287
10292
|
}
|
|
10288
10293
|
|
|
10294
|
+
// src/core/overlay-host.js
|
|
10295
|
+
var SCOPE_SELECTOR = ".zx-scope, [data-zx-theme], [data-zx-density], [data-zx-preset]";
|
|
10296
|
+
function overlayHost(source = null) {
|
|
10297
|
+
const context = source == null ? document.activeElement instanceof Element ? document.activeElement : null : resolveElement(source);
|
|
10298
|
+
const scope2 = context?.closest?.(SCOPE_SELECTOR) ?? null;
|
|
10299
|
+
if (!scope2 || scope2 === document.documentElement || scope2 === document.body) return document.body;
|
|
10300
|
+
return scope2;
|
|
10301
|
+
}
|
|
10302
|
+
|
|
10289
10303
|
// src/components/modal/modal.js
|
|
10290
10304
|
var Modal = class extends Component {
|
|
10291
10305
|
static cssName = "modal";
|
|
@@ -10295,13 +10309,17 @@ var Modal = class extends Component {
|
|
|
10295
10309
|
width: "auto",
|
|
10296
10310
|
closable: true,
|
|
10297
10311
|
lightDismiss: false,
|
|
10298
|
-
destroyOnClose: false
|
|
10312
|
+
destroyOnClose: false,
|
|
10313
|
+
scope: null
|
|
10299
10314
|
};
|
|
10300
|
-
|
|
10301
|
-
#
|
|
10315
|
+
/** @type {Map<number, {result: unknown}>} */
|
|
10316
|
+
#pendingResults = /* @__PURE__ */ new Map();
|
|
10302
10317
|
#destroying = false;
|
|
10318
|
+
#presentation = 0;
|
|
10319
|
+
/** @type {number[]} */
|
|
10320
|
+
#nativeClosingPresentations = [];
|
|
10303
10321
|
/**
|
|
10304
|
-
* Creates a modal
|
|
10322
|
+
* Creates a modal in the configured theme scope, or at document level when none is present.
|
|
10305
10323
|
* @param {Element|string|null} [_target=null] Ignored; Modal always owns its root dialog.
|
|
10306
10324
|
* @param {ModalOptions} [options={}] Modal options.
|
|
10307
10325
|
*/
|
|
@@ -10314,14 +10332,7 @@ var Modal = class extends Component {
|
|
|
10314
10332
|
if (!this.options.closable || cancelEvent.defaultPrevented) event.preventDefault();
|
|
10315
10333
|
});
|
|
10316
10334
|
this.listen(this.el, "close", () => {
|
|
10317
|
-
|
|
10318
|
-
this.el.dataset.state = "closed";
|
|
10319
|
-
delete this.el.dataset.zxOverlayOrder;
|
|
10320
|
-
const result = this.#hasPendingResult ? this.#pendingResult : this.el.returnValue || void 0;
|
|
10321
|
-
this.#pendingResult = void 0;
|
|
10322
|
-
this.#hasPendingResult = false;
|
|
10323
|
-
this.emit("close", { result });
|
|
10324
|
-
if (this.options.destroyOnClose && !this.#destroying) this.destroy();
|
|
10335
|
+
this._settleClose(this.#nativeClosingPresentations.shift());
|
|
10325
10336
|
});
|
|
10326
10337
|
this.listen(this.el, "click", (event) => {
|
|
10327
10338
|
if (!this.options.lightDismiss || event.target !== this.el || !isBackdropClick(this.el, event)) return;
|
|
@@ -10337,7 +10348,7 @@ var Modal = class extends Component {
|
|
|
10337
10348
|
* @returns {Element}
|
|
10338
10349
|
*/
|
|
10339
10350
|
mountTarget() {
|
|
10340
|
-
return
|
|
10351
|
+
return overlayHost(this.options.scope);
|
|
10341
10352
|
}
|
|
10342
10353
|
/**
|
|
10343
10354
|
* Puts the dialog on screen. Overridden by subclasses that open non-modally.
|
|
@@ -10346,17 +10357,78 @@ var Modal = class extends Component {
|
|
|
10346
10357
|
_show() {
|
|
10347
10358
|
this.el.showModal();
|
|
10348
10359
|
}
|
|
10360
|
+
/**
|
|
10361
|
+
* Removes the dialog from its current presentation layer.
|
|
10362
|
+
* Subclasses may replace native dialog presentation while preserving Modal lifecycle events.
|
|
10363
|
+
* @param {string} [result=''] Native string return value.
|
|
10364
|
+
* @param {number} [presentation=this.#presentation] Presentation generation being hidden.
|
|
10365
|
+
* @returns {void}
|
|
10366
|
+
*/
|
|
10367
|
+
_hide(result = "", presentation = this.#presentation) {
|
|
10368
|
+
this.#nativeClosingPresentations.push(presentation);
|
|
10369
|
+
try {
|
|
10370
|
+
this.el.close(result);
|
|
10371
|
+
} catch (error2) {
|
|
10372
|
+
this.#nativeClosingPresentations.pop();
|
|
10373
|
+
throw error2;
|
|
10374
|
+
}
|
|
10375
|
+
}
|
|
10376
|
+
/**
|
|
10377
|
+
* Shows a new presentation and gives it a generation of its own. Close events are queued by the
|
|
10378
|
+
* platform, so the generation lets an event from an earlier presentation expire harmlessly if
|
|
10379
|
+
* the same overlay has already reopened.
|
|
10380
|
+
* @returns {void}
|
|
10381
|
+
*/
|
|
10382
|
+
_present() {
|
|
10383
|
+
this._show();
|
|
10384
|
+
this.#presentation += 1;
|
|
10385
|
+
}
|
|
10386
|
+
/**
|
|
10387
|
+
* Hides the current presentation and records which generation its queued close event belongs to.
|
|
10388
|
+
* @param {string} [result=''] Native string return value.
|
|
10389
|
+
* @returns {void}
|
|
10390
|
+
*/
|
|
10391
|
+
_dismiss(result = "") {
|
|
10392
|
+
this._hide(result, this.#presentation);
|
|
10393
|
+
}
|
|
10394
|
+
/**
|
|
10395
|
+
* Reports whether the current presentation layer is visible.
|
|
10396
|
+
* @returns {boolean}
|
|
10397
|
+
*/
|
|
10398
|
+
_isShown() {
|
|
10399
|
+
return this.el.open;
|
|
10400
|
+
}
|
|
10349
10401
|
/**
|
|
10350
10402
|
* Whether an incoming `close` event is a real dismissal.
|
|
10351
10403
|
*
|
|
10352
|
-
*
|
|
10353
|
-
*
|
|
10354
|
-
*
|
|
10355
|
-
*
|
|
10356
|
-
* `destroyOnClose`. Returning false from an override drops it at the source instead.
|
|
10404
|
+
* `close` is dispatched in a queued task, not synchronously. By the time it arrives the overlay
|
|
10405
|
+
* may have reopened or crossed presentation layers, so only the event for the current hidden
|
|
10406
|
+
* generation may settle state, emit lifecycle events, or trigger `destroyOnClose`.
|
|
10407
|
+
* @param {number|undefined} closingPresentation Presentation that produced the event, if known.
|
|
10357
10408
|
* @returns {boolean}
|
|
10358
10409
|
*/
|
|
10359
|
-
_isRealClose() {
|
|
10410
|
+
_isRealClose(closingPresentation) {
|
|
10411
|
+
if (closingPresentation !== void 0 && closingPresentation !== this.#presentation) return false;
|
|
10412
|
+
return !this._isShown();
|
|
10413
|
+
}
|
|
10414
|
+
/**
|
|
10415
|
+
* Settles one presentation's close without allowing a queued event to invalidate a newer one.
|
|
10416
|
+
* Popover-based subclasses call this directly; native dialog events arrive through the listener.
|
|
10417
|
+
* @param {number|undefined} closingPresentation Presentation that was hidden, if known.
|
|
10418
|
+
* @returns {boolean} Whether this event closed the current presentation.
|
|
10419
|
+
*/
|
|
10420
|
+
_settleClose(closingPresentation) {
|
|
10421
|
+
if (!this._isRealClose(closingPresentation)) {
|
|
10422
|
+
if (closingPresentation !== void 0) this.#pendingResults.delete(closingPresentation);
|
|
10423
|
+
return false;
|
|
10424
|
+
}
|
|
10425
|
+
this.el.dataset.state = "closed";
|
|
10426
|
+
delete this.el.dataset.zxOverlayOrder;
|
|
10427
|
+
const pending = closingPresentation === void 0 ? void 0 : this.#pendingResults.get(closingPresentation);
|
|
10428
|
+
if (closingPresentation !== void 0) this.#pendingResults.delete(closingPresentation);
|
|
10429
|
+
const result = pending ? pending.result : this.el.returnValue || void 0;
|
|
10430
|
+
this.emit("close", { result });
|
|
10431
|
+
if (this.options.destroyOnClose && !this.#destroying) this.destroy();
|
|
10360
10432
|
return true;
|
|
10361
10433
|
}
|
|
10362
10434
|
/**
|
|
@@ -10381,10 +10453,12 @@ var Modal = class extends Component {
|
|
|
10381
10453
|
*/
|
|
10382
10454
|
open() {
|
|
10383
10455
|
if (this.isOpen()) return this;
|
|
10456
|
+
const host = this.mountTarget();
|
|
10457
|
+
if (this.el.parentElement !== host) host.append(this.el);
|
|
10384
10458
|
this.el.dataset.zxOverlayOrder = String(nextOverlayOrder());
|
|
10385
10459
|
this.el.returnValue = "";
|
|
10386
10460
|
try {
|
|
10387
|
-
this.
|
|
10461
|
+
this._present();
|
|
10388
10462
|
} catch (error2) {
|
|
10389
10463
|
delete this.el.dataset.zxOverlayOrder;
|
|
10390
10464
|
throw error2;
|
|
@@ -10400,9 +10474,14 @@ var Modal = class extends Component {
|
|
|
10400
10474
|
*/
|
|
10401
10475
|
close(result) {
|
|
10402
10476
|
if (!this.isOpen()) return this;
|
|
10403
|
-
|
|
10404
|
-
this.#
|
|
10405
|
-
|
|
10477
|
+
const presentation = this.#presentation;
|
|
10478
|
+
this.#pendingResults.set(presentation, { result });
|
|
10479
|
+
try {
|
|
10480
|
+
this._dismiss(typeof result === "string" ? result : "");
|
|
10481
|
+
} catch (error2) {
|
|
10482
|
+
this.#pendingResults.delete(presentation);
|
|
10483
|
+
throw error2;
|
|
10484
|
+
}
|
|
10406
10485
|
return this;
|
|
10407
10486
|
}
|
|
10408
10487
|
/**
|
|
@@ -10419,7 +10498,7 @@ var Modal = class extends Component {
|
|
|
10419
10498
|
* @returns {boolean}
|
|
10420
10499
|
*/
|
|
10421
10500
|
isOpen() {
|
|
10422
|
-
return this.
|
|
10501
|
+
return this._isShown();
|
|
10423
10502
|
}
|
|
10424
10503
|
/**
|
|
10425
10504
|
* Closes and removes the owned dialog. Safe to call repeatedly.
|
|
@@ -10428,7 +10507,7 @@ var Modal = class extends Component {
|
|
|
10428
10507
|
destroy() {
|
|
10429
10508
|
if (this.#destroying) return;
|
|
10430
10509
|
this.#destroying = true;
|
|
10431
|
-
if (this.isOpen()) this.
|
|
10510
|
+
if (this.isOpen()) this._dismiss();
|
|
10432
10511
|
super.destroy();
|
|
10433
10512
|
}
|
|
10434
10513
|
/** @param {string|number} width @returns {void} */
|
|
@@ -10493,7 +10572,7 @@ var Dialog = class _Dialog extends Modal {
|
|
|
10493
10572
|
/** @type {Element|null} */
|
|
10494
10573
|
#opener = null;
|
|
10495
10574
|
/**
|
|
10496
|
-
* Creates a structured dialog
|
|
10575
|
+
* Creates a structured dialog in the configured theme scope, or at document level when unscoped.
|
|
10497
10576
|
* @param {Element|string|null} [_target=null] Ignored; Dialog always owns its root.
|
|
10498
10577
|
* @param {DialogOptions} [options={}] Dialog options.
|
|
10499
10578
|
*/
|
|
@@ -15390,7 +15469,8 @@ var Dropdown = class extends Component {
|
|
|
15390
15469
|
offset: 4,
|
|
15391
15470
|
matchWidth: false,
|
|
15392
15471
|
openOn: "click",
|
|
15393
|
-
closeOnSelect: false
|
|
15472
|
+
closeOnSelect: false,
|
|
15473
|
+
scope: null
|
|
15394
15474
|
};
|
|
15395
15475
|
/** @type {Element} */
|
|
15396
15476
|
#anchor;
|
|
@@ -15409,6 +15489,7 @@ var Dropdown = class extends Component {
|
|
|
15409
15489
|
if (!resolved) throw new TypeError("Dropdown anchor could not be resolved");
|
|
15410
15490
|
super(null, options);
|
|
15411
15491
|
this.#anchor = resolved;
|
|
15492
|
+
overlayHost(this.options.scope ?? resolved).append(this.el);
|
|
15412
15493
|
this.#rememberAnchorAttribute("aria-expanded");
|
|
15413
15494
|
this.#rememberAnchorAttribute("aria-controls");
|
|
15414
15495
|
this.#anchor.setAttribute("aria-expanded", "false");
|
|
@@ -15431,7 +15512,6 @@ var Dropdown = class extends Component {
|
|
|
15431
15512
|
popover: "manual"
|
|
15432
15513
|
});
|
|
15433
15514
|
panel.dataset.state = "closed";
|
|
15434
|
-
document.body.append(panel);
|
|
15435
15515
|
return panel;
|
|
15436
15516
|
}
|
|
15437
15517
|
/**
|
|
@@ -15441,6 +15521,8 @@ var Dropdown = class extends Component {
|
|
|
15441
15521
|
*/
|
|
15442
15522
|
open() {
|
|
15443
15523
|
if (this.isOpen() || this.#destroyed) return this;
|
|
15524
|
+
const host = overlayHost(this.options.scope ?? this.#anchor);
|
|
15525
|
+
if (this.el.parentElement !== host) host.append(this.el);
|
|
15444
15526
|
this.#position = position(this.#anchor, this.el, {
|
|
15445
15527
|
placement: this.options.placement,
|
|
15446
15528
|
offset: Number(this.options.offset),
|
|
@@ -20100,7 +20182,8 @@ var Launcher = class extends Component {
|
|
|
20100
20182
|
emptyText: "No results",
|
|
20101
20183
|
loadingText: "Searching\u2026",
|
|
20102
20184
|
shortcut: "mod+k",
|
|
20103
|
-
hints: { move: "Move", open: "Open", close: "Close" }
|
|
20185
|
+
hints: { move: "Move", open: "Open", close: "Close" },
|
|
20186
|
+
scope: null
|
|
20104
20187
|
};
|
|
20105
20188
|
/**
|
|
20106
20189
|
* Creates an owned launcher dialog or enhances an existing `<dialog>` target.
|
|
@@ -20191,7 +20274,7 @@ var Launcher = class extends Component {
|
|
|
20191
20274
|
)
|
|
20192
20275
|
);
|
|
20193
20276
|
dialog.dataset.state = "closed";
|
|
20194
|
-
if (this._createdRoot)
|
|
20277
|
+
if (this._createdRoot) overlayHost(this.options.scope).append(dialog);
|
|
20195
20278
|
this.listen(input, "input", () => {
|
|
20196
20279
|
this._query = /** @type {HTMLInputElement} */
|
|
20197
20280
|
input.value;
|
|
@@ -20242,6 +20325,10 @@ var Launcher = class extends Component {
|
|
|
20242
20325
|
/** Opens the launcher and focuses/selects its query. @returns {this} @fires Launcher#open */
|
|
20243
20326
|
open() {
|
|
20244
20327
|
if (this.isOpen() || this._destroyed) return this;
|
|
20328
|
+
if (this._createdRoot) {
|
|
20329
|
+
const host = overlayHost(this.options.scope);
|
|
20330
|
+
if (this.el.parentElement !== host) host.append(this.el);
|
|
20331
|
+
}
|
|
20245
20332
|
this.el.showModal();
|
|
20246
20333
|
this.el.dataset.state = "open";
|
|
20247
20334
|
this.refs.input.setAttribute("aria-expanded", "true");
|
|
@@ -21120,6 +21207,7 @@ var Tooltip = class extends Component {
|
|
|
21120
21207
|
if (!resolved) throw new TypeError("Tooltip anchor could not be resolved");
|
|
21121
21208
|
super(null, options);
|
|
21122
21209
|
this.#anchor = resolved;
|
|
21210
|
+
overlayHost(resolved).append(this.el);
|
|
21123
21211
|
this.#content = this.options.content;
|
|
21124
21212
|
this.#disabled = Boolean(this.options.disabled);
|
|
21125
21213
|
this.#describedBy = resolved.getAttribute("aria-describedby");
|
|
@@ -21180,7 +21268,6 @@ var Tooltip = class extends Component {
|
|
|
21180
21268
|
popover: "manual"
|
|
21181
21269
|
});
|
|
21182
21270
|
panel.dataset.state = "closed";
|
|
21183
|
-
document.body.append(panel);
|
|
21184
21271
|
return panel;
|
|
21185
21272
|
}
|
|
21186
21273
|
/**
|
|
@@ -21192,6 +21279,8 @@ var Tooltip = class extends Component {
|
|
|
21192
21279
|
show() {
|
|
21193
21280
|
if (this.#destroyed || this.#disabled || this.isOpen()) return this;
|
|
21194
21281
|
this.#cancelTimers();
|
|
21282
|
+
const host = overlayHost(this.#anchor);
|
|
21283
|
+
if (this.el.parentElement !== host) host.append(this.el);
|
|
21195
21284
|
const node = resolveContent(this.#content);
|
|
21196
21285
|
if (node === null) return this;
|
|
21197
21286
|
this.el.replaceChildren(node);
|
|
@@ -22638,8 +22727,8 @@ var Message = class _Message extends Component {
|
|
|
22638
22727
|
closable: true,
|
|
22639
22728
|
maxVisible: 5
|
|
22640
22729
|
};
|
|
22641
|
-
/** @type {Message
|
|
22642
|
-
static #floating =
|
|
22730
|
+
/** @type {WeakMap<Element, Message>} */
|
|
22731
|
+
static #floating = /* @__PURE__ */ new WeakMap();
|
|
22643
22732
|
/** @returns {HTMLElement} */
|
|
22644
22733
|
render() {
|
|
22645
22734
|
const created = !this.el;
|
|
@@ -22718,15 +22807,16 @@ var Message = class _Message extends Component {
|
|
|
22718
22807
|
* @returns {MessageHandle}
|
|
22719
22808
|
*/
|
|
22720
22809
|
static show(msg, options = {}) {
|
|
22721
|
-
return this.#floatingRegion().show(msg, options);
|
|
22810
|
+
return this.#floatingRegion(options.scope).show(msg, options);
|
|
22722
22811
|
}
|
|
22723
22812
|
/**
|
|
22724
22813
|
* Shows a floating progress status.
|
|
22725
22814
|
* @param {string} text Initial status text.
|
|
22815
|
+
* @param {{scope?: Element|string|null}} [options={}] Floating-region scope.
|
|
22726
22816
|
* @returns {ProgressHandle}
|
|
22727
22817
|
*/
|
|
22728
|
-
static progress(text4) {
|
|
22729
|
-
return this.#floatingRegion()._progress(text4);
|
|
22818
|
+
static progress(text4, options = {}) {
|
|
22819
|
+
return this.#floatingRegion(options.scope)._progress(text4);
|
|
22730
22820
|
}
|
|
22731
22821
|
/** @returns {void} */
|
|
22732
22822
|
destroy() {
|
|
@@ -22904,19 +22994,22 @@ var Message = class _Message extends Component {
|
|
|
22904
22994
|
this._entries.delete(entry);
|
|
22905
22995
|
this._queue.remove(entry);
|
|
22906
22996
|
}
|
|
22907
|
-
/** @returns {Message} */
|
|
22908
|
-
static #floatingRegion() {
|
|
22909
|
-
|
|
22997
|
+
/** @param {Element|string|null|undefined} scope @returns {Message} */
|
|
22998
|
+
static #floatingRegion(scope2) {
|
|
22999
|
+
const host = overlayHost(scope2 ?? null);
|
|
23000
|
+
const existing = this.#floating.get(host);
|
|
23001
|
+
if (existing) return existing;
|
|
22910
23002
|
const region = h("div", {
|
|
22911
23003
|
class: "zx-message-region",
|
|
22912
23004
|
popover: "manual",
|
|
22913
23005
|
role: "status",
|
|
22914
23006
|
ariaLive: "polite"
|
|
22915
23007
|
});
|
|
22916
|
-
|
|
22917
|
-
|
|
23008
|
+
host.append(region);
|
|
23009
|
+
const floating = new _Message(region);
|
|
23010
|
+
this.#floating.set(host, floating);
|
|
22918
23011
|
if (typeof region.showPopover === "function") region.showPopover();
|
|
22919
|
-
return
|
|
23012
|
+
return floating;
|
|
22920
23013
|
}
|
|
22921
23014
|
};
|
|
22922
23015
|
function normalizeKind3(value) {
|
|
@@ -23004,7 +23097,6 @@ var Sheet = class extends Dialog {
|
|
|
23004
23097
|
constructor(_target = null, options = {}) {
|
|
23005
23098
|
super(null, options);
|
|
23006
23099
|
this._dock = null;
|
|
23007
|
-
this._handoff = false;
|
|
23008
23100
|
this.setSide(this.options.side);
|
|
23009
23101
|
this.el.dataset.backdrop = BACKDROPS.has(this.options.backdrop) ? this.options.backdrop : "dim";
|
|
23010
23102
|
this.el.dataset.modality = modality(this.options.modal);
|
|
@@ -23179,7 +23271,7 @@ var Sheet = class extends Dialog {
|
|
|
23179
23271
|
* @returns {Element}
|
|
23180
23272
|
*/
|
|
23181
23273
|
mountTarget() {
|
|
23182
|
-
return this._dock?.el ??
|
|
23274
|
+
return this._dock?.el ?? super.mountTarget();
|
|
23183
23275
|
}
|
|
23184
23276
|
/**
|
|
23185
23277
|
* Reports whether a dock currently owns this sheet's positioning.
|
|
@@ -23205,35 +23297,23 @@ var Sheet = class extends Dialog {
|
|
|
23205
23297
|
/**
|
|
23206
23298
|
* Moves the dialog to its current mount and, when it was open, reopens it there.
|
|
23207
23299
|
*
|
|
23208
|
-
* The close is unavoidable — a dialog cannot cross between
|
|
23209
|
-
*
|
|
23210
|
-
* `open()` so no lifecycle event is emitted for what is only a change of address.
|
|
23300
|
+
* The close is unavoidable — a dialog cannot cross between presentation layers while open —
|
|
23301
|
+
* but it is invisible: Modal's presentation generation drops the stale event, and reopening
|
|
23302
|
+
* bypasses `open()` so no lifecycle event is emitted for what is only a change of address.
|
|
23211
23303
|
* @returns {void}
|
|
23212
23304
|
*/
|
|
23213
23305
|
_rehost() {
|
|
23214
23306
|
const wasOpen = this.isOpen();
|
|
23215
|
-
if (wasOpen)
|
|
23216
|
-
this._handoff = true;
|
|
23217
|
-
this.el.close();
|
|
23218
|
-
}
|
|
23307
|
+
if (wasOpen) this._dismiss();
|
|
23219
23308
|
this.mountTarget().append(this.el);
|
|
23220
23309
|
if (!wasOpen) return;
|
|
23221
23310
|
this.el.returnValue = "";
|
|
23222
|
-
this.
|
|
23311
|
+
this._present();
|
|
23223
23312
|
this.el.dataset.state = "open";
|
|
23224
23313
|
if (this._dock) delete /** @type {HTMLElement} */
|
|
23225
23314
|
this.el.dataset.zxOverlayOrder;
|
|
23226
23315
|
else this.el.dataset.zxOverlayOrder = String(nextOverlayOrder3());
|
|
23227
23316
|
}
|
|
23228
|
-
/**
|
|
23229
|
-
* Drops the stale `close` a re-hosting handoff leaves behind.
|
|
23230
|
-
* @returns {boolean}
|
|
23231
|
-
*/
|
|
23232
|
-
_isRealClose() {
|
|
23233
|
-
if (!this._handoff) return true;
|
|
23234
|
-
this._handoff = false;
|
|
23235
|
-
return false;
|
|
23236
|
-
}
|
|
23237
23317
|
/**
|
|
23238
23318
|
* Opens the sheet, trapping focus first where the modality asks for it.
|
|
23239
23319
|
* @returns {this}
|
|
@@ -23314,10 +23394,38 @@ var Sheet = class extends Dialog {
|
|
|
23314
23394
|
*/
|
|
23315
23395
|
_show() {
|
|
23316
23396
|
if (this.options.modal === true && !this._dock) {
|
|
23397
|
+
this.el.removeAttribute("popover");
|
|
23398
|
+
this.el.removeAttribute("aria-modal");
|
|
23317
23399
|
super._show();
|
|
23318
23400
|
return;
|
|
23319
23401
|
}
|
|
23320
|
-
this.el.
|
|
23402
|
+
this.el.setAttribute("aria-modal", "false");
|
|
23403
|
+
if (this._dock) {
|
|
23404
|
+
this.el.removeAttribute("popover");
|
|
23405
|
+
this.el.show();
|
|
23406
|
+
return;
|
|
23407
|
+
}
|
|
23408
|
+
this.el.setAttribute("popover", "manual");
|
|
23409
|
+
this.el.showPopover();
|
|
23410
|
+
}
|
|
23411
|
+
/**
|
|
23412
|
+
* Hides a native dialog or a non-modal Sheet promoted through the popover top layer.
|
|
23413
|
+
* @param {string} [result=''] Native string return value.
|
|
23414
|
+
* @param {number} [presentation] Presentation generation being hidden.
|
|
23415
|
+
* @returns {void}
|
|
23416
|
+
*/
|
|
23417
|
+
_hide(result = "", presentation) {
|
|
23418
|
+
if (!this.el.matches(":popover-open")) {
|
|
23419
|
+
super._hide(result, presentation);
|
|
23420
|
+
return;
|
|
23421
|
+
}
|
|
23422
|
+
this.el.returnValue = result;
|
|
23423
|
+
this.el.hidePopover();
|
|
23424
|
+
queueMicrotask(() => this._settleClose(presentation));
|
|
23425
|
+
}
|
|
23426
|
+
/** @returns {boolean} */
|
|
23427
|
+
_isShown() {
|
|
23428
|
+
return this.el.matches(":popover-open") || super._isShown();
|
|
23321
23429
|
}
|
|
23322
23430
|
/**
|
|
23323
23431
|
* Releases the focus trap and the owned dialog.
|
|
@@ -23642,7 +23750,11 @@ var ContextMenu = class extends Component {
|
|
|
23642
23750
|
super(null, options);
|
|
23643
23751
|
this.#target = resolved;
|
|
23644
23752
|
document.body.append(this.el);
|
|
23645
|
-
this.#dropdown = new Dropdown(this.el, {
|
|
23753
|
+
this.#dropdown = new Dropdown(this.el, {
|
|
23754
|
+
openOn: "manual",
|
|
23755
|
+
placement: "bottom-start",
|
|
23756
|
+
scope: resolved
|
|
23757
|
+
});
|
|
23646
23758
|
this.#panel = this.#dropdown.getPanel();
|
|
23647
23759
|
this.#panel.classList.add("zx-context-menu");
|
|
23648
23760
|
this.#panel.setAttribute("role", "menu");
|
|
@@ -23902,6 +24014,29 @@ function calendarRange(view, anchor, options = {}) {
|
|
|
23902
24014
|
}
|
|
23903
24015
|
return { start, end };
|
|
23904
24016
|
}
|
|
24017
|
+
function calendarPageDate(view, anchor, direction, options = {}) {
|
|
24018
|
+
const date = validDate5(anchor, "Calendar date");
|
|
24019
|
+
const step = integer(direction, "direction");
|
|
24020
|
+
if (view === "day") date.setDate(date.getDate() + step);
|
|
24021
|
+
else if (view === "agenda") {
|
|
24022
|
+
date.setDate(date.getDate() + step * positiveInteger(options.agendaDays ?? 14, "agendaDays"));
|
|
24023
|
+
} else if (view === "week") date.setDate(date.getDate() + step * 7);
|
|
24024
|
+
else if (view === "month") return addCalendarMonths(date, step);
|
|
24025
|
+
else if (view === "year") return addCalendarMonths(date, step * 12);
|
|
24026
|
+
else throw new RangeError(`Unknown calendar view: ${view}`);
|
|
24027
|
+
return date;
|
|
24028
|
+
}
|
|
24029
|
+
function addCalendarMonths(value, months) {
|
|
24030
|
+
const date = validDate5(value, "Calendar date");
|
|
24031
|
+
const day = date.getDate();
|
|
24032
|
+
date.setDate(1);
|
|
24033
|
+
date.setMonth(date.getMonth() + integer(months, "month amount"));
|
|
24034
|
+
date.setDate(Math.min(day, daysInCalendarMonth(date.getFullYear(), date.getMonth())));
|
|
24035
|
+
return date;
|
|
24036
|
+
}
|
|
24037
|
+
function daysInCalendarMonth(year, month) {
|
|
24038
|
+
return new Date(year, month + 1, 0).getDate();
|
|
24039
|
+
}
|
|
23905
24040
|
function normalizeCalendarEvents(records, options = {}) {
|
|
23906
24041
|
if (!Array.isArray(records)) throw new TypeError("Calendar events must be an array");
|
|
23907
24042
|
const readers = {
|
|
@@ -24345,7 +24480,7 @@ var Calendar = class extends Component {
|
|
|
24345
24480
|
durationEditable: Object.prototype.hasOwnProperty.call(changes, "durationEditable") ? changes.durationEditable : current.durationEditable,
|
|
24346
24481
|
data: changes.data ?? current.data
|
|
24347
24482
|
};
|
|
24348
|
-
const [next] = normalizeCalendarEvents([source]);
|
|
24483
|
+
const [next] = normalizeCalendarEvents([source], { dateUnit: this.options.dateUnit });
|
|
24349
24484
|
next.data = source.data;
|
|
24350
24485
|
this._events = this._events.map((event, eventIndex) => eventIndex === index ? next : event);
|
|
24351
24486
|
this._mutationVersions.delete(String(id));
|
|
@@ -24496,7 +24631,11 @@ var Calendar = class extends Component {
|
|
|
24496
24631
|
}).sort(compareEvents);
|
|
24497
24632
|
const row2 = h(
|
|
24498
24633
|
"section",
|
|
24499
|
-
{
|
|
24634
|
+
{
|
|
24635
|
+
class: "zx-calendar__agenda-day",
|
|
24636
|
+
role: "row",
|
|
24637
|
+
dataset: { date: calendarDayKey(day) }
|
|
24638
|
+
},
|
|
24500
24639
|
h("button", {
|
|
24501
24640
|
type: "button",
|
|
24502
24641
|
class: "zx-calendar__agenda-heading",
|
|
@@ -24506,11 +24645,13 @@ var Calendar = class extends Component {
|
|
|
24506
24645
|
disabled: this.options.disabled
|
|
24507
24646
|
}, this._format(day, { weekday: "long", day: "numeric", month: "long" }))
|
|
24508
24647
|
);
|
|
24648
|
+
const entries = h("div", { class: "zx-calendar__agenda-events", role: "gridcell" });
|
|
24509
24649
|
if (!events.length) {
|
|
24510
|
-
|
|
24650
|
+
entries.append(h("p", { class: "zx-calendar__agenda-empty" }, this.msg("noEvents")));
|
|
24511
24651
|
} else {
|
|
24512
|
-
for (const event of events)
|
|
24652
|
+
for (const event of events) entries.append(this._eventElement(event, { variant: "agenda", day }));
|
|
24513
24653
|
}
|
|
24654
|
+
row2.append(entries);
|
|
24514
24655
|
grid2.append(row2);
|
|
24515
24656
|
}
|
|
24516
24657
|
return grid2;
|
|
@@ -24780,6 +24921,7 @@ var Calendar = class extends Component {
|
|
|
24780
24921
|
dataset: {
|
|
24781
24922
|
eventId: key,
|
|
24782
24923
|
variant: context.variant,
|
|
24924
|
+
allDay: event.allDay ? "true" : null,
|
|
24783
24925
|
draggable: this._canMove(event) ? "true" : null
|
|
24784
24926
|
},
|
|
24785
24927
|
style: event.color ? { "--zx-calendar-event-color": event.color } : null
|
|
@@ -25046,7 +25188,9 @@ var Calendar = class extends Component {
|
|
|
25046
25188
|
const item = this._eventById(control.dataset.eventId);
|
|
25047
25189
|
const action = control.dataset.eventAction === "resize" ? "resize" : "move";
|
|
25048
25190
|
if (!item || (action === "move" ? !this._canMove(item) : !this._canResize(item))) return;
|
|
25049
|
-
|
|
25191
|
+
event.preventDefault();
|
|
25192
|
+
this._capturePointer(event.pointerId);
|
|
25193
|
+
const offset = action === "move" ? this._dragOffsetForEvent(item, event.clientX, event.clientY) : null;
|
|
25050
25194
|
this._pointer = {
|
|
25051
25195
|
type: "event",
|
|
25052
25196
|
pointerId: event.pointerId,
|
|
@@ -25056,6 +25200,8 @@ var Calendar = class extends Component {
|
|
|
25056
25200
|
event: cloneCalendarEvent(item),
|
|
25057
25201
|
action,
|
|
25058
25202
|
control,
|
|
25203
|
+
timeOffsetMinutes: offset?.minutes ?? 0,
|
|
25204
|
+
dayOffset: offset?.days ?? 0,
|
|
25059
25205
|
moved: false
|
|
25060
25206
|
};
|
|
25061
25207
|
return;
|
|
@@ -25064,7 +25210,8 @@ var Calendar = class extends Component {
|
|
|
25064
25210
|
if (!slot || !this.options.selectable) return;
|
|
25065
25211
|
const date = this._dateFromTarget(slot);
|
|
25066
25212
|
if (!date) return;
|
|
25067
|
-
|
|
25213
|
+
event.preventDefault();
|
|
25214
|
+
this._capturePointer(event.pointerId);
|
|
25068
25215
|
this._pointer = {
|
|
25069
25216
|
type: "selection",
|
|
25070
25217
|
pointerId: event.pointerId,
|
|
@@ -25097,7 +25244,13 @@ var Calendar = class extends Component {
|
|
|
25097
25244
|
const wrapper = pointer.control.closest(".zx-calendar__event");
|
|
25098
25245
|
if (wrapper) {
|
|
25099
25246
|
wrapper.dataset.state = "dragging";
|
|
25100
|
-
|
|
25247
|
+
if (pointer.action === "resize") {
|
|
25248
|
+
wrapper.style.transform = "";
|
|
25249
|
+
const height = this._timedPreviewHeight(proposed);
|
|
25250
|
+
if (height) wrapper.style.blockSize = height;
|
|
25251
|
+
} else {
|
|
25252
|
+
wrapper.style.transform = `translate(${event.clientX - pointer.startX}px, ${event.clientY - pointer.startY}px)`;
|
|
25253
|
+
}
|
|
25101
25254
|
wrapper.dataset.preview = this._eventTime(proposed, proposed.start);
|
|
25102
25255
|
}
|
|
25103
25256
|
this._announce(this.msg(
|
|
@@ -25112,6 +25265,7 @@ var Calendar = class extends Component {
|
|
|
25112
25265
|
const pointer = this._pointer;
|
|
25113
25266
|
if (!pointer || pointer.pointerId !== event.pointerId) return;
|
|
25114
25267
|
this._pointer = null;
|
|
25268
|
+
this._releasePointerCapture(pointer.pointerId);
|
|
25115
25269
|
this._clearSelectionPreview();
|
|
25116
25270
|
if (!pointer.moved) return;
|
|
25117
25271
|
event.preventDefault();
|
|
@@ -25129,6 +25283,7 @@ var Calendar = class extends Component {
|
|
|
25129
25283
|
_cancelPointer(event) {
|
|
25130
25284
|
if (!this._pointer || this._pointer.pointerId !== event.pointerId) return;
|
|
25131
25285
|
const control = this._pointer.control;
|
|
25286
|
+
this._releasePointerCapture(this._pointer.pointerId);
|
|
25132
25287
|
this._pointer = null;
|
|
25133
25288
|
this._clearSelectionPreview();
|
|
25134
25289
|
control.closest?.(".zx-calendar__event")?.removeAttribute("data-state");
|
|
@@ -25136,13 +25291,14 @@ var Calendar = class extends Component {
|
|
|
25136
25291
|
}
|
|
25137
25292
|
/** @param {CalendarEvent} event @param {'move'|'resize'} action @param {number} x @param {number} y @returns {CalendarEvent|null} */
|
|
25138
25293
|
_proposalAtPoint(event, action, x, y) {
|
|
25139
|
-
const
|
|
25294
|
+
const resizeTimed = action === "resize" && !calendarEventSpansDays(event);
|
|
25295
|
+
const timed = this._timeAtPoint(x, y, resizeTimed ? event.start : null);
|
|
25140
25296
|
if (timed && !calendarEventSpansDays(event)) {
|
|
25141
25297
|
if (action === "move") {
|
|
25142
25298
|
const duration = event.end.getTime() - event.start.getTime();
|
|
25143
25299
|
const next3 = cloneCalendarEvent(event);
|
|
25144
|
-
next3.start = timed;
|
|
25145
|
-
next3.end = new Date(
|
|
25300
|
+
next3.start = addMinutes(timed, -Number(this._pointer?.timeOffsetMinutes ?? 0));
|
|
25301
|
+
next3.end = new Date(next3.start.getTime() + duration);
|
|
25146
25302
|
return next3;
|
|
25147
25303
|
}
|
|
25148
25304
|
const next2 = cloneCalendarEvent(event);
|
|
@@ -25151,33 +25307,36 @@ var Calendar = class extends Component {
|
|
|
25151
25307
|
}
|
|
25152
25308
|
const day = this._dayAtPoint(x, y);
|
|
25153
25309
|
if (!day) return null;
|
|
25154
|
-
const
|
|
25310
|
+
const targetDay = addCalendarDays(day, -Number(this._pointer?.dayOffset ?? 0));
|
|
25311
|
+
const difference = calendarDayDifference(startOfCalendarDay(event.start), targetDay);
|
|
25155
25312
|
if (action === "move") return shiftCalendarEvent(event, { days: difference, minutes: 0 });
|
|
25156
25313
|
const next = cloneCalendarEvent(event);
|
|
25157
25314
|
const endDay = addCalendarDays(day, 1);
|
|
25158
25315
|
next.end = new Date(endDay.getTime());
|
|
25159
25316
|
return next.end > next.start ? next : null;
|
|
25160
25317
|
}
|
|
25161
|
-
/** @param {number} x @param {number} y @returns {Date|null} */
|
|
25162
|
-
_timeAtPoint(x, y) {
|
|
25318
|
+
/** @param {number} x @param {number} y @param {Date|null} [fixedDay=null] @returns {Date|null} */
|
|
25319
|
+
_timeAtPoint(x, y, fixedDay = null) {
|
|
25163
25320
|
const slots = this.refs.body.querySelector(".zx-calendar__slots");
|
|
25164
25321
|
if (!slots) return null;
|
|
25165
25322
|
const rect = slots.getBoundingClientRect();
|
|
25166
|
-
|
|
25323
|
+
const scroller = slots.closest(".zx-calendar__time-scroller");
|
|
25324
|
+
const viewport = scroller?.getBoundingClientRect();
|
|
25325
|
+
const left = viewport ? Math.max(rect.left, viewport.left) : rect.left;
|
|
25326
|
+
const right = viewport ? Math.min(rect.right, viewport.right) : rect.right;
|
|
25327
|
+
const top = viewport ? Math.max(rect.top, viewport.top) : rect.top;
|
|
25328
|
+
const bottom = viewport ? Math.min(rect.bottom, viewport.bottom) : rect.bottom;
|
|
25329
|
+
if (x < left || x > right || y < top || y > bottom || rect.width <= 0 || rect.height <= 0) return null;
|
|
25167
25330
|
const columns = Number(slots.closest('[role="grid"]')?.getAttribute("aria-colcount")) || 1;
|
|
25168
25331
|
const rows = Number(slots.closest('[role="grid"]')?.getAttribute("aria-rowcount")) || 1;
|
|
25169
|
-
const
|
|
25332
|
+
const fixedColumn = fixedDay ? this._timeGridDayIndex(fixedDay) : -1;
|
|
25333
|
+
const column = fixedColumn >= 0 ? fixedColumn : Math.max(0, Math.min(columns - 1, Math.floor((x - rect.left) / rect.width * columns)));
|
|
25170
25334
|
const row2 = Math.max(0, Math.min(rows - 1, Math.floor((y - rect.top) / rect.height * rows)));
|
|
25171
25335
|
const target = slots.querySelector(`[data-row-index="${row2}"][data-day-index="${column}"]`);
|
|
25172
25336
|
return target ? this._dateFromTarget(target) : null;
|
|
25173
25337
|
}
|
|
25174
25338
|
/** @param {number} x @param {number} y @returns {Date|null} */
|
|
25175
25339
|
_dayAtPoint(x, y) {
|
|
25176
|
-
const elements = typeof document.elementsFromPoint === "function" ? document.elementsFromPoint(x, y) : [];
|
|
25177
|
-
for (const element of elements) {
|
|
25178
|
-
const date = element instanceof HTMLElement ? element.dataset.date : null;
|
|
25179
|
-
if (date) return parseDayKey(date);
|
|
25180
|
-
}
|
|
25181
25340
|
const allDay = this.refs.body.querySelector(".zx-calendar__all-day-grid");
|
|
25182
25341
|
if (allDay) {
|
|
25183
25342
|
const rect = allDay.getBoundingClientRect();
|
|
@@ -25187,8 +25346,72 @@ var Calendar = class extends Component {
|
|
|
25187
25346
|
return parseDayKey(cells[index]?.dataset.date);
|
|
25188
25347
|
}
|
|
25189
25348
|
}
|
|
25349
|
+
for (const monthEvents of this.refs.body.querySelectorAll(".zx-calendar__month-events")) {
|
|
25350
|
+
const rect = monthEvents.getBoundingClientRect();
|
|
25351
|
+
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom || rect.width <= 0) continue;
|
|
25352
|
+
const start = parseDayKey(monthEvents.closest(".zx-calendar__month-week")?.dataset.weekStart);
|
|
25353
|
+
if (!start) continue;
|
|
25354
|
+
const index = Math.max(0, Math.min(6, Math.floor((x - rect.left) / rect.width * 7)));
|
|
25355
|
+
return addCalendarDays(start, index);
|
|
25356
|
+
}
|
|
25357
|
+
for (const agendaDay of this.refs.body.querySelectorAll(".zx-calendar__agenda-day")) {
|
|
25358
|
+
const rect = agendaDay.getBoundingClientRect();
|
|
25359
|
+
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) continue;
|
|
25360
|
+
return parseDayKey(agendaDay.dataset.date);
|
|
25361
|
+
}
|
|
25362
|
+
const elements = typeof document.elementsFromPoint === "function" ? document.elementsFromPoint(x, y) : [];
|
|
25363
|
+
for (const element of elements) {
|
|
25364
|
+
const dateTarget = element instanceof HTMLElement ? element.closest("[data-date]") : null;
|
|
25365
|
+
const date = dateTarget instanceof HTMLElement ? dateTarget.dataset.date : null;
|
|
25366
|
+
if (date) return parseDayKey(date);
|
|
25367
|
+
}
|
|
25190
25368
|
return null;
|
|
25191
25369
|
}
|
|
25370
|
+
/** @param {CalendarEvent} event @param {number} x @param {number} y @returns {{minutes: number, days: number}} */
|
|
25371
|
+
_dragOffsetForEvent(event, x, y) {
|
|
25372
|
+
if (!calendarEventSpansDays(event)) {
|
|
25373
|
+
const timed = this._timeAtPoint(x, y);
|
|
25374
|
+
if (timed) {
|
|
25375
|
+
const slot = normalizeSlot(this.options.slotDuration);
|
|
25376
|
+
const duration = Math.max(0, Math.round((event.end.getTime() - event.start.getTime()) / 6e4));
|
|
25377
|
+
const maxOffset = Math.max(0, duration - slot);
|
|
25378
|
+
const minutes = Math.round((timed.getTime() - event.start.getTime()) / 6e4);
|
|
25379
|
+
return { minutes: Math.max(0, Math.min(maxOffset, minutes)), days: 0 };
|
|
25380
|
+
}
|
|
25381
|
+
}
|
|
25382
|
+
const day = this._dayAtPoint(x, y);
|
|
25383
|
+
const days = day ? calendarDayDifference(startOfCalendarDay(event.start), day) : 0;
|
|
25384
|
+
return { minutes: 0, days: Math.max(0, days) };
|
|
25385
|
+
}
|
|
25386
|
+
/** @param {Date} day @returns {number} */
|
|
25387
|
+
_timeGridDayIndex(day) {
|
|
25388
|
+
const key = calendarDayKey(day);
|
|
25389
|
+
const headings = [...this.refs.body.querySelectorAll(".zx-calendar__day-heading")];
|
|
25390
|
+
return headings.findIndex((heading) => heading.dataset.date === key);
|
|
25391
|
+
}
|
|
25392
|
+
/** @param {CalendarEvent} event @returns {string|null} */
|
|
25393
|
+
_timedPreviewHeight(event) {
|
|
25394
|
+
const grid2 = this.refs.body.querySelector(".zx-calendar__time-grid");
|
|
25395
|
+
if (!grid2 || calendarEventSpansDays(event)) return null;
|
|
25396
|
+
const min = normalizeMinute(this.options.slotMinTime, 0);
|
|
25397
|
+
const max = normalizeMinute(this.options.slotMaxTime, 1440, true);
|
|
25398
|
+
const slot = normalizeSlot(this.options.slotDuration);
|
|
25399
|
+
const start = event.start.getHours() * 60 + event.start.getMinutes();
|
|
25400
|
+
const end = event.end.getHours() * 60 + event.end.getMinutes();
|
|
25401
|
+
const minutes = Math.max(slot, Math.min(max, end) - Math.max(min, start));
|
|
25402
|
+
return `${minutes / (max - min) * 100}%`;
|
|
25403
|
+
}
|
|
25404
|
+
/** @param {number} pointerId @returns {void} */
|
|
25405
|
+
_capturePointer(pointerId) {
|
|
25406
|
+
try {
|
|
25407
|
+
this.el.setPointerCapture?.(pointerId);
|
|
25408
|
+
} catch {
|
|
25409
|
+
}
|
|
25410
|
+
}
|
|
25411
|
+
/** @param {number} pointerId @returns {void} */
|
|
25412
|
+
_releasePointerCapture(pointerId) {
|
|
25413
|
+
if (this.el.hasPointerCapture?.(pointerId)) this.el.releasePointerCapture?.(pointerId);
|
|
25414
|
+
}
|
|
25192
25415
|
/** @param {Date} start @param {Date} end @returns {void} */
|
|
25193
25416
|
_previewSelection(start, end) {
|
|
25194
25417
|
const low = Math.min(start.getTime(), end.getTime());
|
|
@@ -25283,13 +25506,9 @@ var Calendar = class extends Component {
|
|
|
25283
25506
|
}
|
|
25284
25507
|
/** @param {number} direction @returns {this} */
|
|
25285
25508
|
_page(direction) {
|
|
25286
|
-
|
|
25287
|
-
|
|
25288
|
-
|
|
25289
|
-
else if (this._view === "week") date.setDate(date.getDate() + direction * 7);
|
|
25290
|
-
else if (this._view === "month") date.setMonth(date.getMonth() + direction);
|
|
25291
|
-
else date.setFullYear(date.getFullYear() + direction);
|
|
25292
|
-
return this.setDate(date);
|
|
25509
|
+
return this.setDate(calendarPageDate(this._view, this._date, direction, {
|
|
25510
|
+
agendaDays: this.options.agendaDays
|
|
25511
|
+
}));
|
|
25293
25512
|
}
|
|
25294
25513
|
/** @param {{start: Date, end: Date}} range @returns {string} */
|
|
25295
25514
|
_rangeTitle(range) {
|
|
@@ -26446,6 +26665,16 @@ function createRecordCard(record, index, options = {}) {
|
|
|
26446
26665
|
class: "zx-record-card__heading",
|
|
26447
26666
|
id: headingId
|
|
26448
26667
|
}, headingContent);
|
|
26668
|
+
const fieldGroups = partitionRecordCardFields(visibleFields, titleSource, subtitleSource);
|
|
26669
|
+
const eyebrow = createEyebrow(record, index, fieldGroups.eyebrowStart, fieldGroups.eyebrowEnd);
|
|
26670
|
+
const titlePrefix = createFieldList(
|
|
26671
|
+
record,
|
|
26672
|
+
index,
|
|
26673
|
+
fieldGroups.titlePrefix,
|
|
26674
|
+
"zx-record-card__title-prefix",
|
|
26675
|
+
"title-prefix"
|
|
26676
|
+
);
|
|
26677
|
+
const titleRow = titlePrefix ? h("div", { class: "zx-record-card__title-row" }, titlePrefix, heading) : heading;
|
|
26449
26678
|
const selection = selectable === "multi" ? h("label", {
|
|
26450
26679
|
class: "zx-record-card__selection"
|
|
26451
26680
|
}, h("input", {
|
|
@@ -26467,19 +26696,19 @@ function createRecordCard(record, index, options = {}) {
|
|
|
26467
26696
|
h(
|
|
26468
26697
|
"div",
|
|
26469
26698
|
{ class: "zx-record-card__titles" },
|
|
26470
|
-
|
|
26699
|
+
titleRow,
|
|
26471
26700
|
isEmptyContent(subtitle) ? null : h("p", { class: "zx-record-card__subtitle" }, contentNode(subtitle))
|
|
26472
26701
|
),
|
|
26473
26702
|
actionArea
|
|
26474
26703
|
);
|
|
26475
|
-
const metadata =
|
|
26476
|
-
const body = h("div", { class: "zx-record-card__body" }, header, metadata);
|
|
26704
|
+
const metadata = createFieldList(record, index, fieldGroups.metadata, "zx-record-card__metadata");
|
|
26705
|
+
const body = h("div", { class: "zx-record-card__body" }, eyebrow, header, metadata);
|
|
26477
26706
|
const card = (
|
|
26478
26707
|
/** @type {HTMLLIElement} */
|
|
26479
26708
|
h("li", {
|
|
26480
26709
|
class: "zx-record-card",
|
|
26481
26710
|
tabindex: 0,
|
|
26482
|
-
|
|
26711
|
+
ariaDescription: selectable ? selected ? "Selected" : "Not selected" : void 0,
|
|
26483
26712
|
ariaLabelledby: headingId,
|
|
26484
26713
|
dataset: {
|
|
26485
26714
|
recordIndex: String(index),
|
|
@@ -26490,6 +26719,20 @@ function createRecordCard(record, index, options = {}) {
|
|
|
26490
26719
|
);
|
|
26491
26720
|
return card;
|
|
26492
26721
|
}
|
|
26722
|
+
function partitionRecordCardFields(fields, titleSource = null, subtitleSource = null) {
|
|
26723
|
+
const groups = { eyebrowStart: [], eyebrowEnd: [], titlePrefix: [], metadata: [] };
|
|
26724
|
+
const titleId = typeof titleSource === "string" ? titleSource : null;
|
|
26725
|
+
const subtitleId = typeof subtitleSource === "string" ? subtitleSource : null;
|
|
26726
|
+
for (const field of Array.isArray(fields) ? fields : []) {
|
|
26727
|
+
if (!field.duplicate && (field.id === titleId || field.id === subtitleId)) continue;
|
|
26728
|
+
const slot = resolveRecordCardFieldSlot(field);
|
|
26729
|
+
if (slot === "eyebrow-start") groups.eyebrowStart.push(field);
|
|
26730
|
+
else if (slot === "eyebrow-end") groups.eyebrowEnd.push(field);
|
|
26731
|
+
else if (slot === "title-prefix") groups.titlePrefix.push(field);
|
|
26732
|
+
else groups.metadata.push(field);
|
|
26733
|
+
}
|
|
26734
|
+
return groups;
|
|
26735
|
+
}
|
|
26493
26736
|
function resolveRecordPreview(record, index, preview, previewAlt, fields = []) {
|
|
26494
26737
|
if (preview == null) return null;
|
|
26495
26738
|
const candidate = typeof preview === "function" ? preview(record, index) : readRecordSource(record, index, preview, fields, false);
|
|
@@ -26558,21 +26801,49 @@ function createPreview(preview) {
|
|
|
26558
26801
|
dataset: { failed: String(preview.rejected) }
|
|
26559
26802
|
}, fallback, image);
|
|
26560
26803
|
}
|
|
26561
|
-
function
|
|
26562
|
-
const
|
|
26563
|
-
|
|
26564
|
-
|
|
26565
|
-
|
|
26566
|
-
|
|
26804
|
+
function createEyebrow(record, index, eyebrowStart, eyebrowEnd) {
|
|
26805
|
+
const start = createFieldList(
|
|
26806
|
+
record,
|
|
26807
|
+
index,
|
|
26808
|
+
eyebrowStart,
|
|
26809
|
+
"zx-record-card__eyebrow-group",
|
|
26810
|
+
"eyebrow-start"
|
|
26811
|
+
);
|
|
26812
|
+
const end = createFieldList(
|
|
26813
|
+
record,
|
|
26814
|
+
index,
|
|
26815
|
+
eyebrowEnd,
|
|
26816
|
+
"zx-record-card__eyebrow-group",
|
|
26817
|
+
"eyebrow-end"
|
|
26818
|
+
);
|
|
26819
|
+
return start || end ? h("div", { class: "zx-record-card__eyebrow" }, start, end) : null;
|
|
26820
|
+
}
|
|
26821
|
+
function createFieldList(record, index, fields, className, slot = null) {
|
|
26822
|
+
if (fields.length === 0) return null;
|
|
26823
|
+
return h("dl", {
|
|
26824
|
+
class: className,
|
|
26825
|
+
dataset: slot ? { cardSlot: slot } : void 0
|
|
26826
|
+
}, fields.map((field) => h(
|
|
26567
26827
|
"div",
|
|
26568
26828
|
{
|
|
26569
26829
|
class: "zx-record-card__field",
|
|
26570
|
-
dataset: { fieldId: field.id }
|
|
26830
|
+
dataset: { fieldId: field.id, ...slot ? { cardSlot: slot } : {} }
|
|
26571
26831
|
},
|
|
26572
26832
|
h("dt", { class: "zx-record-card__label" }, field.label),
|
|
26573
26833
|
h("dd", { class: "zx-record-card__value" }, contentNode(renderViewField(field, record, index)))
|
|
26574
26834
|
)));
|
|
26575
26835
|
}
|
|
26836
|
+
function resolveRecordCardFieldSlot(field) {
|
|
26837
|
+
const view = field?.view;
|
|
26838
|
+
if (!view || typeof view !== "object" || Array.isArray(view)) return null;
|
|
26839
|
+
const card = view.card;
|
|
26840
|
+
if (!card || typeof card !== "object" || Array.isArray(card)) return null;
|
|
26841
|
+
const slot = (
|
|
26842
|
+
/** @type {{slot?:unknown}} */
|
|
26843
|
+
card.slot
|
|
26844
|
+
);
|
|
26845
|
+
return slot === "eyebrow-start" || slot === "eyebrow-end" || slot === "title-prefix" ? slot : null;
|
|
26846
|
+
}
|
|
26576
26847
|
function createAction(action) {
|
|
26577
26848
|
const label = String(action.label ?? "");
|
|
26578
26849
|
const title = String(action.title ?? (label || action.id));
|
|
@@ -26642,7 +26913,7 @@ function safePreviewUrl(value) {
|
|
|
26642
26913
|
try {
|
|
26643
26914
|
const base = globalThis.location?.href ?? "https://zx.invalid/";
|
|
26644
26915
|
const protocol = new URL(href, base).protocol.toLowerCase();
|
|
26645
|
-
return ["http:", "https:", "blob:"
|
|
26916
|
+
return ["http:", "https:", "blob:"].includes(protocol) ? href : null;
|
|
26646
26917
|
} catch {
|
|
26647
26918
|
return null;
|
|
26648
26919
|
}
|
|
@@ -26686,7 +26957,7 @@ var CardView = class extends RecordView {
|
|
|
26686
26957
|
actions: [],
|
|
26687
26958
|
groupBy: null,
|
|
26688
26959
|
groupOrder: [],
|
|
26689
|
-
minCardWidth: "
|
|
26960
|
+
minCardWidth: "15rem",
|
|
26690
26961
|
maxColumns: null,
|
|
26691
26962
|
variant: "outlined",
|
|
26692
26963
|
loadingCount: 6,
|
|
@@ -26960,7 +27231,7 @@ var CardView = class extends RecordView {
|
|
|
26960
27231
|
for (const [id, card] of this._cardNodesById) {
|
|
26961
27232
|
const selected = this._isViewSelected(id);
|
|
26962
27233
|
card.dataset.selected = String(selected);
|
|
26963
|
-
if (this.options.selectable) card.setAttribute("aria-
|
|
27234
|
+
if (this.options.selectable) card.setAttribute("aria-description", selected ? "Selected" : "Not selected");
|
|
26964
27235
|
const checkbox = (
|
|
26965
27236
|
/** @type {HTMLInputElement|null} */
|
|
26966
27237
|
card.querySelector("[data-record-selection]")
|
|
@@ -27104,11 +27375,11 @@ function normalizeGroupId(value) {
|
|
|
27104
27375
|
return value == null || value === "" ? "" : String(value);
|
|
27105
27376
|
}
|
|
27106
27377
|
function normalizeCardWidth(value) {
|
|
27107
|
-
if (typeof value === "number") return Number.isFinite(value) && value > 0 ? `${value}px` : "
|
|
27378
|
+
if (typeof value === "number") return Number.isFinite(value) && value > 0 ? `${value}px` : "15rem";
|
|
27108
27379
|
const text4 = String(value ?? "").trim();
|
|
27109
|
-
if (!text4 || text4.length > 128 || /[;{}\u0000-\u001f\u007f]/.test(text4)) return "
|
|
27110
|
-
if (/(?:url|image|image-set|cross-fade|element|expression)\s*\(/i.test(text4)) return "
|
|
27111
|
-
if (typeof globalThis.CSS?.supports === "function" && !globalThis.CSS.supports("width", text4)) return "
|
|
27380
|
+
if (!text4 || text4.length > 128 || /[;{}\u0000-\u001f\u007f]/.test(text4)) return "15rem";
|
|
27381
|
+
if (/(?:url|image|image-set|cross-fade|element|expression)\s*\(/i.test(text4)) return "15rem";
|
|
27382
|
+
if (typeof globalThis.CSS?.supports === "function" && !globalThis.CSS.supports("width", text4)) return "15rem";
|
|
27112
27383
|
return text4;
|
|
27113
27384
|
}
|
|
27114
27385
|
function normalizeMaxColumns(value) {
|
|
@@ -27146,8 +27417,284 @@ function safeDomPart(value) {
|
|
|
27146
27417
|
return value.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "empty";
|
|
27147
27418
|
}
|
|
27148
27419
|
|
|
27420
|
+
// src/components/kanban-view/kanban-policy.js
|
|
27421
|
+
var RULE_TONES = Object.freeze(["neutral", "accent", "success", "warning", "danger", "info"]);
|
|
27422
|
+
function normalizeKanbanRules(rules) {
|
|
27423
|
+
if (rules == null) return [];
|
|
27424
|
+
if (!Array.isArray(rules)) throw new TypeError("Kanban rules must be an array or null");
|
|
27425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
27426
|
+
return rules.map((rule, index) => {
|
|
27427
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
|
27428
|
+
throw new TypeError(`Kanban rule at index ${index} must be an object`);
|
|
27429
|
+
}
|
|
27430
|
+
const source = (
|
|
27431
|
+
/** @type {Record<string, any>} */
|
|
27432
|
+
rule
|
|
27433
|
+
);
|
|
27434
|
+
const id = String(source.id ?? "").trim();
|
|
27435
|
+
if (!id) throw new TypeError(`Kanban rule at index ${index} requires an id`);
|
|
27436
|
+
if (seen.has(id)) throw new TypeError(`Duplicate Kanban rule: ${id}`);
|
|
27437
|
+
if (typeof source.when !== "function") {
|
|
27438
|
+
throw new TypeError(`Kanban rule ${id} requires a when predicate`);
|
|
27439
|
+
}
|
|
27440
|
+
seen.add(id);
|
|
27441
|
+
return {
|
|
27442
|
+
id,
|
|
27443
|
+
when: source.when,
|
|
27444
|
+
tone: RULE_TONES.includes(source.tone) ? source.tone : "accent",
|
|
27445
|
+
label: source.label == null ? "" : String(source.label),
|
|
27446
|
+
icon: source.icon == null ? null : String(source.icon),
|
|
27447
|
+
description: source.description == null ? "" : String(source.description)
|
|
27448
|
+
};
|
|
27449
|
+
});
|
|
27450
|
+
}
|
|
27451
|
+
function resolveKanbanRules(rules, record, context) {
|
|
27452
|
+
if (!Array.isArray(rules) || !rules.length) return [];
|
|
27453
|
+
return rules.filter((rule) => {
|
|
27454
|
+
try {
|
|
27455
|
+
return rule.when(record, context) === true;
|
|
27456
|
+
} catch {
|
|
27457
|
+
return false;
|
|
27458
|
+
}
|
|
27459
|
+
});
|
|
27460
|
+
}
|
|
27461
|
+
function evaluateKanbanMove(input) {
|
|
27462
|
+
const { records, column, swimlane, context, from } = input;
|
|
27463
|
+
const moving = Math.max(1, records.length);
|
|
27464
|
+
const policy = column?.wipPolicy === "block" || column?.wipPolicy === "warn" ? column.wipPolicy : input.policy === "block" ? "block" : "warn";
|
|
27465
|
+
const refuse = (reason, extra = {}) => ({
|
|
27466
|
+
allowed: false,
|
|
27467
|
+
reason,
|
|
27468
|
+
limit: null,
|
|
27469
|
+
count: 0,
|
|
27470
|
+
limitExceeded: false,
|
|
27471
|
+
limitReached: false,
|
|
27472
|
+
policy,
|
|
27473
|
+
...extra
|
|
27474
|
+
});
|
|
27475
|
+
const blockedTransition = allowsKanbanTransition(column, swimlane, from);
|
|
27476
|
+
if (blockedTransition) return refuse(blockedTransition);
|
|
27477
|
+
for (const record of records) {
|
|
27478
|
+
if (column.accept && column.accept(record, context) === false) return refuse("accept");
|
|
27479
|
+
if (swimlane?.accept && swimlane.accept(record, context) === false) return refuse("lane-accept");
|
|
27480
|
+
}
|
|
27481
|
+
const capacities = [];
|
|
27482
|
+
if (column.limit != null) {
|
|
27483
|
+
capacities.push({ reason: "wip", limit: column.limit, count: input.columnCount + moving });
|
|
27484
|
+
}
|
|
27485
|
+
const cellLimit = swimlane ? readLaneLimit(column.laneLimits, swimlane.id) : null;
|
|
27486
|
+
if (cellLimit != null) {
|
|
27487
|
+
capacities.push({ reason: "wip", limit: cellLimit, count: input.cellCount + moving });
|
|
27488
|
+
}
|
|
27489
|
+
if (swimlane?.limit != null) {
|
|
27490
|
+
capacities.push({ reason: "lane-wip", limit: swimlane.limit, count: input.laneCount + moving });
|
|
27491
|
+
}
|
|
27492
|
+
const violated = capacities.find((capacity) => capacity.count > capacity.limit);
|
|
27493
|
+
const reported = violated ?? capacities.find((capacity) => capacity.count === capacity.limit) ?? capacities[0];
|
|
27494
|
+
if (violated && policy === "block") {
|
|
27495
|
+
return refuse(violated.reason, {
|
|
27496
|
+
limit: violated.limit,
|
|
27497
|
+
count: violated.count,
|
|
27498
|
+
limitExceeded: true
|
|
27499
|
+
});
|
|
27500
|
+
}
|
|
27501
|
+
return {
|
|
27502
|
+
allowed: true,
|
|
27503
|
+
reason: null,
|
|
27504
|
+
limit: reported ? reported.limit : null,
|
|
27505
|
+
count: reported ? reported.count : 0,
|
|
27506
|
+
limitExceeded: Boolean(violated),
|
|
27507
|
+
limitReached: Boolean(reported && !violated && reported.count === reported.limit),
|
|
27508
|
+
policy
|
|
27509
|
+
};
|
|
27510
|
+
}
|
|
27511
|
+
function allowsKanbanTransition(column, swimlane, from) {
|
|
27512
|
+
if (column.id !== from.column && !allowsOrigin(column.from, from.column)) return "transition";
|
|
27513
|
+
if (swimlane && swimlane.id !== from.lane && !allowsOrigin(swimlane.from, from.lane)) {
|
|
27514
|
+
return "lane-transition";
|
|
27515
|
+
}
|
|
27516
|
+
return null;
|
|
27517
|
+
}
|
|
27518
|
+
function kanbanSearchTerms(search) {
|
|
27519
|
+
const text4 = search == null ? "" : String(search).trim().toLowerCase();
|
|
27520
|
+
if (!text4) return [];
|
|
27521
|
+
return [...text4.matchAll(/"([^"]*)"|(\S+)/g)].map((match) => (match[1] ?? match[2] ?? "").trim()).filter(Boolean);
|
|
27522
|
+
}
|
|
27523
|
+
function matchesKanbanSearch(text4, terms) {
|
|
27524
|
+
if (!terms.length) return true;
|
|
27525
|
+
const haystack = text4.toLowerCase();
|
|
27526
|
+
return terms.every((term) => haystack.includes(term));
|
|
27527
|
+
}
|
|
27528
|
+
function createKanbanHistory(limit = 50) {
|
|
27529
|
+
const max = Number.isFinite(Number(limit)) && Number(limit) > 0 ? Math.floor(Number(limit)) : 50;
|
|
27530
|
+
let undoStack = [];
|
|
27531
|
+
let redoStack = [];
|
|
27532
|
+
return {
|
|
27533
|
+
push(entry) {
|
|
27534
|
+
undoStack.push(entry);
|
|
27535
|
+
if (undoStack.length > max) undoStack = undoStack.slice(-max);
|
|
27536
|
+
redoStack = [];
|
|
27537
|
+
},
|
|
27538
|
+
undo(current) {
|
|
27539
|
+
const entry = undoStack.pop();
|
|
27540
|
+
if (!entry) return null;
|
|
27541
|
+
redoStack.push(current);
|
|
27542
|
+
if (redoStack.length > max) redoStack = redoStack.slice(-max);
|
|
27543
|
+
return entry;
|
|
27544
|
+
},
|
|
27545
|
+
redo(current) {
|
|
27546
|
+
const entry = redoStack.pop();
|
|
27547
|
+
if (!entry) return null;
|
|
27548
|
+
undoStack.push(current);
|
|
27549
|
+
if (undoStack.length > max) undoStack = undoStack.slice(-max);
|
|
27550
|
+
return entry;
|
|
27551
|
+
},
|
|
27552
|
+
canUndo: () => undoStack.length > 0,
|
|
27553
|
+
canRedo: () => redoStack.length > 0,
|
|
27554
|
+
clear() {
|
|
27555
|
+
undoStack = [];
|
|
27556
|
+
redoStack = [];
|
|
27557
|
+
},
|
|
27558
|
+
depth: () => ({ undo: undoStack.length, redo: redoStack.length })
|
|
27559
|
+
};
|
|
27560
|
+
}
|
|
27561
|
+
function reorderKanbanRecords(records, entries, options) {
|
|
27562
|
+
if (!Array.isArray(records) || !Array.isArray(entries) || !entries.length) return null;
|
|
27563
|
+
const sources = entries.map((entry) => ({
|
|
27564
|
+
...entry,
|
|
27565
|
+
index: records.findIndex((record) => Object.is(viewRecordId(record, options.recordId), entry.id))
|
|
27566
|
+
}));
|
|
27567
|
+
if (sources.some((source) => source.index < 0 || !source.record || typeof source.record !== "object")) {
|
|
27568
|
+
return null;
|
|
27569
|
+
}
|
|
27570
|
+
sources.sort((left, right) => left.index - right.index);
|
|
27571
|
+
const moves = sources.map((source) => ({
|
|
27572
|
+
id: source.id,
|
|
27573
|
+
from: locateKanbanRecord(records, source.index, options)
|
|
27574
|
+
}));
|
|
27575
|
+
const movingIndices = new Set(sources.map((source) => source.index));
|
|
27576
|
+
const next = records.filter((_, index) => !movingIndices.has(index));
|
|
27577
|
+
const destinationRecords = next.filter((record, index) => normalizeAxisId(readKanbanAccessor(record, index, options.columnBy)) === options.destination.column && (options.swimlaneBy == null || normalizeAxisId(readKanbanAccessor(record, index, options.swimlaneBy)) === options.destination.lane));
|
|
27578
|
+
const targetIndex = clampKanbanIndex(options.destination.index, destinationRecords.length);
|
|
27579
|
+
let insertion = next.length;
|
|
27580
|
+
if (destinationRecords[targetIndex]) insertion = next.indexOf(destinationRecords[targetIndex]);
|
|
27581
|
+
else if (destinationRecords.length) insertion = next.indexOf(destinationRecords.at(-1)) + 1;
|
|
27582
|
+
next.splice(insertion, 0, ...sources.map((source) => source.record));
|
|
27583
|
+
return {
|
|
27584
|
+
records: next,
|
|
27585
|
+
moves,
|
|
27586
|
+
to: {
|
|
27587
|
+
column: options.destination.column,
|
|
27588
|
+
lane: options.swimlaneBy == null ? null : options.destination.lane,
|
|
27589
|
+
index: targetIndex
|
|
27590
|
+
}
|
|
27591
|
+
};
|
|
27592
|
+
}
|
|
27593
|
+
function locateKanbanRecord(records, index, options) {
|
|
27594
|
+
const record = records[index];
|
|
27595
|
+
const column = normalizeAxisId(readKanbanAccessor(record, index, options.columnBy));
|
|
27596
|
+
const lane = options.swimlaneBy == null ? null : normalizeAxisId(readKanbanAccessor(record, index, options.swimlaneBy));
|
|
27597
|
+
const group = records.filter((candidate, candidateIndex) => normalizeAxisId(readKanbanAccessor(candidate, candidateIndex, options.columnBy)) === column && (options.swimlaneBy == null || normalizeAxisId(readKanbanAccessor(candidate, candidateIndex, options.swimlaneBy)) === lane));
|
|
27598
|
+
const id = viewRecordId(record, options.recordId);
|
|
27599
|
+
return {
|
|
27600
|
+
column,
|
|
27601
|
+
lane,
|
|
27602
|
+
index: group.findIndex((candidate) => Object.is(viewRecordId(candidate, options.recordId), id))
|
|
27603
|
+
};
|
|
27604
|
+
}
|
|
27605
|
+
function absoluteKanbanIndex(visibility, visibleIndex) {
|
|
27606
|
+
const target = clampKanbanIndex(visibleIndex, visibility.filter(Boolean).length);
|
|
27607
|
+
let seen = 0;
|
|
27608
|
+
for (let index = 0; index < visibility.length; index += 1) {
|
|
27609
|
+
if (!visibility[index]) continue;
|
|
27610
|
+
if (seen === target) return index;
|
|
27611
|
+
seen += 1;
|
|
27612
|
+
}
|
|
27613
|
+
return visibility.length;
|
|
27614
|
+
}
|
|
27615
|
+
function readKanbanAccessor(record, index, accessor) {
|
|
27616
|
+
return typeof accessor === "function" ? accessor(record, index) : accessor == null ? null : record?.[accessor];
|
|
27617
|
+
}
|
|
27618
|
+
function normalizeAxisId(value) {
|
|
27619
|
+
return value == null ? "" : String(value);
|
|
27620
|
+
}
|
|
27621
|
+
function clampKanbanIndex(value, maximum) {
|
|
27622
|
+
if (value == null || !Number.isFinite(Number(value))) return maximum;
|
|
27623
|
+
return Math.max(0, Math.min(maximum, Math.trunc(Number(value))));
|
|
27624
|
+
}
|
|
27625
|
+
function allowsOrigin(allowed, origin) {
|
|
27626
|
+
if (allowed == null) return true;
|
|
27627
|
+
if (!Array.isArray(allowed)) return true;
|
|
27628
|
+
if (!allowed.length) return false;
|
|
27629
|
+
return allowed.some((entry) => String(entry) === "*" || String(entry) === normalizeAxisId(origin));
|
|
27630
|
+
}
|
|
27631
|
+
function readLaneLimit(laneLimits, lane) {
|
|
27632
|
+
if (!laneLimits || typeof laneLimits !== "object") return null;
|
|
27633
|
+
const value = (
|
|
27634
|
+
/** @type {Record<string, unknown>} */
|
|
27635
|
+
laneLimits[lane]
|
|
27636
|
+
);
|
|
27637
|
+
if (value == null || value === "") return null;
|
|
27638
|
+
const number2 = Number(value);
|
|
27639
|
+
return Number.isFinite(number2) && number2 >= 0 ? Math.floor(number2) : null;
|
|
27640
|
+
}
|
|
27641
|
+
|
|
27149
27642
|
// src/components/kanban-view/kanban-view.js
|
|
27150
27643
|
var INTERACTIVE = 'a, button, input, select, textarea, summary, [contenteditable="true"]';
|
|
27644
|
+
var DRAG_THRESHOLD = 4;
|
|
27645
|
+
var TOUCH_HOLD = 350;
|
|
27646
|
+
var SEARCH_DEBOUNCE = 120;
|
|
27647
|
+
var SCROLL_EDGE = 56;
|
|
27648
|
+
var SCROLL_SPEED = 18;
|
|
27649
|
+
var DEFAULT_LABELS = Object.freeze({
|
|
27650
|
+
fields: "Fields",
|
|
27651
|
+
search: "Search board",
|
|
27652
|
+
searchPlaceholder: "Search cards",
|
|
27653
|
+
clearSearch: "Clear search",
|
|
27654
|
+
searchSummary: "%count% of %total% cards match.",
|
|
27655
|
+
undo: "Undo",
|
|
27656
|
+
redo: "Redo",
|
|
27657
|
+
add: "Add card",
|
|
27658
|
+
addTo: "Add card to %target%",
|
|
27659
|
+
move: "Move %name%",
|
|
27660
|
+
moveMultiple: "Move %count% selected cards",
|
|
27661
|
+
collapseColumn: "Collapse column %label%",
|
|
27662
|
+
expandColumn: "Expand column %label%",
|
|
27663
|
+
collapseLane: "Collapse swim lane %label%",
|
|
27664
|
+
expandLane: "Expand swim lane %label%",
|
|
27665
|
+
count: "%count% records",
|
|
27666
|
+
countFiltered: "%count% of %total% records",
|
|
27667
|
+
countLimit: "%count% records, work-in-progress limit %limit%",
|
|
27668
|
+
countLimitInLane: "%count% records in this lane, %total% of work-in-progress limit %limit% in the column",
|
|
27669
|
+
empty: "No records",
|
|
27670
|
+
columnEmpty: "",
|
|
27671
|
+
selected: "Selected",
|
|
27672
|
+
notSelected: "Not selected",
|
|
27673
|
+
noMatches: "No cards match the current search.",
|
|
27674
|
+
record: "record",
|
|
27675
|
+
grabbed: "Grabbed %name%. Use left and right for columns, up and down for position, and Alt plus up or down for swim lanes.",
|
|
27676
|
+
grabbedMultiple: "Grabbed %count% cards. Use left and right for columns, up and down for position, and Alt plus up or down for swim lanes.",
|
|
27677
|
+
target: "Target %target%, position %position% of %total%.",
|
|
27678
|
+
moved: "Moved %name% to %target%.%wip%",
|
|
27679
|
+
movedMultiple: "Moved %count% cards to %target%.%wip%",
|
|
27680
|
+
moveRequested: "Move requested for %name% to %target%.%wip%",
|
|
27681
|
+
moveCanceled: "Move canceled for %name%.",
|
|
27682
|
+
moveCanceledPlain: "Move canceled.",
|
|
27683
|
+
limitReached: " Work-in-progress limit %limit% reached.",
|
|
27684
|
+
limitExceeded: " Work-in-progress limit %limit% exceeded.",
|
|
27685
|
+
rejectAccept: "Move rejected: %target% does not accept %name%.",
|
|
27686
|
+
rejectTransition: "Move rejected: %target% does not accept cards from %origin%.",
|
|
27687
|
+
rejectWip: "Move rejected: %target% is at its work-in-progress limit of %limit%.",
|
|
27688
|
+
rejectDestination: "Move rejected: the destination is not available.",
|
|
27689
|
+
rejectGrouping: "Move rejected: callback grouping requires external move mode.",
|
|
27690
|
+
undone: "Move undone.",
|
|
27691
|
+
redone: "Move redone.",
|
|
27692
|
+
nothingToUndo: "Nothing to undo.",
|
|
27693
|
+
nothingToRedo: "Nothing to redo.",
|
|
27694
|
+
menuMoveTo: "Move to %target%",
|
|
27695
|
+
menuMoveToLane: "Move to lane %target%",
|
|
27696
|
+
dragPreview: "%count% cards"
|
|
27697
|
+
});
|
|
27151
27698
|
var KanbanView = class extends RecordView {
|
|
27152
27699
|
static cssName = "kanban-view";
|
|
27153
27700
|
/** @type {Readonly<KanbanViewOptions & RecordViewOptions>} */
|
|
@@ -27164,6 +27711,27 @@ var KanbanView = class extends RecordView {
|
|
|
27164
27711
|
link: null,
|
|
27165
27712
|
actions: [],
|
|
27166
27713
|
moveMode: "local",
|
|
27714
|
+
dragFrom: "card",
|
|
27715
|
+
multiMove: true,
|
|
27716
|
+
autoScroll: true,
|
|
27717
|
+
wipPolicy: "warn",
|
|
27718
|
+
rules: [],
|
|
27719
|
+
search: "",
|
|
27720
|
+
searchFields: null,
|
|
27721
|
+
filter: null,
|
|
27722
|
+
searchControl: false,
|
|
27723
|
+
history: true,
|
|
27724
|
+
historyLimit: 50,
|
|
27725
|
+
historyControls: false,
|
|
27726
|
+
columnHeight: null,
|
|
27727
|
+
allowAdd: false,
|
|
27728
|
+
contextMenu: false,
|
|
27729
|
+
renderCard: null,
|
|
27730
|
+
renderColumnHeader: null,
|
|
27731
|
+
renderSwimlaneHeader: null,
|
|
27732
|
+
renderColumnEmpty: null,
|
|
27733
|
+
renderDragPreview: null,
|
|
27734
|
+
labels: {},
|
|
27167
27735
|
columnOrder: [],
|
|
27168
27736
|
swimlaneOrder: [],
|
|
27169
27737
|
collapsedColumns: [],
|
|
@@ -27199,17 +27767,45 @@ var KanbanView = class extends RecordView {
|
|
|
27199
27767
|
this._kanbanSwimlaneOrder = normalizeOrder(options.swimlaneOrder);
|
|
27200
27768
|
this._collapsedColumns = new Set(normalizeOrder(options.collapsedColumns));
|
|
27201
27769
|
this._collapsedSwimlanes = new Set(normalizeOrder(options.collapsedSwimlanes));
|
|
27770
|
+
this._kanbanRules = normalizeKanbanRules(options.rules);
|
|
27771
|
+
this._kanbanSearch = options.search == null ? "" : String(options.search);
|
|
27772
|
+
this._kanbanFilter = typeof options.filter === "function" ? options.filter : null;
|
|
27773
|
+
this._kanbanHistory = createKanbanHistory(options.historyLimit);
|
|
27202
27774
|
this._keyboardMove = /** @type {KanbanKeyboardMove|null} */
|
|
27203
27775
|
null;
|
|
27204
|
-
this.
|
|
27776
|
+
this._dragIds = /** @type {unknown[]} */
|
|
27777
|
+
[];
|
|
27205
27778
|
this._dragDestination = /** @type {KanbanMovePoint|null} */
|
|
27206
27779
|
null;
|
|
27780
|
+
this._pointerStart = /** @type {{x:number,y:number,id:number,ids:unknown[],touch:boolean}|null} */
|
|
27781
|
+
null;
|
|
27782
|
+
this._pointerHold = 0;
|
|
27783
|
+
this._dragFrame = 0;
|
|
27784
|
+
this._dragPreview = /** @type {HTMLElement|null} */
|
|
27785
|
+
null;
|
|
27786
|
+
this._dragPointer = /** @type {number|null} */
|
|
27787
|
+
null;
|
|
27788
|
+
this._dragCaptured = false;
|
|
27789
|
+
this._searchTimer = 0;
|
|
27790
|
+
this._dragPoint = { x: 0, y: 0 };
|
|
27791
|
+
this._suppressClick = false;
|
|
27207
27792
|
this._announcementToken = 0;
|
|
27208
27793
|
this._cardMeta = /* @__PURE__ */ new WeakMap();
|
|
27209
27794
|
this._sectionMeta = /* @__PURE__ */ new WeakMap();
|
|
27210
27795
|
this._cardsById = /* @__PURE__ */ new Map();
|
|
27796
|
+
this._visibleIds = /* @__PURE__ */ new Set();
|
|
27797
|
+
this._kanbanMenu = /** @type {ContextMenu|null} */
|
|
27798
|
+
null;
|
|
27799
|
+
this._searchInput = /** @type {HTMLInputElement|null} */
|
|
27800
|
+
null;
|
|
27801
|
+
this._undoButton = /** @type {HTMLButtonElement|null} */
|
|
27802
|
+
null;
|
|
27803
|
+
this._redoButton = /** @type {HTMLButtonElement|null} */
|
|
27804
|
+
null;
|
|
27211
27805
|
this._toolbar = h("div", { class: "zx-record-view__toolbar zx-kanban-view__toolbar" });
|
|
27212
|
-
if (options.fieldControls) this._toolbar.append(this._createViewFieldControls());
|
|
27806
|
+
if (options.fieldControls) this._toolbar.append(this._createViewFieldControls(this._text("fields")));
|
|
27807
|
+
if (options.searchControl) this._toolbar.append(this._createSearchControl());
|
|
27808
|
+
if (options.historyControls) this._toolbar.append(this._createHistoryControls());
|
|
27213
27809
|
this._board = h("div", {
|
|
27214
27810
|
class: "zx-kanban-view__board",
|
|
27215
27811
|
role: "region",
|
|
@@ -27222,6 +27818,10 @@ var KanbanView = class extends RecordView {
|
|
|
27222
27818
|
ariaAtomic: "true"
|
|
27223
27819
|
});
|
|
27224
27820
|
root.replaceChildren(this._toolbar, this._board, this._live);
|
|
27821
|
+
if (options.columnHeight) {
|
|
27822
|
+
root.style.setProperty("--zx-kanban-column-height", String(options.columnHeight));
|
|
27823
|
+
root.dataset.columnScroll = "true";
|
|
27824
|
+
}
|
|
27225
27825
|
this.listen(this._board, "click", (event) => this._handleBoardClick(
|
|
27226
27826
|
/** @type {MouseEvent} */
|
|
27227
27827
|
event
|
|
@@ -27235,20 +27835,22 @@ var KanbanView = class extends RecordView {
|
|
|
27235
27835
|
event
|
|
27236
27836
|
));
|
|
27237
27837
|
this.listen(this._board, "error", (event) => this._handlePreviewError(event), { capture: true });
|
|
27238
|
-
this.listen(this._board, "
|
|
27239
|
-
/** @type {
|
|
27838
|
+
this.listen(this._board, "pointerdown", (event) => this._handlePointerDown(
|
|
27839
|
+
/** @type {PointerEvent} */
|
|
27240
27840
|
event
|
|
27241
27841
|
));
|
|
27242
|
-
this.listen(this._board, "
|
|
27243
|
-
/** @type {
|
|
27842
|
+
this.listen(this._board, "pointermove", (event) => this._handlePointerMove(
|
|
27843
|
+
/** @type {PointerEvent} */
|
|
27244
27844
|
event
|
|
27245
27845
|
));
|
|
27246
|
-
this.listen(this._board, "
|
|
27247
|
-
/** @type {
|
|
27846
|
+
this.listen(this._board, "pointerup", (event) => this._handlePointerUp(
|
|
27847
|
+
/** @type {PointerEvent} */
|
|
27248
27848
|
event
|
|
27249
27849
|
));
|
|
27250
|
-
this.listen(this._board, "
|
|
27850
|
+
this.listen(this._board, "pointercancel", () => this._clearDrag());
|
|
27851
|
+
this.listen(this._board, "dragstart", (event) => event.preventDefault());
|
|
27251
27852
|
this._refreshView("init");
|
|
27853
|
+
if (options.contextMenu) this._createContextMenu();
|
|
27252
27854
|
return root;
|
|
27253
27855
|
} catch (error2) {
|
|
27254
27856
|
if (this._kanbanSnapshot) restoreTarget(root, this._kanbanSnapshot);
|
|
@@ -27266,7 +27868,16 @@ var KanbanView = class extends RecordView {
|
|
|
27266
27868
|
*/
|
|
27267
27869
|
setColumns(columns) {
|
|
27268
27870
|
this._kanbanColumns = columns === null ? null : normalizeKanbanDescriptors(columns, "column");
|
|
27269
|
-
|
|
27871
|
+
const descriptors = this._rawColumns();
|
|
27872
|
+
this._kanbanColumnOrder = reconcileKanbanPreference(
|
|
27873
|
+
descriptors,
|
|
27874
|
+
this._kanbanColumnOrder,
|
|
27875
|
+
this._kanbanColumns === null
|
|
27876
|
+
);
|
|
27877
|
+
if (this._kanbanColumns !== null) {
|
|
27878
|
+
const ids = new Set(descriptors.map((column) => column.id));
|
|
27879
|
+
this._collapsedColumns = new Set([...this._collapsedColumns].filter((id) => ids.has(id)));
|
|
27880
|
+
}
|
|
27270
27881
|
this._refreshView("columns");
|
|
27271
27882
|
this._emitKanbanState("columns");
|
|
27272
27883
|
return this;
|
|
@@ -27282,7 +27893,17 @@ var KanbanView = class extends RecordView {
|
|
|
27282
27893
|
*/
|
|
27283
27894
|
setSwimlanes(lanes) {
|
|
27284
27895
|
this._kanbanSwimlanes = lanes === null ? null : normalizeKanbanDescriptors(lanes, "swimlane");
|
|
27285
|
-
|
|
27896
|
+
const descriptors = this._rawSwimlanes();
|
|
27897
|
+
const preserveUnknown = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
27898
|
+
this._kanbanSwimlaneOrder = reconcileKanbanPreference(
|
|
27899
|
+
descriptors,
|
|
27900
|
+
this._kanbanSwimlaneOrder,
|
|
27901
|
+
preserveUnknown
|
|
27902
|
+
);
|
|
27903
|
+
if (!preserveUnknown) {
|
|
27904
|
+
const ids = new Set(descriptors.map((lane) => lane.id));
|
|
27905
|
+
this._collapsedSwimlanes = new Set([...this._collapsedSwimlanes].filter((id) => ids.has(id)));
|
|
27906
|
+
}
|
|
27286
27907
|
this._refreshView("swimlanes");
|
|
27287
27908
|
this._emitKanbanState("swimlanes");
|
|
27288
27909
|
return this;
|
|
@@ -27298,9 +27919,16 @@ var KanbanView = class extends RecordView {
|
|
|
27298
27919
|
* @returns {this}
|
|
27299
27920
|
*/
|
|
27300
27921
|
setColumnOrder(order, options = {}) {
|
|
27301
|
-
const
|
|
27302
|
-
|
|
27303
|
-
|
|
27922
|
+
const descriptors = this._rawColumns();
|
|
27923
|
+
const current = this.getColumnOrder();
|
|
27924
|
+
const next = reconcileKanbanOrder(descriptors, order);
|
|
27925
|
+
if (sameArray3(next, current)) return this;
|
|
27926
|
+
this._kanbanColumnOrder = mergeKanbanResolvedOrder(
|
|
27927
|
+
this._kanbanColumnOrder,
|
|
27928
|
+
descriptors,
|
|
27929
|
+
next,
|
|
27930
|
+
this._kanbanColumns === null
|
|
27931
|
+
);
|
|
27304
27932
|
this._refreshView("columnorder");
|
|
27305
27933
|
if (!options.silent) this._emitKanbanState("columnorder");
|
|
27306
27934
|
return this;
|
|
@@ -27326,9 +27954,17 @@ var KanbanView = class extends RecordView {
|
|
|
27326
27954
|
* @returns {this}
|
|
27327
27955
|
*/
|
|
27328
27956
|
setSwimlaneOrder(order, options = {}) {
|
|
27329
|
-
const
|
|
27330
|
-
|
|
27331
|
-
|
|
27957
|
+
const descriptors = this._rawSwimlanes();
|
|
27958
|
+
const current = this.getSwimlaneOrder();
|
|
27959
|
+
const next = reconcileKanbanOrder(descriptors, order);
|
|
27960
|
+
if (sameArray3(next, current)) return this;
|
|
27961
|
+
const preserveUnknown = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
27962
|
+
this._kanbanSwimlaneOrder = mergeKanbanResolvedOrder(
|
|
27963
|
+
this._kanbanSwimlaneOrder,
|
|
27964
|
+
descriptors,
|
|
27965
|
+
next,
|
|
27966
|
+
preserveUnknown
|
|
27967
|
+
);
|
|
27332
27968
|
this._refreshView("swimlaneorder");
|
|
27333
27969
|
if (!options.silent) this._emitKanbanState("swimlaneorder");
|
|
27334
27970
|
return this;
|
|
@@ -27396,126 +28032,292 @@ var KanbanView = class extends RecordView {
|
|
|
27396
28032
|
return this.setSwimlaneCollapsed(id, !this._collapsedSwimlanes.has(String(id)));
|
|
27397
28033
|
}
|
|
27398
28034
|
/**
|
|
27399
|
-
* Proposes and, in local mode, commits a record move.
|
|
27400
|
-
* a cancelable `recordmove` event. WIP limits are reported but remain advisory.
|
|
28035
|
+
* Proposes and, in local mode, commits a record move. Equivalent to `moveRecords([id], …)`.
|
|
27401
28036
|
* @param {unknown} id Record id.
|
|
27402
28037
|
* @param {KanbanDestination} destination Destination.
|
|
27403
28038
|
* @returns {this}
|
|
27404
28039
|
* @fires KanbanView#recordmove
|
|
28040
|
+
* @fires KanbanView#movereject
|
|
27405
28041
|
* @fires RecordView#datachange
|
|
27406
28042
|
*/
|
|
27407
28043
|
moveRecord(id, destination) {
|
|
28044
|
+
return this.moveRecords([id], destination);
|
|
28045
|
+
}
|
|
28046
|
+
/**
|
|
28047
|
+
* Proposes and, in local mode, commits an atomic move of one or more records. Destination
|
|
28048
|
+
* eligibility, transition rules, and work-in-progress policy are all checked before a single
|
|
28049
|
+
* cancelable `recordmove` event; the move commits completely or not at all, and the moving
|
|
28050
|
+
* records keep their relative order at the destination.
|
|
28051
|
+
* @param {unknown[]} ids Record ids, in any order.
|
|
28052
|
+
* @param {KanbanDestination} destination Destination.
|
|
28053
|
+
* @returns {this}
|
|
28054
|
+
* @fires KanbanView#recordmove
|
|
28055
|
+
* @fires KanbanView#movereject
|
|
28056
|
+
* @fires RecordView#datachange
|
|
28057
|
+
*/
|
|
28058
|
+
moveRecords(ids, destination) {
|
|
27408
28059
|
const options = this._kanbanOptions();
|
|
27409
|
-
|
|
27410
|
-
const
|
|
27411
|
-
if (!
|
|
28060
|
+
if (!Array.isArray(ids) || !destination || typeof destination !== "object") return this;
|
|
28061
|
+
const moving = this._orderedIds(ids);
|
|
28062
|
+
if (!moving.length) return this;
|
|
28063
|
+
const records = moving.map((id) => (
|
|
28064
|
+
/** @type {KanbanRecord} */
|
|
28065
|
+
this.getRecord(id)
|
|
28066
|
+
));
|
|
28067
|
+
const origins = moving.map((id) => (
|
|
28068
|
+
/** @type {KanbanMovePoint} */
|
|
28069
|
+
this._recordLocation(id)
|
|
28070
|
+
));
|
|
28071
|
+
const from = origins[0];
|
|
27412
28072
|
const columns = this._resolvedColumns();
|
|
27413
28073
|
const lanes = this._resolvedSwimlanes();
|
|
27414
28074
|
const columnId = destination.column == null ? from.column : String(destination.column);
|
|
27415
28075
|
const laneId = options.swimlaneBy == null ? null : destination.lane === void 0 ? from.lane : normalizeAxisId(destination.lane);
|
|
27416
28076
|
const column = columns.find((candidate) => candidate.id === columnId);
|
|
27417
|
-
const lane = options.swimlaneBy == null ? null : lanes.find((candidate) => candidate.id === laneId);
|
|
28077
|
+
const lane = options.swimlaneBy == null ? null : lanes.find((candidate) => candidate.id === laneId) ?? null;
|
|
27418
28078
|
if (!column || options.swimlaneBy != null && !lane) {
|
|
27419
|
-
this.
|
|
28079
|
+
this._rejectMove("destination", { ids: moving, records, from, to: null, column: null, swimlane: null });
|
|
27420
28080
|
return this;
|
|
27421
28081
|
}
|
|
27422
|
-
const
|
|
27423
|
-
const
|
|
27424
|
-
const index =
|
|
28082
|
+
const excluded = new Set(moving);
|
|
28083
|
+
const cellCount = this._recordsIn(columnId, laneId).filter((entry) => !excluded.has(this._viewRecordId(entry.record))).length;
|
|
28084
|
+
const index = clampKanbanIndex(destination.index, cellCount);
|
|
27425
28085
|
const to = { column: columnId, lane: laneId, index };
|
|
27426
|
-
if (sameMovePoint(from, to)) return this;
|
|
27427
|
-
const context = {
|
|
27428
|
-
|
|
27429
|
-
|
|
28086
|
+
if (moving.length === 1 && sameMovePoint(from, to)) return this;
|
|
28087
|
+
const context = {
|
|
28088
|
+
id: moving[0],
|
|
28089
|
+
from: { ...from },
|
|
28090
|
+
to: { ...to },
|
|
28091
|
+
column: { ...column },
|
|
28092
|
+
swimlane: lane ? { ...lane } : null
|
|
28093
|
+
};
|
|
28094
|
+
const evaluation = this._evaluateMove(records, origins, column, lane, to, context, excluded);
|
|
28095
|
+
if (!evaluation.allowed) {
|
|
28096
|
+
this._rejectMove(
|
|
28097
|
+
/** @type {KanbanRejectReason} */
|
|
28098
|
+
evaluation.reason,
|
|
28099
|
+
{
|
|
28100
|
+
ids: moving,
|
|
28101
|
+
records,
|
|
28102
|
+
from,
|
|
28103
|
+
to,
|
|
28104
|
+
column,
|
|
28105
|
+
swimlane: lane,
|
|
28106
|
+
evaluation
|
|
28107
|
+
}
|
|
28108
|
+
);
|
|
27430
28109
|
return this;
|
|
27431
28110
|
}
|
|
27432
|
-
|
|
27433
|
-
|
|
27434
|
-
|
|
27435
|
-
|
|
27436
|
-
|
|
28111
|
+
const entries = [];
|
|
28112
|
+
for (let position2 = 0; position2 < moving.length; position2 += 1) {
|
|
28113
|
+
const record = records[position2];
|
|
28114
|
+
const moved = options.moveMode === "local" ? this._localMovedRecord(record, origins[position2], to, column, lane) : record;
|
|
28115
|
+
if (!moved) {
|
|
28116
|
+
this._rejectMove("grouping", { ids: moving, records, from, to, column, swimlane: lane });
|
|
27437
28117
|
return this;
|
|
27438
28118
|
}
|
|
28119
|
+
entries.push({ id: moving[position2], record: moved });
|
|
27439
28120
|
}
|
|
27440
|
-
const next =
|
|
28121
|
+
const next = reorderKanbanRecords(this._viewData, entries, {
|
|
27441
28122
|
recordId: options.recordId,
|
|
27442
28123
|
columnBy: (candidate, candidateIndex) => this._readAxis(options.columnBy, candidate, candidateIndex),
|
|
27443
28124
|
swimlaneBy: options.swimlaneBy == null ? null : (candidate, candidateIndex) => this._readAxis(options.swimlaneBy, candidate, candidateIndex),
|
|
27444
28125
|
destination: to
|
|
27445
28126
|
});
|
|
27446
28127
|
if (!next) {
|
|
27447
|
-
this.
|
|
28128
|
+
this._rejectMove("destination", { ids: moving, records, from, to, column, swimlane: lane });
|
|
27448
28129
|
return this;
|
|
27449
28130
|
}
|
|
27450
|
-
const
|
|
27451
|
-
const limitExceeded = column.limit != null && finalCount > column.limit;
|
|
28131
|
+
const wip = this._wipAnnouncement(evaluation);
|
|
27452
28132
|
const moveEvent = this.emit("recordmove", {
|
|
27453
|
-
record,
|
|
27454
|
-
id,
|
|
28133
|
+
record: records[0],
|
|
28134
|
+
id: moving[0],
|
|
28135
|
+
records: [...records],
|
|
28136
|
+
ids: [...moving],
|
|
28137
|
+
moves: next.moves.map((move) => ({ id: move.id, from: { ...move.from } })),
|
|
27455
28138
|
from: { ...from },
|
|
27456
28139
|
to: { ...next.to },
|
|
27457
28140
|
column: { ...column },
|
|
27458
28141
|
swimlane: lane ? { ...lane } : null,
|
|
27459
|
-
limitExceeded
|
|
27460
|
-
});
|
|
28142
|
+
limitExceeded: evaluation.limitExceeded
|
|
28143
|
+
}, { honorDomCancellation: true });
|
|
27461
28144
|
if (moveEvent.defaultPrevented) {
|
|
27462
|
-
this._announce(
|
|
28145
|
+
this._announce(this._text("moveCanceled", { name: this._movingName(records) }));
|
|
27463
28146
|
return this;
|
|
27464
28147
|
}
|
|
27465
28148
|
if (options.moveMode === "external") {
|
|
27466
|
-
this._announce(
|
|
28149
|
+
this._announce(this._text("moveRequested", {
|
|
28150
|
+
name: this._movingName(records),
|
|
28151
|
+
target: this._targetName(next.to),
|
|
28152
|
+
wip
|
|
28153
|
+
}));
|
|
27467
28154
|
return this;
|
|
27468
28155
|
}
|
|
27469
|
-
this.
|
|
27470
|
-
this.
|
|
27471
|
-
|
|
27472
|
-
|
|
27473
|
-
|
|
28156
|
+
this._commitLocalRecords(next.records);
|
|
28157
|
+
this._announce(moving.length > 1 ? this._text("movedMultiple", { count: moving.length, target: this._targetName(next.to), wip }) : this._text("moved", {
|
|
28158
|
+
name: this._recordName(entries[0].record, next.to.index),
|
|
28159
|
+
target: this._targetName(next.to),
|
|
28160
|
+
wip
|
|
28161
|
+
}));
|
|
28162
|
+
queueMicrotask(() => this._moveHandle(moving[0])?.focus());
|
|
27474
28163
|
return this;
|
|
27475
28164
|
}
|
|
27476
|
-
/**
|
|
27477
|
-
|
|
27478
|
-
return
|
|
27479
|
-
|
|
27480
|
-
|
|
27481
|
-
|
|
27482
|
-
|
|
27483
|
-
collapsedSwimlanes: this.getCollapsedSwimlanes()
|
|
27484
|
-
};
|
|
28165
|
+
/** Whether a committed local move can be reverted. @returns {boolean} */
|
|
28166
|
+
canUndo() {
|
|
28167
|
+
return this._kanbanOptions().history !== false && this._kanbanHistory.canUndo();
|
|
28168
|
+
}
|
|
28169
|
+
/** Whether a reverted local move can be reapplied. @returns {boolean} */
|
|
28170
|
+
canRedo() {
|
|
28171
|
+
return this._kanbanOptions().history !== false && this._kanbanHistory.canRedo();
|
|
27485
28172
|
}
|
|
27486
28173
|
/**
|
|
27487
|
-
*
|
|
27488
|
-
* @param {Partial<KanbanViewState>|null} state Saved state.
|
|
27489
|
-
* @param {{silent?:boolean}} [options={}] Event behavior.
|
|
28174
|
+
* Reverts the last committed local move, restoring the exact record order and selection.
|
|
27490
28175
|
* @returns {this}
|
|
28176
|
+
* @fires RecordView#datachange
|
|
28177
|
+
* @fires KanbanView#historychange
|
|
27491
28178
|
*/
|
|
27492
|
-
|
|
27493
|
-
|
|
27494
|
-
|
|
27495
|
-
|
|
27496
|
-
|
|
27497
|
-
|
|
27498
|
-
|
|
27499
|
-
|
|
27500
|
-
|
|
27501
|
-
|
|
27502
|
-
);
|
|
27503
|
-
|
|
27504
|
-
|
|
27505
|
-
|
|
27506
|
-
|
|
27507
|
-
|
|
27508
|
-
|
|
27509
|
-
|
|
27510
|
-
|
|
27511
|
-
|
|
27512
|
-
|
|
27513
|
-
return this;
|
|
28179
|
+
undo() {
|
|
28180
|
+
return this._travelHistory("undo");
|
|
28181
|
+
}
|
|
28182
|
+
/**
|
|
28183
|
+
* Reapplies the last reverted local move.
|
|
28184
|
+
* @returns {this}
|
|
28185
|
+
* @fires RecordView#datachange
|
|
28186
|
+
* @fires KanbanView#historychange
|
|
28187
|
+
*/
|
|
28188
|
+
redo() {
|
|
28189
|
+
return this._travelHistory("redo");
|
|
28190
|
+
}
|
|
28191
|
+
/** Empties both history stacks. @returns {this} @fires KanbanView#historychange */
|
|
28192
|
+
clearHistory() {
|
|
28193
|
+
this._kanbanHistory.clear();
|
|
28194
|
+
this._syncHistoryControls();
|
|
28195
|
+
this._emitHistory();
|
|
28196
|
+
return this;
|
|
28197
|
+
}
|
|
28198
|
+
/** Returns the current board search text. @returns {string} */
|
|
28199
|
+
getSearch() {
|
|
28200
|
+
return this._kanbanSearch;
|
|
28201
|
+
}
|
|
28202
|
+
/**
|
|
28203
|
+
* Narrows visible cards to those matching a search. Hidden records keep their position in the
|
|
28204
|
+
* data, so a card dropped between two visible cards still lands between them.
|
|
28205
|
+
* @param {string} search Search text.
|
|
28206
|
+
* @param {{silent?:boolean}} [options={}] Event behavior.
|
|
28207
|
+
* @returns {this}
|
|
28208
|
+
* @fires KanbanView#searchchange
|
|
28209
|
+
*/
|
|
28210
|
+
setSearch(search, options = {}) {
|
|
28211
|
+
const next = search == null ? "" : String(search);
|
|
28212
|
+
if (next === this._kanbanSearch) return this;
|
|
28213
|
+
this._kanbanSearch = next;
|
|
28214
|
+
if (this._searchInput && this._searchInput.value !== next) this._searchInput.value = next;
|
|
28215
|
+
this._refreshView("search");
|
|
28216
|
+
if (!options.silent) this.emit("searchchange", { search: next });
|
|
28217
|
+
return this;
|
|
28218
|
+
}
|
|
28219
|
+
/**
|
|
28220
|
+
* Replaces the extra card predicate. Pass null to clear it.
|
|
28221
|
+
* @param {((record:KanbanRecord,index:number)=>boolean)|null} filter Predicate.
|
|
28222
|
+
* @returns {this}
|
|
28223
|
+
*/
|
|
28224
|
+
setFilter(filter) {
|
|
28225
|
+
if (filter != null && typeof filter !== "function") {
|
|
28226
|
+
throw new TypeError("Kanban filter must be a function or null");
|
|
28227
|
+
}
|
|
28228
|
+
this._kanbanFilter = filter ?? null;
|
|
28229
|
+
this._refreshView("filter");
|
|
28230
|
+
return this;
|
|
28231
|
+
}
|
|
28232
|
+
/** Returns the records currently rendered, in board order. @returns {KanbanRecord[]} */
|
|
28233
|
+
getVisibleRecords() {
|
|
28234
|
+
return this._viewData.filter((record) => this._visibleIds.has(this._viewRecordId(record)));
|
|
28235
|
+
}
|
|
28236
|
+
/**
|
|
28237
|
+
* Replaces every record. History is dropped because its snapshots describe records the host has
|
|
28238
|
+
* just replaced, and reinstating them would resurrect data the application no longer owns.
|
|
28239
|
+
* @param {KanbanRecord[]} records New records.
|
|
28240
|
+
* @returns {this}
|
|
28241
|
+
*/
|
|
28242
|
+
setData(records) {
|
|
28243
|
+
this._resetHistory();
|
|
28244
|
+
return super.setData(records);
|
|
28245
|
+
}
|
|
28246
|
+
/** @param {KanbanRecord[]} records Records to append. @returns {this} */
|
|
28247
|
+
addData(records) {
|
|
28248
|
+
this._resetHistory();
|
|
28249
|
+
return super.addData(records);
|
|
28250
|
+
}
|
|
28251
|
+
/** @param {unknown} id Existing record id. @param {KanbanRecord} record Replacement. @returns {this} */
|
|
28252
|
+
updateRecord(id, record) {
|
|
28253
|
+
this._resetHistory();
|
|
28254
|
+
return super.updateRecord(id, record);
|
|
28255
|
+
}
|
|
28256
|
+
/** @param {unknown} id Record id. @returns {this} */
|
|
28257
|
+
removeRecord(id) {
|
|
28258
|
+
this._resetHistory();
|
|
28259
|
+
return super.removeRecord(id);
|
|
28260
|
+
}
|
|
28261
|
+
/** Returns common plus board-specific serializable configuration. @returns {KanbanViewState} */
|
|
28262
|
+
getViewState() {
|
|
28263
|
+
this.getColumnOrder();
|
|
28264
|
+
this.getSwimlaneOrder();
|
|
28265
|
+
const derivedColumns = this._kanbanColumns === null;
|
|
28266
|
+
const derivedSwimlanes = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
28267
|
+
return {
|
|
28268
|
+
...super.getViewState(),
|
|
28269
|
+
columnOrder: derivedColumns ? [...this._kanbanColumnOrder] : this.getColumnOrder(),
|
|
28270
|
+
swimlaneOrder: derivedSwimlanes ? [...this._kanbanSwimlaneOrder] : this.getSwimlaneOrder(),
|
|
28271
|
+
collapsedColumns: derivedColumns ? [...this._collapsedColumns] : this.getCollapsedColumns(),
|
|
28272
|
+
collapsedSwimlanes: derivedSwimlanes ? [...this._collapsedSwimlanes] : this.getCollapsedSwimlanes()
|
|
28273
|
+
};
|
|
28274
|
+
}
|
|
28275
|
+
/**
|
|
28276
|
+
* Restores common and Kanban configuration. State from another record view is accepted safely.
|
|
28277
|
+
* @param {Partial<KanbanViewState>|null} state Saved state.
|
|
28278
|
+
* @param {{silent?:boolean}} [options={}] Event behavior.
|
|
28279
|
+
* @returns {this}
|
|
28280
|
+
*/
|
|
28281
|
+
setViewState(state, options = {}) {
|
|
28282
|
+
const source = state && typeof state === "object" ? state : {};
|
|
28283
|
+
super.setViewState(source, { silent: true });
|
|
28284
|
+
const columns = this._rawColumns();
|
|
28285
|
+
const lanes = this._rawSwimlanes();
|
|
28286
|
+
const derivedColumns = this._kanbanColumns === null;
|
|
28287
|
+
const derivedSwimlanes = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
28288
|
+
this._kanbanColumnOrder = reconcileKanbanPreference(
|
|
28289
|
+
columns,
|
|
28290
|
+
Array.isArray(source.columnOrder) ? source.columnOrder : this._kanbanColumnOrder,
|
|
28291
|
+
derivedColumns
|
|
28292
|
+
);
|
|
28293
|
+
this._kanbanSwimlaneOrder = reconcileKanbanPreference(
|
|
28294
|
+
lanes,
|
|
28295
|
+
Array.isArray(source.swimlaneOrder) ? source.swimlaneOrder : this._kanbanSwimlaneOrder,
|
|
28296
|
+
derivedSwimlanes
|
|
28297
|
+
);
|
|
28298
|
+
const columnIds = new Set(columns.map((column) => column.id));
|
|
28299
|
+
const laneIds = new Set(lanes.map((lane) => lane.id));
|
|
28300
|
+
if (Array.isArray(source.collapsedColumns)) {
|
|
28301
|
+
const collapsed = normalizeOrder(source.collapsedColumns);
|
|
28302
|
+
this._collapsedColumns = new Set(derivedColumns ? collapsed : collapsed.filter((id) => columnIds.has(id)));
|
|
28303
|
+
}
|
|
28304
|
+
if (Array.isArray(source.collapsedSwimlanes)) {
|
|
28305
|
+
const collapsed = normalizeOrder(source.collapsedSwimlanes);
|
|
28306
|
+
this._collapsedSwimlanes = new Set(derivedSwimlanes ? collapsed : collapsed.filter((id) => laneIds.has(id)));
|
|
28307
|
+
}
|
|
28308
|
+
this._refreshView("state");
|
|
28309
|
+
if (!options.silent) this._emitKanbanState("restore");
|
|
28310
|
+
return this;
|
|
27514
28311
|
}
|
|
27515
28312
|
/** Restores enhanced targets after base lifecycle cleanup. @returns {void} */
|
|
27516
28313
|
destroy() {
|
|
27517
28314
|
if (this._kanbanDestroyed) return;
|
|
27518
28315
|
this._kanbanDestroyed = true;
|
|
28316
|
+
this._clearDrag();
|
|
28317
|
+
clearTimeout(this._pointerHold);
|
|
28318
|
+
clearTimeout(this._searchTimer);
|
|
28319
|
+
this._kanbanMenu?.destroy();
|
|
28320
|
+
this._kanbanMenu = null;
|
|
27519
28321
|
const root = this.el;
|
|
27520
28322
|
const snapshot10 = this._kanbanSnapshot;
|
|
27521
28323
|
super.destroy();
|
|
@@ -27530,25 +28332,61 @@ var KanbanView = class extends RecordView {
|
|
|
27530
28332
|
}
|
|
27531
28333
|
if (_reason === "loading") return;
|
|
27532
28334
|
const options = this._kanbanOptions();
|
|
28335
|
+
const activeElement = document.activeElement;
|
|
27533
28336
|
const activeMeta = this._metaForTarget(document.activeElement);
|
|
27534
28337
|
const restoreHandle = Boolean(document.activeElement?.classList?.contains("zx-kanban-view__move-handle"));
|
|
28338
|
+
const activeCollapse = activeElement instanceof Element ? activeElement.closest("[data-kanban-collapse]") : null;
|
|
28339
|
+
const activeSection = activeCollapse?.closest(".zx-kanban-view__column");
|
|
28340
|
+
const activeCollapseFocus = activeCollapse && this._board.contains(activeCollapse) ? {
|
|
28341
|
+
axis: (
|
|
28342
|
+
/** @type {'column'|'swimlane'} */
|
|
28343
|
+
/** @type {HTMLElement} */
|
|
28344
|
+
activeCollapse.dataset.kanbanCollapse
|
|
28345
|
+
),
|
|
28346
|
+
id: String(
|
|
28347
|
+
/** @type {HTMLElement} */
|
|
28348
|
+
activeCollapse.dataset.kanbanId
|
|
28349
|
+
),
|
|
28350
|
+
lane: activeSection ? this._sectionMeta.get(activeSection)?.lane ?? null : null
|
|
28351
|
+
} : null;
|
|
27535
28352
|
const columns = this._resolvedColumns();
|
|
27536
28353
|
const lanes = this._resolvedSwimlanes();
|
|
27537
28354
|
this._cardMeta = /* @__PURE__ */ new WeakMap();
|
|
27538
28355
|
this._sectionMeta = /* @__PURE__ */ new WeakMap();
|
|
27539
28356
|
this._cardsById = /* @__PURE__ */ new Map();
|
|
28357
|
+
this._resolveVisibility();
|
|
27540
28358
|
const content = [];
|
|
27541
28359
|
if (options.swimlaneBy == null) {
|
|
27542
28360
|
content.push(this._renderColumns(columns, null, false));
|
|
27543
28361
|
} else {
|
|
27544
28362
|
lanes.forEach((lane) => content.push(this._renderLane(lane, columns)));
|
|
27545
28363
|
}
|
|
27546
|
-
|
|
27547
|
-
if (
|
|
28364
|
+
if (!this._viewData.length) content.push(this._emptyContent(this._text("empty")));
|
|
28365
|
+
else if (!this._visibleIds.size) content.push(this._emptyContent(this._text("noMatches")));
|
|
27548
28366
|
this._board.replaceChildren(...content);
|
|
27549
|
-
if (this._keyboardMove
|
|
28367
|
+
if (this._keyboardMove) {
|
|
28368
|
+
this._keyboardMove.ids = this._keyboardMove.ids.filter((id) => this.getRecord(id));
|
|
28369
|
+
if (!this._keyboardMove.ids.length) this._keyboardMove = null;
|
|
28370
|
+
}
|
|
27550
28371
|
this._syncKeyboardTarget();
|
|
27551
|
-
if (activeMeta) queueMicrotask(() => {
|
|
28372
|
+
if (activeCollapseFocus || activeMeta) queueMicrotask(() => {
|
|
28373
|
+
if (activeCollapseFocus) {
|
|
28374
|
+
this._collapseControl(
|
|
28375
|
+
activeCollapseFocus.axis,
|
|
28376
|
+
activeCollapseFocus.id,
|
|
28377
|
+
activeCollapseFocus.lane
|
|
28378
|
+
)?.focus();
|
|
28379
|
+
return;
|
|
28380
|
+
}
|
|
28381
|
+
if (!activeMeta) return;
|
|
28382
|
+
if (activeMeta.lane != null && this._collapsedSwimlanes.has(activeMeta.lane)) {
|
|
28383
|
+
this._collapseControl("swimlane", activeMeta.lane)?.focus();
|
|
28384
|
+
return;
|
|
28385
|
+
}
|
|
28386
|
+
if (this._collapsedColumns.has(activeMeta.column)) {
|
|
28387
|
+
this._collapseControl("column", activeMeta.column, activeMeta.lane)?.focus();
|
|
28388
|
+
return;
|
|
28389
|
+
}
|
|
27552
28390
|
const card = this._cardsById.get(activeMeta.id);
|
|
27553
28391
|
const target = restoreHandle ? card?.querySelector(".zx-kanban-view__move-handle") : card;
|
|
27554
28392
|
if (target?.isConnected) target.focus();
|
|
@@ -27560,21 +28398,36 @@ var KanbanView = class extends RecordView {
|
|
|
27560
28398
|
const collapsed = this._collapsedSwimlanes.has(lane.id);
|
|
27561
28399
|
const headingId = uid("zx-kanban-lane");
|
|
27562
28400
|
const bodyId = uid("zx-kanban-lane-body");
|
|
27563
|
-
const
|
|
28401
|
+
const entries = this._viewData.filter((record, index) => normalizeAxisId(this._readAxis(options.swimlaneBy, record, index)) === lane.id);
|
|
28402
|
+
const total = entries.length;
|
|
28403
|
+
const count2 = entries.filter((record) => this._visibleIds.has(this._viewRecordId(record))).length;
|
|
27564
28404
|
const toggle = h("button", {
|
|
27565
28405
|
class: "zx-kanban-view__collapse",
|
|
27566
28406
|
type: "button",
|
|
27567
28407
|
ariaExpanded: String(!collapsed),
|
|
27568
28408
|
ariaControls: bodyId,
|
|
27569
|
-
ariaLabel:
|
|
28409
|
+
ariaLabel: this._text(collapsed ? "expandLane" : "collapseLane", { label: lane.label }),
|
|
27570
28410
|
dataset: { kanbanCollapse: "swimlane", kanbanId: lane.id }
|
|
27571
28411
|
}, icon(collapsed ? "chevron-right" : "chevron-down", { size: 13 }));
|
|
28412
|
+
const limit = lane.limit ?? null;
|
|
28413
|
+
const wip = wipState(total, limit);
|
|
28414
|
+
const custom = this._renderHook("renderSwimlaneHeader", {
|
|
28415
|
+
swimlane: { ...lane },
|
|
28416
|
+
count: count2,
|
|
28417
|
+
total,
|
|
28418
|
+
columnTotal: total,
|
|
28419
|
+
limit,
|
|
28420
|
+
wip,
|
|
28421
|
+
collapsed
|
|
28422
|
+
});
|
|
27572
28423
|
const heading = h(
|
|
27573
28424
|
"h2",
|
|
27574
28425
|
{ class: "zx-kanban-view__lane-heading", id: headingId },
|
|
27575
28426
|
toggle,
|
|
27576
|
-
|
|
27577
|
-
|
|
28427
|
+
custom ?? [
|
|
28428
|
+
h("span", {}, lane.label),
|
|
28429
|
+
options.showCounts || limit != null ? this._countIndicator(count2, total, limit) : null
|
|
28430
|
+
]
|
|
27578
28431
|
);
|
|
27579
28432
|
const body = this._renderColumns(columns, lane, true);
|
|
27580
28433
|
body.id = bodyId;
|
|
@@ -27582,7 +28435,7 @@ var KanbanView = class extends RecordView {
|
|
|
27582
28435
|
return h("section", {
|
|
27583
28436
|
class: "zx-kanban-view__lane",
|
|
27584
28437
|
ariaLabelledby: headingId,
|
|
27585
|
-
dataset: { collapsed: String(collapsed) }
|
|
28438
|
+
dataset: { collapsed: String(collapsed), wip }
|
|
27586
28439
|
}, heading, body);
|
|
27587
28440
|
}
|
|
27588
28441
|
/** @param {KanbanColumn[]} columns @param {KanbanSwimlane|null} lane @param {boolean} hasLanes @returns {HTMLElement} */
|
|
@@ -27608,36 +28461,67 @@ var KanbanView = class extends RecordView {
|
|
|
27608
28461
|
const collapsed = this._collapsedColumns.has(column.id);
|
|
27609
28462
|
const headingId = uid("zx-kanban-column");
|
|
27610
28463
|
const listId = uid("zx-kanban-column-list");
|
|
27611
|
-
const
|
|
27612
|
-
const
|
|
28464
|
+
const columnEntries = this._viewData.filter((record, index) => normalizeAxisId(this._readAxis(options.columnBy, record, index)) === column.id);
|
|
28465
|
+
const columnTotal = columnEntries.length;
|
|
28466
|
+
const visible = entries.filter((entry) => this._visibleIds.has(this._viewRecordId(entry.record)));
|
|
28467
|
+
const laneLimit = lane ? readColumnLaneLimit(column, lane.id) : null;
|
|
28468
|
+
const limit = laneLimit ?? column.limit ?? null;
|
|
28469
|
+
const wip = wipState(laneLimit == null ? columnTotal : entries.length, limit);
|
|
27613
28470
|
const toggle = h("button", {
|
|
27614
28471
|
class: "zx-kanban-view__collapse",
|
|
27615
28472
|
type: "button",
|
|
27616
28473
|
ariaExpanded: String(!collapsed),
|
|
27617
28474
|
ariaControls: listId,
|
|
27618
|
-
ariaLabel:
|
|
28475
|
+
ariaLabel: this._text(collapsed ? "expandColumn" : "collapseColumn", { label: column.label }),
|
|
27619
28476
|
dataset: { kanbanCollapse: "column", kanbanId: column.id }
|
|
27620
28477
|
}, icon(collapsed ? "chevron-right" : "chevron-down", { size: 13 }));
|
|
27621
|
-
const
|
|
27622
|
-
|
|
27623
|
-
|
|
27624
|
-
|
|
28478
|
+
const custom = this._renderHook("renderColumnHeader", {
|
|
28479
|
+
column: { ...column },
|
|
28480
|
+
swimlane: lane ? { ...lane } : null,
|
|
28481
|
+
count: visible.length,
|
|
28482
|
+
total: entries.length,
|
|
28483
|
+
columnTotal,
|
|
28484
|
+
limit,
|
|
28485
|
+
wip,
|
|
28486
|
+
collapsed
|
|
28487
|
+
});
|
|
28488
|
+
const indicator = options.showCounts || limit != null ? this._columnIndicator(visible.length, entries.length, columnTotal, limit, hasLanes && laneLimit == null) : null;
|
|
27625
28489
|
const heading = h(hasLanes ? "h3" : "h2", {
|
|
27626
28490
|
class: "zx-kanban-view__column-heading",
|
|
27627
28491
|
id: headingId
|
|
27628
|
-
}, toggle, h("span", { class: "zx-kanban-view__column-label" }, column.label), indicator);
|
|
28492
|
+
}, toggle, custom ?? [h("span", { class: "zx-kanban-view__column-label" }, column.label), indicator]);
|
|
27629
28493
|
const list = h("ul", {
|
|
27630
28494
|
class: "zx-kanban-view__cards",
|
|
27631
28495
|
id: listId,
|
|
27632
28496
|
ariaLabel: `${column.label}${lane ? `, ${lane.label}` : ""}`
|
|
27633
28497
|
});
|
|
27634
|
-
|
|
28498
|
+
visible.forEach((entry) => list.append(this._renderCard(entry.record, entry.index, column.id, lane?.id ?? null, hasLanes)));
|
|
28499
|
+
if (!visible.length) {
|
|
28500
|
+
const placeholder = this._renderHook("renderColumnEmpty", {
|
|
28501
|
+
column: { ...column },
|
|
28502
|
+
swimlane: lane ? { ...lane } : null,
|
|
28503
|
+
count: 0,
|
|
28504
|
+
total: entries.length,
|
|
28505
|
+
columnTotal,
|
|
28506
|
+
limit,
|
|
28507
|
+
wip,
|
|
28508
|
+
collapsed
|
|
28509
|
+
}) ?? this._placeholderText();
|
|
28510
|
+
if (placeholder) list.append(h("li", { class: "zx-kanban-view__column-empty", role: "presentation" }, placeholder));
|
|
28511
|
+
}
|
|
27635
28512
|
list.hidden = collapsed;
|
|
28513
|
+
const add = this._allowsAdd(column, lane) ? h("button", {
|
|
28514
|
+
class: "zx-kanban-view__add",
|
|
28515
|
+
type: "button",
|
|
28516
|
+
ariaLabel: this._text("addTo", { target: `${column.label}${lane ? `, ${lane.label}` : ""}` }),
|
|
28517
|
+
dataset: { kanbanAdd: column.id, kanbanLane: lane?.id ?? "" }
|
|
28518
|
+
}, icon("plus", { size: 13 }), h("span", {}, this._text("add"))) : null;
|
|
28519
|
+
if (add) add.hidden = collapsed;
|
|
27636
28520
|
const section = h("section", {
|
|
27637
28521
|
class: "zx-kanban-view__column",
|
|
27638
28522
|
ariaLabelledby: headingId,
|
|
27639
28523
|
dataset: { column: column.id, collapsed: String(collapsed), wip }
|
|
27640
|
-
}, heading, list);
|
|
28524
|
+
}, heading, list, add);
|
|
27641
28525
|
this._sectionMeta.set(section, { column: column.id, lane: lane?.id ?? null, list });
|
|
27642
28526
|
return section;
|
|
27643
28527
|
}
|
|
@@ -27645,13 +28529,14 @@ var KanbanView = class extends RecordView {
|
|
|
27645
28529
|
_renderCard(record, index, column, lane, hasLanes) {
|
|
27646
28530
|
const options = this._kanbanOptions();
|
|
27647
28531
|
const id = this._viewRecordId(record);
|
|
27648
|
-
const grabbed = Boolean(this._keyboardMove
|
|
28532
|
+
const grabbed = Boolean(this._keyboardMove?.ids.some((candidate) => Object.is(candidate, id)));
|
|
28533
|
+
const rules = resolveKanbanRules(this._kanbanRules, record, { index, column, lane });
|
|
28534
|
+
const multiple = grabbed && this._keyboardMove ? this._keyboardMove.ids.length : 1;
|
|
27649
28535
|
const handle = h("button", {
|
|
27650
28536
|
class: "zx-kanban-view__move-handle",
|
|
27651
28537
|
type: "button",
|
|
27652
|
-
draggable: true,
|
|
27653
28538
|
ariaPressed: String(grabbed),
|
|
27654
|
-
ariaLabel:
|
|
28539
|
+
ariaLabel: multiple > 1 ? this._text("moveMultiple", { count: multiple }) : this._text("move", { name: this._recordName(record, index) })
|
|
27655
28540
|
}, icon("drag", { size: 13 }));
|
|
27656
28541
|
const card = createRecordCard(record, index, {
|
|
27657
28542
|
fields: this.getFields(),
|
|
@@ -27670,15 +28555,81 @@ var KanbanView = class extends RecordView {
|
|
|
27670
28555
|
});
|
|
27671
28556
|
card.tabIndex = 0;
|
|
27672
28557
|
if (grabbed) card.dataset.grabbed = "true";
|
|
28558
|
+
this._applyCardContent(card, { record, id, index, column, lane, rules, selected: this._isViewSelected(id) });
|
|
28559
|
+
this._applyCardRules(card, rules);
|
|
28560
|
+
this._applyCardDescription(card, id);
|
|
27673
28561
|
const meta = { record, id, index, column, lane };
|
|
27674
28562
|
this._cardMeta.set(card, meta);
|
|
27675
28563
|
this._cardsById.set(id, card);
|
|
27676
28564
|
return card;
|
|
27677
28565
|
}
|
|
27678
|
-
/**
|
|
27679
|
-
|
|
28566
|
+
/**
|
|
28567
|
+
* Hands card content to `renderCard` while keeping the managed shell: the list item, its
|
|
28568
|
+
* selection control, the action group, and the move handle are never given away, because they
|
|
28569
|
+
* carry the drop, focus, and movement semantics the board is responsible for.
|
|
28570
|
+
* @param {HTMLElement} card Card built from the shared anatomy.
|
|
28571
|
+
* @param {KanbanCardContext} context Card context.
|
|
28572
|
+
* @returns {void}
|
|
28573
|
+
*/
|
|
28574
|
+
_applyCardContent(card, context) {
|
|
28575
|
+
const custom = this._renderHook("renderCard", context);
|
|
28576
|
+
if (!custom) return;
|
|
28577
|
+
const body = card.querySelector(".zx-record-card__body");
|
|
28578
|
+
const titles = card.querySelector(".zx-record-card__titles");
|
|
28579
|
+
if (!body || !titles) return;
|
|
28580
|
+
titles.replaceChildren(custom);
|
|
28581
|
+
body.querySelector(".zx-record-card__eyebrow")?.remove();
|
|
28582
|
+
body.querySelector(".zx-record-card__metadata")?.remove();
|
|
28583
|
+
card.removeAttribute("aria-labelledby");
|
|
28584
|
+
card.setAttribute("aria-label", this._recordName(context.record, context.index));
|
|
28585
|
+
card.dataset.customCard = "true";
|
|
28586
|
+
}
|
|
28587
|
+
/**
|
|
28588
|
+
* Applies ordered card rules. The first match owns the marker tone; every match with a label adds
|
|
28589
|
+
* a badge, and rule descriptions join the accessible description so the marker is never the only
|
|
28590
|
+
* carrier of the meaning.
|
|
28591
|
+
* @param {HTMLElement} card Card element.
|
|
28592
|
+
* @param {KanbanRule[]} rules Matching rules.
|
|
28593
|
+
* @returns {void}
|
|
28594
|
+
*/
|
|
28595
|
+
_applyCardRules(card, rules) {
|
|
28596
|
+
if (!rules.length) return;
|
|
28597
|
+
card.dataset.rule = rules.map((rule) => rule.id).join(" ");
|
|
28598
|
+
card.dataset.ruleTone = rules[0].tone;
|
|
28599
|
+
const badges = rules.filter((rule) => rule.label);
|
|
28600
|
+
if (badges.length) {
|
|
28601
|
+
const list = h("p", { class: "zx-kanban-view__rules" }, badges.map((rule) => badge({
|
|
28602
|
+
label: rule.label,
|
|
28603
|
+
icon: rule.icon ?? void 0,
|
|
28604
|
+
kind: rule.tone,
|
|
28605
|
+
size: "sm",
|
|
28606
|
+
variant: "soft"
|
|
28607
|
+
})));
|
|
28608
|
+
card.querySelector(".zx-record-card__body")?.append(list);
|
|
28609
|
+
}
|
|
28610
|
+
const descriptions = rules.map((rule) => rule.description).filter(Boolean).join(". ");
|
|
28611
|
+
if (descriptions) card.dataset.ruleDescription = descriptions;
|
|
28612
|
+
}
|
|
28613
|
+
/**
|
|
28614
|
+
* Composes one card's accessible description from its selection state and its rules, so that
|
|
28615
|
+
* refreshing selection cannot quietly drop what a rule contributed.
|
|
28616
|
+
* @param {HTMLElement} card Card element.
|
|
28617
|
+
* @param {unknown} id Record id.
|
|
28618
|
+
* @returns {void}
|
|
28619
|
+
*/
|
|
28620
|
+
_applyCardDescription(card, id) {
|
|
28621
|
+
const parts = [];
|
|
28622
|
+
if (this.options.selectable) {
|
|
28623
|
+
parts.push(this._text(this._isViewSelected(id) ? "selected" : "notSelected"));
|
|
28624
|
+
}
|
|
28625
|
+
if (card.dataset.ruleDescription) parts.push(card.dataset.ruleDescription);
|
|
28626
|
+
if (parts.length) card.setAttribute("aria-description", parts.join(". "));
|
|
28627
|
+
else card.removeAttribute("aria-description");
|
|
28628
|
+
}
|
|
28629
|
+
/** @param {string} text Board-level empty message. @returns {HTMLElement} */
|
|
28630
|
+
_emptyContent(text4) {
|
|
27680
28631
|
const supplied = typeof this.options.emptyText === "function" ? this.options.emptyText() : this.options.emptyText;
|
|
27681
|
-
const content = supplied ??
|
|
28632
|
+
const content = supplied ?? text4;
|
|
27682
28633
|
return h(
|
|
27683
28634
|
"div",
|
|
27684
28635
|
{ class: "zx-kanban-view__empty", role: "status" },
|
|
@@ -27688,6 +28639,37 @@ var KanbanView = class extends RecordView {
|
|
|
27688
28639
|
) : String(content)
|
|
27689
28640
|
);
|
|
27690
28641
|
}
|
|
28642
|
+
/** @returns {string} Per-column placeholder text, or an empty string for none. */
|
|
28643
|
+
_placeholderText() {
|
|
28644
|
+
return this._text("columnEmpty");
|
|
28645
|
+
}
|
|
28646
|
+
/**
|
|
28647
|
+
* @param {number} count Visible records.
|
|
28648
|
+
* @param {number} total Records before search and filters.
|
|
28649
|
+
* @param {number|null} limit Applicable limit.
|
|
28650
|
+
* @returns {HTMLElement}
|
|
28651
|
+
*/
|
|
28652
|
+
_countIndicator(count2, total, limit) {
|
|
28653
|
+
const filtered = count2 !== total;
|
|
28654
|
+
return h("span", {
|
|
28655
|
+
class: "zx-kanban-view__count",
|
|
28656
|
+
ariaLabel: limit != null ? this._text("countLimit", { count: total, limit }) : filtered ? this._text("countFiltered", { count: count2, total }) : this._text("count", { count: count2 })
|
|
28657
|
+
}, limit != null ? `${total} / ${limit}` : filtered ? `${count2} / ${total}` : String(count2));
|
|
28658
|
+
}
|
|
28659
|
+
/**
|
|
28660
|
+
* @param {number} count Visible records in this column and lane.
|
|
28661
|
+
* @param {number} total Records in this column and lane before search and filters.
|
|
28662
|
+
* @param {number} columnTotal Records across the whole column.
|
|
28663
|
+
* @param {number|null} limit Applicable limit.
|
|
28664
|
+
* @param {boolean} columnScoped Whether the limit counts the column rather than this lane.
|
|
28665
|
+
* @returns {HTMLElement}
|
|
28666
|
+
*/
|
|
28667
|
+
_columnIndicator(count2, total, columnTotal, limit, columnScoped) {
|
|
28668
|
+
const filtered = count2 !== total;
|
|
28669
|
+
const label = limit == null ? filtered ? this._text("countFiltered", { count: count2, total }) : this._text("count", { count: count2 }) : columnScoped ? this._text("countLimitInLane", { count: total, total: columnTotal, limit }) : this._text("countLimit", { count: total, limit });
|
|
28670
|
+
const text4 = limit == null ? filtered ? `${count2} / ${total}` : String(count2) : columnScoped ? `${total} \xB7 ${columnTotal} / ${limit}` : `${total} / ${limit}`;
|
|
28671
|
+
return h("span", { class: "zx-kanban-view__count", ariaLabel: label }, text4);
|
|
28672
|
+
}
|
|
27691
28673
|
/** @returns {KanbanColumn[]} */
|
|
27692
28674
|
_rawColumns() {
|
|
27693
28675
|
const options = this._kanbanOptions();
|
|
@@ -27696,7 +28678,11 @@ var KanbanView = class extends RecordView {
|
|
|
27696
28678
|
/** @returns {KanbanColumn[]} */
|
|
27697
28679
|
_resolvedColumns() {
|
|
27698
28680
|
const columns = this._rawColumns();
|
|
27699
|
-
this._kanbanColumnOrder =
|
|
28681
|
+
this._kanbanColumnOrder = reconcileKanbanPreference(
|
|
28682
|
+
columns,
|
|
28683
|
+
this._kanbanColumnOrder,
|
|
28684
|
+
this._kanbanColumns === null
|
|
28685
|
+
);
|
|
27700
28686
|
return orderDescriptors(columns, this._kanbanColumnOrder);
|
|
27701
28687
|
}
|
|
27702
28688
|
/** @returns {KanbanSwimlane[]} */
|
|
@@ -27708,7 +28694,12 @@ var KanbanView = class extends RecordView {
|
|
|
27708
28694
|
/** @returns {KanbanSwimlane[]} */
|
|
27709
28695
|
_resolvedSwimlanes() {
|
|
27710
28696
|
const lanes = this._rawSwimlanes();
|
|
27711
|
-
this.
|
|
28697
|
+
const preserveUnknown = this._kanbanSwimlanes === null && this._kanbanOptions().swimlaneBy != null;
|
|
28698
|
+
this._kanbanSwimlaneOrder = reconcileKanbanPreference(
|
|
28699
|
+
lanes,
|
|
28700
|
+
this._kanbanSwimlaneOrder,
|
|
28701
|
+
preserveUnknown
|
|
28702
|
+
);
|
|
27712
28703
|
return orderDescriptors(lanes, this._kanbanSwimlaneOrder);
|
|
27713
28704
|
}
|
|
27714
28705
|
/** @param {KanbanAccessor} accessor @param {KanbanRecord} record @param {number} index @returns {unknown} */
|
|
@@ -27732,13 +28723,84 @@ var KanbanView = class extends RecordView {
|
|
|
27732
28723
|
/** @param {unknown} id @returns {KanbanMovePoint|null} */
|
|
27733
28724
|
_recordLocation(id) {
|
|
27734
28725
|
const options = this._kanbanOptions();
|
|
27735
|
-
const index = this._viewData.findIndex((
|
|
28726
|
+
const index = this._viewData.findIndex((record) => Object.is(this._viewRecordId(record), id));
|
|
27736
28727
|
if (index < 0) return null;
|
|
27737
|
-
|
|
27738
|
-
|
|
27739
|
-
|
|
27740
|
-
|
|
27741
|
-
|
|
28728
|
+
return locateKanbanRecord(this._viewData, index, {
|
|
28729
|
+
recordId: options.recordId,
|
|
28730
|
+
columnBy: (record, recordIndex) => this._readAxis(options.columnBy, record, recordIndex),
|
|
28731
|
+
swimlaneBy: options.swimlaneBy == null ? null : (record, recordIndex) => this._readAxis(options.swimlaneBy, record, recordIndex)
|
|
28732
|
+
});
|
|
28733
|
+
}
|
|
28734
|
+
/**
|
|
28735
|
+
* Recomputes which records survive the search and the filter predicate.
|
|
28736
|
+
* @returns {void}
|
|
28737
|
+
*/
|
|
28738
|
+
_resolveVisibility() {
|
|
28739
|
+
const terms = kanbanSearchTerms(this._kanbanSearch);
|
|
28740
|
+
const filter = this._kanbanFilter;
|
|
28741
|
+
const sources = terms.length ? this._searchSources() : null;
|
|
28742
|
+
this._visibleIds = /* @__PURE__ */ new Set();
|
|
28743
|
+
this._viewData.forEach((record, index) => {
|
|
28744
|
+
if (filter && filter(record, index) !== true) return;
|
|
28745
|
+
if (sources && !matchesKanbanSearch(this._searchableText(record, index, sources), terms)) return;
|
|
28746
|
+
this._visibleIds.add(this._viewRecordId(record));
|
|
28747
|
+
});
|
|
28748
|
+
}
|
|
28749
|
+
/**
|
|
28750
|
+
* Resolves what a search looks at, once. `searchFields` names them explicitly; otherwise the
|
|
28751
|
+
* visible fields are searched, plus the title and subtitle sources — a card is findable by the
|
|
28752
|
+
* name it shows even when that field is hidden from the metadata list.
|
|
28753
|
+
* @returns {{fields:import('../view/record-view.js').ViewField[],readers:((record:KanbanRecord,index:number)=>unknown)[]}}
|
|
28754
|
+
*/
|
|
28755
|
+
_searchSources() {
|
|
28756
|
+
const options = this._kanbanOptions();
|
|
28757
|
+
const configured = Array.isArray(options.searchFields) ? options.searchFields.map(String) : null;
|
|
28758
|
+
const all = this.getFields();
|
|
28759
|
+
if (configured) return { fields: all.filter((field) => configured.includes(field.id)), readers: [] };
|
|
28760
|
+
const fields = this.getVisibleFields();
|
|
28761
|
+
const known = new Set(fields.map((field) => field.id));
|
|
28762
|
+
const readers = [];
|
|
28763
|
+
for (const source of [options.titleField, options.subtitleField]) {
|
|
28764
|
+
if (source == null) continue;
|
|
28765
|
+
if (typeof source === "function") {
|
|
28766
|
+
readers.push(source);
|
|
28767
|
+
continue;
|
|
28768
|
+
}
|
|
28769
|
+
if (known.has(source)) continue;
|
|
28770
|
+
const field = all.find((candidate) => candidate.id === source);
|
|
28771
|
+
if (field) fields.push(field);
|
|
28772
|
+
else readers.push((record) => record?.[source]);
|
|
28773
|
+
}
|
|
28774
|
+
return { fields, readers };
|
|
28775
|
+
}
|
|
28776
|
+
/**
|
|
28777
|
+
* Builds the text one record is searched through. Field `render` output is deliberately ignored:
|
|
28778
|
+
* it may be a DOM node, and matching what a formatter produced rather than what the record holds
|
|
28779
|
+
* would make the same query behave differently in two views over the same data.
|
|
28780
|
+
* @param {KanbanRecord} record Record.
|
|
28781
|
+
* @param {number} index Display index.
|
|
28782
|
+
* @param {{fields:import('../view/record-view.js').ViewField[],readers:((record:KanbanRecord,index:number)=>unknown)[]}} sources Resolved sources.
|
|
28783
|
+
* @returns {string} Searchable text.
|
|
28784
|
+
*/
|
|
28785
|
+
_searchableText(record, index, sources) {
|
|
28786
|
+
const parts = sources.fields.map((field) => stringifyValue(readViewField(field, record, index)));
|
|
28787
|
+
for (const reader of sources.readers) parts.push(stringifyValue(reader(record, index)));
|
|
28788
|
+
return parts.filter(Boolean).join(" ");
|
|
28789
|
+
}
|
|
28790
|
+
/**
|
|
28791
|
+
* Translates a position among rendered cards into a position among all records in one bucket.
|
|
28792
|
+
* @param {string} column Column id.
|
|
28793
|
+
* @param {string|null} lane Lane id.
|
|
28794
|
+
* @param {number} visibleIndex Position among visible cards.
|
|
28795
|
+
* @param {Set<unknown>} excluded Ids being moved, which the visible list already omits.
|
|
28796
|
+
* @returns {number} Absolute position.
|
|
28797
|
+
*/
|
|
28798
|
+
_absoluteIndex(column, lane, visibleIndex, excluded) {
|
|
28799
|
+
const entries = this._recordsIn(column, lane).filter((entry) => !excluded.has(this._viewRecordId(entry.record)));
|
|
28800
|
+
return absoluteKanbanIndex(
|
|
28801
|
+
entries.map((entry) => this._visibleIds.has(this._viewRecordId(entry.record))),
|
|
28802
|
+
visibleIndex
|
|
28803
|
+
);
|
|
27742
28804
|
}
|
|
27743
28805
|
/**
|
|
27744
28806
|
* @param {KanbanRecord} record @param {KanbanMovePoint} from @param {KanbanMovePoint} to
|
|
@@ -27760,8 +28822,327 @@ var KanbanView = class extends RecordView {
|
|
|
27760
28822
|
if (options.swimlaneBy != null && normalizeAxisId(this._readAxis(options.swimlaneBy, clone, currentIndex)) !== to.lane) return null;
|
|
27761
28823
|
return clone;
|
|
27762
28824
|
}
|
|
28825
|
+
/**
|
|
28826
|
+
* Runs transition, eligibility, and capacity policy for a whole move. Every distinct origin is
|
|
28827
|
+
* checked, so a multi-card move drawn from two columns cannot slip past a transition rule that
|
|
28828
|
+
* only the second origin violates.
|
|
28829
|
+
* @param {KanbanRecord[]} records Moving records.
|
|
28830
|
+
* @param {KanbanMovePoint[]} origins Their origins.
|
|
28831
|
+
* @param {KanbanColumn} column Destination column.
|
|
28832
|
+
* @param {KanbanSwimlane|null} lane Destination lane.
|
|
28833
|
+
* @param {KanbanMovePoint} to Destination point.
|
|
28834
|
+
* @param {KanbanMoveContext} context Predicate context.
|
|
28835
|
+
* @param {Set<unknown>} excluded Moving ids, excluded from destination counts.
|
|
28836
|
+
* @returns {KanbanMoveEvaluation} Decision.
|
|
28837
|
+
*/
|
|
28838
|
+
_evaluateMove(records, origins, column, lane, to, context, excluded) {
|
|
28839
|
+
const options = this._kanbanOptions();
|
|
28840
|
+
for (const origin of origins) {
|
|
28841
|
+
const blocked = allowsKanbanTransition(column, lane, origin);
|
|
28842
|
+
if (blocked) {
|
|
28843
|
+
return {
|
|
28844
|
+
allowed: false,
|
|
28845
|
+
reason: blocked,
|
|
28846
|
+
limit: null,
|
|
28847
|
+
count: 0,
|
|
28848
|
+
limitExceeded: false,
|
|
28849
|
+
limitReached: false,
|
|
28850
|
+
policy: column.wipPolicy ?? (options.wipPolicy === "block" ? "block" : "warn")
|
|
28851
|
+
};
|
|
28852
|
+
}
|
|
28853
|
+
}
|
|
28854
|
+
const excludes = (record) => !excluded.has(this._viewRecordId(record));
|
|
28855
|
+
const columnCount = this._viewData.filter((record, index) => normalizeAxisId(this._readAxis(options.columnBy, record, index)) === to.column && excludes(record)).length;
|
|
28856
|
+
const laneCount = options.swimlaneBy == null ? 0 : this._viewData.filter((record, index) => normalizeAxisId(this._readAxis(options.swimlaneBy, record, index)) === to.lane && excludes(record)).length;
|
|
28857
|
+
const cellCount = this._recordsIn(to.column, to.lane).filter((entry) => excludes(entry.record)).length;
|
|
28858
|
+
return evaluateKanbanMove({
|
|
28859
|
+
records,
|
|
28860
|
+
column,
|
|
28861
|
+
swimlane: lane,
|
|
28862
|
+
context,
|
|
28863
|
+
from: origins[0],
|
|
28864
|
+
columnCount,
|
|
28865
|
+
laneCount,
|
|
28866
|
+
cellCount,
|
|
28867
|
+
policy: options.wipPolicy === "block" ? "block" : "warn"
|
|
28868
|
+
});
|
|
28869
|
+
}
|
|
28870
|
+
/**
|
|
28871
|
+
* Announces and reports a refused move. Nothing is written and no `recordmove` is emitted, so a
|
|
28872
|
+
* listener can neither observe nor undo a move that never happened.
|
|
28873
|
+
* @param {KanbanRejectReason} reason Refusal reason.
|
|
28874
|
+
* @param {{ids:unknown[],records:KanbanRecord[],from:KanbanMovePoint|null,to:KanbanMovePoint|null,column:KanbanColumn|null,swimlane:KanbanSwimlane|null,evaluation?:KanbanMoveEvaluation}} detail Context.
|
|
28875
|
+
* @returns {void}
|
|
28876
|
+
* @fires KanbanView#movereject
|
|
28877
|
+
*/
|
|
28878
|
+
_rejectMove(reason, detail) {
|
|
28879
|
+
const target = detail.to ? this._targetName(detail.to) : "";
|
|
28880
|
+
const name = this._movingName(detail.records);
|
|
28881
|
+
const message = reason === "destination" ? this._text("rejectDestination") : reason === "grouping" ? this._text("rejectGrouping") : reason === "transition" || reason === "lane-transition" ? this._text("rejectTransition", { target, origin: detail.from ? this._targetName(detail.from) : "" }) : reason === "wip" || reason === "lane-wip" ? this._text("rejectWip", { target, limit: detail.evaluation?.limit ?? "" }) : this._text("rejectAccept", { target, name });
|
|
28882
|
+
this._announce(message);
|
|
28883
|
+
this.emit("movereject", {
|
|
28884
|
+
reason,
|
|
28885
|
+
ids: [...detail.ids],
|
|
28886
|
+
records: [...detail.records],
|
|
28887
|
+
record: detail.records[0] ?? null,
|
|
28888
|
+
id: detail.ids[0],
|
|
28889
|
+
from: detail.from ? { ...detail.from } : null,
|
|
28890
|
+
to: detail.to ? { ...detail.to } : null,
|
|
28891
|
+
column: detail.column ? { ...detail.column } : null,
|
|
28892
|
+
swimlane: detail.swimlane ? { ...detail.swimlane } : null,
|
|
28893
|
+
limit: detail.evaluation?.limit ?? null,
|
|
28894
|
+
count: detail.evaluation?.count ?? 0,
|
|
28895
|
+
message
|
|
28896
|
+
});
|
|
28897
|
+
}
|
|
28898
|
+
/**
|
|
28899
|
+
* Installs a committed local record order, remembering the previous one for undo.
|
|
28900
|
+
* @param {KanbanRecord[]} records Next record order.
|
|
28901
|
+
* @returns {void}
|
|
28902
|
+
*/
|
|
28903
|
+
_commitLocalRecords(records) {
|
|
28904
|
+
const options = this._kanbanOptions();
|
|
28905
|
+
if (options.history !== false) {
|
|
28906
|
+
this._kanbanHistory.push({ records: [...this._viewData], selection: this.getSelectionIds() });
|
|
28907
|
+
this._syncHistoryControls();
|
|
28908
|
+
}
|
|
28909
|
+
this._viewData = records;
|
|
28910
|
+
if (options.sortMode === "local" && this.getSort()) this.setSort(null);
|
|
28911
|
+
else this._refreshView("move");
|
|
28912
|
+
this.emit("datachange", { records: this.getData() });
|
|
28913
|
+
if (options.history !== false) this._emitHistory();
|
|
28914
|
+
}
|
|
28915
|
+
/**
|
|
28916
|
+
* @param {'undo'|'redo'} direction Travel direction.
|
|
28917
|
+
* @returns {this}
|
|
28918
|
+
*/
|
|
28919
|
+
_travelHistory(direction) {
|
|
28920
|
+
if (this._kanbanOptions().history === false) return this;
|
|
28921
|
+
const current = { records: [...this._viewData], selection: this.getSelectionIds() };
|
|
28922
|
+
const entry = direction === "undo" ? this._kanbanHistory.undo(current) : this._kanbanHistory.redo(current);
|
|
28923
|
+
if (!entry) {
|
|
28924
|
+
this._announce(this._text(direction === "undo" ? "nothingToUndo" : "nothingToRedo"));
|
|
28925
|
+
return this;
|
|
28926
|
+
}
|
|
28927
|
+
this._viewData = [...entry.records];
|
|
28928
|
+
this._refreshView(direction);
|
|
28929
|
+
this.setSelection(entry.selection);
|
|
28930
|
+
this._syncHistoryControls();
|
|
28931
|
+
this.emit("datachange", { records: this.getData() });
|
|
28932
|
+
this._emitHistory();
|
|
28933
|
+
this._announce(this._text(direction === "undo" ? "undone" : "redone"));
|
|
28934
|
+
return this;
|
|
28935
|
+
}
|
|
28936
|
+
/** Drops both stacks without announcing, for host-driven data replacement. @returns {void} */
|
|
28937
|
+
_resetHistory() {
|
|
28938
|
+
if (!this._kanbanHistory) return;
|
|
28939
|
+
const had = this._kanbanHistory.canUndo() || this._kanbanHistory.canRedo();
|
|
28940
|
+
this._kanbanHistory.clear();
|
|
28941
|
+
this._syncHistoryControls();
|
|
28942
|
+
if (had) this._emitHistory();
|
|
28943
|
+
}
|
|
28944
|
+
/** @returns {void} */
|
|
28945
|
+
_emitHistory() {
|
|
28946
|
+
this.emit("historychange", {
|
|
28947
|
+
canUndo: this.canUndo(),
|
|
28948
|
+
canRedo: this.canRedo(),
|
|
28949
|
+
depth: this._kanbanHistory.depth()
|
|
28950
|
+
});
|
|
28951
|
+
}
|
|
28952
|
+
/** @returns {void} */
|
|
28953
|
+
_syncHistoryControls() {
|
|
28954
|
+
if (this._undoButton) this._undoButton.disabled = !this.canUndo();
|
|
28955
|
+
if (this._redoButton) this._redoButton.disabled = !this.canRedo();
|
|
28956
|
+
}
|
|
28957
|
+
/** @returns {HTMLElement} Built-in search control. */
|
|
28958
|
+
_createSearchControl() {
|
|
28959
|
+
const input = (
|
|
28960
|
+
/** @type {HTMLInputElement} */
|
|
28961
|
+
h("input", {
|
|
28962
|
+
class: "zx-kanban-view__search-input",
|
|
28963
|
+
type: "search",
|
|
28964
|
+
value: this._kanbanSearch,
|
|
28965
|
+
ariaLabel: this._text("search"),
|
|
28966
|
+
placeholder: this._text("searchPlaceholder")
|
|
28967
|
+
})
|
|
28968
|
+
);
|
|
28969
|
+
this._searchInput = input;
|
|
28970
|
+
this.listen(input, "input", () => {
|
|
28971
|
+
clearTimeout(this._searchTimer);
|
|
28972
|
+
this._searchTimer = setTimeout(() => {
|
|
28973
|
+
this._searchTimer = 0;
|
|
28974
|
+
if (!this._kanbanDestroyed) this.setSearch(input.value);
|
|
28975
|
+
}, SEARCH_DEBOUNCE);
|
|
28976
|
+
});
|
|
28977
|
+
this.listen(input, "keydown", (event) => {
|
|
28978
|
+
if (
|
|
28979
|
+
/** @type {KeyboardEvent} */
|
|
28980
|
+
event.key !== "Escape" || !input.value
|
|
28981
|
+
) return;
|
|
28982
|
+
event.stopPropagation();
|
|
28983
|
+
clearTimeout(this._searchTimer);
|
|
28984
|
+
this._searchTimer = 0;
|
|
28985
|
+
this.setSearch("");
|
|
28986
|
+
});
|
|
28987
|
+
return h("div", { class: "zx-kanban-view__search" }, icon("search", { size: 13 }), input);
|
|
28988
|
+
}
|
|
28989
|
+
/** @returns {HTMLElement} Built-in history controls. */
|
|
28990
|
+
_createHistoryControls() {
|
|
28991
|
+
this._undoButton = /** @type {HTMLButtonElement} */
|
|
28992
|
+
h("button", {
|
|
28993
|
+
class: "zx-button zx-button--sm",
|
|
28994
|
+
type: "button",
|
|
28995
|
+
disabled: true,
|
|
28996
|
+
dataset: { kanbanHistory: "undo" }
|
|
28997
|
+
}, this._text("undo"));
|
|
28998
|
+
this._redoButton = /** @type {HTMLButtonElement} */
|
|
28999
|
+
h("button", {
|
|
29000
|
+
class: "zx-button zx-button--sm",
|
|
29001
|
+
type: "button",
|
|
29002
|
+
disabled: true,
|
|
29003
|
+
dataset: { kanbanHistory: "redo" }
|
|
29004
|
+
}, this._text("redo"));
|
|
29005
|
+
const group = h(
|
|
29006
|
+
"div",
|
|
29007
|
+
{ class: "zx-kanban-view__history", role: "group" },
|
|
29008
|
+
this._undoButton,
|
|
29009
|
+
this._redoButton
|
|
29010
|
+
);
|
|
29011
|
+
this.listen(group, "click", (event) => {
|
|
29012
|
+
const control = (
|
|
29013
|
+
/** @type {Element|null} */
|
|
29014
|
+
event.target?.closest?.("[data-kanban-history]")
|
|
29015
|
+
);
|
|
29016
|
+
if (!control) return;
|
|
29017
|
+
if (
|
|
29018
|
+
/** @type {HTMLElement} */
|
|
29019
|
+
control.dataset.kanbanHistory === "undo"
|
|
29020
|
+
) this.undo();
|
|
29021
|
+
else this.redo();
|
|
29022
|
+
});
|
|
29023
|
+
return group;
|
|
29024
|
+
}
|
|
29025
|
+
/** Attaches the card context menu. @returns {void} */
|
|
29026
|
+
_createContextMenu() {
|
|
29027
|
+
this._kanbanMenu = new ContextMenu(this._board, {
|
|
29028
|
+
selector: ".zx-record-card",
|
|
29029
|
+
items: (context) => this._menuItems(context),
|
|
29030
|
+
onselect: ({ detail }) => this._handleMenuSelect(detail)
|
|
29031
|
+
});
|
|
29032
|
+
}
|
|
29033
|
+
/**
|
|
29034
|
+
* Builds the menu for one card: every destination the same policy path would accept, plus any
|
|
29035
|
+
* application items. Destinations the board would refuse are shown disabled rather than hidden,
|
|
29036
|
+
* so a rule is discoverable instead of silently removing a command.
|
|
29037
|
+
* @param {Element|null} context Card element the menu opened on.
|
|
29038
|
+
* @returns {(MenuItem|'-')[]} Menu entries.
|
|
29039
|
+
*/
|
|
29040
|
+
_menuItems(context) {
|
|
29041
|
+
const meta = context ? this._cardMeta.get(context) : null;
|
|
29042
|
+
if (!meta) return [];
|
|
29043
|
+
const options = this._kanbanOptions();
|
|
29044
|
+
const ids = this._movingIds(meta.id);
|
|
29045
|
+
const items = [];
|
|
29046
|
+
for (const column of this._resolvedColumns()) {
|
|
29047
|
+
if (column.id === meta.column) continue;
|
|
29048
|
+
items.push({
|
|
29049
|
+
label: this._text("menuMoveTo", { target: column.label }),
|
|
29050
|
+
value: { kind: "column", id: column.id, ids },
|
|
29051
|
+
disabled: !this._canMove(ids, { column: column.id })
|
|
29052
|
+
});
|
|
29053
|
+
}
|
|
29054
|
+
if (options.swimlaneBy != null) {
|
|
29055
|
+
for (const lane of this._resolvedSwimlanes()) {
|
|
29056
|
+
if (lane.id === meta.lane) continue;
|
|
29057
|
+
items.push({
|
|
29058
|
+
label: this._text("menuMoveToLane", { target: lane.label }),
|
|
29059
|
+
value: { kind: "lane", id: lane.id, ids },
|
|
29060
|
+
disabled: !this._canMove(ids, { lane: lane.id })
|
|
29061
|
+
});
|
|
29062
|
+
}
|
|
29063
|
+
}
|
|
29064
|
+
const extra = typeof options.contextMenu === "function" ? options.contextMenu({
|
|
29065
|
+
record: meta.record,
|
|
29066
|
+
id: meta.id,
|
|
29067
|
+
index: meta.index,
|
|
29068
|
+
column: meta.column,
|
|
29069
|
+
lane: meta.lane,
|
|
29070
|
+
rules: resolveKanbanRules(this._kanbanRules, meta.record, meta),
|
|
29071
|
+
selected: this._isViewSelected(meta.id)
|
|
29072
|
+
}) : Array.isArray(options.contextMenu) ? options.contextMenu : [];
|
|
29073
|
+
if (Array.isArray(extra) && extra.length) items.push("-", ...extra);
|
|
29074
|
+
return items;
|
|
29075
|
+
}
|
|
29076
|
+
/**
|
|
29077
|
+
* @param {{value:unknown,item:MenuItem,context:Element|null}} detail Selection detail.
|
|
29078
|
+
* @returns {void}
|
|
29079
|
+
*/
|
|
29080
|
+
_handleMenuSelect(detail) {
|
|
29081
|
+
const value = (
|
|
29082
|
+
/** @type {{kind?:string,id?:string,ids?:unknown[]}|null} */
|
|
29083
|
+
detail.value
|
|
29084
|
+
);
|
|
29085
|
+
if (!value || typeof value !== "object" || !value.kind || !Array.isArray(value.ids)) return;
|
|
29086
|
+
this.moveRecords(value.ids, value.kind === "lane" ? { lane: value.id } : { column: value.id });
|
|
29087
|
+
}
|
|
29088
|
+
/**
|
|
29089
|
+
* Dry-runs the policy path for a destination without emitting or committing anything.
|
|
29090
|
+
* @param {unknown[]} ids Moving ids.
|
|
29091
|
+
* @param {KanbanDestination} destination Destination.
|
|
29092
|
+
* @returns {boolean} Whether the move would be allowed.
|
|
29093
|
+
*/
|
|
29094
|
+
_canMove(ids, destination) {
|
|
29095
|
+
const options = this._kanbanOptions();
|
|
29096
|
+
const moving = this._orderedIds(ids);
|
|
29097
|
+
if (!moving.length) return false;
|
|
29098
|
+
const records = moving.map((id) => (
|
|
29099
|
+
/** @type {KanbanRecord} */
|
|
29100
|
+
this.getRecord(id)
|
|
29101
|
+
));
|
|
29102
|
+
const origins = moving.map((id) => (
|
|
29103
|
+
/** @type {KanbanMovePoint} */
|
|
29104
|
+
this._recordLocation(id)
|
|
29105
|
+
));
|
|
29106
|
+
const from = origins[0];
|
|
29107
|
+
const columnId = destination.column == null ? from.column : String(destination.column);
|
|
29108
|
+
const laneId = options.swimlaneBy == null ? null : destination.lane === void 0 ? from.lane : normalizeAxisId(destination.lane);
|
|
29109
|
+
const column = this._resolvedColumns().find((candidate) => candidate.id === columnId);
|
|
29110
|
+
const lane = options.swimlaneBy == null ? null : this._resolvedSwimlanes().find((candidate) => candidate.id === laneId) ?? null;
|
|
29111
|
+
if (!column || options.swimlaneBy != null && !lane) return false;
|
|
29112
|
+
const excluded = new Set(moving);
|
|
29113
|
+
const to = { column: columnId, lane: laneId, index: 0 };
|
|
29114
|
+
const context = {
|
|
29115
|
+
id: moving[0],
|
|
29116
|
+
from: { ...from },
|
|
29117
|
+
to: { ...to },
|
|
29118
|
+
column: { ...column },
|
|
29119
|
+
swimlane: lane ? { ...lane } : null
|
|
29120
|
+
};
|
|
29121
|
+
return this._evaluateMove(records, origins, column, lane, to, context, excluded).allowed;
|
|
29122
|
+
}
|
|
29123
|
+
/**
|
|
29124
|
+
* Resolves the ids one gesture moves: the whole selection when the grabbed card belongs to it,
|
|
29125
|
+
* otherwise just that card. Picking up an unselected card never drags the selection along.
|
|
29126
|
+
* @param {unknown} id Grabbed record id.
|
|
29127
|
+
* @returns {unknown[]} Ids to move, in board order.
|
|
29128
|
+
*/
|
|
29129
|
+
_movingIds(id) {
|
|
29130
|
+
const options = this._kanbanOptions();
|
|
29131
|
+
if (options.multiMove === false || options.selectable !== "multi" || !this._isViewSelected(id)) return [id];
|
|
29132
|
+
const selection = this.getSelectionIds();
|
|
29133
|
+
return selection.length > 1 ? selection : [id];
|
|
29134
|
+
}
|
|
29135
|
+
/** @param {unknown[]} ids Requested ids. @returns {unknown[]} Existing ids in board order. */
|
|
29136
|
+
_orderedIds(ids) {
|
|
29137
|
+
const requested = new Set(ids);
|
|
29138
|
+
return this._viewData.map((record) => this._viewRecordId(record)).filter((id) => requested.has(id));
|
|
29139
|
+
}
|
|
27763
29140
|
/** @param {MouseEvent} event @returns {void} */
|
|
27764
29141
|
_handleBoardClick(event) {
|
|
29142
|
+
if (this._suppressClick) {
|
|
29143
|
+
this._suppressClick = false;
|
|
29144
|
+
return;
|
|
29145
|
+
}
|
|
27765
29146
|
const target = elementTarget(event);
|
|
27766
29147
|
const collapse = target?.closest("[data-kanban-collapse]");
|
|
27767
29148
|
if (collapse) {
|
|
@@ -27773,6 +29154,23 @@ var KanbanView = class extends RecordView {
|
|
|
27773
29154
|
else this.toggleSwimlane(String(element.dataset.kanbanId));
|
|
27774
29155
|
return;
|
|
27775
29156
|
}
|
|
29157
|
+
const add = target?.closest("[data-kanban-add]");
|
|
29158
|
+
if (add) {
|
|
29159
|
+
const element = (
|
|
29160
|
+
/** @type {HTMLElement} */
|
|
29161
|
+
add
|
|
29162
|
+
);
|
|
29163
|
+
const columnId = String(element.dataset.kanbanAdd);
|
|
29164
|
+
const laneId = element.dataset.kanbanLane || null;
|
|
29165
|
+
this.emit("recordadd", {
|
|
29166
|
+
column: this._resolvedColumns().find((candidate) => candidate.id === columnId) ?? null,
|
|
29167
|
+
swimlane: laneId == null ? null : this._resolvedSwimlanes().find((candidate) => candidate.id === laneId) ?? null,
|
|
29168
|
+
columnId,
|
|
29169
|
+
laneId,
|
|
29170
|
+
event
|
|
29171
|
+
});
|
|
29172
|
+
return;
|
|
29173
|
+
}
|
|
27776
29174
|
const meta = this._metaForTarget(target);
|
|
27777
29175
|
if (!meta) return;
|
|
27778
29176
|
const selection = (
|
|
@@ -27821,7 +29219,14 @@ var KanbanView = class extends RecordView {
|
|
|
27821
29219
|
}
|
|
27822
29220
|
/** @param {KeyboardEvent} event @returns {void} */
|
|
27823
29221
|
_handleBoardKeydown(event) {
|
|
29222
|
+
if (this._dragIds.length && event.key === "Escape") {
|
|
29223
|
+
event.preventDefault();
|
|
29224
|
+
this._clearDrag();
|
|
29225
|
+
this._announce(this._text("moveCanceledPlain"));
|
|
29226
|
+
return;
|
|
29227
|
+
}
|
|
27824
29228
|
const target = elementTarget(event);
|
|
29229
|
+
if (this._handleHistoryKeydown(event, target)) return;
|
|
27825
29230
|
const handle = target?.closest(".zx-kanban-view__move-handle");
|
|
27826
29231
|
const meta = this._metaForTarget(handle);
|
|
27827
29232
|
if (handle && meta) {
|
|
@@ -27845,12 +29250,33 @@ var KanbanView = class extends RecordView {
|
|
|
27845
29250
|
this.toggleSelection(cardMeta.id);
|
|
27846
29251
|
}
|
|
27847
29252
|
}
|
|
29253
|
+
/**
|
|
29254
|
+
* @param {KeyboardEvent} event Keyboard event.
|
|
29255
|
+
* @param {Element|null} target Event target.
|
|
29256
|
+
* @returns {boolean} Whether the event was handled as a history command.
|
|
29257
|
+
*/
|
|
29258
|
+
_handleHistoryKeydown(event, target) {
|
|
29259
|
+
if (this._kanbanOptions().history === false || !(event.ctrlKey || event.metaKey)) return false;
|
|
29260
|
+
if (target?.closest('input, textarea, [contenteditable="true"]')) return false;
|
|
29261
|
+
const key = event.key.toLowerCase();
|
|
29262
|
+
if (key === "z" && !event.shiftKey && this.canUndo()) {
|
|
29263
|
+
event.preventDefault();
|
|
29264
|
+
this.undo();
|
|
29265
|
+
return true;
|
|
29266
|
+
}
|
|
29267
|
+
if ((key === "y" || key === "z" && event.shiftKey) && this.canRedo()) {
|
|
29268
|
+
event.preventDefault();
|
|
29269
|
+
this.redo();
|
|
29270
|
+
return true;
|
|
29271
|
+
}
|
|
29272
|
+
return false;
|
|
29273
|
+
}
|
|
27848
29274
|
/** @param {KeyboardEvent} event @param {HTMLElement} handle @param {KanbanCardMeta} meta @returns {void} */
|
|
27849
29275
|
_handleMoveKeydown(event, handle, meta) {
|
|
27850
29276
|
if (event.key === " " || event.key === "Enter") {
|
|
27851
29277
|
event.preventDefault();
|
|
27852
|
-
if (this._keyboardMove
|
|
27853
|
-
const
|
|
29278
|
+
if (this._keyboardMove?.ids.some((candidate) => Object.is(candidate, meta.id))) {
|
|
29279
|
+
const move2 = this._keyboardMove;
|
|
27854
29280
|
this._keyboardMove = null;
|
|
27855
29281
|
handle.setAttribute("aria-pressed", "false");
|
|
27856
29282
|
const card = (
|
|
@@ -27859,22 +29285,24 @@ var KanbanView = class extends RecordView {
|
|
|
27859
29285
|
);
|
|
27860
29286
|
if (card) delete card.dataset.grabbed;
|
|
27861
29287
|
this._syncKeyboardTarget();
|
|
27862
|
-
this.
|
|
29288
|
+
this._commitInteractionMove(move2.ids, move2.to);
|
|
27863
29289
|
} else {
|
|
27864
29290
|
const from = this._recordLocation(meta.id);
|
|
27865
29291
|
if (!from) return;
|
|
27866
|
-
|
|
29292
|
+
const ids = this._movingIds(meta.id);
|
|
29293
|
+
const start = { ...from, index: this._visibleIndex(meta.id) };
|
|
29294
|
+
this._keyboardMove = { ids, from: { ...from }, to: start };
|
|
27867
29295
|
handle.setAttribute("aria-pressed", "true");
|
|
27868
29296
|
const card = handle.closest(".zx-record-card");
|
|
27869
29297
|
if (card) card.dataset.grabbed = "true";
|
|
27870
29298
|
this._syncKeyboardTarget();
|
|
27871
|
-
this._announce(
|
|
29299
|
+
this._announce(ids.length > 1 ? this._text("grabbedMultiple", { count: ids.length }) : this._text("grabbed", { name: this._recordName(meta.record, meta.index) }));
|
|
27872
29300
|
}
|
|
27873
29301
|
return;
|
|
27874
29302
|
}
|
|
27875
29303
|
if (event.key === "Escape" && this._keyboardMove) {
|
|
27876
29304
|
event.preventDefault();
|
|
27877
|
-
const
|
|
29305
|
+
const records = this._keyboardMove.ids.map((id) => this.getRecord(id)).filter(Boolean);
|
|
27878
29306
|
this._keyboardMove = null;
|
|
27879
29307
|
handle.setAttribute("aria-pressed", "false");
|
|
27880
29308
|
const card = (
|
|
@@ -27883,10 +29311,13 @@ var KanbanView = class extends RecordView {
|
|
|
27883
29311
|
);
|
|
27884
29312
|
if (card) delete card.dataset.grabbed;
|
|
27885
29313
|
this._syncKeyboardTarget();
|
|
27886
|
-
this._announce(
|
|
29314
|
+
this._announce(records.length ? this._text("moveCanceled", { name: this._movingName(
|
|
29315
|
+
/** @type {KanbanRecord[]} */
|
|
29316
|
+
records
|
|
29317
|
+
) }) : this._text("moveCanceledPlain"));
|
|
27887
29318
|
return;
|
|
27888
29319
|
}
|
|
27889
|
-
if (!this._keyboardMove || !Object.is(
|
|
29320
|
+
if (!this._keyboardMove || !this._keyboardMove.ids.some((candidate) => Object.is(candidate, meta.id)) || !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return;
|
|
27890
29321
|
event.preventDefault();
|
|
27891
29322
|
const move = this._keyboardMove;
|
|
27892
29323
|
const laneModifier = event.altKey || event.ctrlKey || event.metaKey;
|
|
@@ -27903,10 +29334,14 @@ var KanbanView = class extends RecordView {
|
|
|
27903
29334
|
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
27904
29335
|
move.to.index += event.key === "ArrowUp" ? -1 : 1;
|
|
27905
29336
|
}
|
|
27906
|
-
const targetCount = this.
|
|
27907
|
-
move.to.index =
|
|
29337
|
+
const targetCount = this._visibleCount(move.to.column, move.to.lane, new Set(move.ids));
|
|
29338
|
+
move.to.index = clampKanbanIndex(move.to.index, targetCount);
|
|
27908
29339
|
this._syncKeyboardTarget();
|
|
27909
|
-
this._announce(
|
|
29340
|
+
this._announce(this._text("target", {
|
|
29341
|
+
target: this._targetName(move.to),
|
|
29342
|
+
position: move.to.index + 1,
|
|
29343
|
+
total: targetCount + 1
|
|
29344
|
+
}));
|
|
27910
29345
|
}
|
|
27911
29346
|
/** @param {Event} event @returns {void} */
|
|
27912
29347
|
_handlePreviewError(event) {
|
|
@@ -27922,80 +29357,246 @@ var KanbanView = class extends RecordView {
|
|
|
27922
29357
|
if (preview) preview.dataset.failed = "true";
|
|
27923
29358
|
image.hidden = true;
|
|
27924
29359
|
}
|
|
27925
|
-
/**
|
|
27926
|
-
|
|
27927
|
-
|
|
27928
|
-
|
|
27929
|
-
|
|
27930
|
-
|
|
27931
|
-
|
|
29360
|
+
/**
|
|
29361
|
+
* Records a candidate drag. Nothing moves yet: a press that never travels stays a click, and a
|
|
29362
|
+
* touch on the card body stays a scroll until the long press elapses.
|
|
29363
|
+
* @param {PointerEvent} event Pointer event.
|
|
29364
|
+
* @returns {void}
|
|
29365
|
+
*/
|
|
29366
|
+
_handlePointerDown(event) {
|
|
29367
|
+
this._suppressClick = false;
|
|
29368
|
+
if (event.button != null && event.button !== 0) return;
|
|
29369
|
+
const target = elementTarget(event);
|
|
29370
|
+
const handle = target?.closest(".zx-kanban-view__move-handle");
|
|
29371
|
+
const meta = this._metaForTarget(handle ?? target);
|
|
29372
|
+
if (!meta) return;
|
|
29373
|
+
if (!handle) {
|
|
29374
|
+
if (this._kanbanOptions().dragFrom !== "card") return;
|
|
29375
|
+
if (target?.closest(INTERACTIVE) || target?.closest(".zx-record-card__selection")) return;
|
|
29376
|
+
}
|
|
29377
|
+
const ids = this._movingIds(meta.id);
|
|
29378
|
+
this._pointerStart = {
|
|
29379
|
+
x: event.clientX,
|
|
29380
|
+
y: event.clientY,
|
|
29381
|
+
id: event.pointerId,
|
|
29382
|
+
ids,
|
|
29383
|
+
touch: event.pointerType === "touch"
|
|
29384
|
+
};
|
|
29385
|
+
this._dragPoint = { x: event.clientX, y: event.clientY };
|
|
29386
|
+
clearTimeout(this._pointerHold);
|
|
29387
|
+
if (event.pointerType !== "touch" || handle) return;
|
|
29388
|
+
this._pointerHold = setTimeout(() => this._beginDrag(this._dragPoint.x, this._dragPoint.y), TOUCH_HOLD);
|
|
29389
|
+
}
|
|
29390
|
+
/** @param {PointerEvent} event @returns {void} */
|
|
29391
|
+
_handlePointerMove(event) {
|
|
29392
|
+
const start = this._pointerStart;
|
|
29393
|
+
if (!this._dragIds.length) {
|
|
29394
|
+
if (!start || event.pointerId !== start.id) return;
|
|
29395
|
+
const distance = Math.hypot(event.clientX - start.x, event.clientY - start.y);
|
|
29396
|
+
this._dragPoint = { x: event.clientX, y: event.clientY };
|
|
29397
|
+
if (distance <= DRAG_THRESHOLD) return;
|
|
29398
|
+
if (start.touch && this._pointerHold) {
|
|
29399
|
+
clearTimeout(this._pointerHold);
|
|
29400
|
+
this._pointerHold = 0;
|
|
29401
|
+
this._pointerStart = null;
|
|
29402
|
+
return;
|
|
29403
|
+
}
|
|
29404
|
+
this._beginDrag(event.clientX, event.clientY);
|
|
29405
|
+
if (!this._dragIds.length) return;
|
|
27932
29406
|
}
|
|
27933
|
-
this.
|
|
27934
|
-
|
|
27935
|
-
|
|
27936
|
-
|
|
27937
|
-
|
|
29407
|
+
if (this._dragPointer != null && event.pointerId !== this._dragPointer) return;
|
|
29408
|
+
event.preventDefault();
|
|
29409
|
+
this._dragPoint = { x: event.clientX, y: event.clientY };
|
|
29410
|
+
this._positionDragPreview();
|
|
29411
|
+
this._updateDropTarget(event.clientX, event.clientY);
|
|
27938
29412
|
}
|
|
27939
|
-
/** @param {
|
|
27940
|
-
|
|
27941
|
-
if (this.
|
|
27942
|
-
|
|
27943
|
-
|
|
27944
|
-
|
|
29413
|
+
/** @param {PointerEvent} event @returns {void} */
|
|
29414
|
+
_handlePointerUp(event) {
|
|
29415
|
+
if (this._dragIds.length && this._dragPointer != null && event.pointerId !== this._dragPointer) return;
|
|
29416
|
+
clearTimeout(this._pointerHold);
|
|
29417
|
+
this._pointerHold = 0;
|
|
29418
|
+
this._pointerStart = null;
|
|
29419
|
+
if (!this._dragIds.length) return;
|
|
27945
29420
|
event.preventDefault();
|
|
27946
|
-
const
|
|
27947
|
-
this._dragDestination
|
|
29421
|
+
const ids = [...this._dragIds];
|
|
29422
|
+
const destination = this._dragDestination ? { ...this._dragDestination } : null;
|
|
29423
|
+
this._clearDrag();
|
|
29424
|
+
this._suppressClick = true;
|
|
29425
|
+
if (destination) this._commitInteractionMove(ids, destination);
|
|
29426
|
+
}
|
|
29427
|
+
/**
|
|
29428
|
+
* Starts a pointer drag: the floating preview appears, the board stops selecting text, and the
|
|
29429
|
+
* auto-scroll loop begins.
|
|
29430
|
+
* @param {number} x Pointer client x.
|
|
29431
|
+
* @param {number} y Pointer client y.
|
|
29432
|
+
* @returns {void}
|
|
29433
|
+
*/
|
|
29434
|
+
_beginDrag(x, y) {
|
|
29435
|
+
const start = this._pointerStart;
|
|
29436
|
+
if (!start || this._dragIds.length) return;
|
|
29437
|
+
this._pointerHold = 0;
|
|
29438
|
+
this._dragIds = start.ids.filter((id) => this.getRecord(id));
|
|
29439
|
+
if (!this._dragIds.length) return;
|
|
29440
|
+
this._dragPoint = { x, y };
|
|
29441
|
+
this._board.dataset.dragging = "true";
|
|
29442
|
+
for (const id of this._dragIds) {
|
|
29443
|
+
const card = this._cardsById.get(id);
|
|
29444
|
+
if (card) card.dataset.dragging = "true";
|
|
29445
|
+
}
|
|
29446
|
+
this._dragPointer = start.id;
|
|
29447
|
+
try {
|
|
29448
|
+
this._board.setPointerCapture(start.id);
|
|
29449
|
+
this._dragCaptured = true;
|
|
29450
|
+
} catch {
|
|
29451
|
+
}
|
|
29452
|
+
globalThis.getSelection?.()?.removeAllRanges();
|
|
29453
|
+
this._dragPreview = this._createDragPreview();
|
|
29454
|
+
this.el.append(this._dragPreview);
|
|
29455
|
+
this._positionDragPreview();
|
|
29456
|
+
this._updateDropTarget(x, y);
|
|
29457
|
+
if (this._kanbanOptions().autoScroll !== false) this._scheduleAutoScroll();
|
|
29458
|
+
}
|
|
29459
|
+
/** @returns {HTMLElement} Floating drag preview. */
|
|
29460
|
+
_createDragPreview() {
|
|
29461
|
+
const records = this._dragIds.map((id) => (
|
|
29462
|
+
/** @type {KanbanRecord} */
|
|
29463
|
+
this.getRecord(id)
|
|
29464
|
+
));
|
|
29465
|
+
const custom = this._renderHook("renderDragPreview", {
|
|
29466
|
+
records,
|
|
29467
|
+
ids: [...this._dragIds],
|
|
29468
|
+
count: this._dragIds.length
|
|
29469
|
+
});
|
|
29470
|
+
const content = custom ?? (this._dragIds.length > 1 ? this._text("dragPreview", { count: this._dragIds.length }) : this._movingName(records));
|
|
29471
|
+
return h("div", {
|
|
29472
|
+
class: "zx-kanban-view__drag-preview",
|
|
29473
|
+
ariaHidden: "true",
|
|
29474
|
+
dataset: { count: String(this._dragIds.length) }
|
|
29475
|
+
}, content);
|
|
29476
|
+
}
|
|
29477
|
+
/** @returns {void} */
|
|
29478
|
+
_positionDragPreview() {
|
|
29479
|
+
if (!this._dragPreview) return;
|
|
29480
|
+
this._dragPreview.style.transform = `translate3d(${Math.round(this._dragPoint.x)}px, ${Math.round(this._dragPoint.y)}px, 0)`;
|
|
29481
|
+
}
|
|
29482
|
+
/**
|
|
29483
|
+
* Resolves and marks the drop destination under the pointer. Pointer capture retargets events to
|
|
29484
|
+
* the board, so the element under the pointer is looked up rather than read off the event.
|
|
29485
|
+
* @param {number} x Pointer client x.
|
|
29486
|
+
* @param {number} y Pointer client y.
|
|
29487
|
+
* @returns {void}
|
|
29488
|
+
*/
|
|
29489
|
+
_updateDropTarget(x, y) {
|
|
29490
|
+
const element = document.elementFromPoint(x, y);
|
|
29491
|
+
const section = element?.closest(".zx-kanban-view__column");
|
|
29492
|
+
const sectionMeta = section && this._board.contains(section) ? this._sectionMeta.get(section) : null;
|
|
27948
29493
|
this._clearDropMarkers();
|
|
27949
|
-
|
|
27950
|
-
|
|
27951
|
-
|
|
27952
|
-
const rectangle = card.getBoundingClientRect();
|
|
27953
|
-
card.dataset.drop = event.clientY >= rectangle.top + rectangle.height / 2 ? "after" : "before";
|
|
27954
|
-
} else {
|
|
27955
|
-
sectionMeta.list.dataset.drop = "append";
|
|
29494
|
+
if (!sectionMeta || this._collapsedColumns.has(sectionMeta.column)) {
|
|
29495
|
+
this._dragDestination = null;
|
|
29496
|
+
return;
|
|
27956
29497
|
}
|
|
27957
|
-
|
|
29498
|
+
const cards = this._draggableCards(sectionMeta.list);
|
|
29499
|
+
const index = cards.findIndex((card) => {
|
|
29500
|
+
const rectangle = card.getBoundingClientRect();
|
|
29501
|
+
return y < rectangle.top + rectangle.height / 2;
|
|
29502
|
+
});
|
|
29503
|
+
const position2 = index < 0 ? cards.length : index;
|
|
29504
|
+
if (cards[position2]) cards[position2].dataset.drop = "before";
|
|
29505
|
+
else if (cards.length) cards[cards.length - 1].dataset.drop = "after";
|
|
29506
|
+
else sectionMeta.list.dataset.drop = "append";
|
|
29507
|
+
this._dragDestination = { column: sectionMeta.column, lane: sectionMeta.lane, index: position2 };
|
|
27958
29508
|
}
|
|
27959
|
-
/** @
|
|
27960
|
-
|
|
27961
|
-
if (this.
|
|
27962
|
-
|
|
27963
|
-
|
|
27964
|
-
|
|
27965
|
-
|
|
27966
|
-
|
|
27967
|
-
|
|
27968
|
-
|
|
27969
|
-
|
|
27970
|
-
|
|
27971
|
-
const
|
|
27972
|
-
const
|
|
27973
|
-
if (
|
|
27974
|
-
|
|
27975
|
-
|
|
27976
|
-
|
|
27977
|
-
const
|
|
27978
|
-
if (
|
|
27979
|
-
|
|
27980
|
-
|
|
27981
|
-
|
|
27982
|
-
|
|
27983
|
-
|
|
27984
|
-
|
|
29509
|
+
/** @returns {void} */
|
|
29510
|
+
_scheduleAutoScroll() {
|
|
29511
|
+
if (this._dragFrame) return;
|
|
29512
|
+
this._dragFrame = requestAnimationFrame(() => {
|
|
29513
|
+
this._dragFrame = 0;
|
|
29514
|
+
if (!this._dragIds.length) return;
|
|
29515
|
+
this._autoScroll();
|
|
29516
|
+
this._scheduleAutoScroll();
|
|
29517
|
+
});
|
|
29518
|
+
}
|
|
29519
|
+
/** Scrolls the board horizontally and the hovered column vertically near their edges. @returns {void} */
|
|
29520
|
+
_autoScroll() {
|
|
29521
|
+
const { x, y } = this._dragPoint;
|
|
29522
|
+
const board = this._board.getBoundingClientRect();
|
|
29523
|
+
if (x < board.left + SCROLL_EDGE) this._board.scrollLeft -= SCROLL_SPEED;
|
|
29524
|
+
else if (x > board.right - SCROLL_EDGE) this._board.scrollLeft += SCROLL_SPEED;
|
|
29525
|
+
const list = document.elementFromPoint(x, y)?.closest(".zx-kanban-view__cards");
|
|
29526
|
+
if (!list || !this._board.contains(list) || list.scrollHeight <= list.clientHeight) return;
|
|
29527
|
+
const bounds = list.getBoundingClientRect();
|
|
29528
|
+
if (y < bounds.top + SCROLL_EDGE) list.scrollTop -= SCROLL_SPEED;
|
|
29529
|
+
else if (y > bounds.bottom - SCROLL_EDGE) list.scrollTop += SCROLL_SPEED;
|
|
29530
|
+
}
|
|
29531
|
+
/**
|
|
29532
|
+
* Commits a move whose index came from rendered cards. Search and filters hide records without
|
|
29533
|
+
* removing them, so the visible position is translated back before anything is proposed.
|
|
29534
|
+
* @param {unknown[]} ids Moving ids.
|
|
29535
|
+
* @param {KanbanMovePoint} point Destination in visible-card space.
|
|
29536
|
+
* @returns {void}
|
|
29537
|
+
*/
|
|
29538
|
+
_commitInteractionMove(ids, point) {
|
|
29539
|
+
const excluded = new Set(ids);
|
|
29540
|
+
this.moveRecords(ids, {
|
|
29541
|
+
column: point.column,
|
|
29542
|
+
lane: point.lane,
|
|
29543
|
+
index: this._absoluteIndex(point.column, point.lane, point.index, excluded)
|
|
29544
|
+
});
|
|
27985
29545
|
}
|
|
27986
29546
|
/** @returns {void} */
|
|
27987
29547
|
_clearDrag() {
|
|
27988
|
-
this.
|
|
27989
|
-
|
|
27990
|
-
|
|
27991
|
-
this.
|
|
29548
|
+
if (this._dragFrame) cancelAnimationFrame(this._dragFrame);
|
|
29549
|
+
this._dragFrame = 0;
|
|
29550
|
+
clearTimeout(this._pointerHold);
|
|
29551
|
+
this._pointerHold = 0;
|
|
29552
|
+
this._pointerStart = null;
|
|
29553
|
+
this._dragPreview?.remove();
|
|
29554
|
+
this._dragPreview = null;
|
|
29555
|
+
this._dragIds = [];
|
|
27992
29556
|
this._dragDestination = null;
|
|
29557
|
+
if (this._board) {
|
|
29558
|
+
if (this._dragCaptured && this._dragPointer != null) {
|
|
29559
|
+
try {
|
|
29560
|
+
this._board.releasePointerCapture(this._dragPointer);
|
|
29561
|
+
} catch {
|
|
29562
|
+
}
|
|
29563
|
+
}
|
|
29564
|
+
this._clearDropMarkers();
|
|
29565
|
+
delete this._board.dataset.dragging;
|
|
29566
|
+
for (const card of this._board.querySelectorAll("[data-dragging]")) delete /** @type {HTMLElement} */
|
|
29567
|
+
card.dataset.dragging;
|
|
29568
|
+
}
|
|
29569
|
+
this._dragPointer = null;
|
|
29570
|
+
this._dragCaptured = false;
|
|
27993
29571
|
}
|
|
27994
29572
|
/** @returns {void} */
|
|
27995
29573
|
_clearDropMarkers() {
|
|
27996
29574
|
for (const element of this._board.querySelectorAll("[data-drop]")) delete /** @type {HTMLElement} */
|
|
27997
29575
|
element.dataset.drop;
|
|
27998
29576
|
}
|
|
29577
|
+
/** @param {HTMLElement} list Card list. @returns {HTMLElement[]} Cards excluding the ones in flight. */
|
|
29578
|
+
_draggableCards(list) {
|
|
29579
|
+
const moving = new Set(this._dragIds.length ? this._dragIds : this._keyboardMove?.ids ?? []);
|
|
29580
|
+
return (
|
|
29581
|
+
/** @type {HTMLElement[]} */
|
|
29582
|
+
[...list.querySelectorAll(":scope > .zx-record-card")].filter((card) => !hasId(moving, this._cardMeta.get(card)?.id))
|
|
29583
|
+
);
|
|
29584
|
+
}
|
|
29585
|
+
/** @param {string} column @param {string|null} lane @param {Set<unknown>} excluded @returns {number} */
|
|
29586
|
+
_visibleCount(column, lane, excluded) {
|
|
29587
|
+
return this._recordsIn(column, lane).filter((entry) => {
|
|
29588
|
+
const id = this._viewRecordId(entry.record);
|
|
29589
|
+
return !hasId(excluded, id) && this._visibleIds.has(id);
|
|
29590
|
+
}).length;
|
|
29591
|
+
}
|
|
29592
|
+
/** @param {unknown} id Record id. @returns {number} Position among visible cards in its bucket. */
|
|
29593
|
+
_visibleIndex(id) {
|
|
29594
|
+
const location = this._recordLocation(id);
|
|
29595
|
+
if (!location) return 0;
|
|
29596
|
+
const visible = this._recordsIn(location.column, location.lane).filter((entry) => this._visibleIds.has(this._viewRecordId(entry.record)));
|
|
29597
|
+
const position2 = visible.findIndex((entry) => Object.is(this._viewRecordId(entry.record), id));
|
|
29598
|
+
return position2 < 0 ? visible.length : position2;
|
|
29599
|
+
}
|
|
27999
29600
|
/** @returns {void} */
|
|
28000
29601
|
_syncKeyboardTarget() {
|
|
28001
29602
|
for (const element of this._board.querySelectorAll("[data-keyboard-target]")) {
|
|
@@ -28010,7 +29611,7 @@ var KanbanView = class extends RecordView {
|
|
|
28010
29611
|
});
|
|
28011
29612
|
const sectionMeta = section ? this._sectionMeta.get(section) : null;
|
|
28012
29613
|
if (!sectionMeta) return;
|
|
28013
|
-
const cards =
|
|
29614
|
+
const cards = this._draggableCards(sectionMeta.list);
|
|
28014
29615
|
const target = cards[move.to.index];
|
|
28015
29616
|
if (target) target.dataset.keyboardTarget = "before";
|
|
28016
29617
|
else sectionMeta.list.dataset.keyboardTarget = "append";
|
|
@@ -28020,7 +29621,7 @@ var KanbanView = class extends RecordView {
|
|
|
28020
29621
|
for (const [id, card] of this._cardsById) {
|
|
28021
29622
|
const selected = this._isViewSelected(id);
|
|
28022
29623
|
card.dataset.selected = String(selected);
|
|
28023
|
-
|
|
29624
|
+
this._applyCardDescription(card, id);
|
|
28024
29625
|
const input = (
|
|
28025
29626
|
/** @type {HTMLInputElement|null} */
|
|
28026
29627
|
card.querySelector("[data-record-selection]")
|
|
@@ -28040,6 +29641,50 @@ var KanbanView = class extends RecordView {
|
|
|
28040
29641
|
this._cardsById.get(id)?.querySelector(".zx-kanban-view__move-handle") ?? null
|
|
28041
29642
|
);
|
|
28042
29643
|
}
|
|
29644
|
+
/**
|
|
29645
|
+
* Resolves a collapse control after a board refresh, retaining the same lane copy for columns.
|
|
29646
|
+
* @param {'column'|'swimlane'} axis Collapse axis.
|
|
29647
|
+
* @param {string} id Descriptor id.
|
|
29648
|
+
* @param {string|null} [lane=null] Column copy's lane.
|
|
29649
|
+
* @returns {HTMLElement|null}
|
|
29650
|
+
*/
|
|
29651
|
+
_collapseControl(axis, id, lane = null) {
|
|
29652
|
+
return (
|
|
29653
|
+
/** @type {HTMLElement|null} */
|
|
29654
|
+
[...this._board.querySelectorAll("[data-kanban-collapse]")].find((candidate) => {
|
|
29655
|
+
const control = (
|
|
29656
|
+
/** @type {HTMLElement} */
|
|
29657
|
+
candidate
|
|
29658
|
+
);
|
|
29659
|
+
if (control.dataset.kanbanCollapse !== axis || control.dataset.kanbanId !== id) return false;
|
|
29660
|
+
if (axis !== "column") return true;
|
|
29661
|
+
const section = control.closest(".zx-kanban-view__column");
|
|
29662
|
+
return section ? this._sectionMeta.get(section)?.lane === lane : false;
|
|
29663
|
+
}) ?? null
|
|
29664
|
+
);
|
|
29665
|
+
}
|
|
29666
|
+
/**
|
|
29667
|
+
* Runs one presentation hook, ignoring anything that is not a node so a hook cannot smuggle
|
|
29668
|
+
* markup in as a string.
|
|
29669
|
+
* @param {'renderCard'|'renderColumnHeader'|'renderSwimlaneHeader'|'renderColumnEmpty'|'renderDragPreview'} name Hook name.
|
|
29670
|
+
* @param {any} context Hook context.
|
|
29671
|
+
* @returns {Node|null} Node to adopt, or null for the default presentation.
|
|
29672
|
+
*/
|
|
29673
|
+
_renderHook(name, context) {
|
|
29674
|
+
const hook = this._kanbanOptions()[name];
|
|
29675
|
+
if (typeof hook !== "function") return null;
|
|
29676
|
+
const result = hook(context);
|
|
29677
|
+
return result && typeof result === "object" && "nodeType" in result ? (
|
|
29678
|
+
/** @type {Node} */
|
|
29679
|
+
result
|
|
29680
|
+
) : null;
|
|
29681
|
+
}
|
|
29682
|
+
/** @param {KanbanColumn} column @param {KanbanSwimlane|null} lane @returns {boolean} */
|
|
29683
|
+
_allowsAdd(column, lane) {
|
|
29684
|
+
const allow = this._kanbanOptions().allowAdd;
|
|
29685
|
+
if (typeof allow === "function") return allow(column, lane) === true;
|
|
29686
|
+
return allow === true;
|
|
29687
|
+
}
|
|
28043
29688
|
/** @param {KanbanRecord} record @param {number} index @returns {string} */
|
|
28044
29689
|
_recordName(record, index) {
|
|
28045
29690
|
const source = this._kanbanOptions().titleField;
|
|
@@ -28047,10 +29692,16 @@ var KanbanView = class extends RecordView {
|
|
|
28047
29692
|
if (value && typeof value === "object" && "nodeType" in value) {
|
|
28048
29693
|
return (
|
|
28049
29694
|
/** @type {Node} */
|
|
28050
|
-
value.textContent?.trim() || "record"
|
|
29695
|
+
value.textContent?.trim() || this._text("record")
|
|
28051
29696
|
);
|
|
28052
29697
|
}
|
|
28053
|
-
return String(value ?? "record");
|
|
29698
|
+
return String(value ?? this._text("record"));
|
|
29699
|
+
}
|
|
29700
|
+
/** @param {KanbanRecord[]} records Moving records. @returns {string} */
|
|
29701
|
+
_movingName(records) {
|
|
29702
|
+
if (!records.length) return this._text("record");
|
|
29703
|
+
if (records.length === 1) return this._recordName(records[0], this._viewData.indexOf(records[0]));
|
|
29704
|
+
return this._text("dragPreview", { count: records.length });
|
|
28054
29705
|
}
|
|
28055
29706
|
/** @param {KanbanMovePoint} point @returns {string} */
|
|
28056
29707
|
_targetName(point) {
|
|
@@ -28058,6 +29709,26 @@ var KanbanView = class extends RecordView {
|
|
|
28058
29709
|
const lane = point.lane == null ? null : this._resolvedSwimlanes().find((candidate) => candidate.id === point.lane);
|
|
28059
29710
|
return `${column?.label ?? point.column}${lane ? `, ${lane.label}` : ""}`;
|
|
28060
29711
|
}
|
|
29712
|
+
/** @param {KanbanMoveEvaluation} evaluation Capacity report. @returns {string} */
|
|
29713
|
+
_wipAnnouncement(evaluation) {
|
|
29714
|
+
if (evaluation.limit == null) return "";
|
|
29715
|
+
if (evaluation.limitExceeded) return this._text("limitExceeded", { limit: evaluation.limit });
|
|
29716
|
+
return evaluation.limitReached ? this._text("limitReached", { limit: evaluation.limit }) : "";
|
|
29717
|
+
}
|
|
29718
|
+
/**
|
|
29719
|
+
* Resolves one localizable string.
|
|
29720
|
+
* @param {keyof typeof DEFAULT_LABELS|string} key Label key.
|
|
29721
|
+
* @param {Record<string, unknown>} [args] Placeholder values.
|
|
29722
|
+
* @returns {string} Resolved text.
|
|
29723
|
+
*/
|
|
29724
|
+
_text(key, args) {
|
|
29725
|
+
const overrides = this._kanbanOptions().labels;
|
|
29726
|
+
const source = overrides && typeof overrides === "object" && key in overrides ? overrides[key] : (
|
|
29727
|
+
/** @type {Record<string,string>} */
|
|
29728
|
+
DEFAULT_LABELS[key]
|
|
29729
|
+
);
|
|
29730
|
+
return printf(source == null ? String(key) : String(source), args);
|
|
29731
|
+
}
|
|
28061
29732
|
/** @param {string} message @returns {void} */
|
|
28062
29733
|
_announce(message) {
|
|
28063
29734
|
const token = ++this._announcementToken;
|
|
@@ -28097,6 +29768,9 @@ function normalizeKanbanDescriptors(descriptors, axis = "column") {
|
|
|
28097
29768
|
if (source.accept != null && typeof source.accept !== "function") {
|
|
28098
29769
|
throw new TypeError(`Kanban ${axis} ${id} accept must be a function`);
|
|
28099
29770
|
}
|
|
29771
|
+
if (source.from != null && !Array.isArray(source.from)) {
|
|
29772
|
+
throw new TypeError(`Kanban ${axis} ${id} from must be an array or null`);
|
|
29773
|
+
}
|
|
28100
29774
|
seen.add(id);
|
|
28101
29775
|
const normalized = (
|
|
28102
29776
|
/** @type {Record<string, any>} */
|
|
@@ -28104,11 +29778,21 @@ function normalizeKanbanDescriptors(descriptors, axis = "column") {
|
|
|
28104
29778
|
...source,
|
|
28105
29779
|
id,
|
|
28106
29780
|
label: String(source.label ?? axisLabel(source.id)),
|
|
29781
|
+
limit: normalizeLimit2(source.limit),
|
|
29782
|
+
from: Array.isArray(source.from) ? source.from.map(String) : null,
|
|
28107
29783
|
...Object.prototype.hasOwnProperty.call(source, "value") ? {} : { value: source.id }
|
|
28108
29784
|
}
|
|
28109
29785
|
);
|
|
28110
|
-
if (axis === "column")
|
|
28111
|
-
|
|
29786
|
+
if (axis === "column") {
|
|
29787
|
+
const laneLimits = normalizeLaneLimits(source.laneLimits);
|
|
29788
|
+
if (laneLimits) normalized.laneLimits = laneLimits;
|
|
29789
|
+
if (source.wipPolicy === "block" || source.wipPolicy === "warn") {
|
|
29790
|
+
normalized.wipPolicy = source.wipPolicy;
|
|
29791
|
+
}
|
|
29792
|
+
} else {
|
|
29793
|
+
delete normalized.laneLimits;
|
|
29794
|
+
delete normalized.wipPolicy;
|
|
29795
|
+
}
|
|
28112
29796
|
return normalized;
|
|
28113
29797
|
})
|
|
28114
29798
|
);
|
|
@@ -28131,37 +29815,11 @@ function reconcileKanbanOrder(descriptors, order) {
|
|
|
28131
29815
|
const preferred = normalizeOrder(order).filter((id) => allowed.has(id));
|
|
28132
29816
|
return [...new Set(preferred), ...available.filter((id) => !preferred.includes(id))];
|
|
28133
29817
|
}
|
|
28134
|
-
function
|
|
28135
|
-
if (!
|
|
28136
|
-
const
|
|
28137
|
-
|
|
28138
|
-
|
|
28139
|
-
const sourceColumn = normalizeAxisId(readKanbanAccessor(source, sourceIndex, options.columnBy));
|
|
28140
|
-
const sourceLane = options.swimlaneBy == null ? null : normalizeAxisId(readKanbanAccessor(source, sourceIndex, options.swimlaneBy));
|
|
28141
|
-
const sourceGroup = records.filter((record, index) => normalizeAxisId(readKanbanAccessor(record, index, options.columnBy)) === sourceColumn && (options.swimlaneBy == null || normalizeAxisId(readKanbanAccessor(record, index, options.swimlaneBy)) === sourceLane));
|
|
28142
|
-
const from = {
|
|
28143
|
-
column: sourceColumn,
|
|
28144
|
-
lane: sourceLane,
|
|
28145
|
-
index: sourceGroup.findIndex((record) => Object.is(viewRecordId(record, options.recordId), id))
|
|
28146
|
-
};
|
|
28147
|
-
const next = records.filter((_, index) => index !== sourceIndex);
|
|
28148
|
-
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));
|
|
28149
|
-
const targetIndex = normalizeMoveIndex(options.destination.index, destinationRecords.length);
|
|
28150
|
-
let insertion = next.length;
|
|
28151
|
-
if (destinationRecords[targetIndex]) insertion = next.indexOf(destinationRecords[targetIndex]);
|
|
28152
|
-
else if (destinationRecords.length) insertion = next.indexOf(destinationRecords.at(-1)) + 1;
|
|
28153
|
-
next.splice(insertion, 0, movedRecord);
|
|
28154
|
-
return {
|
|
28155
|
-
records: next,
|
|
28156
|
-
from,
|
|
28157
|
-
to: { column: options.destination.column, lane: options.swimlaneBy == null ? null : options.destination.lane, index: targetIndex }
|
|
28158
|
-
};
|
|
28159
|
-
}
|
|
28160
|
-
function readKanbanAccessor(record, index, accessor) {
|
|
28161
|
-
return typeof accessor === "function" ? accessor(record, index) : accessor == null ? null : record?.[accessor];
|
|
28162
|
-
}
|
|
28163
|
-
function normalizeAxisId(value) {
|
|
28164
|
-
return value == null ? "" : String(value);
|
|
29818
|
+
function reconcileKanbanPreference(descriptors, order, preserveUnknown = false) {
|
|
29819
|
+
if (!preserveUnknown) return reconcileKanbanOrder(descriptors, order);
|
|
29820
|
+
const preferred = normalizeOrder(order);
|
|
29821
|
+
const available = descriptors.map((descriptor) => descriptor.id);
|
|
29822
|
+
return [...preferred, ...available.filter((id) => !preferred.includes(id))];
|
|
28165
29823
|
}
|
|
28166
29824
|
function axisLabel(value) {
|
|
28167
29825
|
if (value == null || String(value).trim() === "") return "Unassigned";
|
|
@@ -28173,13 +29831,42 @@ function normalizeLimit2(value) {
|
|
|
28173
29831
|
if (!Number.isFinite(number2) || number2 < 0) throw new TypeError("Kanban column limit must be a non-negative number");
|
|
28174
29832
|
return Math.floor(number2);
|
|
28175
29833
|
}
|
|
28176
|
-
function
|
|
28177
|
-
if (value == null
|
|
28178
|
-
|
|
29834
|
+
function normalizeLaneLimits(value) {
|
|
29835
|
+
if (value == null) return void 0;
|
|
29836
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
29837
|
+
throw new TypeError("Kanban column laneLimits must be an object");
|
|
29838
|
+
}
|
|
29839
|
+
const result = {};
|
|
29840
|
+
for (const [lane, limit] of Object.entries(
|
|
29841
|
+
/** @type {Record<string, unknown>} */
|
|
29842
|
+
value
|
|
29843
|
+
)) {
|
|
29844
|
+
const normalized = normalizeLimit2(limit);
|
|
29845
|
+
if (normalized != null) result[lane] = normalized;
|
|
29846
|
+
}
|
|
29847
|
+
return result;
|
|
29848
|
+
}
|
|
29849
|
+
function readColumnLaneLimit(column, lane) {
|
|
29850
|
+
const limits = column.laneLimits;
|
|
29851
|
+
if (!limits || typeof limits !== "object") return null;
|
|
29852
|
+
const value = limits[lane];
|
|
29853
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
29854
|
+
}
|
|
29855
|
+
function wipState(count2, limit) {
|
|
29856
|
+
if (limit == null) return "none";
|
|
29857
|
+
return count2 > limit ? "exceeded" : count2 === limit ? "limit" : "within";
|
|
28179
29858
|
}
|
|
28180
29859
|
function normalizeOrder(value) {
|
|
28181
29860
|
return Array.isArray(value) ? [...new Set(value.map(String))] : [];
|
|
28182
29861
|
}
|
|
29862
|
+
function stringifyValue(value) {
|
|
29863
|
+
if (value == null) return "";
|
|
29864
|
+
if (typeof value === "object") return "nodeType" in value ? String(
|
|
29865
|
+
/** @type {Node} */
|
|
29866
|
+
value.textContent ?? ""
|
|
29867
|
+
) : "";
|
|
29868
|
+
return String(value);
|
|
29869
|
+
}
|
|
28183
29870
|
function orderDescriptors(descriptors, order) {
|
|
28184
29871
|
const byId = new Map(descriptors.map((descriptor) => [descriptor.id, descriptor]));
|
|
28185
29872
|
return order.map((id) => byId.get(id)).filter(Boolean);
|
|
@@ -28193,12 +29880,28 @@ function moveOrderedId(order, id, targetId, position2) {
|
|
|
28193
29880
|
next.splice(target + (position2 === "after" ? 1 : 0), 0, String(id));
|
|
28194
29881
|
return next;
|
|
28195
29882
|
}
|
|
29883
|
+
function mergeKanbanResolvedOrder(preference, descriptors, resolvedOrder, preserveUnknown) {
|
|
29884
|
+
if (!preserveUnknown) return [...resolvedOrder];
|
|
29885
|
+
const available = new Set(descriptors.map((descriptor) => descriptor.id));
|
|
29886
|
+
const replacements = [...resolvedOrder];
|
|
29887
|
+
let cursor = 0;
|
|
29888
|
+
const merged = normalizeOrder(preference).flatMap((id) => {
|
|
29889
|
+
if (!available.has(id)) return [id];
|
|
29890
|
+
return cursor < replacements.length ? [replacements[cursor++]] : [];
|
|
29891
|
+
});
|
|
29892
|
+
merged.push(...replacements.slice(cursor));
|
|
29893
|
+
return normalizeOrder(merged);
|
|
29894
|
+
}
|
|
28196
29895
|
function sameMovePoint(left, right) {
|
|
28197
29896
|
return left.column === right.column && left.lane === right.lane && left.index === right.index;
|
|
28198
29897
|
}
|
|
28199
29898
|
function sameArray3(left, right) {
|
|
28200
29899
|
return left.length === right.length && left.every((value, index) => Object.is(value, right[index]));
|
|
28201
29900
|
}
|
|
29901
|
+
function hasId(haystack, needle) {
|
|
29902
|
+
for (const candidate of haystack) if (Object.is(candidate, needle)) return true;
|
|
29903
|
+
return false;
|
|
29904
|
+
}
|
|
28202
29905
|
function elementTarget(event) {
|
|
28203
29906
|
const target = event.target;
|
|
28204
29907
|
if (!target) return null;
|
|
@@ -32798,9 +34501,12 @@ export {
|
|
|
32798
34501
|
TreeView,
|
|
32799
34502
|
ValueList,
|
|
32800
34503
|
__,
|
|
34504
|
+
absoluteKanbanIndex,
|
|
32801
34505
|
addCalendarDays,
|
|
34506
|
+
addCalendarMonths,
|
|
32802
34507
|
addDays,
|
|
32803
34508
|
addMonths,
|
|
34509
|
+
allowsKanbanTransition,
|
|
32804
34510
|
appIcon,
|
|
32805
34511
|
appendCompact,
|
|
32806
34512
|
appendCompactChild,
|
|
@@ -32819,6 +34525,7 @@ export {
|
|
|
32819
34525
|
calendarDayKey,
|
|
32820
34526
|
calendarEventIntersects,
|
|
32821
34527
|
calendarEventSpansDays,
|
|
34528
|
+
calendarPageDate,
|
|
32822
34529
|
calendarRange,
|
|
32823
34530
|
chartSummaryTable,
|
|
32824
34531
|
clamp,
|
|
@@ -32832,6 +34539,7 @@ export {
|
|
|
32832
34539
|
configureIcons,
|
|
32833
34540
|
copyButton,
|
|
32834
34541
|
copyToClipboard,
|
|
34542
|
+
createKanbanHistory,
|
|
32835
34543
|
debounce,
|
|
32836
34544
|
deepMerge,
|
|
32837
34545
|
defaultDateRangePresets,
|
|
@@ -32841,6 +34549,7 @@ export {
|
|
|
32841
34549
|
emptyFilterAst,
|
|
32842
34550
|
emptyState,
|
|
32843
34551
|
escapeRegExp,
|
|
34552
|
+
evaluateKanbanMove,
|
|
32844
34553
|
faFamilies,
|
|
32845
34554
|
faIconClasses,
|
|
32846
34555
|
faNames,
|
|
@@ -32875,15 +34584,19 @@ export {
|
|
|
32875
34584
|
isSameDay,
|
|
32876
34585
|
isTruncated,
|
|
32877
34586
|
joinTime,
|
|
34587
|
+
kanbanSearchTerms,
|
|
32878
34588
|
kitUrl,
|
|
32879
34589
|
layoutCalendarSpans,
|
|
32880
34590
|
layoutTimedCalendarEvents,
|
|
32881
34591
|
loadFontAwesome,
|
|
32882
34592
|
loadFontAwesomeKit,
|
|
34593
|
+
locateKanbanRecord,
|
|
32883
34594
|
matchBreakpoint,
|
|
34595
|
+
matchesKanbanSearch,
|
|
32884
34596
|
moveViewField,
|
|
32885
34597
|
normalizeAppItems,
|
|
32886
34598
|
normalizeCalendarEvents,
|
|
34599
|
+
normalizeKanbanRules,
|
|
32887
34600
|
normalizeRange,
|
|
32888
34601
|
normalizeViewFields,
|
|
32889
34602
|
normalizeViewState,
|
|
@@ -32908,8 +34621,10 @@ export {
|
|
|
32908
34621
|
registerFieldAdapters,
|
|
32909
34622
|
registerIcons,
|
|
32910
34623
|
renderViewField,
|
|
34624
|
+
reorderKanbanRecords,
|
|
32911
34625
|
reorderTableRows,
|
|
32912
34626
|
resolveElement,
|
|
34627
|
+
resolveKanbanRules,
|
|
32913
34628
|
resolveSize,
|
|
32914
34629
|
rovingTabindex,
|
|
32915
34630
|
setLanguage,
|