/**
* 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 += '
';
// 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;
})();