@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 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
 
@@ -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
@@ -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
  */
@@ -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
- handleApiError(select, 'Cannot link to multi-select country. Use data-country-code instead.', true);
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
- handleApiError(select, 'No country select present');
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
- const preferOfficial = select.hasAttribute('data-prefer-official') ||
525
- config.preferOfficialSubdivisions;
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
- handleApiError(select, 'No country select present');
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, '&mdash;');
651
- handleApiError(select, 'Duplicate field');
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
- handleApiError(select, errorMessage, true);
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
- handleApiError(select, errorMessage, true);
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;