flexdesk 0.1.0
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/LICENSE +21 -0
- package/README.md +195 -0
- package/THIRD_PARTY_LICENSES.md +53 -0
- package/css/base.css +3439 -0
- package/css/flexdesk.css +5797 -0
- package/css/overrides.css +2349 -0
- package/css/reset.css +46 -0
- package/css/tokens.css +137 -0
- package/dist/charts.js +780 -0
- package/dist/charts.js.map +7 -0
- package/dist/chunk-3PHCPZHT.js +222 -0
- package/dist/chunk-3PHCPZHT.js.map +7 -0
- package/dist/chunk-CT4YXXLP.js +1770 -0
- package/dist/chunk-CT4YXXLP.js.map +7 -0
- package/dist/chunk-DRYCDMEG.js +2217 -0
- package/dist/chunk-DRYCDMEG.js.map +7 -0
- package/dist/chunk-DVU44T77.js +855 -0
- package/dist/chunk-DVU44T77.js.map +7 -0
- package/dist/chunk-FL5KFNQH.js +640 -0
- package/dist/chunk-FL5KFNQH.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +117 -0
- package/dist/chunk-FOOS3T5L.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +32 -0
- package/dist/chunk-JYWURG5T.js.map +7 -0
- package/dist/chunk-TLZUUFOE.js +276 -0
- package/dist/chunk-TLZUUFOE.js.map +7 -0
- package/dist/chunk-UCJ2WD4D.js +625 -0
- package/dist/chunk-UCJ2WD4D.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +205 -0
- package/dist/chunk-WVFGV5FT.js.map +7 -0
- package/dist/core.js +1098 -0
- package/dist/core.js.map +7 -0
- package/dist/editor.js +1661 -0
- package/dist/editor.js.map +7 -0
- package/dist/flexdesk.css +5797 -0
- package/dist/host.js +132 -0
- package/dist/host.js.map +7 -0
- package/dist/reset.css +46 -0
- package/dist/tile_registry-6WZPTUZV.js +28 -0
- package/dist/tile_registry-6WZPTUZV.js.map +7 -0
- package/dist/tiles.js +2068 -0
- package/dist/tiles.js.map +7 -0
- package/dist/tokens.css +137 -0
- package/dist/widgets.js +5438 -0
- package/dist/widgets.js.map +7 -0
- package/dist/wm.js +4830 -0
- package/dist/wm.js.map +7 -0
- package/package.json +80 -0
- package/src/charts/chart_types.js +699 -0
- package/src/charts/data_series_plot_window.js +761 -0
- package/src/charts/downsample.js +119 -0
- package/src/charts/plot_config.js +176 -0
- package/src/charts/plot_popout_window.js +1560 -0
- package/src/charts/plotly_wrapper.js +432 -0
- package/src/core/event_bus.js +230 -0
- package/src/core/logging.js +300 -0
- package/src/core/settings.js +813 -0
- package/src/core/state_guard.js +382 -0
- package/src/core/state_guard_config.js +309 -0
- package/src/core/state_machine.js +258 -0
- package/src/editor/editor_pane.js +264 -0
- package/src/editor/monaco_editor_factory.js +449 -0
- package/src/editor/monaco_loader.js +74 -0
- package/src/editor/notebook_search.js +460 -0
- package/src/editor/notebook_tab_bar.js +456 -0
- package/src/editor/notebook_undo_manager.js +108 -0
- package/src/editor/split_pane_container.js +197 -0
- package/src/help/help_modal.js +409 -0
- package/src/help/help_registry.js +65 -0
- package/src/host/host.js +69 -0
- package/src/host/pywebview_host.js +133 -0
- package/src/tiles/config_schema.js +256 -0
- package/src/tiles/layout_persistence.js +662 -0
- package/src/tiles/tile_base.js +785 -0
- package/src/tiles/tile_grid.js +905 -0
- package/src/tiles/tile_registry.js +136 -0
- package/src/tiling/command_palette.js +328 -0
- package/src/tiling/content_registry.js +110 -0
- package/src/tiling/desktops.js +133 -0
- package/src/tiling/entity_sources.js +179 -0
- package/src/tiling/keymap.js +156 -0
- package/src/tiling/kind_taxonomy.js +172 -0
- package/src/tiling/landing_table.js +717 -0
- package/src/tiling/loading_overlay.js +33 -0
- package/src/tiling/nav_panel.js +142 -0
- package/src/tiling/page_factory.js +215 -0
- package/src/tiling/panel_keys.js +145 -0
- package/src/tiling/shell.js +437 -0
- package/src/tiling/tile_breadcrumb.js +222 -0
- package/src/tiling/tile_renderer.js +459 -0
- package/src/tiling/tile_tab_menu.js +340 -0
- package/src/tiling/tile_tree.js +880 -0
- package/src/tiling/wm.js +1352 -0
- package/src/ui/base/component_base.js +30 -0
- package/src/ui/base/controller_base.js +48 -0
- package/src/ui/base/page_base.js +102 -0
- package/src/ui/components/about_dialog.js +224 -0
- package/src/ui/components/action_dropdown.js +389 -0
- package/src/ui/components/attribute_list_editor.js +652 -0
- package/src/ui/components/autocomplete_field.js +732 -0
- package/src/ui/components/computing_status_window.js +81 -0
- package/src/ui/components/confirm_dialog.js +307 -0
- package/src/ui/components/context_menu.js +87 -0
- package/src/ui/components/data_table.js +2228 -0
- package/src/ui/components/detail_header.js +177 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/gallery_picker.js +147 -0
- package/src/ui/components/inline_renamer.js +129 -0
- package/src/ui/components/managed_window.js +804 -0
- package/src/ui/components/modal.js +512 -0
- package/src/ui/components/notification_history.js +264 -0
- package/src/ui/components/slide_out_panel.js +185 -0
- package/src/ui/components/slider_field.js +122 -0
- package/src/ui/components/sortable_list.js +309 -0
- package/src/ui/components/table_state_store.js +81 -0
- package/src/ui/components/toast.js +59 -0
- package/src/ui/components/tree_view.js +460 -0
- package/src/ui/components/window_taskbar.js +143 -0
- package/src/ui/controllers/panel_state_machine.js +634 -0
- package/src/ui/controllers/window_chrome_controller.js +659 -0
- package/src/ui/notification_center.js +576 -0
- package/src/ui/utils/overlay_scrollbar.js +492 -0
- package/src/ui/utils/raf_resize_observer.js +31 -0
- package/src/ui/utils/tooltip_service.js +406 -0
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tile_grid.js
|
|
3
|
+
*
|
|
4
|
+
* Core draggable tile grid system.
|
|
5
|
+
* Manages a 12-column CSS grid with drag-and-drop repositioning and resize functionality.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createWidget, getWidgetCatalog } from './tile_registry.js';
|
|
9
|
+
import { ActionDropdown } from '../ui/components/action_dropdown.js';
|
|
10
|
+
|
|
11
|
+
export class TileGrid {
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {HTMLElement} options.container - Container element for the grid
|
|
15
|
+
* @param {number} [options.columns=12] - Number of grid columns
|
|
16
|
+
* @param {number} [options.rowHeight=80] - Height of each grid row in pixels
|
|
17
|
+
* @param {number} [options.gap=16] - Gap between tiles in pixels
|
|
18
|
+
* @param {Object} [options.eventBus] - Event bus for cross-component communication
|
|
19
|
+
* @param {boolean} [options.showToolbar=true] - Whether to show built-in toolbar
|
|
20
|
+
* @param {Function} [options.onLayoutChange] - Callback when layout changes
|
|
21
|
+
* @param {Function} [options.onAddWidget] - Callback for add widget action
|
|
22
|
+
* @param {Function} [options.onResetLayout] - Callback for reset layout action
|
|
23
|
+
* @param {Function} [options.onExport] - Callback for export action
|
|
24
|
+
*/
|
|
25
|
+
constructor({ container, columns = 12, rowHeight = 80, gap = 16, eventBus = null,
|
|
26
|
+
showToolbar = true, readonly = false, onLayoutChange, onBeforeLayoutChange,
|
|
27
|
+
onAddWidget, onResetLayout, onExport,
|
|
28
|
+
// Handed down to every tile. TileBase used to import appHost() from
|
|
29
|
+
// ui/js/ecoagent/ to back Expand -> Export CSV/PNG; a grid that has
|
|
30
|
+
// no host simply gets the Blob-download fallback the host contract
|
|
31
|
+
// promises. It is not an error to have none.
|
|
32
|
+
host = null, stateGuard = null }) {
|
|
33
|
+
this.host = host;
|
|
34
|
+
this.stateGuard = stateGuard;
|
|
35
|
+
this.container = container;
|
|
36
|
+
this.columns = columns;
|
|
37
|
+
this.rowHeight = rowHeight;
|
|
38
|
+
this.gap = gap;
|
|
39
|
+
this.eventBus = eventBus;
|
|
40
|
+
this.showToolbar = showToolbar;
|
|
41
|
+
this.readonly = readonly;
|
|
42
|
+
this._onLayoutChangeCallback = onLayoutChange;
|
|
43
|
+
this._onBeforeLayoutChangeCallback = onBeforeLayoutChange;
|
|
44
|
+
this._onAddWidgetCallback = onAddWidget;
|
|
45
|
+
this._onResetLayoutCallback = onResetLayout;
|
|
46
|
+
this._onExportCallback = onExport;
|
|
47
|
+
|
|
48
|
+
/** @type {Map<string, import('./tile_base.js').TileBase>} */
|
|
49
|
+
this.tiles = new Map();
|
|
50
|
+
|
|
51
|
+
/** @type {Array<{id: string, x: number, y: number, w: number, h: number}>} */
|
|
52
|
+
this.layout = [];
|
|
53
|
+
|
|
54
|
+
/** @type {Set<string>|null} IDs of documentation cells (for "Show in Documentation" visibility) */
|
|
55
|
+
this.documentationCellIds = null;
|
|
56
|
+
|
|
57
|
+
this.gridElement = null;
|
|
58
|
+
this.data = null;
|
|
59
|
+
this.fullData = null;
|
|
60
|
+
|
|
61
|
+
// Drag state
|
|
62
|
+
this._dragState = null;
|
|
63
|
+
this._resizeState = null;
|
|
64
|
+
this._ghostElement = null;
|
|
65
|
+
|
|
66
|
+
// Bound handlers for cleanup
|
|
67
|
+
this._boundOnPointerMove = this._onPointerMove.bind(this);
|
|
68
|
+
this._boundOnPointerUp = this._onPointerUp.bind(this);
|
|
69
|
+
|
|
70
|
+
this._init();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Initialize the grid container.
|
|
75
|
+
* @private
|
|
76
|
+
*/
|
|
77
|
+
_init() {
|
|
78
|
+
// Create grid element
|
|
79
|
+
this.gridElement = document.createElement('div');
|
|
80
|
+
this.gridElement.className = 'tile-grid';
|
|
81
|
+
this.gridElement.style.setProperty('--grid-columns', this.columns);
|
|
82
|
+
this.gridElement.style.setProperty('--grid-row-height', `${this.rowHeight}px`);
|
|
83
|
+
this.gridElement.style.setProperty('--grid-gap', `${this.gap}px`);
|
|
84
|
+
|
|
85
|
+
// Add grid to container
|
|
86
|
+
this.container.innerHTML = '';
|
|
87
|
+
this.container.appendChild(this.gridElement);
|
|
88
|
+
|
|
89
|
+
// Add toolbar (can be disabled for external toolbar)
|
|
90
|
+
if (this.showToolbar) {
|
|
91
|
+
this._buildToolbar();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Listen for tile events
|
|
95
|
+
this.gridElement.addEventListener('tile:config-request', this._onTileConfigRequest.bind(this));
|
|
96
|
+
this.gridElement.addEventListener('tile:remove-request', this._onTileRemoveRequest.bind(this));
|
|
97
|
+
|
|
98
|
+
// Global pointer events for drag/resize
|
|
99
|
+
document.addEventListener('pointermove', this._boundOnPointerMove);
|
|
100
|
+
document.addEventListener('pointerup', this._boundOnPointerUp);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build the dashboard bottom toolbar with add widget dropdown.
|
|
105
|
+
* @private
|
|
106
|
+
*/
|
|
107
|
+
_buildToolbar() {
|
|
108
|
+
const toolbar = document.createElement('div');
|
|
109
|
+
toolbar.className = 'tile-grid-toolbar tile-grid-toolbar--bottom';
|
|
110
|
+
this._toolbarElement = toolbar;
|
|
111
|
+
|
|
112
|
+
toolbar.innerHTML = `
|
|
113
|
+
<div class="toolbar-group toolbar-group--main">
|
|
114
|
+
<div class="add-widget-container">
|
|
115
|
+
<button class="toolbar-btn add-widget-btn twm-has-tooltip" data-tooltip="Add widget">
|
|
116
|
+
<span class="material-symbols-outlined">add</span>
|
|
117
|
+
<span>Add Widget</span>
|
|
118
|
+
<span class="material-symbols-outlined dropdown-arrow">expand_less</span>
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
<button class="toolbar-btn reset-layout-btn twm-has-tooltip" data-tooltip="Reset to default layout">
|
|
122
|
+
<span class="material-symbols-outlined">restart_alt</span>
|
|
123
|
+
<span class="btn-text">Reset</span>
|
|
124
|
+
</button>
|
|
125
|
+
<button class="toolbar-btn export-btn twm-has-tooltip" data-tooltip="Export report">
|
|
126
|
+
<span class="material-symbols-outlined">download</span>
|
|
127
|
+
<span class="btn-text">Export</span>
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
// Create add-widget dropdown via ActionDropdown
|
|
133
|
+
const addBtn = toolbar.querySelector('.add-widget-btn');
|
|
134
|
+
const catalog = getWidgetCatalog();
|
|
135
|
+
this._addWidgetDropdown = new ActionDropdown({
|
|
136
|
+
trigger: addBtn,
|
|
137
|
+
options: catalog.map(w => ({ type: w.type, label: w.title, icon: w.icon, description: w.description })),
|
|
138
|
+
onSelect: (opt) => this._addWidgetFromDropdown(opt.type),
|
|
139
|
+
className: 'add-widget-dropdown-menu'
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
toolbar.querySelector('.reset-layout-btn')?.addEventListener('click', () => this._resetLayout());
|
|
143
|
+
toolbar.querySelector('.export-btn')?.addEventListener('click', () => this._exportReport());
|
|
144
|
+
|
|
145
|
+
// Insert after grid element (at bottom)
|
|
146
|
+
this.container.appendChild(toolbar);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Add a widget from the dropdown selection.
|
|
151
|
+
* @param {string} type - Widget type
|
|
152
|
+
* @private
|
|
153
|
+
*/
|
|
154
|
+
_addWidgetFromDropdown(type) {
|
|
155
|
+
import('./tile_registry.js').then(({ getWidget }) => {
|
|
156
|
+
const WidgetClass = getWidget(type);
|
|
157
|
+
if (WidgetClass) {
|
|
158
|
+
const size = WidgetClass.DEFAULT_SIZE || { w: 4, h: 3 };
|
|
159
|
+
const pos = this.findNextPosition(size.w, size.h);
|
|
160
|
+
this.addTile(type, { ...pos, ...size });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Add a tile to the grid.
|
|
167
|
+
* @param {string} type - Widget type
|
|
168
|
+
* @param {Object} position - Grid position {x, y, w, h}
|
|
169
|
+
* @param {Object} [config] - Widget configuration
|
|
170
|
+
* @param {string} [id] - Optional tile ID (generated if not provided)
|
|
171
|
+
* @returns {import('./tile_base.js').TileBase | null} The created tile or null
|
|
172
|
+
*/
|
|
173
|
+
addTile(type, position, config = {}, id = null) {
|
|
174
|
+
const tileId = id || `tile-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
175
|
+
|
|
176
|
+
const tile = createWidget(type, {
|
|
177
|
+
id: tileId,
|
|
178
|
+
grid: this,
|
|
179
|
+
eventBus: this.eventBus,
|
|
180
|
+
config,
|
|
181
|
+
readonly: this.readonly,
|
|
182
|
+
host: this.host,
|
|
183
|
+
stateGuard: this.stateGuard,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (!tile) return null;
|
|
187
|
+
|
|
188
|
+
// Add to layout
|
|
189
|
+
const layoutEntry = {
|
|
190
|
+
id: tileId,
|
|
191
|
+
type,
|
|
192
|
+
x: position.x,
|
|
193
|
+
y: position.y,
|
|
194
|
+
w: position.w,
|
|
195
|
+
h: position.h,
|
|
196
|
+
config
|
|
197
|
+
};
|
|
198
|
+
this.layout.push(layoutEntry);
|
|
199
|
+
|
|
200
|
+
// Store tile instance
|
|
201
|
+
this.tiles.set(tileId, tile);
|
|
202
|
+
|
|
203
|
+
// Mount to grid
|
|
204
|
+
tile.mount(this.gridElement);
|
|
205
|
+
this._applyTilePosition(tile, layoutEntry);
|
|
206
|
+
|
|
207
|
+
// Setup drag handles
|
|
208
|
+
this._setupTileDragHandlers(tile);
|
|
209
|
+
|
|
210
|
+
// Render with current data if available
|
|
211
|
+
if (this.data) {
|
|
212
|
+
tile.fullData = this.fullData;
|
|
213
|
+
tile.update(this.data);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Emit layout changed
|
|
217
|
+
this._emitLayoutChanged();
|
|
218
|
+
|
|
219
|
+
return tile;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Remove a tile from the grid.
|
|
224
|
+
* @param {string} tileId - Tile ID to remove
|
|
225
|
+
*/
|
|
226
|
+
removeTile(tileId) {
|
|
227
|
+
const tile = this.tiles.get(tileId);
|
|
228
|
+
if (!tile) return;
|
|
229
|
+
|
|
230
|
+
tile.dispose();
|
|
231
|
+
this.tiles.delete(tileId);
|
|
232
|
+
|
|
233
|
+
// Remove from layout
|
|
234
|
+
this.layout = this.layout.filter(l => l.id !== tileId);
|
|
235
|
+
|
|
236
|
+
this._emitLayoutChanged();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Update all tiles with new data.
|
|
241
|
+
* @param {Object} data - Analytics data
|
|
242
|
+
*/
|
|
243
|
+
setData(data) {
|
|
244
|
+
this.data = data;
|
|
245
|
+
this.tiles.forEach(tile => {
|
|
246
|
+
tile.fullData = this.fullData;
|
|
247
|
+
tile.update(data);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get current layout.
|
|
253
|
+
* @returns {Array} Layout array
|
|
254
|
+
*/
|
|
255
|
+
getLayout() {
|
|
256
|
+
return this.layout.map(l => ({
|
|
257
|
+
id: l.id,
|
|
258
|
+
type: l.type,
|
|
259
|
+
x: l.x,
|
|
260
|
+
y: l.y,
|
|
261
|
+
w: l.w,
|
|
262
|
+
h: l.h,
|
|
263
|
+
config: this.tiles.get(l.id)?.config || l.config
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Set layout and rebuild tiles.
|
|
269
|
+
* @param {Array|Object} layout - Layout array or object with tiles property
|
|
270
|
+
*/
|
|
271
|
+
setLayout(layout) {
|
|
272
|
+
// Clear existing tiles
|
|
273
|
+
this.tiles.forEach(tile => tile.dispose());
|
|
274
|
+
this.tiles.clear();
|
|
275
|
+
this.layout = [];
|
|
276
|
+
|
|
277
|
+
// Clear grid content (keep toolbar)
|
|
278
|
+
this.gridElement.innerHTML = '';
|
|
279
|
+
|
|
280
|
+
// Handle both array and object with tiles property
|
|
281
|
+
const tilesArray = Array.isArray(layout) ? layout : (layout?.tiles || []);
|
|
282
|
+
|
|
283
|
+
// Rebuild from layout
|
|
284
|
+
tilesArray.forEach(item => {
|
|
285
|
+
this.addTile(item.type, {
|
|
286
|
+
x: item.x,
|
|
287
|
+
y: item.y,
|
|
288
|
+
w: item.w,
|
|
289
|
+
h: item.h
|
|
290
|
+
}, item.config, item.id);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Apply CSS grid position to a tile element.
|
|
296
|
+
* @param {import('./tile_base.js').TileBase} tile
|
|
297
|
+
* @param {{x: number, y: number, w: number, h: number}} position
|
|
298
|
+
* @private
|
|
299
|
+
*/
|
|
300
|
+
_applyTilePosition(tile, position) {
|
|
301
|
+
if (!tile.element) return;
|
|
302
|
+
|
|
303
|
+
tile.element.style.gridColumn = `${position.x + 1} / span ${position.w}`;
|
|
304
|
+
tile.element.style.gridRow = `${position.y + 1} / span ${position.h}`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Setup drag and resize handlers for a tile.
|
|
309
|
+
* @param {import('./tile_base.js').TileBase} tile
|
|
310
|
+
* @private
|
|
311
|
+
*/
|
|
312
|
+
_setupTileDragHandlers(tile) {
|
|
313
|
+
if (!tile.element || this.readonly) return;
|
|
314
|
+
|
|
315
|
+
const dragHandle = tile.element.querySelector('.tile-drag-handle');
|
|
316
|
+
const resizeHandle = tile.element.querySelector('.tile-resize-handle');
|
|
317
|
+
|
|
318
|
+
// Drag handle
|
|
319
|
+
if (dragHandle) {
|
|
320
|
+
dragHandle.addEventListener('pointerdown', (e) => this._onDragStart(tile, e));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Resize handle
|
|
324
|
+
if (resizeHandle) {
|
|
325
|
+
resizeHandle.addEventListener('pointerdown', (e) => this._onResizeStart(tile, e));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Handle drag start.
|
|
331
|
+
* @param {import('./tile_base.js').TileBase} tile
|
|
332
|
+
* @param {PointerEvent} e
|
|
333
|
+
* @private
|
|
334
|
+
*/
|
|
335
|
+
_onDragStart(tile, e) {
|
|
336
|
+
e.preventDefault();
|
|
337
|
+
e.stopPropagation();
|
|
338
|
+
|
|
339
|
+
const rect = tile.element.getBoundingClientRect();
|
|
340
|
+
const gridRect = this.gridElement.getBoundingClientRect();
|
|
341
|
+
|
|
342
|
+
const layoutItem = this.layout.find(l => l.id === tile.id);
|
|
343
|
+
if (!layoutItem) return;
|
|
344
|
+
|
|
345
|
+
// Notify before mutation so caller can snapshot current state
|
|
346
|
+
this._emitBeforeLayoutChange();
|
|
347
|
+
|
|
348
|
+
this._dragState = {
|
|
349
|
+
tile,
|
|
350
|
+
tileId: tile.id,
|
|
351
|
+
startX: e.clientX,
|
|
352
|
+
startY: e.clientY,
|
|
353
|
+
startGridX: layoutItem.x,
|
|
354
|
+
startGridY: layoutItem.y,
|
|
355
|
+
tileRect: rect,
|
|
356
|
+
gridRect,
|
|
357
|
+
w: layoutItem.w,
|
|
358
|
+
h: layoutItem.h
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
tile.element.classList.add('dragging');
|
|
362
|
+
|
|
363
|
+
// Create ghost element
|
|
364
|
+
this._createGhost(rect);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Handle resize start.
|
|
369
|
+
* @param {import('./tile_base.js').TileBase} tile
|
|
370
|
+
* @param {PointerEvent} e
|
|
371
|
+
* @private
|
|
372
|
+
*/
|
|
373
|
+
_onResizeStart(tile, e) {
|
|
374
|
+
e.preventDefault();
|
|
375
|
+
e.stopPropagation();
|
|
376
|
+
|
|
377
|
+
const layoutItem = this.layout.find(l => l.id === tile.id);
|
|
378
|
+
if (!layoutItem) return;
|
|
379
|
+
|
|
380
|
+
const gridRect = this.gridElement.getBoundingClientRect();
|
|
381
|
+
const cellWidth = (gridRect.width - (this.columns - 1) * this.gap) / this.columns;
|
|
382
|
+
|
|
383
|
+
// Notify before mutation so caller can snapshot current state
|
|
384
|
+
this._emitBeforeLayoutChange();
|
|
385
|
+
|
|
386
|
+
this._resizeState = {
|
|
387
|
+
tile,
|
|
388
|
+
tileId: tile.id,
|
|
389
|
+
startX: e.clientX,
|
|
390
|
+
startY: e.clientY,
|
|
391
|
+
startW: layoutItem.w,
|
|
392
|
+
startH: layoutItem.h,
|
|
393
|
+
gridRect,
|
|
394
|
+
cellWidth,
|
|
395
|
+
x: layoutItem.x,
|
|
396
|
+
y: layoutItem.y
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
tile.element.classList.add('resizing');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Handle pointer move (drag/resize).
|
|
404
|
+
* @param {PointerEvent} e
|
|
405
|
+
* @private
|
|
406
|
+
*/
|
|
407
|
+
_onPointerMove(e) {
|
|
408
|
+
if (this._dragState) {
|
|
409
|
+
this._onDragMove(e);
|
|
410
|
+
} else if (this._resizeState) {
|
|
411
|
+
this._onResizeMove(e);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Handle drag move.
|
|
417
|
+
* @param {PointerEvent} e
|
|
418
|
+
* @private
|
|
419
|
+
*/
|
|
420
|
+
_onDragMove(e) {
|
|
421
|
+
const state = this._dragState;
|
|
422
|
+
if (!state) return;
|
|
423
|
+
|
|
424
|
+
const dx = e.clientX - state.startX;
|
|
425
|
+
const dy = e.clientY - state.startY;
|
|
426
|
+
|
|
427
|
+
// Calculate cell dimensions
|
|
428
|
+
const gridRect = state.gridRect;
|
|
429
|
+
const cellWidth = (gridRect.width - (this.columns - 1) * this.gap) / this.columns;
|
|
430
|
+
const cellHeight = this.rowHeight;
|
|
431
|
+
|
|
432
|
+
// Calculate new grid position
|
|
433
|
+
const deltaGridX = Math.round(dx / (cellWidth + this.gap));
|
|
434
|
+
const deltaGridY = Math.round(dy / (cellHeight + this.gap));
|
|
435
|
+
|
|
436
|
+
let newX = state.startGridX + deltaGridX;
|
|
437
|
+
let newY = state.startGridY + deltaGridY;
|
|
438
|
+
|
|
439
|
+
// Clamp to grid bounds
|
|
440
|
+
newX = Math.max(0, Math.min(newX, this.columns - state.w));
|
|
441
|
+
newY = Math.max(0, newY);
|
|
442
|
+
|
|
443
|
+
// Update ghost position
|
|
444
|
+
if (this._ghostElement) {
|
|
445
|
+
const ghostLeft = newX * (cellWidth + this.gap);
|
|
446
|
+
const ghostTop = newY * (cellHeight + this.gap);
|
|
447
|
+
this._ghostElement.style.transform = `translate(${ghostLeft}px, ${ghostTop}px)`;
|
|
448
|
+
this._ghostElement.style.width = `${state.w * cellWidth + (state.w - 1) * this.gap}px`;
|
|
449
|
+
this._ghostElement.style.height = `${state.h * cellHeight + (state.h - 1) * this.gap}px`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Store pending position
|
|
453
|
+
state.pendingX = newX;
|
|
454
|
+
state.pendingY = newY;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Handle resize move.
|
|
459
|
+
* @param {PointerEvent} e
|
|
460
|
+
* @private
|
|
461
|
+
*/
|
|
462
|
+
_onResizeMove(e) {
|
|
463
|
+
const state = this._resizeState;
|
|
464
|
+
if (!state) return;
|
|
465
|
+
|
|
466
|
+
const dx = e.clientX - state.startX;
|
|
467
|
+
const dy = e.clientY - state.startY;
|
|
468
|
+
|
|
469
|
+
// Calculate new size in grid cells
|
|
470
|
+
const deltaW = Math.round(dx / (state.cellWidth + this.gap));
|
|
471
|
+
const deltaH = Math.round(dy / (this.rowHeight + this.gap));
|
|
472
|
+
|
|
473
|
+
let newW = state.startW + deltaW;
|
|
474
|
+
let newH = state.startH + deltaH;
|
|
475
|
+
|
|
476
|
+
// Get constraints
|
|
477
|
+
const tileClass = state.tile.constructor;
|
|
478
|
+
const constraints = tileClass.SIZE_CONSTRAINTS || { minW: 2, minH: 2, maxW: 12, maxH: 8 };
|
|
479
|
+
|
|
480
|
+
// Apply constraints
|
|
481
|
+
newW = Math.max(constraints.minW, Math.min(newW, constraints.maxW));
|
|
482
|
+
newH = Math.max(constraints.minH, Math.min(newH, constraints.maxH));
|
|
483
|
+
|
|
484
|
+
// Don't exceed grid width
|
|
485
|
+
newW = Math.min(newW, this.columns - state.x);
|
|
486
|
+
|
|
487
|
+
// Live update tile size
|
|
488
|
+
state.tile.element.style.gridColumn = `${state.x + 1} / span ${newW}`;
|
|
489
|
+
state.tile.element.style.gridRow = `${state.y + 1} / span ${newH}`;
|
|
490
|
+
|
|
491
|
+
state.pendingW = newW;
|
|
492
|
+
state.pendingH = newH;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Handle pointer up (end drag/resize).
|
|
497
|
+
* @param {PointerEvent} e
|
|
498
|
+
* @private
|
|
499
|
+
*/
|
|
500
|
+
_onPointerUp(e) {
|
|
501
|
+
if (this._dragState) {
|
|
502
|
+
this._onDragEnd();
|
|
503
|
+
} else if (this._resizeState) {
|
|
504
|
+
this._onResizeEnd();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Handle drag end.
|
|
510
|
+
* @private
|
|
511
|
+
*/
|
|
512
|
+
_onDragEnd() {
|
|
513
|
+
const state = this._dragState;
|
|
514
|
+
if (!state) return;
|
|
515
|
+
|
|
516
|
+
state.tile.element.classList.remove('dragging');
|
|
517
|
+
this._removeGhost();
|
|
518
|
+
|
|
519
|
+
// Update layout if position changed
|
|
520
|
+
if (state.pendingX !== undefined && state.pendingY !== undefined) {
|
|
521
|
+
const layoutItem = this.layout.find(l => l.id === state.tileId);
|
|
522
|
+
if (layoutItem) {
|
|
523
|
+
layoutItem.x = state.pendingX;
|
|
524
|
+
layoutItem.y = state.pendingY;
|
|
525
|
+
this._applyTilePosition(state.tile, layoutItem);
|
|
526
|
+
|
|
527
|
+
// Resolve overlaps - push other tiles down
|
|
528
|
+
this._resolveOverlaps(state.tileId);
|
|
529
|
+
|
|
530
|
+
this._emitLayoutChanged();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
this._dragState = null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Handle resize end.
|
|
539
|
+
* @private
|
|
540
|
+
*/
|
|
541
|
+
_onResizeEnd() {
|
|
542
|
+
const state = this._resizeState;
|
|
543
|
+
if (!state) return;
|
|
544
|
+
|
|
545
|
+
state.tile.element.classList.remove('resizing');
|
|
546
|
+
|
|
547
|
+
// Update layout if size changed
|
|
548
|
+
if (state.pendingW !== undefined && state.pendingH !== undefined) {
|
|
549
|
+
const layoutItem = this.layout.find(l => l.id === state.tileId);
|
|
550
|
+
if (layoutItem) {
|
|
551
|
+
layoutItem.w = state.pendingW;
|
|
552
|
+
layoutItem.h = state.pendingH;
|
|
553
|
+
|
|
554
|
+
// Resolve overlaps - push other tiles down
|
|
555
|
+
this._resolveOverlaps(state.tileId);
|
|
556
|
+
|
|
557
|
+
this._emitLayoutChanged();
|
|
558
|
+
|
|
559
|
+
// Re-render tile content for new size
|
|
560
|
+
if (this.data) {
|
|
561
|
+
state.tile.fullData = this.fullData;
|
|
562
|
+
state.tile.update(this.data);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
this._resizeState = null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Check if two layout items overlap.
|
|
572
|
+
* @param {Object} a - First layout item {x, y, w, h}
|
|
573
|
+
* @param {Object} b - Second layout item {x, y, w, h}
|
|
574
|
+
* @returns {boolean}
|
|
575
|
+
* @private
|
|
576
|
+
*/
|
|
577
|
+
_tilesOverlap(a, b) {
|
|
578
|
+
return !(
|
|
579
|
+
a.x + a.w <= b.x ||
|
|
580
|
+
b.x + b.w <= a.x ||
|
|
581
|
+
a.y + a.h <= b.y ||
|
|
582
|
+
b.y + b.h <= a.y
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Resolve overlaps by pushing tiles down.
|
|
588
|
+
* @param {string} sourceTileId - The tile that was resized/moved
|
|
589
|
+
* @private
|
|
590
|
+
*/
|
|
591
|
+
_resolveOverlaps(sourceTileId) {
|
|
592
|
+
const sourceItem = this.layout.find(l => l.id === sourceTileId);
|
|
593
|
+
if (!sourceItem) return;
|
|
594
|
+
|
|
595
|
+
// Sort tiles by y position (top to bottom), then by x
|
|
596
|
+
const otherTiles = this.layout
|
|
597
|
+
.filter(l => l.id !== sourceTileId)
|
|
598
|
+
.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
599
|
+
|
|
600
|
+
let hasChanges = true;
|
|
601
|
+
let iterations = 0;
|
|
602
|
+
const maxIterations = 100; // Prevent infinite loops
|
|
603
|
+
|
|
604
|
+
while (hasChanges && iterations < maxIterations) {
|
|
605
|
+
hasChanges = false;
|
|
606
|
+
iterations++;
|
|
607
|
+
|
|
608
|
+
for (const item of otherTiles) {
|
|
609
|
+
if (this._tilesOverlap(sourceItem, item)) {
|
|
610
|
+
// Push this tile down to be below the source tile
|
|
611
|
+
const newY = sourceItem.y + sourceItem.h;
|
|
612
|
+
if (item.y !== newY) {
|
|
613
|
+
item.y = newY;
|
|
614
|
+
hasChanges = true;
|
|
615
|
+
|
|
616
|
+
// Apply the new position visually
|
|
617
|
+
const tile = this.tiles.get(item.id);
|
|
618
|
+
if (tile) {
|
|
619
|
+
this._applyTilePosition(tile, item);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Check for cascading overlaps among the moved tiles
|
|
626
|
+
for (let i = 0; i < otherTiles.length; i++) {
|
|
627
|
+
for (let j = i + 1; j < otherTiles.length; j++) {
|
|
628
|
+
if (this._tilesOverlap(otherTiles[i], otherTiles[j])) {
|
|
629
|
+
// Push the lower-index tile (earlier in sort order) doesn't need to move
|
|
630
|
+
// Push the higher one down
|
|
631
|
+
const pusher = otherTiles[i];
|
|
632
|
+
const pushed = otherTiles[j];
|
|
633
|
+
const newY = pusher.y + pusher.h;
|
|
634
|
+
if (pushed.y < newY && this._tilesOverlap(pusher, pushed)) {
|
|
635
|
+
pushed.y = newY;
|
|
636
|
+
hasChanges = true;
|
|
637
|
+
|
|
638
|
+
const tile = this.tiles.get(pushed.id);
|
|
639
|
+
if (tile) {
|
|
640
|
+
this._applyTilePosition(tile, pushed);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Compact the layout - remove vertical gaps
|
|
649
|
+
this._compactLayout();
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Compact the layout by removing vertical gaps.
|
|
654
|
+
* @private
|
|
655
|
+
*/
|
|
656
|
+
_compactLayout() {
|
|
657
|
+
// Sort tiles by y position
|
|
658
|
+
const sortedTiles = [...this.layout].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
659
|
+
|
|
660
|
+
for (const item of sortedTiles) {
|
|
661
|
+
// Try to move up as far as possible
|
|
662
|
+
let newY = 0;
|
|
663
|
+
|
|
664
|
+
while (newY < item.y) {
|
|
665
|
+
const testPosition = { ...item, y: newY };
|
|
666
|
+
|
|
667
|
+
// Check for overlaps with tiles above this one
|
|
668
|
+
let hasOverlap = false;
|
|
669
|
+
for (const other of this.layout) {
|
|
670
|
+
if (other.id === item.id) continue;
|
|
671
|
+
if (this._tilesOverlap(testPosition, other)) {
|
|
672
|
+
hasOverlap = true;
|
|
673
|
+
// Move below this overlapping tile
|
|
674
|
+
newY = other.y + other.h;
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (!hasOverlap) {
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (newY < item.y) {
|
|
685
|
+
item.y = newY;
|
|
686
|
+
const tile = this.tiles.get(item.id);
|
|
687
|
+
if (tile) {
|
|
688
|
+
this._applyTilePosition(tile, item);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Create ghost element for drag preview.
|
|
696
|
+
* @param {DOMRect} rect - Original tile rect
|
|
697
|
+
* @private
|
|
698
|
+
*/
|
|
699
|
+
_createGhost(rect) {
|
|
700
|
+
this._ghostElement = document.createElement('div');
|
|
701
|
+
this._ghostElement.className = 'tile-ghost';
|
|
702
|
+
this.gridElement.appendChild(this._ghostElement);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Remove ghost element.
|
|
707
|
+
* @private
|
|
708
|
+
*/
|
|
709
|
+
_removeGhost() {
|
|
710
|
+
if (this._ghostElement) {
|
|
711
|
+
this._ghostElement.remove();
|
|
712
|
+
this._ghostElement = null;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Find next available position for a new tile.
|
|
718
|
+
* @param {number} w - Width in cells
|
|
719
|
+
* @param {number} h - Height in cells
|
|
720
|
+
* @returns {{x: number, y: number}}
|
|
721
|
+
*/
|
|
722
|
+
findNextPosition(w, h) {
|
|
723
|
+
// Build occupancy grid
|
|
724
|
+
const maxY = Math.max(...this.layout.map(l => l.y + l.h), 0);
|
|
725
|
+
const occupied = new Set();
|
|
726
|
+
|
|
727
|
+
this.layout.forEach(l => {
|
|
728
|
+
for (let x = l.x; x < l.x + l.w; x++) {
|
|
729
|
+
for (let y = l.y; y < l.y + l.h; y++) {
|
|
730
|
+
occupied.add(`${x},${y}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// Find first available position
|
|
736
|
+
for (let y = 0; y <= maxY + 1; y++) {
|
|
737
|
+
for (let x = 0; x <= this.columns - w; x++) {
|
|
738
|
+
let fits = true;
|
|
739
|
+
for (let dx = 0; dx < w && fits; dx++) {
|
|
740
|
+
for (let dy = 0; dy < h && fits; dy++) {
|
|
741
|
+
if (occupied.has(`${x + dx},${y + dy}`)) {
|
|
742
|
+
fits = false;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (fits) {
|
|
747
|
+
return { x, y };
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Default: place at bottom
|
|
753
|
+
return { x: 0, y: maxY + 1 };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Handle tile config request.
|
|
758
|
+
* @param {CustomEvent} e
|
|
759
|
+
* @private
|
|
760
|
+
*/
|
|
761
|
+
_onTileConfigRequest(e) {
|
|
762
|
+
const { tileId, tileType, config, schema } = e.detail;
|
|
763
|
+
this.eventBus?.emit?.('grid:config-modal-show', {
|
|
764
|
+
tileId,
|
|
765
|
+
tileType,
|
|
766
|
+
config,
|
|
767
|
+
schema,
|
|
768
|
+
variables: this.data ? this._getVariableList() : [],
|
|
769
|
+
onSave: (newConfig) => {
|
|
770
|
+
const tile = this.tiles.get(tileId);
|
|
771
|
+
if (tile) {
|
|
772
|
+
tile.fullData = this.fullData;
|
|
773
|
+
tile.update(this.data, newConfig);
|
|
774
|
+
// Update layout config
|
|
775
|
+
const layoutItem = this.layout.find(l => l.id === tileId);
|
|
776
|
+
if (layoutItem) {
|
|
777
|
+
layoutItem.config = newConfig;
|
|
778
|
+
}
|
|
779
|
+
this._emitLayoutChanged();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Handle tile remove request.
|
|
787
|
+
* Note: Let the event bubble up to the dashboard which handles confirmation.
|
|
788
|
+
* @param {CustomEvent} e
|
|
789
|
+
* @private
|
|
790
|
+
*/
|
|
791
|
+
_onTileRemoveRequest(e) {
|
|
792
|
+
// Don't auto-remove - let event bubble up to dashboard
|
|
793
|
+
// Dashboard shows a confirm dialog before calling removeTile()
|
|
794
|
+
// Do NOT stop propagation - dashboard listener needs to receive this event
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Get list of available variables from data.
|
|
799
|
+
* @returns {Array<{value: string, label: string}>}
|
|
800
|
+
* @private
|
|
801
|
+
*/
|
|
802
|
+
_getVariableList() {
|
|
803
|
+
if (!this.data) return [];
|
|
804
|
+
|
|
805
|
+
const variables = [];
|
|
806
|
+
if (this.data.stocks) {
|
|
807
|
+
Object.keys(this.data.stocks).forEach(name => {
|
|
808
|
+
variables.push({ value: name, label: name });
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
if (this.data.indicators) {
|
|
812
|
+
Object.keys(this.data.indicators).forEach(name => {
|
|
813
|
+
variables.push({ value: name, label: name });
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
return variables;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Show add widget menu.
|
|
821
|
+
* @private
|
|
822
|
+
*/
|
|
823
|
+
_showAddWidgetMenu() {
|
|
824
|
+
this.eventBus?.emit?.('grid:add-widget-menu-show', {
|
|
825
|
+
onSelect: (type) => {
|
|
826
|
+
const { getWidget } = require('./tile_registry.js');
|
|
827
|
+
const WidgetClass = getWidget(type);
|
|
828
|
+
if (WidgetClass) {
|
|
829
|
+
const size = WidgetClass.DEFAULT_SIZE || { w: 4, h: 3 };
|
|
830
|
+
const pos = this.findNextPosition(size.w, size.h);
|
|
831
|
+
this.addTile(type, { ...pos, ...size });
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Reset to default layout.
|
|
839
|
+
* @private
|
|
840
|
+
*/
|
|
841
|
+
_resetLayout() {
|
|
842
|
+
this.eventBus?.emit?.('grid:reset-layout-confirm', {
|
|
843
|
+
onConfirm: () => {
|
|
844
|
+
this.eventBus?.emit?.('grid:layout-reset-requested');
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Export report.
|
|
851
|
+
* @private
|
|
852
|
+
*/
|
|
853
|
+
_exportReport() {
|
|
854
|
+
this.eventBus?.emit?.('grid:export-requested', {
|
|
855
|
+
gridElement: this.gridElement,
|
|
856
|
+
layout: this.getLayout()
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Notify that a layout mutation is about to begin (drag/resize start).
|
|
862
|
+
* @private
|
|
863
|
+
*/
|
|
864
|
+
_emitBeforeLayoutChange() {
|
|
865
|
+
if (typeof this._onBeforeLayoutChangeCallback === 'function') {
|
|
866
|
+
this._onBeforeLayoutChangeCallback(this.getLayout());
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Emit layout changed event and call the onLayoutChange callback.
|
|
872
|
+
* @private
|
|
873
|
+
*/
|
|
874
|
+
_emitLayoutChanged() {
|
|
875
|
+
const layout = this.getLayout();
|
|
876
|
+
this.eventBus?.emit?.('grid:layout-changed', { layout });
|
|
877
|
+
|
|
878
|
+
// Call the callback if provided (used by ResultsTileDashboard for autosave)
|
|
879
|
+
if (typeof this._onLayoutChangeCallback === 'function') {
|
|
880
|
+
this._onLayoutChangeCallback(layout);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Clean up resources.
|
|
886
|
+
*/
|
|
887
|
+
dispose() {
|
|
888
|
+
document.removeEventListener('pointermove', this._boundOnPointerMove);
|
|
889
|
+
document.removeEventListener('pointerup', this._boundOnPointerUp);
|
|
890
|
+
|
|
891
|
+
this._addWidgetDropdown?.destroy();
|
|
892
|
+
this._addWidgetDropdown = null;
|
|
893
|
+
|
|
894
|
+
this.tiles.forEach(tile => tile.dispose());
|
|
895
|
+
this.tiles.clear();
|
|
896
|
+
this.layout = [];
|
|
897
|
+
|
|
898
|
+
if (this.gridElement) {
|
|
899
|
+
this.gridElement.remove();
|
|
900
|
+
this.gridElement = null;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
export default TileGrid;
|