diff --git a/scripts/langindex.json b/scripts/langindex.json index df6d554e3..cc6776547 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1507,6 +1507,10 @@ "core.course.errordownloadingcourse": "local_moodlemobileapp", "core.course.errordownloadingsection": "local_moodlemobileapp", "core.course.errorgetmodule": "local_moodlemobileapp", + "core.course.gotonextactivity": "local_moodlemobileapp", + "core.course.gotonextactivitynotfound": "local_moodlemobileapp", + "core.course.gotopreviousactivity": "local_moodlemobileapp", + "core.course.gotopreviousactivitynotfound": "local_moodlemobileapp", "core.course.hiddenfromstudents": "moodle", "core.course.hiddenoncoursepage": "moodle", "core.course.insufficientavailablequota": "local_moodlemobileapp", diff --git a/src/addons/mod/assign/components/index/addon-mod-assign-index.html b/src/addons/mod/assign/components/index/addon-mod-assign-index.html index fd835cefd..2382ff0e7 100644 --- a/src/addons/mod/assign/components/index/addon-mod-assign-index.html +++ b/src/addons/mod/assign/components/index/addon-mod-assign-index.html @@ -147,3 +147,5 @@ [moduleId]="module.id"> + + 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 f6f090647..ce5420250 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 @@ -35,20 +35,23 @@ -
- +
+ - -
- {{ 'core.tag.tags' | translate }}: - -
+
+ - +
+ {{ '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 352b6cdd0..5d3281c25 100644 --- a/src/addons/mod/book/components/index/index.ts +++ b/src/addons/mod/book/components/index/index.ts @@ -26,11 +26,11 @@ import { import { CoreTag, CoreTagItem } from '@features/tag/services/tag'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; -import { Translate } from '@singletons'; 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'; /** * Component that displays a book. @@ -45,19 +45,16 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp component = AddonModBookProvider.COMPONENT; chapterContent?: string; - previousChapter?: AddonModBookTocChapter; - nextChapter?: AddonModBookTocChapter; tagsEnabled = false; - displayNavBar = true; - previousNavBarTitle?: string; - nextNavBarTitle?: string; warning = ''; tags?: CoreTagItem[]; + displayNavBar = true; + navigationItems: CoreNavigationBarItem[] = []; + displayTitlesInNavBar = false; protected chapters: AddonModBookTocChapter[] = []; protected currentChapter?: number; protected book?: AddonModBookBookWSData; - protected displayTitlesInNavBar = false; protected contentsMap: AddonModBookContentsMap = {}; constructor( @@ -148,14 +145,18 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp } } - if (typeof this.currentChapter == 'undefined') { + if (this.currentChapter === undefined) { // Load the first chapter. this.currentChapter = AddonModBook.getFirstChapter(this.chapters); } + if (this.currentChapter === undefined) { + return; + } + // Show chapter. try { - await this.loadChapter(this.currentChapter!, refresh); + await this.loadChapter(this.currentChapter, refresh); this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; } catch { @@ -199,15 +200,10 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp this.tags = this.tagsEnabled ? this.contentsMap[this.currentChapter].tags : []; this.chapterContent = content; - this.previousChapter = AddonModBook.getPreviousChapter(this.chapters, chapterId); - this.nextChapter = AddonModBook.getNextChapter(this.chapters, chapterId); - this.previousNavBarTitle = this.previousChapter && this.displayTitlesInNavBar - ? Translate.instant('addon.mod_book.navprevtitle', { $a: this.previousChapter.title }) - : ''; - this.nextNavBarTitle = this.nextChapter && this.displayTitlesInNavBar - ? Translate.instant('addon.mod_book.navnexttitle', { $a: this.nextChapter.title }) - : ''; + if (this.displayNavBar) { + this.navigationItems = this.getNavigationItems(chapterId); + } // Chapter loaded, log view. We don't return the promise because we don't want to block the user for this. await CoreUtils.ignoreErrors(AddonModBook.logView( @@ -216,8 +212,11 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp this.module.name, )); + const currentChapterIndex = this.chapters.findIndex((chapter) => chapter.id == chapterId); + const isLastChapter = currentChapterIndex < 0 || this.chapters[currentChapterIndex + 1] === undefined; + // Module is completed when last chapter is viewed, so we only check completion if the last is reached. - if (!this.nextChapter) { + if (isLastChapter) { CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); } } catch (error) { @@ -230,4 +229,19 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp } } + /** + * Converts chapters to navigation items. + * + * @param chapterId Current chapter Id. + * @return Navigation items. + */ + protected getNavigationItems(chapterId: number): CoreNavigationBarItem[] { + return this.chapters.map((chapter) => ({ + item: chapter, + title: chapter.title, + current: chapter.id == chapterId, + enabled: true, + })); + } + } diff --git a/src/addons/mod/book/services/book.ts b/src/addons/mod/book/services/book.ts index edf9c5800..9cd689d84 100644 --- a/src/addons/mod/book/services/book.ts +++ b/src/addons/mod/book/services/book.ts @@ -216,36 +216,6 @@ export class AddonModBookProvider { return chapters[0].id; } - /** - * Get the next chapter to the given one. - * - * @param chapters The chapters list. - * @param chapterId The current chapter. - * @return The next chapter. - */ - getNextChapter(chapters: AddonModBookTocChapter[], chapterId: number): AddonModBookTocChapter | undefined { - const currentChapterIndex = chapters.findIndex((chapter) => chapter.id == chapterId); - - if (currentChapterIndex >= 0 && typeof chapters[currentChapterIndex + 1] != 'undefined') { - return chapters[currentChapterIndex + 1]; - } - } - - /** - * Get the previous chapter to the given one. - * - * @param chapters The chapters list. - * @param chapterId The current chapter. - * @return The next chapter. - */ - getPreviousChapter(chapters: AddonModBookTocChapter[], chapterId: number): AddonModBookTocChapter | undefined { - const currentChapterIndex = chapters.findIndex((chapter) => chapter.id == chapterId); - - if (currentChapterIndex > 0) { - return chapters[currentChapterIndex - 1]; - } - } - /** * Get the book toc as an array. * diff --git a/src/addons/mod/chat/components/components.module.ts b/src/addons/mod/chat/components/components.module.ts index 954e3a8b4..c0bdb4ad4 100644 --- a/src/addons/mod/chat/components/components.module.ts +++ b/src/addons/mod/chat/components/components.module.ts @@ -27,8 +27,6 @@ import { AddonModChatUsersModalComponent } from './users-modal/users-modal'; CoreSharedModule, CoreCourseComponentsModule, ], - providers: [ - ], exports: [ AddonModChatIndexComponent, AddonModChatUsersModalComponent, diff --git a/src/addons/mod/chat/components/index/addon-mod-chat-index.html b/src/addons/mod/chat/components/index/addon-mod-chat-index.html index 93265acea..046853afd 100644 --- a/src/addons/mod/chat/components/index/addon-mod-chat-index.html +++ b/src/addons/mod/chat/components/index/addon-mod-chat-index.html @@ -47,3 +47,6 @@ + + + diff --git a/src/addons/mod/choice/components/index/addon-mod-choice-index.html b/src/addons/mod/choice/components/index/addon-mod-choice-index.html index a767fe487..4d291227e 100644 --- a/src/addons/mod/choice/components/index/addon-mod-choice-index.html +++ b/src/addons/mod/choice/components/index/addon-mod-choice-index.html @@ -155,6 +155,9 @@ + + +

diff --git a/src/addons/mod/data/components/index/addon-mod-data-index.html b/src/addons/mod/data/components/index/addon-mod-data-index.html index 8a1f45d10..1bb70403b 100644 --- a/src/addons/mod/data/components/index/addon-mod-data-index.html +++ b/src/addons/mod/data/components/index/addon-mod-data-index.html @@ -138,6 +138,9 @@ + + + diff --git a/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html b/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html index aacd8344e..c9cb04c87 100644 --- a/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html +++ b/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html @@ -55,6 +55,9 @@ + + + diff --git a/src/addons/mod/folder/components/index/addon-mod-folder-index.html b/src/addons/mod/folder/components/index/addon-mod-folder-index.html index cb1093a10..6c8d1aaeb 100644 --- a/src/addons/mod/folder/components/index/addon-mod-folder-index.html +++ b/src/addons/mod/folder/components/index/addon-mod-folder-index.html @@ -48,3 +48,6 @@ [message]=" 'addon.mod_folder.emptyfilelist' | translate"> + + + diff --git a/src/addons/mod/forum/components/index/index.html b/src/addons/mod/forum/components/index/index.html index d282b828e..63cd1b7b6 100644 --- a/src/addons/mod/forum/components/index/index.html +++ b/src/addons/mod/forum/components/index/index.html @@ -140,6 +140,9 @@ + + + diff --git a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html index c8a816a25..d6f12a61e 100644 --- a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html +++ b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html @@ -96,6 +96,9 @@ + + + diff --git a/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html b/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html index 4c7b7cd88..300b5feb7 100644 --- a/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html +++ b/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html @@ -84,3 +84,6 @@ [trackComponent]="trackComponent" [contextId]="h5pActivity?.context"> + + + diff --git a/src/addons/mod/h5pactivity/components/index/index.ts b/src/addons/mod/h5pactivity/components/index/index.ts index cf7b0b60b..762c6fefe 100644 --- a/src/addons/mod/h5pactivity/components/index/index.ts +++ b/src/addons/mod/h5pactivity/components/index/index.ts @@ -45,7 +45,6 @@ import { } from '../../services/h5pactivity-sync'; import { CoreFileHelper } from '@services/file-helper'; import { AddonModH5PActivityModuleHandlerService } from '../../services/handlers/module'; -import { CoreMainMenuPage } from '@features/mainmenu/pages/menu/menu'; /** * Component that displays an H5P activity entry page. @@ -87,7 +86,6 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv protected messageListenerFunction: (event: MessageEvent) => Promise; constructor( - protected mainMenuPage: CoreMainMenuPage, protected content?: IonContent, @Optional() courseContentsPage?: CoreCourseContentsPage, ) { diff --git a/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html b/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html index 4f15b3bc9..056553b9f 100644 --- a/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html +++ b/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html @@ -40,10 +40,11 @@

- +
+ + + diff --git a/src/addons/mod/imscp/components/index/index.scss b/src/addons/mod/imscp/components/index/index.scss index 42c755d23..78b62ae20 100644 --- a/src/addons/mod/imscp/components/index/index.scss +++ b/src/addons/mod/imscp/components/index/index.scss @@ -1,6 +1,4 @@ .addon-mod-imscp-container { - position: absolute; - width: 100%; height: 100%; display: flex; flex-direction: column; diff --git a/src/addons/mod/imscp/components/index/index.ts b/src/addons/mod/imscp/components/index/index.ts index b905e10e8..502478fab 100644 --- a/src/addons/mod/imscp/components/index/index.ts +++ b/src/addons/mod/imscp/components/index/index.ts @@ -14,6 +14,7 @@ import { Component, OnInit, Optional } from '@angular/core'; import { CoreSilentError } from '@classes/errors/silenterror'; +import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar'; import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component'; import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; import { CoreCourse } from '@features/course/services/course'; @@ -32,22 +33,19 @@ import { AddonModImscpTocComponent } from '../toc/toc'; export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { component = AddonModImscpProvider.COMPONENT; - - items: AddonModImscpTocItem[] = []; - currentItem?: string; src = ''; warning = ''; + navigationItems: CoreNavigationBarItem[] = []; - // Initialize empty previous/next to prevent showing arrows for an instant before they're hidden. - previousItem = ''; - nextItem = ''; + protected items: AddonModImscpTocItem[] = []; + protected currentHref?: string; constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) { super('AddonModImscpIndexComponent', courseContentsPage); } /** - * Component being initialized. + * @inheritdoc */ async ngOnInit(): Promise { super.ngOnInit(); @@ -90,19 +88,19 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom this.items = AddonModImscp.createItemList(contents); - if (this.items.length && typeof this.currentItem == 'undefined') { - this.currentItem = this.items[0].href; + if (this.items.length && this.currentHref === undefined) { + this.currentHref = this.items[0].href; } try { - await this.loadItem(this.currentItem); + await this.loadItemHref(this.currentHref); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_imscp.deploymenterror', true); throw new CoreSilentError(error); } - this.warning = downloadResult!.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult!.error!) : ''; + this.warning = downloadResult.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; } finally { // Pass false because downloadResourceIfNeeded already invalidates and refresh data if refresh=true. @@ -113,14 +111,18 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom /** * Loads an item. * - * @param itemId Item ID. + * @param itemHref Item Href. * @return Promise resolved when done. */ - async loadItem(itemId?: string): Promise { - const src = await AddonModImscp.getIframeSrc(this.module, itemId); - this.currentItem = itemId; - this.previousItem = itemId ? AddonModImscp.getPreviousItem(this.items, itemId) : ''; - this.nextItem = itemId ? AddonModImscp.getNextItem(this.items, itemId) : ''; + async loadItemHref(itemHref?: string): Promise { + const src = await AddonModImscp.getIframeSrc(this.module, itemHref); + this.currentHref = itemHref; + + this.navigationItems = this.items.map((item) => ({ + item: item, + current: item.href == this.currentHref, + enabled: !!item.href, + })); if (this.src && src == this.src) { // Re-loading same page. Set it to empty and then re-set the src in the next digest so it detects it has changed. @@ -133,6 +135,15 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom } } + /** + * Loads an item. + * + * @param item Item. + */ + loadItem(item: AddonModImscpTocItem): void { + this.loadItemHref(item.href); + } + /** * Show the TOC. */ @@ -142,12 +153,12 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom component: AddonModImscpTocComponent, componentProps: { items: this.items, - selected: this.currentItem, + selected: this.currentHref, }, }); if (modalData) { - this.loadItem(modalData); + this.loadItemHref(modalData); } } diff --git a/src/addons/mod/imscp/services/imscp.ts b/src/addons/mod/imscp/services/imscp.ts index 746cabcbc..3783cf37d 100644 --- a/src/addons/mod/imscp/services/imscp.ts +++ b/src/addons/mod/imscp/services/imscp.ts @@ -70,63 +70,6 @@ export class AddonModImscpProvider { return items; } - /** - * Get the previous item to the given one. - * - * @param items The items list. - * @param itemId The current item. - * @return The previous item id. - */ - getPreviousItem(items: AddonModImscpTocItem[], itemId: string): string { - const position = this.getItemPosition(items, itemId); - - if (position == -1) { - return ''; - } - - for (let i = position - 1; i >= 0; i--) { - if (items[i] && items[i].href) { - return items[i].href; - } - } - - return ''; - } - - /** - * Get the next item to the given one. - * - * @param items The items list. - * @param itemId The current item. - * @return The next item id. - */ - getNextItem(items: AddonModImscpTocItem[], itemId: string): string { - const position = this.getItemPosition(items, itemId); - - if (position == -1) { - return ''; - } - - for (let i = position + 1; i < items.length; i++) { - if (items[i] && items[i].href) { - return items[i].href; - } - } - - return ''; - } - - /** - * Get the position of a item. - * - * @param items The items list. - * @param itemId The item to search. - * @return The item position. - */ - protected getItemPosition(items: AddonModImscpTocItem[], itemId: string): number { - return items.findIndex((item) => item.href == itemId); - } - /** * Check if we should ommit the file download. * @@ -242,7 +185,7 @@ export class AddonModImscpProvider { const siteId = CoreSites.getCurrentSiteId(); try { - const dirPath = await CoreFilepool.getPackageDirUrlByUrl(siteId, module!.url!); + const dirPath = await CoreFilepool.getPackageDirUrlByUrl(siteId, module.url!); return CoreTextUtils.concatenatePaths(dirPath, itemHref); } catch (error) { diff --git a/src/addons/mod/label/services/handlers/module.ts b/src/addons/mod/label/services/handlers/module.ts index fe1306f3e..495e9cd51 100644 --- a/src/addons/mod/label/services/handlers/module.ts +++ b/src/addons/mod/label/services/handlers/module.ts @@ -39,6 +39,7 @@ export class AddonModLabelModuleHandlerService extends CoreModuleHandlerBase imp [CoreConstants.FEATURE_GRADE_OUTCOMES]: false, [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, + [CoreConstants.FEATURE_NO_VIEW_LINK]: true, }; /** diff --git a/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html b/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html index a9eeb2a60..0219bef8c 100644 --- a/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html +++ b/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html @@ -297,3 +297,6 @@ + + + diff --git a/src/addons/mod/lti/components/index/addon-mod-lti-index.html b/src/addons/mod/lti/components/index/addon-mod-lti-index.html index ef10e579e..20cf1a86e 100644 --- a/src/addons/mod/lti/components/index/addon-mod-lti-index.html +++ b/src/addons/mod/lti/components/index/addon-mod-lti-index.html @@ -32,3 +32,6 @@
+ + + diff --git a/src/addons/mod/page/components/index/addon-mod-page-index.html b/src/addons/mod/page/components/index/addon-mod-page-index.html index 2dcb76d66..078f5c5c9 100644 --- a/src/addons/mod/page/components/index/addon-mod-page-index.html +++ b/src/addons/mod/page/components/index/addon-mod-page-index.html @@ -48,3 +48,6 @@ + + + diff --git a/src/addons/mod/quiz/components/index/addon-mod-quiz-index.html b/src/addons/mod/quiz/components/index/addon-mod-quiz-index.html index cf49f5b7b..5d39ece10 100644 --- a/src/addons/mod/quiz/components/index/addon-mod-quiz-index.html +++ b/src/addons/mod/quiz/components/index/addon-mod-quiz-index.html @@ -226,3 +226,6 @@ + + + diff --git a/src/addons/mod/resource/components/index/addon-mod-resource-index.html b/src/addons/mod/resource/components/index/addon-mod-resource-index.html index 4c8e7daf6..9638db04e 100644 --- a/src/addons/mod/resource/components/index/addon-mod-resource-index.html +++ b/src/addons/mod/resource/components/index/addon-mod-resource-index.html @@ -18,7 +18,7 @@ - + - + + + diff --git a/src/addons/mod/scorm/components/index/addon-mod-scorm-index.html b/src/addons/mod/scorm/components/index/addon-mod-scorm-index.html index f184db40a..37ae91bfa 100644 --- a/src/addons/mod/scorm/components/index/addon-mod-scorm-index.html +++ b/src/addons/mod/scorm/components/index/addon-mod-scorm-index.html @@ -236,3 +236,6 @@ + + + diff --git a/src/addons/mod/scorm/pages/player/player.html b/src/addons/mod/scorm/pages/player/player.html index 469a6bd01..73bb4de59 100644 --- a/src/addons/mod/scorm/pages/player/player.html +++ b/src/addons/mod/scorm/pages/player/player.html @@ -20,8 +20,8 @@ - - + + diff --git a/src/addons/mod/scorm/pages/player/player.ts b/src/addons/mod/scorm/pages/player/player.ts index 9c4bdc9c9..d6dabb226 100644 --- a/src/addons/mod/scorm/pages/player/player.ts +++ b/src/addons/mod/scorm/pages/player/player.ts @@ -13,6 +13,7 @@ // limitations under the License. import { Component, OnInit, OnDestroy } from '@angular/core'; +import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar'; import { CoreMainMenuPage } from '@features/mainmenu/pages/menu/menu'; import { CoreNavigator } from '@services/navigator'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; @@ -50,8 +51,6 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { loadingToc = true; // Whether the TOC is being loaded. toc: AddonModScormTOCScoWithIcon[] = []; // List of SCOs. loaded = false; // Whether the data has been loaded. - previousSco?: AddonModScormScoWithData; // Previous SCO. - nextSco?: AddonModScormScoWithData; // Next SCO. src?: string; // Iframe src. errorMessage?: string; // Error message. accessInfo?: AddonModScormGetScormAccessInformationWSResponse; // Access information. @@ -60,6 +59,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { incomplete = false; // Whether last attempt is incomplete. cmId!: number; // Course module ID. courseId!: number; // Course ID. + navigationItems: CoreNavigationBarItem[] = []; protected siteId!: string; protected mode!: string; // Mode to play the SCORM. @@ -110,6 +110,8 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { await this.fetchData(); if (!this.currentSco) { + CoreNavigator.back(); + return; } @@ -176,14 +178,20 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { }, this.siteId); this.launchNextObserver = CoreEvents.on(AddonModScormProvider.LAUNCH_NEXT_SCO_EVENT, (data) => { - if (data.scormId === this.scorm.id && this.nextSco) { - this.loadSco(this.nextSco); + if (data.scormId === this.scorm.id && this.currentSco) { + const nextSco = AddonModScormHelper.getNextScoFromToc(this.toc, this.currentSco.id); + if (nextSco) { + this.loadSco(nextSco); + } } }, this.siteId); this.launchPrevObserver = CoreEvents.on(AddonModScormProvider.LAUNCH_PREV_SCO_EVENT, (data) => { - if (data.scormId === this.scorm.id && this.previousSco) { - this.loadSco(this.previousSco); + if (data.scormId === this.scorm.id && this.currentSco) { + const previousSco = AddonModScormHelper.getPreviousScoFromToc(this.toc, this.currentSco.id); + if (previousSco) { + this.loadSco(previousSco); + } } }, this.siteId); @@ -211,9 +219,16 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { * * @param scoId Current SCO ID. */ - protected calculateNextAndPreviousSco(scoId: number): void { - this.previousSco = AddonModScormHelper.getPreviousScoFromToc(this.toc, scoId); - this.nextSco = AddonModScormHelper.getNextScoFromToc(this.toc, scoId); + protected calculateNavigationItems(scoId: number): void { + this.navigationItems = this.toc + .filter((item) => item.isvisible) + .map>((item) => + ({ + item: item, + title: item.title, + current: item.id == scoId, + enabled: !!(item.prereq && item.launch), + })); } /** @@ -398,7 +413,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { this.currentSco = sco; this.title = sco.title || this.scorm.name; // Try to use SCO title. - this.calculateNextAndPreviousSco(sco.id); + this.calculateNavigationItems(sco.id); // Load the SCO source. this.loadScoSrc(sco); @@ -540,7 +555,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { } /** - * Component being destroyed. + * @inheritdoc */ ngOnDestroy(): void { // Empty src when leaving the state so unload event is triggered in the iframe. diff --git a/src/addons/mod/scorm/services/scorm-helper.ts b/src/addons/mod/scorm/services/scorm-helper.ts index d62311a2c..ffc21754c 100644 --- a/src/addons/mod/scorm/services/scorm-helper.ts +++ b/src/addons/mod/scorm/services/scorm-helper.ts @@ -280,18 +280,13 @@ export class AddonModScormHelperProvider { * @return Next SCO. */ getNextScoFromToc(toc: AddonModScormScoWithData[], scoId: number): AddonModScormScoWithData | undefined { - for (let i = 0; i < toc.length; i++) { - if (toc[i].id != scoId) { - continue; - } + const currentTocIndex = toc.findIndex((item) => item.id == scoId); - // We found the current SCO. Now search the next visible SCO with fulfilled prerequisites. - for (let j = i + 1; j < toc.length; j++) { - if (toc[j].isvisible && toc[j].prereq && toc[j].launch) { - return toc[j]; - } + // We found the current SCO. Now search the next visible SCO with fulfilled prerequisites. + for (let j = currentTocIndex + 1; j < toc.length; j++) { + if (toc[j].isvisible && toc[j].prereq && toc[j].launch) { + return toc[j]; } - break; } } @@ -303,18 +298,13 @@ export class AddonModScormHelperProvider { * @return Previous SCO. */ getPreviousScoFromToc(toc: AddonModScormScoWithData[], scoId: number): AddonModScormScoWithData | undefined { - for (let i = 0; i < toc.length; i++) { - if (toc[i].id != scoId) { - continue; - } + const currentTocIndex = toc.findIndex((item) => item.id == scoId); - // We found the current SCO. Now let's search the previous visible SCO with fulfilled prerequisites. - for (let j = i - 1; j >= 0; j--) { - if (toc[j].isvisible && toc[j].prereq && toc[j].launch) { - return toc[j]; - } + // We found the current SCO. Now let's search the previous visible SCO with fulfilled prerequisites. + for (let j = currentTocIndex - 1; j >= 0; j--) { + if (toc[j].isvisible && toc[j].prereq && toc[j].launch) { + return toc[j]; } - break; } } diff --git a/src/addons/mod/survey/components/index/addon-mod-survey-index.html b/src/addons/mod/survey/components/index/addon-mod-survey-index.html index b5042fbf5..345c10b5a 100644 --- a/src/addons/mod/survey/components/index/addon-mod-survey-index.html +++ b/src/addons/mod/survey/components/index/addon-mod-survey-index.html @@ -147,3 +147,6 @@ + + + diff --git a/src/addons/mod/url/components/index/addon-mod-url-index.html b/src/addons/mod/url/components/index/addon-mod-url-index.html index 491e186eb..7026fddf3 100644 --- a/src/addons/mod/url/components/index/addon-mod-url-index.html +++ b/src/addons/mod/url/components/index/addon-mod-url-index.html @@ -13,7 +13,7 @@ - + + + + diff --git a/src/addons/mod/url/services/handlers/module.ts b/src/addons/mod/url/services/handlers/module.ts index e6257f845..69c7d2c4b 100644 --- a/src/addons/mod/url/services/handlers/module.ts +++ b/src/addons/mod/url/services/handlers/module.ts @@ -19,7 +19,7 @@ import { CoreModuleHandlerBase } from '@features/course/classes/module-base-hand import { CoreCourse } from '@features/course/services/course'; import { CoreCourseModule } from '@features/course/services/course-helper'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; -import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; +import { CoreNavigationOptions } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; @@ -90,12 +90,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple if (shouldOpen) { openUrl(module, courseId); } else { - options = options || {}; - options.params = options.params || {}; - Object.assign(options.params, { module }); - const routeParams = '/' + courseId + '/' + module.id; - - CoreNavigator.navigateToSitePath(AddonModUrlModuleHandlerService.PAGE_NAME + routeParams, options); + this.openActivityPage(module, courseId, options); } } finally { modal.dismiss(); diff --git a/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html b/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html index e6628246f..ee8fec538 100644 --- a/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html +++ b/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html @@ -89,6 +89,9 @@ + + + diff --git a/src/addons/mod/workshop/components/index/addon-mod-workshop-index.html b/src/addons/mod/workshop/components/index/addon-mod-workshop-index.html index 1aedc639d..8a5e5543d 100644 --- a/src/addons/mod/workshop/components/index/addon-mod-workshop-index.html +++ b/src/addons/mod/workshop/components/index/addon-mod-workshop-index.html @@ -253,3 +253,6 @@ + + + diff --git a/src/core/components/loading/loading.scss b/src/core/components/loading/loading.scss index c3cfbf655..3861b04e2 100644 --- a/src/core/components/loading/loading.scss +++ b/src/core/components/loading/loading.scss @@ -44,6 +44,7 @@ } .core-loading-content { + display: contents; @include core-transition(opacity, 200ms); } @@ -51,10 +52,6 @@ @include margin(10px, 0, 0, 0); } - &.core-loading-fullheight .core-loading-content { - height: 100%; - } - &.core-loading-loaded { position: unset; display: contents; diff --git a/src/core/components/navigation-bar/core-navigation-bar.html b/src/core/components/navigation-bar/core-navigation-bar.html index 175e0f549..3f5b6df49 100644 --- a/src/core/components/navigation-bar/core-navigation-bar.html +++ b/src/core/components/navigation-bar/core-navigation-bar.html @@ -1,27 +1,27 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + +

{{currentIndex + 1}} / {{items.length}}

+
+
+ + + + + + +
diff --git a/src/core/components/navigation-bar/navigation-bar.scss b/src/core/components/navigation-bar/navigation-bar.scss index f48edf9b3..b79c274dd 100644 --- a/src/core/components/navigation-bar/navigation-bar.scss +++ b/src/core/components/navigation-bar/navigation-bar.scss @@ -1,7 +1,15 @@ -.core-navigation-bar-arrow { - text-transform: none; - max-width: 100%; - ion-icon { - flex-shrink: 0; +:host { + --background: var(--core-course-module-navigation-background); + + width: 100%; + background-color: var(--background); + display: block; + + .core-navigation-bar-arrow { + text-transform: none; + max-width: 100%; + ion-icon { + flex-shrink: 0; + } } } diff --git a/src/core/components/navigation-bar/navigation-bar.ts b/src/core/components/navigation-bar/navigation-bar.ts index 71f2f28ef..92681ea06 100644 --- a/src/core/components/navigation-bar/navigation-bar.ts +++ b/src/core/components/navigation-bar/navigation-bar.ts @@ -12,48 +12,103 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { CoreTextUtils } from '@services/utils/text'; +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange } from '@angular/core'; +import { Translate } from '@singletons'; /** - * Component to show a "bar" with arrows to navigate forward/backward and a "info" icon to display more data. + * Component to show a "bar" with arrows to navigate forward/backward and an slider to move around. * * This directive will show two arrows at the left and right of the screen to navigate to previous/next item when clicked. - * If no previous/next item is defined, that arrow won't be shown. It will also show a button to show more info. + * If no previous/next item is defined, that arrow won't be shown. * * Example usage: - * + * */ @Component({ selector: 'core-navigation-bar', templateUrl: 'core-navigation-bar.html', styleUrls: ['navigation-bar.scss'], }) -export class CoreNavigationBarComponent { +export class CoreNavigationBarComponent implements OnChanges { - @Input() previous?: unknown; // Previous item. If not defined, the previous arrow won't be shown. - @Input() previousTitle?: string; // Previous item title. If not defined, only the arrow will be shown. - @Input() next?: unknown; // Next item. If not defined, the next arrow won't be shown. - @Input() nextTitle?: string; // Next item title. If not defined, only the arrow will be shown. - @Input() info = ''; // Info to show when clicking the info button. If not defined, the info button won't be shown. - @Input() title = ''; // Title to show when seeing the info (new page). + @Input() items: CoreNavigationBarItem[] = []; // List of items. + @Input() showTitles = false; // Display titles on buttons. + @Input() previousTranslate = 'core.previous'; // Previous translatable text, can admit $a variable. + @Input() nextTranslate = 'core.next'; // Next translatable text, can admit $a variable. @Input() component?: string; // Component the bar belongs to. @Input() componentId?: number; // Component ID. @Input() contextLevel?: string; // The context level. @Input() contextInstanceId?: number; // The instance ID related to the context. @Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters. - @Output() action?: EventEmitter = - new EventEmitter(); // Function to call when arrow is clicked. Will receive as a param the item to load. - showInfo(): void { - CoreTextUtils.viewText(this.title, this.info, { - component: this.component, - componentId: this.componentId, - filter: true, - contextLevel: this.contextLevel, - instanceId: this.contextInstanceId, - courseId: this.courseId, - }); + previousTitle?: string; // Previous item title. + nextTitle?: string; // Next item title. + previousIndex = -1; // Previous item index. If -1, the previous arrow won't be shown. + nextIndex = -1; // Next item index. If -1, the next arrow won't be shown. + currentIndex = 0; + + // Function to call when arrow is clicked. Will receive as a param the item to load. + @Output() action: EventEmitter = new EventEmitter(); + + /** + * @inheritdoc + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + if (!changes.items || !this.items.length) { + return; + } + + this.currentIndex = this.items.findIndex((item) => item.current); + if (this.currentIndex < 0) { + return; + } + + this.nextIndex = this.items[this.currentIndex + 1]?.enabled ? this.currentIndex + 1 : -1; + if (this.nextIndex >= 0) { + this.nextTitle = Translate.instant(this.nextTranslate, { $a: this.items[this.nextIndex].title || '' }); + } + + this.previousIndex = this.items[this.currentIndex - 1]?.enabled ? this.currentIndex - 1 : -1; + if (this.previousIndex >= 0) { + this.previousTitle = Translate.instant(this.previousTranslate, { $a: this.items[this.previousIndex].title || '' }); + } + } + + /** + * Navigate to an item. + * + * @param itemIndex Selected item index. + */ + navigate(itemIndex: number): void { + if (this.currentIndex == itemIndex || !this.items[itemIndex].enabled) { + return; + } + + this.currentIndex = itemIndex; + this.action.emit(this.items[itemIndex].item); + } + + /** + * Navigate to an item with the range component. + * + * @param target: Element changed. + */ + navigateOnRange(target: HTMLIonRangeElement): void { + const selectedIndex = target.value as number; // Single value, use number. + if (!this.items[selectedIndex].enabled) { + target.value = this.currentIndex; + + return; + } + + this.navigate(selectedIndex); } } + +export type CoreNavigationBarItem = { + item: T; + title?: string; + current: boolean; + enabled: boolean; +}; diff --git a/src/core/components/split-view/split-view.scss b/src/core/components/split-view/split-view.scss index deb84173f..a333f7623 100644 --- a/src/core/components/split-view/split-view.scss +++ b/src/core/components/split-view/split-view.scss @@ -22,6 +22,7 @@ .menu, .content-outlet { top: var(--offset-top); + height: calc(100% - var(--offset-top)); right: 0; bottom: 0; left: 0; diff --git a/src/core/directives/collapsible-header.ts b/src/core/directives/collapsible-header.ts index 11191f7c6..626a5e7b6 100644 --- a/src/core/directives/collapsible-header.ts +++ b/src/core/directives/collapsible-header.ts @@ -48,8 +48,12 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { this.header = el.nativeElement; this.loadingObserver = CoreEvents.on(CoreEvents.CORE_LOADING_CHANGED, async (data) => { + if (!data.loaded) { + return; + } + const loadingId = await this.getLoadingId(); - if (loadingId && data.loaded && data.uniqueId == loadingId) { + if (loadingId && data.uniqueId == loadingId) { // Remove event when loading is done. this.loadingObserver.off(); @@ -78,7 +82,7 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { } } - return this.content.querySelector('core-loading .core-loading-content')?.id; + return this.content.querySelector('core-loading.core-loading-loaded:not(.core-loading-inline) .core-loading-content')?.id; } /** diff --git a/src/core/directives/fab.ts b/src/core/directives/fab.ts index d3d0677cb..d8cd1db30 100644 --- a/src/core/directives/fab.ts +++ b/src/core/directives/fab.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Directive, ElementRef, OnDestroy } from '@angular/core'; -import { IonContent } from '@ionic/angular'; +import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core'; +import { CoreUtils } from '@services/utils/utils'; /** * Directive to move ion-fab components as direct children of the nearest ion-content. @@ -25,47 +25,50 @@ import { IonContent } from '@ionic/angular'; @Directive({ selector: 'ion-fab[core-fab]', }) -export class CoreFabDirective implements OnDestroy { +export class CoreFabDirective implements OnInit, OnDestroy { - protected static readonly PADDINGBOTTOM = 56; - - protected scrollElement?: HTMLElement; - protected done = false; protected element: HTMLElement; + protected content?: HTMLIonContentElement | null; + protected initialPaddingBottom = 0; - constructor(el: ElementRef, protected content: IonContent) { + constructor(el: ElementRef) { this.element = el.nativeElement; - this.asyncInit(); + this.element.setAttribute('slot', 'fixed'); } /** - * Initialize Component. + * @inheritdoc */ - async asyncInit(): Promise { - if (this.content) { - this.scrollElement = await this.content.getScrollElement(); - if (!this.done) { - // Move element to the nearest ion-content if it's not the parent - if (this.element.parentElement?.nodeName != 'ION-CONTENT') { - const ionContent = this.element.closest('ion-content'); - ionContent?.appendChild(this.element); - } + async ngOnInit(retries = 3): Promise { + this.content = this.element.closest('ion-content'); + if (!this.content) { + if(retries > 0) { + await CoreUtils.nextTicks(50); - // Add space at the bottom to let the user see the whole content. - const bottom = parseInt(this.scrollElement.style.paddingBottom, 10) || 0; - this.scrollElement.style.paddingBottom = (bottom + CoreFabDirective.PADDINGBOTTOM) + 'px'; - this.done = true; + this.ngOnInit(retries - 1); } + + return; } + + const initialHeight = this.element.getBoundingClientRect().height || 56; + + // Move element to the nearest ion-content if it's not the parent + if (this.element.parentElement?.nodeName != 'ION-CONTENT') { + this.content.appendChild(this.element); + } + + // Add space at the bottom to let the user see the whole content. + this.initialPaddingBottom = parseFloat(this.content.style.getPropertyValue('--padding-bottom') || '0'); + this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + initialHeight + 'px'); } /** - * Destroy component. + * @inheritdoc */ ngOnDestroy(): void { - if (this.done && this.scrollElement) { - const bottom = parseInt(this.scrollElement.style.paddingBottom, 10) || 0; - this.scrollElement.style.paddingBottom = (bottom - CoreFabDirective.PADDINGBOTTOM) + 'px'; + if (this.content) { + this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px'); } } diff --git a/src/core/features/course/classes/module-base-handler.ts b/src/core/features/course/classes/module-base-handler.ts index f45aa6a53..3d59a9d00 100644 --- a/src/core/features/course/classes/module-base-handler.ts +++ b/src/core/features/course/classes/module-base-handler.ts @@ -45,15 +45,37 @@ export class CoreModuleHandlerBase implements Partial { title: module.name, class: 'addon-mod_' + module.modname + '-handler', showDownloadButton: true, - action: (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void => { - options = options || {}; - options.params = options.params || {}; - Object.assign(options.params, { module }); - const routeParams = '/' + courseId + '/' + module.id; - - CoreNavigator.navigateToSitePath(this.pageName + routeParams, options); + action: async ( + event: Event, + module: CoreCourseModule, + courseId: number, + options?: CoreNavigationOptions, + ): Promise => { + await this.openActivityPage(module, courseId, options); }, }; } + /** + * Opens the activity page. + * + * @param module The module object. + * @param courseId The course ID. + * @param options Options for the navigation. + * @return Promise resolved when done. + */ + async openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise { + if (!CoreCourse.moduleHasView(module)) { + return; + } + + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { module }); + + const routeParams = '/' + courseId + '/' + module.id; + + await CoreNavigator.navigateToSitePath(this.pageName + routeParams, options); + } + } diff --git a/src/core/features/course/components/components.module.ts b/src/core/features/course/components/components.module.ts index 3680f6f36..516a43111 100644 --- a/src/core/features/course/components/components.module.ts +++ b/src/core/features/course/components/components.module.ts @@ -26,6 +26,7 @@ import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsup import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy'; import { CoreCourseModuleInfoComponent } from './module-info/module-info'; import { CoreCourseModuleManualCompletionComponent } from './module-manual-completion/module-manual-completion'; +import { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation'; @NgModule({ declarations: [ @@ -39,6 +40,7 @@ import { CoreCourseModuleManualCompletionComponent } from './module-manual-compl CoreCourseSectionSelectorComponent, CoreCourseTagAreaComponent, CoreCourseUnsupportedModuleComponent, + CoreCourseModuleNavigationComponent, ], imports: [ CoreBlockComponentsModule, @@ -55,6 +57,7 @@ import { CoreCourseModuleManualCompletionComponent } from './module-manual-compl CoreCourseSectionSelectorComponent, CoreCourseTagAreaComponent, CoreCourseUnsupportedModuleComponent, + CoreCourseModuleNavigationComponent, ], }) export class CoreCourseComponentsModule {} diff --git a/src/core/features/course/components/module-info/core-course-module-info.html b/src/core/features/course/components/module-info/core-course-module-info.html index 7afb15368..702b6ff54 100644 --- a/src/core/features/course/components/module-info/core-course-module-info.html +++ b/src/core/features/course/components/module-info/core-course-module-info.html @@ -13,24 +13,25 @@ + [contextInstanceId]="module.id" [courseId]="courseId" [maxHeight]="expandDescription ? null : 120"> - + -
+

{{ date.label }} {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }}

- + diff --git a/src/core/features/course/components/module-info/module-info.ts b/src/core/features/course/components/module-info/module-info.ts index 063ebc88f..2c958c99f 100644 --- a/src/core/features/course/components/module-info/module-info.ts +++ b/src/core/features/course/components/module-info/module-info.ts @@ -36,12 +36,14 @@ import { CoreSites } from '@services/sites'; export class CoreCourseModuleInfoComponent implements OnInit { @Input() module!: CoreCourseModule; // The module to render. + @Input() showManualCompletion = true; // Whether to show manual completion, true by default. @Input() courseId!: number; // The courseId the module belongs to. @Input() component!: string; // Component for format text directive. @Input() componentId!: string | number; // Component ID to use in conjunction with the component. @Input() description?: string | false; // The description to display. If false, no description will be shown. + @Input() expandDescription = false; // If the description should be expanded by default. @Input() hasDataToSync = false; // If the activity has any data to be synced. diff --git a/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html b/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html index ab4055ff0..d205a5a65 100644 --- a/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html +++ b/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html @@ -2,20 +2,22 @@ - + {{ 'core.course.completion_manual:done' | translate }} - + {{ 'core.course.completion_manual:markdone' | translate }} - + {{ 'core.course.completion_manual:markdone' | translate }} diff --git a/src/core/features/course/components/module-navigation/core-course-module-navigation.html b/src/core/features/course/components/module-navigation/core-course-module-navigation.html new file mode 100644 index 000000000..74fc6f584 --- /dev/null +++ b/src/core/features/course/components/module-navigation/core-course-module-navigation.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/src/core/features/course/components/module-navigation/module-navigation.scss b/src/core/features/course/components/module-navigation/module-navigation.scss new file mode 100644 index 000000000..2a8b74f54 --- /dev/null +++ b/src/core/features/course/components/module-navigation/module-navigation.scss @@ -0,0 +1,43 @@ +@import "~theme/globals"; + +:host { + --height: var(--core-course-module-navigation-height, var(--core-course-module-navigation-max-height)); + --background: var(--core-course-module-navigation-background); + + height: var(--height); + width: 100%; + background-color: var(--background); + display: block; + bottom: 0; + z-index: 3; + box-shadow: 0px -3px 3px rgba(var(--drop-shadow)); + + @include core-transition(all, 200ms); + + ion-col { + padding: 2px; + } + + core-loading { + text-align: center; + } + + ion-buttom { + margin-top: 5px; + margin-bottom: 5px; + } + + core-loading { + --loading-inline-min-height: var(--height); + } +} + +:host-context(.core-iframe-fullscreen) { + opacity: 0 !important; + height: 0 !important; +} + +:host-context(core-course-format.core-course-format-singleactivity) { + opacity: 0 !important; + height: 0 !important; +} diff --git a/src/core/features/course/components/module-navigation/module-navigation.ts b/src/core/features/course/components/module-navigation/module-navigation.ts new file mode 100644 index 000000000..dfb9602a4 --- /dev/null +++ b/src/core/features/course/components/module-navigation/module-navigation.ts @@ -0,0 +1,338 @@ +// (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, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { CoreCourse, CoreCourseProvider, CoreCourseWSSection } from '@features/course/services/course'; +import { CoreCourseModule, CoreCourseSection } from '@features/course/services/course-helper'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { IonContent } from '@ionic/angular'; +import { ScrollDetail } from '@ionic/core'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; + +/** + * Component to show a button to go to the next resource/activity. + * + * Example usage: + * + */ +@Component({ + selector: 'core-course-module-navigation', + templateUrl: 'core-course-module-navigation.html', + styleUrls: ['module-navigation.scss'], +}) +export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy { + + @Input() courseId!: number; // Course ID. + @Input() currentModuleId!: number; // Current module ID. + + nextModule?: CoreCourseModule; + previousModule?: CoreCourseModule; + nextModuleSection?: CoreCourseSection; + previousModuleSection?: CoreCourseSection; + loaded = false; + + protected element: HTMLElement; + protected initialHeight = 0; + protected initialPaddingBottom = 0; + protected previousTop = 0; + protected content?: HTMLIonContentElement | null; + protected completionObserver: CoreEventObserver; + + constructor(el: ElementRef, protected ionContent: IonContent) { + const siteId = CoreSites.getCurrentSiteId(); + + this.element = el.nativeElement; + this.element.setAttribute('slot', 'fixed'); + + this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_MODULE_VIEWED, async (data) => { + if (data && data.courseId == this.courseId) { + // Check if now there's a next module. + await this.setNextAndPreviousModules( + CoreSitesReadingStrategy.PREFER_NETWORK, + !this.nextModule, + !this.previousModule, + ); + } + }, siteId); + } + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + try { + await this.setNextAndPreviousModules(CoreSitesReadingStrategy.PREFER_CACHE); + } finally { + this.loaded = true; + + await CoreUtils.nextTicks(50); + this.listenScrollEvents(); + } + } + + /** + * Setup scroll event listener. + * + * @param retries Number of retries left. + */ + protected async listenScrollEvents(retries = 3): Promise { + this.initialHeight = this.element.getBoundingClientRect().height; + + if (this.initialHeight == 0 && retries > 0) { + await CoreUtils.nextTicks(50); + + this.listenScrollEvents(retries - 1); + + return; + } + // Set a minimum height value. + this.initialHeight = this.initialHeight || 56; + + this.content = this.element.closest('ion-content'); + + if (!this.content) { + return; + } + + // Special case where there's no navigation. + const courseFormat = this.element.closest('core-course-format.core-course-format-singleactivity'); + if (courseFormat) { + this.element.remove(); + this.ngOnDestroy(); + + return; + } + + // Move element to the nearest ion-content if it's not the parent. + if (this.element.parentElement?.nodeName != 'ION-CONTENT') { + this.content.appendChild(this.element); + } + + // Set a padding to not overlap elements. + this.initialPaddingBottom = parseFloat(this.content.style.getPropertyValue('--padding-bottom') || '0'); + this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + this.initialHeight + 'px'); + const scroll = await this.content.getScrollElement(); + this.content.scrollEvents = true; + + this.setBarHeight(this.initialHeight); + this.content.addEventListener('ionScroll', (e: CustomEvent): void => { + if (!this.content) { + return; + } + + this.onScroll(e.detail.scrollTop, scroll.scrollHeight - scroll.offsetHeight); + }); + + } + + /** + * @inheritdoc + */ + async ngOnDestroy(): Promise { + this.completionObserver.off(); + this.content?.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px'); + } + + /** + * Set previous and next modules. + * + * @param readingStrategy Reading strategy. + * @param checkNext Check next module. + * @param checkPrevious Check previous module. + * @return Promise resolved when done. + */ + protected async setNextAndPreviousModules( + readingStrategy: CoreSitesReadingStrategy, + checkNext = true, + checkPrevious = true, + ): Promise { + if (!checkNext && !checkPrevious) { + return; + } + + const preSets = CoreSites.getReadingStrategyPreSets(readingStrategy); + + const sections = await CoreCourse.getSections(this.courseId, false, true, preSets); + + // Search the next module. + let currentModuleIndex = -1; + + const currentSectionIndex = sections.findIndex((section) => { + if (!this.isSectionAvailable(section)) { + // User cannot view the section, skip it. + return false; + } + + currentModuleIndex = section.modules.findIndex((module: CoreCourseModule) => module.id == this.currentModuleId); + + return currentModuleIndex >= 0; + }); + + if (currentSectionIndex < 0) { + // Nothing found. Return. + + return; + } + + if (checkNext) { + // Find next Module. + this.nextModule = undefined; + for (let i = currentSectionIndex; i < sections.length && this.nextModule == undefined; i++) { + const section = sections[i]; + + if (!this.isSectionAvailable(section)) { + // User cannot view the section, skip it. + continue; + } + + const startModule = i == currentSectionIndex ? currentModuleIndex + 1 : 0; + for (let j = startModule; j < section.modules.length && this.nextModule == undefined; j++) { + const module = section.modules[j]; + + const found = await this.isModuleAvailable(module); + if (found) { + this.nextModule = module; + this.nextModuleSection = section; + } + } + } + } + + if (checkPrevious) { + // Find previous Module. + this.previousModule = undefined; + for (let i = currentSectionIndex; i >= 0 && this.previousModule == undefined; i--) { + const section = sections[i]; + + if (!this.isSectionAvailable(section)) { + // User cannot view the section, skip it. + continue; + } + + const startModule = i == currentSectionIndex ? currentModuleIndex - 1 : section.modules.length - 1; + for (let j = startModule; j >= 0 && this.previousModule == undefined; j--) { + const module = section.modules[j]; + + const found = await this.isModuleAvailable(module); + if (found) { + this.previousModule = module; + this.previousModuleSection = section; + } + } + } + } + } + + /** + * Module is visible by the user and it has a specific view (e.g. not a label). + * + * @param module Module to check. + * @return Wether the module is available to the user or not. + */ + protected async isModuleAvailable(module: CoreCourseModule): Promise { + return CoreCourse.instance.moduleHasView(module); + } + + /** + * Section is visible by the user and its not stealth + * + * @param section Section to check. + * @return Wether the module is available to the user or not. + */ + protected isSectionAvailable(section: CoreCourseWSSection): boolean { + return section.uservisible !== false && section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID; + } + + /** + * Go to next/previous module. + * + * @return Promise resolved when done. + */ + async goToActivity(next = true): Promise { + if (!this.loaded) { + return; + } + + const modal = await CoreDomUtils.showModalLoading(); + + // Re-calculate module in case a new module was made visible. + await CoreUtils.ignoreErrors(this.setNextAndPreviousModules(CoreSitesReadingStrategy.PREFER_NETWORK, next, !next)); + + modal.dismiss(); + + const module = next ? this.nextModule : this.previousModule; + if (!module) { + // It seems the module was hidden. Show a message. + CoreDomUtils.instance.showErrorModal( + next ? 'core.course.gotonextactivitynotfound' : 'core.course.gotopreviousactivitynotfound', + true, + ); + + return; + } + + if (module.uservisible === false) { + const section = next ? this.nextModuleSection : this.previousModuleSection; + const options: CoreNavigationOptions = { + replace: true, + params: { + module, + section, + }, + }; + CoreNavigator.navigateToSitePath('course/' + this.courseId + '/' + module.id +'/module-preview', options); + } else { + CoreCourseModuleDelegate.openActivityPage(module.modname, module, this.courseId, { replace: true }); + } + } + + /** + * On scroll function. + * + * @param top Scroll top measure. + * @param maxScroll Scroll height. + */ + protected onScroll(top: number, maxScroll: number): void { + if (top == 0 || top == maxScroll) { + // Reset. + this.setBarHeight(this.initialHeight); + } else { + const diffHeight = this.element.clientHeight - (top - this.previousTop); + this.setBarHeight(diffHeight); + } + + this.previousTop = top; + } + + /** + * Sets the bar height. + * + * @param height The new bar height. + */ + protected setBarHeight(height: number): void { + if (height <= 0) { + height = 0; + } else if (height > this.initialHeight) { + height = this.initialHeight; + } + + this.element.style.opacity = height == 0 ? '0' : '1'; + this.content?.style.setProperty('--core-course-module-navigation-height', height + 'px'); + } + +} diff --git a/src/core/features/course/components/module/core-course-module.html b/src/core/features/course/components/module/core-course-module.html index ea1bec5df..65637562c 100644 --- a/src/core/features/course/components/module/core-course-module.html +++ b/src/core/features/course/components/module/core-course-module.html @@ -75,8 +75,8 @@
- diff --git a/src/core/features/course/components/module/module.ts b/src/core/features/course/components/module/module.ts index 5431e5114..b077bd66f 100644 --- a/src/core/features/course/components/module/module.ts +++ b/src/core/features/course/components/module/module.ts @@ -161,7 +161,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { event.preventDefault(); event.stopPropagation(); - button.action(event, this.module!, this.courseId!); + button.action(event, this.module, this.courseId!); } /** diff --git a/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html b/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html index 6b7fe1848..a55256ca3 100644 --- a/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html +++ b/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html @@ -1,6 +1,3 @@ - - -

{{ 'core.whoops' | translate }}

{{ 'core.uhoh' | translate }}

diff --git a/src/core/features/course/course-lazy.module.ts b/src/core/features/course/course-lazy.module.ts index 80f28334e..5233864dd 100644 --- a/src/core/features/course/course-lazy.module.ts +++ b/src/core/features/course/course-lazy.module.ts @@ -23,9 +23,9 @@ const routes: Routes = [ loadChildren: () => import('./pages/index/index.module').then( m => m.CoreCourseIndexPageModule), }, { - path: ':courseId/unsupported-module', - loadChildren: () => import('./pages/unsupported-module/unsupported-module.module') - .then( m => m.CoreCourseUnsupportedModulePageModule), + path: ':courseId/:cmId/module-preview', + loadChildren: () => import('./pages/module-preview/module-preview.module') + .then( m => m.CoreCourseModulePreviewPageModule), }, { path: ':courseId/list-mod-type', diff --git a/src/core/features/course/lang.json b/src/core/features/course/lang.json index 7afa1a068..bba7ba758 100644 --- a/src/core/features/course/lang.json +++ b/src/core/features/course/lang.json @@ -27,6 +27,10 @@ "confirmpartialdownloadsize": "You are about to download at least {{size}}.{{availableSpace}} Are you sure you want to continue?", "confirmlimiteddownload": "You are not currently connected to Wi-Fi. ", "contents": "Contents", + "gotonextactivity": "Continue to next activity", + "gotonextactivitynotfound": "Next activity not found. It's possible that it has been hidden or deleted.", + "gotopreviousactivity": "Continue to previous activity", + "gotopreviousactivitynotfound": "Previous activity not found. It's possible that it has been hidden or deleted.", "couldnotloadsectioncontent": "Could not load the section content. Please try again later.", "couldnotloadsections": "Could not load the sections. Please try again later.", "coursesummary": "Course summary", diff --git a/src/core/features/course/pages/module-preview/module-preview.html b/src/core/features/course/pages/module-preview/module-preview.html new file mode 100644 index 000000000..8f352f3b5 --- /dev/null +++ b/src/core/features/course/pages/module-preview/module-preview.html @@ -0,0 +1,65 @@ + + + + + + +

+ + +

+
+ + + + + + + +
+
+ + + + + + + +
+ + + +
+
+ + {{ 'core.course.hiddenfromstudents' | translate }} + +
+
+ + {{ 'core.course.hiddenoncoursepage' | translate }} + +
+
+ {{ 'core.restricted' | translate }} +
+ + +
+
+
+ + {{ 'core.course.manualcompletionnotsynced' | translate }} + +
+ + +
+
+ + +
diff --git a/src/core/features/course/pages/unsupported-module/unsupported-module.module.ts b/src/core/features/course/pages/module-preview/module-preview.module.ts similarity index 83% rename from src/core/features/course/pages/unsupported-module/unsupported-module.module.ts rename to src/core/features/course/pages/module-preview/module-preview.module.ts index 596d96650..ab346fcb0 100644 --- a/src/core/features/course/pages/unsupported-module/unsupported-module.module.ts +++ b/src/core/features/course/pages/module-preview/module-preview.module.ts @@ -16,13 +16,13 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; -import { CoreCourseUnsupportedModulePage } from './unsupported-module.page'; +import { CoreCourseModulePreviewPage } from './module-preview.page'; import { CoreCourseComponentsModule } from '@features/course/components/components.module'; const routes: Routes = [ { path: '', - component: CoreCourseUnsupportedModulePage, + component: CoreCourseModulePreviewPage, }, ]; @@ -33,8 +33,8 @@ const routes: Routes = [ CoreCourseComponentsModule, ], declarations: [ - CoreCourseUnsupportedModulePage, + CoreCourseModulePreviewPage, ], exports: [RouterModule], }) -export class CoreCourseUnsupportedModulePageModule {} +export class CoreCourseModulePreviewPageModule { } diff --git a/src/core/features/course/pages/module-preview/module-preview.page.ts b/src/core/features/course/pages/module-preview/module-preview.page.ts new file mode 100644 index 000000000..51dc29b23 --- /dev/null +++ b/src/core/features/course/pages/module-preview/module-preview.page.ts @@ -0,0 +1,118 @@ +// (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, OnInit } from '@angular/core'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreCourseHelper, CoreCourseModule, CoreCourseSection } from '@features/course/services/course-helper'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { IonRefresher } from '@ionic/angular'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; + +/** + * Page that displays a module preview. + */ +@Component({ + selector: 'page-core-course-module-preview', + templateUrl: 'module-preview.html', +}) +export class CoreCourseModulePreviewPage implements OnInit { + + title!: string; + module!: CoreCourseModule; + section?: CoreCourseSection; // The section the module belongs to. + courseId!: number; + loaded = false; + unsupported = false; + showManualCompletion = false; + + protected debouncedUpdateModule?: () => void; // Update the module after a certain time. + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + try { + this.module = CoreNavigator.getRequiredRouteParam('module'); + this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); + this.section = CoreNavigator.getRouteParam('section'); + } catch (error) { + CoreDomUtils.showErrorModal(error); + + CoreNavigator.back(); + + return; + } + + this.debouncedUpdateModule = CoreUtils.debounce(() => { + this.doRefresh(); + }, 10000); + + await this.fetchModule(); + } + + /** + * Fetch module. + * + * @return Promise resolved when done. + */ + protected async fetchModule(refresh = false): Promise { + if (refresh) { + this.module = await CoreCourse.getModule(this.module.id, this.courseId); + } + + CoreCourseHelper.calculateModuleCompletionData(this.module, this.courseId); + + await CoreCourseHelper.loadModuleOfflineCompletion(this.courseId, this.module); + + this.unsupported = !CoreCourseModuleDelegate.getHandlerName(this.module.modname); + if (!this.unsupported) { + this.module.handlerData = + await CoreCourseModuleDelegate.getModuleDataFor(this.module.modname, this.module, this.courseId); + } + + this.title = this.module.name; + + this.showManualCompletion = await CoreCourseModuleDelegate.manualCompletionAlwaysShown(this.module); + + this.loaded = true; + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + * @return Promise resolved when done. + */ + async doRefresh(refresher?: IonRefresher): Promise { + + await CoreCourse.invalidateModule(this.module.id); + + this.fetchModule(true); + + refresher?.complete(); + } + + /** + * The completion of the modules has changed. + * + * @return Promise resolved when done. + */ + async onCompletionChange(): Promise { + // Update the module data after a while. + this.debouncedUpdateModule?.(); + } + +} diff --git a/src/core/features/course/pages/unsupported-module/unsupported-module.html b/src/core/features/course/pages/unsupported-module/unsupported-module.html deleted file mode 100644 index 6760fda91..000000000 --- a/src/core/features/course/pages/unsupported-module/unsupported-module.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - -

- - -

-
- - - - - - - - - -
-
- - - diff --git a/src/core/features/course/pages/unsupported-module/unsupported-module.page.ts b/src/core/features/course/pages/unsupported-module/unsupported-module.page.ts deleted file mode 100644 index 78bb28a44..000000000 --- a/src/core/features/course/pages/unsupported-module/unsupported-module.page.ts +++ /dev/null @@ -1,54 +0,0 @@ -// (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, OnInit } from '@angular/core'; - -import { CoreCourseWSModule } from '@features/course/services/course'; -import { CoreNavigator } from '@services/navigator'; -import { CoreTextUtils } from '@services/utils/text'; -import { Translate } from '@singletons'; - -/** - * Page that displays info about an unsupported module. - */ -@Component({ - selector: 'page-core-course-unsupported-module', - templateUrl: 'unsupported-module.html', -}) -export class CoreCourseUnsupportedModulePage implements OnInit { - - module?: CoreCourseWSModule; - courseId?: number; - - /** - * @inheritDoc - */ - ngOnInit(): void { - this.module = CoreNavigator.getRouteParam('module'); - this.courseId = CoreNavigator.getRouteNumberParam('courseId'); - } - - /** - * Expand the description. - */ - expandDescription(): void { - CoreTextUtils.viewText(Translate.instant('core.description'), this.module!.description!, { - filter: true, - contextLevel: 'module', - instanceId: this.module!.id, - courseId: this.courseId, - }); - } - -} diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 81505df14..49590c2af 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -44,6 +44,7 @@ import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync'; import { CoreTagItem } from '@features/tag/services/tag'; import { CoreNavigator } from '@services/navigator'; +import { CoreCourseModuleDelegate } from './module-delegate'; const ROOT_CACHE_KEY = 'mmCourse:'; @@ -1058,6 +1059,14 @@ export class CoreCourseProvider { * @return Whether the module has a view page. */ moduleHasView(module: CoreCourseModuleSummary | CoreCourseWSModule): boolean { + if ('modname' in module) { + // noviewlink was introduced in 3.8.5, use supports feature as a fallback. + if (module.noviewlink || + CoreCourseModuleDelegate.supportsFeature(module.modname, CoreConstants.FEATURE_NO_VIEW_LINK, false)) { + return false; + } + } + return !!module.url; } diff --git a/src/core/features/course/services/handlers/default-module.ts b/src/core/features/course/services/handlers/default-module.ts index 25bf41663..6e8cb25d6 100644 --- a/src/core/features/course/services/handlers/default-module.ts +++ b/src/core/features/course/services/handlers/default-module.ts @@ -49,14 +49,11 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler { icon: await CoreCourse.getModuleIconSrc(module.modname, module.modicon), title: module.name, class: 'core-course-default-handler core-course-module-' + module.modname + '-handler', - action: (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => { + action: async (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => { event.preventDefault(); event.stopPropagation(); - options = options || {}; - options.params = { module }; - - CoreNavigator.navigateToSitePath('course/' + courseId + '/unsupported-module', options); + await this.openActivityPage(module, courseId, options); }, }; @@ -92,4 +89,15 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler { return true; } + /** + * @inheritdoc + */ + async openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise { + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { module }); + + await CoreNavigator.navigateToSitePath('course/' + courseId + '/' + module.id +'/module-preview', options); + } + } diff --git a/src/core/features/course/services/module-delegate.ts b/src/core/features/course/services/module-delegate.ts index e5f918d1f..2776043ff 100644 --- a/src/core/features/course/services/module-delegate.ts +++ b/src/core/features/course/services/module-delegate.ts @@ -102,6 +102,16 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler { * @return Promise resolved with boolean: whether the manual completion should always be displayed. */ manualCompletionAlwaysShown?(module: CoreCourseModule): Promise; + + /** + * Opens the activity page. + * + * @param module The module object. + * @param courseId The course ID. + * @param options Options for the navigation. + * @return Promise resolved when done. + */ + openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise; } /** @@ -167,8 +177,9 @@ export interface CoreCourseModuleHandlerData { * @param module The module object. * @param courseId The course ID. * @param options Options for the navigation. + * @return Promise resolved when done. */ - action?(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void; + action?(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise | void; /** * Updates the status of the module. @@ -236,8 +247,10 @@ export interface CoreCourseModuleHandlerButton { * @param event The click event. * @param module The module object. * @param courseId The course ID. + * @param options Options for the navigation. + * @return Promise resolved when done. */ - action(event: Event, module: CoreCourseModule, courseId: number): void; + action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise | void; } /** @@ -292,6 +305,27 @@ export class CoreCourseModuleDelegateService extends CoreDelegate { + return await this.executeFunctionOnEnabled( + modname, + 'openActivityPage', + [module, courseId, options], + ); + } + /** * Check if a certain module type is disabled in a site. * diff --git a/src/core/features/emulator/components/capture-media/capture-media.html b/src/core/features/emulator/components/capture-media/capture-media.html index f7be75b5c..f85e62a51 100644 --- a/src/core/features/emulator/components/capture-media/capture-media.html +++ b/src/core/features/emulator/components/capture-media/capture-media.html @@ -11,7 +11,7 @@ - +
diff --git a/src/core/features/siteplugins/classes/handlers/module-handler.ts b/src/core/features/siteplugins/classes/handlers/module-handler.ts index 1276a3a87..847d2894b 100644 --- a/src/core/features/siteplugins/classes/handlers/module-handler.ts +++ b/src/core/features/siteplugins/classes/handlers/module-handler.ts @@ -15,7 +15,7 @@ import { Type } from '@angular/core'; import { CoreConstants } from '@/core/constants'; -import { CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course'; +import { CoreCourse, CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course'; import { CoreCourseModule } from '@features/course/services/course-helper'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index'; @@ -92,17 +92,16 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp if (this.handlerSchema.method) { // There is a method, add an action. - handlerData.action = (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => { + handlerData.action = async ( + event: Event, + module: CoreCourseModule, + courseId: number, + options?: CoreNavigationOptions, + ) => { event.preventDefault(); event.stopPropagation(); - options = options || {}; - options.params = { - title: module.name, - module, - }; - - CoreNavigator.navigateToSitePath(`siteplugins/module/${courseId}/${module.id}`, options); + await this.openActivityPage(module, courseId, options); }; } @@ -229,4 +228,22 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp return false; } + /** + * @inheritdoc + */ + async openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise { + if (!CoreCourse.moduleHasView(module)) { + return; + } + + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { + title: module.name, + module, + }); + + CoreNavigator.navigateToSitePath(`siteplugins/module/${courseId}/${module.id}`, options); + } + } diff --git a/src/core/features/siteplugins/components/components.module.ts b/src/core/features/siteplugins/components/components.module.ts index 2969000e5..b9482e300 100644 --- a/src/core/features/siteplugins/components/components.module.ts +++ b/src/core/features/siteplugins/components/components.module.ts @@ -28,6 +28,7 @@ import { CoreSitePluginsAssignSubmissionComponent } from './assign-submission/as import { CoreSitePluginsWorkshopAssessmentStrategyComponent } from './workshop-assessment-strategy/workshop-assessment-strategy'; import { CoreSitePluginsBlockComponent } from './block/block'; import { CoreSitePluginsOnlyTitleBlockComponent } from './only-title-block/only-title-block'; +import { CoreCourseComponentsModule } from '@features/course/components/components.module'; @NgModule({ declarations: [ @@ -47,6 +48,7 @@ import { CoreSitePluginsOnlyTitleBlockComponent } from './only-title-block/only- imports: [ CoreSharedModule, CoreCompileHtmlComponentModule, + CoreCourseComponentsModule, ], exports: [ CoreSitePluginsPluginContentComponent, diff --git a/src/core/features/siteplugins/components/module-index/core-siteplugins-module-index.html b/src/core/features/siteplugins/components/module-index/core-siteplugins-module-index.html index 930a6e0a0..ac5c6b837 100644 --- a/src/core/features/siteplugins/components/module-index/core-siteplugins-module-index.html +++ b/src/core/features/siteplugins/components/module-index/core-siteplugins-module-index.html @@ -11,8 +11,7 @@ + [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> + + diff --git a/src/core/features/viewer/pages/iframe/iframe.html b/src/core/features/viewer/pages/iframe/iframe.html index 5b2ae550d..213a23e16 100644 --- a/src/core/features/viewer/pages/iframe/iframe.html +++ b/src/core/features/viewer/pages/iframe/iframe.html @@ -9,7 +9,7 @@ - + diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index c23675e5c..77a664817 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -975,7 +975,7 @@ export class CoreDomUtilsProvider { * @deprecated since 3.9.5. Use directly the IonContent class. */ scrollTo(content: IonContent, x: number, y: number, duration?: number): Promise { - return content?.scrollToPoint(x, y, duration || 0); + return content.scrollToPoint(x, y, duration || 0); } /** @@ -987,7 +987,7 @@ export class CoreDomUtilsProvider { * @deprecated since 3.9.5. Use directly the IonContent class. */ scrollToBottom(content: IonContent, duration?: number): Promise { - return content?.scrollToBottom(duration); + return content.scrollToBottom(duration); } /** @@ -999,7 +999,7 @@ export class CoreDomUtilsProvider { * @deprecated since 3.9.5. Use directly the IonContent class. */ scrollToTop(content: IonContent, duration?: number): Promise { - return content?.scrollToTop(duration); + return content.scrollToTop(duration); } /** @@ -1010,9 +1010,9 @@ export class CoreDomUtilsProvider { */ async getContentHeight(content: IonContent): Promise { try { - const scrollElement = await content?.getScrollElement(); + const scrollElement = await content.getScrollElement(); - return scrollElement?.clientHeight || 0; + return scrollElement.clientHeight || 0; } catch (error) { return 0; } @@ -1026,9 +1026,9 @@ export class CoreDomUtilsProvider { */ async getScrollHeight(content: IonContent): Promise { try { - const scrollElement = await content?.getScrollElement(); + const scrollElement = await content.getScrollElement(); - return scrollElement?.scrollHeight || 0; + return scrollElement.scrollHeight || 0; } catch (error) { return 0; } @@ -1042,9 +1042,9 @@ export class CoreDomUtilsProvider { */ async getScrollTop(content: IonContent): Promise { try { - const scrollElement = await content?.getScrollElement(); + const scrollElement = await content.getScrollElement(); - return scrollElement?.scrollTop || 0; + return scrollElement.scrollTop || 0; } catch (error) { return 0; } @@ -1065,7 +1065,7 @@ export class CoreDomUtilsProvider { return false; } - content?.scrollToPoint(position[0], position[1], duration || 0); + content.scrollToPoint(position[0], position[1], duration || 0); return true; } @@ -1097,7 +1097,7 @@ export class CoreDomUtilsProvider { return false; } - content?.scrollToPoint(position[0], position[1], duration || 0); + content.scrollToPoint(position[0], position[1], duration || 0); return true; } catch (error) { diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index 4b9278132..51f3cb7f2 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -906,13 +906,16 @@ ion-back-button.md::part(text) { display: none; } +// Hide close button because when present is read on voice over. ion-fab[core-fab] { - position: fixed; - - // Hide close button because when present is read on voice over. ion-fab-button::part(close-icon) { display: none; } +} + +core-course-module-navigation + ion-fab { + bottom: calc(var(--core-course-module-navigation-height, 0px) + 10px); + @include core-transition(all, 200ms); } .core-media-adapt-width { diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss index fa001efb9..a4b0b935e 100644 --- a/src/theme/theme.light.scss +++ b/src/theme/theme.light.scss @@ -257,6 +257,9 @@ --core-courseimage-on-course-height: 150px; + --core-course-module-navigation-max-height: 56px; + --core-course-module-navigation-background: var(--contrast-background); + --addon-calendar-event-category-color: var(--purple); --addon-calendar-event-course-color: var(--red); --addon-calendar-event-group-color: var(--yellow); diff --git a/upgrade.txt b/upgrade.txt index 19393ed60..4f36e38bc 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -4,6 +4,8 @@ information provided here is intended especially for developers. === 3.9.6 === - The parameters of the functions confirmAndPrefetchCourse and confirmAndPrefetchCourses have changed, they now accept an object with options. +- Component core-navigation-bar changed to add an slider inside. previous, previousTitle, next, nextTitle, info and title have been removed. + Now you have to pass all items and 3 optional params have been added. === 3.9.5 ===