@dragonworks/ngx-dashboard 20.0.5 → 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 -2178
  77. package/fesm2022/dragonworks-ngx-dashboard.mjs.map +0 -1
  78. package/index.d.ts +0 -678
@@ -0,0 +1,36 @@
1
+ import { Injectable, signal } from '@angular/core';
2
+
3
+ export type CellContextMenuItem =
4
+ | {
5
+ label: string;
6
+ icon?: string; // Material icon name (e.g., 'edit', 'settings', 'delete')
7
+ action: () => void;
8
+ disabled?: boolean;
9
+ divider?: false;
10
+ }
11
+ | {
12
+ divider: true;
13
+ label?: never;
14
+ icon?: never;
15
+ action?: never;
16
+ disabled?: never;
17
+ };
18
+
19
+ @Injectable()
20
+ export class CellContextMenuService {
21
+ #activeMenu = signal<{
22
+ x: number;
23
+ y: number;
24
+ items: CellContextMenuItem[];
25
+ } | null>(null);
26
+
27
+ activeMenu = this.#activeMenu.asReadonly();
28
+
29
+ show(x: number, y: number, items: CellContextMenuItem[]) {
30
+ this.#activeMenu.set({ x, y, items });
31
+ }
32
+
33
+ hide() {
34
+ this.#activeMenu.set(null);
35
+ }
36
+ }
@@ -0,0 +1,37 @@
1
+ <!-- cell.component.html -->
2
+ <div
3
+ class="cell"
4
+ [class.is-resizing]="isResizing()"
5
+ [class.flat]="flat() === true"
6
+ [draggable]="draggable()"
7
+ (dragstart)="onDragStart($event)"
8
+ (dragend)="onDragEnd()"
9
+ (contextmenu)="onContextMenu($event)"
10
+ >
11
+ <div class="content-area">
12
+ <ng-template #container></ng-template>
13
+ </div>
14
+ @if (isEditMode() && !isDragging()) {
15
+ <!-- Right resize handle -->
16
+ <div
17
+ class="resize-handle resize-handle--right"
18
+ (mousedown)="onResizeStart($event, 'horizontal')"
19
+ >
20
+ <div class="resize-handle-line"></div>
21
+ </div>
22
+ <!-- Bottom resize handle -->
23
+ <div
24
+ class="resize-handle resize-handle--bottom"
25
+ (mousedown)="onResizeStart($event, 'vertical')"
26
+ >
27
+ <div class="resize-handle-line"></div>
28
+ </div>
29
+ }
30
+ </div>
31
+
32
+ @if (isResizing()) {
33
+ <div class="resize-preview">
34
+ {{ resizeData()?.previewColSpan ?? colSpan() }} ×
35
+ {{ resizeData()?.previewRowSpan ?? rowSpan() }}
36
+ </div>
37
+ }
@@ -0,0 +1,198 @@
1
+ // cell.component.sccs
2
+
3
+ :host {
4
+ display: block;
5
+ width: 100%;
6
+ height: 100%;
7
+ position: relative;
8
+ z-index: 1;
9
+ container-type: inline-size; // Enable container queries
10
+ }
11
+
12
+ // When any drag is active, disable pointer events on non-dragging cells
13
+ // This allows drag events to pass through to drop zones underneath
14
+ :host(.drag-active):not(.is-dragging) {
15
+ pointer-events: none;
16
+ }
17
+
18
+ :host(.is-dragging) {
19
+ z-index: 100; // While dragging
20
+ opacity: 0.5;
21
+ pointer-events: none; // Allow drag events to pass through to drop zones
22
+
23
+ // Ensure content area also doesn't block events during drag
24
+ .content-area {
25
+ pointer-events: none;
26
+ }
27
+ }
28
+
29
+ :host(:hover) .resize-handle {
30
+ opacity: 1;
31
+ }
32
+
33
+ .cell {
34
+ width: 100%;
35
+ height: 100%;
36
+ // background-color: white;
37
+ border-radius: 4px;
38
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
39
+ padding: 0;
40
+ box-sizing: border-box;
41
+ overflow: hidden;
42
+ position: relative; // for overlay positioning
43
+ container-type: inline-size; // enable container queries on cell
44
+
45
+ &:hover {
46
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
47
+ transform: translateY(-2px);
48
+ }
49
+
50
+ // Flat mode styles
51
+ &.flat {
52
+ box-shadow: none;
53
+ border: none; //1px solid #e0e0e0;
54
+
55
+ &:hover {
56
+ box-shadow: none;
57
+ transform: none;
58
+ border-color: #bdbdbd;
59
+ }
60
+ }
61
+
62
+ // prevent text selection during resize
63
+ &.resizing {
64
+ user-select: none;
65
+ }
66
+ }
67
+
68
+ .content-area {
69
+ width: 100%;
70
+ height: 100%;
71
+ overflow: auto;
72
+ // Ensure widget hover events work independently of cell hover state
73
+ pointer-events: auto;
74
+ position: relative;
75
+ z-index: 1;
76
+
77
+ // Isolate widget content from parent hover effects
78
+ &:hover {
79
+ // Reset any inherited hover effects that might interfere with widget hover
80
+ transform: initial;
81
+ }
82
+ }
83
+
84
+ // Ensure flat mode doesn't interfere with widget hover events
85
+ .cell.flat .content-area {
86
+ // Explicitly ensure widget hover works in flat mode
87
+ pointer-events: auto;
88
+
89
+ &:hover {
90
+ // Prevent flat mode hover reset from affecting widget content
91
+ transform: initial;
92
+ }
93
+ }
94
+
95
+ ///
96
+ /// Resize stuff
97
+ ///
98
+ .resize-handle {
99
+ position: absolute;
100
+ z-index: 20;
101
+ // transition: opacity 0.2s ease;
102
+
103
+ &--right {
104
+ cursor: col-resize;
105
+ width: 16px;
106
+ height: 100%;
107
+ right: -8px;
108
+ top: 0;
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ opacity: 0;
113
+
114
+ &:hover {
115
+ opacity: 1;
116
+
117
+ .resize-handle-line {
118
+ background-color: var(--mat-sys-primary-container);
119
+ // background-color: #2196f3;
120
+ }
121
+ }
122
+ }
123
+
124
+ &--bottom {
125
+ cursor: row-resize;
126
+ width: 100%;
127
+ height: 16px;
128
+ bottom: -8px;
129
+ left: 0;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ opacity: 0;
134
+
135
+ &:hover {
136
+ opacity: 1;
137
+
138
+ .resize-handle-line {
139
+ background-color: var(--mat-sys-primary-container);
140
+ // background-color: #2196f3;
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ .resize-handle-line {
147
+ background-color: rgba(0, 0, 0, 0.1);
148
+ // transition: background-color 0.2s ease;
149
+
150
+ .resize-handle--right & {
151
+ width: 8px;
152
+ height: 40px;
153
+ border-radius: 2px;
154
+ }
155
+
156
+ .resize-handle--bottom & {
157
+ width: 40px;
158
+ height: 8px;
159
+ border-radius: 2px;
160
+ }
161
+ }
162
+
163
+ .resize-preview {
164
+ position: absolute;
165
+ top: 50%;
166
+ left: 50%;
167
+ transform: translate(-50%, -50%);
168
+ background-color: var(--mat-sys-primary);
169
+ color: var(--mat-sys-on-primary);
170
+
171
+ // background: rgba(33, 150, 243, 0.9);
172
+ // color: white;
173
+ padding: 4px 12px;
174
+ border-radius: 4px;
175
+ font-size: 14px;
176
+ font-weight: 500;
177
+ pointer-events: none;
178
+ z-index: 30;
179
+ }
180
+
181
+ .cell.is-resizing {
182
+ opacity: 0.6;
183
+ .resize-handle {
184
+ background-color: rgba(33, 150, 243, 0.5);
185
+ }
186
+ }
187
+
188
+ // Global cursor classes for resize operations
189
+ // These are applied to document.body to ensure cursor shows everywhere during resize
190
+ :root {
191
+ .cursor-col-resize {
192
+ cursor: col-resize !important;
193
+ }
194
+
195
+ .cursor-row-resize {
196
+ cursor: row-resize !important;
197
+ }
198
+ }
@@ -0,0 +1,375 @@
1
+ // cell.component.ts
2
+ import {
3
+ Component,
4
+ ComponentRef,
5
+ computed,
6
+ DestroyRef,
7
+ effect,
8
+ inject,
9
+ input,
10
+ model,
11
+ signal,
12
+ ViewContainerRef,
13
+ output,
14
+ ElementRef,
15
+ viewChild,
16
+ Renderer2,
17
+ ChangeDetectionStrategy,
18
+ } from '@angular/core';
19
+ // RxJS removed: Using native DOM events with Renderer2 for performance
20
+ // - Context menu uses template event binding (element-specific)
21
+ // - Resize uses conditional document listeners (only when actively resizing)
22
+ // - Eliminates N*mousemove performance issue with @HostListener approach
23
+ import { CommonModule } from '@angular/common';
24
+ import {
25
+ CellId,
26
+ CellIdUtils,
27
+ WidgetId,
28
+ DragData,
29
+ WidgetFactory,
30
+ Widget,
31
+ } from '../models';
32
+ import { DashboardStore } from '../store/dashboard-store';
33
+ import { CellDisplayData } from '../models';
34
+ import { CELL_SETTINGS_DIALOG_PROVIDER } from '../providers/cell-settings-dialog';
35
+ import { CellContextMenuService, CellContextMenuItem } from './cell-context-menu.service';
36
+
37
+ @Component({
38
+ selector: 'lib-cell',
39
+ standalone: true,
40
+ imports: [CommonModule],
41
+ changeDetection: ChangeDetectionStrategy.OnPush,
42
+ templateUrl: './cell.component.html',
43
+ styleUrl: './cell.component.scss',
44
+ host: {
45
+ '[style.grid-row]': 'gridRowStyle()',
46
+ '[style.grid-column]': 'gridColumnStyle()',
47
+ '[class.is-dragging]': 'isDragging()',
48
+ '[class.drag-active]': 'isDragActive()',
49
+ '[class.flat]': 'flat() === true',
50
+ },
51
+ })
52
+ export class CellComponent {
53
+ widgetId = input.required<WidgetId>(); // Unique widget instance identifier
54
+ cellId = input.required<CellId>(); // Current grid position
55
+ widgetFactory = input<WidgetFactory | undefined>(undefined);
56
+ widgetState = input<unknown | undefined>(undefined);
57
+ isEditMode = input<boolean>(false);
58
+ flat = input<boolean | undefined>(undefined);
59
+
60
+ row = model.required<number>();
61
+ column = model.required<number>();
62
+ rowSpan = input<number>(1);
63
+ colSpan = input<number>(1);
64
+ draggable = input<boolean>(false);
65
+
66
+ dragStart = output<DragData>();
67
+ dragEnd = output<void>();
68
+
69
+ edit = output<WidgetId>();
70
+ delete = output<WidgetId>();
71
+ settings = output<{ id: WidgetId; flat: boolean }>();
72
+ resizeStart = output<{ cellId: CellId; direction: 'horizontal' | 'vertical' }>();
73
+ resizeMove = output<{
74
+ cellId: CellId;
75
+ direction: 'horizontal' | 'vertical';
76
+ delta: number;
77
+ }>();
78
+ resizeEnd = output<{ cellId: CellId; apply: boolean }>();
79
+
80
+ private container = viewChild.required<ElementRef, ViewContainerRef>(
81
+ 'container',
82
+ { read: ViewContainerRef }
83
+ );
84
+
85
+ readonly #store = inject(DashboardStore);
86
+ readonly #dialogProvider = inject(CELL_SETTINGS_DIALOG_PROVIDER);
87
+ readonly #destroyRef = inject(DestroyRef);
88
+ readonly #renderer = inject(Renderer2);
89
+ readonly #contextMenuService = inject(CellContextMenuService, {
90
+ optional: true,
91
+ });
92
+ readonly #elementRef = inject(ElementRef);
93
+
94
+ #widgetRef?: ComponentRef<Widget>;
95
+
96
+ // Document event listeners cleanup function
97
+ // Performance: Only created when actively resizing, not for every cell
98
+ #documentListeners?: () => void;
99
+
100
+ isDragging = signal(false);
101
+
102
+ readonly gridRowStyle = computed(
103
+ () => `${this.row()} / span ${this.rowSpan()}`
104
+ );
105
+ readonly gridColumnStyle = computed(
106
+ () => `${this.column()} / span ${this.colSpan()}`
107
+ );
108
+
109
+ isResizing = computed(() => {
110
+ const resizeData = this.#store.resizeData();
111
+ return resizeData
112
+ ? CellIdUtils.equals(resizeData.cellId, this.cellId())
113
+ : false;
114
+ });
115
+
116
+ isDragActive = computed(() => !!this.#store.dragData());
117
+
118
+ resizeData = this.#store.resizeData;
119
+ gridCellDimensions = this.#store.gridCellDimensions;
120
+ private resizeDirection = signal<'horizontal' | 'vertical' | null>(null);
121
+ private resizeStartPos = signal({ x: 0, y: 0 });
122
+
123
+ constructor() {
124
+ // widget creation - triggers when factory or state changes
125
+ effect(() => {
126
+ const factory = this.widgetFactory();
127
+ const state = this.widgetState();
128
+ const container = this.container();
129
+
130
+ if (factory && container) {
131
+ // Clean up previous widget
132
+ this.#widgetRef?.destroy();
133
+
134
+ // Create new widget
135
+ container.clear();
136
+ try {
137
+ this.#widgetRef = factory.createInstance(container, state);
138
+ } catch (error) {
139
+ console.error('Failed to create widget:', error);
140
+ this.#widgetRef = undefined;
141
+ }
142
+ }
143
+ });
144
+
145
+ // Auto cleanup on destroy
146
+ this.#destroyRef.onDestroy(() => {
147
+ this.#widgetRef?.destroy();
148
+ this.#widgetRef = undefined;
149
+ // Clean up any active document listeners
150
+ this.#cleanupDocumentListeners();
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Setup document-level event listeners for resize operations
156
+ * Performance: Only creates listeners when actively resizing (not for every cell)
157
+ * Angular-idiomatic: Uses Renderer2 for dynamic listener management
158
+ */
159
+ private setupDocumentListeners(): void {
160
+ // Clean up any existing listeners first
161
+ this.#cleanupDocumentListeners();
162
+
163
+ // Create document listeners with proper cleanup functions
164
+ const unlistenMove = this.#renderer.listen(
165
+ 'document',
166
+ 'mousemove',
167
+ this.handleResizeMove.bind(this)
168
+ );
169
+ const unlistenUp = this.#renderer.listen(
170
+ 'document',
171
+ 'mouseup',
172
+ this.handleResizeEnd.bind(this)
173
+ );
174
+
175
+ // Store cleanup function for later use
176
+ this.#documentListeners = () => {
177
+ unlistenMove();
178
+ unlistenUp();
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Clean up document-level event listeners
184
+ * Called on resize end and component destruction
185
+ */
186
+ #cleanupDocumentListeners(): void {
187
+ if (this.#documentListeners) {
188
+ this.#documentListeners();
189
+ this.#documentListeners = undefined;
190
+ }
191
+ }
192
+
193
+ setPosition(row: number, column: number): void {
194
+ this.row.set(row);
195
+ this.column.set(column);
196
+ }
197
+
198
+ onDragStart(event: DragEvent): void {
199
+ if (!event.dataTransfer) return;
200
+ event.dataTransfer.effectAllowed = 'move';
201
+
202
+ const cell = {
203
+ cellId: this.cellId(),
204
+ widgetId: this.widgetId(),
205
+ row: this.row(),
206
+ col: this.column(),
207
+ rowSpan: this.rowSpan(),
208
+ colSpan: this.colSpan(),
209
+ };
210
+
211
+ const content: DragData = { kind: 'cell', content: cell };
212
+ this.dragStart.emit(content);
213
+ this.isDragging.set(true);
214
+ }
215
+
216
+ onDragEnd(/*_: DragEvent*/): void {
217
+ this.isDragging.set(false);
218
+ this.dragEnd.emit();
219
+ }
220
+
221
+ /**
222
+ * Handle context menu events (called from template)
223
+ * Performance: Element-specific event binding, not document-level
224
+ * Angular-idiomatic: Template event binding instead of fromEvent
225
+ */
226
+ onContextMenu(event: MouseEvent): void {
227
+ if (!this.isEditMode() || !this.#contextMenuService) return;
228
+
229
+ event.preventDefault();
230
+ event.stopPropagation();
231
+
232
+ const items: CellContextMenuItem[] = [
233
+ {
234
+ label: 'Edit Widget',
235
+ icon: 'edit',
236
+ action: () => this.onEdit(),
237
+ disabled: !this.canEdit(),
238
+ },
239
+ {
240
+ label: 'Settings',
241
+ icon: 'settings',
242
+ action: () => this.onSettings(),
243
+ },
244
+ { divider: true },
245
+ {
246
+ label: 'Delete',
247
+ icon: 'delete',
248
+ action: () => this.onDelete(),
249
+ },
250
+ ];
251
+
252
+ // Position menu at exact mouse coordinates
253
+ this.#contextMenuService.show(event.clientX, event.clientY, items);
254
+ }
255
+
256
+ canEdit(): boolean {
257
+ if (this.#widgetRef?.instance?.dashboardEditState) {
258
+ return true;
259
+ }
260
+ return false;
261
+ }
262
+
263
+ onEdit(): void {
264
+ this.edit.emit(this.widgetId());
265
+
266
+ // Call the widget's edit dialog method if it exists
267
+ if (this.#widgetRef?.instance?.dashboardEditState) {
268
+ this.#widgetRef.instance.dashboardEditState();
269
+ }
270
+ }
271
+
272
+ onDelete(): void {
273
+ this.delete.emit(this.widgetId());
274
+ }
275
+
276
+ async onSettings(): Promise<void> {
277
+ const currentSettings: CellDisplayData = {
278
+ id: CellIdUtils.toString(this.cellId()), // Use cellId for display position
279
+ flat: this.flat(),
280
+ };
281
+
282
+ try {
283
+ const result = await this.#dialogProvider.openCellSettings(
284
+ currentSettings
285
+ );
286
+
287
+ if (result) {
288
+ this.settings.emit({
289
+ id: this.widgetId(),
290
+ flat: result.flat ?? false,
291
+ });
292
+ }
293
+ } catch (error) {
294
+ console.error('Error opening cell settings dialog:', error);
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Start resize operation and setup document listeners
300
+ * Performance: Only THIS cell creates document listeners when actively resizing
301
+ * RxJS-free: Uses Renderer2 for dynamic listener management
302
+ */
303
+ onResizeStart(event: MouseEvent, direction: 'horizontal' | 'vertical'): void {
304
+ event.preventDefault();
305
+ event.stopPropagation();
306
+
307
+ this.resizeDirection.set(direction);
308
+ this.resizeStartPos.set({ x: event.clientX, y: event.clientY });
309
+ this.resizeStart.emit({ cellId: this.cellId(), direction });
310
+
311
+ // Setup document listeners only when actively resizing
312
+ this.setupDocumentListeners();
313
+
314
+ const cursorClass =
315
+ direction === 'horizontal' ? 'cursor-col-resize' : 'cursor-row-resize';
316
+ this.#renderer.addClass(document.body, cursorClass);
317
+ }
318
+
319
+ /**
320
+ * Handle resize move events (called from document listener)
321
+ * Performance: Only called for the actively resizing cell
322
+ * Bound method: Maintains component context without arrow functions
323
+ */
324
+ private handleResizeMove(event: MouseEvent): void {
325
+ const direction = this.resizeDirection();
326
+ if (!direction) return;
327
+
328
+ const startPos = this.resizeStartPos();
329
+ const cellSize = this.gridCellDimensions();
330
+
331
+ if (direction === 'horizontal') {
332
+ const deltaX = event.clientX - startPos.x;
333
+ const deltaSpan = Math.round(deltaX / cellSize.width);
334
+ this.resizeMove.emit({ cellId: this.cellId(), direction, delta: deltaSpan });
335
+ } else {
336
+ const deltaY = event.clientY - startPos.y;
337
+ const deltaSpan = Math.round(deltaY / cellSize.height);
338
+ this.resizeMove.emit({ cellId: this.cellId(), direction, delta: deltaSpan });
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Handle resize end events (called from document listener)
344
+ * Performance: Cleans up document listeners immediately after resize
345
+ * State cleanup: Resets resize direction to stop further event processing
346
+ */
347
+ private handleResizeEnd(): void {
348
+ this.#renderer.removeClass(document.body, 'cursor-col-resize');
349
+ this.#renderer.removeClass(document.body, 'cursor-row-resize');
350
+
351
+ // Clean up document listeners immediately
352
+ this.#cleanupDocumentListeners();
353
+
354
+ this.resizeEnd.emit({ cellId: this.cellId(), apply: true });
355
+ this.resizeDirection.set(null);
356
+ }
357
+
358
+ /**
359
+ * Get the current widget state by calling dashboardGetState() on the widget instance.
360
+ * Used during dashboard export to get live widget state instead of stale stored state.
361
+ */
362
+ getCurrentWidgetState(): unknown | undefined {
363
+ if (!this.#widgetRef?.instance) {
364
+ return undefined;
365
+ }
366
+
367
+ // Call dashboardGetState() if the widget implements it
368
+ if (typeof this.#widgetRef.instance.dashboardGetState === 'function') {
369
+ return this.#widgetRef.instance.dashboardGetState();
370
+ }
371
+
372
+ // Fall back to stored state if widget doesn't implement dashboardGetState
373
+ return this.widgetState();
374
+ }
375
+ }
@@ -0,0 +1,18 @@
1
+ <!-- dashboard.component.html -->
2
+ <div class="grid-container">
3
+ @if (editMode()) {
4
+ <!-- Full editor with drag & drop capabilities -->
5
+ <ngx-dashboard-editor
6
+ [rows]="store.rows()"
7
+ [columns]="store.columns()"
8
+ [gutterSize]="store.gutterSize()"
9
+ ></ngx-dashboard-editor>
10
+ } @else {
11
+ <!-- Read-only viewer -->
12
+ <ngx-dashboard-viewer
13
+ [rows]="store.rows()"
14
+ [columns]="store.columns()"
15
+ [gutterSize]="store.gutterSize()"
16
+ ></ngx-dashboard-viewer>
17
+ }
18
+ </div>
@@ -0,0 +1,17 @@
1
+ /* dashboard.component.scss */
2
+ :host {
3
+ display: block;
4
+ container-type: inline-size;
5
+ box-sizing: border-box;
6
+ aspect-ratio: var(--columns) / var(--rows);
7
+ width: 100%;
8
+ height: auto;
9
+
10
+ // background-color: var(--mat-sys-surface-container);
11
+ }
12
+
13
+ .grid-container {
14
+ position: relative;
15
+ width: 100%;
16
+ height: 100%;
17
+ }