Make '+N more' actually show the events behind it
The link had a custom hover tooltip that took the hidden events with dayEvents.slice(3) - correct while dayMaxEvents was a fixed 3, wrong the moment that cap started varying with the row height. On a short row showing one chip, '+4 more' sliced from index 3 and listed the wrong events; on a day with three events it sliced to nothing, hit the empty-list guard and rendered no tooltip at all. A link with nothing behind it. FullCalendar's own popover replaces it rather than the arithmetic being fixed: no index to drift out of step with the cap, a header and a close button, every event for that day listed, and it works on a touch screen - which a hover tooltip never did on a kiosk. Clicking an event in the popover still opens the detail modal. That takes the hover handlers, the container mouseenter/mouseleave delegation, the tooltip markup and styles, and the by-date index that existed only to feed them: 108 lines out, 25 in. The popover is themed through the CSS variables, since FullCalendar ships it light.
This commit is contained in:
@@ -7,25 +7,6 @@
|
|||||||
<FullCalendar :options="calendarOptions" />
|
<FullCalendar :options="calendarOptions" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- More events tooltip -->
|
|
||||||
<div
|
|
||||||
v-if="moreTooltipData.length"
|
|
||||||
ref="moreTooltip"
|
|
||||||
class="fc-more-tooltip"
|
|
||||||
:style="{ left: tooltipPosition.left + 'px', top: tooltipPosition.top + 'px' }"
|
|
||||||
@mouseleave="hideMoreTooltip"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(evt, idx) in moreTooltipData"
|
|
||||||
:key="idx"
|
|
||||||
class="fc-more-tooltip-event"
|
|
||||||
:style="{ borderLeftColor: evt.color }"
|
|
||||||
@click="openEventFromTooltip(evt)"
|
|
||||||
>
|
|
||||||
{{ evt.title }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Event details modal -->
|
<!-- Event details modal -->
|
||||||
<div v-if="selectedEvent" class="modal-overlay" @click.self="closeEventModal">
|
<div v-if="selectedEvent" class="modal-overlay" @click.self="closeEventModal">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
@@ -95,12 +76,8 @@ const events = ref([])
|
|||||||
const selectedEvent = ref(null)
|
const selectedEvent = ref(null)
|
||||||
const calendarRef = ref(null)
|
const calendarRef = ref(null)
|
||||||
const calendarContainer = ref(null)
|
const calendarContainer = ref(null)
|
||||||
const moreTooltip = ref(null)
|
|
||||||
const moreTooltipData = ref([])
|
|
||||||
const tooltipPosition = ref({ left: 0, top: 0 })
|
|
||||||
|
|
||||||
// Store events by date for hover lookup
|
// Store events by date for hover lookup
|
||||||
const eventsByDate = ref({})
|
|
||||||
|
|
||||||
const calendarOptions = ref({
|
const calendarOptions = ref({
|
||||||
plugins: [dayGridPlugin],
|
plugins: [dayGridPlugin],
|
||||||
@@ -127,70 +104,16 @@ const calendarOptions = ref({
|
|||||||
// weeks behind the calendar's own scroller. Compact chip styling below buys
|
// weeks behind the calendar's own scroller. Compact chip styling below buys
|
||||||
// back a chip or two per day.
|
// back a chip or two per day.
|
||||||
dayMaxEvents: true,
|
dayMaxEvents: true,
|
||||||
moreLinkClick: () => 'none' // Disable click, we use hover
|
// FullCalendar's own popover: click '+N more' and it lists that day's events,
|
||||||
|
// each still opening the detail modal through eventClick. The custom hover
|
||||||
|
// tooltip this replaces sliced the hidden events from a hardcoded index of 3,
|
||||||
|
// which was the fixed dayMaxEvents at the time. Now the cap varies with the
|
||||||
|
// row height, so that slice pointed at the wrong events - and when a short
|
||||||
|
// row showed one chip, at an empty slice, leaving '+2 more' with nothing
|
||||||
|
// behind it. A popover has no such bookkeeping, and works on a touch screen.
|
||||||
|
moreLinkClick: 'popover'
|
||||||
})
|
})
|
||||||
|
|
||||||
function hideMoreTooltip() {
|
|
||||||
moreTooltipData.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMoreLinkHover(e) {
|
|
||||||
const moreLink = e.target.closest('.fc-daygrid-more-link')
|
|
||||||
if (!moreLink) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the day cell and get its date
|
|
||||||
const dayCell = moreLink.closest('.fc-daygrid-day')
|
|
||||||
if (!dayCell) return
|
|
||||||
|
|
||||||
const dateStr = dayCell.getAttribute('data-date')
|
|
||||||
if (!dateStr || !eventsByDate.value[dateStr]) return
|
|
||||||
|
|
||||||
// Get events for this date that would be hidden (after first 3)
|
|
||||||
const dayEvents = eventsByDate.value[dateStr]
|
|
||||||
const hiddenEvents = dayEvents.slice(3).map(evt => ({
|
|
||||||
title: evt.title,
|
|
||||||
color: evt.backgroundColor || '#14abef',
|
|
||||||
// Include full event data for modal
|
|
||||||
start: evt.start,
|
|
||||||
end: evt.end,
|
|
||||||
extendedProps: evt.extendedProps || {}
|
|
||||||
}))
|
|
||||||
|
|
||||||
if (hiddenEvents.length === 0) return
|
|
||||||
|
|
||||||
moreTooltipData.value = hiddenEvents
|
|
||||||
|
|
||||||
// Position tooltip
|
|
||||||
const rect = moreLink.getBoundingClientRect()
|
|
||||||
tooltipPosition.value = {
|
|
||||||
left: rect.left,
|
|
||||||
top: rect.bottom + 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMoreLinkLeave(e) {
|
|
||||||
const related = e.relatedTarget
|
|
||||||
// Don't hide if moving to the tooltip itself
|
|
||||||
if (related && (related.closest('.fc-more-tooltip') || related.closest('.fc-daygrid-more-link'))) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hideMoreTooltip()
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildEventsByDate() {
|
|
||||||
const byDate = {}
|
|
||||||
for (const evt of events.value) {
|
|
||||||
const dateStr = evt.start ? evt.start.split('T')[0] : null
|
|
||||||
if (dateStr) {
|
|
||||||
if (!byDate[dateStr]) byDate[dateStr] = []
|
|
||||||
byDate[dateStr].push(evt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
eventsByDate.value = byDate
|
|
||||||
}
|
|
||||||
|
|
||||||
// Height the calendar can have without pushing the page into a scrollbar.
|
// Height the calendar can have without pushing the page into a scrollbar.
|
||||||
// A first guess from the container's top, then corrected against whatever the
|
// A first guess from the container's top, then corrected against whatever the
|
||||||
// page actually overflows by - the card's padding, page margins and anything
|
// page actually overflows by - the card's padding, page margins and anything
|
||||||
@@ -220,24 +143,13 @@ async function fitHeight() {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadEvents()
|
await loadEvents()
|
||||||
|
|
||||||
// Add event delegation for more links
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
fitHeight()
|
fitHeight()
|
||||||
window.addEventListener('resize', fitHeight)
|
window.addEventListener('resize', fitHeight)
|
||||||
const container = document.querySelector('.calendar-container')
|
|
||||||
if (container) {
|
|
||||||
container.addEventListener('mouseenter', handleMoreLinkHover, true)
|
|
||||||
container.addEventListener('mouseleave', handleMoreLinkLeave, true)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', fitHeight)
|
window.removeEventListener('resize', fitHeight)
|
||||||
const container = document.querySelector('.calendar-container')
|
|
||||||
if (container) {
|
|
||||||
container.removeEventListener('mouseenter', handleMoreLinkHover, true)
|
|
||||||
container.removeEventListener('mouseleave', handleMoreLinkLeave, true)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -249,7 +161,6 @@ async function loadEvents() {
|
|||||||
const response = await notificationsApi.getCalendar()
|
const response = await notificationsApi.getCalendar()
|
||||||
events.value = response.data.data
|
events.value = response.data.data
|
||||||
calendarOptions.value.events = events.value
|
calendarOptions.value.events = events.value
|
||||||
buildEventsByDate()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading calendar events:', error)
|
console.error('Error loading calendar events:', error)
|
||||||
}
|
}
|
||||||
@@ -259,17 +170,6 @@ function closeEventModal() {
|
|||||||
selectedEvent.value = null
|
selectedEvent.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEventFromTooltip(evt) {
|
|
||||||
// Create an event-like object that matches FullCalendar's event structure
|
|
||||||
selectedEvent.value = {
|
|
||||||
title: evt.title,
|
|
||||||
start: evt.start,
|
|
||||||
end: evt.end,
|
|
||||||
extendedProps: evt.extendedProps
|
|
||||||
}
|
|
||||||
hideMoreTooltip()
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(dateStr) {
|
function formatDate(dateStr) {
|
||||||
if (!dateStr) return ''
|
if (!dateStr) return ''
|
||||||
// allDay events carry a site-local date-only value (YYYY-MM-DD). Build the
|
// allDay events carry a site-local date-only value (YYYY-MM-DD). Build the
|
||||||
@@ -304,6 +204,23 @@ function formatDate(dateStr) {
|
|||||||
overflow-y: hidden !important;
|
overflow-y: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The popover is FullCalendar's own element, so it ships light-themed. */
|
||||||
|
:deep(.fc-popover) {
|
||||||
|
background: var(--bg-card-solid);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
:deep(.fc-popover-header) {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
:deep(.fc-popover-body) {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
/* Compact chips: the row height decides how many events a day can show before
|
/* Compact chips: the row height decides how many events a day can show before
|
||||||
they roll into a '+N more', so every pixel saved here is one more visible. */
|
they roll into a '+N more', so every pixel saved here is one more visible. */
|
||||||
:deep(.fc-daygrid-event) {
|
:deep(.fc-daygrid-event) {
|
||||||
|
|||||||
Reference in New Issue
Block a user