161 lines
6.1 KiB
JavaScript
161 lines
6.1 KiB
JavaScript
// ── Custom dialog system ──
|
|
|
|
import { $ } from './util.js';
|
|
|
|
const overlay = $('dialog');
|
|
const titleEl = $('dialog-title');
|
|
const msgEl = $('dialog-msg');
|
|
const inputEl = $('dialog-input');
|
|
const datepickerEl = $('dialog-datepicker');
|
|
const cancelBtn = $('dialog-cancel');
|
|
const okBtn = $('dialog-ok');
|
|
const closeBtn = $('dialog-close');
|
|
let resolve = null;
|
|
let dpMode = false;
|
|
let dpYear, dpMonth, dpSelected;
|
|
|
|
const DAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
|
|
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
|
|
function canCancel() { return !cancelBtn.classList.contains('hidden'); }
|
|
|
|
function close(result) {
|
|
overlay.classList.remove('open');
|
|
datepickerEl.classList.add('hidden');
|
|
datepickerEl.innerHTML = '';
|
|
dpMode = false;
|
|
if (resolve) { resolve(result); resolve = null; }
|
|
}
|
|
|
|
function submit() {
|
|
if (dpMode) {
|
|
close(dpSelected || null);
|
|
} else {
|
|
close(inputEl.classList.contains('hidden') ? true : inputEl.value);
|
|
}
|
|
}
|
|
|
|
function pad(n) { return String(n).padStart(2, '0'); }
|
|
|
|
function renderDatepicker() {
|
|
const today = new Date();
|
|
const todayStr = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`;
|
|
const firstDay = new Date(dpYear, dpMonth, 1).getDay();
|
|
const startOffset = (firstDay + 6) % 7;
|
|
const daysInMonth = new Date(dpYear, dpMonth + 1, 0).getDate();
|
|
const prevDays = new Date(dpYear, dpMonth, 0).getDate();
|
|
|
|
let html = `<div class="dp-header">
|
|
<button data-dp-nav="-1"><i class="ph ph-caret-left text-xs"></i></button>
|
|
<span class="dp-title">${MONTHS[dpMonth]} ${dpYear}</span>
|
|
<button data-dp-nav="1"><i class="ph ph-caret-right text-xs"></i></button>
|
|
</div><div class="dp-grid">`;
|
|
|
|
for (const d of DAYS) html += `<div class="dp-day-label">${d}</div>`;
|
|
|
|
for (let i = 0; i < 42; i++) {
|
|
const dayNum = i - startOffset + 1;
|
|
let ds, display, outClass = '';
|
|
if (dayNum < 1) {
|
|
display = prevDays + dayNum;
|
|
const pm = dpMonth === 0 ? 11 : dpMonth - 1;
|
|
const py = dpMonth === 0 ? dpYear - 1 : dpYear;
|
|
ds = `${py}-${pad(pm + 1)}-${pad(display)}`;
|
|
outClass = 'dp-out';
|
|
} else if (dayNum > daysInMonth) {
|
|
display = dayNum - daysInMonth;
|
|
const nm = dpMonth === 11 ? 0 : dpMonth + 1;
|
|
const ny = dpMonth === 11 ? dpYear + 1 : dpYear;
|
|
ds = `${ny}-${pad(nm + 1)}-${pad(display)}`;
|
|
outClass = 'dp-out';
|
|
} else {
|
|
display = dayNum;
|
|
ds = `${dpYear}-${pad(dpMonth + 1)}-${pad(dayNum)}`;
|
|
}
|
|
const isToday = ds === todayStr ? 'dp-today' : '';
|
|
const isSel = ds === dpSelected ? 'dp-selected' : '';
|
|
html += `<div class="dp-day ${outClass} ${isToday} ${isSel}" data-dp-date="${ds}">${display}</div>`;
|
|
}
|
|
|
|
html += '</div>';
|
|
datepickerEl.innerHTML = html;
|
|
|
|
// Nav buttons
|
|
datepickerEl.querySelectorAll('[data-dp-nav]').forEach(btn => {
|
|
btn.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
dpMonth += parseInt(btn.dataset.dpNav);
|
|
if (dpMonth > 11) { dpMonth = 0; dpYear++; }
|
|
if (dpMonth < 0) { dpMonth = 11; dpYear--; }
|
|
renderDatepicker();
|
|
});
|
|
});
|
|
|
|
// Day click
|
|
datepickerEl.querySelectorAll('[data-dp-date]').forEach(day => {
|
|
day.addEventListener('click', () => {
|
|
dpSelected = day.dataset.dpDate;
|
|
// Parse selected date to navigate to its month
|
|
const [y, m] = dpSelected.split('-').map(Number);
|
|
if (y !== dpYear || m - 1 !== dpMonth) {
|
|
dpYear = y;
|
|
dpMonth = m - 1;
|
|
}
|
|
renderDatepicker();
|
|
});
|
|
day.addEventListener('dblclick', () => {
|
|
dpSelected = day.dataset.dpDate;
|
|
submit();
|
|
});
|
|
});
|
|
}
|
|
|
|
export function showDialog({ title, message = '', input = false, placeholder = '', defaultValue = '', danger = false, okLabel = 'ok', cancelLabel = 'cancel' }) {
|
|
return new Promise(res => {
|
|
resolve = res;
|
|
titleEl.textContent = title;
|
|
msgEl.textContent = message;
|
|
msgEl.classList.toggle('hidden', !message);
|
|
dpMode = input === 'date';
|
|
|
|
if (dpMode) {
|
|
inputEl.classList.add('hidden');
|
|
datepickerEl.classList.remove('hidden');
|
|
const d = defaultValue ? new Date(defaultValue + 'T00:00:00') : new Date();
|
|
dpYear = d.getFullYear();
|
|
dpMonth = d.getMonth();
|
|
dpSelected = defaultValue || `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
renderDatepicker();
|
|
} else {
|
|
datepickerEl.classList.add('hidden');
|
|
datepickerEl.innerHTML = '';
|
|
inputEl.classList.toggle('hidden', input === false);
|
|
inputEl.value = defaultValue;
|
|
inputEl.placeholder = placeholder;
|
|
inputEl.type = input === 'password' ? 'password' : 'text';
|
|
}
|
|
|
|
okBtn.textContent = okLabel;
|
|
okBtn.classList.toggle('border-t-danger', danger);
|
|
okBtn.classList.toggle('text-t-danger', danger);
|
|
okBtn.classList.toggle('border-t-accent', !danger);
|
|
okBtn.classList.toggle('text-t-accent', !danger);
|
|
cancelBtn.textContent = cancelLabel;
|
|
cancelBtn.classList.toggle('hidden', !cancelLabel);
|
|
if (closeBtn) closeBtn.classList.toggle('hidden', !cancelLabel);
|
|
overlay.classList.add('open');
|
|
if (input && !dpMode) inputEl.focus(); else okBtn.focus();
|
|
});
|
|
}
|
|
|
|
export function isDialogOpen() { return overlay.classList.contains('open'); }
|
|
|
|
okBtn.addEventListener('click', submit);
|
|
cancelBtn.addEventListener('click', () => { if (canCancel()) close(null); });
|
|
if (closeBtn) closeBtn.addEventListener('click', () => { if (canCancel()) close(null); });
|
|
overlay.addEventListener('click', e => { if (e.target === overlay && canCancel()) close(null); });
|
|
overlay.addEventListener('keydown', e => {
|
|
if (e.key === 'Escape' && canCancel()) close(null);
|
|
if (e.key === 'Enter') submit();
|
|
});
|