@moni-labs/moni-ui 0.4.13 → 0.4.14
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 +91 -81
- package/dist/browser/moni-ui.iife.min.js +1537 -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 +2703 -2481
- 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 +1 -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-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 +97 -88
- 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/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-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,1815 +1,1963 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file components/moni-select.ts
|
|
3
|
-
* @package @moni-labs/moni-ui
|
|
4
|
-
* @license MIT
|
|
5
|
-
* @contributors Moni Labs & Contributors
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { html, css, nothing } from
|
|
9
|
-
import { customElement, property, state, query } from
|
|
10
|
-
import { ifDefined } from
|
|
11
|
-
import { classMap } from
|
|
12
|
-
import { MoniElement, sharedStyles, fieldStyles } from
|
|
13
|
-
import { emitMoniEvent } from
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Representación interna de una opción individual dentro del menú desplegable del select.
|
|
20
|
-
*
|
|
21
|
-
* @internal
|
|
22
|
-
*/
|
|
23
|
-
interface OptionNode {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Representación interna de un grupo de opciones dentro del menú desplegable del select.
|
|
40
|
-
*
|
|
41
|
-
* @internal
|
|
42
|
-
*/
|
|
43
|
-
interface GroupNode {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Tipo de unión para cualquier nodo en el árbol del menú desplegable. @internal */
|
|
53
|
-
type DropdownNode = OptionNode | GroupNode;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Componente Material Design 3 Select (Menú Desplegable).
|
|
57
|
-
*
|
|
58
|
-
* Un menú desplegable select personalizado completo con capacidad de búsqueda, grupos de opciones,
|
|
59
|
-
* navegación por teclado, animación y un modo de cajón/hoja (drawer/sheet) móvil opcional.
|
|
60
|
-
* Reemplaza el elemento nativo `<select>` con una alternativa completamente estilizada y accesible
|
|
61
|
-
* que cumple con M3.
|
|
62
|
-
*
|
|
63
|
-
* **Referencia a la especificación M3:** `m3-docs/components/menus/specs.md` (menús desplegables)
|
|
64
|
-
*
|
|
65
|
-
* **Resumen de características:**
|
|
66
|
-
* - Variantes Filled y Outlined que coinciden con los estilos de campo de texto de M3.
|
|
67
|
-
* - Etiqueta flotante (floating label) con la animación estándar de etiqueta flotante de estilos de campo.
|
|
68
|
-
* - Modo de búsqueda: el atributo `searchable` agrega un campo de entrada de filtro en línea.
|
|
69
|
-
* - Grupos de opciones: elementos `<moni-select-group>` en el slot para opciones jerárquicas.
|
|
70
|
-
* - Cajón móvil: el atributo `drawer` abre las opciones en un `<moni-bottom-sheet>`
|
|
71
|
-
* en lugar de un popup desplegable, ideal para interfaces táctiles (touch UIs).
|
|
72
|
-
* - Navegación por teclado: Teclas de flecha, Enter, Escape y Tab según combobox de ARIA.
|
|
73
|
-
* - Estado de carga: `loading` muestra un progreso circular indeterminado.
|
|
74
|
-
* - Soporte multi-valor: `multiple` permite selección múltiple.
|
|
75
|
-
*
|
|
76
|
-
* **Fuentes de opciones:**
|
|
77
|
-
* Las opciones se pueden proporcionar de dos maneras:
|
|
78
|
-
* 1. **Elementos `<moni-select-option>` en la ranura (slot)** (por defecto, recomendado para SSR).
|
|
79
|
-
* 2. **Propiedad `options`** — un array de `DropdownNode[]` para un control completamente programático.
|
|
80
|
-
*
|
|
81
|
-
* **Vinculación de valores (Value binding):**
|
|
82
|
-
* La propiedad `value` contiene la cadena de valor de la opción actualmente seleccionada.
|
|
83
|
-
* Para selección múltiple, `values` contiene `string[]`. Al cambiar, se dispara un
|
|
84
|
-
* evento compuesto `'moni-change'`.
|
|
85
|
-
*
|
|
86
|
-
* @fires moni-change - Burbujea y está compuesto. Se dispara cuando el valor seleccionado cambia.
|
|
87
|
-
* Lee `element.value` (o `element.values` para `multiple`).
|
|
88
|
-
* @fires moni-select-open - Se dispara al abrirse para que los demás selects cierren sus menús.
|
|
89
|
-
*
|
|
90
|
-
* @example
|
|
91
|
-
* ```html
|
|
92
|
-
* <moni-select label="País" name="country" variant="outlined">
|
|
93
|
-
* <moni-select-option value="us">Estados Unidos</moni-select-option>
|
|
94
|
-
* <moni-select-option value="gb">Reino Unido</moni-select-option>
|
|
95
|
-
* <moni-select-option value="de">Alemania</moni-select-option>
|
|
96
|
-
* </moni-select>
|
|
97
|
-
*
|
|
98
|
-
* <!-- Select con búsqueda -->
|
|
99
|
-
* <moni-select label="Idioma" searchable>
|
|
100
|
-
* <moni-select-option value="ts">TypeScript</moni-select-option>
|
|
101
|
-
* <moni-select-option value="py">Python</moni-select-option>
|
|
102
|
-
* </moni-select>
|
|
103
|
-
* ```
|
|
104
|
-
*
|
|
105
|
-
* @slot default - Hijos `<moni-select-option>` o `<moni-select-group>`.
|
|
106
|
-
*
|
|
107
|
-
* @csspart field - El contenedor div exterior `.field`.
|
|
108
|
-
* @csspart dropdown - El contenedor flotante de la lista de opciones.
|
|
109
|
-
*/
|
|
110
|
-
@customElement(
|
|
111
|
-
export class MoniSelect extends MoniElement {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
-
|
|
1529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file components/moni-select.ts
|
|
3
|
+
* @package @moni-labs/moni-ui
|
|
4
|
+
* @license MIT
|
|
5
|
+
* @contributors Moni Labs & Contributors
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { html, css, nothing } from "lit";
|
|
9
|
+
import { customElement, property, state, query } from "lit/decorators.js";
|
|
10
|
+
import { ifDefined } from "lit/directives/if-defined.js";
|
|
11
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
12
|
+
import { MoniElement, sharedStyles, fieldStyles } from "./_base/index.js";
|
|
13
|
+
import { emitMoniEvent } from "../utils/event-emitter.js";
|
|
14
|
+
import "./moni-icon.js";
|
|
15
|
+
import "./moni-progress.js";
|
|
16
|
+
import "./moni-select-option.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Representación interna de una opción individual dentro del menú desplegable del select.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
interface OptionNode {
|
|
24
|
+
/** Propiedad discriminante que identifica este nodo como una opción. */
|
|
25
|
+
type: "option";
|
|
26
|
+
/** El valor enviado cuando se selecciona esta opción. */
|
|
27
|
+
value: string;
|
|
28
|
+
/** La etiqueta mostrada en la lista desplegable. */
|
|
29
|
+
label: string;
|
|
30
|
+
/** Cuando es `true`, la opción no es interactiva y aparece atenuada (gris). */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** Referencia al elemento original `<moni-select-option>` de la ranura (slot), si lo hay. */
|
|
33
|
+
element?: HTMLElement;
|
|
34
|
+
/** Nombre de grupo opcional para organizar opciones bajo un `<moni-select-group>`. */
|
|
35
|
+
group?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Representación interna de un grupo de opciones dentro del menú desplegable del select.
|
|
40
|
+
*
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
interface GroupNode {
|
|
44
|
+
/** Propiedad discriminante que identifica este nodo como un grupo. */
|
|
45
|
+
type: "group";
|
|
46
|
+
/** La etiqueta de encabezado del grupo mostrada sobre las opciones del grupo. */
|
|
47
|
+
label: string;
|
|
48
|
+
/** Las opciones anidadas dentro de este grupo. */
|
|
49
|
+
children: DropdownNode[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Tipo de unión para cualquier nodo en el árbol del menú desplegable. @internal */
|
|
53
|
+
type DropdownNode = OptionNode | GroupNode;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Componente Material Design 3 Select (Menú Desplegable).
|
|
57
|
+
*
|
|
58
|
+
* Un menú desplegable select personalizado completo con capacidad de búsqueda, grupos de opciones,
|
|
59
|
+
* navegación por teclado, animación y un modo de cajón/hoja (drawer/sheet) móvil opcional.
|
|
60
|
+
* Reemplaza el elemento nativo `<select>` con una alternativa completamente estilizada y accesible
|
|
61
|
+
* que cumple con M3.
|
|
62
|
+
*
|
|
63
|
+
* **Referencia a la especificación M3:** `m3-docs/components/menus/specs.md` (menús desplegables)
|
|
64
|
+
*
|
|
65
|
+
* **Resumen de características:**
|
|
66
|
+
* - Variantes Filled y Outlined que coinciden con los estilos de campo de texto de M3.
|
|
67
|
+
* - Etiqueta flotante (floating label) con la animación estándar de etiqueta flotante de estilos de campo.
|
|
68
|
+
* - Modo de búsqueda: el atributo `searchable` agrega un campo de entrada de filtro en línea.
|
|
69
|
+
* - Grupos de opciones: elementos `<moni-select-group>` en el slot para opciones jerárquicas.
|
|
70
|
+
* - Cajón móvil: el atributo `drawer` abre las opciones en un `<moni-bottom-sheet>`
|
|
71
|
+
* en lugar de un popup desplegable, ideal para interfaces táctiles (touch UIs).
|
|
72
|
+
* - Navegación por teclado: Teclas de flecha, Enter, Escape y Tab según combobox de ARIA.
|
|
73
|
+
* - Estado de carga: `loading` muestra un progreso circular indeterminado.
|
|
74
|
+
* - Soporte multi-valor: `multiple` permite selección múltiple.
|
|
75
|
+
*
|
|
76
|
+
* **Fuentes de opciones:**
|
|
77
|
+
* Las opciones se pueden proporcionar de dos maneras:
|
|
78
|
+
* 1. **Elementos `<moni-select-option>` en la ranura (slot)** (por defecto, recomendado para SSR).
|
|
79
|
+
* 2. **Propiedad `options`** — un array de `DropdownNode[]` para un control completamente programático.
|
|
80
|
+
*
|
|
81
|
+
* **Vinculación de valores (Value binding):**
|
|
82
|
+
* La propiedad `value` contiene la cadena de valor de la opción actualmente seleccionada.
|
|
83
|
+
* Para selección múltiple, `values` contiene `string[]`. Al cambiar, se dispara un
|
|
84
|
+
* evento compuesto `'moni-change'`.
|
|
85
|
+
*
|
|
86
|
+
* @fires moni-change - Burbujea y está compuesto. Se dispara cuando el valor seleccionado cambia.
|
|
87
|
+
* Lee `element.value` (o `element.values` para `multiple`).
|
|
88
|
+
* @fires moni-select-open - Se dispara al abrirse para que los demás selects cierren sus menús.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```html
|
|
92
|
+
* <moni-select label="País" name="country" variant="outlined">
|
|
93
|
+
* <moni-select-option value="us">Estados Unidos</moni-select-option>
|
|
94
|
+
* <moni-select-option value="gb">Reino Unido</moni-select-option>
|
|
95
|
+
* <moni-select-option value="de">Alemania</moni-select-option>
|
|
96
|
+
* </moni-select>
|
|
97
|
+
*
|
|
98
|
+
* <!-- Select con búsqueda -->
|
|
99
|
+
* <moni-select label="Idioma" searchable>
|
|
100
|
+
* <moni-select-option value="ts">TypeScript</moni-select-option>
|
|
101
|
+
* <moni-select-option value="py">Python</moni-select-option>
|
|
102
|
+
* </moni-select>
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @slot default - Hijos `<moni-select-option>` o `<moni-select-group>`.
|
|
106
|
+
*
|
|
107
|
+
* @csspart field - El contenedor div exterior `.field`.
|
|
108
|
+
* @csspart dropdown - El contenedor flotante de la lista de opciones.
|
|
109
|
+
*/
|
|
110
|
+
@customElement("moni-select")
|
|
111
|
+
export class MoniSelect extends MoniElement {
|
|
112
|
+
static formAssociated = true;
|
|
113
|
+
private _internals: ElementInternals;
|
|
114
|
+
|
|
115
|
+
constructor() {
|
|
116
|
+
super();
|
|
117
|
+
this._internals = this.attachInternals();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Nombre del campo select, usado para el envío del formulario.
|
|
122
|
+
* @type {string}
|
|
123
|
+
*/
|
|
124
|
+
@property({ reflect: true }) name = "";
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Texto de la etiqueta flotante.
|
|
128
|
+
* @type {string}
|
|
129
|
+
*/
|
|
130
|
+
@property({ reflect: true }) label = "";
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Variante visual del campo select.
|
|
134
|
+
* @type {'filled' | 'outlined'}
|
|
135
|
+
* @default 'filled'
|
|
136
|
+
*/
|
|
137
|
+
@property({ reflect: true }) variant: "filled" | "outlined" = "filled";
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Define las dimensiones del campo select.
|
|
141
|
+
* @type {'small' | 'medium' | 'large' | 'extra'}
|
|
142
|
+
* @default 'medium'
|
|
143
|
+
*/
|
|
144
|
+
@property({ reflect: true })
|
|
145
|
+
size: "small" | "medium" | "large" | "extra" = "medium";
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Forma (radio de borde) del campo.
|
|
149
|
+
* @type {'round' | 'small-round' | 'square' | 'no-round'}
|
|
150
|
+
* @default 'no-round'
|
|
151
|
+
*/
|
|
152
|
+
@property({ reflect: true })
|
|
153
|
+
shape: "round" | "small-round" | "square" | "no-round" = "no-round";
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Deshabilita el campo select.
|
|
157
|
+
* @type {boolean}
|
|
158
|
+
*/
|
|
159
|
+
@property({ type: Boolean, reflect: true }) disabled = false;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Obliga a elegir una opción antes de enviar el formulario.
|
|
163
|
+
*
|
|
164
|
+
* El `<input>` visible es de sólo lectura, así que la validación nativa no
|
|
165
|
+
* se aplica sobre él: la restricción se declara en el host con
|
|
166
|
+
* `ElementInternals.setValidity`, que es lo que mira el formulario.
|
|
167
|
+
*/
|
|
168
|
+
@property({ type: Boolean, reflect: true }) required = false;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Si es verdadero, muestra un indicador de carga (progreso lineal).
|
|
172
|
+
* @type {boolean}
|
|
173
|
+
*/
|
|
174
|
+
@property({ type: Boolean, reflect: true }) loading = false;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Texto de ayuda mostrado debajo del campo.
|
|
178
|
+
* @type {string}
|
|
179
|
+
*/
|
|
180
|
+
@property({ reflect: true }) helper = "";
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Texto de error mostrado debajo del campo cuando `error` es true.
|
|
184
|
+
* Reemplaza al texto de ayuda.
|
|
185
|
+
* @type {string}
|
|
186
|
+
*/
|
|
187
|
+
@property({ reflect: true, attribute: "error-text" }) errorText = "";
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Si es verdadero, establece el campo en un estado de error.
|
|
191
|
+
* @type {boolean}
|
|
192
|
+
*/
|
|
193
|
+
@property({ type: Boolean, reflect: true }) error = false;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* El valor actual del select.
|
|
197
|
+
* @type {string}
|
|
198
|
+
*/
|
|
199
|
+
@property({ reflect: true }) value = "";
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Nombre del icono principal (leading icon, Material Symbols).
|
|
203
|
+
* @type {string}
|
|
204
|
+
*/
|
|
205
|
+
@property({ reflect: true }) icon = "";
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Nombre del icono final (trailing icon, Material Symbols) que indica el estado del menú desplegable.
|
|
209
|
+
* @type {string}
|
|
210
|
+
* @default 'arrow_drop_down'
|
|
211
|
+
*/
|
|
212
|
+
@property({ reflect: true, attribute: "trailing-icon" }) trailingIcon =
|
|
213
|
+
"arrow_drop_down";
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Habilita un campo de búsqueda en la parte superior del menú desplegable para filtrar opciones.
|
|
217
|
+
* @type {boolean}
|
|
218
|
+
*/
|
|
219
|
+
@property({ type: Boolean, reflect: true }) searchable = false;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Muestra un botón de limpieza cuando se selecciona un valor para restablecer fácilmente el campo.
|
|
223
|
+
* @type {boolean}
|
|
224
|
+
*/
|
|
225
|
+
@property({ type: Boolean, reflect: true }) clearable = false;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Renderiza las opciones como una hoja inferior (bottom sheet, ideal para dispositivos móviles) en lugar de un menú desplegable.
|
|
229
|
+
* @type {boolean}
|
|
230
|
+
*/
|
|
231
|
+
@property({ type: Boolean, reflect: true }) sheet = false;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Texto de marcador de posición (placeholder) mostrado cuando no hay ningún valor seleccionado.
|
|
235
|
+
* @type {string}
|
|
236
|
+
*/
|
|
237
|
+
@property({ reflect: true }) placeholder = "";
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Estrategia de posicionamiento para el menú desplegable.
|
|
241
|
+
*
|
|
242
|
+
* - `'absolute'`: permanece en el contexto de posicionamiento del select.
|
|
243
|
+
* - `'fixed'`: usa coordenadas del viewport, pero conserva su contexto de apilamiento.
|
|
244
|
+
* - `'body'`: usa la capa superior nativa (Popover API) para escapar de `overflow`,
|
|
245
|
+
* diálogos, bottom sheets y contextos de apilamiento.
|
|
246
|
+
*
|
|
247
|
+
* @type {'absolute' | 'fixed' | 'body'}
|
|
248
|
+
* @default 'absolute'
|
|
249
|
+
*/
|
|
250
|
+
@property({ reflect: true }) positioning: "absolute" | "fixed" | "body" =
|
|
251
|
+
"absolute";
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Ubicación preferida del menú desplegable con respecto al disparador (trigger).
|
|
255
|
+
* @type {'top' | 'bottom' | 'left' | 'right' | 'auto'}
|
|
256
|
+
* @default 'auto'
|
|
257
|
+
*/
|
|
258
|
+
@property({ reflect: true }) placement:
|
|
259
|
+
"top" | "bottom" | "left" | "right" | "auto" = "auto";
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Restricción de ancho del menú desplegable.
|
|
263
|
+
* - `'trigger'`: Coincide con el ancho del campo de entrada.
|
|
264
|
+
* - `'auto'`: Coincide con el ancho del contenido del menú desplegable.
|
|
265
|
+
* - O cualquier valor CSS de ancho válido (ej. '200px').
|
|
266
|
+
* @type {string}
|
|
267
|
+
* @default 'trigger'
|
|
268
|
+
*/
|
|
269
|
+
@property({ reflect: true, attribute: "dropdown-width" }) dropdownWidth =
|
|
270
|
+
"trigger";
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Límite manual opcional para la altura del menú.
|
|
274
|
+
*
|
|
275
|
+
* Acepta cualquier longitud CSS válida, por ejemplo `240px`, `40vh` o
|
|
276
|
+
* `calc(100vh - 8rem)`. El valor nunca supera el límite automático calculado
|
|
277
|
+
* según el viewport, la posición del campo y el tope predeterminado de 75vh.
|
|
278
|
+
* Una cadena vacía conserva el cálculo automático.
|
|
279
|
+
*
|
|
280
|
+
* @default ''
|
|
281
|
+
*/
|
|
282
|
+
@property({ reflect: true, attribute: "dropdown-max-height" })
|
|
283
|
+
dropdownMaxHeight = "";
|
|
284
|
+
|
|
285
|
+
@state() private _open = false;
|
|
286
|
+
@state() private _searchQuery = "";
|
|
287
|
+
@state() private _actualPlacement: "top" | "bottom" | "left" | "right" =
|
|
288
|
+
"bottom";
|
|
289
|
+
@state() private _parsedOptions: DropdownNode[] = [];
|
|
290
|
+
@state() private _activeIndex = -1;
|
|
291
|
+
@state() private _menuStyle = "";
|
|
292
|
+
@state() private _menuMaxHeight = "75vh";
|
|
293
|
+
@state() private _useInlineCategories = false;
|
|
294
|
+
@state() private _drilldownPath: GroupNode[] = [];
|
|
295
|
+
|
|
296
|
+
@query("slot") private _slot!: HTMLSlotElement;
|
|
297
|
+
@query("input") private _input!: HTMLInputElement;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Declara la validez del host según `required`.
|
|
301
|
+
*
|
|
302
|
+
* No se puede delegar en el `<input>` interno porque es `readonly`, y un
|
|
303
|
+
* campo de sólo lectura queda excluido de la validación de restricciones. El
|
|
304
|
+
* ancla sigue siendo ese input para que el navegador tenga dónde mostrar el
|
|
305
|
+
* mensaje y a dónde llevar el foco.
|
|
306
|
+
*/
|
|
307
|
+
/**
|
|
308
|
+
* API de validación equivalente a la de un control nativo.
|
|
309
|
+
*
|
|
310
|
+
* Los elementos form-associated participan en la validación del formulario,
|
|
311
|
+
* pero no reciben estos miembros automáticamente: hay que exponerlos para que
|
|
312
|
+
* un `moni-select` se pueda interrogar igual que un `<input>`.
|
|
313
|
+
*/
|
|
314
|
+
get validity(): ValidityState {
|
|
315
|
+
return this._internals.validity;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
get validationMessage(): string {
|
|
319
|
+
return this._internals.validationMessage;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
checkValidity(): boolean {
|
|
323
|
+
return this._internals.checkValidity();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
reportValidity(): boolean {
|
|
327
|
+
return this._internals.reportValidity();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private _syncValidity() {
|
|
331
|
+
if (!this._internals?.setValidity) return;
|
|
332
|
+
if (this.required && !this.value) {
|
|
333
|
+
this._internals.setValidity(
|
|
334
|
+
{ valueMissing: true },
|
|
335
|
+
"Selecciona una opción.",
|
|
336
|
+
this._input ?? undefined,
|
|
337
|
+
);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
this._internals.setValidity({});
|
|
341
|
+
}
|
|
342
|
+
@query(".dropdown-menu") private _menu?: HTMLElement;
|
|
343
|
+
|
|
344
|
+
static override styles = [
|
|
345
|
+
sharedStyles,
|
|
346
|
+
fieldStyles,
|
|
347
|
+
css`
|
|
348
|
+
:host {
|
|
349
|
+
position: relative;
|
|
350
|
+
display: block;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.field {
|
|
354
|
+
cursor: pointer;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
input[type="text"] {
|
|
358
|
+
cursor: pointer;
|
|
359
|
+
width: 100%;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
input[type="text"][readonly] {
|
|
363
|
+
caret-color: transparent;
|
|
364
|
+
user-select: none;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* Dropdown menu overlay */
|
|
368
|
+
.dropdown-menu {
|
|
369
|
+
position: absolute;
|
|
370
|
+
top: 100%;
|
|
371
|
+
left: 0;
|
|
372
|
+
right: 0;
|
|
373
|
+
z-index: 100;
|
|
374
|
+
background-color: var(--surface-container);
|
|
375
|
+
box-shadow: var(--elevate3);
|
|
376
|
+
border-radius: 0.5rem;
|
|
377
|
+
margin-top: 4px;
|
|
378
|
+
padding: 4px 0;
|
|
379
|
+
list-style: none;
|
|
380
|
+
opacity: 0;
|
|
381
|
+
visibility: hidden;
|
|
382
|
+
transform: scale(0.95) translateY(-8px);
|
|
383
|
+
transform-origin: top;
|
|
384
|
+
transition:
|
|
385
|
+
opacity var(--speed3) cubic-bezier(0.2, 0.8, 0.2, 1),
|
|
386
|
+
transform var(--speed3) cubic-bezier(0.34, 1.56, 0.64, 1),
|
|
387
|
+
visibility var(--speed3);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.dropdown-menu.open {
|
|
391
|
+
opacity: 1;
|
|
392
|
+
visibility: visible;
|
|
393
|
+
transform: scale(1) translateY(0);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* positioning="body" promotes the menu to the browser's top layer.
|
|
397
|
+
Reset the Popover UA box while retaining Shadow DOM styles. */
|
|
398
|
+
.dropdown-menu[popover] {
|
|
399
|
+
margin: 0;
|
|
400
|
+
border: 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.dropdown-menu.placement-top {
|
|
404
|
+
top: auto;
|
|
405
|
+
bottom: 100%;
|
|
406
|
+
margin-top: 0;
|
|
407
|
+
margin-bottom: 4px;
|
|
408
|
+
transform-origin: bottom;
|
|
409
|
+
transform: scale(0.95) translateY(8px);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.dropdown-menu.placement-top.open {
|
|
413
|
+
transform: scale(1) translateY(0);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.dropdown-menu.placement-left {
|
|
417
|
+
top: 0;
|
|
418
|
+
left: auto;
|
|
419
|
+
right: 100%;
|
|
420
|
+
margin-top: 0;
|
|
421
|
+
margin-right: 4px;
|
|
422
|
+
transform-origin: right center;
|
|
423
|
+
transform: scale(0.95) translateX(8px);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.dropdown-menu.placement-left.open {
|
|
427
|
+
transform: scale(1) translateX(0);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.dropdown-menu.placement-right {
|
|
431
|
+
top: 0;
|
|
432
|
+
left: 100%;
|
|
433
|
+
right: auto;
|
|
434
|
+
margin-top: 0;
|
|
435
|
+
margin-left: 4px;
|
|
436
|
+
transform-origin: left center;
|
|
437
|
+
transform: scale(0.95) translateX(-8px);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.dropdown-menu.placement-right.open {
|
|
441
|
+
transform: scale(1) translateX(0);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.dropdown-menu.open.scrollable {
|
|
445
|
+
overflow-x: hidden;
|
|
446
|
+
overflow-y: auto;
|
|
447
|
+
overscroll-behavior: contain;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* Desktop category flyouts need visible overflow. If the root list no
|
|
451
|
+
longer fits, the component switches to the inline drilldown layout. */
|
|
452
|
+
.dropdown-menu.open:not(.scrollable) {
|
|
453
|
+
overflow: visible;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/* Bottom sheet drawer styles */
|
|
457
|
+
.sheet-backdrop {
|
|
458
|
+
position: fixed;
|
|
459
|
+
inset: 0;
|
|
460
|
+
z-index: 1000;
|
|
461
|
+
background-color: rgba(0, 0, 0, 0.4);
|
|
462
|
+
opacity: 0;
|
|
463
|
+
visibility: hidden;
|
|
464
|
+
transition:
|
|
465
|
+
opacity var(--speed3) cubic-bezier(0.2, 0.8, 0.2, 1),
|
|
466
|
+
visibility var(--speed3);
|
|
467
|
+
display: flex;
|
|
468
|
+
flex-direction: column;
|
|
469
|
+
justify-content: flex-end;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.sheet-backdrop.open {
|
|
473
|
+
opacity: 1;
|
|
474
|
+
visibility: visible;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.sheet-drawer {
|
|
478
|
+
background-color: var(--surface-container);
|
|
479
|
+
border-radius: 1.75rem 1.75rem 0 0;
|
|
480
|
+
box-shadow: var(--elevate3);
|
|
481
|
+
max-height: 70vh;
|
|
482
|
+
display: flex;
|
|
483
|
+
flex-direction: column;
|
|
484
|
+
transform: translateY(100%);
|
|
485
|
+
transition: transform var(--speed3) cubic-bezier(0.16, 1, 0.3, 1);
|
|
486
|
+
padding: 16px 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.sheet-drawer.open {
|
|
490
|
+
transform: translateY(0);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.sheet-handle {
|
|
494
|
+
width: 40px;
|
|
495
|
+
height: 4px;
|
|
496
|
+
background-color: var(--outline-variant);
|
|
497
|
+
border-radius: 2px;
|
|
498
|
+
margin: 0 auto 16px;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.sheet-title {
|
|
502
|
+
padding: 0 24px 8px;
|
|
503
|
+
font-size: 1.125rem;
|
|
504
|
+
font-weight: 700;
|
|
505
|
+
color: var(--on-surface);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.sheet-search-wrapper {
|
|
509
|
+
padding: 0 16px 12px;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.sheet-search-input {
|
|
513
|
+
width: 100%;
|
|
514
|
+
box-sizing: border-box;
|
|
515
|
+
padding: 10px 16px;
|
|
516
|
+
border: 1px solid var(--outline);
|
|
517
|
+
border-radius: 9999px;
|
|
518
|
+
background: var(--surface-container-high);
|
|
519
|
+
color: var(--on-surface);
|
|
520
|
+
font-size: 1rem;
|
|
521
|
+
outline: none;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.sheet-search-input:focus {
|
|
525
|
+
border-color: var(--primary);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.sheet-options-list {
|
|
529
|
+
list-style: none;
|
|
530
|
+
padding: 0;
|
|
531
|
+
margin: 0;
|
|
532
|
+
overflow-y: auto;
|
|
533
|
+
flex: 1;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/* Animated Drilldown Slide Layout */
|
|
537
|
+
.drilldown-wrapper {
|
|
538
|
+
width: 100%;
|
|
539
|
+
overflow: hidden;
|
|
540
|
+
position: relative;
|
|
541
|
+
transition: height var(--speed3) cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.drilldown-track {
|
|
545
|
+
display: flex;
|
|
546
|
+
width: 200%;
|
|
547
|
+
transition: transform var(--speed3) cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.drilldown-track.slide-active {
|
|
551
|
+
transform: translateX(-50%);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.drilldown-panel {
|
|
555
|
+
width: 50%;
|
|
556
|
+
box-sizing: border-box;
|
|
557
|
+
list-style: none;
|
|
558
|
+
padding: 0;
|
|
559
|
+
margin: 0;
|
|
560
|
+
flex-shrink: 0;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.drilldown-panel.panel-active {
|
|
564
|
+
max-height: none;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.drilldown-panel.panel-inactive {
|
|
568
|
+
max-height: 0;
|
|
569
|
+
overflow: hidden;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/* Group / Subcategory header (Browsing mode - acts as a hover trigger for submenus) */
|
|
573
|
+
.group-header {
|
|
574
|
+
padding: 8px 16px;
|
|
575
|
+
font-size: 0.875rem;
|
|
576
|
+
font-weight: 500;
|
|
577
|
+
color: var(--on-surface);
|
|
578
|
+
cursor: pointer;
|
|
579
|
+
display: flex;
|
|
580
|
+
align-items: center;
|
|
581
|
+
position: relative;
|
|
582
|
+
user-select: none;
|
|
583
|
+
transition:
|
|
584
|
+
background-color var(--speed2) ease,
|
|
585
|
+
color var(--speed2) ease;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.group-header:hover,
|
|
589
|
+
.group-header.active {
|
|
590
|
+
background-color: var(--surface-container-high);
|
|
591
|
+
color: var(--primary);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/* Inline header (Searching mode & Sheet mode - flat layout) */
|
|
595
|
+
.group-header-flat {
|
|
596
|
+
padding: 6px 16px;
|
|
597
|
+
font-size: 0.75rem;
|
|
598
|
+
font-weight: 700;
|
|
599
|
+
text-transform: uppercase;
|
|
600
|
+
color: var(--primary);
|
|
601
|
+
letter-spacing: 0.05em;
|
|
602
|
+
background-color: var(--surface-container-low);
|
|
603
|
+
user-select: none;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/* Hierarchical sub-menu with premium flyout animation */
|
|
607
|
+
.submenu {
|
|
608
|
+
display: block;
|
|
609
|
+
opacity: 0;
|
|
610
|
+
visibility: hidden;
|
|
611
|
+
position: absolute;
|
|
612
|
+
top: 0;
|
|
613
|
+
/* Intermediate flyouts must keep overflow visible so deeper
|
|
614
|
+
subcategories are never clipped. Oversized trees switch to the
|
|
615
|
+
scrollable inline drilldown layout before rendering. */
|
|
616
|
+
overflow: visible;
|
|
617
|
+
background-color: var(--surface-container-highest);
|
|
618
|
+
box-shadow: var(--elevate3);
|
|
619
|
+
border-radius: 0.5rem;
|
|
620
|
+
padding: 4px 0;
|
|
621
|
+
list-style: none;
|
|
622
|
+
min-width: 160px;
|
|
623
|
+
z-index: 110;
|
|
624
|
+
transition:
|
|
625
|
+
opacity var(--speed2) cubic-bezier(0.2, 0.8, 0.2, 1),
|
|
626
|
+
transform var(--speed2) cubic-bezier(0.34, 1.56, 0.64, 1),
|
|
627
|
+
visibility var(--speed2);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.submenu.open-right {
|
|
631
|
+
left: 100%;
|
|
632
|
+
margin-left: 2px;
|
|
633
|
+
transform: scale(0.9) translateX(-10px);
|
|
634
|
+
transform-origin: top left;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.submenu.open-left {
|
|
638
|
+
right: 100%;
|
|
639
|
+
margin-right: 2px;
|
|
640
|
+
transform: scale(0.9) translateX(10px);
|
|
641
|
+
transform-origin: top right;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.group-header:hover > .submenu.active,
|
|
645
|
+
.group-header.active > .submenu.active {
|
|
646
|
+
opacity: 1;
|
|
647
|
+
visibility: visible;
|
|
648
|
+
transform: scale(1) translateX(0);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.option-item {
|
|
652
|
+
cursor: pointer;
|
|
653
|
+
padding: 8px 16px;
|
|
654
|
+
font-size: 0.875rem;
|
|
655
|
+
color: var(--on-surface);
|
|
656
|
+
display: flex;
|
|
657
|
+
align-items: center;
|
|
658
|
+
transition: background-color var(--speed2) ease;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.option-item:hover,
|
|
662
|
+
.option-item.active-nav {
|
|
663
|
+
background-color: var(--surface-container-high);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.option-item.selected {
|
|
667
|
+
background-color: var(--tertiary-container);
|
|
668
|
+
color: var(--on-tertiary-container);
|
|
669
|
+
font-weight: 500;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.option-item.disabled {
|
|
673
|
+
opacity: 0.5;
|
|
674
|
+
pointer-events: none;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.no-options {
|
|
678
|
+
padding: 12px 16px;
|
|
679
|
+
font-size: 0.875rem;
|
|
680
|
+
color: var(--outline);
|
|
681
|
+
text-align: center;
|
|
682
|
+
}
|
|
683
|
+
`,
|
|
684
|
+
];
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Hook del ciclo de vida (Lit).
|
|
688
|
+
* Vincula event listeners globales necesarios para la lógica del dropdown.
|
|
689
|
+
* Se usa `capture: true` en el evento de scroll para poder reaccionar
|
|
690
|
+
* al desplazamiento de cualquier contenedor padre y reposicionar el menú a tiempo.
|
|
691
|
+
*/
|
|
692
|
+
override connectedCallback() {
|
|
693
|
+
super.connectedCallback();
|
|
694
|
+
document.addEventListener("click", this._handleOutsideClick);
|
|
695
|
+
document.addEventListener(
|
|
696
|
+
"moni-select-open",
|
|
697
|
+
this._handlePeerOpen as EventListener,
|
|
698
|
+
);
|
|
699
|
+
window.addEventListener("scroll", this._handleScroll, { capture: true });
|
|
700
|
+
window.addEventListener("resize", this._handleResize);
|
|
701
|
+
window.visualViewport?.addEventListener("scroll", this._handleScroll);
|
|
702
|
+
window.visualViewport?.addEventListener("resize", this._handleResize);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Hook del ciclo de vida (Lit).
|
|
707
|
+
* Desmonta rigurosamente todos los listeners globales para prevenir memory leaks
|
|
708
|
+
* o llamadas accidentales cuando el select ya fue destruido del DOM.
|
|
709
|
+
*/
|
|
710
|
+
override disconnectedCallback() {
|
|
711
|
+
this._hideBodyPopover();
|
|
712
|
+
super.disconnectedCallback();
|
|
713
|
+
document.removeEventListener("click", this._handleOutsideClick);
|
|
714
|
+
document.removeEventListener(
|
|
715
|
+
"moni-select-open",
|
|
716
|
+
this._handlePeerOpen as EventListener,
|
|
717
|
+
);
|
|
718
|
+
window.removeEventListener("scroll", this._handleScroll, { capture: true });
|
|
719
|
+
window.removeEventListener("resize", this._handleResize);
|
|
720
|
+
window.visualViewport?.removeEventListener("scroll", this._handleScroll);
|
|
721
|
+
window.visualViewport?.removeEventListener("resize", this._handleResize);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Hook de actualización reactiva (Lit).
|
|
726
|
+
* Verifica si alguna propiedad relacionada con el tamaño del contenido (como la búsqueda,
|
|
727
|
+
* la ruta de anidación o el estado abierto) ha cambiado, para forzar un recálculo
|
|
728
|
+
* de la altura animada del contenedor `drilldown-wrapper`.
|
|
729
|
+
*/
|
|
730
|
+
override updated(changedProperties: Map<string | number | symbol, unknown>) {
|
|
731
|
+
super.updated(changedProperties);
|
|
732
|
+
if (changedProperties.has("value")) {
|
|
733
|
+
this._internals?.setFormValue?.(this.value);
|
|
734
|
+
}
|
|
735
|
+
this._syncValidity();
|
|
736
|
+
if (
|
|
737
|
+
changedProperties.has("_open") ||
|
|
738
|
+
changedProperties.has("_drilldownPath") ||
|
|
739
|
+
changedProperties.has("_searchQuery") ||
|
|
740
|
+
changedProperties.has("_useInlineCategories") ||
|
|
741
|
+
changedProperties.has("_parsedOptions")
|
|
742
|
+
) {
|
|
743
|
+
this._updateWrapperHeight();
|
|
744
|
+
}
|
|
745
|
+
if (
|
|
746
|
+
this._open &&
|
|
747
|
+
(changedProperties.has("positioning") ||
|
|
748
|
+
changedProperties.has("placement") ||
|
|
749
|
+
changedProperties.has("dropdownWidth") ||
|
|
750
|
+
changedProperties.has("dropdownMaxHeight"))
|
|
751
|
+
) {
|
|
752
|
+
this._updateMenuPosition();
|
|
753
|
+
}
|
|
754
|
+
if (
|
|
755
|
+
changedProperties.has("_open") ||
|
|
756
|
+
changedProperties.has("positioning") ||
|
|
757
|
+
changedProperties.has("sheet")
|
|
758
|
+
) {
|
|
759
|
+
this._syncBodyPopover();
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Lógica de animación de altura (Height Animation).
|
|
765
|
+
* Cuando se navega entre categorías anidadas (`_drilldownPath`), las listas hijas pueden
|
|
766
|
+
* tener diferentes tamaños. Este método lee el `scrollHeight` del panel activo y lo aplica
|
|
767
|
+
* imperativamente al `wrapper` para disparar una transición CSS suave en la altura del dropdown.
|
|
768
|
+
*/
|
|
769
|
+
private _updateWrapperHeight() {
|
|
770
|
+
const wrapper = this.shadowRoot?.querySelector(
|
|
771
|
+
".drilldown-wrapper",
|
|
772
|
+
) as HTMLElement;
|
|
773
|
+
if (!wrapper) return;
|
|
774
|
+
|
|
775
|
+
if (!this._open || this.sheet) {
|
|
776
|
+
wrapper.style.height = "";
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Wait for Lit render and styles to apply
|
|
781
|
+
requestAnimationFrame(() => {
|
|
782
|
+
const activePanel = this.shadowRoot?.querySelector(
|
|
783
|
+
".drilldown-panel.panel-active",
|
|
784
|
+
) as HTMLElement;
|
|
785
|
+
if (activePanel) {
|
|
786
|
+
wrapper.style.height = `${activePanel.scrollHeight}px`;
|
|
787
|
+
} else {
|
|
788
|
+
wrapper.style.height = "";
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Cierra el Dropdown cuando el usuario hace clic fuera de las dimensiones del componente.
|
|
795
|
+
* Utiliza `e.composedPath()` para poder atravesar barreras de Shadow DOM si el select
|
|
796
|
+
* está dentro de un contenedor web-component externo.
|
|
797
|
+
*/
|
|
798
|
+
private _handleOutsideClick = (e: MouseEvent) => {
|
|
799
|
+
if (this._open && !e.composedPath().includes(this)) {
|
|
800
|
+
this._closeDropdown();
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
/** Cierra este menú cuando otro `<moni-select>` anuncia que va a abrirse. */
|
|
805
|
+
private _handlePeerOpen = (e: Event) => {
|
|
806
|
+
const source = (e as CustomEvent<{ item?: MoniSelect }>).detail?.item;
|
|
807
|
+
if (source && source !== this && this._open) {
|
|
808
|
+
this._closeDropdown();
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Sincroniza el modo `positioning="body"` con la Popover API. Un popover
|
|
814
|
+
* permanece en este Shadow DOM para conservar estilos y listeners, pero el
|
|
815
|
+
* navegador lo promueve a la capa superior, fuera de cualquier `overflow`.
|
|
816
|
+
*/
|
|
817
|
+
private _syncBodyPopover() {
|
|
818
|
+
const menu = this._menu;
|
|
819
|
+
if (!menu || typeof menu.showPopover !== "function") return;
|
|
820
|
+
|
|
821
|
+
const shouldBeOpen =
|
|
822
|
+
this._open && !this.sheet && this.positioning === "body";
|
|
823
|
+
try {
|
|
824
|
+
const isOpen = menu.matches(":popover-open");
|
|
825
|
+
if (shouldBeOpen && !isOpen) {
|
|
826
|
+
menu.showPopover();
|
|
827
|
+
} else if (!shouldBeOpen && isOpen) {
|
|
828
|
+
menu.hidePopover();
|
|
829
|
+
}
|
|
830
|
+
} catch {
|
|
831
|
+
/* Browsers without a complete Popover implementation use fixed fallback. */
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
private _hideBodyPopover() {
|
|
836
|
+
const menu = this._menu;
|
|
837
|
+
if (!menu || typeof menu.hidePopover !== "function") return;
|
|
838
|
+
try {
|
|
839
|
+
if (menu.matches(":popover-open")) menu.hidePopover();
|
|
840
|
+
} catch {
|
|
841
|
+
/* No-op during teardown or in partial DOM implementations. */
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Motor de Cálculo de Colisiones de la Interfaz (Collision Detection).
|
|
847
|
+
*
|
|
848
|
+
* @logic
|
|
849
|
+
* 1. Calcula las coordenadas espaciales (`getBoundingClientRect`) del select base.
|
|
850
|
+
* 2. Compara el espacio disponible (`spaceAbove`, `spaceBelow`, `spaceRight`, `spaceLeft`)
|
|
851
|
+
* respecto al viewport (`window.innerHeight`, `window.innerWidth`).
|
|
852
|
+
* 3. Si el dropdown no cabe en la posición preferida (`placement`), revierte lógicamente
|
|
853
|
+
* la posición (ej: si `bottom` desborda, lo abre hacia `top`).
|
|
854
|
+
* 4. Actualiza `_actualPlacement` con la dirección segura calculada.
|
|
855
|
+
*/
|
|
856
|
+
private _getViewportMetrics() {
|
|
857
|
+
const viewport = window.visualViewport;
|
|
858
|
+
const top = viewport?.offsetTop ?? 0;
|
|
859
|
+
const left = viewport?.offsetLeft ?? 0;
|
|
860
|
+
const height = viewport?.height ?? window.innerHeight;
|
|
861
|
+
const width = viewport?.width ?? window.innerWidth;
|
|
862
|
+
return {
|
|
863
|
+
top,
|
|
864
|
+
left,
|
|
865
|
+
height,
|
|
866
|
+
width,
|
|
867
|
+
bottom: top + height,
|
|
868
|
+
right: left + width,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Calcula el espacio vertical en una dirección. Además del espacio físico,
|
|
874
|
+
* descuenta la posición del trigger del presupuesto de 75% del viewport:
|
|
875
|
+
* `75vh - offset del campo`, tal como se espera para campos a media pantalla.
|
|
876
|
+
*/
|
|
877
|
+
private _getDirectionalAvailableHeight(
|
|
878
|
+
direction: "top" | "bottom",
|
|
879
|
+
rect: DOMRectReadOnly,
|
|
880
|
+
): number {
|
|
881
|
+
const viewport = this._getViewportMetrics();
|
|
882
|
+
const gap = 8;
|
|
883
|
+
const viewportCap = viewport.height * 0.75;
|
|
884
|
+
|
|
885
|
+
if (direction === "bottom") {
|
|
886
|
+
const physicalSpace = viewport.bottom - rect.bottom - gap;
|
|
887
|
+
const triggerOffset = rect.bottom - viewport.top + gap;
|
|
888
|
+
return Math.max(
|
|
889
|
+
0,
|
|
890
|
+
Math.floor(
|
|
891
|
+
Math.min(viewportCap, physicalSpace, viewportCap - triggerOffset),
|
|
892
|
+
),
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const physicalSpace = rect.top - viewport.top - gap;
|
|
897
|
+
const triggerOffset = viewport.bottom - rect.top + gap;
|
|
898
|
+
return Math.max(
|
|
899
|
+
0,
|
|
900
|
+
Math.floor(
|
|
901
|
+
Math.min(viewportCap, physicalSpace, viewportCap - triggerOffset),
|
|
902
|
+
),
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
private _normalizeManualMaxHeight(): string {
|
|
907
|
+
const raw = this.dropdownMaxHeight.trim();
|
|
908
|
+
if (!raw) return "";
|
|
909
|
+
const normalized = /^\d+(?:\.\d+)?$/.test(raw) ? `${raw}px` : raw;
|
|
910
|
+
if (
|
|
911
|
+
typeof CSS !== "undefined" &&
|
|
912
|
+
typeof CSS.supports === "function" &&
|
|
913
|
+
!CSS.supports("max-height", normalized)
|
|
914
|
+
) {
|
|
915
|
+
return "";
|
|
916
|
+
}
|
|
917
|
+
return normalized;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** Convierte las unidades manuales comunes a px para el motor de colisiones. */
|
|
921
|
+
private _resolveManualMaxHeightPx(
|
|
922
|
+
value: string,
|
|
923
|
+
viewportHeight: number,
|
|
924
|
+
): number | null {
|
|
925
|
+
const match = value.match(/^(\d+(?:\.\d+)?)(px|vh|dvh|svh|lvh|%|rem)$/i);
|
|
926
|
+
if (!match) return null;
|
|
927
|
+
const amount = Number(match[1]);
|
|
928
|
+
const unit = match[2].toLowerCase();
|
|
929
|
+
if (unit === "px") return amount;
|
|
930
|
+
if (unit === "rem") {
|
|
931
|
+
const rootSize =
|
|
932
|
+
Number.parseFloat(
|
|
933
|
+
getComputedStyle(document.documentElement).fontSize,
|
|
934
|
+
) || 16;
|
|
935
|
+
return amount * rootSize;
|
|
936
|
+
}
|
|
937
|
+
return viewportHeight * (amount / 100);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
private _getMenuHeightConstraint(
|
|
941
|
+
rect: DOMRectReadOnly,
|
|
942
|
+
placement = this._actualPlacement,
|
|
943
|
+
): { css: string; px: number } {
|
|
944
|
+
const viewport = this._getViewportMetrics();
|
|
945
|
+
const automatic =
|
|
946
|
+
placement === "top" || placement === "bottom"
|
|
947
|
+
? this._getDirectionalAvailableHeight(placement, rect)
|
|
948
|
+
: Math.max(
|
|
949
|
+
0,
|
|
950
|
+
Math.floor(Math.min(viewport.height * 0.75, viewport.height - 16)),
|
|
951
|
+
);
|
|
952
|
+
const manualCss = this._normalizeManualMaxHeight();
|
|
953
|
+
const manualPx = manualCss
|
|
954
|
+
? this._resolveManualMaxHeightPx(manualCss, viewport.height)
|
|
955
|
+
: null;
|
|
956
|
+
const px = manualPx === null ? automatic : Math.min(automatic, manualPx);
|
|
957
|
+
|
|
958
|
+
return {
|
|
959
|
+
css: manualCss ? `min(${automatic}px, ${manualCss})` : `${automatic}px`,
|
|
960
|
+
px: Math.max(0, px),
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
private _estimateDesiredMenuHeight(): number {
|
|
965
|
+
const viewport = this._getViewportMetrics();
|
|
966
|
+
const itemCount = this._searchQuery
|
|
967
|
+
? this._getFilteredOptions().length
|
|
968
|
+
: this._parsedOptions.length;
|
|
969
|
+
let desired = Math.min(
|
|
970
|
+
viewport.height * 0.75,
|
|
971
|
+
Math.max(48, itemCount * 40 + 8),
|
|
972
|
+
);
|
|
973
|
+
const manual = this._normalizeManualMaxHeight();
|
|
974
|
+
const manualPx = manual
|
|
975
|
+
? this._resolveManualMaxHeightPx(manual, viewport.height)
|
|
976
|
+
: null;
|
|
977
|
+
if (manualPx !== null) desired = Math.min(desired, manualPx);
|
|
978
|
+
return desired;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/** Profundidad máxima de flyouts necesaria para representar el árbol. */
|
|
982
|
+
private _getGroupDepth(nodes: DropdownNode[]): number {
|
|
983
|
+
return nodes.reduce((depth, node) => {
|
|
984
|
+
if (node.type === "option") return depth;
|
|
985
|
+
return Math.max(depth, 1 + this._getGroupDepth(node.children));
|
|
986
|
+
}, 0);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** Detecta cualquier nivel cuya lista directa necesitaría scroll propio. */
|
|
990
|
+
private _hasOversizedGroupPanel(
|
|
991
|
+
nodes: DropdownNode[],
|
|
992
|
+
maxHeight: number,
|
|
993
|
+
): boolean {
|
|
994
|
+
return nodes.some((node) => {
|
|
995
|
+
if (node.type === "option") return false;
|
|
996
|
+
const panelHeight = node.children.length * 40 + 8;
|
|
997
|
+
return (
|
|
998
|
+
panelHeight > maxHeight ||
|
|
999
|
+
this._hasOversizedGroupPanel(node.children, maxHeight)
|
|
1000
|
+
);
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Mide el espacio horizontal real para flyouts. En modo local también
|
|
1006
|
+
* intersecta ancestros con overflow para no diseñar submenús fuera de una
|
|
1007
|
+
* región que terminaría recortándolos. `body` ignora esos ancestros porque
|
|
1008
|
+
* se renderiza en la top layer.
|
|
1009
|
+
*/
|
|
1010
|
+
private _getAvailableFlyoutWidth(rect: DOMRectReadOnly): number {
|
|
1011
|
+
const viewport = this._getViewportMetrics();
|
|
1012
|
+
let boundaryLeft = viewport.left;
|
|
1013
|
+
let boundaryRight = viewport.right;
|
|
1014
|
+
|
|
1015
|
+
if (this.positioning !== "body") {
|
|
1016
|
+
let current: Node | null = this;
|
|
1017
|
+
while (current) {
|
|
1018
|
+
const parent: Node | null =
|
|
1019
|
+
current.parentNode instanceof ShadowRoot
|
|
1020
|
+
? current.parentNode.host
|
|
1021
|
+
: current.parentNode;
|
|
1022
|
+
if (parent instanceof HTMLElement) {
|
|
1023
|
+
const styles = getComputedStyle(parent);
|
|
1024
|
+
if (/(auto|scroll|hidden|clip)/.test(styles.overflowX)) {
|
|
1025
|
+
const parentRect = parent.getBoundingClientRect();
|
|
1026
|
+
boundaryLeft = Math.max(boundaryLeft, parentRect.left);
|
|
1027
|
+
boundaryRight = Math.min(boundaryRight, parentRect.right);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
current = parent;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return Math.max(
|
|
1035
|
+
0,
|
|
1036
|
+
Math.max(boundaryRight - rect.right, rect.left - boundaryLeft),
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
private _determineActualPlacement() {
|
|
1041
|
+
if (this.sheet) {
|
|
1042
|
+
this._actualPlacement = "bottom";
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const rect = this.getBoundingClientRect();
|
|
1047
|
+
const viewport = this._getViewportMetrics();
|
|
1048
|
+
const spaceBelow = this._getDirectionalAvailableHeight("bottom", rect);
|
|
1049
|
+
const spaceAbove = this._getDirectionalAvailableHeight("top", rect);
|
|
1050
|
+
const spaceRight = viewport.right - rect.right;
|
|
1051
|
+
const spaceLeft = rect.left - viewport.left;
|
|
1052
|
+
const menuHeight = this._estimateDesiredMenuHeight();
|
|
1053
|
+
const menuWidth = 180;
|
|
1054
|
+
|
|
1055
|
+
let preferred = this.placement;
|
|
1056
|
+
if (preferred === "auto") {
|
|
1057
|
+
preferred =
|
|
1058
|
+
spaceBelow >= menuHeight
|
|
1059
|
+
? "bottom"
|
|
1060
|
+
: spaceAbove > spaceBelow
|
|
1061
|
+
? "top"
|
|
1062
|
+
: "bottom";
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (preferred === "top") {
|
|
1066
|
+
this._actualPlacement =
|
|
1067
|
+
spaceAbove < menuHeight && spaceBelow > spaceAbove ? "bottom" : "top";
|
|
1068
|
+
} else if (preferred === "bottom") {
|
|
1069
|
+
this._actualPlacement =
|
|
1070
|
+
spaceBelow < menuHeight && spaceAbove > spaceBelow ? "top" : "bottom";
|
|
1071
|
+
} else if (preferred === "left") {
|
|
1072
|
+
this._actualPlacement =
|
|
1073
|
+
spaceLeft < menuWidth && spaceRight > spaceLeft ? "right" : "left";
|
|
1074
|
+
} else if (preferred === "right") {
|
|
1075
|
+
this._actualPlacement =
|
|
1076
|
+
spaceRight < menuWidth && spaceLeft > spaceRight ? "left" : "right";
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Handler de Scroll global.
|
|
1082
|
+
* Fuerza un recálculo de la posición y dimensiones del dropdown si el usuario
|
|
1083
|
+
* hace scroll en la página mientras el select permanece abierto.
|
|
1084
|
+
*/
|
|
1085
|
+
private _handleScroll = () => {
|
|
1086
|
+
if (this._open) {
|
|
1087
|
+
this._determineActualPlacement();
|
|
1088
|
+
this._updateMenuPosition();
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Handler de Resize global.
|
|
1094
|
+
* Asegura que el popover del select no quede fuera del viewport cuando
|
|
1095
|
+
* se cambia el tamaño de la ventana o se rota la pantalla en dispositivos móviles.
|
|
1096
|
+
*/
|
|
1097
|
+
private _handleResize = () => {
|
|
1098
|
+
if (this._open) {
|
|
1099
|
+
this._determineActualPlacement();
|
|
1100
|
+
this._updateMenuPosition();
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Aplica las coordenadas y dimensiones absolutas o fijas al menú desplegable.
|
|
1106
|
+
* Este método es fundamental cuando `positioning === 'fixed'`, ya que el dropdown
|
|
1107
|
+
* se saca del flujo normal del documento para evitar `overflow: hidden` de los padres.
|
|
1108
|
+
* Mapea el `width` para coincidir opcionalmente con el gatillo (`dropdownWidth="trigger"`).
|
|
1109
|
+
*/
|
|
1110
|
+
private _updateMenuPosition() {
|
|
1111
|
+
if (!this._open || this.sheet) return;
|
|
1112
|
+
|
|
1113
|
+
this._determineActualPlacement();
|
|
1114
|
+
const rect = this.getBoundingClientRect();
|
|
1115
|
+
const viewport = this._getViewportMetrics();
|
|
1116
|
+
const constraint = this._getMenuHeightConstraint(rect);
|
|
1117
|
+
this._menuMaxHeight = constraint.css;
|
|
1118
|
+
|
|
1119
|
+
const hasGroups = this._parsedOptions.some((node) => node.type === "group");
|
|
1120
|
+
const estimatedRootHeight = this._parsedOptions.length * 40 + 8;
|
|
1121
|
+
const groupDepth = this._getGroupDepth(this._parsedOptions);
|
|
1122
|
+
const requiredFlyoutWidth = groupDepth * 160;
|
|
1123
|
+
const availableFlyoutWidth = this._getAvailableFlyoutWidth(rect);
|
|
1124
|
+
const lacksFlyoutSpace =
|
|
1125
|
+
viewport.width < 600 ||
|
|
1126
|
+
(hasGroups && availableFlyoutWidth < requiredFlyoutWidth);
|
|
1127
|
+
const needsVerticalDrilldown =
|
|
1128
|
+
hasGroups &&
|
|
1129
|
+
(estimatedRootHeight > constraint.px ||
|
|
1130
|
+
this._hasOversizedGroupPanel(this._parsedOptions, constraint.px));
|
|
1131
|
+
this._useInlineCategories = lacksFlyoutSpace || needsVerticalDrilldown;
|
|
1132
|
+
|
|
1133
|
+
if (this.positioning !== "fixed" && this.positioning !== "body") {
|
|
1134
|
+
this._menuStyle = "";
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
let widthStyle = "";
|
|
1139
|
+
if (this.dropdownWidth === "trigger") {
|
|
1140
|
+
widthStyle = `width: ${rect.width}px;`;
|
|
1141
|
+
} else if (this.dropdownWidth === "auto") {
|
|
1142
|
+
widthStyle = "width: auto; min-width: 160px; max-width: 320px;";
|
|
1143
|
+
} else {
|
|
1144
|
+
widthStyle = `width: ${this.dropdownWidth};`;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (this._actualPlacement === "top") {
|
|
1148
|
+
this._menuStyle = `
|
|
1149
|
+
position: fixed;
|
|
1150
|
+
bottom: ${window.innerHeight - rect.top}px;
|
|
1151
|
+
left: ${rect.left}px;
|
|
1152
|
+
${widthStyle}
|
|
1153
|
+
top: auto;
|
|
1154
|
+
right: auto;
|
|
1155
|
+
`;
|
|
1156
|
+
} else if (this._actualPlacement === "bottom") {
|
|
1157
|
+
this._menuStyle = `
|
|
1158
|
+
position: fixed;
|
|
1159
|
+
top: ${rect.bottom}px;
|
|
1160
|
+
left: ${rect.left}px;
|
|
1161
|
+
${widthStyle}
|
|
1162
|
+
bottom: auto;
|
|
1163
|
+
right: auto;
|
|
1164
|
+
`;
|
|
1165
|
+
} else {
|
|
1166
|
+
const sideTop = Math.max(
|
|
1167
|
+
viewport.top + 8,
|
|
1168
|
+
Math.min(rect.top, viewport.bottom - constraint.px - 8),
|
|
1169
|
+
);
|
|
1170
|
+
if (this._actualPlacement === "left") {
|
|
1171
|
+
this._menuStyle = `
|
|
1172
|
+
position: fixed;
|
|
1173
|
+
top: ${sideTop}px;
|
|
1174
|
+
right: ${window.innerWidth - rect.left + 4}px;
|
|
1175
|
+
${widthStyle}
|
|
1176
|
+
left: auto;
|
|
1177
|
+
bottom: auto;
|
|
1178
|
+
`;
|
|
1179
|
+
} else {
|
|
1180
|
+
this._menuStyle = `
|
|
1181
|
+
position: fixed;
|
|
1182
|
+
top: ${sideTop}px;
|
|
1183
|
+
left: ${rect.right + 4}px;
|
|
1184
|
+
${widthStyle}
|
|
1185
|
+
right: auto;
|
|
1186
|
+
bottom: auto;
|
|
1187
|
+
`;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Devuelve las coordenadas, el ancho y la restricción dinámica de altura del
|
|
1194
|
+
* menú. `body` comparte coordenadas fixed, pero se pinta en la capa superior.
|
|
1195
|
+
*/
|
|
1196
|
+
private _getMenuStyle() {
|
|
1197
|
+
const constraintStyle = `max-height: ${this._menuMaxHeight}; --_select-menu-max-height: ${this._menuMaxHeight};`;
|
|
1198
|
+
if (this.positioning === "fixed" || this.positioning === "body") {
|
|
1199
|
+
return `${this._menuStyle} ${constraintStyle}`;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
if (this.dropdownWidth === "auto") {
|
|
1203
|
+
return `width: max-content; min-width: 160px; max-width: 320px; ${constraintStyle}`;
|
|
1204
|
+
} else if (this.dropdownWidth !== "trigger") {
|
|
1205
|
+
return `width: ${this.dropdownWidth}; ${constraintStyle}`;
|
|
1206
|
+
}
|
|
1207
|
+
return constraintStyle;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Parseador del Shadow DOM (Slot Change Handler).
|
|
1212
|
+
*
|
|
1213
|
+
* @logic
|
|
1214
|
+
* 1. Cuando los hijos proyectados cambian (ej. se agregan nuevos `<option>`),
|
|
1215
|
+
* lee iterativamente todos los nodos del Light DOM.
|
|
1216
|
+
* 2. Si encuentra `<optgroup>` o `<moni-select-option group="A/B">`, construye dinámicamente
|
|
1217
|
+
* un árbol estructural de `GroupNode` anidados.
|
|
1218
|
+
* 3. Si encuentra opciones, las inyecta en el grupo correspondiente del árbol.
|
|
1219
|
+
* 4. Almacena el árbol resultante en `_parsedOptions` para que el componente
|
|
1220
|
+
* pueda renderizar sus propios submenús nativos independientemente del markup inicial.
|
|
1221
|
+
*/
|
|
1222
|
+
private _onSlotChange() {
|
|
1223
|
+
if (!this._slot) return;
|
|
1224
|
+
const assigned = this._slot.assignedElements({ flatten: true });
|
|
1225
|
+
|
|
1226
|
+
const tree: DropdownNode[] = [];
|
|
1227
|
+
|
|
1228
|
+
const findOrCreateGroup = (
|
|
1229
|
+
nodes: DropdownNode[],
|
|
1230
|
+
label: string,
|
|
1231
|
+
): GroupNode => {
|
|
1232
|
+
let group = nodes.find(
|
|
1233
|
+
(n) => n.type === "group" && n.label === label,
|
|
1234
|
+
) as GroupNode;
|
|
1235
|
+
if (!group) {
|
|
1236
|
+
group = { type: "group", label, children: [] };
|
|
1237
|
+
nodes.push(group);
|
|
1238
|
+
}
|
|
1239
|
+
return group;
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
const insertOption = (
|
|
1243
|
+
targetTree: DropdownNode[],
|
|
1244
|
+
opt: OptionNode,
|
|
1245
|
+
groupPath: string[],
|
|
1246
|
+
) => {
|
|
1247
|
+
let currentLevel = targetTree;
|
|
1248
|
+
if (groupPath.length > 0) {
|
|
1249
|
+
opt.group = groupPath.join(" / ");
|
|
1250
|
+
}
|
|
1251
|
+
groupPath.forEach((part) => {
|
|
1252
|
+
const group = findOrCreateGroup(currentLevel, part);
|
|
1253
|
+
currentLevel = group.children;
|
|
1254
|
+
});
|
|
1255
|
+
currentLevel.push(opt);
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
const parseElement = (el: Element, parentGroupPath: string[]) => {
|
|
1259
|
+
const tag = el.tagName.toLowerCase();
|
|
1260
|
+
if (tag === "option" || tag === "moni-select-option") {
|
|
1261
|
+
const opt: OptionNode = {
|
|
1262
|
+
type: "option",
|
|
1263
|
+
value: el.getAttribute("value") || "",
|
|
1264
|
+
label:
|
|
1265
|
+
el.textContent?.trim() ||
|
|
1266
|
+
el.getAttribute("label") ||
|
|
1267
|
+
el.getAttribute("value") ||
|
|
1268
|
+
"",
|
|
1269
|
+
disabled: el.hasAttribute("disabled"),
|
|
1270
|
+
element: el as HTMLElement,
|
|
1271
|
+
};
|
|
1272
|
+
const groupAttr = el.getAttribute("group");
|
|
1273
|
+
const localPath = groupAttr
|
|
1274
|
+
? groupAttr
|
|
1275
|
+
.split("/")
|
|
1276
|
+
.map((s) => s.trim())
|
|
1277
|
+
.filter(Boolean)
|
|
1278
|
+
: [];
|
|
1279
|
+
const finalPath = [...parentGroupPath, ...localPath];
|
|
1280
|
+
insertOption(tree, opt, finalPath);
|
|
1281
|
+
} else if (tag === "optgroup") {
|
|
1282
|
+
const label = el.getAttribute("label") || "";
|
|
1283
|
+
const localPath = [...parentGroupPath, label];
|
|
1284
|
+
Array.from(el.children).forEach((child) =>
|
|
1285
|
+
parseElement(child, localPath),
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
assigned.forEach((el) => parseElement(el, []));
|
|
1291
|
+
this._parsedOptions = tree;
|
|
1292
|
+
this.requestUpdate();
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* Alterna el estado de apertura/cierre del dropdown, previniendo
|
|
1297
|
+
* la acción si el componente está deshabilitado.
|
|
1298
|
+
*/
|
|
1299
|
+
private _toggleDropdown(e?: Event) {
|
|
1300
|
+
if (e) {
|
|
1301
|
+
e.stopPropagation();
|
|
1302
|
+
}
|
|
1303
|
+
if (this.disabled) return;
|
|
1304
|
+
if (this._open) {
|
|
1305
|
+
this._closeDropdown();
|
|
1306
|
+
} else {
|
|
1307
|
+
this._openDropdown();
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Manejador de clics en el campo de texto (Trigger).
|
|
1313
|
+
* Si el componente es `searchable` (permite escribir para buscar), un clic
|
|
1314
|
+
* sólo abrirá el menú (no lo cerrará si ya estaba abierto, para permitir al usuario seguir escribiendo).
|
|
1315
|
+
*/
|
|
1316
|
+
private _onInputClick(e: MouseEvent) {
|
|
1317
|
+
if (this.disabled) return;
|
|
1318
|
+
if (this.searchable && !this.sheet) {
|
|
1319
|
+
if (!this._open) {
|
|
1320
|
+
this._openDropdown();
|
|
1321
|
+
}
|
|
1322
|
+
} else {
|
|
1323
|
+
this._toggleDropdown(e);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Despliega el menú del select y coordina la accesibilidad, posicionamiento y foco inicial.
|
|
1329
|
+
*/
|
|
1330
|
+
private _openDropdown() {
|
|
1331
|
+
if (this.disabled || this._open) return;
|
|
1332
|
+
|
|
1333
|
+
this._open = true;
|
|
1334
|
+
this._activeIndex = -1;
|
|
1335
|
+
this._drilldownPath = [];
|
|
1336
|
+
// La búsqueda siempre comienza limpia, aunque exista un valor seleccionado.
|
|
1337
|
+
this._searchQuery = "";
|
|
1338
|
+
|
|
1339
|
+
// El evento global permite que cualquier otro select abierto se cierre,
|
|
1340
|
+
// incluso cuando la apertura vino del teclado o del icono trailing.
|
|
1341
|
+
emitMoniEvent(this, "moni-select-open", { detail: { item: this } });
|
|
1342
|
+
|
|
1343
|
+
this._updateMenuPosition();
|
|
1344
|
+
|
|
1345
|
+
if (this.searchable) {
|
|
1346
|
+
// Esperamos un tick para que el input o sheet esté renderizado antes de enfocarlo.
|
|
1347
|
+
setTimeout(() => {
|
|
1348
|
+
if (!this._open) return;
|
|
1349
|
+
if (this._input && !this.sheet) {
|
|
1350
|
+
this._input.focus();
|
|
1351
|
+
} else if (this.sheet) {
|
|
1352
|
+
const sheetInput = this.shadowRoot?.querySelector(
|
|
1353
|
+
".sheet-search-input",
|
|
1354
|
+
) as HTMLInputElement;
|
|
1355
|
+
sheetInput?.focus();
|
|
1356
|
+
}
|
|
1357
|
+
}, 50);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Cierra el dropdown y purga la consulta de búsqueda (`_searchQuery`)
|
|
1363
|
+
* para que la próxima vez que se abra, la lista muestre todas las opciones por defecto.
|
|
1364
|
+
*/
|
|
1365
|
+
private _closeDropdown() {
|
|
1366
|
+
if (!this._open) return;
|
|
1367
|
+
this._open = false;
|
|
1368
|
+
this._searchQuery = "";
|
|
1369
|
+
this._hideBodyPopover();
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
private _findOptionByValue(value: string): OptionNode | undefined {
|
|
1373
|
+
return this._getFlatOptions(this._parsedOptions).find(
|
|
1374
|
+
(opt) => opt.value === value,
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/**
|
|
1379
|
+
* Evento de input (tecleo) en la barra de búsqueda.
|
|
1380
|
+
*
|
|
1381
|
+
* @logic
|
|
1382
|
+
* 1. Actualiza `_searchQuery` y fuerza la apertura del dropdown.
|
|
1383
|
+
* 2. Si es `clearable` y el usuario borró todo, emite eventos vacíos.
|
|
1384
|
+
* 3. Busca coincidencias exactas en el texto. Si el usuario teclea exactamente el
|
|
1385
|
+
* label de una opción existente, la selecciona automáticamente (auto-completado nativo).
|
|
1386
|
+
*/
|
|
1387
|
+
private _onSearchInput(e: Event) {
|
|
1388
|
+
const query = (e.target as HTMLInputElement).value;
|
|
1389
|
+
if (!this._open) this._openDropdown();
|
|
1390
|
+
this._searchQuery = query;
|
|
1391
|
+
this._activeIndex = -1;
|
|
1392
|
+
|
|
1393
|
+
if (this.clearable && query.trim() === "") {
|
|
1394
|
+
this.value = "";
|
|
1395
|
+
emitMoniEvent(this, "moni-change", {
|
|
1396
|
+
detail: { value: this.value, originalEvent: e },
|
|
1397
|
+
});
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
const flat = this._getFlatOptions(this._parsedOptions);
|
|
1402
|
+
const exactMatch = flat.find(
|
|
1403
|
+
(opt) => opt.label.toLowerCase() === query.trim().toLowerCase(),
|
|
1404
|
+
);
|
|
1405
|
+
if (exactMatch && !exactMatch.disabled) {
|
|
1406
|
+
this._selectOption(exactMatch);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Confirma la selección de una opción específica.
|
|
1412
|
+
* Actualiza el valor (estado reactivo), cierra el dropdown y despacha los eventos
|
|
1413
|
+
* estándar HTML `input` y `change` para integrarse fluidamente con formularios nativos o frameworks.
|
|
1414
|
+
*/
|
|
1415
|
+
private _selectOption(option: OptionNode) {
|
|
1416
|
+
if (option.disabled) return;
|
|
1417
|
+
this.value = option.value;
|
|
1418
|
+
this._closeDropdown();
|
|
1419
|
+
|
|
1420
|
+
this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
1421
|
+
this.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
|
|
1422
|
+
emitMoniEvent(this, "moni-change", { detail: { value: this.value } });
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* Maneja el Hover sobre categorías en modo Desktop (Submenús emergentes).
|
|
1427
|
+
* Detecta dinámicamente si el submenú se saldrá del lado derecho de la pantalla.
|
|
1428
|
+
* Si es así, altera las clases de CSS (`open-left` vs `open-right`) para que
|
|
1429
|
+
* el menú flote en la dirección opuesta, evitando desbordamientos de viewport (Overflow prevention).
|
|
1430
|
+
*/
|
|
1431
|
+
private _onSubmenuMouseEnter(e: MouseEvent) {
|
|
1432
|
+
const headerEl = e.currentTarget as HTMLElement;
|
|
1433
|
+
const submenu = headerEl.querySelector(":scope > .submenu") as HTMLElement;
|
|
1434
|
+
if (!submenu) return;
|
|
1435
|
+
|
|
1436
|
+
const rect = headerEl.getBoundingClientRect();
|
|
1437
|
+
const spaceOnRight = window.innerWidth - rect.right;
|
|
1438
|
+
const spaceOnLeft = rect.left;
|
|
1439
|
+
const submenuWidth = 160;
|
|
1440
|
+
|
|
1441
|
+
if (spaceOnRight < submenuWidth && spaceOnLeft > spaceOnRight) {
|
|
1442
|
+
submenu.classList.remove("open-right");
|
|
1443
|
+
submenu.classList.add("open-left");
|
|
1444
|
+
} else {
|
|
1445
|
+
submenu.classList.remove("open-left");
|
|
1446
|
+
submenu.classList.add("open-right");
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Gestiona la navegación completa por teclado siguiendo el patrón WAI-ARIA Combobox.
|
|
1452
|
+
* Soporta flechas direccionales, selección (Enter), y cierre (Escape).
|
|
1453
|
+
*/
|
|
1454
|
+
private _onKeyDown(e: KeyboardEvent) {
|
|
1455
|
+
if (this.disabled) return;
|
|
1456
|
+
|
|
1457
|
+
// Obtenemos solo las opciones que están visualmente disponibles en la jerarquía actual
|
|
1458
|
+
const visible = this._getVisibleOptions();
|
|
1459
|
+
|
|
1460
|
+
if (e.key === "ArrowDown") {
|
|
1461
|
+
e.preventDefault();
|
|
1462
|
+
if (!this._open) {
|
|
1463
|
+
// Si está cerrado, ArrowDown lo abre (comportamiento estándar nativo)
|
|
1464
|
+
this._openDropdown();
|
|
1465
|
+
} else if (visible.length > 0) {
|
|
1466
|
+
// Navegamos circularmente hacia abajo
|
|
1467
|
+
this._activeIndex = (this._activeIndex + 1) % visible.length;
|
|
1468
|
+
this._scrollToActive();
|
|
1469
|
+
}
|
|
1470
|
+
} else if (e.key === "ArrowUp") {
|
|
1471
|
+
e.preventDefault();
|
|
1472
|
+
if (this._open && visible.length > 0) {
|
|
1473
|
+
// Navegamos circularmente hacia arriba (+visible.length evita valores negativos en JS)
|
|
1474
|
+
this._activeIndex =
|
|
1475
|
+
(this._activeIndex - 1 + visible.length) % visible.length;
|
|
1476
|
+
this._scrollToActive();
|
|
1477
|
+
}
|
|
1478
|
+
} else if (e.key === "Enter") {
|
|
1479
|
+
e.preventDefault();
|
|
1480
|
+
if (this._open) {
|
|
1481
|
+
// Si está abierto y hay una opción resaltada, simulamos el click para seleccionarla
|
|
1482
|
+
if (this._activeIndex >= 0 && this._activeIndex < visible.length) {
|
|
1483
|
+
this._selectOption(visible[this._activeIndex]);
|
|
1484
|
+
}
|
|
1485
|
+
} else {
|
|
1486
|
+
// Enter cuando está cerrado simplemente abre el menú
|
|
1487
|
+
this._openDropdown();
|
|
1488
|
+
}
|
|
1489
|
+
} else if (e.key === "Escape") {
|
|
1490
|
+
e.preventDefault();
|
|
1491
|
+
// Escapa el menú sin hacer ninguna selección
|
|
1492
|
+
this._closeDropdown();
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* Asegura que el elemento resaltado por teclado sea visible dentro del contenedor scrolleable.
|
|
1498
|
+
*/
|
|
1499
|
+
private _scrollToActive() {
|
|
1500
|
+
// Se requiere setTimeout para darle tiempo a Lit a renderizar la clase 'active-nav'
|
|
1501
|
+
setTimeout(() => {
|
|
1502
|
+
const activeEl = this.shadowRoot?.querySelector(
|
|
1503
|
+
".option-item.active-nav",
|
|
1504
|
+
);
|
|
1505
|
+
if (activeEl) {
|
|
1506
|
+
activeEl.scrollIntoView({ block: "nearest" });
|
|
1507
|
+
}
|
|
1508
|
+
}, 0);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* Aplana el árbol jerárquico recursivo de <moni-select-group> a un array unidimensional de opciones puras.
|
|
1513
|
+
* Esencial para que la búsqueda por texto funcione globalmente ignorando las categorías.
|
|
1514
|
+
*/
|
|
1515
|
+
private _getFlatOptions(nodes: DropdownNode[]): OptionNode[] {
|
|
1516
|
+
const flat: OptionNode[] = [];
|
|
1517
|
+
const traverse = (n: DropdownNode) => {
|
|
1518
|
+
if (n.type === "option") {
|
|
1519
|
+
flat.push(n);
|
|
1520
|
+
} else {
|
|
1521
|
+
n.children.forEach(traverse);
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
nodes.forEach(traverse);
|
|
1525
|
+
return flat;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
private _getFilteredOptions(): OptionNode[] {
|
|
1529
|
+
const flat = this._getFlatOptions(this._parsedOptions);
|
|
1530
|
+
if (!this._searchQuery) return flat;
|
|
1531
|
+
const query = this._searchQuery.toLowerCase();
|
|
1532
|
+
return flat.filter(
|
|
1533
|
+
(opt) =>
|
|
1534
|
+
opt.label.toLowerCase().includes(query) ||
|
|
1535
|
+
opt.value.toLowerCase().includes(query),
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
private _getVisibleOptions(): OptionNode[] {
|
|
1540
|
+
const filtered = this._getFilteredOptions();
|
|
1541
|
+
if (this._searchQuery) return filtered;
|
|
1542
|
+
|
|
1543
|
+
if (this.sheet || this._useInlineCategories) {
|
|
1544
|
+
if (this._drilldownPath.length > 0) {
|
|
1545
|
+
const activeGroup = this._drilldownPath[this._drilldownPath.length - 1];
|
|
1546
|
+
return activeGroup.children.filter(
|
|
1547
|
+
(n) => n.type === "option",
|
|
1548
|
+
) as OptionNode[];
|
|
1549
|
+
}
|
|
1550
|
+
return this._parsedOptions.filter(
|
|
1551
|
+
(n) => n.type === "option",
|
|
1552
|
+
) as OptionNode[];
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
return filtered;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* Motor de Renderizado: Lista Raíz (Modo Móvil / Drilldown).
|
|
1560
|
+
*
|
|
1561
|
+
* Renderiza las opciones iniciales y los grupos base de la categoría actual (`_drilldownPath`).
|
|
1562
|
+
* Asigna clases dinámicas (`selected`, `active-nav`) para la retroalimentación visual
|
|
1563
|
+
* del teclado y el valor actualmente seleccionado.
|
|
1564
|
+
*/
|
|
1565
|
+
private _renderRootList() {
|
|
1566
|
+
let visibleIndex = 0;
|
|
1567
|
+
return this._parsedOptions.map((node) => {
|
|
1568
|
+
if (node.type === "option") {
|
|
1569
|
+
const currentIdx = visibleIndex++;
|
|
1570
|
+
const isSelected = node.value === this.value;
|
|
1571
|
+
const isActiveNav =
|
|
1572
|
+
this._drilldownPath.length === 0 && currentIdx === this._activeIndex;
|
|
1573
|
+
return html`
|
|
1574
|
+
<li
|
|
1575
|
+
class="option-item ${isSelected ? "selected" : ""} ${isActiveNav ? "active-nav" : ""} ${node.disabled ? "disabled" : ""}"
|
|
1576
|
+
@click=${() => this._selectOption(node)}
|
|
1577
|
+
>
|
|
1578
|
+
${node.label}
|
|
1579
|
+
</li>
|
|
1580
|
+
`;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
return html`
|
|
1584
|
+
<li
|
|
1585
|
+
class="group-header"
|
|
1586
|
+
@click=${(e: Event) => {
|
|
1587
|
+
e.stopPropagation();
|
|
1588
|
+
this._drilldownPath = [...this._drilldownPath, node];
|
|
1589
|
+
}}
|
|
1590
|
+
>
|
|
1591
|
+
<span>${node.label}</span>
|
|
1592
|
+
<moni-icon
|
|
1593
|
+
name="chevron_right"
|
|
1594
|
+
style="margin-inline-start: auto; font-size: 1.125rem;"
|
|
1595
|
+
></moni-icon>
|
|
1596
|
+
</li>
|
|
1597
|
+
`;
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/**
|
|
1602
|
+
* Motor de Renderizado: Subcategorías (Modo Móvil / Drilldown).
|
|
1603
|
+
*
|
|
1604
|
+
* A diferencia de desktop (donde los submenús flotan a los lados), en dispositivos
|
|
1605
|
+
* móviles o espacios constreñidos, el menú transiciona in-situ. Este método renderiza
|
|
1606
|
+
* el header del grupo activo, un botón de "Regresar" (Arrow Back) y las opciones hijas.
|
|
1607
|
+
*/
|
|
1608
|
+
private _renderSubcategoryList() {
|
|
1609
|
+
if (this._drilldownPath.length === 0) return nothing;
|
|
1610
|
+
const activeGroup = this._drilldownPath[this._drilldownPath.length - 1];
|
|
1611
|
+
let visibleIndex = 0;
|
|
1612
|
+
|
|
1613
|
+
return html`
|
|
1614
|
+
<li
|
|
1615
|
+
class="group-header"
|
|
1616
|
+
@click=${(e: Event) => {
|
|
1617
|
+
e.stopPropagation();
|
|
1618
|
+
this._drilldownPath = this._drilldownPath.slice(0, -1);
|
|
1619
|
+
}}
|
|
1620
|
+
>
|
|
1621
|
+
<moni-icon
|
|
1622
|
+
name="arrow_back"
|
|
1623
|
+
style="font-size: 1.25rem; margin-inline-end: 8px;"
|
|
1624
|
+
></moni-icon>
|
|
1625
|
+
<span>Regresar</span>
|
|
1626
|
+
</li>
|
|
1627
|
+
<li class="group-header-flat">${activeGroup.label}</li>
|
|
1628
|
+
${activeGroup.children.map((node) => {
|
|
1629
|
+
if (node.type === "option") {
|
|
1630
|
+
const currentIdx = visibleIndex++;
|
|
1631
|
+
const isSelected = node.value === this.value;
|
|
1632
|
+
const isActiveNav =
|
|
1633
|
+
this._drilldownPath.length > 0 && currentIdx === this._activeIndex;
|
|
1634
|
+
return html`
|
|
1635
|
+
<li
|
|
1636
|
+
class="option-item ${isSelected ? "selected" : ""} ${isActiveNav ? "active-nav" : ""} ${node.disabled ? "disabled" : ""}"
|
|
1637
|
+
@click=${() => this._selectOption(node)}
|
|
1638
|
+
>
|
|
1639
|
+
${node.label}
|
|
1640
|
+
</li>
|
|
1641
|
+
`;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
return html`
|
|
1645
|
+
<li
|
|
1646
|
+
class="group-header"
|
|
1647
|
+
@click=${(e: Event) => {
|
|
1648
|
+
e.stopPropagation();
|
|
1649
|
+
this._drilldownPath = [...this._drilldownPath, node];
|
|
1650
|
+
}}
|
|
1651
|
+
>
|
|
1652
|
+
<span>${node.label}</span>
|
|
1653
|
+
<moni-icon
|
|
1654
|
+
name="chevron_right"
|
|
1655
|
+
style="margin-inline-start: auto; font-size: 1.125rem;"
|
|
1656
|
+
></moni-icon>
|
|
1657
|
+
</li>
|
|
1658
|
+
`;
|
|
1659
|
+
})}
|
|
1660
|
+
`;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
private _renderDesktopNode(
|
|
1664
|
+
node: DropdownNode,
|
|
1665
|
+
flatIndexRef: { value: number },
|
|
1666
|
+
): any {
|
|
1667
|
+
if (node.type === "option") {
|
|
1668
|
+
const currentIdx = flatIndexRef.value++;
|
|
1669
|
+
const isSelected = node.value === this.value;
|
|
1670
|
+
const isActiveNav = currentIdx === this._activeIndex;
|
|
1671
|
+
return html`
|
|
1672
|
+
<li
|
|
1673
|
+
class="option-item ${isSelected ? "selected" : ""} ${isActiveNav ? "active-nav" : ""} ${node.disabled ? "disabled" : ""}"
|
|
1674
|
+
@click=${() => this._selectOption(node)}
|
|
1675
|
+
>
|
|
1676
|
+
${node.label}
|
|
1677
|
+
</li>
|
|
1678
|
+
`;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
return html`
|
|
1682
|
+
<li
|
|
1683
|
+
class="group-header"
|
|
1684
|
+
@mouseenter=${this._onSubmenuMouseEnter}
|
|
1685
|
+
@click=${(e: Event) => e.stopPropagation()}
|
|
1686
|
+
>
|
|
1687
|
+
<span>${node.label}</span>
|
|
1688
|
+
<moni-icon
|
|
1689
|
+
name="chevron_right"
|
|
1690
|
+
style="margin-inline-start: auto; font-size: 1.125rem;"
|
|
1691
|
+
></moni-icon>
|
|
1692
|
+
|
|
1693
|
+
<ul class="submenu active open-right">
|
|
1694
|
+
${node.children.map((child) => this._renderDesktopNode(child, flatIndexRef))}
|
|
1695
|
+
</ul>
|
|
1696
|
+
</li>
|
|
1697
|
+
`;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* Orquestador principal de renderizado de la lista de opciones.
|
|
1702
|
+
*
|
|
1703
|
+
* Si el usuario introdujo texto (`_searchQuery`), renderiza los resultados filtrados.
|
|
1704
|
+
* Si la lista original está vacía, muestra un mensaje amigable (Empty State).
|
|
1705
|
+
* Determina condicionalmente si usar el modo plano/Drilldown (Móviles) o
|
|
1706
|
+
* el modo Desktop con submenús flotantes (`_renderDesktopNode`).
|
|
1707
|
+
*/
|
|
1708
|
+
private _renderOptionsList(filtered: OptionNode[]) {
|
|
1709
|
+
if (this._parsedOptions.length === 0) {
|
|
1710
|
+
return html`<li class="no-options">No options found</li>`;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
if (this._searchQuery) {
|
|
1714
|
+
const groups: { [key: string]: OptionNode[] } = {};
|
|
1715
|
+
filtered.forEach((opt) => {
|
|
1716
|
+
const gName = opt.group || "";
|
|
1717
|
+
if (!groups[gName]) groups[gName] = [];
|
|
1718
|
+
groups[gName].push(opt);
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
let flatIndex = 0;
|
|
1722
|
+
return Object.entries(groups).map(
|
|
1723
|
+
([groupName, groupOpts]) => html`
|
|
1724
|
+
${groupName ? html`<li class="group-header-flat">${groupName}</li>` : nothing}
|
|
1725
|
+
${groupOpts.map((opt) => {
|
|
1726
|
+
const currentIdx = flatIndex++;
|
|
1727
|
+
const isSelected = opt.value === this.value;
|
|
1728
|
+
const isActiveNav = currentIdx === this._activeIndex;
|
|
1729
|
+
return html`
|
|
1730
|
+
<li
|
|
1731
|
+
class="option-item ${isSelected ? "selected" : ""} ${isActiveNav ? "active-nav" : ""} ${opt.disabled ? "disabled" : ""}"
|
|
1732
|
+
@click=${() => this._selectOption(opt)}
|
|
1733
|
+
>
|
|
1734
|
+
${opt.label}
|
|
1735
|
+
</li>
|
|
1736
|
+
`;
|
|
1737
|
+
})}
|
|
1738
|
+
`,
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
if (this.sheet || this._useInlineCategories) {
|
|
1743
|
+
const isRootActive = this._drilldownPath.length === 0;
|
|
1744
|
+
const isSubActive = !isRootActive;
|
|
1745
|
+
|
|
1746
|
+
return html`
|
|
1747
|
+
<div class="drilldown-wrapper">
|
|
1748
|
+
<div
|
|
1749
|
+
class="drilldown-track ${this._drilldownPath.length > 0 ? "slide-active" : ""}"
|
|
1750
|
+
>
|
|
1751
|
+
<ul
|
|
1752
|
+
class="drilldown-panel ${isRootActive ? "panel-active" : "panel-inactive"}"
|
|
1753
|
+
>
|
|
1754
|
+
${this._renderRootList()}
|
|
1755
|
+
</ul>
|
|
1756
|
+
<ul
|
|
1757
|
+
class="drilldown-panel ${isSubActive ? "panel-active" : "panel-inactive"}"
|
|
1758
|
+
>
|
|
1759
|
+
${this._renderSubcategoryList()}
|
|
1760
|
+
</ul>
|
|
1761
|
+
</div>
|
|
1762
|
+
</div>
|
|
1763
|
+
`;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
const flatIndexRef = { value: 0 };
|
|
1767
|
+
return this._parsedOptions.map((node) =>
|
|
1768
|
+
this._renderDesktopNode(node, flatIndexRef),
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* Ensambla el Shadow DOM del campo select, coordinando dos modos de interfaz de usuario distintos.
|
|
1774
|
+
*
|
|
1775
|
+
* **Resolución del valor a mostrar (Display value):**
|
|
1776
|
+
* El contenido de texto visible en el input está determinado por prioridad:
|
|
1777
|
+
* 1. Mientras el menú desplegable está abierto Y `searchable=true` Y NO es modo sheet:
|
|
1778
|
+
* muestra `_searchQuery` (el texto del filtro en vivo que el usuario está escribiendo).
|
|
1779
|
+
* 2. De lo contrario: muestra `selectedOpt.label` (la etiqueta de la opción actualmente
|
|
1780
|
+
* seleccionada) o vuelve a la cadena vacía.
|
|
1781
|
+
*
|
|
1782
|
+
* **Dos rutas de renderizado:**
|
|
1783
|
+
* - **Modo Sheet (`sheet=true`):** Renderiza un `<moni-bottom-sheet>` como un
|
|
1784
|
+
* cajón (drawer) superpuesto a pantalla completa, ideal para IU táctiles móviles. La lista de opciones
|
|
1785
|
+
* se renderiza dentro de la hoja en lugar del menú desplegable.
|
|
1786
|
+
* - **Modo desplegable (por defecto):** Renderiza un `.dropdown-menu` flotante que
|
|
1787
|
+
* se abre debajo o arriba del campo. La cadena `_menuStyle` (calculada por
|
|
1788
|
+
* `_computeMenuStyle()`) inyecta los `top`, `left`, `width`, y
|
|
1789
|
+
* sobreescrituras `position: fixed` correctas para contenedores con scroll (scroll-contained) y parents con overflow cortado (overflow-clipping).
|
|
1790
|
+
*
|
|
1791
|
+
* **Patrón combobox ARIA:**
|
|
1792
|
+
* El input de texto lleva `role="combobox"`, `aria-haspopup="listbox"` y
|
|
1793
|
+
* `aria-expanded` para satisfacer los requisitos del combobox WCAG 2.1 para lectores de pantalla.
|
|
1794
|
+
* `aria-activedescendant` se establece al ID de la opción actualmente enfocada por teclado
|
|
1795
|
+
* para habilitar el movimiento del cursor virtual sin gestión del foco.
|
|
1796
|
+
*
|
|
1797
|
+
* **Composición de `fieldClasses`:**
|
|
1798
|
+
* Sigue la convención de nomenclatura de clases de estilos de campo de BeerCSS. `prefix` se agrega
|
|
1799
|
+
* cuando `icon` está establecido (para desplazar el relleno -padding- inline-start del input de texto para el icono).
|
|
1800
|
+
*/
|
|
1801
|
+
override render() {
|
|
1802
|
+
const hasLeading = Boolean(this.icon);
|
|
1803
|
+
const filtered = this._getFilteredOptions();
|
|
1804
|
+
|
|
1805
|
+
// Find selected option label
|
|
1806
|
+
const selectedOpt = this._findOptionByValue(this.value);
|
|
1807
|
+
const displayValue =
|
|
1808
|
+
this._open && this.searchable && !this.sheet
|
|
1809
|
+
? this._searchQuery
|
|
1810
|
+
: selectedOpt
|
|
1811
|
+
? selectedOpt.label
|
|
1812
|
+
: "";
|
|
1813
|
+
|
|
1814
|
+
const isActive = Boolean(displayValue || this.placeholder);
|
|
1815
|
+
|
|
1816
|
+
const fieldClasses = {
|
|
1817
|
+
field: true,
|
|
1818
|
+
label: Boolean(this.label),
|
|
1819
|
+
fill: this.variant === "filled",
|
|
1820
|
+
border: this.variant === "outlined",
|
|
1821
|
+
small: this.size === "small",
|
|
1822
|
+
large: this.size === "large",
|
|
1823
|
+
extra: this.size === "extra",
|
|
1824
|
+
prefix: hasLeading,
|
|
1825
|
+
suffix: true,
|
|
1826
|
+
invalid: this.error,
|
|
1827
|
+
round: this.shape === "round",
|
|
1828
|
+
"small-round": this.shape === "small-round",
|
|
1829
|
+
square: this.shape === "no-round",
|
|
1830
|
+
};
|
|
1831
|
+
|
|
1832
|
+
const leading = this.icon
|
|
1833
|
+
? html`<i class="leading-icon" part="leading-icon"
|
|
1834
|
+
><moni-icon name="${this.icon}"></moni-icon
|
|
1835
|
+
></i>`
|
|
1836
|
+
: nothing;
|
|
1837
|
+
|
|
1838
|
+
const trailing = this.loading
|
|
1839
|
+
? html`<i class="trailing-icon" part="trailing-icon"
|
|
1840
|
+
><moni-progress
|
|
1841
|
+
variant="circular"
|
|
1842
|
+
indeterminate
|
|
1843
|
+
size="small"
|
|
1844
|
+
style="inline-size: 1.25rem; block-size: 1.25rem; color: currentColor;"
|
|
1845
|
+
></moni-progress
|
|
1846
|
+
></i>`
|
|
1847
|
+
: html`<i
|
|
1848
|
+
class="trailing-icon"
|
|
1849
|
+
part="trailing-icon"
|
|
1850
|
+
@click=${this._toggleDropdown}
|
|
1851
|
+
><moni-icon name="${this.trailingIcon}"></moni-icon
|
|
1852
|
+
></i>`;
|
|
1853
|
+
|
|
1854
|
+
const hasGroups = this._parsedOptions.some((node) => node.type === "group");
|
|
1855
|
+
const usesFlyoutSubmenus =
|
|
1856
|
+
hasGroups && !this._searchQuery && !this._useInlineCategories;
|
|
1857
|
+
const menuClasses = {
|
|
1858
|
+
"dropdown-menu": true,
|
|
1859
|
+
open: this._open,
|
|
1860
|
+
scrollable: !usesFlyoutSubmenus,
|
|
1861
|
+
searching: Boolean(this._searchQuery),
|
|
1862
|
+
"inline-categories": this._useInlineCategories,
|
|
1863
|
+
"placement-top": this._actualPlacement === "top",
|
|
1864
|
+
"placement-bottom": this._actualPlacement === "bottom",
|
|
1865
|
+
"placement-left": this._actualPlacement === "left",
|
|
1866
|
+
"placement-right": this._actualPlacement === "right",
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
return html`<div class=${classMap(fieldClasses)} part="field">
|
|
1870
|
+
${leading}
|
|
1871
|
+
<input
|
|
1872
|
+
id="input"
|
|
1873
|
+
type="text"
|
|
1874
|
+
part="input"
|
|
1875
|
+
.value=${displayValue}
|
|
1876
|
+
?readonly=${!this.searchable || this.sheet}
|
|
1877
|
+
?disabled=${this.disabled}
|
|
1878
|
+
inputmode=${ifDefined(!this.searchable || this.sheet ? "none" : undefined)}
|
|
1879
|
+
placeholder=${this.placeholder || ""}
|
|
1880
|
+
class=${isActive ? "active" : ""}
|
|
1881
|
+
@click=${this._onInputClick}
|
|
1882
|
+
@input=${this._onSearchInput}
|
|
1883
|
+
@keydown=${this._onKeyDown}
|
|
1884
|
+
/>
|
|
1885
|
+
${
|
|
1886
|
+
this.label
|
|
1887
|
+
? html`<label
|
|
1888
|
+
for="input"
|
|
1889
|
+
part="label"
|
|
1890
|
+
class=${classMap({ active: isActive })}
|
|
1891
|
+
>${this.label}</label
|
|
1892
|
+
>`
|
|
1893
|
+
: nothing
|
|
1894
|
+
}
|
|
1895
|
+
${trailing}
|
|
1896
|
+
${
|
|
1897
|
+
this.sheet
|
|
1898
|
+
? html`
|
|
1899
|
+
<div
|
|
1900
|
+
class="sheet-backdrop ${this._open ? "open" : ""}"
|
|
1901
|
+
@click=${this._closeDropdown}
|
|
1902
|
+
>
|
|
1903
|
+
<div
|
|
1904
|
+
class="sheet-drawer ${this._open ? "open" : ""}"
|
|
1905
|
+
@click=${(e: Event) => e.stopPropagation()}
|
|
1906
|
+
>
|
|
1907
|
+
<div class="sheet-handle"></div>
|
|
1908
|
+
${this.label ? html`<div class="sheet-title">${this.label}</div>` : nothing}
|
|
1909
|
+
${
|
|
1910
|
+
this.searchable
|
|
1911
|
+
? html`
|
|
1912
|
+
<div class="sheet-search-wrapper">
|
|
1913
|
+
<input
|
|
1914
|
+
type="text"
|
|
1915
|
+
class="sheet-search-input"
|
|
1916
|
+
.value=${this._searchQuery}
|
|
1917
|
+
placeholder="Buscar..."
|
|
1918
|
+
@input=${this._onSearchInput}
|
|
1919
|
+
/>
|
|
1920
|
+
</div>
|
|
1921
|
+
`
|
|
1922
|
+
: nothing
|
|
1923
|
+
}
|
|
1924
|
+
<ul class="sheet-options-list">
|
|
1925
|
+
${this._renderOptionsList(filtered)}
|
|
1926
|
+
</ul>
|
|
1927
|
+
</div>
|
|
1928
|
+
</div>
|
|
1929
|
+
`
|
|
1930
|
+
: html`
|
|
1931
|
+
<ul
|
|
1932
|
+
class=${classMap(menuClasses)}
|
|
1933
|
+
part="menu"
|
|
1934
|
+
popover=${this.positioning === "body" ? "manual" : nothing}
|
|
1935
|
+
style=${this._getMenuStyle()}
|
|
1936
|
+
>
|
|
1937
|
+
${this._renderOptionsList(filtered)}
|
|
1938
|
+
</ul>
|
|
1939
|
+
`
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
<slot style="display: none;" @slotchange=${this._onSlotChange}></slot>
|
|
1943
|
+
|
|
1944
|
+
${
|
|
1945
|
+
this.error
|
|
1946
|
+
? html`<output part="helper" class="invalid"
|
|
1947
|
+
>${this.errorText || this.helper}</output
|
|
1948
|
+
>`
|
|
1949
|
+
: this.helper
|
|
1950
|
+
? html`<output part="helper">${this.helper}</output>`
|
|
1951
|
+
: nothing
|
|
1952
|
+
}
|
|
1953
|
+
</div>`;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
declare global {
|
|
1958
|
+
interface HTMLElementTagNameMap {
|
|
1959
|
+
"moni-select": MoniSelect;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
export default MoniSelect;
|