From 372c5920a7b2c53ed03cf8a76eb63067cb999134 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 16 Dec 2021 12:29:13 +0100 Subject: [PATCH] MOBILE-3927 swipe: Apply IR changes --- .../pages/issued-badge/issued-badge.page.ts | 6 +- .../calendar/addon-calendar-calendar.html | 16 +- .../calendar/components/calendar/calendar.ts | 350 +++++++++--------- src/addons/calendar/pages/day/day.html | 18 +- src/addons/calendar/pages/day/day.page.ts | 232 ++++++------ .../calendar/services/calendar-helper.ts | 19 +- src/addons/calendar/services/calendar-sync.ts | 5 +- .../submission-review/submission-review.ts | 4 +- .../index/addon-mod-book-index.html | 10 +- src/addons/mod/book/components/index/index.ts | 70 ++-- .../forum-discussions-swipe-manager.ts | 4 +- .../classes/glossary-entries-swipe-manager.ts | 4 +- .../items-management/items-manager-source.ts | 39 +- .../classes/items-management/items-manager.ts | 62 +++- .../items-management/routed-items-manager.ts | 13 +- ...r.ts => swipe-navigation-items-manager.ts} | 4 +- ...pe-slides-dynamic-items-manager-source.ts} | 54 +-- ... => swipe-slides-dynamic-items-manager.ts} | 4 +- ...s => swipe-slides-items-manager-source.ts} | 72 ++-- ...nager.ts => swipe-slides-items-manager.ts} | 20 +- .../swipe-navigation/swipe-navigation.ts | 4 +- .../components/swipe-slides/swipe-slides.html | 5 +- .../components/swipe-slides/swipe-slides.ts | 60 ++- .../user/pages/profile/profile.page.ts | 4 +- src/core/services/utils/dom.ts | 33 +- src/core/services/utils/time.ts | 155 -------- 26 files changed, 591 insertions(+), 676 deletions(-) rename src/core/classes/items-management/{swipe-items-manager.ts => swipe-navigation-items-manager.ts} (97%) rename src/core/classes/items-management/{slides-dynamic-items-manager-source.ts => swipe-slides-dynamic-items-manager-source.ts} (77%) rename src/core/classes/items-management/{slides-dynamic-items-manager.ts => swipe-slides-dynamic-items-manager.ts} (89%) rename src/core/classes/items-management/{slides-items-manager-source.ts => swipe-slides-items-manager-source.ts} (50%) rename src/core/classes/items-management/{slides-items-manager.ts => swipe-slides-items-manager.ts} (66%) diff --git a/src/addons/badges/pages/issued-badge/issued-badge.page.ts b/src/addons/badges/pages/issued-badge/issued-badge.page.ts index c48a21034..4a9285995 100644 --- a/src/addons/badges/pages/issued-badge/issued-badge.page.ts +++ b/src/addons/badges/pages/issued-badge/issued-badge.page.ts @@ -23,7 +23,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses'; import { CoreNavigator } from '@services/navigator'; import { ActivatedRoute } from '@angular/router'; -import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; +import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; @@ -43,7 +43,7 @@ export class AddonBadgesIssuedBadgePage implements OnInit { user?: CoreUserProfile; course?: CoreEnrolledCourseData; badge?: AddonBadgesUserBadge; - badges?: CoreSwipeItemsManager; + badges?: CoreSwipeNavigationItemsManager; badgeLoaded = false; currentTime = 0; @@ -65,7 +65,7 @@ export class AddonBadgesIssuedBadgePage implements OnInit { AddonBadgesUserBadgesSource, [this.courseId, this.userId], ); - this.badges = new CoreSwipeItemsManager(source); + this.badges = new CoreSwipeNavigationItemsManager(source); this.badges.start(); } diff --git a/src/addons/calendar/components/calendar/addon-calendar-calendar.html b/src/addons/calendar/components/calendar/addon-calendar-calendar.html index 40ba979c3..ca1e7dc08 100644 --- a/src/addons/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addons/calendar/components/calendar/addon-calendar-calendar.html @@ -1,7 +1,7 @@ - @@ -19,7 +19,7 @@

{{ periodName }} - +

@@ -31,14 +31,14 @@ - - + +
- + {{ day.fullname | translate }} @@ -47,16 +47,16 @@
- +

diff --git a/src/addons/calendar/components/calendar/calendar.ts b/src/addons/calendar/components/calendar/calendar.ts index 36614d3d5..ed3e500c7 100644 --- a/src/addons/calendar/components/calendar/calendar.ts +++ b/src/addons/calendar/components/calendar/calendar.ts @@ -27,7 +27,7 @@ import { import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTimeUtils, YearAndMonth } from '@services/utils/time'; +import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; import { AddonCalendar, @@ -42,12 +42,13 @@ 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 { CoreSwipeCurrentItemData, CoreSwipeSlidesComponent } from '@components/swipe-slides/swipe-slides'; +import { CoreSwipeSlidesComponent } from '@components/swipe-slides/swipe-slides'; import { CoreSwipeSlidesDynamicItem, CoreSwipeSlidesDynamicItemsManagerSource, -} from '@classes/items-management/slides-dynamic-items-manager-source'; -import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/slides-dynamic-items-manager'; +} from '@classes/items-management/swipe-slides-dynamic-items-manager-source'; +import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/swipe-slides-dynamic-items-manager'; +import moment from 'moment'; /** * Component that displays a calendar. @@ -72,15 +73,13 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro periodName?: string; manager?: CoreSwipeSlidesDynamicItemsManager; loaded = false; - isCurrentMonth = false; - isPastMonth = false; protected currentSiteId: string; - protected currentTime?: number; protected differ: KeyValueDiffer; // To detect changes in the data input. - // Observers. + // Observers and listeners. protected undeleteEventObserver: CoreEventObserver; protected obsDefaultTimeChange?: CoreEventObserver; + protected managerUnsubscribe?: () => void; constructor( differs: KeyValueDiffers, @@ -95,7 +94,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro return; } - month.weeks.forEach((week) => { + month.weeks?.forEach((week) => { week.days.forEach((day) => { AddonCalendar.scheduleEventsNotifications(day.eventsFormated || []); }); @@ -131,19 +130,20 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro * Component loaded. */ ngOnInit(): void { - const currentMonth = CoreTimeUtils.getCurrentMonth(); - 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 ?? currentMonth.year, - monthNumber: this.initialMonth ?? currentMonth.monthNumber, - }); + const source = new AddonCalendarMonthSlidesItemsManagerSource(this, moment({ + year: this.initialYear, + month: this.initialMonth ? this.initialMonth - 1 : undefined, + })); this.manager = new CoreSwipeSlidesDynamicItemsManager(source); - - this.calculateIsCurrentMonth(); + this.managerUnsubscribe = this.manager.addListener({ + onSelectedItemUpdated: (item) => { + this.onMonthViewed(item); + }, + }); this.fetchData(); } @@ -159,7 +159,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro const changes = this.differ.diff(this.filter || {}); if (changes) { items.forEach((month) => { - if (month.loaded) { + if (month.loaded && month.weeks) { this.manager?.getSource().filterEvents(month.weeks, this.filter); } }); @@ -190,14 +190,15 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro /** * Update data related to month being viewed. + * + * @param month Month being viewed. */ - viewMonth(month: YearAndMonth): void { + onMonthViewed(month: MonthBasicData): void { // Calculate the period name. We don't use the one in result because it's in server's language. this.periodName = CoreTimeUtils.userDate( - new Date(month.year, month.monthNumber - 1).getTime(), + month.moment.unix() * 1000, 'core.strftimemonthyear', ); - this.calculateIsCurrentMonth(); } /** @@ -207,13 +208,13 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro * @return Promise resolved when done. */ async refreshData(afterChange = false): Promise { - const visibleMonth = this.slides?.getCurrentItem() || null; + const selectedMonth = this.manager?.getSelectedItem() || null; if (afterChange) { this.manager?.getSource().markAllItemsDirty(); } - await this.manager?.getSource().invalidateContent(visibleMonth); + await this.manager?.getSource().invalidateContent(selectedMonth); await this.fetchData(); } @@ -249,39 +250,53 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro * @param day Day. */ dayClicked(day: number): void { - const visibleMonth = this.slides?.getCurrentItem(); - if (!visibleMonth) { + const selectedMonth = this.manager?.getSelectedItem(); + if (!selectedMonth) { 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.isSameMonth(visibleMonth, CoreTimeUtils.getCurrentMonth()); - this.isPastMonth = CoreTimeUtils.isPastMonth(visibleMonth); + this.onDayClicked.emit({ day: day, month: selectedMonth.moment.month() + 1, year: selectedMonth.moment.year() }); } /** * Go to current month. */ async goToCurrentMonth(): Promise { - const now = new Date(); - const currentMonth = { - monthNumber: now.getMonth() + 1, - year: now.getFullYear(), - }; + const manager = this.manager; + const slides = this.slides; + if (!manager || !slides) { + return; + } - this.slides?.slideToItem(currentMonth); + const currentMonth = { + moment: moment(), + }; + this.loaded = false; + + try { + // Make sure the day is loaded. + await manager.getSource().loadItem(currentMonth); + + slides.slideToItem(currentMonth); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + } finally { + this.loaded = true; + } + } + + /** + * Check whether selected month is loaded. + */ + selectedMonthLoaded(): boolean { + return !!this.manager?.getSelectedItem()?.loaded; + } + + /** + * Check whether selected month is current month. + */ + selectedMonthIsCurrent(): boolean { + return !!this.manager?.getSelectedItem()?.isCurrentMonth; } /** @@ -295,7 +310,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro return false; } - return month.weeks.some((week) => week.days.some((day) => { + return month.weeks?.some((week) => week.days.some((day) => { const event = day.eventsFormated?.find((event) => event.id == eventId); if (event) { @@ -309,41 +324,32 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro }); } - /** - * Returns if the event is in the past or not. - * - * @param event Event object. - * @return True if it's in the past. - */ - isEventPast(event: { timestart: number; timeduration: number}): boolean { - return (event.timestart + event.timeduration) < (this.currentTime || CoreTimeUtils.timestamp()); - } - - /** - * Slide has changed. - * - * @param data Data about new item. - */ - slideChanged(data: CoreSwipeCurrentItemData): void { - this.viewMonth(data.item); - } - /** * Component destroyed. */ ngOnDestroy(): void { this.undeleteEventObserver?.off(); this.obsDefaultTimeChange?.off(); + this.managerUnsubscribe && this.managerUnsubscribe(); } } +/** + * Basic data to identify a month. + */ +type MonthBasicData = { + moment: moment.Moment; +}; + /** * Preloaded month. */ -type PreloadedMonth = YearAndMonth & CoreSwipeSlidesDynamicItem & { - weekDays: AddonCalendarWeekDaysTranslationKeys[]; - weeks: AddonCalendarWeek[]; +type PreloadedMonth = MonthBasicData & CoreSwipeSlidesDynamicItem & { + weekDays?: AddonCalendarWeekDaysTranslationKeys[]; + weeks?: AddonCalendarWeek[]; + isCurrentMonth?: boolean; + isPastMonth?: boolean; }; /** @@ -351,8 +357,7 @@ type PreloadedMonth = YearAndMonth & CoreSwipeSlidesDynamicItem & { */ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicItemsManagerSource { - monthLoaded = false; - categories: { [id: number]: CoreCategoryData } = {}; + categories?: { [id: number]: CoreCategoryData }; // Offline events classified in month & day. offlineEvents: { [monthId: string]: { [day: number]: AddonCalendarEventToDisplay[] } } = {}; offlineEditedEventsIds: number[] = []; // IDs of events edited in offline. @@ -360,10 +365,9 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI timeFormat?: string; protected calendarComponent: AddonCalendarCalendarComponent; - protected categoriesRetrieved = false; - constructor(component: AddonCalendarCalendarComponent, initialMonth: YearAndMonth) { - super(initialMonth); + constructor(component: AddonCalendarCalendarComponent, initialMoment: moment.Moment) { + super({ moment: initialMoment }); this.calendarComponent = component; } @@ -394,7 +398,7 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI day.filteredEvents = AddonCalendarHelper.getFilteredEvents( day.eventsFormated || [], filter, - this.categories, + this.categories || {}, ); // Re-calculate some properties. @@ -409,20 +413,16 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI * @return Promise resolved when done. */ async loadCategories(): Promise { - if (this.categoriesRetrieved) { + if (this.categories) { // Already retrieved, stop. return; } try { - const cats = await CoreCourses.getCategories(0, true); - this.categoriesRetrieved = true; - this.categories = {}; + const categories = await CoreCourses.getCategories(0, true); // Index categories by ID. - cats.forEach((category) => { - this.categories[category.id] = category; - }); + this.categories = CoreUtils.arrayToObject(categories, 'id'); } catch { // Ignore errors. } @@ -465,103 +465,104 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI /** * @inheritdoc */ - getItemId(item: YearAndMonth): string | number { - return AddonCalendarHelper.getMonthId(item); + getItemId(item: MonthBasicData): string | number { + return AddonCalendarHelper.getMonthId(item.moment); } /** * @inheritdoc */ - getPreviousItem(item: YearAndMonth): YearAndMonth | null { - return CoreTimeUtils.getPreviousMonth(item); + getPreviousItem(item: MonthBasicData): MonthBasicData | null { + return { + moment: item.moment.clone().subtract(1, 'month'), + }; } /** * @inheritdoc */ - getNextItem(item: YearAndMonth): YearAndMonth | null { - return CoreTimeUtils.getNextMonth(item); + getNextItem(item: MonthBasicData): MonthBasicData | null { + return { + moment: item.moment.clone().add(1, 'month'), + }; } /** * @inheritdoc */ - async loadItemData(month: YearAndMonth, preload = false): Promise { - if (!preload) { - this.monthLoaded = false; - } + async loadItemData(month: MonthBasicData, preload = false): Promise { + // Load or preload the weeks. + let result: { daynames: Partial[]; weeks: Partial[] }; + const year = month.moment.year(); + const monthNumber = month.moment.month() + 1; - try { - // Load or preload the weeks. - let result: { daynames: Partial[]; weeks: Partial[] }; - if (preload) { - result = await AddonCalendarHelper.getOfflineMonthWeeks(month.year, month.monthNumber); - } else { - try { - // Don't pass courseId and categoryId, we'll filter them locally. - result = await AddonCalendar.getMonthlyEvents(month.year, month.monthNumber); - } catch (error) { - if (!CoreApp.isOnline()) { - // Allow navigating to non-cached months in offline (behave as if using emergency cache). - result = await AddonCalendarHelper.getOfflineMonthWeeks(month.year, month.monthNumber); - } else { - throw error; - } + if (preload) { + result = await AddonCalendarHelper.getOfflineMonthWeeks(year, monthNumber); + } else { + try { + // Don't pass courseId and categoryId, we'll filter them locally. + result = await AddonCalendar.getMonthlyEvents(year, monthNumber); + } catch (error) { + if (!CoreApp.isOnline()) { + // Allow navigating to non-cached months in offline (behave as if using emergency cache). + result = await AddonCalendarHelper.getOfflineMonthWeeks(year, monthNumber); + } else { + throw error; } } - - const weekDays = AddonCalendar.getWeekDays(result.daynames[0].dayno); - const weeks = result.weeks as AddonCalendarWeek[]; - const isCurrentMonth = CoreTimeUtils.isSameMonth(month, CoreTimeUtils.getCurrentMonth()); - const currentDay = new Date().getDate(); - let isPast = true; - - await Promise.all(weeks.map(async (week) => { - await Promise.all(week.days.map(async (day) => { - day.periodName = CoreTimeUtils.userDate( - new Date(month.year, month.monthNumber - 1, day.mday).getTime(), - 'core.strftimedaydate', - ); - day.eventsFormated = day.eventsFormated || []; - day.filteredEvents = day.filteredEvents || []; - // Format online events. - const onlineEventsFormatted = await Promise.all( - day.events.map(async (event) => AddonCalendarHelper.formatEventData(event)), - ); - - day.eventsFormated = day.eventsFormated.concat(onlineEventsFormatted); - - if (isCurrentMonth) { - day.istoday = day.mday == currentDay; - day.ispast = isPast && !day.istoday; - isPast = day.ispast; - - if (day.istoday) { - day.eventsFormated?.forEach((event) => { - event.ispast = this.calendarComponent.isEventPast(event); - }); - } - } - })); - })); - - if (!preload) { - // Merge the online events with offline data. - this.mergeEvents(month, weeks); - // Filter events by course. - this.filterEvents(weeks, this.calendarComponent.filter); - } - - return { - ...month, - weeks, - weekDays, - }; - } finally { - if (!preload) { - this.monthLoaded = true; - } } + + const weekDays = AddonCalendar.getWeekDays(result.daynames[0].dayno); + const weeks = result.weeks as AddonCalendarWeek[]; + const currentDay = new Date().getDate(); + const currentTime = CoreTimeUtils.timestamp(); + let isPast = true; + + const preloadedMonth: PreloadedMonth = { + ...month, + weeks, + weekDays, + isCurrentMonth: month.moment.isSame(moment(), 'month'), + isPastMonth: month.moment.isBefore(moment(), 'month'), + }; + + await Promise.all(weeks.map(async (week) => { + await Promise.all(week.days.map(async (day) => { + day.periodName = CoreTimeUtils.userDate( + month.moment.unix() * 1000, + 'core.strftimedaydate', + ); + day.eventsFormated = day.eventsFormated || []; + day.filteredEvents = day.filteredEvents || []; + // Format online events. + const onlineEventsFormatted = await Promise.all( + day.events.map(async (event) => AddonCalendarHelper.formatEventData(event)), + ); + + day.eventsFormated = day.eventsFormated.concat(onlineEventsFormatted); + + if (preloadedMonth.isCurrentMonth) { + day.istoday = day.mday == currentDay; + day.ispast = isPast && !day.istoday; + isPast = day.ispast; + + if (day.istoday) { + day.eventsFormated?.forEach((event) => { + event.ispast = this.isEventPast(event, currentTime); + }); + } + } + })); + })); + + if (!preload) { + // Merge the online events with offline data. + this.mergeEvents(month, weeks); + // Filter events by course. + this.filterEvents(weeks, this.calendarComponent.filter); + } + + return preloadedMonth; } /** @@ -570,9 +571,9 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI * @param month Month. * @param weeks Weeks with the events to filter. */ - mergeEvents(month: YearAndMonth, weeks: AddonCalendarWeek[]): void { + mergeEvents(month: MonthBasicData, weeks: AddonCalendarWeek[]): void { const monthOfflineEvents: { [day: number]: AddonCalendarEventToDisplay[] } = - this.offlineEvents[AddonCalendarHelper.getMonthId(month)]; + this.offlineEvents[AddonCalendarHelper.getMonthId(month.moment)]; weeks.forEach((week) => { week.days.forEach((day) => { @@ -605,25 +606,36 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI }); } + /** + * Returns if the event is in the past or not. + * + * @param event Event object. + * @param currentTime Current time. + * @return True if it's in the past. + */ + isEventPast(event: { timestart: number; timeduration: number}, currentTime: number): boolean { + return (event.timestart + event.timeduration) < currentTime; + } + /** * Invalidate content. * - * @param visibleMonth The current visible month. + * @param selectedMonth The current selected month. * @return Promise resolved when done. */ - async invalidateContent(visibleMonth: PreloadedMonth | null): Promise { + async invalidateContent(selectedMonth: PreloadedMonth | null): Promise { const promises: Promise[] = []; - if (visibleMonth) { - promises.push(AddonCalendar.invalidateMonthlyEvents(visibleMonth.year, visibleMonth.monthNumber)); + if (selectedMonth) { + promises.push(AddonCalendar.invalidateMonthlyEvents(selectedMonth.moment.year(), selectedMonth.moment.month() + 1)); } promises.push(CoreCourses.invalidateCategories(0, true)); promises.push(AddonCalendar.invalidateTimeFormat()); - this.categoriesRetrieved = false; // Get categories again. + this.categories = undefined; // Get categories again. - if (visibleMonth) { - visibleMonth.dirty = true; + if (selectedMonth) { + selectedMonth.dirty = true; } await Promise.all(promises); diff --git a/src/addons/calendar/pages/day/day.html b/src/addons/calendar/pages/day/day.html index 28b8bf934..01a7daceb 100644 --- a/src/addons/calendar/pages/day/day.html +++ b/src/addons/calendar/pages/day/day.html @@ -11,10 +11,10 @@ - - @@ -47,23 +47,23 @@ - - - + + + - + {{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }} - - - + + diff --git a/src/addons/calendar/pages/day/day.page.ts b/src/addons/calendar/pages/day/day.page.ts index 86039c369..984d5931b 100644 --- a/src/addons/calendar/pages/day/day.page.ts +++ b/src/addons/calendar/pages/day/day.page.ts @@ -19,7 +19,7 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTimeUtils, YearMonthDay } from '@services/utils/time'; +import { CoreTimeUtils } from '@services/utils/time'; import { AddonCalendarProvider, AddonCalendar, @@ -40,12 +40,12 @@ import { Params } from '@angular/router'; import { Subscription } from 'rxjs'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; -import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/slides-dynamic-items-manager'; -import { CoreSwipeCurrentItemData, CoreSwipeSlidesComponent } from '@components/swipe-slides/swipe-slides'; +import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/swipe-slides-dynamic-items-manager'; +import { CoreSwipeSlidesComponent } from '@components/swipe-slides/swipe-slides'; import { CoreSwipeSlidesDynamicItem, CoreSwipeSlidesDynamicItemsManagerSource, -} from '@classes/items-management/slides-dynamic-items-manager-source'; +} from '@classes/items-management/swipe-slides-dynamic-items-manager-source'; /** * Page that displays the calendar events for a certain day. @@ -72,13 +72,13 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { protected onlineObserver: Subscription; protected obsDefaultTimeChange?: CoreEventObserver; protected filterChangedObserver: CoreEventObserver; + protected managerUnsubscribe?: () => void; periodName?: string; manager?: CoreSwipeSlidesDynamicItemsManager; loaded = false; isOnline = false; syncIcon = CoreConstants.ICON_LOADING; - isCurrentDay = false; filter: AddonCalendarFilter = { filtered: false, courseId: undefined, @@ -97,7 +97,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { // Re-schedule events if default time changes. this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { this.manager?.getSource().getItems()?.forEach(day => { - AddonCalendar.scheduleEventsNotifications(day.onlineEvents); + AddonCalendar.scheduleEventsNotifications(day.onlineEvents || []); }); }, this.currentSiteId); } @@ -140,9 +140,8 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { // Refresh data if calendar events are synchronized manually but not by this page. this.manualSyncObserver = CoreEvents.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => { - const visibleDay = this.slides?.getCurrentItem(); - if (data && (data.source != 'day' || !visibleDay || data.day === undefined || data.year != visibleDay.year || - data.month != visibleDay.monthNumber || data.day != visibleDay.dayNumber)) { + const selectedDay = this.manager?.getSelectedItem(); + if (data && (data.source != 'day' || !selectedDay || !data.moment || !selectedDay.moment.isSame(data.moment, 'day'))) { this.manager?.getSource().markAllItemsUnloaded(); this.refreshData(false, true); } @@ -214,15 +213,18 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.filter.filtered = typeof this.filter.courseId != 'undefined' || types.some((name) => !this.filter[name]); - const currentDay = CoreTimeUtils.getCurrentDay(); - const source = new AddonCalendarDaySlidesItemsManagerSource(this, { - year: CoreNavigator.getRouteNumberParam('year') ?? currentDay.year, - monthNumber: CoreNavigator.getRouteNumberParam('month') ?? currentDay.monthNumber, - dayNumber: CoreNavigator.getRouteNumberParam('day') ?? currentDay.dayNumber, - }); + const month = CoreNavigator.getRouteNumberParam('month'); + const source = new AddonCalendarDaySlidesItemsManagerSource(this, moment({ + year: CoreNavigator.getRouteNumberParam('year'), + month: month ? month - 1 : undefined, + date: CoreNavigator.getRouteNumberParam('day'), + })); this.manager = new CoreSwipeSlidesDynamicItemsManager(source); - - this.calculateIsCurrentDay(); + this.managerUnsubscribe = this.manager.addListener({ + onSelectedItemUpdated: (item) => { + this.onDayViewed(item); + }, + }); this.fetchData(true); } @@ -264,13 +266,14 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { /** * Update data related to day being viewed. + * + * @param day Day viewed. */ - viewDay(day: YearMonthDay): void { + onDayViewed(day: DayBasicData): void { this.periodName = CoreTimeUtils.userDate( - new Date(day.year, day.monthNumber - 1, day.dayNumber).getTime(), + day.moment.unix() * 1000, 'core.strftimedaydate', ); - this.calculateIsCurrentDay(); } /** @@ -301,10 +304,10 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { async refreshData(sync?: boolean, afterChange?: boolean): Promise { this.syncIcon = CoreConstants.ICON_LOADING; - const visibleDay = this.slides?.getCurrentItem() || null; + const selectedDay = this.manager?.getSelectedItem() || null; // Don't invalidate day events after a change, it has already been handled. - await this.manager?.getSource().invalidateContent(visibleDay, !afterChange); + await this.manager?.getSource().invalidateContent(selectedDay, !afterChange); await this.fetchData(sync); } @@ -325,14 +328,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { if (result.updated) { // Trigger a manual sync event. - const visibleDay = this.slides?.getCurrentItem(); + const selectedDay = this.manager?.getSelectedItem(); result.source = 'day'; - - if (visibleDay) { - result.day = visibleDay.dayNumber; - result.month = visibleDay.monthNumber; - result.year = visibleDay.year; - } + result.moment = selectedDay?.moment; this.manager?.getSource().markAllItemsUnloaded(); CoreEvents.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId); @@ -344,6 +342,13 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { } } + /** + * Check whether selected day is current day. + */ + selectedDayIsCurrent(): boolean { + return !!this.manager?.getSelectedItem()?.isCurrentDay; + } + /** * Navigate to a particular event. * @@ -381,10 +386,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { // It's a new event, set the time. eventId = 0; - const visibleDay = this.slides?.getCurrentItem(); - if (visibleDay) { - params.timestamp = moment().year(visibleDay.year).month(visibleDay.monthNumber - 1).date(visibleDay.dayNumber) - .unix() * 1000; + const selectedDay = this.manager?.getSelectedItem(); + if (selectedDay) { + params.timestamp = selectedDay.moment.unix() * 1000; } } @@ -396,26 +400,14 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { } /** - * Check if user is viewing the current day. - */ - calculateIsCurrentDay(): void { - const visibleDay = this.slides?.getCurrentItem(); - if (!visibleDay) { - return; - } - - this.isCurrentDay = visibleDay.isCurrentDay; - } - - /** - * Check whether visible day has offline data. + * Check whether selected day has offline data. * - * @return Whether visible day has offline data. + * @return Whether selected day has offline data. */ - visibleDayHasOffline(): boolean { - const visibleDay = this.slides?.getCurrentItem(); + selectedDayHasOffline(): boolean { + const selectedDay = this.manager?.getSelectedItem(); - return !!visibleDay && visibleDay.hasOffline; + return !!(selectedDay?.hasOffline); } /** @@ -428,7 +420,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { return; } - const currentDay = CoreTimeUtils.getCurrentDay(); + const currentDay = { + moment: moment(), + }; this.loaded = false; try { @@ -436,7 +430,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { await manager.getSource().loadItem(currentDay); slides.slideToItem(currentDay); - this.isCurrentDay = true; } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); } finally { @@ -458,15 +451,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.slides?.slidePrev(); } - /** - * Slide has changed. - * - * @param data Data about new item. - */ - slideChanged(data: CoreSwipeCurrentItemData): void { - this.viewDay(data.item); - } - /** * Page destroyed. */ @@ -481,20 +465,28 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.onlineObserver?.unsubscribe(); this.filterChangedObserver?.off(); this.obsDefaultTimeChange?.off(); + this.managerUnsubscribe && this.managerUnsubscribe(); } } +/** + * Basic data to identify a day. + */ +type DayBasicData = { + moment: moment.Moment; +}; + /** * Preloaded month. */ -type PreloadedDay = YearMonthDay & CoreSwipeSlidesDynamicItem & { - events: AddonCalendarEventToDisplay[]; // Events (both online and offline). - onlineEvents: AddonCalendarEventToDisplay[]; - filteredEvents: AddonCalendarEventToDisplay[]; - isCurrentDay: boolean; - isPastDay: boolean; - hasOffline: boolean; // Whether the day has offline data. +type PreloadedDay = DayBasicData & CoreSwipeSlidesDynamicItem & { + events?: AddonCalendarEventToDisplay[]; // Events (both online and offline). + onlineEvents?: AddonCalendarEventToDisplay[]; + filteredEvents?: AddonCalendarEventToDisplay[]; + isCurrentDay?: boolean; + isPastDay?: boolean; + hasOffline?: boolean; // Whether the day has offline data. }; /** @@ -506,16 +498,15 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte // Offline events classified in month & day. offlineEvents: Record> = {}; offlineEditedEventsIds: number[] = []; // IDs of events edited in offline. - categories: { [id: number]: CoreCategoryData } = {}; - deletedEvents: number[] = []; // Events deleted in offline. + categories?: { [id: number]: CoreCategoryData }; + deletedEvents?: Set; // Events deleted in offline. timeFormat?: string; canCreate = false; protected dayPage: AddonCalendarDayPage; - protected categoriesRetrieved = false; - constructor(page: AddonCalendarDayPage, initialDay: YearMonthDay) { - super(initialDay); + constructor(page: AddonCalendarDayPage, initialMoment: moment.Moment) { + super({ moment: initialMoment }); this.dayPage = page; } @@ -553,7 +544,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte * @param filter Filter to apply. */ filterEvents(day: PreloadedDay, filter: AddonCalendarFilter): void { - day.filteredEvents = AddonCalendarHelper.getFilteredEvents(day.events, filter, this.categories); + day.filteredEvents = AddonCalendarHelper.getFilteredEvents(day.events || [], filter, this.categories || {}); } /** @@ -584,20 +575,16 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte * @return Promise resolved when done. */ async loadCategories(): Promise { - if (this.categoriesRetrieved) { + if (this.categories) { // Already retrieved, stop. return; } try { - const cats = await CoreCourses.getCategories(0, true); - this.categoriesRetrieved = true; - this.categories = {}; + const categories = await CoreCourses.getCategories(0, true); // Index categories by ID. - cats.forEach((category) => { - this.categories[category.id] = category; - }); + this.categories = CoreUtils.arrayToObject(categories, 'id'); } catch { // Ignore errors. } @@ -625,7 +612,9 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte * @return Promise resolved when done. */ async loadOfflineDeletedEvents(): Promise { - this.deletedEvents = await AddonCalendarOffline.getAllDeletedEventsIds(); + const deletedEventsIds = await AddonCalendarOffline.getAllDeletedEventsIds(); + + this.deletedEvents = new Set(deletedEventsIds); } /** @@ -640,36 +629,40 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte /** * @inheritdoc */ - getItemId(item: YearMonthDay): string | number { - return AddonCalendarHelper.getDayId(item); + getItemId(item: DayBasicData): string | number { + return AddonCalendarHelper.getDayId(item.moment); } /** * @inheritdoc */ - getPreviousItem(item: YearMonthDay): YearMonthDay | null { - return CoreTimeUtils.getPreviousDay(item); + getPreviousItem(item: DayBasicData): DayBasicData | null { + return { + moment: item.moment.clone().subtract(1, 'day'), + }; } /** * @inheritdoc */ - getNextItem(item: YearMonthDay): YearMonthDay | null { - return CoreTimeUtils.getNextDay(item); + getNextItem(item: DayBasicData): DayBasicData | null { + return { + moment: item.moment.clone().add(1, 'day'), + }; } /** * @inheritdoc */ - async loadItemData(day: YearMonthDay, preload = false): Promise { + async loadItemData(day: DayBasicData, preload = false): Promise { const preloadedDay: PreloadedDay = { ...day, hasOffline: false, events: [], onlineEvents: [], filteredEvents: [], - isCurrentDay: CoreTimeUtils.isSameDay(day, CoreTimeUtils.getCurrentDay()), - isPastDay: CoreTimeUtils.isPastDay(day), + isCurrentDay: day.moment.isSame(moment(), 'day'), + isPastDay: day.moment.isBefore(moment(), 'day'), }; if (preload) { @@ -680,7 +673,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte try { // Don't pass courseId and categoryId, we'll filter them locally. - result = await AddonCalendar.getDayEvents(day.year, day.monthNumber, day.dayNumber); + result = await AddonCalendar.getDayEvents(day.moment.year(), day.moment.month() + 1, day.moment.date()); preloadedDay.onlineEvents = await Promise.all( result.events.map((event) => AddonCalendarHelper.formatEventData(event)), ); @@ -692,7 +685,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte } // Schedule notifications for the events retrieved (only future events will be scheduled). - AddonCalendar.scheduleEventsNotifications(preloadedDay.onlineEvents); + AddonCalendar.scheduleEventsNotifications(preloadedDay.onlineEvents || []); // Merge the online events with offline data. preloadedDay.events = this.mergeEvents(preloadedDay); @@ -701,7 +694,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte this.filterEvents(preloadedDay, this.dayPage.filter); // Re-calculate the formatted time so it uses the device date. - const dayTime = moment().year(day.year).month(day.monthNumber - 1).date(day.dayNumber).unix() * 1000; + const dayTime = day.moment.unix() * 1000; const currentTime = CoreTimeUtils.timestamp(); const promises = preloadedDay.events.map(async (event) => { @@ -734,19 +727,19 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte mergeEvents(day: PreloadedDay): AddonCalendarEventToDisplay[] { day.hasOffline = false; - if (!Object.keys(this.offlineEvents).length && !this.deletedEvents.length) { + if (!Object.keys(this.offlineEvents).length && !this.deletedEvents?.size) { // No offline events, nothing to merge. - return day.onlineEvents; + return day.onlineEvents || []; } - const monthOfflineEvents = this.offlineEvents[AddonCalendarHelper.getMonthId(day)]; - const dayOfflineEvents = monthOfflineEvents && monthOfflineEvents[day.dayNumber]; - let result = day.onlineEvents; + const monthOfflineEvents = this.offlineEvents[AddonCalendarHelper.getMonthId(day.moment)]; + const dayOfflineEvents = monthOfflineEvents && monthOfflineEvents[day.moment.date()]; + let result = day.onlineEvents || []; - if (this.deletedEvents.length) { + if (this.deletedEvents?.size) { // Mark as deleted the events that were deleted in offline. result.forEach((event) => { - event.deleted = this.deletedEvents.indexOf(event.id) != -1; + event.deleted = this.deletedEvents?.has(event.id); if (event.deleted) { day.hasOffline = true; @@ -758,7 +751,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte // Remove the online events that were modified in offline. result = result.filter((event) => this.offlineEditedEventsIds.indexOf(event.id) == -1); - if (result.length != day.onlineEvents.length) { + if (result.length != day.onlineEvents?.length) { day.hasOffline = true; } } @@ -775,24 +768,28 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte /** * Invalidate content. * - * @param visibleDay The current visible day. - * @param invalidateDayEvents Whether to invalidate visible day events. + * @param selectedDay The current selected day. + * @param invalidateDayEvents Whether to invalidate selected day events. * @return Promise resolved when done. */ - async invalidateContent(visibleDay: PreloadedDay | null, invalidateDayEvents?: boolean): Promise { + async invalidateContent(selectedDay: PreloadedDay | null, invalidateDayEvents?: boolean): Promise { const promises: Promise[] = []; - if (invalidateDayEvents && visibleDay) { - promises.push(AddonCalendar.invalidateDayEvents(visibleDay.year, visibleDay.monthNumber, visibleDay.dayNumber)); + if (invalidateDayEvents && selectedDay) { + promises.push(AddonCalendar.invalidateDayEvents( + selectedDay.moment.year(), + selectedDay.moment.month() + 1, + selectedDay.moment.date(), + )); } promises.push(AddonCalendar.invalidateAllowedEventTypes()); promises.push(CoreCourses.invalidateCategories(0, true)); promises.push(AddonCalendar.invalidateTimeFormat()); - this.categoriesRetrieved = false; // Get categories again. + this.categories = undefined; // Get categories again. - if (visibleDay) { - visibleDay.dirty = true; + if (selectedDay) { + selectedDay.dirty = true; } await Promise.all(promises); @@ -807,7 +804,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte markAsDeleted(eventId: number, deleted: boolean): void { // Mark the event as deleted or not. this.getItems()?.some(day => { - const event = day.onlineEvents.find((event) => event.id == eventId); + const event = day.onlineEvents?.find((event) => event.id == eventId); if (!event) { return false; @@ -819,8 +816,8 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte day.hasOffline = true; } else { // Re-calculate "hasOffline". - day.hasOffline = day.events.length != day.onlineEvents.length || - day.events.some((event) => event.deleted || event.offline); + day.hasOffline = day.events?.length != day.onlineEvents?.length || + day.events?.some((event) => event.deleted || event.offline); } return true; @@ -828,14 +825,9 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte // Add it or remove it from the list of deleted events. if (deleted) { - if (!this.deletedEvents.includes(eventId)) { - this.deletedEvents.push(eventId); - } + this.deletedEvents?.add(eventId); } else { - const index = this.deletedEvents.indexOf(eventId); - if (index != -1) { - this.deletedEvents.splice(index, 1); - } + this.deletedEvents?.delete(eventId); } } diff --git a/src/addons/calendar/services/calendar-helper.ts b/src/addons/calendar/services/calendar-helper.ts index 74f588a7b..776c2b38d 100644 --- a/src/addons/calendar/services/calendar-helper.ts +++ b/src/addons/calendar/services/calendar-helper.ts @@ -37,7 +37,6 @@ import { AddonCalendarSyncInvalidateEvent } from './calendar-sync'; import { AddonCalendarOfflineEventDBRecord } from './database/calendar-offline'; import { CoreCategoryData } from '@features/courses/services/courses'; import { AddonCalendarReminderDBRecord } from './database/calendar'; -import { YearAndMonth, YearMonthDay } from '@services/utils/time'; /** * Context levels enumeration. @@ -141,7 +140,7 @@ export class AddonCalendarHelperProvider { // Add the event to all the days it lasts. while (!treatedDay.isAfter(endDay, 'day')) { - const monthId = this.getMonthId({ year: treatedDay.year(), monthNumber: treatedDay.month() + 1 }); + const monthId = this.getMonthId(treatedDay); const day = treatedDay.date(); if (!result[monthId]) { @@ -367,21 +366,21 @@ export class AddonCalendarHelperProvider { /** * Get the month "id". * - * @param month Month data. + * @param moment Month moment. * @return The "id". */ - getMonthId(month: YearAndMonth): string { - return `${month.year}#${month.monthNumber}`; + getMonthId(moment: moment.Moment): string { + return `${moment.year()}#${moment.month() + 1}`; } /** * Get the day "id". * - * @param day Day data. + * @param day Day moment. * @return The "id". */ - getDayId(day: YearMonthDay): string { - return `${day.year}#${day.monthNumber}#${day.dayNumber}`; + getDayId(moment: moment.Moment): string { + return `${this.getMonthId(moment)}#${moment.date()}`; } /** @@ -660,7 +659,7 @@ export class AddonCalendarHelperProvider { fetchTimestarts.map((fetchTime) => { const day = moment(new Date(fetchTime * 1000)); - const monthId = this.getMonthId({ year: day.year(), monthNumber: day.month() + 1 }); + const monthId = this.getMonthId(day); if (!treatedMonths[monthId]) { // Month not refetch or invalidated already, do it now. treatedMonths[monthId] = true; @@ -696,7 +695,7 @@ export class AddonCalendarHelperProvider { invalidateTimestarts.map((fetchTime) => { const day = moment(new Date(fetchTime * 1000)); - const monthId = this.getMonthId({ year: day.year(), monthNumber: day.month() + 1 }); + const monthId = this.getMonthId(day); if (!treatedMonths[monthId]) { // Month not refetch or invalidated already, do it now. treatedMonths[monthId] = true; diff --git a/src/addons/calendar/services/calendar-sync.ts b/src/addons/calendar/services/calendar-sync.ts index 56f3e8a3c..12d26e5bb 100644 --- a/src/addons/calendar/services/calendar-sync.ts +++ b/src/addons/calendar/services/calendar-sync.ts @@ -29,6 +29,7 @@ import { AddonCalendarHelper } from './calendar-helper'; import { makeSingleton, Translate } from '@singletons'; import { CoreSync } from '@services/sync'; import { CoreNetworkError } from '@classes/errors/network-error'; +import moment from 'moment'; /** * Service to sync calendar. @@ -307,9 +308,7 @@ export type AddonCalendarSyncEvents = { toinvalidate: AddonCalendarSyncInvalidateEvent[]; updated: boolean; source?: string; // Added on pages. - day?: number; // Added on day page. - month?: number; // Added on day page. - year?: number; // Added on day page. + moment?: moment.Moment; // Added on day page. }; export type AddonCalendarSyncInvalidateEvent = { diff --git a/src/addons/mod/assign/pages/submission-review/submission-review.ts b/src/addons/mod/assign/pages/submission-review/submission-review.ts index 7f2e278cd..20342ef15 100644 --- a/src/addons/mod/assign/pages/submission-review/submission-review.ts +++ b/src/addons/mod/assign/pages/submission-review/submission-review.ts @@ -15,7 +15,7 @@ import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; -import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; +import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreCourse } from '@features/course/services/course'; import { CanLeave } from '@guards/can-leave'; import { IonRefresher } from '@ionic/angular'; @@ -216,7 +216,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, OnDestroy, Ca /** * Helper to manage swiping within a collection of submissions. */ -class AddonModAssignSubmissionSwipeItemsManager extends CoreSwipeItemsManager { +class AddonModAssignSubmissionSwipeItemsManager extends CoreSwipeNavigationItemsManager { /** * @inheritdoc diff --git a/src/addons/mod/book/components/index/addon-mod-book-index.html b/src/addons/mod/book/components/index/addon-mod-book-index.html index 83787d103..247a4b5c7 100644 --- a/src/addons/mod/book/components/index/addon-mod-book-index.html +++ b/src/addons/mod/book/components/index/addon-mod-book-index.html @@ -40,14 +40,14 @@ previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)"> - - + +

- -
+
{{ 'core.tag.tags' | translate }}: - +
diff --git a/src/addons/mod/book/components/index/index.ts b/src/addons/mod/book/components/index/index.ts index 4c9f35cbd..d7f78d112 100644 --- a/src/addons/mod/book/components/index/index.ts +++ b/src/addons/mod/book/components/index/index.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Optional, Input, OnInit, ViewChild, ElementRef } from '@angular/core'; +import { Component, Optional, Input, OnInit, ViewChild, ElementRef, OnDestroy } from '@angular/core'; import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component'; import { AddonModBookProvider, @@ -31,10 +31,11 @@ import { AddonModBookTocComponent } from '../toc/toc'; 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 { CoreSwipeSlidesComponent, CoreSwipeSlidesOptions } from '@components/swipe-slides/swipe-slides'; +import { CoreSwipeSlidesItemsManagerSource } from '@classes/items-management/swipe-slides-items-manager-source'; import { CoreCourseModule } from '@features/course/services/course-helper'; -import { CoreSwipeSlidesItemsManager } from '@classes/items-management/slides-items-manager'; +import { CoreSwipeSlidesItemsManager } from '@classes/items-management/swipe-slides-items-manager'; +import { CoreTextUtils } from '@services/utils/text'; /** * Component that displays a book. @@ -43,7 +44,7 @@ import { CoreSwipeSlidesItemsManager } from '@classes/items-management/slides-it selector: 'addon-mod-book-index', templateUrl: 'addon-mod-book-index.html', }) -export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { +export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy { @ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent; @@ -51,9 +52,6 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp component = AddonModBookProvider.COMPONENT; manager?: CoreSwipeSlidesItemsManager; - previousChapter?: AddonModBookTocChapter; - nextChapter?: AddonModBookTocChapter; - tagsEnabled = false; warning = ''; displayNavBar = true; navigationItems: CoreNavigationBarItem[] = []; @@ -63,8 +61,9 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp scrollOnChange: 'top', }; - protected currentChapter?: number; + protected firstLoad = true; protected element: HTMLElement; + protected managerUnsubscribe?: () => void; constructor( elementRef: ElementRef, @@ -81,14 +80,18 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp async ngOnInit(): Promise { super.ngOnInit(); - this.tagsEnabled = CoreTag.areTagsAvailableInSite(); const source = new AddonModBookSlidesItemsManagerSource( this.courseId, this.module, - this.tagsEnabled, + CoreTag.areTagsAvailableInSite(), this.initialChapterId, ); this.manager = new CoreSwipeSlidesItemsManager(source); + this.managerUnsubscribe = this.manager.addListener({ + onSelectedItemUpdated: (item) => { + this.onChapterViewed(item.id); + }, + }); this.loadContent(); } @@ -106,12 +109,14 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp */ async showToc(): Promise { // Create the toc modal. + const visibleChapter = this.manager?.getSelectedItem(); + const modalData = await CoreDomUtils.openSideModal({ component: AddonModBookTocComponent, componentProps: { moduleId: this.module.id, chapters: this.chapters, - selected: this.currentChapter, + selected: visibleChapter, courseId: this.courseId, book: this.book, }, @@ -129,7 +134,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp * @return Promise resolved when done. */ changeChapter(chapterId: number): void { - if (!chapterId || chapterId === this.currentChapter) { + if (!chapterId) { return; } @@ -183,14 +188,15 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp } /** - * View a book chapter. + * Update data related to chapter being viewed. * - * @param chapterId Chapter to load. - * @param logChapterId Whether chapter ID should be passed to the log view function. + * @param chapterId Chapter viewed. * @return Promise resolved when done. */ - protected async viewChapter(chapterId: number, logChapterId: boolean): Promise { - this.currentChapter = chapterId; + protected async onChapterViewed(chapterId: number): Promise { + // Don't log the chapter ID when the user has just opened the book. + const logChapterId = this.firstLoad; + this.firstLoad = false; if (this.displayNavBar) { this.navigationItems = this.getNavigationItems(chapterId); @@ -212,15 +218,6 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp } } - /** - * Slide has changed. - * - * @param data Data about new item. - */ - slideChanged(data: CoreSwipeCurrentItemData): void { - this.viewChapter(data.item.id, true); - } - /** * Converts chapters to navigation items. * @@ -236,11 +233,20 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp })); } + /** + * @inheritdoc + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + + this.managerUnsubscribe && this.managerUnsubscribe(); + } + } type LoadedChapter = { id: number; - content: string; + content?: string; tags?: CoreTagItem[]; }; @@ -287,7 +293,6 @@ class AddonModBookSlidesItemsManagerSource extends CoreSwipeSlidesItemsManagerSo * Load module contents. */ async loadContents(): Promise { - // 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); @@ -310,10 +315,9 @@ class AddonModBookSlidesItemsManagerSource extends CoreSwipeSlidesItemsManagerSo })); 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'); + } catch (error) { + if (!CoreTextUtils.getErrorMessageFromError(error)) { + throw new CoreError(Translate.instant('addon.mod_book.errorchapter')); } throw error; diff --git a/src/addons/mod/forum/classes/forum-discussions-swipe-manager.ts b/src/addons/mod/forum/classes/forum-discussions-swipe-manager.ts index d409a6f58..2b1ca1e99 100644 --- a/src/addons/mod/forum/classes/forum-discussions-swipe-manager.ts +++ b/src/addons/mod/forum/classes/forum-discussions-swipe-manager.ts @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; +import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from './forum-discussions-source'; /** * Helper to manage swiping within a collection of discussions. */ export class AddonModForumDiscussionsSwipeManager - extends CoreSwipeItemsManager { + extends CoreSwipeNavigationItemsManager { /** * @inheritdoc diff --git a/src/addons/mod/glossary/classes/glossary-entries-swipe-manager.ts b/src/addons/mod/glossary/classes/glossary-entries-swipe-manager.ts index 45015a760..c6fc2bb57 100644 --- a/src/addons/mod/glossary/classes/glossary-entries-swipe-manager.ts +++ b/src/addons/mod/glossary/classes/glossary-entries-swipe-manager.ts @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; +import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { AddonModGlossaryEntriesSource, AddonModGlossaryEntryItem } from './glossary-entries-source'; /** * Helper to manage swiping within a collection of glossary entries. */ export abstract class AddonModGlossaryEntriesSwipeManager - extends CoreSwipeItemsManager { + extends CoreSwipeNavigationItemsManager { /** * @inheritdoc diff --git a/src/core/classes/items-management/items-manager-source.ts b/src/core/classes/items-management/items-manager-source.ts index c6b8fd275..4a2c7de2e 100644 --- a/src/core/classes/items-management/items-manager-source.ts +++ b/src/core/classes/items-management/items-manager-source.ts @@ -15,7 +15,7 @@ /** * Updates listener. */ -export interface CoreItemsListSourceListener { +export interface CoreItemsManagerSourceListener { onItemsUpdated?(items: Item[]): void; onReset?(): void; } @@ -26,16 +26,40 @@ export interface CoreItemsListSourceListener { export abstract class CoreItemsManagerSource { protected items: Item[] | null = null; - protected listeners: CoreItemsListSourceListener[] = []; + protected listeners: CoreItemsManagerSourceListener[] = []; protected dirty = false; + protected loaded = false; + protected loadedPromise: Promise; + protected resolveLoaded!: () => void; + + constructor() { + this.loadedPromise = new Promise(resolve => this.resolveLoaded = resolve); + } /** - * Check whether any item has been loaded. + * Check whether data is loaded. * - * @returns Whether any item has been loaded. + * @returns Whether data is loaded. */ isLoaded(): boolean { - return this.items !== null; + return this.loaded; + } + + /** + * Return a promise that is resolved when the data is loaded. + * + * @return Promise. + */ + waitForLoaded(): Promise { + return this.loadedPromise; + } + + /** + * Mark the source as initialized. + */ + protected setLoaded(): void { + this.loaded = true; + this.resolveLoaded(); } /** @@ -79,7 +103,7 @@ export abstract class CoreItemsManagerSource { * @param listener Listener. * @returns Unsubscribe function. */ - addListener(listener: CoreItemsListSourceListener): () => void { + addListener(listener: CoreItemsManagerSourceListener): () => void { this.listeners.push(listener); return () => this.removeListener(listener); @@ -90,7 +114,7 @@ export abstract class CoreItemsManagerSource { * * @param listener Listener. */ - removeListener(listener: CoreItemsListSourceListener): void { + removeListener(listener: CoreItemsManagerSourceListener): void { const index = this.listeners.indexOf(listener); if (index === -1) { @@ -107,6 +131,7 @@ export abstract class CoreItemsManagerSource { */ protected setItems(items: Item[]): void { this.items = items; + this.setLoaded(); this.notifyItemsUpdated(); } diff --git a/src/core/classes/items-management/items-manager.ts b/src/core/classes/items-management/items-manager.ts index dda251c1e..2cc88a2ac 100644 --- a/src/core/classes/items-management/items-manager.ts +++ b/src/core/classes/items-management/items-manager.ts @@ -14,6 +14,13 @@ import { CoreItemsManagerSource } from './items-manager-source'; +/** + * Listeners. + */ +export interface CoreItemsanagerListener { + onSelectedItemUpdated?(item: Item): void; +} + /** * Helper to manage a collection of items in a page. */ @@ -25,6 +32,7 @@ export abstract class CoreItemsManager< protected source?: { instance: Source; unsubscribe: () => void }; protected itemsMap: Record | null = null; protected selectedItem: Item | null = null; + protected listeners: CoreItemsanagerListener[] = []; constructor(source: Source) { this.setSource(source); @@ -96,6 +104,35 @@ export abstract class CoreItemsManager< */ setSelectedItem(item: Item | null): void { this.selectedItem = item; + + this.listeners.forEach(listener => listener.onSelectedItemUpdated?.call(listener, item)); + } + + /** + * Register a listener. + * + * @param listener Listener. + * @returns Unsubscribe function. + */ + addListener(listener: CoreItemsanagerListener): () => void { + this.listeners.push(listener); + + return () => this.removeListener(listener); + } + + /** + * Remove a listener. + * + * @param listener Listener. + */ + removeListener(listener: CoreItemsanagerListener): void { + const index = this.listeners.indexOf(listener); + + if (index === -1) { + return; + } + + this.listeners.splice(index, 1); } /** @@ -103,9 +140,12 @@ export abstract class CoreItemsManager< * * @param items New items. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars protected onSourceItemsUpdated(items: Item[]): void { - // Nothing to do. + this.itemsMap = items.reduce((map, item) => { + map[this.getItemId(item)] = item; + + return map; + }, {}); } /** @@ -116,4 +156,22 @@ export abstract class CoreItemsManager< this.selectedItem = null; } + /** + * Get item by ID. + * + * @param id ID + * @return Item, null if not found. + */ + getItemById(id: string | number): Item | null { + return this.itemsMap?.[id] ?? null; + } + + /** + * Get an ID to identify an item. + * + * @param item Data about the item. + * @return Item ID. + */ + abstract getItemId(item: Item): string | number; + } diff --git a/src/core/classes/items-management/routed-items-manager.ts b/src/core/classes/items-management/routed-items-manager.ts index fe8aa8d38..6feb244cc 100644 --- a/src/core/classes/items-management/routed-items-manager.ts +++ b/src/core/classes/items-management/routed-items-manager.ts @@ -123,13 +123,16 @@ export abstract class CoreRoutedItemsManager< * @inheritdoc */ protected onSourceItemsUpdated(items: Item[]): void { - this.itemsMap = items.reduce((map, item) => { - map[this.getSource().getItemPath(item)] = item; - - return map; - }, {}); + super.onSourceItemsUpdated(items); this.updateSelectedItem(); } + /** + * @inheritdoc + */ + getItemId(item: Item): string | number { + return this.getSource().getItemPath(item); + } + } diff --git a/src/core/classes/items-management/swipe-items-manager.ts b/src/core/classes/items-management/swipe-navigation-items-manager.ts similarity index 97% rename from src/core/classes/items-management/swipe-items-manager.ts rename to src/core/classes/items-management/swipe-navigation-items-manager.ts index 0290206ef..a156cedc9 100644 --- a/src/core/classes/items-management/swipe-items-manager.ts +++ b/src/core/classes/items-management/swipe-navigation-items-manager.ts @@ -22,7 +22,7 @@ import { CoreRoutedItemsManagerSource } from './routed-items-manager-source'; /** * Helper class to manage the state and routing of a swipeable page. */ -export class CoreSwipeItemsManager< +export class CoreSwipeNavigationItemsManager< Item = unknown, Source extends CoreRoutedItemsManagerSource = CoreRoutedItemsManagerSource > @@ -99,7 +99,7 @@ export class CoreSwipeItemsManager< protected async getItemBy(delta: number): Promise { const items = this.getSource().getItems(); - // Get current item. + // Get selected item. const index = (this.selectedItem && items?.indexOf(this.selectedItem)) ?? -1; if (index === -1) { diff --git a/src/core/classes/items-management/slides-dynamic-items-manager-source.ts b/src/core/classes/items-management/swipe-slides-dynamic-items-manager-source.ts similarity index 77% rename from src/core/classes/items-management/slides-dynamic-items-manager-source.ts rename to src/core/classes/items-management/swipe-slides-dynamic-items-manager-source.ts index a7324ebbc..cd54848c8 100644 --- a/src/core/classes/items-management/slides-dynamic-items-manager-source.ts +++ b/src/core/classes/items-management/swipe-slides-dynamic-items-manager-source.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreSwipeSlidesItemsManagerSource } from './slides-items-manager-source'; +import { CoreSwipeSlidesItemsManagerSource } from './swipe-slides-items-manager-source'; /** * Items collection source data for "swipe slides". @@ -26,16 +26,16 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource | null): Promise { - if (!this.initialized && this.initialItem) { + async load(selectedItem?: Item | null): Promise { + if (!this.loaded && this.initialItem) { // Load the initial item. await this.loadItem(this.initialItem); - } else if (this.initialized && currentItem) { - // Reload current item if needed. - await this.loadItem(currentItem); + } else if (this.loaded && selectedItem) { + // Reload selected item if needed. + await this.loadItem(selectedItem); } - this.setInitialized(); + this.setLoaded(); } /** @@ -49,35 +49,35 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource): Promise { + async loadItem(item: Item): Promise { 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, + previousItem && this.loadItemInList(previousItem, true), + nextItem && this.loadItemInList(nextItem, true), ]); } /** * Load or preload a certain item and add it to the list. * - * @param item Basic data about the item to load. + * @param item Item to load. * @param preload Whether to preload. * @return Promise resolved when done. */ - async loadItemInList(item: Partial, preload = false): Promise { + async loadItemInList(item: Item, preload = false): Promise { const preloadedItem = await this.performLoadItemData(item, preload); if (!preloadedItem) { return; } // Add the item at the right position. - const existingItem = this.getItem(item); + const existingItem = this.getItem(this.getItemId(item)); if (existingItem) { // Already in list, no need to add it. return; @@ -94,19 +94,19 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource { const itemId = this.getItemId(item); - let positionToInsert = -1; + let indexToInsert = -1; if (itemId === previousItemId) { // Previous item found, add the item after it. - positionToInsert = index + 1; + indexToInsert = index + 1; } if (itemId === nextItemId) { // Next item found, add the item before it. - positionToInsert = index; + indexToInsert = index; } - if (positionToInsert > -1) { - this.items?.splice(positionToInsert, 0, preloadedItem); + if (indexToInsert > -1) { + this.items?.splice(indexToInsert, 0, preloadedItem); return true; } @@ -124,19 +124,19 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource, preload: boolean): Promise { + protected async performLoadItemData(item: Item, preload: boolean): Promise { const itemId = this.getItemId(item); - if (!itemId || this.loadingItems[itemId]) { - // Not valid or already loading, ignore it. + if (this.loadingItems[itemId]) { + // Already loading, ignore it. return null; } - const existingItem = this.getItem(item); + const existingItem = this.getItem(itemId); if (existingItem && ((existingItem.loaded && !existingItem.dirty) || preload)) { // Already loaded, or preloading an already preloaded item. return existingItem; @@ -191,7 +191,7 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource, preload: boolean): Promise; + abstract loadItemData(item: Item, preload: boolean): Promise; /** * Return the data to identify the previous item. @@ -199,7 +199,7 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource): Partial | null; + abstract getPreviousItem(item: Item): Item | null; /** * Return the data to identify the next item. @@ -207,7 +207,7 @@ export abstract class CoreSwipeSlidesDynamicItemsManagerSource): Partial | null; + abstract getNextItem(item: Item): Item | null; } diff --git a/src/core/classes/items-management/slides-dynamic-items-manager.ts b/src/core/classes/items-management/swipe-slides-dynamic-items-manager.ts similarity index 89% rename from src/core/classes/items-management/slides-dynamic-items-manager.ts rename to src/core/classes/items-management/swipe-slides-dynamic-items-manager.ts index 718e71389..6bbb5d542 100644 --- a/src/core/classes/items-management/slides-dynamic-items-manager.ts +++ b/src/core/classes/items-management/swipe-slides-dynamic-items-manager.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreSwipeSlidesDynamicItem, CoreSwipeSlidesDynamicItemsManagerSource } from './slides-dynamic-items-manager-source'; -import { CoreSwipeSlidesItemsManager } from './slides-items-manager'; +import { CoreSwipeSlidesDynamicItem, CoreSwipeSlidesDynamicItemsManagerSource } from './swipe-slides-dynamic-items-manager-source'; +import { CoreSwipeSlidesItemsManager } from './swipe-slides-items-manager'; /** * Helper class to manage items for core-swipe-slides. diff --git a/src/core/classes/items-management/slides-items-manager-source.ts b/src/core/classes/items-management/swipe-slides-items-manager-source.ts similarity index 50% rename from src/core/classes/items-management/slides-items-manager-source.ts rename to src/core/classes/items-management/swipe-slides-items-manager-source.ts index e45c9d5b9..7dd41b754 100644 --- a/src/core/classes/items-management/slides-items-manager-source.ts +++ b/src/core/classes/items-management/swipe-slides-items-manager-source.ts @@ -19,40 +19,12 @@ import { CoreItemsManagerSource } from './items-manager-source'; */ export abstract class CoreSwipeSlidesItemsManagerSource extends CoreItemsManagerSource { - protected initialItem?: Partial; - protected initialized = false; - protected initializePromise: Promise; - protected resolveInitialize!: () => void; + protected initialItem?: Item; - constructor(initialItem?: Partial) { + constructor(initialItem?: 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 { - return this.initializePromise; - } - - /** - * Mark the source as initialized. - */ - protected setInitialized(): void { - this.initialized = true; - this.resolveInitialize(); } /** @@ -62,7 +34,6 @@ export abstract class CoreSwipeSlidesItemsManagerSource extends const items = await this.loadItems(); this.setItems(items); - this.setInitialized(); } /** @@ -75,47 +46,56 @@ export abstract class CoreSwipeSlidesItemsManagerSource extends /** * Get a certain item. * - * @param item Partial data about the item to search. + * @param id Item ID. * @return Item, null if not found. */ - getItem(item: Partial): Item | null { - const index = this.getItemPosition(item); + getItem(id: string | number): Item | null { + const index = this.getItemIndexById(id); return this.items?.[index] ?? null; } /** - * Get a certain item position. + * Get a certain item index. * - * @param item Item to search. - * @return Item position, -1 if not found. + * @param item Item. + * @return Item index, -1 if not found. */ - getItemPosition(item: Partial): number { - const itemId = this.getItemId(item); - const index = this.items?.findIndex((listItem) => itemId === this.getItemId(listItem)); + getItemIndex(item: Item): number { + return this.getItemIndexById(this.getItemId(item)); + } + + /** + * Get a certain item index. + * + * @param id Item ID. + * @return Item index, -1 if not found. + */ + getItemIndexById(id: string | number): number { + const index = this.items?.findIndex((listItem) => id === this.getItemId(listItem)); return index ?? -1; } /** - * Get initial item position. + * Get initial item index. * - * @return Initial item position. + * @return Initial item index. */ - getInitialPosition(): number { + getInitialItemIndex(): number { if (!this.initialItem) { return 0; } - return this.getItemPosition(this.initialItem); + return this.getItemIndex(this.initialItem); } /** * Get the ID of an item. * - * @param item Data about the item. + * @param item Item. * @return Item ID. */ - abstract getItemId(item: Partial): string | number; + abstract getItemId(item: Item): string | number; } diff --git a/src/core/classes/items-management/slides-items-manager.ts b/src/core/classes/items-management/swipe-slides-items-manager.ts similarity index 66% rename from src/core/classes/items-management/slides-items-manager.ts rename to src/core/classes/items-management/swipe-slides-items-manager.ts index 8c01ef571..5cd254b70 100644 --- a/src/core/classes/items-management/slides-items-manager.ts +++ b/src/core/classes/items-management/swipe-slides-items-manager.ts @@ -13,7 +13,7 @@ // limitations under the License. import { CoreItemsManager } from './items-manager'; -import { CoreSwipeSlidesItemsManagerSource } from './slides-items-manager-source'; +import { CoreSwipeSlidesItemsManagerSource } from './swipe-slides-items-manager-source'; /** * Helper class to manage items for core-swipe-slides. @@ -26,22 +26,8 @@ export class CoreSwipeSlidesItemsManager< /** * @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; + getItemId(item: Item): string | number { + return this.getSource().getItemId(item); } } diff --git a/src/core/components/swipe-navigation/swipe-navigation.ts b/src/core/components/swipe-navigation/swipe-navigation.ts index 57dab122d..c26e0ab67 100644 --- a/src/core/components/swipe-navigation/swipe-navigation.ts +++ b/src/core/components/swipe-navigation/swipe-navigation.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, Input } from '@angular/core'; -import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; +import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreScreen } from '@services/screen'; @Component({ @@ -23,7 +23,7 @@ import { CoreScreen } from '@services/screen'; }) export class CoreSwipeNavigationComponent { - @Input() manager?: CoreSwipeItemsManager; + @Input() manager?: CoreSwipeNavigationItemsManager; get enabled(): boolean { return CoreScreen.isMobile && !!this.manager; diff --git a/src/core/components/swipe-slides/swipe-slides.html b/src/core/components/swipe-slides/swipe-slides.html index 461366344..fc18d8c18 100644 --- a/src/core/components/swipe-slides/swipe-slides.html +++ b/src/core/components/swipe-slides/swipe-slides.html @@ -1,6 +1,5 @@ - - + + diff --git a/src/core/components/swipe-slides/swipe-slides.ts b/src/core/components/swipe-slides/swipe-slides.ts index ac88f6e8e..139393670 100644 --- a/src/core/components/swipe-slides/swipe-slides.ts +++ b/src/core/components/swipe-slides/swipe-slides.ts @@ -15,9 +15,10 @@ import { Component, ContentChild, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, TemplateRef, ViewChild, } from '@angular/core'; -import { CoreSwipeSlidesItemsManager } from '@classes/items-management/slides-items-manager'; +import { CoreSwipeSlidesItemsManager } from '@classes/items-management/swipe-slides-items-manager'; import { IonContent, IonSlides } from '@ionic/angular'; -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreDomUtils, VerticalPoint } from '@services/utils/dom'; +import { CoreMath } from '@singletons/math'; /** * Helper component to display swipable slides. @@ -31,7 +32,6 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe @Input() manager?: CoreSwipeSlidesItemsManager; @Input() options: CoreSwipeSlidesOptions = {}; - @Output() onInit = new EventEmitter>(); @Output() onWillChange = new EventEmitter>(); @Output() onDidChange = new EventEmitter>(); @@ -57,6 +57,14 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe } } + get items(): Item[] { + return this.manager?.getSource().getItems() || []; + } + + get loaded(): boolean { + return !!this.manager?.getSource().isLoaded(); + } + /** * Initialize some properties based on the manager. */ @@ -66,15 +74,15 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe }); // Don't call default callbacks on init, emit our own events instead. - // This is because default callbacks aren't triggered for position 0, and to prevent auto scroll on init. + // This is because default callbacks aren't triggered for index 0, and to prevent auto scroll on init. this.options.runCallbacksOnInit = false; - await manager.getSource().waitForInitialized(); + await manager.getSource().waitForLoaded(); if (this.options.initialSlide === undefined) { // Calculate the initial slide. - const position = manager.getSource().getInitialPosition(); - this.options.initialSlide = position > - 1 ? position : 0; + const index = manager.getSource().getInitialItemIndex(); + this.options.initialSlide = Math.max(index, 0); } // Emit change events with the initial item. @@ -83,28 +91,23 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe 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; - } + // Validate that the initial index is inside the valid range. + const initialIndex = CoreMath.clamp(this.options.initialSlide as number, 0, items.length - 1); const initialItemData = { - index: initialPosition, - item: items[initialPosition], + index: initialIndex, + item: items[initialIndex], }; - manager.setSelectedItem(items[initialPosition]); + manager.setSelectedItem(items[initialIndex]); this.onWillChange.emit(initialItemData); this.onDidChange.emit(initialItemData); } /** - * Slide to a certain position. + * Slide to a certain index. * - * @param index Position. + * @param index Index. * @param speed Animation speed. * @param runCallbacks Whether to run callbacks. */ @@ -119,8 +122,8 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe * @param speed Animation speed. * @param runCallbacks Whether to run callbacks. */ - slideToItem(item: Partial, speed?: number, runCallbacks?: boolean): void { - const index = this.manager?.getSource().getItemPosition(item) ?? -1; + slideToItem(item: Item, speed?: number, runCallbacks?: boolean): void { + const index = this.manager?.getSource().getItemIndex(item) ?? -1; if (index != -1) { this.slides?.slideTo(index, speed, runCallbacks); } @@ -146,29 +149,20 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe 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(); + const currentItem = this.manager?.getSelectedItem(); if (!currentItem || !this.manager) { return; } // Keep the same slide in case the list has changed. - const newIndex = this.manager.getSource().getItemPosition(currentItem) ?? -1; + const newIndex = this.manager.getSource().getItemIndex(currentItem) ?? -1; if (newIndex != -1) { this.slides?.slideTo(newIndex, 0, false); } @@ -194,7 +188,7 @@ export class CoreSwipeSlidesComponent implements OnChanges, OnDe // 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')) { + if (!scrollElement || CoreDomUtils.isElementOutsideOfScreen(scrollElement, this.hostElement, VerticalPoint.TOP)) { // Scroll to top. this.hostElement.scrollIntoView({ behavior: 'smooth' }); } diff --git a/src/core/features/user/pages/profile/profile.page.ts b/src/core/features/user/pages/profile/profile.page.ts index ee7c6e39c..69a8fcd26 100644 --- a/src/core/features/user/pages/profile/profile.page.ts +++ b/src/core/features/user/pages/profile/profile.page.ts @@ -27,7 +27,7 @@ import { CoreUserDelegate, CoreUserDelegateService, CoreUserProfileHandlerData } import { CoreUtils } from '@services/utils/utils'; import { CoreNavigator } from '@services/navigator'; import { CoreCourses } from '@features/courses/services/courses'; -import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; +import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreUserParticipantsSource } from '@features/user/classes/participants-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; @@ -227,7 +227,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { /** * Helper to manage swiping within a collection of users. */ -class CoreUserSwipeItemsManager extends CoreSwipeItemsManager { +class CoreUserSwipeItemsManager extends CoreSwipeNavigationItemsManager { /** * @inheritdoc diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index ba8d78d07..9b22c544e 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -772,7 +772,11 @@ export class CoreDomUtilsProvider { * @param point The point of the element to check. * @return Whether the element is outside of the viewport. */ - isElementOutsideOfScreen(scrollEl: HTMLElement, element: HTMLElement, point: 'top' | 'mid' | 'bottom' = 'mid'): boolean { + isElementOutsideOfScreen( + scrollEl: HTMLElement, + element: HTMLElement, + point: VerticalPoint = VerticalPoint.MID, + ): boolean { const elementRect = element.getBoundingClientRect(); if (!elementRect) { @@ -780,12 +784,18 @@ export class CoreDomUtilsProvider { } let elementPoint: number; - if (point === 'top') { - elementPoint = elementRect.top; - } else if (point === 'bottom') { - elementPoint = elementRect.bottom; - } else { - elementPoint = Math.round((elementRect.bottom + elementRect.top) / 2); + switch (point) { + case VerticalPoint.TOP: + elementPoint = elementRect.top; + break; + + case VerticalPoint.BOTTOM: + elementPoint = elementRect.bottom; + break; + + case VerticalPoint.MID: + elementPoint = Math.round((elementRect.bottom + elementRect.top) / 2); + break; } const scrollElRect = scrollEl.getBoundingClientRect(); @@ -2098,3 +2108,12 @@ export type PromptButton = Omit & { // eslint-disable-next-line @typescript-eslint/no-explicit-any handler?: (value: any, resolve: (value: any) => void, reject: (reason: any) => void) => void; }; + +/** + * Vertical points for an element. + */ +export enum VerticalPoint { + TOP = 'top', + MID = 'mid', + BOTTOM = 'bottom', +} diff --git a/src/core/services/utils/time.ts b/src/core/services/utils/time.ts index 5c8e420de..684c741b0 100644 --- a/src/core/services/utils/time.ts +++ b/src/core/services/utils/time.ts @@ -389,161 +389,6 @@ export class CoreTimeUtilsProvider { return String(moment().year() - 20); } - /** - * Given a year and a month, return the previous month and its year. - * - * @param month Year and month. - * @return Previous month and its year. - */ - getPreviousMonth(month: YearAndMonth): YearAndMonth { - const dayMoment = moment().year(month.year).month(month.monthNumber - 1); - dayMoment.subtract(1, 'month'); - - return { - year: dayMoment.year(), - monthNumber: dayMoment.month() + 1, - }; - } - - /** - * Given a year and a month, return the next month and its year. - * - * @param month Year and month. - * @return Next month and its year. - */ - getNextMonth(month: YearAndMonth): YearAndMonth { - const dayMoment = moment().year(month.year).month(month.monthNumber - 1); - dayMoment.add(1, 'month'); - - return { - year: dayMoment.year(), - monthNumber: dayMoment.month() + 1, - }; - } - - /** - * Get current month. - * - * @return Current month. - */ - getCurrentMonth(): YearAndMonth { - const now = new Date(); - - return { - year: now.getFullYear(), - monthNumber: now.getMonth() + 1, - }; - } - - /** - * Check if a certain month is a past month. - * - * @param month Year and month. - * @return Whether it's a past month. - */ - isPastMonth(month: YearAndMonth): boolean { - const now = new Date(); - - return month.year < now.getFullYear() || (month.year == now.getFullYear() && month.monthNumber < now.getMonth() + 1); - } - - /** - * @param monthA Month A. - * @param monthB Month B. - * @return Whether it's same month. - */ - isSameMonth(monthA: YearAndMonth, monthB: YearAndMonth): boolean { - return monthA.monthNumber === monthB.monthNumber && monthA.year === monthB.year; - } - - /** - * Given a day data, return the previous day. - * - * @param day Day. - * @return Previous day. - */ - getPreviousDay(day: YearMonthDay): YearMonthDay { - const dayMoment = moment().year(day.year).month(day.monthNumber - 1).date(day.dayNumber); - dayMoment.subtract(1, 'day'); - - return { - year: dayMoment.year(), - monthNumber: dayMoment.month() + 1, - dayNumber: dayMoment.date(), - }; - } - - /** - * Given a day data, return the next day. - * - * @param day Day. - * @return Next day. - */ - getNextDay(day: YearMonthDay): YearMonthDay { - const dayMoment = moment().year(day.year).month(day.monthNumber - 1).date(day.dayNumber); - dayMoment.add(1, 'day'); - - return { - year: dayMoment.year(), - monthNumber: dayMoment.month() + 1, - dayNumber: dayMoment.date(), - }; - } - - /** - * Get current day. - * - * @return Current day. - */ - getCurrentDay(): YearMonthDay { - const now = new Date(); - - return { - year: now.getFullYear(), - monthNumber: now.getMonth() + 1, - dayNumber: now.getDate(), - }; - } - - /** - * Check if a certain day is a past day. - * - * @param day Day. - * @return Whether it's a past day. - */ - isPastDay(day: YearMonthDay): boolean { - const now = new Date(); - - return day.year < now.getFullYear() || (day.year === now.getFullYear() && day.monthNumber < now.getMonth() + 1) || - (day.year === now.getFullYear() && day.monthNumber === now.getMonth() + 1 && day.dayNumber < now.getDate()); - } - - /** - * Check if two days are the same. - * - * @param dayA Day A. - * @param dayB Day B. - * @return Whether it's same day. - */ - isSameDay(dayA: YearMonthDay, dayB: YearMonthDay): boolean { - return dayA.dayNumber === dayB.dayNumber && dayA.monthNumber === dayB.monthNumber && dayA.year === dayB.year; - } - } export const CoreTimeUtils = makeSingleton(CoreTimeUtilsProvider); - -/** - * Data to identify a month. - */ -export type YearAndMonth = { - year: number; // Year number. - monthNumber: number; // Month number (1 to 12). -}; - -/** - * Data to identify a day. - */ -export type YearMonthDay = YearAndMonth & { - dayNumber: number; // Day number. -};