@r2digisolutions/ui 0.34.0 → 0.34.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,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts" generics="T">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import { tick } from 'svelte';
|
|
3
4
|
import { EllipsisVertical, ChevronDown } from 'lucide-svelte';
|
|
4
5
|
import type { ColumnDef, RowAction } from './core/types.js';
|
|
5
6
|
import type { DataTableController } from './core/DataTableController.svelte';
|
|
@@ -56,10 +57,24 @@
|
|
|
56
57
|
{} as Record<keyof T, { left?: number; right?: number }>
|
|
57
58
|
);
|
|
58
59
|
|
|
60
|
+
// CONTEXT MENU
|
|
59
61
|
let contextPopover = $state<HTMLDivElement | null>(null);
|
|
60
62
|
let contextRow = $state<T | null>(null);
|
|
63
|
+
|
|
64
|
+
// Punto “preferido” (cursor o botón)
|
|
61
65
|
let contextPos = $state<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
62
66
|
|
|
67
|
+
// ✅ Render final (clamp + flip + stick)
|
|
68
|
+
let contextRender = $state<{ x: number; y: number; transform: string }>({
|
|
69
|
+
x: 0,
|
|
70
|
+
y: 0,
|
|
71
|
+
transform: 'translate(-100%, 8px)'
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Ajusta margen si quieres “pegado” más agresivo
|
|
75
|
+
const CONTEXT_MARGIN = 10;
|
|
76
|
+
const CONTEXT_GAP = 8; // separación visual del cursor/botón
|
|
77
|
+
|
|
63
78
|
let openRows = $state<Set<string>>(new Set());
|
|
64
79
|
|
|
65
80
|
const contextOpen = $derived(contextRow !== null);
|
|
@@ -74,7 +89,7 @@
|
|
|
74
89
|
parts.push(`${w}px`);
|
|
75
90
|
});
|
|
76
91
|
|
|
77
|
-
// Columna
|
|
92
|
+
// Columna acciones
|
|
78
93
|
parts.push('64px');
|
|
79
94
|
gridTemplate = parts.join(' ');
|
|
80
95
|
|
|
@@ -86,43 +101,59 @@
|
|
|
86
101
|
let accLeft = controller.multiSelect ? 40 : 0;
|
|
87
102
|
controller.mainColumns.forEach((col) => {
|
|
88
103
|
const w = controller.getColumnWidth(col.id as keyof T);
|
|
89
|
-
if (col.sticky === 'left') {
|
|
90
|
-
offsets[col.id as keyof T] = { left: accLeft };
|
|
91
|
-
}
|
|
104
|
+
if (col.sticky === 'left') offsets[col.id as keyof T] = { left: accLeft };
|
|
92
105
|
accLeft += w;
|
|
93
106
|
});
|
|
107
|
+
|
|
94
108
|
stickyOffsets = offsets;
|
|
95
109
|
});
|
|
96
110
|
|
|
97
111
|
// CHECK ALL
|
|
98
112
|
$effect(() => {
|
|
99
113
|
if (!controller.multiSelect || !selectAllEl) return;
|
|
114
|
+
|
|
100
115
|
if (!controller.currentRows.length) {
|
|
101
116
|
selectAllEl.checked = false;
|
|
102
117
|
selectAllEl.indeterminate = false;
|
|
103
118
|
return;
|
|
104
119
|
}
|
|
120
|
+
|
|
105
121
|
selectAllEl.checked = controller.allVisibleSelected;
|
|
106
122
|
selectAllEl.indeterminate = controller.someVisibleSelected;
|
|
107
123
|
});
|
|
108
124
|
|
|
109
|
-
// CERRAR CONTEXT MENU
|
|
125
|
+
// CERRAR CONTEXT MENU al click fuera
|
|
110
126
|
$effect(() => {
|
|
111
127
|
function handleDocumentClick(event: MouseEvent) {
|
|
112
128
|
if (!contextOpen) return;
|
|
113
129
|
const target = event.target as HTMLElement;
|
|
114
|
-
if (!target.closest('[data-context-host="true"]'))
|
|
115
|
-
closeContext();
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (contextOpen) {
|
|
119
|
-
document.addEventListener('click', handleDocumentClick);
|
|
130
|
+
if (!target.closest('[data-context-host="true"]')) closeContext();
|
|
120
131
|
}
|
|
132
|
+
|
|
133
|
+
if (contextOpen) document.addEventListener('click', handleDocumentClick);
|
|
134
|
+
|
|
135
|
+
return () => document.removeEventListener('click', handleDocumentClick);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ✅ Reposicionar en resize + scroll (capture)
|
|
139
|
+
$effect(() => {
|
|
140
|
+
if (!contextOpen) return;
|
|
141
|
+
|
|
142
|
+
const onWin = () => {
|
|
143
|
+
// reintenta/ajusta por si cambia viewport, barras móviles, etc.
|
|
144
|
+
positionContext(2);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
window.addEventListener('resize', onWin);
|
|
148
|
+
window.addEventListener('scroll', onWin, { passive: true, capture: true });
|
|
149
|
+
|
|
121
150
|
return () => {
|
|
122
|
-
|
|
151
|
+
window.removeEventListener('resize', onWin);
|
|
152
|
+
window.removeEventListener('scroll', onWin, true as any);
|
|
123
153
|
};
|
|
124
154
|
});
|
|
125
155
|
|
|
156
|
+
// RESIZE COLUMNS
|
|
126
157
|
let resizingId: keyof T | null = null;
|
|
127
158
|
let startX = 0;
|
|
128
159
|
let startWidth = 0;
|
|
@@ -140,8 +171,7 @@
|
|
|
140
171
|
function onResizeMove(event: MouseEvent) {
|
|
141
172
|
if (!resizingId) return;
|
|
142
173
|
const dx = event.clientX - startX;
|
|
143
|
-
|
|
144
|
-
controller.resizeColumn(resizingId, width);
|
|
174
|
+
controller.resizeColumn(resizingId, startWidth + dx);
|
|
145
175
|
}
|
|
146
176
|
|
|
147
177
|
function onResizeUp() {
|
|
@@ -150,6 +180,7 @@
|
|
|
150
180
|
window.removeEventListener('mouseup', onResizeUp);
|
|
151
181
|
}
|
|
152
182
|
|
|
183
|
+
// HELPERS
|
|
153
184
|
function rowIdFor(row: T, index: number) {
|
|
154
185
|
return controller.getRowId(row, index);
|
|
155
186
|
}
|
|
@@ -165,28 +196,6 @@
|
|
|
165
196
|
return String(value);
|
|
166
197
|
}
|
|
167
198
|
|
|
168
|
-
function openContextAt(event: MouseEvent, row: T) {
|
|
169
|
-
event.preventDefault();
|
|
170
|
-
event.stopPropagation();
|
|
171
|
-
contextRow = row;
|
|
172
|
-
contextPos = { x: event.clientX, y: event.clientY };
|
|
173
|
-
if (contextPopover) contextPopover.showPopover();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function openContextFromButton(event: MouseEvent, row: T) {
|
|
177
|
-
event.preventDefault();
|
|
178
|
-
event.stopPropagation();
|
|
179
|
-
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
180
|
-
contextRow = row;
|
|
181
|
-
contextPos = { x: rect.right, y: rect.bottom };
|
|
182
|
-
if (contextPopover) contextPopover.showPopover();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function closeContext() {
|
|
186
|
-
if (contextPopover) contextPopover.hidePopover();
|
|
187
|
-
contextRow = null;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
199
|
function toggleRow(row: T, index: number) {
|
|
191
200
|
if (!rowCollapse) return;
|
|
192
201
|
const id = rowIdFor(row, index);
|
|
@@ -204,12 +213,171 @@
|
|
|
204
213
|
|
|
205
214
|
function handleToggleAll(e: Event) {
|
|
206
215
|
const input = e.currentTarget as HTMLInputElement;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
216
|
+
if (input.checked) controller.selectAllCurrentPage();
|
|
217
|
+
else controller.unselectAllCurrentPage();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// =========================
|
|
221
|
+
// ✅ Context positioning (robusto)
|
|
222
|
+
// =========================
|
|
223
|
+
function clamp(n: number, min: number, max: number) {
|
|
224
|
+
return Math.max(min, Math.min(max, n));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function raf() {
|
|
228
|
+
return new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Decide transform y clamp final para que el popover:
|
|
233
|
+
* - no se salga del viewport
|
|
234
|
+
* - si no cabe abajo => se pega arriba
|
|
235
|
+
* - si no cabe arriba => se pega al top margin (scroll dentro)
|
|
236
|
+
* - si no cabe a la izquierda => se pone a la derecha
|
|
237
|
+
* - si no cabe a la derecha => clamp
|
|
238
|
+
*/
|
|
239
|
+
function computeContextPosition(preferred: { x: number; y: number }, pop: DOMRect) {
|
|
240
|
+
const vw = window.innerWidth;
|
|
241
|
+
const vh = window.innerHeight;
|
|
242
|
+
|
|
243
|
+
// Si es tan alto que ni arriba ni abajo, lo forzamos dentro (scroll en contenedor)
|
|
244
|
+
const tooTall = pop.height + CONTEXT_MARGIN * 2 > vh;
|
|
245
|
+
|
|
246
|
+
// Queremos aparecer “cerca” del punto preferido
|
|
247
|
+
// Horizontal por defecto: a la izquierda del punto (como tu translate(-100%, ...))
|
|
248
|
+
const fitsLeft = preferred.x - pop.width - CONTEXT_MARGIN >= 0;
|
|
249
|
+
const fitsRight = preferred.x + pop.width + CONTEXT_MARGIN <= vw;
|
|
250
|
+
|
|
251
|
+
// Vertical por defecto: abajo del punto
|
|
252
|
+
const fitsDown = preferred.y + pop.height + CONTEXT_MARGIN + CONTEXT_GAP <= vh;
|
|
253
|
+
const fitsUp = preferred.y - pop.height - CONTEXT_MARGIN - CONTEXT_GAP >= 0;
|
|
254
|
+
|
|
255
|
+
// Horizontal: si no cabe izquierda pero cabe derecha => derecha
|
|
256
|
+
const placeToRight = !fitsLeft && fitsRight;
|
|
257
|
+
|
|
258
|
+
// Vertical:
|
|
259
|
+
// - preferimos abajo
|
|
260
|
+
// - si no cabe abajo y cabe arriba => arriba
|
|
261
|
+
// - si es tooTall => lo pegamos dentro del viewport
|
|
262
|
+
const placeUp = !tooTall && !fitsDown && fitsUp;
|
|
263
|
+
|
|
264
|
+
// Transform:
|
|
265
|
+
const transformX = placeToRight ? '0%' : '-100%';
|
|
266
|
+
// en vertical: abajo => +gap, arriba => -100% - gap
|
|
267
|
+
const transformY = placeUp ? `calc(-100% - ${CONTEXT_GAP}px)` : `${CONTEXT_GAP}px`;
|
|
268
|
+
|
|
269
|
+
let x = preferred.x;
|
|
270
|
+
let y = preferred.y;
|
|
271
|
+
|
|
272
|
+
// Clamp horizontal según transformX
|
|
273
|
+
if (transformX === '-100%') {
|
|
274
|
+
// pop ocupa [x - w, x]
|
|
275
|
+
x = clamp(x, CONTEXT_MARGIN + pop.width, vw - CONTEXT_MARGIN);
|
|
276
|
+
} else {
|
|
277
|
+
// pop ocupa [x, x + w]
|
|
278
|
+
x = clamp(x, CONTEXT_MARGIN, vw - CONTEXT_MARGIN - pop.width);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Si tooTall, lo pegamos arriba dentro del viewport y dejamos overflow-auto
|
|
282
|
+
if (tooTall) {
|
|
283
|
+
return {
|
|
284
|
+
x,
|
|
285
|
+
y: CONTEXT_MARGIN,
|
|
286
|
+
transform: `translate(${transformX}, 0px)`
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Clamp vertical
|
|
291
|
+
if (placeUp) {
|
|
292
|
+
// pop ocupa [y - gap - h, y - gap]
|
|
293
|
+
y = clamp(y, CONTEXT_MARGIN + pop.height + CONTEXT_GAP, vh - CONTEXT_MARGIN);
|
|
210
294
|
} else {
|
|
211
|
-
|
|
295
|
+
// pop ocupa [y + gap, y + gap + h]
|
|
296
|
+
// aquí el max real del y para que no se salga por abajo:
|
|
297
|
+
y = clamp(y, CONTEXT_MARGIN - CONTEXT_GAP, vh - CONTEXT_MARGIN - pop.height - CONTEXT_GAP);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 🔥 Fallback “stick”:
|
|
301
|
+
// Si aun así por alguna razón se nos quedaría fuera (casos raros), lo pegamos a bottom/top.
|
|
302
|
+
const topIfDown = y + CONTEXT_GAP; // top real cuando está abajo
|
|
303
|
+
const bottomIfDown = topIfDown + pop.height;
|
|
304
|
+
|
|
305
|
+
const bottomIfUp = y - CONTEXT_GAP; // bottom real cuando está arriba
|
|
306
|
+
const topIfUp = bottomIfUp - pop.height;
|
|
307
|
+
|
|
308
|
+
if (!placeUp && bottomIfDown > vh - CONTEXT_MARGIN) {
|
|
309
|
+
// stick to bottom: ajusta y para que bottom quede dentro
|
|
310
|
+
y = vh - CONTEXT_MARGIN - pop.height - CONTEXT_GAP;
|
|
212
311
|
}
|
|
312
|
+
|
|
313
|
+
if (placeUp && topIfUp < CONTEXT_MARGIN) {
|
|
314
|
+
// stick to top: ajusta y
|
|
315
|
+
y = CONTEXT_MARGIN + pop.height + CONTEXT_GAP;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
x,
|
|
320
|
+
y,
|
|
321
|
+
transform: `translate(${transformX}, ${transformY})`
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Posiciona con medición robusta:
|
|
327
|
+
* - espera RAF
|
|
328
|
+
* - si rect aún es 0 => retry
|
|
329
|
+
* - recalcula render
|
|
330
|
+
*/
|
|
331
|
+
async function positionContext(retries = 3) {
|
|
332
|
+
if (!contextPopover || !contextRow) return;
|
|
333
|
+
|
|
334
|
+
// RAF suele ser más fiable que tick() con Popover API
|
|
335
|
+
await raf();
|
|
336
|
+
|
|
337
|
+
const rect = contextPopover.getBoundingClientRect();
|
|
338
|
+
|
|
339
|
+
if ((!rect.width || !rect.height) && retries > 0) {
|
|
340
|
+
return positionContext(retries - 1);
|
|
341
|
+
}
|
|
342
|
+
if (!rect.width || !rect.height) return;
|
|
343
|
+
|
|
344
|
+
contextRender = computeContextPosition(contextPos, rect);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function openContextAt(event: MouseEvent, row: T) {
|
|
348
|
+
event.preventDefault();
|
|
349
|
+
event.stopPropagation();
|
|
350
|
+
|
|
351
|
+
contextRow = row;
|
|
352
|
+
contextPos = { x: event.clientX, y: event.clientY };
|
|
353
|
+
|
|
354
|
+
if (contextPopover) contextPopover.showPopover();
|
|
355
|
+
|
|
356
|
+
// Svelte render + luego RAF retry para layout real
|
|
357
|
+
await tick();
|
|
358
|
+
await positionContext(3);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function openContextFromButton(event: MouseEvent, row: T) {
|
|
362
|
+
event.preventDefault();
|
|
363
|
+
event.stopPropagation();
|
|
364
|
+
|
|
365
|
+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
366
|
+
|
|
367
|
+
contextRow = row;
|
|
368
|
+
|
|
369
|
+
// punto preferido: esquina inferior derecha del botón
|
|
370
|
+
contextPos = { x: rect.right, y: rect.bottom };
|
|
371
|
+
|
|
372
|
+
if (contextPopover) contextPopover.showPopover();
|
|
373
|
+
|
|
374
|
+
await tick();
|
|
375
|
+
await positionContext(3);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function closeContext() {
|
|
379
|
+
if (contextPopover) contextPopover.hidePopover();
|
|
380
|
+
contextRow = null;
|
|
213
381
|
}
|
|
214
382
|
</script>
|
|
215
383
|
|
|
@@ -378,7 +546,7 @@
|
|
|
378
546
|
{@const value = col.accessor ? col.accessor(row) : (row as any)[col.id]}
|
|
379
547
|
{@const sticky = stickyOffsets[col.id as keyof T]}
|
|
380
548
|
<div
|
|
381
|
-
class={`flex items-center border-r border-neutral-200/60 px-3
|
|
549
|
+
class={`flex items-center border-r border-neutral-200/60 px-3 text-black dark:text-neutral-50 ${
|
|
382
550
|
density === 'compact' ? 'py-1.5' : 'py-2.5'
|
|
383
551
|
} dark:border-neutral-800/70 ${
|
|
384
552
|
col.sticky === 'left'
|
|
@@ -390,12 +558,7 @@
|
|
|
390
558
|
: ''}
|
|
391
559
|
>
|
|
392
560
|
{#if cell}
|
|
393
|
-
{@render cell({
|
|
394
|
-
row,
|
|
395
|
-
column: col,
|
|
396
|
-
value,
|
|
397
|
-
index
|
|
398
|
-
})}
|
|
561
|
+
{@render cell({ row, column: col, value, index })}
|
|
399
562
|
{:else}
|
|
400
563
|
<span
|
|
401
564
|
class={`line-clamp-2 text-black dark:text-neutral-50 ${
|
|
@@ -501,6 +664,7 @@
|
|
|
501
664
|
: (row as any)[firstCol.id]
|
|
502
665
|
: null}
|
|
503
666
|
{@const restCols = cols.slice(1)}
|
|
667
|
+
|
|
504
668
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
505
669
|
<div
|
|
506
670
|
class={`group relative rounded-2xl border border-neutral-200/80 bg-white/80 p-3 text-[11px] text-neutral-800 shadow-sm ring-0 transition-all hover:border-purple-400/70 hover:shadow-md dark:border-neutral-800/80 dark:bg-neutral-900/80 dark:text-neutral-50 ${
|
|
@@ -526,12 +690,7 @@
|
|
|
526
690
|
|
|
527
691
|
<div class="mb-2 pr-6 text-black dark:text-neutral-50">
|
|
528
692
|
{#if cell && firstCol}
|
|
529
|
-
{@render cell({
|
|
530
|
-
row,
|
|
531
|
-
column: firstCol,
|
|
532
|
-
value: firstValue,
|
|
533
|
-
index
|
|
534
|
-
})}
|
|
693
|
+
{@render cell({ row, column: firstCol, value: firstValue, index })}
|
|
535
694
|
{:else if firstCol}
|
|
536
695
|
<div
|
|
537
696
|
class="line-clamp-2 text-[12px] leading-snug font-semibold text-neutral-900 dark:text-neutral-50"
|
|
@@ -609,12 +768,13 @@
|
|
|
609
768
|
|
|
610
769
|
<DataTableFooter />
|
|
611
770
|
|
|
771
|
+
<!-- ✅ CONTEXT POPOVER HOST -->
|
|
612
772
|
<div
|
|
613
773
|
bind:this={contextPopover}
|
|
614
774
|
popover="manual"
|
|
615
775
|
data-context-host="true"
|
|
616
|
-
class="z-[1300] max-w-xs min-w-[190px] rounded-2xl border border-neutral-200/80 bg-neutral-50/95 p-1.5 text-xs text-neutral-900 shadow-[0_18px_50px_rgba(15,23,42,0.45)] backdrop-blur-2xl dark:border-neutral-700/80 dark:bg-neutral-900/95 dark:text-neutral-50"
|
|
617
|
-
style={`position: fixed; left: ${
|
|
776
|
+
class="z-[1300] max-h-[calc(100vh-20px)] max-w-xs min-w-[190px] overflow-auto rounded-2xl border border-neutral-200/80 bg-neutral-50/95 p-1.5 text-xs text-neutral-900 shadow-[0_18px_50px_rgba(15,23,42,0.45)] backdrop-blur-2xl transition-transform duration-75 will-change-transform dark:border-neutral-700/80 dark:bg-neutral-900/95 dark:text-neutral-50"
|
|
777
|
+
style={`position: fixed; left: ${contextRender.x}px; top: ${contextRender.y}px; transform: ${contextRender.transform};`}
|
|
618
778
|
onbeforetoggle={(e) => {
|
|
619
779
|
if ((e as any).newState === 'closed') contextRow = null;
|
|
620
780
|
}}
|