Skip to content

Commit d7fbb8b

Browse files
Merge pull request #8853 from madbob/issue_4553
fix: bell icon for events with reminders
2 parents 46afed0 + 8827c44 commit d7fbb8b

8 files changed

Lines changed: 38 additions & 120 deletions

File tree

src/fullcalendar/eventSources/eventSourceFunction.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import useTasksStore from '@/store/unscheduledTasks.js'
99
import { getAllObjectsInTimeRange } from '@/utils/calendarObject.js'
1010
import {
1111
getHexForColorName,
12-
hexToRGB,
13-
isLight,
1412
} from '@/utils/color.js'
1513
import logger from '@/utils/logger.js'
1614
/**
@@ -169,7 +167,6 @@ export function eventSourceFunction(calendarObjects, calendar, start, end, timez
169167
calendarOrder: calendar.order,
170168
calendarName: calendar.displayName,
171169
calendarId: calendar.id,
172-
darkText: isLight(hexToRGB(calendar.color)),
173170
objectType: object.name,
174171
percent: object.percent || null,
175172
davUrl: calendarObject.dav.url,

src/fullcalendar/rendering/eventDidMount.js

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
66
import { errorCatch } from '@/fullcalendar/utils/errors.js'
77
import { formatDateWithTimezone, isMultiDayAllDayEvent } from '@/utils/date.js'
8+
import { createSvgIconElement } from '@/utils/svg.ts'
89

910
/**
1011
* Build time description for all-day events
@@ -136,11 +137,9 @@ export default errorCatch(function({ event, el }) {
136137
const notificationIcon = document.createElement('span')
137138
notificationIcon.classList.add('icon-event-reminder')
138139
notificationIcon.setAttribute('aria-hidden', 'true')
139-
if (event.extendedProps.darkText) {
140-
notificationIcon.classList.add('icon-event-reminder--dark')
141-
} else {
142-
notificationIcon.classList.add('icon-event-reminder--light')
143-
}
140+
// From node_modules/vue-material-design-icons/Bell.vue
141+
const bellIcon = createSvgIconElement('M21,19V20H3V19L5,17V11C5,7.9 7.03,5.17 10,4.29C10,4.19 10,4.1 10,4A2,2 0 0,1 12,2A2,2 0 0,1 14,4C14,4.1 14,4.19 14,4.29C16.97,5.17 19,7.9 19,11V17L21,19M14,21A2,2 0 0,1 12,23A2,2 0 0,1 10,21')
142+
notificationIcon.appendChild(bellIcon)
144143
el.firstChild.appendChild(notificationIcon)
145144
}
146145

@@ -305,11 +304,13 @@ export default errorCatch(function({ event, el }) {
305304
event.extendedProps.attendeeCount >= 1
306305
&& !el.classList.contains('fc-event-nc-task')
307306
) {
308-
prependTitleIcon(el, 'M40-160v-112q0-34 17.5-62.5T104-378q62-31 126-46.5T360-440q66 0 130 15.5T616-378q29 15 46.5 43.5T680-272v112H40Zm640 0v-112q0-51-26-95.5T586-441q51 6 98 20.5t84 35.5q36 20 57 44.5t21 52.5v112H680ZM360-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47Zm400-160q0 66-47 113t-113 47q-11 0-28-2.5t-28-5.5q27-32 41.5-71t14.5-81q0-42-14.5-81T544-792q14-5 28-6.5t28-1.5q66 0 113 47t47 113Z')
307+
// From node_modules/vue-material-design-icons/AccountMultiple.vue
308+
prependTitleIcon(el, 'M16 17V19H2V17S2 13 9 13 16 17 16 17M12.5 7.5A3.5 3.5 0 1 0 9 11A3.5 3.5 0 0 0 12.5 7.5M15.94 13A5.32 5.32 0 0 1 18 17V19H22V17S22 13.37 15.94 13M15 4A3.39 3.39 0 0 0 13.07 4.59A5 5 0 0 1 13.07 10.41A3.39 3.39 0 0 0 15 11A3.5 3.5 0 0 0 15 4Z')
309309
}
310310

311311
if (el.classList.contains('fc-event-nc-all-declined')) {
312-
prependTitleIcon(el, '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')
312+
// From node_modules/vue-material-design-icons/Alert.vue
313+
prependTitleIcon(el, 'M13 14H11V9H13M13 18H11V16H13M1 21H23L12 2L1 21Z')
313314
}
314315

315316
if (el.classList.contains('fc-event-nc-tentative')) {
@@ -374,16 +375,7 @@ function prependTitleIcon(el, svgPath) {
374375
}
375376
}
376377

377-
const svgNS = 'http://www.w3.org/2000/svg'
378-
const svgElement = document.createElementNS(svgNS, 'svg')
379-
svgElement.setAttribute('viewBox', '0 -960 960 960')
380-
const pathElement = document.createElementNS(svgNS, 'path')
381-
pathElement.setAttribute('d', svgPath)
382-
svgElement.appendChild(pathElement)
383-
svgElement.style.fill = el.style.borderColor
384-
svgElement.style.width = '1em'
385-
svgElement.style.marginBottom = '0.2em'
386-
svgElement.style.verticalAlign = 'middle'
378+
const svgElement = createSvgIconElement(svgPath)
387379
titleElement.insertBefore(svgElement, titleElement.firstChild)
388380
}
389381

src/styles/fullcalendar.scss

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -194,19 +194,11 @@
194194
}
195195

196196
// Reminder icon on events with alarms set
197-
.fc-event-nc-alarms {
197+
&.fc-event-nc-alarms {
198198
.icon-event-reminder {
199-
background-color: inherit;
200-
background-position: right;
201199
position: absolute;
202200
top: 0;
203201
inset-inline-end: 0;
204-
&--light {
205-
background-image: var(--icon-calendar-reminder-fffffe)
206-
}
207-
&--dark {
208-
background-image: var(--icon-calendar-reminder-000001)
209-
}
210202
}
211203
}
212204

@@ -243,7 +235,10 @@
243235
}
244236

245237
svg {
238+
width: 1em;
246239
margin-inline-end: 2px;
240+
fill: var(--color-main-text);
241+
vertical-align: middle;
247242
}
248243

249244
@media only screen and (max-width: 767px) {
@@ -373,4 +368,3 @@
373368
[data-themes*="highcontrast"] {
374369
@include _fc-high-contrast-events;
375370
}
376-

src/utils/color.js

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,6 @@ export function isLight({ red, green, blue }) {
2121
return (brightness > 130)
2222
}
2323

24-
/**
25-
* Get a text-color that's readable on a given background color
26-
*
27-
* @param {string} hexColor The hex color to get a text color for
28-
* @return {string} the matching text color
29-
*/
30-
export function generateTextColorForHex(hexColor) {
31-
return generateTextColorForRGB(hexToRGB(hexColor))
32-
}
33-
3424
/**
3525
* Get a text-color that's readable on a given background color
3626
*
@@ -44,20 +34,6 @@ export function generateTextColorForRGB({ red, green, blue }) {
4434
return isLight({ red, green, blue }) ? '#000000' : '#FAFAFA'
4535
}
4636

47-
/**
48-
* Convert hex string to RGB
49-
*
50-
* @param {string} hexColor The hex color to convert
51-
* @return {string} the RGB result
52-
*/
53-
export function hexToRGB(hexColor) {
54-
if (hexColor === null) {
55-
return { red: 0, green: 0, blue: 0 }
56-
}
57-
const [red, green, blue] = convert.hex.rgb(hexColor.slice(1))
58-
return { red, green, blue }
59-
}
60-
6137
/**
6238
* Generates a hex color based on RGB string
6339
*

src/utils/svg.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
/**
7+
* Allocates a DOM element containing the given SVG icon
8+
* It is recommended to always use SVG icons from vue-material-design-icons and
9+
* to specify the original file from which SVG has been retrieved
10+
*
11+
* @param content SVG path data of the icon
12+
* @return The element to append to the DOM
13+
*/
14+
export function createSvgIconElement(content: string): SVGSVGElement {
15+
const svgNS = 'http://www.w3.org/2000/svg'
16+
const svgElement = document.createElementNS(svgNS, 'svg')
17+
svgElement.setAttribute('viewBox', '0 0 24 24')
18+
const pathElement = document.createElementNS(svgNS, 'path')
19+
pathElement.setAttribute('d', content)
20+
svgElement.appendChild(pathElement)
21+
return svgElement
22+
}

tests/javascript/unit/fullcalendar/eventSources/freeBusyResourceEventSourceFunction.test.js

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import freeBusyResourceEventSourceFunction from '@/fullcalendar/eventSources/fre
99
import useSettingsStore from '@/store/settings.js'
1010
import { getAllObjectsInTimeRange } from '@/utils/calendarObject.js'
1111
import {
12-
generateTextColorForHex,
1312
getHexForColorName,
1413
isLight,
1514
} from '@/utils/color.js'
@@ -21,7 +20,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
2120
beforeEach(() => {
2221
translate.mockClear()
2322
getHexForColorName.mockClear()
24-
generateTextColorForHex.mockClear()
2523
getAllObjectsInTimeRange.mockClear()
2624
setActivePinia(createPinia())
2725
})
@@ -31,8 +29,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
3129
.mockImplementation((app, str) => str)
3230
getHexForColorName
3331
.mockImplementation(() => '#ff0000')
34-
generateTextColorForHex
35-
.mockImplementation(() => '#eeeeee')
3632
isLight
3733
.mockImplementation(() => false)
3834

@@ -211,7 +207,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
211207
calendarId: 'Calendar id 456',
212208
calendarName: 'Calendar displayname',
213209
calendarOrder: 1337,
214-
darkText: false,
215210
davUrl: 'url1',
216211
objectType: 'VEVENT',
217212
percent: null,
@@ -235,7 +230,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
235230
calendarId: 'Calendar id 456',
236231
calendarName: 'Calendar displayname',
237232
calendarOrder: 1337,
238-
darkText: false,
239233
davUrl: 'url1',
240234
objectType: 'VEVENT',
241235
percent: null,
@@ -259,7 +253,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
259253
calendarId: 'Calendar id 456',
260254
calendarName: 'Calendar displayname',
261255
calendarOrder: 1337,
262-
darkText: false,
263256
davUrl: 'url1',
264257
objectType: 'VEVENT',
265258
percent: null,
@@ -283,7 +276,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
283276
calendarId: 'Calendar id 456',
284277
calendarName: 'Calendar displayname',
285278
calendarOrder: 1337,
286-
darkText: false,
287279
davUrl: 'url2',
288280
objectType: 'VEVENT',
289281
percent: null,
@@ -307,7 +299,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
307299
calendarId: 'Calendar id 456',
308300
calendarName: 'Calendar displayname',
309301
calendarOrder: 1337,
310-
darkText: false,
311302
davUrl: 'url4',
312303
objectType: 'VEVENT',
313304
percent: null,
@@ -360,8 +351,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
360351
expect(getHexForColorName).toHaveBeenCalledTimes(1)
361352
expect(getHexForColorName).toHaveBeenNthCalledWith(1, 'red')
362353

363-
expect(generateTextColorForHex).toHaveBeenCalledTimes(0)
364-
365354
// Make sure the following dates have not been touched
366355
expect(event11Start.getFullYear()).toEqual(2020)
367356
expect(event11Start.getMonth()).toEqual(1)
@@ -440,8 +429,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
440429
.mockImplementation((app, str) => str)
441430
getHexForColorName
442431
.mockImplementation(() => '#ff0000')
443-
generateTextColorForHex
444-
.mockImplementation(() => '#eeeeee')
445432
isLight
446433
.mockImplementation(() => false)
447434

@@ -609,7 +596,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
609596
calendarName: 'Calendar displayname',
610597
calendarOrder: 1337,
611598
canModifyAllDay: false,
612-
darkText: false,
613599
davUrl: 'url1',
614600
objectId: '1',
615601
vobjectId: '1',
@@ -634,7 +620,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
634620
calendarName: 'Calendar displayname',
635621
calendarOrder: 1337,
636622
canModifyAllDay: false,
637-
darkText: false,
638623
davUrl: 'url1',
639624
objectId: '1',
640625
vobjectId: '2',
@@ -659,7 +644,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
659644
calendarName: 'Calendar displayname',
660645
calendarOrder: 1337,
661646
canModifyAllDay: false,
662-
darkText: false,
663647
davUrl: 'url1',
664648
objectId: '1',
665649
vobjectId: '3',
@@ -684,7 +668,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
684668
calendarName: 'Calendar displayname',
685669
calendarOrder: 1337,
686670
canModifyAllDay: false,
687-
darkText: false,
688671
davUrl: 'url1',
689672
objectId: '1',
690673
vobjectId: '4',
@@ -709,7 +692,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
709692
calendarName: 'Calendar displayname',
710693
calendarOrder: 1337,
711694
canModifyAllDay: false,
712-
darkText: false,
713695
davUrl: 'url1',
714696
objectId: '1',
715697
vobjectId: '5',
@@ -755,7 +737,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
755737
expect(getAllObjectsInTimeRange).toHaveBeenNthCalledWith(1, calendarObjects[0], start, end)
756738

757739
expect(getHexForColorName).toHaveBeenCalledTimes(0)
758-
expect(generateTextColorForHex).toHaveBeenCalledTimes(0)
759740
})
760741

761742
it('should filter events by search query matching title, location, description, attendee, and organizer', () => {

tests/javascript/unit/fullcalendar/rendering/eventDidMount.test.js

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -41,39 +41,7 @@ describe('fullcalendar/eventDidMount test suite', () => {
4141
expect(el.dataset.recurrenceId).toEqual(undefined)
4242
})
4343

44-
it('should add an alarm bell icon if event has an alarm - dark', () => {
45-
const fcTime = document.createElement('span')
46-
fcTime.classList.add('fc-time')
47-
fcTime.appendChild(document.createTextNode('2pm'))
48-
const fcTitle = document.createElement('span')
49-
fcTitle.classList.add('fc-title')
50-
fcTitle.appendChild(document.createTextNode('Title 123'))
51-
52-
const fcContent = document.createElement('div')
53-
fcContent.classList.add('fc-content')
54-
fcContent.appendChild(fcTime)
55-
fcContent.appendChild(fcTitle)
56-
57-
const el = document.createElement('div')
58-
el.classList.add('fc-event-nc-alarms')
59-
el.appendChild(fcContent)
60-
61-
const event = {
62-
source: {},
63-
extendedProps: {
64-
objectId: 'object123',
65-
recurrenceId: 'recurrence456',
66-
darkText: true,
67-
percent: 100,
68-
},
69-
}
70-
71-
eventRender({ event, el })
72-
73-
expect(el.outerHTML).toEqual('<div class="fc-event-nc-alarms" aria-label="undefined" data-object-id="object123" data-recurrence-id="recurrence456"><div class="fc-content"><span class="fc-time">2pm</span><span class="fc-title">Title 123</span><span class="icon-event-reminder icon-event-reminder--dark" aria-hidden="true"></span></div></div>')
74-
})
75-
76-
it('should add an alarm bell icon if event has an alarm - light', () => {
44+
it('should add an alarm bell icon if event has an alarm', () => {
7745
const fcTime = document.createElement('span')
7846
fcTime.classList.add('fc-time')
7947
fcTime.appendChild(document.createTextNode('2pm'))
@@ -102,7 +70,8 @@ describe('fullcalendar/eventDidMount test suite', () => {
10270

10371
eventRender({ event, el })
10472

105-
expect(el.outerHTML).toEqual('<div class="fc-event-nc-alarms" aria-label="undefined" data-object-id="object123" data-recurrence-id="recurrence456"><div class="fc-content"><span class="fc-time">2pm</span><span class="fc-title">Title 123</span><span class="icon-event-reminder icon-event-reminder--light" aria-hidden="true"></span></div></div>')
73+
expect(el.outerHTML).toContain('<div class="fc-event-nc-alarms" aria-label="undefined" data-object-id="object123" data-recurrence-id="recurrence456">')
74+
expect(el.outerHTML).toContain('<span class="icon-event-reminder" aria-hidden="true"><svg viewBox="0 0 24 24">')
10675
})
10776

10877
// TODO: fix me later

tests/javascript/unit/utils/color.test.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,14 @@
44
*/
55
import {
66
detectColor,
7-
generateTextColorForHex,
87
generateTextColorForRGB,
98
getClosestCSS3ColorNameForHex,
109
getHexForColorName,
11-
hexToRGB,
1210
isLight,
1311
uidToHexColor,
1412
} from '@/utils/color.js'
1513

1614
describe('utils/color test suite', () => {
17-
it('should provide a function to generate a text-color from an rgb string', () => {
18-
expect(generateTextColorForHex('#fff')).toEqual('#000000')
19-
expect(generateTextColorForHex('#000')).toEqual('#FAFAFA')
20-
expect(generateTextColorForHex('#FF00FF')).toEqual('#FAFAFA')
21-
expect(generateTextColorForHex('#00FF00')).toEqual('#000000')
22-
})
23-
2415
it('should provide a function to generate a text-color from rgb values', () => {
2516
expect(generateTextColorForRGB({ red: 255, green: 255, blue: 255 })).toEqual('#000000')
2617
expect(generateTextColorForRGB({ red: 0, green: 0, blue: 0 })).toEqual('#FAFAFA')
@@ -33,10 +24,6 @@ describe('utils/color test suite', () => {
3324
expect(isLight({ red: 0, green: 0, blue: 0 })).toEqual(false)
3425
})
3526

36-
it('should provide a RGB object for a hex string', () => {
37-
expect(hexToRGB('#C98879')).toEqual({ red: 201, green: 136, blue: 121 })
38-
})
39-
4027
it('should provide a HEX string for a UID', () => {
4128
expect(uidToHexColor('uid123')).toEqual('#C98879')
4229
expect(uidToHexColor('')).toEqual('#0082C9')

0 commit comments

Comments
 (0)