Broken event support.
This commit is contained in:
parent
9bb3740b61
commit
99b2d1a176
129
calendar.css
129
calendar.css
@ -73,10 +73,10 @@
|
|||||||
/* Layout & typography */
|
/* Layout & typography */
|
||||||
:root {
|
:root {
|
||||||
--row-h: 2.2em;
|
--row-h: 2.2em;
|
||||||
--label-w: 4em;
|
--label-w: minmax(4em, 8%);
|
||||||
--cell-w: 6em;
|
--cell-w: 1fr;
|
||||||
--cell-h: 6em;
|
--cell-h: clamp(4em, 8vh, 8em);
|
||||||
--overlay-w: 3rem;
|
--overlay-w: minmax(3rem, 5%);
|
||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box }
|
* { box-sizing: border-box }
|
||||||
@ -89,13 +89,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
width: fit-content;
|
width: 100%;
|
||||||
margin: 2rem auto;
|
margin: 0;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
height: calc(100vh - 4rem);
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: calc(var(--label-w) + 7 * var(--cell-w) + 2.4rem);
|
padding: 1rem;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,6 +127,7 @@ header {
|
|||||||
border-bottom: .1em solid var(--muted);
|
border-bottom: .1em solid var(--muted);
|
||||||
align-items: last baseline;
|
align-items: last baseline;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-container {
|
.calendar-container {
|
||||||
@ -141,15 +142,17 @@ header {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
flex: 0 0 auto;
|
flex: 1;
|
||||||
width: calc(var(--label-w) + 7 * var(--cell-w) + var(--overlay-w));
|
width: 100%;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
.calendar-viewport::-webkit-scrollbar { display: none }
|
.calendar-viewport::-webkit-scrollbar { display: none }
|
||||||
|
|
||||||
.jogwheel-viewport {
|
.jogwheel-viewport {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0 0 0 auto;
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
width: var(--overlay-w);
|
width: var(--overlay-w);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
@ -170,6 +173,7 @@ header {
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
height: var(--cell-h);
|
height: var(--cell-h);
|
||||||
scroll-snap-align: start;
|
scroll-snap-align: start;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fixed heights for cells and labels */
|
/* Fixed heights for cells and labels */
|
||||||
@ -182,8 +186,8 @@ header h1 { margin: 0; font-size: 1rem }
|
|||||||
.week-label {
|
.week-label {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
width: var(--label-w);
|
width: 100%;
|
||||||
height: var(--row-h);
|
height: var(--cell-h);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
cursor: ns-resize;
|
cursor: ns-resize;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
@ -191,15 +195,85 @@ header h1 { margin: 0; font-size: 1rem }
|
|||||||
|
|
||||||
.dow { text-transform: uppercase; }
|
.dow { text-transform: uppercase; }
|
||||||
.cell {
|
.cell {
|
||||||
display: grid;
|
position: relative;
|
||||||
place-items: center;
|
display: flex;
|
||||||
width: var(--cell-w);
|
flex-direction: column;
|
||||||
height: var(--row-h);
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0.25em;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--cell-h);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color .15s ease;
|
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) }
|
.weekend { color: var(--weekend) }
|
||||||
.firstday { color: var(--firstday); text-shadow: 0 0 .1em rgba(var(--ink-rgb), .5) }
|
.firstday { color: var(--firstday); text-shadow: 0 0 .1em rgba(var(--ink-rgb), .5) }
|
||||||
|
|
||||||
@ -221,29 +295,22 @@ label:has(input[value]) {
|
|||||||
}
|
}
|
||||||
.selected {
|
.selected {
|
||||||
background: var(--select) !important;
|
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,
|
.selected h1,
|
||||||
:is(.range-start,.range-end,.range-middle,.range-single) h1 {
|
:is(.range-start,.range-end,.range-middle,.range-single) h1 {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
color: var(--panel) !important;
|
color: var(--panel) !important;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell {
|
/* Ensure events don't interfere with selection visibility */
|
||||||
position: relative;
|
.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 labels in jogwheel column */
|
||||||
.month-name-label {
|
.month-name-label {
|
||||||
grid-column: -2 / -1;
|
grid-column: -2 / -1;
|
||||||
@ -259,7 +326,7 @@ label:has(input[value]) {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: var(--overlay-w);
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-name-label > span {
|
.month-name-label > span {
|
||||||
|
149
calendar.js
149
calendar.js
@ -37,6 +37,10 @@ class InfiniteCalendar {
|
|||||||
|
|
||||||
this.weekend = [true, false, false, false, false, false, true]
|
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.viewport = document.getElementById('calendar-viewport')
|
||||||
this.content = document.getElementById('calendar-content')
|
this.content = document.getElementById('calendar-content')
|
||||||
this.header = document.getElementById('calendar-header')
|
this.header = document.getElementById('calendar-header')
|
||||||
@ -279,6 +283,9 @@ class InfiniteCalendar {
|
|||||||
weekEl.style.height = `${this.rowHeight}px`
|
weekEl.style.height = `${this.rowHeight}px`
|
||||||
this.content.appendChild(weekEl)
|
this.content.appendChild(weekEl)
|
||||||
this.visibleWeeks.set(vw, weekEl)
|
this.visibleWeeks.set(vw, weekEl)
|
||||||
|
|
||||||
|
// Add events to the newly created week
|
||||||
|
this.addEventsToWeek(weekEl, vw)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.applySelectionToVisible()
|
this.applySelectionToVisible()
|
||||||
@ -588,6 +595,148 @@ class InfiniteCalendar {
|
|||||||
this.isDragging = false
|
this.isDragging = false
|
||||||
this.setSelection(this.dragAnchor, dateStr)
|
this.setSelection(this.dragAnchor, dateStr)
|
||||||
document.body.style.cursor = 'default'
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user