Skip to content

Commit a7d6c27

Browse files
GVodyanovbackportbot[bot]
authored andcommitted
feat(FullCalendar): add conditional styling for participation status in grid
Signed-off-by: Grigory Vodyanov <scratchx@gmx.com> Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
1 parent a509476 commit a7d6c27

File tree

4 files changed

+163
-4
lines changed

4 files changed

+163
-4
lines changed

css/fullcalendar.scss

+7
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575

7676
.fc-event {
7777
box-shadow: 0px 0px 0px 1px var(--color-primary-element-light) !important;
78+
margin-top: 0px;
7879
}
7980

8081
.fc-daygrid-day-top {
@@ -131,6 +132,7 @@
131132
// ### FullCalendar Event adjustments
132133
.fc-event {
133134
padding-left: 3px;
135+
border-width: 2px;
134136

135137
&.fc-event-nc-task-completed,
136138
&.fc-event-nc-tentative,
@@ -149,6 +151,7 @@
149151

150152
.fc-event-title {
151153
text-overflow: ellipsis;
154+
font-weight: 700;
152155
}
153156

154157
// Reminder icon on events with alarms set
@@ -200,6 +203,10 @@
200203
max-width: 25vw;
201204
}
202205

206+
svg {
207+
margin-right: 2px;
208+
}
209+
203210
@media only screen and (max-width: 767px) {
204211
.fc-list-event-location,
205212
.fc-list-event-description {

src/fullcalendar/eventSources/eventSourceFunction.js

+34-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '../../utils/color.js'
1212
import logger from '../../utils/logger.js'
1313
import { getAllObjectsInTimeRange } from '../../utils/calendarObject.js'
14-
14+
import usePrincipalsStore from '../../store/principals.js'
1515
/**
1616
* convert an array of calendar-objects to events
1717
*
@@ -23,6 +23,8 @@ import { getAllObjectsInTimeRange } from '../../utils/calendarObject.js'
2323
* @return {object}[]
2424
*/
2525
export function eventSourceFunction(calendarObjects, calendar, start, end, timezone) {
26+
const principalsStore = usePrincipalsStore()
27+
2628
const fcEvents = []
2729
for (const calendarObject of calendarObjects) {
2830
let allObjectsInTimeRange
@@ -32,16 +34,46 @@ export function eventSourceFunction(calendarObjects, calendar, start, end, timez
3234
logger.error(error.message)
3335
continue
3436
}
35-
3637
for (const object of allObjectsInTimeRange) {
3738
const classNames = []
39+
let didEveryoneDecline = false
40+
41+
// You are an organizer
42+
if (object.getFirstPropertyFirstValue('ORGANIZER') === `mailto:${principalsStore.getCurrentUserPrincipalEmail}`) {
43+
// Check if all the attendees have declined the event
44+
if (object.hasProperty('ATTENDEE')) {
45+
didEveryoneDecline = true
46+
for (const attendeeProperty of object.getPropertyIterator('ATTENDEE')) {
47+
const hasDeclined = attendeeProperty.participationStatus === 'DECLINED'
48+
if (!hasDeclined) {
49+
didEveryoneDecline = false
50+
}
51+
}
52+
if (didEveryoneDecline) {
53+
classNames.push('fc-event-nc-all-declined')
54+
}
55+
}
56+
}
3857

3958
if (object.status === 'CANCELLED') {
4059
classNames.push('fc-event-nc-cancelled')
4160
} else if (object.status === 'TENTATIVE') {
4261
classNames.push('fc-event-nc-tentative')
4362
}
4463

64+
// You are invited
65+
for (const attendeeProperty of object.getPropertyIterator('ATTENDEE')) {
66+
if (attendeeProperty.email === `mailto:${principalsStore.getCurrentUserPrincipalEmail}`) {
67+
if (attendeeProperty.participationStatus === 'DECLINED') {
68+
classNames.push('fc-event-nc-declined')
69+
} else if (attendeeProperty.participationStatus === 'TENTATIVE') {
70+
classNames.push('fc-event-nc-tentative')
71+
} else if (attendeeProperty.participationStatus === 'NEEDS-ACTION') {
72+
classNames.push('fc-event-nc-needs-action')
73+
}
74+
}
75+
}
76+
4577
if (object.hasComponent('VALARM')) {
4678
classNames.push('fc-event-nc-alarms')
4779
}

src/fullcalendar/rendering/eventDidMount.js

+78-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default function({ event, el }) {
2929
const dotElement = el.querySelector('.fc-list-event-dot')
3030
dotElement.classList.remove('fc-list-event-dot')
3131
dotElement.classList.add('fc-list-event-checkbox')
32-
dotElement.style.color = dotElement.style.borderColor
32+
dotElement.style.color = 'var(--color-main-text)'
3333

3434
if (event.extendedProps.percent === 100) {
3535
dotElement.classList.add('calendar-grid-checkbox-checked')
@@ -41,7 +41,7 @@ export default function({ event, el }) {
4141
const dotElement = el.querySelector('.fc-daygrid-event-dot')
4242
dotElement.classList.remove('fc-daygrid-event-dot')
4343
dotElement.classList.add('fc-daygrid-event-checkbox')
44-
dotElement.style.color = dotElement.style.borderColor
44+
dotElement.style.color = 'var(--color-main-text)'
4545

4646
if (event.extendedProps.percent === 100) {
4747
dotElement.classList.add('calendar-grid-checkbox-checked')
@@ -91,4 +91,80 @@ export default function({ event, el }) {
9191
descriptionContainer.appendChild(description)
9292
}
9393
}
94+
95+
if (
96+
el.classList.contains('fc-event-nc-all-declined')
97+
|| el.classList.contains('fc-event-nc-needs-action')
98+
|| el.classList.contains('fc-event-nc-declined')
99+
) {
100+
const titleElement = el.querySelector('.fc-event-title')
101+
const dotElement = el.querySelector('.fc-daygrid-event-dot')
102+
103+
if (dotElement) {
104+
dotElement.style.borderWidth = '2px'
105+
dotElement.style.background = 'transparent'
106+
dotElement.style.minWidth = '10px'
107+
dotElement.style.minHeight = '10px'
108+
}
109+
110+
titleElement.style.color = 'var(--color-main-text)'
111+
el.style.background = 'transparent'
112+
el.title = t('calendar', 'All participants declined')
113+
114+
if (el.classList.contains('fc-event-nc-needs-action')) {
115+
el.title = t('calendar', 'Please confirm your participation')
116+
}
117+
118+
if (el.classList.contains('fc-event-nc-declined')) {
119+
el.title = t('calendar', 'You declined this event')
120+
titleElement.style.textDecoration = 'line-through'
121+
}
122+
}
123+
124+
if (el.classList.contains('fc-event-nc-all-declined')) {
125+
const titleElement = el.querySelector('.fc-event-title')
126+
127+
const svgString = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m40-120 440-760 440 760H40Zm440-120q17 0 28.5-11.5T520-280q0-17-11.5-28.5T480-320q-17 0-28.5 11.5T440-280q0 17 11.5 28.5T480-240Zm-40-120h80v-200h-80v200Z"/></svg>'
128+
titleElement.innerHTML = svgString + titleElement.innerHTML
129+
130+
const svgElement = titleElement.querySelector('svg')
131+
svgElement.style.fill = el.style.borderColor
132+
svgElement.style.width = '1em'
133+
svgElement.style.marginBottom = '0.2em'
134+
svgElement.style.verticalAlign = 'middle'
135+
}
136+
137+
if (el.classList.contains('fc-event-nc-tentative')) {
138+
const dotElement = el.querySelector('.fc-daygrid-event-dot')
139+
140+
const bgColor = el.style.backgroundColor ? el.style.backgroundColor : dotElement.style.borderColor
141+
const bgStripeColor = darkenColor(bgColor)
142+
143+
let backgroundStyling = `repeating-linear-gradient(45deg, ${bgStripeColor}, ${bgStripeColor} 1px, ${bgColor} 1px, ${bgColor} 10px)`
144+
145+
if (dotElement) {
146+
dotElement.style.borderWidth = '2px'
147+
backgroundStyling = `repeating-linear-gradient(45deg, ${bgColor}, ${bgColor} 1px, transparent 1px, transparent 3.5px)`
148+
149+
dotElement.style.background = backgroundStyling
150+
dotElement.style.minWidth = '10px'
151+
dotElement.style.minHeight = '10px'
152+
} else {
153+
el.style.background = backgroundStyling
154+
}
155+
156+
el.title = t('calendar', 'Your participation is tentative')
157+
}
158+
}
159+
160+
/**
161+
* Create a slightly darker color for background stripes
162+
*
163+
* @param {string} color The color to darken
164+
*/
165+
function darkenColor(color) {
166+
const rgb = color.match(/\d+/g)
167+
if (!rgb) return color
168+
const [r, g, b] = rgb.map(c => Math.max(0, Math.min(255, c - (c * 0.3))))
169+
return `rgb(${r}, ${g}, ${b})`
94170
}

0 commit comments

Comments
 (0)