@@ -384,139 +384,19 @@ Module.register("calendar", {
384384 }
385385
386386 if ( this . config . timeFormat === "dateheaders" ) {
387- if ( this . config . flipDateHeaderTitle ) eventWrapper . appendChild ( titleWrapper ) ;
388-
389- if ( event . fullDayEvent ) {
390- titleWrapper . colSpan = "2" ;
391- titleWrapper . classList . add ( "align-left" ) ;
392- } else {
393- const timeWrapper = document . createElement ( "td" ) ;
394- timeWrapper . className = `time light ${ this . config . flipDateHeaderTitle ? "align-right " : "align-left " } ${ this . timeClassForUrl ( event . url ) } ` ;
395- timeWrapper . style . paddingLeft = "2px" ;
396- timeWrapper . style . textAlign = this . config . flipDateHeaderTitle ? "right" : "left" ;
397- timeWrapper . innerHTML = eventStartDateMoment . format ( "LT" ) ;
398-
399- // Add endDate to dataheaders if showEnd is enabled
400- if ( this . config . showEnd ) {
401- if ( this . config . showEndsOnlyWithDuration && event . startDate === event . endDate ) {
402- // no duration here, don't display end
403- } else {
404- timeWrapper . innerHTML += ` - ${ CalendarUtils . capFirst ( eventEndDateMoment . format ( "LT" ) ) } ` ;
405- }
406- }
407-
408- eventWrapper . appendChild ( timeWrapper ) ;
409-
410- if ( ! this . config . flipDateHeaderTitle ) titleWrapper . classList . add ( "align-right" ) ;
411- }
412- if ( ! this . config . flipDateHeaderTitle ) eventWrapper . appendChild ( titleWrapper ) ;
387+ this . renderDateHeadersEventTime ( eventWrapper , titleWrapper , event , eventStartDateMoment , eventEndDateMoment ) ;
413388 } else {
414389 const timeWrapper = document . createElement ( "td" ) ;
415390
416391 eventWrapper . appendChild ( titleWrapper ) ;
417392 const now = moment ( ) ;
418393
419394 if ( this . config . timeFormat === "absolute" ) {
420- // Use dateFormat
421- timeWrapper . innerHTML = CalendarUtils . capFirst ( eventStartDateMoment . format ( this . config . dateFormat ) ) ;
422- // Add end time if showEnd
423- if ( this . config . showEnd ) {
424- // and has a duration
425- if ( event . startDate !== event . endDate ) {
426- timeWrapper . innerHTML += "-" ;
427- timeWrapper . innerHTML += CalendarUtils . capFirst ( eventEndDateMoment . format ( this . config . dateEndFormat ) ) ;
428- }
429- }
430-
431- // For full day events we use the fullDayEventDateFormat
432- if ( event . fullDayEvent ) {
433- //subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
434- eventEndDateMoment . subtract ( 1 , "second" ) ;
435- timeWrapper . innerHTML = CalendarUtils . capFirst ( eventStartDateMoment . format ( this . config . fullDayEventDateFormat ) ) ;
436- // only show end if requested and allowed and the dates are different
437- if ( this . config . showEnd && ! this . config . showEndsOnlyWithDuration && ! eventStartDateMoment . isSame ( eventEndDateMoment , "d" ) ) {
438- timeWrapper . innerHTML += "-" ;
439- timeWrapper . innerHTML += CalendarUtils . capFirst ( eventEndDateMoment . format ( this . config . fullDayEventDateFormat ) ) ;
440- } else if ( ! eventStartDateMoment . isSame ( eventEndDateMoment , "d" ) && eventStartDateMoment . isBefore ( now ) ) {
441- timeWrapper . innerHTML = CalendarUtils . capFirst ( now . format ( this . config . fullDayEventDateFormat ) ) ;
442- }
443- } else if ( this . config . getRelative > 0 && eventStartDateMoment . isBefore ( now ) ) {
444- // Ongoing and getRelative is set
445- timeWrapper . innerHTML = CalendarUtils . capFirst (
446- this . translate ( "RUNNING" , {
447- fallback : `${ this . translate ( "RUNNING" ) } {timeUntilEnd}` ,
448- timeUntilEnd : eventEndDateMoment . fromNow ( true )
449- } )
450- ) ;
451- } else if ( this . config . urgency > 0 && eventStartDateMoment . diff ( now , "d" ) < this . config . urgency ) {
452- // Within urgency days
453- timeWrapper . innerHTML = CalendarUtils . capFirst ( eventStartDateMoment . fromNow ( ) ) ;
454- }
455- if ( event . fullDayEvent && this . config . nextDaysRelative ) {
456- // Full days events within the next two days
457- if ( event . today ) {
458- timeWrapper . innerHTML = CalendarUtils . capFirst ( this . translate ( "TODAY" ) ) ;
459- } else if ( event . yesterday ) {
460- timeWrapper . innerHTML = CalendarUtils . capFirst ( this . translate ( "YESTERDAY" ) ) ;
461- } else if ( event . tomorrow ) {
462- timeWrapper . innerHTML = CalendarUtils . capFirst ( this . translate ( "TOMORROW" ) ) ;
463- } else if ( event . dayAfterTomorrow ) {
464- if ( this . translate ( "DAYAFTERTOMORROW" ) !== "DAYAFTERTOMORROW" ) {
465- timeWrapper . innerHTML = CalendarUtils . capFirst ( this . translate ( "DAYAFTERTOMORROW" ) ) ;
466- }
467- }
468- }
395+ timeWrapper . innerHTML = this . buildAbsoluteTimeText ( event , eventStartDateMoment , eventEndDateMoment , now ) ;
469396 } else {
470- // Show relative times
471- if ( eventStartDateMoment . isSameOrAfter ( now ) || ( event . fullDayEvent && eventEndDateMoment . diff ( now , "days" ) === 0 ) ) {
472- // Use relative time
473- if ( ! this . config . hideTime && ! event . fullDayEvent ) {
474- Log . debug ( "[calendar] event not hidden and not fullday" ) ;
475- timeWrapper . innerHTML = `${ CalendarUtils . capFirst ( eventStartDateMoment . calendar ( null , { sameElse : this . config . dateFormat } ) ) } ` ;
476- } else {
477- Log . debug ( "[calendar] event full day or hidden" ) ;
478- timeWrapper . innerHTML = `${ CalendarUtils . capFirst (
479- eventStartDateMoment . calendar ( null , {
480- sameDay : this . config . showTimeToday ? "LT" : `[${ this . translate ( "TODAY" ) } ]` ,
481- nextDay : `[${ this . translate ( "TOMORROW" ) } ]` ,
482- nextWeek : "dddd" ,
483- sameElse : event . fullDayEvent ? this . config . fullDayEventDateFormat : this . config . dateFormat
484- } )
485- ) } `;
486- }
487- if ( event . fullDayEvent ) {
488- // Full days events within the next two days
489- if ( event . today || ( event . fullDayEvent && eventEndDateMoment . diff ( now , "days" ) === 0 ) ) {
490- timeWrapper . innerHTML = CalendarUtils . capFirst ( this . translate ( "TODAY" ) ) ;
491- } else if ( event . dayBeforeYesterday ) {
492- if ( this . translate ( "DAYBEFOREYESTERDAY" ) !== "DAYBEFOREYESTERDAY" ) {
493- timeWrapper . innerHTML = CalendarUtils . capFirst ( this . translate ( "DAYBEFOREYESTERDAY" ) ) ;
494- }
495- } else if ( event . yesterday ) {
496- timeWrapper . innerHTML = CalendarUtils . capFirst ( this . translate ( "YESTERDAY" ) ) ;
497- } else if ( event . tomorrow ) {
498- timeWrapper . innerHTML = CalendarUtils . capFirst ( this . translate ( "TOMORROW" ) ) ;
499- } else if ( event . dayAfterTomorrow ) {
500- if ( this . translate ( "DAYAFTERTOMORROW" ) !== "DAYAFTERTOMORROW" ) {
501- timeWrapper . innerHTML = CalendarUtils . capFirst ( this . translate ( "DAYAFTERTOMORROW" ) ) ;
502- }
503- }
504- Log . info ( "[calendar] event fullday" ) ;
505- } else if ( eventStartDateMoment . diff ( now , "h" ) < this . config . getRelative ) {
506- Log . info ( "[calendar] not full day but within getRelative size" ) ;
507- // If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
508- timeWrapper . innerHTML = `${ CalendarUtils . capFirst ( eventStartDateMoment . fromNow ( ) ) } ` ;
509- }
510- } else {
511- // Ongoing event
512- timeWrapper . innerHTML = CalendarUtils . capFirst (
513- this . translate ( "RUNNING" , {
514- fallback : `${ this . translate ( "RUNNING" ) } {timeUntilEnd}` ,
515- timeUntilEnd : eventEndDateMoment . fromNow ( true )
516- } )
517- ) ;
518- }
397+ timeWrapper . innerHTML = this . buildRelativeTimeText ( event , eventStartDateMoment , eventEndDateMoment , now ) ;
519398 }
399+
520400 timeWrapper . className = `time light ${ this . timeClassForUrl ( event . url ) } ` ;
521401 eventWrapper . appendChild ( timeWrapper ) ;
522402 }
@@ -793,6 +673,226 @@ Module.register("calendar", {
793673 ) ;
794674 } ,
795675
676+ createDateHeadersTimeWrapper ( url ) {
677+ const timeWrapper = document . createElement ( "td" ) ;
678+ timeWrapper . className = `time light ${ this . config . flipDateHeaderTitle ? "align-right " : "align-left " } ${ this . timeClassForUrl ( url ) } ` ;
679+ timeWrapper . style . paddingLeft = "2px" ;
680+ timeWrapper . style . textAlign = this . config . flipDateHeaderTitle ? "right" : "left" ;
681+ return timeWrapper ;
682+ } ,
683+
684+ hasEventDuration ( event ) {
685+ return event . startDate !== event . endDate ;
686+ } ,
687+
688+ shouldShowDateHeadersTimedEnd ( event ) {
689+ return this . config . showEnd && ( ! this . config . showEndsOnlyWithDuration || this . hasEventDuration ( event ) ) ;
690+ } ,
691+
692+ shouldShowRelativeTimedEnd ( event ) {
693+ return ! this . config . hideTime && this . config . showEnd && ( ! this . config . showEndsOnlyWithDuration || this . hasEventDuration ( event ) ) ;
694+ } ,
695+
696+ getAdjustedFullDayEndMoment ( endMoment ) {
697+ return endMoment . clone ( ) . subtract ( 1 , "second" ) ;
698+ } ,
699+
700+ renderDateHeadersEventTime ( eventWrapper , titleWrapper , event , eventStartDateMoment , eventEndDateMoment ) {
701+ if ( this . config . flipDateHeaderTitle ) eventWrapper . appendChild ( titleWrapper ) ;
702+
703+ if ( event . fullDayEvent ) {
704+ const adjustedEndMoment = this . getAdjustedFullDayEndMoment ( eventEndDateMoment ) ;
705+ if ( this . config . showEnd && ! this . config . showEndsOnlyWithDuration && ! eventStartDateMoment . isSame ( adjustedEndMoment , "d" ) ) {
706+ const timeWrapper = this . createDateHeadersTimeWrapper ( event . url ) ;
707+ timeWrapper . innerHTML = `-${ CalendarUtils . capFirst ( adjustedEndMoment . format ( this . config . fullDayEventDateFormat ) ) } ` ;
708+ eventWrapper . appendChild ( timeWrapper ) ;
709+ if ( ! this . config . flipDateHeaderTitle ) titleWrapper . classList . add ( "align-right" ) ;
710+ } else {
711+ titleWrapper . colSpan = "2" ;
712+ titleWrapper . classList . add ( "align-left" ) ;
713+ }
714+ } else {
715+ const timeWrapper = this . createDateHeadersTimeWrapper ( event . url ) ;
716+ timeWrapper . innerHTML = eventStartDateMoment . format ( "LT" ) ;
717+
718+ // In dateheaders mode, keep the end as time-only to avoid redundant date info under a date header.
719+ if ( this . shouldShowDateHeadersTimedEnd ( event ) ) {
720+ timeWrapper . innerHTML += `-${ CalendarUtils . capFirst ( eventEndDateMoment . format ( "LT" ) ) } ` ;
721+ }
722+
723+ eventWrapper . appendChild ( timeWrapper ) ;
724+ if ( ! this . config . flipDateHeaderTitle ) titleWrapper . classList . add ( "align-right" ) ;
725+ }
726+
727+ if ( ! this . config . flipDateHeaderTitle ) eventWrapper . appendChild ( titleWrapper ) ;
728+ } ,
729+
730+ buildAbsoluteTimeText ( event , eventStartDateMoment , eventEndDateMoment , now ) {
731+ let timeText = CalendarUtils . capFirst ( eventStartDateMoment . format ( this . config . dateFormat ) ) ;
732+
733+ if ( this . config . showEnd && ( ! this . config . showEndsOnlyWithDuration || this . hasEventDuration ( event ) ) ) {
734+ const sameDay = this . isSameDay ( eventStartDateMoment , eventEndDateMoment ) ;
735+ if ( sameDay && ! this . dateFormatIncludesTime ( ) ) {
736+ timeText += `, ${ eventStartDateMoment . format ( "LT" ) } ` ;
737+ }
738+ timeText += `-${ this . formatTimedEventEnd ( eventStartDateMoment , eventEndDateMoment ) } ` ;
739+ }
740+
741+ if ( event . fullDayEvent ) {
742+ const adjustedEndMoment = this . getAdjustedFullDayEndMoment ( eventEndDateMoment ) ;
743+ timeText = CalendarUtils . capFirst ( eventStartDateMoment . format ( this . config . fullDayEventDateFormat ) ) ;
744+
745+ if ( this . config . showEnd && ! this . config . showEndsOnlyWithDuration && ! eventStartDateMoment . isSame ( adjustedEndMoment , "d" ) ) {
746+ timeText += `-${ CalendarUtils . capFirst ( adjustedEndMoment . format ( this . config . fullDayEventDateFormat ) ) } ` ;
747+ } else if ( ! eventStartDateMoment . isSame ( adjustedEndMoment , "d" ) && eventStartDateMoment . isBefore ( now ) ) {
748+ timeText = CalendarUtils . capFirst ( now . format ( this . config . fullDayEventDateFormat ) ) ;
749+ }
750+
751+ if ( this . config . nextDaysRelative ) {
752+ let relativeLabel = false ;
753+ if ( event . today ) {
754+ timeText = CalendarUtils . capFirst ( this . translate ( "TODAY" ) ) ;
755+ relativeLabel = true ;
756+ } else if ( event . yesterday ) {
757+ timeText = CalendarUtils . capFirst ( this . translate ( "YESTERDAY" ) ) ;
758+ relativeLabel = true ;
759+ } else if ( event . tomorrow ) {
760+ timeText = CalendarUtils . capFirst ( this . translate ( "TOMORROW" ) ) ;
761+ relativeLabel = true ;
762+ } else if ( event . dayAfterTomorrow && this . translate ( "DAYAFTERTOMORROW" ) !== "DAYAFTERTOMORROW" ) {
763+ timeText = CalendarUtils . capFirst ( this . translate ( "DAYAFTERTOMORROW" ) ) ;
764+ relativeLabel = true ;
765+ }
766+
767+ if ( relativeLabel && this . config . showEnd && ! this . config . showEndsOnlyWithDuration && ! eventStartDateMoment . isSame ( adjustedEndMoment , "d" ) ) {
768+ timeText += `-${ CalendarUtils . capFirst ( adjustedEndMoment . format ( this . config . fullDayEventDateFormat ) ) } ` ;
769+ }
770+ }
771+
772+ return timeText ;
773+ }
774+
775+ if ( this . config . getRelative > 0 && eventStartDateMoment . isBefore ( now ) ) {
776+ return CalendarUtils . capFirst (
777+ this . translate ( "RUNNING" , {
778+ fallback : `${ this . translate ( "RUNNING" ) } {timeUntilEnd}` ,
779+ timeUntilEnd : eventEndDateMoment . fromNow ( true )
780+ } )
781+ ) ;
782+ }
783+
784+ if ( this . config . urgency > 0 && eventStartDateMoment . diff ( now , "d" ) < this . config . urgency ) {
785+ return CalendarUtils . capFirst ( eventStartDateMoment . fromNow ( ) ) ;
786+ }
787+
788+ return timeText ;
789+ } ,
790+
791+ buildRelativeTimeText ( event , eventStartDateMoment , eventEndDateMoment , now ) {
792+ if ( eventStartDateMoment . isSameOrAfter ( now ) || ( event . fullDayEvent && eventEndDateMoment . diff ( now , "days" ) === 0 ) ) {
793+ let timeText ;
794+
795+ if ( ! this . config . hideTime && ! event . fullDayEvent ) {
796+ Log . debug ( "[calendar] event not hidden and not fullday" ) ;
797+ timeText = `${ CalendarUtils . capFirst ( eventStartDateMoment . calendar ( null , { sameElse : this . config . dateFormat } ) ) } ` ;
798+ } else {
799+ Log . debug ( "[calendar] event full day or hidden" ) ;
800+ timeText = `${ CalendarUtils . capFirst (
801+ eventStartDateMoment . calendar ( null , {
802+ sameDay : this . config . showTimeToday ? "LT" : `[${ this . translate ( "TODAY" ) } ]` ,
803+ nextDay : `[${ this . translate ( "TOMORROW" ) } ]` ,
804+ nextWeek : "dddd" ,
805+ sameElse : event . fullDayEvent ? this . config . fullDayEventDateFormat : this . config . dateFormat
806+ } )
807+ ) } `;
808+ }
809+
810+ if ( event . fullDayEvent ) {
811+ if ( event . today || ( event . fullDayEvent && eventEndDateMoment . diff ( now , "days" ) === 0 ) ) {
812+ timeText = CalendarUtils . capFirst ( this . translate ( "TODAY" ) ) ;
813+ } else if ( event . dayBeforeYesterday ) {
814+ if ( this . translate ( "DAYBEFOREYESTERDAY" ) !== "DAYBEFOREYESTERDAY" ) {
815+ timeText = CalendarUtils . capFirst ( this . translate ( "DAYBEFOREYESTERDAY" ) ) ;
816+ }
817+ } else if ( event . yesterday ) {
818+ timeText = CalendarUtils . capFirst ( this . translate ( "YESTERDAY" ) ) ;
819+ } else if ( event . tomorrow ) {
820+ timeText = CalendarUtils . capFirst ( this . translate ( "TOMORROW" ) ) ;
821+ } else if ( event . dayAfterTomorrow ) {
822+ if ( this . translate ( "DAYAFTERTOMORROW" ) !== "DAYAFTERTOMORROW" ) {
823+ timeText = CalendarUtils . capFirst ( this . translate ( "DAYAFTERTOMORROW" ) ) ;
824+ }
825+ }
826+
827+ if ( this . config . showEnd && ! this . config . showEndsOnlyWithDuration ) {
828+ const adjustedEndMoment = this . getAdjustedFullDayEndMoment ( eventEndDateMoment ) ;
829+ if ( ! eventStartDateMoment . isSame ( adjustedEndMoment , "d" ) ) {
830+ timeText += `-${ CalendarUtils . capFirst ( adjustedEndMoment . format ( this . config . fullDayEventDateFormat ) ) } ` ;
831+ }
832+ }
833+
834+ Log . info ( "[calendar] event fullday" ) ;
835+ } else if ( eventStartDateMoment . diff ( now , "h" ) < this . config . getRelative ) {
836+ Log . info ( "[calendar] not full day but within getRelative size" ) ;
837+ timeText = `${ CalendarUtils . capFirst ( eventStartDateMoment . fromNow ( ) ) } ` ;
838+ } else if ( this . shouldShowRelativeTimedEnd ( event ) ) {
839+ if ( this . isSameDay ( eventStartDateMoment , eventEndDateMoment ) ) {
840+ const sameElseFormat = this . dateFormatIncludesTime ( ) ? this . config . dateFormat : `${ this . config . dateFormat } , LT` ;
841+ timeText = CalendarUtils . capFirst (
842+ eventStartDateMoment . calendar ( null , { sameElse : sameElseFormat } )
843+ ) ;
844+ }
845+ timeText += `-${ this . formatTimedEventEnd ( eventStartDateMoment , eventEndDateMoment ) } ` ;
846+ }
847+
848+ return timeText ;
849+ }
850+
851+ return CalendarUtils . capFirst (
852+ this . translate ( "RUNNING" , {
853+ fallback : `${ this . translate ( "RUNNING" ) } {timeUntilEnd}` ,
854+ timeUntilEnd : eventEndDateMoment . fromNow ( true )
855+ } )
856+ ) ;
857+ } ,
858+
859+ /**
860+ * Determines whether two moments are on the same day.
861+ * @param {moment.Moment } startMoment The start moment.
862+ * @param {moment.Moment } endMoment The end moment.
863+ * @returns {boolean } True when both moments share the same calendar day.
864+ */
865+ isSameDay ( startMoment , endMoment ) {
866+ return startMoment . isSame ( endMoment , "d" ) ;
867+ } ,
868+
869+ /**
870+ * Checks whether the configured dateFormat already contains time components.
871+ * @returns {boolean } True when dateFormat includes time tokens.
872+ */
873+ dateFormatIncludesTime ( ) {
874+ const dateFormatWithoutLiterals = this . config . dateFormat . replace ( / \[ [ ^ \] ] * \] / g, "" ) ;
875+ const localeDateFormat = moment . localeData ( ) ;
876+ const expandedDateFormat = dateFormatWithoutLiterals . replace (
877+ / L T S | L T | L L L L | L L L | L L | L | l l l l | l l l | l l | l / g,
878+ ( token ) => localeDateFormat . longDateFormat ( token ) || token
879+ ) ;
880+ const expandedDateFormatWithoutLiterals = expandedDateFormat . replace ( / \[ [ ^ \] ] * \] / g, "" ) ;
881+ return ( / ( H { 1 , 2 } | h { 1 , 2 } | k { 1 , 2 } | m { 1 , 2 } | s { 1 , 2 } | a | A ) / ) . test ( expandedDateFormatWithoutLiterals ) ;
882+ } ,
883+
884+ /**
885+ * Formats a timed event end value.
886+ * Uses time-only for same-day events and dateEndFormat for multi-day events.
887+ * @param {moment.Moment } startMoment The event start moment.
888+ * @param {moment.Moment } endMoment The event end moment.
889+ * @returns {string } The formatted end value.
890+ */
891+ formatTimedEventEnd ( startMoment , endMoment ) {
892+ const endFormat = this . isSameDay ( startMoment , endMoment ) ? "LT" : this . config . dateEndFormat ;
893+ return CalendarUtils . capFirst ( endMoment . format ( endFormat ) ) ;
894+ } ,
895+
796896 /**
797897 * Retrieves the symbolClass for a specific calendar url.
798898 * @param {string } url The calendar url
0 commit comments