@mk-kit/ui 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -0
  3. package/block-editor/README.md +254 -0
  4. package/fesm2022/mk-kit-ui-block-editor.mjs +2158 -0
  5. package/fesm2022/mk-kit-ui-block-editor.mjs.map +1 -0
  6. package/fesm2022/mk-kit-ui-button.mjs +81 -0
  7. package/fesm2022/mk-kit-ui-button.mjs.map +1 -0
  8. package/fesm2022/mk-kit-ui-checkbox.mjs +136 -0
  9. package/fesm2022/mk-kit-ui-checkbox.mjs.map +1 -0
  10. package/fesm2022/mk-kit-ui-chip.mjs +122 -0
  11. package/fesm2022/mk-kit-ui-chip.mjs.map +1 -0
  12. package/fesm2022/mk-kit-ui-context-menu.mjs +144 -0
  13. package/fesm2022/mk-kit-ui-context-menu.mjs.map +1 -0
  14. package/fesm2022/mk-kit-ui-core.mjs +1576 -0
  15. package/fesm2022/mk-kit-ui-core.mjs.map +1 -0
  16. package/fesm2022/mk-kit-ui-data.mjs +6055 -0
  17. package/fesm2022/mk-kit-ui-data.mjs.map +1 -0
  18. package/fesm2022/mk-kit-ui-datetime.mjs +3409 -0
  19. package/fesm2022/mk-kit-ui-datetime.mjs.map +1 -0
  20. package/fesm2022/mk-kit-ui-directives.mjs +1779 -0
  21. package/fesm2022/mk-kit-ui-directives.mjs.map +1 -0
  22. package/fesm2022/mk-kit-ui-dnd.mjs +1073 -0
  23. package/fesm2022/mk-kit-ui-dnd.mjs.map +1 -0
  24. package/fesm2022/mk-kit-ui-feedback.mjs +2426 -0
  25. package/fesm2022/mk-kit-ui-feedback.mjs.map +1 -0
  26. package/fesm2022/mk-kit-ui-forms.mjs +9208 -0
  27. package/fesm2022/mk-kit-ui-forms.mjs.map +1 -0
  28. package/fesm2022/mk-kit-ui-icon.mjs +470 -0
  29. package/fesm2022/mk-kit-ui-icon.mjs.map +1 -0
  30. package/fesm2022/mk-kit-ui-media.mjs +896 -0
  31. package/fesm2022/mk-kit-ui-media.mjs.map +1 -0
  32. package/fesm2022/mk-kit-ui-navigation.mjs +2542 -0
  33. package/fesm2022/mk-kit-ui-navigation.mjs.map +1 -0
  34. package/fesm2022/mk-kit-ui-rich-text.mjs +565 -0
  35. package/fesm2022/mk-kit-ui-rich-text.mjs.map +1 -0
  36. package/fesm2022/mk-kit-ui-table.mjs +1378 -0
  37. package/fesm2022/mk-kit-ui-table.mjs.map +1 -0
  38. package/fesm2022/mk-kit-ui.mjs +32 -0
  39. package/fesm2022/mk-kit-ui.mjs.map +1 -0
  40. package/package.json +130 -0
  41. package/schematics/collection.json +10 -0
  42. package/schematics/ng-add/index.js +113 -0
  43. package/schematics/ng-add/schema.json +22 -0
  44. package/schematics/package.json +3 -0
  45. package/styles/mk-kit.css +750 -0
  46. package/types/mk-kit-ui-block-editor.d.ts +292 -0
  47. package/types/mk-kit-ui-button.d.ts +40 -0
  48. package/types/mk-kit-ui-checkbox.d.ts +62 -0
  49. package/types/mk-kit-ui-chip.d.ts +59 -0
  50. package/types/mk-kit-ui-context-menu.d.ts +57 -0
  51. package/types/mk-kit-ui-core.d.ts +1105 -0
  52. package/types/mk-kit-ui-data.d.ts +2580 -0
  53. package/types/mk-kit-ui-datetime.d.ts +1171 -0
  54. package/types/mk-kit-ui-directives.d.ts +807 -0
  55. package/types/mk-kit-ui-dnd.d.ts +423 -0
  56. package/types/mk-kit-ui-feedback.d.ts +1270 -0
  57. package/types/mk-kit-ui-forms.d.ts +3586 -0
  58. package/types/mk-kit-ui-icon.d.ts +108 -0
  59. package/types/mk-kit-ui-media.d.ts +549 -0
  60. package/types/mk-kit-ui-navigation.d.ts +1169 -0
  61. package/types/mk-kit-ui-rich-text.d.ts +187 -0
  62. package/types/mk-kit-ui-table.d.ts +739 -0
  63. package/types/mk-kit-ui.d.ts +17 -0
@@ -0,0 +1,1073 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, inject, ElementRef, Directive, input, booleanAttribute, numberAttribute, contentChildren, computed, signal, ChangeDetectionStrategy, Component, output, effect, model, contentChild, TemplateRef } from '@angular/core';
3
+ import { MkLiveAnnouncer, MK_I18N, mkUniqueId } from '@mk-kit/ui/core';
4
+ import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
5
+
6
+ /**
7
+ * Pure array helpers for applying an {@link MkDropEvent}. They mutate the passed
8
+ * array(s) in place and also return the (target) array, mirroring the behaviour
9
+ * of Angular CDK's `moveItemInArray` / `transferArrayItem`.
10
+ */
11
+ function clampIndex(index, max) {
12
+ return Math.max(0, Math.min(index, max));
13
+ }
14
+ /**
15
+ * Move an item within a single array from `fromIndex` to `toIndex`.
16
+ * Mutates and returns `array`.
17
+ *
18
+ * ```ts
19
+ * mkMoveItemInArray(rows, e.previousIndex, e.currentIndex);
20
+ * ```
21
+ */
22
+ function mkMoveItemInArray(array, fromIndex, toIndex) {
23
+ if (array.length === 0)
24
+ return array;
25
+ const from = clampIndex(fromIndex, array.length - 1);
26
+ const to = clampIndex(toIndex, array.length - 1);
27
+ if (from === to)
28
+ return array;
29
+ const item = array[from];
30
+ const delta = to < from ? -1 : 1;
31
+ for (let i = from; i !== to; i += delta) {
32
+ array[i] = array[i + delta];
33
+ }
34
+ array[to] = item;
35
+ return array;
36
+ }
37
+ /**
38
+ * Move an item from one array (`from`) to another (`to`), removing it from
39
+ * `from[fromIndex]` and inserting it at `to[toIndex]`. Mutates both arrays and
40
+ * returns the target (`to`) array.
41
+ *
42
+ * ```ts
43
+ * mkTransferArrayItem(todo, done, e.previousIndex, e.currentIndex);
44
+ * ```
45
+ */
46
+ function mkTransferArrayItem(from, to, fromIndex, toIndex) {
47
+ if (from.length === 0)
48
+ return to;
49
+ const source = clampIndex(fromIndex, from.length - 1);
50
+ const target = clampIndex(toIndex, to.length);
51
+ const [item] = from.splice(source, 1);
52
+ to.splice(target, 0, item);
53
+ return to;
54
+ }
55
+
56
+ /* eslint-disable @typescript-eslint/no-explicit-any -- lists hold heterogeneous
57
+ item types; `any` here avoids generic-variance friction across the registry. */
58
+ /**
59
+ * Central registry of every live `[mkDropList]` on the page, keyed by id.
60
+ *
61
+ * Connected lists (kanban "buckets") use it to resolve the sibling lists named
62
+ * in `mkDropListConnectedTo`, and both pointer and keyboard dragging use it to
63
+ * find the group of lists an item may travel between.
64
+ *
65
+ * Registration is automatic — you never call this service directly; it is
66
+ * documented so tooling/tests can inspect the wiring.
67
+ */
68
+ class MkDragDropRegistry {
69
+ lists = new Map();
70
+ /** Register (or replace) the list published under `id`. */
71
+ register(id, list) {
72
+ this.lists.set(id, list);
73
+ }
74
+ /** Remove `list` from the registry if it is still the holder of `id`. */
75
+ unregister(id, list) {
76
+ if (this.lists.get(id) === list)
77
+ this.lists.delete(id);
78
+ }
79
+ /** Look up a list by its `mkDropListId`. */
80
+ get(id) {
81
+ return this.lists.get(id);
82
+ }
83
+ /** All registered lists, in registration order. */
84
+ all() {
85
+ return [...this.lists.values()];
86
+ }
87
+ /**
88
+ * The ordered travel group for `list`: `list` itself plus every enabled list
89
+ * it is `mkDropListConnectedTo`, in registration (roughly DOM) order. Used to
90
+ * resolve "adjacent" lists for keyboard column-to-column movement and to
91
+ * hit-test the pointer against candidate targets.
92
+ */
93
+ connectedGroup(list) {
94
+ const connected = list.connectedTo();
95
+ return this.all().filter((l) => l === list || (connected.includes(l.id()) && !l.mkDropListDisabled()));
96
+ }
97
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDragDropRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
98
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDragDropRegistry, providedIn: 'root' });
99
+ }
100
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDragDropRegistry, decorators: [{
101
+ type: Injectable,
102
+ args: [{ providedIn: 'root' }]
103
+ }] });
104
+
105
+ /**
106
+ * Optional grip that restricts where a pointer drag of the enclosing
107
+ * `[mkDrag]` may begin. Place it on the element the user should press to drag;
108
+ * without any handle the whole item is draggable.
109
+ *
110
+ * A directive (not a component), so it composes onto anything — a `<span>`,
111
+ * a `<button>`, or another component's host such as `<mk-icon mkDragHandle />`.
112
+ * Its look (grab cursor, muted colour, `touch-action: none`) ships as the
113
+ * global `.mk-drag-handle` class in the theme stylesheet.
114
+ *
115
+ * ```html
116
+ * <div mkDrag [mkDragData]="row">
117
+ * <span mkDragHandle aria-hidden="true">⠿</span>
118
+ * {{ row.name }}
119
+ * </div>
120
+ * ```
121
+ */
122
+ class MkDragHandle {
123
+ /** The handle's host element. */
124
+ element = inject(ElementRef).nativeElement;
125
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDragHandle, deps: [], target: i0.ɵɵFactoryTarget.Directive });
126
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: MkDragHandle, isStandalone: true, selector: "[mkDragHandle]", host: { classAttribute: "mk-drag-handle" }, exportAs: ["mkDragHandle"], ngImport: i0 });
127
+ }
128
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDragHandle, decorators: [{
129
+ type: Directive,
130
+ args: [{
131
+ selector: '[mkDragHandle]',
132
+ exportAs: 'mkDragHandle',
133
+ host: {
134
+ class: 'mk-drag-handle',
135
+ },
136
+ }]
137
+ }] });
138
+
139
+ /* eslint-disable @typescript-eslint/no-explicit-any -- cross-list references
140
+ use `any` for the item type to avoid generic-variance friction. */
141
+ /** Pixels the pointer must travel before a press turns into a drag. */
142
+ const DRAG_THRESHOLD = 5;
143
+ /**
144
+ * Pixels a *touch* pointer may wander during the long-press delay before the
145
+ * press is treated as a scroll and the pending drag is abandoned.
146
+ */
147
+ const TOUCH_SLOP = 10;
148
+ /** Settle animation duration for the pointer preview (ms). */
149
+ const SETTLE_MS = 180;
150
+ /**
151
+ * Makes an item inside a `[mkDropList]` draggable — by pointer (mouse / touch /
152
+ * pen) **and** by keyboard (WCAG 2.1.1). The item is focusable, exposes
153
+ * `role="button"` + `aria-roledescription="Draggable item"`, and every move is
154
+ * announced via {@link MkLiveAnnouncer}.
155
+ *
156
+ * Keyboard: focus an item and press **Space/Enter** to pick it up, **Arrow**
157
+ * keys to move it (crossing into connected lists at the ends / across the
158
+ * perpendicular axis), **Space/Enter** to drop, **Escape** to cancel.
159
+ *
160
+ * Touch: a swipe scrolls the page as usual — the drag only arms after a
161
+ * long-press ({@link mkDragTouchDelay}, default 300 ms). While armed the item
162
+ * gets the `mk-drag--armed` class so consumers can style the lift moment.
163
+ * Mouse and pen drags start immediately, as before.
164
+ *
165
+ * Performance: pointer moves are rAF-coalesced (one hit-test + one set of
166
+ * style/DOM writes per frame) against list/item rects snapshotted when the
167
+ * drag lifts, so a move never forces layout. The pending frame is flushed
168
+ * synchronously on release so drops land exactly where the pointer ended.
169
+ *
170
+ * ```html
171
+ * <li mkDrag [mkDragData]="row" [mkDragDisabled]="row.locked">
172
+ * <span mkDragHandle aria-hidden="true">⠿</span> {{ row.title }}
173
+ * </li>
174
+ * ```
175
+ *
176
+ * @typeParam T item data type.
177
+ */
178
+ class MkDrag {
179
+ doc = inject(DOCUMENT);
180
+ registry = inject(MkDragDropRegistry);
181
+ announcer = inject(MkLiveAnnouncer);
182
+ i18n = inject(MK_I18N);
183
+ home = inject(MkDropList, { optional: true });
184
+ /** The item's host element. */
185
+ element = inject(ElementRef).nativeElement;
186
+ /** Arbitrary payload associated with this item. */
187
+ mkDragData = input(/* @ts-ignore */
188
+ ...(ngDevMode ? [undefined, { debugName: "mkDragData" }] : /* istanbul ignore next */ []));
189
+ /** Disable dragging this specific item. */
190
+ mkDragDisabled = input(false, { ...(ngDevMode ? { debugName: "mkDragDisabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
191
+ /**
192
+ * Long-press delay (ms) before a *touch* pointer arms the drag. Until it
193
+ * elapses a swipe scrolls natively; moving more than {@link TOUCH_SLOP}
194
+ * pixels abandons the pending drag. `0` arms immediately (legacy behavior).
195
+ * Mouse and pen are never delayed.
196
+ */
197
+ mkDragTouchDelay = input(300, { ...(ngDevMode ? { debugName: "mkDragTouchDelay" } : /* istanbul ignore next */ {}), transform: numberAttribute });
198
+ /** Every handle in the projected subtree, including those of nested drags. */
199
+ handles = contentChildren(MkDragHandle, { ...(ngDevMode ? { debugName: "handles" } : /* istanbul ignore next */ {}), descendants: true });
200
+ /**
201
+ * Handles that belong to *this* drag — i.e. whose nearest `[mkDrag]` ancestor
202
+ * is this item, not a nested one. A nested `[mkDropList]`/`[mkDrag]` (a
203
+ * product list inside a draggable category, say) would otherwise have its
204
+ * handles captured by the outer item via `descendants: true`, so pressing an
205
+ * inner handle would start the outer drag and inner dnd would never work.
206
+ */
207
+ ownHandles = computed(() => this.handles().filter((h) => h.element.closest('[mkDrag]') === this.element), /* @ts-ignore */
208
+ ...(ngDevMode ? [{ debugName: "ownHandles" }] : /* istanbul ignore next */ []));
209
+ /** True while a pointer drag is in progress. */
210
+ dragging = signal(false, /* @ts-ignore */
211
+ ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
212
+ /** True while the item is "picked up" for keyboard movement. */
213
+ lifted = signal(false, /* @ts-ignore */
214
+ ...(ngDevMode ? [{ debugName: "lifted" }] : /* istanbul ignore next */ []));
215
+ /** True from the moment a touch long-press arms the drag until release. */
216
+ armed = signal(false, /* @ts-ignore */
217
+ ...(ngDevMode ? [{ debugName: "armed" }] : /* istanbul ignore next */ []));
218
+ /** Whether the home list lays items out horizontally (scopes touch-action). */
219
+ inHorizontalList = computed(() => this.home?.mkDropListOrientation() === 'horizontal', /* @ts-ignore */
220
+ ...(ngDevMode ? [{ debugName: "inHorizontalList" }] : /* istanbul ignore next */ []));
221
+ /** Effective disabled state (item- or list-level). */
222
+ disabled = computed(() => this.mkDragDisabled() || (this.home?.mkDropListDisabled() ?? false), /* @ts-ignore */
223
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
224
+ // --- shared drag session state (only one item is ever active at a time) ---
225
+ targetList = null;
226
+ targetIndex = 0;
227
+ homeIndex = 0;
228
+ placeholder = null;
229
+ // --- pointer session state ---
230
+ pointerId = null;
231
+ started = false;
232
+ startX = 0;
233
+ startY = 0;
234
+ offsetX = 0;
235
+ offsetY = 0;
236
+ originLeft = 0;
237
+ originTop = 0;
238
+ preview = null;
239
+ moveHandler = (e) => this.onPointerMove(e);
240
+ upHandler = (e) => this.onPointerUp(e);
241
+ cancelHandler = () => this.finishPointer(true);
242
+ // --- touch long-press state ---
243
+ /** Gate for the move handler: mouse/pen arm on pointerdown, touch on timer. */
244
+ pointerArmed = false;
245
+ touchTimer = null;
246
+ /** Inline `touch-action` to restore after a drag locked it (null = not locked). */
247
+ savedTouchAction = null;
248
+ /**
249
+ * `touch-action: pan-y` (see drag.scss) keeps native scrolling alive while
250
+ * the long-press is pending, but that also means the browser may still start
251
+ * a scroll once we *are* dragging — so the armed drag must eat `touchmove`.
252
+ * Registered with `passive: false` for `preventDefault` to register.
253
+ */
254
+ touchMoveHandler = (e) => {
255
+ if (this.pointerArmed && e.cancelable)
256
+ e.preventDefault();
257
+ };
258
+ /** Android fires `contextmenu` on long-press — keep it off the gesture. */
259
+ contextMenuHandler = (e) => e.preventDefault();
260
+ // --- frame-coalesced move state (perf) ------------------------------
261
+ //
262
+ // Every `pointermove` used to force layout O(lists + items) times via
263
+ // getBoundingClientRect. Instead, moves now only record the latest
264
+ // coordinates and schedule ONE rAF (same pattern as the table's column
265
+ // resize); the frame resolves list/index from rects snapshotted at lift
266
+ // and does all style/DOM writes in one pass. The pending frame is flushed
267
+ // synchronously on pointerup so drops land exactly where the pointer ended.
268
+ /** Pending rAF id for the coalesced move pass, if any. */
269
+ moveRaf = null;
270
+ pendingX = 0;
271
+ pendingY = 0;
272
+ hasPendingMove = false;
273
+ /** Connected lists resolved once at lift (stable for the drag's duration). */
274
+ cachedGroup = [];
275
+ /** List bounds snapshotted at lift / after invalidation. */
276
+ listRects = new Map();
277
+ /** Item bounds per list, aligned with `itemElementsExcept(this)`. */
278
+ itemRects = new Map();
279
+ /** Lists whose snapshots a placeholder move invalidated (re-measured next frame). */
280
+ dirtyLists = new Set();
281
+ /** Any scroll moves everything — re-snapshot every list on the next frame. */
282
+ scrollDirty = false;
283
+ scrollHandler = () => {
284
+ this.scrollDirty = true;
285
+ };
286
+ /** Last placeholder sync target — makes `syncPlaceholder` idempotent. */
287
+ lastSyncList = null;
288
+ lastSyncIndex = -1;
289
+ // ===================================================================
290
+ // Pointer dragging
291
+ // ===================================================================
292
+ onPointerDown(event) {
293
+ const e = event;
294
+ if (this.disabled() || !this.home || this.lifted())
295
+ return;
296
+ if (e.button !== undefined && e.button !== 0)
297
+ return;
298
+ if (this.ownHandles().length && !this.isHandleTarget(e.target))
299
+ return;
300
+ this.pointerId = e.pointerId;
301
+ this.started = false;
302
+ this.startX = e.clientX;
303
+ this.startY = e.clientY;
304
+ const el = this.element;
305
+ try {
306
+ el.setPointerCapture(e.pointerId);
307
+ }
308
+ catch {
309
+ // Pointer already lifted (fast tap) — nothing left to capture.
310
+ }
311
+ el.addEventListener('pointermove', this.moveHandler);
312
+ el.addEventListener('pointerup', this.upHandler);
313
+ el.addEventListener('pointercancel', this.cancelHandler);
314
+ if (e.pointerType === 'touch') {
315
+ el.addEventListener('touchmove', this.touchMoveHandler, { passive: false });
316
+ el.addEventListener('contextmenu', this.contextMenuHandler);
317
+ const delay = this.mkDragTouchDelay();
318
+ if (delay > 0) {
319
+ // Long-press lift: do NOT preventDefault and do NOT arm yet — until
320
+ // the timer fires this press may just be the start of a scroll.
321
+ this.touchTimer =
322
+ this.doc.defaultView?.setTimeout(() => this.armTouch(), delay) ?? null;
323
+ }
324
+ else {
325
+ // Legacy immediate mode.
326
+ this.pointerArmed = true;
327
+ this.lockTouchAction();
328
+ }
329
+ }
330
+ else {
331
+ // Mouse / pen: armed immediately, the 5px threshold does the rest.
332
+ this.pointerArmed = true;
333
+ }
334
+ }
335
+ onPointerMove(e) {
336
+ if (this.pointerId === null || e.pointerId !== this.pointerId)
337
+ return;
338
+ if (!this.pointerArmed) {
339
+ // Long-press still pending: real movement means the user is scrolling —
340
+ // abandon the pending drag and leave the gesture to the browser.
341
+ if (Math.hypot(e.clientX - this.startX, e.clientY - this.startY) > TOUCH_SLOP) {
342
+ this.finishPointer(true);
343
+ }
344
+ return;
345
+ }
346
+ if (!this.started) {
347
+ if (Math.hypot(e.clientX - this.startX, e.clientY - this.startY) < DRAG_THRESHOLD) {
348
+ return;
349
+ }
350
+ this.beginPointer();
351
+ }
352
+ e.preventDefault();
353
+ // Only record the coordinates here — the heavy work (hit-testing,
354
+ // placeholder sync, preview transform) is coalesced to one rAF.
355
+ this.pendingX = e.clientX;
356
+ this.pendingY = e.clientY;
357
+ this.hasPendingMove = true;
358
+ this.scheduleMoveFrame();
359
+ }
360
+ /** The long-press delay elapsed with the finger still down — lift. */
361
+ armTouch() {
362
+ this.touchTimer = null;
363
+ this.pointerArmed = true;
364
+ this.armed.set(true);
365
+ // `pan-y` would still let the browser start a vertical scroll mid-drag;
366
+ // lock the element down for the rest of the gesture.
367
+ this.lockTouchAction();
368
+ }
369
+ lockTouchAction() {
370
+ this.savedTouchAction = this.element.style.touchAction;
371
+ this.element.style.touchAction = 'none';
372
+ }
373
+ unlockTouchAction() {
374
+ if (this.savedTouchAction === null)
375
+ return;
376
+ this.element.style.touchAction = this.savedTouchAction;
377
+ this.savedTouchAction = null;
378
+ }
379
+ /** Undo everything the touch path set up (timer, listeners, lock, class). */
380
+ clearTouchState() {
381
+ const el = this.element;
382
+ el.removeEventListener('touchmove', this.touchMoveHandler);
383
+ el.removeEventListener('contextmenu', this.contextMenuHandler);
384
+ if (this.touchTimer !== null) {
385
+ this.doc.defaultView?.clearTimeout(this.touchTimer);
386
+ this.touchTimer = null;
387
+ }
388
+ this.pointerArmed = false;
389
+ this.armed.set(false);
390
+ this.unlockTouchAction();
391
+ }
392
+ onPointerUp(e) {
393
+ if (this.pointerId === null || e.pointerId !== this.pointerId)
394
+ return;
395
+ this.finishPointer(!this.started);
396
+ }
397
+ beginPointer() {
398
+ if (!this.home)
399
+ return;
400
+ this.started = true;
401
+ this.dragging.set(true);
402
+ this.homeIndex = this.home.indexOf(this);
403
+ this.targetList = this.home;
404
+ this.targetIndex = this.homeIndex;
405
+ const rect = this.element.getBoundingClientRect();
406
+ this.originLeft = rect.left;
407
+ this.originTop = rect.top;
408
+ this.offsetX = this.startX - rect.left;
409
+ this.offsetY = this.startY - rect.top;
410
+ this.createPlaceholder(rect);
411
+ this.element.parentNode?.insertBefore(this.placeholder, this.element);
412
+ this.element.style.display = 'none';
413
+ this.createPreview(rect);
414
+ this.home.setReceiving(true);
415
+ // The manual insert above already placed the placeholder at homeIndex.
416
+ this.lastSyncList = this.home;
417
+ this.lastSyncIndex = this.homeIndex;
418
+ // One-time layout snapshot at lift; every move hits the cache instead of
419
+ // forcing layout. Scrolling anywhere invalidates the whole snapshot.
420
+ this.snapshotRects();
421
+ this.doc.addEventListener('scroll', this.scrollHandler, {
422
+ capture: true,
423
+ passive: true,
424
+ });
425
+ }
426
+ /** Coalesce move handling to at most one layout pass per animation frame. */
427
+ scheduleMoveFrame() {
428
+ if (this.moveRaf !== null)
429
+ return;
430
+ const raf = this.doc.defaultView?.requestAnimationFrame(() => {
431
+ this.moveRaf = null;
432
+ this.applyPendingMove();
433
+ });
434
+ if (raf === undefined)
435
+ this.applyPendingMove(); // no window — degrade to sync
436
+ else
437
+ this.moveRaf = raf;
438
+ }
439
+ /**
440
+ * Cancel the scheduled frame; when `apply` is set, process the pending
441
+ * coordinates synchronously (flush-on-end, like the table column resize) so
442
+ * a drop lands exactly where the pointer stopped.
443
+ */
444
+ flushMoveFrame(apply) {
445
+ if (this.moveRaf !== null) {
446
+ this.doc.defaultView?.cancelAnimationFrame(this.moveRaf);
447
+ this.moveRaf = null;
448
+ }
449
+ if (apply)
450
+ this.applyPendingMove();
451
+ this.hasPendingMove = false;
452
+ }
453
+ /**
454
+ * The per-frame move pass. Ordered reads → writes: refresh invalidated
455
+ * snapshots first, resolve the hovered list/index from the cache, then do
456
+ * all style/DOM writes — no read ever follows a write within the frame.
457
+ */
458
+ applyPendingMove() {
459
+ if (!this.started || !this.hasPendingMove)
460
+ return;
461
+ this.hasPendingMove = false;
462
+ // Reads: re-measure only what was invalidated since the last frame.
463
+ if (this.scrollDirty) {
464
+ this.scrollDirty = false;
465
+ this.dirtyLists.clear();
466
+ this.snapshotRects();
467
+ }
468
+ else if (this.dirtyLists.size) {
469
+ for (const list of this.dirtyLists)
470
+ this.measureList(list);
471
+ this.dirtyLists.clear();
472
+ }
473
+ const x = this.pendingX;
474
+ const y = this.pendingY;
475
+ const list = this.listUnderPoint(x, y) ?? this.targetList;
476
+ const index = list ? this.indexInList(list, x, y) : this.targetIndex;
477
+ // Writes: follow the cursor, then settle the placeholder.
478
+ if (this.preview) {
479
+ const dx = x - this.offsetX - this.originLeft;
480
+ const dy = y - this.offsetY - this.originTop;
481
+ this.preview.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
482
+ }
483
+ if (!list)
484
+ return;
485
+ if (list !== this.targetList) {
486
+ this.targetList?.setReceiving(false);
487
+ this.targetList = list;
488
+ list.setReceiving(true);
489
+ }
490
+ this.targetIndex = index;
491
+ this.syncPlaceholder();
492
+ }
493
+ finishPointer(cancel) {
494
+ if (this.pointerId !== null) {
495
+ try {
496
+ this.element.releasePointerCapture(this.pointerId);
497
+ }
498
+ catch {
499
+ /* capture may already be gone */
500
+ }
501
+ }
502
+ const el = this.element;
503
+ el.removeEventListener('pointermove', this.moveHandler);
504
+ el.removeEventListener('pointerup', this.upHandler);
505
+ el.removeEventListener('pointercancel', this.cancelHandler);
506
+ this.pointerId = null;
507
+ this.clearTouchState();
508
+ if (!this.started)
509
+ return; // was a click, never a drag
510
+ // Flush the last coalesced move (unless cancelling) so the drop target
511
+ // reflects exactly where the pointer ended, not the last painted frame.
512
+ this.flushMoveFrame(!cancel);
513
+ const settle = () => this.commitPointer(cancel);
514
+ if (cancel || this.prefersReducedMotion() || !this.preview) {
515
+ settle();
516
+ return;
517
+ }
518
+ // Animate the preview onto the placeholder, then commit.
519
+ const dest = this.placeholder?.getBoundingClientRect();
520
+ const preview = this.preview;
521
+ if (dest) {
522
+ const dx = dest.left - this.originLeft;
523
+ const dy = dest.top - this.originTop;
524
+ preview.style.transition = `transform ${SETTLE_MS}ms var(--mk-ease-emphasized)`;
525
+ preview.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
526
+ let done = false;
527
+ const end = () => {
528
+ if (done)
529
+ return;
530
+ done = true;
531
+ settle();
532
+ };
533
+ preview.addEventListener('transitionend', end, { once: true });
534
+ this.doc.defaultView?.setTimeout(end, SETTLE_MS + 40);
535
+ }
536
+ else {
537
+ settle();
538
+ }
539
+ }
540
+ commitPointer(cancel) {
541
+ if (this.destroyed)
542
+ return;
543
+ const container = this.targetList;
544
+ const previousContainer = this.home;
545
+ const currentIndex = this.targetIndex;
546
+ const previousIndex = this.homeIndex;
547
+ this.cleanupDom();
548
+ this.dragging.set(false);
549
+ if (cancel || !container || !previousContainer) {
550
+ this.announceCancelled('polite');
551
+ return;
552
+ }
553
+ this.emit(previousContainer, container, previousIndex, currentIndex, true);
554
+ this.announceDropped(currentIndex, 'polite');
555
+ }
556
+ // ===================================================================
557
+ // Keyboard dragging (WCAG 2.1.1)
558
+ // ===================================================================
559
+ onKeyDown(event) {
560
+ const e = event;
561
+ const key = e.key;
562
+ if (!this.lifted()) {
563
+ if ((key === ' ' || key === 'Enter') && !this.disabled() && this.home && !this.dragging()) {
564
+ e.preventDefault();
565
+ this.pickUp();
566
+ }
567
+ return;
568
+ }
569
+ // Picked up: capture the movement / drop / cancel keys.
570
+ const horizontal = this.targetList?.mkDropListOrientation() === 'horizontal';
571
+ switch (key) {
572
+ case ' ':
573
+ case 'Enter':
574
+ e.preventDefault();
575
+ this.dropKeyboard();
576
+ break;
577
+ case 'Escape':
578
+ e.preventDefault();
579
+ this.cancelKeyboard();
580
+ break;
581
+ case 'ArrowUp':
582
+ e.preventDefault();
583
+ horizontal ? this.stepList(-1) : this.stepPrimary(-1);
584
+ break;
585
+ case 'ArrowDown':
586
+ e.preventDefault();
587
+ horizontal ? this.stepList(1) : this.stepPrimary(1);
588
+ break;
589
+ case 'ArrowLeft':
590
+ e.preventDefault();
591
+ horizontal ? this.stepPrimary(-1) : this.stepList(-1);
592
+ break;
593
+ case 'ArrowRight':
594
+ e.preventDefault();
595
+ horizontal ? this.stepPrimary(1) : this.stepList(1);
596
+ break;
597
+ default:
598
+ break;
599
+ }
600
+ }
601
+ onBlur() {
602
+ // Losing focus mid-lift cancels the keyboard drag to avoid a stuck state.
603
+ if (this.lifted())
604
+ this.cancelKeyboard();
605
+ }
606
+ pickUp() {
607
+ if (!this.home)
608
+ return;
609
+ this.lifted.set(true);
610
+ this.homeIndex = this.home.indexOf(this);
611
+ this.targetList = this.home;
612
+ this.targetIndex = this.homeIndex;
613
+ const rect = this.element.getBoundingClientRect();
614
+ this.createPlaceholder(rect);
615
+ this.home.setReceiving(true);
616
+ // Fresh placeholder — force the first sync through the idempotence guard.
617
+ this.lastSyncList = null;
618
+ this.lastSyncIndex = -1;
619
+ this.syncPlaceholder();
620
+ this.announcePickedUp(this.homeIndex, this.home.size());
621
+ }
622
+ stepPrimary(step) {
623
+ const list = this.targetList;
624
+ if (!list)
625
+ return;
626
+ const max = this.maxIndex(list);
627
+ let idx = this.targetIndex + step;
628
+ if (idx < 0) {
629
+ const prev = this.adjacentList(list, -1);
630
+ if (prev)
631
+ return this.moveToList(prev, this.maxIndex(prev), true);
632
+ idx = 0;
633
+ }
634
+ else if (idx > max) {
635
+ const next = this.adjacentList(list, 1);
636
+ if (next)
637
+ return this.moveToList(next, 0, true);
638
+ idx = max;
639
+ }
640
+ if (idx === this.targetIndex)
641
+ return;
642
+ this.targetIndex = idx;
643
+ this.syncPlaceholder();
644
+ this.announceMove(false);
645
+ }
646
+ stepList(step) {
647
+ const list = this.targetList;
648
+ if (!list)
649
+ return;
650
+ const adj = this.adjacentList(list, step);
651
+ if (!adj)
652
+ return;
653
+ this.moveToList(adj, Math.min(this.targetIndex, this.maxIndex(adj)), true);
654
+ }
655
+ moveToList(list, index, crossed) {
656
+ this.targetList?.setReceiving(false);
657
+ this.targetList = list;
658
+ this.targetIndex = index;
659
+ list.setReceiving(true);
660
+ this.syncPlaceholder();
661
+ this.announceMove(crossed);
662
+ }
663
+ dropKeyboard() {
664
+ const container = this.targetList;
665
+ const previousContainer = this.home;
666
+ const currentIndex = this.targetIndex;
667
+ const previousIndex = this.homeIndex;
668
+ this.cleanupDom();
669
+ this.lifted.set(false);
670
+ if (!container || !previousContainer)
671
+ return;
672
+ this.emit(previousContainer, container, previousIndex, currentIndex, false);
673
+ this.announceDropped(currentIndex, 'assertive');
674
+ }
675
+ cancelKeyboard() {
676
+ this.cleanupDom();
677
+ this.lifted.set(false);
678
+ this.announceCancelled('assertive');
679
+ }
680
+ // ===================================================================
681
+ // Screen-reader announcements
682
+ //
683
+ // All user-facing strings come from MK_I18N so consumers can localize them.
684
+ // ===================================================================
685
+ /** "Picked up…" instructions when a keyboard drag starts. */
686
+ announcePickedUp(index, total) {
687
+ this.announcer.announce(this.i18n.dndPickedUp(index + 1, total), 'assertive');
688
+ }
689
+ /** Position update after each keyboard step (names the list when crossing). */
690
+ announceMove(crossed) {
691
+ const list = this.targetList;
692
+ if (!list)
693
+ return;
694
+ const total = list === this.home ? list.size() : list.size() + 1;
695
+ this.announcer.announce(crossed
696
+ ? this.i18n.dndMovedToList(list.label(), this.targetIndex + 1, total)
697
+ : this.i18n.dndMoved(this.targetIndex + 1, total), 'assertive');
698
+ }
699
+ /** Confirmation after a successful drop (pointer: polite; keyboard: assertive). */
700
+ announceDropped(index, politeness) {
701
+ this.announcer.announce(this.i18n.dndDropped(index + 1), politeness);
702
+ }
703
+ /** The drag was cancelled and the item snapped back. */
704
+ announceCancelled(politeness) {
705
+ this.announcer.announce(this.i18n.dndCancelled, politeness);
706
+ }
707
+ // ===================================================================
708
+ // Shared helpers
709
+ // ===================================================================
710
+ /** Highest valid target index for `list` given the item is being removed. */
711
+ maxIndex(list) {
712
+ return list === this.home ? Math.max(0, list.size() - 1) : list.size();
713
+ }
714
+ adjacentList(list, step) {
715
+ const group = this.registry.connectedGroup(list);
716
+ const i = group.indexOf(list);
717
+ const target = group[i + step];
718
+ return target ?? null;
719
+ }
720
+ /** Snapshot every connected list's bounds + item bounds (at lift / scroll). */
721
+ snapshotRects() {
722
+ this.cachedGroup = this.home ? this.registry.connectedGroup(this.home) : [];
723
+ this.listRects.clear();
724
+ this.itemRects.clear();
725
+ for (const list of this.cachedGroup)
726
+ this.measureList(list);
727
+ }
728
+ /** (Re)measure one list's bounds and item bounds into the cache. */
729
+ measureList(list) {
730
+ this.listRects.set(list, list.element.getBoundingClientRect());
731
+ this.itemRects.set(list, list.itemElementsExcept(this).map((el) => el.getBoundingClientRect()));
732
+ }
733
+ /**
734
+ * Which connected list (if any) the pointer is currently over. Pointer path
735
+ * only — reads the rects snapshotted at lift, not live layout.
736
+ */
737
+ listUnderPoint(x, y) {
738
+ for (const list of this.cachedGroup) {
739
+ const r = this.listRects.get(list) ?? list.element.getBoundingClientRect();
740
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom)
741
+ return list;
742
+ }
743
+ return null;
744
+ }
745
+ /**
746
+ * Insertion index for the pointer position within `list`. Pointer path only
747
+ * — reads the cached item rects (live measurement is the fallback for a
748
+ * list that somehow joined the group mid-drag).
749
+ */
750
+ indexInList(list, x, y) {
751
+ const rects = this.itemRects.get(list) ??
752
+ list.itemElementsExcept(this).map((el) => el.getBoundingClientRect());
753
+ const horizontal = list.mkDropListOrientation() === 'horizontal';
754
+ const pos = horizontal ? x : y;
755
+ for (let i = 0; i < rects.length; i++) {
756
+ const r = rects[i];
757
+ const mid = horizontal ? r.left + r.width / 2 : r.top + r.height / 2;
758
+ if (pos < mid)
759
+ return i;
760
+ }
761
+ return rects.length;
762
+ }
763
+ syncPlaceholder() {
764
+ const list = this.targetList;
765
+ const ph = this.placeholder;
766
+ if (!list || !ph)
767
+ return;
768
+ // Idempotent: same list and index → the placeholder is already in place.
769
+ if (list === this.lastSyncList && this.targetIndex === this.lastSyncIndex) {
770
+ return;
771
+ }
772
+ const prevList = this.lastSyncList;
773
+ this.lastSyncList = list;
774
+ this.lastSyncIndex = this.targetIndex;
775
+ const items = list.itemElementsExcept(this);
776
+ ph.remove();
777
+ if (this.targetIndex >= items.length) {
778
+ if (items.length)
779
+ items[items.length - 1].after(ph);
780
+ else
781
+ list.element.appendChild(ph);
782
+ }
783
+ else {
784
+ items[this.targetIndex].before(ph);
785
+ }
786
+ // Moving the placeholder shifted the affected lists' layout — re-measure
787
+ // just those lists on the next frame (no-op for the cache-less keyboard path).
788
+ this.dirtyLists.add(list);
789
+ if (prevList && prevList !== list)
790
+ this.dirtyLists.add(prevList);
791
+ }
792
+ createPlaceholder(rect) {
793
+ const ph = this.doc.createElement('div');
794
+ ph.className = 'mk-drop-placeholder';
795
+ ph.setAttribute('aria-hidden', 'true');
796
+ const s = ph.style;
797
+ s.boxSizing = 'border-box';
798
+ s.width = `${rect.width}px`;
799
+ s.height = `${rect.height}px`;
800
+ s.border = 'var(--mk-border-width-strong) dashed var(--mk-primary)';
801
+ s.borderRadius = 'var(--mk-radius-md)';
802
+ s.background = 'color-mix(in srgb, var(--mk-primary) 8%, transparent)';
803
+ this.placeholder = ph;
804
+ }
805
+ createPreview(rect) {
806
+ const clone = this.element.cloneNode(true);
807
+ clone.classList.add('mk-drag-preview');
808
+ clone.removeAttribute('tabindex');
809
+ clone.setAttribute('aria-hidden', 'true');
810
+ const s = clone.style;
811
+ s.display = '';
812
+ s.position = 'fixed';
813
+ s.margin = '0';
814
+ s.left = `${rect.left}px`;
815
+ s.top = `${rect.top}px`;
816
+ s.width = `${rect.width}px`;
817
+ s.height = `${rect.height}px`;
818
+ s.pointerEvents = 'none';
819
+ s.zIndex = 'var(--mk-z-tooltip)';
820
+ s.boxShadow = 'var(--mk-shadow-lg)';
821
+ s.borderRadius = 'var(--mk-radius-md)';
822
+ s.transform = 'translate3d(0, 0, 0)';
823
+ this.doc.body.appendChild(clone);
824
+ this.preview = clone;
825
+ }
826
+ /** Remove the body-level preview + placeholder if destroyed mid-drag. */
827
+ ngOnDestroy() {
828
+ this.destroyed = true;
829
+ if (this.pointerId !== null) {
830
+ try {
831
+ this.element.releasePointerCapture(this.pointerId);
832
+ }
833
+ catch {
834
+ /* capture may already be gone */
835
+ }
836
+ this.pointerId = null;
837
+ }
838
+ this.clearTouchState();
839
+ this.cleanupDom();
840
+ }
841
+ destroyed = false;
842
+ cleanupDom() {
843
+ this.flushMoveFrame(false); // drop any scheduled frame, never apply it
844
+ this.doc.removeEventListener('scroll', this.scrollHandler, { capture: true });
845
+ this.placeholder?.remove();
846
+ this.placeholder = null;
847
+ this.preview?.remove();
848
+ this.preview = null;
849
+ this.element.style.display = '';
850
+ this.home?.setReceiving(false);
851
+ this.targetList?.setReceiving(false);
852
+ this.cachedGroup = [];
853
+ this.listRects.clear();
854
+ this.itemRects.clear();
855
+ this.dirtyLists.clear();
856
+ this.scrollDirty = false;
857
+ this.lastSyncList = null;
858
+ this.lastSyncIndex = -1;
859
+ }
860
+ emit(previousContainer, container, previousIndex, currentIndex, isPointerEvent) {
861
+ const event = {
862
+ previousIndex,
863
+ currentIndex,
864
+ item: this,
865
+ previousContainer,
866
+ container,
867
+ isPointerEvent,
868
+ };
869
+ container.emitDrop(event);
870
+ }
871
+ isHandleTarget(target) {
872
+ if (!(target instanceof Node))
873
+ return false;
874
+ return this.ownHandles().some((h) => h.element.contains(target));
875
+ }
876
+ prefersReducedMotion() {
877
+ return (this.doc.defaultView?.matchMedia('(prefers-reduced-motion: reduce)')
878
+ .matches ?? false);
879
+ }
880
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDrag, deps: [], target: i0.ɵɵFactoryTarget.Component });
881
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.0.7", type: MkDrag, isStandalone: true, selector: "[mkDrag]", inputs: { mkDragData: { classPropertyName: "mkDragData", publicName: "mkDragData", isSignal: true, isRequired: false, transformFunction: null }, mkDragDisabled: { classPropertyName: "mkDragDisabled", publicName: "mkDragDisabled", isSignal: true, isRequired: false, transformFunction: null }, mkDragTouchDelay: { classPropertyName: "mkDragTouchDelay", publicName: "mkDragTouchDelay", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "button", "aria-roledescription": "Draggable item", "draggable": "false" }, listeners: { "pointerdown": "onPointerDown($event)", "keydown": "onKeyDown($event)", "blur": "onBlur()" }, properties: { "attr.tabindex": "disabled() ? -1 : 0", "attr.aria-disabled": "disabled() || null", "attr.aria-pressed": "lifted() || null", "attr.aria-grabbed": "dragging() || lifted()", "class.mk-drag--disabled": "disabled()", "class.mk-drag--dragging": "dragging()", "class.mk-drag--lifted": "lifted()", "class.mk-drag--armed": "armed()", "class.mk-drag--has-handle": "ownHandles().length > 0", "class.mk-drag--horizontal": "inHorizontalList()" }, classAttribute: "mk-drag" }, queries: [{ propertyName: "handles", predicate: MkDragHandle, descendants: true, isSignal: true }], exportAs: ["mkDrag"], ngImport: i0, template: "<ng-content />\n", styles: ["@charset \"UTF-8\";:host{position:relative;cursor:grab;touch-action:pan-y;user-select:none;-webkit-user-select:none}:host(.mk-drag--horizontal){touch-action:manipulation}:host(.mk-drag--has-handle){cursor:default;touch-action:auto}:host(.mk-drag--dragging){cursor:grabbing}:host(.mk-drag--armed){cursor:grabbing;border-radius:var(--mk-radius-md);box-shadow:var(--mk-shadow-md)}:host(.mk-drag--lifted){outline:var(--mk-border-width-strong) solid var(--mk-primary);outline-offset:var(--mk-focus-ring-offset);border-radius:var(--mk-radius-md);background-color:var(--mk-surface-2);box-shadow:var(--mk-shadow-lg);z-index:var(--mk-z-sticky)}:host(.mk-drag--disabled){cursor:not-allowed;opacity:.55;touch-action:auto}:host(:focus-visible){outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
882
+ }
883
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDrag, decorators: [{
884
+ type: Component,
885
+ args: [{ selector: '[mkDrag]', exportAs: 'mkDrag', changeDetection: ChangeDetectionStrategy.OnPush, host: {
886
+ class: 'mk-drag',
887
+ role: 'button',
888
+ 'aria-roledescription': 'Draggable item',
889
+ draggable: 'false',
890
+ '[attr.tabindex]': 'disabled() ? -1 : 0',
891
+ '[attr.aria-disabled]': 'disabled() || null',
892
+ '[attr.aria-pressed]': 'lifted() || null',
893
+ '[attr.aria-grabbed]': 'dragging() || lifted()',
894
+ '[class.mk-drag--disabled]': 'disabled()',
895
+ '[class.mk-drag--dragging]': 'dragging()',
896
+ '[class.mk-drag--lifted]': 'lifted()',
897
+ '[class.mk-drag--armed]': 'armed()',
898
+ '[class.mk-drag--has-handle]': 'ownHandles().length > 0',
899
+ '[class.mk-drag--horizontal]': 'inHorizontalList()',
900
+ '(pointerdown)': 'onPointerDown($event)',
901
+ '(keydown)': 'onKeyDown($event)',
902
+ '(blur)': 'onBlur()',
903
+ }, template: "<ng-content />\n", styles: ["@charset \"UTF-8\";:host{position:relative;cursor:grab;touch-action:pan-y;user-select:none;-webkit-user-select:none}:host(.mk-drag--horizontal){touch-action:manipulation}:host(.mk-drag--has-handle){cursor:default;touch-action:auto}:host(.mk-drag--dragging){cursor:grabbing}:host(.mk-drag--armed){cursor:grabbing;border-radius:var(--mk-radius-md);box-shadow:var(--mk-shadow-md)}:host(.mk-drag--lifted){outline:var(--mk-border-width-strong) solid var(--mk-primary);outline-offset:var(--mk-focus-ring-offset);border-radius:var(--mk-radius-md);background-color:var(--mk-surface-2);box-shadow:var(--mk-shadow-lg);z-index:var(--mk-z-sticky)}:host(.mk-drag--disabled){cursor:not-allowed;opacity:.55;touch-action:auto}:host(:focus-visible){outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"] }]
904
+ }], propDecorators: { mkDragData: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDragData", required: false }] }], mkDragDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDragDisabled", required: false }] }], mkDragTouchDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDragTouchDelay", required: false }] }], handles: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => MkDragHandle), { ...{ descendants: true }, isSignal: true }] }] } });
905
+
906
+ /* eslint-disable @typescript-eslint/no-explicit-any -- item-type params use
907
+ `any` to accept drags of any data type without generic-variance friction. */
908
+ /**
909
+ * A drop container for reorderable `[mkDrag]` items.
910
+ *
911
+ * - **Sort list:** a single `[mkDropList]` over an array — items reorder within it.
912
+ * - **Buckets / kanban:** several `[mkDropList]`s wired together with
913
+ * `mkDropListConnectedTo` so items transfer between them (both by pointer and
914
+ * by keyboard at the ends of a list).
915
+ *
916
+ * The array bound to `mkDropListData` is **not** mutated for you — handle
917
+ * `mkDropListDropped` and call {@link mkMoveItemInArray} / {@link mkTransferArrayItem}.
918
+ *
919
+ * ```html
920
+ * <ul mkDropList [mkDropListData]="todo()"
921
+ * mkDropListId="todo" [mkDropListConnectedTo]="['done']"
922
+ * (mkDropListDropped)="drop($event)">
923
+ * @for (t of todo(); track t.id) {
924
+ * <li mkDrag [mkDragData]="t">{{ t.title }}</li>
925
+ * }
926
+ * </ul>
927
+ * ```
928
+ *
929
+ * @typeParam T item data type.
930
+ */
931
+ class MkDropList {
932
+ registry = inject(MkDragDropRegistry);
933
+ /** The list's host element (drop target bounds). */
934
+ element = inject(ElementRef).nativeElement;
935
+ /** The array backing the list. Bound, never mutated by the directive. */
936
+ mkDropListData = input([], /* @ts-ignore */
937
+ ...(ngDevMode ? [{ debugName: "mkDropListData" }] : /* istanbul ignore next */ []));
938
+ /** Stable id used to connect lists. Auto-generated when omitted. */
939
+ mkDropListId = input(/* @ts-ignore */
940
+ ...(ngDevMode ? [undefined, { debugName: "mkDropListId" }] : /* istanbul ignore next */ []));
941
+ /** Ids of other lists items may be transferred into. */
942
+ mkDropListConnectedTo = input([], /* @ts-ignore */
943
+ ...(ngDevMode ? [{ debugName: "mkDropListConnectedTo" }] : /* istanbul ignore next */ []));
944
+ /**
945
+ * Human-readable name used in screen-reader announcements when an item is
946
+ * moved into this list (e.g. `"In progress"`). Falls back to the list `id`
947
+ * — which may be auto-generated gibberish — so set it wherever users can
948
+ * move items across lists by keyboard.
949
+ */
950
+ mkDropListLabel = input('', /* @ts-ignore */
951
+ ...(ngDevMode ? [{ debugName: "mkDropListLabel" }] : /* istanbul ignore next */ []));
952
+ /** Layout axis; controls pointer hit-testing and arrow-key direction. */
953
+ mkDropListOrientation = input('vertical', /* @ts-ignore */
954
+ ...(ngDevMode ? [{ debugName: "mkDropListOrientation" }] : /* istanbul ignore next */ []));
955
+ /** Disable dropping into (and dragging out of) this list. */
956
+ mkDropListDisabled = input(false, { ...(ngDevMode ? { debugName: "mkDropListDisabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
957
+ /** Fires when an item is dropped into this list (pointer or keyboard). */
958
+ mkDropListDropped = output();
959
+ /** Resolved id (input or generated). */
960
+ id = computed(() => this.mkDropListId() ?? this.autoId, /* @ts-ignore */
961
+ ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
962
+ autoId = mkUniqueId('mk-drop-list');
963
+ /** Announceable name: the label when set, otherwise the resolved id. */
964
+ label = computed(() => this.mkDropListLabel() || this.id(), /* @ts-ignore */
965
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
966
+ /** Connected-list ids, normalised to a plain array. */
967
+ connectedTo = computed(() => this.mkDropListConnectedTo() ?? [], /* @ts-ignore */
968
+ ...(ngDevMode ? [{ debugName: "connectedTo" }] : /* istanbul ignore next */ []));
969
+ /** The `mkDrag` items projected into this list, in DOM order. */
970
+ drags = contentChildren(MkDrag, /* @ts-ignore */
971
+ ...(ngDevMode ? [{ debugName: "drags" }] : /* istanbul ignore next */ []));
972
+ /** Highlight while a drag is hovering this list. */
973
+ _receiving = signal(false, /* @ts-ignore */
974
+ ...(ngDevMode ? [{ debugName: "_receiving" }] : /* istanbul ignore next */ []));
975
+ constructor() {
976
+ effect((onCleanup) => {
977
+ const id = this.id();
978
+ this.registry.register(id, this);
979
+ onCleanup(() => this.registry.unregister(id, this));
980
+ });
981
+ }
982
+ /** Number of drag items currently in the list. */
983
+ size() {
984
+ return this.drags().length;
985
+ }
986
+ /** Index of `drag` among this list's items, or -1. */
987
+ indexOf(drag) {
988
+ return this.drags().indexOf(drag);
989
+ }
990
+ /** Host elements of this list's items, excluding `exclude`, in DOM order. */
991
+ itemElementsExcept(exclude) {
992
+ return this.drags()
993
+ .filter((d) => d !== exclude)
994
+ .map((d) => d.element);
995
+ }
996
+ /** Toggle the "receiving" highlight (called by the active drag). */
997
+ setReceiving(value) {
998
+ this._receiving.set(value);
999
+ }
1000
+ /** Emit a drop into this list. Called by the active `MkDrag`. */
1001
+ emitDrop(event) {
1002
+ this.mkDropListDropped.emit(event);
1003
+ }
1004
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDropList, deps: [], target: i0.ɵɵFactoryTarget.Component });
1005
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.0.7", type: MkDropList, isStandalone: true, selector: "[mkDropList]", inputs: { mkDropListData: { classPropertyName: "mkDropListData", publicName: "mkDropListData", isSignal: true, isRequired: false, transformFunction: null }, mkDropListId: { classPropertyName: "mkDropListId", publicName: "mkDropListId", isSignal: true, isRequired: false, transformFunction: null }, mkDropListConnectedTo: { classPropertyName: "mkDropListConnectedTo", publicName: "mkDropListConnectedTo", isSignal: true, isRequired: false, transformFunction: null }, mkDropListLabel: { classPropertyName: "mkDropListLabel", publicName: "mkDropListLabel", isSignal: true, isRequired: false, transformFunction: null }, mkDropListOrientation: { classPropertyName: "mkDropListOrientation", publicName: "mkDropListOrientation", isSignal: true, isRequired: false, transformFunction: null }, mkDropListDisabled: { classPropertyName: "mkDropListDisabled", publicName: "mkDropListDisabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { mkDropListDropped: "mkDropListDropped" }, host: { properties: { "attr.aria-orientation": "mkDropListOrientation()", "attr.aria-disabled": "mkDropListDisabled() || null", "class.mk-drop-list--horizontal": "mkDropListOrientation() === 'horizontal'", "class.mk-drop-list--disabled": "mkDropListDisabled()", "class.mk-drop-list--receiving": "_receiving()" }, classAttribute: "mk-drop-list" }, queries: [{ propertyName: "drags", predicate: MkDrag, isSignal: true }], exportAs: ["mkDropList"], ngImport: i0, template: "<ng-content />\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative}:host(.mk-drop-list--receiving){outline:var(--mk-border-width-strong) solid var(--mk-primary-subtle-text);outline-offset:calc(-1 * var(--mk-border-width-strong));border-radius:var(--mk-radius-md);background-color:color-mix(in srgb,var(--mk-primary) 6%,transparent)}:host(.mk-drop-list--disabled){cursor:not-allowed}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1006
+ }
1007
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDropList, decorators: [{
1008
+ type: Component,
1009
+ args: [{ selector: '[mkDropList]', exportAs: 'mkDropList', changeDetection: ChangeDetectionStrategy.OnPush, host: {
1010
+ class: 'mk-drop-list',
1011
+ '[attr.aria-orientation]': 'mkDropListOrientation()',
1012
+ '[attr.aria-disabled]': 'mkDropListDisabled() || null',
1013
+ '[class.mk-drop-list--horizontal]': "mkDropListOrientation() === 'horizontal'",
1014
+ '[class.mk-drop-list--disabled]': 'mkDropListDisabled()',
1015
+ '[class.mk-drop-list--receiving]': '_receiving()',
1016
+ }, template: "<ng-content />\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative}:host(.mk-drop-list--receiving){outline:var(--mk-border-width-strong) solid var(--mk-primary-subtle-text);outline-offset:calc(-1 * var(--mk-border-width-strong));border-radius:var(--mk-radius-md);background-color:color-mix(in srgb,var(--mk-primary) 6%,transparent)}:host(.mk-drop-list--disabled){cursor:not-allowed}\n"] }]
1017
+ }], ctorParameters: () => [], propDecorators: { mkDropListData: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDropListData", required: false }] }], mkDropListId: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDropListId", required: false }] }], mkDropListConnectedTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDropListConnectedTo", required: false }] }], mkDropListLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDropListLabel", required: false }] }], mkDropListOrientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDropListOrientation", required: false }] }], mkDropListDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkDropListDisabled", required: false }] }], mkDropListDropped: [{ type: i0.Output, args: ["mkDropListDropped"] }], drags: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => MkDrag), { isSignal: true }] }] } });
1018
+
1019
+ /**
1020
+ * Thin convenience wrapper over a single `[mkDropList]` for the common
1021
+ * "reorderable list" case. Bind `items` two-way and provide an `<ng-template>`
1022
+ * to render each row; drops are applied to the model for you (via
1023
+ * {@link mkMoveItemInArray}).
1024
+ *
1025
+ * For connected buckets / kanban, use `[mkDropList]` + `[mkDrag]` directly.
1026
+ *
1027
+ * ```html
1028
+ * <mk-sortable-list [(items)]="rows">
1029
+ * <ng-template let-row let-i="index">
1030
+ * <span mkDragHandle aria-hidden="true">⠿</span> {{ i + 1 }}. {{ row.name }}
1031
+ * </ng-template>
1032
+ * </mk-sortable-list>
1033
+ * ```
1034
+ *
1035
+ * @typeParam T item data type.
1036
+ */
1037
+ class MkSortableList {
1038
+ /** The ordered items (two-way). Reordered in place on drop. */
1039
+ items = model([], /* @ts-ignore */
1040
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1041
+ /** Layout axis of the list. */
1042
+ orientation = input('vertical', /* @ts-ignore */
1043
+ ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
1044
+ /** Disable reordering. */
1045
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1046
+ /** `@for` tracking function. Defaults to identity (track by item). */
1047
+ trackBy = input((_, item) => item, /* @ts-ignore */
1048
+ ...(ngDevMode ? [{ debugName: "trackBy" }] : /* istanbul ignore next */ []));
1049
+ /** Emitted after the model has been reordered. */
1050
+ sorted = output();
1051
+ /** The row template projected as `<ng-template>`. */
1052
+ itemTemplate = contentChild.required(TemplateRef, /* @ts-ignore */
1053
+ ...(ngDevMode ? [{ debugName: "itemTemplate" }] : /* istanbul ignore next */ []));
1054
+ onDrop(event) {
1055
+ const next = [...this.items()];
1056
+ mkMoveItemInArray(next, event.previousIndex, event.currentIndex);
1057
+ this.items.set(next);
1058
+ this.sorted.emit(event);
1059
+ }
1060
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkSortableList, deps: [], target: i0.ɵɵFactoryTarget.Component });
1061
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkSortableList, isStandalone: true, selector: "mk-sortable-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { items: "itemsChange", sorted: "sorted" }, queries: [{ propertyName: "itemTemplate", first: true, predicate: TemplateRef, descendants: true, isSignal: true }], ngImport: i0, template: "<div\n mkDropList\n class=\"mk-sortable-list__list\"\n [mkDropListData]=\"items()\"\n [mkDropListOrientation]=\"orientation()\"\n [mkDropListDisabled]=\"disabled()\"\n (mkDropListDropped)=\"onDrop($event)\"\n>\n @for (item of items(); track trackBy()($index, item)) {\n <div mkDrag class=\"mk-sortable-list__item\" [mkDragData]=\"item\">\n <ng-container\n [ngTemplateOutlet]=\"itemTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: item, index: $index }\"\n />\n </div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.mk-sortable-list__list{display:flex;flex-direction:column;gap:var(--mk-space-2)}:host([data-orientation=horizontal]) .mk-sortable-list__list,.mk-sortable-list__list.mk-drop-list--horizontal{flex-direction:row}.mk-sortable-list__item{display:block}\n"], dependencies: [{ kind: "component", type: MkDropList, selector: "[mkDropList]", inputs: ["mkDropListData", "mkDropListId", "mkDropListConnectedTo", "mkDropListLabel", "mkDropListOrientation", "mkDropListDisabled"], outputs: ["mkDropListDropped"], exportAs: ["mkDropList"] }, { kind: "component", type: MkDrag, selector: "[mkDrag]", inputs: ["mkDragData", "mkDragDisabled", "mkDragTouchDelay"], exportAs: ["mkDrag"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1062
+ }
1063
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkSortableList, decorators: [{
1064
+ type: Component,
1065
+ args: [{ selector: 'mk-sortable-list', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkDropList, MkDrag, NgTemplateOutlet], template: "<div\n mkDropList\n class=\"mk-sortable-list__list\"\n [mkDropListData]=\"items()\"\n [mkDropListOrientation]=\"orientation()\"\n [mkDropListDisabled]=\"disabled()\"\n (mkDropListDropped)=\"onDrop($event)\"\n>\n @for (item of items(); track trackBy()($index, item)) {\n <div mkDrag class=\"mk-sortable-list__item\" [mkDragData]=\"item\">\n <ng-container\n [ngTemplateOutlet]=\"itemTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: item, index: $index }\"\n />\n </div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.mk-sortable-list__list{display:flex;flex-direction:column;gap:var(--mk-space-2)}:host([data-orientation=horizontal]) .mk-sortable-list__list,.mk-sortable-list__list.mk-drop-list--horizontal{flex-direction:row}.mk-sortable-list__item{display:block}\n"] }]
1066
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }, { type: i0.Output, args: ["itemsChange"] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], sorted: [{ type: i0.Output, args: ["sorted"] }], itemTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
1067
+
1068
+ /**
1069
+ * Generated bundle index. Do not edit.
1070
+ */
1071
+
1072
+ export { MkDrag, MkDragDropRegistry, MkDragHandle, MkDropList, MkSortableList, mkMoveItemInArray, mkTransferArrayItem };
1073
+ //# sourceMappingURL=mk-kit-ui-dnd.mjs.map