PageSourceSearch

https://www.norwoodvets.co.uk/media/plg_ckmforms_advancedtel/js/advanced-tel-field.js?1.0.10

js norwoodvets.co.uk collected 2026-09-28 12:42:34 UTC 30,477 bytes, 657 lines download raw bytes

1/**
2 * @package     Clickingmad
3 * @subpackage  Forms
4 *
5 * @copyright   Copyright (C) 2017-2026 Clickingmad Ltd. All rights reserved.
6 * @license     GNU General Public License version 2 or later; see LICENSE
7 */
8class AdvancedTelField {
9  constructor(targetSelector, options = {}) {
10    this.isInitialising = true;
11
12    this.targetInput = typeof targetSelector === 'string'
13      ? document.querySelector(targetSelector)
14      : targetSelector;
15
16    if (!this.targetInput) return;
17
18    // Prevent double initialization
19    if (this.targetInput.dataset.phoneInputInitialized === 'true') return;
20    this.targetInput.dataset.phoneInputInitialized = 'true';
21
22    const dataset = this.targetInput.dataset;
23    const parseList = (str) =>
24      str ? str.toUpperCase().split(',').map(s => s.trim()).filter(Boolean) : [];
25
26    // Default to GB. Handle common "UK" mistake in HTML attributes.
27    let rawDefault = dataset.defaultCountry || "GB";
28    if (rawDefault.toUpperCase() === "UK") rawDefault = "GB";
29
30    const globalDefaults = window.Joomla?.getOptions?.('ckmforms.advancedtel') || {};
31
32    this.options = {
33      defaultCountry:
34        dataset.defaultCountry ||
35        globalDefaults.defaultCountry ||
36        "GB",
37
38      hiddenInputName:
39        dataset.hiddenInputName ||
40        globalDefaults.hiddenInputName ||
41        "phone_e164",
42
43      errorMessage:
44        dataset.errorMessage ||
45        globalDefaults.errorMessage ||
46        "Invalid phone number",
47
48      preferredCountries:
49        parseList(dataset.preferredCountries || globalDefaults.preferredCountries),
50
51      onlyCountries:
52        parseList(dataset.onlyCountries || globalDefaults.onlyCountries),
53
54      cssMode:
55        dataset.css || globalDefaults.cssMode || "minimal",
56
57      ...options,
58    };
59
60    this.isDisabled = this.targetInput.hasAttribute('disabled');
61    this.countries = this.getCountries();
62    this.selected = this.countries.find(c => c.code === this.options.defaultCountry) || this.countries[0];
63
64    // AbortController for easy event listener cleanup
65    this.abortController = new AbortController();
66
67    this.injectStyles();
68    this.buildDOM();
69    this.bindEvents();
70
71    if (this.selected) this.selectCountry(this.selected.code);
72    if (this.isDisabled) this.setDisabled(true);
73
74    this.setupJoomlaValidation();
75
76    // Sync field even through B/F cache
77    queueMicrotask(() => {
78      this.syncFromRestoredValue();
79    });
80    window.addEventListener(
81      'pageshow',
82      () => {
83        setTimeout(() => {
84          this.syncFromRestoredValue();
85        }, 0);
86      },
87      { signal: this.abortController.signal }
88    );
89
90    this.isInitialising = false;
91  }
92
93  syncFromRestoredValue() {
94    // Read the *property*, not the attribute
95    const value = this.targetInput.value;
96
97    if (value && value.trim()) {
98      this.validateAndFormat();
99    }
100  }
101
102  buildDOM() {
103    this.wrapper = document.createElement("div");
104    this.wrapper.className = "phone-input-group";
105    this.wrapper.setAttribute("data-pi-mode", this.options.cssMode);
106
107    this.container = document.createElement("div");
108    this.container.className = "phone-container";
109
110    // Find the hidden input by name (more reliable than ID for subforms)
111    // The tel input name is always {hiddenInputName}-advanced-tel
112    // Scope to the closest form to avoid clashes with identically named fields on other forms
113    const telName = this.targetInput.name;
114    const hiddenName = telName.replace(/-advanced-tel$/, '');
115    const form = this.targetInput.closest('form');
116    const searchRoot = form || document;
117    this.hiddenInput = searchRoot.querySelector(`input[type="hidden"][name="${hiddenName}"]`);
118
119    // Fallback to ID-based lookup
120    if (!this.hiddenInput) {
121      const baseId = this.targetInput.id.replace('-advanced-tel', '').replace(/_advanced_tel$/, '');
122      this.hiddenInput = document.getElementById(baseId);
123    }
124
125    if (!this.hiddenInput) {
126      this.hiddenInput = document.createElement("input");
127      this.hiddenInput.type = "hidden";
128      this.hiddenInput.name = hiddenName || this.options.hiddenInputName;
129      this.hiddenInput.id = this.targetInput.id.replace('-advanced-tel', '').replace(/_advanced_tel$/, '');
130      this.hiddenInput.className = "e164-output";
131    }
132
133    this.trigger = document.createElement("button");
134    this.trigger.type = "button";
135    this.trigger.className = "country-btn";
136    this.trigger.setAttribute("aria-expanded", "false");
137
138    if (this.isDisabled) this.trigger.disabled = true;
139
140    this.trigger.innerHTML = `
141        <img class="selected-flag" src="" alt="Flag" />
142        <svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
143          <polyline points="6 9 12 15 18 9"></polyline>
144        </svg>`;
145
146    const divider = document.createElement("div");
147    divider.className = "divider";
148
149    this.dropdown = document.createElement("div");
150    this.dropdown.className = "country-dropdown";
151    this.dropdown.innerHTML = `
152        <div class="search-container">
153          <input type="text" class="search-input" placeholder="Search countries..." />
154        </div>
155        <ul class="country-list"></ul>`;
156
157    this.validationMsg = document.createElement("div");
158    this.validationMsg.className = "validation-msg";
159    this.validationMsg.innerHTML = `
160        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
161          <circle cx="12" cy="12" r="10"></circle>
162          <line x1="12" y1="8" x2="12" y2="12"></line>
163          <line x1="12" y1="16" x2="12.01" y2="16"></line>
164        </svg>
165        <span class="msg-text">${this.options.errorMessage}</span>`;
166
167    this.flagImg = this.trigger.querySelector(".selected-flag");
168    this.search = this.dropdown.querySelector(".search-input");
169    this.list = this.dropdown.querySelector(".country-list");
170
171    const referenceNode = this.hiddenInput.parentNode ? this.hiddenInput : this.targetInput;
172    referenceNode.parentNode.insertBefore(this.wrapper, referenceNode);
173
174    this.container.appendChild(this.trigger);
175    this.container.appendChild(divider);
176    this.container.appendChild(this.targetInput);
177
178    // Ensure hidden input is inside container if it wasn't already
179    if (!this.hiddenInput.parentNode || this.hiddenInput.parentNode !== this.container) {
180      this.container.appendChild(this.hiddenInput);
181    }
182
183    this.wrapper.appendChild(this.container);
184    this.wrapper.appendChild(this.validationMsg);
185    this.wrapper.appendChild(this.dropdown);
186
187    this.targetInput.classList.add("phone-field");
188    this.targetInput.setAttribute("autocomplete", "tel");
189  }
190
191  getCountries() {
192    if (typeof libphonenumber === "undefined") {
193      console.error("libphonenumber-js not loaded.");
194      return [];
195    }
196
197    let supportedCodes = libphonenumber.getCountries();
198
199    if (this.options.onlyCountries?.length > 0) {
200      supportedCodes = supportedCodes.filter(code => this.options.onlyCountries.includes(code));
201    }
202
203    const regionNames = new Intl.DisplayNames(["en"], { type: "region" });
204
205    let countryList = supportedCodes.map((code) => {
206      try {
207        return {
208          code: code,
209          name: regionNames.of(code),
210          dial: "+" + libphonenumber.getCountryCallingCode(code),
211        };
212      } catch (e) {
213        return null;
214      }
215    }).filter(Boolean);
216
217    countryList.sort((a, b) => a.name.localeCompare(b.name));
218
219    if (this.options.preferredCountries?.length > 0) {
220      const preferred = [];
221      const others = [];
222
223      countryList.forEach(country => {
224        if (this.options.preferredCountries.includes(country.code)) {
225          preferred.push(country);
226        } else {
227          others.push(country);
228        }
229      });
230
231      preferred.sort((a, b) =>
232        this.options.preferredCountries.indexOf(a.code) - this.options.preferredCountries.indexOf(b.code)
233      );
234
235      return [...preferred, ...others];
236    }
237
238    return countryList;
239  }
240
241  validateAndFormat() {
242    const rawValue = this.targetInput.value.trim();
243    if (!this.selected) return;
244
245    if (!rawValue) {
246      this.setInvalid(false);
247      this.hiddenInput.value = "";
248      this.updateJoomlaValidation(true);
249      return;
250    }
251
252    try {
253      const isValid = libphonenumber.isValidPhoneNumber(rawValue, this.selected.code);
254
255      if (isValid) {
256        this.setInvalid(false);
257        const phoneNumber = libphonenumber.parsePhoneNumber(rawValue, this.selected.code);
258        if (phoneNumber) this.hiddenInput.value = phoneNumber.number;
259        this.updateJoomlaValidation(true);
260      } else {
261        throw new Error('Invalid');
262      }
263    } catch (e) {
264      this.setInvalid(true);
265      this.hiddenInput.value = '';
266      this.updateJoomlaValidation(false);
267    }
268  }
269
270  prettyPrint() {
271    const rawValue = this.targetInput.value.trim();
272    if (!rawValue || !this.selected) return;
273
274    try {
275      const phoneNumber = libphonenumber.parsePhoneNumber(rawValue, this.selected.code);
276      if (phoneNumber) this.targetInput.value = phoneNumber.format("NATIONAL");
277    } catch (e) {
278      // ignore formatting errors on blur
279    }
280  }
281
282  selectCountry(code) {
283    const country = this.countries.find((c) => c.code === code);
284    if (!country) return;
285
286    this.selected = country;
287    this.flagImg.src = `https://flagcdn.com/w40/${country.code.toLowerCase()}.png`;
288    this.flagImg.alt = `${country.name}`;
289    this.targetInput.placeholder = "Phone number";
290    this.targetInput.dataset.selectedCountry = country.code;
291
292    this.validateAndFormat();
293    this.toggleDropdown(false);
294
295    if (!this.isInitialising) {
296      this.targetInput.focus();
297    }
298  }
299
300  setInvalid(isInvalid) {
301    this.container.classList.toggle("invalid", isInvalid);
302  }
303
304  setDisabled(disabled) {
305    this.isDisabled = disabled;
306    this.container.classList.toggle("disabled", disabled);
307    this.trigger.disabled = disabled;
308    this.targetInput.disabled = disabled;
309    if (this.hiddenInput) this.hiddenInput.disabled = disabled;
310  }
311
312  setupJoomlaValidation() {
313    if (!document.formvalidator) return;
314
315    // Register handler if not exists
316    if (!document.formvalidator.handlers['advanced-tel']) {
317      document.formvalidator.setHandler('advanced-tel', (value, element) => {
318        const advancedTelInput = element.form.querySelector(`[id="${element.i
318d}-advanced-tel"]`);
319        if (!advancedTelInput) return true; // fallback
320
321        // Optional field check
322        if (!value.trim() && !element.classList.contains('required') && !element.hasAttribute('required')) {
323          return true;
324        }
325        if (!value.trim()) return false;
326
327        const countryCode = advancedTelInput.dataset.selectedCountry || 'US';
328        try {
329          return libphonenumber.isValidPhoneNumber(value, countryCode);
330        } catch (e) {
331          return false;
332        }
333      });
334    }
335
336    if (this.hiddenInput && this.hiddenInput.form) {
337      if (this.selected) this.targetInput.dataset.selectedCountry = this.selected.code;
338      if (!this.hiddenInput.classList.contains('validate-advanced-tel')) {
339        this.hiddenInput.classList.add('validate-advanced-tel');
340      }
341    }
342  }
343
344  updateJoomlaValidation(isValid) {
345    if (!document.formvalidator || !this.hiddenInput?.form) return;
346
347    this.targetInput.dataset.selectedCountry = this.selected.code;
348    const cl = this.hiddenInput.classList;
349
350    if (isValid) {
351      cl.remove('invalid', 'form-control-danger');
352      cl.add('valid', 'form-control-success');
353      this.hiddenInput.setAttribute('aria-invalid', 'false');
354    } else {
355      cl.remove('valid', 'form-control-success');
356      cl.add('invalid', 'form-control-danger');
357      this.hiddenInput.setAttribute('aria-invalid', 'true');
358    }
359  }
360
361  toggleDropdown(state) {
362    if (this.isDisabled && state !== false) return;
363
364    const isOpen = state !== undefined ? state : !this.dropdown.classList.contains("open");
365    this.dropdown.classList.toggle("open", isOpen);
366    this.trigger.setAttribute("aria-expanded", isOpen);
367
368    if (isOpen) {
369      this.search.value = "";
370      this.renderList(this.countries);
371      setTimeout(() => this.search.focus(), 50);
372
373      const sel = this.list.querySelector(`.country-item[data-code="${this.selected.code}"]`);
374      if (sel) sel.scrollIntoView({ block: "center" });
375    }
376  }
377
378  renderList(data) {
379    const prefCount = this.options.preferredCountries?.length || 0;
380
381    this.list.innerHTML = data.map((c, index) => {
382      const isSeparator = prefCount > 0 && index === prefCount - 1;
383      const separatorStyle = isSeparator ? 'border-bottom: 1px solid #e2e8f0; margin-bottom: 4px; padding-bottom: 10px;' : '';
384
385      return `
386          <li class="country-item ${c.code === this.selected.code ? "selected" : ""}" data-code="${c.code}" style="${separatorStyle}">
387            <img src="https://flagcdn.com/w40/${c.code.toLowerCase()}.png" loading="lazy" alt="${c.code}">
388            <span class="country-name">${c.name}</span>
389            <span class="country-dial">${c.dial}</span>
390          </li>`;
391    }).join("");
392  }
393
394  bindEvents() {
395    this.targetInput.addEventListener("input", () => this.validateAndFormat());
396
397    this.targetInput.addEventListener("blur", () => {
398      this.container.classList.remove("focused");
399      this.prettyPrint();
400    });
401
402    this.targetInput.addEventListener("focus", () => {
403      this.container.classList.add("focused");
404    });
405
406    this.trigger.addEventListener("click", (e) => {
407      e.stopPropagation();
408      this.toggleDropdown();
409    });
410
411    this.search.addEventListener("input", (e) => {
412      const term = e.target.value.toLowerCase();
413      const filtered = this.countries.filter(c =>
414        c.name.toLowerCase().includes(term) ||
415        c.dial.includes(term) ||
416        c.code.toLowerCase().includes(term)
417      );
418      this.renderList(filtered);
419    });
420
421    this.search.addEventListener("click", (e) => e.stopPropagation());
422
423    this.list.addEventListener("click", (e) => {
424      const item = e.target.closest(".country-item");
425      if (item) this.selectCountry(item.dataset.code);
426    });
427
428    document.addEventListener("click", (e) => {
429      if (!this.wrapper.contains(e.target)) this.toggleDropdown(false);
430    }, { signal: this.abortController.signal });
431  }
432
433  /**
434   * Destroy the AdvancedTelField instance and restore original DOM
435   */
436  destroy() {
437    if (!this.wrapper || !this.targetInput) return;
438
439    // Abort all event listeners registered with the AbortController
440    if (this.abortController) this.abortController.abort();
441
442    // Restore the original input to its original position
443    if (this.wrapper.parentNode) {
444      this.wrapper.parentNode.insertBefore(this.targetInput, this.wrapper);
445      this.wrapper.parentNode.insertBefore(this.hiddenInput, this.targetInput.nextSibling);
446    }
447
448    // Remove added classes
449    this.targetInput.classList.remove('phone-field');
450    this.targetInput.removeAttribute('autocomplete');
451
452    // Clear initialization flag so it can be re-initialized
453    delete this.targetInput.dataset.phoneInputInitialized;
454
455    // Remove the wrapper (which contains dropdown, validation msg, etc.)
456    this.wrapper.remove();
457
458    // Clear references
459    this.wrapper = null;
460    this.container = null;
461    this.dropdown = null;
462    this.trigger = null;
463    this.search = null;
464    this.list = null;
465    this.validationMsg = null;
466    this.flagImg = null;
467  }
468
469  injectStyles() {
470    const mode = this.options.cssMode;
471
472    // Minimal CSS
473    if (mode === 'minimal' && !document.getElementById("phone-input-minimal-styles")) {
474      const style = document.createElement("style");
475      style.id = "phone-input-minimal-styles";
476      style.textContent = `
477        .phone-input-group[data-pi-mode="minimal"]{position:relative;display:block;width:100%}
478        .phone-input-group[data-pi-mode="minimal"] .phone-container{display:flex;align-items:center;position:relative;width:100%}
479        .phone-input-group[data-pi-mode="minimal"] .country-btn{display:flex;align-items:center;cursor:pointer;background:transparent;border:none;padding:0 5px}
480        .phone-input-group[data-pi-mode="minimal"] .phone-field{flex:1;min-width:0;background:transparent;border:none}
481        .phone-input-group[data-pi-mode="minimal"] .country-dropdown{position:absolute;top:100%;left:0;width:100%;z-index:999;display:none;background:white;border:1px solid #ccc;max-height:200px;overflow-y:auto}
482        .phone-input-group[data-pi-mode="minimal"] .country-dropdown .country-list{padding-left:8px;padding-right:8px;padding-top:8px;}
483        .phone-input-group[data-pi-mode="minimal"] .country-dropdown .country-list .country-item{margin-bottom: 8px;}
484        .phone-input-group[data-pi-mode="minimal"] .country-dropdown .country-list .country-item::before{display:none;}
485        .phone-input-group[data-pi-mode="minimal"] .country-dropdown.open{display:block}
486        .phone-input-group[data-pi-mode="minimal"] .search-container{padding:5px;border-bottom:1px solid #eee}
487        .phone-input-group[data-pi-mode="minimal"] .search-input{width:100%;box-sizing:border-box}
488        .phone-input-group[data-pi-mode="minimal"] .country-list{list-style:none;padding:0;margin:0}
489        .phone-input-group[data-pi-mode="minimal"] .country-item{display:flex;align-items:center;cursor:pointer;padding:5px}
490        .phone-input-group[data-pi-mode="minimal"] .country-item:hover{background-color:#f0f0f0}
491        .phone-input-group[data-pi-mode="minimal"] .country-item img{margin-right:5px;width:20px}
492        .phone-input-group[data-pi-mode="minimal"] .validation-msg{display:none;color:red;font-size:12px}
493        .phone-input-group[data-pi-mode="minimal"] .phone-container.invalid~.validation-msg{display:block}
494        .phone-input-group[data-pi-mode="minimal"] .divider{width:1px;height:1em;background:#ccc;margin:0 5px}`;
495      document.head.appendChild(style);
496    }
497
498    // Full CSS
499    if (mode === 'full' && !document.getElementById("phone-input-full-styles")) {
500      const style = document.createElement("style");
501      style.id = "phone-input-full-styles";
502      style.textContent = `
503        .phone-input-group[data-pi-mode="full"]{--pi-primary:#6366f1;--pi-primary-glow:rgba(99,102,241,0.25);--pi-text-main:#1e293b;--pi-text-muted:#64748b;--pi-bg-surface:#ffffff;--pi-border:#e2e8f0;--pi-error:#ef4444;--pi-radius:12px;--pi-shadow-sm:0 1px 2px 0 rgb(0 0 0/0.05);--pi-shadow-lg:0 10px 15px -3px rgb(0 0 0/0.1);--pi-font:"Inter",-apple-system,BlinkMacSystemFont,sans-serif}
504        .phone-input-group[data-pi-mode="full"],.phone-input-group[data-pi-mode="full"] *{box-sizing:border-box!important;margin:0!important;line-height:normal!important}
505        .phone-input-group[data-pi-mode="full"]{position:relative!important;width:100%!important;max-width:100%!important;font-family:var(--pi-font)!important;display:block!important;float:none!important}
506        .phone-input-group[data-pi-mode="full"] .phone-container{display:flex!important;align-items:center!important;justify-content:flex-start!important;width:100%!important;max-width:100%!important;min-width:0!important;height:56px!important;min-height:56px!important;max-height:56px!important;background:var(--pi-bg-surface)!important;border:1px solid var(--pi-border)!important;border-radius:var(--pi-radius)!important;transition:all .2s!important;box-shadow:var(--pi-shadow-sm)!important;position:relative!important;z-index:10!important;padding:0!important;overflow:visible!important}
507        .phone-input-group[data-pi-mode="full"] .phone-container.focused{border-color:var(--pi-primary)!important;box-shadow:0 0 0 4px var(--pi-primary-glow)!important}
508        .phone-input-group[data-pi-mode="full"] .phone-container.invalid{border-color:var(--pi-error)!important;background-color:#fef2f2!important}
509        .phone-input-group[data-pi-mode="full"] .phone-container.invalid .phone-field{color:var(--pi-error)!important}
510        .phone-input-group[data-pi-mode="full"] .phone-container.disabled{opacity:.6!important;cursor:not-allowed!important;background-color:#f8fafc!important}
511        .phone-input-group[data-pi-mode="full"] .phone-container.disabled .phone-field{cursor:not-allowed!important}
512        .phone-input-group[data-pi-mode="full"] .country-btn{display:flex!important;align-items:center!important;justify-content:center!important;height:100%!important;min-height:56px!important;max-height:56px!important;padding:0 12px 0 16px!important;background:transparent!important;border:none!important;cursor:pointer!important;gap:8px!important;border-radius:var(--pi-radius) 0 0 var(--pi-radius)!important;color:var(--pi-text-main)!important;transition:background .15s!important;flex-shrink:0!important;margin:0!important;line-height:normal!important;font-size:14px!important;text-decoration:none!important}
513        .phone-input-group[data-pi-mode="full"] .country-btn:hover:not(:disabled){background-color:#f1f5f9!important}
514        .phone-input-group[data-pi-mode="full"] .country-btn:disabled{cursor:not-allowed!important;opacity:.6!important}
515        .phone-input-group[data-pi-mode="full"] .selected-flag{width:24px!important;min-width:24px!important;max-width:24px!important;height:16px!important;min-height:16px!important;max-height:16px!important;object-fit:cover!important;border-radius:3px!important;box-shadow:0 0 0 1px rgba(0,0,0,0.05)!important;display:block!important;vertical-align:middle!important}
516        .phone-input-group[data-pi-mode="full"] .chevron{width:14px!important;min-width:14px!important;max-width:14px!important;height:14px!important;min-height:14px!important;max-height:14px!important;opacity:.5!important;transition:transform .2s!important;display:block!important;flex-shrink:0!important}
517        .phone-input-group[data-pi-mode="full"] .country-btn[aria-expanded=true] .chevron{transform:rotate(180deg)!important}
518        .phone-input-group[data-pi-mode="full"] .divider{width:1px!important;min-width:1px!important;max-width:1px!important;height:24px!important;background-color:var(--pi-border)!important;margin:0 4px!important;flex-shrink:0!important;border:none!important}
519        .phone-input-group[data-pi-mode="full"] .phone-field{flex:1!important;height:100%!important;min-height:54px!important;border:none!important;background:transparent!important;padding:0 16px!important;font-size:16px!important;color:var(--pi-text-main)!important;font-weight:500!important;border-radius:0 var(--pi-radius) var(--pi-radius) 0!important;font-family:inherit!important;box-shadow:none!important;width:auto!important;max-width:100%!important;min-width:0!important;text-align:left!important;appearance:none!important}
520        .phone-input-group[data-pi-mode="full"] .phone-field:focus{outline:none!important;box-shadow:none!important}
521        .phone-input-group[data-pi-mode="full"] .phone-field::placeholder{color:#cbd5e1!important;opacity:1!important}
522        .phone-input-group[data-pi-mode="full"] .country-dropdown{position:absolute!important;top:calc(100% + 8px)!important;left:0!important;width:100%!important;max-width:100%!important;max-height:320px!important;background:white!important;border:1px solid var(--pi-border)!important;border-radius:var(--pi-radius)!important;box-shadow:var(--pi-shadow-lg)!important;z-index:50!important;opacity:0!important;visibility:hidden!important;transform:translateY(-8px) scale(.99)!important;transition:all .2s!important;display:flex!important;flex-direction:column!important;overflow:hidden!important}
523        .phone-input-group[data-pi-mode="full"] .country-dropdown.open{opacity:1!important;visibility:visible!important;transform:translateY(0) scale(1)!important}
524        .phone-input-group[data-pi-mode="full"] .search-container{padding:12px!important;border-bottom:1px solid var(--pi-border)!important;background:white!important;margin:0!important;flex-shrink:0!important}
525        .phone-input-group[data-pi-mode="full"] .search-input{width:100%!important;max-width:100%!important;padding:10px 12px!important;font-size:14px!important;border:1px solid var(--pi-border)!important;border-radius:8px!important;background-color:#f8fafc!important;outline:none!important;font-family:inherit!important;color:var(--pi-text-main)!important;height:auto!important;min-height:0!important;appearance:none!important}
526        .phone-input-group[data-pi-mode="full"] .search-input:focus{border-color:var(--pi-primary)!important;box-shadow:0 0 0 3px var(--pi-primary-glow)!important;background:white!important}
527        .phone-input-group[data-pi-mode="full"] .country-list{list-style:none!important;padding:0!important;margin:0!important;overflow-y:auto!important;flex:1!important;max-height:100%!important}
528        .phone-input-group[data-pi-mode="full"] .country-list::-webkit-scrollbar{width:6px!important}
529        .phone-input-group[data-pi-mode="full"] .country-list::-webkit-scrollbar-thumb{background-color:#cbd5e1!important;border-radius:4px!important}
530        .phone-input-group[data-pi-mode="full"] .country-item{display:flex!important;align-items:center!important;padding:10px 16px!important;cursor:pointer!important;transition:background .1s!important;font-size:14px!important;border:none!important;background:transparent!important;list-style:none!important;text-decoration:none!important}
531        .phone-input-group[data-pi-mode="full"] .country-item:hover,.phone-input-group[data-pi-mode="full"] .country-item.highlight{background-color:#f1f5f9!important}
532        .phone-input-group[data-pi-mode="full"] .country-item.selected{background-color:#eef2ff!important}
533        .phone-input-group[data-pi-mode="full"] .country-item img{width:20px!important;min-width:20px!important;height:14px!important;min-height:14px!important;margin-right:12px!important;border-radius:2px!important;box-shadow:0 1px 2px rgba(0,0,0,0.1)!important;object-fit:cover!important;display:block!important;vertical-align:middle!important}
534        .phone-input-group[data-pi-mode="full"] .country-name{flex:1!important;font-weight:500!important;color:var(--pi-text-main)!important;text-align:left!important}
535        .phone-input-group[data-pi-mode="full"] .country-dial{color:var(--pi-text-muted)!important;font-family:monospace!important;font-size:13px!important;text-align:right!important}
536        .phone-input-group[data-pi-mode="full"] .validation-msg{display:flex!important;align-items:center!important;gap:8px!important;font-size:13px!important;color:var(--pi-error)!important;margin-top:8px!important;opacity:0!important;transform:translateY(-5px)!important;transition:all .2s!important;height:0!important;overflow:hidden!important}
537        .phone-input-group[data-pi-mode="full"] .validation-msg svg{width:14px!important;height:14px!important;flex-shrink:0!important}
538        .phone-
538input-group[data-pi-mode="full"] .phone-container.invalid~.validation-msg{opacity:1!important;transform:translateY(0)!important;height:auto!important;margin-bottom:16px!important}`;
539      document.head.appendChild(style);
540    }
541  }
542}
543
544// Store instances for cleanup
545const advancedTelInstances = new WeakMap();
546
547/**
548 * Initialize AdvancedTelField on an input element
549 * @param {HTMLElement} input
550 * @returns {AdvancedTelField|null}
551 */
552function initAdvancedTelField(input) {
553  if (!input || input.dataset.phoneInputInitialized === 'true') return null;
554
555  const instance = new AdvancedTelField(input);
556  if (instance.wrapper) {
557    advancedTelInstances.set(input, instance);
558  }
559  return instance;
560}
561
562/**
563 * Destroy AdvancedTelField instance for an input element
564 * @param {HTMLElement} input
565 */
566function destroyAdvancedTelField(input) {
567  if (!input) return;
568
569  const instance = advancedTelInstances.get(input);
570  if (instance) {
571    instance.destroy();
572    advancedTelInstances.delete(input);
573  }
574}
575
576/**
577 * Initialize all AdvancedTelField inputs within a container
578 * @param {HTMLElement} container
579 */
580function initAdvancedTelFieldsIn(container) {
581  if (!container) return;
582
583  container.querySelectorAll('[data-advanced-tel]').forEach(input => {
584    initAdvancedTelField(input);
585  });
586}
587
588/**
589 * Destroy all AdvancedTelField instances within a container
590 * @param {HTMLElement} container
591 */
592function destroyAdvancedTelFieldsIn(container) {
593  if (!container) return;
594
595  container.querySelectorAll('[data-advanced-tel]').forEach(input => {
596    destroyAdvancedTelField(input);
597  });
598}
599
600document.addEventListener('DOMContentLoaded', () => {
601  // Initialize all existing AdvancedTel fields
602  document.querySelectorAll('[data-advanced-tel]').forEach(input => {
603    initAdvancedTelField(input);
604  });
605
606  // Listen for Joomla subform row additions
607  document.addEventListener('subform-row-add', (event) => {
608    const row = event.detail?.row;
609    if (row) {
610      // Small delay to ensure DOM is fully updated after subform processes the row
611      requestAnimationFrame(() => {
612        initAdvancedTelFieldsIn(row);
613      });
614    }
615  });
616
617  // Listen for Joomla subform row removals (fires before row is removed)
618  document.addEventListener('subform-row-remove', (event) => {
619    const row = event.detail?.row;
620    if (row) {
621      destroyAdvancedTelFieldsIn(row);
622    }
623  });
624
625  // Also listen for joomla:removed event as a fallback
626  document.addEventListener('joomla:removed', (event) => {
627    const element = event.target;
628    if (element) {
629      // Check if the element itself is an advanced-tel input
630      if (element.hasAttribute?.('data-advanced-tel')) {
631        destroyAdvancedTelField(element);
632      }
633      // Also check for any advanced-tel inputs within the element
634      destroyAdvancedTelFieldsIn(element);
635    }
636  });
637
638  // Listen for joomla:updated event to initialize new fields (e.g., when content is dynamically added)
639  document.addEventListener('joomla:updated', (event) => {
640    const element = event.target;
641    if (element) {
642      // Check if the element itself is an advanced-tel input
643      if (element.hasAttribute?.('data-advanced-tel')) {
644        initAdvancedTelField(element);
645      }
646      // Also check for any advanced-tel inputs within the element
647      initAdvancedTelFieldsIn(element);
648    }
649  });
650});
651
652// Expose functions globally for manual control if needed
653window.AdvancedTelField = AdvancedTelField;
654window.initAdvancedTelField = initAdvancedTelField;
655window.destroyAdvancedTelField = destroyAdvancedTelField;
656window.initAdvancedTelFieldsIn = initAdvancedTelFieldsIn;
657window.destroyAdvancedTelFieldsIn = destroyAdvancedTelFieldsIn;

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.