Files
2026-07-28 02:04:36 +03:30

282 lines
9.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
"use strict";
var monthNames = [
"",
"فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور",
"مهر", "آبان", "آذر", "دی", "بهمن", "اسفند"
];
var weekNames = ["ش", "ی", "د", "س", "چ", "پ", "ج"];
var openPicker = null;
function div(a, b) {
return Math.floor(a / b);
}
function gregorianToJalali(gy, gm, gd) {
var gdm = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
var gy2 = gy + (gm > 2 ? 1 : 0);
var days = 355666 + 365 * gy + div(gy2 + 3, 4) - div(gy2 + 99, 100) +
div(gy2 + 399, 400) + gd + gdm[gm - 1];
var jy = -1595 + 33 * div(days, 12053);
days %= 12053;
jy += 4 * div(days, 1461);
days %= 1461;
if (days > 365) {
jy += div(days - 1, 365);
days = (days - 1) % 365;
}
if (days < 186) {
return { year: jy, month: 1 + div(days, 31), day: 1 + (days % 31) };
}
return { year: jy, month: 7 + div(days - 186, 30), day: 1 + ((days - 186) % 30) };
}
function jalaliToGregorian(jy, jm, jd) {
jy += 1595;
var days = -355668 + 365 * jy + div(jy, 33) * 8 + div((jy % 33) + 3, 4) + jd;
days += jm < 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186;
var gy = 400 * div(days, 146097);
days %= 146097;
if (days > 36524) {
gy += 100 * div(days - 1, 36524);
days = (days - 1) % 36524;
if (days >= 365) {
days += 1;
}
}
gy += 4 * div(days, 1461);
days %= 1461;
if (days > 365) {
gy += div(days - 1, 365);
days = (days - 1) % 365;
}
var gd = days + 1;
var lengths = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if ((gy % 4 === 0 && gy % 100 !== 0) || gy % 400 === 0) {
lengths[2] = 29;
}
var gm = 1;
while (gm <= 12 && gd > lengths[gm]) {
gd -= lengths[gm];
gm += 1;
}
return { year: gy, month: gm, day: gd };
}
function todayJalali() {
var now = new Date();
return gregorianToJalali(now.getFullYear(), now.getMonth() + 1, now.getDate());
}
function daysInMonth(year, month) {
if (month <= 6) {
return 31;
}
if (month <= 11) {
return 30;
}
var current = jalaliToGregorian(year, 1, 1);
var next = jalaliToGregorian(year + 1, 1, 1);
var currentUTC = Date.UTC(current.year, current.month - 1, current.day);
var nextUTC = Date.UTC(next.year, next.month - 1, next.day);
return (nextUTC - currentUTC) / 86400000 === 366 ? 30 : 29;
}
function parseDate(value) {
var match = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(value);
if (!match) {
return null;
}
var date = { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) };
if (date.year < 1200 || date.month < 1 || date.month > 12 ||
date.day < 1 || date.day > daysInMonth(date.year, date.month)) {
return null;
}
return date;
}
function formatDate(date) {
return String(date.year).padStart(4, "0") + "-" +
String(date.month).padStart(2, "0") + "-" +
String(date.day).padStart(2, "0");
}
function sameDate(a, b) {
return a && b && a.year === b.year && a.month === b.month && a.day === b.day;
}
function JalaliPicker(input) {
this.input = input;
this.field = input.closest(".jalali-field");
this.trigger = this.field.querySelector(".jalali-trigger");
this.selected = parseDate(input.value);
var initial = this.selected || todayJalali();
this.year = initial.year;
this.month = initial.month;
this.popup = document.createElement("div");
this.popup.className = "jalali-picker";
this.popup.hidden = true;
this.popup.setAttribute("role", "dialog");
this.popup.setAttribute("aria-label", "Persian date picker");
this.popup.setAttribute("dir", "rtl");
this.field.appendChild(this.popup);
this.trigger.addEventListener("click", this.toggle.bind(this));
this.input.addEventListener("focus", this.open.bind(this));
this.input.addEventListener("change", this.sync.bind(this));
this.input.addEventListener("keydown", this.onKeyDown.bind(this));
}
JalaliPicker.prototype.sync = function () {
var parsed = parseDate(this.input.value);
this.input.setCustomValidity(this.input.value && !parsed ? "Use a valid Persian date in YYYY-MM-DD format." : "");
if (parsed) {
this.selected = parsed;
this.year = parsed.year;
this.month = parsed.month;
if (!this.popup.hidden) {
this.render();
}
}
};
JalaliPicker.prototype.onKeyDown = function (event) {
if (event.key === "Escape") {
this.close();
this.input.blur();
} else if (event.key === "ArrowDown" && this.popup.hidden) {
event.preventDefault();
this.open();
}
};
JalaliPicker.prototype.toggle = function () {
if (this.popup.hidden) {
this.open();
} else {
this.close();
}
};
JalaliPicker.prototype.open = function () {
if (openPicker && openPicker !== this) {
openPicker.close();
}
this.sync();
this.popup.hidden = false;
this.field.classList.add("picker-open");
this.trigger.setAttribute("aria-expanded", "true");
openPicker = this;
this.render();
};
JalaliPicker.prototype.close = function () {
this.popup.hidden = true;
this.field.classList.remove("picker-open");
this.trigger.setAttribute("aria-expanded", "false");
if (openPicker === this) {
openPicker = null;
}
};
JalaliPicker.prototype.moveMonth = function (amount) {
this.month += amount;
if (this.month < 1) {
this.month = 12;
this.year -= 1;
} else if (this.month > 12) {
this.month = 1;
this.year += 1;
}
this.render();
};
JalaliPicker.prototype.choose = function (day) {
this.selected = { year: this.year, month: this.month, day: day };
this.input.value = formatDate(this.selected);
this.input.setCustomValidity("");
this.input.dispatchEvent(new Event("input", { bubbles: true }));
this.input.dispatchEvent(new Event("change", { bubbles: true }));
if (this.input.dataset.jalaliRole === "start") {
var end = document.querySelector('[data-jalali-role="end"]');
var endDate = end && parseDate(end.value);
if (end && (!endDate || end.value < this.input.value)) {
end.value = this.input.value;
end.dispatchEvent(new Event("change", { bubbles: true }));
}
}
this.close();
this.input.focus();
};
JalaliPicker.prototype.render = function () {
var picker = this;
var today = todayJalali();
var selected = parseDate(this.input.value) || this.selected;
var firstGregorian = jalaliToGregorian(this.year, this.month, 1);
var offset = (new Date(firstGregorian.year, firstGregorian.month - 1, firstGregorian.day).getDay() + 1) % 7;
var count = daysInMonth(this.year, this.month);
this.popup.innerHTML =
'<div class="jalali-picker-head">' +
'<button type="button" data-next aria-label="ماه بعد"></button>' +
'<strong>' + monthNames[this.month] + ' <span>' + this.year + '</span></strong>' +
'<button type="button" data-prev aria-label="ماه قبل"></button>' +
'</div>' +
'<div class="jalali-weekdays">' +
weekNames.map(function (name) { return "<span>" + name + "</span>"; }).join("") +
'</div>' +
'<div class="jalali-days"></div>' +
'<button type="button" class="jalali-today">امروز · ' + formatDate(today) + "</button>";
var days = this.popup.querySelector(".jalali-days");
for (var blank = 0; blank < offset; blank += 1) {
var spacer = document.createElement("span");
spacer.className = "jalali-blank";
days.appendChild(spacer);
}
for (var day = 1; day <= count; day += 1) {
var date = { year: this.year, month: this.month, day: day };
var button = document.createElement("button");
button.type = "button";
button.textContent = String(day);
button.dataset.day = String(day);
if ((offset + day - 1) % 7 === 6) {
button.classList.add("friday");
}
if (sameDate(date, today)) {
button.classList.add("today");
}
if (sameDate(date, selected)) {
button.classList.add("selected");
button.setAttribute("aria-current", "date");
}
button.addEventListener("click", function () {
picker.choose(Number(this.dataset.day));
});
days.appendChild(button);
}
this.popup.querySelector("[data-prev]").addEventListener("click", function () { picker.moveMonth(-1); });
this.popup.querySelector("[data-next]").addEventListener("click", function () { picker.moveMonth(1); });
this.popup.querySelector(".jalali-today").addEventListener("click", function () {
picker.year = today.year;
picker.month = today.month;
picker.choose(today.day);
});
};
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll("[data-jalali-picker]").forEach(function (input) {
new JalaliPicker(input);
});
document.addEventListener("pointerdown", function (event) {
if (openPicker && !openPicker.field.contains(event.target)) {
openPicker.close();
}
});
});
})();