humanmap-vas 1.0.22 → 1.0.24
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/humanmap-vas-standalone.js +545 -15
- package/package.json +1 -1
@@ -6,12 +6,31 @@
|
|
6
6
|
.hm-toolbar { display:grid; grid-template-columns: auto 1fr auto; align-items:center; gap:8px; padding:8px 10px; border-bottom:1px solid #eef2f7; background:#fafafa; }
|
7
7
|
.hm-center { text-align:center; font-weight:600; color:#1f2937; }
|
8
8
|
.hm-toolbar select, .hm-toolbar button { appearance:none; border:1px solid #d1d5db; border-radius:10px; padding:6px 10px; background:#fff; cursor:pointer; font-weight:500; }
|
9
|
-
.hm-canvas-wrap { position:relative; width:100%; height:
|
10
|
-
svg.hm-svg { position:absolute; inset:0; width:100%; height:100%; }
|
9
|
+
.hm-canvas-wrap { position:relative; width:100%; height:600px; margin:auto; aspect-ratio: 2/3; background:#fff; }
|
10
|
+
svg.hm-svg { position:absolute; inset:0; width:100%; height:100%; margin: auto; }
|
11
11
|
.zone { fill: rgba(31,41,55,0); transition: fill 120ms ease; cursor: pointer; }
|
12
12
|
.zone:hover { fill: rgba(31,41,55,0.22); }
|
13
|
+
.zone.readonly { cursor: default; }
|
14
|
+
.zone.readonly:not(.selected):hover { fill: rgba(31,41,55,0); }
|
13
15
|
.zone.selected { fill: rgba(31,41,55,0.36); }
|
16
|
+
.hm-all-label { fill: #111827; font-weight: 700; font-size: 36px; text-anchor: middle; dominant-baseline: middle; }
|
14
17
|
.label { fill:#0a0a0a; font-size:36px; pointer-events: none; user-select: none; text-anchor: middle; dominant-baseline: middle; font-weight:800; }
|
18
|
+
.hm-print-btn { position: absolute; top: 10px; right: 10px; background: rgba(17,24,39,0.85); color: #f9fafb; border: none; border-radius: 8px; cursor: pointer; font-size: 18px; padding: 6px 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.3); transition: opacity 0.25s ease, background 0.2s ease; opacity: 0; pointer-events: none; }
|
19
|
+
.hm-canvas-wrap:hover .hm-print-btn { opacity: 1; pointer-events: auto; }
|
20
|
+
.hm-print-btn:hover { background: rgba(37,99,235,0.9); }
|
21
|
+
.hm-zoom-float { position: absolute; bottom: 10px; right: 10px; background: rgba(31,41,55,0.85); color: #fff; border: none; border-radius: 50%; width: 42px; height: 42px; font-size: 20px; line-height: 1; cursor: pointer; box-shadow: 0 3px 8px rgba(0,0,0,0.3); transition: transform 0.2s ease, background 0.2s ease; z-index: 20; }
|
22
|
+
.hm-zoom-float:hover { transform: scale(1.08); background: rgba(31,41,55,1); }
|
23
|
+
.hm-zoom-modal { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.85); display: flex; align-items: center; justify-content: center; z-index: 9999; opacity: 0; pointer-events: none; transition: opacity 0.3s ease; }
|
24
|
+
.hm-zoom-modal.active { opacity: 1; pointer-events: auto; }
|
25
|
+
.hm-zoom-inner { position: relative; width: 95vw; height: 90vh; background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; }
|
26
|
+
.hm-zoom-content { position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #fff; overflow: auto; }
|
27
|
+
.hm-zoom-close { position: absolute; top: 12px; right: 12px; border: none; background: rgba(0, 0, 0, 0.75); color: #fff; border-radius: 50%; width: 42px; height: 42px; font-size: 24px; cursor: pointer; z-index: 10000; line-height: 1; }
|
28
|
+
.hm-zoom-close:hover { background: rgba(0, 0, 0, 0.9); }
|
29
|
+
.hm-zoom-modal svg { width: auto; height: auto; max-width: 100%; max-height: 100%; display: block; transition: transform 0.25s ease; }
|
30
|
+
.hm-zoom-inner { transform: scale(0.95); opacity: 0; transition: transform 0.3s ease, opacity 0.3s ease; }
|
31
|
+
.hm-zoom-modal.active .hm-zoom-inner { transform: scale(1); opacity: 1; }
|
32
|
+
.hm-zoom-hint { position: absolute; bottom: 10px; right: 20px; color: rgba(0,0,0,0.4); font-size: 14px; font-family: system-ui, sans-serif; background: rgba(255,255,255,0.7); padding: 4px 10px; border-radius: 6px; pointer-events: none; user-select: none; transition: opacity 1s ease 2s; opacity: 1; }
|
33
|
+
|
15
34
|
`;
|
16
35
|
|
17
36
|
// ───────────────────────────────────────────────────────────────────────────
|
@@ -117,6 +136,7 @@
|
|
117
136
|
this._view=this.getAttribute('view') || 'head_right';
|
118
137
|
this._zones=ZONES;
|
119
138
|
this._selected=new Set();
|
139
|
+
this._readOnly = this.hasAttribute('read-only') && this.getAttribute('read-only') !== 'false';
|
120
140
|
|
121
141
|
this._upgradeProperty('selectedIds');
|
122
142
|
this._upgradeProperty('selectedZones');
|
@@ -161,9 +181,23 @@
|
|
161
181
|
}
|
162
182
|
}
|
163
183
|
|
164
|
-
connectedCallback(){
|
184
|
+
connectedCallback(){
|
185
|
+
this._renderShell();
|
186
|
+
this._renderCanvas();
|
187
|
+
this.dispatchEvent(new CustomEvent('human-map-vas:ready'));
|
188
|
+
this.dispatchEvent(new CustomEvent('human-map-vas:readonly-change', {
|
189
|
+
detail: { readOnly: this._readOnly }
|
190
|
+
}));
|
191
|
+
|
192
|
+
window.addEventListener('resize', () => {
|
193
|
+
if (this._view === 'all') {
|
194
|
+
clearTimeout(this._resizeTimer);
|
195
|
+
this._resizeTimer = setTimeout(() => this._renderAllViews(), 150);
|
196
|
+
}
|
197
|
+
});
|
198
|
+
}
|
165
199
|
|
166
|
-
static get observedAttributes() { return ['view', 'img-root']; }
|
200
|
+
static get observedAttributes() { return ['view', 'img-root', 'read-only']; }
|
167
201
|
|
168
202
|
attributeChangedCallback (name, oldValue, newValue) {
|
169
203
|
if (oldValue === newValue) return;
|
@@ -171,6 +205,11 @@
|
|
171
205
|
if (name === 'view') {
|
172
206
|
this._view = newValue;
|
173
207
|
if (this._root) this._renderCanvas();
|
208
|
+
|
209
|
+
this.dispatchEvent(new CustomEvent('human-map-vas:view-changed', {
|
210
|
+
detail: { view: newValue }
|
211
|
+
}));
|
212
|
+
|
174
213
|
return;
|
175
214
|
}
|
176
215
|
|
@@ -187,6 +226,34 @@
|
|
187
226
|
if (this._root) this._renderCanvas();
|
188
227
|
return;
|
189
228
|
}
|
229
|
+
|
230
|
+
if (name === 'read-only') {
|
231
|
+
this._readOnly = newValue === 'true' || newValue === true;
|
232
|
+
|
233
|
+
if (this._root) {
|
234
|
+
// Refrescar la vista completamente según el modo
|
235
|
+
if (this._view === 'all') {
|
236
|
+
this._renderAllViews();
|
237
|
+
} else {
|
238
|
+
this._renderCanvas();
|
239
|
+
}
|
240
|
+
|
241
|
+
// Actualizar la visibilidad de toolbar y botón de impresión
|
242
|
+
const toolbar = this._root.querySelector('.hm-toolbar');
|
243
|
+
const printBtn = this.shadowRoot.getElementById('printBtn');
|
244
|
+
|
245
|
+
if (this._view === 'all' && this._readOnly) {
|
246
|
+
if (toolbar) toolbar.style.display = 'none';
|
247
|
+
if (printBtn) printBtn.style.display = 'block';
|
248
|
+
} else {
|
249
|
+
if (toolbar) toolbar.style.display = '';
|
250
|
+
if (printBtn) printBtn.style.display = 'none';
|
251
|
+
}
|
252
|
+
}
|
253
|
+
|
254
|
+
return;
|
255
|
+
}
|
256
|
+
|
190
257
|
}
|
191
258
|
|
192
259
|
// Devuelve solo IDs seleccionados
|
@@ -198,7 +265,13 @@
|
|
198
265
|
set selectedIds(ids) {
|
199
266
|
if (!Array.isArray(ids)) return;
|
200
267
|
this._selected = new Set(ids);
|
201
|
-
|
268
|
+
|
269
|
+
if (this._root) {
|
270
|
+
// Si está en modo global, renderizamos todas las vistas
|
271
|
+
if (this._view === 'all') this._renderAllViews();
|
272
|
+
else this._renderZones();
|
273
|
+
this._emit();
|
274
|
+
}
|
202
275
|
}
|
203
276
|
|
204
277
|
// Devuelve objetos completos (id, code, label, view)
|
@@ -242,7 +315,11 @@
|
|
242
315
|
this._root=document.createElement('div');
|
243
316
|
this._root.className='hm';
|
244
317
|
|
245
|
-
const opts=
|
318
|
+
const opts = [
|
319
|
+
`<option value="all">Todas las vistas</option>`,
|
320
|
+
...VIEWS.map(v => `<option value="${v.id}">${v.label}</option>`)
|
321
|
+
].join('');
|
322
|
+
|
246
323
|
this._root.innerHTML=`
|
247
324
|
<div class="hm-toolbar">
|
248
325
|
<button id="prev">◀</button>
|
@@ -259,6 +336,8 @@
|
|
259
336
|
<g id="bg"></g>
|
260
337
|
<g id="zones"></g>
|
261
338
|
</svg>
|
339
|
+
<button id="printBtn" title="Imprimir vista" class="hm-print-btn">🖨️</button>
|
340
|
+
<button id="zoom-float" class="hm-zoom-float" title="Ampliar vista">⤢</button>
|
262
341
|
</div>`;
|
263
342
|
this.shadowRoot.append(style,this._root);
|
264
343
|
|
@@ -277,6 +356,17 @@
|
|
277
356
|
this._els.prev.addEventListener('click',()=>this._cycle(-1));
|
278
357
|
this._els.next.addEventListener('click',()=>this._cycle(1));
|
279
358
|
this._els.reset.addEventListener('click',()=>this.clear());
|
359
|
+
this._els.printBtn = this.shadowRoot.getElementById('printBtn');
|
360
|
+
this._els.printBtn.addEventListener('click', () => {
|
361
|
+
this._els.printBtn.style.transform = 'scale(0.94)';
|
362
|
+
setTimeout(() => {
|
363
|
+
this._els.printBtn.style.transform = '';
|
364
|
+
this._printCanvasOnly(); // ⬅️ imprime solo el área actual
|
365
|
+
}, 120);
|
366
|
+
});
|
367
|
+
this._els.zoomFloat = this.shadowRoot.getElementById('zoom-float');
|
368
|
+
this._els.zoomFloat.addEventListener('click', () => this._openPreviewModal());
|
369
|
+
|
280
370
|
}
|
281
371
|
|
282
372
|
_cycle(dir){
|
@@ -286,8 +376,59 @@
|
|
286
376
|
}
|
287
377
|
|
288
378
|
_renderCanvas(){
|
289
|
-
|
290
|
-
|
379
|
+
// Ocultar toolbar si está en modo global y solo lectura
|
380
|
+
if (this._els && this._root) {
|
381
|
+
const toolbar = this._root.querySelector('.hm-toolbar');
|
382
|
+
if (this._view === 'all' && this._readOnly) {
|
383
|
+
toolbar.style.display = 'none';
|
384
|
+
} else {
|
385
|
+
toolbar.style.display = '';
|
386
|
+
}
|
387
|
+
}
|
388
|
+
|
389
|
+
// Mostrar botón de impresión solo en modo global + solo lectura
|
390
|
+
const printBtn = this.shadowRoot.getElementById('printBtn');
|
391
|
+
if (printBtn) {
|
392
|
+
if (this._view === 'all' && this._readOnly) {
|
393
|
+
printBtn.style.display = 'block';
|
394
|
+
} else {
|
395
|
+
printBtn.style.display = 'none';
|
396
|
+
}
|
397
|
+
}
|
398
|
+
|
399
|
+
if (this._els.zoomFloat) {
|
400
|
+
this._els.zoomFloat.style.display = this._readOnly ? 'block' : 'none';
|
401
|
+
}
|
402
|
+
|
403
|
+
// Limpia cualquier render anterior (modo all o vista única)
|
404
|
+
if (this._els && this._els.svg) {
|
405
|
+
this._els.svg.innerHTML = `
|
406
|
+
<defs id="defs"></defs>
|
407
|
+
<g id="bg"></g>
|
408
|
+
<g id="zones"></g>
|
409
|
+
`;
|
410
|
+
this._els.bg = this._els.svg.querySelector('#bg');
|
411
|
+
this._els.zones = this._els.svg.querySelector('#zones');
|
412
|
+
}
|
413
|
+
|
414
|
+
// 🆕 Detección de modo global
|
415
|
+
if (this._view === 'all') {
|
416
|
+
this._renderAllViews();
|
417
|
+
this._els.cur.textContent = 'Todas las vistas';
|
418
|
+
this._els.picker.value = 'all';
|
419
|
+
this._els.prev.disabled = true;
|
420
|
+
this._els.next.disabled = true;
|
421
|
+
this._els.reset.disabled = this._readOnly;
|
422
|
+
return;
|
423
|
+
} else {
|
424
|
+
this._els.prev.disabled = false;
|
425
|
+
this._els.next.disabled = false;
|
426
|
+
this._els.reset.disabled = false;
|
427
|
+
}
|
428
|
+
|
429
|
+
// --- Render normal ---
|
430
|
+
const layout = VIEW_LAYOUTS[this._view];
|
431
|
+
const v = VIEWS.find(v => v.id === this._view);
|
291
432
|
if(!layout||!v)return;
|
292
433
|
this._els.cur.textContent=v.label;
|
293
434
|
this._els.picker.value=v.id;
|
@@ -298,6 +439,124 @@
|
|
298
439
|
this._renderZones();
|
299
440
|
}
|
300
441
|
|
442
|
+
_renderAllViews() {
|
443
|
+
const svg = this._els.svg;
|
444
|
+
svg.innerHTML = ''; // limpiar
|
445
|
+
|
446
|
+
// Permitir configurar columnas dinámicas (por atributo) o calcular automáticamente
|
447
|
+
const defaultCols = 4;
|
448
|
+
const attrCols = parseInt(this.getAttribute('columns'), 10);
|
449
|
+
let cols = !isNaN(attrCols) && attrCols > 0 ? attrCols : defaultCols;
|
450
|
+
|
451
|
+
const gap = 80;
|
452
|
+
const cellW = 560, cellH = 720;
|
453
|
+
|
454
|
+
const gridW = cols * (cellW + gap);
|
455
|
+
const gridH = Math.ceil(VIEWS.length / cols) * (cellH + gap);
|
456
|
+
|
457
|
+
svg.setAttribute('viewBox', `0 0 ${gridW} ${gridH}`);
|
458
|
+
const gRoot = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
459
|
+
svg.appendChild(gRoot);
|
460
|
+
|
461
|
+
VIEWS.forEach((v, i) => {
|
462
|
+
const layout = VIEW_LAYOUTS[v.id] || { vb: [0, 0, 1024, 1536], y: 0, h: 1536, rotate: 0 };
|
463
|
+
const row = Math.floor(i / cols);
|
464
|
+
const col = i % cols;
|
465
|
+
const gx = col * (cellW + gap);
|
466
|
+
const gy = row * (cellH + gap);
|
467
|
+
|
468
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
469
|
+
// Escala dinámica: reduce más si hay muchas columnas
|
470
|
+
const scale = Math.max(0.3, 0.9 - cols * 0.1);
|
471
|
+
g.setAttribute('transform', `translate(${gx}, ${gy}) scale(${scale})`);
|
472
|
+
|
473
|
+
// Fondo (imagen)
|
474
|
+
const img = document.createElementNS('http://www.w3.org/2000/svg', 'image');
|
475
|
+
img.setAttribute('href', this._bg[v.id]);
|
476
|
+
img.setAttribute('x', '0');
|
477
|
+
img.setAttribute('y', layout?.y || 0);
|
478
|
+
img.setAttribute('width', layout?.vb[2] || 1024);
|
479
|
+
img.setAttribute('height', layout?.h || 1536);
|
480
|
+
img.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
481
|
+
g.appendChild(img);
|
482
|
+
|
483
|
+
// Grupo para las zonas (permite rotar sin afectar la imagen)
|
484
|
+
const gZones = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
485
|
+
|
486
|
+
// Rotar solo las zonas del cuello
|
487
|
+
let rotation = 0;
|
488
|
+
if (v.id === 'neck_right') rotation = 12;
|
489
|
+
if (v.id === 'neck_left') rotation = -12;
|
490
|
+
if (rotation !== 0) {
|
491
|
+
gZones.setAttribute(
|
492
|
+
'transform',
|
493
|
+
`rotate(${rotation}, ${layout.vb[2]*0.55}, ${layout.vb[3]*0.45})`
|
494
|
+
);
|
495
|
+
}
|
496
|
+
|
497
|
+
// --- Dibujar zonas ---
|
498
|
+
const zones = this._zones.filter(z => z.view === v.id);
|
499
|
+
zones.forEach(z => {
|
500
|
+
const { x, y, w, h } = z.shape;
|
501
|
+
|
502
|
+
// Zona rectangular
|
503
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
504
|
+
rect.setAttribute('x', x * layout.vb[2]);
|
505
|
+
rect.setAttribute('y', y * layout.vb[3]);
|
506
|
+
rect.setAttribute('width', w * layout.vb[2]);
|
507
|
+
rect.setAttribute('height', h * layout.vb[3]);
|
508
|
+
rect.setAttribute('rx', Math.min(w, h) * layout.vb[2] * 0.1);
|
509
|
+
rect.classList.add('zone');
|
510
|
+
if (this._selected.has(z.id)) rect.classList.add('selected');
|
511
|
+
if (this._readOnly) {
|
512
|
+
rect.classList.add('readonly');
|
513
|
+
} else {
|
514
|
+
rect.addEventListener('click', e => {
|
515
|
+
e.stopPropagation();
|
516
|
+
if (this._selected.has(z.id)) this._selected.delete(z.id);
|
517
|
+
else this._selected.add(z.id);
|
518
|
+
this._renderAllViews(); // refrescar vista global
|
519
|
+
this._emit();
|
520
|
+
});
|
521
|
+
}
|
522
|
+
gZones.appendChild(rect);
|
523
|
+
|
524
|
+
// Label centrado
|
525
|
+
const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
526
|
+
t.textContent = z.code;
|
527
|
+
t.setAttribute('x', (x + w / 2) * layout.vb[2]);
|
528
|
+
t.setAttribute('y', (y + h / 2) * layout.vb[3]);
|
529
|
+
t.setAttribute('fill', '#0a0a0a');
|
530
|
+
t.setAttribute('font-size', '32');
|
531
|
+
t.setAttribute('font-weight', '700');
|
532
|
+
t.setAttribute('text-anchor', 'middle');
|
533
|
+
t.setAttribute('dominant-baseline', 'middle');
|
534
|
+
t.setAttribute('pointer-events', 'none');
|
535
|
+
gZones.appendChild(t);
|
536
|
+
});
|
537
|
+
|
538
|
+
// Añadir grupo de zonas dentro del grupo principal
|
539
|
+
g.appendChild(gZones);
|
540
|
+
|
541
|
+
|
542
|
+
// Label de la vista
|
543
|
+
const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
544
|
+
t.textContent = v.label;
|
545
|
+
t.setAttribute('x', layout.vb[2] / 2);
|
546
|
+
t.setAttribute('y', 80);
|
547
|
+
t.setAttribute('class', 'hm-all-label');
|
548
|
+
g.appendChild(t);
|
549
|
+
|
550
|
+
|
551
|
+
gRoot.appendChild(g);
|
552
|
+
});
|
553
|
+
|
554
|
+
if (this._els.zoomFloat) {
|
555
|
+
this._els.zoomFloat.style.display = this._readOnly ? 'block' : 'none';
|
556
|
+
}
|
557
|
+
|
558
|
+
}
|
559
|
+
|
301
560
|
_renderZones(){
|
302
561
|
const g=this._els.zones;g.innerHTML='';
|
303
562
|
const layout=VIEW_LAYOUTS[this._view];
|
@@ -309,19 +568,34 @@
|
|
309
568
|
Z.forEach(z=>{
|
310
569
|
const{x,y,w,h}=z.shape;
|
311
570
|
const rect=document.createElementNS('http://www.w3.org/2000/svg','rect');
|
571
|
+
|
312
572
|
rect.setAttribute('x',x*vw);rect.setAttribute('y',y*vh);
|
313
573
|
rect.setAttribute('width',w*vw);rect.setAttribute('height',h*vh);
|
314
574
|
rect.setAttribute('rx',Math.min(w*vw,h*vh)*0.1);
|
575
|
+
|
315
576
|
rect.classList.add('zone');
|
316
|
-
|
317
|
-
rect.
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
}
|
577
|
+
|
578
|
+
if (this._selected.has(z.id)) rect.classList.add('selected');
|
579
|
+
|
580
|
+
// Si está en modo lectura, añadir clase visual
|
581
|
+
if (this._readOnly) {
|
582
|
+
rect.classList.add('readonly');
|
583
|
+
} else {
|
584
|
+
rect.classList.remove('readonly');
|
585
|
+
rect.addEventListener('click', e => {
|
586
|
+
e.stopPropagation();
|
587
|
+
if (this._selected.has(z.id)) this._selected.delete(z.id);
|
588
|
+
else this._selected.add(z.id);
|
589
|
+
this._renderZones();
|
590
|
+
this._emit();
|
591
|
+
});
|
592
|
+
}
|
593
|
+
|
594
|
+
|
323
595
|
g.appendChild(rect);
|
596
|
+
|
324
597
|
const t=document.createElementNS('http://www.w3.org/2000/svg','text');
|
598
|
+
|
325
599
|
t.setAttribute('x',(x+w/2)*vw);
|
326
600
|
t.setAttribute('y',(y+h/2)*vh);
|
327
601
|
t.textContent=z.code;
|
@@ -330,6 +604,262 @@
|
|
330
604
|
});
|
331
605
|
}
|
332
606
|
|
607
|
+
// Imprime solo el área visible (hm-canvas-wrap) en una nueva ventana
|
608
|
+
_printCanvasOnly() {
|
609
|
+
if (!this._els || !this._els.svg) return;
|
610
|
+
|
611
|
+
// Clonar el SVG actual completo (vistas individuales o "all")
|
612
|
+
const clone = this._els.svg.cloneNode(true);
|
613
|
+
|
614
|
+
// Asegurar rutas absolutas en <image> (importante para que se muestren)
|
615
|
+
clone.querySelectorAll('image').forEach(img => {
|
616
|
+
const href = img.getAttribute('href') || img.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
|
617
|
+
if (href) {
|
618
|
+
const a = document.createElement('a');
|
619
|
+
a.href = href;
|
620
|
+
const abs = a.href;
|
621
|
+
img.setAttribute('href', abs);
|
622
|
+
img.setAttributeNS('http://www.w3.org/1999/xlink', 'href', abs);
|
623
|
+
}
|
624
|
+
});
|
625
|
+
|
626
|
+
// Inyectar los estilos básicos directamente en el SVG (para conservar colores y transparencias)
|
627
|
+
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
628
|
+
style.textContent = `
|
629
|
+
.zone { fill: rgba(31,41,55,0); cursor: pointer; transition: fill 120ms ease; }
|
630
|
+
.zone:hover { fill: rgba(31,41,55,0.22); }
|
631
|
+
.zone.selected { fill: rgba(31,41,55,0.36); }
|
632
|
+
.label { fill: #0a0a0a; font-size: 36px; font-weight: 800;
|
633
|
+
text-anchor: middle; dominant-baseline: middle; pointer-events: none; user-select: none; }
|
634
|
+
`;
|
635
|
+
clone.insertBefore(style, clone.firstChild);
|
636
|
+
|
637
|
+
const serializer = new XMLSerializer();
|
638
|
+
const svgMarkup = serializer.serializeToString(clone);
|
639
|
+
|
640
|
+
const isGlobal = this._view === 'all';
|
641
|
+
|
642
|
+
// HTML limpio con estilos mínimos
|
643
|
+
const html = `
|
644
|
+
<!DOCTYPE html>
|
645
|
+
<html lang="es">
|
646
|
+
<head>
|
647
|
+
<meta charset="utf-8">
|
648
|
+
<title>Impresión del mapa anatómico</title>
|
649
|
+
<style>
|
650
|
+
html, body {
|
651
|
+
margin: 0;
|
652
|
+
padding: 0mm;
|
653
|
+
background: #fff;
|
654
|
+
text-align: center;
|
655
|
+
}
|
656
|
+
svg {
|
657
|
+
width: 100%;
|
658
|
+
height: auto;
|
659
|
+
display: block;
|
660
|
+
}
|
661
|
+
.hm-all-label {
|
662
|
+
font-family: system-ui, sans-serif;
|
663
|
+
font-size: 48px;
|
664
|
+
font-weight: 800;
|
665
|
+
fill: #111827;
|
666
|
+
text-anchor: middle;
|
667
|
+
dominant-baseline: middle;
|
668
|
+
}
|
669
|
+
@page {
|
670
|
+
size: ${isGlobal ? 'A4 landscape' : 'A4 portrait'};
|
671
|
+
margin: 5mm;
|
672
|
+
}
|
673
|
+
</style>
|
674
|
+
</head>
|
675
|
+
<body>
|
676
|
+
${svgMarkup}
|
677
|
+
</body>
|
678
|
+
</html>
|
679
|
+
`;
|
680
|
+
|
681
|
+
// Abrir ventana y escribir el HTML
|
682
|
+
const printWin = window.open('', '_blank', 'width=1024,height=768');
|
683
|
+
if (!printWin) {
|
684
|
+
alert('El navegador bloqueó la ventana de impresión. Permite popups para continuar.');
|
685
|
+
return;
|
686
|
+
}
|
687
|
+
|
688
|
+
printWin.document.open();
|
689
|
+
printWin.document.write(html);
|
690
|
+
printWin.document.close();
|
691
|
+
|
692
|
+
// Esperar hasta que las imágenes del SVG estén listas
|
693
|
+
const waitForImages = () => {
|
694
|
+
const imgs = printWin.document.querySelectorAll('image');
|
695
|
+
const promises = Array.from(imgs).map(img => {
|
696
|
+
return new Promise(resolve => {
|
697
|
+
const test = new Image();
|
698
|
+
test.onload = test.onerror = resolve;
|
699
|
+
test.src = img.getAttribute('href') || img.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
|
700
|
+
});
|
701
|
+
});
|
702
|
+
return Promise.all(promises);
|
703
|
+
};
|
704
|
+
|
705
|
+
waitForImages().then(() => {
|
706
|
+
printWin.focus();
|
707
|
+
printWin.print();
|
708
|
+
setTimeout(() => printWin.close(), 1000);
|
709
|
+
});
|
710
|
+
}
|
711
|
+
|
712
|
+
_openPreviewModal() {
|
713
|
+
if (!this._els || !this._els.svg) return;
|
714
|
+
|
715
|
+
// Clonar el SVG actual
|
716
|
+
const clone = this._els.svg.cloneNode(true);
|
717
|
+
|
718
|
+
// Asegurar que las imágenes se vean correctamente (usar rutas absolutas)
|
719
|
+
clone.querySelectorAll('image').forEach(img => {
|
720
|
+
const href = img.getAttribute('href') || img.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
|
721
|
+
if (href) {
|
722
|
+
const a = document.createElement('a');
|
723
|
+
a.href = href;
|
724
|
+
const abs = a.href;
|
725
|
+
img.setAttribute('href', abs);
|
726
|
+
img.setAttributeNS('http://www.w3.org/1999/xlink', 'href', abs);
|
727
|
+
}
|
728
|
+
});
|
729
|
+
|
730
|
+
// Ajustar estilos dentro del SVG
|
731
|
+
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
732
|
+
style.textContent = `
|
733
|
+
.zone { fill: rgba(31,41,55,0); cursor: default; transition: fill 120ms ease; }
|
734
|
+
.zone.selected { fill: rgba(31,41,55,0.36); }
|
735
|
+
.label { fill: #0a0a0a; font-size: 42px; font-weight: 800;
|
736
|
+
text-anchor: middle; dominant-baseline: middle;
|
737
|
+
pointer-events: none; user-select: none; }
|
738
|
+
.hm-all-label {
|
739
|
+
font-family: system-ui, sans-serif;
|
740
|
+
font-size: 48px;
|
741
|
+
font-weight: 800;
|
742
|
+
fill: #111827;
|
743
|
+
text-anchor: middle;
|
744
|
+
dominant-baseline: middle;
|
745
|
+
}
|
746
|
+
`;
|
747
|
+
clone.insertBefore(style, clone.firstChild);
|
748
|
+
|
749
|
+
// Crear el modal
|
750
|
+
const modal = document.createElement('div');
|
751
|
+
modal.className = 'hm-zoom-modal';
|
752
|
+
modal.innerHTML = `
|
753
|
+
<div class="hm-zoom-inner">
|
754
|
+
<button class="hm-zoom-close" title="Cerrar">×</button>
|
755
|
+
<div class="hm-zoom-content"></div>
|
756
|
+
<div class="hm-zoom-hint">🖱️ Usa la rueda para hacer zoom y arrastra para mover. Presiona <strong>Esc</strong> para cerrar</div>
|
757
|
+
</div>
|
758
|
+
`;
|
759
|
+
|
760
|
+
// Insertar el SVG clonado dentro del modal
|
761
|
+
modal.querySelector('.hm-zoom-content').appendChild(clone);
|
762
|
+
this.shadowRoot.appendChild(modal);
|
763
|
+
|
764
|
+
// Forzar render y animación
|
765
|
+
requestAnimationFrame(() => modal.classList.add('active'));
|
766
|
+
// 🔒 Bloquear scroll del fondo
|
767
|
+
document.body.style.overflow = 'hidden';
|
768
|
+
|
769
|
+
// Cerrar modal
|
770
|
+
const close = () => {
|
771
|
+
modal.classList.remove('active');
|
772
|
+
|
773
|
+
// 🔓 Restaurar scroll del fondo
|
774
|
+
document.body.style.overflow = '';
|
775
|
+
|
776
|
+
setTimeout(() => modal.remove(), 300);
|
777
|
+
|
778
|
+
// Quitar listener del teclado al cerrar
|
779
|
+
document.removeEventListener('keydown', onKey);
|
780
|
+
};
|
781
|
+
|
782
|
+
modal.querySelector('.hm-zoom-close').addEventListener('click', close);
|
783
|
+
modal.addEventListener('click', e => {
|
784
|
+
if (e.target === modal) close();
|
785
|
+
});
|
786
|
+
|
787
|
+
// 🔑 Cerrar con tecla Escape
|
788
|
+
const onKey = e => {
|
789
|
+
if (e.key === 'Escape') close();
|
790
|
+
};
|
791
|
+
document.addEventListener('keydown', onKey);
|
792
|
+
|
793
|
+
// ───────────────────────────────
|
794
|
+
// 🔍 Zoom + Pan interactivo
|
795
|
+
// ───────────────────────────────
|
796
|
+
const content = modal.querySelector('.hm-zoom-content');
|
797
|
+
let scale = 1;
|
798
|
+
let translateX = 0, translateY = 0;
|
799
|
+
let isPanning = false;
|
800
|
+
let startX = 0, startY = 0;
|
801
|
+
|
802
|
+
content.addEventListener('wheel', e => {
|
803
|
+
e.preventDefault();
|
804
|
+
|
805
|
+
const rect = clone.getBoundingClientRect();
|
806
|
+
const offsetX = e.clientX - rect.left;
|
807
|
+
const offsetY = e.clientY - rect.top;
|
808
|
+
|
809
|
+
const delta = e.deltaY < 0 ? 0.1 : -0.1;
|
810
|
+
const newScale = Math.min(Math.max(scale + delta, 0.5), 3);
|
811
|
+
|
812
|
+
// Mantener el punto bajo el puntero "anclado"
|
813
|
+
const dx = offsetX - (offsetX / scale) * newScale;
|
814
|
+
const dy = offsetY - (offsetY / scale) * newScale;
|
815
|
+
translateX += dx;
|
816
|
+
translateY += dy;
|
817
|
+
|
818
|
+
clone.style.transform = `translate(${translateX}px, ${translateY}px) scale(${newScale})`;
|
819
|
+
clone.style.transformOrigin = '0 0';
|
820
|
+
scale = newScale;
|
821
|
+
});
|
822
|
+
|
823
|
+
// ───────────────────────────────
|
824
|
+
// 🖱️ Arrastrar (Pan)
|
825
|
+
// ───────────────────────────────
|
826
|
+
content.addEventListener('mousedown', e => {
|
827
|
+
e.preventDefault();
|
828
|
+
isPanning = true;
|
829
|
+
startX = e.clientX - translateX;
|
830
|
+
startY = e.clientY - translateY;
|
831
|
+
content.style.cursor = 'grabbing';
|
832
|
+
});
|
833
|
+
|
834
|
+
content.addEventListener('mousemove', e => {
|
835
|
+
if (!isPanning) return;
|
836
|
+
translateX = e.clientX - startX;
|
837
|
+
translateY = e.clientY - startY;
|
838
|
+
clone.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
|
839
|
+
});
|
840
|
+
|
841
|
+
content.addEventListener('mouseup', () => {
|
842
|
+
isPanning = false;
|
843
|
+
content.style.cursor = 'default';
|
844
|
+
});
|
845
|
+
|
846
|
+
content.addEventListener('mouseleave', () => {
|
847
|
+
isPanning = false;
|
848
|
+
content.style.cursor = 'default';
|
849
|
+
});
|
850
|
+
|
851
|
+
// ───────────────────────────────
|
852
|
+
// 🔄 Doble clic para resetear vista
|
853
|
+
// ───────────────────────────────
|
854
|
+
content.addEventListener('dblclick', () => {
|
855
|
+
scale = 1;
|
856
|
+
translateX = 0;
|
857
|
+
translateY = 0;
|
858
|
+
clone.style.transform = '';
|
859
|
+
});
|
860
|
+
|
861
|
+
}
|
862
|
+
|
333
863
|
_emit(){this.dispatchEvent(new CustomEvent('human-map-vas:select',{detail:{selected:this.getSelected()}}));}
|
334
864
|
}
|
335
865
|
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "humanmap-vas",
|
3
|
-
"version": "1.0.
|
3
|
+
"version": "1.0.24",
|
4
4
|
"description": "**HumanMap VAS** es una librería web que permite graficar el cuerpo humano con vistas anatómicas interactivas para identificar zonas según el sistema VAS. Desarrollada como *Web Component standalone*, puede integrarse fácilmente en proyectos **HTML**, **Django**, o **Vue.js**.",
|
5
5
|
"main": "humanmap-vas-standalone.js",
|
6
6
|
"files": [
|