From 99b2d1a176ee12aed9d80390f76f0a6490875094 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Wed, 20 Aug 2025 13:37:18 -0600 Subject: [PATCH] Broken event support. --- calendar.css | 129 +++++++++++++++++++++++++++++++++----------- calendar.js | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 31 deletions(-) diff --git a/calendar.css b/calendar.css index 1c42b59..604ddd1 100644 --- a/calendar.css +++ b/calendar.css @@ -73,10 +73,10 @@ /* Layout & typography */ :root { --row-h: 2.2em; - --label-w: 4em; - --cell-w: 6em; - --cell-h: 6em; - --overlay-w: 3rem; + --label-w: minmax(4em, 8%); + --cell-w: 1fr; + --cell-h: clamp(4em, 8vh, 8em); + --overlay-w: minmax(3rem, 5%); } * { box-sizing: border-box } @@ -89,13 +89,13 @@ body { } .wrap { - width: fit-content; - margin: 2rem auto; + width: 100%; + margin: 0; background: var(--panel); - height: calc(100vh - 4rem); + height: 100vh; display: flex; flex-direction: column; - min-width: calc(var(--label-w) + 7 * var(--cell-w) + 2.4rem); + padding: 1rem; white-space: pre-wrap; } @@ -127,6 +127,7 @@ header { border-bottom: .1em solid var(--muted); align-items: last baseline; flex-shrink: 0; + width: 100%; } .calendar-container { @@ -141,15 +142,17 @@ header { height: 100%; overflow-y: auto; overflow-x: hidden; - flex: 0 0 auto; - width: calc(var(--label-w) + 7 * var(--cell-w) + var(--overlay-w)); + flex: 1; + width: 100%; scrollbar-width: none; } .calendar-viewport::-webkit-scrollbar { display: none } .jogwheel-viewport { position: absolute; - inset: 0 0 0 auto; + top: 0; + right: 0; + bottom: 0; width: var(--overlay-w); overflow-y: auto; overflow-x: hidden; @@ -170,6 +173,7 @@ header { overflow: visible; height: var(--cell-h); scroll-snap-align: start; + width: 100%; } /* Fixed heights for cells and labels */ @@ -182,8 +186,8 @@ header h1 { margin: 0; font-size: 1rem } .week-label { display: grid; place-items: center; - width: var(--label-w); - height: var(--row-h); + width: 100%; + height: var(--cell-h); color: var(--muted); cursor: ns-resize; font-size: 1.2em; @@ -191,15 +195,85 @@ header h1 { margin: 0; font-size: 1rem } .dow { text-transform: uppercase; } .cell { - display: grid; - place-items: center; - width: var(--cell-w); - height: var(--row-h); + position: relative; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + padding: 0.25em; + overflow: hidden; + width: 100%; + height: var(--cell-h); font-weight: 700; cursor: pointer; transition: background-color .15s ease; } +.cell h1 { + position: absolute; + top: 0.25em; + right: 0.25em; + padding: 0; + margin: 0; + transition: background-color .15s ease; + font-size: 1em; + z-index: 10; +} +.cell:hover h1 { text-shadow: 0 0 .2em; } + +/* Event styles */ +.event { + font-size: 0.75em; + padding: 0.1em 0.3em; + margin: 0.1em 0; + border-radius: 0.2em; + color: white; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + max-width: calc(100% - 0.5em); + line-height: 1.2; + cursor: pointer; + z-index: 5; +} + +.event:hover { + opacity: 0.8; +} + +/* Spanning event styles */ +.event-span { + font-size: 0.75em; + padding: 0.1em 0.3em; + border-radius: 0.2em; + color: white; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.2; + cursor: pointer; + transition: opacity 0.15s ease; + + /* Calculate position and width using CSS variables and custom properties */ + left: calc(var(--label-w) + var(--start-day) * (100% - var(--label-w) - var(--overlay-w)) / 7); + width: calc(var(--span-days) * (100% - var(--label-w) - var(--overlay-w)) / 7); +} + +.event-span:hover { + opacity: 0.8; +} + +.event-more { + font-size: 0.7em; + color: var(--muted); + font-style: italic; + margin-top: 0.1em; +} + +/* Selection styles */ .weekend { color: var(--weekend) } .firstday { color: var(--firstday); text-shadow: 0 0 .1em rgba(var(--ink-rgb), .5) } @@ -221,29 +295,22 @@ label:has(input[value]) { } .selected { background: var(--select) !important; + border: 2px solid rgba(var(--ink-rgb), 0.3); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3); } .selected h1, :is(.range-start,.range-end,.range-middle,.range-single) h1 { background: transparent !important; color: var(--panel) !important; font-weight: 700; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } -.cell { - position: relative; +/* Ensure events don't interfere with selection visibility */ +.selected .event { + opacity: 0.7; } -.cell h1 { - position: absolute; - top: 0; - left: 0; - padding: .25em; - margin: 0; - transition: background-color .15s ease; - font-size: 1em; -} -.cell:hover h1 { text-shadow: 0 0 .2em; } - /* Month labels in jogwheel column */ .month-name-label { grid-column: -2 / -1; @@ -259,7 +326,7 @@ label:has(input[value]) { position: absolute; top: 0; right: 0; - width: var(--overlay-w); + width: 100%; } .month-name-label > span { diff --git a/calendar.js b/calendar.js index efd05c1..93d2e09 100644 --- a/calendar.js +++ b/calendar.js @@ -37,6 +37,10 @@ class InfiniteCalendar { this.weekend = [true, false, false, false, false, false, true] + // Event storage + this.events = new Map() // Map of date strings to arrays of events + this.eventIdCounter = 1 + this.viewport = document.getElementById('calendar-viewport') this.content = document.getElementById('calendar-content') this.header = document.getElementById('calendar-header') @@ -279,6 +283,9 @@ class InfiniteCalendar { weekEl.style.height = `${this.rowHeight}px` this.content.appendChild(weekEl) this.visibleWeeks.set(vw, weekEl) + + // Add events to the newly created week + this.addEventsToWeek(weekEl, vw) } this.applySelectionToVisible() @@ -588,6 +595,148 @@ class InfiniteCalendar { this.isDragging = false this.setSelection(this.dragAnchor, dateStr) document.body.style.cursor = 'default' + + // Trigger event creation after selection with a small delay + if (this.selStart && this.selEnd) { + setTimeout(() => this.promptForEvent(), 100) + } + } + + // -------- Event Management -------- + + promptForEvent() { + const title = prompt('Enter event title:') + if (!title || title.trim() === '') { + this.clearSelection() + return + } + + this.createEvent({ + title: title.trim(), + startDate: this.selStart, + endDate: this.selEnd + }) + + this.clearSelection() + } + + createEvent(eventData) { + const event = { + id: this.eventIdCounter++, + title: eventData.title, + startDate: eventData.startDate, + endDate: eventData.endDate, + color: this.generateEventColor() + } + + // Add event to all dates in the range + const startDate = new Date(fromLocalString(event.startDate)) + const endDate = new Date(fromLocalString(event.endDate)) + + for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { + const dateStr = toLocalString(d) + if (!this.events.has(dateStr)) { + this.events.set(dateStr, []) + } + this.events.get(dateStr).push({...event, isSpanning: startDate < endDate}) + } + + // Re-render visible weeks to show the new event + this.refreshEvents() + } + + generateEventColor() { + const colors = [ + '#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', + '#ff9ff3', '#54a0ff', '#5f27cd', '#00d2d3', '#ff9f43' + ] + return colors[Math.floor(Math.random() * colors.length)] + } + + refreshEvents() { + // Re-render events for all visible weeks + for (const [weekDateStr, weekEl] of this.visibleWeeks) { + this.addEventsToWeek(weekEl, weekDateStr) + } + } + + addEventsToWeek(weekEl, weekDateStr) { + const cells = weekEl.querySelectorAll('.cell[data-date]') + + // Remove existing event elements from both week and individual cells + weekEl.querySelectorAll('.event-span').forEach(el => el.remove()) + cells.forEach(cell => { + cell.querySelectorAll('.event-span').forEach(el => el.remove()) + }) + + // Group events by their date ranges within this week + const weekEvents = new Map() // Map of event ID to event info + + for (const cell of cells) { + const dateStr = cell.dataset.date + const events = this.events.get(dateStr) || [] + + events.forEach(event => { + if (!weekEvents.has(event.id)) { + weekEvents.set(event.id, { + ...event, + startDateInWeek: dateStr, + endDateInWeek: dateStr, + startCell: cell, + endCell: cell, + daysInWeek: 1 + }) + } else { + const weekEvent = weekEvents.get(event.id) + weekEvent.endDateInWeek = dateStr + weekEvent.endCell = cell + weekEvent.daysInWeek++ + } + }) + } + + // Create spanning elements for each event + weekEvents.forEach((weekEvent, eventId) => { + this.createSpanningEvent(weekEl, weekEvent) + }) + } + + createSpanningEvent(weekEl, weekEvent) { + const spanEl = document.createElement('div') + spanEl.className = 'event-span' + spanEl.style.backgroundColor = weekEvent.color + spanEl.textContent = weekEvent.title + spanEl.title = `${weekEvent.title} (${weekEvent.startDate === weekEvent.endDate ? weekEvent.startDate : weekEvent.startDate + ' - ' + weekEvent.endDate})` + + // Get all cells in the week (excluding week label and overlay) + const cells = Array.from(weekEl.querySelectorAll('.cell[data-date]')) + const startCellIndex = cells.indexOf(weekEvent.startCell) + const endCellIndex = cells.indexOf(weekEvent.endCell) + const spanDays = endCellIndex - startCellIndex + 1 + + // Use CSS custom properties for positioning + spanEl.style.setProperty('--start-day', startCellIndex) + spanEl.style.setProperty('--span-days', spanDays) + + // Style the spanning event + spanEl.style.position = 'absolute' + spanEl.style.top = '0.25em' + spanEl.style.height = '1.2em' + spanEl.style.zIndex = '15' + spanEl.style.fontSize = '0.75em' + spanEl.style.padding = '0.1em 0.3em' + spanEl.style.borderRadius = '0.2em' + spanEl.style.color = 'white' + spanEl.style.fontWeight = '500' + spanEl.style.whiteSpace = 'nowrap' + spanEl.style.overflow = 'hidden' + spanEl.style.textOverflow = 'ellipsis' + spanEl.style.cursor = 'pointer' + spanEl.style.lineHeight = '1.2' + spanEl.style.pointerEvents = 'auto' + + // Append to the week element (not individual cells) + weekEl.appendChild(spanEl) } }