@svgrid/grid 2.6.23 → 3.0.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.
- package/CHANGELOG.md +88 -88
- package/README.md +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +181 -181
- package/dist/SvAutoComplete.svelte +169 -169
- package/dist/SvAvatar.svelte +75 -75
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +102 -102
- package/dist/SvCircularProgress.svelte +109 -109
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +279 -279
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +163 -163
- package/dist/SvDrawer.svelte +254 -254
- package/dist/SvDropDownList.svelte +378 -378
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGridCellEditor.svelte +744 -744
- package/dist/SvGridChart.svelte +1716 -1716
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +728 -728
- package/dist/SvGridSelect.svelte +270 -270
- package/dist/SvGroupCell.svelte +116 -116
- package/dist/SvListBox.svelte +334 -334
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +293 -293
- package/dist/SvNumberInput.svelte +172 -172
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -133
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvProgress.svelte +116 -116
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRating.svelte +112 -112
- package/dist/SvResult.svelte +73 -73
- package/dist/SvRichText.svelte +211 -211
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +203 -203
- package/dist/SvSwitchButton.svelte +108 -108
- package/dist/SvTagsInput.svelte +115 -115
- 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 +208 -208
- package/dist/SvTree.svelte +444 -444
- package/dist/SvTreeSelect.svelte +239 -239
- package/dist/cdn/{GridMenus-BzWXQjv3.js → GridMenus-DMihUtma.js} +3 -3
- package/dist/cdn/{GridMenus-xr_rHx8F.js → GridMenus-nmNDj1a3.js} +3 -3
- package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-Bh0A0JkF.js} +1 -1
- package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-DflbiP7N.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-BWwpfB_o.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-CCbDZNZB.js → SvDateTimePicker-Bivn8dAP.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-BAPSC7EL.js → SvGridCellEditor-AXL8cHGO.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-BwzmUWtL.js → SvGridCellEditor-Ba7rY3eu.js} +1 -1
- package/dist/cdn/{SvGridChart-BwVos976.js → SvGridChart-Bs2GIR2Q.js} +1 -1
- package/dist/cdn/{SvGridChart-BEJmNNx9.js → SvGridChart-CwhFz7GV.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-BOknOkrP.js → SvGridChartPanel-1WXuStzM.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-D2PjII4O.js → SvGridChartPanel-e5_nqNP9.js} +1 -1
- package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-SmPW10dI.js} +1 -1
- package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-eSPuJE6g.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-B1ZcLpgs.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CtdbHcIS.js} +1 -1
- package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CcP1tafP.js} +2 -2
- package/dist/cdn/{date-format-BNii4zeD.js → date-format-DZT7T1wf.js} +2 -2
- package/dist/cdn/{row-drag-touch-Ddaa-QeB.js → row-drag-touch-CycRR65n.js} +12 -10
- package/dist/cdn/{src-skQN5eqh.js → src-B_YS5AOc.js} +45 -45
- package/dist/cdn/{src-DGo7IHug.js → src-DYXXpuSk.js} +44 -44
- package/dist/cdn/svgrid.js +7 -7
- package/dist/cdn/svgrid.svelte-external.js +7 -7
- package/dist/chart-export.js +8 -8
- package/dist/row-drag-touch.d.ts +5 -1
- package/dist/row-drag-touch.js +15 -4
- package/dist/row-drag.js +7 -3
- package/package.json +11 -11
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +181 -181
- package/src/SvAutoComplete.svelte +169 -169
- package/src/SvAvatar.svelte +75 -75
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +102 -102
- package/src/SvCircularProgress.svelte +109 -109
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +279 -279
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +163 -163
- package/src/SvDrawer.svelte +254 -254
- package/src/SvDropDownList.svelte +378 -378
- 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.types.ts +2092 -2092
- package/src/SvGridCellEditor.svelte +744 -744
- package/src/SvGridChart.svelte +1716 -1716
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +728 -728
- package/src/SvGridSelect.svelte +270 -270
- package/src/SvGroupCell.svelte +116 -116
- package/src/SvListBox.svelte +334 -334
- 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 +293 -293
- package/src/SvNumberInput.svelte +172 -172
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -133
- package/src/SvPopover.svelte +197 -197
- package/src/SvProgress.svelte +116 -116
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRating.svelte +112 -112
- package/src/SvResult.svelte +73 -73
- package/src/SvRichText.svelte +211 -211
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +203 -203
- package/src/SvSwitchButton.svelte +108 -108
- package/src/SvTagsInput.svelte +115 -115
- 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 +208 -208
- package/src/SvTree.svelte +444 -444
- package/src/SvTreeSelect.svelte +239 -239
- 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 +1419 -1419
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +846 -846
- package/src/builtin-editors.grid.test.ts +83 -83
- package/src/cell-formatting.ts +171 -171
- package/src/cell-render.test.ts +513 -513
- package/src/cell-render.ts +496 -496
- package/src/cell-values.ts +148 -148
- package/src/chart-export.ts +202 -202
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2321 -2321
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/column-groups.ts +78 -78
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1865 -1865
- 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 +250 -250
- package/src/createMenu.svelte.ts +224 -224
- 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 +322 -322
- package/src/datetime/date-core.ts +206 -206
- package/src/datetime/date-restrict.ts +61 -61
- package/src/datetime/timezone.ts +135 -135
- package/src/dock-manager-model.ts +596 -596
- package/src/dock-model.ts +374 -374
- package/src/editing.test.ts +974 -974
- package/src/editing.ts +609 -609
- package/src/editor-contract.ts +171 -171
- package/src/editor-registry.grid.test.ts +144 -144
- package/src/editor-registry.ts +122 -122
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +601 -601
- package/src/filter-operators.ts +160 -160
- package/src/filtering/excel-filters.ts +325 -325
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/group-display.test.ts +167 -167
- package/src/group-display.ts +200 -200
- package/src/headless.ts +87 -87
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard-handlers.ts +270 -270
- 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 +179 -179
- package/src/menus.ts +597 -597
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/positioning.ts +268 -268
- package/src/render-component.ts +28 -28
- package/src/row-drag-touch.ts +20 -5
- package/src/row-drag.test.ts +401 -353
- package/src/row-drag.ts +419 -415
- package/src/row-resize.test.ts +524 -524
- package/src/row-resize.ts +228 -228
- package/src/scheduler-ical.ts +181 -181
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +873 -873
- package/src/selection.test.ts +885 -885
- package/src/server-data-source.test.ts +383 -383
- package/src/server-data-source.ts +469 -469
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +488 -488
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/summaries.ts +113 -113
- package/src/svgrid-wrapper.types.ts +563 -563
- package/src/svgrid.async-editor-options.test.ts +273 -273
- package/src/svgrid.auto-row-height.test.ts +204 -204
- package/src/svgrid.behavior.test.ts +910 -910
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.comments-autocomplete.test.ts +127 -127
- package/src/svgrid.context-menu.test.ts +147 -147
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-depth.test.ts +163 -163
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +377 -377
- package/src/svgrid.filter-menu-scroll.test.ts +112 -112
- package/src/svgrid.grand-total.test.ts +188 -188
- package/src/svgrid.group-display-mode.test.ts +171 -171
- package/src/svgrid.group-footers.test.ts +121 -121
- package/src/svgrid.group-pagination.test.ts +153 -153
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.tree-data.test.ts +186 -186
- package/src/svgrid.wrapper.test.ts +63 -63
- package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +288 -288
- package/src/toast-store.svelte.ts +250 -250
- package/src/toast-store.test.ts +147 -147
- package/src/tree-row-model.test.ts +168 -168
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-localization.test.ts +113 -113
- 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 +322 -322
|
@@ -1,325 +1,325 @@
|
|
|
1
|
-
export type ExcelFilterOperator =
|
|
2
|
-
| 'contains'
|
|
3
|
-
| 'notContains'
|
|
4
|
-
| 'equals'
|
|
5
|
-
| 'notEquals'
|
|
6
|
-
| 'startsWith'
|
|
7
|
-
| 'endsWith'
|
|
8
|
-
| 'regex'
|
|
9
|
-
| 'in'
|
|
10
|
-
| 'notIn'
|
|
11
|
-
| 'greaterThan'
|
|
12
|
-
| 'lessThan'
|
|
13
|
-
| 'between'
|
|
14
|
-
| 'isBlank'
|
|
15
|
-
| 'isNotBlank'
|
|
16
|
-
|
|
17
|
-
export type ExcelFilter = {
|
|
18
|
-
id: string
|
|
19
|
-
operator: ExcelFilterOperator
|
|
20
|
-
value?: unknown
|
|
21
|
-
valueTo?: unknown
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type ExcelFilterOptions = {
|
|
25
|
-
/**
|
|
26
|
-
* BCP-47 locale tag (or fallback list) for accent- and case-
|
|
27
|
-
* insensitive text comparison. When set, "café", "Café" and "CAFÉ"
|
|
28
|
-
* all match "cafe" without strain.
|
|
29
|
-
*
|
|
30
|
-
* Implementation: NFD-decompose both sides, strip combining marks
|
|
31
|
-
* (diacritics), then locale-aware lowercase. This is the standard
|
|
32
|
-
* "lowercase + asciifolding" pipeline used by Elasticsearch, Postgres
|
|
33
|
-
* (with `unaccent`), and Lucene. Cheap, deterministic, no Collator
|
|
34
|
-
* round-trip.
|
|
35
|
-
*/
|
|
36
|
-
locale?: string | ReadonlyArray<string>
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// U+0300..U+036F is the Combining Diacritical Marks block. After NFD
|
|
40
|
-
// decomposition accented characters become "base char + combining mark";
|
|
41
|
-
// stripping the marks leaves the bare ASCII base.
|
|
42
|
-
const DIACRITIC_RE = /[̀-ͯ]/g
|
|
43
|
-
|
|
44
|
-
/** NFD-decompose, strip combining marks (diacritics), then locale-aware
|
|
45
|
-
* lowercase. The locale-aware lowercasing handles Turkish dotted-I /
|
|
46
|
-
* dotless-i correctly when the consumer threads `"tr"` through. */
|
|
47
|
-
export function normalizeForFilter(
|
|
48
|
-
s: string,
|
|
49
|
-
locale?: string | ReadonlyArray<string>,
|
|
50
|
-
): string {
|
|
51
|
-
if (!s) return ''
|
|
52
|
-
|
|
53
|
-
// Fast path for pure-ASCII input, which is the overwhelming majority of what
|
|
54
|
-
// a grid filters. Both of the expensive steps are provably no-ops there:
|
|
55
|
-
// ASCII is already NFD-normalised, and the combining-marks block this strips
|
|
56
|
-
// (U+0300..U+036F) contains no ASCII. So lowercasing alone is the same
|
|
57
|
-
// answer, and `normalize-fast-path.test.ts` checks that exhaustively over
|
|
58
|
-
// every ASCII code point rather than taking the argument on trust.
|
|
59
|
-
//
|
|
60
|
-
// Worth the branch: this runs once per cell per filter - 100,000 times for a
|
|
61
|
-
// one-column filter over a 100k grid - and a CPU profile of a browser filter
|
|
62
|
-
// put more time in this function than in any other.
|
|
63
|
-
//
|
|
64
|
-
// Note the locale still routes through toLocaleLowerCase even on the fast
|
|
65
|
-
// path: 'I'.toLocaleLowerCase('tr') is 'ı', so ASCII does NOT imply the
|
|
66
|
-
// locale can be ignored.
|
|
67
|
-
if (isAsciiOnly(s)) {
|
|
68
|
-
return locale ? s.toLocaleLowerCase(locale as string | string[]) : s.toLowerCase()
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const stripped = s.normalize('NFD').replace(DIACRITIC_RE, '')
|
|
72
|
-
return locale
|
|
73
|
-
? stripped.toLocaleLowerCase(locale as string | string[])
|
|
74
|
-
: stripped.toLowerCase()
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* True when every code unit is ASCII. A plain char-code scan, which is far
|
|
79
|
-
* cheaper than the `normalize()` + regex pass it lets us skip.
|
|
80
|
-
*/
|
|
81
|
-
function isAsciiOnly(s: string): boolean {
|
|
82
|
-
for (let i = 0; i < s.length; i++) {
|
|
83
|
-
if (s.charCodeAt(i) > 127) return false
|
|
84
|
-
}
|
|
85
|
-
return true
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Delimiter used to serialise the `in` / `notIn` value list into the single
|
|
90
|
-
* `value` string that the filter model carries.
|
|
91
|
-
*
|
|
92
|
-
* This has to be single-line safe: the tool panel and the filter menu show the
|
|
93
|
-
* serialised list in a plain `<input type="text">`, and the HTML input value
|
|
94
|
-
* sanitiser STRIPS newlines - so a newline-joined list came back out of the DOM
|
|
95
|
-
* as one run-together token. `splitInTokens` accepts newlines too, so values
|
|
96
|
-
* serialised by older builds still parse.
|
|
97
|
-
*/
|
|
98
|
-
export const IN_TOKEN_SEP = ', '
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Split a serialised `in` / `notIn` value into its individual tokens.
|
|
102
|
-
*
|
|
103
|
-
* The grammar is CSV-shaped: comma OR newline separates, and a token may be
|
|
104
|
-
* double-quoted to carry a separator literally (`""` escapes a quote inside
|
|
105
|
-
* one). Quoting is what makes a comma separator safe - facet values commonly
|
|
106
|
-
* contain commas of their own, because numeric bucket labels are built with
|
|
107
|
-
* `toLocaleString` ("1,234 - 5,678") and date labels with a short month
|
|
108
|
-
* ("Aug 17, 2026"). A quote only opens a token when it is the token's first
|
|
109
|
-
* non-space character, so an unquoted `5" pipe` stays literal.
|
|
110
|
-
*/
|
|
111
|
-
export function splitInTokens(value: unknown): string[] {
|
|
112
|
-
const { tokens, trailing } = scanInTokens(value)
|
|
113
|
-
if (trailing) tokens.push(trailing)
|
|
114
|
-
return tokens
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Serialise a list of tokens back into the single `in` / `notIn` value,
|
|
119
|
-
* quoting any token that carries a separator or a quote of its own.
|
|
120
|
-
*/
|
|
121
|
-
export function joinInTokens(tokens: ReadonlyArray<string>): string {
|
|
122
|
-
return tokens
|
|
123
|
-
.map((t) => t.trim())
|
|
124
|
-
.filter((t) => t.length > 0)
|
|
125
|
-
.map((t) => (/["\n,]/.test(t) ? `"${t.replace(/"/g, '""')}"` : t))
|
|
126
|
-
.join(IN_TOKEN_SEP)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* The token still being typed at the end of an `in` / `notIn` value - the text
|
|
131
|
-
* after the last separator. `'AAPL, MS'` -> `'MS'`; a value ending in a
|
|
132
|
-
* separator (or empty) has no trailing token.
|
|
133
|
-
*
|
|
134
|
-
* Inputs that hold the whole token list use this to drive the value-suggestion
|
|
135
|
-
* dropdown: the trailing fragment is the search query, not a committed token.
|
|
136
|
-
*/
|
|
137
|
-
export function trailingInToken(value: unknown): string {
|
|
138
|
-
return scanInTokens(value).trailing
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* One pass over a serialised `in` / `notIn` value, splitting it into the
|
|
143
|
-
* tokens that are terminated by a separator and the unterminated fragment at
|
|
144
|
-
* the end. Every token helper above is a view onto this, so they can never
|
|
145
|
-
* disagree about where a quoted token starts or ends.
|
|
146
|
-
*/
|
|
147
|
-
function scanInTokens(value: unknown): { tokens: string[]; trailing: string } {
|
|
148
|
-
const raw = String(value ?? '')
|
|
149
|
-
const tokens: string[] = []
|
|
150
|
-
let buf = ''
|
|
151
|
-
let quoted = false
|
|
152
|
-
let wasQuoted = false
|
|
153
|
-
let closed = false
|
|
154
|
-
|
|
155
|
-
// A quoted token keeps its inner spacing; an unquoted one is trimmed.
|
|
156
|
-
const take = () => {
|
|
157
|
-
const t = wasQuoted ? buf : buf.trim()
|
|
158
|
-
buf = ''
|
|
159
|
-
wasQuoted = false
|
|
160
|
-
closed = false
|
|
161
|
-
return t
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
for (let i = 0; i < raw.length; i += 1) {
|
|
165
|
-
const ch = raw[i]
|
|
166
|
-
if (quoted) {
|
|
167
|
-
if (ch !== '"') { buf += ch; continue }
|
|
168
|
-
// A doubled quote is an escaped literal quote, not the closing one.
|
|
169
|
-
if (raw[i + 1] === '"') { buf += '"'; i += 1; continue }
|
|
170
|
-
quoted = false
|
|
171
|
-
closed = true
|
|
172
|
-
continue
|
|
173
|
-
}
|
|
174
|
-
if (ch === ',' || ch === '\n') {
|
|
175
|
-
const t = take()
|
|
176
|
-
if (t.length) tokens.push(t)
|
|
177
|
-
continue
|
|
178
|
-
}
|
|
179
|
-
// Only opens a quoted token at the token's start - `5" pipe` is literal.
|
|
180
|
-
if (ch === '"' && !closed && !buf.trim()) {
|
|
181
|
-
quoted = true
|
|
182
|
-
wasQuoted = true
|
|
183
|
-
buf = ''
|
|
184
|
-
continue
|
|
185
|
-
}
|
|
186
|
-
buf += ch
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return { tokens, trailing: take() }
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Re-exported here so consumers can reach the catalogue through the
|
|
193
|
-
// `@svgrid/grid/filtering` subpath. Importing it from the package root would
|
|
194
|
-
// pull in SvGrid.svelte, which is a runtime cost (and a bundler hazard) for
|
|
195
|
-
// anything that only wants operator semantics.
|
|
196
|
-
export {
|
|
197
|
-
ALL_FILTER_OPERATORS,
|
|
198
|
-
SET_OPERATOR_IDS,
|
|
199
|
-
VALUELESS_OPERATOR_IDS,
|
|
200
|
-
RANGE_OPERATOR_IDS,
|
|
201
|
-
isSetOperator,
|
|
202
|
-
isValuelessOperator,
|
|
203
|
-
isRangeOperator,
|
|
204
|
-
type FilterValueType,
|
|
205
|
-
} from './filter-operator-catalogue.js'
|
|
206
|
-
|
|
207
|
-
/** A filter with its needle-side work already done. Call per row. */
|
|
208
|
-
export type CompiledExcelFilter = (cellValue: unknown) => boolean
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Compile a filter once, then test many rows against it.
|
|
212
|
-
*
|
|
213
|
-
* Everything that depends only on the FILTER - folding the needle, splitting
|
|
214
|
-
* `in` tokens, building the regex, coercing range endpoints - happens here,
|
|
215
|
-
* so the per-row closure does the minimum. Filtering 100k rows used to redo
|
|
216
|
-
* all of it 100k times.
|
|
217
|
-
*
|
|
218
|
-
* `applyExcelFilter` is defined in terms of this, so there is exactly one
|
|
219
|
-
* copy of the operator semantics.
|
|
220
|
-
*/
|
|
221
|
-
export function compileExcelFilter(
|
|
222
|
-
filter: ExcelFilter,
|
|
223
|
-
options?: ExcelFilterOptions,
|
|
224
|
-
): CompiledExcelFilter {
|
|
225
|
-
const locale = options?.locale
|
|
226
|
-
const fold = (v: unknown) => normalizeForFilter(String(v ?? ''), locale)
|
|
227
|
-
const needle = fold(filter.value)
|
|
228
|
-
|
|
229
|
-
switch (filter.operator) {
|
|
230
|
-
case 'contains':
|
|
231
|
-
return (cellValue) => fold(cellValue).includes(needle)
|
|
232
|
-
case 'notContains':
|
|
233
|
-
// An empty needle is no constraint (mirrors `contains` returning true),
|
|
234
|
-
// so nothing is excluded until the user types something.
|
|
235
|
-
if (needle === '') return () => true
|
|
236
|
-
return (cellValue) => !fold(cellValue).includes(needle)
|
|
237
|
-
case 'equals':
|
|
238
|
-
return (cellValue) => fold(cellValue) === needle
|
|
239
|
-
case 'notEquals':
|
|
240
|
-
if (needle === '') return () => true
|
|
241
|
-
return (cellValue) => fold(cellValue) !== needle
|
|
242
|
-
case 'startsWith':
|
|
243
|
-
return (cellValue) => fold(cellValue).startsWith(needle)
|
|
244
|
-
case 'endsWith':
|
|
245
|
-
return (cellValue) => fold(cellValue).endsWith(needle)
|
|
246
|
-
case 'regex': {
|
|
247
|
-
// Case-insensitive by default (matches the accent/case-folded feel of
|
|
248
|
-
// the other text operators). An invalid pattern matches nothing rather
|
|
249
|
-
// than throwing, so a half-typed regex never crashes the row model.
|
|
250
|
-
// Note this tests the RAW text, not the folded text.
|
|
251
|
-
const pattern = String(filter.value ?? '')
|
|
252
|
-
if (!pattern) return () => true
|
|
253
|
-
let re: RegExp
|
|
254
|
-
try {
|
|
255
|
-
re = new RegExp(pattern, 'i')
|
|
256
|
-
} catch {
|
|
257
|
-
return () => false
|
|
258
|
-
}
|
|
259
|
-
return (cellValue) => re.test(String(cellValue ?? ''))
|
|
260
|
-
}
|
|
261
|
-
case 'in':
|
|
262
|
-
case 'notIn': {
|
|
263
|
-
const tokens = splitInTokens(filter.value)
|
|
264
|
-
if (tokens.length === 0) return () => true
|
|
265
|
-
// A Set of pre-folded tokens turns the old per-row `tokens.some(...)`
|
|
266
|
-
// (which re-folded every token for every row) into one lookup.
|
|
267
|
-
const wanted = new Set(tokens.map((t) => normalizeForFilter(t, locale)))
|
|
268
|
-
if (filter.operator === 'in') return (cellValue) => wanted.has(fold(cellValue))
|
|
269
|
-
return (cellValue) => !wanted.has(fold(cellValue))
|
|
270
|
-
}
|
|
271
|
-
case 'greaterThan': {
|
|
272
|
-
const b = Number(filter.value)
|
|
273
|
-
const bFinite = Number.isFinite(b)
|
|
274
|
-
const bText = String(filter.value ?? '')
|
|
275
|
-
return (cellValue) => {
|
|
276
|
-
const a = Number(cellValue)
|
|
277
|
-
if (bFinite && Number.isFinite(a)) return a > b
|
|
278
|
-
return String(cellValue ?? '') > bText
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
case 'lessThan': {
|
|
282
|
-
const b = Number(filter.value)
|
|
283
|
-
const bFinite = Number.isFinite(b)
|
|
284
|
-
const bText = String(filter.value ?? '')
|
|
285
|
-
return (cellValue) => {
|
|
286
|
-
const a = Number(cellValue)
|
|
287
|
-
if (bFinite && Number.isFinite(a)) return a < b
|
|
288
|
-
return String(cellValue ?? '') < bText
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
case 'between': {
|
|
292
|
-
// Two paths: numeric (coerce both endpoints with the historical
|
|
293
|
-
// `?? 0` fallback) and string (used when EITHER endpoint is a
|
|
294
|
-
// non-numeric string like an ISO date). Numeric wins when its
|
|
295
|
-
// operands are finite so callers passing numeric ages, prices, etc.
|
|
296
|
-
// see the historical inclusive-range behaviour.
|
|
297
|
-
const lo = filter.value == null ? 0 : Number(filter.value)
|
|
298
|
-
const hi = filter.valueTo == null ? 0 : Number(filter.valueTo)
|
|
299
|
-
const rangeFinite = Number.isFinite(lo) && Number.isFinite(hi)
|
|
300
|
-
// Either endpoint is non-numeric (ISO date string, etc). Compare
|
|
301
|
-
// lexicographically - YYYY-MM-DD orders chronologically so the
|
|
302
|
-
// result matches user intent.
|
|
303
|
-
const sl = String(filter.value ?? '')
|
|
304
|
-
const sh = String(filter.valueTo ?? '')
|
|
305
|
-
return (cellValue) => {
|
|
306
|
-
const a = Number(cellValue ?? 0)
|
|
307
|
-
if (rangeFinite && Number.isFinite(a)) return a >= lo && a <= hi
|
|
308
|
-
const s = String(cellValue ?? '')
|
|
309
|
-
return s >= sl && s <= sh
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
case 'isBlank':
|
|
313
|
-
return (cellValue) => String(cellValue ?? '').trim().length === 0
|
|
314
|
-
case 'isNotBlank':
|
|
315
|
-
return (cellValue) => String(cellValue ?? '').trim().length > 0
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export function applyExcelFilter(
|
|
320
|
-
cellValue: unknown,
|
|
321
|
-
filter: ExcelFilter,
|
|
322
|
-
options?: ExcelFilterOptions,
|
|
323
|
-
) {
|
|
324
|
-
return compileExcelFilter(filter, options)(cellValue)
|
|
325
|
-
}
|
|
1
|
+
export type ExcelFilterOperator =
|
|
2
|
+
| 'contains'
|
|
3
|
+
| 'notContains'
|
|
4
|
+
| 'equals'
|
|
5
|
+
| 'notEquals'
|
|
6
|
+
| 'startsWith'
|
|
7
|
+
| 'endsWith'
|
|
8
|
+
| 'regex'
|
|
9
|
+
| 'in'
|
|
10
|
+
| 'notIn'
|
|
11
|
+
| 'greaterThan'
|
|
12
|
+
| 'lessThan'
|
|
13
|
+
| 'between'
|
|
14
|
+
| 'isBlank'
|
|
15
|
+
| 'isNotBlank'
|
|
16
|
+
|
|
17
|
+
export type ExcelFilter = {
|
|
18
|
+
id: string
|
|
19
|
+
operator: ExcelFilterOperator
|
|
20
|
+
value?: unknown
|
|
21
|
+
valueTo?: unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ExcelFilterOptions = {
|
|
25
|
+
/**
|
|
26
|
+
* BCP-47 locale tag (or fallback list) for accent- and case-
|
|
27
|
+
* insensitive text comparison. When set, "café", "Café" and "CAFÉ"
|
|
28
|
+
* all match "cafe" without strain.
|
|
29
|
+
*
|
|
30
|
+
* Implementation: NFD-decompose both sides, strip combining marks
|
|
31
|
+
* (diacritics), then locale-aware lowercase. This is the standard
|
|
32
|
+
* "lowercase + asciifolding" pipeline used by Elasticsearch, Postgres
|
|
33
|
+
* (with `unaccent`), and Lucene. Cheap, deterministic, no Collator
|
|
34
|
+
* round-trip.
|
|
35
|
+
*/
|
|
36
|
+
locale?: string | ReadonlyArray<string>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// U+0300..U+036F is the Combining Diacritical Marks block. After NFD
|
|
40
|
+
// decomposition accented characters become "base char + combining mark";
|
|
41
|
+
// stripping the marks leaves the bare ASCII base.
|
|
42
|
+
const DIACRITIC_RE = /[̀-ͯ]/g
|
|
43
|
+
|
|
44
|
+
/** NFD-decompose, strip combining marks (diacritics), then locale-aware
|
|
45
|
+
* lowercase. The locale-aware lowercasing handles Turkish dotted-I /
|
|
46
|
+
* dotless-i correctly when the consumer threads `"tr"` through. */
|
|
47
|
+
export function normalizeForFilter(
|
|
48
|
+
s: string,
|
|
49
|
+
locale?: string | ReadonlyArray<string>,
|
|
50
|
+
): string {
|
|
51
|
+
if (!s) return ''
|
|
52
|
+
|
|
53
|
+
// Fast path for pure-ASCII input, which is the overwhelming majority of what
|
|
54
|
+
// a grid filters. Both of the expensive steps are provably no-ops there:
|
|
55
|
+
// ASCII is already NFD-normalised, and the combining-marks block this strips
|
|
56
|
+
// (U+0300..U+036F) contains no ASCII. So lowercasing alone is the same
|
|
57
|
+
// answer, and `normalize-fast-path.test.ts` checks that exhaustively over
|
|
58
|
+
// every ASCII code point rather than taking the argument on trust.
|
|
59
|
+
//
|
|
60
|
+
// Worth the branch: this runs once per cell per filter - 100,000 times for a
|
|
61
|
+
// one-column filter over a 100k grid - and a CPU profile of a browser filter
|
|
62
|
+
// put more time in this function than in any other.
|
|
63
|
+
//
|
|
64
|
+
// Note the locale still routes through toLocaleLowerCase even on the fast
|
|
65
|
+
// path: 'I'.toLocaleLowerCase('tr') is 'ı', so ASCII does NOT imply the
|
|
66
|
+
// locale can be ignored.
|
|
67
|
+
if (isAsciiOnly(s)) {
|
|
68
|
+
return locale ? s.toLocaleLowerCase(locale as string | string[]) : s.toLowerCase()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const stripped = s.normalize('NFD').replace(DIACRITIC_RE, '')
|
|
72
|
+
return locale
|
|
73
|
+
? stripped.toLocaleLowerCase(locale as string | string[])
|
|
74
|
+
: stripped.toLowerCase()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* True when every code unit is ASCII. A plain char-code scan, which is far
|
|
79
|
+
* cheaper than the `normalize()` + regex pass it lets us skip.
|
|
80
|
+
*/
|
|
81
|
+
function isAsciiOnly(s: string): boolean {
|
|
82
|
+
for (let i = 0; i < s.length; i++) {
|
|
83
|
+
if (s.charCodeAt(i) > 127) return false
|
|
84
|
+
}
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Delimiter used to serialise the `in` / `notIn` value list into the single
|
|
90
|
+
* `value` string that the filter model carries.
|
|
91
|
+
*
|
|
92
|
+
* This has to be single-line safe: the tool panel and the filter menu show the
|
|
93
|
+
* serialised list in a plain `<input type="text">`, and the HTML input value
|
|
94
|
+
* sanitiser STRIPS newlines - so a newline-joined list came back out of the DOM
|
|
95
|
+
* as one run-together token. `splitInTokens` accepts newlines too, so values
|
|
96
|
+
* serialised by older builds still parse.
|
|
97
|
+
*/
|
|
98
|
+
export const IN_TOKEN_SEP = ', '
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Split a serialised `in` / `notIn` value into its individual tokens.
|
|
102
|
+
*
|
|
103
|
+
* The grammar is CSV-shaped: comma OR newline separates, and a token may be
|
|
104
|
+
* double-quoted to carry a separator literally (`""` escapes a quote inside
|
|
105
|
+
* one). Quoting is what makes a comma separator safe - facet values commonly
|
|
106
|
+
* contain commas of their own, because numeric bucket labels are built with
|
|
107
|
+
* `toLocaleString` ("1,234 - 5,678") and date labels with a short month
|
|
108
|
+
* ("Aug 17, 2026"). A quote only opens a token when it is the token's first
|
|
109
|
+
* non-space character, so an unquoted `5" pipe` stays literal.
|
|
110
|
+
*/
|
|
111
|
+
export function splitInTokens(value: unknown): string[] {
|
|
112
|
+
const { tokens, trailing } = scanInTokens(value)
|
|
113
|
+
if (trailing) tokens.push(trailing)
|
|
114
|
+
return tokens
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Serialise a list of tokens back into the single `in` / `notIn` value,
|
|
119
|
+
* quoting any token that carries a separator or a quote of its own.
|
|
120
|
+
*/
|
|
121
|
+
export function joinInTokens(tokens: ReadonlyArray<string>): string {
|
|
122
|
+
return tokens
|
|
123
|
+
.map((t) => t.trim())
|
|
124
|
+
.filter((t) => t.length > 0)
|
|
125
|
+
.map((t) => (/["\n,]/.test(t) ? `"${t.replace(/"/g, '""')}"` : t))
|
|
126
|
+
.join(IN_TOKEN_SEP)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The token still being typed at the end of an `in` / `notIn` value - the text
|
|
131
|
+
* after the last separator. `'AAPL, MS'` -> `'MS'`; a value ending in a
|
|
132
|
+
* separator (or empty) has no trailing token.
|
|
133
|
+
*
|
|
134
|
+
* Inputs that hold the whole token list use this to drive the value-suggestion
|
|
135
|
+
* dropdown: the trailing fragment is the search query, not a committed token.
|
|
136
|
+
*/
|
|
137
|
+
export function trailingInToken(value: unknown): string {
|
|
138
|
+
return scanInTokens(value).trailing
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* One pass over a serialised `in` / `notIn` value, splitting it into the
|
|
143
|
+
* tokens that are terminated by a separator and the unterminated fragment at
|
|
144
|
+
* the end. Every token helper above is a view onto this, so they can never
|
|
145
|
+
* disagree about where a quoted token starts or ends.
|
|
146
|
+
*/
|
|
147
|
+
function scanInTokens(value: unknown): { tokens: string[]; trailing: string } {
|
|
148
|
+
const raw = String(value ?? '')
|
|
149
|
+
const tokens: string[] = []
|
|
150
|
+
let buf = ''
|
|
151
|
+
let quoted = false
|
|
152
|
+
let wasQuoted = false
|
|
153
|
+
let closed = false
|
|
154
|
+
|
|
155
|
+
// A quoted token keeps its inner spacing; an unquoted one is trimmed.
|
|
156
|
+
const take = () => {
|
|
157
|
+
const t = wasQuoted ? buf : buf.trim()
|
|
158
|
+
buf = ''
|
|
159
|
+
wasQuoted = false
|
|
160
|
+
closed = false
|
|
161
|
+
return t
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
165
|
+
const ch = raw[i]
|
|
166
|
+
if (quoted) {
|
|
167
|
+
if (ch !== '"') { buf += ch; continue }
|
|
168
|
+
// A doubled quote is an escaped literal quote, not the closing one.
|
|
169
|
+
if (raw[i + 1] === '"') { buf += '"'; i += 1; continue }
|
|
170
|
+
quoted = false
|
|
171
|
+
closed = true
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
if (ch === ',' || ch === '\n') {
|
|
175
|
+
const t = take()
|
|
176
|
+
if (t.length) tokens.push(t)
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
// Only opens a quoted token at the token's start - `5" pipe` is literal.
|
|
180
|
+
if (ch === '"' && !closed && !buf.trim()) {
|
|
181
|
+
quoted = true
|
|
182
|
+
wasQuoted = true
|
|
183
|
+
buf = ''
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
buf += ch
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { tokens, trailing: take() }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Re-exported here so consumers can reach the catalogue through the
|
|
193
|
+
// `@svgrid/grid/filtering` subpath. Importing it from the package root would
|
|
194
|
+
// pull in SvGrid.svelte, which is a runtime cost (and a bundler hazard) for
|
|
195
|
+
// anything that only wants operator semantics.
|
|
196
|
+
export {
|
|
197
|
+
ALL_FILTER_OPERATORS,
|
|
198
|
+
SET_OPERATOR_IDS,
|
|
199
|
+
VALUELESS_OPERATOR_IDS,
|
|
200
|
+
RANGE_OPERATOR_IDS,
|
|
201
|
+
isSetOperator,
|
|
202
|
+
isValuelessOperator,
|
|
203
|
+
isRangeOperator,
|
|
204
|
+
type FilterValueType,
|
|
205
|
+
} from './filter-operator-catalogue.js'
|
|
206
|
+
|
|
207
|
+
/** A filter with its needle-side work already done. Call per row. */
|
|
208
|
+
export type CompiledExcelFilter = (cellValue: unknown) => boolean
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Compile a filter once, then test many rows against it.
|
|
212
|
+
*
|
|
213
|
+
* Everything that depends only on the FILTER - folding the needle, splitting
|
|
214
|
+
* `in` tokens, building the regex, coercing range endpoints - happens here,
|
|
215
|
+
* so the per-row closure does the minimum. Filtering 100k rows used to redo
|
|
216
|
+
* all of it 100k times.
|
|
217
|
+
*
|
|
218
|
+
* `applyExcelFilter` is defined in terms of this, so there is exactly one
|
|
219
|
+
* copy of the operator semantics.
|
|
220
|
+
*/
|
|
221
|
+
export function compileExcelFilter(
|
|
222
|
+
filter: ExcelFilter,
|
|
223
|
+
options?: ExcelFilterOptions,
|
|
224
|
+
): CompiledExcelFilter {
|
|
225
|
+
const locale = options?.locale
|
|
226
|
+
const fold = (v: unknown) => normalizeForFilter(String(v ?? ''), locale)
|
|
227
|
+
const needle = fold(filter.value)
|
|
228
|
+
|
|
229
|
+
switch (filter.operator) {
|
|
230
|
+
case 'contains':
|
|
231
|
+
return (cellValue) => fold(cellValue).includes(needle)
|
|
232
|
+
case 'notContains':
|
|
233
|
+
// An empty needle is no constraint (mirrors `contains` returning true),
|
|
234
|
+
// so nothing is excluded until the user types something.
|
|
235
|
+
if (needle === '') return () => true
|
|
236
|
+
return (cellValue) => !fold(cellValue).includes(needle)
|
|
237
|
+
case 'equals':
|
|
238
|
+
return (cellValue) => fold(cellValue) === needle
|
|
239
|
+
case 'notEquals':
|
|
240
|
+
if (needle === '') return () => true
|
|
241
|
+
return (cellValue) => fold(cellValue) !== needle
|
|
242
|
+
case 'startsWith':
|
|
243
|
+
return (cellValue) => fold(cellValue).startsWith(needle)
|
|
244
|
+
case 'endsWith':
|
|
245
|
+
return (cellValue) => fold(cellValue).endsWith(needle)
|
|
246
|
+
case 'regex': {
|
|
247
|
+
// Case-insensitive by default (matches the accent/case-folded feel of
|
|
248
|
+
// the other text operators). An invalid pattern matches nothing rather
|
|
249
|
+
// than throwing, so a half-typed regex never crashes the row model.
|
|
250
|
+
// Note this tests the RAW text, not the folded text.
|
|
251
|
+
const pattern = String(filter.value ?? '')
|
|
252
|
+
if (!pattern) return () => true
|
|
253
|
+
let re: RegExp
|
|
254
|
+
try {
|
|
255
|
+
re = new RegExp(pattern, 'i')
|
|
256
|
+
} catch {
|
|
257
|
+
return () => false
|
|
258
|
+
}
|
|
259
|
+
return (cellValue) => re.test(String(cellValue ?? ''))
|
|
260
|
+
}
|
|
261
|
+
case 'in':
|
|
262
|
+
case 'notIn': {
|
|
263
|
+
const tokens = splitInTokens(filter.value)
|
|
264
|
+
if (tokens.length === 0) return () => true
|
|
265
|
+
// A Set of pre-folded tokens turns the old per-row `tokens.some(...)`
|
|
266
|
+
// (which re-folded every token for every row) into one lookup.
|
|
267
|
+
const wanted = new Set(tokens.map((t) => normalizeForFilter(t, locale)))
|
|
268
|
+
if (filter.operator === 'in') return (cellValue) => wanted.has(fold(cellValue))
|
|
269
|
+
return (cellValue) => !wanted.has(fold(cellValue))
|
|
270
|
+
}
|
|
271
|
+
case 'greaterThan': {
|
|
272
|
+
const b = Number(filter.value)
|
|
273
|
+
const bFinite = Number.isFinite(b)
|
|
274
|
+
const bText = String(filter.value ?? '')
|
|
275
|
+
return (cellValue) => {
|
|
276
|
+
const a = Number(cellValue)
|
|
277
|
+
if (bFinite && Number.isFinite(a)) return a > b
|
|
278
|
+
return String(cellValue ?? '') > bText
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
case 'lessThan': {
|
|
282
|
+
const b = Number(filter.value)
|
|
283
|
+
const bFinite = Number.isFinite(b)
|
|
284
|
+
const bText = String(filter.value ?? '')
|
|
285
|
+
return (cellValue) => {
|
|
286
|
+
const a = Number(cellValue)
|
|
287
|
+
if (bFinite && Number.isFinite(a)) return a < b
|
|
288
|
+
return String(cellValue ?? '') < bText
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
case 'between': {
|
|
292
|
+
// Two paths: numeric (coerce both endpoints with the historical
|
|
293
|
+
// `?? 0` fallback) and string (used when EITHER endpoint is a
|
|
294
|
+
// non-numeric string like an ISO date). Numeric wins when its
|
|
295
|
+
// operands are finite so callers passing numeric ages, prices, etc.
|
|
296
|
+
// see the historical inclusive-range behaviour.
|
|
297
|
+
const lo = filter.value == null ? 0 : Number(filter.value)
|
|
298
|
+
const hi = filter.valueTo == null ? 0 : Number(filter.valueTo)
|
|
299
|
+
const rangeFinite = Number.isFinite(lo) && Number.isFinite(hi)
|
|
300
|
+
// Either endpoint is non-numeric (ISO date string, etc). Compare
|
|
301
|
+
// lexicographically - YYYY-MM-DD orders chronologically so the
|
|
302
|
+
// result matches user intent.
|
|
303
|
+
const sl = String(filter.value ?? '')
|
|
304
|
+
const sh = String(filter.valueTo ?? '')
|
|
305
|
+
return (cellValue) => {
|
|
306
|
+
const a = Number(cellValue ?? 0)
|
|
307
|
+
if (rangeFinite && Number.isFinite(a)) return a >= lo && a <= hi
|
|
308
|
+
const s = String(cellValue ?? '')
|
|
309
|
+
return s >= sl && s <= sh
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
case 'isBlank':
|
|
313
|
+
return (cellValue) => String(cellValue ?? '').trim().length === 0
|
|
314
|
+
case 'isNotBlank':
|
|
315
|
+
return (cellValue) => String(cellValue ?? '').trim().length > 0
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function applyExcelFilter(
|
|
320
|
+
cellValue: unknown,
|
|
321
|
+
filter: ExcelFilter,
|
|
322
|
+
options?: ExcelFilterOptions,
|
|
323
|
+
) {
|
|
324
|
+
return compileExcelFilter(filter, options)(cellValue)
|
|
325
|
+
}
|
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'
|