@f-ewald/components 0.1.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/LICENSE +28 -0
- package/README.md +147 -0
- package/custom-elements.json +3398 -0
- package/dist/address-autocomplete.d.ts +75 -0
- package/dist/address-autocomplete.d.ts.map +1 -0
- package/dist/address-autocomplete.js +342 -0
- package/dist/address-autocomplete.js.map +1 -0
- package/dist/animate-confetti.d.ts +53 -0
- package/dist/animate-confetti.d.ts.map +1 -0
- package/dist/animate-confetti.js +180 -0
- package/dist/animate-confetti.js.map +1 -0
- package/dist/autocomplete-input.d.ts +73 -0
- package/dist/autocomplete-input.d.ts.map +1 -0
- package/dist/autocomplete-input.js +309 -0
- package/dist/autocomplete-input.js.map +1 -0
- package/dist/confirm-dialog.d.ts +36 -0
- package/dist/confirm-dialog.d.ts.map +1 -0
- package/dist/confirm-dialog.js +191 -0
- package/dist/confirm-dialog.js.map +1 -0
- package/dist/copy-link-button.d.ts +27 -0
- package/dist/copy-link-button.d.ts.map +1 -0
- package/dist/copy-link-button.js +84 -0
- package/dist/copy-link-button.js.map +1 -0
- package/dist/distance-value.d.ts +24 -0
- package/dist/distance-value.d.ts.map +1 -0
- package/dist/distance-value.js +50 -0
- package/dist/distance-value.js.map +1 -0
- package/dist/distribution-chart.d.ts +52 -0
- package/dist/distribution-chart.d.ts.map +1 -0
- package/dist/distribution-chart.js +299 -0
- package/dist/distribution-chart.js.map +1 -0
- package/dist/icons.d.ts +32 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +35 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/percent-bar-chart.d.ts +29 -0
- package/dist/percent-bar-chart.d.ts.map +1 -0
- package/dist/percent-bar-chart.js +92 -0
- package/dist/percent-bar-chart.js.map +1 -0
- package/dist/price-history-chart.d.ts +39 -0
- package/dist/price-history-chart.d.ts.map +1 -0
- package/dist/price-history-chart.js +217 -0
- package/dist/price-history-chart.js.map +1 -0
- package/dist/relative-time.d.ts +26 -0
- package/dist/relative-time.d.ts.map +1 -0
- package/dist/relative-time.js +64 -0
- package/dist/relative-time.js.map +1 -0
- package/dist/reveal-button.d.ts +19 -0
- package/dist/reveal-button.d.ts.map +1 -0
- package/dist/reveal-button.js +62 -0
- package/dist/reveal-button.js.map +1 -0
- package/dist/roman-numeral.d.ts +18 -0
- package/dist/roman-numeral.d.ts.map +1 -0
- package/dist/roman-numeral.js +54 -0
- package/dist/roman-numeral.js.map +1 -0
- package/dist/slide-panel.d.ts +27 -0
- package/dist/slide-panel.d.ts.map +1 -0
- package/dist/slide-panel.js +149 -0
- package/dist/slide-panel.js.map +1 -0
- package/dist/toast-notification.d.ts +53 -0
- package/dist/toast-notification.d.ts.map +1 -0
- package/dist/toast-notification.js +173 -0
- package/dist/toast-notification.js.map +1 -0
- package/dist/tokens.css +21 -0
- package/dist/tokens.d.ts +16 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +79 -0
- package/dist/tokens.js.map +1 -0
- package/dist/utils/distance.d.ts +11 -0
- package/dist/utils/distance.d.ts.map +1 -0
- package/dist/utils/distance.js +25 -0
- package/dist/utils/distance.js.map +1 -0
- package/dist/utils/time.d.ts +10 -0
- package/dist/utils/time.d.ts.map +1 -0
- package/dist/utils/time.js +33 -0
- package/dist/utils/time.js.map +1 -0
- package/dist/weight-bar-chart.d.ts +29 -0
- package/dist/weight-bar-chart.d.ts.map +1 -0
- package/dist/weight-bar-chart.js +96 -0
- package/dist/weight-bar-chart.js.map +1 -0
- package/docs/address-autocomplete.md +68 -0
- package/docs/animate-confetti.md +34 -0
- package/docs/autocomplete-input.md +79 -0
- package/docs/confirm-dialog.md +65 -0
- package/docs/copy-link-button.md +46 -0
- package/docs/distance-value.md +38 -0
- package/docs/distribution-chart.md +51 -0
- package/docs/mcp-evaluation.md +31 -0
- package/docs/percent-bar-chart.md +40 -0
- package/docs/price-history-chart.md +44 -0
- package/docs/relative-time.md +37 -0
- package/docs/reveal-button.md +43 -0
- package/docs/roman-numeral.md +33 -0
- package/docs/slide-panel.md +56 -0
- package/docs/toast-notification.md +52 -0
- package/docs/weight-bar-chart.md +53 -0
- package/llms.txt +361 -0
- package/package.json +69 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { LitElement, type PropertyValues } from "lit";
|
|
2
|
+
/** A picked address suggestion. */
|
|
3
|
+
export interface AddressSuggestion {
|
|
4
|
+
address: string;
|
|
5
|
+
lat: number;
|
|
6
|
+
lng: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Form-associated text input with a suggestion dropdown. Works as a
|
|
10
|
+
* drop-in replacement for a plain `<input name="address">`: consumers keep
|
|
11
|
+
* reading `new FormData(form).get("address")` and calling `form.reset()`
|
|
12
|
+
* unchanged.
|
|
13
|
+
*
|
|
14
|
+
* Supports two suggestion sources:
|
|
15
|
+
* - **API** (default): fetches from `endpoint`, a Mapbox Geocoding v6-shaped
|
|
16
|
+
* forward-geocode URL, debounced as the user types.
|
|
17
|
+
* - **Local array**: set `suggestions` to a fixed `AddressSuggestion[]` and
|
|
18
|
+
* the component filters it client-side instead of making any network
|
|
19
|
+
* request. Useful for small/fixed address lists, offline use, or tests.
|
|
20
|
+
* Takes priority over `endpoint` whenever it's non-null.
|
|
21
|
+
*
|
|
22
|
+
* @element address-autocomplete
|
|
23
|
+
* @fires address-select - A suggestion was picked; detail: AddressSuggestion.
|
|
24
|
+
*/
|
|
25
|
+
export declare class AddressAutocomplete extends LitElement {
|
|
26
|
+
#private;
|
|
27
|
+
static formAssociated: boolean;
|
|
28
|
+
static styles: import("lit").CSSResult[];
|
|
29
|
+
/** Current input value; also the form-associated value submitted with the form. */
|
|
30
|
+
value: string;
|
|
31
|
+
/** Placeholder text shown when the input is empty. */
|
|
32
|
+
placeholder: string;
|
|
33
|
+
/** Marks the input as required for native form validation. */
|
|
34
|
+
required: boolean;
|
|
35
|
+
/** Mapbox `types` param, e.g. "address" or "address,poi". */
|
|
36
|
+
types: string;
|
|
37
|
+
/** Geocoding endpoint URL. Defaults to the Mapbox Geocoding v6 forward URL. */
|
|
38
|
+
endpoint: string;
|
|
39
|
+
/** Mapbox access token. Required for requests to succeed. */
|
|
40
|
+
accessToken: string;
|
|
41
|
+
/** Optional bounding box bias, comma-separated `west,south,east,north`. */
|
|
42
|
+
bbox: string;
|
|
43
|
+
/** Optional proximity bias, comma-separated `lng,lat`. */
|
|
44
|
+
proximity: string;
|
|
45
|
+
/** Debounce delay (ms) before firing a geocode request after typing. */
|
|
46
|
+
debounce: number;
|
|
47
|
+
/** Minimum query length before suggestions are fetched. */
|
|
48
|
+
minLength: number;
|
|
49
|
+
/**
|
|
50
|
+
* Fixed candidate list to filter locally instead of fetching from
|
|
51
|
+
* `endpoint`. When set (non-null), no network request is ever made —
|
|
52
|
+
* this takes priority over the API mode.
|
|
53
|
+
*/
|
|
54
|
+
suggestions: AddressSuggestion[] | null;
|
|
55
|
+
private _suggestions;
|
|
56
|
+
private _open;
|
|
57
|
+
private _activeIndex;
|
|
58
|
+
/** Last-picked suggestion, or null once the input diverges from it. */
|
|
59
|
+
get selectedSuggestion(): AddressSuggestion | null;
|
|
60
|
+
protected updated(changed: PropertyValues): void;
|
|
61
|
+
disconnectedCallback(): void;
|
|
62
|
+
/** Resets to empty on `form.reset()`, per the form-associated custom element contract. */
|
|
63
|
+
formResetCallback(): void;
|
|
64
|
+
private onInput;
|
|
65
|
+
private onKeydown;
|
|
66
|
+
private onBlur;
|
|
67
|
+
private renderSuggestions;
|
|
68
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
69
|
+
}
|
|
70
|
+
declare global {
|
|
71
|
+
interface HTMLElementTagNameMap {
|
|
72
|
+
"address-autocomplete": AddressAutocomplete;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=address-autocomplete.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"address-autocomplete.d.ts","sourceRoot":"","sources":["../src/address-autocomplete.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAI1E,mCAAmC;AACnC,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAOD;;;;;;;;;;;;;;;;GAgBG;AACH,qBACa,mBAAoB,SAAQ,UAAU;;IACjD,MAAM,CAAC,cAAc,UAAQ;IAE7B,OAAgB,MAAM,4BA0CpB;IAEF,mFAAmF;IACvE,KAAK,SAAM;IACvB,sDAAsD;IAC1C,WAAW,SAAM;IAC7B,8DAA8D;IACjC,QAAQ,UAAS;IAC9C,6DAA6D;IACjD,KAAK,SAAa;IAC9B,+EAA+E;IACnE,QAAQ,EAAE,MAAM,CAAsD;IAClF,6DAA6D;IACpB,WAAW,SAAM;IAC1D,2EAA2E;IAC/D,IAAI,SAAM;IACtB,0DAA0D;IAC9C,SAAS,SAAM;IAC3B,wEAAwE;IAC5C,QAAQ,SAAO;IAC3C,2DAA2D;IACN,SAAS,SAAK;IACnE;;;;OAIG;IAC6B,WAAW,EAAE,iBAAiB,EAAE,GAAG,IAAI,CAAQ;IAEtE,OAAO,CAAC,YAAY,CAA2B;IAC/C,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,YAAY,CAAM;IAOnC,uEAAuE;IACvE,IAAI,kBAAkB,IAAI,iBAAiB,GAAG,IAAI,CAGjD;IAED,UAAmB,OAAO,CAAC,OAAO,EAAE,cAAc,QAEjD;IAEQ,oBAAoB,SAI5B;IAED,0FAA0F;IAC1F,iBAAiB,SAKhB;IAED,OAAO,CAAC,OAAO;IAmEf,OAAO,CAAC,SAAS;IA2BjB,OAAO,CAAC,MAAM;IAkBd,OAAO,CAAC,iBAAiB;IAoBhB,MAAM,yCAgBd;CACF;AAcD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,sBAAsB,EAAE,mBAAmB,CAAC;KAC7C;CACF"}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { LitElement, css, html, nothing } from "lit";
|
|
8
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
9
|
+
import { tokens } from "./tokens.js";
|
|
10
|
+
/**
|
|
11
|
+
* Form-associated text input with a suggestion dropdown. Works as a
|
|
12
|
+
* drop-in replacement for a plain `<input name="address">`: consumers keep
|
|
13
|
+
* reading `new FormData(form).get("address")` and calling `form.reset()`
|
|
14
|
+
* unchanged.
|
|
15
|
+
*
|
|
16
|
+
* Supports two suggestion sources:
|
|
17
|
+
* - **API** (default): fetches from `endpoint`, a Mapbox Geocoding v6-shaped
|
|
18
|
+
* forward-geocode URL, debounced as the user types.
|
|
19
|
+
* - **Local array**: set `suggestions` to a fixed `AddressSuggestion[]` and
|
|
20
|
+
* the component filters it client-side instead of making any network
|
|
21
|
+
* request. Useful for small/fixed address lists, offline use, or tests.
|
|
22
|
+
* Takes priority over `endpoint` whenever it's non-null.
|
|
23
|
+
*
|
|
24
|
+
* @element address-autocomplete
|
|
25
|
+
* @fires address-select - A suggestion was picked; detail: AddressSuggestion.
|
|
26
|
+
*/
|
|
27
|
+
let AddressAutocomplete = class AddressAutocomplete extends LitElement {
|
|
28
|
+
constructor() {
|
|
29
|
+
super(...arguments);
|
|
30
|
+
/** Current input value; also the form-associated value submitted with the form. */
|
|
31
|
+
this.value = "";
|
|
32
|
+
/** Placeholder text shown when the input is empty. */
|
|
33
|
+
this.placeholder = "";
|
|
34
|
+
/** Marks the input as required for native form validation. */
|
|
35
|
+
this.required = false;
|
|
36
|
+
/** Mapbox `types` param, e.g. "address" or "address,poi". */
|
|
37
|
+
this.types = "address";
|
|
38
|
+
/** Geocoding endpoint URL. Defaults to the Mapbox Geocoding v6 forward URL. */
|
|
39
|
+
this.endpoint = "https://api.mapbox.com/search/geocode/v6/forward";
|
|
40
|
+
/** Mapbox access token. Required for requests to succeed. */
|
|
41
|
+
this.accessToken = "";
|
|
42
|
+
/** Optional bounding box bias, comma-separated `west,south,east,north`. */
|
|
43
|
+
this.bbox = "";
|
|
44
|
+
/** Optional proximity bias, comma-separated `lng,lat`. */
|
|
45
|
+
this.proximity = "";
|
|
46
|
+
/** Debounce delay (ms) before firing a geocode request after typing. */
|
|
47
|
+
this.debounce = 300;
|
|
48
|
+
/** Minimum query length before suggestions are fetched. */
|
|
49
|
+
this.minLength = 3;
|
|
50
|
+
/**
|
|
51
|
+
* Fixed candidate list to filter locally instead of fetching from
|
|
52
|
+
* `endpoint`. When set (non-null), no network request is ever made —
|
|
53
|
+
* this takes priority over the API mode.
|
|
54
|
+
*/
|
|
55
|
+
this.suggestions = null;
|
|
56
|
+
this._suggestions = [];
|
|
57
|
+
this._open = false;
|
|
58
|
+
this._activeIndex = -1;
|
|
59
|
+
this.#internals = this.attachInternals();
|
|
60
|
+
this.#lastPicked = null;
|
|
61
|
+
this.#debounceTimer = null;
|
|
62
|
+
this.#abortController = null;
|
|
63
|
+
}
|
|
64
|
+
static { this.formAssociated = true; }
|
|
65
|
+
static { this.styles = [
|
|
66
|
+
tokens,
|
|
67
|
+
css `
|
|
68
|
+
:host {
|
|
69
|
+
display: block;
|
|
70
|
+
position: relative;
|
|
71
|
+
}
|
|
72
|
+
input {
|
|
73
|
+
font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);
|
|
74
|
+
font-size: var(--ui-font-size-sm, 0.75rem);
|
|
75
|
+
padding: 0.35rem 0.5rem;
|
|
76
|
+
border: 1px solid var(--ui-border, #e2e8f0);
|
|
77
|
+
border-radius: var(--ui-radius-sm, 0.25rem);
|
|
78
|
+
width: 100%;
|
|
79
|
+
box-sizing: border-box;
|
|
80
|
+
}
|
|
81
|
+
.suggestions {
|
|
82
|
+
position: absolute;
|
|
83
|
+
top: 100%;
|
|
84
|
+
left: 0;
|
|
85
|
+
right: 0;
|
|
86
|
+
z-index: 10;
|
|
87
|
+
max-height: 40vh;
|
|
88
|
+
overflow-y: auto;
|
|
89
|
+
margin: 2px 0 0;
|
|
90
|
+
padding: 4px 0;
|
|
91
|
+
list-style: none;
|
|
92
|
+
background: var(--ui-surface, #fff);
|
|
93
|
+
border: 1px solid var(--ui-border, #e2e8f0);
|
|
94
|
+
border-radius: var(--ui-radius-sm, 0.25rem);
|
|
95
|
+
box-shadow: var(--ui-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1));
|
|
96
|
+
}
|
|
97
|
+
.suggestion {
|
|
98
|
+
padding: 0.4rem 0.6rem;
|
|
99
|
+
font-size: var(--ui-font-size-sm, 0.75rem);
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
}
|
|
102
|
+
.suggestion.active,
|
|
103
|
+
.suggestion:hover {
|
|
104
|
+
background: var(--ui-surface-muted, #f8fafc);
|
|
105
|
+
}
|
|
106
|
+
`,
|
|
107
|
+
]; }
|
|
108
|
+
#internals;
|
|
109
|
+
#lastPicked;
|
|
110
|
+
#debounceTimer;
|
|
111
|
+
#abortController;
|
|
112
|
+
/** Last-picked suggestion, or null once the input diverges from it. */
|
|
113
|
+
get selectedSuggestion() {
|
|
114
|
+
if (this.#lastPicked && this.#lastPicked.address === this.value)
|
|
115
|
+
return this.#lastPicked;
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
updated(changed) {
|
|
119
|
+
if (changed.has("value"))
|
|
120
|
+
this.#internals.setFormValue(this.value);
|
|
121
|
+
}
|
|
122
|
+
disconnectedCallback() {
|
|
123
|
+
super.disconnectedCallback();
|
|
124
|
+
if (this.#debounceTimer)
|
|
125
|
+
clearTimeout(this.#debounceTimer);
|
|
126
|
+
this.#abortController?.abort();
|
|
127
|
+
}
|
|
128
|
+
/** Resets to empty on `form.reset()`, per the form-associated custom element contract. */
|
|
129
|
+
formResetCallback() {
|
|
130
|
+
this.value = "";
|
|
131
|
+
this._suggestions = [];
|
|
132
|
+
this._open = false;
|
|
133
|
+
this.#lastPicked = null;
|
|
134
|
+
}
|
|
135
|
+
onInput(e) {
|
|
136
|
+
this.value = e.target.value;
|
|
137
|
+
this.#lastPicked = null;
|
|
138
|
+
this.#scheduleFetch(this.value);
|
|
139
|
+
}
|
|
140
|
+
#scheduleFetch(query) {
|
|
141
|
+
if (this.#debounceTimer)
|
|
142
|
+
clearTimeout(this.#debounceTimer);
|
|
143
|
+
if (query.trim().length < this.minLength) {
|
|
144
|
+
this._suggestions = [];
|
|
145
|
+
this._open = false;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (this.suggestions) {
|
|
149
|
+
// Local mode: filtering is synchronous and cheap, but keep the same
|
|
150
|
+
// debounce as the API mode so the UX doesn't shift between sources.
|
|
151
|
+
this.#debounceTimer = setTimeout(() => this.#filterLocalSuggestions(query), this.debounce);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.#debounceTimer = setTimeout(() => void this.#fetchSuggestions(query), this.debounce);
|
|
155
|
+
}
|
|
156
|
+
/** Filters the locally-supplied `suggestions` list for `query`, case-insensitively. */
|
|
157
|
+
#filterLocalSuggestions(query) {
|
|
158
|
+
const needle = query.trim().toLowerCase();
|
|
159
|
+
this._suggestions = (this.suggestions ?? [])
|
|
160
|
+
.filter((s) => s.address.toLowerCase().includes(needle))
|
|
161
|
+
.slice(0, 5);
|
|
162
|
+
this._activeIndex = -1;
|
|
163
|
+
this._open = this._suggestions.length > 0;
|
|
164
|
+
}
|
|
165
|
+
/** Fetches suggestions for `query`, cancelling any request still in flight. */
|
|
166
|
+
async #fetchSuggestions(query) {
|
|
167
|
+
this.#abortController?.abort();
|
|
168
|
+
const controller = new AbortController();
|
|
169
|
+
this.#abortController = controller;
|
|
170
|
+
try {
|
|
171
|
+
const res = await fetch(this.#buildUrl(query), { signal: controller.signal });
|
|
172
|
+
if (!res.ok)
|
|
173
|
+
throw new Error(`geocode request failed: ${res.status}`);
|
|
174
|
+
const body = (await res.json());
|
|
175
|
+
this._suggestions = parseFeatures(body.features ?? []);
|
|
176
|
+
this._activeIndex = -1;
|
|
177
|
+
this._open = this._suggestions.length > 0;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Aborted (superseded by newer keystroke) or a network failure while
|
|
181
|
+
// typing — fail silently and just close the list.
|
|
182
|
+
if (!controller.signal.aborted) {
|
|
183
|
+
this._suggestions = [];
|
|
184
|
+
this._open = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
#buildUrl(query) {
|
|
189
|
+
const params = new URLSearchParams({
|
|
190
|
+
q: query,
|
|
191
|
+
access_token: this.accessToken,
|
|
192
|
+
autocomplete: "true",
|
|
193
|
+
limit: "5",
|
|
194
|
+
types: this.types,
|
|
195
|
+
});
|
|
196
|
+
if (this.bbox)
|
|
197
|
+
params.set("bbox", this.bbox);
|
|
198
|
+
if (this.proximity)
|
|
199
|
+
params.set("proximity", this.proximity);
|
|
200
|
+
return `${this.endpoint}?${params.toString()}`;
|
|
201
|
+
}
|
|
202
|
+
onKeydown(e) {
|
|
203
|
+
if (e.key === "ArrowDown")
|
|
204
|
+
this.#moveActive(1, e);
|
|
205
|
+
else if (e.key === "ArrowUp")
|
|
206
|
+
this.#moveActive(-1, e);
|
|
207
|
+
else if (e.key === "Enter")
|
|
208
|
+
this.#handleEnter(e);
|
|
209
|
+
else if (e.key === "Escape")
|
|
210
|
+
this.#handleEscape(e);
|
|
211
|
+
}
|
|
212
|
+
#moveActive(delta, e) {
|
|
213
|
+
if (!this._open || this._suggestions.length === 0)
|
|
214
|
+
return;
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
const n = this._suggestions.length;
|
|
217
|
+
this._activeIndex = (this._activeIndex + delta + n) % n;
|
|
218
|
+
}
|
|
219
|
+
#handleEnter(e) {
|
|
220
|
+
if (!this._open)
|
|
221
|
+
return; // closed list: let the form submit normally
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
const pick = this._suggestions[this._activeIndex] ?? this._suggestions[0];
|
|
224
|
+
if (pick)
|
|
225
|
+
this.#selectSuggestion(pick);
|
|
226
|
+
}
|
|
227
|
+
#handleEscape(e) {
|
|
228
|
+
if (!this._open)
|
|
229
|
+
return;
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
this._open = false;
|
|
232
|
+
}
|
|
233
|
+
onBlur() {
|
|
234
|
+
this._open = false;
|
|
235
|
+
}
|
|
236
|
+
#selectSuggestion(s) {
|
|
237
|
+
this.value = s.address;
|
|
238
|
+
this.#lastPicked = s;
|
|
239
|
+
this._open = false;
|
|
240
|
+
this._suggestions = [];
|
|
241
|
+
this.dispatchEvent(new CustomEvent("address-select", { detail: s, bubbles: true, composed: true }));
|
|
242
|
+
}
|
|
243
|
+
/** Selects on mousedown (not click) so it wins over the input's blur, which closes the list. */
|
|
244
|
+
#onSuggestionMousedown(e, s) {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
this.#selectSuggestion(s);
|
|
247
|
+
}
|
|
248
|
+
renderSuggestions() {
|
|
249
|
+
if (!this._open || this._suggestions.length === 0)
|
|
250
|
+
return nothing;
|
|
251
|
+
return html `
|
|
252
|
+
<ul class="suggestions" role="listbox">
|
|
253
|
+
${this._suggestions.map((s, i) => html `
|
|
254
|
+
<li
|
|
255
|
+
role="option"
|
|
256
|
+
aria-selected=${i === this._activeIndex}
|
|
257
|
+
class="suggestion ${i === this._activeIndex ? "active" : ""}"
|
|
258
|
+
@mousedown=${(e) => this.#onSuggestionMousedown(e, s)}
|
|
259
|
+
>
|
|
260
|
+
${s.address}
|
|
261
|
+
</li>
|
|
262
|
+
`)}
|
|
263
|
+
</ul>
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
render() {
|
|
267
|
+
return html `
|
|
268
|
+
<div role="combobox" aria-expanded=${this._open} aria-haspopup="listbox">
|
|
269
|
+
<input
|
|
270
|
+
type="text"
|
|
271
|
+
.value=${this.value}
|
|
272
|
+
placeholder=${this.placeholder}
|
|
273
|
+
?required=${this.required}
|
|
274
|
+
autocomplete="off"
|
|
275
|
+
@input=${this.onInput}
|
|
276
|
+
@keydown=${this.onKeydown}
|
|
277
|
+
@blur=${this.onBlur}
|
|
278
|
+
/>
|
|
279
|
+
${this.renderSuggestions()}
|
|
280
|
+
</div>
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
__decorate([
|
|
285
|
+
property()
|
|
286
|
+
], AddressAutocomplete.prototype, "value", void 0);
|
|
287
|
+
__decorate([
|
|
288
|
+
property()
|
|
289
|
+
], AddressAutocomplete.prototype, "placeholder", void 0);
|
|
290
|
+
__decorate([
|
|
291
|
+
property({ type: Boolean })
|
|
292
|
+
], AddressAutocomplete.prototype, "required", void 0);
|
|
293
|
+
__decorate([
|
|
294
|
+
property()
|
|
295
|
+
], AddressAutocomplete.prototype, "types", void 0);
|
|
296
|
+
__decorate([
|
|
297
|
+
property()
|
|
298
|
+
], AddressAutocomplete.prototype, "endpoint", void 0);
|
|
299
|
+
__decorate([
|
|
300
|
+
property({ attribute: "access-token" })
|
|
301
|
+
], AddressAutocomplete.prototype, "accessToken", void 0);
|
|
302
|
+
__decorate([
|
|
303
|
+
property()
|
|
304
|
+
], AddressAutocomplete.prototype, "bbox", void 0);
|
|
305
|
+
__decorate([
|
|
306
|
+
property()
|
|
307
|
+
], AddressAutocomplete.prototype, "proximity", void 0);
|
|
308
|
+
__decorate([
|
|
309
|
+
property({ type: Number })
|
|
310
|
+
], AddressAutocomplete.prototype, "debounce", void 0);
|
|
311
|
+
__decorate([
|
|
312
|
+
property({ type: Number, attribute: "min-length" })
|
|
313
|
+
], AddressAutocomplete.prototype, "minLength", void 0);
|
|
314
|
+
__decorate([
|
|
315
|
+
property({ attribute: false })
|
|
316
|
+
], AddressAutocomplete.prototype, "suggestions", void 0);
|
|
317
|
+
__decorate([
|
|
318
|
+
state()
|
|
319
|
+
], AddressAutocomplete.prototype, "_suggestions", void 0);
|
|
320
|
+
__decorate([
|
|
321
|
+
state()
|
|
322
|
+
], AddressAutocomplete.prototype, "_open", void 0);
|
|
323
|
+
__decorate([
|
|
324
|
+
state()
|
|
325
|
+
], AddressAutocomplete.prototype, "_activeIndex", void 0);
|
|
326
|
+
AddressAutocomplete = __decorate([
|
|
327
|
+
customElement("address-autocomplete")
|
|
328
|
+
], AddressAutocomplete);
|
|
329
|
+
export { AddressAutocomplete };
|
|
330
|
+
/** Maps Mapbox v6 forward-geocode features to suggestions, skipping ones without a full_address. */
|
|
331
|
+
function parseFeatures(features) {
|
|
332
|
+
const suggestions = [];
|
|
333
|
+
for (const f of features) {
|
|
334
|
+
const address = f.properties?.full_address;
|
|
335
|
+
if (!address)
|
|
336
|
+
continue;
|
|
337
|
+
const [lng, lat] = f.geometry.coordinates;
|
|
338
|
+
suggestions.push({ address, lng, lat });
|
|
339
|
+
}
|
|
340
|
+
return suggestions;
|
|
341
|
+
}
|
|
342
|
+
//# sourceMappingURL=address-autocomplete.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"address-autocomplete.js","sourceRoot":"","sources":["../src/address-autocomplete.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAuB,MAAM,KAAK,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAcrC;;;;;;;;;;;;;;;;GAgBG;AAEI,IAAM,mBAAmB,GAAzB,MAAM,mBAAoB,SAAQ,UAAU;IAA5C;;QA+CL,mFAAmF;QACvE,UAAK,GAAG,EAAE,CAAC;QACvB,sDAAsD;QAC1C,gBAAW,GAAG,EAAE,CAAC;QAC7B,8DAA8D;QACjC,aAAQ,GAAG,KAAK,CAAC;QAC9C,6DAA6D;QACjD,UAAK,GAAG,SAAS,CAAC;QAC9B,+EAA+E;QACnE,aAAQ,GAAW,kDAAkD,CAAC;QAClF,6DAA6D;QACpB,gBAAW,GAAG,EAAE,CAAC;QAC1D,2EAA2E;QAC/D,SAAI,GAAG,EAAE,CAAC;QACtB,0DAA0D;QAC9C,cAAS,GAAG,EAAE,CAAC;QAC3B,wEAAwE;QAC5C,aAAQ,GAAG,GAAG,CAAC;QAC3C,2DAA2D;QACN,cAAS,GAAG,CAAC,CAAC;QACnE;;;;WAIG;QAC6B,gBAAW,GAA+B,IAAI,CAAC;QAE9D,iBAAY,GAAwB,EAAE,CAAC;QACvC,UAAK,GAAG,KAAK,CAAC;QACd,iBAAY,GAAG,CAAC,CAAC,CAAC;QAEnC,eAAU,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QACpC,gBAAW,GAA6B,IAAI,CAAC;QAC7C,mBAAc,GAAyC,IAAI,CAAC;QAC5D,qBAAgB,GAA2B,IAAI,CAAC;IA+KlD,CAAC;aA/PQ,mBAAc,GAAG,IAAI,AAAP,CAAQ;aAEb,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAuCF;KACF,AA1CqB,CA0CpB;IAiCF,UAAU,CAA0B;IACpC,WAAW,CAAkC;IAC7C,cAAc,CAA8C;IAC5D,gBAAgB,CAAgC;IAEhD,uEAAuE;IACvE,IAAI,kBAAkB;QACpB,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,KAAK,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC,WAAW,CAAC;QACzF,OAAO,IAAI,CAAC;IACd,CAAC;IAEkB,OAAO,CAAC,OAAuB;QAChD,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrE,CAAC;IAEQ,oBAAoB;QAC3B,KAAK,CAAC,oBAAoB,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,cAAc;YAAE,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAC3D,IAAI,CAAC,gBAAgB,EAAE,KAAK,EAAE,CAAC;IACjC,CAAC;IAED,0FAA0F;IAC1F,iBAAiB;QACf,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAChB,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;IAC1B,CAAC;IAEO,OAAO,CAAC,CAAa;QAC3B,IAAI,CAAC,KAAK,GAAI,CAAC,CAAC,MAA2B,CAAC,KAAK,CAAC;QAClD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;IAED,cAAc,CAAC,KAAa;QAC1B,IAAI,IAAI,CAAC,cAAc;YAAE,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAC3D,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YACzC,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;YACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;YACnB,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,oEAAoE;YACpE,oEAAoE;YACpE,IAAI,CAAC,cAAc,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,uBAAuB,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC3F,OAAO;QACT,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC5F,CAAC;IAED,uFAAuF;IACvF,uBAAuB,CAAC,KAAa;QACnC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC1C,IAAI,CAAC,YAAY,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC;aACzC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;aACvD,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACf,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;IAC5C,CAAC;IAED,+EAA+E;IAC/E,KAAK,CAAC,iBAAiB,CAAC,KAAa;QACnC,IAAI,CAAC,gBAAgB,EAAE,KAAK,EAAE,CAAC;QAC/B,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,IAAI,CAAC,gBAAgB,GAAG,UAAU,CAAC;QACnC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;YAC9E,IAAI,CAAC,GAAG,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;YACtE,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA0C,CAAC;YACzE,IAAI,CAAC,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;YACvD,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC;YACvB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;QAC5C,CAAC;QAAC,MAAM,CAAC;YACP,qEAAqE;YACrE,kDAAkD;YAClD,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC/B,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;gBACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;YACrB,CAAC;QACH,CAAC;IACH,CAAC;IAED,SAAS,CAAC,KAAa;QACrB,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;YACjC,CAAC,EAAE,KAAK;YACR,YAAY,EAAE,IAAI,CAAC,WAAW;YAC9B,YAAY,EAAE,MAAM;YACpB,KAAK,EAAE,GAAG;YACV,KAAK,EAAE,IAAI,CAAC,KAAK;SAClB,CAAC,CAAC;QACH,IAAI,IAAI,CAAC,IAAI;YAAE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,IAAI,IAAI,CAAC,SAAS;YAAE,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAC5D,OAAO,GAAG,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;IACjD,CAAC;IAEO,SAAS,CAAC,CAAgB;QAChC,IAAI,CAAC,CAAC,GAAG,KAAK,WAAW;YAAE,IAAI,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;aAC7C,IAAI,CAAC,CAAC,GAAG,KAAK,SAAS;YAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;aACjD,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO;YAAE,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;aAC5C,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ;YAAE,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IACrD,CAAC;IAED,WAAW,CAAC,KAAa,EAAE,CAAgB;QACzC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC1D,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,MAAM,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;QACnC,IAAI,CAAC,YAAY,GAAG,CAAC,IAAI,CAAC,YAAY,GAAG,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;IAC1D,CAAC;IAED,YAAY,CAAC,CAAgB;QAC3B,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,4CAA4C;QACrE,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC1E,IAAI,IAAI;YAAE,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;IACzC,CAAC;IAED,aAAa,CAAC,CAAgB;QAC5B,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO;QACxB,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAEO,MAAM;QACZ,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAED,iBAAiB,CAAC,CAAoB;QACpC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC;QACvB,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAoB,gBAAgB,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACzH,CAAC;IAED,gGAAgG;IAChG,sBAAsB,CAAC,CAAa,EAAE,CAAoB;QACxD,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAC5B,CAAC;IAEO,iBAAiB;QACvB,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,OAAO,CAAC;QAClE,OAAO,IAAI,CAAA;;UAEL,IAAI,CAAC,YAAY,CAAC,GAAG,CACrB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAA;;;8BAGM,CAAC,KAAK,IAAI,CAAC,YAAY;kCACnB,CAAC,KAAK,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;2BAC9C,CAAC,CAAa,EAAE,EAAE,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC;;gBAE/D,CAAC,CAAC,OAAO;;WAEd,CACF;;KAEJ,CAAC;IACJ,CAAC;IAEQ,MAAM;QACb,OAAO,IAAI,CAAA;2CAC4B,IAAI,CAAC,KAAK;;;mBAGlC,IAAI,CAAC,KAAK;wBACL,IAAI,CAAC,WAAW;sBAClB,IAAI,CAAC,QAAQ;;mBAEhB,IAAI,CAAC,OAAO;qBACV,IAAI,CAAC,SAAS;kBACjB,IAAI,CAAC,MAAM;;UAEnB,IAAI,CAAC,iBAAiB,EAAE;;KAE7B,CAAC;IACJ,CAAC;CACF,CAAA;AAhNa;IAAX,QAAQ,EAAE;kDAAY;AAEX;IAAX,QAAQ,EAAE;wDAAkB;AAEA;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;qDAAkB;AAElC;IAAX,QAAQ,EAAE;kDAAmB;AAElB;IAAX,QAAQ,EAAE;qDAAuE;AAEzC;IAAxC,QAAQ,CAAC,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC;wDAAkB;AAE9C;IAAX,QAAQ,EAAE;iDAAW;AAEV;IAAX,QAAQ,EAAE;sDAAgB;AAEC;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;qDAAgB;AAEU;IAApD,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;sDAAe;AAMnC;IAA/B,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;wDAAgD;AAE9D;IAAhB,KAAK,EAAE;yDAAgD;AACvC;IAAhB,KAAK,EAAE;kDAAuB;AACd;IAAhB,KAAK,EAAE;yDAA2B;AA5ExB,mBAAmB;IAD/B,aAAa,CAAC,sBAAsB,CAAC;GACzB,mBAAmB,CAgQ/B;;AAED,oGAAoG;AACpG,SAAS,aAAa,CAAC,QAAgC;IACrD,MAAM,WAAW,GAAwB,EAAE,CAAC;IAC5C,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,CAAC,CAAC,UAAU,EAAE,YAAY,CAAC;QAC3C,IAAI,CAAC,OAAO;YAAE,SAAS;QACvB,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;QAC1C,WAAW,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC","sourcesContent":["import { LitElement, css, html, nothing, type PropertyValues } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { tokens } from \"./tokens.js\";\n\n/** A picked address suggestion. */\nexport interface AddressSuggestion {\n address: string;\n lat: number;\n lng: number;\n}\n\ninterface MapboxGeocodeFeature {\n properties?: { full_address?: string };\n geometry: { coordinates: [number, number] };\n}\n\n/**\n * Form-associated text input with a suggestion dropdown. Works as a\n * drop-in replacement for a plain `<input name=\"address\">`: consumers keep\n * reading `new FormData(form).get(\"address\")` and calling `form.reset()`\n * unchanged.\n *\n * Supports two suggestion sources:\n * - **API** (default): fetches from `endpoint`, a Mapbox Geocoding v6-shaped\n * forward-geocode URL, debounced as the user types.\n * - **Local array**: set `suggestions` to a fixed `AddressSuggestion[]` and\n * the component filters it client-side instead of making any network\n * request. Useful for small/fixed address lists, offline use, or tests.\n * Takes priority over `endpoint` whenever it's non-null.\n *\n * @element address-autocomplete\n * @fires address-select - A suggestion was picked; detail: AddressSuggestion.\n */\n@customElement(\"address-autocomplete\")\nexport class AddressAutocomplete extends LitElement {\n static formAssociated = true;\n\n static override styles = [\n tokens,\n css`\n :host {\n display: block;\n position: relative;\n }\n input {\n font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);\n font-size: var(--ui-font-size-sm, 0.75rem);\n padding: 0.35rem 0.5rem;\n border: 1px solid var(--ui-border, #e2e8f0);\n border-radius: var(--ui-radius-sm, 0.25rem);\n width: 100%;\n box-sizing: border-box;\n }\n .suggestions {\n position: absolute;\n top: 100%;\n left: 0;\n right: 0;\n z-index: 10;\n max-height: 40vh;\n overflow-y: auto;\n margin: 2px 0 0;\n padding: 4px 0;\n list-style: none;\n background: var(--ui-surface, #fff);\n border: 1px solid var(--ui-border, #e2e8f0);\n border-radius: var(--ui-radius-sm, 0.25rem);\n box-shadow: var(--ui-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1));\n }\n .suggestion {\n padding: 0.4rem 0.6rem;\n font-size: var(--ui-font-size-sm, 0.75rem);\n cursor: pointer;\n }\n .suggestion.active,\n .suggestion:hover {\n background: var(--ui-surface-muted, #f8fafc);\n }\n `,\n ];\n\n /** Current input value; also the form-associated value submitted with the form. */\n @property() value = \"\";\n /** Placeholder text shown when the input is empty. */\n @property() placeholder = \"\";\n /** Marks the input as required for native form validation. */\n @property({ type: Boolean }) required = false;\n /** Mapbox `types` param, e.g. \"address\" or \"address,poi\". */\n @property() types = \"address\";\n /** Geocoding endpoint URL. Defaults to the Mapbox Geocoding v6 forward URL. */\n @property() endpoint: string = \"https://api.mapbox.com/search/geocode/v6/forward\";\n /** Mapbox access token. Required for requests to succeed. */\n @property({ attribute: \"access-token\" }) accessToken = \"\";\n /** Optional bounding box bias, comma-separated `west,south,east,north`. */\n @property() bbox = \"\";\n /** Optional proximity bias, comma-separated `lng,lat`. */\n @property() proximity = \"\";\n /** Debounce delay (ms) before firing a geocode request after typing. */\n @property({ type: Number }) debounce = 300;\n /** Minimum query length before suggestions are fetched. */\n @property({ type: Number, attribute: \"min-length\" }) minLength = 3;\n /**\n * Fixed candidate list to filter locally instead of fetching from\n * `endpoint`. When set (non-null), no network request is ever made —\n * this takes priority over the API mode.\n */\n @property({ attribute: false }) suggestions: AddressSuggestion[] | null = null;\n\n @state() private _suggestions: AddressSuggestion[] = [];\n @state() private _open = false;\n @state() private _activeIndex = -1;\n\n #internals = this.attachInternals();\n #lastPicked: AddressSuggestion | null = null;\n #debounceTimer: ReturnType<typeof setTimeout> | null = null;\n #abortController: AbortController | null = null;\n\n /** Last-picked suggestion, or null once the input diverges from it. */\n get selectedSuggestion(): AddressSuggestion | null {\n if (this.#lastPicked && this.#lastPicked.address === this.value) return this.#lastPicked;\n return null;\n }\n\n protected override updated(changed: PropertyValues) {\n if (changed.has(\"value\")) this.#internals.setFormValue(this.value);\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n if (this.#debounceTimer) clearTimeout(this.#debounceTimer);\n this.#abortController?.abort();\n }\n\n /** Resets to empty on `form.reset()`, per the form-associated custom element contract. */\n formResetCallback() {\n this.value = \"\";\n this._suggestions = [];\n this._open = false;\n this.#lastPicked = null;\n }\n\n private onInput(e: InputEvent) {\n this.value = (e.target as HTMLInputElement).value;\n this.#lastPicked = null;\n this.#scheduleFetch(this.value);\n }\n\n #scheduleFetch(query: string) {\n if (this.#debounceTimer) clearTimeout(this.#debounceTimer);\n if (query.trim().length < this.minLength) {\n this._suggestions = [];\n this._open = false;\n return;\n }\n if (this.suggestions) {\n // Local mode: filtering is synchronous and cheap, but keep the same\n // debounce as the API mode so the UX doesn't shift between sources.\n this.#debounceTimer = setTimeout(() => this.#filterLocalSuggestions(query), this.debounce);\n return;\n }\n this.#debounceTimer = setTimeout(() => void this.#fetchSuggestions(query), this.debounce);\n }\n\n /** Filters the locally-supplied `suggestions` list for `query`, case-insensitively. */\n #filterLocalSuggestions(query: string) {\n const needle = query.trim().toLowerCase();\n this._suggestions = (this.suggestions ?? [])\n .filter((s) => s.address.toLowerCase().includes(needle))\n .slice(0, 5);\n this._activeIndex = -1;\n this._open = this._suggestions.length > 0;\n }\n\n /** Fetches suggestions for `query`, cancelling any request still in flight. */\n async #fetchSuggestions(query: string) {\n this.#abortController?.abort();\n const controller = new AbortController();\n this.#abortController = controller;\n try {\n const res = await fetch(this.#buildUrl(query), { signal: controller.signal });\n if (!res.ok) throw new Error(`geocode request failed: ${res.status}`);\n const body = (await res.json()) as { features?: MapboxGeocodeFeature[] };\n this._suggestions = parseFeatures(body.features ?? []);\n this._activeIndex = -1;\n this._open = this._suggestions.length > 0;\n } catch {\n // Aborted (superseded by newer keystroke) or a network failure while\n // typing — fail silently and just close the list.\n if (!controller.signal.aborted) {\n this._suggestions = [];\n this._open = false;\n }\n }\n }\n\n #buildUrl(query: string): string {\n const params = new URLSearchParams({\n q: query,\n access_token: this.accessToken,\n autocomplete: \"true\",\n limit: \"5\",\n types: this.types,\n });\n if (this.bbox) params.set(\"bbox\", this.bbox);\n if (this.proximity) params.set(\"proximity\", this.proximity);\n return `${this.endpoint}?${params.toString()}`;\n }\n\n private onKeydown(e: KeyboardEvent) {\n if (e.key === \"ArrowDown\") this.#moveActive(1, e);\n else if (e.key === \"ArrowUp\") this.#moveActive(-1, e);\n else if (e.key === \"Enter\") this.#handleEnter(e);\n else if (e.key === \"Escape\") this.#handleEscape(e);\n }\n\n #moveActive(delta: number, e: KeyboardEvent) {\n if (!this._open || this._suggestions.length === 0) return;\n e.preventDefault();\n const n = this._suggestions.length;\n this._activeIndex = (this._activeIndex + delta + n) % n;\n }\n\n #handleEnter(e: KeyboardEvent) {\n if (!this._open) return; // closed list: let the form submit normally\n e.preventDefault();\n const pick = this._suggestions[this._activeIndex] ?? this._suggestions[0];\n if (pick) this.#selectSuggestion(pick);\n }\n\n #handleEscape(e: KeyboardEvent) {\n if (!this._open) return;\n e.preventDefault();\n this._open = false;\n }\n\n private onBlur() {\n this._open = false;\n }\n\n #selectSuggestion(s: AddressSuggestion) {\n this.value = s.address;\n this.#lastPicked = s;\n this._open = false;\n this._suggestions = [];\n this.dispatchEvent(new CustomEvent<AddressSuggestion>(\"address-select\", { detail: s, bubbles: true, composed: true }));\n }\n\n /** Selects on mousedown (not click) so it wins over the input's blur, which closes the list. */\n #onSuggestionMousedown(e: MouseEvent, s: AddressSuggestion) {\n e.preventDefault();\n this.#selectSuggestion(s);\n }\n\n private renderSuggestions() {\n if (!this._open || this._suggestions.length === 0) return nothing;\n return html`\n <ul class=\"suggestions\" role=\"listbox\">\n ${this._suggestions.map(\n (s, i) => html`\n <li\n role=\"option\"\n aria-selected=${i === this._activeIndex}\n class=\"suggestion ${i === this._activeIndex ? \"active\" : \"\"}\"\n @mousedown=${(e: MouseEvent) => this.#onSuggestionMousedown(e, s)}\n >\n ${s.address}\n </li>\n `,\n )}\n </ul>\n `;\n }\n\n override render() {\n return html`\n <div role=\"combobox\" aria-expanded=${this._open} aria-haspopup=\"listbox\">\n <input\n type=\"text\"\n .value=${this.value}\n placeholder=${this.placeholder}\n ?required=${this.required}\n autocomplete=\"off\"\n @input=${this.onInput}\n @keydown=${this.onKeydown}\n @blur=${this.onBlur}\n />\n ${this.renderSuggestions()}\n </div>\n `;\n }\n}\n\n/** Maps Mapbox v6 forward-geocode features to suggestions, skipping ones without a full_address. */\nfunction parseFeatures(features: MapboxGeocodeFeature[]): AddressSuggestion[] {\n const suggestions: AddressSuggestion[] = [];\n for (const f of features) {\n const address = f.properties?.full_address;\n if (!address) continue;\n const [lng, lat] = f.geometry.coordinates;\n suggestions.push({ address, lng, lat });\n }\n return suggestions;\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"address-autocomplete\": AddressAutocomplete;\n }\n}\n"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
/**
|
|
3
|
+
* Fullscreen confetti animation overlay, rendered on a fixed-position canvas.
|
|
4
|
+
* Starts automatically on first render and stops after `duration` ms.
|
|
5
|
+
*
|
|
6
|
+
* @element animate-confetti
|
|
7
|
+
*/
|
|
8
|
+
export declare class AnimateConfetti extends LitElement {
|
|
9
|
+
/** How long the confetti streams before stopping, in ms. */
|
|
10
|
+
duration: number;
|
|
11
|
+
private maxParticleCount;
|
|
12
|
+
private particleSpeed;
|
|
13
|
+
private particles;
|
|
14
|
+
private streamingConfetti;
|
|
15
|
+
private colors;
|
|
16
|
+
private waveAngle;
|
|
17
|
+
private animationTimer?;
|
|
18
|
+
connectedCallback(): void;
|
|
19
|
+
disconnectedCallback(): void;
|
|
20
|
+
static styles: import("lit").CSSResult;
|
|
21
|
+
/**
|
|
22
|
+
* @returns The canvas element
|
|
23
|
+
*/
|
|
24
|
+
private get _canvas();
|
|
25
|
+
/**
|
|
26
|
+
* Resize canvas to browser size.
|
|
27
|
+
*/
|
|
28
|
+
private _handleResize;
|
|
29
|
+
/**
|
|
30
|
+
* Start confetti once shadow dom has been rendered for the first time.
|
|
31
|
+
*/
|
|
32
|
+
protected firstUpdated(): void;
|
|
33
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
34
|
+
private startConfetti;
|
|
35
|
+
private runAnimation;
|
|
36
|
+
/**
|
|
37
|
+
* Stop confetti and clean up after reasonable time.
|
|
38
|
+
*/
|
|
39
|
+
private stopConfetti;
|
|
40
|
+
/**
|
|
41
|
+
* Removes any associated particle data from internal storage to regain memory.
|
|
42
|
+
*/
|
|
43
|
+
private _cleanup;
|
|
44
|
+
private updateParticles;
|
|
45
|
+
private drawParticles;
|
|
46
|
+
private resetParticle;
|
|
47
|
+
}
|
|
48
|
+
declare global {
|
|
49
|
+
interface HTMLElementTagNameMap {
|
|
50
|
+
"animate-confetti": AnimateConfetti;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=animate-confetti.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animate-confetti.d.ts","sourceRoot":"","sources":["../src/animate-confetti.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAa5C;;;;;GAKG;AACH,qBACa,eAAgB,SAAQ,UAAU;IAC7C,4DAA4D;IAE5D,QAAQ,EAAE,MAAM,CAAQ;IAExB,OAAO,CAAC,gBAAgB,CAAO;IAC/B,OAAO,CAAC,aAAa,CAAK;IAE1B,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,iBAAiB,CAAkB;IAC3C,OAAO,CAAC,MAAM,CAaZ;IACF,OAAO,CAAC,SAAS,CAAe;IAChC,OAAO,CAAC,cAAc,CAAC,CAAS;IAEvB,iBAAiB,SAGzB;IAEQ,oBAAoB,SAI5B;IAED,OAAgB,MAAM,0BASpB;IAEF;;OAEG;IACH,OAAO,KAAK,OAAO,GAElB;IAED;;OAEG;IACH,OAAO,CAAC,aAAa,CAInB;IAEF;;OAEG;IACH,UAAmB,YAAY,IAAI,IAAI,CAKtC;IAEQ,MAAM,yCAEd;IAED,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,YAAY,CAUlB;IAEF;;OAEG;IACH,OAAO,CAAC,YAAY;IAKpB;;OAEG;IACH,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,eAAe;IAyBvB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,aAAa;CAUtB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,kBAAkB,EAAE,eAAe,CAAC;KACrC;CACF"}
|