forked from EVOgeek/Vmeda.Online
		
	
						commit
						994374c0b3
					
				| @ -1507,6 +1507,10 @@ | |||||||
|   "core.course.errordownloadingcourse": "local_moodlemobileapp", |   "core.course.errordownloadingcourse": "local_moodlemobileapp", | ||||||
|   "core.course.errordownloadingsection": "local_moodlemobileapp", |   "core.course.errordownloadingsection": "local_moodlemobileapp", | ||||||
|   "core.course.errorgetmodule": "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.hiddenfromstudents": "moodle", | ||||||
|   "core.course.hiddenoncoursepage": "moodle", |   "core.course.hiddenoncoursepage": "moodle", | ||||||
|   "core.course.insufficientavailablequota": "local_moodlemobileapp", |   "core.course.insufficientavailablequota": "local_moodlemobileapp", | ||||||
|  | |||||||
| @ -147,3 +147,5 @@ | |||||||
|         [moduleId]="module.id"> |         [moduleId]="module.id"> | ||||||
|     </addon-mod-assign-submission> |     </addon-mod-assign-submission> | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"></core-course-module-navigation> | ||||||
|  | |||||||
| @ -35,20 +35,23 @@ | |||||||
|         </ion-item> |         </ion-item> | ||||||
|     </ion-card> |     </ion-card> | ||||||
| 
 | 
 | ||||||
|     <div class="ion-padding safe-area-padding-horizontal"> |     <div class="safe-area-padding-horizontal"> | ||||||
|         <core-navigation-bar *ngIf="displayNavBar" [previous]="previousChapter?.id" [previousTitle]="previousNavBarTitle" |         <core-navigation-bar *ngIf="displayNavBar" [items]="navigationItems" [showTitles]="displayTitlesInNavBar" | ||||||
|             [next]="nextChapter?.id" [nextTitle]="nextNavBarTitle" (action)="changeChapter($event)"> |             previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)"> | ||||||
|         </core-navigation-bar> |         </core-navigation-bar> | ||||||
| 
 | 
 | ||||||
|         <core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent" contextLevel="module" |         <div class="ion-padding"> | ||||||
|             [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text> |             <core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent" contextLevel="module" | ||||||
|         <div class="ion-margin-top" *ngIf="tagsEnabled && tags?.length > 0"> |                 [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text> | ||||||
|             <strong>{{ 'core.tag.tags' | translate }}: </strong> |  | ||||||
|             <core-tag-list [tags]="tags"></core-tag-list> |  | ||||||
|         </div> |  | ||||||
| 
 | 
 | ||||||
|         <core-navigation-bar *ngIf="displayNavBar" [previous]="previousChapter?.id" [previousTitle]="previousNavBarTitle" |             <div class="ion-margin-top" *ngIf="tagsEnabled && tags?.length > 0"> | ||||||
|             [next]="nextChapter?.id" [nextTitle]="nextNavBarTitle" (action)="changeChapter($event)"></core-navigation-bar> |                 <strong>{{ 'core.tag.tags' | translate }}: </strong> | ||||||
|  |                 <core-tag-list [tags]="tags"></core-tag-list> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | |||||||
| @ -26,11 +26,11 @@ import { | |||||||
| import { CoreTag, CoreTagItem } from '@features/tag/services/tag'; | import { CoreTag, CoreTagItem } from '@features/tag/services/tag'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
| import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; | import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; | ||||||
| import { Translate } from '@singletons'; |  | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| import { CoreCourse } from '@features/course/services/course'; | import { CoreCourse } from '@features/course/services/course'; | ||||||
| import { AddonModBookTocComponent } from '../toc/toc'; | import { AddonModBookTocComponent } from '../toc/toc'; | ||||||
| import { CoreConstants } from '@/core/constants'; | import { CoreConstants } from '@/core/constants'; | ||||||
|  | import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Component that displays a book. |  * Component that displays a book. | ||||||
| @ -45,19 +45,16 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp | |||||||
| 
 | 
 | ||||||
|     component = AddonModBookProvider.COMPONENT; |     component = AddonModBookProvider.COMPONENT; | ||||||
|     chapterContent?: string; |     chapterContent?: string; | ||||||
|     previousChapter?: AddonModBookTocChapter; |  | ||||||
|     nextChapter?: AddonModBookTocChapter; |  | ||||||
|     tagsEnabled = false; |     tagsEnabled = false; | ||||||
|     displayNavBar = true; |  | ||||||
|     previousNavBarTitle?: string; |  | ||||||
|     nextNavBarTitle?: string; |  | ||||||
|     warning = ''; |     warning = ''; | ||||||
|     tags?: CoreTagItem[]; |     tags?: CoreTagItem[]; | ||||||
|  |     displayNavBar = true; | ||||||
|  |     navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = []; | ||||||
|  |     displayTitlesInNavBar = false; | ||||||
| 
 | 
 | ||||||
|     protected chapters: AddonModBookTocChapter[] = []; |     protected chapters: AddonModBookTocChapter[] = []; | ||||||
|     protected currentChapter?: number; |     protected currentChapter?: number; | ||||||
|     protected book?: AddonModBookBookWSData; |     protected book?: AddonModBookBookWSData; | ||||||
|     protected displayTitlesInNavBar = false; |  | ||||||
|     protected contentsMap: AddonModBookContentsMap = {}; |     protected contentsMap: AddonModBookContentsMap = {}; | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
| @ -148,14 +145,18 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (typeof this.currentChapter == 'undefined') { |             if (this.currentChapter === undefined) { | ||||||
|                 // Load the first chapter.
 |                 // Load the first chapter.
 | ||||||
|                 this.currentChapter = AddonModBook.getFirstChapter(this.chapters); |                 this.currentChapter = AddonModBook.getFirstChapter(this.chapters); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             if (this.currentChapter === undefined) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             // Show chapter.
 |             // Show chapter.
 | ||||||
|             try { |             try { | ||||||
|                 await this.loadChapter(this.currentChapter!, refresh); |                 await this.loadChapter(this.currentChapter, refresh); | ||||||
| 
 | 
 | ||||||
|                 this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; |                 this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; | ||||||
|             } catch { |             } catch { | ||||||
| @ -199,15 +200,10 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp | |||||||
|             this.tags = this.tagsEnabled ? this.contentsMap[this.currentChapter].tags : []; |             this.tags = this.tagsEnabled ? this.contentsMap[this.currentChapter].tags : []; | ||||||
| 
 | 
 | ||||||
|             this.chapterContent = content; |             this.chapterContent = content; | ||||||
|             this.previousChapter = AddonModBook.getPreviousChapter(this.chapters, chapterId); |  | ||||||
|             this.nextChapter = AddonModBook.getNextChapter(this.chapters, chapterId); |  | ||||||
| 
 | 
 | ||||||
|             this.previousNavBarTitle = this.previousChapter && this.displayTitlesInNavBar |             if (this.displayNavBar) { | ||||||
|                 ? Translate.instant('addon.mod_book.navprevtitle', { $a: this.previousChapter.title }) |                 this.navigationItems = this.getNavigationItems(chapterId); | ||||||
|                 : ''; |             } | ||||||
|             this.nextNavBarTitle = this.nextChapter && this.displayTitlesInNavBar |  | ||||||
|                 ? Translate.instant('addon.mod_book.navnexttitle', { $a: this.nextChapter.title }) |  | ||||||
|                 : ''; |  | ||||||
| 
 | 
 | ||||||
|             // Chapter loaded, log view. We don't return the promise because we don't want to block the user for this.
 |             // 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( |             await CoreUtils.ignoreErrors(AddonModBook.logView( | ||||||
| @ -216,8 +212,11 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp | |||||||
|                 this.module.name, |                 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.
 |             // 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); |                 CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); | ||||||
|             } |             } | ||||||
|         } catch (error) { |         } 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<AddonModBookTocChapter>[] { | ||||||
|  |         return this.chapters.map((chapter) => ({ | ||||||
|  |             item: chapter, | ||||||
|  |             title: chapter.title, | ||||||
|  |             current: chapter.id == chapterId, | ||||||
|  |             enabled: true, | ||||||
|  |         })); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -216,36 +216,6 @@ export class AddonModBookProvider { | |||||||
|         return chapters[0].id; |         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. |      * Get the book toc as an array. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -27,8 +27,6 @@ import { AddonModChatUsersModalComponent } from './users-modal/users-modal'; | |||||||
|         CoreSharedModule, |         CoreSharedModule, | ||||||
|         CoreCourseComponentsModule, |         CoreCourseComponentsModule, | ||||||
|     ], |     ], | ||||||
|     providers: [ |  | ||||||
|     ], |  | ||||||
|     exports: [ |     exports: [ | ||||||
|         AddonModChatIndexComponent, |         AddonModChatIndexComponent, | ||||||
|         AddonModChatUsersModalComponent, |         AddonModChatUsersModalComponent, | ||||||
|  | |||||||
| @ -47,3 +47,6 @@ | |||||||
|         </ion-button> |         </ion-button> | ||||||
|     </ng-container> |     </ng-container> | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | |||||||
| @ -155,6 +155,9 @@ | |||||||
|     </ion-card> |     </ion-card> | ||||||
| </core-loading> | </core-loading> | ||||||
| 
 | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | 
 | ||||||
| <!-- Template to render a choice option label. --> | <!-- Template to render a choice option label. --> | ||||||
| <ng-template #optionLabelTemplate let-option="option"> | <ng-template #optionLabelTemplate let-option="option"> | ||||||
|     <p> |     <p> | ||||||
|  | |||||||
| @ -138,6 +138,9 @@ | |||||||
| 
 | 
 | ||||||
| </core-loading> | </core-loading> | ||||||
| 
 | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | 
 | ||||||
| <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAdd"> | <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAdd"> | ||||||
|     <ion-fab-button (click)="gotoAddEntries()" [attr.aria-label]="'addon.mod_data.addentries' | translate"> |     <ion-fab-button (click)="gotoAddEntries()" [attr.aria-label]="'addon.mod_data.addentries' | translate"> | ||||||
|         <ion-icon name="fas-plus" aria-hidden="true"></ion-icon> |         <ion-icon name="fas-plus" aria-hidden="true"></ion-icon> | ||||||
|  | |||||||
| @ -55,6 +55,9 @@ | |||||||
|     </core-tabs> |     </core-tabs> | ||||||
| </core-loading> | </core-loading> | ||||||
| 
 | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | 
 | ||||||
| <ng-template #basicInfo> | <ng-template #basicInfo> | ||||||
|     <ion-list *ngIf="access && access.canviewanalysis && !access.isempty"> |     <ion-list *ngIf="access && access.canviewanalysis && !access.isempty"> | ||||||
|         <ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)"> |         <ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)"> | ||||||
|  | |||||||
| @ -48,3 +48,6 @@ | |||||||
|         [message]=" 'addon.mod_folder.emptyfilelist' | translate"></core-empty-box> |         [message]=" 'addon.mod_folder.emptyfilelist' | translate"></core-empty-box> | ||||||
| 
 | 
 | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | |||||||
| @ -140,6 +140,9 @@ | |||||||
|         </ng-container> |         </ng-container> | ||||||
|     </core-loading> |     </core-loading> | ||||||
| 
 | 
 | ||||||
|  |     <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  |     </core-course-module-navigation> | ||||||
|  | 
 | ||||||
|     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="forum && canAddDiscussion"> |     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="forum && canAddDiscussion"> | ||||||
|         <ion-fab-button (click)="openNewDiscussion()" [attr.aria-label]="addDiscussionText"> |         <ion-fab-button (click)="openNewDiscussion()" [attr.aria-label]="addDiscussionText"> | ||||||
|             <ion-icon name="fas-plus" aria-hidden="true"></ion-icon> |             <ion-icon name="fas-plus" aria-hidden="true"></ion-icon> | ||||||
|  | |||||||
| @ -96,6 +96,9 @@ | |||||||
|         </core-infinite-loading> |         </core-infinite-loading> | ||||||
|     </core-loading> |     </core-loading> | ||||||
| 
 | 
 | ||||||
|  |     <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  |     </core-course-module-navigation> | ||||||
|  | 
 | ||||||
|     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAdd"> |     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAdd"> | ||||||
|         <ion-fab-button (click)="openNewEntry()" [attr.aria-label]="'addon.mod_glossary.addentry' | translate"> |         <ion-fab-button (click)="openNewEntry()" [attr.aria-label]="'addon.mod_glossary.addentry' | translate"> | ||||||
|             <ion-icon name="fas-plus" aria-hidden="true"></ion-icon> |             <ion-icon name="fas-plus" aria-hidden="true"></ion-icon> | ||||||
|  | |||||||
| @ -84,3 +84,6 @@ | |||||||
|         [trackComponent]="trackComponent" [contextId]="h5pActivity?.context"> |         [trackComponent]="trackComponent" [contextId]="h5pActivity?.context"> | ||||||
|     </core-h5p-iframe> |     </core-h5p-iframe> | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | |||||||
| @ -45,7 +45,6 @@ import { | |||||||
| } from '../../services/h5pactivity-sync'; | } from '../../services/h5pactivity-sync'; | ||||||
| import { CoreFileHelper } from '@services/file-helper'; | import { CoreFileHelper } from '@services/file-helper'; | ||||||
| import { AddonModH5PActivityModuleHandlerService } from '../../services/handlers/module'; | import { AddonModH5PActivityModuleHandlerService } from '../../services/handlers/module'; | ||||||
| import { CoreMainMenuPage } from '@features/mainmenu/pages/menu/menu'; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Component that displays an H5P activity entry page. |  * Component that displays an H5P activity entry page. | ||||||
| @ -87,7 +86,6 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv | |||||||
|     protected messageListenerFunction: (event: MessageEvent) => Promise<void>; |     protected messageListenerFunction: (event: MessageEvent) => Promise<void>; | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         protected mainMenuPage: CoreMainMenuPage, |  | ||||||
|         protected content?: IonContent, |         protected content?: IonContent, | ||||||
|         @Optional() courseContentsPage?: CoreCourseContentsPage, |         @Optional() courseContentsPage?: CoreCourseContentsPage, | ||||||
|     ) { |     ) { | ||||||
|  | |||||||
| @ -40,10 +40,11 @@ | |||||||
|     </ion-card> |     </ion-card> | ||||||
| 
 | 
 | ||||||
|     <div class="addon-mod-imscp-container"> |     <div class="addon-mod-imscp-container"> | ||||||
|         <core-navigation-bar [previous]="previousItem" [next]="nextItem" (action)="loadItem($event)" [info]="description" |         <core-navigation-bar [items]="navigationItems" (action)="loadItem($event)"> | ||||||
|             [title]="'core.description' | translate" [component]="component" [componentId]="componentId" contextLevel="module" |  | ||||||
|             [contextInstanceId]="module.id" [courseId]="courseId"> |  | ||||||
|         </core-navigation-bar> |         </core-navigation-bar> | ||||||
|         <core-iframe [src]="src"></core-iframe> |         <core-iframe [src]="src"></core-iframe> | ||||||
|     </div> |     </div> | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| .addon-mod-imscp-container { | .addon-mod-imscp-container { | ||||||
|     position: absolute; |  | ||||||
|     width: 100%; |  | ||||||
|     height: 100%; |     height: 100%; | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ | |||||||
| 
 | 
 | ||||||
| import { Component, OnInit, Optional } from '@angular/core'; | import { Component, OnInit, Optional } from '@angular/core'; | ||||||
| import { CoreSilentError } from '@classes/errors/silenterror'; | import { CoreSilentError } from '@classes/errors/silenterror'; | ||||||
|  | import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar'; | ||||||
| import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component'; | import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component'; | ||||||
| import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; | import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; | ||||||
| import { CoreCourse } from '@features/course/services/course'; | import { CoreCourse } from '@features/course/services/course'; | ||||||
| @ -32,22 +33,19 @@ import { AddonModImscpTocComponent } from '../toc/toc'; | |||||||
| export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { | export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { | ||||||
| 
 | 
 | ||||||
|     component = AddonModImscpProvider.COMPONENT; |     component = AddonModImscpProvider.COMPONENT; | ||||||
| 
 |  | ||||||
|     items: AddonModImscpTocItem[] = []; |  | ||||||
|     currentItem?: string; |  | ||||||
|     src = ''; |     src = ''; | ||||||
|     warning = ''; |     warning = ''; | ||||||
|  |     navigationItems: CoreNavigationBarItem<AddonModImscpTocItem>[] = []; | ||||||
| 
 | 
 | ||||||
|     // Initialize empty previous/next to prevent showing arrows for an instant before they're hidden.
 |     protected items: AddonModImscpTocItem[] = []; | ||||||
|     previousItem = ''; |     protected currentHref?: string; | ||||||
|     nextItem = ''; |  | ||||||
| 
 | 
 | ||||||
|     constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) { |     constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) { | ||||||
|         super('AddonModImscpIndexComponent', courseContentsPage); |         super('AddonModImscpIndexComponent', courseContentsPage); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Component being initialized. |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     async ngOnInit(): Promise<void> { |     async ngOnInit(): Promise<void> { | ||||||
|         super.ngOnInit(); |         super.ngOnInit(); | ||||||
| @ -90,19 +88,19 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom | |||||||
| 
 | 
 | ||||||
|             this.items = AddonModImscp.createItemList(contents); |             this.items = AddonModImscp.createItemList(contents); | ||||||
| 
 | 
 | ||||||
|             if (this.items.length && typeof this.currentItem == 'undefined') { |             if (this.items.length && this.currentHref === undefined) { | ||||||
|                 this.currentItem = this.items[0].href; |                 this.currentHref = this.items[0].href; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             try { |             try { | ||||||
|                 await this.loadItem(this.currentItem); |                 await this.loadItemHref(this.currentHref); | ||||||
|             } catch (error) { |             } catch (error) { | ||||||
|                 CoreDomUtils.showErrorModalDefault(error, 'addon.mod_imscp.deploymenterror', true); |                 CoreDomUtils.showErrorModalDefault(error, 'addon.mod_imscp.deploymenterror', true); | ||||||
| 
 | 
 | ||||||
|                 throw new CoreSilentError(error); |                 throw new CoreSilentError(error); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             this.warning = downloadResult!.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult!.error!) : ''; |             this.warning = downloadResult.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; | ||||||
| 
 | 
 | ||||||
|         } finally { |         } finally { | ||||||
|             // Pass false because downloadResourceIfNeeded already invalidates and refresh data if refresh=true.
 |             // Pass false because downloadResourceIfNeeded already invalidates and refresh data if refresh=true.
 | ||||||
| @ -113,14 +111,18 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom | |||||||
|     /** |     /** | ||||||
|      * Loads an item. |      * Loads an item. | ||||||
|      * |      * | ||||||
|      * @param itemId Item ID. |      * @param itemHref Item Href. | ||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     async loadItem(itemId?: string): Promise<void> { |     async loadItemHref(itemHref?: string): Promise<void> { | ||||||
|         const src = await AddonModImscp.getIframeSrc(this.module, itemId); |         const src = await AddonModImscp.getIframeSrc(this.module, itemHref); | ||||||
|         this.currentItem = itemId; |         this.currentHref = itemHref; | ||||||
|         this.previousItem = itemId ? AddonModImscp.getPreviousItem(this.items, itemId) : ''; | 
 | ||||||
|         this.nextItem = itemId ? AddonModImscp.getNextItem(this.items, itemId) : ''; |         this.navigationItems = this.items.map((item) => ({ | ||||||
|  |             item: item, | ||||||
|  |             current: item.href == this.currentHref, | ||||||
|  |             enabled: !!item.href, | ||||||
|  |         })); | ||||||
| 
 | 
 | ||||||
|         if (this.src && src == this.src) { |         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.
 |             // 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. |      * Show the TOC. | ||||||
|      */ |      */ | ||||||
| @ -142,12 +153,12 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom | |||||||
|             component: AddonModImscpTocComponent, |             component: AddonModImscpTocComponent, | ||||||
|             componentProps: { |             componentProps: { | ||||||
|                 items: this.items, |                 items: this.items, | ||||||
|                 selected: this.currentItem, |                 selected: this.currentHref, | ||||||
|             }, |             }, | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         if (modalData) { |         if (modalData) { | ||||||
|             this.loadItem(modalData); |             this.loadItemHref(modalData); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -70,63 +70,6 @@ export class AddonModImscpProvider { | |||||||
|         return items; |         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. |      * Check if we should ommit the file download. | ||||||
|      * |      * | ||||||
| @ -242,7 +185,7 @@ export class AddonModImscpProvider { | |||||||
|         const siteId = CoreSites.getCurrentSiteId(); |         const siteId = CoreSites.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             const dirPath = await CoreFilepool.getPackageDirUrlByUrl(siteId, module!.url!); |             const dirPath = await CoreFilepool.getPackageDirUrlByUrl(siteId, module.url!); | ||||||
| 
 | 
 | ||||||
|             return CoreTextUtils.concatenatePaths(dirPath, itemHref); |             return CoreTextUtils.concatenatePaths(dirPath, itemHref); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|  | |||||||
| @ -39,6 +39,7 @@ export class AddonModLabelModuleHandlerService extends CoreModuleHandlerBase imp | |||||||
|         [CoreConstants.FEATURE_GRADE_OUTCOMES]: false, |         [CoreConstants.FEATURE_GRADE_OUTCOMES]: false, | ||||||
|         [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, |         [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, | ||||||
|         [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, |         [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, | ||||||
|  |         [CoreConstants.FEATURE_NO_VIEW_LINK]: true, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -297,3 +297,6 @@ | |||||||
|         </core-tab> |         </core-tab> | ||||||
|     </core-tabs> |     </core-tabs> | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | |||||||
| @ -32,3 +32,6 @@ | |||||||
|         </ion-button> |         </ion-button> | ||||||
|     </div> |     </div> | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | |||||||
| @ -48,3 +48,6 @@ | |||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | |||||||
| @ -226,3 +226,6 @@ | |||||||
|         </ion-list> |         </ion-list> | ||||||
|     </ion-card> |     </ion-card> | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ | |||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
| 
 | 
 | ||||||
| <!-- Content. --> | <!-- Content. --> | ||||||
| <core-loading [hideUntil]="loaded" class="safe-area-padding core-loading-fullheight"> | <core-loading [hideUntil]="loaded" class="safe-area-padding"> | ||||||
| 
 | 
 | ||||||
|     <!-- Activity info. --> |     <!-- Activity info. --> | ||||||
|     <core-course-module-info [module]="module" [courseId]="courseId" (completionChanged)="onCompletionChange()" |     <core-course-module-info [module]="module" [courseId]="courseId" (completionChanged)="onCompletionChange()" | ||||||
| @ -59,5 +59,7 @@ | |||||||
|             {{ 'core.openwith' | translate }} |             {{ 'core.openwith' | translate }} | ||||||
|         </ion-button> |         </ion-button> | ||||||
|     </ng-container> |     </ng-container> | ||||||
| 
 |  | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | |||||||
| @ -236,3 +236,6 @@ | |||||||
|         </ion-card> |         </ion-card> | ||||||
|     </ng-container> |     </ng-container> | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | |||||||
| @ -20,8 +20,8 @@ | |||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <core-loading [hideUntil]="loaded" class="core-loading-fullheight"> |     <core-loading [hideUntil]="loaded"> | ||||||
|         <core-navigation-bar [previous]="previousSco" [next]="nextSco" (action)="loadSco($event)"></core-navigation-bar> |         <core-navigation-bar [items]="navigationItems" (action)="loadSco($event)"></core-navigation-bar> | ||||||
| 
 | 
 | ||||||
|         <core-iframe *ngIf="loaded && src" [src]="src" [iframeWidth]="scormWidth" [iframeHeight]="scormHeight" |         <core-iframe *ngIf="loaded && src" [src]="src" [iframeWidth]="scormWidth" [iframeHeight]="scormHeight" | ||||||
|             [showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="true"> |             [showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="true"> | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Component, OnInit, OnDestroy } from '@angular/core'; | import { Component, OnInit, OnDestroy } from '@angular/core'; | ||||||
|  | import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar'; | ||||||
| import { CoreMainMenuPage } from '@features/mainmenu/pages/menu/menu'; | import { CoreMainMenuPage } from '@features/mainmenu/pages/menu/menu'; | ||||||
| import { CoreNavigator } from '@services/navigator'; | import { CoreNavigator } from '@services/navigator'; | ||||||
| import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; | import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; | ||||||
| @ -50,8 +51,6 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { | |||||||
|     loadingToc = true; // Whether the TOC is being loaded.
 |     loadingToc = true; // Whether the TOC is being loaded.
 | ||||||
|     toc: AddonModScormTOCScoWithIcon[] = []; // List of SCOs.
 |     toc: AddonModScormTOCScoWithIcon[] = []; // List of SCOs.
 | ||||||
|     loaded = false; // Whether the data has been loaded.
 |     loaded = false; // Whether the data has been loaded.
 | ||||||
|     previousSco?: AddonModScormScoWithData; // Previous SCO.
 |  | ||||||
|     nextSco?: AddonModScormScoWithData; // Next SCO.
 |  | ||||||
|     src?: string; // Iframe src.
 |     src?: string; // Iframe src.
 | ||||||
|     errorMessage?: string; // Error message.
 |     errorMessage?: string; // Error message.
 | ||||||
|     accessInfo?: AddonModScormGetScormAccessInformationWSResponse; // Access information.
 |     accessInfo?: AddonModScormGetScormAccessInformationWSResponse; // Access information.
 | ||||||
| @ -60,6 +59,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { | |||||||
|     incomplete = false; // Whether last attempt is incomplete.
 |     incomplete = false; // Whether last attempt is incomplete.
 | ||||||
|     cmId!: number; // Course module ID.
 |     cmId!: number; // Course module ID.
 | ||||||
|     courseId!: number; // Course ID.
 |     courseId!: number; // Course ID.
 | ||||||
|  |     navigationItems: CoreNavigationBarItem<AddonModScormTOCScoWithIcon>[] = []; | ||||||
| 
 | 
 | ||||||
|     protected siteId!: string; |     protected siteId!: string; | ||||||
|     protected mode!: string; // Mode to play the SCORM.
 |     protected mode!: string; // Mode to play the SCORM.
 | ||||||
| @ -110,6 +110,8 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { | |||||||
|             await this.fetchData(); |             await this.fetchData(); | ||||||
| 
 | 
 | ||||||
|             if (!this.currentSco) { |             if (!this.currentSco) { | ||||||
|  |                 CoreNavigator.back(); | ||||||
|  | 
 | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @ -176,14 +178,20 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { | |||||||
|         }, this.siteId); |         }, this.siteId); | ||||||
| 
 | 
 | ||||||
|         this.launchNextObserver = CoreEvents.on(AddonModScormProvider.LAUNCH_NEXT_SCO_EVENT, (data) => { |         this.launchNextObserver = CoreEvents.on(AddonModScormProvider.LAUNCH_NEXT_SCO_EVENT, (data) => { | ||||||
|             if (data.scormId === this.scorm.id && this.nextSco) { |             if (data.scormId === this.scorm.id && this.currentSco) { | ||||||
|                 this.loadSco(this.nextSco); |                 const nextSco = AddonModScormHelper.getNextScoFromToc(this.toc, this.currentSco.id); | ||||||
|  |                 if (nextSco) { | ||||||
|  |                     this.loadSco(nextSco); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         }, this.siteId); |         }, this.siteId); | ||||||
| 
 | 
 | ||||||
|         this.launchPrevObserver = CoreEvents.on(AddonModScormProvider.LAUNCH_PREV_SCO_EVENT, (data) => { |         this.launchPrevObserver = CoreEvents.on(AddonModScormProvider.LAUNCH_PREV_SCO_EVENT, (data) => { | ||||||
|             if (data.scormId === this.scorm.id && this.previousSco) { |             if (data.scormId === this.scorm.id && this.currentSco) { | ||||||
|                 this.loadSco(this.previousSco); |                 const previousSco = AddonModScormHelper.getPreviousScoFromToc(this.toc, this.currentSco.id); | ||||||
|  |                 if (previousSco) { | ||||||
|  |                     this.loadSco(previousSco); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         }, this.siteId); |         }, this.siteId); | ||||||
| 
 | 
 | ||||||
| @ -211,9 +219,16 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { | |||||||
|      * |      * | ||||||
|      * @param scoId Current SCO ID. |      * @param scoId Current SCO ID. | ||||||
|      */ |      */ | ||||||
|     protected calculateNextAndPreviousSco(scoId: number): void { |     protected calculateNavigationItems(scoId: number): void { | ||||||
|         this.previousSco = AddonModScormHelper.getPreviousScoFromToc(this.toc, scoId); |         this.navigationItems = this.toc | ||||||
|         this.nextSco = AddonModScormHelper.getNextScoFromToc(this.toc, scoId); |             .filter((item) => item.isvisible) | ||||||
|  |             .map<CoreNavigationBarItem<AddonModScormTOCScoWithIcon>>((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.currentSco = sco; | ||||||
|         this.title = sco.title || this.scorm.name; // Try to use SCO title.
 |         this.title = sco.title || this.scorm.name; // Try to use SCO title.
 | ||||||
| 
 | 
 | ||||||
|         this.calculateNextAndPreviousSco(sco.id); |         this.calculateNavigationItems(sco.id); | ||||||
| 
 | 
 | ||||||
|         // Load the SCO source.
 |         // Load the SCO source.
 | ||||||
|         this.loadScoSrc(sco); |         this.loadScoSrc(sco); | ||||||
| @ -540,7 +555,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Component being destroyed. |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     ngOnDestroy(): void { |     ngOnDestroy(): void { | ||||||
|         // Empty src when leaving the state so unload event is triggered in the iframe.
 |         // Empty src when leaving the state so unload event is triggered in the iframe.
 | ||||||
|  | |||||||
| @ -280,18 +280,13 @@ export class AddonModScormHelperProvider { | |||||||
|      * @return Next SCO. |      * @return Next SCO. | ||||||
|      */ |      */ | ||||||
|     getNextScoFromToc(toc: AddonModScormScoWithData[], scoId: number): AddonModScormScoWithData | undefined { |     getNextScoFromToc(toc: AddonModScormScoWithData[], scoId: number): AddonModScormScoWithData | undefined { | ||||||
|         for (let i = 0; i < toc.length; i++) { |         const currentTocIndex = toc.findIndex((item) => item.id == scoId); | ||||||
|             if (toc[i].id != scoId) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             // We found the current SCO. Now search the next visible SCO with fulfilled prerequisites.
 |         // We found the current SCO. Now search the next visible SCO with fulfilled prerequisites.
 | ||||||
|             for (let j = i + 1; j < toc.length; j++) { |         for (let j = currentTocIndex + 1; j < toc.length; j++) { | ||||||
|                 if (toc[j].isvisible && toc[j].prereq && toc[j].launch) { |             if (toc[j].isvisible && toc[j].prereq && toc[j].launch) { | ||||||
|                     return toc[j]; |                 return toc[j]; | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|             break; |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -303,18 +298,13 @@ export class AddonModScormHelperProvider { | |||||||
|      * @return Previous SCO. |      * @return Previous SCO. | ||||||
|      */ |      */ | ||||||
|     getPreviousScoFromToc(toc: AddonModScormScoWithData[], scoId: number): AddonModScormScoWithData | undefined { |     getPreviousScoFromToc(toc: AddonModScormScoWithData[], scoId: number): AddonModScormScoWithData | undefined { | ||||||
|         for (let i = 0; i < toc.length; i++) { |         const currentTocIndex = toc.findIndex((item) => item.id == scoId); | ||||||
|             if (toc[i].id != scoId) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             // We found the current SCO. Now let's search the previous visible SCO with fulfilled prerequisites.
 |         // We found the current SCO. Now let's search the previous visible SCO with fulfilled prerequisites.
 | ||||||
|             for (let j = i - 1; j >= 0; j--) { |         for (let j = currentTocIndex - 1; j >= 0; j--) { | ||||||
|                 if (toc[j].isvisible && toc[j].prereq && toc[j].launch) { |             if (toc[j].isvisible && toc[j].prereq && toc[j].launch) { | ||||||
|                     return toc[j]; |                 return toc[j]; | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|             break; |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -147,3 +147,6 @@ | |||||||
|     </form> |     </form> | ||||||
| 
 | 
 | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ | |||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
| 
 | 
 | ||||||
| <!-- Content. --> | <!-- Content. --> | ||||||
| <core-loading [hideUntil]="loaded" class="core-loading-fullheight"> | <core-loading [hideUntil]="loaded"> | ||||||
| 
 | 
 | ||||||
|     <!-- Activity info. --> |     <!-- Activity info. --> | ||||||
|     <core-course-module-info [module]="module" (completionChanged)="onCompletionChange()" [description]="displayDescription && description" |     <core-course-module-info [module]="module" (completionChanged)="onCompletionChange()" [description]="displayDescription && description" | ||||||
| @ -52,3 +52,6 @@ | |||||||
|         </ion-item> |         </ion-item> | ||||||
|     </ion-list> |     </ion-list> | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ import { CoreModuleHandlerBase } from '@features/course/classes/module-base-hand | |||||||
| import { CoreCourse } from '@features/course/services/course'; | import { CoreCourse } from '@features/course/services/course'; | ||||||
| import { CoreCourseModule } from '@features/course/services/course-helper'; | import { CoreCourseModule } from '@features/course/services/course-helper'; | ||||||
| import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; | 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 { CoreDomUtils } from '@services/utils/dom'; | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| import { makeSingleton } from '@singletons'; | import { makeSingleton } from '@singletons'; | ||||||
| @ -90,12 +90,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple | |||||||
|                     if (shouldOpen) { |                     if (shouldOpen) { | ||||||
|                         openUrl(module, courseId); |                         openUrl(module, courseId); | ||||||
|                     } else { |                     } else { | ||||||
|                         options = options || {}; |                         this.openActivityPage(module, courseId, options); | ||||||
|                         options.params = options.params || {}; |  | ||||||
|                         Object.assign(options.params, { module }); |  | ||||||
|                         const routeParams = '/' + courseId + '/' + module.id; |  | ||||||
| 
 |  | ||||||
|                         CoreNavigator.navigateToSitePath(AddonModUrlModuleHandlerService.PAGE_NAME + routeParams, options); |  | ||||||
|                     } |                     } | ||||||
|                 } finally { |                 } finally { | ||||||
|                     modal.dismiss(); |                     modal.dismiss(); | ||||||
|  | |||||||
| @ -89,6 +89,9 @@ | |||||||
|     </div> |     </div> | ||||||
| </core-loading> | </core-loading> | ||||||
| 
 | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | 
 | ||||||
| <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canEdit"> | <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canEdit"> | ||||||
|     <ion-fab-button (click)="goToNewPage()" [attr.aria-label]="'addon.mod_wiki.createpage' | translate"> |     <ion-fab-button (click)="goToNewPage()" [attr.aria-label]="'addon.mod_wiki.createpage' | translate"> | ||||||
|         <ion-icon name="fas-plus" aria-hidden="true"></ion-icon> |         <ion-icon name="fas-plus" aria-hidden="true"></ion-icon> | ||||||
|  | |||||||
| @ -253,3 +253,6 @@ | |||||||
|         </ion-card> |         </ion-card> | ||||||
|     </div> |     </div> | ||||||
| </core-loading> | </core-loading> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|  | </core-course-module-navigation> | ||||||
|  | |||||||
| @ -44,6 +44,7 @@ | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .core-loading-content { |     .core-loading-content { | ||||||
|  |         display: contents; | ||||||
|         @include core-transition(opacity, 200ms); |         @include core-transition(opacity, 200ms); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -51,10 +52,6 @@ | |||||||
|         @include margin(10px, 0, 0, 0); |         @include margin(10px, 0, 0, 0); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     &.core-loading-fullheight .core-loading-content { |  | ||||||
|         height: 100%; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     &.core-loading-loaded { |     &.core-loading-loaded { | ||||||
|         position: unset; |         position: unset; | ||||||
|         display: contents; |         display: contents; | ||||||
|  | |||||||
| @ -1,27 +1,27 @@ | |||||||
| <ion-grid class="ion-no-padding ion-padding-bottom" *ngIf="previous || info || next"> | <ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding" | ||||||
|     <ion-row> |     *ngIf="previousIndex >= 0 || nextIndex >= 0 || items.length > 1"> | ||||||
|         <ion-col class="ion-text-start ion-padding-end" [size]="info ? 4 : 6"> |     <ion-col class="ion-text-start ion-padding-end" [size]="showTitles ? 4 : 3"> | ||||||
|             <ion-button *ngIf="previous" class="core-navigation-bar-arrow" fill="outline" |         <ion-button *ngIf="previousIndex >=0" class="core-navigation-bar-arrow" fill="clear" [attr.aria-label]="previousTitle" | ||||||
|                 [attr.aria-label]="previousTitle || ('core.previous' | translate)" (click)="action?.emit(previous)"> |             (click)="navigate(previousIndex)"> | ||||||
|                 <ion-icon name="fas-arrow-left" [slot]="previousTitle ? 'start' : 'icon-only'" aria-hidden="true"></ion-icon> |             <ion-icon name="fas-arrow-left" [slot]="showTitles ? 'start' : 'icon-only'" aria-hidden="true"></ion-icon> | ||||||
|                 <core-format-text *ngIf="previousTitle" [text]="previousTitle" [component]="component" [componentId]="componentId" |             <core-format-text *ngIf="showTitles" [text]="previousTitle" [component]="component" [componentId]="componentId" | ||||||
|                     [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" aria-hidden="true"> |                 [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" aria-hidden="true"> | ||||||
|                 </core-format-text> |             </core-format-text> | ||||||
|             </ion-button> |         </ion-button> | ||||||
|         </ion-col> |     </ion-col> | ||||||
|         <ion-col class="ion-text-center" size="4" *ngIf="info"> |     <ion-col class="ion-text-center" [size]="showTitles ? 4 : 6"> | ||||||
|             <ion-button fill="clear" (click)="showInfo()" [attr.aria-label]="title"> |         <ion-range min="0" [max]="items.length -1" debounce="500" snaps="true" (ionChange)="navigateOnRange($event.target)" | ||||||
|                 <ion-icon slot="icon-only" name="fas-info-circle" aria-hidden="true"></ion-icon> |             [value]="currentIndex"> | ||||||
|             </ion-button> |             <p slot="end">{{currentIndex + 1}} / {{items.length}}</p> | ||||||
|         </ion-col> |         </ion-range> | ||||||
|         <ion-col class="ion-text-end ion-padding-start" [size]="info ? 4 : 6"> |     </ion-col> | ||||||
|             <ion-button *ngIf="next" class="core-navigation-bar-arrow" [attr.aria-label]="nextTitle || ('core.next' | translate)" |     <ion-col class="ion-text-end ion-padding-start" [size]="showTitles ? 4 : 3"> | ||||||
|                 (click)="action?.emit(next)"> |         <ion-button fill="clear" *ngIf="nextIndex >= 0" class="core-navigation-bar-arrow" [attr.aria-label]="nextTitle" | ||||||
|                 <core-format-text *ngIf="nextTitle" [text]="nextTitle" [component]="component" [componentId]="componentId" |             (click)="navigate(nextIndex)"> | ||||||
|                     [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" aria-hidden="true"> |             <core-format-text *ngIf="showTitles" [text]="nextTitle" [component]="component" [componentId]="componentId" | ||||||
|                 </core-format-text> |                 [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" aria-hidden="true"> | ||||||
|                 <ion-icon name="fas-arrow-right" [slot]="nextTitle ? 'end' : 'icon-only'" aria-hidden="true"></ion-icon> |             </core-format-text> | ||||||
|             </ion-button> |             <ion-icon name="fas-arrow-right" [slot]="showTitles ? 'end' : 'icon-only'" aria-hidden="true"></ion-icon> | ||||||
|         </ion-col> |         </ion-button> | ||||||
|     </ion-row> |     </ion-col> | ||||||
| </ion-grid> | </ion-row> | ||||||
|  | |||||||
| @ -1,7 +1,15 @@ | |||||||
| .core-navigation-bar-arrow { | :host { | ||||||
|     text-transform: none; |     --background: var(--core-course-module-navigation-background); | ||||||
|     max-width: 100%; | 
 | ||||||
|     ion-icon { |     width: 100%; | ||||||
|         flex-shrink: 0; |     background-color: var(--background); | ||||||
|  |     display: block; | ||||||
|  | 
 | ||||||
|  |     .core-navigation-bar-arrow { | ||||||
|  |         text-transform: none; | ||||||
|  |         max-width: 100%; | ||||||
|  |         ion-icon { | ||||||
|  |             flex-shrink: 0; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -12,48 +12,103 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Component, EventEmitter, Input, Output } from '@angular/core'; | import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange } from '@angular/core'; | ||||||
| import { CoreTextUtils } from '@services/utils/text'; | 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. |  * 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: |  * Example usage: | ||||||
|  * <core-navigation-bar [previous]="prevItem" [next]="nextItem" (action)="goTo($event)"></core-navigation-bar> |  * <core-navigation-bar [items]="items" (action)="goTo($event)"></core-navigation-bar> | ||||||
|  */ |  */ | ||||||
| @Component({ | @Component({ | ||||||
|     selector: 'core-navigation-bar', |     selector: 'core-navigation-bar', | ||||||
|     templateUrl: 'core-navigation-bar.html', |     templateUrl: 'core-navigation-bar.html', | ||||||
|     styleUrls: ['navigation-bar.scss'], |     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() items: CoreNavigationBarItem[] = []; // List of items.
 | ||||||
|     @Input() previousTitle?: string; // Previous item title. If not defined, only the arrow will be shown.
 |     @Input() showTitles = false; // Display titles on buttons.
 | ||||||
|     @Input() next?: unknown; // Next item. If not defined, the next arrow won't be shown.
 |     @Input() previousTranslate = 'core.previous'; // Previous translatable text, can admit $a variable.
 | ||||||
|     @Input() nextTitle?: string; // Next item title. If not defined, only the arrow will be shown.
 |     @Input() nextTranslate = 'core.next'; // Next translatable text, can admit $a variable.
 | ||||||
|     @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() component?: string; // Component the bar belongs to.
 |     @Input() component?: string; // Component the bar belongs to.
 | ||||||
|     @Input() componentId?: number; // Component ID.
 |     @Input() componentId?: number; // Component ID.
 | ||||||
|     @Input() contextLevel?: string; // The context level.
 |     @Input() contextLevel?: string; // The context level.
 | ||||||
|     @Input() contextInstanceId?: number; // The instance ID related to the context.
 |     @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.
 |     @Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters.
 | ||||||
|     @Output() action?: EventEmitter<unknown> = |  | ||||||
|         new EventEmitter<unknown>(); // Function to call when arrow is clicked. Will receive as a param the item to load.
 |  | ||||||
| 
 | 
 | ||||||
|     showInfo(): void { |     previousTitle?: string; // Previous item title.
 | ||||||
|         CoreTextUtils.viewText(this.title, this.info, { |     nextTitle?: string; // Next item title.
 | ||||||
|             component: this.component, |     previousIndex = -1; // Previous item index. If -1, the previous arrow won't be shown.
 | ||||||
|             componentId: this.componentId, |     nextIndex = -1; // Next item index. If -1, the next arrow won't be shown.
 | ||||||
|             filter: true, |     currentIndex = 0; | ||||||
|             contextLevel: this.contextLevel, | 
 | ||||||
|             instanceId: this.contextInstanceId, |     // Function to call when arrow is clicked. Will receive as a param the item to load.
 | ||||||
|             courseId: this.courseId, |     @Output() action: EventEmitter<unknown> = new EventEmitter<unknown>(); | ||||||
|         }); | 
 | ||||||
|  |     /** | ||||||
|  |      * @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<T = unknown> = { | ||||||
|  |     item: T; | ||||||
|  |     title?: string; | ||||||
|  |     current: boolean; | ||||||
|  |     enabled: boolean; | ||||||
|  | }; | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ | |||||||
|     .menu, |     .menu, | ||||||
|     .content-outlet { |     .content-outlet { | ||||||
|         top: var(--offset-top); |         top: var(--offset-top); | ||||||
|  |         height: calc(100% - var(--offset-top)); | ||||||
|         right: 0; |         right: 0; | ||||||
|         bottom: 0; |         bottom: 0; | ||||||
|         left: 0; |         left: 0; | ||||||
|  | |||||||
| @ -48,8 +48,12 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | |||||||
|         this.header = el.nativeElement; |         this.header = el.nativeElement; | ||||||
| 
 | 
 | ||||||
|         this.loadingObserver = CoreEvents.on(CoreEvents.CORE_LOADING_CHANGED, async (data) => { |         this.loadingObserver = CoreEvents.on(CoreEvents.CORE_LOADING_CHANGED, async (data) => { | ||||||
|  |             if (!data.loaded) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             const loadingId = await this.getLoadingId(); |             const loadingId = await this.getLoadingId(); | ||||||
|             if (loadingId && data.loaded && data.uniqueId == loadingId) { |             if (loadingId && data.uniqueId == loadingId) { | ||||||
|                 // Remove event when loading is done.
 |                 // Remove event when loading is done.
 | ||||||
|                 this.loadingObserver.off(); |                 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; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -12,8 +12,8 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Directive, ElementRef, OnDestroy } from '@angular/core'; | import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core'; | ||||||
| import { IonContent } from '@ionic/angular'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Directive to move ion-fab components as direct children of the nearest ion-content. |  * Directive to move ion-fab components as direct children of the nearest ion-content. | ||||||
| @ -25,47 +25,50 @@ import { IonContent } from '@ionic/angular'; | |||||||
| @Directive({ | @Directive({ | ||||||
|     selector: 'ion-fab[core-fab]', |     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 element: HTMLElement; | ||||||
|  |     protected content?: HTMLIonContentElement | null; | ||||||
|  |     protected initialPaddingBottom = 0; | ||||||
| 
 | 
 | ||||||
|     constructor(el: ElementRef, protected content: IonContent) { |     constructor(el: ElementRef) { | ||||||
|         this.element = el.nativeElement; |         this.element = el.nativeElement; | ||||||
|         this.asyncInit(); |         this.element.setAttribute('slot', 'fixed'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Initialize Component. |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     async asyncInit(): Promise<void> { |     async ngOnInit(retries = 3): Promise<void> { | ||||||
|         if (this.content) { |         this.content = this.element.closest('ion-content'); | ||||||
|             this.scrollElement = await this.content.getScrollElement(); |         if (!this.content) { | ||||||
|             if (!this.done) { |             if(retries > 0) { | ||||||
|                 // Move element to the nearest ion-content if it's not the parent
 |                 await CoreUtils.nextTicks(50); | ||||||
|                 if (this.element.parentElement?.nodeName != 'ION-CONTENT') { |  | ||||||
|                     const ionContent = this.element.closest('ion-content'); |  | ||||||
|                     ionContent?.appendChild(this.element); |  | ||||||
|                 } |  | ||||||
| 
 | 
 | ||||||
|                 // Add space at the bottom to let the user see the whole content.
 |                 this.ngOnInit(retries - 1); | ||||||
|                 const bottom = parseInt(this.scrollElement.style.paddingBottom, 10) || 0; |  | ||||||
|                 this.scrollElement.style.paddingBottom = (bottom + CoreFabDirective.PADDINGBOTTOM) + 'px'; |  | ||||||
|                 this.done = true; |  | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  |             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 { |     ngOnDestroy(): void { | ||||||
|         if (this.done && this.scrollElement) { |         if (this.content) { | ||||||
|             const bottom = parseInt(this.scrollElement.style.paddingBottom, 10) || 0; |             this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px'); | ||||||
|             this.scrollElement.style.paddingBottom = (bottom - CoreFabDirective.PADDINGBOTTOM) + 'px'; |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -45,15 +45,37 @@ export class CoreModuleHandlerBase implements Partial<CoreCourseModuleHandler> { | |||||||
|             title: module.name, |             title: module.name, | ||||||
|             class: 'addon-mod_' + module.modname + '-handler', |             class: 'addon-mod_' + module.modname + '-handler', | ||||||
|             showDownloadButton: true, |             showDownloadButton: true, | ||||||
|             action: (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void => { |             action: async ( | ||||||
|                 options = options || {}; |                 event: Event, | ||||||
|                 options.params = options.params || {}; |                 module: CoreCourseModule, | ||||||
|                 Object.assign(options.params, { module }); |                 courseId: number, | ||||||
|                 const routeParams = '/' + courseId + '/' + module.id; |                 options?: CoreNavigationOptions, | ||||||
| 
 |             ): Promise<void> => { | ||||||
|                 CoreNavigator.navigateToSitePath(this.pageName + routeParams, options); |                 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<void> { | ||||||
|  |         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); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -26,6 +26,7 @@ import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsup | |||||||
| import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy'; | import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy'; | ||||||
| import { CoreCourseModuleInfoComponent } from './module-info/module-info'; | import { CoreCourseModuleInfoComponent } from './module-info/module-info'; | ||||||
| import { CoreCourseModuleManualCompletionComponent } from './module-manual-completion/module-manual-completion'; | import { CoreCourseModuleManualCompletionComponent } from './module-manual-completion/module-manual-completion'; | ||||||
|  | import { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     declarations: [ |     declarations: [ | ||||||
| @ -39,6 +40,7 @@ import { CoreCourseModuleManualCompletionComponent } from './module-manual-compl | |||||||
|         CoreCourseSectionSelectorComponent, |         CoreCourseSectionSelectorComponent, | ||||||
|         CoreCourseTagAreaComponent, |         CoreCourseTagAreaComponent, | ||||||
|         CoreCourseUnsupportedModuleComponent, |         CoreCourseUnsupportedModuleComponent, | ||||||
|  |         CoreCourseModuleNavigationComponent, | ||||||
|     ], |     ], | ||||||
|     imports: [ |     imports: [ | ||||||
|         CoreBlockComponentsModule, |         CoreBlockComponentsModule, | ||||||
| @ -55,6 +57,7 @@ import { CoreCourseModuleManualCompletionComponent } from './module-manual-compl | |||||||
|         CoreCourseSectionSelectorComponent, |         CoreCourseSectionSelectorComponent, | ||||||
|         CoreCourseTagAreaComponent, |         CoreCourseTagAreaComponent, | ||||||
|         CoreCourseUnsupportedModuleComponent, |         CoreCourseUnsupportedModuleComponent, | ||||||
|  |         CoreCourseModuleNavigationComponent, | ||||||
|     ], |     ], | ||||||
| }) | }) | ||||||
| export class CoreCourseComponentsModule {} | export class CoreCourseComponentsModule {} | ||||||
|  | |||||||
| @ -13,24 +13,25 @@ | |||||||
| <ion-item class="ion-text-wrap" *ngIf="description" lines="none"> | <ion-item class="ion-text-wrap" *ngIf="description" lines="none"> | ||||||
|     <ion-label> |     <ion-label> | ||||||
|         <core-format-text [text]="description" [component]="component" [componentId]="componentId" contextLevel="module" |         <core-format-text [text]="description" [component]="component" [componentId]="componentId" contextLevel="module" | ||||||
|             [contextInstanceId]="module.id" [courseId]="courseId" [maxHeight]="120"> |             [contextInstanceId]="module.id" [courseId]="courseId" [maxHeight]="expandDescription ? null : 120"> | ||||||
|         </core-format-text> |         </core-format-text> | ||||||
|     </ion-label> |     </ion-label> | ||||||
| </ion-item> | </ion-item> | ||||||
| <ng-content select="[description]"></ng-content> | <ng-content select="[description]"></ng-content> | ||||||
| <ion-item class="ion-text-wrap" lines="none" *ngIf="showCompletion && (module.dates?.length || module.completiondata)"> | <ion-item class="ion-text-wrap" lines="none" *ngIf="showCompletion && (module.dates?.length || | ||||||
|  |         (module.completiondata && (module.completiondata.isautomatic || showManualCompletion) && module.uservisible))"> | ||||||
|     <ion-label> |     <ion-label> | ||||||
|         <!-- Activity dates. --> |         <!-- Activity dates. --> | ||||||
|         <div *ngIf="module.dates && module.dates.length" class="core-module-dates"> |         <div *ngIf="module.dates?.length" class="core-module-dates"> | ||||||
|             <p *ngFor="let date of module.dates"> |             <p *ngFor="let date of module.dates"> | ||||||
|                 <strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }} |                 <strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }} | ||||||
|             </p> |             </p> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <!-- Module completion. --> |         <!-- Module completion. --> | ||||||
|         <core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata" [moduleName]="module.name" |         <core-course-module-completion *ngIf="module.completiondata && module.uservisible" [completion]="module.completiondata" | ||||||
|             [moduleId]="module.id" [showCompletionConditions]="true" [showManualCompletion]="true" |             [moduleName]="module.name" [moduleId]="module.id" [showCompletionConditions]="true" | ||||||
|             (completionChanged)="completionChanged.emit($event)"> |             [showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)"> | ||||||
|         </core-course-module-completion> |         </core-course-module-completion> | ||||||
|     </ion-label> |     </ion-label> | ||||||
| </ion-item> | </ion-item> | ||||||
|  | |||||||
| @ -36,12 +36,14 @@ import { CoreSites } from '@services/sites'; | |||||||
| export class CoreCourseModuleInfoComponent implements OnInit { | export class CoreCourseModuleInfoComponent implements OnInit { | ||||||
| 
 | 
 | ||||||
|     @Input() module!: CoreCourseModule; // The module to render.
 |     @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() courseId!: number; // The courseId the module belongs to.
 | ||||||
| 
 | 
 | ||||||
|     @Input() component!: string; // Component for format text directive.
 |     @Input() component!: string; // Component for format text directive.
 | ||||||
|     @Input() componentId!: string | number; // Component ID to use in conjunction with the component.
 |     @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() 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.
 |     @Input() hasDataToSync = false; // If the activity has any data to be synced.
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -2,20 +2,22 @@ | |||||||
| 
 | 
 | ||||||
|     <ng-container *ngIf="completion.istrackeduser"> |     <ng-container *ngIf="completion.istrackeduser"> | ||||||
|         <ng-container *ngIf="completion.state"> |         <ng-container *ngIf="completion.state"> | ||||||
|             <ion-button color="success" fill="outline" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)"> |             <ion-button color="success" expand="block" fill="outline" [attr.aria-label]="accessibleDescription" | ||||||
|  |                 (click)="completionClicked($event)"> | ||||||
|                 <ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon> |                 <ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon> | ||||||
|                 {{ 'core.course.completion_manual:done' | translate }} |                 {{ 'core.course.completion_manual:done' | translate }} | ||||||
|             </ion-button> |             </ion-button> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|         <ng-container *ngIf="!completion.state"> |         <ng-container *ngIf="!completion.state"> | ||||||
|             <ion-button color="light" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)"> |             <ion-button color="dark" expand="block" fill="outline" [attr.aria-label]="accessibleDescription" | ||||||
|  |                 (click)="completionClicked($event)"> | ||||||
|                 {{ 'core.course.completion_manual:markdone' | translate }} |                 {{ 'core.course.completion_manual:markdone' | translate }} | ||||||
|             </ion-button> |             </ion-button> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|     </ng-container> |     </ng-container> | ||||||
| 
 | 
 | ||||||
|     <ng-container *ngIf="!completion.istrackeduser"> |     <ng-container *ngIf="!completion.istrackeduser"> | ||||||
|         <ion-button disabled="true" color="light"> |         <ion-button disabled="true" color="dark" expand="block" fill="outline"> | ||||||
|             {{ 'core.course.completion_manual:markdone' | translate }} |             {{ 'core.course.completion_manual:markdone' | translate }} | ||||||
|         </ion-button> |         </ion-button> | ||||||
|     </ng-container> |     </ng-container> | ||||||
|  | |||||||
| @ -0,0 +1,16 @@ | |||||||
|  | <core-loading [hideUntil]="loaded" [fullscreen]="false"> | ||||||
|  |     <ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding" *ngIf="previousModule || nextModule"> | ||||||
|  |         <ion-col size="auto"> | ||||||
|  |             <ion-button fill="clear" class="core-course-previous-module" *ngIf="previousModule" (click)="goToActivity(false)" | ||||||
|  |                 [attr.aria-label]="'core.course.gotopreviousactivity' | translate"> | ||||||
|  |                 <ion-icon name="fas-arrow-left" slot="icon-only" aria-hidden="true"></ion-icon> | ||||||
|  |             </ion-button> | ||||||
|  |         </ion-col> | ||||||
|  |         <ion-col size="auto"> | ||||||
|  |             <ion-button fill="clear" class="core-course-next-module" *ngIf="nextModule" (click)="goToActivity(true)" | ||||||
|  |                 [attr.aria-label]="'core.course.gotonextactivity' | translate"> | ||||||
|  |                 <ion-icon name="fas-arrow-right" slot="icon-only" aria-hidden="true"></ion-icon> | ||||||
|  |             </ion-button> | ||||||
|  |         </ion-col> | ||||||
|  |     </ion-row> | ||||||
|  | </core-loading> | ||||||
| @ -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; | ||||||
|  | } | ||||||
| @ -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: | ||||||
|  |  * <core-course-module-navigation [courseId]="courseId" [currentModuleId]="module.id"></core-course-module-navigation> | ||||||
|  |  */ | ||||||
|  | @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<void> { | ||||||
|  |         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<void> { | ||||||
|  |         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<ScrollDetail>): void => { | ||||||
|  |             if (!this.content) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             this.onScroll(e.detail.scrollTop, scroll.scrollHeight - scroll.offsetHeight); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     async ngOnDestroy(): Promise<void> { | ||||||
|  |         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<void> { | ||||||
|  |         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<boolean> { | ||||||
|  |         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<void> { | ||||||
|  |         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'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -75,8 +75,8 @@ | |||||||
|                 </div> |                 </div> | ||||||
| 
 | 
 | ||||||
|                 <!-- Module completion. --> |                 <!-- Module completion. --> | ||||||
|                 <core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata" [moduleName]="module.name" |                 <core-course-module-completion *ngIf="module.completiondata && module.uservisible" [completion]="module.completiondata" | ||||||
|                     [moduleId]="module.id" [showCompletionConditions]="showCompletionConditions" |                     [moduleName]="module.name" [moduleId]="module.id" [showCompletionConditions]="showCompletionConditions" | ||||||
|                     [showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)"> |                     [showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)"> | ||||||
|                 </core-course-module-completion> |                 </core-course-module-completion> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -161,7 +161,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { | |||||||
|         event.preventDefault(); |         event.preventDefault(); | ||||||
|         event.stopPropagation(); |         event.stopPropagation(); | ||||||
| 
 | 
 | ||||||
|         button.action(event, this.module!, this.courseId!); |         button.action(event, this.module, this.courseId!); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -1,6 +1,3 @@ | |||||||
| <core-course-module-info [description]="module?.description" [courseId]="courseId" [module]="module"> |  | ||||||
| </core-course-module-info> |  | ||||||
| 
 |  | ||||||
| <div class="ion-padding"> | <div class="ion-padding"> | ||||||
|     <h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2> |     <h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2> | ||||||
|     <h2 *ngIf="isDisabledInSite || !isSupportedByTheApp">{{ 'core.uhoh' | translate }}</h2> |     <h2 *ngIf="isDisabledInSite || !isSupportedByTheApp">{{ 'core.uhoh' | translate }}</h2> | ||||||
|  | |||||||
| @ -23,9 +23,9 @@ const routes: Routes = [ | |||||||
|         loadChildren: () => import('./pages/index/index.module').then( m => m.CoreCourseIndexPageModule), |         loadChildren: () => import('./pages/index/index.module').then( m => m.CoreCourseIndexPageModule), | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|         path: ':courseId/unsupported-module', |         path: ':courseId/:cmId/module-preview', | ||||||
|         loadChildren: () => import('./pages/unsupported-module/unsupported-module.module') |         loadChildren: () => import('./pages/module-preview/module-preview.module') | ||||||
|             .then( m => m.CoreCourseUnsupportedModulePageModule), |             .then( m => m.CoreCourseModulePreviewPageModule), | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|         path: ':courseId/list-mod-type', |         path: ':courseId/list-mod-type', | ||||||
|  | |||||||
| @ -27,6 +27,10 @@ | |||||||
|     "confirmpartialdownloadsize": "You are about to download <strong>at least</strong> {{size}}.{{availableSpace}} Are you sure you want to continue?", |     "confirmpartialdownloadsize": "You are about to download <strong>at least</strong> {{size}}.{{availableSpace}} Are you sure you want to continue?", | ||||||
|     "confirmlimiteddownload": "You are not currently connected to Wi-Fi. ", |     "confirmlimiteddownload": "You are not currently connected to Wi-Fi. ", | ||||||
|     "contents": "Contents", |     "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.", |     "couldnotloadsectioncontent": "Could not load the section content. Please try again later.", | ||||||
|     "couldnotloadsections": "Could not load the sections. Please try again later.", |     "couldnotloadsections": "Could not load the sections. Please try again later.", | ||||||
|     "coursesummary": "Course summary", |     "coursesummary": "Course summary", | ||||||
|  | |||||||
| @ -0,0 +1,65 @@ | |||||||
|  | <ion-header collapsible> | ||||||
|  |     <ion-toolbar> | ||||||
|  |         <ion-buttons slot="start"> | ||||||
|  |             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||||
|  |         </ion-buttons> | ||||||
|  |         <ion-title> | ||||||
|  |             <h1> | ||||||
|  |                 <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"> | ||||||
|  |                 </core-format-text> | ||||||
|  |             </h1> | ||||||
|  |         </ion-title> | ||||||
|  | 
 | ||||||
|  |         <ion-buttons slot="end"> | ||||||
|  |             <core-context-menu> | ||||||
|  |                 <core-context-menu-item [priority]="900" *ngIf="module.url" [href]="module!.url" | ||||||
|  |                     [content]="'core.openinbrowser' | translate" iconAction="fas-external-link-alt"> | ||||||
|  |                 </core-context-menu-item> | ||||||
|  |             </core-context-menu> | ||||||
|  |         </ion-buttons> | ||||||
|  |     </ion-toolbar> | ||||||
|  | </ion-header> | ||||||
|  | <ion-content> | ||||||
|  |     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)"> | ||||||
|  |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||||
|  |     </ion-refresher> | ||||||
|  |     <core-loading [hideUntil]="loaded"> | ||||||
|  |         <core-course-module-info [module]="module" [courseId]="courseId" [description]="module.description" [component]="module.modname" | ||||||
|  |             [componentId]="module.id" (completionChanged)="onCompletionChange()" [expandDescription]="true" | ||||||
|  |             [showManualCompletion]="showManualCompletion"> | ||||||
|  | 
 | ||||||
|  |             <div class="safe-area-padding-horizontal ion-padding" *ngIf="module.handlerData?.extraBadge"> | ||||||
|  |                 <ion-badge class="ion-text-wrap ion-text-start" [color]="module.handlerData?.extraBadgeColor"> | ||||||
|  |                     <span [innerHTML]="module.handlerData?.extraBadge"></span> | ||||||
|  |                 </ion-badge> | ||||||
|  |             </div> | ||||||
|  |             <div class="safe-area-padding-horizontal ion-padding" *ngIf="module.visible === 0 && (!section || section.visible)"> | ||||||
|  |                 <ion-badge class="ion-text-wrap"> | ||||||
|  |                     {{ 'core.course.hiddenfromstudents' | translate }} | ||||||
|  |                 </ion-badge> | ||||||
|  |             </div> | ||||||
|  |             <div class="safe-area-padding-horizontal ion-padding" *ngIf="module.visible !== 0 && module.isStealth"> | ||||||
|  |                 <ion-badge class="ion-text-wrap"> | ||||||
|  |                     {{ 'core.course.hiddenoncoursepage' | translate }} | ||||||
|  |                 </ion-badge> | ||||||
|  |             </div> | ||||||
|  |             <div class="safe-area-padding-horizontal ion-padding core-module-availabilityinfo" *ngIf="module.availabilityinfo"> | ||||||
|  |                 <ion-badge class="ion-text-wrap">{{ 'core.restricted' | translate }}</ion-badge> | ||||||
|  |                 <div> | ||||||
|  |                     <core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id" | ||||||
|  |                         [courseId]="courseId" class="ion-text-wrap"> | ||||||
|  |                     </core-format-text> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |             <div class="safe-area-padding-horizontal ion-padding" *ngIf="module.completiondata?.offline"> | ||||||
|  |                 <ion-badge color="warning" class="ion-text-wrap"> | ||||||
|  |                     {{ 'core.course.manualcompletionnotsynced' | translate }} | ||||||
|  |                 </ion-badge> | ||||||
|  |             </div> | ||||||
|  | 
 | ||||||
|  |             <core-course-unsupported-module *ngIf="unsupported" [module]="module" [courseId]="courseId"></core-course-unsupported-module> | ||||||
|  |         </core-course-module-info> | ||||||
|  |     </core-loading> | ||||||
|  | 
 | ||||||
|  |     <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"></core-course-module-navigation> | ||||||
|  | </ion-content> | ||||||
| @ -16,13 +16,13 @@ import { NgModule } from '@angular/core'; | |||||||
| import { RouterModule, Routes } from '@angular/router'; | import { RouterModule, Routes } from '@angular/router'; | ||||||
| 
 | 
 | ||||||
| import { CoreSharedModule } from '@/core/shared.module'; | 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'; | import { CoreCourseComponentsModule } from '@features/course/components/components.module'; | ||||||
| 
 | 
 | ||||||
| const routes: Routes = [ | const routes: Routes = [ | ||||||
|     { |     { | ||||||
|         path: '', |         path: '', | ||||||
|         component: CoreCourseUnsupportedModulePage, |         component: CoreCourseModulePreviewPage, | ||||||
|     }, |     }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| @ -33,8 +33,8 @@ const routes: Routes = [ | |||||||
|         CoreCourseComponentsModule, |         CoreCourseComponentsModule, | ||||||
|     ], |     ], | ||||||
|     declarations: [ |     declarations: [ | ||||||
|         CoreCourseUnsupportedModulePage, |         CoreCourseModulePreviewPage, | ||||||
|     ], |     ], | ||||||
|     exports: [RouterModule], |     exports: [RouterModule], | ||||||
| }) | }) | ||||||
| export class CoreCourseUnsupportedModulePageModule {} | export class CoreCourseModulePreviewPageModule { } | ||||||
| @ -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<void> { | ||||||
|  |         try { | ||||||
|  |             this.module = CoreNavigator.getRequiredRouteParam<CoreCourseModule>('module'); | ||||||
|  |             this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); | ||||||
|  |             this.section = CoreNavigator.getRouteParam<CoreCourseSection>('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<void> { | ||||||
|  |         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<void> { | ||||||
|  | 
 | ||||||
|  |         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<void> { | ||||||
|  |         // Update the module data after a while.
 | ||||||
|  |         this.debouncedUpdateModule?.(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -1,27 +0,0 @@ | |||||||
| <ion-header collapsible> |  | ||||||
|     <ion-toolbar> |  | ||||||
|         <ion-buttons slot="start"> |  | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |  | ||||||
|         </ion-buttons> |  | ||||||
|         <ion-title> |  | ||||||
|             <h1> |  | ||||||
|                 <core-format-text [text]="module?.name" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId"> |  | ||||||
|                 </core-format-text> |  | ||||||
|             </h1> |  | ||||||
|         </ion-title> |  | ||||||
| 
 |  | ||||||
|         <ion-buttons slot="end"> |  | ||||||
|             <core-context-menu> |  | ||||||
|                 <core-context-menu-item [priority]="900" *ngIf="module?.url" [href]="module!.url" |  | ||||||
|                     [content]="'core.openinbrowser' | translate" iconAction="fas-external-link-alt"> |  | ||||||
|                 </core-context-menu-item> |  | ||||||
|                 <core-context-menu-item [priority]="800" *ngIf="module?.description" [content]="'core.moduleintro' | translate" |  | ||||||
|                     (action)="expandDescription()" iconAction="fas-arrow-right"> |  | ||||||
|                 </core-context-menu-item> |  | ||||||
|             </core-context-menu> |  | ||||||
|         </ion-buttons> |  | ||||||
|     </ion-toolbar> |  | ||||||
| </ion-header> |  | ||||||
| <ion-content> |  | ||||||
|     <core-course-unsupported-module [module]="module" [courseId]="courseId"></core-course-unsupported-module> |  | ||||||
| </ion-content> |  | ||||||
| @ -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, |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
| @ -44,6 +44,7 @@ import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; | |||||||
| import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync'; | import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync'; | ||||||
| import { CoreTagItem } from '@features/tag/services/tag'; | import { CoreTagItem } from '@features/tag/services/tag'; | ||||||
| import { CoreNavigator } from '@services/navigator'; | import { CoreNavigator } from '@services/navigator'; | ||||||
|  | import { CoreCourseModuleDelegate } from './module-delegate'; | ||||||
| 
 | 
 | ||||||
| const ROOT_CACHE_KEY = 'mmCourse:'; | const ROOT_CACHE_KEY = 'mmCourse:'; | ||||||
| 
 | 
 | ||||||
| @ -1058,6 +1059,14 @@ export class CoreCourseProvider { | |||||||
|      * @return Whether the module has a view page. |      * @return Whether the module has a view page. | ||||||
|      */ |      */ | ||||||
|     moduleHasView(module: CoreCourseModuleSummary | CoreCourseWSModule): boolean { |     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; |         return !!module.url; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -49,14 +49,11 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler { | |||||||
|             icon: await CoreCourse.getModuleIconSrc(module.modname, module.modicon), |             icon: await CoreCourse.getModuleIconSrc(module.modname, module.modicon), | ||||||
|             title: module.name, |             title: module.name, | ||||||
|             class: 'core-course-default-handler core-course-module-' + module.modname + '-handler', |             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.preventDefault(); | ||||||
|                 event.stopPropagation(); |                 event.stopPropagation(); | ||||||
| 
 | 
 | ||||||
|                 options = options || {}; |                 await this.openActivityPage(module, courseId, options); | ||||||
|                 options.params = { module }; |  | ||||||
| 
 |  | ||||||
|                 CoreNavigator.navigateToSitePath('course/' + courseId + '/unsupported-module', options); |  | ||||||
|             }, |             }, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
| @ -92,4 +89,15 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler { | |||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     async openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise<void> { | ||||||
|  |         options = options || {}; | ||||||
|  |         options.params = options.params || {}; | ||||||
|  |         Object.assign(options.params, { module }); | ||||||
|  | 
 | ||||||
|  |         await CoreNavigator.navigateToSitePath('course/' + courseId + '/' + module.id +'/module-preview', options); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -102,6 +102,16 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler { | |||||||
|      * @return Promise resolved with boolean: whether the manual completion should always be displayed. |      * @return Promise resolved with boolean: whether the manual completion should always be displayed. | ||||||
|      */ |      */ | ||||||
|     manualCompletionAlwaysShown?(module: CoreCourseModule): Promise<boolean>; |     manualCompletionAlwaysShown?(module: CoreCourseModule): Promise<boolean>; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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<void>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -167,8 +177,9 @@ export interface CoreCourseModuleHandlerData { | |||||||
|      * @param module The module object. |      * @param module The module object. | ||||||
|      * @param courseId The course ID. |      * @param courseId The course ID. | ||||||
|      * @param options Options for the navigation. |      * @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> | void; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Updates the status of the module. |      * Updates the status of the module. | ||||||
| @ -236,8 +247,10 @@ export interface CoreCourseModuleHandlerButton { | |||||||
|      * @param event The click event. |      * @param event The click event. | ||||||
|      * @param module The module object. |      * @param module The module object. | ||||||
|      * @param courseId The course ID. |      * @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> | void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -292,6 +305,27 @@ export class CoreCourseModuleDelegateService extends CoreDelegate<CoreCourseModu | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * 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( | ||||||
|  |         modname: string, | ||||||
|  |         module: CoreCourseModule, | ||||||
|  |         courseId: number, | ||||||
|  |         options?: CoreNavigationOptions, | ||||||
|  |     ): Promise<void> { | ||||||
|  |         return await this.executeFunctionOnEnabled<void>( | ||||||
|  |             modname, | ||||||
|  |             'openActivityPage', | ||||||
|  |             [module, courseId, options], | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Check if a certain module type is disabled in a site. |      * Check if a certain module type is disabled in a site. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ | |||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <core-loading [hideUntil]="readyToCapture" class="core-loading-fullheight"> |     <core-loading [hideUntil]="readyToCapture"> | ||||||
|         <div class="core-av-wrapper"> |         <div class="core-av-wrapper"> | ||||||
|             <!-- Video stream for image and video. --> |             <!-- Video stream for image and video. --> | ||||||
|             <video *ngIf="!isAudio" [hidden]="hasCaptured" class="core-webcam-stream" autoplay #streamVideo></video> |             <video *ngIf="!isAudio" [hidden]="hasCaptured" class="core-webcam-stream" autoplay #streamVideo></video> | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ | |||||||
| import { Type } from '@angular/core'; | import { Type } from '@angular/core'; | ||||||
| 
 | 
 | ||||||
| import { CoreConstants } from '@/core/constants'; | 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 { CoreCourseModule } from '@features/course/services/course-helper'; | ||||||
| import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; | import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; | ||||||
| import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index'; | import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index'; | ||||||
| @ -92,17 +92,16 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp | |||||||
| 
 | 
 | ||||||
|         if (this.handlerSchema.method) { |         if (this.handlerSchema.method) { | ||||||
|             // There is a method, add an action.
 |             // 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.preventDefault(); | ||||||
|                 event.stopPropagation(); |                 event.stopPropagation(); | ||||||
| 
 | 
 | ||||||
|                 options = options || {}; |                 await this.openActivityPage(module, courseId, options); | ||||||
|                 options.params = { |  | ||||||
|                     title: module.name, |  | ||||||
|                     module, |  | ||||||
|                 }; |  | ||||||
| 
 |  | ||||||
|                 CoreNavigator.navigateToSitePath(`siteplugins/module/${courseId}/${module.id}`, options); |  | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -229,4 +228,22 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp | |||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     async openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise<void> { | ||||||
|  |         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); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -28,6 +28,7 @@ import { CoreSitePluginsAssignSubmissionComponent } from './assign-submission/as | |||||||
| import { CoreSitePluginsWorkshopAssessmentStrategyComponent } from './workshop-assessment-strategy/workshop-assessment-strategy'; | import { CoreSitePluginsWorkshopAssessmentStrategyComponent } from './workshop-assessment-strategy/workshop-assessment-strategy'; | ||||||
| import { CoreSitePluginsBlockComponent } from './block/block'; | import { CoreSitePluginsBlockComponent } from './block/block'; | ||||||
| import { CoreSitePluginsOnlyTitleBlockComponent } from './only-title-block/only-title-block'; | import { CoreSitePluginsOnlyTitleBlockComponent } from './only-title-block/only-title-block'; | ||||||
|  | import { CoreCourseComponentsModule } from '@features/course/components/components.module'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     declarations: [ |     declarations: [ | ||||||
| @ -47,6 +48,7 @@ import { CoreSitePluginsOnlyTitleBlockComponent } from './only-title-block/only- | |||||||
|     imports: [ |     imports: [ | ||||||
|         CoreSharedModule, |         CoreSharedModule, | ||||||
|         CoreCompileHtmlComponentModule, |         CoreCompileHtmlComponentModule, | ||||||
|  |         CoreCourseComponentsModule, | ||||||
|     ], |     ], | ||||||
|     exports: [ |     exports: [ | ||||||
|         CoreSitePluginsPluginContentComponent, |         CoreSitePluginsPluginContentComponent, | ||||||
|  | |||||||
| @ -11,8 +11,7 @@ | |||||||
|         </core-context-menu-item> |         </core-context-menu-item> | ||||||
|         <core-context-menu-item [hidden]="!displayRefresh || ( |         <core-context-menu-item [hidden]="!displayRefresh || ( | ||||||
|             content?.compileComponent?.componentInstance?.displayRefresh === false)" [priority]="700" |             content?.compileComponent?.componentInstance?.displayRefresh === false)" [priority]="700" | ||||||
|             [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" |             [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"> | ||||||
|             [closeOnClick]="false"> |  | ||||||
|         </core-context-menu-item> |         </core-context-menu-item> | ||||||
|         <core-context-menu-item [hidden]="!displayPrefetch || !prefetchStatusIcon || ( |         <core-context-menu-item [hidden]="!displayPrefetch || !prefetchStatusIcon || ( | ||||||
|             content?.compileComponent?.componentInstance?.displayPrefetch === false)" [priority]="600" [content]="prefetchText" |             content?.compileComponent?.componentInstance?.displayPrefetch === false)" [priority]="600" [content]="prefetchText" | ||||||
| @ -30,3 +29,5 @@ | |||||||
|     [initResult]="initResult" [data]="jsData" [pageTitle]="pageTitle" [preSets]="preSets" (onContentLoaded)="contentLoaded($event)" |     [initResult]="initResult" [data]="jsData" [pageTitle]="pageTitle" [preSets]="preSets" (onContentLoaded)="contentLoaded($event)" | ||||||
|     (onLoadingContent)="contentLoading()"> |     (onLoadingContent)="contentLoading()"> | ||||||
| </core-site-plugins-plugin-content> | </core-site-plugins-plugin-content> | ||||||
|  | 
 | ||||||
|  | <core-course-module-navigation *ngIf="module" [courseId]="courseId" [currentModuleId]="module.id"></core-course-module-navigation> | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ | |||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <core-loading [hideUntil]="finalUrl" class="core-loading-fullheight"> |     <core-loading [hideUntil]="finalUrl"> | ||||||
|         <core-iframe *ngIf="finalUrl" [src]="finalUrl"></core-iframe> |         <core-iframe *ngIf="finalUrl" [src]="finalUrl"></core-iframe> | ||||||
|     </core-loading> |     </core-loading> | ||||||
| </ion-content> | </ion-content> | ||||||
|  | |||||||
| @ -975,7 +975,7 @@ export class CoreDomUtilsProvider { | |||||||
|      * @deprecated since 3.9.5. Use directly the IonContent class. |      * @deprecated since 3.9.5. Use directly the IonContent class. | ||||||
|      */ |      */ | ||||||
|     scrollTo(content: IonContent, x: number, y: number, duration?: number): Promise<void> { |     scrollTo(content: IonContent, x: number, y: number, duration?: number): Promise<void> { | ||||||
|         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. |      * @deprecated since 3.9.5. Use directly the IonContent class. | ||||||
|      */ |      */ | ||||||
|     scrollToBottom(content: IonContent, duration?: number): Promise<void> { |     scrollToBottom(content: IonContent, duration?: number): Promise<void> { | ||||||
|         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. |      * @deprecated since 3.9.5. Use directly the IonContent class. | ||||||
|      */ |      */ | ||||||
|     scrollToTop(content: IonContent, duration?: number): Promise<void> { |     scrollToTop(content: IonContent, duration?: number): Promise<void> { | ||||||
|         return content?.scrollToTop(duration); |         return content.scrollToTop(duration); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -1010,9 +1010,9 @@ export class CoreDomUtilsProvider { | |||||||
|      */ |      */ | ||||||
|     async getContentHeight(content: IonContent): Promise<number> { |     async getContentHeight(content: IonContent): Promise<number> { | ||||||
|         try { |         try { | ||||||
|             const scrollElement = await content?.getScrollElement(); |             const scrollElement = await content.getScrollElement(); | ||||||
| 
 | 
 | ||||||
|             return scrollElement?.clientHeight || 0; |             return scrollElement.clientHeight || 0; | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             return 0; |             return 0; | ||||||
|         } |         } | ||||||
| @ -1026,9 +1026,9 @@ export class CoreDomUtilsProvider { | |||||||
|      */ |      */ | ||||||
|     async getScrollHeight(content: IonContent): Promise<number> { |     async getScrollHeight(content: IonContent): Promise<number> { | ||||||
|         try { |         try { | ||||||
|             const scrollElement = await content?.getScrollElement(); |             const scrollElement = await content.getScrollElement(); | ||||||
| 
 | 
 | ||||||
|             return scrollElement?.scrollHeight || 0; |             return scrollElement.scrollHeight || 0; | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             return 0; |             return 0; | ||||||
|         } |         } | ||||||
| @ -1042,9 +1042,9 @@ export class CoreDomUtilsProvider { | |||||||
|      */ |      */ | ||||||
|     async getScrollTop(content: IonContent): Promise<number> { |     async getScrollTop(content: IonContent): Promise<number> { | ||||||
|         try { |         try { | ||||||
|             const scrollElement = await content?.getScrollElement(); |             const scrollElement = await content.getScrollElement(); | ||||||
| 
 | 
 | ||||||
|             return scrollElement?.scrollTop || 0; |             return scrollElement.scrollTop || 0; | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             return 0; |             return 0; | ||||||
|         } |         } | ||||||
| @ -1065,7 +1065,7 @@ export class CoreDomUtilsProvider { | |||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         content?.scrollToPoint(position[0], position[1], duration || 0); |         content.scrollToPoint(position[0], position[1], duration || 0); | ||||||
| 
 | 
 | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| @ -1097,7 +1097,7 @@ export class CoreDomUtilsProvider { | |||||||
|                 return false; |                 return false; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             content?.scrollToPoint(position[0], position[1], duration || 0); |             content.scrollToPoint(position[0], position[1], duration || 0); | ||||||
| 
 | 
 | ||||||
|             return true; |             return true; | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|  | |||||||
| @ -906,15 +906,18 @@ ion-back-button.md::part(text) { | |||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Hide close button because when present is read on voice over. | ||||||
| ion-fab[core-fab] { | ion-fab[core-fab] { | ||||||
|     position: fixed; |  | ||||||
|      |  | ||||||
|     // Hide close button because when present is read on voice over. |  | ||||||
|     ion-fab-button::part(close-icon) { |     ion-fab-button::part(close-icon) { | ||||||
|         display: none; |         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 { | .core-media-adapt-width { | ||||||
|     max-width: 100%; |     max-width: 100%; | ||||||
| } | } | ||||||
|  | |||||||
| @ -257,6 +257,9 @@ | |||||||
| 
 | 
 | ||||||
|     --core-courseimage-on-course-height: 150px; |     --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-category-color: var(--purple); | ||||||
|     --addon-calendar-event-course-color: var(--red); |     --addon-calendar-event-course-color: var(--red); | ||||||
|     --addon-calendar-event-group-color: var(--yellow); |     --addon-calendar-event-group-color: var(--yellow); | ||||||
|  | |||||||
| @ -4,6 +4,8 @@ information provided here is intended especially for developers. | |||||||
| === 3.9.6 === | === 3.9.6 === | ||||||
| 
 | 
 | ||||||
| - The parameters of the functions confirmAndPrefetchCourse and confirmAndPrefetchCourses have changed, they now accept an object with options. | - 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 === | === 3.9.5 === | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user