夜间控件改走现有 token 与 color-scheme,日期弹层锚定触发器并可翻转; 创建公司账号改为一次性可复制口令窗口,列表与后续接口不再回明文。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
269 lines
8.9 KiB
JavaScript
269 lines
8.9 KiB
JavaScript
/* 锚定在日期输入旁的页内日历。不引入第三方库;滚动/缩放后跟随触发器。 */
|
||
(function (global) {
|
||
var WEEK = ["日", "一", "二", "三", "四", "五", "六"];
|
||
var panel = null;
|
||
var grid = null;
|
||
var titleEl = null;
|
||
var activeInput = null;
|
||
var viewYear = 0;
|
||
var viewMonth = 0;
|
||
var bound = false;
|
||
|
||
function pad(n) {
|
||
return n < 10 ? "0" + n : String(n);
|
||
}
|
||
|
||
function toISO(year, month, day) {
|
||
return year + "-" + pad(month + 1) + "-" + pad(day);
|
||
}
|
||
|
||
function parseISO(value) {
|
||
var match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || "").trim());
|
||
if (!match) return null;
|
||
var year = Number(match[1]);
|
||
var month = Number(match[2]) - 1;
|
||
var day = Number(match[3]);
|
||
var date = new Date(year, month, day);
|
||
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
|
||
return null;
|
||
}
|
||
return date;
|
||
}
|
||
|
||
function todayISO() {
|
||
var now = new Date();
|
||
return toISO(now.getFullYear(), now.getMonth(), now.getDate());
|
||
}
|
||
|
||
function ensurePanel() {
|
||
if (panel) return;
|
||
panel = document.createElement("div");
|
||
panel.className = "ds-datepicker";
|
||
panel.id = "ds-datepicker";
|
||
panel.hidden = true;
|
||
panel.setAttribute("role", "dialog");
|
||
panel.setAttribute("aria-label", "选择日期");
|
||
panel.innerHTML =
|
||
'<div class="ds-dp-head">' +
|
||
'<button type="button" class="ds-dp-nav" data-dp-nav="-1" aria-label="上一月">‹</button>' +
|
||
'<div class="ds-dp-title"></div>' +
|
||
'<button type="button" class="ds-dp-nav" data-dp-nav="1" aria-label="下一月">›</button>' +
|
||
"</div>" +
|
||
'<div class="ds-dp-week">' + WEEK.map(function (d) { return "<span>" + d + "</span>"; }).join("") + "</div>" +
|
||
'<div class="ds-dp-grid" role="grid"></div>' +
|
||
'<div class="ds-dp-foot">' +
|
||
'<button type="button" data-dp-today>今天</button>' +
|
||
'<button type="button" data-dp-clear>清除</button>' +
|
||
"</div>";
|
||
document.body.appendChild(panel);
|
||
titleEl = panel.querySelector(".ds-dp-title");
|
||
grid = panel.querySelector(".ds-dp-grid");
|
||
panel.addEventListener("mousedown", function (event) {
|
||
event.preventDefault();
|
||
});
|
||
panel.addEventListener("click", function (event) {
|
||
var nav = event.target.closest("[data-dp-nav]");
|
||
if (nav) {
|
||
shiftMonth(Number(nav.getAttribute("data-dp-nav")));
|
||
return;
|
||
}
|
||
if (event.target.closest("[data-dp-today]")) {
|
||
commit(todayISO());
|
||
return;
|
||
}
|
||
if (event.target.closest("[data-dp-clear]")) {
|
||
commit("");
|
||
return;
|
||
}
|
||
var dayBtn = event.target.closest("[data-iso]");
|
||
if (dayBtn && !dayBtn.disabled) commit(dayBtn.getAttribute("data-iso"));
|
||
});
|
||
}
|
||
|
||
function enhance(input) {
|
||
if (!input || input.dataset.dsDate === "1") return;
|
||
input.dataset.dsDate = "1";
|
||
if (input.type === "date") {
|
||
var value = input.value;
|
||
try {
|
||
input.type = "text";
|
||
} catch (err) {
|
||
/* keep native type if the engine refuses */
|
||
}
|
||
if (value) input.value = value;
|
||
}
|
||
input.setAttribute("inputmode", "numeric");
|
||
input.setAttribute("autocomplete", "off");
|
||
input.setAttribute("spellcheck", "false");
|
||
input.setAttribute("placeholder", input.getAttribute("placeholder") || "YYYY-MM-DD");
|
||
if (!input.getAttribute("pattern")) input.setAttribute("pattern", "\\d{4}-\\d{2}-\\d{2}");
|
||
if (!input.classList.contains("num-input")) input.classList.add("num-input");
|
||
input.setAttribute("aria-haspopup", "dialog");
|
||
}
|
||
|
||
function scan() {
|
||
document.querySelectorAll('input[type="date"]').forEach(enhance);
|
||
}
|
||
|
||
function render() {
|
||
if (!grid || !titleEl) return;
|
||
titleEl.textContent = viewYear + "年" + pad(viewMonth + 1) + "月";
|
||
var first = new Date(viewYear, viewMonth, 1);
|
||
var start = first.getDay();
|
||
var selected = activeInput ? parseISO(activeInput.value) : null;
|
||
var selectedISO = selected ? toISO(selected.getFullYear(), selected.getMonth(), selected.getDate()) : "";
|
||
var today = todayISO();
|
||
var min = activeInput && activeInput.min ? activeInput.min : "";
|
||
var max = activeInput && activeInput.max ? activeInput.max : "";
|
||
var html = "";
|
||
var cursor = new Date(viewYear, viewMonth, 1 - start);
|
||
for (var i = 0; i < 42; i += 1) {
|
||
var iso = toISO(cursor.getFullYear(), cursor.getMonth(), cursor.getDate());
|
||
var other = cursor.getMonth() !== viewMonth;
|
||
var disabled = (min && iso < min) || (max && iso > max);
|
||
var cls = "ds-dp-day";
|
||
if (other) cls += " is-other";
|
||
if (iso === today) cls += " is-today";
|
||
if (iso === selectedISO) cls += " is-selected";
|
||
html +=
|
||
'<button type="button" class="' + cls + '" data-iso="' + iso + '"' +
|
||
(disabled ? " disabled" : "") +
|
||
' aria-label="' + iso + '"' +
|
||
(iso === selectedISO ? ' aria-current="date"' : "") +
|
||
">" + cursor.getDate() + "</button>";
|
||
cursor.setDate(cursor.getDate() + 1);
|
||
}
|
||
grid.innerHTML = html;
|
||
position();
|
||
}
|
||
|
||
function viewportBox() {
|
||
var vv = global.visualViewport;
|
||
if (vv) {
|
||
return {
|
||
left: vv.offsetLeft,
|
||
top: vv.offsetTop,
|
||
width: vv.width,
|
||
height: vv.height,
|
||
};
|
||
}
|
||
return { left: 0, top: 0, width: global.innerWidth, height: global.innerHeight };
|
||
}
|
||
|
||
function position() {
|
||
if (!panel || panel.hidden || !activeInput) return;
|
||
var rect = activeInput.getBoundingClientRect();
|
||
var view = viewportBox();
|
||
var gap = 6;
|
||
var width = panel.offsetWidth || 280;
|
||
var height = panel.offsetHeight || 320;
|
||
var left = rect.left;
|
||
var top = rect.bottom + gap;
|
||
if (top + height > view.top + view.height - 8 && rect.top - gap - height >= view.top + 8) {
|
||
top = rect.top - gap - height;
|
||
}
|
||
if (left + width > view.left + view.width - 8) {
|
||
left = Math.max(view.left + 8, rect.right - width);
|
||
}
|
||
if (left < view.left + 8) left = view.left + 8;
|
||
if (top < view.top + 8) top = view.top + 8;
|
||
panel.style.left = Math.round(left) + "px";
|
||
panel.style.top = Math.round(top) + "px";
|
||
}
|
||
|
||
function openPicker(input) {
|
||
if (!input || input.disabled) return;
|
||
enhance(input);
|
||
ensurePanel();
|
||
activeInput = input;
|
||
var parsed = parseISO(input.value) || new Date();
|
||
viewYear = parsed.getFullYear();
|
||
viewMonth = parsed.getMonth();
|
||
panel.hidden = false;
|
||
render();
|
||
input.setAttribute("aria-expanded", "true");
|
||
}
|
||
|
||
function closePicker() {
|
||
if (!panel || panel.hidden) return;
|
||
panel.hidden = true;
|
||
if (activeInput) activeInput.setAttribute("aria-expanded", "false");
|
||
activeInput = null;
|
||
}
|
||
|
||
function shiftMonth(delta) {
|
||
viewMonth += delta;
|
||
while (viewMonth < 0) {
|
||
viewMonth += 12;
|
||
viewYear -= 1;
|
||
}
|
||
while (viewMonth > 11) {
|
||
viewMonth -= 12;
|
||
viewYear += 1;
|
||
}
|
||
render();
|
||
}
|
||
|
||
function commit(iso) {
|
||
if (!activeInput) return;
|
||
var input = activeInput;
|
||
input.value = iso;
|
||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||
closePicker();
|
||
input.focus({ preventScroll: true });
|
||
}
|
||
|
||
function onPointer(event) {
|
||
var input = event.target.closest && event.target.closest('input[type="date"], input[data-ds-date="1"]');
|
||
if (input) {
|
||
if (event.type === "mousedown" && event.button === 0) event.preventDefault();
|
||
openPicker(input);
|
||
return;
|
||
}
|
||
if (panel && !panel.hidden && !panel.contains(event.target)) closePicker();
|
||
}
|
||
|
||
function onKey(event) {
|
||
if (event.key === "Escape") {
|
||
closePicker();
|
||
return;
|
||
}
|
||
var input = event.target.closest && event.target.closest('input[data-ds-date="1"]');
|
||
if (!input) return;
|
||
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
|
||
event.preventDefault();
|
||
openPicker(input);
|
||
}
|
||
}
|
||
|
||
function bind() {
|
||
if (bound) return;
|
||
bound = true;
|
||
document.addEventListener("mousedown", onPointer, true);
|
||
document.addEventListener("focusin", function (event) {
|
||
var input = event.target.closest && event.target.closest('input[type="date"]');
|
||
if (input) openPicker(input);
|
||
});
|
||
document.addEventListener("keydown", onKey);
|
||
document.addEventListener("scroll", position, true);
|
||
global.addEventListener("resize", position);
|
||
if (global.visualViewport) {
|
||
global.visualViewport.addEventListener("resize", position);
|
||
global.visualViewport.addEventListener("scroll", position);
|
||
}
|
||
if (typeof MutationObserver === "function") {
|
||
new MutationObserver(scan).observe(document.documentElement, { childList: true, subtree: true });
|
||
}
|
||
scan();
|
||
}
|
||
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", bind);
|
||
} else {
|
||
bind();
|
||
}
|
||
|
||
global.JinniuDatePicker = { open: openPicker, close: closePicker, scan: scan };
|
||
})(window);
|