@countriesdb/widget 0.1.30 → 0.1.32

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/dist/index.js CHANGED
@@ -1,1480 +1,2 @@
1
- (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3
- typeof define === 'function' && define.amd ? define(factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.CountriesWidget = factory());
5
- })(this, (function () { 'use strict';
6
-
7
- /**
8
- * API client for CountriesDB API
9
- */
10
- class CountriesDBClient {
11
- /**
12
- * Fetch countries from the API
13
- */
14
- static async fetchCountries(options) {
15
- const { apiKey, backendUrl, shouldUseGeoIP = true, isoCountryNames = false, languageHeaders = {}, } = options;
16
- const base = `${backendUrl}/api/countries`;
17
- const params = [];
18
- if (!shouldUseGeoIP) {
19
- params.push('no_geoip=1');
20
- }
21
- if (isoCountryNames) {
22
- params.push('country_name_source=iso');
23
- }
24
- const url = params.length ? `${base}?${params.join('&')}` : base;
25
- return this.fetchFromApi(url, apiKey, languageHeaders);
26
- }
27
- /**
28
- * Fetch subdivisions for a specific country
29
- */
30
- static async fetchSubdivisions(options) {
31
- const { apiKey, backendUrl, countryCode, shouldUseGeoIP = true, preferOfficial = false, subdivisionRomanizationPreference, preferLocalVariant = false, languageHeaders = {}, } = options;
32
- if (!countryCode) {
33
- return { data: [], language: null };
34
- }
35
- const base = `${backendUrl}/api/countries/${countryCode}/subdivisions`;
36
- const params = [];
37
- if (!shouldUseGeoIP) {
38
- params.push('no_geoip=1');
39
- }
40
- if (subdivisionRomanizationPreference) {
41
- params.push(`subdivision_romanization_preference=${encodeURIComponent(subdivisionRomanizationPreference)}`);
42
- }
43
- if (preferLocalVariant) {
44
- params.push('prefer_local_variant=1');
45
- }
46
- if (preferOfficial) {
47
- params.push('prefer_official=1');
48
- }
49
- const url = params.length ? `${base}?${params.join('&')}` : base;
50
- return this.fetchFromApi(url, apiKey, languageHeaders);
51
- }
52
- /**
53
- * Generic API fetch method
54
- */
55
- static async fetchFromApi(url, apiKey, languageHeaders) {
56
- try {
57
- const response = await fetch(url, {
58
- headers: {
59
- ...languageHeaders,
60
- 'X-API-KEY': apiKey,
61
- 'Content-Type': 'application/json',
62
- },
63
- });
64
- if (!response.ok) {
65
- let errorMessage = `HTTP Error: ${response.status}`;
66
- try {
67
- const errorData = await response.json();
68
- if (errorData.error) {
69
- errorMessage = errorData.error;
70
- }
71
- }
72
- catch {
73
- // Ignore JSON parse errors
74
- }
75
- throw new Error(errorMessage);
76
- }
77
- const data = (await response.json());
78
- const language = response.headers.get('X-Selected-Language') || null;
79
- return { data, language };
80
- }
81
- catch (error) {
82
- console.error(`Failed to fetch data from ${url}:`, error);
83
- throw error;
84
- }
85
- }
86
- /**
87
- * Get language headers from browser and config
88
- */
89
- static getLanguageHeaders(forcedLanguage, defaultLanguage) {
90
- const headers = {};
91
- if (forcedLanguage) {
92
- headers['X-Forced-Language'] = forcedLanguage;
93
- }
94
- if (defaultLanguage) {
95
- headers['X-Default-Language'] = defaultLanguage;
96
- }
97
- // Use browser's language preference
98
- if (typeof navigator !== 'undefined' && navigator.language) {
99
- headers['Accept-Language'] = navigator.language;
100
- }
101
- else {
102
- headers['Accept-Language'] = 'en';
103
- }
104
- return headers;
105
- }
106
- }
107
-
108
- /**
109
- * Utilities for building and working with subdivision trees
110
- */
111
- /**
112
- * Build a tree structure from a flat list of subdivisions
113
- */
114
- function buildSubdivisionTree(subdivisions) {
115
- const map = {};
116
- const roots = [];
117
- // Create map of all subdivisions
118
- subdivisions.forEach((item) => {
119
- map[item.id] = {
120
- ...item,
121
- children: [],
122
- };
123
- });
124
- // Build tree structure
125
- subdivisions.forEach((item) => {
126
- const node = map[item.id];
127
- if (item.parent_id && map[item.parent_id]) {
128
- map[item.parent_id].children.push(node);
129
- }
130
- else {
131
- roots.push(node);
132
- }
133
- });
134
- return roots;
135
- }
136
- /**
137
- * Flatten subdivision tree into HTML options
138
- */
139
- function flattenSubdivisionOptions(nodes, level, prefixes, labelKey, language, allowParentSelection, subdivisionNameFilter) {
140
- let html = '';
141
- // Apply name filter and get display names
142
- const nodesWithDisplayNames = nodes
143
- .map((node) => {
144
- let displayName = node[labelKey] || node.name;
145
- const countryCode = node.code ? node.code.split('-')[0] : null;
146
- // Apply subdivisionNameFilter if provided
147
- if (subdivisionNameFilter && typeof subdivisionNameFilter === 'function') {
148
- const filteredName = subdivisionNameFilter(node.code, displayName, language, countryCode, node);
149
- if (filteredName === false) {
150
- return null; // Mark for filtering - remove this item
151
- }
152
- if (filteredName !== null && filteredName !== undefined) {
153
- displayName = filteredName;
154
- }
155
- }
156
- return { ...node, _displayName: displayName };
157
- })
158
- .filter((node) => node !== null);
159
- // Sort: use Unicode-aware sorting if subdivisionNameFilter was used
160
- const locale = subdivisionNameFilter && typeof subdivisionNameFilter === 'function' && language
161
- ? language
162
- : undefined;
163
- nodesWithDisplayNames.sort((a, b) => {
164
- if (locale) {
165
- return a._displayName.localeCompare(b._displayName, locale, {
166
- sensitivity: 'accent', // Case-insensitive, accent-sensitive
167
- ignorePunctuation: false,
168
- });
169
- }
170
- return a._displayName.localeCompare(b._displayName); // Default behavior
171
- });
172
- const prefix = level > 0 ? (prefixes[level] ?? ' '.repeat(level * 2)) : '';
173
- const cssClass = `subdivision-level-${level}`;
174
- for (const node of nodesWithDisplayNames) {
175
- const hasChildren = node.children && node.children.length > 0;
176
- const displayName = node._displayName;
177
- const label = `${prefix}${displayName}`;
178
- if (hasChildren) {
179
- if (allowParentSelection) {
180
- html += `<option value="${node.code}" class="${cssClass}">${label}</option>`;
181
- }
182
- else {
183
- html += `<option disabled value="${node.code}" class="${cssClass}">${label}</option>`;
184
- }
185
- html += flattenSubdivisionOptions(node.children, level + 1, prefixes, labelKey, language, allowParentSelection, subdivisionNameFilter);
186
- }
187
- else {
188
- html += `<option value="${node.code}" class="${cssClass}">${label}</option>`;
189
- }
190
- }
191
- return html;
192
- }
193
- /**
194
- * Parse nesting prefixes from data attributes
195
- */
196
- function parseNestingPrefixes(dataAttributes) {
197
- const prefixes = {};
198
- for (let lvl = 1; lvl <= 10; lvl++) {
199
- const key = `nested${lvl}Prefix`;
200
- const value = dataAttributes[key];
201
- if (value !== undefined) {
202
- prefixes[lvl] = value;
203
- }
204
- else {
205
- break;
206
- }
207
- }
208
- return prefixes;
209
- }
210
-
211
- /**
212
- * Name filtering utilities
213
- */
214
- /**
215
- * Apply country name filter to a list of countries
216
- */
217
- function applyCountryNameFilter(countries, filter, language) {
218
- if (!filter || typeof filter !== 'function') {
219
- return countries.map((c) => ({ ...c, _displayName: c.name }));
220
- }
221
- return countries
222
- .map((country) => {
223
- const filteredName = filter(country.iso_alpha_2, country.name, language, country);
224
- if (filteredName === false) {
225
- return null; // Skip this item
226
- }
227
- const displayName = filteredName !== null && filteredName !== undefined
228
- ? filteredName
229
- : country.name;
230
- return { ...country, _displayName: displayName };
231
- })
232
- .filter((c) => c !== null);
233
- }
234
- /**
235
- * Sort countries with Unicode-aware sorting
236
- */
237
- function sortCountries(countries, language) {
238
- const locale = language;
239
- return [...countries].sort((a, b) => {
240
- {
241
- return a._displayName.localeCompare(b._displayName, locale, {
242
- sensitivity: 'accent', // Case-insensitive, accent-sensitive
243
- ignorePunctuation: false,
244
- });
245
- }
246
- });
247
- }
248
-
249
- /**
250
- * DOM manipulation utilities
251
- */
252
- /**
253
- * Initialize a select element with default option
254
- */
255
- function initializeSelect(select, fallbackLabel = 'Not Applicable', isSubdivision = false) {
256
- const isMultiple = select.hasAttribute('multiple');
257
- // For multi-select, don't add default option or use data-label/data-default-value
258
- if (isMultiple) {
259
- select.innerHTML = '';
260
- // Remove data-label and data-default-value for multi-select (they're ignored)
261
- if (select.dataset.label !== undefined) {
262
- delete select.dataset.label;
263
- }
264
- if (select.dataset.defaultValue !== undefined) {
265
- delete select.dataset.defaultValue;
266
- }
267
- }
268
- else {
269
- const dataLabel = select.dataset.label;
270
- const dataDefaultValue = select.dataset.defaultValue;
271
- const label = dataLabel || fallbackLabel;
272
- const defaultValue = dataDefaultValue ?? '';
273
- select.innerHTML = `<option value="${defaultValue}">${label}</option>`;
274
- if (dataLabel !== undefined) {
275
- select.dataset.label = dataLabel;
276
- }
277
- if (dataDefaultValue !== undefined) {
278
- select.dataset.defaultValue = dataDefaultValue;
279
- }
280
- }
281
- if (isSubdivision) {
282
- select.disabled = true;
283
- }
284
- }
285
- /**
286
- * Populate a select element with countries
287
- */
288
- function populateCountrySelect(select, countries, language, countryNameFilter) {
289
- // Apply filter and get display names
290
- let filteredCountries = applyCountryNameFilter(countries, countryNameFilter, language);
291
- // Sort if filter was applied
292
- if (countryNameFilter) {
293
- filteredCountries = sortCountries(filteredCountries, language);
294
- }
295
- // Check for multi-select using both attribute and property
296
- const isMultiple = select.hasAttribute('multiple') || select.multiple;
297
- if (isMultiple) {
298
- // For multi-select, don't add default option (like old widget)
299
- select.innerHTML = '';
300
- }
301
- else {
302
- // Clear existing options (except default)
303
- const defaultOption = select.querySelector('option[value=""]') ||
304
- (select.dataset.defaultValue ? select.querySelector(`option[value="${select.dataset.defaultValue}"]`) : null);
305
- const defaultValue = select.dataset.defaultValue ?? '';
306
- // Keep default option if it exists
307
- select.innerHTML = defaultOption ? defaultOption.outerHTML : `<option value="${defaultValue}">${select.dataset.label || '&mdash;'}</option>`;
308
- }
309
- // Add country options
310
- filteredCountries.forEach((country) => {
311
- const option = document.createElement('option');
312
- option.value = country.iso_alpha_2;
313
- option.textContent = country._displayName;
314
- select.appendChild(option);
315
- });
316
- }
317
- /**
318
- * Populate a select element with subdivisions
319
- */
320
- function buildSubdivisionOptionsHTML(subdivisions, select, language, showSubdivisionType, allowParentSelection, subdivisionNameFilter) {
321
- const tree = buildSubdivisionTree(subdivisions);
322
- // Parse nesting prefixes from data attributes
323
- const dataAttributes = {};
324
- for (let lvl = 1; lvl <= 10; lvl++) {
325
- const key = `nested${lvl}Prefix`;
326
- const value = select.dataset[key];
327
- if (value !== undefined) {
328
- dataAttributes[`nested${lvl}Prefix`] = value;
329
- }
330
- else {
331
- break;
332
- }
333
- }
334
- const prefixes = parseNestingPrefixes(dataAttributes);
335
- const isMultiple = select.hasAttribute('multiple');
336
- const labelKey = showSubdivisionType ? 'full_name' : 'name';
337
- let html = '';
338
- // For multi-select, don't add default option
339
- if (!isMultiple) {
340
- const defaultLabel = select.dataset.label || '&mdash;';
341
- const defaultValue = select.dataset.defaultValue ?? '';
342
- html = `<option value="${defaultValue}">${defaultLabel}</option>`;
343
- }
344
- html += flattenSubdivisionOptions(tree, 0, prefixes, labelKey, language, allowParentSelection, subdivisionNameFilter);
345
- return html;
346
- }
347
- /**
348
- * Apply preselected value to a select element
349
- * Like old widget, reads the value from the select element itself to get current state
350
- */
351
- function applyPreselectedValue(select, apiKey) {
352
- // Read from select element like old widget (line 740)
353
- const tempOnce = select.dataset._widgetTempPreselect;
354
- const permanent = select.getAttribute('data-preselected') || select.dataset.preselected;
355
- const chosen = (tempOnce !== undefined && tempOnce !== null && String(tempOnce).trim() !== '')
356
- ? tempOnce
357
- : permanent;
358
- if (!chosen || (typeof chosen === 'string' && chosen.trim() === '')) {
359
- return;
360
- }
361
- const value = String(chosen);
362
- const isMultiple = select.hasAttribute('multiple');
363
- if (isMultiple) {
364
- // For multi-select, parse comma-separated values
365
- const values = value
366
- .split(',')
367
- .map((v) => v.trim())
368
- .filter((v) => v !== '');
369
- // Select all matching options
370
- Array.from(select.options).forEach((option) => {
371
- option.selected = values.includes(option.value);
372
- });
373
- }
374
- else {
375
- // Single select: set single value
376
- select.value = value;
377
- }
378
- // Consume preselect so it's only applied once (like old widget)
379
- if (select.dataset._widgetTempPreselect !== undefined) {
380
- delete select.dataset._widgetTempPreselect;
381
- }
382
- if (select.dataset.preselected !== undefined) {
383
- delete select.dataset.preselected;
384
- }
385
- if (select.hasAttribute('data-preselected')) {
386
- select.removeAttribute('data-preselected');
387
- }
388
- }
389
- /**
390
- * Handle API error by showing error message in select
391
- */
392
- function handleApiError(select, errorMessage, replace = false) {
393
- const message = errorMessage instanceof Error ? errorMessage.message : errorMessage;
394
- const defaultValue = select.dataset.defaultValue ?? '';
395
- // Add "Error: " prefix to match old widget behavior and test expectations
396
- const formattedMessage = message.startsWith('Error: ') ? message : `Error: ${message}`;
397
- if (replace) {
398
- select.innerHTML = `<option value="${defaultValue}" disabled>${formattedMessage}</option>`;
399
- }
400
- else {
401
- select.innerHTML += `<option value="${defaultValue}" disabled>${formattedMessage}</option>`;
402
- }
403
- }
404
-
405
- /**
406
- * Event system for widget updates
407
- */
408
- /**
409
- * Dispatch a custom update event for widget changes
410
- */
411
- function dispatchUpdateEvent(select, detail = {}) {
412
- let selectedValues = [];
413
- if (select.multiple) {
414
- // For multi-select, selectedOptions is a HTMLCollection, convert to array
415
- selectedValues = Array.from(select.selectedOptions || [])
416
- .map((opt) => opt.value)
417
- .filter((v) => v !== '');
418
- }
419
- else {
420
- // For single-select, use value
421
- selectedValues = select.value ? [select.value] : [];
422
- }
423
- const evt = new CustomEvent('countriesWidget:update', {
424
- bubbles: true,
425
- detail: {
426
- value: select.value || '',
427
- selectedValues,
428
- name: select.dataset.name || null,
429
- country: select.dataset.country || null,
430
- isSubdivision: select.classList.contains('subdivision-selection'),
431
- ...detail,
432
- },
433
- });
434
- select.dispatchEvent(evt);
435
- }
436
- /**
437
- * Dispatch a custom ready event once a select has been populated.
438
- */
439
- function dispatchReadyEvent(select, detail = {}) {
440
- const selectedValues = select.multiple
441
- ? Array.from(select.selectedOptions || [])
442
- .map((opt) => opt.value)
443
- .filter((v) => v !== '')
444
- : select.value
445
- ? [select.value]
446
- : [];
447
- const evt = new CustomEvent('countriesWidget:ready', {
448
- bubbles: true,
449
- detail: {
450
- value: select.value || '',
451
- selectedValues,
452
- name: select.dataset.name || null,
453
- country: select.dataset.country || null,
454
- isSubdivision: select.classList.contains('subdivision-selection'),
455
- type: select.classList.contains('subdivision-selection')
456
- ? 'subdivision'
457
- : 'country',
458
- phase: 'initial',
459
- ...detail,
460
- },
461
- });
462
- select.dispatchEvent(evt);
463
- }
464
- /**
465
- * Check if an event was initiated by the widget (not user)
466
- */
467
- function isWidgetInitiatedEvent(event) {
468
- return event.isWidgetInitiated === true;
469
- }
470
-
471
- /**
472
- * Follow logic for related and upward navigation
473
- */
474
- /**
475
- * Trigger follow logic when a subdivision is selected
476
- */
477
- async function triggerFollowLogic(select, apiKey, backendUrl, state, followRelated, followUpward, updateSubdivisionSelectFn, updateSubdivisionsFn) {
478
- if (!followRelated && !followUpward) {
479
- return;
480
- }
481
- const linkedCountrySelect = document.querySelector(`.country-selection[data-name="${select.dataset.country}"]`);
482
- // Only work if country is single-select (never when country is multi)
483
- if (!linkedCountrySelect || linkedCountrySelect.multiple) {
484
- return;
485
- }
486
- const allSubs = state.subdivisionsMap.get(select);
487
- if (!allSubs) {
488
- return;
489
- }
490
- // For follow_upward, only work if subdivision is single-select
491
- if (followUpward && select.multiple) {
492
- return;
493
- }
494
- const selectedCode = select.value;
495
- if (!selectedCode) {
496
- return;
497
- }
498
- const picked = allSubs.find((s) => s.code === selectedCode);
499
- if (!picked) {
500
- return;
501
- }
502
- // follow_related
503
- if (followRelated && picked.related_country_code) {
504
- const targetCountry = picked.related_country_code;
505
- const targetSubdivision = picked.related_subdivision_code;
506
- const relatedSubsSelect = document.querySelector(`.subdivision-selection[data-country="${linkedCountrySelect.dataset.name}"]`);
507
- if (relatedSubsSelect && targetSubdivision) {
508
- relatedSubsSelect.dataset._widgetTempPreselect = targetSubdivision; // one-time preselect
509
- }
510
- if (linkedCountrySelect.value !== targetCountry) {
511
- // Directly set value like old widget did
512
- linkedCountrySelect.value = targetCountry;
513
- dispatchUpdateEvent(linkedCountrySelect, { type: 'country', reason: 'regular' });
514
- // Update all subdivision selects for the new country (like old widget)
515
- if (updateSubdivisionsFn) {
516
- await updateSubdivisionsFn(linkedCountrySelect, apiKey);
517
- }
518
- else if (relatedSubsSelect) {
519
- // Fallback: update just the one subdivision select
520
- await updateSubdivisionSelectFn(relatedSubsSelect, apiKey, targetCountry);
521
- }
522
- }
523
- else {
524
- if (relatedSubsSelect && targetSubdivision) {
525
- await updateSubdivisionSelectFn(relatedSubsSelect, apiKey, targetCountry);
526
- }
527
- }
528
- }
529
- // follow_upward
530
- if (followUpward && picked.is_subdivision_of) {
531
- const parentCode = picked.is_subdivision_of.parent_country_code;
532
- const parentSub = picked.is_subdivision_of.subdivision_code;
533
- if (parentSub) {
534
- const relatedSubsSelect = document.querySelector(`.subdivision-selection[data-country="${linkedCountrySelect.dataset.name}"]`);
535
- // Only preselect if subdivision is single-select (follow_upward doesn't work with multi)
536
- if (relatedSubsSelect && !relatedSubsSelect.multiple) {
537
- relatedSubsSelect.dataset._widgetTempPreselect = parentSub; // one-time preselect
538
- }
539
- }
540
- if (linkedCountrySelect.value !== parentCode) {
541
- // Directly set value like old widget did
542
- linkedCountrySelect.value = parentCode;
543
- dispatchUpdateEvent(linkedCountrySelect, { type: 'country', reason: 'regular' });
544
- // Update all subdivision selects for the new country (like old widget)
545
- if (updateSubdivisionsFn) {
546
- await updateSubdivisionsFn(linkedCountrySelect, apiKey);
547
- }
548
- else {
549
- const relatedSubsSelect = document.querySelector(`.subdivision-selection[data-country="${linkedCountrySelect.dataset.name}"]`);
550
- if (relatedSubsSelect) {
551
- await updateSubdivisionSelectFn(relatedSubsSelect, apiKey, parentCode);
552
- }
553
- }
554
- }
555
- else if (parentSub) {
556
- const relatedSubsSelect = document.querySelector(`.subdivision-selection[data-country="${linkedCountrySelect.dataset.name}"]`);
557
- if (relatedSubsSelect && !relatedSubsSelect.multiple) {
558
- await updateSubdivisionSelectFn(relatedSubsSelect, apiKey, parentCode);
559
- }
560
- }
561
- }
562
- }
563
- /**
564
- * Handle follow_related from subdivision change event
565
- */
566
- async function handleFollowRelatedFromSubdivision(select, apiKey, backendUrl, state, followRelated, updateSubdivisionSelectFn) {
567
- if (!followRelated) {
568
- return;
569
- }
570
- const linkedCountrySelect = document.querySelector(`.country-selection[data-name="${select.dataset.country}"]`);
571
- // Only work if country is single-select (never when country is multi)
572
- if (!linkedCountrySelect || linkedCountrySelect.multiple) {
573
- return;
574
- }
575
- const allSubs = state.subdivisionsMap.get(select);
576
- if (!allSubs) {
577
- return;
578
- }
579
- // Get selected code (for multi-select, use first selected value)
580
- const selectedCode = select.multiple
581
- ? (select.selectedOptions[0]?.value || null)
582
- : select.value;
583
- if (!selectedCode) {
584
- return;
585
- }
586
- const picked = allSubs.find((s) => s.code === selectedCode);
587
- if (!picked || !picked.related_country_code) {
588
- return;
589
- }
590
- const targetCountry = picked.related_country_code;
591
- const targetSubdivision = picked.related_subdivision_code;
592
- const relatedSubsSelect = document.querySelector(`.subdivision-selection[data-country="${linkedCountrySelect.dataset.name}"]`);
593
- if (relatedSubsSelect && targetSubdivision) {
594
- relatedSubsSelect.dataset.preselected = targetSubdivision;
595
- }
596
- if (linkedCountrySelect.value !== targetCountry) {
597
- // Directly set value like old widget did
598
- linkedCountrySelect.value = targetCountry;
599
- dispatchUpdateEvent(linkedCountrySelect, { type: 'country', reason: 'regular' });
600
- // Update all subdivision selects for the new country (like old widget)
601
- // Note: updateSubdivisionsFn is not available in handleFollowRelatedFromSubdivision,
602
- // so we update the subdivision select manually
603
- if (relatedSubsSelect) {
604
- await updateSubdivisionSelectFn(relatedSubsSelect, apiKey, targetCountry);
605
- }
606
- }
607
- else {
608
- if (relatedSubsSelect && targetSubdivision) {
609
- await updateSubdivisionSelectFn(relatedSubsSelect, apiKey, targetCountry);
610
- }
611
- }
612
- }
613
- /**
614
- * Handle follow_upward from subdivision change event
615
- */
616
- async function handleFollowUpwardFromSubdivision(select, apiKey, backendUrl, state, followUpward, updateSubdivisionSelectFn) {
617
- if (!followUpward) {
618
- return;
619
- }
620
- // Disable for multi-select subdivisions
621
- if (select.multiple) {
622
- return;
623
- }
624
- const linkedCountrySelect = document.querySelector(`.country-selection[data-name="${select.dataset.country}"]`);
625
- // Only work if country is single-select (never when country is multi)
626
- if (!linkedCountrySelect || linkedCountrySelect.multiple) {
627
- return;
628
- }
629
- const allSubs = state.subdivisionsMap.get(select);
630
- if (!allSubs) {
631
- return;
632
- }
633
- const selectedCode = select.value;
634
- if (!selectedCode) {
635
- return;
636
- }
637
- const picked = allSubs.find((s) => s.code === selectedCode);
638
- if (!picked || !picked.is_subdivision_of) {
639
- return;
640
- }
641
- const parentCode = picked.is_subdivision_of.parent_country_code;
642
- const parentSub = picked.is_subdivision_of.subdivision_code;
643
- if (parentSub) {
644
- const relatedSubsSelect = document.querySelector(`.subdivision-selection[data-country="${linkedCountrySelect.dataset.name}"]`);
645
- if (relatedSubsSelect) {
646
- relatedSubsSelect.dataset.preselected = parentSub;
647
- }
648
- }
649
- if (linkedCountrySelect.value !== parentCode) {
650
- // Directly set value like old widget did
651
- linkedCountrySelect.value = parentCode;
652
- dispatchUpdateEvent(linkedCountrySelect, { type: 'country', reason: 'follow' });
653
- // Update all subdivision selects for the new country (like old widget)
654
- // Note: updateSubdivisionsFn is not available in handleFollowUpwardFromSubdivision,
655
- // so we update the subdivision select manually
656
- const relatedSubsSelect = document.querySelector(`.subdivision-selection[data-country="${linkedCountrySelect.dataset.name}"]`);
657
- if (relatedSubsSelect) {
658
- await updateSubdivisionSelectFn(relatedSubsSelect, apiKey, parentCode);
659
- }
660
- }
661
- else if (parentSub) {
662
- const relatedSubsSelect = document.querySelector(`.subdivision-selection[data-country="${linkedCountrySelect.dataset.name}"]`);
663
- if (relatedSubsSelect) {
664
- await updateSubdivisionSelectFn(relatedSubsSelect, apiKey, parentCode);
665
- }
666
- }
667
- }
668
- /**
669
- * Handle follow_upward from country change event
670
- */
671
- async function handleFollowUpwardFromCountry(select, apiKey, backendUrl, state, followUpward, updateSubdivisionSelectFn) {
672
- // Only works when country is single-select (never for multi-select)
673
- if (!followUpward || select.multiple) {
674
- return;
675
- }
676
- const countries = state.countriesMap.get(select);
677
- if (!countries) {
678
- return;
679
- }
680
- const chosen = select.value;
681
- if (!chosen) {
682
- return;
683
- }
684
- const picked = countries.find((c) => c.iso_alpha_2 === chosen);
685
- if (!picked || !picked.is_subdivision_of) {
686
- return;
687
- }
688
- const parentCode = picked.is_subdivision_of.parent_country_code;
689
- const parentSub = picked.is_subdivision_of.subdivision_code;
690
- if (select.value !== parentCode) {
691
- if (parentSub) {
692
- const linkedSubdivisionSelects = Array.from(document.querySelectorAll(`.subdivision-selection[data-country="${select.dataset.name}"]`));
693
- for (const s of linkedSubdivisionSelects) {
694
- // Only preselect if subdivision is single-select (follow_upward doesn't work with multi)
695
- if (!s.multiple) {
696
- s.dataset.preselected = parentSub;
697
- }
698
- }
699
- }
700
- // Directly set value like old widget did
701
- select.value = parentCode;
702
- dispatchUpdateEvent(select, { type: 'country', reason: 'regular' });
703
- // Update all subdivision selects for the new country (like old widget)
704
- // Note: updateSubdivisionsFn is not available in handleFollowUpwardFromCountry,
705
- // so we update all subdivision selects manually
706
- const linkedSubdivisionSelects = Array.from(document.querySelectorAll(`.subdivision-selection[data-country="${select.dataset.name}"]`));
707
- for (const s of linkedSubdivisionSelects) {
708
- await updateSubdivisionSelectFn(s, apiKey, parentCode);
709
- }
710
- }
711
- else if (parentSub) {
712
- const linkedSubdivisionSelects = Array.from(document.querySelectorAll(`.subdivision-selection[data-country="${select.dataset.name}"]`));
713
- for (const s of linkedSubdivisionSelects) {
714
- // Only update if subdivision is single-select
715
- if (!s.multiple) {
716
- await updateSubdivisionSelectFn(s, apiKey, parentCode);
717
- }
718
- }
719
- }
720
- }
721
-
722
- /**
723
- * Widget initialization logic
724
- */
725
- /**
726
- * Setup subdivision selection elements
727
- */
728
- async function setupSubdivisionSelection(apiKey, backendUrl, state, config) {
729
- const subdivisionSelects = Array.from(document.querySelectorAll('.subdivision-selection'));
730
- for (const select of subdivisionSelects) {
731
- // Initialize with a default option
732
- initializeSelect(select, '&mdash;', true);
733
- // Linked country select (if any)
734
- const countryName = select.dataset.country;
735
- const linkedCountrySelect = countryName
736
- ? document.querySelector(`.country-selection[data-name="${countryName}"]`)
737
- : null;
738
- // Check if linked country select is multi-select (not allowed)
739
- if (linkedCountrySelect && linkedCountrySelect.hasAttribute('multiple')) {
740
- const defaultValue = select.dataset.defaultValue ?? '';
741
- select.innerHTML = `<option value="${defaultValue}" disabled>Error: Cannot link to multi-select country. Use data-country-code instead.</option>`;
742
- continue;
743
- }
744
- // No direct link → maybe data-country-code
745
- if (!countryName || !linkedCountrySelect) {
746
- if (select.hasAttribute('data-country-code') &&
747
- select.dataset.countryCode) {
748
- await updateSubdivisionSelect(select, apiKey, backendUrl, state, config, select.dataset.countryCode);
749
- }
750
- else {
751
- const defaultValue = select.dataset.defaultValue ?? '';
752
- select.innerHTML += `<option value="${defaultValue}" disabled>Error: No country select present</option>`;
753
- }
754
- }
755
- // Remove old event handlers if they exist (for re-initialization)
756
- const oldHandlers = state.eventHandlers.get(select);
757
- if (oldHandlers) {
758
- if (oldHandlers.update) {
759
- select.removeEventListener('change', oldHandlers.update);
760
- }
761
- if (oldHandlers.followRelated) {
762
- select.removeEventListener('change', oldHandlers.followRelated);
763
- }
764
- if (oldHandlers.followUpward) {
765
- select.removeEventListener('change', oldHandlers.followUpward);
766
- }
767
- }
768
- // Create new event handlers
769
- const handlers = {};
770
- // Always dispatch an update event for user-initiated subdivision changes
771
- handlers.update = (event) => {
772
- if (isWidgetInitiatedEvent(event)) {
773
- return;
774
- }
775
- dispatchUpdateEvent(select, { type: 'subdivision', reason: 'regular' });
776
- };
777
- select.addEventListener('change', handlers.update);
778
- // --- follow_related (forward direction) ---
779
- handlers.followRelated = async (event) => {
780
- if (isWidgetInitiatedEvent(event)) {
781
- return;
782
- }
783
- await handleFollowRelatedFromSubdivision(select, apiKey, backendUrl, state, config.followRelated, (s, key, code) => updateSubdivisionSelect(s, key, backendUrl, state, config, code));
784
- };
785
- select.addEventListener('change', handlers.followRelated);
786
- // --- follow_upward (reverse direction) ---
787
- handlers.followUpward = async (event) => {
788
- if (isWidgetInitiatedEvent(event)) {
789
- return;
790
- }
791
- await handleFollowUpwardFromSubdivision(select, apiKey, backendUrl, state, config.followUpward, (s, key, code) => updateSubdivisionSelect(s, key, backendUrl, state, config, code));
792
- };
793
- select.addEventListener('change', handlers.followUpward);
794
- // Store handlers for future cleanup
795
- state.eventHandlers.set(select, handlers);
796
- }
797
- }
798
- /**
799
- * Update subdivision select with data for a specific country
800
- */
801
- async function updateSubdivisionSelect(select, apiKey, backendUrl, state, config, countryCode) {
802
- const preselectedValue = select.getAttribute('data-preselected') || select.dataset.preselected;
803
- // Check if this is a reload (select was already populated before)
804
- const isReload = state.subdivisionsMap.has(select);
805
- initializeSelect(select, '&mdash;', true);
806
- // Mark as initializing to prevent change events
807
- state.isInitializing.add(select);
808
- let effectiveCountryCode = countryCode;
809
- if (!effectiveCountryCode) {
810
- effectiveCountryCode = select.dataset.countryCode || '';
811
- }
812
- if (effectiveCountryCode) {
813
- let valueSetByWidget = false; // Track if a value was set by widget
814
- try {
815
- // Use GeoIP only if data-preselected attribute is not set at all
816
- const shouldUseGeoIP = preselectedValue === undefined || preselectedValue === null;
817
- // Check if this subdivision select prefers official subdivisions
818
- // Use data attribute if present, otherwise use config
819
- const preferOfficial = select.hasAttribute('data-prefer-official')
820
- ? true
821
- : config.preferOfficialSubdivisions;
822
- const languageHeaders = CountriesDBClient.getLanguageHeaders(config.forcedLanguage, config.defaultLanguage);
823
- const subdivisionsResult = await CountriesDBClient.fetchSubdivisions({
824
- apiKey,
825
- backendUrl,
826
- countryCode: effectiveCountryCode,
827
- shouldUseGeoIP,
828
- preferOfficial,
829
- subdivisionRomanizationPreference: config.subdivisionRomanizationPreference,
830
- preferLocalVariant: config.preferLocalVariant,
831
- languageHeaders,
832
- });
833
- const subdivisions = subdivisionsResult.data;
834
- const subdivisionsLanguage = subdivisionsResult.language || 'en';
835
- // Store in memory
836
- state.subdivisionsMap.set(select, subdivisions);
837
- state.subdivisionsLanguageMap.set(select, subdivisionsLanguage);
838
- if (subdivisions.length > 0) {
839
- // Populate <option>
840
- select.disabled = false;
841
- // Preserve data attributes before setting innerHTML (only for non-multi-select)
842
- const isMultiple = select.hasAttribute('multiple');
843
- const dataLabel = !isMultiple ? select.dataset.label : undefined;
844
- const dataDefaultValue = !isMultiple
845
- ? select.dataset.defaultValue
846
- : undefined;
847
- // Preserve user's current selection before clearing innerHTML
848
- // This prevents preselected values from overwriting user selections
849
- const currentValue = select.value;
850
- const defaultValue = select.dataset.defaultValue || '';
851
- const hasUserSelection = currentValue && currentValue !== defaultValue && currentValue.trim() !== '';
852
- select.innerHTML = buildSubdivisionOptionsHTML(subdivisions, select, subdivisionsLanguage, config.showSubdivisionType, config.allowParentSelection, config.subdivisionNameFilter);
853
- // Restore data attributes after setting innerHTML
854
- if (!isMultiple) {
855
- if (dataLabel !== undefined) {
856
- select.dataset.label = dataLabel;
857
- }
858
- if (dataDefaultValue !== undefined) {
859
- select.dataset.defaultValue = dataDefaultValue;
860
- }
861
- }
862
- // Restore user's selection if it exists in the new options (user selection takes priority)
863
- let userSelectionRestored = false;
864
- if (hasUserSelection && !isMultiple) {
865
- const optionExists = Array.from(select.options).some(opt => opt.value === currentValue);
866
- if (optionExists) {
867
- select.value = currentValue;
868
- userSelectionRestored = true;
869
- // Don't dispatch event here - user already selected it, no need to notify again
870
- }
871
- }
872
- // Manual preselection only if user hasn't selected anything
873
- if (!userSelectionRestored) {
874
- // Check if preselected value exists (applyPreselectedValue will read it from select element)
875
- const hasPreselectedValue = (select.getAttribute('data-preselected') || select.dataset.preselected || select.dataset._widgetTempPreselect) !== undefined;
876
- if (hasPreselectedValue) {
877
- applyPreselectedValue(select, apiKey);
878
- dispatchUpdateEvent(select, {
879
- type: 'subdivision',
880
- reason: 'preselected',
881
- });
882
- valueSetByWidget = true;
883
- await triggerFollowLogic(select, apiKey, backendUrl, state, config.followRelated, config.followUpward, (s, key, code) => updateSubdivisionSelect(s, key, backendUrl, state, config, code), (countrySelect, key) => updateSubdivisions(countrySelect, key, backendUrl, state, config));
884
- }
885
- else {
886
- // Try GeoIP preselect (only if user hasn't selected anything)
887
- if (shouldUseGeoIP) {
888
- const preselectedSubdivision = subdivisions.find((subdivision) => subdivision.preselected);
889
- if (preselectedSubdivision) {
890
- const isMultiple = select.hasAttribute('multiple');
891
- if (isMultiple) {
892
- // For multi-select, find and select the option
893
- const option = Array.from(select.options).find((opt) => opt.value === preselectedSubdivision.code);
894
- if (option) {
895
- option.selected = true;
896
- }
897
- }
898
- else {
899
- // Single select: set value directly
900
- select.value = preselectedSubdivision.code;
901
- }
902
- dispatchUpdateEvent(select, {
903
- type: 'subdivision',
904
- reason: 'geoip',
905
- });
906
- valueSetByWidget = true;
907
- await triggerFollowLogic(select, apiKey, backendUrl, state, config.followRelated, config.followUpward, (s, key, code) => updateSubdivisionSelect(s, key, backendUrl, state, config, code), (countrySelect, key) => updateSubdivisions(countrySelect, key, backendUrl, state, config));
908
- }
909
- }
910
- }
911
- }
912
- }
913
- else {
914
- select.disabled = true;
915
- }
916
- }
917
- catch (error) {
918
- console.error('Failed to fetch subdivisions:', error);
919
- handleApiError(select, error);
920
- }
921
- finally {
922
- // Mark initialization as complete
923
- state.isInitializing.delete(select);
924
- dispatchReadyEvent(select, {
925
- type: 'subdivision',
926
- phase: isReload ? 'reload' : 'initial',
927
- });
928
- // Only fire 'reload' if this is a reload, not initial load
929
- if (isReload && !valueSetByWidget) {
930
- dispatchUpdateEvent(select, {
931
- type: 'subdivision',
932
- reason: 'reload',
933
- });
934
- }
935
- }
936
- }
937
- else if (!select.dataset.country ||
938
- !document.querySelector(`.country-selection[data-name="${select.dataset.country}"]`)) {
939
- const defaultValue = select.dataset.defaultValue ?? '';
940
- select.innerHTML += `<option value="${defaultValue}" disabled>Error: No country select present</option>`;
941
- }
942
- }
943
- /**
944
- * Get current subdivision config from window.CountriesDBConfig
945
- * This ensures we always use the latest config values, even after widget reload
946
- */
947
- function getCurrentSubdivisionConfig(apiKey, backendUrl) {
948
- const globalConfig = typeof window !== 'undefined' && window.CountriesDBConfig
949
- ? window.CountriesDBConfig
950
- : null;
951
- return {
952
- followRelated: globalConfig?.followRelated || false,
953
- followUpward: globalConfig?.followUpward || false,
954
- showSubdivisionType: globalConfig?.showSubdivisionType !== false,
955
- allowParentSelection: globalConfig?.allowParentSelection || false,
956
- preferOfficialSubdivisions: globalConfig?.preferOfficialSubdivisions || false,
957
- subdivisionRomanizationPreference: globalConfig?.subdivisionRomanizationPreference,
958
- preferLocalVariant: globalConfig?.preferLocalVariant || false,
959
- forcedLanguage: globalConfig?.forcedLanguage,
960
- defaultLanguage: globalConfig?.defaultLanguage,
961
- subdivisionNameFilter: globalConfig?.subdivisionNameFilter,
962
- };
963
- }
964
- /**
965
- * Update subdivisions for all linked subdivision selects when country changes
966
- */
967
- async function updateSubdivisions(countrySelect, apiKey, backendUrl, state, config) {
968
- // Don't update subdivisions for multi-select countries (not supported)
969
- if (countrySelect.hasAttribute('multiple')) {
970
- return;
971
- }
972
- const selectedCountry = countrySelect.value;
973
- const linkedSubdivisionSelects = Array.from(document.querySelectorAll(`.subdivision-selection[data-country="${countrySelect.dataset.name}"]`));
974
- for (const select of linkedSubdivisionSelects) {
975
- await updateSubdivisionSelect(select, apiKey, backendUrl, state, config, selectedCountry);
976
- }
977
- }
978
- /**
979
- * Setup country selection elements
980
- */
981
- async function setupCountrySelection(apiKey, backendUrl, state, config, subdivisionConfig) {
982
- const countrySelects = Array.from(document.querySelectorAll('.country-selection'));
983
- const seenNames = {}; // track data-name to detect duplicates
984
- for (const select of countrySelects) {
985
- const name = select.dataset.name;
986
- // Duplicates
987
- if (name && seenNames[name]) {
988
- select.removeAttribute('data-name');
989
- initializeSelect(select, '&mdash;');
990
- const defaultValue = select.dataset.defaultValue ?? '';
991
- select.innerHTML += `<option value="${defaultValue}" disabled>Error: Duplicate field</option>`;
992
- continue;
993
- }
994
- if (name) {
995
- seenNames[name] = true;
996
- }
997
- // Note: Class renaming is now handled earlier in fixMisClassedElements()
998
- // This check is kept as a safety net but should not be needed
999
- // Initialize
1000
- initializeSelect(select, '&mdash;');
1001
- // Mark as initializing to prevent change events
1002
- state.isInitializing.add(select);
1003
- let valueSetByWidget = false; // Track if value was set by widget
1004
- let loadedInitialSubdivisions = false; // Track if subdivisions were loaded
1005
- try {
1006
- const preselectedValue = select.getAttribute('data-preselected') || select.dataset.preselected;
1007
- // Use GeoIP only if data-preselected attribute is not set at all
1008
- const shouldUseGeoIP = preselectedValue === undefined || preselectedValue === null;
1009
- const languageHeaders = CountriesDBClient.getLanguageHeaders(config.forcedLanguage, config.defaultLanguage);
1010
- // Fetch & populate countries
1011
- const countriesResult = await CountriesDBClient.fetchCountries({
1012
- apiKey,
1013
- backendUrl,
1014
- shouldUseGeoIP,
1015
- isoCountryNames: config.isoCountryNames,
1016
- languageHeaders,
1017
- });
1018
- const countries = countriesResult.data;
1019
- const countriesLanguage = countriesResult.language || 'en';
1020
- state.countriesMap.set(select, countries);
1021
- populateCountrySelect(select, countries, countriesLanguage, config.countryNameFilter);
1022
- // Apply preselected (manual)
1023
- // Check if preselected value exists (applyPreselectedValue will read it from select element)
1024
- const hasPreselectedValue = (select.getAttribute('data-preselected') || select.dataset.preselected || select.dataset._widgetTempPreselect) !== undefined;
1025
- if (hasPreselectedValue) {
1026
- applyPreselectedValue(select, apiKey);
1027
- dispatchUpdateEvent(select, { type: 'country', reason: 'preselected' });
1028
- valueSetByWidget = true;
1029
- // Load subdivisions after applying preselected value
1030
- if (select.value &&
1031
- select.value !== (select.dataset.defaultValue || '')) {
1032
- // This will be handled by the updateSubdivisions call below
1033
- // We need to pass the config for subdivisions
1034
- }
1035
- }
1036
- // GeoIP auto-select
1037
- if (shouldUseGeoIP) {
1038
- const preselectedCountry = countries.find((country) => country.preselected);
1039
- if (preselectedCountry) {
1040
- select.value = preselectedCountry.iso_alpha_2;
1041
- dispatchUpdateEvent(select, { type: 'country', reason: 'geoip' });
1042
- valueSetByWidget = true;
1043
- }
1044
- }
1045
- // If already chosen, load subdivisions
1046
- if (!loadedInitialSubdivisions &&
1047
- select.value &&
1048
- select.value !== (select.dataset.defaultValue || '')) {
1049
- // Subdivisions will be loaded by event handler
1050
- loadedInitialSubdivisions = true;
1051
- }
1052
- // Remove old event handlers if they exist (for re-initialization)
1053
- const oldCountryHandlers = state.eventHandlers.get(select);
1054
- if (oldCountryHandlers?.countryChange) {
1055
- select.removeEventListener('change', oldCountryHandlers.countryChange);
1056
- }
1057
- // Create new country change handler
1058
- const countryChangeHandler = async (event) => {
1059
- if (isWidgetInitiatedEvent(event)) {
1060
- return;
1061
- }
1062
- // Dispatch update event for user-initiated country change
1063
- dispatchUpdateEvent(select, { type: 'country', reason: 'regular' });
1064
- // Get current config dynamically to ensure we use the latest values
1065
- const currentSubdivisionConfig = getCurrentSubdivisionConfig(apiKey, backendUrl);
1066
- const currentConfig = typeof window !== 'undefined' && window.CountriesDBConfig
1067
- ? window.CountriesDBConfig
1068
- : config;
1069
- // Update subdivisions with current config
1070
- await updateSubdivisions(select, apiKey, backendUrl, state, currentSubdivisionConfig);
1071
- const chosen = select.value;
1072
- if (!chosen) {
1073
- return;
1074
- }
1075
- const stored = state.countriesMap.get(select) || [];
1076
- const picked = stored.find((c) => c.iso_alpha_2 === chosen);
1077
- if (!picked) {
1078
- return;
1079
- }
1080
- // followUpward from country perspective
1081
- // Only works when country is single-select (never for multi-select)
1082
- const currentFollowUpward = currentConfig?.followUpward || false;
1083
- if (currentFollowUpward && !select.multiple && picked.is_subdivision_of) {
1084
- await handleFollowUpwardFromCountry(select, apiKey, backendUrl, state, currentFollowUpward, (s, key, code) => updateSubdivisionSelect(s, key, backendUrl, state, currentSubdivisionConfig, code));
1085
- }
1086
- };
1087
- // Store and attach the handler
1088
- const countryHandlers = state.eventHandlers.get(select) || {};
1089
- countryHandlers.countryChange = countryChangeHandler;
1090
- state.eventHandlers.set(select, countryHandlers);
1091
- select.addEventListener('change', countryChangeHandler);
1092
- }
1093
- catch (error) {
1094
- console.error('Failed to fetch countries:', error);
1095
- handleApiError(select, error);
1096
- // Handle subdivision errors
1097
- if (select.dataset.name) {
1098
- const linkedSubdivisionSelects = Array.from(document.querySelectorAll(`.subdivision-selection[data-country="${select.dataset.name}"]`));
1099
- for (const s of linkedSubdivisionSelects) {
1100
- initializeSelect(s, '&mdash;');
1101
- handleApiError(s, error);
1102
- }
1103
- }
1104
- }
1105
- finally {
1106
- // Mark initialization as complete
1107
- state.isInitializing.delete(select);
1108
- dispatchReadyEvent(select, {
1109
- type: 'country',
1110
- phase: 'initial',
1111
- });
1112
- // If no preselected and no geoip selection happened, emit a regular update
1113
- if (!valueSetByWidget) {
1114
- dispatchUpdateEvent(select, { type: 'country', reason: 'regular' });
1115
- }
1116
- }
1117
- }
1118
- }
1119
-
1120
- /**
1121
- * @countriesdb/widget
1122
- *
1123
- * Plain JavaScript widget for CountriesDB.
1124
- * Provides DOM manipulation and auto-initialization for country/subdivision selects.
1125
- */
1126
- // Global namespace to prevent double initialization
1127
- const NS_KEY = '__CountriesWidgetNS__';
1128
- /**
1129
- * Main widget initialization function
1130
- */
1131
- async function CountriesWidgetLoad(options = {}) {
1132
- // Initialize namespace
1133
- if (!window[NS_KEY]) {
1134
- window[NS_KEY] = {
1135
- initialized: false,
1136
- initPromise: null,
1137
- version: 0,
1138
- eventHandlers: new WeakMap(),
1139
- };
1140
- }
1141
- const NS = window[NS_KEY];
1142
- // Ensure eventHandlers exists (for backwards compatibility)
1143
- if (!NS.eventHandlers) {
1144
- NS.eventHandlers = new WeakMap();
1145
- }
1146
- // Handle reload option - reset state to force re-initialization
1147
- const shouldReload = options.reload === true;
1148
- if (shouldReload) {
1149
- NS.initialized = false;
1150
- NS.initPromise = null;
1151
- }
1152
- // Share the same promise across concurrent calls
1153
- NS.initPromise = (async () => {
1154
- // Wait for DOM if needed
1155
- if (document.readyState === 'loading') {
1156
- await new Promise((resolve) => {
1157
- document.addEventListener('DOMContentLoaded', () => resolve(), {
1158
- once: true,
1159
- });
1160
- });
1161
- }
1162
- // Get configuration from options or script URL
1163
- const config = getConfigFromOptionsOrScript(options);
1164
- // Fix mis-classed elements early (DOM cleanup that should always happen)
1165
- fixMisClassedElements();
1166
- // Check for conflicting parameters
1167
- if (config.followRelated && config.followUpward) {
1168
- showParamConflictError();
1169
- return false;
1170
- }
1171
- // Initialize widget state
1172
- // Use global eventHandlers from namespace so we can clean up old handlers on re-initialization
1173
- const state = {
1174
- countriesMap: new WeakMap(),
1175
- subdivisionsMap: new WeakMap(),
1176
- subdivisionsLanguageMap: new WeakMap(),
1177
- isInitializing: new Set(),
1178
- eventHandlers: NS.eventHandlers, // Use global WeakMap that persists across calls
1179
- };
1180
- // Use empty string if publicKey is missing (will show error when API calls fail)
1181
- const apiKey = config.publicKey || '';
1182
- // Setup subdivisions first (they depend on countries)
1183
- await setupSubdivisionSelection(apiKey, config.backendUrl, state, {
1184
- followRelated: config.followRelated || false,
1185
- followUpward: config.followUpward || false,
1186
- showSubdivisionType: config.showSubdivisionType !== false,
1187
- allowParentSelection: config.allowParentSelection || false,
1188
- preferOfficialSubdivisions: config.preferOfficialSubdivisions || false,
1189
- subdivisionRomanizationPreference: config.subdivisionRomanizationPreference,
1190
- preferLocalVariant: config.preferLocalVariant || false,
1191
- subdivisionNameFilter: config.subdivisionNameFilter,
1192
- });
1193
- const subdivisionConfig = {
1194
- followRelated: config.followRelated || false,
1195
- followUpward: config.followUpward || false,
1196
- showSubdivisionType: config.showSubdivisionType !== false,
1197
- allowParentSelection: config.allowParentSelection || false,
1198
- preferOfficialSubdivisions: config.preferOfficialSubdivisions || false,
1199
- subdivisionRomanizationPreference: config.subdivisionRomanizationPreference,
1200
- preferLocalVariant: config.preferLocalVariant || false,
1201
- forcedLanguage: config.forcedLanguage,
1202
- defaultLanguage: config.defaultLanguage,
1203
- subdivisionNameFilter: config.subdivisionNameFilter,
1204
- };
1205
- // Setup countries
1206
- await setupCountrySelection(apiKey, config.backendUrl, state, {
1207
- defaultLanguage: config.defaultLanguage,
1208
- forcedLanguage: config.forcedLanguage,
1209
- isoCountryNames: config.isoCountryNames || false,
1210
- followRelated: config.followRelated || false,
1211
- followUpward: config.followUpward || false,
1212
- countryNameFilter: config.countryNameFilter,
1213
- });
1214
- // After countries are set up, update subdivisions for any preselected countries
1215
- const countrySelects = Array.from(document.querySelectorAll('.country-selection'));
1216
- for (const select of countrySelects) {
1217
- if (select.value &&
1218
- select.value !== (select.dataset.defaultValue || '')) {
1219
- await updateSubdivisions(select, apiKey, config.backendUrl, state, subdivisionConfig);
1220
- }
1221
- }
1222
- NS.initialized = true;
1223
- return true;
1224
- })();
1225
- return NS.initPromise;
1226
- }
1227
- /**
1228
- * Get configuration from options or script URL parameters
1229
- */
1230
- function getConfigFromOptionsOrScript(options) {
1231
- // Check for global config first (for bundled widgets that need config before auto-init)
1232
- const globalConfig = typeof window !== 'undefined' && window.CountriesDBConfig
1233
- ? window.CountriesDBConfig
1234
- : null;
1235
- // Try to get config from script URL (for backward compatibility with widget.blade.php)
1236
- let scriptUrl = null;
1237
- try {
1238
- let loaderScript = null;
1239
- // First try document.currentScript (works during script execution)
1240
- // But only if it matches the widget pattern (not a bundled file)
1241
- if (document.currentScript && document.currentScript instanceof HTMLScriptElement) {
1242
- const src = document.currentScript.src;
1243
- if (src && (src.includes('@countriesdb/widget') || src.includes('widget/dist/index.js'))) {
1244
- loaderScript = document.currentScript;
1245
- }
1246
- }
1247
- // If currentScript didn't match, search for widget script
1248
- if (!loaderScript) {
1249
- const scripts = Array.from(document.getElementsByTagName('script'));
1250
- loaderScript = scripts.find((s) => s.src && (s.src.includes('@countriesdb/widget') ||
1251
- s.src.includes('widget/dist/index.js'))) || null;
1252
- }
1253
- if (loaderScript && loaderScript.src) {
1254
- scriptUrl = new URL(loaderScript.src);
1255
- }
1256
- }
1257
- catch {
1258
- // Ignore errors
1259
- }
1260
- const config = {
1261
- // Priority: options > globalConfig > scriptUrl params > defaults
1262
- publicKey: options.publicKey ?? globalConfig?.publicKey ?? scriptUrl?.searchParams.get('public_key') ?? '',
1263
- backendUrl: options.backendUrl ?? globalConfig?.backendUrl ?? scriptUrl?.searchParams.get('backend_url') ?? getBackendUrlFromScript(),
1264
- defaultLanguage: options.defaultLanguage ?? globalConfig?.defaultLanguage ?? scriptUrl?.searchParams.get('default_language') ?? undefined,
1265
- forcedLanguage: options.forcedLanguage ?? globalConfig?.forcedLanguage ?? scriptUrl?.searchParams.get('forced_language') ?? undefined,
1266
- showSubdivisionType: options.showSubdivisionType !== undefined
1267
- ? options.showSubdivisionType
1268
- : globalConfig?.showSubdivisionType !== undefined
1269
- ? globalConfig.showSubdivisionType
1270
- : parseBoolean(scriptUrl?.searchParams.get('show_subdivision_type') ?? '1'),
1271
- followRelated: options.followRelated !== undefined
1272
- ? options.followRelated
1273
- : globalConfig?.followRelated !== undefined
1274
- ? globalConfig.followRelated
1275
- : parseBoolean(scriptUrl?.searchParams.get('follow_related') ?? 'false'),
1276
- followUpward: options.followUpward !== undefined
1277
- ? options.followUpward
1278
- : globalConfig?.followUpward !== undefined
1279
- ? globalConfig.followUpward
1280
- : parseBoolean(scriptUrl?.searchParams.get('follow_upward') ?? 'false'),
1281
- allowParentSelection: options.allowParentSelection !== undefined
1282
- ? options.allowParentSelection
1283
- : globalConfig?.allowParentSelection !== undefined
1284
- ? globalConfig.allowParentSelection
1285
- : parseBoolean(scriptUrl?.searchParams.get('allow_parent_selection') ?? 'false'),
1286
- isoCountryNames: options.isoCountryNames !== undefined
1287
- ? options.isoCountryNames
1288
- : globalConfig?.isoCountryNames !== undefined
1289
- ? globalConfig.isoCountryNames
1290
- : parseBoolean(scriptUrl?.searchParams.get('iso_country_names') ?? 'false'),
1291
- subdivisionRomanizationPreference: options.subdivisionRomanizationPreference ||
1292
- globalConfig?.subdivisionRomanizationPreference ||
1293
- scriptUrl?.searchParams.get('subdivision_romanization_preference') ||
1294
- undefined,
1295
- preferLocalVariant: options.preferLocalVariant !== undefined
1296
- ? options.preferLocalVariant
1297
- : globalConfig?.preferLocalVariant !== undefined
1298
- ? globalConfig.preferLocalVariant
1299
- : parseBoolean(scriptUrl?.searchParams.get('prefer_local_variant') ?? 'false'),
1300
- preferOfficialSubdivisions: options.preferOfficialSubdivisions !== undefined
1301
- ? options.preferOfficialSubdivisions
1302
- : globalConfig?.preferOfficialSubdivisions !== undefined
1303
- ? globalConfig.preferOfficialSubdivisions
1304
- : parseBoolean(scriptUrl?.searchParams.get('prefer_official') ?? 'false'),
1305
- countryNameFilter: options.countryNameFilter ?? globalConfig?.countryNameFilter,
1306
- subdivisionNameFilter: options.subdivisionNameFilter ?? globalConfig?.subdivisionNameFilter,
1307
- autoInit: options.autoInit !== undefined
1308
- ? options.autoInit
1309
- : globalConfig?.autoInit !== undefined
1310
- ? globalConfig.autoInit
1311
- : parseBoolean(scriptUrl?.searchParams.get('auto_init') ?? 'true'),
1312
- };
1313
- // Resolve filter functions from global scope if specified by name
1314
- if (scriptUrl) {
1315
- const countryNameFilterName = scriptUrl.searchParams.get('countryNameFilter');
1316
- if (countryNameFilterName && typeof window !== 'undefined') {
1317
- const filter = window[countryNameFilterName];
1318
- if (typeof filter === 'function') {
1319
- config.countryNameFilter = filter;
1320
- }
1321
- }
1322
- const subdivisionNameFilterName = scriptUrl.searchParams.get('subdivisionNameFilter');
1323
- if (subdivisionNameFilterName && typeof window !== 'undefined') {
1324
- const filter = window[subdivisionNameFilterName];
1325
- if (typeof filter === 'function') {
1326
- config.subdivisionNameFilter = filter;
1327
- }
1328
- }
1329
- }
1330
- return config;
1331
- }
1332
- /**
1333
- * Get backend URL from script tag or use default
1334
- */
1335
- function getBackendUrlFromScript() {
1336
- try {
1337
- let loaderScript = null;
1338
- // First try document.currentScript (works during script execution)
1339
- // But only if it matches the widget pattern (not a bundled file)
1340
- if (document.currentScript && document.currentScript instanceof HTMLScriptElement) {
1341
- const src = document.currentScript.src;
1342
- if (src && (src.includes('@countriesdb/widget') || src.includes('widget/dist/index.js'))) {
1343
- loaderScript = document.currentScript;
1344
- }
1345
- }
1346
- // If currentScript didn't match, search for widget script
1347
- if (!loaderScript) {
1348
- // Only consider script tags that loaded the widget bundle
1349
- const scripts = Array.from(document.getElementsByTagName('script'));
1350
- loaderScript = scripts.find((s) => s.src && (s.src.includes('@countriesdb/widget') ||
1351
- s.src.includes('widget/dist/index.js'))) || null;
1352
- }
1353
- if (loaderScript && loaderScript.src) {
1354
- const scriptUrl = new URL(loaderScript.src);
1355
- const hostname = scriptUrl.hostname;
1356
- // List of known CDN hostnames - if script is from a CDN, use default API
1357
- const cdnHostnames = [
1358
- 'unpkg.com',
1359
- 'cdn.jsdelivr.net',
1360
- 'cdnjs.cloudflare.com',
1361
- 'jsdelivr.com',
1362
- 'cdn.jsdelivr.com',
1363
- ];
1364
- // If script is from a CDN, don't use its origin as backend URL
1365
- if (cdnHostnames.some(cdn => hostname.includes(cdn))) {
1366
- return 'https://api.countriesdb.com';
1367
- }
1368
- // Only use script origin if it's from the same domain (self-hosted widget)
1369
- // This allows self-hosting the widget on the same domain as the API
1370
- const scheme = scriptUrl.protocol;
1371
- const host = scriptUrl.hostname;
1372
- const port = scriptUrl.port ? `:${scriptUrl.port}` : '';
1373
- return `${scheme}//${host}${port}`;
1374
- }
1375
- }
1376
- catch {
1377
- // Ignore errors
1378
- }
1379
- // Default to API domain (never use embedding site's domain)
1380
- return 'https://api.countriesdb.com';
1381
- }
1382
- /**
1383
- * Fix mis-classed elements (e.g., country-selection with subdivision-selection class)
1384
- * This should happen early, before API calls, as it's just DOM cleanup
1385
- */
1386
- function fixMisClassedElements() {
1387
- const countrySelects = Array.from(document.querySelectorAll('.country-selection'));
1388
- for (const select of countrySelects) {
1389
- // Avoid conflict if mis-classed
1390
- if (select.classList.contains('subdivision-selection')) {
1391
- select.classList.remove('subdivision-selection');
1392
- select.classList.add('subdivision-selection-removed');
1393
- }
1394
- }
1395
- }
1396
- /**
1397
- * Parse boolean from string value
1398
- */
1399
- function parseBoolean(value) {
1400
- if (value === null || value === undefined) {
1401
- return false;
1402
- }
1403
- const lowered = String(value).trim().toLowerCase();
1404
- return !(lowered === '0' || lowered === 'false');
1405
- }
1406
- /**
1407
- * Show error when both follow_related and follow_upward are enabled
1408
- */
1409
- function showParamConflictError() {
1410
- const errorMessage = 'Error: Cannot enable both follow_related and follow_upward';
1411
- const countrySelects = Array.from(document.querySelectorAll('.country-selection'));
1412
- for (const select of countrySelects) {
1413
- select.innerHTML = `<option value="${select.dataset.defaultValue ?? ''}" disabled>${errorMessage}</option>`;
1414
- }
1415
- const subdivisionSelects = Array.from(document.querySelectorAll('.subdivision-selection'));
1416
- for (const select of subdivisionSelects) {
1417
- select.innerHTML = `<option value="${select.dataset.defaultValue ?? ''}" disabled>${errorMessage}</option>`;
1418
- }
1419
- }
1420
- // Expose public loader
1421
- if (typeof window !== 'undefined') {
1422
- window.CountriesWidgetLoad = CountriesWidgetLoad;
1423
- // Auto-init if script URL has auto_init=true (or not set, default is true)
1424
- // Use a function that waits for DOM to be ready and finds the script tag reliably
1425
- (function checkAutoInit() {
1426
- // Wait for DOM to be ready
1427
- if (document.readyState === 'loading') {
1428
- document.addEventListener('DOMContentLoaded', checkAutoInit, { once: true });
1429
- return;
1430
- }
1431
- // Find the script tag that loaded this widget
1432
- let loaderScript = null;
1433
- // First try document.currentScript (works during script execution)
1434
- // But only if it matches the widget pattern (not a bundled file)
1435
- if (document.currentScript && document.currentScript instanceof HTMLScriptElement) {
1436
- const src = document.currentScript.src;
1437
- if (src && (src.includes('@countriesdb/widget') || src.includes('widget/dist/index.js'))) {
1438
- loaderScript = document.currentScript;
1439
- }
1440
- }
1441
- // If currentScript didn't match, search for widget script
1442
- if (!loaderScript) {
1443
- const scripts = Array.from(document.getElementsByTagName('script'));
1444
- loaderScript = scripts.find((s) => s.src && (s.src.includes('@countriesdb/widget') ||
1445
- s.src.includes('widget/dist/index.js'))) || null;
1446
- }
1447
- // Default to auto-init = true (only disable if explicitly set to false)
1448
- let shouldAutoInit = true;
1449
- const globalConfig = typeof window !== 'undefined'
1450
- ? window.CountriesDBConfig || null
1451
- : null;
1452
- if (globalConfig && typeof globalConfig.autoInit !== 'undefined') {
1453
- shouldAutoInit = !!globalConfig.autoInit;
1454
- }
1455
- else if (loaderScript && loaderScript.src) {
1456
- try {
1457
- const scriptUrl = new URL(loaderScript.src);
1458
- const autoInit = scriptUrl.searchParams.get('auto_init');
1459
- // Only disable if explicitly set to false/0
1460
- shouldAutoInit = autoInit === null || autoInit === 'true' || autoInit === '1';
1461
- }
1462
- catch {
1463
- // If URL parsing fails, default to true (auto-init enabled)
1464
- shouldAutoInit = true;
1465
- }
1466
- }
1467
- // Auto-init by default (unless explicitly disabled)
1468
- if (shouldAutoInit) {
1469
- // Use setTimeout to ensure script tag is fully processed
1470
- setTimeout(() => {
1471
- CountriesWidgetLoad().catch(console.error);
1472
- }, 0);
1473
- }
1474
- })();
1475
- }
1476
-
1477
- return CountriesWidgetLoad;
1478
-
1479
- }));
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).CountriesWidget=t()}(this,function(){"use strict";class e{static async fetchCountries(e){const{apiKey:t,backendUrl:n,shouldUseGeoIP:i=!0,isoCountryNames:a=!1,languageHeaders:o={}}=e,s=`${n}/api/countries`,r=[];i||r.push("no_geoip=1"),a&&r.push("country_name_source=iso");const l=r.length?`${s}?${r.join("&")}`:s;return this.fetchFromApi(l,t,o)}static async fetchSubdivisions(e){const{apiKey:t,backendUrl:n,countryCode:i,shouldUseGeoIP:a=!0,preferOfficial:o=!1,subdivisionRomanizationPreference:s,preferLocalVariant:r=!1,languageHeaders:l={}}=e;if(!i)return{data:[],language:null};const d=`${n}/api/countries/${i}/subdivisions`,u=[];a||u.push("no_geoip=1"),s&&u.push(`subdivision_romanization_preference=${encodeURIComponent(s)}`),r&&u.push("prefer_local_variant=1"),o&&u.push("prefer_official=1");const c=u.length?`${d}?${u.join("&")}`:d;return this.fetchFromApi(c,t,l)}static async fetchFromApi(e,t,n){try{const i=await fetch(e,{headers:{...n,"X-API-KEY":t,"Content-Type":"application/json"}});if(!i.ok){let e=`HTTP Error: ${i.status}`;try{const t=await i.json();t.error&&(e=t.error)}catch{}throw new Error(e)}const a=await i.json();return{data:a,language:i.headers.get("X-Selected-Language")||null}}catch(t){throw console.error(`Failed to fetch data from ${e}:`,t),t}}static getLanguageHeaders(e,t){const n={};return e&&(n["X-Forced-Language"]=e),t&&(n["X-Default-Language"]=t),"undefined"!=typeof navigator&&navigator.language?n["Accept-Language"]=navigator.language:n["Accept-Language"]="en",n}}function t(e,n,i,a,o,s,r){let l="";const d=e.map(e=>{let t=e[a]||e.name;const n=e.code?e.code.split("-")[0]:null;if(r&&"function"==typeof r){const i=r(e.code,t,o,n,e);if(!1===i)return null;null!=i&&(t=i)}return{...e,_displayName:t}}).filter(e=>null!==e),u=r&&"function"==typeof r&&o?o:void 0;d.sort((e,t)=>u?e._displayName.localeCompare(t._displayName,u,{sensitivity:"accent",ignorePunctuation:!1}):e._displayName.localeCompare(t._displayName));const c=n>0?i[n]??"&nbsp;".repeat(2*n):"",f=`subdivision-level-${n}`;for(const e of d){const d=e.children&&e.children.length>0,u=`${c}${e._displayName}`;d?(l+=s?`<option value="${e.code}" class="${f}">${u}</option>`:`<option disabled value="${e.code}" class="${f}">${u}</option>`,l+=t(e.children,n+1,i,a,o,s,r)):l+=`<option value="${e.code}" class="${f}">${u}</option>`}return l}function n(e,t="Not Applicable",n=!1){if(e.hasAttribute("multiple"))e.innerHTML="",void 0!==e.dataset.label&&delete e.dataset.label,void 0!==e.dataset.defaultValue&&delete e.dataset.defaultValue;else{const n=e.dataset.label,i=e.dataset.defaultValue,a=n||t,o=i??"";e.innerHTML=`<option value="${o}">${a}</option>`,void 0!==n&&(e.dataset.label=n),void 0!==i&&(e.dataset.defaultValue=i)}n&&(e.disabled=!0)}function i(e,t,n,i){let a=function(e,t,n){return t&&"function"==typeof t?e.map(e=>{const i=t(e.iso_alpha_2,e.name,n,e);if(!1===i)return null;const a=null!=i?i:e.name;return{...e,_displayName:a}}).filter(e=>null!==e):e.map(e=>({...e,_displayName:e.name}))}(t,i,n);i&&(a=function(e,t){const n=t;return[...e].sort((e,t)=>e._displayName.localeCompare(t._displayName,n,{sensitivity:"accent",ignorePunctuation:!1}))}(a,n));if(e.hasAttribute("multiple")||e.multiple)e.innerHTML="";else{const t=e.querySelector('option[value=""]')||(e.dataset.defaultValue?e.querySelector(`option[value="${e.dataset.defaultValue}"]`):null),n=e.dataset.defaultValue??"";e.innerHTML=t?t.outerHTML:`<option value="${n}">${e.dataset.label||"&mdash;"}</option>`}a.forEach(t=>{const n=document.createElement("option");n.value=t.iso_alpha_2,n.textContent=t._displayName,e.appendChild(n)})}function a(e,n,i,a,o,s){const r=function(e){const t={},n=[];return e.forEach(e=>{t[e.id]={...e,children:[]}}),e.forEach(e=>{const i=t[e.id];e.parent_id&&t[e.parent_id]?t[e.parent_id].children.push(i):n.push(i)}),n}(e),l={};for(let e=1;e<=10;e++){const t=`nested${e}Prefix`,i=n.dataset[t];if(void 0===i)break;l[`nested${e}Prefix`]=i}const d=function(e){const t={};for(let n=1;n<=10;n++){const i=e[`nested${n}Prefix`];if(void 0===i)break;t[n]=i}return t}(l),u=a?"full_name":"name";let c="";if(!n.hasAttribute("multiple")){const e=n.dataset.label||"&mdash;";c=`<option value="${n.dataset.defaultValue??""}">${e}</option>`}return c+=t(r,0,d,u,i,o,s),c}function o(e,t){const n=e.dataset._widgetTempPreselect,i=e.getAttribute("data-preselected")||e.dataset.preselected,a=null!=n&&""!==String(n).trim()?n:i;if(!a||"string"==typeof a&&""===a.trim())return;const o=String(a);if(e.hasAttribute("multiple")){const t=o.split(",").map(e=>e.trim()).filter(e=>""!==e);Array.from(e.options).forEach(e=>{e.selected=t.includes(e.value)})}else e.value=o;void 0!==e.dataset._widgetTempPreselect&&delete e.dataset._widgetTempPreselect}function s(e,t,n=!1){const i=t instanceof Error?t.message:t,a=e.dataset.defaultValue??"",o=i.startsWith("Error: ")?i:`Error: ${i}`;n?e.innerHTML=`<option value="${a}" disabled>${o}</option>`:e.innerHTML+=`<option value="${a}" disabled>${o}</option>`}function r(e,t={}){let n=[];n=e.multiple?Array.from(e.selectedOptions||[]).map(e=>e.value).filter(e=>""!==e):e.value?[e.value]:[];const i=new CustomEvent("countriesWidget:update",{bubbles:!0,detail:{value:e.value||"",selectedValues:n,name:e.dataset.name||null,country:e.dataset.country||null,isSubdivision:e.classList.contains("subdivision-selection"),...t}});e.dispatchEvent(i)}function l(e,t={}){const n=e.multiple?Array.from(e.selectedOptions||[]).map(e=>e.value).filter(e=>""!==e):e.value?[e.value]:[],i=new CustomEvent("countriesWidget:ready",{bubbles:!0,detail:{value:e.value||"",selectedValues:n,name:e.dataset.name||null,country:e.dataset.country||null,isSubdivision:e.classList.contains("subdivision-selection"),type:e.classList.contains("subdivision-selection")?"subdivision":"country",phase:"initial",...t}});e.dispatchEvent(i)}function d(e){return!0===e.isWidgetInitiated}async function u(e,t,n,i,a,o,s,l){if(!a&&!o)return;const d=document.querySelector(`.country-selection[data-name="${e.dataset.country}"]`);if(!d||d.multiple)return;const u=i.subdivisionsMap.get(e);if(!u)return;if(o&&e.multiple)return;const c=e.value;if(!c)return;const f=u.find(e=>e.code===c);if(f){if(a&&f.related_country_code){const e=f.related_country_code,n=f.related_subdivision_code,i=document.querySelector(`.subdivision-selection[data-country="${d.dataset.name}"]`);i&&n&&(i.dataset._widgetTempPreselect=n),d.value!==e?(d.value=e,r(d,{type:"country",reason:"regular"}),l?await l(d,t):i&&await s(i,t,e)):i&&n&&await s(i,t,e)}if(o&&f.is_subdivision_of){const e=f.is_subdivision_of.parent_country_code,n=f.is_subdivision_of.subdivision_code;if(n){const e=document.querySelector(`.subdivision-selection[data-country="${d.dataset.name}"]`);e&&!e.multiple&&(e.dataset._widgetTempPreselect=n)}if(d.value!==e)if(d.value=e,r(d,{type:"country",reason:"regular"}),l)await l(d,t);else{const n=document.querySelector(`.subdivision-selection[data-country="${d.dataset.name}"]`);n&&await s(n,t,e)}else if(n){const n=document.querySelector(`.subdivision-selection[data-country="${d.dataset.name}"]`);n&&!n.multiple&&await s(n,t,e)}}}}async function c(e,t,n,i,a,o){if(!a)return;const s=document.querySelector(`.country-selection[data-name="${e.dataset.country}"]`);if(!s||s.multiple)return;const l=i.subdivisionsMap.get(e);if(!l)return;const d=e.multiple?e.selectedOptions[0]?.value||null:e.value;if(!d)return;const u=l.find(e=>e.code===d);if(!u||!u.related_country_code)return;const c=u.related_country_code,f=u.related_subdivision_code,p=document.querySelector(`.subdivision-selection[data-country="${s.dataset.name}"]`);p&&f&&(p.dataset.preselected=f),s.value!==c?(s.value=c,r(s,{type:"country",reason:"regular"}),p&&await o(p,t,c)):p&&f&&await o(p,t,c)}async function f(e,t,n,i,a,o){if(!a)return;if(e.multiple)return;const s=document.querySelector(`.country-selection[data-name="${e.dataset.country}"]`);if(!s||s.multiple)return;const l=i.subdivisionsMap.get(e);if(!l)return;const d=e.value;if(!d)return;const u=l.find(e=>e.code===d);if(!u||!u.is_subdivision_of)return;const c=u.is_subdivision_of.parent_country_code,f=u.is_subdivision_of.subdivision_code;if(f){const e=document.querySelector(`.subdivision-selection[data-country="${s.dataset.name}"]`);e&&(e.dataset.preselected=f)}if(s.value!==c){s.value=c,r(s,{type:"country",reason:"follow"});const e=document.querySelector(`.subdivision-selection[data-country="${s.dataset.name}"]`);e&&await o(e,t,c)}else if(f){const e=document.querySelector(`.subdivision-selection[data-country="${s.dataset.name}"]`);e&&await o(e,t,c)}}async function p(e,t,n,i,a,o){if(!a||e.multiple)return;const s=i.countriesMap.get(e);if(!s)return;const l=e.value;if(!l)return;const d=s.find(e=>e.iso_alpha_2===l);if(!d||!d.is_subdivision_of)return;const u=d.is_subdivision_of.parent_country_code,c=d.is_subdivision_of.subdivision_code;if(e.value!==u){if(c){const t=Array.from(document.querySelectorAll(`.subdivision-selection[data-country="${e.dataset.name}"]`));for(const e of t)e.multiple||(e.dataset.preselected=c)}e.value=u,r(e,{type:"country",reason:"regular"});const n=Array.from(document.querySelectorAll(`.subdivision-selection[data-country="${e.dataset.name}"]`));for(const e of n)await o(e,t,u)}else if(c){const n=Array.from(document.querySelectorAll(`.subdivision-selection[data-country="${e.dataset.name}"]`));for(const e of n)e.multiple||await o(e,t,u)}}async function m(t,i,d,c,f,p){const v=t.getAttribute("data-preselected")||t.dataset.preselected,g=c.subdivisionsMap.has(t);n(t,"&mdash;",!0),c.isInitializing.add(t);let w=p;if(w||(w=t.dataset.countryCode||""),w){let n=!1;try{const s=null==v,l=!!t.hasAttribute("data-prefer-official")||f.preferOfficialSubdivisions,p=e.getLanguageHeaders(f.forcedLanguage,f.defaultLanguage),g=await e.fetchSubdivisions({apiKey:i,backendUrl:d,countryCode:w,shouldUseGeoIP:s,preferOfficial:l,subdivisionRomanizationPreference:f.subdivisionRomanizationPreference,preferLocalVariant:f.preferLocalVariant,languageHeaders:p}),b=g.data,h=g.language||"en";if(c.subdivisionsMap.set(t,b),c.subdivisionsLanguageMap.set(t,h),b.length>0){t.disabled=!1;const e=t.hasAttribute("multiple"),l=e?void 0:t.dataset.label,p=e?void 0:t.dataset.defaultValue,v=t.value,g=t.dataset.defaultValue||"",w=v&&v!==g&&""!==v.trim();t.innerHTML=a(b,t,h,f.showSubdivisionType,f.allowParentSelection,f.subdivisionNameFilter),e||(void 0!==l&&(t.dataset.label=l),void 0!==p&&(t.dataset.defaultValue=p));let _=!1;if(w&&!e){Array.from(t.options).some(e=>e.value===v)&&(t.value=v,_=!0)}if(!_){if(void 0!==(t.getAttribute("data-preselected")||t.dataset.preselected||t.dataset._widgetTempPreselect))o(t),r(t,{type:"subdivision",reason:"preselected"}),n=!0,await u(t,i,0,c,f.followRelated,f.followUpward,(e,t,n)=>m(e,t,d,c,f,n),(e,t)=>y(e,t,d,c,f));else if(s){const e=b.find(e=>e.preselected);if(e){if(t.hasAttribute("multiple")){const n=Array.from(t.options).find(t=>t.value===e.code);n&&(n.selected=!0)}else t.value=e.code;r(t,{type:"subdivision",reason:"geoip"}),n=!0,await u(t,i,0,c,f.followRelated,f.followUpward,(e,t,n)=>m(e,t,d,c,f,n),(e,t)=>y(e,t,d,c,f))}}}}else t.disabled=!0}catch(e){console.error("Failed to fetch subdivisions:",e),s(t,e)}finally{c.isInitializing.delete(t),l(t,{type:"subdivision",phase:g?"reload":"initial"}),g&&!n&&r(t,{type:"subdivision",reason:"reload"})}}else if(!t.dataset.country||!document.querySelector(`.country-selection[data-name="${t.dataset.country}"]`)){const e=t.dataset.defaultValue??"";t.innerHTML+=`<option value="${e}" disabled>Error: No country select present</option>`}}function v(e,t){const n="undefined"!=typeof window&&window.CountriesDBConfig?window.CountriesDBConfig:null;return{followRelated:n?.followRelated||!1,followUpward:n?.followUpward||!1,showSubdivisionType:!1!==n?.showSubdivisionType,allowParentSelection:n?.allowParentSelection||!1,preferOfficialSubdivisions:n?.preferOfficialSubdivisions||!1,subdivisionRomanizationPreference:n?.subdivisionRomanizationPreference,preferLocalVariant:n?.preferLocalVariant||!1,forcedLanguage:n?.forcedLanguage,defaultLanguage:n?.defaultLanguage,subdivisionNameFilter:n?.subdivisionNameFilter}}async function y(e,t,n,i,a){if(e.hasAttribute("multiple"))return;const o=e.value,s=Array.from(document.querySelectorAll(`.subdivision-selection[data-country="${e.dataset.name}"]`));for(const e of s)await m(e,t,n,i,a,o)}const g="__CountriesWidgetNS__";async function w(t={}){window[g]||(window[g]={initialized:!1,initPromise:null,version:0,eventHandlers:new WeakMap});const a=window[g];a.eventHandlers||(a.eventHandlers=new WeakMap);return!0===t.reload&&(a.initialized=!1,a.initPromise=null),a.initPromise=(async()=>{"loading"===document.readyState&&await new Promise(e=>{document.addEventListener("DOMContentLoaded",()=>e(),{once:!0})});const u=function(e){const t="undefined"!=typeof window&&window.CountriesDBConfig?window.CountriesDBConfig:null;let n=null;try{let e=null;if(document.currentScript&&document.currentScript instanceof HTMLScriptElement){const t=document.currentScript.src;t&&(t.includes("@countriesdb/widget")||t.includes("widget/dist/index.js"))&&(e=document.currentScript)}if(!e){e=Array.from(document.getElementsByTagName("script")).find(e=>e.src&&(e.src.includes("@countriesdb/widget")||e.src.includes("widget/dist/index.js")))||null}e&&e.src&&(n=new URL(e.src))}catch{}const i={publicKey:e.publicKey??t?.publicKey??n?.searchParams.get("public_key")??"",backendUrl:e.backendUrl??t?.backendUrl??n?.searchParams.get("backend_url")??"https://api.countriesdb.com",defaultLanguage:e.defaultLanguage??t?.defaultLanguage??n?.searchParams.get("default_language")??void 0,forcedLanguage:e.forcedLanguage??t?.forcedLanguage??n?.searchParams.get("forced_language")??void 0,showSubdivisionType:void 0!==e.showSubdivisionType?e.showSubdivisionType:void 0!==t?.showSubdivisionType?t.showSubdivisionType:b(n?.searchParams.get("show_subdivision_type")??"1"),followRelated:void 0!==e.followRelated?e.followRelated:void 0!==t?.followRelated?t.followRelated:b(n?.searchParams.get("follow_related")??"false"),followUpward:void 0!==e.followUpward?e.followUpward:void 0!==t?.followUpward?t.followUpward:b(n?.searchParams.get("follow_upward")??"false"),allowParentSelection:void 0!==e.allowParentSelection?e.allowParentSelection:void 0!==t?.allowParentSelection?t.allowParentSelection:b(n?.searchParams.get("allow_parent_selection")??"false"),isoCountryNames:void 0!==e.isoCountryNames?e.isoCountryNames:void 0!==t?.isoCountryNames?t.isoCountryNames:b(n?.searchParams.get("iso_country_names")??"false"),subdivisionRomanizationPreference:e.subdivisionRomanizationPreference||t?.subdivisionRomanizationPreference||n?.searchParams.get("subdivision_romanization_preference")||void 0,preferLocalVariant:void 0!==e.preferLocalVariant?e.preferLocalVariant:void 0!==t?.preferLocalVariant?t.preferLocalVariant:b(n?.searchParams.get("prefer_local_variant")??"false"),preferOfficialSubdivisions:void 0!==e.preferOfficialSubdivisions?e.preferOfficialSubdivisions:void 0!==t?.preferOfficialSubdivisions?t.preferOfficialSubdivisions:b(n?.searchParams.get("prefer_official")??"false"),countryNameFilter:e.countryNameFilter??t?.countryNameFilter,subdivisionNameFilter:e.subdivisionNameFilter??t?.subdivisionNameFilter,autoInit:void 0!==e.autoInit?e.autoInit:void 0!==t?.autoInit?t.autoInit:b(n?.searchParams.get("auto_init")??"true")};if(n){const e=n.searchParams.get("countryNameFilter");if(e&&"undefined"!=typeof window){const t=window[e];"function"==typeof t&&(i.countryNameFilter=t)}const t=n.searchParams.get("subdivisionNameFilter");if(t&&"undefined"!=typeof window){const e=window[t];"function"==typeof e&&(i.subdivisionNameFilter=e)}}return i}(t);if(function(){const e=Array.from(document.querySelectorAll(".country-selection"));for(const t of e)t.classList.contains("subdivision-selection")&&(t.classList.remove("subdivision-selection"),t.classList.add("subdivision-selection-removed"))}(),u.followRelated&&u.followUpward)return function(){const e="Error: Cannot enable both follow_related and follow_upward",t=Array.from(document.querySelectorAll(".country-selection"));for(const n of t)n.innerHTML=`<option value="${n.dataset.defaultValue??""}" disabled>${e}</option>`;const n=Array.from(document.querySelectorAll(".subdivision-selection"));for(const t of n)t.innerHTML=`<option value="${t.dataset.defaultValue??""}" disabled>${e}</option>`}(),!1;const g={countriesMap:new WeakMap,subdivisionsMap:new WeakMap,subdivisionsLanguageMap:new WeakMap,isInitializing:new Set,eventHandlers:a.eventHandlers},w=u.publicKey||"";await async function(e,t,i,a){const o=Array.from(document.querySelectorAll(".subdivision-selection"));for(const s of o){n(s,"&mdash;",!0);const o=s.dataset.country,l=o?document.querySelector(`.country-selection[data-name="${o}"]`):null;if(l&&l.hasAttribute("multiple")){const e=s.dataset.defaultValue??"";s.innerHTML=`<option value="${e}" disabled>Error: Cannot link to multi-select country. Use data-country-code instead.</option>`;continue}if(!o||!l)if(s.hasAttribute("data-country-code")&&s.dataset.countryCode)await m(s,e,t,i,a,s.dataset.countryCode);else{const e=s.dataset.defaultValue??"";s.innerHTML+=`<option value="${e}" disabled>Error: No country select present</option>`}const u=i.eventHandlers.get(s);u&&(u.update&&s.removeEventListener("change",u.update),u.followRelated&&s.removeEventListener("change",u.followRelated),u.followUpward&&s.removeEventListener("change",u.followUpward));const p={update:e=>{d(e)||r(s,{type:"subdivision",reason:"regular"})}};s.addEventListener("change",p.update),p.followRelated=async n=>{d(n)||await c(s,e,0,i,a.followRelated,(e,n,o)=>m(e,n,t,i,a,o))},s.addEventListener("change",p.followRelated),p.followUpward=async n=>{d(n)||await f(s,e,0,i,a.followUpward,(e,n,o)=>m(e,n,t,i,a,o))},s.addEventListener("change",p.followUpward),i.eventHandlers.set(s,p)}}(w,u.backendUrl,g,{followRelated:u.followRelated||!1,followUpward:u.followUpward||!1,showSubdivisionType:!1!==u.showSubdivisionType,allowParentSelection:u.allowParentSelection||!1,preferOfficialSubdivisions:u.preferOfficialSubdivisions||!1,subdivisionRomanizationPreference:u.subdivisionRomanizationPreference,preferLocalVariant:u.preferLocalVariant||!1,subdivisionNameFilter:u.subdivisionNameFilter});const h={followRelated:u.followRelated||!1,followUpward:u.followUpward||!1,showSubdivisionType:!1!==u.showSubdivisionType,allowParentSelection:u.allowParentSelection||!1,preferOfficialSubdivisions:u.preferOfficialSubdivisions||!1,subdivisionRomanizationPreference:u.subdivisionRomanizationPreference,preferLocalVariant:u.preferLocalVariant||!1,forcedLanguage:u.forcedLanguage,defaultLanguage:u.defaultLanguage,subdivisionNameFilter:u.subdivisionNameFilter};await async function(t,a,u,c){const f=Array.from(document.querySelectorAll(".country-selection")),g={};for(const w of f){const f=w.dataset.name;if(f&&g[f]){w.removeAttribute("data-name"),n(w,"&mdash;");const e=w.dataset.defaultValue??"";w.innerHTML+=`<option value="${e}" disabled>Error: Duplicate field</option>`;continue}f&&(g[f]=!0),n(w,"&mdash;"),u.isInitializing.add(w);let b=!1,h=!1;try{const n=null==(w.getAttribute("data-preselected")||w.dataset.preselected),s=e.getLanguageHeaders(c.forcedLanguage,c.defaultLanguage),l=await e.fetchCountries({apiKey:t,backendUrl:a,shouldUseGeoIP:n,isoCountryNames:c.isoCountryNames,languageHeaders:s}),f=l.data,g=l.language||"en";if(u.countriesMap.set(w,f),i(w,f,g,c.countryNameFilter),void 0!==(w.getAttribute("data-preselected")||w.dataset.preselected||w.dataset._widgetTempPreselect)&&(o(w),r(w,{type:"country",reason:"preselected"}),b=!0,w.value&&(w.value,w.dataset.defaultValue)),n){const e=f.find(e=>e.preselected);e&&(w.value=e.iso_alpha_2,r(w,{type:"country",reason:"geoip"}),b=!0)}!h&&w.value&&w.value!==(w.dataset.defaultValue||"")&&(h=!0);const _=u.eventHandlers.get(w);_?.countryChange&&w.removeEventListener("change",_.countryChange);const L=async e=>{if(d(e))return;r(w,{type:"country",reason:"regular"});const n=v(),i="undefined"!=typeof window&&window.CountriesDBConfig?window.CountriesDBConfig:c;await y(w,t,a,u,n);const o=w.value;if(!o)return;const s=(u.countriesMap.get(w)||[]).find(e=>e.iso_alpha_2===o);if(!s)return;const l=i?.followUpward||!1;l&&!w.multiple&&s.is_subdivision_of&&await p(w,t,0,u,l,(e,t,i)=>m(e,t,a,u,n,i))},S=u.eventHandlers.get(w)||{};S.countryChange=L,u.eventHandlers.set(w,S),w.addEventListener("change",L)}catch(e){if(console.error("Failed to fetch countries:",e),s(w,e),w.dataset.name){const t=Array.from(document.querySelectorAll(`.subdivision-selection[data-country="${w.dataset.name}"]`));for(const i of t)n(i,"&mdash;"),s(i,e)}}finally{u.isInitializing.delete(w),l(w,{type:"country",phase:"initial"}),b||r(w,{type:"country",reason:"regular"})}}}(w,u.backendUrl,g,{defaultLanguage:u.defaultLanguage,forcedLanguage:u.forcedLanguage,isoCountryNames:u.isoCountryNames||!1,followRelated:u.followRelated||!1,followUpward:u.followUpward||!1,countryNameFilter:u.countryNameFilter});const _=Array.from(document.querySelectorAll(".country-selection"));for(const e of _)e.value&&e.value!==(e.dataset.defaultValue||"")&&await y(e,w,u.backendUrl,g,h);return a.initialized=!0,!0})(),a.initPromise}function b(e){if(null==e)return!1;const t=String(e).trim().toLowerCase();return!("0"===t||"false"===t)}return"undefined"!=typeof window&&(window.CountriesWidgetLoad=w,function e(){if("loading"===document.readyState)return void document.addEventListener("DOMContentLoaded",e,{once:!0});let t=null;if(document.currentScript&&document.currentScript instanceof HTMLScriptElement){const e=document.currentScript.src;e&&(e.includes("@countriesdb/widget")||e.includes("widget/dist/index.js"))&&(t=document.currentScript)}if(!t){t=Array.from(document.getElementsByTagName("script")).find(e=>e.src&&(e.src.includes("@countriesdb/widget")||e.src.includes("widget/dist/index.js")))||null}let n=!0;const i="undefined"!=typeof window&&window.CountriesDBConfig||null;if(i&&void 0!==i.autoInit)n=!!i.autoInit;else if(t&&t.src)try{const e=new URL(t.src).searchParams.get("auto_init");n=null===e||"true"===e||"1"===e}catch{n=!0}n&&setTimeout(()=>{w().catch(console.error)},0)}()),w});
1480
2
  //# sourceMappingURL=index.js.map