ngx-kit 6.3.0 → 7.0.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 (67) hide show
  1. package/README.md +3 -3
  2. package/angle-selector/README.md +34 -34
  3. package/box-shadow/README.md +33 -33
  4. package/color-picker/README.md +52 -49
  5. package/data-table/README.md +120 -113
  6. package/date-picker/README.md +74 -69
  7. package/dialog/README.md +92 -92
  8. package/drag-resize/README.md +55 -0
  9. package/dropzone/README.md +422 -422
  10. package/fesm2022/ngx-kit-angle-selector.mjs +2 -2
  11. package/fesm2022/ngx-kit-angle-selector.mjs.map +1 -1
  12. package/fesm2022/ngx-kit-box-shadow.mjs +2 -2
  13. package/fesm2022/ngx-kit-box-shadow.mjs.map +1 -1
  14. package/fesm2022/ngx-kit-color-picker.mjs +265 -73
  15. package/fesm2022/ngx-kit-color-picker.mjs.map +1 -1
  16. package/fesm2022/ngx-kit-data-table.mjs +2 -2
  17. package/fesm2022/ngx-kit-data-table.mjs.map +1 -1
  18. package/fesm2022/ngx-kit-date-picker.mjs +7 -9
  19. package/fesm2022/ngx-kit-date-picker.mjs.map +1 -1
  20. package/fesm2022/ngx-kit-dialog.mjs +3 -3
  21. package/fesm2022/ngx-kit-dialog.mjs.map +1 -1
  22. package/fesm2022/ngx-kit-drag-resize.mjs +1664 -0
  23. package/fesm2022/ngx-kit-drag-resize.mjs.map +1 -0
  24. package/fesm2022/ngx-kit-drawer-menu.mjs +389 -285
  25. package/fesm2022/ngx-kit-drawer-menu.mjs.map +1 -1
  26. package/fesm2022/ngx-kit-dropzone.mjs +2 -2
  27. package/fesm2022/ngx-kit-dropzone.mjs.map +1 -1
  28. package/fesm2022/ngx-kit-form-field.mjs +3 -5
  29. package/fesm2022/ngx-kit-form-field.mjs.map +1 -1
  30. package/fesm2022/ngx-kit-gallery.mjs +2 -2
  31. package/fesm2022/ngx-kit-gallery.mjs.map +1 -1
  32. package/fesm2022/ngx-kit-gradient-picker.mjs.map +1 -1
  33. package/fesm2022/ngx-kit-grid-layout.mjs +927 -0
  34. package/fesm2022/ngx-kit-grid-layout.mjs.map +1 -0
  35. package/fesm2022/ngx-kit-image-editor.mjs.map +1 -1
  36. package/fesm2022/ngx-kit-menu.mjs +8 -8
  37. package/fesm2022/ngx-kit-menu.mjs.map +1 -1
  38. package/fesm2022/ngx-kit-message.mjs.map +1 -1
  39. package/fesm2022/ngx-kit-notify.mjs +2 -2
  40. package/fesm2022/ngx-kit-notify.mjs.map +1 -1
  41. package/fesm2022/ngx-kit-shared.mjs +54 -3
  42. package/fesm2022/ngx-kit-shared.mjs.map +1 -1
  43. package/fesm2022/ngx-kit-time-picker.mjs +2 -2
  44. package/fesm2022/ngx-kit-time-picker.mjs.map +1 -1
  45. package/fesm2022/ngx-kit-virtual-scroll.mjs +392 -0
  46. package/fesm2022/ngx-kit-virtual-scroll.mjs.map +1 -0
  47. package/fesm2022/ngx-kit-webflow.mjs +709 -0
  48. package/fesm2022/ngx-kit-webflow.mjs.map +1 -0
  49. package/gallery/README.md +71 -71
  50. package/gradient-picker/README.md +41 -41
  51. package/grid-layout/README.md +64 -0
  52. package/image-editor/README.md +12 -12
  53. package/menu/README.md +85 -85
  54. package/message/README.md +70 -70
  55. package/notify/README.md +56 -58
  56. package/package.json +17 -1
  57. package/shared/README.md +117 -117
  58. package/time-picker/README.md +89 -95
  59. package/types/ngx-kit-color-picker.d.ts +72 -16
  60. package/types/ngx-kit-drag-resize.d.ts +368 -0
  61. package/types/ngx-kit-drawer-menu.d.ts +135 -86
  62. package/types/ngx-kit-grid-layout.d.ts +310 -0
  63. package/types/ngx-kit-image-editor.d.ts +1 -1
  64. package/types/ngx-kit-shared.d.ts +12 -1
  65. package/types/ngx-kit-virtual-scroll.d.ts +179 -0
  66. package/types/ngx-kit-webflow.d.ts +286 -0
  67. package/virtual-scroll/README.md +199 -0
@@ -0,0 +1,927 @@
1
+ import * as i0 from '@angular/core';
2
+ import { signal, computed, effect, Injectable, input, output, inject, ElementRef, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
3
+ import * as i1 from 'ngx-kit/drag-resize';
4
+ import { isRtl, NgxDraggable, NgxResizable } from 'ngx-kit/drag-resize';
5
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
+
7
+ class GridLayoutOptions {
8
+ cols = 12;
9
+ rowHeight = 50;
10
+ gap = 8;
11
+ padding = 0;
12
+ flow = 'free';
13
+ compact = 'vertical';
14
+ allowOverlap = false;
15
+ pushItems = true;
16
+ swap = false;
17
+ dragThreshold = 3;
18
+ animate = true;
19
+ rtl = 'auto';
20
+ maxRows;
21
+ gridBackgroundConfig = {
22
+ show: 'always',
23
+ borderColor: 'rgba(128,128,128,.16)',
24
+ gapColor: 'transparent',
25
+ rowColor: 'rgba(128,128,128,.07)',
26
+ columnColor: 'rgba(128,128,128,.07)',
27
+ borderWidth: 1,
28
+ };
29
+ }
30
+
31
+ class GridItemConfig {
32
+ x;
33
+ y;
34
+ w;
35
+ h;
36
+ minW;
37
+ minH;
38
+ maxW;
39
+ maxH;
40
+ /** Item can't be dragged or resized, and never moves during compaction/push. */
41
+ static;
42
+ /** Overrides the layout-level draggable/resizable flags for this item only. */
43
+ isDraggable;
44
+ isResizable;
45
+ constructor(x = 0, y = 0, w = 1, h = 1, s) {
46
+ this.x = x;
47
+ this.y = y;
48
+ this.w = w;
49
+ this.h = h;
50
+ this.static = s == true;
51
+ }
52
+ }
53
+ const normalizeGridItem = (value) => ({
54
+ x: Math.max(0, Math.floor(value?.x ?? 0)),
55
+ y: Math.max(0, Math.floor(value?.y ?? 0)),
56
+ w: Math.max(1, Math.floor(value?.w ?? 1)),
57
+ h: Math.max(1, Math.floor(value?.h ?? 1)),
58
+ minW: value?.minW,
59
+ minH: value?.minH,
60
+ maxW: value?.maxW,
61
+ maxH: value?.maxH,
62
+ static: value?.static ?? false,
63
+ isDraggable: value?.isDraggable,
64
+ isResizable: value?.isResizable,
65
+ });
66
+ /**
67
+ * Value equality for two (already-normalized) configs.
68
+ *
69
+ * This is what breaks the register -> settle -> new-object -> re-register
70
+ * feedback loop: signals compare by *reference*, so anything that recomputes
71
+ * a fresh config object on every change-detection pass (a `computed()` layout,
72
+ * an inline object literal in a template, etc.) will always look "changed" to
73
+ * an `effect()` even when nothing meaningful did. Callers must compare by
74
+ * value before writing back to a signal, otherwise the write itself produces
75
+ * a new reference that trips the same effect again — forever.
76
+ */
77
+ function gridItemConfigsEqual(a, b) {
78
+ return (a.x === b.x &&
79
+ a.y === b.y &&
80
+ a.w === b.w &&
81
+ a.h === b.h &&
82
+ (a.minW ?? null) === (b.minW ?? null) &&
83
+ (a.minH ?? null) === (b.minH ?? null) &&
84
+ (a.maxW ?? null) === (b.maxW ?? null) &&
85
+ (a.maxH ?? null) === (b.maxH ?? null) &&
86
+ !!a.static === !!b.static &&
87
+ (a.isDraggable ?? null) === (b.isDraggable ?? null) &&
88
+ (a.isResizable ?? null) === (b.isResizable ?? null));
89
+ }
90
+
91
+ /** Axis-aligned bounding-box overlap test in grid-cell space. */
92
+ function collides(a, b) {
93
+ return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
94
+ }
95
+ /** First item (other than `self`) whose cells overlap `config`, or null. */
96
+ function getFirstCollision(items, selfId, config) {
97
+ for (const item of items) {
98
+ if (item.id === selfId)
99
+ continue;
100
+ if (collides(item.config, config))
101
+ return item;
102
+ }
103
+ return null;
104
+ }
105
+ /** All items whose cells overlap `config`, excluding `selfId`. */
106
+ function getAllCollisions(items, selfId, config) {
107
+ return items.filter((item) => item.id !== selfId && collides(item.config, config));
108
+ }
109
+
110
+ function isStatic(node) {
111
+ return !!node.config.static;
112
+ }
113
+ function clampToCols(config, cols) {
114
+ config.w = Math.min(Math.max(1, config.w), cols);
115
+ config.x = Math.max(0, Math.min(config.x, cols - config.w));
116
+ config.y = Math.max(0, config.y);
117
+ }
118
+ function clone(items) {
119
+ return items.map((i) => ({ ...i, config: { ...i.config } }));
120
+ }
121
+ /**
122
+ * Moves `id` to grid cell (x, y) and, unless overlap is explicitly allowed,
123
+ * cascades any resulting collisions by pushing the colliding items out of the
124
+ * way (downward for vertical-flow grids, rightward for horizontal-flow ones).
125
+ * Static items are treated as fixed obstacles: they block, but never move.
126
+ * Pure function — returns a new array, never mutates its input.
127
+ */
128
+ function moveItem(items, id, x, y, opts, depth = 0) {
129
+ let arr = clone(items);
130
+ const item = arr.find((i) => i.id === id);
131
+ if (!item || isStatic(item))
132
+ return arr;
133
+ item.config.x = x;
134
+ item.config.y = y;
135
+ clampToCols(item.config, opts.cols);
136
+ if (opts.allowOverlap || depth > arr.length + 24)
137
+ return arr;
138
+ const compareFn = compareForCompaction(opts.compact);
139
+ let guard = 0;
140
+ while (guard++ < arr.length + 24) {
141
+ const mover = arr.find((i) => i.id === id);
142
+ const blockers = arr
143
+ .filter((i) => i.id !== id && !isStatic(i) && collides(mover.config, i.config))
144
+ .sort(compareFn);
145
+ if (blockers.length === 0)
146
+ break;
147
+ arr = pushAway(arr, mover, blockers[0], opts, depth + 1);
148
+ }
149
+ return arr;
150
+ }
151
+ function pushAway(items, mover, blocked, opts, depth) {
152
+ const targetX = opts.compact === 'horizontal' ? mover.config.x + mover.config.w : blocked.config.x;
153
+ const targetY = opts.compact === 'horizontal' ? blocked.config.y : mover.config.y + mover.config.h;
154
+ return moveItem(items, blocked.id, targetX, targetY, opts, depth);
155
+ }
156
+ function compareForCompaction(mode) {
157
+ return (a, b) => mode === 'horizontal'
158
+ ? a.config.x - b.config.x || a.config.y - b.config.y
159
+ : a.config.y - b.config.y || a.config.x - b.config.x;
160
+ }
161
+ /**
162
+ * When the actively-moved item directly overlaps exactly one same-size item,
163
+ * exchanging their positions reads much better than a cascading push. The
164
+ * caller (service) decides when to offer this vs. falling back to `moveItem`.
165
+ */
166
+ function trySwap(items, id, previous, next) {
167
+ const collisions = items.filter((i) => i.id !== id && !isStatic(i) && collides(i.config, next));
168
+ if (collisions.length !== 1)
169
+ return null;
170
+ const [target] = collisions;
171
+ if (target.config.w !== next.w || target.config.h !== next.h)
172
+ return null;
173
+ const arr = clone(items);
174
+ const mover = arr.find((i) => i.id === id);
175
+ const other = arr.find((i) => i.id === target.id);
176
+ mover.config.x = next.x;
177
+ mover.config.y = next.y;
178
+ other.config.x = previous.x;
179
+ other.config.y = previous.y;
180
+ return arr;
181
+ }
182
+ /**
183
+ * Removes gaps by pulling every non-static item as far toward the start of the
184
+ * grid as it can go without overlapping another item, in reading order.
185
+ */
186
+ function compact(items, mode) {
187
+ const arr = clone(items);
188
+ if (mode === 'none')
189
+ return arr;
190
+ const canX = mode === 'horizontal' || mode === 'both';
191
+ const canY = mode === 'vertical' || mode === 'both';
192
+ const sorted = [...arr].sort((a, b) => a.config.y - b.config.y || a.config.x - b.config.x);
193
+ for (const item of sorted) {
194
+ if (isStatic(item))
195
+ continue;
196
+ if (canY) {
197
+ let y = item.config.y;
198
+ while (y > 0 && !getFirstCollision(arr, item.id, { ...item.config, y: y - 1 }))
199
+ y--;
200
+ item.config.y = y;
201
+ }
202
+ if (canX) {
203
+ let x = item.config.x;
204
+ while (x > 0 && !getFirstCollision(arr, item.id, { ...item.config, x: x - 1 }))
205
+ x--;
206
+ item.config.x = x;
207
+ }
208
+ }
209
+ return arr;
210
+ }
211
+ /** Highest occupied row (0-based, exclusive) across all items — drives the grid's auto-height. */
212
+ function maxOccupiedRow(items) {
213
+ return items.reduce((max, i) => Math.max(max, i.config.y + i.config.h), 0);
214
+ }
215
+
216
+ function computeMetrics(containerWidth, options) {
217
+ const cols = Math.max(1, Math.floor(options.cols));
218
+ const gap = options.gap ?? 0;
219
+ const padding = options.padding ?? 0;
220
+ const contentWidth = Math.max(0, containerWidth - padding * 2);
221
+ const colWidth = Math.max(1, (contentWidth - gap * (cols - 1)) / cols);
222
+ const rowHeight = options.rowHeight === 'fit' ? Math.max(1, colWidth) : Math.max(1, options.rowHeight);
223
+ return { contentWidth, colWidth, rowHeight, gap, padding };
224
+ }
225
+ /** Physical CSS `left` (px, relative to the grid surface's content box) for a logical column. */
226
+ function colToLeft(colIndex, spanCols, m, rtl) {
227
+ if (!rtl)
228
+ return m.padding + colIndex * (m.colWidth + m.gap);
229
+ const widthPx = spanCols * m.colWidth + (spanCols - 1) * m.gap;
230
+ const rightEdge = m.padding + colIndex * (m.colWidth + m.gap);
231
+ return m.contentWidth + m.padding - rightEdge - widthPx;
232
+ }
233
+ function rowToTop(rowIndex, m) {
234
+ return m.padding + rowIndex * (m.rowHeight + m.gap);
235
+ }
236
+ function spanToWidthPx(spanCols, m) {
237
+ return spanCols * m.colWidth + Math.max(0, spanCols - 1) * m.gap;
238
+ }
239
+ function spanToHeightPx(spanRows, m) {
240
+ return spanRows * m.rowHeight + Math.max(0, spanRows - 1) * m.gap;
241
+ }
242
+ /** Physical left px (as above) -> nearest logical column index. Inverse of `colToLeft`. */
243
+ function leftToCol(leftPx, spanCols, m, rtl) {
244
+ if (!rtl)
245
+ return Math.round((leftPx - m.padding) / (m.colWidth + m.gap));
246
+ const widthPx = spanCols * m.colWidth + (spanCols - 1) * m.gap;
247
+ const rightEdge = m.contentWidth + m.padding - leftPx - widthPx;
248
+ return Math.round((rightEdge - m.padding) / (m.colWidth + m.gap));
249
+ }
250
+ function topToRow(topPx, m) {
251
+ return Math.round((topPx - m.padding) / (m.rowHeight + m.gap));
252
+ }
253
+ /** Places an item's pixel box (left/top/width/height) inside the grid surface, RTL-aware. */
254
+ function placeItem(config, m, rtl) {
255
+ return {
256
+ left: colToLeft(config.x, config.w, m, rtl),
257
+ top: rowToTop(config.y, m),
258
+ width: spanToWidthPx(config.w, m),
259
+ height: spanToHeightPx(config.h, m),
260
+ };
261
+ }
262
+
263
+ /** Structural + value equality for two item-state arrays (order-independent). */
264
+ function itemsEqual(a, b) {
265
+ if (a.length !== b.length)
266
+ return false;
267
+ return a.every((itemA) => {
268
+ const itemB = b.find((x) => x.id === itemA.id);
269
+ return (!!itemB && itemA.element === itemB.element && gridItemConfigsEqual(itemA.config, itemB.config));
270
+ });
271
+ }
272
+ class GridLayoutService {
273
+ options = signal(new GridLayoutOptions(), /* @ts-ignore */
274
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
275
+ items = signal([], /* @ts-ignore */
276
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
277
+ editMode = signal(true, /* @ts-ignore */
278
+ ...(ngDevMode ? [{ debugName: "editMode" }] : /* istanbul ignore next */ []));
279
+ activeId = signal(null, /* @ts-ignore */
280
+ ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
281
+ isInteracting = computed(() => this.activeId() !== null, /* @ts-ignore */
282
+ ...(ngDevMode ? [{ debugName: "isInteracting" }] : /* istanbul ignore next */ []));
283
+ height = computed(() => this.calculateHeight(this.items(), this.options()), /* @ts-ignore */
284
+ ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
285
+ layout = computed(() => this.items().map((x) => ({ id: x.id, ...x.config })), /* @ts-ignore */
286
+ ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
287
+ columns = computed(() => Math.max(1, Math.floor(this.options().cols)), /* @ts-ignore */
288
+ ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
289
+ /** Resolves `rtl: 'auto'` against the attached element's *actual* computed direction. */
290
+ rtl = computed(() => this.resolveRtl(), /* @ts-ignore */
291
+ ...(ngDevMode ? [{ debugName: "rtl" }] : /* istanbul ignore next */ []));
292
+ rtlTick = signal(0, /* @ts-ignore */
293
+ ...(ngDevMode ? [{ debugName: "rtlTick" }] : /* istanbul ignore next */ []));
294
+ layoutListener;
295
+ startConfig = null;
296
+ startItems = null;
297
+ previewItems = signal(null, /* @ts-ignore */
298
+ ...(ngDevMode ? [{ debugName: "previewItems" }] : /* istanbul ignore next */ []));
299
+ _element;
300
+ placeholder;
301
+ constructor() {
302
+ effect(() => {
303
+ // Re-run whenever options or items change so CSS positions always reflect state.
304
+ this.options();
305
+ this.items();
306
+ this.previewItems();
307
+ this.rtlTick();
308
+ if (this._element)
309
+ this.applyCss();
310
+ });
311
+ }
312
+ connect(onLayout) {
313
+ this.layoutListener = onLayout;
314
+ }
315
+ setOptions(value) {
316
+ this.options.update((o) => Object.assign(new GridLayoutOptions(), o, value));
317
+ }
318
+ setEditMode(value) {
319
+ this.editMode.set(value);
320
+ }
321
+ setItems(items) {
322
+ this.items.set(items.map((x) => ({ ...x, config: normalizeGridItem(x.config) })));
323
+ this.settle();
324
+ this.emit();
325
+ }
326
+ /**
327
+ * Registers a new item, or refreshes the DOM element reference of one that's
328
+ * already tracked. Adding an item can shift the whole layout (compaction /
329
+ * push), so that path settles. Merely re-supplying the same element on an
330
+ * already-known id has no geometric effect, so it is a deliberate no-op —
331
+ * skipping it is what keeps a per-item `effect()` that calls this on every
332
+ * change-detection pass from re-triggering `settle()` indefinitely.
333
+ */
334
+ registerItem(item) {
335
+ const existing = this.items().find((x) => x.id === item.id);
336
+ if (!existing) {
337
+ this.items.update((xs) => [...xs, { ...item, config: normalizeGridItem(item.config) }]);
338
+ this.settle();
339
+ return;
340
+ }
341
+ if (existing.element === item.element)
342
+ return;
343
+ this.items.update((xs) => xs.map((x) => (x.id === item.id ? { ...x, element: item.element } : x)));
344
+ }
345
+ /**
346
+ * Updates an item's config. Only writes the signal and re-settles the layout
347
+ * when the *value* actually changed. Without this guard, any caller that
348
+ * supplies a structurally-equal but referentially-new config object (e.g. a
349
+ * template bound to a `computed()` layout) would cause an unconditional
350
+ * `items.set()` on every run — which produces new output objects, which
351
+ * feed back into that same new config reference, forever.
352
+ */
353
+ updateItemConfig(id, config) {
354
+ const normalized = normalizeGridItem(config);
355
+ const existing = this.items().find((x) => x.id === id);
356
+ if (!existing || gridItemConfigsEqual(existing.config, normalized))
357
+ return;
358
+ this.items.update((xs) => xs.map((x) => (x.id === id ? { ...x, config: normalized } : x)));
359
+ this.settle();
360
+ }
361
+ unregisterItem(id) {
362
+ this.items.update((xs) => xs.filter((x) => x.id !== id));
363
+ this.settle();
364
+ this.emit();
365
+ }
366
+ /** Called once the grid surface element exists (e.g. from `ngAfterViewInit`). */
367
+ attachElement(el) {
368
+ this._element = el;
369
+ this.placeholder =
370
+ el.querySelector('.ngx-grid-layout__placeholder') ?? undefined;
371
+ if (this.placeholder) {
372
+ this.placeholder.style.position = 'absolute';
373
+ this.placeholder.style.display = 'none';
374
+ this.placeholder.style.pointerEvents = 'none';
375
+ this.placeholder.setAttribute('aria-hidden', 'true');
376
+ }
377
+ // `rtl` reads `this._element` directly (it isn't a signal), so bump a tick
378
+ // signal to force the `rtl` computed to re-evaluate now that the element,
379
+ // and therefore its computed `direction`, is known.
380
+ this.rtlTick.update((n) => n + 1);
381
+ this.applyCss();
382
+ }
383
+ /** Recomputes pixel positions after the surface resizes. Call from a `ResizeObserver`. */
384
+ refreshMetrics() {
385
+ this.applyCss();
386
+ }
387
+ isItemDraggable(item) {
388
+ return this.editMode() && !item.config.static && (item.config.isDraggable ?? true);
389
+ }
390
+ isItemResizable(item) {
391
+ return this.editMode() && !item.config.static && (item.config.isResizable ?? true);
392
+ }
393
+ // ---- Pointer-driven interaction (drag / resize) ----------------------------------
394
+ begin(id) {
395
+ const target = this.items().find((x) => x.id === id);
396
+ if (!target)
397
+ return;
398
+ this.startItems = this.items().map((x) => ({ ...x, config: { ...x.config } }));
399
+ this.startConfig = { ...target.config };
400
+ this.previewItems.set(null);
401
+ this.activeId.set(id);
402
+ this.applyCss();
403
+ }
404
+ move(id) {
405
+ const target = this.items().find((it) => it.id === id);
406
+ if (!target?.element)
407
+ return;
408
+ const next = this.screenToGrid(target);
409
+ this.preview(id, next);
410
+ }
411
+ resize(id, out) {
412
+ const target = this.items().find((x) => x.id === id);
413
+ if (!target?.element)
414
+ return;
415
+ const next = this.screenResizeToGrid(target, out);
416
+ this.preview(id, next);
417
+ }
418
+ end(id) {
419
+ if (this.activeId() !== id)
420
+ return;
421
+ const preview = this.previewItems();
422
+ if (preview) {
423
+ this.items.set(preview.map((x) => ({ ...x, config: { ...x.config } })));
424
+ }
425
+ const target = this.items().find((x) => x.id === id);
426
+ if (target?.element)
427
+ target.element.style.transform = '';
428
+ this.previewItems.set(null);
429
+ this.activeId.set(null);
430
+ this.startConfig = null;
431
+ this.startItems = null;
432
+ // Final normalization/compaction happens only after the preview is committed.
433
+ this.settle(id);
434
+ this.applyCss();
435
+ this.emit();
436
+ }
437
+ // ---- Keyboard-driven interaction (a11y) -------------------------------------------
438
+ /** Moves an item by whole grid cells — used by arrow-key handling in the item component. */
439
+ moveByCells(id, dCols, dRows) {
440
+ const target = this.items().find((x) => x.id === id);
441
+ if (!target)
442
+ return;
443
+ this.begin(id);
444
+ this.preview(id, { ...target.config, x: target.config.x + dCols, y: target.config.y + dRows });
445
+ this.end(id);
446
+ }
447
+ /** Resizes an item by whole grid cells — used by shift+arrow handling in the item component. */
448
+ resizeByCells(id, dCols, dRows) {
449
+ const target = this.items().find((x) => x.id === id);
450
+ if (!target)
451
+ return;
452
+ this.begin(id);
453
+ const w = this.clampSize(target.config.w + dCols, target.config.minW, target.config.maxW, this.columns());
454
+ const h = this.clampSize(target.config.h + dRows, target.config.minH, target.config.maxH, Infinity);
455
+ this.preview(id, { ...target.config, w, h });
456
+ this.end(id);
457
+ }
458
+ // ---- Internals ----------------------------------------------------------------------
459
+ preview(id, config) {
460
+ const base = this.startItems;
461
+ if (!base)
462
+ return;
463
+ const clamped = this.clamp(config);
464
+ const opt = this.options();
465
+ // The placeholder is the moving item during preview. The real DOM element
466
+ // remains under ngxDraggable's control and is never repositioned by the grid.
467
+ let candidate;
468
+ if (!opt.allowOverlap && opt.swap && this.startConfig) {
469
+ const swapped = trySwap(base, id, this.startConfig, clamped);
470
+ candidate =
471
+ swapped ??
472
+ base.map((x) => x.id === id ? { ...x, config: { ...clamped } } : { ...x, config: { ...x.config } });
473
+ }
474
+ else {
475
+ candidate = base.map((x) => x.id === id ? { ...x, config: { ...clamped } } : { ...x, config: { ...x.config } });
476
+ }
477
+ if (!opt.allowOverlap && opt.pushItems) {
478
+ candidate = moveItem(candidate, id, clamped.x, clamped.y, {
479
+ cols: this.columns(),
480
+ compact: opt.compact === 'none' ? 'vertical' : opt.compact,
481
+ allowOverlap: false,
482
+ maxRows: opt.maxRows,
483
+ });
484
+ }
485
+ if (!opt.allowOverlap && opt.compact !== 'none') {
486
+ candidate = compact(candidate, opt.compact);
487
+ }
488
+ this.previewItems.set(candidate);
489
+ this.applyCss();
490
+ }
491
+ screenToGrid(item) {
492
+ const containerRect = this._element.getBoundingClientRect();
493
+ const r = item.element.getBoundingClientRect();
494
+ const m = this.metrics();
495
+ const rtl = this.rtl();
496
+ const leftPx = r.left - containerRect.left;
497
+ const topPx = r.top - containerRect.top;
498
+ const nx = leftToCol(leftPx, item.config.w, m, rtl);
499
+ const ny = topToRow(topPx, m);
500
+ return this.clamp({ ...item.config, x: nx, y: ny });
501
+ }
502
+ screenResizeToGrid(item, out) {
503
+ const m = this.metrics();
504
+ const gap = m.gap;
505
+ let w = Math.max(1, Math.round((out.width + gap) / (m.colWidth + gap)));
506
+ let h = Math.max(1, Math.round((out.height + gap) / (m.rowHeight + gap)));
507
+ let x = item.config.x;
508
+ let y = item.config.y;
509
+ // moveLeft/moveTop are physical (viewport) deltas from the resizable directive;
510
+ // moving the item's *start* edge left/up in physical space means, in RTL, that
511
+ // the logical column count grows from the *end* instead — handled by re-deriving
512
+ // x from the resulting physical box rather than from moveLeft directly.
513
+ if (this.rtl()) {
514
+ // 'w' handle grows width to the right physically but that's the grid's logical end;
515
+ // recompute x from the (possibly shifted) physical left edge captured via the element.
516
+ const r = item.element.getBoundingClientRect();
517
+ const containerRect = this._element.getBoundingClientRect();
518
+ const leftPx = r.left + out.moveLeft - containerRect.left;
519
+ x = leftToCol(leftPx, w, m, true);
520
+ }
521
+ else {
522
+ if (out.moveLeft)
523
+ x += Math.round(out.moveLeft / (m.colWidth + gap));
524
+ }
525
+ if (out.moveTop)
526
+ y += Math.round(out.moveTop / (m.rowHeight + gap));
527
+ return this.clamp({ ...item.config, x, y, w, h });
528
+ }
529
+ clamp(c) {
530
+ const cols = this.columns();
531
+ const next = { ...c };
532
+ next.w = this.clampSize(next.w, next.minW, next.maxW, cols);
533
+ next.h = this.clampSize(next.h, next.minH, next.maxH, this.options().maxRows ?? Infinity);
534
+ next.x = Math.max(0, Math.min(next.x, cols - next.w));
535
+ next.y = Math.max(0, next.y);
536
+ if (this.options().maxRows)
537
+ next.y = Math.min(next.y, Math.max(0, this.options().maxRows - next.h));
538
+ return next;
539
+ }
540
+ clampSize(size, min, max, hardMax) {
541
+ let v = Math.max(min ?? 1, size);
542
+ if (max)
543
+ v = Math.min(v, max);
544
+ return Math.min(v, hardMax);
545
+ }
546
+ /** Applies pushes on drag-end and, when configured, removes gaps via compaction. */
547
+ settle(activeId) {
548
+ const opt = this.options();
549
+ const before = this.items();
550
+ let arr = before.map((x) => ({ ...x, config: { ...x.config } }));
551
+ if (!opt.allowOverlap) {
552
+ if (activeId && opt.pushItems) {
553
+ arr = moveItem(arr, activeId, arr.find((x) => x.id === activeId).config.x, arr.find((x) => x.id === activeId).config.y, {
554
+ cols: this.columns(),
555
+ compact: opt.compact === 'none' ? 'vertical' : opt.compact,
556
+ allowOverlap: false,
557
+ maxRows: opt.maxRows,
558
+ });
559
+ }
560
+ if (opt.compact !== 'none')
561
+ arr = compact(arr, opt.compact);
562
+ }
563
+ // Compaction/push are deterministic pure functions: if they didn't change
564
+ // anything, keep the previous array reference instead of writing an
565
+ // equal-but-new one — that would otherwise ripple into every computed
566
+ // derived from `items` (layout, height, per-item `interactive`, ...) for
567
+ // no reason, and risks re-arming the same feedback loop described above.
568
+ if (itemsEqual(before, arr))
569
+ return;
570
+ this.items.set(arr);
571
+ this.applyCss();
572
+ }
573
+ applyCss() {
574
+ if (!this._element)
575
+ return;
576
+ const source = this.previewItems() ?? this.items();
577
+ const activeId = this.activeId();
578
+ const m = this.metrics();
579
+ const rtl = this.rtl();
580
+ for (const item of source) {
581
+ if (!item.element)
582
+ continue;
583
+ // While dragging/resizing, the real active element is moved by
584
+ // ngxDraggable/ngxResizable. Never fight those directives by writing
585
+ // left/top/width/height here.
586
+ if (item.id === activeId)
587
+ continue;
588
+ const box = placeItem(item.config, m, rtl);
589
+ const style = item.element.style;
590
+ style.position = 'absolute';
591
+ style.left = `${box.left}px`;
592
+ style.top = `${box.top}px`;
593
+ style.width = `${box.width}px`;
594
+ style.height = `${box.height}px`;
595
+ item.element.dataset['gridX'] = String(item.config.x);
596
+ item.element.dataset['gridY'] = String(item.config.y);
597
+ item.element.dataset['gridW'] = String(item.config.w);
598
+ item.element.dataset['gridH'] = String(item.config.h);
599
+ }
600
+ const placeholderItem = activeId ? source.find((x) => x.id === activeId) : undefined;
601
+ if (this.placeholder) {
602
+ if (placeholderItem && activeId) {
603
+ const box = placeItem(placeholderItem.config, m, rtl);
604
+ const style = this.placeholder.style;
605
+ style.display = 'block';
606
+ style.left = `${box.left}px`;
607
+ style.top = `${box.top}px`;
608
+ style.width = `${box.width}px`;
609
+ style.height = `${box.height}px`;
610
+ this.placeholder.dataset['gridX'] = String(placeholderItem.config.x);
611
+ this.placeholder.dataset['gridY'] = String(placeholderItem.config.y);
612
+ this.placeholder.dataset['gridW'] = String(placeholderItem.config.w);
613
+ this.placeholder.dataset['gridH'] = String(placeholderItem.config.h);
614
+ }
615
+ else {
616
+ this.placeholder.style.display = 'none';
617
+ }
618
+ }
619
+ }
620
+ metrics() {
621
+ const rect = this._element?.getBoundingClientRect() ?? { width: 0 };
622
+ return computeMetrics(rect.width, this.options());
623
+ }
624
+ resolveRtl() {
625
+ const opt = this.options().rtl;
626
+ if (opt === true || opt === false)
627
+ return opt;
628
+ return this._element ? isRtl(this._element) : false;
629
+ }
630
+ calculateHeight(items, o) {
631
+ const preview = this.previewItems();
632
+ const source = preview ?? items;
633
+ const rows = Math.max(1, maxOccupiedRow(source));
634
+ const m = this._element
635
+ ? this.metrics()
636
+ : {
637
+ rowHeight: o.rowHeight === 'fit' ? 100 : o.rowHeight,
638
+ gap: o.gap ?? 0,
639
+ padding: o.padding ?? 0,
640
+ };
641
+ return m.padding * 2 + rows * m.rowHeight + Math.max(0, rows - 1) * m.gap;
642
+ }
643
+ emit() {
644
+ this.layoutListener?.(this.layout());
645
+ }
646
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: GridLayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
647
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: GridLayoutService });
648
+ }
649
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: GridLayoutService, decorators: [{
650
+ type: Injectable
651
+ }], ctorParameters: () => [] });
652
+
653
+ class NgxGridLayoutComponent {
654
+ options = input(new GridLayoutOptions(), /* @ts-ignore */
655
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
656
+ editMode = input(true, /* @ts-ignore */
657
+ ...(ngDevMode ? [{ debugName: "editMode" }] : /* istanbul ignore next */ []));
658
+ layoutChange = output();
659
+ /** Explicit direction override. Leave unset to inherit the page/host direction automatically. */
660
+ dir = input(undefined, /* @ts-ignore */
661
+ ...(ngDevMode ? [{ debugName: "dir" }] : /* istanbul ignore next */ []));
662
+ dirOverride = this.dir;
663
+ service = inject(GridLayoutService);
664
+ el = inject((ElementRef));
665
+ surface;
666
+ resizeObserver;
667
+ constructor() {
668
+ effect(() => {
669
+ const opts = { ...this.options() };
670
+ if (this.dir())
671
+ opts.rtl = this.dir() === 'rtl';
672
+ this.service.setOptions(opts);
673
+ this.service.setEditMode(this.editMode());
674
+ });
675
+ this.service.connect((layout) => this.layoutChange.emit(layout));
676
+ }
677
+ ngAfterViewInit() {
678
+ this.surface = this.el.nativeElement.querySelector('.ngx-grid-layout__surface');
679
+ this.service.attachElement(this.surface);
680
+ this.setCss();
681
+ this.resizeObserver = new ResizeObserver(() => {
682
+ this.service.refreshMetrics();
683
+ this.setCss();
684
+ });
685
+ this.resizeObserver.observe(this.surface);
686
+ }
687
+ ngOnDestroy() {
688
+ this.resizeObserver?.disconnect();
689
+ }
690
+ setItems(items) {
691
+ this.service.setItems(items);
692
+ }
693
+ update(options) {
694
+ this.service.setOptions(options);
695
+ this.setCss();
696
+ }
697
+ setCss() {
698
+ const el = this.el.nativeElement, o = this.service.options();
699
+ el.style.setProperty('--grid-cols', String(o.cols));
700
+ el.style.setProperty('--grid-gap', `${o.gap ?? 0}px`);
701
+ el.style.setProperty('--grid-row', `${o.rowHeight === 'fit' ? 100 : o.rowHeight}px`);
702
+ const b = o.gridBackgroundConfig;
703
+ el.style.setProperty('--grid-gap-color', `${b.gapColor}`);
704
+ el.style.setProperty('--grid-border-width', `${b?.borderWidth ?? 0}px`);
705
+ el.style.setProperty('--grid-border-color', b?.borderColor ?? 'transparent');
706
+ el.style.setProperty('--grid-row-color', b?.rowColor ?? 'transparent');
707
+ el.style.setProperty('--grid-col-color', b?.columnColor ?? 'transparent');
708
+ el.style.setProperty('--grid-bg-opacity', b?.show === 'never'
709
+ ? '0'
710
+ : b?.show === 'whenDragging'
711
+ ? this.service.isInteracting()
712
+ ? '1'
713
+ : '0'
714
+ : '1');
715
+ }
716
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxGridLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
717
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.4", type: NgxGridLayoutComponent, isStandalone: true, selector: "ngx-grid-layout", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null }, dir: { classPropertyName: "dir", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { layoutChange: "layoutChange" }, host: { properties: { "class.ngx-grid-layout--dragging": "service.isInteracting()", "class.ngx-grid-layout--view": "!editMode()", "style.height.px": "service.height()", "attr.dir": "dirOverride() ?? null" } }, ngImport: i0, template: `
718
+ <div class="ngx-grid-layout__surface">
719
+ <div class="ngx-grid-layout__placeholder"></div>
720
+ <ng-content />
721
+ </div>
722
+ `, isInline: true, styles: [":host{display:block;position:relative;contain:layout style;min-height:1px}.ngx-grid-layout__surface{position:relative;width:100%;height:100%;min-height:inherit}.ngx-grid-layout__placeholder{display:none;position:absolute;box-sizing:border-box;pointer-events:none;z-index:0;border:2px dashed currentColor;border-radius:inherit;opacity:.45}.ngx-grid-layout__surface:before{content:\"\";position:absolute;inset:0;pointer-events:none;opacity:var(--grid-bg-opacity);border:var(--grid-border-width) solid var(--grid-border-color);box-sizing:border-box;transition:opacity .15s ease;background-image:repeating-linear-gradient(var(--grid-border-color) 0 var(--grid-border-width),var(--grid-row-color) var(--grid-border-width) calc(var(--grid-row) - var(--grid-border-width)),var(--grid-border-color) calc(var(--grid-row) - var(--grid-border-width)) calc(var(--grid-row)),var(--grid-gap-color) calc(var(--grid-row)) calc(var(--grid-row) + var(--grid-gap))),repeating-linear-gradient(90deg,var(--grid-border-color) 0 var(--grid-border-width),var(--grid-col-color) var(--grid-border-width) calc(100% - (var(--grid-border-width) + var(--grid-gap))),var(--grid-border-color) calc(100% - (var(--grid-border-width) + var(--grid-gap))) calc(100% - var(--grid-gap)),var(--grid-gap-color) calc(100% - var(--grid-gap)) 100%);background-size:calc((100% + var(--grid-gap)) / var(--grid-cols)) calc(var(--grid-row) + var(--grid-gap));background-position:0 0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
723
+ }
724
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxGridLayoutComponent, decorators: [{
725
+ type: Component,
726
+ args: [{ selector: 'ngx-grid-layout', standalone: true, template: `
727
+ <div class="ngx-grid-layout__surface">
728
+ <div class="ngx-grid-layout__placeholder"></div>
729
+ <ng-content />
730
+ </div>
731
+ `, host: {
732
+ '[class.ngx-grid-layout--dragging]': 'service.isInteracting()',
733
+ '[class.ngx-grid-layout--view]': '!editMode()',
734
+ '[style.height.px]': 'service.height()',
735
+ '[attr.dir]': 'dirOverride() ?? null',
736
+ }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;position:relative;contain:layout style;min-height:1px}.ngx-grid-layout__surface{position:relative;width:100%;height:100%;min-height:inherit}.ngx-grid-layout__placeholder{display:none;position:absolute;box-sizing:border-box;pointer-events:none;z-index:0;border:2px dashed currentColor;border-radius:inherit;opacity:.45}.ngx-grid-layout__surface:before{content:\"\";position:absolute;inset:0;pointer-events:none;opacity:var(--grid-bg-opacity);border:var(--grid-border-width) solid var(--grid-border-color);box-sizing:border-box;transition:opacity .15s ease;background-image:repeating-linear-gradient(var(--grid-border-color) 0 var(--grid-border-width),var(--grid-row-color) var(--grid-border-width) calc(var(--grid-row) - var(--grid-border-width)),var(--grid-border-color) calc(var(--grid-row) - var(--grid-border-width)) calc(var(--grid-row)),var(--grid-gap-color) calc(var(--grid-row)) calc(var(--grid-row) + var(--grid-gap))),repeating-linear-gradient(90deg,var(--grid-border-color) 0 var(--grid-border-width),var(--grid-col-color) var(--grid-border-width) calc(100% - (var(--grid-border-width) + var(--grid-gap))),var(--grid-border-color) calc(100% - (var(--grid-border-width) + var(--grid-gap))) calc(100% - var(--grid-gap)),var(--grid-gap-color) calc(100% - var(--grid-gap)) 100%);background-size:calc((100% + var(--grid-gap)) / var(--grid-cols)) calc(var(--grid-row) + var(--grid-gap));background-position:0 0}\n"] }]
737
+ }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], editMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "editMode", required: false }] }], layoutChange: [{ type: i0.Output, args: ["layoutChange"] }], dir: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }] } });
738
+
739
+ class NgxGridItemComponent {
740
+ /** Bound as `[id]` in templates (matches the demo app's existing usage). */
741
+ itemId = input(undefined, { ...(ngDevMode ? { debugName: "itemId" } : /* istanbul ignore next */ {}), alias: 'id' });
742
+ config = input({ x: 0, y: 0, w: 1, h: 1 }, /* @ts-ignore */
743
+ ...(ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []));
744
+ layoutChange = output();
745
+ dragging = signal(false, /* @ts-ignore */
746
+ ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
747
+ resizing = signal(false, /* @ts-ignore */
748
+ ...(ngDevMode ? [{ debugName: "resizing" }] : /* istanbul ignore next */ []));
749
+ active = computed(() => this.dragging() || this.resizing(), /* @ts-ignore */
750
+ ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
751
+ transitionStyle = computed(() => !this.active() && this.service.options().animate
752
+ ? 'left .18s ease, top .18s ease, width .18s ease, height .18s ease'
753
+ : 'none', /* @ts-ignore */
754
+ ...(ngDevMode ? [{ debugName: "transitionStyle" }] : /* istanbul ignore next */ []));
755
+ interactive = computed(() => {
756
+ const id = this.itemId();
757
+ const item = id ? this.service.items().find((x) => x.id === id) : undefined;
758
+ return !!item && (this.service.isItemDraggable(item) || this.service.isItemResizable(item));
759
+ }, /* @ts-ignore */
760
+ ...(ngDevMode ? [{ debugName: "interactive" }] : /* istanbul ignore next */ []));
761
+ service = inject(GridLayoutService);
762
+ el = inject((ElementRef));
763
+ drag = inject(NgxDraggable);
764
+ resize = inject(NgxResizable);
765
+ resizable = computed(() => {
766
+ return !this.resize.disabled;
767
+ }, /* @ts-ignore */
768
+ ...(ngDevMode ? [{ debugName: "resizable" }] : /* istanbul ignore next */ []));
769
+ constructor() {
770
+ // `takeUntilDestroyed()` releases these subscriptions on destroy — the
771
+ // event for every grid item ever created (real damage in layouts where
772
+ // items are added/removed dynamically, e.g. via *ngFor + trackBy).
773
+ this.drag.dragStart.pipe(takeUntilDestroyed()).subscribe(() => this.onDragStart());
774
+ this.drag.dragMove.pipe(takeUntilDestroyed()).subscribe(() => this.onDragMove());
775
+ this.drag.dragEnd.pipe(takeUntilDestroyed()).subscribe(() => this.onDragEnd());
776
+ this.resize.resizeStart.pipe(takeUntilDestroyed()).subscribe(() => this.onResizeStart());
777
+ this.resize.resizeMove.pipe(takeUntilDestroyed()).subscribe((v) => this.onResizeMove(v));
778
+ this.resize.resizeEnd.pipe(takeUntilDestroyed()).subscribe(() => this.onResizeEnd());
779
+ // Keep the hosted directives in sync with per-item state: static items and
780
+ // items explicitly marked non-draggable/non-resizable stop reacting entirely.
781
+ effect(() => {
782
+ const id = this.itemId();
783
+ const item = id ? this.service.items().find((x) => x.id === id) : undefined;
784
+ this.drag.disabled = !item || !this.service.isItemDraggable(item);
785
+ this.resize.disabled = !item || !this.service.isItemResizable(item);
786
+ if (item)
787
+ this.applySizeConstraints(item.config);
788
+ });
789
+ }
790
+ ngOnInit() {
791
+ const id = this.itemId();
792
+ if (!id)
793
+ return;
794
+ this.service.registerItem({
795
+ id,
796
+ config: this.config(),
797
+ element: this.el.nativeElement,
798
+ });
799
+ }
800
+ ngOnDestroy() {
801
+ const id = this.itemId();
802
+ if (id)
803
+ this.service.unregisterItem(id);
804
+ }
805
+ onDragStart = () => {
806
+ this.dragging.set(true);
807
+ const id = this.itemId();
808
+ if (id)
809
+ this.service.begin(id);
810
+ };
811
+ onDragMove = () => {
812
+ const id = this.itemId();
813
+ if (id)
814
+ this.service.move(id);
815
+ };
816
+ onDragEnd = () => {
817
+ this.dragging.set(false);
818
+ this.finishInteraction();
819
+ };
820
+ onResizeStart = () => {
821
+ this.resizing.set(true);
822
+ const id = this.itemId();
823
+ if (id)
824
+ this.service.begin(id);
825
+ };
826
+ onResizeMove = (v) => {
827
+ const id = this.itemId();
828
+ if (id)
829
+ this.service.resize(id, v);
830
+ };
831
+ onResizeEnd = () => {
832
+ this.resizing.set(false);
833
+ this.finishInteraction();
834
+ };
835
+ /** Arrow keys move the item by one cell; Shift+arrow resizes it by one cell. */
836
+ onKeyDown(e) {
837
+ const id = this.itemId();
838
+ if (!id || !this.interactive())
839
+ return;
840
+ const item = this.service.items().find((x) => x.id === id);
841
+ if (!item)
842
+ return;
843
+ const deltas = {
844
+ ArrowLeft: [-1, 0],
845
+ ArrowRight: [1, 0],
846
+ ArrowUp: [0, -1],
847
+ ArrowDown: [0, 1],
848
+ };
849
+ const delta = deltas[e.key];
850
+ if (!delta)
851
+ return;
852
+ e.preventDefault();
853
+ if (e.shiftKey && this.service.isItemResizable(item)) {
854
+ this.service.resizeByCells(id, delta[0], delta[1]);
855
+ }
856
+ else if (this.service.isItemDraggable(item)) {
857
+ this.service.moveByCells(id, delta[0], delta[1]);
858
+ }
859
+ this.layoutChange.emit(this.service.items().find((x) => x.id === id)?.config ?? this.config());
860
+ }
861
+ finishInteraction() {
862
+ const id = this.itemId();
863
+ if (!id)
864
+ return;
865
+ this.service.end(id);
866
+ this.layoutChange.emit(this.service.items().find((x) => x.id === id)?.config ?? this.config());
867
+ }
868
+ /** Converts the item's min/max cell counts (config) into px for the resize directive. */
869
+ applySizeConstraints(config) {
870
+ const cellPx = this.el.nativeElement.getBoundingClientRect();
871
+ const cols = Math.max(1, config.w);
872
+ const rows = Math.max(1, config.h);
873
+ const colPx = cellPx.width / cols || 1;
874
+ const rowPx = cellPx.height / rows || 1;
875
+ this.resize.minWidth = (config.minW ?? 1) * colPx;
876
+ this.resize.minHeight = (config.minH ?? 1) * rowPx;
877
+ this.resize.maxWidth = config.maxW ? config.maxW * colPx : Infinity;
878
+ this.resize.maxHeight = config.maxH ? config.maxH * rowPx : Infinity;
879
+ }
880
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxGridItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
881
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.4", type: NgxGridItemComponent, isStandalone: true, selector: "ngx-grid-item", inputs: { itemId: { classPropertyName: "itemId", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { layoutChange: "layoutChange" }, host: { listeners: { "keydown": "onKeyDown($event)" }, properties: { "style.position": "\"absolute\"", "style.display": "\"block\"", "style.boxSizing": "\"border-box\"", "style.transition": "transitionStyle()", "class.ngx-grid-item--dragging": "dragging()", "class.ngx-grid-item--resizing": "resizing()", "class.ngx-grid-item--static": "config().static", "class.with-resize-handler": "resizable()", "attr.tabindex": "interactive() ? 0 : null", "attr.aria-grabbed": "dragging()", "attr.data-grid-id": "itemId()" } }, hostDirectives: [{ directive: i1.NgxDraggable, inputs: ["dragHandle", "dragHandle", "lockAxis", "lockAxis"] }, { directive: i1.NgxResizable }], ngImport: i0, template: `
882
+ <ng-content />
883
+ `, isInline: true, styles: [":host{display:block}:host.with-resize-handler:after{position:absolute;pointer-events:none;outline:1px #2196f3 solid;outline-offset:-5px;background:linear-gradient(#2196f3,#2196f3) 0 0/10px 10px no-repeat,linear-gradient(#2196f3,#2196f3) 100% 0/10px 10px no-repeat,linear-gradient(#2196f3,#2196f3) 0 100%/10px 10px no-repeat,linear-gradient(#2196f3,#2196f3) 100% 100%/10px 10px no-repeat,linear-gradient(#2196f3,#2196f3) 50% 0/10px 10px no-repeat,linear-gradient(#2196f3,#2196f3) 50% 100%/10px 10px no-repeat,linear-gradient(#2196f3,#2196f3) 0 50%/10px 10px no-repeat,linear-gradient(#2196f3,#2196f3) 100% 50%/10px 10px no-repeat;background-repeat:no-repeat;transition:none;inset:-2.5px;opacity:0;animation:showResizeHandle .3s forwards}:host:hover:after{opacity:1;content:\" \"}@keyframes showResizeHandle{0%{opacity:0}to{opacity:1}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
884
+ }
885
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxGridItemComponent, decorators: [{
886
+ type: Component,
887
+ args: [{ selector: 'ngx-grid-item', standalone: true, hostDirectives: [
888
+ { directive: NgxDraggable, inputs: ['dragHandle', 'lockAxis'], outputs: [] },
889
+ NgxResizable,
890
+ ], template: `
891
+ <ng-content />
892
+ `, host: {
893
+ '[style.position]': '"absolute"',
894
+ '[style.display]': '"block"',
895
+ '[style.boxSizing]': '"border-box"',
896
+ '[style.transition]': 'transitionStyle()',
897
+ '[class.ngx-grid-item--dragging]': 'dragging()',
898
+ '[class.ngx-grid-item--resizing]': 'resizing()',
899
+ '[class.ngx-grid-item--static]': 'config().static',
900
+ '[class.with-resize-handler]': 'resizable()',
901
+ '[attr.tabindex]': 'interactive() ? 0 : null',
902
+ '[attr.aria-grabbed]': 'dragging()',
903
+ '[attr.data-grid-id]': 'itemId()',
904
+ '(keydown)': 'onKeyDown($event)',
905
+ }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block}:host.with-resize-handler:after{position:absolute;pointer-events:none;outline:1px #2196f3 solid;outline-offset:-5px;background:linear-gradient(#2196f3,#2196f3) 0 0/10px 10px no-repeat,linear-gradient(#2196f3,#2196f3) 100% 0/10px 10px no-repeat,linear-gradient(#2196f3,#2196f3) 0 100%/10px 10px no-repeat,linear-gradient(#2196f3,#2196f3) 100% 100%/10px 10px no-repeat,linear-gradient(#2196f3,#2196f3) 50% 0/10px 10px no-repeat,linear-gradient(#2196f3,#2196f3) 50% 100%/10px 10px no-repeat,linear-gradient(#2196f3,#2196f3) 0 50%/10px 10px no-repeat,linear-gradient(#2196f3,#2196f3) 100% 50%/10px 10px no-repeat;background-repeat:no-repeat;transition:none;inset:-2.5px;opacity:0;animation:showResizeHandle .3s forwards}:host:hover:after{opacity:1;content:\" \"}@keyframes showResizeHandle{0%{opacity:0}to{opacity:1}}\n"] }]
906
+ }], ctorParameters: () => [], propDecorators: { itemId: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], layoutChange: [{ type: i0.Output, args: ["layoutChange"] }] } });
907
+
908
+ class NgxGridLayoutModule {
909
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxGridLayoutModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
910
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.4", ngImport: i0, type: NgxGridLayoutModule, imports: [NgxGridLayoutComponent, NgxGridItemComponent], exports: [NgxGridLayoutComponent, NgxGridItemComponent] });
911
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxGridLayoutModule, providers: [GridLayoutService] });
912
+ }
913
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxGridLayoutModule, decorators: [{
914
+ type: NgModule,
915
+ args: [{
916
+ imports: [NgxGridLayoutComponent, NgxGridItemComponent],
917
+ exports: [NgxGridLayoutComponent, NgxGridItemComponent],
918
+ providers: [GridLayoutService],
919
+ }]
920
+ }] });
921
+
922
+ /**
923
+ * Generated bundle index. Do not edit.
924
+ */
925
+
926
+ export { GridItemConfig, GridLayoutOptions, GridLayoutService, NgxGridItemComponent, NgxGridLayoutComponent, NgxGridLayoutModule, colToLeft, collides, compact, computeMetrics, getAllCollisions, getFirstCollision, gridItemConfigsEqual, leftToCol, maxOccupiedRow, moveItem, normalizeGridItem, placeItem, rowToTop, spanToHeightPx, spanToWidthPx, topToRow, trySwap };
927
+ //# sourceMappingURL=ngx-kit-grid-layout.mjs.map