PageSourceSearch

https://xemketqua.net/assets/js/date-picker.js?v=1780248964

js xemketqua.net collected 2026-10-10 00:12:36 UTC 12,109 bytes, 333 lines download raw bytes

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">&lsaquo;</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">&rsaquo;</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.