MOBILE-3927 swipe: Apply IR changes

main
Dani Palou 2021-12-16 12:29:13 +01:00
parent d527695977
commit 372c5920a7
26 changed files with 591 additions and 676 deletions

View File

@ -23,7 +23,7 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses'; import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-source'; import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-source';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
@ -43,7 +43,7 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
user?: CoreUserProfile; user?: CoreUserProfile;
course?: CoreEnrolledCourseData; course?: CoreEnrolledCourseData;
badge?: AddonBadgesUserBadge; badge?: AddonBadgesUserBadge;
badges?: CoreSwipeItemsManager; badges?: CoreSwipeNavigationItemsManager;
badgeLoaded = false; badgeLoaded = false;
currentTime = 0; currentTime = 0;
@ -65,7 +65,7 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
AddonBadgesUserBadgesSource, AddonBadgesUserBadgesSource,
[this.courseId, this.userId], [this.courseId, this.userId],
); );
this.badges = new CoreSwipeItemsManager(source); this.badges = new CoreSwipeNavigationItemsManager(source);
this.badges.start(); this.badges.start();
} }

View File

@ -1,7 +1,7 @@
<!-- Add buttons to the nav bar. --> <!-- Add buttons to the nav bar. -->
<core-navbar-buttons slot="end" prepend> <core-navbar-buttons slot="end" prepend>
<core-context-menu> <core-context-menu>
<core-context-menu-item *ngIf="canNavigate && !isCurrentMonth && displayNavButtons" [priority]="900" <core-context-menu-item *ngIf="canNavigate && !selectedMonthIsCurrent() && displayNavButtons" [priority]="900"
[content]="'addon.calendar.currentmonth' | translate" iconAction="fas-calendar-day" (action)="goToCurrentMonth()"> [content]="'addon.calendar.currentmonth' | translate" iconAction="fas-calendar-day" (action)="goToCurrentMonth()">
</core-context-menu-item> </core-context-menu-item>
</core-context-menu> </core-context-menu>
@ -19,7 +19,7 @@
<ion-col class="ion-text-center addon-calendar-period"> <ion-col class="ion-text-center addon-calendar-period">
<h2 id="addon-calendar-monthname"> <h2 id="addon-calendar-monthname">
{{ periodName }} {{ periodName }}
<ion-spinner *ngIf="manager && !manager.getSource().monthLoaded" class="addon-calendar-loading-month"> <ion-spinner *ngIf="!selectedMonthLoaded()" class="addon-calendar-loading-month">
</ion-spinner> </ion-spinner>
</h2> </h2>
</ion-col> </ion-col>
@ -31,14 +31,14 @@
</ion-row> </ion-row>
</ion-grid> </ion-grid>
<core-swipe-slides [manager]="manager" (onWillChange)="slideChanged($event)"> <core-swipe-slides [manager]="manager">
<ng-template let-item="item"> <ng-template let-month="item">
<!-- Calendar view. --> <!-- Calendar view. -->
<ion-grid class="addon-calendar-months" role="table" aria-describedby="addon-calendar-monthname"> <ion-grid class="addon-calendar-months" role="table" aria-describedby="addon-calendar-monthname">
<div role="rowgroup"> <div role="rowgroup">
<!-- List of days. --> <!-- List of days. -->
<ion-row role="row"> <ion-row role="row">
<ion-col class="ion-text-center addon-calendar-weekday" *ngFor="let day of item.weekDays" role="columnheader"> <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="sr-only">{{ day.fullname | translate }}</span>
<span class="ion-hide-md-up" aria-hidden="true">{{ day.shortname | 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> <span class="ion-hide-md-down" aria-hidden="true">{{ day.fullname | translate }}</span>
@ -47,16 +47,16 @@
</div> </div>
<div role="rowgroup"> <div role="rowgroup">
<!-- Weeks. --> <!-- Weeks. -->
<ion-row *ngFor="let week of item.weeks" class="addon-calendar-week" role="row"> <ion-row *ngFor="let week of month.weeks" class="addon-calendar-week" role="row">
<!-- Empty slots (first week). --> <!-- Empty slots (first week). -->
<ion-col *ngFor="let value of week.prepadding" class="dayblank addon-calendar-day" role="cell"> <ion-col *ngFor="let value of week.prepadding" class="dayblank addon-calendar-day" role="cell">
</ion-col> </ion-col>
<ion-col *ngFor="let day of week.days" class="addon-calendar-day ion-text-center" [ngClass]='{ <ion-col *ngFor="let day of week.days" class="addon-calendar-day ion-text-center" [ngClass]='{
"hasevents": day.hasevents, "hasevents": day.hasevents,
"today": isCurrentMonth && day.istoday, "today": month.isCurrentMonth && day.istoday,
"weekend": day.isweekend, "weekend": day.isweekend,
"duration_finish": day.haslastdayofevent "duration_finish": day.haslastdayofevent
}' [class.addon-calendar-event-past-day]="isPastMonth || day.ispast" role="cell" tabindex="0" }' [class.addon-calendar-event-past-day]="month.isPastMonth || day.ispast" role="cell" tabindex="0"
(ariaButtonClick)="dayClicked(day.mday)"> (ariaButtonClick)="dayClicked(day.mday)">
<p class="addon-calendar-day-number" role="button"> <p class="addon-calendar-day-number" role="button">
<span aria-hidden="true">{{ day.mday }}</span> <span aria-hidden="true">{{ day.mday }}</span>

View File

@ -27,7 +27,7 @@ import {
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, YearAndMonth } from '@services/utils/time'; import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { import {
AddonCalendar, AddonCalendar,
@ -42,12 +42,13 @@ 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 { CoreSwipeCurrentItemData, CoreSwipeSlidesComponent } from '@components/swipe-slides/swipe-slides'; import { CoreSwipeSlidesComponent } from '@components/swipe-slides/swipe-slides';
import { import {
CoreSwipeSlidesDynamicItem, CoreSwipeSlidesDynamicItem,
CoreSwipeSlidesDynamicItemsManagerSource, CoreSwipeSlidesDynamicItemsManagerSource,
} from '@classes/items-management/slides-dynamic-items-manager-source'; } from '@classes/items-management/swipe-slides-dynamic-items-manager-source';
import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/slides-dynamic-items-manager'; import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/swipe-slides-dynamic-items-manager';
import moment from 'moment';
/** /**
* Component that displays a calendar. * Component that displays a calendar.
@ -72,15 +73,13 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
periodName?: string; periodName?: string;
manager?: CoreSwipeSlidesDynamicItemsManager<PreloadedMonth, AddonCalendarMonthSlidesItemsManagerSource>; manager?: CoreSwipeSlidesDynamicItemsManager<PreloadedMonth, AddonCalendarMonthSlidesItemsManagerSource>;
loaded = false; loaded = false;
isCurrentMonth = false;
isPastMonth = false;
protected currentSiteId: string; protected currentSiteId: string;
protected currentTime?: number;
protected differ: KeyValueDiffer<unknown, unknown>; // To detect changes in the data input. protected differ: KeyValueDiffer<unknown, unknown>; // To detect changes in the data input.
// Observers. // Observers and listeners.
protected undeleteEventObserver: CoreEventObserver; protected undeleteEventObserver: CoreEventObserver;
protected obsDefaultTimeChange?: CoreEventObserver; protected obsDefaultTimeChange?: CoreEventObserver;
protected managerUnsubscribe?: () => void;
constructor( constructor(
differs: KeyValueDiffers, differs: KeyValueDiffers,
@ -95,7 +94,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
return; return;
} }
month.weeks.forEach((week) => { month.weeks?.forEach((week) => {
week.days.forEach((day) => { week.days.forEach((day) => {
AddonCalendar.scheduleEventsNotifications(day.eventsFormated || []); AddonCalendar.scheduleEventsNotifications(day.eventsFormated || []);
}); });
@ -131,19 +130,20 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
* Component loaded. * Component loaded.
*/ */
ngOnInit(): void { ngOnInit(): void {
const currentMonth = CoreTimeUtils.getCurrentMonth();
this.canNavigate = typeof this.canNavigate == 'undefined' ? true : CoreUtils.isTrueOrOne(this.canNavigate); this.canNavigate = typeof this.canNavigate == 'undefined' ? true : CoreUtils.isTrueOrOne(this.canNavigate);
this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true : this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true :
CoreUtils.isTrueOrOne(this.displayNavButtons); CoreUtils.isTrueOrOne(this.displayNavButtons);
const source = new AddonCalendarMonthSlidesItemsManagerSource(this, { const source = new AddonCalendarMonthSlidesItemsManagerSource(this, moment({
year: this.initialYear ?? currentMonth.year, year: this.initialYear,
monthNumber: this.initialMonth ?? currentMonth.monthNumber, month: this.initialMonth ? this.initialMonth - 1 : undefined,
}); }));
this.manager = new CoreSwipeSlidesDynamicItemsManager(source); this.manager = new CoreSwipeSlidesDynamicItemsManager(source);
this.managerUnsubscribe = this.manager.addListener({
this.calculateIsCurrentMonth(); onSelectedItemUpdated: (item) => {
this.onMonthViewed(item);
},
});
this.fetchData(); this.fetchData();
} }
@ -159,7 +159,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
const changes = this.differ.diff(this.filter || {}); const changes = this.differ.diff(this.filter || {});
if (changes) { if (changes) {
items.forEach((month) => { items.forEach((month) => {
if (month.loaded) { if (month.loaded && month.weeks) {
this.manager?.getSource().filterEvents(month.weeks, this.filter); this.manager?.getSource().filterEvents(month.weeks, this.filter);
} }
}); });
@ -190,14 +190,15 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
/** /**
* Update data related to month being viewed. * Update data related to month being viewed.
*
* @param month Month being viewed.
*/ */
viewMonth(month: YearAndMonth): void { onMonthViewed(month: MonthBasicData): void {
// Calculate the period name. We don't use the one in result because it's in server's language. // Calculate the period name. We don't use the one in result because it's in server's language.
this.periodName = CoreTimeUtils.userDate( this.periodName = CoreTimeUtils.userDate(
new Date(month.year, month.monthNumber - 1).getTime(), month.moment.unix() * 1000,
'core.strftimemonthyear', 'core.strftimemonthyear',
); );
this.calculateIsCurrentMonth();
} }
/** /**
@ -207,13 +208,13 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async refreshData(afterChange = false): Promise<void> { async refreshData(afterChange = false): Promise<void> {
const visibleMonth = this.slides?.getCurrentItem() || null; const selectedMonth = this.manager?.getSelectedItem() || null;
if (afterChange) { if (afterChange) {
this.manager?.getSource().markAllItemsDirty(); this.manager?.getSource().markAllItemsDirty();
} }
await this.manager?.getSource().invalidateContent(visibleMonth); await this.manager?.getSource().invalidateContent(selectedMonth);
await this.fetchData(); await this.fetchData();
} }
@ -249,39 +250,53 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
* @param day Day. * @param day Day.
*/ */
dayClicked(day: number): void { dayClicked(day: number): void {
const visibleMonth = this.slides?.getCurrentItem(); const selectedMonth = this.manager?.getSelectedItem();
if (!visibleMonth) { if (!selectedMonth) {
return; return;
} }
this.onDayClicked.emit({ day: day, month: visibleMonth.monthNumber, year: visibleMonth.year }); this.onDayClicked.emit({ day: day, month: selectedMonth.moment.month() + 1, year: selectedMonth.moment.year() });
}
/**
* Check if user is viewing the current month.
*/
calculateIsCurrentMonth(): void {
const visibleMonth = this.slides?.getCurrentItem();
if (!visibleMonth) {
return;
}
this.currentTime = CoreTimeUtils.timestamp();
this.isCurrentMonth = CoreTimeUtils.isSameMonth(visibleMonth, CoreTimeUtils.getCurrentMonth());
this.isPastMonth = CoreTimeUtils.isPastMonth(visibleMonth);
} }
/** /**
* Go to current month. * Go to current month.
*/ */
async goToCurrentMonth(): Promise<void> { async goToCurrentMonth(): Promise<void> {
const now = new Date(); const manager = this.manager;
const currentMonth = { const slides = this.slides;
monthNumber: now.getMonth() + 1, if (!manager || !slides) {
year: now.getFullYear(), return;
}; }
this.slides?.slideToItem(currentMonth); const currentMonth = {
moment: moment(),
};
this.loaded = false;
try {
// Make sure the day is loaded.
await manager.getSource().loadItem(currentMonth);
slides.slideToItem(currentMonth);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
} finally {
this.loaded = true;
}
}
/**
* Check whether selected month is loaded.
*/
selectedMonthLoaded(): boolean {
return !!this.manager?.getSelectedItem()?.loaded;
}
/**
* Check whether selected month is current month.
*/
selectedMonthIsCurrent(): boolean {
return !!this.manager?.getSelectedItem()?.isCurrentMonth;
} }
/** /**
@ -295,7 +310,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
return false; return false;
} }
return month.weeks.some((week) => week.days.some((day) => { return month.weeks?.some((week) => week.days.some((day) => {
const event = day.eventsFormated?.find((event) => event.id == eventId); const event = day.eventsFormated?.find((event) => event.id == eventId);
if (event) { if (event) {
@ -309,41 +324,32 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
}); });
} }
/**
* Returns if the event is in the past or not.
*
* @param event Event object.
* @return True if it's in the past.
*/
isEventPast(event: { timestart: number; timeduration: number}): boolean {
return (event.timestart + event.timeduration) < (this.currentTime || CoreTimeUtils.timestamp());
}
/**
* Slide has changed.
*
* @param data Data about new item.
*/
slideChanged(data: CoreSwipeCurrentItemData<PreloadedMonth>): void {
this.viewMonth(data.item);
}
/** /**
* Component destroyed. * Component destroyed.
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.undeleteEventObserver?.off(); this.undeleteEventObserver?.off();
this.obsDefaultTimeChange?.off(); this.obsDefaultTimeChange?.off();
this.managerUnsubscribe && this.managerUnsubscribe();
} }
} }
/**
* Basic data to identify a month.
*/
type MonthBasicData = {
moment: moment.Moment;
};
/** /**
* Preloaded month. * Preloaded month.
*/ */
type PreloadedMonth = YearAndMonth & CoreSwipeSlidesDynamicItem & { type PreloadedMonth = MonthBasicData & CoreSwipeSlidesDynamicItem & {
weekDays: AddonCalendarWeekDaysTranslationKeys[]; weekDays?: AddonCalendarWeekDaysTranslationKeys[];
weeks: AddonCalendarWeek[]; weeks?: AddonCalendarWeek[];
isCurrentMonth?: boolean;
isPastMonth?: boolean;
}; };
/** /**
@ -351,8 +357,7 @@ type PreloadedMonth = YearAndMonth & CoreSwipeSlidesDynamicItem & {
*/ */
class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicItemsManagerSource<PreloadedMonth> { class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicItemsManagerSource<PreloadedMonth> {
monthLoaded = false; categories?: { [id: number]: CoreCategoryData };
categories: { [id: number]: CoreCategoryData } = {};
// Offline events classified in month & day. // Offline events classified in month & day.
offlineEvents: { [monthId: string]: { [day: number]: AddonCalendarEventToDisplay[] } } = {}; offlineEvents: { [monthId: string]: { [day: number]: AddonCalendarEventToDisplay[] } } = {};
offlineEditedEventsIds: number[] = []; // IDs of events edited in offline. offlineEditedEventsIds: number[] = []; // IDs of events edited in offline.
@ -360,10 +365,9 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI
timeFormat?: string; timeFormat?: string;
protected calendarComponent: AddonCalendarCalendarComponent; protected calendarComponent: AddonCalendarCalendarComponent;
protected categoriesRetrieved = false;
constructor(component: AddonCalendarCalendarComponent, initialMonth: YearAndMonth) { constructor(component: AddonCalendarCalendarComponent, initialMoment: moment.Moment) {
super(initialMonth); super({ moment: initialMoment });
this.calendarComponent = component; this.calendarComponent = component;
} }
@ -394,7 +398,7 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI
day.filteredEvents = AddonCalendarHelper.getFilteredEvents( day.filteredEvents = AddonCalendarHelper.getFilteredEvents(
day.eventsFormated || [], day.eventsFormated || [],
filter, filter,
this.categories, this.categories || {},
); );
// Re-calculate some properties. // Re-calculate some properties.
@ -409,20 +413,16 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async loadCategories(): Promise<void> { async loadCategories(): Promise<void> {
if (this.categoriesRetrieved) { if (this.categories) {
// Already retrieved, stop. // Already retrieved, stop.
return; return;
} }
try { try {
const cats = await CoreCourses.getCategories(0, true); const categories = await CoreCourses.getCategories(0, true);
this.categoriesRetrieved = true;
this.categories = {};
// Index categories by ID. // Index categories by ID.
cats.forEach((category) => { this.categories = CoreUtils.arrayToObject(categories, 'id');
this.categories[category.id] = category;
});
} catch { } catch {
// Ignore errors. // Ignore errors.
} }
@ -465,45 +465,47 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI
/** /**
* @inheritdoc * @inheritdoc
*/ */
getItemId(item: YearAndMonth): string | number { getItemId(item: MonthBasicData): string | number {
return AddonCalendarHelper.getMonthId(item); return AddonCalendarHelper.getMonthId(item.moment);
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
getPreviousItem(item: YearAndMonth): YearAndMonth | null { getPreviousItem(item: MonthBasicData): MonthBasicData | null {
return CoreTimeUtils.getPreviousMonth(item); return {
moment: item.moment.clone().subtract(1, 'month'),
};
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
getNextItem(item: YearAndMonth): YearAndMonth | null { getNextItem(item: MonthBasicData): MonthBasicData | null {
return CoreTimeUtils.getNextMonth(item); return {
moment: item.moment.clone().add(1, 'month'),
};
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
async loadItemData(month: YearAndMonth, preload = false): Promise<PreloadedMonth | null> { async loadItemData(month: MonthBasicData, preload = false): Promise<PreloadedMonth | null> {
if (!preload) {
this.monthLoaded = false;
}
try {
// Load or preload the weeks. // Load or preload the weeks.
let result: { daynames: Partial<AddonCalendarDayName>[]; weeks: Partial<AddonCalendarWeek>[] }; let result: { daynames: Partial<AddonCalendarDayName>[]; weeks: Partial<AddonCalendarWeek>[] };
const year = month.moment.year();
const monthNumber = month.moment.month() + 1;
if (preload) { if (preload) {
result = await AddonCalendarHelper.getOfflineMonthWeeks(month.year, month.monthNumber); result = await AddonCalendarHelper.getOfflineMonthWeeks(year, monthNumber);
} else { } else {
try { try {
// Don't pass courseId and categoryId, we'll filter them locally. // Don't pass courseId and categoryId, we'll filter them locally.
result = await AddonCalendar.getMonthlyEvents(month.year, month.monthNumber); result = await AddonCalendar.getMonthlyEvents(year, monthNumber);
} catch (error) { } catch (error) {
if (!CoreApp.isOnline()) { if (!CoreApp.isOnline()) {
// Allow navigating to non-cached months in offline (behave as if using emergency cache). // Allow navigating to non-cached months in offline (behave as if using emergency cache).
result = await AddonCalendarHelper.getOfflineMonthWeeks(month.year, month.monthNumber); result = await AddonCalendarHelper.getOfflineMonthWeeks(year, monthNumber);
} else { } else {
throw error; throw error;
} }
@ -512,14 +514,22 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI
const weekDays = AddonCalendar.getWeekDays(result.daynames[0].dayno); const weekDays = AddonCalendar.getWeekDays(result.daynames[0].dayno);
const weeks = result.weeks as AddonCalendarWeek[]; const weeks = result.weeks as AddonCalendarWeek[];
const isCurrentMonth = CoreTimeUtils.isSameMonth(month, CoreTimeUtils.getCurrentMonth());
const currentDay = new Date().getDate(); const currentDay = new Date().getDate();
const currentTime = CoreTimeUtils.timestamp();
let isPast = true; let isPast = true;
const preloadedMonth: PreloadedMonth = {
...month,
weeks,
weekDays,
isCurrentMonth: month.moment.isSame(moment(), 'month'),
isPastMonth: month.moment.isBefore(moment(), 'month'),
};
await Promise.all(weeks.map(async (week) => { await Promise.all(weeks.map(async (week) => {
await Promise.all(week.days.map(async (day) => { await Promise.all(week.days.map(async (day) => {
day.periodName = CoreTimeUtils.userDate( day.periodName = CoreTimeUtils.userDate(
new Date(month.year, month.monthNumber - 1, day.mday).getTime(), month.moment.unix() * 1000,
'core.strftimedaydate', 'core.strftimedaydate',
); );
day.eventsFormated = day.eventsFormated || []; day.eventsFormated = day.eventsFormated || [];
@ -531,14 +541,14 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI
day.eventsFormated = day.eventsFormated.concat(onlineEventsFormatted); day.eventsFormated = day.eventsFormated.concat(onlineEventsFormatted);
if (isCurrentMonth) { if (preloadedMonth.isCurrentMonth) {
day.istoday = day.mday == currentDay; day.istoday = day.mday == currentDay;
day.ispast = isPast && !day.istoday; day.ispast = isPast && !day.istoday;
isPast = day.ispast; isPast = day.ispast;
if (day.istoday) { if (day.istoday) {
day.eventsFormated?.forEach((event) => { day.eventsFormated?.forEach((event) => {
event.ispast = this.calendarComponent.isEventPast(event); event.ispast = this.isEventPast(event, currentTime);
}); });
} }
} }
@ -552,16 +562,7 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI
this.filterEvents(weeks, this.calendarComponent.filter); this.filterEvents(weeks, this.calendarComponent.filter);
} }
return { return preloadedMonth;
...month,
weeks,
weekDays,
};
} finally {
if (!preload) {
this.monthLoaded = true;
}
}
} }
/** /**
@ -570,9 +571,9 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI
* @param month Month. * @param month Month.
* @param weeks Weeks with the events to filter. * @param weeks Weeks with the events to filter.
*/ */
mergeEvents(month: YearAndMonth, weeks: AddonCalendarWeek[]): void { mergeEvents(month: MonthBasicData, weeks: AddonCalendarWeek[]): void {
const monthOfflineEvents: { [day: number]: AddonCalendarEventToDisplay[] } = const monthOfflineEvents: { [day: number]: AddonCalendarEventToDisplay[] } =
this.offlineEvents[AddonCalendarHelper.getMonthId(month)]; this.offlineEvents[AddonCalendarHelper.getMonthId(month.moment)];
weeks.forEach((week) => { weeks.forEach((week) => {
week.days.forEach((day) => { week.days.forEach((day) => {
@ -605,25 +606,36 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI
}); });
} }
/**
* Returns if the event is in the past or not.
*
* @param event Event object.
* @param currentTime Current time.
* @return True if it's in the past.
*/
isEventPast(event: { timestart: number; timeduration: number}, currentTime: number): boolean {
return (event.timestart + event.timeduration) < currentTime;
}
/** /**
* Invalidate content. * Invalidate content.
* *
* @param visibleMonth The current visible month. * @param selectedMonth The current selected month.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async invalidateContent(visibleMonth: PreloadedMonth | null): Promise<void> { async invalidateContent(selectedMonth: PreloadedMonth | null): Promise<void> {
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
if (visibleMonth) { if (selectedMonth) {
promises.push(AddonCalendar.invalidateMonthlyEvents(visibleMonth.year, visibleMonth.monthNumber)); promises.push(AddonCalendar.invalidateMonthlyEvents(selectedMonth.moment.year(), selectedMonth.moment.month() + 1));
} }
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.categories = undefined; // Get categories again.
if (visibleMonth) { if (selectedMonth) {
visibleMonth.dirty = true; selectedMonth.dirty = true;
} }
await Promise.all(promises); await Promise.all(promises);

View File

@ -11,10 +11,10 @@
<ion-icon slot="icon-only" name="fas-filter" aria-hidden="true"></ion-icon> <ion-icon slot="icon-only" name="fas-filter" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
<core-context-menu> <core-context-menu>
<core-context-menu-item *ngIf="!isCurrentDay" [priority]="900" [content]="'addon.calendar.today' | translate" <core-context-menu-item *ngIf="!selectedDayIsCurrent()" [priority]="900" [content]="'addon.calendar.today' | translate"
iconAction="fas-calendar-day" (action)="goToCurrentDay()"> iconAction="fas-calendar-day" (action)="goToCurrentDay()">
</core-context-menu-item> </core-context-menu-item>
<core-context-menu-item [hidden]="!loaded || !this.visibleDayHasOffline() || !isOnline" [priority]="400" <core-context-menu-item [hidden]="!loaded || !selectedDayHasOffline() || !isOnline" [priority]="400"
[content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event)" [iconAction]="syncIcon" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event)" [iconAction]="syncIcon"
[closeOnClick]="false"> [closeOnClick]="false">
</core-context-menu-item> </core-context-menu-item>
@ -47,23 +47,23 @@
</ion-row> </ion-row>
</ion-grid> </ion-grid>
<core-swipe-slides [manager]="manager" (onWillChange)="slideChanged($event)"> <core-swipe-slides [manager]="manager">
<ng-template let-item="item"> <ng-template let-day="item">
<core-loading [hideUntil]="item.loaded" class="safe-area-padding"> <core-loading [hideUntil]="day.loaded" class="safe-area-padding">
<!-- There is data to be synchronized --> <!-- There is data to be synchronized -->
<ion-card class="core-warning-card" *ngIf="item.hasOffline"> <ion-card class="core-warning-card" *ngIf="day.hasOffline">
<ion-item> <ion-item>
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon> <ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }}</ion-label> <ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }}</ion-label>
</ion-item> </ion-item>
</ion-card> </ion-card>
<core-empty-box *ngIf="!item.filteredEvents || !item.filteredEvents.length" icon="fas-calendar" inline="true" <core-empty-box *ngIf="!day.filteredEvents || !day.filteredEvents.length" icon="fas-calendar" inline="true"
[message]="'addon.calendar.noevents' | translate"> [message]="'addon.calendar.noevents' | translate">
</core-empty-box> </core-empty-box>
<ion-list *ngIf="item.filteredEvents && item.filteredEvents.length" class="ion-no-margin"> <ion-list *ngIf="day.filteredEvents && day.filteredEvents.length" class="ion-no-margin">
<ng-container *ngFor="let event of item.filteredEvents"> <ng-container *ngFor="let event of day.filteredEvents">
<ion-item class="addon-calendar-event ion-text-wrap" [attr.aria-label]="event.name" <ion-item class="addon-calendar-event ion-text-wrap" [attr.aria-label]="event.name"
(click)="gotoEvent(event.id)" [class.item-dimmed]="event.ispast" (click)="gotoEvent(event.id)" [class.item-dimmed]="event.ispast"
[ngClass]="['addon-calendar-eventtype-'+event.eventtype]" button detail="true"> [ngClass]="['addon-calendar-eventtype-'+event.eventtype]" button detail="true">

View File

@ -19,7 +19,7 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreLocalNotifications } from '@services/local-notifications';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreTimeUtils, YearMonthDay } from '@services/utils/time'; import { CoreTimeUtils } from '@services/utils/time';
import { import {
AddonCalendarProvider, AddonCalendarProvider,
AddonCalendar, AddonCalendar,
@ -40,12 +40,12 @@ import { Params } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/slides-dynamic-items-manager'; import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/swipe-slides-dynamic-items-manager';
import { CoreSwipeCurrentItemData, CoreSwipeSlidesComponent } from '@components/swipe-slides/swipe-slides'; import { CoreSwipeSlidesComponent } from '@components/swipe-slides/swipe-slides';
import { import {
CoreSwipeSlidesDynamicItem, CoreSwipeSlidesDynamicItem,
CoreSwipeSlidesDynamicItemsManagerSource, CoreSwipeSlidesDynamicItemsManagerSource,
} from '@classes/items-management/slides-dynamic-items-manager-source'; } from '@classes/items-management/swipe-slides-dynamic-items-manager-source';
/** /**
* Page that displays the calendar events for a certain day. * Page that displays the calendar events for a certain day.
@ -72,13 +72,13 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
protected onlineObserver: Subscription; protected onlineObserver: Subscription;
protected obsDefaultTimeChange?: CoreEventObserver; protected obsDefaultTimeChange?: CoreEventObserver;
protected filterChangedObserver: CoreEventObserver; protected filterChangedObserver: CoreEventObserver;
protected managerUnsubscribe?: () => void;
periodName?: string; periodName?: string;
manager?: CoreSwipeSlidesDynamicItemsManager<PreloadedDay, AddonCalendarDaySlidesItemsManagerSource>; manager?: CoreSwipeSlidesDynamicItemsManager<PreloadedDay, AddonCalendarDaySlidesItemsManagerSource>;
loaded = false; loaded = false;
isOnline = false; isOnline = false;
syncIcon = CoreConstants.ICON_LOADING; syncIcon = CoreConstants.ICON_LOADING;
isCurrentDay = false;
filter: AddonCalendarFilter = { filter: AddonCalendarFilter = {
filtered: false, filtered: false,
courseId: undefined, courseId: undefined,
@ -97,7 +97,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
// 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.manager?.getSource().getItems()?.forEach(day => { this.manager?.getSource().getItems()?.forEach(day => {
AddonCalendar.scheduleEventsNotifications(day.onlineEvents); AddonCalendar.scheduleEventsNotifications(day.onlineEvents || []);
}); });
}, this.currentSiteId); }, this.currentSiteId);
} }
@ -140,9 +140,8 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
// Refresh data if calendar events are synchronized manually but not by this page. // Refresh data if calendar events are synchronized manually but not by this page.
this.manualSyncObserver = CoreEvents.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => { this.manualSyncObserver = CoreEvents.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => {
const visibleDay = this.slides?.getCurrentItem(); const selectedDay = this.manager?.getSelectedItem();
if (data && (data.source != 'day' || !visibleDay || data.day === undefined || data.year != visibleDay.year || if (data && (data.source != 'day' || !selectedDay || !data.moment || !selectedDay.moment.isSame(data.moment, 'day'))) {
data.month != visibleDay.monthNumber || data.day != visibleDay.dayNumber)) {
this.manager?.getSource().markAllItemsUnloaded(); this.manager?.getSource().markAllItemsUnloaded();
this.refreshData(false, true); this.refreshData(false, true);
} }
@ -214,15 +213,18 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
this.filter.filtered = typeof this.filter.courseId != 'undefined' || types.some((name) => !this.filter[name]); this.filter.filtered = typeof this.filter.courseId != 'undefined' || types.some((name) => !this.filter[name]);
const currentDay = CoreTimeUtils.getCurrentDay(); const month = CoreNavigator.getRouteNumberParam('month');
const source = new AddonCalendarDaySlidesItemsManagerSource(this, { const source = new AddonCalendarDaySlidesItemsManagerSource(this, moment({
year: CoreNavigator.getRouteNumberParam('year') ?? currentDay.year, year: CoreNavigator.getRouteNumberParam('year'),
monthNumber: CoreNavigator.getRouteNumberParam('month') ?? currentDay.monthNumber, month: month ? month - 1 : undefined,
dayNumber: CoreNavigator.getRouteNumberParam('day') ?? currentDay.dayNumber, date: CoreNavigator.getRouteNumberParam('day'),
}); }));
this.manager = new CoreSwipeSlidesDynamicItemsManager(source); this.manager = new CoreSwipeSlidesDynamicItemsManager(source);
this.managerUnsubscribe = this.manager.addListener({
this.calculateIsCurrentDay(); onSelectedItemUpdated: (item) => {
this.onDayViewed(item);
},
});
this.fetchData(true); this.fetchData(true);
} }
@ -264,13 +266,14 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
/** /**
* Update data related to day being viewed. * Update data related to day being viewed.
*
* @param day Day viewed.
*/ */
viewDay(day: YearMonthDay): void { onDayViewed(day: DayBasicData): void {
this.periodName = CoreTimeUtils.userDate( this.periodName = CoreTimeUtils.userDate(
new Date(day.year, day.monthNumber - 1, day.dayNumber).getTime(), day.moment.unix() * 1000,
'core.strftimedaydate', 'core.strftimedaydate',
); );
this.calculateIsCurrentDay();
} }
/** /**
@ -301,10 +304,10 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
async refreshData(sync?: boolean, afterChange?: boolean): Promise<void> { async refreshData(sync?: boolean, afterChange?: boolean): Promise<void> {
this.syncIcon = CoreConstants.ICON_LOADING; this.syncIcon = CoreConstants.ICON_LOADING;
const visibleDay = this.slides?.getCurrentItem() || null; const selectedDay = this.manager?.getSelectedItem() || null;
// Don't invalidate day events after a change, it has already been handled. // Don't invalidate day events after a change, it has already been handled.
await this.manager?.getSource().invalidateContent(visibleDay, !afterChange); await this.manager?.getSource().invalidateContent(selectedDay, !afterChange);
await this.fetchData(sync); await this.fetchData(sync);
} }
@ -325,14 +328,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
if (result.updated) { if (result.updated) {
// Trigger a manual sync event. // Trigger a manual sync event.
const visibleDay = this.slides?.getCurrentItem(); const selectedDay = this.manager?.getSelectedItem();
result.source = 'day'; result.source = 'day';
result.moment = selectedDay?.moment;
if (visibleDay) {
result.day = visibleDay.dayNumber;
result.month = visibleDay.monthNumber;
result.year = visibleDay.year;
}
this.manager?.getSource().markAllItemsUnloaded(); this.manager?.getSource().markAllItemsUnloaded();
CoreEvents.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId); CoreEvents.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId);
@ -344,6 +342,13 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
} }
} }
/**
* Check whether selected day is current day.
*/
selectedDayIsCurrent(): boolean {
return !!this.manager?.getSelectedItem()?.isCurrentDay;
}
/** /**
* Navigate to a particular event. * Navigate to a particular event.
* *
@ -381,10 +386,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
// It's a new event, set the time. // It's a new event, set the time.
eventId = 0; eventId = 0;
const visibleDay = this.slides?.getCurrentItem(); const selectedDay = this.manager?.getSelectedItem();
if (visibleDay) { if (selectedDay) {
params.timestamp = moment().year(visibleDay.year).month(visibleDay.monthNumber - 1).date(visibleDay.dayNumber) params.timestamp = selectedDay.moment.unix() * 1000;
.unix() * 1000;
} }
} }
@ -396,26 +400,14 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
} }
/** /**
* Check if user is viewing the current day. * Check whether selected day has offline data.
*/
calculateIsCurrentDay(): void {
const visibleDay = this.slides?.getCurrentItem();
if (!visibleDay) {
return;
}
this.isCurrentDay = visibleDay.isCurrentDay;
}
/**
* Check whether visible day has offline data.
* *
* @return Whether visible day has offline data. * @return Whether selected day has offline data.
*/ */
visibleDayHasOffline(): boolean { selectedDayHasOffline(): boolean {
const visibleDay = this.slides?.getCurrentItem(); const selectedDay = this.manager?.getSelectedItem();
return !!visibleDay && visibleDay.hasOffline; return !!(selectedDay?.hasOffline);
} }
/** /**
@ -428,7 +420,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
return; return;
} }
const currentDay = CoreTimeUtils.getCurrentDay(); const currentDay = {
moment: moment(),
};
this.loaded = false; this.loaded = false;
try { try {
@ -436,7 +430,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
await manager.getSource().loadItem(currentDay); await manager.getSource().loadItem(currentDay);
slides.slideToItem(currentDay); slides.slideToItem(currentDay);
this.isCurrentDay = true;
} catch (error) { } catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
} finally { } finally {
@ -458,15 +451,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
this.slides?.slidePrev(); this.slides?.slidePrev();
} }
/**
* Slide has changed.
*
* @param data Data about new item.
*/
slideChanged(data: CoreSwipeCurrentItemData<PreloadedDay>): void {
this.viewDay(data.item);
}
/** /**
* Page destroyed. * Page destroyed.
*/ */
@ -481,20 +465,28 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
this.onlineObserver?.unsubscribe(); this.onlineObserver?.unsubscribe();
this.filterChangedObserver?.off(); this.filterChangedObserver?.off();
this.obsDefaultTimeChange?.off(); this.obsDefaultTimeChange?.off();
this.managerUnsubscribe && this.managerUnsubscribe();
} }
} }
/**
* Basic data to identify a day.
*/
type DayBasicData = {
moment: moment.Moment;
};
/** /**
* Preloaded month. * Preloaded month.
*/ */
type PreloadedDay = YearMonthDay & CoreSwipeSlidesDynamicItem & { type PreloadedDay = DayBasicData & CoreSwipeSlidesDynamicItem & {
events: AddonCalendarEventToDisplay[]; // Events (both online and offline). events?: AddonCalendarEventToDisplay[]; // Events (both online and offline).
onlineEvents: AddonCalendarEventToDisplay[]; onlineEvents?: AddonCalendarEventToDisplay[];
filteredEvents: AddonCalendarEventToDisplay[]; filteredEvents?: AddonCalendarEventToDisplay[];
isCurrentDay: boolean; isCurrentDay?: boolean;
isPastDay: boolean; isPastDay?: boolean;
hasOffline: boolean; // Whether the day has offline data. hasOffline?: boolean; // Whether the day has offline data.
}; };
/** /**
@ -506,16 +498,15 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
// Offline events classified in month & day. // Offline events classified in month & day.
offlineEvents: Record<string, Record<number, AddonCalendarEventToDisplay[]>> = {}; offlineEvents: Record<string, Record<number, AddonCalendarEventToDisplay[]>> = {};
offlineEditedEventsIds: number[] = []; // IDs of events edited in offline. offlineEditedEventsIds: number[] = []; // IDs of events edited in offline.
categories: { [id: number]: CoreCategoryData } = {}; categories?: { [id: number]: CoreCategoryData };
deletedEvents: number[] = []; // Events deleted in offline. deletedEvents?: Set<number>; // Events deleted in offline.
timeFormat?: string; timeFormat?: string;
canCreate = false; canCreate = false;
protected dayPage: AddonCalendarDayPage; protected dayPage: AddonCalendarDayPage;
protected categoriesRetrieved = false;
constructor(page: AddonCalendarDayPage, initialDay: YearMonthDay) { constructor(page: AddonCalendarDayPage, initialMoment: moment.Moment) {
super(initialDay); super({ moment: initialMoment });
this.dayPage = page; this.dayPage = page;
} }
@ -553,7 +544,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
* @param filter Filter to apply. * @param filter Filter to apply.
*/ */
filterEvents(day: PreloadedDay, filter: AddonCalendarFilter): void { filterEvents(day: PreloadedDay, filter: AddonCalendarFilter): void {
day.filteredEvents = AddonCalendarHelper.getFilteredEvents(day.events, filter, this.categories); day.filteredEvents = AddonCalendarHelper.getFilteredEvents(day.events || [], filter, this.categories || {});
} }
/** /**
@ -584,20 +575,16 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async loadCategories(): Promise<void> { async loadCategories(): Promise<void> {
if (this.categoriesRetrieved) { if (this.categories) {
// Already retrieved, stop. // Already retrieved, stop.
return; return;
} }
try { try {
const cats = await CoreCourses.getCategories(0, true); const categories = await CoreCourses.getCategories(0, true);
this.categoriesRetrieved = true;
this.categories = {};
// Index categories by ID. // Index categories by ID.
cats.forEach((category) => { this.categories = CoreUtils.arrayToObject(categories, 'id');
this.categories[category.id] = category;
});
} catch { } catch {
// Ignore errors. // Ignore errors.
} }
@ -625,7 +612,9 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async loadOfflineDeletedEvents(): Promise<void> { async loadOfflineDeletedEvents(): Promise<void> {
this.deletedEvents = await AddonCalendarOffline.getAllDeletedEventsIds(); const deletedEventsIds = await AddonCalendarOffline.getAllDeletedEventsIds();
this.deletedEvents = new Set(deletedEventsIds);
} }
/** /**
@ -640,36 +629,40 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
/** /**
* @inheritdoc * @inheritdoc
*/ */
getItemId(item: YearMonthDay): string | number { getItemId(item: DayBasicData): string | number {
return AddonCalendarHelper.getDayId(item); return AddonCalendarHelper.getDayId(item.moment);
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
getPreviousItem(item: YearMonthDay): YearMonthDay | null { getPreviousItem(item: DayBasicData): DayBasicData | null {
return CoreTimeUtils.getPreviousDay(item); return {
moment: item.moment.clone().subtract(1, 'day'),
};
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
getNextItem(item: YearMonthDay): YearMonthDay | null { getNextItem(item: DayBasicData): DayBasicData | null {
return CoreTimeUtils.getNextDay(item); return {
moment: item.moment.clone().add(1, 'day'),
};
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
async loadItemData(day: YearMonthDay, preload = false): Promise<PreloadedDay | null> { async loadItemData(day: DayBasicData, preload = false): Promise<PreloadedDay | null> {
const preloadedDay: PreloadedDay = { const preloadedDay: PreloadedDay = {
...day, ...day,
hasOffline: false, hasOffline: false,
events: [], events: [],
onlineEvents: [], onlineEvents: [],
filteredEvents: [], filteredEvents: [],
isCurrentDay: CoreTimeUtils.isSameDay(day, CoreTimeUtils.getCurrentDay()), isCurrentDay: day.moment.isSame(moment(), 'day'),
isPastDay: CoreTimeUtils.isPastDay(day), isPastDay: day.moment.isBefore(moment(), 'day'),
}; };
if (preload) { if (preload) {
@ -680,7 +673,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
try { try {
// Don't pass courseId and categoryId, we'll filter them locally. // Don't pass courseId and categoryId, we'll filter them locally.
result = await AddonCalendar.getDayEvents(day.year, day.monthNumber, day.dayNumber); result = await AddonCalendar.getDayEvents(day.moment.year(), day.moment.month() + 1, day.moment.date());
preloadedDay.onlineEvents = await Promise.all( preloadedDay.onlineEvents = await Promise.all(
result.events.map((event) => AddonCalendarHelper.formatEventData(event)), result.events.map((event) => AddonCalendarHelper.formatEventData(event)),
); );
@ -692,7 +685,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
} }
// Schedule notifications for the events retrieved (only future events will be scheduled). // Schedule notifications for the events retrieved (only future events will be scheduled).
AddonCalendar.scheduleEventsNotifications(preloadedDay.onlineEvents); AddonCalendar.scheduleEventsNotifications(preloadedDay.onlineEvents || []);
// Merge the online events with offline data. // Merge the online events with offline data.
preloadedDay.events = this.mergeEvents(preloadedDay); preloadedDay.events = this.mergeEvents(preloadedDay);
@ -701,7 +694,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
this.filterEvents(preloadedDay, this.dayPage.filter); this.filterEvents(preloadedDay, this.dayPage.filter);
// Re-calculate the formatted time so it uses the device date. // Re-calculate the formatted time so it uses the device date.
const dayTime = moment().year(day.year).month(day.monthNumber - 1).date(day.dayNumber).unix() * 1000; const dayTime = day.moment.unix() * 1000;
const currentTime = CoreTimeUtils.timestamp(); const currentTime = CoreTimeUtils.timestamp();
const promises = preloadedDay.events.map(async (event) => { const promises = preloadedDay.events.map(async (event) => {
@ -734,19 +727,19 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
mergeEvents(day: PreloadedDay): AddonCalendarEventToDisplay[] { mergeEvents(day: PreloadedDay): AddonCalendarEventToDisplay[] {
day.hasOffline = false; day.hasOffline = false;
if (!Object.keys(this.offlineEvents).length && !this.deletedEvents.length) { if (!Object.keys(this.offlineEvents).length && !this.deletedEvents?.size) {
// No offline events, nothing to merge. // No offline events, nothing to merge.
return day.onlineEvents; return day.onlineEvents || [];
} }
const monthOfflineEvents = this.offlineEvents[AddonCalendarHelper.getMonthId(day)]; const monthOfflineEvents = this.offlineEvents[AddonCalendarHelper.getMonthId(day.moment)];
const dayOfflineEvents = monthOfflineEvents && monthOfflineEvents[day.dayNumber]; const dayOfflineEvents = monthOfflineEvents && monthOfflineEvents[day.moment.date()];
let result = day.onlineEvents; let result = day.onlineEvents || [];
if (this.deletedEvents.length) { if (this.deletedEvents?.size) {
// Mark as deleted the events that were deleted in offline. // Mark as deleted the events that were deleted in offline.
result.forEach((event) => { result.forEach((event) => {
event.deleted = this.deletedEvents.indexOf(event.id) != -1; event.deleted = this.deletedEvents?.has(event.id);
if (event.deleted) { if (event.deleted) {
day.hasOffline = true; day.hasOffline = true;
@ -758,7 +751,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
// Remove the online events that were modified in offline. // Remove the online events that were modified in offline.
result = result.filter((event) => this.offlineEditedEventsIds.indexOf(event.id) == -1); result = result.filter((event) => this.offlineEditedEventsIds.indexOf(event.id) == -1);
if (result.length != day.onlineEvents.length) { if (result.length != day.onlineEvents?.length) {
day.hasOffline = true; day.hasOffline = true;
} }
} }
@ -775,24 +768,28 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
/** /**
* Invalidate content. * Invalidate content.
* *
* @param visibleDay The current visible day. * @param selectedDay The current selected day.
* @param invalidateDayEvents Whether to invalidate visible day events. * @param invalidateDayEvents Whether to invalidate selected day events.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async invalidateContent(visibleDay: PreloadedDay | null, invalidateDayEvents?: boolean): Promise<void> { async invalidateContent(selectedDay: PreloadedDay | null, invalidateDayEvents?: boolean): Promise<void> {
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
if (invalidateDayEvents && visibleDay) { if (invalidateDayEvents && selectedDay) {
promises.push(AddonCalendar.invalidateDayEvents(visibleDay.year, visibleDay.monthNumber, visibleDay.dayNumber)); promises.push(AddonCalendar.invalidateDayEvents(
selectedDay.moment.year(),
selectedDay.moment.month() + 1,
selectedDay.moment.date(),
));
} }
promises.push(AddonCalendar.invalidateAllowedEventTypes()); promises.push(AddonCalendar.invalidateAllowedEventTypes());
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.categories = undefined; // Get categories again.
if (visibleDay) { if (selectedDay) {
visibleDay.dirty = true; selectedDay.dirty = true;
} }
await Promise.all(promises); await Promise.all(promises);
@ -807,7 +804,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
markAsDeleted(eventId: number, deleted: boolean): void { markAsDeleted(eventId: number, deleted: boolean): void {
// Mark the event as deleted or not. // Mark the event as deleted or not.
this.getItems()?.some(day => { this.getItems()?.some(day => {
const event = day.onlineEvents.find((event) => event.id == eventId); const event = day.onlineEvents?.find((event) => event.id == eventId);
if (!event) { if (!event) {
return false; return false;
@ -819,8 +816,8 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
day.hasOffline = true; day.hasOffline = true;
} else { } else {
// Re-calculate "hasOffline". // Re-calculate "hasOffline".
day.hasOffline = day.events.length != day.onlineEvents.length || day.hasOffline = day.events?.length != day.onlineEvents?.length ||
day.events.some((event) => event.deleted || event.offline); day.events?.some((event) => event.deleted || event.offline);
} }
return true; return true;
@ -828,14 +825,9 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
// Add it or remove it from the list of deleted events. // Add it or remove it from the list of deleted events.
if (deleted) { if (deleted) {
if (!this.deletedEvents.includes(eventId)) { this.deletedEvents?.add(eventId);
this.deletedEvents.push(eventId);
}
} else { } else {
const index = this.deletedEvents.indexOf(eventId); this.deletedEvents?.delete(eventId);
if (index != -1) {
this.deletedEvents.splice(index, 1);
}
} }
} }

View File

@ -37,7 +37,6 @@ import { AddonCalendarSyncInvalidateEvent } from './calendar-sync';
import { AddonCalendarOfflineEventDBRecord } from './database/calendar-offline'; import { AddonCalendarOfflineEventDBRecord } from './database/calendar-offline';
import { CoreCategoryData } from '@features/courses/services/courses'; import { CoreCategoryData } from '@features/courses/services/courses';
import { AddonCalendarReminderDBRecord } from './database/calendar'; import { AddonCalendarReminderDBRecord } from './database/calendar';
import { YearAndMonth, YearMonthDay } from '@services/utils/time';
/** /**
* Context levels enumeration. * Context levels enumeration.
@ -141,7 +140,7 @@ export class AddonCalendarHelperProvider {
// Add the event to all the days it lasts. // Add the event to all the days it lasts.
while (!treatedDay.isAfter(endDay, 'day')) { while (!treatedDay.isAfter(endDay, 'day')) {
const monthId = this.getMonthId({ year: treatedDay.year(), monthNumber: treatedDay.month() + 1 }); const monthId = this.getMonthId(treatedDay);
const day = treatedDay.date(); const day = treatedDay.date();
if (!result[monthId]) { if (!result[monthId]) {
@ -367,21 +366,21 @@ export class AddonCalendarHelperProvider {
/** /**
* Get the month "id". * Get the month "id".
* *
* @param month Month data. * @param moment Month moment.
* @return The "id". * @return The "id".
*/ */
getMonthId(month: YearAndMonth): string { getMonthId(moment: moment.Moment): string {
return `${month.year}#${month.monthNumber}`; return `${moment.year()}#${moment.month() + 1}`;
} }
/** /**
* Get the day "id". * Get the day "id".
* *
* @param day Day data. * @param day Day moment.
* @return The "id". * @return The "id".
*/ */
getDayId(day: YearMonthDay): string { getDayId(moment: moment.Moment): string {
return `${day.year}#${day.monthNumber}#${day.dayNumber}`; return `${this.getMonthId(moment)}#${moment.date()}`;
} }
/** /**
@ -660,7 +659,7 @@ export class AddonCalendarHelperProvider {
fetchTimestarts.map((fetchTime) => { fetchTimestarts.map((fetchTime) => {
const day = moment(new Date(fetchTime * 1000)); const day = moment(new Date(fetchTime * 1000));
const monthId = this.getMonthId({ year: day.year(), monthNumber: day.month() + 1 }); const monthId = this.getMonthId(day);
if (!treatedMonths[monthId]) { if (!treatedMonths[monthId]) {
// Month not refetch or invalidated already, do it now. // Month not refetch or invalidated already, do it now.
treatedMonths[monthId] = true; treatedMonths[monthId] = true;
@ -696,7 +695,7 @@ export class AddonCalendarHelperProvider {
invalidateTimestarts.map((fetchTime) => { invalidateTimestarts.map((fetchTime) => {
const day = moment(new Date(fetchTime * 1000)); const day = moment(new Date(fetchTime * 1000));
const monthId = this.getMonthId({ year: day.year(), monthNumber: day.month() + 1 }); const monthId = this.getMonthId(day);
if (!treatedMonths[monthId]) { if (!treatedMonths[monthId]) {
// Month not refetch or invalidated already, do it now. // Month not refetch or invalidated already, do it now.
treatedMonths[monthId] = true; treatedMonths[monthId] = true;

View File

@ -29,6 +29,7 @@ import { AddonCalendarHelper } from './calendar-helper';
import { makeSingleton, Translate } from '@singletons'; import { makeSingleton, Translate } from '@singletons';
import { CoreSync } from '@services/sync'; import { CoreSync } from '@services/sync';
import { CoreNetworkError } from '@classes/errors/network-error'; import { CoreNetworkError } from '@classes/errors/network-error';
import moment from 'moment';
/** /**
* Service to sync calendar. * Service to sync calendar.
@ -307,9 +308,7 @@ export type AddonCalendarSyncEvents = {
toinvalidate: AddonCalendarSyncInvalidateEvent[]; toinvalidate: AddonCalendarSyncInvalidateEvent[];
updated: boolean; updated: boolean;
source?: string; // Added on pages. source?: string; // Added on pages.
day?: number; // Added on day page. moment?: moment.Moment; // Added on day page.
month?: number; // Added on day page.
year?: number; // Added on day page.
}; };
export type AddonCalendarSyncInvalidateEvent = { export type AddonCalendarSyncInvalidateEvent = {

View File

@ -15,7 +15,7 @@
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { CoreCourse } from '@features/course/services/course'; import { CoreCourse } from '@features/course/services/course';
import { CanLeave } from '@guards/can-leave'; import { CanLeave } from '@guards/can-leave';
import { IonRefresher } from '@ionic/angular'; import { IonRefresher } from '@ionic/angular';
@ -216,7 +216,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, OnDestroy, Ca
/** /**
* Helper to manage swiping within a collection of submissions. * Helper to manage swiping within a collection of submissions.
*/ */
class AddonModAssignSubmissionSwipeItemsManager extends CoreSwipeItemsManager { class AddonModAssignSubmissionSwipeItemsManager extends CoreSwipeNavigationItemsManager {
/** /**
* @inheritdoc * @inheritdoc

View File

@ -40,14 +40,14 @@
previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)"> previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)">
</core-navigation-bar> </core-navigation-bar>
<core-swipe-slides [manager]="manager" [options]="slidesOpts" (onWillChange)="slideChanged($event)"> <core-swipe-slides [manager]="manager" [options]="slidesOpts">
<ng-template let-item="item"> <ng-template let-chapter="item">
<div class="ion-padding"> <div class="ion-padding">
<core-format-text [component]="component" [componentId]="componentId" [text]="item.content" contextLevel="module" <core-format-text [component]="component" [componentId]="componentId" [text]="chapter.content" contextLevel="module"
[contextInstanceId]="module.id" [courseId]="courseId"></core-format-text> [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
<div class="ion-margin-top" *ngIf="tagsEnabled && item.tags?.length > 0"> <div class="ion-margin-top" *ngIf="chapter.tags?.length > 0">
<strong>{{ 'core.tag.tags' | translate }}: </strong> <strong>{{ 'core.tag.tags' | translate }}: </strong>
<core-tag-list [tags]="item.tags"></core-tag-list> <core-tag-list [tags]="chapter.tags"></core-tag-list>
</div> </div>
</div> </div>
</ng-template> </ng-template>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, Optional, Input, OnInit, ViewChild, ElementRef } from '@angular/core'; import { Component, Optional, Input, OnInit, ViewChild, ElementRef, OnDestroy } from '@angular/core';
import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component'; import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component';
import { import {
AddonModBookProvider, AddonModBookProvider,
@ -31,10 +31,11 @@ import { AddonModBookTocComponent } from '../toc/toc';
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar'; import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreSwipeCurrentItemData, CoreSwipeSlidesComponent, CoreSwipeSlidesOptions } from '@components/swipe-slides/swipe-slides'; import { CoreSwipeSlidesComponent, CoreSwipeSlidesOptions } from '@components/swipe-slides/swipe-slides';
import { CoreSwipeSlidesItemsManagerSource } from '@classes/items-management/slides-items-manager-source'; import { CoreSwipeSlidesItemsManagerSource } from '@classes/items-management/swipe-slides-items-manager-source';
import { CoreCourseModule } from '@features/course/services/course-helper'; import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreSwipeSlidesItemsManager } from '@classes/items-management/slides-items-manager'; import { CoreSwipeSlidesItemsManager } from '@classes/items-management/swipe-slides-items-manager';
import { CoreTextUtils } from '@services/utils/text';
/** /**
* Component that displays a book. * Component that displays a book.
@ -43,7 +44,7 @@ import { CoreSwipeSlidesItemsManager } from '@classes/items-management/slides-it
selector: 'addon-mod-book-index', selector: 'addon-mod-book-index',
templateUrl: 'addon-mod-book-index.html', templateUrl: 'addon-mod-book-index.html',
}) })
export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy {
@ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent; @ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent;
@ -51,9 +52,6 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
component = AddonModBookProvider.COMPONENT; component = AddonModBookProvider.COMPONENT;
manager?: CoreSwipeSlidesItemsManager<LoadedChapter, AddonModBookSlidesItemsManagerSource>; manager?: CoreSwipeSlidesItemsManager<LoadedChapter, AddonModBookSlidesItemsManagerSource>;
previousChapter?: AddonModBookTocChapter;
nextChapter?: AddonModBookTocChapter;
tagsEnabled = false;
warning = ''; warning = '';
displayNavBar = true; displayNavBar = true;
navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = []; navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
@ -63,8 +61,9 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
scrollOnChange: 'top', scrollOnChange: 'top',
}; };
protected currentChapter?: number; protected firstLoad = true;
protected element: HTMLElement; protected element: HTMLElement;
protected managerUnsubscribe?: () => void;
constructor( constructor(
elementRef: ElementRef, elementRef: ElementRef,
@ -81,14 +80,18 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
super.ngOnInit(); super.ngOnInit();
this.tagsEnabled = CoreTag.areTagsAvailableInSite();
const source = new AddonModBookSlidesItemsManagerSource( const source = new AddonModBookSlidesItemsManagerSource(
this.courseId, this.courseId,
this.module, this.module,
this.tagsEnabled, CoreTag.areTagsAvailableInSite(),
this.initialChapterId, this.initialChapterId,
); );
this.manager = new CoreSwipeSlidesItemsManager(source); this.manager = new CoreSwipeSlidesItemsManager(source);
this.managerUnsubscribe = this.manager.addListener({
onSelectedItemUpdated: (item) => {
this.onChapterViewed(item.id);
},
});
this.loadContent(); this.loadContent();
} }
@ -106,12 +109,14 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
*/ */
async showToc(): Promise<void> { async showToc(): Promise<void> {
// Create the toc modal. // Create the toc modal.
const visibleChapter = this.manager?.getSelectedItem();
const modalData = await CoreDomUtils.openSideModal<number>({ const modalData = await CoreDomUtils.openSideModal<number>({
component: AddonModBookTocComponent, component: AddonModBookTocComponent,
componentProps: { componentProps: {
moduleId: this.module.id, moduleId: this.module.id,
chapters: this.chapters, chapters: this.chapters,
selected: this.currentChapter, selected: visibleChapter,
courseId: this.courseId, courseId: this.courseId,
book: this.book, book: this.book,
}, },
@ -129,7 +134,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
changeChapter(chapterId: number): void { changeChapter(chapterId: number): void {
if (!chapterId || chapterId === this.currentChapter) { if (!chapterId) {
return; return;
} }
@ -183,14 +188,15 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
} }
/** /**
* View a book chapter. * Update data related to chapter being viewed.
* *
* @param chapterId Chapter to load. * @param chapterId Chapter viewed.
* @param logChapterId Whether chapter ID should be passed to the log view function.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async viewChapter(chapterId: number, logChapterId: boolean): Promise<void> { protected async onChapterViewed(chapterId: number): Promise<void> {
this.currentChapter = chapterId; // Don't log the chapter ID when the user has just opened the book.
const logChapterId = this.firstLoad;
this.firstLoad = false;
if (this.displayNavBar) { if (this.displayNavBar) {
this.navigationItems = this.getNavigationItems(chapterId); this.navigationItems = this.getNavigationItems(chapterId);
@ -212,15 +218,6 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
} }
} }
/**
* Slide has changed.
*
* @param data Data about new item.
*/
slideChanged(data: CoreSwipeCurrentItemData<LoadedChapter>): void {
this.viewChapter(data.item.id, true);
}
/** /**
* Converts chapters to navigation items. * Converts chapters to navigation items.
* *
@ -236,11 +233,20 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
})); }));
} }
/**
* @inheritdoc
*/
ngOnDestroy(): void {
super.ngOnDestroy();
this.managerUnsubscribe && this.managerUnsubscribe();
}
} }
type LoadedChapter = { type LoadedChapter = {
id: number; id: number;
content: string; content?: string;
tags?: CoreTagItem[]; tags?: CoreTagItem[];
}; };
@ -287,7 +293,6 @@ class AddonModBookSlidesItemsManagerSource extends CoreSwipeSlidesItemsManagerSo
* Load module contents. * Load module contents.
*/ */
async loadContents(): Promise<void> { async loadContents(): Promise<void> {
// Get contents. No need to refresh, it has been done in downloadResourceIfNeeded.
const contents = await CoreCourse.getModuleContents(this.MODULE, this.COURSE_ID); const contents = await CoreCourse.getModuleContents(this.MODULE, this.COURSE_ID);
this.contentsMap = AddonModBook.getContentsMap(contents); this.contentsMap = AddonModBook.getContentsMap(contents);
@ -310,10 +315,9 @@ class AddonModBookSlidesItemsManagerSource extends CoreSwipeSlidesItemsManagerSo
})); }));
return newChapters; return newChapters;
} catch (exception) { } catch (error) {
const error = exception ?? new CoreError(Translate.instant('addon.mod_book.errorchapter')); if (!CoreTextUtils.getErrorMessageFromError(error)) {
if (!error.message) { throw new CoreError(Translate.instant('addon.mod_book.errorchapter'));
error.message = Translate.instant('addon.mod_book.errorchapter');
} }
throw error; throw error;

View File

@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from './forum-discussions-source'; import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from './forum-discussions-source';
/** /**
* Helper to manage swiping within a collection of discussions. * Helper to manage swiping within a collection of discussions.
*/ */
export class AddonModForumDiscussionsSwipeManager export class AddonModForumDiscussionsSwipeManager
extends CoreSwipeItemsManager<AddonModForumDiscussionItem, AddonModForumDiscussionsSource> { extends CoreSwipeNavigationItemsManager<AddonModForumDiscussionItem, AddonModForumDiscussionsSource> {
/** /**
* @inheritdoc * @inheritdoc

View File

@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { AddonModGlossaryEntriesSource, AddonModGlossaryEntryItem } from './glossary-entries-source'; import { AddonModGlossaryEntriesSource, AddonModGlossaryEntryItem } from './glossary-entries-source';
/** /**
* Helper to manage swiping within a collection of glossary entries. * Helper to manage swiping within a collection of glossary entries.
*/ */
export abstract class AddonModGlossaryEntriesSwipeManager export abstract class AddonModGlossaryEntriesSwipeManager
extends CoreSwipeItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> { extends CoreSwipeNavigationItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> {
/** /**
* @inheritdoc * @inheritdoc

View File

@ -15,7 +15,7 @@
/** /**
* Updates listener. * Updates listener.
*/ */
export interface CoreItemsListSourceListener<Item> { export interface CoreItemsManagerSourceListener<Item> {
onItemsUpdated?(items: Item[]): void; onItemsUpdated?(items: Item[]): void;
onReset?(): void; onReset?(): void;
} }
@ -26,16 +26,40 @@ export interface CoreItemsListSourceListener<Item> {
export abstract class CoreItemsManagerSource<Item = unknown> { export abstract class CoreItemsManagerSource<Item = unknown> {
protected items: Item[] | null = null; protected items: Item[] | null = null;
protected listeners: CoreItemsListSourceListener<Item>[] = []; protected listeners: CoreItemsManagerSourceListener<Item>[] = [];
protected dirty = false; protected dirty = false;
protected loaded = false;
protected loadedPromise: Promise<void>;
protected resolveLoaded!: () => void;
constructor() {
this.loadedPromise = new Promise(resolve => this.resolveLoaded = resolve);
}
/** /**
* Check whether any item has been loaded. * Check whether data is loaded.
* *
* @returns Whether any item has been loaded. * @returns Whether data is loaded.
*/ */
isLoaded(): boolean { isLoaded(): boolean {
return this.items !== null; return this.loaded;
}
/**
* Return a promise that is resolved when the data is loaded.
*
* @return Promise.
*/
waitForLoaded(): Promise<void> {
return this.loadedPromise;
}
/**
* Mark the source as initialized.
*/
protected setLoaded(): void {
this.loaded = true;
this.resolveLoaded();
} }
/** /**
@ -79,7 +103,7 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
* @param listener Listener. * @param listener Listener.
* @returns Unsubscribe function. * @returns Unsubscribe function.
*/ */
addListener(listener: CoreItemsListSourceListener<Item>): () => void { addListener(listener: CoreItemsManagerSourceListener<Item>): () => void {
this.listeners.push(listener); this.listeners.push(listener);
return () => this.removeListener(listener); return () => this.removeListener(listener);
@ -90,7 +114,7 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
* *
* @param listener Listener. * @param listener Listener.
*/ */
removeListener(listener: CoreItemsListSourceListener<Item>): void { removeListener(listener: CoreItemsManagerSourceListener<Item>): void {
const index = this.listeners.indexOf(listener); const index = this.listeners.indexOf(listener);
if (index === -1) { if (index === -1) {
@ -107,6 +131,7 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
*/ */
protected setItems(items: Item[]): void { protected setItems(items: Item[]): void {
this.items = items; this.items = items;
this.setLoaded();
this.notifyItemsUpdated(); this.notifyItemsUpdated();
} }

View File

@ -14,6 +14,13 @@
import { CoreItemsManagerSource } from './items-manager-source'; import { CoreItemsManagerSource } from './items-manager-source';
/**
* Listeners.
*/
export interface CoreItemsanagerListener<Item> {
onSelectedItemUpdated?(item: Item): void;
}
/** /**
* Helper to manage a collection of items in a page. * Helper to manage a collection of items in a page.
*/ */
@ -25,6 +32,7 @@ export abstract class CoreItemsManager<
protected source?: { instance: Source; unsubscribe: () => void }; protected source?: { instance: Source; unsubscribe: () => void };
protected itemsMap: Record<string, Item> | null = null; protected itemsMap: Record<string, Item> | null = null;
protected selectedItem: Item | null = null; protected selectedItem: Item | null = null;
protected listeners: CoreItemsanagerListener<Item>[] = [];
constructor(source: Source) { constructor(source: Source) {
this.setSource(source); this.setSource(source);
@ -96,6 +104,35 @@ export abstract class CoreItemsManager<
*/ */
setSelectedItem(item: Item | null): void { setSelectedItem(item: Item | null): void {
this.selectedItem = item; this.selectedItem = item;
this.listeners.forEach(listener => listener.onSelectedItemUpdated?.call(listener, item));
}
/**
* Register a listener.
*
* @param listener Listener.
* @returns Unsubscribe function.
*/
addListener(listener: CoreItemsanagerListener<Item>): () => void {
this.listeners.push(listener);
return () => this.removeListener(listener);
}
/**
* Remove a listener.
*
* @param listener Listener.
*/
removeListener(listener: CoreItemsanagerListener<Item>): void {
const index = this.listeners.indexOf(listener);
if (index === -1) {
return;
}
this.listeners.splice(index, 1);
} }
/** /**
@ -103,9 +140,12 @@ export abstract class CoreItemsManager<
* *
* @param items New items. * @param items New items.
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected onSourceItemsUpdated(items: Item[]): void { protected onSourceItemsUpdated(items: Item[]): void {
// Nothing to do. this.itemsMap = items.reduce((map, item) => {
map[this.getItemId(item)] = item;
return map;
}, {});
} }
/** /**
@ -116,4 +156,22 @@ export abstract class CoreItemsManager<
this.selectedItem = null; this.selectedItem = null;
} }
/**
* Get item by ID.
*
* @param id ID
* @return Item, null if not found.
*/
getItemById(id: string | number): Item | null {
return this.itemsMap?.[id] ?? null;
}
/**
* Get an ID to identify an item.
*
* @param item Data about the item.
* @return Item ID.
*/
abstract getItemId(item: Item): string | number;
} }

View File

@ -123,13 +123,16 @@ export abstract class CoreRoutedItemsManager<
* @inheritdoc * @inheritdoc
*/ */
protected onSourceItemsUpdated(items: Item[]): void { protected onSourceItemsUpdated(items: Item[]): void {
this.itemsMap = items.reduce((map, item) => { super.onSourceItemsUpdated(items);
map[this.getSource().getItemPath(item)] = item;
return map;
}, {});
this.updateSelectedItem(); this.updateSelectedItem();
} }
/**
* @inheritdoc
*/
getItemId(item: Item): string | number {
return this.getSource().getItemPath(item);
}
} }

View File

@ -22,7 +22,7 @@ import { CoreRoutedItemsManagerSource } from './routed-items-manager-source';
/** /**
* Helper class to manage the state and routing of a swipeable page. * Helper class to manage the state and routing of a swipeable page.
*/ */
export class CoreSwipeItemsManager< export class CoreSwipeNavigationItemsManager<
Item = unknown, Item = unknown,
Source extends CoreRoutedItemsManagerSource<Item> = CoreRoutedItemsManagerSource<Item> Source extends CoreRoutedItemsManagerSource<Item> = CoreRoutedItemsManagerSource<Item>
> >
@ -99,7 +99,7 @@ export class CoreSwipeItemsManager<
protected async getItemBy(delta: number): Promise<Item | null> { protected async getItemBy(delta: number): Promise<Item | null> {
const items = this.getSource().getItems(); const items = this.getSource().getItems();
// Get current item. // Get selected item.
const index = (this.selectedItem && items?.indexOf(this.selectedItem)) ?? -1; const index = (this.selectedItem && items?.indexOf(this.selectedItem)) ?? -1;
if (index === -1) { if (index === -1) {

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { CoreSwipeSlidesItemsManagerSource } from './slides-items-manager-source'; import { CoreSwipeSlidesItemsManagerSource } from './swipe-slides-items-manager-source';
/** /**
* Items collection source data for "swipe slides". * Items collection source data for "swipe slides".
@ -26,16 +26,16 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource<Item extends Core
/** /**
* @inheritdoc * @inheritdoc
*/ */
async load(currentItem?: Partial<Item> | null): Promise<void> { async load(selectedItem?: Item | null): Promise<void> {
if (!this.initialized && this.initialItem) { if (!this.loaded && this.initialItem) {
// Load the initial item. // Load the initial item.
await this.loadItem(this.initialItem); await this.loadItem(this.initialItem);
} else if (this.initialized && currentItem) { } else if (this.loaded && selectedItem) {
// Reload current item if needed. // Reload selected item if needed.
await this.loadItem(currentItem); await this.loadItem(selectedItem);
} }
this.setInitialized(); this.setLoaded();
} }
/** /**
@ -49,35 +49,35 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource<Item extends Core
/** /**
* Load a certain item and preload next and previous ones. * Load a certain item and preload next and previous ones.
* *
* @param item Basic data about the item to load. * @param item Item to load.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async loadItem(item: Partial<Item>): Promise<void> { async loadItem(item: Item): Promise<void> {
const previousItem = this.getPreviousItem(item); const previousItem = this.getPreviousItem(item);
const nextItem = this.getNextItem(item); const nextItem = this.getNextItem(item);
await Promise.all([ await Promise.all([
this.loadItemInList(item, false), this.loadItemInList(item, false),
previousItem ? this.loadItemInList(previousItem, true) : undefined, previousItem && this.loadItemInList(previousItem, true),
nextItem ? this.loadItemInList(nextItem, true) : undefined, nextItem && this.loadItemInList(nextItem, true),
]); ]);
} }
/** /**
* Load or preload a certain item and add it to the list. * Load or preload a certain item and add it to the list.
* *
* @param item Basic data about the item to load. * @param item Item to load.
* @param preload Whether to preload. * @param preload Whether to preload.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async loadItemInList(item: Partial<Item>, preload = false): Promise<void> { async loadItemInList(item: Item, preload = false): Promise<void> {
const preloadedItem = await this.performLoadItemData(item, preload); const preloadedItem = await this.performLoadItemData(item, preload);
if (!preloadedItem) { if (!preloadedItem) {
return; return;
} }
// Add the item at the right position. // Add the item at the right position.
const existingItem = this.getItem(item); const existingItem = this.getItem(this.getItemId(item));
if (existingItem) { if (existingItem) {
// Already in list, no need to add it. // Already in list, no need to add it.
return; return;
@ -94,19 +94,19 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource<Item extends Core
const added = this.items.some((item, index) => { const added = this.items.some((item, index) => {
const itemId = this.getItemId(item); const itemId = this.getItemId(item);
let positionToInsert = -1; let indexToInsert = -1;
if (itemId === previousItemId) { if (itemId === previousItemId) {
// Previous item found, add the item after it. // Previous item found, add the item after it.
positionToInsert = index + 1; indexToInsert = index + 1;
} }
if (itemId === nextItemId) { if (itemId === nextItemId) {
// Next item found, add the item before it. // Next item found, add the item before it.
positionToInsert = index; indexToInsert = index;
} }
if (positionToInsert > -1) { if (indexToInsert > -1) {
this.items?.splice(positionToInsert, 0, preloadedItem); this.items?.splice(indexToInsert, 0, preloadedItem);
return true; return true;
} }
@ -124,19 +124,19 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource<Item extends Core
* Load or preload a certain item data. * Load or preload a certain item data.
* This helper function will check some common cases so they don't have to be replicated in all loadItemData implementations. * This helper function will check some common cases so they don't have to be replicated in all loadItemData implementations.
* *
* @param item Basic data about the item to load. * @param item Item to load.
* @param preload Whether to preload. * @param preload Whether to preload.
* @return Promise resolved with item. Resolve with null if already loading or item is not valid (e.g. there are no more items). * @return Promise resolved with item. Resolve with null if already loading or item is not valid (e.g. there are no more items).
*/ */
protected async performLoadItemData(item: Partial<Item>, preload: boolean): Promise<Item | null> { protected async performLoadItemData(item: Item, preload: boolean): Promise<Item | null> {
const itemId = this.getItemId(item); const itemId = this.getItemId(item);
if (!itemId || this.loadingItems[itemId]) { if (this.loadingItems[itemId]) {
// Not valid or already loading, ignore it. // Already loading, ignore it.
return null; return null;
} }
const existingItem = this.getItem(item); const existingItem = this.getItem(itemId);
if (existingItem && ((existingItem.loaded && !existingItem.dirty) || preload)) { if (existingItem && ((existingItem.loaded && !existingItem.dirty) || preload)) {
// Already loaded, or preloading an already preloaded item. // Already loaded, or preloading an already preloaded item.
return existingItem; return existingItem;
@ -191,7 +191,7 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource<Item extends Core
* @param preload Whether to preload. * @param preload Whether to preload.
* @return Promise resolved with item. Resolve with null if item is not valid (e.g. there are no more items). * @return Promise resolved with item. Resolve with null if item is not valid (e.g. there are no more items).
*/ */
abstract loadItemData(item: Partial<Item>, preload: boolean): Promise<Item | null>; abstract loadItemData(item: Item, preload: boolean): Promise<Item | null>;
/** /**
* Return the data to identify the previous item. * Return the data to identify the previous item.
@ -199,7 +199,7 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource<Item extends Core
* @param item Data about the item. * @param item Data about the item.
* @return Previous item data. Null if no previous item. * @return Previous item data. Null if no previous item.
*/ */
abstract getPreviousItem(item: Partial<Item>): Partial<Item> | null; abstract getPreviousItem(item: Item): Item | null;
/** /**
* Return the data to identify the next item. * Return the data to identify the next item.
@ -207,7 +207,7 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource<Item extends Core
* @param item Data about the item. * @param item Data about the item.
* @return Next item data. Null if no next item. * @return Next item data. Null if no next item.
*/ */
abstract getNextItem(item: Partial<Item>): Partial<Item> | null; abstract getNextItem(item: Item): Item | null;
} }

View File

@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { CoreSwipeSlidesDynamicItem, CoreSwipeSlidesDynamicItemsManagerSource } from './slides-dynamic-items-manager-source'; import { CoreSwipeSlidesDynamicItem, CoreSwipeSlidesDynamicItemsManagerSource } from './swipe-slides-dynamic-items-manager-source';
import { CoreSwipeSlidesItemsManager } from './slides-items-manager'; import { CoreSwipeSlidesItemsManager } from './swipe-slides-items-manager';
/** /**
* Helper class to manage items for core-swipe-slides. * Helper class to manage items for core-swipe-slides.

View File

@ -19,40 +19,12 @@ import { CoreItemsManagerSource } from './items-manager-source';
*/ */
export abstract class CoreSwipeSlidesItemsManagerSource<Item = unknown> extends CoreItemsManagerSource<Item> { export abstract class CoreSwipeSlidesItemsManagerSource<Item = unknown> extends CoreItemsManagerSource<Item> {
protected initialItem?: Partial<Item>; protected initialItem?: Item;
protected initialized = false;
protected initializePromise: Promise<void>;
protected resolveInitialize!: () => void;
constructor(initialItem?: Partial<Item>) { constructor(initialItem?: Item) {
super(); super();
this.initialItem = initialItem; this.initialItem = initialItem;
this.initializePromise = new Promise(resolve => this.resolveInitialize = resolve);
}
/**
* @inheritdoc
*/
isLoaded(): boolean {
return this.initialized && super.isLoaded();
}
/**
* Return a promise that is resolved when the source is initialized.
*
* @return Promise.
*/
waitForInitialized(): Promise<void> {
return this.initializePromise;
}
/**
* Mark the source as initialized.
*/
protected setInitialized(): void {
this.initialized = true;
this.resolveInitialize();
} }
/** /**
@ -62,7 +34,6 @@ export abstract class CoreSwipeSlidesItemsManagerSource<Item = unknown> extends
const items = await this.loadItems(); const items = await this.loadItems();
this.setItems(items); this.setItems(items);
this.setInitialized();
} }
/** /**
@ -75,47 +46,56 @@ export abstract class CoreSwipeSlidesItemsManagerSource<Item = unknown> extends
/** /**
* Get a certain item. * Get a certain item.
* *
* @param item Partial data about the item to search. * @param id Item ID.
* @return Item, null if not found. * @return Item, null if not found.
*/ */
getItem(item: Partial<Item>): Item | null { getItem(id: string | number): Item | null {
const index = this.getItemPosition(item); const index = this.getItemIndexById(id);
return this.items?.[index] ?? null; return this.items?.[index] ?? null;
} }
/** /**
* Get a certain item position. * Get a certain item index.
* *
* @param item Item to search. * @param item Item.
* @return Item position, -1 if not found. * @return Item index, -1 if not found.
*/ */
getItemPosition(item: Partial<Item>): number { getItemIndex(item: Item): number {
const itemId = this.getItemId(item); return this.getItemIndexById(this.getItemId(item));
const index = this.items?.findIndex((listItem) => itemId === this.getItemId(listItem)); }
/**
* Get a certain item index.
*
* @param id Item ID.
* @return Item index, -1 if not found.
*/
getItemIndexById(id: string | number): number {
const index = this.items?.findIndex((listItem) => id === this.getItemId(listItem));
return index ?? -1; return index ?? -1;
} }
/** /**
* Get initial item position. * Get initial item index.
* *
* @return Initial item position. * @return Initial item index.
*/ */
getInitialPosition(): number { getInitialItemIndex(): number {
if (!this.initialItem) { if (!this.initialItem) {
return 0; return 0;
} }
return this.getItemPosition(this.initialItem); return this.getItemIndex(this.initialItem);
} }
/** /**
* Get the ID of an item. * Get the ID of an item.
* *
* @param item Data about the item. * @param item Item.
* @return Item ID. * @return Item ID.
*/ */
abstract getItemId(item: Partial<Item>): string | number; abstract getItemId(item: Item): string | number;
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import { CoreItemsManager } from './items-manager'; import { CoreItemsManager } from './items-manager';
import { CoreSwipeSlidesItemsManagerSource } from './slides-items-manager-source'; import { CoreSwipeSlidesItemsManagerSource } from './swipe-slides-items-manager-source';
/** /**
* Helper class to manage items for core-swipe-slides. * Helper class to manage items for core-swipe-slides.
@ -26,22 +26,8 @@ export class CoreSwipeSlidesItemsManager<
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected onSourceItemsUpdated(items: Item[]): void { getItemId(item: Item): string | number {
this.itemsMap = items.reduce((map, item) => { return this.getSource().getItemId(item);
map[this.getSource().getItemId(item)] = item;
return map;
}, {});
}
/**
* Get item by ID.
*
* @param id ID
* @return Item, null if not found.
*/
getItemById(id: string): Item | null {
return this.itemsMap?.[id] ?? null;
} }
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { CoreScreen } from '@services/screen'; import { CoreScreen } from '@services/screen';
@Component({ @Component({
@ -23,7 +23,7 @@ import { CoreScreen } from '@services/screen';
}) })
export class CoreSwipeNavigationComponent { export class CoreSwipeNavigationComponent {
@Input() manager?: CoreSwipeItemsManager; @Input() manager?: CoreSwipeNavigationItemsManager;
get enabled(): boolean { get enabled(): boolean {
return CoreScreen.isMobile && !!this.manager; return CoreScreen.isMobile && !!this.manager;

View File

@ -1,6 +1,5 @@
<ion-slides *ngIf="manager && manager.getSource().isLoaded()" (ionSlideWillChange)="slideWillChange()" <ion-slides *ngIf="loaded" (ionSlideWillChange)="slideWillChange()" (ionSlideDidChange)="slideDidChange()" [options]="options">
(ionSlideDidChange)="slideDidChange()" [options]="options"> <ion-slide *ngFor="let item of items">
<ion-slide *ngFor="let item of manager.getSource().getItems()">
<ng-container *ngIf="template" [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{item: item}"></ng-container> <ng-container *ngIf="template" [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{item: item}"></ng-container>
</ion-slide> </ion-slide>
</ion-slides> </ion-slides>

View File

@ -15,9 +15,10 @@
import { import {
Component, ContentChild, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, TemplateRef, ViewChild, Component, ContentChild, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, TemplateRef, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { CoreSwipeSlidesItemsManager } from '@classes/items-management/slides-items-manager'; import { CoreSwipeSlidesItemsManager } from '@classes/items-management/swipe-slides-items-manager';
import { IonContent, IonSlides } from '@ionic/angular'; import { IonContent, IonSlides } from '@ionic/angular';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils, VerticalPoint } from '@services/utils/dom';
import { CoreMath } from '@singletons/math';
/** /**
* Helper component to display swipable slides. * Helper component to display swipable slides.
@ -31,7 +32,6 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
@Input() manager?: CoreSwipeSlidesItemsManager<Item>; @Input() manager?: CoreSwipeSlidesItemsManager<Item>;
@Input() options: CoreSwipeSlidesOptions = {}; @Input() options: CoreSwipeSlidesOptions = {};
@Output() onInit = new EventEmitter<CoreSwipeCurrentItemData<Item>>();
@Output() onWillChange = new EventEmitter<CoreSwipeCurrentItemData<Item>>(); @Output() onWillChange = new EventEmitter<CoreSwipeCurrentItemData<Item>>();
@Output() onDidChange = new EventEmitter<CoreSwipeCurrentItemData<Item>>(); @Output() onDidChange = new EventEmitter<CoreSwipeCurrentItemData<Item>>();
@ -57,6 +57,14 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
} }
} }
get items(): Item[] {
return this.manager?.getSource().getItems() || [];
}
get loaded(): boolean {
return !!this.manager?.getSource().isLoaded();
}
/** /**
* Initialize some properties based on the manager. * Initialize some properties based on the manager.
*/ */
@ -66,15 +74,15 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
}); });
// Don't call default callbacks on init, emit our own events instead. // Don't call default callbacks on init, emit our own events instead.
// This is because default callbacks aren't triggered for position 0, and to prevent auto scroll on init. // This is because default callbacks aren't triggered for index 0, and to prevent auto scroll on init.
this.options.runCallbacksOnInit = false; this.options.runCallbacksOnInit = false;
await manager.getSource().waitForInitialized(); await manager.getSource().waitForLoaded();
if (this.options.initialSlide === undefined) { if (this.options.initialSlide === undefined) {
// Calculate the initial slide. // Calculate the initial slide.
const position = manager.getSource().getInitialPosition(); const index = manager.getSource().getInitialItemIndex();
this.options.initialSlide = position > - 1 ? position : 0; this.options.initialSlide = Math.max(index, 0);
} }
// Emit change events with the initial item. // Emit change events with the initial item.
@ -83,28 +91,23 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
return; return;
} }
// Validate that the initial position is inside the valid range. // Validate that the initial index is inside the valid range.
let initialPosition = this.options.initialSlide as number; const initialIndex = CoreMath.clamp(this.options.initialSlide as number, 0, items.length - 1);
if (initialPosition < 0) {
initialPosition = 0;
} else if (initialPosition >= items.length) {
initialPosition = items.length - 1;
}
const initialItemData = { const initialItemData = {
index: initialPosition, index: initialIndex,
item: items[initialPosition], item: items[initialIndex],
}; };
manager.setSelectedItem(items[initialPosition]); manager.setSelectedItem(items[initialIndex]);
this.onWillChange.emit(initialItemData); this.onWillChange.emit(initialItemData);
this.onDidChange.emit(initialItemData); this.onDidChange.emit(initialItemData);
} }
/** /**
* Slide to a certain position. * Slide to a certain index.
* *
* @param index Position. * @param index Index.
* @param speed Animation speed. * @param speed Animation speed.
* @param runCallbacks Whether to run callbacks. * @param runCallbacks Whether to run callbacks.
*/ */
@ -119,8 +122,8 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
* @param speed Animation speed. * @param speed Animation speed.
* @param runCallbacks Whether to run callbacks. * @param runCallbacks Whether to run callbacks.
*/ */
slideToItem(item: Partial<Item>, speed?: number, runCallbacks?: boolean): void { slideToItem(item: Item, speed?: number, runCallbacks?: boolean): void {
const index = this.manager?.getSource().getItemPosition(item) ?? -1; const index = this.manager?.getSource().getItemIndex(item) ?? -1;
if (index != -1) { if (index != -1) {
this.slides?.slideTo(index, speed, runCallbacks); this.slides?.slideTo(index, speed, runCallbacks);
} }
@ -146,29 +149,20 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
this.slides?.slidePrev(speed, runCallbacks); this.slides?.slidePrev(speed, runCallbacks);
} }
/**
* Get current item.
*
* @return Current item. Undefined if no current item yet.
*/
getCurrentItem(): Item | null {
return this.manager?.getSelectedItem() || null;
}
/** /**
* Called when items list has been updated. * Called when items list has been updated.
* *
* @param items New items. * @param items New items.
*/ */
protected onItemsUpdated(): void { protected onItemsUpdated(): void {
const currentItem = this.getCurrentItem(); const currentItem = this.manager?.getSelectedItem();
if (!currentItem || !this.manager) { if (!currentItem || !this.manager) {
return; return;
} }
// Keep the same slide in case the list has changed. // Keep the same slide in case the list has changed.
const newIndex = this.manager.getSource().getItemPosition(currentItem) ?? -1; const newIndex = this.manager.getSource().getItemIndex(currentItem) ?? -1;
if (newIndex != -1) { if (newIndex != -1) {
this.slides?.slideTo(newIndex, 0, false); this.slides?.slideTo(newIndex, 0, false);
} }
@ -194,7 +188,7 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
// Scroll top. This can be improved in the future to keep the scroll for each slide. // Scroll top. This can be improved in the future to keep the scroll for each slide.
const scrollElement = await this.content?.getScrollElement(); const scrollElement = await this.content?.getScrollElement();
if (!scrollElement || CoreDomUtils.isElementOutsideOfScreen(scrollElement, this.hostElement, 'top')) { if (!scrollElement || CoreDomUtils.isElementOutsideOfScreen(scrollElement, this.hostElement, VerticalPoint.TOP)) {
// Scroll to top. // Scroll to top.
this.hostElement.scrollIntoView({ behavior: 'smooth' }); this.hostElement.scrollIntoView({ behavior: 'smooth' });
} }

View File

@ -27,7 +27,7 @@ import { CoreUserDelegate, CoreUserDelegateService, CoreUserProfileHandlerData }
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreCourses } from '@features/courses/services/courses'; import { CoreCourses } from '@features/courses/services/courses';
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { CoreUserParticipantsSource } from '@features/user/classes/participants-source'; import { CoreUserParticipantsSource } from '@features/user/classes/participants-source';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
@ -227,7 +227,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
/** /**
* Helper to manage swiping within a collection of users. * Helper to manage swiping within a collection of users.
*/ */
class CoreUserSwipeItemsManager extends CoreSwipeItemsManager { class CoreUserSwipeItemsManager extends CoreSwipeNavigationItemsManager {
/** /**
* @inheritdoc * @inheritdoc

View File

@ -772,7 +772,11 @@ export class CoreDomUtilsProvider {
* @param point The point of the element to check. * @param point The point of the element to check.
* @return Whether the element is outside of the viewport. * @return Whether the element is outside of the viewport.
*/ */
isElementOutsideOfScreen(scrollEl: HTMLElement, element: HTMLElement, point: 'top' | 'mid' | 'bottom' = 'mid'): boolean { isElementOutsideOfScreen(
scrollEl: HTMLElement,
element: HTMLElement,
point: VerticalPoint = VerticalPoint.MID,
): boolean {
const elementRect = element.getBoundingClientRect(); const elementRect = element.getBoundingClientRect();
if (!elementRect) { if (!elementRect) {
@ -780,12 +784,18 @@ export class CoreDomUtilsProvider {
} }
let elementPoint: number; let elementPoint: number;
if (point === 'top') { switch (point) {
case VerticalPoint.TOP:
elementPoint = elementRect.top; elementPoint = elementRect.top;
} else if (point === 'bottom') { break;
case VerticalPoint.BOTTOM:
elementPoint = elementRect.bottom; elementPoint = elementRect.bottom;
} else { break;
case VerticalPoint.MID:
elementPoint = Math.round((elementRect.bottom + elementRect.top) / 2); elementPoint = Math.round((elementRect.bottom + elementRect.top) / 2);
break;
} }
const scrollElRect = scrollEl.getBoundingClientRect(); const scrollElRect = scrollEl.getBoundingClientRect();
@ -2098,3 +2108,12 @@ export type PromptButton = Omit<AlertButton, 'handler'> & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
handler?: (value: any, resolve: (value: any) => void, reject: (reason: any) => void) => void; handler?: (value: any, resolve: (value: any) => void, reject: (reason: any) => void) => void;
}; };
/**
* Vertical points for an element.
*/
export enum VerticalPoint {
TOP = 'top',
MID = 'mid',
BOTTOM = 'bottom',
}

View File

@ -389,161 +389,6 @@ 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 {
const dayMoment = moment().year(month.year).month(month.monthNumber - 1);
dayMoment.subtract(1, 'month');
return {
year: dayMoment.year(),
monthNumber: dayMoment.month() + 1,
};
}
/**
* 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 {
const dayMoment = moment().year(month.year).month(month.monthNumber - 1);
dayMoment.add(1, 'month');
return {
year: dayMoment.year(),
monthNumber: dayMoment.month() + 1,
};
}
/**
* Get current month.
*
* @return Current month.
*/
getCurrentMonth(): YearAndMonth {
const now = new Date();
return {
year: now.getFullYear(),
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);
}
/**
* @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;
}
/**
* Given a day data, return the previous day.
*
* @param day Day.
* @return Previous day.
*/
getPreviousDay(day: YearMonthDay): YearMonthDay {
const dayMoment = moment().year(day.year).month(day.monthNumber - 1).date(day.dayNumber);
dayMoment.subtract(1, 'day');
return {
year: dayMoment.year(),
monthNumber: dayMoment.month() + 1,
dayNumber: dayMoment.date(),
};
}
/**
* Given a day data, return the next day.
*
* @param day Day.
* @return Next day.
*/
getNextDay(day: YearMonthDay): YearMonthDay {
const dayMoment = moment().year(day.year).month(day.monthNumber - 1).date(day.dayNumber);
dayMoment.add(1, 'day');
return {
year: dayMoment.year(),
monthNumber: dayMoment.month() + 1,
dayNumber: dayMoment.date(),
};
}
/**
* Get current day.
*
* @return Current day.
*/
getCurrentDay(): YearMonthDay {
const now = new Date();
return {
year: now.getFullYear(),
monthNumber: now.getMonth() + 1,
dayNumber: now.getDate(),
};
}
/**
* Check if a certain day is a past day.
*
* @param day Day.
* @return Whether it's a past day.
*/
isPastDay(day: YearMonthDay): boolean {
const now = new Date();
return day.year < now.getFullYear() || (day.year === now.getFullYear() && day.monthNumber < now.getMonth() + 1) ||
(day.year === now.getFullYear() && day.monthNumber === now.getMonth() + 1 && day.dayNumber < now.getDate());
}
/**
* Check if two days are the same.
*
* @param dayA Day A.
* @param dayB Day B.
* @return Whether it's same day.
*/
isSameDay(dayA: YearMonthDay, dayB: YearMonthDay): boolean {
return dayA.dayNumber === dayB.dayNumber && dayA.monthNumber === dayB.monthNumber && dayA.year === dayB.year;
}
} }
export const CoreTimeUtils = makeSingleton(CoreTimeUtilsProvider); export const CoreTimeUtils = makeSingleton(CoreTimeUtilsProvider);
/**
* Data to identify a month.
*/
export type YearAndMonth = {
year: number; // Year number.
monthNumber: number; // Month number (1 to 12).
};
/**
* Data to identify a day.
*/
export type YearMonthDay = YearAndMonth & {
dayNumber: number; // Day number.
};