@dragonworks/ngx-dashboard 20.2.0 → 20.3.1
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.
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { signal, computed, ChangeDetectionStrategy, Component,
|
|
3
|
+
import { signal, computed, ChangeDetectionStrategy, Component, inject, Injectable, InjectionToken, input, model, output, viewChild, ViewContainerRef, DestroyRef, Renderer2, effect, viewChildren, ElementRef, afterNextRender, PLATFORM_ID, untracked } from '@angular/core';
|
|
4
4
|
import { signalStoreFeature, withState, withMethods, patchState, withComputed, signalStore, withProps } from '@ngrx/signals';
|
|
5
|
-
import * as
|
|
5
|
+
import * as i2 from '@angular/material/icon';
|
|
6
6
|
import { MatIconModule } from '@angular/material/icon';
|
|
7
|
-
import * as i2 from '@angular/material/tooltip';
|
|
7
|
+
import * as i2$1 from '@angular/material/tooltip';
|
|
8
8
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
9
|
-
import * as i2$
|
|
9
|
+
import * as i2$2 from '@angular/material/dialog';
|
|
10
10
|
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule, MatDialog } from '@angular/material/dialog';
|
|
11
11
|
import { firstValueFrom } from 'rxjs';
|
|
12
|
-
import * as i1
|
|
12
|
+
import * as i1 from '@angular/forms';
|
|
13
13
|
import { FormsModule } from '@angular/forms';
|
|
14
|
-
import * as
|
|
14
|
+
import * as i4 from '@angular/material/button';
|
|
15
15
|
import { MatButtonModule } from '@angular/material/button';
|
|
16
|
-
import * as i4 from '@angular/material/radio';
|
|
16
|
+
import * as i4$1 from '@angular/material/radio';
|
|
17
17
|
import { MatRadioModule } from '@angular/material/radio';
|
|
18
|
-
import * as i1$
|
|
18
|
+
import * as i1$1 from '@angular/material/menu';
|
|
19
19
|
import { MatMenuTrigger, MatMenuModule } from '@angular/material/menu';
|
|
20
|
-
import * as i3
|
|
20
|
+
import * as i3 from '@angular/material/divider';
|
|
21
21
|
import { MatDividerModule } from '@angular/material/divider';
|
|
22
22
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
23
23
|
|
|
24
|
+
// Auto-generated by scripts/generate-versions.js
|
|
25
|
+
// Do not edit manually
|
|
26
|
+
const NGX_DASHBOARD_VERSION = '20.3.1';
|
|
27
|
+
|
|
24
28
|
/**
|
|
25
29
|
* Maximum number of columns supported by the grid.
|
|
26
30
|
* This determines the encoding scheme for cell coordinates.
|
|
@@ -172,7 +176,7 @@ const WidgetIdUtils = {
|
|
|
172
176
|
*/
|
|
173
177
|
function createEmptyDashboard(dashboardId, rows, columns, gutterSize = '0.5em') {
|
|
174
178
|
return {
|
|
175
|
-
version: '1.
|
|
179
|
+
version: '1.1.0',
|
|
176
180
|
dashboardId,
|
|
177
181
|
rows,
|
|
178
182
|
columns,
|
|
@@ -215,6 +219,8 @@ function createFactoryFromComponent(component) {
|
|
|
215
219
|
};
|
|
216
220
|
}
|
|
217
221
|
|
|
222
|
+
// widget-shared-state-provider.ts
|
|
223
|
+
|
|
218
224
|
class UnknownWidgetComponent {
|
|
219
225
|
static metadata = {
|
|
220
226
|
widgetTypeid: '__internal/unknown-widget',
|
|
@@ -239,7 +245,7 @@ class UnknownWidgetComponent {
|
|
|
239
245
|
<div class="unknown-widget-container" [matTooltip]="tooltipText()">
|
|
240
246
|
<mat-icon class="unknown-widget-icon">error_outline</mat-icon>
|
|
241
247
|
</div>
|
|
242
|
-
`, isInline: true, styles: [".unknown-widget-container{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background-color:var(--mat-sys-error);border-radius:8px;container-type:size}.unknown-widget-icon{color:var(--mat-sys-on-error);font-size:clamp(12px,75cqmin,68px);width:clamp(12px,75cqmin,68px);height:clamp(12px,75cqmin,68px)}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type:
|
|
248
|
+
`, isInline: true, styles: [".unknown-widget-container{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background-color:var(--mat-sys-error);border-radius:8px;container-type:size}.unknown-widget-icon{color:var(--mat-sys-on-error);font-size:clamp(12px,75cqmin,68px);width:clamp(12px,75cqmin,68px);height:clamp(12px,75cqmin,68px)}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
243
249
|
}
|
|
244
250
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: UnknownWidgetComponent, decorators: [{
|
|
245
251
|
type: Component,
|
|
@@ -254,15 +260,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
|
|
|
254
260
|
class DashboardService {
|
|
255
261
|
#widgetTypes = signal([], ...(ngDevMode ? [{ debugName: "#widgetTypes" }] : []));
|
|
256
262
|
#widgetFactoryMap = new Map();
|
|
263
|
+
#sharedStateProviders = new Map();
|
|
257
264
|
#unknownWidgetFactory = createFactoryFromComponent(UnknownWidgetComponent);
|
|
258
265
|
widgetTypes = this.#widgetTypes.asReadonly(); // make the widget list available as a readonly signal
|
|
259
|
-
registerWidgetType(widget) {
|
|
260
|
-
|
|
261
|
-
|
|
266
|
+
registerWidgetType(widget, sharedStateProvider) {
|
|
267
|
+
const widgetTypeid = widget.metadata.widgetTypeid;
|
|
268
|
+
if (this.#widgetTypes().some((w) => w.metadata.widgetTypeid === widgetTypeid)) {
|
|
269
|
+
throw new Error(`Widget type '${widgetTypeid}' is already registered`);
|
|
270
|
+
}
|
|
271
|
+
// Register widget factory
|
|
272
|
+
this.#widgetFactoryMap.set(widgetTypeid, createFactoryFromComponent(widget));
|
|
273
|
+
// Register shared state provider if provided
|
|
274
|
+
if (sharedStateProvider) {
|
|
275
|
+
const provider = this.#resolveProvider(sharedStateProvider);
|
|
276
|
+
this.#sharedStateProviders.set(widgetTypeid, provider);
|
|
262
277
|
}
|
|
263
|
-
this.#widgetFactoryMap.set(widget.metadata.widgetTypeid, createFactoryFromComponent(widget));
|
|
264
278
|
this.#widgetTypes.set([...this.#widgetTypes(), widget]);
|
|
265
279
|
}
|
|
280
|
+
#resolveProvider(provider) {
|
|
281
|
+
// If it's a class/service, inject it
|
|
282
|
+
if (typeof provider === 'function') {
|
|
283
|
+
return inject(provider);
|
|
284
|
+
}
|
|
285
|
+
return provider;
|
|
286
|
+
}
|
|
266
287
|
getFactory(widgetTypeid) {
|
|
267
288
|
const factory = this.#widgetFactoryMap.get(widgetTypeid);
|
|
268
289
|
if (factory) {
|
|
@@ -282,6 +303,49 @@ class DashboardService {
|
|
|
282
303
|
},
|
|
283
304
|
};
|
|
284
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Get the shared state provider for a specific widget type.
|
|
308
|
+
*
|
|
309
|
+
* @param widgetTypeid The widget type identifier
|
|
310
|
+
* @returns The shared state provider, or undefined if none is registered
|
|
311
|
+
*/
|
|
312
|
+
getSharedStateProvider(widgetTypeid) {
|
|
313
|
+
return this.#sharedStateProviders.get(widgetTypeid);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Collect shared states for all widget types currently on the dashboard.
|
|
317
|
+
* Called during dashboard export/serialization.
|
|
318
|
+
*
|
|
319
|
+
* @param activeWidgetTypes Set of widget type IDs that are currently in use
|
|
320
|
+
* @returns Map of widget type IDs to their shared states
|
|
321
|
+
*/
|
|
322
|
+
collectSharedStates(activeWidgetTypes) {
|
|
323
|
+
const sharedStates = new Map();
|
|
324
|
+
for (const widgetTypeid of activeWidgetTypes) {
|
|
325
|
+
const provider = this.#sharedStateProviders.get(widgetTypeid);
|
|
326
|
+
if (provider) {
|
|
327
|
+
const state = provider.getSharedState();
|
|
328
|
+
if (state !== undefined) {
|
|
329
|
+
sharedStates.set(widgetTypeid, state);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return sharedStates;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Restore shared states for widget types.
|
|
337
|
+
* Called during dashboard import/deserialization, before widget instances are created.
|
|
338
|
+
*
|
|
339
|
+
* @param states Map of widget type IDs to their shared states
|
|
340
|
+
*/
|
|
341
|
+
restoreSharedStates(states) {
|
|
342
|
+
for (const [widgetTypeid, state] of states) {
|
|
343
|
+
const provider = this.#sharedStateProviders.get(widgetTypeid);
|
|
344
|
+
if (provider) {
|
|
345
|
+
provider.setSharedState(state);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
285
349
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
286
350
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardService, providedIn: 'root' });
|
|
287
351
|
}
|
|
@@ -366,6 +430,116 @@ function calculateHighlightedZones(dragData, hovered) {
|
|
|
366
430
|
return zones;
|
|
367
431
|
}
|
|
368
432
|
|
|
433
|
+
/**
|
|
434
|
+
* Calculate the minimal bounding box for a set of cells.
|
|
435
|
+
* Returns the tightest rectangle that contains all the widgets.
|
|
436
|
+
*
|
|
437
|
+
* @param cells - The cells to calculate bounds for
|
|
438
|
+
* @returns The minimal bounding box, or null if no cells
|
|
439
|
+
*/
|
|
440
|
+
function calculateMinimalBoundingBox(cells) {
|
|
441
|
+
if (cells.length === 0) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
let minRow = Infinity;
|
|
445
|
+
let maxRow = -Infinity;
|
|
446
|
+
let minCol = Infinity;
|
|
447
|
+
let maxCol = -Infinity;
|
|
448
|
+
for (const cell of cells) {
|
|
449
|
+
const cellEndRow = cell.row + cell.rowSpan - 1;
|
|
450
|
+
const cellEndCol = cell.col + cell.colSpan - 1;
|
|
451
|
+
minRow = Math.min(minRow, cell.row);
|
|
452
|
+
maxRow = Math.max(maxRow, cellEndRow);
|
|
453
|
+
minCol = Math.min(minCol, cell.col);
|
|
454
|
+
maxCol = Math.max(maxCol, cellEndCol);
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
minRow,
|
|
458
|
+
maxRow,
|
|
459
|
+
minCol,
|
|
460
|
+
maxCol,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Apply selection filtering to extract cells within specified bounds.
|
|
465
|
+
* Calculates new grid dimensions and coordinate offsets for export.
|
|
466
|
+
*
|
|
467
|
+
* @param selection - The grid selection to filter by
|
|
468
|
+
* @param allCells - All cells in the dashboard
|
|
469
|
+
* @param options - Optional filtering options
|
|
470
|
+
* @returns Filtered cells with export parameters
|
|
471
|
+
*/
|
|
472
|
+
function applySelectionFilter(selection, allCells, options = {}) {
|
|
473
|
+
// Filter widgets that are completely within the selection bounds
|
|
474
|
+
const filteredCells = allCells.filter((cell) => {
|
|
475
|
+
const cellEndRow = cell.row + cell.rowSpan - 1;
|
|
476
|
+
const cellEndCol = cell.col + cell.colSpan - 1;
|
|
477
|
+
// Widget must be completely within the selection
|
|
478
|
+
return cell.row >= selection.topLeft.row &&
|
|
479
|
+
cell.col >= selection.topLeft.col &&
|
|
480
|
+
cellEndRow <= selection.bottomRight.row &&
|
|
481
|
+
cellEndCol <= selection.bottomRight.col;
|
|
482
|
+
});
|
|
483
|
+
// Determine the actual bounds to use
|
|
484
|
+
let actualMinRow;
|
|
485
|
+
let actualMaxRow;
|
|
486
|
+
let actualMinCol;
|
|
487
|
+
let actualMaxCol;
|
|
488
|
+
if (options.useMinimalBounds && filteredCells.length > 0) {
|
|
489
|
+
// Calculate minimal bounding box of actual widgets
|
|
490
|
+
const bounds = calculateMinimalBoundingBox(filteredCells);
|
|
491
|
+
if (bounds) {
|
|
492
|
+
actualMinRow = bounds.minRow;
|
|
493
|
+
actualMaxRow = bounds.maxRow;
|
|
494
|
+
actualMinCol = bounds.minCol;
|
|
495
|
+
actualMaxCol = bounds.maxCol;
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
// Fallback to selection bounds (shouldn't happen if filteredCells.length > 0)
|
|
499
|
+
actualMinRow = selection.topLeft.row;
|
|
500
|
+
actualMaxRow = selection.bottomRight.row;
|
|
501
|
+
actualMinCol = selection.topLeft.col;
|
|
502
|
+
actualMaxCol = selection.bottomRight.col;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
// Use selection bounds as-is
|
|
507
|
+
actualMinRow = selection.topLeft.row;
|
|
508
|
+
actualMaxRow = selection.bottomRight.row;
|
|
509
|
+
actualMinCol = selection.topLeft.col;
|
|
510
|
+
actualMaxCol = selection.bottomRight.col;
|
|
511
|
+
}
|
|
512
|
+
// Apply padding if specified (after minimal bounds calculation)
|
|
513
|
+
const padding = options.padding ?? 0;
|
|
514
|
+
let desiredMinRow = actualMinRow;
|
|
515
|
+
let desiredMinCol = actualMinCol;
|
|
516
|
+
if (padding > 0) {
|
|
517
|
+
// Calculate desired bounds (may go below 1)
|
|
518
|
+
desiredMinRow = actualMinRow - padding;
|
|
519
|
+
const desiredMaxRow = actualMaxRow + padding;
|
|
520
|
+
desiredMinCol = actualMinCol - padding;
|
|
521
|
+
const desiredMaxCol = actualMaxCol + padding;
|
|
522
|
+
// Clamp to grid minimum (1-indexed)
|
|
523
|
+
actualMinRow = Math.max(1, desiredMinRow);
|
|
524
|
+
actualMaxRow = desiredMaxRow;
|
|
525
|
+
actualMinCol = Math.max(1, desiredMinCol);
|
|
526
|
+
actualMaxCol = desiredMaxCol;
|
|
527
|
+
}
|
|
528
|
+
// Calculate new grid dimensions from desired bounds (includes full padding)
|
|
529
|
+
const exportRows = actualMaxRow - desiredMinRow + 1;
|
|
530
|
+
const exportColumns = actualMaxCol - desiredMinCol + 1;
|
|
531
|
+
// Calculate offsets for coordinate transformation (from desired, not clamped)
|
|
532
|
+
const rowOffset = desiredMinRow - 1;
|
|
533
|
+
const colOffset = desiredMinCol - 1;
|
|
534
|
+
return {
|
|
535
|
+
cells: filteredCells,
|
|
536
|
+
rows: exportRows,
|
|
537
|
+
columns: exportColumns,
|
|
538
|
+
rowOffset,
|
|
539
|
+
colOffset,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
369
543
|
const initialGridConfigState = {
|
|
370
544
|
rows: 8,
|
|
371
545
|
columns: 16,
|
|
@@ -777,31 +951,60 @@ withMethods((store) => ({
|
|
|
777
951
|
store._endResize(apply, {
|
|
778
952
|
updateWidgetSpan: (cellId, rowSpan, colSpan) => {
|
|
779
953
|
// Adapter: find widget by cellId and update using widgetId
|
|
780
|
-
const widget = store
|
|
954
|
+
const widget = store
|
|
955
|
+
.cells()
|
|
956
|
+
.find((c) => CellIdUtils.equals(c.cellId, cellId));
|
|
781
957
|
if (widget) {
|
|
782
958
|
store.updateWidgetSpan(widget.widgetId, rowSpan, colSpan);
|
|
783
959
|
}
|
|
784
|
-
}
|
|
960
|
+
},
|
|
785
961
|
});
|
|
786
962
|
},
|
|
787
963
|
// EXPORT/IMPORT METHODS (need access to multiple features)
|
|
788
|
-
exportDashboard(getCurrentWidgetStates) {
|
|
964
|
+
exportDashboard(getCurrentWidgetStates, selection, selectionOptions) {
|
|
789
965
|
// Get live widget states if callback provided, otherwise use stored states
|
|
790
966
|
const liveWidgetStates = getCurrentWidgetStates?.() || new Map();
|
|
967
|
+
// Determine which widgets to export and grid dimensions
|
|
968
|
+
let widgetsToExport = store.cells();
|
|
969
|
+
let exportRows = store.rows();
|
|
970
|
+
let exportColumns = store.columns();
|
|
971
|
+
let rowOffset = 0;
|
|
972
|
+
let colOffset = 0;
|
|
973
|
+
// Apply selection filtering if specified
|
|
974
|
+
if (selection) {
|
|
975
|
+
const selectionResult = applySelectionFilter(selection, store.cells(), selectionOptions);
|
|
976
|
+
widgetsToExport = selectionResult.cells;
|
|
977
|
+
exportRows = selectionResult.rows;
|
|
978
|
+
exportColumns = selectionResult.columns;
|
|
979
|
+
rowOffset = selectionResult.rowOffset;
|
|
980
|
+
colOffset = selectionResult.colOffset;
|
|
981
|
+
}
|
|
982
|
+
// Collect widget types in use for shared state collection
|
|
983
|
+
const activeWidgetTypes = new Set(widgetsToExport
|
|
984
|
+
.filter((cell) => cell.widgetFactory.widgetTypeid !== '__internal/unknown-widget')
|
|
985
|
+
.map((cell) => cell.widgetFactory.widgetTypeid));
|
|
986
|
+
// Collect shared states from DashboardService
|
|
987
|
+
const sharedStatesMap = store.dashboardService.collectSharedStates(activeWidgetTypes);
|
|
988
|
+
const sharedStates = sharedStatesMap.size > 0
|
|
989
|
+
? Object.fromEntries(sharedStatesMap)
|
|
990
|
+
: undefined;
|
|
791
991
|
return {
|
|
792
|
-
version: '1.
|
|
992
|
+
version: '1.1.0',
|
|
793
993
|
dashboardId: store.dashboardId(),
|
|
794
|
-
rows:
|
|
795
|
-
columns:
|
|
994
|
+
rows: exportRows,
|
|
995
|
+
columns: exportColumns,
|
|
796
996
|
gutterSize: store.gutterSize(),
|
|
797
|
-
cells:
|
|
997
|
+
cells: widgetsToExport
|
|
798
998
|
.filter((cell) => cell.widgetFactory.widgetTypeid !== '__internal/unknown-widget')
|
|
799
999
|
.map((cell) => {
|
|
800
1000
|
const cellIdString = CellIdUtils.toString(cell.cellId);
|
|
801
1001
|
const currentState = liveWidgetStates.get(cellIdString);
|
|
1002
|
+
// Transform coordinates if selection is specified
|
|
1003
|
+
const exportRow = selection ? cell.row - rowOffset : cell.row;
|
|
1004
|
+
const exportCol = selection ? cell.col - colOffset : cell.col;
|
|
802
1005
|
return {
|
|
803
|
-
row:
|
|
804
|
-
col:
|
|
1006
|
+
row: exportRow,
|
|
1007
|
+
col: exportCol,
|
|
805
1008
|
rowSpan: cell.rowSpan,
|
|
806
1009
|
colSpan: cell.colSpan,
|
|
807
1010
|
flat: cell.flat,
|
|
@@ -809,9 +1012,15 @@ withMethods((store) => ({
|
|
|
809
1012
|
widgetState: currentState !== undefined ? currentState : cell.widgetState,
|
|
810
1013
|
};
|
|
811
1014
|
}),
|
|
1015
|
+
...(sharedStates && { sharedStates }),
|
|
812
1016
|
};
|
|
813
1017
|
},
|
|
814
1018
|
loadDashboard(data) {
|
|
1019
|
+
// Restore shared states FIRST, before creating widget instances
|
|
1020
|
+
if (data.sharedStates) {
|
|
1021
|
+
const statesMap = new Map(Object.entries(data.sharedStates));
|
|
1022
|
+
store.dashboardService.restoreSharedStates(statesMap);
|
|
1023
|
+
}
|
|
815
1024
|
// Import full dashboard data with grid configuration
|
|
816
1025
|
const widgetsById = {};
|
|
817
1026
|
data.cells.forEach((cellData) => {
|
|
@@ -838,34 +1047,6 @@ withMethods((store) => ({
|
|
|
838
1047
|
widgetsById,
|
|
839
1048
|
});
|
|
840
1049
|
},
|
|
841
|
-
// INITIALIZATION METHODS
|
|
842
|
-
initializeFromDto(dashboardData) {
|
|
843
|
-
// Inline the loadDashboard logic since it's defined later in the same methods block
|
|
844
|
-
const widgetsById = {};
|
|
845
|
-
dashboardData.cells.forEach((cellData) => {
|
|
846
|
-
const factory = store.dashboardService.getFactory(cellData.widgetTypeid);
|
|
847
|
-
const widgetId = WidgetIdUtils.generate();
|
|
848
|
-
const cell = {
|
|
849
|
-
widgetId,
|
|
850
|
-
cellId: CellIdUtils.create(cellData.row, cellData.col),
|
|
851
|
-
row: cellData.row,
|
|
852
|
-
col: cellData.col,
|
|
853
|
-
rowSpan: cellData.rowSpan,
|
|
854
|
-
colSpan: cellData.colSpan,
|
|
855
|
-
flat: cellData.flat,
|
|
856
|
-
widgetFactory: factory,
|
|
857
|
-
widgetState: cellData.widgetState,
|
|
858
|
-
};
|
|
859
|
-
widgetsById[WidgetIdUtils.toString(widgetId)] = cell;
|
|
860
|
-
});
|
|
861
|
-
patchState(store, {
|
|
862
|
-
dashboardId: dashboardData.dashboardId,
|
|
863
|
-
rows: dashboardData.rows,
|
|
864
|
-
columns: dashboardData.columns,
|
|
865
|
-
gutterSize: dashboardData.gutterSize,
|
|
866
|
-
widgetsById,
|
|
867
|
-
});
|
|
868
|
-
},
|
|
869
1050
|
})),
|
|
870
1051
|
// Cross-feature computed properties that depend on resize + widget data (using utility functions)
|
|
871
1052
|
withComputed((store) => ({
|
|
@@ -973,7 +1154,7 @@ class CellSettingsDialogComponent {
|
|
|
973
1154
|
Apply
|
|
974
1155
|
</button>
|
|
975
1156
|
</mat-dialog-actions>
|
|
976
|
-
`, isInline: true, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden;padding-top:.5rem}.cell-info{margin:0 0 1.5rem;padding-bottom:1rem}.radio-group{width:100%}mat-radio-group{display:block}mat-radio-button{width:100%;display:block;margin-bottom:1rem}mat-radio-button:last-child{margin-bottom:0}.radio-option{margin-left:.75rem;padding:.25rem 0}.option-title{display:block;margin-bottom:.25rem}.option-description{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1
|
|
1157
|
+
`, isInline: true, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden;padding-top:.5rem}.cell-info{margin:0 0 1.5rem;padding-bottom:1rem}.radio-group{width:100%}mat-radio-group{display:block}mat-radio-button{width:100%;display:block;margin-bottom:1rem}mat-radio-button:last-child{margin-bottom:0}.radio-option{margin-left:.75rem;padding:.25rem 0}.option-title{display:block;margin-bottom:.25rem}.option-description{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i2$2.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i2$2.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i2$2.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatRadioModule }, { kind: "directive", type: i4$1.MatRadioGroup, selector: "mat-radio-group", inputs: ["color", "name", "labelPosition", "value", "selected", "disabled", "required", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioGroup"] }, { kind: "component", type: i4$1.MatRadioButton, selector: "mat-radio-button", inputs: ["id", "name", "aria-label", "aria-labelledby", "aria-describedby", "disableRipple", "tabIndex", "checked", "value", "labelPosition", "disabled", "required", "color", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioButton"] }] });
|
|
977
1158
|
}
|
|
978
1159
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellSettingsDialogComponent, decorators: [{
|
|
979
1160
|
type: Component,
|
|
@@ -1392,13 +1573,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
|
|
|
1392
1573
|
|
|
1393
1574
|
class DashboardViewerComponent {
|
|
1394
1575
|
#store = inject(DashboardStore);
|
|
1576
|
+
#renderer = inject(Renderer2);
|
|
1577
|
+
#destroyRef = inject(DestroyRef);
|
|
1395
1578
|
cellComponents = viewChildren(CellComponent, ...(ngDevMode ? [{ debugName: "cellComponents" }] : []));
|
|
1579
|
+
gridElement = viewChild('gridElement', ...(ngDevMode ? [{ debugName: "gridElement" }] : []));
|
|
1396
1580
|
rows = input.required(...(ngDevMode ? [{ debugName: "rows" }] : []));
|
|
1397
1581
|
columns = input.required(...(ngDevMode ? [{ debugName: "columns" }] : []));
|
|
1398
1582
|
gutterSize = input('1em', ...(ngDevMode ? [{ debugName: "gutterSize" }] : []));
|
|
1399
1583
|
gutters = computed(() => this.columns() + 1, ...(ngDevMode ? [{ debugName: "gutters" }] : []));
|
|
1584
|
+
// Selection feature
|
|
1585
|
+
enableSelection = input(false, ...(ngDevMode ? [{ debugName: "enableSelection" }] : []));
|
|
1586
|
+
selectionComplete = output();
|
|
1400
1587
|
// store signals - read-only
|
|
1401
1588
|
cells = this.#store.cells;
|
|
1589
|
+
// Selection state
|
|
1590
|
+
isSelecting = signal(false, ...(ngDevMode ? [{ debugName: "isSelecting" }] : []));
|
|
1591
|
+
selectionStart = signal(null, ...(ngDevMode ? [{ debugName: "selectionStart" }] : []));
|
|
1592
|
+
selectionCurrent = signal(null, ...(ngDevMode ? [{ debugName: "selectionCurrent" }] : []));
|
|
1593
|
+
// Computed selection bounds (normalized)
|
|
1594
|
+
selectionBounds = computed(() => {
|
|
1595
|
+
const start = this.selectionStart();
|
|
1596
|
+
const current = this.selectionCurrent();
|
|
1597
|
+
if (!start || !current)
|
|
1598
|
+
return null;
|
|
1599
|
+
return {
|
|
1600
|
+
startRow: Math.min(start.row, current.row),
|
|
1601
|
+
endRow: Math.max(start.row, current.row),
|
|
1602
|
+
startCol: Math.min(start.col, current.col),
|
|
1603
|
+
endCol: Math.max(start.col, current.col),
|
|
1604
|
+
};
|
|
1605
|
+
}, ...(ngDevMode ? [{ debugName: "selectionBounds" }] : []));
|
|
1606
|
+
// Generate array for template iteration
|
|
1607
|
+
rowNumbers = computed(() => Array.from({ length: this.rows() }, (_, i) => i + 1), ...(ngDevMode ? [{ debugName: "rowNumbers" }] : []));
|
|
1608
|
+
colNumbers = computed(() => Array.from({ length: this.columns() }, (_, i) => i + 1), ...(ngDevMode ? [{ debugName: "colNumbers" }] : []));
|
|
1609
|
+
// Document-level event listeners (cleanup needed)
|
|
1610
|
+
#mouseMoveListener;
|
|
1611
|
+
#mouseUpListener;
|
|
1402
1612
|
constructor() {
|
|
1403
1613
|
// Sync grid configuration with store when inputs change
|
|
1404
1614
|
effect(() => {
|
|
@@ -1408,32 +1618,101 @@ class DashboardViewerComponent {
|
|
|
1408
1618
|
gutterSize: this.gutterSize(),
|
|
1409
1619
|
});
|
|
1410
1620
|
});
|
|
1621
|
+
// Clear selection when selection mode is disabled
|
|
1622
|
+
effect(() => {
|
|
1623
|
+
if (!this.enableSelection()) {
|
|
1624
|
+
this.selectionStart.set(null);
|
|
1625
|
+
this.selectionCurrent.set(null);
|
|
1626
|
+
this.isSelecting.set(false);
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1411
1629
|
}
|
|
1630
|
+
// Selection methods
|
|
1412
1631
|
/**
|
|
1413
|
-
*
|
|
1414
|
-
* Used during dashboard export to get live widget states.
|
|
1632
|
+
* Check if a cell is within the current selection bounds
|
|
1415
1633
|
*/
|
|
1416
|
-
|
|
1417
|
-
const
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1634
|
+
isCellSelected(row, col) {
|
|
1635
|
+
const bounds = this.selectionBounds();
|
|
1636
|
+
if (!bounds)
|
|
1637
|
+
return false;
|
|
1638
|
+
return (row >= bounds.startRow &&
|
|
1639
|
+
row <= bounds.endRow &&
|
|
1640
|
+
col >= bounds.startCol &&
|
|
1641
|
+
col <= bounds.endCol);
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* Handle mouse down on ghost cell to start selection
|
|
1645
|
+
*/
|
|
1646
|
+
onGhostCellMouseDown(event, row, col) {
|
|
1647
|
+
if (!this.enableSelection())
|
|
1648
|
+
return;
|
|
1649
|
+
if (event.button !== 0)
|
|
1650
|
+
return; // Only left button
|
|
1651
|
+
event.preventDefault();
|
|
1652
|
+
event.stopPropagation();
|
|
1653
|
+
this.isSelecting.set(true);
|
|
1654
|
+
this.selectionStart.set({ row, col });
|
|
1655
|
+
this.selectionCurrent.set({ row, col });
|
|
1656
|
+
// Add document-level listeners for drag
|
|
1657
|
+
this.#mouseMoveListener = this.#renderer.listen('document', 'mousemove', () => this.onDocumentMouseMove());
|
|
1658
|
+
this.#mouseUpListener = this.#renderer.listen('document', 'mouseup', () => this.onDocumentMouseUp());
|
|
1659
|
+
// Register cleanup
|
|
1660
|
+
this.#destroyRef.onDestroy(() => {
|
|
1661
|
+
this.cleanupListeners();
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Handle mouse enter on ghost cell during selection
|
|
1666
|
+
*/
|
|
1667
|
+
onGhostCellMouseEnter(row, col) {
|
|
1668
|
+
if (!this.isSelecting())
|
|
1669
|
+
return;
|
|
1670
|
+
this.selectionCurrent.set({ row, col });
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Handle document mouse move during selection
|
|
1674
|
+
*/
|
|
1675
|
+
onDocumentMouseMove() {
|
|
1676
|
+
if (!this.isSelecting())
|
|
1677
|
+
return;
|
|
1678
|
+
// The actual selection update is handled by onGhostCellMouseEnter
|
|
1679
|
+
// This just ensures we capture the event
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Handle document mouse up to complete selection
|
|
1683
|
+
*/
|
|
1684
|
+
onDocumentMouseUp() {
|
|
1685
|
+
if (!this.isSelecting())
|
|
1686
|
+
return;
|
|
1687
|
+
this.isSelecting.set(false);
|
|
1688
|
+
// Emit selection event
|
|
1689
|
+
const bounds = this.selectionBounds();
|
|
1690
|
+
if (bounds) {
|
|
1691
|
+
this.selectionComplete.emit({
|
|
1692
|
+
topLeft: { row: bounds.startRow, col: bounds.startCol },
|
|
1693
|
+
bottomRight: { row: bounds.endRow, col: bounds.endCol },
|
|
1694
|
+
});
|
|
1425
1695
|
}
|
|
1426
|
-
|
|
1696
|
+
// Clean up listeners
|
|
1697
|
+
this.cleanupListeners();
|
|
1698
|
+
// Don't clear selection - let the parent control when to clear
|
|
1699
|
+
// Selection remains visible until enableSelection becomes false
|
|
1427
1700
|
}
|
|
1428
1701
|
/**
|
|
1429
|
-
*
|
|
1430
|
-
* This ensures the most up-to-date widget states are captured.
|
|
1702
|
+
* Clean up document-level event listeners
|
|
1431
1703
|
*/
|
|
1432
|
-
|
|
1433
|
-
|
|
1704
|
+
cleanupListeners() {
|
|
1705
|
+
if (this.#mouseMoveListener) {
|
|
1706
|
+
this.#mouseMoveListener();
|
|
1707
|
+
this.#mouseMoveListener = undefined;
|
|
1708
|
+
}
|
|
1709
|
+
if (this.#mouseUpListener) {
|
|
1710
|
+
this.#mouseUpListener();
|
|
1711
|
+
this.#mouseUpListener = undefined;
|
|
1712
|
+
}
|
|
1434
1713
|
}
|
|
1435
1714
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1436
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DashboardViewerComponent, isStandalone: true, selector: "ngx-dashboard-viewer", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, gutterSize: { classPropertyName: "gutterSize", publicName: "gutterSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.--rows": "rows()", "style.--columns": "columns()", "style.--gutter-size": "gutterSize()", "style.--gutters": "gutters()" } }, viewQueries: [{ propertyName: "cellComponents", predicate: CellComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Dashboard viewer - read-only grid -->\r\n<div class=\"grid top-grid\">\r\n @for (cell of cells(); track cell.widgetId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [widgetId]=\"cell.widgetId\"\r\n [cellId]=\"cell.cellId\"\r\n [isEditMode]=\"false\"\r\n [draggable]=\"false\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n >\r\n </lib-cell>\r\n }\r\n</div>\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto;background-color:var(--mat-sys-surface-container)}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);width:100%;height:100%;box-sizing:border-box;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}.grid-cell{pointer-events:none}.grid-cell:not(.flat){pointer-events:auto;cursor:default}.grid-cell:not(.flat) .content-area{pointer-events:none}.top-grid{z-index:2;pointer-events:none}\n"], dependencies: [{ kind: "component", type: CellComponent, selector: "lib-cell", inputs: ["widgetId", "cellId", "widgetFactory", "widgetState", "isEditMode", "flat", "row", "column", "rowSpan", "colSpan", "draggable"], outputs: ["rowChange", "columnChange", "dragStart", "dragEnd", "edit", "delete", "settings", "resizeStart", "resizeMove", "resizeEnd"] }, { kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1715
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DashboardViewerComponent, isStandalone: true, selector: "ngx-dashboard-viewer", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, gutterSize: { classPropertyName: "gutterSize", publicName: "gutterSize", isSignal: true, isRequired: false, transformFunction: null }, enableSelection: { classPropertyName: "enableSelection", publicName: "enableSelection", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionComplete: "selectionComplete" }, host: { properties: { "style.--rows": "rows()", "style.--columns": "columns()", "style.--gutter-size": "gutterSize()", "style.--gutters": "gutters()" } }, viewQueries: [{ propertyName: "cellComponents", predicate: CellComponent, descendants: true, isSignal: true }, { propertyName: "gridElement", first: true, predicate: ["gridElement"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Dashboard viewer - read-only grid -->\r\n<div class=\"grid top-grid\" #gridElement>\r\n @for (cell of cells(); track cell.widgetId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [widgetId]=\"cell.widgetId\"\r\n [cellId]=\"cell.cellId\"\r\n [isEditMode]=\"false\"\r\n [draggable]=\"false\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n >\r\n </lib-cell>\r\n }\r\n</div>\r\n\r\n<!-- Selection overlay grid - mirror of main grid for cell selection -->\r\n@if (enableSelection()) {\r\n <div class=\"selection-overlay-grid\">\r\n @for (row of rowNumbers(); track row) {\r\n @for (col of colNumbers(); track col) {\r\n <div\r\n class=\"selection-ghost-cell\"\r\n [class.selected]=\"isCellSelected(row, col)\"\r\n [class.selecting]=\"isSelecting()\"\r\n [style.grid-row]=\"row\"\r\n [style.grid-column]=\"col\"\r\n (mousedown)=\"onGhostCellMouseDown($event, row, col)\"\r\n (mouseenter)=\"onGhostCellMouseEnter(row, col)\"\r\n ></div>\r\n }\r\n }\r\n </div>\r\n}\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto;position:relative;background-color:var(--mat-sys-surface-container)}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);width:100%;height:100%;box-sizing:border-box;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}.grid-cell{pointer-events:none}.grid-cell:not(.flat){pointer-events:auto;cursor:default}.grid-cell:not(.flat) .content-area{pointer-events:none}.top-grid{z-index:2;pointer-events:none}.selection-overlay-grid{position:absolute;top:0;left:0;width:100%;height:100%;display:grid;gap:var(--gutter-size);padding:var(--gutter-size);grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size));z-index:5;pointer-events:auto;-webkit-user-select:none;user-select:none}.selection-ghost-cell{cursor:crosshair;transition:background-color .1s ease,border-radius .1s ease;border-radius:2px}.selection-ghost-cell:hover:not(.selecting){background-color:var(--mat-sys-primary);opacity:.08}.selection-ghost-cell.selected{background-color:var(--mat-sys-primary);opacity:.25;border-radius:4px}.selection-ghost-cell.selecting{cursor:crosshair}\n"], dependencies: [{ kind: "component", type: CellComponent, selector: "lib-cell", inputs: ["widgetId", "cellId", "widgetFactory", "widgetState", "isEditMode", "flat", "row", "column", "rowSpan", "colSpan", "draggable"], outputs: ["rowChange", "columnChange", "dragStart", "dragEnd", "edit", "delete", "settings", "resizeStart", "resizeMove", "resizeEnd"] }, { kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1437
1716
|
}
|
|
1438
1717
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardViewerComponent, decorators: [{
|
|
1439
1718
|
type: Component,
|
|
@@ -1442,12 +1721,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
|
|
|
1442
1721
|
'[style.--columns]': 'columns()',
|
|
1443
1722
|
'[style.--gutter-size]': 'gutterSize()',
|
|
1444
1723
|
'[style.--gutters]': 'gutters()',
|
|
1445
|
-
}, template: "<!-- Dashboard viewer - read-only grid -->\r\n<div class=\"grid top-grid\">\r\n @for (cell of cells(); track cell.widgetId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [widgetId]=\"cell.widgetId\"\r\n [cellId]=\"cell.cellId\"\r\n [isEditMode]=\"false\"\r\n [draggable]=\"false\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n >\r\n </lib-cell>\r\n }\r\n</div>\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto;background-color:var(--mat-sys-surface-container)}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);width:100%;height:100%;box-sizing:border-box;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}.grid-cell{pointer-events:none}.grid-cell:not(.flat){pointer-events:auto;cursor:default}.grid-cell:not(.flat) .content-area{pointer-events:none}.top-grid{z-index:2;pointer-events:none}\n"] }]
|
|
1724
|
+
}, template: "<!-- Dashboard viewer - read-only grid -->\r\n<div class=\"grid top-grid\" #gridElement>\r\n @for (cell of cells(); track cell.widgetId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [widgetId]=\"cell.widgetId\"\r\n [cellId]=\"cell.cellId\"\r\n [isEditMode]=\"false\"\r\n [draggable]=\"false\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n >\r\n </lib-cell>\r\n }\r\n</div>\r\n\r\n<!-- Selection overlay grid - mirror of main grid for cell selection -->\r\n@if (enableSelection()) {\r\n <div class=\"selection-overlay-grid\">\r\n @for (row of rowNumbers(); track row) {\r\n @for (col of colNumbers(); track col) {\r\n <div\r\n class=\"selection-ghost-cell\"\r\n [class.selected]=\"isCellSelected(row, col)\"\r\n [class.selecting]=\"isSelecting()\"\r\n [style.grid-row]=\"row\"\r\n [style.grid-column]=\"col\"\r\n (mousedown)=\"onGhostCellMouseDown($event, row, col)\"\r\n (mouseenter)=\"onGhostCellMouseEnter(row, col)\"\r\n ></div>\r\n }\r\n }\r\n </div>\r\n}\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto;position:relative;background-color:var(--mat-sys-surface-container)}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);width:100%;height:100%;box-sizing:border-box;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}.grid-cell{pointer-events:none}.grid-cell:not(.flat){pointer-events:auto;cursor:default}.grid-cell:not(.flat) .content-area{pointer-events:none}.top-grid{z-index:2;pointer-events:none}.selection-overlay-grid{position:absolute;top:0;left:0;width:100%;height:100%;display:grid;gap:var(--gutter-size);padding:var(--gutter-size);grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size));z-index:5;pointer-events:auto;-webkit-user-select:none;user-select:none}.selection-ghost-cell{cursor:crosshair;transition:background-color .1s ease,border-radius .1s ease;border-radius:2px}.selection-ghost-cell:hover:not(.selecting){background-color:var(--mat-sys-primary);opacity:.08}.selection-ghost-cell.selected{background-color:var(--mat-sys-primary);opacity:.25;border-radius:4px}.selection-ghost-cell.selecting{cursor:crosshair}\n"] }]
|
|
1446
1725
|
}], ctorParameters: () => [] });
|
|
1447
1726
|
|
|
1448
1727
|
class CellContextMenuComponent {
|
|
1449
1728
|
menuTrigger = viewChild.required('menuTrigger', { read: MatMenuTrigger });
|
|
1450
1729
|
menuService = inject(CellContextMenuService);
|
|
1730
|
+
#renderer = inject(Renderer2);
|
|
1731
|
+
#destroyRef = inject(DestroyRef);
|
|
1451
1732
|
menuPosition = computed(() => {
|
|
1452
1733
|
const menu = this.menuService.activeMenu();
|
|
1453
1734
|
return menu ? { left: `${menu.x}px`, top: `${menu.y}px` } : { left: '0px', top: '0px' };
|
|
@@ -1457,6 +1738,23 @@ class CellContextMenuComponent {
|
|
|
1457
1738
|
return menu?.items || [];
|
|
1458
1739
|
}, ...(ngDevMode ? [{ debugName: "menuItems" }] : []));
|
|
1459
1740
|
constructor() {
|
|
1741
|
+
// Material Menu has a backdrop that blocks events from reaching other elements.
|
|
1742
|
+
// When any right-click occurs while menu is open, we need to:
|
|
1743
|
+
// 1. Close the current menu
|
|
1744
|
+
// 2. Prevent the browser's default context menu
|
|
1745
|
+
//
|
|
1746
|
+
// Users will need to right-click again to open empty cell menus.
|
|
1747
|
+
// This follows standard UX patterns used by most applications.
|
|
1748
|
+
const unlisten = this.#renderer.listen('document', 'contextmenu', (event) => {
|
|
1749
|
+
if (this.menuService.activeMenu()) {
|
|
1750
|
+
// Prevent browser's default context menu when closing widget menu
|
|
1751
|
+
event.preventDefault();
|
|
1752
|
+
this.menuService.hide();
|
|
1753
|
+
}
|
|
1754
|
+
});
|
|
1755
|
+
this.#destroyRef.onDestroy(() => {
|
|
1756
|
+
unlisten();
|
|
1757
|
+
});
|
|
1460
1758
|
effect(() => {
|
|
1461
1759
|
const menu = this.menuService.activeMenu();
|
|
1462
1760
|
if (menu) {
|
|
@@ -1552,7 +1850,7 @@ class CellContextMenuComponent {
|
|
|
1552
1850
|
}
|
|
1553
1851
|
}
|
|
1554
1852
|
</mat-menu>
|
|
1555
|
-
`, isInline: true, styles: [":host{display:contents}\n"], dependencies: [{ kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i1$
|
|
1853
|
+
`, isInline: true, styles: [":host{display:contents}\n"], dependencies: [{ kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i1$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i1$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i1$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i3.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1556
1854
|
}
|
|
1557
1855
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellContextMenuComponent, decorators: [{
|
|
1558
1856
|
type: Component,
|
|
@@ -1611,6 +1909,232 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
|
|
|
1611
1909
|
`, styles: [":host{display:contents}\n"] }]
|
|
1612
1910
|
}], ctorParameters: () => [] });
|
|
1613
1911
|
|
|
1912
|
+
/**
|
|
1913
|
+
* Abstract provider for handling context menu events on empty dashboard cells.
|
|
1914
|
+
* Implement this to provide custom behavior when users right-click on unoccupied grid spaces.
|
|
1915
|
+
*
|
|
1916
|
+
* @example
|
|
1917
|
+
* ```typescript
|
|
1918
|
+
* @Injectable()
|
|
1919
|
+
* export class CustomEmptyCellProvider extends EmptyCellContextProvider {
|
|
1920
|
+
* handleEmptyCellContext(event: MouseEvent, context: EmptyCellContext): void {
|
|
1921
|
+
* event.preventDefault();
|
|
1922
|
+
* // Show custom menu, open dialog, etc.
|
|
1923
|
+
* }
|
|
1924
|
+
* }
|
|
1925
|
+
* ```
|
|
1926
|
+
*/
|
|
1927
|
+
class EmptyCellContextProvider {
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
/**
|
|
1931
|
+
* Default empty cell context provider that prevents the browser's context menu
|
|
1932
|
+
* and performs no other action.
|
|
1933
|
+
*
|
|
1934
|
+
* This is the default behavior that allows users to right-click on empty dashboard
|
|
1935
|
+
* cells without triggering the browser's default context menu.
|
|
1936
|
+
*/
|
|
1937
|
+
class DefaultEmptyCellContextProvider extends EmptyCellContextProvider {
|
|
1938
|
+
/**
|
|
1939
|
+
* Default empty cell context handler.
|
|
1940
|
+
* The browser context menu is already prevented by the component.
|
|
1941
|
+
* No additional action is taken by default.
|
|
1942
|
+
*/
|
|
1943
|
+
handleEmptyCellContext() {
|
|
1944
|
+
// Default behavior: do nothing
|
|
1945
|
+
}
|
|
1946
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DefaultEmptyCellContextProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
1947
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DefaultEmptyCellContextProvider, providedIn: 'root' });
|
|
1948
|
+
}
|
|
1949
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DefaultEmptyCellContextProvider, decorators: [{
|
|
1950
|
+
type: Injectable,
|
|
1951
|
+
args: [{
|
|
1952
|
+
providedIn: 'root',
|
|
1953
|
+
}]
|
|
1954
|
+
}] });
|
|
1955
|
+
|
|
1956
|
+
/**
|
|
1957
|
+
* Injection token for the empty cell context provider.
|
|
1958
|
+
* Use this to provide your custom implementation for handling right-clicks on empty dashboard cells.
|
|
1959
|
+
*
|
|
1960
|
+
* @example
|
|
1961
|
+
* ```typescript
|
|
1962
|
+
* // Provide a custom implementation
|
|
1963
|
+
* providers: [
|
|
1964
|
+
* {
|
|
1965
|
+
* provide: EMPTY_CELL_CONTEXT_PROVIDER,
|
|
1966
|
+
* useClass: MyCustomEmptyCellProvider
|
|
1967
|
+
* }
|
|
1968
|
+
* ]
|
|
1969
|
+
* ```
|
|
1970
|
+
*/
|
|
1971
|
+
const EMPTY_CELL_CONTEXT_PROVIDER = new InjectionToken('EmptyCellContextProvider', {
|
|
1972
|
+
providedIn: 'root',
|
|
1973
|
+
factory: () => new DefaultEmptyCellContextProvider(),
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
/**
|
|
1977
|
+
* Service for managing empty cell context menu state.
|
|
1978
|
+
* Similar to CellContextMenuService but specifically for empty cells.
|
|
1979
|
+
*
|
|
1980
|
+
* This service is internal to the library and manages the display state
|
|
1981
|
+
* of the context menu that appears when right-clicking on empty dashboard cells.
|
|
1982
|
+
*/
|
|
1983
|
+
class EmptyCellContextMenuService {
|
|
1984
|
+
#activeMenu = signal(null, ...(ngDevMode ? [{ debugName: "#activeMenu" }] : []));
|
|
1985
|
+
#lastSelectedWidgetTypeId = signal(null, ...(ngDevMode ? [{ debugName: "#lastSelectedWidgetTypeId" }] : []));
|
|
1986
|
+
activeMenu = this.#activeMenu.asReadonly();
|
|
1987
|
+
lastSelectedWidgetTypeId = this.#lastSelectedWidgetTypeId.asReadonly();
|
|
1988
|
+
/**
|
|
1989
|
+
* Show the context menu at specific coordinates with given items.
|
|
1990
|
+
*
|
|
1991
|
+
* @param x - X coordinate (clientX from mouse event)
|
|
1992
|
+
* @param y - Y coordinate (clientY from mouse event)
|
|
1993
|
+
* @param items - Menu items to display
|
|
1994
|
+
*/
|
|
1995
|
+
show(x, y, items) {
|
|
1996
|
+
this.#activeMenu.set({ x, y, items });
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Hide the context menu.
|
|
2000
|
+
*/
|
|
2001
|
+
hide() {
|
|
2002
|
+
this.#activeMenu.set(null);
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Set the last selected widget type ID for quick-repeat functionality.
|
|
2006
|
+
* Pass null to clear the last selection.
|
|
2007
|
+
*
|
|
2008
|
+
* @param widgetTypeId - The widget type identifier, or null to clear
|
|
2009
|
+
*/
|
|
2010
|
+
setLastSelection(widgetTypeId) {
|
|
2011
|
+
this.#lastSelectedWidgetTypeId.set(widgetTypeId);
|
|
2012
|
+
}
|
|
2013
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2014
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuService, providedIn: 'root' });
|
|
2015
|
+
}
|
|
2016
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuService, decorators: [{
|
|
2017
|
+
type: Injectable,
|
|
2018
|
+
args: [{
|
|
2019
|
+
providedIn: 'root',
|
|
2020
|
+
}]
|
|
2021
|
+
}] });
|
|
2022
|
+
|
|
2023
|
+
/**
|
|
2024
|
+
* Context provider that displays a widget list menu when right-clicking on empty cells.
|
|
2025
|
+
*
|
|
2026
|
+
* This provider shows a Material Design context menu with all available widget types.
|
|
2027
|
+
* When a user clicks on a widget in the menu, it's immediately added to the empty cell.
|
|
2028
|
+
*
|
|
2029
|
+
* @example
|
|
2030
|
+
* ```typescript
|
|
2031
|
+
* // In your component or app config
|
|
2032
|
+
* providers: [
|
|
2033
|
+
* {
|
|
2034
|
+
* provide: EMPTY_CELL_CONTEXT_PROVIDER,
|
|
2035
|
+
* useClass: WidgetListContextMenuProvider
|
|
2036
|
+
* }
|
|
2037
|
+
* ]
|
|
2038
|
+
* ```
|
|
2039
|
+
*
|
|
2040
|
+
* @public
|
|
2041
|
+
*/
|
|
2042
|
+
class WidgetListContextMenuProvider extends EmptyCellContextProvider {
|
|
2043
|
+
#menuService = inject(EmptyCellContextMenuService);
|
|
2044
|
+
#dashboardService = inject(DashboardService);
|
|
2045
|
+
/**
|
|
2046
|
+
* Handle empty cell context menu by showing available widgets.
|
|
2047
|
+
*
|
|
2048
|
+
* @param event - The mouse event from the right-click
|
|
2049
|
+
* @param context - Information about the empty cell and dashboard
|
|
2050
|
+
*/
|
|
2051
|
+
handleEmptyCellContext(event, context) {
|
|
2052
|
+
event.preventDefault();
|
|
2053
|
+
// Get available widgets from dashboard service
|
|
2054
|
+
const widgets = this.#dashboardService.widgetTypes();
|
|
2055
|
+
// Create menu items from widgets
|
|
2056
|
+
const items = this.#createMenuItems(widgets, context);
|
|
2057
|
+
// Show the context menu at mouse coordinates
|
|
2058
|
+
this.#menuService.show(event.clientX, event.clientY, items);
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Create menu items from available widget types.
|
|
2062
|
+
* Each item includes the widget's icon and display name.
|
|
2063
|
+
* If a widget was previously selected, it appears first as a quick-repeat option.
|
|
2064
|
+
*
|
|
2065
|
+
* @param widgets - Array of widget component classes
|
|
2066
|
+
* @param context - The empty cell context with createWidget callback
|
|
2067
|
+
* @returns Array of menu items ready for display
|
|
2068
|
+
*/
|
|
2069
|
+
#createMenuItems(widgets, context) {
|
|
2070
|
+
if (widgets.length === 0) {
|
|
2071
|
+
// Show a message if no widgets are registered
|
|
2072
|
+
return [
|
|
2073
|
+
{
|
|
2074
|
+
label: $localize `:@@ngx.dashboard.emptyCellMenu.noWidgets:No widgets available`,
|
|
2075
|
+
disabled: true,
|
|
2076
|
+
action: () => {
|
|
2077
|
+
// No action needed
|
|
2078
|
+
},
|
|
2079
|
+
},
|
|
2080
|
+
];
|
|
2081
|
+
}
|
|
2082
|
+
// Build standard menu items with widgetTypeId
|
|
2083
|
+
const allItems = widgets.map((widget) => ({
|
|
2084
|
+
label: widget.metadata.name,
|
|
2085
|
+
svgIcon: widget.metadata.svgIcon,
|
|
2086
|
+
widgetTypeId: widget.metadata.widgetTypeid,
|
|
2087
|
+
action: () => this.#createWidget(widget.metadata.widgetTypeid, context),
|
|
2088
|
+
}));
|
|
2089
|
+
// Check if there's a last selected widget to show as quick-repeat
|
|
2090
|
+
const lastSelectedTypeId = this.#menuService.lastSelectedWidgetTypeId();
|
|
2091
|
+
if (!lastSelectedTypeId) {
|
|
2092
|
+
return allItems;
|
|
2093
|
+
}
|
|
2094
|
+
// Find the last selected widget in the list
|
|
2095
|
+
const lastSelectedWidget = widgets.find((w) => w.metadata.widgetTypeid === lastSelectedTypeId);
|
|
2096
|
+
if (!lastSelectedWidget) {
|
|
2097
|
+
// Last selected widget no longer available, return normal list
|
|
2098
|
+
return allItems;
|
|
2099
|
+
}
|
|
2100
|
+
// Create quick-repeat item (duplicate of the last selected widget)
|
|
2101
|
+
const quickRepeatItem = {
|
|
2102
|
+
label: lastSelectedWidget.metadata.name,
|
|
2103
|
+
svgIcon: lastSelectedWidget.metadata.svgIcon,
|
|
2104
|
+
widgetTypeId: lastSelectedWidget.metadata.widgetTypeid,
|
|
2105
|
+
action: () => this.#createWidget(lastSelectedWidget.metadata.widgetTypeid, context),
|
|
2106
|
+
};
|
|
2107
|
+
// Create divider
|
|
2108
|
+
const divider = { divider: true };
|
|
2109
|
+
// Return special structure: [quick-repeat, divider, full list]
|
|
2110
|
+
return [quickRepeatItem, divider, ...allItems];
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* Create a widget at the empty cell position.
|
|
2114
|
+
* Uses the createWidget callback provided in the context.
|
|
2115
|
+
*
|
|
2116
|
+
* @param widgetTypeid - The widget type identifier to create
|
|
2117
|
+
* @param context - The empty cell context with createWidget callback
|
|
2118
|
+
*/
|
|
2119
|
+
#createWidget(widgetTypeid, context) {
|
|
2120
|
+
if (context.createWidget) {
|
|
2121
|
+
const success = context.createWidget(widgetTypeid);
|
|
2122
|
+
if (!success) {
|
|
2123
|
+
console.error(`Failed to create widget '${widgetTypeid}' at (${context.row}, ${context.col})`);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
else {
|
|
2127
|
+
console.warn('createWidget callback not available in EmptyCellContext. ' +
|
|
2128
|
+
'Ensure you are using a compatible version of the dashboard component.');
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListContextMenuProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
2132
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListContextMenuProvider });
|
|
2133
|
+
}
|
|
2134
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListContextMenuProvider, decorators: [{
|
|
2135
|
+
type: Injectable
|
|
2136
|
+
}] });
|
|
2137
|
+
|
|
1614
2138
|
// drop-zone.component.ts
|
|
1615
2139
|
class DropZoneComponent {
|
|
1616
2140
|
// Required inputs
|
|
@@ -1645,6 +2169,10 @@ class DropZoneComponent {
|
|
|
1645
2169
|
}, ...(ngDevMode ? [{ debugName: "dropEffect" }] : []));
|
|
1646
2170
|
#store = inject(DashboardStore);
|
|
1647
2171
|
#elementRef = inject(ElementRef);
|
|
2172
|
+
#dashboardService = inject(DashboardService);
|
|
2173
|
+
#contextProvider = inject(EMPTY_CELL_CONTEXT_PROVIDER, {
|
|
2174
|
+
optional: true,
|
|
2175
|
+
});
|
|
1648
2176
|
get nativeElement() {
|
|
1649
2177
|
return this.#elementRef.nativeElement;
|
|
1650
2178
|
}
|
|
@@ -1689,14 +2217,267 @@ class DropZoneComponent {
|
|
|
1689
2217
|
});
|
|
1690
2218
|
}
|
|
1691
2219
|
}
|
|
2220
|
+
/**
|
|
2221
|
+
* Handle context menu events on empty cells.
|
|
2222
|
+
* Only active in edit mode. Delegates to the context provider if available.
|
|
2223
|
+
*/
|
|
2224
|
+
onContextMenu(event) {
|
|
2225
|
+
if (!this.editMode())
|
|
2226
|
+
return;
|
|
2227
|
+
// Prevent default browser menu and stop propagation in edit mode
|
|
2228
|
+
// stopPropagation prevents the event from reaching the document-level
|
|
2229
|
+
// listener in EmptyCellContextMenuComponent which would immediately hide the menu
|
|
2230
|
+
event.preventDefault();
|
|
2231
|
+
event.stopPropagation();
|
|
2232
|
+
if (this.#contextProvider) {
|
|
2233
|
+
this.#contextProvider.handleEmptyCellContext(event, {
|
|
2234
|
+
row: this.row(),
|
|
2235
|
+
col: this.col(),
|
|
2236
|
+
totalRows: this.#store.rows(),
|
|
2237
|
+
totalColumns: this.#store.columns(),
|
|
2238
|
+
gutterSize: this.#store.gutterSize(),
|
|
2239
|
+
createWidget: (widgetTypeid) => {
|
|
2240
|
+
const factory = this.#dashboardService.getFactory(widgetTypeid);
|
|
2241
|
+
this.#store.createWidget(this.row(), this.col(), factory, undefined);
|
|
2242
|
+
return true; // Widget created successfully
|
|
2243
|
+
},
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
1692
2247
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DropZoneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1693
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DropZoneComponent, isStandalone: true, selector: "lib-drop-zone", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, col: { classPropertyName: "col", publicName: "col", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null }, highlight: { classPropertyName: "highlight", publicName: "highlight", isSignal: true, isRequired: false, transformFunction: null }, highlightInvalid: { classPropertyName: "highlightInvalid", publicName: "highlightInvalid", isSignal: true, isRequired: false, transformFunction: null }, highlightResize: { classPropertyName: "highlightResize", publicName: "highlightResize", isSignal: true, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dragEnter: "dragEnter", dragExit: "dragExit", dragOver: "dragOver", dragDrop: "dragDrop" }, ngImport: i0, template: "<!-- drop-zone.component.html -->\r\n<div\r\n class=\"drop-zone\"\r\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\r\n [class.drop-zone--invalid]=\"highlightInvalid()\"\r\n [class.drop-zone--resize]=\"highlightResize()\"\r\n [style.grid-row]=\"row()\"\r\n [style.grid-column]=\"col()\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n>\r\n @if (editMode()) {\r\n <div class=\"edit-mode-cell-number\">\r\n {{ index() }}<br />\r\n {{ row() }},{{ col() }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".drop-zone{width:100%;height:100%;z-index:0;align-self:stretch;justify-self:stretch;display:block;box-sizing:border-box}.drop-zone--active,.drop-zone--highlight{background-color:#80808080}.drop-zone--invalid{background-color:light-dark(color-mix(in srgb,var(--mat-sys-error) 40%,white),color-mix(in srgb,var(--mat-sys-error) 80%,black))}.drop-zone--resize{background-color:#2196f34d;outline:1px solid rgba(33,150,243,.6)}.edit-mode-cell-number{font-size:10px;line-height:1.1;color:#64646499;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:-1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2248
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DropZoneComponent, isStandalone: true, selector: "lib-drop-zone", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, col: { classPropertyName: "col", publicName: "col", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null }, highlight: { classPropertyName: "highlight", publicName: "highlight", isSignal: true, isRequired: false, transformFunction: null }, highlightInvalid: { classPropertyName: "highlightInvalid", publicName: "highlightInvalid", isSignal: true, isRequired: false, transformFunction: null }, highlightResize: { classPropertyName: "highlightResize", publicName: "highlightResize", isSignal: true, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dragEnter: "dragEnter", dragExit: "dragExit", dragOver: "dragOver", dragDrop: "dragDrop" }, ngImport: i0, template: "<!-- drop-zone.component.html -->\r\n<div\r\n class=\"drop-zone\"\r\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\r\n [class.drop-zone--invalid]=\"highlightInvalid()\"\r\n [class.drop-zone--resize]=\"highlightResize()\"\r\n [style.grid-row]=\"row()\"\r\n [style.grid-column]=\"col()\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n @if (editMode()) {\r\n <div class=\"edit-mode-cell-number\">\r\n {{ index() }}<br />\r\n {{ row() }},{{ col() }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".drop-zone{width:100%;height:100%;z-index:0;align-self:stretch;justify-self:stretch;display:block;box-sizing:border-box}.drop-zone--active,.drop-zone--highlight{background-color:#80808080}.drop-zone--invalid{background-color:light-dark(color-mix(in srgb,var(--mat-sys-error) 40%,white),color-mix(in srgb,var(--mat-sys-error) 80%,black))}.drop-zone--resize{background-color:#2196f34d;outline:1px solid rgba(33,150,243,.6)}.edit-mode-cell-number{font-size:10px;line-height:1.1;color:#64646499;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:-1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1694
2249
|
}
|
|
1695
2250
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DropZoneComponent, decorators: [{
|
|
1696
2251
|
type: Component,
|
|
1697
|
-
args: [{ selector: 'lib-drop-zone', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- drop-zone.component.html -->\r\n<div\r\n class=\"drop-zone\"\r\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\r\n [class.drop-zone--invalid]=\"highlightInvalid()\"\r\n [class.drop-zone--resize]=\"highlightResize()\"\r\n [style.grid-row]=\"row()\"\r\n [style.grid-column]=\"col()\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n>\r\n @if (editMode()) {\r\n <div class=\"edit-mode-cell-number\">\r\n {{ index() }}<br />\r\n {{ row() }},{{ col() }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".drop-zone{width:100%;height:100%;z-index:0;align-self:stretch;justify-self:stretch;display:block;box-sizing:border-box}.drop-zone--active,.drop-zone--highlight{background-color:#80808080}.drop-zone--invalid{background-color:light-dark(color-mix(in srgb,var(--mat-sys-error) 40%,white),color-mix(in srgb,var(--mat-sys-error) 80%,black))}.drop-zone--resize{background-color:#2196f34d;outline:1px solid rgba(33,150,243,.6)}.edit-mode-cell-number{font-size:10px;line-height:1.1;color:#64646499;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:-1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;width:100%;height:100%}\n"] }]
|
|
2252
|
+
args: [{ selector: 'lib-drop-zone', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- drop-zone.component.html -->\r\n<div\r\n class=\"drop-zone\"\r\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\r\n [class.drop-zone--invalid]=\"highlightInvalid()\"\r\n [class.drop-zone--resize]=\"highlightResize()\"\r\n [style.grid-row]=\"row()\"\r\n [style.grid-column]=\"col()\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n @if (editMode()) {\r\n <div class=\"edit-mode-cell-number\">\r\n {{ index() }}<br />\r\n {{ row() }},{{ col() }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".drop-zone{width:100%;height:100%;z-index:0;align-self:stretch;justify-self:stretch;display:block;box-sizing:border-box}.drop-zone--active,.drop-zone--highlight{background-color:#80808080}.drop-zone--invalid{background-color:light-dark(color-mix(in srgb,var(--mat-sys-error) 40%,white),color-mix(in srgb,var(--mat-sys-error) 80%,black))}.drop-zone--resize{background-color:#2196f34d;outline:1px solid rgba(33,150,243,.6)}.edit-mode-cell-number{font-size:10px;line-height:1.1;color:#64646499;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:-1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;width:100%;height:100%}\n"] }]
|
|
1698
2253
|
}] });
|
|
1699
2254
|
|
|
2255
|
+
/**
|
|
2256
|
+
* Context menu component for empty dashboard cells.
|
|
2257
|
+
* Displays a list of available widgets that can be added to the empty cell.
|
|
2258
|
+
*
|
|
2259
|
+
* This component is similar to CellContextMenuComponent but designed specifically
|
|
2260
|
+
* for empty cells and includes support for SVG icons from widget metadata.
|
|
2261
|
+
*
|
|
2262
|
+
* @internal This component is for internal library use only
|
|
2263
|
+
*/
|
|
2264
|
+
class EmptyCellContextMenuComponent {
|
|
2265
|
+
menuTrigger = viewChild.required('menuTrigger', { read: MatMenuTrigger });
|
|
2266
|
+
menuService = inject(EmptyCellContextMenuService);
|
|
2267
|
+
#sanitizer = inject(DomSanitizer);
|
|
2268
|
+
#renderer = inject(Renderer2);
|
|
2269
|
+
#destroyRef = inject(DestroyRef);
|
|
2270
|
+
menuPosition = computed(() => {
|
|
2271
|
+
const menu = this.menuService.activeMenu();
|
|
2272
|
+
return menu
|
|
2273
|
+
? { left: `${menu.x}px`, top: `${menu.y}px` }
|
|
2274
|
+
: { left: '0px', top: '0px' };
|
|
2275
|
+
}, ...(ngDevMode ? [{ debugName: "menuPosition" }] : []));
|
|
2276
|
+
menuItems = computed(() => {
|
|
2277
|
+
const menu = this.menuService.activeMenu();
|
|
2278
|
+
return menu?.items || [];
|
|
2279
|
+
}, ...(ngDevMode ? [{ debugName: "menuItems" }] : []));
|
|
2280
|
+
constructor() {
|
|
2281
|
+
// Material Menu has a backdrop that blocks events from reaching other elements.
|
|
2282
|
+
// When any right-click occurs while menu is open, we need to:
|
|
2283
|
+
// 1. Close the current menu
|
|
2284
|
+
// 2. Prevent the browser's default context menu
|
|
2285
|
+
//
|
|
2286
|
+
// Users will need to right-click again to open empty cell menus.
|
|
2287
|
+
// This follows standard UX patterns used by most applications.
|
|
2288
|
+
const unlisten = this.#renderer.listen('document', 'contextmenu', (event) => {
|
|
2289
|
+
if (this.menuService.activeMenu()) {
|
|
2290
|
+
// Prevent browser's default context menu when closing widget menu
|
|
2291
|
+
event.preventDefault();
|
|
2292
|
+
this.menuService.hide();
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
this.#destroyRef.onDestroy(() => {
|
|
2296
|
+
unlisten();
|
|
2297
|
+
});
|
|
2298
|
+
// Show menu when service state changes
|
|
2299
|
+
effect(() => {
|
|
2300
|
+
const menu = this.menuService.activeMenu();
|
|
2301
|
+
if (menu) {
|
|
2302
|
+
// Use queueMicrotask to ensure the view is fully initialized
|
|
2303
|
+
// This fixes the issue where the menu disappears on first right-click
|
|
2304
|
+
queueMicrotask(() => {
|
|
2305
|
+
const trigger = this.menuTrigger();
|
|
2306
|
+
if (trigger) {
|
|
2307
|
+
// Close any existing menu first
|
|
2308
|
+
if (trigger.menuOpen) {
|
|
2309
|
+
trigger.closeMenu();
|
|
2310
|
+
}
|
|
2311
|
+
// Open menu - position is handled by signal
|
|
2312
|
+
trigger.openMenu();
|
|
2313
|
+
}
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
else {
|
|
2317
|
+
const trigger = this.menuTrigger();
|
|
2318
|
+
if (trigger) {
|
|
2319
|
+
trigger.closeMenu();
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
// Subscribe to menu closed event once when trigger becomes available
|
|
2324
|
+
// This ensures the service state is synchronized when Material menu closes
|
|
2325
|
+
effect((onCleanup) => {
|
|
2326
|
+
const trigger = this.menuTrigger();
|
|
2327
|
+
if (trigger) {
|
|
2328
|
+
const subscription = trigger.menuClosed.subscribe(() => {
|
|
2329
|
+
if (this.menuService.activeMenu()) {
|
|
2330
|
+
this.menuService.hide();
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
2333
|
+
// Clean up subscription when effect re-runs or component destroys
|
|
2334
|
+
onCleanup(() => subscription.unsubscribe());
|
|
2335
|
+
}
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
executeAction(item) {
|
|
2339
|
+
if (!item.divider && item.action) {
|
|
2340
|
+
// Track widget type if provided
|
|
2341
|
+
if (item.widgetTypeId) {
|
|
2342
|
+
this.menuService.setLastSelection(item.widgetTypeId);
|
|
2343
|
+
}
|
|
2344
|
+
item.action();
|
|
2345
|
+
this.menuService.hide();
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
sanitizeSvg(svg) {
|
|
2349
|
+
return this.#sanitizer.bypassSecurityTrustHtml(svg);
|
|
2350
|
+
}
|
|
2351
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2352
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: EmptyCellContextMenuComponent, isStandalone: true, selector: "lib-empty-cell-context-menu", viewQueries: [{ propertyName: "menuTrigger", first: true, predicate: ["menuTrigger"], descendants: true, read: MatMenuTrigger, isSignal: true }], ngImport: i0, template: `
|
|
2353
|
+
<!-- Hidden trigger for menu positioned at exact mouse coordinates
|
|
2354
|
+
|
|
2355
|
+
IMPORTANT: Angular Material applies its own positioning logic to menus,
|
|
2356
|
+
which by default offsets the menu from the trigger element to avoid overlap.
|
|
2357
|
+
To achieve precise positioning at mouse coordinates, we use these workarounds:
|
|
2358
|
+
|
|
2359
|
+
1. The trigger container is 1x1px (not 0x0) because Material needs a physical
|
|
2360
|
+
element to calculate position against. Zero-sized elements cause unpredictable
|
|
2361
|
+
positioning.
|
|
2362
|
+
|
|
2363
|
+
2. We use opacity:0 instead of visibility:hidden to keep the element in the
|
|
2364
|
+
layout flow while making it invisible.
|
|
2365
|
+
|
|
2366
|
+
3. The button itself is styled to 1x1px with no padding to serve as a precise
|
|
2367
|
+
anchor point for the menu.
|
|
2368
|
+
|
|
2369
|
+
4. The mat-menu uses [overlapTrigger]="true" to allow the menu to appear
|
|
2370
|
+
directly at the trigger position rather than offset from it.
|
|
2371
|
+
|
|
2372
|
+
This approach ensures the menu appears at the exact mouse coordinates passed
|
|
2373
|
+
from the empty cell context provider's right-click handler.
|
|
2374
|
+
-->
|
|
2375
|
+
<div
|
|
2376
|
+
style="position: fixed; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
|
|
2377
|
+
[style]="menuPosition()">
|
|
2378
|
+
<button
|
|
2379
|
+
mat-button
|
|
2380
|
+
#menuTrigger="matMenuTrigger"
|
|
2381
|
+
[matMenuTriggerFor]="contextMenu"
|
|
2382
|
+
style="width: 1px; height: 1px; padding: 0; min-width: 0; line-height: 0;">
|
|
2383
|
+
</button>
|
|
2384
|
+
</div>
|
|
2385
|
+
|
|
2386
|
+
<!-- Context menu with widget list -->
|
|
2387
|
+
<mat-menu
|
|
2388
|
+
#contextMenu="matMenu"
|
|
2389
|
+
[overlapTrigger]="true"
|
|
2390
|
+
class="empty-cell-widget-menu">
|
|
2391
|
+
@for (item of menuItems(); track $index) {
|
|
2392
|
+
@if (item.divider) {
|
|
2393
|
+
<mat-divider></mat-divider>
|
|
2394
|
+
} @else {
|
|
2395
|
+
<button
|
|
2396
|
+
mat-menu-item
|
|
2397
|
+
(click)="executeAction(item)"
|
|
2398
|
+
[disabled]="item.disabled"
|
|
2399
|
+
[attr.aria-label]="item.label">
|
|
2400
|
+
@if (item.svgIcon) {
|
|
2401
|
+
<div class="widget-icon" [innerHTML]="sanitizeSvg(item.svgIcon)"></div>
|
|
2402
|
+
} @else if (item.icon) {
|
|
2403
|
+
<mat-icon>{{ item.icon }}</mat-icon>
|
|
2404
|
+
}
|
|
2405
|
+
{{ item.label }}
|
|
2406
|
+
</button>
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
</mat-menu>
|
|
2410
|
+
`, isInline: true, styles: [":host{display:contents}.empty-cell-widget-menu{max-height:400px;overflow-y:auto}.widget-icon{width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;vertical-align:middle}.widget-icon :deep(svg){width:20px;height:20px;fill:currentColor}\n"], dependencies: [{ kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i1$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i1$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i1$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i3.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2411
|
+
}
|
|
2412
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuComponent, decorators: [{
|
|
2413
|
+
type: Component,
|
|
2414
|
+
args: [{ selector: 'lib-empty-cell-context-menu', standalone: true, imports: [
|
|
2415
|
+
MatMenuModule,
|
|
2416
|
+
MatIconModule,
|
|
2417
|
+
MatDividerModule,
|
|
2418
|
+
MatButtonModule,
|
|
2419
|
+
CommonModule,
|
|
2420
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
2421
|
+
<!-- Hidden trigger for menu positioned at exact mouse coordinates
|
|
2422
|
+
|
|
2423
|
+
IMPORTANT: Angular Material applies its own positioning logic to menus,
|
|
2424
|
+
which by default offsets the menu from the trigger element to avoid overlap.
|
|
2425
|
+
To achieve precise positioning at mouse coordinates, we use these workarounds:
|
|
2426
|
+
|
|
2427
|
+
1. The trigger container is 1x1px (not 0x0) because Material needs a physical
|
|
2428
|
+
element to calculate position against. Zero-sized elements cause unpredictable
|
|
2429
|
+
positioning.
|
|
2430
|
+
|
|
2431
|
+
2. We use opacity:0 instead of visibility:hidden to keep the element in the
|
|
2432
|
+
layout flow while making it invisible.
|
|
2433
|
+
|
|
2434
|
+
3. The button itself is styled to 1x1px with no padding to serve as a precise
|
|
2435
|
+
anchor point for the menu.
|
|
2436
|
+
|
|
2437
|
+
4. The mat-menu uses [overlapTrigger]="true" to allow the menu to appear
|
|
2438
|
+
directly at the trigger position rather than offset from it.
|
|
2439
|
+
|
|
2440
|
+
This approach ensures the menu appears at the exact mouse coordinates passed
|
|
2441
|
+
from the empty cell context provider's right-click handler.
|
|
2442
|
+
-->
|
|
2443
|
+
<div
|
|
2444
|
+
style="position: fixed; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
|
|
2445
|
+
[style]="menuPosition()">
|
|
2446
|
+
<button
|
|
2447
|
+
mat-button
|
|
2448
|
+
#menuTrigger="matMenuTrigger"
|
|
2449
|
+
[matMenuTriggerFor]="contextMenu"
|
|
2450
|
+
style="width: 1px; height: 1px; padding: 0; min-width: 0; line-height: 0;">
|
|
2451
|
+
</button>
|
|
2452
|
+
</div>
|
|
2453
|
+
|
|
2454
|
+
<!-- Context menu with widget list -->
|
|
2455
|
+
<mat-menu
|
|
2456
|
+
#contextMenu="matMenu"
|
|
2457
|
+
[overlapTrigger]="true"
|
|
2458
|
+
class="empty-cell-widget-menu">
|
|
2459
|
+
@for (item of menuItems(); track $index) {
|
|
2460
|
+
@if (item.divider) {
|
|
2461
|
+
<mat-divider></mat-divider>
|
|
2462
|
+
} @else {
|
|
2463
|
+
<button
|
|
2464
|
+
mat-menu-item
|
|
2465
|
+
(click)="executeAction(item)"
|
|
2466
|
+
[disabled]="item.disabled"
|
|
2467
|
+
[attr.aria-label]="item.label">
|
|
2468
|
+
@if (item.svgIcon) {
|
|
2469
|
+
<div class="widget-icon" [innerHTML]="sanitizeSvg(item.svgIcon)"></div>
|
|
2470
|
+
} @else if (item.icon) {
|
|
2471
|
+
<mat-icon>{{ item.icon }}</mat-icon>
|
|
2472
|
+
}
|
|
2473
|
+
{{ item.label }}
|
|
2474
|
+
</button>
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
</mat-menu>
|
|
2478
|
+
`, styles: [":host{display:contents}.empty-cell-widget-menu{max-height:400px;overflow-y:auto}.widget-icon{width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;vertical-align:middle}.widget-icon :deep(svg){width:20px;height:20px;fill:currentColor}\n"] }]
|
|
2479
|
+
}], ctorParameters: () => [] });
|
|
2480
|
+
|
|
1700
2481
|
// dashboard-editor.component.ts
|
|
1701
2482
|
class DashboardEditorComponent {
|
|
1702
2483
|
bottomGridRef = viewChild.required('bottomGrid');
|
|
@@ -1802,33 +2583,10 @@ class DashboardEditorComponent {
|
|
|
1802
2583
|
this.#store.handleDrop(event.data, event.target);
|
|
1803
2584
|
// Note: Store handles all validation and error handling internally
|
|
1804
2585
|
}
|
|
1805
|
-
/**
|
|
1806
|
-
* Get current widget states from all cell components.
|
|
1807
|
-
* Used during dashboard export to get live widget states.
|
|
1808
|
-
*/
|
|
1809
|
-
getCurrentWidgetStates() {
|
|
1810
|
-
const stateMap = new Map();
|
|
1811
|
-
const cells = this.cellComponents();
|
|
1812
|
-
for (const cell of cells) {
|
|
1813
|
-
const cellId = cell.cellId();
|
|
1814
|
-
const currentState = cell.getCurrentWidgetState();
|
|
1815
|
-
if (currentState !== undefined) {
|
|
1816
|
-
stateMap.set(CellIdUtils.toString(cellId), currentState);
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
return stateMap;
|
|
1820
|
-
}
|
|
1821
|
-
/**
|
|
1822
|
-
* Export dashboard with live widget states from current component instances.
|
|
1823
|
-
* This ensures the most up-to-date widget states are captured.
|
|
1824
|
-
*/
|
|
1825
|
-
exportDashboard() {
|
|
1826
|
-
return this.#store.exportDashboard(() => this.getCurrentWidgetStates());
|
|
1827
|
-
}
|
|
1828
2586
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1829
2587
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DashboardEditorComponent, isStandalone: true, selector: "ngx-dashboard-editor", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, gutterSize: { classPropertyName: "gutterSize", publicName: "gutterSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.--rows": "rows()", "style.--columns": "columns()", "style.--gutter-size": "gutterSize()", "style.--gutters": "gutters()", "class.is-edit-mode": "true" } }, providers: [
|
|
1830
2588
|
CellContextMenuService,
|
|
1831
|
-
], viewQueries: [{ propertyName: "bottomGridRef", first: true, predicate: ["bottomGrid"], descendants: true, isSignal: true }, { propertyName: "dropZones", predicate: DropZoneComponent, descendants: true, isSignal: true }, { propertyName: "cellComponents", predicate: CellComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- dashboard-editor.component.html -->\n<div class=\"grid-container\">\n <!-- Bottom grid with drop zones -->\n <div class=\"grid\" id=\"bottom-grid\" #bottomGrid>\n @for (position of dropzonePositions(); track position.id) {\n <lib-drop-zone\n class=\"drop-zone\"\n [row]=\"position.row\"\n [col]=\"position.col\"\n [index]=\"position.index\"\n [highlight]=\"highlightMap().has(createCellId(position.row, position.col))\"\n [highlightInvalid]=\"\n invalidHighlightMap().has(createCellId(position.row, position.col))\n \"\n [highlightResize]=\"\n resizePreviewMap().has(createCellId(position.row, position.col))\n \"\n [editMode]=\"true\"\n (dragEnter)=\"onDragEnter($event)\"\n (dragExit)=\"onDragExit()\"\n (dragOver)=\"onDragOver($event)\"\n (dragDrop)=\"onDragDrop($event)\"\n ></lib-drop-zone>\n }\n </div>\n\n <!-- Top grid with interactive cells -->\n <div class=\"grid\" id=\"top-grid\">\n @for (cell of cells(); track cell.widgetId) {\n <lib-cell\n class=\"grid-cell\"\n [widgetId]=\"cell.widgetId\"\n [cellId]=\"cell.cellId\"\n [isEditMode]=\"true\"\n [draggable]=\"true\"\n [row]=\"cell.row\"\n [column]=\"cell.col\"\n [rowSpan]=\"cell.rowSpan\"\n [colSpan]=\"cell.colSpan\"\n [flat]=\"cell.flat\"\n [widgetFactory]=\"cell.widgetFactory\"\n [widgetState]=\"cell.widgetState\"\n (dragStart)=\"onCellDragStart($event)\"\n (dragEnd)=\"dragEnd()\"\n (delete)=\"onCellDelete($event)\"\n (settings)=\"onCellSettings($event)\"\n (resizeStart)=\"onCellResizeStart($event)\"\n (resizeMove)=\"onCellResizeMove($event)\"\n (resizeEnd)=\"onCellResizeEnd($event)\"\n >\n </lib-cell>\n }\n </div>\n</div>\n\n<!-- Context
|
|
2589
|
+
], viewQueries: [{ propertyName: "bottomGridRef", first: true, predicate: ["bottomGrid"], descendants: true, isSignal: true }, { propertyName: "dropZones", predicate: DropZoneComponent, descendants: true, isSignal: true }, { propertyName: "cellComponents", predicate: CellComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- dashboard-editor.component.html -->\r\n<div class=\"grid-container\">\r\n <!-- Bottom grid with drop zones -->\r\n <div class=\"grid\" id=\"bottom-grid\" #bottomGrid>\r\n @for (position of dropzonePositions(); track position.id) {\r\n <lib-drop-zone\r\n class=\"drop-zone\"\r\n [row]=\"position.row\"\r\n [col]=\"position.col\"\r\n [index]=\"position.index\"\r\n [highlight]=\"highlightMap().has(createCellId(position.row, position.col))\"\r\n [highlightInvalid]=\"\r\n invalidHighlightMap().has(createCellId(position.row, position.col))\r\n \"\r\n [highlightResize]=\"\r\n resizePreviewMap().has(createCellId(position.row, position.col))\r\n \"\r\n [editMode]=\"true\"\r\n (dragEnter)=\"onDragEnter($event)\"\r\n (dragExit)=\"onDragExit()\"\r\n (dragOver)=\"onDragOver($event)\"\r\n (dragDrop)=\"onDragDrop($event)\"\r\n ></lib-drop-zone>\r\n }\r\n </div>\r\n\r\n <!-- Top grid with interactive cells -->\r\n <div class=\"grid\" id=\"top-grid\">\r\n @for (cell of cells(); track cell.widgetId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [widgetId]=\"cell.widgetId\"\r\n [cellId]=\"cell.cellId\"\r\n [isEditMode]=\"true\"\r\n [draggable]=\"true\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n (dragStart)=\"onCellDragStart($event)\"\r\n (dragEnd)=\"dragEnd()\"\r\n (delete)=\"onCellDelete($event)\"\r\n (settings)=\"onCellSettings($event)\"\r\n (resizeStart)=\"onCellResizeStart($event)\"\r\n (resizeMove)=\"onCellResizeMove($event)\"\r\n (resizeEnd)=\"onCellResizeEnd($event)\"\r\n >\r\n </lib-cell>\r\n }\r\n </div>\r\n</div>\r\n\r\n<!-- Context menus -->\r\n<lib-cell-context-menu></lib-cell-context-menu>\r\n<lib-empty-cell-context-menu></lib-empty-cell-context-menu>\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}:host .grid{background-image:linear-gradient(to right,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px);background-size:var(--tile-size) var(--tile-size),var(--tile-size) var(--tile-size),100% 1px;background-position:var(--tile-offset) var(--tile-offset),var(--tile-offset) var(--tile-offset),bottom;background-repeat:repeat,repeat,no-repeat}.grid-container{position:relative;width:100%;height:100%}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);position:absolute;inset:0;width:100%;height:100%;box-sizing:border-box;align-items:stretch;justify-items:stretch;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}#bottom-grid{z-index:1}#top-grid{z-index:2;pointer-events:none}.grid-cell{pointer-events:auto}.grid-cell.is-dragging{pointer-events:none;opacity:.5}\n"], dependencies: [{ kind: "component", type: CellComponent, selector: "lib-cell", inputs: ["widgetId", "cellId", "widgetFactory", "widgetState", "isEditMode", "flat", "row", "column", "rowSpan", "colSpan", "draggable"], outputs: ["rowChange", "columnChange", "dragStart", "dragEnd", "edit", "delete", "settings", "resizeStart", "resizeMove", "resizeEnd"] }, { kind: "ngmodule", type: CommonModule }, { kind: "component", type: DropZoneComponent, selector: "lib-drop-zone", inputs: ["row", "col", "index", "highlight", "highlightInvalid", "highlightResize", "editMode"], outputs: ["dragEnter", "dragExit", "dragOver", "dragDrop"] }, { kind: "component", type: CellContextMenuComponent, selector: "lib-cell-context-menu" }, { kind: "component", type: EmptyCellContextMenuComponent, selector: "lib-empty-cell-context-menu" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1832
2590
|
}
|
|
1833
2591
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardEditorComponent, decorators: [{
|
|
1834
2592
|
type: Component,
|
|
@@ -1837,6 +2595,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
|
|
|
1837
2595
|
CommonModule,
|
|
1838
2596
|
DropZoneComponent,
|
|
1839
2597
|
CellContextMenuComponent,
|
|
2598
|
+
EmptyCellContextMenuComponent,
|
|
1840
2599
|
], providers: [
|
|
1841
2600
|
CellContextMenuService,
|
|
1842
2601
|
], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
@@ -1845,7 +2604,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
|
|
|
1845
2604
|
'[style.--gutter-size]': 'gutterSize()',
|
|
1846
2605
|
'[style.--gutters]': 'gutters()',
|
|
1847
2606
|
'[class.is-edit-mode]': 'true', // Always in edit mode
|
|
1848
|
-
}, template: "<!-- dashboard-editor.component.html -->\n<div class=\"grid-container\">\n <!-- Bottom grid with drop zones -->\n <div class=\"grid\" id=\"bottom-grid\" #bottomGrid>\n @for (position of dropzonePositions(); track position.id) {\n <lib-drop-zone\n class=\"drop-zone\"\n [row]=\"position.row\"\n [col]=\"position.col\"\n [index]=\"position.index\"\n [highlight]=\"highlightMap().has(createCellId(position.row, position.col))\"\n [highlightInvalid]=\"\n invalidHighlightMap().has(createCellId(position.row, position.col))\n \"\n [highlightResize]=\"\n resizePreviewMap().has(createCellId(position.row, position.col))\n \"\n [editMode]=\"true\"\n (dragEnter)=\"onDragEnter($event)\"\n (dragExit)=\"onDragExit()\"\n (dragOver)=\"onDragOver($event)\"\n (dragDrop)=\"onDragDrop($event)\"\n ></lib-drop-zone>\n }\n </div>\n\n <!-- Top grid with interactive cells -->\n <div class=\"grid\" id=\"top-grid\">\n @for (cell of cells(); track cell.widgetId) {\n <lib-cell\n class=\"grid-cell\"\n [widgetId]=\"cell.widgetId\"\n [cellId]=\"cell.cellId\"\n [isEditMode]=\"true\"\n [draggable]=\"true\"\n [row]=\"cell.row\"\n [column]=\"cell.col\"\n [rowSpan]=\"cell.rowSpan\"\n [colSpan]=\"cell.colSpan\"\n [flat]=\"cell.flat\"\n [widgetFactory]=\"cell.widgetFactory\"\n [widgetState]=\"cell.widgetState\"\n (dragStart)=\"onCellDragStart($event)\"\n (dragEnd)=\"dragEnd()\"\n (delete)=\"onCellDelete($event)\"\n (settings)=\"onCellSettings($event)\"\n (resizeStart)=\"onCellResizeStart($event)\"\n (resizeMove)=\"onCellResizeMove($event)\"\n (resizeEnd)=\"onCellResizeEnd($event)\"\n >\n </lib-cell>\n }\n </div>\n</div>\n\n<!-- Context
|
|
2607
|
+
}, template: "<!-- dashboard-editor.component.html -->\r\n<div class=\"grid-container\">\r\n <!-- Bottom grid with drop zones -->\r\n <div class=\"grid\" id=\"bottom-grid\" #bottomGrid>\r\n @for (position of dropzonePositions(); track position.id) {\r\n <lib-drop-zone\r\n class=\"drop-zone\"\r\n [row]=\"position.row\"\r\n [col]=\"position.col\"\r\n [index]=\"position.index\"\r\n [highlight]=\"highlightMap().has(createCellId(position.row, position.col))\"\r\n [highlightInvalid]=\"\r\n invalidHighlightMap().has(createCellId(position.row, position.col))\r\n \"\r\n [highlightResize]=\"\r\n resizePreviewMap().has(createCellId(position.row, position.col))\r\n \"\r\n [editMode]=\"true\"\r\n (dragEnter)=\"onDragEnter($event)\"\r\n (dragExit)=\"onDragExit()\"\r\n (dragOver)=\"onDragOver($event)\"\r\n (dragDrop)=\"onDragDrop($event)\"\r\n ></lib-drop-zone>\r\n }\r\n </div>\r\n\r\n <!-- Top grid with interactive cells -->\r\n <div class=\"grid\" id=\"top-grid\">\r\n @for (cell of cells(); track cell.widgetId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [widgetId]=\"cell.widgetId\"\r\n [cellId]=\"cell.cellId\"\r\n [isEditMode]=\"true\"\r\n [draggable]=\"true\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n (dragStart)=\"onCellDragStart($event)\"\r\n (dragEnd)=\"dragEnd()\"\r\n (delete)=\"onCellDelete($event)\"\r\n (settings)=\"onCellSettings($event)\"\r\n (resizeStart)=\"onCellResizeStart($event)\"\r\n (resizeMove)=\"onCellResizeMove($event)\"\r\n (resizeEnd)=\"onCellResizeEnd($event)\"\r\n >\r\n </lib-cell>\r\n }\r\n </div>\r\n</div>\r\n\r\n<!-- Context menus -->\r\n<lib-cell-context-menu></lib-cell-context-menu>\r\n<lib-empty-cell-context-menu></lib-empty-cell-context-menu>\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}:host .grid{background-image:linear-gradient(to right,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px);background-size:var(--tile-size) var(--tile-size),var(--tile-size) var(--tile-size),100% 1px;background-position:var(--tile-offset) var(--tile-offset),var(--tile-offset) var(--tile-offset),bottom;background-repeat:repeat,repeat,no-repeat}.grid-container{position:relative;width:100%;height:100%}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);position:absolute;inset:0;width:100%;height:100%;box-sizing:border-box;align-items:stretch;justify-items:stretch;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}#bottom-grid{z-index:1}#top-grid{z-index:2;pointer-events:none}.grid-cell{pointer-events:auto}.grid-cell.is-dragging{pointer-events:none;opacity:.5}\n"] }]
|
|
1849
2608
|
}], ctorParameters: () => [] });
|
|
1850
2609
|
|
|
1851
2610
|
// dashboard-bridge.service.ts
|
|
@@ -2108,6 +2867,7 @@ class DashboardComponent {
|
|
|
2108
2867
|
#store = inject(DashboardStore);
|
|
2109
2868
|
#bridge = inject(DashboardBridgeService);
|
|
2110
2869
|
#viewport = inject(DashboardViewportService);
|
|
2870
|
+
#emptyCellMenuService = inject(EmptyCellContextMenuService);
|
|
2111
2871
|
#destroyRef = inject(DestroyRef);
|
|
2112
2872
|
// Public accessors for template
|
|
2113
2873
|
store = this.#store;
|
|
@@ -2116,6 +2876,9 @@ class DashboardComponent {
|
|
|
2116
2876
|
dashboardData = input.required(...(ngDevMode ? [{ debugName: "dashboardData" }] : []));
|
|
2117
2877
|
editMode = input(false, ...(ngDevMode ? [{ debugName: "editMode" }] : []));
|
|
2118
2878
|
reservedSpace = input(...(ngDevMode ? [undefined, { debugName: "reservedSpace" }] : []));
|
|
2879
|
+
enableSelection = input(false, ...(ngDevMode ? [{ debugName: "enableSelection" }] : []));
|
|
2880
|
+
// Component outputs
|
|
2881
|
+
selectionComplete = output();
|
|
2119
2882
|
// Store signals - shared by both child components
|
|
2120
2883
|
cells = this.#store.cells;
|
|
2121
2884
|
// ViewChild references for export/import functionality
|
|
@@ -2134,7 +2897,7 @@ class DashboardComponent {
|
|
|
2134
2897
|
effect(() => {
|
|
2135
2898
|
const data = this.dashboardData();
|
|
2136
2899
|
if (data) {
|
|
2137
|
-
this.#store.
|
|
2900
|
+
this.#store.loadDashboard(data);
|
|
2138
2901
|
// Register with bridge service after dashboard ID is set
|
|
2139
2902
|
this.#bridge.updateDashboardRegistration(this.#store);
|
|
2140
2903
|
this.#isInitialized = true;
|
|
@@ -2154,6 +2917,16 @@ class DashboardComponent {
|
|
|
2154
2917
|
this.#viewport.setReservedSpace(reserved);
|
|
2155
2918
|
}
|
|
2156
2919
|
});
|
|
2920
|
+
// Reset last widget selection when exiting edit mode
|
|
2921
|
+
effect(() => {
|
|
2922
|
+
const isEditMode = this.editMode();
|
|
2923
|
+
if (!isEditMode) {
|
|
2924
|
+
// Reset last selection when exiting edit mode
|
|
2925
|
+
untracked(() => {
|
|
2926
|
+
this.#emptyCellMenuService.setLastSelection(null);
|
|
2927
|
+
});
|
|
2928
|
+
}
|
|
2929
|
+
});
|
|
2157
2930
|
}
|
|
2158
2931
|
ngOnChanges(changes) {
|
|
2159
2932
|
// Handle edit mode changes after initialization
|
|
@@ -2168,17 +2941,30 @@ class DashboardComponent {
|
|
|
2168
2941
|
}
|
|
2169
2942
|
}
|
|
2170
2943
|
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2944
|
+
/**
|
|
2945
|
+
* Get current widget states from all cell components.
|
|
2946
|
+
* Used during dashboard export to get live widget states.
|
|
2947
|
+
*/
|
|
2948
|
+
getCurrentWidgetStates() {
|
|
2949
|
+
const stateMap = new Map();
|
|
2950
|
+
// Get cell components from the active child
|
|
2951
|
+
const cells = this.editMode()
|
|
2952
|
+
? this.dashboardEditor()?.cellComponents()
|
|
2953
|
+
: this.dashboardViewer()?.cellComponents();
|
|
2954
|
+
if (cells) {
|
|
2955
|
+
for (const cell of cells) {
|
|
2956
|
+
const cellId = cell.cellId();
|
|
2957
|
+
const currentState = cell.getCurrentWidgetState();
|
|
2958
|
+
if (currentState !== undefined) {
|
|
2959
|
+
stateMap.set(CellIdUtils.toString(cellId), currentState);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2181
2962
|
}
|
|
2963
|
+
return stateMap;
|
|
2964
|
+
}
|
|
2965
|
+
exportDashboard(selection, options) {
|
|
2966
|
+
// Export dashboard with live widget states, optionally filtering by selection
|
|
2967
|
+
return this.#store.exportDashboard(() => this.getCurrentWidgetStates(), selection, options);
|
|
2182
2968
|
}
|
|
2183
2969
|
loadDashboard(data) {
|
|
2184
2970
|
this.#store.loadDashboard(data);
|
|
@@ -2200,25 +2986,24 @@ class DashboardComponent {
|
|
|
2200
2986
|
}
|
|
2201
2987
|
this.#isPreservingStates = true;
|
|
2202
2988
|
try {
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
currentWidgetStates = viewer.getCurrentWidgetStates();
|
|
2989
|
+
const stateMap = new Map();
|
|
2990
|
+
// Get cell components from the previously active child
|
|
2991
|
+
const cells = previousEditMode
|
|
2992
|
+
? this.dashboardEditor()?.cellComponents()
|
|
2993
|
+
: this.dashboardViewer()?.cellComponents();
|
|
2994
|
+
if (cells) {
|
|
2995
|
+
for (const cell of cells) {
|
|
2996
|
+
const cellId = cell.cellId();
|
|
2997
|
+
const currentState = cell.getCurrentWidgetState();
|
|
2998
|
+
if (currentState !== undefined) {
|
|
2999
|
+
stateMap.set(CellIdUtils.toString(cellId), currentState);
|
|
3000
|
+
}
|
|
2216
3001
|
}
|
|
2217
3002
|
}
|
|
2218
3003
|
// Update the store with the live widget states using untracked to avoid triggering effects
|
|
2219
|
-
if (
|
|
3004
|
+
if (stateMap.size > 0) {
|
|
2220
3005
|
untracked(() => {
|
|
2221
|
-
this.#store.updateAllWidgetStates(
|
|
3006
|
+
this.#store.updateAllWidgetStates(stateMap);
|
|
2222
3007
|
});
|
|
2223
3008
|
}
|
|
2224
3009
|
}
|
|
@@ -2227,7 +3012,7 @@ class DashboardComponent {
|
|
|
2227
3012
|
}
|
|
2228
3013
|
}
|
|
2229
3014
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2230
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DashboardComponent, isStandalone: true, selector: "ngx-dashboard", inputs: { dashboardData: { classPropertyName: "dashboardData", publicName: "dashboardData", isSignal: true, isRequired: true, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null }, reservedSpace: { classPropertyName: "reservedSpace", publicName: "reservedSpace", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.--rows": "store.rows()", "style.--columns": "store.columns()", "style.--gutter-size": "store.gutterSize()", "style.--gutters": "store.columns() + 1", "class.is-edit-mode": "editMode()", "style.max-width.px": "viewport.constraints().maxWidth", "style.max-height.px": "viewport.constraints().maxHeight" } }, providers: [DashboardStore, DashboardViewportService], viewQueries: [{ propertyName: "dashboardEditor", first: true, predicate: DashboardEditorComponent, descendants: true, isSignal: true }, { propertyName: "dashboardViewer", first: true, predicate: DashboardViewerComponent, descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<!-- dashboard.component.html -->\r\n<div class=\"grid-container\">\r\n @if (editMode()) {\r\n <!-- Full editor with drag & drop capabilities -->\r\n <ngx-dashboard-editor\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-editor>\r\n } @else {\r\n <!-- Read-only viewer -->\r\n <ngx-dashboard-viewer\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-viewer>\r\n }\r\n</div>\r\n", styles: [":host{display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}.grid-container{position:relative;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: DashboardViewerComponent, selector: "ngx-dashboard-viewer", inputs: ["rows", "columns", "gutterSize"] }, { kind: "component", type: DashboardEditorComponent, selector: "ngx-dashboard-editor", inputs: ["rows", "columns", "gutterSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3015
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DashboardComponent, isStandalone: true, selector: "ngx-dashboard", inputs: { dashboardData: { classPropertyName: "dashboardData", publicName: "dashboardData", isSignal: true, isRequired: true, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null }, reservedSpace: { classPropertyName: "reservedSpace", publicName: "reservedSpace", isSignal: true, isRequired: false, transformFunction: null }, enableSelection: { classPropertyName: "enableSelection", publicName: "enableSelection", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionComplete: "selectionComplete" }, host: { properties: { "style.--rows": "store.rows()", "style.--columns": "store.columns()", "style.--gutter-size": "store.gutterSize()", "style.--gutters": "store.columns() + 1", "class.is-edit-mode": "editMode()", "style.max-width.px": "viewport.constraints().maxWidth", "style.max-height.px": "viewport.constraints().maxHeight" } }, providers: [DashboardStore, DashboardViewportService], viewQueries: [{ propertyName: "dashboardEditor", first: true, predicate: DashboardEditorComponent, descendants: true, isSignal: true }, { propertyName: "dashboardViewer", first: true, predicate: DashboardViewerComponent, descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<!-- dashboard.component.html -->\r\n<div class=\"grid-container\">\r\n @if (editMode()) {\r\n <!-- Full editor with drag & drop capabilities -->\r\n <ngx-dashboard-editor\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-editor>\r\n } @else {\r\n <!-- Read-only viewer -->\r\n <ngx-dashboard-viewer\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n [enableSelection]=\"enableSelection()\"\r\n (selectionComplete)=\"selectionComplete.emit($event)\"\r\n ></ngx-dashboard-viewer>\r\n }\r\n</div>\r\n", styles: [":host{display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}.grid-container{position:relative;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: DashboardViewerComponent, selector: "ngx-dashboard-viewer", inputs: ["rows", "columns", "gutterSize", "enableSelection"], outputs: ["selectionComplete"] }, { kind: "component", type: DashboardEditorComponent, selector: "ngx-dashboard-editor", inputs: ["rows", "columns", "gutterSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2231
3016
|
}
|
|
2232
3017
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardComponent, decorators: [{
|
|
2233
3018
|
type: Component,
|
|
@@ -2239,7 +3024,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
|
|
|
2239
3024
|
'[class.is-edit-mode]': 'editMode()',
|
|
2240
3025
|
'[style.max-width.px]': 'viewport.constraints().maxWidth',
|
|
2241
3026
|
'[style.max-height.px]': 'viewport.constraints().maxHeight',
|
|
2242
|
-
}, template: "<!-- dashboard.component.html -->\r\n<div class=\"grid-container\">\r\n @if (editMode()) {\r\n <!-- Full editor with drag & drop capabilities -->\r\n <ngx-dashboard-editor\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-editor>\r\n } @else {\r\n <!-- Read-only viewer -->\r\n <ngx-dashboard-viewer\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-viewer>\r\n }\r\n</div>\r\n", styles: [":host{display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}.grid-container{position:relative;width:100%;height:100%}\n"] }]
|
|
3027
|
+
}, template: "<!-- dashboard.component.html -->\r\n<div class=\"grid-container\">\r\n @if (editMode()) {\r\n <!-- Full editor with drag & drop capabilities -->\r\n <ngx-dashboard-editor\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-editor>\r\n } @else {\r\n <!-- Read-only viewer -->\r\n <ngx-dashboard-viewer\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n [enableSelection]=\"enableSelection()\"\r\n (selectionComplete)=\"selectionComplete.emit($event)\"\r\n ></ngx-dashboard-viewer>\r\n }\r\n</div>\r\n", styles: [":host{display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}.grid-container{position:relative;width:100%;height:100%}\n"] }]
|
|
2243
3028
|
}], ctorParameters: () => [] });
|
|
2244
3029
|
|
|
2245
3030
|
// widget-list.component.ts
|
|
@@ -2248,6 +3033,8 @@ class WidgetListComponent {
|
|
|
2248
3033
|
#sanitizer = inject(DomSanitizer);
|
|
2249
3034
|
#renderer = inject(Renderer2);
|
|
2250
3035
|
#bridge = inject(DashboardBridgeService);
|
|
3036
|
+
// Input to track collapsed state for tooltip display
|
|
3037
|
+
collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
|
|
2251
3038
|
activeWidget = signal(null, ...(ngDevMode ? [{ debugName: "activeWidget" }] : []));
|
|
2252
3039
|
// Get grid cell dimensions from bridge service (uses first available dashboard)
|
|
2253
3040
|
gridCellDimensions = this.#bridge.availableDimensions;
|
|
@@ -2307,21 +3094,21 @@ class WidgetListComponent {
|
|
|
2307
3094
|
return $localize `:@@ngx.dashboard.widget.list.item.ariaLabel:${widget.name} widget: ${widget.description}`;
|
|
2308
3095
|
}
|
|
2309
3096
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2310
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: WidgetListComponent, isStandalone: true, selector: "ngx-dashboard-widget-list", ngImport: i0, template: "<!-- widget-list.component.html -->\r\n<div\r\n class=\"widget-list\"\r\n role=\"list\"\r\n i18n-aria-label=\"@@ngx.dashboard.widget.list.available\"\r\n aria-label=\"Available widgets\"\r\n>\r\n @for (widget of widgets(); track widget.widgetTypeid) {\r\n <div\r\n class=\"widget-list-item\"\r\n [class.active]=\"activeWidget() === widget.widgetTypeid\"\r\n draggable=\"true\"\r\n (dragstart)=\"onDragStart($event, widget)\"\r\n (dragend)=\"onDragEnd()\"\r\n role=\"listitem\"\r\n [attr.aria-grabbed]=\"activeWidget() === widget.widgetTypeid\"\r\n [attr.aria-label]=\"getWidgetAriaLabel(widget)\"\r\n tabindex=\"0\"\r\n >\r\n <div class=\"icon\" [innerHTML]=\"widget.safeSvgIcon\" aria-hidden=\"true\"></div>\r\n <div class=\"content\">\r\n <strong>{{ widget.name }}</strong>\r\n <small>{{ widget.description }}</small>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [":host{background-color:var(--mat-sys-surface-container, #f5f5f5);container-type:inline-size}.widget-list{display:flex;flex-direction:column;gap:var(--mat-sys-spacing-2, 8px)}@container (max-width: 200px){.widget-list{gap:var(--mat-sys-spacing-1, 4px)}}@container (min-width: 400px){.widget-list{gap:var(--mat-sys-spacing-3, 12px)}}.widget-list-item{display:flex;align-items:start;gap:var(--mat-sys-spacing-3, 12px);background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);padding:var(--mat-sys-spacing-3, 12px);border-radius:var(--mat-sys-corner-small, 4px);cursor:grab;transition:background-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),border-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),box-shadow var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out);box-shadow:var(--mat-sys-elevation-level1, 0 1px 2px rgba(0, 0, 0, .05))}@container (max-width: 200px){.widget-list-item{padding:var(--mat-sys-spacing-2, 8px);gap:var(--mat-sys-spacing-2, 8px)}}@container (min-width: 400px){.widget-list-item{padding:var(--mat-sys-spacing-4, 16px);gap:var(--mat-sys-spacing-4, 16px)}}.widget-list-item .icon{width:clamp(20px,4vw,28px);height:clamp(20px,4vw,28px);flex-shrink:0;color:var(--mat-sys-on-surface-variant, #5f5f5f);transition:color var(--mat-sys-motion-duration-short2, .15s) var(--mat-sys-motion-easing-standard, ease-in-out)}.widget-list-item .icon ::ng-deep svg{width:100%;height:100%;display:block}.widget-list-item .content{display:flex;flex-direction:column;line-height:1.2;color:var(--mat-sys-on-surface, #1c1c1c);flex:1;min-width:0}.widget-list-item .content strong{color:var(--mat-sys-on-surface, #1c1c1c);font-weight:500;font-size:clamp(.875rem,2.5vw,1rem);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item .content small{color:var(--mat-sys-on-surface-variant, #5f5f5f);font-size:clamp(.75rem,2vw,.875rem);margin-top:var(--mat-sys-spacing-1, 4px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item:hover{background-color:var(--mat-sys-surface-container-low, #f0f0f0);box-shadow:var(--mat-sys-elevation-level2, 0 2px 4px rgba(0, 0, 0, .1))}.widget-list-item:hover .icon{color:var(--mat-sys-on-surface, #1c1c1c)}.widget-list-item:active{cursor:grabbing;background-color:var(--mat-sys-surface-container, #f5f5f5)}.widget-list-item.active{background-color:var(--mat-sys-primary-container, #e6f2ff);border-color:var(--mat-sys-primary, #1976d2);color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content strong{color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content small{color:var(--mat-sys-on-primary-container, #004a99);opacity:.8}.widget-list-item.active .icon{color:var(--mat-sys-on-primary-container, #004a99)}.drag-ghost{position:absolute;top:0;left:0;z-index:9999;margin:0;pointer-events:none;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);border-radius:var(--mat-sys-corner-small, 4px);box-shadow:var(--mat-sys-elevation-level3, 0 4px 6px rgba(0, 0, 0, .15));opacity:.8}.drag-ghost .icon{display:flex;align-items:center;justify-content:center;color:var(--mat-sys-on-surface-variant, #5f5f5f);opacity:.6}.drag-ghost .icon ::ng-deep svg{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3097
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: WidgetListComponent, isStandalone: true, selector: "ngx-dashboard-widget-list", inputs: { collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!-- widget-list.component.html -->\r\n<div\r\n class=\"widget-list\"\r\n role=\"list\"\r\n i18n-aria-label=\"@@ngx.dashboard.widget.list.available\"\r\n aria-label=\"Available widgets\"\r\n>\r\n @for (widget of widgets(); track widget.widgetTypeid) {\r\n <div\r\n class=\"widget-list-item\"\r\n [class.active]=\"activeWidget() === widget.widgetTypeid\"\r\n draggable=\"true\"\r\n (dragstart)=\"onDragStart($event, widget)\"\r\n (dragend)=\"onDragEnd()\"\r\n role=\"listitem\"\r\n [attr.aria-grabbed]=\"activeWidget() === widget.widgetTypeid\"\r\n [attr.aria-label]=\"getWidgetAriaLabel(widget)\"\r\n [matTooltip]=\"widget.description\"\r\n [matTooltipDisabled]=\"!collapsed()\"\r\n matTooltipPosition=\"right\"\r\n tabindex=\"0\"\r\n >\r\n <div class=\"icon\" [innerHTML]=\"widget.safeSvgIcon\" aria-hidden=\"true\"></div>\r\n <div class=\"content\">\r\n <strong>{{ widget.name }}</strong>\r\n <small>{{ widget.description }}</small>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [":host{background-color:var(--mat-sys-surface-container, #f5f5f5);container-type:inline-size}.widget-list{display:flex;flex-direction:column;gap:var(--mat-sys-spacing-2, 8px)}@container (max-width: 200px){.widget-list{gap:var(--mat-sys-spacing-1, 4px)}}@container (min-width: 400px){.widget-list{gap:var(--mat-sys-spacing-3, 12px)}}.widget-list-item{display:flex;align-items:start;gap:var(--mat-sys-spacing-3, 12px);background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);padding:var(--mat-sys-spacing-3, 12px);border-radius:var(--mat-sys-corner-small, 4px);cursor:grab;transition:background-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),border-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),box-shadow var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out);box-shadow:var(--mat-sys-elevation-level1, 0 1px 2px rgba(0, 0, 0, .05))}@container (max-width: 200px){.widget-list-item{padding:var(--mat-sys-spacing-2, 8px);gap:var(--mat-sys-spacing-2, 8px)}}@container (min-width: 400px){.widget-list-item{padding:var(--mat-sys-spacing-4, 16px);gap:var(--mat-sys-spacing-4, 16px)}}.widget-list-item .icon{width:clamp(20px,4vw,28px);height:clamp(20px,4vw,28px);flex-shrink:0;color:var(--mat-sys-on-surface-variant, #5f5f5f);transition:color var(--mat-sys-motion-duration-short2, .15s) var(--mat-sys-motion-easing-standard, ease-in-out)}.widget-list-item .icon ::ng-deep svg{width:100%;height:100%;display:block}.widget-list-item .content{display:flex;flex-direction:column;line-height:1.2;color:var(--mat-sys-on-surface, #1c1c1c);flex:1;min-width:0}.widget-list-item .content strong{color:var(--mat-sys-on-surface, #1c1c1c);font-weight:500;font-size:clamp(.875rem,2.5vw,1rem);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item .content small{color:var(--mat-sys-on-surface-variant, #5f5f5f);font-size:clamp(.75rem,2vw,.875rem);margin-top:var(--mat-sys-spacing-1, 4px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item:hover{background-color:var(--mat-sys-surface-container-low, #f0f0f0);box-shadow:var(--mat-sys-elevation-level2, 0 2px 4px rgba(0, 0, 0, .1))}.widget-list-item:hover .icon{color:var(--mat-sys-on-surface, #1c1c1c)}.widget-list-item:active{cursor:grabbing;background-color:var(--mat-sys-surface-container, #f5f5f5)}.widget-list-item.active{background-color:var(--mat-sys-primary-container, #e6f2ff);border-color:var(--mat-sys-primary, #1976d2);color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content strong{color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content small{color:var(--mat-sys-on-primary-container, #004a99);opacity:.8}.widget-list-item.active .icon{color:var(--mat-sys-on-primary-container, #004a99)}.drag-ghost{position:absolute;top:0;left:0;z-index:9999;margin:0;pointer-events:none;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);border-radius:var(--mat-sys-corner-small, 4px);box-shadow:var(--mat-sys-elevation-level3, 0 4px 6px rgba(0, 0, 0, .15));opacity:.8}.drag-ghost .icon{display:flex;align-items:center;justify-content:center;color:var(--mat-sys-on-surface-variant, #5f5f5f);opacity:.6}.drag-ghost .icon ::ng-deep svg{display:block}\n"], dependencies: [{ kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2311
3098
|
}
|
|
2312
3099
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListComponent, decorators: [{
|
|
2313
3100
|
type: Component,
|
|
2314
|
-
args: [{ selector: 'ngx-dashboard-widget-list', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- widget-list.component.html -->\r\n<div\r\n class=\"widget-list\"\r\n role=\"list\"\r\n i18n-aria-label=\"@@ngx.dashboard.widget.list.available\"\r\n aria-label=\"Available widgets\"\r\n>\r\n @for (widget of widgets(); track widget.widgetTypeid) {\r\n <div\r\n class=\"widget-list-item\"\r\n [class.active]=\"activeWidget() === widget.widgetTypeid\"\r\n draggable=\"true\"\r\n (dragstart)=\"onDragStart($event, widget)\"\r\n (dragend)=\"onDragEnd()\"\r\n role=\"listitem\"\r\n [attr.aria-grabbed]=\"activeWidget() === widget.widgetTypeid\"\r\n [attr.aria-label]=\"getWidgetAriaLabel(widget)\"\r\n tabindex=\"0\"\r\n >\r\n <div class=\"icon\" [innerHTML]=\"widget.safeSvgIcon\" aria-hidden=\"true\"></div>\r\n <div class=\"content\">\r\n <strong>{{ widget.name }}</strong>\r\n <small>{{ widget.description }}</small>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [":host{background-color:var(--mat-sys-surface-container, #f5f5f5);container-type:inline-size}.widget-list{display:flex;flex-direction:column;gap:var(--mat-sys-spacing-2, 8px)}@container (max-width: 200px){.widget-list{gap:var(--mat-sys-spacing-1, 4px)}}@container (min-width: 400px){.widget-list{gap:var(--mat-sys-spacing-3, 12px)}}.widget-list-item{display:flex;align-items:start;gap:var(--mat-sys-spacing-3, 12px);background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);padding:var(--mat-sys-spacing-3, 12px);border-radius:var(--mat-sys-corner-small, 4px);cursor:grab;transition:background-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),border-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),box-shadow var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out);box-shadow:var(--mat-sys-elevation-level1, 0 1px 2px rgba(0, 0, 0, .05))}@container (max-width: 200px){.widget-list-item{padding:var(--mat-sys-spacing-2, 8px);gap:var(--mat-sys-spacing-2, 8px)}}@container (min-width: 400px){.widget-list-item{padding:var(--mat-sys-spacing-4, 16px);gap:var(--mat-sys-spacing-4, 16px)}}.widget-list-item .icon{width:clamp(20px,4vw,28px);height:clamp(20px,4vw,28px);flex-shrink:0;color:var(--mat-sys-on-surface-variant, #5f5f5f);transition:color var(--mat-sys-motion-duration-short2, .15s) var(--mat-sys-motion-easing-standard, ease-in-out)}.widget-list-item .icon ::ng-deep svg{width:100%;height:100%;display:block}.widget-list-item .content{display:flex;flex-direction:column;line-height:1.2;color:var(--mat-sys-on-surface, #1c1c1c);flex:1;min-width:0}.widget-list-item .content strong{color:var(--mat-sys-on-surface, #1c1c1c);font-weight:500;font-size:clamp(.875rem,2.5vw,1rem);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item .content small{color:var(--mat-sys-on-surface-variant, #5f5f5f);font-size:clamp(.75rem,2vw,.875rem);margin-top:var(--mat-sys-spacing-1, 4px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item:hover{background-color:var(--mat-sys-surface-container-low, #f0f0f0);box-shadow:var(--mat-sys-elevation-level2, 0 2px 4px rgba(0, 0, 0, .1))}.widget-list-item:hover .icon{color:var(--mat-sys-on-surface, #1c1c1c)}.widget-list-item:active{cursor:grabbing;background-color:var(--mat-sys-surface-container, #f5f5f5)}.widget-list-item.active{background-color:var(--mat-sys-primary-container, #e6f2ff);border-color:var(--mat-sys-primary, #1976d2);color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content strong{color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content small{color:var(--mat-sys-on-primary-container, #004a99);opacity:.8}.widget-list-item.active .icon{color:var(--mat-sys-on-primary-container, #004a99)}.drag-ghost{position:absolute;top:0;left:0;z-index:9999;margin:0;pointer-events:none;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);border-radius:var(--mat-sys-corner-small, 4px);box-shadow:var(--mat-sys-elevation-level3, 0 4px 6px rgba(0, 0, 0, .15));opacity:.8}.drag-ghost .icon{display:flex;align-items:center;justify-content:center;color:var(--mat-sys-on-surface-variant, #5f5f5f);opacity:.6}.drag-ghost .icon ::ng-deep svg{display:block}\n"] }]
|
|
3101
|
+
args: [{ selector: 'ngx-dashboard-widget-list', standalone: true, imports: [MatTooltipModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- widget-list.component.html -->\r\n<div\r\n class=\"widget-list\"\r\n role=\"list\"\r\n i18n-aria-label=\"@@ngx.dashboard.widget.list.available\"\r\n aria-label=\"Available widgets\"\r\n>\r\n @for (widget of widgets(); track widget.widgetTypeid) {\r\n <div\r\n class=\"widget-list-item\"\r\n [class.active]=\"activeWidget() === widget.widgetTypeid\"\r\n draggable=\"true\"\r\n (dragstart)=\"onDragStart($event, widget)\"\r\n (dragend)=\"onDragEnd()\"\r\n role=\"listitem\"\r\n [attr.aria-grabbed]=\"activeWidget() === widget.widgetTypeid\"\r\n [attr.aria-label]=\"getWidgetAriaLabel(widget)\"\r\n [matTooltip]=\"widget.description\"\r\n [matTooltipDisabled]=\"!collapsed()\"\r\n matTooltipPosition=\"right\"\r\n tabindex=\"0\"\r\n >\r\n <div class=\"icon\" [innerHTML]=\"widget.safeSvgIcon\" aria-hidden=\"true\"></div>\r\n <div class=\"content\">\r\n <strong>{{ widget.name }}</strong>\r\n <small>{{ widget.description }}</small>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [":host{background-color:var(--mat-sys-surface-container, #f5f5f5);container-type:inline-size}.widget-list{display:flex;flex-direction:column;gap:var(--mat-sys-spacing-2, 8px)}@container (max-width: 200px){.widget-list{gap:var(--mat-sys-spacing-1, 4px)}}@container (min-width: 400px){.widget-list{gap:var(--mat-sys-spacing-3, 12px)}}.widget-list-item{display:flex;align-items:start;gap:var(--mat-sys-spacing-3, 12px);background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);padding:var(--mat-sys-spacing-3, 12px);border-radius:var(--mat-sys-corner-small, 4px);cursor:grab;transition:background-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),border-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),box-shadow var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out);box-shadow:var(--mat-sys-elevation-level1, 0 1px 2px rgba(0, 0, 0, .05))}@container (max-width: 200px){.widget-list-item{padding:var(--mat-sys-spacing-2, 8px);gap:var(--mat-sys-spacing-2, 8px)}}@container (min-width: 400px){.widget-list-item{padding:var(--mat-sys-spacing-4, 16px);gap:var(--mat-sys-spacing-4, 16px)}}.widget-list-item .icon{width:clamp(20px,4vw,28px);height:clamp(20px,4vw,28px);flex-shrink:0;color:var(--mat-sys-on-surface-variant, #5f5f5f);transition:color var(--mat-sys-motion-duration-short2, .15s) var(--mat-sys-motion-easing-standard, ease-in-out)}.widget-list-item .icon ::ng-deep svg{width:100%;height:100%;display:block}.widget-list-item .content{display:flex;flex-direction:column;line-height:1.2;color:var(--mat-sys-on-surface, #1c1c1c);flex:1;min-width:0}.widget-list-item .content strong{color:var(--mat-sys-on-surface, #1c1c1c);font-weight:500;font-size:clamp(.875rem,2.5vw,1rem);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item .content small{color:var(--mat-sys-on-surface-variant, #5f5f5f);font-size:clamp(.75rem,2vw,.875rem);margin-top:var(--mat-sys-spacing-1, 4px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item:hover{background-color:var(--mat-sys-surface-container-low, #f0f0f0);box-shadow:var(--mat-sys-elevation-level2, 0 2px 4px rgba(0, 0, 0, .1))}.widget-list-item:hover .icon{color:var(--mat-sys-on-surface, #1c1c1c)}.widget-list-item:active{cursor:grabbing;background-color:var(--mat-sys-surface-container, #f5f5f5)}.widget-list-item.active{background-color:var(--mat-sys-primary-container, #e6f2ff);border-color:var(--mat-sys-primary, #1976d2);color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content strong{color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content small{color:var(--mat-sys-on-primary-container, #004a99);opacity:.8}.widget-list-item.active .icon{color:var(--mat-sys-on-primary-container, #004a99)}.drag-ghost{position:absolute;top:0;left:0;z-index:9999;margin:0;pointer-events:none;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);border-radius:var(--mat-sys-corner-small, 4px);box-shadow:var(--mat-sys-elevation-level3, 0 4px 6px rgba(0, 0, 0, .15));opacity:.8}.drag-ghost .icon{display:flex;align-items:center;justify-content:center;color:var(--mat-sys-on-surface-variant, #5f5f5f);opacity:.6}.drag-ghost .icon ::ng-deep svg{display:block}\n"] }]
|
|
2315
3102
|
}] });
|
|
2316
3103
|
|
|
2317
3104
|
/*
|
|
2318
3105
|
* Public API Surface of ngx-dashboard
|
|
2319
3106
|
*/
|
|
2320
|
-
//
|
|
3107
|
+
// Library version
|
|
2321
3108
|
|
|
2322
3109
|
/**
|
|
2323
3110
|
* Generated bundle index. Do not edit.
|
|
2324
3111
|
*/
|
|
2325
3112
|
|
|
2326
|
-
export { CELL_SETTINGS_DIALOG_PROVIDER, CellSettingsDialogProvider, DashboardComponent, DashboardService, DefaultCellSettingsDialogProvider, WidgetListComponent, createDefaultDashboard, createEmptyDashboard };
|
|
3113
|
+
export { CELL_SETTINGS_DIALOG_PROVIDER, CellSettingsDialogProvider, DashboardComponent, DashboardService, DefaultCellSettingsDialogProvider, DefaultEmptyCellContextProvider, EMPTY_CELL_CONTEXT_PROVIDER, EmptyCellContextProvider, NGX_DASHBOARD_VERSION, WidgetListComponent, WidgetListContextMenuProvider, createDefaultDashboard, createEmptyDashboard };
|
|
2327
3114
|
//# sourceMappingURL=dragonworks-ngx-dashboard.mjs.map
|