@r2digisolutions/ui 0.24.0 → 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.
- package/dist/components/container/DataTable/components/ContextMenu.svelte +90 -5
- package/dist/components/container/DataTable/components/ContextMenu.svelte.d.ts +1 -0
- package/dist/components/container/DataTable/utils/portal.d.ts +3 -0
- package/dist/components/container/DataTable/utils/portal.js +13 -0
- package/dist/components/container/DataTable/utils/position.d.ts +4 -0
- package/dist/components/container/DataTable/utils/position.js +11 -0
- package/package.json +14 -14
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { TContextMenuEntry } from '../core/types.js';
|
|
3
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 ---
|
|
4
31
|
type Props = {
|
|
5
32
|
items?: TContextMenuEntry[];
|
|
6
33
|
x?: number;
|
|
@@ -9,6 +36,7 @@
|
|
|
9
36
|
title?: string;
|
|
10
37
|
searchable?: boolean;
|
|
11
38
|
context?: any;
|
|
39
|
+
portalTarget?: HTMLElement | null;
|
|
12
40
|
};
|
|
13
41
|
let {
|
|
14
42
|
items = [],
|
|
@@ -17,18 +45,21 @@
|
|
|
17
45
|
open = $bindable(false),
|
|
18
46
|
title = '',
|
|
19
47
|
searchable = true,
|
|
20
|
-
context = null
|
|
48
|
+
context = null,
|
|
49
|
+
portalTarget = null
|
|
21
50
|
}: Props = $props();
|
|
22
51
|
|
|
52
|
+
// --- Estado interno ---
|
|
23
53
|
let stack = $state<{ label: string; items: TContextMenuEntry[] }[]>([]);
|
|
24
54
|
let q = $state('');
|
|
25
|
-
|
|
26
55
|
const current = $derived(stack.length ? stack[stack.length - 1] : { label: title, items });
|
|
27
56
|
|
|
28
57
|
function close() {
|
|
29
58
|
open = false;
|
|
30
59
|
stack = [];
|
|
31
60
|
q = '';
|
|
61
|
+
// opcional si quieres limpiar coords:
|
|
62
|
+
// x = 0; y = 0;
|
|
32
63
|
}
|
|
33
64
|
|
|
34
65
|
function hasChildren(it: TContextMenuEntry) {
|
|
@@ -61,7 +92,7 @@
|
|
|
61
92
|
const query = q.trim().toLowerCase();
|
|
62
93
|
let arr = query ? list.filter((it) => matches(it, query)) : list.slice();
|
|
63
94
|
|
|
64
|
-
// limpiar divisores
|
|
95
|
+
// limpiar divisores consecutivos y extremos
|
|
65
96
|
const out: TContextMenuEntry[] = [];
|
|
66
97
|
let prevDiv = false;
|
|
67
98
|
for (const it of arr) {
|
|
@@ -94,26 +125,80 @@
|
|
|
94
125
|
}
|
|
95
126
|
}
|
|
96
127
|
|
|
128
|
+
// --- Ref al elemento del menú para detectar click-fuera ---
|
|
129
|
+
let menuEl: HTMLDivElement | null = $state(null);
|
|
130
|
+
|
|
131
|
+
// Teclado (Escape / Backspace)
|
|
97
132
|
$effect(() => {
|
|
98
133
|
if (!open) return;
|
|
99
134
|
const handler = (e: KeyboardEvent) => onKey(e);
|
|
100
135
|
document.addEventListener('keydown', handler);
|
|
101
136
|
return () => document.removeEventListener('keydown', handler);
|
|
102
137
|
});
|
|
138
|
+
|
|
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
|
+
});
|
|
168
|
+
|
|
169
|
+
// Clamp de posición tras abrir o cambiar coords
|
|
170
|
+
$effect(() => {
|
|
171
|
+
if (!open || !menuEl) return;
|
|
172
|
+
requestAnimationFrame(() => {
|
|
173
|
+
if (!menuEl) return;
|
|
174
|
+
const rect = menuEl.getBoundingClientRect();
|
|
175
|
+
const { x: nx, y: ny } = clampToViewport(x, y, rect.width, rect.height, 8);
|
|
176
|
+
if (nx !== x || ny !== y) {
|
|
177
|
+
x = nx;
|
|
178
|
+
y = ny;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
103
182
|
</script>
|
|
104
183
|
|
|
105
184
|
{#if open}
|
|
185
|
+
<!-- BACKDROP (portaleado al body) -->
|
|
106
186
|
<div
|
|
187
|
+
use:portal={portalTarget}
|
|
107
188
|
role="dialog"
|
|
108
|
-
class="fixed inset-0 z-
|
|
189
|
+
class="fixed inset-0 z-[2147483646]"
|
|
109
190
|
onclick={() => close()}
|
|
110
191
|
oncontextmenu={(e) => e.preventDefault()}
|
|
111
192
|
aria-modal="true"
|
|
112
193
|
tabindex="0"
|
|
194
|
+
style="pointer-events:auto"
|
|
113
195
|
/>
|
|
114
196
|
|
|
197
|
+
<!-- MENÚ (portaleado al body) -->
|
|
115
198
|
<div
|
|
116
|
-
|
|
199
|
+
bind:this={menuEl}
|
|
200
|
+
use:portal={portalTarget}
|
|
201
|
+
class="fixed z-[2147483647] w-72 rounded-2xl bg-white p-2 shadow-xl ring-1 ring-black/5 dark:bg-gray-900"
|
|
117
202
|
style={`left:${x}px; top:${y}px`}
|
|
118
203
|
oncontextmenu={(e) => e.preventDefault()}
|
|
119
204
|
>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function portal(node, target = null) {
|
|
2
|
+
const tgt = target ?? document.body;
|
|
3
|
+
const placeholder = document.createComment('portal-placeholder');
|
|
4
|
+
node.parentNode?.insertBefore(placeholder, node);
|
|
5
|
+
tgt.appendChild(node);
|
|
6
|
+
return {
|
|
7
|
+
destroy() {
|
|
8
|
+
node.remove();
|
|
9
|
+
placeholder.parentNode?.insertBefore(node, placeholder);
|
|
10
|
+
placeholder.remove();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function clampToViewport(x, y, menuW, menuH, padding = 8) {
|
|
2
|
+
const vw = document.documentElement.clientWidth;
|
|
3
|
+
const vh = document.documentElement.clientHeight;
|
|
4
|
+
let nx = x;
|
|
5
|
+
let ny = y;
|
|
6
|
+
if (nx + menuW + padding > vw)
|
|
7
|
+
nx = Math.max(padding, vw - menuW - padding);
|
|
8
|
+
if (ny + menuH + padding > vh)
|
|
9
|
+
ny = Math.max(padding, vh - menuH - padding);
|
|
10
|
+
return { x: nx, y: ny };
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@r2digisolutions/ui",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"packageManager": "bun@1.2.23",
|
|
6
6
|
"publishConfig": {
|
|
@@ -53,20 +53,20 @@
|
|
|
53
53
|
"@playwright/test": "^1.55.1",
|
|
54
54
|
"@storybook/addon-essentials": "^8.6.14",
|
|
55
55
|
"@storybook/addon-interactions": "^8.6.14",
|
|
56
|
-
"@storybook/addon-svelte-csf": "5.0.
|
|
56
|
+
"@storybook/addon-svelte-csf": "5.0.10",
|
|
57
57
|
"@storybook/blocks": "^8.6.14",
|
|
58
|
-
"@storybook/svelte": "^9.1.
|
|
59
|
-
"@storybook/sveltekit": "^9.1.
|
|
58
|
+
"@storybook/svelte": "^9.1.10",
|
|
59
|
+
"@storybook/sveltekit": "^9.1.10",
|
|
60
60
|
"@storybook/test": "^8.6.14",
|
|
61
|
-
"@sveltejs/adapter-static": "^3.0.
|
|
62
|
-
"@sveltejs/kit": "^2.
|
|
61
|
+
"@sveltejs/adapter-static": "^3.0.10",
|
|
62
|
+
"@sveltejs/kit": "^2.44.0",
|
|
63
63
|
"@sveltejs/package": "^2.5.4",
|
|
64
64
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
|
65
|
-
"@tailwindcss/postcss": "^4.1.
|
|
65
|
+
"@tailwindcss/postcss": "^4.1.14",
|
|
66
66
|
"@testing-library/svelte": "^5.2.8",
|
|
67
67
|
"@vitest/browser": "^3.2.4",
|
|
68
68
|
"changeset": "^0.2.6",
|
|
69
|
-
"eslint": "^9.
|
|
69
|
+
"eslint": "^9.37.0",
|
|
70
70
|
"eslint-config-prettier": "^10.1.8",
|
|
71
71
|
"eslint-plugin-svelte": "^3.12.4",
|
|
72
72
|
"globals": "^16.4.0",
|
|
@@ -75,14 +75,14 @@
|
|
|
75
75
|
"prettier": "^3.6.2",
|
|
76
76
|
"prettier-plugin-svelte": "^3.4.0",
|
|
77
77
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
78
|
-
"publint": "^0.3.
|
|
79
|
-
"storybook": "^9.1.
|
|
80
|
-
"svelte": "^5.39.
|
|
78
|
+
"publint": "^0.3.14",
|
|
79
|
+
"storybook": "^9.1.10",
|
|
80
|
+
"svelte": "^5.39.9",
|
|
81
81
|
"svelte-check": "^4.3.2",
|
|
82
|
-
"tailwindcss": "^4.1.
|
|
83
|
-
"typescript": "^5.9.
|
|
82
|
+
"tailwindcss": "^4.1.14",
|
|
83
|
+
"typescript": "^5.9.3",
|
|
84
84
|
"typescript-eslint": "^8.45.0",
|
|
85
|
-
"vite": "^7.1.
|
|
85
|
+
"vite": "^7.1.9",
|
|
86
86
|
"vitest": "^3.2.4"
|
|
87
87
|
},
|
|
88
88
|
"dependencies": {
|