@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.
- package/ng-package.json +7 -0
- package/package.json +34 -45
- package/src/lib/__tests__/dashboard-component-widget-state-integration.spec.ts +537 -0
- package/src/lib/cell/__tests__/cell-resize.component.spec.ts +442 -0
- package/src/lib/cell/__tests__/cell.component.spec.ts +541 -0
- package/src/lib/cell/cell-context-menu.component.ts +138 -0
- package/src/lib/cell/cell-context-menu.service.ts +36 -0
- package/src/lib/cell/cell.component.html +37 -0
- package/src/lib/cell/cell.component.scss +198 -0
- package/src/lib/cell/cell.component.ts +375 -0
- package/src/lib/dashboard/dashboard.component.html +18 -0
- package/src/lib/dashboard/dashboard.component.scss +17 -0
- package/src/lib/dashboard/dashboard.component.ts +187 -0
- package/src/lib/dashboard-editor/dashboard-editor.component.html +57 -0
- package/src/lib/dashboard-editor/dashboard-editor.component.scss +87 -0
- package/src/lib/dashboard-editor/dashboard-editor.component.ts +219 -0
- package/src/lib/dashboard-viewer/__tests__/dashboard-viewer.component.spec.ts +258 -0
- package/src/lib/dashboard-viewer/dashboard-viewer.component.html +20 -0
- package/src/lib/dashboard-viewer/dashboard-viewer.component.scss +50 -0
- package/src/lib/dashboard-viewer/dashboard-viewer.component.ts +70 -0
- package/src/lib/drop-zone/__tests__/drop-zone.component.spec.ts +465 -0
- package/src/lib/drop-zone/drop-zone.component.html +20 -0
- package/src/lib/drop-zone/drop-zone.component.scss +67 -0
- package/src/lib/drop-zone/drop-zone.component.ts +122 -0
- package/src/lib/internal-widgets/unknown-widget/unknown-widget.component.ts +72 -0
- package/src/lib/models/cell-data.ts +13 -0
- package/src/lib/models/cell-dialog.ts +7 -0
- package/src/lib/models/cell-id.ts +85 -0
- package/src/lib/models/cell-position.ts +15 -0
- package/src/lib/models/dashboard-data.dto.ts +44 -0
- package/src/lib/models/dashboard-data.utils.ts +49 -0
- package/src/lib/models/drag-data.ts +6 -0
- package/src/lib/models/index.ts +11 -0
- package/src/lib/models/reserved-space.ts +24 -0
- package/src/lib/models/widget-factory.ts +33 -0
- package/src/lib/models/widget-id.ts +70 -0
- package/src/lib/models/widget.ts +21 -0
- package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.component.ts +127 -0
- package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.provider.ts +15 -0
- package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.tokens.ts +20 -0
- package/src/lib/providers/cell-settings-dialog/default-cell-settings-dialog.provider.ts +32 -0
- package/src/lib/providers/cell-settings-dialog/index.ts +3 -0
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/services/__tests__/dashboard-bridge.service.spec.ts +220 -0
- package/src/lib/services/__tests__/dashboard-viewport.service.spec.ts +362 -0
- package/src/lib/services/dashboard-bridge.service.ts +155 -0
- package/src/lib/services/dashboard-viewport.service.ts +148 -0
- package/src/lib/services/dashboard.service.ts +62 -0
- package/src/lib/store/__tests__/dashboard-store-collision-detection.spec.ts +756 -0
- package/src/lib/store/__tests__/dashboard-store-computed-properties.spec.ts +974 -0
- package/src/lib/store/__tests__/dashboard-store-drag-drop.spec.ts +279 -0
- package/src/lib/store/__tests__/dashboard-store-export-import.spec.ts +780 -0
- package/src/lib/store/__tests__/dashboard-store-grid-config.spec.ts +128 -0
- package/src/lib/store/__tests__/dashboard-store-query-methods.spec.ts +229 -0
- package/src/lib/store/__tests__/dashboard-store-resize-operations.spec.ts +652 -0
- package/src/lib/store/__tests__/dashboard-store-widget-management.spec.ts +461 -0
- package/src/lib/store/__tests__/dashboard-store-widget-state-preservation.spec.ts +369 -0
- package/src/lib/store/dashboard-store.ts +239 -0
- package/src/lib/store/features/drag-drop.feature.ts +140 -0
- package/src/lib/store/features/grid-config.feature.ts +43 -0
- package/src/lib/store/features/resize.feature.ts +140 -0
- package/src/lib/store/features/utils/collision.utils.ts +89 -0
- package/src/lib/store/features/utils/grid-query-internal.utils.ts +37 -0
- package/src/lib/store/features/utils/resize.utils.ts +165 -0
- package/src/lib/store/features/widget-management.feature.ts +158 -0
- package/src/lib/styles/_dashboard-grid-vars.scss +11 -0
- package/src/lib/widget-list/__tests__/widget-list-bridge-integration.spec.ts +137 -0
- package/src/lib/widget-list/widget-list.component.html +22 -0
- package/src/lib/widget-list/widget-list.component.scss +154 -0
- package/src/lib/widget-list/widget-list.component.ts +106 -0
- package/src/public-api.ts +21 -0
- package/src/test-setup.ts +10 -0
- package/tsconfig.lib.json +15 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +14 -0
- package/fesm2022/dragonworks-ngx-dashboard.mjs +0 -2192
- package/fesm2022/dragonworks-ngx-dashboard.mjs.map +0 -1
- package/index.d.ts +0 -678
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
signalStoreFeature,
|
|
3
|
+
withMethods,
|
|
4
|
+
withState,
|
|
5
|
+
withComputed,
|
|
6
|
+
patchState,
|
|
7
|
+
} from '@ngrx/signals';
|
|
8
|
+
import { computed } from '@angular/core';
|
|
9
|
+
import {
|
|
10
|
+
CellId,
|
|
11
|
+
CellIdUtils,
|
|
12
|
+
CellData,
|
|
13
|
+
DragData,
|
|
14
|
+
WidgetFactory,
|
|
15
|
+
WidgetId,
|
|
16
|
+
} from '../../models';
|
|
17
|
+
import {
|
|
18
|
+
calculateCollisionInfo,
|
|
19
|
+
calculateHighlightedZones,
|
|
20
|
+
} from './utils/collision.utils';
|
|
21
|
+
import { DashboardService } from '../../services/dashboard.service';
|
|
22
|
+
|
|
23
|
+
export interface DragDropState {
|
|
24
|
+
dragData: DragData | null;
|
|
25
|
+
hoveredDropZone: { row: number; col: number } | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const initialDragDropState: DragDropState = {
|
|
29
|
+
dragData: null,
|
|
30
|
+
hoveredDropZone: null,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const withDragDrop = () =>
|
|
34
|
+
signalStoreFeature(
|
|
35
|
+
withState<DragDropState>(initialDragDropState),
|
|
36
|
+
withComputed((store) => ({
|
|
37
|
+
// Highlighted zones during drag
|
|
38
|
+
highlightedZones: computed(() =>
|
|
39
|
+
calculateHighlightedZones(store.dragData(), store.hoveredDropZone()),
|
|
40
|
+
),
|
|
41
|
+
})),
|
|
42
|
+
withComputed((store) => ({
|
|
43
|
+
// Map for quick highlight lookup - reuse highlightedZones computation
|
|
44
|
+
highlightMap: computed(() => {
|
|
45
|
+
const zones = store.highlightedZones();
|
|
46
|
+
const map = new Set<CellId>();
|
|
47
|
+
|
|
48
|
+
for (const z of zones) {
|
|
49
|
+
map.add(CellIdUtils.create(z.row, z.col));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return map;
|
|
53
|
+
}),
|
|
54
|
+
})),
|
|
55
|
+
withMethods((store) => ({
|
|
56
|
+
startDrag(dragData: DragData) {
|
|
57
|
+
patchState(store, { dragData });
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
endDrag() {
|
|
61
|
+
patchState(store, {
|
|
62
|
+
dragData: null,
|
|
63
|
+
hoveredDropZone: null,
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
setHoveredDropZone(zone: { row: number; col: number } | null) {
|
|
68
|
+
patchState(store, { hoveredDropZone: zone });
|
|
69
|
+
},
|
|
70
|
+
})),
|
|
71
|
+
|
|
72
|
+
// Second withMethods block for drop handling that can access endDrag
|
|
73
|
+
withMethods((store) => ({
|
|
74
|
+
// Drop handling logic with dependency injection
|
|
75
|
+
_handleDrop(
|
|
76
|
+
dragData: DragData,
|
|
77
|
+
targetPosition: { row: number; col: number },
|
|
78
|
+
dependencies: {
|
|
79
|
+
cells: CellData[];
|
|
80
|
+
rows: number;
|
|
81
|
+
columns: number;
|
|
82
|
+
dashboardService: DashboardService;
|
|
83
|
+
createWidget: (
|
|
84
|
+
row: number,
|
|
85
|
+
col: number,
|
|
86
|
+
factory: WidgetFactory,
|
|
87
|
+
widgetState?: string,
|
|
88
|
+
) => void;
|
|
89
|
+
updateWidgetPosition: (
|
|
90
|
+
widgetId: WidgetId,
|
|
91
|
+
row: number,
|
|
92
|
+
col: number,
|
|
93
|
+
) => void;
|
|
94
|
+
},
|
|
95
|
+
): boolean {
|
|
96
|
+
// 1. Validate placement using existing collision detection
|
|
97
|
+
const collisionInfo = calculateCollisionInfo(
|
|
98
|
+
dragData,
|
|
99
|
+
targetPosition,
|
|
100
|
+
dependencies.cells,
|
|
101
|
+
dependencies.rows,
|
|
102
|
+
dependencies.columns,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// 2. End drag state first
|
|
106
|
+
store.endDrag();
|
|
107
|
+
|
|
108
|
+
// 3. Early return if invalid placement
|
|
109
|
+
if (collisionInfo.hasCollisions || collisionInfo.outOfBounds) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 4. Handle widget creation from palette
|
|
114
|
+
if (dragData.kind === 'widget') {
|
|
115
|
+
const factory = dependencies.dashboardService.getFactory(
|
|
116
|
+
dragData.content.widgetTypeid,
|
|
117
|
+
);
|
|
118
|
+
dependencies.createWidget(
|
|
119
|
+
targetPosition.row,
|
|
120
|
+
targetPosition.col,
|
|
121
|
+
factory,
|
|
122
|
+
undefined,
|
|
123
|
+
);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 5. Handle cell movement
|
|
128
|
+
if (dragData.kind === 'cell') {
|
|
129
|
+
dependencies.updateWidgetPosition(
|
|
130
|
+
dragData.content.widgetId, // Use widgetId instead of cellId
|
|
131
|
+
targetPosition.row,
|
|
132
|
+
targetPosition.col,
|
|
133
|
+
);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return false;
|
|
138
|
+
},
|
|
139
|
+
})),
|
|
140
|
+
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { signalStoreFeature, withMethods, withState, patchState } from '@ngrx/signals';
|
|
2
|
+
|
|
3
|
+
export interface GridConfigState {
|
|
4
|
+
rows: number;
|
|
5
|
+
columns: number;
|
|
6
|
+
gutterSize: string;
|
|
7
|
+
isEditMode: boolean;
|
|
8
|
+
gridCellDimensions: { width: number; height: number };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const initialGridConfigState: GridConfigState = {
|
|
12
|
+
rows: 8,
|
|
13
|
+
columns: 16,
|
|
14
|
+
gutterSize: '0.5em',
|
|
15
|
+
isEditMode: false,
|
|
16
|
+
gridCellDimensions: { width: 0, height: 0 },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const withGridConfig = () =>
|
|
20
|
+
signalStoreFeature(
|
|
21
|
+
withState<GridConfigState>(initialGridConfigState),
|
|
22
|
+
withMethods((store) => ({
|
|
23
|
+
setGridConfig(config: {
|
|
24
|
+
rows?: number;
|
|
25
|
+
columns?: number;
|
|
26
|
+
gutterSize?: string;
|
|
27
|
+
}) {
|
|
28
|
+
patchState(store, config);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
setGridCellDimensions(width: number, height: number) {
|
|
32
|
+
patchState(store, { gridCellDimensions: { width, height } });
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
toggleEditMode() {
|
|
36
|
+
patchState(store, { isEditMode: !store.isEditMode() });
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
setEditMode(isEditMode: boolean) {
|
|
40
|
+
patchState(store, { isEditMode });
|
|
41
|
+
},
|
|
42
|
+
}))
|
|
43
|
+
);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
signalStoreFeature,
|
|
3
|
+
withMethods,
|
|
4
|
+
withState,
|
|
5
|
+
patchState,
|
|
6
|
+
} from '@ngrx/signals';
|
|
7
|
+
import { CellId, CellIdUtils, CellData } from '../../models';
|
|
8
|
+
import { calculateResizePreview, type ResizeData } from './utils/resize.utils';
|
|
9
|
+
|
|
10
|
+
export interface ResizeState {
|
|
11
|
+
resizeData: ResizeData | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const initialResizeState: ResizeState = {
|
|
15
|
+
resizeData: null,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Utility functions for resize preview computations
|
|
19
|
+
export const ResizePreviewUtils = {
|
|
20
|
+
computePreviewCells(
|
|
21
|
+
resizeData: ResizeData | null,
|
|
22
|
+
cells: CellData[],
|
|
23
|
+
): { row: number; col: number }[] {
|
|
24
|
+
if (!resizeData) return [];
|
|
25
|
+
|
|
26
|
+
const cell = cells.find((cell) =>
|
|
27
|
+
CellIdUtils.equals(cell.cellId, resizeData.cellId),
|
|
28
|
+
);
|
|
29
|
+
if (!cell) return [];
|
|
30
|
+
|
|
31
|
+
const previewCells: { row: number; col: number }[] = [];
|
|
32
|
+
for (let r = 0; r < resizeData.previewRowSpan; r++) {
|
|
33
|
+
for (let c = 0; c < resizeData.previewColSpan; c++) {
|
|
34
|
+
previewCells.push({
|
|
35
|
+
row: cell.row + r,
|
|
36
|
+
col: cell.col + c,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return previewCells;
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
computePreviewMap(previewCells: { row: number; col: number }[]): Set<CellId> {
|
|
45
|
+
const map = new Set<CellId>();
|
|
46
|
+
for (const cell of previewCells) {
|
|
47
|
+
map.add(CellIdUtils.create(cell.row, cell.col));
|
|
48
|
+
}
|
|
49
|
+
return map;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const withResize = () =>
|
|
54
|
+
signalStoreFeature(
|
|
55
|
+
withState<ResizeState>(initialResizeState),
|
|
56
|
+
withMethods((store) => ({
|
|
57
|
+
// Resize methods that need cross-feature dependencies
|
|
58
|
+
_startResize(
|
|
59
|
+
cellId: CellId,
|
|
60
|
+
dependencies: {
|
|
61
|
+
cells: CellData[];
|
|
62
|
+
},
|
|
63
|
+
) {
|
|
64
|
+
const cell = dependencies.cells.find((c) =>
|
|
65
|
+
CellIdUtils.equals(c.cellId, cellId),
|
|
66
|
+
);
|
|
67
|
+
if (!cell) return;
|
|
68
|
+
|
|
69
|
+
patchState(store, {
|
|
70
|
+
resizeData: {
|
|
71
|
+
cellId,
|
|
72
|
+
originalRowSpan: cell.rowSpan,
|
|
73
|
+
originalColSpan: cell.colSpan,
|
|
74
|
+
previewRowSpan: cell.rowSpan,
|
|
75
|
+
previewColSpan: cell.colSpan,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
_updateResizePreview(
|
|
81
|
+
direction: 'horizontal' | 'vertical',
|
|
82
|
+
delta: number,
|
|
83
|
+
dependencies: {
|
|
84
|
+
cells: CellData[];
|
|
85
|
+
rows: number;
|
|
86
|
+
columns: number;
|
|
87
|
+
},
|
|
88
|
+
) {
|
|
89
|
+
const resizeData = store.resizeData();
|
|
90
|
+
if (!resizeData) return;
|
|
91
|
+
|
|
92
|
+
const newSpans = calculateResizePreview(
|
|
93
|
+
resizeData,
|
|
94
|
+
direction,
|
|
95
|
+
delta,
|
|
96
|
+
dependencies.cells,
|
|
97
|
+
dependencies.rows,
|
|
98
|
+
dependencies.columns,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (newSpans) {
|
|
102
|
+
patchState(store, {
|
|
103
|
+
resizeData: {
|
|
104
|
+
...resizeData,
|
|
105
|
+
previewRowSpan: newSpans.rowSpan,
|
|
106
|
+
previewColSpan: newSpans.colSpan,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
_endResize(
|
|
113
|
+
apply: boolean,
|
|
114
|
+
dependencies: {
|
|
115
|
+
updateWidgetSpan: (
|
|
116
|
+
id: CellId,
|
|
117
|
+
rowSpan: number,
|
|
118
|
+
colSpan: number,
|
|
119
|
+
) => void;
|
|
120
|
+
},
|
|
121
|
+
) {
|
|
122
|
+
const resizeData = store.resizeData();
|
|
123
|
+
if (!resizeData) return;
|
|
124
|
+
|
|
125
|
+
if (
|
|
126
|
+
apply &&
|
|
127
|
+
(resizeData.previewRowSpan !== resizeData.originalRowSpan ||
|
|
128
|
+
resizeData.previewColSpan !== resizeData.originalColSpan)
|
|
129
|
+
) {
|
|
130
|
+
dependencies.updateWidgetSpan(
|
|
131
|
+
resizeData.cellId,
|
|
132
|
+
resizeData.previewRowSpan,
|
|
133
|
+
resizeData.previewColSpan,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
patchState(store, { resizeData: null });
|
|
138
|
+
},
|
|
139
|
+
})),
|
|
140
|
+
);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { CellId, CellIdUtils, CellData, DragData } from '../../../models';
|
|
2
|
+
import { GridQueryInternalUtils } from './grid-query-internal.utils';
|
|
3
|
+
|
|
4
|
+
export interface CollisionInfo {
|
|
5
|
+
hasCollisions: boolean;
|
|
6
|
+
invalidCells: CellId[];
|
|
7
|
+
outOfBounds: boolean;
|
|
8
|
+
footprint: { row: number; col: number }[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function calculateCollisionInfo(
|
|
12
|
+
dragData: DragData | null,
|
|
13
|
+
hovered: { row: number; col: number } | null,
|
|
14
|
+
cells: CellData[],
|
|
15
|
+
rows: number,
|
|
16
|
+
columns: number,
|
|
17
|
+
): CollisionInfo {
|
|
18
|
+
if (!dragData || !hovered) {
|
|
19
|
+
return {
|
|
20
|
+
hasCollisions: false,
|
|
21
|
+
invalidCells: [],
|
|
22
|
+
outOfBounds: false,
|
|
23
|
+
footprint: [],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const isCell = dragData.kind === 'cell';
|
|
28
|
+
const rowSpan = isCell ? dragData.content.rowSpan : 1;
|
|
29
|
+
const colSpan = isCell ? dragData.content.colSpan : 1;
|
|
30
|
+
|
|
31
|
+
// Check bounds
|
|
32
|
+
const outOfBounds = GridQueryInternalUtils.isOutOfBounds(
|
|
33
|
+
hovered.row,
|
|
34
|
+
hovered.col,
|
|
35
|
+
rowSpan,
|
|
36
|
+
colSpan,
|
|
37
|
+
rows,
|
|
38
|
+
columns
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Generate footprint
|
|
42
|
+
const footprint: { row: number; col: number }[] = [];
|
|
43
|
+
for (let r = 0; r < rowSpan; r++) {
|
|
44
|
+
for (let c = 0; c < colSpan; c++) {
|
|
45
|
+
footprint.push({ row: hovered.row + r, col: hovered.col + c });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const excludeWidgetId = isCell ? dragData.content.widgetId : undefined;
|
|
50
|
+
|
|
51
|
+
// Check for actual collisions with other widgets (not self)
|
|
52
|
+
const hasCollisions = footprint.some((pos) =>
|
|
53
|
+
GridQueryInternalUtils.isCellOccupied(cells, pos.row, pos.col, excludeWidgetId)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Generate invalid cell IDs
|
|
57
|
+
const invalidCells: CellId[] = [];
|
|
58
|
+
if (hasCollisions || outOfBounds) {
|
|
59
|
+
for (const pos of footprint) {
|
|
60
|
+
invalidCells.push(CellIdUtils.create(pos.row, pos.col));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
hasCollisions,
|
|
66
|
+
invalidCells,
|
|
67
|
+
outOfBounds,
|
|
68
|
+
footprint,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function calculateHighlightedZones(
|
|
73
|
+
dragData: DragData | null,
|
|
74
|
+
hovered: { row: number; col: number } | null,
|
|
75
|
+
): { row: number; col: number }[] {
|
|
76
|
+
if (!dragData || !hovered) return [];
|
|
77
|
+
|
|
78
|
+
const zones: { row: number; col: number }[] = [];
|
|
79
|
+
const rowSpan = dragData.kind === 'cell' ? dragData.content.rowSpan : 1;
|
|
80
|
+
const colSpan = dragData.kind === 'cell' ? dragData.content.colSpan : 1;
|
|
81
|
+
|
|
82
|
+
for (let r = 0; r < rowSpan; r++) {
|
|
83
|
+
for (let c = 0; c < colSpan; c++) {
|
|
84
|
+
zones.push({ row: hovered.row + r, col: hovered.col + c });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return zones;
|
|
89
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CellId, CellIdUtils, CellData, WidgetId } from '../../../models';
|
|
2
|
+
|
|
3
|
+
// Internal utility functions used by collision detection, resize logic, and tests
|
|
4
|
+
export const GridQueryInternalUtils = {
|
|
5
|
+
isCellOccupied(cells: CellData[], row: number, col: number, excludeWidgetId?: WidgetId): boolean {
|
|
6
|
+
return cells.some((cell) => {
|
|
7
|
+
// Skip checking against the widget being dragged (use widgetId for stable identity)
|
|
8
|
+
if (excludeWidgetId && cell.widgetId === excludeWidgetId)
|
|
9
|
+
return false;
|
|
10
|
+
|
|
11
|
+
const endRow = cell.row + cell.rowSpan - 1;
|
|
12
|
+
const endCol = cell.col + cell.colSpan - 1;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
cell.row <= row && row <= endRow && cell.col <= col && col <= endCol
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
isOutOfBounds(
|
|
21
|
+
targetRow: number,
|
|
22
|
+
targetCol: number,
|
|
23
|
+
spanRow: number,
|
|
24
|
+
spanCol: number,
|
|
25
|
+
maxRows: number,
|
|
26
|
+
maxColumns: number,
|
|
27
|
+
): boolean {
|
|
28
|
+
const rowLimit = targetRow + spanRow - 1;
|
|
29
|
+
const colLimit = targetCol + spanCol - 1;
|
|
30
|
+
|
|
31
|
+
return rowLimit > maxRows || colLimit > maxColumns;
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
getCellAt(cells: CellData[], row: number, col: number): CellData | null {
|
|
35
|
+
return cells.find((cell) => cell.row === row && cell.col === col) ?? null;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { CellId, CellIdUtils, CellData } from '../../../models';
|
|
2
|
+
|
|
3
|
+
export function getMaxColSpan(
|
|
4
|
+
cellId: CellId,
|
|
5
|
+
row: number,
|
|
6
|
+
col: number,
|
|
7
|
+
cells: CellData[],
|
|
8
|
+
columns: number,
|
|
9
|
+
): number {
|
|
10
|
+
const currentCell = cells.find((c) => CellIdUtils.equals(c.cellId, cellId));
|
|
11
|
+
if (!currentCell) return 1;
|
|
12
|
+
|
|
13
|
+
// Start from current position and check each column until we hit a boundary or collision
|
|
14
|
+
let maxSpan = 1;
|
|
15
|
+
|
|
16
|
+
for (let testCol = col + 1; testCol <= columns; testCol++) {
|
|
17
|
+
// Check if this column is free for all rows the widget spans
|
|
18
|
+
let columnIsFree = true;
|
|
19
|
+
|
|
20
|
+
for (let testRow = row; testRow < row + currentCell.rowSpan; testRow++) {
|
|
21
|
+
const occupied = cells.some((cell) => {
|
|
22
|
+
if (CellIdUtils.equals(cell.cellId, cellId)) return false;
|
|
23
|
+
|
|
24
|
+
const wStartCol = cell.col;
|
|
25
|
+
const wEndCol = cell.col + cell.colSpan - 1;
|
|
26
|
+
const wStartRow = cell.row;
|
|
27
|
+
const wEndRow = cell.row + cell.rowSpan - 1;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
testCol >= wStartCol &&
|
|
31
|
+
testCol <= wEndCol &&
|
|
32
|
+
testRow >= wStartRow &&
|
|
33
|
+
testRow <= wEndRow
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (occupied) {
|
|
38
|
+
columnIsFree = false;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!columnIsFree) {
|
|
44
|
+
break; // Hit a collision, stop here
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
maxSpan = testCol - col + 1; // Update max span to include this column
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return maxSpan;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getMaxRowSpan(
|
|
54
|
+
cellId: CellId,
|
|
55
|
+
row: number,
|
|
56
|
+
col: number,
|
|
57
|
+
cells: CellData[],
|
|
58
|
+
rows: number,
|
|
59
|
+
): number {
|
|
60
|
+
const currentCell = cells.find((c) => CellIdUtils.equals(c.cellId, cellId));
|
|
61
|
+
if (!currentCell) return 1;
|
|
62
|
+
|
|
63
|
+
// Start from current position and check each row until we hit a boundary or collision
|
|
64
|
+
let maxSpan = 1;
|
|
65
|
+
|
|
66
|
+
for (let testRow = row + 1; testRow <= rows; testRow++) {
|
|
67
|
+
// Check if this row is free for all columns the widget spans
|
|
68
|
+
let rowIsFree = true;
|
|
69
|
+
|
|
70
|
+
for (let testCol = col; testCol < col + currentCell.colSpan; testCol++) {
|
|
71
|
+
const occupied = cells.some((cell) => {
|
|
72
|
+
if (CellIdUtils.equals(cell.cellId, cellId)) return false;
|
|
73
|
+
|
|
74
|
+
const wStartRow = cell.row;
|
|
75
|
+
const wEndRow = cell.row + cell.rowSpan - 1;
|
|
76
|
+
const wStartCol = cell.col;
|
|
77
|
+
const wEndCol = cell.col + cell.colSpan - 1;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
testRow >= wStartRow &&
|
|
81
|
+
testRow <= wEndRow &&
|
|
82
|
+
testCol >= wStartCol &&
|
|
83
|
+
testCol <= wEndCol
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (occupied) {
|
|
88
|
+
rowIsFree = false;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!rowIsFree) {
|
|
94
|
+
break; // Hit a collision, stop here
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
maxSpan = testRow - row + 1; // Update max span to include this row
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return maxSpan;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ResizeData {
|
|
104
|
+
cellId: CellId;
|
|
105
|
+
originalRowSpan: number;
|
|
106
|
+
originalColSpan: number;
|
|
107
|
+
previewRowSpan: number;
|
|
108
|
+
previewColSpan: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function calculateResizePreview(
|
|
112
|
+
resizeData: ResizeData,
|
|
113
|
+
direction: 'horizontal' | 'vertical',
|
|
114
|
+
delta: number,
|
|
115
|
+
cells: CellData[],
|
|
116
|
+
rows: number,
|
|
117
|
+
columns: number,
|
|
118
|
+
): { rowSpan: number; colSpan: number } | null {
|
|
119
|
+
const cell = cells.find((c) =>
|
|
120
|
+
CellIdUtils.equals(c.cellId, resizeData.cellId),
|
|
121
|
+
);
|
|
122
|
+
if (!cell) return null;
|
|
123
|
+
|
|
124
|
+
if (direction === 'horizontal') {
|
|
125
|
+
// Calculate the desired span based on the delta
|
|
126
|
+
const desiredColSpan = Math.max(1, resizeData.originalColSpan + delta);
|
|
127
|
+
|
|
128
|
+
// Get the maximum allowed span
|
|
129
|
+
const maxColSpan = getMaxColSpan(
|
|
130
|
+
cell.cellId,
|
|
131
|
+
cell.row,
|
|
132
|
+
cell.col,
|
|
133
|
+
cells,
|
|
134
|
+
columns,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Clamp to the maximum
|
|
138
|
+
const newColSpan = Math.min(desiredColSpan, maxColSpan);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
rowSpan: resizeData.previewRowSpan,
|
|
142
|
+
colSpan: newColSpan,
|
|
143
|
+
};
|
|
144
|
+
} else {
|
|
145
|
+
// Calculate the desired span based on the delta
|
|
146
|
+
const desiredRowSpan = Math.max(1, resizeData.originalRowSpan + delta);
|
|
147
|
+
|
|
148
|
+
// Get the maximum allowed span
|
|
149
|
+
const maxRowSpan = getMaxRowSpan(
|
|
150
|
+
cell.cellId,
|
|
151
|
+
cell.row,
|
|
152
|
+
cell.col,
|
|
153
|
+
cells,
|
|
154
|
+
rows,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Clamp to the maximum
|
|
158
|
+
const newRowSpan = Math.min(desiredRowSpan, maxRowSpan);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
rowSpan: newRowSpan,
|
|
162
|
+
colSpan: resizeData.previewColSpan,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|