PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.13
Yatra – Travel Booking & Tour Operator Software v3.0.13
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / assets / js / country-select.js

country-select.js in Yatra – Travel Booking & Tour Operator Software 3.0.13, at assets/js/country-select.js

295 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Country selector with flags.
3 *
4 * Upgrades any `<select data-yatra-country-select>` into the same searchable,
5 * flag-bearing dropdown the phone widget uses (assets/js/phone-input.js), so
6 * every country field in the plugin looks and behaves alike.
7 *
8 * Progressive enhancement on purpose: the original <select> stays in the DOM as
9 * the value holder. The widget writes the chosen ISO code back to it and fires a
10 * `change` event, so form submission, required-field validation and any existing
11 * listener keep working exactly as before. If this script fails to load — or the
12 * browser is very old — the untouched native select is still fully usable.
13 *
14 * Reads flag URLs from the field's own `data-flag-base`, matching the phone
15 * widget, so there is a single place that knows where the SVGs live.
16 */
17 (function () {
18 "use strict";
19
20 var DATA = window.yatraCountrySelectData || {};
21 var I18N = DATA.i18n || {};
22 var SEARCH_PLACEHOLDER = I18N.search || "Search country";
23 var NO_RESULTS = I18N.noResults || "No matches";
24
25 /** Native pickers are better on touch devices, so leave those alone. */
26 function prefersNativePicker() {
27 return (
28 window.matchMedia &&
29 window.matchMedia("(max-width: 640px), (pointer: coarse)").matches
30 );
31 }
32
33 function flagUrl(base, iso) {
34 if (!base || !iso) return "";
35 return base + String(iso).toLowerCase() + ".svg";
36 }
37
38 function closeAll(except) {
39 document
40 .querySelectorAll(".yatra-country-field.is-open")
41 .forEach(function (f) {
42 if (f === except) return;
43 f.classList.remove("is-open");
44 var btn = f.querySelector(".yatra-country-trigger");
45 if (btn) btn.setAttribute("aria-expanded", "false");
46 });
47 }
48
49 function enhance(select) {
50 if (!select || select.dataset.yatraCountryReady === "1") return;
51 // A disabled or readonly field gains nothing from the widget.
52 if (select.disabled) return;
53
54 select.dataset.yatraCountryReady = "1";
55
56 var flagBase = select.getAttribute("data-flag-base") || "";
57
58 // Build the option model straight from the existing <select>, so whatever
59 // list, order or filtering the server rendered is preserved verbatim.
60 var options = [];
61 Array.prototype.forEach.call(select.options, function (opt) {
62 options.push({
63 value: opt.value,
64 label: opt.textContent.trim(),
65 isPlaceholder: opt.value === "",
66 });
67 });
68
69 if (options.length === 0) return;
70
71 var field = document.createElement("div");
72 field.className = "yatra-country-field";
73
74 var trigger = document.createElement("button");
75 trigger.type = "button";
76 trigger.className = "yatra-country-trigger";
77 trigger.setAttribute("aria-haspopup", "listbox");
78 trigger.setAttribute("aria-expanded", "false");
79 trigger.innerHTML =
80 '<img class="yatra-country-flag" width="22" height="16" alt="" src="">' +
81 '<span class="yatra-country-label"></span>' +
82 '<svg class="yatra-country-caret" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>';
83
84 var flagImg = trigger.querySelector(".yatra-country-flag");
85 var labelEl = trigger.querySelector(".yatra-country-label");
86
87 var dropdown = null;
88 var listEl = null;
89 var searchEl = null;
90 var emptyEl = null;
91
92 function syncFromSelect() {
93 var opt = select.options[select.selectedIndex];
94 var value = opt ? opt.value : "";
95 var label = opt ? opt.textContent.trim() : "";
96
97 labelEl.textContent = label;
98
99 var url = value ? flagUrl(flagBase, value) : "";
100 if (url) {
101 flagImg.setAttribute("src", url);
102 flagImg.setAttribute("alt", label);
103 flagImg.style.visibility = "visible";
104 } else {
105 // Placeholder ("Select Country") has no flag to show.
106 flagImg.removeAttribute("src");
107 flagImg.setAttribute("alt", "");
108 flagImg.style.visibility = "hidden";
109 }
110
111 if (listEl) {
112 listEl
113 .querySelectorAll(".yatra-country-option")
114 .forEach(function (li) {
115 li.classList.toggle("is-active", li.getAttribute("data-value") === value);
116 li.setAttribute(
117 "aria-selected",
118 li.getAttribute("data-value") === value ? "true" : "false",
119 );
120 });
121 }
122 }
123
124 function choose(value) {
125 select.value = value;
126 // Let anything already listening (validation, pricing, Pro modules) react
127 // exactly as it would to a real user picking from the native control.
128 select.dispatchEvent(new Event("change", { bubbles: true }));
129 syncFromSelect();
130 }
131
132 function filter(term) {
133 var needle = String(term || "").trim().toLowerCase();
134 var visible = 0;
135
136 listEl.querySelectorAll(".yatra-country-option").forEach(function (li) {
137 var hay = li.getAttribute("data-search") || "";
138 var show = needle === "" || hay.indexOf(needle) !== -1;
139 li.style.display = show ? "" : "none";
140 if (show) visible++;
141 });
142
143 emptyEl.style.display = visible === 0 ? "" : "none";
144 }
145
146 function buildDropdown() {
147 if (dropdown) return;
148
149 dropdown = document.createElement("div");
150 dropdown.className = "yatra-country-dropdown";
151
152 var searchWrap = document.createElement("div");
153 searchWrap.className = "yatra-country-search-wrap";
154 searchEl = document.createElement("input");
155 searchEl.type = "text";
156 searchEl.className = "yatra-country-search";
157 searchEl.setAttribute("placeholder", SEARCH_PLACEHOLDER);
158 searchEl.setAttribute("aria-label", SEARCH_PLACEHOLDER);
159 searchEl.setAttribute("autocomplete", "off");
160 searchWrap.appendChild(searchEl);
161
162 listEl = document.createElement("ul");
163 listEl.className = "yatra-country-list";
164 listEl.setAttribute("role", "listbox");
165
166 options.forEach(function (o) {
167 if (o.isPlaceholder) return;
168
169 var li = document.createElement("li");
170 li.className = "yatra-country-option";
171 li.setAttribute("role", "option");
172 li.setAttribute("data-value", o.value);
173 li.setAttribute(
174 "data-search",
175 (o.label + " " + o.value).toLowerCase(),
176 );
177 li.innerHTML =
178 '<img class="yatra-country-flag" loading="lazy" width="22" height="16" alt="" src="' +
179 flagUrl(flagBase, o.value) +
180 '">' +
181 '<span class="yatra-country-option-name"></span>';
182 li.querySelector(".yatra-country-option-name").textContent = o.label;
183
184 li.addEventListener("click", function () {
185 choose(o.value);
186 close();
187 trigger.focus();
188 });
189
190 listEl.appendChild(li);
191 });
192
193 emptyEl = document.createElement("div");
194 emptyEl.className = "yatra-country-empty";
195 emptyEl.textContent = NO_RESULTS;
196 emptyEl.style.display = "none";
197
198 dropdown.appendChild(searchWrap);
199 dropdown.appendChild(listEl);
200 dropdown.appendChild(emptyEl);
201 field.appendChild(dropdown);
202
203 searchEl.addEventListener("input", function () {
204 filter(searchEl.value);
205 });
206
207 searchEl.addEventListener("keydown", function (e) {
208 if (e.key === "Escape") {
209 close();
210 trigger.focus();
211 }
212 });
213 }
214
215 function open() {
216 buildDropdown();
217 closeAll(field);
218 field.classList.add("is-open");
219 trigger.setAttribute("aria-expanded", "true");
220 syncFromSelect();
221 filter("");
222 searchEl.value = "";
223 searchEl.focus();
224
225 var active = listEl.querySelector(".yatra-country-option.is-active");
226 if (active && active.scrollIntoView) {
227 active.scrollIntoView({ block: "nearest" });
228 }
229 }
230
231 function close() {
232 field.classList.remove("is-open");
233 trigger.setAttribute("aria-expanded", "false");
234 }
235
236 trigger.addEventListener("click", function (e) {
237 e.preventDefault();
238 e.stopPropagation();
239 if (field.classList.contains("is-open")) {
240 close();
241 } else {
242 open();
243 }
244 });
245
246 trigger.addEventListener("keydown", function (e) {
247 if (e.key === "Escape") close();
248 });
249
250 // Keep the widget in step if other code sets the value programmatically
251 // (e.g. a saved profile prefilling the checkout).
252 select.addEventListener("change", syncFromSelect);
253
254 // Wrap the original control: field takes the select's place, the select
255 // moves inside it (hidden, still the value holder) and the trigger renders
256 // in front of it.
257 select.parentNode.insertBefore(field, select);
258 field.appendChild(select);
259 select.classList.add("yatra-country-native");
260 field.insertBefore(trigger, select);
261
262 syncFromSelect();
263 }
264
265 function initAll(root) {
266 if (prefersNativePicker()) return;
267
268 (root || document)
269 .querySelectorAll("select[data-yatra-country-select]")
270 .forEach(enhance);
271 }
272
273 document.addEventListener("click", function () {
274 closeAll(null);
275 });
276
277 document.addEventListener("keydown", function (e) {
278 if (e.key === "Escape") closeAll(null);
279 });
280
281 if (document.readyState === "loading") {
282 document.addEventListener("DOMContentLoaded", function () {
283 initAll(document);
284 });
285 } else {
286 initAll(document);
287 }
288
289 // Re-scan when fields arrive later (traveler rows, AJAX-rendered checkout).
290 window.yatraInitCountrySelects = initAll;
291 document.addEventListener("yatra:content-updated", function (e) {
292 initAll((e && e.detail && e.detail.root) || document);
293 });
294 })();
295