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.