next-recomponents 2.0.41 → 2.0.43

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.
@@ -0,0 +1,551 @@
1
+ import React, {
2
+ ReactNode,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+ import Pre from "../pre";
10
+ import { ContextType } from "./context";
11
+ import { Header } from "./header";
12
+ import { ExcelIcon, SearchIcon } from "./icons";
13
+ import Searchable from "./searchable";
14
+ import Modal from "../modal";
15
+ import { EditIcon } from "./icons";
16
+ import regularExpresions, { TableProps } from "./types";
17
+ import { valueFormatter } from "./formatter";
18
+ type Widths = Record<string, number>;
19
+ export default function HTable({
20
+ context,
21
+ }: {
22
+ context: ContextType & Omit<TableProps, "data">;
23
+ }) {
24
+ const [isLoading, setIsloading] = useState(false);
25
+ const { cols, headers } = context;
26
+ const [currentIndex, setCurrentIndex] = useState(-1);
27
+ const tableRef = useRef<HTMLDivElement>(null);
28
+ const modalRef = useRef<HTMLButtonElement>(null);
29
+ const catDensities: any = { confortable: 80, compact: 30, standard: 53 };
30
+ const padding =
31
+ context?.rowHeight || context?.density
32
+ ? catDensities[context?.density]
33
+ : 53;
34
+
35
+ function useResizableColumns(
36
+ headers: string[],
37
+ defaultWidth = 160,
38
+ minWidth = 60,
39
+ ) {
40
+ function getWidth(h: string) {
41
+ return [
42
+ "__select__",
43
+ "__modal__",
44
+ ...(context?.hideColumns || []),
45
+ ].includes(h)
46
+ ? minWidth
47
+ : defaultWidth;
48
+ }
49
+
50
+ function resize(headers: string[]) {
51
+ return Object.fromEntries(headers.map((h) => [h, getWidth(h)]));
52
+ }
53
+ const [widths, setWidths] = useState<Widths>({});
54
+
55
+ const startDrag = useCallback(
56
+ (e: React.MouseEvent, headerKey: string) => {
57
+ e.preventDefault();
58
+ const startX = e.clientX;
59
+ const startWidth = widths[headerKey] ?? defaultWidth;
60
+
61
+ const onMove = (ev: MouseEvent) => {
62
+ const newWidth = Math.max(
63
+ minWidth,
64
+ startWidth + (ev.clientX - startX),
65
+ );
66
+ setWidths((prev) => ({ ...prev, [headerKey]: newWidth }));
67
+ };
68
+ const onUp = () => {
69
+ window.removeEventListener("mousemove", onMove);
70
+ window.removeEventListener("mouseup", onUp);
71
+ };
72
+ window.addEventListener("mousemove", onMove);
73
+ window.addEventListener("mouseup", onUp);
74
+ },
75
+ [widths, defaultWidth, minWidth],
76
+ );
77
+
78
+ const gridTemplateColumns = headers
79
+ .filter((h) => {
80
+ if (["__modal__", "__select__"].includes(h)) {
81
+ return true;
82
+ }
83
+
84
+ return !context?.hideColumns?.includes?.(h) && !h.startsWith("_");
85
+ })
86
+ .map((h) => `${widths[h] ?? defaultWidth}px`)
87
+ .join(" ");
88
+
89
+ return { widths, gridTemplateColumns, startDrag, setWidths };
90
+ }
91
+ const { gridTemplateColumns, startDrag, widths, setWidths } =
92
+ useResizableColumns(headers);
93
+
94
+ useEffect(() => {
95
+ if (!tableRef.current) return;
96
+ const observer = new ResizeObserver(([entry]) => {
97
+ const ocultos = headers.filter(
98
+ (h) =>
99
+ !h.startsWith("__") &&
100
+ (h.startsWith("_") || context?.hideColumns?.includes?.(h)),
101
+ );
102
+ const sizeados = headers.filter((h) => h.startsWith("__"));
103
+
104
+ const personalizados = context?.colSize
105
+ ? (Object.values(context.colSize) as number[])
106
+ : [];
107
+
108
+ const personalizadosSum =
109
+ personalizados.length > 0
110
+ ? personalizados.reduce((acc, i) => acc + i, 0)
111
+ : 0;
112
+ const ancho = +entry.contentRect.width;
113
+ const sizeadosLength = sizeados.length;
114
+ const sizeadosWidth = sizeadosLength * 60;
115
+
116
+ const w =
117
+ (ancho - sizeadosWidth - personalizadosSum) /
118
+ (headers.length -
119
+ sizeadosLength -
120
+ personalizados.length -
121
+ ocultos.length);
122
+ console.log({
123
+ w,
124
+ ancho,
125
+ sizeadosWidth,
126
+ personalizadosSum,
127
+ headers,
128
+ sizeadosLength,
129
+ personalizados,
130
+ });
131
+ setWidths(
132
+ Object.fromEntries(
133
+ headers.map((h) => [
134
+ h,
135
+ context?.colSize?.[h]
136
+ ? context?.colSize?.[h]
137
+ : ["__select__", "__modal__"].includes(h)
138
+ ? 60
139
+ : w,
140
+ ]),
141
+ ),
142
+ );
143
+ });
144
+ observer.observe(tableRef.current);
145
+ return () => observer.disconnect();
146
+ }, [headers.length]);
147
+ const searchedData = context.filteredData.filter((row) => {
148
+ if (context.searchBy === "") return true;
149
+
150
+ return Object.values(row).some((value) =>
151
+ String(value).toLowerCase().includes(context.searchBy.toLowerCase()),
152
+ );
153
+ });
154
+ return (
155
+ <div
156
+ className={[context.className, "bg-white relative"].join(" ")}
157
+ ref={tableRef}
158
+ >
159
+ <div className="bg-white px-1 font-bold">{context.header}</div>
160
+ <div className="bg-white flex gap-2 items-center">
161
+ {context.searchable && <Searchable context={context} />}
162
+ {context.exportName && (
163
+ <button
164
+ onClick={(e) => {
165
+ context.excel.export(
166
+ context.data.map((d) => {
167
+ const { __select__, __modal__, ...datums } = d;
168
+ return datums;
169
+ }),
170
+ );
171
+ }}
172
+ className={
173
+ "flex gap-1 items-center border shadow rounded p-1 text-white bg-green-800 px-2"
174
+ }
175
+ >
176
+ <ExcelIcon />
177
+ Exportar
178
+ </button>
179
+ )}
180
+ {context.onSelect && (
181
+ <button
182
+ className={
183
+ "border shadow rounded p-1 text-white px-2 " +
184
+ (context.selected.length > 0 ? "bg-blue-500" : "bg-gray-500")
185
+ }
186
+ disabled={context.selected.length == 0}
187
+ onClick={async (e) => {
188
+ setIsloading(true);
189
+
190
+ const ex = context.filteredData
191
+ .filter((d) => context.selected.includes(d.id))
192
+ .map((d) => {
193
+ const { __select__, __modal__, ...datums } = d;
194
+ return datums;
195
+ });
196
+ await context.onSelect(ex);
197
+ setIsloading(false);
198
+ }}
199
+ >
200
+ {context?.onSelectLabel || "Guardar selección"}
201
+ </button>
202
+ )}
203
+ {context.onSave && (
204
+ <button
205
+ className={
206
+ "border shadow rounded p-1 text-white px-2 " +
207
+ (context.editions.length > 0 ? "bg-blue-500" : "bg-gray-500")
208
+ }
209
+ disabled={context.editions.length == 0}
210
+ onClick={async (e) => {
211
+ setIsloading(true);
212
+ const ex = context.filteredData.map((d) => {
213
+ const { __select__, __modal__, ...datums } = d;
214
+ return datums;
215
+ });
216
+ await context.onSave(ex);
217
+ setIsloading(false);
218
+ }}
219
+ >
220
+ {context.onSaveLabel || "Guardar cambios"}
221
+ </button>
222
+ )}
223
+ </div>
224
+ <div className="grid " style={{ gridTemplateColumns }}>
225
+ {headers.map((header, i) => {
226
+ if (context?.hideColumns?.includes?.(header)) return null;
227
+ if (header.startsWith("_") && !header.startsWith("__")) return null;
228
+ return (
229
+ <div
230
+ key={header}
231
+ className="bg-white"
232
+ style={{ position: "relative" }}
233
+ >
234
+ <Header
235
+ padding={padding}
236
+ header={header}
237
+ context={context}
238
+ drag={{ gridTemplateColumns, startDrag, widths }}
239
+ />
240
+ {i < headers.length - 1 && (
241
+ <div
242
+ onMouseDown={(e) => startDrag(e, header)}
243
+ style={{
244
+ position: "absolute",
245
+ right: -3,
246
+ top: 0,
247
+ width: 6,
248
+ height: "100%",
249
+ cursor: "col-resize",
250
+ background: "transparent",
251
+ }}
252
+ onMouseEnter={(e) =>
253
+ (e.currentTarget.style.background =
254
+ "var(--border-strong, #ccc)")
255
+ }
256
+ onMouseLeave={(e) =>
257
+ (e.currentTarget.style.background = "transparent")
258
+ }
259
+ />
260
+ )}
261
+ </div>
262
+ );
263
+ })}
264
+ {searchedData.map((row, rowIndex) => {
265
+ const items: any[] = Object.entries(row);
266
+
267
+ return items.map(([key, item], i) => {
268
+ if (context?.hideColumns?.includes?.(key)) return null;
269
+ if (key.startsWith("_") && !key.startsWith("__")) return null;
270
+
271
+ if (key == "__modal__") {
272
+ return (
273
+ <div
274
+ onMouseEnter={(e) => setCurrentIndex(rowIndex)}
275
+ key={row.id + i}
276
+ style={{
277
+ height: `${padding}px`,
278
+ }}
279
+ className={
280
+ (rowIndex == currentIndex
281
+ ? " bg-blue-100 "
282
+ : " bg-white ") +
283
+ " border-b p-2 flex items-center justify-center " +
284
+ (context.searchBy &&
285
+ String(item)
286
+ .toLowerCase()
287
+ .includes(context.searchBy.toLowerCase())
288
+ ? "bg-yellow-100"
289
+ : "")
290
+ }
291
+ >
292
+ {context?.modalButton || (
293
+ <button
294
+ className="border shadow rounded bg-blue-500 text-white p-2 "
295
+ onClick={(e) => {
296
+ setCurrentIndex(rowIndex);
297
+ modalRef.current?.click();
298
+ }}
299
+ >
300
+ <EditIcon />
301
+ </button>
302
+ )}
303
+ </div>
304
+ );
305
+ }
306
+
307
+ if (key == "__select__") {
308
+ return (
309
+ <div
310
+ key={row.id + i}
311
+ onMouseEnter={(e) => setCurrentIndex(rowIndex)}
312
+ style={{
313
+ height: `${padding}px`,
314
+ }}
315
+ className={
316
+ (rowIndex == currentIndex
317
+ ? " bg-blue-100 "
318
+ : " bg-white ") +
319
+ " border-b p-2 flex items-center justify-center " +
320
+ (context.searchBy &&
321
+ String(item)
322
+ .toLowerCase()
323
+ .includes(context.searchBy.toLowerCase())
324
+ ? "bg-yellow-100"
325
+ : "")
326
+ }
327
+ >
328
+ <input
329
+ type="checkbox"
330
+ className="w-5 h-5 accent-blue-600 transition-all duration-300 checked:scale-110 "
331
+ checked={Boolean(
332
+ context.selected.find((s) => s == row?.id),
333
+ )}
334
+ onChange={(e) => {
335
+ const newSelected = [...context.selected];
336
+ const index = newSelected.findIndex((s) => s == row?.id);
337
+ if (index >= 0) {
338
+ newSelected.splice(index, 1);
339
+ } else {
340
+ newSelected.push(row?.id);
341
+ }
342
+ context.setSelected(newSelected);
343
+ }}
344
+ />
345
+ </div>
346
+ );
347
+ }
348
+
349
+ return (
350
+ <div
351
+ onMouseEnter={(e) => setCurrentIndex(rowIndex)}
352
+ key={row.id + i}
353
+ style={{
354
+ fontSize: context.fontSize,
355
+ ...(context?.wrapText
356
+ ? { minHeight: `${padding}px` }
357
+ : { height: `${padding}px` }),
358
+ }}
359
+ className={
360
+ " flex justify-center items-center " +
361
+ (rowIndex == currentIndex ? " bg-blue-100 " : " bg-white ") +
362
+ (context.wrapText ? " text-wrap truncate " : " truncate ") +
363
+ " border-b " +
364
+ (context.searchBy &&
365
+ String(item)
366
+ .toLowerCase()
367
+ .includes(context.searchBy.toLowerCase())
368
+ ? rowIndex == currentIndex
369
+ ? " bg-yellow-200 "
370
+ : " bg-yellow-100 "
371
+ : "")
372
+ }
373
+ >
374
+ {context?.editableFields &&
375
+ context.editableFields.includes(key) ? (
376
+ <input
377
+ defaultValue={item}
378
+ className={
379
+ "w-full " +
380
+ (context.editions.includes(row?.id)
381
+ ? "bg-blue-400 text-white"
382
+ : "bg-blue-100")
383
+ }
384
+ onBlur={(e) => {
385
+ const id = row?.id;
386
+ const newData = [...context.data];
387
+ const index = newData.findIndex((d) => +d.id == +id);
388
+ newData[index][key] = e.target.value;
389
+ context.setData(newData);
390
+
391
+ const newEditions = [
392
+ ...new Set([...context.editions, id]),
393
+ ];
394
+ context.setEditions(newEditions);
395
+ }}
396
+ />
397
+ ) : context.buttons?.[key] ? (
398
+ React.cloneElement(context.buttons[key], {
399
+ // children: context.buttons[key]?.props?.children
400
+ // ? item
401
+ // : undefined,
402
+ value: item,
403
+ children: context.buttons[key]?.props?.children
404
+ ? item
405
+ : null,
406
+ onClick: async (e: any) => {
407
+ const ret =
408
+ (await context.buttons[key].props?.onClick?.(e)) || {};
409
+ const newData = [...context.data];
410
+ const index = newData.findIndex((f) => f.id == row?.id);
411
+ newData[index] = { ...newData[index], ...ret };
412
+ context.setData(newData);
413
+ },
414
+ onChange: async (e: { target: { value: string } }) => {
415
+ const value = e.target.value;
416
+ const ret =
417
+ (await context.buttons[key].props?.onChange?.(e)) || {};
418
+
419
+ const newData = [...context.data];
420
+ const index = newData.findIndex((f) => f.id == row?.id);
421
+
422
+ if (index >= 0) {
423
+ newData[index][key] = value;
424
+ }
425
+
426
+ for (let i in ret) {
427
+ if (newData[index]?.[i]) {
428
+ const v = ret[i];
429
+ newData[index][i] = v;
430
+ }
431
+ }
432
+
433
+ context.setData(newData);
434
+ },
435
+ className: `${context.buttons[key]?.props?.className} text-xs w-full`,
436
+ })
437
+ ) : ["number", "string"].includes(typeof item) ? (
438
+ valueFormatter({
439
+ value: item,
440
+ currentCoin: context.currentCoin,
441
+ })
442
+ ) : React.isValidElement(item) ? (
443
+ item
444
+ ) : (
445
+ JSON.stringify(item)
446
+ )}
447
+ </div>
448
+ );
449
+ });
450
+ })}
451
+ {Object.keys(context?.footer || {}).length > 0 &&
452
+ headers.map((header) => {
453
+ return (
454
+ <div
455
+ key={header}
456
+ style={{
457
+ height: `${padding}px`,
458
+ maxHeight: "100px",
459
+ }}
460
+ className={
461
+ "border-b font-bold " +
462
+ " bg-white flex items-center justify-center"
463
+ }
464
+ >
465
+ {valueFormatter({
466
+ currentCoin: context.currentCoin,
467
+ value: context.footer?.[header]
468
+ ? context.footer[header] == "sum"
469
+ ? context.filteredData
470
+ .map((d) => +d[header])
471
+ .reduce((acc, h) => h + acc, 0)
472
+ : context.footer[header] == "count"
473
+ ? context.filteredData
474
+ .map((d) => (d[header] != "" ? 1 : 0))
475
+ .reduce((acc: number, h) => acc + h, 0)
476
+ : context.footer[header] == "avg"
477
+ ? context.filteredData
478
+ .map((d) => +d[header])
479
+ .reduce((acc, h) => h + acc, 0) /
480
+ context.filteredData
481
+ .map((d) => (d[header] != "" ? 1 : 0))
482
+ .reduce((acc: number, h) => acc + h, 0)
483
+ : ""
484
+ : "",
485
+ })}
486
+ </div>
487
+ );
488
+ })}
489
+ </div>
490
+
491
+ {context.modal && (
492
+ <Modal
493
+ button={<button ref={modalRef}></button>}
494
+ color={"white"}
495
+ onClose={async () => {
496
+ if (context?.onCloseModal) {
497
+ const c = await context?.onCloseModal?.(
498
+ searchedData[currentIndex],
499
+ );
500
+
501
+ return c;
502
+ }
503
+
504
+ return true;
505
+ }}
506
+ >
507
+ <div className="relative">
508
+ <div className="fixed top-0 left-0 p-10 ">
509
+ <button
510
+ className={
511
+ "p-2 border shadow rounded " +
512
+ (currentIndex > 0 ? "bg-white " : "bg-gray-100")
513
+ }
514
+ onClick={(e) => {
515
+ currentIndex > 0 && setCurrentIndex(currentIndex - 1);
516
+ }}
517
+ >
518
+ Anterior
519
+ </button>
520
+ <button
521
+ className={
522
+ "p-2 border shadow rounded " +
523
+ (currentIndex < searchedData.length - 1
524
+ ? "bg-white "
525
+ : "bg-gray-100")
526
+ }
527
+ onClick={(e) => {
528
+ currentIndex < searchedData.length - 1 &&
529
+ setCurrentIndex(currentIndex + 1);
530
+ }}
531
+ >
532
+ Siguiente
533
+ </button>
534
+ </div>
535
+ {React.cloneElement(context.modal, {
536
+ row: searchedData[currentIndex],
537
+ })}
538
+ </div>
539
+ </Modal>
540
+ )}
541
+ {isLoading && (
542
+ <div
543
+ className="bg-white/40 absolute inset-0 text-black flex items-center justify-center"
544
+ style={{ zIndex: 9999999 }}
545
+ >
546
+ Loading...
547
+ </div>
548
+ )}
549
+ </div>
550
+ );
551
+ }