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 +4 -6
- package/dist/country-select.js +152 -144
- package/dist/country-select.min.js +1 -0
- package/package.json +1 -1
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/
|
|
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.
|
|
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
|
-
│ └─
|
|
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
|
package/dist/country-select.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CountrySelect Pro v6.1.
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
this.
|
|
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
|
-
|
|
292
|
+
preferred.forEach((c, i) => listContainer.appendChild(createOption(c, i)));
|
|
293
293
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
343
|
+
list.style.maxHeight = `${this.rowLimit * this.rowHeight}px`;
|
|
344
344
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
351
|
-
|
|
350
|
+
// ΑΛΛΑΓΗ: Ενημέρωσε το active element αντί να κάνεις re-render όλη τη λίστα
|
|
351
|
+
this._updateActiveElement();
|
|
352
352
|
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
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
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
list.scrollTop =
|
|
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
|
+
"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",
|