MOBILE-3927 calendar: Add swipe to calendar monthly view

main
Dani Palou 2021-11-24 09:34:33 +01:00
parent 9bf939ab2f
commit 6674262bb7
5 changed files with 429 additions and 230 deletions

View File

@ -17,7 +17,10 @@
</ion-button> </ion-button>
</ion-col> </ion-col>
<ion-col class="ion-text-center addon-calendar-period"> <ion-col class="ion-text-center addon-calendar-period">
<h2 id="addon-calendar-monthname">{{ periodName }}</h2> <h2 id="addon-calendar-monthname">
{{ periodName }}
<ion-spinner *ngIf="!monthLoaded" class="addon-calendar-loading-month"></ion-spinner>
</h2>
</ion-col> </ion-col>
<ion-col class="ion-text-end" *ngIf="canNavigate"> <ion-col class="ion-text-end" *ngIf="canNavigate">
<ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'core.next' | translate"> <ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'core.next' | translate">
@ -27,74 +30,81 @@
</ion-row> </ion-row>
</ion-grid> </ion-grid>
<!-- Calendar view. --> <ion-slides *ngIf="loaded" (ionSlideWillChange)="slideChanged()" [options]="{initialSlide: 1}">
<ion-grid class="addon-calendar-months" role="table" aria-describedby="addon-calendar-monthname"> <ion-slide *ngFor="let month of preloadedMonths">
<div role="rowgroup"> <!-- Calendar view. -->
<!-- List of days. --> <ion-grid class="addon-calendar-months" role="table" aria-describedby="addon-calendar-monthname">
<ion-row role="row"> <div role="rowgroup">
<ion-col class="ion-text-center addon-calendar-weekday" *ngFor="let day of weekDays" role="columnheader"> <!-- List of days. -->
<span class="sr-only">{{ day.fullname | translate }}</span> <ion-row role="row">
<span class="ion-hide-md-up" aria-hidden="true">{{ day.shortname | translate }}</span> <ion-col class="ion-text-center addon-calendar-weekday" *ngFor="let day of month.weekDays" role="columnheader">
<span class="ion-hide-md-down" aria-hidden="true">{{ day.fullname | translate }}</span> <span class="sr-only">{{ day.fullname | translate }}</span>
</ion-col> <span class="ion-hide-md-up" aria-hidden="true">{{ day.shortname | translate }}</span>
</ion-row> <span class="ion-hide-md-down" aria-hidden="true">{{ day.fullname | translate }}</span>
</div> </ion-col>
<div role="rowgroup"> </ion-row>
</div>
<div role="rowgroup">
<!-- Weeks. -->
<ion-row *ngFor="let week of month.weeks" class="addon-calendar-week" role="row">
<!-- Empty slots (first week). -->
<ion-col *ngFor="let value of week.prepadding" class="dayblank addon-calendar-day" role="cell">
</ion-col>
<ion-col *ngFor="let day of week.days" class="addon-calendar-day ion-text-center" [ngClass]='{
"hasevents": day.hasevents,
"today": isCurrentMonth && day.istoday,
"weekend": day.isweekend,
"duration_finish": day.haslastdayofevent
}' [class.addon-calendar-event-past-day]="isPastMonth || day.ispast" role="cell" tabindex="0"
(ariaButtonClick)="dayClicked(day.mday)">
<p class="addon-calendar-day-number" role="button">
<span aria-hidden="true">{{ day.mday }}</span>
<span class="sr-only">{{ day.periodName | translate }}</span>
</p>
<!-- Weeks. --> <!-- In phone, display some dots to indicate the type of events. -->
<ion-row *ngFor="let week of weeks" class="addon-calendar-week" role="row"> <p class="ion-hide-md-up addon-calendar-dot-types"><span *ngFor="let type of day.calendareventtypes"
<!-- Empty slots (first week). --> class="calendar_event_type calendar_event_{{type}}"></span></p>
<ion-col *ngFor="let value of week.prepadding" class="dayblank addon-calendar-day" role="cell"></ion-col>
<ion-col *ngFor="let day of week.days" class="addon-calendar-day ion-text-center" [ngClass]='{
"hasevents": day.hasevents,
"today": isCurrentMonth && day.istoday,
"weekend": day.isweekend,
"duration_finish": day.haslastdayofevent
}' [class.addon-calendar-event-past-day]="isPastMonth || day.ispast" role="cell" tabindex="0"
(ariaButtonClick)="dayClicked(day.mday)">
<p class="addon-calendar-day-number" role="button">
<span aria-hidden="true">{{ day.mday }}</span>
<span class="sr-only">{{ day.periodName | translate }}</span>
</p>
<!-- In phone, display some dots to indicate the type of events. --> <!-- In tablet, display list of events. -->
<p class="ion-hide-md-up addon-calendar-dot-types"><span *ngFor="let type of day.calendareventtypes" <div class="ion-hide-md-down addon-calendar-day-events" *ngIf="day.filteredEvents">
class="calendar_event_type calendar_event_{{type}}"></span></p> <ng-container *ngFor="let event of day.filteredEvents | slice:0:4; let index = index">
<div *ngIf="index < 3 || day.filteredEvents.length == 4" class="addon-calendar-event"
<!-- In tablet, display list of events. --> [class.addon-calendar-event-past]="event.ispast" role="button" tabindex="0"
<div class="ion-hide-md-down addon-calendar-day-events"> (ariaButtonClick)="eventClicked(event, $event)">
<ng-container *ngFor="let event of day.filteredEvents | slice:0:4; let index = index"> <span class="calendar_event_type calendar_event_{{event.formattedType}}"></span>
<div *ngIf="index < 3 || day.filteredEvents.length == 4" class="addon-calendar-event" <ion-icon *ngIf="event.offline && !event.deleted" name="fas-clock"
[class.addon-calendar-event-past]="event.ispast" role="button" tabindex="0" [attr.aria-label]="'core.notsent' | translate"></ion-icon>
(ariaButtonClick)="eventClicked(event, $event)"> <ion-icon *ngIf="event.deleted" name="fas-trash"
<span class="calendar_event_type calendar_event_{{event.formattedType}}"></span> [attr.aria-label]="'core.deletedoffline' | translate"></ion-icon>
<ion-icon *ngIf="event.offline && !event.deleted" name="fas-clock" <span class="addon-calendar-event-time">
[attr.aria-label]="'core.notsent' | translate"></ion-icon> {{ event.timestart * 1000 | coreFormatDate: timeFormat }}
<ion-icon *ngIf="event.deleted" name="fas-trash" [attr.aria-label]="'core.deletedoffline' | translate"> </span>
</ion-icon> <core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" [showAlt]="false"
<span class="addon-calendar-event-time"> [modname]="event.modulename" [componentId]="event.instance">
{{ event.timestart * 1000 | coreFormatDate: timeFormat }} </core-mod-icon>
</span> <!-- Add the icon title so accessibility tools read it. -->
<core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" [showAlt]="false" <span class="sr-only">
[modname]="event.modulename" [componentId]="event.instance"> {{ 'addon.calendar.type' + event.formattedType | translate }}
</core-mod-icon> <span class="sr-only" *ngIf="event.moduleIcon && event.iconTitle">
<!-- Add the icon title so accessibility tools read it. --> {{ event.iconTitle }}
<span class="sr-only"> </span>
{{ 'addon.calendar.type' + event.formattedType | translate }} </span>
<span class="sr-only" *ngIf="event.moduleIcon && event.iconTitle">{{ event.iconTitle }}</span> <span class="addon-calendar-event-name">{{event.name}}</span>
</span> </div>
<span class="addon-calendar-event-name">{{event.name}}</span> </ng-container>
<p *ngIf="day.filteredEvents.length > 4" class="addon-calendar-day-more">
<b>{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}</b>
</p>
</div> </div>
</ng-container> </ion-col>
<p *ngIf="day.filteredEvents.length > 4" class="addon-calendar-day-more"> <!-- Empty slots (last week). -->
<b>{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}</b> <ion-col *ngFor="let value of week.postpadding" class="dayblank addon-calendar-day" role="cell">
</p> </ion-col>
</div> </ion-row>
</ion-col> </div>
<!-- Empty slots (last week). --> </ion-grid>
<ion-col *ngFor="let value of week.postpadding" class="dayblank addon-calendar-day" role="cell"></ion-col> </ion-slide>
</ion-row> </ion-slides>
</div>
</ion-grid>
</core-loading> </core-loading>

View File

@ -98,6 +98,10 @@
margin-top: 10px; margin-top: 10px;
font-size: 1.2rem; font-size: 1.2rem;
} }
.addon-calendar-loading-month {
height: 20px;
}
} }
.addon-calendar-weekday { .addon-calendar-weekday {
@ -154,6 +158,14 @@
display: block; display: block;
} }
} }
ion-slide {
display: block;
font-size: inherit;
justify-content: start;
align-items: start;
text-align: start;
}
} }
:host-context([dir=rtl]) { :host-context([dir=rtl]) {

View File

@ -22,11 +22,12 @@ import {
EventEmitter, EventEmitter,
KeyValueDiffers, KeyValueDiffers,
KeyValueDiffer, KeyValueDiffer,
ViewChild,
} from '@angular/core'; } from '@angular/core';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreTimeUtils } from '@services/utils/time'; import { CoreTimeUtils, YearAndMonth } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { import {
AddonCalendar, AddonCalendar,
@ -41,6 +42,7 @@ import { AddonCalendarOffline } from '../../services/calendar-offline';
import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses'; import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreLocalNotifications } from '@services/local-notifications';
import { IonSlides } from '@ionic/angular';
/** /**
* Component that displays a calendar. * Component that displays a calendar.
@ -52,6 +54,8 @@ import { CoreLocalNotifications } from '@services/local-notifications';
}) })
export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestroy { export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestroy {
@ViewChild(IonSlides) slides?: IonSlides;
@Input() initialYear?: number; // Initial year to load. @Input() initialYear?: number; // Initial year to load.
@Input() initialMonth?: number; // Initial month to load. @Input() initialMonth?: number; // Initial month to load.
@Input() filter?: AddonCalendarFilter; // Filter to apply. @Input() filter?: AddonCalendarFilter; // Filter to apply.
@ -61,15 +65,14 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
@Output() onDayClicked = new EventEmitter<{day: number; month: number; year: number}>(); @Output() onDayClicked = new EventEmitter<{day: number; month: number; year: number}>();
periodName?: string; periodName?: string;
weekDays: AddonCalendarWeekDaysTranslationKeys[] = []; preloadedMonths: PreloadedMonth[] = [];
weeks: AddonCalendarWeek[] = [];
loaded = false; loaded = false;
monthLoaded = false;
timeFormat?: string; timeFormat?: string;
isCurrentMonth = false; isCurrentMonth = false;
isPastMonth = false; isPastMonth = false;
protected year: number; protected visibleMonth: YearAndMonth;
protected month: number;
protected categoriesRetrieved = false; protected categoriesRetrieved = false;
protected categories: { [id: number]: CoreCategoryData } = {}; protected categories: { [id: number]: CoreCategoryData } = {};
protected currentSiteId: string; protected currentSiteId: string;
@ -92,9 +95,15 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
if (CoreLocalNotifications.isAvailable()) { if (CoreLocalNotifications.isAvailable()) {
// Re-schedule events if default time changes. // Re-schedule events if default time changes.
this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
this.weeks.forEach((week) => { this.preloadedMonths.forEach((month) => {
week.days.forEach((day) => { if (!month.loaded) {
AddonCalendar.scheduleEventsNotifications(day.eventsFormated || []); return;
}
month.weeks.forEach((week) => {
week.days.forEach((day) => {
AddonCalendar.scheduleEventsNotifications(day.eventsFormated || []);
});
}); });
}); });
}, this.currentSiteId); }, this.currentSiteId);
@ -124,16 +133,18 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
const now = new Date(); const now = new Date();
this.year = now.getFullYear(); this.visibleMonth = {
this.month = now.getMonth() + 1; year: now.getFullYear(),
monthNumber: now.getMonth() + 1,
};
} }
/** /**
* Component loaded. * Component loaded.
*/ */
ngOnInit(): void { ngOnInit(): void {
this.year = this.initialYear ? this.initialYear : this.year; this.visibleMonth.year = this.initialYear ?? this.visibleMonth.year;
this.month = this.initialMonth ? this.initialMonth : this.month; this.visibleMonth.monthNumber = this.initialMonth ?? this.visibleMonth.monthNumber;
this.calculateIsCurrentMonth(); this.calculateIsCurrentMonth();
@ -148,11 +159,15 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true : this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true :
CoreUtils.isTrueOrOne(this.displayNavButtons); CoreUtils.isTrueOrOne(this.displayNavButtons);
if (this.weeks) { if (this.preloadedMonths.length) {
// Check if there's any change in the filter object. // Check if there's any change in the filter object.
const changes = this.differ.diff(this.filter || {}); const changes = this.differ.diff(this.filter || {});
if (changes) { if (changes) {
this.filterEvents(); this.preloadedMonths.forEach((month) => {
if (month.loaded) {
this.filterEvents(month.weeks);
}
});
} }
} }
} }
@ -196,7 +211,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
try { try {
await Promise.all(promises); await Promise.all(promises);
await this.fetchEvents(); await this.viewMonth();
} catch (error) { } catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
@ -206,72 +221,162 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
} }
/** /**
* Fetch the events for current month. * Load or preload a month.
* *
* @param year Year.
* @param monthNumber Month number.
* @param preload Whether to "preload" the month. When preloading, no events will be fetched.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async fetchEvents(): Promise<void> { async loadMonth(month: YearAndMonth, preload = false): Promise<void> {
// Don't pass courseId and categoryId, we'll filter them locally. // Check if it's already loaded.
let result: { daynames: Partial<AddonCalendarDayName>[]; weeks: Partial<AddonCalendarWeek>[] }; const existingMonth = this.findPreloadedMonth(month);
try { if (existingMonth && ((existingMonth.loaded && !existingMonth.needsRefresh) || preload)) {
result = await AddonCalendar.getMonthlyEvents(this.year, this.month); return;
} catch (error) {
if (!CoreApp.isOnline()) {
// Allow navigating to non-cached months in offline (behave as if using emergency cache).
result = await AddonCalendarHelper.getOfflineMonthWeeks(this.year, this.month);
} else {
throw error;
}
} }
// Calculate the period name. We don't use the one in result because it's in server's language. if (!preload) {
this.periodName = CoreTimeUtils.userDate( this.monthLoaded = false;
new Date(this.year, this.month - 1).getTime(), }
'core.strftimemonthyear',
);
this.weekDays = AddonCalendar.getWeekDays(result.daynames[0].dayno);
this.weeks = result.weeks as AddonCalendarWeek[];
this.calculateIsCurrentMonth();
await Promise.all(this.weeks.map(async (week) => { try {
await Promise.all(week.days.map(async (day) => { // Load or preload the weeks.
day.periodName = CoreTimeUtils.userDate( let result: { daynames: Partial<AddonCalendarDayName>[]; weeks: Partial<AddonCalendarWeek>[] };
new Date(this.year, this.month - 1, day.mday).getTime(), if (preload) {
'core.strftimedaydate', result = await AddonCalendarHelper.getOfflineMonthWeeks(month.year, month.monthNumber);
); } else {
day.eventsFormated = day.eventsFormated || []; try {
day.filteredEvents = day.filteredEvents || []; // Don't pass courseId and categoryId, we'll filter them locally.
// Format online events. result = await AddonCalendar.getMonthlyEvents(month.year, month.monthNumber);
const onlineEventsFormatted = await Promise.all( } catch (error) {
day.events.map(async (event) => AddonCalendarHelper.formatEventData(event)), if (!CoreApp.isOnline()) {
); // Allow navigating to non-cached months in offline (behave as if using emergency cache).
result = await AddonCalendarHelper.getOfflineMonthWeeks(month.year, month.monthNumber);
} else {
throw error;
}
}
}
day.eventsFormated = day.eventsFormated.concat(onlineEventsFormatted); const weekDays = AddonCalendar.getWeekDays(result.daynames[0].dayno);
})); const weeks = result.weeks as AddonCalendarWeek[];
})); const isCurrentMonth = CoreTimeUtils.isCurrentMonth(month);
if (this.isCurrentMonth) {
const currentDay = new Date().getDate(); const currentDay = new Date().getDate();
let isPast = true; let isPast = true;
this.weeks.forEach((week) => { await Promise.all(weeks.map(async (week) => {
week.days.forEach((day) => { await Promise.all(week.days.map(async (day) => {
day.istoday = day.mday == currentDay; day.periodName = CoreTimeUtils.userDate(
day.ispast = isPast && !day.istoday; new Date(month.year, month.monthNumber - 1, day.mday).getTime(),
isPast = day.ispast; 'core.strftimedaydate',
);
day.eventsFormated = day.eventsFormated || [];
day.filteredEvents = day.filteredEvents || [];
// Format online events.
const onlineEventsFormatted = await Promise.all(
day.events.map(async (event) => AddonCalendarHelper.formatEventData(event)),
);
if (day.istoday) { day.eventsFormated = day.eventsFormated.concat(onlineEventsFormatted);
day.eventsFormated?.forEach((event) => {
event.ispast = this.isEventPast(event); if (isCurrentMonth) {
}); day.istoday = day.mday == currentDay;
day.ispast = isPast && !day.istoday;
isPast = day.ispast;
if (day.istoday) {
day.eventsFormated?.forEach((event) => {
event.ispast = this.isEventPast(event);
});
}
} }
}); }));
}));
if (!preload) {
// Merge the online events with offline data.
this.mergeEvents(month, weeks);
// Filter events by course.
this.filterEvents(weeks);
}
if (existingMonth) {
// Month already exists, update it.
existingMonth.loaded = !preload;
existingMonth.weeks = weeks;
existingMonth.weekDays = weekDays;
return;
}
// Add the preloaded month at the right position.
const preloadedMonth: PreloadedMonth = {
...month,
loaded: !preload,
weeks,
weekDays,
};
const previousMonth = CoreTimeUtils.getPreviousMonth(month);
const nextMonth = CoreTimeUtils.getNextMonth(month);
const activeIndex = await this.slides?.getActiveIndex();
const added = this.preloadedMonths.some((month, index) => {
let positionToInsert = -1;
if (CoreTimeUtils.isSameMonth(month, previousMonth)) {
// Previous month found, add the month after it.
positionToInsert = index + 1;
}
if (CoreTimeUtils.isSameMonth(month, nextMonth)) {
// Next month found, add the month before it.
positionToInsert = index;
}
if (positionToInsert > -1) {
this.preloadedMonths.splice(positionToInsert, 0, preloadedMonth);
if (activeIndex !== undefined && positionToInsert <= activeIndex) {
// Added a slide before the active one, keep current slide.
this.slides?.slideTo(activeIndex + 1, 0, false);
}
return true;
}
}); });
if (!added) {
// Previous and next months not found, this probably means the array is still empty. Add it at the end.
this.preloadedMonths.push(preloadedMonth);
}
} finally {
if (!preload) {
this.monthLoaded = true;
}
}
}
/**
* Load current month and preload next and previous ones.
*
* @return Promise resolved when done.
*/
async viewMonth(): Promise<void> {
// Calculate the period name. We don't use the one in result because it's in server's language.
this.periodName = CoreTimeUtils.userDate(
new Date(this.visibleMonth.year, this.visibleMonth.monthNumber - 1).getTime(),
'core.strftimemonthyear',
);
this.calculateIsCurrentMonth();
try {
// Load current month, and preload next and previous ones.
await Promise.all([
this.loadMonth(this.visibleMonth, false),
this.loadMonth(CoreTimeUtils.getPreviousMonth(this.visibleMonth), true),
this.loadMonth(CoreTimeUtils.getNextMonth(this.visibleMonth), true),
]);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
} }
// Merge the online events with offline data.
this.mergeEvents();
// Filter events by course.
this.filterEvents();
} }
/** /**
@ -301,9 +406,11 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
/** /**
* Filter events based on the filter popover. * Filter events based on the filter popover.
*
* @param weeks Weeks with the events to filter.
*/ */
filterEvents(): void { filterEvents(weeks: AddonCalendarWeek[]): void {
this.weeks.forEach((week) => { weeks.forEach((week) => {
week.days.forEach((day) => { week.days.forEach((day) => {
day.filteredEvents = AddonCalendarHelper.getFilteredEvents( day.filteredEvents = AddonCalendarHelper.getFilteredEvents(
day.eventsFormated || [], day.eventsFormated || [],
@ -323,18 +430,20 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
* @param afterChange Whether the refresh is done after an event has changed or has been synced. * @param afterChange Whether the refresh is done after an event has changed or has been synced.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async refreshData(afterChange?: boolean): Promise<void> { async refreshData(): Promise<void> {
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
// Don't invalidate monthly events after a change, it has already been handled. promises.push(AddonCalendar.invalidateMonthlyEvents(this.visibleMonth.year, this.visibleMonth.monthNumber));
if (!afterChange) {
promises.push(AddonCalendar.invalidateMonthlyEvents(this.year, this.month));
}
promises.push(CoreCourses.invalidateCategories(0, true)); promises.push(CoreCourses.invalidateCategories(0, true));
promises.push(AddonCalendar.invalidateTimeFormat()); promises.push(AddonCalendar.invalidateTimeFormat());
this.categoriesRetrieved = false; // Get categories again. this.categoriesRetrieved = false; // Get categories again.
const preloadedMonth = this.findPreloadedMonth(this.visibleMonth);
if (preloadedMonth) {
preloadedMonth.needsRefresh = true;
}
await Promise.all(promises); await Promise.all(promises);
this.fetchData(); this.fetchData();
@ -343,35 +452,15 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
/** /**
* Load next month. * Load next month.
*/ */
async loadNext(): Promise<void> { loadNext(): void {
this.increaseMonth(); this.slides?.slideNext();
this.loaded = false;
try {
await this.fetchEvents();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
this.decreaseMonth();
}
this.loaded = true;
} }
/** /**
* Load previous month. * Load previous month.
*/ */
async loadPrevious(): Promise<void> { loadPrevious(): void {
this.decreaseMonth(); this.slides?.slidePrev();
this.loaded = false;
try {
await this.fetchEvents();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
this.increaseMonth();
}
this.loaded = true;
} }
/** /**
@ -391,78 +480,46 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
* @param day Day. * @param day Day.
*/ */
dayClicked(day: number): void { dayClicked(day: number): void {
this.onDayClicked.emit({ day: day, month: this.month, year: this.year }); this.onDayClicked.emit({ day: day, month: this.visibleMonth.monthNumber, year: this.visibleMonth.year });
} }
/** /**
* Check if user is viewing the current month. * Check if user is viewing the current month.
*/ */
calculateIsCurrentMonth(): void { calculateIsCurrentMonth(): void {
const now = new Date();
this.currentTime = CoreTimeUtils.timestamp(); this.currentTime = CoreTimeUtils.timestamp();
this.isCurrentMonth = CoreTimeUtils.isCurrentMonth(this.visibleMonth);
this.isCurrentMonth = this.year == now.getFullYear() && this.month == now.getMonth() + 1; this.isPastMonth = CoreTimeUtils.isCurrentMonth(this.visibleMonth);
this.isPastMonth = this.year < now.getFullYear() || (this.year == now.getFullYear() && this.month < now.getMonth() + 1);
} }
/** /**
* Go to current month. * Go to current month.
*/ */
async goToCurrentMonth(): Promise<void> { async goToCurrentMonth(): Promise<void> {
const now = new Date(); const now = new Date();
const initialMonth = this.month; const currentMonth = {
const initialYear = this.year; monthNumber: now.getMonth() + 1,
year: now.getFullYear(),
};
this.month = now.getMonth() + 1; const index = this.preloadedMonths.findIndex((month) => CoreTimeUtils.isSameMonth(month, currentMonth));
this.year = now.getFullYear(); if (index > -1) {
this.slides?.slideTo(index);
this.loaded = false;
try {
await this.fetchEvents();
this.isCurrentMonth = true;
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
this.year = initialYear;
this.month = initialMonth;
}
this.loaded = true;
}
/**
* Decrease the current month.
*/
protected decreaseMonth(): void {
if (this.month === 1) {
this.month = 12;
this.year--;
} else {
this.month--;
}
}
/**
* Increase the current month.
*/
protected increaseMonth(): void {
if (this.month === 12) {
this.month = 1;
this.year++;
} else {
this.month++;
} }
} }
/** /**
* Merge online events with the offline events of that period. * Merge online events with the offline events of that period.
*
* @param month Month.
* @param weeks Weeks with the events to filter.
*/ */
protected mergeEvents(): void { protected mergeEvents(month: YearAndMonth, weeks: AddonCalendarWeek[]): void {
const monthOfflineEvents: { [day: number]: AddonCalendarEventToDisplay[] } = const monthOfflineEvents: { [day: number]: AddonCalendarEventToDisplay[] } =
this.offlineEvents[AddonCalendarHelper.getMonthId(this.year, this.month)]; this.offlineEvents[AddonCalendarHelper.getMonthId(month.year, month.monthNumber)];
this.weeks.forEach((week) => { weeks.forEach((week) => {
week.days.forEach((day) => { week.days.forEach((day) => {
// Schedule notifications for the events retrieved (only future events will be scheduled). // Schedule notifications for the events retrieved (only future events will be scheduled).
@ -498,18 +555,20 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
* *
* @param eventId Event ID. * @param eventId Event ID.
*/ */
protected undeleteEvent(eventId: number): void { protected async undeleteEvent(eventId: number): Promise<void> {
if (!this.weeks) { this.preloadedMonths.forEach((month) => {
return; if (!month.loaded) {
} return;
}
this.weeks.forEach((week) => { month.weeks.forEach((week) => {
week.days.forEach((day) => { week.days.forEach((day) => {
const event = day.eventsFormated?.find((event) => event.id == eventId); const event = day.eventsFormated?.find((event) => event.id == eventId);
if (event) { if (event) {
event.deleted = false; event.deleted = false;
} }
});
}); });
}); });
} }
@ -524,6 +583,38 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
return (event.timestart + event.timeduration) < (this.currentTime || CoreTimeUtils.timestamp()); return (event.timestart + event.timeduration) < (this.currentTime || CoreTimeUtils.timestamp());
} }
/**
* Slide has changed.
*
* @return Promise resolved when done.
*/
async slideChanged(): Promise<void> {
if (!this.slides) {
return;
}
const index = await this.slides.getActiveIndex();
const preloadedMonth = this.preloadedMonths[index];
if (!preloadedMonth) {
return;
}
this.visibleMonth.year = preloadedMonth.year;
this.visibleMonth.monthNumber = preloadedMonth.monthNumber;
await this.viewMonth();
}
/**
* Find a certain preloaded month.
*
* @param month Month to search.
* @return Preloaded month, undefined if not found.
*/
protected findPreloadedMonth(month: YearAndMonth): PreloadedMonth | undefined {
return this.preloadedMonths.find(preloadedMonth => CoreTimeUtils.isSameMonth(month, preloadedMonth));
}
/** /**
* Component destroyed. * Component destroyed.
*/ */
@ -533,3 +624,13 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
} }
} }
/**
* Preloaded month.
*/
type PreloadedMonth = YearAndMonth & {
loaded: boolean; // Whether the events have been loaded.
weekDays: AddonCalendarWeekDaysTranslationKeys[];
weeks: AddonCalendarWeek[];
needsRefresh?: boolean; // Whether the events needs to be re-loaded.
};

View File

@ -227,16 +227,12 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On
/** /**
* Refresh events. * Refresh events.
* *
* @param afterChange Whether the refresh is done after an event has changed or has been synced.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async refreshData(afterChange?: boolean): Promise<void> { async refreshData(): Promise<void> {
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
// Don't invalidate upcoming events after a change, it has already been handled. promises.push(AddonCalendar.invalidateAllUpcomingEvents());
if (!afterChange) {
promises.push(AddonCalendar.invalidateAllUpcomingEvents());
}
promises.push(CoreCourses.invalidateCategories(0, true)); promises.push(CoreCourses.invalidateCategories(0, true));
promises.push(AddonCalendar.invalidateLookAhead()); promises.push(AddonCalendar.invalidateLookAhead());
promises.push(AddonCalendar.invalidateTimeFormat()); promises.push(AddonCalendar.invalidateTimeFormat());

View File

@ -389,6 +389,86 @@ export class CoreTimeUtilsProvider {
return String(moment().year() - 20); return String(moment().year() - 20);
} }
/**
* Given a year and a month, return the previous month and its year.
*
* @param month Year and month.
* @return Previous month and its year.
*/
getPreviousMonth(month: YearAndMonth): YearAndMonth {
if (month.monthNumber === 1) {
return {
monthNumber: 12,
year: month.year - 1,
};
}
return {
monthNumber: month.monthNumber - 1,
year: month.year,
};
}
/**
* Given a year and a month, return the next month and its year.
*
* @param month Year and month.
* @return Next month and its year.
*/
getNextMonth(month: YearAndMonth): YearAndMonth {
if (month.monthNumber === 12) {
return {
monthNumber: 1,
year: month.year + 1,
};
}
return {
monthNumber: month.monthNumber + 1,
year: month.year,
};
}
/**
* Check if a certain month is current month.
*
* @param month Year and month.
* @return Whether it's current month.
*/
isCurrentMonth(month: YearAndMonth): boolean {
const now = new Date();
return month.year == now.getFullYear() && month.monthNumber == now.getMonth() + 1;
}
/**
* Check if a certain month is a past month.
*
* @param month Year and month.
* @return Whether it's a past month.
*/
isPastMonth(month: YearAndMonth): boolean {
const now = new Date();
return month.year < now.getFullYear() || (month.year == now.getFullYear() && month.monthNumber < now.getMonth() + 1);
}
/**
* Check if two months are the same.
*
* @param monthA Month A.
* @param monthB Month B.
* @return Whether it's same month.
*/
isSameMonth(monthA: YearAndMonth, monthB: YearAndMonth): boolean {
return monthA.monthNumber === monthB.monthNumber && monthA.year === monthB.year;
}
} }
export const CoreTimeUtils = makeSingleton(CoreTimeUtilsProvider); export const CoreTimeUtils = makeSingleton(CoreTimeUtilsProvider);
export type YearAndMonth = {
year: number; // Year number.
monthNumber: number; // Month number (1 to 12).
};