@countriesdb/widget 0.1.23 → 0.1.25
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 +21 -3
- package/dist/dom-manipulation.js +0 -2
- package/dist/event-system.d.ts +5 -1
- package/dist/event-system.js +28 -0
- package/dist/index.esm.js +59 -31
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +59 -31
- package/dist/index.js.map +1 -1
- package/dist/initialization.js +21 -7
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,7 +78,6 @@ import { CountriesWidgetLoad } from '@countriesdb/widget';
|
|
|
78
78
|
|
|
79
79
|
CountriesWidgetLoad({
|
|
80
80
|
publicKey: 'YOUR_PUBLIC_KEY',
|
|
81
|
-
backendUrl: 'https://api.countriesdb.com',
|
|
82
81
|
defaultLanguage: 'en',
|
|
83
82
|
enableGeolocation: true
|
|
84
83
|
});
|
|
@@ -89,7 +88,6 @@ CountriesWidgetLoad({
|
|
|
89
88
|
### Options
|
|
90
89
|
|
|
91
90
|
- `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
91
|
- `defaultLanguage` (optional): Default language for country/subdivision names
|
|
94
92
|
- `forcedLanguage` (optional): Force a specific language
|
|
95
93
|
- `showSubdivisionType` (optional): Show subdivision type names (default: `true`)
|
|
@@ -99,6 +97,7 @@ CountriesWidgetLoad({
|
|
|
99
97
|
- `isoCountryNames` (optional): Use ISO country names (default: `false`)
|
|
100
98
|
- `subdivisionRomanizationPreference` (optional): Preferred romanization system
|
|
101
99
|
- `preferLocalVariant` (optional): Prefer local name variants (default: `false`)
|
|
100
|
+
- `preferOfficialSubdivisions` (optional): Prefer official ISO subdivisions (default: `false`)
|
|
102
101
|
- `countryNameFilter` (optional): Custom function to filter/transform country names
|
|
103
102
|
- `subdivisionNameFilter` (optional): Custom function to filter/transform subdivision names
|
|
104
103
|
- `autoInit` (optional): Auto-initialize on load (default: `true`)
|
|
@@ -143,6 +142,25 @@ document.addEventListener('countriesWidget:update', (event) => {
|
|
|
143
142
|
});
|
|
144
143
|
```
|
|
145
144
|
|
|
145
|
+
It also emits `countriesWidget:ready` whenever a country or subdivision select
|
|
146
|
+
finishes loading options (initial load and subsequent reloads, like when a country
|
|
147
|
+
changes). Listen once on `document` to react as soon as the widget is fully wired:
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
document.addEventListener('countriesWidget:ready', (event) => {
|
|
151
|
+
console.log('Select ready:', event.detail);
|
|
152
|
+
// {
|
|
153
|
+
// value: 'US',
|
|
154
|
+
// selectedValues: ['US'],
|
|
155
|
+
// name: 'country1',
|
|
156
|
+
// country: null,
|
|
157
|
+
// isSubdivision: false,
|
|
158
|
+
// type: 'country' | 'subdivision',
|
|
159
|
+
// phase: 'initial' | 'reload'
|
|
160
|
+
// }
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
146
164
|
## Examples
|
|
147
165
|
|
|
148
166
|
### Basic Usage
|
|
@@ -196,7 +214,7 @@ For complete API documentation, visit [countriesdb.com/docs](https://countriesdb
|
|
|
196
214
|
|
|
197
215
|
## License
|
|
198
216
|
|
|
199
|
-
PROPRIETARY - Copyright (c) NAYEE LLC. See [LICENSE](LICENSE) for details.
|
|
217
|
+
PROPRIETARY - Copyright (c) NAYEE LLC. See [LICENSE](https://github.com/countriesdb/countriesdb/blob/main/packages/npm/widget/LICENSE) for details.
|
|
200
218
|
|
|
201
219
|
**Developed by [NAYEE LLC](https://nayee.net)**
|
|
202
220
|
|
package/dist/dom-manipulation.js
CHANGED
|
@@ -153,8 +153,6 @@ export function handleApiError(select, errorMessage, replace = false) {
|
|
|
153
153
|
else {
|
|
154
154
|
select.innerHTML += `<option value="${defaultValue}" disabled>${formattedMessage}</option>`;
|
|
155
155
|
}
|
|
156
|
-
// Ensure select is enabled so users can see the error
|
|
157
|
-
select.disabled = false;
|
|
158
156
|
}
|
|
159
157
|
/**
|
|
160
158
|
* Parse boolean from string value
|
package/dist/event-system.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Event system for widget updates
|
|
3
3
|
*/
|
|
4
|
-
import type { SelectElement, UpdateEventDetail } from './types';
|
|
4
|
+
import type { ReadyEventDetail, SelectElement, UpdateEventDetail } from './types';
|
|
5
5
|
/**
|
|
6
6
|
* Dispatch a custom update event for widget changes
|
|
7
7
|
*/
|
|
8
8
|
export declare function dispatchUpdateEvent(select: SelectElement, detail?: Partial<UpdateEventDetail>): void;
|
|
9
|
+
/**
|
|
10
|
+
* Dispatch a custom ready event once a select has been populated.
|
|
11
|
+
*/
|
|
12
|
+
export declare function dispatchReadyEvent(select: SelectElement, detail?: Partial<ReadyEventDetail>): void;
|
|
9
13
|
/**
|
|
10
14
|
* Check if an event was initiated by the widget (not user)
|
|
11
15
|
*/
|
package/dist/event-system.js
CHANGED
|
@@ -29,6 +29,34 @@ export function dispatchUpdateEvent(select, detail = {}) {
|
|
|
29
29
|
});
|
|
30
30
|
select.dispatchEvent(evt);
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Dispatch a custom ready event once a select has been populated.
|
|
34
|
+
*/
|
|
35
|
+
export function dispatchReadyEvent(select, detail = {}) {
|
|
36
|
+
const selectedValues = select.multiple
|
|
37
|
+
? Array.from(select.selectedOptions || [])
|
|
38
|
+
.map((opt) => opt.value)
|
|
39
|
+
.filter((v) => v !== '')
|
|
40
|
+
: select.value
|
|
41
|
+
? [select.value]
|
|
42
|
+
: [];
|
|
43
|
+
const evt = new CustomEvent('countriesWidget:ready', {
|
|
44
|
+
bubbles: true,
|
|
45
|
+
detail: {
|
|
46
|
+
value: select.value || '',
|
|
47
|
+
selectedValues,
|
|
48
|
+
name: select.dataset.name || null,
|
|
49
|
+
country: select.dataset.country || null,
|
|
50
|
+
isSubdivision: select.classList.contains('subdivision-selection'),
|
|
51
|
+
type: select.classList.contains('subdivision-selection')
|
|
52
|
+
? 'subdivision'
|
|
53
|
+
: 'country',
|
|
54
|
+
phase: 'initial',
|
|
55
|
+
...detail,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
select.dispatchEvent(evt);
|
|
59
|
+
}
|
|
32
60
|
/**
|
|
33
61
|
* Check if an event was initiated by the widget (not user)
|
|
34
62
|
*/
|
package/dist/index.esm.js
CHANGED
|
@@ -154,8 +154,6 @@ function handleApiError(select, errorMessage, replace = false) {
|
|
|
154
154
|
else {
|
|
155
155
|
select.innerHTML += `<option value="${defaultValue}" disabled>${formattedMessage}</option>`;
|
|
156
156
|
}
|
|
157
|
-
// Ensure select is enabled so users can see the error
|
|
158
|
-
select.disabled = false;
|
|
159
157
|
}
|
|
160
158
|
|
|
161
159
|
/**
|
|
@@ -189,6 +187,34 @@ function dispatchUpdateEvent(select, detail = {}) {
|
|
|
189
187
|
});
|
|
190
188
|
select.dispatchEvent(evt);
|
|
191
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Dispatch a custom ready event once a select has been populated.
|
|
192
|
+
*/
|
|
193
|
+
function dispatchReadyEvent(select, detail = {}) {
|
|
194
|
+
const selectedValues = select.multiple
|
|
195
|
+
? Array.from(select.selectedOptions || [])
|
|
196
|
+
.map((opt) => opt.value)
|
|
197
|
+
.filter((v) => v !== '')
|
|
198
|
+
: select.value
|
|
199
|
+
? [select.value]
|
|
200
|
+
: [];
|
|
201
|
+
const evt = new CustomEvent('countriesWidget:ready', {
|
|
202
|
+
bubbles: true,
|
|
203
|
+
detail: {
|
|
204
|
+
value: select.value || '',
|
|
205
|
+
selectedValues,
|
|
206
|
+
name: select.dataset.name || null,
|
|
207
|
+
country: select.dataset.country || null,
|
|
208
|
+
isSubdivision: select.classList.contains('subdivision-selection'),
|
|
209
|
+
type: select.classList.contains('subdivision-selection')
|
|
210
|
+
? 'subdivision'
|
|
211
|
+
: 'country',
|
|
212
|
+
phase: 'initial',
|
|
213
|
+
...detail,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
select.dispatchEvent(evt);
|
|
217
|
+
}
|
|
192
218
|
/**
|
|
193
219
|
* Check if an event was initiated by the widget (not user)
|
|
194
220
|
*/
|
|
@@ -465,7 +491,8 @@ async function setupSubdivisionSelection(apiKey, backendUrl, state, config) {
|
|
|
465
491
|
: null;
|
|
466
492
|
// Check if linked country select is multi-select (not allowed)
|
|
467
493
|
if (linkedCountrySelect && linkedCountrySelect.hasAttribute('multiple')) {
|
|
468
|
-
|
|
494
|
+
const defaultValue = select.dataset.defaultValue ?? '';
|
|
495
|
+
select.innerHTML = `<option value="${defaultValue}" disabled>Error: Cannot link to multi-select country. Use data-country-code instead.</option>`;
|
|
469
496
|
continue;
|
|
470
497
|
}
|
|
471
498
|
// No direct link → maybe data-country-code
|
|
@@ -475,7 +502,8 @@ async function setupSubdivisionSelection(apiKey, backendUrl, state, config) {
|
|
|
475
502
|
await updateSubdivisionSelect(select, apiKey, backendUrl, state, config, select.dataset.countryCode);
|
|
476
503
|
}
|
|
477
504
|
else {
|
|
478
|
-
|
|
505
|
+
const defaultValue = select.dataset.defaultValue ?? '';
|
|
506
|
+
select.innerHTML += `<option value="${defaultValue}" disabled>Error: No country select present</option>`;
|
|
479
507
|
}
|
|
480
508
|
}
|
|
481
509
|
// Always dispatch an update event for user-initiated subdivision changes
|
|
@@ -521,8 +549,10 @@ async function updateSubdivisionSelect(select, apiKey, backendUrl, state, config
|
|
|
521
549
|
// Use GeoIP only if data-preselected attribute is not set at all
|
|
522
550
|
const shouldUseGeoIP = preselectedValue === undefined || preselectedValue === null;
|
|
523
551
|
// Check if this subdivision select prefers official subdivisions
|
|
524
|
-
|
|
525
|
-
|
|
552
|
+
// Use data attribute if present, otherwise use config
|
|
553
|
+
const preferOfficial = select.hasAttribute('data-prefer-official')
|
|
554
|
+
? true
|
|
555
|
+
: config.preferOfficialSubdivisions;
|
|
526
556
|
const languageHeaders = CountriesDBClient.getLanguageHeaders(config.forcedLanguage, config.defaultLanguage);
|
|
527
557
|
const subdivisionsResult = await CountriesDBClient.fetchSubdivisions({
|
|
528
558
|
apiKey,
|
|
@@ -608,6 +638,10 @@ async function updateSubdivisionSelect(select, apiKey, backendUrl, state, config
|
|
|
608
638
|
finally {
|
|
609
639
|
// Mark initialization as complete
|
|
610
640
|
state.isInitializing.delete(select);
|
|
641
|
+
dispatchReadyEvent(select, {
|
|
642
|
+
type: 'subdivision',
|
|
643
|
+
phase: isReload ? 'reload' : 'initial',
|
|
644
|
+
});
|
|
611
645
|
// Only fire 'reload' if this is a reload, not initial load
|
|
612
646
|
if (isReload && !valueSetByWidget) {
|
|
613
647
|
dispatchUpdateEvent(select, {
|
|
@@ -619,7 +653,8 @@ async function updateSubdivisionSelect(select, apiKey, backendUrl, state, config
|
|
|
619
653
|
}
|
|
620
654
|
else if (!select.dataset.country ||
|
|
621
655
|
!document.querySelector(`.country-selection[data-name="${select.dataset.country}"]`)) {
|
|
622
|
-
|
|
656
|
+
const defaultValue = select.dataset.defaultValue ?? '';
|
|
657
|
+
select.innerHTML += `<option value="${defaultValue}" disabled>Error: No country select present</option>`;
|
|
623
658
|
}
|
|
624
659
|
}
|
|
625
660
|
/**
|
|
@@ -648,7 +683,8 @@ async function setupCountrySelection(apiKey, backendUrl, state, config, subdivis
|
|
|
648
683
|
if (name && seenNames[name]) {
|
|
649
684
|
select.removeAttribute('data-name');
|
|
650
685
|
initializeSelect(select, '—');
|
|
651
|
-
|
|
686
|
+
const defaultValue = select.dataset.defaultValue ?? '';
|
|
687
|
+
select.innerHTML += `<option value="${defaultValue}" disabled>Error: Duplicate field</option>`;
|
|
652
688
|
continue;
|
|
653
689
|
}
|
|
654
690
|
if (name) {
|
|
@@ -749,6 +785,10 @@ async function setupCountrySelection(apiKey, backendUrl, state, config, subdivis
|
|
|
749
785
|
finally {
|
|
750
786
|
// Mark initialization as complete
|
|
751
787
|
state.isInitializing.delete(select);
|
|
788
|
+
dispatchReadyEvent(select, {
|
|
789
|
+
type: 'country',
|
|
790
|
+
phase: 'initial',
|
|
791
|
+
});
|
|
752
792
|
// If no preselected and no geoip selection happened, emit a regular update
|
|
753
793
|
if (!valueSetByWidget) {
|
|
754
794
|
dispatchUpdateEvent(select, { type: 'country', reason: 'regular' });
|
|
@@ -867,14 +907,12 @@ function getConfigFromOptionsOrScript(options) {
|
|
|
867
907
|
// But only if it matches the widget pattern (not a bundled file)
|
|
868
908
|
if (document.currentScript && document.currentScript instanceof HTMLScriptElement) {
|
|
869
909
|
const src = document.currentScript.src;
|
|
870
|
-
if (src && (src.includes('@countriesdb/widget') ||
|
|
871
|
-
src.includes('widget/dist/index.js'))) {
|
|
910
|
+
if (src && (src.includes('@countriesdb/widget') || src.includes('widget/dist/index.js'))) {
|
|
872
911
|
loaderScript = document.currentScript;
|
|
873
912
|
}
|
|
874
913
|
}
|
|
875
914
|
// If currentScript didn't match, search for widget script
|
|
876
915
|
if (!loaderScript) {
|
|
877
|
-
// Only consider script tags that loaded the widget bundle
|
|
878
916
|
const scripts = Array.from(document.getElementsByTagName('script'));
|
|
879
917
|
loaderScript = scripts.find((s) => s.src && (s.src.includes('@countriesdb/widget') ||
|
|
880
918
|
s.src.includes('widget/dist/index.js'))) || null;
|
|
@@ -917,11 +955,6 @@ function getConfigFromOptionsOrScript(options) {
|
|
|
917
955
|
: globalConfig?.isoCountryNames !== undefined
|
|
918
956
|
? globalConfig.isoCountryNames
|
|
919
957
|
: parseBoolean(scriptUrl?.searchParams.get('iso_country_names') ?? 'false'),
|
|
920
|
-
preferOfficialSubdivisions: options.preferOfficialSubdivisions !== undefined
|
|
921
|
-
? options.preferOfficialSubdivisions
|
|
922
|
-
: globalConfig?.preferOfficialSubdivisions !== undefined
|
|
923
|
-
? globalConfig.preferOfficialSubdivisions
|
|
924
|
-
: parseBoolean(scriptUrl?.searchParams.get('prefer_official') ?? 'false'),
|
|
925
958
|
subdivisionRomanizationPreference: options.subdivisionRomanizationPreference ||
|
|
926
959
|
globalConfig?.subdivisionRomanizationPreference ||
|
|
927
960
|
scriptUrl?.searchParams.get('subdivision_romanization_preference') ||
|
|
@@ -931,6 +964,11 @@ function getConfigFromOptionsOrScript(options) {
|
|
|
931
964
|
: globalConfig?.preferLocalVariant !== undefined
|
|
932
965
|
? globalConfig.preferLocalVariant
|
|
933
966
|
: parseBoolean(scriptUrl?.searchParams.get('prefer_local_variant') ?? 'false'),
|
|
967
|
+
preferOfficialSubdivisions: options.preferOfficialSubdivisions !== undefined
|
|
968
|
+
? options.preferOfficialSubdivisions
|
|
969
|
+
: globalConfig?.preferOfficialSubdivisions !== undefined
|
|
970
|
+
? globalConfig.preferOfficialSubdivisions
|
|
971
|
+
: parseBoolean(scriptUrl?.searchParams.get('prefer_official') ?? 'false'),
|
|
934
972
|
countryNameFilter: options.countryNameFilter ?? globalConfig?.countryNameFilter,
|
|
935
973
|
subdivisionNameFilter: options.subdivisionNameFilter ?? globalConfig?.subdivisionNameFilter,
|
|
936
974
|
autoInit: options.autoInit !== undefined
|
|
@@ -968,8 +1006,7 @@ function getBackendUrlFromScript() {
|
|
|
968
1006
|
// But only if it matches the widget pattern (not a bundled file)
|
|
969
1007
|
if (document.currentScript && document.currentScript instanceof HTMLScriptElement) {
|
|
970
1008
|
const src = document.currentScript.src;
|
|
971
|
-
if (src && (src.includes('@countriesdb/widget') ||
|
|
972
|
-
src.includes('widget/dist/index.js'))) {
|
|
1009
|
+
if (src && (src.includes('@countriesdb/widget') || src.includes('widget/dist/index.js'))) {
|
|
973
1010
|
loaderScript = document.currentScript;
|
|
974
1011
|
}
|
|
975
1012
|
}
|
|
@@ -1037,26 +1074,19 @@ function parseBoolean(value) {
|
|
|
1037
1074
|
* Show error when both follow_related and follow_upward are enabled
|
|
1038
1075
|
*/
|
|
1039
1076
|
function showParamConflictError() {
|
|
1040
|
-
const errorMessage = 'Cannot enable both follow_related and follow_upward';
|
|
1077
|
+
const errorMessage = 'Error: Cannot enable both follow_related and follow_upward';
|
|
1041
1078
|
const countrySelects = Array.from(document.querySelectorAll('.country-selection'));
|
|
1042
1079
|
for (const select of countrySelects) {
|
|
1043
|
-
|
|
1080
|
+
select.innerHTML = `<option value="${select.dataset.defaultValue ?? ''}" disabled>${errorMessage}</option>`;
|
|
1044
1081
|
}
|
|
1045
1082
|
const subdivisionSelects = Array.from(document.querySelectorAll('.subdivision-selection'));
|
|
1046
1083
|
for (const select of subdivisionSelects) {
|
|
1047
|
-
|
|
1084
|
+
select.innerHTML = `<option value="${select.dataset.defaultValue ?? ''}" disabled>${errorMessage}</option>`;
|
|
1048
1085
|
}
|
|
1049
1086
|
}
|
|
1050
1087
|
// Expose public loader
|
|
1051
1088
|
if (typeof window !== 'undefined') {
|
|
1052
1089
|
window.CountriesWidgetLoad = CountriesWidgetLoad;
|
|
1053
|
-
// Dispatch ready event to notify consumers that the widget is available
|
|
1054
|
-
// Use setTimeout(0) to ensure it fires after any synchronous code completes
|
|
1055
|
-
setTimeout(() => {
|
|
1056
|
-
if (typeof window !== 'undefined') {
|
|
1057
|
-
window.dispatchEvent(new CustomEvent('countriesWidget:ready'));
|
|
1058
|
-
}
|
|
1059
|
-
}, 0);
|
|
1060
1090
|
// Auto-init if script URL has auto_init=true (or not set, default is true)
|
|
1061
1091
|
// Use a function that waits for DOM to be ready and finds the script tag reliably
|
|
1062
1092
|
(function checkAutoInit() {
|
|
@@ -1071,14 +1101,12 @@ if (typeof window !== 'undefined') {
|
|
|
1071
1101
|
// But only if it matches the widget pattern (not a bundled file)
|
|
1072
1102
|
if (document.currentScript && document.currentScript instanceof HTMLScriptElement) {
|
|
1073
1103
|
const src = document.currentScript.src;
|
|
1074
|
-
if (src && (src.includes('@countriesdb/widget') ||
|
|
1075
|
-
src.includes('widget/dist/index.js'))) {
|
|
1104
|
+
if (src && (src.includes('@countriesdb/widget') || src.includes('widget/dist/index.js'))) {
|
|
1076
1105
|
loaderScript = document.currentScript;
|
|
1077
1106
|
}
|
|
1078
1107
|
}
|
|
1079
1108
|
// If currentScript didn't match, search for widget script
|
|
1080
1109
|
if (!loaderScript) {
|
|
1081
|
-
// Fallback: find script tag with @countriesdb/widget in src
|
|
1082
1110
|
const scripts = Array.from(document.getElementsByTagName('script'));
|
|
1083
1111
|
loaderScript = scripts.find((s) => s.src && (s.src.includes('@countriesdb/widget') ||
|
|
1084
1112
|
s.src.includes('widget/dist/index.js'))) || null;
|