82 lines
5.1 KiB
JavaScript
82 lines
5.1 KiB
JavaScript
/* Local Persian date selector. No libraries, CDN, network or production writes.
|
|
Use Intl's Persian calendar to determine leap Esfand; don't approximate with year % 4.
|
|
Display is Jalali; submission is Gregorian YYYY-MM-DD, date-only (no timezone timestamp). */
|
|
(() => {
|
|
'use strict';
|
|
const day=document.querySelector('#receipt-day');
|
|
if(!day)return;
|
|
const month=document.querySelector('#receipt-month');
|
|
const year=document.querySelector('#receipt-year');
|
|
const output=document.querySelector('#receipt-date-value');
|
|
const status=document.querySelector('#receipt-date-status');
|
|
const todayButton=document.querySelector('#receipt-date-today');
|
|
const monthNames=['فروردین','اردیبهشت','خرداد','تیر','مرداد','شهریور','مهر','آبان','آذر','دی','بهمن','اسفند'];
|
|
const digits=value=>String(value).replace(/\d/g,digit=>'۰۱۲۳۴۵۶۷۸۹'[digit]);
|
|
const formatter=new Intl.DateTimeFormat('en-US',{calendar:'persian',numberingSystem:'latn',timeZone:'Asia/Tehran',year:'numeric',month:'numeric',day:'numeric'});
|
|
function parts(date){
|
|
const values=Object.fromEntries(formatter.formatToParts(date).map(part=>[part.type,part.value]));
|
|
return {year:Number(values.year),month:Number(values.month),day:Number(values.day)};
|
|
}
|
|
function newYear(selectedYear){
|
|
// The native Persian calendar provides the exact first day; bounded March search.
|
|
for(let marchDay=18;marchDay<=24;marchDay++){
|
|
const date=new Date(Date.UTC(selectedYear+621,2,marchDay,12));
|
|
const value=parts(date);
|
|
if(value.year===selectedYear && value.month===1 && value.day===1)return date.getTime();
|
|
}
|
|
throw new Error('Persian new year unavailable');
|
|
}
|
|
const yearDays=new Map();
|
|
function monthLength(selectedYear,selectedMonth){
|
|
if(selectedMonth<=6)return 31;
|
|
if(selectedMonth<=11)return 30;
|
|
if(!yearDays.has(selectedYear))yearDays.set(selectedYear,(newYear(selectedYear+1)-newYear(selectedYear))/86400000);
|
|
return yearDays.get(selectedYear)===366?30:29;
|
|
}
|
|
function addOption(select,value,label){
|
|
const option=document.createElement('option');option.value=String(value);option.textContent=label;select.append(option);
|
|
}
|
|
function updateDays(){
|
|
const previous=day.value;
|
|
const length=year.value && month.value?monthLength(Number(year.value),Number(month.value)):31;
|
|
day.replaceChildren();addOption(day,'','روز');
|
|
for(let value=1;value<=length;value++)addOption(day,value,digits(value));
|
|
const invalid=previous && Number(previous)>length;
|
|
day.value=invalid?'':previous;
|
|
if(invalid){status.textContent='این ماه '+digits(length)+' روز دارد؛ روز پرداخت را دوباره انتخاب کنید.';output.value='';}
|
|
else updateValue();
|
|
}
|
|
function updateValue(){
|
|
if(!day.value || !month.value || !year.value){day.setCustomValidity('');output.value='';status.textContent='روز، ماه و سال پرداخت را کامل انتخاب کنید.';return;}
|
|
const selectedYear=Number(year.value),selectedMonth=Number(month.value),selectedDay=Number(day.value);
|
|
if(!Number.isInteger(selectedYear) || selectedMonth<1 || selectedMonth>12 || selectedDay<1 || selectedDay>monthLength(selectedYear,selectedMonth)){output.value='';day.setCustomValidity('تاریخ معتبر انتخاب کنید.');return;}
|
|
const offset=selectedMonth<=6?(selectedMonth-1)*31:186+(selectedMonth-7)*30;
|
|
// Anchor at UTC noon, add whole calendar days, serialize UTC date: no local midnight drift.
|
|
const gregorian=new Date(newYear(selectedYear)+(offset+selectedDay-1)*86400000);
|
|
const check=parts(gregorian);
|
|
if(check.year!==selectedYear || check.month!==selectedMonth || check.day!==selectedDay){
|
|
output.value='';day.setCustomValidity('تاریخ معتبر انتخاب کنید.');status.textContent='تاریخ معتبر انتخاب کنید.';return;
|
|
}
|
|
day.setCustomValidity('');output.value=gregorian.toISOString().slice(0,10);
|
|
status.textContent=digits(day.value)+' '+monthNames[Number(month.value)-1]+' '+digits(year.value);
|
|
}
|
|
if(formatter.resolvedOptions().calendar!=='persian'){
|
|
status.textContent='این مرورگر تقویم شمسی را پشتیبانی نمیکند. از نسخهٔ جدید مرورگر استفاده کنید.';
|
|
[day,month,year,todayButton].forEach(control=>control.disabled=true);return;
|
|
}
|
|
const today=parts(new Date());
|
|
month.replaceChildren();year.replaceChildren();
|
|
monthNames.forEach((name,index)=>addOption(month,index+1,name));
|
|
for(let value=today.year+1;value>=today.year-10;value--)addOption(year,value,digits(value));
|
|
year.value=String(today.year);month.value=String(today.month);updateDays();
|
|
document.querySelector('#receipt-form').addEventListener('receipt-date:validate',updateValue);
|
|
day.addEventListener('change',updateValue);
|
|
month.addEventListener('change',updateDays);year.addEventListener('change',updateDays);
|
|
todayButton.addEventListener('click',()=>{
|
|
const current=parts(new Date());
|
|
if(![...year.options].some(option=>option.value===String(current.year)))addOption(year,current.year,digits(current.year));
|
|
year.value=String(current.year);month.value=String(current.month);day.value='';updateDays();
|
|
day.value=String(current.day);updateValue();day.focus();
|
|
});
|
|
})();
|