diff --git a/src/fullcalendar/eventSources/eventSourceFunction.js b/src/fullcalendar/eventSources/eventSourceFunction.js index 04babcc08e..5a6be1c25a 100644 --- a/src/fullcalendar/eventSources/eventSourceFunction.js +++ b/src/fullcalendar/eventSources/eventSourceFunction.js @@ -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' /** @@ -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, diff --git a/src/fullcalendar/rendering/eventDidMount.js b/src/fullcalendar/rendering/eventDidMount.js index a73f16824e..32279d41d2 100644 --- a/src/fullcalendar/rendering/eventDidMount.js +++ b/src/fullcalendar/rendering/eventDidMount.js @@ -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 @@ -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) } @@ -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')) { @@ -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) } diff --git a/src/styles/fullcalendar.scss b/src/styles/fullcalendar.scss index ad39863b13..0dd7ad4b03 100644 --- a/src/styles/fullcalendar.scss +++ b/src/styles/fullcalendar.scss @@ -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) - } } } @@ -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) { @@ -373,4 +368,3 @@ [data-themes*="highcontrast"] { @include _fc-high-contrast-events; } - diff --git a/src/utils/color.js b/src/utils/color.js index 298b5524fd..9db53a1a7e 100644 --- a/src/utils/color.js +++ b/src/utils/color.js @@ -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 * @@ -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 * diff --git a/src/utils/svg.ts b/src/utils/svg.ts new file mode 100644 index 0000000000..3815c947ce --- /dev/null +++ b/src/utils/svg.ts @@ -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 +} diff --git a/tests/javascript/unit/fullcalendar/eventSources/freeBusyResourceEventSourceFunction.test.js b/tests/javascript/unit/fullcalendar/eventSources/freeBusyResourceEventSourceFunction.test.js index f70eed721c..5806fe8ca0 100644 --- a/tests/javascript/unit/fullcalendar/eventSources/freeBusyResourceEventSourceFunction.test.js +++ b/tests/javascript/unit/fullcalendar/eventSources/freeBusyResourceEventSourceFunction.test.js @@ -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' @@ -20,7 +19,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => { beforeEach(() => { translate.mockClear() getHexForColorName.mockClear() - generateTextColorForHex.mockClear() getAllObjectsInTimeRange.mockClear() setActivePinia(createPinia()) }) @@ -30,8 +28,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => { .mockImplementation((app, str) => str) getHexForColorName .mockImplementation(() => '#ff0000') - generateTextColorForHex - .mockImplementation(() => '#eeeeee') isLight .mockImplementation(() => false) @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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) @@ -439,8 +428,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => { .mockImplementation((app, str) => str) getHexForColorName .mockImplementation(() => '#ff0000') - generateTextColorForHex - .mockImplementation(() => '#eeeeee') isLight .mockImplementation(() => false) @@ -608,7 +595,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => { calendarName: 'Calendar displayname', calendarOrder: 1337, canModifyAllDay: false, - darkText: false, davUrl: 'url1', objectId: '1', vobjectId: '1', @@ -633,7 +619,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => { calendarName: 'Calendar displayname', calendarOrder: 1337, canModifyAllDay: false, - darkText: false, davUrl: 'url1', objectId: '1', vobjectId: '2', @@ -658,7 +643,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => { calendarName: 'Calendar displayname', calendarOrder: 1337, canModifyAllDay: false, - darkText: false, davUrl: 'url1', objectId: '1', vobjectId: '3', @@ -683,7 +667,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => { calendarName: 'Calendar displayname', calendarOrder: 1337, canModifyAllDay: false, - darkText: false, davUrl: 'url1', objectId: '1', vobjectId: '4', @@ -708,7 +691,6 @@ describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => { calendarName: 'Calendar displayname', calendarOrder: 1337, canModifyAllDay: false, - darkText: false, davUrl: 'url1', objectId: '1', vobjectId: '5', @@ -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', () => { diff --git a/tests/javascript/unit/fullcalendar/rendering/eventDidMount.test.js b/tests/javascript/unit/fullcalendar/rendering/eventDidMount.test.js index 754938a001..62f8126480 100644 --- a/tests/javascript/unit/fullcalendar/rendering/eventDidMount.test.js +++ b/tests/javascript/unit/fullcalendar/rendering/eventDidMount.test.js @@ -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('