@vc-shell/framework 1.1.0-alpha.4 → 1.1.0-alpha.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/core/composables/useMenuService/index.ts +20 -110
- package/core/composables/useWidgets/index.ts +2 -1
- package/core/plugins/modularity/index.ts +3 -3
- package/core/services/menu-service.ts +195 -0
- package/core/services/widget-service.ts +20 -0
- package/core/types/index.ts +1 -1
- package/dist/core/composables/useMenuService/index.d.ts +4 -10
- package/dist/core/composables/useMenuService/index.d.ts.map +1 -1
- package/dist/core/composables/useWidgets/index.d.ts +2 -1
- package/dist/core/composables/useWidgets/index.d.ts.map +1 -1
- package/dist/core/plugins/modularity/index.d.ts.map +1 -1
- package/dist/core/services/menu-service.d.ts +17 -0
- package/dist/core/services/menu-service.d.ts.map +1 -0
- package/dist/core/services/widget-service.d.ts +4 -0
- package/dist/core/services/widget-service.d.ts.map +1 -1
- package/dist/core/types/index.d.ts +1 -1
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/framework.js +220 -210
- package/dist/{index-CrxFDC2b.js → index-3ySdd-mG.js} +1 -1
- package/dist/{index-B1YR_MYV.js → index-B-nvqNbp.js} +1 -1
- package/dist/{index-xLYzNPa7.js → index-BQF2-UMe.js} +1 -1
- package/dist/{index-BBYyHeYA.js → index-BXlxP2d2.js} +1 -1
- package/dist/{index-Cf9Tz1ql.js → index-C7P-aBjd.js} +1 -1
- package/dist/{index-8LELHzw9.js → index-CO_2IshF.js} +1 -1
- package/dist/{index-BA98L1jI.js → index-CfyFpaKq.js} +1 -1
- package/dist/{index-DVljTjbf.js → index-Ci23AX3j.js} +1 -1
- package/dist/{index-D1JchciU.js → index-CyuFXG83.js} +1 -1
- package/dist/{index-CWKrD2Cd.js → index-D1rpRTKf.js} +1 -1
- package/dist/{index-BAeTsi-X.js → index-DLxTAT7x.js} +1 -1
- package/dist/{index-BuO5ByG9.js → index-DOVhosAY.js} +1 -1
- package/dist/{index-DLtsQ_PJ.js → index-DZAq0B3U.js} +23780 -23339
- package/dist/{index-BrUitdDo.js → index-DtkJ7xTB.js} +1 -1
- package/dist/{index-9lJxZE5w.js → index-DvGVm1rK.js} +1 -1
- package/dist/{index-RwX3kiZh.js → index-EDF1MDtU.js} +1 -1
- package/dist/{index-CJ5I7vTn.js → index-LjqdX6jw.js} +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/injection-keys.d.ts +2 -0
- package/dist/injection-keys.d.ts.map +1 -1
- package/dist/shared/components/draggable-dashboard/DraggableDashboard.vue.d.ts +2 -0
- package/dist/shared/components/draggable-dashboard/DraggableDashboard.vue.d.ts.map +1 -1
- package/dist/shared/components/draggable-dashboard/composables/useCellSizeCalculator.d.ts +25 -0
- package/dist/shared/components/draggable-dashboard/composables/useCellSizeCalculator.d.ts.map +1 -0
- package/dist/shared/components/draggable-dashboard/composables/useCollisionDetection.d.ts +27 -0
- package/dist/shared/components/draggable-dashboard/composables/useCollisionDetection.d.ts.map +1 -0
- package/dist/shared/components/draggable-dashboard/composables/useDashboardDragAndDrop.d.ts +22 -0
- package/dist/shared/components/draggable-dashboard/composables/useDashboardDragAndDrop.d.ts.map +1 -1
- package/dist/shared/components/draggable-dashboard/composables/useDashboardGrid.d.ts +12 -4
- package/dist/shared/components/draggable-dashboard/composables/useDashboardGrid.d.ts.map +1 -1
- package/dist/shared/components/draggable-dashboard/composables/useDragClone.d.ts +15 -0
- package/dist/shared/components/draggable-dashboard/composables/useDragClone.d.ts.map +1 -0
- package/dist/shared/components/draggable-dashboard/composables/useEventCoordinates.d.ts +33 -0
- package/dist/shared/components/draggable-dashboard/composables/useEventCoordinates.d.ts.map +1 -0
- package/dist/shared/components/draggable-dashboard/composables/useGridPosition.d.ts +57 -0
- package/dist/shared/components/draggable-dashboard/composables/useGridPosition.d.ts.map +1 -0
- package/dist/shared/components/draggable-dashboard/composables/useGridSystem.d.ts +22 -0
- package/dist/shared/components/draggable-dashboard/composables/useGridSystem.d.ts.map +1 -0
- package/dist/shared/components/draggable-dashboard/composables/useLayoutPersistence.d.ts +19 -0
- package/dist/shared/components/draggable-dashboard/composables/useLayoutPersistence.d.ts.map +1 -0
- package/dist/shared/components/draggable-dashboard/composables/useResizeObserver.d.ts +18 -0
- package/dist/shared/components/draggable-dashboard/composables/useResizeObserver.d.ts.map +1 -0
- package/dist/shared/components/draggable-dashboard/composables/useWidgetLayout.d.ts +14 -0
- package/dist/shared/components/draggable-dashboard/composables/useWidgetLayout.d.ts.map +1 -0
- package/dist/shared/components/draggable-dashboard/composables/useWidgetStyles.d.ts +21 -0
- package/dist/shared/components/draggable-dashboard/composables/useWidgetStyles.d.ts.map +1 -0
- package/dist/shared/components/draggable-dashboard/types.d.ts +5 -1
- package/dist/shared/components/draggable-dashboard/types.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/ui/components/atoms/vc-icon/icons/FulfillmentCentersIcon.vue.d.ts +18 -0
- package/dist/ui/components/atoms/vc-icon/icons/FulfillmentCentersIcon.vue.d.ts.map +1 -0
- package/dist/ui/components/atoms/vc-icon/icons/OffersIcon.vue.d.ts +18 -0
- package/dist/ui/components/atoms/vc-icon/icons/OffersIcon.vue.d.ts.map +1 -0
- package/dist/ui/components/atoms/vc-icon/icons/OrdersIcon.vue.d.ts +18 -0
- package/dist/ui/components/atoms/vc-icon/icons/OrdersIcon.vue.d.ts.map +1 -0
- package/dist/ui/components/atoms/vc-icon/icons/PeopleIcon.vue.d.ts +18 -0
- package/dist/ui/components/atoms/vc-icon/icons/PeopleIcon.vue.d.ts.map +1 -0
- package/dist/ui/components/atoms/vc-icon/icons/ProductsIcon.vue.d.ts +18 -0
- package/dist/ui/components/atoms/vc-icon/icons/ProductsIcon.vue.d.ts.map +1 -0
- package/dist/ui/components/atoms/vc-icon/icons/ProfileIcon.vue.d.ts +18 -0
- package/dist/ui/components/atoms/vc-icon/icons/ProfileIcon.vue.d.ts.map +1 -0
- package/dist/ui/components/atoms/vc-icon/icons/index.d.ts +6 -0
- package/dist/ui/components/atoms/vc-icon/icons/index.d.ts.map +1 -1
- package/dist/ui/components/atoms/vc-icon/vc-icon.vue.d.ts.map +1 -1
- package/dist/ui/components/organisms/vc-app/_internal/vc-app-bar/components/app-bar-button/app-bar-button.vue.d.ts +1 -0
- package/dist/ui/components/organisms/vc-app/_internal/vc-app-bar/components/app-bar-button/app-bar-button.vue.d.ts.map +1 -1
- package/dist/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue.d.ts +2 -1
- package/dist/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue.d.ts.map +1 -1
- package/dist/ui/components/organisms/vc-app/vc-app.vue.d.ts.map +1 -1
- package/dist/ui/components/organisms/vc-table/vc-table.vue.d.ts.map +1 -1
- package/package.json +4 -4
- package/shared/components/draggable-dashboard/DraggableDashboard.vue +114 -148
- package/shared/components/draggable-dashboard/composables/useCellSizeCalculator.ts +121 -0
- package/shared/components/draggable-dashboard/composables/useCollisionDetection.ts +219 -0
- package/shared/components/draggable-dashboard/composables/useDashboardDragAndDrop.ts +126 -331
- package/shared/components/draggable-dashboard/composables/useDashboardGrid.ts +74 -220
- package/shared/components/draggable-dashboard/composables/useDragClone.ts +97 -0
- package/shared/components/draggable-dashboard/composables/useEventCoordinates.ts +91 -0
- package/shared/components/draggable-dashboard/composables/useGridPosition.ts +150 -0
- package/shared/components/draggable-dashboard/composables/useGridSystem.ts +169 -0
- package/shared/components/draggable-dashboard/composables/useLayoutPersistence.ts +89 -0
- package/shared/components/draggable-dashboard/composables/useResizeObserver.ts +105 -0
- package/shared/components/draggable-dashboard/composables/useWidgetLayout.ts +264 -0
- package/shared/components/draggable-dashboard/composables/useWidgetStyles.ts +120 -0
- package/shared/components/draggable-dashboard/types.ts +6 -1
- package/shared/components/notification-dropdown/notification-dropdown.vue +1 -0
- package/ui/components/atoms/vc-icon/icons/FulfillmentCentersIcon.vue +27 -0
- package/ui/components/atoms/vc-icon/icons/OffersIcon.vue +23 -0
- package/ui/components/atoms/vc-icon/icons/OrdersIcon.vue +19 -0
- package/ui/components/atoms/vc-icon/icons/PeopleIcon.vue +21 -0
- package/ui/components/atoms/vc-icon/icons/ProductsIcon.vue +23 -0
- package/ui/components/atoms/vc-icon/icons/ProfileIcon.vue +18 -0
- package/ui/components/atoms/vc-icon/icons/index.ts +6 -0
- package/ui/components/atoms/vc-icon/vc-icon.vue +101 -82
- package/ui/components/organisms/vc-app/_internal/vc-app-bar/components/app-bar-button/app-bar-button.vue +10 -3
- package/ui/components/organisms/vc-app/_internal/vc-app-bar/vc-app-bar.vue +3 -4
- package/ui/components/organisms/vc-app/_internal/vc-app-menu/_internal/vc-app-menu-item/vc-app-menu-item.vue +2 -2
- package/ui/components/organisms/vc-app/_internal/vc-app-menu/vc-app-menu.vue +1 -0
- package/ui/components/organisms/vc-app/vc-app.vue +2 -3
- package/ui/components/organisms/vc-table/vc-table.vue +4 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { ref, computed } from "vue";
|
|
2
|
+
import type { IDashboardWidget, DashboardWidgetPosition } from "../types";
|
|
3
|
+
|
|
4
|
+
// Константы для работы с сеткой
|
|
5
|
+
export const GRID_COLUMNS = 12;
|
|
6
|
+
export const MIN_GRID_ROWS = 12;
|
|
7
|
+
export const ROWS_BUFFER = 10;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hook for working with the dashboard grid
|
|
11
|
+
*
|
|
12
|
+
* Provides basic functions and constants for working with the grid
|
|
13
|
+
*
|
|
14
|
+
* @returns An object with functions and constants for working with the grid
|
|
15
|
+
*/
|
|
16
|
+
export function useGridSystem() {
|
|
17
|
+
// Dynamic number of rows in the grid
|
|
18
|
+
const dynamicRows = ref(MIN_GRID_ROWS);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Updates the number of rows based on the widget positions
|
|
22
|
+
*
|
|
23
|
+
* @param widgets The array of widgets
|
|
24
|
+
* @param layout The map of widget positions
|
|
25
|
+
* @returns The current number of rows
|
|
26
|
+
*/
|
|
27
|
+
const updateGridRows = (widgets: IDashboardWidget[], layout: Map<string, DashboardWidgetPosition>): number => {
|
|
28
|
+
if (widgets.length === 0) {
|
|
29
|
+
dynamicRows.value = MIN_GRID_ROWS;
|
|
30
|
+
return dynamicRows.value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Find the maximum occupied row
|
|
34
|
+
const maxY = Math.max(
|
|
35
|
+
...Array.from(layout.values()).map((pos, index) => {
|
|
36
|
+
const widget = widgets.find((w) => w.id === Array.from(layout.keys())[index]);
|
|
37
|
+
return widget ? pos.y + widget.size.height : pos.y + 1;
|
|
38
|
+
}),
|
|
39
|
+
0,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Set the number of rows with a buffer for scrolling
|
|
43
|
+
dynamicRows.value = Math.max(maxY + ROWS_BUFFER, MIN_GRID_ROWS);
|
|
44
|
+
return dynamicRows.value;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Checks if there is a collision (overlap) of the widget in the specified position
|
|
49
|
+
*
|
|
50
|
+
* @param widget The widget to check
|
|
51
|
+
* @param position The position to check
|
|
52
|
+
* @param widgets The array of all widgets
|
|
53
|
+
* @param layout The map of widget positions
|
|
54
|
+
* @param excludeWidgetId The ID of the widget to exclude from the check (optional)
|
|
55
|
+
* @returns true if there is a collision, false otherwise
|
|
56
|
+
*/
|
|
57
|
+
const hasCollision = (
|
|
58
|
+
widget: IDashboardWidget,
|
|
59
|
+
position: DashboardWidgetPosition,
|
|
60
|
+
widgets: IDashboardWidget[],
|
|
61
|
+
layout: Map<string, DashboardWidgetPosition>,
|
|
62
|
+
excludeWidgetId?: string,
|
|
63
|
+
): boolean => {
|
|
64
|
+
// Check if the position is out of the grid
|
|
65
|
+
if (position.x < 0 || position.x + widget.size.width > GRID_COLUMNS || position.y < 0) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create a map of occupied cells for optimization
|
|
70
|
+
const occupiedCells = new Set<string>();
|
|
71
|
+
|
|
72
|
+
// Fill the map with occupied cells
|
|
73
|
+
widgets.forEach((other) => {
|
|
74
|
+
if (other.id === widget.id || (excludeWidgetId && other.id === excludeWidgetId)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const otherPos = layout.get(other.id);
|
|
79
|
+
if (!otherPos) return;
|
|
80
|
+
|
|
81
|
+
for (let x = otherPos.x; x < otherPos.x + other.size.width; x++) {
|
|
82
|
+
for (let y = otherPos.y; y < otherPos.y + other.size.height; y++) {
|
|
83
|
+
occupiedCells.add(`${x},${y}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Check for collisions for each cell of the widget
|
|
89
|
+
for (let x = position.x; x < position.x + widget.size.width; x++) {
|
|
90
|
+
for (let y = position.y; y < position.y + widget.size.height; y++) {
|
|
91
|
+
if (occupiedCells.has(`${x},${y}`)) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return false;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Checks if the specified area is free for placement of the widget
|
|
102
|
+
*
|
|
103
|
+
* @param x The X coordinate
|
|
104
|
+
* @param y The Y coordinate
|
|
105
|
+
* @param width The width of the widget
|
|
106
|
+
* @param height The height of the widget
|
|
107
|
+
* @param occupiedCells The set of occupied cells
|
|
108
|
+
* @returns true if the area is free, false otherwise
|
|
109
|
+
*/
|
|
110
|
+
const isAreaFree = (x: number, y: number, width: number, height: number, occupiedCells: Set<string>): boolean => {
|
|
111
|
+
// Check if the area is out of the grid
|
|
112
|
+
if (x < 0 || x + width > GRID_COLUMNS || y < 0) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check each cell in the area
|
|
117
|
+
for (let ix = x; ix < x + width; ix++) {
|
|
118
|
+
for (let iy = y; iy < y + height; iy++) {
|
|
119
|
+
if (occupiedCells.has(`${ix},${iy}`)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Creates a map of occupied cells based on the current layout
|
|
129
|
+
*
|
|
130
|
+
* @param widgets The array of widgets
|
|
131
|
+
* @param layout The map of widget positions
|
|
132
|
+
* @returns The set of occupied cells in the format "x,y"
|
|
133
|
+
*/
|
|
134
|
+
const createOccupiedCellsMap = (
|
|
135
|
+
widgets: IDashboardWidget[],
|
|
136
|
+
layout: Map<string, DashboardWidgetPosition>,
|
|
137
|
+
excludeWidgetId?: string,
|
|
138
|
+
): Set<string> => {
|
|
139
|
+
const occupiedCells = new Set<string>();
|
|
140
|
+
|
|
141
|
+
widgets.forEach((widget) => {
|
|
142
|
+
if (excludeWidgetId && widget.id === excludeWidgetId) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const pos = layout.get(widget.id);
|
|
147
|
+
if (!pos) return;
|
|
148
|
+
|
|
149
|
+
for (let x = pos.x; x < pos.x + widget.size.width; x++) {
|
|
150
|
+
for (let y = pos.y; y < pos.y + widget.size.height; y++) {
|
|
151
|
+
occupiedCells.add(`${x},${y}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return occupiedCells;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
GRID_COLUMNS,
|
|
161
|
+
MIN_GRID_ROWS,
|
|
162
|
+
ROWS_BUFFER,
|
|
163
|
+
dynamicRows,
|
|
164
|
+
updateGridRows,
|
|
165
|
+
hasCollision,
|
|
166
|
+
isAreaFree,
|
|
167
|
+
createOccupiedCellsMap,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useLocalStorage } from "@vueuse/core";
|
|
2
|
+
import type { DashboardWidgetPosition } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook for managing the saving and loading of the dashboard layout
|
|
6
|
+
*
|
|
7
|
+
* Provides functions for saving and loading the dashboard layout from localStorage
|
|
8
|
+
*
|
|
9
|
+
* @param storageKey The key for local storage
|
|
10
|
+
* @param updateCallback The function for updating the position of the widget
|
|
11
|
+
* @returns An object with functions for working with the saved layout
|
|
12
|
+
*/
|
|
13
|
+
export function useLayoutPersistence(
|
|
14
|
+
storageKey: string,
|
|
15
|
+
updateCallback: (widgetId: string, position: DashboardWidgetPosition) => void,
|
|
16
|
+
) {
|
|
17
|
+
// Use useLocalStorage for working with localStorage
|
|
18
|
+
const savedLayout = useLocalStorage<Record<string, DashboardWidgetPosition>>(storageKey, {});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Saves the current layout to localStorage
|
|
22
|
+
*
|
|
23
|
+
* @param layout The current layout to save
|
|
24
|
+
*/
|
|
25
|
+
const saveLayout = (layout: Map<string, DashboardWidgetPosition>): void => {
|
|
26
|
+
try {
|
|
27
|
+
// Convert Map to an object for serialization
|
|
28
|
+
const layoutData: Record<string, DashboardWidgetPosition> = {};
|
|
29
|
+
|
|
30
|
+
layout.forEach((position, widgetId) => {
|
|
31
|
+
layoutData[widgetId] = position;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Save to localStorage through the reactive variable
|
|
35
|
+
savedLayout.value = layoutData;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error("Failed to save dashboard layout to localStorage:", error);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Loads the saved layout from localStorage
|
|
43
|
+
*
|
|
44
|
+
* @param widgets The array of widgets to check for existence
|
|
45
|
+
* @returns true if the layout was successfully loaded, false otherwise
|
|
46
|
+
*/
|
|
47
|
+
const loadLayout = <T extends { id: string }>(widgets: T[]): boolean => {
|
|
48
|
+
try {
|
|
49
|
+
// Check for the existence of the saved layout
|
|
50
|
+
if (!savedLayout.value || Object.keys(savedLayout.value).length === 0) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Apply positions to existing widgets
|
|
55
|
+
Object.entries(savedLayout.value).forEach(([widgetId, position]) => {
|
|
56
|
+
const widget = widgets.find((w) => w.id === widgetId);
|
|
57
|
+
if (widget) {
|
|
58
|
+
updateCallback(widgetId, position);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return true;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error("Failed to load dashboard layout from localStorage:", error);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Clears the saved layout from localStorage
|
|
71
|
+
*/
|
|
72
|
+
const clearSavedLayout = (): void => {
|
|
73
|
+
savedLayout.value = {};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Checks if there is a saved layout in localStorage
|
|
78
|
+
*/
|
|
79
|
+
const hasSavedLayout = (): boolean => {
|
|
80
|
+
return Object.keys(savedLayout.value).length > 0;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
saveLayout,
|
|
85
|
+
loadLayout,
|
|
86
|
+
clearSavedLayout,
|
|
87
|
+
hasSavedLayout,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ref, onUnmounted } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook for managing ResizeObserver
|
|
5
|
+
*
|
|
6
|
+
* Provides functionality for tracking changes in the sizes of elements
|
|
7
|
+
*
|
|
8
|
+
* @param callback The function called when the sizes change
|
|
9
|
+
* @param options Options
|
|
10
|
+
* @returns An object with methods for working with ResizeObserver
|
|
11
|
+
*/
|
|
12
|
+
export function useResizeObserver(
|
|
13
|
+
callback: (entries: ResizeObserverEntry[]) => void,
|
|
14
|
+
options?: {
|
|
15
|
+
debounceMs?: number;
|
|
16
|
+
},
|
|
17
|
+
) {
|
|
18
|
+
// Instance of ResizeObserver
|
|
19
|
+
const observer = ref<ResizeObserver | null>(null);
|
|
20
|
+
|
|
21
|
+
// Timer identifier for debounce
|
|
22
|
+
const debounceTimer = ref<number | null>(null);
|
|
23
|
+
|
|
24
|
+
// Determine if the browser supports ResizeObserver
|
|
25
|
+
const isSupported = typeof window !== "undefined" && "ResizeObserver" in window;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The handler of the event of changing the sizes with debounce
|
|
29
|
+
*
|
|
30
|
+
* @param entries The array of ResizeObserverEntry
|
|
31
|
+
*/
|
|
32
|
+
const handleResize = (entries: ResizeObserverEntry[]) => {
|
|
33
|
+
if (options?.debounceMs) {
|
|
34
|
+
// Clear the previous timer if it exists
|
|
35
|
+
if (debounceTimer.value !== null) {
|
|
36
|
+
window.clearTimeout(debounceTimer.value);
|
|
37
|
+
debounceTimer.value = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Set a new timer
|
|
41
|
+
debounceTimer.value = window.setTimeout(() => {
|
|
42
|
+
callback(entries);
|
|
43
|
+
}, options.debounceMs);
|
|
44
|
+
} else {
|
|
45
|
+
// Call immediately without debounce
|
|
46
|
+
callback(entries);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Initializes ResizeObserver for the specified element
|
|
52
|
+
*
|
|
53
|
+
* @param element The element to monitor the sizes
|
|
54
|
+
*/
|
|
55
|
+
const observe = (element: Element) => {
|
|
56
|
+
if (!isSupported) return;
|
|
57
|
+
|
|
58
|
+
// Create a new ResizeObserver if it doesn't exist
|
|
59
|
+
if (!observer.value) {
|
|
60
|
+
observer.value = new ResizeObserver(handleResize);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Start monitoring the changes in the sizes
|
|
64
|
+
observer.value.observe(element);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Stops monitoring the changes in the sizes for the specified element
|
|
69
|
+
*
|
|
70
|
+
* @param element The element for which to stop monitoring
|
|
71
|
+
*/
|
|
72
|
+
const unobserve = (element: Element) => {
|
|
73
|
+
if (!isSupported || !observer.value) return;
|
|
74
|
+
|
|
75
|
+
observer.value.unobserve(element);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Disconnects ResizeObserver and clears all resources
|
|
80
|
+
*/
|
|
81
|
+
const disconnect = () => {
|
|
82
|
+
if (!isSupported || !observer.value) return;
|
|
83
|
+
|
|
84
|
+
observer.value.disconnect();
|
|
85
|
+
observer.value = null;
|
|
86
|
+
|
|
87
|
+
// Clear the debounce timer
|
|
88
|
+
if (debounceTimer.value !== null) {
|
|
89
|
+
window.clearTimeout(debounceTimer.value);
|
|
90
|
+
debounceTimer.value = null;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Automatic cleanup when the component is unmounted
|
|
95
|
+
onUnmounted(() => {
|
|
96
|
+
disconnect();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
observe,
|
|
101
|
+
unobserve,
|
|
102
|
+
disconnect,
|
|
103
|
+
isSupported,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
import type { IDashboardWidget, DashboardWidgetPosition } from "../types";
|
|
3
|
+
import { useGridSystem } from "./useGridSystem";
|
|
4
|
+
|
|
5
|
+
// Constants for finding the optimal position
|
|
6
|
+
const SEARCH_DIRECTIONS = [
|
|
7
|
+
{ dx: 0, dy: 0 }, // Current position
|
|
8
|
+
{ dx: 0, dy: 1 }, // Down
|
|
9
|
+
{ dx: 1, dy: 0 }, // Right
|
|
10
|
+
{ dx: 0, dy: -1 }, // Up
|
|
11
|
+
{ dx: -1, dy: 0 }, // Left
|
|
12
|
+
{ dx: 1, dy: 1 }, // Right-down
|
|
13
|
+
{ dx: -1, dy: 1 }, // Left-down
|
|
14
|
+
{ dx: 1, dy: -1 }, // Right-up
|
|
15
|
+
{ dx: -1, dy: -1 }, // Left-up
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hook for managing the placement of widgets on the dashboard
|
|
20
|
+
*
|
|
21
|
+
* @param updatePositionCallback The function for updating the position of the widget
|
|
22
|
+
* @returns An object with functions for working with the placement of widgets
|
|
23
|
+
*/
|
|
24
|
+
export function useWidgetLayout(updatePositionCallback: (widgetId: string, position: DashboardWidgetPosition) => void) {
|
|
25
|
+
const grid = useGridSystem();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Finds a free position for the widget
|
|
29
|
+
*
|
|
30
|
+
* @param widget The widget to place
|
|
31
|
+
* @param widgets The array of all widgets
|
|
32
|
+
* @param layout The map of widget positions
|
|
33
|
+
* @returns The free position for the widget
|
|
34
|
+
*/
|
|
35
|
+
const findFreePosition = (
|
|
36
|
+
widget: IDashboardWidget,
|
|
37
|
+
widgets: IDashboardWidget[],
|
|
38
|
+
layout: Map<string, DashboardWidgetPosition>,
|
|
39
|
+
): DashboardWidgetPosition => {
|
|
40
|
+
const currentPos = layout.get(widget.id);
|
|
41
|
+
|
|
42
|
+
// If the current position is free, use it
|
|
43
|
+
if (currentPos && !grid.hasCollision(widget, currentPos, widgets, layout, widget.id)) {
|
|
44
|
+
return currentPos;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Start searching from the top left corner or the current position
|
|
48
|
+
const startX = currentPos?.x || 0;
|
|
49
|
+
const startY = currentPos?.y || 0;
|
|
50
|
+
|
|
51
|
+
// Spiral search for a free position
|
|
52
|
+
let layer = 0;
|
|
53
|
+
const maxLayers = Math.max(grid.GRID_COLUMNS, grid.dynamicRows.value);
|
|
54
|
+
|
|
55
|
+
while (layer < maxLayers) {
|
|
56
|
+
// Check all directions in the current layer
|
|
57
|
+
for (const { dx, dy } of SEARCH_DIRECTIONS) {
|
|
58
|
+
const x = startX + dx * layer;
|
|
59
|
+
const y = startY + dy * layer;
|
|
60
|
+
|
|
61
|
+
const position = { x, y };
|
|
62
|
+
|
|
63
|
+
// Check the position
|
|
64
|
+
if (!grid.hasCollision(widget, position, widgets, layout, widget.id)) {
|
|
65
|
+
return position;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
layer++;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If we didn't find a position, add to the end
|
|
73
|
+
return {
|
|
74
|
+
x: 0,
|
|
75
|
+
y: grid.dynamicRows.value,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Finds the optimal position for the widget considering the occupied cells
|
|
81
|
+
*
|
|
82
|
+
* @param widget The widget to place
|
|
83
|
+
* @param occupiedCells The set of occupied cells
|
|
84
|
+
* @param maxRows The maximum number of rows
|
|
85
|
+
* @returns The optimal position for the widget
|
|
86
|
+
*/
|
|
87
|
+
const findOptimalPosition = (
|
|
88
|
+
widget: IDashboardWidget,
|
|
89
|
+
occupiedCells: Set<string>,
|
|
90
|
+
maxRows: number,
|
|
91
|
+
): DashboardWidgetPosition => {
|
|
92
|
+
// Search for a position with the minimum Y, then with the minimum X
|
|
93
|
+
let bestPos: DashboardWidgetPosition = { x: 0, y: 0 };
|
|
94
|
+
let minY = Number.MAX_SAFE_INTEGER;
|
|
95
|
+
|
|
96
|
+
// Sliding window over the entire grid to find free space
|
|
97
|
+
for (let y = 0; y < maxRows; y++) {
|
|
98
|
+
for (let x = 0; x <= grid.GRID_COLUMNS - widget.size.width; x++) {
|
|
99
|
+
if (grid.isAreaFree(x, y, widget.size.width, widget.size.height, occupiedCells)) {
|
|
100
|
+
// Found free space
|
|
101
|
+
if (y < minY) {
|
|
102
|
+
minY = y;
|
|
103
|
+
bestPos = { x, y };
|
|
104
|
+
} else if (y === minY && x < bestPos.x) {
|
|
105
|
+
// If on the same row, choose the left one
|
|
106
|
+
bestPos = { x, y };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// If we found a place in the first row, return it immediately
|
|
110
|
+
if (y === 0) {
|
|
111
|
+
return bestPos;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If we found something, and moved to the next row, stop searching
|
|
117
|
+
if (minY !== Number.MAX_SAFE_INTEGER && y > minY) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// If we didn't find a place, return the extreme position
|
|
123
|
+
if (minY === Number.MAX_SAFE_INTEGER) {
|
|
124
|
+
return { x: 0, y: maxRows };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return bestPos;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Places widgets in rows, minimizing empty spaces
|
|
132
|
+
*
|
|
133
|
+
* @param widgetsToArrange The array of widgets to place
|
|
134
|
+
* @param allWidgets The array of all widgets
|
|
135
|
+
* @param layout The map of widget positions
|
|
136
|
+
*/
|
|
137
|
+
const arrangeWidgetsInRows = (
|
|
138
|
+
widgetsToArrange: IDashboardWidget[],
|
|
139
|
+
allWidgets: IDashboardWidget[],
|
|
140
|
+
layout: Map<string, DashboardWidgetPosition>,
|
|
141
|
+
): void => {
|
|
142
|
+
if (widgetsToArrange.length === 0) return;
|
|
143
|
+
|
|
144
|
+
const sortedWidgets = [...widgetsToArrange].sort((a, b) => {
|
|
145
|
+
// Sort by size (bigger first)
|
|
146
|
+
const aSize = a.size.width * a.size.height;
|
|
147
|
+
const bSize = b.size.width * b.size.height;
|
|
148
|
+
return bSize - aSize;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Current position for placement
|
|
152
|
+
let currentX = 0;
|
|
153
|
+
let currentY = 0;
|
|
154
|
+
let rowHeight = 0;
|
|
155
|
+
|
|
156
|
+
sortedWidgets.forEach((widget) => {
|
|
157
|
+
// Проверяем, помещается ли виджет в текущую строку
|
|
158
|
+
if (currentX + widget.size.width > grid.GRID_COLUMNS) {
|
|
159
|
+
// If it doesn't fit, move to the next row
|
|
160
|
+
currentX = 0;
|
|
161
|
+
currentY += rowHeight;
|
|
162
|
+
rowHeight = 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Place the widget on the current position
|
|
166
|
+
const position = { x: currentX, y: currentY };
|
|
167
|
+
updatePositionCallback(widget.id, position);
|
|
168
|
+
|
|
169
|
+
// Update the current position
|
|
170
|
+
currentX += widget.size.width;
|
|
171
|
+
|
|
172
|
+
// Update the maximum height for the current row
|
|
173
|
+
rowHeight = Math.max(rowHeight, widget.size.height);
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Initializes widgets with their built-in positions
|
|
179
|
+
*
|
|
180
|
+
* @param widgets The array of widgets
|
|
181
|
+
* @param layout The map of widget positions
|
|
182
|
+
* @returns true if at least one widget had a built-in position
|
|
183
|
+
*/
|
|
184
|
+
const initializeWithBuiltInPositions = (
|
|
185
|
+
widgets: IDashboardWidget[],
|
|
186
|
+
layout: Map<string, DashboardWidgetPosition>,
|
|
187
|
+
): boolean => {
|
|
188
|
+
if (widgets.length === 0) return false;
|
|
189
|
+
|
|
190
|
+
// Separate widgets into those that have built-in positions and those that don't
|
|
191
|
+
const widgetsWithPositions = widgets.filter((widget) => widget.position);
|
|
192
|
+
const widgetsWithoutPositions = widgets.filter((widget) => !widget.position);
|
|
193
|
+
|
|
194
|
+
// If there are no widgets with built-in positions, exit
|
|
195
|
+
if (widgetsWithPositions.length === 0) return false;
|
|
196
|
+
|
|
197
|
+
// Sort by rows and columns for more predictable placement
|
|
198
|
+
const sortedWidgets = [...widgetsWithPositions].sort((a, b) => {
|
|
199
|
+
if (!a.position || !b.position) return 0;
|
|
200
|
+
// First sort by y (rows)
|
|
201
|
+
if (a.position.y !== b.position.y) return a.position.y - b.position.y;
|
|
202
|
+
// Then by x (columns)
|
|
203
|
+
return a.position.x - b.position.x;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Clear the current layout
|
|
207
|
+
layout.clear();
|
|
208
|
+
|
|
209
|
+
// Place widgets with given positions
|
|
210
|
+
sortedWidgets.forEach((widget) => {
|
|
211
|
+
if (widget.position) {
|
|
212
|
+
updatePositionCallback(widget.id, widget.position);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Check and fix collisions for widgets with given positions
|
|
217
|
+
let hasCollisions;
|
|
218
|
+
do {
|
|
219
|
+
hasCollisions = false;
|
|
220
|
+
for (const widget of sortedWidgets) {
|
|
221
|
+
if (!widget.position) continue;
|
|
222
|
+
|
|
223
|
+
const position = layout.get(widget.id);
|
|
224
|
+
if (position && grid.hasCollision(widget, position, widgets, layout, widget.id)) {
|
|
225
|
+
hasCollisions = true;
|
|
226
|
+
const freePosition = findFreePosition(widget, widgets, layout);
|
|
227
|
+
updatePositionCallback(widget.id, freePosition);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} while (hasCollisions);
|
|
231
|
+
|
|
232
|
+
// Create a map of occupied cells for optimal placement of the remaining widgets
|
|
233
|
+
const occupiedCells = grid.createOccupiedCellsMap(widgets, layout);
|
|
234
|
+
|
|
235
|
+
// Sort the remaining widgets by size (bigger first)
|
|
236
|
+
const sortedRemaining = [...widgetsWithoutPositions].sort((a, b) => {
|
|
237
|
+
const aSize = a.size.width * a.size.height;
|
|
238
|
+
const bSize = b.size.width * b.size.height;
|
|
239
|
+
return bSize - aSize; // Bigger first
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Place the remaining widgets using the optimal algorithm
|
|
243
|
+
for (const widget of sortedRemaining) {
|
|
244
|
+
const position = findOptimalPosition(widget, occupiedCells, grid.dynamicRows.value + grid.ROWS_BUFFER);
|
|
245
|
+
updatePositionCallback(widget.id, position);
|
|
246
|
+
|
|
247
|
+
// Update the map of occupied cells
|
|
248
|
+
for (let x = position.x; x < position.x + widget.size.width; x++) {
|
|
249
|
+
for (let y = position.y; y < position.y + widget.size.height; y++) {
|
|
250
|
+
occupiedCells.add(`${x},${y}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return true;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
findFreePosition,
|
|
260
|
+
findOptimalPosition,
|
|
261
|
+
arrangeWidgetsInRows,
|
|
262
|
+
initializeWithBuiltInPositions,
|
|
263
|
+
};
|
|
264
|
+
}
|