@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
@@ -0,0 +1,465 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { ElementRef } from '@angular/core';
3
+ import { DropZoneComponent } from '../drop-zone.component';
4
+ import { DashboardStore } from '../../store/dashboard-store';
5
+ import { DragData, CellIdUtils, WidgetIdUtils, WidgetMetadata } from '../../models';
6
+ import { DashboardService } from '../../services/dashboard.service';
7
+
8
+ describe('DropZoneComponent - Focused Regression Tests', () => {
9
+ let component: DropZoneComponent;
10
+ let fixture: ComponentFixture<DropZoneComponent>;
11
+ let store: InstanceType<typeof DashboardStore>;
12
+ let dashboardService: jasmine.SpyObj<DashboardService>;
13
+
14
+ const testWidgetMetadata: WidgetMetadata = {
15
+ widgetTypeid: 'test-widget',
16
+ name: 'Test Widget',
17
+ description: 'A test widget',
18
+ svgIcon: '<svg></svg>'
19
+ };
20
+
21
+ const cellDragData: DragData = {
22
+ kind: 'cell',
23
+ content: {
24
+ cellId: CellIdUtils.create(2, 2),
25
+ widgetId: WidgetIdUtils.generate(),
26
+ row: 2,
27
+ col: 2,
28
+ rowSpan: 1,
29
+ colSpan: 1
30
+ }
31
+ };
32
+
33
+ const widgetDragData: DragData = {
34
+ kind: 'widget',
35
+ content: testWidgetMetadata
36
+ };
37
+
38
+ beforeEach(async () => {
39
+ const spy = jasmine.createSpyObj('DashboardService', ['getFactory']);
40
+
41
+ await TestBed.configureTestingModule({
42
+ imports: [DropZoneComponent],
43
+ providers: [
44
+ DashboardStore,
45
+ { provide: DashboardService, useValue: spy }
46
+ ]
47
+ }).compileComponents();
48
+
49
+ fixture = TestBed.createComponent(DropZoneComponent);
50
+ component = fixture.componentInstance;
51
+ store = TestBed.inject(DashboardStore);
52
+ dashboardService = TestBed.inject(DashboardService) as jasmine.SpyObj<DashboardService>;
53
+
54
+ // Set required inputs
55
+ fixture.componentRef.setInput('row', 3);
56
+ fixture.componentRef.setInput('col', 4);
57
+ fixture.componentRef.setInput('index', 12);
58
+
59
+ fixture.detectChanges();
60
+ });
61
+
62
+ describe('Critical Drag & Drop Event Handling', () => {
63
+ it('should prevent default and stop propagation on dragenter', () => {
64
+ const event = new DragEvent('dragenter', { bubbles: true });
65
+ spyOn(event, 'preventDefault');
66
+ spyOn(event, 'stopPropagation');
67
+
68
+ component.onDragEnter(event);
69
+
70
+ expect(event.preventDefault).toHaveBeenCalled();
71
+ expect(event.stopPropagation).toHaveBeenCalled();
72
+ });
73
+
74
+ it('should prevent default and stop propagation on dragover', () => {
75
+ const event = new DragEvent('dragover', { bubbles: true });
76
+ Object.defineProperty(event, 'dataTransfer', {
77
+ value: { dropEffect: 'none' }
78
+ });
79
+ spyOn(event, 'preventDefault');
80
+ spyOn(event, 'stopPropagation');
81
+
82
+ component.onDragOver(event);
83
+
84
+ expect(event.preventDefault).toHaveBeenCalled();
85
+ expect(event.stopPropagation).toHaveBeenCalled();
86
+ });
87
+
88
+ it('should prevent default and stop propagation on dragleave', () => {
89
+ const event = new DragEvent('dragleave', { bubbles: true });
90
+ spyOn(event, 'preventDefault');
91
+ spyOn(event, 'stopPropagation');
92
+
93
+ component.onDragLeave(event);
94
+
95
+ expect(event.preventDefault).toHaveBeenCalled();
96
+ expect(event.stopPropagation).toHaveBeenCalled();
97
+ });
98
+
99
+ it('should prevent default and stop propagation on drop', () => {
100
+ const event = new DragEvent('drop', { bubbles: true });
101
+ Object.defineProperty(event, 'dataTransfer', {
102
+ value: {}
103
+ });
104
+ spyOn(event, 'preventDefault');
105
+ spyOn(event, 'stopPropagation');
106
+
107
+ component.onDrop(event);
108
+
109
+ expect(event.preventDefault).toHaveBeenCalled();
110
+ expect(event.stopPropagation).toHaveBeenCalled();
111
+ });
112
+
113
+ it('should return move drop effect for cell drag data', () => {
114
+ store.startDrag(cellDragData);
115
+ fixture.detectChanges();
116
+
117
+ expect(component.dropEffect()).toBe('move');
118
+ });
119
+
120
+ it('should return copy drop effect for widget drag data', () => {
121
+ store.startDrag(widgetDragData);
122
+ fixture.detectChanges();
123
+
124
+ expect(component.dropEffect()).toBe('copy');
125
+ });
126
+
127
+ it('should return none drop effect for invalid drops', () => {
128
+ store.endDrag();
129
+ fixture.componentRef.setInput('highlightInvalid', true);
130
+ fixture.detectChanges();
131
+
132
+ expect(component.dropEffect()).toBe('none');
133
+ });
134
+
135
+ it('should return none drop effect when no drag data', () => {
136
+ store.endDrag();
137
+ fixture.detectChanges();
138
+
139
+ expect(component.dropEffect()).toBe('none');
140
+ });
141
+
142
+ it('should detect when mouse leaves element boundaries through dragExit behavior', () => {
143
+ spyOn(component.dragExit, 'emit');
144
+ const mockBoundingRect = {
145
+ left: 100,
146
+ right: 200,
147
+ top: 50,
148
+ bottom: 150
149
+ };
150
+ spyOn(component.nativeElement, 'getBoundingClientRect').and.returnValue(mockBoundingRect as DOMRect);
151
+
152
+ // Mouse outside left boundary - should emit dragExit
153
+ const event1 = new DragEvent('dragleave', { clientX: 90, clientY: 100 });
154
+ component.onDragLeave(event1);
155
+ expect(component.dragExit.emit).toHaveBeenCalled();
156
+
157
+ // Reset spy
158
+ (component.dragExit.emit as jasmine.Spy).calls.reset();
159
+
160
+ // Mouse inside boundaries - should not emit dragExit
161
+ const event2 = new DragEvent('dragleave', { clientX: 150, clientY: 100 });
162
+ component.onDragLeave(event2);
163
+ expect(component.dragExit.emit).not.toHaveBeenCalled();
164
+ });
165
+ });
166
+
167
+ describe('Output Event Emission', () => {
168
+ it('should emit dragEnter with correct row and col data', () => {
169
+ spyOn(component.dragEnter, 'emit');
170
+ const event = new DragEvent('dragenter');
171
+
172
+ component.onDragEnter(event);
173
+
174
+ expect(component.dragEnter.emit).toHaveBeenCalledWith({ row: 3, col: 4 });
175
+ });
176
+
177
+ it('should emit dragOver with correct row and col data', () => {
178
+ spyOn(component.dragOver, 'emit');
179
+ const event = new DragEvent('dragover');
180
+ Object.defineProperty(event, 'dataTransfer', {
181
+ value: { dropEffect: 'none' }
182
+ });
183
+
184
+ component.onDragOver(event);
185
+
186
+ expect(component.dragOver.emit).toHaveBeenCalledWith({ row: 3, col: 4 });
187
+ });
188
+
189
+ it('should emit dragDrop with correct data structure when drag data exists', () => {
190
+ spyOn(component.dragDrop, 'emit');
191
+ store.startDrag(cellDragData);
192
+ fixture.detectChanges();
193
+
194
+ const event = new DragEvent('drop');
195
+ Object.defineProperty(event, 'dataTransfer', {
196
+ value: {}
197
+ });
198
+
199
+ component.onDrop(event);
200
+
201
+ expect(component.dragDrop.emit).toHaveBeenCalledWith({
202
+ data: cellDragData,
203
+ target: { row: 3, col: 4 }
204
+ });
205
+ });
206
+
207
+ it('should not emit dragDrop when no drag data exists', () => {
208
+ spyOn(component.dragDrop, 'emit');
209
+ store.endDrag();
210
+ fixture.detectChanges();
211
+
212
+ const event = new DragEvent('drop');
213
+ Object.defineProperty(event, 'dataTransfer', {
214
+ value: {}
215
+ });
216
+
217
+ component.onDrop(event);
218
+
219
+ expect(component.dragDrop.emit).not.toHaveBeenCalled();
220
+ });
221
+
222
+ it('should not emit dragDrop when dataTransfer is missing', () => {
223
+ spyOn(component.dragDrop, 'emit');
224
+ store.startDrag(cellDragData);
225
+ fixture.detectChanges();
226
+
227
+ const event = new DragEvent('drop');
228
+
229
+ component.onDrop(event);
230
+
231
+ expect(component.dragDrop.emit).not.toHaveBeenCalled();
232
+ });
233
+
234
+ it('should emit dragExit only when actually leaving element', () => {
235
+ spyOn(component.dragExit, 'emit');
236
+ const mockBoundingRect = {
237
+ left: 100,
238
+ right: 200,
239
+ top: 50,
240
+ bottom: 150
241
+ };
242
+ spyOn(component.nativeElement, 'getBoundingClientRect').and.returnValue(mockBoundingRect as DOMRect);
243
+
244
+ // Mouse outside boundaries - should emit
245
+ const event = new DragEvent('dragleave', { clientX: 90, clientY: 100 });
246
+ component.onDragLeave(event);
247
+
248
+ expect(component.dragExit.emit).toHaveBeenCalled();
249
+ });
250
+
251
+ it('should not emit dragExit when not leaving element', () => {
252
+ spyOn(component.dragExit, 'emit');
253
+ const mockBoundingRect = {
254
+ left: 100,
255
+ right: 200,
256
+ top: 50,
257
+ bottom: 150
258
+ };
259
+ spyOn(component.nativeElement, 'getBoundingClientRect').and.returnValue(mockBoundingRect as DOMRect);
260
+
261
+ // Mouse inside boundaries - should not emit
262
+ const event = new DragEvent('dragleave', { clientX: 150, clientY: 100 });
263
+ component.onDragLeave(event);
264
+
265
+ expect(component.dragExit.emit).not.toHaveBeenCalled();
266
+ });
267
+ });
268
+
269
+ describe('Visual State Management', () => {
270
+ it('should apply highlight class when highlight is true and highlightInvalid is false', () => {
271
+ fixture.componentRef.setInput('highlight', true);
272
+ fixture.componentRef.setInput('highlightInvalid', false);
273
+ fixture.detectChanges();
274
+
275
+ const dropZone = fixture.nativeElement.querySelector('.drop-zone');
276
+ expect(dropZone.classList).toContain('drop-zone--highlight');
277
+ });
278
+
279
+ it('should not apply highlight class when highlightInvalid is true', () => {
280
+ fixture.componentRef.setInput('highlight', true);
281
+ fixture.componentRef.setInput('highlightInvalid', true);
282
+ fixture.detectChanges();
283
+
284
+ const dropZone = fixture.nativeElement.querySelector('.drop-zone');
285
+ expect(dropZone.classList).not.toContain('drop-zone--highlight');
286
+ });
287
+
288
+ it('should apply invalid class when highlightInvalid is true', () => {
289
+ fixture.componentRef.setInput('highlightInvalid', true);
290
+ fixture.detectChanges();
291
+
292
+ const dropZone = fixture.nativeElement.querySelector('.drop-zone');
293
+ expect(dropZone.classList).toContain('drop-zone--invalid');
294
+ });
295
+
296
+ it('should apply resize class when highlightResize is true', () => {
297
+ fixture.componentRef.setInput('highlightResize', true);
298
+ fixture.detectChanges();
299
+
300
+ const dropZone = fixture.nativeElement.querySelector('.drop-zone');
301
+ expect(dropZone.classList).toContain('drop-zone--resize');
302
+ });
303
+
304
+ it('should display edit mode cell numbers with correct values', () => {
305
+ fixture.componentRef.setInput('editMode', true);
306
+ fixture.detectChanges();
307
+
308
+ const cellNumber = fixture.nativeElement.querySelector('.edit-mode-cell-number');
309
+ expect(cellNumber).toBeTruthy();
310
+ expect(cellNumber.textContent.trim()).toBe('12 3,4');
311
+ });
312
+
313
+ it('should not display edit mode cell numbers when editMode is false', () => {
314
+ fixture.componentRef.setInput('editMode', false);
315
+ fixture.detectChanges();
316
+
317
+ const cellNumber = fixture.nativeElement.querySelector('.edit-mode-cell-number');
318
+ expect(cellNumber).toBeFalsy();
319
+ });
320
+ });
321
+
322
+ describe('Integration & Edge Cases', () => {
323
+ it('should correctly read dragData from store', () => {
324
+ store.startDrag(cellDragData);
325
+ fixture.detectChanges();
326
+
327
+ expect(component.dragData()).toEqual(cellDragData);
328
+ });
329
+
330
+ it('should handle null dragData gracefully', () => {
331
+ store.endDrag();
332
+ fixture.detectChanges();
333
+
334
+ expect(component.dragData()).toBeNull();
335
+ expect(component.dropEffect()).toBe('none');
336
+ });
337
+
338
+ it('should set dataTransfer dropEffect on dragover when drag data exists', () => {
339
+ store.startDrag(cellDragData);
340
+ fixture.detectChanges();
341
+
342
+ const mockDataTransfer = { dropEffect: 'none' };
343
+ const event = new DragEvent('dragover');
344
+ Object.defineProperty(event, 'dataTransfer', {
345
+ value: mockDataTransfer
346
+ });
347
+
348
+ component.onDragOver(event);
349
+
350
+ expect(mockDataTransfer.dropEffect).toBe('move');
351
+ });
352
+
353
+ it('should not set dataTransfer dropEffect when no drag data', () => {
354
+ store.endDrag();
355
+ fixture.detectChanges();
356
+
357
+ const mockDataTransfer = { dropEffect: 'copy' };
358
+ const event = new DragEvent('dragover');
359
+ Object.defineProperty(event, 'dataTransfer', {
360
+ value: mockDataTransfer
361
+ });
362
+
363
+ component.onDragOver(event);
364
+
365
+ expect(mockDataTransfer.dropEffect).toBe('copy'); // Should remain unchanged
366
+ });
367
+
368
+ it('should generate unique dropZoneId', () => {
369
+ expect(component.dropZoneId()).toBe('drop-zone-3-4');
370
+ });
371
+
372
+ it('should provide correct dropData object', () => {
373
+ expect(component.dropData()).toEqual({ row: 3, col: 4 });
374
+ });
375
+
376
+ it('should handle rapid drag events without state corruption', () => {
377
+ spyOn(component.dragEnter, 'emit');
378
+ spyOn(component.dragExit, 'emit');
379
+
380
+ const mockBoundingRect = {
381
+ left: 100,
382
+ right: 200,
383
+ top: 50,
384
+ bottom: 150
385
+ };
386
+ spyOn(component.nativeElement, 'getBoundingClientRect').and.returnValue(mockBoundingRect as DOMRect);
387
+
388
+ const enterEvent = new DragEvent('dragenter');
389
+ const leaveEvent = new DragEvent('dragleave', { clientX: 90, clientY: 100 }); // Outside bounds
390
+
391
+ // Rapid fire events
392
+ component.onDragEnter(enterEvent);
393
+ component.onDragLeave(leaveEvent);
394
+ component.onDragEnter(enterEvent);
395
+ component.onDragLeave(leaveEvent);
396
+
397
+ expect(component.dragEnter.emit).toHaveBeenCalledTimes(2);
398
+ expect(component.dragExit.emit).toHaveBeenCalledTimes(2);
399
+ });
400
+
401
+ it('should handle drag events in correct sequence', () => {
402
+ spyOn(component.dragEnter, 'emit');
403
+ spyOn(component.dragOver, 'emit');
404
+ spyOn(component.dragExit, 'emit');
405
+ spyOn(component.dragDrop, 'emit');
406
+
407
+ const mockBoundingRect = {
408
+ left: 100,
409
+ right: 200,
410
+ top: 50,
411
+ bottom: 150
412
+ };
413
+ spyOn(component.nativeElement, 'getBoundingClientRect').and.returnValue(mockBoundingRect as DOMRect);
414
+
415
+ store.startDrag(cellDragData);
416
+ fixture.detectChanges();
417
+
418
+ const enterEvent = new DragEvent('dragenter');
419
+ const overEvent = new DragEvent('dragover');
420
+ Object.defineProperty(overEvent, 'dataTransfer', {
421
+ value: { dropEffect: 'none' }
422
+ });
423
+ const dropEvent = new DragEvent('drop');
424
+ Object.defineProperty(dropEvent, 'dataTransfer', {
425
+ value: {}
426
+ });
427
+ const leaveEvent = new DragEvent('dragleave', { clientX: 90, clientY: 100 }); // Outside bounds
428
+
429
+ component.onDragEnter(enterEvent);
430
+ component.onDragOver(overEvent);
431
+ component.onDrop(dropEvent);
432
+ component.onDragLeave(leaveEvent);
433
+
434
+ expect(component.dragEnter.emit).toHaveBeenCalledWith({ row: 3, col: 4 });
435
+ expect(component.dragOver.emit).toHaveBeenCalledWith({ row: 3, col: 4 });
436
+ expect(component.dragDrop.emit).toHaveBeenCalledWith({
437
+ data: cellDragData,
438
+ target: { row: 3, col: 4 }
439
+ });
440
+ expect(component.dragExit.emit).toHaveBeenCalled();
441
+ });
442
+ });
443
+
444
+ describe('Grid Positioning', () => {
445
+ it('should apply correct grid positioning styles', () => {
446
+ fixture.detectChanges();
447
+
448
+ const dropZone = fixture.nativeElement.querySelector('.drop-zone');
449
+ expect(dropZone.style.gridRow).toBe('3');
450
+ expect(dropZone.style.gridColumn).toBe('4');
451
+ });
452
+
453
+ it('should update positioning when inputs change', () => {
454
+ fixture.componentRef.setInput('row', 5);
455
+ fixture.componentRef.setInput('col', 6);
456
+ fixture.detectChanges();
457
+
458
+ const dropZone = fixture.nativeElement.querySelector('.drop-zone');
459
+ expect(dropZone.style.gridRow).toBe('5');
460
+ expect(dropZone.style.gridColumn).toBe('6');
461
+ expect(component.dropZoneId()).toBe('drop-zone-5-6');
462
+ expect(component.dropData()).toEqual({ row: 5, col: 6 });
463
+ });
464
+ });
465
+ });
@@ -0,0 +1,20 @@
1
+ <!-- drop-zone.component.html -->
2
+ <div
3
+ class="drop-zone"
4
+ [class.drop-zone--highlight]="highlight() && !highlightInvalid()"
5
+ [class.drop-zone--invalid]="highlightInvalid()"
6
+ [class.drop-zone--resize]="highlightResize()"
7
+ [style.grid-row]="row()"
8
+ [style.grid-column]="col()"
9
+ (dragenter)="onDragEnter($event)"
10
+ (dragover)="onDragOver($event)"
11
+ (dragleave)="onDragLeave($event)"
12
+ (drop)="onDrop($event)"
13
+ >
14
+ @if (editMode()) {
15
+ <div class="edit-mode-cell-number">
16
+ {{ index() }}<br />
17
+ {{ row() }},{{ col() }}
18
+ </div>
19
+ }
20
+ </div>
@@ -0,0 +1,67 @@
1
+ // drop-zone.component.scss
2
+
3
+ .drop-zone {
4
+ width: 100%;
5
+ height: 100%;
6
+ z-index: 0;
7
+
8
+ // Ensure proper alignment when explicitly positioned in CSS Grid
9
+ align-self: stretch;
10
+ justify-self: stretch;
11
+
12
+ // Ensure the drop zone takes up the full grid area
13
+ display: block;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ .drop-zone--active {
18
+ background-color: rgba(128, 128, 128, 0.5);
19
+ // background-color: var(--mat-sys-surface-dim);
20
+ }
21
+
22
+ .drop-zone--highlight {
23
+ background-color: rgba(128, 128, 128, 0.5);
24
+ // background-color: var(--mat-sys-surface-dim);
25
+ }
26
+
27
+ .drop-zone--invalid {
28
+ // background-color: var(--mat-sys-error);
29
+ // background-color: color-mix(in srgb, var(--mat-sys-error) 50%, transparent);
30
+ //background-color: rgba(255, 0, 0, 0.2);
31
+
32
+ background-color: light-dark(
33
+ color-mix(in srgb, var(--mat-sys-error) 40%, white),
34
+ color-mix(in srgb, var(--mat-sys-error) 80%, black)
35
+ );
36
+ }
37
+
38
+ .drop-zone--resize {
39
+ background-color: rgba(33, 150, 243, 0.3);
40
+ outline: 1px solid rgba(33, 150, 243, 0.6);
41
+ }
42
+
43
+ // Edit mode cell numbers - centered in cell
44
+ .edit-mode-cell-number {
45
+ // Text styling
46
+ font-size: 10px;
47
+ // color: var(-mat-sys-surface-container-highest);
48
+ color: rgba(100, 100, 100, 0.6);
49
+ // color: color-mix(in srgb, var(--mat-sys-surface-on-dim) 40%, transparent);
50
+
51
+ pointer-events: none;
52
+ user-select: none;
53
+ z-index: -1;
54
+
55
+ // Center in cell
56
+ display: flex;
57
+ flex-direction: column;
58
+ align-items: center;
59
+ justify-content: center;
60
+ text-align: center;
61
+
62
+ // Take up entire cell area
63
+ width: 100%;
64
+ height: 100%;
65
+
66
+ // background-color: rgba(255, 255, 255, 0.1);
67
+ }
@@ -0,0 +1,122 @@
1
+ // drop-zone.component.ts
2
+ import {
3
+ Component,
4
+ ElementRef,
5
+ inject,
6
+ input,
7
+ output,
8
+ computed,
9
+ ChangeDetectionStrategy,
10
+ } from '@angular/core';
11
+ import { CommonModule } from '@angular/common';
12
+ import { DashboardStore } from '../store/dashboard-store';
13
+ import { DragData } from '../models';
14
+
15
+ @Component({
16
+ selector: 'lib-drop-zone',
17
+ standalone: true,
18
+ imports: [CommonModule],
19
+ changeDetection: ChangeDetectionStrategy.OnPush,
20
+ templateUrl: './drop-zone.component.html',
21
+ styleUrl: './drop-zone.component.scss',
22
+ })
23
+ export class DropZoneComponent {
24
+ // Required inputs
25
+ row = input.required<number>();
26
+ col = input.required<number>();
27
+ index = input.required<number>();
28
+
29
+ // Optional inputs with defaults
30
+ highlight = input(false);
31
+ highlightInvalid = input(false);
32
+ highlightResize = input(false);
33
+ editMode = input(false);
34
+
35
+ // Outputs
36
+ dragEnter = output<{ row: number; col: number }>();
37
+ dragExit = output<void>();
38
+ dragOver = output<{ row: number; col: number }>();
39
+ dragDrop = output<{
40
+ data: DragData;
41
+ target: { row: number; col: number };
42
+ }>();
43
+
44
+ // Computed properties
45
+ dropZoneId = computed(() => `drop-zone-${this.row()}-${this.col()}`);
46
+
47
+ dropData = computed(() => ({
48
+ row: this.row(),
49
+ col: this.col(),
50
+ }));
51
+
52
+ // Abstract drag state from store
53
+ dragData = computed(() => this.#store.dragData());
54
+
55
+ // Computed drop effect based on drag data and validity
56
+ dropEffect = computed(() => {
57
+ const data = this.dragData();
58
+ if (!data || this.highlightInvalid()) {
59
+ return 'none';
60
+ }
61
+ return data.kind === 'cell' ? 'move' : 'copy';
62
+ });
63
+
64
+ readonly #store = inject(DashboardStore);
65
+ readonly #elementRef = inject(ElementRef);
66
+
67
+ get nativeElement(): HTMLElement {
68
+ return this.#elementRef.nativeElement;
69
+ }
70
+
71
+ onDragEnter(event: DragEvent): void {
72
+ event.preventDefault();
73
+ event.stopPropagation();
74
+ this.dragEnter.emit({ row: this.row(), col: this.col() });
75
+ }
76
+
77
+ onDragOver(event: DragEvent): void {
78
+ event.preventDefault();
79
+ event.stopPropagation();
80
+
81
+ if (event.dataTransfer && this.dragData()) {
82
+ event.dataTransfer.dropEffect = this.dropEffect();
83
+ }
84
+
85
+ this.dragOver.emit({ row: this.row(), col: this.col() });
86
+ }
87
+
88
+ onDragLeave(event: DragEvent): void {
89
+ event.preventDefault();
90
+ event.stopPropagation();
91
+
92
+ // Only emit if actually leaving the element (not entering a child)
93
+ if (this.#isLeavingElement(event)) {
94
+ this.dragExit.emit();
95
+ }
96
+ }
97
+
98
+ #isLeavingElement(event: DragEvent): boolean {
99
+ const rect = this.#elementRef.nativeElement.getBoundingClientRect();
100
+ return (
101
+ event.clientX <= rect.left ||
102
+ event.clientX >= rect.right ||
103
+ event.clientY <= rect.top ||
104
+ event.clientY >= rect.bottom
105
+ );
106
+ }
107
+
108
+ onDrop(event: DragEvent): void {
109
+ event.preventDefault();
110
+ event.stopPropagation();
111
+
112
+ if (!event.dataTransfer) return;
113
+
114
+ const data = this.dragData();
115
+ if (data) {
116
+ this.dragDrop.emit({
117
+ data,
118
+ target: { row: this.row(), col: this.col() },
119
+ });
120
+ }
121
+ }
122
+ }