MOBILE-3915 course: Move Course summary info outside the tabs
This commit is contained in:
		
							parent
							
								
									86365d260d
								
							
						
					
					
						commit
						901a445408
					
				| @ -62,7 +62,7 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle | ||||
|     @Input() layout: 'icon-top' | 'icon-start' | 'icon-end' | 'icon-bottom' | 'icon-hide' | 'label-hide' = 'icon-hide'; | ||||
|     @Input() tabs: CoreTabsOutletTab[] = []; | ||||
| 
 | ||||
|     @ViewChild(IonTabs) protected ionTabs?: IonTabs; | ||||
|     @ViewChild(IonTabs) protected ionTabs!: IonTabs; | ||||
| 
 | ||||
|     protected stackEventsSubscription?: Subscription; | ||||
|     protected outletActivatedSubscription?: Subscription; | ||||
| @ -96,11 +96,17 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle | ||||
|         } | ||||
| 
 | ||||
|         this.tabsElement = this.element.nativeElement.querySelector('ion-tabs'); | ||||
|         this.stackEventsSubscription = this.ionTabs?.outlet.stackEvents.subscribe(async (stackEvent: StackEvent) => { | ||||
|         this.stackEventsSubscription = this.ionTabs.outlet.stackEvents.subscribe(async (stackEvent: StackEvent) => { | ||||
|             if (!this.isCurrentView) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Add tabid to the tab content element.
 | ||||
|             if (stackEvent.enteringView.element.id == '') { | ||||
|                 const tab = this.tabs.find((tab) => tab.page == stackEvent.enteringView.url); | ||||
|                 stackEvent.enteringView.element.id = tab?.id || ''; | ||||
|             } | ||||
| 
 | ||||
|             this.showHideNavBarButtons(stackEvent.enteringView.element.tagName); | ||||
| 
 | ||||
|             await this.listenContentScroll(stackEvent.enteringView.element, stackEvent.enteringView.id); | ||||
| @ -111,8 +117,8 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle | ||||
|                 this.showHideTabs(scrollElement.scrollTop, scrollElement); | ||||
|             } | ||||
|         }); | ||||
|         this.outletActivatedSubscription = this.ionTabs?.outlet.activateEvents.subscribe(() => { | ||||
|             this.lastActiveComponent = this.ionTabs?.outlet.component; | ||||
|         this.outletActivatedSubscription = this.ionTabs.outlet.activateEvents.subscribe(() => { | ||||
|             this.lastActiveComponent = this.ionTabs.outlet.component; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| @ -140,8 +146,8 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle | ||||
|         // The `ionViewDidEnter` method is not called on nested outlets unless the parent page is leaving the navigation stack,
 | ||||
|         // that's why we need to call it manually if the page that is entering already existed in the stack (meaning that it is
 | ||||
|         // entering in response to a back navigation from the page on top).
 | ||||
|         if (this.existsInNavigationStack && this.ionTabs?.outlet.isActivated) { | ||||
|             (this.ionTabs?.outlet.component as Partial<ViewDidEnter>).ionViewDidEnter?.(); | ||||
|         if (this.existsInNavigationStack && this.ionTabs.outlet.isActivated) { | ||||
|             (this.ionTabs.outlet.component as Partial<ViewDidEnter>).ionViewDidEnter?.(); | ||||
|         } | ||||
| 
 | ||||
|         // After the view has entered for the first time, we can assume that it'll always be in the navigation stack
 | ||||
| @ -180,10 +186,9 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle | ||||
|      * @param activatedPageName Activated page name. | ||||
|      */ | ||||
|     protected showHideNavBarButtons(activatedPageName: string): void { | ||||
|         const elements = this.ionTabs!.outlet.nativeEl.querySelectorAll('core-navbar-buttons'); | ||||
|         const domUtils = CoreDomUtils.instance; | ||||
|         const elements = this.ionTabs.outlet.nativeEl.querySelectorAll('core-navbar-buttons'); | ||||
|         elements.forEach((element) => { | ||||
|             const instance = domUtils.getInstanceByElement<CoreNavBarButtonsComponent>(element); | ||||
|             const instance = CoreDomUtils.getInstanceByElement<CoreNavBarButtonsComponent>(element); | ||||
| 
 | ||||
|             if (instance) { | ||||
|                 const pagetagName = element.closest('.ion-page')?.tagName; | ||||
|  | ||||
| @ -43,6 +43,10 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | ||||
|     protected headerSubHeadingFontSize = 0; | ||||
|     protected contentSubHeadingFontSize = 0; | ||||
|     protected subHeadingStartDifference = 0; | ||||
|     protected inContent = true; | ||||
|     protected title?: HTMLElement | null; | ||||
|     protected titleHeight = 0; | ||||
|     protected contentH1?: HTMLElement | null; | ||||
| 
 | ||||
|     constructor(el: ElementRef<HTMLIonHeaderElement>) { | ||||
|         this.header = el.nativeElement; | ||||
| @ -67,8 +71,6 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | ||||
|     /** | ||||
|      * Gets the loading content id to wait for the loading to finish. | ||||
|      * | ||||
|      * @TODO: If no core-loading is present, load directly. Take into account content needs to be initialized. | ||||
|      * | ||||
|      * @return Promise resolved with Loading Id, if any. | ||||
|      */ | ||||
|     protected async getLoadingId(): Promise<string | undefined> { | ||||
| @ -80,6 +82,17 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const title = this.header.parentElement?.querySelector('.collapsible-title') || null; | ||||
| 
 | ||||
|             if (title) { | ||||
|                 // Title already found, no need to wait for loading.
 | ||||
|                 this.loadingObserver.off(); | ||||
|                 this.setupRealTitle(); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         return this.content.querySelector('core-loading.core-loading-loaded:not(.core-loading-inline) .core-loading-content')?.id; | ||||
| @ -89,6 +102,7 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | ||||
|      * Call this function when header is not collapsible. | ||||
|      */ | ||||
|     protected cannotCollapse(): void { | ||||
|         this.content = undefined; | ||||
|         this.loadingObserver.off(); | ||||
|         this.header.classList.add('core-header-collapsed'); | ||||
|     } | ||||
| @ -112,16 +126,25 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | ||||
|             await animation.finished; | ||||
|         })); | ||||
| 
 | ||||
|         const title = this.content.querySelector<HTMLElement>('.collapsible-title, h1'); | ||||
|         const contentH1 = this.content.querySelector<HTMLElement>('h1'); | ||||
|         let title = this.content.querySelector<HTMLElement>('.collapsible-title'); | ||||
|         if (!title) { | ||||
|             // Title is outside the ion-content.
 | ||||
|             title = this.header.parentElement?.querySelector('.collapsible-title') || null; | ||||
|             this.inContent = false; | ||||
|         } | ||||
|         this.contentH1 = title?.querySelector<HTMLElement>('h1'); | ||||
|         const headerH1 = this.header.querySelector<HTMLElement>('h1'); | ||||
|         if (!title || !contentH1 || !headerH1) { | ||||
|         if (!title || !this.contentH1 || !headerH1 || !this.contentH1.parentElement) { | ||||
|             this.cannotCollapse(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.titleTopDifference = contentH1.getBoundingClientRect().top - headerH1.getBoundingClientRect().top; | ||||
|         this.title = title; | ||||
|         this.titleHeight = title.getBoundingClientRect().height; | ||||
| 
 | ||||
|         this.titleTopDifference = this.contentH1.getBoundingClientRect().top - headerH1.getBoundingClientRect().top; | ||||
| 
 | ||||
|         if (this.titleTopDifference <= 0) { | ||||
|             this.cannotCollapse(); | ||||
| 
 | ||||
| @ -141,17 +164,17 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | ||||
|         } | ||||
| 
 | ||||
|         const headerH1Styles = getComputedStyle(headerH1); | ||||
|         const contentH1Styles = getComputedStyle(contentH1); | ||||
|         const contentH1Styles = getComputedStyle(this.contentH1); | ||||
| 
 | ||||
|         if (Platform.isRTL) { | ||||
|             // Checking position over parent because transition may not be finished.
 | ||||
|             const contentH1Position = contentH1.getBoundingClientRect().right - this.content.getBoundingClientRect().right; | ||||
|             const contentH1Position = this.contentH1.getBoundingClientRect().right - this.content.getBoundingClientRect().right; | ||||
|             const headerH1Position = headerH1.getBoundingClientRect().right - this.header.getBoundingClientRect().right; | ||||
| 
 | ||||
|             this.h1StartDifference = Math.round(contentH1Position - (headerH1Position - parseFloat(headerH1Styles.paddingRight))); | ||||
|         } else { | ||||
|             // Checking position over parent because transition may not be finished.
 | ||||
|             const contentH1Position = contentH1.getBoundingClientRect().left - this.content.getBoundingClientRect().left; | ||||
|             const contentH1Position = this.contentH1.getBoundingClientRect().left - this.content.getBoundingClientRect().left; | ||||
|             const headerH1Position = headerH1.getBoundingClientRect().left - this.header.getBoundingClientRect().left; | ||||
| 
 | ||||
|             this.h1StartDifference = Math.round(contentH1Position - (headerH1Position + parseFloat(headerH1Styles.paddingLeft))); | ||||
| @ -165,10 +188,10 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | ||||
|             if (styleName != 'font-size' && | ||||
|                 styleName != 'font-family' && | ||||
|                 (styleName.startsWith('font-') || styleName.startsWith('letter-'))) { | ||||
|                 contentH1.style.setProperty(styleName, headerH1Styles.getPropertyValue(styleName)); | ||||
|                 this.contentH1?.style.setProperty(styleName, headerH1Styles.getPropertyValue(styleName)); | ||||
|             } | ||||
|         }); | ||||
|         contentH1.style.setProperty( | ||||
|         this.contentH1.style.setProperty( | ||||
|             '--max-width', | ||||
|             (parseFloat(headerH1Styles.width) | ||||
|                 -parseFloat(headerH1Styles.paddingLeft) | ||||
| @ -176,47 +199,70 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | ||||
|                 +'px'), | ||||
|         ); | ||||
| 
 | ||||
|         contentH1.setAttribute('aria-hidden', 'true'); | ||||
|         this.contentH1.setAttribute('aria-hidden', 'true'); | ||||
| 
 | ||||
|         // Clone element to let the other elements be static.
 | ||||
|         const contentH1Clone = this.contentH1.cloneNode(true) as HTMLElement; | ||||
|         contentH1Clone.classList.add('cloned'); | ||||
|         this.contentH1.parentElement.insertBefore(contentH1Clone, this.contentH1); | ||||
|         this.contentH1.style.setProperty( | ||||
|             'top', | ||||
|             (contentH1Clone.getBoundingClientRect().top - | ||||
|             this.contentH1.parentElement.getBoundingClientRect().top + | ||||
|             parseInt(getComputedStyle(this.contentH1.parentElement).marginTop || '0', 10)) + 'px', | ||||
|         ); | ||||
|         this.contentH1.style.setProperty('position', 'absolute'); | ||||
| 
 | ||||
|         this.setupContent(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Setup content scroll. | ||||
|      * | ||||
|      * @param parentId Parent id to recalculate content | ||||
|      * @param retries  Retries to find content in case it's loading. | ||||
|      */ | ||||
|     async setupContent(parentId?: string, retries = 5): Promise<void> { | ||||
|         if (parentId) { | ||||
|             this.content = this.header.parentElement?.querySelector(`#${parentId} ion-content:not(.disable-scroll-y)`); | ||||
|             this.inContent = false; | ||||
|             if (!this.content && retries > 0) { | ||||
|                 await CoreUtils.nextTick(); | ||||
|                 await this.setupContent(parentId, --retries); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             this.onScroll(this.content?.scrollTop || 0); | ||||
|         } | ||||
| 
 | ||||
|         if (!this.title || !this.content) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Add something under the hood to change the page background.
 | ||||
|         let color = getComputedStyle(title).getPropertyValue('backgroundColor').trim(); | ||||
|         let color = getComputedStyle(this.title).getPropertyValue('backgroundColor').trim(); | ||||
|         if (color == '') { | ||||
|             color = getComputedStyle(title).getPropertyValue('--background').trim(); | ||||
|             color = getComputedStyle(this.title).getPropertyValue('--background').trim(); | ||||
|         } | ||||
| 
 | ||||
|         const underHeader = document.createElement('div'); | ||||
|         underHeader.classList.add('core-underheader'); | ||||
|         underHeader.style.setProperty('height', this.header.clientHeight + 'px'); | ||||
|         underHeader.style.setProperty('background', color); | ||||
|         this.content.shadowRoot?.querySelector('#background-content')?.prepend(underHeader); | ||||
| 
 | ||||
|         this.content.style.setProperty('--offset-top', this.header.clientHeight + 'px'); | ||||
| 
 | ||||
|         // Subheading.
 | ||||
|         const headerSubHeading = this.header.querySelector<HTMLElement>('h2,.subheading'); | ||||
|         const contentSubHeading = title.querySelector<HTMLElement>('h2,.subheading'); | ||||
|         if (headerSubHeading && contentSubHeading) { | ||||
|             const headerSubHeadingStyles = getComputedStyle(headerSubHeading); | ||||
|             this.headerSubHeadingFontSize = parseFloat(headerSubHeadingStyles.fontSize); | ||||
| 
 | ||||
|             const contentSubHeadingStyles = getComputedStyle(contentSubHeading); | ||||
|             this.contentSubHeadingFontSize = parseFloat(contentSubHeadingStyles.fontSize); | ||||
| 
 | ||||
|             if (Platform.isRTL) { | ||||
|                 this.subHeadingStartDifference = contentSubHeading.getBoundingClientRect().right - | ||||
|                     (headerSubHeading.getBoundingClientRect().right - parseFloat(headerSubHeadingStyles.paddingRight)); | ||||
|             } else { | ||||
|                 this.subHeadingStartDifference = contentSubHeading.getBoundingClientRect().left - | ||||
|                     (headerSubHeading.getBoundingClientRect().left + parseFloat(headerSubHeadingStyles.paddingLeft)); | ||||
|         if (this.inContent) { | ||||
|             this.content.shadowRoot?.querySelector('#background-content')?.prepend(underHeader); | ||||
|             this.content.style.setProperty('--offset-top', this.header.clientHeight + 'px'); | ||||
|         } else { | ||||
|             if (!this.header.closest('.ion-page')?.querySelector('.core-underheader')) { | ||||
|                 this.header.closest('.ion-page')?.insertBefore(underHeader, this.header); | ||||
|             } | ||||
| 
 | ||||
|             contentSubHeading.setAttribute('aria-hidden', 'true'); | ||||
|         } | ||||
| 
 | ||||
|         this.content.scrollEvents = true; | ||||
|         this.content.addEventListener('ionScroll', (e: CustomEvent<ScrollDetail>): void => { | ||||
|             if (e.target == this.content) { | ||||
|                 this.onScroll(title, contentH1, contentSubHeading, e.detail); | ||||
|                 this.onScroll(e.detail.scrollTop); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| @ -224,54 +270,45 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | ||||
|     /** | ||||
|      * On scroll function. | ||||
|      * | ||||
|      * @param title Title on ion content. | ||||
|      * @param contentH1 Heading 1 of title, if found. | ||||
|      * @param scrollDetail Event details. | ||||
|      * @param scrollTop Scroll top measure. | ||||
|      */ | ||||
|     protected onScroll( | ||||
|         title: HTMLElement, | ||||
|         contentH1: HTMLElement, | ||||
|         contentSubheading: HTMLElement | null, | ||||
|         scrollDetail: ScrollDetail, | ||||
|         scrollTop: number, | ||||
|     ): void { | ||||
|         const progress = CoreMath.clamp(scrollDetail.scrollTop / this.titleTopDifference, 0, 1); | ||||
|         if (!this.title || !this.contentH1) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const progress = CoreMath.clamp(scrollTop / this.titleTopDifference, 0, 1); | ||||
|         const collapsed = progress >= 1; | ||||
| 
 | ||||
|         if (!this.inContent) { | ||||
|             this.title.style.transform = 'translateY(-' + scrollTop + 'px)'; | ||||
|             const height = this.titleHeight - scrollTop; | ||||
|             this.title.style.height = (height > 0 ? height : 0) + 'px'; | ||||
|         } | ||||
| 
 | ||||
|         // Check total collapse.
 | ||||
|         this.header.classList.toggle('core-header-collapsed', collapsed); | ||||
|         title.classList.toggle('collapsible-title-collapsed', collapsed); | ||||
|         title.classList.toggle('collapsible-title-collapse-started', scrollDetail.scrollTop > 0); | ||||
|         title.classList.toggle('collapsible-title-collapse-nowrap', progress > 0.5); | ||||
|         title.style.setProperty('--collapse-opacity', (1 - progress) +''); | ||||
|         this.title.classList.toggle('collapsible-title-collapsed', collapsed); | ||||
|         this.title.classList.toggle('collapsible-title-collapse-started', scrollTop > 0); | ||||
|         this.title.classList.toggle('collapsible-title-collapse-nowrap', progress > 0.5); | ||||
|         this.title.style.setProperty('--collapse-opacity', (1 - progress) +''); | ||||
| 
 | ||||
|         if (collapsed) { | ||||
|             contentH1.style.transform = 'translateX(-' + this.h1StartDifference + 'px)'; | ||||
|             contentH1.style.setProperty('font-size', this.headerH1FontSize + 'px'); | ||||
| 
 | ||||
|             if (contentSubheading) { | ||||
|                 contentSubheading.style.transform = 'translateX(-' + this.subHeadingStartDifference + 'px)'; | ||||
|                 contentSubheading.style.setProperty('font-size', this.headerSubHeadingFontSize + 'px'); | ||||
|             } | ||||
|             this.contentH1.style.transform = 'translateX(-' + this.h1StartDifference + 'px)'; | ||||
|             this.contentH1.style.setProperty('font-size', this.headerH1FontSize + 'px'); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Zoom font-size out.
 | ||||
|         const newFontSize = this.contentH1FontSize - ((this.contentH1FontSize - this.headerH1FontSize) * progress); | ||||
|         contentH1.style.setProperty('font-size', newFontSize + 'px'); | ||||
|         this.contentH1.style.setProperty('font-size', newFontSize + 'px'); | ||||
| 
 | ||||
|         // Move.
 | ||||
|         const newStart = - this.h1StartDifference * progress; | ||||
|         contentH1.style.transform = 'translateX(' + newStart + 'px)'; | ||||
| 
 | ||||
|         if (contentSubheading) { | ||||
|             const newFontSize = this.contentSubHeadingFontSize - | ||||
|                 ((this.contentSubHeadingFontSize - this.headerSubHeadingFontSize) * progress); | ||||
|             contentSubheading.style.setProperty('font-size', newFontSize + 'px'); | ||||
| 
 | ||||
|             const newStart = - this.subHeadingStartDifference * progress; | ||||
|             contentSubheading.style.transform = 'translateX(' + newStart + 'px)'; | ||||
|         } | ||||
|         this.contentH1.style.transform = 'translateX(' + newStart + 'px)'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -4,6 +4,7 @@ | ||||
|     @include position(50%, 0px, null, null); | ||||
|     position: fixed; | ||||
|     z-index: 10; | ||||
|     transform: translateY(-50%); | ||||
| 
 | ||||
|     ion-button { | ||||
|         margin: 0; | ||||
|  | ||||
| @ -10,30 +10,6 @@ | ||||
|     <!-- Default course format. --> | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
| 
 | ||||
|         <!-- Course summary. By default we only display the course progress. --> | ||||
|         <core-dynamic-component [component]="courseSummaryComponent" [data]="data"> | ||||
|             <ion-item lines="full" class="core-format-progress-list ion-text-wrap"> | ||||
|                 <ion-avatar slot="start" class="core-course-thumb" *ngIf="imageThumb"> | ||||
|                     <img [src]="imageThumb" core-external-content alt="" /> | ||||
|                 </ion-avatar> | ||||
|                 <ion-label> | ||||
|                     <p *ngIf="course.categoryname"> | ||||
|                         <core-format-text [text]="course.categoryname" contextLevel="coursecat" [contextInstanceId]="course.categoryid"> | ||||
|                         </core-format-text> | ||||
|                     </p> | ||||
|                     <p class="item-heading">{{ course.displayname || course.fullname }}</p> | ||||
|                     <div class="core-course-progress" *ngIf="progress !== undefined"> | ||||
|                         <core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress"> | ||||
|                         </core-progress-bar> | ||||
|                     </div> | ||||
|                 </ion-label> | ||||
|                 <ion-button fill="clear" slot="end" (click)="openCourseSummary()" [attr.aria-label]="'core.course.coursesummary' |translate" | ||||
|                     color="dark"> | ||||
|                     <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|                 </ion-button> | ||||
|             </ion-item> | ||||
|         </core-dynamic-component> | ||||
| 
 | ||||
|         <!-- Single section. --> | ||||
|         <div *ngIf="selectedSection && selectedSection.id != allSectionsId"> | ||||
|             <core-dynamic-component [component]="singleSectionComponent" [data]="data"> | ||||
| @ -72,15 +48,15 @@ | ||||
|                 <ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
| 
 | ||||
|         <core-block-side-blocks-button *ngIf="course && displayBlocks && hasBlocks" [courseId]="course.id"> | ||||
|         </core-block-side-blocks-button> | ||||
| 
 | ||||
|     </core-loading> | ||||
| </core-dynamic-component> | ||||
| 
 | ||||
| 
 | ||||
| <core-block-side-blocks-button slot="fixed" *ngIf="loaded && course && displayBlocks && hasBlocks" [courseId]="course.id"> | ||||
| </core-block-side-blocks-button> | ||||
| 
 | ||||
| <!-- Course Index button. --> | ||||
| <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="displayCourseIndex"> | ||||
| <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="loaded && displayCourseIndex"> | ||||
|     <ion-fab-button (click)="openCourseIndex()" [attr.aria-label]="'core.course.courseindex' | translate"> | ||||
|         <ion-icon name="fas-list-ul" aria-hidden="true"></ion-icon> | ||||
|         <span class="sr-only">{{'core.course.courseindex' | translate }}</span> | ||||
| @ -121,8 +97,7 @@ | ||||
| 
 | ||||
|         <ng-container *ngFor="let module of section.modules"> | ||||
|             <core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section" | ||||
|                 (completionChanged)="onCompletionChange($event)" [showActivityDates]="course.showactivitydates" | ||||
|                 [showCompletionConditions]="course.showcompletionconditions"> | ||||
|                 [showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions"> | ||||
|             </core-course-module> | ||||
|         </ng-container> | ||||
|     </section> | ||||
|  | ||||
| @ -1,55 +1,11 @@ | ||||
| @import '~theme/globals.scss'; | ||||
| .core-course-section-nav-buttons { | ||||
|     display: flex; | ||||
|     justify-content: flex-end; | ||||
| 
 | ||||
| :host { | ||||
| 
 | ||||
|     .core-format-progress-list { | ||||
|         margin-bottom: 0; | ||||
| 
 | ||||
|         .item { | ||||
|             background: transparent; | ||||
| 
 | ||||
|             .label { | ||||
|                 margin-top: 0; | ||||
|                 margin-bottom: 0; | ||||
|             } | ||||
|         } | ||||
|     core-format-text { | ||||
|         white-space: nowrap; | ||||
|         text-overflow: ellipsis; | ||||
|         overflow: hidden; | ||||
|         text-transform: none; | ||||
|     } | ||||
| 
 | ||||
|     .core-course-thumb { | ||||
|         height: var(--core-courseimage-on-course-size); | ||||
|         width: var(--core-courseimage-on-course-size); | ||||
|     } | ||||
| 
 | ||||
|     @if ($core-show-courseimage-on-course) { | ||||
|         .core-course-thumb { | ||||
|             display: block; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @if ($core-hide-progress-on-course) { | ||||
|         .core-course-progress { | ||||
|             display: none; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     .core-button-selector-row { | ||||
|         display: flex; | ||||
|         core-combobox { | ||||
|             flex-grow: 1; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .core-course-section-nav-buttons { | ||||
|         display: flex; | ||||
|         justify-content: flex-end; | ||||
| 
 | ||||
|         core-format-text { | ||||
|             white-space: nowrap; | ||||
|             text-overflow: ellipsis; | ||||
|             overflow: hidden; | ||||
|             text-transform: none; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -68,7 +68,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     static readonly LOAD_MORE_ACTIVITIES = 20; // How many activities should load each time showMoreActivities is called.
 | ||||
| 
 | ||||
|     @Input() course!: CoreCourseAnyCourseData; // The course to render.
 | ||||
|     @Input() sections?: CoreCourseSection[]; // List of course sections.
 | ||||
|     @Input() sections: CoreCourseSectionWithStatus[] = []; // List of course sections.
 | ||||
|     @Input() initialSectionId?: number; // The section to load first (by ID).
 | ||||
|     @Input() initialSectionNumber?: number; // The section to load first (by number).
 | ||||
|     @Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
 | ||||
| @ -95,10 +95,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID; | ||||
|     stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID; | ||||
|     loaded = false; | ||||
|     imageThumb?: string; | ||||
|     progress?: number; | ||||
| 
 | ||||
|     protected selectTabObserver?: CoreEventObserver; | ||||
|     protected completionObserver?: CoreEventObserver; | ||||
|     protected lastCourseFormat?: string; | ||||
| 
 | ||||
|     constructor( | ||||
| @ -139,6 +139,37 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|                 this.sectionChanged(section); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         // The completion of any of the modules have changed.
 | ||||
|         this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => { | ||||
|             if (data.completion.courseId != this.course.id) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Emit a new event for other components.
 | ||||
|             this.completionChanged.emit(data.completion); | ||||
| 
 | ||||
|             if (data.completion.valueused !== false || !this.course || !('progress' in this.course) || | ||||
|                     typeof this.course.progress != 'number') { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // If the completion value is not used, the page won't be reloaded, so update the progress bar.
 | ||||
|             const completionModules = (<CoreCourseModuleData[]> []) | ||||
|                 .concat(...this.sections.map((section) => section.modules)) | ||||
|                 .map((module) => module.completion && module.completion > 0 ? 1 : module.completion) | ||||
|                 .reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0); | ||||
| 
 | ||||
|             const moduleProgressPercent = 100 / (completionModules || 1); | ||||
|             // Use min/max here to avoid floating point rounding errors over/under-flowing the progress bar.
 | ||||
|             if (data.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { | ||||
|                 this.course.progress = Math.min(100, this.course.progress + moduleProgressPercent); | ||||
|             } else { | ||||
|                 this.course.progress = Math.max(0, this.course.progress - moduleProgressPercent); | ||||
|             } | ||||
| 
 | ||||
|             this.updateProgress(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -157,10 +188,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|             this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id); | ||||
| 
 | ||||
|             this.updateProgress(); | ||||
| 
 | ||||
|             if ('overviewfiles' in this.course) { | ||||
|                 this.imageThumb = this.course.overviewfiles?.[0]?.fileurl; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (changes.sections && this.sections) { | ||||
| @ -246,8 +273,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|         const hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections); | ||||
| 
 | ||||
|         if (this.selectedSection) { | ||||
|             const selectedSection = this.selectedSection; | ||||
|             // We have a selected section, but the list has changed. Search the section in the list.
 | ||||
|             let newSection = sections.find(section => this.compareSections(section, this.selectedSection!)); | ||||
|             let newSection = sections.find(section => this.compareSections(section, selectedSection)); | ||||
| 
 | ||||
|             if (!newSection) { | ||||
|                 // Section not found, calculate which one to use.
 | ||||
| @ -317,22 +345,22 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
| 
 | ||||
|         if (newSection.id != this.allSectionsId) { | ||||
|             // Select next and previous sections to show the arrows.
 | ||||
|             const i = this.sections!.findIndex((value) => this.compareSections(value, this.selectedSection!)); | ||||
|             const i = this.sections.findIndex((value) => this.compareSections(value, newSection)); | ||||
| 
 | ||||
|             let j: number; | ||||
|             for (j = i - 1; j >= 1; j--) { | ||||
|                 if (this.canViewSection(this.sections![j])) { | ||||
|                 if (this.canViewSection(this.sections[j])) { | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|             this.previousSection = j >= 1 ? this.sections![j] : undefined; | ||||
|             this.previousSection = j >= 1 ? this.sections[j] : undefined; | ||||
| 
 | ||||
|             for (j = i + 1; j < this.sections!.length; j++) { | ||||
|                 if (this.canViewSection(this.sections![j])) { | ||||
|             for (j = i + 1; j < this.sections.length; j++) { | ||||
|                 if (this.canViewSection(this.sections[j])) { | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|             this.nextSection = j < this.sections!.length ? this.sections![j] : undefined; | ||||
|             this.nextSection = j < this.sections.length ? this.sections[j] : undefined; | ||||
|         } else { | ||||
|             this.previousSection = undefined; | ||||
|             this.nextSection = undefined; | ||||
| @ -463,7 +491,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component destroyed. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.selectTabObserver && this.selectTabObserver.off(); | ||||
| @ -498,35 +526,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|                 section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The completion of any of the modules have changed. | ||||
|      */ | ||||
|     onCompletionChange(completionData: CoreCourseModuleCompletionData): void { | ||||
|         // Emit a new event for other components.
 | ||||
|         this.completionChanged.emit(completionData); | ||||
| 
 | ||||
|         if (completionData.valueused !== false || !this.course || !('progress' in this.course) || | ||||
|                 typeof this.course.progress != 'number') { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // If the completion value is not used, the page won't be reloaded, so update the progress bar.
 | ||||
|         const completionModules = (<CoreCourseModuleData[]> []) | ||||
|             .concat(...this.sections!.map((section) => section.modules)) | ||||
|             .map((module) => module.completion && module.completion > 0 ? 1 : module.completion) | ||||
|             .reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0); | ||||
| 
 | ||||
|         const moduleProgressPercent = 100 / (completionModules || 1); | ||||
|         // Use min/max here to avoid floating point rounding errors over/under-flowing the progress bar.
 | ||||
|         if (completionData.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { | ||||
|             this.course.progress = Math.min(100, this.course.progress + moduleProgressPercent); | ||||
|         } else { | ||||
|             this.course.progress = Math.max(0, this.course.progress - moduleProgressPercent); | ||||
|         } | ||||
| 
 | ||||
|         this.updateProgress(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update course progress. | ||||
|      */ | ||||
| @ -546,14 +545,4 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|         this.progress = this.course.progress; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Open the course summary | ||||
|      */ | ||||
|     openCourseSummary(): void { | ||||
|         CoreNavigator.navigateToSitePath( | ||||
|             '/course/' + this.course.id + '/preview', | ||||
|             { params: { course: this.course, avoidOpenCourse: true } }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -12,14 +12,19 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component } from '@angular/core'; | ||||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreUser } from '@features/user/services/user'; | ||||
| import { CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking } from '@features/course/services/course'; | ||||
| import { | ||||
|     CoreCourseCompletionType, | ||||
|     CoreCourseModuleCompletionStatus, | ||||
|     CoreCourseModuleCompletionTracking, | ||||
| } from '@features/course/services/course'; | ||||
| import { CoreFilterHelper } from '@features/filter/services/filter-helper'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion'; | ||||
| import { CoreCourseHelper } from '@features/course/services/course-helper'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to handle activity completion in sites previous to 3.11. | ||||
| @ -35,11 +40,29 @@ import { CoreCourseHelper } from '@features/course/services/course-helper'; | ||||
|     templateUrl: 'core-course-module-completion-legacy.html', | ||||
|     styleUrls: ['module-completion-legacy.scss'], | ||||
| }) | ||||
| export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleCompletionBaseComponent { | ||||
| export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleCompletionBaseComponent | ||||
|     implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     completionImage?: string; | ||||
|     completionDescription?: string; | ||||
| 
 | ||||
|     protected completionObserver?: CoreEventObserver; | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => { | ||||
|             if (!this.completion || this.completion.cmid != data.completion.cmid && data.type != CoreCourseCompletionType.MANUAL) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             this.completion = data.completion; | ||||
|             this.calculateData(); | ||||
|             this.completionChanged.emit(this.completion); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
| @ -126,9 +149,16 @@ export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleC | ||||
| 
 | ||||
|         await CoreCourseHelper.changeManualCompletion(this.completion, event); | ||||
| 
 | ||||
|         this.calculateData(); | ||||
|         // @deprecated MANUAL_COMPLETION_CHANGED is deprecated since 4.0 use COMPLETION_CHANGED instead.
 | ||||
|         CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion }); | ||||
|         CoreEvents.trigger(CoreEvents.COMPLETION_CHANGED, { completion: this.completion, type: CoreCourseCompletionType.MANUAL }); | ||||
|     } | ||||
| 
 | ||||
|         this.completionChanged.emit(this.completion); | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.completionObserver?.off(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -13,6 +13,7 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange } from '@angular/core'; | ||||
| import { CoreCourseCompletionType } from '@features/course/services/course'; | ||||
| 
 | ||||
| import { CoreCourseHelper, CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; | ||||
| import { CoreUser } from '@features/user/services/user'; | ||||
| @ -34,14 +35,14 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan | ||||
| 
 | ||||
|     accessibleDescription: string | null = null; | ||||
| 
 | ||||
|     protected manualChangedObserver?: CoreEventObserver; | ||||
|     protected completionObserver?: CoreEventObserver; | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.manualChangedObserver = CoreEvents.on(CoreEvents.MANUAL_COMPLETION_CHANGED, (data) => { | ||||
|             if (!this.completion || this.completion.cmid != data.completion.cmid) { | ||||
|         this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => { | ||||
|             if (!this.completion || this.completion.cmid != data.completion.cmid && data.type != CoreCourseCompletionType.MANUAL) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
| @ -98,14 +99,16 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan | ||||
| 
 | ||||
|         await CoreCourseHelper.changeManualCompletion(this.completion, event); | ||||
| 
 | ||||
|         // @deprecated MANUAL_COMPLETION_CHANGED is deprecated since 4.0 use COMPLETION_CHANGED instead.
 | ||||
|         CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion }); | ||||
|         CoreEvents.trigger(CoreEvents.COMPLETION_CHANGED, { completion: this.completion, type: CoreCourseCompletionType.MANUAL }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.manualChangedObserver?.off(); | ||||
|         this.completionObserver?.off(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -15,7 +15,8 @@ | ||||
| 
 | ||||
|     <core-loading [hideUntil]="dataLoaded"> | ||||
|         <core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId" [initialSectionNumber]="sectionNumber" | ||||
|             [moduleId]="moduleId" (completionChanged)="onCompletionChange($event)" class="core-course-format-{{course.format}}"> | ||||
|             [moduleId]="moduleId" (completionChanged)="onCompletionChange($event)" class="core-course-format-{{course.format}}" | ||||
|             *ngIf="dataLoaded"> | ||||
|         </core-course-format> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ion-header> | ||||
| <ion-header collapsible> | ||||
|     <ion-toolbar> | ||||
|         <ion-buttons slot="start"> | ||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||
| @ -12,4 +12,31 @@ | ||||
|         <ion-buttons slot="end"></ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <core-tabs-outlet [tabs]="tabs" [hideUntil]="loaded" (ionChange)="tabSelected()"></core-tabs-outlet> | ||||
| <ion-item lines="full" class="core-format-progress-list ion-text-wrap collapsible-title"> | ||||
|     <ion-avatar slot="start" class="core-course-thumb" *ngIf="imageThumb"> | ||||
|         <img [src]="imageThumb" core-external-content alt="" /> | ||||
|     </ion-avatar> | ||||
|     <ion-label> | ||||
|         <ion-row> | ||||
|             <ion-col> | ||||
|                 <p *ngIf="category"> | ||||
|                     <core-format-text [text]="category" contextLevel="coursecat" [contextInstanceId]="course!.categoryid"> | ||||
|                     </core-format-text> | ||||
|                 </p> | ||||
|                 <h1>{{ title }}</h1> | ||||
|             </ion-col> | ||||
|             <ion-col size="auto" class="ion-align-self-center"> | ||||
|                 <ion-button fill="clear" (click)="openCourseSummary()" [attr.aria-label]="'core.course.coursesummary' | translate" | ||||
|                     color="dark"> | ||||
|                     <ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|                 </ion-button> | ||||
|             </ion-col> | ||||
|         </ion-row> | ||||
|         <div class="core-course-progress" *ngIf="progress !== undefined"> | ||||
|             <core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress"> | ||||
|             </core-progress-bar> | ||||
|         </div> | ||||
|     </ion-label> | ||||
| 
 | ||||
| </ion-item> | ||||
| <core-tabs-outlet [tabs]="tabs" [hideUntil]="loaded" (ionChange)="tabSelected($event)"></core-tabs-outlet> | ||||
|  | ||||
| @ -17,7 +17,7 @@ import { RouterModule, ROUTES, Routes } from '@angular/router'; | ||||
| 
 | ||||
| import { resolveModuleRoutes } from '@/app/app-routing.module'; | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { CoreCourseIndexPage } from './index.page'; | ||||
| import { CoreCourseIndexPage } from '.'; | ||||
| import { COURSE_INDEX_ROUTES } from './index-routing.module'; | ||||
| 
 | ||||
| function buildRoutes(injector: Injector): Routes { | ||||
|  | ||||
							
								
								
									
										26
									
								
								src/core/features/course/pages/index/index.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/core/features/course/pages/index/index.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| @import '~theme/globals.scss'; | ||||
| 
 | ||||
| :host { | ||||
|     .core-course-thumb { | ||||
|         height: var(--core-courseimage-on-course-size); | ||||
|         min-height: var(--core-courseimage-on-course-size); | ||||
|         width: var(--core-courseimage-on-course-size); | ||||
|         min-width: var(--core-courseimage-on-course-size); | ||||
|     } | ||||
| 
 | ||||
|     @if ($core-show-courseimage-on-course) { | ||||
|         .core-course-thumb { | ||||
|             display: block; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @if ($core-hide-progress-on-course) { | ||||
|         .core-course-progress { | ||||
|             display: none; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     h1 { | ||||
|         font-size: 20px; | ||||
|     } | ||||
| } | ||||
| @ -26,6 +26,8 @@ import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CONTENTS_PAGE_NAME } from '@features/course/course.module'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreCollapsibleHeaderDirective } from '@directives/collapsible-header'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the list of courses the user is enrolled in. | ||||
| @ -33,23 +35,28 @@ import { CONTENTS_PAGE_NAME } from '@features/course/course.module'; | ||||
| @Component({ | ||||
|     selector: 'page-core-course-index', | ||||
|     templateUrl: 'index.html', | ||||
|     styleUrls: ['index.scss'], | ||||
| }) | ||||
| export class CoreCourseIndexPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     @ViewChild(CoreTabsOutletComponent) tabsComponent?: CoreTabsOutletComponent; | ||||
|     @ViewChild(CoreCollapsibleHeaderDirective) ionCollapsibleHeader?: CoreCollapsibleHeaderDirective; | ||||
| 
 | ||||
|     title?: string; | ||||
|     title = ''; | ||||
|     category = ''; | ||||
|     course?: CoreCourseAnyCourseData; | ||||
|     tabs: CourseTab[] = []; | ||||
|     loaded = false; | ||||
|     imageThumb?: string; | ||||
|     progress?: number; | ||||
| 
 | ||||
|     protected currentPagePath = ''; | ||||
|     protected selectTabObserver: CoreEventObserver; | ||||
|     protected firstTabName?: string; | ||||
|     protected module?: CoreCourseModuleData; | ||||
|     protected modParams?: Params; | ||||
|     protected isGuest?: boolean; | ||||
|     protected contentsTab: CoreTabsOutletTab = { | ||||
|     protected isGuest = false; | ||||
|     protected contentsTab: CoreTabsOutletTab & { pageParams: Params } = { | ||||
|         page: CONTENTS_PAGE_NAME, | ||||
|         title: 'core.course', | ||||
|         pageParams: {}, | ||||
| @ -60,10 +67,10 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | ||||
|             if (!data.name) { | ||||
|                 // If needed, set sectionId and sectionNumber. They'll only be used if the content tabs hasn't been loaded yet.
 | ||||
|                 if (data.sectionId) { | ||||
|                     this.contentsTab.pageParams!.sectionId = data.sectionId; | ||||
|                     this.contentsTab.pageParams.sectionId = data.sectionId; | ||||
|                 } | ||||
|                 if (data.sectionNumber) { | ||||
|                     this.contentsTab.pageParams!.sectionNumber = data.sectionNumber; | ||||
|                     this.contentsTab.pageParams.sectionNumber = data.sectionNumber; | ||||
|                 } | ||||
| 
 | ||||
|                 // Select course contents.
 | ||||
| @ -79,7 +86,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         // Increase route depth.
 | ||||
| @ -87,12 +94,19 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|         CoreNavigator.increaseRouteDepth(path.replace(/(\/deep)+/, '')); | ||||
| 
 | ||||
|         // Get params.
 | ||||
|         this.course = CoreNavigator.getRouteParam('course'); | ||||
|         try { | ||||
|             this.course = CoreNavigator.getRequiredRouteParam('course'); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModal(error); | ||||
|             CoreNavigator.back(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.firstTabName = CoreNavigator.getRouteParam('selectedTab'); | ||||
|         this.module = CoreNavigator.getRouteParam<CoreCourseModuleData>('module'); | ||||
|         this.modParams = CoreNavigator.getRouteParam<Params>('modParams'); | ||||
|         this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest'); | ||||
|         this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest'); | ||||
| 
 | ||||
|         this.currentPagePath = CoreNavigator.getCurrentPath(); | ||||
|         this.contentsTab.page = CoreTextUtils.concatenatePaths(this.currentPagePath, this.contentsTab.page); | ||||
| @ -104,7 +118,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | ||||
|         }; | ||||
| 
 | ||||
|         if (this.module) { | ||||
|             this.contentsTab.pageParams!.moduleId = this.module.id; | ||||
|             this.contentsTab.pageParams.moduleId = this.module.id; | ||||
|         } | ||||
| 
 | ||||
|         this.tabs.push(this.contentsTab); | ||||
| @ -112,20 +126,23 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|         await Promise.all([ | ||||
|             this.loadCourseHandlers(), | ||||
|             this.loadTitle(), | ||||
|             this.loadBasinInfo(), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * A tab was selected. | ||||
|      */ | ||||
|     tabSelected(): void { | ||||
|         if (this.module) { | ||||
|             // Now that the first tab has been selected we can load the module.
 | ||||
|             CoreCourseHelper.openModule(this.module, this.course!.id, this.contentsTab.pageParams!.sectionId, this.modParams); | ||||
|     tabSelected(tabToSelect: CoreTabsOutletTab): void { | ||||
|         this.ionCollapsibleHeader?.setupContent(tabToSelect.id); | ||||
| 
 | ||||
|             delete this.module; | ||||
|         if (!this.module || !this.course) { | ||||
|             return; | ||||
|         } | ||||
|         // Now that the first tab has been selected we can load the module.
 | ||||
|         CoreCourseHelper.openModule(this.module, this.course.id, this.contentsTab.pageParams.sectionId, this.modParams); | ||||
| 
 | ||||
|         delete this.module; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -134,8 +151,12 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadCourseHandlers(): Promise<void> { | ||||
|         if (!this.course) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Load the course handlers.
 | ||||
|         const handlers = await CoreCourseOptionsDelegate.getHandlersToDisplay(this.course!, false, this.isGuest); | ||||
|         const handlers = await CoreCourseOptionsDelegate.getHandlersToDisplay(this.course, false, this.isGuest); | ||||
| 
 | ||||
|         let tabToLoad: number | undefined; | ||||
| 
 | ||||
| @ -169,23 +190,34 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadTitle(): Promise<void> { | ||||
|     protected async loadBasinInfo(): Promise<void> { | ||||
|         if (!this.course) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Get the title to display initially.
 | ||||
|         this.title = CoreCourseFormatDelegate.getCourseTitle(this.course!); | ||||
|         this.title = CoreCourseFormatDelegate.getCourseTitle(this.course); | ||||
|         this.category = 'categoryname' in this.course ? this.course.categoryname : ''; | ||||
| 
 | ||||
|         if ('overviewfiles' in this.course) { | ||||
|             this.imageThumb = this.course.overviewfiles?.[0]?.fileurl; | ||||
|         } | ||||
| 
 | ||||
|         this.updateProgress(); | ||||
| 
 | ||||
|         // Load sections.
 | ||||
|         const sections = await CoreUtils.ignoreErrors(CoreCourse.getSections(this.course!.id, false, true)); | ||||
|         const sections = await CoreUtils.ignoreErrors(CoreCourse.getSections(this.course.id, false, true)); | ||||
| 
 | ||||
|         if (!sections) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Get the title again now that we have sections.
 | ||||
|         this.title = CoreCourseFormatDelegate.getCourseTitle(this.course!, sections); | ||||
|         this.title = CoreCourseFormatDelegate.getCourseTitle(this.course, sections); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Page destroyed. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         const path = CoreNavigator.getRouteFullPath(this.route.snapshot); | ||||
| @ -208,6 +240,39 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | ||||
|         this.tabsComponent?.ionViewDidLeave(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Open the course summary | ||||
|      */ | ||||
|     openCourseSummary(): void { | ||||
|         if (!this.course) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         CoreNavigator.navigateToSitePath( | ||||
|             '/course/' + this.course.id + '/preview', | ||||
|             { params: { course: this.course, avoidOpenCourse: true } }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update course progress. | ||||
|      */ | ||||
|     protected updateProgress(): void { | ||||
|         if ( | ||||
|             !this.course || | ||||
|                 !('progress' in this.course) || | ||||
|                 typeof this.course.progress !== 'number' || | ||||
|                 this.course.progress < 0 || | ||||
|                 this.course.completionusertracked === false | ||||
|         ) { | ||||
|             this.progress = undefined; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.progress = this.course.progress; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type CourseTab = CoreTabsOutletTab & { | ||||
| @ -71,6 +71,11 @@ export enum CoreCourseModuleCompletionStatus { | ||||
|     COMPLETION_COMPLETE_FAIL = 3, | ||||
| } | ||||
| 
 | ||||
| export enum CoreCourseCompletionType { | ||||
|     MANUAL = 0, | ||||
|     AUTO = 1, | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Completion tracking valid values. | ||||
|  */ | ||||
|  | ||||
| @ -267,8 +267,8 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm | ||||
|      * @param sections List of sections. | ||||
|      * @return Course title. | ||||
|      */ | ||||
|     getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseWSSection[]): string | undefined { | ||||
|         return this.executeFunctionOnEnabled(course.format || '', 'getCourseTitle', [course, sections]); | ||||
|     getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseWSSection[]): string { | ||||
|         return this.executeFunctionOnEnabled(course.format || '', 'getCourseTitle', [course, sections]) || ''; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -20,6 +20,7 @@ import { CoreFilepoolComponentFileEventData } from '@services/filepool'; | ||||
| import { CoreNavigationOptions } from '@services/navigator'; | ||||
| import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; | ||||
| import { CoreScreenOrientation } from '@services/screen'; | ||||
| import { CoreCourseCompletionType } from '@features/course/services/course'; | ||||
| 
 | ||||
| /** | ||||
|  * Observer instance to stop listening to an event. | ||||
| @ -48,6 +49,7 @@ export interface CoreEventsData { | ||||
|     [CoreEvents.SELECT_COURSE_TAB]: CoreEventSelectCourseTabData; | ||||
|     [CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData; | ||||
|     [CoreEvents.MANUAL_COMPLETION_CHANGED]: CoreEventManualCompletionChangedData; | ||||
|     [CoreEvents.COMPLETION_CHANGED]: CoreEventCompletionChangedData; | ||||
|     [CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData; | ||||
|     [CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData; | ||||
|     [CoreEvents.IAB_LOAD_START]: InAppBrowserEvent; | ||||
| @ -77,7 +79,11 @@ export class CoreEvents { | ||||
|     static readonly SITE_UPDATED = 'site_updated'; | ||||
|     static readonly SITE_DELETED = 'site_deleted'; | ||||
|     static readonly COMPLETION_MODULE_VIEWED = 'completion_module_viewed'; | ||||
|     /** | ||||
|      * Deprecated on 4.0 use COMPLETION_CHANGED instead. | ||||
|      */ | ||||
|     static readonly MANUAL_COMPLETION_CHANGED = 'manual_completion_changed'; | ||||
|     static readonly COMPLETION_CHANGED = 'completion_changed'; | ||||
|     static readonly USER_DELETED = 'user_deleted'; | ||||
|     static readonly PACKAGE_STATUS_CHANGED = 'package_status_changed'; | ||||
|     static readonly COURSE_STATUS_CHANGED = 'course_status_changed'; | ||||
| @ -347,6 +353,14 @@ export type CoreEventManualCompletionChangedData = { | ||||
|     completion: CoreCourseModuleCompletionData; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data passed to COMPLETION_CHANGED event. | ||||
|  */ | ||||
| export type CoreEventCompletionChangedData = { | ||||
|     completion: CoreCourseModuleCompletionData; | ||||
|     type: CoreCourseCompletionType; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data passed to SECTION_STATUS_CHANGED event. | ||||
|  */ | ||||
|  | ||||
| @ -1225,6 +1225,13 @@ ion-grid.core-no-grid > ion-row { | ||||
|     display: block; | ||||
| } | ||||
| 
 | ||||
| .core-underheader { | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
| } | ||||
| 
 | ||||
| ion-header[collapsible] { | ||||
|     @include core-transition(all, 500ms); | ||||
| 
 | ||||
| @ -1247,7 +1254,14 @@ ion-header[collapsible] { | ||||
| 
 | ||||
| .collapsible-title { | ||||
|     overflow: visible; | ||||
|     *, h1, h2, .subheading { | ||||
|     --inner-padding-top: 0px; | ||||
|     --padding-top: 0px; | ||||
| 
 | ||||
|     ion-label { | ||||
|         margin-top: 0px; | ||||
|     } | ||||
| 
 | ||||
|     *, h1 { | ||||
|         @include core-transition(all, 200ms, linear); | ||||
|     } | ||||
| 
 | ||||
| @ -1255,9 +1269,12 @@ ion-header[collapsible] { | ||||
|         overflow: visible !important; | ||||
|     } | ||||
| 
 | ||||
|     h1, h2, .subheading { | ||||
|     h1 { | ||||
|         --max-width: none; | ||||
|     } | ||||
|     h1.cloned { | ||||
|         opacity: 0 !important; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| ion-app.ios .collapsible-title h1 { | ||||
| @ -1269,7 +1286,7 @@ ion-app.md .collapsible-title h1 { | ||||
| } | ||||
| 
 | ||||
| .collapsible-title.collapsible-title-collapsed { | ||||
|     ion-label, h1, h2, ion-row, ion-col, .subheading { | ||||
|     ion-label, h1, ion-row, ion-col { | ||||
|         opacity: 0; | ||||
|     } | ||||
| } | ||||
| @ -1279,13 +1296,13 @@ ion-app.md .collapsible-title h1 { | ||||
|         opacity: var(--collapse-opacity, 0); | ||||
|     } | ||||
| 
 | ||||
|     ion-label, h1, h2, ion-row, ion-col, .subheading { | ||||
|     ion-label, h1, ion-row, ion-col, .subheading { | ||||
|         opacity: 1; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .collapsible-title.collapsible-title-collapse-nowrap { | ||||
|     h1, h2, .subheading { | ||||
|     h1:not(.cloned) { | ||||
|         max-width: var(--max-width); | ||||
|         white-space: nowrap; | ||||
|         overflow: hidden; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user