@svgrid/grid 2.2.27 → 2.2.29
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/README.md +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +179 -179
- package/dist/GridMenus.svelte +782 -734
- package/dist/SvAutoComplete.svelte +165 -165
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +101 -101
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +275 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +160 -160
- package/dist/SvDrawer.svelte +254 -250
- package/dist/SvDropDownList.svelte +374 -374
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.controller.svelte.d.ts +14 -1
- package/dist/SvGrid.controller.svelte.js +112 -28
- package/dist/SvGrid.css +2704 -2723
- package/dist/SvGrid.svelte +3361 -3333
- package/dist/SvGrid.types.d.ts +20 -5
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +696 -696
- package/dist/SvGridSelect.svelte +259 -259
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +326 -287
- package/dist/SvListBox.svelte.d.ts +10 -1
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +281 -281
- package/dist/SvNumberInput.svelte +168 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -131
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +200 -200
- package/dist/SvSwitchButton.svelte +107 -107
- package/dist/SvTagsInput.svelte +112 -112
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +204 -204
- package/dist/SvTree.svelte +437 -437
- package/dist/SvTreeSelect.svelte +233 -233
- package/dist/cdn/GridMenus-Da_p7SKS.js +486 -0
- package/dist/cdn/GridMenus-Ds3OpzZT.js +490 -0
- package/dist/cdn/{SvDateTimePicker-B1sDc9Pz.js → SvDateTimePicker-B8GopjhC.js} +2 -2
- package/dist/cdn/{SvDateTimePicker-Bh0FTs1E.js → SvDateTimePicker-DB0aIEk8.js} +2 -2
- package/dist/cdn/{SvGridChart-Bgk4hDs8.js → SvGridChart-BcLCYvw9.js} +1 -1
- package/dist/cdn/{SvGridChart-BKQsO5fZ.js → SvGridChart-BhnFdLd0.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DkCc2Z7b.js → SvGridChartPanel-Cmx9DHAS.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DSkHIQDd.js → SvGridChartPanel-DvjwxLvK.js} +1 -1
- package/dist/cdn/{SvGridChartView-BrbVaELm.js → SvGridChartView-B40DTJSX.js} +1 -1
- package/dist/cdn/{SvGridChartView-i9rWvSKg.js → SvGridChartView-PwqI4s02.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D08sVmAy.js → SvGridDropdown-DjALc_4f.js} +1 -1
- package/dist/cdn/{SvGridDropdown-CGxxopQ4.js → SvGridDropdown-hpnHvOGt.js} +1 -1
- package/dist/cdn/{src-cgs1wcUS.js → src-BLJfdyL0.js} +4034 -3889
- package/dist/cdn/{src-Cw8d9U8j.js → src-DYDCiC0D.js} +7053 -6908
- package/dist/cdn/svgrid.js +9 -9
- package/dist/cdn/svgrid.svelte-external.js +9 -9
- package/dist/chart-export.js +8 -8
- package/dist/column-groups.js +8 -10
- package/dist/column-id.d.ts +9 -0
- package/dist/column-id.js +25 -0
- package/dist/core.js +2 -1
- package/dist/createListbox.svelte.d.ts +15 -2
- package/dist/createListbox.svelte.js +27 -6
- package/dist/editing.d.ts +1 -0
- package/dist/editing.js +45 -10
- package/dist/filtering/excel-filters.d.ts +30 -10
- package/dist/filtering/excel-filters.js +96 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/menus.d.ts +2 -0
- package/dist/menus.js +25 -0
- package/dist/row-resize.js +40 -0
- package/dist/selection.js +1 -1
- package/package.json +10 -10
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +179 -179
- package/src/GridMenus.svelte +782 -734
- package/src/SvAutoComplete.svelte +165 -165
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +101 -101
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +275 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +160 -160
- package/src/SvDrawer.svelte +254 -250
- package/src/SvDrawer.test.ts +18 -1
- package/src/SvDropDownList.svelte +374 -374
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.controller.svelte.ts +3335 -3239
- package/src/SvGrid.css +2704 -2723
- package/src/SvGrid.svelte +3361 -3333
- package/src/SvGrid.types.ts +1628 -1613
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +696 -696
- package/src/SvGridSelect.svelte +259 -259
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +326 -287
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +281 -281
- package/src/SvNavPane.test.ts +15 -4
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -131
- package/src/SvPopover.svelte +197 -197
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +200 -200
- package/src/SvSwitchButton.svelte +107 -107
- package/src/SvTagsInput.svelte +112 -112
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +204 -204
- package/src/SvTree.svelte +437 -437
- package/src/SvTreeSelect.svelte +233 -233
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1391 -1391
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +895 -895
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/column-groups.test.ts +48 -0
- package/src/column-groups.ts +8 -10
- package/src/column-id.ts +30 -0
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1112 -1111
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createListbox.svelte.ts +39 -7
- package/src/createMenu.svelte.ts +225 -225
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +319 -319
- package/src/editing.test.ts +949 -859
- package/src/editing.ts +709 -675
- package/src/editor-contract.ts +157 -157
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/filter-operators.ts +157 -157
- package/src/filtering/excel-filters.test.ts +44 -1
- package/src/filtering/excel-filters.ts +91 -14
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/index.ts +715 -713
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +172 -172
- package/src/menus.test.ts +736 -700
- package/src/menus.ts +679 -652
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/row-resize.test.ts +100 -0
- package/src/row-resize.ts +36 -0
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +872 -872
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +524 -524
- package/src/svgrid.api-extensions.test.ts +8 -3
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.cell-dom-ids.test.ts +96 -0
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.conditional-stat-scope.test.ts +111 -0
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +308 -0
- package/src/svgrid.filter-menu-scroll.test.ts +3 -1
- package/src/svgrid.group-header-ids.test.ts +121 -0
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +215 -215
- package/src/toast-store.svelte.ts +246 -246
- package/src/toast-store.test.ts +147 -147
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
- package/themes/ember.css +71 -71
- package/dist/cdn/GridMenus-BzxEYNtW.js +0 -421
- package/dist/cdn/GridMenus-Dr-nGEXn.js +0 -417
package/src/filter-operators.ts
CHANGED
|
@@ -1,157 +1,157 @@
|
|
|
1
|
-
// Filter-operator catalogue + the pure helpers that pick which operators
|
|
2
|
-
// apply to a column and how they're labelled. Fully static - no grid state -
|
|
3
|
-
// so it lives outside the controller.
|
|
4
|
-
import type { Column, RowData } from "./index";
|
|
5
|
-
import type { FilterOperator, FilterOption } from "./SvGrid.types";
|
|
6
|
-
|
|
7
|
-
export const filterOperatorOptions: Array<FilterOption> = [
|
|
8
|
-
{ value: "contains", label: "Contains", iconName: "op-contains" },
|
|
9
|
-
{ value: "notContains", label: "Not contains", iconName: "op-notContains" },
|
|
10
|
-
{ value: "equals", label: "Equals", iconName: "op-equals" },
|
|
11
|
-
{ value: "notEquals", label: "Not equals", iconName: "op-notEquals" },
|
|
12
|
-
{ value: "startsWith", label: "Starts with", iconName: "op-startsWith" },
|
|
13
|
-
{ value: "endsWith", label: "Ends with", iconName: "op-endsWith" },
|
|
14
|
-
{ value: "regex", label: "Regex", iconName: "op-regex" },
|
|
15
|
-
{ value: "in", label: "In", iconName: "op-in" },
|
|
16
|
-
{ value: "notIn", label: "Not in", iconName: "op-notIn" },
|
|
17
|
-
{ value: "greaterThan", label: "Greater than", iconName: "op-greaterThan" },
|
|
18
|
-
{ value: "lessThan", label: "Less than", iconName: "op-lessThan" },
|
|
19
|
-
{ value: "between", label: "Between", iconName: "op-between" },
|
|
20
|
-
{ value: "isBlank", label: "Blank", iconName: "op-isBlank" },
|
|
21
|
-
{ value: "isNotBlank", label: "Not blank", iconName: "op-isNotBlank" },
|
|
22
|
-
];
|
|
23
|
-
/** Operators whose predicate is a set-membership test over a token list.
|
|
24
|
-
* The filter row renders a multi-value chip input for these instead of a
|
|
25
|
-
* single text box. */
|
|
26
|
-
export const SET_OPERATORS: ReadonlyArray<FilterOperator> = ["in", "notIn"];
|
|
27
|
-
/** Operators that need no value input - they act on emptiness alone. */
|
|
28
|
-
export const VALUELESS_OPERATORS: ReadonlyArray<FilterOperator> = [
|
|
29
|
-
"isBlank",
|
|
30
|
-
"isNotBlank",
|
|
31
|
-
];
|
|
32
|
-
/** Which operators make sense for each column editor type. */
|
|
33
|
-
export const TEXT_OPERATORS: Array<FilterOperator> = [
|
|
34
|
-
"contains",
|
|
35
|
-
"notContains",
|
|
36
|
-
"equals",
|
|
37
|
-
"notEquals",
|
|
38
|
-
"startsWith",
|
|
39
|
-
"endsWith",
|
|
40
|
-
"regex",
|
|
41
|
-
"in",
|
|
42
|
-
"notIn",
|
|
43
|
-
"isBlank",
|
|
44
|
-
"isNotBlank",
|
|
45
|
-
];
|
|
46
|
-
export const NUMBER_OPERATORS: Array<FilterOperator> = [
|
|
47
|
-
"equals",
|
|
48
|
-
"notEquals",
|
|
49
|
-
"greaterThan",
|
|
50
|
-
"lessThan",
|
|
51
|
-
"between",
|
|
52
|
-
"in",
|
|
53
|
-
"notIn",
|
|
54
|
-
"isBlank",
|
|
55
|
-
"isNotBlank",
|
|
56
|
-
];
|
|
57
|
-
export const DATE_OPERATORS: Array<FilterOperator> = [
|
|
58
|
-
"equals",
|
|
59
|
-
"notEquals",
|
|
60
|
-
"lessThan",
|
|
61
|
-
"greaterThan",
|
|
62
|
-
"between",
|
|
63
|
-
"isBlank",
|
|
64
|
-
"isNotBlank",
|
|
65
|
-
];
|
|
66
|
-
export const CHECKBOX_OPERATORS: Array<FilterOperator> = [
|
|
67
|
-
"equals",
|
|
68
|
-
"isBlank",
|
|
69
|
-
"isNotBlank",
|
|
70
|
-
];
|
|
71
|
-
export const fallbackOperatorOption: FilterOption = {
|
|
72
|
-
value: "contains",
|
|
73
|
-
label: "Contains",
|
|
74
|
-
iconName: "op-contains",
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export function operatorOption(value: FilterOperator): FilterOption {
|
|
78
|
-
return (
|
|
79
|
-
filterOperatorOptions.find((option) => option.value === value) ??
|
|
80
|
-
fallbackOperatorOption
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Returns the operators that make sense for the given column's data type. */
|
|
85
|
-
export function operatorsForColumn<TData extends RowData>(
|
|
86
|
-
column: Column<TData> | undefined,
|
|
87
|
-
): Array<FilterOption> {
|
|
88
|
-
const editorType = column?.columnDef.editorType ?? "text";
|
|
89
|
-
const ids =
|
|
90
|
-
editorType === "number"
|
|
91
|
-
? NUMBER_OPERATORS
|
|
92
|
-
: editorType === "date" || editorType === "datetime"
|
|
93
|
-
? DATE_OPERATORS
|
|
94
|
-
: editorType === "checkbox"
|
|
95
|
-
? CHECKBOX_OPERATORS
|
|
96
|
-
: TEXT_OPERATORS;
|
|
97
|
-
return ids
|
|
98
|
-
.map((id) => filterOperatorOptions.find((option) => option.value === id))
|
|
99
|
-
.filter((option): option is FilterOption => Boolean(option));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Default operator for a column (first one valid for its type). */
|
|
103
|
-
export function defaultOperatorFor<TData extends RowData>(
|
|
104
|
-
column: Column<TData> | undefined,
|
|
105
|
-
): FilterOperator {
|
|
106
|
-
return operatorsForColumn(column)[0]?.value ?? "contains";
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Date columns get friendlier labels for "less / greater than". */
|
|
110
|
-
export function operatorLabelFor<TData extends RowData>(
|
|
111
|
-
option: FilterOption,
|
|
112
|
-
column: Column<TData> | undefined,
|
|
113
|
-
): string {
|
|
114
|
-
const editorType = column?.columnDef.editorType;
|
|
115
|
-
if (editorType === "date" || editorType === "datetime") {
|
|
116
|
-
if (option.value === "lessThan") return "Before";
|
|
117
|
-
if (option.value === "greaterThan") return "After";
|
|
118
|
-
}
|
|
119
|
-
return option.label;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** GridMessages key for each operator's label (see grid-messages.ts). */
|
|
123
|
-
const OPERATOR_MESSAGE_KEY: Record<FilterOperator, string> = {
|
|
124
|
-
contains: "opContains",
|
|
125
|
-
notContains: "opNotContains",
|
|
126
|
-
equals: "opEquals",
|
|
127
|
-
notEquals: "opNotEquals",
|
|
128
|
-
startsWith: "opStartsWith",
|
|
129
|
-
endsWith: "opEndsWith",
|
|
130
|
-
regex: "opRegex",
|
|
131
|
-
in: "opIn",
|
|
132
|
-
notIn: "opNotIn",
|
|
133
|
-
greaterThan: "opGreaterThan",
|
|
134
|
-
lessThan: "opLessThan",
|
|
135
|
-
between: "opBetween",
|
|
136
|
-
isBlank: "opIsBlank",
|
|
137
|
-
isNotBlank: "opIsNotBlank",
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Localized operator label: honors date `Before`/`After` relabels, then the
|
|
142
|
-
* `localeText` override for the operator, falling back to the English
|
|
143
|
-
* `option.label`. `messages` is a loose string map to avoid a type cycle.
|
|
144
|
-
*/
|
|
145
|
-
export function localizeOperatorLabel<TData extends RowData>(
|
|
146
|
-
option: FilterOption,
|
|
147
|
-
column: Column<TData> | undefined,
|
|
148
|
-
messages?: Record<string, string> | null,
|
|
149
|
-
): string {
|
|
150
|
-
const editorType = column?.columnDef.editorType;
|
|
151
|
-
if (editorType === "date" || editorType === "datetime") {
|
|
152
|
-
if (option.value === "lessThan") return messages?.opBefore || "Before";
|
|
153
|
-
if (option.value === "greaterThan") return messages?.opAfter || "After";
|
|
154
|
-
}
|
|
155
|
-
const key = OPERATOR_MESSAGE_KEY[option.value];
|
|
156
|
-
return (key && messages?.[key]) || option.label;
|
|
157
|
-
}
|
|
1
|
+
// Filter-operator catalogue + the pure helpers that pick which operators
|
|
2
|
+
// apply to a column and how they're labelled. Fully static - no grid state -
|
|
3
|
+
// so it lives outside the controller.
|
|
4
|
+
import type { Column, RowData } from "./index";
|
|
5
|
+
import type { FilterOperator, FilterOption } from "./SvGrid.types";
|
|
6
|
+
|
|
7
|
+
export const filterOperatorOptions: Array<FilterOption> = [
|
|
8
|
+
{ value: "contains", label: "Contains", iconName: "op-contains" },
|
|
9
|
+
{ value: "notContains", label: "Not contains", iconName: "op-notContains" },
|
|
10
|
+
{ value: "equals", label: "Equals", iconName: "op-equals" },
|
|
11
|
+
{ value: "notEquals", label: "Not equals", iconName: "op-notEquals" },
|
|
12
|
+
{ value: "startsWith", label: "Starts with", iconName: "op-startsWith" },
|
|
13
|
+
{ value: "endsWith", label: "Ends with", iconName: "op-endsWith" },
|
|
14
|
+
{ value: "regex", label: "Regex", iconName: "op-regex" },
|
|
15
|
+
{ value: "in", label: "In", iconName: "op-in" },
|
|
16
|
+
{ value: "notIn", label: "Not in", iconName: "op-notIn" },
|
|
17
|
+
{ value: "greaterThan", label: "Greater than", iconName: "op-greaterThan" },
|
|
18
|
+
{ value: "lessThan", label: "Less than", iconName: "op-lessThan" },
|
|
19
|
+
{ value: "between", label: "Between", iconName: "op-between" },
|
|
20
|
+
{ value: "isBlank", label: "Blank", iconName: "op-isBlank" },
|
|
21
|
+
{ value: "isNotBlank", label: "Not blank", iconName: "op-isNotBlank" },
|
|
22
|
+
];
|
|
23
|
+
/** Operators whose predicate is a set-membership test over a token list.
|
|
24
|
+
* The filter row renders a multi-value chip input for these instead of a
|
|
25
|
+
* single text box. */
|
|
26
|
+
export const SET_OPERATORS: ReadonlyArray<FilterOperator> = ["in", "notIn"];
|
|
27
|
+
/** Operators that need no value input - they act on emptiness alone. */
|
|
28
|
+
export const VALUELESS_OPERATORS: ReadonlyArray<FilterOperator> = [
|
|
29
|
+
"isBlank",
|
|
30
|
+
"isNotBlank",
|
|
31
|
+
];
|
|
32
|
+
/** Which operators make sense for each column editor type. */
|
|
33
|
+
export const TEXT_OPERATORS: Array<FilterOperator> = [
|
|
34
|
+
"contains",
|
|
35
|
+
"notContains",
|
|
36
|
+
"equals",
|
|
37
|
+
"notEquals",
|
|
38
|
+
"startsWith",
|
|
39
|
+
"endsWith",
|
|
40
|
+
"regex",
|
|
41
|
+
"in",
|
|
42
|
+
"notIn",
|
|
43
|
+
"isBlank",
|
|
44
|
+
"isNotBlank",
|
|
45
|
+
];
|
|
46
|
+
export const NUMBER_OPERATORS: Array<FilterOperator> = [
|
|
47
|
+
"equals",
|
|
48
|
+
"notEquals",
|
|
49
|
+
"greaterThan",
|
|
50
|
+
"lessThan",
|
|
51
|
+
"between",
|
|
52
|
+
"in",
|
|
53
|
+
"notIn",
|
|
54
|
+
"isBlank",
|
|
55
|
+
"isNotBlank",
|
|
56
|
+
];
|
|
57
|
+
export const DATE_OPERATORS: Array<FilterOperator> = [
|
|
58
|
+
"equals",
|
|
59
|
+
"notEquals",
|
|
60
|
+
"lessThan",
|
|
61
|
+
"greaterThan",
|
|
62
|
+
"between",
|
|
63
|
+
"isBlank",
|
|
64
|
+
"isNotBlank",
|
|
65
|
+
];
|
|
66
|
+
export const CHECKBOX_OPERATORS: Array<FilterOperator> = [
|
|
67
|
+
"equals",
|
|
68
|
+
"isBlank",
|
|
69
|
+
"isNotBlank",
|
|
70
|
+
];
|
|
71
|
+
export const fallbackOperatorOption: FilterOption = {
|
|
72
|
+
value: "contains",
|
|
73
|
+
label: "Contains",
|
|
74
|
+
iconName: "op-contains",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function operatorOption(value: FilterOperator): FilterOption {
|
|
78
|
+
return (
|
|
79
|
+
filterOperatorOptions.find((option) => option.value === value) ??
|
|
80
|
+
fallbackOperatorOption
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Returns the operators that make sense for the given column's data type. */
|
|
85
|
+
export function operatorsForColumn<TData extends RowData>(
|
|
86
|
+
column: Column<TData> | undefined,
|
|
87
|
+
): Array<FilterOption> {
|
|
88
|
+
const editorType = column?.columnDef.editorType ?? "text";
|
|
89
|
+
const ids =
|
|
90
|
+
editorType === "number"
|
|
91
|
+
? NUMBER_OPERATORS
|
|
92
|
+
: editorType === "date" || editorType === "datetime"
|
|
93
|
+
? DATE_OPERATORS
|
|
94
|
+
: editorType === "checkbox"
|
|
95
|
+
? CHECKBOX_OPERATORS
|
|
96
|
+
: TEXT_OPERATORS;
|
|
97
|
+
return ids
|
|
98
|
+
.map((id) => filterOperatorOptions.find((option) => option.value === id))
|
|
99
|
+
.filter((option): option is FilterOption => Boolean(option));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Default operator for a column (first one valid for its type). */
|
|
103
|
+
export function defaultOperatorFor<TData extends RowData>(
|
|
104
|
+
column: Column<TData> | undefined,
|
|
105
|
+
): FilterOperator {
|
|
106
|
+
return operatorsForColumn(column)[0]?.value ?? "contains";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Date columns get friendlier labels for "less / greater than". */
|
|
110
|
+
export function operatorLabelFor<TData extends RowData>(
|
|
111
|
+
option: FilterOption,
|
|
112
|
+
column: Column<TData> | undefined,
|
|
113
|
+
): string {
|
|
114
|
+
const editorType = column?.columnDef.editorType;
|
|
115
|
+
if (editorType === "date" || editorType === "datetime") {
|
|
116
|
+
if (option.value === "lessThan") return "Before";
|
|
117
|
+
if (option.value === "greaterThan") return "After";
|
|
118
|
+
}
|
|
119
|
+
return option.label;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** GridMessages key for each operator's label (see grid-messages.ts). */
|
|
123
|
+
const OPERATOR_MESSAGE_KEY: Record<FilterOperator, string> = {
|
|
124
|
+
contains: "opContains",
|
|
125
|
+
notContains: "opNotContains",
|
|
126
|
+
equals: "opEquals",
|
|
127
|
+
notEquals: "opNotEquals",
|
|
128
|
+
startsWith: "opStartsWith",
|
|
129
|
+
endsWith: "opEndsWith",
|
|
130
|
+
regex: "opRegex",
|
|
131
|
+
in: "opIn",
|
|
132
|
+
notIn: "opNotIn",
|
|
133
|
+
greaterThan: "opGreaterThan",
|
|
134
|
+
lessThan: "opLessThan",
|
|
135
|
+
between: "opBetween",
|
|
136
|
+
isBlank: "opIsBlank",
|
|
137
|
+
isNotBlank: "opIsNotBlank",
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Localized operator label: honors date `Before`/`After` relabels, then the
|
|
142
|
+
* `localeText` override for the operator, falling back to the English
|
|
143
|
+
* `option.label`. `messages` is a loose string map to avoid a type cycle.
|
|
144
|
+
*/
|
|
145
|
+
export function localizeOperatorLabel<TData extends RowData>(
|
|
146
|
+
option: FilterOption,
|
|
147
|
+
column: Column<TData> | undefined,
|
|
148
|
+
messages?: Record<string, string> | null,
|
|
149
|
+
): string {
|
|
150
|
+
const editorType = column?.columnDef.editorType;
|
|
151
|
+
if (editorType === "date" || editorType === "datetime") {
|
|
152
|
+
if (option.value === "lessThan") return messages?.opBefore || "Before";
|
|
153
|
+
if (option.value === "greaterThan") return messages?.opAfter || "After";
|
|
154
|
+
}
|
|
155
|
+
const key = OPERATOR_MESSAGE_KEY[option.value];
|
|
156
|
+
return (key && messages?.[key]) || option.label;
|
|
157
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
applyExcelFilter,
|
|
7
7
|
splitInTokens,
|
|
8
8
|
joinInTokens,
|
|
9
|
+
trailingInToken,
|
|
9
10
|
type ExcelFilter,
|
|
10
11
|
} from './excel-filters'
|
|
11
12
|
|
|
@@ -211,6 +212,48 @@ describe('in-token serialization helpers', () => {
|
|
|
211
212
|
})
|
|
212
213
|
it('joinInTokens round-trips through splitInTokens', () => {
|
|
213
214
|
expect(splitInTokens(joinInTokens(['a', 'b', 'c']))).toEqual(['a', 'b', 'c'])
|
|
214
|
-
expect(joinInTokens([' a ', '', 'b'])).toBe('a
|
|
215
|
+
expect(joinInTokens([' a ', '', 'b'])).toBe('a, b')
|
|
216
|
+
})
|
|
217
|
+
it('joinInTokens survives an <input type="text"> value round-trip', () => {
|
|
218
|
+
// The input value sanitiser strips newlines, so the separator must not be
|
|
219
|
+
// one - otherwise the list comes back as a single run-together token.
|
|
220
|
+
const input = document.createElement('input')
|
|
221
|
+
input.value = joinInTokens(['AAPL', 'MSFT'])
|
|
222
|
+
expect(splitInTokens(input.value)).toEqual(['AAPL', 'MSFT'])
|
|
223
|
+
})
|
|
224
|
+
it('trailingInToken returns the fragment still being typed', () => {
|
|
225
|
+
expect(trailingInToken('AAPL, MS')).toBe('MS')
|
|
226
|
+
expect(trailingInToken('AAPL,MS')).toBe('MS')
|
|
227
|
+
expect(trailingInToken('MS')).toBe('MS')
|
|
228
|
+
expect(trailingInToken('AAPL\nMS')).toBe('MS')
|
|
229
|
+
})
|
|
230
|
+
it('trailingInToken is empty once the fragment is separated off', () => {
|
|
231
|
+
expect(trailingInToken('AAPL, ')).toBe('')
|
|
232
|
+
expect(trailingInToken('AAPL,')).toBe('')
|
|
233
|
+
expect(trailingInToken('AAPL\n')).toBe('')
|
|
234
|
+
expect(trailingInToken('')).toBe('')
|
|
235
|
+
expect(trailingInToken(null)).toBe('')
|
|
236
|
+
})
|
|
237
|
+
it('quotes tokens containing a separator so they survive the round-trip', () => {
|
|
238
|
+
// Facet labels really do contain commas: numeric buckets come from
|
|
239
|
+
// toLocaleString ("1,234 - 5,678") and date buckets use a short month.
|
|
240
|
+
const labels = ['1,234 - 5,678', 'Aug 17, 2026', 'plain']
|
|
241
|
+
expect(splitInTokens(joinInTokens(labels))).toEqual(labels)
|
|
242
|
+
expect(joinInTokens(['Aug 17, 2026'])).toBe('"Aug 17, 2026"')
|
|
243
|
+
})
|
|
244
|
+
it('round-trips a token containing a quote', () => {
|
|
245
|
+
expect(splitInTokens(joinInTokens(['5" pipe']))).toEqual(['5" pipe'])
|
|
246
|
+
expect(splitInTokens(joinInTokens(['a"b, c']))).toEqual(['a"b, c'])
|
|
247
|
+
})
|
|
248
|
+
it('treats a quote mid-token as literal, not as an opening quote', () => {
|
|
249
|
+
expect(splitInTokens('5" pipe, plain')).toEqual(['5" pipe', 'plain'])
|
|
250
|
+
})
|
|
251
|
+
it('keeps inner spacing of a quoted token, trims an unquoted one', () => {
|
|
252
|
+
expect(splitInTokens('" padded ", bare ')).toEqual([' padded ', 'bare'])
|
|
253
|
+
})
|
|
254
|
+
it('trailingInToken understands quoted fragments', () => {
|
|
255
|
+
expect(trailingInToken('AAPL, "Aug 17, 20')).toBe('Aug 17, 20')
|
|
256
|
+
expect(trailingInToken('AAPL, "Aug 17, 2026"')).toBe('Aug 17, 2026')
|
|
257
|
+
expect(trailingInToken('"Aug 17, 2026", ')).toBe('')
|
|
215
258
|
})
|
|
216
259
|
})
|
|
@@ -57,29 +57,106 @@ export function normalizeForFilter(
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Delimiter used to serialise the `in` / `notIn` value list into the single
|
|
60
|
-
* `value` string that the filter model carries.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
60
|
+
* `value` string that the filter model carries.
|
|
61
|
+
*
|
|
62
|
+
* This has to be single-line safe: the tool panel and the filter menu show the
|
|
63
|
+
* serialised list in a plain `<input type="text">`, and the HTML input value
|
|
64
|
+
* sanitiser STRIPS newlines - so a newline-joined list came back out of the DOM
|
|
65
|
+
* as one run-together token. `splitInTokens` accepts newlines too, so values
|
|
66
|
+
* serialised by older builds still parse.
|
|
64
67
|
*/
|
|
65
|
-
export const IN_TOKEN_SEP = '
|
|
68
|
+
export const IN_TOKEN_SEP = ', '
|
|
66
69
|
|
|
67
70
|
/**
|
|
68
|
-
* Split a serialised `in` / `notIn` value into its individual tokens.
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
71
|
+
* Split a serialised `in` / `notIn` value into its individual tokens.
|
|
72
|
+
*
|
|
73
|
+
* The grammar is CSV-shaped: comma OR newline separates, and a token may be
|
|
74
|
+
* double-quoted to carry a separator literally (`""` escapes a quote inside
|
|
75
|
+
* one). Quoting is what makes a comma separator safe - facet values commonly
|
|
76
|
+
* contain commas of their own, because numeric bucket labels are built with
|
|
77
|
+
* `toLocaleString` ("1,234 - 5,678") and date labels with a short month
|
|
78
|
+
* ("Aug 17, 2026"). A quote only opens a token when it is the token's first
|
|
79
|
+
* non-space character, so an unquoted `5" pipe` stays literal.
|
|
72
80
|
*/
|
|
73
81
|
export function splitInTokens(value: unknown): string[] {
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
const { tokens, trailing } = scanInTokens(value)
|
|
83
|
+
if (trailing) tokens.push(trailing)
|
|
84
|
+
return tokens
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Serialise a list of tokens back into the single `in` / `notIn` value,
|
|
89
|
+
* quoting any token that carries a separator or a quote of its own.
|
|
90
|
+
*/
|
|
91
|
+
export function joinInTokens(tokens: ReadonlyArray<string>): string {
|
|
92
|
+
return tokens
|
|
76
93
|
.map((t) => t.trim())
|
|
77
94
|
.filter((t) => t.length > 0)
|
|
95
|
+
.map((t) => (/["\n,]/.test(t) ? `"${t.replace(/"/g, '""')}"` : t))
|
|
96
|
+
.join(IN_TOKEN_SEP)
|
|
78
97
|
}
|
|
79
98
|
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The token still being typed at the end of an `in` / `notIn` value - the text
|
|
101
|
+
* after the last separator. `'AAPL, MS'` -> `'MS'`; a value ending in a
|
|
102
|
+
* separator (or empty) has no trailing token.
|
|
103
|
+
*
|
|
104
|
+
* Inputs that hold the whole token list use this to drive the value-suggestion
|
|
105
|
+
* dropdown: the trailing fragment is the search query, not a committed token.
|
|
106
|
+
*/
|
|
107
|
+
export function trailingInToken(value: unknown): string {
|
|
108
|
+
return scanInTokens(value).trailing
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* One pass over a serialised `in` / `notIn` value, splitting it into the
|
|
113
|
+
* tokens that are terminated by a separator and the unterminated fragment at
|
|
114
|
+
* the end. Every token helper above is a view onto this, so they can never
|
|
115
|
+
* disagree about where a quoted token starts or ends.
|
|
116
|
+
*/
|
|
117
|
+
function scanInTokens(value: unknown): { tokens: string[]; trailing: string } {
|
|
118
|
+
const raw = String(value ?? '')
|
|
119
|
+
const tokens: string[] = []
|
|
120
|
+
let buf = ''
|
|
121
|
+
let quoted = false
|
|
122
|
+
let wasQuoted = false
|
|
123
|
+
let closed = false
|
|
124
|
+
|
|
125
|
+
// A quoted token keeps its inner spacing; an unquoted one is trimmed.
|
|
126
|
+
const take = () => {
|
|
127
|
+
const t = wasQuoted ? buf : buf.trim()
|
|
128
|
+
buf = ''
|
|
129
|
+
wasQuoted = false
|
|
130
|
+
closed = false
|
|
131
|
+
return t
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
135
|
+
const ch = raw[i]
|
|
136
|
+
if (quoted) {
|
|
137
|
+
if (ch !== '"') { buf += ch; continue }
|
|
138
|
+
// A doubled quote is an escaped literal quote, not the closing one.
|
|
139
|
+
if (raw[i + 1] === '"') { buf += '"'; i += 1; continue }
|
|
140
|
+
quoted = false
|
|
141
|
+
closed = true
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if (ch === ',' || ch === '\n') {
|
|
145
|
+
const t = take()
|
|
146
|
+
if (t.length) tokens.push(t)
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
// Only opens a quoted token at the token's start - `5" pipe` is literal.
|
|
150
|
+
if (ch === '"' && !closed && !buf.trim()) {
|
|
151
|
+
quoted = true
|
|
152
|
+
wasQuoted = true
|
|
153
|
+
buf = ''
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
buf += ch
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { tokens, trailing: take() }
|
|
83
160
|
}
|
|
84
161
|
|
|
85
162
|
export function applyExcelFilter(
|
package/src/flex-render.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/// <reference types="svelte" />
|
|
2
|
-
|
|
3
|
-
export { default as FlexRender } from './FlexRender.svelte'
|
|
1
|
+
/// <reference types="svelte" />
|
|
2
|
+
|
|
3
|
+
export { default as FlexRender } from './FlexRender.svelte'
|