forked from EVOgeek/Vmeda.Online
		
	
						commit
						49afa105bc
					
				| @ -1536,19 +1536,23 @@ | ||||
|   "core.course.confirmpartialdownloadsize": "local_moodlemobileapp", | ||||
|   "core.course.couldnotloadsectioncontent": "local_moodlemobileapp", | ||||
|   "core.course.couldnotloadsections": "local_moodlemobileapp", | ||||
|   "core.course.courseindex": "courseformat", | ||||
|   "core.course.coursesummary": "moodle", | ||||
|   "core.course.done": "completion", | ||||
|   "core.course.downloadcourse": "tool_mobile", | ||||
|   "core.course.downloadcoursesprogressdescription": "local_moodlemobileapp", | ||||
|   "core.course.downloadsectionprogressdescription": "local_moodlemobileapp", | ||||
|   "core.course.errordownloadingcourse": "local_moodlemobileapp", | ||||
|   "core.course.errordownloadingsection": "local_moodlemobileapp", | ||||
|   "core.course.errorgetmodule": "local_moodlemobileapp", | ||||
|   "core.course.failed": "completion", | ||||
|   "core.course.gotonextactivity": "local_moodlemobileapp", | ||||
|   "core.course.gotonextactivitynotfound": "local_moodlemobileapp", | ||||
|   "core.course.gotopreviousactivity": "local_moodlemobileapp", | ||||
|   "core.course.gotopreviousactivitynotfound": "local_moodlemobileapp", | ||||
|   "core.course.hiddenfromstudents": "moodle", | ||||
|   "core.course.hiddenoncoursepage": "moodle", | ||||
|   "core.course.highlighted": "moodle", | ||||
|   "core.course.insufficientavailablequota": "local_moodlemobileapp", | ||||
|   "core.course.insufficientavailablespace": "local_moodlemobileapp", | ||||
|   "core.course.manualcompletionnotsynced": "local_moodlemobileapp", | ||||
| @ -1557,7 +1561,8 @@ | ||||
|   "core.course.overriddennotice": "grades", | ||||
|   "core.course.refreshcourse": "local_moodlemobileapp", | ||||
|   "core.course.section": "moodle", | ||||
|   "core.course.sections": "moodle", | ||||
|   "core.course.thisweek": "format_weeks/currentsection", | ||||
|   "core.course.todo": "completion", | ||||
|   "core.course.useactivityonbrowser": "local_moodlemobileapp", | ||||
|   "core.course.warningmanualcompletionmodified": "local_moodlemobileapp", | ||||
|   "core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp", | ||||
| @ -2257,6 +2262,7 @@ | ||||
|   "core.strftimetime24": "langconfig", | ||||
|   "core.submit": "moodle", | ||||
|   "core.success": "moodle", | ||||
|   "core.summary": "moodle", | ||||
|   "core.tablet": "local_moodlemobileapp", | ||||
|   "core.tag.defautltagcoll": "tag", | ||||
|   "core.tag.errorareanotsupported": "local_moodlemobileapp", | ||||
|  | ||||
| @ -95,7 +95,7 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase | ||||
| 
 | ||||
|         this.getResourceData(module, courseId, handlerData).then((extra) => { | ||||
|             handlerData.extraBadge = extra; | ||||
|             handlerData.extraBadgeColor = 'light'; | ||||
|             handlerData.extraBadgeColor = ''; | ||||
| 
 | ||||
|             return; | ||||
|         }).catch(() => { | ||||
|  | ||||
| @ -93,6 +93,7 @@ export const moodleTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp | ||||
|     } | ||||
| 
 | ||||
|     rootAnimation.addAnimation(enteringContentAnimation); | ||||
|     enteringContentAnimation.beforeAddClass('animating').afterRemoveClass('animating'); | ||||
| 
 | ||||
|     if (backDirection) { | ||||
|         enteringContentAnimation | ||||
| @ -214,6 +215,8 @@ export const moodleTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp | ||||
|     // setup leaving view
 | ||||
|     if (leavingEl) { | ||||
|         const leavingContent = createAnimation(); | ||||
|         leavingContent.beforeAddClass('animating').afterRemoveClass('animating'); | ||||
| 
 | ||||
|         const leavingContentEl = leavingEl.querySelector(':scope > ion-content'); | ||||
|         const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar'); | ||||
|         const leavingHeaderEls = leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *'); | ||||
|  | ||||
| @ -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); | ||||
|         if (this.inContent) { | ||||
|             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.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; | ||||
|  | ||||
| @ -20,7 +20,7 @@ import { CoreCourseFormatComponent } from './format/format'; | ||||
| import { CoreCourseModuleComponent } from './module/module'; | ||||
| import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion'; | ||||
| import { CoreCourseModuleDescriptionComponent } from './module-description/module-description'; | ||||
| import { CoreCourseSectionSelectorComponent } from './section-selector/section-selector'; | ||||
| import { CoreCourseCourseIndexComponent } from './course-index/course-index'; | ||||
| import { CoreCourseTagAreaComponent } from './tag-area/tag-area'; | ||||
| import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module'; | ||||
| import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy'; | ||||
| @ -37,7 +37,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module- | ||||
|         CoreCourseModuleDescriptionComponent, | ||||
|         CoreCourseModuleInfoComponent, | ||||
|         CoreCourseModuleManualCompletionComponent, | ||||
|         CoreCourseSectionSelectorComponent, | ||||
|         CoreCourseCourseIndexComponent, | ||||
|         CoreCourseTagAreaComponent, | ||||
|         CoreCourseUnsupportedModuleComponent, | ||||
|         CoreCourseModuleNavigationComponent, | ||||
| @ -54,7 +54,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module- | ||||
|         CoreCourseModuleDescriptionComponent, | ||||
|         CoreCourseModuleInfoComponent, | ||||
|         CoreCourseModuleManualCompletionComponent, | ||||
|         CoreCourseSectionSelectorComponent, | ||||
|         CoreCourseCourseIndexComponent, | ||||
|         CoreCourseTagAreaComponent, | ||||
|         CoreCourseUnsupportedModuleComponent, | ||||
|         CoreCourseModuleNavigationComponent, | ||||
|  | ||||
| @ -0,0 +1,79 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title> | ||||
|             <h2 id="core-course-section-selector-label">{{ 'core.course.courseindex' | translate }}</h2> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|                 <ion-icon slot="icon-only" name="fas-times" aria-hidden="true"></ion-icon> | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <ion-list id="core-course-section-selector" role="listbox" aria-labelledby="core-course-section-selector-label"> | ||||
|         <ng-container *ngFor="let section of sections"> | ||||
|             <ion-item *ngIf="allSectionId == section.id" class="ion-text-wrap divider" (click)="selectSection($event, section)" button | ||||
|                 [class.item-current]="selectedId === section.id" detail="false"> | ||||
|                 <ion-label> | ||||
|                     <p class="item-heading"> | ||||
|                         <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id"> | ||||
|                         </core-format-text> | ||||
|                     </p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ng-container *ngIf="allSectionId != section.id && !section.hiddenbynumsections && | ||||
|                     section.id != stealthModulesSectionId && section.uservisible !== false"> | ||||
|                 <ion-item class="ion-text-wrap divider section" (click)="selectSection($event, section)" | ||||
|                     [button]="section.visible !== 0 && section.uservisible !== false" [class.item-current]="selectedId === section.id" | ||||
|                     [class.item-dimmed]="section.visible === 0" detail="false" sticky="true"> | ||||
|                     <ion-icon [name]="section.expanded ? 'fas-chevron-down' : 'fas-chevron-right'" flip-rtl slot="start" | ||||
|                         class="expandable-status-icon" (click)="toggleExpand($event, section)" | ||||
|                         [attr.aria-label]="(section.expanded ? 'core.collapse' : 'core.expand') | translate" | ||||
|                         [attr.aria-expanded]="section.expanded" [attr.aria-controls]="'core-course-index-section-' + section.id"> | ||||
|                     </ion-icon> | ||||
|                     <ion-label> | ||||
|                         <p class="item-heading"> | ||||
|                             <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id"> | ||||
|                             </core-format-text> | ||||
|                         </p> | ||||
|                     </ion-label> | ||||
|                     <ion-badge *ngIf="section.highlighted && highlighted">{{highlighted}}</ion-badge> | ||||
|                     <ion-icon name="fas-lock" *ngIf="section.availabilityinfo" slot="end" class="restricted" | ||||
|                         [attr.aria-label]="'core.restricted' | translate"></ion-icon> | ||||
|                 </ion-item> | ||||
|                 <div [hidden]="!section.expanded" [id]="'core-course-index-section-' + section.id"> | ||||
|                     <ng-container *ngIf="section.expanded"> | ||||
|                         <ng-container *ngFor="let module of section.modules"> | ||||
|                             <ion-item [class.item-dimmed]="module.visible === 0" | ||||
|                                 *ngIf="module.visibleoncoursepage !== 0 && !module.noviewlink" | ||||
|                                 (click)="selectModule($event, section, module)" button> | ||||
|                                 <ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined" | ||||
|                                     slot="start" aria-hidden="true"></ion-icon> | ||||
|                                 <ion-icon class="completioninfo completion_incomplete" name="far-circle" | ||||
|                                     *ngIf="module.completionStatus === 0" slot="start" [attr.aria-label]="'core.course.todo' | translate"> | ||||
|                                 </ion-icon> | ||||
|                                 <ion-icon class="completioninfo completion_complete" name="fas-circle" | ||||
|                                     *ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start" | ||||
|                                     [attr.aria-label]="'core.course.done' | translate"> | ||||
|                                 </ion-icon> | ||||
|                                 <ion-icon class="completioninfo completion_fail" name="fas-circle" *ngIf="module.completionStatus === 3" | ||||
|                                     color="danger" slot="start" [attr.aria-label]="'core.course.failed' | translate"> | ||||
|                                 </ion-icon> | ||||
|                                 <ion-label> | ||||
|                                     <p class="item-heading"> | ||||
|                                         <core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id" | ||||
|                                             [courseId]="module.courseid"> | ||||
|                                         </core-format-text> | ||||
|                                     </p> | ||||
|                                 </ion-label> | ||||
|                                 <ion-icon name="fas-lock" *ngIf="module.uservisible === false" slot="end" class="restricted" | ||||
|                                     [attr.aria-label]="'core.restricted' | translate"></ion-icon> | ||||
|                             </ion-item> | ||||
|                         </ng-container> | ||||
|                     </ng-container> | ||||
|                 </div> | ||||
|             </ng-container> | ||||
|         </ng-container> | ||||
|     </ion-list> | ||||
| </ion-content> | ||||
| @ -0,0 +1,35 @@ | ||||
| @import '~theme/globals.scss'; | ||||
| core-progress-bar { | ||||
|     --bar-margin: 8px 0 4px 0; | ||||
|     --line-height: 20px; | ||||
|     --background: var(--contrast-background); | ||||
| } | ||||
| 
 | ||||
| @if ($core-hide-progress-on-section-selector) { | ||||
|     core-progress-bar { | ||||
|         display: none; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| ion-icon.completioninfo { | ||||
|     font-size: 10px; | ||||
|     width: 18px; | ||||
| } | ||||
| 
 | ||||
| ion-item.section::part(native) { | ||||
|     --padding-start: 0; | ||||
| } | ||||
| 
 | ||||
| ion-icon.expandable-status-icon { | ||||
|     margin: 0; | ||||
|     @include padding(12px, 32px, 12px, 16px); | ||||
| } | ||||
| 
 | ||||
| ion-item.item-current ion-icon.expandable-status-icon { | ||||
|     @include padding(null, null, null, 11px); | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| ion-icon.restricted { | ||||
|     font-size: 14px; | ||||
| } | ||||
							
								
								
									
										162
									
								
								src/core/features/course/components/course-index/course-index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								src/core/features/course/components/course-index/course-index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,162 @@ | ||||
| // (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, OnInit, ViewChild } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper'; | ||||
| import { | ||||
|     CoreCourseModuleCompletionStatus, | ||||
|     CoreCourseModuleCompletionTracking, | ||||
|     CoreCourseProvider, | ||||
| } from '@features/course/services/course'; | ||||
| import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { ModalController } from '@singletons'; | ||||
| import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; | ||||
| import { IonContent } from '@ionic/angular'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to display course index modal. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'core-course-course-index', | ||||
|     templateUrl: 'course-index.html', | ||||
|     styleUrls: ['course-index.scss'], | ||||
| }) | ||||
| export class CoreCourseCourseIndexComponent implements OnInit { | ||||
| 
 | ||||
|     @ViewChild(IonContent) content?: IonContent; | ||||
| 
 | ||||
|     @Input() sections?: CourseIndexSection[]; | ||||
|     @Input() selectedId?: number; | ||||
|     @Input() course?: CoreCourseAnyCourseData; | ||||
| 
 | ||||
|     stealthModulesSectionId = CoreCourseProvider.STEALTH_MODULES_SECTION_ID; | ||||
|     allSectionId = CoreCourseProvider.ALL_SECTIONS_ID; | ||||
|     highlighted?: string; | ||||
| 
 | ||||
|     constructor( | ||||
|         protected elementRef: ElementRef, | ||||
|     ) { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
| 
 | ||||
|         if (!this.course || !this.sections || !this.course.enablecompletion || !('courseformatoptions' in this.course) || | ||||
|                 !this.course.courseformatoptions) { | ||||
|             this.closeModal(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const formatOptions = CoreUtils.objectToKeyValueMap(this.course.courseformatoptions, 'name', 'value'); | ||||
| 
 | ||||
|         if (!formatOptions || formatOptions.completionusertracked === false) { | ||||
|             return; | ||||
|         } | ||||
|         const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections); | ||||
|         currentSection.highlighted = true; | ||||
|         if (this.selectedId === undefined) { | ||||
|             currentSection.expanded = true; | ||||
|             this.selectedId = currentSection.id; | ||||
|         } else { | ||||
|             const selectedSection = this.sections.find((section) => section.id == this.selectedId); | ||||
|             if (selectedSection) { | ||||
|                 selectedSection.expanded = true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.sections.forEach((section) => { | ||||
|             section.modules.forEach((module) => { | ||||
|                 module.completionStatus = module.completiondata === undefined || | ||||
|                     module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE | ||||
|                     ? undefined | ||||
|                     : module.completiondata.state; | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course); | ||||
| 
 | ||||
|         setTimeout(() => { | ||||
|             CoreDomUtils.scrollToElementBySelector( | ||||
|                 this.elementRef.nativeElement, | ||||
|                 this.content, | ||||
|                 '.item.item-current', | ||||
|             ); | ||||
|         }, 200); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Toggle expand status. | ||||
|      * | ||||
|      * @param event Event object. | ||||
|      * @param section Section to expand / collapse. | ||||
|      */ | ||||
|     toggleExpand(event: Event, section: CourseIndexSection): void { | ||||
|         section.expanded = !section.expanded; | ||||
|         event.stopPropagation(); | ||||
|         event.preventDefault(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Close the modal. | ||||
|      */ | ||||
|     closeModal(): void { | ||||
|         ModalController.dismiss(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Select a section. | ||||
|      * | ||||
|      * @param event Event. | ||||
|      * @param section Selected section object. | ||||
|      */ | ||||
|     selectSection(event: Event, section: CoreCourseSection): void { | ||||
|         if (section.uservisible !== false) { | ||||
|             ModalController.dismiss({ event, section }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Select a section and open a module | ||||
|      * | ||||
|      * @param event Event. | ||||
|      * @param section Selected section object. | ||||
|      * @param module Selected module object. | ||||
|      */ | ||||
|     selectModule(event: Event,section: CoreCourseSection, module: CoreCourseModuleData): void { | ||||
|         if (module.uservisible !== false) { | ||||
|             ModalController.dismiss({ event, section, module }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type CourseIndexSection = Omit<CoreCourseSection, 'modules'> & { | ||||
|     highlighted?: boolean; | ||||
|     expanded?: boolean; | ||||
|     modules: (CoreCourseModuleData & { | ||||
|         completionStatus?: CoreCourseModuleCompletionStatus; | ||||
|     })[]; | ||||
| }; | ||||
| 
 | ||||
| export type CoreCourseIndexSectionWithModule = { | ||||
|     event: Event; | ||||
|     section: CourseIndexSection; | ||||
|     module?: CoreCourseModuleData; | ||||
| }; | ||||
| @ -1,64 +1,14 @@ | ||||
| <!-- Buttons to add to the header. *ngIf is needed, otherwise the component is executed too soon and doesn't find the header. --> | ||||
| <core-navbar-buttons slot="end" *ngIf="loaded"> | ||||
|     <core-context-menu> | ||||
|         <core-context-menu-item [hidden]="!displaySectionSelector || !sections || !sections.length" [priority]="500" | ||||
|             [content]="'core.course.sections' | translate" (action)="showSectionSelector()" iconAction="menu"> | ||||
|         <core-context-menu-item [hidden]="!displayCourseIndex || !sections || !sections.length" [priority]="500" | ||||
|             [content]="'core.course.courseindex' | translate" (action)="openCourseIndex()" iconAction="menu"> | ||||
|         </core-context-menu-item> | ||||
|     </core-context-menu> | ||||
| </core-navbar-buttons> | ||||
| <core-dynamic-component [component]="courseFormatComponent" [data]="data"> | ||||
|     <!-- Default course format. --> | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
|         <!-- Section selector. --> | ||||
|         <core-dynamic-component [component]="sectionSelectorComponent" [data]="data"> | ||||
|             <div *ngIf="displaySectionSelector && sections && hasSeveralSections" | ||||
|                 class="ion-text-wrap ion-justify-content-between ion-align-items-center core-button-selector-row"> | ||||
|                 <core-combobox [modalOptions]="sectionSelectorModalOptions" interface="modal" listboxId="core-course-section-button" | ||||
|                     icon="fas-folder" [label]="'core.course.section' | translate" | ||||
|                     [selection]="selectedSection ? selectedSection.name : 'core.course.sections' | translate" | ||||
|                     (onChange)="sectionChanged($event)"> | ||||
|                     <span slot="text"> | ||||
|                         <core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course" | ||||
|                             [contextInstanceId]="course?.id" [clean]="true" [singleLine]="true"> | ||||
|                         </core-format-text> | ||||
|                         <ng-container *ngIf="!selectedSection">{{ 'core.course.sections' | translate }}</ng-container> | ||||
|                     </span> | ||||
|                 </core-combobox> | ||||
|             </div> | ||||
|         </core-dynamic-component> | ||||
| 
 | ||||
|         <!-- Course summary. By default we only display the course progress. --> | ||||
|         <core-dynamic-component [component]="courseSummaryComponent" [data]="data"> | ||||
|             <ion-list *ngIf="imageThumb || (selectedSection?.id == allSectionsId && progress !== undefined) || | ||||
|                     (selectedSection && selectedSection.id != allSectionsId && | ||||
|                     (selectedSection.availabilityinfo || selectedSection.visible === 0))" lines="none" class="core-format-progress-list"> | ||||
|                 <div *ngIf="imageThumb" class="core-course-thumb"> | ||||
|                     <img [src]="imageThumb" core-external-content alt="" /> | ||||
|                 </div> | ||||
|                 <ng-container *ngIf="selectedSection"> | ||||
|                     <ion-item class="core-course-progress" *ngIf="selectedSection?.id == allSectionsId && progress !== undefined"> | ||||
|                         <core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress"> | ||||
|                         </core-progress-bar> | ||||
|                     </ion-item> | ||||
|                     <ion-item *ngIf="selectedSection && selectedSection.id != allSectionsId && | ||||
|                         (selectedSection.availabilityinfo || selectedSection.visible === 0)"> | ||||
|                         <ion-badge color="info" class="ion-text-wrap" | ||||
|                             *ngIf="selectedSection.visible === 0 && selectedSection.uservisible !== false"> | ||||
|                             {{ 'core.course.hiddenfromstudents' | translate }} | ||||
|                         </ion-badge> | ||||
|                         <ion-badge color="info" class="ion-text-wrap" | ||||
|                             *ngIf="selectedSection.visible === 0 && selectedSection.uservisible === false"> | ||||
|                             {{ 'core.notavailable' | translate }} | ||||
|                         </ion-badge> | ||||
|                         <ion-badge color="info" class="ion-text-wrap" *ngIf="selectedSection.availabilityinfo"> | ||||
|                             <core-format-text [text]="selectedSection.availabilityinfo" contextLevel="course" | ||||
|                                 [contextInstanceId]="course?.id"> | ||||
|                             </core-format-text> | ||||
|                         </ion-badge> | ||||
|                     </ion-item> | ||||
|                 </ng-container> | ||||
|             </ion-list> | ||||
|         </core-dynamic-component> | ||||
| 
 | ||||
|         <!-- Single section. --> | ||||
|         <div *ngIf="selectedSection && selectedSection.id != allSectionsId"> | ||||
| @ -84,63 +34,75 @@ | ||||
|         </div> | ||||
| 
 | ||||
|         <ion-buttons class="ion-padding core-course-section-nav-buttons safe-area-padding-horizontal" | ||||
|             *ngIf="displaySectionSelector && sections?.length"> | ||||
|             *ngIf="displayCourseIndex && sections?.length"> | ||||
|             <ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" fill="outline" color="primary" | ||||
|                 [attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name"> | ||||
|                 <ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|                 <core-format-text class="sr-only" [text]="previousSection.name" contextLevel="course" [contextInstanceId]="course?.id"> | ||||
|                 <core-format-text class="sr-only" [text]="previousSection.name" contextLevel="course" [contextInstanceId]="course.id"> | ||||
|                 </core-format-text> | ||||
|             </ion-button> | ||||
|             <ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" fill="solid" color="primary" | ||||
|                 [attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name"> | ||||
|                 <core-format-text class="sr-only" [text]="nextSection.name" contextLevel="course" [contextInstanceId]="course?.id"> | ||||
|                 <core-format-text class="sr-only" [text]="nextSection.name" contextLevel="course" [contextInstanceId]="course.id"> | ||||
|                 </core-format-text> | ||||
|                 <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="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> | ||||
|     </ion-fab-button> | ||||
| </ion-fab> | ||||
| 
 | ||||
| <!-- Template to render a section. --> | ||||
| <ng-template #sectionTemplate let-section="section"> | ||||
|     <section *ngIf="!section.hiddenbynumsections && section.id != allSectionsId && section.id != stealthModulesSectionId"> | ||||
|         <!-- Title is only displayed when viewing all sections. --> | ||||
|         <ion-item-divider *ngIf="selectedSection?.id == allSectionsId && section.name" class="ion-text-wrap" color="light" | ||||
|         <ion-item-divider class="course-section ion-text-wrap" color="light" | ||||
|             [class.item-dimmed]="section.visible === 0 || section.uservisible === false"> | ||||
|             <ion-icon name="fas-folder" aria-label="hidden" slot="start"></ion-icon> | ||||
|             <ion-label> | ||||
|                 <h2> | ||||
|                     <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id"> | ||||
|                 <h2 *ngIf="section.name"> | ||||
|                     <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course.id"> | ||||
|                     </core-format-text> | ||||
|                 </h2> | ||||
|                 <p *ngIf="section.visible === 0 || section.availabilityinfo"> | ||||
|                     <ion-badge color="info" *ngIf="section.visible === 0 && section.uservisible !== false" class="ion-text-wrap"> | ||||
|                 <p *ngIf="section.visible === 0 || section.availabilityinfo || (section.highlighted && highlighted)"> | ||||
|                     <ion-badge color="info" *ngIf="section.visible === 0 && section.uservisible !== false" class="ion-text-wrap block"> | ||||
|                         <ion-icon name="fas-eye-slash" aria-hidden="true"></ion-icon> | ||||
|                         {{ 'core.course.hiddenfromstudents' | translate }} | ||||
|                     </ion-badge> | ||||
|                     <ion-badge color="info" *ngIf="section.visible === 0 && section.uservisible === false" class="ion-text-wrap"> | ||||
|                     <ion-badge color="info" *ngIf="section.visible === 0 && section.uservisible === false" class="ion-text-wrap block"> | ||||
|                         <ion-icon name="fas-eye-slash" aria-hidden="true"></ion-icon> | ||||
|                         {{ 'core.notavailable' | translate }} | ||||
|                     </ion-badge> | ||||
|                     <ion-badge color="info" *ngIf="section.availabilityinfo" class="ion-text-wrap"> | ||||
|                         <core-format-text [text]=" section.availabilityinfo" contextLevel="course" [contextInstanceId]="course?.id"> | ||||
|                     <ion-badge color="info" *ngIf="section.availabilityinfo" class="ion-text-wrap block"> | ||||
|                         <ion-icon name="fas-lock" [attr.aria-label]="'core.restricted' | translate"></ion-icon> | ||||
|                         <core-format-text [text]=" section.availabilityinfo" contextLevel="course" [contextInstanceId]="course.id"> | ||||
|                         </core-format-text> | ||||
|                     </ion-badge> | ||||
|                 </p> | ||||
|             </ion-label> | ||||
|             <ion-badge class="ion-text-wrap block" *ngIf="section.highlighted && highlighted" slot="end">{{highlighted}}</ion-badge> | ||||
|         </ion-item-divider> | ||||
| 
 | ||||
|         <ion-item class="ion-text-wrap" *ngIf="section.summary"> | ||||
|             <ion-label> | ||||
|                 <core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="course?.id"> | ||||
|                 <core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="course.id"> | ||||
|                 </core-format-text> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
| 
 | ||||
|         <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,61 +1,4 @@ | ||||
| @import '~theme/globals.scss'; | ||||
| 
 | ||||
| :host { | ||||
| 
 | ||||
|     .core-format-progress-list { | ||||
|         margin-bottom: 0; | ||||
| 
 | ||||
|         .item { | ||||
|             background: transparent; | ||||
| 
 | ||||
|             .label { | ||||
|                 margin-top: 0; | ||||
|                 margin-bottom: 0; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .core-course-thumb { | ||||
|         display: none; | ||||
|         height: var(--core-courseimage-on-course-height); | ||||
|         width: 100%; | ||||
|         overflow: hidden; | ||||
|         cursor: pointer; | ||||
|         pointer-events: auto; | ||||
|         position: relative; | ||||
|         background: var(--ion-item-background); | ||||
|         border-bottom: 1px solid var(--stroke); | ||||
| 
 | ||||
|         img { | ||||
|             position: absolute; | ||||
|             top: 0; | ||||
|             bottom: 0; | ||||
|             margin: auto; | ||||
|             width: 100%; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @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 { | ||||
| .core-course-section-nav-buttons { | ||||
|     display: flex; | ||||
|     justify-content: flex-end; | ||||
| 
 | ||||
| @ -65,6 +8,10 @@ | ||||
|         overflow: hidden; | ||||
|         text-transform: none; | ||||
|     } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| .course-section { | ||||
|     ion-badge { | ||||
|         text-align: start; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -26,7 +26,6 @@ import { | ||||
|     Type, | ||||
|     ElementRef, | ||||
| } from '@angular/core'; | ||||
| import { ModalOptions } from '@ionic/core'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; | ||||
| import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | ||||
| @ -44,8 +43,10 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { IonContent, IonRefresher } from '@ionic/angular'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreCourseSectionSelectorComponent } from '../section-selector/section-selector'; | ||||
| import { CoreCourseCourseIndexComponent, CoreCourseIndexSectionWithModule } from '../course-index/course-index'; | ||||
| import { CoreBlockHelper } from '@features/block/services/block-helper'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to display course contents using a certain format. If the format isn't found, use default one. | ||||
| @ -66,8 +67,8 @@ 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() course!: CoreCourseAnyCourseData; // The course to render.
 | ||||
|     @Input() sections: CoreCourseSectionToDisplay[] = []; // 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.
 | ||||
| @ -78,7 +79,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     // All the possible component classes.
 | ||||
|     courseFormatComponent?: Type<unknown>; | ||||
|     courseSummaryComponent?: Type<unknown>; | ||||
|     sectionSelectorComponent?: Type<unknown>; | ||||
|     singleSectionComponent?: Type<unknown>; | ||||
|     allSectionsComponent?: Type<unknown>; | ||||
| 
 | ||||
| @ -86,7 +86,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     showSectionId = 0; | ||||
|     data: Record<string, unknown> = {}; // Data to pass to the components.
 | ||||
| 
 | ||||
|     displaySectionSelector = false; | ||||
|     displayCourseIndex = false; | ||||
|     displayBlocks = false; | ||||
|     hasBlocks = false; | ||||
|     selectedSection?: CoreCourseSection; | ||||
| @ -95,17 +95,12 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID; | ||||
|     stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID; | ||||
|     loaded = false; | ||||
|     hasSeveralSections?: boolean; | ||||
|     imageThumb?: string; | ||||
|     progress?: number; | ||||
|     sectionSelectorModalOptions: ModalOptions = { | ||||
|         component: CoreCourseSectionSelectorComponent, | ||||
|         componentProps: {}, | ||||
|     }; | ||||
|     highlighted?: string; | ||||
| 
 | ||||
|     protected selectTabObserver?: CoreEventObserver; | ||||
|     protected completionObserver?: CoreEventObserver; | ||||
|     protected lastCourseFormat?: string; | ||||
|     protected sectionSelectorExpanded = false; | ||||
| 
 | ||||
|     constructor( | ||||
|         protected content: IonContent, | ||||
| @ -116,9 +111,17 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         if (this.course === undefined) { | ||||
|             CoreDomUtils.showErrorModal('Course not set'); | ||||
| 
 | ||||
|             CoreNavigator.back(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Listen for select course tab events to select the right section if needed.
 | ||||
|         this.selectTabObserver = CoreEvents.on(CoreEvents.SELECT_COURSE_TAB, (data) => { | ||||
|             if (data.name) { | ||||
| @ -137,6 +140,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(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -144,27 +178,20 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|      */ | ||||
|     async ngOnChanges(changes: { [name: string]: SimpleChange }): Promise<void> { | ||||
|         this.setInputData(); | ||||
|         this.sectionSelectorModalOptions.componentProps!.course = this.course; | ||||
|         this.sectionSelectorModalOptions.componentProps!.sections = this.sections; | ||||
| 
 | ||||
|         if (changes.course && this.course) { | ||||
|             // Course has changed, try to get the components.
 | ||||
|             this.getComponents(); | ||||
| 
 | ||||
|             this.displaySectionSelector = CoreCourseFormatDelegate.displaySectionSelector(this.course); | ||||
|             this.displayCourseIndex = CoreCourseFormatDelegate.displayCourseIndex(this.course); | ||||
|             this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course); | ||||
| 
 | ||||
|             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) { | ||||
|             this.sectionSelectorModalOptions.componentProps!.sections = this.sections; | ||||
|             this.treatSections(this.sections); | ||||
|         } | ||||
|     } | ||||
| @ -192,10 +219,13 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|         // Format has changed or it's the first time, load all the components.
 | ||||
|         this.lastCourseFormat = this.course.format; | ||||
| 
 | ||||
|         this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course); | ||||
|         const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections); | ||||
|         currentSection.highlighted = true; | ||||
| 
 | ||||
|         await Promise.all([ | ||||
|             this.loadCourseFormatComponent(), | ||||
|             this.loadCourseSummaryComponent(), | ||||
|             this.loadSectionSelectorComponent(), | ||||
|             this.loadSingleSectionComponent(), | ||||
|             this.loadAllSectionsComponent(), | ||||
|         ]); | ||||
| @ -207,7 +237,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadCourseFormatComponent(): Promise<void> { | ||||
|         this.courseFormatComponent = await CoreCourseFormatDelegate.getCourseFormatComponent(this.course!); | ||||
|         this.courseFormatComponent = await CoreCourseFormatDelegate.getCourseFormatComponent(this.course); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -216,16 +246,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadCourseSummaryComponent(): Promise<void> { | ||||
|         this.courseSummaryComponent = await CoreCourseFormatDelegate.getCourseSummaryComponent(this.course!); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load section selector component. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadSectionSelectorComponent(): Promise<void> { | ||||
|         this.sectionSelectorComponent = await CoreCourseFormatDelegate.getSectionSelectorComponent(this.course!); | ||||
|         this.courseSummaryComponent = await CoreCourseFormatDelegate.getCourseSummaryComponent(this.course); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -234,7 +255,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadSingleSectionComponent(): Promise<void> { | ||||
|         this.singleSectionComponent = await CoreCourseFormatDelegate.getSingleSectionComponent(this.course!); | ||||
|         this.singleSectionComponent = await CoreCourseFormatDelegate.getSingleSectionComponent(this.course); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -243,7 +264,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadAllSectionsComponent(): Promise<void> { | ||||
|         this.allSectionsComponent = await CoreCourseFormatDelegate.getAllSectionsComponent(this.course!); | ||||
|         this.allSectionsComponent = await CoreCourseFormatDelegate.getAllSectionsComponent(this.course); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -254,15 +275,16 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|      */ | ||||
|     protected async treatSections(sections: CoreCourseSection[]): Promise<void> { | ||||
|         const hasAllSections = sections[0].id == CoreCourseProvider.ALL_SECTIONS_ID; | ||||
|         this.hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections); | ||||
|         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.
 | ||||
|                 newSection = await CoreCourseFormatDelegate.getCurrentSection(this.course!, sections); | ||||
|                 newSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections); | ||||
|             } | ||||
| 
 | ||||
|             this.sectionChanged(newSection); | ||||
| @ -271,7 +293,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|         } | ||||
| 
 | ||||
|         // There is no selected section yet, calculate which one to load.
 | ||||
|         if (!this.hasSeveralSections) { | ||||
|         if (!hasSeveralSections) { | ||||
|             // Always load "All sections" to display the section title. If it isn't there just load the section.
 | ||||
|             this.loaded = true; | ||||
|             this.sectionChanged(sections[0]); | ||||
| @ -289,7 +311,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
| 
 | ||||
|         if (!this.loaded) { | ||||
|             // No section specified, not found or not visible, get current section.
 | ||||
|             const section = await CoreCourseFormatDelegate.getCurrentSection(this.course!, sections); | ||||
|             const section = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections); | ||||
| 
 | ||||
|             this.loaded = true; | ||||
|             this.sectionChanged(section); | ||||
| @ -299,20 +321,31 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Display the section selector modal. | ||||
|      * Display the course index modal. | ||||
|      */ | ||||
|     async showSectionSelector(): Promise<void> { | ||||
|         if (this.sectionSelectorExpanded) { | ||||
|             return; | ||||
|     async openCourseIndex(): Promise<void> { | ||||
|         const data = await CoreDomUtils.openModal<CoreCourseIndexSectionWithModule>({ | ||||
|             component: CoreCourseCourseIndexComponent, | ||||
|             componentProps: { | ||||
|                 course: this.course, | ||||
|                 sections: this.sections, | ||||
|                 selectedId: this.selectedSection?.id, | ||||
|             }, | ||||
|         }); | ||||
| 
 | ||||
|         if (data) { | ||||
|             this.sectionChanged(data.section); | ||||
|             if (data.module) { | ||||
|                 if (!data.module.handlerData) { | ||||
|                     data.module.handlerData = | ||||
|                         await CoreCourseModuleDelegate.getModuleDataFor(data.module.modname, data.module, this.course.id); | ||||
|                 } | ||||
| 
 | ||||
|         this.sectionSelectorExpanded = true; | ||||
| 
 | ||||
|         const data = await CoreDomUtils.openModal<CoreCourseSection>(this.sectionSelectorModalOptions); | ||||
| 
 | ||||
|         this.sectionSelectorExpanded = false; | ||||
|         if (data) { | ||||
|             this.sectionChanged(data); | ||||
|                 if (data.module.uservisible !== false && data.module.handlerData?.action) { | ||||
|                     data.module.handlerData.action(data.event, data.module, data.module.course); | ||||
|                 } | ||||
|                 this.moduleId = data.module.id; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -324,27 +357,26 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     sectionChanged(newSection: CoreCourseSection): void { | ||||
|         const previousValue = this.selectedSection; | ||||
|         this.selectedSection = newSection; | ||||
|         this.sectionSelectorModalOptions.componentProps!.selected = this.selectedSection; | ||||
|         this.data.section = this.selectedSection; | ||||
| 
 | ||||
|         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; | ||||
| @ -368,7 +400,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|         if (!previousValue || previousValue.id != newSection.id) { | ||||
|             // First load or section changed, add log in Moodle.
 | ||||
|             CoreUtils.ignoreErrors( | ||||
|                 CoreCourse.logView(this.course!.id, newSection.section, undefined, this.course!.fullname), | ||||
|                 CoreCourse.logView(this.course.id, newSection.section, undefined, this.course.fullname), | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
| @ -475,7 +507,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component destroyed. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.selectTabObserver && this.selectTabObserver.off(); | ||||
| @ -510,35 +542,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. | ||||
|      */ | ||||
| @ -559,3 +562,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type CoreCourseSectionToDisplay = CoreCourseSection & { | ||||
|     highlighted?: boolean; | ||||
| }; | ||||
|  | ||||
| @ -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 | ||||
|      */ | ||||
| @ -52,31 +75,29 @@ export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleC | ||||
|         let langKey: string | undefined; | ||||
|         let image: string | undefined; | ||||
| 
 | ||||
|         if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL && | ||||
|                 this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) { | ||||
|         if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL) { | ||||
|             if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) { | ||||
|                 image = 'completion-manual-n'; | ||||
|                 langKey = 'core.completion-alt-manual-n'; | ||||
|         } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL && | ||||
|                 this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { | ||||
|             } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { | ||||
|                 image = 'completion-manual-y'; | ||||
|                 langKey = 'core.completion-alt-manual-y'; | ||||
|         } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC && | ||||
|                 this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) { | ||||
|             } | ||||
|         } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC) { | ||||
|             if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) { | ||||
|                 image = 'completion-auto-n'; | ||||
|                 langKey = 'core.completion-alt-auto-n'; | ||||
|         } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC && | ||||
|                 this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { | ||||
|             } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { | ||||
|                 image = 'completion-auto-y'; | ||||
|                 langKey = 'core.completion-alt-auto-y'; | ||||
|         } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC && | ||||
|                 this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) { | ||||
|             } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) { | ||||
|                 image = 'completion-auto-pass'; | ||||
|                 langKey = 'core.completion-alt-auto-pass'; | ||||
|         } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC && | ||||
|                 this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL) { | ||||
|             } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL) { | ||||
|                 image = 'completion-auto-fail'; | ||||
|                 langKey = 'core.completion-alt-auto-fail'; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (image) { | ||||
|             if (this.completion.overrideby && this.completion.overrideby > 0) { | ||||
| @ -128,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(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -4,16 +4,19 @@ | ||||
|     <ng-container *ngIf="completion.istrackeduser"> | ||||
|         <ng-container *ngFor="let rule of details"> | ||||
|             <ion-chip *ngIf="rule.statuscomplete" color="success" role="listitem" [attr.aria-label]="rule.accessibleDescription"> | ||||
|                 <ion-icon name="fas-check" aria-hidden="true"></ion-icon> | ||||
|                 <ion-label><strong>{{ 'core.course.completion_automatic:done' | translate }}</strong> {{ rule.rulevalue.description }} | ||||
|                 </ion-label> | ||||
|             </ion-chip> | ||||
| 
 | ||||
|             <ion-chip *ngIf="rule.statuscompletefail" color="danger" role="listitem" [attr.aria-label]="rule.accessibleDescription"> | ||||
|                 <ion-icon name="fas-times" aria-hidden="true"></ion-icon> | ||||
|                 <ion-label><strong>{{ 'core.course.completion_automatic:failed' | translate }}</strong> {{ rule.rulevalue.description }} | ||||
|                 </ion-label> | ||||
|             </ion-chip> | ||||
| 
 | ||||
|             <ion-chip *ngIf="rule.statusincomplete" color="medium" role="listitem" [attr.aria-label]="rule.accessibleDescription"> | ||||
|             <ion-chip *ngIf="rule.statusincomplete" role="listitem" [attr.aria-label]="rule.accessibleDescription"> | ||||
|                 <ion-icon name="fas-edit" aria-hidden="true"></ion-icon> | ||||
|                 <ion-label><strong>{{ 'core.course.completion_automatic:todo' | translate }}</strong> {{ rule.rulevalue.description }} | ||||
|                 </ion-label> | ||||
|             </ion-chip> | ||||
|  | ||||
| @ -19,12 +19,21 @@ | ||||
|     </ion-label> | ||||
| </ion-item> | ||||
| <ng-content select="[description]"></ng-content> | ||||
| <!-- Activity dates. --> | ||||
| <ion-item class="ion-text-wrap core-module-dates" lines="none" *ngIf="showCompletion && module.dates?.length"> | ||||
| 
 | ||||
| <ion-item class="ion-text-wrap core-module-dates" lines="none" *ngIf="showCompletion && (module.dates?.length || | ||||
| (module.completiondata && module.completiondata.isautomatic && module.uservisible))"> | ||||
|     <ion-label> | ||||
|         <!-- Activity dates. --> | ||||
|         <div *ngIf="module.dates?.length" class="core-module-dates"> | ||||
|             <p *ngFor="let date of module.dates"> | ||||
|             <strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }} | ||||
|                 <ion-icon name="fas-calendar" aria-hidden="true"></ion-icon><strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | | ||||
|                 coreFormatDate:'strftimedatetime' }} | ||||
|             </p> | ||||
|         </div> | ||||
|         <!-- Module completion. --> | ||||
|         <core-course-module-completion [completion]="module.completiondata" [moduleName]="module.name" [moduleId]="module.id" | ||||
|             [showCompletionConditions]="true"> | ||||
|         </core-course-module-completion> | ||||
|     </ion-label> | ||||
| </ion-item> | ||||
| <ng-content></ng-content> | ||||
|  | ||||
| @ -16,4 +16,9 @@ | ||||
|     core-mod-icon { | ||||
|         align-self: flex-start; | ||||
|     } | ||||
| 
 | ||||
|     .core-module-dates ion-icon { | ||||
|         @include margin-horizontal(null, 8px); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -2,22 +2,20 @@ | ||||
| 
 | ||||
|     <ng-container *ngIf="completion.istrackeduser"> | ||||
|         <ng-container *ngIf="completion.state"> | ||||
|             <ion-button color="success" expand="block" fill="outline" [attr.aria-label]="accessibleDescription" | ||||
|                 (click)="completionClicked($event)"> | ||||
|             <ion-button color="success" fill="outline" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)"> | ||||
|                 <ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon> | ||||
|                 {{ 'core.course.completion_manual:done' | translate }} | ||||
|             </ion-button> | ||||
|         </ng-container> | ||||
|         <ng-container *ngIf="!completion.state"> | ||||
|             <ion-button color="dark" expand="block" fill="outline" [attr.aria-label]="accessibleDescription" | ||||
|                 (click)="completionClicked($event)"> | ||||
|             <ion-button color="dark" fill="outline" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)"> | ||||
|                 {{ 'core.course.completion_manual:markdone' | translate }} | ||||
|             </ion-button> | ||||
|         </ng-container> | ||||
|     </ng-container> | ||||
| 
 | ||||
|     <ng-container *ngIf="!completion.istrackeduser"> | ||||
|         <ion-button disabled="true" color="dark" expand="block" fill="outline"> | ||||
|         <ion-button disabled="true" color="dark" fill="outline"> | ||||
|             {{ 'core.course.completion_manual:markdone' | translate }} | ||||
|         </ion-button> | ||||
|     </ng-container> | ||||
|  | ||||
| @ -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(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -6,12 +6,10 @@ | ||||
|                 <ion-icon name="fas-arrow-left" slot="icon-only" aria-hidden="true"></ion-icon> | ||||
|             </ion-button> | ||||
|         </ion-col> | ||||
|         <ion-col *ngIf="showCompletion && (currentModule.completiondata && | ||||
|             (currentModule.completiondata.isautomatic || showManualCompletion) && currentModule.uservisible)"> | ||||
|         <ion-col *ngIf="showCompletion && (currentModule.completiondata && showManualCompletion && currentModule.uservisible)"> | ||||
|             <!-- Module completion. --> | ||||
|             <core-course-module-completion [completion]="currentModule.completiondata" [moduleName]="currentModule.name" | ||||
|                 [moduleId]="currentModule.id" [showCompletionConditions]="true" [showManualCompletion]="showManualCompletion" | ||||
|                 (completionChanged)="completionChanged.emit($event)"> | ||||
|                 [moduleId]="currentModule.id" [showManualCompletion]="true" (completionChanged)="completionChanged.emit($event)"> | ||||
|             </core-course-module-completion> | ||||
|         </ion-col> | ||||
|         <ion-col size="auto"> | ||||
|  | ||||
| @ -18,25 +18,26 @@ | ||||
|                         [courseId]="module.course" [attr.aria-label]="module.handlerData.a11yTitle + ', ' + modNameTranslated"> | ||||
|                     </core-format-text> | ||||
|                 </p> | ||||
|                 <ion-badge *ngIf="module.handlerData.extraBadge" [color]="module.handlerData.extraBadgeColor" | ||||
|                     class="ion-text-wrap ion-text-start"> | ||||
|                     <span [innerHTML]="module.handlerData.extraBadge"></span> | ||||
|                 </ion-badge> | ||||
|                 <ion-badge *ngIf="module.visible === 0 && (!section || section.visible)" class="ion-text-wrap"> | ||||
|                     {{ 'core.course.hiddenfromstudents' | translate }} | ||||
|                 </ion-badge> | ||||
|                 <ion-badge *ngIf="module.visible !== 0 && module.isStealth" class="ion-text-wrap"> | ||||
|                     {{ 'core.course.hiddenoncoursepage' | translate }} | ||||
|                 </ion-badge> | ||||
|                 <div class="core-module-availabilityinfo" *ngIf="module.availabilityinfo"> | ||||
|                     <ion-badge class="ion-text-wrap">{{ 'core.restricted' | translate }}</ion-badge> | ||||
|                     <core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id" | ||||
|                         [courseId]="module.course" class="ion-text-wrap"> | ||||
|                     </core-format-text> | ||||
|                 </div> | ||||
|                 <ion-badge *ngIf="module.completiondata?.offline" color="warning" class="ion-text-wrap"> | ||||
|                     {{ 'core.course.manualcompletionnotsynced' | translate }} | ||||
|                 </ion-badge> | ||||
|                 <ion-chip class="completioninfo completion_incomplete" *ngIf="completionStatus === 0"> | ||||
|                     <ion-label> | ||||
|                         <ion-icon name="fas-edit" aria-hidden="true"></ion-icon> | ||||
|                         {{ 'core.course.todo' | translate }} | ||||
|                     </ion-label> | ||||
|                 </ion-chip> | ||||
|                 <ion-chip class="completioninfo completion_complete" *ngIf="completionStatus === 1 || completionStatus === 2" | ||||
|                     color="success"> | ||||
|                     <ion-icon name="fas-check" aria-hidden="true"></ion-icon> | ||||
|                     <ion-label>{{'core.course.done' | translate }}</ion-label> | ||||
|                 </ion-chip> | ||||
|                 <ion-chip class="completioninfo completion_fail" *ngIf="completionStatus === 3" color="danger"> | ||||
|                     <ion-icon name="fas-times" aria-hidden="true"></ion-icon> | ||||
|                     <ion-label>{{'core.course.failed' | translate }}</ion-label> | ||||
|                 </ion-chip> | ||||
| 
 | ||||
|                 <ion-chip *ngIf="module.handlerData.extraBadge" [color]="module.handlerData.extraBadgeColor" | ||||
|                     class="ion-text-wrap ion-text-start" [outline]="true"> | ||||
|                     <ion-label><span [innerHTML]="module.handlerData.extraBadge"></span></ion-label> | ||||
|                 </ion-chip> | ||||
|             </ion-label> | ||||
|             <!-- Buttons. --> | ||||
|             <div slot="end" *ngIf="module.uservisible !== false" class="buttons core-module-buttons" | ||||
| @ -63,10 +64,15 @@ | ||||
|                 'item-dimmed': module.visible === 0 || module.uservisible === false | ||||
|             }" detail="false"> | ||||
|             <ion-label> | ||||
|                 <core-format-text class="core-module-description" *ngIf="module.description" [maxHeight]="80" [text]="module.description" | ||||
|                     contextLevel="module" [contextInstanceId]="module.id" [courseId]="module.course"> | ||||
|                 </core-format-text> | ||||
| 
 | ||||
|                 <!-- Activity dates. --> | ||||
|                 <div *ngIf="showActivityDates && module.dates && module.dates.length" class="core-module-dates"> | ||||
|                     <p *ngFor="let date of module.dates"> | ||||
|                         <strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }} | ||||
|                         <ion-icon name="fas-calendar" aria-hidden="true"></ion-icon><strong>{{ date.label }}</strong> {{ date.timestamp * | ||||
|                         1000 | coreFormatDate:'strftimedatetime' }} | ||||
|                     </p> | ||||
|                 </div> | ||||
| 
 | ||||
| @ -76,10 +82,32 @@ | ||||
|                     [showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)"> | ||||
|                 </core-course-module-completion> | ||||
| 
 | ||||
|                 <core-format-text class="core-module-description" *ngIf="module.description" [maxHeight]="80" [text]="module.description" | ||||
|                     contextLevel="module" [contextInstanceId]="module.id" [courseId]="module.course"> | ||||
|                 <ion-chip *ngIf="module.completiondata?.offline" color="warning" class="ion-text-wrap block"> | ||||
|                     <ion-icon name="fas-sync" aria-hidden="true"></ion-icon> | ||||
|                     <ion-label>{{ 'core.course.manualcompletionnotsynced' | translate }}</ion-label> | ||||
|                 </ion-chip> | ||||
| 
 | ||||
|                 <!-- Availability --> | ||||
|                 <ion-chip *ngIf="module.visible === 0 && (!section || section.visible)" class="ion-text-wrap block"> | ||||
|                     <ion-icon name="fas-eye-slash" aria-hidden="true"></ion-icon> | ||||
|                     <ion-label>{{ 'core.course.hiddenfromstudents' | translate }}</ion-label> | ||||
|                 </ion-chip> | ||||
| 
 | ||||
|                 <ion-chip *ngIf="module.visible !== 0 && module.isStealth" class="ion-text-wrap block"> | ||||
|                     <ion-icon name="fas-eye-slash" aria-hidden="true"></ion-icon> | ||||
|                     <ion-label>{{ 'core.course.hiddenoncoursepage' | translate }}</ion-label> | ||||
|                 </ion-chip> | ||||
| 
 | ||||
|                 <ion-chip class="core-module-availabilityinfo ion-text-wrap block" *ngIf="module.availabilityinfo"> | ||||
|                     <ion-icon name="fas-lock" [attr.aria-label]="'core.restricted' | translate"></ion-icon> | ||||
|                     <ion-label> | ||||
|                         <core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id" | ||||
|                             [courseId]="module.course" class="ion-text-wrap"> | ||||
|                         </core-format-text> | ||||
|                     </ion-label> | ||||
|                 </ion-chip> | ||||
| 
 | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
|     </ng-container> | ||||
| 
 | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| @import "~theme/globals"; | ||||
| 
 | ||||
| :host { | ||||
| 
 | ||||
|     .item.core-module-main-item { | ||||
| @ -59,4 +61,9 @@ | ||||
|         --inner-border-width: 0px; | ||||
|     } | ||||
| 
 | ||||
|     .core-module-availabilityinfo  ion-icon, | ||||
|     .core-module-dates ion-icon { | ||||
|         @include margin-horizontal(null, 8px); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -20,7 +20,7 @@ import { | ||||
|     CoreCourseModuleCompletionData, | ||||
|     CoreCourseSection, | ||||
| } from '@features/course/services/course-helper'; | ||||
| import { CoreCourse } from '@features/course/services/course'; | ||||
| import { CoreCourse, CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking } from '@features/course/services/course'; | ||||
| import { CoreCourseModuleDelegate, CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate'; | ||||
| 
 | ||||
| /** | ||||
| @ -47,6 +47,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { | ||||
|     hasInfo = false; | ||||
|     showLegacyCompletion = false; // Whether to show module completion in the old format.
 | ||||
|     showManualCompletion = false; // Whether to show manual completion when completion conditions are disabled.
 | ||||
|     completionStatus?: CoreCourseModuleCompletionStatus; | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
| @ -61,13 +62,22 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { | ||||
|         } | ||||
| 
 | ||||
|         this.module.handlerData.a11yTitle = this.module.handlerData.a11yTitle ?? this.module.handlerData.title; | ||||
|         this.completionStatus = this.module.completiondata === undefined || | ||||
|                     this.module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE | ||||
|             ? undefined | ||||
|             : this.module.completiondata.state; | ||||
| 
 | ||||
|         this.hasInfo = !!( | ||||
|             this.module.description || | ||||
|             (this.showActivityDates && this.module.dates && this.module.dates.length) || | ||||
|             (this.module.completiondata && | ||||
|                 ((this.showManualCompletion && !this.module.completiondata.isautomatic) || | ||||
|                     (this.showCompletionConditions && this.module.completiondata.isautomatic)) | ||||
|             ) | ||||
|             ) || | ||||
|             this.module.completiondata?.offline || | ||||
|             (this.module.visible === 0 && (!this.section || this.section.visible)) || | ||||
|             (this.module.visible !== 0 && this.module.isStealth) || | ||||
|             (this.module.availabilityinfo) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,45 +0,0 @@ | ||||
| <ion-header> | ||||
|     <ion-toolbar> | ||||
|         <ion-title> | ||||
|             <h2 id="core-course-section-selector-label">{{ 'core.course.sections' | translate }}</h2> | ||||
|         </ion-title> | ||||
|         <ion-buttons slot="end"> | ||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|                 <ion-icon slot="icon-only" name="fas-times" aria-hidden="true"></ion-icon> | ||||
|             </ion-button> | ||||
|         </ion-buttons> | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <ion-list id="core-course-section-selector" role="listbox" aria-labelledby="core-course-section-selector-label"> | ||||
|         <ng-container *ngFor="let section of sections"> | ||||
|             <ion-item *ngIf="!section.hiddenbynumsections && section.id != stealthModulesSectionId" class="ion-text-wrap" | ||||
|                 (click)="selectSection(section)" [attr.aria-current]="selected?.id == section.id ? 'page' : 'false'" | ||||
|                 [class.item-dimmed]="section.visible === 0 || section.uservisible === false" detail="false" | ||||
|                 [attr.aria-hidden]="section.uservisible === false" button> | ||||
| 
 | ||||
|                 <ion-icon name="fas-folder" slot="start" aria-hidden="true"></ion-icon> | ||||
|                 <ion-label> | ||||
|                     <p class="item-heading"> | ||||
|                         <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id"> | ||||
|                         </core-format-text> | ||||
|                     </p> | ||||
|                     <core-progress-bar *ngIf="section.progress >= 0" [progress]="section.progress" | ||||
|                         a11yText="core.course.aria:sectionprogress"> | ||||
|                     </core-progress-bar> | ||||
| 
 | ||||
|                     <ion-badge color="info" *ngIf="section.visible === 0 && section.uservisible !== false" class="ion-text-wrap"> | ||||
|                         {{ 'core.course.hiddenfromstudents' | translate }} | ||||
|                     </ion-badge> | ||||
|                     <ion-badge color="info" *ngIf="section.visible === 0 && section.uservisible === false" class="ion-text-wrap"> | ||||
|                         {{ 'core.notavailable' | translate }} | ||||
|                     </ion-badge> | ||||
|                     <ion-badge color="info" *ngIf="section.availabilityinfo" class="ion-text-wrap"> | ||||
|                         <core-format-text [text]=" section.availabilityinfo" contextLevel="course" [contextInstanceId]="course?.id"> | ||||
|                         </core-format-text> | ||||
|                     </ion-badge> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ng-container> | ||||
|     </ion-list> | ||||
| </ion-content> | ||||
| @ -1,15 +0,0 @@ | ||||
| @import '~theme/globals.scss'; | ||||
| core-progress-bar { | ||||
|     --bar-margin: 8px 0 4px 0; | ||||
|     --line-height: 20px; | ||||
| } | ||||
| 
 | ||||
| @if ($core-hide-progress-on-section-selector) { | ||||
|     core-progress-bar { | ||||
|         display: none; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| ion-badge { | ||||
|     text-align: start; | ||||
| } | ||||
| @ -1,103 +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, Input, OnInit } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreCourseSection } from '@features/course/services/course-helper'; | ||||
| import { | ||||
|     CoreCourseModuleCompletionStatus, | ||||
|     CoreCourseModuleCompletionTracking, | ||||
|     CoreCourseProvider, | ||||
| } from '@features/course/services/course'; | ||||
| import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { ModalController } from '@singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to display course section selector in a modal. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'core-course-section-selector', | ||||
|     templateUrl: 'section-selector.html', | ||||
|     styleUrls: ['section-selector.scss'], | ||||
| }) | ||||
| export class CoreCourseSectionSelectorComponent implements OnInit { | ||||
| 
 | ||||
|     @Input() sections?: SectionWithProgress[]; | ||||
|     @Input() selected?: CoreCourseSection; | ||||
|     @Input() course?: CoreCourseAnyCourseData; | ||||
| 
 | ||||
|     stealthModulesSectionId = CoreCourseProvider.STEALTH_MODULES_SECTION_ID; | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
| 
 | ||||
|         if (!this.course || !this.sections || !this.course.enablecompletion || !('courseformatoptions' in this.course) || | ||||
|                 !this.course.courseformatoptions) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const formatOptions = CoreUtils.objectToKeyValueMap(this.course.courseformatoptions, 'name', 'value'); | ||||
| 
 | ||||
|         if (!formatOptions || formatOptions.coursedisplay != 1 || formatOptions.completionusertracked === false) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.sections.forEach((section) => { | ||||
|             let complete = 0; | ||||
|             let total = 0; | ||||
|             section.modules.forEach((module) => { | ||||
|                 if (!module.uservisible || module.completiondata === undefined || | ||||
|                         module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 total++; | ||||
|                 if (module.completiondata.state == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE || | ||||
|                         module.completiondata.state == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) { | ||||
|                     complete++; | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             if (total > 0) { | ||||
|                 section.progress = complete / total * 100; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Close the modal. | ||||
|      */ | ||||
|     closeModal(): void { | ||||
|         ModalController.dismiss(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Select a section. | ||||
|      * | ||||
|      * @param section Selected section object. | ||||
|      */ | ||||
|     selectSection(section: SectionWithProgress): void { | ||||
|         if (section.uservisible !== false) { | ||||
|             ModalController.dismiss(section); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type SectionWithProgress = CoreCourseSection & { | ||||
|     progress?: number; | ||||
| }; | ||||
| @ -0,0 +1,3 @@ | ||||
| :host ::ng-deep .collapsible-title { | ||||
|     display: none; | ||||
| } | ||||
| @ -19,7 +19,7 @@ import { CoreCourseUnsupportedModuleComponent } from '@features/course/component | ||||
| import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; | ||||
| import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreCourseModuleCompletionData, CoreCourseSectionWithStatus } from '@features/course/services/course-helper'; | ||||
| import { CoreCourseModuleCompletionData, CoreCourseSection } from '@features/course/services/course-helper'; | ||||
| import { CoreBlockHelper } from '@features/block/services/block-helper'; | ||||
| import { CoreCourse } from '@features/course/services/course'; | ||||
| 
 | ||||
| @ -31,11 +31,12 @@ import { CoreCourse } from '@features/course/services/course'; | ||||
| @Component({ | ||||
|     selector: 'core-course-format-single-activity', | ||||
|     templateUrl: 'core-course-format-single-activity.html', | ||||
|     styleUrls: ['single-activity.scss'], | ||||
| }) | ||||
| export class CoreCourseFormatSingleActivityComponent implements OnChanges { | ||||
| 
 | ||||
|     @Input() course?: CoreCourseAnyCourseData; // The course to render.
 | ||||
|     @Input() sections?: CoreCourseSectionWithStatus[]; // List of course sections.
 | ||||
|     @Input() sections?: CoreCourseSection[]; // 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.
 | ||||
|  | ||||
| @ -71,7 +71,7 @@ export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseF | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     displaySectionSelector(): boolean { | ||||
|     displayCourseIndex(): boolean { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreTimeUtils } from '@services/utils/time'; | ||||
| import { CoreCourseFormatHandler } from '@features/course/services/format-delegate'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { makeSingleton, Translate } from '@singletons'; | ||||
| import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | ||||
| import { CoreCourseWSSection } from '@features/course/services/course'; | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| @ -32,20 +32,14 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand | ||||
|     format = 'weeks'; | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return True or promise resolved with true if enabled. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given a list of sections, get the "current" section that should be displayed first. | ||||
|      * | ||||
|      * @param course The course to get the title. | ||||
|      * @param sections List of sections. | ||||
|      * @return Current section (or promise resolved with current section). | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> { | ||||
|         const now = CoreTimeUtils.timestamp(); | ||||
| @ -71,6 +65,13 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand | ||||
|         return sections[0]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getSectionHightlightedName(): string { | ||||
|         return Translate.instant('core.course.thisweek'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Return the start and end date of a section. | ||||
|      * | ||||
| @ -83,7 +84,7 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand | ||||
|         startDate = startDate + 7200; | ||||
| 
 | ||||
|         const dates = { | ||||
|             start: startDate + (CoreConstants.SECONDS_WEEK * (section.section! - 1)), | ||||
|             start: startDate + (CoreConstants.SECONDS_WEEK * ((section.section || 0) - 1)), | ||||
|             end: 0, | ||||
|         }; | ||||
|         dates.end = dates.start + CoreConstants.SECONDS_WEEK; | ||||
|  | ||||
| @ -26,21 +26,25 @@ | ||||
|     "confirmdownloadzerosize": "You are about to start downloading.{{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. ", | ||||
|     "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.", | ||||
|     "courseindex": "Course index", | ||||
|     "couldnotloadsectioncontent": "Could not load the section content. Please try again later.", | ||||
|     "couldnotloadsections": "Could not load the sections. Please try again later.", | ||||
|     "coursesummary": "Course summary", | ||||
|     "done": "Done", | ||||
|     "downloadcourse": "Download course", | ||||
|     "downloadcoursesprogressdescription": "Downloading courses: downloaded {{count}} out of {{total}}.", | ||||
|     "downloadsectionprogressdescription": "Downloading section: downloaded {{count}} out of {{total}}.", | ||||
|     "errordownloadingcourse": "Error downloading course.", | ||||
|     "errordownloadingsection": "Error downloading section.", | ||||
|     "errorgetmodule": "Error getting activity data.", | ||||
|     "failed": "Failed", | ||||
|     "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.", | ||||
|     "hiddenfromstudents": "Hidden from students", | ||||
|     "hiddenoncoursepage": "Available but not shown on course page", | ||||
|     "highlighted": "Highlighted", | ||||
|     "insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.", | ||||
|     "insufficientavailablequota": "Your device could not allocate space to save this download. It may be reserving space for app and system updates. Please clear some storage space first.", | ||||
|     "manualcompletionnotsynced": "Manual completion not synchronised.", | ||||
| @ -49,7 +53,8 @@ | ||||
|     "overriddennotice": "Your final grade from this activity was manually adjusted.", | ||||
|     "refreshcourse": "Refresh course", | ||||
|     "section": "Section", | ||||
|     "sections": "Sections", | ||||
|     "thisweek": "This week", | ||||
|     "todo": "To do", | ||||
|     "useactivityonbrowser": "You can still use it using your device's web browser.", | ||||
|     "warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.", | ||||
|     "warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}" | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -384,8 +384,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { | ||||
|      */ | ||||
|     openCourseSummary(): void { | ||||
|         CoreNavigator.navigateToSitePath( | ||||
|             '/course/' + this.course.id + '/preview', | ||||
|             { params: { course: this.course } }, | ||||
|             `/course/${this.course.id}/preview`, | ||||
|             { params: { course: this.course, avoidOpenCourse: true } }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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,21 +126,24 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|         await Promise.all([ | ||||
|             this.loadCourseHandlers(), | ||||
|             this.loadTitle(), | ||||
|             this.loadBasinInfo(), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * A tab was selected. | ||||
|      */ | ||||
|     tabSelected(): void { | ||||
|         if (this.module) { | ||||
|     tabSelected(tabToSelect: CoreTabsOutletTab): void { | ||||
|         this.ionCollapsibleHeader?.setupContent(tabToSelect.id); | ||||
| 
 | ||||
|         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); | ||||
|         CoreCourseHelper.openModule(this.module, this.course.id, this.contentsTab.pageParams.sectionId, this.modParams); | ||||
| 
 | ||||
|         delete this.module; | ||||
|     } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load course option handlers. | ||||
| @ -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 & { | ||||
| @ -27,33 +27,35 @@ | ||||
|         <core-course-module-info [module]="module" [courseId]="courseId" [description]="module.description" [component]="module.modname" | ||||
|             [componentId]="module.id" [expandDescription]="true"> | ||||
| 
 | ||||
|             <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 class="ion-padding" *ngIf="module.handlerData?.extraBadge"> | ||||
|                 <ion-chip class="ion-text-wrap ion-text-start" [color]="module.handlerData?.extraBadgeColor"> | ||||
|                     <ion-label><span [innerHTML]="module.handlerData?.extraBadge"></span></ion-label> | ||||
|                 </ion-chip> | ||||
|             </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 class="ion-padding" *ngIf="module.visible === 0 && (!section || section.visible)"> | ||||
|                 <ion-chip class="ion-text-wrap"> | ||||
|                     <ion-icon name="fas-eye-slash" aria-hidden="true"></ion-icon> | ||||
|                     <ion-label>{{ 'core.course.hiddenfromstudents' | translate }}</ion-label> | ||||
|                 </ion-chip> | ||||
|             </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 class="ion-padding" *ngIf="module.visible !== 0 && module.isStealth"> | ||||
|                 <ion-chip class="ion-text-wrap"> | ||||
|                     <ion-icon name="fas-eye-slash" aria-hidden="true"></ion-icon> | ||||
|                     <ion-label>{{ 'core.course.hiddenoncoursepage' | translate }}</ion-label> | ||||
|                 </ion-chip> | ||||
|             </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> | ||||
|             <div class="ion-padding core-module-availabilityinfo" *ngIf="module.availabilityinfo"> | ||||
|                 <ion-icon name="fas-lock" [attr.aria-label]="'core.restricted' | translate"></ion-icon> | ||||
|                 <ion-label> | ||||
|                     <core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id" | ||||
|                         [courseId]="courseId" class="ion-text-wrap"> | ||||
|                     </core-format-text> | ||||
|                 </ion-label> | ||||
|             </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 class="ion-padding" *ngIf="module.completiondata?.offline"> | ||||
|                 <ion-chip color="warning" class="ion-text-wrap"> | ||||
|                     <ion-label>{{ 'core.course.manualcompletionnotsynced' | translate }}</ion-label> | ||||
|                 </ion-chip> | ||||
|             </div> | ||||
| 
 | ||||
|             <core-course-unsupported-module *ngIf="unsupported" [module]="module" [courseId]="courseId"></core-course-unsupported-module> | ||||
|  | ||||
| @ -5,7 +5,7 @@ | ||||
|         </ion-buttons> | ||||
|         <ion-title> | ||||
|             <h1> | ||||
|                 <core-format-text [text]="course?.fullname" contextLevel="course" [contextInstanceId]="course?.id"></core-format-text> | ||||
|                 {{'core.course.coursesummary' | translate}} | ||||
|             </h1> | ||||
|         </ion-title> | ||||
|     </ion-toolbar> | ||||
| @ -16,44 +16,49 @@ | ||||
|     </ion-refresher> | ||||
|     <core-loading [hideUntil]="dataLoaded"> | ||||
|         <div *ngIf="courseImageUrl" class="core-course-thumb-parallax"> | ||||
|             <div (click)="openCourse()" class="core-course-thumb"> | ||||
|             <div class="core-course-thumb"> | ||||
|                 <img [src]="courseImageUrl" core-external-content alt="" /> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="core-course-thumb-parallax-content" *ngIf="course"> | ||||
|             <ion-item class="ion-text-wrap" (click)="openCourse()" [attr.aria-label]="course.fullname" [detail]="canAccessCourse" | ||||
|                 [button]="canAccessCourse"> | ||||
|                 <ion-icon name="fas-graduation-cap" fixed-width slot="start" aria-hidden="true"></ion-icon> | ||||
|             <ion-item class="ion-text-wrap"> | ||||
|                 <ion-label> | ||||
|                     <h2> | ||||
|                         <core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"> | ||||
|                         </core-format-text> | ||||
|                     </h2> | ||||
|                     <p *ngIf="course.categoryname"> | ||||
|                         <core-format-text [text]="course.categoryname" contextLevel="coursecat" [contextInstanceId]="course.categoryid"> | ||||
|                         </core-format-text> | ||||
|                     </p> | ||||
|                     <h2> | ||||
|                         <core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"> | ||||
|                         </core-format-text> | ||||
|                     </h2> | ||||
|                     <p *ngIf="course.startdate"> | ||||
|                         {{course.startdate * 1000 | coreFormatDate:"strftimedatefullshort" }} | ||||
|                         <span *ngIf="course.enddate"> - {{course.enddate * 1000 | coreFormatDate:"strftimedatefullshort" }}</span> | ||||
|                     </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-item> | ||||
| 
 | ||||
|             <ion-item class="ion-text-wrap" *ngIf="course.summary" detail="false"> | ||||
|                 <ion-label> | ||||
|                     <p class="item-heading"> | ||||
|                         {{'core.summary' | translate}} | ||||
|                     </p> | ||||
|                     <core-format-text [text]="course.summary" [maxHeight]="120" contextLevel="course" [contextInstanceId]="course.id"> | ||||
|                     </core-format-text> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <ng-container class="ion-text-wrap" *ngIf="course.contacts && course.contacts.length"> | ||||
|                 <ion-item-divider> | ||||
|             <ion-list *ngIf="course.contacts && course.contacts.length"> | ||||
|                 <ion-item-divider class="ion-text-wrap"> | ||||
|                     <ion-label> | ||||
|                         <h2>{{ 'core.teachers' | translate }}</h2> | ||||
|                     </ion-label> | ||||
|                 </ion-item-divider> | ||||
|                 <ion-item class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link [userId]="contact.id" | ||||
|                 <ion-item button class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link [userId]="contact.id" | ||||
|                     [courseId]="isEnrolled ? course.id : null" [attr.aria-label]="'core.viewprofile' | translate" detail="true"> | ||||
|                     <core-user-avatar [user]="contact" slot="start" [userId]="contact.id" [courseId]="isEnrolled ? course.id : null"> | ||||
|                     </core-user-avatar> | ||||
| @ -62,7 +67,7 @@ | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <core-spacer></core-spacer> | ||||
|             </ng-container> | ||||
|             </ion-list> | ||||
| 
 | ||||
|             <ion-item class="ion-text-wrap" *ngIf="course.customfields"> | ||||
|                 <ion-label> | ||||
| @ -83,7 +88,8 @@ | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <div *ngIf="!isEnrolled" detail="false"> | ||||
|             <!-- Enrol --> | ||||
|             <ng-container *ngIf="!isEnrolled"> | ||||
|                 <ion-item class="ion-text-wrap" *ngFor="let instance of selfEnrolInstances"> | ||||
|                     <ion-label> | ||||
|                         <p class="item-heading">{{ instance.name }}</p> | ||||
| @ -92,23 +98,24 @@ | ||||
|                         </ion-button> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|             </div> | ||||
|             <ion-item class="ion-text-wrap" *ngIf="!isEnrolled && paypalEnabled"> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="paypalEnabled"> | ||||
|                     <ion-label> | ||||
|                     <h2>{{ 'core.courses.paypalaccepted' | translate }}</h2> | ||||
|                     <p>{{ 'core.paymentinstant' | translate }}</p> | ||||
|                     <ion-button expand="block" class="ion-margin-top" (click)="paypalEnrol()" *ngIf="isMobile"> | ||||
|                         <p class="item-heading">{{ 'core.courses.paypalaccepted' | translate }}</p> | ||||
|                         <p *ngIf="isMobile">{{ 'core.paymentinstant' | translate }}</p> | ||||
|                         <ion-button *ngIf="isMobile" expand="block" class="ion-margin-top" (click)="paypalEnrol()"> | ||||
|                             {{ 'core.courses.sendpaymentbutton' | translate }} | ||||
|                         </ion-button> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|             <ion-item *ngIf="!isEnrolled && !selfEnrolInstances.length && !paypalEnabled"> | ||||
|                 <ion-item *ngIf="!selfEnrolInstances.length && !paypalEnabled"> | ||||
|                     <ion-label> | ||||
|                     <p>{{ 'core.courses.notenrollable' | translate }}</p> | ||||
|                         <p class="item-heading">{{ 'core.courses.notenrollable' | translate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|             <ion-item *ngIf="canAccessCourse && downloadCourseEnabled" (click)="prefetchCourse()" detail="false" | ||||
|                 [attr.aria-label]="prefetchCourseData.statusTranslatable | translate" button> | ||||
|             </ng-container> | ||||
| 
 | ||||
|             <ion-button class="ion-margin" *ngIf="canAccessCourse && downloadCourseEnabled" (click)="prefetchCourse()" expand="block" | ||||
|                 [attr.aria-label]="prefetchCourseData.statusTranslatable | translate"> | ||||
|                 <ion-icon *ngIf="(prefetchCourseData.status != statusDownloaded) && !prefetchCourseData.loading" | ||||
|                     [name]="prefetchCourseData.icon" slot="start" aria-hidden="true"> | ||||
|                 </ion-icon> | ||||
| @ -116,23 +123,24 @@ | ||||
|                     [name]="prefetchCourseData.icon" color="success" aria-hidden="true" role="status"> | ||||
|                 </ion-icon> | ||||
|                 <ion-spinner *ngIf="prefetchCourseData.loading" slot="start" [attr.aria-label]="'core.loading' | translate"></ion-spinner> | ||||
|                 <ion-label> | ||||
|                     <h2 *ngIf="prefetchCourseData.status != statusDownloaded">{{ 'core.course.downloadcourse' | translate }}</h2> | ||||
|                     <h2 *ngIf="prefetchCourseData.status == statusDownloaded">{{ 'core.course.refreshcourse' | translate }}</h2> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item button (click)="openCourse()" [attr.aria-label]="course.fullname" *ngIf="canAccessCourse" detail="true"> | ||||
|                 <ion-label *ngIf="prefetchCourseData.status != statusDownloaded">{{ 'core.course.downloadcourse' | translate }}</ion-label> | ||||
|                 <ion-label *ngIf="prefetchCourseData.status == statusDownloaded">{{ 'core.course.refreshcourse' | translate }}</ion-label> | ||||
|             </ion-button> | ||||
| 
 | ||||
|             <ion-button class="ion-margin" (click)="openCourse()" *ngIf="!avoidOpenCourse && canAccessCourse" expand="block"> | ||||
|                 <ion-icon name="fas-briefcase" slot="start" aria-hidden="true"></ion-icon> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'core.course' | translate }}</h2> | ||||
|                     {{ 'core.course' | translate }} | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item [href]="courseUrl" core-link [attr.aria-label]="course.fullname" button detail="false" [showBrowserWarning]="false"> | ||||
|             </ion-button> | ||||
| 
 | ||||
|             <ion-button class="ion-margin" [href]="courseUrl" core-link [showBrowserWarning]="false" expand="block"> | ||||
|                 <ion-icon name="fas-external-link-alt" slot="start" aria-hidden="true"></ion-icon> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'core.openinbrowser' | translate }}</h2> | ||||
|                     {{ 'core.openinbrowser' | translate }} | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             </ion-button> | ||||
| 
 | ||||
|         </div> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
|  | ||||
| @ -20,8 +20,8 @@ import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { | ||||
|     CoreCourseCustomField, | ||||
|     CoreCourseEnrolmentMethod, | ||||
|     CoreCourseGetCoursesData, | ||||
|     CoreCourses, | ||||
|     CoreCourseSearchedData, | ||||
|     CoreCoursesProvider, | ||||
| @ -34,6 +34,8 @@ import { Translate } from '@singletons'; | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { CoreCoursesSelfEnrolPasswordComponent } from '../../../courses/components/self-enrol-password/self-enrol-password'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreCourseWithImageAndColor } from '@features/courses/services/courses-helper'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that allows "previewing" a course and enrolling in it if enabled and not enrolled. | ||||
| @ -45,12 +47,13 @@ import { CoreNavigator } from '@services/navigator'; | ||||
| }) | ||||
| export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     course?: CoreCourseSearchedData; | ||||
|     course?: CoreCourseSummaryData; | ||||
|     isEnrolled = false; | ||||
|     canAccessCourse = true; | ||||
|     selfEnrolInstances: CoreCourseEnrolmentMethod[] = []; | ||||
|     paypalEnabled = false; | ||||
|     dataLoaded = false; | ||||
|     avoidOpenCourse = false; | ||||
|     prefetchCourseData: CorePrefetchStatusInfo = { | ||||
|         icon: '', | ||||
|         statusTranslatable: 'core.loading', | ||||
| @ -64,6 +67,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|     courseUrl = ''; | ||||
|     courseImageUrl?: string; | ||||
|     isMobile: boolean; | ||||
|     progress?: number; | ||||
| 
 | ||||
|     protected isGuestEnabled = false; | ||||
|     protected useGuestAccess = false; | ||||
| @ -74,6 +78,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|     protected paypalReturnUrl = ''; | ||||
|     protected pageDestroyed = false; | ||||
|     protected courseStatusObserver?: CoreEventObserver; | ||||
|     protected courseId!: number; | ||||
| 
 | ||||
|     constructor( | ||||
|         protected zone: NgZone, | ||||
| @ -84,7 +89,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|         if (this.downloadCourseEnabled) { | ||||
|             // Listen for status change in course.
 | ||||
|             this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => { | ||||
|                 if (data.courseId == this.course!.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) { | ||||
|                 if (data.courseId == this.courseId || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) { | ||||
|                     this.updateCourseStatus(data.status); | ||||
|                 } | ||||
|             }, CoreSites.getCurrentSiteId()); | ||||
| @ -92,27 +97,25 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * View loaded. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         this.course = CoreNavigator.getRouteParam('course'); | ||||
| 
 | ||||
|         if (!this.course) { | ||||
|         try { | ||||
|             this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModal(error); | ||||
|             CoreNavigator.back(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const currentSite = CoreSites.getCurrentSite(); | ||||
|         const currentSiteUrl = currentSite && currentSite.getURL(); | ||||
|         this.avoidOpenCourse = !!CoreNavigator.getRouteBooleanParam('avoidOpenCourse'); | ||||
|         this.course = CoreNavigator.getRouteParam('course'); | ||||
| 
 | ||||
|         this.paypalEnabled = this.course!.enrollmentmethods?.indexOf('paypal') > -1; | ||||
|         this.enrolUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'enrol/index.php?id=' + this.course!.id); | ||||
|         this.courseUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'course/view.php?id=' + this.course!.id); | ||||
|         this.paypalReturnUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'enrol/paypal/return.php'); | ||||
|         if (this.course.overviewfiles.length > 0) { | ||||
|             this.courseImageUrl = this.course.overviewfiles[0].fileurl; | ||||
|         } | ||||
|         const currentSiteUrl = CoreSites.getRequiredCurrentSite().getURL(); | ||||
|         this.enrolUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'enrol/index.php?id=' + this.courseId); | ||||
|         this.courseUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'course/view.php?id=' + this.courseId); | ||||
|         this.paypalReturnUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'enrol/paypal/return.php'); | ||||
| 
 | ||||
|         try { | ||||
|             await this.getCourse(); | ||||
| @ -120,11 +123,11 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|             if (this.downloadCourseEnabled) { | ||||
| 
 | ||||
|                 // Determine course prefetch icon.
 | ||||
|                 this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.course!.id); | ||||
|                 this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.courseId); | ||||
| 
 | ||||
|                 if (this.prefetchCourseData.loading) { | ||||
|                     // Course is being downloaded. Get the download promise.
 | ||||
|                     const promise = CoreCourseHelper.getCourseDownloadPromise(this.course!.id); | ||||
|                     const promise = CoreCourseHelper.getCourseDownloadPromise(this.courseId); | ||||
|                     if (promise) { | ||||
|                         // There is a download promise. If it fails, show an error.
 | ||||
|                         promise.catch((error) => { | ||||
| @ -134,7 +137,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|                         }); | ||||
|                     } else { | ||||
|                         // No download, this probably means that the app was closed while downloading. Set previous status.
 | ||||
|                         CoreCourse.setCoursePreviousStatus(this.course!.id); | ||||
|                         CoreCourse.setCoursePreviousStatus(this.courseId); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| @ -177,13 +180,15 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|         this.selfEnrolInstances = []; | ||||
| 
 | ||||
|         try { | ||||
|             this.enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(this.course!.id); | ||||
|             this.enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(this.courseId); | ||||
| 
 | ||||
|             this.enrolmentMethods.forEach((method) => { | ||||
|                 if (method.type === 'self') { | ||||
|                     this.selfEnrolInstances.push(method); | ||||
|                 } else if (method.type === 'guest') { | ||||
|                     this.isGuestEnabled = true; | ||||
|                 } else if (method.type === 'paypal') { | ||||
|                     this.paypalEnabled = true; | ||||
|                 } | ||||
|             }); | ||||
|         } catch (error) { | ||||
| @ -191,22 +196,17 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             let course: CoreEnrolledCourseData | CoreCourseGetCoursesData; | ||||
| 
 | ||||
|             // Check if user is enrolled in the course.
 | ||||
|             try { | ||||
|                 course = await CoreCourses.getUserCourse(this.course!.id); | ||||
|                 this.course = await CoreCourses.getUserCourse(this.courseId); | ||||
|                 this.isEnrolled = true; | ||||
|             } catch { | ||||
|                 // The user is not enrolled in the course. Use getCourses to see if it's an admin/manager and can see the course.
 | ||||
|                 this.isEnrolled = false; | ||||
| 
 | ||||
|                 course = await CoreCourses.getCourse(this.course!.id); | ||||
|                 this.course = await CoreCourses.getCourse(this.courseId); | ||||
|             } | ||||
| 
 | ||||
|             // Success retrieving the course, we can assume the user has permissions to view it.
 | ||||
|             this.course!.fullname = course.fullname || this.course!.fullname; | ||||
|             this.course!.summary = course.summary || this.course!.summary; | ||||
|             this.canAccessCourse = true; | ||||
|             this.useGuestAccess = false; | ||||
|         } catch { | ||||
| @ -219,14 +219,37 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (!CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.7')) { | ||||
|             try { | ||||
|                 const course = await CoreCourses.getCourseByField('id', this.course!.id); | ||||
|         if (this.course && 'overviewfiles' in this.course && this.course.overviewfiles?.length) { | ||||
|             this.courseImageUrl = this.course.overviewfiles[0].fileurl; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             const courseByField = await CoreCourses.getCourseByField('id', this.courseId); | ||||
|             if (this.course) { | ||||
|                 this.course.customfields = courseByField.customfields; | ||||
|                 this.course.contacts = courseByField.contacts; | ||||
|                 this.course.displayname = courseByField.displayname; | ||||
|                 this.course.categoryname = courseByField.categoryname; | ||||
|                 this.course.overviewfiles = courseByField.overviewfiles; | ||||
|             } else  { | ||||
|                 this.course = courseByField; | ||||
|             } | ||||
| 
 | ||||
|             this.paypalEnabled = !this.isEnrolled && courseByField.enrollmentmethods?.indexOf('paypal') > -1; | ||||
| 
 | ||||
|                 this.course!.customfields = course.customfields; | ||||
|         } catch { | ||||
|             // Ignore errors.
 | ||||
|         } | ||||
| 
 | ||||
|         if (!this.course || | ||||
|             !('progress' in this.course) || | ||||
|             typeof this.course.progress !== 'number' || | ||||
|             this.course.progress < 0 || | ||||
|             this.course.completionusertracked === false | ||||
|         ) { | ||||
|             this.progress = undefined; | ||||
|         } else { | ||||
|             this.progress = this.course.progress; | ||||
|         } | ||||
| 
 | ||||
|         this.dataLoaded = true; | ||||
| @ -234,13 +257,15 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     /** | ||||
|      * Open the course. | ||||
|      * | ||||
|      * @param replaceCurrentPage If current place should be replaced in the navigation stack. | ||||
|      */ | ||||
|     openCourse(): void { | ||||
|         if (!this.canAccessCourse) { | ||||
|     openCourse(replaceCurrentPage = false): void { | ||||
|         if (!this.canAccessCourse || !this.course || this.avoidOpenCourse) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         CoreCourseHelper.openCourse(this.course!, { isGuest: this.useGuestAccess }); | ||||
|         CoreCourseHelper.openCourse(this.course, { params: { isGuest: this.useGuestAccess }, replace: replaceCurrentPage }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -279,7 +304,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|         }; | ||||
| 
 | ||||
|         // Open the enrolment page in InAppBrowser.
 | ||||
|         const window = await CoreSites.getCurrentSite()!.openInAppWithAutoLogin(this.enrolUrl); | ||||
|         const window = await CoreSites.getRequiredCurrentSite().openInAppWithAutoLogin(this.enrolUrl); | ||||
| 
 | ||||
|         // Observe loaded pages in the InAppBrowser to check if the enrol process has ended.
 | ||||
|         const inAppLoadSubscription = window.on('loadstart').subscribe((event) => { | ||||
| @ -319,7 +344,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|         const modal = await CoreDomUtils.showModalLoading('core.loading', true); | ||||
| 
 | ||||
|         try { | ||||
|             await CoreCourses.selfEnrol(this.course!.id, password, instanceId); | ||||
|             await CoreCourses.selfEnrol(this.courseId, password, instanceId); | ||||
| 
 | ||||
|             // Close modal and refresh data.
 | ||||
|             this.isEnrolled = true; | ||||
| @ -331,13 +356,13 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|             await this.refreshData().finally(() => { | ||||
|                 // My courses have been updated, trigger event.
 | ||||
|                 CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { | ||||
|                     courseId: this.course!.id, | ||||
|                     courseId: this.courseId, | ||||
|                     course: this.course, | ||||
|                     action: CoreCoursesProvider.ACTION_ENROL, | ||||
|                 }, CoreSites.getCurrentSiteId()); | ||||
|             }); | ||||
| 
 | ||||
|             this.openCourse(); | ||||
|             this.openCourse(true); | ||||
| 
 | ||||
|             modal?.dismiss(); | ||||
|         } catch (error) { | ||||
| @ -378,12 +403,10 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         promises.push(CoreCourses.invalidateUserCourses()); | ||||
|         promises.push(CoreCourses.invalidateCourse(this.course!.id)); | ||||
|         promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.course!.id)); | ||||
|         promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(this.course!.id)); | ||||
|         if (CoreSites.getCurrentSite() && !CoreSites.getCurrentSite()!.isVersionGreaterEqualThan('3.7')) { | ||||
|             promises.push(CoreCourses.invalidateCoursesByField('id', this.course!.id)); | ||||
|         } | ||||
|         promises.push(CoreCourses.invalidateCourse(this.courseId)); | ||||
|         promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.courseId)); | ||||
|         promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(this.courseId)); | ||||
|         promises.push(CoreCourses.invalidateCoursesByField('id', this.courseId)); | ||||
|         if (this.guestInstanceId) { | ||||
|             promises.push(CoreCourses.invalidateCourseGuestEnrolmentInfo(this.guestInstanceId)); | ||||
|         } | ||||
| @ -419,14 +442,10 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|         } | ||||
| 
 | ||||
|         // Check if user is enrolled in the course.
 | ||||
|         try { | ||||
|             CoreCourses.invalidateUserCourses(); | ||||
|         } catch { | ||||
|             // Ignore errors.
 | ||||
|         } | ||||
|         await CoreUtils.ignoreErrors(CoreCourses.invalidateUserCourses()); | ||||
| 
 | ||||
|         try { | ||||
|             await CoreCourses.getUserCourse(this.course!.id); | ||||
|             await CoreCourses.getUserCourse(this.courseId); | ||||
|         } catch { | ||||
|             // Not enrolled, wait a bit and try again.
 | ||||
|             if (this.pageDestroyed || (Date.now() - this.waitStart > 60000)) { | ||||
| @ -451,7 +470,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|      */ | ||||
|     async prefetchCourse(): Promise<void> { | ||||
|         try { | ||||
|             await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course!, { | ||||
|             await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course as CoreEnrolledCourseData, { | ||||
|                 isGuest: this.useGuestAccess, | ||||
|             }); | ||||
|         } catch (error) { | ||||
| @ -462,14 +481,20 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Page destroyed. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.pageDestroyed = true; | ||||
| 
 | ||||
|         if (this.courseStatusObserver) { | ||||
|             this.courseStatusObserver.off(); | ||||
|         } | ||||
|         this.courseStatusObserver?.off(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type CoreCourseSummaryData = CoreCourseWithImageAndColor & (CoreEnrolledCourseData | CoreCourseSearchedData) & { | ||||
|     contacts?: { // Contact users.
 | ||||
|         id: number; // Contact user id.
 | ||||
|         fullname: string; // Contact user fullname.
 | ||||
|     }[]; | ||||
|     customfields?: CoreCourseCustomField[]; // Custom fields and associated values.
 | ||||
|     categoryname?: string; // Category name.
 | ||||
| }; | ||||
|  | ||||
| @ -1,41 +1,43 @@ | ||||
| :host { | ||||
|     --scroll-factor: 0.5; | ||||
|     --translate-z: calc(-2 * var(--scroll-factor))px; | ||||
|     --scale: calc(1 + var(--scroll-factor) * 2); | ||||
| 
 | ||||
|     ion-content:not(.animating) { | ||||
|         &::part(scroll) { | ||||
|             perspective: 1px; | ||||
|             perspective-origin: center top; | ||||
|             transform-style: preserve-3d; | ||||
|         } | ||||
| 
 | ||||
|     // @todo This parallax effect caused the image to be scaled during page transitions, | ||||
|     // and in some devices it seems like the problem persisted even after the transition. | ||||
|     // We should decide whether we want to keep this parallax or not, and if we do fix | ||||
|     // the problem or find an alternative implementation. For now, it's disabled. | ||||
| 
 | ||||
|     // .core-course-thumb-parallax-content { | ||||
|     //     transform: translateZ(0); | ||||
|     //     -webkit-filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow))); | ||||
|     //     filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow))); | ||||
|     // } | ||||
|     // .core-course-thumb-parallax { | ||||
|     //     height: 40vw; | ||||
|     //     max-height: 35vh; | ||||
|     //     z-index: -1; | ||||
|     //     overflow: hidden; | ||||
|     // } | ||||
|         .core-course-thumb { | ||||
|         overflow: hidden; | ||||
|         text-align: center; | ||||
|         cursor: pointer; | ||||
|         pointer-events: auto; | ||||
|             transform-origin: center top; | ||||
| 
 | ||||
|             --scroll-factor: 0.5; | ||||
|             --translate-z: calc(-2 * var(--scroll-factor))px; | ||||
|             --scale: calc(1 + var(--scroll-factor) * 2); | ||||
| 
 | ||||
|             /** | ||||
|             * Calculated with scroll-factor: 0.5; | ||||
|             * translate-z: -2 * $scroll-factor px; | ||||
|             * scale: 1 + $scroll-factor * 2; | ||||
|             */ | ||||
|         // transform: translateZ(-1px) scale(2); | ||||
|             transform: translateZ(-1px) scale(2); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .core-course-thumb-parallax-content { | ||||
|         transform: translateZ(0); | ||||
|         -webkit-filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow))); | ||||
|         filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow))); | ||||
|     } | ||||
| 
 | ||||
|     .core-course-thumb-parallax { | ||||
|         height: 40vw; | ||||
|         max-height: 35vh; | ||||
|         z-index: -1; | ||||
|         overflow: hidden; | ||||
|     } | ||||
| 
 | ||||
|     .core-course-thumb { | ||||
|         overflow: hidden; | ||||
|         text-align: center; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -68,7 +68,7 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { CoreFilterHelper } from '@features/filter/services/filter-helper'; | ||||
| import { CoreNetworkError } from '@classes/errors/network-error'; | ||||
| import { CoreSiteHome } from '@features/sitehome/services/sitehome'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSiteHomeHomeHandlerService } from '@features/sitehome/services/handlers/sitehome-home'; | ||||
| import { CoreStatusWithWarningsWSResponse } from '@services/ws'; | ||||
| 
 | ||||
| @ -1178,7 +1178,7 @@ export class CoreCourseHelperProvider { | ||||
| 
 | ||||
|         modal?.dismiss(); | ||||
| 
 | ||||
|         return this.openCourse(course, params, siteId); | ||||
|         return this.openCourse(course, { params , siteId }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -2020,20 +2020,25 @@ export class CoreCourseHelperProvider { | ||||
|      * they will see the result immediately. | ||||
|      * | ||||
|      * @param course Course to open | ||||
|      * @param params Params to pass to the course page. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param navOptions Navigation options that includes params to pass to the page. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params, siteId?: string): Promise<void> { | ||||
|     async openCourse( | ||||
|         course: CoreCourseAnyCourseData | { id: number }, | ||||
|         navOptions?: CoreNavigationOptions & { siteId?: string }, | ||||
|     ): Promise<void> { | ||||
|         const siteId = navOptions?.siteId; | ||||
|         if (!siteId || siteId == CoreSites.getCurrentSiteId()) { | ||||
|             // Current site, we can open the course.
 | ||||
|             return CoreCourse.openCourse(course, params); | ||||
|             return CoreCourse.openCourse(course, navOptions); | ||||
|         } else { | ||||
|             // We need to load the site first.
 | ||||
|             params = params || {}; | ||||
|             Object.assign(params, { course: course }); | ||||
|             navOptions = navOptions || {}; | ||||
| 
 | ||||
|             await CoreNavigator.navigateToSitePath(`course/${course.id}`, { siteId, params }); | ||||
|             navOptions.params = navOptions.params || {}; | ||||
|             Object.assign(navOptions.params, { course: course }); | ||||
| 
 | ||||
|             await CoreNavigator.navigateToSitePath(`course/${course.id}`, navOptions); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -43,7 +43,7 @@ import { CoreCourseLogCronHandler } from './handlers/log-cron'; | ||||
| import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; | ||||
| import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync'; | ||||
| import { CoreTagItem } from '@features/tag/services/tag'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; | ||||
| import { CoreCourseModuleDelegate } from './module-delegate'; | ||||
| 
 | ||||
| const ROOT_CACHE_KEY = 'mmCourse:'; | ||||
| @ -62,7 +62,7 @@ declare module '@singletons/events' { | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Completion status valid values. | ||||
|  * Course Module completion status enumeration. | ||||
|  */ | ||||
| export enum CoreCourseModuleCompletionStatus { | ||||
|     COMPLETION_INCOMPLETE = 0, | ||||
| @ -71,6 +71,11 @@ export enum CoreCourseModuleCompletionStatus { | ||||
|     COMPLETION_COMPLETE_FAIL = 3, | ||||
| } | ||||
| 
 | ||||
| export enum CoreCourseCompletionType { | ||||
|     MANUAL = 0, | ||||
|     AUTO = 1, | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Completion tracking valid values. | ||||
|  */ | ||||
| @ -1172,10 +1177,13 @@ export class CoreCourseProvider { | ||||
|      * This function must be in here instead of course helper to prevent circular dependencies. | ||||
|      * | ||||
|      * @param course Course to open | ||||
|      * @param params Other params to pass to the course page. | ||||
|      * @param navOptions Navigation options that includes params to pass to the page. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params): Promise<void> { | ||||
|     async openCourse( | ||||
|         course: CoreCourseAnyCourseData | { id: number }, | ||||
|         navOptions?: CoreNavigationOptions, | ||||
|     ): Promise<void> { | ||||
|         const loading = await CoreDomUtils.showModalLoading(); | ||||
| 
 | ||||
|         // Wait for site plugins to be fetched.
 | ||||
| @ -1192,7 +1200,7 @@ export class CoreCourseProvider { | ||||
|         if (!format || !CoreSitePlugins.sitePluginPromiseExists(`format_${format}`)) { | ||||
|             // No custom format plugin. We don't need to wait for anything.
 | ||||
|             loading.dismiss(); | ||||
|             await CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params); | ||||
|             await CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| @ -1203,7 +1211,7 @@ export class CoreCourseProvider { | ||||
| 
 | ||||
|             // The format loaded successfully, but the handlers wont be registered until all site plugins have loaded.
 | ||||
|             if (CoreSitePlugins.sitePluginsFinishedLoading) { | ||||
|                 return CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params); | ||||
|                 return CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions); | ||||
|             } | ||||
| 
 | ||||
|             // Wait for plugins to be loaded.
 | ||||
| @ -1212,7 +1220,7 @@ export class CoreCourseProvider { | ||||
|             const observer = CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => { | ||||
|                 observer?.off(); | ||||
| 
 | ||||
|                 CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params) | ||||
|                 CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions) | ||||
|                     .then(deferred.resolve).catch(deferred.reject); | ||||
|             }); | ||||
| 
 | ||||
|  | ||||
| @ -13,10 +13,10 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable, Type } from '@angular/core'; | ||||
| import { Params } from '@angular/router'; | ||||
| 
 | ||||
| import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; | ||||
| import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | ||||
| import { CoreNavigationOptions } from '@services/navigator'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreCourseWSSection } from './course'; | ||||
| import { CoreCourseSection } from './course-helper'; | ||||
| @ -66,13 +66,22 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler { | ||||
|      */ | ||||
|     displayEnableDownload?(course: CoreCourseAnyCourseData): boolean; | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the default course index should be displayed. Defaults to true. | ||||
|      * | ||||
|      * @deprecated on 4.0. Please use displayCourseIndex instead. | ||||
|      * @param course The course to check. | ||||
|      * @return Whether the default course index should be displayed. | ||||
|      */ | ||||
|     displaySectionSelector?(course: CoreCourseAnyCourseData): boolean; | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the default section selector should be displayed. Defaults to true. | ||||
|      * | ||||
|      * @param course The course to check. | ||||
|      * @return Whether the default section selector should be displayed. | ||||
|      */ | ||||
|     displaySectionSelector?(course: CoreCourseAnyCourseData): boolean; | ||||
|     displayCourseIndex?(course: CoreCourseAnyCourseData): boolean; | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format, | ||||
| @ -93,6 +102,13 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler { | ||||
|      */ | ||||
|     getCurrentSection?(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection>; | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the name for the highlighted section. | ||||
|      * | ||||
|      * @return The name for the highlighted section based on the given course format. | ||||
|      */ | ||||
|     getSectionHightlightedName?(): string; | ||||
| 
 | ||||
|     /** | ||||
|      * Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened. | ||||
|      * Implement it only if you want to create your own page to display the course. In general it's better to use the method | ||||
| @ -100,10 +116,10 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler { | ||||
|      * Your page should include the course handlers using CoreCoursesDelegate. | ||||
|      * | ||||
|      * @param course The course to open. It should contain a "format" attribute. | ||||
|      * @param params Params to pass to the course page. | ||||
|      * @param navOptions Navigation options that includes params to pass to the page. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     openCourse?(course: CoreCourseAnyCourseData, params?: Params): Promise<void>; | ||||
|     openCourse?(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise<void>; | ||||
| 
 | ||||
|     /** | ||||
|      * Return the Component to use to display the course format instead of using the default one. | ||||
| @ -125,15 +141,6 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler { | ||||
|      */ | ||||
|     getCourseSummaryComponent?(course: CoreCourseAnyCourseData): Promise<Type<unknown> | undefined>; | ||||
| 
 | ||||
|     /** | ||||
|      * Return the Component to use to display the section selector inside the default course format. | ||||
|      * It's recommended to return the class of the component, but you can also return an instance of the component. | ||||
|      * | ||||
|      * @param course The course to render. | ||||
|      * @return Promise resolved with component to use, undefined if not found. | ||||
|      */ | ||||
|     getSectionSelectorComponent?(course: CoreCourseAnyCourseData): Promise<Type<unknown> | undefined>; | ||||
| 
 | ||||
|     /** | ||||
|      * Return the Component to use to display a single section. This component will only be used if the user is viewing a | ||||
|      * single section. If all the sections are displayed at once then it won't be used. | ||||
| @ -218,12 +225,19 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the default section selector should be displayed. Defaults to true. | ||||
|      * Whether the default course index should be displayed. Defaults to true. | ||||
|      * | ||||
|      * @param course The course to check. | ||||
|      * @return Whether the section selector should be displayed. | ||||
|      * @return Whether the course index should be displayed. | ||||
|      */ | ||||
|     displaySectionSelector(course: CoreCourseAnyCourseData): boolean { | ||||
|     displayCourseIndex(course: CoreCourseAnyCourseData): boolean { | ||||
|         const display = this.executeFunctionOnEnabled<boolean>(course.format || '', 'displayCourseIndex', [course]); | ||||
| 
 | ||||
|         if (display !== undefined) { | ||||
|             return display; | ||||
|         } | ||||
| 
 | ||||
|         // Use displaySectionSelector while is not completely deprecated.
 | ||||
|         return !!this.executeFunctionOnEnabled<boolean>(course.format || '', 'displaySectionSelector', [course]); | ||||
|     } | ||||
| 
 | ||||
| @ -276,8 +290,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]) || ''; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -287,9 +301,9 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm | ||||
|      * @param sections List of sections. | ||||
|      * @return Promise resolved with current section. | ||||
|      */ | ||||
|     async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> { | ||||
|     async getCurrentSection<T = CoreCourseSection>(course: CoreCourseAnyCourseData, sections: T[]): Promise<T> { | ||||
|         try { | ||||
|             const section = await this.executeFunctionOnEnabled<CoreCourseSection>( | ||||
|             const section = await this.executeFunctionOnEnabled<T>( | ||||
|                 course.format || '', | ||||
|                 'getCurrentSection', | ||||
|                 [course, sections], | ||||
| @ -303,17 +317,16 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the component to use to display the section selector inside the default course format. | ||||
|      * Returns the name for the highlighted section. | ||||
|      * | ||||
|      * @param course The course to render. | ||||
|      * @return Promise resolved with component to use, undefined if not found. | ||||
|      * @param course The course to get the text. | ||||
|      * @return The name for the highlighted section based on the given course format. | ||||
|      */ | ||||
|     async getSectionSelectorComponent(course: CoreCourseAnyCourseData): Promise<Type<unknown> | undefined> { | ||||
|         try { | ||||
|             return await this.executeFunctionOnEnabled<Type<unknown>>(course.format || '', 'getSectionSelectorComponent', [course]); | ||||
|         } catch (error) { | ||||
|             this.logger.error('Error getting section selector component', error); | ||||
|         } | ||||
|     getSectionHightlightedName(course: CoreCourseAnyCourseData): string | undefined { | ||||
|         return this.executeFunctionOnEnabled<string>( | ||||
|             course.format || '', | ||||
|             'getSectionHightlightedName', | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -346,11 +359,11 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm | ||||
|      * Open a course. Should not be called directly. Call CoreCourseHelper.openCourse instead. | ||||
|      * | ||||
|      * @param course The course to open. It should contain a "format" attribute. | ||||
|      * @param params Params to pass to the course page. | ||||
|      * @param navOptions Navigation options that includes params to pass to the page. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async openCourse(course: CoreCourseAnyCourseData, params?: Params): Promise<void> { | ||||
|         await this.executeFunctionOnEnabled(course.format || '', 'openCourse', [course, params]); | ||||
|     async openCourse(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise<void> { | ||||
|         await this.executeFunctionOnEnabled(course.format || '', 'openCourse', [course, navOptions]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -13,12 +13,10 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { Params } from '@angular/router'; | ||||
| 
 | ||||
| import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreCourseWSSection } from '../course'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreCourseSection } from '../course-helper'; | ||||
| import { CoreCourseFormatHandler } from '../format-delegate'; | ||||
| 
 | ||||
| @ -32,19 +30,14 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { | ||||
|     format = 'default'; | ||||
| 
 | ||||
|     /** | ||||
|      * Whether or not the handler is enabled on a site level. | ||||
|      * | ||||
|      * @return Promise resolved with true if enabled. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the title to use in course page. | ||||
|      * | ||||
|      * @param course The course. | ||||
|      * @return Title. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getCourseTitle(course: CoreCourseAnyCourseData): string { | ||||
|         if (course.displayname) { | ||||
| @ -57,57 +50,35 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether it allows seeing all sections at the same time. Defaults to true. | ||||
|      * | ||||
|      * @param course The course to check. | ||||
|      * @return Whether it can view all sections. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     canViewAllSections(course: CoreCourseAnyCourseData): boolean { | ||||
|     canViewAllSections(): boolean { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the option blocks should be displayed. Defaults to true. | ||||
|      * | ||||
|      * @param course The course to check. | ||||
|      * @return Whether it can display blocks. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     displayBlocks(course: CoreCourseAnyCourseData): boolean { | ||||
|     displayBlocks(): boolean { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the default section selector should be displayed. Defaults to true. | ||||
|      * | ||||
|      * @param course The course to check. | ||||
|      * @return Whether the default section selector should be displayed. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     displaySectionSelector(course: CoreCourseAnyCourseData): boolean { | ||||
|     displayCourseIndex(): boolean { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format, | ||||
|      * and the doRefresh method of CoreCourseSectionPage must be called on refresh. Defaults to true. | ||||
|      * | ||||
|      * @param course The course to check. | ||||
|      * @param sections List of course sections. | ||||
|      * @return Whether the refresher should be displayed. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     displayRefresher(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): boolean { | ||||
|     displayRefresher(): boolean { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given a list of sections, get the "current" section that should be displayed first. | ||||
|      * | ||||
|      * @param course The course to get the title. | ||||
|      * @param sections List of sections. | ||||
|      * @return Current section (or promise resolved with current section). | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> { | ||||
|         let marker: number | undefined; | ||||
| @ -137,48 +108,40 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Invalidate the data required to load the course format. | ||||
|      * | ||||
|      * @param course The course to get the title. | ||||
|      * @param sections List of sections. | ||||
|      * @return Promise resolved when the data is invalidated. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     async invalidateData(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): Promise<void> { | ||||
|     getSectionHightlightedName(): string { | ||||
|         return Translate.instant('core.course.highlighted'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async invalidateData(course: CoreCourseAnyCourseData): Promise<void> { | ||||
|         await CoreCourses.invalidateCoursesByField('id', course.id); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened. | ||||
|      * Implement it only if you want to create your own page to display the course. In general it's better to use the method | ||||
|      * getCourseFormatComponent because it will display the course handlers at the top. | ||||
|      * Your page should include the course handlers using CoreCoursesDelegate. | ||||
|      * | ||||
|      * @param course The course to open. It should contain a "format" attribute. | ||||
|      * @param params Params to pass to the course page. | ||||
|      * @return Promise resolved when done. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async openCourse(course: CoreCourseAnyCourseData, params?: Params): Promise<void> { | ||||
|         params = params || {}; | ||||
|         Object.assign(params, { course: course }); | ||||
|     async openCourse(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise<void> { | ||||
|         navOptions = navOptions || {}; | ||||
| 
 | ||||
|         navOptions.params = navOptions.params || {}; | ||||
|         Object.assign(navOptions.params, { course: course }); | ||||
| 
 | ||||
|         // Don't return the .push promise, we don't want to display a loading modal during the page transition.
 | ||||
|         const currentTab = CoreNavigator.getCurrentMainMenuTab(); | ||||
|         const routeDepth = CoreNavigator.getRouteDepth(`/main/${currentTab}/course/${course.id}`); | ||||
|         const deepPath = '/deep'.repeat(routeDepth); | ||||
| 
 | ||||
|         CoreNavigator.navigateToSitePath(`course${deepPath}/${course.id}`, { params }); | ||||
|         CoreNavigator.navigateToSitePath(`course${deepPath}/${course.id}`, navOptions); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether the view should be refreshed when completion changes. If your course format doesn't display | ||||
|      * activity completion then you should return false. | ||||
|      * | ||||
|      * @param course The course. | ||||
|      * @return Whether course view should be refreshed when an activity completion changes. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|     async shouldRefreshWhenCompletionChanges(course: CoreCourseAnyCourseData): Promise<boolean> { | ||||
|     async shouldRefreshWhenCompletionChanges(): Promise<boolean> { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -171,7 +171,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On | ||||
|             CoreCourseHelper.openCourse(this.course); | ||||
|         } else { | ||||
|             CoreNavigator.navigateToSitePath( | ||||
|                 '/course/' + this.course.id + '/preview', | ||||
|                 `/course/${this.course.id}/preview`, | ||||
|                 { params: { course: this.course } }, | ||||
|             ); | ||||
|         } | ||||
|  | ||||
| @ -156,7 +156,7 @@ export class CoreCoursesCourseLinkHandlerService extends CoreContentLinksHandler | ||||
|         modal.dismiss(); | ||||
| 
 | ||||
|         // Now open the course.
 | ||||
|         CoreCourseHelper.openCourse(course, pageParams); | ||||
|         CoreCourseHelper.openCourse(course, { params: pageParams }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -38,8 +38,9 @@ export class CoreSitePluginsCourseFormatHandler extends CoreSitePluginsBaseHandl | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     displaySectionSelector(): boolean { | ||||
|         return this.handlerSchema.displaysectionselector ?? true; | ||||
|     displayCourseIndex(): boolean { | ||||
|         // Use displaysectionselector while is not completely deprecated.
 | ||||
|         return this.handlerSchema.displaycourseindex ?? this.handlerSchema.displaysectionselector ?? true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -16,7 +16,7 @@ import { Component, OnChanges, Input, ViewChild, Output, EventEmitter } from '@a | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| 
 | ||||
| import { CoreCourseFormatComponent } from '@features/course/components/format/format'; | ||||
| import { CoreCourseModuleCompletionData, CoreCourseSectionWithStatus } from '@features/course/services/course-helper'; | ||||
| import { CoreCourseModuleCompletionData, CoreCourseSection } from '@features/course/services/course-helper'; | ||||
| import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; | ||||
| import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | ||||
| import { CoreSitePlugins, CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins'; | ||||
| @ -33,7 +33,7 @@ import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin- | ||||
| export class CoreSitePluginsCourseFormatComponent implements OnChanges { | ||||
| 
 | ||||
|     @Input() course?: CoreCourseAnyCourseData; // The course to render.
 | ||||
|     @Input() sections?: CoreCourseSectionWithStatus[]; // List of course sections. The status will be calculated in this component.
 | ||||
|     @Input() sections?: CoreCourseSection[]; // List of course sections. The status will be calculated in this component.
 | ||||
|     @Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
 | ||||
|     @Input() initialSectionId?: number; // The section to load first (by ID).
 | ||||
|     @Input() initialSectionNumber?: number; // The section to load first (by number).
 | ||||
|  | ||||
| @ -884,7 +884,11 @@ export type CoreSitePluginsCourseModuleHandlerData = CoreSitePluginsHandlerCommo | ||||
| export type CoreSitePluginsCourseFormatHandlerData = CoreSitePluginsHandlerCommonData & { | ||||
|     canviewallsections?: boolean; | ||||
|     displayenabledownload?: boolean; | ||||
|     /** | ||||
|      * @deprecated on 4.0, use displaycourseindex instead. | ||||
|      */ | ||||
|     displaysectionselector?: boolean; | ||||
|     displaycourseindex?: boolean; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -301,6 +301,7 @@ | ||||
|     "strftimetime24": "%H:%M", | ||||
|     "submit": "Submit", | ||||
|     "success": "Success", | ||||
|     "summary": "Summary", | ||||
|     "tablet": "Tablet", | ||||
|     "teachers": "Teachers", | ||||
|     "thereisdatatosync": "There are offline {{$a}} to be synchronised.", | ||||
|  | ||||
| @ -1118,7 +1118,7 @@ export class CoreDomUtilsProvider { | ||||
|             content.scrollToPoint(position[0], position[1], duration || 0); | ||||
| 
 | ||||
|             return true; | ||||
|         } catch (error) { | ||||
|         } catch { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -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. | ||||
|  */ | ||||
|  | ||||
| @ -382,17 +382,21 @@ ion-alert.core-nohead { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| ion-alert .alert-wrapper { | ||||
| ion-alert { | ||||
|     --border-radius: var(--huge-radius); | ||||
|     .alert-wrapper { | ||||
|         overflow: auto; | ||||
| } | ||||
|         border-radius: var(--border-radius) !important; | ||||
| 
 | ||||
| ion-alert .alert-message { | ||||
|         button.alert-button { | ||||
|             color: var(--primary); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .alert-message { | ||||
|         user-select: text; | ||||
|         flex-shrink: 0; | ||||
| } | ||||
| 
 | ||||
| ion-alert .alert-wrapper button.alert-button { | ||||
|     color: var(--primary); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Ionic list. | ||||
| @ -844,6 +848,8 @@ ion-select-popover ion-item.core-select-option-title { | ||||
| 
 | ||||
| ion-chip { | ||||
|     line-height: 1.1; | ||||
|     border-radius: var(--medium-radius); | ||||
|     @include padding-horizontal(16px); | ||||
| } | ||||
| 
 | ||||
| ion-searchbar { | ||||
| @ -1167,9 +1173,8 @@ ion-item.item-input ion-input.has-focus { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Ionic set this value to 0 without px that provoked miscalculations. | ||||
| ion-item-divider { | ||||
|     --inner-padding-end: 0px; | ||||
|     --inner-padding-end: 8px; | ||||
| } | ||||
| 
 | ||||
| // Change default outline. | ||||
| @ -1225,6 +1230,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 +1259,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 +1274,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 +1291,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 +1301,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; | ||||
|  | ||||
| @ -105,7 +105,9 @@ | ||||
|     --subdued-text-color: #595959; | ||||
| 
 | ||||
|     --small-radius: 4px; | ||||
|     --big-radius: 8px; | ||||
|     --medium-radius: 8px; | ||||
|     --big-radius: 16px; | ||||
|     --huge-radius: 24px; | ||||
| 
 | ||||
|     --ion-card-color: var(--text-color); | ||||
|     ion-card { | ||||
| @ -113,7 +115,7 @@ | ||||
|         --border-style: solid; | ||||
|         --border-color: var(--stroke); | ||||
|         --box-shadow: none; | ||||
|         --border-radius: var(--big-radius); | ||||
|         --border-radius: var(--medium-radius); | ||||
|     } | ||||
| 
 | ||||
|     --text-hightlight-background-color: #{$core-text-hightlight-background-color}; | ||||
| @ -181,7 +183,7 @@ | ||||
|             height: var(--a11y-min-target-size); | ||||
|             border: 1px solid var(--stroke); | ||||
|             box-shadow: none; | ||||
|             border-radius: var(--big-radius); | ||||
|             border-radius: var(--medium-radius); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -282,7 +284,7 @@ | ||||
|     --core-combobox-color: var(--gray-900); | ||||
|     --core-combobox-border-color: var(--stroke); | ||||
|     --core-combobox-border-width: 1px; | ||||
|     --core-combobox-radius: var(--big-radius); | ||||
|     --core-combobox-radius: var(--medium-radius); | ||||
|     --core-combobox-box-shadow: none; | ||||
| 
 | ||||
|     --selected-item-color: var(--primary); | ||||
| @ -301,7 +303,7 @@ | ||||
|     --core-send-message-input-background: var(--gray-200); | ||||
|     --core-send-message-input-color: var(--gray-900); | ||||
| 
 | ||||
|     --core-courseimage-on-course-height: 150px; | ||||
|     --core-courseimage-on-course-size: 72px; | ||||
| 
 | ||||
|     --core-course-module-navigation-max-height: 56px; | ||||
|     --core-course-module-navigation-background: var(--contrast-background); | ||||
|  | ||||
| @ -15,6 +15,8 @@ information provided here is intended especially for developers. | ||||
|     The function CoreUserDelegate.getProfileHandlersFor must now receive a context + contextId instead of a courseId. | ||||
|     The user handler function isEnabledForCourse is now called isEnabledForContext and receives a context + contextId instead of a courseId. | ||||
|     Some user handler's functions have also changed to accept context + contextId instead of a courseId: isEnabledForUser, getDisplayData, action. | ||||
| - CoreCourseHelperProvider.openCourse parameters changed, now it admits CoreNavigationOptions + siteId on the same object that includes Params passed to page. | ||||
| - displaySectionSelector has been deprecated on CoreCourseFormatHandler, use displayCourseIndex instead. | ||||
| 
 | ||||
| === 3.9.5 === | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user