@nxgt/material-core 0.7.1 → 0.8.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 +26 -0
- package/README.md +79 -1
- package/dist/chunks/index-f24x24a8.js +37 -0
- package/dist/chunks/index-f24x24a8.js.map +10 -0
- package/dist/lib/async-search.d.ts +46 -0
- package/dist/lib/async-search.d.ts.map +1 -0
- package/dist/lib/async-search.js +116 -0
- package/dist/lib/async-search.js.map +11 -0
- package/dist/lib/debounce.d.ts +6 -0
- package/dist/lib/debounce.d.ts.map +1 -0
- package/dist/lib/debounce.js +9 -0
- package/dist/lib/debounce.js.map +9 -0
- package/dist/lib/search-announcement.d.ts +22 -0
- package/dist/lib/search-announcement.d.ts.map +1 -0
- package/dist/lib/text-match.d.ts +16 -0
- package/dist/lib/text-match.d.ts.map +1 -0
- package/dist/lib/text-match.js +25 -0
- package/dist/lib/text-match.js.map +10 -0
- package/docs/roadmap.md +5 -0
- package/package.json +25 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @nxgt/material-core
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#521](https://github.com/softistx/nxgt-material/pull/521) [`b76cd9a`](https://github.com/softistx/nxgt-material/commit/b76cd9ae3cc4f4d0f1b20cec31c9471819ffcd3c) Thanks [@SteveGT96](https://github.com/SteveGT96)! - `Autocomplete` can search a server (softistx/nxgt-material#514). Fixes [#514](https://github.com/softistx/nxgt-material/issues/514).
|
|
8
|
+
|
|
9
|
+
- **Server search.** `onSearch(query, { signal })` (Vue: the function prop `on-search`, so `@search="fn"` binds it too) is called once typing pauses and its results become the list. Each search aborts the previous one (pass `signal` to `fetch`), and a response that lands after a newer one is dropped, even from an API that ignores the signal. `minQueryLength` (Vue: `min-query-length`, default 1) is how many characters it waits for; below it the list says "Type at least N characters to search." A rejected search is shown and announced ("The search failed. Try again.") and reported to `onSearchError` (Vue: `@search-error`). `options` is now optional: with `onSearch` it is the list before a search and the selected options' labels, and a picked option keeps its chip once a new search no longer returns it.
|
|
10
|
+
- **Controlled mode.** `loading` and `searchError` (Vue: `search-error`), beside `onQueryChange` (Vue: `@query-change`), for an app that fetches `options` itself.
|
|
11
|
+
- **A configurable debounce.** `debounceMs` (Vue: `debounce-ms`), 250 ms by default, sets the pause before `onSearch` and `onQueryChange`.
|
|
12
|
+
- **Accent-insensitive matching.** `filter`: `'accent-insensitive'` (the default without `onSearch`), `'none'` (the default with `onSearch`) or `(option, query) => boolean`. The default compares the label or value with the query lowercased in the provider's language and stripped of accents, so "e" finds "École" and "orle" finds "Orléans". Vue's exported `filterAutocompleteOptions` matches the same way and takes an optional `locale`.
|
|
13
|
+
- **Accessibility.** A polite live region announces "Loading…", then "N results" / "No results" (or the failure) once the search answers or typing pauses, not on every keystroke; the list is `aria-busy` while loading. New strings: `material.autocomplete.loading`, `.results`, `.search-error`, `.min-query` (en, fr). The keyboard works as before.
|
|
14
|
+
- React: clearing a single `Autocomplete`'s input no longer throws (`Cannot read properties of null (reading 'value')`).
|
|
15
|
+
- `@nxgt/material-core` adds `/debounce` (the `debounce` both libraries' `SearchField` kept a copy of, still exported by both, Vue now with its `Debounced` type), `/text-match` (`foldText`, `matchesText`) and `/async-search` (`createAsyncSearch`, `searchAnnouncement`).
|
|
16
|
+
|
|
17
|
+
Apps that built their own search field and result list while waiting for this (marked `Temporary, until material [#514](https://github.com/softistx/nxgt-material/issues/514)`) can replace it with `onSearch` and remove the workaround.
|
|
18
|
+
|
|
19
|
+
**Behaviour changes, no API removed.**
|
|
20
|
+
|
|
21
|
+
- Local matching ignores accents by default, so a query can keep more options than before. To match as before, pass your own `filter`, for example `filter={(option, query) => option.label.toLowerCase().includes(query.toLowerCase())}`; to show `options` exactly as given (filtered on your server), pass `filter="none"`.
|
|
22
|
+
- `onQueryChange` (`@query-change`) now runs 250 ms after typing pauses; it was a fixed 1 s in `@nxgt/material` and 300 ms in `@nxgt/material-vue`. Search for `onQueryChange` / `@query-change`, and add `debounceMs={1000}` (`:debounce-ms="300"`) where the old pause mattered — or move the fetch to `onSearch`.
|
|
23
|
+
- React: the list's empty text is no longer a live region of its own (Base UI's `ComboboxEmpty` had `role="status"`); the field's one status region (`[data-slot="autocomplete-status"]`) says "No results" instead. A test that found the empty text by `role="status"` finds it by its text.
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- [#519](https://github.com/softistx/nxgt-material/pull/519) [`18f30c1`](https://github.com/softistx/nxgt-material/commit/18f30c1cc6cc41c2b73a43fad2501b7db2bbc016) Thanks [@SteveGT96](https://github.com/SteveGT96)! - package.json names its repository, so issue reports route to it. Each package now has a `repository` field (`git+https://github.com/softistx/nxgt-material.git`, with its `directory` under `packages/`).
|
|
28
|
+
|
|
3
29
|
## 0.7.1
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ The framework-free core that [`@nxgt/material`](https://www.npmjs.com/package/@n
|
|
|
6
6
|
the print formatters, the PDF file helpers, the PDF viewer's framework-free
|
|
7
7
|
half (page geometry, scroll spy, text search), the currency field's decimals,
|
|
8
8
|
the file-size formatter, the slider's tick marks, the emoji list's groups,
|
|
9
|
-
the date range presets, the file preview's kind detection, the fields' blur
|
|
9
|
+
the date range presets, a debounce, accent-insensitive text matching and a race-free async search (`/debounce`, `/text-match`, `/async-search`), the file preview's kind detection, the fields' blur
|
|
10
10
|
rule, a field's first reachable control (`/focus-control`) and its `aria-describedby`
|
|
11
11
|
(`/described-by`), the page behind a modal layer made inert
|
|
12
12
|
(`/inert-others`), Escape pressed while an overlay closes
|
|
@@ -93,6 +93,9 @@ built-in.
|
|
|
93
93
|
| `/slider-ticks` | `tickValues` — where a slider's tick marks sit |
|
|
94
94
|
| `/emoji-groups` | `groupKey`, `groupEmojis`, `searchEmojis`, `groupName`, `GROUP_NAMES` and the type `EmojiItem` — how the emoji list groups, searches and names its emojis |
|
|
95
95
|
| `/date-range` | `DATE_RANGE_PRESET_IDS`, `presetRange`, `resolvePresetRange`, `isSameDayRange`, `isRangeDisabled`, `withTimeOf`, `endOfDayTime`, `clampRangeEnd`, `keepRangeTimes` and the types `DayRange`, `PartialDayRange`, `DateRangePreset`, `DateRangePresetId`, `DateRangePresetOption`, `DateRangeRules` — `DateRangeField`'s presets and times |
|
|
96
|
+
| `/debounce` | `debounce`, the type `Debounced` — a function called once calls pause, with `cancel` and `flush` (`SearchField`'s `debounceMs`) |
|
|
97
|
+
| `/text-match` | `foldText`, `matchesText` — case- and accent-insensitive matching (`'e'` finds "École"), lowercased for a locale |
|
|
98
|
+
| `/async-search` | `createAsyncSearch`, `searchAnnouncement`, `DEFAULT_SEARCH_DEBOUNCE_MS`, `DEFAULT_MIN_QUERY_LENGTH` and the types `AsyncSearch`, `AsyncSearchConfig`, `AsyncSearchState`, `AsyncSearchStatus`, `SearchFn`, `SearchContext`, `SearchAnnouncement` — `Autocomplete`'s server search: debounced, each run aborting the last, a stale response dropped |
|
|
96
99
|
| `/file-preview` | `fileKind`, `describeFile`, `safePreviewUrl`, `readTextPreview`, `FilePreviewHttpError`, `FilePreviewUnsafeUrlError`, `TEXT_PREVIEW_MAX_BYTES`, `fileExtension`, `fileNameFromUrl`, `dataUrlType`, `kindOfMimeType`, `normaliseMimeType`, `isTextFileKind`, `TEXT_FILE_KINDS` and the types `FileKind`, `FileKindInput`, `DescribedFile`, `FilePreviewSource`, `TextPreview` — `FilePreview`'s kind detection, URL allow-list and text reader |
|
|
97
100
|
| `/vite` | `pdfkitStandardFonts()` — see below |
|
|
98
101
|
| `/pdfkit-fonts` | **Node only.** `pdfkitStandardFontDirs`, `pdfkitStandardFontFiles`, `PDFKIT_STANDARD_FONTS`, the types `PdfkitStandardFont` and `PdfkitFontsWarn` — where the app's pdfkit keeps its standard fonts, for a server tracer |
|
|
@@ -375,6 +378,81 @@ tickValues(0, 100, true, 1).length; // 21 — capped
|
|
|
375
378
|
tickValues(5, 5, true); // [5] — min === max
|
|
376
379
|
```
|
|
377
380
|
|
|
381
|
+
### A debounced call: `/debounce`
|
|
382
|
+
|
|
383
|
+
The debounce both libraries' `SearchField` (`debounceMs`) and `Autocomplete`
|
|
384
|
+
use: the wrapped function runs `ms` after the last call, with the last
|
|
385
|
+
arguments. `cancel()` drops a pending call, `flush()` runs it now.
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import { debounce } from '@nxgt/material-core/debounce';
|
|
389
|
+
|
|
390
|
+
const save = debounce((text: string) => console.log('save', text), 300);
|
|
391
|
+
save('a');
|
|
392
|
+
save('ab'); // 300 ms later: "save ab", once
|
|
393
|
+
save.flush(); // or now
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Matching text as the user types: `/text-match`
|
|
397
|
+
|
|
398
|
+
`foldText(text, locale?)` lowercases `text` for `locale` (the runtime's when
|
|
399
|
+
omitted; an invalid tag falls back to it), then strips its diacritics
|
|
400
|
+
(`normalize('NFD')`, combining marks removed). `matchesText(text, query,
|
|
401
|
+
locale?)` says whether the folded, trimmed query occurs in the folded text;
|
|
402
|
+
a blank query matches everything. Both libraries' `Autocomplete` filters its
|
|
403
|
+
local options with it, in the provider's language.
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
import { foldText, matchesText } from '@nxgt/material-core/text-match';
|
|
407
|
+
|
|
408
|
+
foldText('Élève'); // 'eleve'
|
|
409
|
+
foldText('İSTANBUL', 'tr'); // 'istanbul'
|
|
410
|
+
matchesText('Orléans', 'orle'); // true
|
|
411
|
+
matchesText('Cafe', 'café'); // true — both ways
|
|
412
|
+
matchesText('Lyon', ' '); // true — a blank query keeps everything
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Letters that are not a base letter plus a mark stay as they are: `ß`, `ø`
|
|
416
|
+
and `æ` do not fold to `ss`, `o` or `ae`.
|
|
417
|
+
|
|
418
|
+
### A search that never answers out of order: `/async-search`
|
|
419
|
+
|
|
420
|
+
`createAsyncSearch(config, onChange)` runs `config.search(query, { signal })`
|
|
421
|
+
once `request(query)` calls pause for `debounceMs` (250 by default). Each run
|
|
422
|
+
aborts the one before it, and a response that still lands for an older run
|
|
423
|
+
is dropped, so a slow first query never overwrites a faster second one; an
|
|
424
|
+
abort is never reported as an error. A query shorter than `minQueryLength`
|
|
425
|
+
(1 by default, trimmed) searches nothing and resets the state to `idle`.
|
|
426
|
+
`onChange` receives each state: `{ status: 'idle' | 'loading' | 'success' |
|
|
427
|
+
'error', query, results, error? }`, the last results kept while the next
|
|
428
|
+
query loads.
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
import { createAsyncSearch } from '@nxgt/material-core/async-search';
|
|
432
|
+
|
|
433
|
+
type City = { value: string; label: string };
|
|
434
|
+
|
|
435
|
+
const search = createAsyncSearch<City>(
|
|
436
|
+
{
|
|
437
|
+
search: (query, { signal }) =>
|
|
438
|
+
fetch(`/api/cities?q=${encodeURIComponent(query)}`, { signal }).then((r) => r.json()),
|
|
439
|
+
minQueryLength: 2,
|
|
440
|
+
},
|
|
441
|
+
(state) => render(state),
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
input.addEventListener('input', () => search.request(input.value));
|
|
445
|
+
// On teardown: search.cancel() drops the pending search and aborts the one in flight.
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
`configure(partial)` swaps the search, the debounce or the minimum length
|
|
449
|
+
(React re-reads the latest props through it), `flush()` runs the pending
|
|
450
|
+
search now, and requesting the query the state already holds (or is loading)
|
|
451
|
+
does nothing. `searchAnnouncement({ loading, failed, settled, count })` is
|
|
452
|
+
what a combobox's polite live region says: the failure, else "loading",
|
|
453
|
+
else the count once the query has settled — `null` while the user is still
|
|
454
|
+
typing.
|
|
455
|
+
|
|
378
456
|
### The emoji list's groups: `/emoji-groups`
|
|
379
457
|
|
|
380
458
|
How both libraries' `EmojiList` groups, searches and names its emojis:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/lib/debounce.ts
|
|
2
|
+
function debounce2(fn, ms) {
|
|
3
|
+
let timer;
|
|
4
|
+
let lastArgs;
|
|
5
|
+
const run = () => {
|
|
6
|
+
timer = undefined;
|
|
7
|
+
const pending = lastArgs;
|
|
8
|
+
lastArgs = undefined;
|
|
9
|
+
if (pending)
|
|
10
|
+
fn(...pending);
|
|
11
|
+
};
|
|
12
|
+
const wrapped = (...args) => {
|
|
13
|
+
lastArgs = args;
|
|
14
|
+
if (timer !== undefined)
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
timer = setTimeout(run, ms);
|
|
17
|
+
};
|
|
18
|
+
wrapped.cancel = () => {
|
|
19
|
+
if (timer !== undefined) {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
timer = undefined;
|
|
22
|
+
}
|
|
23
|
+
lastArgs = undefined;
|
|
24
|
+
};
|
|
25
|
+
wrapped.flush = () => {
|
|
26
|
+
if (timer === undefined)
|
|
27
|
+
return;
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
run();
|
|
30
|
+
};
|
|
31
|
+
return wrapped;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { debounce2 };
|
|
35
|
+
|
|
36
|
+
//# debugId=863EA47559EC981164756E2164756E21
|
|
37
|
+
//# sourceMappingURL=index-f24x24a8.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/debounce.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"export type Debounced<Args extends unknown[]> = ((...args: Args) => void) & {\n\tcancel: () => void;\n\tflush: () => void;\n};\n\nexport function debounce<Args extends unknown[]>(\n\tfn: (...args: Args) => void,\n\tms: number,\n): Debounced<Args> {\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tlet lastArgs: Args | undefined;\n\n\tconst run = () => {\n\t\ttimer = undefined;\n\t\tconst pending = lastArgs;\n\t\tlastArgs = undefined;\n\t\tif (pending) fn(...pending);\n\t};\n\n\tconst wrapped = ((...args: Args) => {\n\t\tlastArgs = args;\n\t\tif (timer !== undefined) clearTimeout(timer);\n\t\ttimer = setTimeout(run, ms);\n\t}) as Debounced<Args>;\n\n\twrapped.cancel = () => {\n\t\tif (timer !== undefined) {\n\t\t\tclearTimeout(timer);\n\t\t\ttimer = undefined;\n\t\t}\n\t\tlastArgs = undefined;\n\t};\n\n\twrapped.flush = () => {\n\t\tif (timer === undefined) return;\n\t\tclearTimeout(timer);\n\t\trun();\n\t};\n\n\treturn wrapped;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAKO,SAAS,SAAgC,CAC/C,IACA,IACkB;AAAA,EAClB,IAAI;AAAA,EACJ,IAAI;AAAA,EAEJ,MAAM,MAAM,MAAM;AAAA,IACjB,QAAQ;AAAA,IACR,MAAM,UAAU;AAAA,IAChB,WAAW;AAAA,IACX,IAAI;AAAA,MAAS,GAAG,GAAG,OAAO;AAAA;AAAA,EAG3B,MAAM,UAAW,IAAI,SAAe;AAAA,IACnC,WAAW;AAAA,IACX,IAAI,UAAU;AAAA,MAAW,aAAa,KAAK;AAAA,IAC3C,QAAQ,WAAW,KAAK,EAAE;AAAA;AAAA,EAG3B,QAAQ,SAAS,MAAM;AAAA,IACtB,IAAI,UAAU,WAAW;AAAA,MACxB,aAAa,KAAK;AAAA,MAClB,QAAQ;AAAA,IACT;AAAA,IACA,WAAW;AAAA;AAAA,EAGZ,QAAQ,QAAQ,MAAM;AAAA,IACrB,IAAI,UAAU;AAAA,MAAW;AAAA,IACzB,aAAa,KAAK;AAAA,IAClB,IAAI;AAAA;AAAA,EAGL,OAAO;AAAA;",
|
|
8
|
+
"debugId": "863EA47559EC981164756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export { type SearchAnnouncement, searchAnnouncement, } from './search-announcement';
|
|
2
|
+
/** What a search is handed besides the query: aborted once it is stale. */
|
|
3
|
+
export type SearchContext = {
|
|
4
|
+
signal: AbortSignal;
|
|
5
|
+
};
|
|
6
|
+
/** A server search: the options for `query`. Pass `signal` on to `fetch`. */
|
|
7
|
+
export type SearchFn<T> = (query: string, context: SearchContext) => Promise<T[]>;
|
|
8
|
+
export type AsyncSearchStatus = 'idle' | 'loading' | 'success' | 'error';
|
|
9
|
+
export type AsyncSearchState<T> = {
|
|
10
|
+
status: AsyncSearchStatus;
|
|
11
|
+
/** The query being searched (loading) or the one `results` answer. */
|
|
12
|
+
query: string;
|
|
13
|
+
/** The last results. Kept while the next query loads; `[]` when idle. */
|
|
14
|
+
results: T[];
|
|
15
|
+
/** What the search rejected with (`status: 'error'`). */
|
|
16
|
+
error?: unknown;
|
|
17
|
+
};
|
|
18
|
+
export type AsyncSearchConfig<T> = {
|
|
19
|
+
search: SearchFn<T>;
|
|
20
|
+
/** How long typing must pause before a search runs. Default 250. */
|
|
21
|
+
debounceMs?: number;
|
|
22
|
+
/** Below this many characters (trimmed) nothing is searched. Default 1. */
|
|
23
|
+
minQueryLength?: number;
|
|
24
|
+
};
|
|
25
|
+
export type AsyncSearch<T> = {
|
|
26
|
+
/** The current state; `onChange` receives each new one. */
|
|
27
|
+
readonly state: AsyncSearchState<T>;
|
|
28
|
+
/** Search `query` (trimmed) once typing pauses; a short one resets to idle at once. */
|
|
29
|
+
request: (query: string) => void;
|
|
30
|
+
/** Run the pending search now. */
|
|
31
|
+
flush: () => void;
|
|
32
|
+
/** Drop the pending search and abort the one in flight. */
|
|
33
|
+
cancel: () => void;
|
|
34
|
+
/** Swap the search, the debounce or the minimum length. */
|
|
35
|
+
configure: (config: Partial<AsyncSearchConfig<T>>) => void;
|
|
36
|
+
};
|
|
37
|
+
export declare const DEFAULT_SEARCH_DEBOUNCE_MS = 250;
|
|
38
|
+
export declare const DEFAULT_MIN_QUERY_LENGTH = 1;
|
|
39
|
+
/**
|
|
40
|
+
* A debounced search whose responses can never arrive out of order: each run
|
|
41
|
+
* aborts the one before it (its `signal`), and a response that still lands
|
|
42
|
+
* for an older run is dropped, so a slow first query never overwrites the
|
|
43
|
+
* results of a faster second one. An abort is not an error.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createAsyncSearch<T>(config: AsyncSearchConfig<T>, onChange: (state: AsyncSearchState<T>) => void): AsyncSearch<T>;
|
|
46
|
+
//# sourceMappingURL=async-search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-search.d.ts","sourceRoot":"","sources":["../../src/lib/async-search.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,KAAK,kBAAkB,EACvB,kBAAkB,GAClB,MAAM,uBAAuB,CAAC;AAE/B,2EAA2E;AAC3E,MAAM,MAAM,aAAa,GAAG;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,CAAC;AAEpD,6EAA6E;AAC7E,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,CACzB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,KAClB,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;AAElB,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAEzE,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI;IACjC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,OAAO,EAAE,CAAC,EAAE,CAAC;IACb,yDAAyD;IACzD,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI;IAClC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IACpB,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC5B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IACpC,uFAAuF;IACvF,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,kCAAkC;IAClC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,2DAA2D;IAC3D,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,2DAA2D;IAC3D,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;CAC3D,CAAC;AAEF,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAC9C,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAQ1C;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAClC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAC5B,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,IAAI,GAC5C,WAAW,CAAC,CAAC,CAAC,CAEhB"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
debounce2
|
|
3
|
+
} from "../chunks/index-f24x24a8.js";
|
|
4
|
+
|
|
5
|
+
// src/lib/search-announcement.ts
|
|
6
|
+
function searchAnnouncement(list) {
|
|
7
|
+
if (list.failed)
|
|
8
|
+
return { kind: "error" };
|
|
9
|
+
if (list.loading)
|
|
10
|
+
return { kind: "loading" };
|
|
11
|
+
return list.settled ? { kind: "results", count: list.count } : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/lib/async-search.ts
|
|
15
|
+
var DEFAULT_SEARCH_DEBOUNCE_MS = 250;
|
|
16
|
+
var DEFAULT_MIN_QUERY_LENGTH = 1;
|
|
17
|
+
var idle = () => ({
|
|
18
|
+
status: "idle",
|
|
19
|
+
query: "",
|
|
20
|
+
results: []
|
|
21
|
+
});
|
|
22
|
+
function createAsyncSearch(config, onChange) {
|
|
23
|
+
return new AsyncSearchRunner(config, onChange);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class AsyncSearchRunner {
|
|
27
|
+
#config;
|
|
28
|
+
#onChange;
|
|
29
|
+
#state;
|
|
30
|
+
#controller;
|
|
31
|
+
#ticket;
|
|
32
|
+
#pending;
|
|
33
|
+
#pendingMs;
|
|
34
|
+
constructor(config, onChange) {
|
|
35
|
+
this.#state = idle();
|
|
36
|
+
this.#ticket = 0;
|
|
37
|
+
this.#config = config;
|
|
38
|
+
this.#onChange = onChange;
|
|
39
|
+
}
|
|
40
|
+
get state() {
|
|
41
|
+
return this.#state;
|
|
42
|
+
}
|
|
43
|
+
request(text) {
|
|
44
|
+
const query = text.trim();
|
|
45
|
+
const min = this.#config.minQueryLength ?? DEFAULT_MIN_QUERY_LENGTH;
|
|
46
|
+
if (query.length < min) {
|
|
47
|
+
this.cancel();
|
|
48
|
+
if (this.#state.status !== "idle")
|
|
49
|
+
this.#set(idle());
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const { status } = this.#state;
|
|
53
|
+
if ((status === "loading" || status === "success") && this.#state.query === query) {
|
|
54
|
+
this.#pending?.cancel();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.#debounced()(query);
|
|
58
|
+
}
|
|
59
|
+
flush() {
|
|
60
|
+
this.#pending?.flush();
|
|
61
|
+
}
|
|
62
|
+
cancel() {
|
|
63
|
+
this.#pending?.cancel();
|
|
64
|
+
this.#abort();
|
|
65
|
+
if (this.#state.status === "loading")
|
|
66
|
+
this.#set(idle());
|
|
67
|
+
}
|
|
68
|
+
configure(config) {
|
|
69
|
+
this.#config = { ...this.#config, ...config };
|
|
70
|
+
}
|
|
71
|
+
#set(next) {
|
|
72
|
+
this.#state = next;
|
|
73
|
+
this.#onChange(next);
|
|
74
|
+
}
|
|
75
|
+
#abort() {
|
|
76
|
+
this.#ticket += 1;
|
|
77
|
+
this.#controller?.abort();
|
|
78
|
+
this.#controller = undefined;
|
|
79
|
+
}
|
|
80
|
+
#debounced() {
|
|
81
|
+
const ms = this.#config.debounceMs ?? DEFAULT_SEARCH_DEBOUNCE_MS;
|
|
82
|
+
if (!this.#pending || this.#pendingMs !== ms) {
|
|
83
|
+
this.#pending?.cancel();
|
|
84
|
+
this.#pending = debounce2((query) => void this.#run(query), ms);
|
|
85
|
+
this.#pendingMs = ms;
|
|
86
|
+
}
|
|
87
|
+
return this.#pending;
|
|
88
|
+
}
|
|
89
|
+
async#run(query) {
|
|
90
|
+
this.#abort();
|
|
91
|
+
const own = new AbortController;
|
|
92
|
+
this.#controller = own;
|
|
93
|
+
const ticket = this.#ticket;
|
|
94
|
+
this.#set({ status: "loading", query, results: this.#state.results });
|
|
95
|
+
try {
|
|
96
|
+
const results = await this.#config.search(query, { signal: own.signal });
|
|
97
|
+
if (ticket === this.#ticket)
|
|
98
|
+
this.#set({ status: "success", query, results });
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (ticket === this.#ticket && !own.signal.aborted)
|
|
101
|
+
this.#set({ status: "error", query, results: [], error });
|
|
102
|
+
} finally {
|
|
103
|
+
if (this.#controller === own)
|
|
104
|
+
this.#controller = undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
DEFAULT_MIN_QUERY_LENGTH,
|
|
110
|
+
DEFAULT_SEARCH_DEBOUNCE_MS,
|
|
111
|
+
createAsyncSearch,
|
|
112
|
+
searchAnnouncement
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
//# debugId=DF9F6FB2BDDD168C64756E2164756E21
|
|
116
|
+
//# sourceMappingURL=async-search.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/search-announcement.ts", "../src/lib/async-search.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/** What a combobox's polite live region says about its list, if anything. */\nexport type SearchAnnouncement =\n\t| { kind: 'loading' }\n\t| { kind: 'error' }\n\t| { kind: 'results'; count: number };\n\n/**\n * The announcement for a list: its failure, else that it is loading, else\n * its count once the query has `settled` (searched, or typing paused) —\n * `null` while the user is still typing, so a screen reader is not told\n * about every keystroke.\n */\nexport function searchAnnouncement(list: {\n\tloading: boolean;\n\tfailed: boolean;\n\tsettled: boolean;\n\tcount: number;\n}): SearchAnnouncement | null {\n\tif (list.failed) return { kind: 'error' };\n\tif (list.loading) return { kind: 'loading' };\n\treturn list.settled ? { kind: 'results', count: list.count } : null;\n}\n",
|
|
6
|
+
"import { type Debounced, debounce } from './debounce';\n\nexport {\n\ttype SearchAnnouncement,\n\tsearchAnnouncement,\n} from './search-announcement';\n\n/** What a search is handed besides the query: aborted once it is stale. */\nexport type SearchContext = { signal: AbortSignal };\n\n/** A server search: the options for `query`. Pass `signal` on to `fetch`. */\nexport type SearchFn<T> = (\n\tquery: string,\n\tcontext: SearchContext,\n) => Promise<T[]>;\n\nexport type AsyncSearchStatus = 'idle' | 'loading' | 'success' | 'error';\n\nexport type AsyncSearchState<T> = {\n\tstatus: AsyncSearchStatus;\n\t/** The query being searched (loading) or the one `results` answer. */\n\tquery: string;\n\t/** The last results. Kept while the next query loads; `[]` when idle. */\n\tresults: T[];\n\t/** What the search rejected with (`status: 'error'`). */\n\terror?: unknown;\n};\n\nexport type AsyncSearchConfig<T> = {\n\tsearch: SearchFn<T>;\n\t/** How long typing must pause before a search runs. Default 250. */\n\tdebounceMs?: number;\n\t/** Below this many characters (trimmed) nothing is searched. Default 1. */\n\tminQueryLength?: number;\n};\n\nexport type AsyncSearch<T> = {\n\t/** The current state; `onChange` receives each new one. */\n\treadonly state: AsyncSearchState<T>;\n\t/** Search `query` (trimmed) once typing pauses; a short one resets to idle at once. */\n\trequest: (query: string) => void;\n\t/** Run the pending search now. */\n\tflush: () => void;\n\t/** Drop the pending search and abort the one in flight. */\n\tcancel: () => void;\n\t/** Swap the search, the debounce or the minimum length. */\n\tconfigure: (config: Partial<AsyncSearchConfig<T>>) => void;\n};\n\nexport const DEFAULT_SEARCH_DEBOUNCE_MS = 250;\nexport const DEFAULT_MIN_QUERY_LENGTH = 1;\n\nconst idle = <T>(): AsyncSearchState<T> => ({\n\tstatus: 'idle',\n\tquery: '',\n\tresults: [],\n});\n\n/**\n * A debounced search whose responses can never arrive out of order: each run\n * aborts the one before it (its `signal`), and a response that still lands\n * for an older run is dropped, so a slow first query never overwrites the\n * results of a faster second one. An abort is not an error.\n */\nexport function createAsyncSearch<T>(\n\tconfig: AsyncSearchConfig<T>,\n\tonChange: (state: AsyncSearchState<T>) => void,\n): AsyncSearch<T> {\n\treturn new AsyncSearchRunner(config, onChange);\n}\n\nclass AsyncSearchRunner<T> implements AsyncSearch<T> {\n\t#config: AsyncSearchConfig<T>;\n\t#onChange: (state: AsyncSearchState<T>) => void;\n\t#state: AsyncSearchState<T> = idle();\n\t#controller: AbortController | undefined;\n\t/** Bumped by every run and every abort: a response for an older one is stale. */\n\t#ticket = 0;\n\t#pending: Debounced<[string]> | undefined;\n\t#pendingMs: number | undefined;\n\n\tconstructor(\n\t\tconfig: AsyncSearchConfig<T>,\n\t\tonChange: (state: AsyncSearchState<T>) => void,\n\t) {\n\t\tthis.#config = config;\n\t\tthis.#onChange = onChange;\n\t}\n\n\tget state() {\n\t\treturn this.#state;\n\t}\n\n\trequest(text: string) {\n\t\t// \"pa\" and \"pa \" are one search, and the server never sees the space.\n\t\tconst query = text.trim();\n\t\tconst min = this.#config.minQueryLength ?? DEFAULT_MIN_QUERY_LENGTH;\n\t\tif (query.length < min) {\n\t\t\tthis.cancel();\n\t\t\tif (this.#state.status !== 'idle') this.#set(idle());\n\t\t\treturn;\n\t\t}\n\t\tconst { status } = this.#state;\n\t\tif (\n\t\t\t(status === 'loading' || status === 'success') &&\n\t\t\tthis.#state.query === query\n\t\t) {\n\t\t\tthis.#pending?.cancel();\n\t\t\treturn;\n\t\t}\n\t\tthis.#debounced()(query);\n\t}\n\n\tflush() {\n\t\tthis.#pending?.flush();\n\t}\n\n\tcancel() {\n\t\tthis.#pending?.cancel();\n\t\tthis.#abort();\n\t\tif (this.#state.status === 'loading') this.#set(idle());\n\t}\n\n\tconfigure(config: Partial<AsyncSearchConfig<T>>) {\n\t\tthis.#config = { ...this.#config, ...config };\n\t}\n\n\t#set(next: AsyncSearchState<T>) {\n\t\tthis.#state = next;\n\t\tthis.#onChange(next);\n\t}\n\n\t#abort() {\n\t\tthis.#ticket += 1;\n\t\tthis.#controller?.abort();\n\t\tthis.#controller = undefined;\n\t}\n\n\t#debounced() {\n\t\tconst ms = this.#config.debounceMs ?? DEFAULT_SEARCH_DEBOUNCE_MS;\n\t\tif (!this.#pending || this.#pendingMs !== ms) {\n\t\t\tthis.#pending?.cancel();\n\t\t\tthis.#pending = debounce((query: string) => void this.#run(query), ms);\n\t\t\tthis.#pendingMs = ms;\n\t\t}\n\t\treturn this.#pending;\n\t}\n\n\tasync #run(query: string) {\n\t\tthis.#abort();\n\t\tconst own = new AbortController();\n\t\tthis.#controller = own;\n\t\tconst ticket = this.#ticket;\n\t\tthis.#set({ status: 'loading', query, results: this.#state.results });\n\t\ttry {\n\t\t\tconst results = await this.#config.search(query, { signal: own.signal });\n\t\t\tif (ticket === this.#ticket)\n\t\t\t\tthis.#set({ status: 'success', query, results });\n\t\t} catch (error) {\n\t\t\tif (ticket === this.#ticket && !own.signal.aborted)\n\t\t\t\tthis.#set({ status: 'error', query, results: [], error });\n\t\t} finally {\n\t\t\tif (this.#controller === own) this.#controller = undefined;\n\t\t}\n\t}\n}\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "DF9F6FB2BDDD168C64756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type Debounced<Args extends unknown[]> = ((...args: Args) => void) & {
|
|
2
|
+
cancel: () => void;
|
|
3
|
+
flush: () => void;
|
|
4
|
+
};
|
|
5
|
+
export declare function debounce<Args extends unknown[]>(fn: (...args: Args) => void, ms: number): Debounced<Args>;
|
|
6
|
+
//# sourceMappingURL=debounce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debounce.d.ts","sourceRoot":"","sources":["../../src/lib/debounce.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,CAAC,IAAI,SAAS,OAAO,EAAE,IAAI,CAAC,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC,GAAG;IAC3E,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF,wBAAgB,QAAQ,CAAC,IAAI,SAAS,OAAO,EAAE,EAC9C,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,IAAI,EAC3B,EAAE,EAAE,MAAM,GACR,SAAS,CAAC,IAAI,CAAC,CAgCjB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** What a combobox's polite live region says about its list, if anything. */
|
|
2
|
+
export type SearchAnnouncement = {
|
|
3
|
+
kind: 'loading';
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'error';
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'results';
|
|
8
|
+
count: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The announcement for a list: its failure, else that it is loading, else
|
|
12
|
+
* its count once the query has `settled` (searched, or typing paused) —
|
|
13
|
+
* `null` while the user is still typing, so a screen reader is not told
|
|
14
|
+
* about every keystroke.
|
|
15
|
+
*/
|
|
16
|
+
export declare function searchAnnouncement(list: {
|
|
17
|
+
loading: boolean;
|
|
18
|
+
failed: boolean;
|
|
19
|
+
settled: boolean;
|
|
20
|
+
count: number;
|
|
21
|
+
}): SearchAnnouncement | null;
|
|
22
|
+
//# sourceMappingURL=search-announcement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-announcement.d.ts","sourceRoot":"","sources":["../../src/lib/search-announcement.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,MAAM,MAAM,kBAAkB,GAC3B;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACd,GAAG,kBAAkB,GAAG,IAAI,CAI5B"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accent- and case-insensitive text matching, for a list filtered as the user
|
|
3
|
+
* types: `'e'` finds "École", `'ecole'` finds "ÉCOLE".
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* `text` lowercased for `locale` (the runtime's when omitted), then without
|
|
7
|
+
* its diacritics: `foldText('Élève')` is `'eleve'`. Lowercasing comes first,
|
|
8
|
+
* so a Turkish `'İ'` (`'tr'`) folds to `'i'` rather than keeping a dot.
|
|
9
|
+
*/
|
|
10
|
+
export declare function foldText(text: string, locale?: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Whether `query` occurs in `text`, ignoring case and accents. A query of
|
|
13
|
+
* spaces only matches everything, as an empty one does.
|
|
14
|
+
*/
|
|
15
|
+
export declare function matchesText(text: string, query: string, locale?: string): boolean;
|
|
16
|
+
//# sourceMappingURL=text-match.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-match.d.ts","sourceRoot":"","sources":["../../src/lib/text-match.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9D;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAC1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,MAAM,GACb,OAAO,CAIT"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/lib/text-match.ts
|
|
2
|
+
var DIACRITICS = /[̀-ͯ]/g;
|
|
3
|
+
function foldText(text, locale) {
|
|
4
|
+
return lowercase(text, locale).normalize("NFD").replace(DIACRITICS, "");
|
|
5
|
+
}
|
|
6
|
+
function lowercase(text, locale) {
|
|
7
|
+
try {
|
|
8
|
+
return text.toLocaleLowerCase(locale);
|
|
9
|
+
} catch {
|
|
10
|
+
return text.toLocaleLowerCase();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function matchesText(text, query, locale) {
|
|
14
|
+
const needle = foldText(query.trim(), locale);
|
|
15
|
+
if (!needle)
|
|
16
|
+
return true;
|
|
17
|
+
return foldText(text, locale).includes(needle);
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
foldText,
|
|
21
|
+
matchesText
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
//# debugId=FCBE43FC1537D3D864756E2164756E21
|
|
25
|
+
//# sourceMappingURL=text-match.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/text-match.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Accent- and case-insensitive text matching, for a list filtered as the user\n * types: `'e'` finds \"École\", `'ecole'` finds \"ÉCOLE\".\n */\n\n/** Combining diacritical marks, which `normalize('NFD')` splits from their letter. */\nconst DIACRITICS = /[̀-ͯ]/g;\n\n/**\n * `text` lowercased for `locale` (the runtime's when omitted), then without\n * its diacritics: `foldText('Élève')` is `'eleve'`. Lowercasing comes first,\n * so a Turkish `'İ'` (`'tr'`) folds to `'i'` rather than keeping a dot.\n */\nexport function foldText(text: string, locale?: string): string {\n\treturn lowercase(text, locale).normalize('NFD').replace(DIACRITICS, '');\n}\n\nfunction lowercase(text: string, locale?: string): string {\n\ttry {\n\t\treturn text.toLocaleLowerCase(locale);\n\t} catch {\n\t\t// An invalid language tag: the runtime's own rules, never a throw.\n\t\treturn text.toLocaleLowerCase();\n\t}\n}\n\n/**\n * Whether `query` occurs in `text`, ignoring case and accents. A query of\n * spaces only matches everything, as an empty one does.\n */\nexport function matchesText(\n\ttext: string,\n\tquery: string,\n\tlocale?: string,\n): boolean {\n\tconst needle = foldText(query.trim(), locale);\n\tif (!needle) return true;\n\treturn foldText(text, locale).includes(needle);\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAMA,IAAM,aAAa;AAOZ,SAAS,QAAQ,CAAC,MAAc,QAAyB;AAAA,EAC/D,OAAO,UAAU,MAAM,MAAM,EAAE,UAAU,KAAK,EAAE,QAAQ,YAAY,EAAE;AAAA;AAGvE,SAAS,SAAS,CAAC,MAAc,QAAyB;AAAA,EACzD,IAAI;AAAA,IACH,OAAO,KAAK,kBAAkB,MAAM;AAAA,IACnC,MAAM;AAAA,IAEP,OAAO,KAAK,kBAAkB;AAAA;AAAA;AAQzB,SAAS,WAAW,CAC1B,MACA,OACA,QACU;AAAA,EACV,MAAM,SAAS,SAAS,MAAM,KAAK,GAAG,MAAM;AAAA,EAC5C,IAAI,CAAC;AAAA,IAAQ,OAAO;AAAA,EACpB,OAAO,SAAS,MAAM,MAAM,EAAE,SAAS,MAAM;AAAA;",
|
|
8
|
+
"debugId": "FCBE43FC1537D3D864756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/docs/roadmap.md
CHANGED
|
@@ -51,6 +51,11 @@ Nothing in progress right now; see Next.
|
|
|
51
51
|
|
|
52
52
|
## Shipped
|
|
53
53
|
|
|
54
|
+
- **Debounce, text matching and the async search** (`/debounce`,
|
|
55
|
+
`/text-match`, `/async-search`) — the debounce both libraries'
|
|
56
|
+
`SearchField` kept a copy of each, accent- and case-insensitive matching,
|
|
57
|
+
and a debounced search that aborts the previous run and drops a stale
|
|
58
|
+
response, behind both libraries' `Autocomplete` server search. 0.8.0.
|
|
54
59
|
- **The focus a toast's action hands back** (`/toast-focus`) — where the
|
|
55
60
|
focus was before it entered a toast, given back before the action runs, so
|
|
56
61
|
a dialog the action opens keeps the focus in both libraries. 0.7.0.
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/material-core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The framework-free core of @nxgt/material and @nxgt/material-vue: the PDF document model and its react-pdf renderer, print formatters, PDF file helpers, the PDF viewer's pure modules, the currency digits, the file-size formatter, the field-blur layers, a field's first focusable control, the aria-describedby joiner, the inert background of a modal layer, Escape forwarded while an overlay closes, the Toaster's touch-hover guard and its focus hand-back, the zod issue messages, the date range presets, the file preview's kind detection and text reader, the pdfkit fonts Vite plugin and the Node-only pdfkit fonts resolution for Next.js and Nuxt builds",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "The framework-free core of @nxgt/material and @nxgt/material-vue: the PDF document model and its react-pdf renderer, print formatters, PDF file helpers, the PDF viewer's pure modules, the currency digits, the file-size formatter, the field-blur layers, a field's first focusable control, the aria-describedby joiner, the inert background of a modal layer, Escape forwarded while an overlay closes, the Toaster's touch-hover guard and its focus hand-back, the zod issue messages, the date range presets, the debounce, accent-insensitive text matching and the race-free async search behind Autocomplete and SearchField, the file preview's kind detection and text reader, the pdfkit fonts Vite plugin and the Node-only pdfkit fonts resolution for Next.js and Nuxt builds",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"email": "stevegires@gmail.com"
|
|
8
8
|
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/softistx/nxgt-material.git",
|
|
12
|
+
"directory": "packages/core"
|
|
13
|
+
},
|
|
9
14
|
"type": "module",
|
|
10
15
|
"main": "./dist/index.js",
|
|
11
16
|
"types": "./dist/index.d.ts",
|
|
@@ -153,6 +158,21 @@
|
|
|
153
158
|
"import": "./dist/lib/date-range.js",
|
|
154
159
|
"default": "./dist/lib/date-range.js"
|
|
155
160
|
},
|
|
161
|
+
"./debounce": {
|
|
162
|
+
"types": "./dist/lib/debounce.d.ts",
|
|
163
|
+
"import": "./dist/lib/debounce.js",
|
|
164
|
+
"default": "./dist/lib/debounce.js"
|
|
165
|
+
},
|
|
166
|
+
"./text-match": {
|
|
167
|
+
"types": "./dist/lib/text-match.d.ts",
|
|
168
|
+
"import": "./dist/lib/text-match.js",
|
|
169
|
+
"default": "./dist/lib/text-match.js"
|
|
170
|
+
},
|
|
171
|
+
"./async-search": {
|
|
172
|
+
"types": "./dist/lib/async-search.d.ts",
|
|
173
|
+
"import": "./dist/lib/async-search.js",
|
|
174
|
+
"default": "./dist/lib/async-search.js"
|
|
175
|
+
},
|
|
156
176
|
"./file-preview": {
|
|
157
177
|
"types": "./dist/file-preview/index.d.ts",
|
|
158
178
|
"import": "./dist/file-preview/index.js",
|
|
@@ -229,6 +249,9 @@
|
|
|
229
249
|
"src/lib/slider-ticks.ts",
|
|
230
250
|
"src/lib/emoji-groups.ts",
|
|
231
251
|
"src/lib/date-range.ts",
|
|
252
|
+
"src/lib/debounce.ts",
|
|
253
|
+
"src/lib/text-match.ts",
|
|
254
|
+
"src/lib/async-search.ts",
|
|
232
255
|
"src/file-preview/index.ts",
|
|
233
256
|
"src/pdf-viewer/index.ts",
|
|
234
257
|
"src/vite/index.ts",
|