MOBILE-3927 swipe: Create swipe-slides component and refactor managers
parent
6674262bb7
commit
741880f8df
|
@ -13,13 +13,13 @@
|
|||
// limitations under the License.
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
* Provides a collection of user badges.
|
||||
*/
|
||||
export class AddonBadgesUserBadgesSource extends CoreItemsManagerSource<AddonBadgesUserBadge> {
|
||||
export class AddonBadgesUserBadgesSource extends CoreRoutedItemsManagerSource<AddonBadgesUserBadge> {
|
||||
|
||||
readonly COURSE_ID: number;
|
||||
readonly USER_ID: number;
|
||||
|
|
|
@ -24,8 +24,8 @@ import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/
|
|||
import { CoreNavigator } from '@services/navigator';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
|
||||
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||
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.
|
||||
|
@ -61,7 +61,10 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
|
|||
this.badgeLoaded = true;
|
||||
});
|
||||
|
||||
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(AddonBadgesUserBadgesSource, [this.courseId, this.userId]);
|
||||
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||
AddonBadgesUserBadgesSource,
|
||||
[this.courseId, this.userId],
|
||||
);
|
||||
this.badges = new CoreSwipeItemsManager(source);
|
||||
|
||||
this.badges.start();
|
||||
|
|
|
@ -23,7 +23,7 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
|||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||
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.
|
||||
|
@ -49,7 +49,7 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
|
|||
}
|
||||
|
||||
this.badges = new CoreListItemsManager(
|
||||
CoreItemsManagerSourcesTracker.getOrCreateSource(AddonBadgesUserBadgesSource, [courseId, userId]),
|
||||
CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(AddonBadgesUserBadgesSource, [courseId, userId]),
|
||||
AddonBadgesUserBadgesPage,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,8 @@
|
|||
<ion-col class="ion-text-center addon-calendar-period">
|
||||
<h2 id="addon-calendar-monthname">
|
||||
{{ periodName }}
|
||||
<ion-spinner *ngIf="!monthLoaded" class="addon-calendar-loading-month"></ion-spinner>
|
||||
<ion-spinner *ngIf="manager && !manager.getSource().monthLoaded" class="addon-calendar-loading-month">
|
||||
</ion-spinner>
|
||||
</h2>
|
||||
</ion-col>
|
||||
<ion-col class="ion-text-end" *ngIf="canNavigate">
|
||||
|
@ -30,14 +31,14 @@
|
|||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
<ion-slides *ngIf="loaded" (ionSlideWillChange)="slideChanged()" [options]="{initialSlide: 1}">
|
||||
<ion-slide *ngFor="let month of preloadedMonths">
|
||||
<core-swipe-slides [manager]="manager" (onWillChange)="slideChanged($event)">
|
||||
<ng-template let-item="item">
|
||||
<!-- Calendar view. -->
|
||||
<ion-grid class="addon-calendar-months" role="table" aria-describedby="addon-calendar-monthname">
|
||||
<div role="rowgroup">
|
||||
<!-- List of days. -->
|
||||
<ion-row role="row">
|
||||
<ion-col class="ion-text-center addon-calendar-weekday" *ngFor="let day of month.weekDays" role="columnheader">
|
||||
<ion-col class="ion-text-center addon-calendar-weekday" *ngFor="let day of item.weekDays" role="columnheader">
|
||||
<span class="sr-only">{{ day.fullname | translate }}</span>
|
||||
<span class="ion-hide-md-up" aria-hidden="true">{{ day.shortname | translate }}</span>
|
||||
<span class="ion-hide-md-down" aria-hidden="true">{{ day.fullname | translate }}</span>
|
||||
|
@ -46,7 +47,7 @@
|
|||
</div>
|
||||
<div role="rowgroup">
|
||||
<!-- Weeks. -->
|
||||
<ion-row *ngFor="let week of month.weeks" class="addon-calendar-week" role="row">
|
||||
<ion-row *ngFor="let week of item.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>
|
||||
|
@ -104,7 +105,7 @@
|
|||
</ion-row>
|
||||
</div>
|
||||
</ion-grid>
|
||||
</ion-slide>
|
||||
</ion-slides>
|
||||
</ng-template>
|
||||
</core-swipe-slides>
|
||||
|
||||
</core-loading>
|
||||
|
|
|
@ -42,7 +42,9 @@ import { AddonCalendarOffline } from '../../services/calendar-offline';
|
|||
import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreLocalNotifications } from '@services/local-notifications';
|
||||
import { IonSlides } from '@ionic/angular';
|
||||
import { CoreSwipeCurrentItemData, CoreSwipeSlidesComponent } from '@components/swipe-slides/swipe-slides';
|
||||
import { CoreSwipeSlidesDynamicItemsManagerSource } from '@classes/items-management/slides-dynamic-items-manager-source';
|
||||
import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/slides-dynamic-items-manager';
|
||||
|
||||
/**
|
||||
* Component that displays a calendar.
|
||||
|
@ -54,7 +56,7 @@ import { IonSlides } from '@ionic/angular';
|
|||
})
|
||||
export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestroy {
|
||||
|
||||
@ViewChild(IonSlides) slides?: IonSlides;
|
||||
@ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent<PreloadedMonth>;
|
||||
|
||||
@Input() initialYear?: number; // Initial year to load.
|
||||
@Input() initialMonth?: number; // Initial month to load.
|
||||
|
@ -65,22 +67,12 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
@Output() onDayClicked = new EventEmitter<{day: number; month: number; year: number}>();
|
||||
|
||||
periodName?: string;
|
||||
preloadedMonths: PreloadedMonth[] = [];
|
||||
manager?: CoreSwipeSlidesDynamicItemsManager<PreloadedMonth, AddonCalendarMonthSlidesItemsManagerSource>;
|
||||
loaded = false;
|
||||
monthLoaded = false;
|
||||
timeFormat?: string;
|
||||
isCurrentMonth = false;
|
||||
isPastMonth = false;
|
||||
|
||||
protected visibleMonth: YearAndMonth;
|
||||
protected categoriesRetrieved = false;
|
||||
protected categories: { [id: number]: CoreCategoryData } = {};
|
||||
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.
|
||||
// Observers.
|
||||
|
@ -95,7 +87,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
if (CoreLocalNotifications.isAvailable()) {
|
||||
// Re-schedule events if default time changes.
|
||||
this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
|
||||
this.preloadedMonths.forEach((month) => {
|
||||
this.manager?.getSource().getItems()?.forEach((month) => {
|
||||
if (!month.loaded) {
|
||||
return;
|
||||
}
|
||||
|
@ -121,30 +113,32 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
this.undeleteEvent(data.eventId);
|
||||
|
||||
// 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);
|
||||
const index = this.manager?.getSource().deletedEvents.indexOf(data.eventId);
|
||||
if (index !== undefined && index != -1) {
|
||||
this.manager?.getSource().deletedEvents.splice(index, 1);
|
||||
}
|
||||
},
|
||||
this.currentSiteId,
|
||||
);
|
||||
|
||||
this.differ = differs.find([]).create();
|
||||
|
||||
const now = new Date();
|
||||
|
||||
this.visibleMonth = {
|
||||
year: now.getFullYear(),
|
||||
monthNumber: now.getMonth() + 1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Component loaded.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.visibleMonth.year = this.initialYear ?? this.visibleMonth.year;
|
||||
this.visibleMonth.monthNumber = this.initialMonth ?? this.visibleMonth.monthNumber;
|
||||
const now = new Date();
|
||||
|
||||
this.canNavigate = typeof this.canNavigate == 'undefined' ? true : CoreUtils.isTrueOrOne(this.canNavigate);
|
||||
this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true :
|
||||
CoreUtils.isTrueOrOne(this.displayNavButtons);
|
||||
|
||||
const source = new AddonCalendarMonthSlidesItemsManagerSource(this, {
|
||||
year: this.initialYear ?? now.getFullYear(),
|
||||
monthNumber: this.initialMonth ?? now.getMonth() + 1,
|
||||
});
|
||||
this.manager = new CoreSwipeSlidesDynamicItemsManager(source);
|
||||
|
||||
this.calculateIsCurrentMonth();
|
||||
|
||||
|
@ -155,15 +149,13 @@ 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).
|
||||
*/
|
||||
ngDoCheck(): void {
|
||||
this.canNavigate = typeof this.canNavigate == 'undefined' ? true : CoreUtils.isTrueOrOne(this.canNavigate);
|
||||
this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true :
|
||||
CoreUtils.isTrueOrOne(this.displayNavButtons);
|
||||
const items = this.manager?.getSource().getItems();
|
||||
|
||||
if (this.preloadedMonths.length) {
|
||||
if (items?.length) {
|
||||
// Check if there's any change in the filter object.
|
||||
const changes = this.differ.diff(this.filter || {});
|
||||
if (changes) {
|
||||
this.preloadedMonths.forEach((month) => {
|
||||
items.forEach((month) => {
|
||||
if (month.loaded) {
|
||||
this.filterEvents(month.weeks);
|
||||
}
|
||||
|
@ -172,47 +164,20 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
}
|
||||
}
|
||||
|
||||
get timeFormat(): string {
|
||||
return this.manager?.getSource().timeFormat || 'core.strftimetime';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch contacts.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
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 {
|
||||
await Promise.all(promises);
|
||||
|
||||
await this.viewMonth();
|
||||
await this.manager?.getSource().fetchData();
|
||||
|
||||
await this.manager?.getSource().load();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
||||
}
|
||||
|
@ -221,20 +186,310 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
}
|
||||
|
||||
/**
|
||||
* Load or preload a month.
|
||||
* Load current month and preload next and previous ones.
|
||||
*
|
||||
* @param year Year.
|
||||
* @param monthNumber Month number.
|
||||
* @param preload Whether to "preload" the month. When preloading, no events will be fetched.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async loadMonth(month: YearAndMonth, preload = false): Promise<void> {
|
||||
// Check if it's already loaded.
|
||||
const existingMonth = this.findPreloadedMonth(month);
|
||||
if (existingMonth && ((existingMonth.loaded && !existingMonth.needsRefresh) || preload)) {
|
||||
async viewMonth(month: YearAndMonth): Promise<void> {
|
||||
// Calculate the period name. We don't use the one in result because it's in server's language.
|
||||
this.periodName = CoreTimeUtils.userDate(
|
||||
new Date(month.year, month.monthNumber - 1).getTime(),
|
||||
'core.strftimemonthyear',
|
||||
);
|
||||
this.calculateIsCurrentMonth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter events based on the filter popover.
|
||||
*
|
||||
* @param weeks Weeks with the events to filter.
|
||||
*/
|
||||
filterEvents(weeks: AddonCalendarWeek[]): void {
|
||||
weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
day.filteredEvents = AddonCalendarHelper.getFilteredEvents(
|
||||
day.eventsFormated || [],
|
||||
this.filter,
|
||||
this.manager?.getSource().categories || {},
|
||||
);
|
||||
|
||||
// Re-calculate some properties.
|
||||
AddonCalendarHelper.calculateDayData(day, day.filteredEvents);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh events.
|
||||
*
|
||||
* @param afterChange Whether the refresh is done after an event has changed or has been synced.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async refreshData(): Promise<void> {
|
||||
const visibleMonth = this.slides?.getCurrentItem() || null;
|
||||
|
||||
await this.manager?.getSource().invalidateContent(visibleMonth);
|
||||
|
||||
await this.fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load next month.
|
||||
*/
|
||||
loadNext(): void {
|
||||
this.slides?.slideNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load previous month.
|
||||
*/
|
||||
loadPrevious(): void {
|
||||
this.slides?.slidePrev();
|
||||
}
|
||||
|
||||
/**
|
||||
* An event was clicked.
|
||||
*
|
||||
* @param calendarEvent Calendar event..
|
||||
* @param event Mouse event.
|
||||
*/
|
||||
eventClicked(calendarEvent: AddonCalendarEventToDisplay, event: Event): void {
|
||||
this.onEventClicked.emit(calendarEvent.id);
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* A day was clicked.
|
||||
*
|
||||
* @param day Day.
|
||||
*/
|
||||
dayClicked(day: number): void {
|
||||
const visibleMonth = this.slides?.getCurrentItem();
|
||||
if (!visibleMonth) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onDayClicked.emit({ day: day, month: visibleMonth.monthNumber, year: visibleMonth.year });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is viewing the current month.
|
||||
*/
|
||||
calculateIsCurrentMonth(): void {
|
||||
const visibleMonth = this.slides?.getCurrentItem();
|
||||
if (!visibleMonth) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentTime = CoreTimeUtils.timestamp();
|
||||
this.isCurrentMonth = CoreTimeUtils.isCurrentMonth(visibleMonth);
|
||||
this.isPastMonth = CoreTimeUtils.isCurrentMonth(visibleMonth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to current month.
|
||||
*/
|
||||
async goToCurrentMonth(): Promise<void> {
|
||||
const now = new Date();
|
||||
const currentMonth = {
|
||||
monthNumber: now.getMonth() + 1,
|
||||
year: now.getFullYear(),
|
||||
};
|
||||
|
||||
this.slides?.slideToItem(currentMonth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Undelete a certain event.
|
||||
*
|
||||
* @param eventId Event ID.
|
||||
*/
|
||||
protected async undeleteEvent(eventId: number): Promise<void> {
|
||||
this.manager?.getSource().getItems()?.forEach((month) => {
|
||||
if (!month.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
month.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.
|
||||
*
|
||||
* @param event Event object.
|
||||
* @return True if it's in the past.
|
||||
*/
|
||||
isEventPast(event: { timestart: number; timeduration: number}): boolean {
|
||||
return (event.timestart + event.timeduration) < (this.currentTime || CoreTimeUtils.timestamp());
|
||||
}
|
||||
|
||||
/**
|
||||
* Slide has changed.
|
||||
*
|
||||
* @param data Data about new item.
|
||||
*/
|
||||
slideChanged(data: CoreSwipeCurrentItemData<PreloadedMonth>): void {
|
||||
this.viewMonth(data.item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.undeleteEventObserver?.off();
|
||||
this.obsDefaultTimeChange?.off();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloaded month.
|
||||
*/
|
||||
type PreloadedMonth = YearAndMonth & {
|
||||
loaded: boolean; // Whether the events have been loaded.
|
||||
weekDays: AddonCalendarWeekDaysTranslationKeys[];
|
||||
weeks: AddonCalendarWeek[];
|
||||
needsRefresh?: boolean; // Whether the events needs to be re-loaded.
|
||||
};
|
||||
|
||||
// CoreSwipeSlidesDynamicItemsManagerSource
|
||||
|
||||
/**
|
||||
* Helper to manage swiping within a collection of chapters.
|
||||
*/
|
||||
class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicItemsManagerSource<PreloadedMonth> {
|
||||
|
||||
monthLoaded = false;
|
||||
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;
|
||||
protected categoriesRetrieved = false;
|
||||
|
||||
constructor(component: AddonCalendarCalendarComponent, initialMonth: YearAndMonth) {
|
||||
super(initialMonth);
|
||||
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load categories to be able to filter events.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
const filtered = events.filter((event) => event.id > 0);
|
||||
this.offlineEditedEventsIds = filtered.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: YearAndMonth): string | number {
|
||||
return `${item.year}#${item.monthNumber}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getPreviousItem(item: YearAndMonth): YearAndMonth | null {
|
||||
return CoreTimeUtils.getPreviousMonth(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getNextItem(item: YearAndMonth): YearAndMonth | null {
|
||||
return CoreTimeUtils.getNextMonth(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async loadItemData(month: YearAndMonth, preload = false): Promise<PreloadedMonth | null> {
|
||||
// Check if it's already loaded.
|
||||
const existingMonth = this.getItem(month);
|
||||
if (existingMonth && ((existingMonth.loaded && !existingMonth.needsRefresh) || preload)) {
|
||||
return existingMonth;
|
||||
}
|
||||
|
||||
if (!preload) {
|
||||
this.monthLoaded = false;
|
||||
}
|
||||
|
@ -286,7 +541,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
|
||||
if (day.istoday) {
|
||||
day.eventsFormated?.forEach((event) => {
|
||||
event.ispast = this.isEventPast(event);
|
||||
event.ispast = this.calendarComponent.isEventPast(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -297,7 +552,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
// Merge the online events with offline data.
|
||||
this.mergeEvents(month, weeks);
|
||||
// Filter events by course.
|
||||
this.filterEvents(weeks);
|
||||
this.calendarComponent.filterEvents(weeks);
|
||||
}
|
||||
|
||||
if (existingMonth) {
|
||||
|
@ -306,47 +561,16 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
existingMonth.weeks = weeks;
|
||||
existingMonth.weekDays = weekDays;
|
||||
|
||||
return;
|
||||
return existingMonth;
|
||||
}
|
||||
|
||||
// Add the preloaded month at the right position.
|
||||
const preloadedMonth: PreloadedMonth = {
|
||||
return {
|
||||
...month,
|
||||
loaded: !preload,
|
||||
weeks,
|
||||
weekDays,
|
||||
};
|
||||
const previousMonth = CoreTimeUtils.getPreviousMonth(month);
|
||||
const nextMonth = CoreTimeUtils.getNextMonth(month);
|
||||
const activeIndex = await this.slides?.getActiveIndex();
|
||||
|
||||
const added = this.preloadedMonths.some((month, index) => {
|
||||
let positionToInsert = -1;
|
||||
if (CoreTimeUtils.isSameMonth(month, previousMonth)) {
|
||||
// Previous month found, add the month after it.
|
||||
positionToInsert = index + 1;
|
||||
}
|
||||
|
||||
if (CoreTimeUtils.isSameMonth(month, nextMonth)) {
|
||||
// Next month found, add the month before it.
|
||||
positionToInsert = index;
|
||||
}
|
||||
|
||||
if (positionToInsert > -1) {
|
||||
this.preloadedMonths.splice(positionToInsert, 0, preloadedMonth);
|
||||
if (activeIndex !== undefined && positionToInsert <= activeIndex) {
|
||||
// Added a slide before the active one, keep current slide.
|
||||
this.slides?.slideTo(activeIndex + 1, 0, false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!added) {
|
||||
// Previous and next months not found, this probably means the array is still empty. Add it at the end.
|
||||
this.preloadedMonths.push(preloadedMonth);
|
||||
}
|
||||
} finally {
|
||||
if (!preload) {
|
||||
this.monthLoaded = true;
|
||||
|
@ -354,168 +578,13 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current month and preload next and previous ones.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async viewMonth(): Promise<void> {
|
||||
// Calculate the period name. We don't use the one in result because it's in server's language.
|
||||
this.periodName = CoreTimeUtils.userDate(
|
||||
new Date(this.visibleMonth.year, this.visibleMonth.monthNumber - 1).getTime(),
|
||||
'core.strftimemonthyear',
|
||||
);
|
||||
this.calculateIsCurrentMonth();
|
||||
|
||||
try {
|
||||
// Load current month, and preload next and previous ones.
|
||||
await Promise.all([
|
||||
this.loadMonth(this.visibleMonth, false),
|
||||
this.loadMonth(CoreTimeUtils.getPreviousMonth(this.visibleMonth), true),
|
||||
this.loadMonth(CoreTimeUtils.getNextMonth(this.visibleMonth), true),
|
||||
]);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param weeks Weeks with the events to filter.
|
||||
*/
|
||||
filterEvents(weeks: AddonCalendarWeek[]): void {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh events.
|
||||
*
|
||||
* @param afterChange Whether the refresh is done after an event has changed or has been synced.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async refreshData(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(AddonCalendar.invalidateMonthlyEvents(this.visibleMonth.year, this.visibleMonth.monthNumber));
|
||||
promises.push(CoreCourses.invalidateCategories(0, true));
|
||||
promises.push(AddonCalendar.invalidateTimeFormat());
|
||||
|
||||
this.categoriesRetrieved = false; // Get categories again.
|
||||
|
||||
const preloadedMonth = this.findPreloadedMonth(this.visibleMonth);
|
||||
if (preloadedMonth) {
|
||||
preloadedMonth.needsRefresh = true;
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load next month.
|
||||
*/
|
||||
loadNext(): void {
|
||||
this.slides?.slideNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load previous month.
|
||||
*/
|
||||
loadPrevious(): void {
|
||||
this.slides?.slidePrev();
|
||||
}
|
||||
|
||||
/**
|
||||
* An event was clicked.
|
||||
*
|
||||
* @param calendarEvent Calendar event..
|
||||
* @param event Mouse event.
|
||||
*/
|
||||
eventClicked(calendarEvent: AddonCalendarEventToDisplay, event: Event): void {
|
||||
this.onEventClicked.emit(calendarEvent.id);
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* A day was clicked.
|
||||
*
|
||||
* @param day Day.
|
||||
*/
|
||||
dayClicked(day: number): void {
|
||||
this.onDayClicked.emit({ day: day, month: this.visibleMonth.monthNumber, year: this.visibleMonth.year });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is viewing the current month.
|
||||
*/
|
||||
calculateIsCurrentMonth(): void {
|
||||
this.currentTime = CoreTimeUtils.timestamp();
|
||||
this.isCurrentMonth = CoreTimeUtils.isCurrentMonth(this.visibleMonth);
|
||||
this.isPastMonth = CoreTimeUtils.isCurrentMonth(this.visibleMonth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to current month.
|
||||
*/
|
||||
async goToCurrentMonth(): Promise<void> {
|
||||
|
||||
const now = new Date();
|
||||
const currentMonth = {
|
||||
monthNumber: now.getMonth() + 1,
|
||||
year: now.getFullYear(),
|
||||
};
|
||||
|
||||
const index = this.preloadedMonths.findIndex((month) => CoreTimeUtils.isSameMonth(month, currentMonth));
|
||||
if (index > -1) {
|
||||
this.slides?.slideTo(index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge online events with the offline events of that period.
|
||||
*
|
||||
* @param month Month.
|
||||
* @param weeks Weeks with the events to filter.
|
||||
*/
|
||||
protected mergeEvents(month: YearAndMonth, weeks: AddonCalendarWeek[]): void {
|
||||
mergeEvents(month: YearAndMonth, weeks: AddonCalendarWeek[]): void {
|
||||
const monthOfflineEvents: { [day: number]: AddonCalendarEventToDisplay[] } =
|
||||
this.offlineEvents[AddonCalendarHelper.getMonthId(month.year, month.monthNumber)];
|
||||
|
||||
|
@ -551,86 +620,27 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
}
|
||||
|
||||
/**
|
||||
* Undelete a certain event.
|
||||
*
|
||||
* @param eventId Event ID.
|
||||
*/
|
||||
protected async undeleteEvent(eventId: number): Promise<void> {
|
||||
this.preloadedMonths.forEach((month) => {
|
||||
if (!month.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
month.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.
|
||||
*
|
||||
* @param event Event object.
|
||||
* @return True if it's in the past.
|
||||
*/
|
||||
protected isEventPast(event: { timestart: number; timeduration: number}): boolean {
|
||||
return (event.timestart + event.timeduration) < (this.currentTime || CoreTimeUtils.timestamp());
|
||||
}
|
||||
|
||||
/**
|
||||
* Slide has changed.
|
||||
* Invalidate content.
|
||||
*
|
||||
* @param visibleMonth The current visible month.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async slideChanged(): Promise<void> {
|
||||
if (!this.slides) {
|
||||
return;
|
||||
async invalidateContent(visibleMonth: PreloadedMonth | null): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
if (visibleMonth) {
|
||||
promises.push(AddonCalendar.invalidateMonthlyEvents(visibleMonth.year, visibleMonth.monthNumber));
|
||||
}
|
||||
promises.push(CoreCourses.invalidateCategories(0, true));
|
||||
promises.push(AddonCalendar.invalidateTimeFormat());
|
||||
|
||||
this.categoriesRetrieved = false; // Get categories again.
|
||||
|
||||
if (visibleMonth) {
|
||||
visibleMonth.needsRefresh = true;
|
||||
}
|
||||
|
||||
const index = await this.slides.getActiveIndex();
|
||||
const preloadedMonth = this.preloadedMonths[index];
|
||||
if (!preloadedMonth) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.visibleMonth.year = preloadedMonth.year;
|
||||
this.visibleMonth.monthNumber = preloadedMonth.monthNumber;
|
||||
|
||||
await this.viewMonth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a certain preloaded month.
|
||||
*
|
||||
* @param month Month to search.
|
||||
* @return Preloaded month, undefined if not found.
|
||||
*/
|
||||
protected findPreloadedMonth(month: YearAndMonth): PreloadedMonth | undefined {
|
||||
return this.preloadedMonths.find(preloadedMonth => CoreTimeUtils.isSameMonth(month, preloadedMonth));
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.undeleteEventObserver?.off();
|
||||
this.obsDefaultTimeChange?.off();
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloaded month.
|
||||
*/
|
||||
type PreloadedMonth = YearAndMonth & {
|
||||
loaded: boolean; // Whether the events have been loaded.
|
||||
weekDays: AddonCalendarWeekDaysTranslationKeys[];
|
||||
weeks: AddonCalendarWeek[];
|
||||
needsRefresh?: boolean; // Whether the events needs to be re-loaded.
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
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 { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
@ -33,7 +33,7 @@ import { AddonModAssignSync, AddonModAssignSyncProvider } from '../services/assi
|
|||
/**
|
||||
* Provides a collection of assignment submissions.
|
||||
*/
|
||||
export class AddonModAssignSubmissionsSource extends CoreItemsManagerSource<AddonModAssignSubmissionForList> {
|
||||
export class AddonModAssignSubmissionsSource extends CoreRoutedItemsManagerSource<AddonModAssignSubmissionForList> {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
// limitations under the License.
|
||||
|
||||
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 { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { CoreGroupInfo } from '@services/groups';
|
||||
|
@ -86,7 +86,7 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
|||
const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||
const groupId = CoreNavigator.getRouteNumberParam('groupId') || 0;
|
||||
const selectedStatus = CoreNavigator.getRouteParam('status');
|
||||
const submissionsSource = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||
const submissionsSource = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||
AddonModAssignSubmissionsSource,
|
||||
[courseId, moduleId, selectedStatus],
|
||||
);
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
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 { CoreCourse } from '@features/course/services/course';
|
||||
import { CanLeave } from '@guards/can-leave';
|
||||
|
@ -64,7 +64,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, OnDestroy, Ca
|
|||
this.blindId = CoreNavigator.getRouteNumberParam('blindId', { params });
|
||||
const groupId = CoreNavigator.getRequiredRouteNumberParam('groupId');
|
||||
const selectedStatus = CoreNavigator.getRouteParam('selectedStatus');
|
||||
const submissionsSource = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||
const submissionsSource = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||
AddonModAssignSubmissionsSource,
|
||||
[this.courseId, this.moduleId, selectedStatus],
|
||||
);
|
||||
|
|
|
@ -40,18 +40,18 @@
|
|||
previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)">
|
||||
</core-navigation-bar>
|
||||
|
||||
<ion-slides (ionSlideWillChange)="slideChanged()" [options]="slidesOpts">
|
||||
<ion-slide *ngFor="let chapter of loadedChapters">
|
||||
<core-swipe-slides [manager]="manager" [options]="slidesOpts" (onWillChange)="slideChanged($event)">
|
||||
<ng-template let-item="item">
|
||||
<div class="ion-padding">
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="chapter.content" contextLevel="module"
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="item.content" contextLevel="module"
|
||||
[contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
|
||||
<div class="ion-margin-top" *ngIf="tagsEnabled && chapter.tags?.length > 0">
|
||||
<div class="ion-margin-top" *ngIf="tagsEnabled && item.tags?.length > 0">
|
||||
<strong>{{ 'core.tag.tags' | translate }}: </strong>
|
||||
<core-tag-list [tags]="chapter.tags"></core-tag-list>
|
||||
<core-tag-list [tags]="item.tags"></core-tag-list>
|
||||
</div>
|
||||
</div>
|
||||
</ion-slide>
|
||||
</ion-slides>
|
||||
</ng-template>
|
||||
</core-swipe-slides>
|
||||
</div>
|
||||
|
||||
</core-loading>
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, Optional, Input, OnInit, ViewChild, ElementRef } from '@angular/core';
|
||||
import { IonContent, IonSlides } from '@ionic/angular';
|
||||
import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component';
|
||||
import {
|
||||
AddonModBookProvider,
|
||||
|
@ -29,10 +28,13 @@ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents
|
|||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { AddonModBookTocComponent } from '../toc/toc';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreSwipeCurrentItemData, CoreSwipeSlidesComponent, CoreSwipeSlidesOptions } from '@components/swipe-slides/swipe-slides';
|
||||
import { CoreSwipeSlidesItemsManagerSource } from '@classes/items-management/slides-items-manager-source';
|
||||
import { CoreCourseModule } from '@features/course/services/course-helper';
|
||||
import { CoreSwipeSlidesItemsManager } from '@classes/items-management/slides-items-manager';
|
||||
|
||||
/**
|
||||
* Component that displays a book.
|
||||
|
@ -40,38 +42,32 @@ import { Translate } from '@singletons';
|
|||
@Component({
|
||||
selector: 'addon-mod-book-index',
|
||||
templateUrl: 'addon-mod-book-index.html',
|
||||
styleUrls: ['index.scss'],
|
||||
})
|
||||
export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit {
|
||||
|
||||
@ViewChild(IonSlides) slides?: IonSlides;
|
||||
@ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent;
|
||||
|
||||
@Input() initialChapterId?: number; // The initial chapter ID to load.
|
||||
|
||||
component = AddonModBookProvider.COMPONENT;
|
||||
loadedChapters: LoadedChapter[] = [];
|
||||
manager?: CoreSwipeSlidesItemsManager<LoadedChapter, AddonModBookSlidesItemsManagerSource>;
|
||||
previousChapter?: AddonModBookTocChapter;
|
||||
nextChapter?: AddonModBookTocChapter;
|
||||
tagsEnabled = false;
|
||||
warning = '';
|
||||
tags?: CoreTagItem[];
|
||||
displayNavBar = true;
|
||||
navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
|
||||
displayTitlesInNavBar = false;
|
||||
slidesOpts = {
|
||||
initialSlide: 0,
|
||||
slidesOpts: CoreSwipeSlidesOptions = {
|
||||
autoHeight: true,
|
||||
scrollOnChange: 'top',
|
||||
};
|
||||
|
||||
protected chapters: AddonModBookTocChapter[] = [];
|
||||
protected currentChapter?: number;
|
||||
protected book?: AddonModBookBookWSData;
|
||||
protected contentsMap: AddonModBookContentsMap = {};
|
||||
protected element: HTMLElement;
|
||||
|
||||
constructor(
|
||||
elementRef: ElementRef,
|
||||
protected content?: IonContent,
|
||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||
) {
|
||||
super('AddonModBookIndexComponent', courseContentsPage);
|
||||
|
@ -86,9 +82,25 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
|||
super.ngOnInit();
|
||||
|
||||
this.tagsEnabled = CoreTag.areTagsAvailableInSite();
|
||||
const source = new AddonModBookSlidesItemsManagerSource(
|
||||
this.courseId,
|
||||
this.module,
|
||||
this.tagsEnabled,
|
||||
this.initialChapterId,
|
||||
);
|
||||
this.manager = new CoreSwipeSlidesItemsManager(source);
|
||||
|
||||
this.loadContent();
|
||||
}
|
||||
|
||||
get book(): AddonModBookBookWSData | undefined {
|
||||
return this.manager?.getSource().book;
|
||||
}
|
||||
|
||||
get chapters(): AddonModBookTocChapter[] {
|
||||
return this.manager?.getSource().chapters || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the TOC.
|
||||
*/
|
||||
|
@ -121,10 +133,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
|||
return;
|
||||
}
|
||||
|
||||
const index = this.loadedChapters.findIndex(chapter => chapter.id === chapterId);
|
||||
if (index > -1) {
|
||||
this.slides?.slideTo(index);
|
||||
}
|
||||
this.slides?.slideToItem({ id: chapterId });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -132,8 +141,8 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
|||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected invalidateContent(): Promise<void> {
|
||||
return AddonModBook.invalidateContent(this.module.id, this.courseId);
|
||||
protected async invalidateContent(): Promise<void> {
|
||||
await this.manager?.getSource().invalidateContent();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -144,100 +153,35 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
|||
*/
|
||||
protected async fetchContent(refresh = false): Promise<void> {
|
||||
try {
|
||||
const downloadResult = await this.downloadResourceIfNeeded(refresh);
|
||||
|
||||
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 index = this.chapters.findIndex((chapter) => chapter.id == this.initialChapterId);
|
||||
|
||||
if (index >= 0) {
|
||||
this.currentChapter = this.initialChapterId;
|
||||
this.slidesOpts.initialSlide = index;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.currentChapter === undefined) {
|
||||
// Load the first chapter.
|
||||
this.currentChapter = AddonModBook.getFirstChapter(this.chapters);
|
||||
}
|
||||
|
||||
if (this.currentChapter === undefined) {
|
||||
const source = this.manager?.getSource();
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadChapters();
|
||||
const downloadResult = await this.downloadResourceIfNeeded(refresh);
|
||||
|
||||
// Show chapter.
|
||||
await this.viewChapter(this.currentChapter, refresh);
|
||||
const book = await source.loadBookData();
|
||||
|
||||
this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : '';
|
||||
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 {
|
||||
// Pass false because downloadResourceIfNeeded already invalidates and refresh data if refresh=true.
|
||||
this.fillContextMenu(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load book data from WS.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadBookData(): Promise<void> {
|
||||
this.book = await AddonModBook.getBook(this.courseId, this.module.id);
|
||||
|
||||
this.dataRetrieved.emit(this.book);
|
||||
|
||||
this.description = this.book.intro;
|
||||
this.displayNavBar = this.book.navstyle != AddonModBookNavStyle.TOC_ONLY;
|
||||
this.displayTitlesInNavBar = this.book.navstyle == AddonModBookNavStyle.TEXT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load book chapters.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadChapters(): Promise<void> {
|
||||
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.tagsEnabled ? this.contentsMap[chapter.id].tags : [],
|
||||
};
|
||||
}));
|
||||
|
||||
let newIndex = -1;
|
||||
if (this.loadedChapters.length && newChapters.length != this.loadedChapters.length) {
|
||||
// Number of chapters has changed. Search the chapter to display, otherwise it could change automatically.
|
||||
newIndex = this.chapters.findIndex((chapter) => chapter.id === this.currentChapter);
|
||||
}
|
||||
|
||||
this.loadedChapters = newChapters;
|
||||
|
||||
if (newIndex > -1) {
|
||||
this.slides?.slideTo(newIndex, 0, false);
|
||||
}
|
||||
} catch (exception) {
|
||||
const error = exception ?? new CoreError(Translate.instant('addon.mod_book.errorchapter'));
|
||||
if (!error.message) {
|
||||
error.message = Translate.instant('addon.mod_book.errorchapter');
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View a book chapter.
|
||||
*
|
||||
|
@ -271,24 +215,10 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
|||
/**
|
||||
* Slide has changed.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
* @param data Data about new item.
|
||||
*/
|
||||
async slideChanged(): Promise<void> {
|
||||
if (!this.slides) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollElement = await this.content?.getScrollElement();
|
||||
const container = this.element.querySelector<HTMLElement>('.addon-mod_book-container');
|
||||
|
||||
if (container && (!scrollElement || CoreDomUtils.isElementOutsideOfScreen(scrollElement, container, 'top'))) {
|
||||
// Scroll to top.
|
||||
container.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
const index = await this.slides.getActiveIndex();
|
||||
|
||||
this.viewChapter(this.loadedChapters[index].id, true);
|
||||
slideChanged(data: CoreSwipeCurrentItemData<LoadedChapter>): void {
|
||||
this.viewChapter(data.item.id, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -313,3 +243,90 @@ type LoadedChapter = {
|
|||
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> {
|
||||
// Get contents. No need to refresh, it has been done in downloadResourceIfNeeded.
|
||||
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 (exception) {
|
||||
const error = exception ?? new CoreError(Translate.instant('addon.mod_book.errorchapter'));
|
||||
if (!error.message) {
|
||||
error.message = 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.
|
||||
|
||||
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 {
|
||||
AddonModForum,
|
||||
|
@ -24,7 +24,7 @@ import {
|
|||
} from '../services/forum';
|
||||
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 };
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
|||
import { ContextLevel } from '@/core/constants';
|
||||
import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
||||
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.
|
||||
|
@ -149,7 +149,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
await super.ngOnInit();
|
||||
|
||||
// Initialize discussions manager.
|
||||
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||
AddonModForumDiscussionsSource,
|
||||
[this.courseId, this.module.id, this.courseContentsPage ? `${AddonModForumModuleHandlerService.PAGE_NAME}/` : ''],
|
||||
);
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
import { ContextLevel, CoreConstants } from '@/core/constants';
|
||||
import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Optional } from '@angular/core';
|
||||
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 { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||
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)) {
|
||||
this.discussions = new AddonModForumDiscussionDiscussionsSwipeManager(
|
||||
CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||
CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||
AddonModForumDiscussionsSource,
|
||||
[this.courseId, this.cmId, routeData.discussionsPathPrefix ?? ''],
|
||||
),
|
||||
|
|
|
@ -42,8 +42,8 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
|||
import { CoreForms } from '@singletons/form';
|
||||
import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discussions-swipe-manager';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||
import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||
|
||||
type NewDiscussionData = {
|
||||
subject: string;
|
||||
|
@ -117,7 +117,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
|
|||
this.timeCreated = CoreNavigator.getRequiredRouteNumberParam('timeCreated');
|
||||
|
||||
if (this.timeCreated !== 0 && (routeData.swipeEnabled ?? true)) {
|
||||
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||
AddonModForumDiscussionsSource,
|
||||
[this.courseId, this.cmId, routeData.discussionsPathPrefix ?? ''],
|
||||
);
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
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 {
|
||||
AddonModGlossary,
|
||||
AddonModGlossaryEntry,
|
||||
|
@ -27,7 +27,7 @@ import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '../servic
|
|||
/**
|
||||
* 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 };
|
||||
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
import { ContextLevel } from '@/core/constants';
|
||||
import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
|
||||
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 { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||
|
@ -113,7 +113,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
|||
await super.ngOnInit();
|
||||
|
||||
// Initialize entries manager.
|
||||
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||
AddonModGlossaryEntriesSource,
|
||||
[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 { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||
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 { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||
import { CanLeave } from '@guards/can-leave';
|
||||
|
@ -101,7 +101,7 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
|||
this.editorExtraParams.timecreated = this.timecreated;
|
||||
|
||||
if (this.timecreated !== 0 && (routeData.swipeEnabled ?? true)) {
|
||||
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||
AddonModGlossaryEntriesSource,
|
||||
[this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
|
||||
);
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
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 { CoreComments } from '@features/comments/services/comments';
|
||||
import { CoreRatingInfo } from '@features/rating/services/rating';
|
||||
|
@ -73,7 +73,7 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
|||
|
||||
if (routeData.swipeEnabled ?? true) {
|
||||
const cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||
AddonModGlossaryEntriesSource,
|
||||
[this.courseId, cmId, routeData.glossaryPathPrefix ?? ''],
|
||||
);
|
||||
|
|
|
@ -12,13 +12,11 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Params } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Updates listener.
|
||||
*/
|
||||
export interface CoreItemsListSourceListener<Item> {
|
||||
onItemsUpdated?(items: Item[], hasMoreItems: boolean): void;
|
||||
onItemsUpdated?(items: Item[]): void;
|
||||
onReset?(): void;
|
||||
}
|
||||
|
||||
|
@ -27,39 +25,19 @@ export interface CoreItemsListSourceListener<Item> {
|
|||
*/
|
||||
export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||
|
||||
/**
|
||||
* 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('-');
|
||||
}
|
||||
|
||||
protected items: Item[] | null = null;
|
||||
protected hasMoreItems = true;
|
||||
protected listeners: CoreItemsListSourceListener<Item>[] = [];
|
||||
protected dirty = false;
|
||||
|
||||
/**
|
||||
* Check whether any page has been loaded.
|
||||
* Check whether any item has been loaded.
|
||||
*
|
||||
* @returns Whether any page has been loaded.
|
||||
* @returns Whether any item has been loaded.
|
||||
*/
|
||||
isLoaded(): boolean {
|
||||
return this.items !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether there are more pages to be loaded.
|
||||
*
|
||||
* @return Whether there are more pages to be loaded.
|
||||
*/
|
||||
isCompleted(): boolean {
|
||||
return !this.hasMoreItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the source as dirty.
|
||||
*
|
||||
|
@ -80,35 +58,21 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
|||
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(): void {
|
||||
this.items = null;
|
||||
this.hasMoreItems = true;
|
||||
this.dirty = false;
|
||||
|
||||
this.listeners.forEach(listener => listener.onReset?.call(listener));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load items.
|
||||
*/
|
||||
abstract load(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Register a listener.
|
||||
*
|
||||
|
@ -136,85 +100,22 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
|||
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.
|
||||
*
|
||||
* @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.hasMoreItems = hasMoreItems;
|
||||
|
||||
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,16 +12,15 @@
|
|||
// 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 { CoreItemsManagerSource } from './items-manager-source';
|
||||
import { CoreItemsManagerSourcesTracker } from './items-manager-sources-tracker';
|
||||
|
||||
/**
|
||||
* 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 itemsMap: Record<string, Item> | null = null;
|
||||
|
@ -51,8 +50,6 @@ export abstract class CoreItemsManager<Item = unknown, Source extends CoreItemsM
|
|||
*/
|
||||
setSource(newSource: Source | null): void {
|
||||
if (this.source) {
|
||||
CoreItemsManagerSourcesTracker.removeReference(this.source.instance, this);
|
||||
|
||||
this.source.unsubscribe();
|
||||
delete this.source;
|
||||
|
||||
|
@ -60,8 +57,6 @@ export abstract class CoreItemsManager<Item = unknown, Source extends CoreItemsM
|
|||
}
|
||||
|
||||
if (newSource) {
|
||||
CoreItemsManagerSourcesTracker.addReference(newSource, this);
|
||||
|
||||
this.source = {
|
||||
instance: newSource,
|
||||
unsubscribe: newSource.addListener({
|
||||
|
@ -86,79 +81,21 @@ 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;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
getSelectedItem(): Item | null {
|
||||
return this.selectedItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
route = route ?? this.getCurrentPageRoute()?.snapshot ?? null;
|
||||
|
||||
const selectedItemPath = this.getSelectedItemPath(route);
|
||||
|
||||
this.selectedItem = selectedItemPath
|
||||
? this.itemsMap?.[selectedItemPath] ?? null
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 });
|
||||
setSelectedItem(item: Item | null): void {
|
||||
this.selectedItem = item;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -166,14 +103,9 @@ export abstract class CoreItemsManager<Item = unknown, Source extends CoreItemsM
|
|||
*
|
||||
* @param items New items.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected onSourceItemsUpdated(items: Item[]): void {
|
||||
this.itemsMap = items.reduce((map, item) => {
|
||||
map[this.getSource().getItemPath(item)] = item;
|
||||
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
this.updateSelectedItem();
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,16 +20,16 @@ import { CoreNavigator } from '@services/navigator';
|
|||
import { CoreScreen } from '@services/screen';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
||||
import { CoreItemsManager } from './items-manager';
|
||||
import { CoreItemsManagerSource } from './items-manager-source';
|
||||
import { CoreRoutedItemsManagerSource } from './routed-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.
|
||||
*/
|
||||
export class CoreListItemsManager<
|
||||
Item = unknown,
|
||||
Source extends CoreItemsManagerSource<Item> = CoreItemsManagerSource<Item>
|
||||
> extends CoreItemsManager<Item, Source> {
|
||||
Source extends CoreRoutedItemsManagerSource<Item> = CoreRoutedItemsManagerSource<Item>
|
||||
> extends CoreRoutedItemsManager<Item, Source> {
|
||||
|
||||
protected pageRouteLocator?: unknown | ActivatedRoute;
|
||||
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
|
||||
// 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;
|
||||
new (...args: unknown[]): T;
|
||||
};
|
||||
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>;
|
||||
|
||||
/**
|
||||
* 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 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.
|
||||
|
@ -37,7 +37,7 @@ export class CoreItemsManagerSourcesTracker {
|
|||
* @param constructorArguments Arguments to create a new instance, used to find out if an instance already exists.
|
||||
* @returns Source.
|
||||
*/
|
||||
static getOrCreateSource<T extends CoreItemsManagerSource, C extends SourceConstructor<T>>(
|
||||
static getOrCreateSource<T extends CoreRoutedItemsManagerSource, C extends SourceConstructor<T>>(
|
||||
constructor: C,
|
||||
constructorArguments: ConstructorParameters<C>,
|
||||
): SourceConstuctorInstance<C> {
|
||||
|
@ -54,7 +54,7 @@ export class CoreItemsManagerSourcesTracker {
|
|||
* @param source 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 instanceId = this.instanceIds.get(source);
|
||||
|
||||
|
@ -78,7 +78,7 @@ export class CoreItemsManagerSourcesTracker {
|
|||
* @param source 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 instanceId = this.instanceIds.get(source);
|
||||
const index = constructorInstances?.[instanceId ?? '']?.references.indexOf(reference) ?? -1;
|
||||
|
@ -127,7 +127,7 @@ export class CoreItemsManagerSourcesTracker {
|
|||
* @param constructorArguments Source constructor arguments.
|
||||
* @returns Source instance.
|
||||
*/
|
||||
private static createInstance<T extends CoreItemsManagerSource>(
|
||||
private static createInstance<T extends CoreRoutedItemsManagerSource>(
|
||||
id: string,
|
||||
constructor: SourceConstructor<T>,
|
||||
constructorArguments: ConstructorParameters<SourceConstructor<T>>,
|
|
@ -0,0 +1,135 @@
|
|||
// (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 {
|
||||
this.itemsMap = items.reduce((map, item) => {
|
||||
map[this.getSource().getItemPath(item)] = item;
|
||||
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
this.updateSelectedItem();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
// (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 './slides-items-manager-source';
|
||||
|
||||
/**
|
||||
* Items collection source data for "swipe slides".
|
||||
*/
|
||||
export abstract class CoreSwipeSlidesDynamicItemsManagerSource<Item = unknown> extends CoreSwipeSlidesItemsManagerSource<Item> {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async load(): Promise<void> {
|
||||
if (this.initialItem) {
|
||||
// Load the initial item.
|
||||
await this.loadItem(this.initialItem);
|
||||
}
|
||||
|
||||
this.setInitialized();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async loadItems(): Promise<Item[]> {
|
||||
// Not used in dynamic slides.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a certain item and preload next and previous ones.
|
||||
*
|
||||
* @param item Basic data about the item to load.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async loadItem(item: Partial<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) : undefined,
|
||||
nextItem ? this.loadItemInList(nextItem, true) : undefined,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load or preload a certain item and add it to the list.
|
||||
*
|
||||
* @param item Basic data about the item to load.
|
||||
* @param preload Whether to preload.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async loadItemInList(item: Partial<Item>, preload = false): Promise<void> {
|
||||
const preloadedItem = await this.loadItemData(item, preload);
|
||||
if (!preloadedItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the item at the right position.
|
||||
const existingItem = this.getItem(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 positionToInsert = -1;
|
||||
if (itemId === previousItemId) {
|
||||
// Previous item found, add the item after it.
|
||||
positionToInsert = index + 1;
|
||||
}
|
||||
|
||||
if (itemId === nextItemId) {
|
||||
// Next item found, add the item before it.
|
||||
positionToInsert = index;
|
||||
}
|
||||
|
||||
if (positionToInsert > -1) {
|
||||
this.items?.splice(positionToInsert, 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.
|
||||
*
|
||||
* @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: Partial<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: Partial<Item>): Partial<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: Partial<Item>): Partial<Item> | null;
|
||||
|
||||
}
|
|
@ -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 { CoreSwipeSlidesDynamicItemsManagerSource } from './slides-dynamic-items-manager-source';
|
||||
import { CoreSwipeSlidesItemsManager } from './slides-items-manager';
|
||||
|
||||
/**
|
||||
* Helper class to manage items for core-swipe-slides.
|
||||
*/
|
||||
export class CoreSwipeSlidesDynamicItemsManager<
|
||||
Item = unknown,
|
||||
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,121 @@
|
|||
// (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?: Partial<Item>;
|
||||
protected initialized = false;
|
||||
protected initializePromise: Promise<void>;
|
||||
protected resolveInitialize!: () => void;
|
||||
|
||||
constructor(initialItem?: Partial<Item>) {
|
||||
super();
|
||||
|
||||
this.initialItem = initialItem;
|
||||
this.initializePromise = new Promise(resolve => this.resolveInitialize = resolve);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
isLoaded(): boolean {
|
||||
return this.initialized && super.isLoaded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a promise that is resolved when the source is initialized.
|
||||
*
|
||||
* @return Promise.
|
||||
*/
|
||||
waitForInitialized(): Promise<void> {
|
||||
return this.initializePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the source as initialized.
|
||||
*/
|
||||
protected setInitialized(): void {
|
||||
this.initialized = true;
|
||||
this.resolveInitialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async load(): Promise<void> {
|
||||
const items = await this.loadItems();
|
||||
|
||||
this.setItems(items);
|
||||
this.setInitialized();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load items.
|
||||
*
|
||||
* @return Items list.
|
||||
*/
|
||||
protected abstract loadItems(): Promise<Item[]>;
|
||||
|
||||
/**
|
||||
* Get a certain item.
|
||||
*
|
||||
* @param item Partial data about the item to search.
|
||||
* @return Item, null if not found.
|
||||
*/
|
||||
getItem(item: Partial<Item>): Item | null {
|
||||
const index = this.getItemPosition(item);
|
||||
|
||||
return this.items?.[index] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a certain item position.
|
||||
*
|
||||
* @param item Item to search.
|
||||
* @return Item position, -1 if not found.
|
||||
*/
|
||||
getItemPosition(item: Partial<Item>): number {
|
||||
const itemId = this.getItemId(item);
|
||||
const index = this.items?.findIndex((listItem) => itemId === this.getItemId(listItem));
|
||||
|
||||
return index ?? -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get initial item position.
|
||||
*
|
||||
* @return Initial item position.
|
||||
*/
|
||||
getInitialPosition(): number {
|
||||
if (!this.initialItem) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.getItemPosition(this.initialItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of an item.
|
||||
*
|
||||
* @param item Data about the item.
|
||||
* @return Item ID.
|
||||
*/
|
||||
abstract getItemId(item: Partial<Item>): string | number;
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// (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 './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
|
||||
*/
|
||||
protected onSourceItemsUpdated(items: Item[]): void {
|
||||
this.itemsMap = items.reduce((map, item) => {
|
||||
map[this.getSource().getItemId(item)] = item;
|
||||
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item by ID.
|
||||
*
|
||||
* @param id ID
|
||||
* @return Item, null if not found.
|
||||
*/
|
||||
getItemById(id: string): Item | null {
|
||||
return this.itemsMap?.[id] ?? null;
|
||||
}
|
||||
|
||||
}
|
|
@ -16,17 +16,17 @@ import { ActivatedRoute, ActivatedRouteSnapshot, UrlSegment } from '@angular/rou
|
|||
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
|
||||
import { CoreItemsManager } from './items-manager';
|
||||
import { CoreItemsManagerSource } from './items-manager-source';
|
||||
import { CoreRoutedItemsManager } from './routed-items-manager';
|
||||
import { CoreRoutedItemsManagerSource } from './routed-items-manager-source';
|
||||
|
||||
/**
|
||||
* Helper class to manage the state and routing of a swipeable page.
|
||||
*/
|
||||
export class CoreSwipeItemsManager<
|
||||
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.
|
||||
|
|
|
@ -60,6 +60,7 @@ import { CoreComboboxComponent } from './combobox/combobox';
|
|||
import { CoreSpacerComponent } from './spacer/spacer';
|
||||
import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-controls/horizontal-scroll-controls';
|
||||
import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-with-spinner';
|
||||
import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -94,6 +95,7 @@ import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-wit
|
|||
CoreSplitViewComponent,
|
||||
CoreStyleComponent,
|
||||
CoreSwipeNavigationComponent,
|
||||
CoreSwipeSlidesComponent,
|
||||
CoreTabComponent,
|
||||
CoreTabsComponent,
|
||||
CoreTabsOutletComponent,
|
||||
|
@ -143,6 +145,7 @@ import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-wit
|
|||
CoreSplitViewComponent,
|
||||
CoreStyleComponent,
|
||||
CoreSwipeNavigationComponent,
|
||||
CoreSwipeSlidesComponent,
|
||||
CoreTabComponent,
|
||||
CoreTabsComponent,
|
||||
CoreTabsOutletComponent,
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<ion-slides *ngIf="manager && manager.getSource().isLoaded()" (ionSlideWillChange)="slideWillChange()"
|
||||
(ionSlideDidChange)="slideDidChange()" [options]="options">
|
||||
<ion-slide *ngFor="let item of manager.getSource().getItems()">
|
||||
<ng-container *ngIf="template" [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{item: item}"></ng-container>
|
||||
</ion-slide>
|
||||
</ion-slides>
|
|
@ -0,0 +1,263 @@
|
|||
// (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/slides-items-manager';
|
||||
import { IonContent, IonSlides } from '@ionic/angular';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
|
||||
/**
|
||||
* 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() onInit = new EventEmitter<CoreSwipeCurrentItemData<Item>>();
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 position 0, and to prevent auto scroll on init.
|
||||
this.options.runCallbacksOnInit = false;
|
||||
|
||||
await manager.getSource().waitForInitialized();
|
||||
|
||||
if (this.options.initialSlide === undefined) {
|
||||
// Calculate the initial slide.
|
||||
const position = manager.getSource().getInitialPosition();
|
||||
this.options.initialSlide = position > - 1 ? position : 0;
|
||||
}
|
||||
|
||||
// Emit change events with the initial item.
|
||||
const items = manager.getSource().getItems();
|
||||
if (!items || !items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that the initial position is inside the valid range.
|
||||
let initialPosition = this.options.initialSlide as number;
|
||||
if (initialPosition < 0) {
|
||||
initialPosition = 0;
|
||||
} else if (initialPosition >= items.length) {
|
||||
initialPosition = items.length - 1;
|
||||
}
|
||||
|
||||
const initialItemData = {
|
||||
index: initialPosition,
|
||||
item: items[initialPosition],
|
||||
};
|
||||
|
||||
manager.setSelectedItem(items[initialPosition]);
|
||||
this.onWillChange.emit(initialItemData);
|
||||
this.onDidChange.emit(initialItemData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Slide to a certain position.
|
||||
*
|
||||
* @param index Position.
|
||||
* @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: Partial<Item>, speed?: number, runCallbacks?: boolean): void {
|
||||
const index = this.manager?.getSource().getItemPosition(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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current item.
|
||||
*
|
||||
* @return Current item. Undefined if no current item yet.
|
||||
*/
|
||||
getCurrentItem(): Item | null {
|
||||
return this.manager?.getSelectedItem() || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when items list has been updated.
|
||||
*
|
||||
* @param items New items.
|
||||
*/
|
||||
protected onItemsUpdated(): void {
|
||||
const currentItem = this.getCurrentItem();
|
||||
|
||||
if (!currentItem || !this.manager) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep the same slide in case the list has changed.
|
||||
const newIndex = this.manager.getSource().getItemPosition(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, '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.
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
* Provides a collection of course participants.
|
||||
*/
|
||||
export class CoreUserParticipantsSource extends CoreItemsManagerSource<CoreUserParticipant | CoreUserData> {
|
||||
export class CoreUserParticipantsSource extends CoreRoutedItemsManagerSource<CoreUserParticipant | CoreUserData> {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
|
|
|
@ -23,7 +23,7 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
|||
import { CoreUser, CoreUserParticipant, CoreUserData } from '@features/user/services/user';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
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.
|
||||
|
@ -48,7 +48,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
|||
try {
|
||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||
this.participants = new CoreUserParticipantsManager(
|
||||
CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]),
|
||||
CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]),
|
||||
CoreUserParticipantsPage,
|
||||
);
|
||||
} catch (error) {
|
||||
|
@ -107,7 +107,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
|||
return;
|
||||
}
|
||||
|
||||
const newSource = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]);
|
||||
const newSource = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]);
|
||||
|
||||
this.searchQuery = null;
|
||||
this.searchInProgress = false;
|
||||
|
@ -124,7 +124,10 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
|||
async search(query: string): Promise<void> {
|
||||
CoreApp.closeKeyboard();
|
||||
|
||||
const newSource = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId, query]);
|
||||
const newSource = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||
CoreUserParticipantsSource,
|
||||
[this.courseId, query],
|
||||
);
|
||||
|
||||
this.searchInProgress = true;
|
||||
this.searchQuery = query;
|
||||
|
|
|
@ -29,7 +29,7 @@ import { CoreNavigator } from '@services/navigator';
|
|||
import { CoreCourses } from '@features/courses/services/courses';
|
||||
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
|
||||
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({
|
||||
selector: 'page-core-user-profile',
|
||||
|
@ -90,7 +90,10 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
|
|||
|
||||
if (this.courseId && this.route.snapshot.data.swipeManagerSource === 'participants') {
|
||||
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.start();
|
||||
|
|
Loading…
Reference in New Issue