country-select-pro 6.1.3 → 6.1.4

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
@@ -39,7 +39,7 @@ https://nikmauro.github.io/country-select-pro/docs/
39
39
  Include the script via **jsDelivr**:
40
40
 
41
41
  ```html
42
- <script src="https://cdn.jsdelivr.net/gh/nikmauro/country-select-pro@4.8.2/dist/country-select.min.js"></script>
42
+ <script src="https://cdn.jsdelivr.net/npm/country-select-pro@6.1.4/dist/country-select.min.js"></script>
43
43
  ```
44
44
 
45
45
  ---
@@ -49,7 +49,7 @@ Include the script via **jsDelivr**:
49
49
  Download the repository and include:
50
50
 
51
51
  ```html
52
- <script src="dist/country-select.min.js"></script>
52
+ <script src="dist/country-select.js"></script>
53
53
  ```
54
54
 
55
55
  ---
@@ -128,8 +128,8 @@ country-select-pro
128
128
  │
129
129
  ├─ dist
130
130
  │ ├─ country-select.js
131
- │ └─ country-select.min.js
132
- │
131
+ │ └─ data.json
132
+ ├─ flags
133
133
  ├─ demo
134
134
  ├─ src
135
135
  └─ README.md
@@ -173,8 +173,6 @@ Contributions are welcome.
173
173
  Developed by **Nikolaos Mavroeidis**
174
174
  GitHub: https://github.com/nikmauro
175
175
 
176
- With collaborative assistance from **Gemini (Google AI)**.
177
-
178
176
  ---
179
177
 
180
178
  # 📄 License
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CountrySelect Pro v6.1.3
2
+ * CountrySelect Pro v6.1.4
3
3
  * ---------------------------------------------------------------------------
4
4
  * Features:
5
5
  * - SVG Arrow Down icon
@@ -13,7 +13,7 @@
13
13
 
14
14
  class CountrySelect {
15
15
 
16
-
16
+
17
17
  constructor(element, options = {}) {
18
18
  this.input = element;
19
19
  if (!this.input) return;
@@ -46,10 +46,10 @@ class CountrySelect {
46
46
  this.isOpen = false;
47
47
  this.activeIndex = -1;
48
48
  this.rowHeight = 44;
49
-
49
+
50
50
  // Base URL για το JSON και τα SVG flags
51
51
  this.baseUrl = 'https://cdn.jsdelivr.net/gh/nikmauro/country-select-pro@6.1.3/';
52
-
52
+
53
53
  this._init();
54
54
  }
55
55
 
@@ -186,17 +186,17 @@ class CountrySelect {
186
186
  }
187
187
 
188
188
  _updateActiveElement() {
189
- const options = this.wrapper.querySelectorAll('.cs-option');
190
- options.forEach((el, index) => {
191
- if (index === this.activeIndex) {
192
- el.classList.add('active');
193
- // Bonus UX: Κάνει scroll αν το element είναι εκτός οθόνης
194
- el.scrollIntoView({ block: 'nearest' });
195
- } else {
196
- el.classList.remove('active');
197
- }
198
- });
199
- }
189
+ const options = this.wrapper.querySelectorAll('.cs-option');
190
+ options.forEach((el, index) => {
191
+ if (index === this.activeIndex) {
192
+ el.classList.add('active');
193
+ // Bonus UX: Κάνει scroll αν το element είναι εκτός οθόνης
194
+ el.scrollIntoView({block: 'nearest'});
195
+ } else {
196
+ el.classList.remove('active');
197
+ }
198
+ });
199
+ }
200
200
 
201
201
  _updatePosition() {
202
202
  const rect = this.wrapper.getBoundingClientRect();
@@ -212,35 +212,35 @@ class CountrySelect {
212
212
  }
213
213
 
214
214
  async _loadData() {
215
- try {
216
- const jsonUrl = `${this.baseUrl}dist/data.json`; // Χρήση του baseUrl
217
-
218
- const res = await fetch(jsonUrl);
219
- const json = await res.json();
220
- const data = json?.data?.objects || [];
221
-
222
- this.countries = data.map(c => {
223
- const code = c.codes?.alpha_2 ? c.codes.alpha_2.toUpperCase() : "";
224
- return {
225
- name: c.names?.common || "",
226
- code: code,
227
- // Δημιουργία path για το SVG με βάση το code
228
- flag: code ? `${this.baseUrl}dist/flags/4x3/${code.toLowerCase()}.svg` : "",
229
- phone: (c.calling_codes && c.calling_codes.length > 0) ? `+${c.calling_codes[0]}` : "",
230
- isPreferred: code ? this.preferredCodes.includes(code) : false
231
- };
232
- }).filter(c => c.code !== "")
233
- .sort((a, b) => a.name.localeCompare(b.name));
234
-
235
- this.filteredCountries = [...this.countries];
236
- this._renderOptions();
237
- this._syncInitialValue();
238
- } catch (e) {
239
- console.error("CountrySelect Load Fail:", e);
215
+ try {
216
+ const jsonUrl = `${this.baseUrl}dist/data.json`; // Χρήση του baseUrl
217
+
218
+ const res = await fetch(jsonUrl);
219
+ const json = await res.json();
220
+ const data = json?.data?.objects || [];
221
+
222
+ this.countries = data.map(c => {
223
+ const code = c.codes?.alpha_2 ? c.codes.alpha_2.toUpperCase() : "";
224
+ return {
225
+ name: c.names?.common || "",
226
+ code: code,
227
+ // Δημιουργία path για το SVG με βάση το code
228
+ flag: code ? `${this.baseUrl}dist/flags/4x3/${code.toLowerCase()}.svg` : "",
229
+ phone: (c.calling_codes && c.calling_codes.length > 0) ? `+${c.calling_codes[0]}` : "",
230
+ isPreferred: code ? this.preferredCodes.includes(code) : false
231
+ };
232
+ }).filter(c => c.code !== "")
233
+ .sort((a, b) => a.name.localeCompare(b.name));
234
+
235
+ this.filteredCountries = [...this.countries];
236
+ this._renderOptions();
237
+ this._syncInitialValue();
238
+ } catch (e) {
239
+ console.error("CountrySelect Load Fail:", e);
240
+ }
240
241
  }
241
- }
242
242
 
243
-
243
+
244
244
  _syncInitialValue() {
245
245
  const val = this.input.value;
246
246
  if (!val) {
@@ -253,52 +253,52 @@ class CountrySelect {
253
253
 
254
254
 
255
255
  _parseTemplate(template, country) {
256
- return template
257
- .replace('{img}', `<img src="${country.flag}" class="cs-flag-img" loading="lazy" alt="${country.name}">`)
258
- .replace('{name}', `<span>${country.name}</span>`)
259
- .replace('{code}', `<span>${country.code}</span>`)
260
- .replace('{phone}', `<span>${country.phone}</span>`);
261
- }
262
-
263
- _renderOptions() {
264
- const listContainer = this.wrapper.querySelector('.cs-list');
265
- if (!listContainer) return;
266
- listContainer.innerHTML = '';
267
-
268
- // Έλεγχος αν η λίστα είναι άδεια
269
- if (this.filteredCountries.length === 0) {
270
- const noResults = document.createElement('div');
271
- noResults.className = 'cs-no-results';
272
- noResults.innerHTML = '<span class="cs-no-results-text">Not found</span>';
273
- listContainer.appendChild(noResults);
274
- return; // Σταματάμε εδώ
256
+ return template
257
+ .replace('{img}', `<img src="${country.flag}" class="cs-flag-img" loading="lazy" alt="${country.name}">`)
258
+ .replace('{name}', `<span>${country.name}</span>`)
259
+ .replace('{code}', `<span>${country.code}</span>`)
260
+ .replace('{phone}', `<span>${country.phone}</span>`);
275
261
  }
276
262
 
277
- const searchVal = this.wrapper.querySelector('.cs-search-input')?.value || "";
278
- const preferred = this.filteredCountries.filter(c => c.isPreferred);
279
- const others = this.filteredCountries.filter(c => !c.isPreferred);
263
+ _renderOptions() {
264
+ const listContainer = this.wrapper.querySelector('.cs-list');
265
+ if (!listContainer) return;
266
+ listContainer.innerHTML = '';
267
+
268
+ // Έλεγχος αν η λίστα είναι άδεια
269
+ if (this.filteredCountries.length === 0) {
270
+ const noResults = document.createElement('div');
271
+ noResults.className = 'cs-no-results';
272
+ noResults.innerHTML = '<span class="cs-no-results-text">Not found</span>';
273
+ listContainer.appendChild(noResults);
274
+ return; // Σταματάμε εδώ
275
+ }
280
276
 
281
- const createOption = (country, index) => {
282
- const div = document.createElement('div');
283
- div.className = `cs-option ${index === this.activeIndex ? 'active' : ''}`;
284
- div.innerHTML = this._parseTemplate(this.schema, country);
285
- div.onclick = (e) => {
286
- e.stopPropagation();
287
- this._select(country);
277
+ const searchVal = this.wrapper.querySelector('.cs-search-input')?.value || "";
278
+ const preferred = this.filteredCountries.filter(c => c.isPreferred);
279
+ const others = this.filteredCountries.filter(c => !c.isPreferred);
280
+
281
+ const createOption = (country, index) => {
282
+ const div = document.createElement('div');
283
+ div.className = `cs-option ${index === this.activeIndex ? 'active' : ''}`;
284
+ div.innerHTML = this._parseTemplate(this.schema, country);
285
+ div.onclick = (e) => {
286
+ e.stopPropagation();
287
+ this._select(country);
288
+ };
289
+ return div;
288
290
  };
289
- return div;
290
- };
291
291
 
292
- preferred.forEach((c, i) => listContainer.appendChild(createOption(c, i)));
292
+ preferred.forEach((c, i) => listContainer.appendChild(createOption(c, i)));
293
293
 
294
- if (preferred.length > 0 && others.length > 0 && !searchVal) {
295
- const hr = document.createElement('div');
296
- hr.className = 'cs-divider';
297
- listContainer.appendChild(hr);
298
- }
294
+ if (preferred.length > 0 && others.length > 0 && !searchVal) {
295
+ const hr = document.createElement('div');
296
+ hr.className = 'cs-divider';
297
+ listContainer.appendChild(hr);
298
+ }
299
299
 
300
- others.forEach((c, i) => listContainer.appendChild(createOption(c, preferred.length + i)));
301
- }
300
+ others.forEach((c, i) => listContainer.appendChild(createOption(c, preferred.length + i)));
301
+ }
302
302
 
303
303
  _select(country) {
304
304
  this._updateUI(country);
@@ -326,35 +326,35 @@ class CountrySelect {
326
326
  if (content) content.innerHTML = this._parseTemplate(this.schemaReturn, country);
327
327
  }
328
328
 
329
- _toggle(force) {
330
- if (this.countries.length === 0) return;
331
- this.isOpen = force !== undefined ? force : !this.isOpen;
332
-
333
- if (this.isOpen) {
334
- this._updatePosition();
335
- document.dispatchEvent(new CustomEvent('cs-close-others', {detail: {opener: this.wrapper}}));
329
+ _toggle(force) {
330
+ if (this.countries.length === 0) return;
331
+ this.isOpen = force !== undefined ? force : !this.isOpen;
336
332
 
337
- // ΑΛΛΑΓΗ: Έλεγχος αν η λίστα έχει ήδη στοιχεία
338
- const list = this.wrapper.querySelector('.cs-list');
339
- if (list.children.length === 0) {
340
- this._renderOptions();
341
- }
333
+ if (this.isOpen) {
334
+ this._updatePosition();
335
+ document.dispatchEvent(new CustomEvent('cs-close-others', {detail: {opener: this.wrapper}}));
336
+
337
+ // ΑΛΛΑΓΗ: Έλεγχος αν η λίστα έχει ήδη στοιχεία
338
+ const list = this.wrapper.querySelector('.cs-list');
339
+ if (list.children.length === 0) {
340
+ this._renderOptions();
341
+ }
342
342
 
343
- list.style.maxHeight = `${this.rowLimit * this.rowHeight}px`;
343
+ list.style.maxHeight = `${this.rowLimit * this.rowHeight}px`;
344
344
 
345
- const pref = this.filteredCountries.filter(c => c.isPreferred);
346
- const oth = this.filteredCountries.filter(c => !c.isPreferred);
347
- const fullList = [...pref, ...oth];
348
- this.activeIndex = fullList.findIndex(c => this.input.value.includes(c.code) || this.input.value.includes(c.phone));
345
+ const pref = this.filteredCountries.filter(c => c.isPreferred);
346
+ const oth = this.filteredCountries.filter(c => !c.isPreferred);
347
+ const fullList = [...pref, ...oth];
348
+ this.activeIndex = fullList.findIndex(c => this.input.value.includes(c.code) || this.input.value.includes(c.phone));
349
349
 
350
- // ΑΛΛΑΓΗ: Ενημέρωσε το active element αντί να κάνεις re-render όλη τη λίστα
351
- this._updateActiveElement();
350
+ // ΑΛΛΑΓΗ: Ενημέρωσε το active element αντί να κάνεις re-render όλη τη λίστα
351
+ this._updateActiveElement();
352
352
 
353
- if (this.activeIndex > -1) setTimeout(() => this._scrollToActive(), 0);
354
- if (this.hasSearch) setTimeout(() => this.wrapper.querySelector('.cs-search-input').focus(), 50);
353
+ if (this.activeIndex > -1) setTimeout(() => this._scrollToActive(true), 0); // 6.1.4: από την αρχή, αν η επιλεγμένη φαίνεται εκεί
354
+ if (this.hasSearch) setTimeout(() => this.wrapper.querySelector('.cs-search-input').focus(), 50);
355
+ }
356
+ this.wrapper.classList.toggle('open', this.isOpen);
355
357
  }
356
- this.wrapper.classList.toggle('open', this.isOpen);
357
- }
358
358
 
359
359
  _bindEvents() {
360
360
  this.wrapper.querySelector('.cs-trigger').onclick = (e) => {
@@ -362,42 +362,42 @@ class CountrySelect {
362
362
  this._toggle();
363
363
  };
364
364
 
365
- this.wrapper.addEventListener('keydown', (e) => {
366
- if (!this.isOpen && (e.key === 'ArrowDown')) {
367
- e.preventDefault();
368
- this._toggle(true);
369
- return;
370
- }
371
-
372
- if (this.isOpen) {
373
- if (e.key === 'ArrowDown') {
374
- e.preventDefault();
375
- // ΕΛΕΓΧΟΣ: Μπορούμε να πάμε παρακάτω;
376
- if (this.activeIndex < this.filteredCountries.length - 1) {
377
- this.activeIndex++;
378
- this._updateActiveElement();
379
- this._scrollToActive();
365
+ this.wrapper.addEventListener('keydown', (e) => {
366
+ if (!this.isOpen && (e.key === 'ArrowDown')) {
367
+ e.preventDefault();
368
+ this._toggle(true);
369
+ return;
380
370
  }
381
- } else if (e.key === 'ArrowUp') {
382
- e.preventDefault();
383
- // ΕΛΕΓΧΟΣ: Μπορούμε να πάμε παραπάνω;
384
- if (this.activeIndex > 0) {
385
- this.activeIndex--;
386
- this._updateActiveElement();
387
- this._scrollToActive();
388
- }
389
- } else if (e.key === 'Escape') {
390
- this._toggle(false);
391
- } else if (e.key === 'Enter') {
392
- e.preventDefault();
393
- // Εδώ πρέπει να είμαστε σίγουροι ότι το activeIndex είναι έγκυρο
394
- if (this.activeIndex >= 0 && this.activeIndex < this.filteredCountries.length) {
395
- this._select(this.filteredCountries[this.activeIndex]);
371
+
372
+ if (this.isOpen) {
373
+ if (e.key === 'ArrowDown') {
374
+ e.preventDefault();
375
+ // ΕΛΕΓΧΟΣ: Μπορούμε να πάμε παρακάτω;
376
+ if (this.activeIndex < this.filteredCountries.length - 1) {
377
+ this.activeIndex++;
378
+ this._updateActiveElement();
379
+ this._scrollToActive();
380
+ }
381
+ } else if (e.key === 'ArrowUp') {
382
+ e.preventDefault();
383
+ // ΕΛΕΓΧΟΣ: Μπορούμε να πάμε παραπάνω;
384
+ if (this.activeIndex > 0) {
385
+ this.activeIndex--;
386
+ this._updateActiveElement();
387
+ this._scrollToActive();
388
+ }
389
+ } else if (e.key === 'Escape') {
390
+ this._toggle(false);
391
+ } else if (e.key === 'Enter') {
392
+ e.preventDefault();
393
+ // Εδώ πρέπει να είμαστε σίγουροι ότι το activeIndex είναι έγκυρο
394
+ if (this.activeIndex >= 0 && this.activeIndex < this.filteredCountries.length) {
395
+ this._select(this.filteredCountries[this.activeIndex]);
396
+ }
397
+ }
396
398
  }
397
- }
398
- }
399
- });
400
-
399
+ });
400
+
401
401
  if (this.hasSearch) {
402
402
  this.wrapper.querySelector('.cs-search-input').oninput = (e) => {
403
403
  const term = e.target.value.toLowerCase();
@@ -417,14 +417,22 @@ class CountrySelect {
417
417
  });
418
418
  }
419
419
 
420
- _scrollToActive() {
420
+ /**
421
+ * Scroll της λίστας στην επιλεγμένη χώρα.
422
+ * 6.1.4: fromTop (άνοιγμα της λίστας) — αν η επιλεγμένη φαίνεται ήδη από την αρχή της λίστας, η λίστα ξεκινά από την αρχή
423
+ * (πριν: κεντράριζε ΠΑΝΤΑ → με επιλεγμένη την 3η χώρα, π.χ. Greece μετά από Bulgaria · Cyprus, κρυβόταν η 1η / οι προτιμώμενες).
424
+ * Θέση μέσα στη λίστα από τα getBoundingClientRect (το offsetTop μετρά από το .cs-dropdown — μαζί με το πεδίο αναζήτησης).
425
+ */
426
+ _scrollToActive(fromTop = false) {
421
427
  const list = this.wrapper.querySelector('.cs-list');
422
- const options = list.querySelectorAll('.cs-option');
423
- const activeItem = options[this.activeIndex];
424
- if (activeItem) {
425
- const scrollPos = activeItem.offsetTop - (list.offsetHeight / 2) + (this.rowHeight / 2);
426
- list.scrollTop = scrollPos;
428
+ const activeItem = list.querySelectorAll('.cs-option')[this.activeIndex];
429
+ if (!activeItem) return;
430
+ const top = activeItem.getBoundingClientRect().top - list.getBoundingClientRect().top + list.scrollTop;
431
+ if (fromTop && top + activeItem.offsetHeight <= list.clientHeight) {
432
+ list.scrollTop = 0;
433
+ return;
427
434
  }
435
+ list.scrollTop = top - (list.clientHeight / 2) + (activeItem.offsetHeight / 2);
428
436
  }
429
437
  }
430
438
 
@@ -0,0 +1 @@
1
+ class CountrySelect{constructor(e,t={}){if(this.input=e,!this.input)return;if("1"===this.input.dataset.csInitialized)return;this.input.dataset.csInitialized="1",this.schema=this.input.dataset.schema||"{img} {name}",this.schemaReturn=this.input.dataset.schemaReturn||this.schema,this.valueType=this.input.dataset.valueType||"code",this.rowLimit=parseInt(this.input.dataset.countryRowValues)||5,this.hasSearch="false"!==this.input.dataset.countrySearch,this.dropdownWidth=this.input.dataset.dropdownWidth||"auto";const i=this.input.dataset.preferred||"";this.preferredCodes=i?i.split(",").map(e=>e.trim().toUpperCase()):[],this.config={placeholder:this.input.dataset.placeholder||"Select",...t},this.countries=[],this.filteredCountries=[],this.isOpen=!1,this.activeIndex=-1,this.rowHeight=44,this.baseUrl="https://cdn.jsdelivr.net/gh/nikmauro/country-select-pro@6.1.3/",this._init()}async _init(){this._applyStyles(),this._setupDOM(),this._bindEvents(),await this._loadData()}_applyStyles(){const e="cs-v57-styles";if(!document.getElementById(e)){const t=document.createElement("style");t.id=e,t.textContent="\n.cs-no-results {\n padding: 10px;\n text-align: center;\n color: #999;\n cursor: default;\n font-style: italic;\n}\n\n.cs-flag-img {\n width: 22px !important; \n height: 16px !important; \n object-fit: cover; /* Ή contain, ανάλογα πώς φαίνεται καλύτερα */\n vertical-align: middle;\n margin-right: 8px;\n border-radius: 2px;\n display: inline-block;\n}\n \n .cs-wrapper { position: relative; font-family: inherit; width: 100%; outline: none; }\n .cs-trigger { \n /* Bootstrap 5 Exact Specs */\n padding: 0.375rem 2.25rem 0.375rem 0.75rem; \n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n border-radius: 0.375rem;\n transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;\n display: flex; align-items: center; gap: 8px; cursor: pointer; \n min-height: calc(1.5em + 0.75rem + 2px); \n box-sizing: border-box; position: relative;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3E%3C/svg%3E\");\n background-repeat: no-repeat; background-position: right 0.75rem center; background-size: 16px 12px;\n }\n \n .input-group > .cs-wrapper { flex: 1 1 auto; width: 1%; }\n .cs-wrapper.is-invalid .cs-trigger { border-color: #dc3545 !important; }\n .cs-error-msg { display: none; color: #dc3545; font-size: 0.825em; margin-top: 4px; position: absolute; left: 0; top: 100%; width: 100%; z-index: 10; }\n .cs-wrapper.is-invalid .cs-error-msg { display: block; }\n\n /* Focus state Bootstrap 5 */\n .cs-wrapper:focus-within .cs-trigger {\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n }\n\n .flag-emoji {\n font-size: 1.2em; /* Για να φαίνεται πιο καθαρό το emoji */\n margin-right: 8px;\n }\n\n .cs-dropdown { \n position: absolute; left: 0; right: 0; background: #fff; \n border: 1px solid rgba(0,0,0,.15); border-radius: 0.375rem;\n z-index: 2050; display: none; margin-top: 0.125rem;\n box-shadow: 0 0.5rem 1rem rgba(0,0,0,.175);\n overflow: hidden; max-width: 95vw;\n }\n \n .cs-wrapper.drop-up .cs-dropdown { \n bottom: calc(100% + 0.25rem); top: auto;\n box-shadow: 0 -0.5rem 1rem rgba(0,0,0,.175);\n }\n .cs-wrapper.open .cs-dropdown { display: block; }\n \n .cs-search-box { padding: 8px; border-bottom: 1px solid #dee2e6; background: #f8f9fa; }\n .cs-search-input { \n width: 100%; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; \n border-radius: 0.25rem; outline: none; font-size: 0.875rem;\n }\n .cs-list { overflow-y: auto; scrollbar-width: thin; scroll-behavior: auto; min-height: 50px; padding: 0.375rem 0; }\n .cs-option { \n padding: 0.5rem 0.75rem; display: flex; align-items: center; gap: 10px; \n cursor: pointer; font-size: 0.9rem; transition: background 0.1s;\n }\n .cs-option:hover { background-color: #f8f9fa; }\n .cs-option.active { background-color: #0d6efd; color: #fff; }\n \n .cs-wrapper img { width: 20px !important; height: 14px !important; object-fit: cover; border-radius: 2px; flex-shrink: 0; }\n .cs-selected-content { display: flex; align-items: center; gap: 8px; overflow: hidden; white-space: nowrap; line-height: 1; }\n .cs-divider { height: 1px; background: #dee2e6; margin: 0.375rem 0; pointer-events: none; }\n .cs-hidden { display: none !important; }\n ",document.head.appendChild(t)}}_setupDOM(){Object.assign(this.input.style,{position:"absolute",opacity:"0",width:"1px",height:"1px",pointerEvents:"none"}),this.wrapper=document.createElement("div"),this.wrapper.className="cs-wrapper",this.wrapper.tabIndex=0;const e=this.input.value?"":this.config.placeholder;this.wrapper.innerHTML=`\n <div class="cs-trigger"><span class="cs-selected-content">${e}</span></div>\n <div class="cs-error-msg"></div>\n <div class="cs-dropdown">\n <div class="cs-search-box ${this.hasSearch?"":"cs-hidden"}">\n <input type="text" class="cs-search-input" placeholder="Search..." autocomplete="off" tabindex="-1">\n </div>\n <div class="cs-list"><div class="cs-loading" style="padding:15px; text-align:center;">Loading...</div></div>\n </div>\n `;this.wrapper.querySelector(".cs-dropdown").style.width="auto"===this.dropdownWidth?"100%":this.dropdownWidth,this.input.parentNode.insertBefore(this.wrapper,this.input.nextSibling)}_updateActiveElement(){this.wrapper.querySelectorAll(".cs-option").forEach((e,t)=>{t===this.activeIndex?(e.classList.add("active"),e.scrollIntoView({block:"nearest"})):e.classList.remove("active")})}_updatePosition(){const e=this.wrapper.getBoundingClientRect(),t=this.rowLimit*this.rowHeight+(this.hasSearch?60:0),i=window.innerHeight-e.bottom,n=e.top;i<t&&n>t?this.wrapper.classList.add("drop-up"):this.wrapper.classList.remove("drop-up")}async _loadData(){try{const e=`${this.baseUrl}dist/data.json`,t=await fetch(e),i=await t.json(),n=i?.data?.objects||[];this.countries=n.map(e=>{const t=e.codes?.alpha_2?e.codes.alpha_2.toUpperCase():"";return{name:e.names?.common||"",code:t,flag:t?`${this.baseUrl}dist/flags/4x3/${t.toLowerCase()}.svg`:"",phone:e.calling_codes&&e.calling_codes.length>0?`+${e.calling_codes[0]}`:"",isPreferred:!!t&&this.preferredCodes.includes(t)}}).filter(e=>""!==e.code).sort((e,t)=>e.name.localeCompare(t.name)),this.filteredCountries=[...this.countries],this._renderOptions(),this._syncInitialValue()}catch(e){console.error("CountrySelect Load Fail:",e)}}_syncInitialValue(){const e=this.input.value;if(!e)return void(this.wrapper.querySelector(".cs-selected-content").innerHTML=this.config.placeholder);const t=this.countries.find(t=>"phone"===this.valueType?t.phone===e:"name"===this.valueType?t.name===e:t.code===e);t&&this._updateUI(t)}_parseTemplate(e,t){return e.replace("{img}",`<img src="${t.flag}" class="cs-flag-img" loading="lazy" alt="${t.name}">`).replace("{name}",`<span>${t.name}</span>`).replace("{code}",`<span>${t.code}</span>`).replace("{phone}",`<span>${t.phone}</span>`)}_renderOptions(){const e=this.wrapper.querySelector(".cs-list");if(!e)return;if(e.innerHTML="",0===this.filteredCountries.length){const t=document.createElement("div");return t.className="cs-no-results",t.innerHTML='<span class="cs-no-results-text">Not found</span>',void e.appendChild(t)}const t=this.wrapper.querySelector(".cs-search-input")?.value||"",i=this.filteredCountries.filter(e=>e.isPreferred),n=this.filteredCountries.filter(e=>!e.isPreferred),s=(e,t)=>{const i=document.createElement("div");return i.className="cs-option "+(t===this.activeIndex?"active":""),i.innerHTML=this._parseTemplate(this.schema,e),i.onclick=t=>{t.stopPropagation(),this._select(e)},i};if(i.forEach((t,i)=>e.appendChild(s(t,i))),i.length>0&&n.length>0&&!t){const t=document.createElement("div");t.className="cs-divider",e.appendChild(t)}n.forEach((t,n)=>e.appendChild(s(t,i.length+n)))}_select(e){this._updateUI(e),this.input.value="phone"===this.valueType?e.phone:"name"===this.valueType?e.name:e.code,this.input.setCustomValidity(""),this.wrapper.classList.remove("is-invalid"),this.input.dispatchEvent(new Event("change",{bubbles:!0})),this.input.dispatchEvent(new Event("input",{bubbles:!0})),this.input.dispatchEvent(new Event("country-change",{bubbles:!0})),this._toggle(!1),this.wrapper.focus()}_updateUI(e){const t=this.wrapper.querySelector(".cs-selected-content");t&&(t.innerHTML=this._parseTemplate(this.schemaReturn,e))}_toggle(e){if(0!==this.countries.length){if(this.isOpen=void 0!==e?e:!this.isOpen,this.isOpen){this._updatePosition(),document.dispatchEvent(new CustomEvent("cs-close-others",{detail:{opener:this.wrapper}}));const e=this.wrapper.querySelector(".cs-list");0===e.children.length&&this._renderOptions(),e.style.maxHeight=this.rowLimit*this.rowHeight+"px";const t=[...this.filteredCountries.filter(e=>e.isPreferred),...this.filteredCountries.filter(e=>!e.isPreferred)];this.activeIndex=t.findIndex(e=>this.input.value.includes(e.code)||this.input.value.includes(e.phone)),this._updateActiveElement(),this.activeIndex>-1&&setTimeout(()=>this._scrollToActive(!0),0),this.hasSearch&&setTimeout(()=>this.wrapper.querySelector(".cs-search-input").focus(),50)}this.wrapper.classList.toggle("open",this.isOpen)}}_bindEvents(){this.wrapper.querySelector(".cs-trigger").onclick=e=>{e.stopPropagation(),this._toggle()},this.wrapper.addEventListener("keydown",e=>{if(!this.isOpen&&"ArrowDown"===e.key)return e.preventDefault(),void this._toggle(!0);this.isOpen&&("ArrowDown"===e.key?(e.preventDefault(),this.activeIndex<this.filteredCountries.length-1&&(this.activeIndex++,this._updateActiveElement(),this._scrollToActive())):"ArrowUp"===e.key?(e.preventDefault(),this.activeIndex>0&&(this.activeIndex--,this._updateActiveElement(),this._scrollToActive())):"Escape"===e.key?this._toggle(!1):"Enter"===e.key&&(e.preventDefault(),this.activeIndex>=0&&this.activeIndex<this.filteredCountries.length&&this._select(this.filteredCountries[this.activeIndex])))}),this.hasSearch&&(this.wrapper.querySelector(".cs-search-input").oninput=e=>{const t=e.target.value.toLowerCase();this.filteredCountries=this.countries.filter(e=>e.name.toLowerCase().includes(t)||e.phone.includes(t)),this.activeIndex=0,this._renderOptions()}),document.addEventListener("click",e=>{this.wrapper.contains(e.target)||this._toggle(!1)}),document.addEventListener("cs-close-others",e=>{e.detail.opener!==this.wrapper&&(this.isOpen=!1,this.wrapper.classList.remove("open"))})}_scrollToActive(e=!1){const t=this.wrapper.querySelector(".cs-list"),i=t.querySelectorAll(".cs-option")[this.activeIndex];if(!i)return;const n=i.getBoundingClientRect().top-t.getBoundingClientRect().top+t.scrollTop;e&&n+i.offsetHeight<=t.clientHeight?t.scrollTop=0:t.scrollTop=n-t.clientHeight/2+i.offsetHeight/2}}const initCS=e=>{const t=e&&e.querySelectorAll?e:document;t.matches?.(".country-select")&&"1"!==t.dataset.csInitialized&&new CountrySelect(t),t.querySelectorAll?.(".country-select").forEach(e=>{"1"!==e.dataset.csInitialized&&new CountrySelect(e)})},setupListeners=()=>{initCS(document),document.body.addEventListener("htmx:afterSettle",e=>{initCS(e.detail.target||e.target)}),new MutationObserver(e=>{e.forEach(e=>{e.addedNodes.forEach(e=>{1===e.nodeType&&initCS(e)})})}).observe(document.body,{childList:!0,subtree:!0})};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",setupListeners):setupListeners();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "country-select-pro",
3
- "version": "6.1.3",
3
+ "version": "6.1.4",
4
4
  "description": "A lightweight, zero-dependency, vanilla JavaScript country selector with localized browser validation and Bootstrap 5 integration.",
5
5
  "main": "dist/country-select.js",
6
6
  "jsdelivr": "dist/country-select.js",