@opencorestack/opengridx 3.7.0 → 3.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/ai.cjs +3 -3
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.es.js +3 -3
- package/dist/ai.es.js.map +1 -1
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +3195 -3119
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +15 -15
- package/dist/opengridx.umd.js.map +1 -1
- package/lib/components/DataGrid/DataGrid.css +0 -3
- package/lib/components/ListView/ListView.css +4 -4
- package/lib/hooks/core/useLayout.ts +34 -24
- package/lib/styles/base/reset.css +5 -3
- package/lib/utils/export/exportShared.ts +22 -4
- package/lib/utils/export/index.ts +8 -4
- package/lib/utils/filtering/index.ts +8 -2
- package/lib/utils/parsing.ts +24 -1
- package/lib/utils/values.ts +31 -0
- package/package.json +1 -1
|
@@ -19,9 +19,6 @@
|
|
|
19
19
|
border-bottom-left-radius: max(0px, calc(var(--ogx-grid-border-radius, var(--ogx-border-radius-lg)) - var(--ogx-grid-border-width)));
|
|
20
20
|
border-bottom-right-radius: max(0px, calc(var(--ogx-grid-border-radius, var(--ogx-border-radius-lg)) - var(--ogx-grid-border-width)));
|
|
21
21
|
}
|
|
22
|
-
.ogx:hover {
|
|
23
|
-
box-shadow: var(--ogx-shadow-md);
|
|
24
|
-
}
|
|
25
22
|
.ogx__viewport {
|
|
26
23
|
flex: 1;
|
|
27
24
|
overflow: auto;
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
.ogx-list-view__row:hover {
|
|
31
|
-
background: var(--ogx-
|
|
31
|
+
background: var(--ogx-grid-row-hover-background);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/* Keyboard focus (roving tab stop between list rows). */
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
.ogx-list-view__row--selected {
|
|
44
|
-
background: var(--ogx-selection-bg,
|
|
44
|
+
background: var(--ogx-selection-bg, var(--ogx-grid-row-selected-background));
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
.ogx-list-view__row--selected:hover {
|
|
48
|
-
background: var(--ogx-selection-hover-bg,
|
|
48
|
+
background: var(--ogx-selection-hover-bg, var(--ogx-grid-row-selected-hover-background));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/* ─── Checkbox column inside list row ────────────────────────────────────── */
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
min-width: 0;
|
|
80
80
|
font-family: var(--ogx-font-family);
|
|
81
81
|
font-size: var(--ogx-font-size-sm);
|
|
82
|
-
color: var(--ogx-grid-
|
|
82
|
+
color: var(--ogx-grid-row-text);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/* ─── Toolbar in list view mode ──────────────────────────────────────────── */
|
|
@@ -245,7 +245,9 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
245
245
|
getRowId = defaultGetRowId,
|
|
246
246
|
} = params;
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
// Rows and columns are measured in separate memos: a column resize must not walk every row
|
|
249
|
+
// (1M rows), and a sort or filter must not recompute column widths.
|
|
250
|
+
const rowLayout = useMemo(() => {
|
|
249
251
|
const unpinnedRows = pagination ? paginatedUnpinnedRows : sortedUnpinnedRows;
|
|
250
252
|
|
|
251
253
|
const heightOf = (row: R, rowIndex: number) => {
|
|
@@ -279,6 +281,35 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
279
281
|
const pinnedTopHeight = pinnedBlockHeight(pinnedTopRows, pinnedTopRowsLength);
|
|
280
282
|
const pinnedBottomHeight = pinnedBlockHeight(pinnedBottomRows, pinnedBottomRowsLength);
|
|
281
283
|
|
|
284
|
+
return {
|
|
285
|
+
rowHeights,
|
|
286
|
+
cumulativeHeights,
|
|
287
|
+
unpinnedRowsHeight,
|
|
288
|
+
pinnedTopHeight,
|
|
289
|
+
pinnedBottomHeight,
|
|
290
|
+
pinnedTopRowsLength,
|
|
291
|
+
pinnedBottomRowsLength,
|
|
292
|
+
unpinnedRowsLength: rowCount,
|
|
293
|
+
};
|
|
294
|
+
}, [
|
|
295
|
+
rowHeight,
|
|
296
|
+
pagination,
|
|
297
|
+
paginatedUnpinnedRows,
|
|
298
|
+
sortedUnpinnedRows,
|
|
299
|
+
expandedRowIds,
|
|
300
|
+
getDetailPanelHeight,
|
|
301
|
+
detailPanelHeights,
|
|
302
|
+
pinnedTopRowsLength,
|
|
303
|
+
pinnedBottomRowsLength,
|
|
304
|
+
pinnedTopRows,
|
|
305
|
+
pinnedBottomRows,
|
|
306
|
+
paginationMode,
|
|
307
|
+
isLoading,
|
|
308
|
+
pageSize,
|
|
309
|
+
getRowId,
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
const columnLayout = useMemo(() => {
|
|
282
313
|
// Pinned columns render in pinnedColumns.left / .right order; unpinned ones in column order.
|
|
283
314
|
const { left: rawLeftPinnedCols, center: unpinnedCols, right: rawRightPinnedCols } =
|
|
284
315
|
getPinnedColumnGroups(visibleOrderedColumns, pinnedColumns);
|
|
@@ -326,11 +357,6 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
326
357
|
}, [] as number[]);
|
|
327
358
|
|
|
328
359
|
return {
|
|
329
|
-
rowHeights,
|
|
330
|
-
cumulativeHeights,
|
|
331
|
-
unpinnedRowsHeight,
|
|
332
|
-
pinnedTopHeight,
|
|
333
|
-
pinnedBottomHeight,
|
|
334
360
|
leftPinnedCols,
|
|
335
361
|
rightPinnedCols,
|
|
336
362
|
unpinnedColsWithWidth,
|
|
@@ -342,22 +368,8 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
342
368
|
unpinnedTotalWidth,
|
|
343
369
|
systemColumnsWidth,
|
|
344
370
|
pinnedSystemColumnsWidth,
|
|
345
|
-
pinnedTopRowsLength,
|
|
346
|
-
pinnedBottomRowsLength,
|
|
347
|
-
unpinnedRowsLength: unpinnedRows.length,
|
|
348
371
|
};
|
|
349
372
|
}, [
|
|
350
|
-
rowHeight,
|
|
351
|
-
pagination,
|
|
352
|
-
paginatedUnpinnedRows,
|
|
353
|
-
sortedUnpinnedRows,
|
|
354
|
-
expandedRowIds,
|
|
355
|
-
getDetailPanelHeight,
|
|
356
|
-
detailPanelHeights,
|
|
357
|
-
pinnedTopRowsLength,
|
|
358
|
-
pinnedBottomRowsLength,
|
|
359
|
-
pinnedTopRows,
|
|
360
|
-
pinnedBottomRows,
|
|
361
373
|
visibleOrderedColumns,
|
|
362
374
|
pinnedColumns,
|
|
363
375
|
columnWidths,
|
|
@@ -367,9 +379,7 @@ export function useLayout<R extends GridRowModel>(params: UseLayoutParams<R>): L
|
|
|
367
379
|
rowReordering,
|
|
368
380
|
pinCheckboxColumn,
|
|
369
381
|
pinExpandColumn,
|
|
370
|
-
paginationMode,
|
|
371
|
-
isLoading,
|
|
372
|
-
pageSize,
|
|
373
|
-
getRowId,
|
|
374
382
|
]);
|
|
383
|
+
|
|
384
|
+
return useMemo<LayoutResult<R>>(() => ({ ...rowLayout, ...columnLayout }), [rowLayout, columnLayout]);
|
|
375
385
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/* Only the grid's own elements, including its portalled menus and panels (all carry an ogx class).
|
|
2
|
+
:where() keeps the specificity at zero, so a consumer's own box-sizing rule still wins. */
|
|
3
|
+
:where([class*="ogx"], [class*="ogx"] *),
|
|
4
|
+
:where([class*="ogx"], [class*="ogx"] *)::before,
|
|
5
|
+
:where([class*="ogx"], [class*="ogx"] *)::after {
|
|
4
6
|
box-sizing: border-box;
|
|
5
7
|
}
|
|
6
8
|
.ogx--dragging {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GridAggregationModel, GridColDef, GridGroupedExportRow, GridRowId, GridRowModel } from '../../types';
|
|
2
2
|
import { computeAggregations, formatAggregateForColumn } from '../aggregation';
|
|
3
|
-
import { formatValueByType } from '../values';
|
|
3
|
+
import { formatValueByType, getCellValue } from '../values';
|
|
4
4
|
import { isSystemField } from '../focus';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -77,10 +77,12 @@ export function aggregationForExport<R extends GridRowModel>(
|
|
|
77
77
|
return computeAggregations(exportedRows, aggregationModel, lookup, 'export');
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* The cell's raw value, read like the grid reads it (`getCellValue`): a `valueGetter` that throws
|
|
82
|
+
* exports as an empty value instead of aborting the export.
|
|
83
|
+
*/
|
|
81
84
|
export function getRawExportValue<R extends GridRowModel>(row: R, col: GridColDef<R>): unknown {
|
|
82
|
-
|
|
83
|
-
return col.valueGetter ? col.valueGetter({ row, field: col.field, value }) : value;
|
|
85
|
+
return getCellValue(row, col.field, col);
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
/**
|
|
@@ -159,6 +161,22 @@ export function neutralizeFormula(text: string, fromNonText = false): string {
|
|
|
159
161
|
return `'${text}`;
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Text that reads as a formatted number (digits, signs, separators, brackets, %, currency symbols),
|
|
166
|
+
* which no spreadsheet runs as a formula. One character class: linear on any input.
|
|
167
|
+
*/
|
|
168
|
+
const NUMERIC_DISPLAY = /^[\d\s.,'%+\-()\p{Sc}]*$/u;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Whether a data cell's text may skip formula escaping: the grid formatted a number, date or
|
|
172
|
+
* boolean itself, or a `valueFormatter` turned such a value into plain numeric text ("-$3").
|
|
173
|
+
* Anything else a formatter writes is escaped like text, since it may come from user data.
|
|
174
|
+
*/
|
|
175
|
+
export function isTrustedNonText(raw: unknown, text: string, hasFormatter: boolean): boolean {
|
|
176
|
+
if (!isNonTextValue(raw)) return false;
|
|
177
|
+
return !hasFormatter || NUMERIC_DISPLAY.test(text);
|
|
178
|
+
}
|
|
179
|
+
|
|
162
180
|
/** Whether a raw value is a number, bigint, boolean or Date, whose formatted text is not user-typed. */
|
|
163
181
|
export function isNonTextValue(value: unknown): boolean {
|
|
164
182
|
return typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean' || value instanceof Date;
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getRawExportValue,
|
|
11
11
|
hasSelection,
|
|
12
12
|
isNonTextValue,
|
|
13
|
+
isTrustedNonText,
|
|
13
14
|
neutralizeFormula,
|
|
14
15
|
pickExportedAggregates,
|
|
15
16
|
rowsForExport,
|
|
@@ -55,11 +56,14 @@ export interface CsvExportOptions<R extends GridValidRowModel = GridRowModel> {
|
|
|
55
56
|
interface ExportCell {
|
|
56
57
|
text: string;
|
|
57
58
|
raw: unknown;
|
|
59
|
+
/** The text came from the grid's own formatting of a number, date or boolean (see `isTrustedNonText`). */
|
|
60
|
+
fromNonText: boolean;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
function exportCell<R extends GridRowModel>(row: R, col: GridColDef<R>): ExportCell {
|
|
61
64
|
const raw = getRawExportValue(row, col);
|
|
62
|
-
|
|
65
|
+
const text = formatExportValue(row, col, raw);
|
|
66
|
+
return { text, raw, fromNonText: isTrustedNonText(raw, text, !!col.valueFormatter) };
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
/** Aggregate text per export column for one subtotal / total record. */
|
|
@@ -111,7 +115,7 @@ export function exportToCsv<R extends GridRowModel>(
|
|
|
111
115
|
const row = entry.row as R;
|
|
112
116
|
const cells = exportColumns.map((col, i) => {
|
|
113
117
|
const cell = exportCell(row, col);
|
|
114
|
-
const text = escapeFormulas ? neutralizeFormula(cell.text,
|
|
118
|
+
const text = escapeFormulas ? neutralizeFormula(cell.text, cell.fromNonText) : cell.text;
|
|
115
119
|
return escapeCSV((i === 0 ? indent : '') + text, delimiter);
|
|
116
120
|
});
|
|
117
121
|
csvContent += cells.join(delimiter) + '\n';
|
|
@@ -138,7 +142,7 @@ export function exportToCsv<R extends GridRowModel>(
|
|
|
138
142
|
rowsToExport.forEach(row => {
|
|
139
143
|
const values = exportColumns.map(col => {
|
|
140
144
|
const cell = exportCell(row, col);
|
|
141
|
-
return field(cell.text,
|
|
145
|
+
return field(cell.text, cell.fromNonText);
|
|
142
146
|
});
|
|
143
147
|
csvContent += values.join(delimiter) + '\n';
|
|
144
148
|
});
|
|
@@ -255,7 +259,7 @@ export function exportToExcel<R extends GridRowModel>(
|
|
|
255
259
|
html += '<tbody>';
|
|
256
260
|
|
|
257
261
|
const dataCell = (cell: ExportCell, col: GridColDef<R>, prefix = ''): string => {
|
|
258
|
-
const content = prefix + cellText(cell.text,
|
|
262
|
+
const content = prefix + cellText(cell.text, cell.fromNonText);
|
|
259
263
|
return isXlsTextCell(col, cell.raw) ? textTd(content) : `<td>${content}</td>`;
|
|
260
264
|
};
|
|
261
265
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import type { GridRowModel, GridFilterItem, GridFilterModel, GridFilterGroup, GridFilterOperator, GridColDef } from '../../types';
|
|
3
3
|
import type { GridColumnLookup } from '../columnLookup';
|
|
4
|
-
import { getCellValue, getFormattedValue, toLocalDateString, toLocalDayKey, toNumber } from '../values';
|
|
4
|
+
import { getCellValue, formatValueByType, getFormattedValue, toLocalDateString, toLocalDayKey, toNumber } from '../values';
|
|
5
5
|
|
|
6
6
|
/** Compares a cell value with the filter item's value. */
|
|
7
7
|
export type GridFilterOperatorFn = (value: unknown, filterValue?: unknown) => boolean;
|
|
@@ -230,7 +230,13 @@ function getQuickFilterTexts(row: GridRowModel, columns: GridColumnLookup | unde
|
|
|
230
230
|
for (const col of columns.quickFilterColumns) {
|
|
231
231
|
const value = getCellValue(row, col.field, col);
|
|
232
232
|
const formatted = getFormattedValue(row, col.field, value, col);
|
|
233
|
-
if (formatted != null && formatted !== '')
|
|
233
|
+
if (formatted != null && formatted !== '') {
|
|
234
|
+
texts.push(String(formatted).toLowerCase());
|
|
235
|
+
} else if (col.type === 'date' || col.type === 'boolean' || col.type === 'singleSelect') {
|
|
236
|
+
// Without a formatter the grid shows a locale date, Yes / No or the option label: search that too.
|
|
237
|
+
const shown = formatValueByType(value, col);
|
|
238
|
+
if (shown !== '') texts.push(shown.toLowerCase());
|
|
239
|
+
}
|
|
234
240
|
pushSearchText(texts, value);
|
|
235
241
|
}
|
|
236
242
|
return texts;
|
package/lib/utils/parsing.ts
CHANGED
|
@@ -131,6 +131,19 @@ export function parseNumberText(text: string): GridParsedValue {
|
|
|
131
131
|
|
|
132
132
|
const ISO_DATE_PREFIX = /^(\d{4}-\d{2}-\d{2})/;
|
|
133
133
|
const pad2 = (n: number) => String(n).padStart(2, '0');
|
|
134
|
+
/** Whether an ISO date-time string ends with `Z` or a `±hh:mm` / `±hhmm` offset. A loop, not a regex. */
|
|
135
|
+
function hasZone(text: string): boolean {
|
|
136
|
+
const end = text.trimEnd();
|
|
137
|
+
if (end.endsWith('Z') || end.endsWith('z')) return true;
|
|
138
|
+
const tIndex = end.indexOf('T');
|
|
139
|
+
if (tIndex < 0) return false;
|
|
140
|
+
for (let i = end.length - 1; i > tIndex; i--) {
|
|
141
|
+
const ch = end[i];
|
|
142
|
+
if (ch === '+' || ch === '-') return true;
|
|
143
|
+
if (!(ch >= '0' && ch <= '9') && ch !== ':') return false;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
134
147
|
const localDay = (date: Date) => `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
|
|
135
148
|
|
|
136
149
|
/**
|
|
@@ -147,7 +160,17 @@ export function parseDateText(text: string, current?: unknown): GridParsedValue
|
|
|
147
160
|
if (typeof current === 'number') return valid(date.getTime());
|
|
148
161
|
if (typeof current === 'string') {
|
|
149
162
|
const day = localDay(date);
|
|
150
|
-
|
|
163
|
+
if (!ISO_DATE_PREFIX.test(current) || current.length <= 10) return valid(day);
|
|
164
|
+
// A zoned date-time ("…Z", "…+05:30") is shown as a local day: keep its local time of day on
|
|
165
|
+
// the pasted day and write UTC, or west of UTC the cell would show the day before.
|
|
166
|
+
const held = hasZone(current) ? new Date(current) : null;
|
|
167
|
+
if (held && !Number.isNaN(held.getTime())) {
|
|
168
|
+
return valid(new Date(
|
|
169
|
+
date.getFullYear(), date.getMonth(), date.getDate(),
|
|
170
|
+
held.getHours(), held.getMinutes(), held.getSeconds(), held.getMilliseconds(),
|
|
171
|
+
).toISOString());
|
|
172
|
+
}
|
|
173
|
+
return valid(day + current.slice(10));
|
|
151
174
|
}
|
|
152
175
|
return valid(date);
|
|
153
176
|
}
|
package/lib/utils/values.ts
CHANGED
|
@@ -132,6 +132,7 @@ export function toDate(value: unknown): Date | null {
|
|
|
132
132
|
// Reject roll-overs such as 2024-02-31.
|
|
133
133
|
if (date.getMonth() !== month || date.getDate() !== day) return null;
|
|
134
134
|
} else {
|
|
135
|
+
if (!hasOnlyDateWords(trimmed)) return null;
|
|
135
136
|
date = new Date(trimmed);
|
|
136
137
|
}
|
|
137
138
|
} else {
|
|
@@ -140,6 +141,36 @@ export function toDate(value: unknown): Date | null {
|
|
|
140
141
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
const DATE_WORDS = [
|
|
145
|
+
'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october',
|
|
146
|
+
'november', 'december', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
|
|
147
|
+
];
|
|
148
|
+
const DATE_TOKENS = new Set(['t', 'z', 'am', 'pm', 'gmt', 'utc', 'sept']);
|
|
149
|
+
|
|
150
|
+
function isDateWord(word: string): boolean {
|
|
151
|
+
if (DATE_TOKENS.has(word)) return true;
|
|
152
|
+
return word.length >= 3 && DATE_WORDS.some(name => name.startsWith(word));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Whether every run of letters in `text` is a month or weekday name (or abbreviation), or one of
|
|
157
|
+
* T, Z, AM, PM, GMT, UTC. `Date.parse` accepts free text such as "hello 5" (as a 2001 date in V8),
|
|
158
|
+
* which must not become a date. A loop, not a regex.
|
|
159
|
+
*/
|
|
160
|
+
function hasOnlyDateWords(text: string): boolean {
|
|
161
|
+
let word = '';
|
|
162
|
+
for (let i = 0; i <= text.length; i++) {
|
|
163
|
+
const ch = i < text.length ? text[i].toLowerCase() : '';
|
|
164
|
+
if (ch >= 'a' && ch <= 'z') {
|
|
165
|
+
word += ch;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (word !== '' && !isDateWord(word)) return false;
|
|
169
|
+
word = '';
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
143
174
|
/** A sortable integer for the local calendar day of a date (YYYYMMDD), or null when unparsable. */
|
|
144
175
|
export function toLocalDayKey(value: unknown): number | null {
|
|
145
176
|
const date = toDate(value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencorestack/opengridx",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.1",
|
|
4
4
|
"description": "OpenGridX: High-performance React data infrastructure. Unlock advanced Row Grouping, Excel Export, and Column Pinning without the usual \"Pro\" gatekeeping. Built for speed, scale, and complete architectural freedom. Fully open, virtualization-ready, and feature-complete.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/opengridx.umd.js",
|