commit
efece1d4ac
|
@ -13,13 +13,13 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
|
import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source';
|
||||||
import { AddonBadges, AddonBadgesUserBadge } from '../services/badges';
|
import { AddonBadges, AddonBadgesUserBadge } from '../services/badges';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a collection of user badges.
|
* Provides a collection of user badges.
|
||||||
*/
|
*/
|
||||||
export class AddonBadgesUserBadgesSource extends CoreItemsManagerSource<AddonBadgesUserBadge> {
|
export class AddonBadgesUserBadgesSource extends CoreRoutedItemsManagerSource<AddonBadgesUserBadge> {
|
||||||
|
|
||||||
readonly COURSE_ID: number;
|
readonly COURSE_ID: number;
|
||||||
readonly USER_ID: number;
|
readonly USER_ID: number;
|
||||||
|
|
|
@ -23,9 +23,9 @@ 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 { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
|
||||||
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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays the list of calendar events.
|
* Page that displays the list of calendar events.
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -61,8 +61,11 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
|
||||||
this.badgeLoaded = true;
|
this.badgeLoaded = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(AddonBadgesUserBadgesSource, [this.courseId, this.userId]);
|
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
this.badges = new CoreSwipeItemsManager(source);
|
AddonBadgesUserBadgesSource,
|
||||||
|
[this.courseId, this.userId],
|
||||||
|
);
|
||||||
|
this.badges = new CoreSwipeNavigationItemsManager(source);
|
||||||
|
|
||||||
this.badges.start();
|
this.badges.start();
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||||
import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-source';
|
import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-source';
|
||||||
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays the list of calendar events.
|
* Page that displays the list of calendar events.
|
||||||
|
@ -49,7 +49,7 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.badges = new CoreListItemsManager(
|
this.badges = new CoreListItemsManager(
|
||||||
CoreItemsManagerSourcesTracker.getOrCreateSource(AddonBadgesUserBadgesSource, [courseId, userId]),
|
CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(AddonBadgesUserBadgesSource, [courseId, userId]),
|
||||||
AddonBadgesUserBadgesPage,
|
AddonBadgesUserBadgesPage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -17,7 +17,11 @@
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-col>
|
</ion-col>
|
||||||
<ion-col class="ion-text-center addon-calendar-period">
|
<ion-col class="ion-text-center addon-calendar-period">
|
||||||
<h2 id="addon-calendar-monthname">{{ periodName }}</h2>
|
<h2 id="addon-calendar-monthname">
|
||||||
|
{{ periodName }}
|
||||||
|
<ion-spinner *ngIf="!selectedMonthLoaded()" class="addon-calendar-loading-month">
|
||||||
|
</ion-spinner>
|
||||||
|
</h2>
|
||||||
</ion-col>
|
</ion-col>
|
||||||
<ion-col class="ion-text-end" *ngIf="canNavigate">
|
<ion-col class="ion-text-end" *ngIf="canNavigate">
|
||||||
<ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'core.next' | translate">
|
<ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'core.next' | translate">
|
||||||
|
@ -27,74 +31,81 @@
|
||||||
</ion-row>
|
</ion-row>
|
||||||
</ion-grid>
|
</ion-grid>
|
||||||
|
|
||||||
<!-- Calendar view. -->
|
<core-swipe-slides [manager]="manager">
|
||||||
<ion-grid class="addon-calendar-months" role="table" aria-describedby="addon-calendar-monthname">
|
<ng-template let-month="item">
|
||||||
<div role="rowgroup">
|
<!-- Calendar view. -->
|
||||||
<!-- List of days. -->
|
<ion-grid class="addon-calendar-months" role="table" aria-describedby="addon-calendar-monthname">
|
||||||
<ion-row role="row">
|
<div role="rowgroup">
|
||||||
<ion-col class="ion-text-center addon-calendar-weekday" *ngFor="let day of weekDays" role="columnheader">
|
<!-- List of days. -->
|
||||||
<span class="sr-only">{{ day.fullname | translate }}</span>
|
<ion-row role="row">
|
||||||
<span class="ion-hide-md-up" aria-hidden="true">{{ day.shortname | translate }}</span>
|
<ion-col class="ion-text-center addon-calendar-weekday" *ngFor="let day of month.weekDays" role="columnheader">
|
||||||
<span class="ion-hide-md-down" aria-hidden="true">{{ day.fullname | translate }}</span>
|
<span class="sr-only">{{ day.fullname | translate }}</span>
|
||||||
</ion-col>
|
<span class="ion-hide-md-up" aria-hidden="true">{{ day.shortname | translate }}</span>
|
||||||
</ion-row>
|
<span class="ion-hide-md-down" aria-hidden="true">{{ day.fullname | translate }}</span>
|
||||||
</div>
|
</ion-col>
|
||||||
<div role="rowgroup">
|
</ion-row>
|
||||||
|
</div>
|
||||||
|
<div role="rowgroup">
|
||||||
|
<!-- Weeks. -->
|
||||||
|
<ion-row *ngFor="let week of month.weeks" class="addon-calendar-week" role="row">
|
||||||
|
<!-- Empty slots (first week). -->
|
||||||
|
<ion-col *ngFor="let value of week.prepadding" class="dayblank addon-calendar-day" role="cell">
|
||||||
|
</ion-col>
|
||||||
|
<ion-col *ngFor="let day of week.days" class="addon-calendar-day ion-text-center" [ngClass]='{
|
||||||
|
"hasevents": day.hasevents,
|
||||||
|
"today": month.isCurrentMonth && day.istoday,
|
||||||
|
"weekend": day.isweekend,
|
||||||
|
"duration_finish": day.haslastdayofevent
|
||||||
|
}' [class.addon-calendar-event-past-day]="month.isPastMonth || day.ispast" role="cell" tabindex="0"
|
||||||
|
(ariaButtonClick)="dayClicked(day.mday)">
|
||||||
|
<p class="addon-calendar-day-number" role="button">
|
||||||
|
<span aria-hidden="true">{{ day.mday }}</span>
|
||||||
|
<span class="sr-only">{{ day.periodName | translate }}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- Weeks. -->
|
<!-- In phone, display some dots to indicate the type of events. -->
|
||||||
<ion-row *ngFor="let week of weeks" class="addon-calendar-week" role="row">
|
<p class="ion-hide-md-up addon-calendar-dot-types"><span *ngFor="let type of day.calendareventtypes"
|
||||||
<!-- Empty slots (first week). -->
|
class="calendar_event_type calendar_event_{{type}}"></span></p>
|
||||||
<ion-col *ngFor="let value of week.prepadding" class="dayblank addon-calendar-day" role="cell"></ion-col>
|
|
||||||
<ion-col *ngFor="let day of week.days" class="addon-calendar-day ion-text-center" [ngClass]='{
|
|
||||||
"hasevents": day.hasevents,
|
|
||||||
"today": isCurrentMonth && day.istoday,
|
|
||||||
"weekend": day.isweekend,
|
|
||||||
"duration_finish": day.haslastdayofevent
|
|
||||||
}' [class.addon-calendar-event-past-day]="isPastMonth || day.ispast" role="cell" tabindex="0"
|
|
||||||
(ariaButtonClick)="dayClicked(day.mday)">
|
|
||||||
<p class="addon-calendar-day-number" role="button">
|
|
||||||
<span aria-hidden="true">{{ day.mday }}</span>
|
|
||||||
<span class="sr-only">{{ day.periodName | translate }}</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- In phone, display some dots to indicate the type of events. -->
|
<!-- In tablet, display list of events. -->
|
||||||
<p class="ion-hide-md-up addon-calendar-dot-types"><span *ngFor="let type of day.calendareventtypes"
|
<div class="ion-hide-md-down addon-calendar-day-events" *ngIf="day.filteredEvents">
|
||||||
class="calendar_event_type calendar_event_{{type}}"></span></p>
|
<ng-container *ngFor="let event of day.filteredEvents | slice:0:4; let index = index">
|
||||||
|
<div *ngIf="index < 3 || day.filteredEvents.length == 4" class="addon-calendar-event"
|
||||||
<!-- In tablet, display list of events. -->
|
[class.addon-calendar-event-past]="event.ispast" role="button" tabindex="0"
|
||||||
<div class="ion-hide-md-down addon-calendar-day-events">
|
(ariaButtonClick)="eventClicked(event, $event)">
|
||||||
<ng-container *ngFor="let event of day.filteredEvents | slice:0:4; let index = index">
|
<span class="calendar_event_type calendar_event_{{event.formattedType}}"></span>
|
||||||
<div *ngIf="index < 3 || day.filteredEvents.length == 4" class="addon-calendar-event"
|
<ion-icon *ngIf="event.offline && !event.deleted" name="fas-clock"
|
||||||
[class.addon-calendar-event-past]="event.ispast" role="button" tabindex="0"
|
[attr.aria-label]="'core.notsent' | translate"></ion-icon>
|
||||||
(ariaButtonClick)="eventClicked(event, $event)">
|
<ion-icon *ngIf="event.deleted" name="fas-trash"
|
||||||
<span class="calendar_event_type calendar_event_{{event.formattedType}}"></span>
|
[attr.aria-label]="'core.deletedoffline' | translate"></ion-icon>
|
||||||
<ion-icon *ngIf="event.offline && !event.deleted" name="fas-clock"
|
<span class="addon-calendar-event-time">
|
||||||
[attr.aria-label]="'core.notsent' | translate"></ion-icon>
|
{{ event.timestart * 1000 | coreFormatDate: timeFormat }}
|
||||||
<ion-icon *ngIf="event.deleted" name="fas-trash" [attr.aria-label]="'core.deletedoffline' | translate">
|
</span>
|
||||||
</ion-icon>
|
<core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" [showAlt]="false"
|
||||||
<span class="addon-calendar-event-time">
|
[modname]="event.modulename" [componentId]="event.instance">
|
||||||
{{ event.timestart * 1000 | coreFormatDate: timeFormat }}
|
</core-mod-icon>
|
||||||
</span>
|
<!-- Add the icon title so accessibility tools read it. -->
|
||||||
<core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" [showAlt]="false"
|
<span class="sr-only">
|
||||||
[modname]="event.modulename" [componentId]="event.instance">
|
{{ 'addon.calendar.type' + event.formattedType | translate }}
|
||||||
</core-mod-icon>
|
<span class="sr-only" *ngIf="event.moduleIcon && event.iconTitle">
|
||||||
<!-- Add the icon title so accessibility tools read it. -->
|
{{ event.iconTitle }}
|
||||||
<span class="sr-only">
|
</span>
|
||||||
{{ 'addon.calendar.type' + event.formattedType | translate }}
|
</span>
|
||||||
<span class="sr-only" *ngIf="event.moduleIcon && event.iconTitle">{{ event.iconTitle }}</span>
|
<span class="addon-calendar-event-name">{{event.name}}</span>
|
||||||
</span>
|
</div>
|
||||||
<span class="addon-calendar-event-name">{{event.name}}</span>
|
</ng-container>
|
||||||
|
<p *ngIf="day.filteredEvents.length > 4" class="addon-calendar-day-more">
|
||||||
|
<b>{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}</b>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ion-col>
|
||||||
<p *ngIf="day.filteredEvents.length > 4" class="addon-calendar-day-more">
|
<!-- Empty slots (last week). -->
|
||||||
<b>{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}</b>
|
<ion-col *ngFor="let value of week.postpadding" class="dayblank addon-calendar-day" role="cell">
|
||||||
</p>
|
</ion-col>
|
||||||
</div>
|
</ion-row>
|
||||||
</ion-col>
|
</div>
|
||||||
<!-- Empty slots (last week). -->
|
</ion-grid>
|
||||||
<ion-col *ngFor="let value of week.postpadding" class="dayblank addon-calendar-day" role="cell"></ion-col>
|
</ng-template>
|
||||||
</ion-row>
|
</core-swipe-slides>
|
||||||
</div>
|
|
||||||
</ion-grid>
|
|
||||||
|
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
|
@ -98,6 +98,10 @@
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.addon-calendar-loading-month {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.addon-calendar-weekday {
|
.addon-calendar-weekday {
|
||||||
|
@ -154,6 +158,14 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-slide {
|
||||||
|
display: block;
|
||||||
|
font-size: inherit;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: start;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context([dir=rtl]) {
|
:host-context([dir=rtl]) {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
KeyValueDiffers,
|
KeyValueDiffers,
|
||||||
KeyValueDiffer,
|
KeyValueDiffer,
|
||||||
|
ViewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
|
@ -41,6 +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 { CoreSwipeSlidesComponent } from '@components/swipe-slides/swipe-slides';
|
||||||
|
import {
|
||||||
|
CoreSwipeSlidesDynamicItem,
|
||||||
|
CoreSwipeSlidesDynamicItemsManagerSource,
|
||||||
|
} from '@classes/items-management/swipe-slides-dynamic-items-manager-source';
|
||||||
|
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.
|
||||||
|
@ -52,6 +60,8 @@ import { CoreLocalNotifications } from '@services/local-notifications';
|
||||||
})
|
})
|
||||||
export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestroy {
|
export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestroy {
|
||||||
|
|
||||||
|
@ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent<PreloadedMonth>;
|
||||||
|
|
||||||
@Input() initialYear?: number; // Initial year to load.
|
@Input() initialYear?: number; // Initial year to load.
|
||||||
@Input() initialMonth?: number; // Initial month to load.
|
@Input() initialMonth?: number; // Initial month to load.
|
||||||
@Input() filter?: AddonCalendarFilter; // Filter to apply.
|
@Input() filter?: AddonCalendarFilter; // Filter to apply.
|
||||||
|
@ -61,28 +71,15 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
||||||
@Output() onDayClicked = new EventEmitter<{day: number; month: number; year: number}>();
|
@Output() onDayClicked = new EventEmitter<{day: number; month: number; year: number}>();
|
||||||
|
|
||||||
periodName?: string;
|
periodName?: string;
|
||||||
weekDays: AddonCalendarWeekDaysTranslationKeys[] = [];
|
manager?: CoreSwipeSlidesDynamicItemsManager<PreloadedMonth, AddonCalendarMonthSlidesItemsManagerSource>;
|
||||||
weeks: AddonCalendarWeek[] = [];
|
|
||||||
loaded = false;
|
loaded = false;
|
||||||
timeFormat?: string;
|
|
||||||
isCurrentMonth = false;
|
|
||||||
isPastMonth = false;
|
|
||||||
|
|
||||||
protected year: number;
|
|
||||||
protected month: number;
|
|
||||||
protected categoriesRetrieved = false;
|
|
||||||
protected categories: { [id: number]: CoreCategoryData } = {};
|
|
||||||
protected currentSiteId: string;
|
protected currentSiteId: string;
|
||||||
protected offlineEvents: { [monthId: string]: { [day: number]: AddonCalendarEventToDisplay[] } } =
|
|
||||||
{}; // Offline events classified in month & day.
|
|
||||||
|
|
||||||
protected offlineEditedEventsIds: number[] = []; // IDs of events edited in offline.
|
|
||||||
protected deletedEvents: number[] = []; // Events deleted in offline.
|
|
||||||
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,
|
||||||
|
@ -92,9 +89,15 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
||||||
if (CoreLocalNotifications.isAvailable()) {
|
if (CoreLocalNotifications.isAvailable()) {
|
||||||
// Re-schedule events if default time changes.
|
// Re-schedule events if default time changes.
|
||||||
this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
|
this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
|
||||||
this.weeks.forEach((week) => {
|
this.manager?.getSource().getItems()?.forEach((month) => {
|
||||||
week.days.forEach((day) => {
|
if (!month.loaded) {
|
||||||
AddonCalendar.scheduleEventsNotifications(day.eventsFormated || []);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
month.weeks?.forEach((week) => {
|
||||||
|
week.days.forEach((day) => {
|
||||||
|
AddonCalendar.scheduleEventsNotifications(day.eventsFormated || []);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, this.currentSiteId);
|
}, this.currentSiteId);
|
||||||
|
@ -112,30 +115,35 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
||||||
this.undeleteEvent(data.eventId);
|
this.undeleteEvent(data.eventId);
|
||||||
|
|
||||||
// Remove it from the list of deleted events if it's there.
|
// Remove it from the list of deleted events if it's there.
|
||||||
const index = this.deletedEvents.indexOf(data.eventId);
|
const index = this.manager?.getSource().deletedEvents.indexOf(data.eventId) ?? -1;
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
this.deletedEvents.splice(index, 1);
|
this.manager?.getSource().deletedEvents.splice(index, 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
this.currentSiteId,
|
this.currentSiteId,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.differ = differs.find([]).create();
|
this.differ = differs.find([]).create();
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
this.year = now.getFullYear();
|
|
||||||
this.month = now.getMonth() + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component loaded.
|
* Component loaded.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.year = this.initialYear ? this.initialYear : this.year;
|
this.canNavigate = typeof this.canNavigate == 'undefined' ? true : CoreUtils.isTrueOrOne(this.canNavigate);
|
||||||
this.month = this.initialMonth ? this.initialMonth : this.month;
|
this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true :
|
||||||
|
CoreUtils.isTrueOrOne(this.displayNavButtons);
|
||||||
|
|
||||||
this.calculateIsCurrentMonth();
|
const source = new AddonCalendarMonthSlidesItemsManagerSource(this, moment({
|
||||||
|
year: this.initialYear,
|
||||||
|
month: this.initialMonth ? this.initialMonth - 1 : undefined,
|
||||||
|
}));
|
||||||
|
this.manager = new CoreSwipeSlidesDynamicItemsManager(source);
|
||||||
|
this.managerUnsubscribe = this.manager.addListener({
|
||||||
|
onSelectedItemUpdated: (item) => {
|
||||||
|
this.onMonthViewed(item);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
}
|
}
|
||||||
|
@ -144,60 +152,35 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
||||||
* Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays).
|
* Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays).
|
||||||
*/
|
*/
|
||||||
ngDoCheck(): void {
|
ngDoCheck(): void {
|
||||||
this.canNavigate = typeof this.canNavigate == 'undefined' ? true : CoreUtils.isTrueOrOne(this.canNavigate);
|
const items = this.manager?.getSource().getItems();
|
||||||
this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true :
|
|
||||||
CoreUtils.isTrueOrOne(this.displayNavButtons);
|
|
||||||
|
|
||||||
if (this.weeks) {
|
if (items?.length) {
|
||||||
// Check if there's any change in the filter object.
|
// Check if there's any change in the filter object.
|
||||||
const changes = this.differ.diff(this.filter || {});
|
const changes = this.differ.diff(this.filter || {});
|
||||||
if (changes) {
|
if (changes) {
|
||||||
this.filterEvents();
|
items.forEach((month) => {
|
||||||
|
if (month.loaded && month.weeks) {
|
||||||
|
this.manager?.getSource().filterEvents(month.weeks, this.filter);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get timeFormat(): string {
|
||||||
|
return this.manager?.getSource().timeFormat || 'core.strftimetime';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch contacts.
|
* Fetch contacts.
|
||||||
*
|
*
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async fetchData(): Promise<void> {
|
async fetchData(): Promise<void> {
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
|
|
||||||
promises.push(this.loadCategories());
|
|
||||||
|
|
||||||
// Get offline events.
|
|
||||||
promises.push(AddonCalendarOffline.getAllEditedEvents().then((events) => {
|
|
||||||
// Classify them by month.
|
|
||||||
this.offlineEvents = AddonCalendarHelper.classifyIntoMonths(events);
|
|
||||||
|
|
||||||
// Get the IDs of events edited in offline.
|
|
||||||
const filtered = events.filter((event) => event.id > 0);
|
|
||||||
this.offlineEditedEventsIds = filtered.map((event) => event.id);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Get events deleted in offline.
|
|
||||||
promises.push(AddonCalendarOffline.getAllDeletedEventsIds().then((ids) => {
|
|
||||||
this.deletedEvents = ids;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Get time format to use.
|
|
||||||
promises.push(AddonCalendar.getCalendarTimeFormat().then((value) => {
|
|
||||||
this.timeFormat = value;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(promises);
|
await this.manager?.getSource().fetchData();
|
||||||
|
|
||||||
await this.fetchEvents();
|
|
||||||
|
|
||||||
|
await this.manager?.getSource().load(this.manager?.getSelectedItem());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
||||||
}
|
}
|
||||||
|
@ -206,115 +189,16 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the events for current month.
|
* Update data related to month being viewed.
|
||||||
*
|
*
|
||||||
* @return Promise resolved when done.
|
* @param month Month being viewed.
|
||||||
*/
|
*/
|
||||||
async fetchEvents(): Promise<void> {
|
onMonthViewed(month: MonthBasicData): void {
|
||||||
// Don't pass courseId and categoryId, we'll filter them locally.
|
|
||||||
let result: { daynames: Partial<AddonCalendarDayName>[]; weeks: Partial<AddonCalendarWeek>[] };
|
|
||||||
try {
|
|
||||||
result = await AddonCalendar.getMonthlyEvents(this.year, this.month);
|
|
||||||
} catch (error) {
|
|
||||||
if (!CoreApp.isOnline()) {
|
|
||||||
// Allow navigating to non-cached months in offline (behave as if using emergency cache).
|
|
||||||
result = await AddonCalendarHelper.getOfflineMonthWeeks(this.year, this.month);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(this.year, this.month - 1).getTime(),
|
month.moment.unix() * 1000,
|
||||||
'core.strftimemonthyear',
|
'core.strftimemonthyear',
|
||||||
);
|
);
|
||||||
this.weekDays = AddonCalendar.getWeekDays(result.daynames[0].dayno);
|
|
||||||
this.weeks = result.weeks as AddonCalendarWeek[];
|
|
||||||
this.calculateIsCurrentMonth();
|
|
||||||
|
|
||||||
await Promise.all(this.weeks.map(async (week) => {
|
|
||||||
await Promise.all(week.days.map(async (day) => {
|
|
||||||
day.periodName = CoreTimeUtils.userDate(
|
|
||||||
new Date(this.year, this.month - 1, day.mday).getTime(),
|
|
||||||
'core.strftimedaydate',
|
|
||||||
);
|
|
||||||
day.eventsFormated = day.eventsFormated || [];
|
|
||||||
day.filteredEvents = day.filteredEvents || [];
|
|
||||||
// Format online events.
|
|
||||||
const onlineEventsFormatted = await Promise.all(
|
|
||||||
day.events.map(async (event) => AddonCalendarHelper.formatEventData(event)),
|
|
||||||
);
|
|
||||||
|
|
||||||
day.eventsFormated = day.eventsFormated.concat(onlineEventsFormatted);
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (this.isCurrentMonth) {
|
|
||||||
const currentDay = new Date().getDate();
|
|
||||||
let isPast = true;
|
|
||||||
|
|
||||||
this.weeks.forEach((week) => {
|
|
||||||
week.days.forEach((day) => {
|
|
||||||
day.istoday = day.mday == currentDay;
|
|
||||||
day.ispast = isPast && !day.istoday;
|
|
||||||
isPast = day.ispast;
|
|
||||||
|
|
||||||
if (day.istoday) {
|
|
||||||
day.eventsFormated?.forEach((event) => {
|
|
||||||
event.ispast = this.isEventPast(event);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Merge the online events with offline data.
|
|
||||||
this.mergeEvents();
|
|
||||||
// Filter events by course.
|
|
||||||
this.filterEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load categories to be able to filter events.
|
|
||||||
*
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
protected async loadCategories(): Promise<void> {
|
|
||||||
if (this.categoriesRetrieved) {
|
|
||||||
// Already retrieved, stop.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cats = await CoreCourses.getCategories(0, true);
|
|
||||||
this.categoriesRetrieved = true;
|
|
||||||
this.categories = {};
|
|
||||||
|
|
||||||
// Index categories by ID.
|
|
||||||
cats.forEach((category) => {
|
|
||||||
this.categories[category.id] = category;
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// Ignore errors.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter events based on the filter popover.
|
|
||||||
*/
|
|
||||||
filterEvents(): void {
|
|
||||||
this.weeks.forEach((week) => {
|
|
||||||
week.days.forEach((day) => {
|
|
||||||
day.filteredEvents = AddonCalendarHelper.getFilteredEvents(
|
|
||||||
day.eventsFormated || [],
|
|
||||||
this.filter,
|
|
||||||
this.categories,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Re-calculate some properties.
|
|
||||||
AddonCalendarHelper.calculateDayData(day, day.filteredEvents);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -323,55 +207,30 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
||||||
* @param afterChange Whether the refresh is done after an event has changed or has been synced.
|
* @param afterChange Whether the refresh is done after an event has changed or has been synced.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async refreshData(afterChange?: boolean): Promise<void> {
|
async refreshData(afterChange = false): Promise<void> {
|
||||||
const promises: Promise<void>[] = [];
|
const selectedMonth = this.manager?.getSelectedItem() || null;
|
||||||
|
|
||||||
// Don't invalidate monthly events after a change, it has already been handled.
|
if (afterChange) {
|
||||||
if (!afterChange) {
|
this.manager?.getSource().markAllItemsDirty();
|
||||||
promises.push(AddonCalendar.invalidateMonthlyEvents(this.year, this.month));
|
|
||||||
}
|
}
|
||||||
promises.push(CoreCourses.invalidateCategories(0, true));
|
|
||||||
promises.push(AddonCalendar.invalidateTimeFormat());
|
|
||||||
|
|
||||||
this.categoriesRetrieved = false; // Get categories again.
|
await this.manager?.getSource().invalidateContent(selectedMonth);
|
||||||
|
|
||||||
await Promise.all(promises);
|
await this.fetchData();
|
||||||
|
|
||||||
this.fetchData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load next month.
|
* Load next month.
|
||||||
*/
|
*/
|
||||||
async loadNext(): Promise<void> {
|
loadNext(): void {
|
||||||
this.increaseMonth();
|
this.slides?.slideNext();
|
||||||
|
|
||||||
this.loaded = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.fetchEvents();
|
|
||||||
} catch (error) {
|
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
|
||||||
this.decreaseMonth();
|
|
||||||
}
|
|
||||||
this.loaded = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load previous month.
|
* Load previous month.
|
||||||
*/
|
*/
|
||||||
async loadPrevious(): Promise<void> {
|
loadPrevious(): void {
|
||||||
this.decreaseMonth();
|
this.slides?.slidePrev();
|
||||||
|
|
||||||
this.loaded = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.fetchEvents();
|
|
||||||
} catch (error) {
|
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
|
||||||
this.increaseMonth();
|
|
||||||
}
|
|
||||||
this.loaded = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -391,78 +250,332 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
||||||
* @param day Day.
|
* @param day Day.
|
||||||
*/
|
*/
|
||||||
dayClicked(day: number): void {
|
dayClicked(day: number): void {
|
||||||
this.onDayClicked.emit({ day: day, month: this.month, year: this.year });
|
const selectedMonth = this.manager?.getSelectedItem();
|
||||||
}
|
if (!selectedMonth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
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 now = new Date();
|
|
||||||
|
|
||||||
this.currentTime = CoreTimeUtils.timestamp();
|
|
||||||
|
|
||||||
this.isCurrentMonth = this.year == now.getFullYear() && this.month == now.getMonth() + 1;
|
|
||||||
this.isPastMonth = this.year < now.getFullYear() || (this.year == now.getFullYear() && this.month < now.getMonth() + 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 initialMonth = this.month;
|
const slides = this.slides;
|
||||||
const initialYear = this.year;
|
if (!manager || !slides) {
|
||||||
|
return;
|
||||||
this.month = now.getMonth() + 1;
|
}
|
||||||
this.year = now.getFullYear();
|
|
||||||
|
|
||||||
|
const currentMonth = {
|
||||||
|
moment: moment(),
|
||||||
|
};
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.fetchEvents();
|
// Make sure the day is loaded.
|
||||||
this.isCurrentMonth = true;
|
await manager.getSource().loadItem(currentMonth);
|
||||||
|
|
||||||
|
slides.slideToItem(currentMonth);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
||||||
this.year = initialYear;
|
} finally {
|
||||||
this.month = initialMonth;
|
this.loaded = true;
|
||||||
}
|
|
||||||
|
|
||||||
this.loaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrease the current month.
|
|
||||||
*/
|
|
||||||
protected decreaseMonth(): void {
|
|
||||||
if (this.month === 1) {
|
|
||||||
this.month = 12;
|
|
||||||
this.year--;
|
|
||||||
} else {
|
|
||||||
this.month--;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increase the current month.
|
* Check whether selected month is loaded.
|
||||||
*/
|
*/
|
||||||
protected increaseMonth(): void {
|
selectedMonthLoaded(): boolean {
|
||||||
if (this.month === 12) {
|
return !!this.manager?.getSelectedItem()?.loaded;
|
||||||
this.month = 1;
|
}
|
||||||
this.year++;
|
|
||||||
} else {
|
/**
|
||||||
this.month++;
|
* Check whether selected month is current month.
|
||||||
|
*/
|
||||||
|
selectedMonthIsCurrent(): boolean {
|
||||||
|
return !!this.manager?.getSelectedItem()?.isCurrentMonth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undelete a certain event.
|
||||||
|
*
|
||||||
|
* @param eventId Event ID.
|
||||||
|
*/
|
||||||
|
protected async undeleteEvent(eventId: number): Promise<void> {
|
||||||
|
this.manager?.getSource().getItems()?.some((month) => {
|
||||||
|
if (!month.loaded) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return month.weeks?.some((week) => week.days.some((day) => {
|
||||||
|
const event = day.eventsFormated?.find((event) => event.id == eventId);
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
event.deleted = false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.undeleteEventObserver?.off();
|
||||||
|
this.obsDefaultTimeChange?.off();
|
||||||
|
this.managerUnsubscribe && this.managerUnsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic data to identify a month.
|
||||||
|
*/
|
||||||
|
type MonthBasicData = {
|
||||||
|
moment: moment.Moment;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preloaded month.
|
||||||
|
*/
|
||||||
|
type PreloadedMonth = MonthBasicData & CoreSwipeSlidesDynamicItem & {
|
||||||
|
weekDays?: AddonCalendarWeekDaysTranslationKeys[];
|
||||||
|
weeks?: AddonCalendarWeek[];
|
||||||
|
isCurrentMonth?: boolean;
|
||||||
|
isPastMonth?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within months.
|
||||||
|
*/
|
||||||
|
class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicItemsManagerSource<PreloadedMonth> {
|
||||||
|
|
||||||
|
categories?: { [id: number]: CoreCategoryData };
|
||||||
|
// Offline events classified in month & day.
|
||||||
|
offlineEvents: { [monthId: string]: { [day: number]: AddonCalendarEventToDisplay[] } } = {};
|
||||||
|
offlineEditedEventsIds: number[] = []; // IDs of events edited in offline.
|
||||||
|
deletedEvents: number[] = []; // Events deleted in offline.
|
||||||
|
timeFormat?: string;
|
||||||
|
|
||||||
|
protected calendarComponent: AddonCalendarCalendarComponent;
|
||||||
|
|
||||||
|
constructor(component: AddonCalendarCalendarComponent, initialMoment: moment.Moment) {
|
||||||
|
super({ moment: initialMoment });
|
||||||
|
|
||||||
|
this.calendarComponent = component;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async fetchData(): Promise<void> {
|
||||||
|
await Promise.all([
|
||||||
|
this.loadCategories(),
|
||||||
|
this.loadOfflineEvents(),
|
||||||
|
this.loadOfflineDeletedEvents(),
|
||||||
|
this.loadTimeFormat(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter events based on the filter popover.
|
||||||
|
*
|
||||||
|
* @param weeks Weeks with the events to filter.
|
||||||
|
* @param filter Filter to apply.
|
||||||
|
*/
|
||||||
|
filterEvents(weeks: AddonCalendarWeek[], filter?: AddonCalendarFilter): void {
|
||||||
|
weeks.forEach((week) => {
|
||||||
|
week.days.forEach((day) => {
|
||||||
|
day.filteredEvents = AddonCalendarHelper.getFilteredEvents(
|
||||||
|
day.eventsFormated || [],
|
||||||
|
filter,
|
||||||
|
this.categories || {},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Re-calculate some properties.
|
||||||
|
AddonCalendarHelper.calculateDayData(day, day.filteredEvents);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load categories to be able to filter events.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async loadCategories(): Promise<void> {
|
||||||
|
if (this.categories) {
|
||||||
|
// Already retrieved, stop.
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const categories = await CoreCourses.getCategories(0, true);
|
||||||
|
|
||||||
|
// Index categories by ID.
|
||||||
|
this.categories = CoreUtils.arrayToObject(categories, 'id');
|
||||||
|
} catch {
|
||||||
|
// Ignore errors.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load events created or edited in offline.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async loadOfflineEvents(): Promise<void> {
|
||||||
|
// Get offline events.
|
||||||
|
const events = await AddonCalendarOffline.getAllEditedEvents();
|
||||||
|
|
||||||
|
// Classify them by month.
|
||||||
|
this.offlineEvents = AddonCalendarHelper.classifyIntoMonths(events);
|
||||||
|
|
||||||
|
// Get the IDs of events edited in offline.
|
||||||
|
this.offlineEditedEventsIds = events.filter((event) => event.id > 0).map((event) => event.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load events deleted in offline.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async loadOfflineDeletedEvents(): Promise<void> {
|
||||||
|
this.deletedEvents = await AddonCalendarOffline.getAllDeletedEventsIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load time format.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async loadTimeFormat(): Promise<void> {
|
||||||
|
this.timeFormat = await AddonCalendar.getCalendarTimeFormat();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemId(item: MonthBasicData): string | number {
|
||||||
|
return AddonCalendarHelper.getMonthId(item.moment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getPreviousItem(item: MonthBasicData): MonthBasicData | null {
|
||||||
|
return {
|
||||||
|
moment: item.moment.clone().subtract(1, 'month'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getNextItem(item: MonthBasicData): MonthBasicData | null {
|
||||||
|
return {
|
||||||
|
moment: item.moment.clone().add(1, 'month'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async loadItemData(month: MonthBasicData, preload = false): Promise<PreloadedMonth | null> {
|
||||||
|
// Load or preload the weeks.
|
||||||
|
let result: { daynames: Partial<AddonCalendarDayName>[]; weeks: Partial<AddonCalendarWeek>[] };
|
||||||
|
const year = month.moment.year();
|
||||||
|
const monthNumber = month.moment.month() + 1;
|
||||||
|
|
||||||
|
if (preload) {
|
||||||
|
result = await AddonCalendarHelper.getOfflineMonthWeeks(year, monthNumber);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// Don't pass courseId and categoryId, we'll filter them locally.
|
||||||
|
result = await AddonCalendar.getMonthlyEvents(year, monthNumber);
|
||||||
|
} catch (error) {
|
||||||
|
if (!CoreApp.isOnline()) {
|
||||||
|
// Allow navigating to non-cached months in offline (behave as if using emergency cache).
|
||||||
|
result = await AddonCalendarHelper.getOfflineMonthWeeks(year, monthNumber);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const weekDays = AddonCalendar.getWeekDays(result.daynames[0].dayno);
|
||||||
|
const weeks = result.weeks as AddonCalendarWeek[];
|
||||||
|
const currentDay = new Date().getDate();
|
||||||
|
const currentTime = CoreTimeUtils.timestamp();
|
||||||
|
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(week.days.map(async (day) => {
|
||||||
|
day.periodName = CoreTimeUtils.userDate(
|
||||||
|
month.moment.unix() * 1000,
|
||||||
|
'core.strftimedaydate',
|
||||||
|
);
|
||||||
|
day.eventsFormated = day.eventsFormated || [];
|
||||||
|
day.filteredEvents = day.filteredEvents || [];
|
||||||
|
// Format online events.
|
||||||
|
const onlineEventsFormatted = await Promise.all(
|
||||||
|
day.events.map(async (event) => AddonCalendarHelper.formatEventData(event)),
|
||||||
|
);
|
||||||
|
|
||||||
|
day.eventsFormated = day.eventsFormated.concat(onlineEventsFormatted);
|
||||||
|
|
||||||
|
if (preloadedMonth.isCurrentMonth) {
|
||||||
|
day.istoday = day.mday == currentDay;
|
||||||
|
day.ispast = isPast && !day.istoday;
|
||||||
|
isPast = day.ispast;
|
||||||
|
|
||||||
|
if (day.istoday) {
|
||||||
|
day.eventsFormated?.forEach((event) => {
|
||||||
|
event.ispast = this.isEventPast(event, currentTime);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!preload) {
|
||||||
|
// Merge the online events with offline data.
|
||||||
|
this.mergeEvents(month, weeks);
|
||||||
|
// Filter events by course.
|
||||||
|
this.filterEvents(weeks, this.calendarComponent.filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return preloadedMonth;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge online events with the offline events of that period.
|
* Merge online events with the offline events of that period.
|
||||||
|
*
|
||||||
|
* @param month Month.
|
||||||
|
* @param weeks Weeks with the events to filter.
|
||||||
*/
|
*/
|
||||||
protected mergeEvents(): void {
|
mergeEvents(month: MonthBasicData, weeks: AddonCalendarWeek[]): void {
|
||||||
const monthOfflineEvents: { [day: number]: AddonCalendarEventToDisplay[] } =
|
const monthOfflineEvents: { [day: number]: AddonCalendarEventToDisplay[] } =
|
||||||
this.offlineEvents[AddonCalendarHelper.getMonthId(this.year, this.month)];
|
this.offlineEvents[AddonCalendarHelper.getMonthId(month.moment)];
|
||||||
|
|
||||||
this.weeks.forEach((week) => {
|
weeks.forEach((week) => {
|
||||||
week.days.forEach((day) => {
|
week.days.forEach((day) => {
|
||||||
|
|
||||||
// Schedule notifications for the events retrieved (only future events will be scheduled).
|
// Schedule notifications for the events retrieved (only future events will be scheduled).
|
||||||
|
@ -493,43 +606,39 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Undelete a certain event.
|
|
||||||
*
|
|
||||||
* @param eventId Event ID.
|
|
||||||
*/
|
|
||||||
protected undeleteEvent(eventId: number): void {
|
|
||||||
if (!this.weeks) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.weeks.forEach((week) => {
|
|
||||||
week.days.forEach((day) => {
|
|
||||||
const event = day.eventsFormated?.find((event) => event.id == eventId);
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
event.deleted = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns if the event is in the past or not.
|
* Returns if the event is in the past or not.
|
||||||
*
|
*
|
||||||
* @param event Event object.
|
* @param event Event object.
|
||||||
|
* @param currentTime Current time.
|
||||||
* @return True if it's in the past.
|
* @return True if it's in the past.
|
||||||
*/
|
*/
|
||||||
protected isEventPast(event: { timestart: number; timeduration: number}): boolean {
|
isEventPast(event: { timestart: number; timeduration: number}, currentTime: number): boolean {
|
||||||
return (event.timestart + event.timeduration) < (this.currentTime || CoreTimeUtils.timestamp());
|
return (event.timestart + event.timeduration) < currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component destroyed.
|
* Invalidate content.
|
||||||
|
*
|
||||||
|
* @param selectedMonth The current selected month.
|
||||||
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
async invalidateContent(selectedMonth: PreloadedMonth | null): Promise<void> {
|
||||||
this.undeleteEventObserver?.off();
|
const promises: Promise<void>[] = [];
|
||||||
this.obsDefaultTimeChange?.off();
|
|
||||||
|
if (selectedMonth) {
|
||||||
|
promises.push(AddonCalendar.invalidateMonthlyEvents(selectedMonth.moment.year(), selectedMonth.moment.month() + 1));
|
||||||
|
}
|
||||||
|
promises.push(CoreCourses.invalidateCategories(0, true));
|
||||||
|
promises.push(AddonCalendar.invalidateTimeFormat());
|
||||||
|
|
||||||
|
this.categories = undefined; // Get categories again.
|
||||||
|
|
||||||
|
if (selectedMonth) {
|
||||||
|
selectedMonth.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -227,16 +227,12 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On
|
||||||
/**
|
/**
|
||||||
* Refresh events.
|
* Refresh events.
|
||||||
*
|
*
|
||||||
* @param afterChange Whether the refresh is done after an event has changed or has been synced.
|
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async refreshData(afterChange?: boolean): Promise<void> {
|
async refreshData(): Promise<void> {
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
// Don't invalidate upcoming events after a change, it has already been handled.
|
promises.push(AddonCalendar.invalidateAllUpcomingEvents());
|
||||||
if (!afterChange) {
|
|
||||||
promises.push(AddonCalendar.invalidateAllUpcomingEvents());
|
|
||||||
}
|
|
||||||
promises.push(CoreCourses.invalidateCategories(0, true));
|
promises.push(CoreCourses.invalidateCategories(0, true));
|
||||||
promises.push(AddonCalendar.invalidateLookAhead());
|
promises.push(AddonCalendar.invalidateLookAhead());
|
||||||
promises.push(AddonCalendar.invalidateTimeFormat());
|
promises.push(AddonCalendar.invalidateTimeFormat());
|
||||||
|
|
|
@ -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 || !hasOffline || !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>
|
||||||
|
@ -27,70 +27,78 @@
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
</ion-refresher>
|
</ion-refresher>
|
||||||
|
|
||||||
<!-- Period name and arrows to navigate. -->
|
<core-loading [hideUntil]="loaded">
|
||||||
<ion-grid class="ion-no-padding safe-area-padding">
|
<!-- Period name and arrows to navigate. -->
|
||||||
<ion-row class="ion-align-items-center">
|
<ion-grid class="ion-no-padding safe-area-padding">
|
||||||
<ion-col class="ion-text-start" *ngIf="currentMoment">
|
<ion-row class="ion-align-items-center">
|
||||||
<ion-button fill="clear" (click)="loadPrevious()" [attr.aria-label]="'addon.calendar.dayprev' | translate">
|
<ion-col class="ion-text-start">
|
||||||
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
|
<ion-button fill="clear" (click)="loadPrevious()" [attr.aria-label]="'addon.calendar.dayprev' | translate">
|
||||||
</ion-button>
|
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||||
</ion-col>
|
</ion-button>
|
||||||
<ion-col class="ion-text-center addon-calendar-period">
|
</ion-col>
|
||||||
<h3>{{ periodName }}</h3>
|
<ion-col class="ion-text-center addon-calendar-period">
|
||||||
</ion-col>
|
<h3>{{ periodName }}</h3>
|
||||||
<ion-col class="ion-text-end" *ngIf="currentMoment">
|
</ion-col>
|
||||||
<ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'addon.calendar.daynext' | translate">
|
<ion-col class="ion-text-end">
|
||||||
<ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
|
<ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'addon.calendar.daynext' | translate">
|
||||||
</ion-button>
|
<ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||||
</ion-col>
|
</ion-button>
|
||||||
</ion-row>
|
</ion-col>
|
||||||
</ion-grid>
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
|
||||||
<core-loading [hideUntil]="loaded" class="safe-area-padding">
|
<core-swipe-slides [manager]="manager">
|
||||||
<!-- There is data to be synchronized -->
|
<ng-template let-day="item">
|
||||||
<ion-card class="core-warning-card" *ngIf="hasOffline">
|
<core-loading [hideUntil]="day.loaded" class="safe-area-padding">
|
||||||
<ion-item>
|
<!-- There is data to be synchronized -->
|
||||||
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
|
<ion-card class="core-warning-card" *ngIf="day.hasOffline">
|
||||||
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }}</ion-label>
|
<ion-item>
|
||||||
</ion-item>
|
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
|
||||||
</ion-card>
|
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
<core-empty-box *ngIf="!filteredEvents || !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="filteredEvents && filteredEvents.length" class="ion-no-margin">
|
<ion-list *ngIf="day.filteredEvents && day.filteredEvents.length" class="ion-no-margin">
|
||||||
<ng-container *ngFor="let event of filteredEvents">
|
<ng-container *ngFor="let event of day.filteredEvents">
|
||||||
<ion-item class="addon-calendar-event ion-text-wrap" [attr.aria-label]="event.name" (click)="gotoEvent(event.id)"
|
<ion-item class="addon-calendar-event ion-text-wrap" [attr.aria-label]="event.name"
|
||||||
[class.item-dimmed]="event.ispast" [ngClass]="['addon-calendar-eventtype-'+event.eventtype]" button detail="true">
|
(click)="gotoEvent(event.id)" [class.item-dimmed]="event.ispast"
|
||||||
<core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" slot="start" [showAlt]="false"
|
[ngClass]="['addon-calendar-eventtype-'+event.eventtype]" button detail="true">
|
||||||
[modname]="event.modname" [componentId]="event.instance">
|
<core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" slot="start" [showAlt]="false"
|
||||||
</core-mod-icon>
|
[modname]="event.modname" [componentId]="event.instance">
|
||||||
<ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start" aria-hidden="true">
|
</core-mod-icon>
|
||||||
</ion-icon>
|
<ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start"
|
||||||
<ion-label>
|
aria-hidden="true">
|
||||||
<!-- Add the icon title so accessibility tools read it. -->
|
</ion-icon>
|
||||||
<span class="sr-only">
|
<ion-label>
|
||||||
{{ 'addon.calendar.type' + event.formattedType | translate }}
|
<!-- Add the icon title so accessibility tools read it. -->
|
||||||
<span class="sr-only" *ngIf="event.moduleIcon && event.iconTitle">{{ event.iconTitle }}</span>
|
<span class="sr-only">
|
||||||
</span>
|
{{ 'addon.calendar.type' + event.formattedType | translate }}
|
||||||
<p class="item-heading">
|
<span class="sr-only" *ngIf="event.moduleIcon && event.iconTitle">{{ event.iconTitle }}</span>
|
||||||
<core-format-text [text]="event.name" [contextLevel]="event.contextLevel"
|
</span>
|
||||||
[contextInstanceId]="event.contextInstanceId"></core-format-text>
|
<p class="item-heading">
|
||||||
</p>
|
<core-format-text [text]="event.name" [contextLevel]="event.contextLevel"
|
||||||
<p [innerHTML]="event.formattedtime"></p>
|
[contextInstanceId]="event.contextInstanceId"></core-format-text>
|
||||||
</ion-label>
|
</p>
|
||||||
<ion-note *ngIf="event.offline && !event.deleted" slot="end">
|
<p [innerHTML]="event.formattedtime"></p>
|
||||||
<ion-icon name="fas-clock" aria-hidden="true"></ion-icon>
|
</ion-label>
|
||||||
<span class="ion-text-wrap">{{ 'core.notsent' | translate }}</span>
|
<ion-note *ngIf="event.offline && !event.deleted" slot="end">
|
||||||
</ion-note>
|
<ion-icon name="fas-clock" aria-hidden="true"></ion-icon>
|
||||||
<ion-note *ngIf="event.deleted" slot="end">
|
<span class="ion-text-wrap">{{ 'core.notsent' | translate }}</span>
|
||||||
<ion-icon name="fas-trash" aria-hidden="true"></ion-icon>
|
</ion-note>
|
||||||
<span class="ion-text-wrap">{{ 'core.deletedoffline' | translate }}</span>
|
<ion-note *ngIf="event.deleted" slot="end">
|
||||||
</ion-note>
|
<ion-icon name="fas-trash" aria-hidden="true"></ion-icon>
|
||||||
</ion-item>
|
<span class="ion-text-wrap">{{ 'core.deletedoffline' | translate }}</span>
|
||||||
</ng-container>
|
</ion-note>
|
||||||
</ion-list>
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
</ion-list>
|
||||||
|
</core-loading>
|
||||||
|
</ng-template>
|
||||||
|
</core-swipe-slides>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
||||||
<!-- Create a calendar event. -->
|
<!-- Create a calendar event. -->
|
||||||
|
|
|
@ -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, OnInit, OnDestroy } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
import { CoreApp } from '@services/app';
|
import { CoreApp } from '@services/app';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
@ -40,6 +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/swipe-slides-dynamic-items-manager';
|
||||||
|
import { CoreSwipeSlidesComponent } from '@components/swipe-slides/swipe-slides';
|
||||||
|
import {
|
||||||
|
CoreSwipeSlidesDynamicItem,
|
||||||
|
CoreSwipeSlidesDynamicItemsManagerSource,
|
||||||
|
} 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.
|
||||||
|
@ -51,20 +57,9 @@ import { CoreConstants } from '@/core/constants';
|
||||||
})
|
})
|
||||||
export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
protected currentSiteId: string;
|
@ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent<PreloadedDay>;
|
||||||
protected year!: number;
|
|
||||||
protected month!: number;
|
|
||||||
protected day!: number;
|
|
||||||
protected categories: { [id: number]: CoreCategoryData } = {};
|
|
||||||
protected events: AddonCalendarEventToDisplay[] = []; // Events (both online and offline).
|
|
||||||
protected onlineEvents: AddonCalendarEventToDisplay[] = [];
|
|
||||||
protected offlineEvents: { [monthId: string]: { [day: number]: AddonCalendarEventToDisplay[] } } =
|
|
||||||
{}; // Offline events classified in month & day.
|
|
||||||
|
|
||||||
protected offlineEditedEventsIds: number[] = []; // IDs of events edited in offline.
|
protected currentSiteId: string;
|
||||||
protected deletedEvents: number[] = []; // Events deleted in offline.
|
|
||||||
protected timeFormat?: string;
|
|
||||||
protected currentTime!: number;
|
|
||||||
|
|
||||||
// Observers.
|
// Observers.
|
||||||
protected newEventObserver: CoreEventObserver;
|
protected newEventObserver: CoreEventObserver;
|
||||||
|
@ -77,18 +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;
|
||||||
filteredEvents: AddonCalendarEventToDisplay [] = [];
|
manager?: CoreSwipeSlidesDynamicItemsManager<PreloadedDay, AddonCalendarDaySlidesItemsManagerSource>;
|
||||||
canCreate = false;
|
|
||||||
courses: Partial<CoreEnrolledCourseData>[] = [];
|
|
||||||
loaded = false;
|
loaded = false;
|
||||||
hasOffline = false;
|
|
||||||
isOnline = false;
|
isOnline = false;
|
||||||
syncIcon = CoreConstants.ICON_LOADING;
|
syncIcon = CoreConstants.ICON_LOADING;
|
||||||
isCurrentDay = false;
|
|
||||||
isPastDay = false;
|
|
||||||
currentMoment!: moment.Moment;
|
|
||||||
filter: AddonCalendarFilter = {
|
filter: AddonCalendarFilter = {
|
||||||
filtered: false,
|
filtered: false,
|
||||||
courseId: undefined,
|
courseId: undefined,
|
||||||
|
@ -106,7 +96,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
if (CoreLocalNotifications.isAvailable()) {
|
if (CoreLocalNotifications.isAvailable()) {
|
||||||
// Re-schedule events if default time changes.
|
// Re-schedule events if default time changes.
|
||||||
this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
|
this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
|
||||||
AddonCalendar.scheduleEventsNotifications(this.onlineEvents);
|
this.manager?.getSource().getItems()?.forEach(day => {
|
||||||
|
AddonCalendar.scheduleEventsNotifications(day.onlineEvents || []);
|
||||||
|
});
|
||||||
}, this.currentSiteId);
|
}, this.currentSiteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +107,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
AddonCalendarProvider.NEW_EVENT_EVENT,
|
AddonCalendarProvider.NEW_EVENT_EVENT,
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data && data.eventId) {
|
if (data && data.eventId) {
|
||||||
this.loaded = false;
|
this.manager?.getSource().markAllItemsUnloaded();
|
||||||
this.refreshData(true, true);
|
this.refreshData(true, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -124,7 +116,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
// Listen for new event discarded event. When it does, reload the data.
|
// Listen for new event discarded event. When it does, reload the data.
|
||||||
this.discardedObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => {
|
this.discardedObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => {
|
||||||
this.loaded = false;
|
this.manager?.getSource().markAllItemsUnloaded();
|
||||||
this.refreshData(true, true);
|
this.refreshData(true, true);
|
||||||
}, this.currentSiteId);
|
}, this.currentSiteId);
|
||||||
|
|
||||||
|
@ -133,7 +125,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
AddonCalendarProvider.EDIT_EVENT_EVENT,
|
AddonCalendarProvider.EDIT_EVENT_EVENT,
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data && data.eventId) {
|
if (data && data.eventId) {
|
||||||
this.loaded = false;
|
this.manager?.getSource().markAllItemsUnloaded();
|
||||||
this.refreshData(true, true);
|
this.refreshData(true, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -142,14 +134,15 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
// Refresh data if calendar events are synchronized automatically.
|
// Refresh data if calendar events are synchronized automatically.
|
||||||
this.syncObserver = CoreEvents.on(AddonCalendarSyncProvider.AUTO_SYNCED, () => {
|
this.syncObserver = CoreEvents.on(AddonCalendarSyncProvider.AUTO_SYNCED, () => {
|
||||||
this.loaded = false;
|
this.manager?.getSource().markAllItemsUnloaded();
|
||||||
this.refreshData(false, true);
|
this.refreshData(false, true);
|
||||||
}, this.currentSiteId);
|
}, this.currentSiteId);
|
||||||
|
|
||||||
// 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) => {
|
||||||
if (data && (data.source != 'day' || data.year != this.year || data.month != this.month || data.day != this.day)) {
|
const selectedDay = this.manager?.getSelectedItem();
|
||||||
this.loaded = false;
|
if (data && (data.source != 'day' || !selectedDay || !data.moment || !selectedDay.moment.isSame(data.moment, 'day'))) {
|
||||||
|
this.manager?.getSource().markAllItemsUnloaded();
|
||||||
this.refreshData(false, true);
|
this.refreshData(false, true);
|
||||||
}
|
}
|
||||||
}, this.currentSiteId);
|
}, this.currentSiteId);
|
||||||
|
@ -160,10 +153,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data && !data.sent) {
|
if (data && !data.sent) {
|
||||||
// Event was deleted in offline. Just mark it as deleted, no need to refresh.
|
// Event was deleted in offline. Just mark it as deleted, no need to refresh.
|
||||||
this.hasOffline = this.markAsDeleted(data.eventId, true) || this.hasOffline;
|
this.manager?.getSource().markAsDeleted(data.eventId, true);
|
||||||
this.deletedEvents.push(data.eventId);
|
|
||||||
} else {
|
} else {
|
||||||
this.loaded = false;
|
this.manager?.getSource().markAllItemsUnloaded();
|
||||||
this.refreshData(false, true);
|
this.refreshData(false, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -179,26 +171,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark it as undeleted, no need to refresh.
|
// Mark it as undeleted, no need to refresh.
|
||||||
const found = this.markAsDeleted(data.eventId, false);
|
this.manager?.getSource().markAsDeleted(data.eventId, false);
|
||||||
|
|
||||||
// Remove it from the list of deleted events if it's there.
|
|
||||||
const index = this.deletedEvents.indexOf(data.eventId);
|
|
||||||
if (index != -1) {
|
|
||||||
this.deletedEvents.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found) {
|
|
||||||
// The deleted event belongs to current list. Re-calculate "hasOffline".
|
|
||||||
this.hasOffline = false;
|
|
||||||
|
|
||||||
if (this.events.length != this.onlineEvents.length) {
|
|
||||||
this.hasOffline = true;
|
|
||||||
} else {
|
|
||||||
const event = this.events.find((event) => event.deleted || event.offline);
|
|
||||||
|
|
||||||
this.hasOffline = !!event;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
this.currentSiteId,
|
this.currentSiteId,
|
||||||
);
|
);
|
||||||
|
@ -209,9 +182,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
this.filter = data;
|
this.filter = data;
|
||||||
|
|
||||||
// Course viewed has changed, check if the user can create events for this course calendar.
|
// Course viewed has changed, check if the user can create events for this course calendar.
|
||||||
this.canCreate = await AddonCalendarHelper.canEditEvents(this.filter.courseId);
|
await this.manager?.getSource().loadCanCreate(this.filter.courseId);
|
||||||
|
|
||||||
this.filterEvents();
|
this.manager?.getSource().filterAllDayEvents(this.filter);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -240,17 +213,30 @@ 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 now = new Date();
|
const month = CoreNavigator.getRouteNumberParam('month');
|
||||||
this.year = CoreNavigator.getRouteNumberParam('year') || now.getFullYear();
|
const source = new AddonCalendarDaySlidesItemsManagerSource(this, moment({
|
||||||
this.month = CoreNavigator.getRouteNumberParam('month') || (now.getMonth() + 1);
|
year: CoreNavigator.getRouteNumberParam('year'),
|
||||||
this.day = CoreNavigator.getRouteNumberParam('day') || now.getDate();
|
month: month ? month - 1 : undefined,
|
||||||
|
date: CoreNavigator.getRouteNumberParam('day'),
|
||||||
this.calculateCurrentMoment();
|
}));
|
||||||
this.calculateIsCurrentDay();
|
this.manager = new CoreSwipeSlidesDynamicItemsManager(source);
|
||||||
|
this.managerUnsubscribe = this.manager.addListener({
|
||||||
|
onSelectedItemUpdated: (item) => {
|
||||||
|
this.onDayViewed(item);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.fetchData(true);
|
this.fetchData(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canCreate(): boolean {
|
||||||
|
return this.manager?.getSource().canCreate || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get timeFormat(): string {
|
||||||
|
return this.manager?.getSource().timeFormat || 'core.strftimetime';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all the data required for the view.
|
* Fetch all the data required for the view.
|
||||||
*
|
*
|
||||||
|
@ -259,7 +245,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async fetchData(sync?: boolean): Promise<void> {
|
async fetchData(sync?: boolean): Promise<void> {
|
||||||
|
|
||||||
this.syncIcon = CoreConstants.ICON_LOADING;
|
this.syncIcon = CoreConstants.ICON_LOADING;
|
||||||
this.isOnline = CoreApp.isOnline();
|
this.isOnline = CoreApp.isOnline();
|
||||||
|
|
||||||
|
@ -268,53 +253,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const promises: Promise<void>[] = [];
|
await this.manager?.getSource().fetchData(this.filter.courseId);
|
||||||
|
|
||||||
// Load courses for the popover.
|
await this.manager?.getSource().load(this.manager?.getSelectedItem());
|
||||||
promises.push(CoreCoursesHelper.getCoursesForPopover(this.filter.courseId).then((data) => {
|
|
||||||
this.courses = data.courses;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Get categories.
|
|
||||||
promises.push(this.loadCategories());
|
|
||||||
|
|
||||||
// Get offline events.
|
|
||||||
promises.push(AddonCalendarOffline.getAllEditedEvents().then((offlineEvents) => {
|
|
||||||
// Classify them by month & day.
|
|
||||||
this.offlineEvents = AddonCalendarHelper.classifyIntoMonths(offlineEvents);
|
|
||||||
|
|
||||||
// Get the IDs of events edited in offline.
|
|
||||||
this.offlineEditedEventsIds = offlineEvents.filter((event) => event.id > 0).map((event) => event.id);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Get events deleted in offline.
|
|
||||||
promises.push(AddonCalendarOffline.getAllDeletedEventsIds().then((ids) => {
|
|
||||||
this.deletedEvents = ids;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Check if user can create events.
|
|
||||||
promises.push(AddonCalendarHelper.canEditEvents(this.filter.courseId).then((canEdit) => {
|
|
||||||
this.canCreate = canEdit;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Get user preferences.
|
|
||||||
promises.push(AddonCalendar.getCalendarTimeFormat().then((value) => {
|
|
||||||
this.timeFormat = value;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}));
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
|
|
||||||
await this.fetchEvents();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
||||||
}
|
}
|
||||||
|
@ -324,104 +265,15 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the events for current day.
|
* Update data related to day being viewed.
|
||||||
*
|
*
|
||||||
* @return Promise resolved when done.
|
* @param day Day viewed.
|
||||||
*/
|
*/
|
||||||
async fetchEvents(): Promise<void> {
|
onDayViewed(day: DayBasicData): void {
|
||||||
let result: AddonCalendarCalendarDay;
|
|
||||||
try {
|
|
||||||
// Don't pass courseId and categoryId, we'll filter them locally.
|
|
||||||
result = await AddonCalendar.getDayEvents(this.year, this.month, this.day);
|
|
||||||
this.onlineEvents = await Promise.all(result.events.map((event) => AddonCalendarHelper.formatEventData(event)));
|
|
||||||
} catch (error) {
|
|
||||||
if (CoreApp.isOnline()) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
// Allow navigating to non-cached days in offline (behave as if using emergency cache).
|
|
||||||
this.onlineEvents = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(this.year, this.month - 1, this.day).getTime(),
|
day.moment.unix() * 1000,
|
||||||
'core.strftimedaydate',
|
'core.strftimedaydate',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Schedule notifications for the events retrieved (only future events will be scheduled).
|
|
||||||
AddonCalendar.scheduleEventsNotifications(this.onlineEvents);
|
|
||||||
// Merge the online events with offline data.
|
|
||||||
this.events = this.mergeEvents();
|
|
||||||
// Filter events by course.
|
|
||||||
this.filterEvents();
|
|
||||||
this.calculateIsCurrentDay();
|
|
||||||
// Re-calculate the formatted time so it uses the device date.
|
|
||||||
const dayTime = this.currentMoment.unix() * 1000;
|
|
||||||
|
|
||||||
const promises = this.events.map((event) => {
|
|
||||||
event.ispast = this.isPastDay || (this.isCurrentDay && this.isEventPast(event));
|
|
||||||
|
|
||||||
return AddonCalendar.formatEventTime(event, this.timeFormat, true, dayTime).then((time) => {
|
|
||||||
event.formattedtime = time;
|
|
||||||
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge online events with the offline events of that period.
|
|
||||||
*
|
|
||||||
* @return Merged events.
|
|
||||||
*/
|
|
||||||
protected mergeEvents(): AddonCalendarEventToDisplay[] {
|
|
||||||
this.hasOffline = false;
|
|
||||||
|
|
||||||
if (!Object.keys(this.offlineEvents).length && !this.deletedEvents.length) {
|
|
||||||
// No offline events, nothing to merge.
|
|
||||||
return this.onlineEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
const monthOfflineEvents = this.offlineEvents[AddonCalendarHelper.getMonthId(this.year, this.month)];
|
|
||||||
const dayOfflineEvents = monthOfflineEvents && monthOfflineEvents[this.day];
|
|
||||||
let result = this.onlineEvents;
|
|
||||||
|
|
||||||
if (this.deletedEvents.length) {
|
|
||||||
// Mark as deleted the events that were deleted in offline.
|
|
||||||
result.forEach((event) => {
|
|
||||||
event.deleted = this.deletedEvents.indexOf(event.id) != -1;
|
|
||||||
|
|
||||||
if (event.deleted) {
|
|
||||||
this.hasOffline = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.offlineEditedEventsIds.length) {
|
|
||||||
// Remove the online events that were modified in offline.
|
|
||||||
result = result.filter((event) => this.offlineEditedEventsIds.indexOf(event.id) == -1);
|
|
||||||
|
|
||||||
if (result.length != this.onlineEvents.length) {
|
|
||||||
this.hasOffline = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dayOfflineEvents && dayOfflineEvents.length) {
|
|
||||||
// Add the offline events (either new or edited).
|
|
||||||
this.hasOffline = true;
|
|
||||||
result = AddonCalendarHelper.sortEvents(result.concat(dayOfflineEvents));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter events based on the filter popover.
|
|
||||||
*/
|
|
||||||
protected filterEvents(): void {
|
|
||||||
this.filteredEvents = AddonCalendarHelper.getFilteredEvents(this.events, this.filter, this.categories);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -452,37 +304,12 @@ 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 promises: Promise<void>[] = [];
|
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.
|
||||||
if (!afterChange) {
|
await this.manager?.getSource().invalidateContent(selectedDay, !afterChange);
|
||||||
promises.push(AddonCalendar.invalidateDayEvents(this.year, this.month, this.day));
|
|
||||||
}
|
|
||||||
promises.push(AddonCalendar.invalidateAllowedEventTypes());
|
|
||||||
promises.push(CoreCourses.invalidateCategories(0, true));
|
|
||||||
promises.push(AddonCalendar.invalidateTimeFormat());
|
|
||||||
|
|
||||||
await Promise.all(promises).finally(() =>
|
await this.fetchData(sync);
|
||||||
this.fetchData(sync));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load categories to be able to filter events.
|
|
||||||
*
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
protected async loadCategories(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const cats = await CoreCourses.getCategories(0, true);
|
|
||||||
this.categories = {};
|
|
||||||
|
|
||||||
// Index categories by ID.
|
|
||||||
cats.forEach((category) => {
|
|
||||||
this.categories[category.id] = category;
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// Ignore errors.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -501,11 +328,11 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
if (result.updated) {
|
if (result.updated) {
|
||||||
// Trigger a manual sync event.
|
// Trigger a manual sync event.
|
||||||
|
const selectedDay = this.manager?.getSelectedItem();
|
||||||
result.source = 'day';
|
result.source = 'day';
|
||||||
result.day = this.day;
|
result.moment = selectedDay?.moment;
|
||||||
result.month = this.month;
|
|
||||||
result.year = this.year;
|
|
||||||
|
|
||||||
|
this.manager?.getSource().markAllItemsUnloaded();
|
||||||
CoreEvents.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId);
|
CoreEvents.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -515,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.
|
||||||
*
|
*
|
||||||
|
@ -533,7 +367,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
await CoreDomUtils.openPopover({
|
await CoreDomUtils.openPopover({
|
||||||
component: AddonCalendarFilterPopoverComponent,
|
component: AddonCalendarFilterPopoverComponent,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
courses: this.courses,
|
courses: this.manager?.getSource().courses,
|
||||||
filter: this.filter,
|
filter: this.filter,
|
||||||
},
|
},
|
||||||
event,
|
event,
|
||||||
|
@ -551,7 +385,11 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
if (!eventId) {
|
if (!eventId) {
|
||||||
// It's a new event, set the time.
|
// It's a new event, set the time.
|
||||||
eventId = 0;
|
eventId = 0;
|
||||||
params.timestamp = moment().year(this.year).month(this.month - 1).date(this.day).unix() * 1000;
|
|
||||||
|
const selectedDay = this.manager?.getSelectedItem();
|
||||||
|
if (selectedDay) {
|
||||||
|
params.timestamp = selectedDay.moment.unix() * 1000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.filter.courseId) {
|
if (this.filter.courseId) {
|
||||||
|
@ -562,140 +400,55 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate current moment.
|
* Check whether selected day has offline data.
|
||||||
|
*
|
||||||
|
* @return Whether selected day has offline data.
|
||||||
*/
|
*/
|
||||||
calculateCurrentMoment(): void {
|
selectedDayHasOffline(): boolean {
|
||||||
this.currentMoment = moment().year(this.year).month(this.month - 1).date(this.day);
|
const selectedDay = this.manager?.getSelectedItem();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return !!(selectedDay?.hasOffline);
|
||||||
* Check if user is viewing the current day.
|
|
||||||
*/
|
|
||||||
calculateIsCurrentDay(): void {
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
this.currentTime = CoreTimeUtils.timestamp();
|
|
||||||
|
|
||||||
this.isCurrentDay = this.year == now.getFullYear() && this.month == now.getMonth() + 1 && this.day == now.getDate();
|
|
||||||
this.isPastDay = this.year < now.getFullYear() || (this.year == now.getFullYear() && this.month < now.getMonth()) ||
|
|
||||||
(this.year == now.getFullYear() && this.month == now.getMonth() + 1 && this.day < now.getDate());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Go to current day.
|
* Go to current day.
|
||||||
*/
|
*/
|
||||||
async goToCurrentDay(): Promise<void> {
|
async goToCurrentDay(): Promise<void> {
|
||||||
const now = new Date();
|
const manager = this.manager;
|
||||||
const initialDay = this.day;
|
const slides = this.slides;
|
||||||
const initialMonth = this.month;
|
if (!manager || !slides) {
|
||||||
const initialYear = this.year;
|
return;
|
||||||
|
}
|
||||||
this.day = now.getDate();
|
|
||||||
this.month = now.getMonth() + 1;
|
|
||||||
this.year = now.getFullYear();
|
|
||||||
this.calculateCurrentMoment();
|
|
||||||
|
|
||||||
|
const currentDay = {
|
||||||
|
moment: moment(),
|
||||||
|
};
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.fetchEvents();
|
// Make sure the day is loaded.
|
||||||
|
await manager.getSource().loadItem(currentDay);
|
||||||
|
|
||||||
this.isCurrentDay = true;
|
slides.slideToItem(currentDay);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
||||||
|
} finally {
|
||||||
this.year = initialYear;
|
this.loaded = true;
|
||||||
this.month = initialMonth;
|
|
||||||
this.day = initialDay;
|
|
||||||
this.calculateCurrentMoment();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loaded = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load next day.
|
* Load next day.
|
||||||
*/
|
*/
|
||||||
async loadNext(): Promise<void> {
|
async loadNext(): Promise<void> {
|
||||||
this.increaseDay();
|
this.slides?.slideNext();
|
||||||
|
|
||||||
this.loaded = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.fetchEvents();
|
|
||||||
} catch (error) {
|
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
|
||||||
this.decreaseDay();
|
|
||||||
}
|
|
||||||
this.loaded = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load previous day.
|
* Load previous day.
|
||||||
*/
|
*/
|
||||||
async loadPrevious(): Promise<void> {
|
async loadPrevious(): Promise<void> {
|
||||||
this.decreaseDay();
|
this.slides?.slidePrev();
|
||||||
|
|
||||||
this.loaded = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.fetchEvents();
|
|
||||||
} catch (error) {
|
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
|
||||||
this.increaseDay();
|
|
||||||
}
|
|
||||||
this.loaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrease the current day.
|
|
||||||
*/
|
|
||||||
protected decreaseDay(): void {
|
|
||||||
this.currentMoment.subtract(1, 'day');
|
|
||||||
|
|
||||||
this.year = this.currentMoment.year();
|
|
||||||
this.month = this.currentMoment.month() + 1;
|
|
||||||
this.day = this.currentMoment.date();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Increase the current day.
|
|
||||||
*/
|
|
||||||
protected increaseDay(): void {
|
|
||||||
this.currentMoment.add(1, 'day');
|
|
||||||
|
|
||||||
this.year = this.currentMoment.year();
|
|
||||||
this.month = this.currentMoment.month() + 1;
|
|
||||||
this.day = this.currentMoment.date();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find an event and mark it as deleted.
|
|
||||||
*
|
|
||||||
* @param eventId Event ID.
|
|
||||||
* @param deleted Whether to mark it as deleted or not.
|
|
||||||
* @return Whether the event was found.
|
|
||||||
*/
|
|
||||||
protected markAsDeleted(eventId: number, deleted: boolean): boolean {
|
|
||||||
const event = this.onlineEvents.find((event) => event.id == eventId);
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
event.deleted = deleted;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns if the event is in the past or not.
|
|
||||||
*
|
|
||||||
* @param event Event object.
|
|
||||||
* @return True if it's in the past.
|
|
||||||
*/
|
|
||||||
isEventPast(event: AddonCalendarEventToDisplay): boolean {
|
|
||||||
return (event.timestart + event.timeduration) < this.currentTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -712,6 +465,370 @@ 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.
|
||||||
|
*/
|
||||||
|
type PreloadedDay = DayBasicData & CoreSwipeSlidesDynamicItem & {
|
||||||
|
events?: AddonCalendarEventToDisplay[]; // Events (both online and offline).
|
||||||
|
onlineEvents?: AddonCalendarEventToDisplay[];
|
||||||
|
filteredEvents?: AddonCalendarEventToDisplay[];
|
||||||
|
isCurrentDay?: boolean;
|
||||||
|
isPastDay?: boolean;
|
||||||
|
hasOffline?: boolean; // Whether the day has offline data.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within days.
|
||||||
|
*/
|
||||||
|
class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicItemsManagerSource<PreloadedDay> {
|
||||||
|
|
||||||
|
courses: Partial<CoreEnrolledCourseData>[] = [];
|
||||||
|
// Offline events classified in month & day.
|
||||||
|
offlineEvents: Record<string, Record<number, AddonCalendarEventToDisplay[]>> = {};
|
||||||
|
offlineEditedEventsIds: number[] = []; // IDs of events edited in offline.
|
||||||
|
categories?: { [id: number]: CoreCategoryData };
|
||||||
|
deletedEvents?: Set<number>; // Events deleted in offline.
|
||||||
|
timeFormat?: string;
|
||||||
|
canCreate = false;
|
||||||
|
|
||||||
|
protected dayPage: AddonCalendarDayPage;
|
||||||
|
|
||||||
|
constructor(page: AddonCalendarDayPage, initialMoment: moment.Moment) {
|
||||||
|
super({ moment: initialMoment });
|
||||||
|
|
||||||
|
this.dayPage = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data.
|
||||||
|
*
|
||||||
|
* @param courseId Current selected course id (if any).
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async fetchData(courseId?: number): Promise<void> {
|
||||||
|
await Promise.all([
|
||||||
|
this.loadCourses(courseId),
|
||||||
|
this.loadCanCreate(courseId),
|
||||||
|
this.loadCategories(),
|
||||||
|
this.loadOfflineEvents(),
|
||||||
|
this.loadOfflineDeletedEvents(),
|
||||||
|
this.loadTimeFormat(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter all loaded days events based on the filter popover.
|
||||||
|
*
|
||||||
|
* @param filter Filter to apply.
|
||||||
|
*/
|
||||||
|
filterAllDayEvents(filter: AddonCalendarFilter): void {
|
||||||
|
this.getItems()?.forEach(day => this.filterEvents(day, filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter events of a certain day based on the filter popover.
|
||||||
|
*
|
||||||
|
* @param day Day with the events.
|
||||||
|
* @param filter Filter to apply.
|
||||||
|
*/
|
||||||
|
filterEvents(day: PreloadedDay, filter: AddonCalendarFilter): void {
|
||||||
|
day.filteredEvents = AddonCalendarHelper.getFilteredEvents(day.events || [], filter, this.categories || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load courses.
|
||||||
|
*
|
||||||
|
* @param courseId Current selected course id (if any).
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async loadCourses(courseId?: number): Promise<void> {
|
||||||
|
const data = await CoreCoursesHelper.getCoursesForPopover(courseId);
|
||||||
|
|
||||||
|
this.courses = data.courses;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load whether user can create events.
|
||||||
|
*
|
||||||
|
* @param courseId Current selected course id (if any).
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async loadCanCreate(courseId?: number): Promise<void> {
|
||||||
|
this.canCreate = await AddonCalendarHelper.canEditEvents(courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load categories to be able to filter events.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async loadCategories(): Promise<void> {
|
||||||
|
if (this.categories) {
|
||||||
|
// Already retrieved, stop.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const categories = await CoreCourses.getCategories(0, true);
|
||||||
|
|
||||||
|
// Index categories by ID.
|
||||||
|
this.categories = CoreUtils.arrayToObject(categories, 'id');
|
||||||
|
} catch {
|
||||||
|
// Ignore errors.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load events created or edited in offline.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async loadOfflineEvents(): Promise<void> {
|
||||||
|
// Get offline events.
|
||||||
|
const events = await AddonCalendarOffline.getAllEditedEvents();
|
||||||
|
|
||||||
|
// Classify them by month & day.
|
||||||
|
this.offlineEvents = AddonCalendarHelper.classifyIntoMonths(events);
|
||||||
|
|
||||||
|
// Get the IDs of events edited in offline.
|
||||||
|
this.offlineEditedEventsIds = events.filter((event) => event.id > 0).map((event) => event.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load events deleted in offline.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async loadOfflineDeletedEvents(): Promise<void> {
|
||||||
|
const deletedEventsIds = await AddonCalendarOffline.getAllDeletedEventsIds();
|
||||||
|
|
||||||
|
this.deletedEvents = new Set(deletedEventsIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load time format.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async loadTimeFormat(): Promise<void> {
|
||||||
|
this.timeFormat = await AddonCalendar.getCalendarTimeFormat();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemId(item: DayBasicData): string | number {
|
||||||
|
return AddonCalendarHelper.getDayId(item.moment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getPreviousItem(item: DayBasicData): DayBasicData | null {
|
||||||
|
return {
|
||||||
|
moment: item.moment.clone().subtract(1, 'day'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getNextItem(item: DayBasicData): DayBasicData | null {
|
||||||
|
return {
|
||||||
|
moment: item.moment.clone().add(1, 'day'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async loadItemData(day: DayBasicData, preload = false): Promise<PreloadedDay | null> {
|
||||||
|
const preloadedDay: PreloadedDay = {
|
||||||
|
...day,
|
||||||
|
hasOffline: false,
|
||||||
|
events: [],
|
||||||
|
onlineEvents: [],
|
||||||
|
filteredEvents: [],
|
||||||
|
isCurrentDay: day.moment.isSame(moment(), 'day'),
|
||||||
|
isPastDay: day.moment.isBefore(moment(), 'day'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (preload) {
|
||||||
|
return preloadedDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: AddonCalendarCalendarDay;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Don't pass courseId and categoryId, we'll filter them locally.
|
||||||
|
result = await AddonCalendar.getDayEvents(day.moment.year(), day.moment.month() + 1, day.moment.date());
|
||||||
|
preloadedDay.onlineEvents = await Promise.all(
|
||||||
|
result.events.map((event) => AddonCalendarHelper.formatEventData(event)),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// Allow navigating to non-cached days in offline (behave as if using emergency cache).
|
||||||
|
if (CoreApp.isOnline()) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule notifications for the events retrieved (only future events will be scheduled).
|
||||||
|
AddonCalendar.scheduleEventsNotifications(preloadedDay.onlineEvents || []);
|
||||||
|
|
||||||
|
// Merge the online events with offline data.
|
||||||
|
preloadedDay.events = this.mergeEvents(preloadedDay);
|
||||||
|
|
||||||
|
// Filter events by course.
|
||||||
|
this.filterEvents(preloadedDay, this.dayPage.filter);
|
||||||
|
|
||||||
|
// Re-calculate the formatted time so it uses the device date.
|
||||||
|
const dayTime = day.moment.unix() * 1000;
|
||||||
|
const currentTime = CoreTimeUtils.timestamp();
|
||||||
|
|
||||||
|
const promises = preloadedDay.events.map(async (event) => {
|
||||||
|
event.ispast = preloadedDay.isPastDay || (preloadedDay.isCurrentDay && this.isEventPast(event, currentTime));
|
||||||
|
event.formattedtime = await AddonCalendar.formatEventTime(event, this.dayPage.timeFormat, true, dayTime);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
return preloadedDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: AddonCalendarEventToDisplay, currentTime: number): boolean {
|
||||||
|
return (event.timestart + event.timeduration) < currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge online events with the offline events of that period.
|
||||||
|
*
|
||||||
|
* @param day Day with the events.
|
||||||
|
* @return Merged events.
|
||||||
|
*/
|
||||||
|
mergeEvents(day: PreloadedDay): AddonCalendarEventToDisplay[] {
|
||||||
|
day.hasOffline = false;
|
||||||
|
|
||||||
|
if (!Object.keys(this.offlineEvents).length && !this.deletedEvents?.size) {
|
||||||
|
// No offline events, nothing to merge.
|
||||||
|
return day.onlineEvents || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthOfflineEvents = this.offlineEvents[AddonCalendarHelper.getMonthId(day.moment)];
|
||||||
|
const dayOfflineEvents = monthOfflineEvents && monthOfflineEvents[day.moment.date()];
|
||||||
|
let result = day.onlineEvents || [];
|
||||||
|
|
||||||
|
if (this.deletedEvents?.size) {
|
||||||
|
// Mark as deleted the events that were deleted in offline.
|
||||||
|
result.forEach((event) => {
|
||||||
|
event.deleted = this.deletedEvents?.has(event.id);
|
||||||
|
|
||||||
|
if (event.deleted) {
|
||||||
|
day.hasOffline = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.offlineEditedEventsIds.length) {
|
||||||
|
// Remove the online events that were modified in offline.
|
||||||
|
result = result.filter((event) => this.offlineEditedEventsIds.indexOf(event.id) == -1);
|
||||||
|
|
||||||
|
if (result.length != day.onlineEvents?.length) {
|
||||||
|
day.hasOffline = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dayOfflineEvents && dayOfflineEvents.length) {
|
||||||
|
// Add the offline events (either new or edited).
|
||||||
|
day.hasOffline = true;
|
||||||
|
result = AddonCalendarHelper.sortEvents(result.concat(dayOfflineEvents));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate content.
|
||||||
|
*
|
||||||
|
* @param selectedDay The current selected day.
|
||||||
|
* @param invalidateDayEvents Whether to invalidate selected day events.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async invalidateContent(selectedDay: PreloadedDay | null, invalidateDayEvents?: boolean): Promise<void> {
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
if (invalidateDayEvents && selectedDay) {
|
||||||
|
promises.push(AddonCalendar.invalidateDayEvents(
|
||||||
|
selectedDay.moment.year(),
|
||||||
|
selectedDay.moment.month() + 1,
|
||||||
|
selectedDay.moment.date(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
promises.push(AddonCalendar.invalidateAllowedEventTypes());
|
||||||
|
promises.push(CoreCourses.invalidateCategories(0, true));
|
||||||
|
promises.push(AddonCalendar.invalidateTimeFormat());
|
||||||
|
|
||||||
|
this.categories = undefined; // Get categories again.
|
||||||
|
|
||||||
|
if (selectedDay) {
|
||||||
|
selectedDay.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find an event and mark it as deleted.
|
||||||
|
*
|
||||||
|
* @param eventId Event ID.
|
||||||
|
* @param deleted Whether to mark it as deleted or not.
|
||||||
|
*/
|
||||||
|
markAsDeleted(eventId: number, deleted: boolean): void {
|
||||||
|
// Mark the event as deleted or not.
|
||||||
|
this.getItems()?.some(day => {
|
||||||
|
const event = day.onlineEvents?.find((event) => event.id == eventId);
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.deleted = deleted;
|
||||||
|
|
||||||
|
if (deleted) {
|
||||||
|
day.hasOffline = true;
|
||||||
|
} else {
|
||||||
|
// Re-calculate "hasOffline".
|
||||||
|
day.hasOffline = day.events?.length != day.onlineEvents?.length ||
|
||||||
|
day.events?.some((event) => event.deleted || event.offline);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add it or remove it from the list of deleted events.
|
||||||
|
if (deleted) {
|
||||||
|
this.deletedEvents?.add(eventId);
|
||||||
|
} else {
|
||||||
|
this.deletedEvents?.delete(eventId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,4 +6,8 @@
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
core-swipe-slides {
|
||||||
|
--swipe-slides-min-height: calc(100% - 52px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data && data.eventId) {
|
if (data && data.eventId) {
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.refreshData(true, false);
|
this.refreshData(true, false, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
this.currentSiteId,
|
this.currentSiteId,
|
||||||
|
@ -101,7 +101,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
|
||||||
// Listen for new event discarded event. When it does, reload the data.
|
// Listen for new event discarded event. When it does, reload the data.
|
||||||
this.discardedObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => {
|
this.discardedObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => {
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.refreshData(true, false);
|
this.refreshData(true, false, true);
|
||||||
}, this.currentSiteId);
|
}, this.currentSiteId);
|
||||||
|
|
||||||
// Listen for events edited. When an event is edited, reload the data.
|
// Listen for events edited. When an event is edited, reload the data.
|
||||||
|
@ -110,7 +110,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data && data.eventId) {
|
if (data && data.eventId) {
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.refreshData(true, false);
|
this.refreshData(true, false, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
this.currentSiteId,
|
this.currentSiteId,
|
||||||
|
@ -119,21 +119,21 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
|
||||||
// Refresh data if calendar events are synchronized automatically.
|
// Refresh data if calendar events are synchronized automatically.
|
||||||
this.syncObserver = CoreEvents.on(AddonCalendarSyncProvider.AUTO_SYNCED, () => {
|
this.syncObserver = CoreEvents.on(AddonCalendarSyncProvider.AUTO_SYNCED, () => {
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.refreshData(false, false);
|
this.refreshData(false, false, true);
|
||||||
}, this.currentSiteId);
|
}, this.currentSiteId);
|
||||||
|
|
||||||
// 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) => {
|
||||||
if (data && data.source != 'index') {
|
if (data && data.source != 'index') {
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.refreshData(false, false);
|
this.refreshData(false, false, true);
|
||||||
}
|
}
|
||||||
}, this.currentSiteId);
|
}, this.currentSiteId);
|
||||||
|
|
||||||
// Update the events when an event is deleted.
|
// Update the events when an event is deleted.
|
||||||
this.deleteEventObserver = CoreEvents.on(AddonCalendarProvider.DELETED_EVENT_EVENT, () => {
|
this.deleteEventObserver = CoreEvents.on(AddonCalendarProvider.DELETED_EVENT_EVENT, () => {
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.refreshData(false, false);
|
this.refreshData(false, false, true);
|
||||||
}, this.currentSiteId);
|
}, this.currentSiteId);
|
||||||
|
|
||||||
// Update the "hasOffline" property if an event deleted in offline is restored.
|
// Update the "hasOffline" property if an event deleted in offline is restored.
|
||||||
|
@ -278,7 +278,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
|
||||||
* @param afterChange Whether the refresh is done after an event has changed or has been synced.
|
* @param afterChange Whether the refresh is done after an event has changed or has been synced.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async refreshData(sync = false, showErrors = false): Promise<void> {
|
async refreshData(sync = false, showErrors = false, afterChange = false): Promise<void> {
|
||||||
this.syncIcon = CoreConstants.ICON_LOADING;
|
this.syncIcon = CoreConstants.ICON_LOADING;
|
||||||
|
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
|
@ -287,7 +287,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
// Refresh the sub-component.
|
// Refresh the sub-component.
|
||||||
if (this.showCalendar && this.calendarComponent) {
|
if (this.showCalendar && this.calendarComponent) {
|
||||||
promises.push(this.calendarComponent.refreshData());
|
promises.push(this.calendarComponent.refreshData(afterChange));
|
||||||
} else if (!this.showCalendar && this.upcomingEventsComponent) {
|
} else if (!this.showCalendar && this.upcomingEventsComponent) {
|
||||||
promises.push(this.upcomingEventsComponent.refreshData());
|
promises.push(this.upcomingEventsComponent.refreshData());
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,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(treatedDay.year(), treatedDay.month() + 1);
|
const monthId = this.getMonthId(treatedDay);
|
||||||
const day = treatedDay.date();
|
const day = treatedDay.date();
|
||||||
|
|
||||||
if (!result[monthId]) {
|
if (!result[monthId]) {
|
||||||
|
@ -364,14 +364,23 @@ export class AddonCalendarHelperProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the month "id" (year + month).
|
* Get the month "id".
|
||||||
*
|
*
|
||||||
* @param year Year.
|
* @param moment Month moment.
|
||||||
* @param month Month.
|
|
||||||
* @return The "id".
|
* @return The "id".
|
||||||
*/
|
*/
|
||||||
getMonthId(year: number, month: number): string {
|
getMonthId(moment: moment.Moment): string {
|
||||||
return year + '#' + month;
|
return `${moment.year()}#${moment.month() + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the day "id".
|
||||||
|
*
|
||||||
|
* @param day Day moment.
|
||||||
|
* @return The "id".
|
||||||
|
*/
|
||||||
|
getDayId(moment: moment.Moment): string {
|
||||||
|
return `${this.getMonthId(moment)}#${moment.date()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -650,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(day.year(), 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;
|
||||||
|
@ -686,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(day.year(), 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;
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
|
import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source';
|
||||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
|
import { CoreGroupInfo, CoreGroups } from '@services/groups';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
@ -33,7 +33,7 @@ import { AddonModAssignSync, AddonModAssignSyncProvider } from '../services/assi
|
||||||
/**
|
/**
|
||||||
* Provides a collection of assignment submissions.
|
* Provides a collection of assignment submissions.
|
||||||
*/
|
*/
|
||||||
export class AddonModAssignSubmissionsSource extends CoreItemsManagerSource<AddonModAssignSubmissionForList> {
|
export class AddonModAssignSubmissionsSource extends CoreRoutedItemsManagerSource<AddonModAssignSubmissionForList> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
|
|
|
@ -13,8 +13,8 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
|
import { Component, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
|
||||||
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
|
||||||
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||||
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
import { CoreGroupInfo } from '@services/groups';
|
import { CoreGroupInfo } from '@services/groups';
|
||||||
|
@ -86,7 +86,7 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
||||||
const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
const groupId = CoreNavigator.getRouteNumberParam('groupId') || 0;
|
const groupId = CoreNavigator.getRouteNumberParam('groupId') || 0;
|
||||||
const selectedStatus = CoreNavigator.getRouteParam('status');
|
const selectedStatus = CoreNavigator.getRouteParam('status');
|
||||||
const submissionsSource = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
const submissionsSource = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
AddonModAssignSubmissionsSource,
|
AddonModAssignSubmissionsSource,
|
||||||
[courseId, moduleId, selectedStatus],
|
[courseId, moduleId, selectedStatus],
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,8 +14,8 @@
|
||||||
|
|
||||||
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 { CoreItemsManagerSourcesTracker } from '@classes/items-management/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';
|
||||||
|
@ -64,7 +64,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, OnDestroy, Ca
|
||||||
this.blindId = CoreNavigator.getRouteNumberParam('blindId', { params });
|
this.blindId = CoreNavigator.getRouteNumberParam('blindId', { params });
|
||||||
const groupId = CoreNavigator.getRequiredRouteNumberParam('groupId');
|
const groupId = CoreNavigator.getRequiredRouteNumberParam('groupId');
|
||||||
const selectedStatus = CoreNavigator.getRouteParam('selectedStatus');
|
const selectedStatus = CoreNavigator.getRouteParam('selectedStatus');
|
||||||
const submissionsSource = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
const submissionsSource = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
AddonModAssignSubmissionsSource,
|
AddonModAssignSubmissionsSource,
|
||||||
[this.courseId, this.moduleId, selectedStatus],
|
[this.courseId, this.moduleId, selectedStatus],
|
||||||
);
|
);
|
||||||
|
@ -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
|
||||||
|
|
|
@ -40,15 +40,18 @@
|
||||||
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>
|
||||||
|
|
||||||
<div class="ion-padding">
|
<core-swipe-slides [manager]="manager" [options]="slidesOpts">
|
||||||
<core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent" contextLevel="module"
|
<ng-template let-chapter="item">
|
||||||
[contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
|
<div class="ion-padding">
|
||||||
|
<core-format-text [component]="component" [componentId]="componentId" [text]="chapter.content" contextLevel="module"
|
||||||
<div class="ion-margin-top" *ngIf="tagsEnabled && tags?.length > 0">
|
[contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
|
||||||
<strong>{{ 'core.tag.tags' | translate }}: </strong>
|
<div class="ion-margin-top" *ngIf="chapter.tags?.length > 0">
|
||||||
<core-tag-list [tags]="tags"></core-tag-list>
|
<strong>{{ 'core.tag.tags' | translate }}: </strong>
|
||||||
</div>
|
<core-tag-list [tags]="chapter.tags"></core-tag-list>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</core-swipe-slides>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
|
@ -12,8 +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 } from '@angular/core';
|
import { Component, Optional, Input, OnInit, ViewChild, ElementRef, OnDestroy } from '@angular/core';
|
||||||
import { IonContent } from '@ionic/angular';
|
|
||||||
import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component';
|
import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component';
|
||||||
import {
|
import {
|
||||||
AddonModBookProvider,
|
AddonModBookProvider,
|
||||||
|
@ -29,8 +28,14 @@ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreCourse } from '@features/course/services/course';
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { AddonModBookTocComponent } from '../toc/toc';
|
import { AddonModBookTocComponent } from '../toc/toc';
|
||||||
import { CoreConstants } from '@/core/constants';
|
|
||||||
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
|
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
|
||||||
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
import { CoreSwipeSlidesComponent, CoreSwipeSlidesOptions } from '@components/swipe-slides/swipe-slides';
|
||||||
|
import { CoreSwipeSlidesItemsManagerSource } from '@classes/items-management/swipe-slides-items-manager-source';
|
||||||
|
import { CoreCourseModule } from '@features/course/services/course-helper';
|
||||||
|
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.
|
||||||
|
@ -39,29 +44,34 @@ import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar
|
||||||
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;
|
||||||
|
|
||||||
@Input() initialChapterId?: number; // The initial chapter ID to load.
|
@Input() initialChapterId?: number; // The initial chapter ID to load.
|
||||||
|
|
||||||
component = AddonModBookProvider.COMPONENT;
|
component = AddonModBookProvider.COMPONENT;
|
||||||
chapterContent?: string;
|
manager?: CoreSwipeSlidesItemsManager<LoadedChapter, AddonModBookSlidesItemsManagerSource>;
|
||||||
tagsEnabled = false;
|
|
||||||
warning = '';
|
warning = '';
|
||||||
tags?: CoreTagItem[];
|
|
||||||
displayNavBar = true;
|
displayNavBar = true;
|
||||||
navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
|
navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
|
||||||
displayTitlesInNavBar = false;
|
displayTitlesInNavBar = false;
|
||||||
|
slidesOpts: CoreSwipeSlidesOptions = {
|
||||||
|
autoHeight: true,
|
||||||
|
scrollOnChange: 'top',
|
||||||
|
};
|
||||||
|
|
||||||
protected chapters: AddonModBookTocChapter[] = [];
|
protected firstLoad = true;
|
||||||
protected currentChapter?: number;
|
protected element: HTMLElement;
|
||||||
protected book?: AddonModBookBookWSData;
|
protected managerUnsubscribe?: () => void;
|
||||||
protected contentsMap: AddonModBookContentsMap = {};
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected content?: IonContent,
|
elementRef: ElementRef,
|
||||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||||
) {
|
) {
|
||||||
super('AddonModBookIndexComponent', courseContentsPage);
|
super('AddonModBookIndexComponent', courseContentsPage);
|
||||||
|
|
||||||
|
this.element = elementRef.nativeElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -70,21 +80,43 @@ 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(
|
||||||
|
this.courseId,
|
||||||
|
this.module,
|
||||||
|
CoreTag.areTagsAvailableInSite(),
|
||||||
|
this.initialChapterId,
|
||||||
|
);
|
||||||
|
this.manager = new CoreSwipeSlidesItemsManager(source);
|
||||||
|
this.managerUnsubscribe = this.manager.addListener({
|
||||||
|
onSelectedItemUpdated: (item) => {
|
||||||
|
this.onChapterViewed(item.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.loadContent();
|
this.loadContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get book(): AddonModBookBookWSData | undefined {
|
||||||
|
return this.manager?.getSource().book;
|
||||||
|
}
|
||||||
|
|
||||||
|
get chapters(): AddonModBookTocChapter[] {
|
||||||
|
return this.manager?.getSource().chapters || [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the TOC.
|
* Show the TOC.
|
||||||
*/
|
*/
|
||||||
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,
|
||||||
},
|
},
|
||||||
|
@ -102,11 +134,11 @@ 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) {
|
||||||
this.loaded = false;
|
return;
|
||||||
this.refreshIcon = CoreConstants.ICON_LOADING;
|
|
||||||
this.loadChapter(chapterId, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.slides?.slideToItem({ id: chapterId });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -114,8 +146,8 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
*
|
*
|
||||||
* @return Resolved when done.
|
* @return Resolved when done.
|
||||||
*/
|
*/
|
||||||
protected invalidateContent(): Promise<void> {
|
protected async invalidateContent(): Promise<void> {
|
||||||
return AddonModBook.invalidateContent(this.module.id, this.courseId);
|
await this.manager?.getSource().invalidateContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,42 +158,29 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
*/
|
*/
|
||||||
protected async fetchContent(refresh = false): Promise<void> {
|
protected async fetchContent(refresh = false): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const downloadResult = await this.downloadResourceIfNeeded(refresh);
|
const source = this.manager?.getSource();
|
||||||
|
if (!source) {
|
||||||
await this.loadBookData();
|
|
||||||
|
|
||||||
// Get contents. No need to refresh, it has been done in downloadResourceIfNeeded.
|
|
||||||
const contents = await CoreCourse.getModuleContents(this.module, this.courseId);
|
|
||||||
|
|
||||||
this.contentsMap = AddonModBook.getContentsMap(contents);
|
|
||||||
this.chapters = AddonModBook.getTocList(contents);
|
|
||||||
|
|
||||||
if (typeof this.currentChapter == 'undefined' && typeof this.initialChapterId != 'undefined' && this.chapters) {
|
|
||||||
// Initial chapter set. Validate that the chapter exists.
|
|
||||||
const chapter = this.chapters.find((chapter) => chapter.id == this.initialChapterId);
|
|
||||||
|
|
||||||
if (chapter) {
|
|
||||||
this.currentChapter = this.initialChapterId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.currentChapter === undefined) {
|
|
||||||
// Load the first chapter.
|
|
||||||
this.currentChapter = AddonModBook.getFirstChapter(this.chapters);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.currentChapter === undefined) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show chapter.
|
const downloadResult = await this.downloadResourceIfNeeded(refresh);
|
||||||
try {
|
|
||||||
await this.loadChapter(this.currentChapter, refresh);
|
|
||||||
|
|
||||||
this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : '';
|
const book = await source.loadBookData();
|
||||||
} catch {
|
|
||||||
// Ignore errors, they're handled inside the loadChapter function.
|
if (book) {
|
||||||
|
this.dataRetrieved.emit(book);
|
||||||
|
|
||||||
|
this.description = book.intro;
|
||||||
|
this.displayNavBar = book.navstyle != AddonModBookNavStyle.TOC_ONLY;
|
||||||
|
this.displayTitlesInNavBar = book.navstyle == AddonModBookNavStyle.TEXT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get contents. No need to refresh, it has been done in downloadResourceIfNeeded.
|
||||||
|
await source.loadContents();
|
||||||
|
|
||||||
|
await source.load();
|
||||||
|
|
||||||
|
this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error || '') : '';
|
||||||
} finally {
|
} finally {
|
||||||
// Pass false because downloadResourceIfNeeded already invalidates and refresh data if refresh=true.
|
// Pass false because downloadResourceIfNeeded already invalidates and refresh data if refresh=true.
|
||||||
this.fillContextMenu(false);
|
this.fillContextMenu(false);
|
||||||
|
@ -169,63 +188,33 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load book data from WS.
|
* Update data related to chapter being viewed.
|
||||||
*
|
*
|
||||||
|
* @param chapterId Chapter viewed.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async loadBookData(): Promise<void> {
|
protected async onChapterViewed(chapterId: number): Promise<void> {
|
||||||
this.book = await AddonModBook.getBook(this.courseId, this.module.id);
|
// Don't log the chapter ID when the user has just opened the book.
|
||||||
|
const logChapterId = this.firstLoad;
|
||||||
|
this.firstLoad = false;
|
||||||
|
|
||||||
this.dataRetrieved.emit(this.book);
|
if (this.displayNavBar) {
|
||||||
|
this.navigationItems = this.getNavigationItems(chapterId);
|
||||||
|
}
|
||||||
|
|
||||||
this.description = this.book.intro;
|
// Chapter loaded, log view.
|
||||||
this.displayNavBar = this.book.navstyle != AddonModBookNavStyle.TOC_ONLY;
|
await CoreUtils.ignoreErrors(AddonModBook.logView(
|
||||||
this.displayTitlesInNavBar = this.book.navstyle == AddonModBookNavStyle.TEXT;
|
this.module.instance!,
|
||||||
}
|
logChapterId ? chapterId : undefined,
|
||||||
|
this.module.name,
|
||||||
|
));
|
||||||
|
|
||||||
/**
|
const currentChapterIndex = this.chapters.findIndex((chapter) => chapter.id == chapterId);
|
||||||
* Load a book chapter.
|
const isLastChapter = currentChapterIndex < 0 || this.chapters[currentChapterIndex + 1] === undefined;
|
||||||
*
|
|
||||||
* @param chapterId Chapter to load.
|
|
||||||
* @param logChapterId Whether chapter ID should be passed to the log view function.
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
protected async loadChapter(chapterId: number, logChapterId: boolean): Promise<void> {
|
|
||||||
this.currentChapter = chapterId;
|
|
||||||
this.content?.scrollToTop();
|
|
||||||
|
|
||||||
try {
|
// Module is completed when last chapter is viewed, so we only check completion if the last is reached.
|
||||||
const content = await AddonModBook.getChapterContent(this.contentsMap, chapterId, this.module.id);
|
if (isLastChapter) {
|
||||||
|
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||||
this.tags = this.tagsEnabled ? this.contentsMap[this.currentChapter].tags : [];
|
|
||||||
|
|
||||||
this.chapterContent = content;
|
|
||||||
|
|
||||||
if (this.displayNavBar) {
|
|
||||||
this.navigationItems = this.getNavigationItems(chapterId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapter loaded, log view. We don't return the promise because we don't want to block the user for this.
|
|
||||||
await CoreUtils.ignoreErrors(AddonModBook.logView(
|
|
||||||
this.module.instance!,
|
|
||||||
logChapterId ? chapterId : undefined,
|
|
||||||
this.module.name,
|
|
||||||
));
|
|
||||||
|
|
||||||
const currentChapterIndex = this.chapters.findIndex((chapter) => chapter.id == chapterId);
|
|
||||||
const isLastChapter = currentChapterIndex < 0 || this.chapters[currentChapterIndex + 1] === undefined;
|
|
||||||
|
|
||||||
// Module is completed when last chapter is viewed, so we only check completion if the last is reached.
|
|
||||||
if (isLastChapter) {
|
|
||||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_book.errorchapter', true);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
this.loaded = true;
|
|
||||||
this.refreshIcon = CoreConstants.ICON_REFRESH;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,4 +233,104 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
|
||||||
|
this.managerUnsubscribe && this.managerUnsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoadedChapter = {
|
||||||
|
id: number;
|
||||||
|
content?: string;
|
||||||
|
tags?: CoreTagItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within a collection of chapters.
|
||||||
|
*/
|
||||||
|
class AddonModBookSlidesItemsManagerSource extends CoreSwipeSlidesItemsManagerSource<LoadedChapter> {
|
||||||
|
|
||||||
|
readonly COURSE_ID: number;
|
||||||
|
readonly MODULE: CoreCourseModule;
|
||||||
|
readonly TAGS_ENABLED: boolean;
|
||||||
|
|
||||||
|
book?: AddonModBookBookWSData;
|
||||||
|
chapters: AddonModBookTocChapter[] = [];
|
||||||
|
contentsMap: AddonModBookContentsMap = {};
|
||||||
|
|
||||||
|
constructor(courseId: number, module: CoreCourseModule, tagsEnabled: boolean, initialChapterId?: number) {
|
||||||
|
super(initialChapterId ? { id: initialChapterId } : undefined);
|
||||||
|
|
||||||
|
this.COURSE_ID = courseId;
|
||||||
|
this.MODULE = module;
|
||||||
|
this.TAGS_ENABLED = tagsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemId(item: LoadedChapter): string | number {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load book data from WS.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async loadBookData(): Promise<AddonModBookBookWSData> {
|
||||||
|
this.book = await AddonModBook.getBook(this.COURSE_ID, this.MODULE.id);
|
||||||
|
|
||||||
|
return this.book;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load module contents.
|
||||||
|
*/
|
||||||
|
async loadContents(): Promise<void> {
|
||||||
|
const contents = await CoreCourse.getModuleContents(this.MODULE, this.COURSE_ID);
|
||||||
|
|
||||||
|
this.contentsMap = AddonModBook.getContentsMap(contents);
|
||||||
|
this.chapters = AddonModBook.getTocList(contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async loadItems(): Promise<LoadedChapter[]> {
|
||||||
|
try {
|
||||||
|
const newChapters = await Promise.all(this.chapters.map(async (chapter) => {
|
||||||
|
const content = await AddonModBook.getChapterContent(this.contentsMap, chapter.id, this.MODULE.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: chapter.id,
|
||||||
|
content,
|
||||||
|
tags: this.TAGS_ENABLED ? this.contentsMap[chapter.id].tags : [],
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
return newChapters;
|
||||||
|
} catch (error) {
|
||||||
|
if (!CoreTextUtils.getErrorMessageFromError(error)) {
|
||||||
|
throw new CoreError(Translate.instant('addon.mod_book.errorchapter'));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the invalidate content function.
|
||||||
|
*
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
invalidateContent(): Promise<void> {
|
||||||
|
return AddonModBook.invalidateContent(this.MODULE.id, this.COURSE_ID);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
|
import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source';
|
||||||
import { CoreUser } from '@features/user/services/user';
|
import { CoreUser } from '@features/user/services/user';
|
||||||
import {
|
import {
|
||||||
AddonModForum,
|
AddonModForum,
|
||||||
|
@ -24,7 +24,7 @@ import {
|
||||||
} from '../services/forum';
|
} from '../services/forum';
|
||||||
import { AddonModForumOffline, AddonModForumOfflineDiscussion } from '../services/forum-offline';
|
import { AddonModForumOffline, AddonModForumOfflineDiscussion } from '../services/forum-offline';
|
||||||
|
|
||||||
export class AddonModForumDiscussionsSource extends CoreItemsManagerSource<AddonModForumDiscussionItem> {
|
export class AddonModForumDiscussionsSource extends CoreRoutedItemsManagerSource<AddonModForumDiscussionItem> {
|
||||||
|
|
||||||
static readonly NEW_DISCUSSION: AddonModForumNewDiscussionForm = { newDiscussion: true };
|
static readonly NEW_DISCUSSION: AddonModForumNewDiscussionForm = { newDiscussion: true };
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -57,7 +57,7 @@ import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
||||||
import { ContextLevel } from '@/core/constants';
|
import { ContextLevel } from '@/core/constants';
|
||||||
import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
||||||
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||||
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays a forum entry page.
|
* Component that displays a forum entry page.
|
||||||
|
@ -149,7 +149,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
await super.ngOnInit();
|
await super.ngOnInit();
|
||||||
|
|
||||||
// Initialize discussions manager.
|
// Initialize discussions manager.
|
||||||
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
AddonModForumDiscussionsSource,
|
AddonModForumDiscussionsSource,
|
||||||
[this.courseId, this.module.id, this.courseContentsPage ? `${AddonModForumModuleHandlerService.PAGE_NAME}/` : ''],
|
[this.courseId, this.module.id, this.courseContentsPage ? `${AddonModForumModuleHandlerService.PAGE_NAME}/` : ''],
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
import { ContextLevel, CoreConstants } from '@/core/constants';
|
import { ContextLevel, CoreConstants } from '@/core/constants';
|
||||||
import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Optional } from '@angular/core';
|
import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Optional } from '@angular/core';
|
||||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||||
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
|
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
|
||||||
|
@ -145,7 +145,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
||||||
|
|
||||||
if (this.courseId && this.cmId && (routeData.swipeEnabled ?? true)) {
|
if (this.courseId && this.cmId && (routeData.swipeEnabled ?? true)) {
|
||||||
this.discussions = new AddonModForumDiscussionDiscussionsSwipeManager(
|
this.discussions = new AddonModForumDiscussionDiscussionsSwipeManager(
|
||||||
CoreItemsManagerSourcesTracker.getOrCreateSource(
|
CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
AddonModForumDiscussionsSource,
|
AddonModForumDiscussionsSource,
|
||||||
[this.courseId, this.cmId, routeData.discussionsPathPrefix ?? ''],
|
[this.courseId, this.cmId, routeData.discussionsPathPrefix ?? ''],
|
||||||
),
|
),
|
||||||
|
|
|
@ -42,8 +42,8 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreForms } from '@singletons/form';
|
import { CoreForms } from '@singletons/form';
|
||||||
import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discussions-swipe-manager';
|
import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discussions-swipe-manager';
|
||||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
|
||||||
import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
||||||
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
|
|
||||||
type NewDiscussionData = {
|
type NewDiscussionData = {
|
||||||
subject: string;
|
subject: string;
|
||||||
|
@ -117,7 +117,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
|
||||||
this.timeCreated = CoreNavigator.getRequiredRouteNumberParam('timeCreated');
|
this.timeCreated = CoreNavigator.getRequiredRouteNumberParam('timeCreated');
|
||||||
|
|
||||||
if (this.timeCreated !== 0 && (routeData.swipeEnabled ?? true)) {
|
if (this.timeCreated !== 0 && (routeData.swipeEnabled ?? true)) {
|
||||||
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
AddonModForumDiscussionsSource,
|
AddonModForumDiscussionsSource,
|
||||||
[this.courseId, this.cmId, routeData.discussionsPathPrefix ?? ''],
|
[this.courseId, this.cmId, routeData.discussionsPathPrefix ?? ''],
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
|
import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source';
|
||||||
import {
|
import {
|
||||||
AddonModGlossary,
|
AddonModGlossary,
|
||||||
AddonModGlossaryEntry,
|
AddonModGlossaryEntry,
|
||||||
|
@ -27,7 +27,7 @@ import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '../servic
|
||||||
/**
|
/**
|
||||||
* Provides a collection of glossary entries.
|
* Provides a collection of glossary entries.
|
||||||
*/
|
*/
|
||||||
export class AddonModGlossaryEntriesSource extends CoreItemsManagerSource<AddonModGlossaryEntryItem> {
|
export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<AddonModGlossaryEntryItem> {
|
||||||
|
|
||||||
static readonly NEW_ENTRY: AddonModGlossaryNewEntryForm = { newEntry: true };
|
static readonly NEW_ENTRY: AddonModGlossaryNewEntryForm = { newEntry: true };
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -15,8 +15,8 @@
|
||||||
import { ContextLevel } from '@/core/constants';
|
import { ContextLevel } from '@/core/constants';
|
||||||
import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
|
import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
|
||||||
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||||
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||||
|
@ -113,7 +113,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
await super.ngOnInit();
|
await super.ngOnInit();
|
||||||
|
|
||||||
// Initialize entries manager.
|
// Initialize entries manager.
|
||||||
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
AddonModGlossaryEntriesSource,
|
AddonModGlossaryEntriesSource,
|
||||||
[this.courseId, this.module.id, this.courseContentsPage ? `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` : ''],
|
[this.courseId, this.module.id, this.courseContentsPage ? `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` : ''],
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { Component, OnInit, ViewChild, ElementRef, Optional, OnDestroy } from '@
|
||||||
import { FormControl } from '@angular/forms';
|
import { FormControl } from '@angular/forms';
|
||||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||||
import { CanLeave } from '@guards/can-leave';
|
import { CanLeave } from '@guards/can-leave';
|
||||||
|
@ -101,7 +101,7 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
this.editorExtraParams.timecreated = this.timecreated;
|
this.editorExtraParams.timecreated = this.timecreated;
|
||||||
|
|
||||||
if (this.timecreated !== 0 && (routeData.swipeEnabled ?? true)) {
|
if (this.timecreated !== 0 && (routeData.swipeEnabled ?? true)) {
|
||||||
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
AddonModGlossaryEntriesSource,
|
AddonModGlossaryEntriesSource,
|
||||||
[this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
|
[this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,7 +14,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 { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
|
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
|
||||||
import { CoreComments } from '@features/comments/services/comments';
|
import { CoreComments } from '@features/comments/services/comments';
|
||||||
import { CoreRatingInfo } from '@features/rating/services/rating';
|
import { CoreRatingInfo } from '@features/rating/services/rating';
|
||||||
|
@ -73,7 +73,7 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
if (routeData.swipeEnabled ?? true) {
|
if (routeData.swipeEnabled ?? true) {
|
||||||
const cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
const cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
AddonModGlossaryEntriesSource,
|
AddonModGlossaryEntriesSource,
|
||||||
[this.courseId, cmId, routeData.glossaryPathPrefix ?? ''],
|
[this.courseId, cmId, routeData.glossaryPathPrefix ?? ''],
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,13 +12,11 @@
|
||||||
// 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 { Params } from '@angular/router';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates listener.
|
* Updates listener.
|
||||||
*/
|
*/
|
||||||
export interface CoreItemsListSourceListener<Item> {
|
export interface CoreItemsManagerSourceListener<Item> {
|
||||||
onItemsUpdated?(items: Item[], hasMoreItems: boolean): void;
|
onItemsUpdated?(items: Item[]): void;
|
||||||
onReset?(): void;
|
onReset?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,37 +25,41 @@ export interface CoreItemsListSourceListener<Item> {
|
||||||
*/
|
*/
|
||||||
export abstract class CoreItemsManagerSource<Item = unknown> {
|
export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
|
|
||||||
/**
|
protected items: Item[] | null = null;
|
||||||
* Get a string to identify instances constructed with the given arguments as being reusable.
|
protected listeners: CoreItemsManagerSourceListener<Item>[] = [];
|
||||||
*
|
protected dirty = false;
|
||||||
* @param args Constructor arguments.
|
protected loaded = false;
|
||||||
* @returns Id.
|
protected loadedPromise: Promise<void>;
|
||||||
*/
|
protected resolveLoaded!: () => void;
|
||||||
static getSourceId(...args: unknown[]): string {
|
|
||||||
return args.map(argument => String(argument)).join('-');
|
constructor() {
|
||||||
|
this.loadedPromise = new Promise(resolve => this.resolveLoaded = resolve);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected items: Item[] | null = null;
|
|
||||||
protected hasMoreItems = true;
|
|
||||||
protected listeners: CoreItemsListSourceListener<Item>[] = [];
|
|
||||||
protected dirty = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether any page has been loaded.
|
* Check whether data is loaded.
|
||||||
*
|
*
|
||||||
* @returns Whether any page has been loaded.
|
* @returns Whether data is loaded.
|
||||||
*/
|
*/
|
||||||
isLoaded(): boolean {
|
isLoaded(): boolean {
|
||||||
return this.items !== null;
|
return this.loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether there are more pages to be loaded.
|
* Return a promise that is resolved when the data is loaded.
|
||||||
*
|
*
|
||||||
* @return Whether there are more pages to be loaded.
|
* @return Promise.
|
||||||
*/
|
*/
|
||||||
isCompleted(): boolean {
|
waitForLoaded(): Promise<void> {
|
||||||
return !this.hasMoreItems;
|
return this.loadedPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the source as initialized.
|
||||||
|
*/
|
||||||
|
protected setLoaded(): void {
|
||||||
|
this.loaded = true;
|
||||||
|
this.resolveLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,42 +82,28 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
return this.items;
|
return this.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the count of pages that have been loaded.
|
|
||||||
*
|
|
||||||
* @returns Pages loaded.
|
|
||||||
*/
|
|
||||||
getPagesLoaded(): number {
|
|
||||||
if (this.items === null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageLength = this.getPageLength();
|
|
||||||
if (pageLength === null) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.ceil(this.items.length / pageLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset collection data.
|
* Reset collection data.
|
||||||
*/
|
*/
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.items = null;
|
this.items = null;
|
||||||
this.hasMoreItems = true;
|
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
|
|
||||||
this.listeners.forEach(listener => listener.onReset?.call(listener));
|
this.listeners.forEach(listener => listener.onReset?.call(listener));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load items.
|
||||||
|
*/
|
||||||
|
abstract load(): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a listener.
|
* Register a listener.
|
||||||
*
|
*
|
||||||
* @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);
|
||||||
|
@ -126,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) {
|
||||||
|
@ -136,85 +124,23 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
this.listeners.splice(index, 1);
|
this.listeners.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reload the collection, this resets the data to the first page.
|
|
||||||
*/
|
|
||||||
async reload(): Promise<void> {
|
|
||||||
const { items, hasMoreItems } = await this.loadPageItems(0);
|
|
||||||
|
|
||||||
this.dirty = false;
|
|
||||||
this.setItems(items, hasMoreItems ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load more items, if any.
|
|
||||||
*/
|
|
||||||
async load(): Promise<void> {
|
|
||||||
if (this.dirty) {
|
|
||||||
const { items, hasMoreItems } = await this.loadPageItems(0);
|
|
||||||
|
|
||||||
this.dirty = false;
|
|
||||||
this.setItems(items, hasMoreItems ?? false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.hasMoreItems) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { items, hasMoreItems } = await this.loadPageItems(this.getPagesLoaded());
|
|
||||||
|
|
||||||
this.setItems((this.items ?? []).concat(items), hasMoreItems ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the query parameters to use when navigating to an item page.
|
|
||||||
*
|
|
||||||
* @param item Item.
|
|
||||||
* @return Query parameters to use when navigating to the item page.
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
getItemQueryParams(item: Item): Params {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the path to use when navigating to an item page.
|
|
||||||
*
|
|
||||||
* @param item Item.
|
|
||||||
* @return Path to use when navigating to the item page.
|
|
||||||
*/
|
|
||||||
abstract getItemPath(item: Item): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load page items.
|
|
||||||
*
|
|
||||||
* @param page Page number (starting at 0).
|
|
||||||
* @return Page items data.
|
|
||||||
*/
|
|
||||||
protected abstract loadPageItems(page: number): Promise<{ items: Item[]; hasMoreItems?: boolean }>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the length of each page in the collection.
|
|
||||||
*
|
|
||||||
* @return Page length; null for collections that don't support pagination.
|
|
||||||
*/
|
|
||||||
protected getPageLength(): number | null {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the collection items.
|
* Update the collection items.
|
||||||
*
|
*
|
||||||
* @param items Items.
|
* @param items Items.
|
||||||
* @param hasMoreItems Whether there are more pages to be loaded.
|
|
||||||
*/
|
*/
|
||||||
protected setItems(items: Item[], hasMoreItems: boolean): void {
|
protected setItems(items: Item[]): void {
|
||||||
this.items = items;
|
this.items = items;
|
||||||
this.hasMoreItems = hasMoreItems;
|
this.setLoaded();
|
||||||
|
|
||||||
this.listeners.forEach(listener => listener.onItemsUpdated?.call(listener, items, hasMoreItems));
|
this.notifyItemsUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify that items have been updated.
|
||||||
|
*/
|
||||||
|
protected notifyItemsUpdated(): void {
|
||||||
|
this.listeners.forEach(listener => listener.onItemsUpdated?.call(listener, this.items));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,20 +12,27 @@
|
||||||
// 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 { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
|
||||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
|
||||||
|
|
||||||
import { CoreItemsManagerSource } from './items-manager-source';
|
import { CoreItemsManagerSource } from './items-manager-source';
|
||||||
import { CoreItemsManagerSourcesTracker } from './items-manager-sources-tracker';
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
*/
|
*/
|
||||||
export abstract class CoreItemsManager<Item = unknown, Source extends CoreItemsManagerSource<Item> = CoreItemsManagerSource<Item>> {
|
export abstract class CoreItemsManager<
|
||||||
|
Item = unknown,
|
||||||
|
Source extends CoreItemsManagerSource<Item> = CoreItemsManagerSource<Item>,
|
||||||
|
> {
|
||||||
|
|
||||||
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);
|
||||||
|
@ -51,8 +58,6 @@ export abstract class CoreItemsManager<Item = unknown, Source extends CoreItemsM
|
||||||
*/
|
*/
|
||||||
setSource(newSource: Source | null): void {
|
setSource(newSource: Source | null): void {
|
||||||
if (this.source) {
|
if (this.source) {
|
||||||
CoreItemsManagerSourcesTracker.removeReference(this.source.instance, this);
|
|
||||||
|
|
||||||
this.source.unsubscribe();
|
this.source.unsubscribe();
|
||||||
delete this.source;
|
delete this.source;
|
||||||
|
|
||||||
|
@ -60,8 +65,6 @@ export abstract class CoreItemsManager<Item = unknown, Source extends CoreItemsM
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newSource) {
|
if (newSource) {
|
||||||
CoreItemsManagerSourcesTracker.addReference(newSource, this);
|
|
||||||
|
|
||||||
this.source = {
|
this.source = {
|
||||||
instance: newSource,
|
instance: newSource,
|
||||||
unsubscribe: newSource.addListener({
|
unsubscribe: newSource.addListener({
|
||||||
|
@ -86,79 +89,50 @@ export abstract class CoreItemsManager<Item = unknown, Source extends CoreItemsM
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get page route.
|
* Get selected item.
|
||||||
*
|
*
|
||||||
* @returns Current page route, if any.
|
* @return Selected item, null if none.
|
||||||
*/
|
*/
|
||||||
protected abstract getCurrentPageRoute(): ActivatedRoute | null;
|
getSelectedItem(): Item | null {
|
||||||
|
return this.selectedItem;
|
||||||
/**
|
|
||||||
* Get the path of the selected item given the current route.
|
|
||||||
*
|
|
||||||
* @param route Page route.
|
|
||||||
* @return Path of the selected item in the given route.
|
|
||||||
*/
|
|
||||||
protected abstract getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the path of the selected item.
|
|
||||||
*
|
|
||||||
* @param route Page route, if any.
|
|
||||||
* @return Path of the selected item.
|
|
||||||
*/
|
|
||||||
protected getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null {
|
|
||||||
if (!route) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.getSelectedItemPathFromRoute(route);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the selected item given the current route.
|
* Set selected item.
|
||||||
*
|
*
|
||||||
* @param route Current route.
|
* @param item Item, null if none.
|
||||||
*/
|
*/
|
||||||
protected updateSelectedItem(route: ActivatedRouteSnapshot | null = null): void {
|
setSelectedItem(item: Item | null): void {
|
||||||
route = route ?? this.getCurrentPageRoute()?.snapshot ?? null;
|
this.selectedItem = item;
|
||||||
|
|
||||||
const selectedItemPath = this.getSelectedItemPath(route);
|
this.listeners.forEach(listener => listener.onSelectedItemUpdated?.call(listener, item));
|
||||||
|
|
||||||
this.selectedItem = selectedItemPath
|
|
||||||
? this.itemsMap?.[selectedItemPath] ?? null
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to an item in the collection.
|
* Register a listener.
|
||||||
*
|
*
|
||||||
* @param item Item.
|
* @param listener Listener.
|
||||||
* @param options Navigation options.
|
* @returns Unsubscribe function.
|
||||||
*/
|
*/
|
||||||
protected async navigateToItem(
|
addListener(listener: CoreItemsanagerListener<Item>): () => void {
|
||||||
item: Item,
|
this.listeners.push(listener);
|
||||||
options: Pick<CoreNavigationOptions, 'reset' | 'replace' | 'animationDirection'> = {},
|
|
||||||
): Promise<void> {
|
|
||||||
// Get current route in the page.
|
|
||||||
const route = this.getCurrentPageRoute();
|
|
||||||
|
|
||||||
if (route === null) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this item is already selected, do nothing.
|
this.listeners.splice(index, 1);
|
||||||
const itemPath = this.getSource().getItemPath(item);
|
|
||||||
const selectedItemPath = this.getSelectedItemPath(route.snapshot);
|
|
||||||
|
|
||||||
if (selectedItemPath === itemPath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to item.
|
|
||||||
const params = this.getSource().getItemQueryParams(item);
|
|
||||||
const pathPrefix = selectedItemPath ? selectedItemPath.split('/').fill('../').join('') : '';
|
|
||||||
|
|
||||||
await CoreNavigator.navigate(pathPrefix + itemPath, { params, ...options });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -168,12 +142,10 @@ export abstract class CoreItemsManager<Item = unknown, Source extends CoreItemsM
|
||||||
*/
|
*/
|
||||||
protected onSourceItemsUpdated(items: Item[]): void {
|
protected onSourceItemsUpdated(items: Item[]): void {
|
||||||
this.itemsMap = items.reduce((map, item) => {
|
this.itemsMap = items.reduce((map, item) => {
|
||||||
map[this.getSource().getItemPath(item)] = item;
|
map[this.getItemId(item)] = item;
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
this.updateSelectedItem();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -184,4 +156,22 @@ export abstract class CoreItemsManager<Item = unknown, Source extends CoreItemsM
|
||||||
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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,16 +20,16 @@ import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreScreen } from '@services/screen';
|
import { CoreScreen } from '@services/screen';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
import { CoreItemsManager } from './items-manager';
|
import { CoreRoutedItemsManagerSource } from './routed-items-manager-source';
|
||||||
import { CoreItemsManagerSource } from './items-manager-source';
|
import { CoreRoutedItemsManager } from './routed-items-manager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper class to manage the state and routing of a list of items in a page.
|
* Helper class to manage the state and routing of a list of items in a page.
|
||||||
*/
|
*/
|
||||||
export class CoreListItemsManager<
|
export class CoreListItemsManager<
|
||||||
Item = unknown,
|
Item = unknown,
|
||||||
Source extends CoreItemsManagerSource<Item> = CoreItemsManagerSource<Item>
|
Source extends CoreRoutedItemsManagerSource<Item> = CoreRoutedItemsManagerSource<Item>
|
||||||
> extends CoreItemsManager<Item, Source> {
|
> extends CoreRoutedItemsManager<Item, Source> {
|
||||||
|
|
||||||
protected pageRouteLocator?: unknown | ActivatedRoute;
|
protected pageRouteLocator?: unknown | ActivatedRoute;
|
||||||
protected splitView?: CoreSplitViewComponent;
|
protected splitView?: CoreSplitViewComponent;
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Params } from '@angular/router';
|
||||||
|
import { CoreItemsManagerSource } from './items-manager-source';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routed items collection source data.
|
||||||
|
*/
|
||||||
|
export abstract class CoreRoutedItemsManagerSource<Item = unknown> extends CoreItemsManagerSource<Item> {
|
||||||
|
|
||||||
|
protected hasMoreItems = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string to identify instances constructed with the given arguments as being reusable.
|
||||||
|
*
|
||||||
|
* @param args Constructor arguments.
|
||||||
|
* @returns Id.
|
||||||
|
*/
|
||||||
|
static getSourceId(...args: unknown[]): string {
|
||||||
|
return args.map(argument => String(argument)).join('-');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether there are more pages to be loaded.
|
||||||
|
*
|
||||||
|
* @return Whether there are more pages to be loaded.
|
||||||
|
*/
|
||||||
|
isCompleted(): boolean {
|
||||||
|
return !this.hasMoreItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the count of pages that have been loaded.
|
||||||
|
*
|
||||||
|
* @returns Pages loaded.
|
||||||
|
*/
|
||||||
|
getPagesLoaded(): number {
|
||||||
|
if (this.items === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageLength = this.getPageLength();
|
||||||
|
if (pageLength === null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.ceil(this.items.length / pageLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset collection data.
|
||||||
|
*/
|
||||||
|
reset(): void {
|
||||||
|
this.hasMoreItems = true;
|
||||||
|
|
||||||
|
super.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the collection, this resets the data to the first page.
|
||||||
|
*/
|
||||||
|
async reload(): Promise<void> {
|
||||||
|
this.dirty = true;
|
||||||
|
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load more items, if any.
|
||||||
|
*/
|
||||||
|
async load(): Promise<void> {
|
||||||
|
if (this.dirty) {
|
||||||
|
const { items, hasMoreItems } = await this.loadPageItems(0);
|
||||||
|
|
||||||
|
this.dirty = false;
|
||||||
|
this.setItems(items, hasMoreItems ?? false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.hasMoreItems) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { items, hasMoreItems } = await this.loadPageItems(this.getPagesLoaded());
|
||||||
|
|
||||||
|
this.setItems((this.items ?? []).concat(items), hasMoreItems ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load page items.
|
||||||
|
*
|
||||||
|
* @param page Page number (starting at 0).
|
||||||
|
* @return Page items data.
|
||||||
|
*/
|
||||||
|
protected abstract loadPageItems(page: number): Promise<{ items: Item[]; hasMoreItems?: boolean }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the length of each page in the collection.
|
||||||
|
*
|
||||||
|
* @return Page length; null for collections that don't support pagination.
|
||||||
|
*/
|
||||||
|
protected getPageLength(): number | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the collection items.
|
||||||
|
*
|
||||||
|
* @param items Items.
|
||||||
|
* @param hasMoreItems Whether there are more pages to be loaded.
|
||||||
|
*/
|
||||||
|
protected setItems(items: Item[], hasMoreItems = false): void {
|
||||||
|
this.hasMoreItems = hasMoreItems;
|
||||||
|
|
||||||
|
super.setItems(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the query parameters to use when navigating to an item page.
|
||||||
|
*
|
||||||
|
* @param item Item.
|
||||||
|
* @return Query parameters to use when navigating to the item page.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
getItemQueryParams(item: Item): Params {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to use when navigating to an item page.
|
||||||
|
*
|
||||||
|
* @param item Item.
|
||||||
|
* @return Path to use when navigating to the item page.
|
||||||
|
*/
|
||||||
|
abstract getItemPath(item: Item): string;
|
||||||
|
|
||||||
|
}
|
|
@ -12,23 +12,23 @@
|
||||||
// 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 { CoreItemsManagerSource } from './items-manager-source';
|
import { CoreRoutedItemsManagerSource } from './routed-items-manager-source';
|
||||||
|
|
||||||
type SourceConstructor<T extends CoreItemsManagerSource = CoreItemsManagerSource> = {
|
type SourceConstructor<T extends CoreRoutedItemsManagerSource = CoreRoutedItemsManagerSource> = {
|
||||||
getSourceId(...args: unknown[]): string;
|
getSourceId(...args: unknown[]): string;
|
||||||
new (...args: unknown[]): T;
|
new (...args: unknown[]): T;
|
||||||
};
|
};
|
||||||
type SourceConstuctorInstance<T> = T extends { new(...args: unknown[]): infer P } ? P : never;
|
type SourceConstuctorInstance<T> = T extends { new(...args: unknown[]): infer P } ? P : never;
|
||||||
type InstanceTracking = { instance: CoreItemsManagerSource; references: unknown[] };
|
type InstanceTracking = { instance: CoreRoutedItemsManagerSource; references: unknown[] };
|
||||||
type Instances = Record<string, InstanceTracking>;
|
type Instances = Record<string, InstanceTracking>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks CoreItemsManagerSource instances to reuse between pages.
|
* Tracks CoreRoutedItemsManagerSource instances to reuse between pages.
|
||||||
*/
|
*/
|
||||||
export class CoreItemsManagerSourcesTracker {
|
export class CoreRoutedItemsManagerSourcesTracker {
|
||||||
|
|
||||||
private static instances: WeakMap<SourceConstructor, Instances> = new WeakMap();
|
private static instances: WeakMap<SourceConstructor, Instances> = new WeakMap();
|
||||||
private static instanceIds: WeakMap<CoreItemsManagerSource, string> = new WeakMap();
|
private static instanceIds: WeakMap<CoreRoutedItemsManagerSource, string> = new WeakMap();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an instance of the given source or retrieve one if it's already in use.
|
* Create an instance of the given source or retrieve one if it's already in use.
|
||||||
|
@ -37,7 +37,7 @@ export class CoreItemsManagerSourcesTracker {
|
||||||
* @param constructorArguments Arguments to create a new instance, used to find out if an instance already exists.
|
* @param constructorArguments Arguments to create a new instance, used to find out if an instance already exists.
|
||||||
* @returns Source.
|
* @returns Source.
|
||||||
*/
|
*/
|
||||||
static getOrCreateSource<T extends CoreItemsManagerSource, C extends SourceConstructor<T>>(
|
static getOrCreateSource<T extends CoreRoutedItemsManagerSource, C extends SourceConstructor<T>>(
|
||||||
constructor: C,
|
constructor: C,
|
||||||
constructorArguments: ConstructorParameters<C>,
|
constructorArguments: ConstructorParameters<C>,
|
||||||
): SourceConstuctorInstance<C> {
|
): SourceConstuctorInstance<C> {
|
||||||
|
@ -54,7 +54,7 @@ export class CoreItemsManagerSourcesTracker {
|
||||||
* @param source Source.
|
* @param source Source.
|
||||||
* @param reference Object referncing this source.
|
* @param reference Object referncing this source.
|
||||||
*/
|
*/
|
||||||
static addReference(source: CoreItemsManagerSource, reference: unknown): void {
|
static addReference(source: CoreRoutedItemsManagerSource, reference: unknown): void {
|
||||||
const constructorInstances = this.getConstructorInstances(source.constructor as SourceConstructor);
|
const constructorInstances = this.getConstructorInstances(source.constructor as SourceConstructor);
|
||||||
const instanceId = this.instanceIds.get(source);
|
const instanceId = this.instanceIds.get(source);
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ export class CoreItemsManagerSourcesTracker {
|
||||||
* @param source Source.
|
* @param source Source.
|
||||||
* @param reference Object that was referncing this source.
|
* @param reference Object that was referncing this source.
|
||||||
*/
|
*/
|
||||||
static removeReference(source: CoreItemsManagerSource, reference: unknown): void {
|
static removeReference(source: CoreRoutedItemsManagerSource, reference: unknown): void {
|
||||||
const constructorInstances = this.instances.get(source.constructor as SourceConstructor);
|
const constructorInstances = this.instances.get(source.constructor as SourceConstructor);
|
||||||
const instanceId = this.instanceIds.get(source);
|
const instanceId = this.instanceIds.get(source);
|
||||||
const index = constructorInstances?.[instanceId ?? '']?.references.indexOf(reference) ?? -1;
|
const index = constructorInstances?.[instanceId ?? '']?.references.indexOf(reference) ?? -1;
|
||||||
|
@ -127,7 +127,7 @@ export class CoreItemsManagerSourcesTracker {
|
||||||
* @param constructorArguments Source constructor arguments.
|
* @param constructorArguments Source constructor arguments.
|
||||||
* @returns Source instance.
|
* @returns Source instance.
|
||||||
*/
|
*/
|
||||||
private static createInstance<T extends CoreItemsManagerSource>(
|
private static createInstance<T extends CoreRoutedItemsManagerSource>(
|
||||||
id: string,
|
id: string,
|
||||||
constructor: SourceConstructor<T>,
|
constructor: SourceConstructor<T>,
|
||||||
constructorArguments: ConstructorParameters<SourceConstructor<T>>,
|
constructorArguments: ConstructorParameters<SourceConstructor<T>>,
|
|
@ -0,0 +1,138 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
|
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreItemsManager } from './items-manager';
|
||||||
|
|
||||||
|
import { CoreRoutedItemsManagerSource } from './routed-items-manager-source';
|
||||||
|
import { CoreRoutedItemsManagerSourcesTracker } from './routed-items-manager-sources-tracker';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage a collection of items in a page.
|
||||||
|
*/
|
||||||
|
export abstract class CoreRoutedItemsManager<
|
||||||
|
Item = unknown,
|
||||||
|
Source extends CoreRoutedItemsManagerSource<Item> = CoreRoutedItemsManagerSource<Item>,
|
||||||
|
> extends CoreItemsManager<Item, Source> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
setSource(newSource: Source | null): void {
|
||||||
|
if (this.source) {
|
||||||
|
CoreRoutedItemsManagerSourcesTracker.removeReference(this.source.instance, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newSource) {
|
||||||
|
CoreRoutedItemsManagerSourcesTracker.addReference(newSource, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.setSource(newSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get page route.
|
||||||
|
*
|
||||||
|
* @returns Current page route, if any.
|
||||||
|
*/
|
||||||
|
protected abstract getCurrentPageRoute(): ActivatedRoute | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path of the selected item given the current route.
|
||||||
|
*
|
||||||
|
* @param route Page route.
|
||||||
|
* @return Path of the selected item in the given route.
|
||||||
|
*/
|
||||||
|
protected abstract getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path of the selected item.
|
||||||
|
*
|
||||||
|
* @param route Page route, if any.
|
||||||
|
* @return Path of the selected item.
|
||||||
|
*/
|
||||||
|
protected getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null {
|
||||||
|
if (!route) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getSelectedItemPathFromRoute(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the selected item given the current route.
|
||||||
|
*
|
||||||
|
* @param route Current route.
|
||||||
|
*/
|
||||||
|
protected updateSelectedItem(route: ActivatedRouteSnapshot | null = null): void {
|
||||||
|
route = route ?? this.getCurrentPageRoute()?.snapshot ?? null;
|
||||||
|
|
||||||
|
const selectedItemPath = this.getSelectedItemPath(route);
|
||||||
|
|
||||||
|
const selectedItem = selectedItemPath
|
||||||
|
? this.itemsMap?.[selectedItemPath] ?? null
|
||||||
|
: null;
|
||||||
|
this.setSelectedItem(selectedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to an item in the collection.
|
||||||
|
*
|
||||||
|
* @param item Item.
|
||||||
|
* @param options Navigation options.
|
||||||
|
*/
|
||||||
|
protected async navigateToItem(
|
||||||
|
item: Item,
|
||||||
|
options: Pick<CoreNavigationOptions, 'reset' | 'replace' | 'animationDirection'> = {},
|
||||||
|
): Promise<void> {
|
||||||
|
// Get current route in the page.
|
||||||
|
const route = this.getCurrentPageRoute();
|
||||||
|
|
||||||
|
if (route === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this item is already selected, do nothing.
|
||||||
|
const itemPath = this.getSource().getItemPath(item);
|
||||||
|
const selectedItemPath = this.getSelectedItemPath(route.snapshot);
|
||||||
|
|
||||||
|
if (selectedItemPath === itemPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to item.
|
||||||
|
const params = this.getSource().getItemQueryParams(item);
|
||||||
|
const pathPrefix = selectedItemPath ? selectedItemPath.split('/').fill('../').join('') : '';
|
||||||
|
|
||||||
|
await CoreNavigator.navigate(pathPrefix + itemPath, { params, ...options });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected onSourceItemsUpdated(items: Item[]): void {
|
||||||
|
super.onSourceItemsUpdated(items);
|
||||||
|
|
||||||
|
this.updateSelectedItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemId(item: Item): string | number {
|
||||||
|
return this.getSource().getItemPath(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,17 +16,17 @@ import { ActivatedRoute, ActivatedRouteSnapshot, UrlSegment } from '@angular/rou
|
||||||
|
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
import { CoreItemsManager } from './items-manager';
|
import { CoreRoutedItemsManager } from './routed-items-manager';
|
||||||
import { CoreItemsManagerSource } from './items-manager-source';
|
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 CoreItemsManagerSource<Item> = CoreItemsManagerSource<Item>
|
Source extends CoreRoutedItemsManagerSource<Item> = CoreRoutedItemsManagerSource<Item>
|
||||||
>
|
>
|
||||||
extends CoreItemsManager<Item, Source> {
|
extends CoreRoutedItemsManager<Item, Source> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process page started operations.
|
* Process page started operations.
|
||||||
|
@ -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) {
|
|
@ -0,0 +1,217 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { CoreSwipeSlidesItemsManagerSource } from './swipe-slides-items-manager-source';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Items collection source data for "swipe slides".
|
||||||
|
*/
|
||||||
|
export abstract class CoreSwipeSlidesDynamicItemsManagerSource<Item extends CoreSwipeSlidesDynamicItem>
|
||||||
|
extends CoreSwipeSlidesItemsManagerSource<Item> {
|
||||||
|
|
||||||
|
// Items being loaded, to prevent loading them twice.
|
||||||
|
protected loadingItems: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async load(selectedItem?: Item | null): Promise<void> {
|
||||||
|
if (!this.loaded && this.initialItem) {
|
||||||
|
// Load the initial item.
|
||||||
|
await this.loadItem(this.initialItem);
|
||||||
|
} else if (this.loaded && selectedItem) {
|
||||||
|
// Reload selected item if needed.
|
||||||
|
await this.loadItem(selectedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setLoaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async loadItems(): Promise<Item[]> {
|
||||||
|
// Not used in dynamic slides.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a certain item and preload next and previous ones.
|
||||||
|
*
|
||||||
|
* @param item Item to load.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async loadItem(item: Item): Promise<void> {
|
||||||
|
const previousItem = this.getPreviousItem(item);
|
||||||
|
const nextItem = this.getNextItem(item);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.loadItemInList(item, false),
|
||||||
|
previousItem && this.loadItemInList(previousItem, true),
|
||||||
|
nextItem && this.loadItemInList(nextItem, true),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load or preload a certain item and add it to the list.
|
||||||
|
*
|
||||||
|
* @param item Item to load.
|
||||||
|
* @param preload Whether to preload.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async loadItemInList(item: Item, preload = false): Promise<void> {
|
||||||
|
const preloadedItem = await this.performLoadItemData(item, preload);
|
||||||
|
if (!preloadedItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the item at the right position.
|
||||||
|
const existingItem = this.getItem(this.getItemId(item));
|
||||||
|
if (existingItem) {
|
||||||
|
// Already in list, no need to add it.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.items) {
|
||||||
|
this.items = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousItem = this.getPreviousItem(item);
|
||||||
|
const nextItem = this.getNextItem(item);
|
||||||
|
const previousItemId = previousItem ? this.getItemId(previousItem) : null;
|
||||||
|
const nextItemId = nextItem ? this.getItemId(nextItem) : null;
|
||||||
|
|
||||||
|
const added = this.items.some((item, index) => {
|
||||||
|
const itemId = this.getItemId(item);
|
||||||
|
let indexToInsert = -1;
|
||||||
|
if (itemId === previousItemId) {
|
||||||
|
// Previous item found, add the item after it.
|
||||||
|
indexToInsert = index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemId === nextItemId) {
|
||||||
|
// Next item found, add the item before it.
|
||||||
|
indexToInsert = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexToInsert > -1) {
|
||||||
|
this.items?.splice(indexToInsert, 0, preloadedItem);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!added) {
|
||||||
|
// Previous and next items not found, this probably means the array is still empty. Add it at the end.
|
||||||
|
this.items.push(preloadedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notifyItemsUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param item Item to load.
|
||||||
|
* @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).
|
||||||
|
*/
|
||||||
|
protected async performLoadItemData(item: Item, preload: boolean): Promise<Item | null> {
|
||||||
|
const itemId = this.getItemId(item);
|
||||||
|
|
||||||
|
if (this.loadingItems[itemId]) {
|
||||||
|
// Already loading, ignore it.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingItem = this.getItem(itemId);
|
||||||
|
if (existingItem && ((existingItem.loaded && !existingItem.dirty) || preload)) {
|
||||||
|
// Already loaded, or preloading an already preloaded item.
|
||||||
|
return existingItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the item.
|
||||||
|
this.loadingItems[itemId] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const itemData = await this.loadItemData(item, preload);
|
||||||
|
|
||||||
|
if (itemData && !preload) {
|
||||||
|
itemData.loaded = true;
|
||||||
|
itemData.dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingItem && itemData) {
|
||||||
|
// Update item that is already in list.
|
||||||
|
Object.assign(existingItem, itemData);
|
||||||
|
|
||||||
|
return existingItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemData;
|
||||||
|
} finally {
|
||||||
|
this.loadingItems[itemId] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all items as dirty.
|
||||||
|
*/
|
||||||
|
markAllItemsDirty(): void {
|
||||||
|
this.getItems()?.forEach(item => {
|
||||||
|
item.dirty = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all items as not loaded.
|
||||||
|
*/
|
||||||
|
markAllItemsUnloaded(): void {
|
||||||
|
this.getItems()?.forEach(item => {
|
||||||
|
item.loaded = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load or preload a certain item data.
|
||||||
|
*
|
||||||
|
* @param item Basic data about the item to load.
|
||||||
|
* @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).
|
||||||
|
*/
|
||||||
|
abstract loadItemData(item: Item, preload: boolean): Promise<Item | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the data to identify the previous item.
|
||||||
|
*
|
||||||
|
* @param item Data about the item.
|
||||||
|
* @return Previous item data. Null if no previous item.
|
||||||
|
*/
|
||||||
|
abstract getPreviousItem(item: Item): Item | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the data to identify the next item.
|
||||||
|
*
|
||||||
|
* @param item Data about the item.
|
||||||
|
* @return Next item data. Null if no next item.
|
||||||
|
*/
|
||||||
|
abstract getNextItem(item: Item): Item | null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CoreSwipeSlidesDynamicItem = {
|
||||||
|
loaded?: boolean; // Whether the item has been loaded. This value can affect UI (e.g. to display a spinner).
|
||||||
|
dirty?: boolean; // Whether the item data needs to be reloaded. This value usually shouldn't affect UI.
|
||||||
|
};
|
|
@ -0,0 +1,38 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { CoreSwipeSlidesDynamicItem, CoreSwipeSlidesDynamicItemsManagerSource } from './swipe-slides-dynamic-items-manager-source';
|
||||||
|
import { CoreSwipeSlidesItemsManager } from './swipe-slides-items-manager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to manage items for core-swipe-slides.
|
||||||
|
*/
|
||||||
|
export class CoreSwipeSlidesDynamicItemsManager<
|
||||||
|
Item extends CoreSwipeSlidesDynamicItem,
|
||||||
|
Source extends CoreSwipeSlidesDynamicItemsManagerSource<Item> = CoreSwipeSlidesDynamicItemsManagerSource<Item>,
|
||||||
|
> extends CoreSwipeSlidesItemsManager<Item, Source> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
setSelectedItem(item: Item | null): void {
|
||||||
|
super.setSelectedItem(item);
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
// Load the item if not loaded yet.
|
||||||
|
this.getSource().loadItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { CoreItemsManagerSource } from './items-manager-source';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Items collection source data for "swipe slides".
|
||||||
|
*/
|
||||||
|
export abstract class CoreSwipeSlidesItemsManagerSource<Item = unknown> extends CoreItemsManagerSource<Item> {
|
||||||
|
|
||||||
|
protected initialItem?: Item;
|
||||||
|
|
||||||
|
constructor(initialItem?: Item) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.initialItem = initialItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async load(): Promise<void> {
|
||||||
|
const items = await this.loadItems();
|
||||||
|
|
||||||
|
this.setItems(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load items.
|
||||||
|
*
|
||||||
|
* @return Items list.
|
||||||
|
*/
|
||||||
|
protected abstract loadItems(): Promise<Item[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a certain item.
|
||||||
|
*
|
||||||
|
* @param id Item ID.
|
||||||
|
* @return Item, null if not found.
|
||||||
|
*/
|
||||||
|
getItem(id: string | number): Item | null {
|
||||||
|
const index = this.getItemIndexById(id);
|
||||||
|
|
||||||
|
return this.items?.[index] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a certain item index.
|
||||||
|
*
|
||||||
|
* @param item Item.
|
||||||
|
* @return Item index, -1 if not found.
|
||||||
|
*/
|
||||||
|
getItemIndex(item: Item): number {
|
||||||
|
return this.getItemIndexById(this.getItemId(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get initial item index.
|
||||||
|
*
|
||||||
|
* @return Initial item index.
|
||||||
|
*/
|
||||||
|
getInitialItemIndex(): number {
|
||||||
|
if (!this.initialItem) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getItemIndex(this.initialItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of an item.
|
||||||
|
*
|
||||||
|
* @param item Item.
|
||||||
|
* @return Item ID.
|
||||||
|
*/
|
||||||
|
abstract getItemId(item: Item): string | number;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { CoreItemsManager } from './items-manager';
|
||||||
|
import { CoreSwipeSlidesItemsManagerSource } from './swipe-slides-items-manager-source';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to manage items for core-swipe-slides.
|
||||||
|
*/
|
||||||
|
export class CoreSwipeSlidesItemsManager<
|
||||||
|
Item = unknown,
|
||||||
|
Source extends CoreSwipeSlidesItemsManagerSource<Item> = CoreSwipeSlidesItemsManagerSource<Item>,
|
||||||
|
> extends CoreItemsManager<Item, Source> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemId(item: Item): string | number {
|
||||||
|
return this.getSource().getItemId(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -60,6 +60,7 @@ import { CoreComboboxComponent } from './combobox/combobox';
|
||||||
import { CoreSpacerComponent } from './spacer/spacer';
|
import { CoreSpacerComponent } from './spacer/spacer';
|
||||||
import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-controls/horizontal-scroll-controls';
|
import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-controls/horizontal-scroll-controls';
|
||||||
import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-with-spinner';
|
import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-with-spinner';
|
||||||
|
import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -94,6 +95,7 @@ import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-wit
|
||||||
CoreSplitViewComponent,
|
CoreSplitViewComponent,
|
||||||
CoreStyleComponent,
|
CoreStyleComponent,
|
||||||
CoreSwipeNavigationComponent,
|
CoreSwipeNavigationComponent,
|
||||||
|
CoreSwipeSlidesComponent,
|
||||||
CoreTabComponent,
|
CoreTabComponent,
|
||||||
CoreTabsComponent,
|
CoreTabsComponent,
|
||||||
CoreTabsOutletComponent,
|
CoreTabsOutletComponent,
|
||||||
|
@ -143,6 +145,7 @@ import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-wit
|
||||||
CoreSplitViewComponent,
|
CoreSplitViewComponent,
|
||||||
CoreStyleComponent,
|
CoreStyleComponent,
|
||||||
CoreSwipeNavigationComponent,
|
CoreSwipeNavigationComponent,
|
||||||
|
CoreSwipeSlidesComponent,
|
||||||
CoreTabComponent,
|
CoreTabComponent,
|
||||||
CoreTabsComponent,
|
CoreTabsComponent,
|
||||||
CoreTabsOutletComponent,
|
CoreTabsOutletComponent,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<ion-slides *ngIf="loaded" (ionSlideWillChange)="slideWillChange()" (ionSlideDidChange)="slideDidChange()" [options]="options">
|
||||||
|
<ion-slide *ngFor="let item of items">
|
||||||
|
<ng-container *ngIf="template" [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{item: item}"></ng-container>
|
||||||
|
</ion-slide>
|
||||||
|
</ion-slides>
|
|
@ -0,0 +1,15 @@
|
||||||
|
:host {
|
||||||
|
--swipe-slides-min-height: auto;
|
||||||
|
|
||||||
|
ion-slides {
|
||||||
|
min-height: var(--swipe-slides-min-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-slide {
|
||||||
|
display: block;
|
||||||
|
font-size: inherit;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: start;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,257 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import {
|
||||||
|
Component, ContentChild, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, TemplateRef, ViewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { CoreSwipeSlidesItemsManager } from '@classes/items-management/swipe-slides-items-manager';
|
||||||
|
import { IonContent, IonSlides } from '@ionic/angular';
|
||||||
|
import { CoreDomUtils, VerticalPoint } from '@services/utils/dom';
|
||||||
|
import { CoreMath } from '@singletons/math';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper component to display swipable slides.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-swipe-slides',
|
||||||
|
templateUrl: 'swipe-slides.html',
|
||||||
|
styleUrls: ['swipe-slides.scss'],
|
||||||
|
})
|
||||||
|
export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDestroy {
|
||||||
|
|
||||||
|
@Input() manager?: CoreSwipeSlidesItemsManager<Item>;
|
||||||
|
@Input() options: CoreSwipeSlidesOptions = {};
|
||||||
|
@Output() onWillChange = new EventEmitter<CoreSwipeCurrentItemData<Item>>();
|
||||||
|
@Output() onDidChange = new EventEmitter<CoreSwipeCurrentItemData<Item>>();
|
||||||
|
|
||||||
|
@ViewChild(IonSlides) slides?: IonSlides;
|
||||||
|
@ContentChild(TemplateRef) template?: TemplateRef<unknown>; // Template defined by the content.
|
||||||
|
|
||||||
|
protected hostElement: HTMLElement;
|
||||||
|
protected unsubscribe?: () => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
elementRef: ElementRef<HTMLElement>,
|
||||||
|
protected content?: IonContent,
|
||||||
|
) {
|
||||||
|
this.hostElement = elementRef.nativeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnChanges(): void {
|
||||||
|
if (!this.unsubscribe && this.manager) {
|
||||||
|
this.initialize(this.manager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get items(): Item[] {
|
||||||
|
return this.manager?.getSource().getItems() || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get loaded(): boolean {
|
||||||
|
return !!this.manager?.getSource().isLoaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize some properties based on the manager.
|
||||||
|
*/
|
||||||
|
protected async initialize(manager: CoreSwipeSlidesItemsManager<Item>): Promise<void> {
|
||||||
|
this.unsubscribe = manager.getSource().addListener({
|
||||||
|
onItemsUpdated: () => this.onItemsUpdated(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't call default callbacks on init, emit our own events instead.
|
||||||
|
// This is because default callbacks aren't triggered for index 0, and to prevent auto scroll on init.
|
||||||
|
this.options.runCallbacksOnInit = false;
|
||||||
|
|
||||||
|
await manager.getSource().waitForLoaded();
|
||||||
|
|
||||||
|
if (this.options.initialSlide === undefined) {
|
||||||
|
// Calculate the initial slide.
|
||||||
|
const index = manager.getSource().getInitialItemIndex();
|
||||||
|
this.options.initialSlide = Math.max(index, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit change events with the initial item.
|
||||||
|
const items = manager.getSource().getItems();
|
||||||
|
if (!items || !items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that the initial index is inside the valid range.
|
||||||
|
const initialIndex = CoreMath.clamp(this.options.initialSlide as number, 0, items.length - 1);
|
||||||
|
|
||||||
|
const initialItemData = {
|
||||||
|
index: initialIndex,
|
||||||
|
item: items[initialIndex],
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.setSelectedItem(items[initialIndex]);
|
||||||
|
this.onWillChange.emit(initialItemData);
|
||||||
|
this.onDidChange.emit(initialItemData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slide to a certain index.
|
||||||
|
*
|
||||||
|
* @param index Index.
|
||||||
|
* @param speed Animation speed.
|
||||||
|
* @param runCallbacks Whether to run callbacks.
|
||||||
|
*/
|
||||||
|
slideToIndex(index: number, speed?: number, runCallbacks?: boolean): void {
|
||||||
|
this.slides?.slideTo(index, speed, runCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slide to a certain item.
|
||||||
|
*
|
||||||
|
* @param item Item.
|
||||||
|
* @param speed Animation speed.
|
||||||
|
* @param runCallbacks Whether to run callbacks.
|
||||||
|
*/
|
||||||
|
slideToItem(item: Item, speed?: number, runCallbacks?: boolean): void {
|
||||||
|
const index = this.manager?.getSource().getItemIndex(item) ?? -1;
|
||||||
|
if (index != -1) {
|
||||||
|
this.slides?.slideTo(index, speed, runCallbacks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slide to next slide.
|
||||||
|
*
|
||||||
|
* @param speed Animation speed.
|
||||||
|
* @param runCallbacks Whether to run callbacks.
|
||||||
|
*/
|
||||||
|
slideNext(speed?: number, runCallbacks?: boolean): void {
|
||||||
|
this.slides?.slideNext(speed, runCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slide to previous slide.
|
||||||
|
*
|
||||||
|
* @param speed Animation speed.
|
||||||
|
* @param runCallbacks Whether to run callbacks.
|
||||||
|
*/
|
||||||
|
slidePrev(speed?: number, runCallbacks?: boolean): void {
|
||||||
|
this.slides?.slidePrev(speed, runCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when items list has been updated.
|
||||||
|
*
|
||||||
|
* @param items New items.
|
||||||
|
*/
|
||||||
|
protected onItemsUpdated(): void {
|
||||||
|
const currentItem = this.manager?.getSelectedItem();
|
||||||
|
|
||||||
|
if (!currentItem || !this.manager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the same slide in case the list has changed.
|
||||||
|
const newIndex = this.manager.getSource().getItemIndex(currentItem) ?? -1;
|
||||||
|
if (newIndex != -1) {
|
||||||
|
this.slides?.slideTo(newIndex, 0, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slide will change.
|
||||||
|
*/
|
||||||
|
async slideWillChange(): Promise<void> {
|
||||||
|
const currentItemData = await this.getCurrentSlideItemData();
|
||||||
|
if (!currentItemData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.manager?.setSelectedItem(currentItemData.item);
|
||||||
|
|
||||||
|
this.onWillChange.emit(currentItemData);
|
||||||
|
|
||||||
|
if (this.options.scrollOnChange !== 'top') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll top. This can be improved in the future to keep the scroll for each slide.
|
||||||
|
const scrollElement = await this.content?.getScrollElement();
|
||||||
|
|
||||||
|
if (!scrollElement || CoreDomUtils.isElementOutsideOfScreen(scrollElement, this.hostElement, VerticalPoint.TOP)) {
|
||||||
|
// Scroll to top.
|
||||||
|
this.hostElement.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slide did change.
|
||||||
|
*/
|
||||||
|
async slideDidChange(): Promise<void> {
|
||||||
|
const currentItemData = await this.getCurrentSlideItemData();
|
||||||
|
if (!currentItemData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onDidChange.emit(currentItemData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current item and index based on current slide.
|
||||||
|
*
|
||||||
|
* @return Promise resolved with current item data. Null if not found.
|
||||||
|
*/
|
||||||
|
protected async getCurrentSlideItemData(): Promise<CoreSwipeCurrentItemData<Item> | null> {
|
||||||
|
if (!this.slides || !this.manager) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = await this.slides.getActiveIndex();
|
||||||
|
const items = this.manager.getSource().getItems();
|
||||||
|
const currentItem = items && items[index];
|
||||||
|
|
||||||
|
if (!currentItem) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
item: currentItem,
|
||||||
|
index,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.unsubscribe && this.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to pass to the component.
|
||||||
|
*
|
||||||
|
* @todo Change unknown with the right type once Swiper library is used.
|
||||||
|
*/
|
||||||
|
export type CoreSwipeSlidesOptions = Record<string, unknown> & {
|
||||||
|
scrollOnChange?: 'top' | 'none'; // Scroll behaviour on change slide. By default, none.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data about current item.
|
||||||
|
*/
|
||||||
|
export type CoreSwipeCurrentItemData<Item> = {
|
||||||
|
index: number;
|
||||||
|
item: Item;
|
||||||
|
};
|
|
@ -13,14 +13,14 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
|
import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source';
|
||||||
|
|
||||||
import { CoreUser, CoreUserData, CoreUserParticipant, CoreUserProvider } from '../services/user';
|
import { CoreUser, CoreUserData, CoreUserParticipant, CoreUserProvider } from '../services/user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a collection of course participants.
|
* Provides a collection of course participants.
|
||||||
*/
|
*/
|
||||||
export class CoreUserParticipantsSource extends CoreItemsManagerSource<CoreUserParticipant | CoreUserData> {
|
export class CoreUserParticipantsSource extends CoreRoutedItemsManagerSource<CoreUserParticipant | CoreUserData> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreUser, CoreUserParticipant, CoreUserData } from '@features/user/services/user';
|
import { CoreUser, CoreUserParticipant, CoreUserData } from '@features/user/services/user';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreUserParticipantsSource } from '@features/user/classes/participants-source';
|
import { CoreUserParticipantsSource } from '@features/user/classes/participants-source';
|
||||||
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays the list of course participants.
|
* Page that displays the list of course participants.
|
||||||
|
@ -48,7 +48,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
||||||
try {
|
try {
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
this.participants = new CoreUserParticipantsManager(
|
this.participants = new CoreUserParticipantsManager(
|
||||||
CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]),
|
CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]),
|
||||||
CoreUserParticipantsPage,
|
CoreUserParticipantsPage,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -107,7 +107,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSource = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]);
|
const newSource = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]);
|
||||||
|
|
||||||
this.searchQuery = null;
|
this.searchQuery = null;
|
||||||
this.searchInProgress = false;
|
this.searchInProgress = false;
|
||||||
|
@ -124,7 +124,10 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
||||||
async search(query: string): Promise<void> {
|
async search(query: string): Promise<void> {
|
||||||
CoreApp.closeKeyboard();
|
CoreApp.closeKeyboard();
|
||||||
|
|
||||||
const newSource = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId, query]);
|
const newSource = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
CoreUserParticipantsSource,
|
||||||
|
[this.courseId, query],
|
||||||
|
);
|
||||||
|
|
||||||
this.searchInProgress = true;
|
this.searchInProgress = true;
|
||||||
this.searchQuery = query;
|
this.searchQuery = query;
|
||||||
|
|
|
@ -27,9 +27,9 @@ 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 { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'page-core-user-profile',
|
selector: 'page-core-user-profile',
|
||||||
|
@ -90,7 +90,10 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
if (this.courseId && this.route.snapshot.data.swipeManagerSource === 'participants') {
|
if (this.courseId && this.route.snapshot.data.swipeManagerSource === 'participants') {
|
||||||
const search = CoreNavigator.getRouteParam('search');
|
const search = CoreNavigator.getRouteParam('search');
|
||||||
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId, search]);
|
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
CoreUserParticipantsSource,
|
||||||
|
[this.courseId, search],
|
||||||
|
);
|
||||||
this.users = new CoreUserSwipeItemsManager(source);
|
this.users = new CoreUserSwipeItemsManager(source);
|
||||||
|
|
||||||
this.users.start();
|
this.users.start();
|
||||||
|
@ -224,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
|
||||||
|
|
|
@ -769,21 +769,39 @@ export class CoreDomUtilsProvider {
|
||||||
*
|
*
|
||||||
* @param scrollEl The element that must be scrolled.
|
* @param scrollEl The element that must be scrolled.
|
||||||
* @param element DOM element to check.
|
* @param element DOM 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): boolean {
|
isElementOutsideOfScreen(
|
||||||
|
scrollEl: HTMLElement,
|
||||||
|
element: HTMLElement,
|
||||||
|
point: VerticalPoint = VerticalPoint.MID,
|
||||||
|
): boolean {
|
||||||
const elementRect = element.getBoundingClientRect();
|
const elementRect = element.getBoundingClientRect();
|
||||||
|
|
||||||
if (!elementRect) {
|
if (!elementRect) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementMidPoint = Math.round((elementRect.bottom + elementRect.top) / 2);
|
let elementPoint: number;
|
||||||
|
switch (point) {
|
||||||
|
case VerticalPoint.TOP:
|
||||||
|
elementPoint = elementRect.top;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VerticalPoint.BOTTOM:
|
||||||
|
elementPoint = elementRect.bottom;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VerticalPoint.MID:
|
||||||
|
elementPoint = Math.round((elementRect.bottom + elementRect.top) / 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const scrollElRect = scrollEl.getBoundingClientRect();
|
const scrollElRect = scrollEl.getBoundingClientRect();
|
||||||
const scrollTopPos = scrollElRect?.top || 0;
|
const scrollTopPos = scrollElRect?.top || 0;
|
||||||
|
|
||||||
return elementMidPoint > window.innerHeight || elementMidPoint < scrollTopPos;
|
return elementPoint > window.innerHeight || elementPoint < scrollTopPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2090,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',
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue