Skip to content
Open
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
133 changes: 133 additions & 0 deletions src/fullcalendar/rendering/eventDidMount.js
Comment thread
tcitworld marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,125 @@
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
import { formatDateWithTimezone, isMultiDayAllDayEvent } from '../../utils/date.js'
import { errorCatch } from '../utils/errors.js'

/**
* Build time description for all-day events
*
* @param {EventApi} event The event
* @param {string|undefined} locale Locale for event time formatting
* @return {string} Time description
*/
function buildAllDayTimeDescription(event, locale) {
if (!event.start) {
return ''
}

const dateOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
}
const startStr = formatDateWithTimezone(event.start, locale, dateOptions)

if (!event.end || !isMultiDayAllDayEvent(event.start, event.end)) {
return startStr
}

// Multi-day event: calculate end date (exclusive, so subtract 1 day)
const adjustedEnd = new Date(event.end)
adjustedEnd.setDate(adjustedEnd.getDate() - 1)
const endStr = formatDateWithTimezone(adjustedEnd, locale, dateOptions)

return t('calendar', '{startDate} to {endDate}', {
startDate: startStr,
endDate: endStr,
})
}

/**
* Build time description for timed events
*
* @param {EventApi} event The event
* @param {string|undefined} locale The locale to use
* @return {string} Time description
*/
function buildTimedEventDescription(event, locale) {
if (!event.start) {
return ''
}

const dateTimeOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
}
const timeOptions = {
hour: 'numeric',
minute: 'numeric',
}

const startStr = formatDateWithTimezone(event.start, locale, dateTimeOptions, true)

if (!event.end) {
return startStr
}

// Check if same day - only show time for end
const sameDay = event.start.toDateString() === event.end.toDateString()
Comment thread
GVodyanov marked this conversation as resolved.
if (sameDay) {
const endTimeStr = formatDateWithTimezone(event.end, locale, timeOptions, true)
return t('calendar', '{startDateTime} to {endTime}', {
startDateTime: startStr,
endTime: endTimeStr,
})
}

// Multi-day timed event
const endStr = formatDateWithTimezone(event.end, locale, dateTimeOptions, true)
return t('calendar', '{startDateTime} to {endDateTime}', {
startDateTime: startStr,
endDateTime: endStr,
})
}

/**
* Builds an accessible label for a calendar event including its title and time.
*
* @param {EventApi} event The fullcalendar event object
* @return {string} A human-readable label for screen readers
*/
function buildAriaLabel(event) {
const locale = getCanonicalLocale() || undefined
const title = event.title || t('calendar', 'Untitled event')

if (event.allDay) {
const timeDescription = buildAllDayTimeDescription(event, locale)
if (timeDescription) {
return t('calendar', '{title}, All day: {timeDescription}', {
title,
timeDescription,
})
}
return t('calendar', '{title}, All day', { title })
}

const timeDescription = buildTimedEventDescription(event, locale)
if (timeDescription) {
return t('calendar', '{title}, {timeDescription}', {
title,
timeDescription,
})
}
return title
}

/**
* Adds data to the html element representing the event in the fullcalendar grid.
* This is used to later on position the popover
Expand All @@ -13,9 +130,12 @@
* @param {Node} data.el The HTML element
*/
export default errorCatch(function({ event, el }) {
// Set aria-label for screen reader accessibility
el.setAttribute('aria-label', buildAriaLabel(event))
if (el.classList.contains('fc-event-nc-alarms')) {
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 {
Expand Down Expand Up @@ -188,6 +308,19 @@
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')
}

if (titleElement) {

Check failure on line 311 in src/fullcalendar/rendering/eventDidMount.js

View workflow job for this annotation

GitHub Actions / NPM lint

'titleElement' is not defined

Check failure on line 311 in src/fullcalendar/rendering/eventDidMount.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 1 tab but found 2 spaces
const svgString = '<svg aria-hidden="true" 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>'

Check failure on line 312 in src/fullcalendar/rendering/eventDidMount.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 2 tabs but found 4 spaces
titleElement.innerHTML = svgString + titleElement.innerHTML

Check failure on line 313 in src/fullcalendar/rendering/eventDidMount.js

View workflow job for this annotation

GitHub Actions / NPM lint

'titleElement' is not defined

Check failure on line 313 in src/fullcalendar/rendering/eventDidMount.js

View workflow job for this annotation

GitHub Actions / NPM lint

'titleElement' is not defined

Check failure on line 313 in src/fullcalendar/rendering/eventDidMount.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 2 tabs but found 4 spaces

const svgElement = titleElement.querySelector('svg')

Check failure on line 315 in src/fullcalendar/rendering/eventDidMount.js

View workflow job for this annotation

GitHub Actions / NPM lint

'titleElement' is not defined

Check failure on line 315 in src/fullcalendar/rendering/eventDidMount.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 2 tabs but found 4 spaces
if (svgElement) {

Check failure on line 316 in src/fullcalendar/rendering/eventDidMount.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 2 tabs but found 4 spaces
svgElement.style.fill = el.style.borderColor

Check failure on line 317 in src/fullcalendar/rendering/eventDidMount.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 3 tabs but found 6 spaces
svgElement.style.width = '1em'
svgElement.style.marginBottom = '0.2em'
svgElement.style.verticalAlign = 'middle'
}
}

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')
}
Expand Down
55 changes: 55 additions & 0 deletions src/utils/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { DateTimeValue } from '@nextcloud/calendar-js'
import getTimezoneManager from '../services/timezoneDataProviderService.js'
import useSettingsStore from '../store/settings.js'
import logger from './logger.js'

/**
Expand Down Expand Up @@ -115,3 +118,55 @@ export function modifyDate(date, { day = 0, week = 0, month = 0, year = 0 }) {

return date
}

/**
* Convert a date from UTC to user's timezone
*
* @param {Date} date The date to convert (in UTC)
* @return {Date} Converted date in user's timezone
*/
export function convertDateToUserTimezone(date) {
const settingsStore = useSettingsStore()
const userTimezoneId = settingsStore.getResolvedTimezone

const tzManager = getTimezoneManager()
const utcTimezone = tzManager.getTimezoneForId('UTC')
const userTimezone = tzManager.getTimezoneForId(userTimezoneId)

const dateTimeValue = DateTimeValue.fromJSDate(date, true)
dateTimeValue.replaceTimezone(utcTimezone)
return getDateFromDateTimeValue(dateTimeValue.getInTimezone(userTimezone))
}

/**
* Format a date with specified options
*
* @param {Date} date The date to format (in UTC for timed events, local for all-day)
* @param {string|undefined} locale The locale to use
* @param {object} options Formatting options
* @param {boolean} convertTimezone Whether to convert from UTC to user timezone
* @return {string} Formatted date string
*/
export function formatDateWithTimezone(date, locale, options, convertTimezone = false) {
const dateToFormat = convertTimezone ? convertDateToUserTimezone(date) : date
return dateToFormat.toLocaleString(locale ?? 'en_001', options)
}

/**
* Check if an all-day event spans multiple days
*
* @param {Date} start Start date
* @param {Date} end End date (exclusive in FullCalendar)
* @return {boolean} True if multi-day
*/
export function isMultiDayAllDayEvent(start, end) {
// FullCalendar all-day end dates are exclusive, so subtract one day
const adjustedEnd = new Date(end)
adjustedEnd.setDate(adjustedEnd.getDate() - 1)
adjustedEnd.setHours(0, 0, 0, 0)

const startMidnight = new Date(start)
startMidnight.setHours(0, 0, 0, 0)

return adjustedEnd.getTime() > startMidnight.getTime()
}
Loading
Loading