@ibobbyts/svelte-ui-utils 0.4.6 → 0.5.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/README.md +37 -125
- package/dist/data-table/DateRangeFilter.svelte +119 -47
- package/dist/data-table/DateRangeFilter.svelte.d.ts +4 -3
- package/dist/data-table/DateRangeFilter.svelte.d.ts.map +1 -1
- package/dist/data-table/FilterControl.svelte +136 -27
- package/dist/data-table/FilterControl.svelte.d.ts.map +1 -1
- package/dist/data-table/date-range.d.ts +1 -0
- package/dist/data-table/date-range.d.ts.map +1 -1
- package/dist/data-table/date-range.js +12 -0
- package/dist/data-table/index.d.ts +2 -2
- package/dist/data-table/index.d.ts.map +1 -1
- package/dist/data-table/index.js +1 -1
- package/dist/data-table/types.d.ts +76 -4
- package/dist/data-table/types.d.ts.map +1 -1
- package/dist/dropdown/Dropdown.svelte +27 -11
- package/dist/dropdown/Dropdown.svelte.d.ts +1 -1
- package/dist/dropdown/Dropdown.svelte.d.ts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/style.css +12 -282
- package/package.json +4 -12
- package/dist/dropdown-search/DropdownSearch.svelte +0 -725
- package/dist/dropdown-search/DropdownSearch.svelte.d.ts +0 -62
- package/dist/dropdown-search/DropdownSearch.svelte.d.ts.map +0 -1
- package/dist/dropdown-search/DropdownSearchMultiSelect.svelte +0 -94
- package/dist/dropdown-search/DropdownSearchMultiSelect.svelte.d.ts +0 -54
- package/dist/dropdown-search/DropdownSearchMultiSelect.svelte.d.ts.map +0 -1
- package/dist/dropdown-search/index.d.ts +0 -7
- package/dist/dropdown-search/index.d.ts.map +0 -1
- package/dist/dropdown-search/index.js +0 -4
- package/dist/dropdown-search/state.d.ts +0 -17
- package/dist/dropdown-search/state.d.ts.map +0 -1
- package/dist/dropdown-search/state.js +0 -51
- package/dist/dropdown-search/types.d.ts +0 -39
- package/dist/dropdown-search/types.d.ts.map +0 -1
- package/dist/dropdown-search/types.js +0 -1
package/README.md
CHANGED
|
@@ -40,7 +40,6 @@ import '@ibobbyts/svelte-ui-utils/style.css';
|
|
|
40
40
|
<script lang="ts">
|
|
41
41
|
import { ToastManager, toast } from '@ibobbyts/svelte-ui-utils/toast';
|
|
42
42
|
import { Dropdown } from '@ibobbyts/svelte-ui-utils/dropdown';
|
|
43
|
-
import { DropdownSearch, DropdownSearchMultiSelect } from '@ibobbyts/svelte-ui-utils/dropdown-search';
|
|
44
43
|
import { Dialog, ConfirmDialog, InputDialog, CsvUploadDialog, ImagePreviewDialog, PasswordCopyDialog } from '@ibobbyts/svelte-ui-utils/dialog';
|
|
45
44
|
import { DataTable, DateRangeFilter, FilterTable, NumberRangeFilter } from '@ibobbyts/svelte-ui-utils/table';
|
|
46
45
|
</script>
|
|
@@ -49,7 +48,7 @@ import '@ibobbyts/svelte-ui-utils/style.css';
|
|
|
49
48
|
The package root also re-exports the public modules:
|
|
50
49
|
|
|
51
50
|
```ts
|
|
52
|
-
import { ToastManager, Dropdown,
|
|
51
|
+
import { ToastManager, Dropdown, Dialog, DataTable } from '@ibobbyts/svelte-ui-utils';
|
|
53
52
|
```
|
|
54
53
|
|
|
55
54
|
## Toast
|
|
@@ -85,103 +84,6 @@ moves. Set `placement="up"` or `placement="down"` to force a direction. Use
|
|
|
85
84
|
`fitViewport={false}` to disable the default panel-height constraint for the
|
|
86
85
|
chosen side.
|
|
87
86
|
|
|
88
|
-
## DropdownSearch
|
|
89
|
-
|
|
90
|
-
`DropdownSearch` is the free-text input entry with server-driven validation
|
|
91
|
-
(`exactMatch`), input statuses, focus options, footer text, a clear button, and
|
|
92
|
-
multiselect chips. For a trigger-style dropdown that combines single or multi
|
|
93
|
-
select with async search and dynamic result groups, use the unified `Dropdown`
|
|
94
|
-
search mode instead.
|
|
95
|
-
|
|
96
|
-
```svelte
|
|
97
|
-
<script lang="ts">
|
|
98
|
-
import { DropdownSearch } from '@ibobbyts/svelte-ui-utils/dropdown-search';
|
|
99
|
-
|
|
100
|
-
async function loadOptions(query, { limit, signal }) {
|
|
101
|
-
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}&limit=${limit}`, { signal });
|
|
102
|
-
return response.json();
|
|
103
|
-
}
|
|
104
|
-
</script>
|
|
105
|
-
|
|
106
|
-
<DropdownSearch
|
|
107
|
-
language="en_us"
|
|
108
|
-
placeholder="Search"
|
|
109
|
-
debounceMs={500}
|
|
110
|
-
clearLabel="Clear search"
|
|
111
|
-
width="24rem"
|
|
112
|
-
maxWidth="100%"
|
|
113
|
-
{loadOptions}
|
|
114
|
-
searchOnExternalValueChange={true}
|
|
115
|
-
closeOnValid={true}
|
|
116
|
-
/>
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
`loadOptions` returns `{ options, exactMatch }`. An item uses this shape:
|
|
120
|
-
|
|
121
|
-
```ts
|
|
122
|
-
{
|
|
123
|
-
value: '123',
|
|
124
|
-
label: 'Jane Doe',
|
|
125
|
-
param_dict: { ID: 'M-123' }
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
`label` is the display text and `value` is the submitted string identifier;
|
|
130
|
-
extra fields such as `param_dict` are preserved as business metadata. The
|
|
131
|
-
free-text `value` prop and `onChange` detail stay the raw input text, so they
|
|
132
|
-
never collide with the submitted `item.value`.
|
|
133
|
-
|
|
134
|
-
The input is valid when the server returns one unique `exactMatch`, or when the
|
|
135
|
-
user selects an item. Non-empty text without a unique match is invalid.
|
|
136
|
-
Set `validate={false}` when the field should only show selectable options and
|
|
137
|
-
stay visually neutral instead of turning green or red. In that mode the
|
|
138
|
-
component ignores `exactMatch` for status and auto-selection.
|
|
139
|
-
Use `searchOnExternalValueChange` for scanner or programmatic input workflows.
|
|
140
|
-
Set `closeOnValid={true}` to hide the result list after validation finds a
|
|
141
|
-
usable exact match. The default is `false`, so existing validated searches keep
|
|
142
|
-
showing their current result list until the field closes or the user selects an
|
|
143
|
-
option.
|
|
144
|
-
Use `showOptionsOnFocus={true}` with `focusOptions` to show a controlled set of
|
|
145
|
-
options as soon as the field receives focus, including when the current value is
|
|
146
|
-
empty or already valid. Pass `footerText` to render a non-selectable note below
|
|
147
|
-
the options, separated by a divider.
|
|
148
|
-
When the input has text, `DropdownSearch` shows an internal clear button on the
|
|
149
|
-
right side of the field. Use `clearLabel` to localize that button's accessible
|
|
150
|
-
label, or use `language` to select the package default.
|
|
151
|
-
Use `width`, `minWidth`, and `maxWidth` to size the control directly when a
|
|
152
|
-
wrapper is not convenient.
|
|
153
|
-
Server-side code and Node tests that only need pure helpers should import from
|
|
154
|
-
`@ibobbyts/svelte-ui-utils/dropdown-search/state` so they do not load Svelte
|
|
155
|
-
component files.
|
|
156
|
-
|
|
157
|
-
Set `multiselect={true}` when the search box should collect multiple selected
|
|
158
|
-
items as chips. `DropdownSearchMultiSelect` is a convenience wrapper with the
|
|
159
|
-
same controlled contract:
|
|
160
|
-
|
|
161
|
-
```svelte
|
|
162
|
-
<script lang="ts">
|
|
163
|
-
import { DropdownSearchMultiSelect, type DropdownSearchItem } from '@ibobbyts/svelte-ui-utils/dropdown-search';
|
|
164
|
-
|
|
165
|
-
let selectedMembers: DropdownSearchItem[] = [];
|
|
166
|
-
</script>
|
|
167
|
-
|
|
168
|
-
<DropdownSearchMultiSelect
|
|
169
|
-
language="en_us"
|
|
170
|
-
placeholder="Search members"
|
|
171
|
-
selectedItems={selectedMembers}
|
|
172
|
-
selectedItemsLabel="Selected members"
|
|
173
|
-
removeSelectedItemLabel={(item) => `Remove ${item.label}`}
|
|
174
|
-
{loadOptions}
|
|
175
|
-
onSelectedItemsChange={(items) => {
|
|
176
|
-
selectedMembers = items;
|
|
177
|
-
}}
|
|
178
|
-
/>
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
In multiselect mode, the text input remains a search query. Selecting an item
|
|
182
|
-
adds or removes it from `selectedItems`, clears the query, and emits both
|
|
183
|
-
`onChange` and `onSelectedItemsChange`.
|
|
184
|
-
|
|
185
87
|
## SortableList
|
|
186
88
|
|
|
187
89
|
`SortableList` provides controlled vertical HTML5 drag-and-drop ordering. Each
|
|
@@ -417,8 +319,8 @@ the menu to its longest option while keeping the selected edge aligned.
|
|
|
417
319
|
`DataTable` uses this same component for its page-size picker.
|
|
418
320
|
|
|
419
321
|
Set `search={true}` with `loadOptions` to load remote options as the user
|
|
420
|
-
types. The default `
|
|
421
|
-
expanded menu. Set `
|
|
322
|
+
types. The default `inputStyle="dropdown"` keeps the search field inside the
|
|
323
|
+
expanded menu. Set `inputStyle="input"` to render a directly editable input;
|
|
422
324
|
results appear while it is focused and hide on blur. In input style, the
|
|
423
325
|
optional `getItemLabel` callback controls the displayed label while `value`
|
|
424
326
|
and `onChange` continue to represent submitted option values. The input style
|
|
@@ -766,10 +668,8 @@ for the left column and a controlled filter created with the `filter` helper:
|
|
|
766
668
|
filter.dropdownSearch({
|
|
767
669
|
value: searchValue,
|
|
768
670
|
selectedItem,
|
|
769
|
-
status: searchStatus,
|
|
770
671
|
width: '24rem',
|
|
771
672
|
maxWidth: '100%',
|
|
772
|
-
clearLabel: 'Clear search',
|
|
773
673
|
loadOptions,
|
|
774
674
|
onChange: (detail) => updateSearch(detail)
|
|
775
675
|
}),
|
|
@@ -811,13 +711,6 @@ same menu, keyboard, and visual behavior as standalone dropdowns.
|
|
|
811
711
|
}
|
|
812
712
|
```
|
|
813
713
|
|
|
814
|
-
If a dropdown-style filter appears clipped, check the parent containers first.
|
|
815
|
-
`DropdownSearch` renders its result list as an absolutely positioned child, so
|
|
816
|
-
any ancestor with `overflow: hidden`, `overflow: auto`, or `overflow: scroll`
|
|
817
|
-
can clip the menu even when the menu has a high `z-index`. Keep the nearest
|
|
818
|
-
filter container at `overflow: visible`, or move the clipping/scrolling behavior
|
|
819
|
-
to a parent that does not wrap the dropdown menu directly.
|
|
820
|
-
|
|
821
714
|
`dateRange` renders two browser date inputs, preset buttons, and quick month/year
|
|
822
715
|
selects. The presets are `last 24 hours`, `last 7 days`, `last 30 days`,
|
|
823
716
|
`today`, `this week`, `this month`, and `this year`. Manual changes and quick
|
|
@@ -832,6 +725,28 @@ period, including days after today. The `last24Hours` preset also emits
|
|
|
832
725
|
a consuming app can run an exact timestamp query while still showing the covered
|
|
833
726
|
dates in the inputs.
|
|
834
727
|
|
|
728
|
+
Pass `presets` to fully control the preset row. Each entry is one of:
|
|
729
|
+
|
|
730
|
+
- a built-in preset key: `'last24Hours'`, `'last7Days'`, `'last30Days'`,
|
|
731
|
+
`'today'`, `'thisWeek'`, `'thisMonth'`, `'thisYear'`;
|
|
732
|
+
- a custom preset object `{ key, label?, resolve }`, where `resolve` receives
|
|
733
|
+
`{ now, weekStartsOn }` and returns `{ startDate, endDate }` dates that the
|
|
734
|
+
component formats into the value;
|
|
735
|
+
- a consumer-controlled select object `{ type: 'select', key, value, options,
|
|
736
|
+
ariaLabel?, onChange }`, rendered as a dropdown inside the preset row (for
|
|
737
|
+
example a year picker whose selection feeds custom preset resolvers); the
|
|
738
|
+
component only reports selection changes, the consumer owns the value;
|
|
739
|
+
- `'quickMonth'` or `'quickYear'` for the quick month/year selects;
|
|
740
|
+
- `'divider'` for a visual separator between entries.
|
|
741
|
+
|
|
742
|
+
When `presets` is omitted the default row is used: the seven built-in buttons
|
|
743
|
+
with dividers after `last 30 days` and `this year`, then both quick selects.
|
|
744
|
+
Custom preset keys are stored in `value.preset` as plain strings, so
|
|
745
|
+
`defaultPreset` and the active-button highlight work with them too. Custom
|
|
746
|
+
labels resolve from `label`, then `presetLabels[key]`, then the key itself.
|
|
747
|
+
Preset and select `key`s must be unique within one `presets` list; duplicate
|
|
748
|
+
keys make the active-state highlight and the `defaultPreset` lookup ambiguous.
|
|
749
|
+
|
|
835
750
|
`numberRange` renders min/max number inputs and supports `prefixLabel`, for
|
|
836
751
|
example `$` for currency filters.
|
|
837
752
|
|
|
@@ -928,21 +843,18 @@ calculations.
|
|
|
928
843
|
## Release
|
|
929
844
|
|
|
930
845
|
Do not publish this package as part of consumer-app local integration work.
|
|
931
|
-
Publish only when a release is explicitly requested.
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
npm
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
```
|
|
944
|
-
|
|
945
|
-
Never commit the npm token or copy it into repository settings.
|
|
846
|
+
Publish only when a release is explicitly requested.
|
|
847
|
+
|
|
848
|
+
Releases publish from CI via npm Trusted Publishing (OIDC): bump the version
|
|
849
|
+
in `package.json`, commit, then push a `vX.Y.Z` tag. That runs
|
|
850
|
+
[.github/workflows/npm-publish.yml](.github/workflows/npm-publish.yml), which
|
|
851
|
+
installs from the committed lockfile, runs `svelte-check` and `vitest`, and
|
|
852
|
+
publishes with no npm token in the environment — the runner's OIDC identity is
|
|
853
|
+
the credential (`id-token: write`). The tag must match `package.json`'s
|
|
854
|
+
version. The trusted publisher is linked on npmjs.com (user `iBobbyTS`, repo
|
|
855
|
+
`svelte-ui-utils`, workflow `npm-publish.yml`); the workflow filename is part
|
|
856
|
+
of that link and must not be renamed. `prepublishOnly` builds `dist/` before
|
|
857
|
+
the tarball is packed, so no publish path can ship stale output.
|
|
946
858
|
|
|
947
859
|
## Bun install verification
|
|
948
860
|
|
|
@@ -4,8 +4,14 @@
|
|
|
4
4
|
import { onMount } from 'svelte';
|
|
5
5
|
import Dropdown from '../dropdown/Dropdown.svelte';
|
|
6
6
|
import { getUiMessages, type UiLanguage } from '../i18n.js';
|
|
7
|
-
import { endOfMonth, formatDate, resolveDateRangePreset, startOfDay } from './date-range.js';
|
|
8
|
-
import type {
|
|
7
|
+
import { endOfMonth, formatDate, isDateRangePreset, resolveDateRangePreset, startOfDay } from './date-range.js';
|
|
8
|
+
import type {
|
|
9
|
+
CustomDateRangePreset,
|
|
10
|
+
DateRangeFilterValue,
|
|
11
|
+
DateRangePreset,
|
|
12
|
+
DateRangePresetEntry,
|
|
13
|
+
DateRangePresetSelect
|
|
14
|
+
} from './types.js';
|
|
9
15
|
|
|
10
16
|
export let value: DateRangeFilterValue = {
|
|
11
17
|
startDate: '',
|
|
@@ -15,23 +21,29 @@
|
|
|
15
21
|
export let language: UiLanguage = 'en_us';
|
|
16
22
|
export let startLabel: string | undefined = undefined;
|
|
17
23
|
export let endLabel: string | undefined = undefined;
|
|
18
|
-
export let
|
|
19
|
-
export let
|
|
24
|
+
export let presets: DateRangePresetEntry[] | undefined = undefined;
|
|
25
|
+
export let presetLabels: Partial<Record<string, string>> = {};
|
|
26
|
+
export let defaultPreset: string | undefined = undefined;
|
|
20
27
|
export let quickYears: number[] | undefined = undefined;
|
|
21
28
|
export let now: () => Date = () => new Date();
|
|
22
29
|
export let weekStartsOn: 0 | 1 = 1;
|
|
23
30
|
export let onChange: ((value: DateRangeFilterValue) => void) | undefined = undefined;
|
|
24
31
|
|
|
25
|
-
const
|
|
32
|
+
const defaultPresetEntries: DateRangePresetEntry[] = [
|
|
26
33
|
'last24Hours',
|
|
27
34
|
'last7Days',
|
|
28
35
|
'last30Days',
|
|
36
|
+
'divider',
|
|
29
37
|
'today',
|
|
30
38
|
'thisWeek',
|
|
31
39
|
'thisMonth',
|
|
32
|
-
'thisYear'
|
|
40
|
+
'thisYear',
|
|
41
|
+
'divider',
|
|
42
|
+
'quickMonth',
|
|
43
|
+
'quickYear'
|
|
33
44
|
];
|
|
34
45
|
const monthNumbers = Array.from({ length: 12 }, (_, index) => index + 1);
|
|
46
|
+
const structuralEntryKeys = new Set(['quickMonth', 'quickYear', 'divider']);
|
|
35
47
|
|
|
36
48
|
let quickYear = '';
|
|
37
49
|
let quickMonth = '';
|
|
@@ -39,6 +51,7 @@
|
|
|
39
51
|
$: messages = getUiMessages(language);
|
|
40
52
|
$: resolvedStartLabel = startLabel ?? messages.dateRange.startLabel;
|
|
41
53
|
$: resolvedEndLabel = endLabel ?? messages.dateRange.endLabel;
|
|
54
|
+
$: presetEntries = presets ?? defaultPresetEntries;
|
|
42
55
|
$: currentYear = startOfDay(now()).getFullYear();
|
|
43
56
|
$: quickYearOptions = quickYears ?? [currentYear - 1, currentYear, currentYear + 1];
|
|
44
57
|
$: quickMonthOptions = [
|
|
@@ -71,8 +84,38 @@
|
|
|
71
84
|
quickMonth = '';
|
|
72
85
|
}
|
|
73
86
|
|
|
74
|
-
function
|
|
75
|
-
return
|
|
87
|
+
function isSelectEntry(entry: DateRangePresetEntry): entry is DateRangePresetSelect {
|
|
88
|
+
return typeof entry === 'object' && 'type' in entry && entry.type === 'select';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isPresetButtonEntry(entry: DateRangePresetEntry): entry is DateRangePreset | CustomDateRangePreset {
|
|
92
|
+
if (typeof entry === 'string') {
|
|
93
|
+
return !structuralEntryKeys.has(entry);
|
|
94
|
+
}
|
|
95
|
+
return !isSelectEntry(entry);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function presetKeyOf(entry: DateRangePreset | CustomDateRangePreset): string {
|
|
99
|
+
return typeof entry === 'string' ? entry : entry.key;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolvePresetEntry(entry: DateRangePreset | CustomDateRangePreset): DateRangeFilterValue {
|
|
103
|
+
if (typeof entry === 'string') {
|
|
104
|
+
return resolveDateRangePreset(entry, now(), weekStartsOn);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const range = entry.resolve({ now: now(), weekStartsOn });
|
|
108
|
+
return {
|
|
109
|
+
startDate: formatDate(range.startDate),
|
|
110
|
+
endDate: formatDate(range.endDate),
|
|
111
|
+
preset: entry.key
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function findPresetEntryByKey(key: string): DateRangePreset | CustomDateRangePreset | undefined {
|
|
116
|
+
return presetEntries
|
|
117
|
+
.filter(isPresetButtonEntry)
|
|
118
|
+
.find((entry) => presetKeyOf(entry) === key);
|
|
76
119
|
}
|
|
77
120
|
|
|
78
121
|
function emit(next: DateRangeFilterValue) {
|
|
@@ -132,18 +175,22 @@
|
|
|
132
175
|
emit(resolveQuickRange(Number(quickYear), Number(nextValue)));
|
|
133
176
|
}
|
|
134
177
|
|
|
135
|
-
function
|
|
178
|
+
function applyPresetEntry(entry: DateRangePreset | CustomDateRangePreset) {
|
|
136
179
|
clearQuickSelection();
|
|
137
|
-
if (value.preset ===
|
|
180
|
+
if (value.preset === presetKeyOf(entry)) {
|
|
138
181
|
emit(emptyRange());
|
|
139
182
|
return;
|
|
140
183
|
}
|
|
141
184
|
|
|
142
|
-
emit(
|
|
185
|
+
emit(resolvePresetEntry(entry));
|
|
143
186
|
}
|
|
144
187
|
|
|
145
|
-
function labelFor(
|
|
146
|
-
|
|
188
|
+
function labelFor(entry: DateRangePreset | CustomDateRangePreset) {
|
|
189
|
+
if (typeof entry === 'string') {
|
|
190
|
+
return presetLabels[entry] ?? messages.dateRange.presetLabels[entry] ?? entry;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return entry.label ?? presetLabels[entry.key] ?? entry.key;
|
|
147
194
|
}
|
|
148
195
|
|
|
149
196
|
function monthLabel(month: number) {
|
|
@@ -151,8 +198,14 @@
|
|
|
151
198
|
}
|
|
152
199
|
|
|
153
200
|
onMount(() => {
|
|
154
|
-
if (defaultPreset
|
|
155
|
-
|
|
201
|
+
if (!defaultPreset || !isEmptyRange(value)) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const entry = findPresetEntryByKey(defaultPreset) ??
|
|
206
|
+
(isDateRangePreset(defaultPreset) ? defaultPreset : undefined);
|
|
207
|
+
if (entry) {
|
|
208
|
+
emit(resolvePresetEntry(entry));
|
|
156
209
|
}
|
|
157
210
|
});
|
|
158
211
|
</script>
|
|
@@ -175,40 +228,59 @@
|
|
|
175
228
|
/>
|
|
176
229
|
</label>
|
|
177
230
|
<div class="suu-filter-preset-row">
|
|
178
|
-
{#each
|
|
179
|
-
|
|
180
|
-
type="button"
|
|
181
|
-
class="suu-filter-preset"
|
|
182
|
-
class:suu-filter-preset--active={value.preset === preset}
|
|
183
|
-
on:click={() => applyPreset(preset)}
|
|
184
|
-
>
|
|
185
|
-
{labelFor(preset)}
|
|
186
|
-
</button>
|
|
187
|
-
{#if preset === 'last30Days' || preset === 'thisYear'}
|
|
231
|
+
{#each presetEntries as entry}
|
|
232
|
+
{#if entry === 'divider'}
|
|
188
233
|
<span class="suu-filter-preset-divider" aria-hidden="true"></span>
|
|
234
|
+
{:else if entry === 'quickMonth'}
|
|
235
|
+
<label class="suu-filter-preset-select">
|
|
236
|
+
<span class="suu-visually-hidden">{messages.dateRange.quickMonthLabel}</span>
|
|
237
|
+
<Dropdown
|
|
238
|
+
ariaLabel={messages.dateRange.quickMonthLabel}
|
|
239
|
+
value={quickMonth}
|
|
240
|
+
options={quickMonthOptions}
|
|
241
|
+
fitViewport={true}
|
|
242
|
+
fitContent={true}
|
|
243
|
+
onChange={(nextValue) => updateQuickMonth(String(nextValue))}
|
|
244
|
+
/>
|
|
245
|
+
</label>
|
|
246
|
+
{:else if entry === 'quickYear'}
|
|
247
|
+
<label class="suu-filter-preset-select">
|
|
248
|
+
<span class="suu-visually-hidden">{messages.dateRange.quickYearLabel}</span>
|
|
249
|
+
<Dropdown
|
|
250
|
+
ariaLabel={messages.dateRange.quickYearLabel}
|
|
251
|
+
value={quickYear}
|
|
252
|
+
options={quickYearDropdownOptions}
|
|
253
|
+
fitViewport={true}
|
|
254
|
+
fitContent={true}
|
|
255
|
+
onChange={(nextValue) => updateQuickYear(String(nextValue))}
|
|
256
|
+
/>
|
|
257
|
+
</label>
|
|
258
|
+
{:else if isSelectEntry(entry)}
|
|
259
|
+
<label class="suu-filter-preset-select">
|
|
260
|
+
<span class="suu-visually-hidden">{entry.ariaLabel ?? entry.key}</span>
|
|
261
|
+
<Dropdown
|
|
262
|
+
ariaLabel={entry.ariaLabel ?? entry.key}
|
|
263
|
+
value={entry.value}
|
|
264
|
+
options={entry.options}
|
|
265
|
+
fitViewport={true}
|
|
266
|
+
fitContent={true}
|
|
267
|
+
onChange={(nextValue) => {
|
|
268
|
+
if (isSelectEntry(entry)) {
|
|
269
|
+
void entry.onChange(String(nextValue));
|
|
270
|
+
}
|
|
271
|
+
}}
|
|
272
|
+
/>
|
|
273
|
+
</label>
|
|
274
|
+
{:else}
|
|
275
|
+
<button
|
|
276
|
+
type="button"
|
|
277
|
+
class="suu-filter-preset"
|
|
278
|
+
class:suu-filter-preset--active={value.preset === presetKeyOf(entry)}
|
|
279
|
+
on:click={() => applyPresetEntry(entry)}
|
|
280
|
+
>
|
|
281
|
+
{labelFor(entry)}
|
|
282
|
+
</button>
|
|
189
283
|
{/if}
|
|
190
284
|
{/each}
|
|
191
|
-
<label class="suu-filter-preset-select">
|
|
192
|
-
<span class="suu-visually-hidden">{messages.dateRange.quickMonthLabel}</span>
|
|
193
|
-
<Dropdown
|
|
194
|
-
ariaLabel={messages.dateRange.quickMonthLabel}
|
|
195
|
-
value={quickMonth}
|
|
196
|
-
options={quickMonthOptions}
|
|
197
|
-
fitViewport={true}
|
|
198
|
-
fitContent={true}
|
|
199
|
-
onChange={(nextValue) => updateQuickMonth(String(nextValue))}
|
|
200
|
-
/>
|
|
201
|
-
</label>
|
|
202
|
-
<label class="suu-filter-preset-select">
|
|
203
|
-
<span class="suu-visually-hidden">{messages.dateRange.quickYearLabel}</span>
|
|
204
|
-
<Dropdown
|
|
205
|
-
ariaLabel={messages.dateRange.quickYearLabel}
|
|
206
|
-
value={quickYear}
|
|
207
|
-
options={quickYearDropdownOptions}
|
|
208
|
-
fitViewport={true}
|
|
209
|
-
fitContent={true}
|
|
210
|
-
onChange={(nextValue) => updateQuickYear(String(nextValue))}
|
|
211
|
-
/>
|
|
212
|
-
</label>
|
|
213
285
|
</div>
|
|
214
286
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type UiLanguage } from '../i18n.js';
|
|
2
|
-
import type { DateRangeFilterValue,
|
|
2
|
+
import type { DateRangeFilterValue, DateRangePresetEntry } from './types.js';
|
|
3
3
|
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
4
4
|
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
5
5
|
$$bindings?: Bindings;
|
|
@@ -18,8 +18,9 @@ declare const DateRangeFilter: $$__sveltets_2_IsomorphicComponent<{
|
|
|
18
18
|
language?: UiLanguage;
|
|
19
19
|
startLabel?: string | undefined;
|
|
20
20
|
endLabel?: string | undefined;
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
presets?: DateRangePresetEntry[] | undefined;
|
|
22
|
+
presetLabels?: Partial<Record<string, string>>;
|
|
23
|
+
defaultPreset?: string | undefined;
|
|
23
24
|
quickYears?: number[] | undefined;
|
|
24
25
|
now?: () => Date;
|
|
25
26
|
weekStartsOn?: 0 | 1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DateRangeFilter.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/data-table/DateRangeFilter.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"DateRangeFilter.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/data-table/DateRangeFilter.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,KAAK,EAER,oBAAoB,EAEpB,oBAAoB,EAErB,MAAM,YAAY,CAAC;AA2PtB,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,eAAe;YAV8P,oBAAoB;eAAa,UAAU;iBAAe,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;cAAY,oBAAoB,EAAE,GAAG,SAAS;mBAAiB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;oBAAkB,MAAM,GAAG,SAAS;iBAAe,MAAM,EAAE,GAAG,SAAS;UAAQ,MAAM,IAAI;mBAAiB,CAAC,GAAG,CAAC;eAAa,CAAC,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC,GAAG,SAAS;;;kBAUviB,CAAC;AACnF,KAAK,eAAe,GAAG,YAAY,CAAC,OAAO,eAAe,CAAC,CAAC;AAC9D,eAAe,eAAe,CAAC"}
|
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
<script lang="ts">
|
|
4
4
|
import Dropdown from '../dropdown/Dropdown.svelte';
|
|
5
5
|
import DropdownMultiSelect from '../dropdown/DropdownMultiSelect.svelte';
|
|
6
|
-
import
|
|
6
|
+
import type {
|
|
7
|
+
DropdownLoadContext,
|
|
8
|
+
DropdownLoadOptionsResult,
|
|
9
|
+
DropdownOption,
|
|
10
|
+
DropdownSelection
|
|
11
|
+
} from '../dropdown/types.js';
|
|
7
12
|
import type { UiLanguage } from '../i18n.js';
|
|
8
13
|
import DateRangeFilter from './DateRangeFilter.svelte';
|
|
9
14
|
import NumberRangeFilter from './NumberRangeFilter.svelte';
|
|
10
|
-
import type { FilterControl } from './types.js';
|
|
15
|
+
import type { DropdownSearchChangeDetail, DropdownSearchItem, FilterControl } from './types.js';
|
|
11
16
|
|
|
12
17
|
export let control: FilterControl;
|
|
13
18
|
export let language: UiLanguage = 'en_us';
|
|
@@ -32,6 +37,111 @@
|
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
// The removed DropdownSearch spoke free text in `value` while the root
|
|
41
|
+
// Dropdown reports option identifiers. This adapter keeps the legacy
|
|
42
|
+
// `{ value: label, selectedItem, selectedItems, status }` detail by resolving
|
|
43
|
+
// each identifier back to the item that loadOptions returned.
|
|
44
|
+
let dropdownSearchQuery = '';
|
|
45
|
+
const dropdownSearchItems = new Map<string, DropdownSearchItem>();
|
|
46
|
+
|
|
47
|
+
function resolveDropdownSearchItemLabel(item: DropdownSearchItem): string {
|
|
48
|
+
return control.type === 'dropdownSearch' && control.getItemLabel
|
|
49
|
+
? control.getItemLabel(item)
|
|
50
|
+
: item.label;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The root Dropdown types labels against its own option shape; the options it
|
|
54
|
+
// hands back are the very `DropdownSearchItem` objects we returned, so the
|
|
55
|
+
// structural widening is lossless for consumers' getItemLabel callbacks.
|
|
56
|
+
function dropdownSearchOptionLabel(option: DropdownOption): string {
|
|
57
|
+
return resolveDropdownSearchItemLabel(option as DropdownSearchItem);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function findDropdownSearchItem(identifier: string): DropdownSearchItem | undefined {
|
|
61
|
+
const cached = dropdownSearchItems.get(identifier);
|
|
62
|
+
if (cached !== undefined) {
|
|
63
|
+
return cached;
|
|
64
|
+
}
|
|
65
|
+
if (control.type === 'dropdownSearch' && control.selectedItem?.value === identifier) {
|
|
66
|
+
return control.selectedItem;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function loadDropdownSearchOptions(
|
|
72
|
+
rawQuery: string,
|
|
73
|
+
context: DropdownLoadContext
|
|
74
|
+
): Promise<DropdownLoadOptionsResult> {
|
|
75
|
+
if (control.type !== 'dropdownSearch') {
|
|
76
|
+
return { options: [] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const query = rawQuery.trim();
|
|
80
|
+
// minLength has no root Dropdown counterpart, so the wrapper keeps the old
|
|
81
|
+
// "below threshold returns no options" contract itself.
|
|
82
|
+
if (query.length < (control.minLength ?? 1)) {
|
|
83
|
+
return { options: [] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = await control.loadOptions(query, context);
|
|
87
|
+
// Stale responses must not seed the identifier cache either.
|
|
88
|
+
if (context.signal.aborted) {
|
|
89
|
+
return { options: [] };
|
|
90
|
+
}
|
|
91
|
+
const items = result?.options ?? [];
|
|
92
|
+
for (const item of items) {
|
|
93
|
+
dropdownSearchItems.set(item.value, item);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { options: items };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function handleDropdownSearchQueryChange(query: string) {
|
|
100
|
+
dropdownSearchQuery = query;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handleDropdownSearchChange(selection: DropdownSelection) {
|
|
104
|
+
if (control.type !== 'dropdownSearch') {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const identifier = Array.isArray(selection) ? (selection[0] ?? '') : selection;
|
|
109
|
+
// The root Dropdown reports an empty identifier when the user types over a
|
|
110
|
+
// controlled selection (input/value divergence). That divergent-clear is
|
|
111
|
+
// dropped so `onChange` stays a selection-change (submit) event; a real
|
|
112
|
+
// selection always carries a non-empty identifier.
|
|
113
|
+
if (!identifier) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const item = findDropdownSearchItem(identifier);
|
|
117
|
+
const detail: DropdownSearchChangeDetail = {
|
|
118
|
+
value: item ? resolveDropdownSearchItemLabel(item) : identifier,
|
|
119
|
+
selectedItem: item ?? null,
|
|
120
|
+
selectedItems: [],
|
|
121
|
+
status: item ? 'valid' : dropdownSearchQuery.trim() ? 'invalid' : 'empty'
|
|
122
|
+
};
|
|
123
|
+
void control.onChange(detail);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// External value/selected-item changes rebuild the root Dropdown instance:
|
|
127
|
+
// controlled `value` resets its internal query, results, and draft, and
|
|
128
|
+
// `selectedOptions` then displays the new selection.
|
|
129
|
+
$: dropdownSearchKey = control.type === 'dropdownSearch'
|
|
130
|
+
? `${control.value}\u0000${control.selectedItem?.value ?? ''}`
|
|
131
|
+
: '';
|
|
132
|
+
// A selected item displays through `selectedOptions`; a free-text external
|
|
133
|
+
// value has no option to resolve and falls back to the Dropdown value itself.
|
|
134
|
+
$: dropdownSearchValue = control.type === 'dropdownSearch'
|
|
135
|
+
? control.selectedItem
|
|
136
|
+
? control.selectedItem.value
|
|
137
|
+
: control.value
|
|
138
|
+
: '';
|
|
139
|
+
// The raw item is handed to the Dropdown untouched so its `getItemLabel` prop
|
|
140
|
+
// is the single label conversion on the display path.
|
|
141
|
+
$: dropdownSearchSelectedOptions = control.type === 'dropdownSearch' && control.selectedItem
|
|
142
|
+
? [control.selectedItem]
|
|
143
|
+
: [];
|
|
144
|
+
|
|
35
145
|
</script>
|
|
36
146
|
|
|
37
147
|
{#if control.type === 'container'}
|
|
@@ -110,37 +220,36 @@
|
|
|
110
220
|
{/each}
|
|
111
221
|
</div>
|
|
112
222
|
{:else if control.type === 'dropdownSearch'}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
onChange={(detail) => control.type === 'dropdownSearch' && void control.onChange(detail)}
|
|
137
|
-
/>
|
|
223
|
+
{#key dropdownSearchKey}
|
|
224
|
+
<Dropdown
|
|
225
|
+
value={dropdownSearchValue}
|
|
226
|
+
search
|
|
227
|
+
inputStyle="input"
|
|
228
|
+
portal
|
|
229
|
+
selectedOptions={dropdownSearchSelectedOptions}
|
|
230
|
+
loadOptions={loadDropdownSearchOptions}
|
|
231
|
+
searchDebounceMs={control.debounceMs ?? 500}
|
|
232
|
+
searchLimit={control.limit ?? 10}
|
|
233
|
+
placeholder={control.placeholder ?? ''}
|
|
234
|
+
ariaLabel={control.ariaLabel}
|
|
235
|
+
{language}
|
|
236
|
+
noResultsText={control.noResultsText}
|
|
237
|
+
loadingText={control.loadingText}
|
|
238
|
+
width={control.width}
|
|
239
|
+
minWidth={control.minWidth}
|
|
240
|
+
maxWidth={control.maxWidth}
|
|
241
|
+
getItemLabel={dropdownSearchOptionLabel}
|
|
242
|
+
onChange={handleDropdownSearchChange}
|
|
243
|
+
onSearchChange={handleDropdownSearchQueryChange}
|
|
244
|
+
/>
|
|
245
|
+
{/key}
|
|
138
246
|
{:else if control.type === 'dateRange'}
|
|
139
247
|
<DateRangeFilter
|
|
140
248
|
value={control.value}
|
|
141
249
|
{language}
|
|
142
250
|
startLabel={control.startLabel}
|
|
143
251
|
endLabel={control.endLabel}
|
|
252
|
+
presets={control.presets}
|
|
144
253
|
presetLabels={control.presetLabels ?? {}}
|
|
145
254
|
defaultPreset={control.defaultPreset}
|
|
146
255
|
quickYears={control.quickYears}
|