/* 锚定在日期输入旁的页内日历。不引入第三方库;滚动/缩放后跟随触发器。 */ (function (global) { var WEEK = ["日", "一", "二", "三", "四", "五", "六"]; var panel = null; var grid = null; var titleEl = null; var yearSelect = null; var monthSelect = null; var activeInput = null; var viewYear = 0; var viewMonth = 0; var bound = false; var YEAR_PAD = 80; var YEAR_AHEAD = 20; 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 = '
' + '' + '
' + '' + '' + "
" + '' + "
" + '
' + WEEK.map(function (d) { return "" + d + ""; }).join("") + "
" + '
' + '
' + '' + '' + "
"; document.body.appendChild(panel); titleEl = panel.querySelector(".ds-dp-title"); yearSelect = panel.querySelector("[data-dp-year]"); monthSelect = panel.querySelector("[data-dp-month]"); grid = panel.querySelector(".ds-dp-grid"); yearSelect.addEventListener("change", function () { var next = Number(yearSelect.value); if (!next) return; viewYear = next; render(); }); monthSelect.addEventListener("change", function () { var next = Number(monthSelect.value); if (Number.isNaN(next)) return; viewMonth = next; render(); }); panel.addEventListener("mousedown", function (event) { if (event.target.closest(".ds-combo, select, input, textarea")) return; event.preventDefault(); }); panel.addEventListener("click", function (event) { if (event.target.closest(".ds-combo, .ds-listbox, select")) return; 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 boundYears() { var now = new Date().getFullYear(); var minY = now - YEAR_PAD; var maxY = now + YEAR_AHEAD; var hasMin = false; var hasMax = false; if (activeInput && activeInput.min) { var minDate = parseISO(activeInput.min); if (minDate) { minY = minDate.getFullYear(); hasMin = true; } } if (activeInput && activeInput.max) { var maxDate = parseISO(activeInput.max); if (maxDate) { maxY = maxDate.getFullYear(); hasMax = true; } } if (!hasMin && viewYear < minY) minY = viewYear; if (!hasMax && viewYear > maxY) maxY = viewYear; if (hasMin && viewYear < minY) minY = viewYear; if (hasMax && viewYear > maxY) maxY = viewYear; return { minY: minY, maxY: maxY }; } function lastDayOfMonth(year, month) { return new Date(year, month + 1, 0).getDate(); } function monthHasValidDay(year, month) { var min = activeInput && activeInput.min ? activeInput.min : ""; var max = activeInput && activeInput.max ? activeInput.max : ""; var start = toISO(year, month, 1); var end = toISO(year, month, lastDayOfMonth(year, month)); if (min && end < min) return false; if (max && start > max) return false; return true; } function syncYearMonthSelects() { if (!yearSelect || !monthSelect) return; var years = boundYears(); var yearHtml = ""; for (var year = years.minY; year <= years.maxY; year += 1) { yearHtml += '"; } yearSelect.innerHTML = yearHtml; yearSelect.value = String(viewYear); var monthHtml = ""; for (var month = 0; month < 12; month += 1) { var disabled = !monthHasValidDay(viewYear, month); monthHtml += '"; } monthSelect.innerHTML = monthHtml; monthSelect.value = String(viewMonth); if (global.JinniuSelect && typeof global.JinniuSelect.scan === "function") { global.JinniuSelect.scan(panel); } } function render() { if (!grid || !titleEl) return; syncYearMonthSelects(); 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 += '"; 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; if (global.JinniuSelect && typeof global.JinniuSelect.close === "function") { global.JinniuSelect.close(); } 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) && !(event.target.closest && event.target.closest(".ds-listbox")) ) { 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);