1/** 2 * DatePicker â Custom calendar widget dùng chung cho TẤT CẢ modules 3 * 4 * Auto-init: Tá»± Äá»ng attach và o tất cả .date-picker-trigger[data-target] 5 * 6 * Format output: dd-mm-yyyy 7 * 8 * Usage: 9 * <input type="text" id="myDate"> 10 * <button class="date-picker-trigger" data-target="myDate">ð </button> 11 */ 12 13class DatePicker { 14 constructor(triggerBtn) { 15 this.trigger = triggerBtn; 16 this.targetId = triggerBtn.dataset.target; 17 this.input = document.getElementById(this.targetId); 18 if (!this.input) return; 19 20 this.calendar = null; 21 this.currentMonth = new Date().getMonth(); 22 this.currentYear = new Date().getFullYear(); 23 this.selectedDate = null; 24 this.isOpen = false; 25 26 // Parse existing value 27 this._parseInputValue(); 28 29 // Events 30 this.trigger.addEventListener('click', (e) => { 31 e.preventDefault(); 32 e.stopPropagation(); 33 this.toggle(); 34 }); 35 36 this.input.addEventListener('focus', () => this.open()); 37 38 // Close on outside click 39 document.addEventListener('click', (e) => { 40 if (this.isOpen && !this._isInsideCalendar(e.target) && e.target !== this.input && e.target !== this.trigger) { 41 this.close(); 42 } 43 }); 44 } 45 46 _parseInputValue() { 47 const val = this.input.value.trim(); 48 if (!val) return; 49 50 // Try dd-mm-yyyy or dd/mm/yyyy 51 const match = val.match(/^(\d{1,2})[\-\/](\d{1,2})[\-\/](\d{4})$/); 52 if (match) { 53 const d = parseInt(match[1], 10); 54 const m = parseInt(match[2], 10) - 1; 55 const y = parseInt(match[3], 10); 56 const date = new Date(y, m, d); 57 if (!isNaN(date.getTime())) { 58 this.selectedDate = date; 59 this.currentMonth = m; 60 this.currentYear = y; 61 } 62 } 63 } 64 65 toggle() { 66 this.isOpen ? this.close() : this.open(); 67 } 68 69 open() { 70 if (this.isOpen) return; 71 this._parseInputValue(); 72 this._createCalendar(); 73 this._render(); 74 this.isOpen = true; 75 } 76 77 close() { 78 if (!this.isOpen || !this.calendar) return; 79 this.calendar.remove(); 80 this.calendar = null; 81 this.isOpen = false; 82 } 83 84 _createCalendar() { 85 if (this.calendar) this.calendar.remove(); 86 87 this.calendar = document.createElement('div'); 88 this.calendar.className = 'dp-calendar'; 89 this.calendar.setAttribute('role', 'dialog'); 90 this.calendar.setAttribute('aria-label', 'Chá»n ngà y'); 91 92 // Position relative to input wrapper 93 const wrapper = this.input.closest('.date-picker-wrapper'); 94 if (wrapper) { 95 wrapper.style.position = 'relative'; 96 wrapper.appendChild(this.calendar); 97 } else { 98 this.input.parentNode.style.position = 'relative'; 99 this.input.parentNode.appendChild(this.calendar); 100 } 101 } 102 103 _render() { 104 if (!this.calendar) return; 105 106 const today = new Date(); 107 today.setHours(0, 0, 0, 0); 108 109 const monthNames = [ 110 'Tháng 1', 'Tháng 2', 'Tháng 3', 'Tháng 4', 'Tháng 5', 'Tháng 6', 111 'Tháng 7', 'Tháng 8', 'Tháng 9', 'Tháng 10', 'Tháng 11', 'Tháng 12' 112 ]; 113 const dayHeaders = ['T2', 'T3', 'T4', 'T5', 'T6', 'T7', 'CN']; 114 115 // First day of month 116 const firstDay = new Date(this.currentYear, this.currentMonth, 1); 117 const lastDay = new Date(this.currentYear, this.currentMonth + 1, 0); 118 const daysInMonth = lastDay.getDate(); 119 120 // Day of week for first day (0=Sun, convert to Mon-based) 121 let startDayOfWeek = firstDay.getDay(); // 0=Sun 122 startDayOfWeek = startDayOfWeek === 0 ? 6 : startDayOfWeek - 1; // Mon=0 123 124 // Previous month days to fill 125 const prevMonthLast = new Date(this.currentYear, this.currentMonth, 0).getDate(); 126 127 let html = ''; 128 129 // Header: nav + month/year 130 html += '<div class="dp-header">'; 131 html += `<button type="button" class="dp-nav-btn dp-prev" aria-label="Tháng trưá»c">‹</button>`; 132 html += `<span class="dp-month-year">${monthNames[this.currentMonth]} ${this.currentYear}</span>`;
133 html += `<button type="button" class="dp-nav-btn dp-next" aria-label="Tháng sau">›</button>`; 134 html += '</div>'; 135 136 // Weekday headers 137 html += '<div class="dp-weekdays">'; 138 dayHeaders.forEach(d => { html += `<span class="dp-weekday">${d}</span>`; }); 139 html += '</div>'; 140 141 // Days grid 142 html += '<div class="dp-days">'; 143 144 // Previous month trailing days 145 for (let i = startDayOfWeek - 1; i >= 0; i--) { 146 const d = prevMonthLast - i; 147 html += `<button type="button" class="dp-day dp-day--other" data-date="${this.currentYear}-${this.currentMonth}-${d}" tabindex="-1">${d}</button>`; 148 } 149 150 // Current month days 151 for (let d = 1; d <= daysInMonth; d++) { 152 const date = new Date(this.currentYear, this.currentMonth, d); 153 let cls = 'dp-day'; 154 155 if (date.getTime() === today.getTime()) cls += ' dp-day--today'; 156 if (this.selectedDate && date.getTime() === this.selectedDate.getTime()) cls += ' dp-day--selected'; 157 if (date.getDay() === 0) cls += ' dp-day--sunday'; 158 159 const dateStr = `${this.currentYear}-${this.currentMonth + 1}-${d}`; 160 html += `<button type="button" class="${cls}" data-date="${dateStr}">${d}</button>`; 161 } 162 163 // Next month leading days 164 const totalCells = startDayOfWeek + daysInMonth; 165 const remaining = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7); 166 for (let d = 1; d <= remaining; d++) { 167 html += `<button type="button" class="dp-day dp-day--other" tabindex="-1">${d}</button>`; 168 } 169 170 html += '</div>'; 171 172 // Footer 173 html += '<div class="dp-footer">'; 174 html += '<button type="button" class="dp-today-btn">Hôm nay</button>'; 175 html += '<button type="button" class="dp-close-btn">Äóng</button>'; 176 html += '</div>'; 177 178 this.calendar.innerHTML = html; 179 180 // Attach events 181 this.calendar.querySelector('.dp-prev').addEventListener('click', (e) => { 182 e.preventDefault(); 183 this._prevMonth(); 184 }); 185 this.calendar.querySelector('.dp-next').addEventListener('click', (e) => { 186 e.preventDefault(); 187 this._nextMonth(); 188 }); 189 this.calendar.querySelector('.dp-today-btn').addEventListener('click', (e) => { 190 e.preventDefault(); 191 this._selectDate(today); 192 }); 193 this.calendar.querySelector('.dp-close-btn').addEventListener('click', (e) => { 194 e.preventDefault(); 195 this.close(); 196 }); 197 198 // Day click 199 this.calendar.querySelectorAll('.dp-day:not(.dp-day--other)').forEach(btn => { 200 btn.addEventListener('click', (e) => { 201 e.preventDefault(); 202 const parts = btn.dataset.date.split('-'); 203 const date = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); 204 this._selectDate(date); 205 }); 206 }); 207 } 208 209 _selectDate(date) { 210 this.selectedDate = date; 211 const dd = String(date.getDate()).padStart(2, '0'); 212 const mm = String(date.getMonth() + 1).padStart(2, '0'); 213 const yyyy = date.getFullYear(); 214 this.input.value = `${dd}-${mm}-${yyyy}`; 215 this.input.dispatchEvent(new Event('change', { bubbles: true })); 216 this.close(); 217 } 218 219 _prevMonth() { 220 this.currentMonth--; 221 if (this.currentMonth < 0) { 222 this.currentMonth = 11; 223 this.currentYear--; 224 } 225 this._render(); 226 } 227 228 _nextMonth() { 229 this.currentMonth++; 230 if (this.currentMonth > 11) { 231 this.currentMonth = 0; 232 this.currentYear++; 233 } 234 this._render(); 235 } 236 237 _isInsideCalendar(el) { 238 if (!this.calendar) return false; 239 return this.calendar.contains(el) || el === this.trigger; 240 } 241} 242 243// Compatibility wrapper to prevent errors on legacy pages 244window.initDatePicker = function(input) { 245 if (!input) return; 246 const container = input.parentElement; 247 if (container) { 248 const trigger = container.querySelector('.date-picker-trigger'); 249 if (trigger && !trigger.dataset.initialized) { 250 trigger.dataset.initialized = 'true'; 251 new DatePicker(trigger); 252 } 253 } 254}; 255 256// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 257// AUTO-INIT â Tá»± Äá»ng khá»i tạo cho tất cả date picker triggers 258// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 259document.addEventListener('DOMContentLoaded', () => { 260 // 1. Auto-detect calendar icons inside input groups and wrap them in button triggers 261 const calendarIcons = document.querySelectorAll('i.fa-calendar-days, i.fa-calendar, i.fa-calendar-day'); 262 calendarIcons.forEach(icon => { 263 const container = icon.parentElement; 264 if (!container) return; 265 266 const input = container.querySelector('input[type="text"]'); 267 if (!input) return; 268 269 // Skip if input is handled by flatpickr or another picker 270 if (input.classList.contains('datepicker') || input.classList.contains('date-picker-input')) { 271 return; 272 } 273 274 // If there's already a trigger element that is a button/link, skip 275 if (container.querySelector('.date-picker-trigger:not(i)')) { 276 return; 277 } 278 279 if (!input.id) { 280 input.id = 'dp_input_' + Math.random().toString(36).substr(2, 9); 281 } 282 283 // Detect positioning classes from the icon 284 let isLeft = false;
285 const originalClasses = []; 286 icon.classList.forEach(cls => { 287 if (cls === 'absolute' || cls.startsWith('left-') || cls.startsWith('right-') || cls.startsWith('top-') || cls === 'transform' || cls.startsWith('-translate-') || cls === 'pointer-events-none' || cls === 'cursor-pointer') { 288 originalClasses.push(cls); 289 if (cls.startsWith('left-') || cls === 'left-3') { 290 isLeft = true; 291 } 292 } 293 }); 294 295 // Create the button trigger wrapper 296 const button = document.createElement('button'); 297 button.type = 'button'; 298 button.className = 'date-picker-trigger'; 299 300 // Transfer positioning classes to the button, remove from icon 301 originalClasses.forEach(cls => { 302 if (cls !== 'pointer-events-none') { 303 button.classList.add(cls); 304 } 305 icon.classList.remove(cls); 306 }); 307 308 // Add visual styling classes to button 309 button.classList.add('cursor-pointer'); 310 311 // Prevent layout styling conflicts with standard CSS (.date-picker-trigger specifies right: 4px) 312 if (isLeft) { 313 button.style.right = 'auto'; 314 } else { 315 button.style.left = 'auto'; 316 } 317 318 button.setAttribute('data-target', input.id); 319 button.setAttribute('aria-label', 'Chá»n ngà y'); 320 321 // Wrap the icon inside the button 322 icon.parentNode.insertBefore(button, icon); 323 button.appendChild(icon); 324 }); 325 326 // 2. Initialize all triggers 327 document.querySelectorAll('.date-picker-trigger[data-target]').forEach(btn => { 328 if (!btn.dataset.initialized) { 329 btn.dataset.initialized = 'true'; 330 new DatePicker(btn); 331 } 332 }); 333});
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.