@r2digisolutions/ui 0.24.3 → 0.24.4
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.
|
@@ -124,7 +124,6 @@
|
|
|
124
124
|
) {
|
|
125
125
|
e.preventDefault();
|
|
126
126
|
const columnIndex = columnId ? manager.state.visibleColumns.indexOf(columnId) : null;
|
|
127
|
-
// Always set fresh coordinates and ensure menu is closed before reopening
|
|
128
127
|
if (rightMenu.open) {
|
|
129
128
|
rightMenu = { open: false, x: 0, y: 0 };
|
|
130
129
|
setTimeout(() => {
|
|
@@ -148,7 +147,6 @@
|
|
|
148
147
|
return manager.state.items.filter((r) => ids.has(rowId(r)));
|
|
149
148
|
}
|
|
150
149
|
|
|
151
|
-
// Tracks
|
|
152
150
|
function colTrack(cId: string, measuring: boolean) {
|
|
153
151
|
if (measuring) return 'max-content';
|
|
154
152
|
const c = manager.getColumn(cId);
|
|
@@ -291,7 +289,7 @@
|
|
|
291
289
|
data-dt-cell="1"
|
|
292
290
|
data-col-id={cid}
|
|
293
291
|
data-row-index={i}
|
|
294
|
-
class="px-3"
|
|
292
|
+
class="flex h-full w-full items-center px-3"
|
|
295
293
|
onclick={() => onRowClick?.(row)}
|
|
296
294
|
oncontextmenu={(e) => onCellContext(e, row, cid, i)}
|
|
297
295
|
>
|
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { TContextMenuEntry } from '../core/types.js';
|
|
3
3
|
|
|
4
|
+
// --- Action portal ---
|
|
5
|
+
function portal(node: HTMLElement, target: HTMLElement | null = null) {
|
|
6
|
+
const tgt = target ?? document.body;
|
|
7
|
+
const placeholder = document.createComment('portal-placeholder');
|
|
8
|
+
if (!node.parentNode) return { destroy: () => {} };
|
|
9
|
+
node.parentNode.insertBefore(placeholder, node);
|
|
10
|
+
tgt.appendChild(node);
|
|
11
|
+
return {
|
|
12
|
+
destroy() {
|
|
13
|
+
if (node.isConnected) node.remove();
|
|
14
|
+
if (placeholder.isConnected && placeholder.parentNode) {
|
|
15
|
+
placeholder.parentNode.insertBefore(node, placeholder);
|
|
16
|
+
placeholder.remove();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// --- Util para clamping a viewport ---
|
|
23
|
+
function clampToViewport(x: number, y: number, menuW: number, menuH: number, padding = 8) {
|
|
24
|
+
const vw = document.documentElement.clientWidth;
|
|
25
|
+
const vh = document.documentElement.clientHeight;
|
|
26
|
+
let nx = x;
|
|
27
|
+
let ny = y;
|
|
28
|
+
if (nx + menuW + padding > vw) nx = Math.max(padding, vw - menuW - padding);
|
|
29
|
+
if (ny + menuH + padding > vh) ny = Math.max(padding, vh - menuH - padding);
|
|
30
|
+
return { x: nx, y: ny };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- Props ---
|
|
4
34
|
type Props = {
|
|
5
35
|
items?: TContextMenuEntry[];
|
|
6
36
|
x?: number;
|
|
@@ -9,6 +39,7 @@
|
|
|
9
39
|
title?: string;
|
|
10
40
|
searchable?: boolean;
|
|
11
41
|
context?: any;
|
|
42
|
+
portalTarget?: HTMLElement | null;
|
|
12
43
|
};
|
|
13
44
|
let {
|
|
14
45
|
items = [],
|
|
@@ -17,9 +48,11 @@
|
|
|
17
48
|
open = $bindable(false),
|
|
18
49
|
title = '',
|
|
19
50
|
searchable = true,
|
|
20
|
-
context = null
|
|
51
|
+
context = null,
|
|
52
|
+
portalTarget = null
|
|
21
53
|
}: Props = $props();
|
|
22
54
|
|
|
55
|
+
// --- Estado interno ---
|
|
23
56
|
let stack = $state<{ label: string; items: TContextMenuEntry[] }[]>([]);
|
|
24
57
|
let q = $state('');
|
|
25
58
|
const current = $derived(stack.length ? stack[stack.length - 1] : { label: title, items });
|
|
@@ -27,7 +60,6 @@
|
|
|
27
60
|
let menuEl: HTMLDivElement | null = $state(null);
|
|
28
61
|
|
|
29
62
|
function close() {
|
|
30
|
-
if (menuEl) menuEl.hidePopover();
|
|
31
63
|
open = false;
|
|
32
64
|
stack = [];
|
|
33
65
|
q = '';
|
|
@@ -96,54 +128,89 @@
|
|
|
96
128
|
}
|
|
97
129
|
}
|
|
98
130
|
|
|
99
|
-
//
|
|
131
|
+
// Teclado (Escape / Backspace)
|
|
100
132
|
$effect(() => {
|
|
101
|
-
if (!
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
133
|
+
if (!open) return;
|
|
134
|
+
const handler = (e: KeyboardEvent) => onKey(e);
|
|
135
|
+
document.addEventListener('keydown', handler);
|
|
136
|
+
return () => document.removeEventListener('keydown', handler);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Cierres robustos: click-fuera, scroll/resize, y bloqueo de menú nativo
|
|
140
|
+
$effect(() => {
|
|
141
|
+
if (!open || !menuEl) return;
|
|
142
|
+
|
|
143
|
+
const onPointerDown = (ev: PointerEvent) => {
|
|
144
|
+
if (!menuEl || menuEl.contains(ev.target as Node)) return;
|
|
145
|
+
close();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const onNativeCtx = (ev: MouseEvent) => {
|
|
149
|
+
if (!menuEl || menuEl.contains(ev.target as Node)) return;
|
|
150
|
+
ev.preventDefault();
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
let timeout: NodeJS.Timeout;
|
|
154
|
+
const onScrollOrResize = () => {
|
|
155
|
+
clearTimeout(timeout);
|
|
156
|
+
timeout = setTimeout(() => close(), 100);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
document.addEventListener('pointerdown', onPointerDown);
|
|
160
|
+
document.addEventListener('contextmenu', onNativeCtx);
|
|
161
|
+
window.addEventListener('scroll', onScrollOrResize, true);
|
|
162
|
+
window.addEventListener('resize', onScrollOrResize, true);
|
|
163
|
+
|
|
164
|
+
return () => {
|
|
165
|
+
clearTimeout(timeout);
|
|
166
|
+
document.removeEventListener('pointerdown', onPointerDown);
|
|
167
|
+
document.removeEventListener('contextmenu', onNativeCtx);
|
|
168
|
+
window.removeEventListener('scroll', onScrollOrResize, true);
|
|
169
|
+
window.removeEventListener('resize', onScrollOrResize, true);
|
|
170
|
+
};
|
|
109
171
|
});
|
|
110
172
|
|
|
111
|
-
// Clamp
|
|
173
|
+
// Clamp de posición tras abrir o cambiar coords
|
|
112
174
|
$effect(() => {
|
|
113
175
|
if (!open || !menuEl) return;
|
|
114
176
|
requestAnimationFrame(() => {
|
|
115
177
|
if (!menuEl) return;
|
|
116
178
|
const rect = menuEl.getBoundingClientRect();
|
|
117
|
-
const
|
|
118
|
-
const vh = document.documentElement.clientHeight;
|
|
119
|
-
let nx = x;
|
|
120
|
-
let ny = y;
|
|
121
|
-
const padding = 8;
|
|
122
|
-
if (nx + rect.width + padding > vw) nx = Math.max(padding, vw - rect.width - padding);
|
|
123
|
-
if (ny + rect.height + padding > vh) ny = Math.max(padding, vh - rect.height - padding);
|
|
179
|
+
const { x: nx, y: ny } = clampToViewport(x, y, rect.width, rect.height, 8);
|
|
124
180
|
if (nx !== x || ny !== y) {
|
|
125
181
|
x = nx;
|
|
126
182
|
y = ny;
|
|
127
|
-
menuEl.style.setProperty('--popover-x', `${nx}px`);
|
|
128
|
-
menuEl.style.setProperty('--popover-y', `${ny}px`);
|
|
129
183
|
}
|
|
130
184
|
});
|
|
131
185
|
});
|
|
132
186
|
</script>
|
|
133
187
|
|
|
134
188
|
{#if open}
|
|
189
|
+
<!-- BACKDROP -->
|
|
190
|
+
<div
|
|
191
|
+
use:portal={portalTarget}
|
|
192
|
+
role="dialog"
|
|
193
|
+
class="fixed inset-0 z-[2147483646]"
|
|
194
|
+
onclick={() => close()}
|
|
195
|
+
oncontextmenu={(e) => e.preventDefault()}
|
|
196
|
+
aria-modal="true"
|
|
197
|
+
tabindex="0"
|
|
198
|
+
style="pointer-events:auto"
|
|
199
|
+
/>
|
|
200
|
+
|
|
201
|
+
<!-- MENÚ -->
|
|
135
202
|
<div
|
|
136
203
|
bind:this={menuEl}
|
|
137
|
-
|
|
138
|
-
class="w-72 rounded-2xl bg-white p-2 shadow-xl ring-1 ring-black/5 dark:bg-gray-900"
|
|
139
|
-
style=
|
|
204
|
+
use:portal={portalTarget}
|
|
205
|
+
class="fixed z-[2147483647] w-72 rounded-2xl bg-white p-2 shadow-xl ring-1 ring-black/5 dark:bg-gray-900"
|
|
206
|
+
style={`left:${x}px; top:${y}px`}
|
|
140
207
|
oncontextmenu={(e) => e.preventDefault()}
|
|
141
|
-
onkeydown={onKey}
|
|
142
208
|
>
|
|
143
209
|
<div class="flex items-center gap-1 px-1 py-1">
|
|
144
210
|
{#if stack.length > 0}
|
|
145
211
|
<button
|
|
146
212
|
class="rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-800"
|
|
213
|
+
role="dialog"
|
|
147
214
|
aria-label="Atrás"
|
|
148
215
|
onclick={back}
|
|
149
216
|
>
|
|
@@ -155,10 +222,8 @@
|
|
|
155
222
|
stroke="currentColor"
|
|
156
223
|
stroke-width="2"
|
|
157
224
|
stroke-linecap="round"
|
|
158
|
-
stroke-linejoin="round"
|
|
225
|
+
stroke-linejoin="round"><polyline points="15 18 9 12 15 6" /></svg
|
|
159
226
|
>
|
|
160
|
-
<polyline points="15 18 9 12 15 6" />
|
|
161
|
-
</svg>
|
|
162
227
|
</button>
|
|
163
228
|
{/if}
|
|
164
229
|
<div class="min-w-0 flex-1 truncate px-1 text-xs font-medium opacity-70">
|
|
@@ -184,15 +249,16 @@
|
|
|
184
249
|
{:else}
|
|
185
250
|
<button
|
|
186
251
|
class="flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm hover:bg-gray-100 disabled:opacity-50 dark:hover:bg-gray-800"
|
|
252
|
+
role="dialog"
|
|
187
253
|
disabled={it.disabled}
|
|
188
254
|
onclick={() => clickItem(it)}
|
|
189
255
|
>
|
|
190
256
|
<span class="truncate">{it.label}</span>
|
|
191
257
|
<span class="flex items-center gap-2">
|
|
192
258
|
{#if it.shortcut}
|
|
193
|
-
<kbd class="rounded bg-gray-100 px-1 text-[10px] dark:bg-gray-800"
|
|
194
|
-
{it.shortcut}
|
|
195
|
-
|
|
259
|
+
<kbd class="rounded bg-gray-100 px-1 text-[10px] dark:bg-gray-800"
|
|
260
|
+
>{it.shortcut}</kbd
|
|
261
|
+
>
|
|
196
262
|
{/if}
|
|
197
263
|
{#if hasChildren(it)}
|
|
198
264
|
<svg
|
|
@@ -204,10 +270,8 @@
|
|
|
204
270
|
stroke="currentColor"
|
|
205
271
|
stroke-width="2"
|
|
206
272
|
stroke-linecap="round"
|
|
207
|
-
stroke-linejoin="round"
|
|
273
|
+
stroke-linejoin="round"><polyline points="9 18 15 12 9 6" /></svg
|
|
208
274
|
>
|
|
209
|
-
<polyline points="9 18 15 12 9 6" />
|
|
210
|
-
</svg>
|
|
211
275
|
{/if}
|
|
212
276
|
</span>
|
|
213
277
|
</button>
|
|
@@ -216,12 +280,3 @@
|
|
|
216
280
|
</div>
|
|
217
281
|
</div>
|
|
218
282
|
{/if}
|
|
219
|
-
|
|
220
|
-
<style>
|
|
221
|
-
[popover] {
|
|
222
|
-
margin: 0;
|
|
223
|
-
border: none;
|
|
224
|
-
padding: 0;
|
|
225
|
-
z-index: 2147483647;
|
|
226
|
-
}
|
|
227
|
-
</style>
|