@miguimono/json-schema 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/dist/schema/README.md +421 -0
- package/dist/schema/fesm2022/json-schema.mjs +1654 -0
- package/dist/schema/fesm2022/json-schema.mjs.map +1 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/lib/components/schema-card.component.d.ts +128 -0
- package/dist/schema/lib/components/schema-links.component.d.ts +61 -0
- package/dist/schema/lib/components/schema.component.d.ts +115 -0
- package/dist/schema/lib/models.d.ts +1 -0
- package/dist/schema/lib/services/json-adapter.service.d.ts +1 -0
- package/dist/schema/lib/services/schema-layout.service.d.ts +1 -0
- package/dist/schema/lib/shared/models.d.ts +319 -0
- package/dist/schema/lib/shared/services/json-adapter.service.d.ts +12 -0
- package/dist/schema/lib/shared/services/schema-layout.service.d.ts +21 -0
- package/dist/schema/ng16/fesm2022/json-schema-ng16.mjs +1656 -0
- package/dist/schema/ng16/fesm2022/json-schema-ng16.mjs.map +1 -0
- package/dist/schema/ng16/index.d.ts +5 -0
- package/dist/schema/ng16/lib/components/schema-card.component.d.ts +64 -0
- package/dist/schema/ng16/lib/components/schema-links.component.d.ts +28 -0
- package/dist/schema/ng16/lib/components/schema.component.d.ts +132 -0
- package/dist/schema/ng16/lib/models.d.ts +1 -0
- package/dist/schema/ng16/lib/services/json-adapter.service.d.ts +1 -0
- package/dist/schema/ng16/lib/services/schema-layout.service.d.ts +1 -0
- package/dist/schema/ng16/lib/shared/models.d.ts +319 -0
- package/dist/schema/ng16/lib/shared/services/json-adapter.service.d.ts +12 -0
- package/dist/schema/ng16/lib/shared/services/schema-layout.service.d.ts +21 -0
- package/dist/schema/ng16/public-api.d.ts +6 -0
- package/dist/schema/public-api.d.ts +5 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1654 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, EventEmitter, computed, Output, ChangeDetectionStrategy, Component, Input, Injectable, signal, ViewChild } from '@angular/core';
|
|
3
|
+
import * as i1 from '@angular/common';
|
|
4
|
+
import { CommonModule, NgTemplateOutlet, NgFor, NgIf } from '@angular/common';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tipos y modelos base de la librería Schema.
|
|
8
|
+
* ---------------------------------------------------------------------------
|
|
9
|
+
* Este archivo define:
|
|
10
|
+
* - Tipos de configuración (dirección del layout, estilos, depuración, etc.)
|
|
11
|
+
* - Estructuras del grafo normalizado (nodos y aristas)
|
|
12
|
+
* - Estructura de configuración SchemaSettings (por secciones)
|
|
13
|
+
* - Valores por defecto (DEFAULT_SETTINGS)
|
|
14
|
+
*
|
|
15
|
+
* CONVENCIONES:
|
|
16
|
+
* - No se realizan operaciones lógicas aquí; solo contratos y defaults.
|
|
17
|
+
* - No modifiques nombres de propiedades existentes: otros módulos dependen
|
|
18
|
+
* de ellas. Cualquier cambio es breaking.
|
|
19
|
+
* - Todos los ejemplos son ilustrativos y no afectan el comportamiento.
|
|
20
|
+
*/
|
|
21
|
+
/* =========================================
|
|
22
|
+
* Valores por defecto
|
|
23
|
+
* ========================================= */
|
|
24
|
+
/**
|
|
25
|
+
* Valores por defecto (seguros) de configuración.
|
|
26
|
+
* - Estos valores son fusionados con `SchemaSettings` provistos por el usuario.
|
|
27
|
+
* - No contienen lógica condicional; son constantes.
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_SETTINGS = {
|
|
30
|
+
messages: {
|
|
31
|
+
isLoading: false,
|
|
32
|
+
isError: false,
|
|
33
|
+
loadingMessage: "Cargando…",
|
|
34
|
+
errorMessage: "Error al cargar el esquema",
|
|
35
|
+
emptyMessage: "No hay datos para mostrar",
|
|
36
|
+
},
|
|
37
|
+
colors: {
|
|
38
|
+
linkStroke: "#019df4",
|
|
39
|
+
linkStrokeWidth: 2,
|
|
40
|
+
accentByKey: null,
|
|
41
|
+
accentInverse: false,
|
|
42
|
+
accentFill: false,
|
|
43
|
+
showColorTrue: false,
|
|
44
|
+
showColorFalse: false,
|
|
45
|
+
showColorNull: false,
|
|
46
|
+
},
|
|
47
|
+
layout: {
|
|
48
|
+
layoutDirection: "RIGHT",
|
|
49
|
+
layoutAlign: "firstChild",
|
|
50
|
+
linkStyle: "curve",
|
|
51
|
+
curveTension: 30,
|
|
52
|
+
straightThresholdDx: 60,
|
|
53
|
+
columnGapPx: 64,
|
|
54
|
+
rowGapPx: 32,
|
|
55
|
+
},
|
|
56
|
+
dataView: {
|
|
57
|
+
/* a) Extracción */
|
|
58
|
+
titleKeyPriority: [],
|
|
59
|
+
hiddenKeysGlobal: [],
|
|
60
|
+
treatScalarArraysAsAttribute: true,
|
|
61
|
+
maxDepth: null,
|
|
62
|
+
labelData: {},
|
|
63
|
+
/* b) Presentación general */
|
|
64
|
+
previewMaxKeys: 999,
|
|
65
|
+
valueMaxChars: null,
|
|
66
|
+
valueShowTooltip: false,
|
|
67
|
+
noWrapKeys: [],
|
|
68
|
+
maxCardWidth: null,
|
|
69
|
+
maxCardHeight: null,
|
|
70
|
+
defaultNodeSize: { width: 256, height: 64 },
|
|
71
|
+
/* c) Presentación de imagen */
|
|
72
|
+
showImage: null,
|
|
73
|
+
imageSizePx: 32,
|
|
74
|
+
imageShape: "rounded",
|
|
75
|
+
imageBorder: false,
|
|
76
|
+
imageBg: "transparent",
|
|
77
|
+
imageFit: "contain",
|
|
78
|
+
imageFallback: null,
|
|
79
|
+
/* d) Interacción */
|
|
80
|
+
enableCollapse: true,
|
|
81
|
+
/* e) Medición */
|
|
82
|
+
autoResizeCards: true,
|
|
83
|
+
paddingWidthPx: 16,
|
|
84
|
+
paddingHeightPx: 0,
|
|
85
|
+
},
|
|
86
|
+
viewport: {
|
|
87
|
+
height: 800,
|
|
88
|
+
minHeight: 480,
|
|
89
|
+
showToolbar: true,
|
|
90
|
+
toolbarControls: {
|
|
91
|
+
showLinkStyle: true,
|
|
92
|
+
showLayoutAlign: true,
|
|
93
|
+
showLayoutDirection: true,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// URL: projects/schema-ng19/src/lib/models.ts
|
|
99
|
+
|
|
100
|
+
// URL: projects/schema-ng19/src/lib/components/schema-card.component.ts
|
|
101
|
+
/**
|
|
102
|
+
* Componente: SchemaCardComponent
|
|
103
|
+
* ----------------------------------------------------------------------------
|
|
104
|
+
* Renderiza un `SchemaNode` como tarjeta posicionada en el “stage”.*
|
|
105
|
+
* Responsabilidades:
|
|
106
|
+
* - Mostrar título, atributos de vista previa y badges de arrays no escalares.
|
|
107
|
+
* - Soportar una miniatura de imagen opcional (grupo de settings de imagen).
|
|
108
|
+
* - Emitir eventos de interacción: click de nodo y solicitud de colapso.
|
|
109
|
+
* - Aplicar clases de acento (accent-true/false/null y variantes fill).
|
|
110
|
+
*
|
|
111
|
+
* Reglas:
|
|
112
|
+
* - La posición (left/top) y tamaño (width/height) vienen del layout/medición.
|
|
113
|
+
* - No calcula layout; solo representa el nodo con base en `SchemaSettings`.
|
|
114
|
+
* - Si `cardTemplate` está definido, se usa en lugar del template por defecto.
|
|
115
|
+
*
|
|
116
|
+
* Imagen (grupo completo en `settings.dataView`):
|
|
117
|
+
* - `showImage`: clave en `node.data` con la URL a mostrar (si existe).
|
|
118
|
+
* - `imageSizePx`: tamaño en px del cuadro (ancho/alto iguales).
|
|
119
|
+
* - `imageShape`: "square" | "rounded" | "circle".
|
|
120
|
+
* - `imageBorder`: borde sutil opcional alrededor de la miniatura.
|
|
121
|
+
* - `imageBg`: color/fondo CSS del cuadro de imagen (e.g., "transparent").
|
|
122
|
+
* - `imageFit`: "contain" | "cover" | "scale-down" (mapea a `object-fit`).
|
|
123
|
+
* - `imageFallback`: URL local/remota a usar si la imagen falla (no implementado aquí).
|
|
124
|
+
*
|
|
125
|
+
* Acentos (grupo `settings.colors`):
|
|
126
|
+
* - `accentByKey`: clave booleana en `node.data`. Si no hay clave, no hay acento.
|
|
127
|
+
* - `accentInverse`: invierte la semántica (true↔false).
|
|
128
|
+
* - `accentFill`: si `true`, además del borde aplica fondo suave.
|
|
129
|
+
* - `showColorTrue/False/Null`: habilitan visuales para cada caso.
|
|
130
|
+
*
|
|
131
|
+
* Vista previa:
|
|
132
|
+
* - `jsonMeta.attributes` se deriva en el adapter y se trunca según settings globales.
|
|
133
|
+
* - Las claves incluidas en `noWrapKeys` se presentan en una sola línea (nowrap).
|
|
134
|
+
*
|
|
135
|
+
* Accesibilidad:
|
|
136
|
+
* - `alt` para la imagen se deriva del título o `label` del nodo.
|
|
137
|
+
*
|
|
138
|
+
* Nota:
|
|
139
|
+
* - Este componente no decide si se muestra el botón de colapso; expone
|
|
140
|
+
* `showCollapseControls` e `isCollapsed`, y emite `toggleRequest`. La UI del
|
|
141
|
+
* botón puede implementarse en el template custom si se usa `cardTemplate`.
|
|
142
|
+
*/
|
|
143
|
+
class SchemaCardComponent {
|
|
144
|
+
/* ============================ Inputs ============================ */
|
|
145
|
+
/** Nodo a representar. Requerido. */
|
|
146
|
+
node = input.required();
|
|
147
|
+
/** Template alternativo para renderizar la card (reemplaza al default). */
|
|
148
|
+
cardTemplate = input(null);
|
|
149
|
+
/** Settings efectivos aplicables a la card (colores, imagen, etc.). */
|
|
150
|
+
settings = input(DEFAULT_SETTINGS);
|
|
151
|
+
/** Indica si este nodo tiene hijos (para mostrar controles de colapso). */
|
|
152
|
+
hasChildren = input(false);
|
|
153
|
+
/** Controla si se muestra la UI de colapso (el botón/indicador). */
|
|
154
|
+
showCollapseControls = input(false);
|
|
155
|
+
/** Indica si el nodo está colapsado (para iconografía/estado visual). */
|
|
156
|
+
isCollapsed = input(false);
|
|
157
|
+
/* ============================ Outputs =========================== */
|
|
158
|
+
/** Click del usuario sobre la card (burbujeo controlado). */
|
|
159
|
+
nodeClick = new EventEmitter();
|
|
160
|
+
/** Solicitud de colapso/expandir desde la card. */
|
|
161
|
+
toggleRequest = new EventEmitter();
|
|
162
|
+
/* ============================ View derivada ===================== */
|
|
163
|
+
/**
|
|
164
|
+
* Vista aplanada derivada de `settings`.
|
|
165
|
+
* - Si `accentByKey` está definido y no hay showColor* en true,
|
|
166
|
+
* se activan internamente los tres (true/false/null) para visual por defecto.
|
|
167
|
+
*/
|
|
168
|
+
view = computed(() => {
|
|
169
|
+
const s = this.settings() ?? DEFAULT_SETTINGS;
|
|
170
|
+
// Grupo de imagen
|
|
171
|
+
const imageSizePx = s.dataView?.imageSizePx ?? DEFAULT_SETTINGS.dataView.imageSizePx;
|
|
172
|
+
const imageShape = (s.dataView?.imageShape ?? DEFAULT_SETTINGS.dataView.imageShape);
|
|
173
|
+
const imageBorder = s.dataView?.imageBorder ?? DEFAULT_SETTINGS.dataView.imageBorder;
|
|
174
|
+
const imageBg = s.dataView?.imageBg ?? DEFAULT_SETTINGS.dataView.imageBg;
|
|
175
|
+
const imageFit = s.dataView?.imageFit ?? DEFAULT_SETTINGS.dataView.imageFit;
|
|
176
|
+
// Presentación general
|
|
177
|
+
const maxCardWidth = s.dataView?.maxCardWidth ?? null;
|
|
178
|
+
const maxCardHeight = s.dataView?.maxCardHeight ?? null;
|
|
179
|
+
const noWrapKeys = s.dataView?.noWrapKeys ?? [];
|
|
180
|
+
const labelData = s.dataView?.labelData ?? {};
|
|
181
|
+
const valueShowTooltip = s.dataView?.valueShowTooltip ?? false;
|
|
182
|
+
const valueMaxChars = s.dataView?.valueMaxChars ?? null;
|
|
183
|
+
// Acentos
|
|
184
|
+
const accentByKey = s.colors?.accentByKey ?? null;
|
|
185
|
+
const accentFill = s.colors?.accentFill ?? false;
|
|
186
|
+
const accentInverse = s.colors?.accentInverse ?? false;
|
|
187
|
+
const reqTrue = s.colors?.showColorTrue ?? false;
|
|
188
|
+
const reqFalse = s.colors?.showColorFalse ?? false;
|
|
189
|
+
const reqNull = s.colors?.showColorNull ?? false;
|
|
190
|
+
let showTrue = reqTrue;
|
|
191
|
+
let showFalse = reqFalse;
|
|
192
|
+
let showNull = reqNull;
|
|
193
|
+
const anyRequested = reqTrue || reqFalse || reqNull;
|
|
194
|
+
if (accentByKey && !anyRequested) {
|
|
195
|
+
// Activación interna por DX; no modifica settings globales
|
|
196
|
+
showTrue = true;
|
|
197
|
+
showFalse = true;
|
|
198
|
+
showNull = true;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
// Imagen
|
|
202
|
+
showImageKey: s.dataView?.showImage ?? null,
|
|
203
|
+
imageSizePx,
|
|
204
|
+
imageShape,
|
|
205
|
+
imageBorder,
|
|
206
|
+
imageBg,
|
|
207
|
+
imageFit,
|
|
208
|
+
imageFallback: s.dataView?.imageFallback ?? null,
|
|
209
|
+
// Presentación
|
|
210
|
+
maxCardWidth,
|
|
211
|
+
maxCardHeight,
|
|
212
|
+
noWrapKeys,
|
|
213
|
+
labelData,
|
|
214
|
+
valueShowTooltip,
|
|
215
|
+
valueMaxChars,
|
|
216
|
+
// Defaults para tamaño en ausencia de medición
|
|
217
|
+
defaultNodeW: DEFAULT_SETTINGS.dataView.defaultNodeSize?.width ?? 120,
|
|
218
|
+
defaultNodeH: DEFAULT_SETTINGS.dataView.defaultNodeSize?.height ?? 60,
|
|
219
|
+
// Acentos
|
|
220
|
+
accentByKey,
|
|
221
|
+
accentFill,
|
|
222
|
+
accentInverse,
|
|
223
|
+
showColorTrue: showTrue,
|
|
224
|
+
showColorFalse: showFalse,
|
|
225
|
+
showColorNull: showNull,
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
/* ============================ API interna ======================= */
|
|
229
|
+
/** Emite click del nodo y evita burbujeo accidental. */
|
|
230
|
+
onClick(event) {
|
|
231
|
+
event.stopPropagation();
|
|
232
|
+
const n = this.node();
|
|
233
|
+
if (n)
|
|
234
|
+
this.nodeClick.emit(n);
|
|
235
|
+
}
|
|
236
|
+
/** Emite solicitud de toggle (colapso/expandir). */
|
|
237
|
+
onToggle(event) {
|
|
238
|
+
event.stopPropagation();
|
|
239
|
+
const n = this.node();
|
|
240
|
+
if (n)
|
|
241
|
+
this.toggleRequest.emit(n);
|
|
242
|
+
}
|
|
243
|
+
/** Convierte objeto a pares clave-valor iterables. */
|
|
244
|
+
objToPairs(obj) {
|
|
245
|
+
const entries = Object.entries(obj ?? {});
|
|
246
|
+
const imgKey = this.view().showImageKey;
|
|
247
|
+
const accentKey = this.view().accentByKey;
|
|
248
|
+
return entries.filter(([k]) => {
|
|
249
|
+
if (imgKey && k === imgKey)
|
|
250
|
+
return false; // Oculta la URL de imagen
|
|
251
|
+
if (accentKey && k === accentKey)
|
|
252
|
+
return false; // Oculta la clave de acento (true/false)
|
|
253
|
+
return true;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/** Clave configurada para la imagen; `null` si no hay. */
|
|
257
|
+
showImageKey() {
|
|
258
|
+
const k = this.view().showImageKey;
|
|
259
|
+
return k && typeof k === "string" && k.trim() ? k : null;
|
|
260
|
+
}
|
|
261
|
+
/** URL de imagen si existe y es válida. */
|
|
262
|
+
imageSrc() {
|
|
263
|
+
const key = this.showImageKey();
|
|
264
|
+
if (!key)
|
|
265
|
+
return null;
|
|
266
|
+
const v = this.node()?.data?.[key];
|
|
267
|
+
return typeof v === "string" && v.trim() !== "" ? v : null;
|
|
268
|
+
}
|
|
269
|
+
/** Texto alternativo accesible para la miniatura. */
|
|
270
|
+
imageAlt() {
|
|
271
|
+
const n = this.node();
|
|
272
|
+
if (!n)
|
|
273
|
+
return "imagen";
|
|
274
|
+
return n.jsonMeta?.title || n.label || "imagen";
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Determina clases de acento según `accentByKey` y flags de color.
|
|
278
|
+
* No aplica estilos si `accentByKey` no está definido en `node.data`.
|
|
279
|
+
*/
|
|
280
|
+
getAccentClasses() {
|
|
281
|
+
const v = this.view();
|
|
282
|
+
const k = v.accentByKey;
|
|
283
|
+
if (!k)
|
|
284
|
+
return [];
|
|
285
|
+
const n = this.node();
|
|
286
|
+
const val = n?.data?.[k];
|
|
287
|
+
const classes = [];
|
|
288
|
+
const pushIf = (cond, cls) => {
|
|
289
|
+
if (cond)
|
|
290
|
+
classes.push(cls);
|
|
291
|
+
};
|
|
292
|
+
if (!v.accentInverse) {
|
|
293
|
+
pushIf(val === true && v.showColorTrue, "accent-true");
|
|
294
|
+
pushIf(val === false && v.showColorFalse, "accent-false");
|
|
295
|
+
pushIf(val === null && v.showColorNull, "accent-null");
|
|
296
|
+
if (v.accentFill) {
|
|
297
|
+
pushIf(val === true && v.showColorTrue, "accent-fill-true");
|
|
298
|
+
pushIf(val === false && v.showColorFalse, "accent-fill-false");
|
|
299
|
+
pushIf(val === null && v.showColorNull, "accent-fill-null");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
pushIf(val === true && v.showColorTrue, "accent-false");
|
|
304
|
+
pushIf(val === false && v.showColorFalse, "accent-true");
|
|
305
|
+
pushIf(val === null && v.showColorNull, "accent-null");
|
|
306
|
+
if (v.accentFill) {
|
|
307
|
+
pushIf(val === true && v.showColorTrue, "accent-fill-false");
|
|
308
|
+
pushIf(val === false && v.showColorFalse, "accent-fill-true");
|
|
309
|
+
pushIf(val === null && v.showColorNull, "accent-fill-null");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return classes;
|
|
313
|
+
}
|
|
314
|
+
/** Indica si una clave debe mostrarse en una sola línea. */
|
|
315
|
+
isNoWrapKey(key) {
|
|
316
|
+
const arr = this.view().noWrapKeys ?? [];
|
|
317
|
+
return Array.isArray(arr) ? arr.includes(key) : false;
|
|
318
|
+
}
|
|
319
|
+
/** Glifo de flecha para estados colapsado/expandido (cuando se use). */
|
|
320
|
+
arrowGlyph() {
|
|
321
|
+
const dir = this.settings()?.layout?.layoutDirection ?? DEFAULT_SETTINGS.layout.layoutDirection;
|
|
322
|
+
const collapsed = !!this.isCollapsed();
|
|
323
|
+
if (dir === "DOWN")
|
|
324
|
+
return collapsed ? "▼" : "▲";
|
|
325
|
+
return collapsed ? "▶" : "◀";
|
|
326
|
+
}
|
|
327
|
+
/** Presentación del valor (aplica truncamiento cuando corresponde). */
|
|
328
|
+
displayValue(val) {
|
|
329
|
+
const str = val == null ? String(val) : String(val);
|
|
330
|
+
const limit = this.view().valueMaxChars;
|
|
331
|
+
if (typeof limit === "number" && limit > 0 && str.length > limit) {
|
|
332
|
+
return str.slice(0, limit) + "…";
|
|
333
|
+
}
|
|
334
|
+
return str;
|
|
335
|
+
}
|
|
336
|
+
/** Tooltip con el valor completo (si está habilitado). */
|
|
337
|
+
valueTitle(val) {
|
|
338
|
+
if (!this.view().valueShowTooltip)
|
|
339
|
+
return null;
|
|
340
|
+
return val == null ? String(val) : String(val);
|
|
341
|
+
}
|
|
342
|
+
/** Mapea clave a etiqueta legible, si está definida en `labelData`. */
|
|
343
|
+
displayKey(key) {
|
|
344
|
+
const map = this.view().labelData ?? {};
|
|
345
|
+
return Object.prototype.hasOwnProperty.call(map, key) ? map[key] : key;
|
|
346
|
+
}
|
|
347
|
+
/** Indica si existe un título calculado para la card. */
|
|
348
|
+
hasComputedTitle() {
|
|
349
|
+
const t = this.node()?.jsonMeta?.title;
|
|
350
|
+
return !!t && String(t).trim() !== "";
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Manejo de error en carga de imagen:
|
|
354
|
+
* - Si existe `imageFallback` y no se intentó antes, usarla (una sola vez).
|
|
355
|
+
* - Si no hay fallback o ya falló, remover `src` y aplicar fondo suave.
|
|
356
|
+
*/
|
|
357
|
+
onImgError(ev) {
|
|
358
|
+
const el = ev.target;
|
|
359
|
+
if (!el)
|
|
360
|
+
return;
|
|
361
|
+
const ds = el.dataset;
|
|
362
|
+
const tried = ds["fallbackApplied"] === "1";
|
|
363
|
+
const fallback = this.view().imageFallback;
|
|
364
|
+
if (fallback && !tried) {
|
|
365
|
+
ds["fallbackApplied"] = "1";
|
|
366
|
+
el.src = fallback;
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
// Sin fallback (o ya falló)
|
|
370
|
+
el.removeAttribute("src");
|
|
371
|
+
if (!this.view().imageBg) {
|
|
372
|
+
el.style.background = "#e2e8f0";
|
|
373
|
+
}
|
|
374
|
+
if (this.view().imageBorder === undefined) {
|
|
375
|
+
el.style.border = "1px solid rgba(0,0,0,0.06)";
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
379
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.18", type: SchemaCardComponent, isStandalone: true, selector: "schema-card", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, cardTemplate: { classPropertyName: "cardTemplate", publicName: "cardTemplate", isSignal: true, isRequired: false, transformFunction: null }, settings: { classPropertyName: "settings", publicName: "settings", isSignal: true, isRequired: false, transformFunction: null }, hasChildren: { classPropertyName: "hasChildren", publicName: "hasChildren", isSignal: true, isRequired: false, transformFunction: null }, showCollapseControls: { classPropertyName: "showCollapseControls", publicName: "showCollapseControls", isSignal: true, isRequired: false, transformFunction: null }, isCollapsed: { classPropertyName: "isCollapsed", publicName: "isCollapsed", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeClick: "nodeClick", toggleRequest: "toggleRequest" }, ngImport: i0, template: "<div\n class=\"schema-card\"\n [attr.data-node-id]=\"node()?.id ?? ''\"\n [ngClass]=\"getAccentClasses()\"\n [style.left.px]=\"node()?.x ?? 0\"\n [style.top.px]=\"node()?.y ?? 0\"\n [style.width.px]=\"node()?.width ?? view().defaultNodeW\"\n [style.height.px]=\"node()?.height ?? view().defaultNodeH\"\n [style.maxWidth.px]=\"view().maxCardWidth\"\n [style.maxHeight.px]=\"view().maxCardHeight\"\n (click)=\"onClick($event)\"\n style=\"z-index: 1; position: absolute;\"\n>\n <button\n *ngIf=\"showCollapseControls() && hasChildren()\"\n type=\"button\"\n class=\"collapse-btn\"\n [attr.aria-pressed]=\"isCollapsed()\"\n (click)=\"onToggle($event)\"\n [attr.title]=\"isCollapsed() ? 'Expandir' : 'Colapsar'\"\n >\n <span class=\"chev\">{{ arrowGlyph() }}</span>\n </button>\n\n <!--\n Si se provee un template externo (ng-template), se renderiza en lugar\n del contenido por defecto. En ese caso, el consumidor puede usar el\n contexto \"$implicit: node\" para personalizar completamente la card.\n -->\n <ng-container\n *ngIf=\"cardTemplate(); else defaultTpl\"\n [ngTemplateOutlet]=\"cardTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: node() }\"\n ></ng-container>\n\n <!-- Template por defecto -->\n <ng-template #defaultTpl>\n <div class=\"card-body\">\n <div class=\"card-row\">\n <!-- Miniatura opcional -->\n <img\n *ngIf=\"imageSrc() as src\"\n class=\"thumb\"\n [class.shape-square]=\"view().imageShape === 'square'\"\n [class.shape-rounded]=\"view().imageShape === 'rounded'\"\n [class.shape-circle]=\"view().imageShape === 'circle'\"\n [attr.width]=\"view().imageSizePx\"\n [attr.height]=\"view().imageSizePx\"\n [style.width.px]=\"view().imageSizePx\"\n [style.height.px]=\"view().imageSizePx\"\n [style.background]=\"view().imageBg ?? null\"\n [style.border]=\"view().imageBorder ? '1px solid rgba(0,0,0,0.06)' : 'none'\"\n [style.object-fit]=\"view().imageFit\"\n [src]=\"src\"\n [alt]=\"imageAlt()\"\n loading=\"lazy\"\n decoding=\"async\"\n (error)=\"onImgError($event)\"\n />\n\n <div class=\"card-col\">\n <div class=\"card-title\" *ngIf=\"hasComputedTitle()\">\n {{ node()?.jsonMeta?.title }}\n </div>\n\n <!-- Vista previa de atributos (derivados por el adapter) -->\n <div class=\"card-preview\" *ngIf=\"node()?.jsonMeta?.attributes as attrs\">\n <div *ngFor=\"let kv of objToPairs(attrs)\" class=\"kv\">\n <span class=\"k\">{{ displayKey(kv[0]) }}:</span>\n <span\n class=\"v\"\n [class.v-true]=\"kv[1] === true\"\n [class.v-false]=\"kv[1] === false\"\n [class.v-null]=\"kv[1] === null\"\n [class.nowrap]=\"isNoWrapKey(kv[0])\"\n [attr.title]=\"valueTitle(kv[1])\"\n >\n {{ displayValue(kv[1]) }}\n </span>\n </div>\n </div>\n\n <!-- Badges con conteos de arrays no escalares -->\n <div class=\"array-badges\" *ngIf=\"node()?.jsonMeta?.arrayCounts as arrs\">\n <ng-container *ngFor=\"let e of objToPairs(arrs)\">\n <span class=\"arr-badge\">\n {{ displayKey(e[0]) }}: {{ e[1] }} {{ e[1] === 1 ? \"item\" : \"items\" }}\n </span>\n </ng-container>\n </div>\n </div>\n </div>\n </div>\n </ng-template>\n</div>\n", styles: ["@charset \"UTF-8\";.schema-card{border-radius:12px;border:1px solid rgba(0,0,0,.08);background:#fff;box-shadow:0 2px 8px #00000014;-webkit-user-select:none;user-select:none;box-sizing:border-box;word-break:normal;overflow-wrap:normal;overflow:hidden;transition:left .16s ease,top .16s ease,opacity .12s ease}.collapse-btn{position:absolute;top:6px;right:8px;width:24px;height:24px;border-radius:6px;border:1px solid rgba(0,0,0,.12);background:#f8fafc;cursor:pointer;display:grid;place-items:center;padding:0;line-height:1;z-index:2}.collapse-btn span{font-size:.5rem}.chev{display:inline-block;transition:transform .16s ease}.chev.collapsed{transform:rotate(180deg)}.card-body{padding:12px 20px}.card-row{display:flex;align-items:flex-start;gap:10px}.card-col{flex:1 1 auto;min-width:0}.thumb{flex:0 0 auto;display:block;object-fit:contain;background:transparent;border:1px solid rgba(0,0,0,.06)}.shape-square{border-radius:4px}.shape-rounded{border-radius:8px}.shape-circle{border-radius:999px}.card-title{font-weight:700;font-size:14px;margin-bottom:6px}.card-preview{font-size:12px;line-height:1.28}.kv{display:grid;grid-template-columns:max-content max-content;column-gap:10px;align-items:baseline;margin:3px 0;padding-right:8px}.k{opacity:.66;font-weight:600;font-size:12px}.v{font-size:10.75px;line-height:1.25;letter-spacing:.1px;padding-right:6px;display:inline-block}.v-true{color:#16a34a;font-weight:700}.v-false{color:#dc2626;font-weight:700}.v-null{color:#6b7280;font-weight:700}.array-badges{margin-top:6px;display:flex;flex-wrap:wrap;gap:6px}.arr-badge{font-size:10px;background:#eef2ff;color:#3730a3;padding:2px 6px;border-radius:999px}.nowrap{white-space:nowrap!important;word-break:keep-all!important;overflow-wrap:normal!important}.schema-card.accent-true{border-color:#16a34a;box-shadow:0 2px 10px #16a34a26}.schema-card.accent-false{border-color:#dc2626;box-shadow:0 2px 10px #dc262626}.schema-card.accent-null{border-color:#6b7280;box-shadow:0 2px 10px #6b72802e}.schema-card.accent-fill-true{background:#16a34a1a}.schema-card.accent-fill-false{background:#dc26261a}.schema-card.accent-fill-null{background:#6b72801f}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
380
|
+
}
|
|
381
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaCardComponent, decorators: [{
|
|
382
|
+
type: Component,
|
|
383
|
+
args: [{ selector: "schema-card", standalone: true, imports: [CommonModule, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"schema-card\"\n [attr.data-node-id]=\"node()?.id ?? ''\"\n [ngClass]=\"getAccentClasses()\"\n [style.left.px]=\"node()?.x ?? 0\"\n [style.top.px]=\"node()?.y ?? 0\"\n [style.width.px]=\"node()?.width ?? view().defaultNodeW\"\n [style.height.px]=\"node()?.height ?? view().defaultNodeH\"\n [style.maxWidth.px]=\"view().maxCardWidth\"\n [style.maxHeight.px]=\"view().maxCardHeight\"\n (click)=\"onClick($event)\"\n style=\"z-index: 1; position: absolute;\"\n>\n <button\n *ngIf=\"showCollapseControls() && hasChildren()\"\n type=\"button\"\n class=\"collapse-btn\"\n [attr.aria-pressed]=\"isCollapsed()\"\n (click)=\"onToggle($event)\"\n [attr.title]=\"isCollapsed() ? 'Expandir' : 'Colapsar'\"\n >\n <span class=\"chev\">{{ arrowGlyph() }}</span>\n </button>\n\n <!--\n Si se provee un template externo (ng-template), se renderiza en lugar\n del contenido por defecto. En ese caso, el consumidor puede usar el\n contexto \"$implicit: node\" para personalizar completamente la card.\n -->\n <ng-container\n *ngIf=\"cardTemplate(); else defaultTpl\"\n [ngTemplateOutlet]=\"cardTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: node() }\"\n ></ng-container>\n\n <!-- Template por defecto -->\n <ng-template #defaultTpl>\n <div class=\"card-body\">\n <div class=\"card-row\">\n <!-- Miniatura opcional -->\n <img\n *ngIf=\"imageSrc() as src\"\n class=\"thumb\"\n [class.shape-square]=\"view().imageShape === 'square'\"\n [class.shape-rounded]=\"view().imageShape === 'rounded'\"\n [class.shape-circle]=\"view().imageShape === 'circle'\"\n [attr.width]=\"view().imageSizePx\"\n [attr.height]=\"view().imageSizePx\"\n [style.width.px]=\"view().imageSizePx\"\n [style.height.px]=\"view().imageSizePx\"\n [style.background]=\"view().imageBg ?? null\"\n [style.border]=\"view().imageBorder ? '1px solid rgba(0,0,0,0.06)' : 'none'\"\n [style.object-fit]=\"view().imageFit\"\n [src]=\"src\"\n [alt]=\"imageAlt()\"\n loading=\"lazy\"\n decoding=\"async\"\n (error)=\"onImgError($event)\"\n />\n\n <div class=\"card-col\">\n <div class=\"card-title\" *ngIf=\"hasComputedTitle()\">\n {{ node()?.jsonMeta?.title }}\n </div>\n\n <!-- Vista previa de atributos (derivados por el adapter) -->\n <div class=\"card-preview\" *ngIf=\"node()?.jsonMeta?.attributes as attrs\">\n <div *ngFor=\"let kv of objToPairs(attrs)\" class=\"kv\">\n <span class=\"k\">{{ displayKey(kv[0]) }}:</span>\n <span\n class=\"v\"\n [class.v-true]=\"kv[1] === true\"\n [class.v-false]=\"kv[1] === false\"\n [class.v-null]=\"kv[1] === null\"\n [class.nowrap]=\"isNoWrapKey(kv[0])\"\n [attr.title]=\"valueTitle(kv[1])\"\n >\n {{ displayValue(kv[1]) }}\n </span>\n </div>\n </div>\n\n <!-- Badges con conteos de arrays no escalares -->\n <div class=\"array-badges\" *ngIf=\"node()?.jsonMeta?.arrayCounts as arrs\">\n <ng-container *ngFor=\"let e of objToPairs(arrs)\">\n <span class=\"arr-badge\">\n {{ displayKey(e[0]) }}: {{ e[1] }} {{ e[1] === 1 ? \"item\" : \"items\" }}\n </span>\n </ng-container>\n </div>\n </div>\n </div>\n </div>\n </ng-template>\n</div>\n", styles: ["@charset \"UTF-8\";.schema-card{border-radius:12px;border:1px solid rgba(0,0,0,.08);background:#fff;box-shadow:0 2px 8px #00000014;-webkit-user-select:none;user-select:none;box-sizing:border-box;word-break:normal;overflow-wrap:normal;overflow:hidden;transition:left .16s ease,top .16s ease,opacity .12s ease}.collapse-btn{position:absolute;top:6px;right:8px;width:24px;height:24px;border-radius:6px;border:1px solid rgba(0,0,0,.12);background:#f8fafc;cursor:pointer;display:grid;place-items:center;padding:0;line-height:1;z-index:2}.collapse-btn span{font-size:.5rem}.chev{display:inline-block;transition:transform .16s ease}.chev.collapsed{transform:rotate(180deg)}.card-body{padding:12px 20px}.card-row{display:flex;align-items:flex-start;gap:10px}.card-col{flex:1 1 auto;min-width:0}.thumb{flex:0 0 auto;display:block;object-fit:contain;background:transparent;border:1px solid rgba(0,0,0,.06)}.shape-square{border-radius:4px}.shape-rounded{border-radius:8px}.shape-circle{border-radius:999px}.card-title{font-weight:700;font-size:14px;margin-bottom:6px}.card-preview{font-size:12px;line-height:1.28}.kv{display:grid;grid-template-columns:max-content max-content;column-gap:10px;align-items:baseline;margin:3px 0;padding-right:8px}.k{opacity:.66;font-weight:600;font-size:12px}.v{font-size:10.75px;line-height:1.25;letter-spacing:.1px;padding-right:6px;display:inline-block}.v-true{color:#16a34a;font-weight:700}.v-false{color:#dc2626;font-weight:700}.v-null{color:#6b7280;font-weight:700}.array-badges{margin-top:6px;display:flex;flex-wrap:wrap;gap:6px}.arr-badge{font-size:10px;background:#eef2ff;color:#3730a3;padding:2px 6px;border-radius:999px}.nowrap{white-space:nowrap!important;word-break:keep-all!important;overflow-wrap:normal!important}.schema-card.accent-true{border-color:#16a34a;box-shadow:0 2px 10px #16a34a26}.schema-card.accent-false{border-color:#dc2626;box-shadow:0 2px 10px #dc262626}.schema-card.accent-null{border-color:#6b7280;box-shadow:0 2px 10px #6b72802e}.schema-card.accent-fill-true{background:#16a34a1a}.schema-card.accent-fill-false{background:#dc26261a}.schema-card.accent-fill-null{background:#6b72801f}\n"] }]
|
|
384
|
+
}], propDecorators: { nodeClick: [{
|
|
385
|
+
type: Output
|
|
386
|
+
}], toggleRequest: [{
|
|
387
|
+
type: Output
|
|
388
|
+
}] } });
|
|
389
|
+
|
|
390
|
+
// URL: projects/schema-ng19/src/lib/components/schema-links.component.ts
|
|
391
|
+
/**
|
|
392
|
+
* Componente: SchemaLinksComponent
|
|
393
|
+
* ----------------------------------------------------------------------------
|
|
394
|
+
* Dibuja todas las aristas del grafo en un único SVG posicionado tras las cards.
|
|
395
|
+
*
|
|
396
|
+
* Responsabilidades:
|
|
397
|
+
* - Convertir `SchemaEdge.points` (calculados en el layout) en paths SVG.
|
|
398
|
+
* - Adaptar la forma según `settings.layout.linkStyle`:
|
|
399
|
+
* - "line" : segmento recto entre extremos.
|
|
400
|
+
* - "curve" : curva cúbica con tensión configurable (si el dx supera
|
|
401
|
+
* `straightThresholdDx`; en tramos muy cortos, usa línea).
|
|
402
|
+
* - "orthogonal" : secuencia de segmentos “L” usando todos los bends.
|
|
403
|
+
* - Emitir `linkClick` cuando se hace click sobre una arista.
|
|
404
|
+
*
|
|
405
|
+
* Parámetros relevantes de `SchemaSettings`:
|
|
406
|
+
* - colors.linkStroke : color del trazo (CSS).
|
|
407
|
+
* - colors.linkStrokeWidth : grosor del trazo (px).
|
|
408
|
+
* - layout.linkStyle : "orthogonal" | "curve" | "line".
|
|
409
|
+
* - layout.curveTension : control de curvatura (px). Rango sugerido: 20–200.
|
|
410
|
+
* - layout.straightThresholdDx: si |dx| < threshold → línea recta en modo "curve".
|
|
411
|
+
*
|
|
412
|
+
* Notas:
|
|
413
|
+
* - Este componente no calcula puntos; asume que `SchemaLayoutService` ya los
|
|
414
|
+
* generó y decide la forma final según el estilo actual.
|
|
415
|
+
* - `width/height` del SVG deben sincronizarse con el “stage” del contenedor.
|
|
416
|
+
*/
|
|
417
|
+
class SchemaLinksComponent {
|
|
418
|
+
/* ============================ Inputs ============================ */
|
|
419
|
+
/** Conjunto de aristas a dibujar (sus `points` provienen del layout). */
|
|
420
|
+
edges = input.required();
|
|
421
|
+
/** Settings efectivos para estilo y color de enlaces. */
|
|
422
|
+
settings = input(DEFAULT_SETTINGS);
|
|
423
|
+
/** Ancho del lienzo virtual (coherente con el “stage” del contenedor). */
|
|
424
|
+
width = 4000;
|
|
425
|
+
/** Alto del lienzo virtual (coherente con el “stage” del contenedor). */
|
|
426
|
+
height = 2000;
|
|
427
|
+
/* ============================ Outputs =========================== */
|
|
428
|
+
/** Evento emitido al hacer click en una arista. */
|
|
429
|
+
linkClick = new EventEmitter();
|
|
430
|
+
/* ============================ View derivada ===================== */
|
|
431
|
+
/**
|
|
432
|
+
* Vista aplanada para estilos de path.
|
|
433
|
+
* Usa `DEFAULT_SETTINGS` como fallback para garantizar estabilidad.
|
|
434
|
+
*/
|
|
435
|
+
view = computed(() => {
|
|
436
|
+
const s = this.settings() ?? DEFAULT_SETTINGS;
|
|
437
|
+
const colors = s.colors ?? DEFAULT_SETTINGS.colors;
|
|
438
|
+
const layout = s.layout ?? DEFAULT_SETTINGS.layout;
|
|
439
|
+
// Defaults seguros (numéricos y cadenas)
|
|
440
|
+
const linkStroke = colors.linkStroke ?? DEFAULT_SETTINGS.colors.linkStroke;
|
|
441
|
+
const linkStrokeWidth = colors.linkStrokeWidth ?? DEFAULT_SETTINGS.colors.linkStrokeWidth;
|
|
442
|
+
const linkStyle = layout.linkStyle ?? DEFAULT_SETTINGS.layout.linkStyle;
|
|
443
|
+
const curveTension = layout.curveTension ?? DEFAULT_SETTINGS.layout.curveTension;
|
|
444
|
+
const straightThresholdDx = layout.straightThresholdDx ?? DEFAULT_SETTINGS.layout.straightThresholdDx;
|
|
445
|
+
return {
|
|
446
|
+
linkStroke,
|
|
447
|
+
linkStrokeWidth,
|
|
448
|
+
linkStyle,
|
|
449
|
+
curveTension,
|
|
450
|
+
straightThresholdDx,
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
/* ============================ Render helpers ==================== */
|
|
454
|
+
/**
|
|
455
|
+
* Construye el atributo `d` del path según el estilo de enlace.
|
|
456
|
+
* Supone que `e.points` contiene al menos el punto de inicio y fin.
|
|
457
|
+
*/
|
|
458
|
+
pathFor(e) {
|
|
459
|
+
const pts = e.points ?? [];
|
|
460
|
+
if (pts.length === 0)
|
|
461
|
+
return "";
|
|
462
|
+
const v = this.view();
|
|
463
|
+
const style = v.linkStyle;
|
|
464
|
+
// Estilo "line": un segmento recto de A a B
|
|
465
|
+
if (style === "line") {
|
|
466
|
+
const a = pts[0];
|
|
467
|
+
const b = pts[pts.length - 1];
|
|
468
|
+
return `M ${a.x},${a.y} L ${b.x},${b.y}`;
|
|
469
|
+
}
|
|
470
|
+
// Estilo "curve": curva cúbica con tensión en X
|
|
471
|
+
if (style === "curve") {
|
|
472
|
+
const a = pts[0];
|
|
473
|
+
const b = pts[pts.length - 1];
|
|
474
|
+
// Evita curvas “demasiado cortas”: usa línea recta cuando |dx| < threshold
|
|
475
|
+
const threshold = Number.isFinite(v.straightThresholdDx)
|
|
476
|
+
? v.straightThresholdDx
|
|
477
|
+
: DEFAULT_SETTINGS.layout.straightThresholdDx;
|
|
478
|
+
const dxAbs = Math.abs(b.x - a.x);
|
|
479
|
+
if (dxAbs < (threshold ?? 0)) {
|
|
480
|
+
return `M ${a.x},${a.y} L ${b.x},${b.y}`;
|
|
481
|
+
}
|
|
482
|
+
// Asegura límites razonables de tensión
|
|
483
|
+
const baseT = Number.isFinite(v.curveTension) ? v.curveTension : DEFAULT_SETTINGS.layout.curveTension;
|
|
484
|
+
const t = Math.max(20, Math.min(200, baseT ?? 30));
|
|
485
|
+
const dir = Math.sign(b.x - a.x) || 1;
|
|
486
|
+
const dy = b.y - a.y;
|
|
487
|
+
// Puntos de control con separación horizontal `t`
|
|
488
|
+
let c1x = a.x + dir * t;
|
|
489
|
+
let c1y = a.y;
|
|
490
|
+
let c2x = b.x - dir * t;
|
|
491
|
+
let c2y = b.y;
|
|
492
|
+
// Si el delta vertical es muy pequeño, introduce una “panza” suave
|
|
493
|
+
if (Math.abs(dy) < 1) {
|
|
494
|
+
const bow = Math.max(8, Math.min(96, t * 0.5));
|
|
495
|
+
c1y = a.y - bow;
|
|
496
|
+
c2y = b.y + bow;
|
|
497
|
+
}
|
|
498
|
+
return `M ${a.x},${a.y} C ${c1x},${c1y} ${c2x},${c2y} ${b.x},${b.y}`;
|
|
499
|
+
}
|
|
500
|
+
// Estilo "orthogonal": usa todos los puntos como quiebres (L)
|
|
501
|
+
if (pts.length === 1)
|
|
502
|
+
return `M ${pts[0].x},${pts[0].y}`;
|
|
503
|
+
const first = pts[0];
|
|
504
|
+
let d = `M ${first.x},${first.y}`;
|
|
505
|
+
for (let i = 1; i < pts.length; i++) {
|
|
506
|
+
const p = pts[i];
|
|
507
|
+
d += ` L ${p.x},${p.y}`;
|
|
508
|
+
}
|
|
509
|
+
return d;
|
|
510
|
+
}
|
|
511
|
+
/* ============================ Eventos ========================== */
|
|
512
|
+
/** Maneja el click en una arista sin burbujarlo al stage. */
|
|
513
|
+
onLinkClick(e, ev) {
|
|
514
|
+
ev.stopPropagation();
|
|
515
|
+
this.linkClick.emit(e);
|
|
516
|
+
}
|
|
517
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaLinksComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
518
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.18", type: SchemaLinksComponent, isStandalone: true, selector: "schema-links", inputs: { edges: { classPropertyName: "edges", publicName: "edges", isSignal: true, isRequired: true, transformFunction: null }, settings: { classPropertyName: "settings", publicName: "settings", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: false, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { linkClick: "linkClick" }, ngImport: i0, template: "<svg class=\"schema-links\" [attr.width]=\"width\" [attr.height]=\"height\">\n <g>\n @for (e of edges(); track e.id) {\n <path\n [attr.d]=\"pathFor(e)\"\n [attr.stroke]=\"view().linkStroke\"\n [attr.stroke-width]=\"view().linkStrokeWidth\"\n fill=\"none\"\n (click)=\"onLinkClick(e, $event)\"\n ></path>\n }\n </g>\n</svg>\n", styles: [".schema-links{position:absolute;left:0;top:0;pointer-events:auto;overflow:visible;z-index:0}path{cursor:pointer}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
519
|
+
}
|
|
520
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaLinksComponent, decorators: [{
|
|
521
|
+
type: Component,
|
|
522
|
+
args: [{ selector: "schema-links", standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg class=\"schema-links\" [attr.width]=\"width\" [attr.height]=\"height\">\n <g>\n @for (e of edges(); track e.id) {\n <path\n [attr.d]=\"pathFor(e)\"\n [attr.stroke]=\"view().linkStroke\"\n [attr.stroke-width]=\"view().linkStrokeWidth\"\n fill=\"none\"\n (click)=\"onLinkClick(e, $event)\"\n ></path>\n }\n </g>\n</svg>\n", styles: [".schema-links{position:absolute;left:0;top:0;pointer-events:auto;overflow:visible;z-index:0}path{cursor:pointer}\n"] }]
|
|
523
|
+
}], propDecorators: { width: [{
|
|
524
|
+
type: Input
|
|
525
|
+
}], height: [{
|
|
526
|
+
type: Input
|
|
527
|
+
}], linkClick: [{
|
|
528
|
+
type: Output
|
|
529
|
+
}] } });
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Servicio: JsonAdapterService
|
|
533
|
+
* Convierte un JSON arbitrario en un grafo normalizado (nodes + edges).
|
|
534
|
+
* - Aplica reglas de extracción (titleKeyPriority, hiddenKeysGlobal, etc.).
|
|
535
|
+
* - Conservar rutas únicas (`jsonPath`) como IDs de nodos.
|
|
536
|
+
* - Incluye metadatos útiles (childOrder, arrayCounts, atributos).
|
|
537
|
+
* - No calcula layout.
|
|
538
|
+
*/
|
|
539
|
+
class JsonAdapterService {
|
|
540
|
+
/**
|
|
541
|
+
* Normaliza un JSON a grafo de nodos y aristas.
|
|
542
|
+
* @param input Objeto JSON arbitrario.
|
|
543
|
+
* @param opts Configuración parcial (`SchemaSettings`).
|
|
544
|
+
*/
|
|
545
|
+
normalize(input, opts = {}) {
|
|
546
|
+
// Merge por secciones
|
|
547
|
+
const settings = {
|
|
548
|
+
colors: { ...DEFAULT_SETTINGS.colors, ...(opts.colors ?? {}) },
|
|
549
|
+
layout: { ...DEFAULT_SETTINGS.layout, ...(opts.layout ?? {}) },
|
|
550
|
+
dataView: { ...DEFAULT_SETTINGS.dataView, ...(opts.dataView ?? {}) },
|
|
551
|
+
messages: { ...DEFAULT_SETTINGS.messages, ...(opts.messages ?? {}) },
|
|
552
|
+
viewport: { ...DEFAULT_SETTINGS.viewport, ...(opts.viewport ?? {}) },
|
|
553
|
+
};
|
|
554
|
+
// Accesos rápidos
|
|
555
|
+
const dv = settings.dataView;
|
|
556
|
+
const titleKeyPriority = dv.titleKeyPriority ?? [];
|
|
557
|
+
const hiddenKeysGlobal = dv.hiddenKeysGlobal ?? [];
|
|
558
|
+
const treatScalarArraysAsAttribute = dv.treatScalarArraysAsAttribute ?? false;
|
|
559
|
+
const previewMaxKeys = dv.previewMaxKeys ?? 999;
|
|
560
|
+
const defaultNodeSize = dv.defaultNodeSize ?? { width: 256, height: 64 };
|
|
561
|
+
const maxDepth = dv.maxDepth ?? null;
|
|
562
|
+
// Claves a ocultar explícitamente en preview
|
|
563
|
+
const imageKey = dv.showImage && dv.showImage.trim() !== "" ? dv.showImage : null;
|
|
564
|
+
const accentKey = settings.colors.accentByKey && settings.colors.accentByKey.trim() !== "" ? settings.colors.accentByKey : null;
|
|
565
|
+
const nodes = [];
|
|
566
|
+
const edges = [];
|
|
567
|
+
// Helpers
|
|
568
|
+
const isScalar = (v) => v === null || ["string", "number", "boolean"].includes(typeof v);
|
|
569
|
+
/** Determina si un array contiene solo escalares. */
|
|
570
|
+
const arrayIsScalar = (arr) => Array.isArray(arr) && arr.length > 0 && arr.every(isScalar);
|
|
571
|
+
/**
|
|
572
|
+
* Escoge un título a partir de prioridades de clave.
|
|
573
|
+
* Retorna también la clave usada para no duplicar en atributos.
|
|
574
|
+
*/ const pickTitle = (obj, priorities) => {
|
|
575
|
+
if (Array.isArray(priorities) && priorities.length > 0) {
|
|
576
|
+
for (const k of priorities) {
|
|
577
|
+
const v = obj?.[k];
|
|
578
|
+
if (v != null && String(v).trim() !== "") {
|
|
579
|
+
return { title: String(v), usedKey: k };
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return { title: "", usedKey: undefined };
|
|
584
|
+
};
|
|
585
|
+
/**
|
|
586
|
+
* Construye un subconjunto de atributos (clave → valor) para vista previa.
|
|
587
|
+
* - Excluye claves ocultas o la usada como título.
|
|
588
|
+
* - Incluye escalares y arrays escalares (si está habilitado).
|
|
589
|
+
*/
|
|
590
|
+
const buildPreviewAttributes = (obj, usedKey) => {
|
|
591
|
+
const toHide = new Set(hiddenKeysGlobal);
|
|
592
|
+
if (usedKey)
|
|
593
|
+
toHide.add(usedKey);
|
|
594
|
+
if (imageKey)
|
|
595
|
+
toHide.add(imageKey);
|
|
596
|
+
if (accentKey)
|
|
597
|
+
toHide.add(accentKey);
|
|
598
|
+
const entries = [];
|
|
599
|
+
for (const [k, v] of Object.entries(obj ?? {})) {
|
|
600
|
+
if (toHide.has(k))
|
|
601
|
+
continue;
|
|
602
|
+
if (isScalar(v)) {
|
|
603
|
+
entries.push([k, v]);
|
|
604
|
+
}
|
|
605
|
+
else if (Array.isArray(v) && treatScalarArraysAsAttribute && arrayIsScalar(v)) {
|
|
606
|
+
entries.push([k, v.join(", ")]);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return Object.fromEntries(entries.slice(0, previewMaxKeys));
|
|
610
|
+
};
|
|
611
|
+
/** Determina si un objeto puede representarse como entidad (nodo). */
|
|
612
|
+
const isEntity = (obj) => {
|
|
613
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj))
|
|
614
|
+
return false;
|
|
615
|
+
return Object.values(obj).some(isScalar);
|
|
616
|
+
};
|
|
617
|
+
/** Retorna un mapa clave → longitud de arrays no escalares. */
|
|
618
|
+
const arrayCountsOf = (obj) => {
|
|
619
|
+
const out = {};
|
|
620
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
621
|
+
if (Array.isArray(v) && !(v.length > 0 && v.every(isScalar))) {
|
|
622
|
+
out[k] = v.length;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return out;
|
|
626
|
+
};
|
|
627
|
+
// Contadores auxiliares
|
|
628
|
+
const childCounter = new Map();
|
|
629
|
+
const childOrderByParent = new Map();
|
|
630
|
+
/**
|
|
631
|
+
* Crea un nodo y opcionalmente una arista hacia su padre.
|
|
632
|
+
*/
|
|
633
|
+
const addNode = (jsonPath, obj, parentId) => {
|
|
634
|
+
const { title, usedKey } = pickTitle(obj, titleKeyPriority);
|
|
635
|
+
const attrs = buildPreviewAttributes(obj, usedKey);
|
|
636
|
+
// Orden relativo entre hermanos
|
|
637
|
+
let childOrder = undefined;
|
|
638
|
+
if (parentId) {
|
|
639
|
+
const idx = childOrderByParent.get(parentId) ?? 0;
|
|
640
|
+
childOrder = idx;
|
|
641
|
+
childOrderByParent.set(parentId, idx + 1);
|
|
642
|
+
}
|
|
643
|
+
const node = {
|
|
644
|
+
id: jsonPath,
|
|
645
|
+
jsonPath,
|
|
646
|
+
label: title,
|
|
647
|
+
data: obj,
|
|
648
|
+
jsonMeta: {
|
|
649
|
+
title,
|
|
650
|
+
titleKeyUsed: usedKey,
|
|
651
|
+
attributes: attrs,
|
|
652
|
+
childrenCount: 0,
|
|
653
|
+
arrayCounts: arrayCountsOf(obj),
|
|
654
|
+
childOrder,
|
|
655
|
+
},
|
|
656
|
+
width: defaultNodeSize.width,
|
|
657
|
+
height: defaultNodeSize.height,
|
|
658
|
+
};
|
|
659
|
+
nodes.push(node);
|
|
660
|
+
if (parentId) {
|
|
661
|
+
edges.push({
|
|
662
|
+
id: `${parentId}__${node.id}`,
|
|
663
|
+
source: parentId,
|
|
664
|
+
target: node.id,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
return node.id;
|
|
668
|
+
};
|
|
669
|
+
/**
|
|
670
|
+
* Recorrido recursivo del JSON.
|
|
671
|
+
* - Usa `jsonPath` para mantener rutas únicas.
|
|
672
|
+
* - Crea nodos solo cuando `isEntity(obj)` es verdadero.
|
|
673
|
+
*/
|
|
674
|
+
const traverse = (val, path, parentId, depth = 0) => {
|
|
675
|
+
if (maxDepth !== null && depth > maxDepth)
|
|
676
|
+
return;
|
|
677
|
+
if (Array.isArray(val)) {
|
|
678
|
+
val.forEach((c, i) => traverse(c, `${path}[${i}]`, parentId, depth + 1));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (val && typeof val === "object") {
|
|
682
|
+
const obj = val;
|
|
683
|
+
let myId = parentId;
|
|
684
|
+
if (isEntity(obj)) {
|
|
685
|
+
myId = addNode(path, obj, parentId);
|
|
686
|
+
if (parentId)
|
|
687
|
+
childCounter.set(parentId, (childCounter.get(parentId) ?? 0) + 1);
|
|
688
|
+
}
|
|
689
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
690
|
+
if (isScalar(v))
|
|
691
|
+
continue;
|
|
692
|
+
if (Array.isArray(v)) {
|
|
693
|
+
const scalarArr = v.length > 0 && v.every(isScalar);
|
|
694
|
+
if (scalarArr && treatScalarArraysAsAttribute)
|
|
695
|
+
continue;
|
|
696
|
+
v.forEach((c, i) => traverse(c, `${path}.${k}[${i}]`, myId, depth + 1));
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
traverse(v, `${path}.${k}`, myId, depth + 1);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
traverse(input, "$", undefined, 0);
|
|
705
|
+
// Completar metadatos (childrenCount)
|
|
706
|
+
nodes.forEach((n) => {
|
|
707
|
+
n.jsonMeta = n.jsonMeta ?? {};
|
|
708
|
+
n.jsonMeta.childrenCount = childCounter.get(n.id) ?? 0;
|
|
709
|
+
});
|
|
710
|
+
return { nodes, edges, meta: {} };
|
|
711
|
+
}
|
|
712
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: JsonAdapterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
713
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: JsonAdapterService, providedIn: "root" });
|
|
714
|
+
}
|
|
715
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: JsonAdapterService, decorators: [{
|
|
716
|
+
type: Injectable,
|
|
717
|
+
args: [{ providedIn: "root" }]
|
|
718
|
+
}] });
|
|
719
|
+
|
|
720
|
+
// URL: projects/schema-ng19/src/lib/services/json-adapter.service.ts
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Servicio: SchemaLayoutService
|
|
724
|
+
* ----------------------------------------------------------------------------
|
|
725
|
+
* Calcula posiciones (x,y) para un grafo normalizado (nodos/aristas) usando
|
|
726
|
+
* un layout jerárquico tipo "tidy tree" en dos variantes:
|
|
727
|
+
* - layoutDirection = "RIGHT": el árbol crece de izquierda → derecha.
|
|
728
|
+
* - layoutDirection = "DOWN" : el árbol crece de arriba → abajo.
|
|
729
|
+
*
|
|
730
|
+
* Características:
|
|
731
|
+
* - Respeta el orden de los hijos según `jsonMeta.childOrder` (estable, derivado del JSON).
|
|
732
|
+
* - Soporta alineación del padre con respecto a los hijos: "firstChild" o "center".
|
|
733
|
+
* - Genera puntos para aristas compatibles con estilos: "orthogonal", "curve" y "line".
|
|
734
|
+
* - Mantiene "pins" en `meta.pinY` o `meta.pinX` (pre-alistado por el componente) para anclajes.
|
|
735
|
+
*/
|
|
736
|
+
/**
|
|
737
|
+
* Servicio de layout tipo “tidy tree” para RIGHT/DOWN.
|
|
738
|
+
* No gestiona pan/zoom ni overlays; solo posiciones y trayectorias.
|
|
739
|
+
*/
|
|
740
|
+
class SchemaLayoutService {
|
|
741
|
+
/**
|
|
742
|
+
* Aplica layout al grafo.
|
|
743
|
+
* @param g Grafo normalizado (nodos/aristas).
|
|
744
|
+
* @param settings Configuración parcial. Se fusiona con DEFAULT_SETTINGS.
|
|
745
|
+
* @returns Grafo con nodos posicionados y aristas con `points`.
|
|
746
|
+
*/
|
|
747
|
+
async layout(g, settings = DEFAULT_SETTINGS) {
|
|
748
|
+
const s = this.mergeSettings(settings);
|
|
749
|
+
// Dirección principal y opciones de alineación/estilo
|
|
750
|
+
const dir = s.layout?.layoutDirection ?? DEFAULT_SETTINGS.layout.layoutDirection;
|
|
751
|
+
const alignFirstChild = (s.layout?.layoutAlign ?? DEFAULT_SETTINGS.layout.layoutAlign) === "firstChild";
|
|
752
|
+
const linkStyle = s.layout?.linkStyle ?? DEFAULT_SETTINGS.layout.linkStyle;
|
|
753
|
+
// Gaps (separaciones) con defaults seguros
|
|
754
|
+
const GAP_X = s.layout?.columnGapPx ?? DEFAULT_SETTINGS.layout.columnGapPx ?? 0;
|
|
755
|
+
const GAP_Y = s.layout?.rowGapPx ?? DEFAULT_SETTINGS.layout.rowGapPx ?? 0;
|
|
756
|
+
// Índices de consulta rápida
|
|
757
|
+
const nodesById = new Map(g.nodes.map((n) => [n.id, n]));
|
|
758
|
+
const childrenById = new Map();
|
|
759
|
+
const parentsById = new Map();
|
|
760
|
+
for (const n of g.nodes) {
|
|
761
|
+
childrenById.set(n.id, []);
|
|
762
|
+
parentsById.set(n.id, []);
|
|
763
|
+
}
|
|
764
|
+
for (const e of g.edges) {
|
|
765
|
+
if (childrenById.has(e.source))
|
|
766
|
+
childrenById.get(e.source).push(e.target);
|
|
767
|
+
if (parentsById.has(e.target))
|
|
768
|
+
parentsById.get(e.target).push(e.source);
|
|
769
|
+
}
|
|
770
|
+
// Orden estable por childOrder (si empatan, ordena por id)
|
|
771
|
+
for (const [pid, arr] of childrenById) {
|
|
772
|
+
arr.sort((aId, bId) => {
|
|
773
|
+
const a = nodesById.get(aId);
|
|
774
|
+
const b = nodesById.get(bId);
|
|
775
|
+
const ao = a?.jsonMeta?.childOrder ?? 0;
|
|
776
|
+
const bo = b?.jsonMeta?.childOrder ?? 0;
|
|
777
|
+
return ao === bo ? (a?.id ?? "").localeCompare(b?.id ?? "") : ao - bo;
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
// Raíces (nodos sin padres)
|
|
781
|
+
const roots = g.nodes.filter((n) => (parentsById.get(n.id)?.length ?? 0) === 0);
|
|
782
|
+
// Profundidad por nodo (BFS)
|
|
783
|
+
const depthById = new Map();
|
|
784
|
+
const q = [...roots];
|
|
785
|
+
for (const r of roots)
|
|
786
|
+
depthById.set(r.id, 0);
|
|
787
|
+
while (q.length) {
|
|
788
|
+
const n = q.shift();
|
|
789
|
+
const d = depthById.get(n.id) ?? 0;
|
|
790
|
+
for (const cid of childrenById.get(n.id) ?? []) {
|
|
791
|
+
if (!depthById.has(cid)) {
|
|
792
|
+
depthById.set(cid, d + 1);
|
|
793
|
+
const cn = nodesById.get(cid);
|
|
794
|
+
if (cn)
|
|
795
|
+
q.push(cn);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// Helpers de tamaño (con fallback a defaults)
|
|
800
|
+
const getW = (n) => {
|
|
801
|
+
const fallback = DEFAULT_SETTINGS.dataView.defaultNodeSize?.width ?? 1;
|
|
802
|
+
const w = n.width ?? fallback;
|
|
803
|
+
return Number.isFinite(w) && w > 0 ? w : fallback;
|
|
804
|
+
};
|
|
805
|
+
const getH = (n) => {
|
|
806
|
+
const fallback = DEFAULT_SETTINGS.dataView.defaultNodeSize?.height ?? 1;
|
|
807
|
+
const h = n.height ?? fallback;
|
|
808
|
+
return Number.isFinite(h) && h > 0 ? h : fallback;
|
|
809
|
+
};
|
|
810
|
+
// Tamaño acumulado de cada subárbol en el eje secundario (para apilado)
|
|
811
|
+
const subtreeSize = new Map();
|
|
812
|
+
const measureSubtree = (id) => {
|
|
813
|
+
const node = nodesById.get(id);
|
|
814
|
+
const kids = childrenById.get(id) ?? [];
|
|
815
|
+
if (!node) {
|
|
816
|
+
subtreeSize.set(id, 0);
|
|
817
|
+
return 0;
|
|
818
|
+
}
|
|
819
|
+
if (kids.length === 0) {
|
|
820
|
+
const leafSize = dir === "RIGHT" ? getH(node) : getW(node);
|
|
821
|
+
subtreeSize.set(id, leafSize);
|
|
822
|
+
return leafSize;
|
|
823
|
+
}
|
|
824
|
+
let sum = 0;
|
|
825
|
+
for (let i = 0; i < kids.length; i++) {
|
|
826
|
+
sum += measureSubtree(kids[i]);
|
|
827
|
+
if (i < kids.length - 1)
|
|
828
|
+
sum += GAP_Y; // espacio entre hermanos
|
|
829
|
+
}
|
|
830
|
+
subtreeSize.set(id, sum);
|
|
831
|
+
return sum;
|
|
832
|
+
};
|
|
833
|
+
for (const r of roots)
|
|
834
|
+
measureSubtree(r.id);
|
|
835
|
+
// Offset acumulado por profundidad en el eje principal
|
|
836
|
+
const depths = Array.from(depthById.values());
|
|
837
|
+
const maxDepth = depths.length ? Math.max(0, ...depths) : 0;
|
|
838
|
+
const sizeByDepth = new Array(maxDepth + 1).fill(0);
|
|
839
|
+
for (let d = 0; d <= maxDepth; d++) {
|
|
840
|
+
const nodesAtD = g.nodes.filter((n) => (depthById.get(n.id) ?? 0) === d);
|
|
841
|
+
sizeByDepth[d] = dir === "RIGHT" ? Math.max(1, ...nodesAtD.map(getW), 1) : Math.max(1, ...nodesAtD.map(getH), 1);
|
|
842
|
+
}
|
|
843
|
+
const mainOffset = new Array(maxDepth + 1).fill(0);
|
|
844
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
845
|
+
mainOffset[d] = mainOffset[d - 1] + sizeByDepth[d - 1] + GAP_X;
|
|
846
|
+
}
|
|
847
|
+
// Preparación de "pin" en meta (mapa de centros por eje secundario)
|
|
848
|
+
const meta = g.meta ?? {};
|
|
849
|
+
const pinKey = dir === "RIGHT" ? "pinY" : "pinX";
|
|
850
|
+
if (!meta[pinKey])
|
|
851
|
+
meta[pinKey] = {};
|
|
852
|
+
const pin = meta[pinKey] ?? {};
|
|
853
|
+
/**
|
|
854
|
+
* Coloca recursivamente un subárbol:
|
|
855
|
+
* - depth: profundidad actual
|
|
856
|
+
* - start: posición inicial en el eje secundario para apilar hijos
|
|
857
|
+
* Retorna el tamaño del subárbol en el eje secundario.
|
|
858
|
+
*/
|
|
859
|
+
const placeSubtree = (id, depth, start) => {
|
|
860
|
+
const node = nodesById.get(id);
|
|
861
|
+
const kids = childrenById.get(id) ?? [];
|
|
862
|
+
const mySize = subtreeSize.get(id) ?? 0;
|
|
863
|
+
const mainPos = mainOffset[depth] ?? 0;
|
|
864
|
+
if (!node)
|
|
865
|
+
return mySize;
|
|
866
|
+
// Hoja: centra sobre el rango [start, start+mySize]
|
|
867
|
+
if (kids.length === 0) {
|
|
868
|
+
const centerSec = start + mySize / 2;
|
|
869
|
+
if (dir === "RIGHT") {
|
|
870
|
+
node.x = Math.round(mainPos);
|
|
871
|
+
node.y = Math.round(centerSec - getH(node) / 2);
|
|
872
|
+
pin[node.id] = Math.round((node.y ?? 0) + getH(node) / 2);
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
node.y = Math.round(mainPos);
|
|
876
|
+
node.x = Math.round(centerSec - getW(node) / 2);
|
|
877
|
+
pin[node.id] = Math.round((node.x ?? 0) + getW(node) / 2);
|
|
878
|
+
}
|
|
879
|
+
return mySize;
|
|
880
|
+
}
|
|
881
|
+
// Nodo con hijos: posiciona hijos apilados y centra el padre
|
|
882
|
+
let cursor = start;
|
|
883
|
+
const childCenters = [];
|
|
884
|
+
for (let i = 0; i < kids.length; i++) {
|
|
885
|
+
const cid = kids[i];
|
|
886
|
+
const cNode = nodesById.get(cid);
|
|
887
|
+
const cSize = subtreeSize.get(cid) ?? (cNode ? (dir === "RIGHT" ? getH(cNode) : getW(cNode)) : 0);
|
|
888
|
+
placeSubtree(cid, depth + 1, cursor);
|
|
889
|
+
if (cNode) {
|
|
890
|
+
const cCenter = dir === "RIGHT" ? (cNode.y ?? 0) + getH(cNode) / 2 : (cNode.x ?? 0) + getW(cNode) / 2;
|
|
891
|
+
childCenters.push(cCenter);
|
|
892
|
+
}
|
|
893
|
+
cursor += cSize + (i < kids.length - 1 ? GAP_Y : 0);
|
|
894
|
+
}
|
|
895
|
+
// Objetivo de centrado del padre
|
|
896
|
+
const targetCenter = alignFirstChild || childCenters.length === 0
|
|
897
|
+
? (childCenters[0] ?? 0)
|
|
898
|
+
: childCenters.reduce((a, b) => a + b, 0) / Math.max(1, childCenters.length);
|
|
899
|
+
if (dir === "RIGHT") {
|
|
900
|
+
node.x = Math.round(mainPos);
|
|
901
|
+
node.y = Math.round(targetCenter - getH(node) / 2);
|
|
902
|
+
pin[node.id] = Math.round(targetCenter);
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
node.y = Math.round(mainPos);
|
|
906
|
+
node.x = Math.round(targetCenter - getW(node) / 2);
|
|
907
|
+
pin[node.id] = Math.round(targetCenter);
|
|
908
|
+
}
|
|
909
|
+
return mySize;
|
|
910
|
+
};
|
|
911
|
+
// Coloca cada raíz con un margen superior/izquierdo inicial
|
|
912
|
+
let globalCursor = 40;
|
|
913
|
+
for (let i = 0; i < roots.length; i++) {
|
|
914
|
+
const r = roots[i];
|
|
915
|
+
const rSize = subtreeSize.get(r.id) ?? (dir === "RIGHT" ? getH(r) : getW(r));
|
|
916
|
+
placeSubtree(r.id, 0, globalCursor);
|
|
917
|
+
globalCursor += rSize + (i < roots.length - 1 ? (s.layout.rowGapPx ?? 0) : 0);
|
|
918
|
+
}
|
|
919
|
+
// Generación de puntos para aristas en función del estilo
|
|
920
|
+
const edges = g.edges.map((e) => {
|
|
921
|
+
const a = nodesById.get(e.source);
|
|
922
|
+
const b = nodesById.get(e.target);
|
|
923
|
+
if (!a || !b)
|
|
924
|
+
return { ...e, points: [] };
|
|
925
|
+
if (dir === "RIGHT") {
|
|
926
|
+
const ax = (a.x ?? 0) + getW(a);
|
|
927
|
+
const ay = (a.y ?? 0) + Math.round(getH(a) / 2);
|
|
928
|
+
const bx = b.x ?? 0;
|
|
929
|
+
const by = (b.y ?? 0) + Math.round(getH(b) / 2);
|
|
930
|
+
if (linkStyle === "orthogonal") {
|
|
931
|
+
const midX = Math.round((ax + bx) / 2);
|
|
932
|
+
return {
|
|
933
|
+
...e,
|
|
934
|
+
points: [
|
|
935
|
+
{ x: ax, y: ay },
|
|
936
|
+
{ x: midX, y: ay },
|
|
937
|
+
{ x: midX, y: by },
|
|
938
|
+
{ x: bx, y: by },
|
|
939
|
+
],
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
// curve/line: el componente define la forma final (curva o línea)
|
|
943
|
+
return {
|
|
944
|
+
...e,
|
|
945
|
+
points: [
|
|
946
|
+
{ x: ax, y: ay },
|
|
947
|
+
{ x: bx, y: by },
|
|
948
|
+
],
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
const ax = (a.x ?? 0) + Math.round(getW(a) / 2);
|
|
953
|
+
const ay = (a.y ?? 0) + getH(a);
|
|
954
|
+
const bx = (b.x ?? 0) + Math.round(getW(b) / 2);
|
|
955
|
+
const by = b.y ?? 0;
|
|
956
|
+
if (linkStyle === "orthogonal") {
|
|
957
|
+
const midY = Math.round((ay + by) / 2);
|
|
958
|
+
return {
|
|
959
|
+
...e,
|
|
960
|
+
points: [
|
|
961
|
+
{ x: ax, y: ay },
|
|
962
|
+
{ x: ax, y: midY },
|
|
963
|
+
{ x: bx, y: midY },
|
|
964
|
+
{ x: bx, y: by },
|
|
965
|
+
],
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
return {
|
|
969
|
+
...e,
|
|
970
|
+
points: [
|
|
971
|
+
{ x: ax, y: ay },
|
|
972
|
+
{ x: bx, y: by },
|
|
973
|
+
],
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
// Retorna copias superficiales (no muta el original `g`)
|
|
978
|
+
return { nodes: g.nodes.map((n) => ({ ...n })), edges, meta: { ...(g.meta ?? {}) } };
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Fusiona settings parciales con DEFAULT_SETTINGS, sección por sección.
|
|
982
|
+
*/
|
|
983
|
+
mergeSettings(s) {
|
|
984
|
+
return {
|
|
985
|
+
colors: { ...DEFAULT_SETTINGS.colors, ...(s.colors ?? {}) },
|
|
986
|
+
layout: { ...DEFAULT_SETTINGS.layout, ...(s.layout ?? {}) },
|
|
987
|
+
dataView: { ...DEFAULT_SETTINGS.dataView, ...(s.dataView ?? {}) },
|
|
988
|
+
messages: { ...DEFAULT_SETTINGS.messages, ...(s.messages ?? {}) },
|
|
989
|
+
viewport: { ...DEFAULT_SETTINGS.viewport, ...(s.viewport ?? {}) },
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaLayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
993
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaLayoutService, providedIn: "root" });
|
|
994
|
+
}
|
|
995
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaLayoutService, decorators: [{
|
|
996
|
+
type: Injectable,
|
|
997
|
+
args: [{ providedIn: "root" }]
|
|
998
|
+
}] });
|
|
999
|
+
|
|
1000
|
+
// URL: projects/schema-ng19/src/lib/services/schema-layout.service.ts
|
|
1001
|
+
|
|
1002
|
+
// URL: projects/schema-ng19/src/lib/components/schema.component.ts
|
|
1003
|
+
class SchemaComponent {
|
|
1004
|
+
adapter;
|
|
1005
|
+
layoutService;
|
|
1006
|
+
// ===== Inputs =====
|
|
1007
|
+
data = input();
|
|
1008
|
+
settings = input(null);
|
|
1009
|
+
cardTemplate = input(null);
|
|
1010
|
+
// Overlays
|
|
1011
|
+
isLoading = input(false);
|
|
1012
|
+
isError = input(false);
|
|
1013
|
+
emptyMessage = input("No hay datos para mostrar");
|
|
1014
|
+
loadingMessage = input("Cargando…");
|
|
1015
|
+
errorMessage = input("Error al cargar el esquema");
|
|
1016
|
+
// Viewport
|
|
1017
|
+
viewportHeight = signal(800);
|
|
1018
|
+
minViewportHeight = signal(480);
|
|
1019
|
+
showToolbar = signal(true);
|
|
1020
|
+
// ===== Outputs =====
|
|
1021
|
+
nodeClick = new EventEmitter();
|
|
1022
|
+
linkClick = new EventEmitter();
|
|
1023
|
+
// ===== Estado de grafo =====
|
|
1024
|
+
fullGraph = signal({ nodes: [], edges: [] });
|
|
1025
|
+
graph = signal({ nodes: [], edges: [] });
|
|
1026
|
+
nodes = computed(() => this.graph().nodes);
|
|
1027
|
+
edges = computed(() => this.graph().edges);
|
|
1028
|
+
rootRef;
|
|
1029
|
+
// ===== Pan/zoom =====
|
|
1030
|
+
scale = signal(1);
|
|
1031
|
+
minScale = signal(0.2);
|
|
1032
|
+
maxScale = signal(3);
|
|
1033
|
+
tx = signal(0);
|
|
1034
|
+
ty = signal(0);
|
|
1035
|
+
dragging = false;
|
|
1036
|
+
lastX = 0;
|
|
1037
|
+
lastY = 0;
|
|
1038
|
+
lastViewport = null;
|
|
1039
|
+
/** Último punto de interacción sobre la stage (coordenadas de pantalla relativas al root). */
|
|
1040
|
+
lastPointerScreen = null;
|
|
1041
|
+
wheelRaf = null;
|
|
1042
|
+
pendingWheel = null;
|
|
1043
|
+
transform = computed(() => `translate(${this.tx()}px, ${this.ty()}px) scale(${this.scale()})`);
|
|
1044
|
+
virtualWidth = 12000;
|
|
1045
|
+
virtualHeight = 6000;
|
|
1046
|
+
// ===== Toolbar overrides =====
|
|
1047
|
+
opt_linkStyle = signal("orthogonal");
|
|
1048
|
+
opt_layoutAlign = signal("firstChild");
|
|
1049
|
+
opt_layoutDirection = signal("RIGHT");
|
|
1050
|
+
// ===== Mensajes derivados =====
|
|
1051
|
+
isLoadingView = computed(() => this.settings()?.messages?.isLoading ?? this.isLoading());
|
|
1052
|
+
isErrorView = computed(() => this.settings()?.messages?.isError ?? this.isError());
|
|
1053
|
+
emptyMessageView = computed(() => this.settings()?.messages?.emptyMessage ?? this.emptyMessage());
|
|
1054
|
+
loadingMessageView = computed(() => this.settings()?.messages?.loadingMessage ?? this.loadingMessage());
|
|
1055
|
+
errorMessageView = computed(() => this.settings()?.messages?.errorMessage ?? this.errorMessage());
|
|
1056
|
+
// ===== Collapse/expand =====
|
|
1057
|
+
childrenById = new Map();
|
|
1058
|
+
parentsById = new Map();
|
|
1059
|
+
collapsed = new Set();
|
|
1060
|
+
measureIdsToCheck = null;
|
|
1061
|
+
lastDataRef;
|
|
1062
|
+
lastSettingsRef;
|
|
1063
|
+
hasComputedOnce = false;
|
|
1064
|
+
enableCollapse = computed(() => {
|
|
1065
|
+
const b = this.baseSettings();
|
|
1066
|
+
return b.dataView?.enableCollapse ?? DEFAULT_SETTINGS.dataView.enableCollapse;
|
|
1067
|
+
});
|
|
1068
|
+
// ===== Toolbar controls visibility =====
|
|
1069
|
+
toolbarShowLinkStyle = computed(() => {
|
|
1070
|
+
const b = this.baseSettings();
|
|
1071
|
+
return b.viewport?.toolbarControls?.showLinkStyle ?? true;
|
|
1072
|
+
});
|
|
1073
|
+
toolbarShowLayoutAlign = computed(() => {
|
|
1074
|
+
const b = this.baseSettings();
|
|
1075
|
+
return b.viewport?.toolbarControls?.showLayoutAlign ?? true;
|
|
1076
|
+
});
|
|
1077
|
+
toolbarShowLayoutDirection = computed(() => {
|
|
1078
|
+
const b = this.baseSettings();
|
|
1079
|
+
return b.viewport?.toolbarControls?.showLayoutDirection ?? true;
|
|
1080
|
+
});
|
|
1081
|
+
constructor(adapter, layoutService) {
|
|
1082
|
+
this.adapter = adapter;
|
|
1083
|
+
this.layoutService = layoutService;
|
|
1084
|
+
}
|
|
1085
|
+
// ===== Settings efectivos =====
|
|
1086
|
+
baseSettings = computed(() => {
|
|
1087
|
+
const s = this.settings() ?? {};
|
|
1088
|
+
return {
|
|
1089
|
+
messages: { ...DEFAULT_SETTINGS.messages, ...s.messages },
|
|
1090
|
+
viewport: { ...DEFAULT_SETTINGS.viewport, ...s.viewport },
|
|
1091
|
+
colors: { ...DEFAULT_SETTINGS.colors, ...s.colors },
|
|
1092
|
+
layout: { ...DEFAULT_SETTINGS.layout, ...s.layout },
|
|
1093
|
+
dataView: { ...DEFAULT_SETTINGS.dataView, ...s.dataView },
|
|
1094
|
+
};
|
|
1095
|
+
});
|
|
1096
|
+
effectiveSettings = computed(() => {
|
|
1097
|
+
const b = this.baseSettings();
|
|
1098
|
+
return {
|
|
1099
|
+
...b,
|
|
1100
|
+
layout: {
|
|
1101
|
+
...b.layout,
|
|
1102
|
+
linkStyle: this.opt_linkStyle(),
|
|
1103
|
+
layoutAlign: this.opt_layoutAlign(),
|
|
1104
|
+
layoutDirection: this.opt_layoutDirection(),
|
|
1105
|
+
},
|
|
1106
|
+
};
|
|
1107
|
+
});
|
|
1108
|
+
recomputeFromSettings() {
|
|
1109
|
+
const b = this.baseSettings();
|
|
1110
|
+
this.viewportHeight.set(b.viewport?.height ?? DEFAULT_SETTINGS.viewport.height);
|
|
1111
|
+
this.minViewportHeight.set(b.viewport?.minHeight ?? DEFAULT_SETTINGS.viewport.minHeight);
|
|
1112
|
+
this.showToolbar.set(b.viewport?.showToolbar ?? DEFAULT_SETTINGS.viewport.showToolbar);
|
|
1113
|
+
this.opt_linkStyle.set((b.layout?.linkStyle ?? DEFAULT_SETTINGS.layout.linkStyle));
|
|
1114
|
+
this.opt_layoutAlign.set((b.layout?.layoutAlign ?? DEFAULT_SETTINGS.layout.layoutAlign));
|
|
1115
|
+
this.opt_layoutDirection.set((b.layout?.layoutDirection ?? DEFAULT_SETTINGS.layout.layoutDirection));
|
|
1116
|
+
}
|
|
1117
|
+
// ===== Ciclo de vida =====
|
|
1118
|
+
resizeObs;
|
|
1119
|
+
ngAfterViewInit() {
|
|
1120
|
+
this.recomputeFromSettings();
|
|
1121
|
+
const rootEl = this.rootRef?.nativeElement;
|
|
1122
|
+
if (rootEl && "ResizeObserver" in window) {
|
|
1123
|
+
const r0 = rootEl.getBoundingClientRect();
|
|
1124
|
+
this.lastViewport = { w: r0.width, h: r0.height };
|
|
1125
|
+
this.resizeObs = new ResizeObserver(() => {
|
|
1126
|
+
const rect = rootEl.getBoundingClientRect();
|
|
1127
|
+
this.preserveCenterOnResize(rect.width, rect.height);
|
|
1128
|
+
});
|
|
1129
|
+
this.resizeObs.observe(rootEl);
|
|
1130
|
+
}
|
|
1131
|
+
this.compute();
|
|
1132
|
+
}
|
|
1133
|
+
ngOnChanges(_) {
|
|
1134
|
+
this.recomputeFromSettings();
|
|
1135
|
+
this.compute();
|
|
1136
|
+
}
|
|
1137
|
+
ngOnDestroy() {
|
|
1138
|
+
if (this.resizeObs) {
|
|
1139
|
+
try {
|
|
1140
|
+
this.resizeObs.disconnect();
|
|
1141
|
+
}
|
|
1142
|
+
catch { }
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
// ===== Pipeline principal =====
|
|
1146
|
+
async compute() {
|
|
1147
|
+
if (this.isLoadingView())
|
|
1148
|
+
return;
|
|
1149
|
+
if (this.hasComputedOnce && this.data() === this.lastDataRef && this.settings() === this.lastSettingsRef) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const s = this.effectiveSettings();
|
|
1153
|
+
const normalized = this.adapter.normalize(this.data(), s);
|
|
1154
|
+
this.ensurePinMeta(normalized, s);
|
|
1155
|
+
this.fullGraph.set(this.cloneGraph(normalized));
|
|
1156
|
+
this.buildIndices();
|
|
1157
|
+
if (!this.enableCollapse())
|
|
1158
|
+
this.collapsed.clear();
|
|
1159
|
+
const visible = this.buildVisibleGraphFromCollapsed();
|
|
1160
|
+
let laid = await this.layoutService.layout(visible, s);
|
|
1161
|
+
this.graph.set(this.cloneGraph(laid));
|
|
1162
|
+
if (s.dataView?.autoResizeCards ?? DEFAULT_SETTINGS.dataView.autoResizeCards) {
|
|
1163
|
+
const maxPasses = 6;
|
|
1164
|
+
this.measureIdsToCheck = null; // primera pasada: medir todo
|
|
1165
|
+
for (let pass = 1; pass <= maxPasses; pass++) {
|
|
1166
|
+
await this.nextFrame();
|
|
1167
|
+
const changed = this.measureAndApply(pass);
|
|
1168
|
+
if (!changed)
|
|
1169
|
+
break;
|
|
1170
|
+
laid = await this.layoutService.layout(this.graph(), s);
|
|
1171
|
+
this.graph.set(this.cloneGraph(laid));
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
this.updateVirtualSizeFromGraph(laid);
|
|
1175
|
+
this.fitToViewByBounds(); // asegura minScale y encuadre base
|
|
1176
|
+
this.centerOnFirstNodeOrFit(); // centra como doble clic
|
|
1177
|
+
this.lastDataRef = this.data();
|
|
1178
|
+
this.lastSettingsRef = this.settings();
|
|
1179
|
+
this.hasComputedOnce = true;
|
|
1180
|
+
}
|
|
1181
|
+
async relayoutVisible(anchorId, anchorScreen) {
|
|
1182
|
+
const s = this.effectiveSettings();
|
|
1183
|
+
const visible = this.buildVisibleGraphFromCollapsed();
|
|
1184
|
+
let laid = await this.layoutService.layout(visible, s);
|
|
1185
|
+
this.graph.set(this.cloneGraph(laid));
|
|
1186
|
+
if (s.dataView?.autoResizeCards ?? DEFAULT_SETTINGS.dataView.autoResizeCards) {
|
|
1187
|
+
const maxPasses = 4;
|
|
1188
|
+
this.measureIdsToCheck = null; // primera pasada: medir todo
|
|
1189
|
+
for (let pass = 1; pass <= maxPasses; pass++) {
|
|
1190
|
+
await this.nextFrame();
|
|
1191
|
+
const changed = this.measureAndApply(pass);
|
|
1192
|
+
if (!changed)
|
|
1193
|
+
break;
|
|
1194
|
+
laid = await this.layoutService.layout(this.graph(), s);
|
|
1195
|
+
this.graph.set(this.cloneGraph(laid));
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
this.updateVirtualSizeFromGraph(laid);
|
|
1199
|
+
await this.animateToGraph(laid, 260, anchorId, anchorScreen);
|
|
1200
|
+
}
|
|
1201
|
+
async onCardToggle(n) {
|
|
1202
|
+
if (!this.enableCollapse() || !n?.id)
|
|
1203
|
+
return;
|
|
1204
|
+
const anchorBefore = this.getNodeScreenCenter(n);
|
|
1205
|
+
if (this.collapsed.has(n.id))
|
|
1206
|
+
this.collapsed.delete(n.id);
|
|
1207
|
+
else
|
|
1208
|
+
this.collapsed.add(n.id);
|
|
1209
|
+
await this.relayoutVisible(n.id, anchorBefore);
|
|
1210
|
+
}
|
|
1211
|
+
/** Devuelve si el nodo tiene hijos (para pintar botón de colapso). */
|
|
1212
|
+
hasChildren = (id) => (this.childrenById.get(id)?.length ?? 0) > 0;
|
|
1213
|
+
/** Devuelve si el nodo está colapsado (estado visual en la card). */
|
|
1214
|
+
isNodeCollapsed = (id) => this.collapsed.has(id);
|
|
1215
|
+
// --- Animación de transición entre grafos (requerido por relayoutVisible) ---
|
|
1216
|
+
async animateToGraph(target, durationMs = 260, anchorId, anchorScreen) {
|
|
1217
|
+
const start = this.cloneGraph(this.graph());
|
|
1218
|
+
const startNodeById = new Map(start.nodes.map((n) => [n.id, n]));
|
|
1219
|
+
const startEdgeById = new Map(start.edges.map((e) => [e.id, e]));
|
|
1220
|
+
const lerp = (a, b, t) => a + (b - a) * t;
|
|
1221
|
+
const easeInOut = (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2);
|
|
1222
|
+
const alignPoints = (a = [], b = []) => {
|
|
1223
|
+
const aa = a.length ? [...a] : [];
|
|
1224
|
+
const bb = b.length ? [...b] : [];
|
|
1225
|
+
const len = Math.max(aa.length, bb.length, 2);
|
|
1226
|
+
while (aa.length < len)
|
|
1227
|
+
aa.push(aa[aa.length - 1] ?? { x: 0, y: 0 });
|
|
1228
|
+
while (bb.length < len)
|
|
1229
|
+
bb.push(bb[bb.length - 1] ?? { x: 0, y: 0 });
|
|
1230
|
+
return { aa, bb, len };
|
|
1231
|
+
};
|
|
1232
|
+
const t0 = performance.now();
|
|
1233
|
+
const run = (resolve) => {
|
|
1234
|
+
const now = performance.now();
|
|
1235
|
+
const raw = Math.min(1, (now - t0) / Math.max(1, durationMs));
|
|
1236
|
+
const t = easeInOut(raw);
|
|
1237
|
+
const frame = { nodes: [], edges: [], meta: { ...(target.meta ?? {}) } };
|
|
1238
|
+
for (const endNode of target.nodes) {
|
|
1239
|
+
const sNode = startNodeById.get(endNode.id) ?? endNode;
|
|
1240
|
+
const xn = lerp(sNode.x ?? 0, endNode.x ?? 0, t);
|
|
1241
|
+
const yn = lerp(sNode.y ?? 0, endNode.y ?? 0, t);
|
|
1242
|
+
const wn = lerp(sNode.width ?? 0, endNode.width ?? 0, t);
|
|
1243
|
+
const hn = lerp(sNode.height ?? 0, endNode.height ?? 0, t);
|
|
1244
|
+
frame.nodes.push({
|
|
1245
|
+
...endNode,
|
|
1246
|
+
x: Math.round(xn),
|
|
1247
|
+
y: Math.round(yn),
|
|
1248
|
+
width: Math.round(wn),
|
|
1249
|
+
height: Math.round(hn),
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
for (const endEdge of target.edges) {
|
|
1253
|
+
const sEdge = startEdgeById.get(endEdge.id) ?? endEdge;
|
|
1254
|
+
const { aa, bb, len } = alignPoints(sEdge.points, endEdge.points);
|
|
1255
|
+
const pts = new Array(len)
|
|
1256
|
+
.fill(0)
|
|
1257
|
+
.map((_, i) => ({ x: lerp(aa[i].x, bb[i].x, t), y: lerp(aa[i].y, bb[i].y, t) }));
|
|
1258
|
+
frame.edges.push({ ...endEdge, points: pts });
|
|
1259
|
+
}
|
|
1260
|
+
this.graph.set(frame);
|
|
1261
|
+
if (anchorId && anchorScreen)
|
|
1262
|
+
this.applyAnchorAfterLayout(anchorId, anchorScreen);
|
|
1263
|
+
if (raw < 1) {
|
|
1264
|
+
requestAnimationFrame(() => run(resolve));
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
this.graph.set(this.cloneGraph(target));
|
|
1268
|
+
if (anchorId && anchorScreen)
|
|
1269
|
+
this.applyAnchorAfterLayout(anchorId, anchorScreen);
|
|
1270
|
+
resolve();
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
await new Promise((resolve) => requestAnimationFrame(() => run(resolve)));
|
|
1274
|
+
}
|
|
1275
|
+
// ===== Interacción (solo en .stage) =====
|
|
1276
|
+
onWheel(e) {
|
|
1277
|
+
e.preventDefault();
|
|
1278
|
+
this.pendingWheel = { deltaY: e.deltaY, clientX: e.clientX, clientY: e.clientY };
|
|
1279
|
+
if (this.wheelRaf !== null)
|
|
1280
|
+
return;
|
|
1281
|
+
this.wheelRaf = requestAnimationFrame(() => {
|
|
1282
|
+
const ev = this.pendingWheel;
|
|
1283
|
+
this.pendingWheel = null;
|
|
1284
|
+
this.wheelRaf = null;
|
|
1285
|
+
if (!ev)
|
|
1286
|
+
return;
|
|
1287
|
+
const rect = this.rootRef.nativeElement.getBoundingClientRect();
|
|
1288
|
+
const mouseX = ev.clientX - rect.left;
|
|
1289
|
+
const mouseY = ev.clientY - rect.top;
|
|
1290
|
+
this.lastPointerScreen = { x: mouseX, y: mouseY };
|
|
1291
|
+
const oldScale = this.scale();
|
|
1292
|
+
const factor = 1 + (-ev.deltaY > 0 ? 0.08 : -0.08);
|
|
1293
|
+
const newScale = Math.max(this.minScale(), Math.min(this.maxScale(), oldScale * factor));
|
|
1294
|
+
const worldX = (mouseX - this.tx()) / oldScale;
|
|
1295
|
+
const worldY = (mouseY - this.ty()) / oldScale;
|
|
1296
|
+
this.tx.set(mouseX - worldX * newScale);
|
|
1297
|
+
this.ty.set(mouseY - worldY * newScale);
|
|
1298
|
+
this.scale.set(newScale);
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
onPointerDown(e) {
|
|
1302
|
+
e.preventDefault();
|
|
1303
|
+
this.dragging = true;
|
|
1304
|
+
const el = e.target;
|
|
1305
|
+
if (el && el.closest && el.closest(".collapse-btn")) {
|
|
1306
|
+
this.dragging = false;
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
try {
|
|
1310
|
+
e.target.setPointerCapture?.(e.pointerId);
|
|
1311
|
+
}
|
|
1312
|
+
catch { }
|
|
1313
|
+
const rect = this.rootRef.nativeElement.getBoundingClientRect();
|
|
1314
|
+
this.lastX = e.clientX;
|
|
1315
|
+
this.lastY = e.clientY;
|
|
1316
|
+
this.lastPointerScreen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
1317
|
+
}
|
|
1318
|
+
onPointerMove(e) {
|
|
1319
|
+
const rect = this.rootRef.nativeElement.getBoundingClientRect();
|
|
1320
|
+
this.lastPointerScreen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
1321
|
+
if (!this.dragging)
|
|
1322
|
+
return;
|
|
1323
|
+
const dx = e.clientX - this.lastX;
|
|
1324
|
+
const dy = e.clientY - this.lastY;
|
|
1325
|
+
this.tx.set(this.tx() + dx);
|
|
1326
|
+
this.ty.set(this.ty() + dy);
|
|
1327
|
+
this.lastX = e.clientX;
|
|
1328
|
+
this.lastY = e.clientY;
|
|
1329
|
+
}
|
|
1330
|
+
onPointerUp() {
|
|
1331
|
+
this.dragging = false;
|
|
1332
|
+
}
|
|
1333
|
+
onPointerLeave() {
|
|
1334
|
+
this.dragging = false;
|
|
1335
|
+
this.lastPointerScreen = null;
|
|
1336
|
+
}
|
|
1337
|
+
onDblClick() {
|
|
1338
|
+
this.centerOnFirstNodeOrFit();
|
|
1339
|
+
}
|
|
1340
|
+
// ===== Toolbar actions =====
|
|
1341
|
+
zoomIn() {
|
|
1342
|
+
this.applyZoom(1.15);
|
|
1343
|
+
}
|
|
1344
|
+
zoomOut() {
|
|
1345
|
+
this.applyZoom(1 / 1.15);
|
|
1346
|
+
}
|
|
1347
|
+
resetView() {
|
|
1348
|
+
this.centerOnFirstNodeOrFit();
|
|
1349
|
+
}
|
|
1350
|
+
setLinkStyle(v) {
|
|
1351
|
+
const ok = v === "orthogonal" || v === "curve" || v === "line";
|
|
1352
|
+
this.opt_linkStyle.set(ok ? v : "orthogonal");
|
|
1353
|
+
this.relayoutVisible();
|
|
1354
|
+
}
|
|
1355
|
+
setLayoutAlign(v) {
|
|
1356
|
+
const ok = v === "firstChild" || v === "center";
|
|
1357
|
+
this.opt_layoutAlign.set(ok ? v : "center");
|
|
1358
|
+
this.relayoutVisible();
|
|
1359
|
+
}
|
|
1360
|
+
setLayoutDirection(v) {
|
|
1361
|
+
const ok = v === "RIGHT" || v === "DOWN";
|
|
1362
|
+
this.opt_layoutDirection.set(ok ? v : "RIGHT");
|
|
1363
|
+
this.relayoutVisible();
|
|
1364
|
+
}
|
|
1365
|
+
/* ========================================================================
|
|
1366
|
+
* Helpers (posibles extraíbles)
|
|
1367
|
+
* ===================================================================== */
|
|
1368
|
+
// ---- Gráfico / DOM ----
|
|
1369
|
+
cloneGraph(g) {
|
|
1370
|
+
return {
|
|
1371
|
+
nodes: g.nodes.map((n) => ({ ...n })),
|
|
1372
|
+
edges: g.edges.map((e) => ({ ...e })),
|
|
1373
|
+
meta: { ...(g.meta ?? {}) },
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
nextFrame() {
|
|
1377
|
+
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
1378
|
+
}
|
|
1379
|
+
ensurePinMeta(g, s) {
|
|
1380
|
+
if (!g.meta)
|
|
1381
|
+
g.meta = {};
|
|
1382
|
+
const dir = s.layout?.layoutDirection ?? DEFAULT_SETTINGS.layout.layoutDirection;
|
|
1383
|
+
const key = dir === "RIGHT" ? "pinY" : "pinX";
|
|
1384
|
+
if (!g.meta[key])
|
|
1385
|
+
g.meta[key] = {};
|
|
1386
|
+
}
|
|
1387
|
+
updateVirtualSizeFromGraph(g) {
|
|
1388
|
+
const pad = 200;
|
|
1389
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1390
|
+
for (const n of g.nodes) {
|
|
1391
|
+
const x = n.x ?? 0;
|
|
1392
|
+
const y = n.y ?? 0;
|
|
1393
|
+
const w = n.width ?? 0;
|
|
1394
|
+
const h = n.height ?? 0;
|
|
1395
|
+
minX = Math.min(minX, x);
|
|
1396
|
+
minY = Math.min(minY, y);
|
|
1397
|
+
maxX = Math.max(maxX, x + w);
|
|
1398
|
+
maxY = Math.max(maxY, y + h);
|
|
1399
|
+
}
|
|
1400
|
+
if (!isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY)) {
|
|
1401
|
+
this.virtualWidth = 12000;
|
|
1402
|
+
this.virtualHeight = 6000;
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
const neededW = Math.max(1, Math.ceil(maxX - Math.min(0, minX)) + pad);
|
|
1406
|
+
const neededH = Math.max(1, Math.ceil(maxY - Math.min(0, minY)) + pad);
|
|
1407
|
+
this.virtualWidth = Math.max(neededW, 2000);
|
|
1408
|
+
this.virtualHeight = Math.max(neededH, 1200);
|
|
1409
|
+
}
|
|
1410
|
+
buildIndices() {
|
|
1411
|
+
this.childrenById.clear();
|
|
1412
|
+
this.parentsById.clear();
|
|
1413
|
+
const g = this.fullGraph();
|
|
1414
|
+
for (const n of g.nodes) {
|
|
1415
|
+
this.childrenById.set(n.id, []);
|
|
1416
|
+
this.parentsById.set(n.id, []);
|
|
1417
|
+
}
|
|
1418
|
+
for (const e of g.edges) {
|
|
1419
|
+
const ch = this.childrenById.get(e.source);
|
|
1420
|
+
if (ch)
|
|
1421
|
+
ch.push(e.target);
|
|
1422
|
+
const pr = this.parentsById.get(e.target);
|
|
1423
|
+
if (pr)
|
|
1424
|
+
pr.push(e.source);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
isVisibleNodeByCollapsedAncestors(id) {
|
|
1428
|
+
if (!this.enableCollapse())
|
|
1429
|
+
return true;
|
|
1430
|
+
const stack = [...(this.parentsById.get(id) ?? [])];
|
|
1431
|
+
const seen = new Set();
|
|
1432
|
+
while (stack.length) {
|
|
1433
|
+
const p = stack.pop();
|
|
1434
|
+
if (seen.has(p))
|
|
1435
|
+
continue;
|
|
1436
|
+
seen.add(p);
|
|
1437
|
+
if (this.collapsed.has(p))
|
|
1438
|
+
return false;
|
|
1439
|
+
const pp = this.parentsById.get(p);
|
|
1440
|
+
if (pp && pp.length)
|
|
1441
|
+
stack.push(...pp);
|
|
1442
|
+
}
|
|
1443
|
+
return true;
|
|
1444
|
+
}
|
|
1445
|
+
buildVisibleGraphFromCollapsed() {
|
|
1446
|
+
const full = this.fullGraph();
|
|
1447
|
+
if (!this.enableCollapse())
|
|
1448
|
+
return this.cloneGraph(full);
|
|
1449
|
+
const visibleNodeSet = new Set();
|
|
1450
|
+
for (const n of full.nodes) {
|
|
1451
|
+
if (this.isVisibleNodeByCollapsedAncestors(n.id))
|
|
1452
|
+
visibleNodeSet.add(n.id);
|
|
1453
|
+
}
|
|
1454
|
+
const nodes = full.nodes.filter((n) => visibleNodeSet.has(n.id));
|
|
1455
|
+
const edges = full.edges.filter((e) => visibleNodeSet.has(e.source) && visibleNodeSet.has(e.target));
|
|
1456
|
+
return { nodes, edges, meta: full.meta };
|
|
1457
|
+
}
|
|
1458
|
+
// ---- Medición ----
|
|
1459
|
+
measureAndApply(pass) {
|
|
1460
|
+
const s = this.effectiveSettings();
|
|
1461
|
+
const extraW = s.dataView?.paddingWidthPx ?? 0;
|
|
1462
|
+
const extraH = s.dataView?.paddingHeightPx ?? 0;
|
|
1463
|
+
const maxW = s.dataView?.maxCardWidth ?? Infinity;
|
|
1464
|
+
const maxH = s.dataView?.maxCardHeight ?? Infinity;
|
|
1465
|
+
const root = this.rootRef.nativeElement;
|
|
1466
|
+
const cards = Array.from(root.querySelectorAll(".schema-card"));
|
|
1467
|
+
const visMap = new Map(this.graph().nodes.map((n) => [n.id, n]));
|
|
1468
|
+
const fullMap = new Map(this.fullGraph().nodes.map((n) => [n.id, n]));
|
|
1469
|
+
let changed = false;
|
|
1470
|
+
const idsToMeasure = pass === 1 ? null : this.measureIdsToCheck;
|
|
1471
|
+
const nextChanged = new Set();
|
|
1472
|
+
const measures = [];
|
|
1473
|
+
for (const el of cards) {
|
|
1474
|
+
const id = el.getAttribute("data-node-id") ?? undefined;
|
|
1475
|
+
if (!id)
|
|
1476
|
+
continue;
|
|
1477
|
+
if (idsToMeasure && !idsToMeasure.has(id))
|
|
1478
|
+
continue;
|
|
1479
|
+
const node = visMap.get(id);
|
|
1480
|
+
if (!node)
|
|
1481
|
+
continue;
|
|
1482
|
+
const prevW = el.style.width;
|
|
1483
|
+
const prevH = el.style.height;
|
|
1484
|
+
el.style.width = "auto";
|
|
1485
|
+
el.style.height = "auto";
|
|
1486
|
+
const wIntrinsic = Math.ceil(el.scrollWidth);
|
|
1487
|
+
const hIntrinsic = Math.ceil(el.scrollHeight);
|
|
1488
|
+
el.style.width = prevW;
|
|
1489
|
+
el.style.height = prevH;
|
|
1490
|
+
const targetW = Math.min(wIntrinsic + extraW, maxW);
|
|
1491
|
+
const targetH = Math.min(hIntrinsic + extraH, maxH);
|
|
1492
|
+
measures.push({ id, w: targetW, h: targetH });
|
|
1493
|
+
}
|
|
1494
|
+
for (const m of measures) {
|
|
1495
|
+
const node = visMap.get(m.id);
|
|
1496
|
+
if (!node)
|
|
1497
|
+
continue;
|
|
1498
|
+
if ((node.width ?? 0) !== m.w || (node.height ?? 0) !== m.h) {
|
|
1499
|
+
node.width = m.w;
|
|
1500
|
+
node.height = m.h;
|
|
1501
|
+
changed = true;
|
|
1502
|
+
nextChanged.add(m.id);
|
|
1503
|
+
const full = fullMap.get(m.id);
|
|
1504
|
+
if (full) {
|
|
1505
|
+
full.width = m.w;
|
|
1506
|
+
full.height = m.h;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
this.measureIdsToCheck = nextChanged.size ? nextChanged : null;
|
|
1511
|
+
return changed;
|
|
1512
|
+
}
|
|
1513
|
+
// ---- Viewport / encuadre ----
|
|
1514
|
+
getViewportSize() {
|
|
1515
|
+
const el = this.rootRef.nativeElement;
|
|
1516
|
+
const rect = el.getBoundingClientRect();
|
|
1517
|
+
return { w: rect.width, h: rect.height };
|
|
1518
|
+
}
|
|
1519
|
+
getGraphBounds() {
|
|
1520
|
+
const ns = this.nodes();
|
|
1521
|
+
if (!ns.length)
|
|
1522
|
+
return { minX: 0, minY: 0, maxX: 1, maxY: 1 };
|
|
1523
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1524
|
+
for (const n of ns) {
|
|
1525
|
+
const nx = n.x ?? 0;
|
|
1526
|
+
const ny = n.y ?? 0;
|
|
1527
|
+
const nw = n.width ?? 0;
|
|
1528
|
+
const nh = n.height ?? 0;
|
|
1529
|
+
minX = Math.min(minX, nx);
|
|
1530
|
+
minY = Math.min(minY, ny);
|
|
1531
|
+
maxX = Math.max(maxX, nx + nw);
|
|
1532
|
+
maxY = Math.max(maxY, ny + nh);
|
|
1533
|
+
}
|
|
1534
|
+
return { minX, minY, maxX, maxY };
|
|
1535
|
+
}
|
|
1536
|
+
computeScaleToFit(pad = 24) {
|
|
1537
|
+
const { w, h } = this.getViewportSize();
|
|
1538
|
+
const { minX, minY, maxX, maxY } = this.getGraphBounds();
|
|
1539
|
+
const gw = Math.max(1, maxX - minX);
|
|
1540
|
+
const gh = Math.max(1, maxY - minY);
|
|
1541
|
+
const sx = (w - pad) / gw;
|
|
1542
|
+
const sy = (h - pad) / gh;
|
|
1543
|
+
const fit = Math.max(0.05, Math.min(sx, sy));
|
|
1544
|
+
return { fit, minX, minY, maxX, maxY, w, h };
|
|
1545
|
+
}
|
|
1546
|
+
fitToViewByBounds() {
|
|
1547
|
+
const { fit, minX, minY, maxX, maxY, w, h } = this.computeScaleToFit(24);
|
|
1548
|
+
this.minScale.set(Math.min(fit, 1));
|
|
1549
|
+
this.scale.set(Math.max(this.scale(), this.minScale()));
|
|
1550
|
+
const cx = (minX + maxX) / 2;
|
|
1551
|
+
const cy = (minY + maxY) / 2;
|
|
1552
|
+
const s = this.scale();
|
|
1553
|
+
this.tx.set(w / 2 - cx * s);
|
|
1554
|
+
this.ty.set(h / 2 - cy * s);
|
|
1555
|
+
}
|
|
1556
|
+
/** Zoom anclado al último punto de interacción sobre la stage; si no hay, usa el centro del viewport. */
|
|
1557
|
+
applyZoom(factor) {
|
|
1558
|
+
const rect = this.rootRef.nativeElement.getBoundingClientRect();
|
|
1559
|
+
const anchor = this.lastPointerScreen ?? { x: rect.width / 2, y: rect.height / 2 };
|
|
1560
|
+
const oldScale = this.scale();
|
|
1561
|
+
const newScale = Math.max(this.minScale(), Math.min(this.maxScale(), oldScale * factor));
|
|
1562
|
+
const worldX = (anchor.x - this.tx()) / oldScale;
|
|
1563
|
+
const worldY = (anchor.y - this.ty()) / oldScale;
|
|
1564
|
+
this.tx.set(anchor.x - worldX * newScale);
|
|
1565
|
+
this.ty.set(anchor.y - worldY * newScale);
|
|
1566
|
+
this.scale.set(newScale);
|
|
1567
|
+
}
|
|
1568
|
+
preserveCenterOnResize(newW, newH) {
|
|
1569
|
+
if (!this.lastViewport) {
|
|
1570
|
+
this.lastViewport = { w: newW, h: newH };
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
const oldW = this.lastViewport.w;
|
|
1574
|
+
const oldH = this.lastViewport.h;
|
|
1575
|
+
const s = this.scale();
|
|
1576
|
+
const worldCx = (oldW / 2 - this.tx()) / s;
|
|
1577
|
+
const worldCy = (oldH / 2 - this.ty()) / s;
|
|
1578
|
+
this.tx.set(newW / 2 - worldCx * s);
|
|
1579
|
+
this.ty.set(newH / 2 - worldCy * s);
|
|
1580
|
+
this.lastViewport = { w: newW, h: newH };
|
|
1581
|
+
}
|
|
1582
|
+
// ---- Foco / anclaje ----
|
|
1583
|
+
getNodeById(id) {
|
|
1584
|
+
if (!id)
|
|
1585
|
+
return undefined;
|
|
1586
|
+
return this.graph().nodes.find((n) => n.id === id);
|
|
1587
|
+
}
|
|
1588
|
+
getNodeScreenCenter(n) {
|
|
1589
|
+
const s = this.scale();
|
|
1590
|
+
const cx = (n.x ?? 0) + (n.width ?? 0) / 2;
|
|
1591
|
+
const cy = (n.y ?? 0) + (n.height ?? 0) / 2;
|
|
1592
|
+
return { x: cx * s + this.tx(), y: cy * s + this.ty() };
|
|
1593
|
+
}
|
|
1594
|
+
applyAnchorAfterLayout(nodeId, targetScreen) {
|
|
1595
|
+
const n = this.getNodeById(nodeId);
|
|
1596
|
+
if (!n)
|
|
1597
|
+
return;
|
|
1598
|
+
this.centerOnNodeAtScreen(n, targetScreen);
|
|
1599
|
+
}
|
|
1600
|
+
// ---- Centrados reutilizables ----
|
|
1601
|
+
getFirstVisibleNode() {
|
|
1602
|
+
const list = this.nodes();
|
|
1603
|
+
return list.length ? list[0] : null;
|
|
1604
|
+
}
|
|
1605
|
+
centerOnNode(n) {
|
|
1606
|
+
const rect = this.rootRef.nativeElement.getBoundingClientRect();
|
|
1607
|
+
const viewportCx = rect.width / 2;
|
|
1608
|
+
const viewportCy = rect.height / 2;
|
|
1609
|
+
const s = this.scale();
|
|
1610
|
+
const nodeCx = (n.x ?? 0) + (n.width ?? 0) / 2;
|
|
1611
|
+
const nodeCy = (n.y ?? 0) + (n.height ?? 0) / 2;
|
|
1612
|
+
this.tx.set(viewportCx - nodeCx * s);
|
|
1613
|
+
this.ty.set(viewportCy - nodeCy * s);
|
|
1614
|
+
}
|
|
1615
|
+
/** Centra un nodo de forma que su centro quede en una posición de pantalla dada (x,y). */
|
|
1616
|
+
centerOnNodeAtScreen(n, screen) {
|
|
1617
|
+
const s = this.scale();
|
|
1618
|
+
const nodeCx = (n.x ?? 0) + (n.width ?? 0) / 2;
|
|
1619
|
+
const nodeCy = (n.y ?? 0) + (n.height ?? 0) / 2;
|
|
1620
|
+
this.tx.set(screen.x - nodeCx * s);
|
|
1621
|
+
this.ty.set(screen.y - nodeCy * s);
|
|
1622
|
+
}
|
|
1623
|
+
centerOnFirstNodeOrFit() {
|
|
1624
|
+
const first = this.getFirstVisibleNode();
|
|
1625
|
+
if (first)
|
|
1626
|
+
this.centerOnNode(first);
|
|
1627
|
+
else
|
|
1628
|
+
this.fitToViewByBounds();
|
|
1629
|
+
}
|
|
1630
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaComponent, deps: [{ token: JsonAdapterService }, { token: SchemaLayoutService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1631
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.18", type: SchemaComponent, isStandalone: true, selector: "schema", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, settings: { classPropertyName: "settings", publicName: "settings", isSignal: true, isRequired: false, transformFunction: null }, cardTemplate: { classPropertyName: "cardTemplate", publicName: "cardTemplate", isSignal: true, isRequired: false, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: false, transformFunction: null }, isError: { classPropertyName: "isError", publicName: "isError", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, loadingMessage: { classPropertyName: "loadingMessage", publicName: "loadingMessage", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeClick: "nodeClick", linkClick: "linkClick" }, viewQueries: [{ propertyName: "rootRef", first: true, predicate: ["root"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<!-- URL: projects/schema-ng19/src/lib/components/schema.component.html -->\n<div class=\"schema-root\" #root [style.height.px]=\"viewportHeight()\" [style.minHeight.px]=\"minViewportHeight()\">\n <div class=\"schema-toolbar\" *ngIf=\"showToolbar() && !isLoadingView() && !isErrorView()\">\n <div class=\"toolbar-actions\">\n <button type=\"button\" (click)=\"zoomOut()\" title=\"Zoom out\">\u2212</button>\n <button type=\"button\" (click)=\"zoomIn()\" title=\"Zoom in\">+</button>\n <button type=\"button\" (click)=\"resetView()\" title=\"Centrar\">\u293E</button>\n </div>\n\n <div class=\"toolbar-selectors\">\n <label *ngIf=\"toolbarShowLinkStyle()\">\n Enlaces:\n <select #ls [value]=\"opt_linkStyle()\" (change)=\"setLinkStyle(ls.value)\">\n <option value=\"curve\">Curvo</option>\n <option value=\"orthogonal\">Ortogonal</option>\n <option value=\"line\">Lineal</option>\n </select>\n </label>\n\n <label *ngIf=\"toolbarShowLayoutAlign()\">\n Alineaci\u00F3n:\n <select #la [value]=\"opt_layoutAlign()\" (change)=\"setLayoutAlign(la.value)\">\n <option value=\"firstChild\">Superior</option>\n <option value=\"center\">Centrado</option>\n </select>\n </label>\n\n <label *ngIf=\"toolbarShowLayoutDirection()\">\n Direcci\u00F3n:\n <select #ld [value]=\"opt_layoutDirection()\" (change)=\"setLayoutDirection(ld.value)\">\n <option value=\"RIGHT\">Derecha</option>\n <option value=\"DOWN\">Abajo</option>\n </select>\n </label>\n </div>\n </div>\n\n <!-- Overlays -->\n <div class=\"overlay loading\" *ngIf=\"isLoadingView()\">\n <div class=\"loading-banner\">\n <div class=\"shimmer\"></div>\n <span class=\"msg\">{{ loadingMessageView() }}</span>\n </div>\n </div>\n\n <div class=\"overlay empty\" *ngIf=\"!isLoadingView() && data() == null\">\n <div class=\"empty-banner\">{{ emptyMessageView() }}</div>\n </div>\n\n <div class=\"overlay error\" *ngIf=\"!isLoadingView() && isErrorView()\">\n <div class=\"error-banner\">{{ errorMessageView() }}</div>\n </div>\n\n <!-- Stage -->\n <div\n class=\"stage\"\n (wheel)=\"onWheel($event)\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp()\"\n (pointerleave)=\"onPointerLeave()\"\n (dblclick)=\"onDblClick()\"\n [style.transform]=\"transform()\"\n [style.width.px]=\"virtualWidth\"\n [style.height.px]=\"virtualHeight\"\n *ngIf=\"!isLoadingView() && !isErrorView()\"\n >\n <schema-links\n [edges]=\"edges()\"\n [settings]=\"effectiveSettings()\"\n (linkClick)=\"linkClick.emit($event)\"\n [width]=\"virtualWidth\"\n [height]=\"virtualHeight\"\n ></schema-links>\n\n @for (n of nodes(); track n.id) {\n <schema-card\n [node]=\"n\"\n [settings]=\"effectiveSettings()\"\n [cardTemplate]=\"cardTemplate()\"\n [hasChildren]=\"hasChildren(n.id)\"\n [showCollapseControls]=\"enableCollapse()\"\n [isCollapsed]=\"isNodeCollapsed(n.id)\"\n (toggleRequest)=\"onCardToggle($event)\"\n (nodeClick)=\"nodeClick.emit($event)\"\n ></schema-card>\n }\n </div>\n</div>\n", styles: [".toolbar-actions button{border:0;background:transparent;padding:0;margin:0;font:inherit;color:inherit}.toolbar-selectors select{border:0;background:transparent;appearance:none;font:inherit;color:inherit}.schema-toolbar{border-radius:10px;border:1px solid rgba(0,0,0,.06);box-shadow:0 2px 8px #00000014}.loading-banner,.empty-banner,.error-banner{pointer-events:auto;border-radius:10px;padding:16px 20px;box-shadow:0 6px 20px #00000014;font-weight:600}.schema-root{position:relative;width:100%;overflow:hidden;background:#f7f9fb;border-radius:8px}.schema-toolbar{position:absolute;inset:12px 12px auto;z-index:20;display:grid;grid-template-columns:1fr auto;align-items:center;gap:8px 12px;background:#ffffffe6;padding:8px 12px;box-shadow:0 2px 8px #00000014}.toolbar-actions{display:inline-flex;gap:8px;align-items:center}.toolbar-actions button{min-width:32px;height:32px;border-radius:8px;border:1px solid rgba(0,0,0,.12);background:#f7f9fb;font-weight:600;font-size:13px;line-height:1.2;cursor:pointer}.toolbar-selectors{display:inline-flex;gap:10px;align-items:center;justify-content:flex-end}.toolbar-selectors select{height:28px;border-radius:6px;border:1px solid rgba(0,0,0,.12);background:#f9fafb;padding:0 6px;cursor:pointer;font-size:12px;line-height:1.2}@media (max-width: 768px){.schema-toolbar{grid-template-columns:1fr;grid-auto-rows:auto}.toolbar-actions{order:1;justify-content:flex-start}.toolbar-selectors{order:2;justify-content:stretch;flex-wrap:wrap;gap:8px 10px}}@media (max-width: 600px){.toolbar-selectors{flex-direction:column;align-items:stretch;gap:8px}.toolbar-selectors label{display:flex;align-items:center;justify-content:space-between;gap:8px}.toolbar-selectors select{width:100%}}.stage{position:absolute;left:0;top:0;width:12000px;height:6000px;transform-origin:0 0;touch-action:none;-ms-touch-action:none;overscroll-behavior:contain;-webkit-user-select:none;user-select:none}.overlay{position:absolute;inset:0;display:grid;place-items:center;z-index:10;pointer-events:none}.loading-banner,.empty-banner,.error-banner{font-size:13px;line-height:1.2}.loading .loading-banner{width:min(720px,90%);background:#eee;position:relative;overflow:hidden;color:#374151}.loading .msg{position:relative;z-index:1}.loading .shimmer{position:absolute;inset:0;background:linear-gradient(to right,#e0e0e0 8%,#f0f0f0 18%,#e0e0e0 33%);background-size:1000px 100%;animation:shimmer 3s infinite linear;opacity:.9}@keyframes shimmer{0%{background-position:-1000px 0}to{background-position:1000px 0}}.empty .empty-banner{background:#fff;border:1px dashed #cbd5e1;color:#475569}.error .error-banner{background:#fff1f2;border:1px solid #fecdd3;color:#b91c1c}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: SchemaCardComponent, selector: "schema-card", inputs: ["node", "cardTemplate", "settings", "hasChildren", "showCollapseControls", "isCollapsed"], outputs: ["nodeClick", "toggleRequest"] }, { kind: "component", type: SchemaLinksComponent, selector: "schema-links", inputs: ["edges", "settings", "width", "height"], outputs: ["linkClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1632
|
+
}
|
|
1633
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: SchemaComponent, decorators: [{
|
|
1634
|
+
type: Component,
|
|
1635
|
+
args: [{ selector: "schema", standalone: true, imports: [CommonModule, NgFor, NgIf, SchemaCardComponent, SchemaLinksComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- URL: projects/schema-ng19/src/lib/components/schema.component.html -->\n<div class=\"schema-root\" #root [style.height.px]=\"viewportHeight()\" [style.minHeight.px]=\"minViewportHeight()\">\n <div class=\"schema-toolbar\" *ngIf=\"showToolbar() && !isLoadingView() && !isErrorView()\">\n <div class=\"toolbar-actions\">\n <button type=\"button\" (click)=\"zoomOut()\" title=\"Zoom out\">\u2212</button>\n <button type=\"button\" (click)=\"zoomIn()\" title=\"Zoom in\">+</button>\n <button type=\"button\" (click)=\"resetView()\" title=\"Centrar\">\u293E</button>\n </div>\n\n <div class=\"toolbar-selectors\">\n <label *ngIf=\"toolbarShowLinkStyle()\">\n Enlaces:\n <select #ls [value]=\"opt_linkStyle()\" (change)=\"setLinkStyle(ls.value)\">\n <option value=\"curve\">Curvo</option>\n <option value=\"orthogonal\">Ortogonal</option>\n <option value=\"line\">Lineal</option>\n </select>\n </label>\n\n <label *ngIf=\"toolbarShowLayoutAlign()\">\n Alineaci\u00F3n:\n <select #la [value]=\"opt_layoutAlign()\" (change)=\"setLayoutAlign(la.value)\">\n <option value=\"firstChild\">Superior</option>\n <option value=\"center\">Centrado</option>\n </select>\n </label>\n\n <label *ngIf=\"toolbarShowLayoutDirection()\">\n Direcci\u00F3n:\n <select #ld [value]=\"opt_layoutDirection()\" (change)=\"setLayoutDirection(ld.value)\">\n <option value=\"RIGHT\">Derecha</option>\n <option value=\"DOWN\">Abajo</option>\n </select>\n </label>\n </div>\n </div>\n\n <!-- Overlays -->\n <div class=\"overlay loading\" *ngIf=\"isLoadingView()\">\n <div class=\"loading-banner\">\n <div class=\"shimmer\"></div>\n <span class=\"msg\">{{ loadingMessageView() }}</span>\n </div>\n </div>\n\n <div class=\"overlay empty\" *ngIf=\"!isLoadingView() && data() == null\">\n <div class=\"empty-banner\">{{ emptyMessageView() }}</div>\n </div>\n\n <div class=\"overlay error\" *ngIf=\"!isLoadingView() && isErrorView()\">\n <div class=\"error-banner\">{{ errorMessageView() }}</div>\n </div>\n\n <!-- Stage -->\n <div\n class=\"stage\"\n (wheel)=\"onWheel($event)\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp()\"\n (pointerleave)=\"onPointerLeave()\"\n (dblclick)=\"onDblClick()\"\n [style.transform]=\"transform()\"\n [style.width.px]=\"virtualWidth\"\n [style.height.px]=\"virtualHeight\"\n *ngIf=\"!isLoadingView() && !isErrorView()\"\n >\n <schema-links\n [edges]=\"edges()\"\n [settings]=\"effectiveSettings()\"\n (linkClick)=\"linkClick.emit($event)\"\n [width]=\"virtualWidth\"\n [height]=\"virtualHeight\"\n ></schema-links>\n\n @for (n of nodes(); track n.id) {\n <schema-card\n [node]=\"n\"\n [settings]=\"effectiveSettings()\"\n [cardTemplate]=\"cardTemplate()\"\n [hasChildren]=\"hasChildren(n.id)\"\n [showCollapseControls]=\"enableCollapse()\"\n [isCollapsed]=\"isNodeCollapsed(n.id)\"\n (toggleRequest)=\"onCardToggle($event)\"\n (nodeClick)=\"nodeClick.emit($event)\"\n ></schema-card>\n }\n </div>\n</div>\n", styles: [".toolbar-actions button{border:0;background:transparent;padding:0;margin:0;font:inherit;color:inherit}.toolbar-selectors select{border:0;background:transparent;appearance:none;font:inherit;color:inherit}.schema-toolbar{border-radius:10px;border:1px solid rgba(0,0,0,.06);box-shadow:0 2px 8px #00000014}.loading-banner,.empty-banner,.error-banner{pointer-events:auto;border-radius:10px;padding:16px 20px;box-shadow:0 6px 20px #00000014;font-weight:600}.schema-root{position:relative;width:100%;overflow:hidden;background:#f7f9fb;border-radius:8px}.schema-toolbar{position:absolute;inset:12px 12px auto;z-index:20;display:grid;grid-template-columns:1fr auto;align-items:center;gap:8px 12px;background:#ffffffe6;padding:8px 12px;box-shadow:0 2px 8px #00000014}.toolbar-actions{display:inline-flex;gap:8px;align-items:center}.toolbar-actions button{min-width:32px;height:32px;border-radius:8px;border:1px solid rgba(0,0,0,.12);background:#f7f9fb;font-weight:600;font-size:13px;line-height:1.2;cursor:pointer}.toolbar-selectors{display:inline-flex;gap:10px;align-items:center;justify-content:flex-end}.toolbar-selectors select{height:28px;border-radius:6px;border:1px solid rgba(0,0,0,.12);background:#f9fafb;padding:0 6px;cursor:pointer;font-size:12px;line-height:1.2}@media (max-width: 768px){.schema-toolbar{grid-template-columns:1fr;grid-auto-rows:auto}.toolbar-actions{order:1;justify-content:flex-start}.toolbar-selectors{order:2;justify-content:stretch;flex-wrap:wrap;gap:8px 10px}}@media (max-width: 600px){.toolbar-selectors{flex-direction:column;align-items:stretch;gap:8px}.toolbar-selectors label{display:flex;align-items:center;justify-content:space-between;gap:8px}.toolbar-selectors select{width:100%}}.stage{position:absolute;left:0;top:0;width:12000px;height:6000px;transform-origin:0 0;touch-action:none;-ms-touch-action:none;overscroll-behavior:contain;-webkit-user-select:none;user-select:none}.overlay{position:absolute;inset:0;display:grid;place-items:center;z-index:10;pointer-events:none}.loading-banner,.empty-banner,.error-banner{font-size:13px;line-height:1.2}.loading .loading-banner{width:min(720px,90%);background:#eee;position:relative;overflow:hidden;color:#374151}.loading .msg{position:relative;z-index:1}.loading .shimmer{position:absolute;inset:0;background:linear-gradient(to right,#e0e0e0 8%,#f0f0f0 18%,#e0e0e0 33%);background-size:1000px 100%;animation:shimmer 3s infinite linear;opacity:.9}@keyframes shimmer{0%{background-position:-1000px 0}to{background-position:1000px 0}}.empty .empty-banner{background:#fff;border:1px dashed #cbd5e1;color:#475569}.error .error-banner{background:#fff1f2;border:1px solid #fecdd3;color:#b91c1c}\n"] }]
|
|
1636
|
+
}], ctorParameters: () => [{ type: JsonAdapterService }, { type: SchemaLayoutService }], propDecorators: { nodeClick: [{
|
|
1637
|
+
type: Output
|
|
1638
|
+
}], linkClick: [{
|
|
1639
|
+
type: Output
|
|
1640
|
+
}], rootRef: [{
|
|
1641
|
+
type: ViewChild,
|
|
1642
|
+
args: ["root", { static: true }]
|
|
1643
|
+
}] } });
|
|
1644
|
+
|
|
1645
|
+
/*
|
|
1646
|
+
* Public API Surface of scheme
|
|
1647
|
+
*/
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Generated bundle index. Do not edit.
|
|
1651
|
+
*/
|
|
1652
|
+
|
|
1653
|
+
export { DEFAULT_SETTINGS, JsonAdapterService, SchemaCardComponent, SchemaComponent, SchemaLayoutService };
|
|
1654
|
+
//# sourceMappingURL=json-schema.mjs.map
|