django-npdatetime 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_npdatetime-0.1.0.dist-info/METADATA +397 -0
- django_npdatetime-0.1.0.dist-info/RECORD +23 -0
- django_npdatetime-0.1.0.dist-info/WHEEL +5 -0
- django_npdatetime-0.1.0.dist-info/licenses/LICENSE +21 -0
- django_npdatetime-0.1.0.dist-info/top_level.txt +1 -0
- npdatetime_django/__init__.py +22 -0
- npdatetime_django/apps.py +13 -0
- npdatetime_django/forms.py +239 -0
- npdatetime_django/models.py +198 -0
- npdatetime_django/static/npdatetime_django/css/date_picker.css +746 -0
- npdatetime_django/static/npdatetime_django/js/date_picker.min.js +1381 -0
- npdatetime_django/static/npdatetime_django/js/pkg/README.md +345 -0
- npdatetime_django/static/npdatetime_django/js/pkg/npdatetime_wasm.d.ts +205 -0
- npdatetime_django/static/npdatetime_django/js/pkg/npdatetime_wasm.js +813 -0
- npdatetime_django/static/npdatetime_django/js/pkg/npdatetime_wasm_bg.wasm +0 -0
- npdatetime_django/static/npdatetime_django/js/pkg/npdatetime_wasm_bg.wasm.d.ts +36 -0
- npdatetime_django/static/npdatetime_django/js/pkg/package.json +15 -0
- npdatetime_django/templates/npdatetime_django/widgets/date_picker.html +28 -0
- npdatetime_django/templates/npdatetime_django/widgets/date_range.html +25 -0
- npdatetime_django/templatetags/__init__.py +259 -0
- npdatetime_django/templatetags/nepali_date.py +1 -0
- npdatetime_django/utils.py +131 -0
- npdatetime_django/widgets.py +168 -0
|
@@ -0,0 +1,1381 @@
|
|
|
1
|
+
import init, { NepaliDate } from "./pkg/npdatetime_wasm.js";
|
|
2
|
+
|
|
3
|
+
export class NepaliDatePicker {
|
|
4
|
+
static initialized = false;
|
|
5
|
+
static instances = new Map();
|
|
6
|
+
static daysCache = new Map();
|
|
7
|
+
|
|
8
|
+
constructor(element, options = {}) {
|
|
9
|
+
if (typeof element === "string") {
|
|
10
|
+
element = document.querySelector(element);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!element) {
|
|
14
|
+
throw new Error("Invalid element provided to NepaliDatePicker");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
this.input = element;
|
|
18
|
+
this.id = `npd-${Math.random().toString(36).substr(2, 9)}`;
|
|
19
|
+
|
|
20
|
+
this.options = {
|
|
21
|
+
mode: (element.dataset.mode || options.mode || "BS").toUpperCase(),
|
|
22
|
+
language: (
|
|
23
|
+
element.dataset.language ||
|
|
24
|
+
options.language ||
|
|
25
|
+
"en"
|
|
26
|
+
).toLowerCase(),
|
|
27
|
+
format: options.format || "%Y-%m-%d",
|
|
28
|
+
minDate: options.minDate || null,
|
|
29
|
+
maxDate: options.maxDate || null,
|
|
30
|
+
disabledDates: options.disabledDates || [],
|
|
31
|
+
theme: element.dataset.theme || options.theme || "auto",
|
|
32
|
+
position: options.position || "auto",
|
|
33
|
+
closeOnSelect: options.closeOnSelect !== false,
|
|
34
|
+
showTodayButton: options.showTodayButton !== false,
|
|
35
|
+
showClearButton: options.showClearButton !== false,
|
|
36
|
+
onChange: options.onChange || null,
|
|
37
|
+
onOpen: options.onOpen || null,
|
|
38
|
+
onClose: options.onClose || null,
|
|
39
|
+
...options,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
this.selectedDate = null;
|
|
43
|
+
this.rangeStart = null;
|
|
44
|
+
this.rangeEnd = null;
|
|
45
|
+
|
|
46
|
+
const now = new Date();
|
|
47
|
+
this.selectedTime = {
|
|
48
|
+
hour:
|
|
49
|
+
options.defaultHour !== undefined
|
|
50
|
+
? options.defaultHour
|
|
51
|
+
: now.getHours(),
|
|
52
|
+
minute:
|
|
53
|
+
options.defaultMinute !== undefined
|
|
54
|
+
? options.defaultMinute
|
|
55
|
+
: now.getMinutes(),
|
|
56
|
+
};
|
|
57
|
+
this.viewDate = { year: 2081, month: 1 };
|
|
58
|
+
this.viewMode = "days";
|
|
59
|
+
this.isOpen = false;
|
|
60
|
+
this.switchRequest = null;
|
|
61
|
+
this.renderRequest = null;
|
|
62
|
+
|
|
63
|
+
this.init();
|
|
64
|
+
NepaliDatePicker.instances.set(element, this);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async init() {
|
|
68
|
+
if (!NepaliDatePicker.initialized) {
|
|
69
|
+
await init();
|
|
70
|
+
NepaliDatePicker.initialized = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.setupInput();
|
|
74
|
+
this.createPicker();
|
|
75
|
+
this.attachEvents();
|
|
76
|
+
this.parseInitialValue();
|
|
77
|
+
|
|
78
|
+
if (!this.selectedDate) {
|
|
79
|
+
this.setDetailsFromToday();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setupInput() {
|
|
84
|
+
this.input.setAttribute("autocomplete", "off");
|
|
85
|
+
this.input.setAttribute("data-npd-id", this.id);
|
|
86
|
+
this.input.classList.add("npd-input");
|
|
87
|
+
|
|
88
|
+
if (!this.input.placeholder) {
|
|
89
|
+
this.input.placeholder =
|
|
90
|
+
this.options.mode === "BS" ? "Select Nepali Date" : "Select Date";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
createPicker() {
|
|
95
|
+
const picker = document.createElement("div");
|
|
96
|
+
picker.className = "npd-picker";
|
|
97
|
+
picker.id = this.id;
|
|
98
|
+
picker.setAttribute("role", "dialog");
|
|
99
|
+
picker.setAttribute("aria-modal", "true");
|
|
100
|
+
picker.innerHTML = `
|
|
101
|
+
<div class="npd-header">
|
|
102
|
+
<button type="button" class="npd-title" aria-label="Change view">
|
|
103
|
+
<span class="npd-title-text"></span>
|
|
104
|
+
<svg class="npd-title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
105
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
106
|
+
</svg>
|
|
107
|
+
</button>
|
|
108
|
+
<div class="npd-nav">
|
|
109
|
+
<button type="button" class="npd-nav-btn npd-prev" aria-label="Previous">
|
|
110
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
111
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
112
|
+
</svg>
|
|
113
|
+
</button>
|
|
114
|
+
<button type="button" class="npd-nav-btn npd-next" aria-label="Next">
|
|
115
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
116
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
117
|
+
</svg>
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="npd-body">
|
|
123
|
+
<div class="npd-view npd-view-days"></div>
|
|
124
|
+
<div class="npd-view npd-view-months"></div>
|
|
125
|
+
<div class="npd-view npd-view-years"></div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div class="npd-footer">
|
|
129
|
+
<div class="npd-mode-toggle">
|
|
130
|
+
<button type="button" class="npd-mode-btn ${this.options.mode === "BS" ? "active" : ""}" data-mode="BS">
|
|
131
|
+
<span>BS</span>
|
|
132
|
+
</button>
|
|
133
|
+
<button type="button" class="npd-mode-btn ${this.options.mode === "AD" ? "active" : ""}" data-mode="AD">
|
|
134
|
+
<span>AD</span>
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="npd-actions">
|
|
138
|
+
${this.options.showClearButton ? '<button type="button" class="npd-btn npd-clear">Clear</button>' : ""}
|
|
139
|
+
<button type="button" class="npd-btn npd-yesterday">Yesterday</button>
|
|
140
|
+
${this.options.showTodayButton ? '<button type="button" class="npd-btn npd-today">Today</button>' : ""}
|
|
141
|
+
<button type="button" class="npd-btn npd-tomorrow">Tomorrow</button>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div class="npd-time-picker">
|
|
145
|
+
<div class="npd-time-field">
|
|
146
|
+
<label>Time</label>
|
|
147
|
+
<div class="npd-time-inputs">
|
|
148
|
+
<input type="number" class="npd-time-input npd-hour" min="0" max="23" value="${this.selectedTime.hour}" placeholder="HH">
|
|
149
|
+
<span>:</span>
|
|
150
|
+
<input type="number" class="npd-time-input npd-minute" min="0" max="59" value="${this.selectedTime.minute}" placeholder="mm">
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
document.body.appendChild(picker);
|
|
158
|
+
this.picker = picker;
|
|
159
|
+
this.elements = {
|
|
160
|
+
title: picker.querySelector(".npd-title-text"),
|
|
161
|
+
daysView: picker.querySelector(".npd-view-days"),
|
|
162
|
+
monthsView: picker.querySelector(".npd-view-months"),
|
|
163
|
+
yearsView: picker.querySelector(".npd-view-years"),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
this.createTooltip(); // Create tooltip element
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
createTooltip() {
|
|
170
|
+
this.tooltip = document.createElement("div");
|
|
171
|
+
this.tooltip.className = "npd-tooltip";
|
|
172
|
+
this.picker.appendChild(this.tooltip);
|
|
173
|
+
this.longPressTimer = null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
showTooltip(element, text) {
|
|
177
|
+
if (!this.tooltip || !text) return;
|
|
178
|
+
this.tooltip.textContent = text;
|
|
179
|
+
|
|
180
|
+
// Position tooltip
|
|
181
|
+
const rect = element.getBoundingClientRect();
|
|
182
|
+
const pickerRect = this.picker.getBoundingClientRect();
|
|
183
|
+
|
|
184
|
+
// Calculate relative position within picker
|
|
185
|
+
const top = rect.top - pickerRect.top - this.tooltip.offsetHeight - 8;
|
|
186
|
+
const left =
|
|
187
|
+
rect.left - pickerRect.left + (rect.width - this.tooltip.offsetWidth) / 2;
|
|
188
|
+
|
|
189
|
+
this.tooltip.style.top = `${top}px`;
|
|
190
|
+
this.tooltip.style.left = `${left}px`;
|
|
191
|
+
this.tooltip.classList.add("active");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
hideTooltip() {
|
|
195
|
+
if (this.tooltip) {
|
|
196
|
+
this.tooltip.classList.remove("active");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
attachEvents() {
|
|
201
|
+
this.input.addEventListener("focus", () => this.open());
|
|
202
|
+
this.input.addEventListener("click", () => this.open());
|
|
203
|
+
this.input.addEventListener("keydown", (e) => this.handleInputKeydown(e));
|
|
204
|
+
this.input.addEventListener("input", (e) => this.handleInputChange(e));
|
|
205
|
+
this.input.addEventListener("blur", () => this.handleInputBlur());
|
|
206
|
+
|
|
207
|
+
this.picker
|
|
208
|
+
.querySelector(".npd-title")
|
|
209
|
+
.addEventListener("click", () => this.changeViewMode());
|
|
210
|
+
this.picker
|
|
211
|
+
.querySelector(".npd-prev")
|
|
212
|
+
.addEventListener("click", () => this.navigate(-1));
|
|
213
|
+
this.picker
|
|
214
|
+
.querySelector(".npd-next")
|
|
215
|
+
.addEventListener("click", () => this.navigate(1));
|
|
216
|
+
|
|
217
|
+
this.picker.querySelectorAll(".npd-mode-btn").forEach((btn) => {
|
|
218
|
+
btn.addEventListener("click", () => this.switchMode(btn.dataset.mode));
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (this.options.showTodayButton) {
|
|
222
|
+
this.picker
|
|
223
|
+
.querySelector(".npd-today")
|
|
224
|
+
.addEventListener("click", () => this.selectToday());
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (this.options.showClearButton) {
|
|
228
|
+
this.picker
|
|
229
|
+
.querySelector(".npd-clear")
|
|
230
|
+
.addEventListener("click", () => this.clear());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.picker
|
|
234
|
+
.querySelector(".npd-yesterday")
|
|
235
|
+
.addEventListener("click", () => this.selectYesterday());
|
|
236
|
+
this.picker
|
|
237
|
+
.querySelector(".npd-tomorrow")
|
|
238
|
+
.addEventListener("click", () => this.selectTomorrow());
|
|
239
|
+
|
|
240
|
+
this.picker.querySelector(".npd-hour").addEventListener("input", (e) => {
|
|
241
|
+
let val = parseInt(e.target.value);
|
|
242
|
+
if (isNaN(val)) val = 0;
|
|
243
|
+
if (val < 0) val = 0;
|
|
244
|
+
if (val > 23) val = 23;
|
|
245
|
+
this.selectedTime.hour = val;
|
|
246
|
+
this.updateInput();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
this.picker.querySelector(".npd-minute").addEventListener("input", (e) => {
|
|
250
|
+
let val = parseInt(e.target.value);
|
|
251
|
+
if (isNaN(val)) val = 0;
|
|
252
|
+
if (val < 0) val = 0;
|
|
253
|
+
if (val > 59) val = 59;
|
|
254
|
+
this.selectedTime.minute = val;
|
|
255
|
+
this.updateInput();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
document.addEventListener("click", (e) => {
|
|
259
|
+
if (!this.picker.contains(e.target) && e.target !== this.input) {
|
|
260
|
+
this.close();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
window.addEventListener("resize", () => {
|
|
265
|
+
if (this.isOpen) this.position();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
window.addEventListener(
|
|
269
|
+
"scroll",
|
|
270
|
+
() => {
|
|
271
|
+
if (this.isOpen) this.position();
|
|
272
|
+
},
|
|
273
|
+
true,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
handleInputKeydown(e) {
|
|
278
|
+
if (e.key === "Escape") {
|
|
279
|
+
this.close();
|
|
280
|
+
} else if (e.key === "Enter") {
|
|
281
|
+
e.preventDefault();
|
|
282
|
+
this.open();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
handleInputChange(e) {
|
|
287
|
+
// Prevent infinite loop if the event was triggered by our own updateInput
|
|
288
|
+
if (e.isTrusted === false && e.detail?.origin === "datepicker") return;
|
|
289
|
+
|
|
290
|
+
const value = e.target.value.trim();
|
|
291
|
+
if (!value) {
|
|
292
|
+
this.selectedDate = null;
|
|
293
|
+
this.render();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const [datePart, timePart] = value.split(" ");
|
|
299
|
+
let year, month, day;
|
|
300
|
+
|
|
301
|
+
// Try YYYY-MM-DD
|
|
302
|
+
if (datePart.includes("-")) {
|
|
303
|
+
[year, month, day] = datePart.split("-").map(Number);
|
|
304
|
+
} else if (datePart.length === 10) {
|
|
305
|
+
// Handle 2081/01/01 if user types slashes, though format option usually controls this
|
|
306
|
+
// For now let's assume standard format matches options.
|
|
307
|
+
// But simple split is safest for demo.
|
|
308
|
+
// If manual typing, we should be forgiving or strict.
|
|
309
|
+
// Let's reuse the parsing logic slightly.
|
|
310
|
+
[year, month, day] = datePart
|
|
311
|
+
.replace(/\//g, "-")
|
|
312
|
+
.split("-")
|
|
313
|
+
.map(Number);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!year || !month || !day) return; // Incomplete
|
|
317
|
+
|
|
318
|
+
// Parse Time
|
|
319
|
+
if (timePart) {
|
|
320
|
+
const [h, m] = timePart.split(":").map(Number);
|
|
321
|
+
if (!isNaN(h)) this.selectedTime.hour = h;
|
|
322
|
+
if (!isNaN(m)) this.selectedTime.minute = m;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let newDate;
|
|
326
|
+
if (this.options.mode === "BS") {
|
|
327
|
+
newDate = new NepaliDate(year, month, day);
|
|
328
|
+
} else {
|
|
329
|
+
newDate = NepaliDate.fromGregorian(year, month, day);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
this.selectedDate = newDate;
|
|
333
|
+
this.viewDate = {
|
|
334
|
+
year: this.selectedDate.year,
|
|
335
|
+
month: this.selectedDate.month,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// Update time inputs in picker UI
|
|
339
|
+
const hourInput = this.picker.querySelector(".npd-hour");
|
|
340
|
+
const minInput = this.picker.querySelector(".npd-minute");
|
|
341
|
+
if (hourInput) hourInput.value = this.selectedTime.hour;
|
|
342
|
+
if (minInput) minInput.value = this.selectedTime.minute;
|
|
343
|
+
|
|
344
|
+
this.render();
|
|
345
|
+
} catch (err) {
|
|
346
|
+
// incomplete or invalid date, ignore until valid
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
parseInitialValue() {
|
|
351
|
+
const value = this.input.value.trim();
|
|
352
|
+
if (!value) return;
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
// Split date and time
|
|
356
|
+
const [datePart, timePart] = value.split(" ");
|
|
357
|
+
|
|
358
|
+
let year, month, day;
|
|
359
|
+
if (datePart.includes("-")) {
|
|
360
|
+
[year, month, day] = datePart.split("-").map(Number);
|
|
361
|
+
} else {
|
|
362
|
+
// fallback or other format? assume YYYY-MM-DD for now
|
|
363
|
+
[year, month, day] = datePart.split("-").map(Number);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (timePart) {
|
|
367
|
+
const [h, m] = timePart.split(":").map(Number);
|
|
368
|
+
this.selectedTime = {
|
|
369
|
+
hour: isNaN(h) ? 0 : h,
|
|
370
|
+
minute: isNaN(m) ? 0 : m,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (this.options.mode === "BS") {
|
|
375
|
+
this.selectedDate = new NepaliDate(year, month, day);
|
|
376
|
+
} else {
|
|
377
|
+
this.selectedDate = NepaliDate.fromGregorian(year, month, day);
|
|
378
|
+
}
|
|
379
|
+
this.viewDate = {
|
|
380
|
+
year: this.selectedDate.year,
|
|
381
|
+
month: this.selectedDate.month,
|
|
382
|
+
};
|
|
383
|
+
} catch (e) {
|
|
384
|
+
console.warn("Invalid initial date value:", value);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
setDetailsFromToday() {
|
|
389
|
+
try {
|
|
390
|
+
const today = NepaliDate.today();
|
|
391
|
+
if (this.options.mode === "BS") {
|
|
392
|
+
this.viewDate = { year: today.year, month: today.month };
|
|
393
|
+
} else {
|
|
394
|
+
const [y, m] = today.toGregorian();
|
|
395
|
+
this.viewDate = { year: y, month: m };
|
|
396
|
+
}
|
|
397
|
+
} catch (e) {
|
|
398
|
+
console.error("Failed to set default today date:", e);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
open() {
|
|
403
|
+
if (this.isOpen) return;
|
|
404
|
+
|
|
405
|
+
this.isOpen = true;
|
|
406
|
+
this.picker.classList.add("active");
|
|
407
|
+
this.position();
|
|
408
|
+
this.render();
|
|
409
|
+
|
|
410
|
+
if (this.options.onOpen) {
|
|
411
|
+
this.options.onOpen(this);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
close() {
|
|
416
|
+
if (!this.isOpen) return;
|
|
417
|
+
|
|
418
|
+
this.isOpen = false;
|
|
419
|
+
this.picker.classList.remove("active");
|
|
420
|
+
|
|
421
|
+
if (this.options.onClose) {
|
|
422
|
+
this.options.onClose(this);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
position() {
|
|
427
|
+
const inputRect = this.input.getBoundingClientRect();
|
|
428
|
+
const pickerHeight = this.picker.offsetHeight || 400;
|
|
429
|
+
const spaceBelow = window.innerHeight - inputRect.bottom;
|
|
430
|
+
const spaceAbove = inputRect.top;
|
|
431
|
+
|
|
432
|
+
this.picker.style.left = `${inputRect.left}px`;
|
|
433
|
+
// Set picker width with a reasonable max-width instead of matching input width
|
|
434
|
+
// Min: 320px, Max: 400px to prevent overly wide pickers on full-width inputs
|
|
435
|
+
this.picker.style.width = `${Math.min(Math.max(inputRect.width, 320), 400)}px`;
|
|
436
|
+
|
|
437
|
+
if (
|
|
438
|
+
this.options.position === "top" ||
|
|
439
|
+
(this.options.position === "auto" &&
|
|
440
|
+
spaceBelow < pickerHeight &&
|
|
441
|
+
spaceAbove > spaceBelow)
|
|
442
|
+
) {
|
|
443
|
+
this.picker.style.bottom = `${window.innerHeight - inputRect.top + 8}px`;
|
|
444
|
+
this.picker.style.top = "auto";
|
|
445
|
+
this.picker.classList.add("npd-position-top");
|
|
446
|
+
} else {
|
|
447
|
+
this.picker.style.top = `${inputRect.bottom + 8}px`;
|
|
448
|
+
this.picker.style.bottom = "auto";
|
|
449
|
+
this.picker.classList.remove("npd-position-top");
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
switchMode(mode) {
|
|
454
|
+
if (this.options.mode === mode) return;
|
|
455
|
+
|
|
456
|
+
// pending switch
|
|
457
|
+
if (this.switchRequest) {
|
|
458
|
+
cancelAnimationFrame(this.switchRequest);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
this.switchRequest = requestAnimationFrame(() => {
|
|
462
|
+
this._performSwitchMode(mode);
|
|
463
|
+
this.switchRequest = null;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
_performSwitchMode(mode) {
|
|
468
|
+
if (this.options.mode === mode) return;
|
|
469
|
+
|
|
470
|
+
this.options.mode = mode;
|
|
471
|
+
this.picker.querySelectorAll(".npd-mode-btn").forEach((btn) => {
|
|
472
|
+
btn.classList.toggle("active", btn.dataset.mode === mode);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
if (this.selectedDate) {
|
|
477
|
+
if (mode === "AD") {
|
|
478
|
+
const [y, m] = this.selectedDate.toGregorian();
|
|
479
|
+
this.viewDate = { year: y, month: m };
|
|
480
|
+
} else {
|
|
481
|
+
this.viewDate = {
|
|
482
|
+
year: this.selectedDate.year,
|
|
483
|
+
month: this.selectedDate.month,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
// If no date is selected, convert the current view date
|
|
488
|
+
try {
|
|
489
|
+
if (mode === "AD") {
|
|
490
|
+
// BS -> AD: Use Day 15 to avoid backward drift (Day 1 usually maps to previous month)
|
|
491
|
+
const bsDate = new NepaliDate(
|
|
492
|
+
this.viewDate.year,
|
|
493
|
+
this.viewDate.month,
|
|
494
|
+
15,
|
|
495
|
+
);
|
|
496
|
+
const [y, m] = bsDate.toGregorian();
|
|
497
|
+
this.viewDate = { year: y, month: m };
|
|
498
|
+
} else {
|
|
499
|
+
// AD -> BS: Use Day 15
|
|
500
|
+
const bsDate = NepaliDate.fromGregorian(
|
|
501
|
+
this.viewDate.year,
|
|
502
|
+
this.viewDate.month,
|
|
503
|
+
15,
|
|
504
|
+
);
|
|
505
|
+
this.viewDate = { year: bsDate.year, month: bsDate.month };
|
|
506
|
+
}
|
|
507
|
+
} catch (e) {
|
|
508
|
+
console.error("Failed to convert view date on mode switch:", e);
|
|
509
|
+
this.setDetailsFromToday();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
} catch (err) {
|
|
513
|
+
console.error("Switch mode error:", err);
|
|
514
|
+
this.setDetailsFromToday();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
this.render();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
changeViewMode() {
|
|
521
|
+
const modes = ["days", "months", "years"];
|
|
522
|
+
const currentIndex = modes.indexOf(this.viewMode);
|
|
523
|
+
this.viewMode = modes[(currentIndex + 1) % modes.length];
|
|
524
|
+
this.render();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
navigate(direction) {
|
|
528
|
+
if (this.viewMode === "days") {
|
|
529
|
+
this.viewDate.month += direction;
|
|
530
|
+
if (this.viewDate.month > 12) {
|
|
531
|
+
this.viewDate.month = 1;
|
|
532
|
+
this.viewDate.year++;
|
|
533
|
+
} else if (this.viewDate.month < 1) {
|
|
534
|
+
this.viewDate.month = 12;
|
|
535
|
+
this.viewDate.year--;
|
|
536
|
+
}
|
|
537
|
+
} else if (this.viewMode === "months") {
|
|
538
|
+
this.viewDate.year += direction;
|
|
539
|
+
} else {
|
|
540
|
+
this.viewDate.year += direction * 12;
|
|
541
|
+
}
|
|
542
|
+
this.render();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
render() {
|
|
546
|
+
if (this.renderRequest) {
|
|
547
|
+
cancelAnimationFrame(this.renderRequest);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
this.renderRequest = requestAnimationFrame(() => {
|
|
551
|
+
this._performRender();
|
|
552
|
+
this.renderRequest = null;
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
_performRender() {
|
|
557
|
+
this.picker
|
|
558
|
+
.querySelectorAll(".npd-view")
|
|
559
|
+
.forEach((v) => v.classList.remove("active"));
|
|
560
|
+
|
|
561
|
+
if (this.viewMode === "days") {
|
|
562
|
+
this.renderDays();
|
|
563
|
+
} else if (this.viewMode === "months") {
|
|
564
|
+
this.renderMonths();
|
|
565
|
+
} else {
|
|
566
|
+
this.renderYears();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
renderDays() {
|
|
571
|
+
let months;
|
|
572
|
+
if (this.options.mode === "BS") {
|
|
573
|
+
months =
|
|
574
|
+
this.options.language === "np"
|
|
575
|
+
? [
|
|
576
|
+
"बैशाख",
|
|
577
|
+
"जेठ",
|
|
578
|
+
"असार",
|
|
579
|
+
"साउन",
|
|
580
|
+
"भदौ",
|
|
581
|
+
"असोज",
|
|
582
|
+
"कात्तिक",
|
|
583
|
+
"मंसिर",
|
|
584
|
+
"पुस",
|
|
585
|
+
"माघ",
|
|
586
|
+
"फागुन",
|
|
587
|
+
"चैत",
|
|
588
|
+
]
|
|
589
|
+
: [
|
|
590
|
+
"Baisakh",
|
|
591
|
+
"Jestha",
|
|
592
|
+
"Ashadh",
|
|
593
|
+
"Shrawan",
|
|
594
|
+
"Bhadra",
|
|
595
|
+
"Ashwin",
|
|
596
|
+
"Kartik",
|
|
597
|
+
"Mangshir",
|
|
598
|
+
"Poush",
|
|
599
|
+
"Magh",
|
|
600
|
+
"Falgun",
|
|
601
|
+
"Chaitra",
|
|
602
|
+
];
|
|
603
|
+
} else {
|
|
604
|
+
months = [
|
|
605
|
+
"January",
|
|
606
|
+
"February",
|
|
607
|
+
"March",
|
|
608
|
+
"April",
|
|
609
|
+
"May",
|
|
610
|
+
"June",
|
|
611
|
+
"July",
|
|
612
|
+
"August",
|
|
613
|
+
"September",
|
|
614
|
+
"October",
|
|
615
|
+
"November",
|
|
616
|
+
"December",
|
|
617
|
+
];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const weekdays =
|
|
621
|
+
this.options.language === "np"
|
|
622
|
+
? ["आइत", "सोम", "मंगल", "बुध", "बिहि", "शुक्र", "शनि"]
|
|
623
|
+
: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
624
|
+
|
|
625
|
+
if (this.options.mode === "BS") {
|
|
626
|
+
const year =
|
|
627
|
+
this.options.language === "np"
|
|
628
|
+
? this.toNepaliNum(this.viewDate.year)
|
|
629
|
+
: this.viewDate.year;
|
|
630
|
+
this.elements.title.textContent = `${months[this.viewDate.month - 1]} ${year}`;
|
|
631
|
+
} else {
|
|
632
|
+
const date = new Date(this.viewDate.year, this.viewDate.month - 1);
|
|
633
|
+
this.elements.title.textContent = date.toLocaleDateString("en-US", {
|
|
634
|
+
month: "long",
|
|
635
|
+
year: "numeric",
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
let html = '<div class="npd-days-grid">';
|
|
640
|
+
weekdays.forEach((day, index) => {
|
|
641
|
+
let isHoliday = false;
|
|
642
|
+
if (this.options.mode === "BS") {
|
|
643
|
+
isHoliday = index === 6; // Saturday
|
|
644
|
+
} else {
|
|
645
|
+
isHoliday = index === 0; // Sunday
|
|
646
|
+
}
|
|
647
|
+
html += `<div class="npd-weekday ${isHoliday ? "holiday" : ""}">${day}</div>`;
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
if (this.options.mode === "BS") {
|
|
651
|
+
const firstDate = new NepaliDate(
|
|
652
|
+
this.viewDate.year,
|
|
653
|
+
this.viewDate.month,
|
|
654
|
+
1,
|
|
655
|
+
);
|
|
656
|
+
const [gy, gm, gd] = firstDate.toGregorian();
|
|
657
|
+
const startWeekday = new Date(gy, gm - 1, gd).getDay();
|
|
658
|
+
const daysInMonth = this.getDaysInMonth(
|
|
659
|
+
this.viewDate.year,
|
|
660
|
+
this.viewDate.month,
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
const prevMonth =
|
|
664
|
+
this.viewDate.month === 1 ? 12 : this.viewDate.month - 1;
|
|
665
|
+
const prevYear =
|
|
666
|
+
this.viewDate.month === 1 ? this.viewDate.year - 1 : this.viewDate.year;
|
|
667
|
+
const daysInPrevMonth = this.getDaysInMonth(prevYear, prevMonth);
|
|
668
|
+
|
|
669
|
+
for (let i = startWeekday - 1; i >= 0; i--) {
|
|
670
|
+
const day = daysInPrevMonth - i;
|
|
671
|
+
const currentWeekday = (startWeekday - 1 - i) % 7;
|
|
672
|
+
// Actually, we are counting down.
|
|
673
|
+
// startWeekday is the first day of CURRENT month.
|
|
674
|
+
// So the day before is startWeekday-1.
|
|
675
|
+
// If startWeekday is 0 (Sunday), loops runs for i=-1 which is false? No startWeekday=0 means loop doesn't run.
|
|
676
|
+
// If startWeekday is 3 (Wed), i runs 2,1,0.
|
|
677
|
+
// i=2: day before start
|
|
678
|
+
// Wait, startWeekday is 0..6.
|
|
679
|
+
// The cell index for "day before start" is `startWeekday - 1`.
|
|
680
|
+
// The cell index relative to 0 is `startWeekday - 1 - i`?
|
|
681
|
+
// Let's count forward positions in the grid.
|
|
682
|
+
// The grid fills 0 to startWeekday-1 with prev month.
|
|
683
|
+
// So cell position is `startWeekday - 1 - i`.
|
|
684
|
+
// If startWeekday is 3 (Wed, index 3). i=2. 3-1-2 = 0 (Sunday). Correct.
|
|
685
|
+
// i=1. 3-1-1 = 1 (Monday).
|
|
686
|
+
// i=0. 3-1-0 = 2 (Tuesday).
|
|
687
|
+
|
|
688
|
+
// Wait, startWeekday comes from .getDay(). 0=Sun, 6=Sat.
|
|
689
|
+
// In BS mode, holiday is 6 (Sat).
|
|
690
|
+
// So we just check if the cell position % 7 === 6.
|
|
691
|
+
|
|
692
|
+
const cellIndex = startWeekday - 1 - i;
|
|
693
|
+
const isHoliday = cellIndex % 7 === 6;
|
|
694
|
+
|
|
695
|
+
const dayText =
|
|
696
|
+
this.options.language === "np" ? this.toNepaliNum(day) : day;
|
|
697
|
+
html += `<button type="button" class="npd-day npd-overflow ${isHoliday ? "holiday" : ""}" data-day="${day}" data-month-offset="-1">${dayText}</button>`;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const todayBS = NepaliDate.today();
|
|
701
|
+
const isCurrentYear = this.viewDate.year === todayBS.year;
|
|
702
|
+
const isCurrentMonth = this.viewDate.month === todayBS.month;
|
|
703
|
+
|
|
704
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
705
|
+
let isSelected = false;
|
|
706
|
+
let isRangeStart = false;
|
|
707
|
+
let isRangeEnd = false;
|
|
708
|
+
let isInRange = false;
|
|
709
|
+
|
|
710
|
+
const currentDayDate = new NepaliDate(
|
|
711
|
+
this.viewDate.year,
|
|
712
|
+
this.viewDate.month,
|
|
713
|
+
day,
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
if (this.options.isRange) {
|
|
717
|
+
isRangeStart = this.isSameDate(currentDayDate, this.rangeStart);
|
|
718
|
+
isRangeEnd = this.isSameDate(currentDayDate, this.rangeEnd);
|
|
719
|
+
isInRange = this.isDateInRange(currentDayDate);
|
|
720
|
+
isSelected = isRangeStart || isRangeEnd;
|
|
721
|
+
} else {
|
|
722
|
+
isSelected =
|
|
723
|
+
this.selectedDate?.year === this.viewDate.year &&
|
|
724
|
+
this.selectedDate?.month === this.viewDate.month &&
|
|
725
|
+
this.selectedDate?.day === day;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const isToday = isCurrentYear && isCurrentMonth && day === todayBS.day;
|
|
729
|
+
|
|
730
|
+
const currentWeekday = (startWeekday + day - 1) % 7;
|
|
731
|
+
const isHoliday = currentWeekday === 6; // Saturday in Nepal
|
|
732
|
+
|
|
733
|
+
const dayText =
|
|
734
|
+
this.options.language === "np" ? this.toNepaliNum(day) : day;
|
|
735
|
+
|
|
736
|
+
let classes = `npd-day ${isSelected ? "selected" : ""} ${isToday ? "today" : ""} ${isHoliday ? "holiday" : ""}`;
|
|
737
|
+
if (this.options.isRange) {
|
|
738
|
+
if (isRangeStart) classes += " range-start";
|
|
739
|
+
if (isRangeEnd) classes += " range-end";
|
|
740
|
+
if (isInRange) classes += " in-range";
|
|
741
|
+
if (isRangeStart && !this.rangeEnd) classes += " range-only";
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const fullDate =
|
|
745
|
+
this.options.mode == "BS"
|
|
746
|
+
? `${currentDayDate.year}-${String(currentDayDate.month).padStart(2, "0")}-${String(currentDayDate.day).padStart(2, "0")}`
|
|
747
|
+
: `${currentDayDate.year}-${String(currentDayDate.month).padStart(2, "0")}-${String(currentDayDate.day).padStart(2, "0")}`;
|
|
748
|
+
|
|
749
|
+
let details = fullDate;
|
|
750
|
+
try {
|
|
751
|
+
if (typeof currentDayDate.tithi === "function") {
|
|
752
|
+
const tithi = currentDayDate.tithi();
|
|
753
|
+
if (tithi) {
|
|
754
|
+
details += `\n${tithi}`;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} catch (e) {
|
|
758
|
+
// Tithi calculation failed or feature disabled
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
html += `<button type="button" class="${classes}" data-day="${day}" data-month-offset="0" data-details="${details}">${dayText}</button>`;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Next month overflow
|
|
765
|
+
const totalCells = 42;
|
|
766
|
+
const currentCells = startWeekday + daysInMonth;
|
|
767
|
+
for (let day = 1; day <= totalCells - currentCells; day++) {
|
|
768
|
+
const cellIndex = currentCells + day - 1;
|
|
769
|
+
const isHoliday = cellIndex % 7 === 6;
|
|
770
|
+
|
|
771
|
+
const dayText =
|
|
772
|
+
this.options.language === "np" ? this.toNepaliNum(day) : day;
|
|
773
|
+
html += `<button type="button" class="npd-day npd-overflow ${isHoliday ? "holiday" : ""}" data-day="${day}" data-month-offset="1">${dayText}</button>`;
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
const date = new Date(this.viewDate.year, this.viewDate.month - 1, 1);
|
|
777
|
+
const startWeekday = date.getDay();
|
|
778
|
+
const daysInMonth = new Date(
|
|
779
|
+
this.viewDate.year,
|
|
780
|
+
this.viewDate.month,
|
|
781
|
+
0,
|
|
782
|
+
).getDate();
|
|
783
|
+
|
|
784
|
+
const daysInPrevMonth = new Date(
|
|
785
|
+
this.viewDate.year,
|
|
786
|
+
this.viewDate.month - 1,
|
|
787
|
+
0,
|
|
788
|
+
).getDate();
|
|
789
|
+
|
|
790
|
+
for (let i = startWeekday - 1; i >= 0; i--) {
|
|
791
|
+
const cellIndex = startWeekday - 1 - i;
|
|
792
|
+
const isHoliday = cellIndex % 7 === 0; // Sunday logic for AD
|
|
793
|
+
const day = daysInPrevMonth - i;
|
|
794
|
+
html += `<button type="button" class="npd-day npd-overflow ${isHoliday ? "holiday" : ""}" data-day="${day}" data-month-offset="-1">${day}</button>`;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const [selY, selM, selD] = this.selectedDate
|
|
798
|
+
? this.selectedDate.toGregorian()
|
|
799
|
+
: [null, null, null];
|
|
800
|
+
|
|
801
|
+
const today = new Date();
|
|
802
|
+
const isCurrentYear = this.viewDate.year === today.getFullYear();
|
|
803
|
+
const isCurrentMonth = this.viewDate.month === today.getMonth() + 1;
|
|
804
|
+
|
|
805
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
806
|
+
let isSelected = false;
|
|
807
|
+
let isRangeStart = false;
|
|
808
|
+
let isRangeEnd = false;
|
|
809
|
+
let isInRange = false;
|
|
810
|
+
|
|
811
|
+
// Construct comparable object for AD (using existing helper logic or simple object)
|
|
812
|
+
// Since we store rangeStart/End as NepaliDate objects or wrappers?
|
|
813
|
+
// Wait, rangeStart is a NepaliDate object from selectDate.
|
|
814
|
+
// But in AD mode, selectDate creates a NepaliDate wrapper around the AD date via .fromGregorian().
|
|
815
|
+
// So comparison logic `isSameDate` works if `currentDayDate` is also a NepaliDate.
|
|
816
|
+
|
|
817
|
+
const currentAdDate = new Date(
|
|
818
|
+
this.viewDate.year,
|
|
819
|
+
this.viewDate.month - 1,
|
|
820
|
+
day,
|
|
821
|
+
);
|
|
822
|
+
const currentDayDate = NepaliDate.fromGregorian(
|
|
823
|
+
currentAdDate.getFullYear(),
|
|
824
|
+
currentAdDate.getMonth() + 1,
|
|
825
|
+
currentAdDate.getDate(),
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
if (this.options.isRange) {
|
|
829
|
+
isRangeStart = this.isSameDate(currentDayDate, this.rangeStart);
|
|
830
|
+
isRangeEnd = this.isSameDate(currentDayDate, this.rangeEnd);
|
|
831
|
+
isInRange = this.isDateInRange(currentDayDate);
|
|
832
|
+
isSelected = isRangeStart || isRangeEnd;
|
|
833
|
+
} else {
|
|
834
|
+
isSelected =
|
|
835
|
+
selY === this.viewDate.year &&
|
|
836
|
+
selM === this.viewDate.month &&
|
|
837
|
+
selD === day;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const isToday =
|
|
841
|
+
isCurrentYear && isCurrentMonth && day === today.getDate();
|
|
842
|
+
|
|
843
|
+
const currentWeekday = (startWeekday + day - 1) % 7;
|
|
844
|
+
const isHoliday = currentWeekday === 0; // Sunday for AD
|
|
845
|
+
|
|
846
|
+
let classes = `npd-day ${isSelected ? "selected" : ""} ${isToday ? "today" : ""} ${isHoliday ? "holiday" : ""}`;
|
|
847
|
+
if (this.options.isRange) {
|
|
848
|
+
if (isRangeStart) classes += " range-start";
|
|
849
|
+
if (isRangeEnd) classes += " range-end";
|
|
850
|
+
if (isInRange) classes += " in-range";
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
html += `<button type="button" class="${classes}" data-day="${day}" data-month-offset="0">${day}</button>`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Next month overflow
|
|
857
|
+
const totalCells = 42;
|
|
858
|
+
const currentCells = startWeekday + daysInMonth;
|
|
859
|
+
for (let day = 1; day <= totalCells - currentCells; day++) {
|
|
860
|
+
const cellIndex = currentCells + day - 1;
|
|
861
|
+
const isHoliday = cellIndex % 7 === 0; // Sunday for AD
|
|
862
|
+
html += `<button type="button" class="npd-day npd-overflow ${isHoliday ? "holiday" : ""}" data-day="${day}" data-month-offset="1">${day}</button>`;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
html += "</div>";
|
|
867
|
+
this.elements.daysView.innerHTML = html;
|
|
868
|
+
this.elements.daysView.classList.add("active");
|
|
869
|
+
|
|
870
|
+
this.elements.daysView
|
|
871
|
+
.querySelectorAll(".npd-day[data-day]")
|
|
872
|
+
.forEach((btn) => {
|
|
873
|
+
btn.addEventListener("click", (e) => {
|
|
874
|
+
e.stopPropagation();
|
|
875
|
+
const day = parseInt(btn.dataset.day);
|
|
876
|
+
const offset = parseInt(btn.dataset.monthOffset || "0");
|
|
877
|
+
|
|
878
|
+
if (offset !== 0) {
|
|
879
|
+
let newMonth = this.viewDate.month + offset;
|
|
880
|
+
let newYear = this.viewDate.year;
|
|
881
|
+
|
|
882
|
+
if (newMonth > 12) {
|
|
883
|
+
newMonth = 1;
|
|
884
|
+
newYear++;
|
|
885
|
+
} else if (newMonth < 1) {
|
|
886
|
+
newMonth = 12;
|
|
887
|
+
newYear--;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
this.viewDate = { year: newYear, month: newMonth };
|
|
891
|
+
this.render();
|
|
892
|
+
} else {
|
|
893
|
+
this.selectDate(day);
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// Tooltip Events
|
|
898
|
+
this.elements.daysView
|
|
899
|
+
.querySelectorAll(".npd-day[data-details]")
|
|
900
|
+
.forEach((btn) => {
|
|
901
|
+
// Desktop Hover
|
|
902
|
+
btn.addEventListener("mouseenter", () => {
|
|
903
|
+
this.showTooltip(btn, btn.dataset.details);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
btn.addEventListener("mouseleave", () => {
|
|
907
|
+
this.hideTooltip();
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// Mobile Long Press
|
|
911
|
+
btn.addEventListener(
|
|
912
|
+
"touchstart",
|
|
913
|
+
(e) => {
|
|
914
|
+
// e.preventDefault(); // Don't block click
|
|
915
|
+
this.longPressTimer = setTimeout(() => {
|
|
916
|
+
this.showTooltip(btn, btn.dataset.details);
|
|
917
|
+
}, 500); // 500ms long press
|
|
918
|
+
},
|
|
919
|
+
{ passive: true },
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
btn.addEventListener("touchend", () => {
|
|
923
|
+
if (this.longPressTimer) clearTimeout(this.longPressTimer);
|
|
924
|
+
this.hideTooltip();
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
btn.addEventListener("touchmove", () => {
|
|
928
|
+
if (this.longPressTimer) clearTimeout(this.longPressTimer);
|
|
929
|
+
this.hideTooltip();
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
renderMonths() {
|
|
936
|
+
let months;
|
|
937
|
+
if (this.options.mode === "BS") {
|
|
938
|
+
months =
|
|
939
|
+
this.options.language === "np"
|
|
940
|
+
? [
|
|
941
|
+
"बैशाख",
|
|
942
|
+
"जेठ",
|
|
943
|
+
"असार",
|
|
944
|
+
"साउन",
|
|
945
|
+
"भदौ",
|
|
946
|
+
"असोज",
|
|
947
|
+
"कात्तिक",
|
|
948
|
+
"मंसिर",
|
|
949
|
+
"पुस",
|
|
950
|
+
"माघ",
|
|
951
|
+
"फागुन",
|
|
952
|
+
"चैत",
|
|
953
|
+
]
|
|
954
|
+
: [
|
|
955
|
+
"Baisakh",
|
|
956
|
+
"Jestha",
|
|
957
|
+
"Ashadh",
|
|
958
|
+
"Shrawan",
|
|
959
|
+
"Bhadra",
|
|
960
|
+
"Ashwin",
|
|
961
|
+
"Kartik",
|
|
962
|
+
"Mangshir",
|
|
963
|
+
"Poush",
|
|
964
|
+
"Magh",
|
|
965
|
+
"Falgun",
|
|
966
|
+
"Chaitra",
|
|
967
|
+
];
|
|
968
|
+
} else {
|
|
969
|
+
months = [
|
|
970
|
+
"January",
|
|
971
|
+
"February",
|
|
972
|
+
"March",
|
|
973
|
+
"April",
|
|
974
|
+
"May",
|
|
975
|
+
"June",
|
|
976
|
+
"July",
|
|
977
|
+
"August",
|
|
978
|
+
"September",
|
|
979
|
+
"October",
|
|
980
|
+
"November",
|
|
981
|
+
"December",
|
|
982
|
+
];
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const year =
|
|
986
|
+
this.options.language === "np"
|
|
987
|
+
? this.toNepaliNum(this.viewDate.year)
|
|
988
|
+
: this.viewDate.year;
|
|
989
|
+
this.elements.title.textContent = year;
|
|
990
|
+
|
|
991
|
+
let html = '<div class="npd-months-grid">';
|
|
992
|
+
months.forEach((month, index) => {
|
|
993
|
+
const isSelected =
|
|
994
|
+
this.selectedDate?.year === this.viewDate.year &&
|
|
995
|
+
this.selectedDate?.month === index + 1;
|
|
996
|
+
html += `<button type="button" class="npd-month ${isSelected ? "selected" : ""}" data-month="${index + 1}">${month}</button>`;
|
|
997
|
+
});
|
|
998
|
+
html += "</div>";
|
|
999
|
+
|
|
1000
|
+
this.elements.monthsView.innerHTML = html;
|
|
1001
|
+
this.elements.monthsView.classList.add("active");
|
|
1002
|
+
|
|
1003
|
+
this.elements.monthsView.querySelectorAll(".npd-month").forEach((btn) => {
|
|
1004
|
+
btn.addEventListener("click", () => {
|
|
1005
|
+
this.viewDate.month = parseInt(btn.dataset.month);
|
|
1006
|
+
this.viewMode = "days";
|
|
1007
|
+
this.render();
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
renderYears() {
|
|
1013
|
+
const startYear = Math.floor(this.viewDate.year / 12) * 12;
|
|
1014
|
+
this.elements.title.textContent = `${startYear} - ${startYear + 11}`;
|
|
1015
|
+
|
|
1016
|
+
let html = '<div class="npd-years-grid">';
|
|
1017
|
+
for (let i = 0; i < 12; i++) {
|
|
1018
|
+
const year = startYear + i;
|
|
1019
|
+
const isSelected = this.selectedDate?.year === year;
|
|
1020
|
+
const yearText =
|
|
1021
|
+
this.options.language === "np" ? this.toNepaliNum(year) : year;
|
|
1022
|
+
html += `<button type="button" class="npd-year ${isSelected ? "selected" : ""}" data-year="${year}">${yearText}</button>`;
|
|
1023
|
+
}
|
|
1024
|
+
html += "</div>";
|
|
1025
|
+
|
|
1026
|
+
this.elements.yearsView.innerHTML = html;
|
|
1027
|
+
this.elements.yearsView.classList.add("active");
|
|
1028
|
+
|
|
1029
|
+
this.elements.yearsView.querySelectorAll(".npd-year").forEach((btn) => {
|
|
1030
|
+
btn.addEventListener("click", () => {
|
|
1031
|
+
this.viewDate.year = parseInt(btn.dataset.year);
|
|
1032
|
+
this.viewMode = "months";
|
|
1033
|
+
this.render();
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
isSameDate(d1, d2) {
|
|
1039
|
+
if (!d1 || !d2) return false;
|
|
1040
|
+
return d1.year === d2.year && d1.month === d2.month && d1.day === d2.day;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
compareDates(d1, d2) {
|
|
1044
|
+
if (!d1 || !d2) return 0;
|
|
1045
|
+
if (d1.year !== d2.year) return d1.year - d2.year;
|
|
1046
|
+
if (d1.month !== d2.month) return d1.month - d2.month;
|
|
1047
|
+
return d1.day - d2.day;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
isDateInRange(date) {
|
|
1051
|
+
if (!this.rangeStart || !this.rangeEnd || !date) return false;
|
|
1052
|
+
return (
|
|
1053
|
+
this.compareDates(date, this.rangeStart) > 0 &&
|
|
1054
|
+
this.compareDates(date, this.rangeEnd) < 0
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
selectDate(day) {
|
|
1059
|
+
try {
|
|
1060
|
+
let selected;
|
|
1061
|
+
if (this.options.mode === "BS") {
|
|
1062
|
+
selected = new NepaliDate(this.viewDate.year, this.viewDate.month, day);
|
|
1063
|
+
} else {
|
|
1064
|
+
selected = NepaliDate.fromGregorian(
|
|
1065
|
+
this.viewDate.year,
|
|
1066
|
+
this.viewDate.month,
|
|
1067
|
+
day,
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (this.options.isRange) {
|
|
1072
|
+
if (!this.rangeStart || (this.rangeStart && this.rangeEnd)) {
|
|
1073
|
+
this.rangeStart = selected;
|
|
1074
|
+
this.rangeEnd = null;
|
|
1075
|
+
} else {
|
|
1076
|
+
if (this.compareDates(selected, this.rangeStart) < 0) {
|
|
1077
|
+
this.rangeEnd = this.rangeStart;
|
|
1078
|
+
this.rangeStart = selected;
|
|
1079
|
+
} else {
|
|
1080
|
+
this.rangeEnd = selected;
|
|
1081
|
+
}
|
|
1082
|
+
if (this.options.closeOnSelect) {
|
|
1083
|
+
this.close();
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Update view to maintain visibility if range spans months?
|
|
1088
|
+
// Actually typically we don't jump view on second click unless necessary.
|
|
1089
|
+
|
|
1090
|
+
this.updateInput();
|
|
1091
|
+
this.render();
|
|
1092
|
+
|
|
1093
|
+
if (this.options.onChange) {
|
|
1094
|
+
this.options.onChange(
|
|
1095
|
+
{ start: this.rangeStart, end: this.rangeEnd },
|
|
1096
|
+
this,
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
} else {
|
|
1100
|
+
this.selectedDate = selected;
|
|
1101
|
+
this.updateInput();
|
|
1102
|
+
if (this.options.closeOnSelect) {
|
|
1103
|
+
this.close();
|
|
1104
|
+
} else {
|
|
1105
|
+
this.render();
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
if (this.options.onChange) {
|
|
1109
|
+
this.options.onChange(this.selectedDate, this);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
} catch (e) {
|
|
1113
|
+
console.error("Invalid date selection:", e);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
selectToday() {
|
|
1118
|
+
this.selectedDate = NepaliDate.today();
|
|
1119
|
+
this.viewDate = {
|
|
1120
|
+
year: this.selectedDate.year,
|
|
1121
|
+
month: this.selectedDate.month,
|
|
1122
|
+
};
|
|
1123
|
+
this.updateInput();
|
|
1124
|
+
this.close();
|
|
1125
|
+
|
|
1126
|
+
if (this.options.onChange) {
|
|
1127
|
+
this.options.onChange(this.selectedDate, this);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
selectYesterday() {
|
|
1132
|
+
const today = NepaliDate.today();
|
|
1133
|
+
this.selectedDate = today.addDays(-1);
|
|
1134
|
+
this.viewDate = {
|
|
1135
|
+
year: this.selectedDate.year,
|
|
1136
|
+
month: this.selectedDate.month,
|
|
1137
|
+
};
|
|
1138
|
+
this.updateInput();
|
|
1139
|
+
this.close();
|
|
1140
|
+
|
|
1141
|
+
if (this.options.onChange) {
|
|
1142
|
+
this.options.onChange(this.selectedDate, this);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
selectTomorrow() {
|
|
1147
|
+
const today = NepaliDate.today();
|
|
1148
|
+
this.selectedDate = today.addDays(1);
|
|
1149
|
+
this.viewDate = {
|
|
1150
|
+
year: this.selectedDate.year,
|
|
1151
|
+
month: this.selectedDate.month,
|
|
1152
|
+
};
|
|
1153
|
+
this.updateInput();
|
|
1154
|
+
this.close();
|
|
1155
|
+
|
|
1156
|
+
if (this.options.onChange) {
|
|
1157
|
+
this.options.onChange(this.selectedDate, this);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
clear() {
|
|
1162
|
+
this.selectedDate = null;
|
|
1163
|
+
this.input.value = "";
|
|
1164
|
+
this.close();
|
|
1165
|
+
|
|
1166
|
+
if (this.options.onChange) {
|
|
1167
|
+
this.options.onChange(null, this);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
updateInput() {
|
|
1172
|
+
if (this.options.isRange) {
|
|
1173
|
+
if (!this.rangeStart) {
|
|
1174
|
+
this.input.value = "";
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
let startStr = "";
|
|
1179
|
+
let endStr = "";
|
|
1180
|
+
|
|
1181
|
+
if (this.options.mode === "BS") {
|
|
1182
|
+
startStr = this.rangeStart.format(this.options.format);
|
|
1183
|
+
if (this.rangeEnd) endStr = this.rangeEnd.format(this.options.format);
|
|
1184
|
+
} else {
|
|
1185
|
+
const [y1, m1, d1] = this.rangeStart.toGregorian();
|
|
1186
|
+
startStr = `${y1}-${String(m1).padStart(2, "0")}-${String(d1).padStart(2, "0")}`;
|
|
1187
|
+
|
|
1188
|
+
if (this.rangeEnd) {
|
|
1189
|
+
const [y2, m2, d2] = this.rangeEnd.toGregorian();
|
|
1190
|
+
endStr = `${y2}-${String(m2).padStart(2, "0")}-${String(d2).padStart(2, "0")}`;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
this.input.value = endStr ? `${startStr} to ${endStr}` : startStr;
|
|
1195
|
+
} else {
|
|
1196
|
+
if (!this.selectedDate) {
|
|
1197
|
+
this.input.value = "";
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const timeStr = `${String(this.selectedTime.hour).padStart(2, "0")}:${String(this.selectedTime.minute).padStart(2, "0")}`;
|
|
1202
|
+
|
|
1203
|
+
if (this.options.mode === "BS") {
|
|
1204
|
+
let value = this.selectedDate.format(this.options.format);
|
|
1205
|
+
// Always append time for now if not present, because our format engine is simple
|
|
1206
|
+
// and user expects time if they see the picker
|
|
1207
|
+
if (!value.match(/\d{2}:\d{2}/)) {
|
|
1208
|
+
value += ` ${timeStr}`;
|
|
1209
|
+
}
|
|
1210
|
+
this.input.value = value;
|
|
1211
|
+
} else {
|
|
1212
|
+
const [y, m, d] = this.selectedDate.toGregorian();
|
|
1213
|
+
this.input.value = `${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")} ${timeStr}`;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
this.input.dispatchEvent(
|
|
1218
|
+
new CustomEvent("change", {
|
|
1219
|
+
bubbles: true,
|
|
1220
|
+
detail: { origin: "datepicker" },
|
|
1221
|
+
}),
|
|
1222
|
+
);
|
|
1223
|
+
this.input.dispatchEvent(
|
|
1224
|
+
new CustomEvent("input", {
|
|
1225
|
+
bubbles: true,
|
|
1226
|
+
detail: { origin: "datepicker" },
|
|
1227
|
+
}),
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
getDaysInMonth(year, month) {
|
|
1232
|
+
const key = `${year}-${month}`;
|
|
1233
|
+
if (NepaliDatePicker.daysCache.has(key)) {
|
|
1234
|
+
return NepaliDatePicker.daysCache.get(key);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
try {
|
|
1238
|
+
for (let d = 32; d >= 27; d--) {
|
|
1239
|
+
try {
|
|
1240
|
+
new NepaliDate(year, month, d);
|
|
1241
|
+
NepaliDatePicker.daysCache.set(key, d);
|
|
1242
|
+
return d;
|
|
1243
|
+
} catch (e) {}
|
|
1244
|
+
}
|
|
1245
|
+
} catch (e) {}
|
|
1246
|
+
return 30;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
toNepaliNum(num) {
|
|
1250
|
+
const map = ["०", "१", "२", "३", "४", "५", "६", "७", "८", "९"];
|
|
1251
|
+
return String(num).replace(/\d/g, (d) => map[+d]);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
destroy() {
|
|
1255
|
+
this.close();
|
|
1256
|
+
this.picker.remove();
|
|
1257
|
+
this.input.classList.remove("npd-input");
|
|
1258
|
+
this.input.removeAttribute("data-npd-id");
|
|
1259
|
+
NepaliDatePicker.instances.delete(this.input);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
handleInputBlur() {
|
|
1263
|
+
const value = this.input.value.trim();
|
|
1264
|
+
if (!value) {
|
|
1265
|
+
this.updateInput();
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
try {
|
|
1270
|
+
const [datePart, timePart] = value.split(" ");
|
|
1271
|
+
let year, month, day;
|
|
1272
|
+
|
|
1273
|
+
if (datePart.includes("-")) {
|
|
1274
|
+
[year, month, day] = datePart.split("-").map(Number);
|
|
1275
|
+
} else {
|
|
1276
|
+
[year, month, day] = datePart
|
|
1277
|
+
.replace(/\//g, "-")
|
|
1278
|
+
.split("-")
|
|
1279
|
+
.map(Number);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
if (!year || !month || !day) {
|
|
1283
|
+
this.updateInput();
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Smart Rollover Logic
|
|
1288
|
+
let yearOffset = 0;
|
|
1289
|
+
let monthOffset = 0;
|
|
1290
|
+
|
|
1291
|
+
// Handle Month Overflow first (e.g. Month 13 -> Year + 1, Month 1)
|
|
1292
|
+
while (month > 12) {
|
|
1293
|
+
month -= 12;
|
|
1294
|
+
yearOffset++;
|
|
1295
|
+
}
|
|
1296
|
+
while (month < 1) {
|
|
1297
|
+
month += 12;
|
|
1298
|
+
yearOffset--;
|
|
1299
|
+
}
|
|
1300
|
+
year += yearOffset;
|
|
1301
|
+
|
|
1302
|
+
// Handle Day Overflow
|
|
1303
|
+
// We need loop because adding days might push us through multiple months (e.g. day 90)
|
|
1304
|
+
// But for simple "32", one check is usually enough. Let's do a robust loop.
|
|
1305
|
+
let maxDays = 32; // safe upper bound to start checking
|
|
1306
|
+
let safety = 0;
|
|
1307
|
+
|
|
1308
|
+
while (true && safety < 12) {
|
|
1309
|
+
// limit 1 year rollover to prevent infinite loops
|
|
1310
|
+
// Get max days for current Year/Month
|
|
1311
|
+
if (this.options.mode === "BS") {
|
|
1312
|
+
maxDays = this.getDaysInMonth(year, month);
|
|
1313
|
+
} else {
|
|
1314
|
+
maxDays = new Date(year, month, 0).getDate();
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
if (day <= maxDays) break;
|
|
1318
|
+
|
|
1319
|
+
day -= maxDays;
|
|
1320
|
+
month++;
|
|
1321
|
+
if (month > 12) {
|
|
1322
|
+
month = 1;
|
|
1323
|
+
year++;
|
|
1324
|
+
}
|
|
1325
|
+
safety++;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// Parse Time (preserve existing logic)
|
|
1329
|
+
if (timePart) {
|
|
1330
|
+
const [h, m] = timePart.split(":").map(Number);
|
|
1331
|
+
if (!isNaN(h)) this.selectedTime.hour = h;
|
|
1332
|
+
if (!isNaN(m)) this.selectedTime.minute = m;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
let newDate;
|
|
1336
|
+
if (this.options.mode === "BS") {
|
|
1337
|
+
newDate = new NepaliDate(year, month, day);
|
|
1338
|
+
} else {
|
|
1339
|
+
newDate = NepaliDate.fromGregorian(year, month, day);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
this.selectedDate = newDate;
|
|
1343
|
+
this.viewDate = {
|
|
1344
|
+
year: this.selectedDate.year,
|
|
1345
|
+
month: this.selectedDate.month,
|
|
1346
|
+
};
|
|
1347
|
+
this.render();
|
|
1348
|
+
this.updateInput(); // Format it nicely
|
|
1349
|
+
} catch (e) {
|
|
1350
|
+
// If totally invalid, revert
|
|
1351
|
+
this.updateInput();
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
static init(
|
|
1356
|
+
selector = 'input[type="npdate"], input[data-npdate]',
|
|
1357
|
+
options = {},
|
|
1358
|
+
) {
|
|
1359
|
+
const inputs = document.querySelectorAll(selector);
|
|
1360
|
+
inputs.forEach((input) => {
|
|
1361
|
+
if (!NepaliDatePicker.instances.has(input)) {
|
|
1362
|
+
new NepaliDatePicker(input, options);
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (typeof document !== "undefined") {
|
|
1369
|
+
if (document.readyState === "loading") {
|
|
1370
|
+
document.addEventListener("DOMContentLoaded", () =>
|
|
1371
|
+
NepaliDatePicker.init(),
|
|
1372
|
+
);
|
|
1373
|
+
} else {
|
|
1374
|
+
NepaliDatePicker.init();
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
export default NepaliDatePicker;
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
|