@svgrid/grid 1.0.2 → 1.1.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/LICENSE +1 -1
- package/README.md +137 -39
- package/dist/GridFooter.svelte +164 -0
- package/dist/GridFooter.svelte.d.ts +29 -0
- package/dist/GridMenus.svelte +648 -0
- package/dist/GridMenus.svelte.d.ts +31 -0
- package/dist/SvGrid.controller.svelte.d.ts +472 -0
- package/dist/SvGrid.controller.svelte.js +1917 -0
- package/dist/SvGrid.css +2012 -0
- package/dist/SvGrid.helpers.d.ts +35 -0
- package/dist/SvGrid.helpers.js +160 -0
- package/dist/SvGrid.helpers.test.d.ts +1 -0
- package/dist/SvGrid.helpers.test.js +298 -0
- package/dist/SvGrid.svelte +2346 -8742
- package/dist/SvGrid.svelte.d.ts +4 -357
- package/dist/SvGrid.types.d.ts +523 -0
- package/dist/SvGrid.types.js +1 -0
- package/dist/SvGridChart.svelte +1060 -23
- package/dist/SvGridChart.svelte.d.ts +17 -0
- package/dist/aligned-grids.d.ts +6 -0
- package/dist/aligned-grids.js +84 -0
- package/dist/aligned-grids.test.d.ts +1 -0
- package/dist/aligned-grids.test.js +75 -0
- package/dist/build-api.coverage.test.d.ts +20 -0
- package/dist/build-api.coverage.test.js +505 -0
- package/dist/build-api.d.ts +5 -0
- package/dist/build-api.js +557 -0
- package/dist/cell-render.d.ts +15 -0
- package/dist/cell-render.js +246 -0
- package/dist/cell-render.test.d.ts +1 -0
- package/dist/cell-render.test.js +338 -0
- package/dist/cell-values.d.ts +28 -0
- package/dist/cell-values.js +89 -0
- package/dist/chart-export.test.d.ts +1 -0
- package/dist/chart-export.test.js +302 -0
- package/dist/chart.coverage.test.d.ts +1 -0
- package/dist/chart.coverage.test.js +748 -0
- package/dist/chart.d.ts +370 -3
- package/dist/chart.js +1135 -42
- package/dist/clipboard.d.ts +15 -0
- package/dist/clipboard.js +420 -0
- package/dist/clipboard.test.d.ts +1 -0
- package/dist/clipboard.test.js +700 -0
- package/dist/collaboration.coverage.test.d.ts +1 -0
- package/dist/collaboration.coverage.test.js +200 -0
- package/dist/column-groups.d.ts +19 -0
- package/dist/column-groups.js +62 -0
- package/dist/column-groups.test.d.ts +1 -0
- package/dist/column-groups.test.js +56 -0
- package/dist/column-types.d.ts +10 -0
- package/dist/column-types.js +63 -0
- package/dist/column-types.test.d.ts +1 -0
- package/dist/column-types.test.js +62 -0
- package/dist/columns.d.ts +30 -0
- package/dist/columns.js +277 -0
- package/dist/columns.test.d.ts +1 -0
- package/dist/columns.test.js +625 -0
- package/dist/core.d.ts +86 -1
- package/dist/css.d.ts +3 -0
- package/dist/editing.d.ts +31 -0
- package/dist/editing.js +529 -0
- package/dist/editing.test.d.ts +1 -0
- package/dist/editing.test.js +732 -0
- package/dist/editors/cell-editors.coverage.test.d.ts +1 -0
- package/dist/editors/cell-editors.coverage.test.js +139 -0
- package/dist/editors/cell-editors.d.ts +1 -1
- package/dist/facet-buckets.d.ts +13 -0
- package/dist/facet-buckets.js +54 -0
- package/dist/facet-buckets.test.d.ts +1 -0
- package/dist/facet-buckets.test.js +296 -0
- package/dist/features.d.ts +5 -0
- package/dist/features.js +30 -0
- package/dist/filter-operators.d.ts +16 -0
- package/dist/filter-operators.js +69 -0
- package/dist/filter-operators.test.d.ts +1 -0
- package/dist/filter-operators.test.js +135 -0
- package/dist/hyperformula-adapter.d.ts +82 -0
- package/dist/hyperformula-adapter.js +73 -0
- package/dist/hyperformula-adapter.test.d.ts +1 -0
- package/dist/hyperformula-adapter.test.js +205 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +8 -2
- package/dist/keyboard-handlers.coverage.test.d.ts +1 -0
- package/dist/keyboard-handlers.coverage.test.js +495 -0
- package/dist/keyboard-handlers.d.ts +7 -0
- package/dist/keyboard-handlers.js +202 -0
- package/dist/menus.d.ts +40 -0
- package/dist/menus.js +390 -0
- package/dist/menus.test.d.ts +1 -0
- package/dist/menus.test.js +560 -0
- package/dist/named-views.coverage.test.d.ts +1 -0
- package/dist/named-views.coverage.test.js +180 -0
- package/dist/named-views.d.ts +27 -0
- package/dist/named-views.js +39 -0
- package/dist/row-drag.d.ts +49 -0
- package/dist/row-drag.js +221 -0
- package/dist/row-drag.test.d.ts +1 -0
- package/dist/row-drag.test.js +142 -0
- package/dist/row-resize.d.ts +43 -0
- package/dist/row-resize.js +158 -0
- package/dist/row-resize.test.d.ts +1 -0
- package/dist/row-resize.test.js +329 -0
- package/dist/scroll-sync.d.ts +9 -0
- package/dist/scroll-sync.js +89 -0
- package/dist/scroll-sync.test.d.ts +1 -0
- package/dist/scroll-sync.test.js +290 -0
- package/dist/selection.d.ts +32 -0
- package/dist/selection.js +427 -0
- package/dist/selection.multi-range.test.d.ts +1 -0
- package/dist/selection.multi-range.test.js +55 -0
- package/dist/selection.test.d.ts +1 -0
- package/dist/selection.test.js +647 -0
- package/dist/server-data-source.coverage.test.d.ts +1 -0
- package/dist/server-data-source.coverage.test.js +154 -0
- package/dist/spreadsheet.d.ts +110 -0
- package/dist/spreadsheet.js +242 -0
- package/dist/spreadsheet.test.d.ts +1 -0
- package/dist/spreadsheet.test.js +446 -0
- package/dist/summaries.d.ts +12 -0
- package/dist/summaries.js +65 -0
- package/dist/sv-grid-scrollbar.js +13 -1
- package/dist/svgrid-wrapper.types.d.ts +31 -1
- package/dist/svgrid.behavior.test.js +20 -0
- package/dist/svgrid.comments-autocomplete.test.d.ts +1 -0
- package/dist/svgrid.comments-autocomplete.test.js +96 -0
- package/dist/svgrid.context-menu.test.d.ts +1 -0
- package/dist/svgrid.context-menu.test.js +102 -0
- package/dist/svgrid.interaction.test.js +31 -0
- package/dist/svgrid.new-features.wrapper.test.js +64 -6
- package/dist/svgrid.wrapper.test.js +27 -1
- package/dist/test-setup.js +9 -3
- package/dist/virtualization/column-virtualizer.d.ts +2 -0
- package/dist/virtualization/scroll-scaling.d.ts +45 -0
- package/dist/virtualization/scroll-scaling.js +99 -0
- package/dist/virtualization/scroll-scaling.test.d.ts +1 -0
- package/dist/virtualization/scroll-scaling.test.js +127 -0
- package/dist/virtualization/svelte-virtualizer.svelte.d.ts +2 -0
- package/dist/virtualization/svelte-virtualizer.svelte.js +2 -0
- package/dist/virtualization/virtualizer.d.ts +7 -0
- package/dist/virtualization/virtualizer.js +30 -0
- package/package.json +2 -1
- package/src/GridFooter.svelte +164 -0
- package/src/GridMenus.svelte +648 -0
- package/src/SvGrid.controller.svelte.ts +2352 -0
- package/src/SvGrid.css +2012 -0
- package/src/SvGrid.helpers.test.ts +415 -0
- package/src/SvGrid.helpers.ts +185 -0
- package/src/SvGrid.svelte +2346 -8742
- package/src/SvGrid.types.ts +537 -0
- package/src/SvGridChart.svelte +1060 -23
- package/src/aligned-grids.test.ts +80 -0
- package/src/aligned-grids.ts +87 -0
- package/src/build-api.coverage.test.ts +532 -0
- package/src/build-api.ts +683 -0
- package/src/cell-render.test.ts +451 -0
- package/src/cell-render.ts +426 -0
- package/src/cell-values.ts +114 -0
- package/src/chart-export.test.ts +370 -0
- package/src/chart.coverage.test.ts +814 -0
- package/src/chart.ts +1352 -47
- package/src/clipboard.test.ts +780 -0
- package/src/clipboard.ts +552 -0
- package/src/collaboration.coverage.test.ts +220 -0
- package/src/column-groups.test.ts +59 -0
- package/src/column-groups.ts +80 -0
- package/src/column-types.test.ts +68 -0
- package/src/column-types.ts +82 -0
- package/src/columns.test.ts +702 -0
- package/src/columns.ts +419 -0
- package/src/core.ts +86 -0
- package/src/css.d.ts +3 -0
- package/src/editing.test.ts +837 -0
- package/src/editing.ts +669 -0
- package/src/editors/cell-editors.coverage.test.ts +156 -0
- package/src/editors/cell-editors.ts +1 -0
- package/src/facet-buckets.test.ts +353 -0
- package/src/facet-buckets.ts +67 -0
- package/src/features.ts +128 -0
- package/src/filter-operators.test.ts +174 -0
- package/src/filter-operators.ts +87 -0
- package/src/hyperformula-adapter.test.ts +256 -0
- package/src/hyperformula-adapter.ts +124 -0
- package/src/index.ts +49 -0
- package/src/keyboard-handlers.coverage.test.ts +560 -0
- package/src/keyboard-handlers.ts +353 -0
- package/src/keyboard.ts +97 -97
- package/src/menus.test.ts +620 -0
- package/src/menus.ts +555 -0
- package/src/named-views.coverage.test.ts +210 -0
- package/src/named-views.ts +48 -0
- package/src/row-drag.test.ts +168 -0
- package/src/row-drag.ts +255 -0
- package/src/row-resize.test.ts +369 -0
- package/src/row-resize.ts +171 -0
- package/src/scroll-sync.test.ts +330 -0
- package/src/scroll-sync.ts +218 -0
- package/src/selection.multi-range.test.ts +61 -0
- package/src/selection.test.ts +722 -0
- package/src/selection.ts +579 -0
- package/src/server-data-source.coverage.test.ts +180 -0
- package/src/spreadsheet.test.ts +489 -0
- package/src/spreadsheet.ts +304 -0
- package/src/summaries.ts +204 -0
- package/src/sv-grid-scrollbar.ts +13 -1
- package/src/svgrid-wrapper.types.ts +31 -1
- package/src/svgrid.behavior.test.ts +22 -0
- package/src/svgrid.comments-autocomplete.test.ts +112 -0
- package/src/svgrid.context-menu.test.ts +126 -0
- package/src/svgrid.interaction.test.ts +30 -0
- package/src/svgrid.new-features.wrapper.test.ts +67 -6
- package/src/svgrid.wrapper.test.ts +27 -1
- package/src/test-setup.ts +9 -6
- package/src/virtualization/scroll-scaling.test.ts +148 -0
- package/src/virtualization/scroll-scaling.ts +121 -0
- package/src/virtualization/svelte-virtualizer.svelte.ts +2 -0
- package/src/virtualization/virtualizer.ts +26 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CellEditorType, CellEditorOption } from "./index";
|
|
2
|
+
import type { ResolvedCellFormat } from "./conditional-formatting";
|
|
3
|
+
export declare function cfTextStyle(cf: ResolvedCellFormat): string;
|
|
4
|
+
export declare function fmtStat(n: number): string;
|
|
5
|
+
export declare function getCellKey(rowId: string, columnId: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Normalise the return value of a `cellClass` / `rowClass` callback
|
|
8
|
+
* (or a static `cellClass` field) into a single space-separated
|
|
9
|
+
* class string. Accepts: string, string[], Record<string, boolean>,
|
|
10
|
+
* null/undefined. Anything else returns ''.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveClassList(value: string | ReadonlyArray<string> | Record<string, boolean> | undefined | null): string;
|
|
13
|
+
export declare function toDateInputValue(value: unknown): string;
|
|
14
|
+
export declare function toDateTimeLocalInputValue(value: unknown): string;
|
|
15
|
+
export declare function getEditableInputValue(editorType: CellEditorType, value: unknown): string;
|
|
16
|
+
export declare function getEditorInputType(editorType: CellEditorType): "number" | "date" | "text" | "time" | "password" | "color" | "datetime-local";
|
|
17
|
+
/** Resolve a stored value to an array form (for list/chips multi-select). */
|
|
18
|
+
export declare function toValueArray(value: unknown): Array<string | number>;
|
|
19
|
+
/** Look up the display label for a given option value. */
|
|
20
|
+
export declare function getOptionLabel(options: CellEditorOption[], value: unknown): string;
|
|
21
|
+
/** Look up the configured `color` for a value (for colorful chips). */
|
|
22
|
+
export declare function getOptionColor(options: CellEditorOption[], value: unknown): string | undefined;
|
|
23
|
+
/** Build a theme-aware inline `style` string for a colorful chip. The
|
|
24
|
+
* color value can be any CSS color (hex, rgb, hsl, oklch, named).
|
|
25
|
+
* We tint the background and border via `color-mix` so the chip stays
|
|
26
|
+
* readable on light AND dark themes, and use the color itself as the
|
|
27
|
+
* text color for a soft-pill look (à la GitHub labels). */
|
|
28
|
+
export declare function colorfulChipStyle(color: string | undefined): string;
|
|
29
|
+
export declare function getEditorClass(editorType: CellEditorType): "sv-grid-cell-editor sv-grid-cell-editor-number" | "sv-grid-cell-editor sv-grid-cell-editor-date" | "sv-grid-cell-editor sv-grid-cell-editor-datetime" | "sv-grid-cell-editor sv-grid-cell-editor-color" | "sv-grid-cell-editor";
|
|
30
|
+
export declare function asDate(value: unknown): Date | null;
|
|
31
|
+
export declare function clampMenuX(x: number, width: number): number;
|
|
32
|
+
export declare function cssEscape(s: string): string;
|
|
33
|
+
export declare function rawToNumber(raw: unknown, isDate: boolean): number;
|
|
34
|
+
export declare function formatFacetNumber(n: number): string;
|
|
35
|
+
export declare function formatFacetDate(timestamp: number): string;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
export function cfTextStyle(cf) {
|
|
2
|
+
let s = "";
|
|
3
|
+
if (cf.color)
|
|
4
|
+
s += `color:${cf.color};`;
|
|
5
|
+
if (cf.fontWeight != null)
|
|
6
|
+
s += `font-weight:${cf.fontWeight};`;
|
|
7
|
+
return s;
|
|
8
|
+
}
|
|
9
|
+
export function fmtStat(n) {
|
|
10
|
+
return Number.isInteger(n)
|
|
11
|
+
? n.toLocaleString()
|
|
12
|
+
: n.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
|
13
|
+
}
|
|
14
|
+
export function getCellKey(rowId, columnId) {
|
|
15
|
+
return `${rowId}:${columnId}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Normalise the return value of a `cellClass` / `rowClass` callback
|
|
19
|
+
* (or a static `cellClass` field) into a single space-separated
|
|
20
|
+
* class string. Accepts: string, string[], Record<string, boolean>,
|
|
21
|
+
* null/undefined. Anything else returns ''.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveClassList(value) {
|
|
24
|
+
if (!value)
|
|
25
|
+
return "";
|
|
26
|
+
if (typeof value === "string")
|
|
27
|
+
return value;
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
return value.filter(Boolean).join(" ");
|
|
30
|
+
if (typeof value === "object") {
|
|
31
|
+
const parts = [];
|
|
32
|
+
for (const [k, v] of Object.entries(value))
|
|
33
|
+
if (v)
|
|
34
|
+
parts.push(k);
|
|
35
|
+
return parts.join(" ");
|
|
36
|
+
}
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
export function toDateInputValue(value) {
|
|
40
|
+
if (value == null || value === "")
|
|
41
|
+
return "";
|
|
42
|
+
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
43
|
+
return value;
|
|
44
|
+
const parsed = value instanceof Date ? value : new Date(value);
|
|
45
|
+
if (Number.isNaN(parsed.getTime()))
|
|
46
|
+
return "";
|
|
47
|
+
return parsed.toISOString().slice(0, 10);
|
|
48
|
+
}
|
|
49
|
+
export function toDateTimeLocalInputValue(value) {
|
|
50
|
+
if (value == null || value === "")
|
|
51
|
+
return "";
|
|
52
|
+
if (typeof value === "string" &&
|
|
53
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value))
|
|
54
|
+
return value;
|
|
55
|
+
const parsed = value instanceof Date ? value : new Date(value);
|
|
56
|
+
if (Number.isNaN(parsed.getTime()))
|
|
57
|
+
return "";
|
|
58
|
+
return parsed.toISOString().slice(0, 16);
|
|
59
|
+
}
|
|
60
|
+
export function getEditableInputValue(editorType, value) {
|
|
61
|
+
if (editorType === "date")
|
|
62
|
+
return toDateInputValue(value);
|
|
63
|
+
if (editorType === "datetime")
|
|
64
|
+
return toDateTimeLocalInputValue(value);
|
|
65
|
+
return String(value ?? "");
|
|
66
|
+
}
|
|
67
|
+
export function getEditorInputType(editorType) {
|
|
68
|
+
if (editorType === "number")
|
|
69
|
+
return "number";
|
|
70
|
+
if (editorType === "date")
|
|
71
|
+
return "date";
|
|
72
|
+
if (editorType === "datetime")
|
|
73
|
+
return "datetime-local";
|
|
74
|
+
if (editorType === "time")
|
|
75
|
+
return "time";
|
|
76
|
+
if (editorType === "password")
|
|
77
|
+
return "password";
|
|
78
|
+
if (editorType === "color")
|
|
79
|
+
return "color";
|
|
80
|
+
return "text";
|
|
81
|
+
}
|
|
82
|
+
/** Resolve a stored value to an array form (for list/chips multi-select). */
|
|
83
|
+
export function toValueArray(value) {
|
|
84
|
+
if (value == null || value === "")
|
|
85
|
+
return [];
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
return value.filter((v) => v != null && v !== "");
|
|
88
|
+
}
|
|
89
|
+
return [value];
|
|
90
|
+
}
|
|
91
|
+
/** Look up the display label for a given option value. */
|
|
92
|
+
export function getOptionLabel(options, value) {
|
|
93
|
+
const match = options.find((o) => o.value === value || String(o.value) === String(value));
|
|
94
|
+
return match ? match.label : String(value ?? "");
|
|
95
|
+
}
|
|
96
|
+
/** Look up the configured `color` for a value (for colorful chips). */
|
|
97
|
+
export function getOptionColor(options, value) {
|
|
98
|
+
const match = options.find((o) => o.value === value || String(o.value) === String(value));
|
|
99
|
+
return match?.color;
|
|
100
|
+
}
|
|
101
|
+
/** Build a theme-aware inline `style` string for a colorful chip. The
|
|
102
|
+
* color value can be any CSS color (hex, rgb, hsl, oklch, named).
|
|
103
|
+
* We tint the background and border via `color-mix` so the chip stays
|
|
104
|
+
* readable on light AND dark themes, and use the color itself as the
|
|
105
|
+
* text color for a soft-pill look (à la GitHub labels). */
|
|
106
|
+
export function colorfulChipStyle(color) {
|
|
107
|
+
if (!color)
|
|
108
|
+
return "";
|
|
109
|
+
return (`background: color-mix(in srgb, ${color} 22%, transparent);` +
|
|
110
|
+
`border-color: color-mix(in srgb, ${color} 45%, transparent);` +
|
|
111
|
+
`color: color-mix(in srgb, ${color} 80%, var(--sg-fg, #0f172a));`);
|
|
112
|
+
}
|
|
113
|
+
export function getEditorClass(editorType) {
|
|
114
|
+
if (editorType === "number")
|
|
115
|
+
return "sv-grid-cell-editor sv-grid-cell-editor-number";
|
|
116
|
+
if (editorType === "date")
|
|
117
|
+
return "sv-grid-cell-editor sv-grid-cell-editor-date";
|
|
118
|
+
if (editorType === "datetime")
|
|
119
|
+
return "sv-grid-cell-editor sv-grid-cell-editor-datetime";
|
|
120
|
+
if (editorType === "color")
|
|
121
|
+
return "sv-grid-cell-editor sv-grid-cell-editor-color";
|
|
122
|
+
return "sv-grid-cell-editor";
|
|
123
|
+
}
|
|
124
|
+
export function asDate(value) {
|
|
125
|
+
if (value == null || value === "")
|
|
126
|
+
return null;
|
|
127
|
+
const parsed = value instanceof Date ? value : new Date(value);
|
|
128
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
129
|
+
}
|
|
130
|
+
export function clampMenuX(x, width) {
|
|
131
|
+
return Math.max(8, Math.min(x, window.innerWidth - width - 8));
|
|
132
|
+
}
|
|
133
|
+
export function cssEscape(s) {
|
|
134
|
+
return typeof CSS !== "undefined" && CSS.escape ? CSS.escape(s) : s.replace(/"/g, '\\"');
|
|
135
|
+
}
|
|
136
|
+
export function rawToNumber(raw, isDate) {
|
|
137
|
+
if (raw == null || raw === "")
|
|
138
|
+
return Number.NaN;
|
|
139
|
+
if (isDate) {
|
|
140
|
+
const d = raw instanceof Date ? raw : new Date(String(raw));
|
|
141
|
+
const t = d.getTime();
|
|
142
|
+
return Number.isFinite(t) ? t : Number.NaN;
|
|
143
|
+
}
|
|
144
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
145
|
+
return Number.isFinite(n) ? n : Number.NaN;
|
|
146
|
+
}
|
|
147
|
+
export function formatFacetNumber(n) {
|
|
148
|
+
const abs = Math.abs(n);
|
|
149
|
+
const maxFractionDigits = abs >= 1000 ? 0 : abs >= 100 ? 1 : abs >= 1 ? 2 : 4;
|
|
150
|
+
return n.toLocaleString(undefined, {
|
|
151
|
+
maximumFractionDigits: maxFractionDigits,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
export function formatFacetDate(timestamp) {
|
|
155
|
+
return new Date(timestamp).toLocaleDateString(undefined, {
|
|
156
|
+
year: "numeric",
|
|
157
|
+
month: "short",
|
|
158
|
+
day: "numeric",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { cfTextStyle, fmtStat, getCellKey, resolveClassList, toDateInputValue, toDateTimeLocalInputValue, getEditableInputValue, getEditorInputType, toValueArray, getOptionLabel, getOptionColor, colorfulChipStyle, getEditorClass, asDate, clampMenuX, cssEscape, rawToNumber, formatFacetNumber, formatFacetDate, } from './SvGrid.helpers';
|
|
3
|
+
describe('cfTextStyle', () => {
|
|
4
|
+
it('returns empty string when no color or fontWeight', () => {
|
|
5
|
+
expect(cfTextStyle({})).toBe('');
|
|
6
|
+
});
|
|
7
|
+
it('emits a color declaration', () => {
|
|
8
|
+
expect(cfTextStyle({ color: 'red' })).toBe('color:red;');
|
|
9
|
+
});
|
|
10
|
+
it('emits a font-weight declaration including 0 (not null)', () => {
|
|
11
|
+
expect(cfTextStyle({ fontWeight: 700 })).toBe('font-weight:700;');
|
|
12
|
+
});
|
|
13
|
+
it('combines color and fontWeight', () => {
|
|
14
|
+
expect(cfTextStyle({ color: '#fff', fontWeight: 'bold' })).toBe('color:#fff;font-weight:bold;');
|
|
15
|
+
});
|
|
16
|
+
it('omits font-weight when it is null', () => {
|
|
17
|
+
expect(cfTextStyle({ color: 'blue', fontWeight: null })).toBe('color:blue;');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('fmtStat', () => {
|
|
21
|
+
it('formats integers with locale grouping', () => {
|
|
22
|
+
expect(fmtStat(1000)).toBe((1000).toLocaleString());
|
|
23
|
+
});
|
|
24
|
+
it('rounds floats to at most 2 fraction digits', () => {
|
|
25
|
+
expect(fmtStat(1.23456)).toBe((1.23456).toLocaleString(undefined, { maximumFractionDigits: 2 }));
|
|
26
|
+
});
|
|
27
|
+
it('treats whole-number floats as integers', () => {
|
|
28
|
+
expect(fmtStat(5.0)).toBe((5).toLocaleString());
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('getCellKey', () => {
|
|
32
|
+
it('joins rowId and columnId with a colon', () => {
|
|
33
|
+
expect(getCellKey('r1', 'colA')).toBe('r1:colA');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('resolveClassList', () => {
|
|
37
|
+
it('returns empty for null/undefined/empty', () => {
|
|
38
|
+
expect(resolveClassList(null)).toBe('');
|
|
39
|
+
expect(resolveClassList(undefined)).toBe('');
|
|
40
|
+
expect(resolveClassList('')).toBe('');
|
|
41
|
+
});
|
|
42
|
+
it('passes a plain string through unchanged', () => {
|
|
43
|
+
expect(resolveClassList('a b')).toBe('a b');
|
|
44
|
+
});
|
|
45
|
+
it('joins arrays and drops falsy entries', () => {
|
|
46
|
+
expect(resolveClassList(['a', '', 'b'])).toBe('a b');
|
|
47
|
+
});
|
|
48
|
+
it('emits only truthy keys of a record', () => {
|
|
49
|
+
expect(resolveClassList({ a: true, b: false, c: true })).toBe('a c');
|
|
50
|
+
});
|
|
51
|
+
it('returns empty string for an all-false record', () => {
|
|
52
|
+
expect(resolveClassList({ a: false })).toBe('');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('toDateInputValue', () => {
|
|
56
|
+
it('returns empty for null/undefined/empty', () => {
|
|
57
|
+
expect(toDateInputValue(null)).toBe('');
|
|
58
|
+
expect(toDateInputValue(undefined)).toBe('');
|
|
59
|
+
expect(toDateInputValue('')).toBe('');
|
|
60
|
+
});
|
|
61
|
+
it('passes an already-ISO date string through', () => {
|
|
62
|
+
expect(toDateInputValue('2024-01-15')).toBe('2024-01-15');
|
|
63
|
+
});
|
|
64
|
+
it('formats a Date instance to yyyy-mm-dd', () => {
|
|
65
|
+
expect(toDateInputValue(new Date(Date.UTC(2024, 0, 15)))).toBe('2024-01-15');
|
|
66
|
+
});
|
|
67
|
+
it('parses a numeric timestamp', () => {
|
|
68
|
+
const ts = Date.UTC(2020, 5, 1);
|
|
69
|
+
expect(toDateInputValue(ts)).toBe('2020-06-01');
|
|
70
|
+
});
|
|
71
|
+
it('returns empty for an unparseable value', () => {
|
|
72
|
+
expect(toDateInputValue('not-a-date')).toBe('');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('toDateTimeLocalInputValue', () => {
|
|
76
|
+
it('returns empty for null/empty', () => {
|
|
77
|
+
expect(toDateTimeLocalInputValue(null)).toBe('');
|
|
78
|
+
expect(toDateTimeLocalInputValue('')).toBe('');
|
|
79
|
+
});
|
|
80
|
+
it('passes a matching datetime-local string through', () => {
|
|
81
|
+
expect(toDateTimeLocalInputValue('2024-01-15T13:45')).toBe('2024-01-15T13:45');
|
|
82
|
+
});
|
|
83
|
+
it('formats a Date instance to yyyy-mm-ddThh:mm', () => {
|
|
84
|
+
expect(toDateTimeLocalInputValue(new Date(Date.UTC(2024, 0, 15, 8, 30)))).toBe('2024-01-15T08:30');
|
|
85
|
+
});
|
|
86
|
+
it('returns empty for an invalid value', () => {
|
|
87
|
+
expect(toDateTimeLocalInputValue('garbage')).toBe('');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('getEditableInputValue', () => {
|
|
91
|
+
it('delegates to the date formatter for date editors', () => {
|
|
92
|
+
expect(getEditableInputValue('date', '2024-01-15')).toBe('2024-01-15');
|
|
93
|
+
});
|
|
94
|
+
it('delegates to the datetime formatter for datetime editors', () => {
|
|
95
|
+
expect(getEditableInputValue('datetime', '2024-01-15T10:00')).toBe('2024-01-15T10:00');
|
|
96
|
+
});
|
|
97
|
+
it('stringifies other editor types', () => {
|
|
98
|
+
expect(getEditableInputValue('text', 42)).toBe('42');
|
|
99
|
+
});
|
|
100
|
+
it('returns empty string for nullish values', () => {
|
|
101
|
+
expect(getEditableInputValue('text', null)).toBe('');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('getEditorInputType', () => {
|
|
105
|
+
it('maps each known editor type to its input type', () => {
|
|
106
|
+
expect(getEditorInputType('number')).toBe('number');
|
|
107
|
+
expect(getEditorInputType('date')).toBe('date');
|
|
108
|
+
expect(getEditorInputType('datetime')).toBe('datetime-local');
|
|
109
|
+
expect(getEditorInputType('time')).toBe('time');
|
|
110
|
+
expect(getEditorInputType('password')).toBe('password');
|
|
111
|
+
expect(getEditorInputType('color')).toBe('color');
|
|
112
|
+
});
|
|
113
|
+
it('falls back to text for unknown editor types', () => {
|
|
114
|
+
expect(getEditorInputType('select')).toBe('text');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe('toValueArray', () => {
|
|
118
|
+
it('returns empty for null/empty', () => {
|
|
119
|
+
expect(toValueArray(null)).toEqual([]);
|
|
120
|
+
expect(toValueArray('')).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
it('filters nullish/empty entries from an array', () => {
|
|
123
|
+
expect(toValueArray([1, null, '', 2, undefined])).toEqual([1, 2]);
|
|
124
|
+
});
|
|
125
|
+
it('wraps a scalar in an array', () => {
|
|
126
|
+
expect(toValueArray('x')).toEqual(['x']);
|
|
127
|
+
expect(toValueArray(7)).toEqual([7]);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('getOptionLabel', () => {
|
|
131
|
+
const options = [
|
|
132
|
+
{ value: 1, label: 'One' },
|
|
133
|
+
{ value: 'two', label: 'Two' },
|
|
134
|
+
];
|
|
135
|
+
it('finds the label by strict value match', () => {
|
|
136
|
+
expect(getOptionLabel(options, 1)).toBe('One');
|
|
137
|
+
});
|
|
138
|
+
it('finds the label by stringified value match', () => {
|
|
139
|
+
expect(getOptionLabel(options, '1')).toBe('One');
|
|
140
|
+
});
|
|
141
|
+
it('falls back to the stringified value when no option matches', () => {
|
|
142
|
+
expect(getOptionLabel(options, 99)).toBe('99');
|
|
143
|
+
});
|
|
144
|
+
it('returns empty string for a nullish unmatched value', () => {
|
|
145
|
+
expect(getOptionLabel(options, null)).toBe('');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe('getOptionColor', () => {
|
|
149
|
+
const options = [
|
|
150
|
+
{ value: 'a', label: 'A', color: '#f00' },
|
|
151
|
+
{ value: 'b', label: 'B' },
|
|
152
|
+
];
|
|
153
|
+
it('returns the configured color', () => {
|
|
154
|
+
expect(getOptionColor(options, 'a')).toBe('#f00');
|
|
155
|
+
});
|
|
156
|
+
it('returns undefined when matched option has no color', () => {
|
|
157
|
+
expect(getOptionColor(options, 'b')).toBeUndefined();
|
|
158
|
+
});
|
|
159
|
+
it('returns undefined when no option matches', () => {
|
|
160
|
+
expect(getOptionColor(options, 'z')).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
it('matches by stringified value', () => {
|
|
163
|
+
const opts = [{ value: 5, label: 'Five', color: 'green' }];
|
|
164
|
+
expect(getOptionColor(opts, '5')).toBe('green');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe('colorfulChipStyle', () => {
|
|
168
|
+
it('returns empty for no color', () => {
|
|
169
|
+
expect(colorfulChipStyle(undefined)).toBe('');
|
|
170
|
+
});
|
|
171
|
+
it('builds a color-mix based style with the given color', () => {
|
|
172
|
+
const style = colorfulChipStyle('#abc');
|
|
173
|
+
expect(style).toContain('background: color-mix(in srgb, #abc 22%, transparent);');
|
|
174
|
+
expect(style).toContain('border-color: color-mix(in srgb, #abc 45%, transparent);');
|
|
175
|
+
expect(style).toContain('color: color-mix(in srgb, #abc 80%, var(--sg-fg, #0f172a));');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe('getEditorClass', () => {
|
|
179
|
+
it('returns the modifier class for each editor type', () => {
|
|
180
|
+
expect(getEditorClass('number')).toBe('sv-grid-cell-editor sv-grid-cell-editor-number');
|
|
181
|
+
expect(getEditorClass('date')).toBe('sv-grid-cell-editor sv-grid-cell-editor-date');
|
|
182
|
+
expect(getEditorClass('datetime')).toBe('sv-grid-cell-editor sv-grid-cell-editor-datetime');
|
|
183
|
+
expect(getEditorClass('color')).toBe('sv-grid-cell-editor sv-grid-cell-editor-color');
|
|
184
|
+
});
|
|
185
|
+
it('returns the base class for other editor types', () => {
|
|
186
|
+
expect(getEditorClass('text')).toBe('sv-grid-cell-editor');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
describe('asDate', () => {
|
|
190
|
+
it('returns null for null/empty', () => {
|
|
191
|
+
expect(asDate(null)).toBeNull();
|
|
192
|
+
expect(asDate('')).toBeNull();
|
|
193
|
+
});
|
|
194
|
+
it('returns the same Date instance for a Date', () => {
|
|
195
|
+
const d = new Date(2024, 0, 1);
|
|
196
|
+
expect(asDate(d)).toBe(d);
|
|
197
|
+
});
|
|
198
|
+
it('parses a date string', () => {
|
|
199
|
+
const result = asDate('2024-01-15');
|
|
200
|
+
expect(result).toBeInstanceOf(Date);
|
|
201
|
+
expect(result?.getUTCFullYear()).toBe(2024);
|
|
202
|
+
});
|
|
203
|
+
it('returns null for an invalid date', () => {
|
|
204
|
+
expect(asDate('nope')).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('clampMenuX', () => {
|
|
208
|
+
it('clamps to the left minimum of 8', () => {
|
|
209
|
+
expect(clampMenuX(-50, 100)).toBe(8);
|
|
210
|
+
});
|
|
211
|
+
it('clamps against the right edge', () => {
|
|
212
|
+
const max = window.innerWidth - 100 - 8;
|
|
213
|
+
expect(clampMenuX(window.innerWidth, 100)).toBe(max);
|
|
214
|
+
});
|
|
215
|
+
it('returns the value unchanged when it fits', () => {
|
|
216
|
+
expect(clampMenuX(20, 50)).toBe(20);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
describe('cssEscape', () => {
|
|
220
|
+
const hasCssEscape = typeof CSS !== 'undefined' && typeof CSS.escape === 'function';
|
|
221
|
+
it('delegates to CSS.escape when available, otherwise escapes quotes', () => {
|
|
222
|
+
if (hasCssEscape) {
|
|
223
|
+
expect(cssEscape('1abc')).toBe(CSS.escape('1abc'));
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// Fallback only escapes double quotes.
|
|
227
|
+
expect(cssEscape('a"b')).toBe('a\\"b');
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
it('uses the quote-escaping fallback path when CSS is unavailable', () => {
|
|
231
|
+
const original = globalThis.CSS;
|
|
232
|
+
globalThis.CSS = undefined;
|
|
233
|
+
try {
|
|
234
|
+
expect(cssEscape('say "hi"')).toBe('say \\"hi\\"');
|
|
235
|
+
expect(cssEscape('plain')).toBe('plain');
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
;
|
|
239
|
+
globalThis.CSS = original;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
it('leaves a plain identifier intact', () => {
|
|
243
|
+
expect(cssEscape('abc')).toBe('abc');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
describe('rawToNumber', () => {
|
|
247
|
+
it('returns NaN for null/empty', () => {
|
|
248
|
+
expect(rawToNumber(null, false)).toBeNaN();
|
|
249
|
+
expect(rawToNumber('', false)).toBeNaN();
|
|
250
|
+
});
|
|
251
|
+
it('returns a numeric value as-is', () => {
|
|
252
|
+
expect(rawToNumber(42, false)).toBe(42);
|
|
253
|
+
});
|
|
254
|
+
it('coerces a numeric string', () => {
|
|
255
|
+
expect(rawToNumber('3.5', false)).toBe(3.5);
|
|
256
|
+
});
|
|
257
|
+
it('returns NaN for a non-numeric string', () => {
|
|
258
|
+
expect(rawToNumber('abc', false)).toBeNaN();
|
|
259
|
+
});
|
|
260
|
+
it('returns the timestamp for a Date when isDate is true', () => {
|
|
261
|
+
const d = new Date(Date.UTC(2024, 0, 1));
|
|
262
|
+
expect(rawToNumber(d, true)).toBe(d.getTime());
|
|
263
|
+
});
|
|
264
|
+
it('parses a date string to a timestamp when isDate is true', () => {
|
|
265
|
+
expect(rawToNumber('2024-01-01', true)).toBe(new Date('2024-01-01').getTime());
|
|
266
|
+
});
|
|
267
|
+
it('returns NaN for an invalid date when isDate is true', () => {
|
|
268
|
+
expect(rawToNumber('not-a-date', true)).toBeNaN();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
describe('formatFacetNumber', () => {
|
|
272
|
+
it('uses 0 fraction digits for magnitudes >= 1000', () => {
|
|
273
|
+
expect(formatFacetNumber(1234.56)).toBe((1234.56).toLocaleString(undefined, { maximumFractionDigits: 0 }));
|
|
274
|
+
});
|
|
275
|
+
it('uses 1 fraction digit for magnitudes >= 100', () => {
|
|
276
|
+
expect(formatFacetNumber(123.456)).toBe((123.456).toLocaleString(undefined, { maximumFractionDigits: 1 }));
|
|
277
|
+
});
|
|
278
|
+
it('uses 2 fraction digits for magnitudes >= 1', () => {
|
|
279
|
+
expect(formatFacetNumber(1.2345)).toBe((1.2345).toLocaleString(undefined, { maximumFractionDigits: 2 }));
|
|
280
|
+
});
|
|
281
|
+
it('uses 4 fraction digits for small magnitudes', () => {
|
|
282
|
+
expect(formatFacetNumber(0.123456)).toBe((0.123456).toLocaleString(undefined, { maximumFractionDigits: 4 }));
|
|
283
|
+
});
|
|
284
|
+
it('handles negative numbers via absolute magnitude', () => {
|
|
285
|
+
expect(formatFacetNumber(-2000)).toBe((-2000).toLocaleString(undefined, { maximumFractionDigits: 0 }));
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
describe('formatFacetDate', () => {
|
|
289
|
+
it('formats a timestamp into a short localized date', () => {
|
|
290
|
+
const ts = Date.UTC(2024, 0, 15);
|
|
291
|
+
const expected = new Date(ts).toLocaleDateString(undefined, {
|
|
292
|
+
year: 'numeric',
|
|
293
|
+
month: 'short',
|
|
294
|
+
day: 'numeric',
|
|
295
|
+
});
|
|
296
|
+
expect(formatFacetDate(ts)).toBe(expected);
|
|
297
|
+
});
|
|
298
|
+
});
|