|
|
@@ -1,4 +1,272 @@
|
|
|
$(document).ready(function () {
|
|
|
+ function initMobileDatePicker() {
|
|
|
+ const mobileQuery = window.matchMedia('(max-width: 767.98px)');
|
|
|
+ const coarsePointer = window.matchMedia('(pointer: coarse)');
|
|
|
+
|
|
|
+ if (!mobileQuery.matches || !coarsePointer.matches) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const dateInputs = Array.from(document.querySelectorAll('input[type="date"]')).filter(function (input) {
|
|
|
+ return !input.disabled && !input.dataset.mobileDateEnhanced;
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!dateInputs.length) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const monthFormatter = new Intl.DateTimeFormat('ru-RU', {
|
|
|
+ month: 'long',
|
|
|
+ year: 'numeric',
|
|
|
+ });
|
|
|
+ const displayFormatter = new Intl.DateTimeFormat('ru-RU');
|
|
|
+ const weekdayLabels = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
|
|
|
+
|
|
|
+ const overlay = document.createElement('div');
|
|
|
+ overlay.className = 'mobile-date-picker';
|
|
|
+ overlay.hidden = true;
|
|
|
+ overlay.innerHTML = '' +
|
|
|
+ '<div class="mobile-date-picker__dialog" role="dialog" aria-modal="true" aria-label="Выбор даты">' +
|
|
|
+ '<div class="mobile-date-picker__header">' +
|
|
|
+ '<button type="button" class="btn btn-link mobile-date-picker__close" aria-label="Закрыть"><i class="bi bi-x-lg"></i></button>' +
|
|
|
+ '<div class="mobile-date-picker__title"></div>' +
|
|
|
+ '<button type="button" class="btn btn-link mobile-date-picker__today">Сегодня</button>' +
|
|
|
+ '</div>' +
|
|
|
+ '<div class="mobile-date-picker__month-nav">' +
|
|
|
+ '<button type="button" class="btn btn-link mobile-date-picker__nav mobile-date-picker__nav--prev" aria-label="Предыдущий месяц"><i class="bi bi-chevron-left"></i></button>' +
|
|
|
+ '<div class="mobile-date-picker__month-label"></div>' +
|
|
|
+ '<button type="button" class="btn btn-link mobile-date-picker__nav mobile-date-picker__nav--next" aria-label="Следующий месяц"><i class="bi bi-chevron-right"></i></button>' +
|
|
|
+ '</div>' +
|
|
|
+ '<div class="mobile-date-picker__weekdays"></div>' +
|
|
|
+ '<div class="mobile-date-picker__grid"></div>' +
|
|
|
+ '<div class="mobile-date-picker__footer">' +
|
|
|
+ '<button type="button" class="btn btn-outline-secondary mobile-date-picker__clear">Очистить</button>' +
|
|
|
+ '<button type="button" class="btn btn-primary mobile-date-picker__apply">Готово</button>' +
|
|
|
+ '</div>' +
|
|
|
+ '</div>';
|
|
|
+ document.body.appendChild(overlay);
|
|
|
+
|
|
|
+ const titleEl = overlay.querySelector('.mobile-date-picker__title');
|
|
|
+ const monthLabelEl = overlay.querySelector('.mobile-date-picker__month-label');
|
|
|
+ const weekdaysEl = overlay.querySelector('.mobile-date-picker__weekdays');
|
|
|
+ const gridEl = overlay.querySelector('.mobile-date-picker__grid');
|
|
|
+ const closeBtn = overlay.querySelector('.mobile-date-picker__close');
|
|
|
+ const todayBtn = overlay.querySelector('.mobile-date-picker__today');
|
|
|
+ const clearBtn = overlay.querySelector('.mobile-date-picker__clear');
|
|
|
+ const applyBtn = overlay.querySelector('.mobile-date-picker__apply');
|
|
|
+ const prevBtn = overlay.querySelector('.mobile-date-picker__nav--prev');
|
|
|
+ const nextBtn = overlay.querySelector('.mobile-date-picker__nav--next');
|
|
|
+
|
|
|
+ weekdaysEl.innerHTML = weekdayLabels.map(function (day) {
|
|
|
+ return '<div class="mobile-date-picker__weekday">' + day + '</div>';
|
|
|
+ }).join('');
|
|
|
+
|
|
|
+ function parseIsoDate(value) {
|
|
|
+ if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const parts = value.split('-').map(Number);
|
|
|
+ const date = new Date(parts[0], parts[1] - 1, parts[2]);
|
|
|
+
|
|
|
+ if (Number.isNaN(date.getTime())) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return date;
|
|
|
+ }
|
|
|
+
|
|
|
+ function formatIsoDate(date) {
|
|
|
+ const year = date.getFullYear();
|
|
|
+ const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
+ const day = String(date.getDate()).padStart(2, '0');
|
|
|
+
|
|
|
+ return year + '-' + month + '-' + day;
|
|
|
+ }
|
|
|
+
|
|
|
+ function formatDisplayDate(value) {
|
|
|
+ const date = parseIsoDate(value);
|
|
|
+
|
|
|
+ return date ? displayFormatter.format(date) : '';
|
|
|
+ }
|
|
|
+
|
|
|
+ function normalizeMonthLabel(date) {
|
|
|
+ const label = monthFormatter.format(date);
|
|
|
+
|
|
|
+ return label.charAt(0).toUpperCase() + label.slice(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ function startOfMonth(date) {
|
|
|
+ return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ function isSameDate(a, b) {
|
|
|
+ return a && b
|
|
|
+ && a.getFullYear() === b.getFullYear()
|
|
|
+ && a.getMonth() === b.getMonth()
|
|
|
+ && a.getDate() === b.getDate();
|
|
|
+ }
|
|
|
+
|
|
|
+ const state = {
|
|
|
+ activeInput: null,
|
|
|
+ hiddenInput: null,
|
|
|
+ displayInput: null,
|
|
|
+ selectedDate: null,
|
|
|
+ draftDate: null,
|
|
|
+ viewDate: startOfMonth(new Date()),
|
|
|
+ };
|
|
|
+
|
|
|
+ function syncDisplay(hiddenInput, displayInput) {
|
|
|
+ displayInput.value = formatDisplayDate(hiddenInput.value);
|
|
|
+ displayInput.classList.toggle('mobile-date-input--empty', !hiddenInput.value);
|
|
|
+ }
|
|
|
+
|
|
|
+ function closePicker() {
|
|
|
+ overlay.hidden = true;
|
|
|
+ document.body.classList.remove('mobile-date-picker-open');
|
|
|
+ state.activeInput = null;
|
|
|
+ state.hiddenInput = null;
|
|
|
+ state.displayInput = null;
|
|
|
+ state.selectedDate = null;
|
|
|
+ state.draftDate = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderCalendar() {
|
|
|
+ const today = new Date();
|
|
|
+ const monthStart = startOfMonth(state.viewDate);
|
|
|
+ const gridStart = new Date(monthStart);
|
|
|
+ const firstWeekday = (monthStart.getDay() + 6) % 7;
|
|
|
+
|
|
|
+ gridStart.setDate(monthStart.getDate() - firstWeekday);
|
|
|
+
|
|
|
+ monthLabelEl.textContent = normalizeMonthLabel(monthStart);
|
|
|
+ gridEl.innerHTML = '';
|
|
|
+
|
|
|
+ for (let index = 0; index < 42; index += 1) {
|
|
|
+ const day = new Date(gridStart);
|
|
|
+ day.setDate(gridStart.getDate() + index);
|
|
|
+
|
|
|
+ const button = document.createElement('button');
|
|
|
+ button.type = 'button';
|
|
|
+ button.className = 'mobile-date-picker__day';
|
|
|
+ button.textContent = String(day.getDate());
|
|
|
+ button.dataset.value = formatIsoDate(day);
|
|
|
+
|
|
|
+ if (day.getMonth() !== monthStart.getMonth()) {
|
|
|
+ button.classList.add('is-outside');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isSameDate(day, today)) {
|
|
|
+ button.classList.add('is-today');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (state.draftDate && isSameDate(day, state.draftDate)) {
|
|
|
+ button.classList.add('is-selected');
|
|
|
+ }
|
|
|
+
|
|
|
+ button.addEventListener('click', function () {
|
|
|
+ state.draftDate = parseIsoDate(button.dataset.value);
|
|
|
+ renderCalendar();
|
|
|
+ });
|
|
|
+
|
|
|
+ gridEl.appendChild(button);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function openPicker(hiddenInput, displayInput) {
|
|
|
+ state.activeInput = hiddenInput;
|
|
|
+ state.hiddenInput = hiddenInput;
|
|
|
+ state.displayInput = displayInput;
|
|
|
+ state.selectedDate = parseIsoDate(hiddenInput.value);
|
|
|
+ state.draftDate = state.selectedDate;
|
|
|
+ state.viewDate = startOfMonth(state.selectedDate || new Date());
|
|
|
+
|
|
|
+ titleEl.textContent = hiddenInput.dataset.mobileDateTitle || 'Выбор даты';
|
|
|
+ overlay.hidden = false;
|
|
|
+ document.body.classList.add('mobile-date-picker-open');
|
|
|
+ renderCalendar();
|
|
|
+ }
|
|
|
+
|
|
|
+ closeBtn.addEventListener('click', closePicker);
|
|
|
+
|
|
|
+ overlay.addEventListener('click', function (event) {
|
|
|
+ if (event.target === overlay) {
|
|
|
+ closePicker();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ prevBtn.addEventListener('click', function () {
|
|
|
+ state.viewDate = new Date(state.viewDate.getFullYear(), state.viewDate.getMonth() - 1, 1);
|
|
|
+ renderCalendar();
|
|
|
+ });
|
|
|
+
|
|
|
+ nextBtn.addEventListener('click', function () {
|
|
|
+ state.viewDate = new Date(state.viewDate.getFullYear(), state.viewDate.getMonth() + 1, 1);
|
|
|
+ renderCalendar();
|
|
|
+ });
|
|
|
+
|
|
|
+ todayBtn.addEventListener('click', function () {
|
|
|
+ state.draftDate = startOfMonth(new Date());
|
|
|
+ state.draftDate = new Date(state.draftDate.getFullYear(), state.draftDate.getMonth(), new Date().getDate());
|
|
|
+ state.viewDate = startOfMonth(state.draftDate);
|
|
|
+ renderCalendar();
|
|
|
+ });
|
|
|
+
|
|
|
+ clearBtn.addEventListener('click', function () {
|
|
|
+ if (!state.hiddenInput || !state.displayInput) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ state.hiddenInput.value = '';
|
|
|
+ state.hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
+ state.hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
+ syncDisplay(state.hiddenInput, state.displayInput);
|
|
|
+ closePicker();
|
|
|
+ });
|
|
|
+
|
|
|
+ applyBtn.addEventListener('click', function () {
|
|
|
+ if (!state.hiddenInput || !state.displayInput || !state.draftDate) {
|
|
|
+ closePicker();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ state.hiddenInput.value = formatIsoDate(state.draftDate);
|
|
|
+ state.hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
+ state.hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
+ syncDisplay(state.hiddenInput, state.displayInput);
|
|
|
+ closePicker();
|
|
|
+ });
|
|
|
+
|
|
|
+ document.addEventListener('keydown', function (event) {
|
|
|
+ if (event.key === 'Escape' && !overlay.hidden) {
|
|
|
+ closePicker();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ dateInputs.forEach(function (input) {
|
|
|
+ input.dataset.mobileDateEnhanced = '1';
|
|
|
+ input.dataset.mobileDateTitle = input.closest('.row')?.querySelector('label[for="' + input.id + '"]')?.textContent.trim() || 'Выбор даты';
|
|
|
+
|
|
|
+ const displayInput = input.cloneNode(false);
|
|
|
+ displayInput.type = 'text';
|
|
|
+ displayInput.removeAttribute('name');
|
|
|
+ displayInput.value = '';
|
|
|
+ displayInput.readOnly = true;
|
|
|
+ displayInput.dataset.mobileDateDisplay = '1';
|
|
|
+ displayInput.classList.add('mobile-date-input');
|
|
|
+ displayInput.placeholder = 'Выберите дату';
|
|
|
+ input.type = 'hidden';
|
|
|
+ input.dataset.mobileDateHidden = '1';
|
|
|
+
|
|
|
+ input.parentNode.insertBefore(displayInput, input.nextSibling);
|
|
|
+ syncDisplay(input, displayInput);
|
|
|
+
|
|
|
+ displayInput.addEventListener('click', function () {
|
|
|
+ openPicker(input, displayInput);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
function cleanupStaleModalState() {
|
|
|
if (!document.querySelector('.modal.show')) {
|
|
|
document.querySelectorAll('.modal-backdrop').forEach(function (backdrop) {
|
|
|
@@ -98,6 +366,7 @@ $(document).ready(function () {
|
|
|
}
|
|
|
|
|
|
cleanupStaleModalState();
|
|
|
+ initMobileDatePicker();
|
|
|
window.addEventListener('pageshow', cleanupStaleModalState);
|
|
|
|
|
|
if ($('.main-alert').length) {
|