@r2digisolutions/ui 0.24.1 → 0.24.2
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.
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { TContextMenuEntry } from '../core/types.js';
|
|
3
|
-
import { portal } from '../utils/portal.js';
|
|
4
|
-
import { clampToViewport } from '../utils/position.js';
|
|
5
3
|
|
|
4
|
+
// --- Action portal (interna al componente) ---
|
|
5
|
+
function portal(node: HTMLElement, target: HTMLElement | null = null) {
|
|
6
|
+
const tgt = target ?? document.body;
|
|
7
|
+
const placeholder = document.createComment('portal-placeholder');
|
|
8
|
+
node.parentNode?.insertBefore(placeholder, node);
|
|
9
|
+
tgt.appendChild(node);
|
|
10
|
+
return {
|
|
11
|
+
destroy() {
|
|
12
|
+
node.remove();
|
|
13
|
+
placeholder.parentNode?.insertBefore(node, placeholder);
|
|
14
|
+
placeholder.remove();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// --- Util para clamping a viewport (interna) ---
|
|
20
|
+
function clampToViewport(x: number, y: number, menuW: number, menuH: number, padding = 8) {
|
|
21
|
+
const vw = document.documentElement.clientWidth;
|
|
22
|
+
const vh = document.documentElement.clientHeight;
|
|
23
|
+
let nx = x;
|
|
24
|
+
let ny = y;
|
|
25
|
+
if (nx + menuW + padding > vw) nx = Math.max(padding, vw - menuW - padding);
|
|
26
|
+
if (ny + menuH + padding > vh) ny = Math.max(padding, vh - menuH - padding);
|
|
27
|
+
return { x: nx, y: ny };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- Props ---
|
|
6
31
|
type Props = {
|
|
7
32
|
items?: TContextMenuEntry[];
|
|
8
33
|
x?: number;
|
|
@@ -11,7 +36,6 @@
|
|
|
11
36
|
title?: string;
|
|
12
37
|
searchable?: boolean;
|
|
13
38
|
context?: any;
|
|
14
|
-
// por si algún día quieres desactivar el portal:
|
|
15
39
|
portalTarget?: HTMLElement | null;
|
|
16
40
|
};
|
|
17
41
|
let {
|
|
@@ -25,15 +49,17 @@
|
|
|
25
49
|
portalTarget = null
|
|
26
50
|
}: Props = $props();
|
|
27
51
|
|
|
52
|
+
// --- Estado interno ---
|
|
28
53
|
let stack = $state<{ label: string; items: TContextMenuEntry[] }[]>([]);
|
|
29
54
|
let q = $state('');
|
|
30
|
-
|
|
31
55
|
const current = $derived(stack.length ? stack[stack.length - 1] : { label: title, items });
|
|
32
56
|
|
|
33
57
|
function close() {
|
|
34
58
|
open = false;
|
|
35
59
|
stack = [];
|
|
36
60
|
q = '';
|
|
61
|
+
// opcional si quieres limpiar coords:
|
|
62
|
+
// x = 0; y = 0;
|
|
37
63
|
}
|
|
38
64
|
|
|
39
65
|
function hasChildren(it: TContextMenuEntry) {
|
|
@@ -66,6 +92,7 @@
|
|
|
66
92
|
const query = q.trim().toLowerCase();
|
|
67
93
|
let arr = query ? list.filter((it) => matches(it, query)) : list.slice();
|
|
68
94
|
|
|
95
|
+
// limpiar divisores consecutivos y extremos
|
|
69
96
|
const out: TContextMenuEntry[] = [];
|
|
70
97
|
let prevDiv = false;
|
|
71
98
|
for (const it of arr) {
|
|
@@ -98,6 +125,10 @@
|
|
|
98
125
|
}
|
|
99
126
|
}
|
|
100
127
|
|
|
128
|
+
// --- Ref al elemento del menú para detectar click-fuera ---
|
|
129
|
+
let menuEl: HTMLDivElement | null = $state(null);
|
|
130
|
+
|
|
131
|
+
// Teclado (Escape / Backspace)
|
|
101
132
|
$effect(() => {
|
|
102
133
|
if (!open) return;
|
|
103
134
|
const handler = (e: KeyboardEvent) => onKey(e);
|
|
@@ -105,17 +136,43 @@
|
|
|
105
136
|
return () => document.removeEventListener('keydown', handler);
|
|
106
137
|
});
|
|
107
138
|
|
|
108
|
-
//
|
|
109
|
-
|
|
139
|
+
// Cierres robustos: click-fuera en capture, scroll/resize, y bloqueo de menú nativo
|
|
140
|
+
$effect(() => {
|
|
141
|
+
if (!open) return;
|
|
142
|
+
|
|
143
|
+
const onPointerDownCapture = (ev: PointerEvent) => {
|
|
144
|
+
if (!menuEl) return;
|
|
145
|
+
if (!menuEl.contains(ev.target as Node)) {
|
|
146
|
+
close();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const onNativeCtx = (ev: MouseEvent) => {
|
|
151
|
+
ev.preventDefault();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const onScrollOrResize = () => close();
|
|
155
|
+
|
|
156
|
+
document.addEventListener('pointerdown', onPointerDownCapture, true);
|
|
157
|
+
document.addEventListener('contextmenu', onNativeCtx);
|
|
158
|
+
window.addEventListener('scroll', onScrollOrResize, true);
|
|
159
|
+
window.addEventListener('resize', onScrollOrResize, true);
|
|
160
|
+
|
|
161
|
+
return () => {
|
|
162
|
+
document.removeEventListener('pointerdown', onPointerDownCapture, true);
|
|
163
|
+
document.removeEventListener('contextmenu', onNativeCtx);
|
|
164
|
+
window.removeEventListener('scroll', onScrollOrResize, true);
|
|
165
|
+
window.removeEventListener('resize', onScrollOrResize, true);
|
|
166
|
+
};
|
|
167
|
+
});
|
|
110
168
|
|
|
169
|
+
// Clamp de posición tras abrir o cambiar coords
|
|
111
170
|
$effect(() => {
|
|
112
171
|
if (!open || !menuEl) return;
|
|
113
|
-
// siguiente frame para medir dimensiones reales
|
|
114
172
|
requestAnimationFrame(() => {
|
|
115
173
|
if (!menuEl) return;
|
|
116
174
|
const rect = menuEl.getBoundingClientRect();
|
|
117
175
|
const { x: nx, y: ny } = clampToViewport(x, y, rect.width, rect.height, 8);
|
|
118
|
-
// solo si cambian, re-ubica
|
|
119
176
|
if (nx !== x || ny !== y) {
|
|
120
177
|
x = nx;
|
|
121
178
|
y = ny;
|
|
@@ -125,7 +182,7 @@
|
|
|
125
182
|
</script>
|
|
126
183
|
|
|
127
184
|
{#if open}
|
|
128
|
-
<!-- BACKDROP
|
|
185
|
+
<!-- BACKDROP (portaleado al body) -->
|
|
129
186
|
<div
|
|
130
187
|
use:portal={portalTarget}
|
|
131
188
|
role="dialog"
|
|
@@ -137,7 +194,7 @@
|
|
|
137
194
|
style="pointer-events:auto"
|
|
138
195
|
/>
|
|
139
196
|
|
|
140
|
-
<!--
|
|
197
|
+
<!-- MENÚ (portaleado al body) -->
|
|
141
198
|
<div
|
|
142
199
|
bind:this={menuEl}
|
|
143
200
|
use:portal={portalTarget}
|
|
@@ -161,10 +218,8 @@
|
|
|
161
218
|
stroke="currentColor"
|
|
162
219
|
stroke-width="2"
|
|
163
220
|
stroke-linecap="round"
|
|
164
|
-
stroke-linejoin="round"
|
|
221
|
+
stroke-linejoin="round"><polyline points="15 18 9 12 15 6" /></svg
|
|
165
222
|
>
|
|
166
|
-
<polyline points="15 18 9 12 15 6" />
|
|
167
|
-
</svg>
|
|
168
223
|
</button>
|
|
169
224
|
{/if}
|
|
170
225
|
<div class="min-w-0 flex-1 truncate px-1 text-xs font-medium opacity-70">
|
|
@@ -211,10 +266,8 @@
|
|
|
211
266
|
stroke="currentColor"
|
|
212
267
|
stroke-width="2"
|
|
213
268
|
stroke-linecap="round"
|
|
214
|
-
stroke-linejoin="round"
|
|
269
|
+
stroke-linejoin="round"><polyline points="9 18 15 12 9 6" /></svg
|
|
215
270
|
>
|
|
216
|
-
<polyline points="9 18 15 12 9 6" />
|
|
217
|
-
</svg>
|
|
218
271
|
{/if}
|
|
219
272
|
</span>
|
|
220
273
|
</button>
|