@moni-labs/moni-ui 0.4.13 → 0.4.16
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/custom-elements.json +101 -81
- package/dist/browser/moni-ui.iife.min.js +1538 -1384
- package/dist/browser/moni-ui.iife.min.js.br +0 -0
- package/dist/browser/moni-ui.iife.min.js.gz +0 -0
- package/dist/browser/moni-ui.iife.min.js.map +1 -1
- package/dist/browser/moni-ui.min.js +2751 -2501
- package/dist/browser/moni-ui.min.js.br +0 -0
- package/dist/browser/moni-ui.min.js.gz +0 -0
- package/dist/browser/moni-ui.min.js.map +1 -1
- package/dist/cdn/chunks/{class-map-DsL1Jr2T.js → class-map-QyT9W7rZ.js} +1 -1
- package/dist/cdn/chunks/{decorate-DxJYS4gw.js → decorate-B7wZvwrn.js} +629 -543
- package/dist/cdn/chunks/if-defined-5hr_ccFw.js +1 -0
- package/dist/cdn/chunks/{live-D52JqeKf.js → live-Hm1Di_ND.js} +1 -1
- package/dist/cdn/components/moni-app-bar.js +1 -1
- package/dist/cdn/components/moni-badge.js +1 -1
- package/dist/cdn/components/moni-bottom-sheet.js +2 -1
- package/dist/cdn/components/moni-button-group.js +1 -1
- package/dist/cdn/components/moni-button-segment.js +1 -1
- package/dist/cdn/components/moni-button.js +1 -1
- package/dist/cdn/components/moni-card.js +1 -1
- package/dist/cdn/components/moni-carousel.js +1 -1
- package/dist/cdn/components/moni-checkbox.js +1 -1
- package/dist/cdn/components/moni-chip.js +1 -1
- package/dist/cdn/components/moni-color-field.js +1 -1
- package/dist/cdn/components/moni-context-menu.js +1 -1
- package/dist/cdn/components/moni-dialog.js +1 -1
- package/dist/cdn/components/moni-divider.js +1 -1
- package/dist/cdn/components/moni-expansion.js +1 -1
- package/dist/cdn/components/moni-fab-menu.js +1 -1
- package/dist/cdn/components/moni-fab.js +1 -1
- package/dist/cdn/components/moni-file-field.js +1 -1
- package/dist/cdn/components/moni-icon.js +1 -1
- package/dist/cdn/components/moni-list-item.js +1 -1
- package/dist/cdn/components/moni-list.js +1 -1
- package/dist/cdn/components/moni-loading-indicator.js +1 -1
- package/dist/cdn/components/moni-menu-item.js +1 -1
- package/dist/cdn/components/moni-menu.js +1 -1
- package/dist/cdn/components/moni-morph-modal.js +3 -2
- package/dist/cdn/components/moni-nav-item.js +1 -1
- package/dist/cdn/components/moni-nav.js +1 -1
- package/dist/cdn/components/moni-progress.js +1 -1
- package/dist/cdn/components/moni-radio.js +1 -1
- package/dist/cdn/components/moni-ripple.js +1 -1
- package/dist/cdn/components/moni-segmented-button.js +1 -1
- package/dist/cdn/components/moni-select-option.js +1 -1
- package/dist/cdn/components/moni-select.js +512 -473
- package/dist/cdn/components/moni-shape.js +1 -1
- package/dist/cdn/components/moni-side-sheet.js +1 -1
- package/dist/cdn/components/moni-slider.js +1 -1
- package/dist/cdn/components/moni-snackbar.js +1 -1
- package/dist/cdn/components/moni-split-button.js +1 -1
- package/dist/cdn/components/moni-step.js +1 -1
- package/dist/cdn/components/moni-stepper.js +1 -1
- package/dist/cdn/components/moni-switch.js +1 -1
- package/dist/cdn/components/moni-tab.js +1 -1
- package/dist/cdn/components/moni-tabs.js +1 -1
- package/dist/cdn/components/moni-text-field.js +122 -95
- package/dist/cdn/components/moni-textarea.js +52 -52
- package/dist/cdn/components/moni-time-picker.js +1 -1
- package/dist/cdn/components/moni-toolbar.js +1 -1
- package/dist/cdn/components/moni-tooltip.js +1 -1
- package/dist/cdn/components/moni-typography.js +1 -1
- package/dist/cdn/importmap.json +98 -98
- package/dist/components/_base/field-styles.d.ts.map +1 -1
- package/dist/components/_base/field-styles.js +662 -576
- package/dist/components/moni-bottom-sheet.d.ts +19 -5
- package/dist/components/moni-bottom-sheet.d.ts.map +1 -1
- package/dist/components/moni-bottom-sheet.js +111 -30
- package/dist/components/moni-morph-modal.d.ts +10 -0
- package/dist/components/moni-morph-modal.d.ts.map +1 -1
- package/dist/components/moni-morph-modal.js +323 -217
- package/dist/components/moni-select.d.ts +16 -16
- package/dist/components/moni-select.d.ts.map +1 -1
- package/dist/components/moni-select.js +797 -727
- package/dist/components/moni-text-field.d.ts +15 -15
- package/dist/components/moni-text-field.d.ts.map +1 -1
- package/dist/components/moni-text-field.js +224 -180
- package/dist/components/moni-textarea.d.ts +13 -13
- package/dist/components/moni-textarea.d.ts.map +1 -1
- package/dist/components/moni-textarea.js +127 -122
- package/docs/api.json +109 -89
- package/docs/assets/moni-color-field/shape--square.png +0 -0
- package/docs/assets/moni-file-field/shape--square.png +0 -0
- package/docs/assets/moni-morph-modal/autoHeight--false.png +0 -0
- package/docs/assets/moni-morph-modal/autoHeight--true.png +0 -0
- package/docs/components/bottom-sheet.md +2 -1
- package/docs/components/morph-modal.md +16 -7
- package/docs/components/select.md +74 -74
- package/docs/components/text-field.md +75 -66
- package/docs/components/textarea.md +51 -51
- package/docs/llms.txt +3 -3
- package/package.json +1 -1
- package/src/components/_base/field-styles.ts +723 -637
- package/src/components/moni-bottom-sheet.test.ts +331 -0
- package/src/components/moni-bottom-sheet.ts +117 -29
- package/src/components/moni-morph-modal.test.ts +490 -413
- package/src/components/moni-morph-modal.ts +2274 -2162
- package/src/components/moni-select.test.ts +489 -466
- package/src/components/moni-select.ts +1963 -1815
- package/src/components/moni-text-field.test.ts +366 -342
- package/src/components/moni-text-field.ts +717 -645
- package/src/components/moni-textarea.test.ts +201 -197
- package/src/components/moni-textarea.ts +476 -460
- package/dist/cdn/chunks/if-defined-DdzvcoI7.js +0 -1
|
@@ -1,234 +1,238 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file components/moni-morph-modal.ts
|
|
3
|
-
* @package @moni-labs/moni-ui
|
|
4
|
-
* @license MIT
|
|
5
|
-
* @contributors Moni Labs & Contributors
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { html, css } from 'lit';
|
|
9
|
-
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
10
|
-
import { MoniElement, sharedStyles } from './_base/index.js';
|
|
11
|
-
import { gsap } from 'gsap';
|
|
12
|
-
import { Flip } from 'gsap/Flip';
|
|
13
|
-
import { CustomEase } from 'gsap/CustomEase';
|
|
14
|
-
import './moni-icon.js';
|
|
15
|
-
|
|
16
|
-
gsap.registerPlugin(Flip, CustomEase);
|
|
17
|
-
|
|
18
|
-
CustomEase.create('morph-open', '0.15, 0.85, 0.2, 1');
|
|
19
|
-
CustomEase.create('morph-close', '0.4, 0, 0.2, 1');
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
type Placement =
|
|
23
|
-
| 'center'
|
|
24
|
-
| 'top'
|
|
25
|
-
| 'top-start'
|
|
26
|
-
| 'top-end'
|
|
27
|
-
| 'bottom'
|
|
28
|
-
| 'bottom-start'
|
|
29
|
-
| 'bottom-end'
|
|
30
|
-
| 'left'
|
|
31
|
-
| 'left-start'
|
|
32
|
-
| 'left-end'
|
|
33
|
-
| 'right'
|
|
34
|
-
| 'right-start'
|
|
35
|
-
| 'right-end'
|
|
36
|
-
| 'viewport-center'
|
|
37
|
-
| 'viewport-top'
|
|
38
|
-
| 'viewport-bottom'
|
|
39
|
-
| 'viewport-left'
|
|
40
|
-
| 'viewport-right'
|
|
41
|
-
| 'viewport-top-left'
|
|
42
|
-
| 'viewport-top-right'
|
|
43
|
-
| 'viewport-bottom-left'
|
|
44
|
-
| 'viewport-bottom-right';
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Estado interno de coordenadas y estilos del objetivo de morphing antes de la animación.
|
|
48
|
-
*
|
|
49
|
-
* @internal
|
|
50
|
-
*/
|
|
51
|
-
interface TargetState {
|
|
52
|
-
rect: DOMRect;
|
|
53
|
-
backgroundColor: string;
|
|
54
|
-
color: string;
|
|
55
|
-
borderRadius: string;
|
|
56
|
-
boxShadow: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Referencias a los elementos internos de icono y texto animados durante el morphing.
|
|
61
|
-
*
|
|
62
|
-
* @internal
|
|
63
|
-
*/
|
|
64
|
-
interface MorphElements {
|
|
65
|
-
text: HTMLElement | null;
|
|
66
|
-
icon: HTMLElement | null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Componente Material Design 3 Morph Modal.
|
|
71
|
-
*
|
|
72
|
-
* Un diálogo altamente interactivo que usa animaciones GSAP FLIP (First, Last, Invert, Play)
|
|
73
|
-
* para "transformar" (morph) sin problemas cualquier elemento clickeado en la página en una superficie
|
|
74
|
-
* modal completa, y transformarlo de vuelta cuando se cierra.
|
|
75
|
-
*
|
|
76
|
-
* **Coreografía de movimiento:**
|
|
77
|
-
* Este componente implementa el patrón complejo de transformación de contenedor de M3. Cuando
|
|
78
|
-
* se activa, el elemento origen se expande visualmente hacia el modal. El color de fondo,
|
|
79
|
-
* el radio del borde y la tipografía del modal se desvanecen y se funden
|
|
80
|
-
* perfectamente con el elemento de origen.
|
|
81
|
-
*
|
|
82
|
-
* **Mecanismo de activación:**
|
|
83
|
-
* Los consumidores deben proporcionar la propiedad `triggerEvent` que contenga el `Event`
|
|
84
|
-
* de puntero/clic original que inició la acción de apertura. El componente extrae
|
|
85
|
-
* el `event.target` (o usa `clientX/Y` como respaldo) para determinar las coordenadas
|
|
86
|
-
* de origen exactas para la animación GSAP FLIP.
|
|
87
|
-
*
|
|
88
|
-
* **Comportamiento del diálogo:**
|
|
89
|
-
* Internamente, utiliza el elemento nativo `<dialog>`. Atrapa el foco, soporta
|
|
90
|
-
* `Escape` para cerrar, y maneja clics en el fondo/scrim (que activan la
|
|
91
|
-
* animación de morphing inverso antes de cerrar realmente el diálogo nativo).
|
|
92
|
-
*
|
|
93
|
-
* @example
|
|
94
|
-
* ```html
|
|
95
|
-
* <moni-morph-modal id="myModal" title="Detalles">
|
|
96
|
-
* <p>Este modal se transformó desde el botón que acabas de clickear.</p>
|
|
97
|
-
* </moni-morph-modal>
|
|
98
|
-
*
|
|
99
|
-
* <moni-button id="openBtn">Abrir Detalles</moni-button>
|
|
100
|
-
*
|
|
101
|
-
* <script>
|
|
102
|
-
* const modal = document.getElementById('myModal');
|
|
103
|
-
* document.getElementById('openBtn').addEventListener('click', (e) => {
|
|
104
|
-
* modal.triggerEvent = e; // Pasa el evento para que sepa desde dónde transformarse
|
|
105
|
-
* modal.open = true;
|
|
106
|
-
* });
|
|
107
|
-
* </script>
|
|
108
|
-
* ```
|
|
109
|
-
*
|
|
110
|
-
* @slot default - El contenido del cuerpo principal del modal.
|
|
111
|
-
* @slot header - Contenido de encabezado personalizado (sobrescribe el atributo `title`).
|
|
112
|
-
* @slot footer - Botones de acción mostrados en la parte inferior derecha.
|
|
113
|
-
*/
|
|
114
|
-
|
|
115
|
-
const litBool = {
|
|
116
|
-
fromAttribute: (value: string | null) => value !== 'false' && value !== null,
|
|
117
|
-
toAttribute: (value: boolean) => (value ? '' : null)
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
@customElement('moni-morph-modal')
|
|
121
|
-
export class MoniMorphModal extends MoniElement {
|
|
122
|
-
/**
|
|
123
|
-
* Selector del elemento disparador desde el cual transformarse (ej. '#fab' o '.my-button').
|
|
124
|
-
* Si se omite, el modal aún puede activarse programáticamente estableciendo `open = true`.
|
|
125
|
-
* @type {string}
|
|
126
|
-
*/
|
|
127
|
-
@property({ reflect: true }) target = '';
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Controla el estado abierto/cerrado del modal.
|
|
131
|
-
* Puede establecerse directamente, o se gestiona internamente cuando se hace clic en el elemento `target`.
|
|
132
|
-
* @type {boolean}
|
|
133
|
-
*/
|
|
134
|
-
@property({ type: Boolean, reflect: true, converter: litBool }) open = false;
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Si es true, renderiza un fondo y atrapa el foco, actuando como un verdadero diálogo modal.
|
|
138
|
-
* @type {boolean}
|
|
139
|
-
* @default true
|
|
140
|
-
*/
|
|
141
|
-
@property({ type: Boolean, reflect: true, converter: litBool }) modal = true;
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Posición preferida del modal en relación con la ventana gráfica.
|
|
145
|
-
* @type {Placement}
|
|
146
|
-
* @default 'center'
|
|
147
|
-
*/
|
|
148
|
-
@property({ reflect: true }) placement: Placement = 'center';
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* El ancho del modal cuando está completamente expandido.
|
|
152
|
-
* @type {string}
|
|
153
|
-
* @default '22rem'
|
|
154
|
-
*/
|
|
155
|
-
@property({ reflect: true, attribute: 'expanded-width' }) expandedWidth =
|
|
156
|
-
'22rem';
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* La altura del modal cuando está completamente expandido.
|
|
160
|
-
* @type {string}
|
|
161
|
-
* @default '18rem'
|
|
162
|
-
*/
|
|
163
|
-
@property({ reflect: true, attribute: 'expanded-height' }) expandedHeight =
|
|
164
|
-
'18rem';
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Si es true, hacer clic fuera del modal lo cerrará.
|
|
168
|
-
* @type {boolean}
|
|
169
|
-
* @default true
|
|
170
|
-
*/
|
|
171
|
-
@property({ type: Boolean, reflect: true, attribute: 'close-on-click-outside', converter: litBool })
|
|
172
|
-
closeOnClickOutside = true;
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Si es true, presionar la tecla Escape cerrará el modal.
|
|
176
|
-
* @type {boolean}
|
|
177
|
-
* @default true
|
|
178
|
-
*/
|
|
179
|
-
@property({ type: Boolean, reflect: true, attribute: 'close-on-esc', converter: litBool })
|
|
180
|
-
closeOnEsc = true;
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Si es true, muestra un botón de icono de cierre por defecto en el encabezado.
|
|
184
|
-
* @type {boolean}
|
|
185
|
-
*/
|
|
186
|
-
@property({ type: Boolean, reflect: true, attribute: 'show-close-button', converter: litBool })
|
|
187
|
-
showCloseButton = false;
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Si es true, intenta fundir/transformar una etiqueta del disparador hacia el encabezado del modal.
|
|
191
|
-
* @type {boolean}
|
|
192
|
-
*/
|
|
193
|
-
@property({ type: Boolean, reflect: true, attribute: 'morph-label', converter: litBool })
|
|
194
|
-
morphLabel = false;
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Un selector CSS personalizado utilizado para encontrar el elemento de etiqueta específico dentro del target a transformar.
|
|
198
|
-
* @type {string}
|
|
199
|
-
*/
|
|
200
|
-
@property({ reflect: true, attribute: 'morph-label-selector' })
|
|
201
|
-
morphLabelSelector = '';
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Si es true, renderiza un fondo atenuado detrás del modal.
|
|
205
|
-
* @type {boolean}
|
|
206
|
-
* @default true
|
|
207
|
-
*/
|
|
208
|
-
@property({ type: Boolean, reflect: true, attribute: 'has-backdrop', converter: litBool })
|
|
209
|
-
hasBackdrop = true;
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Si es true, oculta el elemento disparador (target) mientras el modal está abierto.
|
|
213
|
-
* @type {boolean}
|
|
214
|
-
*/
|
|
215
|
-
@property({ type: Boolean, reflect: true, attribute: 'hide-target', converter: litBool })
|
|
216
|
-
hideTarget = false;
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Si es true, fuerza al modal a cubrir visualmente la ubicación original del target antes de expandirse.
|
|
220
|
-
* @type {boolean}
|
|
221
|
-
*/
|
|
222
|
-
@property({ type: Boolean, reflect: true, attribute: 'cover-target', converter: litBool })
|
|
223
|
-
coverTarget = false;
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Si es true, el modal calcula automáticamente su tamaño final basándose en el contenido interno (ignora expandedWidth y expandedHeight).
|
|
227
|
-
* @type {boolean}
|
|
228
|
-
*/
|
|
1
|
+
/**
|
|
2
|
+
* @file components/moni-morph-modal.ts
|
|
3
|
+
* @package @moni-labs/moni-ui
|
|
4
|
+
* @license MIT
|
|
5
|
+
* @contributors Moni Labs & Contributors
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { html, css } from 'lit';
|
|
9
|
+
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
10
|
+
import { MoniElement, sharedStyles } from './_base/index.js';
|
|
11
|
+
import { gsap } from 'gsap';
|
|
12
|
+
import { Flip } from 'gsap/Flip';
|
|
13
|
+
import { CustomEase } from 'gsap/CustomEase';
|
|
14
|
+
import './moni-icon.js';
|
|
15
|
+
|
|
16
|
+
gsap.registerPlugin(Flip, CustomEase);
|
|
17
|
+
|
|
18
|
+
CustomEase.create('morph-open', '0.15, 0.85, 0.2, 1');
|
|
19
|
+
CustomEase.create('morph-close', '0.4, 0, 0.2, 1');
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
type Placement =
|
|
23
|
+
| 'center'
|
|
24
|
+
| 'top'
|
|
25
|
+
| 'top-start'
|
|
26
|
+
| 'top-end'
|
|
27
|
+
| 'bottom'
|
|
28
|
+
| 'bottom-start'
|
|
29
|
+
| 'bottom-end'
|
|
30
|
+
| 'left'
|
|
31
|
+
| 'left-start'
|
|
32
|
+
| 'left-end'
|
|
33
|
+
| 'right'
|
|
34
|
+
| 'right-start'
|
|
35
|
+
| 'right-end'
|
|
36
|
+
| 'viewport-center'
|
|
37
|
+
| 'viewport-top'
|
|
38
|
+
| 'viewport-bottom'
|
|
39
|
+
| 'viewport-left'
|
|
40
|
+
| 'viewport-right'
|
|
41
|
+
| 'viewport-top-left'
|
|
42
|
+
| 'viewport-top-right'
|
|
43
|
+
| 'viewport-bottom-left'
|
|
44
|
+
| 'viewport-bottom-right';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Estado interno de coordenadas y estilos del objetivo de morphing antes de la animación.
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
interface TargetState {
|
|
52
|
+
rect: DOMRect;
|
|
53
|
+
backgroundColor: string;
|
|
54
|
+
color: string;
|
|
55
|
+
borderRadius: string;
|
|
56
|
+
boxShadow: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Referencias a los elementos internos de icono y texto animados durante el morphing.
|
|
61
|
+
*
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
interface MorphElements {
|
|
65
|
+
text: HTMLElement | null;
|
|
66
|
+
icon: HTMLElement | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Componente Material Design 3 Morph Modal.
|
|
71
|
+
*
|
|
72
|
+
* Un diálogo altamente interactivo que usa animaciones GSAP FLIP (First, Last, Invert, Play)
|
|
73
|
+
* para "transformar" (morph) sin problemas cualquier elemento clickeado en la página en una superficie
|
|
74
|
+
* modal completa, y transformarlo de vuelta cuando se cierra.
|
|
75
|
+
*
|
|
76
|
+
* **Coreografía de movimiento:**
|
|
77
|
+
* Este componente implementa el patrón complejo de transformación de contenedor de M3. Cuando
|
|
78
|
+
* se activa, el elemento origen se expande visualmente hacia el modal. El color de fondo,
|
|
79
|
+
* el radio del borde y la tipografía del modal se desvanecen y se funden
|
|
80
|
+
* perfectamente con el elemento de origen.
|
|
81
|
+
*
|
|
82
|
+
* **Mecanismo de activación:**
|
|
83
|
+
* Los consumidores deben proporcionar la propiedad `triggerEvent` que contenga el `Event`
|
|
84
|
+
* de puntero/clic original que inició la acción de apertura. El componente extrae
|
|
85
|
+
* el `event.target` (o usa `clientX/Y` como respaldo) para determinar las coordenadas
|
|
86
|
+
* de origen exactas para la animación GSAP FLIP.
|
|
87
|
+
*
|
|
88
|
+
* **Comportamiento del diálogo:**
|
|
89
|
+
* Internamente, utiliza el elemento nativo `<dialog>`. Atrapa el foco, soporta
|
|
90
|
+
* `Escape` para cerrar, y maneja clics en el fondo/scrim (que activan la
|
|
91
|
+
* animación de morphing inverso antes de cerrar realmente el diálogo nativo).
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```html
|
|
95
|
+
* <moni-morph-modal id="myModal" title="Detalles">
|
|
96
|
+
* <p>Este modal se transformó desde el botón que acabas de clickear.</p>
|
|
97
|
+
* </moni-morph-modal>
|
|
98
|
+
*
|
|
99
|
+
* <moni-button id="openBtn">Abrir Detalles</moni-button>
|
|
100
|
+
*
|
|
101
|
+
* <script>
|
|
102
|
+
* const modal = document.getElementById('myModal');
|
|
103
|
+
* document.getElementById('openBtn').addEventListener('click', (e) => {
|
|
104
|
+
* modal.triggerEvent = e; // Pasa el evento para que sepa desde dónde transformarse
|
|
105
|
+
* modal.open = true;
|
|
106
|
+
* });
|
|
107
|
+
* </script>
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @slot default - El contenido del cuerpo principal del modal.
|
|
111
|
+
* @slot header - Contenido de encabezado personalizado (sobrescribe el atributo `title`).
|
|
112
|
+
* @slot footer - Botones de acción mostrados en la parte inferior derecha.
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
const litBool = {
|
|
116
|
+
fromAttribute: (value: string | null) => value !== 'false' && value !== null,
|
|
117
|
+
toAttribute: (value: boolean) => (value ? '' : null)
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
@customElement('moni-morph-modal')
|
|
121
|
+
export class MoniMorphModal extends MoniElement {
|
|
122
|
+
/**
|
|
123
|
+
* Selector del elemento disparador desde el cual transformarse (ej. '#fab' o '.my-button').
|
|
124
|
+
* Si se omite, el modal aún puede activarse programáticamente estableciendo `open = true`.
|
|
125
|
+
* @type {string}
|
|
126
|
+
*/
|
|
127
|
+
@property({ reflect: true }) target = '';
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Controla el estado abierto/cerrado del modal.
|
|
131
|
+
* Puede establecerse directamente, o se gestiona internamente cuando se hace clic en el elemento `target`.
|
|
132
|
+
* @type {boolean}
|
|
133
|
+
*/
|
|
134
|
+
@property({ type: Boolean, reflect: true, converter: litBool }) open = false;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Si es true, renderiza un fondo y atrapa el foco, actuando como un verdadero diálogo modal.
|
|
138
|
+
* @type {boolean}
|
|
139
|
+
* @default true
|
|
140
|
+
*/
|
|
141
|
+
@property({ type: Boolean, reflect: true, converter: litBool }) modal = true;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Posición preferida del modal en relación con la ventana gráfica.
|
|
145
|
+
* @type {Placement}
|
|
146
|
+
* @default 'center'
|
|
147
|
+
*/
|
|
148
|
+
@property({ reflect: true }) placement: Placement = 'center';
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* El ancho del modal cuando está completamente expandido.
|
|
152
|
+
* @type {string}
|
|
153
|
+
* @default '22rem'
|
|
154
|
+
*/
|
|
155
|
+
@property({ reflect: true, attribute: 'expanded-width' }) expandedWidth =
|
|
156
|
+
'22rem';
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* La altura del modal cuando está completamente expandido.
|
|
160
|
+
* @type {string}
|
|
161
|
+
* @default '18rem'
|
|
162
|
+
*/
|
|
163
|
+
@property({ reflect: true, attribute: 'expanded-height' }) expandedHeight =
|
|
164
|
+
'18rem';
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Si es true, hacer clic fuera del modal lo cerrará.
|
|
168
|
+
* @type {boolean}
|
|
169
|
+
* @default true
|
|
170
|
+
*/
|
|
171
|
+
@property({ type: Boolean, reflect: true, attribute: 'close-on-click-outside', converter: litBool })
|
|
172
|
+
closeOnClickOutside = true;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Si es true, presionar la tecla Escape cerrará el modal.
|
|
176
|
+
* @type {boolean}
|
|
177
|
+
* @default true
|
|
178
|
+
*/
|
|
179
|
+
@property({ type: Boolean, reflect: true, attribute: 'close-on-esc', converter: litBool })
|
|
180
|
+
closeOnEsc = true;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Si es true, muestra un botón de icono de cierre por defecto en el encabezado.
|
|
184
|
+
* @type {boolean}
|
|
185
|
+
*/
|
|
186
|
+
@property({ type: Boolean, reflect: true, attribute: 'show-close-button', converter: litBool })
|
|
187
|
+
showCloseButton = false;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Si es true, intenta fundir/transformar una etiqueta del disparador hacia el encabezado del modal.
|
|
191
|
+
* @type {boolean}
|
|
192
|
+
*/
|
|
193
|
+
@property({ type: Boolean, reflect: true, attribute: 'morph-label', converter: litBool })
|
|
194
|
+
morphLabel = false;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Un selector CSS personalizado utilizado para encontrar el elemento de etiqueta específico dentro del target a transformar.
|
|
198
|
+
* @type {string}
|
|
199
|
+
*/
|
|
200
|
+
@property({ reflect: true, attribute: 'morph-label-selector' })
|
|
201
|
+
morphLabelSelector = '';
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Si es true, renderiza un fondo atenuado detrás del modal.
|
|
205
|
+
* @type {boolean}
|
|
206
|
+
* @default true
|
|
207
|
+
*/
|
|
208
|
+
@property({ type: Boolean, reflect: true, attribute: 'has-backdrop', converter: litBool })
|
|
209
|
+
hasBackdrop = true;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Si es true, oculta el elemento disparador (target) mientras el modal está abierto.
|
|
213
|
+
* @type {boolean}
|
|
214
|
+
*/
|
|
215
|
+
@property({ type: Boolean, reflect: true, attribute: 'hide-target', converter: litBool })
|
|
216
|
+
hideTarget = false;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Si es true, fuerza al modal a cubrir visualmente la ubicación original del target antes de expandirse.
|
|
220
|
+
* @type {boolean}
|
|
221
|
+
*/
|
|
222
|
+
@property({ type: Boolean, reflect: true, attribute: 'cover-target', converter: litBool })
|
|
223
|
+
coverTarget = false;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Si es true, el modal calcula automáticamente su tamaño final basándose en el contenido interno (ignora expandedWidth y expandedHeight).
|
|
227
|
+
* @type {boolean}
|
|
228
|
+
*/
|
|
229
229
|
@property({ type: Boolean, reflect: true, attribute: 'auto-size', converter: litBool })
|
|
230
230
|
autoSize = false;
|
|
231
231
|
|
|
232
|
+
/** Ajusta sólo la altura al contenido, sin superar expanded-height. */
|
|
233
|
+
@property({ type: Boolean, reflect: true, attribute: 'auto-height', converter: litBool })
|
|
234
|
+
autoHeight = false;
|
|
235
|
+
|
|
232
236
|
/** Fuerza al modal expandido a ocupar todo el viewport. */
|
|
233
237
|
@property({ type: Boolean, reflect: true, converter: litBool })
|
|
234
238
|
fullscreen = false;
|
|
@@ -240,1674 +244,1734 @@ export class MoniMorphModal extends MoniElement {
|
|
|
240
244
|
/** Ancho máximo del viewport, en píxeles, para aplicar responsive-fullscreen. */
|
|
241
245
|
@property({ type: Number, reflect: true, attribute: 'fullscreen-breakpoint' })
|
|
242
246
|
fullscreenBreakpoint = 600;
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Si es true, aplica un efecto de desenfoque (blur) al contenido durante la animación de entrada y salida.
|
|
246
|
-
* @type {boolean}
|
|
247
|
-
*/
|
|
248
|
-
@property({ type: Boolean, reflect: true, attribute: 'blur-content', converter: litBool })
|
|
249
|
-
blurContent = false;
|
|
250
|
-
|
|
251
|
-
/** Muestra en consola el origen, geometría y estrategia usada por cada morph. */
|
|
252
|
-
@property({ type: Boolean, reflect: true, converter: litBool })
|
|
253
|
-
debug = false;
|
|
254
|
-
|
|
255
|
-
@query('.panel') private _panel!: HTMLDivElement;
|
|
256
|
-
@query('.panel-inner') private _inner!: HTMLDivElement;
|
|
257
|
-
@query('.backdrop') private _backdrop!: HTMLDivElement;
|
|
258
|
-
@query('header') private _header!: HTMLElement;
|
|
259
|
-
|
|
260
|
-
@state() private _hasHeader = false;
|
|
261
|
-
@state() private _hasFooter = false;
|
|
262
|
-
@state() private _hasTriggerLabel = false;
|
|
263
|
-
|
|
264
|
-
private _targetEl: HTMLElement | null = null;
|
|
265
|
-
private _isAnimating = false;
|
|
266
|
-
private _visible = false;
|
|
267
|
-
private _handlingOpenChange = false;
|
|
268
|
-
private _zIndex = 100;
|
|
269
|
-
private _morphEls: MorphElements = { text: null, icon: null };
|
|
270
|
-
private _targetSnapshot: HTMLElement | null = null;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Si es true, aplica un efecto de desenfoque (blur) al contenido durante la animación de entrada y salida.
|
|
250
|
+
* @type {boolean}
|
|
251
|
+
*/
|
|
252
|
+
@property({ type: Boolean, reflect: true, attribute: 'blur-content', converter: litBool })
|
|
253
|
+
blurContent = false;
|
|
254
|
+
|
|
255
|
+
/** Muestra en consola el origen, geometría y estrategia usada por cada morph. */
|
|
256
|
+
@property({ type: Boolean, reflect: true, converter: litBool })
|
|
257
|
+
debug = false;
|
|
258
|
+
|
|
259
|
+
@query('.panel') private _panel!: HTMLDivElement;
|
|
260
|
+
@query('.panel-inner') private _inner!: HTMLDivElement;
|
|
261
|
+
@query('.backdrop') private _backdrop!: HTMLDivElement;
|
|
262
|
+
@query('header') private _header!: HTMLElement;
|
|
263
|
+
|
|
264
|
+
@state() private _hasHeader = false;
|
|
265
|
+
@state() private _hasFooter = false;
|
|
266
|
+
@state() private _hasTriggerLabel = false;
|
|
267
|
+
|
|
268
|
+
private _targetEl: HTMLElement | null = null;
|
|
269
|
+
private _isAnimating = false;
|
|
270
|
+
private _visible = false;
|
|
271
|
+
private _handlingOpenChange = false;
|
|
272
|
+
private _zIndex = 100;
|
|
273
|
+
private _morphEls: MorphElements = { text: null, icon: null };
|
|
274
|
+
private _targetSnapshot: HTMLElement | null = null;
|
|
271
275
|
private _originFallbacks: HTMLElement[] = [];
|
|
272
276
|
private static _openStack: MoniMorphModal[] = [];
|
|
273
277
|
private _targetStylesCache = new Map<HTMLElement, { opacity: string; color: string }>();
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
*
|
|
288
|
-
*/
|
|
289
|
-
private
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
this.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
document.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
this.
|
|
313
|
-
|
|
278
|
+
private static _scrollLockOwners = new Set<MoniMorphModal>();
|
|
279
|
+
private static _scrollLockSnapshot: {
|
|
280
|
+
documentOverflow: string;
|
|
281
|
+
bodyOverflow: string;
|
|
282
|
+
bodyOverscrollBehavior: string;
|
|
283
|
+
} | null = null;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Handler encapsulado para el evento click en el trigger.
|
|
287
|
+
* Dispara la animación de apertura.
|
|
288
|
+
*/
|
|
289
|
+
private _onTargetClick = () => this.show();
|
|
290
|
+
/**
|
|
291
|
+
* Handler encapsulado para interceptar teclas a nivel global (ej: Escape).
|
|
292
|
+
*/
|
|
293
|
+
private _onDocKeydown = (e: KeyboardEvent) => this._handleDocKeydown(e);
|
|
294
|
+
/**
|
|
295
|
+
* Handler de clics globales para detectar interacciones fuera del modal (Click outside).
|
|
296
|
+
* Sólo cierra el modal si `closeOnClickOutside` es true y el usuario no hizo clic
|
|
297
|
+
* dentro de las dimensiones del panel animado ni del botón original.
|
|
298
|
+
*/
|
|
299
|
+
private _onDocClick = (e: MouseEvent) => {
|
|
300
|
+
if (!this.open || !this._visible || !this.closeOnClickOutside) return;
|
|
301
|
+
if (e.composedPath().includes(this._panel)) return;
|
|
302
|
+
if (this._targetEl && e.composedPath().includes(this._targetEl)) return;
|
|
303
|
+
this.hide();
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
override connectedCallback(): void {
|
|
307
|
+
super.connectedCallback();
|
|
308
|
+
this._resolveTarget();
|
|
309
|
+
document.addEventListener('keydown', this._onDocKeydown);
|
|
310
|
+
document.addEventListener('click', this._onDocClick);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
override disconnectedCallback(): void {
|
|
314
|
+
document.removeEventListener('keydown', this._onDocKeydown);
|
|
315
|
+
document.removeEventListener('click', this._onDocClick);
|
|
316
|
+
if (this._targetEl) {
|
|
317
|
+
this._targetEl.removeEventListener('click', this._onTargetClick);
|
|
318
|
+
}
|
|
319
|
+
const idx = MoniMorphModal._openStack.indexOf(this);
|
|
320
|
+
if (idx > -1) MoniMorphModal._openStack.splice(idx, 1);
|
|
321
|
+
this.style.removeProperty('z-index');
|
|
322
|
+
this.style.removeProperty('--_z-index');
|
|
323
|
+
this.style.removeProperty('--_backdrop-duration');
|
|
314
324
|
this.style.removeProperty('--_backdrop-ease');
|
|
325
|
+
this._releaseDocumentScrollLock();
|
|
315
326
|
this._cleanupMorphElements();
|
|
316
|
-
this._restoreTargetContent();
|
|
317
|
-
super.disconnectedCallback();
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Hook del ciclo de vida reactivo (Lit).
|
|
322
|
-
* Observa cambios asíncronos en el atributo `target` para re-bindear el disparador,
|
|
323
|
-
* y reacciona a cambios programáticos de la propiedad `open` para ejecutar
|
|
324
|
-
* imperativamente la animación de apertura (`show`) o cierre (`hide`).
|
|
325
|
-
*/
|
|
326
|
-
override updated(changed: Map<string, unknown>) {
|
|
327
|
-
super.updated(changed);
|
|
328
|
-
if (changed.has('target')) {
|
|
329
|
-
this._resolveTarget();
|
|
330
|
-
}
|
|
327
|
+
this._restoreTargetContent();
|
|
328
|
+
super.disconnectedCallback();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Hook del ciclo de vida reactivo (Lit).
|
|
333
|
+
* Observa cambios asíncronos en el atributo `target` para re-bindear el disparador,
|
|
334
|
+
* y reacciona a cambios programáticos de la propiedad `open` para ejecutar
|
|
335
|
+
* imperativamente la animación de apertura (`show`) o cierre (`hide`).
|
|
336
|
+
*/
|
|
337
|
+
override updated(changed: Map<string, unknown>) {
|
|
338
|
+
super.updated(changed);
|
|
339
|
+
if (changed.has('target')) {
|
|
340
|
+
this._resolveTarget();
|
|
341
|
+
}
|
|
331
342
|
if (changed.has('open') && !this._handlingOpenChange) {
|
|
332
343
|
if (this.open) this.show();
|
|
333
344
|
else this.hide();
|
|
334
345
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Busca en el DOM el elemento que dispara el modal (el trigger) basándose en la propiedad `target`.
|
|
339
|
-
* Establece el listener de click que iniciará la animación de morphing.
|
|
340
|
-
*/
|
|
341
|
-
private _resolveTarget(): void {
|
|
342
|
-
if (!this.target) return;
|
|
343
|
-
|
|
344
|
-
const el = document.querySelector<HTMLElement>(this.target);
|
|
345
|
-
if (!el) {
|
|
346
|
-
// Es normal que no se encuentre el target si está condicionado por un #if en Svelte
|
|
347
|
-
// Se intentará resolver nuevamente al llamar a show()
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Si el target cambió dinámicamente, limpiamos el listener anterior para evitar memory leaks
|
|
352
|
-
if (this._targetEl && this._targetEl !== el) {
|
|
353
|
-
this._targetEl.removeEventListener('click', this._onTargetClick);
|
|
346
|
+
if ((changed.has('hasBackdrop') || changed.has('fullscreen') || changed.has('responsiveFullscreen') || changed.has('fullscreenBreakpoint')) && (this.open || this._visible)) {
|
|
347
|
+
this._syncDocumentScrollLock();
|
|
354
348
|
}
|
|
355
|
-
this._targetEl = el;
|
|
356
|
-
this._targetEl.addEventListener('click', this._onTargetClick);
|
|
357
349
|
}
|
|
358
350
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
* 1. Oculta visualmente el target original.
|
|
362
|
-
* 2. Posiciona el modal colapsado exactamente sobre el target original (mismas dimensiones, color, y radios).
|
|
363
|
-
* 3. Expande fluidamente el modal a sus dimensiones finales mediante transformaciones CSS.
|
|
364
|
-
*/
|
|
365
|
-
show(): void {
|
|
366
|
-
// Prevenimos ejecuciones concurrentes si ya se está animando o está visible
|
|
367
|
-
if (this._isAnimating || this._visible) return;
|
|
368
|
-
|
|
369
|
-
if (!this._targetEl) this._resolveTarget();
|
|
370
|
-
if (!this._targetEl) {
|
|
371
|
-
console.warn('[moni-morph-modal] cannot show: target not found');
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
this._captureOriginFallbacks(this._targetEl);
|
|
375
|
-
|
|
376
|
-
this._isAnimating = true;
|
|
377
|
-
|
|
378
|
-
// Flag interno para evitar loops infinitos en el hook `updated()`
|
|
379
|
-
this._handlingOpenChange = true;
|
|
380
|
-
this.open = true;
|
|
381
|
-
this._handlingOpenChange = false;
|
|
382
|
-
|
|
383
|
-
// Apilamos el modal globalmente para manejar correctamente el z-index en modales anidados
|
|
384
|
-
MoniMorphModal._openStack.push(this);
|
|
385
|
-
this._zIndex = 1000 + MoniMorphModal._openStack.length;
|
|
386
|
-
|
|
387
|
-
// Aplicamos variables CSS inline para gestionar las transiciones del backdrop y el modal
|
|
388
|
-
this.style.zIndex = String(this._zIndex);
|
|
389
|
-
this.style.setProperty('--_z-index', String(this._zIndex));
|
|
390
|
-
this.style.setProperty('--_backdrop-duration', '0.38s');
|
|
391
|
-
this.style.setProperty('--_backdrop-ease', 'cubic-bezier(0.15, 0.85, 0.2, 1)');
|
|
392
|
-
if (this._backdrop) gsap.set(this._backdrop, { clearProps: 'opacity,transition' });
|
|
393
|
-
|
|
394
|
-
// Fase "First": Ocultamos o cubrimos el trigger original según la configuración del desarrollador
|
|
395
|
-
this._hideTargetContent();
|
|
396
|
-
|
|
397
|
-
void this.updateComplete.then(async () => {
|
|
398
|
-
if (!this._targetEl || !this._panel || !this._inner) {
|
|
399
|
-
this._isAnimating = false;
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Añadimos clase para prevenir interacciones durante la animación
|
|
404
|
-
this._panel.classList.add('is-animating');
|
|
405
|
-
|
|
406
|
-
// Esperamos un frame para asegurar que el navegador aplicó el `display: block`
|
|
407
|
-
// necesario antes de poder leer las dimensiones.
|
|
408
|
-
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
409
|
-
|
|
410
|
-
// 1. "First": Calculamos el estado visual actual del trigger
|
|
411
|
-
const targetState = this._getTargetState(this._targetEl);
|
|
412
|
-
this._debugMorph('open', this._targetEl, 'target', [this._targetEl]);
|
|
413
|
-
const panel = this._panel;
|
|
414
|
-
|
|
415
|
-
// 2. "Invert": Posicionamos el modal como un clon visual exacto del trigger.
|
|
416
|
-
// Desactivamos la sombra y forzamos el color/borde para coincidir con el botón/FAB.
|
|
417
|
-
this._applyRect(panel, this._toPanelCoordinateRect(targetState.rect));
|
|
418
|
-
panel.style.backgroundColor = targetState.backgroundColor;
|
|
419
|
-
panel.style.color = targetState.color;
|
|
420
|
-
panel.style.borderRadius = targetState.borderRadius;
|
|
421
|
-
panel.style.boxShadow = targetState.boxShadow;
|
|
422
|
-
panel.style.zIndex = String(this._zIndex);
|
|
423
|
-
panel.style.visibility = 'visible';
|
|
424
|
-
panel.style.opacity = '1';
|
|
425
|
-
panel.style.pointerEvents = 'auto';
|
|
426
|
-
|
|
427
|
-
if (this._backdrop) {
|
|
428
|
-
this._backdrop.style.zIndex = String(this._zIndex - 1);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Limpiamos elementos fantasma previos si el usuario interactuó muy rápido
|
|
432
|
-
this._cleanupMorphElements();
|
|
433
|
-
|
|
434
|
-
// Respetamos la preferencia de sistema de animaciones reducidas por accesibilidad (a11y)
|
|
435
|
-
const prefersReducedMotion = window.matchMedia(
|
|
436
|
-
'(prefers-reduced-motion: reduce)'
|
|
437
|
-
).matches;
|
|
438
|
-
|
|
439
|
-
// 3. Verificamos si debemos orquestar la animación compleja del label flotante cruzado
|
|
440
|
-
let shouldMorph = this._shouldMorphLabel() && !prefersReducedMotion;
|
|
441
|
-
const morphTargets: HTMLElement[] = [];
|
|
442
|
-
let source: any = null;
|
|
443
|
-
const headerWrapper = this.shadowRoot!.querySelector('.header-morph-wrapper') as HTMLElement;
|
|
444
|
-
|
|
445
|
-
if (shouldMorph) {
|
|
446
|
-
// Preparamos la estructura del header para que el texto real entre con un fade in
|
|
447
|
-
this._animateLabelOpen();
|
|
448
|
-
|
|
449
|
-
// Buscamos el ícono y el texto dentro del botón de origen (trigger)
|
|
450
|
-
source = this._resolveMorphSource();
|
|
451
|
-
|
|
452
|
-
// Si existe un nodo de texto, creamos un elemento flotante (fantasma) en el modal
|
|
453
|
-
if (source.text) {
|
|
454
|
-
const el = this._createMorphText(source.text.content, source.text.sourceEl);
|
|
455
|
-
this._positionElement(el, source.text.rect);
|
|
456
|
-
morphTargets.push(el);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Hacemos lo mismo si el botón original contenía un icono
|
|
460
|
-
if (source.icon) {
|
|
461
|
-
const el = this._createMorphIcon(source.icon.element);
|
|
462
|
-
this._positionElement(el, source.icon.rect);
|
|
463
|
-
morphTargets.push(el);
|
|
464
|
-
}
|
|
465
|
-
if (morphTargets.length === 0) shouldMorph = false;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (shouldMorph && headerWrapper) {
|
|
469
|
-
// Ocultamos temporalmente el contenido final del header mientras animamos los elementos flotantes
|
|
470
|
-
headerWrapper.style.opacity = '0';
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Capturamos el estado "First" del panel (tamaño pequeño de botón) y de los elementos flotantes
|
|
474
|
-
const state = Flip.getState(panel, {
|
|
475
|
-
props: 'backgroundColor,borderRadius,color,boxShadow'
|
|
476
|
-
});
|
|
477
|
-
const morphState = Flip.getState(morphTargets, {
|
|
478
|
-
props: 'color,fontSize,fontWeight,lineHeight'
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
// Fase "Last": Calculamos las dimensiones expandidas del modal basándonos en la configuración
|
|
482
|
-
let naturalSize: { width: number; height: number } | undefined;
|
|
483
|
-
if (this.autoSize) {
|
|
484
|
-
const prevW = panel.style.getPropertyValue('--_panel-width');
|
|
485
|
-
const prevH = panel.style.getPropertyValue('--_panel-height');
|
|
486
|
-
const prevPanelW = panel.style.width;
|
|
487
|
-
const prevPanelH = panel.style.height;
|
|
488
|
-
|
|
489
|
-
panel.style.width = 'auto';
|
|
490
|
-
panel.style.height = 'auto';
|
|
491
|
-
panel.style.setProperty('--_panel-width', 'max-content');
|
|
492
|
-
panel.style.setProperty('--_panel-height', 'max-content');
|
|
493
|
-
|
|
494
|
-
const bodyEl = this.shadowRoot!.querySelector('.body') as HTMLElement;
|
|
495
|
-
const prevOverflow = bodyEl ? bodyEl.style.overflow : '';
|
|
496
|
-
if (bodyEl) bodyEl.style.overflow = 'visible';
|
|
497
|
-
|
|
498
|
-
// Forzamos al panel-inner a expandirse para medir su contenido real
|
|
499
|
-
naturalSize = { width: this._inner.scrollWidth, height: this._inner.scrollHeight };
|
|
500
|
-
|
|
501
|
-
panel.style.setProperty('--_panel-width', prevW);
|
|
502
|
-
panel.style.setProperty('--_panel-height', prevH);
|
|
503
|
-
panel.style.width = prevPanelW;
|
|
504
|
-
panel.style.height = prevPanelH;
|
|
505
|
-
|
|
506
|
-
if (bodyEl) bodyEl.style.overflow = prevOverflow;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const finalRect = this._computeFinalRect(targetState.rect, naturalSize);
|
|
510
|
-
panel.style.setProperty('--_panel-width', `${finalRect.width}px`);
|
|
511
|
-
panel.style.setProperty('--_panel-height', `${finalRect.height}px`);
|
|
512
|
-
this._applyRect(panel, finalRect);
|
|
513
|
-
|
|
514
|
-
// Removemos las forzadas de estilo del trigger para que herede las propias del modal
|
|
515
|
-
panel.style.backgroundColor = '';
|
|
516
|
-
panel.style.color = '';
|
|
517
|
-
panel.style.borderRadius = '';
|
|
518
|
-
panel.style.boxShadow = '';
|
|
519
|
-
|
|
520
|
-
if (shouldMorph && source) {
|
|
521
|
-
// Medimos dónde van a terminar el icono y el texto dentro del modal ya expandido
|
|
522
|
-
const textRect = this._measureHeaderTextRect();
|
|
523
|
-
const iconRect = this._measureHeaderIconRect();
|
|
524
|
-
const headerComputed = window.getComputedStyle(this._header);
|
|
525
|
-
|
|
526
|
-
if (this._morphEls.text && textRect) {
|
|
527
|
-
// Movemos el texto flotante a su destino y le pedimos que adopte los estilos del header
|
|
528
|
-
this._positionElement(this._morphEls.text, textRect);
|
|
529
|
-
this._morphEls.text.style.color = headerComputed.color || 'var(--on-surface)';
|
|
530
|
-
this._morphEls.text.style.fontSize = headerComputed.fontSize || 'inherit';
|
|
531
|
-
this._morphEls.text.style.fontWeight = headerComputed.fontWeight || 'inherit';
|
|
532
|
-
this._morphEls.text.style.lineHeight = headerComputed.lineHeight || 'inherit';
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (this._morphEls.icon && iconRect) {
|
|
536
|
-
// Hacemos lo mismo con el icono
|
|
537
|
-
this._positionElement(this._morphEls.icon, iconRect);
|
|
538
|
-
this._morphEls.icon.style.color = headerComputed.color || 'var(--on-surface)';
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
Flip.from(state, {
|
|
543
|
-
targets: panel,
|
|
544
|
-
duration: 0.38,
|
|
545
|
-
ease: 'morph-open',
|
|
546
|
-
zIndex: this._zIndex,
|
|
547
|
-
onComplete: () => {
|
|
548
|
-
this._isAnimating = false;
|
|
549
|
-
this._visible = true;
|
|
550
|
-
this._cleanupMorphElements();
|
|
551
|
-
if (headerWrapper) headerWrapper.style.opacity = '1';
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
if (morphTargets.length > 0) {
|
|
556
|
-
if (this.blurContent) {
|
|
557
|
-
gsap.set(morphTargets, { filter: 'blur(16px)' });
|
|
558
|
-
}
|
|
559
|
-
const flipConfig: any = {
|
|
560
|
-
targets: morphTargets,
|
|
561
|
-
duration: 0.38,
|
|
562
|
-
ease: 'morph-open',
|
|
563
|
-
zIndex: this._zIndex + 10
|
|
564
|
-
};
|
|
565
|
-
if (this.blurContent) {
|
|
566
|
-
flipConfig.filter = 'blur(0px)';
|
|
567
|
-
flipConfig.clearProps = 'filter';
|
|
568
|
-
}
|
|
569
|
-
Flip.from(morphState, flipConfig);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
const body = this.shadowRoot!.querySelector('.body');
|
|
573
|
-
const footer = this.shadowRoot!.querySelector('footer');
|
|
574
|
-
const closeBtn = this.shadowRoot!.querySelector('.close-btn');
|
|
575
|
-
const fadeTargets = [body, footer, closeBtn].filter(Boolean) as HTMLElement[];
|
|
576
|
-
|
|
577
|
-
const initialBlur = this.blurContent ? 'blur(16px)' : 'blur(0px)';
|
|
578
|
-
gsap.set(fadeTargets, { opacity: 0, y: 8, filter: initialBlur });
|
|
579
|
-
gsap.fromTo(
|
|
580
|
-
fadeTargets,
|
|
581
|
-
{ opacity: 0, y: 8, filter: initialBlur },
|
|
582
|
-
{
|
|
583
|
-
opacity: 1,
|
|
584
|
-
y: 0,
|
|
585
|
-
filter: 'blur(0px)',
|
|
586
|
-
duration: 0.32,
|
|
587
|
-
delay: 0.08,
|
|
588
|
-
stagger: 0.04,
|
|
589
|
-
ease: 'power2.out',
|
|
590
|
-
onComplete: () => {
|
|
591
|
-
gsap.set(fadeTargets, { clearProps: 'opacity,transform,filter' });
|
|
592
|
-
this._panel.classList.remove('is-animating');
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
);
|
|
596
|
-
});
|
|
351
|
+
private _shouldLockDocumentScroll(): boolean {
|
|
352
|
+
return this.hasBackdrop || this._usesFullscreen();
|
|
597
353
|
}
|
|
598
354
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
*/
|
|
603
|
-
showFrom(target: HTMLElement): void {
|
|
604
|
-
if (!target) return;
|
|
605
|
-
this._targetEl = target;
|
|
606
|
-
this.show();
|
|
355
|
+
private _syncDocumentScrollLock(): void {
|
|
356
|
+
if (this._shouldLockDocumentScroll()) this._acquireDocumentScrollLock();
|
|
357
|
+
else this._releaseDocumentScrollLock();
|
|
607
358
|
}
|
|
608
359
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
let ancestorRect: DOMRect | undefined;
|
|
621
|
-
if (transformedAncestor && transform !== 'none') {
|
|
622
|
-
const matrix = new DOMMatrix(transform);
|
|
623
|
-
scaleX = Math.hypot(matrix.a, matrix.b) || 1;
|
|
624
|
-
scaleY = Math.hypot(matrix.c, matrix.d) || 1;
|
|
625
|
-
ancestorRect = transformedAncestor.getBoundingClientRect();
|
|
626
|
-
}
|
|
627
|
-
if (anchor === 'top-left') {
|
|
628
|
-
const current = this._panel.getBoundingClientRect();
|
|
629
|
-
let left = current.left;
|
|
630
|
-
let top = current.top;
|
|
631
|
-
if (ancestorRect) {
|
|
632
|
-
left = (current.left - ancestorRect.left) / scaleX;
|
|
633
|
-
top = (current.top - ancestorRect.top) / scaleY;
|
|
634
|
-
}
|
|
635
|
-
finalRect = new DOMRect(left, top, finalRect.width, finalRect.height);
|
|
636
|
-
} else if (anchor === 'center' && ancestorRect) {
|
|
637
|
-
const visualLeft = window.innerWidth / 2 - (finalRect.width * scaleX) / 2;
|
|
638
|
-
const visualTop = window.innerHeight / 2 - (finalRect.height * scaleY) / 2;
|
|
639
|
-
finalRect = new DOMRect(
|
|
640
|
-
(visualLeft - ancestorRect.left) / scaleX,
|
|
641
|
-
(visualTop - ancestorRect.top) / scaleY,
|
|
642
|
-
finalRect.width,
|
|
643
|
-
finalRect.height
|
|
644
|
-
);
|
|
360
|
+
private _acquireDocumentScrollLock(): void {
|
|
361
|
+
if (MoniMorphModal._scrollLockOwners.has(this)) return;
|
|
362
|
+
if (MoniMorphModal._scrollLockOwners.size === 0) {
|
|
363
|
+
MoniMorphModal._scrollLockSnapshot = {
|
|
364
|
+
documentOverflow: document.documentElement.style.overflow,
|
|
365
|
+
bodyOverflow: document.body.style.overflow,
|
|
366
|
+
bodyOverscrollBehavior: document.body.style.overscrollBehavior
|
|
367
|
+
};
|
|
368
|
+
document.documentElement.style.overflow = 'hidden';
|
|
369
|
+
document.body.style.overflow = 'hidden';
|
|
370
|
+
document.body.style.overscrollBehavior = 'none';
|
|
645
371
|
}
|
|
646
|
-
|
|
647
|
-
gsap.to(this._panel, {
|
|
648
|
-
top: finalRect.top,
|
|
649
|
-
left: finalRect.left,
|
|
650
|
-
width: finalRect.width,
|
|
651
|
-
height: finalRect.height,
|
|
652
|
-
'--_panel-width': `${finalRect.width}px`,
|
|
653
|
-
'--_panel-height': `${finalRect.height}px`,
|
|
654
|
-
duration: 0.32,
|
|
655
|
-
ease: 'morph-open',
|
|
656
|
-
overwrite: true,
|
|
657
|
-
onComplete: () => this._panel?.classList.remove('is-animating'),
|
|
658
|
-
onInterrupt: () => this._panel?.classList.remove('is-animating')
|
|
659
|
-
});
|
|
372
|
+
MoniMorphModal._scrollLockOwners.add(this);
|
|
660
373
|
}
|
|
661
374
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
private _ensureSolidPanelBackground() {
|
|
670
|
-
if (!this._panel) return;
|
|
671
|
-
let bg = window.getComputedStyle(this._panel).backgroundColor;
|
|
672
|
-
if (this._isColorTransparent(bg)) {
|
|
673
|
-
const solidBg = this._findSolidBackground(this._panel);
|
|
674
|
-
if (solidBg) {
|
|
675
|
-
this._panel.style.backgroundColor = solidBg;
|
|
676
|
-
} else {
|
|
677
|
-
// Aseguramos un color sólido para evitar bugs de animación FLIP hacia rgba(0,0,0,0).
|
|
678
|
-
// NOTA: GSAP falla si el color inline es un string con var(). Usamos un fallback seguro.
|
|
679
|
-
const fallback = getComputedStyle(document.body).getPropertyValue('--surface-container-high').trim();
|
|
680
|
-
this._panel.style.backgroundColor = fallback || '#2d2d2d';
|
|
681
|
-
}
|
|
375
|
+
private _releaseDocumentScrollLock(): void {
|
|
376
|
+
if (!MoniMorphModal._scrollLockOwners.delete(this) || MoniMorphModal._scrollLockOwners.size > 0) return;
|
|
377
|
+
const snapshot = MoniMorphModal._scrollLockSnapshot;
|
|
378
|
+
if (snapshot) {
|
|
379
|
+
document.documentElement.style.overflow = snapshot.documentOverflow;
|
|
380
|
+
document.body.style.overflow = snapshot.bodyOverflow;
|
|
381
|
+
document.body.style.overscrollBehavior = snapshot.bodyOverscrollBehavior;
|
|
682
382
|
}
|
|
383
|
+
MoniMorphModal._scrollLockSnapshot = null;
|
|
683
384
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
this.
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
gsap.
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Busca en el DOM el elemento que dispara el modal (el trigger) basándose en la propiedad `target`.
|
|
388
|
+
* Establece el listener de click que iniciará la animación de morphing.
|
|
389
|
+
*/
|
|
390
|
+
private _resolveTarget(): void {
|
|
391
|
+
if (!this.target) return;
|
|
392
|
+
|
|
393
|
+
const el = document.querySelector<HTMLElement>(this.target);
|
|
394
|
+
if (!el) {
|
|
395
|
+
// Es normal que no se encuentre el target si está condicionado por un #if en Svelte
|
|
396
|
+
// Se intentará resolver nuevamente al llamar a show()
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Si el target cambió dinámicamente, limpiamos el listener anterior para evitar memory leaks
|
|
401
|
+
if (this._targetEl && this._targetEl !== el) {
|
|
402
|
+
this._targetEl.removeEventListener('click', this._onTargetClick);
|
|
403
|
+
}
|
|
404
|
+
this._targetEl = el;
|
|
405
|
+
this._targetEl.addEventListener('click', this._onTargetClick);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Dispara la apertura del modal y coordina la animación de FLIP (First, Last, Invert, Play) / Morphing.
|
|
410
|
+
* 1. Oculta visualmente el target original.
|
|
411
|
+
* 2. Posiciona el modal colapsado exactamente sobre el target original (mismas dimensiones, color, y radios).
|
|
412
|
+
* 3. Expande fluidamente el modal a sus dimensiones finales mediante transformaciones CSS.
|
|
413
|
+
*/
|
|
414
|
+
show(): void {
|
|
415
|
+
// Prevenimos ejecuciones concurrentes si ya se está animando o está visible
|
|
416
|
+
if (this._isAnimating || this._visible) return;
|
|
417
|
+
|
|
418
|
+
if (!this._targetEl) this._resolveTarget();
|
|
419
|
+
if (!this._targetEl) {
|
|
420
|
+
console.warn('[moni-morph-modal] cannot show: target not found');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
this._captureOriginFallbacks(this._targetEl);
|
|
424
|
+
|
|
425
|
+
this._isAnimating = true;
|
|
426
|
+
|
|
427
|
+
// Flag interno para evitar loops infinitos en el hook `updated()`
|
|
428
|
+
this._handlingOpenChange = true;
|
|
429
|
+
this.open = true;
|
|
430
|
+
this._handlingOpenChange = false;
|
|
431
|
+
this._syncDocumentScrollLock();
|
|
432
|
+
|
|
433
|
+
// Apilamos el modal globalmente para manejar correctamente el z-index en modales anidados
|
|
434
|
+
MoniMorphModal._openStack.push(this);
|
|
435
|
+
this._zIndex = 1000 + MoniMorphModal._openStack.length;
|
|
436
|
+
|
|
437
|
+
// Aplicamos variables CSS inline para gestionar las transiciones del backdrop y el modal
|
|
438
|
+
this.style.zIndex = String(this._zIndex);
|
|
439
|
+
this.style.setProperty('--_z-index', String(this._zIndex));
|
|
440
|
+
this.style.setProperty('--_backdrop-duration', '0.38s');
|
|
441
|
+
this.style.setProperty('--_backdrop-ease', 'cubic-bezier(0.15, 0.85, 0.2, 1)');
|
|
442
|
+
if (this._backdrop) gsap.set(this._backdrop, { clearProps: 'opacity,transition' });
|
|
443
|
+
|
|
444
|
+
// Fase "First": Ocultamos o cubrimos el trigger original según la configuración del desarrollador
|
|
445
|
+
this._hideTargetContent();
|
|
446
|
+
|
|
447
|
+
void this.updateComplete.then(async () => {
|
|
448
|
+
if (!this._targetEl || !this._panel || !this._inner) {
|
|
449
|
+
this._isAnimating = false;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Añadimos clase para prevenir interacciones durante la animación
|
|
454
|
+
this._panel.classList.add('is-animating');
|
|
455
|
+
|
|
456
|
+
// Esperamos un frame para asegurar que el navegador aplicó el `display: block`
|
|
457
|
+
// necesario antes de poder leer las dimensiones.
|
|
458
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
459
|
+
|
|
460
|
+
// 1. "First": Calculamos el estado visual actual del trigger
|
|
461
|
+
const targetState = this._getTargetState(this._targetEl);
|
|
462
|
+
this._debugMorph('open', this._targetEl, 'target', [this._targetEl]);
|
|
463
|
+
const panel = this._panel;
|
|
464
|
+
|
|
465
|
+
// 2. "Invert": Posicionamos el modal como un clon visual exacto del trigger.
|
|
466
|
+
// Desactivamos la sombra y forzamos el color/borde para coincidir con el botón/FAB.
|
|
467
|
+
this._applyRect(panel, this._toPanelCoordinateRect(targetState.rect));
|
|
468
|
+
panel.style.backgroundColor = targetState.backgroundColor;
|
|
469
|
+
panel.style.color = targetState.color;
|
|
470
|
+
panel.style.borderRadius = targetState.borderRadius;
|
|
471
|
+
panel.style.boxShadow = targetState.boxShadow;
|
|
472
|
+
panel.style.zIndex = String(this._zIndex);
|
|
473
|
+
panel.style.visibility = 'visible';
|
|
474
|
+
panel.style.opacity = '1';
|
|
475
|
+
panel.style.pointerEvents = 'auto';
|
|
476
|
+
|
|
477
|
+
if (this._backdrop) {
|
|
478
|
+
this._backdrop.style.zIndex = String(this._zIndex - 1);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Limpiamos elementos fantasma previos si el usuario interactuó muy rápido
|
|
482
|
+
this._cleanupMorphElements();
|
|
483
|
+
|
|
484
|
+
// Respetamos la preferencia de sistema de animaciones reducidas por accesibilidad (a11y)
|
|
485
|
+
const prefersReducedMotion = window.matchMedia(
|
|
486
|
+
'(prefers-reduced-motion: reduce)'
|
|
487
|
+
).matches;
|
|
488
|
+
|
|
489
|
+
// 3. Verificamos si debemos orquestar la animación compleja del label flotante cruzado
|
|
490
|
+
let shouldMorph = this._shouldMorphLabel() && !prefersReducedMotion;
|
|
491
|
+
const morphTargets: HTMLElement[] = [];
|
|
492
|
+
let source: any = null;
|
|
493
|
+
const headerWrapper = this.shadowRoot!.querySelector('.header-morph-wrapper') as HTMLElement;
|
|
494
|
+
|
|
495
|
+
if (shouldMorph) {
|
|
496
|
+
// Preparamos la estructura del header para que el texto real entre con un fade in
|
|
497
|
+
this._animateLabelOpen();
|
|
498
|
+
|
|
499
|
+
// Buscamos el ícono y el texto dentro del botón de origen (trigger)
|
|
500
|
+
source = this._resolveMorphSource();
|
|
501
|
+
|
|
502
|
+
// Si existe un nodo de texto, creamos un elemento flotante (fantasma) en el modal
|
|
503
|
+
if (source.text) {
|
|
504
|
+
const el = this._createMorphText(source.text.content, source.text.sourceEl);
|
|
505
|
+
this._positionElement(el, source.text.rect);
|
|
506
|
+
morphTargets.push(el);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Hacemos lo mismo si el botón original contenía un icono
|
|
510
|
+
if (source.icon) {
|
|
511
|
+
const el = this._createMorphIcon(source.icon.element);
|
|
512
|
+
this._positionElement(el, source.icon.rect);
|
|
513
|
+
morphTargets.push(el);
|
|
514
|
+
}
|
|
515
|
+
if (morphTargets.length === 0) shouldMorph = false;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (shouldMorph && headerWrapper) {
|
|
519
|
+
// Ocultamos temporalmente el contenido final del header mientras animamos los elementos flotantes
|
|
520
|
+
headerWrapper.style.opacity = '0';
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Capturamos el estado "First" del panel (tamaño pequeño de botón) y de los elementos flotantes
|
|
524
|
+
const state = Flip.getState(panel, {
|
|
525
|
+
props: 'backgroundColor,borderRadius,color,boxShadow'
|
|
526
|
+
});
|
|
527
|
+
const morphState = Flip.getState(morphTargets, {
|
|
528
|
+
props: 'color,fontSize,fontWeight,lineHeight'
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Fase "Last": Calculamos las dimensiones expandidas del modal basándonos en la configuración
|
|
532
|
+
let naturalSize: { width: number; height: number } | undefined;
|
|
533
|
+
if (this.autoSize || this.autoHeight) naturalSize = this._measureNaturalSize(panel);
|
|
534
|
+
|
|
535
|
+
const finalRect = this._computeFinalRect(targetState.rect, naturalSize);
|
|
536
|
+
panel.style.setProperty('--_panel-width', `${finalRect.width}px`);
|
|
537
|
+
panel.style.setProperty('--_panel-height', `${finalRect.height}px`);
|
|
538
|
+
this._applyRect(panel, finalRect);
|
|
539
|
+
|
|
540
|
+
// Removemos las forzadas de estilo del trigger para que herede las propias del modal
|
|
541
|
+
panel.style.backgroundColor = '';
|
|
542
|
+
panel.style.color = '';
|
|
543
|
+
panel.style.borderRadius = '';
|
|
544
|
+
panel.style.boxShadow = '';
|
|
545
|
+
|
|
546
|
+
if (shouldMorph && source) {
|
|
547
|
+
// Medimos dónde van a terminar el icono y el texto dentro del modal ya expandido
|
|
548
|
+
const textRect = this._measureHeaderTextRect();
|
|
549
|
+
const iconRect = this._measureHeaderIconRect();
|
|
550
|
+
const headerComputed = window.getComputedStyle(this._header);
|
|
551
|
+
|
|
552
|
+
if (this._morphEls.text && textRect) {
|
|
553
|
+
// Movemos el texto flotante a su destino y le pedimos que adopte los estilos del header
|
|
554
|
+
this._positionElement(this._morphEls.text, textRect);
|
|
555
|
+
this._morphEls.text.style.color = headerComputed.color || 'var(--on-surface)';
|
|
556
|
+
this._morphEls.text.style.fontSize = headerComputed.fontSize || 'inherit';
|
|
557
|
+
this._morphEls.text.style.fontWeight = headerComputed.fontWeight || 'inherit';
|
|
558
|
+
this._morphEls.text.style.lineHeight = headerComputed.lineHeight || 'inherit';
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (this._morphEls.icon && iconRect) {
|
|
562
|
+
// Hacemos lo mismo con el icono
|
|
563
|
+
this._positionElement(this._morphEls.icon, iconRect);
|
|
564
|
+
this._morphEls.icon.style.color = headerComputed.color || 'var(--on-surface)';
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
Flip.from(state, {
|
|
569
|
+
targets: panel,
|
|
570
|
+
duration: 0.38,
|
|
571
|
+
ease: 'morph-open',
|
|
572
|
+
zIndex: this._zIndex,
|
|
573
|
+
onComplete: () => {
|
|
574
|
+
this._isAnimating = false;
|
|
575
|
+
this._visible = true;
|
|
576
|
+
this._cleanupMorphElements();
|
|
577
|
+
if (headerWrapper) headerWrapper.style.opacity = '1';
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
if (morphTargets.length > 0) {
|
|
582
|
+
if (this.blurContent) {
|
|
583
|
+
gsap.set(morphTargets, { filter: 'blur(16px)' });
|
|
584
|
+
}
|
|
585
|
+
const flipConfig: any = {
|
|
586
|
+
targets: morphTargets,
|
|
587
|
+
duration: 0.38,
|
|
588
|
+
ease: 'morph-open',
|
|
589
|
+
zIndex: this._zIndex + 10
|
|
590
|
+
};
|
|
591
|
+
if (this.blurContent) {
|
|
592
|
+
flipConfig.filter = 'blur(0px)';
|
|
593
|
+
flipConfig.clearProps = 'filter';
|
|
594
|
+
}
|
|
595
|
+
Flip.from(morphState, flipConfig);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const body = this.shadowRoot!.querySelector('.body');
|
|
599
|
+
const footer = this.shadowRoot!.querySelector('footer');
|
|
600
|
+
const closeBtn = this.shadowRoot!.querySelector('.close-btn');
|
|
601
|
+
const fadeTargets = [body, footer, closeBtn].filter(Boolean) as HTMLElement[];
|
|
602
|
+
|
|
603
|
+
const initialBlur = this.blurContent ? 'blur(16px)' : 'blur(0px)';
|
|
604
|
+
gsap.set(fadeTargets, { opacity: 0, y: 8, filter: initialBlur });
|
|
605
|
+
gsap.fromTo(
|
|
606
|
+
fadeTargets,
|
|
607
|
+
{ opacity: 0, y: 8, filter: initialBlur },
|
|
608
|
+
{
|
|
609
|
+
opacity: 1,
|
|
610
|
+
y: 0,
|
|
611
|
+
filter: 'blur(0px)',
|
|
612
|
+
duration: 0.32,
|
|
613
|
+
delay: 0.08,
|
|
614
|
+
stagger: 0.04,
|
|
615
|
+
ease: 'power2.out',
|
|
616
|
+
onComplete: () => {
|
|
617
|
+
gsap.set(fadeTargets, { clearProps: 'opacity,transform,filter' });
|
|
618
|
+
this._panel.classList.remove('is-animating');
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Abre el modal realizando el morph desde un elemento proporcionado directamente.
|
|
627
|
+
* Útil cuando el trigger vive dentro de un Shadow DOM y no puede resolverse con `target`.
|
|
628
|
+
*/
|
|
629
|
+
showFrom(target: HTMLElement): void {
|
|
630
|
+
if (!target) return;
|
|
631
|
+
this._targetEl = target;
|
|
632
|
+
this.show();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** Redimensiona y reposiciona suavemente un modal que ya está abierto. */
|
|
636
|
+
resizeTo(width: string, height: string, anchor: 'center' | 'top-left' = 'center'): void {
|
|
637
|
+
this.expandedWidth = width;
|
|
638
|
+
this.expandedHeight = height;
|
|
639
|
+
if (!this._visible || !this._panel || !this._targetEl) return;
|
|
640
|
+
|
|
641
|
+
let finalRect = this._computeFinalRect(this._targetEl.getBoundingClientRect());
|
|
642
|
+
const transformedAncestor = this.parentElement?.closest<HTMLElement>('.lab-canvas, [data-morph-coordinate-space]');
|
|
643
|
+
const transform = transformedAncestor ? getComputedStyle(transformedAncestor).transform : 'none';
|
|
644
|
+
let scaleX = 1;
|
|
645
|
+
let scaleY = 1;
|
|
646
|
+
let ancestorRect: DOMRect | undefined;
|
|
647
|
+
if (transformedAncestor && transform !== 'none') {
|
|
648
|
+
const matrix = new DOMMatrix(transform);
|
|
649
|
+
scaleX = Math.hypot(matrix.a, matrix.b) || 1;
|
|
650
|
+
scaleY = Math.hypot(matrix.c, matrix.d) || 1;
|
|
651
|
+
ancestorRect = transformedAncestor.getBoundingClientRect();
|
|
652
|
+
}
|
|
653
|
+
if (anchor === 'top-left') {
|
|
654
|
+
const current = this._panel.getBoundingClientRect();
|
|
655
|
+
let left = current.left;
|
|
656
|
+
let top = current.top;
|
|
657
|
+
if (ancestorRect) {
|
|
658
|
+
left = (current.left - ancestorRect.left) / scaleX;
|
|
659
|
+
top = (current.top - ancestorRect.top) / scaleY;
|
|
660
|
+
}
|
|
661
|
+
finalRect = new DOMRect(left, top, finalRect.width, finalRect.height);
|
|
662
|
+
} else if (anchor === 'center' && ancestorRect) {
|
|
663
|
+
const visualLeft = window.innerWidth / 2 - (finalRect.width * scaleX) / 2;
|
|
664
|
+
const visualTop = window.innerHeight / 2 - (finalRect.height * scaleY) / 2;
|
|
665
|
+
finalRect = new DOMRect(
|
|
666
|
+
(visualLeft - ancestorRect.left) / scaleX,
|
|
667
|
+
(visualTop - ancestorRect.top) / scaleY,
|
|
668
|
+
finalRect.width,
|
|
669
|
+
finalRect.height
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
this._panel.classList.add('is-animating');
|
|
673
|
+
gsap.to(this._panel, {
|
|
674
|
+
top: finalRect.top,
|
|
675
|
+
left: finalRect.left,
|
|
676
|
+
width: finalRect.width,
|
|
677
|
+
height: finalRect.height,
|
|
678
|
+
'--_panel-width': `${finalRect.width}px`,
|
|
679
|
+
'--_panel-height': `${finalRect.height}px`,
|
|
680
|
+
duration: 0.32,
|
|
681
|
+
ease: 'morph-open',
|
|
682
|
+
overwrite: true,
|
|
683
|
+
onComplete: () => this._panel?.classList.remove('is-animating'),
|
|
684
|
+
onInterrupt: () => this._panel?.classList.remove('is-animating')
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Solución a un caso borde de FLIP Animations:
|
|
690
|
+
* Si el `.panel` original carece de un color de fondo sólido, la interpolación
|
|
691
|
+
* de GSAP puede comportarse erráticamente. Este método "asciende" el árbol del DOM
|
|
692
|
+
* hasta encontrar un padre con un color de fondo sólido y lo aplica, garantizando
|
|
693
|
+
* que el modal no colapse de forma transparente de regreso hacia el gatillo.
|
|
694
|
+
*/
|
|
695
|
+
private _ensureSolidPanelBackground() {
|
|
696
|
+
if (!this._panel) return;
|
|
697
|
+
let bg = window.getComputedStyle(this._panel).backgroundColor;
|
|
698
|
+
if (this._isColorTransparent(bg)) {
|
|
699
|
+
const solidBg = this._findSolidBackground(this._panel);
|
|
700
|
+
if (solidBg) {
|
|
701
|
+
this._panel.style.backgroundColor = solidBg;
|
|
702
|
+
} else {
|
|
703
|
+
// Aseguramos un color sólido para evitar bugs de animación FLIP hacia rgba(0,0,0,0).
|
|
704
|
+
// NOTA: GSAP falla si el color inline es un string con var(). Usamos un fallback seguro.
|
|
705
|
+
const fallback = getComputedStyle(document.body).getPropertyValue('--surface-container-high').trim();
|
|
706
|
+
this._panel.style.backgroundColor = fallback || '#2d2d2d';
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
hide(): void {
|
|
712
|
+
if (this._isAnimating || !this._visible) return;
|
|
713
|
+
|
|
714
|
+
this._isAnimating = true;
|
|
715
|
+
this._panel.classList.add('is-animating');
|
|
716
|
+
|
|
717
|
+
// Soluciona el caso donde el usuario no definió un fondo para .panel y lo aplicó en .body
|
|
718
|
+
this._ensureSolidPanelBackground();
|
|
719
|
+
|
|
720
|
+
this.style.setProperty('--_backdrop-duration', '0.3s');
|
|
721
|
+
this.style.setProperty('--_backdrop-ease', 'cubic-bezier(0.4, 0, 0.2, 1)');
|
|
722
|
+
|
|
723
|
+
const body = this.shadowRoot!.querySelector('.body');
|
|
724
|
+
const footer = this.shadowRoot!.querySelector('footer');
|
|
725
|
+
const closeBtn = this.shadowRoot!.querySelector('.close-btn');
|
|
726
|
+
const headerWrapper = this.shadowRoot!.querySelector('.header-morph-wrapper') as HTMLElement;
|
|
727
|
+
const fadeTargets = [body, footer, closeBtn, headerWrapper].filter(Boolean) as HTMLElement[];
|
|
728
|
+
|
|
729
|
+
// El contenido pierde definición al retirarse, evitando un corte plano
|
|
730
|
+
// antes de que la superficie comience su recorrido. `blurContent`
|
|
731
|
+
// conserva la variante más expresiva para quienes desean mayor intensidad.
|
|
732
|
+
const targetBlur = this.blurContent ? 'blur(12px)' : 'blur(6px)';
|
|
733
|
+
gsap.fromTo(
|
|
734
|
+
fadeTargets,
|
|
735
|
+
{ opacity: 1, filter: 'blur(0px)' },
|
|
736
|
+
{
|
|
737
|
+
opacity: 0,
|
|
738
|
+
filter: targetBlur,
|
|
739
|
+
duration: 0.12,
|
|
740
|
+
ease: 'power2.in',
|
|
741
|
+
onComplete: () => {
|
|
742
|
+
const panel = this._panel;
|
|
743
|
+
const closingTarget = this._resolveClosingTarget();
|
|
744
|
+
|
|
745
|
+
this._cleanupMorphElements();
|
|
746
|
+
const prefersReducedMotion = window.matchMedia(
|
|
747
|
+
'(prefers-reduced-motion: reduce)'
|
|
748
|
+
).matches;
|
|
749
|
+
// El cierre usa un único snapshot compuesto. La ruta histórica que
|
|
750
|
+
// creaba `morph-text` y `morph-icon` por separado podía ejecutarse a
|
|
751
|
+
// la vez que el snapshot y dejar etiquetas flotando sobre el modal.
|
|
752
|
+
let shouldMorph = false;
|
|
753
|
+
const morphTargets: HTMLElement[] = [];
|
|
754
|
+
let source: any = null;
|
|
755
|
+
let destinationMorph: { element: HTMLElement; rect: DOMRect; source: HTMLElement } | null = null;
|
|
756
|
+
|
|
757
|
+
if (shouldMorph) {
|
|
758
|
+
this._animateLabelClose();
|
|
759
|
+
source = this._resolveMorphSource();
|
|
760
|
+
const textRect = this._measureHeaderTextRect();
|
|
761
|
+
const iconRect = this._measureHeaderIconRect();
|
|
762
|
+
const headerComputed = window.getComputedStyle(this._header);
|
|
763
|
+
|
|
764
|
+
if (source.text && textRect) {
|
|
765
|
+
const el = this._createMorphText(source.text.content, source.text.sourceEl);
|
|
766
|
+
this._positionElement(el, textRect);
|
|
767
|
+
el.style.color = headerComputed.color || 'var(--on-surface)';
|
|
768
|
+
el.style.fontSize = headerComputed.fontSize || 'inherit';
|
|
769
|
+
el.style.fontWeight = headerComputed.fontWeight || 'inherit';
|
|
770
|
+
el.style.lineHeight = headerComputed.lineHeight || 'inherit';
|
|
771
|
+
morphTargets.push(el);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (source.icon && iconRect) {
|
|
775
|
+
const el = this._createMorphIcon(source.icon.element);
|
|
776
|
+
this._positionElement(el, iconRect);
|
|
777
|
+
el.style.color = headerComputed.color || 'var(--on-surface)';
|
|
778
|
+
morphTargets.push(el);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (morphTargets.length === 0) shouldMorph = false;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Si el destino de cierre cambió, el contenido final debe ser una
|
|
785
|
+
// representación completa del target (no solamente su icono). Esto
|
|
786
|
+
// permite hacer morph hacia rich buttons, cards, imágenes y cualquier
|
|
787
|
+
// otra superficie compuesta.
|
|
788
|
+
if (closingTarget && !shouldMorph && !prefersReducedMotion) {
|
|
789
|
+
const visualTarget = this._resolveVisualTarget(closingTarget);
|
|
790
|
+
const destinationRect = visualTarget.getBoundingClientRect();
|
|
791
|
+
if (destinationRect.width > 0 && destinationRect.height > 0) {
|
|
792
|
+
// El contenido del trigger permanece oculto mientras el modal está
|
|
793
|
+
// abierto. Lo restauramos únicamente durante la captura para que el
|
|
794
|
+
// snapshot no herede opacity:0/color:transparent.
|
|
795
|
+
if (closingTarget === this._targetEl) this._restoreTargetContent();
|
|
796
|
+
const element = this._createTargetSnapshot(visualTarget, destinationRect);
|
|
797
|
+
if (closingTarget === this._targetEl) this._hideTargetContent();
|
|
798
|
+
element.style.opacity = '0';
|
|
799
|
+
destinationMorph = { element, rect: destinationRect, source: visualTarget };
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (shouldMorph && headerWrapper) {
|
|
804
|
+
headerWrapper.style.opacity = '0';
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const state = Flip.getState(panel, {
|
|
808
|
+
props: 'backgroundColor,borderRadius,color,boxShadow'
|
|
809
|
+
});
|
|
810
|
+
const morphState = Flip.getState(morphTargets, {
|
|
811
|
+
props: 'color,fontSize,fontWeight,lineHeight,opacity'
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
if (!closingTarget) {
|
|
815
|
+
gsap.to(this._backdrop, {
|
|
816
|
+
opacity: 0,
|
|
817
|
+
duration: prefersReducedMotion ? 0.01 : 0.22,
|
|
818
|
+
ease: 'power2.in'
|
|
819
|
+
});
|
|
820
|
+
gsap.to(panel, {
|
|
821
|
+
autoAlpha: 0,
|
|
822
|
+
scale: 0.92,
|
|
823
|
+
duration: prefersReducedMotion ? 0.01 : 0.22,
|
|
824
|
+
ease: 'power2.in',
|
|
825
|
+
overwrite: true,
|
|
826
|
+
onComplete: () => {
|
|
827
|
+
gsap.set(panel, { clearProps: 'transform' });
|
|
828
|
+
this._finishHide(fadeTargets, headerWrapper);
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const targetState = this._getTargetState(closingTarget);
|
|
835
|
+
if (destinationMorph) {
|
|
836
|
+
this._runTargetMorphTimeline(
|
|
837
|
+
panel,
|
|
838
|
+
targetState,
|
|
839
|
+
destinationMorph,
|
|
840
|
+
fadeTargets,
|
|
841
|
+
headerWrapper,
|
|
842
|
+
prefersReducedMotion
|
|
843
|
+
);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
let naturalSize: { width: number; height: number } | undefined;
|
|
848
|
+
if (this.autoSize || this.autoHeight) naturalSize = this._measureNaturalSize(panel);
|
|
849
|
+
|
|
850
|
+
const finalRect = this._computeFinalRect(targetState.rect, naturalSize);
|
|
851
|
+
panel.style.setProperty('--_panel-width', `${finalRect.width}px`);
|
|
852
|
+
panel.style.setProperty('--_panel-height', `${finalRect.height}px`);
|
|
853
|
+
|
|
854
|
+
this._applyRect(panel, this._toPanelCoordinateRect(targetState.rect));
|
|
855
|
+
panel.style.backgroundColor = targetState.backgroundColor;
|
|
856
|
+
panel.style.color = targetState.color;
|
|
857
|
+
panel.style.borderRadius = targetState.borderRadius;
|
|
858
|
+
panel.style.boxShadow = targetState.boxShadow;
|
|
859
|
+
|
|
860
|
+
if (shouldMorph && source) {
|
|
861
|
+
if (this._morphEls.text && source.text) {
|
|
862
|
+
this._positionElement(this._morphEls.text, source.text.rect);
|
|
863
|
+
this._morphEls.text.style.color =
|
|
864
|
+
window.getComputedStyle(source.text.sourceEl).color || 'inherit';
|
|
865
|
+
this._morphEls.text.style.fontSize =
|
|
866
|
+
window.getComputedStyle(source.text.sourceEl).fontSize || 'inherit';
|
|
867
|
+
this._morphEls.text.style.fontWeight =
|
|
868
|
+
window.getComputedStyle(source.text.sourceEl).fontWeight || 'inherit';
|
|
869
|
+
this._morphEls.text.style.lineHeight =
|
|
870
|
+
window.getComputedStyle(source.text.sourceEl).lineHeight || 'inherit';
|
|
871
|
+
}
|
|
872
|
+
if (this._morphEls.icon && source.icon) {
|
|
873
|
+
this._positionElement(this._morphEls.icon, source.icon.rect);
|
|
874
|
+
this._morphEls.icon.style.color =
|
|
875
|
+
window.getComputedStyle(source.icon.element).color || 'inherit';
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
Flip.from(state, {
|
|
879
|
+
targets: panel,
|
|
880
|
+
duration: 0.3,
|
|
881
|
+
ease: 'morph-close',
|
|
882
|
+
zIndex: this._zIndex,
|
|
883
|
+
onComplete: () => this._finishHide(fadeTargets, headerWrapper)
|
|
884
|
+
});
|
|
885
|
+
gsap.to(this._backdrop, {
|
|
886
|
+
opacity: 0,
|
|
887
|
+
duration: 0.3,
|
|
888
|
+
ease: 'power2.inOut'
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
if (morphTargets.length > 0) {
|
|
892
|
+
if (this.blurContent) {
|
|
893
|
+
gsap.set(morphTargets, { filter: 'blur(0px)' });
|
|
894
|
+
}
|
|
895
|
+
const flipConfig: any = {
|
|
896
|
+
targets: morphTargets,
|
|
897
|
+
duration: 0.3,
|
|
898
|
+
ease: 'morph-close',
|
|
899
|
+
zIndex: this._zIndex + 10
|
|
900
|
+
};
|
|
901
|
+
if (this.blurContent) {
|
|
902
|
+
flipConfig.filter = 'blur(12px)';
|
|
903
|
+
}
|
|
904
|
+
Flip.from(morphState, flipConfig);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
private _runTargetMorphTimeline(
|
|
912
|
+
panel: HTMLElement,
|
|
913
|
+
target: TargetState,
|
|
914
|
+
destination: { element: HTMLElement; rect: DOMRect; source: HTMLElement },
|
|
915
|
+
fadeTargets: HTMLElement[],
|
|
916
|
+
headerWrapper: HTMLElement | null,
|
|
917
|
+
prefersReducedMotion: boolean
|
|
918
|
+
): void {
|
|
919
|
+
const current = this._toPanelCoordinateRect(panel.getBoundingClientRect());
|
|
920
|
+
const targetRect = this._toPanelCoordinateRect(target.rect);
|
|
921
|
+
const destinationRect = this._toPanelCoordinateRect(destination.rect);
|
|
922
|
+
const destinationContent = destination.element;
|
|
923
|
+
const activeTargetAnimations = destination.source.getAnimations?.({ subtree: true }) ?? [];
|
|
924
|
+
const remainingTargetMotion = activeTargetAnimations.reduce((remaining, animation) => {
|
|
925
|
+
const timing = animation.effect?.getComputedTiming();
|
|
926
|
+
const endTime = typeof timing?.endTime === 'number' ? timing.endTime : 0;
|
|
927
|
+
const currentTime = typeof animation.currentTime === 'number' ? animation.currentTime : 0;
|
|
928
|
+
return Math.max(remaining, Math.max(0, endTime - currentTime) / 1000);
|
|
929
|
+
}, 0);
|
|
930
|
+
const morphDuration = Math.min(1.2, Math.max(0.46, remainingTargetMotion));
|
|
931
|
+
const start = {
|
|
932
|
+
x: current.left + current.width / 2,
|
|
933
|
+
y: current.top + current.height / 2
|
|
934
|
+
};
|
|
935
|
+
const end = {
|
|
936
|
+
x: targetRect.left + targetRect.width / 2,
|
|
937
|
+
y: targetRect.top + targetRect.height / 2
|
|
938
|
+
};
|
|
939
|
+
const dx = end.x - start.x;
|
|
940
|
+
const dy = end.y - start.y;
|
|
941
|
+
const distance = Math.hypot(dx, dy) || 1;
|
|
942
|
+
// En una Bézier cuadrática el desplazamiento máximo visible equivale
|
|
943
|
+
// aproximadamente a la mitad del offset del control. 0.72 produce una
|
|
944
|
+
// curva expresiva visible (~36% de la distancia) sin formar un bucle.
|
|
945
|
+
const arc = Math.min(360, Math.max(48, distance * 0.72));
|
|
946
|
+
const perpendicular = { x: -dy / distance, y: dx / distance };
|
|
947
|
+
const midpoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
|
948
|
+
const viewportCenter = this._toPanelCoordinateRect(
|
|
949
|
+
new DOMRect(window.innerWidth / 2, window.innerHeight / 2, 0, 0)
|
|
950
|
+
);
|
|
951
|
+
const candidates = [1, -1].map((direction) => ({
|
|
952
|
+
x: midpoint.x + perpendicular.x * arc * direction,
|
|
953
|
+
y: midpoint.y + perpendicular.y * arc * direction
|
|
954
|
+
}));
|
|
955
|
+
const centerDistance = (point: { x: number; y: number }) =>
|
|
956
|
+
Math.abs(
|
|
957
|
+
Math.abs(dy) >= Math.abs(dx)
|
|
958
|
+
? point.x - viewportCenter.x
|
|
959
|
+
: point.y - viewportCenter.y
|
|
960
|
+
);
|
|
961
|
+
const control = centerDistance(candidates[0]) <= centerDistance(candidates[1])
|
|
962
|
+
? candidates[0]
|
|
963
|
+
: candidates[1];
|
|
964
|
+
|
|
965
|
+
Object.assign(destinationContent.style, {
|
|
966
|
+
position: 'absolute',
|
|
967
|
+
inset: '50% auto auto 50%',
|
|
968
|
+
width: `${destinationRect.width}px`,
|
|
969
|
+
height: `${destinationRect.height}px`,
|
|
970
|
+
transform: 'translate(-50%, -50%) scale(.78)',
|
|
971
|
+
color: window.getComputedStyle(destination.source).color || target.color,
|
|
972
|
+
opacity: '0'
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
if (prefersReducedMotion) {
|
|
976
|
+
panel.appendChild(destinationContent);
|
|
977
|
+
this._applyRect(panel, targetRect);
|
|
978
|
+
panel.style.backgroundColor = target.backgroundColor;
|
|
979
|
+
panel.style.color = target.color;
|
|
980
|
+
panel.style.borderRadius = target.borderRadius;
|
|
981
|
+
panel.style.boxShadow = target.boxShadow;
|
|
982
|
+
this._backdrop.style.opacity = '0';
|
|
983
|
+
destinationContent.style.opacity = '1';
|
|
984
|
+
this._finishHide(fadeTargets, headerWrapper);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const timeline = gsap.timeline({
|
|
989
|
+
onComplete: () => {
|
|
990
|
+
gsap.set(panel, { clearProps: 'transform' });
|
|
991
|
+
this._finishHide(fadeTargets, headerWrapper);
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
panel.style.left = `${current.left}px`;
|
|
995
|
+
panel.style.top = `${current.top}px`;
|
|
996
|
+
panel.style.width = `${current.width}px`;
|
|
997
|
+
panel.style.height = `${current.height}px`;
|
|
998
|
+
const panelStartStyle = window.getComputedStyle(panel);
|
|
999
|
+
const panelStartVisual = {
|
|
1000
|
+
backgroundColor: panelStartStyle.backgroundColor,
|
|
1001
|
+
color: panelStartStyle.color,
|
|
1002
|
+
borderRadius: panelStartStyle.borderRadius,
|
|
1003
|
+
boxShadow: panelStartStyle.boxShadow
|
|
1004
|
+
};
|
|
1005
|
+
// GSAP es el único propietario de la opacidad durante el recorrido.
|
|
1006
|
+
// La transición CSS del estado `.open` produciría un segundo easing y
|
|
1007
|
+
// haría que el scrim pareciera desaparecer al final de golpe.
|
|
1005
1008
|
this._backdrop.style.transition = 'none';
|
|
1006
1009
|
this._backdrop.style.opacity = '1';
|
|
1007
1010
|
const motion = { progress: 0 };
|
|
1008
1011
|
let contentAttached = false;
|
|
1012
|
+
const sampleInterval = 1000 / 30;
|
|
1013
|
+
const initialVisualWidth = destination.source.offsetWidth || targetRect.width;
|
|
1014
|
+
const initialVisualHeight = destination.source.offsetHeight || targetRect.height;
|
|
1015
|
+
const captureTargetSample = () => {
|
|
1016
|
+
const state = this._getTargetState(destination.source);
|
|
1017
|
+
const rect = this._toPanelCoordinateRect(state.rect);
|
|
1018
|
+
return {
|
|
1019
|
+
rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
|
|
1020
|
+
transform: this._readTransformState(destination.source),
|
|
1021
|
+
visualWidth: destination.source.offsetWidth || rect.width,
|
|
1022
|
+
visualHeight: destination.source.offsetHeight || rect.height,
|
|
1023
|
+
backgroundColor: state.backgroundColor,
|
|
1024
|
+
color: state.color,
|
|
1025
|
+
borderRadius: state.borderRadius,
|
|
1026
|
+
boxShadow: state.boxShadow
|
|
1027
|
+
};
|
|
1028
|
+
};
|
|
1029
|
+
let previousSample = captureTargetSample();
|
|
1030
|
+
let nextSample = previousSample;
|
|
1031
|
+
let sampledAt = performance.now();
|
|
1032
|
+
const lerp = (from: number, to: number, amount: number) => from + (to - from) * amount;
|
|
1009
1033
|
if (this.debug) {
|
|
1010
1034
|
console.log('[moni-morph-modal] arc', { start, control, end, distance, arc, morphDuration });
|
|
1011
1035
|
}
|
|
1012
|
-
timeline
|
|
1013
|
-
.addLabel('morph', 0)
|
|
1014
|
-
.to(motion, {
|
|
1015
|
-
progress: 1,
|
|
1016
|
-
duration: morphDuration,
|
|
1036
|
+
timeline
|
|
1037
|
+
.addLabel('morph', 0)
|
|
1038
|
+
.to(motion, {
|
|
1039
|
+
progress: 1,
|
|
1040
|
+
duration: morphDuration,
|
|
1017
1041
|
ease: 'power2.inOut',
|
|
1018
1042
|
onUpdate: () => {
|
|
1019
1043
|
const progress = motion.progress;
|
|
1020
|
-
const
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1044
|
+
const now = performance.now();
|
|
1045
|
+
const forceFinalSample = progress >= 0.999;
|
|
1046
|
+
if (forceFinalSample || now - sampledAt >= sampleInterval) {
|
|
1047
|
+
previousSample = nextSample;
|
|
1048
|
+
nextSample = captureTargetSample();
|
|
1049
|
+
sampledAt = now;
|
|
1050
|
+
}
|
|
1051
|
+
const sampleProgress = forceFinalSample
|
|
1052
|
+
? 1
|
|
1053
|
+
: Math.min(1, (now - sampledAt) / sampleInterval);
|
|
1054
|
+
const liveRect = {
|
|
1055
|
+
left: lerp(previousSample.rect.left, nextSample.rect.left, sampleProgress),
|
|
1056
|
+
top: lerp(previousSample.rect.top, nextSample.rect.top, sampleProgress),
|
|
1057
|
+
width: lerp(previousSample.rect.width, nextSample.rect.width, sampleProgress),
|
|
1058
|
+
height: lerp(previousSample.rect.height, nextSample.rect.height, sampleProgress)
|
|
1026
1059
|
};
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
const liveMidpoint = {
|
|
1033
|
-
x: (start.x + liveEnd.x) / 2,
|
|
1034
|
-
y: (start.y + liveEnd.y) / 2
|
|
1060
|
+
const liveTransform = {
|
|
1061
|
+
rotation: lerp(previousSample.transform.rotation, nextSample.transform.rotation, sampleProgress),
|
|
1062
|
+
skewX: lerp(previousSample.transform.skewX, nextSample.transform.skewX, sampleProgress),
|
|
1063
|
+
scaleX: lerp(previousSample.transform.scaleX, nextSample.transform.scaleX, sampleProgress),
|
|
1064
|
+
scaleY: lerp(previousSample.transform.scaleY, nextSample.transform.scaleY, sampleProgress)
|
|
1035
1065
|
};
|
|
1036
|
-
const
|
|
1037
|
-
x:
|
|
1038
|
-
y:
|
|
1039
|
-
}
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
//
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
const
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1066
|
+
const liveEnd = {
|
|
1067
|
+
x: liveRect.left + liveRect.width / 2,
|
|
1068
|
+
y: liveRect.top + liveRect.height / 2
|
|
1069
|
+
};
|
|
1070
|
+
const liveDx = liveEnd.x - start.x;
|
|
1071
|
+
const liveDy = liveEnd.y - start.y;
|
|
1072
|
+
const liveDistance = Math.hypot(liveDx, liveDy) || 1;
|
|
1073
|
+
const liveArc = Math.min(360, Math.max(48, liveDistance * 0.72));
|
|
1074
|
+
const livePerpendicular = { x: -liveDy / liveDistance, y: liveDx / liveDistance };
|
|
1075
|
+
const liveMidpoint = {
|
|
1076
|
+
x: (start.x + liveEnd.x) / 2,
|
|
1077
|
+
y: (start.y + liveEnd.y) / 2
|
|
1078
|
+
};
|
|
1079
|
+
const liveCandidates = [1, -1].map((direction) => ({
|
|
1080
|
+
x: liveMidpoint.x + livePerpendicular.x * liveArc * direction,
|
|
1081
|
+
y: liveMidpoint.y + livePerpendicular.y * liveArc * direction
|
|
1082
|
+
}));
|
|
1083
|
+
const liveControl = centerDistance(liveCandidates[0]) <= centerDistance(liveCandidates[1])
|
|
1084
|
+
? liveCandidates[0]
|
|
1085
|
+
: liveCandidates[1];
|
|
1086
|
+
// El backdrop comparte exactamente el playhead del arco. Se mantiene
|
|
1087
|
+
// algo más presente al comienzo y luego se retira progresivamente.
|
|
1088
|
+
this._backdrop.style.opacity = String(Math.max(0, 1 - progress));
|
|
1089
|
+
const inverse = 1 - progress;
|
|
1090
|
+
const centerX =
|
|
1091
|
+
inverse * inverse * start.x +
|
|
1092
|
+
2 * inverse * progress * liveControl.x +
|
|
1093
|
+
progress * progress * liveEnd.x;
|
|
1094
|
+
const centerY =
|
|
1095
|
+
inverse * inverse * start.y +
|
|
1096
|
+
2 * inverse * progress * liveControl.y +
|
|
1097
|
+
progress * progress * liveEnd.y;
|
|
1098
|
+
// La contracción modal mantiene su easing, pero cualquier mutación
|
|
1099
|
+
// adicional del target se suma completa para seguirla en tiempo real.
|
|
1100
|
+
const liveVisualWidth = lerp(previousSample.visualWidth, nextSample.visualWidth, sampleProgress);
|
|
1101
|
+
const liveVisualHeight = lerp(previousSample.visualHeight, nextSample.visualHeight, sampleProgress);
|
|
1102
|
+
const width =
|
|
1103
|
+
current.width + (initialVisualWidth - current.width) * progress +
|
|
1104
|
+
(liveVisualWidth - initialVisualWidth);
|
|
1105
|
+
const height =
|
|
1106
|
+
current.height + (initialVisualHeight - current.height) * progress +
|
|
1107
|
+
(liveVisualHeight - initialVisualHeight);
|
|
1108
|
+
panel.style.width = `${width}px`;
|
|
1109
|
+
panel.style.height = `${height}px`;
|
|
1110
|
+
gsap.set(panel, {
|
|
1111
|
+
x: centerX - width / 2 - current.left,
|
|
1112
|
+
y: centerY - height / 2 - current.top,
|
|
1113
|
+
rotation: liveTransform.rotation * progress,
|
|
1114
|
+
skewX: liveTransform.skewX * progress,
|
|
1115
|
+
scaleX: 1 + (liveTransform.scaleX - 1) * progress,
|
|
1116
|
+
scaleY: 1 + (liveTransform.scaleY - 1) * progress,
|
|
1076
1117
|
backgroundColor: gsap.utils.interpolate(
|
|
1077
1118
|
panelStartVisual.backgroundColor,
|
|
1078
|
-
|
|
1119
|
+
gsap.utils.interpolate(previousSample.backgroundColor, nextSample.backgroundColor, sampleProgress),
|
|
1120
|
+
progress
|
|
1121
|
+
),
|
|
1122
|
+
color: gsap.utils.interpolate(
|
|
1123
|
+
panelStartVisual.color,
|
|
1124
|
+
gsap.utils.interpolate(previousSample.color, nextSample.color, sampleProgress),
|
|
1079
1125
|
progress
|
|
1080
1126
|
),
|
|
1081
|
-
color: gsap.utils.interpolate(panelStartVisual.color, liveState.color, progress),
|
|
1082
1127
|
borderRadius: gsap.utils.interpolate(
|
|
1083
1128
|
panelStartVisual.borderRadius,
|
|
1084
|
-
|
|
1129
|
+
gsap.utils.interpolate(previousSample.borderRadius, nextSample.borderRadius, sampleProgress),
|
|
1085
1130
|
progress
|
|
1086
1131
|
),
|
|
1087
1132
|
boxShadow: gsap.utils.interpolate(
|
|
1088
1133
|
panelStartVisual.boxShadow,
|
|
1089
|
-
|
|
1134
|
+
gsap.utils.interpolate(previousSample.boxShadow, nextSample.boxShadow, sampleProgress),
|
|
1090
1135
|
progress
|
|
1091
|
-
)
|
|
1092
|
-
});
|
|
1093
|
-
// El contenido del target no debe flotar sobre el modal grande. Solo
|
|
1094
|
-
// comienza a materializarse cuando posición y relación de aspecto ya
|
|
1095
|
-
// se aproximan al destino, pero continúa viajando con la superficie.
|
|
1096
|
-
const contentProgress = Math.max(0, Math.min(1, (progress - 0.6) / 0.4));
|
|
1097
|
-
if (!contentAttached && contentProgress > 0) {
|
|
1098
|
-
panel.appendChild(destinationContent);
|
|
1099
|
-
contentAttached = true;
|
|
1100
|
-
}
|
|
1101
|
-
// Conservamos el layout nativo del target y escalamos toda su
|
|
1102
|
-
// composición. Redimensionar el wrapper convertiría porcentajes
|
|
1103
|
-
// computados en píxeles fijos y dejaría la imagen arriba a la izquierda.
|
|
1104
|
-
destinationContent.style.width = `${liveVisualWidth}px`;
|
|
1105
|
-
destinationContent.style.height = `${liveVisualHeight}px`;
|
|
1106
|
-
const revealScale = 0.82 + contentProgress * 0.18;
|
|
1107
|
-
gsap.set(destinationContent, {
|
|
1108
|
-
opacity: contentProgress,
|
|
1109
|
-
scaleX: (width / Math.max(1, liveVisualWidth)) * revealScale,
|
|
1110
|
-
scaleY: (height / Math.max(1, liveVisualHeight)) * revealScale
|
|
1111
|
-
});
|
|
1112
|
-
}
|
|
1113
|
-
}, 'morph')
|
|
1114
|
-
.addLabel('target-fade')
|
|
1115
|
-
.to(panel, {
|
|
1116
|
-
autoAlpha: 0,
|
|
1117
|
-
duration: 0.08,
|
|
1118
|
-
ease: 'power1.out',
|
|
1119
|
-
onStart: () => {
|
|
1120
|
-
// Handoff atómico: el contenido real ocupa el lugar del snapshot
|
|
1121
|
-
// antes de retirar la última superficie. Evita un frame vacío o una
|
|
1122
|
-
// duplicación perceptible al finalizar el morph.
|
|
1123
|
-
destinationContent.style.visibility = 'hidden';
|
|
1124
|
-
this._restoreTargetContent();
|
|
1125
|
-
panel.style.backgroundColor = 'transparent';
|
|
1126
|
-
panel.style.boxShadow = 'none';
|
|
1127
|
-
panel.style.borderColor = 'transparent';
|
|
1128
|
-
}
|
|
1129
|
-
}, `morph+=${morphDuration}`);
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
private _finishHide(fadeTargets: HTMLElement[], headerWrapper: HTMLElement | null): void {
|
|
1133
|
-
const panel = this._panel;
|
|
1134
|
-
panel.style.visibility = 'hidden';
|
|
1135
|
-
panel.style.opacity = '0';
|
|
1136
|
-
panel.style.pointerEvents = 'none';
|
|
1137
|
-
this._visible = false;
|
|
1138
|
-
this._handlingOpenChange = true;
|
|
1139
|
-
this.open = false;
|
|
1140
|
-
this._handlingOpenChange = false;
|
|
1141
|
-
this._isAnimating = false;
|
|
1142
|
-
panel.classList.remove('is-animating');
|
|
1143
|
-
panel.style.removeProperty('--_panel-width');
|
|
1144
|
-
panel.style.removeProperty('--_panel-height');
|
|
1145
|
-
this._cleanupMorphElements();
|
|
1146
|
-
this._restoreTargetContent();
|
|
1147
|
-
gsap.set(fadeTargets, { clearProps: 'opacity,transform,filter' });
|
|
1148
|
-
if (headerWrapper) headerWrapper.style.opacity = '1';
|
|
1149
|
-
this.style.removeProperty('z-index');
|
|
1150
|
-
this.style.removeProperty('--_z-index');
|
|
1151
|
-
this.style.removeProperty('--_backdrop-duration');
|
|
1152
|
-
this.style.removeProperty('--_backdrop-ease');
|
|
1153
|
-
// Conservamos opacity:0 hasta la próxima apertura. Limpiarla aquí puede
|
|
1154
|
-
// producir un flash de scrim antes de que Lit retire la clase `.open`.
|
|
1136
|
+
)
|
|
1137
|
+
});
|
|
1138
|
+
// El contenido del target no debe flotar sobre el modal grande. Solo
|
|
1139
|
+
// comienza a materializarse cuando posición y relación de aspecto ya
|
|
1140
|
+
// se aproximan al destino, pero continúa viajando con la superficie.
|
|
1141
|
+
const contentProgress = Math.max(0, Math.min(1, (progress - 0.6) / 0.4));
|
|
1142
|
+
if (!contentAttached && contentProgress > 0) {
|
|
1143
|
+
panel.appendChild(destinationContent);
|
|
1144
|
+
contentAttached = true;
|
|
1145
|
+
}
|
|
1146
|
+
// Conservamos el layout nativo del target y escalamos toda su
|
|
1147
|
+
// composición. Redimensionar el wrapper convertiría porcentajes
|
|
1148
|
+
// computados en píxeles fijos y dejaría la imagen arriba a la izquierda.
|
|
1149
|
+
destinationContent.style.width = `${liveVisualWidth}px`;
|
|
1150
|
+
destinationContent.style.height = `${liveVisualHeight}px`;
|
|
1151
|
+
const revealScale = 0.82 + contentProgress * 0.18;
|
|
1152
|
+
gsap.set(destinationContent, {
|
|
1153
|
+
opacity: contentProgress,
|
|
1154
|
+
scaleX: (width / Math.max(1, liveVisualWidth)) * revealScale,
|
|
1155
|
+
scaleY: (height / Math.max(1, liveVisualHeight)) * revealScale
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
}, 'morph')
|
|
1159
|
+
.addLabel('target-fade')
|
|
1160
|
+
.to(panel, {
|
|
1161
|
+
autoAlpha: 0,
|
|
1162
|
+
duration: 0.08,
|
|
1163
|
+
ease: 'power1.out',
|
|
1164
|
+
onStart: () => {
|
|
1165
|
+
// Handoff atómico: el contenido real ocupa el lugar del snapshot
|
|
1166
|
+
// antes de retirar la última superficie. Evita un frame vacío o una
|
|
1167
|
+
// duplicación perceptible al finalizar el morph.
|
|
1168
|
+
destinationContent.style.visibility = 'hidden';
|
|
1169
|
+
this._restoreTargetContent();
|
|
1170
|
+
panel.style.backgroundColor = 'transparent';
|
|
1171
|
+
panel.style.boxShadow = 'none';
|
|
1172
|
+
panel.style.borderColor = 'transparent';
|
|
1173
|
+
}
|
|
1174
|
+
}, `morph+=${morphDuration}`);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
private _finishHide(fadeTargets: HTMLElement[], headerWrapper: HTMLElement | null): void {
|
|
1178
|
+
const panel = this._panel;
|
|
1179
|
+
panel.style.visibility = 'hidden';
|
|
1180
|
+
panel.style.opacity = '0';
|
|
1181
|
+
panel.style.pointerEvents = 'none';
|
|
1182
|
+
this._visible = false;
|
|
1183
|
+
this._handlingOpenChange = true;
|
|
1184
|
+
this.open = false;
|
|
1185
|
+
this._handlingOpenChange = false;
|
|
1186
|
+
this._isAnimating = false;
|
|
1187
|
+
panel.classList.remove('is-animating');
|
|
1188
|
+
panel.style.removeProperty('--_panel-width');
|
|
1189
|
+
panel.style.removeProperty('--_panel-height');
|
|
1190
|
+
this._cleanupMorphElements();
|
|
1191
|
+
this._restoreTargetContent();
|
|
1192
|
+
gsap.set(fadeTargets, { clearProps: 'opacity,transform,filter' });
|
|
1193
|
+
if (headerWrapper) headerWrapper.style.opacity = '1';
|
|
1194
|
+
this.style.removeProperty('z-index');
|
|
1195
|
+
this.style.removeProperty('--_z-index');
|
|
1196
|
+
this.style.removeProperty('--_backdrop-duration');
|
|
1197
|
+
this.style.removeProperty('--_backdrop-ease');
|
|
1198
|
+
// Conservamos opacity:0 hasta la próxima apertura. Limpiarla aquí puede
|
|
1199
|
+
// producir un flash de scrim antes de que Lit retire la clase `.open`.
|
|
1155
1200
|
const idx = MoniMorphModal._openStack.indexOf(this);
|
|
1156
1201
|
if (idx > -1) MoniMorphModal._openStack.splice(idx, 1);
|
|
1202
|
+
this._releaseDocumentScrollLock();
|
|
1157
1203
|
}
|
|
1158
|
-
|
|
1159
|
-
private _captureOriginFallbacks(origin: HTMLElement): void {
|
|
1160
|
-
this._originFallbacks = [];
|
|
1161
|
-
let current: HTMLElement | null = this._getComposedParent(origin);
|
|
1162
|
-
while (current && current !== document.body && current !== document.documentElement) {
|
|
1163
|
-
this._originFallbacks.push(current);
|
|
1164
|
-
current = this._getComposedParent(current);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
private _getComposedParent(el: HTMLElement): HTMLElement | null {
|
|
1169
|
-
if (el.parentElement) return el.parentElement;
|
|
1170
|
-
const root = el.getRootNode();
|
|
1171
|
-
return root instanceof ShadowRoot ? (root.host as HTMLElement) : null;
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1204
|
+
|
|
1205
|
+
private _captureOriginFallbacks(origin: HTMLElement): void {
|
|
1206
|
+
this._originFallbacks = [];
|
|
1207
|
+
let current: HTMLElement | null = this._getComposedParent(origin);
|
|
1208
|
+
while (current && current !== document.body && current !== document.documentElement) {
|
|
1209
|
+
this._originFallbacks.push(current);
|
|
1210
|
+
current = this._getComposedParent(current);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
private _getComposedParent(el: HTMLElement): HTMLElement | null {
|
|
1215
|
+
if (el.parentElement) return el.parentElement;
|
|
1216
|
+
const root = el.getRootNode();
|
|
1217
|
+
return root instanceof ShadowRoot ? (root.host as HTMLElement) : null;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1174
1220
|
private _resolveClosingTarget(): HTMLElement | null {
|
|
1175
1221
|
if (this.target) this._resolveTarget();
|
|
1176
1222
|
const candidates = [this._targetEl, ...this._originFallbacks].filter(
|
|
1177
1223
|
(candidate, index, all): candidate is HTMLElement => !!candidate && all.indexOf(candidate) === index
|
|
1178
1224
|
);
|
|
1225
|
+
|
|
1226
|
+
// Si el trigger original sigue en el DOM pero salio del viewport, no se
|
|
1227
|
+
// debe sustituir por uno de sus ancestros visibles. Ese fallback haria que
|
|
1228
|
+
// el modal colapsara hacia un contenedor que nunca fue su origen visual.
|
|
1229
|
+
// En este caso el cierre seguro es la animacion sin origen (fade + scale).
|
|
1230
|
+
if (this._targetEl?.isConnected && !this._intersectsViewport(this._targetEl)) {
|
|
1231
|
+
this._debugMorph('close', null, 'fade', candidates);
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1179
1235
|
const resolved = candidates.find((candidate) => this._isUsableClosingTarget(candidate)) ?? null;
|
|
1180
1236
|
const strategy = !resolved ? 'fade' : resolved === this._targetEl ? 'target' : 'ancestor';
|
|
1181
1237
|
this._debugMorph('close', resolved, strategy, candidates);
|
|
1182
1238
|
return resolved;
|
|
1183
1239
|
}
|
|
1184
1240
|
|
|
1185
|
-
private
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
const rect = element.getBoundingClientRect();
|
|
1194
|
-
return {
|
|
1195
|
-
index,
|
|
1196
|
-
element: `${element.tagName.toLowerCase()}${element.id ? `#${element.id}` : ''}`,
|
|
1197
|
-
connected: element.isConnected,
|
|
1198
|
-
usable: this._isUsableClosingTarget(element),
|
|
1199
|
-
x: Math.round(rect.x),
|
|
1200
|
-
y: Math.round(rect.y),
|
|
1201
|
-
width: Math.round(rect.width),
|
|
1202
|
-
height: Math.round(rect.height)
|
|
1203
|
-
};
|
|
1204
|
-
});
|
|
1205
|
-
console.groupCollapsed(`[moni-morph-modal] ${phase} → ${strategy}`);
|
|
1206
|
-
console.log('Origin:', this._targetEl);
|
|
1207
|
-
console.log('Resolved morph target:', resolved);
|
|
1208
|
-
console.table(rows);
|
|
1209
|
-
console.groupEnd();
|
|
1241
|
+
private _intersectsViewport(el: HTMLElement): boolean {
|
|
1242
|
+
const rect = el.getBoundingClientRect();
|
|
1243
|
+
return rect.width >= 2
|
|
1244
|
+
&& rect.height >= 2
|
|
1245
|
+
&& rect.right > 0
|
|
1246
|
+
&& rect.bottom > 0
|
|
1247
|
+
&& rect.left < window.innerWidth
|
|
1248
|
+
&& rect.top < window.innerHeight;
|
|
1210
1249
|
}
|
|
1211
|
-
|
|
1250
|
+
|
|
1251
|
+
private _debugMorph(
|
|
1252
|
+
phase: 'open' | 'close',
|
|
1253
|
+
resolved: HTMLElement | null,
|
|
1254
|
+
strategy: 'target' | 'ancestor' | 'fade',
|
|
1255
|
+
candidates: HTMLElement[]
|
|
1256
|
+
): void {
|
|
1257
|
+
if (!this.debug) return;
|
|
1258
|
+
const rows = candidates.map((element, index) => {
|
|
1259
|
+
const rect = element.getBoundingClientRect();
|
|
1260
|
+
return {
|
|
1261
|
+
index,
|
|
1262
|
+
element: `${element.tagName.toLowerCase()}${element.id ? `#${element.id}` : ''}`,
|
|
1263
|
+
connected: element.isConnected,
|
|
1264
|
+
usable: this._isUsableClosingTarget(element),
|
|
1265
|
+
x: Math.round(rect.x),
|
|
1266
|
+
y: Math.round(rect.y),
|
|
1267
|
+
width: Math.round(rect.width),
|
|
1268
|
+
height: Math.round(rect.height)
|
|
1269
|
+
};
|
|
1270
|
+
});
|
|
1271
|
+
console.groupCollapsed(`[moni-morph-modal] ${phase} → ${strategy}`);
|
|
1272
|
+
console.log('Origin:', this._targetEl);
|
|
1273
|
+
console.log('Resolved morph target:', resolved);
|
|
1274
|
+
console.table(rows);
|
|
1275
|
+
console.groupEnd();
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1212
1278
|
private _isUsableClosingTarget(el: HTMLElement | null): el is HTMLElement {
|
|
1213
1279
|
if (!el?.isConnected) return false;
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
if (
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
if (parts.
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
saveAndHide(
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
if (parts.
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
restore(
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
// =========================================================================
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
el.
|
|
1375
|
-
el.
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
el.style.
|
|
1381
|
-
el.style.
|
|
1382
|
-
el.style.
|
|
1383
|
-
el.style.
|
|
1384
|
-
el.style.
|
|
1385
|
-
el.style.
|
|
1386
|
-
el.style.
|
|
1387
|
-
el.style.
|
|
1388
|
-
el.style.
|
|
1389
|
-
el.style.
|
|
1390
|
-
el.style.
|
|
1391
|
-
|
|
1392
|
-
el
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
el.
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
const
|
|
1409
|
-
span.
|
|
1410
|
-
|
|
1411
|
-
span.style.
|
|
1412
|
-
span.style.
|
|
1413
|
-
span.style.
|
|
1414
|
-
span.style.
|
|
1415
|
-
span.style.
|
|
1416
|
-
span.style.
|
|
1417
|
-
span.style.
|
|
1418
|
-
span.style.
|
|
1419
|
-
span.style.
|
|
1420
|
-
span.style.
|
|
1421
|
-
span.style.
|
|
1422
|
-
span.style.
|
|
1423
|
-
span.style.
|
|
1424
|
-
span.style.
|
|
1425
|
-
span.style.
|
|
1426
|
-
span.style.
|
|
1427
|
-
|
|
1428
|
-
span.style.
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
el.appendChild(
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
el.style.
|
|
1438
|
-
el.style.
|
|
1439
|
-
el.style.
|
|
1440
|
-
el.style.
|
|
1441
|
-
el.style.
|
|
1442
|
-
el.style.
|
|
1443
|
-
el.style.
|
|
1444
|
-
|
|
1445
|
-
el
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
el.style.
|
|
1455
|
-
el.style.
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
);
|
|
1464
|
-
if (!
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
wrapper.style.
|
|
1529
|
-
wrapper
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
*
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
snapshot.
|
|
1760
|
-
//
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
if (
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
const
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
//
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
:
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
clone.
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
clone.
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
c === '
|
|
1852
|
-
c
|
|
1853
|
-
c
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
let
|
|
1874
|
-
let
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
if (values?.length ===
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
const
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
};
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
el.style.
|
|
1906
|
-
el.style.
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1280
|
+
if (!this._intersectsViewport(el)) return false;
|
|
1281
|
+
let current: HTMLElement | null = el;
|
|
1282
|
+
while (current && current !== document.body) {
|
|
1283
|
+
const style = getComputedStyle(current);
|
|
1284
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.contentVisibility === 'hidden') return false;
|
|
1285
|
+
if (Number.parseFloat(style.opacity || '1') === 0 && !this._targetStylesCache.has(current)) return false;
|
|
1286
|
+
current = this._getComposedParent(current);
|
|
1287
|
+
}
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
toggle(): void {
|
|
1292
|
+
if (this._visible || this.open) this.hide();
|
|
1293
|
+
else this.show();
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
private _handleDocKeydown(e: KeyboardEvent): void {
|
|
1297
|
+
if (e.key !== 'Escape' || !this.closeOnEsc) return;
|
|
1298
|
+
const top = MoniMorphModal._openStack[MoniMorphModal._openStack.length - 1];
|
|
1299
|
+
if (top !== this) return;
|
|
1300
|
+
e.stopPropagation();
|
|
1301
|
+
this.hide();
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Fase "First" de la animación (Cierre del Trigger):
|
|
1306
|
+
* Determina cómo desaparecer u ocultar el botón original mientras el modal ocupa su lugar.
|
|
1307
|
+
* Mantiene el cache de opacidades y colores originales para poder restaurarlos
|
|
1308
|
+
* idénticamente al cerrar el modal (evitando saltos visuales).
|
|
1309
|
+
*/
|
|
1310
|
+
private _hideTargetContent() {
|
|
1311
|
+
if (!this._targetEl) return;
|
|
1312
|
+
|
|
1313
|
+
const saveAndHide = (el: HTMLElement, prop: 'opacity' | 'color') => {
|
|
1314
|
+
if (!this._targetStylesCache.has(el)) {
|
|
1315
|
+
this._targetStylesCache.set(el, { opacity: el.style.opacity, color: el.style.color });
|
|
1316
|
+
}
|
|
1317
|
+
if (prop === 'opacity') el.style.opacity = '0';
|
|
1318
|
+
if (prop === 'color') el.style.color = 'transparent';
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
if (this.hideTarget) {
|
|
1322
|
+
saveAndHide(this._targetEl, 'opacity');
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (this._targetEl.tagName === 'MONI-BUTTON') {
|
|
1327
|
+
const parts = this._getMoniButtonParts();
|
|
1328
|
+
if (parts.label) saveAndHide(parts.label as HTMLElement, 'opacity');
|
|
1329
|
+
if (parts.icon) saveAndHide(parts.icon as HTMLElement, 'opacity');
|
|
1330
|
+
if (parts.iconTrailing) saveAndHide(parts.iconTrailing as HTMLElement, 'opacity');
|
|
1331
|
+
} else if (this._targetEl.tagName === 'MONI-FAB') {
|
|
1332
|
+
const icon = this._targetEl.shadowRoot?.querySelector('.icon') as HTMLElement | null;
|
|
1333
|
+
const label = this._targetEl.shadowRoot?.querySelector('.label') as HTMLElement | null;
|
|
1334
|
+
if (icon) saveAndHide(icon, 'opacity');
|
|
1335
|
+
if (label) saveAndHide(label, 'opacity');
|
|
1336
|
+
} else {
|
|
1337
|
+
saveAndHide(this._targetEl, 'color');
|
|
1338
|
+
const icon = this._targetEl.querySelector('moni-icon, svg, [class*="icon"]');
|
|
1339
|
+
if (icon) saveAndHide(icon as HTMLElement, 'opacity');
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Restaura puramente los estilos originales del botón Trigger tras terminar
|
|
1345
|
+
* la animación de cierre, leyendo las opacidades/colores almacenados en caché.
|
|
1346
|
+
*/
|
|
1347
|
+
private _restoreTargetContent() {
|
|
1348
|
+
if (!this._targetEl) return;
|
|
1349
|
+
|
|
1350
|
+
const restore = (el: HTMLElement) => {
|
|
1351
|
+
if (this._targetStylesCache.has(el)) {
|
|
1352
|
+
const cache = this._targetStylesCache.get(el)!;
|
|
1353
|
+
el.style.opacity = cache.opacity;
|
|
1354
|
+
el.style.color = cache.color;
|
|
1355
|
+
this._targetStylesCache.delete(el);
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
|
|
1359
|
+
if (this.hideTarget) {
|
|
1360
|
+
restore(this._targetEl);
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
if (this._targetEl.tagName === 'MONI-BUTTON') {
|
|
1365
|
+
const parts = this._getMoniButtonParts();
|
|
1366
|
+
if (parts.label) restore(parts.label as HTMLElement);
|
|
1367
|
+
if (parts.icon) restore(parts.icon as HTMLElement);
|
|
1368
|
+
if (parts.iconTrailing) restore(parts.iconTrailing as HTMLElement);
|
|
1369
|
+
} else if (this._targetEl.tagName === 'MONI-FAB') {
|
|
1370
|
+
const icon = this._targetEl.shadowRoot?.querySelector('.icon') as HTMLElement | null;
|
|
1371
|
+
const label = this._targetEl.shadowRoot?.querySelector('.label') as HTMLElement | null;
|
|
1372
|
+
if (icon) restore(icon);
|
|
1373
|
+
if (label) restore(label);
|
|
1374
|
+
} else {
|
|
1375
|
+
restore(this._targetEl);
|
|
1376
|
+
const icon = this._targetEl.querySelector('moni-icon, svg, [class*="icon"]');
|
|
1377
|
+
if (icon) restore(icon as HTMLElement);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
private _onHeaderSlotChange(e: Event): void {
|
|
1382
|
+
const slot = e.target as HTMLSlotElement;
|
|
1383
|
+
this._hasHeader = slot.assignedNodes({ flatten: true }).length > 0;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
private _onFooterSlotChange(e: Event): void {
|
|
1387
|
+
const slot = e.target as HTMLSlotElement;
|
|
1388
|
+
this._hasFooter = slot.assignedNodes({ flatten: true }).length > 0;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
private _onTriggerLabelSlotChange(e: Event): void {
|
|
1392
|
+
const slot = e.target as HTMLSlotElement;
|
|
1393
|
+
this._hasTriggerLabel = slot.assignedNodes({ flatten: true }).length > 0;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
private _getTriggerLabelNodes(): Node[] {
|
|
1397
|
+
const slot = this.shadowRoot?.querySelector(
|
|
1398
|
+
'slot[name="trigger-label"]'
|
|
1399
|
+
) as HTMLSlotElement | undefined;
|
|
1400
|
+
return slot?.assignedNodes({ flatten: true }) ?? [];
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
private _shouldMorphLabel(): boolean {
|
|
1404
|
+
return (
|
|
1405
|
+
this.morphLabel && !!this._targetEl && (this._hasHeader || this._hasTriggerLabel)
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// =========================================================================
|
|
1410
|
+
// LABEL FLIP HELPERS
|
|
1411
|
+
// =========================================================================
|
|
1412
|
+
|
|
1413
|
+
private _cleanupMorphElements(): void {
|
|
1414
|
+
if (this._targetSnapshot) {
|
|
1415
|
+
this._targetSnapshot.remove();
|
|
1416
|
+
this._targetSnapshot = null;
|
|
1417
|
+
}
|
|
1418
|
+
if (this._morphEls.text) {
|
|
1419
|
+
this._morphEls.text.remove();
|
|
1420
|
+
this._morphEls.text = null;
|
|
1421
|
+
}
|
|
1422
|
+
if (this._morphEls.icon) {
|
|
1423
|
+
this._morphEls.icon.remove();
|
|
1424
|
+
this._morphEls.icon = null;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
private _animateLabelOpen(): void {
|
|
1429
|
+
// No-op: el morphing de la etiqueta es manejado por el timeline unificado FLIP en show()
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
private _animateLabelClose(): void {
|
|
1433
|
+
// No-op: el morphing de la etiqueta es manejado por el timeline unificado FLIP en hide()
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
private _createMorphText(sourceText: string, sourceEl: Element): HTMLElement {
|
|
1437
|
+
const el = document.createElement('div');
|
|
1438
|
+
el.className = 'morph-text';
|
|
1439
|
+
el.setAttribute('data-flip-id', `morph-text-${this.target}`);
|
|
1440
|
+
el.setAttribute('aria-hidden', 'true');
|
|
1441
|
+
el.textContent = sourceText;
|
|
1442
|
+
|
|
1443
|
+
const computed = window.getComputedStyle(sourceEl);
|
|
1444
|
+
el.style.position = 'fixed';
|
|
1445
|
+
el.style.pointerEvents = 'none';
|
|
1446
|
+
el.style.zIndex = String(this._zIndex + 10);
|
|
1447
|
+
el.style.display = 'inline-flex';
|
|
1448
|
+
el.style.alignItems = computed.alignItems !== 'normal' ? computed.alignItems : 'center';
|
|
1449
|
+
el.style.justifyContent = computed.justifyContent !== 'normal' ? computed.justifyContent : 'flex-start';
|
|
1450
|
+
el.style.whiteSpace = 'nowrap';
|
|
1451
|
+
el.style.color = computed.color || 'inherit';
|
|
1452
|
+
el.style.fontFamily = computed.fontFamily || 'inherit';
|
|
1453
|
+
el.style.fontSize = computed.fontSize || 'inherit';
|
|
1454
|
+
el.style.fontWeight = computed.fontWeight || 'inherit';
|
|
1455
|
+
el.style.lineHeight = computed.lineHeight || 'inherit';
|
|
1456
|
+
el.style.letterSpacing = computed.letterSpacing || 'inherit';
|
|
1457
|
+
|
|
1458
|
+
this.shadowRoot!.appendChild(el);
|
|
1459
|
+
this._morphEls.text = el;
|
|
1460
|
+
return el;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
private _createMorphIcon(sourceIcon: Element): HTMLElement {
|
|
1464
|
+
const el = document.createElement('div');
|
|
1465
|
+
el.className = 'morph-icon';
|
|
1466
|
+
el.setAttribute('data-flip-id', `morph-icon-${this.target}`);
|
|
1467
|
+
el.setAttribute('aria-hidden', 'true');
|
|
1468
|
+
|
|
1469
|
+
// Para <moni-icon> renderizamos el glifo directamente para evitar
|
|
1470
|
+
// esperar a que hidrate su shadow DOM.
|
|
1471
|
+
if (sourceIcon.tagName === 'MONI-ICON') {
|
|
1472
|
+
const span = document.createElement('span');
|
|
1473
|
+
span.textContent = sourceIcon.getAttribute('name') || '';
|
|
1474
|
+
const computed = window.getComputedStyle(sourceIcon);
|
|
1475
|
+
span.style.display = 'inline-flex';
|
|
1476
|
+
span.style.alignItems = 'center';
|
|
1477
|
+
span.style.justifyContent = 'center';
|
|
1478
|
+
span.style.fontFamily = computed.fontFamily || 'var(--font-icon)';
|
|
1479
|
+
span.style.fontSize = computed.fontSize || '1.125rem';
|
|
1480
|
+
span.style.inlineSize = computed.inlineSize || '1.125rem';
|
|
1481
|
+
span.style.blockSize = computed.blockSize || '1.125rem';
|
|
1482
|
+
span.style.lineHeight = '1';
|
|
1483
|
+
span.style.color = 'inherit';
|
|
1484
|
+
span.style.fontWeight = 'normal';
|
|
1485
|
+
span.style.fontStyle = 'normal';
|
|
1486
|
+
span.style.letterSpacing = 'normal';
|
|
1487
|
+
span.style.textTransform = 'none';
|
|
1488
|
+
span.style.whiteSpace = 'nowrap';
|
|
1489
|
+
span.style.wordWrap = 'normal';
|
|
1490
|
+
span.style.direction = 'ltr';
|
|
1491
|
+
span.style.fontFeatureSettings = "'liga'";
|
|
1492
|
+
span.style.fontVariationSettings =
|
|
1493
|
+
"'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24";
|
|
1494
|
+
span.style.setProperty('-webkit-font-smoothing', 'antialiased');
|
|
1495
|
+
el.appendChild(span);
|
|
1496
|
+
} else {
|
|
1497
|
+
el.appendChild(sourceIcon.cloneNode(true) as HTMLElement);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
const computed = window.getComputedStyle(sourceIcon);
|
|
1501
|
+
el.style.position = 'fixed';
|
|
1502
|
+
el.style.pointerEvents = 'none';
|
|
1503
|
+
el.style.zIndex = String(this._zIndex + 10);
|
|
1504
|
+
el.style.display = 'inline-flex';
|
|
1505
|
+
el.style.alignItems = 'center';
|
|
1506
|
+
el.style.justifyContent = 'center';
|
|
1507
|
+
el.style.color = computed.color || 'inherit';
|
|
1508
|
+
el.style.inlineSize = computed.inlineSize || '1.125rem';
|
|
1509
|
+
el.style.blockSize = computed.blockSize || '1.125rem';
|
|
1510
|
+
|
|
1511
|
+
this.shadowRoot!.appendChild(el);
|
|
1512
|
+
this._morphEls.icon = el;
|
|
1513
|
+
return el;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
private _positionElement(el: HTMLElement, rect: DOMRect): void {
|
|
1517
|
+
const localRect = this._toPanelCoordinateRect(rect);
|
|
1518
|
+
el.style.top = `${localRect.top}px`;
|
|
1519
|
+
el.style.left = `${localRect.left}px`;
|
|
1520
|
+
el.style.width = `${localRect.width}px`;
|
|
1521
|
+
el.style.height = `${localRect.height}px`;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
private _toPanelCoordinateRect(rect: DOMRect): DOMRect {
|
|
1525
|
+
const ancestor = this.parentElement?.closest<HTMLElement>(
|
|
1526
|
+
'.lab-canvas, [data-morph-coordinate-space]'
|
|
1527
|
+
);
|
|
1528
|
+
if (!ancestor) return rect;
|
|
1529
|
+
const transform = getComputedStyle(ancestor).transform;
|
|
1530
|
+
if (!transform || transform === 'none') return rect;
|
|
1531
|
+
let scaleX = 1;
|
|
1532
|
+
let scaleY = 1;
|
|
1533
|
+
if (typeof DOMMatrix !== 'undefined') {
|
|
1534
|
+
const matrix = new DOMMatrix(transform);
|
|
1535
|
+
scaleX = Math.hypot(matrix.a, matrix.b) || 1;
|
|
1536
|
+
scaleY = Math.hypot(matrix.c, matrix.d) || 1;
|
|
1537
|
+
} else {
|
|
1538
|
+
const matrixMatch = transform.match(/^matrix\(([^)]+)\)$/);
|
|
1539
|
+
const scaleMatch = transform.match(/^scale\(([^)]+)\)$/);
|
|
1540
|
+
if (matrixMatch) {
|
|
1541
|
+
const values = matrixMatch[1].split(',').map(Number);
|
|
1542
|
+
scaleX = Math.hypot(values[0], values[1]) || 1;
|
|
1543
|
+
scaleY = Math.hypot(values[2], values[3]) || 1;
|
|
1544
|
+
} else if (scaleMatch) {
|
|
1545
|
+
const values = scaleMatch[1].split(/[,\s]+/).map(Number);
|
|
1546
|
+
scaleX = values[0] || 1;
|
|
1547
|
+
scaleY = values[1] || scaleX;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
const ancestorRect = ancestor.getBoundingClientRect();
|
|
1551
|
+
return new DOMRect(
|
|
1552
|
+
(rect.left - ancestorRect.left) / scaleX,
|
|
1553
|
+
(rect.top - ancestorRect.top) / scaleY,
|
|
1554
|
+
rect.width / scaleX,
|
|
1555
|
+
rect.height / scaleY
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
private _getMoniButtonParts(): {
|
|
1560
|
+
label: Element | null;
|
|
1561
|
+
icon: Element | null;
|
|
1562
|
+
iconTrailing: Element | null;
|
|
1563
|
+
} {
|
|
1564
|
+
const sr = this._targetEl?.shadowRoot;
|
|
1565
|
+
return {
|
|
1566
|
+
label: sr?.querySelector('[part="label"]') ?? null,
|
|
1567
|
+
icon: sr?.querySelector('[part="icon"]') ?? null,
|
|
1568
|
+
iconTrailing: sr?.querySelector('[part="icon-trailing"]') ?? null
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
private _resolveMorphSource(): {
|
|
1573
|
+
text: { content: string; rect: DOMRect; sourceEl: Element } | null;
|
|
1574
|
+
icon: { element: Element; rect: DOMRect } | null;
|
|
1575
|
+
} {
|
|
1576
|
+
const result: {
|
|
1577
|
+
text: { content: string; rect: DOMRect; sourceEl: Element } | null;
|
|
1578
|
+
icon: { element: Element; rect: DOMRect } | null;
|
|
1579
|
+
} = { text: null, icon: null };
|
|
1580
|
+
|
|
1581
|
+
if (!this._targetEl) return result;
|
|
1582
|
+
|
|
1583
|
+
// Slot trigger-label tiene prioridad.
|
|
1584
|
+
if (this._hasTriggerLabel) {
|
|
1585
|
+
const nodes = this._getTriggerLabelNodes();
|
|
1586
|
+
if (nodes.length > 0) {
|
|
1587
|
+
const wrapper = document.createElement('div');
|
|
1588
|
+
for (const node of nodes) {
|
|
1589
|
+
wrapper.appendChild(node.cloneNode(true));
|
|
1590
|
+
}
|
|
1591
|
+
// Posicionamos el wrapper sobre el target para la medición inicial.
|
|
1592
|
+
wrapper.style.position = 'fixed';
|
|
1593
|
+
wrapper.style.visibility = 'hidden';
|
|
1594
|
+
wrapper.style.pointerEvents = 'none';
|
|
1595
|
+
this.shadowRoot!.appendChild(wrapper);
|
|
1596
|
+
const rect = this._targetEl.getBoundingClientRect();
|
|
1597
|
+
wrapper.remove();
|
|
1598
|
+
|
|
1599
|
+
result.text = {
|
|
1600
|
+
content: wrapper.textContent?.trim() || '',
|
|
1601
|
+
rect,
|
|
1602
|
+
sourceEl: this._targetEl
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
return result;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// <moni-button>: extraemos label e icono desde su shadow DOM.
|
|
1609
|
+
if (this._targetEl.tagName === 'MONI-BUTTON') {
|
|
1610
|
+
const parts = this._getMoniButtonParts();
|
|
1611
|
+
if (parts.label) {
|
|
1612
|
+
result.text = {
|
|
1613
|
+
content: this._targetEl.textContent?.trim() || '',
|
|
1614
|
+
rect: parts.label.getBoundingClientRect(),
|
|
1615
|
+
sourceEl: parts.label
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
if (parts.icon) {
|
|
1619
|
+
const innerIcon = parts.icon.querySelector('moni-icon, svg') || parts.icon;
|
|
1620
|
+
result.icon = {
|
|
1621
|
+
element: innerIcon,
|
|
1622
|
+
rect: innerIcon.getBoundingClientRect()
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
return result;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// Target arbitrario.
|
|
1629
|
+
let sourceEl: Element = this._targetEl;
|
|
1630
|
+
if (this.morphLabelSelector) {
|
|
1631
|
+
const selected = this._targetEl.querySelector(this.morphLabelSelector);
|
|
1632
|
+
if (selected) sourceEl = selected;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
const text = sourceEl.textContent?.trim() || '';
|
|
1636
|
+
if (text) {
|
|
1637
|
+
result.text = {
|
|
1638
|
+
content: text,
|
|
1639
|
+
rect: sourceEl.getBoundingClientRect(),
|
|
1640
|
+
sourceEl
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// Buscamos un icono dentro del target.
|
|
1645
|
+
const iconEl = sourceEl.querySelector('moni-icon, svg, [class*="icon"]');
|
|
1646
|
+
if (iconEl) {
|
|
1647
|
+
result.icon = {
|
|
1648
|
+
element: iconEl,
|
|
1649
|
+
rect: iconEl.getBoundingClientRect()
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
return result;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
private _measureHeaderTextRect(): DOMRect | null {
|
|
1657
|
+
if (!this._header) return null;
|
|
1658
|
+
|
|
1659
|
+
const slot = this.shadowRoot?.querySelector(
|
|
1660
|
+
'slot[name="header"]'
|
|
1661
|
+
) as HTMLSlotElement | undefined;
|
|
1662
|
+
const nodes = slot?.assignedNodes({ flatten: true }) ?? [];
|
|
1663
|
+
|
|
1664
|
+
// Buscamos el primer nodo con texto visible.
|
|
1665
|
+
for (const node of nodes) {
|
|
1666
|
+
if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) {
|
|
1667
|
+
const range = document.createRange();
|
|
1668
|
+
range.selectNode(node);
|
|
1669
|
+
return range.getBoundingClientRect();
|
|
1670
|
+
}
|
|
1671
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1672
|
+
const el = node as Element;
|
|
1673
|
+
if (el.tagName === 'MONI-ICON' || el.tagName === 'SVG') {
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
const childNodes = Array.from(el.childNodes);
|
|
1677
|
+
for (const child of childNodes) {
|
|
1678
|
+
if (child.nodeType === Node.TEXT_NODE && child.textContent?.trim()) {
|
|
1679
|
+
const range = document.createRange();
|
|
1680
|
+
range.selectNode(child);
|
|
1681
|
+
return range.getBoundingClientRect();
|
|
1682
|
+
}
|
|
1683
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
1684
|
+
const cel = child as Element;
|
|
1685
|
+
if (cel.tagName !== 'MONI-ICON' && cel.tagName !== 'SVG' && cel.textContent?.trim()) {
|
|
1686
|
+
return cel.getBoundingClientRect();
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
if (el.textContent?.trim()) {
|
|
1691
|
+
return el.getBoundingClientRect();
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
return this._header.getBoundingClientRect();
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
private _measureHeaderIconRect(): DOMRect | null {
|
|
1700
|
+
const textRect = this._measureHeaderTextRect();
|
|
1701
|
+
if (!textRect) return null;
|
|
1702
|
+
|
|
1703
|
+
const slot = this.shadowRoot?.querySelector(
|
|
1704
|
+
'slot[name="header"]'
|
|
1705
|
+
) as HTMLSlotElement | undefined;
|
|
1706
|
+
const nodes = slot?.assignedNodes({ flatten: true }) ?? [];
|
|
1707
|
+
|
|
1708
|
+
// Si el header ya tiene un icono, lo usamos como destino.
|
|
1709
|
+
for (const node of nodes) {
|
|
1710
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1711
|
+
const el = node as Element;
|
|
1712
|
+
if (el.tagName === 'MONI-ICON' || el.tagName === 'SVG') {
|
|
1713
|
+
return el.getBoundingClientRect();
|
|
1714
|
+
}
|
|
1715
|
+
const nestedIcon = el.querySelector('moni-icon, svg');
|
|
1716
|
+
if (nestedIcon) {
|
|
1717
|
+
return nestedIcon.getBoundingClientRect();
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// Si no hay icono en el header, calculamos una posición a la
|
|
1723
|
+
// izquierda del texto, alineada verticalmente.
|
|
1724
|
+
const iconSize = 24;
|
|
1725
|
+
const gap = 12;
|
|
1726
|
+
return new DOMRect(
|
|
1727
|
+
textRect.left - iconSize - gap,
|
|
1728
|
+
textRect.top + textRect.height / 2 - iconSize / 2,
|
|
1729
|
+
iconSize,
|
|
1730
|
+
iconSize
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
private _findSolidBackground(el: Element): string | null {
|
|
1735
|
+
const elements = [el, ...Array.from(el.querySelectorAll('*'))];
|
|
1736
|
+
for (const e of elements) {
|
|
1737
|
+
const bg = window.getComputedStyle(e).backgroundColor;
|
|
1738
|
+
if (!this._isColorTransparent(bg)) {
|
|
1739
|
+
return bg;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
// Si es un Web Component, revisamos su shadowRoot de forma superficial
|
|
1743
|
+
if (el.shadowRoot) {
|
|
1744
|
+
const shadowElements = Array.from(el.shadowRoot.querySelectorAll('*'));
|
|
1745
|
+
for (const e of shadowElements) {
|
|
1746
|
+
const bg = window.getComputedStyle(e).backgroundColor;
|
|
1747
|
+
if (!this._isColorTransparent(bg)) {
|
|
1748
|
+
return bg;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return null;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
private _getTargetState(el: HTMLElement): TargetState {
|
|
1756
|
+
// Los Web Components interactivos exponen un host transparente. El morph
|
|
1757
|
+
// debe usar la superficie pintada real dentro de su Shadow DOM.
|
|
1758
|
+
const visualEl = this._resolveVisualTarget(el);
|
|
1759
|
+
const rect = visualEl.getBoundingClientRect();
|
|
1760
|
+
|
|
1761
|
+
let bg = window.getComputedStyle(visualEl).backgroundColor;
|
|
1762
|
+
|
|
1763
|
+
// Si es transparente, intentamos buscar en pseudo-elementos (usado en botones Material 3)
|
|
1764
|
+
if (this._isColorTransparent(bg)) {
|
|
1765
|
+
const beforeBg = window.getComputedStyle(visualEl, '::before').backgroundColor;
|
|
1766
|
+
if (!this._isColorTransparent(beforeBg)) {
|
|
1767
|
+
bg = beforeBg;
|
|
1768
|
+
} else {
|
|
1769
|
+
const afterBg = window.getComputedStyle(visualEl, '::after').backgroundColor;
|
|
1770
|
+
if (!this._isColorTransparent(afterBg)) {
|
|
1771
|
+
bg = afterBg;
|
|
1772
|
+
} else {
|
|
1773
|
+
// Buscamos el primer elemento con fondo sólido en el árbol del objetivo
|
|
1774
|
+
const solidBg = this._findSolidBackground(visualEl);
|
|
1775
|
+
if (solidBg) bg = solidBg;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// Si sigue siendo transparente, evitamos que el modal se vuelva transparente
|
|
1781
|
+
// leyendo su propio color computado actual para que mantenga su opacidad.
|
|
1782
|
+
if (this._isColorTransparent(bg) && this._panel) {
|
|
1783
|
+
bg = window.getComputedStyle(this._panel).backgroundColor;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
const computed = window.getComputedStyle(visualEl);
|
|
1787
|
+
|
|
1788
|
+
return {
|
|
1789
|
+
rect,
|
|
1790
|
+
backgroundColor: bg,
|
|
1791
|
+
color: computed.color || 'var(--on-surface)',
|
|
1792
|
+
borderRadius: computed.borderRadius || '1.25rem',
|
|
1793
|
+
boxShadow: computed.boxShadow || 'none'
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
private _resolveVisualTarget(el: HTMLElement): HTMLElement {
|
|
1798
|
+
if (el.tagName === 'MONI-BUTTON') {
|
|
1799
|
+
return (el.shadowRoot?.querySelector('.button') as HTMLElement | null) ?? el;
|
|
1800
|
+
}
|
|
1801
|
+
if (el.tagName === 'MONI-FAB') {
|
|
1802
|
+
return (el.shadowRoot?.querySelector('button') as HTMLElement | null) ?? el;
|
|
1803
|
+
}
|
|
1804
|
+
if (el.tagName === 'MONI-FAB-MENU') {
|
|
1805
|
+
const trigger = el.shadowRoot?.querySelector('.trigger') as HTMLElement | null;
|
|
1806
|
+
return (trigger?.shadowRoot?.querySelector('button') as HTMLElement | null) ?? trigger ?? el;
|
|
1807
|
+
}
|
|
1808
|
+
return el;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/**
|
|
1812
|
+
* Crea una copia visual inerte del árbol compuesto del target. Los `<slot>`
|
|
1813
|
+
* se sustituyen por sus nodos asignados, por lo que la captura incluye tanto
|
|
1814
|
+
* contenido light DOM como superficies internas de Web Components.
|
|
1815
|
+
*/
|
|
1816
|
+
private _createTargetSnapshot(source: HTMLElement, rect: DOMRect): HTMLElement {
|
|
1817
|
+
this._targetSnapshot?.remove();
|
|
1818
|
+
const snapshot = document.createElement('div');
|
|
1819
|
+
snapshot.className = 'morph-target-snapshot';
|
|
1820
|
+
snapshot.setAttribute('aria-hidden', 'true');
|
|
1821
|
+
const clone = this._cloneComposedNode(source);
|
|
1822
|
+
if (clone) {
|
|
1823
|
+
snapshot.appendChild(clone);
|
|
1824
|
+
// La geometría, el color y la elevación pertenecen al panel animado.
|
|
1825
|
+
// Repetirlos en el snapshot crea una segunda píldora encima del morph.
|
|
1826
|
+
// En medios visuales, en cambio, la propia superficie sí es contenido.
|
|
1827
|
+
if (
|
|
1828
|
+
clone instanceof HTMLElement &&
|
|
1829
|
+
!['IMG', 'VIDEO', 'CANVAS', 'SVG', 'PICTURE'].includes(source.tagName)
|
|
1830
|
+
) {
|
|
1831
|
+
Object.assign(clone.style, {
|
|
1832
|
+
background: 'transparent',
|
|
1833
|
+
backgroundColor: 'transparent',
|
|
1834
|
+
boxShadow: 'none',
|
|
1835
|
+
borderColor: 'transparent',
|
|
1836
|
+
outline: 'none',
|
|
1837
|
+
position: 'relative',
|
|
1838
|
+
inset: 'auto',
|
|
1839
|
+
transform: 'none',
|
|
1840
|
+
opacity: '1',
|
|
1841
|
+
visibility: 'visible',
|
|
1842
|
+
width: '100%',
|
|
1843
|
+
height: '100%'
|
|
1844
|
+
});
|
|
1845
|
+
if (source.tagName === 'BUTTON') {
|
|
1846
|
+
clone.style.whiteSpace = 'nowrap';
|
|
1847
|
+
clone.querySelectorAll<HTMLElement>('span, [part~="label"]').forEach((child) => {
|
|
1848
|
+
child.style.whiteSpace = 'nowrap';
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
Object.assign(snapshot.style, {
|
|
1854
|
+
width: `${rect.width}px`,
|
|
1855
|
+
height: `${rect.height}px`,
|
|
1856
|
+
overflow: 'hidden',
|
|
1857
|
+
pointerEvents: 'none'
|
|
1858
|
+
});
|
|
1859
|
+
this._targetSnapshot = snapshot;
|
|
1860
|
+
return snapshot;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
private _cloneComposedNode(source: Node): Node | null {
|
|
1864
|
+
if (source.nodeType === Node.TEXT_NODE) return source.cloneNode(false);
|
|
1865
|
+
if (!(source instanceof Element)) return null;
|
|
1866
|
+
|
|
1867
|
+
if (source instanceof HTMLSlotElement) {
|
|
1868
|
+
const fragment = document.createDocumentFragment();
|
|
1869
|
+
const nodes = source.assignedNodes({ flatten: true });
|
|
1870
|
+
for (const node of nodes.length ? nodes : Array.from(source.childNodes)) {
|
|
1871
|
+
const child = this._cloneComposedNode(node);
|
|
1872
|
+
if (child) fragment.appendChild(child);
|
|
1873
|
+
}
|
|
1874
|
+
return fragment;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// No clonamos directamente un Custom Element con Shadow DOM: su
|
|
1878
|
+
// constructor crearía una segunda sombra y duplicaría el render. Una
|
|
1879
|
+
// superficie neutra recibe en cambio el árbol visual ya compuesto.
|
|
1880
|
+
const clone = (source.shadowRoot
|
|
1881
|
+
? document.createElement('div')
|
|
1882
|
+
: source.cloneNode(false)) as HTMLElement;
|
|
1883
|
+
if (source instanceof HTMLImageElement && clone instanceof HTMLImageElement) {
|
|
1884
|
+
// El snapshot vive menos de medio segundo: no puede depender de que el
|
|
1885
|
+
// lazy-loader observe nuevamente una imagen que ya estaba renderizada.
|
|
1886
|
+
clone.loading = 'eager';
|
|
1887
|
+
clone.decoding = 'sync';
|
|
1888
|
+
if (source.currentSrc) clone.src = source.currentSrc;
|
|
1889
|
+
}
|
|
1890
|
+
clone.removeAttribute('id');
|
|
1891
|
+
clone.removeAttribute('name');
|
|
1892
|
+
clone.setAttribute('aria-hidden', 'true');
|
|
1893
|
+
const computed = window.getComputedStyle(source);
|
|
1894
|
+
for (let index = 0; index < computed.length; index += 1) {
|
|
1895
|
+
const property = computed.item(index);
|
|
1896
|
+
clone.style.setProperty(property, computed.getPropertyValue(property), computed.getPropertyPriority(property));
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
const children = source.shadowRoot
|
|
1900
|
+
? Array.from(source.shadowRoot.childNodes).filter(
|
|
1901
|
+
(node) => !(node instanceof HTMLStyleElement)
|
|
1902
|
+
)
|
|
1903
|
+
: Array.from(source.childNodes);
|
|
1904
|
+
for (const node of children) {
|
|
1905
|
+
const child = this._cloneComposedNode(node);
|
|
1906
|
+
if (child) clone.appendChild(child);
|
|
1907
|
+
}
|
|
1908
|
+
return clone;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
private _isColorTransparent(color: string | null | undefined): boolean {
|
|
1912
|
+
if (!color) return true;
|
|
1913
|
+
const c = color.replace(/\s/g, '').toLowerCase();
|
|
1914
|
+
return (
|
|
1915
|
+
c === 'rgba(0,0,0,0)' ||
|
|
1916
|
+
c === 'transparent' ||
|
|
1917
|
+
c === '' ||
|
|
1918
|
+
c.includes(',0)') ||
|
|
1919
|
+
c.includes('/0)')
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
private _readTransformState(el: HTMLElement): {
|
|
1924
|
+
rotation: number;
|
|
1925
|
+
skewX: number;
|
|
1926
|
+
scaleX: number;
|
|
1927
|
+
scaleY: number;
|
|
1928
|
+
} {
|
|
1929
|
+
const transform = window.getComputedStyle(el).transform;
|
|
1930
|
+
if (!transform || transform === 'none') {
|
|
1931
|
+
return { rotation: 0, skewX: 0, scaleX: 1, scaleY: 1 };
|
|
1932
|
+
}
|
|
1933
|
+
try {
|
|
1934
|
+
const values = transform.match(/matrix(?:3d)?\(([^)]+)\)/)?.[1]
|
|
1935
|
+
.split(',')
|
|
1936
|
+
.map((value) => Number.parseFloat(value.trim()));
|
|
1937
|
+
let a: number;
|
|
1938
|
+
let b: number;
|
|
1939
|
+
let c: number;
|
|
1940
|
+
let d: number;
|
|
1941
|
+
if (values?.length === 6) {
|
|
1942
|
+
[a, b, c, d] = values;
|
|
1943
|
+
} else if (values?.length === 16) {
|
|
1944
|
+
a = values[0];
|
|
1945
|
+
b = values[1];
|
|
1946
|
+
c = values[4];
|
|
1947
|
+
d = values[5];
|
|
1948
|
+
} else {
|
|
1949
|
+
const matrix = new DOMMatrix(transform);
|
|
1950
|
+
({ a, b, c, d } = matrix);
|
|
1951
|
+
}
|
|
1952
|
+
const scaleX = Math.hypot(a, b) || 1;
|
|
1953
|
+
const determinant = a * d - b * c;
|
|
1954
|
+
const scaleY = determinant / scaleX || 1;
|
|
1955
|
+
return {
|
|
1956
|
+
rotation: Math.atan2(b, a) * (180 / Math.PI),
|
|
1957
|
+
skewX:
|
|
1958
|
+
Math.atan2(a * c + b * d, scaleX * scaleX) *
|
|
1959
|
+
(180 / Math.PI),
|
|
1960
|
+
scaleX,
|
|
1961
|
+
scaleY
|
|
1962
|
+
};
|
|
1963
|
+
} catch {
|
|
1964
|
+
return { rotation: 0, skewX: 0, scaleX: 1, scaleY: 1 };
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
private _applyRect(el: HTMLElement, rect: DOMRect): void {
|
|
1969
|
+
el.style.top = `${rect.top}px`;
|
|
1970
|
+
el.style.left = `${rect.left}px`;
|
|
1971
|
+
el.style.width = `${rect.width}px`;
|
|
1972
|
+
el.style.height = `${rect.height}px`;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1911
1975
|
private _usesFullscreen(): boolean {
|
|
1912
1976
|
if (this.fullscreen) return true;
|
|
1913
1977
|
return this.responsiveFullscreen && typeof window !== 'undefined' && window.innerWidth <= this.fullscreenBreakpoint;
|
|
@@ -1918,185 +1982,232 @@ export class MoniMorphModal extends MoniElement {
|
|
|
1918
1982
|
const vh = window.innerHeight;
|
|
1919
1983
|
if (this._usesFullscreen()) return new DOMRect(0, 0, vw, vh);
|
|
1920
1984
|
const padding = 16;
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
case 'viewport-
|
|
1937
|
-
left = vw / 2 - width / 2;
|
|
1938
|
-
top =
|
|
1939
|
-
break;
|
|
1940
|
-
case 'viewport-
|
|
1941
|
-
left = vw / 2 - width / 2;
|
|
1942
|
-
top =
|
|
1943
|
-
break;
|
|
1944
|
-
case 'viewport-
|
|
1945
|
-
left =
|
|
1946
|
-
top = vh
|
|
1947
|
-
break;
|
|
1948
|
-
case 'viewport-
|
|
1949
|
-
left =
|
|
1950
|
-
top = vh / 2 - height / 2;
|
|
1951
|
-
break;
|
|
1952
|
-
case 'viewport-
|
|
1953
|
-
left = padding;
|
|
1954
|
-
top =
|
|
1955
|
-
break;
|
|
1956
|
-
case 'viewport-top-
|
|
1957
|
-
left =
|
|
1958
|
-
top = padding;
|
|
1959
|
-
break;
|
|
1960
|
-
case 'viewport-
|
|
1961
|
-
left = padding;
|
|
1962
|
-
top =
|
|
1963
|
-
break;
|
|
1964
|
-
case 'viewport-bottom-
|
|
1965
|
-
left =
|
|
1966
|
-
top = vh - height - padding;
|
|
1967
|
-
break;
|
|
1968
|
-
case '
|
|
1969
|
-
left =
|
|
1970
|
-
top =
|
|
1971
|
-
break;
|
|
1972
|
-
case '
|
|
1973
|
-
left = targetRect.left + targetRect.width / 2 - width / 2;
|
|
1974
|
-
top =
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
1985
|
+
|
|
1986
|
+
const configuredWidth = this._parseSize(this.expandedWidth, vw, 'width');
|
|
1987
|
+
const configuredHeight = this._parseSize(this.expandedHeight, vh, 'height');
|
|
1988
|
+
let width = this.autoSize && naturalSize ? naturalSize.width : configuredWidth;
|
|
1989
|
+
let height = (this.autoSize || this.autoHeight) && naturalSize
|
|
1990
|
+
? Math.min(naturalSize.height, configuredHeight)
|
|
1991
|
+
: configuredHeight;
|
|
1992
|
+
|
|
1993
|
+
width = Math.min(width, vw - padding * 2);
|
|
1994
|
+
height = Math.min(height, vh - padding * 2);
|
|
1995
|
+
|
|
1996
|
+
let top = targetRect.top;
|
|
1997
|
+
let left = targetRect.left;
|
|
1998
|
+
|
|
1999
|
+
switch (this.placement) {
|
|
2000
|
+
case 'viewport-center':
|
|
2001
|
+
left = vw / 2 - width / 2;
|
|
2002
|
+
top = vh / 2 - height / 2;
|
|
2003
|
+
break;
|
|
2004
|
+
case 'viewport-top':
|
|
2005
|
+
left = vw / 2 - width / 2;
|
|
2006
|
+
top = padding;
|
|
2007
|
+
break;
|
|
2008
|
+
case 'viewport-bottom':
|
|
2009
|
+
left = vw / 2 - width / 2;
|
|
2010
|
+
top = vh - height - padding;
|
|
2011
|
+
break;
|
|
2012
|
+
case 'viewport-left':
|
|
2013
|
+
left = padding;
|
|
2014
|
+
top = vh / 2 - height / 2;
|
|
2015
|
+
break;
|
|
2016
|
+
case 'viewport-right':
|
|
2017
|
+
left = vw - width - padding;
|
|
2018
|
+
top = vh / 2 - height / 2;
|
|
2019
|
+
break;
|
|
2020
|
+
case 'viewport-top-left':
|
|
2021
|
+
left = padding;
|
|
2022
|
+
top = padding;
|
|
2023
|
+
break;
|
|
2024
|
+
case 'viewport-top-right':
|
|
2025
|
+
left = vw - width - padding;
|
|
2026
|
+
top = padding;
|
|
2027
|
+
break;
|
|
2028
|
+
case 'viewport-bottom-left':
|
|
2029
|
+
left = padding;
|
|
2030
|
+
top = vh - height - padding;
|
|
2031
|
+
break;
|
|
2032
|
+
case 'viewport-bottom-right':
|
|
2033
|
+
left = vw - width - padding;
|
|
2034
|
+
top = vh - height - padding;
|
|
2035
|
+
break;
|
|
2036
|
+
case 'center':
|
|
2037
|
+
left = targetRect.left + targetRect.width / 2 - width / 2;
|
|
2038
|
+
top = targetRect.top + targetRect.height / 2 - height / 2;
|
|
2039
|
+
break;
|
|
2040
|
+
case 'top':
|
|
2041
|
+
left = targetRect.left + targetRect.width / 2 - width / 2;
|
|
2042
|
+
top = this.coverTarget
|
|
2043
|
+
? targetRect.top + targetRect.height - height
|
|
2044
|
+
: targetRect.top - height;
|
|
2045
|
+
break;
|
|
2046
|
+
case 'top-start':
|
|
2047
|
+
left = targetRect.left + targetRect.width - width;
|
|
2048
|
+
top = this.coverTarget
|
|
2049
|
+
? targetRect.top + targetRect.height - height
|
|
2050
|
+
: targetRect.top - height;
|
|
2051
|
+
break;
|
|
2052
|
+
case 'top-end':
|
|
2053
|
+
left = targetRect.left;
|
|
2054
|
+
top = this.coverTarget
|
|
2055
|
+
? targetRect.top + targetRect.height - height
|
|
2056
|
+
: targetRect.top - height;
|
|
2057
|
+
break;
|
|
2058
|
+
case 'bottom':
|
|
2059
|
+
left = targetRect.left + targetRect.width / 2 - width / 2;
|
|
2060
|
+
top = this.coverTarget
|
|
2061
|
+
? targetRect.top
|
|
2062
|
+
: targetRect.top + targetRect.height;
|
|
2063
|
+
break;
|
|
2064
|
+
case 'bottom-start':
|
|
2065
|
+
left = targetRect.left + targetRect.width - width;
|
|
2066
|
+
top = this.coverTarget
|
|
2067
|
+
? targetRect.top
|
|
2068
|
+
: targetRect.top + targetRect.height;
|
|
2069
|
+
break;
|
|
2070
|
+
case 'bottom-end':
|
|
2071
|
+
left = targetRect.left;
|
|
2072
|
+
top = this.coverTarget
|
|
2073
|
+
? targetRect.top
|
|
2074
|
+
: targetRect.top + targetRect.height;
|
|
2075
|
+
break;
|
|
2076
|
+
case 'left':
|
|
2077
|
+
left = this.coverTarget
|
|
2078
|
+
? targetRect.left + targetRect.width - width
|
|
2079
|
+
: targetRect.left - width;
|
|
2080
|
+
top = targetRect.top + targetRect.height / 2 - height / 2;
|
|
2081
|
+
break;
|
|
2082
|
+
case 'left-start':
|
|
2083
|
+
left = this.coverTarget
|
|
2084
|
+
? targetRect.left + targetRect.width - width
|
|
2085
|
+
: targetRect.left - width;
|
|
2086
|
+
top = targetRect.top + targetRect.height - height;
|
|
2087
|
+
break;
|
|
2088
|
+
case 'left-end':
|
|
2089
|
+
left = this.coverTarget
|
|
2090
|
+
? targetRect.left + targetRect.width - width
|
|
2091
|
+
: targetRect.left - width;
|
|
2092
|
+
top = targetRect.top;
|
|
2093
|
+
break;
|
|
2094
|
+
case 'right':
|
|
2095
|
+
left = this.coverTarget
|
|
2096
|
+
? targetRect.left
|
|
2097
|
+
: targetRect.left + targetRect.width;
|
|
2098
|
+
top = targetRect.top + targetRect.height / 2 - height / 2;
|
|
2099
|
+
break;
|
|
2100
|
+
case 'right-start':
|
|
2101
|
+
left = this.coverTarget
|
|
2102
|
+
? targetRect.left
|
|
2103
|
+
: targetRect.left + targetRect.width;
|
|
2104
|
+
top = targetRect.top + targetRect.height - height;
|
|
2105
|
+
break;
|
|
2106
|
+
case 'right-end':
|
|
2107
|
+
left = this.coverTarget
|
|
2108
|
+
? targetRect.left
|
|
2109
|
+
: targetRect.left + targetRect.width;
|
|
2110
|
+
top = targetRect.top;
|
|
2111
|
+
break;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
left = Math.max(padding, Math.min(left, vw - width - padding));
|
|
2115
|
+
top = Math.max(padding, Math.min(top, vh - height - padding));
|
|
2116
|
+
|
|
2049
2117
|
return new DOMRect(left, top, width, height);
|
|
2050
2118
|
}
|
|
2051
2119
|
|
|
2052
|
-
private
|
|
2053
|
-
const
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
static override styles = [
|
|
2065
|
-
sharedStyles,
|
|
2066
|
-
css`
|
|
2067
|
-
:host {
|
|
2068
|
-
display: contents;
|
|
2069
|
-
}
|
|
2070
|
-
|
|
2071
|
-
.backdrop {
|
|
2072
|
-
position: fixed;
|
|
2073
|
-
inset: 0;
|
|
2074
|
-
background-color: var(--scrim);
|
|
2075
|
-
opacity: 0;
|
|
2076
|
-
visibility: hidden;
|
|
2077
|
-
transition:
|
|
2078
|
-
opacity var(--_backdrop-duration, 0.35s) var(--_backdrop-ease, cubic-bezier(0.2, 0, 0, 1)),
|
|
2079
|
-
visibility var(--_backdrop-duration, 0.35s) var(--_backdrop-ease, cubic-bezier(0.2, 0, 0, 1));
|
|
2080
|
-
z-index: calc(var(--_z-index, 100) - 1);
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
.backdrop.open {
|
|
2084
|
-
opacity: 1;
|
|
2085
|
-
visibility: visible;
|
|
2086
|
-
}
|
|
2120
|
+
private _measureNaturalSize(panel: HTMLElement): { width: number; height: number } {
|
|
2121
|
+
const prevW = panel.style.getPropertyValue('--_panel-width');
|
|
2122
|
+
const prevH = panel.style.getPropertyValue('--_panel-height');
|
|
2123
|
+
const prevPanelW = panel.style.width;
|
|
2124
|
+
const prevPanelH = panel.style.height;
|
|
2125
|
+
const bodyEl = this.shadowRoot!.querySelector('.body') as HTMLElement;
|
|
2126
|
+
const prevOverflow = bodyEl?.style.overflow ?? '';
|
|
2127
|
+
const configuredWidth = Math.min(
|
|
2128
|
+
this._parseSize(this.expandedWidth, window.innerWidth, 'width'),
|
|
2129
|
+
window.innerWidth - 32
|
|
2130
|
+
);
|
|
2087
2131
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2132
|
+
panel.style.width = this.autoSize ? 'auto' : `${configuredWidth}px`;
|
|
2133
|
+
panel.style.height = 'auto';
|
|
2134
|
+
panel.style.setProperty('--_panel-width', this.autoSize ? 'max-content' : `${configuredWidth}px`);
|
|
2135
|
+
panel.style.setProperty('--_panel-height', 'max-content');
|
|
2136
|
+
if (bodyEl) bodyEl.style.overflow = 'visible';
|
|
2137
|
+
const size = { width: this._inner.scrollWidth, height: this._inner.scrollHeight };
|
|
2138
|
+
|
|
2139
|
+
panel.style.setProperty('--_panel-width', prevW);
|
|
2140
|
+
panel.style.setProperty('--_panel-height', prevH);
|
|
2141
|
+
panel.style.width = prevPanelW;
|
|
2142
|
+
panel.style.height = prevPanelH;
|
|
2143
|
+
if (bodyEl) bodyEl.style.overflow = prevOverflow;
|
|
2144
|
+
return size;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
private _parseSize(value: string, ref: number, axis: 'width' | 'height'): number {
|
|
2148
|
+
const normalized = value.trim();
|
|
2149
|
+
if (/^(calc|min|max|clamp)\(/i.test(normalized) && CSS.supports(axis, normalized)) {
|
|
2150
|
+
const probe = document.createElement('div');
|
|
2151
|
+
Object.assign(probe.style, {
|
|
2152
|
+
position: 'fixed',
|
|
2153
|
+
visibility: 'hidden',
|
|
2154
|
+
pointerEvents: 'none',
|
|
2155
|
+
contain: 'strict',
|
|
2156
|
+
width: axis === 'width' ? normalized : '0',
|
|
2157
|
+
height: axis === 'height' ? normalized : '0'
|
|
2158
|
+
});
|
|
2159
|
+
document.body.appendChild(probe);
|
|
2160
|
+
const measured = probe.getBoundingClientRect()[axis];
|
|
2161
|
+
probe.remove();
|
|
2162
|
+
if (measured > 0) return measured;
|
|
2163
|
+
}
|
|
2164
|
+
const num = parseFloat(normalized);
|
|
2165
|
+
if (Number.isNaN(num)) return 352;
|
|
2166
|
+
if (normalized.endsWith('px')) return num;
|
|
2167
|
+
if (normalized.endsWith('rem')) return num * 16;
|
|
2168
|
+
if (normalized.endsWith('em')) return num * 16;
|
|
2169
|
+
if (normalized.endsWith('%')) return (num / 100) * ref;
|
|
2170
|
+
if (normalized.endsWith('vw')) return (num / 100) * window.innerWidth;
|
|
2171
|
+
if (normalized.endsWith('vh')) return (num / 100) * window.innerHeight;
|
|
2172
|
+
return num;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
static override styles = [
|
|
2176
|
+
sharedStyles,
|
|
2177
|
+
css`
|
|
2178
|
+
:host {
|
|
2179
|
+
display: contents;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
.backdrop {
|
|
2183
|
+
position: fixed;
|
|
2184
|
+
inset: 0;
|
|
2185
|
+
background-color: var(--scrim);
|
|
2186
|
+
opacity: 0;
|
|
2187
|
+
visibility: hidden;
|
|
2188
|
+
transition:
|
|
2189
|
+
opacity var(--_backdrop-duration, 0.35s) var(--_backdrop-ease, cubic-bezier(0.2, 0, 0, 1)),
|
|
2190
|
+
visibility var(--_backdrop-duration, 0.35s) var(--_backdrop-ease, cubic-bezier(0.2, 0, 0, 1));
|
|
2191
|
+
z-index: calc(var(--_z-index, 100) - 1);
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
.backdrop.open {
|
|
2195
|
+
opacity: 1;
|
|
2196
|
+
visibility: visible;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
.panel {
|
|
2200
|
+
position: fixed;
|
|
2201
|
+
visibility: hidden;
|
|
2202
|
+
opacity: 0;
|
|
2203
|
+
pointer-events: none;
|
|
2204
|
+
display: flex;
|
|
2205
|
+
flex-direction: column;
|
|
2206
|
+
overflow: hidden;
|
|
2207
|
+
background-color: var(--surface-container-high);
|
|
2208
|
+
color: var(--on-surface);
|
|
2209
|
+
border-radius: var(--moni-morph-panel-radius, 1.75rem);
|
|
2210
|
+
box-shadow: var(--elevate2);
|
|
2100
2211
|
z-index: var(--_z-index, 100);
|
|
2101
2212
|
}
|
|
2102
2213
|
|
|
@@ -2112,164 +2223,165 @@ export class MoniMorphModal extends MoniElement {
|
|
|
2112
2223
|
.panel.fullscreen footer {
|
|
2113
2224
|
padding-block-end: max(1.5rem, env(safe-area-inset-bottom));
|
|
2114
2225
|
}
|
|
2115
|
-
|
|
2116
|
-
.panel-inner {
|
|
2117
|
-
display: flex;
|
|
2118
|
-
flex-direction: column;
|
|
2119
|
-
width: var(--_panel-width, 100%);
|
|
2120
|
-
height: var(--_panel-height, 100%);
|
|
2121
|
-
flex-shrink: 0;
|
|
2122
|
-
overflow: hidden;
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
header {
|
|
2126
|
-
display: flex;
|
|
2127
|
-
align-items: center;
|
|
2128
|
-
justify-content: space-between;
|
|
2129
|
-
gap: 0.75rem;
|
|
2130
|
-
padding: 1.25rem 1.5rem 0.75rem;
|
|
2131
|
-
min-block-size: 3.5rem;
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
header.is-empty {
|
|
2135
|
-
display: none;
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2226
|
+
|
|
2227
|
+
.panel-inner {
|
|
2228
|
+
display: flex;
|
|
2229
|
+
flex-direction: column;
|
|
2230
|
+
width: var(--_panel-width, 100%);
|
|
2231
|
+
height: var(--_panel-height, 100%);
|
|
2232
|
+
flex-shrink: 0;
|
|
2233
|
+
overflow: hidden;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
header {
|
|
2237
|
+
display: flex;
|
|
2238
|
+
align-items: center;
|
|
2239
|
+
justify-content: space-between;
|
|
2240
|
+
gap: 0.75rem;
|
|
2241
|
+
padding: 1.25rem 1.5rem 0.75rem;
|
|
2242
|
+
min-block-size: 3.5rem;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
header.is-empty {
|
|
2246
|
+
display: none;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2138
2249
|
.body {
|
|
2139
2250
|
flex: 1;
|
|
2140
2251
|
overflow-y: var(--moni-morph-body-overflow-y, auto);
|
|
2141
2252
|
overflow-x: var(--moni-morph-body-overflow-x, hidden);
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
border
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
.morph-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
*
|
|
2204
|
-
*
|
|
2205
|
-
* `
|
|
2206
|
-
*
|
|
2207
|
-
*
|
|
2208
|
-
*
|
|
2209
|
-
*
|
|
2210
|
-
*
|
|
2211
|
-
*
|
|
2212
|
-
*
|
|
2213
|
-
*
|
|
2214
|
-
*
|
|
2215
|
-
*
|
|
2216
|
-
*
|
|
2217
|
-
* `
|
|
2218
|
-
*
|
|
2219
|
-
*
|
|
2220
|
-
*
|
|
2221
|
-
|
|
2253
|
+
overscroll-behavior: contain;
|
|
2254
|
+
padding: var(--moni-morph-body-padding, 0 1.5rem);
|
|
2255
|
+
scrollbar-gutter: var(--moni-morph-body-scrollbar-gutter, stable);
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
.panel.is-animating .body {
|
|
2259
|
+
overflow: hidden !important;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
.panel.is-animating .body::-webkit-scrollbar {
|
|
2263
|
+
display: none;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
footer {
|
|
2267
|
+
display: flex;
|
|
2268
|
+
align-items: center;
|
|
2269
|
+
justify-content: flex-end;
|
|
2270
|
+
gap: 0.5rem;
|
|
2271
|
+
padding: 1rem 1.5rem 1.5rem;
|
|
2272
|
+
border-top: 0.0625rem solid var(--outline-variant);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
footer.is-empty {
|
|
2276
|
+
display: none;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
.close-btn {
|
|
2280
|
+
display: inline-flex;
|
|
2281
|
+
align-items: center;
|
|
2282
|
+
justify-content: center;
|
|
2283
|
+
inline-size: 2.5rem;
|
|
2284
|
+
block-size: 2.5rem;
|
|
2285
|
+
padding: 0;
|
|
2286
|
+
border: none;
|
|
2287
|
+
border-radius: 50%;
|
|
2288
|
+
background: transparent;
|
|
2289
|
+
color: var(--on-surface-variant);
|
|
2290
|
+
cursor: pointer;
|
|
2291
|
+
font-size: 1.25rem;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
.close-btn:hover {
|
|
2295
|
+
background-color: var(--active);
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
.morph-text,
|
|
2299
|
+
.morph-icon {
|
|
2300
|
+
position: fixed;
|
|
2301
|
+
pointer-events: none;
|
|
2302
|
+
box-sizing: border-box;
|
|
2303
|
+
overflow: hidden;
|
|
2304
|
+
will-change: transform, width, height, color, font-size;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
.trigger-label-slot {
|
|
2308
|
+
display: none;
|
|
2309
|
+
}
|
|
2310
|
+
`
|
|
2311
|
+
];
|
|
2312
|
+
|
|
2313
|
+
/**
|
|
2314
|
+
* Renderiza el caparazón del modal morph: fondo opcional + contenedor de panel.
|
|
2315
|
+
*
|
|
2316
|
+
* **Bandera `_visible`:**
|
|
2317
|
+
* `visible = this._visible || this.open` mantiene vivo el DOM mientras se reproduce
|
|
2318
|
+
* la animación de cierre de GSAP. `_visible` se establece en `true` al inicio de la apertura y
|
|
2319
|
+
* en `false` solo después de que se dispara el `onComplete` de la animación de cierre. Sin esto,
|
|
2320
|
+
* Lit eliminaría el panel del DOM antes de que la animación termine.
|
|
2321
|
+
*
|
|
2322
|
+
* **Condicional del fondo (backdrop):**
|
|
2323
|
+
* El div `.backdrop` solo se renderiza cuando `hasBackdrop=true`. Cuando `modal=false`,
|
|
2324
|
+
* el `background-color` del fondo se establece en `transparent` para que el fondo superpuesto
|
|
2325
|
+
* (scrim) sea invisible, pero los eventos del puntero aún se consumen (clic para cerrar).
|
|
2326
|
+
*
|
|
2327
|
+
* **Objetivo de morphing de GSAP:**
|
|
2328
|
+
* `.panel` es el objetivo de la animación GSAP. Al abrir: se anima desde el
|
|
2329
|
+
* `getBoundingClientRect()` del elemento anclaje (posición + tamaño) a su tamaño CSS natural
|
|
2330
|
+
* mediante `gsap.fromTo()`. Al cerrar: a la inversa. `.panel-inner` recibe una
|
|
2331
|
+
* contra-escala para evitar que el contenido interno se escale junto con el panel,
|
|
2332
|
+
* creando una ilusión física de "expandirse desde el origen".
|
|
2333
|
+
*/
|
|
2222
2334
|
override render() {
|
|
2223
2335
|
const visible = this._visible || this.open;
|
|
2224
2336
|
const headerEmpty = !this._hasHeader && !this.showCloseButton;
|
|
2225
2337
|
const fullscreen = this._usesFullscreen();
|
|
2226
|
-
return html`
|
|
2227
|
-
${this.hasBackdrop ? html`
|
|
2228
|
-
<div
|
|
2229
|
-
class="backdrop ${visible ? 'open' : ''}"
|
|
2230
|
-
part="backdrop"
|
|
2231
|
-
style="${!this.modal ? 'background-color: transparent;' : ''}"
|
|
2232
|
-
></div>
|
|
2233
|
-
` : ''}
|
|
2338
|
+
return html`
|
|
2339
|
+
${this.hasBackdrop ? html`
|
|
2340
|
+
<div
|
|
2341
|
+
class="backdrop ${visible ? 'open' : ''}"
|
|
2342
|
+
part="backdrop"
|
|
2343
|
+
style="${!this.modal ? 'background-color: transparent;' : ''}"
|
|
2344
|
+
></div>
|
|
2345
|
+
` : ''}
|
|
2234
2346
|
<div class="panel ${fullscreen ? 'fullscreen' : ''}" part="panel">
|
|
2235
|
-
<div class="panel-inner" part="panel-inner">
|
|
2236
|
-
<header
|
|
2237
|
-
class="${headerEmpty ? 'is-empty' : ''}"
|
|
2238
|
-
part="header"
|
|
2239
|
-
>
|
|
2240
|
-
<div class="header-morph-wrapper"><slot name="header" @slotchange=${this._onHeaderSlotChange}></slot></div>
|
|
2241
|
-
${this.showCloseButton
|
|
2242
|
-
? html`
|
|
2243
|
-
<button
|
|
2244
|
-
class="close-btn"
|
|
2245
|
-
@click=${this.hide}
|
|
2246
|
-
aria-label="Cerrar"
|
|
2247
|
-
>
|
|
2248
|
-
<moni-icon name="close"></moni-icon>
|
|
2249
|
-
</button>
|
|
2250
|
-
`
|
|
2251
|
-
: ''}
|
|
2252
|
-
</header>
|
|
2253
|
-
<div class="body" part="body"><slot></slot></div>
|
|
2254
|
-
<footer
|
|
2255
|
-
class="${this._hasFooter ? '' : 'is-empty'}"
|
|
2256
|
-
part="footer"
|
|
2257
|
-
>
|
|
2258
|
-
<slot name="footer" @slotchange=${this._onFooterSlotChange}></slot>
|
|
2259
|
-
</footer>
|
|
2260
|
-
</div>
|
|
2261
|
-
</div>
|
|
2262
|
-
<div class="trigger-label-slot">
|
|
2263
|
-
<slot name="trigger-label" @slotchange=${this._onTriggerLabelSlotChange}></slot>
|
|
2264
|
-
</div>
|
|
2265
|
-
`;
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
|
|
2269
|
-
declare global {
|
|
2270
|
-
interface HTMLElementTagNameMap {
|
|
2271
|
-
'moni-morph-modal': MoniMorphModal;
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
export default MoniMorphModal;
|
|
2347
|
+
<div class="panel-inner" part="panel-inner">
|
|
2348
|
+
<header
|
|
2349
|
+
class="${headerEmpty ? 'is-empty' : ''}"
|
|
2350
|
+
part="header"
|
|
2351
|
+
>
|
|
2352
|
+
<div class="header-morph-wrapper"><slot name="header" @slotchange=${this._onHeaderSlotChange}></slot></div>
|
|
2353
|
+
${this.showCloseButton
|
|
2354
|
+
? html`
|
|
2355
|
+
<button
|
|
2356
|
+
class="close-btn"
|
|
2357
|
+
@click=${this.hide}
|
|
2358
|
+
aria-label="Cerrar"
|
|
2359
|
+
>
|
|
2360
|
+
<moni-icon name="close"></moni-icon>
|
|
2361
|
+
</button>
|
|
2362
|
+
`
|
|
2363
|
+
: ''}
|
|
2364
|
+
</header>
|
|
2365
|
+
<div class="body" part="body"><slot></slot></div>
|
|
2366
|
+
<footer
|
|
2367
|
+
class="${this._hasFooter ? '' : 'is-empty'}"
|
|
2368
|
+
part="footer"
|
|
2369
|
+
>
|
|
2370
|
+
<slot name="footer" @slotchange=${this._onFooterSlotChange}></slot>
|
|
2371
|
+
</footer>
|
|
2372
|
+
</div>
|
|
2373
|
+
</div>
|
|
2374
|
+
<div class="trigger-label-slot">
|
|
2375
|
+
<slot name="trigger-label" @slotchange=${this._onTriggerLabelSlotChange}></slot>
|
|
2376
|
+
</div>
|
|
2377
|
+
`;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
declare global {
|
|
2382
|
+
interface HTMLElementTagNameMap {
|
|
2383
|
+
'moni-morph-modal': MoniMorphModal;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
export default MoniMorphModal;
|