@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 +26 -0
- package/README.md +202 -0
- package/dist/dom-manipulation.d.ts +29 -0
- package/dist/dom-manipulation.js +133 -0
- package/dist/event-system.d.ts +16 -0
- package/dist/event-system.js +43 -0
- package/dist/follow-logic.d.ts +20 -0
- package/dist/follow-logic.js +213 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.esm.js +863 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +1111 -0
- package/dist/index.js.map +1 -0
- package/dist/initialization.d.ts +54 -0
- package/dist/initialization.js +321 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.js +4 -0
- package/package.json +62 -0
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 || '—'}</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 || '—';
|
|
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
|
+
}
|