/** * Vernova - Lightweight Vanilla JS Jalali Date Picker * No jQuery, no eval, no external dependencies. CSP-safe. */ (function () { 'use strict'; // ─── Jalali ↔ Gregorian Conversion ───────────────────────── var JalaliConverter = { gregorianToJalali: function (gy, gm, gd) { var g_d_m = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; var gy2 = (gm > 2) ? (gy + 1) : gy; var days = 355666 + (365 * gy) + Math.floor((gy2 + 3) / 4) - Math.floor((gy2 + 99) / 100) + Math.floor((gy2 + 399) / 400) + gd + g_d_m[gm - 1]; var jy = -1595 + (33 * Math.floor(days / 12053)); days %= 12053; jy += 4 * Math.floor(days / 1461); days %= 1461; if (days > 365) { jy += Math.floor((days - 1) / 365); days = (days - 1) % 365; } var jm, jd; if (days < 186) { jm = 1 + Math.floor(days / 31); jd = 1 + (days % 31); } else { jm = 7 + Math.floor((days - 186) / 30); jd = 1 + ((days - 186) % 30); } return [jy, jm, jd]; }, jalaliToGregorian: function (jy, jm, jd) { var jy2 = jy - 979; var jm2 = jm - 1; var jd2 = jd - 1; var days = 365 * jy2 + Math.floor(jy2 / 33) * 8 + Math.floor((jy2 % 33 + 3) / 4) + 78 + jd2 + ((jm2 < 7) ? (jm2 * 31) : ((jm2 * 30) + 6)); var gy = 1600 + 400 * Math.floor(days / 146097); days %= 146097; if (days > 36524) { gy += 100 * Math.floor(--days / 36524); days %= 36524; if (days >= 365) days++; } gy += 4 * Math.floor(days / 1461); days %= 1461; if (days > 365) { gy += Math.floor((days - 1) / 365); days = (days - 1) % 365; } var gd = days + 1; var sal_a = [0, 31, ((gy % 4 === 0 && gy % 100 !== 0) || (gy % 400 === 0)) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; var gm; for (gm = 0; gm < 13 && gd > sal_a[gm]; gm++) gd -= sal_a[gm]; return [gy, gm, gd]; }, isJalaliLeap: function (jy) { var breaks = [1, 5, 9, 13, 17, 22, 26, 30]; var r = jy % 33; return breaks.indexOf(r) !== -1; }, jalaliMonthDays: function (jy, jm) { if (jm <= 6) return 31; if (jm <= 11) return 30; return this.isJalaliLeap(jy) ? 30 : 29; }, gregorianMonthDays: function (gy, gm) { var days = [0, 31, ((gy % 4 === 0 && gy % 100 !== 0) || (gy % 400 === 0)) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; return days[gm]; } }; // ─── Jalali Month/Day Names ──────────────────────────────── var jalaliMonths = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']; var jalaliWeekDaysShort = ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج']; var jalaliWeekDaysLong = ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه']; // ─── Pad helper ──────────────────────────────────────────── function pad(n) { return n < 10 ? '0' + n : '' + n; } // ─── Date Picker Class ───────────────────────────────────── function JalaliDatePicker(inputEl, hiddenEl) { this.inputEl = inputEl; this.hiddenEl = hiddenEl; this.container = null; this.isOpen = false; this.currentYear = 1404; this.currentMonth = 1; this.selectedYear = null; this.selectedMonth = null; this.selectedDay = null; this._init(); } JalaliDatePicker.prototype = { _init: function () { // Parse initial value var val = this.inputEl.value; if (val && /^\d{4}\/\d{1,2}\/\d{1,2}$/.test(val)) { var parts = val.split('/'); this.selectedYear = this.currentYear = parseInt(parts[0]); this.selectedMonth = this.currentMonth = parseInt(parts[1]); this.selectedDay = parseInt(parts[2]); } else { // Default to today var now = new Date(); var today = JalaliConverter.gregorianToJalali(now.getFullYear(), now.getMonth() + 1, now.getDate()); this.currentYear = today[0]; this.currentMonth = today[1]; } // Create container this.container = document.createElement('div'); this.container.className = 'jalali-picker-dropdown'; this.container.style.display = 'none'; document.body.appendChild(this.container); // Bind events var self = this; this.inputEl.addEventListener('focus', function (e) { self.open(); }); this.inputEl.addEventListener('click', function (e) { e.stopPropagation(); }); this.container.addEventListener('click', function (e) { e.stopPropagation(); }); // Close on outside click document.addEventListener('click', function (e) { if (self.isOpen && e.target !== self.inputEl && !self.container.contains(e.target)) { self.close(); } }); // Handle manual input this.inputEl.addEventListener('blur', function () { self._syncFromInput(); }); // Handle keyboard this.inputEl.addEventListener('keydown', function (e) { if (e.key === 'Escape') self.close(); if (e.key === 'Enter') { self._syncFromInput(); self.close(); } }); }, _syncFromInput: function () { var val = this.inputEl.value.trim(); if (/^\d{4}\/\d{1,2}\/\d{1,2}$/.test(val)) { var parts = val.split('/'); var jy = parseInt(parts[0]), jm = parseInt(parts[1]), jd = parseInt(parts[2]); var maxDay = JalaliConverter.jalaliMonthDays(jy, jm); if (jd > maxDay) jd = maxDay; this.selectedYear = jy; this.selectedMonth = jm; this.selectedDay = jd; this.inputEl.value = pad(jy) + '/' + pad(jm) + '/' + pad(jd); this._updateHidden(); } else if (val === '') { this.selectedYear = null; this.selectedMonth = null; this.selectedDay = null; if (this.hiddenEl) this.hiddenEl.value = ''; } }, _updateHidden: function () { if (this.selectedYear && this.selectedMonth && this.selectedDay) { var g = JalaliConverter.jalaliToGregorian(this.selectedYear, this.selectedMonth, this.selectedDay); var gregStr = pad(g[0]) + '-' + pad(g[1]) + '-' + pad(g[2]); if (this.hiddenEl) this.hiddenEl.value = gregStr; } }, open: function () { this._render(); this.container.style.display = 'block'; this.isOpen = true; this._position(); }, close: function () { this.container.style.display = 'none'; this.isOpen = false; }, _position: function () { var rect = this.inputEl.getBoundingClientRect(); var scrollTop = window.pageYOffset || document.documentElement.scrollTop; var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; var pickerHeight = this.container.offsetHeight; var spaceBelow = window.innerHeight - rect.bottom; var spaceAbove = rect.top; this.container.style.position = 'absolute'; if (spaceBelow < pickerHeight && spaceAbove > spaceBelow) { this.container.style.top = (rect.top + scrollTop - pickerHeight - 4) + 'px'; } else { this.container.style.top = (rect.bottom + scrollTop + 4) + 'px'; } // RTL: align to right edge this.container.style.left = (rect.right + scrollLeft - this.container.offsetWidth) + 'px'; }, _render: function () { var self = this; var y = this.currentYear; var m = this.currentMonth; // First day of month in Gregorian var firstGreg = JalaliConverter.jalaliToGregorian(y, m, 1); var firstDate = new Date(firstGreg[0], firstGreg[1] - 1, firstGreg[2]); // Day of week: 0=Sun, adjust to Saturday=0 for Jalali var dow = firstDate.getDay(); // 0=Sun var jalaliDow = (dow + 1) % 7; // 0=Sat var daysInMonth = JalaliConverter.jalaliMonthDays(y, m); // Today var now = new Date(); var todayJ = JalaliConverter.gregorianToJalali(now.getFullYear(), now.getMonth() + 1, now.getDate()); var html = '
'; html += ''; html += '' + y + ' ' + jalaliMonths[m - 1] + ''; html += ''; html += '
'; // Week day headers html += '
'; for (var i = 0; i < 7; i++) { html += '' + jalaliWeekDaysShort[i] + ''; } html += '
'; // Days grid html += '
'; // Empty cells before first day for (var i = 0; i < jalaliDow; i++) { html += ''; } // Day cells for (var d = 1; d <= daysInMonth; d++) { var classes = 'jp-day'; if (y === todayJ[0] && m === todayJ[1] && d === todayJ[2]) classes += ' jp-today'; if (y === this.selectedYear && m === this.selectedMonth && d === this.selectedDay) classes += ' jp-selected'; html += ''; } html += '
'; // Footer html += ''; this.container.innerHTML = html; // Bind navigation this.container.querySelector('.jp-prev').addEventListener('click', function () { self._navigate(-1); }); this.container.querySelector('.jp-next').addEventListener('click', function () { self._navigate(1); }); this.container.querySelector('.jp-today-btn').addEventListener('click', function () { self._goToday(); }); // Bind day clicks var dayBtns = this.container.querySelectorAll('.jp-day:not(.jp-empty)'); for (var i = 0; i < dayBtns.length; i++) { dayBtns[i].addEventListener('click', function () { var day = parseInt(this.getAttribute('data-day')); self._selectDay(self.currentYear, self.currentMonth, day); }); } }, _navigate: function (dir) { this.currentMonth += dir; if (this.currentMonth > 12) { this.currentMonth = 1; this.currentYear++; } else if (this.currentMonth < 1) { this.currentMonth = 12; this.currentYear--; } this._render(); this._position(); }, _selectDay: function (y, m, d) { this.selectedYear = y; this.selectedMonth = m; this.selectedDay = d; this.inputEl.value = pad(y) + '/' + pad(m) + '/' + pad(d); this._updateHidden(); this.close(); // Trigger change event var event = new Event('change', { bubbles: true }); this.inputEl.dispatchEvent(event); if (this.hiddenEl) { this.hiddenEl.dispatchEvent(new Event('change', { bubbles: true })); } }, _goToday: function () { var now = new Date(); var today = JalaliConverter.gregorianToJalali(now.getFullYear(), now.getMonth() + 1, now.getDate()); this._selectDay(today[0], today[1], today[2]); } }; // ─── Initialize all date pickers ─────────────────────────── function initPickers() { var inputs = document.querySelectorAll('.jalali-datepicker-input'); for (var i = 0; i < inputs.length; i++) { var input = inputs[i]; if (input._jalaliPickerInitialized) continue; var hiddenId = input.getAttribute('data-gregorian-target'); var hidden = hiddenId ? document.getElementById(hiddenId) : null; new JalaliDatePicker(input, hidden); input._jalaliPickerInitialized = true; } } // Run on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initPickers); } else { initPickers(); } })();