@communitiesuk/svelte-component-library 0.1.17 → 0.2.0-alpha.2
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 +116 -98
- package/dist/assets/css/base.css +9 -0
- package/dist/assets/css/code-block.css +116 -0
- package/dist/assets/css/components.css +47 -0
- package/dist/assets/css/fonts.css +19 -0
- package/dist/assets/css/govuk-additional.css +142 -0
- package/dist/assets/css/govuk-frontend.min.css +2 -2
- package/dist/assets/css/moduk.css +1 -0
- package/dist/assets/css/moj-frontend.min copy.css +4108 -0
- package/dist/assets/css/moj-frontend.min.css +2 -0
- package/dist/assets/css/moj-frontend.min.css.map +1 -0
- package/dist/assets/css/utilities.css +0 -0
- package/dist/assets/images/govuk-crest.svg +1 -1
- package/dist/assets/js/govuk-frontend.min.js +1 -0
- package/dist/assets/js/moj-frontend.min.js +1 -0
- package/dist/assets/rebrand/images/favicon.ico +0 -0
- package/dist/assets/rebrand/images/favicon.svg +1 -0
- package/dist/assets/rebrand/images/govuk-crest.svg +1 -0
- package/dist/assets/rebrand/images/govuk-icon-180.png +0 -0
- package/dist/assets/rebrand/images/govuk-icon-192.png +0 -0
- package/dist/assets/rebrand/images/govuk-icon-512.png +0 -0
- package/dist/assets/rebrand/images/govuk-icon-mask.svg +1 -0
- package/dist/assets/rebrand/images/govuk-opengraph-image.png +0 -0
- package/dist/assets/rebrand/manifest.json +39 -0
- package/dist/components/data-vis/line-chart/Line.svelte +48 -41
- package/dist/components/data-vis/line-chart/Line.svelte.d.ts +6 -4
- package/dist/components/data-vis/line-chart/LineChart.svelte +145 -36
- package/dist/components/data-vis/line-chart/LineChart.svelte.d.ts +25 -9
- package/dist/components/data-vis/line-chart/Lines.svelte +10 -23
- package/dist/components/data-vis/line-chart/Lines.svelte.d.ts +8 -4
- package/dist/components/data-vis/line-chart/Marker.svelte +31 -5
- package/dist/components/data-vis/line-chart/Marker.svelte.d.ts +6 -2
- package/dist/components/data-vis/line-chart/SeriesLabel.svelte +7 -8
- package/dist/components/data-vis/line-chart/SeriesLabel.svelte.d.ts +2 -2
- package/dist/components/data-vis/line-chart/ValueLabel.svelte +26 -34
- package/dist/components/data-vis/line-chart/ValueLabel.svelte.d.ts +8 -4
- package/dist/components/data-vis/map/Map.svelte +299 -71
- package/dist/components/data-vis/map/Map.svelte.d.ts +39 -12
- package/dist/components/data-vis/map/NonStandardControls.svelte +10 -1
- package/dist/components/data-vis/map/NonStandardControls.svelte.d.ts +12 -11
- package/dist/components/data-vis/map/Tooltip.svelte +3 -4
- package/dist/components/data-vis/map/Tooltip.svelte.d.ts +0 -2
- package/dist/components/data-vis/map/mapUtils.d.ts +2 -0
- package/dist/components/data-vis/map/mapUtils.js +50 -0
- package/dist/components/data-vis/table/Table.svelte +28 -40
- package/dist/components/data-vis/table/Table.svelte.d.ts +0 -2
- package/dist/components/layout/Breadcrumbs.svelte +10 -12
- package/dist/components/layout/Breadcrumbs.svelte.d.ts +1 -0
- package/dist/components/layout/Footer.svelte +69 -4
- package/dist/components/layout/Footer.svelte.d.ts +3 -0
- package/dist/components/layout/Header.svelte +56 -16
- package/dist/components/layout/Header.svelte.d.ts +1 -0
- package/dist/components/layout/InternalHeader.svelte +155 -150
- package/dist/components/layout/InternalHeader.svelte.d.ts +1 -0
- package/dist/components/ui/Button.svelte +78 -4
- package/dist/components/ui/Button.svelte.d.ts +2 -0
- package/dist/components/ui/CookieBanner.svelte +356 -0
- package/dist/components/ui/CookieBanner.svelte.d.ts +18 -0
- package/dist/components/ui/FilterPanel.svelte +167 -158
- package/dist/components/ui/FilterPanel.svelte.d.ts +2 -0
- package/dist/components/ui/Masthead.svelte +35 -23
- package/dist/components/ui/Masthead.svelte.d.ts +2 -0
- package/dist/components/ui/PostcodeOrAreaSearch.svelte +200 -0
- package/dist/components/ui/PostcodeOrAreaSearch.svelte.d.ts +37 -0
- package/dist/components/ui/Search.svelte +2 -2
- package/dist/components/ui/SearchAutocomplete.svelte +104 -14
- package/dist/components/ui/SearchAutocomplete.svelte.d.ts +4 -0
- package/dist/data/IMD2019.json +32846 -0
- package/dist/data/places.csv +20039 -0
- package/dist/data/places.json +100192 -0
- package/dist/data/svgFontDimensions.json +90 -0
- package/dist/data/testData.json +52632 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/package-wrapping/BaseInformation.svelte +0 -33
- package/dist/package-wrapping/SidebarContainer.svelte +0 -7
- package/dist/utils/area-search/geoConfig.d.ts +435 -0
- package/dist/utils/area-search/geoConfig.js +291 -0
- package/dist/utils/cookiesNavigation.d.ts +44 -0
- package/dist/utils/cookiesNavigation.js +63 -0
- package/dist/utils/data-transformations/convert-csv-to-json-proper.cjs +88 -0
- package/dist/utils/data-transformations/convert-csv-to-json-proper.d.cts +1 -0
- package/dist/utils/data-transformations/convertCSV.d.ts +6 -0
- package/dist/utils/data-transformations/convertCSV.js +40 -21
- package/dist/utils/text-string-conversion/textStringConversion.d.ts +6 -0
- package/dist/utils/text-string-conversion/textStringConversion.js +10 -0
- package/package.json +18 -7
- package/dist/components/ui/Breadcrumbs.svelte +0 -198
- package/dist/components/ui/Breadcrumbs.svelte.d.ts +0 -24
- package/dist/components/ui/Footer.svelte +0 -171
- package/dist/components/ui/Footer.svelte.d.ts +0 -30
- package/dist/components/ui/Header.svelte +0 -43
- package/dist/components/ui/Header.svelte.d.ts +0 -7
- package/dist/components/ui/ServiceNavigation.svelte +0 -143
- package/dist/components/ui/ServiceNavigation.svelte.d.ts +0 -13
- package/dist/components/ui/SideNavigation.svelte +0 -346
- package/dist/components/ui/SideNavigation.svelte.d.ts +0 -25
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import SearchAutocomplete from "./SearchAutocomplete.svelte";
|
|
3
|
+
import { capitalise } from "./../../utils/text-string-conversion/textStringConversion";
|
|
4
|
+
import {
|
|
5
|
+
geoNames,
|
|
6
|
+
geoCodesLookup,
|
|
7
|
+
essGeocodes,
|
|
8
|
+
} from "./../../utils/area-search/geoConfig";
|
|
9
|
+
import defaultPlacesData from "./../../data/places.json";
|
|
10
|
+
|
|
11
|
+
// --- Define Types ---
|
|
12
|
+
type SuggestionObject = { label: string; value: any; [key: string]: any };
|
|
13
|
+
type Suggestion = string | SuggestionObject;
|
|
14
|
+
|
|
15
|
+
type Place = SuggestionObject & {
|
|
16
|
+
areacd: string;
|
|
17
|
+
areanm: string;
|
|
18
|
+
parentcd?: string;
|
|
19
|
+
group?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type Props = {
|
|
23
|
+
// Data source configuration
|
|
24
|
+
customPlacesData?: any[]; // Custom places data (JSON format: [{areacd, areanm, parentcd}])
|
|
25
|
+
essOnly?: boolean; // Whether to filter to essential geocodes only
|
|
26
|
+
|
|
27
|
+
// Custom lookup functions/data (for flexibility)
|
|
28
|
+
customTypeLookup?: Record<
|
|
29
|
+
string,
|
|
30
|
+
{ label: string; plural: string; childGroup?: boolean }
|
|
31
|
+
>;
|
|
32
|
+
customEssGeocodes?: string[];
|
|
33
|
+
customGetTypeLabel?: (type: string) => string;
|
|
34
|
+
|
|
35
|
+
// Postcode API configuration
|
|
36
|
+
postcodeApiUrl?: string;
|
|
37
|
+
postcodeApiKey?: string; // Key in response containing suggestions
|
|
38
|
+
postcodeApiProperty?: string; // Property to extract from API objects
|
|
39
|
+
postcodeApiPathBased?: boolean; // Whether the API uses path-based URLs
|
|
40
|
+
|
|
41
|
+
// Source selection logic
|
|
42
|
+
customSourceSelector?: (
|
|
43
|
+
query: string,
|
|
44
|
+
options: Suggestion[],
|
|
45
|
+
) => "api" | "options";
|
|
46
|
+
|
|
47
|
+
// Pass-through props to SearchAutocomplete
|
|
48
|
+
selectedValue?: any;
|
|
49
|
+
label_text?: string;
|
|
50
|
+
button_text?: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
placeholder?: string;
|
|
53
|
+
hint?: string;
|
|
54
|
+
size?: "large" | "";
|
|
55
|
+
on_govuk_blue?: boolean;
|
|
56
|
+
homepage?: boolean;
|
|
57
|
+
minLength?: number;
|
|
58
|
+
required?: boolean;
|
|
59
|
+
[key: string]: any; // Allow other props
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
let {
|
|
63
|
+
customPlacesData = undefined,
|
|
64
|
+
essOnly = false,
|
|
65
|
+
customTypeLookup = undefined,
|
|
66
|
+
customEssGeocodes = undefined,
|
|
67
|
+
customGetTypeLabel = undefined,
|
|
68
|
+
postcodeApiUrl = "https://api.postcodes.io/postcodes",
|
|
69
|
+
postcodeApiKey = "result",
|
|
70
|
+
postcodeApiProperty = "postcode",
|
|
71
|
+
postcodeApiPathBased = false,
|
|
72
|
+
customSourceSelector = undefined,
|
|
73
|
+
selectedValue = $bindable(),
|
|
74
|
+
label_text = "Search for a postcode or area",
|
|
75
|
+
button_text = "Search",
|
|
76
|
+
name = "location",
|
|
77
|
+
placeholder = "e.g. SW1A 1AA or Westminster",
|
|
78
|
+
hint = "Enter a UK postcode or area name",
|
|
79
|
+
size = "",
|
|
80
|
+
on_govuk_blue = false,
|
|
81
|
+
homepage = false,
|
|
82
|
+
minLength = 2,
|
|
83
|
+
required = false,
|
|
84
|
+
...restProps
|
|
85
|
+
}: Props = $props();
|
|
86
|
+
|
|
87
|
+
// --- Use custom lookups or defaults ---
|
|
88
|
+
const typeLookup =
|
|
89
|
+
customTypeLookup && Object.keys(customTypeLookup).length > 0
|
|
90
|
+
? customTypeLookup
|
|
91
|
+
: geoNames;
|
|
92
|
+
const essGeocodesArray =
|
|
93
|
+
customEssGeocodes && customEssGeocodes.length > 0
|
|
94
|
+
? customEssGeocodes
|
|
95
|
+
: essGeocodes;
|
|
96
|
+
|
|
97
|
+
// --- Helper Functions ---
|
|
98
|
+
const getTypeLabel = (type: string) => {
|
|
99
|
+
// If custom function is provided and returns a non-null value, use it
|
|
100
|
+
if (customGetTypeLabel) {
|
|
101
|
+
const customLabel = customGetTypeLabel(type);
|
|
102
|
+
if (customLabel !== null && customLabel !== undefined) {
|
|
103
|
+
return customLabel;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Otherwise use lookup table (custom or default)
|
|
107
|
+
return typeLookup[type]
|
|
108
|
+
? typeLookup[type].label
|
|
109
|
+
: geoCodesLookup[type]?.label || type;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Default source selector: use API for postcode-like inputs, options for area names
|
|
113
|
+
const defaultSourceSelector = (query: string, options: Suggestion[]) => {
|
|
114
|
+
// If input has 3+ chars and contains a digit, likely a postcode
|
|
115
|
+
return query.length >= 3 && /\d/.test(query) ? "api" : "options";
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const sourceSelector = (query: string, options: Suggestion[]) => {
|
|
119
|
+
// If custom function is provided and returns a non-null value, use it
|
|
120
|
+
if (customSourceSelector) {
|
|
121
|
+
const customResult = customSourceSelector(query, options);
|
|
122
|
+
if (customResult !== null && customResult !== undefined) {
|
|
123
|
+
return customResult;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Otherwise use default selector
|
|
127
|
+
return defaultSourceSelector(query, options);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// --- Process Places Data ---
|
|
131
|
+
const places = $derived.by(() => {
|
|
132
|
+
// Use custom data if provided and not empty, otherwise use default
|
|
133
|
+
let placesData =
|
|
134
|
+
customPlacesData && customPlacesData.length > 0
|
|
135
|
+
? customPlacesData
|
|
136
|
+
: defaultPlacesData;
|
|
137
|
+
|
|
138
|
+
// Filter to essential geocodes if requested
|
|
139
|
+
if (essOnly) {
|
|
140
|
+
placesData = placesData.filter((p: any) =>
|
|
141
|
+
essGeocodesArray.includes(p.areacd.slice(0, 3)),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Sort by area name
|
|
146
|
+
const sortedData = [...placesData].sort((a: any, b: any) =>
|
|
147
|
+
a.areanm.localeCompare(b.areanm),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Create lookup for parent relationships
|
|
151
|
+
const lookup: Record<string, any> = {};
|
|
152
|
+
for (const p of sortedData) lookup[p.areacd] = p;
|
|
153
|
+
|
|
154
|
+
// Add group information and format for autocomplete
|
|
155
|
+
const processedPlaces: Place[] = sortedData.map((p: any) => {
|
|
156
|
+
const type = p.areacd.slice(0, 3);
|
|
157
|
+
const group =
|
|
158
|
+
type === "K02"
|
|
159
|
+
? ""
|
|
160
|
+
: p.parentcd && lookup[p.parentcd]
|
|
161
|
+
? `${capitalise(getTypeLabel(type))} in ${lookup[p.parentcd].areanm}`
|
|
162
|
+
: capitalise(getTypeLabel(type));
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
areacd: p.areacd,
|
|
166
|
+
areanm: p.areanm,
|
|
167
|
+
parentcd: p.parentcd,
|
|
168
|
+
group,
|
|
169
|
+
label: p.areanm,
|
|
170
|
+
value: p.areacd,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return processedPlaces;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// --- Autocomplete Configuration ---
|
|
178
|
+
const autocompleteProps = $derived({
|
|
179
|
+
options: places,
|
|
180
|
+
source_url: postcodeApiUrl,
|
|
181
|
+
source_key: postcodeApiKey,
|
|
182
|
+
source_property: postcodeApiProperty,
|
|
183
|
+
pathBasedApi: postcodeApiPathBased,
|
|
184
|
+
groupKey: "group",
|
|
185
|
+
sourceSelector,
|
|
186
|
+
label_text,
|
|
187
|
+
button_text,
|
|
188
|
+
name,
|
|
189
|
+
placeholder,
|
|
190
|
+
hint,
|
|
191
|
+
size,
|
|
192
|
+
on_govuk_blue,
|
|
193
|
+
homepage,
|
|
194
|
+
minLength,
|
|
195
|
+
required,
|
|
196
|
+
...restProps,
|
|
197
|
+
});
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<SearchAutocomplete {...autocompleteProps} bind:selectedValue />
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
type SuggestionObject = {
|
|
2
|
+
label: string;
|
|
3
|
+
value: any;
|
|
4
|
+
[key: string]: any;
|
|
5
|
+
};
|
|
6
|
+
type Suggestion = string | SuggestionObject;
|
|
7
|
+
type Props = {
|
|
8
|
+
customPlacesData?: any[];
|
|
9
|
+
essOnly?: boolean;
|
|
10
|
+
customTypeLookup?: Record<string, {
|
|
11
|
+
label: string;
|
|
12
|
+
plural: string;
|
|
13
|
+
childGroup?: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
customEssGeocodes?: string[];
|
|
16
|
+
customGetTypeLabel?: (type: string) => string;
|
|
17
|
+
postcodeApiUrl?: string;
|
|
18
|
+
postcodeApiKey?: string;
|
|
19
|
+
postcodeApiProperty?: string;
|
|
20
|
+
postcodeApiPathBased?: boolean;
|
|
21
|
+
customSourceSelector?: (query: string, options: Suggestion[]) => "api" | "options";
|
|
22
|
+
selectedValue?: any;
|
|
23
|
+
label_text?: string;
|
|
24
|
+
button_text?: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
hint?: string;
|
|
28
|
+
size?: "large" | "";
|
|
29
|
+
on_govuk_blue?: boolean;
|
|
30
|
+
homepage?: boolean;
|
|
31
|
+
minLength?: number;
|
|
32
|
+
required?: boolean;
|
|
33
|
+
[key: string]: any;
|
|
34
|
+
};
|
|
35
|
+
declare const PostcodeOrAreaSearch: import("svelte").Component<Props, {}, "selectedValue">;
|
|
36
|
+
type PostcodeOrAreaSearch = ReturnType<typeof PostcodeOrAreaSearch>;
|
|
37
|
+
export default PostcodeOrAreaSearch;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { clsx } from "clsx";
|
|
3
3
|
import { browser } from "$app/environment";
|
|
4
4
|
import { onMount } from "svelte";
|
|
5
|
-
import IconSearch from "
|
|
6
|
-
import closeIconUrl from "
|
|
5
|
+
import IconSearch from "./../../icons/IconSearch.svelte";
|
|
6
|
+
import closeIconUrl from "./../../assets/govuk_publishing_components/images/icon-close.svg?url";
|
|
7
7
|
|
|
8
8
|
// SSR-safe HTML sanitizer: no-op on server
|
|
9
9
|
let sanitize = $state<(html: string) => string>((html) => html);
|
|
@@ -4,15 +4,14 @@
|
|
|
4
4
|
import Search from "./Search.svelte"; // Base component
|
|
5
5
|
import "accessible-autocomplete/dist/accessible-autocomplete.min.css";
|
|
6
6
|
import { browser } from "$app/environment";
|
|
7
|
-
import suggestionIconUrl from "
|
|
8
|
-
import closeIconUrl from "
|
|
9
|
-
|
|
7
|
+
import suggestionIconUrl from "./../../assets/govuk_publishing_components/images/icon-autocomplete-search-suggestion.svg?url";
|
|
8
|
+
import closeIconUrl from "./../../assets/govuk_publishing_components/images/icon-close.svg?url"; // Import for the cancel button
|
|
10
9
|
|
|
11
10
|
// SSR-safe HTML sanitizer: no-op on server
|
|
12
11
|
let sanitize: (html: string) => string = (html) => html;
|
|
13
12
|
|
|
14
13
|
// --- Define Props ---
|
|
15
|
-
type SuggestionObject = { label: string; value: any };
|
|
14
|
+
type SuggestionObject = { label: string; value: any; [key: string]: any }; // Allow additional properties for grouping
|
|
16
15
|
type Suggestion = string | SuggestionObject;
|
|
17
16
|
type Props = {
|
|
18
17
|
// User can supply either an options array or an API source
|
|
@@ -20,6 +19,12 @@
|
|
|
20
19
|
source_url?: string; // Optional: URL for autocomplete suggestions
|
|
21
20
|
source_key?: string; // Optional: Key in the JSON response containing suggestions array
|
|
22
21
|
source_property?: string; // Property to extract from API objects
|
|
22
|
+
pathBasedApi?: boolean; // Whether to use path-based URLs (RESTful) instead of query parameters
|
|
23
|
+
groupKey?: string; // Optional: Key to group suggestions by (e.g., "region", "category")
|
|
24
|
+
sourceSelector?: (
|
|
25
|
+
query: string,
|
|
26
|
+
options: Suggestion[],
|
|
27
|
+
) => "api" | "options"; // Function to determine which source to use
|
|
23
28
|
outerClasses?: string; // Optional classes for the outer wrapper
|
|
24
29
|
outerDataAttributes?: Record<string, string>; // Optional data attributes for the outer wrapper
|
|
25
30
|
// Add other expected props passed down (e.g., size, on_govuk_blue, id, name etc.)
|
|
@@ -52,9 +57,12 @@
|
|
|
52
57
|
source_url = undefined,
|
|
53
58
|
source_key = undefined,
|
|
54
59
|
source_property = undefined,
|
|
55
|
-
|
|
60
|
+
pathBasedApi = false,
|
|
61
|
+
groupKey = undefined,
|
|
62
|
+
sourceSelector = undefined,
|
|
63
|
+
size = "",
|
|
56
64
|
on_govuk_blue = false,
|
|
57
|
-
homepage = false,
|
|
65
|
+
homepage = false,
|
|
58
66
|
outerClasses = "",
|
|
59
67
|
outerDataAttributes = {},
|
|
60
68
|
id, // Pass down id
|
|
@@ -81,6 +89,12 @@
|
|
|
81
89
|
let containerElement: HTMLDivElement; // bind:this target for the outer div
|
|
82
90
|
let autocompleteInstance: { inputElement?: HTMLInputElement } | null = null; // To store instance if needed
|
|
83
91
|
|
|
92
|
+
// Track which source is currently being used for dynamic data attributes
|
|
93
|
+
let currentSource = $state<"api" | "options" | "auto">("auto");
|
|
94
|
+
let currentSourceUrl = $state<string | undefined>(undefined);
|
|
95
|
+
let currentSourceKey = $state<string | undefined>(undefined);
|
|
96
|
+
let currentSourceProperty = $state<string | undefined>(undefined);
|
|
97
|
+
|
|
84
98
|
// --- Derived Values ---
|
|
85
99
|
const wrapperClasses = $derived(
|
|
86
100
|
clsx(
|
|
@@ -148,6 +162,12 @@
|
|
|
148
162
|
query: string,
|
|
149
163
|
populateResults: (results: string[]) => void,
|
|
150
164
|
) => {
|
|
165
|
+
// Update current source tracking
|
|
166
|
+
currentSource = "api";
|
|
167
|
+
currentSourceUrl = source_url;
|
|
168
|
+
currentSourceKey = source_key;
|
|
169
|
+
currentSourceProperty = source_property;
|
|
170
|
+
|
|
151
171
|
if (!source_url || !source_key) {
|
|
152
172
|
console.error(
|
|
153
173
|
"SearchAutocomplete: source_url and source_key are required for API mode.",
|
|
@@ -155,8 +175,21 @@
|
|
|
155
175
|
populateResults([]);
|
|
156
176
|
return;
|
|
157
177
|
}
|
|
158
|
-
|
|
159
|
-
|
|
178
|
+
|
|
179
|
+
// Construct URL based on pathBasedApi setting
|
|
180
|
+
let url: URL;
|
|
181
|
+
if (pathBasedApi) {
|
|
182
|
+
// For RESTful APIs like Zippopotam.us: append query to path
|
|
183
|
+
const baseUrl = source_url.endsWith("/")
|
|
184
|
+
? source_url.slice(0, -1)
|
|
185
|
+
: source_url;
|
|
186
|
+
url = new URL(`${baseUrl}/${encodeURIComponent(query)}`);
|
|
187
|
+
} else {
|
|
188
|
+
// For query parameter APIs: add ?q=query
|
|
189
|
+
url = new URL(source_url);
|
|
190
|
+
url.searchParams.set("q", query);
|
|
191
|
+
}
|
|
192
|
+
|
|
160
193
|
fetch(url, { headers: { Accept: "application/json" } })
|
|
161
194
|
.then((response) => {
|
|
162
195
|
if (!response.ok)
|
|
@@ -209,6 +242,12 @@
|
|
|
209
242
|
query: string,
|
|
210
243
|
populateResults: (results: Suggestion[]) => void,
|
|
211
244
|
) => {
|
|
245
|
+
// Update current source tracking
|
|
246
|
+
currentSource = "options";
|
|
247
|
+
currentSourceUrl = undefined;
|
|
248
|
+
currentSourceKey = undefined;
|
|
249
|
+
currentSourceProperty = undefined;
|
|
250
|
+
|
|
212
251
|
if (!options) {
|
|
213
252
|
populateResults([]);
|
|
214
253
|
return;
|
|
@@ -227,6 +266,43 @@
|
|
|
227
266
|
? getResultsFromOptions
|
|
228
267
|
: getResultsFromApi;
|
|
229
268
|
|
|
269
|
+
// Initialise current source tracking based on initial configuration
|
|
270
|
+
if (sourceSelector) {
|
|
271
|
+
currentSource = "auto"; // Will be determined dynamically
|
|
272
|
+
currentSourceUrl = source_url;
|
|
273
|
+
currentSourceKey = source_key;
|
|
274
|
+
currentSourceProperty = source_property;
|
|
275
|
+
} else if (useOptions) {
|
|
276
|
+
currentSource = "options";
|
|
277
|
+
currentSourceUrl = undefined;
|
|
278
|
+
currentSourceKey = undefined;
|
|
279
|
+
currentSourceProperty = undefined;
|
|
280
|
+
} else {
|
|
281
|
+
currentSource = "api";
|
|
282
|
+
currentSourceUrl = source_url;
|
|
283
|
+
currentSourceKey = source_key;
|
|
284
|
+
currentSourceProperty = source_property;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// If sourceSelector is provided, create a dynamic source function
|
|
288
|
+
const dynamicSourceFunction = sourceSelector
|
|
289
|
+
? (query: string, populateResults: (results: Suggestion[]) => void) => {
|
|
290
|
+
const selectedSource = sourceSelector(query, options || []);
|
|
291
|
+
// Handle invalid returns by falling back to default logic
|
|
292
|
+
if (selectedSource === "api") {
|
|
293
|
+
getResultsFromApi(query, populateResults);
|
|
294
|
+
} else if (selectedSource === "options") {
|
|
295
|
+
getResultsFromOptions(query, populateResults);
|
|
296
|
+
} else {
|
|
297
|
+
// Fall back to default logic if invalid return value
|
|
298
|
+
sourceFunction(query, populateResults);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
: null;
|
|
302
|
+
|
|
303
|
+
// Use dynamic source if available, otherwise fall back to original logic
|
|
304
|
+
const finalSourceFunction = dynamicSourceFunction || sourceFunction;
|
|
305
|
+
|
|
230
306
|
// Define suggestion template function (sanitize and highlight)
|
|
231
307
|
const suggestionTemplate = (result: Suggestion): string => {
|
|
232
308
|
const displayLabel = typeof result === "string" ? result : result.label;
|
|
@@ -256,11 +332,17 @@
|
|
|
256
332
|
html = `${before}<mark class="gem-c-search-with-autocomplete__suggestion-highlight">${match}</mark>${after}`;
|
|
257
333
|
}
|
|
258
334
|
|
|
259
|
-
//
|
|
335
|
+
// Get group text if groupKey is provided and result is an object
|
|
336
|
+
const groupText =
|
|
337
|
+
groupKey && typeof result === "object" && result[groupKey]
|
|
338
|
+
? ` <span class="gem-c-search-with-autocomplete__suggestion-group">${sanitize(String(result[groupKey]))}</span>`
|
|
339
|
+
: "";
|
|
340
|
+
|
|
341
|
+
// Match the GOV.UK structure with integrated group text
|
|
260
342
|
return `
|
|
261
343
|
<div class="gem-c-search-with-autocomplete__option-wrapper">
|
|
262
344
|
<span class="gem-c-search-with-autocomplete__suggestion-icon"></span>
|
|
263
|
-
<span class="gem-c-search-with-autocomplete__suggestion-text">${html}</span>
|
|
345
|
+
<span class="gem-c-search-with-autocomplete__suggestion-text">${html}${groupText}</span>
|
|
264
346
|
</div>
|
|
265
347
|
`;
|
|
266
348
|
};
|
|
@@ -313,7 +395,7 @@
|
|
|
313
395
|
id: searchInput.id, // Use the ID from the *rendered* Search input
|
|
314
396
|
name: searchInput.name, // Use the name from the *rendered* Search input
|
|
315
397
|
inputClasses: searchInput.classList, // Pass original classes directly
|
|
316
|
-
source:
|
|
398
|
+
source: finalSourceFunction,
|
|
317
399
|
minLength: minLength,
|
|
318
400
|
confirmOnBlur: confirmOnBlur,
|
|
319
401
|
showNoOptionsFound: showNoOptionsFound,
|
|
@@ -428,9 +510,11 @@
|
|
|
428
510
|
bind:this={containerElement}
|
|
429
511
|
class={wrapperClasses}
|
|
430
512
|
data-module="gem-search-with-autocomplete"
|
|
431
|
-
data-source-url={
|
|
432
|
-
data-source-key={
|
|
433
|
-
data-source-property={
|
|
513
|
+
data-source-url={currentSourceUrl}
|
|
514
|
+
data-source-key={currentSourceKey}
|
|
515
|
+
data-source-property={currentSourceProperty}
|
|
516
|
+
data-group-key={groupKey}
|
|
517
|
+
data-current-source={currentSource}
|
|
434
518
|
{...outerDataAttributes}
|
|
435
519
|
style={`--suggestion-icon: url("${suggestionIconUrl}"); --cancel-icon: url("${closeIconUrl}")`}
|
|
436
520
|
>
|
|
@@ -567,6 +651,12 @@
|
|
|
567
651
|
background: none;
|
|
568
652
|
}
|
|
569
653
|
|
|
654
|
+
.gem-c-search-with-autocomplete__suggestion-group {
|
|
655
|
+
opacity: 0.8;
|
|
656
|
+
font-size: smaller;
|
|
657
|
+
font-weight: normal;
|
|
658
|
+
}
|
|
659
|
+
|
|
570
660
|
.gem-c-search-with-autocomplete.gem-c-search-with-autocomplete--large
|
|
571
661
|
.gem-c-search-with-autocomplete__menu {
|
|
572
662
|
margin-right: -50px;
|
|
@@ -2,6 +2,7 @@ import "accessible-autocomplete/dist/accessible-autocomplete.min.css";
|
|
|
2
2
|
type SuggestionObject = {
|
|
3
3
|
label: string;
|
|
4
4
|
value: any;
|
|
5
|
+
[key: string]: any;
|
|
5
6
|
};
|
|
6
7
|
type Suggestion = string | SuggestionObject;
|
|
7
8
|
type Props = {
|
|
@@ -9,6 +10,9 @@ type Props = {
|
|
|
9
10
|
source_url?: string;
|
|
10
11
|
source_key?: string;
|
|
11
12
|
source_property?: string;
|
|
13
|
+
pathBasedApi?: boolean;
|
|
14
|
+
groupKey?: string;
|
|
15
|
+
sourceSelector?: (query: string, options: Suggestion[]) => "api" | "options";
|
|
12
16
|
outerClasses?: string;
|
|
13
17
|
outerDataAttributes?: Record<string, string>;
|
|
14
18
|
size?: "large" | "";
|