Files
Htmx/Htmx.ApiDemo/publish-test/wwwroot/js/components.js
T

718 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ─────────────────────────────────────────────────────────────────────────
* components.js client-side logic for htmx server-rendered components
* ───────────────────────────────────────────────────────────────────────── */
// ── Calendar ──────────────────────────────────────────────────────────────
(function () {
var MONTHS = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
function renderCalendar(root) {
var view = root.dataset.view || 'days';
var year = parseInt(root.dataset.year, 10);
var month = parseInt(root.dataset.month, 10);
var selD = parseInt(root.dataset.selDay, 10);
var selM = parseInt(root.dataset.selMonth, 10);
var selY = parseInt(root.dataset.selYear, 10);
var labelBtn = root.querySelector('.cal-month-label');
var grid = root.querySelector('.cal-grid');
var dowRow = root.querySelector('.cal-dow-row');
// ── Update header label based on view ──
if (view === 'days') {
labelBtn.textContent = MONTHS[month] + ' ' + year;
} else if (view === 'months') {
labelBtn.textContent = year;
} else { // years
var ds = Math.floor(year / 12) * 12;
labelBtn.textContent = ds + ' ' + (ds + 11);
}
// Show DOW row only in day view
if (dowRow) dowRow.style.display = view === 'days' ? '' : 'none';
grid.innerHTML = '';
if (view === 'days') {
grid.style.gridTemplateColumns = ''; // let CSS class (grid-cols-7) take over
var firstDay = new Date(year, month, 1).getDay();
var daysInMonth = new Date(year, month + 1, 0).getDate();
for (var i = 0; i < firstDay; i++) {
grid.appendChild(document.createElement('div'));
}
for (var d = 1; d <= daysInMonth; d++) {
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = d;
btn.className = 'cal-day';
if (d === selD && month === selM && year === selY) {
btn.classList.add('cal-day-selected');
}
btn.dataset.date = year + '-'
+ String(month + 1).padStart(2, '0') + '-'
+ String(d).padStart(2, '0');
btn.addEventListener('click', (function (b, r) {
return function () {
var parts = b.dataset.date.split('-');
r.dataset.selYear = parts[0];
r.dataset.selMonth = parseInt(parts[1], 10) - 1;
r.dataset.selDay = parseInt(parts[2], 10);
r.querySelectorAll('.cal-day').forEach(function (el) {
el.classList.remove('cal-day-selected');
});
b.classList.add('cal-day-selected');
r.querySelector('.cal-hidden-input').value = b.dataset.date;
r.dispatchEvent(new CustomEvent('calendarChange', {
detail: { date: b.dataset.date },
bubbles: true
}));
};
})(btn, root));
grid.appendChild(btn);
}
} else if (view === 'months') {
grid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
MONTHS.forEach(function (name, i) {
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = name.slice(0, 3);
btn.className = 'cal-view-btn' + (i === month ? ' cal-view-btn-selected' : '');
btn.addEventListener('click', function () {
root.dataset.month = i;
root.dataset.view = 'days';
renderCalendar(root);
});
grid.appendChild(btn);
});
} else { // years
var decadeStart = Math.floor(year / 12) * 12;
grid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
for (var yi = 0; yi < 12; yi++) {
(function (y) {
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = y;
btn.className = 'cal-view-btn' + (y === year ? ' cal-view-btn-selected' : '');
btn.addEventListener('click', function () {
root.dataset.year = y;
root.dataset.view = 'months';
renderCalendar(root);
});
grid.appendChild(btn);
})(decadeStart + yi);
}
}
}
function initCalendar(root) {
root.querySelector('.cal-prev').addEventListener('click', function () {
var view = root.dataset.view || 'days';
var m = parseInt(root.dataset.month, 10);
var y = parseInt(root.dataset.year, 10);
if (view === 'days') {
if (m === 0) { m = 11; y--; } else { m--; }
root.dataset.month = m;
root.dataset.year = y;
} else if (view === 'months') {
root.dataset.year = y - 1;
} else { // years
root.dataset.year = Math.floor(y / 12) * 12 - 12;
}
renderCalendar(root);
});
root.querySelector('.cal-next').addEventListener('click', function () {
var view = root.dataset.view || 'days';
var m = parseInt(root.dataset.month, 10);
var y = parseInt(root.dataset.year, 10);
if (view === 'days') {
if (m === 11) { m = 0; y++; } else { m++; }
root.dataset.month = m;
root.dataset.year = y;
} else if (view === 'months') {
root.dataset.year = y + 1;
} else { // years
root.dataset.year = Math.floor(y / 12) * 12 + 12;
}
renderCalendar(root);
});
root.querySelector('.cal-month-label').addEventListener('click', function () {
var view = root.dataset.view || 'days';
if (view === 'days') root.dataset.view = 'months';
else if (view === 'months') root.dataset.view = 'years';
// already at years — nothing deeper
renderCalendar(root);
});
renderCalendar(root);
}
// Initialise all calendars on page load, and again after any htmx swap
function initAll() {
document.querySelectorAll('.calendar-root').forEach(initCalendar);
}
document.addEventListener('DOMContentLoaded', initAll);
document.addEventListener('htmx:afterSwap', initAll);
})();
// ── CalendarRange ─────────────────────────────────────────────────────────
(function () {
var MONTHS = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
function cmpDate(a, b) { return a < b ? -1 : a > b ? 1 : 0; }
function isBetween(d, start, end) {
return cmpDate(d, start) > 0 && cmpDate(d, end) < 0;
}
function toDateStr(year, month, day) {
return year + '-'
+ String(month + 1).padStart(2, '0') + '-'
+ String(day).padStart(2, '0');
}
function updateLabel(root) {
var lbl = root.querySelector('.calr-label');
if (!lbl) return;
var start = root.dataset.start;
var end = root.dataset.end;
if (start && end) { lbl.textContent = start + ' → ' + end; return; }
if (start) { lbl.textContent = start + ' → pick end date'; return; }
lbl.textContent = '';
}
// Only updates CSS classes on already-rendered buttons — no DOM destruction.
function updateHoverClasses(root, hoverDate) {
var start = root.dataset.start;
var end = root.dataset.end;
var rangeEnd = (start && !end && hoverDate && cmpDate(hoverDate, start) >= 0)
? hoverDate : end;
root.querySelectorAll('.calr-day').forEach(function (btn) {
var ds = btn.dataset.date;
var isStart = !!(start && ds === start);
var isEnd = !!(end && ds === end);
var isHoverEnd = !!(!end && start && hoverDate && ds === hoverDate
&& cmpDate(hoverDate, start) > 0);
var isMid = !!(start && rangeEnd && isBetween(ds, start, rangeEnd));
btn.classList.remove('calr-day-start', 'calr-day-end', 'calr-day-mid', 'calr-day-plain');
if (isStart) btn.classList.add('calr-day-start');
if (isEnd || isHoverEnd) btn.classList.add('calr-day-end');
if (isMid) btn.classList.add('calr-day-mid');
if (!isStart && !isEnd && !isHoverEnd && !isMid) btn.classList.add('calr-day-plain');
});
}
// Full re-render of the grid. Called on mount, click, and view changes.
function renderRange(root) {
var view = root.dataset.view || 'days';
var year = parseInt(root.dataset.year, 10);
var month = parseInt(root.dataset.month, 10);
var start = root.dataset.start || '';
var end = root.dataset.end || '';
var labelBtn = root.querySelector('.calr-month-label');
var grid = root.querySelector('.calr-grid');
var dowRow = root.querySelector('.cal-dow-row');
// ── Update header label ──
if (view === 'days') {
labelBtn.textContent = MONTHS[month] + ' ' + year;
} else if (view === 'months') {
labelBtn.textContent = year;
} else { // years
var ds = Math.floor(year / 12) * 12;
labelBtn.textContent = ds + ' ' + (ds + 11);
}
if (dowRow) dowRow.style.display = view === 'days' ? '' : 'none';
grid.innerHTML = '';
// ── Clear event handlers (will be reassigned per view below) ──
grid.onmouseover = null;
grid.onmouseleave = null;
if (view === 'days') {
grid.style.gridTemplateColumns = '';
var firstDay = new Date(year, month, 1).getDay();
var daysInMonth = new Date(year, month + 1, 0).getDate();
for (var i = 0; i < firstDay; i++) {
grid.appendChild(document.createElement('div'));
}
for (var d = 1; d <= daysInMonth; d++) {
var dateStr = toDateStr(year, month, d);
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = d;
btn.dataset.date = dateStr;
var isStart = start && dateStr === start;
var isEnd = end && dateStr === end;
var isMid = start && end && isBetween(dateStr, start, end);
var cls = 'calr-day';
if (isStart) cls += ' calr-day-start';
else if (isEnd) cls += ' calr-day-end';
else if (isMid) cls += ' calr-day-mid';
else cls += ' calr-day-plain';
btn.className = cls;
grid.appendChild(btn);
}
// Click: update state → full re-render
grid.onclick = function (e) {
var btn = e.target.closest('.calr-day');
if (!btn) return;
var ds = btn.dataset.date;
var s = root.dataset.start;
var en = root.dataset.end;
if (!s || (s && en)) {
root.dataset.start = ds;
root.dataset.end = '';
} else {
if (cmpDate(ds, s) > 0) {
root.dataset.end = ds;
} else if (cmpDate(ds, s) < 0) {
root.dataset.start = ds;
root.dataset.end = '';
} else {
root.dataset.start = '';
root.dataset.end = '';
}
}
root.querySelector('.calr-hidden-start').value = root.dataset.start;
root.querySelector('.calr-hidden-end').value = root.dataset.end;
root.dispatchEvent(new CustomEvent('rangeChange', {
detail: { start: root.dataset.start, end: root.dataset.end },
bubbles: true
}));
renderRange(root);
updateLabel(root);
};
grid.onmouseover = function (e) {
var btn = e.target.closest('.calr-day');
if (!btn) return;
updateHoverClasses(root, btn.dataset.date);
};
grid.onmouseleave = function () {
updateHoverClasses(root, null);
};
} else if (view === 'months') {
grid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
MONTHS.forEach(function (name, i) {
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = name.slice(0, 3);
btn.className = 'cal-view-btn' + (i === month ? ' cal-view-btn-selected' : '');
btn.addEventListener('click', function () {
root.dataset.month = i;
root.dataset.view = 'days';
renderRange(root);
});
grid.appendChild(btn);
});
} else { // years
var decadeStart = Math.floor(year / 12) * 12;
grid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
for (var yi = 0; yi < 12; yi++) {
(function (y) {
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = y;
btn.className = 'cal-view-btn' + (y === year ? ' cal-view-btn-selected' : '');
btn.addEventListener('click', function () {
root.dataset.year = y;
root.dataset.view = 'months';
renderRange(root);
});
grid.appendChild(btn);
})(decadeStart + yi);
}
}
}
function initCalendarRange(root) {
root.querySelector('.calr-prev').addEventListener('click', function () {
var view = root.dataset.view || 'days';
var m = parseInt(root.dataset.month, 10);
var y = parseInt(root.dataset.year, 10);
if (view === 'days') {
if (m === 0) { m = 11; y--; } else { m--; }
root.dataset.month = m;
root.dataset.year = y;
} else if (view === 'months') {
root.dataset.year = y - 1;
} else { // years
root.dataset.year = Math.floor(y / 12) * 12 - 12;
}
renderRange(root);
});
root.querySelector('.calr-next').addEventListener('click', function () {
var view = root.dataset.view || 'days';
var m = parseInt(root.dataset.month, 10);
var y = parseInt(root.dataset.year, 10);
if (view === 'days') {
if (m === 11) { m = 0; y++; } else { m++; }
root.dataset.month = m;
root.dataset.year = y;
} else if (view === 'months') {
root.dataset.year = y + 1;
} else { // years
root.dataset.year = Math.floor(y / 12) * 12 + 12;
}
renderRange(root);
});
root.querySelector('.calr-month-label').addEventListener('click', function () {
var view = root.dataset.view || 'days';
if (view === 'days') root.dataset.view = 'months';
else if (view === 'months') root.dataset.view = 'years';
renderRange(root);
});
renderRange(root);
updateLabel(root);
}
function initAll() {
document.querySelectorAll('.calr-root').forEach(initCalendarRange);
}
document.addEventListener('DOMContentLoaded', initAll);
document.addEventListener('htmx:afterSwap', initAll);
})();
// ── TimePicker ────────────────────────────────────────────────────────────
(function () {
function syncTime(root) {
var h = parseInt(root.querySelector('.timepicker-hour').value, 10) || 0;
var m = parseInt(root.querySelector('.timepicker-minute').value, 10) || 0;
var use12h = root.dataset.use12h === 'true';
var h24 = h;
if (use12h) {
var ampmEl = root.querySelector('.timepicker-ampm');
var ampm = ampmEl ? ampmEl.value : 'AM';
if (ampm === 'PM') { h24 = h === 12 ? 12 : h + 12; }
else { h24 = h === 12 ? 0 : h; }
}
root.querySelector('.timepicker-hidden').value =
String(h24).padStart(2, '0') + ':' + String(m).padStart(2, '0');
}
function initTimePicker(root) {
var sync = syncTime.bind(null, root);
root.querySelector('.timepicker-hour').addEventListener('input', sync);
root.querySelector('.timepicker-minute').addEventListener('input', sync);
var ampmEl = root.querySelector('.timepicker-ampm');
if (ampmEl) ampmEl.addEventListener('change', sync);
sync();
}
function initAll() {
document.querySelectorAll('.timepicker-root').forEach(initTimePicker);
}
document.addEventListener('DOMContentLoaded', initAll);
document.addEventListener('htmx:afterSwap', initAll);
})();
// ── Switch ────────────────────────────────────────────────────────────────
(function () {
function updateSwitch(input) {
var track = input.parentElement && input.parentElement.querySelector('.switch-track');
if (!track) return;
var thumb = track.querySelector('.switch-thumb');
if (input.checked) {
track.classList.add('bg-primary');
track.classList.remove('bg-input');
if (thumb) thumb.style.transform = 'translateX(1.375rem)';
} else {
track.classList.remove('bg-primary');
track.classList.add('bg-input');
if (thumb) thumb.style.transform = '';
}
}
function initAll() {
document.querySelectorAll('.switch-checkbox').forEach(function (input) {
updateSwitch(input);
if (!input._switchBound) {
input._switchBound = true;
input.addEventListener('change', function () { updateSwitch(input); });
}
});
}
document.addEventListener('DOMContentLoaded', initAll);
document.addEventListener('htmx:afterSwap', initAll);
})();
// ── Tabs ──────────────────────────────────────────────────────────────────
(function () {
var ACTIVE = 'bg-background text-foreground shadow-sm';
var INACTIVE = 'text-muted-foreground';
function initTabs(root) {
if (root._tabsInitialised) return;
root._tabsInitialised = true;
var triggers = Array.from(root.querySelectorAll('.tabs-trigger'));
var panels = Array.from(root.querySelectorAll('.tabs-panel'));
function activate(idx) {
triggers.forEach(function (t, i) {
var active = i === idx;
t.setAttribute('aria-selected', String(active));
ACTIVE.split(' ').forEach(function (c) { t.classList.toggle(c, active); });
INACTIVE.split(' ').forEach(function (c) { t.classList.toggle(c, !active); });
});
panels.forEach(function (p, i) { p.hidden = i !== idx; });
}
triggers.forEach(function (trigger, idx) {
trigger.addEventListener('click', function () { activate(idx); });
});
activate(0);
}
function initAll() {
document.querySelectorAll('.tabs-root').forEach(initTabs);
}
document.addEventListener('DOMContentLoaded', initAll);
document.addEventListener('htmx:afterSwap', initAll);
})();
// ── Accordion ─────────────────────────────────────────────────────────────
(function () {
function initAccordion(root) {
if (root._accInitialised) return;
root._accInitialised = true;
root.querySelectorAll('.accordion-trigger').forEach(function (trigger) {
trigger.addEventListener('click', function () {
var expanded = trigger.getAttribute('aria-expanded') === 'true';
var panel = trigger.closest('.accordion-item').querySelector('.accordion-panel');
if (expanded) {
trigger.setAttribute('aria-expanded', 'false');
panel.style.height = '0';
panel.style.opacity = '0';
} else {
trigger.setAttribute('aria-expanded', 'true');
panel.style.height = panel.scrollHeight + 'px';
panel.style.opacity = '1';
}
var chevron = trigger.querySelector('.accordion-chevron');
if (chevron) chevron.style.transform = expanded ? '' : 'rotate(180deg)';
});
});
}
function initAll() {
document.querySelectorAll('.accordion-root').forEach(initAccordion);
}
document.addEventListener('DOMContentLoaded', initAll);
document.addEventListener('htmx:afterSwap', initAll);
})();
// ── Toast ─────────────────────────────────────────────────────────────────
(function () {
function dismissToast(toast) {
toast.style.opacity = '0';
toast.style.transform = 'translateY(0.5rem)';
setTimeout(function () { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
}
window.showToast = function (options) {
var viewport = document.querySelector('.toast-viewport');
if (!viewport) return;
var title = options.title || '';
var description = options.description || '';
var variant = options.variant || 'default';
var duration = typeof options.duration === 'number' ? options.duration : 5000;
var variantCls = variant === 'destructive' ? ' border-destructive text-destructive' : '';
var toast = document.createElement('div');
toast.setAttribute('role', 'alert');
toast.className = 'toast-item pointer-events-auto relative flex w-full items-center justify-between' +
' space-x-4 overflow-hidden rounded-md border border-border bg-background p-4' +
' shadow-lg transition-all duration-300 opacity-0 translate-y-2' + variantCls;
toast.innerHTML =
'<div class="grid gap-1">' +
(title ? '<div class="text-sm font-semibold">' + title + '</div>' : '') +
(description ? '<div class="text-sm opacity-90">' + description + '</div>' : '') +
'</div>' +
'<button type="button" aria-label="Dismiss"' +
' class="toast-close inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md' +
' text-muted-foreground hover:text-foreground focus:outline-none">' +
'<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"' +
' stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>' +
'</button>';
viewport.appendChild(toast);
requestAnimationFrame(function () {
toast.classList.remove('opacity-0', 'translate-y-2');
});
var timer = duration > 0 ? setTimeout(function () { dismissToast(toast); }, duration) : null;
toast.querySelector('.toast-close').addEventListener('click', function () {
if (timer) clearTimeout(timer);
dismissToast(toast);
});
};
// Delegate clicks on server-rendered toast close buttons
document.addEventListener('click', function (e) {
var btn = e.target.closest('.toast-close');
if (btn) {
var item = btn.closest('.toast-item');
if (item) dismissToast(item);
}
});
document.addEventListener('keydown', function (e) {
var trigger = e.target.closest('.dropdown-trigger');
if (!trigger) return;
if (e.key !== 'Enter' && e.key !== ' ') return;
e.preventDefault();
var root = trigger.closest('.dropdown-root');
var content = root && root.querySelector('.dropdown-content');
var isOpen = content && !content.classList.contains('hidden');
document.querySelectorAll('.dropdown-root').forEach(closeDropdown);
if (!isOpen && root) openDropdown(root);
});
})();
// ── Dialog ────────────────────────────────────────────────────────────────
(function () {
document.addEventListener('click', function (e) {
// Open
var openBtn = e.target.closest('[data-dialog-open]');
if (openBtn) {
var dlg = document.getElementById('dlg-' + openBtn.dataset.dialogOpen);
if (dlg && dlg.showModal) dlg.showModal();
}
// Close via button
var closeBtn = e.target.closest('[data-dialog-close], .dialog-close');
if (closeBtn) {
var dlg = closeBtn.closest('dialog');
if (dlg) dlg.close();
}
});
// Close on backdrop click
document.addEventListener('click', function (e) {
if (e.target && e.target.tagName === 'DIALOG') {
e.target.close();
}
});
})();
// ── DropdownMenu ──────────────────────────────────────────────────────────
(function () {
function closeDropdown(root) {
var trigger = root.querySelector('.dropdown-trigger');
var content = root.querySelector('.dropdown-content');
if (!trigger || !content) return;
content.classList.add('hidden');
trigger.setAttribute('aria-expanded', 'false');
}
function openDropdown(root) {
var trigger = root.querySelector('.dropdown-trigger');
var content = root.querySelector('.dropdown-content');
if (!trigger || !content) return;
content.classList.remove('hidden');
trigger.setAttribute('aria-expanded', 'true');
}
document.addEventListener('click', function (e) {
var trigger = e.target.closest('.dropdown-trigger');
if (trigger) {
var root = trigger.closest('.dropdown-root');
var content = root && root.querySelector('.dropdown-content');
var isOpen = content && !content.classList.contains('hidden');
document.querySelectorAll('.dropdown-root').forEach(closeDropdown);
if (!isOpen && root) openDropdown(root);
return;
}
var insideMenu = e.target.closest('.dropdown-content');
if (insideMenu) {
var rootInMenu = insideMenu.closest('.dropdown-root');
if (e.target.closest('a, button') && rootInMenu) {
closeDropdown(rootInMenu);
}
return;
}
document.querySelectorAll('.dropdown-root').forEach(function (root) {
if (!root.contains(e.target)) closeDropdown(root);
});
});
})();