Diff -- /var/www/vhosts/3dshawn.com/crm.3dshawn.com/assets/javascript/datepicker.js

O Operator
Diff

/var/www/vhosts/3dshawn.com/crm.3dshawn.com/assets/javascript/datepicker.js

added on local at 2026-07-01 15:02:36

Added
+0
lines
Removed
-0
lines
Context
824
unchanged
Blobs
from b14a57ce027b
to b14a57ce027b
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
11/*======================================================================
22 * datepicker.js -- vanilla-JS date + time picker (no deps).
33 *
44 * Auto-attaches to every `<input type="datetime-local" data-fancy>`
55 * on DOMContentLoaded. Renders a polished popover with:
66 * - month-grid calendar (prev / next month arrows, day-of-week header,
77 * cells outside the active month are dimmed, today is ringed,
88 * selected date is filled with the brand color)
99 * - hour / minute number inputs with +/- spinners + quick-set chips
1010 * - "Today" / "Now" / "Clear" actions
1111 * - keyboard nav: arrows move the day, Enter selects, Esc closes,
1212 * PageUp/PageDown change month
1313 * - smart placement (flips above when the viewport bottom is too close)
1414 * - theme follows CSS vars --col-brand / --col-brand-2 / --col-bg-2 /
1515 * --col-border / --col-text / --col-surface (with sensible fallbacks)
1616 *
1717 * The native input is hidden visually but kept in the DOM so the form
1818 * still submits its value. The picker writes to .value with the standard
1919 * datetime-local format (YYYY-MM-DDTHH:MM).
2020 *
2121 * Usage:
2222 * <input type="datetime-local" name="from" data-fancy>
2323 *======================================================================*/
2424(function () {
2525 'use strict';
2626
2727 // --------------------------------------------------------------------
2828 // Style injection -- one global stylesheet for all pickers on the page.
2929 // --------------------------------------------------------------------
3030 function injectStyles() {
3131 if (document.getElementById('fdp-styles')) return;
3232 var css = `
3333 .fdp-wrap { position: relative; display: inline-block; }
3434 .fdp-display {
3535 background: var(--col-bg-2, #0e1822);
3636 color: var(--col-text, #e2e8f0);
3737 border: 1px solid var(--col-border, rgba(148,163,184,0.18));
3838 border-radius: 8px;
3939 padding: 6px 32px 6px 12px;
4040 font: inherit;
4141 font-size: 12.5px;
4242 cursor: pointer;
4343 outline: none;
4444 min-width: 200px;
4545 transition: border-color .15s ease, box-shadow .15s ease;
4646 font-variant-numeric: tabular-nums;
4747 }
4848 .fdp-display:hover, .fdp-display:focus { border-color: var(--col-brand, #06b6d4); }
4949 .fdp-display.has-value { color: var(--col-text, #e2e8f0); }
5050 .fdp-display.empty { color: var(--col-text-3, #64748b); }
5151 .fdp-wrap::after {
5252 content: '';
5353 position: absolute;
5454 right: 12px; top: 50%;
5555 width: 7px; height: 7px;
5656 border-right: 1.5px solid var(--col-text-3, #64748b);
5757 border-bottom: 1.5px solid var(--col-text-3, #64748b);
5858 transform: translateY(-70%) rotate(45deg);
5959 pointer-events: none;
6060 transition: transform .15s ease, border-color .15s ease;
6161 }
6262 .fdp-wrap.is-open::after {
6363 transform: translateY(-30%) rotate(-135deg);
6464 border-color: var(--col-brand, #06b6d4);
6565 }
6666
6767 .fdp-pop {
6868 position: absolute;
6969 top: calc(100% + 6px); left: 0;
7070 background: var(--col-surface, #0f1a26);
7171 border: 1px solid var(--col-border, rgba(148,163,184,0.18));
7272 border-radius: 14px;
7373 box-shadow: 0 20px 60px rgba(0,0,0,0.55), 0 0 0 1px rgba(6,182,212,0.08);
7474 padding: 14px;
7575 z-index: 9999;
7676 width: 320px;
7777 font-size: 12.5px;
7878 color: var(--col-text, #e2e8f0);
7979 user-select: none;
8080 animation: fdp-pop-in .12s ease-out;
8181 backdrop-filter: blur(10px);
8282 -webkit-backdrop-filter: blur(10px);
8383 }
8484 .fdp-pop.flip-up { top: auto; bottom: calc(100% + 6px); }
8585 @keyframes fdp-pop-in {
8686 from { opacity: 0; transform: translateY(-4px); }
8787 to { opacity: 1; transform: translateY(0); }
8888 }
8989
9090 .fdp-pop-head {
9191 display: flex;
9292 align-items: center;
9393 justify-content: space-between;
9494 margin-bottom: 10px;
9595 gap: 6px;
9696 }
9797 .fdp-pop-title {
9898 flex: 1;
9999 text-align: center;
100100 font-weight: 700;
101101 font-size: 13.5px;
102102 letter-spacing: .3px;
103103 color: #fff;
104104 background: none;
105105 border: 1px solid transparent;
106106 border-radius: 6px;
107107 padding: 4px 10px;
108108 cursor: pointer;
109109 transition: all .15s ease;
110110 font-family: inherit;
111111 }
112112 .fdp-pop-title:hover { background: var(--col-bg-2, #0e1822); border-color: var(--col-border, rgba(148,163,184,0.18)); }
113113 .fdp-nav-btn {
114114 background: var(--col-bg-2, #0e1822);
115115 border: 1px solid var(--col-border, rgba(148,163,184,0.18));
116116 color: var(--col-text-2, #cbd5e1);
117117 border-radius: 6px;
118118 width: 28px; height: 28px;
119119 display: flex; align-items: center; justify-content: center;
120120 cursor: pointer;
121121 transition: all .15s ease;
122122 padding: 0;
123123 }
124124 .fdp-nav-btn:hover { color: var(--col-brand, #06b6d4); border-color: var(--col-brand, #06b6d4); }
125125 .fdp-nav-btn svg { width: 12px; height: 12px; }
126126
127127 .fdp-dows {
128128 display: grid;
129129 grid-template-columns: repeat(7, 1fr);
130130 gap: 2px;
131131 margin-bottom: 4px;
132132 }
133133 .fdp-dow {
134134 font-size: 10px;
135135 font-weight: 700;
136136 letter-spacing: 1px;
137137 text-transform: uppercase;
138138 text-align: center;
139139 color: var(--col-text-3, #64748b);
140140 padding: 4px 0;
141141 }
142142
143143 .fdp-grid {
144144 display: grid;
145145 grid-template-columns: repeat(7, 1fr);
146146 gap: 2px;
147147 }
148148 .fdp-cell {
149149 aspect-ratio: 1;
150150 display: flex;
151151 align-items: center;
152152 justify-content: center;
153153 font-size: 12px;
154154 font-weight: 500;
155155 color: var(--col-text-2, #cbd5e1);
156156 background: transparent;
157157 border: 1px solid transparent;
158158 border-radius: 8px;
159159 cursor: pointer;
160160 transition: all .12s ease;
161161 position: relative;
162162 font-variant-numeric: tabular-nums;
163163 padding: 0;
164164 font-family: inherit;
165165 }
166166 .fdp-cell:hover { background: var(--col-bg-2, #0e1822); color: #fff; }
167167 .fdp-cell.outside { color: var(--col-text-3, #64748b); opacity: .5; }
168168 .fdp-cell.today {
169169 border-color: var(--col-border, rgba(148,163,184,0.25));
170170 color: var(--col-brand-2, #67e8f9);
171171 font-weight: 700;
172172 }
173173 .fdp-cell.selected {
174174 background: var(--col-brand, #06b6d4);
175175 color: #0c1018;
176176 font-weight: 700;
177177 border-color: var(--col-brand, #06b6d4);
178178 box-shadow: 0 0 0 2px rgba(6,182,212,0.25);
179179 }
180180 .fdp-cell.selected.today { color: #0c1018; }
181181 .fdp-cell:focus { outline: none; box-shadow: 0 0 0 2px rgba(103,232,249,0.5); }
182182
183183 /* Year/Month grid pickers */
184184 .fdp-yo {
185185 display: grid;
186186 grid-template-columns: repeat(4, 1fr);
187187 gap: 6px;
188188 padding: 4px 0;
189189 }
190190 .fdp-yo-cell {
191191 padding: 10px 6px;
192192 border-radius: 8px;
193193 background: var(--col-bg-2, #0e1822);
194194 border: 1px solid var(--col-border, rgba(148,163,184,0.18));
195195 color: var(--col-text-2, #cbd5e1);
196196 text-align: center;
197197 font-size: 12.5px;
198198 font-weight: 600;
199199 cursor: pointer;
200200 transition: all .12s ease;
201201 font-family: inherit;
202202 }
203203 .fdp-yo-cell:hover { color: #fff; border-color: var(--col-brand, #06b6d4); }
204204 .fdp-yo-cell.selected {
205205 background: var(--col-brand, #06b6d4);
206206 color: #0c1018;
207207 border-color: var(--col-brand, #06b6d4);
208208 font-weight: 700;
209209 }
210210
211211 .fdp-time {
212212 margin-top: 12px;
213213 padding-top: 12px;
214214 border-top: 1px solid var(--col-border, rgba(148,163,184,0.18));
215215 display: flex;
216216 align-items: center;
217217 gap: 6px;
218218 flex-wrap: nowrap;
219219 }
220220 .fdp-time-label {
221221 font-size: 10px;
222222 letter-spacing: 1.2px;
223223 text-transform: uppercase;
224224 font-weight: 700;
225225 color: var(--col-text-3, #64748b);
226226 margin-right: 2px;
227227 flex: 0 0 auto;
228228 }
229229 .fdp-tinput {
230230 display: inline-flex;
231231 align-items: center;
232232 background: var(--col-bg-2, #0e1822);
233233 border: 1px solid var(--col-border, rgba(148,163,184,0.18));
234234 border-radius: 7px;
235235 overflow: hidden;
236236 }
237237 .fdp-tinput.focused { border-color: var(--col-brand, #06b6d4); box-shadow: 0 0 0 2px rgba(6,182,212,0.15); }
238238 .fdp-tspin {
239239 background: transparent;
240240 border: none;
241241 color: var(--col-text-3, #64748b);
242242 width: 20px; height: 28px;
243243 cursor: pointer;
244244 font-size: 14px;
245245 line-height: 1;
246246 padding: 0;
247247 transition: color .12s ease;
248248 font-family: inherit;
249249 }
250250 .fdp-tspin:hover { color: var(--col-brand, #06b6d4); }
251251 .fdp-tnum {
252252 background: transparent;
253253 border: none;
254254 color: #fff;
255255 width: 30px;
256256 text-align: center;
257257 font: inherit;
258258 font-size: 13px;
259259 font-weight: 600;
260260 font-variant-numeric: tabular-nums;
261261 outline: none;
262262 -moz-appearance: textfield;
263263 }
264264 .fdp-tnum::-webkit-outer-spin-button,
265265 .fdp-tnum::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
266266 .fdp-tcolon { color: var(--col-text-3, #64748b); font-weight: 700; }
267267
268268 .fdp-period {
269269 display: inline-flex;
270270 background: var(--col-bg-2, #0e1822);
271271 border: 1px solid var(--col-border, rgba(148,163,184,0.18));
272272 border-radius: 7px;
273273 overflow: hidden;
274274 margin-left: auto;
275275 flex: 0 0 auto;
276276 }
277277 .fdp-pseg {
278278 background: transparent;
279279 border: none;
280280 color: var(--col-text-3, #64748b);
281281 padding: 5px 7px;
282282 font-size: 11px;
283283 font-weight: 700;
284284 letter-spacing: .5px;
285285 cursor: pointer;
286286 transition: all .12s ease;
287287 font-family: inherit;
288288 font-variant-numeric: tabular-nums;
289289 }
290290 .fdp-pseg:hover { color: #fff; }
291291 .fdp-pseg.active {
292292 background: var(--col-brand, #06b6d4);
293293 color: #0c1018;
294294 }
295295
296296 .fdp-quick {
297297 display: flex;
298298 gap: 4px;
299299 margin-top: 10px;
300300 flex-wrap: wrap;
301301 }
302302 .fdp-qbtn {
303303 background: var(--col-bg-2, #0e1822);
304304 border: 1px solid var(--col-border, rgba(148,163,184,0.18));
305305 color: var(--col-text-2, #cbd5e1);
306306 font-size: 11px;
307307 font-weight: 600;
308308 border-radius: 6px;
309309 padding: 4px 9px;
310310 cursor: pointer;
311311 transition: all .12s ease;
312312 font-family: inherit;
313313 }
314314 .fdp-qbtn:hover { color: var(--col-brand, #06b6d4); border-color: var(--col-brand, #06b6d4); }
315315
316316 .fdp-actions {
317317 display: flex;
318318 gap: 6px;
319319 margin-top: 12px;
320320 padding-top: 10px;
321321 border-top: 1px solid var(--col-border, rgba(148,163,184,0.18));
322322 justify-content: flex-end;
323323 }
324324 .fdp-act {
325325 font-size: 12px;
326326 font-weight: 600;
327327 padding: 6px 14px;
328328 border-radius: 7px;
329329 cursor: pointer;
330330 border: 1px solid var(--col-border, rgba(148,163,184,0.18));
331331 background: var(--col-bg-2, #0e1822);
332332 color: var(--col-text-2, #cbd5e1);
333333 transition: all .12s ease;
334334 font-family: inherit;
335335 }
336336 .fdp-act:hover { color: #fff; border-color: var(--col-text-3, #64748b); }
337337 .fdp-act.primary {
338338 background: var(--col-brand, #06b6d4);
339339 color: #0c1018;
340340 border-color: var(--col-brand, #06b6d4);
341341 }
342342 .fdp-act.primary:hover { background: var(--col-brand-2, #67e8f9); border-color: var(--col-brand-2, #67e8f9); }
343343 `;
344344 var s = document.createElement('style');
345345 s.id = 'fdp-styles';
346346 s.appendChild(document.createTextNode(css));
347347 document.head.appendChild(s);
348348 }
349349
350350 // --------------------------------------------------------------------
351351 // Date utilities. All work on local time (matches HTML5 datetime-local).
352352 // --------------------------------------------------------------------
353353 var MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
354354 var DOWS = ['Mo','Tu','We','Th','Fr','Sa','Su'];
355355
356356 function pad(n) { return (n < 10 ? '0' : '') + n; }
357357 // 24-hour -> 12-hour display. Returns { h: 1..12, period: 'AM'|'PM' }.
358358 // Midnight = 12 AM, Noon = 12 PM.
359359 function to12(h24) {
360360 var p = h24 >= 12 ? 'PM' : 'AM';
361361 var h = h24 % 12;
362362 if (h === 0) h = 12;
363363 return { h: h, period: p };
364364 }
365365 // 12-hour + period -> 24-hour (0..23).
366366 function from12(h12, period) {
367367 h12 = ((h12 - 1) % 12 + 12) % 12 + 1; // clamp to 1..12
368368 if (period === 'AM') return h12 === 12 ? 0 : h12;
369369 return h12 === 12 ? 12 : h12 + 12;
370370 }
371371 function toInputString(d) {
372372 if (!d) return '';
373373 return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) +
374374 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes());
375375 }
376376 function toDisplayString(d) {
377377 if (!d) return '';
378378 var t = to12(d.getHours());
379379 return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) +
380380 ' ' + pad(t.h) + ':' + pad(d.getMinutes()) + ' ' + t.period;
381381 }
382382 function parseInputString(s) {
383383 // Accept YYYY-MM-DDTHH:MM, YYYY-MM-DD HH:MM, YYYY-MM-DDTHH:MM:SS, YYYY-MM-DD
384384 if (!s) return null;
385385 var m = String(s).match(/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::\d{2})?)?$/);
386386 if (!m) return null;
387387 var d = new Date(+m[1], +m[2] - 1, +m[3], m[4] ? +m[4] : 0, m[5] ? +m[5] : 0, 0);
388388 return isNaN(d.getTime()) ? null : d;
389389 }
390390 function isSameDay(a, b) {
391391 return a && b && a.getFullYear() === b.getFullYear() &&
392392 a.getMonth() === b.getMonth() &&
393393 a.getDate() === b.getDate();
394394 }
395395 function startOfMonth(y, m) { return new Date(y, m, 1); }
396396
397397 // --------------------------------------------------------------------
398398 // Picker class -- one instance per attached input.
399399 // --------------------------------------------------------------------
400400 function Picker(input) {
401401 this.input = input;
402402 this.value = parseInputString(input.value); // Date | null
403403 // The viewMonth controls the grid's displayed month/year. Defaults to
404404 // the selected month, or the current month if no value.
405405 var ref = this.value || new Date();
406406 this.viewYear = ref.getFullYear();
407407 this.viewMonth = ref.getMonth();
408408 this.viewMode = 'days'; // 'days' | 'months' | 'years'
409409 this.yearOffset = 0; // for years-grid pagination (multiples of 12)
410410 this.build();
411411 this.attach();
412412 }
413413 Picker.prototype.build = function () {
414414 // Hide the native input but keep it in DOM for form submission.
415415 this.input.style.position = 'absolute';
416416 this.input.style.left = '-9999px';
417417 this.input.style.opacity = '0';
418418 this.input.style.pointerEvents = 'none';
419419 this.input.tabIndex = -1;
420420
421421 var wrap = document.createElement('span');
422422 wrap.className = 'fdp-wrap';
423423 var display = document.createElement('button');
424424 display.type = 'button';
425425 display.className = 'fdp-display' + (this.value ? ' has-value' : ' empty');
426426 display.textContent = this.value ? toDisplayString(this.value) : 'mm/dd/yyyy --:--';
427427 this.display = display;
428428 this.wrap = wrap;
429429 wrap.appendChild(display);
430430
431431 this.input.parentNode.insertBefore(wrap, this.input);
432432 wrap.appendChild(this.input); // move native input into wrap so layout stays
433433
434434 var pop = document.createElement('div');
435435 pop.className = 'fdp-pop';
436436 pop.style.display = 'none';
437437 pop.setAttribute('role', 'dialog');
438438 pop.addEventListener('mousedown', function (e) { e.stopPropagation(); });
439439 pop.addEventListener('click', function (e) { e.stopPropagation(); });
440440 wrap.appendChild(pop);
441441 this.pop = pop;
442442 };
443443 Picker.prototype.attach = function () {
444444 var self = this;
445445 this.display.addEventListener('click', function (e) { e.preventDefault(); self.toggle(); });
446446 this.display.addEventListener('keydown', function (e) {
447447 if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
448448 e.preventDefault();
449449 self.open();
450450 }
451451 });
452452 };
453453 Picker.prototype.open = function () {
454454 if (this.isOpen) return;
455455 this.isOpen = true;
456456 this.wrap.classList.add('is-open');
457457 this.render();
458458 this.pop.style.display = 'block';
459459 // Flip up if not enough room below.
460460 var rect = this.pop.getBoundingClientRect();
461461 var below = window.innerHeight - rect.top;
462462 if (below < rect.height + 8) {
463463 this.pop.classList.add('flip-up');
464464 } else {
465465 this.pop.classList.remove('flip-up');
466466 }
467467 document.addEventListener('mousedown', this._outside = this._outside || this.handleOutside.bind(this));
468468 document.addEventListener('keydown', this._keydown = this._keydown || this.handleKeydown.bind(this));
469469 };
470470 Picker.prototype.close = function () {
471471 if (!this.isOpen) return;
472472 this.isOpen = false;
473473 this.wrap.classList.remove('is-open');
474474 this.pop.style.display = 'none';
475475 document.removeEventListener('mousedown', this._outside);
476476 document.removeEventListener('keydown', this._keydown);
477477 };
478478 Picker.prototype.toggle = function () { this.isOpen ? this.close() : this.open(); };
479479 Picker.prototype.handleOutside = function (e) {
480480 if (!this.wrap.contains(e.target)) this.close();
481481 };
482482 Picker.prototype.handleKeydown = function (e) {
483483 if (e.key === 'Escape') { e.preventDefault(); this.close(); this.display.focus(); return; }
484484 if (this.viewMode !== 'days') return;
485485 // Arrow nav on the day grid
486486 var d = this.value || new Date(this.viewYear, this.viewMonth, 1);
487487 var moved = null;
488488 if (e.key === 'ArrowLeft') moved = new Date(d.getFullYear(), d.getMonth(), d.getDate() - 1, d.getHours(), d.getMinutes());
489489 if (e.key === 'ArrowRight') moved = new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1, d.getHours(), d.getMinutes());
490490 if (e.key === 'ArrowUp') moved = new Date(d.getFullYear(), d.getMonth(), d.getDate() - 7, d.getHours(), d.getMinutes());
491491 if (e.key === 'ArrowDown') moved = new Date(d.getFullYear(), d.getMonth(), d.getDate() + 7, d.getHours(), d.getMinutes());
492492 if (e.key === 'PageUp') moved = new Date(d.getFullYear(), d.getMonth() - 1, d.getDate(), d.getHours(), d.getMinutes());
493493 if (e.key === 'PageDown') moved = new Date(d.getFullYear(), d.getMonth() + 1, d.getDate(), d.getHours(), d.getMinutes());
494494 if (e.key === 'Enter') { e.preventDefault(); this.commit(); return; }
495495 if (moved) {
496496 e.preventDefault();
497497 this.value = moved;
498498 this.viewYear = moved.getFullYear();
499499 this.viewMonth = moved.getMonth();
500500 this.render();
501501 }
502502 };
503503 Picker.prototype.shiftMonth = function (delta) {
504504 var d = new Date(this.viewYear, this.viewMonth + delta, 1);
505505 this.viewYear = d.getFullYear();
506506 this.viewMonth = d.getMonth();
507507 this.render();
508508 };
509509 Picker.prototype.commit = function () {
510510 if (!this.value) this.value = new Date();
511511 this.input.value = toInputString(this.value);
512512 this.display.textContent = toDisplayString(this.value);
513513 this.display.classList.remove('empty');
514514 this.display.classList.add('has-value');
515515 // Fire input + change for any framework / form listeners.
516516 this.input.dispatchEvent(new Event('input', { bubbles: true }));
517517 this.input.dispatchEvent(new Event('change', { bubbles: true }));
518518 this.close();
519519 };
520520 Picker.prototype.clear = function () {
521521 this.value = null;
522522 this.input.value = '';
523523 this.display.textContent = 'mm/dd/yyyy --:--';
524524 this.display.classList.add('empty');
525525 this.display.classList.remove('has-value');
526526 this.input.dispatchEvent(new Event('input', { bubbles: true }));
527527 this.input.dispatchEvent(new Event('change', { bubbles: true }));
528528 this.render();
529529 };
530530 Picker.prototype.setQuick = function (kind) {
531531 var now = new Date();
532532 if (kind === 'today') this.value = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0);
533533 if (kind === 'now') this.value = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes());
534534 if (kind === 'yesterday') this.value = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 0, 0);
535535 this.viewYear = this.value.getFullYear();
536536 this.viewMonth = this.value.getMonth();
537537 this.render();
538538 };
539539
540540 // ------------------------------------------------------------------
541541 // Render -- repaints the popover contents based on view mode.
542542 // ------------------------------------------------------------------
543543 Picker.prototype.render = function () {
544544 var html = '';
545545 if (this.viewMode === 'days') html = this.renderDays();
546546 if (this.viewMode === 'months') html = this.renderMonths();
547547 if (this.viewMode === 'years') html = this.renderYears();
548548 this.pop.innerHTML = html;
549549 this.bindPop();
550550 };
551551 Picker.prototype.renderDays = function () {
552552 var y = this.viewYear, m = this.viewMonth;
553553 var first = startOfMonth(y, m);
554554 // first day-of-week (Mon=0)
555555 var dow = (first.getDay() + 6) % 7;
556556 var daysInMonth = new Date(y, m + 1, 0).getDate();
557557 var prevMonth = new Date(y, m, 0);
558558 var daysPrev = prevMonth.getDate();
559559
560560 var cells = [];
561561 // Lead-in (previous month days)
562562 for (var i = dow - 1; i >= 0; i--) {
563563 cells.push({ y: prevMonth.getFullYear(), m: prevMonth.getMonth(), d: daysPrev - i, outside: true });
564564 }
565565 // Current month days
566566 for (var d = 1; d <= daysInMonth; d++) {
567567 cells.push({ y: y, m: m, d: d, outside: false });
568568 }
569569 // Trail (next month) to fill the grid (6 rows of 7 = 42 cells)
570570 var nextDay = 1;
571571 while (cells.length < 42) {
572572 var nm = new Date(y, m + 1, 1);
573573 cells.push({ y: nm.getFullYear(), m: nm.getMonth(), d: nextDay++, outside: true });
574574 }
575575
576576 var today = new Date();
577577 var self = this;
578578 var cellHtml = cells.map(function (c) {
579579 var cd = new Date(c.y, c.m, c.d);
580580 var classes = 'fdp-cell';
581581 if (c.outside) classes += ' outside';
582582 if (isSameDay(cd, today)) classes += ' today';
583583 if (isSameDay(cd, self.value)) classes += ' selected';
584584 return '<button type="button" class="' + classes + '" ' +
585585 'data-y="' + c.y + '" data-m="' + c.m + '" data-d="' + c.d + '">' + c.d + '</button>';
586586 }).join('');
587587
588588 var dowHtml = DOWS.map(function (s) { return '<div class="fdp-dow">' + s + '</div>'; }).join('');
589589
590590 return '' +
591591 '<div class="fdp-pop-head">' +
592592 '<button type="button" class="fdp-nav-btn" data-act="prev" aria-label="Previous month">' +
593593 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>' +
594594 '</button>' +
595595 '<button type="button" class="fdp-pop-title" data-act="months">' + MONTHS[m] + ' ' + y + '</button>' +
596596 '<button type="button" class="fdp-nav-btn" data-act="next" aria-label="Next month">' +
597597 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>' +
598598 '</button>' +
599599 '</div>' +
600600 '<div class="fdp-dows">' + dowHtml + '</div>' +
601601 '<div class="fdp-grid">' + cellHtml + '</div>' +
602602 this.renderTime() +
603603 this.renderQuick() +
604604 this.renderActions();
605605 };
606606 Picker.prototype.renderMonths = function () {
607607 var self = this;
608608 var cells = MONTHS.map(function (mn, i) {
609609 var sel = (self.value && self.value.getMonth() === i && self.value.getFullYear() === self.viewYear) ||
610610 (!self.value && self.viewMonth === i);
611611 return '<button type="button" class="fdp-yo-cell' + (sel ? ' selected' : '') + '" data-act="pickmonth" data-m="' + i + '">' + mn.slice(0, 3) + '</button>';
612612 }).join('');
613613 return '' +
614614 '<div class="fdp-pop-head">' +
615615 '<button type="button" class="fdp-nav-btn" data-act="prevyear" aria-label="Previous year">' +
616616 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>' +
617617 '</button>' +
618618 '<button type="button" class="fdp-pop-title" data-act="years">' + this.viewYear + '</button>' +
619619 '<button type="button" class="fdp-nav-btn" data-act="nextyear" aria-label="Next year">' +
620620 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>' +
621621 '</button>' +
622622 '</div>' +
623623 '<div class="fdp-yo">' + cells + '</div>' +
624624 this.renderTime() +
625625 this.renderActions();
626626 };
627627 Picker.prototype.renderYears = function () {
628628 var self = this;
629629 // Show a 12-year grid centered on viewYear + yearOffset.
630630 var base = (Math.floor((this.viewYear + this.yearOffset) / 12)) * 12;
631631 var cells = [];
632632 for (var i = 0; i < 12; i++) {
633633 var yy = base + i;
634634 var sel = self.value && self.value.getFullYear() === yy;
635635 cells.push('<button type="button" class="fdp-yo-cell' + (sel ? ' selected' : '') + '" data-act="pickyear" data-y="' + yy + '">' + yy + '</button>');
636636 }
637637 return '' +
638638 '<div class="fdp-pop-head">' +
639639 '<button type="button" class="fdp-nav-btn" data-act="prevdecade" aria-label="Previous 12 years">' +
640640 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>' +
641641 '</button>' +
642642 '<div class="fdp-pop-title" style="cursor:default">' + base + ' &ndash; ' + (base + 11) + '</div>' +
643643 '<button type="button" class="fdp-nav-btn" data-act="nextdecade" aria-label="Next 12 years">' +
644644 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>' +
645645 '</button>' +
646646 '</div>' +
647647 '<div class="fdp-yo">' + cells.join('') + '</div>' +
648648 this.renderActions();
649649 };
650650 Picker.prototype.renderTime = function () {
651651 var t = to12(this.value ? this.value.getHours() : 0);
652652 var hh = pad(t.h);
653653 var mm = this.value ? pad(this.value.getMinutes()) : '00';
654654 var pAm = t.period === 'AM';
655655 return '' +
656656 '<div class="fdp-time">' +
657657 '<span class="fdp-time-label">Time</span>' +
658658 '<div class="fdp-tinput" data-time="h">' +
659659 '<button type="button" class="fdp-tspin" data-act="hdec" aria-label="Decrease hour">&#9662;</button>' +
660660 '<input class="fdp-tnum" type="text" data-field="h" value="' + hh + '" maxlength="2" inputmode="numeric">' +
661661 '<button type="button" class="fdp-tspin" data-act="hinc" aria-label="Increase hour">&#9652;</button>' +
662662 '</div>' +
663663 '<span class="fdp-tcolon">:</span>' +
664664 '<div class="fdp-tinput" data-time="m">' +
665665 '<button type="button" class="fdp-tspin" data-act="mdec" aria-label="Decrease minute">&#9662;</button>' +
666666 '<input class="fdp-tnum" type="text" data-field="m" value="' + mm + '" maxlength="2" inputmode="numeric">' +
667667 '<button type="button" class="fdp-tspin" data-act="minc" aria-label="Increase minute">&#9652;</button>' +
668668 '</div>' +
669669 '<div class="fdp-period" role="group" aria-label="AM or PM">' +
670670 '<button type="button" class="fdp-pseg' + (pAm ? ' active' : '') + '" data-act="setam">AM</button>' +
671671 '<button type="button" class="fdp-pseg' + (pAm ? '' : ' active') + '" data-act="setpm">PM</button>' +
672672 '</div>' +
673673 '</div>';
674674 };
675675 Picker.prototype.renderQuick = function () {
676676 return '' +
677677 '<div class="fdp-quick">' +
678678 '<button type="button" class="fdp-qbtn" data-quick="today">Today</button>' +
679679 '<button type="button" class="fdp-qbtn" data-quick="yesterday">Yesterday</button>' +
680680 '<button type="button" class="fdp-qbtn" data-quick="now">Now</button>' +
681681 '</div>';
682682 };
683683 Picker.prototype.renderActions = function () {
684684 return '' +
685685 '<div class="fdp-actions">' +
686686 '<button type="button" class="fdp-act" data-act="clear">Clear</button>' +
687687 '<button type="button" class="fdp-act primary" data-act="apply">Apply</button>' +
688688 '</div>';
689689 };
690690
691691 // ------------------------------------------------------------------
692692 // Bind all interactive elements in the rendered popover.
693693 // ------------------------------------------------------------------
694694 Picker.prototype.bindPop = function () {
695695 var self = this;
696696 // Cell click (day select)
697697 this.pop.querySelectorAll('.fdp-cell').forEach(function (el) {
698698 el.addEventListener('click', function () {
699699 var y = +el.dataset.y, m = +el.dataset.m, d = +el.dataset.d;
700700 var h = self.value ? self.value.getHours() : 0;
701701 var mi = self.value ? self.value.getMinutes() : 0;
702702 self.value = new Date(y, m, d, h, mi);
703703 // If user clicked outside-month cell, advance the view too
704704 if (m !== self.viewMonth || y !== self.viewYear) {
705705 self.viewYear = y;
706706 self.viewMonth = m;
707707 }
708708 self.render();
709709 });
710710 });
711711 // Nav buttons / mode switches
712712 this.pop.querySelectorAll('[data-act]').forEach(function (el) {
713713 el.addEventListener('click', function () {
714714 var act = el.dataset.act;
715715 if (act === 'prev') self.shiftMonth(-1);
716716 else if (act === 'next') self.shiftMonth(+1);
717717 else if (act === 'prevyear') { self.viewYear--; self.render(); }
718718 else if (act === 'nextyear') { self.viewYear++; self.render(); }
719719 else if (act === 'prevdecade') { self.yearOffset -= 12; self.render(); }
720720 else if (act === 'nextdecade') { self.yearOffset += 12; self.render(); }
721721 else if (act === 'months') { self.viewMode = 'months'; self.render(); }
722722 else if (act === 'years') { self.viewMode = 'years'; self.render(); }
723723 else if (act === 'pickmonth') {
724724 self.viewMonth = +el.dataset.m;
725725 self.viewMode = 'days';
726726 self.render();
727727 }
728728 else if (act === 'pickyear') {
729729 self.viewYear = +el.dataset.y;
730730 self.viewMode = 'months';
731731 self.render();
732732 }
733733 else if (act === 'apply') self.commit();
734734 else if (act === 'clear') self.clear();
735735 else if (act === 'hinc') self.bumpTime('h', +1);
736736 else if (act === 'hdec') self.bumpTime('h', -1);
737737 else if (act === 'minc') self.bumpTime('m', +1);
738738 else if (act === 'mdec') self.bumpTime('m', -1);
739739 else if (act === 'setam') self.setPeriod('AM');
740740 else if (act === 'setpm') self.setPeriod('PM');
741741 });
742742 });
743743 // Quick buttons
744744 this.pop.querySelectorAll('[data-quick]').forEach(function (el) {
745745 el.addEventListener('click', function () { self.setQuick(el.dataset.quick); });
746746 });
747747 // Time inputs (live typing + wheel scroll)
748748 this.pop.querySelectorAll('.fdp-tnum').forEach(function (inp) {
749749 var field = inp.dataset.field;
750750 inp.addEventListener('input', function () {
751751 // Strip non-digits, clamp. Hour input is 12-hour (1..12);
752752 // preserve the current AM/PM period when converting to 24-hour storage.
753753 var n = parseInt(inp.value.replace(/[^0-9]/g, ''), 10);
754754 if (isNaN(n)) n = 0;
755755 if (!self.value) self.value = new Date(self.viewYear, self.viewMonth, 1, 0, 0);
756756 if (field === 'h') {
757757 if (n < 1) n = 1;
758758 if (n > 12) n = 12;
759759 var period = to12(self.value.getHours()).period;
760760 self.value.setHours(from12(n, period));
761761 }
762762 if (field === 'm') {
763763 if (n < 0) n = 0;
764764 if (n > 59) n = 59;
765765 self.value.setMinutes(n);
766766 }
767767 });
768768 inp.addEventListener('blur', function () {
769769 // Re-pad after blur. Hour shows 12-hour; minute shows 24-hour digit value.
770770 if (field === 'h' && self.value) inp.value = pad(to12(self.value.getHours()).h);
771771 else inp.value = pad(parseInt(inp.value || '0', 10));
772772 });
773773 inp.addEventListener('focus', function () {
774774 inp.parentNode.classList.add('focused');
775775 inp.select();
776776 });
777777 inp.addEventListener('blur', function () { inp.parentNode.classList.remove('focused'); });
778778 inp.addEventListener('wheel', function (e) {
779779 e.preventDefault();
780780 self.bumpTime(field, e.deltaY < 0 ? +1 : -1);
781781 }, { passive: false });
782782 });
783783 };
784784 Picker.prototype.bumpTime = function (field, delta) {
785785 if (!this.value) this.value = new Date(this.viewYear, this.viewMonth, 1, 0, 0);
786786 if (field === 'h') {
787787 var h = (this.value.getHours() + delta + 24) % 24;
788788 this.value.setHours(h);
789789 }
790790 if (field === 'm') {
791791 var m = (this.value.getMinutes() + delta + 60) % 60;
792792 this.value.setMinutes(m);
793793 }
794794 this.render();
795795 };
796796 Picker.prototype.setPeriod = function (period) {
797797 if (!this.value) this.value = new Date(this.viewYear, this.viewMonth, 1, 0, 0);
798798 var cur = to12(this.value.getHours());
799799 if (cur.period === period) { this.render(); return; }
800800 this.value.setHours(from12(cur.h, period));
801801 this.render();
802802 };
803803
804804 // --------------------------------------------------------------------
805805 // Auto-init.
806806 // --------------------------------------------------------------------
807807 function init() {
808808 injectStyles();
809809 var all = document.querySelectorAll('input[type="datetime-local"][data-fancy]');
810810 Array.prototype.forEach.call(all, function (input) {
811811 if (input.__fdp) return;
812812 input.__fdp = new Picker(input);
813813 });
814814 }
815815
816816 if (document.readyState === 'loading') {
817817 document.addEventListener('DOMContentLoaded', init);
818818 } else {
819819 init();
820820 }
821821
822822 // Expose for manual init if scripts add inputs later.
823823 window.FancyDatePicker = { init: init, Picker: Picker };
824824})();