282 lines
9.1 KiB
JavaScript
282 lines
9.1 KiB
JavaScript
(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();
|
||
}
|
||
});
|
||
});
|
||
})();
|