@countriesdb/widget 0.1.1

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 ADDED
@@ -0,0 +1,26 @@
1
+ PROPRIETARY LICENSE
2
+
3
+ Copyright (c) NAYEE LLC
4
+
5
+ All rights reserved.
6
+
7
+ This software and associated documentation files (the "Software") are the
8
+ proprietary property of NAYEE LLC.
9
+
10
+ PERMITTED USES:
11
+ - You may use this Software in your applications and projects
12
+ - You may distribute this Software as part of your applications
13
+
14
+ PROHIBITED USES:
15
+ - You may NOT modify, adapt, or create derivative works of this Software
16
+ - You may NOT reverse engineer, decompile, or disassemble this Software
17
+ - You may NOT redistribute modified versions of this Software
18
+ - You may NOT remove or alter any copyright notices or proprietary markings
19
+
20
+ The Software is provided "AS IS", without warranty of any kind, express or
21
+ implied, including but not limited to the warranties of merchantability,
22
+ fitness for a particular purpose and noninfringement.
23
+
24
+ For licensing inquiries, please contact NAYEE LLC at https://nayee.net
25
+
26
+
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # @countriesdb/widget
2
+
3
+ **Country and state/province select widget with ISO 3166-1 and ISO 3166-2 codes.** Auto-populates dropdown menus with up-to-date country and subdivision data in multiple languages. Perfect for forms, location selection, and address validation.
4
+
5
+ 📖 **[Full Documentation](https://countriesdb.com/docs)** | 🌐 **[Website](https://countriesdb.com)**
6
+
7
+ ## Getting Started
8
+
9
+ **⚠️ API Key Required:** This widget requires a CountriesDB API key to function. You must create an account at [countriesdb.com](https://countriesdb.com) to obtain your public API key. Test accounts are available with limited functionality. Once you have your key, you can use it to initialize the widget.
10
+
11
+ - 🔑 [Get your API key](https://countriesdb.com) - Create an account
12
+ - 📚 [View documentation](https://countriesdb.com/docs) - Complete API reference and examples
13
+ - 💬 [Support](https://countriesdb.com) - Get help and support
14
+
15
+ ## Features
16
+
17
+ - ✅ **ISO 3166 compliant** - Uses official ISO 3166-1 country codes and ISO 3166-2 subdivision codes
18
+ - ✅ **Multilingual support** - Country and subdivision names in multiple languages
19
+ - ✅ **Always up-to-date** - Regularly updated database with latest country and subdivision data
20
+ - ✅ **Easy integration** - Works with plain HTML, no framework required
21
+ - ✅ **Auto-initialization** - Automatically populates select elements on page load
22
+ - ✅ **TypeScript support** - Full type definitions included
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install @countriesdb/widget
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ 1. **Get your API key** - Sign up at [countriesdb.com](https://countriesdb.com) to get your public API key (test accounts available with limited functionality)
33
+ 2. **Install the package** - `npm install @countriesdb/widget`
34
+ 3. **Initialize the widget** - See usage examples below
35
+
36
+ For detailed documentation, visit [countriesdb.com/docs](https://countriesdb.com/docs).
37
+
38
+ ## Usage
39
+
40
+ ### Script Tag (UMD)
41
+
42
+ ```html
43
+ <script src="https://unpkg.com/@countriesdb/widget@latest/dist/index.js"></script>
44
+ <script>
45
+ CountriesWidget.CountriesWidgetLoad({
46
+ publicKey: 'YOUR_PUBLIC_KEY',
47
+ defaultLanguage: 'en'
48
+ });
49
+ </script>
50
+
51
+ <!-- Add CSS classes to your select elements -->
52
+ <select class="country-selection" name="country">
53
+ <option value="">Select Country</option>
54
+ </select>
55
+
56
+ <select class="subdivision-selection" name="state" data-country="country">
57
+ <option value="">Select State/Province</option>
58
+ </select>
59
+ ```
60
+
61
+ ### ES Module
62
+
63
+ ```html
64
+ <script type="module">
65
+ import { CountriesWidgetLoad } from '@countriesdb/widget';
66
+
67
+ CountriesWidgetLoad({
68
+ publicKey: 'YOUR_PUBLIC_KEY',
69
+ defaultLanguage: 'en'
70
+ });
71
+ </script>
72
+ ```
73
+
74
+ ### Node.js / Bundler
75
+
76
+ ```javascript
77
+ import { CountriesWidgetLoad } from '@countriesdb/widget';
78
+
79
+ CountriesWidgetLoad({
80
+ publicKey: 'YOUR_PUBLIC_KEY',
81
+ backendUrl: 'https://api.countriesdb.com',
82
+ defaultLanguage: 'en',
83
+ enableGeolocation: true
84
+ });
85
+ ```
86
+
87
+ ## Configuration
88
+
89
+ ### Options
90
+
91
+ - `publicKey` (required): Your CountriesDB public API key. [Get your API key](https://countriesdb.com) by creating an account.
92
+ - `backendUrl` (optional): Backend API URL (defaults to script origin or `https://api.countriesdb.com`)
93
+ - `defaultLanguage` (optional): Default language for country/subdivision names
94
+ - `forcedLanguage` (optional): Force a specific language
95
+ - `showSubdivisionType` (optional): Show subdivision type names (default: `true`)
96
+ - `followRelated` (optional): Enable forward navigation between related countries/subdivisions
97
+ - `followUpward` (optional): Enable reverse navigation to parent subdivisions
98
+ - `allowParentSelection` (optional): Allow selecting parent subdivisions (default: `false`)
99
+ - `isoCountryNames` (optional): Use ISO country names (default: `false`)
100
+ - `subdivisionRomanizationPreference` (optional): Preferred romanization system
101
+ - `preferLocalVariant` (optional): Prefer local name variants (default: `false`)
102
+ - `countryNameFilter` (optional): Custom function to filter/transform country names
103
+ - `subdivisionNameFilter` (optional): Custom function to filter/transform subdivision names
104
+ - `autoInit` (optional): Auto-initialize on load (default: `true`)
105
+
106
+ ### Data Attributes
107
+
108
+ #### Country Select
109
+
110
+ - `class="country-selection"` (required): Identifies the country select element
111
+ - `data-name`: Unique name for linking subdivisions
112
+ - `data-preselected`: Preselected country code
113
+ - `data-default-value`: Default option value
114
+ - `data-label`: Default option label
115
+
116
+ #### Subdivision Select
117
+
118
+ - `class="subdivision-selection"` (required): Identifies the subdivision select element
119
+ - `data-country`: Link to country select by `data-name`
120
+ - `data-country-code`: Direct country code (if no linked country select)
121
+ - `data-preselected`: Preselected subdivision code
122
+ - `data-default-value`: Default option value
123
+ - `data-label`: Default option label
124
+ - `data-prefer-official`: Prefer official subdivisions
125
+ - `data-nested{level}-prefix`: Prefix for nested subdivision levels
126
+
127
+ ## Events
128
+
129
+ The widget dispatches custom `countriesWidget:update` events on select elements:
130
+
131
+ ```javascript
132
+ document.addEventListener('countriesWidget:update', (event) => {
133
+ console.log('Widget updated:', event.detail);
134
+ // {
135
+ // value: 'US',
136
+ // selectedValues: ['US'],
137
+ // name: 'country1',
138
+ // country: null,
139
+ // isSubdivision: false,
140
+ // type: 'country',
141
+ // reason: 'regular' | 'geoip' | 'preselected' | 'reload' | 'follow'
142
+ // }
143
+ });
144
+ ```
145
+
146
+ ## Examples
147
+
148
+ ### Basic Usage
149
+
150
+ ```html
151
+ <select class="country-selection" data-name="country1"></select>
152
+ <select class="subdivision-selection" data-country="country1"></select>
153
+ ```
154
+
155
+ ### With Preselected Values
156
+
157
+ ```html
158
+ <select class="country-selection" data-name="country1" data-preselected="US"></select>
159
+ <select class="subdivision-selection" data-country="country1" data-preselected="US-CA"></select>
160
+ ```
161
+
162
+ ### Multi-Select Countries
163
+
164
+ ```html
165
+ <select class="country-selection" data-name="countries" multiple></select>
166
+ ```
167
+
168
+ ### Custom Name Filter
169
+
170
+ ```javascript
171
+ CountriesWidgetLoad({
172
+ publicKey: 'YOUR_PUBLIC_KEY',
173
+ countryNameFilter: (code, name, language, item) => {
174
+ if (code === 'US') {
175
+ return 'United States of America';
176
+ }
177
+ return name;
178
+ }
179
+ });
180
+ ```
181
+
182
+ ## API Reference
183
+
184
+ ### `CountriesWidgetLoad(options)`
185
+
186
+ Main initialization function. Returns a Promise that resolves when initialization is complete.
187
+
188
+ For complete API documentation, visit [countriesdb.com/docs](https://countriesdb.com/docs).
189
+
190
+ ## Resources
191
+
192
+ - 🌐 [Website](https://countriesdb.com) - Main CountriesDB website
193
+ - 📚 [Documentation](https://countriesdb.com/docs) - Complete API reference and guides
194
+ - 🔑 [Get API Key](https://countriesdb.com) - Create account and get your public key
195
+ - 💬 [Support](https://countriesdb.com) - Get help and contact support
196
+
197
+ ## License
198
+
199
+ PROPRIETARY - Copyright (c) NAYEE LLC. See [LICENSE](LICENSE) for details.
200
+
201
+ **Developed by [NAYEE LLC](https://nayee.net)**
202
+
@@ -0,0 +1,29 @@
1
+ /**
2
+ * DOM manipulation utilities
3
+ */
4
+ import type { SelectElement } from './types';
5
+ import type { Country, Subdivision } from '@countriesdb/widget-core';
6
+ /**
7
+ * Initialize a select element with default option
8
+ */
9
+ export declare function initializeSelect(select: SelectElement, fallbackLabel?: string, isSubdivision?: boolean): void;
10
+ /**
11
+ * Populate a select element with countries
12
+ */
13
+ export declare function populateCountrySelect(select: SelectElement, countries: Country[], language: string, countryNameFilter?: (code: string, name: string, lang: string, item: Country) => string | false | null | undefined): void;
14
+ /**
15
+ * Populate a select element with subdivisions
16
+ */
17
+ export declare function buildSubdivisionOptionsHTML(subdivisions: Subdivision[], select: SelectElement, language: string, showSubdivisionType: boolean, allowParentSelection: boolean, subdivisionNameFilter?: (code: string, originalName: string, language: string, countryCode: string | null, item: Subdivision) => string | false | null | undefined): string;
18
+ /**
19
+ * Apply preselected value to a select element
20
+ */
21
+ export declare function applyPreselectedValue(select: SelectElement, value: string): void;
22
+ /**
23
+ * Handle API error by showing error message in select
24
+ */
25
+ export declare function handleApiError(select: SelectElement, errorMessage: string | Error): void;
26
+ /**
27
+ * Parse boolean from string value
28
+ */
29
+ export declare function parseBoolean(value: string | null | undefined): boolean;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * DOM manipulation utilities
3
+ */
4
+ import { applyCountryNameFilter, sortCountries, buildSubdivisionTree, flattenSubdivisionOptions, parseNestingPrefixes, } from '@countriesdb/widget-core';
5
+ /**
6
+ * Initialize a select element with default option
7
+ */
8
+ export function initializeSelect(select, fallbackLabel = 'Not Applicable', isSubdivision = false) {
9
+ const isMultiple = select.hasAttribute('multiple');
10
+ // For multi-select, don't add default option or use data-label/data-default-value
11
+ if (isMultiple) {
12
+ select.innerHTML = '';
13
+ // Remove data-label and data-default-value for multi-select (they're ignored)
14
+ if (select.dataset.label !== undefined) {
15
+ delete select.dataset.label;
16
+ }
17
+ if (select.dataset.defaultValue !== undefined) {
18
+ delete select.dataset.defaultValue;
19
+ }
20
+ }
21
+ else {
22
+ const dataLabel = select.dataset.label;
23
+ const dataDefaultValue = select.dataset.defaultValue;
24
+ const label = dataLabel || fallbackLabel;
25
+ const defaultValue = dataDefaultValue ?? '';
26
+ select.innerHTML = `<option value="${defaultValue}">${label}</option>`;
27
+ if (dataLabel !== undefined) {
28
+ select.dataset.label = dataLabel;
29
+ }
30
+ if (dataDefaultValue !== undefined) {
31
+ select.dataset.defaultValue = dataDefaultValue;
32
+ }
33
+ }
34
+ if (isSubdivision) {
35
+ select.disabled = true;
36
+ }
37
+ }
38
+ /**
39
+ * Populate a select element with countries
40
+ */
41
+ export function populateCountrySelect(select, countries, language, countryNameFilter) {
42
+ // Apply filter and get display names
43
+ let filteredCountries = applyCountryNameFilter(countries, countryNameFilter, language);
44
+ // Sort if filter was applied
45
+ if (countryNameFilter) {
46
+ filteredCountries = sortCountries(filteredCountries, language);
47
+ }
48
+ // Clear existing options (except default)
49
+ const defaultOption = select.querySelector('option[value=""]') ||
50
+ (select.dataset.defaultValue ? select.querySelector(`option[value="${select.dataset.defaultValue}"]`) : null);
51
+ const defaultValue = select.dataset.defaultValue ?? '';
52
+ // Keep default option if it exists
53
+ select.innerHTML = defaultOption ? defaultOption.outerHTML : `<option value="${defaultValue}">${select.dataset.label || '&mdash;'}</option>`;
54
+ // Add country options
55
+ filteredCountries.forEach((country) => {
56
+ const option = document.createElement('option');
57
+ option.value = country.iso_alpha_2;
58
+ option.textContent = country._displayName;
59
+ select.appendChild(option);
60
+ });
61
+ }
62
+ /**
63
+ * Populate a select element with subdivisions
64
+ */
65
+ export function buildSubdivisionOptionsHTML(subdivisions, select, language, showSubdivisionType, allowParentSelection, subdivisionNameFilter) {
66
+ const tree = buildSubdivisionTree(subdivisions);
67
+ // Parse nesting prefixes from data attributes
68
+ const dataAttributes = {};
69
+ for (let lvl = 1; lvl <= 10; lvl++) {
70
+ const key = `nested${lvl}Prefix`;
71
+ const value = select.dataset[key];
72
+ if (value !== undefined) {
73
+ dataAttributes[`nested${lvl}Prefix`] = value;
74
+ }
75
+ else {
76
+ break;
77
+ }
78
+ }
79
+ const prefixes = parseNestingPrefixes(dataAttributes);
80
+ const isMultiple = select.hasAttribute('multiple');
81
+ const labelKey = showSubdivisionType ? 'full_name' : 'name';
82
+ let html = '';
83
+ // For multi-select, don't add default option
84
+ if (!isMultiple) {
85
+ const defaultLabel = select.dataset.label || '&mdash;';
86
+ const defaultValue = select.dataset.defaultValue ?? '';
87
+ html = `<option value="${defaultValue}">${defaultLabel}</option>`;
88
+ }
89
+ html += flattenSubdivisionOptions(tree, 0, prefixes, labelKey, language, allowParentSelection, subdivisionNameFilter);
90
+ return html;
91
+ }
92
+ /**
93
+ * Apply preselected value to a select element
94
+ */
95
+ export function applyPreselectedValue(select, value) {
96
+ if (!value || value.trim() === '') {
97
+ return;
98
+ }
99
+ const isMultiple = select.hasAttribute('multiple');
100
+ if (isMultiple) {
101
+ // For multi-select, parse comma-separated values
102
+ const values = value
103
+ .split(',')
104
+ .map((v) => v.trim())
105
+ .filter((v) => v !== '');
106
+ // Select all matching options
107
+ Array.from(select.options).forEach((option) => {
108
+ option.selected = values.includes(option.value);
109
+ });
110
+ }
111
+ else {
112
+ // Single select: set single value
113
+ select.value = value;
114
+ }
115
+ }
116
+ /**
117
+ * Handle API error by showing error message in select
118
+ */
119
+ export function handleApiError(select, errorMessage) {
120
+ const message = errorMessage instanceof Error ? errorMessage.message : errorMessage;
121
+ const defaultValue = select.dataset.defaultValue ?? '';
122
+ select.innerHTML += `<option value="${defaultValue}" disabled>${message}</option>`;
123
+ }
124
+ /**
125
+ * Parse boolean from string value
126
+ */
127
+ export function parseBoolean(value) {
128
+ if (value === null || value === undefined) {
129
+ return false;
130
+ }
131
+ const lowered = String(value).trim().toLowerCase();
132
+ return !(lowered === '0' || lowered === 'false');
133
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Event system for widget updates
3
+ */
4
+ import type { SelectElement, UpdateEventDetail } from './types';
5
+ /**
6
+ * Dispatch a custom update event for widget changes
7
+ */
8
+ export declare function dispatchUpdateEvent(select: SelectElement, detail?: Partial<UpdateEventDetail>): void;
9
+ /**
10
+ * Check if an event was initiated by the widget (not user)
11
+ */
12
+ export declare function isWidgetInitiatedEvent(event: Event): boolean;
13
+ /**
14
+ * Mark an event as widget-initiated
15
+ */
16
+ export declare function markEventAsWidgetInitiated(event: Event): void;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Event system for widget updates
3
+ */
4
+ /**
5
+ * Dispatch a custom update event for widget changes
6
+ */
7
+ export function dispatchUpdateEvent(select, detail = {}) {
8
+ let selectedValues = [];
9
+ if (select.multiple) {
10
+ // For multi-select, selectedOptions is a HTMLCollection, convert to array
11
+ selectedValues = Array.from(select.selectedOptions || [])
12
+ .map((opt) => opt.value)
13
+ .filter((v) => v !== '');
14
+ }
15
+ else {
16
+ // For single-select, use value
17
+ selectedValues = select.value ? [select.value] : [];
18
+ }
19
+ const evt = new CustomEvent('countriesWidget:update', {
20
+ bubbles: true,
21
+ detail: {
22
+ value: select.value || '',
23
+ selectedValues,
24
+ name: select.dataset.name || null,
25
+ country: select.dataset.country || null,
26
+ isSubdivision: select.classList.contains('subdivision-selection'),
27
+ ...detail,
28
+ },
29
+ });
30
+ select.dispatchEvent(evt);
31
+ }
32
+ /**
33
+ * Check if an event was initiated by the widget (not user)
34
+ */
35
+ export function isWidgetInitiatedEvent(event) {
36
+ return event.isWidgetInitiated === true;
37
+ }
38
+ /**
39
+ * Mark an event as widget-initiated
40
+ */
41
+ export function markEventAsWidgetInitiated(event) {
42
+ event.isWidgetInitiated = true;
43
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Follow logic for related and upward navigation
3
+ */
4
+ import type { SelectElement, WidgetState } from './types';
5
+ /**
6
+ * Trigger follow logic when a subdivision is selected
7
+ */
8
+ export declare function triggerFollowLogic(select: SelectElement, apiKey: string, backendUrl: string, state: WidgetState, followRelated: boolean, followUpward: boolean, updateSubdivisionSelectFn: (select: SelectElement, apiKey: string, countryCode: string) => Promise<void>): Promise<void>;
9
+ /**
10
+ * Handle follow_related from subdivision change event
11
+ */
12
+ export declare function handleFollowRelatedFromSubdivision(select: SelectElement, apiKey: string, backendUrl: string, state: WidgetState, followRelated: boolean, updateSubdivisionSelectFn: (select: SelectElement, apiKey: string, countryCode: string) => Promise<void>): Promise<void>;
13
+ /**
14
+ * Handle follow_upward from subdivision change event
15
+ */
16
+ export declare function handleFollowUpwardFromSubdivision(select: SelectElement, apiKey: string, backendUrl: string, state: WidgetState, followUpward: boolean, updateSubdivisionSelectFn: (select: SelectElement, apiKey: string, countryCode: string) => Promise<void>): Promise<void>;
17
+ /**
18
+ * Handle follow_upward from country change event
19
+ */
20
+ export declare function handleFollowUpwardFromCountry(select: SelectElement, apiKey: string, backendUrl: string, state: WidgetState, followUpward: boolean, updateSubdivisionSelectFn: (select: SelectElement, apiKey: string, countryCode: string) => Promise<void>): Promise<void>;
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Follow logic for related and upward navigation
3
+ */
4
+ import { dispatchUpdateEvent } from './event-system';
5
+ /**
6
+ * Trigger follow logic when a subdivision is selected
7
+ */
8
+ export async function triggerFollowLogic(select, apiKey, backendUrl, state, followRelated, followUpward, updateSubdivisionSelectFn) {
9
+ if (!followRelated && !followUpward) {
10
+ return;
11
+ }
12
+ const linkedCountrySelect = document.querySelector(`.country-selection[data-name="${select.dataset.country}"]`);
13
+ // Only work if country is single-select (never when country is multi)
14
+ if (!linkedCountrySelect || linkedCountrySelect.multiple) {
15
+ return;
16
+ }
17
+ const allSubs = state.subdivisionsMap.get(select);
18
+ if (!allSubs) {
19
+ return;
20
+ }
21
+ // For follow_upward, only work if subdivision is single-select
22
+ if (followUpward && select.multiple) {
23
+ return;
24
+ }
25
+ const selectedCode = select.value;
26
+ if (!selectedCode) {
27
+ return;
28
+ }
29
+ const picked = allSubs.find((s) => s.code === selectedCode);
30
+ if (!picked) {
31
+ return;
32
+ }
33
+ // follow_related
34
+ if (followRelated && picked.related_country_code) {
35
+ const targetCountry = picked.related_country_code;
36
+ const targetSubdivision = picked.related_subdivision_code;
37
+ const relatedSubsSelect = document.querySelector(`.subdivision-selection[data-country="${linkedCountrySelect.dataset.name}"]`);
38
+ if (relatedSubsSelect && targetSubdivision) {
39
+ relatedSubsSelect.dataset._widgetTempPreselect = targetSubdivision; // one-time preselect
40
+ }
41
+ if (linkedCountrySelect.value !== targetCountry) {
42
+ linkedCountrySelect.value = targetCountry;
43
+ dispatchUpdateEvent(linkedCountrySelect, { type: 'country', reason: 'regular' });
44
+ await updateSubdivisionSelectFn(linkedCountrySelect, apiKey, targetCountry);
45
+ }
46
+ else {
47
+ if (relatedSubsSelect && targetSubdivision) {
48
+ await updateSubdivisionSelectFn(relatedSubsSelect, apiKey, targetCountry);
49
+ }
50
+ }
51
+ }
52
+ // follow_upward
53
+ if (followUpward && picked.is_subdivision_of) {
54
+ const parentCode = picked.is_subdivision_of.parent_country_code;
55
+ const parentSub = picked.is_subdivision_of.subdivision_code;
56
+ if (parentSub) {
57
+ const relatedSubsSelect = document.querySelector(`.subdivision-selection[data-country="${linkedCountrySelect.dataset.name}"]`);
58
+ // Only preselect if subdivision is single-select (follow_upward doesn't work with multi)
59
+ if (relatedSubsSelect && !relatedSubsSelect.multiple) {
60
+ relatedSubsSelect.dataset._widgetTempPreselect = parentSub; // one-time preselect
61
+ }
62
+ }
63
+ if (linkedCountrySelect.value !== parentCode) {
64
+ linkedCountrySelect.value = parentCode;
65
+ dispatchUpdateEvent(linkedCountrySelect, { type: 'country', reason: 'regular' });
66
+ await updateSubdivisionSelectFn(linkedCountrySelect, apiKey, parentCode);
67
+ }
68
+ else if (parentSub) {
69
+ const relatedSubsSelect = document.querySelector(`.subdivision-selection[data-country="${linkedCountrySelect.dataset.name}"]`);
70
+ if (relatedSubsSelect && !relatedSubsSelect.multiple) {
71
+ await updateSubdivisionSelectFn(relatedSubsSelect, apiKey, parentCode);
72
+ }
73
+ }
74
+ }
75
+ }
76
+ /**
77
+ * Handle follow_related from subdivision change event
78
+ */
79
+ export async function handleFollowRelatedFromSubdivision(select, apiKey, backendUrl, state, followRelated, updateSubdivisionSelectFn) {
80
+ if (!followRelated) {
81
+ return;
82
+ }
83
+ const linkedCountrySelect = document.querySelector(`.country-selection[data-name="${select.dataset.country}"]`);
84
+ // Only work if country is single-select (never when country is multi)
85
+ if (!linkedCountrySelect || linkedCountrySelect.multiple) {
86
+ return;
87
+ }
88
+ const allSubs = state.subdivisionsMap.get(select);
89
+ if (!allSubs) {
90
+ return;
91
+ }
92
+ // Get selected code (for multi-select, use first selected value)
93
+ const selectedCode = select.multiple
94
+ ? (select.selectedOptions[0]?.value || null)
95
+ : select.value;
96
+ if (!selectedCode) {
97
+ return;
98
+ }
99
+ const picked = allSubs.find((s) => s.code === selectedCode);
100
+ if (!picked || !picked.related_country_code) {
101
+ return;
102
+ }
103
+ const targetCountry = picked.related_country_code;
104
+ const targetSubdivision = picked.related_subdivision_code;
105
+ const relatedSubsSelect = document.querySelector(`.subdivision-selection[data-country="${linkedCountrySelect.dataset.name}"]`);
106
+ if (relatedSubsSelect && targetSubdivision) {
107
+ relatedSubsSelect.dataset.preselected = targetSubdivision;
108
+ }
109
+ if (linkedCountrySelect.value !== targetCountry) {
110
+ linkedCountrySelect.value = targetCountry;
111
+ dispatchUpdateEvent(linkedCountrySelect, { type: 'country', reason: 'regular' });
112
+ await updateSubdivisionSelectFn(linkedCountrySelect, apiKey, targetCountry);
113
+ }
114
+ else {
115
+ if (relatedSubsSelect && targetSubdivision) {
116
+ await updateSubdivisionSelectFn(relatedSubsSelect, apiKey, targetCountry);
117
+ }
118
+ }
119
+ }
120
+ /**
121
+ * Handle follow_upward from subdivision change event
122
+ */
123
+ export async function handleFollowUpwardFromSubdivision(select, apiKey, backendUrl, state, followUpward, updateSubdivisionSelectFn) {
124
+ if (!followUpward) {
125
+ return;
126
+ }
127
+ // Disable for multi-select subdivisions
128
+ if (select.multiple) {
129
+ return;
130
+ }
131
+ const linkedCountrySelect = document.querySelector(`.country-selection[data-name="${select.dataset.country}"]`);
132
+ // Only work if country is single-select (never when country is multi)
133
+ if (!linkedCountrySelect || linkedCountrySelect.multiple) {
134
+ return;
135
+ }
136
+ const allSubs = state.subdivisionsMap.get(select);
137
+ if (!allSubs) {
138
+ return;
139
+ }
140
+ const selectedCode = select.value;
141
+ if (!selectedCode) {
142
+ return;
143
+ }
144
+ const picked = allSubs.find((s) => s.code === selectedCode);
145
+ if (!picked || !picked.is_subdivision_of) {
146
+ return;
147
+ }
148
+ const parentCode = picked.is_subdivision_of.parent_country_code;
149
+ const parentSub = picked.is_subdivision_of.subdivision_code;
150
+ if (parentSub) {
151
+ const relatedSubsSelect = document.querySelector(`.subdivision-selection[data-country="${linkedCountrySelect.dataset.name}"]`);
152
+ if (relatedSubsSelect) {
153
+ relatedSubsSelect.dataset.preselected = parentSub;
154
+ }
155
+ }
156
+ if (linkedCountrySelect.value !== parentCode) {
157
+ linkedCountrySelect.value = parentCode;
158
+ dispatchUpdateEvent(linkedCountrySelect, { type: 'country', reason: 'follow' });
159
+ await updateSubdivisionSelectFn(linkedCountrySelect, apiKey, parentCode);
160
+ }
161
+ else if (parentSub) {
162
+ const relatedSubsSelect = document.querySelector(`.subdivision-selection[data-country="${linkedCountrySelect.dataset.name}"]`);
163
+ if (relatedSubsSelect) {
164
+ await updateSubdivisionSelectFn(relatedSubsSelect, apiKey, parentCode);
165
+ }
166
+ }
167
+ }
168
+ /**
169
+ * Handle follow_upward from country change event
170
+ */
171
+ export async function handleFollowUpwardFromCountry(select, apiKey, backendUrl, state, followUpward, updateSubdivisionSelectFn) {
172
+ // Only works when country is single-select (never for multi-select)
173
+ if (!followUpward || select.multiple) {
174
+ return;
175
+ }
176
+ const countries = state.countriesMap.get(select);
177
+ if (!countries) {
178
+ return;
179
+ }
180
+ const chosen = select.value;
181
+ if (!chosen) {
182
+ return;
183
+ }
184
+ const picked = countries.find((c) => c.iso_alpha_2 === chosen);
185
+ if (!picked || !picked.is_subdivision_of) {
186
+ return;
187
+ }
188
+ const parentCode = picked.is_subdivision_of.parent_country_code;
189
+ const parentSub = picked.is_subdivision_of.subdivision_code;
190
+ if (select.value !== parentCode) {
191
+ if (parentSub) {
192
+ const linkedSubdivisionSelects = Array.from(document.querySelectorAll(`.subdivision-selection[data-country="${select.dataset.name}"]`));
193
+ for (const s of linkedSubdivisionSelects) {
194
+ // Only preselect if subdivision is single-select (follow_upward doesn't work with multi)
195
+ if (!s.multiple) {
196
+ s.dataset.preselected = parentSub;
197
+ }
198
+ }
199
+ }
200
+ select.value = parentCode;
201
+ dispatchUpdateEvent(select, { type: 'country', reason: 'regular' });
202
+ await updateSubdivisionSelectFn(select, apiKey, parentCode);
203
+ }
204
+ else if (parentSub) {
205
+ const linkedSubdivisionSelects = Array.from(document.querySelectorAll(`.subdivision-selection[data-country="${select.dataset.name}"]`));
206
+ for (const s of linkedSubdivisionSelects) {
207
+ // Only update if subdivision is single-select
208
+ if (!s.multiple) {
209
+ await updateSubdivisionSelectFn(s, apiKey, parentCode);
210
+ }
211
+ }
212
+ }
213
+ }