@ngx-km/grid 0.0.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.
- package/README.md +310 -0
- package/fesm2022/ngx-km-grid.mjs +820 -0
- package/fesm2022/ngx-km-grid.mjs.map +1 -0
- package/index.d.ts +362 -0
- package/package.json +24 -0
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, ElementRef, input, Directive, computed, Component, InjectionToken, Injector, DestroyRef, signal, output, effect, untracked } from '@angular/core';
|
|
3
|
+
import { NgStyle, NgComponentOutlet } from '@angular/common';
|
|
4
|
+
import { DomSanitizer } from '@angular/platform-browser';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Directive that automatically registers/unregisters grid elements for dimension tracking
|
|
8
|
+
*/
|
|
9
|
+
class GridElementRefDirective {
|
|
10
|
+
elementRef = inject(ElementRef);
|
|
11
|
+
gridComponent = inject(GridComponent);
|
|
12
|
+
/** The element ID to register */
|
|
13
|
+
gridElementRef = input.required(...(ngDevMode ? [{ debugName: "gridElementRef" }] : []));
|
|
14
|
+
ngOnInit() {
|
|
15
|
+
this.gridComponent.registerElement(this.gridElementRef(), this.elementRef.nativeElement);
|
|
16
|
+
}
|
|
17
|
+
ngOnDestroy() {
|
|
18
|
+
this.gridComponent.unregisterElement(this.gridElementRef());
|
|
19
|
+
}
|
|
20
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GridElementRefDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
21
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.15", type: GridElementRefDirective, isStandalone: true, selector: "[gridElementRef]", inputs: { gridElementRef: { classPropertyName: "gridElementRef", publicName: "gridElementRef", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
22
|
+
}
|
|
23
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GridElementRefDirective, decorators: [{
|
|
24
|
+
type: Directive,
|
|
25
|
+
args: [{
|
|
26
|
+
selector: '[gridElementRef]',
|
|
27
|
+
standalone: true,
|
|
28
|
+
}]
|
|
29
|
+
}], propDecorators: { gridElementRef: [{ type: i0.Input, args: [{ isSignal: true, alias: "gridElementRef", required: true }] }] } });
|
|
30
|
+
|
|
31
|
+
class ToolbarComponent {
|
|
32
|
+
sanitizer = inject(DomSanitizer);
|
|
33
|
+
/** Toolbar configuration */
|
|
34
|
+
config = input.required(...(ngDevMode ? [{ debugName: "config" }] : []));
|
|
35
|
+
/** Computed host classes based on position */
|
|
36
|
+
hostClasses = computed(() => {
|
|
37
|
+
const cfg = this.config();
|
|
38
|
+
const posClass = `pos-${cfg.position}`;
|
|
39
|
+
return cfg.className ? `${posClass} ${cfg.className}` : posClass;
|
|
40
|
+
}, ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
|
|
41
|
+
/** Computed classes for the toolbar div */
|
|
42
|
+
toolbarClasses = computed(() => {
|
|
43
|
+
return '';
|
|
44
|
+
}, ...(ngDevMode ? [{ debugName: "toolbarClasses" }] : []));
|
|
45
|
+
/** Sanitize HTML for safe rendering of SVG icons */
|
|
46
|
+
sanitizeHtml(html) {
|
|
47
|
+
return this.sanitizer.bypassSecurityTrustHtml(html);
|
|
48
|
+
}
|
|
49
|
+
/** Handle button click */
|
|
50
|
+
onButtonClick(button, event) {
|
|
51
|
+
event.stopPropagation();
|
|
52
|
+
if (!button.disabled && button.onClick) {
|
|
53
|
+
button.onClick();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
57
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: ToolbarComponent, isStandalone: true, selector: "ngx-grid-toolbar", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "class": "hostClasses()" } }, ngImport: i0, template: `
|
|
58
|
+
<div class="toolbar" [class]="toolbarClasses()">
|
|
59
|
+
@for (button of config().buttons; track button.id) {
|
|
60
|
+
<button
|
|
61
|
+
class="toolbar-btn"
|
|
62
|
+
[class.active]="button.active"
|
|
63
|
+
[class.disabled]="button.disabled"
|
|
64
|
+
[disabled]="button.disabled"
|
|
65
|
+
[title]="button.label"
|
|
66
|
+
(click)="onButtonClick(button, $event)">
|
|
67
|
+
@if (button.icon) {
|
|
68
|
+
<span class="btn-icon" [innerHTML]="sanitizeHtml(button.icon)"></span>
|
|
69
|
+
} @else {
|
|
70
|
+
<span class="btn-label">{{ button.label }}</span>
|
|
71
|
+
}
|
|
72
|
+
</button>
|
|
73
|
+
}
|
|
74
|
+
</div>
|
|
75
|
+
`, isInline: true, styles: [":host{position:absolute;z-index:100;pointer-events:none}.toolbar{display:flex;gap:4px;padding:6px;background:#1e1e2ef2;border:1px solid #444;border-radius:6px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);pointer-events:auto}:host(.pos-top-left){top:12px;left:12px}:host(.pos-top-center){top:12px;left:50%;transform:translate(-50%)}:host(.pos-top-right){top:12px;right:12px}:host(.pos-bottom-left){bottom:12px;left:12px}:host(.pos-bottom-center){bottom:12px;left:50%;transform:translate(-50%)}:host(.pos-bottom-right){bottom:12px;right:12px}.toolbar-btn{display:flex;align-items:center;justify-content:center;min-width:32px;height:32px;padding:0 8px;border:1px solid #555;background:#2a2a3e;color:#fff;border-radius:4px;cursor:pointer;transition:all .15s ease;font-size:.875rem}.toolbar-btn:hover:not(.disabled){background:#3a3a4e;border-color:#666}.toolbar-btn.active{background:#4a4a6e;border-color:#6a6a8e}.toolbar-btn.disabled{opacity:.5;cursor:not-allowed}.btn-icon{display:flex;align-items:center;justify-content:center;width:18px;height:18px;pointer-events:none}.btn-icon ::ng-deep svg{width:100%;height:100%;pointer-events:none}.btn-label{white-space:nowrap}\n"] });
|
|
76
|
+
}
|
|
77
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ToolbarComponent, decorators: [{
|
|
78
|
+
type: Component,
|
|
79
|
+
args: [{ selector: 'ngx-grid-toolbar', standalone: true, template: `
|
|
80
|
+
<div class="toolbar" [class]="toolbarClasses()">
|
|
81
|
+
@for (button of config().buttons; track button.id) {
|
|
82
|
+
<button
|
|
83
|
+
class="toolbar-btn"
|
|
84
|
+
[class.active]="button.active"
|
|
85
|
+
[class.disabled]="button.disabled"
|
|
86
|
+
[disabled]="button.disabled"
|
|
87
|
+
[title]="button.label"
|
|
88
|
+
(click)="onButtonClick(button, $event)">
|
|
89
|
+
@if (button.icon) {
|
|
90
|
+
<span class="btn-icon" [innerHTML]="sanitizeHtml(button.icon)"></span>
|
|
91
|
+
} @else {
|
|
92
|
+
<span class="btn-label">{{ button.label }}</span>
|
|
93
|
+
}
|
|
94
|
+
</button>
|
|
95
|
+
}
|
|
96
|
+
</div>
|
|
97
|
+
`, host: {
|
|
98
|
+
'[class]': 'hostClasses()',
|
|
99
|
+
}, styles: [":host{position:absolute;z-index:100;pointer-events:none}.toolbar{display:flex;gap:4px;padding:6px;background:#1e1e2ef2;border:1px solid #444;border-radius:6px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);pointer-events:auto}:host(.pos-top-left){top:12px;left:12px}:host(.pos-top-center){top:12px;left:50%;transform:translate(-50%)}:host(.pos-top-right){top:12px;right:12px}:host(.pos-bottom-left){bottom:12px;left:12px}:host(.pos-bottom-center){bottom:12px;left:50%;transform:translate(-50%)}:host(.pos-bottom-right){bottom:12px;right:12px}.toolbar-btn{display:flex;align-items:center;justify-content:center;min-width:32px;height:32px;padding:0 8px;border:1px solid #555;background:#2a2a3e;color:#fff;border-radius:4px;cursor:pointer;transition:all .15s ease;font-size:.875rem}.toolbar-btn:hover:not(.disabled){background:#3a3a4e;border-color:#666}.toolbar-btn.active{background:#4a4a6e;border-color:#6a6a8e}.toolbar-btn.disabled{opacity:.5;cursor:not-allowed}.btn-icon{display:flex;align-items:center;justify-content:center;width:18px;height:18px;pointer-events:none}.btn-icon ::ng-deep svg{width:100%;height:100%;pointer-events:none}.btn-label{white-space:nowrap}\n"] }]
|
|
100
|
+
}], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }] } });
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Injection token for accessing element data in dynamically rendered components.
|
|
104
|
+
* The data can be either a plain value or a Signal for reactive updates.
|
|
105
|
+
*/
|
|
106
|
+
const GRID_ELEMENT_DATA = new InjectionToken('GRID_ELEMENT_DATA');
|
|
107
|
+
/** Default cell size in pixels */
|
|
108
|
+
const DEFAULT_CELL_SIZE = 20;
|
|
109
|
+
/** Default background pattern color */
|
|
110
|
+
const DEFAULT_PATTERN_COLOR = '#3a3a5a';
|
|
111
|
+
/** Default minimum zoom level */
|
|
112
|
+
const DEFAULT_ZOOM_MIN = 0.1;
|
|
113
|
+
/** Default maximum zoom level */
|
|
114
|
+
const DEFAULT_ZOOM_MAX = 5;
|
|
115
|
+
/** Default zoom speed multiplier */
|
|
116
|
+
const DEFAULT_ZOOM_SPEED = 0.001;
|
|
117
|
+
/** Default background color */
|
|
118
|
+
const DEFAULT_BACKGROUND_COLOR = '#1a1a2e';
|
|
119
|
+
/**
|
|
120
|
+
* Default grid configuration
|
|
121
|
+
*/
|
|
122
|
+
const DEFAULT_GRID_CONFIG = {
|
|
123
|
+
dimensionMode: 'full',
|
|
124
|
+
backgroundMode: 'lines',
|
|
125
|
+
cellSize: DEFAULT_CELL_SIZE,
|
|
126
|
+
backgroundPatternColor: DEFAULT_PATTERN_COLOR,
|
|
127
|
+
backgroundPatternOpacity: 1,
|
|
128
|
+
backgroundColor: DEFAULT_BACKGROUND_COLOR,
|
|
129
|
+
panEnabled: true,
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Default viewport state
|
|
133
|
+
*/
|
|
134
|
+
const DEFAULT_VIEWPORT_STATE = {
|
|
135
|
+
x: 0,
|
|
136
|
+
y: 0,
|
|
137
|
+
zoom: 1,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
class GridComponent {
|
|
141
|
+
elementRef = inject(ElementRef);
|
|
142
|
+
injector = inject(Injector);
|
|
143
|
+
destroyRef = inject(DestroyRef);
|
|
144
|
+
/** Grid configuration */
|
|
145
|
+
config = input(DEFAULT_GRID_CONFIG, ...(ngDevMode ? [{ debugName: "config" }] : []));
|
|
146
|
+
/** Elements to render on the grid */
|
|
147
|
+
elements = input([], ...(ngDevMode ? [{ debugName: "elements" }] : []));
|
|
148
|
+
/** Current viewport state (pan offset and zoom) */
|
|
149
|
+
viewport = signal({ ...DEFAULT_VIEWPORT_STATE }, ...(ngDevMode ? [{ debugName: "viewport" }] : []));
|
|
150
|
+
/** Emits when viewport changes */
|
|
151
|
+
viewportChange = output();
|
|
152
|
+
/** Emits when element dimensions are measured or change */
|
|
153
|
+
elementsRendered = output();
|
|
154
|
+
/** Emits when an element is dragged to a new position */
|
|
155
|
+
elementPositionChange = output();
|
|
156
|
+
/** Map of element IDs to their DOM elements for dimension tracking */
|
|
157
|
+
elementRefs = new Map();
|
|
158
|
+
/** ResizeObserver for tracking element dimension changes */
|
|
159
|
+
resizeObserver = null;
|
|
160
|
+
/** Debounce timer for batching dimension updates */
|
|
161
|
+
dimensionUpdateTimer = null;
|
|
162
|
+
/** Internal state for pan tracking */
|
|
163
|
+
isPanning = signal(false, ...(ngDevMode ? [{ debugName: "isPanning" }] : []));
|
|
164
|
+
panStartX = 0;
|
|
165
|
+
panStartY = 0;
|
|
166
|
+
panStartViewportX = 0;
|
|
167
|
+
panStartViewportY = 0;
|
|
168
|
+
/** Internal state for element drag tracking */
|
|
169
|
+
isDraggingElement = signal(false, ...(ngDevMode ? [{ debugName: "isDraggingElement" }] : []));
|
|
170
|
+
draggingElementId = null;
|
|
171
|
+
dragStartX = 0;
|
|
172
|
+
dragStartY = 0;
|
|
173
|
+
dragStartElementX = 0;
|
|
174
|
+
dragStartElementY = 0;
|
|
175
|
+
/** Internal copy of element positions for drag updates */
|
|
176
|
+
elementPositions = new Map();
|
|
177
|
+
/** Track if we're currently dragging to avoid overwriting drag position */
|
|
178
|
+
syncPositionsFromInput = true;
|
|
179
|
+
constructor() {
|
|
180
|
+
// Sync element positions from input when elements change
|
|
181
|
+
// This allows external code to update positions programmatically
|
|
182
|
+
effect(() => {
|
|
183
|
+
const inputElements = this.elements();
|
|
184
|
+
untracked(() => {
|
|
185
|
+
// Skip sync if currently dragging
|
|
186
|
+
if (this.isDraggingElement())
|
|
187
|
+
return;
|
|
188
|
+
// Update internal positions from input
|
|
189
|
+
inputElements.forEach(el => {
|
|
190
|
+
const currentPos = this.elementPositions.get(el.id);
|
|
191
|
+
// Only update if position is different (allows external updates)
|
|
192
|
+
if (!currentPos || currentPos.x !== el.x || currentPos.y !== el.y) {
|
|
193
|
+
this.elementPositions.set(el.id, { x: el.x, y: el.y });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
// Remove positions for elements that no longer exist
|
|
197
|
+
const inputIds = new Set(inputElements.map(e => e.id));
|
|
198
|
+
for (const id of this.elementPositions.keys()) {
|
|
199
|
+
if (!inputIds.has(id)) {
|
|
200
|
+
this.elementPositions.delete(id);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/** Computed container styles based on config */
|
|
207
|
+
containerStyles = computed(() => {
|
|
208
|
+
const cfg = this.config();
|
|
209
|
+
const styles = {};
|
|
210
|
+
if (cfg.dimensionMode === 'fixed') {
|
|
211
|
+
if (cfg.width)
|
|
212
|
+
styles['width'] = `${cfg.width}px`;
|
|
213
|
+
if (cfg.height)
|
|
214
|
+
styles['height'] = `${cfg.height}px`;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
styles['width'] = '100%';
|
|
218
|
+
styles['height'] = '100%';
|
|
219
|
+
}
|
|
220
|
+
if (cfg.minWidth)
|
|
221
|
+
styles['min-width'] = `${cfg.minWidth}px`;
|
|
222
|
+
if (cfg.minHeight)
|
|
223
|
+
styles['min-height'] = `${cfg.minHeight}px`;
|
|
224
|
+
if (cfg.maxWidth)
|
|
225
|
+
styles['max-width'] = `${cfg.maxWidth}px`;
|
|
226
|
+
if (cfg.maxHeight)
|
|
227
|
+
styles['max-height'] = `${cfg.maxHeight}px`;
|
|
228
|
+
// Background color
|
|
229
|
+
styles['background-color'] = cfg.backgroundColor ?? DEFAULT_BACKGROUND_COLOR;
|
|
230
|
+
// Cursor style
|
|
231
|
+
const panEnabled = cfg.panEnabled ?? true;
|
|
232
|
+
if (panEnabled) {
|
|
233
|
+
styles['cursor'] = this.isPanning() ? 'grabbing' : 'grab';
|
|
234
|
+
}
|
|
235
|
+
return styles;
|
|
236
|
+
}, ...(ngDevMode ? [{ debugName: "containerStyles" }] : []));
|
|
237
|
+
/** Computed background styles for the pattern layer */
|
|
238
|
+
backgroundStyles = computed(() => {
|
|
239
|
+
const cfg = this.config();
|
|
240
|
+
const vp = this.viewport();
|
|
241
|
+
const mode = cfg.backgroundMode ?? 'lines';
|
|
242
|
+
const cellSize = cfg.cellSize ?? DEFAULT_CELL_SIZE;
|
|
243
|
+
const patternColor = cfg.backgroundPatternColor ?? DEFAULT_PATTERN_COLOR;
|
|
244
|
+
const opacity = cfg.backgroundPatternOpacity ?? 1;
|
|
245
|
+
const styles = {};
|
|
246
|
+
if (mode === 'none') {
|
|
247
|
+
return styles;
|
|
248
|
+
}
|
|
249
|
+
// Apply zoom to cell size
|
|
250
|
+
const scaledCellSize = cellSize * vp.zoom;
|
|
251
|
+
if (mode === 'lines') {
|
|
252
|
+
styles['background-image'] = `linear-gradient(to right, ${patternColor} 1px, transparent 1px), linear-gradient(to bottom, ${patternColor} 1px, transparent 1px)`;
|
|
253
|
+
styles['background-size'] = `${scaledCellSize}px ${scaledCellSize}px`;
|
|
254
|
+
}
|
|
255
|
+
else if (mode === 'dots') {
|
|
256
|
+
// Scale dot size with zoom
|
|
257
|
+
const dotSize = 1.5 * vp.zoom;
|
|
258
|
+
styles['background-image'] = `radial-gradient(circle, ${patternColor} ${dotSize}px, transparent ${dotSize}px)`;
|
|
259
|
+
styles['background-size'] = `${scaledCellSize}px ${scaledCellSize}px`;
|
|
260
|
+
}
|
|
261
|
+
// Apply pan offset to background position for seamless scrolling
|
|
262
|
+
styles['background-position'] = `${vp.x}px ${vp.y}px`;
|
|
263
|
+
styles['opacity'] = String(opacity);
|
|
264
|
+
return styles;
|
|
265
|
+
}, ...(ngDevMode ? [{ debugName: "backgroundStyles" }] : []));
|
|
266
|
+
/** Computed transform styles for the viewport layer (contains all elements) */
|
|
267
|
+
viewportTransform = computed(() => {
|
|
268
|
+
const vp = this.viewport();
|
|
269
|
+
return `translate(${vp.x}px, ${vp.y}px) scale(${vp.zoom})`;
|
|
270
|
+
}, ...(ngDevMode ? [{ debugName: "viewportTransform" }] : []));
|
|
271
|
+
/** Whether the integrated toolbar should be shown */
|
|
272
|
+
showToolbar = computed(() => !!this.config().toolbar, ...(ngDevMode ? [{ debugName: "showToolbar" }] : []));
|
|
273
|
+
/** Whether the loading overlay should be shown */
|
|
274
|
+
isLoading = computed(() => !!this.config().loading, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
275
|
+
/** Computed toolbar configuration for the integrated toolbar */
|
|
276
|
+
integratedToolbarConfig = computed(() => {
|
|
277
|
+
const cfg = this.config();
|
|
278
|
+
if (!cfg.toolbar)
|
|
279
|
+
return null;
|
|
280
|
+
const toolbarCfg = cfg.toolbar;
|
|
281
|
+
const buttons = [];
|
|
282
|
+
// Add reset view button if enabled (default: true)
|
|
283
|
+
if (toolbarCfg.showResetView !== false) {
|
|
284
|
+
buttons.push({
|
|
285
|
+
id: 'grid-reset-view',
|
|
286
|
+
label: 'Reset View',
|
|
287
|
+
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>',
|
|
288
|
+
onClick: () => this.resetViewport(),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
// Add zoom controls if enabled (default: true)
|
|
292
|
+
if (toolbarCfg.showZoomControls !== false) {
|
|
293
|
+
buttons.push({
|
|
294
|
+
id: 'grid-zoom-in',
|
|
295
|
+
label: 'Zoom In',
|
|
296
|
+
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>',
|
|
297
|
+
onClick: () => this.zoomIn(),
|
|
298
|
+
});
|
|
299
|
+
buttons.push({
|
|
300
|
+
id: 'grid-zoom-out',
|
|
301
|
+
label: 'Zoom Out',
|
|
302
|
+
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>',
|
|
303
|
+
onClick: () => this.zoomOut(),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
// Add custom buttons
|
|
307
|
+
if (toolbarCfg.customButtons?.length) {
|
|
308
|
+
buttons.push(...toolbarCfg.customButtons);
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
position: toolbarCfg.position ?? 'top-left',
|
|
312
|
+
buttons,
|
|
313
|
+
};
|
|
314
|
+
}, ...(ngDevMode ? [{ debugName: "integratedToolbarConfig" }] : []));
|
|
315
|
+
/** Get screen position styles for an element */
|
|
316
|
+
getElementStyles(element) {
|
|
317
|
+
// Use tracked position if dragging, otherwise use input position
|
|
318
|
+
const pos = this.elementPositions.get(element.id) ?? { x: element.x, y: element.y };
|
|
319
|
+
const cfg = this.config();
|
|
320
|
+
const dragEnabled = cfg.dragEnabled ?? true;
|
|
321
|
+
return {
|
|
322
|
+
position: 'absolute',
|
|
323
|
+
left: `${pos.x}px`,
|
|
324
|
+
top: `${pos.y}px`,
|
|
325
|
+
'transform-origin': 'top left',
|
|
326
|
+
cursor: dragEnabled ? (this.isDraggingElement() && this.draggingElementId === element.id ? 'grabbing' : 'grab') : 'default',
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/** Create an injector for passing data to a dynamically rendered component */
|
|
330
|
+
createElementInjector(element) {
|
|
331
|
+
return Injector.create({
|
|
332
|
+
providers: [
|
|
333
|
+
{ provide: GRID_ELEMENT_DATA, useValue: element.data },
|
|
334
|
+
],
|
|
335
|
+
parent: this.injector,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
/** Handle mouse down to start panning */
|
|
339
|
+
onMouseDown(event) {
|
|
340
|
+
const cfg = this.config();
|
|
341
|
+
if (!(cfg.panEnabled ?? true))
|
|
342
|
+
return;
|
|
343
|
+
// Only pan on left mouse button
|
|
344
|
+
if (event.button !== 0)
|
|
345
|
+
return;
|
|
346
|
+
this.isPanning.set(true);
|
|
347
|
+
this.panStartX = event.clientX;
|
|
348
|
+
this.panStartY = event.clientY;
|
|
349
|
+
const vp = this.viewport();
|
|
350
|
+
this.panStartViewportX = vp.x;
|
|
351
|
+
this.panStartViewportY = vp.y;
|
|
352
|
+
// Prevent text selection during drag
|
|
353
|
+
event.preventDefault();
|
|
354
|
+
// Add window-level listeners for move and up
|
|
355
|
+
window.addEventListener('mousemove', this.onMouseMove);
|
|
356
|
+
window.addEventListener('mouseup', this.onMouseUp);
|
|
357
|
+
}
|
|
358
|
+
/** Handle mouse move during panning */
|
|
359
|
+
onMouseMove = (event) => {
|
|
360
|
+
if (!this.isPanning())
|
|
361
|
+
return;
|
|
362
|
+
const deltaX = event.clientX - this.panStartX;
|
|
363
|
+
const deltaY = event.clientY - this.panStartY;
|
|
364
|
+
let newX = this.panStartViewportX + deltaX;
|
|
365
|
+
let newY = this.panStartViewportY + deltaY;
|
|
366
|
+
// Apply boundaries if configured
|
|
367
|
+
const cfg = this.config();
|
|
368
|
+
if (cfg.panMinX !== undefined)
|
|
369
|
+
newX = Math.max(cfg.panMinX, newX);
|
|
370
|
+
if (cfg.panMaxX !== undefined)
|
|
371
|
+
newX = Math.min(cfg.panMaxX, newX);
|
|
372
|
+
if (cfg.panMinY !== undefined)
|
|
373
|
+
newY = Math.max(cfg.panMinY, newY);
|
|
374
|
+
if (cfg.panMaxY !== undefined)
|
|
375
|
+
newY = Math.min(cfg.panMaxY, newY);
|
|
376
|
+
const currentVp = this.viewport();
|
|
377
|
+
const newViewport = { ...currentVp, x: newX, y: newY };
|
|
378
|
+
this.viewport.set(newViewport);
|
|
379
|
+
this.viewportChange.emit(newViewport);
|
|
380
|
+
};
|
|
381
|
+
/** Handle mouse up to stop panning */
|
|
382
|
+
onMouseUp = () => {
|
|
383
|
+
this.isPanning.set(false);
|
|
384
|
+
window.removeEventListener('mousemove', this.onMouseMove);
|
|
385
|
+
window.removeEventListener('mouseup', this.onMouseUp);
|
|
386
|
+
};
|
|
387
|
+
/** Handle mouse down on an element to start dragging */
|
|
388
|
+
onElementMouseDown(event, element) {
|
|
389
|
+
const cfg = this.config();
|
|
390
|
+
if (!(cfg.dragEnabled ?? true))
|
|
391
|
+
return;
|
|
392
|
+
// Only drag on left mouse button
|
|
393
|
+
if (event.button !== 0)
|
|
394
|
+
return;
|
|
395
|
+
// Stop propagation to prevent pan from starting
|
|
396
|
+
event.stopPropagation();
|
|
397
|
+
event.preventDefault();
|
|
398
|
+
// Initialize position tracking if not already done
|
|
399
|
+
if (!this.elementPositions.has(element.id)) {
|
|
400
|
+
this.elementPositions.set(element.id, { x: element.x, y: element.y });
|
|
401
|
+
}
|
|
402
|
+
const pos = this.elementPositions.get(element.id);
|
|
403
|
+
this.isDraggingElement.set(true);
|
|
404
|
+
this.draggingElementId = element.id;
|
|
405
|
+
this.dragStartX = event.clientX;
|
|
406
|
+
this.dragStartY = event.clientY;
|
|
407
|
+
this.dragStartElementX = pos.x;
|
|
408
|
+
this.dragStartElementY = pos.y;
|
|
409
|
+
// Add window-level listeners for move and up
|
|
410
|
+
window.addEventListener('mousemove', this.onElementMouseMove);
|
|
411
|
+
window.addEventListener('mouseup', this.onElementMouseUp);
|
|
412
|
+
}
|
|
413
|
+
/** Handle mouse move during element dragging */
|
|
414
|
+
onElementMouseMove = (event) => {
|
|
415
|
+
if (!this.isDraggingElement() || !this.draggingElementId)
|
|
416
|
+
return;
|
|
417
|
+
const cfg = this.config();
|
|
418
|
+
// Calculate delta in screen space, then convert to world space (account for zoom)
|
|
419
|
+
const vp = this.viewport();
|
|
420
|
+
const deltaX = (event.clientX - this.dragStartX) / vp.zoom;
|
|
421
|
+
const deltaY = (event.clientY - this.dragStartY) / vp.zoom;
|
|
422
|
+
let newX = this.dragStartElementX + deltaX;
|
|
423
|
+
let newY = this.dragStartElementY + deltaY;
|
|
424
|
+
// Apply snap if enabled
|
|
425
|
+
if (cfg.snapEnabled) {
|
|
426
|
+
const snapSize = cfg.snapGridSize ?? cfg.cellSize ?? DEFAULT_CELL_SIZE;
|
|
427
|
+
newX = Math.round(newX / snapSize) * snapSize;
|
|
428
|
+
newY = Math.round(newY / snapSize) * snapSize;
|
|
429
|
+
}
|
|
430
|
+
const previousPos = this.elementPositions.get(this.draggingElementId) ?? { x: this.dragStartElementX, y: this.dragStartElementY };
|
|
431
|
+
// Update internal position
|
|
432
|
+
this.elementPositions.set(this.draggingElementId, { x: newX, y: newY });
|
|
433
|
+
// Emit position change
|
|
434
|
+
this.elementPositionChange.emit({
|
|
435
|
+
id: this.draggingElementId,
|
|
436
|
+
x: newX,
|
|
437
|
+
y: newY,
|
|
438
|
+
previousX: previousPos.x,
|
|
439
|
+
previousY: previousPos.y,
|
|
440
|
+
});
|
|
441
|
+
};
|
|
442
|
+
/** Handle mouse up to stop element dragging */
|
|
443
|
+
onElementMouseUp = () => {
|
|
444
|
+
this.isDraggingElement.set(false);
|
|
445
|
+
this.draggingElementId = null;
|
|
446
|
+
window.removeEventListener('mousemove', this.onElementMouseMove);
|
|
447
|
+
window.removeEventListener('mouseup', this.onElementMouseUp);
|
|
448
|
+
};
|
|
449
|
+
/** Reset viewport to default position */
|
|
450
|
+
resetViewport() {
|
|
451
|
+
const newViewport = { ...DEFAULT_VIEWPORT_STATE };
|
|
452
|
+
this.viewport.set(newViewport);
|
|
453
|
+
this.viewportChange.emit(newViewport);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Fit all content to view
|
|
457
|
+
* Centers content in the viewport, optionally zooming out to fit everything
|
|
458
|
+
* @param fitZoom If true, zooms out if needed to fit all content (never zooms in beyond 1.0)
|
|
459
|
+
* @param padding Padding around content in pixels (default: 40)
|
|
460
|
+
*/
|
|
461
|
+
fitToView(fitZoom = false, padding = 40) {
|
|
462
|
+
const currentElements = this.elements();
|
|
463
|
+
if (currentElements.length === 0) {
|
|
464
|
+
this.resetViewport();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
// Calculate bounding box of all elements
|
|
468
|
+
const bounds = this.calculateContentBounds();
|
|
469
|
+
if (!bounds) {
|
|
470
|
+
this.resetViewport();
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
// Get container dimensions
|
|
474
|
+
const containerRect = this.elementRef.nativeElement.getBoundingClientRect();
|
|
475
|
+
const containerWidth = containerRect.width;
|
|
476
|
+
const containerHeight = containerRect.height;
|
|
477
|
+
// Available space for content (with padding)
|
|
478
|
+
const availableWidth = containerWidth - padding * 2;
|
|
479
|
+
const availableHeight = containerHeight - padding * 2;
|
|
480
|
+
let newZoom = 1;
|
|
481
|
+
if (fitZoom) {
|
|
482
|
+
// Calculate zoom needed to fit content
|
|
483
|
+
const scaleX = availableWidth / bounds.width;
|
|
484
|
+
const scaleY = availableHeight / bounds.height;
|
|
485
|
+
const fitScale = Math.min(scaleX, scaleY);
|
|
486
|
+
// Never zoom in beyond 1.0, only zoom out if needed
|
|
487
|
+
newZoom = Math.min(1, fitScale);
|
|
488
|
+
// Respect zoom limits
|
|
489
|
+
const cfg = this.config();
|
|
490
|
+
const zoomMin = cfg.zoomMin ?? DEFAULT_ZOOM_MIN;
|
|
491
|
+
newZoom = Math.max(zoomMin, newZoom);
|
|
492
|
+
}
|
|
493
|
+
// Calculate center of content in world space
|
|
494
|
+
const contentCenterX = bounds.x + bounds.width / 2;
|
|
495
|
+
const contentCenterY = bounds.y + bounds.height / 2;
|
|
496
|
+
// Calculate viewport offset to center content
|
|
497
|
+
// Container center = contentCenter * zoom + offset
|
|
498
|
+
// offset = containerCenter - contentCenter * zoom
|
|
499
|
+
const newX = containerWidth / 2 - contentCenterX * newZoom;
|
|
500
|
+
const newY = containerHeight / 2 - contentCenterY * newZoom;
|
|
501
|
+
const newViewport = {
|
|
502
|
+
x: newX,
|
|
503
|
+
y: newY,
|
|
504
|
+
zoom: newZoom,
|
|
505
|
+
};
|
|
506
|
+
this.viewport.set(newViewport);
|
|
507
|
+
this.viewportChange.emit(newViewport);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Check if an element is fully visible in the viewport
|
|
511
|
+
* @param elementId The element ID to check
|
|
512
|
+
* @param padding Padding from viewport edges (default: 0)
|
|
513
|
+
* @returns true if element is fully visible, false otherwise
|
|
514
|
+
*/
|
|
515
|
+
isElementVisible(elementId, padding = 0) {
|
|
516
|
+
const bounds = this.getElementBounds(elementId);
|
|
517
|
+
if (!bounds)
|
|
518
|
+
return false;
|
|
519
|
+
const vp = this.viewport();
|
|
520
|
+
const containerRect = this.elementRef.nativeElement.getBoundingClientRect();
|
|
521
|
+
// Convert element bounds to screen space
|
|
522
|
+
const screenLeft = bounds.x * vp.zoom + vp.x;
|
|
523
|
+
const screenTop = bounds.y * vp.zoom + vp.y;
|
|
524
|
+
const screenRight = (bounds.x + bounds.width) * vp.zoom + vp.x;
|
|
525
|
+
const screenBottom = (bounds.y + bounds.height) * vp.zoom + vp.y;
|
|
526
|
+
// Check if fully within viewport (with padding)
|
|
527
|
+
return (screenLeft >= padding &&
|
|
528
|
+
screenTop >= padding &&
|
|
529
|
+
screenRight <= containerRect.width - padding &&
|
|
530
|
+
screenBottom <= containerRect.height - padding);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Center viewport on a specific element (no zoom change)
|
|
534
|
+
* @param elementId The element ID to center on
|
|
535
|
+
*/
|
|
536
|
+
centerOnElement(elementId) {
|
|
537
|
+
const bounds = this.getElementBounds(elementId);
|
|
538
|
+
if (!bounds)
|
|
539
|
+
return;
|
|
540
|
+
const vp = this.viewport();
|
|
541
|
+
const containerRect = this.elementRef.nativeElement.getBoundingClientRect();
|
|
542
|
+
// Calculate element center in world space
|
|
543
|
+
const elementCenterX = bounds.x + bounds.width / 2;
|
|
544
|
+
const elementCenterY = bounds.y + bounds.height / 2;
|
|
545
|
+
// Calculate viewport offset to center element
|
|
546
|
+
const newX = containerRect.width / 2 - elementCenterX * vp.zoom;
|
|
547
|
+
const newY = containerRect.height / 2 - elementCenterY * vp.zoom;
|
|
548
|
+
const newViewport = {
|
|
549
|
+
x: newX,
|
|
550
|
+
y: newY,
|
|
551
|
+
zoom: vp.zoom,
|
|
552
|
+
};
|
|
553
|
+
this.viewport.set(newViewport);
|
|
554
|
+
this.viewportChange.emit(newViewport);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Scroll element into view with minimal pan
|
|
558
|
+
* Only pans if element is not fully visible
|
|
559
|
+
* @param elementId The element ID to scroll into view
|
|
560
|
+
* @param padding Padding from viewport edges (default: 40)
|
|
561
|
+
*/
|
|
562
|
+
scrollToElement(elementId, padding = 40) {
|
|
563
|
+
if (this.isElementVisible(elementId, padding)) {
|
|
564
|
+
return; // Already visible, no need to scroll
|
|
565
|
+
}
|
|
566
|
+
const bounds = this.getElementBounds(elementId);
|
|
567
|
+
if (!bounds)
|
|
568
|
+
return;
|
|
569
|
+
const vp = this.viewport();
|
|
570
|
+
const containerRect = this.elementRef.nativeElement.getBoundingClientRect();
|
|
571
|
+
// Convert element bounds to screen space
|
|
572
|
+
const screenLeft = bounds.x * vp.zoom + vp.x;
|
|
573
|
+
const screenTop = bounds.y * vp.zoom + vp.y;
|
|
574
|
+
const screenRight = (bounds.x + bounds.width) * vp.zoom + vp.x;
|
|
575
|
+
const screenBottom = (bounds.y + bounds.height) * vp.zoom + vp.y;
|
|
576
|
+
let newX = vp.x;
|
|
577
|
+
let newY = vp.y;
|
|
578
|
+
// Calculate minimal pan needed to bring element into view
|
|
579
|
+
// Check horizontal
|
|
580
|
+
if (screenLeft < padding) {
|
|
581
|
+
// Element is too far left, pan right
|
|
582
|
+
newX = vp.x + (padding - screenLeft);
|
|
583
|
+
}
|
|
584
|
+
else if (screenRight > containerRect.width - padding) {
|
|
585
|
+
// Element is too far right, pan left
|
|
586
|
+
newX = vp.x - (screenRight - (containerRect.width - padding));
|
|
587
|
+
}
|
|
588
|
+
// Check vertical
|
|
589
|
+
if (screenTop < padding) {
|
|
590
|
+
// Element is too far up, pan down
|
|
591
|
+
newY = vp.y + (padding - screenTop);
|
|
592
|
+
}
|
|
593
|
+
else if (screenBottom > containerRect.height - padding) {
|
|
594
|
+
// Element is too far down, pan up
|
|
595
|
+
newY = vp.y - (screenBottom - (containerRect.height - padding));
|
|
596
|
+
}
|
|
597
|
+
if (newX !== vp.x || newY !== vp.y) {
|
|
598
|
+
const newViewport = {
|
|
599
|
+
x: newX,
|
|
600
|
+
y: newY,
|
|
601
|
+
zoom: vp.zoom,
|
|
602
|
+
};
|
|
603
|
+
this.viewport.set(newViewport);
|
|
604
|
+
this.viewportChange.emit(newViewport);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Get bounds of a specific element
|
|
609
|
+
* @param elementId The element ID
|
|
610
|
+
* @returns Element bounds in world coordinates, or null if not found
|
|
611
|
+
*/
|
|
612
|
+
getElementBounds(elementId) {
|
|
613
|
+
const element = this.elements().find(e => e.id === elementId);
|
|
614
|
+
if (!element)
|
|
615
|
+
return null;
|
|
616
|
+
const pos = this.elementPositions.get(elementId) ?? { x: element.x, y: element.y };
|
|
617
|
+
const domElement = this.elementRefs.get(elementId);
|
|
618
|
+
let width = 100; // default
|
|
619
|
+
let height = 50; // default
|
|
620
|
+
if (domElement) {
|
|
621
|
+
const rect = domElement.getBoundingClientRect();
|
|
622
|
+
const zoom = this.viewport().zoom;
|
|
623
|
+
width = rect.width / zoom;
|
|
624
|
+
height = rect.height / zoom;
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
x: pos.x,
|
|
628
|
+
y: pos.y,
|
|
629
|
+
width,
|
|
630
|
+
height,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Calculate the bounding box of all elements
|
|
635
|
+
* @returns Bounding box in world coordinates, or null if no elements
|
|
636
|
+
*/
|
|
637
|
+
calculateContentBounds() {
|
|
638
|
+
const currentElements = this.elements();
|
|
639
|
+
if (currentElements.length === 0)
|
|
640
|
+
return null;
|
|
641
|
+
let minX = Infinity;
|
|
642
|
+
let minY = Infinity;
|
|
643
|
+
let maxX = -Infinity;
|
|
644
|
+
let maxY = -Infinity;
|
|
645
|
+
for (const element of currentElements) {
|
|
646
|
+
const bounds = this.getElementBounds(element.id);
|
|
647
|
+
if (!bounds)
|
|
648
|
+
continue;
|
|
649
|
+
minX = Math.min(minX, bounds.x);
|
|
650
|
+
minY = Math.min(minY, bounds.y);
|
|
651
|
+
maxX = Math.max(maxX, bounds.x + bounds.width);
|
|
652
|
+
maxY = Math.max(maxY, bounds.y + bounds.height);
|
|
653
|
+
}
|
|
654
|
+
if (minX === Infinity)
|
|
655
|
+
return null;
|
|
656
|
+
return {
|
|
657
|
+
x: minX,
|
|
658
|
+
y: minY,
|
|
659
|
+
width: maxX - minX,
|
|
660
|
+
height: maxY - minY,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/** Zoom in by a factor (default 1.25x) */
|
|
664
|
+
zoomIn(factor = 1.25) {
|
|
665
|
+
const cfg = this.config();
|
|
666
|
+
const zoomMax = cfg.zoomMax ?? DEFAULT_ZOOM_MAX;
|
|
667
|
+
const vp = this.viewport();
|
|
668
|
+
const newZoom = Math.min(zoomMax, vp.zoom * factor);
|
|
669
|
+
if (newZoom === vp.zoom)
|
|
670
|
+
return;
|
|
671
|
+
const newViewport = { ...vp, zoom: newZoom };
|
|
672
|
+
this.viewport.set(newViewport);
|
|
673
|
+
this.viewportChange.emit(newViewport);
|
|
674
|
+
}
|
|
675
|
+
/** Zoom out by a factor (default 1.25x) */
|
|
676
|
+
zoomOut(factor = 1.25) {
|
|
677
|
+
const cfg = this.config();
|
|
678
|
+
const zoomMin = cfg.zoomMin ?? DEFAULT_ZOOM_MIN;
|
|
679
|
+
const vp = this.viewport();
|
|
680
|
+
const newZoom = Math.max(zoomMin, vp.zoom / factor);
|
|
681
|
+
if (newZoom === vp.zoom)
|
|
682
|
+
return;
|
|
683
|
+
const newViewport = { ...vp, zoom: newZoom };
|
|
684
|
+
this.viewport.set(newViewport);
|
|
685
|
+
this.viewportChange.emit(newViewport);
|
|
686
|
+
}
|
|
687
|
+
/** Handle mouse wheel for zooming */
|
|
688
|
+
onWheel(event) {
|
|
689
|
+
const cfg = this.config();
|
|
690
|
+
if (!(cfg.zoomEnabled ?? true))
|
|
691
|
+
return;
|
|
692
|
+
// Prevent default scroll behavior
|
|
693
|
+
event.preventDefault();
|
|
694
|
+
const zoomMin = cfg.zoomMin ?? DEFAULT_ZOOM_MIN;
|
|
695
|
+
const zoomMax = cfg.zoomMax ?? DEFAULT_ZOOM_MAX;
|
|
696
|
+
const zoomSpeed = cfg.zoomSpeed ?? DEFAULT_ZOOM_SPEED;
|
|
697
|
+
const vp = this.viewport();
|
|
698
|
+
// Calculate new zoom level
|
|
699
|
+
const delta = -event.deltaY * zoomSpeed;
|
|
700
|
+
const newZoom = Math.max(zoomMin, Math.min(zoomMax, vp.zoom * (1 + delta)));
|
|
701
|
+
// If zoom hasn't changed (at limits), don't update
|
|
702
|
+
if (newZoom === vp.zoom)
|
|
703
|
+
return;
|
|
704
|
+
// Get cursor position relative to the grid container
|
|
705
|
+
const rect = this.elementRef.nativeElement.getBoundingClientRect();
|
|
706
|
+
const cursorX = event.clientX - rect.left;
|
|
707
|
+
const cursorY = event.clientY - rect.top;
|
|
708
|
+
// Calculate the world position under the cursor before zoom
|
|
709
|
+
// worldX = (cursorX - vp.x) / vp.zoom
|
|
710
|
+
const worldX = (cursorX - vp.x) / vp.zoom;
|
|
711
|
+
const worldY = (cursorY - vp.y) / vp.zoom;
|
|
712
|
+
// After zoom, we want the same world position under the cursor
|
|
713
|
+
// cursorX = worldX * newZoom + newVpX
|
|
714
|
+
// newVpX = cursorX - worldX * newZoom
|
|
715
|
+
const newX = cursorX - worldX * newZoom;
|
|
716
|
+
const newY = cursorY - worldY * newZoom;
|
|
717
|
+
const newViewport = {
|
|
718
|
+
x: newX,
|
|
719
|
+
y: newY,
|
|
720
|
+
zoom: newZoom,
|
|
721
|
+
};
|
|
722
|
+
this.viewport.set(newViewport);
|
|
723
|
+
this.viewportChange.emit(newViewport);
|
|
724
|
+
}
|
|
725
|
+
/** Initialize ResizeObserver after view is ready */
|
|
726
|
+
ngAfterViewInit() {
|
|
727
|
+
this.setupResizeObserver();
|
|
728
|
+
}
|
|
729
|
+
/** Clean up resources */
|
|
730
|
+
ngOnDestroy() {
|
|
731
|
+
this.cleanupResizeObserver();
|
|
732
|
+
if (this.dimensionUpdateTimer) {
|
|
733
|
+
clearTimeout(this.dimensionUpdateTimer);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/** Register an element for dimension tracking */
|
|
737
|
+
registerElement(elementId, element) {
|
|
738
|
+
this.elementRefs.set(elementId, element);
|
|
739
|
+
if (this.resizeObserver) {
|
|
740
|
+
this.resizeObserver.observe(element);
|
|
741
|
+
}
|
|
742
|
+
this.scheduleDimensionUpdate();
|
|
743
|
+
}
|
|
744
|
+
/** Unregister an element from dimension tracking */
|
|
745
|
+
unregisterElement(elementId) {
|
|
746
|
+
const element = this.elementRefs.get(elementId);
|
|
747
|
+
if (element && this.resizeObserver) {
|
|
748
|
+
this.resizeObserver.unobserve(element);
|
|
749
|
+
}
|
|
750
|
+
this.elementRefs.delete(elementId);
|
|
751
|
+
this.scheduleDimensionUpdate();
|
|
752
|
+
}
|
|
753
|
+
/** Set up the ResizeObserver to track element dimension changes */
|
|
754
|
+
setupResizeObserver() {
|
|
755
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
759
|
+
this.scheduleDimensionUpdate();
|
|
760
|
+
});
|
|
761
|
+
// Observe any already-registered elements
|
|
762
|
+
this.elementRefs.forEach((element) => {
|
|
763
|
+
this.resizeObserver.observe(element);
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
/** Clean up the ResizeObserver */
|
|
767
|
+
cleanupResizeObserver() {
|
|
768
|
+
if (this.resizeObserver) {
|
|
769
|
+
this.resizeObserver.disconnect();
|
|
770
|
+
this.resizeObserver = null;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/** Schedule a debounced dimension update */
|
|
774
|
+
scheduleDimensionUpdate() {
|
|
775
|
+
if (this.dimensionUpdateTimer) {
|
|
776
|
+
clearTimeout(this.dimensionUpdateTimer);
|
|
777
|
+
}
|
|
778
|
+
this.dimensionUpdateTimer = setTimeout(() => {
|
|
779
|
+
this.emitRenderedElements();
|
|
780
|
+
}, 10);
|
|
781
|
+
}
|
|
782
|
+
/** Collect and emit all rendered element dimensions */
|
|
783
|
+
emitRenderedElements() {
|
|
784
|
+
const currentElements = this.elements();
|
|
785
|
+
const renderedElements = [];
|
|
786
|
+
for (const element of currentElements) {
|
|
787
|
+
const domElement = this.elementRefs.get(element.id);
|
|
788
|
+
if (domElement) {
|
|
789
|
+
const rect = domElement.getBoundingClientRect();
|
|
790
|
+
// Get the actual size without zoom transform
|
|
791
|
+
const zoom = this.viewport().zoom;
|
|
792
|
+
renderedElements.push({
|
|
793
|
+
id: element.id,
|
|
794
|
+
x: element.x,
|
|
795
|
+
y: element.y,
|
|
796
|
+
width: rect.width / zoom,
|
|
797
|
+
height: rect.height / zoom,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (renderedElements.length > 0) {
|
|
802
|
+
this.elementsRendered.emit(renderedElements);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
806
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: GridComponent, isStandalone: true, selector: "ngx-grid", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, elements: { classPropertyName: "elements", publicName: "elements", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { viewportChange: "viewportChange", elementsRendered: "elementsRendered", elementPositionChange: "elementPositionChange" }, ngImport: i0, template: "<div\n class=\"grid-container\"\n [ngStyle]=\"containerStyles()\"\n (mousedown)=\"onMouseDown($event)\"\n (wheel)=\"onWheel($event)\">\n <div class=\"grid-background\" [ngStyle]=\"backgroundStyles()\"></div>\n <div class=\"grid-viewport\" [style.transform]=\"viewportTransform()\">\n @for (element of elements(); track element.id) {\n <div\n class=\"grid-element\"\n [gridElementRef]=\"element.id\"\n [ngStyle]=\"getElementStyles(element)\"\n (mousedown)=\"onElementMouseDown($event, element)\">\n <ng-container\n *ngComponentOutlet=\"element.component; injector: createElementInjector(element)\" />\n </div>\n }\n </div>\n @if (showToolbar() && integratedToolbarConfig()) {\n <ngx-grid-toolbar [config]=\"integratedToolbarConfig()!\" />\n }\n @if (isLoading()) {\n <div class=\"loading-overlay\">\n <div class=\"loading-spinner\"></div>\n </div>\n }\n</div>\n", styles: [":host{display:block;width:100%;height:100%;position:absolute;inset:0}.grid-container{position:relative;overflow:hidden}.grid-background{position:absolute;inset:0;pointer-events:none}.grid-viewport{position:absolute;top:0;left:0;transform-origin:0 0;pointer-events:none}.grid-element{pointer-events:auto}.loading-overlay{position:absolute;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.2);border-top-color:#6366f1;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: GridElementRefDirective, selector: "[gridElementRef]", inputs: ["gridElementRef"] }, { kind: "component", type: ToolbarComponent, selector: "ngx-grid-toolbar", inputs: ["config"] }] });
|
|
807
|
+
}
|
|
808
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GridComponent, decorators: [{
|
|
809
|
+
type: Component,
|
|
810
|
+
args: [{ selector: 'ngx-grid', imports: [NgStyle, NgComponentOutlet, GridElementRefDirective, ToolbarComponent], template: "<div\n class=\"grid-container\"\n [ngStyle]=\"containerStyles()\"\n (mousedown)=\"onMouseDown($event)\"\n (wheel)=\"onWheel($event)\">\n <div class=\"grid-background\" [ngStyle]=\"backgroundStyles()\"></div>\n <div class=\"grid-viewport\" [style.transform]=\"viewportTransform()\">\n @for (element of elements(); track element.id) {\n <div\n class=\"grid-element\"\n [gridElementRef]=\"element.id\"\n [ngStyle]=\"getElementStyles(element)\"\n (mousedown)=\"onElementMouseDown($event, element)\">\n <ng-container\n *ngComponentOutlet=\"element.component; injector: createElementInjector(element)\" />\n </div>\n }\n </div>\n @if (showToolbar() && integratedToolbarConfig()) {\n <ngx-grid-toolbar [config]=\"integratedToolbarConfig()!\" />\n }\n @if (isLoading()) {\n <div class=\"loading-overlay\">\n <div class=\"loading-spinner\"></div>\n </div>\n }\n</div>\n", styles: [":host{display:block;width:100%;height:100%;position:absolute;inset:0}.grid-container{position:relative;overflow:hidden}.grid-background{position:absolute;inset:0;pointer-events:none}.grid-viewport{position:absolute;top:0;left:0;transform-origin:0 0;pointer-events:none}.grid-element{pointer-events:auto}.loading-overlay{position:absolute;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.2);border-top-color:#6366f1;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n"] }]
|
|
811
|
+
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], elements: [{ type: i0.Input, args: [{ isSignal: true, alias: "elements", required: false }] }], viewportChange: [{ type: i0.Output, args: ["viewportChange"] }], elementsRendered: [{ type: i0.Output, args: ["elementsRendered"] }], elementPositionChange: [{ type: i0.Output, args: ["elementPositionChange"] }] } });
|
|
812
|
+
|
|
813
|
+
// Components
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Generated bundle index. Do not edit.
|
|
817
|
+
*/
|
|
818
|
+
|
|
819
|
+
export { DEFAULT_BACKGROUND_COLOR, DEFAULT_CELL_SIZE, DEFAULT_GRID_CONFIG, DEFAULT_PATTERN_COLOR, DEFAULT_VIEWPORT_STATE, DEFAULT_ZOOM_MAX, DEFAULT_ZOOM_MIN, DEFAULT_ZOOM_SPEED, GRID_ELEMENT_DATA, GridComponent, ToolbarComponent };
|
|
820
|
+
//# sourceMappingURL=ngx-km-grid.mjs.map
|