/
webru
/
prisma-ui
Обзор
Документация
Войти
/
webru
/
prisma-ui
Код
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ops-datetime.js
442 строки
16 KB
webru
Add Ops narrative patterns, node detail demo, and UI conventions.
14 июн 2026, 14:28
14 июн 2026, 14:28
47b1546
Код
Авторство
О чём код?
/** * Prisma UI — Date/time pickers: calendar dropdown + 24h time selector */ (function () { var DATE_RE = /^(\d{2})\.(\d{2})\.(\d{4})$/; var TIME_RE = /^(\d{2}):(\d{2})(?::(\d{2}))?$/; var MONTHS_RU = [ "январь", "февраль", "март", "апрель", "май", "июнь", "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь" ]; var WEEKDAYS_RU = ["пн", "вт", "ср", "чт", "пт", "сб", "вс"]; var openPicker = null; function pad(n) { return n < 10 ? "0" + n : String(n); } function copyClasses(from, to, list) { list.forEach(function (cls) { if (from.classList.contains(cls)) to.classList.add(cls); }); } function parseDateEU(str) { var m = (str || "").match(DATE_RE); if (!m) return null; var d = new Date(parseInt(m[3], 10), parseInt(m[2], 10) - 1, parseInt(m[1], 10)); return isNaN(d.getTime()) ? null : d; } function formatDateEU(date) { return pad(date.getDate()) + "." + pad(date.getMonth() + 1) + "." + date.getFullYear(); } function parseTime(str) { var m = (str || "").match(TIME_RE); if (!m) return null; return { h: parseInt(m[1], 10), m: parseInt(m[2], 10), s: m[3] != null ? parseInt(m[3], 10) : 0 }; } function formatTime(parts, withSeconds) { var s = pad(parts.h) + ":" + pad(parts.m); if (withSeconds) s += ":" + pad(parts.s); return s; } function positionPanel(trigger, panel) { var rect = trigger.getBoundingClientRect(); panel.style.top = rect.bottom + 4 + "px"; panel.style.left = rect.left + "px"; var w = Math.max(rect.width, panel.classList.contains("ops-timepick__panel") ? 160 : 240); panel.style.minWidth = w + "px"; } function closePicker(picker) { if (!picker) return; picker.classList.remove("is-open"); if (openPicker === picker) openPicker = null; } function openPickerPanel(picker, trigger, panel) { if (openPicker && openPicker !== picker) closePicker(openPicker); positionPanel(trigger, panel); picker.classList.add("is-open"); openPicker = picker; } function maskDateInput(input) { var v = input.value.replace(/[^\d.]/g, ""); var digits = v.replace(/\./g, ""); if (digits.length <= 2) input.value = digits; else if (digits.length <= 4) input.value = digits.slice(0, 2) + "." + digits.slice(2); else input.value = digits.slice(0, 2) + "." + digits.slice(2, 4) + "." + digits.slice(4, 8); } function maskTimeInput(input, withSeconds) { var v = input.value.replace(/[^\d:]/g, ""); var digits = v.replace(/:/g, ""); if (digits.length <= 2) input.value = digits; else if (digits.length <= 4) input.value = digits.slice(0, 2) + ":" + digits.slice(2); else if (withSeconds) input.value = digits.slice(0, 2) + ":" + digits.slice(2, 4) + ":" + digits.slice(4, 6); else input.value = digits.slice(0, 2) + ":" + digits.slice(2, 4); } function renderCalendar(panel, viewDate, selected, onPick) { var y = viewDate.getFullYear(); var mo = viewDate.getMonth(); var first = new Date(y, mo, 1); var start = new Date(first); var dow = (first.getDay() + 6) % 7; start.setDate(start.getDate() - dow); panel.innerHTML = '<div class="ops-cal__head">' + '<button type="button" class="ops-cal__nav" data-cal-prev aria-label="Предыдущий месяц">‹</button>' + '<span class="ops-cal__title"></span>' + '<button type="button" class="ops-cal__nav" data-cal-next aria-label="Следующий месяц">›</button>' + '</div>' + '<div class="ops-cal__weekdays"></div>' + '<div class="ops-cal__grid"></div>' + '<div class="ops-cal__foot">' + '<button type="button" class="ops-cal__foot-btn" data-cal-today>Сегодня</button>' + '<button type="button" class="ops-cal__foot-btn" data-cal-clear>Очистить</button>' + '</div>'; panel.querySelector(".ops-cal__title").textContent = MONTHS_RU[mo] + " " + y; var weekdays = panel.querySelector(".ops-cal__weekdays"); WEEKDAYS_RU.forEach(function (wd) { var el = document.createElement("span"); el.className = "ops-cal__weekday"; el.textContent = wd; weekdays.appendChild(el); }); var grid = panel.querySelector(".ops-cal__grid"); var today = new Date(); today.setHours(0, 0, 0, 0); var cursor = new Date(start); for (var i = 0; i < 42; i++) { var btn = document.createElement("button"); btn.type = "button"; btn.className = "ops-cal__day"; btn.textContent = pad(cursor.getDate()); var cellDate = new Date(cursor); if (cellDate.getMonth() !== mo) btn.classList.add("is-outside"); if (selected && cellDate.getTime() === selected.getTime()) btn.classList.add("is-selected"); if (cellDate.getTime() === today.getTime()) btn.classList.add("is-today"); (function (d) { btn.addEventListener("click", function () { onPick(new Date(d)); }); })(cellDate); grid.appendChild(btn); cursor.setDate(cursor.getDate() + 1); } panel.querySelector("[data-cal-prev]").addEventListener("click", function () { var nd = new Date(viewDate); nd.setMonth(nd.getMonth() - 1); renderCalendar(panel, nd, selected, onPick); }); panel.querySelector("[data-cal-next]").addEventListener("click", function () { var nd = new Date(viewDate); nd.setMonth(nd.getMonth() + 1); renderCalendar(panel, nd, selected, onPick); }); panel.querySelector("[data-cal-today]").addEventListener("click", function () { onPick(new Date(today)); }); panel.querySelector("[data-cal-clear]").addEventListener("click", function () { onPick(null); }); } function renderTimePanel(panel, input, withSeconds) { var current = parseTime(input.value) || { h: 0, m: 0, s: 0 }; panel.innerHTML = '<div class="ops-timepick__cols"></div><div class="ops-timepick__foot"><button type="button" class="ops-cal__foot-btn" data-time-now>Сейчас</button></div>'; var cols = panel.querySelector(".ops-timepick__cols"); function addCol(label, max, key) { var col = document.createElement("div"); col.className = "ops-timepick__col"; col.innerHTML = '<div class="ops-timepick__col-label"></div>'; col.querySelector(".ops-timepick__col-label").textContent = label; for (var i = 0; i <= max; i++) { var btn = document.createElement("button"); btn.type = "button"; btn.className = "ops-timepick__val"; btn.textContent = pad(i); if (current[key] === i) btn.classList.add("is-selected"); (function (val) { btn.addEventListener("click", function () { current[key] = val; input.value = formatTime(current, withSeconds); input.dispatchEvent(new Event("change", { bubbles: true })); col.querySelectorAll(".ops-timepick__val").forEach(function (b) { b.classList.toggle("is-selected", parseInt(b.textContent, 10) === val); }); }); })(i); col.appendChild(btn); } cols.appendChild(col); var sel = col.querySelector(".is-selected"); if (sel) sel.scrollIntoView({ block: "center" }); } addCol("чч", 23, "h"); addCol("мм", 59, "m"); if (withSeconds) addCol("сс", 59, "s"); panel.querySelector("[data-time-now]").addEventListener("click", function () { var n = new Date(); current = { h: n.getHours(), m: n.getMinutes(), s: n.getSeconds() }; input.value = formatTime(current, withSeconds); input.dispatchEvent(new Event("change", { bubbles: true })); renderTimePanel(panel, input, withSeconds); }); } function enhanceDateInput(input) { if (input.dataset.opsDatepick) return; input.dataset.opsDatepick = "true"; var wrap = document.createElement("div"); wrap.className = "ops-datepick"; copyClasses(input, wrap, ["ops-input--filter", "ops-input--sm", "ops-input--lg"]); input.parentNode.insertBefore(wrap, input); var field = document.createElement("div"); field.className = "ops-datepick__field"; wrap.appendChild(field); field.appendChild(input); if (input.type === "date" && input.value) { input.type = "text"; var p = input.value.split("-"); if (p.length === 3) input.value = pad(parseInt(p[2], 10)) + "." + pad(parseInt(p[1], 10)) + "." + p[0]; } else if (input.type === "date") { input.type = "text"; } input.classList.add("ops-input--date-eu"); input.setAttribute("placeholder", "dd.mm.yyyy"); input.setAttribute("autocomplete", "off"); input.setAttribute("inputmode", "numeric"); var btn = document.createElement("button"); btn.type = "button"; btn.className = "ops-datepick__btn"; btn.setAttribute("aria-label", "Открыть календарь"); btn.innerHTML = "📅"; field.appendChild(btn); var panel = document.createElement("div"); panel.className = "ops-datepick__panel"; panel.setAttribute("role", "dialog"); panel.setAttribute("aria-label", "Календарь"); wrap.appendChild(panel); var viewDate = parseDateEU(input.value) || new Date(); function pickDate(date) { if (date) { input.value = formatDateEU(date); viewDate = new Date(date); } else { input.value = ""; } input.classList.remove("ops-input--error"); input.dispatchEvent(new Event("change", { bubbles: true })); closePicker(wrap); renderCalendar(panel, viewDate, date ? new Date(date.getFullYear(), date.getMonth(), date.getDate()) : null, pickDate); } renderCalendar(panel, viewDate, parseDateEU(input.value), pickDate); btn.addEventListener("click", function (e) { e.stopPropagation(); if (wrap.classList.contains("is-open")) closePicker(wrap); else { viewDate = parseDateEU(input.value) || new Date(); renderCalendar(panel, viewDate, parseDateEU(input.value), pickDate); openPickerPanel(wrap, field, panel); } }); input.addEventListener("input", function () { maskDateInput(input); }); input.addEventListener("blur", function () { if (input.value && !parseDateEU(input.value)) input.classList.add("ops-input--error"); else input.classList.remove("ops-input--error"); }); } function enhanceTimeInput(input) { if (input.dataset.opsTimepick) return; input.dataset.opsTimepick = "true"; var withSeconds = input.dataset.opsTimeSeconds === "true"; var wrap = document.createElement("div"); wrap.className = "ops-timepick"; copyClasses(input, wrap, ["ops-input--filter", "ops-input--sm", "ops-input--lg"]); input.parentNode.insertBefore(wrap, input); var field = document.createElement("div"); field.className = "ops-timepick__field"; wrap.appendChild(field); field.appendChild(input); if (input.type === "time" && input.value) { input.type = "text"; var p = input.value.split(":"); input.value = formatTime({ h: parseInt(p[0], 10), m: parseInt(p[1], 10), s: p[2] != null ? parseInt(p[2], 10) : 0 }, withSeconds); } else if (input.type === "time") { input.type = "text"; } input.classList.add("ops-input--time-24"); input.setAttribute("placeholder", withSeconds ? "HH:MM:SS" : "HH:MM"); input.setAttribute("autocomplete", "off"); input.setAttribute("inputmode", "numeric"); var btn = document.createElement("button"); btn.type = "button"; btn.className = "ops-timepick__btn"; btn.setAttribute("aria-label", "Выбрать время"); btn.innerHTML = "🕐"; field.appendChild(btn); var panel = document.createElement("div"); panel.className = "ops-timepick__panel"; panel.setAttribute("role", "dialog"); panel.setAttribute("aria-label", "Выбор времени"); wrap.appendChild(panel); renderTimePanel(panel, input, withSeconds); btn.addEventListener("click", function (e) { e.stopPropagation(); if (wrap.classList.contains("is-open")) closePicker(wrap); else { renderTimePanel(panel, input, withSeconds); openPickerPanel(wrap, field, panel); } }); input.addEventListener("input", function () { maskTimeInput(input, withSeconds); }); input.addEventListener("blur", function () { if (input.value && !parseTime(input.value)) input.classList.add("ops-input--error"); else input.classList.remove("ops-input--error"); }); } function enhanceDatetimePair(input) { if (input.dataset.opsDatetimePair) return; input.dataset.opsDatetimePair = "true"; var val = input.value; var dateVal = ""; var timeVal = ""; if (val && val.indexOf("T") > -1) { var parts = val.split("T"); var dp = parts[0].split("-"); dateVal = pad(parseInt(dp[2], 10)) + "." + pad(parseInt(dp[1], 10)) + "." + dp[0]; timeVal = parts[1].slice(0, 5); } else if (val.indexOf(" ") > -1) { var sp = val.split(" "); dateVal = sp[0]; timeVal = sp[1]; } var pair = document.createElement("div"); pair.className = "ops-datetime-pair"; input.parentNode.insertBefore(pair, input); var dateInput = document.createElement("input"); dateInput.type = "text"; dateInput.className = input.className.replace("ops-input--datetime", "").trim() + " ops-input"; dateInput.value = dateVal; dateInput.id = input.id ? input.id + "-date" : ""; pair.appendChild(dateInput); var timeInput = document.createElement("input"); timeInput.type = "text"; timeInput.className = "ops-input ops-input--time-24"; timeInput.value = timeVal; timeInput.id = input.id ? input.id + "-time" : ""; pair.appendChild(timeInput); input.remove(); enhanceDateInput(dateInput); enhanceTimeInput(timeInput); } function initDateTimeInputs(root) { var scope = root || document; scope.querySelectorAll("input[type='datetime-local'].ops-input, .ops-input--datetime-pair").forEach(enhanceDatetimePair); scope.querySelectorAll(".ops-input--date-eu, input[type='date'].ops-input").forEach(function (input) { if (!input.closest(".ops-datepick")) enhanceDateInput(input); }); scope.querySelectorAll(".ops-input--time-24, input[type='time'].ops-input").forEach(function (input) { if (!input.closest(".ops-timepick")) enhanceTimeInput(input); }); } document.addEventListener("click", function () { if (openPicker) closePicker(openPicker); }); document.addEventListener("keydown", function (e) { if (e.key === "Escape" && openPicker) closePicker(openPicker); }); window.addEventListener("resize", function () { if (!openPicker) return; var field = openPicker.querySelector(".ops-datepick__field, .ops-timepick__field"); var panel = openPicker.querySelector(".ops-datepick__panel, .ops-timepick__panel"); if (field && panel) positionPanel(field, panel); }); function initDisplayTimestamps(root) { var isoRe = /^(\d{4})-(\d{2})-(\d{2})(?:[T\s]+)(.+)$/; (root || document).querySelectorAll(".ops-ts, .ops-datetime, .ops-log__ts").forEach(function (el) { var text = el.textContent.trim(); var m = text.match(isoRe); if (m) el.textContent = m[3] + "." + m[2] + "." + m[1] + " " + m[4].replace(/\s*UTC\s*$/i, ""); }); } document.addEventListener("DOMContentLoaded", function () { initDateTimeInputs(); initDisplayTimestamps(); }); window.initDateTimeInputs = initDateTimeInputs; window.initDisplayTimestamps = initDisplayTimestamps; window.OpsDateTime = { formatDate: formatDateEU, formatTime: formatTime, parseDate: parseDateEU, parseTime: parseTime, formatDisplay: function (y, mo, d, h, mi, s, ms) { var out = pad(d) + "." + pad(mo) + "." + y; if (h != null) { out += " " + pad(h) + ":" + pad(mi); if (s != null) out += ":" + pad(s); if (ms != null) out += "." + ms; } return out; } }; })();