Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/fullcalendar/eventSources/eventSourceFunction.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import useTasksStore from '@/store/unscheduledTasks.js'
import { getAllObjectsInTimeRange } from '@/utils/calendarObject.js'
import {
getHexForColorName,
hexToRGB,
isLight,
} from '@/utils/color.js'
import logger from '@/utils/logger.js'
/**
Expand Down Expand Up @@ -169,7 +167,6 @@ export function eventSourceFunction(calendarObjects, calendar, start, end, timez
calendarOrder: calendar.order,
calendarName: calendar.displayName,
calendarId: calendar.id,
darkText: isLight(hexToRGB(calendar.color)),
objectType: object.name,
percent: object.percent || null,
davUrl: calendarObject.dav.url,
Expand Down
26 changes: 9 additions & 17 deletions src/fullcalendar/rendering/eventDidMount.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
import { errorCatch } from '@/fullcalendar/utils/errors.js'
import { formatDateWithTimezone, isMultiDayAllDayEvent } from '@/utils/date.js'
import { createSvgIconElement } from '@/utils/svg.ts'

/**
* Build time description for all-day events
Expand Down Expand Up @@ -136,11 +137,9 @@ export default errorCatch(function({ event, el }) {
const notificationIcon = document.createElement('span')
notificationIcon.classList.add('icon-event-reminder')
notificationIcon.setAttribute('aria-hidden', 'true')
if (event.extendedProps.darkText) {
notificationIcon.classList.add('icon-event-reminder--dark')
} else {
notificationIcon.classList.add('icon-event-reminder--light')
}
// From node_modules/vue-material-design-icons/Bell.vue
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')
notificationIcon.appendChild(bellIcon)
el.firstChild.appendChild(notificationIcon)
}

Expand Down Expand Up @@ -305,11 +304,13 @@ export default errorCatch(function({ event, el }) {
event.extendedProps.attendeeCount >= 1
&& !el.classList.contains('fc-event-nc-task')
) {
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')
// From node_modules/vue-material-design-icons/AccountMultiple.vue
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')
}

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

if (el.classList.contains('fc-event-nc-tentative')) {
Expand Down Expand Up @@ -374,16 +375,7 @@ function prependTitleIcon(el, svgPath) {
}
}

const svgNS = 'http://www.w3.org/2000/svg'
const svgElement = document.createElementNS(svgNS, 'svg')
svgElement.setAttribute('viewBox', '0 -960 960 960')
const pathElement = document.createElementNS(svgNS, 'path')
pathElement.setAttribute('d', svgPath)
svgElement.appendChild(pathElement)
svgElement.style.fill = el.style.borderColor
svgElement.style.width = '1em'
svgElement.style.marginBottom = '0.2em'
svgElement.style.verticalAlign = 'middle'
const svgElement = createSvgIconElement(svgPath)
titleElement.insertBefore(svgElement, titleElement.firstChild)
}

Expand Down
14 changes: 4 additions & 10 deletions src/styles/fullcalendar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -194,19 +194,11 @@
}

// Reminder icon on events with alarms set
.fc-event-nc-alarms {
&.fc-event-nc-alarms {
.icon-event-reminder {
background-color: inherit;
background-position: right;
position: absolute;
top: 0;
inset-inline-end: 0;
&--light {
background-image: var(--icon-calendar-reminder-fffffe)
}
&--dark {
background-image: var(--icon-calendar-reminder-000001)
}
}
}

Expand Down Expand Up @@ -243,7 +235,10 @@
}

svg {
width: 1em;
margin-inline-end: 2px;
fill: var(--color-main-text);
vertical-align: middle;
}

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

24 changes: 0 additions & 24 deletions src/utils/color.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,6 @@ export function isLight({ red, green, blue }) {
return (brightness > 130)
}

/**
* Get a text-color that's readable on a given background color
*
* @param {string} hexColor The hex color to get a text color for
* @return {string} the matching text color
*/
export function generateTextColorForHex(hexColor) {
return generateTextColorForRGB(hexToRGB(hexColor))
}

/**
* Get a text-color that's readable on a given background color
*
Expand All @@ -44,20 +34,6 @@ export function generateTextColorForRGB({ red, green, blue }) {
return isLight({ red, green, blue }) ? '#000000' : '#FAFAFA'
}

/**
* Convert hex string to RGB
*
* @param {string} hexColor The hex color to convert
* @return {string} the RGB result
*/
export function hexToRGB(hexColor) {
if (hexColor === null) {
return { red: 0, green: 0, blue: 0 }
}
const [red, green, blue] = convert.hex.rgb(hexColor.slice(1))
return { red, green, blue }
}

/**
* Generates a hex color based on RGB string
*
Expand Down
22 changes: 22 additions & 0 deletions src/utils/svg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/**
* Allocates a DOM element containing the given SVG icon
* It is recommended to always use SVG icons from vue-material-design-icons and
* to specify the original file from which SVG has been retrieved
*
* @param content SVG path data of the icon
* @return The element to append to the DOM
*/
export function createSvgIconElement(content: string): SVGSVGElement {
const svgNS = 'http://www.w3.org/2000/svg'
const svgElement = document.createElementNS(svgNS, 'svg')
svgElement.setAttribute('viewBox', '0 0 24 24')
const pathElement = document.createElementNS(svgNS, 'path')
pathElement.setAttribute('d', content)
svgElement.appendChild(pathElement)
return svgElement
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { eventSourceFunction } from '@/fullcalendar/eventSources/eventSourceFunc
import useSettingsStore from '@/store/settings.js'
import { getAllObjectsInTimeRange } from '@/utils/calendarObject.js'
import {
generateTextColorForHex,
getHexForColorName,
isLight,
} from '@/utils/color.js'
Expand All @@ -20,7 +19,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
beforeEach(() => {
translate.mockClear()
getHexForColorName.mockClear()
generateTextColorForHex.mockClear()
getAllObjectsInTimeRange.mockClear()
setActivePinia(createPinia())
})
Expand All @@ -30,8 +28,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
.mockImplementation((app, str) => str)
getHexForColorName
.mockImplementation(() => '#ff0000')
generateTextColorForHex
.mockImplementation(() => '#eeeeee')
isLight
.mockImplementation(() => false)

Expand Down Expand Up @@ -210,7 +206,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
calendarId: 'Calendar id 456',
calendarName: 'Calendar displayname',
calendarOrder: 1337,
darkText: false,
davUrl: 'url1',
objectType: 'VEVENT',
percent: null,
Expand All @@ -234,7 +229,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
calendarId: 'Calendar id 456',
calendarName: 'Calendar displayname',
calendarOrder: 1337,
darkText: false,
davUrl: 'url1',
objectType: 'VEVENT',
percent: null,
Expand All @@ -258,7 +252,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
calendarId: 'Calendar id 456',
calendarName: 'Calendar displayname',
calendarOrder: 1337,
darkText: false,
davUrl: 'url1',
objectType: 'VEVENT',
percent: null,
Expand All @@ -282,7 +275,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
calendarId: 'Calendar id 456',
calendarName: 'Calendar displayname',
calendarOrder: 1337,
darkText: false,
davUrl: 'url2',
objectType: 'VEVENT',
percent: null,
Expand All @@ -306,7 +298,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
calendarId: 'Calendar id 456',
calendarName: 'Calendar displayname',
calendarOrder: 1337,
darkText: false,
davUrl: 'url4',
objectType: 'VEVENT',
percent: null,
Expand Down Expand Up @@ -359,8 +350,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
expect(getHexForColorName).toHaveBeenCalledTimes(1)
expect(getHexForColorName).toHaveBeenNthCalledWith(1, 'red')

expect(generateTextColorForHex).toHaveBeenCalledTimes(0)

// Make sure the following dates have not been touched
expect(event11Start.getFullYear()).toEqual(2020)
expect(event11Start.getMonth()).toEqual(1)
Expand Down Expand Up @@ -439,8 +428,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
.mockImplementation((app, str) => str)
getHexForColorName
.mockImplementation(() => '#ff0000')
generateTextColorForHex
.mockImplementation(() => '#eeeeee')
isLight
.mockImplementation(() => false)

Expand Down Expand Up @@ -608,7 +595,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
calendarName: 'Calendar displayname',
calendarOrder: 1337,
canModifyAllDay: false,
darkText: false,
davUrl: 'url1',
objectId: '1',
vobjectId: '1',
Expand All @@ -633,7 +619,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
calendarName: 'Calendar displayname',
calendarOrder: 1337,
canModifyAllDay: false,
darkText: false,
davUrl: 'url1',
objectId: '1',
vobjectId: '2',
Expand All @@ -658,7 +643,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
calendarName: 'Calendar displayname',
calendarOrder: 1337,
canModifyAllDay: false,
darkText: false,
davUrl: 'url1',
objectId: '1',
vobjectId: '3',
Expand All @@ -683,7 +667,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
calendarName: 'Calendar displayname',
calendarOrder: 1337,
canModifyAllDay: false,
darkText: false,
davUrl: 'url1',
objectId: '1',
vobjectId: '4',
Expand All @@ -708,7 +691,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
calendarName: 'Calendar displayname',
calendarOrder: 1337,
canModifyAllDay: false,
darkText: false,
davUrl: 'url1',
objectId: '1',
vobjectId: '5',
Expand Down Expand Up @@ -754,7 +736,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {
expect(getAllObjectsInTimeRange).toHaveBeenNthCalledWith(1, calendarObjects[0], start, end)

expect(getHexForColorName).toHaveBeenCalledTimes(0)
expect(generateTextColorForHex).toHaveBeenCalledTimes(0)
})

it('should filter events by search query matching title, location, description, attendee, and organizer', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,39 +41,7 @@ describe('fullcalendar/eventDidMount test suite', () => {
expect(el.dataset.recurrenceId).toEqual(undefined)
})

it('should add an alarm bell icon if event has an alarm - dark', () => {
const fcTime = document.createElement('span')
fcTime.classList.add('fc-time')
fcTime.appendChild(document.createTextNode('2pm'))
const fcTitle = document.createElement('span')
fcTitle.classList.add('fc-title')
fcTitle.appendChild(document.createTextNode('Title 123'))

const fcContent = document.createElement('div')
fcContent.classList.add('fc-content')
fcContent.appendChild(fcTime)
fcContent.appendChild(fcTitle)

const el = document.createElement('div')
el.classList.add('fc-event-nc-alarms')
el.appendChild(fcContent)

const event = {
source: {},
extendedProps: {
objectId: 'object123',
recurrenceId: 'recurrence456',
darkText: true,
percent: 100,
},
}

eventRender({ event, el })

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>')
})

it('should add an alarm bell icon if event has an alarm - light', () => {
it('should add an alarm bell icon if event has an alarm', () => {
const fcTime = document.createElement('span')
fcTime.classList.add('fc-time')
fcTime.appendChild(document.createTextNode('2pm'))
Expand Down Expand Up @@ -102,7 +70,8 @@ describe('fullcalendar/eventDidMount test suite', () => {

eventRender({ event, el })

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>')
expect(el.outerHTML).toContain('<div class="fc-event-nc-alarms" aria-label="undefined" data-object-id="object123" data-recurrence-id="recurrence456">')
expect(el.outerHTML).toContain('<span class="icon-event-reminder" aria-hidden="true"><svg viewBox="0 0 24 24">')
})

// TODO: fix me later
Expand Down
13 changes: 0 additions & 13 deletions tests/javascript/unit/utils/color.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,14 @@
*/
import {
detectColor,
generateTextColorForHex,
generateTextColorForRGB,
getClosestCSS3ColorNameForHex,
getHexForColorName,
hexToRGB,
isLight,
uidToHexColor,
} from '@/utils/color.js'

describe('utils/color test suite', () => {
it('should provide a function to generate a text-color from an rgb string', () => {
expect(generateTextColorForHex('#fff')).toEqual('#000000')
expect(generateTextColorForHex('#000')).toEqual('#FAFAFA')
expect(generateTextColorForHex('#FF00FF')).toEqual('#FAFAFA')
expect(generateTextColorForHex('#00FF00')).toEqual('#000000')
})

it('should provide a function to generate a text-color from rgb values', () => {
expect(generateTextColorForRGB({ red: 255, green: 255, blue: 255 })).toEqual('#000000')
expect(generateTextColorForRGB({ red: 0, green: 0, blue: 0 })).toEqual('#FAFAFA')
Expand All @@ -33,10 +24,6 @@ describe('utils/color test suite', () => {
expect(isLight({ red: 0, green: 0, blue: 0 })).toEqual(false)
})

it('should provide a RGB object for a hex string', () => {
expect(hexToRGB('#C98879')).toEqual({ red: 201, green: 136, blue: 121 })
})

it('should provide a HEX string for a UID', () => {
expect(uidToHexColor('uid123')).toEqual('#C98879')
expect(uidToHexColor('')).toEqual('#0082C9')
Expand Down
Loading