ngx-kit 6.2.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.
- package/README.md +3 -3
- package/angle-selector/README.md +34 -34
- package/box-shadow/README.md +33 -33
- package/color-picker/README.md +52 -49
- package/data-table/README.md +120 -113
- package/date-picker/README.md +74 -69
- package/dialog/README.md +92 -92
- package/drag-resize/README.md +55 -0
- package/drawer-menu/README.md +125 -77
- package/dropzone/README.md +422 -422
- package/fesm2022/ngx-kit-angle-selector.mjs +2 -2
- package/fesm2022/ngx-kit-angle-selector.mjs.map +1 -1
- package/fesm2022/ngx-kit-box-shadow.mjs +2 -2
- package/fesm2022/ngx-kit-box-shadow.mjs.map +1 -1
- package/fesm2022/ngx-kit-color-picker.mjs +265 -73
- package/fesm2022/ngx-kit-color-picker.mjs.map +1 -1
- package/fesm2022/ngx-kit-data-table.mjs +2 -2
- package/fesm2022/ngx-kit-data-table.mjs.map +1 -1
- package/fesm2022/ngx-kit-date-picker.mjs +11 -11
- package/fesm2022/ngx-kit-date-picker.mjs.map +1 -1
- package/fesm2022/ngx-kit-dialog.mjs +3 -3
- package/fesm2022/ngx-kit-dialog.mjs.map +1 -1
- package/fesm2022/ngx-kit-drag-resize.mjs +1664 -0
- package/fesm2022/ngx-kit-drag-resize.mjs.map +1 -0
- package/fesm2022/ngx-kit-drawer-menu.mjs +424 -158
- package/fesm2022/ngx-kit-drawer-menu.mjs.map +1 -1
- package/fesm2022/ngx-kit-dropzone.mjs +2 -2
- package/fesm2022/ngx-kit-dropzone.mjs.map +1 -1
- package/fesm2022/ngx-kit-form-field.mjs +3 -5
- package/fesm2022/ngx-kit-form-field.mjs.map +1 -1
- package/fesm2022/ngx-kit-gallery.mjs +2 -2
- package/fesm2022/ngx-kit-gallery.mjs.map +1 -1
- package/fesm2022/ngx-kit-gradient-picker.mjs.map +1 -1
- package/fesm2022/ngx-kit-grid-layout.mjs +927 -0
- package/fesm2022/ngx-kit-grid-layout.mjs.map +1 -0
- package/fesm2022/ngx-kit-image-editor.mjs +2 -2
- package/fesm2022/ngx-kit-image-editor.mjs.map +1 -1
- package/fesm2022/ngx-kit-menu.mjs +8 -8
- package/fesm2022/ngx-kit-menu.mjs.map +1 -1
- package/fesm2022/ngx-kit-message.mjs +2 -2
- package/fesm2022/ngx-kit-message.mjs.map +1 -1
- package/fesm2022/ngx-kit-notify.mjs +2 -2
- package/fesm2022/ngx-kit-notify.mjs.map +1 -1
- package/fesm2022/ngx-kit-shared.mjs +54 -3
- package/fesm2022/ngx-kit-shared.mjs.map +1 -1
- package/fesm2022/ngx-kit-time-picker.mjs +2 -2
- package/fesm2022/ngx-kit-time-picker.mjs.map +1 -1
- package/fesm2022/ngx-kit-virtual-scroll.mjs +392 -0
- package/fesm2022/ngx-kit-virtual-scroll.mjs.map +1 -0
- package/fesm2022/ngx-kit-webflow.mjs +709 -0
- package/fesm2022/ngx-kit-webflow.mjs.map +1 -0
- package/gallery/README.md +71 -71
- package/gradient-picker/README.md +41 -41
- package/grid-layout/README.md +64 -0
- package/image-editor/README.md +12 -12
- package/menu/README.md +85 -85
- package/message/README.md +70 -70
- package/notify/README.md +56 -58
- package/package.json +17 -1
- package/shared/README.md +117 -117
- package/time-picker/README.md +89 -95
- package/types/ngx-kit-color-picker.d.ts +72 -16
- package/types/ngx-kit-date-picker.d.ts +2 -1
- package/types/ngx-kit-drag-resize.d.ts +368 -0
- package/types/ngx-kit-drawer-menu.d.ts +148 -88
- package/types/ngx-kit-grid-layout.d.ts +310 -0
- package/types/ngx-kit-image-editor.d.ts +1 -1
- package/types/ngx-kit-shared.d.ts +12 -1
- package/types/ngx-kit-virtual-scroll.d.ts +179 -0
- package/types/ngx-kit-webflow.d.ts +286 -0
- 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
|