MOBILE-3927 calendar: Add swipe to calendar monthly view
parent
9bf939ab2f
commit
6674262bb7
|
@ -17,7 +17,10 @@
|
|||
</ion-button>
|
||||
</ion-col>
|
||||
<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 class="ion-text-end" *ngIf="canNavigate">
|
||||
<ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'core.next' | translate">
|
||||
|
@ -27,74 +30,81 @@
|
|||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
<!-- Calendar view. -->
|
||||
<ion-grid class="addon-calendar-months" role="table" aria-describedby="addon-calendar-monthname">
|
||||
<div role="rowgroup">
|
||||
<!-- List of days. -->
|
||||
<ion-row role="row">
|
||||
<ion-col class="ion-text-center addon-calendar-weekday" *ngFor="let day of weekDays" role="columnheader">
|
||||
<span class="sr-only">{{ day.fullname | translate }}</span>
|
||||
<span class="ion-hide-md-up" aria-hidden="true">{{ day.shortname | translate }}</span>
|
||||
<span class="ion-hide-md-down" aria-hidden="true">{{ day.fullname | translate }}</span>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</div>
|
||||
<div role="rowgroup">
|
||||
<ion-slides *ngIf="loaded" (ionSlideWillChange)="slideChanged()" [options]="{initialSlide: 1}">
|
||||
<ion-slide *ngFor="let month of preloadedMonths">
|
||||
<!-- Calendar view. -->
|
||||
<ion-grid class="addon-calendar-months" role="table" aria-describedby="addon-calendar-monthname">
|
||||
<div role="rowgroup">
|
||||
<!-- List of days. -->
|
||||
<ion-row role="row">
|
||||
<ion-col class="ion-text-center addon-calendar-weekday" *ngFor="let day of month.weekDays" role="columnheader">
|
||||
<span class="sr-only">{{ day.fullname | translate }}</span>
|
||||
<span class="ion-hide-md-up" aria-hidden="true">{{ day.shortname | translate }}</span>
|
||||
<span class="ion-hide-md-down" aria-hidden="true">{{ day.fullname | translate }}</span>
|
||||
</ion-col>
|
||||
</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. -->
|
||||
<ion-row *ngFor="let week of 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>
|
||||
<!-- In phone, display some dots to indicate the type of events. -->
|
||||
<p class="ion-hide-md-up addon-calendar-dot-types"><span *ngFor="let type of day.calendareventtypes"
|
||||
class="calendar_event_type calendar_event_{{type}}"></span></p>
|
||||
|
||||
<!-- In phone, display some dots to indicate the type of events. -->
|
||||
<p class="ion-hide-md-up addon-calendar-dot-types"><span *ngFor="let type of day.calendareventtypes"
|
||||
class="calendar_event_type calendar_event_{{type}}"></span></p>
|
||||
|
||||
<!-- In tablet, display list of events. -->
|
||||
<div class="ion-hide-md-down addon-calendar-day-events">
|
||||
<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"
|
||||
[class.addon-calendar-event-past]="event.ispast" role="button" tabindex="0"
|
||||
(ariaButtonClick)="eventClicked(event, $event)">
|
||||
<span class="calendar_event_type calendar_event_{{event.formattedType}}"></span>
|
||||
<ion-icon *ngIf="event.offline && !event.deleted" name="fas-clock"
|
||||
[attr.aria-label]="'core.notsent' | translate"></ion-icon>
|
||||
<ion-icon *ngIf="event.deleted" name="fas-trash" [attr.aria-label]="'core.deletedoffline' | translate">
|
||||
</ion-icon>
|
||||
<span class="addon-calendar-event-time">
|
||||
{{ event.timestart * 1000 | coreFormatDate: timeFormat }}
|
||||
</span>
|
||||
<core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" [showAlt]="false"
|
||||
[modname]="event.modulename" [componentId]="event.instance">
|
||||
</core-mod-icon>
|
||||
<!-- Add the icon title so accessibility tools read it. -->
|
||||
<span class="sr-only">
|
||||
{{ 'addon.calendar.type' + event.formattedType | translate }}
|
||||
<span class="sr-only" *ngIf="event.moduleIcon && event.iconTitle">{{ event.iconTitle }}</span>
|
||||
</span>
|
||||
<span class="addon-calendar-event-name">{{event.name}}</span>
|
||||
<!-- In tablet, display list of events. -->
|
||||
<div class="ion-hide-md-down addon-calendar-day-events" *ngIf="day.filteredEvents">
|
||||
<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"
|
||||
[class.addon-calendar-event-past]="event.ispast" role="button" tabindex="0"
|
||||
(ariaButtonClick)="eventClicked(event, $event)">
|
||||
<span class="calendar_event_type calendar_event_{{event.formattedType}}"></span>
|
||||
<ion-icon *ngIf="event.offline && !event.deleted" name="fas-clock"
|
||||
[attr.aria-label]="'core.notsent' | translate"></ion-icon>
|
||||
<ion-icon *ngIf="event.deleted" name="fas-trash"
|
||||
[attr.aria-label]="'core.deletedoffline' | translate"></ion-icon>
|
||||
<span class="addon-calendar-event-time">
|
||||
{{ event.timestart * 1000 | coreFormatDate: timeFormat }}
|
||||
</span>
|
||||
<core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" [showAlt]="false"
|
||||
[modname]="event.modulename" [componentId]="event.instance">
|
||||
</core-mod-icon>
|
||||
<!-- Add the icon title so accessibility tools read it. -->
|
||||
<span class="sr-only">
|
||||
{{ 'addon.calendar.type' + event.formattedType | translate }}
|
||||
<span class="sr-only" *ngIf="event.moduleIcon && event.iconTitle">
|
||||
{{ event.iconTitle }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="addon-calendar-event-name">{{event.name}}</span>
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
</ion-col>
|
||||
<!-- Empty slots (last week). -->
|
||||
<ion-col *ngFor="let value of week.postpadding" class="dayblank addon-calendar-day" role="cell"></ion-col>
|
||||
</ion-row>
|
||||
</div>
|
||||
</ion-grid>
|
||||
</ion-col>
|
||||
<!-- Empty slots (last week). -->
|
||||
<ion-col *ngFor="let value of week.postpadding" class="dayblank addon-calendar-day" role="cell">
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</div>
|
||||
</ion-grid>
|
||||
</ion-slide>
|
||||
</ion-slides>
|
||||
|
||||
</core-loading>
|
||||
|
|
|
@ -98,6 +98,10 @@
|
|||
margin-top: 10px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.addon-calendar-loading-month {
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.addon-calendar-weekday {
|
||||
|
@ -154,6 +158,14 @@
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
ion-slide {
|
||||
display: block;
|
||||
font-size: inherit;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
text-align: start;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context([dir=rtl]) {
|
||||
|
|
|
@ -22,11 +22,12 @@ import {
|
|||
EventEmitter,
|
||||
KeyValueDiffers,
|
||||
KeyValueDiffer,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
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 {
|
||||
AddonCalendar,
|
||||
|
@ -41,6 +42,7 @@ import { AddonCalendarOffline } from '../../services/calendar-offline';
|
|||
import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreLocalNotifications } from '@services/local-notifications';
|
||||
import { IonSlides } from '@ionic/angular';
|
||||
|
||||
/**
|
||||
* Component that displays a calendar.
|
||||
|
@ -52,6 +54,8 @@ import { CoreLocalNotifications } from '@services/local-notifications';
|
|||
})
|
||||
export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestroy {
|
||||
|
||||
@ViewChild(IonSlides) slides?: IonSlides;
|
||||
|
||||
@Input() initialYear?: number; // Initial year to load.
|
||||
@Input() initialMonth?: number; // Initial month to load.
|
||||
@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}>();
|
||||
|
||||
periodName?: string;
|
||||
weekDays: AddonCalendarWeekDaysTranslationKeys[] = [];
|
||||
weeks: AddonCalendarWeek[] = [];
|
||||
preloadedMonths: PreloadedMonth[] = [];
|
||||
loaded = false;
|
||||
monthLoaded = false;
|
||||
timeFormat?: string;
|
||||
isCurrentMonth = false;
|
||||
isPastMonth = false;
|
||||
|
||||
protected year: number;
|
||||
protected month: number;
|
||||
protected visibleMonth: YearAndMonth;
|
||||
protected categoriesRetrieved = false;
|
||||
protected categories: { [id: number]: CoreCategoryData } = {};
|
||||
protected currentSiteId: string;
|
||||
|
@ -92,9 +95,15 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
if (CoreLocalNotifications.isAvailable()) {
|
||||
// Re-schedule events if default time changes.
|
||||
this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
|
||||
this.weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
AddonCalendar.scheduleEventsNotifications(day.eventsFormated || []);
|
||||
this.preloadedMonths.forEach((month) => {
|
||||
if (!month.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
month.weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
AddonCalendar.scheduleEventsNotifications(day.eventsFormated || []);
|
||||
});
|
||||
});
|
||||
});
|
||||
}, this.currentSiteId);
|
||||
|
@ -124,16 +133,18 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
|
||||
const now = new Date();
|
||||
|
||||
this.year = now.getFullYear();
|
||||
this.month = now.getMonth() + 1;
|
||||
this.visibleMonth = {
|
||||
year: now.getFullYear(),
|
||||
monthNumber: now.getMonth() + 1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Component loaded.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.year = this.initialYear ? this.initialYear : this.year;
|
||||
this.month = this.initialMonth ? this.initialMonth : this.month;
|
||||
this.visibleMonth.year = this.initialYear ?? this.visibleMonth.year;
|
||||
this.visibleMonth.monthNumber = this.initialMonth ?? this.visibleMonth.monthNumber;
|
||||
|
||||
this.calculateIsCurrentMonth();
|
||||
|
||||
|
@ -148,11 +159,15 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true :
|
||||
CoreUtils.isTrueOrOne(this.displayNavButtons);
|
||||
|
||||
if (this.weeks) {
|
||||
if (this.preloadedMonths.length) {
|
||||
// Check if there's any change in the filter object.
|
||||
const changes = this.differ.diff(this.filter || {});
|
||||
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 {
|
||||
await Promise.all(promises);
|
||||
|
||||
await this.fetchEvents();
|
||||
await this.viewMonth();
|
||||
|
||||
} catch (error) {
|
||||
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.
|
||||
*/
|
||||
async fetchEvents(): Promise<void> {
|
||||
// Don't pass courseId and categoryId, we'll filter them locally.
|
||||
let result: { daynames: Partial<AddonCalendarDayName>[]; weeks: Partial<AddonCalendarWeek>[] };
|
||||
try {
|
||||
result = await AddonCalendar.getMonthlyEvents(this.year, this.month);
|
||||
} 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;
|
||||
}
|
||||
async loadMonth(month: YearAndMonth, preload = false): Promise<void> {
|
||||
// Check if it's already loaded.
|
||||
const existingMonth = this.findPreloadedMonth(month);
|
||||
if (existingMonth && ((existingMonth.loaded && !existingMonth.needsRefresh) || preload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.year, this.month - 1).getTime(),
|
||||
'core.strftimemonthyear',
|
||||
);
|
||||
this.weekDays = AddonCalendar.getWeekDays(result.daynames[0].dayno);
|
||||
this.weeks = result.weeks as AddonCalendarWeek[];
|
||||
this.calculateIsCurrentMonth();
|
||||
if (!preload) {
|
||||
this.monthLoaded = false;
|
||||
}
|
||||
|
||||
await Promise.all(this.weeks.map(async (week) => {
|
||||
await Promise.all(week.days.map(async (day) => {
|
||||
day.periodName = CoreTimeUtils.userDate(
|
||||
new Date(this.year, this.month - 1, day.mday).getTime(),
|
||||
'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)),
|
||||
);
|
||||
try {
|
||||
// Load or preload the weeks.
|
||||
let result: { daynames: Partial<AddonCalendarDayName>[]; weeks: Partial<AddonCalendarWeek>[] };
|
||||
if (preload) {
|
||||
result = await AddonCalendarHelper.getOfflineMonthWeeks(month.year, month.monthNumber);
|
||||
} else {
|
||||
try {
|
||||
// Don't pass courseId and categoryId, we'll filter them locally.
|
||||
result = await AddonCalendar.getMonthlyEvents(month.year, month.monthNumber);
|
||||
} catch (error) {
|
||||
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);
|
||||
}));
|
||||
}));
|
||||
|
||||
if (this.isCurrentMonth) {
|
||||
const weekDays = AddonCalendar.getWeekDays(result.daynames[0].dayno);
|
||||
const weeks = result.weeks as AddonCalendarWeek[];
|
||||
const isCurrentMonth = CoreTimeUtils.isCurrentMonth(month);
|
||||
const currentDay = new Date().getDate();
|
||||
let isPast = true;
|
||||
|
||||
this.weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
day.istoday = day.mday == currentDay;
|
||||
day.ispast = isPast && !day.istoday;
|
||||
isPast = day.ispast;
|
||||
await Promise.all(weeks.map(async (week) => {
|
||||
await Promise.all(week.days.map(async (day) => {
|
||||
day.periodName = CoreTimeUtils.userDate(
|
||||
new Date(month.year, month.monthNumber - 1, day.mday).getTime(),
|
||||
'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?.forEach((event) => {
|
||||
event.ispast = this.isEventPast(event);
|
||||
});
|
||||
day.eventsFormated = day.eventsFormated.concat(onlineEventsFormatted);
|
||||
|
||||
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.
|
||||
*
|
||||
* @param weeks Weeks with the events to filter.
|
||||
*/
|
||||
filterEvents(): void {
|
||||
this.weeks.forEach((week) => {
|
||||
filterEvents(weeks: AddonCalendarWeek[]): void {
|
||||
weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
day.filteredEvents = AddonCalendarHelper.getFilteredEvents(
|
||||
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.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async refreshData(afterChange?: boolean): Promise<void> {
|
||||
async refreshData(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// Don't invalidate monthly events after a change, it has already been handled.
|
||||
if (!afterChange) {
|
||||
promises.push(AddonCalendar.invalidateMonthlyEvents(this.year, this.month));
|
||||
}
|
||||
promises.push(AddonCalendar.invalidateMonthlyEvents(this.visibleMonth.year, this.visibleMonth.monthNumber));
|
||||
promises.push(CoreCourses.invalidateCategories(0, true));
|
||||
promises.push(AddonCalendar.invalidateTimeFormat());
|
||||
|
||||
this.categoriesRetrieved = false; // Get categories again.
|
||||
|
||||
const preloadedMonth = this.findPreloadedMonth(this.visibleMonth);
|
||||
if (preloadedMonth) {
|
||||
preloadedMonth.needsRefresh = true;
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
this.fetchData();
|
||||
|
@ -343,35 +452,15 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
/**
|
||||
* Load next month.
|
||||
*/
|
||||
async loadNext(): Promise<void> {
|
||||
this.increaseMonth();
|
||||
|
||||
this.loaded = false;
|
||||
|
||||
try {
|
||||
await this.fetchEvents();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
||||
this.decreaseMonth();
|
||||
}
|
||||
this.loaded = true;
|
||||
loadNext(): void {
|
||||
this.slides?.slideNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load previous month.
|
||||
*/
|
||||
async loadPrevious(): Promise<void> {
|
||||
this.decreaseMonth();
|
||||
|
||||
this.loaded = false;
|
||||
|
||||
try {
|
||||
await this.fetchEvents();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
||||
this.increaseMonth();
|
||||
}
|
||||
this.loaded = true;
|
||||
loadPrevious(): void {
|
||||
this.slides?.slidePrev();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -391,78 +480,46 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
* @param day Day.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
calculateIsCurrentMonth(): void {
|
||||
const now = new Date();
|
||||
|
||||
this.currentTime = CoreTimeUtils.timestamp();
|
||||
|
||||
this.isCurrentMonth = this.year == now.getFullYear() && this.month == now.getMonth() + 1;
|
||||
this.isPastMonth = this.year < now.getFullYear() || (this.year == now.getFullYear() && this.month < now.getMonth() + 1);
|
||||
this.isCurrentMonth = CoreTimeUtils.isCurrentMonth(this.visibleMonth);
|
||||
this.isPastMonth = CoreTimeUtils.isCurrentMonth(this.visibleMonth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to current month.
|
||||
*/
|
||||
async goToCurrentMonth(): Promise<void> {
|
||||
|
||||
const now = new Date();
|
||||
const initialMonth = this.month;
|
||||
const initialYear = this.year;
|
||||
const currentMonth = {
|
||||
monthNumber: now.getMonth() + 1,
|
||||
year: now.getFullYear(),
|
||||
};
|
||||
|
||||
this.month = now.getMonth() + 1;
|
||||
this.year = now.getFullYear();
|
||||
|
||||
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++;
|
||||
const index = this.preloadedMonths.findIndex((month) => CoreTimeUtils.isSameMonth(month, currentMonth));
|
||||
if (index > -1) {
|
||||
this.slides?.slideTo(index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[] } =
|
||||
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) => {
|
||||
|
||||
// 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.
|
||||
*/
|
||||
protected undeleteEvent(eventId: number): void {
|
||||
if (!this.weeks) {
|
||||
return;
|
||||
}
|
||||
protected async undeleteEvent(eventId: number): Promise<void> {
|
||||
this.preloadedMonths.forEach((month) => {
|
||||
if (!month.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
const event = day.eventsFormated?.find((event) => event.id == eventId);
|
||||
month.weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
const event = day.eventsFormated?.find((event) => event.id == eventId);
|
||||
|
||||
if (event) {
|
||||
event.deleted = false;
|
||||
}
|
||||
if (event) {
|
||||
event.deleted = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -524,6 +583,38 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
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.
|
||||
*/
|
||||
|
@ -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.
|
||||
};
|
||||
|
|
|
@ -227,16 +227,12 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On
|
|||
/**
|
||||
* Refresh events.
|
||||
*
|
||||
* @param afterChange Whether the refresh is done after an event has changed or has been synced.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async refreshData(afterChange?: boolean): Promise<void> {
|
||||
async refreshData(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// Don't invalidate upcoming events after a change, it has already been handled.
|
||||
if (!afterChange) {
|
||||
promises.push(AddonCalendar.invalidateAllUpcomingEvents());
|
||||
}
|
||||
promises.push(AddonCalendar.invalidateAllUpcomingEvents());
|
||||
promises.push(CoreCourses.invalidateCategories(0, true));
|
||||
promises.push(AddonCalendar.invalidateLookAhead());
|
||||
promises.push(AddonCalendar.invalidateTimeFormat());
|
||||
|
|
|
@ -389,6 +389,86 @@ export class CoreTimeUtilsProvider {
|
|||
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 type YearAndMonth = {
|
||||
year: number; // Year number.
|
||||
monthNumber: number; // Month number (1 to 12).
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue