@youp-grid/react 0.1.0

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,1621 @@
1
+ import { createRemoteCacheKey, createValueHistoryState, exportGridCsv, getFillHandleCells, getFillHandleTargetRange, getInfiniteScrollTrigger, getVirtualRange, getClipboardPasteCells, isCellInRange, isRowGroupNode, normalizeCellRange, parseClipboardText, pushValueHistoryEntry, redoValueHistory, serializeGridRange, undoValueHistory, } from "@youp-grid/core";
2
+ import { createElement, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import { useYoupGrid } from "./useYoupGrid.js";
4
+ const VALUE_HISTORY_LIMIT = 100;
5
+ const DEFAULT_DENSITY = "standard";
6
+ const DENSITY_ROW_HEIGHTS = {
7
+ compact: 30,
8
+ standard: 38,
9
+ comfortable: 46,
10
+ };
11
+ const SELECTION_COLUMN_WIDTH = 44;
12
+ export function YoupGrid(props) {
13
+ const controller = useYoupGrid(props);
14
+ const rowModel = controller.rowModel;
15
+ const [internalDensity, setInternalDensity] = useState(props.defaultDensity ?? DEFAULT_DENSITY);
16
+ const density = props.density ?? internalDensity;
17
+ const rowHeight = props.rowHeight ?? DENSITY_ROW_HEIGHTS[density];
18
+ const viewportHeight = normalizeHeight(props.height) ?? 420;
19
+ const loading = props.loading ?? controller.state.remoteRequest?.status === "loading";
20
+ const infiniteScrollLoading = props.infiniteScrollLoading ?? loading;
21
+ const bodyRef = useRef(null);
22
+ const lastRowsEndReachedKeyRef = useRef();
23
+ const skipNextBlurCommitRef = useRef(false);
24
+ const valueHistoryRef = useRef(createValueHistoryState());
25
+ const [scrollTop, setScrollTop] = useState(0);
26
+ const [focusedRowIndex, setFocusedRowIndex] = useState(0);
27
+ const [focusedColumnIndex, setFocusedColumnIndex] = useState(0);
28
+ const [selectionRange, setSelectionRange] = useState();
29
+ const [fillRange, setFillRange] = useState();
30
+ const [editingCell, setEditingCell] = useState();
31
+ const [columnChooserOpen, setColumnChooserOpen] = useState(false);
32
+ const [columnMenuOpenId, setColumnMenuOpenId] = useState();
33
+ const showRowSelectionColumn = props.showRowSelectionColumn ?? false;
34
+ const displayRows = rowModel.displayRows;
35
+ const virtualRange = useMemo(() => {
36
+ return getVirtualRange({
37
+ itemCount: displayRows.length,
38
+ itemSize: rowHeight,
39
+ viewportSize: viewportHeight,
40
+ scrollOffset: scrollTop,
41
+ overscan: props.overscan,
42
+ });
43
+ }, [displayRows.length, props.overscan, rowHeight, scrollTop, viewportHeight]);
44
+ const lastVisibleRowIndex = useMemo(() => {
45
+ if (rowModel.visibleRows.length === 0) {
46
+ return -1;
47
+ }
48
+ return Math.min(rowModel.visibleRows.length - 1, Math.max(-1, Math.ceil((scrollTop + viewportHeight) / rowHeight) - 1));
49
+ }, [rowHeight, rowModel.visibleRows.length, scrollTop, viewportHeight]);
50
+ const infiniteScrollTrigger = useMemo(() => {
51
+ return getInfiniteScrollTrigger({
52
+ rowCount: rowModel.visibleRows.length,
53
+ lastVisibleRowIndex,
54
+ threshold: props.infiniteScrollThreshold,
55
+ hasMoreRows: props.hasMoreRows,
56
+ loading: infiniteScrollLoading,
57
+ });
58
+ }, [
59
+ infiniteScrollLoading,
60
+ props.hasMoreRows,
61
+ props.infiniteScrollThreshold,
62
+ lastVisibleRowIndex,
63
+ rowModel.visibleRows.length,
64
+ ]);
65
+ const infiniteScrollLoadKey = useMemo(() => {
66
+ return `${createRemoteCacheKey(controller.state)}:${infiniteScrollTrigger.rowCount}`;
67
+ }, [controller.state, infiniteScrollTrigger.rowCount]);
68
+ const currentSelectedRowIds = controller.state.selectedRowIds ?? [];
69
+ const selectedRowIds = new Set(currentSelectedRowIds);
70
+ const visibleRowIds = useMemo(() => rowModel.visibleRows.map((row) => row.id), [rowModel.visibleRows]);
71
+ const selectedVisibleRowCount = visibleRowIds.filter((rowId) => selectedRowIds.has(rowId)).length;
72
+ const allVisibleRowsSelected = visibleRowIds.length > 0 && selectedVisibleRowCount === visibleRowIds.length;
73
+ const someVisibleRowsSelected = selectedVisibleRowCount > 0 && !allVisibleRowsSelected;
74
+ const columnLayouts = useMemo(() => {
75
+ return getColumnLayouts(rowModel.visibleColumns, {
76
+ leftOffset: showRowSelectionColumn ? SELECTION_COLUMN_WIDTH : 0,
77
+ });
78
+ }, [rowModel.visibleColumns, showRowSelectionColumn]);
79
+ const visibleColumns = useMemo(() => columnLayouts.map((layout) => layout.column), [columnLayouts]);
80
+ const visibleRowIndexById = useMemo(() => {
81
+ return new Map(rowModel.visibleRows.map((row, index) => [row.id, index]));
82
+ }, [rowModel.visibleRows]);
83
+ const displayIndexByVisibleRowIndex = useMemo(() => {
84
+ const displayIndexByRowId = new Map();
85
+ displayRows.forEach((row, index) => {
86
+ if (!isRowGroupNode(row)) {
87
+ displayIndexByRowId.set(row.id, index);
88
+ }
89
+ });
90
+ return new Map(rowModel.visibleRows.map((row, index) => [
91
+ index,
92
+ displayIndexByRowId.get(row.id) ?? index,
93
+ ]));
94
+ }, [displayRows, rowModel.visibleRows]);
95
+ const headerGroupLayouts = useMemo(() => getHeaderGroupLayouts(columnLayouts), [columnLayouts]);
96
+ const hasHeaderGroups = headerGroupLayouts.some((layout) => layout.headerGroup);
97
+ const showAggregationFooter = (props.showAggregationFooter ?? true) && rowModel.aggregation.length > 0;
98
+ const focusedCell = {
99
+ rowIndex: focusedRowIndex,
100
+ columnIndex: focusedColumnIndex,
101
+ };
102
+ const gridStyle = {
103
+ ...props.style,
104
+ };
105
+ const renderedRows = virtualRange.items
106
+ .map((item) => {
107
+ const row = displayRows[item.index];
108
+ return row ? { row, displayIndex: item.index } : undefined;
109
+ })
110
+ .filter((item) => Boolean(item));
111
+ useEffect(() => {
112
+ if (focusedRowIndex >= rowModel.visibleRows.length) {
113
+ setFocusedRowIndex(Math.max(0, rowModel.visibleRows.length - 1));
114
+ }
115
+ }, [focusedRowIndex, rowModel.visibleRows.length]);
116
+ useEffect(() => {
117
+ if (focusedColumnIndex >= columnLayouts.length) {
118
+ setFocusedColumnIndex(Math.max(0, columnLayouts.length - 1));
119
+ }
120
+ }, [columnLayouts.length, focusedColumnIndex]);
121
+ useEffect(() => {
122
+ if (columnMenuOpenId && !visibleColumns.some((column) => column.id === columnMenuOpenId)) {
123
+ setColumnMenuOpenId(undefined);
124
+ }
125
+ }, [columnMenuOpenId, visibleColumns]);
126
+ useEffect(() => {
127
+ if (!props.infiniteScroll || !props.onRowsEndReached || !infiniteScrollTrigger.shouldLoadMore) {
128
+ return;
129
+ }
130
+ if (lastRowsEndReachedKeyRef.current === infiniteScrollLoadKey) {
131
+ return;
132
+ }
133
+ lastRowsEndReachedKeyRef.current = infiniteScrollLoadKey;
134
+ props.onRowsEndReached({
135
+ state: controller.state,
136
+ rowModel,
137
+ rowCount: infiniteScrollTrigger.rowCount,
138
+ lastVisibleRowIndex: infiniteScrollTrigger.lastVisibleRowIndex,
139
+ threshold: infiniteScrollTrigger.threshold,
140
+ remainingRows: infiniteScrollTrigger.remainingRows,
141
+ });
142
+ }, [
143
+ controller.state,
144
+ infiniteScrollLoadKey,
145
+ infiniteScrollTrigger,
146
+ props.infiniteScroll,
147
+ props.onRowsEndReached,
148
+ rowModel,
149
+ ]);
150
+ const startEditingCell = (cell) => {
151
+ skipNextBlurCommitRef.current = false;
152
+ setEditingCell(cell);
153
+ };
154
+ const cancelEditingCell = () => {
155
+ skipNextBlurCommitRef.current = true;
156
+ setEditingCell(undefined);
157
+ };
158
+ const applyCellValueChanges = (changes, source) => {
159
+ if (changes.length === 0) {
160
+ return;
161
+ }
162
+ if (source === "edit" || source === "paste" || source === "fill") {
163
+ valueHistoryRef.current = pushValueHistoryEntry(valueHistoryRef.current, {
164
+ changes: changes.map((change) => ({
165
+ rowId: change.rowId,
166
+ rowIndex: change.rowIndex,
167
+ columnId: change.columnId,
168
+ previousValue: change.previousValue,
169
+ value: change.value,
170
+ })),
171
+ }, { maxEntries: VALUE_HISTORY_LIMIT });
172
+ }
173
+ for (const change of changes) {
174
+ props.onCellValueChange?.({
175
+ ...change,
176
+ source,
177
+ });
178
+ }
179
+ };
180
+ const applyValueHistoryEntry = (entry, source) => {
181
+ applyCellValueChanges(getHistoryEntryValueChanges({
182
+ entry,
183
+ rowModel,
184
+ source,
185
+ }), source);
186
+ };
187
+ const undoCellValueChange = () => {
188
+ const result = undoValueHistory(valueHistoryRef.current);
189
+ if (!result.entry) {
190
+ return false;
191
+ }
192
+ valueHistoryRef.current = result.state;
193
+ applyValueHistoryEntry(result.entry, "undo");
194
+ return true;
195
+ };
196
+ const redoCellValueChange = () => {
197
+ const result = redoValueHistory(valueHistoryRef.current);
198
+ if (!result.entry) {
199
+ return false;
200
+ }
201
+ valueHistoryRef.current = result.state;
202
+ applyValueHistoryEntry(result.entry, "redo");
203
+ return true;
204
+ };
205
+ const commitEditingValue = (cell, source = "keyboard") => {
206
+ if (source === "blur" && skipNextBlurCommitRef.current) {
207
+ skipNextBlurCommitRef.current = false;
208
+ return;
209
+ }
210
+ if (source === "keyboard") {
211
+ skipNextBlurCommitRef.current = true;
212
+ }
213
+ commitEditingCell({
214
+ cell,
215
+ rowModel,
216
+ applyCellValueChanges,
217
+ });
218
+ setEditingCell(undefined);
219
+ };
220
+ const setFocusedCell = (cell, extendSelection = false, selectionAnchor) => {
221
+ if (extendSelection) {
222
+ setSelectionRange({
223
+ anchor: selectionRange?.anchor ?? selectionAnchor ?? focusedCell,
224
+ focus: cell,
225
+ });
226
+ }
227
+ else {
228
+ setSelectionRange(undefined);
229
+ }
230
+ setFocusedRowIndex(cell.rowIndex);
231
+ setFocusedColumnIndex(cell.columnIndex);
232
+ };
233
+ const setDensity = (nextDensity) => {
234
+ if (props.density === undefined) {
235
+ setInternalDensity(nextDensity);
236
+ }
237
+ props.onDensityChange?.(nextDensity);
238
+ };
239
+ const setVisibleRowsSelected = (selected) => {
240
+ if (selected) {
241
+ controller.setSelectedRows([...new Set([...currentSelectedRowIds, ...visibleRowIds])]);
242
+ return;
243
+ }
244
+ const visibleRowIdSet = new Set(visibleRowIds);
245
+ controller.setSelectedRows(currentSelectedRowIds.filter((rowId) => !visibleRowIdSet.has(rowId)));
246
+ };
247
+ return createElement("div", {
248
+ className: ["youp-grid", `youp-grid--density-${density}`, props.className].filter(Boolean).join(" "),
249
+ style: gridStyle,
250
+ }, renderColumnToolbar({
251
+ showColumnChooser: props.showColumnChooser ?? true,
252
+ showCsvExport: props.showCsvExport ?? true,
253
+ showDensityControl: props.showDensityControl ?? true,
254
+ density,
255
+ open: columnChooserOpen,
256
+ columns: rowModel.columns,
257
+ toggleOpen: () => setColumnChooserOpen((current) => !current),
258
+ setDensity,
259
+ setColumnHidden: controller.setColumnHidden,
260
+ setColumnPinned: controller.setColumnPinned,
261
+ exportCsv: () => {
262
+ downloadTextFile({
263
+ fileName: props.csvFileName ?? "youp-grid.csv",
264
+ mimeType: "text/csv;charset=utf-8",
265
+ text: exportGridCsv({
266
+ rows: rowModel.visibleRows,
267
+ columns: visibleColumns,
268
+ }),
269
+ });
270
+ },
271
+ }), createElement("div", {
272
+ className: "youp-grid__viewport",
273
+ role: "grid",
274
+ "aria-rowcount": displayRows.length,
275
+ "aria-colcount": rowModel.visibleColumns.length + (showRowSelectionColumn ? 1 : 0),
276
+ "aria-busy": loading || undefined,
277
+ onCopy: (event) => {
278
+ if (editingCell) {
279
+ return;
280
+ }
281
+ handleGridCopy({
282
+ event,
283
+ focusedCell,
284
+ selectionRange,
285
+ rows: rowModel.visibleRows,
286
+ columns: visibleColumns,
287
+ });
288
+ },
289
+ onPaste: (event) => {
290
+ if (editingCell) {
291
+ return;
292
+ }
293
+ handleGridPaste({
294
+ event,
295
+ focusedCell,
296
+ selectionRange,
297
+ rows: rowModel.visibleRows,
298
+ columns: visibleColumns,
299
+ applyCellValueChanges,
300
+ });
301
+ },
302
+ }, createElement("div", { className: "youp-grid__header", role: "rowgroup" }, hasHeaderGroups
303
+ ? createElement("div", { className: "youp-grid__row youp-grid__row--header-group", role: "row" }, showRowSelectionColumn
304
+ ? renderSelectionHeaderGroupCell()
305
+ : undefined, headerGroupLayouts.map((layout) => renderHeaderGroupCell(layout)))
306
+ : undefined, createElement("div", { className: "youp-grid__row youp-grid__row--header", role: "row" }, showRowSelectionColumn
307
+ ? renderSelectionHeaderCell({
308
+ checked: allVisibleRowsSelected,
309
+ indeterminate: someVisibleRowsSelected,
310
+ disabled: visibleRowIds.length === 0,
311
+ toggleSelected: setVisibleRowsSelected,
312
+ })
313
+ : undefined, columnLayouts.map((layout) => {
314
+ const sorted = controller.state.sort?.find((rule) => rule.columnId === layout.column.id)?.direction;
315
+ return renderHeaderCell({
316
+ layout,
317
+ sorted,
318
+ toggleSort: () => controller.toggleSort(layout.column.id),
319
+ setSort: (direction) => controller.setSort(layout.column.id, direction),
320
+ clearSort: () => controller.clearSort(layout.column.id),
321
+ filterValue: getFilterValue(controller.state, layout.column.id),
322
+ showFilter: props.showFilters ?? true,
323
+ setFilter: (value) => {
324
+ if (value) {
325
+ controller.setFilter(layout.column.id, value);
326
+ }
327
+ else {
328
+ controller.clearFilter(layout.column.id);
329
+ }
330
+ },
331
+ resizeColumn: (width) => controller.setColumnWidth(layout.column.id, width),
332
+ showMenu: props.showColumnMenu ?? true,
333
+ menuOpen: columnMenuOpenId === layout.column.id,
334
+ toggleMenu: () => {
335
+ setColumnMenuOpenId((current) => current === layout.column.id ? undefined : layout.column.id);
336
+ },
337
+ closeMenu: () => setColumnMenuOpenId(undefined),
338
+ setColumnHidden: (hidden) => controller.setColumnHidden(layout.column.id, hidden),
339
+ setColumnPinned: (pinned) => controller.setColumnPinned(layout.column.id, pinned),
340
+ renderHeader: props.renderHeader,
341
+ });
342
+ }))), createElement("div", {
343
+ className: "youp-grid__body",
344
+ role: "rowgroup",
345
+ ref: bodyRef,
346
+ style: { height: viewportHeight },
347
+ onScroll: (event) => {
348
+ setScrollTop(event.currentTarget.scrollTop);
349
+ },
350
+ }, rowModel.visibleRows.length === 0 && !props.loading && !props.error
351
+ ? createElement("div", { className: "youp-grid__empty" }, props.emptyContent ?? "No rows")
352
+ : rowModel.visibleRows.length === 0
353
+ ? undefined
354
+ : createElement("div", {
355
+ className: "youp-grid__virtual-spacer",
356
+ style: { height: virtualRange.totalSize },
357
+ }, createElement("div", {
358
+ className: "youp-grid__virtual-window",
359
+ style: { transform: `translateY(${virtualRange.beforeSize}px)` },
360
+ }, renderedRows.map(({ row, displayIndex }) => {
361
+ if (isRowGroupNode(row)) {
362
+ return renderGroupRow({
363
+ row,
364
+ columns: columnLayouts,
365
+ showSelectionColumn: showRowSelectionColumn,
366
+ rowHeight,
367
+ toggleExpanded: controller.toggleRowGroupExpanded,
368
+ });
369
+ }
370
+ const rowIndex = visibleRowIndexById.get(row.id) ?? displayIndex;
371
+ return renderRow({
372
+ row,
373
+ columns: columnLayouts,
374
+ selected: selectedRowIds.has(row.id),
375
+ showSelectionColumn: showRowSelectionColumn,
376
+ displayIndex,
377
+ rowIndex,
378
+ rowHeight,
379
+ focusedCell: {
380
+ rowIndex: focusedRowIndex,
381
+ columnIndex: focusedColumnIndex,
382
+ },
383
+ selectionRange,
384
+ fillRange,
385
+ editingCell,
386
+ editable: props.editable ?? true,
387
+ setRowSelected: (selected) => controller.setRowSelected(row.id, selected),
388
+ setFocusedCell,
389
+ startFillHandle: (event) => {
390
+ const sourceRange = normalizeCellRange(selectionRange ?? {
391
+ anchor: focusedCell,
392
+ focus: focusedCell,
393
+ });
394
+ startFillHandleDrag({
395
+ event,
396
+ sourceRange,
397
+ rowCount: rowModel.visibleRows.length,
398
+ columnCount: columnLayouts.length,
399
+ setFillRange,
400
+ applyFillRange: (targetRange) => {
401
+ applyFillHandleValues({
402
+ sourceRange,
403
+ targetRange,
404
+ rows: rowModel.visibleRows,
405
+ columns: visibleColumns,
406
+ applyCellValueChanges,
407
+ });
408
+ const nextSelectionRange = getFillSelectionRange(sourceRange, targetRange);
409
+ setSelectionRange(nextSelectionRange);
410
+ setFocusedRowIndex(nextSelectionRange.focus.rowIndex);
411
+ setFocusedColumnIndex(nextSelectionRange.focus.columnIndex);
412
+ },
413
+ });
414
+ },
415
+ startEditing: startEditingCell,
416
+ updateEditingDraft: (draftValue) => {
417
+ setEditingCell((current) => current ? { ...current, draftValue } : current);
418
+ },
419
+ cancelEditing: cancelEditingCell,
420
+ commitEditing: commitEditingValue,
421
+ onRowClick: props.onRowClick,
422
+ onRowDoubleClick: props.onRowDoubleClick,
423
+ onCellKeyDown: (event, cell) => {
424
+ handleCellKeyDown({
425
+ event,
426
+ cell,
427
+ rowCount: rowModel.visibleRows.length,
428
+ columnCount: columnLayouts.length,
429
+ rowHeight,
430
+ bodyElement: bodyRef.current,
431
+ getDisplayRowIndex: (rowIndex) => displayIndexByVisibleRowIndex.get(rowIndex) ?? rowIndex,
432
+ setFocusedCell,
433
+ selectionRange,
434
+ startEditing: startEditingCell,
435
+ commitEditing: commitEditingValue,
436
+ cancelEditing: cancelEditingCell,
437
+ undoCellValueChange,
438
+ redoCellValueChange,
439
+ toggleSelected: () => controller.toggleRowSelected(row.id),
440
+ });
441
+ },
442
+ renderCell: props.renderCell,
443
+ });
444
+ }))), renderGridOverlay({
445
+ loading,
446
+ loadingContent: props.loadingContent,
447
+ error: props.error,
448
+ errorContent: props.errorContent,
449
+ })), renderAggregationFooter({
450
+ enabled: showAggregationFooter,
451
+ aggregation: rowModel.aggregation,
452
+ columns: columnLayouts,
453
+ showSelectionColumn: showRowSelectionColumn,
454
+ })), renderPagination({
455
+ enabled: props.showPagination ?? true,
456
+ cursorPagination: controller.state.cursorPagination,
457
+ pageCount: rowModel.pageCount,
458
+ pagination: controller.state.pagination,
459
+ visibleRowCount: rowModel.visibleRowCount,
460
+ filteredRowCount: rowModel.filteredRowCount,
461
+ setCursorPage: controller.setCursorPage,
462
+ setCursorPageSize: controller.setCursorPageSize,
463
+ setPage: controller.setPage,
464
+ setPageSize: controller.setPageSize,
465
+ }));
466
+ }
467
+ function renderGridOverlay(context) {
468
+ if (context.error) {
469
+ return createElement("div", { className: "youp-grid__overlay youp-grid__overlay--error", role: "alert" }, createElement("div", { className: "youp-grid__overlay-content" }, context.errorContent ?? "Unable to load rows"));
470
+ }
471
+ if (context.loading) {
472
+ return createElement("div", { className: "youp-grid__overlay youp-grid__overlay--loading", role: "status", "aria-live": "polite" }, createElement("div", { className: "youp-grid__overlay-content" }, context.loadingContent ?? "Loading rows"));
473
+ }
474
+ return undefined;
475
+ }
476
+ function renderAggregationFooter(context) {
477
+ if (!context.enabled) {
478
+ return undefined;
479
+ }
480
+ const aggregationByColumn = groupAggregationByColumn(context.aggregation);
481
+ return createElement("div", { className: "youp-grid__aggregation", role: "rowgroup" }, createElement("div", { className: "youp-grid__row youp-grid__row--aggregation", role: "row" }, context.showSelectionColumn ? renderSelectionAggregationCell() : undefined, context.columns.map((layout) => {
482
+ const results = aggregationByColumn.get(layout.column.id) ?? [];
483
+ return createElement("div", {
484
+ key: layout.column.id,
485
+ className: getCellClassName("youp-grid__cell youp-grid__cell--aggregation", layout),
486
+ role: "gridcell",
487
+ style: getCellStyle(layout),
488
+ }, results.map(formatAggregationResult).join(" · "));
489
+ })));
490
+ }
491
+ function renderSelectionAggregationCell() {
492
+ return createElement("div", {
493
+ className: "youp-grid__selection-cell youp-grid__selection-cell--aggregation",
494
+ role: "gridcell",
495
+ style: getSelectionCellStyle(),
496
+ "aria-hidden": true,
497
+ });
498
+ }
499
+ function groupAggregationByColumn(results) {
500
+ const byColumn = new Map();
501
+ for (const result of results) {
502
+ const existing = byColumn.get(result.columnId) ?? [];
503
+ existing.push(result);
504
+ byColumn.set(result.columnId, existing);
505
+ }
506
+ return byColumn;
507
+ }
508
+ function formatAggregationResult(result) {
509
+ return `${result.label} ${formatAggregationValue(result.value)}`;
510
+ }
511
+ function formatAggregationValue(value) {
512
+ if (value === undefined) {
513
+ return "-";
514
+ }
515
+ if (Number.isInteger(value)) {
516
+ return String(value);
517
+ }
518
+ return value.toFixed(2);
519
+ }
520
+ function renderHeaderGroupCell(layout) {
521
+ return createElement("div", {
522
+ key: layout.id,
523
+ className: getHeaderGroupClassName(layout),
524
+ role: "columnheader",
525
+ "aria-colspan": layout.columnCount,
526
+ "aria-hidden": layout.headerGroup ? undefined : true,
527
+ style: getHeaderGroupStyle(layout),
528
+ }, layout.headerGroup ?? "");
529
+ }
530
+ function renderSelectionHeaderGroupCell() {
531
+ return createElement("div", {
532
+ key: "__selection-group",
533
+ className: "youp-grid__cell youp-grid__cell--header-group youp-grid__selection-cell youp-grid__selection-cell--header-group",
534
+ role: "columnheader",
535
+ "aria-hidden": true,
536
+ style: getSelectionCellStyle(),
537
+ });
538
+ }
539
+ function renderSelectionHeaderCell(context) {
540
+ return createElement("div", {
541
+ key: "__selection",
542
+ className: "youp-grid__cell youp-grid__cell--header youp-grid__selection-cell youp-grid__selection-cell--header",
543
+ role: "columnheader",
544
+ style: getSelectionCellStyle(),
545
+ }, createElement(SelectionHeaderCheckbox, context));
546
+ }
547
+ function SelectionHeaderCheckbox(context) {
548
+ const inputRef = useRef(null);
549
+ useLayoutEffect(() => {
550
+ if (inputRef.current) {
551
+ inputRef.current.indeterminate = context.indeterminate;
552
+ }
553
+ }, [context.indeterminate]);
554
+ return createElement("input", {
555
+ className: "youp-grid__selection-checkbox",
556
+ type: "checkbox",
557
+ checked: context.checked,
558
+ disabled: context.disabled,
559
+ "aria-label": "Select visible rows",
560
+ "aria-checked": context.indeterminate ? "mixed" : context.checked,
561
+ ref: inputRef,
562
+ onChange: (event) => {
563
+ context.toggleSelected(event.currentTarget.checked);
564
+ },
565
+ onClick: (event) => {
566
+ event.stopPropagation();
567
+ },
568
+ });
569
+ }
570
+ function renderHeaderCell(context) {
571
+ const sortable = context.layout.column.sortable !== false;
572
+ return createElement("div", {
573
+ key: context.layout.column.id,
574
+ className: getCellClassName("youp-grid__cell youp-grid__cell--header", context.layout),
575
+ role: "columnheader",
576
+ style: getCellStyle(context.layout),
577
+ "aria-sort": context.sorted === "desc" ? "descending" : context.sorted === "asc" ? "ascending" : "none",
578
+ }, createElement("div", { className: "youp-grid__header-main" }, createElement("button", {
579
+ className: "youp-grid__sort-button",
580
+ type: "button",
581
+ disabled: !sortable,
582
+ onClick: sortable ? context.toggleSort : undefined,
583
+ }, context.renderHeader
584
+ ? context.renderHeader({
585
+ column: context.layout.column,
586
+ sorted: context.sorted,
587
+ toggleSort: context.toggleSort,
588
+ })
589
+ : createElement("span", { className: "youp-grid__header-label" }, context.layout.column.headerName, context.sorted ? ` ${context.sorted === "desc" ? "↓" : "↑"}` : "")), context.showMenu
590
+ ? createElement("button", {
591
+ className: "youp-grid__column-menu-button",
592
+ type: "button",
593
+ "aria-haspopup": "menu",
594
+ "aria-expanded": context.menuOpen,
595
+ "aria-label": `${context.layout.column.headerName} column menu`,
596
+ onClick: (event) => {
597
+ event.stopPropagation();
598
+ context.toggleMenu();
599
+ },
600
+ }, "⋯")
601
+ : undefined), context.menuOpen
602
+ ? renderColumnMenu({
603
+ column: context.layout.column,
604
+ sortable,
605
+ sorted: context.sorted,
606
+ filterValue: context.filterValue,
607
+ setSort: context.setSort,
608
+ clearSort: context.clearSort,
609
+ clearFilter: () => context.setFilter(""),
610
+ setColumnHidden: context.setColumnHidden,
611
+ setColumnPinned: context.setColumnPinned,
612
+ closeMenu: context.closeMenu,
613
+ })
614
+ : undefined, context.showFilter && context.layout.column.filterable !== false
615
+ ? createElement("input", {
616
+ className: "youp-grid__filter",
617
+ value: context.filterValue,
618
+ placeholder: "Filter",
619
+ "aria-label": `Filter ${context.layout.column.headerName}`,
620
+ onChange: (event) => {
621
+ context.setFilter(event.currentTarget.value);
622
+ },
623
+ onClick: (event) => event.stopPropagation(),
624
+ })
625
+ : undefined, createElement("span", {
626
+ className: "youp-grid__resize-handle",
627
+ role: "separator",
628
+ "aria-orientation": "vertical",
629
+ "aria-label": `Resize ${context.layout.column.headerName}`,
630
+ onMouseDown: (event) => {
631
+ startColumnResize({
632
+ event,
633
+ column: context.layout.column,
634
+ resizeColumn: context.resizeColumn,
635
+ });
636
+ },
637
+ }));
638
+ }
639
+ function renderColumnMenu(context) {
640
+ const runAction = (action) => {
641
+ action();
642
+ context.closeMenu();
643
+ };
644
+ return createElement("div", { className: "youp-grid__column-menu", role: "menu" }, renderColumnMenuButton({
645
+ label: "Sort ascending",
646
+ disabled: !context.sortable || context.sorted === "asc",
647
+ onClick: () => runAction(() => context.setSort("asc")),
648
+ }), renderColumnMenuButton({
649
+ label: "Sort descending",
650
+ disabled: !context.sortable || context.sorted === "desc",
651
+ onClick: () => runAction(() => context.setSort("desc")),
652
+ }), renderColumnMenuButton({
653
+ label: "Clear sort",
654
+ disabled: !context.sorted,
655
+ onClick: () => runAction(context.clearSort),
656
+ }), createElement("div", { className: "youp-grid__column-menu-separator", role: "separator" }), renderColumnMenuButton({
657
+ label: "Pin left",
658
+ disabled: context.column.pinned === "left",
659
+ onClick: () => runAction(() => context.setColumnPinned("left")),
660
+ }), renderColumnMenuButton({
661
+ label: "Pin right",
662
+ disabled: context.column.pinned === "right",
663
+ onClick: () => runAction(() => context.setColumnPinned("right")),
664
+ }), renderColumnMenuButton({
665
+ label: "Unpin",
666
+ disabled: !context.column.pinned,
667
+ onClick: () => runAction(() => context.setColumnPinned(undefined)),
668
+ }), createElement("div", { className: "youp-grid__column-menu-separator", role: "separator" }), renderColumnMenuButton({
669
+ label: "Clear filter",
670
+ disabled: !context.filterValue,
671
+ onClick: () => runAction(context.clearFilter),
672
+ }), renderColumnMenuButton({
673
+ label: "Hide column",
674
+ onClick: () => runAction(() => context.setColumnHidden(true)),
675
+ }));
676
+ }
677
+ function renderColumnMenuButton(context) {
678
+ return createElement("button", {
679
+ className: "youp-grid__column-menu-item",
680
+ type: "button",
681
+ role: "menuitem",
682
+ disabled: context.disabled,
683
+ onClick: (event) => {
684
+ event.stopPropagation();
685
+ context.onClick();
686
+ },
687
+ }, context.label);
688
+ }
689
+ function renderGroupRow(context) {
690
+ const width = getColumnsWidth(context.columns);
691
+ return createElement("div", {
692
+ key: context.row.groupId,
693
+ className: "youp-grid__row youp-grid__row--group",
694
+ role: "row",
695
+ "aria-rowindex": context.row.index + 1,
696
+ style: { height: context.rowHeight },
697
+ }, context.showSelectionColumn ? renderSelectionGroupCell() : undefined, createElement("div", {
698
+ className: "youp-grid__cell youp-grid__cell--group",
699
+ role: "gridcell",
700
+ "aria-colspan": context.columns.length,
701
+ style: {
702
+ width,
703
+ flex: `0 0 ${width}px`,
704
+ paddingLeft: 12 + context.row.depth * 18,
705
+ },
706
+ }, createElement("button", {
707
+ className: "youp-grid__group-toggle",
708
+ type: "button",
709
+ "aria-expanded": context.row.expanded,
710
+ onClick: () => context.toggleExpanded(context.row.groupId),
711
+ }, createElement("span", { className: "youp-grid__group-caret", "aria-hidden": true }, context.row.expanded ? "v" : ">"), createElement("span", { className: "youp-grid__group-label" }, context.row.label), createElement("span", { className: "youp-grid__group-count" }, `${context.row.rowCount} rows`))));
712
+ }
713
+ function renderSelectionGroupCell() {
714
+ return createElement("div", {
715
+ className: "youp-grid__selection-cell youp-grid__selection-cell--group",
716
+ role: "gridcell",
717
+ style: getSelectionCellStyle(),
718
+ "aria-hidden": true,
719
+ });
720
+ }
721
+ function renderRow(context) {
722
+ return createElement("div", {
723
+ key: rowKey(context.row.id),
724
+ className: [
725
+ "youp-grid__row",
726
+ context.selected ? "youp-grid__row--selected" : "",
727
+ ]
728
+ .filter(Boolean)
729
+ .join(" "),
730
+ role: "row",
731
+ "aria-rowindex": context.displayIndex + 1,
732
+ "aria-selected": context.selected,
733
+ "data-youp-row-index": context.rowIndex,
734
+ style: { height: context.rowHeight },
735
+ onClick: (event) => {
736
+ if (!shouldIgnoreRowMouseEvent(event)) {
737
+ context.onRowClick?.(createRowEvent(context, event));
738
+ }
739
+ },
740
+ onDoubleClick: (event) => {
741
+ if (!shouldIgnoreRowMouseEvent(event)) {
742
+ context.onRowDoubleClick?.(createRowEvent(context, event));
743
+ }
744
+ },
745
+ }, context.showSelectionColumn
746
+ ? renderSelectionCell({
747
+ rowIndex: context.rowIndex,
748
+ selected: context.selected,
749
+ setSelected: context.setRowSelected,
750
+ })
751
+ : undefined, context.columns.map((layout, columnIndex) => {
752
+ const focused = context.focusedCell.rowIndex === context.rowIndex && context.focusedCell.columnIndex === columnIndex;
753
+ const editing = isEditingCell(context.editingCell, context.row, layout.column);
754
+ const activeRange = normalizeCellRange(context.selectionRange ?? {
755
+ anchor: context.focusedCell,
756
+ focus: context.focusedCell,
757
+ });
758
+ const selected = context.selectionRange
759
+ ? isCellInRange(context.rowIndex, columnIndex, context.selectionRange)
760
+ : false;
761
+ const fillTargeted = context.fillRange
762
+ ? isCellInNormalizedRange(context.rowIndex, columnIndex, context.fillRange)
763
+ : false;
764
+ const showFillHandle = !context.editingCell &&
765
+ context.rowIndex === activeRange.endRowIndex &&
766
+ columnIndex === activeRange.endColumnIndex;
767
+ return renderCell({
768
+ row: context.row,
769
+ layout,
770
+ rowIndex: context.rowIndex,
771
+ columnIndex,
772
+ ariaColumnOffset: context.showSelectionColumn ? 1 : 0,
773
+ focused,
774
+ selected,
775
+ fillTargeted,
776
+ showFillHandle,
777
+ editing,
778
+ editingCell: context.editingCell,
779
+ editable: context.editable,
780
+ setFocusedCell: context.setFocusedCell,
781
+ startFillHandle: context.startFillHandle,
782
+ startEditing: context.startEditing,
783
+ updateEditingDraft: context.updateEditingDraft,
784
+ cancelEditing: context.cancelEditing,
785
+ commitEditing: context.commitEditing,
786
+ onKeyDown: context.onCellKeyDown,
787
+ renderCell: context.renderCell,
788
+ });
789
+ }));
790
+ }
791
+ function renderSelectionCell(context) {
792
+ return createElement("div", {
793
+ className: "youp-grid__cell youp-grid__selection-cell",
794
+ role: "gridcell",
795
+ "aria-colindex": 1,
796
+ style: getSelectionCellStyle(),
797
+ }, createElement("input", {
798
+ className: "youp-grid__selection-checkbox",
799
+ type: "checkbox",
800
+ checked: context.selected,
801
+ "aria-label": `Select row ${context.rowIndex + 1}`,
802
+ onChange: (event) => {
803
+ context.setSelected(event.currentTarget.checked);
804
+ },
805
+ onClick: (event) => {
806
+ event.stopPropagation();
807
+ },
808
+ }));
809
+ }
810
+ function createRowEvent(context, event) {
811
+ return {
812
+ row: context.row.original,
813
+ rowNode: context.row,
814
+ rowId: context.row.id,
815
+ rowIndex: context.rowIndex,
816
+ event,
817
+ };
818
+ }
819
+ function shouldIgnoreRowMouseEvent(event) {
820
+ const target = event.target;
821
+ if (!(target instanceof Element)) {
822
+ return false;
823
+ }
824
+ return Boolean(target.closest("button,input,select,textarea,a,[contenteditable='true']"));
825
+ }
826
+ function renderCell(context) {
827
+ const column = context.layout.column;
828
+ const value = column.accessor(context.row.original);
829
+ const editable = context.editable && column.editable !== false;
830
+ const cellContext = {
831
+ row: context.row,
832
+ column,
833
+ value,
834
+ editing: context.editing,
835
+ focused: context.focused,
836
+ };
837
+ const cellState = {
838
+ row: context.row,
839
+ rowIndex: context.rowIndex,
840
+ column,
841
+ columnIndex: context.columnIndex,
842
+ value,
843
+ editable,
844
+ };
845
+ const cellContent = context.renderCell
846
+ ? context.renderCell(cellContext)
847
+ : formatCellValue(column, context.row.original, value);
848
+ return createElement("div", {
849
+ key: column.id,
850
+ className: getCellClassName([
851
+ "youp-grid__cell",
852
+ context.focused ? "youp-grid__cell--focused" : "",
853
+ context.selected ? "youp-grid__cell--range-selected" : "",
854
+ context.fillTargeted ? "youp-grid__cell--fill-target" : "",
855
+ context.editing ? "youp-grid__cell--editing" : "",
856
+ ].filter(Boolean).join(" "), context.layout),
857
+ role: "gridcell",
858
+ tabIndex: context.focused && !context.editing ? 0 : -1,
859
+ "aria-colindex": context.columnIndex + context.ariaColumnOffset + 1,
860
+ "data-youp-row-index": context.rowIndex,
861
+ "data-youp-column-index": context.columnIndex,
862
+ "data-youp-column-id": column.id,
863
+ style: getCellStyle(context.layout),
864
+ onClick: (event) => {
865
+ event.currentTarget.focus({ preventScroll: true });
866
+ context.setFocusedCell({ rowIndex: context.rowIndex, columnIndex: context.columnIndex }, event.shiftKey);
867
+ },
868
+ onDoubleClick: () => {
869
+ if (editable) {
870
+ context.startEditing(createEditingCell(cellState, value));
871
+ }
872
+ },
873
+ onKeyDown: (event) => context.onKeyDown(event, cellState),
874
+ }, context.editing
875
+ ? createElement("input", {
876
+ className: "youp-grid__cell-editor",
877
+ value: context.editingCell?.draftValue ?? "",
878
+ autoFocus: true,
879
+ onChange: (event) => {
880
+ context.updateEditingDraft(event.currentTarget.value);
881
+ },
882
+ onBlur: (event) => {
883
+ context.commitEditing(createEditingCell(cellState, event.currentTarget.value), "blur");
884
+ },
885
+ onKeyDown: (event) => {
886
+ event.stopPropagation();
887
+ context.onKeyDown(event, cellState);
888
+ },
889
+ })
890
+ : cellContent, !context.editing && context.showFillHandle
891
+ ? createElement("span", {
892
+ className: "youp-grid__fill-handle",
893
+ role: "button",
894
+ "aria-label": "Fill selection",
895
+ onMouseDown: context.startFillHandle,
896
+ })
897
+ : undefined);
898
+ }
899
+ function renderColumnToolbar(context) {
900
+ if (!context.showColumnChooser && !context.showCsvExport && !context.showDensityControl) {
901
+ return undefined;
902
+ }
903
+ return createElement("div", { className: "youp-grid__toolbar" }, context.showColumnChooser
904
+ ? createElement("button", {
905
+ className: "youp-grid__toolbar-button",
906
+ type: "button",
907
+ "aria-expanded": context.open,
908
+ onClick: context.toggleOpen,
909
+ }, "Columns")
910
+ : undefined, context.showCsvExport
911
+ ? createElement("button", {
912
+ className: "youp-grid__toolbar-button",
913
+ type: "button",
914
+ onClick: context.exportCsv,
915
+ }, "Export CSV")
916
+ : undefined, context.showDensityControl
917
+ ? createElement("label", { className: "youp-grid__density-control" }, "Density", createElement("select", {
918
+ value: context.density,
919
+ onChange: (event) => {
920
+ context.setDensity(event.currentTarget.value);
921
+ },
922
+ }, renderDensityOption("compact", "Compact"), renderDensityOption("standard", "Standard"), renderDensityOption("comfortable", "Comfortable")))
923
+ : undefined, context.showColumnChooser && context.open
924
+ ? createElement("div", { className: "youp-grid__column-panel" }, context.columns.map((column) => {
925
+ return createElement("div", { key: column.id, className: "youp-grid__column-panel-row" }, createElement("label", { className: "youp-grid__column-toggle" }, createElement("input", {
926
+ type: "checkbox",
927
+ checked: !column.hidden,
928
+ onChange: (event) => {
929
+ context.setColumnHidden(column.id, !event.currentTarget.checked);
930
+ },
931
+ }), column.headerName), createElement("div", { className: "youp-grid__pin-controls", role: "group", "aria-label": `Pin ${column.headerName}` }, renderPinButton(column, undefined, "None", context.setColumnPinned), renderPinButton(column, "left", "Left", context.setColumnPinned), renderPinButton(column, "right", "Right", context.setColumnPinned)));
932
+ }))
933
+ : undefined);
934
+ }
935
+ function renderDensityOption(value, label) {
936
+ return createElement("option", { key: value, value }, label);
937
+ }
938
+ function renderPinButton(column, pin, label, setColumnPinned) {
939
+ const active = column.pinned === pin || (!column.pinned && pin === undefined);
940
+ return createElement("button", {
941
+ className: ["youp-grid__pin-button", active ? "youp-grid__pin-button--active" : ""]
942
+ .filter(Boolean)
943
+ .join(" "),
944
+ type: "button",
945
+ "aria-pressed": active,
946
+ onClick: () => setColumnPinned(column.id, pin),
947
+ }, label);
948
+ }
949
+ function getColumnLayouts(columns, options = {}) {
950
+ const leftColumns = columns.filter((column) => column.pinned === "left");
951
+ const centerColumns = columns.filter((column) => !column.pinned);
952
+ const rightColumns = columns.filter((column) => column.pinned === "right");
953
+ const rightOffsets = getRightOffsets(rightColumns);
954
+ return [
955
+ ...leftColumns.map((column, index) => ({
956
+ column,
957
+ pinned: "left",
958
+ stickyOffset: getLeftOffset(leftColumns, index, options.leftOffset ?? 0),
959
+ pinnedEdge: index === leftColumns.length - 1 ? "left-last" : undefined,
960
+ })),
961
+ ...centerColumns.map((column) => ({ column })),
962
+ ...rightColumns.map((column, index) => ({
963
+ column,
964
+ pinned: "right",
965
+ stickyOffset: rightOffsets[index],
966
+ pinnedEdge: index === 0 ? "right-first" : undefined,
967
+ })),
968
+ ];
969
+ }
970
+ function getHeaderGroupLayouts(columns) {
971
+ const groups = [];
972
+ for (const layout of columns) {
973
+ const headerGroup = normalizeHeaderGroup(layout.column.headerGroup);
974
+ const previous = groups[groups.length - 1];
975
+ if (previous && previous.headerGroup === headerGroup && previous.pinned === layout.pinned) {
976
+ previous.columns.push(layout.column);
977
+ previous.columnCount += 1;
978
+ previous.width += getColumnWidth(layout.column);
979
+ if (layout.pinned === "right") {
980
+ previous.stickyOffset = layout.stickyOffset;
981
+ }
982
+ if (layout.pinnedEdge) {
983
+ previous.pinnedEdge = layout.pinnedEdge;
984
+ }
985
+ continue;
986
+ }
987
+ groups.push({
988
+ id: `${layout.pinned ?? "center"}:${headerGroup ?? "none"}:${groups.length}`,
989
+ headerGroup,
990
+ columns: [layout.column],
991
+ columnCount: 1,
992
+ width: getColumnWidth(layout.column),
993
+ pinned: layout.pinned,
994
+ stickyOffset: layout.stickyOffset,
995
+ pinnedEdge: layout.pinnedEdge,
996
+ });
997
+ }
998
+ return groups;
999
+ }
1000
+ function normalizeHeaderGroup(headerGroup) {
1001
+ const trimmed = headerGroup?.trim();
1002
+ return trimmed ? trimmed : undefined;
1003
+ }
1004
+ function getLeftOffset(columns, index, baseOffset = 0) {
1005
+ return columns.slice(0, index).reduce((sum, column) => sum + getColumnWidth(column), baseOffset);
1006
+ }
1007
+ function getRightOffsets(columns) {
1008
+ const offsets = new Array(columns.length);
1009
+ let offset = 0;
1010
+ for (let index = columns.length - 1; index >= 0; index -= 1) {
1011
+ offsets[index] = offset;
1012
+ offset += getColumnWidth(columns[index]);
1013
+ }
1014
+ return offsets;
1015
+ }
1016
+ function getColumnWidth(column) {
1017
+ return column.width ?? 160;
1018
+ }
1019
+ function getColumnsWidth(columns) {
1020
+ return columns.reduce((sum, layout) => sum + getColumnWidth(layout.column), 0);
1021
+ }
1022
+ function getCellClassName(baseClassName, layout) {
1023
+ return [
1024
+ baseClassName,
1025
+ layout.pinned ? `youp-grid__cell--pinned-${layout.pinned}` : "",
1026
+ layout.pinnedEdge ? `youp-grid__cell--${layout.pinnedEdge}` : "",
1027
+ ]
1028
+ .filter(Boolean)
1029
+ .join(" ");
1030
+ }
1031
+ function getHeaderGroupClassName(layout) {
1032
+ return [
1033
+ "youp-grid__cell youp-grid__cell--header-group",
1034
+ layout.headerGroup ? "" : "youp-grid__cell--header-group-empty",
1035
+ layout.pinned ? `youp-grid__cell--pinned-${layout.pinned}` : "",
1036
+ layout.pinnedEdge ? `youp-grid__cell--${layout.pinnedEdge}` : "",
1037
+ ]
1038
+ .filter(Boolean)
1039
+ .join(" ");
1040
+ }
1041
+ function getCellStyle(layout) {
1042
+ const width = getColumnWidth(layout.column);
1043
+ const style = {
1044
+ width,
1045
+ flex: `0 0 ${width}px`,
1046
+ };
1047
+ if (layout.pinned === "left") {
1048
+ style.left = layout.stickyOffset ?? 0;
1049
+ }
1050
+ if (layout.pinned === "right") {
1051
+ style.right = layout.stickyOffset ?? 0;
1052
+ }
1053
+ return style;
1054
+ }
1055
+ function getHeaderGroupStyle(layout) {
1056
+ const style = {
1057
+ width: layout.width,
1058
+ flex: `0 0 ${layout.width}px`,
1059
+ };
1060
+ if (layout.pinned === "left") {
1061
+ style.left = layout.stickyOffset ?? 0;
1062
+ }
1063
+ if (layout.pinned === "right") {
1064
+ style.right = layout.stickyOffset ?? 0;
1065
+ }
1066
+ return style;
1067
+ }
1068
+ function getSelectionCellStyle() {
1069
+ return {
1070
+ left: 0,
1071
+ width: SELECTION_COLUMN_WIDTH,
1072
+ flex: `0 0 ${SELECTION_COLUMN_WIDTH}px`,
1073
+ };
1074
+ }
1075
+ function rowKey(rowId) {
1076
+ return String(rowId);
1077
+ }
1078
+ function renderPagination(context) {
1079
+ if (!context.enabled) {
1080
+ return undefined;
1081
+ }
1082
+ if (context.cursorPagination) {
1083
+ return renderCursorPagination({
1084
+ cursorPagination: context.cursorPagination,
1085
+ visibleRowCount: context.visibleRowCount,
1086
+ filteredRowCount: context.filteredRowCount,
1087
+ setCursorPage: context.setCursorPage,
1088
+ setCursorPageSize: context.setCursorPageSize,
1089
+ });
1090
+ }
1091
+ if (!context.pagination || !context.pageCount) {
1092
+ return undefined;
1093
+ }
1094
+ const currentPage = context.pagination.pageIndex + 1;
1095
+ return createElement("div", { className: "youp-grid__pagination" }, createElement("button", {
1096
+ type: "button",
1097
+ disabled: context.pagination.pageIndex === 0,
1098
+ onClick: () => context.setPage(context.pagination.pageIndex - 1),
1099
+ }, "Previous"), createElement("span", { className: "youp-grid__page-status" }, `Page ${currentPage} of ${context.pageCount} · ${context.visibleRowCount} shown · ${context.filteredRowCount} matched`), createElement("button", {
1100
+ type: "button",
1101
+ disabled: currentPage >= context.pageCount,
1102
+ onClick: () => context.setPage(context.pagination.pageIndex + 1),
1103
+ }, "Next"), createElement("label", { className: "youp-grid__page-size" }, "Rows", createElement("select", {
1104
+ value: context.pagination.pageSize,
1105
+ onChange: (event) => {
1106
+ context.setPageSize(Number(event.currentTarget.value));
1107
+ },
1108
+ }, [50, 100, 250, 500].map((pageSize) => {
1109
+ return createElement("option", { key: pageSize, value: pageSize }, String(pageSize));
1110
+ }))));
1111
+ }
1112
+ function renderCursorPagination(context) {
1113
+ return createElement("div", { className: "youp-grid__pagination youp-grid__pagination--cursor" }, createElement("button", {
1114
+ type: "button",
1115
+ disabled: !context.cursorPagination.hasPreviousPage,
1116
+ onClick: () => context.setCursorPage(context.cursorPagination.previousCursor),
1117
+ }, "Previous"), createElement("span", { className: "youp-grid__page-status" }, `Cursor page · ${context.visibleRowCount} shown · ${context.filteredRowCount} matched`), createElement("button", {
1118
+ type: "button",
1119
+ disabled: !context.cursorPagination.hasNextPage,
1120
+ onClick: () => context.setCursorPage(context.cursorPagination.nextCursor),
1121
+ }, "Next"), createElement("label", { className: "youp-grid__page-size" }, "Rows", createElement("select", {
1122
+ value: context.cursorPagination.pageSize,
1123
+ onChange: (event) => {
1124
+ context.setCursorPageSize(Number(event.currentTarget.value));
1125
+ },
1126
+ }, [50, 100, 250, 500].map((pageSize) => {
1127
+ return createElement("option", { key: pageSize, value: pageSize }, String(pageSize));
1128
+ }))));
1129
+ }
1130
+ function startColumnResize(context) {
1131
+ context.event.preventDefault();
1132
+ context.event.stopPropagation();
1133
+ const startX = context.event.clientX;
1134
+ const startWidth = context.column.width ?? 160;
1135
+ const minWidth = context.column.minWidth ?? 64;
1136
+ const maxWidth = context.column.maxWidth;
1137
+ function handleMouseMove(event) {
1138
+ const nextWidth = clamp(startWidth + event.clientX - startX, minWidth, maxWidth ?? Number.MAX_SAFE_INTEGER);
1139
+ context.resizeColumn(nextWidth);
1140
+ }
1141
+ function handleMouseUp() {
1142
+ document.removeEventListener("mousemove", handleMouseMove);
1143
+ document.removeEventListener("mouseup", handleMouseUp);
1144
+ }
1145
+ document.addEventListener("mousemove", handleMouseMove);
1146
+ document.addEventListener("mouseup", handleMouseUp);
1147
+ }
1148
+ function handleCellKeyDown(context) {
1149
+ if (context.rowCount === 0 || context.columnCount === 0) {
1150
+ return;
1151
+ }
1152
+ const editingCell = context.event.currentTarget.classList.contains("youp-grid__cell-editor")
1153
+ ? createEditingCell(context.cell, context.event.currentTarget.value)
1154
+ : undefined;
1155
+ if (editingCell) {
1156
+ if (context.event.key === "Escape") {
1157
+ context.event.preventDefault();
1158
+ context.cancelEditing();
1159
+ focusCell(context.bodyElement, context.cell);
1160
+ return;
1161
+ }
1162
+ if (context.event.key === "Enter") {
1163
+ context.event.preventDefault();
1164
+ context.commitEditing(editingCell);
1165
+ moveFocusedCell({
1166
+ ...context,
1167
+ nextCell: {
1168
+ rowIndex: clamp(context.cell.rowIndex + 1, 0, context.rowCount - 1),
1169
+ columnIndex: context.cell.columnIndex,
1170
+ },
1171
+ });
1172
+ return;
1173
+ }
1174
+ if (context.event.key === "Tab") {
1175
+ context.event.preventDefault();
1176
+ context.commitEditing(editingCell);
1177
+ moveFocusedCell({
1178
+ ...context,
1179
+ nextCell: getNextTabCell(context.cell, context.rowCount, context.columnCount, context.event.shiftKey),
1180
+ });
1181
+ return;
1182
+ }
1183
+ return;
1184
+ }
1185
+ if (isUndoShortcut(context.event)) {
1186
+ context.event.preventDefault();
1187
+ context.undoCellValueChange();
1188
+ return;
1189
+ }
1190
+ if (isRedoShortcut(context.event)) {
1191
+ context.event.preventDefault();
1192
+ context.redoCellValueChange();
1193
+ return;
1194
+ }
1195
+ if (context.event.key === "Enter" || context.event.key === "F2") {
1196
+ if (context.cell.editable) {
1197
+ context.event.preventDefault();
1198
+ context.startEditing(createEditingCell(context.cell, context.cell.value));
1199
+ }
1200
+ return;
1201
+ }
1202
+ if (context.event.key === " ") {
1203
+ context.event.preventDefault();
1204
+ if (context.event.shiftKey) {
1205
+ context.setFocusedCell(context.cell, true);
1206
+ }
1207
+ else {
1208
+ context.toggleSelected();
1209
+ }
1210
+ return;
1211
+ }
1212
+ if (isPrintableKey(context.event)) {
1213
+ if (context.cell.editable) {
1214
+ context.event.preventDefault();
1215
+ context.startEditing(createEditingCell(context.cell, context.event.key));
1216
+ }
1217
+ return;
1218
+ }
1219
+ const nextCell = getNextNavigationCell(context);
1220
+ if (!nextCell) {
1221
+ return;
1222
+ }
1223
+ context.event.preventDefault();
1224
+ moveFocusedCell({
1225
+ ...context,
1226
+ nextCell,
1227
+ extendSelection: context.event.shiftKey,
1228
+ selectionAnchor: {
1229
+ rowIndex: context.cell.rowIndex,
1230
+ columnIndex: context.cell.columnIndex,
1231
+ },
1232
+ });
1233
+ }
1234
+ function getNextNavigationCell(context) {
1235
+ switch (context.event.key) {
1236
+ case "ArrowDown":
1237
+ return {
1238
+ rowIndex: clamp(context.cell.rowIndex + 1, 0, context.rowCount - 1),
1239
+ columnIndex: context.cell.columnIndex,
1240
+ };
1241
+ case "ArrowUp":
1242
+ return {
1243
+ rowIndex: clamp(context.cell.rowIndex - 1, 0, context.rowCount - 1),
1244
+ columnIndex: context.cell.columnIndex,
1245
+ };
1246
+ case "ArrowRight":
1247
+ return {
1248
+ rowIndex: context.cell.rowIndex,
1249
+ columnIndex: clamp(context.cell.columnIndex + 1, 0, context.columnCount - 1),
1250
+ };
1251
+ case "ArrowLeft":
1252
+ return {
1253
+ rowIndex: context.cell.rowIndex,
1254
+ columnIndex: clamp(context.cell.columnIndex - 1, 0, context.columnCount - 1),
1255
+ };
1256
+ case "Home":
1257
+ return {
1258
+ rowIndex: context.cell.rowIndex,
1259
+ columnIndex: 0,
1260
+ };
1261
+ case "End":
1262
+ return {
1263
+ rowIndex: context.cell.rowIndex,
1264
+ columnIndex: context.columnCount - 1,
1265
+ };
1266
+ case "Tab":
1267
+ return getNextTabCell(context.cell, context.rowCount, context.columnCount, context.event.shiftKey);
1268
+ }
1269
+ return undefined;
1270
+ }
1271
+ function getNextTabCell(cell, rowCount, columnCount, backwards) {
1272
+ const delta = backwards ? -1 : 1;
1273
+ const flatIndex = cell.rowIndex * columnCount + cell.columnIndex;
1274
+ const nextFlatIndex = clamp(flatIndex + delta, 0, rowCount * columnCount - 1);
1275
+ return {
1276
+ rowIndex: Math.floor(nextFlatIndex / columnCount),
1277
+ columnIndex: nextFlatIndex % columnCount,
1278
+ };
1279
+ }
1280
+ function moveFocusedCell(context) {
1281
+ context.setFocusedCell(context.nextCell, context.extendSelection, context.selectionAnchor);
1282
+ if (!context.bodyElement) {
1283
+ return;
1284
+ }
1285
+ ensureCellRowVisible({
1286
+ bodyElement: context.bodyElement,
1287
+ rowIndex: context.getDisplayRowIndex(context.nextCell.rowIndex),
1288
+ rowHeight: context.rowHeight,
1289
+ });
1290
+ focusCellAfterRender({
1291
+ bodyElement: context.bodyElement,
1292
+ cell: context.nextCell,
1293
+ attempts: 2,
1294
+ });
1295
+ }
1296
+ function ensureCellRowVisible(context) {
1297
+ const rowTop = context.rowIndex * context.rowHeight;
1298
+ const rowBottom = rowTop + context.rowHeight;
1299
+ const viewportTop = context.bodyElement.scrollTop;
1300
+ const viewportBottom = viewportTop + context.bodyElement.clientHeight;
1301
+ if (rowTop < viewportTop) {
1302
+ context.bodyElement.scrollTop = rowTop;
1303
+ return;
1304
+ }
1305
+ if (rowBottom > viewportBottom) {
1306
+ context.bodyElement.scrollTop = rowBottom - context.bodyElement.clientHeight;
1307
+ }
1308
+ }
1309
+ function ensureCellColumnVisible(bodyElement, cellElement) {
1310
+ if (cellElement.classList.contains("youp-grid__cell--pinned-left")) {
1311
+ return;
1312
+ }
1313
+ if (cellElement.classList.contains("youp-grid__cell--pinned-right")) {
1314
+ return;
1315
+ }
1316
+ const leftPinnedWidth = getPinnedCellWidth(cellElement.parentElement, "left");
1317
+ const rightPinnedWidth = getPinnedCellWidth(cellElement.parentElement, "right");
1318
+ const cellLeft = cellElement.offsetLeft;
1319
+ const cellRight = cellLeft + cellElement.offsetWidth;
1320
+ const viewportLeft = bodyElement.scrollLeft + leftPinnedWidth;
1321
+ const viewportRight = bodyElement.scrollLeft + bodyElement.clientWidth - rightPinnedWidth;
1322
+ if (cellLeft < viewportLeft) {
1323
+ bodyElement.scrollLeft = Math.max(0, cellLeft - leftPinnedWidth);
1324
+ return;
1325
+ }
1326
+ if (cellRight > viewportRight) {
1327
+ bodyElement.scrollLeft = cellRight - bodyElement.clientWidth + rightPinnedWidth;
1328
+ }
1329
+ }
1330
+ function getPinnedCellWidth(rowElement, pin) {
1331
+ if (!rowElement) {
1332
+ return 0;
1333
+ }
1334
+ const selector = pin === "left"
1335
+ ? ".youp-grid__selection-cell, .youp-grid__cell--pinned-left"
1336
+ : ".youp-grid__cell--pinned-right";
1337
+ return Array.from(rowElement.querySelectorAll(selector))
1338
+ .reduce((width, cell) => width + cell.offsetWidth, 0);
1339
+ }
1340
+ function focusCell(bodyElement, cell) {
1341
+ const cellElement = bodyElement?.querySelector(`[data-youp-row-index="${cell.rowIndex}"][data-youp-column-index="${cell.columnIndex}"]`);
1342
+ cellElement?.focus({ preventScroll: true });
1343
+ return cellElement ?? undefined;
1344
+ }
1345
+ function focusCellAfterRender(context) {
1346
+ requestAnimationFrame(() => {
1347
+ const cellElement = focusCell(context.bodyElement, context.cell);
1348
+ if (cellElement && context.bodyElement) {
1349
+ ensureCellColumnVisible(context.bodyElement, cellElement);
1350
+ return;
1351
+ }
1352
+ if (context.attempts > 0) {
1353
+ focusCellAfterRender({
1354
+ ...context,
1355
+ attempts: context.attempts - 1,
1356
+ });
1357
+ }
1358
+ });
1359
+ }
1360
+ function createEditingCell(cell, value) {
1361
+ return {
1362
+ rowId: cell.row.id,
1363
+ rowIndex: cell.rowIndex,
1364
+ columnId: cell.column.id,
1365
+ columnIndex: cell.columnIndex,
1366
+ draftValue: String(value ?? ""),
1367
+ };
1368
+ }
1369
+ function isEditingCell(editingCell, row, column) {
1370
+ return editingCell?.rowId === row.id && editingCell.columnId === column.id;
1371
+ }
1372
+ function commitEditingCell(context) {
1373
+ const row = context.rowModel.visibleRows[context.cell.rowIndex];
1374
+ const column = context.rowModel.visibleColumns.find((item) => item.id === context.cell.columnId);
1375
+ if (!row || !column) {
1376
+ return;
1377
+ }
1378
+ const previousValue = column.accessor(row.original);
1379
+ const value = column.valueParser
1380
+ ? column.valueParser(context.cell.draftValue, row.original)
1381
+ : context.cell.draftValue;
1382
+ if (Object.is(value, previousValue)) {
1383
+ return;
1384
+ }
1385
+ context.applyCellValueChanges([
1386
+ {
1387
+ row: row.original,
1388
+ rowId: row.id,
1389
+ rowIndex: context.cell.rowIndex,
1390
+ column,
1391
+ columnId: column.id,
1392
+ value,
1393
+ previousValue,
1394
+ },
1395
+ ], "edit");
1396
+ }
1397
+ function handleGridCopy(context) {
1398
+ const range = context.selectionRange ?? {
1399
+ anchor: context.focusedCell,
1400
+ focus: context.focusedCell,
1401
+ };
1402
+ const text = serializeGridRange({
1403
+ rows: context.rows,
1404
+ columns: context.columns,
1405
+ range,
1406
+ });
1407
+ context.event.preventDefault();
1408
+ context.event.clipboardData.setData("text/plain", text);
1409
+ }
1410
+ function handleGridPaste(context) {
1411
+ const values = parseClipboardText(context.event.clipboardData.getData("text/plain"));
1412
+ if (values.length === 0) {
1413
+ return;
1414
+ }
1415
+ const normalizedRange = context.selectionRange ? normalizeCellRange(context.selectionRange) : undefined;
1416
+ const startCell = normalizedRange
1417
+ ? {
1418
+ rowIndex: normalizedRange.startRowIndex,
1419
+ columnIndex: normalizedRange.startColumnIndex,
1420
+ }
1421
+ : context.focusedCell;
1422
+ const fillRange = normalizedRange && values.length === 1 && values[0]?.length === 1
1423
+ ? normalizedRange
1424
+ : undefined;
1425
+ context.event.preventDefault();
1426
+ const changes = [];
1427
+ for (const cell of getClipboardPasteCells({
1428
+ values,
1429
+ startCell,
1430
+ rowCount: context.rows.length,
1431
+ columnCount: context.columns.length,
1432
+ fillRange,
1433
+ })) {
1434
+ const row = context.rows[cell.rowIndex];
1435
+ const column = context.columns[cell.columnIndex];
1436
+ if (!row || !column || column.editable === false) {
1437
+ continue;
1438
+ }
1439
+ const previousValue = column.accessor(row.original);
1440
+ const value = column.valueParser ? column.valueParser(cell.value, row.original) : cell.value;
1441
+ if (Object.is(value, previousValue)) {
1442
+ continue;
1443
+ }
1444
+ changes.push({
1445
+ row: row.original,
1446
+ rowId: row.id,
1447
+ rowIndex: cell.rowIndex,
1448
+ column,
1449
+ columnId: column.id,
1450
+ value,
1451
+ previousValue,
1452
+ });
1453
+ }
1454
+ context.applyCellValueChanges(changes, "paste");
1455
+ }
1456
+ function startFillHandleDrag(context) {
1457
+ context.event.preventDefault();
1458
+ context.event.stopPropagation();
1459
+ let currentRange;
1460
+ const updateRange = (event) => {
1461
+ const targetCell = getCellCoordinateFromPoint(event.clientX, event.clientY);
1462
+ currentRange = targetCell
1463
+ ? getFillHandleTargetRange({
1464
+ sourceRange: context.sourceRange,
1465
+ targetCell,
1466
+ rowCount: context.rowCount,
1467
+ columnCount: context.columnCount,
1468
+ })
1469
+ : undefined;
1470
+ context.setFillRange(currentRange);
1471
+ };
1472
+ const handleMouseMove = (event) => {
1473
+ event.preventDefault();
1474
+ updateRange(event);
1475
+ };
1476
+ const handleMouseUp = (event) => {
1477
+ event.preventDefault();
1478
+ document.removeEventListener("mousemove", handleMouseMove);
1479
+ document.removeEventListener("mouseup", handleMouseUp);
1480
+ updateRange(event);
1481
+ context.setFillRange(undefined);
1482
+ if (currentRange) {
1483
+ context.applyFillRange(currentRange);
1484
+ }
1485
+ };
1486
+ document.addEventListener("mousemove", handleMouseMove);
1487
+ document.addEventListener("mouseup", handleMouseUp);
1488
+ }
1489
+ function applyFillHandleValues(context) {
1490
+ const changes = [];
1491
+ for (const cell of getFillHandleCells({
1492
+ sourceRange: context.sourceRange,
1493
+ targetRange: context.targetRange,
1494
+ getValue: ({ rowIndex, columnIndex }) => {
1495
+ const row = context.rows[rowIndex];
1496
+ const column = context.columns[columnIndex];
1497
+ return row && column ? column.accessor(row.original) : undefined;
1498
+ },
1499
+ })) {
1500
+ const row = context.rows[cell.rowIndex];
1501
+ const column = context.columns[cell.columnIndex];
1502
+ if (!row || !column || column.editable === false) {
1503
+ continue;
1504
+ }
1505
+ const previousValue = column.accessor(row.original);
1506
+ if (Object.is(cell.value, previousValue)) {
1507
+ continue;
1508
+ }
1509
+ changes.push({
1510
+ row: row.original,
1511
+ rowId: row.id,
1512
+ rowIndex: cell.rowIndex,
1513
+ column,
1514
+ columnId: column.id,
1515
+ value: cell.value,
1516
+ previousValue,
1517
+ });
1518
+ }
1519
+ context.applyCellValueChanges(changes, "fill");
1520
+ }
1521
+ function getFillSelectionRange(sourceRange, targetRange) {
1522
+ return {
1523
+ anchor: {
1524
+ rowIndex: Math.min(sourceRange.startRowIndex, targetRange.startRowIndex),
1525
+ columnIndex: Math.min(sourceRange.startColumnIndex, targetRange.startColumnIndex),
1526
+ },
1527
+ focus: {
1528
+ rowIndex: Math.max(sourceRange.endRowIndex, targetRange.endRowIndex),
1529
+ columnIndex: Math.max(sourceRange.endColumnIndex, targetRange.endColumnIndex),
1530
+ },
1531
+ };
1532
+ }
1533
+ function getCellCoordinateFromPoint(clientX, clientY) {
1534
+ const element = document.elementFromPoint(clientX, clientY);
1535
+ const cell = element?.closest('[role="gridcell"][data-youp-row-index][data-youp-column-index]');
1536
+ if (!cell) {
1537
+ return undefined;
1538
+ }
1539
+ const rowIndex = Number(cell.dataset.youpRowIndex);
1540
+ const columnIndex = Number(cell.dataset.youpColumnIndex);
1541
+ if (!Number.isInteger(rowIndex) || !Number.isInteger(columnIndex)) {
1542
+ return undefined;
1543
+ }
1544
+ return { rowIndex, columnIndex };
1545
+ }
1546
+ function isCellInNormalizedRange(rowIndex, columnIndex, range) {
1547
+ return (rowIndex >= range.startRowIndex &&
1548
+ rowIndex <= range.endRowIndex &&
1549
+ columnIndex >= range.startColumnIndex &&
1550
+ columnIndex <= range.endColumnIndex);
1551
+ }
1552
+ function downloadTextFile(options) {
1553
+ const blob = new Blob([options.text], { type: options.mimeType });
1554
+ const url = URL.createObjectURL(blob);
1555
+ const link = document.createElement("a");
1556
+ link.href = url;
1557
+ link.download = options.fileName;
1558
+ document.body.append(link);
1559
+ link.click();
1560
+ link.remove();
1561
+ URL.revokeObjectURL(url);
1562
+ }
1563
+ function getHistoryEntryValueChanges(context) {
1564
+ const changes = [];
1565
+ for (const historyChange of context.entry.changes) {
1566
+ const row = context.rowModel.allRows.find((item) => item.id === historyChange.rowId) ??
1567
+ context.rowModel.visibleRows[historyChange.rowIndex];
1568
+ const column = context.rowModel.columns.find((item) => item.id === historyChange.columnId);
1569
+ if (!row || !column) {
1570
+ continue;
1571
+ }
1572
+ const value = context.source === "undo" ? historyChange.previousValue : historyChange.value;
1573
+ const previousValue = column.accessor(row.original);
1574
+ if (Object.is(value, previousValue)) {
1575
+ continue;
1576
+ }
1577
+ const visibleRowIndex = context.rowModel.visibleRows.findIndex((item) => item.id === row.id);
1578
+ changes.push({
1579
+ row: row.original,
1580
+ rowId: row.id,
1581
+ rowIndex: visibleRowIndex >= 0 ? visibleRowIndex : row.index,
1582
+ column,
1583
+ columnId: column.id,
1584
+ value,
1585
+ previousValue,
1586
+ });
1587
+ }
1588
+ return changes;
1589
+ }
1590
+ function formatCellValue(column, row, value) {
1591
+ return column.valueFormatter ? column.valueFormatter(value, row) : String(value ?? "");
1592
+ }
1593
+ function isPrintableKey(event) {
1594
+ return event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey;
1595
+ }
1596
+ function isUndoShortcut(event) {
1597
+ return (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "z";
1598
+ }
1599
+ function isRedoShortcut(event) {
1600
+ if (!(event.metaKey || event.ctrlKey) || event.altKey) {
1601
+ return false;
1602
+ }
1603
+ const key = event.key.toLowerCase();
1604
+ return key === "y" || (event.shiftKey && key === "z");
1605
+ }
1606
+ function getFilterValue(state, columnId) {
1607
+ const value = state.filters?.find((filter) => filter.columnId === columnId)?.value;
1608
+ return value == null ? "" : String(value);
1609
+ }
1610
+ function clamp(value, min, max) {
1611
+ return Math.min(Math.max(value, min), max);
1612
+ }
1613
+ function normalizeHeight(height) {
1614
+ if (typeof height === "number") {
1615
+ return height;
1616
+ }
1617
+ if (typeof height === "string" && height.endsWith("px")) {
1618
+ return Number.parseInt(height, 10);
1619
+ }
1620
+ return undefined;
1621
+ }