1"use strict"; 2 3(function($) { 4 if (typeof $ === "undefined" || typeof window.jQuery === "undefined") { 5 return; 6 } 7 8 // Constants 9 const DEFAULTS = { 10 LOCALE: "en", 11 DATE_FORMAT: "Y-m-d", 12 SELECTOR: ".datepicker, input[type='date']", 13 INIT_DELAY: 100, 14 RETRY_DELAY: 100, 15 DEBOUNCE_DELAY: 150 16 }; 17 18 const DATE_FORMATS = { 19 ISO: "Y-m-d", 20 DMY_SLASH: "d/m/Y", 21 MDY_SLASH: "m/d/Y", 22 DMY_DOT: "d.m.Y", 23 DMY_DASH: "d-m-Y", 24 US_LONG: "F j, Y", 25 JP_CN: "Yå¹´mædæ¥", 26 KO: "Yë mì dì¼" 27 }; 28 29 // Locale groups Äá» giảm repetition 30 const LOCALE_GROUPS = { 31 CJK: ["zh", "zh-cn", "zh-tw", "zh_CN", "zh_TW", "ja"], 32 KOREAN: ["ko", "ko-kr", "ko_KR"], 33 DMY_SLASH: ["vi", "vn", "th", "id", "fr", "es", "it", "pt", "tr", "ar", "he", "af", "sw", "bn"], 34 DMY_DOT: ["de", "ru", "pl"], 35 DMY_DASH: ["nl"], 36 SPANISH: ["es-mx", "es-ar", "es-co", "es-cl", "es-pe", "es-ve", "es-cr", "es-ec", "es-gt", "es-pa", "es-do", "es-cu", "es-bo", "es-hn", "es-ni", "es-py", "es-sv", "es-uy"], 37 PORTUGUESE: ["pt-br", "pt-pt", "pt_BR", "pt_PT"], 38 AFRICAN: ["af-za", "zu", "xh", "en-ng", "en_NG", "en-zw", "en_ZW"], 39 BENGALI: ["bn-bd", "bn_BD", "bd"] 40 }; 41 42 /** 43 * Utility functions 44 */ 45 const Utils = { 46 /** 47 * Debounce function Äá» tránh call nhiá»u lần 48 */ 49 debounce: function(func, wait) { 50 let timeout; 51 return function executedFunction(...args) { 52 const later = () => { 53 clearTimeout(timeout); 54 func(...args); 55 }; 56 clearTimeout(timeout); 57 timeout = setTimeout(later, wait); 58 }; 59 }, 60 61 /** 62 * Get locale code từ nhiá»u nguá»n 63 */ 64 getLocaleConfig: function() { 65 try { 66 // Priority 1: URL params 67 const urlLocale = this.getUrlParam("flatpickr_locale"); 68 if (urlLocale) { 69 return { locale: urlLocale, source: "url" }; 70 } 71 72 // Priority 2: civi_flatpickr_vars 73 if (typeof civi_flatpickr_vars !== "undefined") { 74 return { 75 locale: civi_flatpickr_vars.locale || DEFAULTS.LOCALE, 76 wpLocale: civi_flatpickr_vars.wp_locale, 77 dateFormat: civi_flatpickr_vars.date_format || DEFAULTS.DATE_FORMAT, 78 source: "flatpickr_vars" 79 }; 80 } 81 82 // Priority 3: civi_template_vars 83 if (typeof civi_template_vars !== "undefined") { 84 return { 85 locale: civi_template_vars.flatpickr_locale || DEFAULTS.LOCALE, 86 wpLocale: civi_template_vars.wp_locale, 87 dateFormat: civi_template_vars.date_format || DEFAULTS.DATE_FORMAT, 88 source: "template_vars" 89 }; 90 } 91 92 // Priority 4: Browser locale 93 const browserLocale = (navigator.language || navigator.userLanguage || "en").substring(0, 2); 94 return { 95 locale: browserLocale, 96 wpLocale: browserLocale, 97 dateFormat: DEFAULTS.DATE_FORMAT, 98 source: "browser" 99 }; 100 } catch (e) { 101 console.error("CiviFlatpickr: Error reading locale config", e); 102 return { 103 locale: DEFAULTS.LOCALE, 104 dateFormat: DEFAULTS.DATE_FORMAT, 105 source: "fallback" 106 }; 107 } 108 }, 109 110 /** 111 * Get URL parameter 112 */ 113 getUrlParam: function(name) { 114 try { 115 if (typeof URLSearchParams !== "undefined") { 116 const urlParams = new URLSearchParams(window.location.search); 117 return urlParams.get(name); 118 } 119 const match = window.location.search.match(new RegExp(`[?&]${name}=([^&]*)`)); 120 return match ? match[1] : null; 121 } catch (e) { 122 return null; 123 } 124 }, 125 126 /** 127 * Check element should be initialized 128 */ 129 shouldInitElement: function($el) { 130 return !$el.data("flatpickr") && 131 !$el.data("flatpickr-initialized") && 132 !$el.hasClass("no-flatpickr"); 133 }, 134 135 /** 136 * Parse date từ string 137 */ 138 parseDate: function(dateStr) { 139 if (!dateStr) return null; 140 141 try { 142 const dateObj = new Date(dateStr); 143 return !isNaN(dateObj.getTime()) ? dateObj : null; 144 } catch (e) { 145 return null; 146 } 147 } 148 }; 149 150 /** 151 * Locale Manager - xá» lý locale và format 152 */ 153 const LocaleManager = { 154 /** 155 * Lấy format dá»±a trên locale 156 */ 157 getFormatByLocale: function(localeCode) { 158 // Normalize locale code 159 const normalizedLocale = localeCode.toLowerCase(); 160 161 // CJK locales 162 if (LOCALE_GROUPS.CJK.some(l =>
162 normalizedLocale.startsWith(l))) { 163 return DATE_FORMATS.JP_CN; 164 } 165 166 // Korean 167 if (LOCALE_GROUPS.KOREAN.some(l => normalizedLocale.startsWith(l))) { 168 return DATE_FORMATS.KO; 169 } 170 171 // DMY with slash 172 if (LOCALE_GROUPS.DMY_SLASH.some(l => normalizedLocale.startsWith(l)) || 173 LOCALE_GROUPS.SPANISH.some(l => normalizedLocale === l) || 174 LOCALE_GROUPS.PORTUGUESE.some(l => normalizedLocale === l) || 175 LOCALE_GROUPS.AFRICAN.some(l => normalizedLocale === l) || 176 LOCALE_GROUPS.BENGALI.some(l => normalizedLocale === l)) { 177 return DATE_FORMATS.DMY_SLASH; 178 } 179 180 // DMY with dot 181 if (LOCALE_GROUPS.DMY_DOT.some(l => normalizedLocale.startsWith(l))) { 182 return DATE_FORMATS.DMY_DOT; 183 } 184 185 // DMY with dash 186 if (LOCALE_GROUPS.DMY_DASH.some(l => normalizedLocale.startsWith(l))) { 187 return DATE_FORMATS.DMY_DASH; 188 } 189 190 // Default 191 return DATE_FORMATS.ISO; 192 }, 193 194 /** 195 * Get flatpickr locale object 196 */ 197 getFlatpickrLocale: function(localeCode, wpLocale) { 198 try { 199 if (localeCode === "en" || typeof flatpickr.l10ns === "undefined") { 200 return null; 201 } 202 203 // Direct match 204 if (flatpickr.l10ns[localeCode]) { 205 return flatpickr.l10ns[localeCode]; 206 } 207 208 // Chinese variations 209 if (localeCode === "zh") { 210 return flatpickr.l10ns["zh-cn"] || flatpickr.l10ns["zh_tw"] || null; 211 } 212 213 // Language family fallback 214 const baseLocale = localeCode.split(/[-_]/)[0]; 215 if (flatpickr.l10ns[baseLocale]) { 216 return flatpickr.l10ns[baseLocale]; 217 } 218 219 // Try wpLocale 220 if (wpLocale && flatpickr.l10ns[wpLocale]) { 221 return flatpickr.l10ns[wpLocale]; 222 } 223 224 return null; 225 } catch (e) { 226 console.warn("CiviFlatpickr: Error loading locale", localeCode, e); 227 return null; 228 } 229 }, 230 231 /** 232 * Convert WP date format to Flatpickr format 233 */ 234 convertDateFormat: function(wpFormat) { 235 if (!wpFormat) return DATE_FORMATS.ISO; 236 237 // Common formats mapping 238 const formatMap = { 239 "F j, Y": DATE_FORMATS.US_LONG, 240 "Y-m-d": DATE_FORMATS.ISO, 241 "Y/m/d": DATE_FORMATS.ISO, 242 "d/m/Y": DATE_FORMATS.DMY_SLASH, 243 "d-m-Y": DATE_FORMATS.DMY_DASH, 244 "m/d/Y": DATE_FORMATS.MDY_SLASH, 245 "m-d-Y": DATE_FORMATS.MDY_SLASH, 246 "d.m.Y": DATE_FORMATS.DMY_DOT 247 }; 248 249 if (formatMap[wpFormat]) { 250 return formatMap[wpFormat]; 251 } 252 253 // Detect separator và order 254 if (wpFormat.includes("/")) { 255 return this.detectFormatOrder(wpFormat, "/"); 256 } else if (wpFormat.includes("-")) { 257 return this.detectFormatOrder(wpFormat, "-"); 258 } else if (wpFormat.includes(".")) { 259 return DATE_FORMATS.DMY_DOT; 260 } 261 262 return DATE_FORMATS.ISO; 263 }, 264 265 /** 266 * Detect date format order (YMD, DMY, MDY) 267 */ 268 detectFormatOrder: function(format, separator) { 269 const yPos = format.indexOf("Y");
270 const mPos = format.indexOf("m"); 271 const dPos = format.indexOf("d"); 272 273 if (yPos < mPos && mPos < dPos) { 274 return `Y${separator}m${separator}d`; 275 } else if (dPos < mPos && mPos < yPos) { 276 return `d${separator}m${separator}Y`; 277 } else if (mPos < dPos && dPos < yPos) { 278 return `m${separator}d${separator}Y`; 279 } 280 281 return DATE_FORMATS.ISO; 282 }, 283 284 /** 285 * Get final date format 286 */ 287 getDateFormat: function(options) { 288 const { localeCode, wpLocale, wpDateFormat, localeObj } = options; 289 290 // Priority 1: Locale object has format 291 if (localeObj?.dateFormat) { 292 return localeObj.dateFormat; 293 } 294 295 // Priority 2: Locale-specific format 296 const localeFormat = this.getFormatByLocale(localeCode); 297 298 // Priority 3: For CJK languages, always use their format 299 if (localeFormat === DATE_FORMATS.JP_CN || localeFormat === DATE_FORMATS.KO) { 300 return localeFormat; 301 } 302 303 // Priority 4: Convert WP format 304 const convertedFormat = this.convertDateFormat(wpDateFormat); 305 306 // Priority 5: US locale with F j, Y format 307 if (wpDateFormat === "F j, Y" && ["en", "en_US", "en_GB"].includes(localeCode)) { 308 return DATE_FORMATS.US_LONG; 309 } 310 311 // Use converted format if compatible, otherwise use locale format 312 return this.isFormatCompatible(wpDateFormat, localeFormat) 313 ? convertedFormat 314 : localeFormat; 315 }, 316 317 /** 318 * Check if formats are compatible 319 */ 320 isFormatCompatible: function(wpFormat, localeFormat) { 321 // ISO vs DMY/MDY incompatible 322 if (wpFormat === "Y-m-d" && (localeFormat.includes("d/m") || localeFormat.includes("m/d"))) { 323 return false; 324 } 325 326 // DMY/MDY vs ISO incompatible 327 if ((wpFormat.includes("d/m") || wpFormat.includes("m/d")) && localeFormat === "Y-m-d") { 328 return false; 329 } 330 331 // CJK formats 332 if (wpFormat.includes("å¹´") || wpFormat.includes("ì")) { 333 return true; 334 } 335 336 return true; 337 } 338 }; 339 340 /** 341 * Main CiviFlatpickr object 342 */ 343 window.CiviFlatpickr = { 344 initialized: false, 345 initInProgress: false, 346 347 /** 348 * Auto initialize 349 */ 350 autoInit: function() { 351 // Check if flatpickr library loaded 352 if (typeof flatpickr === "undefined") { 353 if (!this.waitTimeout) { 354 this.waitTimeout = setTimeout(() => { 355 this.waitTimeout = null; 356 this.autoInit(); 357 }, DEFAULTS.RETRY_DELAY); 358 } 359 return; 360 } 361 362 const config = Utils.getLocaleConfig(); 363 this.init({ 364 localeCode: config.locale, 365 wpLocale: config.wpLocale, 366 wpDateFormat: config.dateFormat || DEFAULTS.DATE_FORMAT, 367 selector: DEFAULTS.SELECTOR 368 }); 369 }, 370 371 /** 372 * Initialize flatpickr 373 */ 374 init: function(options) { 375 // Validation 376 if (typeof flatpickr === "undefined") { 377 console.warn("CiviFlatpickr: flatpickr library not loaded"); 378 return; 379 } 380 381 if (typeof $ === "undefined") { 382 console.warn("CiviFlatpickr: jQuery not loaded"); 383 return; 384 } 385 386 // Prevent concurrent init 387 if (this.initInProgress) { 388 return; 389 } 390 391 this.initInProgress = true; 392 393 try { 394 // Merge options 395 const defaultOptions = { 396 localeCode: DEFAULTS.LOCALE, 397 wpLocale: DEFAULTS.LOCALE, 398 wpDateFormat: DEFAULTS.DATE_FORMAT, 399 selector: DEFAULTS.SELECTOR 400 }; 401 402 options = $.extend({}, defaultOptions, options || {}); 403 404 // Get locale object 405 const localeObj = LocaleManager.getFlatpickrLocale( 406 options.localeCode, 407 options.wpLocale 408 ); 409 410 // Get date format 411 const dateFormat = LocaleManager.getDateFormat({ 412 localeCode: options.localeCode, 413 wpLocale: options.wpLocale, 414 wpDateFormat: options.wpDateFormat, 415 localeObj: localeObj 416 }); 417 418 // Build flatpickr config 419 const flatpickrConfig = this.buildFlatpickrConfig({ 420 dateFormat, 421 localeObj, 422 minDate: options.minDate, 423 maxDate: options.maxDate 424 }); 425 426 // Initialize elements 427 this.initializeElements(options.selector, flatpickrConfig); 428 429 } catch (e) { 430 console.error("CiviFlatpickr: Error in init", e); 431 } finally { 432 this.initInProgress = false; 433 } 434 }, 435 436 /** 437 * Build flatpickr configuration 438 */ 439 buildFlatpickrConfig: function(options) { 440 const { dateFormat, localeObj, minDate, maxDate } = options; 441 442 const config = { 443 dateFormat: dateFormat, 444 allowInput: true, 445 clickOpens: true, 446 animate: true, 447 monthSelectorType: "static", 448 onChange: this.createOnChangeHandler(), 449 onReady: this.createOnReadyHandler() 450 }; 451 452 // Add locale 453 if (localeObj) { 454 config.locale = localeObj; 455 456 if (localeObj.rtl === true) { 457 config.wrap = true; 458 config.position = "auto"; 459 } 460 } 461 462 // Add date constraints 463 if (minDate) config.minDate = minDate; 464 if (maxDate) config.maxDate = maxDate; 465 466 return config; 467 }, 468 469 /**
470 * Create onChange callback 471 */ 472 createOnChangeHandler: function() { 473 return function(selectedDates, dateStr, instance) { 474 try { 475 const $input = $(instance.input); 476 if ($input.length) { 477 $input.val(dateStr).trigger("change"); 478 } 479 } catch (e) { 480 console.warn("CiviFlatpickr: Error in onChange", e); 481 } 482 }; 483 }, 484 485 /** 486 * Create onReady callback 487 */ 488 createOnReadyHandler: function() { 489 return function(selectedDates, dateStr, instance) { 490 try { 491 const $input = $(instance.input); 492 if (!$input.length) return; 493 494 $input.data("flatpickr-initialized", true); 495 496 // Set initial value if exists 497 const originalValue = $input.attr('value') || $input.data('debug-value'); 498 if (originalValue && (!selectedDates || selectedDates.length === 0)) { 499 setTimeout(() => { 500 const dateObj = Utils.parseDate(originalValue); 501 if (dateObj) { 502 instance.setDate(dateObj, true); 503 } 504 }, DEFAULTS.INIT_DELAY); 505 } 506 } catch (e) { 507 console.warn("CiviFlatpickr: Error in onReady", e); 508 } 509 }; 510 }, 511 512 /** 513 * Initialize elements 514 */ 515 initializeElements: function(selector, baseConfig) { 516 try { 517 const $elements = $(selector).filter(function() { 518 return Utils.shouldInitElement($(this)); 519 }); 520 521 if ($elements.length === 0) { 522 return; 523 } 524 525 $elements.each((index, element) => { 526 try { 527 this.initializeSingleElement(element, baseConfig); 528 } catch (e) { 529 console.warn("CiviFlatpickr: Error initializing element", element, e); 530 } 531 }); 532 } catch (e) { 533 console.error("CiviFlatpickr: Error in initializeElements", e); 534 } 535 }, 536 537 /** 538 * Initialize single element 539 */ 540 initializeSingleElement: function(element, baseConfig) { 541 const $input = $(element); 542 const config = { ...baseConfig }; 543 544 // Handle input[type="date"] 545 if ($input.attr("type") === "date") { 546 const minDate = $input.attr("min"); 547 const maxDate = $input.attr("max"); 548 549 $input.attr("type", "text").addClass("datepicker"); 550 551 if (minDate) config.minDate = minDate; 552 if (maxDate) config.maxDate = maxDate; 553 } 554 555 // Set default date 556 const inputValue = $input.val(); 557 if (inputValue) { 558 config.defaultDate = inputValue; 559 } 560 561 // Mobile / touch: native picker path can break inside overflow/transform; use flatpickr on body. 562 const mobileLike = 563 typeof window.matchMedia === "function" && 564 (window.matchMedia("(max-width: 782px)").matches || 565 window.matchMedia("(pointer: coarse)").matches); 566 if (mobileLike && typeof document !== "undefined" && document.body) { 567 config.disableMobile = true; 568 config.appendTo = document.body; 569 } 570 571 // Initialize flatpickr 572 const fpInstance = flatpickr(element, config); 573 574 if (fpInstance) { 575 $input.data("flatpickr", fpInstance); 576 577 // Set value if not already set 578 if (inputValue && !fpInstance.selectedDates.length) { 579 setTimeout(() => { 580 const dateObj = Utils.parseDate(inputValue); 581 if (dateObj) { 582 fpInstance.setDate(dateObj, true); 583 } 584 }, DEFAULTS.INIT_DELAY); 585 } 586 } 587 } 588 }; 589 590 /** 591 * DOM Observer - Quản lý viá»c observe DOM changes 592 */ 593 const DOMObserver = { 594 observer: null, 595 debouncedInit: null, 596 597 init: function() { 598 if (typeof MutationObserver === "undefined") { 599 console.warn("CiviFlatpickr: MutationObserver not supported"); 600 return; 601 } 602 603 // Create debounced init function 604 this.debouncedInit = Utils.debounce(() => { 605 if (typeof window.CiviFlatpickr !== "undefined") { 606 window.CiviFlatpickr.autoInit(); 607 } 608 }, DEFAULTS.DEBOUNCE_DELAY); 609 610 // Create observer 611 this.observer = new MutationObserver((mutations) => { 612 if (this.shouldInit(mutations)) { 613 this.debouncedInit(); 614 } 615 }); 616 617 // Start observing 618 this.observer.observe(document.body, { 619 childList: true, 620 subtree: true 621 }); 622 }, 623 624 shouldInit: function(mutations) { 625 return mutations.some(mutation => { 626 if (!mutation.addedNodes.length) return false; 627 628 return Array.from(mutation.addedNodes).some(node => { 629 if (node.nodeType !== 1) return false;
630 631 const $node = $(node); 632 return $node.is(DEFAULTS.SELECTOR) || 633 $node.find(DEFAULTS.SELECTOR).length > 0; 634 }); 635 }); 636 }, 637 638 disconnect: function() { 639 if (this.observer) { 640 this.observer.disconnect(); 641 } 642 } 643 }; 644 645 /** 646 * Initialize on document ready 647 */ 648 $(document).ready(function() { 649 // Initial init 650 if (typeof window.CiviFlatpickr !== "undefined") { 651 window.CiviFlatpickr.autoInit(); 652 } 653 654 // Setup DOM observer 655 DOMObserver.init(); 656 657 // Handle AJAX complete 658 const debouncedAjaxInit = Utils.debounce(() => { 659 if (typeof window.CiviFlatpickr !== "undefined") { 660 window.CiviFlatpickr.autoInit(); 661 } 662 }, DEFAULTS.DEBOUNCE_DELAY); 663 664 $(document).on("ajaxComplete", debouncedAjaxInit); 665 }); 666 667})(jQuery);
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.