Web: custom calendar dropdown in the event editor (shows group icon)

A native <select> can't render the group icon SVG, so the calendar picker
is replaced with a styled dropdown: colour dot + group icon + name, grouped
by account/source with a "Groups" section. The native <select> is kept
hidden as the value holder + change-event source, so all existing save and
private/reminder-row logic is unchanged. Click-outside / Escape close it.

version v46 -> v47.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-06-09 20:38:47 +02:00
parent c684a424eb
commit 367087450e
4 changed files with 178 additions and 2 deletions

View File

@@ -246,6 +246,51 @@ a { color: var(--primary); text-decoration: none; }
} }
.form-group textarea { resize: vertical; } .form-group textarea { resize: vertical; }
/* ── Custom calendar dropdown (shows colour dot + group icon) ── */
.cal-native-hidden { display: none !important; }
.cal-select { position: relative; }
.cal-select-trigger {
display: flex; align-items: center; gap: 8px;
width: 100%;
background: var(--bg-app);
border: 1px solid var(--border);
border-radius: 8px;
padding: 11px 14px;
color: var(--text-1);
cursor: pointer;
transition: border-color var(--transition), box-shadow var(--transition);
}
.cal-select-trigger:hover { border-color: var(--text-3); }
.cal-select[data-open="1"] .cal-select-trigger {
border-color: var(--primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
}
.cal-select-current { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
.cal-select-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal-select-chevron { flex-shrink: 0; color: var(--text-3); }
.cal-select-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.cal-select-gicon { display: inline-flex; color: var(--text-2); flex-shrink: 0; }
.cal-select-gicon svg { display: block; }
.cal-select-menu {
position: absolute; left: 0; right: 0; top: calc(100% + 4px);
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: var(--shadow-lg);
max-height: 280px; overflow-y: auto;
z-index: 50; padding: 4px;
}
.cal-select-section {
font-size: 11px; color: var(--text-3); font-weight: 600;
padding: 8px 10px 4px; text-transform: uppercase; letter-spacing: .4px;
}
.cal-select-option {
display: flex; align-items: center; gap: 8px;
padding: 9px 10px; border-radius: 8px; cursor: pointer;
}
.cal-select-option:hover { background: var(--bg-hover); }
.cal-select-option.selected { background: var(--primary-dim); }
/* ── Date/time input dark mode ──────────────────────────── */ /* ── Date/time input dark mode ──────────────────────────── */
.form-group input[type="datetime-local"], .form-group input[type="datetime-local"],
.form-group input[type="date"] { .form-group input[type="date"] {

View File

@@ -342,7 +342,16 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Kalender</label> <label>Kalender</label>
<select id="ev-calendar"></select> <!-- Hidden native select stays the value holder + change-event source;
the custom dropdown below renders colour dots and group icons. -->
<select id="ev-calendar" class="cal-native-hidden"></select>
<div class="cal-select" id="ev-cal-select">
<button type="button" class="cal-select-trigger" id="ev-cal-trigger" aria-haspopup="listbox" aria-expanded="false">
<span class="cal-select-current" id="ev-cal-current"></span>
<svg class="cal-select-chevron" viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M7 10l5 5 5-5z"/></svg>
</button>
<div class="cal-select-menu hidden" id="ev-cal-menu" role="listbox"></div>
</div>
</div> </div>
<div class="form-row" id="ev-private-row" style="display:none"> <div class="form-row" id="ev-private-row" style="display:none">
<label class="toggle-label"> <label class="toggle-label">

View File

@@ -1495,6 +1495,7 @@ document.addEventListener('click', e => {
// ── Event Modal ─────────────────────────────────────────── // ── Event Modal ───────────────────────────────────────────
function populateCalendarSelect(selectedId) { function populateCalendarSelect(selectedId) {
closeCalSelectMenu();
const sel = document.getElementById('ev-calendar'); const sel = document.getElementById('ev-calendar');
sel.innerHTML = ''; sel.innerHTML = '';
// CalDAV calendars (show all that aren't removed from sidebar, even if unchecked) // CalDAV calendars (show all that aren't removed from sidebar, even if unchecked)
@@ -1546,6 +1547,117 @@ function populateCalendarSelect(selectedId) {
sel.appendChild(opt); sel.appendChild(opt);
}); });
}); });
renderCalSelectTrigger();
}
// ── Custom calendar dropdown (colour dot + group icon) ────
// The hidden <select id="ev-calendar"> above stays the value holder; this
// renders a styled list from the same state so group icons can show.
function groupIconForLocalCal(calId) {
const g = (state.groups || []).find(x => x.group_calendar_id === calId);
return (g && g.icon) ? g.icon : 'people';
}
function calSelectData() {
const sections = [];
const byValue = {};
const push = (sec, value, name, color, isGroup, iconKey) => {
sec.items.push({ value, name, color, isGroup, iconKey });
byValue[value] = { name, color, isGroup, iconKey };
};
state.accounts.forEach(acc => {
const cals = (acc.calendars || []).filter(c => !c.sidebar_hidden);
if (!cals.length) return;
const sec = { label: acc.name, items: [] };
cals.forEach(cal => push(sec, String(cal.id), cal.name, cal.color || acc.color || '#4285f4', false, null));
sections.push(sec);
});
const locals = state.localCalendars.filter(c => !c.sidebar_hidden && !c.group);
if (locals.length) {
const sec = { label: t('cal_local'), items: [] };
locals.forEach(cal => push(sec, `local-${cal.id}`, cal.name, cal.color, false, null));
sections.push(sec);
}
const groups = state.localCalendars.filter(c => !c.sidebar_hidden && c.group);
if (groups.length) {
const sec = { label: t('groups_title'), items: [] };
groups.forEach(cal => push(sec, `local-${cal.id}`, cal.name, cal.color, true, groupIconForLocalCal(cal.id)));
sections.push(sec);
}
state.googleAccounts.forEach(acc => {
const cals = (acc.calendars || []).filter(c => !c.sidebar_hidden);
if (!cals.length) return;
const sec = { label: acc.email, items: [] };
cals.forEach(cal => push(sec, `google-${cal.id}`, cal.name, cal.color || '#4285f4', false, null));
sections.push(sec);
});
state.haAccounts.forEach(acc => {
const cals = (acc.calendars || []).filter(c => !c.sidebar_hidden);
if (!cals.length) return;
const sec = { label: acc.name, items: [] };
cals.forEach(cal => push(sec, `homeassistant-${cal.id}`, cal.name, cal.color || '#03a9f4', false, null));
sections.push(sec);
});
return { sections, byValue };
}
function calOptionInner(info) {
const dot = `<span class="cal-select-dot" style="background:${info.color || '#4285f4'}"></span>`;
const icon = info.isGroup ? `<span class="cal-select-gicon">${groupIconSvg(info.iconKey || 'people', 16)}</span>` : '';
return `${dot}${icon}<span class="cal-select-name">${escHtml(info.name)}</span>`;
}
function renderCalSelectTrigger() {
const cur = document.getElementById('ev-cal-current');
if (!cur) return;
const val = document.getElementById('ev-calendar').value;
const { byValue } = calSelectData();
const info = byValue[val];
cur.innerHTML = info ? calOptionInner(info) : `<span class="cal-select-name">—</span>`;
}
function renderCalSelectMenu() {
const menu = document.getElementById('ev-cal-menu');
const val = document.getElementById('ev-calendar').value;
const { sections } = calSelectData();
menu.innerHTML = sections.map(sec =>
`<div class="cal-select-section">${escHtml(sec.label)}</div>` +
sec.items.map(it =>
`<div class="cal-select-option ${it.value === val ? 'selected' : ''}" role="option" data-value="${it.value}">${calOptionInner(it)}</div>`
).join('')
).join('');
menu.querySelectorAll('.cal-select-option').forEach(el => {
el.addEventListener('click', () => {
const sel = document.getElementById('ev-calendar');
sel.value = el.dataset.value;
sel.dispatchEvent(new Event('change'));
renderCalSelectTrigger();
closeCalSelectMenu();
});
});
}
function openCalSelectMenu() {
renderCalSelectMenu();
document.getElementById('ev-cal-menu').classList.remove('hidden');
document.getElementById('ev-cal-select').dataset.open = '1';
document.getElementById('ev-cal-trigger').setAttribute('aria-expanded', 'true');
setTimeout(() => document.addEventListener('click', calSelectOutside), 0);
}
function closeCalSelectMenu() {
const menu = document.getElementById('ev-cal-menu');
if (menu) menu.classList.add('hidden');
const wrap = document.getElementById('ev-cal-select');
if (wrap) delete wrap.dataset.open;
const trig = document.getElementById('ev-cal-trigger');
if (trig) trig.setAttribute('aria-expanded', 'false');
document.removeEventListener('click', calSelectOutside);
}
function calSelectOutside(e) {
if (!e.target.closest('#ev-cal-select')) closeCalSelectMenu();
} }
// ── Date field helpers ──────────────────────────────────── // ── Date field helpers ────────────────────────────────────
@@ -1874,6 +1986,16 @@ function bindEventModal() {
document.getElementById('ev-reminder-add').addEventListener('click', addReminderRow); document.getElementById('ev-reminder-add').addEventListener('click', addReminderRow);
// Custom calendar dropdown toggle
document.getElementById('ev-cal-trigger').addEventListener('click', e => {
e.stopPropagation();
const menu = document.getElementById('ev-cal-menu');
if (menu.classList.contains('hidden')) openCalSelectMenu(); else closeCalSelectMenu();
});
document.getElementById('ev-cal-select').addEventListener('keydown', e => {
if (e.key === 'Escape') closeCalSelectMenu();
});
// Date/time pickers with auto-adjustment logic // Date/time pickers with auto-adjustment logic
[ [
{ displayId: 'ev-start-display', inputId: 'ev-start', mode: 'datetime', role: 'start' }, { displayId: 'ev-start-display', inputId: 'ev-start', mode: 'datetime', role: 'start' },

View File

@@ -1,2 +1,2 @@
// Increment APP_VERSION with every code change // Increment APP_VERSION with every code change
export const APP_VERSION = 'v46'; export const APP_VERSION = 'v47';