Files
daily-paper/app/static/js/date-picker.js
T

292 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 += '<div class="kami-date-header">';
html += '<button type="button" class="kami-date-prev" title="上个月"></button>';
html += '<span class="kami-date-label">' + y + ' 年 ' + (m + 1) + ' 月</span>';
html += '<button type="button" class="kami-date-next" title="下个月"></button>';
html += '</div>';
// weekdays
html += '<div class="kami-date-weekdays">';
for (var w = 0; w < 7; w++) {
html += '<span>' + WEEKDAYS[w] + '</span>';
}
html += '</div>';
// 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 += '<div class="kami-date-grid">';
// 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 += '<div class="kami-date-day muted" data-year="' + py + '" data-month="' + pm + '" data-day="' + pd + '">' + pd + '</div>';
}
// 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 += '<div class="' + cls + '" data-year="' + y + '" data-month="' + m + '" data-day="' + d + '">' + d + '</div>';
}
// 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 += '<div class="kami-date-day muted" data-year="' + ny + '" data-month="' + nm + '" data-day="' + n + '">' + n + '</div>';
}
html += '</div>';
// footer — "回到今天"
html += '<div class="kami-date-footer">';
html += '<a href="javascript:void(0)" class="kami-date-today-link">回到今天</a>';
html += '</div>';
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;
})();