/** * KamiDatePicker — 轻量日历弹窗,kami 暖纸风格。 * * 用法: * new KamiDatePicker(triggerEl, { * value: '2025-06-09', // 初始日期 * maxDate: '2025-06-09', // 最晚可选 * minDate: '2024-01-01', // 最早可选 * markedDates: ['2025-06-08'],// 有数据的日期(小圆点) * onChange: function(dateStr) {}, // 选择回调 * }); */ ;(function () { 'use strict'; var INSTANCES = []; var WEEKDAYS = ['日', '一', '二', '三', '四', '五', '六']; function pad(n) { return n < 10 ? '0' + n : '' + n; } function isoDate(y, m, d) { return y + '-' + pad(m + 1) + '-' + pad(d); } function parseDate(s) { if (!s) return null; var parts = s.split('-'); if (parts.length !== 3) return null; return new Date(+parts[0], +parts[1] - 1, +parts[2]); } function sameDay(a, b) { return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); } function KamiDatePicker(triggerEl, opts) { opts = opts || {}; this.trigger = triggerEl; this.value = opts.value || null; this.maxDate = opts.maxDate ? parseDate(opts.maxDate) : null; this.minDate = opts.minDate ? parseDate(opts.minDate) : null; this.markedSet = {}; if (opts.markedDates) { for (var i = 0; i < opts.markedDates.length; i++) { this.markedSet[opts.markedDates[i]] = true; } } this.onChange = opts.onChange || function () {}; this.opened = false; // view state var initial = parseDate(this.value) || new Date(); this.viewYear = initial.getFullYear(); this.viewMonth = initial.getMonth(); this._build(); this._bind(); INSTANCES.push(this); } KamiDatePicker.prototype._build = function () { // wrapper this.wrapper = document.createElement('div'); this.wrapper.className = 'kami-date-picker-wrapper'; this.trigger.parentNode.insertBefore(this.wrapper, this.trigger); this.wrapper.appendChild(this.trigger); // popup this.popup = document.createElement('div'); this.popup.className = 'kami-date-popup'; this.popup.innerHTML = this._renderHTML(); this.wrapper.appendChild(this.popup); // make trigger look clickable this.trigger.style.cursor = 'pointer'; }; KamiDatePicker.prototype._bind = function () { var self = this; // toggle on trigger click this._triggerHandler = function (e) { e.preventDefault(); e.stopPropagation(); self.toggle(); }; this.trigger.addEventListener('click', this._triggerHandler); // popup internal clicks this.popup.addEventListener('click', function (e) { var target = e.target; // prev month if (target.closest('.kami-date-prev')) { e.stopPropagation(); self.viewMonth--; if (self.viewMonth < 0) { self.viewMonth = 11; self.viewYear--; } self._update(); return; } // next month if (target.closest('.kami-date-next')) { e.stopPropagation(); self.viewMonth++; if (self.viewMonth > 11) { self.viewMonth = 0; self.viewYear++; } self._update(); return; } // today link if (target.closest('.kami-date-today-link')) { e.stopPropagation(); self.select(new Date()); return; } // day cell var dayEl = target.closest('.kami-date-day'); if (dayEl && !dayEl.classList.contains('muted')) { e.stopPropagation(); var y = +dayEl.dataset.year; var m = +dayEl.dataset.month; var d = +dayEl.dataset.day; self.select(new Date(y, m, d)); } }); }; KamiDatePicker.prototype._renderHTML = function () { var y = this.viewYear; var m = this.viewMonth; var today = new Date(); var selected = parseDate(this.value); var html = ''; // header html += '
'; html += ''; html += '' + y + ' 年 ' + (m + 1) + ' 月'; html += ''; html += '
'; // weekdays html += '
'; for (var w = 0; w < 7; w++) { html += '' + WEEKDAYS[w] + ''; } html += '
'; // day grid var firstDay = new Date(y, m, 1).getDay(); // 0=Sun var daysInMonth = new Date(y, m + 1, 0).getDate(); var daysInPrevMonth = new Date(y, m, 0).getDate(); html += '
'; // prev month trailing days for (var p = firstDay - 1; p >= 0; p--) { var pd = daysInPrevMonth - p; var pm = m - 1; var py = y; if (pm < 0) { pm = 11; py--; } html += '
' + pd + '
'; } // current month days for (var d = 1; d <= daysInMonth; d++) { var date = new Date(y, m, d); var iso = isoDate(y, m, d); var cls = 'kami-date-day'; if (sameDay(date, today)) cls += ' today'; if (sameDay(date, selected)) cls += ' selected'; if (this.markedSet[iso]) cls += ' marked'; if (this.maxDate && date > this.maxDate) cls += ' disabled'; if (this.minDate && date < this.minDate) cls += ' disabled'; html += '
' + d + '
'; } // next month leading days var totalCells = firstDay + daysInMonth; var remaining = (7 - totalCells % 7) % 7; for (var n = 1; n <= remaining; n++) { var nm = m + 1; var ny = y; if (nm > 11) { nm = 0; ny++; } html += '
' + n + '
'; } html += '
'; // footer — "回到今天" html += ''; return html; }; KamiDatePicker.prototype._update = function () { this.popup.innerHTML = this._renderHTML(); }; KamiDatePicker.prototype.open = function () { // close other instances for (var i = 0; i < INSTANCES.length; i++) { if (INSTANCES[i] !== this && INSTANCES[i].opened) { INSTANCES[i].close(); } } // sync view to current value var sel = parseDate(this.value); if (sel) { this.viewYear = sel.getFullYear(); this.viewMonth = sel.getMonth(); } this._update(); this.popup.classList.add('open'); this.opened = true; this._addGlobalListeners(); }; KamiDatePicker.prototype.close = function () { this.popup.classList.remove('open'); this.opened = false; this._removeGlobalListeners(); }; KamiDatePicker.prototype.toggle = function () { if (this.opened) this.close(); else this.open(); }; KamiDatePicker.prototype.select = function (date) { var iso = isoDate(date.getFullYear(), date.getMonth(), date.getDate()); this.value = iso; this.close(); this.onChange(iso); }; KamiDatePicker.prototype.setValue = function (dateStr) { this.value = dateStr || null; var d = parseDate(dateStr); if (d) { this.viewYear = d.getFullYear(); this.viewMonth = d.getMonth(); } if (this.opened) this._update(); }; KamiDatePicker.prototype.getValue = function () { return this.value; }; KamiDatePicker.prototype.destroy = function () { this.close(); this.trigger.removeEventListener('click', this._triggerHandler); // move trigger back out of wrapper this.wrapper.parentNode.insertBefore(this.trigger, this.wrapper); this.wrapper.remove(); var idx = INSTANCES.indexOf(this); if (idx > -1) INSTANCES.splice(idx, 1); }; // ── Global listeners ── KamiDatePicker.prototype._addGlobalListeners = function () { var self = this; if (!this._docClickHandler) { this._docClickHandler = function (e) { if (self.wrapper.contains(e.target)) return; self.close(); }; } if (!this._keyHandler) { this._keyHandler = function (e) { if (e.key === 'Escape') self.close(); }; } document.addEventListener('click', this._docClickHandler); document.addEventListener('keydown', this._keyHandler); }; KamiDatePicker.prototype._removeGlobalListeners = function () { if (this._docClickHandler) document.removeEventListener('click', this._docClickHandler); if (this._keyHandler) document.removeEventListener('keydown', this._keyHandler); }; // expose window.KamiDatePicker = KamiDatePicker; })();