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:
@@ -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"] {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user