@dragonworks/ngx-dashboard 20.0.4 → 20.0.6

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 (78) hide show
  1. package/ng-package.json +7 -0
  2. package/package.json +34 -45
  3. package/src/lib/__tests__/dashboard-component-widget-state-integration.spec.ts +537 -0
  4. package/src/lib/cell/__tests__/cell-resize.component.spec.ts +442 -0
  5. package/src/lib/cell/__tests__/cell.component.spec.ts +541 -0
  6. package/src/lib/cell/cell-context-menu.component.ts +138 -0
  7. package/src/lib/cell/cell-context-menu.service.ts +36 -0
  8. package/src/lib/cell/cell.component.html +37 -0
  9. package/src/lib/cell/cell.component.scss +198 -0
  10. package/src/lib/cell/cell.component.ts +375 -0
  11. package/src/lib/dashboard/dashboard.component.html +18 -0
  12. package/src/lib/dashboard/dashboard.component.scss +17 -0
  13. package/src/lib/dashboard/dashboard.component.ts +187 -0
  14. package/src/lib/dashboard-editor/dashboard-editor.component.html +57 -0
  15. package/src/lib/dashboard-editor/dashboard-editor.component.scss +87 -0
  16. package/src/lib/dashboard-editor/dashboard-editor.component.ts +219 -0
  17. package/src/lib/dashboard-viewer/__tests__/dashboard-viewer.component.spec.ts +258 -0
  18. package/src/lib/dashboard-viewer/dashboard-viewer.component.html +20 -0
  19. package/src/lib/dashboard-viewer/dashboard-viewer.component.scss +50 -0
  20. package/src/lib/dashboard-viewer/dashboard-viewer.component.ts +70 -0
  21. package/src/lib/drop-zone/__tests__/drop-zone.component.spec.ts +465 -0
  22. package/src/lib/drop-zone/drop-zone.component.html +20 -0
  23. package/src/lib/drop-zone/drop-zone.component.scss +67 -0
  24. package/src/lib/drop-zone/drop-zone.component.ts +122 -0
  25. package/src/lib/internal-widgets/unknown-widget/unknown-widget.component.ts +72 -0
  26. package/src/lib/models/cell-data.ts +13 -0
  27. package/src/lib/models/cell-dialog.ts +7 -0
  28. package/src/lib/models/cell-id.ts +85 -0
  29. package/src/lib/models/cell-position.ts +15 -0
  30. package/src/lib/models/dashboard-data.dto.ts +44 -0
  31. package/src/lib/models/dashboard-data.utils.ts +49 -0
  32. package/src/lib/models/drag-data.ts +6 -0
  33. package/src/lib/models/index.ts +11 -0
  34. package/src/lib/models/reserved-space.ts +24 -0
  35. package/src/lib/models/widget-factory.ts +33 -0
  36. package/src/lib/models/widget-id.ts +70 -0
  37. package/src/lib/models/widget.ts +21 -0
  38. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.component.ts +127 -0
  39. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.provider.ts +15 -0
  40. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.tokens.ts +20 -0
  41. package/src/lib/providers/cell-settings-dialog/default-cell-settings-dialog.provider.ts +32 -0
  42. package/src/lib/providers/cell-settings-dialog/index.ts +3 -0
  43. package/src/lib/providers/index.ts +1 -0
  44. package/src/lib/services/__tests__/dashboard-bridge.service.spec.ts +220 -0
  45. package/src/lib/services/__tests__/dashboard-viewport.service.spec.ts +362 -0
  46. package/src/lib/services/dashboard-bridge.service.ts +155 -0
  47. package/src/lib/services/dashboard-viewport.service.ts +148 -0
  48. package/src/lib/services/dashboard.service.ts +62 -0
  49. package/src/lib/store/__tests__/dashboard-store-collision-detection.spec.ts +756 -0
  50. package/src/lib/store/__tests__/dashboard-store-computed-properties.spec.ts +974 -0
  51. package/src/lib/store/__tests__/dashboard-store-drag-drop.spec.ts +279 -0
  52. package/src/lib/store/__tests__/dashboard-store-export-import.spec.ts +780 -0
  53. package/src/lib/store/__tests__/dashboard-store-grid-config.spec.ts +128 -0
  54. package/src/lib/store/__tests__/dashboard-store-query-methods.spec.ts +229 -0
  55. package/src/lib/store/__tests__/dashboard-store-resize-operations.spec.ts +652 -0
  56. package/src/lib/store/__tests__/dashboard-store-widget-management.spec.ts +461 -0
  57. package/src/lib/store/__tests__/dashboard-store-widget-state-preservation.spec.ts +369 -0
  58. package/src/lib/store/dashboard-store.ts +239 -0
  59. package/src/lib/store/features/drag-drop.feature.ts +140 -0
  60. package/src/lib/store/features/grid-config.feature.ts +43 -0
  61. package/src/lib/store/features/resize.feature.ts +140 -0
  62. package/src/lib/store/features/utils/collision.utils.ts +89 -0
  63. package/src/lib/store/features/utils/grid-query-internal.utils.ts +37 -0
  64. package/src/lib/store/features/utils/resize.utils.ts +165 -0
  65. package/src/lib/store/features/widget-management.feature.ts +158 -0
  66. package/src/lib/styles/_dashboard-grid-vars.scss +11 -0
  67. package/src/lib/widget-list/__tests__/widget-list-bridge-integration.spec.ts +137 -0
  68. package/src/lib/widget-list/widget-list.component.html +22 -0
  69. package/src/lib/widget-list/widget-list.component.scss +154 -0
  70. package/src/lib/widget-list/widget-list.component.ts +106 -0
  71. package/src/public-api.ts +21 -0
  72. package/src/test-setup.ts +10 -0
  73. package/tsconfig.lib.json +15 -0
  74. package/tsconfig.lib.prod.json +11 -0
  75. package/tsconfig.spec.json +14 -0
  76. package/fesm2022/dragonworks-ngx-dashboard.mjs +0 -2192
  77. package/fesm2022/dragonworks-ngx-dashboard.mjs.map +0 -1
  78. package/index.d.ts +0 -678
@@ -1,2192 +0,0 @@
1
- import { CommonModule, isPlatformBrowser } from '@angular/common';
2
- import * as i0 from '@angular/core';
3
- import { signal, computed, ChangeDetectionStrategy, Component, Injectable, inject, Inject, InjectionToken, input, model, output, viewChild, ViewContainerRef, DestroyRef, Renderer2, ElementRef, effect, viewChildren, afterNextRender, PLATFORM_ID, untracked } from '@angular/core';
4
- import { signalStoreFeature, withState, withMethods, patchState, withComputed, signalStore, withProps } from '@ngrx/signals';
5
- import * as i1 from '@angular/material/icon';
6
- import { MatIconModule } from '@angular/material/icon';
7
- import * as i2 from '@angular/material/tooltip';
8
- import { MatTooltipModule } from '@angular/material/tooltip';
9
- import * as i1$1 from '@angular/material/dialog';
10
- import { MAT_DIALOG_DATA, MatDialogModule, MatDialog } from '@angular/material/dialog';
11
- import { firstValueFrom } from 'rxjs';
12
- import * as i2$1 from '@angular/forms';
13
- import { FormsModule } from '@angular/forms';
14
- import * as i3 from '@angular/material/button';
15
- import { MatButtonModule } from '@angular/material/button';
16
- import * as i4 from '@angular/material/radio';
17
- import { MatRadioModule } from '@angular/material/radio';
18
- import * as i1$2 from '@angular/material/menu';
19
- import { MatMenuTrigger, MatMenuModule } from '@angular/material/menu';
20
- import * as i3$1 from '@angular/material/divider';
21
- import { MatDividerModule } from '@angular/material/divider';
22
- import { DomSanitizer } from '@angular/platform-browser';
23
-
24
- /**
25
- * Maximum number of columns supported by the grid.
26
- * This determines the encoding scheme for cell coordinates.
27
- */
28
- const MAX_COLUMNS = 1024;
29
- /**
30
- * Utility functions for working with CellId branded type.
31
- */
32
- const CellIdUtils = {
33
- /**
34
- * Creates a CellId from row and column coordinates.
35
- * @param row - The row number (1-based)
36
- * @param col - The column number (1-based)
37
- * @returns A branded CellId that encodes both coordinates
38
- * @throws Error if row or col is less than 1, or if col exceeds MAX_COLUMNS
39
- */
40
- create(row, col) {
41
- if (row < 1 || col < 1) {
42
- throw new Error(`Invalid cell coordinates: row=${row}, col=${col}. Both must be >= 1`);
43
- }
44
- if (col >= MAX_COLUMNS) {
45
- throw new Error(`Column ${col} exceeds maximum of ${MAX_COLUMNS - 1}`);
46
- }
47
- return (row * MAX_COLUMNS + col);
48
- },
49
- /**
50
- * Decodes a CellId back into row and column coordinates.
51
- * @param cellId - The CellId to decode
52
- * @returns A tuple of [row, col] coordinates (1-based)
53
- */
54
- decode(cellId) {
55
- const id = cellId;
56
- const row = Math.floor(id / MAX_COLUMNS);
57
- const col = id % MAX_COLUMNS;
58
- return [row, col];
59
- },
60
- /**
61
- * Gets the row coordinate from a CellId.
62
- * @param cellId - The CellId to extract row from
63
- * @returns The row number (1-based)
64
- */
65
- getRow(cellId) {
66
- return Math.floor(cellId / MAX_COLUMNS);
67
- },
68
- /**
69
- * Gets the column coordinate from a CellId.
70
- * @param cellId - The CellId to extract column from
71
- * @returns The column number (1-based)
72
- */
73
- getCol(cellId) {
74
- return cellId % MAX_COLUMNS;
75
- },
76
- /**
77
- * Creates a string representation of a CellId for debugging/display purposes.
78
- * @param cellId - The CellId to convert to string
79
- * @returns A string in the format "row-col"
80
- */
81
- toString(cellId) {
82
- const [row, col] = this.decode(cellId);
83
- return `${row}-${col}`;
84
- },
85
- /**
86
- * Checks if two CellIds are equal.
87
- * @param a - First CellId
88
- * @param b - Second CellId
89
- * @returns True if the CellIds represent the same cell
90
- */
91
- equals(a, b) {
92
- return a === b;
93
- },
94
- };
95
-
96
- /**
97
- * Creates an empty dashboard configuration with the specified dimensions.
98
- * This is a convenience function for creating a basic dashboard without any cells.
99
- *
100
- * @param dashboardId - Unique identifier for the dashboard (managed by client)
101
- * @param rows - Number of rows in the dashboard grid
102
- * @param columns - Number of columns in the dashboard grid
103
- * @param gutterSize - CSS size for the gutter between cells (default: '0.5em')
104
- * @returns A DashboardDataDto configured with the specified dimensions and no cells
105
- *
106
- * @example
107
- * // Create an 8x16 dashboard with default gutter
108
- * const dashboard = createEmptyDashboard('my-dashboard-1', 8, 16);
109
- *
110
- * @example
111
- * // Create a 5x10 dashboard with custom gutter
112
- * const dashboard = createEmptyDashboard('my-dashboard-2', 5, 10, '0.5rem');
113
- */
114
- function createEmptyDashboard(dashboardId, rows, columns, gutterSize = '0.5em') {
115
- return {
116
- version: '1.0.0',
117
- dashboardId,
118
- rows,
119
- columns,
120
- gutterSize,
121
- cells: [],
122
- };
123
- }
124
- /**
125
- * Creates a default dashboard configuration with standard dimensions.
126
- * This provides a reasonable starting point for most use cases.
127
- *
128
- * @param dashboardId - Unique identifier for the dashboard (managed by client)
129
- * @returns A DashboardDataDto with 8 rows, 16 columns, and 0.5em gutter
130
- *
131
- * @example
132
- * const dashboard = createDefaultDashboard('my-dashboard-id');
133
- */
134
- function createDefaultDashboard(dashboardId) {
135
- return createEmptyDashboard(dashboardId, 8, 16, '0.5em');
136
- }
137
-
138
- /**
139
- * Default reserved space when none is specified
140
- */
141
- const DEFAULT_RESERVED_SPACE = {
142
- top: 0,
143
- right: 0,
144
- bottom: 0,
145
- left: 0
146
- };
147
-
148
- function createFactoryFromComponent(component) {
149
- return {
150
- ...component.metadata,
151
- createInstance(container, state) {
152
- const ref = container.createComponent(component);
153
- ref.instance.dashboardSetState?.(state);
154
- return ref;
155
- },
156
- };
157
- }
158
-
159
- class UnknownWidgetComponent {
160
- static metadata = {
161
- widgetTypeid: '__internal/unknown-widget',
162
- name: 'Unknown Widget',
163
- description: 'Fallback widget for unknown widget types',
164
- svgIcon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M480-280q17 0 28.5-11.5T520-320q0-17-11.5-28.5T480-360q-17 0-28.5 11.5T440-320q0 17 11.5 28.5T480-280Zm-40-160h80v-240h-80v240Zm40 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/></svg>',
165
- };
166
- state = signal({
167
- originalWidgetTypeid: 'unknown',
168
- });
169
- tooltipText = computed(() => `${this.state().originalWidgetTypeid}`);
170
- dashboardSetState(state) {
171
- if (state && typeof state === 'object' && 'originalWidgetTypeid' in state) {
172
- this.state.set(state);
173
- }
174
- }
175
- dashboardGetState() {
176
- return this.state();
177
- }
178
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: UnknownWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
179
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.6", type: UnknownWidgetComponent, isStandalone: true, selector: "lib-unknown-widget", ngImport: i0, template: `
180
- <div class="unknown-widget-container" [matTooltip]="tooltipText()">
181
- <mat-icon class="unknown-widget-icon">error_outline</mat-icon>
182
- </div>
183
- `, isInline: true, styles: [".unknown-widget-container{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background-color:var(--mat-sys-error);border-radius:8px;container-type:size}.unknown-widget-icon{color:var(--mat-sys-on-error);font-size:clamp(12px,75cqmin,68px);width:clamp(12px,75cqmin,68px);height:clamp(12px,75cqmin,68px)}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
184
- }
185
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: UnknownWidgetComponent, decorators: [{
186
- type: Component,
187
- args: [{ selector: 'lib-unknown-widget', imports: [MatIconModule, MatTooltipModule], template: `
188
- <div class="unknown-widget-container" [matTooltip]="tooltipText()">
189
- <mat-icon class="unknown-widget-icon">error_outline</mat-icon>
190
- </div>
191
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".unknown-widget-container{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background-color:var(--mat-sys-error);border-radius:8px;container-type:size}.unknown-widget-icon{color:var(--mat-sys-on-error);font-size:clamp(12px,75cqmin,68px);width:clamp(12px,75cqmin,68px);height:clamp(12px,75cqmin,68px)}\n"] }]
192
- }] });
193
-
194
- // dashboard.service.ts
195
- class DashboardService {
196
- #widgetTypes = signal([]);
197
- #widgetFactoryMap = new Map();
198
- #unknownWidgetFactory = createFactoryFromComponent(UnknownWidgetComponent);
199
- widgetTypes = this.#widgetTypes.asReadonly(); // make the widget list available as a readonly signal
200
- registerWidgetType(widget) {
201
- if (this.#widgetTypes().some((w) => w.metadata.widgetTypeid === widget.metadata.widgetTypeid)) {
202
- throw new Error(`Widget type '${widget.metadata.widgetTypeid}' is already registered`);
203
- }
204
- this.#widgetFactoryMap.set(widget.metadata.widgetTypeid, createFactoryFromComponent(widget));
205
- this.#widgetTypes.set([...this.#widgetTypes(), widget]);
206
- }
207
- getFactory(widgetTypeid) {
208
- const factory = this.#widgetFactoryMap.get(widgetTypeid);
209
- if (factory) {
210
- return factory;
211
- }
212
- // Return fallback factory for unknown widget types
213
- console.warn(`Unknown widget type: ${widgetTypeid}, using fallback error widget`);
214
- // Create a custom factory that preserves the original widget type ID in state
215
- return {
216
- ...this.#unknownWidgetFactory,
217
- createInstance: (container, state) => {
218
- const ref = this.#unknownWidgetFactory.createInstance(container, {
219
- originalWidgetTypeid: widgetTypeid,
220
- ...(state || {}),
221
- });
222
- return ref;
223
- },
224
- };
225
- }
226
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
227
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardService, providedIn: 'root' });
228
- }
229
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardService, decorators: [{
230
- type: Injectable,
231
- args: [{
232
- providedIn: 'root',
233
- }]
234
- }] });
235
-
236
- // Internal utility functions used by collision detection, resize logic, and tests
237
- const GridQueryInternalUtils = {
238
- isCellOccupied(cells, row, col, excludeId) {
239
- return cells.some((cell) => {
240
- // Skip checking against the cell being dragged
241
- if (excludeId && CellIdUtils.equals(cell.cellId, excludeId))
242
- return false;
243
- const endRow = cell.row + cell.rowSpan - 1;
244
- const endCol = cell.col + cell.colSpan - 1;
245
- return (cell.row <= row && row <= endRow && cell.col <= col && col <= endCol);
246
- });
247
- },
248
- isOutOfBounds(targetRow, targetCol, spanRow, spanCol, maxRows, maxColumns) {
249
- const rowLimit = targetRow + spanRow - 1;
250
- const colLimit = targetCol + spanCol - 1;
251
- return rowLimit > maxRows || colLimit > maxColumns;
252
- },
253
- getCellAt(cells, row, col) {
254
- return cells.find((cell) => cell.row === row && cell.col === col) ?? null;
255
- },
256
- };
257
-
258
- function calculateCollisionInfo(dragData, hovered, cells, rows, columns) {
259
- if (!dragData || !hovered) {
260
- return {
261
- hasCollisions: false,
262
- invalidCells: [],
263
- outOfBounds: false,
264
- footprint: [],
265
- };
266
- }
267
- const isCell = dragData.kind === 'cell';
268
- const rowSpan = isCell ? dragData.content.rowSpan : 1;
269
- const colSpan = isCell ? dragData.content.colSpan : 1;
270
- // Check bounds
271
- const outOfBounds = GridQueryInternalUtils.isOutOfBounds(hovered.row, hovered.col, rowSpan, colSpan, rows, columns);
272
- // Generate footprint
273
- const footprint = [];
274
- for (let r = 0; r < rowSpan; r++) {
275
- for (let c = 0; c < colSpan; c++) {
276
- footprint.push({ row: hovered.row + r, col: hovered.col + c });
277
- }
278
- }
279
- const excludeId = isCell ? dragData.content.cellId : undefined;
280
- // Check for actual collisions with other widgets (not self)
281
- const hasCollisions = footprint.some((pos) => GridQueryInternalUtils.isCellOccupied(cells, pos.row, pos.col, excludeId));
282
- // Generate invalid cell IDs
283
- const invalidCells = [];
284
- if (hasCollisions || outOfBounds) {
285
- for (const pos of footprint) {
286
- invalidCells.push(CellIdUtils.create(pos.row, pos.col));
287
- }
288
- }
289
- return {
290
- hasCollisions,
291
- invalidCells,
292
- outOfBounds,
293
- footprint,
294
- };
295
- }
296
- function calculateHighlightedZones(dragData, hovered) {
297
- if (!dragData || !hovered)
298
- return [];
299
- const zones = [];
300
- const rowSpan = dragData.kind === 'cell' ? dragData.content.rowSpan : 1;
301
- const colSpan = dragData.kind === 'cell' ? dragData.content.colSpan : 1;
302
- for (let r = 0; r < rowSpan; r++) {
303
- for (let c = 0; c < colSpan; c++) {
304
- zones.push({ row: hovered.row + r, col: hovered.col + c });
305
- }
306
- }
307
- return zones;
308
- }
309
-
310
- const initialGridConfigState = {
311
- rows: 8,
312
- columns: 16,
313
- gutterSize: '0.5em',
314
- isEditMode: false,
315
- gridCellDimensions: { width: 0, height: 0 },
316
- };
317
- const withGridConfig = () => signalStoreFeature(withState(initialGridConfigState), withMethods((store) => ({
318
- setGridConfig(config) {
319
- patchState(store, config);
320
- },
321
- setGridCellDimensions(width, height) {
322
- patchState(store, { gridCellDimensions: { width, height } });
323
- },
324
- toggleEditMode() {
325
- patchState(store, { isEditMode: !store.isEditMode() });
326
- },
327
- setEditMode(isEditMode) {
328
- patchState(store, { isEditMode });
329
- },
330
- })));
331
-
332
- const initialWidgetManagementState = {
333
- cellsById: {},
334
- };
335
- const withWidgetManagement = () => signalStoreFeature(withState(initialWidgetManagementState),
336
- // Computed cells array - lazy evaluation, automatic memoization
337
- withComputed((store) => ({
338
- cells: computed(() => Object.values(store.cellsById())),
339
- })), withMethods((store) => ({
340
- addWidget(cell) {
341
- const cellKey = CellIdUtils.toString(cell.cellId);
342
- patchState(store, {
343
- cellsById: { ...store.cellsById(), [cellKey]: cell },
344
- });
345
- },
346
- removeWidget(cellId) {
347
- const cellKey = CellIdUtils.toString(cellId);
348
- const { [cellKey]: removed, ...remaining } = store.cellsById();
349
- patchState(store, { cellsById: remaining });
350
- },
351
- updateWidgetPosition(cellId, row, col) {
352
- const cellKey = CellIdUtils.toString(cellId);
353
- const existingCell = store.cellsById()[cellKey];
354
- if (existingCell) {
355
- patchState(store, {
356
- cellsById: {
357
- ...store.cellsById(),
358
- [cellKey]: { ...existingCell, row, col },
359
- },
360
- });
361
- }
362
- },
363
- createWidget(row, col, widgetFactory, widgetState) {
364
- const cellId = CellIdUtils.create(row, col);
365
- const cell = {
366
- cellId,
367
- row,
368
- col,
369
- rowSpan: 1,
370
- colSpan: 1,
371
- widgetFactory,
372
- widgetState,
373
- };
374
- const cellKey = CellIdUtils.toString(cellId);
375
- patchState(store, {
376
- cellsById: { ...store.cellsById(), [cellKey]: cell },
377
- });
378
- },
379
- updateCellSettings(id, flat) {
380
- const cellKey = CellIdUtils.toString(id);
381
- const existingCell = store.cellsById()[cellKey];
382
- if (existingCell) {
383
- patchState(store, {
384
- cellsById: {
385
- ...store.cellsById(),
386
- [cellKey]: { ...existingCell, flat },
387
- },
388
- });
389
- }
390
- },
391
- updateWidgetSpan(id, rowSpan, colSpan) {
392
- const cellKey = CellIdUtils.toString(id);
393
- const existingCell = store.cellsById()[cellKey];
394
- if (existingCell) {
395
- patchState(store, {
396
- cellsById: {
397
- ...store.cellsById(),
398
- [cellKey]: { ...existingCell, rowSpan, colSpan },
399
- },
400
- });
401
- }
402
- },
403
- updateWidgetState(cellId, widgetState) {
404
- const cellKey = CellIdUtils.toString(cellId);
405
- const existingCell = store.cellsById()[cellKey];
406
- if (existingCell) {
407
- patchState(store, {
408
- cellsById: {
409
- ...store.cellsById(),
410
- [cellKey]: { ...existingCell, widgetState },
411
- },
412
- });
413
- }
414
- },
415
- updateAllWidgetStates(widgetStates) {
416
- const updatedCellsById = { ...store.cellsById() };
417
- for (const [cellIdString, newState] of widgetStates) {
418
- const existingCell = updatedCellsById[cellIdString];
419
- if (existingCell) {
420
- updatedCellsById[cellIdString] = { ...existingCell, widgetState: newState };
421
- }
422
- }
423
- patchState(store, { cellsById: updatedCellsById });
424
- },
425
- clearDashboard() {
426
- patchState(store, { cellsById: {} });
427
- },
428
- })));
429
-
430
- const initialDragDropState = {
431
- dragData: null,
432
- hoveredDropZone: null,
433
- };
434
- const withDragDrop = () => signalStoreFeature(withState(initialDragDropState), withComputed((store) => ({
435
- // Highlighted zones during drag
436
- highlightedZones: computed(() => calculateHighlightedZones(store.dragData(), store.hoveredDropZone())),
437
- })), withComputed((store) => ({
438
- // Map for quick highlight lookup - reuse highlightedZones computation
439
- highlightMap: computed(() => {
440
- const zones = store.highlightedZones();
441
- const map = new Set();
442
- for (const z of zones) {
443
- map.add(CellIdUtils.create(z.row, z.col));
444
- }
445
- return map;
446
- }),
447
- })), withMethods((store) => ({
448
- syncDragState(dragData) {
449
- patchState(store, { dragData });
450
- },
451
- startDrag(dragData) {
452
- patchState(store, { dragData });
453
- },
454
- endDrag() {
455
- patchState(store, {
456
- dragData: null,
457
- hoveredDropZone: null,
458
- });
459
- },
460
- setHoveredDropZone(zone) {
461
- patchState(store, { hoveredDropZone: zone });
462
- },
463
- })),
464
- // Second withMethods block for drop handling that can access endDrag
465
- withMethods((store) => ({
466
- // Drop handling logic with dependency injection
467
- _handleDrop(dragData, targetPosition, dependencies) {
468
- // 1. Validate placement using existing collision detection
469
- const collisionInfo = calculateCollisionInfo(dragData, targetPosition, dependencies.cells, dependencies.rows, dependencies.columns);
470
- // 2. End drag state first
471
- store.endDrag();
472
- // 3. Early return if invalid placement
473
- if (collisionInfo.hasCollisions || collisionInfo.outOfBounds) {
474
- return false;
475
- }
476
- // 4. Handle widget creation from palette
477
- if (dragData.kind === 'widget') {
478
- const factory = dependencies.dashboardService.getFactory(dragData.content.widgetTypeid);
479
- dependencies.createWidget(targetPosition.row, targetPosition.col, factory, undefined);
480
- return true;
481
- }
482
- // 5. Handle cell movement
483
- if (dragData.kind === 'cell') {
484
- dependencies.updateWidgetPosition(dragData.content.cellId, targetPosition.row, targetPosition.col);
485
- return true;
486
- }
487
- return false;
488
- },
489
- })));
490
-
491
- function getMaxColSpan(cellId, row, col, cells, columns) {
492
- const currentCell = cells.find((c) => CellIdUtils.equals(c.cellId, cellId));
493
- if (!currentCell)
494
- return 1;
495
- // Start from current position and check each column until we hit a boundary or collision
496
- let maxSpan = 1;
497
- for (let testCol = col + 1; testCol <= columns; testCol++) {
498
- // Check if this column is free for all rows the widget spans
499
- let columnIsFree = true;
500
- for (let testRow = row; testRow < row + currentCell.rowSpan; testRow++) {
501
- const occupied = cells.some((cell) => {
502
- if (CellIdUtils.equals(cell.cellId, cellId))
503
- return false;
504
- const wStartCol = cell.col;
505
- const wEndCol = cell.col + cell.colSpan - 1;
506
- const wStartRow = cell.row;
507
- const wEndRow = cell.row + cell.rowSpan - 1;
508
- return (testCol >= wStartCol &&
509
- testCol <= wEndCol &&
510
- testRow >= wStartRow &&
511
- testRow <= wEndRow);
512
- });
513
- if (occupied) {
514
- columnIsFree = false;
515
- break;
516
- }
517
- }
518
- if (!columnIsFree) {
519
- break; // Hit a collision, stop here
520
- }
521
- maxSpan = testCol - col + 1; // Update max span to include this column
522
- }
523
- return maxSpan;
524
- }
525
- function getMaxRowSpan(cellId, row, col, cells, rows) {
526
- const currentCell = cells.find((c) => CellIdUtils.equals(c.cellId, cellId));
527
- if (!currentCell)
528
- return 1;
529
- // Start from current position and check each row until we hit a boundary or collision
530
- let maxSpan = 1;
531
- for (let testRow = row + 1; testRow <= rows; testRow++) {
532
- // Check if this row is free for all columns the widget spans
533
- let rowIsFree = true;
534
- for (let testCol = col; testCol < col + currentCell.colSpan; testCol++) {
535
- const occupied = cells.some((cell) => {
536
- if (CellIdUtils.equals(cell.cellId, cellId))
537
- return false;
538
- const wStartRow = cell.row;
539
- const wEndRow = cell.row + cell.rowSpan - 1;
540
- const wStartCol = cell.col;
541
- const wEndCol = cell.col + cell.colSpan - 1;
542
- return (testRow >= wStartRow &&
543
- testRow <= wEndRow &&
544
- testCol >= wStartCol &&
545
- testCol <= wEndCol);
546
- });
547
- if (occupied) {
548
- rowIsFree = false;
549
- break;
550
- }
551
- }
552
- if (!rowIsFree) {
553
- break; // Hit a collision, stop here
554
- }
555
- maxSpan = testRow - row + 1; // Update max span to include this row
556
- }
557
- return maxSpan;
558
- }
559
- function calculateResizePreview(resizeData, direction, delta, cells, rows, columns) {
560
- const cell = cells.find((c) => CellIdUtils.equals(c.cellId, resizeData.cellId));
561
- if (!cell)
562
- return null;
563
- if (direction === 'horizontal') {
564
- // Calculate the desired span based on the delta
565
- const desiredColSpan = Math.max(1, resizeData.originalColSpan + delta);
566
- // Get the maximum allowed span
567
- const maxColSpan = getMaxColSpan(cell.cellId, cell.row, cell.col, cells, columns);
568
- // Clamp to the maximum
569
- const newColSpan = Math.min(desiredColSpan, maxColSpan);
570
- return {
571
- rowSpan: resizeData.previewRowSpan,
572
- colSpan: newColSpan,
573
- };
574
- }
575
- else {
576
- // Calculate the desired span based on the delta
577
- const desiredRowSpan = Math.max(1, resizeData.originalRowSpan + delta);
578
- // Get the maximum allowed span
579
- const maxRowSpan = getMaxRowSpan(cell.cellId, cell.row, cell.col, cells, rows);
580
- // Clamp to the maximum
581
- const newRowSpan = Math.min(desiredRowSpan, maxRowSpan);
582
- return {
583
- rowSpan: newRowSpan,
584
- colSpan: resizeData.previewColSpan,
585
- };
586
- }
587
- }
588
-
589
- const initialResizeState = {
590
- resizeData: null,
591
- };
592
- // Utility functions for resize preview computations
593
- const ResizePreviewUtils = {
594
- computePreviewCells(resizeData, cells) {
595
- if (!resizeData)
596
- return [];
597
- const cell = cells.find((cell) => CellIdUtils.equals(cell.cellId, resizeData.cellId));
598
- if (!cell)
599
- return [];
600
- const previewCells = [];
601
- for (let r = 0; r < resizeData.previewRowSpan; r++) {
602
- for (let c = 0; c < resizeData.previewColSpan; c++) {
603
- previewCells.push({
604
- row: cell.row + r,
605
- col: cell.col + c,
606
- });
607
- }
608
- }
609
- return previewCells;
610
- },
611
- computePreviewMap(previewCells) {
612
- const map = new Set();
613
- for (const cell of previewCells) {
614
- map.add(CellIdUtils.create(cell.row, cell.col));
615
- }
616
- return map;
617
- },
618
- };
619
- const withResize = () => signalStoreFeature(withState(initialResizeState), withMethods((store) => ({
620
- // Resize methods that need cross-feature dependencies
621
- _startResize(cellId, dependencies) {
622
- const cell = dependencies.cells.find((c) => CellIdUtils.equals(c.cellId, cellId));
623
- if (!cell)
624
- return;
625
- patchState(store, {
626
- resizeData: {
627
- cellId,
628
- originalRowSpan: cell.rowSpan,
629
- originalColSpan: cell.colSpan,
630
- previewRowSpan: cell.rowSpan,
631
- previewColSpan: cell.colSpan,
632
- },
633
- });
634
- },
635
- _updateResizePreview(direction, delta, dependencies) {
636
- const resizeData = store.resizeData();
637
- if (!resizeData)
638
- return;
639
- const newSpans = calculateResizePreview(resizeData, direction, delta, dependencies.cells, dependencies.rows, dependencies.columns);
640
- if (newSpans) {
641
- patchState(store, {
642
- resizeData: {
643
- ...resizeData,
644
- previewRowSpan: newSpans.rowSpan,
645
- previewColSpan: newSpans.colSpan,
646
- },
647
- });
648
- }
649
- },
650
- _endResize(apply, dependencies) {
651
- const resizeData = store.resizeData();
652
- if (!resizeData)
653
- return;
654
- if (apply &&
655
- (resizeData.previewRowSpan !== resizeData.originalRowSpan ||
656
- resizeData.previewColSpan !== resizeData.originalColSpan)) {
657
- dependencies.updateWidgetSpan(resizeData.cellId, resizeData.previewRowSpan, resizeData.previewColSpan);
658
- }
659
- patchState(store, { resizeData: null });
660
- },
661
- })));
662
-
663
- const initialState = {
664
- dashboardId: '',
665
- };
666
- const DashboardStore = signalStore(withState(initialState), withProps(() => ({
667
- dashboardService: inject(DashboardService),
668
- })), withGridConfig(), withWidgetManagement(), withResize(), withDragDrop(),
669
- // Cross-feature computed properties (need access to multiple features)
670
- withComputed((store) => ({
671
- // Invalid zones (collision detection)
672
- invalidHighlightMap: computed(() => {
673
- const collisionInfo = calculateCollisionInfo(store.dragData(), store.hoveredDropZone(), store.cells(), store.rows(), store.columns());
674
- return new Set(collisionInfo.invalidCells);
675
- }),
676
- // Check if placement would be valid (for drop validation)
677
- isValidPlacement: computed(() => {
678
- const collisionInfo = calculateCollisionInfo(store.dragData(), store.hoveredDropZone(), store.cells(), store.rows(), store.columns());
679
- return !collisionInfo.hasCollisions && !collisionInfo.outOfBounds;
680
- }),
681
- })),
682
- // Cross-feature methods (need access to multiple features)
683
- withMethods((store) => ({
684
- // DROP HANDLING (delegate to drag-drop feature with dependency injection)
685
- handleDrop(dragData, targetPosition) {
686
- return store._handleDrop(dragData, targetPosition, {
687
- cells: store.cells(),
688
- rows: store.rows(),
689
- columns: store.columns(),
690
- dashboardService: store.dashboardService,
691
- createWidget: store.createWidget,
692
- updateWidgetPosition: store.updateWidgetPosition,
693
- });
694
- },
695
- // RESIZE METHODS (delegate to resize feature with dependency injection)
696
- startResize(cellId) {
697
- store._startResize(cellId, {
698
- cells: store.cells(),
699
- });
700
- },
701
- updateResizePreview(direction, delta) {
702
- store._updateResizePreview(direction, delta, {
703
- cells: store.cells(),
704
- rows: store.rows(),
705
- columns: store.columns(),
706
- });
707
- },
708
- endResize(apply) {
709
- store._endResize(apply, {
710
- updateWidgetSpan: store.updateWidgetSpan,
711
- });
712
- },
713
- // EXPORT/IMPORT METHODS (need access to multiple features)
714
- exportDashboard(getCurrentWidgetStates) {
715
- // Get live widget states if callback provided, otherwise use stored states
716
- const liveWidgetStates = getCurrentWidgetStates?.() || new Map();
717
- return {
718
- version: '1.0.0',
719
- dashboardId: store.dashboardId(),
720
- rows: store.rows(),
721
- columns: store.columns(),
722
- gutterSize: store.gutterSize(),
723
- cells: store.cells()
724
- .filter((cell) => cell.widgetFactory.widgetTypeid !== '__internal/unknown-widget')
725
- .map((cell) => {
726
- const cellIdString = CellIdUtils.toString(cell.cellId);
727
- const currentState = liveWidgetStates.get(cellIdString);
728
- return {
729
- row: cell.row,
730
- col: cell.col,
731
- rowSpan: cell.rowSpan,
732
- colSpan: cell.colSpan,
733
- flat: cell.flat,
734
- widgetTypeid: cell.widgetFactory.widgetTypeid,
735
- widgetState: currentState !== undefined ? currentState : cell.widgetState,
736
- };
737
- }),
738
- };
739
- },
740
- loadDashboard(data) {
741
- // Import full dashboard data with grid configuration
742
- const cellsById = {};
743
- data.cells.forEach((cellData) => {
744
- const factory = store.dashboardService.getFactory(cellData.widgetTypeid);
745
- const cell = {
746
- cellId: CellIdUtils.create(cellData.row, cellData.col),
747
- row: cellData.row,
748
- col: cellData.col,
749
- rowSpan: cellData.rowSpan,
750
- colSpan: cellData.colSpan,
751
- flat: cellData.flat,
752
- widgetFactory: factory,
753
- widgetState: cellData.widgetState,
754
- };
755
- cellsById[CellIdUtils.toString(cell.cellId)] = cell;
756
- });
757
- patchState(store, {
758
- dashboardId: data.dashboardId,
759
- rows: data.rows,
760
- columns: data.columns,
761
- gutterSize: data.gutterSize,
762
- cellsById,
763
- });
764
- },
765
- // INITIALIZATION METHODS
766
- initializeFromDto(dashboardData) {
767
- // Inline the loadDashboard logic since it's defined later in the same methods block
768
- const cellsById = {};
769
- dashboardData.cells.forEach((cellData) => {
770
- const factory = store.dashboardService.getFactory(cellData.widgetTypeid);
771
- const cell = {
772
- cellId: CellIdUtils.create(cellData.row, cellData.col),
773
- row: cellData.row,
774
- col: cellData.col,
775
- rowSpan: cellData.rowSpan,
776
- colSpan: cellData.colSpan,
777
- flat: cellData.flat,
778
- widgetFactory: factory,
779
- widgetState: cellData.widgetState,
780
- };
781
- cellsById[CellIdUtils.toString(cell.cellId)] = cell;
782
- });
783
- patchState(store, {
784
- dashboardId: dashboardData.dashboardId,
785
- rows: dashboardData.rows,
786
- columns: dashboardData.columns,
787
- gutterSize: dashboardData.gutterSize,
788
- cellsById,
789
- });
790
- },
791
- })),
792
- // Cross-feature computed properties that depend on resize + widget data (using utility functions)
793
- withComputed((store) => ({
794
- // Compute preview cells during resize using utility function
795
- resizePreviewCells: computed(() => {
796
- return ResizePreviewUtils.computePreviewCells(store.resizeData(), store.cells());
797
- }),
798
- })),
799
- // Second computed block that depends on the first
800
- withComputed((store) => ({
801
- // Map for resize preview highlighting using utility function
802
- resizePreviewMap: computed(() => {
803
- return ResizePreviewUtils.computePreviewMap(store.resizePreviewCells());
804
- }),
805
- })));
806
-
807
- /**
808
- * Abstract provider for cell settings dialogs.
809
- * Implement this to provide custom dialog solutions.
810
- */
811
- class CellSettingsDialogProvider {
812
- }
813
-
814
- class CellSettingsDialogComponent {
815
- data;
816
- dialogRef;
817
- selectedMode;
818
- currentMode;
819
- constructor(data, dialogRef) {
820
- this.data = data;
821
- this.dialogRef = dialogRef;
822
- this.currentMode = data.flat ? 'flat' : 'normal';
823
- this.selectedMode = this.currentMode;
824
- }
825
- onCancel() {
826
- this.dialogRef.close();
827
- }
828
- save() {
829
- const newData = {
830
- ...this.data,
831
- flat: this.selectedMode === 'flat'
832
- };
833
- this.dialogRef.close(newData);
834
- }
835
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: CellSettingsDialogComponent, deps: [{ token: MAT_DIALOG_DATA }, { token: i1$1.MatDialogRef }], target: i0.ɵɵFactoryTarget.Component });
836
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.6", type: CellSettingsDialogComponent, isStandalone: true, selector: "lib-cell-settings-dialog", ngImport: i0, template: `
837
- <h2 mat-dialog-title>Cell Display Settings</h2>
838
- <mat-dialog-content>
839
- <p class="cell-info">Cell ID: <strong>{{ data.id }}</strong></p>
840
-
841
- <div class="radio-group">
842
- <mat-radio-group [(ngModel)]="selectedMode" name="displayMode">
843
- <mat-radio-button value="normal">
844
- <div class="radio-option">
845
- <div class="option-title">Normal</div>
846
- <div class="option-description">Standard cell display with full content visibility</div>
847
- </div>
848
- </mat-radio-button>
849
-
850
- <mat-radio-button value="flat">
851
- <div class="radio-option">
852
- <div class="option-title">Flat</div>
853
- <div class="option-description">Simplified display with reduced visual emphasis</div>
854
- </div>
855
- </mat-radio-button>
856
- </mat-radio-group>
857
- </div>
858
- </mat-dialog-content>
859
- <mat-dialog-actions align="end">
860
- <button mat-button (click)="onCancel()">Cancel</button>
861
- <button
862
- mat-flat-button
863
- (click)="save()"
864
- [disabled]="selectedMode === currentMode">
865
- Apply
866
- </button>
867
- </mat-dialog-actions>
868
- `, isInline: true, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden;padding-top:.5rem}.cell-info{margin:0 0 1.5rem;padding-bottom:1rem}.radio-group{width:100%}mat-radio-group{display:block}mat-radio-button{width:100%;display:block;margin-bottom:1rem}mat-radio-button:last-child{margin-bottom:0}.radio-option{margin-left:.75rem;padding:.25rem 0}.option-title{display:block;margin-bottom:.25rem}.option-description{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1$1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1$1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1$1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatRadioModule }, { kind: "directive", type: i4.MatRadioGroup, selector: "mat-radio-group", inputs: ["color", "name", "labelPosition", "value", "selected", "disabled", "required", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioGroup"] }, { kind: "component", type: i4.MatRadioButton, selector: "mat-radio-button", inputs: ["id", "name", "aria-label", "aria-labelledby", "aria-describedby", "disableRipple", "tabIndex", "checked", "value", "labelPosition", "disabled", "required", "color", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioButton"] }] });
869
- }
870
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: CellSettingsDialogComponent, decorators: [{
871
- type: Component,
872
- args: [{ selector: 'lib-cell-settings-dialog', standalone: true, imports: [
873
- CommonModule,
874
- FormsModule,
875
- MatDialogModule,
876
- MatButtonModule,
877
- MatRadioModule,
878
- ], template: `
879
- <h2 mat-dialog-title>Cell Display Settings</h2>
880
- <mat-dialog-content>
881
- <p class="cell-info">Cell ID: <strong>{{ data.id }}</strong></p>
882
-
883
- <div class="radio-group">
884
- <mat-radio-group [(ngModel)]="selectedMode" name="displayMode">
885
- <mat-radio-button value="normal">
886
- <div class="radio-option">
887
- <div class="option-title">Normal</div>
888
- <div class="option-description">Standard cell display with full content visibility</div>
889
- </div>
890
- </mat-radio-button>
891
-
892
- <mat-radio-button value="flat">
893
- <div class="radio-option">
894
- <div class="option-title">Flat</div>
895
- <div class="option-description">Simplified display with reduced visual emphasis</div>
896
- </div>
897
- </mat-radio-button>
898
- </mat-radio-group>
899
- </div>
900
- </mat-dialog-content>
901
- <mat-dialog-actions align="end">
902
- <button mat-button (click)="onCancel()">Cancel</button>
903
- <button
904
- mat-flat-button
905
- (click)="save()"
906
- [disabled]="selectedMode === currentMode">
907
- Apply
908
- </button>
909
- </mat-dialog-actions>
910
- `, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden;padding-top:.5rem}.cell-info{margin:0 0 1.5rem;padding-bottom:1rem}.radio-group{width:100%}mat-radio-group{display:block}mat-radio-button{width:100%;display:block;margin-bottom:1rem}mat-radio-button:last-child{margin-bottom:0}.radio-option{margin-left:.75rem;padding:.25rem 0}.option-title{display:block;margin-bottom:.25rem}.option-description{display:block}\n"] }]
911
- }], ctorParameters: () => [{ type: undefined, decorators: [{
912
- type: Inject,
913
- args: [MAT_DIALOG_DATA]
914
- }] }, { type: i1$1.MatDialogRef }] });
915
-
916
- /**
917
- * Default cell dialog provider that uses Material Design dialogs.
918
- * Provides a modern, accessible dialog experience for cell settings.
919
- */
920
- class DefaultCellSettingsDialogProvider extends CellSettingsDialogProvider {
921
- dialog = inject(MatDialog);
922
- async openCellSettings(data) {
923
- const dialogRef = this.dialog.open(CellSettingsDialogComponent, {
924
- data,
925
- width: '400px',
926
- maxWidth: '90vw',
927
- disableClose: false,
928
- autoFocus: false,
929
- });
930
- const result = await firstValueFrom(dialogRef.afterClosed());
931
- return result;
932
- }
933
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DefaultCellSettingsDialogProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
934
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DefaultCellSettingsDialogProvider, providedIn: 'root' });
935
- }
936
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DefaultCellSettingsDialogProvider, decorators: [{
937
- type: Injectable,
938
- args: [{
939
- providedIn: 'root',
940
- }]
941
- }] });
942
-
943
- /**
944
- * Injection token for the cell dialog provider.
945
- * Use this to provide your custom dialog implementation.
946
- *
947
- * @example
948
- * ```typescript
949
- * providers: [
950
- * { provide: CELL_SETTINGS_DIALOG_PROVIDER, useClass: MyCellSettingsDialogProvider }
951
- * ]
952
- * ```
953
- */
954
- const CELL_SETTINGS_DIALOG_PROVIDER = new InjectionToken('CellSettingsDialogProvider', {
955
- providedIn: 'root',
956
- factory: () => new DefaultCellSettingsDialogProvider(),
957
- });
958
-
959
- class CellContextMenuService {
960
- #activeMenu = signal(null);
961
- activeMenu = this.#activeMenu.asReadonly();
962
- show(x, y, items) {
963
- this.#activeMenu.set({ x, y, items });
964
- }
965
- hide() {
966
- this.#activeMenu.set(null);
967
- }
968
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: CellContextMenuService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
969
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: CellContextMenuService });
970
- }
971
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: CellContextMenuService, decorators: [{
972
- type: Injectable
973
- }] });
974
-
975
- // cell.component.ts
976
- class CellComponent {
977
- id = input.required();
978
- widgetFactory = input(undefined);
979
- widgetState = input(undefined);
980
- isEditMode = input(false);
981
- flat = input(undefined);
982
- row = model.required();
983
- column = model.required();
984
- rowSpan = input(1);
985
- colSpan = input(1);
986
- draggable = input(false);
987
- dragStart = output();
988
- dragEnd = output();
989
- edit = output();
990
- delete = output();
991
- settings = output();
992
- resizeStart = output();
993
- resizeMove = output();
994
- resizeEnd = output();
995
- container = viewChild.required('container', { read: ViewContainerRef });
996
- #store = inject(DashboardStore);
997
- #dialogProvider = inject(CELL_SETTINGS_DIALOG_PROVIDER);
998
- #destroyRef = inject(DestroyRef);
999
- #renderer = inject(Renderer2);
1000
- #contextMenuService = inject(CellContextMenuService, {
1001
- optional: true,
1002
- });
1003
- #elementRef = inject(ElementRef);
1004
- #widgetRef;
1005
- // Document event listeners cleanup function
1006
- // Performance: Only created when actively resizing, not for every cell
1007
- #documentListeners;
1008
- isDragging = signal(false);
1009
- gridRowStyle = computed(() => `${this.row()} / span ${this.rowSpan()}`);
1010
- gridColumnStyle = computed(() => `${this.column()} / span ${this.colSpan()}`);
1011
- isResizing = computed(() => {
1012
- const resizeData = this.#store.resizeData();
1013
- return resizeData
1014
- ? CellIdUtils.equals(resizeData.cellId, this.id())
1015
- : false;
1016
- });
1017
- isDragActive = computed(() => !!this.#store.dragData());
1018
- resizeData = this.#store.resizeData;
1019
- gridCellDimensions = this.#store.gridCellDimensions;
1020
- resizeDirection = signal(null);
1021
- resizeStartPos = signal({ x: 0, y: 0 });
1022
- constructor() {
1023
- // widget creation - triggers when factory or state changes
1024
- effect(() => {
1025
- const factory = this.widgetFactory();
1026
- const state = this.widgetState();
1027
- const container = this.container();
1028
- if (factory && container) {
1029
- // Clean up previous widget
1030
- this.#widgetRef?.destroy();
1031
- // Create new widget
1032
- container.clear();
1033
- try {
1034
- this.#widgetRef = factory.createInstance(container, state);
1035
- }
1036
- catch (error) {
1037
- console.error('Failed to create widget:', error);
1038
- this.#widgetRef = undefined;
1039
- }
1040
- }
1041
- });
1042
- // Auto cleanup on destroy
1043
- this.#destroyRef.onDestroy(() => {
1044
- this.#widgetRef?.destroy();
1045
- this.#widgetRef = undefined;
1046
- // Clean up any active document listeners
1047
- this.#cleanupDocumentListeners();
1048
- });
1049
- }
1050
- /**
1051
- * Setup document-level event listeners for resize operations
1052
- * Performance: Only creates listeners when actively resizing (not for every cell)
1053
- * Angular-idiomatic: Uses Renderer2 for dynamic listener management
1054
- */
1055
- setupDocumentListeners() {
1056
- // Clean up any existing listeners first
1057
- this.#cleanupDocumentListeners();
1058
- // Create document listeners with proper cleanup functions
1059
- const unlistenMove = this.#renderer.listen('document', 'mousemove', this.handleResizeMove.bind(this));
1060
- const unlistenUp = this.#renderer.listen('document', 'mouseup', this.handleResizeEnd.bind(this));
1061
- // Store cleanup function for later use
1062
- this.#documentListeners = () => {
1063
- unlistenMove();
1064
- unlistenUp();
1065
- };
1066
- }
1067
- /**
1068
- * Clean up document-level event listeners
1069
- * Called on resize end and component destruction
1070
- */
1071
- #cleanupDocumentListeners() {
1072
- if (this.#documentListeners) {
1073
- this.#documentListeners();
1074
- this.#documentListeners = undefined;
1075
- }
1076
- }
1077
- setPosition(row, column) {
1078
- this.row.set(row);
1079
- this.column.set(column);
1080
- }
1081
- onDragStart(event) {
1082
- if (!event.dataTransfer)
1083
- return;
1084
- event.dataTransfer.effectAllowed = 'move';
1085
- const cell = {
1086
- cellId: this.id(),
1087
- row: this.row(),
1088
- col: this.column(),
1089
- rowSpan: this.rowSpan(),
1090
- colSpan: this.colSpan(),
1091
- };
1092
- const content = { kind: 'cell', content: cell };
1093
- this.dragStart.emit(content);
1094
- this.isDragging.set(true);
1095
- }
1096
- onDragEnd( /*_: DragEvent*/) {
1097
- this.isDragging.set(false);
1098
- this.dragEnd.emit();
1099
- }
1100
- /**
1101
- * Handle context menu events (called from template)
1102
- * Performance: Element-specific event binding, not document-level
1103
- * Angular-idiomatic: Template event binding instead of fromEvent
1104
- */
1105
- onContextMenu(event) {
1106
- if (!this.isEditMode() || !this.#contextMenuService)
1107
- return;
1108
- event.preventDefault();
1109
- event.stopPropagation();
1110
- const items = [
1111
- {
1112
- label: 'Edit Widget',
1113
- icon: 'edit',
1114
- action: () => this.onEdit(),
1115
- disabled: !this.canEdit(),
1116
- },
1117
- {
1118
- label: 'Settings',
1119
- icon: 'settings',
1120
- action: () => this.onSettings(),
1121
- },
1122
- { divider: true },
1123
- {
1124
- label: 'Delete',
1125
- icon: 'delete',
1126
- action: () => this.onDelete(),
1127
- },
1128
- ];
1129
- // Position menu at exact mouse coordinates
1130
- this.#contextMenuService.show(event.clientX, event.clientY, items);
1131
- }
1132
- canEdit() {
1133
- if (this.#widgetRef?.instance?.dashboardEditState) {
1134
- return true;
1135
- }
1136
- return false;
1137
- }
1138
- onEdit() {
1139
- this.edit.emit(this.id());
1140
- // Call the widget's edit dialog method if it exists
1141
- if (this.#widgetRef?.instance?.dashboardEditState) {
1142
- this.#widgetRef.instance.dashboardEditState();
1143
- }
1144
- }
1145
- onDelete() {
1146
- this.delete.emit(this.id());
1147
- }
1148
- async onSettings() {
1149
- const currentSettings = {
1150
- id: CellIdUtils.toString(this.id()),
1151
- flat: this.flat(),
1152
- };
1153
- try {
1154
- const result = await this.#dialogProvider.openCellSettings(currentSettings);
1155
- if (result) {
1156
- this.settings.emit({
1157
- id: this.id(),
1158
- flat: result.flat ?? false,
1159
- });
1160
- }
1161
- }
1162
- catch (error) {
1163
- console.error('Error opening cell settings dialog:', error);
1164
- }
1165
- }
1166
- /**
1167
- * Start resize operation and setup document listeners
1168
- * Performance: Only THIS cell creates document listeners when actively resizing
1169
- * RxJS-free: Uses Renderer2 for dynamic listener management
1170
- */
1171
- onResizeStart(event, direction) {
1172
- event.preventDefault();
1173
- event.stopPropagation();
1174
- this.resizeDirection.set(direction);
1175
- this.resizeStartPos.set({ x: event.clientX, y: event.clientY });
1176
- this.resizeStart.emit({ id: this.id(), direction });
1177
- // Setup document listeners only when actively resizing
1178
- this.setupDocumentListeners();
1179
- const cursorClass = direction === 'horizontal' ? 'cursor-col-resize' : 'cursor-row-resize';
1180
- this.#renderer.addClass(document.body, cursorClass);
1181
- }
1182
- /**
1183
- * Handle resize move events (called from document listener)
1184
- * Performance: Only called for the actively resizing cell
1185
- * Bound method: Maintains component context without arrow functions
1186
- */
1187
- handleResizeMove(event) {
1188
- const direction = this.resizeDirection();
1189
- if (!direction)
1190
- return;
1191
- const startPos = this.resizeStartPos();
1192
- const cellSize = this.gridCellDimensions();
1193
- if (direction === 'horizontal') {
1194
- const deltaX = event.clientX - startPos.x;
1195
- const deltaSpan = Math.round(deltaX / cellSize.width);
1196
- this.resizeMove.emit({ id: this.id(), direction, delta: deltaSpan });
1197
- }
1198
- else {
1199
- const deltaY = event.clientY - startPos.y;
1200
- const deltaSpan = Math.round(deltaY / cellSize.height);
1201
- this.resizeMove.emit({ id: this.id(), direction, delta: deltaSpan });
1202
- }
1203
- }
1204
- /**
1205
- * Handle resize end events (called from document listener)
1206
- * Performance: Cleans up document listeners immediately after resize
1207
- * State cleanup: Resets resize direction to stop further event processing
1208
- */
1209
- handleResizeEnd() {
1210
- this.#renderer.removeClass(document.body, 'cursor-col-resize');
1211
- this.#renderer.removeClass(document.body, 'cursor-row-resize');
1212
- // Clean up document listeners immediately
1213
- this.#cleanupDocumentListeners();
1214
- this.resizeEnd.emit({ id: this.id(), apply: true });
1215
- this.resizeDirection.set(null);
1216
- }
1217
- /**
1218
- * Get the current widget state by calling dashboardGetState() on the widget instance.
1219
- * Used during dashboard export to get live widget state instead of stale stored state.
1220
- */
1221
- getCurrentWidgetState() {
1222
- if (!this.#widgetRef?.instance) {
1223
- return undefined;
1224
- }
1225
- // Call dashboardGetState() if the widget implements it
1226
- if (typeof this.#widgetRef.instance.dashboardGetState === 'function') {
1227
- return this.#widgetRef.instance.dashboardGetState();
1228
- }
1229
- // Fall back to stored state if widget doesn't implement dashboardGetState
1230
- return this.widgetState();
1231
- }
1232
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: CellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1233
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: CellComponent, isStandalone: true, selector: "lib-cell", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, widgetFactory: { classPropertyName: "widgetFactory", publicName: "widgetFactory", isSignal: true, isRequired: false, transformFunction: null }, widgetState: { classPropertyName: "widgetState", publicName: "widgetState", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: false, transformFunction: null }, flat: { classPropertyName: "flat", publicName: "flat", isSignal: true, isRequired: false, transformFunction: null }, row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, rowSpan: { classPropertyName: "rowSpan", publicName: "rowSpan", isSignal: true, isRequired: false, transformFunction: null }, colSpan: { classPropertyName: "colSpan", publicName: "colSpan", isSignal: true, isRequired: false, transformFunction: null }, draggable: { classPropertyName: "draggable", publicName: "draggable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { row: "rowChange", column: "columnChange", dragStart: "dragStart", dragEnd: "dragEnd", edit: "edit", delete: "delete", settings: "settings", resizeStart: "resizeStart", resizeMove: "resizeMove", resizeEnd: "resizeEnd" }, host: { properties: { "style.grid-row": "gridRowStyle()", "style.grid-column": "gridColumnStyle()", "class.is-dragging": "isDragging()", "class.drag-active": "isDragActive()", "class.flat": "flat() === true" } }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "<!-- cell.component.html -->\r\n<div\r\n class=\"cell\"\r\n [class.is-resizing]=\"isResizing()\"\r\n [class.flat]=\"flat() === true\"\r\n [draggable]=\"draggable()\"\r\n (dragstart)=\"onDragStart($event)\"\r\n (dragend)=\"onDragEnd()\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n <div class=\"content-area\">\r\n <ng-template #container></ng-template>\r\n </div>\r\n @if (isEditMode() && !isDragging()) {\r\n <!-- Right resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--right\"\r\n (mousedown)=\"onResizeStart($event, 'horizontal')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n <!-- Bottom resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--bottom\"\r\n (mousedown)=\"onResizeStart($event, 'vertical')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n }\r\n</div>\r\n\r\n@if (isResizing()) {\r\n<div class=\"resize-preview\">\r\n {{ resizeData()?.previewColSpan ?? colSpan() }} \u00D7\r\n {{ resizeData()?.previewRowSpan ?? rowSpan() }}\r\n</div>\r\n}\r\n", styles: [":host{display:block;width:100%;height:100%;position:relative;z-index:1;container-type:inline-size}:host(.drag-active):not(.is-dragging){pointer-events:none}:host(.is-dragging){z-index:100;opacity:.5;pointer-events:none}:host(:hover) .resize-handle{opacity:1}.cell{width:100%;height:100%;border-radius:4px;box-shadow:0 2px 6px #0000001a;padding:0;box-sizing:border-box;overflow:hidden;position:relative;container-type:inline-size}.cell:hover{box-shadow:0 4px 10px #00000026;transform:translateY(-2px)}.cell.flat{box-shadow:none;border:none}.cell.flat:hover{box-shadow:none;transform:none;border-color:#bdbdbd}.cell.resizing{-webkit-user-select:none;user-select:none}.content-area{width:100%;height:100%;overflow:auto}.resize-handle{position:absolute;z-index:20}.resize-handle--right{cursor:col-resize;width:16px;height:100%;right:-8px;top:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--right:hover{opacity:1}.resize-handle--right:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle--bottom{cursor:row-resize;width:100%;height:16px;bottom:-8px;left:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--bottom:hover{opacity:1}.resize-handle--bottom:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle-line{background-color:#0000001a}.resize-handle--right .resize-handle-line{width:8px;height:40px;border-radius:2px}.resize-handle--bottom .resize-handle-line{width:40px;height:8px;border-radius:2px}.resize-preview{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary);padding:4px 12px;border-radius:4px;font-size:14px;font-weight:500;pointer-events:none;z-index:30}.cell.is-resizing{opacity:.6}.cell.is-resizing .resize-handle{background-color:#2196f380}:root .cursor-col-resize{cursor:col-resize!important}:root .cursor-row-resize{cursor:row-resize!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1234
- }
1235
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: CellComponent, decorators: [{
1236
- type: Component,
1237
- args: [{ selector: 'lib-cell', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, host: {
1238
- '[style.grid-row]': 'gridRowStyle()',
1239
- '[style.grid-column]': 'gridColumnStyle()',
1240
- '[class.is-dragging]': 'isDragging()',
1241
- '[class.drag-active]': 'isDragActive()',
1242
- '[class.flat]': 'flat() === true',
1243
- }, template: "<!-- cell.component.html -->\r\n<div\r\n class=\"cell\"\r\n [class.is-resizing]=\"isResizing()\"\r\n [class.flat]=\"flat() === true\"\r\n [draggable]=\"draggable()\"\r\n (dragstart)=\"onDragStart($event)\"\r\n (dragend)=\"onDragEnd()\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n <div class=\"content-area\">\r\n <ng-template #container></ng-template>\r\n </div>\r\n @if (isEditMode() && !isDragging()) {\r\n <!-- Right resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--right\"\r\n (mousedown)=\"onResizeStart($event, 'horizontal')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n <!-- Bottom resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--bottom\"\r\n (mousedown)=\"onResizeStart($event, 'vertical')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n }\r\n</div>\r\n\r\n@if (isResizing()) {\r\n<div class=\"resize-preview\">\r\n {{ resizeData()?.previewColSpan ?? colSpan() }} \u00D7\r\n {{ resizeData()?.previewRowSpan ?? rowSpan() }}\r\n</div>\r\n}\r\n", styles: [":host{display:block;width:100%;height:100%;position:relative;z-index:1;container-type:inline-size}:host(.drag-active):not(.is-dragging){pointer-events:none}:host(.is-dragging){z-index:100;opacity:.5;pointer-events:none}:host(:hover) .resize-handle{opacity:1}.cell{width:100%;height:100%;border-radius:4px;box-shadow:0 2px 6px #0000001a;padding:0;box-sizing:border-box;overflow:hidden;position:relative;container-type:inline-size}.cell:hover{box-shadow:0 4px 10px #00000026;transform:translateY(-2px)}.cell.flat{box-shadow:none;border:none}.cell.flat:hover{box-shadow:none;transform:none;border-color:#bdbdbd}.cell.resizing{-webkit-user-select:none;user-select:none}.content-area{width:100%;height:100%;overflow:auto}.resize-handle{position:absolute;z-index:20}.resize-handle--right{cursor:col-resize;width:16px;height:100%;right:-8px;top:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--right:hover{opacity:1}.resize-handle--right:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle--bottom{cursor:row-resize;width:100%;height:16px;bottom:-8px;left:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--bottom:hover{opacity:1}.resize-handle--bottom:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle-line{background-color:#0000001a}.resize-handle--right .resize-handle-line{width:8px;height:40px;border-radius:2px}.resize-handle--bottom .resize-handle-line{width:40px;height:8px;border-radius:2px}.resize-preview{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary);padding:4px 12px;border-radius:4px;font-size:14px;font-weight:500;pointer-events:none;z-index:30}.cell.is-resizing{opacity:.6}.cell.is-resizing .resize-handle{background-color:#2196f380}:root .cursor-col-resize{cursor:col-resize!important}:root .cursor-row-resize{cursor:row-resize!important}\n"] }]
1244
- }], ctorParameters: () => [] });
1245
-
1246
- class DashboardViewerComponent {
1247
- #store = inject(DashboardStore);
1248
- cellComponents = viewChildren(CellComponent);
1249
- rows = input.required();
1250
- columns = input.required();
1251
- gutterSize = input('1em');
1252
- gutters = computed(() => this.columns() + 1);
1253
- // store signals - read-only
1254
- cells = this.#store.cells;
1255
- constructor() {
1256
- // Sync grid configuration with store when inputs change
1257
- effect(() => {
1258
- this.#store.setGridConfig({
1259
- rows: this.rows(),
1260
- columns: this.columns(),
1261
- gutterSize: this.gutterSize(),
1262
- });
1263
- });
1264
- }
1265
- /**
1266
- * Get current widget states from all cell components.
1267
- * Used during dashboard export to get live widget states.
1268
- */
1269
- getCurrentWidgetStates() {
1270
- const stateMap = new Map();
1271
- const cells = this.cellComponents();
1272
- for (const cell of cells) {
1273
- const cellId = cell.id();
1274
- const currentState = cell.getCurrentWidgetState();
1275
- if (currentState !== undefined) {
1276
- stateMap.set(CellIdUtils.toString(cellId), currentState);
1277
- }
1278
- }
1279
- return stateMap;
1280
- }
1281
- /**
1282
- * Export dashboard with live widget states from current component instances.
1283
- * This ensures the most up-to-date widget states are captured.
1284
- */
1285
- exportDashboard() {
1286
- return this.#store.exportDashboard(() => this.getCurrentWidgetStates());
1287
- }
1288
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1289
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: DashboardViewerComponent, isStandalone: true, selector: "ngx-dashboard-viewer", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, gutterSize: { classPropertyName: "gutterSize", publicName: "gutterSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.--rows": "rows()", "style.--columns": "columns()", "style.--gutter-size": "gutterSize()", "style.--gutters": "gutters()" } }, viewQueries: [{ propertyName: "cellComponents", predicate: CellComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Dashboard viewer - read-only grid -->\r\n<div class=\"grid top-grid\">\r\n @for (cell of cells(); track cell.cellId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [id]=\"cell.cellId\"\r\n [isEditMode]=\"false\"\r\n [draggable]=\"false\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n >\r\n </lib-cell>\r\n }\r\n</div>\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto;background-color:var(--mat-sys-surface-container)}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);width:100%;height:100%;box-sizing:border-box;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}.grid-cell{pointer-events:none}.grid-cell:not(.flat){pointer-events:auto;cursor:default}.grid-cell:not(.flat) .content-area{pointer-events:none}.top-grid{z-index:2;pointer-events:none}\n"], dependencies: [{ kind: "component", type: CellComponent, selector: "lib-cell", inputs: ["id", "widgetFactory", "widgetState", "isEditMode", "flat", "row", "column", "rowSpan", "colSpan", "draggable"], outputs: ["rowChange", "columnChange", "dragStart", "dragEnd", "edit", "delete", "settings", "resizeStart", "resizeMove", "resizeEnd"] }, { kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1290
- }
1291
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardViewerComponent, decorators: [{
1292
- type: Component,
1293
- args: [{ selector: 'ngx-dashboard-viewer', standalone: true, imports: [CellComponent, CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, host: {
1294
- '[style.--rows]': 'rows()',
1295
- '[style.--columns]': 'columns()',
1296
- '[style.--gutter-size]': 'gutterSize()',
1297
- '[style.--gutters]': 'gutters()',
1298
- }, template: "<!-- Dashboard viewer - read-only grid -->\r\n<div class=\"grid top-grid\">\r\n @for (cell of cells(); track cell.cellId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [id]=\"cell.cellId\"\r\n [isEditMode]=\"false\"\r\n [draggable]=\"false\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n >\r\n </lib-cell>\r\n }\r\n</div>\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto;background-color:var(--mat-sys-surface-container)}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);width:100%;height:100%;box-sizing:border-box;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}.grid-cell{pointer-events:none}.grid-cell:not(.flat){pointer-events:auto;cursor:default}.grid-cell:not(.flat) .content-area{pointer-events:none}.top-grid{z-index:2;pointer-events:none}\n"] }]
1299
- }], ctorParameters: () => [] });
1300
-
1301
- class CellContextMenuComponent {
1302
- menuTrigger = viewChild.required('menuTrigger', { read: MatMenuTrigger });
1303
- menuService = inject(CellContextMenuService);
1304
- menuPosition = computed(() => {
1305
- const menu = this.menuService.activeMenu();
1306
- return menu ? { left: `${menu.x}px`, top: `${menu.y}px` } : { left: '0px', top: '0px' };
1307
- });
1308
- menuItems = computed(() => {
1309
- const menu = this.menuService.activeMenu();
1310
- return menu?.items || [];
1311
- });
1312
- constructor() {
1313
- effect(() => {
1314
- const menu = this.menuService.activeMenu();
1315
- if (menu) {
1316
- // Use queueMicrotask to ensure the view is fully initialized
1317
- // This fixes the issue where the menu disappears on first right-click
1318
- queueMicrotask(() => {
1319
- const trigger = this.menuTrigger();
1320
- if (trigger) {
1321
- // Close any existing menu first
1322
- if (trigger.menuOpen) {
1323
- trigger.closeMenu();
1324
- }
1325
- // Open menu - position is handled by signal
1326
- trigger.openMenu();
1327
- }
1328
- });
1329
- }
1330
- else {
1331
- const trigger = this.menuTrigger();
1332
- if (trigger) {
1333
- trigger.closeMenu();
1334
- }
1335
- }
1336
- });
1337
- // Hide service menu when Material menu closes
1338
- effect(() => {
1339
- const trigger = this.menuTrigger();
1340
- if (trigger) {
1341
- trigger.menuClosed.subscribe(() => {
1342
- if (this.menuService.activeMenu()) {
1343
- this.menuService.hide();
1344
- }
1345
- });
1346
- }
1347
- });
1348
- }
1349
- ngAfterViewInit() {
1350
- // Effects moved to constructor to be within injection context
1351
- }
1352
- executeAction(item) {
1353
- if (!item.divider && item.action) {
1354
- item.action();
1355
- this.menuService.hide();
1356
- }
1357
- }
1358
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: CellContextMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1359
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: CellContextMenuComponent, isStandalone: true, selector: "lib-cell-context-menu", viewQueries: [{ propertyName: "menuTrigger", first: true, predicate: ["menuTrigger"], descendants: true, read: MatMenuTrigger, isSignal: true }], ngImport: i0, template: `
1360
- <!-- Hidden trigger for menu positioned at exact mouse coordinates
1361
-
1362
- IMPORTANT: Angular Material applies its own positioning logic to menus,
1363
- which by default offsets the menu from the trigger element to avoid overlap.
1364
- To achieve precise positioning at mouse coordinates, we use these workarounds:
1365
-
1366
- 1. The trigger container is 1x1px (not 0x0) because Material needs a physical
1367
- element to calculate position against. Zero-sized elements cause unpredictable
1368
- positioning.
1369
-
1370
- 2. We use opacity:0 instead of visibility:hidden to keep the element in the
1371
- layout flow while making it invisible.
1372
-
1373
- 3. The button itself is styled to 1x1px with no padding to serve as a precise
1374
- anchor point for the menu.
1375
-
1376
- 4. The mat-menu uses [overlapTrigger]="true" to allow the menu to appear
1377
- directly at the trigger position rather than offset from it.
1378
-
1379
- This approach ensures the menu appears at the exact mouse coordinates passed
1380
- from the cell component's right-click handler.
1381
- -->
1382
- <div
1383
- style="position: fixed; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
1384
- [style]="menuPosition()">
1385
- <button
1386
- mat-button
1387
- #menuTrigger="matMenuTrigger"
1388
- [matMenuTriggerFor]="contextMenu"
1389
- style="width: 1px; height: 1px; padding: 0; min-width: 0; line-height: 0;">
1390
- </button>
1391
- </div>
1392
-
1393
- <!-- Context menu -->
1394
- <mat-menu #contextMenu="matMenu" [overlapTrigger]="true">
1395
- @for (item of menuItems(); track $index) {
1396
- @if (item.divider) {
1397
- <mat-divider></mat-divider>
1398
- } @else {
1399
- <button
1400
- mat-menu-item
1401
- (click)="executeAction(item)"
1402
- [disabled]="item.disabled">
1403
- @if (item.icon) {
1404
- <mat-icon>{{ item.icon }}</mat-icon>
1405
- }
1406
- <span>{{ item.label }}</span>
1407
- </button>
1408
- }
1409
- }
1410
- </mat-menu>
1411
- `, isInline: true, styles: [":host{display:contents}\n"], dependencies: [{ kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i1$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i1$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i1$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i3$1.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1412
- }
1413
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: CellContextMenuComponent, decorators: [{
1414
- type: Component,
1415
- args: [{ selector: 'lib-cell-context-menu', standalone: true, imports: [MatMenuModule, MatIconModule, MatDividerModule, MatButtonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1416
- <!-- Hidden trigger for menu positioned at exact mouse coordinates
1417
-
1418
- IMPORTANT: Angular Material applies its own positioning logic to menus,
1419
- which by default offsets the menu from the trigger element to avoid overlap.
1420
- To achieve precise positioning at mouse coordinates, we use these workarounds:
1421
-
1422
- 1. The trigger container is 1x1px (not 0x0) because Material needs a physical
1423
- element to calculate position against. Zero-sized elements cause unpredictable
1424
- positioning.
1425
-
1426
- 2. We use opacity:0 instead of visibility:hidden to keep the element in the
1427
- layout flow while making it invisible.
1428
-
1429
- 3. The button itself is styled to 1x1px with no padding to serve as a precise
1430
- anchor point for the menu.
1431
-
1432
- 4. The mat-menu uses [overlapTrigger]="true" to allow the menu to appear
1433
- directly at the trigger position rather than offset from it.
1434
-
1435
- This approach ensures the menu appears at the exact mouse coordinates passed
1436
- from the cell component's right-click handler.
1437
- -->
1438
- <div
1439
- style="position: fixed; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
1440
- [style]="menuPosition()">
1441
- <button
1442
- mat-button
1443
- #menuTrigger="matMenuTrigger"
1444
- [matMenuTriggerFor]="contextMenu"
1445
- style="width: 1px; height: 1px; padding: 0; min-width: 0; line-height: 0;">
1446
- </button>
1447
- </div>
1448
-
1449
- <!-- Context menu -->
1450
- <mat-menu #contextMenu="matMenu" [overlapTrigger]="true">
1451
- @for (item of menuItems(); track $index) {
1452
- @if (item.divider) {
1453
- <mat-divider></mat-divider>
1454
- } @else {
1455
- <button
1456
- mat-menu-item
1457
- (click)="executeAction(item)"
1458
- [disabled]="item.disabled">
1459
- @if (item.icon) {
1460
- <mat-icon>{{ item.icon }}</mat-icon>
1461
- }
1462
- <span>{{ item.label }}</span>
1463
- </button>
1464
- }
1465
- }
1466
- </mat-menu>
1467
- `, styles: [":host{display:contents}\n"] }]
1468
- }], ctorParameters: () => [] });
1469
-
1470
- // drop-zone.component.ts
1471
- class DropZoneComponent {
1472
- // Required inputs
1473
- row = input.required();
1474
- col = input.required();
1475
- index = input.required();
1476
- // Optional inputs with defaults
1477
- highlight = input(false);
1478
- highlightInvalid = input(false);
1479
- highlightResize = input(false);
1480
- editMode = input(false);
1481
- // Outputs
1482
- dragEnter = output();
1483
- dragExit = output();
1484
- dragOver = output();
1485
- dragDrop = output();
1486
- // Computed properties
1487
- dropZoneId = computed(() => `drop-zone-${this.row()}-${this.col()}`);
1488
- dropData = computed(() => ({
1489
- row: this.row(),
1490
- col: this.col(),
1491
- }));
1492
- // Abstract drag state from store
1493
- dragData = computed(() => this.#store.dragData());
1494
- // Computed drop effect based on drag data and validity
1495
- dropEffect = computed(() => {
1496
- const data = this.dragData();
1497
- if (!data || this.highlightInvalid()) {
1498
- return 'none';
1499
- }
1500
- return data.kind === 'cell' ? 'move' : 'copy';
1501
- });
1502
- #store = inject(DashboardStore);
1503
- #elementRef = inject(ElementRef);
1504
- get nativeElement() {
1505
- return this.#elementRef.nativeElement;
1506
- }
1507
- onDragEnter(event) {
1508
- event.preventDefault();
1509
- event.stopPropagation();
1510
- this.dragEnter.emit({ row: this.row(), col: this.col() });
1511
- }
1512
- onDragOver(event) {
1513
- event.preventDefault();
1514
- event.stopPropagation();
1515
- if (event.dataTransfer && this.dragData()) {
1516
- event.dataTransfer.dropEffect = this.dropEffect();
1517
- }
1518
- this.dragOver.emit({ row: this.row(), col: this.col() });
1519
- }
1520
- onDragLeave(event) {
1521
- event.preventDefault();
1522
- event.stopPropagation();
1523
- // Only emit if actually leaving the element (not entering a child)
1524
- if (this.#isLeavingElement(event)) {
1525
- this.dragExit.emit();
1526
- }
1527
- }
1528
- #isLeavingElement(event) {
1529
- const rect = this.#elementRef.nativeElement.getBoundingClientRect();
1530
- return (event.clientX <= rect.left ||
1531
- event.clientX >= rect.right ||
1532
- event.clientY <= rect.top ||
1533
- event.clientY >= rect.bottom);
1534
- }
1535
- onDrop(event) {
1536
- event.preventDefault();
1537
- event.stopPropagation();
1538
- if (!event.dataTransfer)
1539
- return;
1540
- const data = this.dragData();
1541
- if (data) {
1542
- this.dragDrop.emit({
1543
- data,
1544
- target: { row: this.row(), col: this.col() },
1545
- });
1546
- }
1547
- }
1548
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DropZoneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1549
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: DropZoneComponent, isStandalone: true, selector: "lib-drop-zone", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, col: { classPropertyName: "col", publicName: "col", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null }, highlight: { classPropertyName: "highlight", publicName: "highlight", isSignal: true, isRequired: false, transformFunction: null }, highlightInvalid: { classPropertyName: "highlightInvalid", publicName: "highlightInvalid", isSignal: true, isRequired: false, transformFunction: null }, highlightResize: { classPropertyName: "highlightResize", publicName: "highlightResize", isSignal: true, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dragEnter: "dragEnter", dragExit: "dragExit", dragOver: "dragOver", dragDrop: "dragDrop" }, ngImport: i0, template: "<!-- drop-zone.component.html -->\r\n<div\r\n class=\"drop-zone\"\r\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\r\n [class.drop-zone--invalid]=\"highlightInvalid()\"\r\n [class.drop-zone--resize]=\"highlightResize()\"\r\n [style.grid-row]=\"row()\"\r\n [style.grid-column]=\"col()\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n>\r\n @if (editMode()) {\r\n <div class=\"edit-mode-cell-number\">\r\n {{ index() }}<br />\r\n {{ row() }},{{ col() }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".drop-zone{width:100%;height:100%;z-index:0;align-self:stretch;justify-self:stretch;display:block;box-sizing:border-box}.drop-zone--active,.drop-zone--highlight{background-color:#80808080}.drop-zone--invalid{background-color:light-dark(color-mix(in srgb,var(--mat-sys-error) 40%,white),color-mix(in srgb,var(--mat-sys-error) 80%,black))}.drop-zone--resize{background-color:#2196f34d;outline:1px solid rgba(33,150,243,.6)}.edit-mode-cell-number{font-size:10px;color:#64646499;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:-1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1550
- }
1551
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DropZoneComponent, decorators: [{
1552
- type: Component,
1553
- args: [{ selector: 'lib-drop-zone', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- drop-zone.component.html -->\r\n<div\r\n class=\"drop-zone\"\r\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\r\n [class.drop-zone--invalid]=\"highlightInvalid()\"\r\n [class.drop-zone--resize]=\"highlightResize()\"\r\n [style.grid-row]=\"row()\"\r\n [style.grid-column]=\"col()\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n>\r\n @if (editMode()) {\r\n <div class=\"edit-mode-cell-number\">\r\n {{ index() }}<br />\r\n {{ row() }},{{ col() }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".drop-zone{width:100%;height:100%;z-index:0;align-self:stretch;justify-self:stretch;display:block;box-sizing:border-box}.drop-zone--active,.drop-zone--highlight{background-color:#80808080}.drop-zone--invalid{background-color:light-dark(color-mix(in srgb,var(--mat-sys-error) 40%,white),color-mix(in srgb,var(--mat-sys-error) 80%,black))}.drop-zone--resize{background-color:#2196f34d;outline:1px solid rgba(33,150,243,.6)}.edit-mode-cell-number{font-size:10px;color:#64646499;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:-1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;width:100%;height:100%}\n"] }]
1554
- }] });
1555
-
1556
- // dashboard-editor.component.ts
1557
- class DashboardEditorComponent {
1558
- bottomGridRef = viewChild.required('bottomGrid');
1559
- dropZones = viewChildren(DropZoneComponent);
1560
- cellComponents = viewChildren(CellComponent);
1561
- #store = inject(DashboardStore);
1562
- #destroyRef = inject(DestroyRef);
1563
- #resizeObserver;
1564
- rows = input.required();
1565
- columns = input.required();
1566
- gutterSize = input('1em');
1567
- gutters = computed(() => this.columns() + 1);
1568
- // store signals
1569
- cells = this.#store.cells;
1570
- highlightedZones = this.#store.highlightedZones;
1571
- highlightMap = this.#store.highlightMap;
1572
- invalidHighlightMap = this.#store.invalidHighlightMap;
1573
- hoveredDropZone = this.#store.hoveredDropZone;
1574
- resizePreviewMap = this.#store.resizePreviewMap;
1575
- // Generate all possible cell positions for the grid
1576
- dropzonePositions = computed(() => {
1577
- const positions = [];
1578
- for (let row = 1; row <= this.rows(); row++) {
1579
- for (let col = 1; col <= this.columns(); col++) {
1580
- positions.push({
1581
- row,
1582
- col,
1583
- id: `dropzone-${row}-${col}`,
1584
- index: (row - 1) * this.columns() + col,
1585
- });
1586
- }
1587
- }
1588
- return positions;
1589
- });
1590
- // Helper method for template
1591
- createCellId(row, col) {
1592
- return CellIdUtils.create(row, col);
1593
- }
1594
- constructor() {
1595
- // Sync grid configuration with store when inputs change
1596
- effect(() => {
1597
- this.#store.setGridConfig({
1598
- rows: this.rows(),
1599
- columns: this.columns(),
1600
- gutterSize: this.gutterSize(),
1601
- });
1602
- });
1603
- // Observe grid size after rendering
1604
- afterNextRender(() => {
1605
- this.#observeGridSize();
1606
- });
1607
- // Always set edit mode to true
1608
- effect(() => {
1609
- this.#store.setEditMode(true);
1610
- });
1611
- }
1612
- #observeGridSize() {
1613
- const gridEl = this.bottomGridRef()?.nativeElement;
1614
- if (!gridEl || this.#resizeObserver)
1615
- return;
1616
- this.#resizeObserver = new ResizeObserver(() => {
1617
- const dropZonesList = this.dropZones();
1618
- const firstDropZone = dropZonesList[0];
1619
- if (!firstDropZone)
1620
- return;
1621
- const el = firstDropZone.nativeElement;
1622
- if (!el)
1623
- return;
1624
- const rect = el.getBoundingClientRect();
1625
- const width = rect.width;
1626
- const height = rect.height;
1627
- this.#store.setGridCellDimensions(width, height);
1628
- });
1629
- this.#resizeObserver.observe(gridEl);
1630
- // Register cleanup with DestroyRef for automatic memory management
1631
- this.#destroyRef.onDestroy(() => {
1632
- this.#resizeObserver?.disconnect();
1633
- this.#resizeObserver = undefined;
1634
- });
1635
- }
1636
- // Pure delegation methods - no business logic in component
1637
- addWidget = (cellData) => this.#store.addWidget(cellData);
1638
- updateCellPosition = (id, row, column) => this.#store.updateWidgetPosition(id, row, column);
1639
- updateCellSpan = (id, colSpan, rowSpan) => this.#store.updateWidgetSpan(id, rowSpan, colSpan);
1640
- updateCellSettings = (id, flat) => this.#store.updateCellSettings(id, flat);
1641
- // Pure delegation - drag and drop event handlers
1642
- onDragOver = (event) => this.#store.setHoveredDropZone(event);
1643
- onDragEnter = (event) => this.onDragOver(event);
1644
- onDragExit = () => this.#store.setHoveredDropZone(null);
1645
- dragEnd = () => this.#store.endDrag();
1646
- // Pure delegation - cell event handlers
1647
- onCellDelete = (id) => this.#store.removeWidget(id);
1648
- onCellSettings = (event) => this.updateCellSettings(event.id, event.flat);
1649
- onCellResize = (event) => this.updateCellSpan(event.id, event.colSpan, event.rowSpan);
1650
- // Handle drag events from cell component
1651
- onCellDragStart = (dragData) => this.#store.startDrag(dragData);
1652
- // Handle resize events from cell component
1653
- onCellResizeStart = (event) => this.#store.startResize(event.id);
1654
- onCellResizeMove = (event) => this.#store.updateResizePreview(event.direction, event.delta);
1655
- onCellResizeEnd = (event) => this.#store.endResize(event.apply);
1656
- // Handle drop events by delegating to store's business logic
1657
- onDragDrop(event) {
1658
- this.#store.handleDrop(event.data, event.target);
1659
- // Note: Store handles all validation and error handling internally
1660
- }
1661
- /**
1662
- * Get current widget states from all cell components.
1663
- * Used during dashboard export to get live widget states.
1664
- */
1665
- getCurrentWidgetStates() {
1666
- const stateMap = new Map();
1667
- const cells = this.cellComponents();
1668
- for (const cell of cells) {
1669
- const cellId = cell.id();
1670
- const currentState = cell.getCurrentWidgetState();
1671
- if (currentState !== undefined) {
1672
- stateMap.set(CellIdUtils.toString(cellId), currentState);
1673
- }
1674
- }
1675
- return stateMap;
1676
- }
1677
- /**
1678
- * Export dashboard with live widget states from current component instances.
1679
- * This ensures the most up-to-date widget states are captured.
1680
- */
1681
- exportDashboard() {
1682
- return this.#store.exportDashboard(() => this.getCurrentWidgetStates());
1683
- }
1684
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1685
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: DashboardEditorComponent, isStandalone: true, selector: "ngx-dashboard-editor", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, gutterSize: { classPropertyName: "gutterSize", publicName: "gutterSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.--rows": "rows()", "style.--columns": "columns()", "style.--gutter-size": "gutterSize()", "style.--gutters": "gutters()", "class.is-edit-mode": "true" } }, providers: [
1686
- CellContextMenuService,
1687
- ], viewQueries: [{ propertyName: "bottomGridRef", first: true, predicate: ["bottomGrid"], descendants: true, isSignal: true }, { propertyName: "dropZones", predicate: DropZoneComponent, descendants: true, isSignal: true }, { propertyName: "cellComponents", predicate: CellComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- dashboard-editor.component.html -->\n<div class=\"grid-container\">\n <!-- Bottom grid with drop zones -->\n <div class=\"grid\" id=\"bottom-grid\" #bottomGrid>\n @for (position of dropzonePositions(); track position.id) {\n <lib-drop-zone\n class=\"drop-zone\"\n [row]=\"position.row\"\n [col]=\"position.col\"\n [index]=\"position.index\"\n [highlight]=\"highlightMap().has(createCellId(position.row, position.col))\"\n [highlightInvalid]=\"\n invalidHighlightMap().has(createCellId(position.row, position.col))\n \"\n [highlightResize]=\"\n resizePreviewMap().has(createCellId(position.row, position.col))\n \"\n [editMode]=\"true\"\n (dragEnter)=\"onDragEnter($event)\"\n (dragExit)=\"onDragExit()\"\n (dragOver)=\"onDragOver($event)\"\n (dragDrop)=\"onDragDrop($event)\"\n ></lib-drop-zone>\n }\n </div>\n\n <!-- Top grid with interactive cells -->\n <div class=\"grid\" id=\"top-grid\">\n @for (cell of cells(); track cell.cellId) {\n <lib-cell\n class=\"grid-cell\"\n [id]=\"cell.cellId\"\n [isEditMode]=\"true\"\n [draggable]=\"true\"\n [row]=\"cell.row\"\n [column]=\"cell.col\"\n [rowSpan]=\"cell.rowSpan\"\n [colSpan]=\"cell.colSpan\"\n [flat]=\"cell.flat\"\n [widgetFactory]=\"cell.widgetFactory\"\n [widgetState]=\"cell.widgetState\"\n (dragStart)=\"onCellDragStart($event)\"\n (dragEnd)=\"dragEnd()\"\n (delete)=\"onCellDelete($event)\"\n (settings)=\"onCellSettings($event)\"\n (resizeStart)=\"onCellResizeStart($event)\"\n (resizeMove)=\"onCellResizeMove($event)\"\n (resizeEnd)=\"onCellResizeEnd($event)\"\n >\n </lib-cell>\n }\n </div>\n</div>\n\n<!-- Context menu -->\n<lib-cell-context-menu></lib-cell-context-menu>\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}:host .grid{background-image:linear-gradient(to right,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px);background-size:var(--tile-size) var(--tile-size),var(--tile-size) var(--tile-size),100% 1px;background-position:var(--tile-offset) var(--tile-offset),var(--tile-offset) var(--tile-offset),bottom;background-repeat:repeat,repeat,no-repeat}.grid-container{position:relative;width:100%;height:100%}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);position:absolute;inset:0;width:100%;height:100%;box-sizing:border-box;align-items:stretch;justify-items:stretch;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}#bottom-grid{z-index:1}#top-grid{z-index:2;pointer-events:none}.grid-cell{pointer-events:auto}.grid-cell.is-dragging{pointer-events:none;opacity:.5}\n"], dependencies: [{ kind: "component", type: CellComponent, selector: "lib-cell", inputs: ["id", "widgetFactory", "widgetState", "isEditMode", "flat", "row", "column", "rowSpan", "colSpan", "draggable"], outputs: ["rowChange", "columnChange", "dragStart", "dragEnd", "edit", "delete", "settings", "resizeStart", "resizeMove", "resizeEnd"] }, { kind: "ngmodule", type: CommonModule }, { kind: "component", type: DropZoneComponent, selector: "lib-drop-zone", inputs: ["row", "col", "index", "highlight", "highlightInvalid", "highlightResize", "editMode"], outputs: ["dragEnter", "dragExit", "dragOver", "dragDrop"] }, { kind: "component", type: CellContextMenuComponent, selector: "lib-cell-context-menu" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1688
- }
1689
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardEditorComponent, decorators: [{
1690
- type: Component,
1691
- args: [{ selector: 'ngx-dashboard-editor', standalone: true, imports: [
1692
- CellComponent,
1693
- CommonModule,
1694
- DropZoneComponent,
1695
- CellContextMenuComponent,
1696
- ], providers: [
1697
- CellContextMenuService,
1698
- ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
1699
- '[style.--rows]': 'rows()',
1700
- '[style.--columns]': 'columns()',
1701
- '[style.--gutter-size]': 'gutterSize()',
1702
- '[style.--gutters]': 'gutters()',
1703
- '[class.is-edit-mode]': 'true', // Always in edit mode
1704
- }, template: "<!-- dashboard-editor.component.html -->\n<div class=\"grid-container\">\n <!-- Bottom grid with drop zones -->\n <div class=\"grid\" id=\"bottom-grid\" #bottomGrid>\n @for (position of dropzonePositions(); track position.id) {\n <lib-drop-zone\n class=\"drop-zone\"\n [row]=\"position.row\"\n [col]=\"position.col\"\n [index]=\"position.index\"\n [highlight]=\"highlightMap().has(createCellId(position.row, position.col))\"\n [highlightInvalid]=\"\n invalidHighlightMap().has(createCellId(position.row, position.col))\n \"\n [highlightResize]=\"\n resizePreviewMap().has(createCellId(position.row, position.col))\n \"\n [editMode]=\"true\"\n (dragEnter)=\"onDragEnter($event)\"\n (dragExit)=\"onDragExit()\"\n (dragOver)=\"onDragOver($event)\"\n (dragDrop)=\"onDragDrop($event)\"\n ></lib-drop-zone>\n }\n </div>\n\n <!-- Top grid with interactive cells -->\n <div class=\"grid\" id=\"top-grid\">\n @for (cell of cells(); track cell.cellId) {\n <lib-cell\n class=\"grid-cell\"\n [id]=\"cell.cellId\"\n [isEditMode]=\"true\"\n [draggable]=\"true\"\n [row]=\"cell.row\"\n [column]=\"cell.col\"\n [rowSpan]=\"cell.rowSpan\"\n [colSpan]=\"cell.colSpan\"\n [flat]=\"cell.flat\"\n [widgetFactory]=\"cell.widgetFactory\"\n [widgetState]=\"cell.widgetState\"\n (dragStart)=\"onCellDragStart($event)\"\n (dragEnd)=\"dragEnd()\"\n (delete)=\"onCellDelete($event)\"\n (settings)=\"onCellSettings($event)\"\n (resizeStart)=\"onCellResizeStart($event)\"\n (resizeMove)=\"onCellResizeMove($event)\"\n (resizeEnd)=\"onCellResizeEnd($event)\"\n >\n </lib-cell>\n }\n </div>\n</div>\n\n<!-- Context menu -->\n<lib-cell-context-menu></lib-cell-context-menu>\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}:host .grid{background-image:linear-gradient(to right,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px);background-size:var(--tile-size) var(--tile-size),var(--tile-size) var(--tile-size),100% 1px;background-position:var(--tile-offset) var(--tile-offset),var(--tile-offset) var(--tile-offset),bottom;background-repeat:repeat,repeat,no-repeat}.grid-container{position:relative;width:100%;height:100%}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);position:absolute;inset:0;width:100%;height:100%;box-sizing:border-box;align-items:stretch;justify-items:stretch;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}#bottom-grid{z-index:1}#top-grid{z-index:2;pointer-events:none}.grid-cell{pointer-events:auto}.grid-cell.is-dragging{pointer-events:none;opacity:.5}\n"] }]
1705
- }], ctorParameters: () => [] });
1706
-
1707
- // dashboard-bridge.service.ts
1708
- /**
1709
- * Internal bridge service that coordinates between component-scoped DashboardStore instances
1710
- * and standalone components like WidgetListComponent.
1711
- *
1712
- * This service is NOT part of the public API and should remain internal to the library.
1713
- */
1714
- class DashboardBridgeService {
1715
- // Map of registered dashboard instances with their reactive dimensions
1716
- dashboards = signal(new Map());
1717
- /**
1718
- * Register a dashboard store instance using its dashboard ID
1719
- */
1720
- registerDashboard(store) {
1721
- const dashboardId = store.dashboardId();
1722
- // If dashboard ID is not set yet, we'll register later when it's available
1723
- if (!dashboardId) {
1724
- return;
1725
- }
1726
- this.dashboards.update(dashboards => {
1727
- const newMap = new Map(dashboards);
1728
- newMap.set(dashboardId, {
1729
- store,
1730
- dimensions: store.gridCellDimensions
1731
- });
1732
- return newMap;
1733
- });
1734
- }
1735
- /**
1736
- * Unregister a dashboard store instance
1737
- */
1738
- unregisterDashboard(store) {
1739
- const dashboardId = store.dashboardId();
1740
- if (!dashboardId) {
1741
- return;
1742
- }
1743
- this.dashboards.update(dashboards => {
1744
- const newMap = new Map(dashboards);
1745
- newMap.delete(dashboardId);
1746
- return newMap;
1747
- });
1748
- }
1749
- /**
1750
- * Get cell dimensions for a specific dashboard instance
1751
- */
1752
- getDashboardDimensions(dashboardId) {
1753
- const dashboard = this.dashboards().get(dashboardId);
1754
- return dashboard?.dimensions() || { width: 100, height: 100 };
1755
- }
1756
- /**
1757
- * Get all available dashboard dimensions (for widget lists to choose from)
1758
- * Returns the first available dashboard's dimensions as fallback
1759
- */
1760
- availableDimensions = computed(() => {
1761
- const dashboardEntries = Array.from(this.dashboards().values());
1762
- if (dashboardEntries.length === 0) {
1763
- return { width: 100, height: 100 }; // fallback
1764
- }
1765
- // Return dimensions from first available dashboard with fallback for undefined
1766
- return dashboardEntries[0].dimensions() || { width: 100, height: 100 };
1767
- });
1768
- /**
1769
- * Start drag operation on the first available dashboard
1770
- * (Widget lists need some dashboard to coordinate with during drag)
1771
- */
1772
- startDrag(dragData) {
1773
- const dashboardEntries = Array.from(this.dashboards().values());
1774
- if (dashboardEntries.length > 0) {
1775
- dashboardEntries[0].store.startDrag(dragData);
1776
- }
1777
- }
1778
- /**
1779
- * End drag operation on all dashboards
1780
- * (Safer to notify all in case drag state got distributed)
1781
- */
1782
- endDrag() {
1783
- for (const dashboard of this.dashboards().values()) {
1784
- dashboard.store.endDrag();
1785
- }
1786
- }
1787
- /**
1788
- * Get all registered dashboard IDs
1789
- */
1790
- registeredDashboards = computed(() => Array.from(this.dashboards().keys()));
1791
- /**
1792
- * Get the number of registered dashboards
1793
- */
1794
- dashboardCount = computed(() => this.dashboards().size);
1795
- /**
1796
- * Check if any dashboards are registered
1797
- */
1798
- hasDashboards = computed(() => this.dashboards().size > 0);
1799
- /**
1800
- * Update registration for a dashboard store when its ID becomes available
1801
- */
1802
- updateDashboardRegistration(store) {
1803
- this.registerDashboard(store);
1804
- }
1805
- /**
1806
- * Get grid configuration for a specific dashboard
1807
- */
1808
- getDashboardGridConfig(dashboardId) {
1809
- const dashboard = this.dashboards().get(dashboardId);
1810
- if (!dashboard) {
1811
- return null;
1812
- }
1813
- const store = dashboard.store;
1814
- return {
1815
- rows: store.rows(),
1816
- columns: store.columns(),
1817
- gutterSize: store.gutterSize()
1818
- };
1819
- }
1820
- /**
1821
- * Get the store instance for a specific dashboard ID
1822
- */
1823
- getDashboardStore(dashboardId) {
1824
- const dashboard = this.dashboards().get(dashboardId);
1825
- return dashboard?.store || null;
1826
- }
1827
- /**
1828
- * Get all registered dashboards (for viewport service integration)
1829
- */
1830
- getAllDashboards() {
1831
- return this.dashboards();
1832
- }
1833
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardBridgeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1834
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardBridgeService, providedIn: 'root' });
1835
- }
1836
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardBridgeService, decorators: [{
1837
- type: Injectable,
1838
- args: [{ providedIn: 'root' }]
1839
- }] });
1840
-
1841
- /**
1842
- * Internal component-scoped service that provides viewport-aware constraints for a single dashboard.
1843
- * Each dashboard component gets its own instance of this service.
1844
- *
1845
- * This service is NOT part of the public API and should remain internal to the library.
1846
- */
1847
- class DashboardViewportService {
1848
- platformId = inject(PLATFORM_ID);
1849
- destroyRef = inject(DestroyRef);
1850
- store = inject(DashboardStore);
1851
- viewportSize = signal({ width: 0, height: 0 });
1852
- reservedSpace = signal(DEFAULT_RESERVED_SPACE);
1853
- resizeObserver = null;
1854
- constructor() {
1855
- if (isPlatformBrowser(this.platformId)) {
1856
- this.initializeViewportTracking();
1857
- }
1858
- }
1859
- /**
1860
- * Initialize viewport size tracking using ResizeObserver on the window
1861
- */
1862
- initializeViewportTracking() {
1863
- // Use ResizeObserver on document.documentElement for accurate viewport tracking
1864
- this.resizeObserver = new ResizeObserver((entries) => {
1865
- if (entries.length > 0) {
1866
- const entry = entries[0];
1867
- const { inlineSize, blockSize } = entry.contentBoxSize[0];
1868
- this.viewportSize.set({
1869
- width: inlineSize,
1870
- height: blockSize
1871
- });
1872
- }
1873
- });
1874
- this.resizeObserver.observe(document.documentElement);
1875
- // Initial size
1876
- this.viewportSize.set({
1877
- width: window.innerWidth,
1878
- height: window.innerHeight
1879
- });
1880
- // Cleanup on destroy
1881
- this.destroyRef.onDestroy(() => {
1882
- this.resizeObserver?.disconnect();
1883
- });
1884
- }
1885
- /**
1886
- * Set reserved space that should be excluded from dashboard calculations
1887
- * (e.g., toolbar height, widget list width, padding)
1888
- */
1889
- setReservedSpace(space) {
1890
- this.reservedSpace.set(space);
1891
- }
1892
- /**
1893
- * Get current viewport size
1894
- */
1895
- currentViewportSize = this.viewportSize.asReadonly();
1896
- /**
1897
- * Get current reserved space
1898
- */
1899
- currentReservedSpace = this.reservedSpace.asReadonly();
1900
- /**
1901
- * Calculate available space for dashboard after accounting for reserved areas
1902
- */
1903
- availableSpace = computed(() => {
1904
- const viewport = this.viewportSize();
1905
- const reserved = this.reservedSpace();
1906
- return {
1907
- width: Math.max(0, viewport.width - reserved.left - reserved.right),
1908
- height: Math.max(0, viewport.height - reserved.top - reserved.bottom)
1909
- };
1910
- });
1911
- /**
1912
- * Calculate dashboard constraints for this dashboard instance
1913
- */
1914
- constraints = computed(() => {
1915
- const availableSize = this.availableSpace();
1916
- // Get grid configuration from our component's store
1917
- const rows = this.store.rows();
1918
- const columns = this.store.columns();
1919
- if (rows === 0 || columns === 0) {
1920
- return {
1921
- maxWidth: availableSize.width,
1922
- maxHeight: availableSize.height,
1923
- constrainedBy: 'none'
1924
- };
1925
- }
1926
- // Calculate aspect ratio
1927
- const aspectRatio = columns / rows;
1928
- // Calculate maximum size that fits within available space
1929
- const maxWidthFromHeight = availableSize.height * aspectRatio;
1930
- const maxHeightFromWidth = availableSize.width / aspectRatio;
1931
- let maxWidth;
1932
- let maxHeight;
1933
- let constrainedBy;
1934
- if (maxWidthFromHeight <= availableSize.width) {
1935
- // Height is the limiting factor
1936
- maxWidth = maxWidthFromHeight;
1937
- maxHeight = availableSize.height;
1938
- constrainedBy = 'height';
1939
- }
1940
- else {
1941
- // Width is the limiting factor
1942
- maxWidth = availableSize.width;
1943
- maxHeight = maxHeightFromWidth;
1944
- constrainedBy = 'width';
1945
- }
1946
- return {
1947
- maxWidth: Math.max(0, maxWidth),
1948
- maxHeight: Math.max(0, maxHeight),
1949
- constrainedBy
1950
- };
1951
- });
1952
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardViewportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1953
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardViewportService });
1954
- }
1955
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardViewportService, decorators: [{
1956
- type: Injectable
1957
- }], ctorParameters: () => [] });
1958
-
1959
- // dashboard.component.ts
1960
- //
1961
- // A performant, modular, and fully reactive Angular dashboard container that orchestrates between
1962
- // editing and viewing modes — with clean component separation and no external dependencies.
1963
- class DashboardComponent {
1964
- #store = inject(DashboardStore);
1965
- #bridge = inject(DashboardBridgeService);
1966
- #viewport = inject(DashboardViewportService);
1967
- #destroyRef = inject(DestroyRef);
1968
- // Public accessors for template
1969
- store = this.#store;
1970
- viewport = this.#viewport;
1971
- // Component inputs
1972
- dashboardData = input.required();
1973
- editMode = input(false);
1974
- reservedSpace = input();
1975
- // Store signals - shared by both child components
1976
- cells = this.#store.cells;
1977
- // ViewChild references for export/import functionality
1978
- dashboardEditor = viewChild(DashboardEditorComponent);
1979
- dashboardViewer = viewChild(DashboardViewerComponent);
1980
- // Track if we're in the middle of preserving states
1981
- #isPreservingStates = false;
1982
- // Track if component has been initialized
1983
- #isInitialized = false;
1984
- constructor() {
1985
- // Cleanup registration when component is destroyed
1986
- this.#destroyRef.onDestroy(() => {
1987
- this.#bridge.unregisterDashboard(this.#store);
1988
- });
1989
- // Initialize from dashboardData
1990
- effect(() => {
1991
- const data = this.dashboardData();
1992
- if (data) {
1993
- this.#store.initializeFromDto(data);
1994
- // Register with bridge service after dashboard ID is set
1995
- this.#bridge.updateDashboardRegistration(this.#store);
1996
- this.#isInitialized = true;
1997
- }
1998
- });
1999
- // Sync edit mode with store (without triggering state preservation)
2000
- effect(() => {
2001
- const editMode = this.editMode();
2002
- untracked(() => {
2003
- this.#store.setEditMode(editMode);
2004
- });
2005
- });
2006
- // Sync reserved space input with viewport service
2007
- effect(() => {
2008
- const reserved = this.reservedSpace();
2009
- if (reserved) {
2010
- this.#viewport.setReservedSpace(reserved);
2011
- }
2012
- });
2013
- }
2014
- ngOnChanges(changes) {
2015
- // Handle edit mode changes after initialization
2016
- if (changes['editMode'] &&
2017
- !changes['editMode'].firstChange &&
2018
- this.#isInitialized) {
2019
- const previousValue = changes['editMode'].previousValue;
2020
- const currentValue = changes['editMode'].currentValue;
2021
- if (previousValue !== currentValue) {
2022
- // Preserve widget states before the mode switch
2023
- this.#preserveWidgetStatesBeforeModeSwitch(previousValue);
2024
- }
2025
- }
2026
- }
2027
- // Public export/import methods
2028
- exportDashboard() {
2029
- // Delegate to the active child component
2030
- if (this.editMode()) {
2031
- const editor = this.dashboardEditor();
2032
- return editor ? editor.exportDashboard() : this.#store.exportDashboard();
2033
- }
2034
- else {
2035
- const viewer = this.dashboardViewer();
2036
- return viewer ? viewer.exportDashboard() : this.#store.exportDashboard();
2037
- }
2038
- }
2039
- loadDashboard(data) {
2040
- this.#store.loadDashboard(data);
2041
- }
2042
- getCurrentDashboardData() {
2043
- return this.exportDashboard();
2044
- }
2045
- clearDashboard() {
2046
- this.#store.clearDashboard();
2047
- }
2048
- /**
2049
- * Preserve widget states before switching modes by collecting live states
2050
- * from the currently active component and updating the store.
2051
- */
2052
- #preserveWidgetStatesBeforeModeSwitch(previousEditMode) {
2053
- // Prevent re-entrant calls
2054
- if (this.#isPreservingStates) {
2055
- return;
2056
- }
2057
- this.#isPreservingStates = true;
2058
- try {
2059
- let currentWidgetStates = null;
2060
- if (previousEditMode) {
2061
- // Previously in edit mode, collect states from editor
2062
- const editor = this.dashboardEditor();
2063
- if (editor) {
2064
- currentWidgetStates = editor.getCurrentWidgetStates();
2065
- }
2066
- }
2067
- else {
2068
- // Previously in view mode, collect states from viewer
2069
- const viewer = this.dashboardViewer();
2070
- if (viewer) {
2071
- currentWidgetStates = viewer.getCurrentWidgetStates();
2072
- }
2073
- }
2074
- // Update the store with the live widget states using untracked to avoid triggering effects
2075
- if (currentWidgetStates && currentWidgetStates.size > 0) {
2076
- untracked(() => {
2077
- this.#store.updateAllWidgetStates(currentWidgetStates);
2078
- });
2079
- }
2080
- }
2081
- finally {
2082
- this.#isPreservingStates = false;
2083
- }
2084
- }
2085
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2086
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: DashboardComponent, isStandalone: true, selector: "ngx-dashboard", inputs: { dashboardData: { classPropertyName: "dashboardData", publicName: "dashboardData", isSignal: true, isRequired: true, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null }, reservedSpace: { classPropertyName: "reservedSpace", publicName: "reservedSpace", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.--rows": "store.rows()", "style.--columns": "store.columns()", "style.--gutter-size": "store.gutterSize()", "style.--gutters": "store.columns() + 1", "class.is-edit-mode": "editMode()", "style.max-width.px": "viewport.constraints().maxWidth", "style.max-height.px": "viewport.constraints().maxHeight" } }, providers: [DashboardStore, DashboardViewportService], viewQueries: [{ propertyName: "dashboardEditor", first: true, predicate: DashboardEditorComponent, descendants: true, isSignal: true }, { propertyName: "dashboardViewer", first: true, predicate: DashboardViewerComponent, descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<!-- dashboard.component.html -->\r\n<div class=\"grid-container\">\r\n @if (editMode()) {\r\n <!-- Full editor with drag & drop capabilities -->\r\n <ngx-dashboard-editor\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-editor>\r\n } @else {\r\n <!-- Read-only viewer -->\r\n <ngx-dashboard-viewer\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-viewer>\r\n }\r\n</div>\r\n", styles: [":host{display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}.grid-container{position:relative;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: DashboardViewerComponent, selector: "ngx-dashboard-viewer", inputs: ["rows", "columns", "gutterSize"] }, { kind: "component", type: DashboardEditorComponent, selector: "ngx-dashboard-editor", inputs: ["rows", "columns", "gutterSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2087
- }
2088
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DashboardComponent, decorators: [{
2089
- type: Component,
2090
- args: [{ selector: 'ngx-dashboard', standalone: true, imports: [CommonModule, DashboardViewerComponent, DashboardEditorComponent], providers: [DashboardStore, DashboardViewportService], changeDetection: ChangeDetectionStrategy.OnPush, host: {
2091
- '[style.--rows]': 'store.rows()',
2092
- '[style.--columns]': 'store.columns()',
2093
- '[style.--gutter-size]': 'store.gutterSize()',
2094
- '[style.--gutters]': 'store.columns() + 1',
2095
- '[class.is-edit-mode]': 'editMode()',
2096
- '[style.max-width.px]': 'viewport.constraints().maxWidth',
2097
- '[style.max-height.px]': 'viewport.constraints().maxHeight',
2098
- }, template: "<!-- dashboard.component.html -->\r\n<div class=\"grid-container\">\r\n @if (editMode()) {\r\n <!-- Full editor with drag & drop capabilities -->\r\n <ngx-dashboard-editor\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-editor>\r\n } @else {\r\n <!-- Read-only viewer -->\r\n <ngx-dashboard-viewer\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-viewer>\r\n }\r\n</div>\r\n", styles: [":host{display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}.grid-container{position:relative;width:100%;height:100%}\n"] }]
2099
- }], ctorParameters: () => [] });
2100
-
2101
- // widget-list.component.ts
2102
- class WidgetListComponent {
2103
- #service = inject(DashboardService);
2104
- #sanitizer = inject(DomSanitizer);
2105
- #renderer = inject(Renderer2);
2106
- #bridge = inject(DashboardBridgeService);
2107
- activeWidget = signal(null);
2108
- // Get grid cell dimensions from bridge service (uses first available dashboard)
2109
- gridCellDimensions = this.#bridge.availableDimensions;
2110
- widgets = computed(() => this.#service.widgetTypes().map((w) => ({
2111
- ...w.metadata,
2112
- safeSvgIcon: this.#sanitizer.bypassSecurityTrustHtml(w.metadata.svgIcon),
2113
- })));
2114
- onDragStart(event, widget) {
2115
- if (!event.dataTransfer)
2116
- return;
2117
- event.dataTransfer.effectAllowed = 'copy';
2118
- const dragData = {
2119
- kind: 'widget',
2120
- content: widget,
2121
- };
2122
- this.activeWidget.set(widget.widgetTypeid);
2123
- this.#bridge.startDrag(dragData);
2124
- // Create custom drag ghost for better UX
2125
- const ghost = this.#createDragGhost(widget.svgIcon);
2126
- document.body.appendChild(ghost);
2127
- // Force reflow to ensure element is rendered
2128
- const _reflow = ghost.offsetHeight;
2129
- event.dataTransfer.setDragImage(ghost, 10, 10);
2130
- // Delay removal to ensure browser has time to snapshot the drag image
2131
- setTimeout(() => ghost.remove());
2132
- }
2133
- onDragEnd() {
2134
- this.activeWidget.set(null);
2135
- this.#bridge.endDrag();
2136
- }
2137
- #createDragGhost(svgIcon) {
2138
- const dimensions = this.gridCellDimensions();
2139
- const el = this.#renderer.createElement('div');
2140
- el.classList.add('drag-ghost');
2141
- if (svgIcon) {
2142
- const iconWrapper = this.#renderer.createElement('div');
2143
- this.#renderer.addClass(iconWrapper, 'icon');
2144
- iconWrapper.innerHTML = svgIcon;
2145
- const svg = iconWrapper.querySelector('svg');
2146
- if (svg) {
2147
- svg.setAttribute('width', `${dimensions.width * 0.8}`);
2148
- svg.setAttribute('height', `${dimensions.height * 0.8}`);
2149
- svg.setAttribute('fill', '#000000');
2150
- svg.setAttribute('opacity', '0.3');
2151
- }
2152
- el.appendChild(iconWrapper);
2153
- }
2154
- Object.assign(el.style, {
2155
- boxSizing: 'border-box',
2156
- position: 'absolute',
2157
- top: '0',
2158
- left: '0',
2159
- width: `${dimensions.width}px`,
2160
- height: `${dimensions.height}px`,
2161
- zIndex: '9999',
2162
- margin: '0',
2163
- background: 'white',
2164
- border: '1px solid #ccc',
2165
- borderRadius: '4px',
2166
- pointerEvents: 'none',
2167
- display: 'flex',
2168
- alignItems: 'center',
2169
- justifyContent: 'center',
2170
- opacity: 0.7,
2171
- });
2172
- return el;
2173
- }
2174
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: WidgetListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2175
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: WidgetListComponent, isStandalone: true, selector: "ngx-dashboard-widget-list", ngImport: i0, template: "<!-- widget-list.component.html -->\r\n<div class=\"widget-list\">\r\n @for (widget of widgets(); track widget.widgetTypeid) {\r\n <div\r\n class=\"widget-list-item\"\r\n [class.active]=\"activeWidget() === widget.widgetTypeid\"\r\n draggable=\"true\"\r\n (dragstart)=\"onDragStart($event, widget)\"\r\n (dragend)=\"onDragEnd()\"\r\n >\r\n <div class=\"icon\" [innerHTML]=\"widget.safeSvgIcon\"></div>\r\n <div class=\"content\">\r\n <strong>{{ widget.name }}</strong>\r\n <small>{{ widget.description }}</small>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [":host{background-color:var(--mat-sys-surface-container)}.widget-list{display:flex;flex-direction:column;gap:.5em}.widget-list-item{display:flex;align-items:start;gap:.75em;background:#fff;border:1px solid #ccc;padding:.75em;border-radius:4px;cursor:grab;box-shadow:0 1px 2px #0000000d;transition:background .2s}.widget-list-item .icon{width:24px;height:24px;flex-shrink:0}.widget-list-item .icon svg{width:100%;height:100%;display:block}.widget-list-item .content{display:flex;flex-direction:column;line-height:1.2}.widget-list-item:hover{background:#f0f0f0}.widget-list-item:active{cursor:grabbing}.widget-list-item.active{background:#e6f7ff;border-color:#91d5ff}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2176
- }
2177
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: WidgetListComponent, decorators: [{
2178
- type: Component,
2179
- args: [{ selector: 'ngx-dashboard-widget-list', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- widget-list.component.html -->\r\n<div class=\"widget-list\">\r\n @for (widget of widgets(); track widget.widgetTypeid) {\r\n <div\r\n class=\"widget-list-item\"\r\n [class.active]=\"activeWidget() === widget.widgetTypeid\"\r\n draggable=\"true\"\r\n (dragstart)=\"onDragStart($event, widget)\"\r\n (dragend)=\"onDragEnd()\"\r\n >\r\n <div class=\"icon\" [innerHTML]=\"widget.safeSvgIcon\"></div>\r\n <div class=\"content\">\r\n <strong>{{ widget.name }}</strong>\r\n <small>{{ widget.description }}</small>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [":host{background-color:var(--mat-sys-surface-container)}.widget-list{display:flex;flex-direction:column;gap:.5em}.widget-list-item{display:flex;align-items:start;gap:.75em;background:#fff;border:1px solid #ccc;padding:.75em;border-radius:4px;cursor:grab;box-shadow:0 1px 2px #0000000d;transition:background .2s}.widget-list-item .icon{width:24px;height:24px;flex-shrink:0}.widget-list-item .icon svg{width:100%;height:100%;display:block}.widget-list-item .content{display:flex;flex-direction:column;line-height:1.2}.widget-list-item:hover{background:#f0f0f0}.widget-list-item:active{cursor:grabbing}.widget-list-item.active{background:#e6f7ff;border-color:#91d5ff}\n"] }]
2180
- }] });
2181
-
2182
- /*
2183
- * Public API Surface of ngx-dashboard
2184
- */
2185
- // Main dashboard components
2186
-
2187
- /**
2188
- * Generated bundle index. Do not edit.
2189
- */
2190
-
2191
- export { CELL_SETTINGS_DIALOG_PROVIDER, CellIdUtils, CellSettingsDialogProvider, DEFAULT_RESERVED_SPACE, DashboardComponent, DashboardEditorComponent, DashboardService, DashboardViewerComponent, DefaultCellSettingsDialogProvider, WidgetListComponent, createDefaultDashboard, createEmptyDashboard, createFactoryFromComponent };
2192
- //# sourceMappingURL=dragonworks-ngx-dashboard.mjs.map