forked from EVOgeek/Vmeda.Online
		
	
						commit
						49afa105bc
					
				| @ -1536,19 +1536,23 @@ | |||||||
|   "core.course.confirmpartialdownloadsize": "local_moodlemobileapp", |   "core.course.confirmpartialdownloadsize": "local_moodlemobileapp", | ||||||
|   "core.course.couldnotloadsectioncontent": "local_moodlemobileapp", |   "core.course.couldnotloadsectioncontent": "local_moodlemobileapp", | ||||||
|   "core.course.couldnotloadsections": "local_moodlemobileapp", |   "core.course.couldnotloadsections": "local_moodlemobileapp", | ||||||
|  |   "core.course.courseindex": "courseformat", | ||||||
|   "core.course.coursesummary": "moodle", |   "core.course.coursesummary": "moodle", | ||||||
|  |   "core.course.done": "completion", | ||||||
|   "core.course.downloadcourse": "tool_mobile", |   "core.course.downloadcourse": "tool_mobile", | ||||||
|   "core.course.downloadcoursesprogressdescription": "local_moodlemobileapp", |   "core.course.downloadcoursesprogressdescription": "local_moodlemobileapp", | ||||||
|   "core.course.downloadsectionprogressdescription": "local_moodlemobileapp", |   "core.course.downloadsectionprogressdescription": "local_moodlemobileapp", | ||||||
|   "core.course.errordownloadingcourse": "local_moodlemobileapp", |   "core.course.errordownloadingcourse": "local_moodlemobileapp", | ||||||
|   "core.course.errordownloadingsection": "local_moodlemobileapp", |   "core.course.errordownloadingsection": "local_moodlemobileapp", | ||||||
|   "core.course.errorgetmodule": "local_moodlemobileapp", |   "core.course.errorgetmodule": "local_moodlemobileapp", | ||||||
|  |   "core.course.failed": "completion", | ||||||
|   "core.course.gotonextactivity": "local_moodlemobileapp", |   "core.course.gotonextactivity": "local_moodlemobileapp", | ||||||
|   "core.course.gotonextactivitynotfound": "local_moodlemobileapp", |   "core.course.gotonextactivitynotfound": "local_moodlemobileapp", | ||||||
|   "core.course.gotopreviousactivity": "local_moodlemobileapp", |   "core.course.gotopreviousactivity": "local_moodlemobileapp", | ||||||
|   "core.course.gotopreviousactivitynotfound": "local_moodlemobileapp", |   "core.course.gotopreviousactivitynotfound": "local_moodlemobileapp", | ||||||
|   "core.course.hiddenfromstudents": "moodle", |   "core.course.hiddenfromstudents": "moodle", | ||||||
|   "core.course.hiddenoncoursepage": "moodle", |   "core.course.hiddenoncoursepage": "moodle", | ||||||
|  |   "core.course.highlighted": "moodle", | ||||||
|   "core.course.insufficientavailablequota": "local_moodlemobileapp", |   "core.course.insufficientavailablequota": "local_moodlemobileapp", | ||||||
|   "core.course.insufficientavailablespace": "local_moodlemobileapp", |   "core.course.insufficientavailablespace": "local_moodlemobileapp", | ||||||
|   "core.course.manualcompletionnotsynced": "local_moodlemobileapp", |   "core.course.manualcompletionnotsynced": "local_moodlemobileapp", | ||||||
| @ -1557,7 +1561,8 @@ | |||||||
|   "core.course.overriddennotice": "grades", |   "core.course.overriddennotice": "grades", | ||||||
|   "core.course.refreshcourse": "local_moodlemobileapp", |   "core.course.refreshcourse": "local_moodlemobileapp", | ||||||
|   "core.course.section": "moodle", |   "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.useactivityonbrowser": "local_moodlemobileapp", | ||||||
|   "core.course.warningmanualcompletionmodified": "local_moodlemobileapp", |   "core.course.warningmanualcompletionmodified": "local_moodlemobileapp", | ||||||
|   "core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp", |   "core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp", | ||||||
| @ -2257,6 +2262,7 @@ | |||||||
|   "core.strftimetime24": "langconfig", |   "core.strftimetime24": "langconfig", | ||||||
|   "core.submit": "moodle", |   "core.submit": "moodle", | ||||||
|   "core.success": "moodle", |   "core.success": "moodle", | ||||||
|  |   "core.summary": "moodle", | ||||||
|   "core.tablet": "local_moodlemobileapp", |   "core.tablet": "local_moodlemobileapp", | ||||||
|   "core.tag.defautltagcoll": "tag", |   "core.tag.defautltagcoll": "tag", | ||||||
|   "core.tag.errorareanotsupported": "local_moodlemobileapp", |   "core.tag.errorareanotsupported": "local_moodlemobileapp", | ||||||
|  | |||||||
| @ -95,7 +95,7 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase | |||||||
| 
 | 
 | ||||||
|         this.getResourceData(module, courseId, handlerData).then((extra) => { |         this.getResourceData(module, courseId, handlerData).then((extra) => { | ||||||
|             handlerData.extraBadge = extra; |             handlerData.extraBadge = extra; | ||||||
|             handlerData.extraBadgeColor = 'light'; |             handlerData.extraBadgeColor = ''; | ||||||
| 
 | 
 | ||||||
|             return; |             return; | ||||||
|         }).catch(() => { |         }).catch(() => { | ||||||
|  | |||||||
| @ -93,6 +93,7 @@ export const moodleTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     rootAnimation.addAnimation(enteringContentAnimation); |     rootAnimation.addAnimation(enteringContentAnimation); | ||||||
|  |     enteringContentAnimation.beforeAddClass('animating').afterRemoveClass('animating'); | ||||||
| 
 | 
 | ||||||
|     if (backDirection) { |     if (backDirection) { | ||||||
|         enteringContentAnimation |         enteringContentAnimation | ||||||
| @ -214,6 +215,8 @@ export const moodleTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp | |||||||
|     // setup leaving view
 |     // setup leaving view
 | ||||||
|     if (leavingEl) { |     if (leavingEl) { | ||||||
|         const leavingContent = createAnimation(); |         const leavingContent = createAnimation(); | ||||||
|  |         leavingContent.beforeAddClass('animating').afterRemoveClass('animating'); | ||||||
|  | 
 | ||||||
|         const leavingContentEl = leavingEl.querySelector(':scope > ion-content'); |         const leavingContentEl = leavingEl.querySelector(':scope > ion-content'); | ||||||
|         const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar'); |         const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar'); | ||||||
|         const leavingHeaderEls = leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *'); |         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() layout: 'icon-top' | 'icon-start' | 'icon-end' | 'icon-bottom' | 'icon-hide' | 'label-hide' = 'icon-hide'; | ||||||
|     @Input() tabs: CoreTabsOutletTab[] = []; |     @Input() tabs: CoreTabsOutletTab[] = []; | ||||||
| 
 | 
 | ||||||
|     @ViewChild(IonTabs) protected ionTabs?: IonTabs; |     @ViewChild(IonTabs) protected ionTabs!: IonTabs; | ||||||
| 
 | 
 | ||||||
|     protected stackEventsSubscription?: Subscription; |     protected stackEventsSubscription?: Subscription; | ||||||
|     protected outletActivatedSubscription?: Subscription; |     protected outletActivatedSubscription?: Subscription; | ||||||
| @ -96,11 +96,17 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.tabsElement = this.element.nativeElement.querySelector('ion-tabs'); |         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) { |             if (!this.isCurrentView) { | ||||||
|                 return; |                 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); |             this.showHideNavBarButtons(stackEvent.enteringView.element.tagName); | ||||||
| 
 | 
 | ||||||
|             await this.listenContentScroll(stackEvent.enteringView.element, stackEvent.enteringView.id); |             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.showHideTabs(scrollElement.scrollTop, scrollElement); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|         this.outletActivatedSubscription = this.ionTabs?.outlet.activateEvents.subscribe(() => { |         this.outletActivatedSubscription = this.ionTabs.outlet.activateEvents.subscribe(() => { | ||||||
|             this.lastActiveComponent = this.ionTabs?.outlet.component; |             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,
 |         // 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
 |         // 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).
 |         // entering in response to a back navigation from the page on top).
 | ||||||
|         if (this.existsInNavigationStack && this.ionTabs?.outlet.isActivated) { |         if (this.existsInNavigationStack && this.ionTabs.outlet.isActivated) { | ||||||
|             (this.ionTabs?.outlet.component as Partial<ViewDidEnter>).ionViewDidEnter?.(); |             (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
 |         // 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. |      * @param activatedPageName Activated page name. | ||||||
|      */ |      */ | ||||||
|     protected showHideNavBarButtons(activatedPageName: string): void { |     protected showHideNavBarButtons(activatedPageName: string): void { | ||||||
|         const elements = this.ionTabs!.outlet.nativeEl.querySelectorAll('core-navbar-buttons'); |         const elements = this.ionTabs.outlet.nativeEl.querySelectorAll('core-navbar-buttons'); | ||||||
|         const domUtils = CoreDomUtils.instance; |  | ||||||
|         elements.forEach((element) => { |         elements.forEach((element) => { | ||||||
|             const instance = domUtils.getInstanceByElement<CoreNavBarButtonsComponent>(element); |             const instance = CoreDomUtils.getInstanceByElement<CoreNavBarButtonsComponent>(element); | ||||||
| 
 | 
 | ||||||
|             if (instance) { |             if (instance) { | ||||||
|                 const pagetagName = element.closest('.ion-page')?.tagName; |                 const pagetagName = element.closest('.ion-page')?.tagName; | ||||||
|  | |||||||
| @ -43,6 +43,10 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | |||||||
|     protected headerSubHeadingFontSize = 0; |     protected headerSubHeadingFontSize = 0; | ||||||
|     protected contentSubHeadingFontSize = 0; |     protected contentSubHeadingFontSize = 0; | ||||||
|     protected subHeadingStartDifference = 0; |     protected subHeadingStartDifference = 0; | ||||||
|  |     protected inContent = true; | ||||||
|  |     protected title?: HTMLElement | null; | ||||||
|  |     protected titleHeight = 0; | ||||||
|  |     protected contentH1?: HTMLElement | null; | ||||||
| 
 | 
 | ||||||
|     constructor(el: ElementRef<HTMLIonHeaderElement>) { |     constructor(el: ElementRef<HTMLIonHeaderElement>) { | ||||||
|         this.header = el.nativeElement; |         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. |      * 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. |      * @return Promise resolved with Loading Id, if any. | ||||||
|      */ |      */ | ||||||
|     protected async getLoadingId(): Promise<string | undefined> { |     protected async getLoadingId(): Promise<string | undefined> { | ||||||
| @ -80,6 +82,17 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | |||||||
| 
 | 
 | ||||||
|                 return; |                 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; |         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. |      * Call this function when header is not collapsible. | ||||||
|      */ |      */ | ||||||
|     protected cannotCollapse(): void { |     protected cannotCollapse(): void { | ||||||
|  |         this.content = undefined; | ||||||
|         this.loadingObserver.off(); |         this.loadingObserver.off(); | ||||||
|         this.header.classList.add('core-header-collapsed'); |         this.header.classList.add('core-header-collapsed'); | ||||||
|     } |     } | ||||||
| @ -112,16 +126,25 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | |||||||
|             await animation.finished; |             await animation.finished; | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         const title = this.content.querySelector<HTMLElement>('.collapsible-title, h1'); |         let title = this.content.querySelector<HTMLElement>('.collapsible-title'); | ||||||
|         const contentH1 = this.content.querySelector<HTMLElement>('h1'); |         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'); |         const headerH1 = this.header.querySelector<HTMLElement>('h1'); | ||||||
|         if (!title || !contentH1 || !headerH1) { |         if (!title || !this.contentH1 || !headerH1 || !this.contentH1.parentElement) { | ||||||
|             this.cannotCollapse(); |             this.cannotCollapse(); | ||||||
| 
 | 
 | ||||||
|             return; |             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) { |         if (this.titleTopDifference <= 0) { | ||||||
|             this.cannotCollapse(); |             this.cannotCollapse(); | ||||||
| 
 | 
 | ||||||
| @ -141,17 +164,17 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const headerH1Styles = getComputedStyle(headerH1); |         const headerH1Styles = getComputedStyle(headerH1); | ||||||
|         const contentH1Styles = getComputedStyle(contentH1); |         const contentH1Styles = getComputedStyle(this.contentH1); | ||||||
| 
 | 
 | ||||||
|         if (Platform.isRTL) { |         if (Platform.isRTL) { | ||||||
|             // Checking position over parent because transition may not be finished.
 |             // 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; |             const headerH1Position = headerH1.getBoundingClientRect().right - this.header.getBoundingClientRect().right; | ||||||
| 
 | 
 | ||||||
|             this.h1StartDifference = Math.round(contentH1Position - (headerH1Position - parseFloat(headerH1Styles.paddingRight))); |             this.h1StartDifference = Math.round(contentH1Position - (headerH1Position - parseFloat(headerH1Styles.paddingRight))); | ||||||
|         } else { |         } else { | ||||||
|             // Checking position over parent because transition may not be finished.
 |             // 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; |             const headerH1Position = headerH1.getBoundingClientRect().left - this.header.getBoundingClientRect().left; | ||||||
| 
 | 
 | ||||||
|             this.h1StartDifference = Math.round(contentH1Position - (headerH1Position + parseFloat(headerH1Styles.paddingLeft))); |             this.h1StartDifference = Math.round(contentH1Position - (headerH1Position + parseFloat(headerH1Styles.paddingLeft))); | ||||||
| @ -165,10 +188,10 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | |||||||
|             if (styleName != 'font-size' && |             if (styleName != 'font-size' && | ||||||
|                 styleName != 'font-family' && |                 styleName != 'font-family' && | ||||||
|                 (styleName.startsWith('font-') || styleName.startsWith('letter-'))) { |                 (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', |             '--max-width', | ||||||
|             (parseFloat(headerH1Styles.width) |             (parseFloat(headerH1Styles.width) | ||||||
|                 -parseFloat(headerH1Styles.paddingLeft) |                 -parseFloat(headerH1Styles.paddingLeft) | ||||||
| @ -176,47 +199,70 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { | |||||||
|                 +'px'), |                 +'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.
 |         // 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 == '') { |         if (color == '') { | ||||||
|             color = getComputedStyle(title).getPropertyValue('--background').trim(); |             color = getComputedStyle(this.title).getPropertyValue('--background').trim(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const underHeader = document.createElement('div'); |         const underHeader = document.createElement('div'); | ||||||
|         underHeader.classList.add('core-underheader'); |         underHeader.classList.add('core-underheader'); | ||||||
|         underHeader.style.setProperty('height', this.header.clientHeight + 'px'); |         underHeader.style.setProperty('height', this.header.clientHeight + 'px'); | ||||||
|         underHeader.style.setProperty('background', color); |         underHeader.style.setProperty('background', color); | ||||||
|         this.content.shadowRoot?.querySelector('#background-content')?.prepend(underHeader); |         if (this.inContent) { | ||||||
| 
 |             this.content.shadowRoot?.querySelector('#background-content')?.prepend(underHeader); | ||||||
|         this.content.style.setProperty('--offset-top', this.header.clientHeight + 'px'); |             this.content.style.setProperty('--offset-top', this.header.clientHeight + 'px'); | ||||||
| 
 |         } else { | ||||||
|         // Subheading.
 |             if (!this.header.closest('.ion-page')?.querySelector('.core-underheader')) { | ||||||
|         const headerSubHeading = this.header.querySelector<HTMLElement>('h2,.subheading'); |                 this.header.closest('.ion-page')?.insertBefore(underHeader, this.header); | ||||||
|         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)); |  | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|             contentSubHeading.setAttribute('aria-hidden', 'true'); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.content.scrollEvents = true; |         this.content.scrollEvents = true; | ||||||
|         this.content.addEventListener('ionScroll', (e: CustomEvent<ScrollDetail>): void => { |         this.content.addEventListener('ionScroll', (e: CustomEvent<ScrollDetail>): void => { | ||||||
|             if (e.target == this.content) { |             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. |      * On scroll function. | ||||||
|      * |      * | ||||||
|      * @param title Title on ion content. |      * @param scrollTop Scroll top measure. | ||||||
|      * @param contentH1 Heading 1 of title, if found. |  | ||||||
|      * @param scrollDetail Event details. |  | ||||||
|      */ |      */ | ||||||
|     protected onScroll( |     protected onScroll( | ||||||
|         title: HTMLElement, |         scrollTop: number, | ||||||
|         contentH1: HTMLElement, |  | ||||||
|         contentSubheading: HTMLElement | null, |  | ||||||
|         scrollDetail: ScrollDetail, |  | ||||||
|     ): void { |     ): 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; |         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.
 |         // Check total collapse.
 | ||||||
|         this.header.classList.toggle('core-header-collapsed', collapsed); |         this.header.classList.toggle('core-header-collapsed', collapsed); | ||||||
|         title.classList.toggle('collapsible-title-collapsed', collapsed); |         this.title.classList.toggle('collapsible-title-collapsed', collapsed); | ||||||
|         title.classList.toggle('collapsible-title-collapse-started', scrollDetail.scrollTop > 0); |         this.title.classList.toggle('collapsible-title-collapse-started', scrollTop > 0); | ||||||
|         title.classList.toggle('collapsible-title-collapse-nowrap', progress > 0.5); |         this.title.classList.toggle('collapsible-title-collapse-nowrap', progress > 0.5); | ||||||
|         title.style.setProperty('--collapse-opacity', (1 - progress) +''); |         this.title.style.setProperty('--collapse-opacity', (1 - progress) +''); | ||||||
| 
 | 
 | ||||||
|         if (collapsed) { |         if (collapsed) { | ||||||
|             contentH1.style.transform = 'translateX(-' + this.h1StartDifference + 'px)'; |             this.contentH1.style.transform = 'translateX(-' + this.h1StartDifference + 'px)'; | ||||||
|             contentH1.style.setProperty('font-size', this.headerH1FontSize + 'px'); |             this.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'); |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Zoom font-size out.
 |         // Zoom font-size out.
 | ||||||
|         const newFontSize = this.contentH1FontSize - ((this.contentH1FontSize - this.headerH1FontSize) * progress); |         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.
 |         // Move.
 | ||||||
|         const newStart = - this.h1StartDifference * progress; |         const newStart = - this.h1StartDifference * progress; | ||||||
|         contentH1.style.transform = 'translateX(' + newStart + 'px)'; |         this.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)'; |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ | |||||||
|     @include position(50%, 0px, null, null); |     @include position(50%, 0px, null, null); | ||||||
|     position: fixed; |     position: fixed; | ||||||
|     z-index: 10; |     z-index: 10; | ||||||
|  |     transform: translateY(-50%); | ||||||
| 
 | 
 | ||||||
|     ion-button { |     ion-button { | ||||||
|         margin: 0; |         margin: 0; | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ import { CoreCourseFormatComponent } from './format/format'; | |||||||
| import { CoreCourseModuleComponent } from './module/module'; | import { CoreCourseModuleComponent } from './module/module'; | ||||||
| import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion'; | import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion'; | ||||||
| import { CoreCourseModuleDescriptionComponent } from './module-description/module-description'; | 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 { CoreCourseTagAreaComponent } from './tag-area/tag-area'; | ||||||
| import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module'; | import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module'; | ||||||
| import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy'; | import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy'; | ||||||
| @ -37,7 +37,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module- | |||||||
|         CoreCourseModuleDescriptionComponent, |         CoreCourseModuleDescriptionComponent, | ||||||
|         CoreCourseModuleInfoComponent, |         CoreCourseModuleInfoComponent, | ||||||
|         CoreCourseModuleManualCompletionComponent, |         CoreCourseModuleManualCompletionComponent, | ||||||
|         CoreCourseSectionSelectorComponent, |         CoreCourseCourseIndexComponent, | ||||||
|         CoreCourseTagAreaComponent, |         CoreCourseTagAreaComponent, | ||||||
|         CoreCourseUnsupportedModuleComponent, |         CoreCourseUnsupportedModuleComponent, | ||||||
|         CoreCourseModuleNavigationComponent, |         CoreCourseModuleNavigationComponent, | ||||||
| @ -54,7 +54,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module- | |||||||
|         CoreCourseModuleDescriptionComponent, |         CoreCourseModuleDescriptionComponent, | ||||||
|         CoreCourseModuleInfoComponent, |         CoreCourseModuleInfoComponent, | ||||||
|         CoreCourseModuleManualCompletionComponent, |         CoreCourseModuleManualCompletionComponent, | ||||||
|         CoreCourseSectionSelectorComponent, |         CoreCourseCourseIndexComponent, | ||||||
|         CoreCourseTagAreaComponent, |         CoreCourseTagAreaComponent, | ||||||
|         CoreCourseUnsupportedModuleComponent, |         CoreCourseUnsupportedModuleComponent, | ||||||
|         CoreCourseModuleNavigationComponent, |         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. --> | <!-- 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-navbar-buttons slot="end" *ngIf="loaded"> | ||||||
|     <core-context-menu> |     <core-context-menu> | ||||||
|         <core-context-menu-item [hidden]="!displaySectionSelector || !sections || !sections.length" [priority]="500" |         <core-context-menu-item [hidden]="!displayCourseIndex || !sections || !sections.length" [priority]="500" | ||||||
|             [content]="'core.course.sections' | translate" (action)="showSectionSelector()" iconAction="menu"> |             [content]="'core.course.courseindex' | translate" (action)="openCourseIndex()" iconAction="menu"> | ||||||
|         </core-context-menu-item> |         </core-context-menu-item> | ||||||
|     </core-context-menu> |     </core-context-menu> | ||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
| <core-dynamic-component [component]="courseFormatComponent" [data]="data"> | <core-dynamic-component [component]="courseFormatComponent" [data]="data"> | ||||||
|     <!-- Default course format. --> |     <!-- Default course format. --> | ||||||
|     <core-loading [hideUntil]="loaded"> |     <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. --> |         <!-- Single section. --> | ||||||
|         <div *ngIf="selectedSection && selectedSection.id != allSectionsId"> |         <div *ngIf="selectedSection && selectedSection.id != allSectionsId"> | ||||||
| @ -84,63 +34,75 @@ | |||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <ion-buttons class="ion-padding core-course-section-nav-buttons safe-area-padding-horizontal" |         <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" |             <ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" fill="outline" color="primary" | ||||||
|                 [attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name"> |                 [attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name"> | ||||||
|                 <ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon> |                 <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> |                 </core-format-text> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|             <ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" fill="solid" color="primary" |             <ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" fill="solid" color="primary" | ||||||
|                 [attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name"> |                 [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> |                 </core-format-text> | ||||||
|                 <ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon> |                 <ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
| 
 |  | ||||||
|         <core-block-side-blocks-button *ngIf="course && displayBlocks && hasBlocks" [courseId]="course.id"> |  | ||||||
|         </core-block-side-blocks-button> |  | ||||||
|     </core-loading> |     </core-loading> | ||||||
| </core-dynamic-component> | </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. --> | <!-- Template to render a section. --> | ||||||
| <ng-template #sectionTemplate let-section="section"> | <ng-template #sectionTemplate let-section="section"> | ||||||
|     <section *ngIf="!section.hiddenbynumsections && section.id != allSectionsId && section.id != stealthModulesSectionId"> |     <section *ngIf="!section.hiddenbynumsections && section.id != allSectionsId && section.id != stealthModulesSectionId"> | ||||||
|         <!-- Title is only displayed when viewing all sections. --> |         <ion-item-divider class="course-section ion-text-wrap" color="light" | ||||||
|         <ion-item-divider *ngIf="selectedSection?.id == allSectionsId && section.name" class="ion-text-wrap" color="light" |  | ||||||
|             [class.item-dimmed]="section.visible === 0 || section.uservisible === false"> |             [class.item-dimmed]="section.visible === 0 || section.uservisible === false"> | ||||||
|  |             <ion-icon name="fas-folder" aria-label="hidden" slot="start"></ion-icon> | ||||||
|             <ion-label> |             <ion-label> | ||||||
|                 <h2> |                 <h2 *ngIf="section.name"> | ||||||
|                     <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id"> |                     <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course.id"> | ||||||
|                     </core-format-text> |                     </core-format-text> | ||||||
|                 </h2> |                 </h2> | ||||||
|                 <p *ngIf="section.visible === 0 || section.availabilityinfo"> |                 <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"> |                     <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 }} |                         {{ 'core.course.hiddenfromstudents' | translate }} | ||||||
|                     </ion-badge> |                     </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 }} |                         {{ 'core.notavailable' | translate }} | ||||||
|                     </ion-badge> |                     </ion-badge> | ||||||
|                     <ion-badge color="info" *ngIf="section.availabilityinfo" class="ion-text-wrap"> |                     <ion-badge color="info" *ngIf="section.availabilityinfo" class="ion-text-wrap block"> | ||||||
|                         <core-format-text [text]=" section.availabilityinfo" contextLevel="course" [contextInstanceId]="course?.id"> |                         <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> |                         </core-format-text> | ||||||
|                     </ion-badge> |                     </ion-badge> | ||||||
|                 </p> |                 </p> | ||||||
|             </ion-label> |             </ion-label> | ||||||
|  |             <ion-badge class="ion-text-wrap block" *ngIf="section.highlighted && highlighted" slot="end">{{highlighted}}</ion-badge> | ||||||
|         </ion-item-divider> |         </ion-item-divider> | ||||||
| 
 | 
 | ||||||
|         <ion-item class="ion-text-wrap" *ngIf="section.summary"> |         <ion-item class="ion-text-wrap" *ngIf="section.summary"> | ||||||
|             <ion-label> |             <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> |                 </core-format-text> | ||||||
|             </ion-label> |             </ion-label> | ||||||
|         </ion-item> |         </ion-item> | ||||||
| 
 | 
 | ||||||
|         <ng-container *ngFor="let module of section.modules"> |         <ng-container *ngFor="let module of section.modules"> | ||||||
|             <core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section" |             <core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section" | ||||||
|                 (completionChanged)="onCompletionChange($event)" [showActivityDates]="course?.showactivitydates" |                 [showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions"> | ||||||
|                 [showCompletionConditions]="course?.showcompletionconditions"> |  | ||||||
|             </core-course-module> |             </core-course-module> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|     </section> |     </section> | ||||||
|  | |||||||
| @ -1,70 +1,17 @@ | |||||||
| @import '~theme/globals.scss'; | .core-course-section-nav-buttons { | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: flex-end; | ||||||
| 
 | 
 | ||||||
| :host { |     core-format-text { | ||||||
| 
 |         white-space: nowrap; | ||||||
|     .core-format-progress-list { |         text-overflow: ellipsis; | ||||||
|         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; |         overflow: hidden; | ||||||
|         cursor: pointer; |         text-transform: none; | ||||||
|         pointer-events: auto; |     } | ||||||
|         position: relative; | } | ||||||
|         background: var(--ion-item-background); | 
 | ||||||
|         border-bottom: 1px solid var(--stroke); | .course-section { | ||||||
| 
 |     ion-badge { | ||||||
|         img { |         text-align: start; | ||||||
|             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 { |  | ||||||
|         display: flex; |  | ||||||
|         justify-content: flex-end; |  | ||||||
| 
 |  | ||||||
|         core-format-text { |  | ||||||
|             white-space: nowrap; |  | ||||||
|             text-overflow: ellipsis; |  | ||||||
|             overflow: hidden; |  | ||||||
|             text-transform: none; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -26,7 +26,6 @@ import { | |||||||
|     Type, |     Type, | ||||||
|     ElementRef, |     ElementRef, | ||||||
| } from '@angular/core'; | } from '@angular/core'; | ||||||
| import { ModalOptions } from '@ionic/core'; |  | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
| import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; | import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; | ||||||
| import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | 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 { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||||
| import { IonContent, IonRefresher } from '@ionic/angular'; | import { IonContent, IonRefresher } from '@ionic/angular'; | ||||||
| import { CoreUtils } from '@services/utils/utils'; | 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 { 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. |  * 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.
 |     static readonly LOAD_MORE_ACTIVITIES = 20; // How many activities should load each time showMoreActivities is called.
 | ||||||
| 
 | 
 | ||||||
|     @Input() course?: CoreCourseAnyCourseData; // The course to render.
 |     @Input() course!: CoreCourseAnyCourseData; // The course to render.
 | ||||||
|     @Input() sections?: CoreCourseSection[]; // List of course sections.
 |     @Input() sections: CoreCourseSectionToDisplay[] = []; // List of course sections.
 | ||||||
|     @Input() initialSectionId?: number; // The section to load first (by ID).
 |     @Input() initialSectionId?: number; // The section to load first (by ID).
 | ||||||
|     @Input() initialSectionNumber?: number; // The section to load first (by number).
 |     @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.
 |     @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.
 |     // All the possible component classes.
 | ||||||
|     courseFormatComponent?: Type<unknown>; |     courseFormatComponent?: Type<unknown>; | ||||||
|     courseSummaryComponent?: Type<unknown>; |     courseSummaryComponent?: Type<unknown>; | ||||||
|     sectionSelectorComponent?: Type<unknown>; |  | ||||||
|     singleSectionComponent?: Type<unknown>; |     singleSectionComponent?: Type<unknown>; | ||||||
|     allSectionsComponent?: Type<unknown>; |     allSectionsComponent?: Type<unknown>; | ||||||
| 
 | 
 | ||||||
| @ -86,7 +86,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|     showSectionId = 0; |     showSectionId = 0; | ||||||
|     data: Record<string, unknown> = {}; // Data to pass to the components.
 |     data: Record<string, unknown> = {}; // Data to pass to the components.
 | ||||||
| 
 | 
 | ||||||
|     displaySectionSelector = false; |     displayCourseIndex = false; | ||||||
|     displayBlocks = false; |     displayBlocks = false; | ||||||
|     hasBlocks = false; |     hasBlocks = false; | ||||||
|     selectedSection?: CoreCourseSection; |     selectedSection?: CoreCourseSection; | ||||||
| @ -95,17 +95,12 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|     allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID; |     allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID; | ||||||
|     stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID; |     stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID; | ||||||
|     loaded = false; |     loaded = false; | ||||||
|     hasSeveralSections?: boolean; |  | ||||||
|     imageThumb?: string; |  | ||||||
|     progress?: number; |     progress?: number; | ||||||
|     sectionSelectorModalOptions: ModalOptions = { |     highlighted?: string; | ||||||
|         component: CoreCourseSectionSelectorComponent, |  | ||||||
|         componentProps: {}, |  | ||||||
|     }; |  | ||||||
| 
 | 
 | ||||||
|     protected selectTabObserver?: CoreEventObserver; |     protected selectTabObserver?: CoreEventObserver; | ||||||
|  |     protected completionObserver?: CoreEventObserver; | ||||||
|     protected lastCourseFormat?: string; |     protected lastCourseFormat?: string; | ||||||
|     protected sectionSelectorExpanded = false; |  | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         protected content: IonContent, |         protected content: IonContent, | ||||||
| @ -116,9 +111,17 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Component being initialized. |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     ngOnInit(): void { |     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.
 |         // Listen for select course tab events to select the right section if needed.
 | ||||||
|         this.selectTabObserver = CoreEvents.on(CoreEvents.SELECT_COURSE_TAB, (data) => { |         this.selectTabObserver = CoreEvents.on(CoreEvents.SELECT_COURSE_TAB, (data) => { | ||||||
|             if (data.name) { |             if (data.name) { | ||||||
| @ -137,6 +140,37 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|                 this.sectionChanged(section); |                 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> { |     async ngOnChanges(changes: { [name: string]: SimpleChange }): Promise<void> { | ||||||
|         this.setInputData(); |         this.setInputData(); | ||||||
|         this.sectionSelectorModalOptions.componentProps!.course = this.course; |  | ||||||
|         this.sectionSelectorModalOptions.componentProps!.sections = this.sections; |  | ||||||
| 
 | 
 | ||||||
|         if (changes.course && this.course) { |         if (changes.course && this.course) { | ||||||
|             // Course has changed, try to get the components.
 |             // Course has changed, try to get the components.
 | ||||||
|             this.getComponents(); |             this.getComponents(); | ||||||
| 
 | 
 | ||||||
|             this.displaySectionSelector = CoreCourseFormatDelegate.displaySectionSelector(this.course); |             this.displayCourseIndex = CoreCourseFormatDelegate.displayCourseIndex(this.course); | ||||||
|             this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course); |             this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course); | ||||||
| 
 | 
 | ||||||
|             this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id); |             this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id); | ||||||
| 
 | 
 | ||||||
|             this.updateProgress(); |             this.updateProgress(); | ||||||
| 
 |  | ||||||
|             if ('overviewfiles' in this.course) { |  | ||||||
|                 this.imageThumb = this.course.overviewfiles?.[0]?.fileurl; |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (changes.sections && this.sections) { |         if (changes.sections && this.sections) { | ||||||
|             this.sectionSelectorModalOptions.componentProps!.sections = this.sections; |  | ||||||
|             this.treatSections(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.
 |         // Format has changed or it's the first time, load all the components.
 | ||||||
|         this.lastCourseFormat = this.course.format; |         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([ |         await Promise.all([ | ||||||
|             this.loadCourseFormatComponent(), |             this.loadCourseFormatComponent(), | ||||||
|             this.loadCourseSummaryComponent(), |             this.loadCourseSummaryComponent(), | ||||||
|             this.loadSectionSelectorComponent(), |  | ||||||
|             this.loadSingleSectionComponent(), |             this.loadSingleSectionComponent(), | ||||||
|             this.loadAllSectionsComponent(), |             this.loadAllSectionsComponent(), | ||||||
|         ]); |         ]); | ||||||
| @ -207,7 +237,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected async loadCourseFormatComponent(): Promise<void> { |     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. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected async loadCourseSummaryComponent(): Promise<void> { |     protected async loadCourseSummaryComponent(): Promise<void> { | ||||||
|         this.courseSummaryComponent = await CoreCourseFormatDelegate.getCourseSummaryComponent(this.course!); |         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!); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -234,7 +255,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected async loadSingleSectionComponent(): Promise<void> { |     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. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected async loadAllSectionsComponent(): Promise<void> { |     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> { |     protected async treatSections(sections: CoreCourseSection[]): Promise<void> { | ||||||
|         const hasAllSections = sections[0].id == CoreCourseProvider.ALL_SECTIONS_ID; |         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) { |         if (this.selectedSection) { | ||||||
|  |             const selectedSection = this.selectedSection; | ||||||
|             // We have a selected section, but the list has changed. Search the section in the list.
 |             // 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) { |             if (!newSection) { | ||||||
|                 // Section not found, calculate which one to use.
 |                 // 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); |             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.
 |         // 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.
 |             // Always load "All sections" to display the section title. If it isn't there just load the section.
 | ||||||
|             this.loaded = true; |             this.loaded = true; | ||||||
|             this.sectionChanged(sections[0]); |             this.sectionChanged(sections[0]); | ||||||
| @ -289,7 +311,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
| 
 | 
 | ||||||
|         if (!this.loaded) { |         if (!this.loaded) { | ||||||
|             // No section specified, not found or not visible, get current section.
 |             // 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.loaded = true; | ||||||
|             this.sectionChanged(section); |             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> { |     async openCourseIndex(): Promise<void> { | ||||||
|         if (this.sectionSelectorExpanded) { |         const data = await CoreDomUtils.openModal<CoreCourseIndexSectionWithModule>({ | ||||||
|             return; |             component: CoreCourseCourseIndexComponent, | ||||||
|         } |             componentProps: { | ||||||
|  |                 course: this.course, | ||||||
|  |                 sections: this.sections, | ||||||
|  |                 selectedId: this.selectedSection?.id, | ||||||
|  |             }, | ||||||
|  |         }); | ||||||
| 
 | 
 | ||||||
|         this.sectionSelectorExpanded = true; |  | ||||||
| 
 |  | ||||||
|         const data = await CoreDomUtils.openModal<CoreCourseSection>(this.sectionSelectorModalOptions); |  | ||||||
| 
 |  | ||||||
|         this.sectionSelectorExpanded = false; |  | ||||||
|         if (data) { |         if (data) { | ||||||
|             this.sectionChanged(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); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 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 { |     sectionChanged(newSection: CoreCourseSection): void { | ||||||
|         const previousValue = this.selectedSection; |         const previousValue = this.selectedSection; | ||||||
|         this.selectedSection = newSection; |         this.selectedSection = newSection; | ||||||
|         this.sectionSelectorModalOptions.componentProps!.selected = this.selectedSection; |  | ||||||
|         this.data.section = this.selectedSection; |         this.data.section = this.selectedSection; | ||||||
| 
 | 
 | ||||||
|         if (newSection.id != this.allSectionsId) { |         if (newSection.id != this.allSectionsId) { | ||||||
|             // Select next and previous sections to show the arrows.
 |             // 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; |             let j: number; | ||||||
|             for (j = i - 1; j >= 1; j--) { |             for (j = i - 1; j >= 1; j--) { | ||||||
|                 if (this.canViewSection(this.sections![j])) { |                 if (this.canViewSection(this.sections[j])) { | ||||||
|                     break; |                     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++) { |             for (j = i + 1; j < this.sections.length; j++) { | ||||||
|                 if (this.canViewSection(this.sections![j])) { |                 if (this.canViewSection(this.sections[j])) { | ||||||
|                     break; |                     break; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             this.nextSection = j < this.sections!.length ? this.sections![j] : undefined; |             this.nextSection = j < this.sections.length ? this.sections[j] : undefined; | ||||||
|         } else { |         } else { | ||||||
|             this.previousSection = undefined; |             this.previousSection = undefined; | ||||||
|             this.nextSection = undefined; |             this.nextSection = undefined; | ||||||
| @ -368,7 +400,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|         if (!previousValue || previousValue.id != newSection.id) { |         if (!previousValue || previousValue.id != newSection.id) { | ||||||
|             // First load or section changed, add log in Moodle.
 |             // First load or section changed, add log in Moodle.
 | ||||||
|             CoreUtils.ignoreErrors( |             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 { |     ngOnDestroy(): void { | ||||||
|         this.selectTabObserver && this.selectTabObserver.off(); |         this.selectTabObserver && this.selectTabObserver.off(); | ||||||
| @ -510,35 +542,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|                 section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID; |                 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. |      * 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
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Component } from '@angular/core'; | import { Component, OnDestroy, OnInit } from '@angular/core'; | ||||||
| 
 | 
 | ||||||
| import { CoreUser } from '@features/user/services/user'; | 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 { CoreFilterHelper } from '@features/filter/services/filter-helper'; | ||||||
| import { Translate } from '@singletons'; | import { Translate } from '@singletons'; | ||||||
| import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion'; | import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion'; | ||||||
| import { CoreCourseHelper } from '@features/course/services/course-helper'; | 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. |  * 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', |     templateUrl: 'core-course-module-completion-legacy.html', | ||||||
|     styleUrls: ['module-completion-legacy.scss'], |     styleUrls: ['module-completion-legacy.scss'], | ||||||
| }) | }) | ||||||
| export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleCompletionBaseComponent { | export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleCompletionBaseComponent | ||||||
|  |     implements OnInit, OnDestroy { | ||||||
| 
 | 
 | ||||||
|     completionImage?: string; |     completionImage?: string; | ||||||
|     completionDescription?: 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 |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
| @ -52,30 +75,28 @@ export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleC | |||||||
|         let langKey: string | undefined; |         let langKey: string | undefined; | ||||||
|         let image: string | undefined; |         let image: string | undefined; | ||||||
| 
 | 
 | ||||||
|         if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL && |         if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL) { | ||||||
|                 this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) { |             if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) { | ||||||
|             image = 'completion-manual-n'; |                 image = 'completion-manual-n'; | ||||||
|             langKey = 'core.completion-alt-manual-n'; |                 langKey = 'core.completion-alt-manual-n'; | ||||||
|         } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL && |             } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { | ||||||
|                 this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { |                 image = 'completion-manual-y'; | ||||||
|             image = 'completion-manual-y'; |                 langKey = 'core.completion-alt-manual-y'; | ||||||
|             langKey = 'core.completion-alt-manual-y'; |             } | ||||||
|         } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC && |         } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC) { | ||||||
|                 this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) { |             if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) { | ||||||
|             image = 'completion-auto-n'; |                 image = 'completion-auto-n'; | ||||||
|             langKey = 'core.completion-alt-auto-n'; |                 langKey = 'core.completion-alt-auto-n'; | ||||||
|         } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC && |             } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { | ||||||
|                 this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { |                 image = 'completion-auto-y'; | ||||||
|             image = 'completion-auto-y'; |                 langKey = 'core.completion-alt-auto-y'; | ||||||
|             langKey = 'core.completion-alt-auto-y'; |             } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) { | ||||||
|         } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC && |                 image = 'completion-auto-pass'; | ||||||
|                 this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) { |                 langKey = 'core.completion-alt-auto-pass'; | ||||||
|             image = 'completion-auto-pass'; |             } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL) { | ||||||
|             langKey = 'core.completion-alt-auto-pass'; |                 image = 'completion-auto-fail'; | ||||||
|         } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC && |                 langKey = 'core.completion-alt-auto-fail'; | ||||||
|                 this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL) { |             } | ||||||
|             image = 'completion-auto-fail'; |  | ||||||
|             langKey = 'core.completion-alt-auto-fail'; |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (image) { |         if (image) { | ||||||
| @ -128,9 +149,16 @@ export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleC | |||||||
| 
 | 
 | ||||||
|         await CoreCourseHelper.changeManualCompletion(this.completion, event); |         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 *ngIf="completion.istrackeduser"> | ||||||
|         <ng-container *ngFor="let rule of details"> |         <ng-container *ngFor="let rule of details"> | ||||||
|             <ion-chip *ngIf="rule.statuscomplete" color="success" role="listitem" [attr.aria-label]="rule.accessibleDescription"> |             <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><strong>{{ 'core.course.completion_automatic:done' | translate }}</strong> {{ rule.rulevalue.description }} | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-chip> |             </ion-chip> | ||||||
| 
 | 
 | ||||||
|             <ion-chip *ngIf="rule.statuscompletefail" color="danger" role="listitem" [attr.aria-label]="rule.accessibleDescription"> |             <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><strong>{{ 'core.course.completion_automatic:failed' | translate }}</strong> {{ rule.rulevalue.description }} | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-chip> |             </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><strong>{{ 'core.course.completion_automatic:todo' | translate }}</strong> {{ rule.rulevalue.description }} | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-chip> |             </ion-chip> | ||||||
|  | |||||||
| @ -19,12 +19,21 @@ | |||||||
|     </ion-label> |     </ion-label> | ||||||
| </ion-item> | </ion-item> | ||||||
| <ng-content select="[description]"></ng-content> | <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> |     <ion-label> | ||||||
|         <p *ngFor="let date of module.dates"> |         <!-- Activity dates. --> | ||||||
|             <strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }} |         <div *ngIf="module.dates?.length" class="core-module-dates"> | ||||||
|         </p> |             <p *ngFor="let date of module.dates"> | ||||||
|  |                 <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-label> | ||||||
| </ion-item> | </ion-item> | ||||||
| <ng-content></ng-content> | <ng-content></ng-content> | ||||||
|  | |||||||
| @ -16,4 +16,9 @@ | |||||||
|     core-mod-icon { |     core-mod-icon { | ||||||
|         align-self: flex-start; |         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.istrackeduser"> | ||||||
|         <ng-container *ngIf="completion.state"> |         <ng-container *ngIf="completion.state"> | ||||||
|             <ion-button color="success" expand="block" fill="outline" [attr.aria-label]="accessibleDescription" |             <ion-button color="success" fill="outline" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)"> | ||||||
|                 (click)="completionClicked($event)"> |  | ||||||
|                 <ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon> |                 <ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon> | ||||||
|                 {{ 'core.course.completion_manual:done' | translate }} |                 {{ 'core.course.completion_manual:done' | translate }} | ||||||
|             </ion-button> |             </ion-button> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|         <ng-container *ngIf="!completion.state"> |         <ng-container *ngIf="!completion.state"> | ||||||
|             <ion-button color="dark" expand="block" fill="outline" [attr.aria-label]="accessibleDescription" |             <ion-button color="dark" fill="outline" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)"> | ||||||
|                 (click)="completionClicked($event)"> |  | ||||||
|                 {{ 'core.course.completion_manual:markdone' | translate }} |                 {{ 'core.course.completion_manual:markdone' | translate }} | ||||||
|             </ion-button> |             </ion-button> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|     </ng-container> |     </ng-container> | ||||||
| 
 | 
 | ||||||
|     <ng-container *ngIf="!completion.istrackeduser"> |     <ng-container *ngIf="!completion.istrackeduser"> | ||||||
|         <ion-button disabled="true" color="dark" expand="block" fill="outline"> |         <ion-button disabled="true" color="dark" fill="outline"> | ||||||
|             {{ 'core.course.completion_manual:markdone' | translate }} |             {{ 'core.course.completion_manual:markdone' | translate }} | ||||||
|         </ion-button> |         </ion-button> | ||||||
|     </ng-container> |     </ng-container> | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange } from '@angular/core'; | 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 { CoreCourseHelper, CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; | ||||||
| import { CoreUser } from '@features/user/services/user'; | import { CoreUser } from '@features/user/services/user'; | ||||||
| @ -34,14 +35,14 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan | |||||||
| 
 | 
 | ||||||
|     accessibleDescription: string | null = null; |     accessibleDescription: string | null = null; | ||||||
| 
 | 
 | ||||||
|     protected manualChangedObserver?: CoreEventObserver; |     protected completionObserver?: CoreEventObserver; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @inheritdoc |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     ngOnInit(): void { |     ngOnInit(): void { | ||||||
|         this.manualChangedObserver = CoreEvents.on(CoreEvents.MANUAL_COMPLETION_CHANGED, (data) => { |         this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => { | ||||||
|             if (!this.completion || this.completion.cmid != data.completion.cmid) { |             if (!this.completion || this.completion.cmid != data.completion.cmid && data.type != CoreCourseCompletionType.MANUAL) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @ -98,14 +99,16 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan | |||||||
| 
 | 
 | ||||||
|         await CoreCourseHelper.changeManualCompletion(this.completion, event); |         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.MANUAL_COMPLETION_CHANGED, { completion: this.completion }); | ||||||
|  |         CoreEvents.trigger(CoreEvents.COMPLETION_CHANGED, { completion: this.completion, type: CoreCourseCompletionType.MANUAL }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @inheritdoc |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     ngOnDestroy(): void { |     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-icon name="fas-arrow-left" slot="icon-only" aria-hidden="true"></ion-icon> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|         </ion-col> |         </ion-col> | ||||||
|         <ion-col *ngIf="showCompletion && (currentModule.completiondata && |         <ion-col *ngIf="showCompletion && (currentModule.completiondata && showManualCompletion && currentModule.uservisible)"> | ||||||
|             (currentModule.completiondata.isautomatic || showManualCompletion) && currentModule.uservisible)"> |  | ||||||
|             <!-- Module completion. --> |             <!-- Module completion. --> | ||||||
|             <core-course-module-completion [completion]="currentModule.completiondata" [moduleName]="currentModule.name" |             <core-course-module-completion [completion]="currentModule.completiondata" [moduleName]="currentModule.name" | ||||||
|                 [moduleId]="currentModule.id" [showCompletionConditions]="true" [showManualCompletion]="showManualCompletion" |                 [moduleId]="currentModule.id" [showManualCompletion]="true" (completionChanged)="completionChanged.emit($event)"> | ||||||
|                 (completionChanged)="completionChanged.emit($event)"> |  | ||||||
|             </core-course-module-completion> |             </core-course-module-completion> | ||||||
|         </ion-col> |         </ion-col> | ||||||
|         <ion-col size="auto"> |         <ion-col size="auto"> | ||||||
|  | |||||||
| @ -18,25 +18,26 @@ | |||||||
|                         [courseId]="module.course" [attr.aria-label]="module.handlerData.a11yTitle + ', ' + modNameTranslated"> |                         [courseId]="module.course" [attr.aria-label]="module.handlerData.a11yTitle + ', ' + modNameTranslated"> | ||||||
|                     </core-format-text> |                     </core-format-text> | ||||||
|                 </p> |                 </p> | ||||||
|                 <ion-badge *ngIf="module.handlerData.extraBadge" [color]="module.handlerData.extraBadgeColor" |                 <ion-chip class="completioninfo completion_incomplete" *ngIf="completionStatus === 0"> | ||||||
|                     class="ion-text-wrap ion-text-start"> |                     <ion-label> | ||||||
|                     <span [innerHTML]="module.handlerData.extraBadge"></span> |                         <ion-icon name="fas-edit" aria-hidden="true"></ion-icon> | ||||||
|                 </ion-badge> |                         {{ 'core.course.todo' | translate }} | ||||||
|                 <ion-badge *ngIf="module.visible === 0 && (!section || section.visible)" class="ion-text-wrap"> |                     </ion-label> | ||||||
|                     {{ 'core.course.hiddenfromstudents' | translate }} |                 </ion-chip> | ||||||
|                 </ion-badge> |                 <ion-chip class="completioninfo completion_complete" *ngIf="completionStatus === 1 || completionStatus === 2" | ||||||
|                 <ion-badge *ngIf="module.visible !== 0 && module.isStealth" class="ion-text-wrap"> |                     color="success"> | ||||||
|                     {{ 'core.course.hiddenoncoursepage' | translate }} |                     <ion-icon name="fas-check" aria-hidden="true"></ion-icon> | ||||||
|                 </ion-badge> |                     <ion-label>{{'core.course.done' | translate }}</ion-label> | ||||||
|                 <div class="core-module-availabilityinfo" *ngIf="module.availabilityinfo"> |                 </ion-chip> | ||||||
|                     <ion-badge class="ion-text-wrap">{{ 'core.restricted' | translate }}</ion-badge> |                 <ion-chip class="completioninfo completion_fail" *ngIf="completionStatus === 3" color="danger"> | ||||||
|                     <core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id" |                     <ion-icon name="fas-times" aria-hidden="true"></ion-icon> | ||||||
|                         [courseId]="module.course" class="ion-text-wrap"> |                     <ion-label>{{'core.course.failed' | translate }}</ion-label> | ||||||
|                     </core-format-text> |                 </ion-chip> | ||||||
|                 </div> | 
 | ||||||
|                 <ion-badge *ngIf="module.completiondata?.offline" color="warning" class="ion-text-wrap"> |                 <ion-chip *ngIf="module.handlerData.extraBadge" [color]="module.handlerData.extraBadgeColor" | ||||||
|                     {{ 'core.course.manualcompletionnotsynced' | translate }} |                     class="ion-text-wrap ion-text-start" [outline]="true"> | ||||||
|                 </ion-badge> |                     <ion-label><span [innerHTML]="module.handlerData.extraBadge"></span></ion-label> | ||||||
|  |                 </ion-chip> | ||||||
|             </ion-label> |             </ion-label> | ||||||
|             <!-- Buttons. --> |             <!-- Buttons. --> | ||||||
|             <div slot="end" *ngIf="module.uservisible !== false" class="buttons core-module-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 |                 'item-dimmed': module.visible === 0 || module.uservisible === false | ||||||
|             }" detail="false"> |             }" detail="false"> | ||||||
|             <ion-label> |             <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. --> |                 <!-- Activity dates. --> | ||||||
|                 <div *ngIf="showActivityDates && module.dates && module.dates.length" class="core-module-dates"> |                 <div *ngIf="showActivityDates && module.dates && module.dates.length" class="core-module-dates"> | ||||||
|                     <p *ngFor="let date of module.dates"> |                     <p *ngFor="let date of module.dates"> | ||||||
|                         <strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }} |                         <ion-icon name="fas-calendar" aria-hidden="true"></ion-icon><strong>{{ date.label }}</strong> {{ date.timestamp * | ||||||
|  |                         1000 | coreFormatDate:'strftimedatetime' }} | ||||||
|                     </p> |                     </p> | ||||||
|                 </div> |                 </div> | ||||||
| 
 | 
 | ||||||
| @ -76,9 +82,31 @@ | |||||||
|                     [showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)"> |                     [showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)"> | ||||||
|                 </core-course-module-completion> |                 </core-course-module-completion> | ||||||
| 
 | 
 | ||||||
|                 <core-format-text class="core-module-description" *ngIf="module.description" [maxHeight]="80" [text]="module.description" |                 <ion-chip *ngIf="module.completiondata?.offline" color="warning" class="ion-text-wrap block"> | ||||||
|                     contextLevel="module" [contextInstanceId]="module.id" [courseId]="module.course"> |                     <ion-icon name="fas-sync" aria-hidden="true"></ion-icon> | ||||||
|                 </core-format-text> |                     <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-label> | ||||||
|         </ion-item> |         </ion-item> | ||||||
|     </ng-container> |     </ng-container> | ||||||
|  | |||||||
| @ -1,3 +1,5 @@ | |||||||
|  | @import "~theme/globals"; | ||||||
|  | 
 | ||||||
| :host { | :host { | ||||||
| 
 | 
 | ||||||
|     .item.core-module-main-item { |     .item.core-module-main-item { | ||||||
| @ -59,4 +61,9 @@ | |||||||
|         --inner-border-width: 0px; |         --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, |     CoreCourseModuleCompletionData, | ||||||
|     CoreCourseSection, |     CoreCourseSection, | ||||||
| } from '@features/course/services/course-helper'; | } 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'; | import { CoreCourseModuleDelegate, CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -47,6 +47,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { | |||||||
|     hasInfo = false; |     hasInfo = false; | ||||||
|     showLegacyCompletion = false; // Whether to show module completion in the old format.
 |     showLegacyCompletion = false; // Whether to show module completion in the old format.
 | ||||||
|     showManualCompletion = false; // Whether to show manual completion when completion conditions are disabled.
 |     showManualCompletion = false; // Whether to show manual completion when completion conditions are disabled.
 | ||||||
|  |     completionStatus?: CoreCourseModuleCompletionStatus; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Component being initialized. |      * 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.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.hasInfo = !!( | ||||||
|             this.module.description || |             this.module.description || | ||||||
|             (this.showActivityDates && this.module.dates && this.module.dates.length) || |             (this.showActivityDates && this.module.dates && this.module.dates.length) || | ||||||
|             (this.module.completiondata && |             (this.module.completiondata && | ||||||
|                 ((this.showManualCompletion && !this.module.completiondata.isautomatic) || |                 ((this.showManualCompletion && !this.module.completiondata.isautomatic) || | ||||||
|                     (this.showCompletionConditions && 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 { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; | ||||||
| import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | ||||||
| import { IonRefresher } from '@ionic/angular'; | 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 { CoreBlockHelper } from '@features/block/services/block-helper'; | ||||||
| import { CoreCourse } from '@features/course/services/course'; | import { CoreCourse } from '@features/course/services/course'; | ||||||
| 
 | 
 | ||||||
| @ -31,11 +31,12 @@ import { CoreCourse } from '@features/course/services/course'; | |||||||
| @Component({ | @Component({ | ||||||
|     selector: 'core-course-format-single-activity', |     selector: 'core-course-format-single-activity', | ||||||
|     templateUrl: 'core-course-format-single-activity.html', |     templateUrl: 'core-course-format-single-activity.html', | ||||||
|  |     styleUrls: ['single-activity.scss'], | ||||||
| }) | }) | ||||||
| export class CoreCourseFormatSingleActivityComponent implements OnChanges { | export class CoreCourseFormatSingleActivityComponent implements OnChanges { | ||||||
| 
 | 
 | ||||||
|     @Input() course?: CoreCourseAnyCourseData; // The course to render.
 |     @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() initialSectionId?: number; // The section to load first (by ID).
 | ||||||
|     @Input() initialSectionNumber?: number; // The section to load first (by number).
 |     @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.
 |     @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 |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     displaySectionSelector(): boolean { |     displayCourseIndex(): boolean { | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; | |||||||
| 
 | 
 | ||||||
| import { CoreTimeUtils } from '@services/utils/time'; | import { CoreTimeUtils } from '@services/utils/time'; | ||||||
| import { CoreCourseFormatHandler } from '@features/course/services/format-delegate'; | 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 { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | ||||||
| import { CoreCourseWSSection } from '@features/course/services/course'; | import { CoreCourseWSSection } from '@features/course/services/course'; | ||||||
| import { CoreConstants } from '@/core/constants'; | import { CoreConstants } from '@/core/constants'; | ||||||
| @ -32,20 +32,14 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand | |||||||
|     format = 'weeks'; |     format = 'weeks'; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Whether or not the handler is enabled on a site level. |      * @inheritdoc | ||||||
|      * |  | ||||||
|      * @return True or promise resolved with true if enabled. |  | ||||||
|      */ |      */ | ||||||
|     async isEnabled(): Promise<boolean> { |     async isEnabled(): Promise<boolean> { | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Given a list of sections, get the "current" section that should be displayed first. |      * @inheritdoc | ||||||
|      * |  | ||||||
|      * @param course The course to get the title. |  | ||||||
|      * @param sections List of sections. |  | ||||||
|      * @return Current section (or promise resolved with current section). |  | ||||||
|      */ |      */ | ||||||
|     async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> { |     async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> { | ||||||
|         const now = CoreTimeUtils.timestamp(); |         const now = CoreTimeUtils.timestamp(); | ||||||
| @ -71,6 +65,13 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand | |||||||
|         return sections[0]; |         return sections[0]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     getSectionHightlightedName(): string { | ||||||
|  |         return Translate.instant('core.course.thisweek'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Return the start and end date of a section. |      * Return the start and end date of a section. | ||||||
|      * |      * | ||||||
| @ -83,7 +84,7 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand | |||||||
|         startDate = startDate + 7200; |         startDate = startDate + 7200; | ||||||
| 
 | 
 | ||||||
|         const dates = { |         const dates = { | ||||||
|             start: startDate + (CoreConstants.SECONDS_WEEK * (section.section! - 1)), |             start: startDate + (CoreConstants.SECONDS_WEEK * ((section.section || 0) - 1)), | ||||||
|             end: 0, |             end: 0, | ||||||
|         }; |         }; | ||||||
|         dates.end = dates.start + CoreConstants.SECONDS_WEEK; |         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?", |     "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?", |     "confirmpartialdownloadsize": "You are about to download <strong>at least</strong> {{size}}.{{availableSpace}} Are you sure you want to continue?", | ||||||
|     "confirmlimiteddownload": "You are not currently connected to Wi-Fi. ", |     "confirmlimiteddownload": "You are not currently connected to Wi-Fi. ", | ||||||
|     "gotonextactivity": "Continue to next activity", |     "courseindex": "Course index", | ||||||
|     "gotonextactivitynotfound": "Next activity not found. It's possible that it has been hidden or deleted.", |  | ||||||
|     "gotopreviousactivity": "Continue to previous activity", |  | ||||||
|     "gotopreviousactivitynotfound": "Previous activity not found. It's possible that it has been hidden or deleted.", |  | ||||||
|     "couldnotloadsectioncontent": "Could not load the section content. Please try again later.", |     "couldnotloadsectioncontent": "Could not load the section content. Please try again later.", | ||||||
|     "couldnotloadsections": "Could not load the sections. Please try again later.", |     "couldnotloadsections": "Could not load the sections. Please try again later.", | ||||||
|     "coursesummary": "Course summary", |     "coursesummary": "Course summary", | ||||||
|  |     "done": "Done", | ||||||
|     "downloadcourse": "Download course", |     "downloadcourse": "Download course", | ||||||
|     "downloadcoursesprogressdescription": "Downloading courses: downloaded {{count}} out of {{total}}.", |     "downloadcoursesprogressdescription": "Downloading courses: downloaded {{count}} out of {{total}}.", | ||||||
|     "downloadsectionprogressdescription": "Downloading section: downloaded {{count}} out of {{total}}.", |     "downloadsectionprogressdescription": "Downloading section: downloaded {{count}} out of {{total}}.", | ||||||
|     "errordownloadingcourse": "Error downloading course.", |     "errordownloadingcourse": "Error downloading course.", | ||||||
|     "errordownloadingsection": "Error downloading section.", |     "errordownloadingsection": "Error downloading section.", | ||||||
|     "errorgetmodule": "Error getting activity data.", |     "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", |     "hiddenfromstudents": "Hidden from students", | ||||||
|     "hiddenoncoursepage": "Available but not shown on course page", |     "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.", |     "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.", |     "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.", |     "manualcompletionnotsynced": "Manual completion not synchronised.", | ||||||
| @ -49,7 +53,8 @@ | |||||||
|     "overriddennotice": "Your final grade from this activity was manually adjusted.", |     "overriddennotice": "Your final grade from this activity was manually adjusted.", | ||||||
|     "refreshcourse": "Refresh course", |     "refreshcourse": "Refresh course", | ||||||
|     "section": "Section", |     "section": "Section", | ||||||
|     "sections": "Sections", |     "thisweek": "This week", | ||||||
|  |     "todo": "To do", | ||||||
|     "useactivityonbrowser": "You can still use it using your device's web browser.", |     "useactivityonbrowser": "You can still use it using your device's web browser.", | ||||||
|     "warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.", |     "warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.", | ||||||
|     "warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}" |     "warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}" | ||||||
|  | |||||||
| @ -15,7 +15,8 @@ | |||||||
| 
 | 
 | ||||||
|     <core-loading [hideUntil]="dataLoaded"> |     <core-loading [hideUntil]="dataLoaded"> | ||||||
|         <core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId" [initialSectionNumber]="sectionNumber" |         <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-course-format> | ||||||
|     </core-loading> |     </core-loading> | ||||||
| </ion-content> | </ion-content> | ||||||
|  | |||||||
| @ -384,8 +384,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { | |||||||
|      */ |      */ | ||||||
|     openCourseSummary(): void { |     openCourseSummary(): void { | ||||||
|         CoreNavigator.navigateToSitePath( |         CoreNavigator.navigateToSitePath( | ||||||
|             '/course/' + this.course.id + '/preview', |             `/course/${this.course.id}/preview`, | ||||||
|             { params: { course: this.course } }, |             { params: { course: this.course, avoidOpenCourse: true } }, | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| <ion-header> | <ion-header collapsible> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||||
| @ -12,4 +12,31 @@ | |||||||
|         <ion-buttons slot="end"></ion-buttons> |         <ion-buttons slot="end"></ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </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 { resolveModuleRoutes } from '@/app/app-routing.module'; | ||||||
| import { CoreSharedModule } from '@/core/shared.module'; | import { CoreSharedModule } from '@/core/shared.module'; | ||||||
| import { CoreCourseIndexPage } from './index.page'; | import { CoreCourseIndexPage } from '.'; | ||||||
| import { COURSE_INDEX_ROUTES } from './index-routing.module'; | import { COURSE_INDEX_ROUTES } from './index-routing.module'; | ||||||
| 
 | 
 | ||||||
| function buildRoutes(injector: Injector): Routes { | 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 { CoreTextUtils } from '@services/utils/text'; | ||||||
| import { CoreNavigator } from '@services/navigator'; | import { CoreNavigator } from '@services/navigator'; | ||||||
| import { CONTENTS_PAGE_NAME } from '@features/course/course.module'; | 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. |  * 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({ | @Component({ | ||||||
|     selector: 'page-core-course-index', |     selector: 'page-core-course-index', | ||||||
|     templateUrl: 'index.html', |     templateUrl: 'index.html', | ||||||
|  |     styleUrls: ['index.scss'], | ||||||
| }) | }) | ||||||
| export class CoreCourseIndexPage implements OnInit, OnDestroy { | export class CoreCourseIndexPage implements OnInit, OnDestroy { | ||||||
| 
 | 
 | ||||||
|     @ViewChild(CoreTabsOutletComponent) tabsComponent?: CoreTabsOutletComponent; |     @ViewChild(CoreTabsOutletComponent) tabsComponent?: CoreTabsOutletComponent; | ||||||
|  |     @ViewChild(CoreCollapsibleHeaderDirective) ionCollapsibleHeader?: CoreCollapsibleHeaderDirective; | ||||||
| 
 | 
 | ||||||
|     title?: string; |     title = ''; | ||||||
|  |     category = ''; | ||||||
|     course?: CoreCourseAnyCourseData; |     course?: CoreCourseAnyCourseData; | ||||||
|     tabs: CourseTab[] = []; |     tabs: CourseTab[] = []; | ||||||
|     loaded = false; |     loaded = false; | ||||||
|  |     imageThumb?: string; | ||||||
|  |     progress?: number; | ||||||
| 
 | 
 | ||||||
|     protected currentPagePath = ''; |     protected currentPagePath = ''; | ||||||
|     protected selectTabObserver: CoreEventObserver; |     protected selectTabObserver: CoreEventObserver; | ||||||
|     protected firstTabName?: string; |     protected firstTabName?: string; | ||||||
|     protected module?: CoreCourseModuleData; |     protected module?: CoreCourseModuleData; | ||||||
|     protected modParams?: Params; |     protected modParams?: Params; | ||||||
|     protected isGuest?: boolean; |     protected isGuest = false; | ||||||
|     protected contentsTab: CoreTabsOutletTab = { |     protected contentsTab: CoreTabsOutletTab & { pageParams: Params } = { | ||||||
|         page: CONTENTS_PAGE_NAME, |         page: CONTENTS_PAGE_NAME, | ||||||
|         title: 'core.course', |         title: 'core.course', | ||||||
|         pageParams: {}, |         pageParams: {}, | ||||||
| @ -60,10 +67,10 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | |||||||
|             if (!data.name) { |             if (!data.name) { | ||||||
|                 // If needed, set sectionId and sectionNumber. They'll only be used if the content tabs hasn't been loaded yet.
 |                 // If needed, set sectionId and sectionNumber. They'll only be used if the content tabs hasn't been loaded yet.
 | ||||||
|                 if (data.sectionId) { |                 if (data.sectionId) { | ||||||
|                     this.contentsTab.pageParams!.sectionId = data.sectionId; |                     this.contentsTab.pageParams.sectionId = data.sectionId; | ||||||
|                 } |                 } | ||||||
|                 if (data.sectionNumber) { |                 if (data.sectionNumber) { | ||||||
|                     this.contentsTab.pageParams!.sectionNumber = data.sectionNumber; |                     this.contentsTab.pageParams.sectionNumber = data.sectionNumber; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // Select course contents.
 |                 // Select course contents.
 | ||||||
| @ -79,7 +86,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Component being initialized. |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     async ngOnInit(): Promise<void> { |     async ngOnInit(): Promise<void> { | ||||||
|         // Increase route depth.
 |         // Increase route depth.
 | ||||||
| @ -87,12 +94,19 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | |||||||
| 
 | 
 | ||||||
|         CoreNavigator.increaseRouteDepth(path.replace(/(\/deep)+/, '')); |         CoreNavigator.increaseRouteDepth(path.replace(/(\/deep)+/, '')); | ||||||
| 
 | 
 | ||||||
|         // Get params.
 |         try { | ||||||
|         this.course = CoreNavigator.getRouteParam('course'); |             this.course = CoreNavigator.getRequiredRouteParam('course'); | ||||||
|  |         } catch (error) { | ||||||
|  |             CoreDomUtils.showErrorModal(error); | ||||||
|  |             CoreNavigator.back(); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         this.firstTabName = CoreNavigator.getRouteParam('selectedTab'); |         this.firstTabName = CoreNavigator.getRouteParam('selectedTab'); | ||||||
|         this.module = CoreNavigator.getRouteParam<CoreCourseModuleData>('module'); |         this.module = CoreNavigator.getRouteParam<CoreCourseModuleData>('module'); | ||||||
|         this.modParams = CoreNavigator.getRouteParam<Params>('modParams'); |         this.modParams = CoreNavigator.getRouteParam<Params>('modParams'); | ||||||
|         this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest'); |         this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest'); | ||||||
| 
 | 
 | ||||||
|         this.currentPagePath = CoreNavigator.getCurrentPath(); |         this.currentPagePath = CoreNavigator.getCurrentPath(); | ||||||
|         this.contentsTab.page = CoreTextUtils.concatenatePaths(this.currentPagePath, this.contentsTab.page); |         this.contentsTab.page = CoreTextUtils.concatenatePaths(this.currentPagePath, this.contentsTab.page); | ||||||
| @ -104,7 +118,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | |||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         if (this.module) { |         if (this.module) { | ||||||
|             this.contentsTab.pageParams!.moduleId = this.module.id; |             this.contentsTab.pageParams.moduleId = this.module.id; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.tabs.push(this.contentsTab); |         this.tabs.push(this.contentsTab); | ||||||
| @ -112,20 +126,23 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | |||||||
| 
 | 
 | ||||||
|         await Promise.all([ |         await Promise.all([ | ||||||
|             this.loadCourseHandlers(), |             this.loadCourseHandlers(), | ||||||
|             this.loadTitle(), |             this.loadBasinInfo(), | ||||||
|         ]); |         ]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * A tab was selected. |      * A tab was selected. | ||||||
|      */ |      */ | ||||||
|     tabSelected(): void { |     tabSelected(tabToSelect: CoreTabsOutletTab): void { | ||||||
|         if (this.module) { |         this.ionCollapsibleHeader?.setupContent(tabToSelect.id); | ||||||
|             // Now that the first tab has been selected we can load the module.
 |  | ||||||
|             CoreCourseHelper.openModule(this.module, this.course!.id, this.contentsTab.pageParams!.sectionId, this.modParams); |  | ||||||
| 
 | 
 | ||||||
|             delete this.module; |         if (!this.module || !this.course) { | ||||||
|  |             return; | ||||||
|         } |         } | ||||||
|  |         // Now that the first tab has been selected we can load the module.
 | ||||||
|  |         CoreCourseHelper.openModule(this.module, this.course.id, this.contentsTab.pageParams.sectionId, this.modParams); | ||||||
|  | 
 | ||||||
|  |         delete this.module; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -134,8 +151,12 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | |||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected async loadCourseHandlers(): Promise<void> { |     protected async loadCourseHandlers(): Promise<void> { | ||||||
|  |         if (!this.course) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         // Load the course handlers.
 |         // 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; |         let tabToLoad: number | undefined; | ||||||
| 
 | 
 | ||||||
| @ -169,23 +190,34 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | |||||||
|      * |      * | ||||||
|      * @return Promise resolved when done. |      * @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.
 |         // 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.
 |         // 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) { |         if (!sections) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Get the title again now that we have sections.
 |         // 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 { |     ngOnDestroy(): void { | ||||||
|         const path = CoreNavigator.getRouteFullPath(this.route.snapshot); |         const path = CoreNavigator.getRouteFullPath(this.route.snapshot); | ||||||
| @ -208,6 +240,39 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { | |||||||
|         this.tabsComponent?.ionViewDidLeave(); |         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 & { | type CourseTab = CoreTabsOutletTab & { | ||||||
| @ -27,33 +27,35 @@ | |||||||
|         <core-course-module-info [module]="module" [courseId]="courseId" [description]="module.description" [component]="module.modname" |         <core-course-module-info [module]="module" [courseId]="courseId" [description]="module.description" [component]="module.modname" | ||||||
|             [componentId]="module.id" [expandDescription]="true"> |             [componentId]="module.id" [expandDescription]="true"> | ||||||
| 
 | 
 | ||||||
|             <div class="safe-area-padding-horizontal ion-padding" *ngIf="module.handlerData?.extraBadge"> |             <div class="ion-padding" *ngIf="module.handlerData?.extraBadge"> | ||||||
|                 <ion-badge class="ion-text-wrap ion-text-start" [color]="module.handlerData?.extraBadgeColor"> |                 <ion-chip class="ion-text-wrap ion-text-start" [color]="module.handlerData?.extraBadgeColor"> | ||||||
|                     <span [innerHTML]="module.handlerData?.extraBadge"></span> |                     <ion-label><span [innerHTML]="module.handlerData?.extraBadge"></span></ion-label> | ||||||
|                 </ion-badge> |                 </ion-chip> | ||||||
|             </div> |             </div> | ||||||
|             <div class="safe-area-padding-horizontal ion-padding" *ngIf="module.visible === 0 && (!section || section.visible)"> |             <div class="ion-padding" *ngIf="module.visible === 0 && (!section || section.visible)"> | ||||||
|                 <ion-badge class="ion-text-wrap"> |                 <ion-chip class="ion-text-wrap"> | ||||||
|                     {{ 'core.course.hiddenfromstudents' | translate }} |                     <ion-icon name="fas-eye-slash" aria-hidden="true"></ion-icon> | ||||||
|                 </ion-badge> |                     <ion-label>{{ 'core.course.hiddenfromstudents' | translate }}</ion-label> | ||||||
|  |                 </ion-chip> | ||||||
|             </div> |             </div> | ||||||
|             <div class="safe-area-padding-horizontal ion-padding" *ngIf="module.visible !== 0 && module.isStealth"> |             <div class="ion-padding" *ngIf="module.visible !== 0 && module.isStealth"> | ||||||
|                 <ion-badge class="ion-text-wrap"> |                 <ion-chip class="ion-text-wrap"> | ||||||
|                     {{ 'core.course.hiddenoncoursepage' | translate }} |                     <ion-icon name="fas-eye-slash" aria-hidden="true"></ion-icon> | ||||||
|                 </ion-badge> |                     <ion-label>{{ 'core.course.hiddenoncoursepage' | translate }}</ion-label> | ||||||
|  |                 </ion-chip> | ||||||
|             </div> |             </div> | ||||||
|             <div class="safe-area-padding-horizontal ion-padding core-module-availabilityinfo" *ngIf="module.availabilityinfo"> |             <div class="ion-padding core-module-availabilityinfo" *ngIf="module.availabilityinfo"> | ||||||
|                 <ion-badge class="ion-text-wrap">{{ 'core.restricted' | translate }}</ion-badge> |                 <ion-icon name="fas-lock" [attr.aria-label]="'core.restricted' | translate"></ion-icon> | ||||||
|                 <div> |                 <ion-label> | ||||||
|                     <core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id" |                     <core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id" | ||||||
|                         [courseId]="courseId" class="ion-text-wrap"> |                         [courseId]="courseId" class="ion-text-wrap"> | ||||||
|                     </core-format-text> |                     </core-format-text> | ||||||
|                 </div> |                 </ion-label> | ||||||
|             </div> |             </div> | ||||||
|             <div class="safe-area-padding-horizontal ion-padding" *ngIf="module.completiondata?.offline"> |             <div class="ion-padding" *ngIf="module.completiondata?.offline"> | ||||||
|                 <ion-badge color="warning" class="ion-text-wrap"> |                 <ion-chip color="warning" class="ion-text-wrap"> | ||||||
|                     {{ 'core.course.manualcompletionnotsynced' | translate }} |                     <ion-label>{{ 'core.course.manualcompletionnotsynced' | translate }}</ion-label> | ||||||
|                 </ion-badge> |                 </ion-chip> | ||||||
|             </div> |             </div> | ||||||
| 
 | 
 | ||||||
|             <core-course-unsupported-module *ngIf="unsupported" [module]="module" [courseId]="courseId"></core-course-unsupported-module> |             <core-course-unsupported-module *ngIf="unsupported" [module]="module" [courseId]="courseId"></core-course-unsupported-module> | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ | |||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1> |             <h1> | ||||||
|                 <core-format-text [text]="course?.fullname" contextLevel="course" [contextInstanceId]="course?.id"></core-format-text> |                 {{'core.course.coursesummary' | translate}} | ||||||
|             </h1> |             </h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| @ -16,44 +16,49 @@ | |||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
|     <core-loading [hideUntil]="dataLoaded"> |     <core-loading [hideUntil]="dataLoaded"> | ||||||
|         <div *ngIf="courseImageUrl" class="core-course-thumb-parallax"> |         <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="" /> |                 <img [src]="courseImageUrl" core-external-content alt="" /> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|         <div class="core-course-thumb-parallax-content" *ngIf="course"> |         <div class="core-course-thumb-parallax-content" *ngIf="course"> | ||||||
|             <ion-item class="ion-text-wrap" (click)="openCourse()" [attr.aria-label]="course.fullname" [detail]="canAccessCourse" |             <ion-item class="ion-text-wrap"> | ||||||
|                 [button]="canAccessCourse"> |  | ||||||
|                 <ion-icon name="fas-graduation-cap" fixed-width slot="start" aria-hidden="true"></ion-icon> |  | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <h2> |  | ||||||
|                         <core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"> |  | ||||||
|                         </core-format-text> |  | ||||||
|                     </h2> |  | ||||||
|                     <p *ngIf="course.categoryname"> |                     <p *ngIf="course.categoryname"> | ||||||
|                         <core-format-text [text]="course.categoryname" contextLevel="coursecat" [contextInstanceId]="course.categoryid"> |                         <core-format-text [text]="course.categoryname" contextLevel="coursecat" [contextInstanceId]="course.categoryid"> | ||||||
|                         </core-format-text> |                         </core-format-text> | ||||||
|                     </p> |                     </p> | ||||||
|  |                     <h2> | ||||||
|  |                         <core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"> | ||||||
|  |                         </core-format-text> | ||||||
|  |                     </h2> | ||||||
|                     <p *ngIf="course.startdate"> |                     <p *ngIf="course.startdate"> | ||||||
|                         {{course.startdate * 1000 | coreFormatDate:"strftimedatefullshort" }} |                         {{course.startdate * 1000 | coreFormatDate:"strftimedatefullshort" }} | ||||||
|                         <span *ngIf="course.enddate"> - {{course.enddate * 1000 | coreFormatDate:"strftimedatefullshort" }}</span> |                         <span *ngIf="course.enddate"> - {{course.enddate * 1000 | coreFormatDate:"strftimedatefullshort" }}</span> | ||||||
|                     </p> |                     </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-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| 
 | 
 | ||||||
|             <ion-item class="ion-text-wrap" *ngIf="course.summary" detail="false"> |             <ion-item class="ion-text-wrap" *ngIf="course.summary" detail="false"> | ||||||
|                 <ion-label> |                 <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 [text]="course.summary" [maxHeight]="120" contextLevel="course" [contextInstanceId]="course.id"> | ||||||
|                     </core-format-text> |                     </core-format-text> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| 
 | 
 | ||||||
|             <ng-container class="ion-text-wrap" *ngIf="course.contacts && course.contacts.length"> |             <ion-list *ngIf="course.contacts && course.contacts.length"> | ||||||
|                 <ion-item-divider> |                 <ion-item-divider class="ion-text-wrap"> | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <h2>{{ 'core.teachers' | translate }}</h2> |                         <h2>{{ 'core.teachers' | translate }}</h2> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item-divider> |                 </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"> |                     [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 [user]="contact" slot="start" [userId]="contact.id" [courseId]="isEnrolled ? course.id : null"> | ||||||
|                     </core-user-avatar> |                     </core-user-avatar> | ||||||
| @ -62,7 +67,7 @@ | |||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
|                 <core-spacer></core-spacer> |                 <core-spacer></core-spacer> | ||||||
|             </ng-container> |             </ion-list> | ||||||
| 
 | 
 | ||||||
|             <ion-item class="ion-text-wrap" *ngIf="course.customfields"> |             <ion-item class="ion-text-wrap" *ngIf="course.customfields"> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
| @ -83,7 +88,8 @@ | |||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| 
 | 
 | ||||||
|             <div *ngIf="!isEnrolled" detail="false"> |             <!-- Enrol --> | ||||||
|  |             <ng-container *ngIf="!isEnrolled"> | ||||||
|                 <ion-item class="ion-text-wrap" *ngFor="let instance of selfEnrolInstances"> |                 <ion-item class="ion-text-wrap" *ngFor="let instance of selfEnrolInstances"> | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <p class="item-heading">{{ instance.name }}</p> |                         <p class="item-heading">{{ instance.name }}</p> | ||||||
| @ -92,23 +98,24 @@ | |||||||
|                         </ion-button> |                         </ion-button> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
|             </div> |                 <ion-item class="ion-text-wrap" *ngIf="paypalEnabled"> | ||||||
|             <ion-item class="ion-text-wrap" *ngIf="!isEnrolled && paypalEnabled"> |                     <ion-label> | ||||||
|                 <ion-label> |                         <p class="item-heading">{{ 'core.courses.paypalaccepted' | translate }}</p> | ||||||
|                     <h2>{{ 'core.courses.paypalaccepted' | translate }}</h2> |                         <p *ngIf="isMobile">{{ 'core.paymentinstant' | translate }}</p> | ||||||
|                     <p>{{ 'core.paymentinstant' | translate }}</p> |                         <ion-button *ngIf="isMobile" expand="block" class="ion-margin-top" (click)="paypalEnrol()"> | ||||||
|                     <ion-button expand="block" class="ion-margin-top" (click)="paypalEnrol()" *ngIf="isMobile"> |                             {{ 'core.courses.sendpaymentbutton' | translate }} | ||||||
|                         {{ 'core.courses.sendpaymentbutton' | translate }} |                         </ion-button> | ||||||
|                     </ion-button> |                     </ion-label> | ||||||
|                 </ion-label> |                 </ion-item> | ||||||
|             </ion-item> |                 <ion-item *ngIf="!selfEnrolInstances.length && !paypalEnabled"> | ||||||
|             <ion-item *ngIf="!isEnrolled && !selfEnrolInstances.length && !paypalEnabled"> |                     <ion-label> | ||||||
|                 <ion-label> |                         <p class="item-heading">{{ 'core.courses.notenrollable' | translate }}</p> | ||||||
|                     <p>{{ 'core.courses.notenrollable' | translate }}</p> |                     </ion-label> | ||||||
|                 </ion-label> |                 </ion-item> | ||||||
|             </ion-item> |             </ng-container> | ||||||
|             <ion-item *ngIf="canAccessCourse && downloadCourseEnabled" (click)="prefetchCourse()" detail="false" | 
 | ||||||
|                 [attr.aria-label]="prefetchCourseData.statusTranslatable | translate" button> |             <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" |                 <ion-icon *ngIf="(prefetchCourseData.status != statusDownloaded) && !prefetchCourseData.loading" | ||||||
|                     [name]="prefetchCourseData.icon" slot="start" aria-hidden="true"> |                     [name]="prefetchCourseData.icon" slot="start" aria-hidden="true"> | ||||||
|                 </ion-icon> |                 </ion-icon> | ||||||
| @ -116,23 +123,24 @@ | |||||||
|                     [name]="prefetchCourseData.icon" color="success" aria-hidden="true" role="status"> |                     [name]="prefetchCourseData.icon" color="success" aria-hidden="true" role="status"> | ||||||
|                 </ion-icon> |                 </ion-icon> | ||||||
|                 <ion-spinner *ngIf="prefetchCourseData.loading" slot="start" [attr.aria-label]="'core.loading' | translate"></ion-spinner> |                 <ion-spinner *ngIf="prefetchCourseData.loading" slot="start" [attr.aria-label]="'core.loading' | translate"></ion-spinner> | ||||||
|                 <ion-label> |                 <ion-label *ngIf="prefetchCourseData.status != statusDownloaded">{{ 'core.course.downloadcourse' | translate }}</ion-label> | ||||||
|                     <h2 *ngIf="prefetchCourseData.status != statusDownloaded">{{ 'core.course.downloadcourse' | translate }}</h2> |                 <ion-label *ngIf="prefetchCourseData.status == statusDownloaded">{{ 'core.course.refreshcourse' | translate }}</ion-label> | ||||||
|                     <h2 *ngIf="prefetchCourseData.status == statusDownloaded">{{ 'core.course.refreshcourse' | translate }}</h2> |             </ion-button> | ||||||
|                 </ion-label> | 
 | ||||||
|             </ion-item> |             <ion-button class="ion-margin" (click)="openCourse()" *ngIf="!avoidOpenCourse && canAccessCourse" expand="block"> | ||||||
|             <ion-item button (click)="openCourse()" [attr.aria-label]="course.fullname" *ngIf="canAccessCourse" detail="true"> |  | ||||||
|                 <ion-icon name="fas-briefcase" slot="start" aria-hidden="true"></ion-icon> |                 <ion-icon name="fas-briefcase" slot="start" aria-hidden="true"></ion-icon> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <h2>{{ 'core.course' | translate }}</h2> |                     {{ 'core.course' | translate }} | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-button> | ||||||
|             <ion-item [href]="courseUrl" core-link [attr.aria-label]="course.fullname" button detail="false" [showBrowserWarning]="false"> | 
 | ||||||
|  |             <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-icon name="fas-external-link-alt" slot="start" aria-hidden="true"></ion-icon> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <h2>{{ 'core.openinbrowser' | translate }}</h2> |                     {{ 'core.openinbrowser' | translate }} | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-button> | ||||||
|  | 
 | ||||||
|         </div> |         </div> | ||||||
|     </core-loading> |     </core-loading> | ||||||
| </ion-content> | </ion-content> | ||||||
|  | |||||||
| @ -20,8 +20,8 @@ import { CoreSites } from '@services/sites'; | |||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
| import { CoreTextUtils } from '@services/utils/text'; | import { CoreTextUtils } from '@services/utils/text'; | ||||||
| import { | import { | ||||||
|  |     CoreCourseCustomField, | ||||||
|     CoreCourseEnrolmentMethod, |     CoreCourseEnrolmentMethod, | ||||||
|     CoreCourseGetCoursesData, |  | ||||||
|     CoreCourses, |     CoreCourses, | ||||||
|     CoreCourseSearchedData, |     CoreCourseSearchedData, | ||||||
|     CoreCoursesProvider, |     CoreCoursesProvider, | ||||||
| @ -34,6 +34,8 @@ import { Translate } from '@singletons'; | |||||||
| import { CoreConstants } from '@/core/constants'; | import { CoreConstants } from '@/core/constants'; | ||||||
| import { CoreCoursesSelfEnrolPasswordComponent } from '../../../courses/components/self-enrol-password/self-enrol-password'; | import { CoreCoursesSelfEnrolPasswordComponent } from '../../../courses/components/self-enrol-password/self-enrol-password'; | ||||||
| import { CoreNavigator } from '@services/navigator'; | 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. |  * 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 { | export class CoreCoursePreviewPage implements OnInit, OnDestroy { | ||||||
| 
 | 
 | ||||||
|     course?: CoreCourseSearchedData; |     course?: CoreCourseSummaryData; | ||||||
|     isEnrolled = false; |     isEnrolled = false; | ||||||
|     canAccessCourse = true; |     canAccessCourse = true; | ||||||
|     selfEnrolInstances: CoreCourseEnrolmentMethod[] = []; |     selfEnrolInstances: CoreCourseEnrolmentMethod[] = []; | ||||||
|     paypalEnabled = false; |     paypalEnabled = false; | ||||||
|     dataLoaded = false; |     dataLoaded = false; | ||||||
|  |     avoidOpenCourse = false; | ||||||
|     prefetchCourseData: CorePrefetchStatusInfo = { |     prefetchCourseData: CorePrefetchStatusInfo = { | ||||||
|         icon: '', |         icon: '', | ||||||
|         statusTranslatable: 'core.loading', |         statusTranslatable: 'core.loading', | ||||||
| @ -64,6 +67,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | |||||||
|     courseUrl = ''; |     courseUrl = ''; | ||||||
|     courseImageUrl?: string; |     courseImageUrl?: string; | ||||||
|     isMobile: boolean; |     isMobile: boolean; | ||||||
|  |     progress?: number; | ||||||
| 
 | 
 | ||||||
|     protected isGuestEnabled = false; |     protected isGuestEnabled = false; | ||||||
|     protected useGuestAccess = false; |     protected useGuestAccess = false; | ||||||
| @ -74,6 +78,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | |||||||
|     protected paypalReturnUrl = ''; |     protected paypalReturnUrl = ''; | ||||||
|     protected pageDestroyed = false; |     protected pageDestroyed = false; | ||||||
|     protected courseStatusObserver?: CoreEventObserver; |     protected courseStatusObserver?: CoreEventObserver; | ||||||
|  |     protected courseId!: number; | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         protected zone: NgZone, |         protected zone: NgZone, | ||||||
| @ -84,7 +89,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | |||||||
|         if (this.downloadCourseEnabled) { |         if (this.downloadCourseEnabled) { | ||||||
|             // Listen for status change in course.
 |             // Listen for status change in course.
 | ||||||
|             this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => { |             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); |                     this.updateCourseStatus(data.status); | ||||||
|                 } |                 } | ||||||
|             }, CoreSites.getCurrentSiteId()); |             }, CoreSites.getCurrentSiteId()); | ||||||
| @ -92,27 +97,25 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * View loaded. |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     async ngOnInit(): Promise<void> { |     async ngOnInit(): Promise<void> { | ||||||
|         this.course = CoreNavigator.getRouteParam('course'); |         try { | ||||||
| 
 |             this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); | ||||||
|         if (!this.course) { |         } catch (error) { | ||||||
|  |             CoreDomUtils.showErrorModal(error); | ||||||
|             CoreNavigator.back(); |             CoreNavigator.back(); | ||||||
| 
 | 
 | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const currentSite = CoreSites.getCurrentSite(); |         this.avoidOpenCourse = !!CoreNavigator.getRouteBooleanParam('avoidOpenCourse'); | ||||||
|         const currentSiteUrl = currentSite && currentSite.getURL(); |         this.course = CoreNavigator.getRouteParam('course'); | ||||||
| 
 | 
 | ||||||
|         this.paypalEnabled = this.course!.enrollmentmethods?.indexOf('paypal') > -1; |         const currentSiteUrl = CoreSites.getRequiredCurrentSite().getURL(); | ||||||
|         this.enrolUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'enrol/index.php?id=' + this.course!.id); |         this.enrolUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'enrol/index.php?id=' + this.courseId); | ||||||
|         this.courseUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'course/view.php?id=' + this.course!.id); |         this.courseUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'course/view.php?id=' + this.courseId); | ||||||
|         this.paypalReturnUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'enrol/paypal/return.php'); |         this.paypalReturnUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'enrol/paypal/return.php'); | ||||||
|         if (this.course.overviewfiles.length > 0) { |  | ||||||
|             this.courseImageUrl = this.course.overviewfiles[0].fileurl; |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             await this.getCourse(); |             await this.getCourse(); | ||||||
| @ -120,11 +123,11 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | |||||||
|             if (this.downloadCourseEnabled) { |             if (this.downloadCourseEnabled) { | ||||||
| 
 | 
 | ||||||
|                 // Determine course prefetch icon.
 |                 // Determine course prefetch icon.
 | ||||||
|                 this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.course!.id); |                 this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.courseId); | ||||||
| 
 | 
 | ||||||
|                 if (this.prefetchCourseData.loading) { |                 if (this.prefetchCourseData.loading) { | ||||||
|                     // Course is being downloaded. Get the download promise.
 |                     // Course is being downloaded. Get the download promise.
 | ||||||
|                     const promise = CoreCourseHelper.getCourseDownloadPromise(this.course!.id); |                     const promise = CoreCourseHelper.getCourseDownloadPromise(this.courseId); | ||||||
|                     if (promise) { |                     if (promise) { | ||||||
|                         // There is a download promise. If it fails, show an error.
 |                         // There is a download promise. If it fails, show an error.
 | ||||||
|                         promise.catch((error) => { |                         promise.catch((error) => { | ||||||
| @ -134,7 +137,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | |||||||
|                         }); |                         }); | ||||||
|                     } else { |                     } else { | ||||||
|                         // No download, this probably means that the app was closed while downloading. Set previous status.
 |                         // 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 = []; |         this.selfEnrolInstances = []; | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             this.enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(this.course!.id); |             this.enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(this.courseId); | ||||||
| 
 | 
 | ||||||
|             this.enrolmentMethods.forEach((method) => { |             this.enrolmentMethods.forEach((method) => { | ||||||
|                 if (method.type === 'self') { |                 if (method.type === 'self') { | ||||||
|                     this.selfEnrolInstances.push(method); |                     this.selfEnrolInstances.push(method); | ||||||
|                 } else if (method.type === 'guest') { |                 } else if (method.type === 'guest') { | ||||||
|                     this.isGuestEnabled = true; |                     this.isGuestEnabled = true; | ||||||
|  |                 } else if (method.type === 'paypal') { | ||||||
|  |                     this.paypalEnabled = true; | ||||||
|                 } |                 } | ||||||
|             }); |             }); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
| @ -191,22 +196,17 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             let course: CoreEnrolledCourseData | CoreCourseGetCoursesData; |  | ||||||
| 
 |  | ||||||
|             // Check if user is enrolled in the course.
 |             // Check if user is enrolled in the course.
 | ||||||
|             try { |             try { | ||||||
|                 course = await CoreCourses.getUserCourse(this.course!.id); |                 this.course = await CoreCourses.getUserCourse(this.courseId); | ||||||
|                 this.isEnrolled = true; |                 this.isEnrolled = true; | ||||||
|             } catch { |             } catch { | ||||||
|                 // The user is not enrolled in the course. Use getCourses to see if it's an admin/manager and can see the course.
 |                 // 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; |                 this.isEnrolled = false; | ||||||
| 
 |                 this.course = await CoreCourses.getCourse(this.courseId); | ||||||
|                 course = await CoreCourses.getCourse(this.course!.id); |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Success retrieving the course, we can assume the user has permissions to view it.
 |             // 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.canAccessCourse = true; | ||||||
|             this.useGuestAccess = false; |             this.useGuestAccess = false; | ||||||
|         } catch { |         } catch { | ||||||
| @ -219,14 +219,37 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.7')) { |         if (this.course && 'overviewfiles' in this.course && this.course.overviewfiles?.length) { | ||||||
|             try { |             this.courseImageUrl = this.course.overviewfiles[0].fileurl; | ||||||
|                 const course = await CoreCourses.getCourseByField('id', this.course!.id); |         } | ||||||
| 
 | 
 | ||||||
|                 this.course!.customfields = course.customfields; |         try { | ||||||
|             } catch { |             const courseByField = await CoreCourses.getCourseByField('id', this.courseId); | ||||||
|                 // Ignore errors.
 |             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; | ||||||
|  | 
 | ||||||
|  |         } 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; |         this.dataLoaded = true; | ||||||
| @ -234,13 +257,15 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Open the course. |      * Open the course. | ||||||
|  |      * | ||||||
|  |      * @param replaceCurrentPage If current place should be replaced in the navigation stack. | ||||||
|      */ |      */ | ||||||
|     openCourse(): void { |     openCourse(replaceCurrentPage = false): void { | ||||||
|         if (!this.canAccessCourse) { |         if (!this.canAccessCourse || !this.course || this.avoidOpenCourse) { | ||||||
|             return; |             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.
 |         // 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.
 |         // Observe loaded pages in the InAppBrowser to check if the enrol process has ended.
 | ||||||
|         const inAppLoadSubscription = window.on('loadstart').subscribe((event) => { |         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); |         const modal = await CoreDomUtils.showModalLoading('core.loading', true); | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             await CoreCourses.selfEnrol(this.course!.id, password, instanceId); |             await CoreCourses.selfEnrol(this.courseId, password, instanceId); | ||||||
| 
 | 
 | ||||||
|             // Close modal and refresh data.
 |             // Close modal and refresh data.
 | ||||||
|             this.isEnrolled = true; |             this.isEnrolled = true; | ||||||
| @ -331,13 +356,13 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | |||||||
|             await this.refreshData().finally(() => { |             await this.refreshData().finally(() => { | ||||||
|                 // My courses have been updated, trigger event.
 |                 // My courses have been updated, trigger event.
 | ||||||
|                 CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { |                 CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { | ||||||
|                     courseId: this.course!.id, |                     courseId: this.courseId, | ||||||
|                     course: this.course, |                     course: this.course, | ||||||
|                     action: CoreCoursesProvider.ACTION_ENROL, |                     action: CoreCoursesProvider.ACTION_ENROL, | ||||||
|                 }, CoreSites.getCurrentSiteId()); |                 }, CoreSites.getCurrentSiteId()); | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|             this.openCourse(); |             this.openCourse(true); | ||||||
| 
 | 
 | ||||||
|             modal?.dismiss(); |             modal?.dismiss(); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
| @ -378,12 +403,10 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | |||||||
|         const promises: Promise<void>[] = []; |         const promises: Promise<void>[] = []; | ||||||
| 
 | 
 | ||||||
|         promises.push(CoreCourses.invalidateUserCourses()); |         promises.push(CoreCourses.invalidateUserCourses()); | ||||||
|         promises.push(CoreCourses.invalidateCourse(this.course!.id)); |         promises.push(CoreCourses.invalidateCourse(this.courseId)); | ||||||
|         promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.course!.id)); |         promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.courseId)); | ||||||
|         promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(this.course!.id)); |         promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(this.courseId)); | ||||||
|         if (CoreSites.getCurrentSite() && !CoreSites.getCurrentSite()!.isVersionGreaterEqualThan('3.7')) { |         promises.push(CoreCourses.invalidateCoursesByField('id', this.courseId)); | ||||||
|             promises.push(CoreCourses.invalidateCoursesByField('id', this.course!.id)); |  | ||||||
|         } |  | ||||||
|         if (this.guestInstanceId) { |         if (this.guestInstanceId) { | ||||||
|             promises.push(CoreCourses.invalidateCourseGuestEnrolmentInfo(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.
 |         // Check if user is enrolled in the course.
 | ||||||
|         try { |         await CoreUtils.ignoreErrors(CoreCourses.invalidateUserCourses()); | ||||||
|             CoreCourses.invalidateUserCourses(); |  | ||||||
|         } catch { |  | ||||||
|             // Ignore errors.
 |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             await CoreCourses.getUserCourse(this.course!.id); |             await CoreCourses.getUserCourse(this.courseId); | ||||||
|         } catch { |         } catch { | ||||||
|             // Not enrolled, wait a bit and try again.
 |             // Not enrolled, wait a bit and try again.
 | ||||||
|             if (this.pageDestroyed || (Date.now() - this.waitStart > 60000)) { |             if (this.pageDestroyed || (Date.now() - this.waitStart > 60000)) { | ||||||
| @ -451,7 +470,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | |||||||
|      */ |      */ | ||||||
|     async prefetchCourse(): Promise<void> { |     async prefetchCourse(): Promise<void> { | ||||||
|         try { |         try { | ||||||
|             await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course!, { |             await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course as CoreEnrolledCourseData, { | ||||||
|                 isGuest: this.useGuestAccess, |                 isGuest: this.useGuestAccess, | ||||||
|             }); |             }); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
| @ -462,14 +481,20 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Page destroyed. |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     ngOnDestroy(): void { |     ngOnDestroy(): void { | ||||||
|         this.pageDestroyed = true; |         this.pageDestroyed = true; | ||||||
| 
 |         this.courseStatusObserver?.off(); | ||||||
|         if (this.courseStatusObserver) { |  | ||||||
|             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 { | :host { | ||||||
|     --scroll-factor: 0.5; |     ion-content:not(.animating) { | ||||||
|     --translate-z: calc(-2 * var(--scroll-factor))px; |         &::part(scroll) { | ||||||
|     --scale: calc(1 + var(--scroll-factor) * 2); |             perspective: 1px; | ||||||
|  |             perspective-origin: center top; | ||||||
|  |             transform-style: preserve-3d; | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|     perspective: 1px; |         .core-course-thumb { | ||||||
|     perspective-origin: center top; |             transform-origin: center top; | ||||||
|     transform-style: preserve-3d; |  | ||||||
| 
 | 
 | ||||||
|     // @todo This parallax effect caused the image to be scaled during page transitions, |             --scroll-factor: 0.5; | ||||||
|     // and in some devices it seems like the problem persisted even after the transition. |             --translate-z: calc(-2 * var(--scroll-factor))px; | ||||||
|     // We should decide whether we want to keep this parallax or not, and if we do fix |             --scale: calc(1 + var(--scroll-factor) * 2); | ||||||
|     // the problem or find an alternative implementation. For now, it's disabled. | 
 | ||||||
|  |             /** | ||||||
|  |             * Calculated with scroll-factor: 0.5; | ||||||
|  |             * translate-z: -2 * $scroll-factor px; | ||||||
|  |             * scale: 1 + $scroll-factor * 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-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 { |     .core-course-thumb { | ||||||
|         overflow: hidden; |         overflow: hidden; | ||||||
|         text-align: center; |         text-align: center; | ||||||
|         cursor: pointer; |  | ||||||
|         pointer-events: auto; |  | ||||||
|         transform-origin: center top; |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Calculated with scroll-factor: 0.5; |  | ||||||
|          * translate-z: -2 * $scroll-factor px; |  | ||||||
|          * scale: 1 + $scroll-factor * 2; |  | ||||||
|          */ |  | ||||||
|         // transform: translateZ(-1px) scale(2); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -68,7 +68,7 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events'; | |||||||
| import { CoreFilterHelper } from '@features/filter/services/filter-helper'; | import { CoreFilterHelper } from '@features/filter/services/filter-helper'; | ||||||
| import { CoreNetworkError } from '@classes/errors/network-error'; | import { CoreNetworkError } from '@classes/errors/network-error'; | ||||||
| import { CoreSiteHome } from '@features/sitehome/services/sitehome'; | 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 { CoreSiteHomeHomeHandlerService } from '@features/sitehome/services/handlers/sitehome-home'; | ||||||
| import { CoreStatusWithWarningsWSResponse } from '@services/ws'; | import { CoreStatusWithWarningsWSResponse } from '@services/ws'; | ||||||
| 
 | 
 | ||||||
| @ -1178,7 +1178,7 @@ export class CoreCourseHelperProvider { | |||||||
| 
 | 
 | ||||||
|         modal?.dismiss(); |         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. |      * they will see the result immediately. | ||||||
|      * |      * | ||||||
|      * @param course Course to open |      * @param course Course to open | ||||||
|      * @param params Params to pass to the course page. |      * @param navOptions Navigation options that includes params to pass to the page. | ||||||
|      * @param siteId Site ID. If not defined, current site. |  | ||||||
|      * @return Promise resolved when done. |      * @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()) { |         if (!siteId || siteId == CoreSites.getCurrentSiteId()) { | ||||||
|             // Current site, we can open the course.
 |             // Current site, we can open the course.
 | ||||||
|             return CoreCourse.openCourse(course, params); |             return CoreCourse.openCourse(course, navOptions); | ||||||
|         } else { |         } else { | ||||||
|             // We need to load the site first.
 |             // We need to load the site first.
 | ||||||
|             params = params || {}; |             navOptions = navOptions || {}; | ||||||
|             Object.assign(params, { course: course }); |  | ||||||
| 
 | 
 | ||||||
|             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 { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; | ||||||
| import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync'; | import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync'; | ||||||
| import { CoreTagItem } from '@features/tag/services/tag'; | import { CoreTagItem } from '@features/tag/services/tag'; | ||||||
| import { CoreNavigator } from '@services/navigator'; | import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; | ||||||
| import { CoreCourseModuleDelegate } from './module-delegate'; | import { CoreCourseModuleDelegate } from './module-delegate'; | ||||||
| 
 | 
 | ||||||
| const ROOT_CACHE_KEY = 'mmCourse:'; | 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 { | export enum CoreCourseModuleCompletionStatus { | ||||||
|     COMPLETION_INCOMPLETE = 0, |     COMPLETION_INCOMPLETE = 0, | ||||||
| @ -71,6 +71,11 @@ export enum CoreCourseModuleCompletionStatus { | |||||||
|     COMPLETION_COMPLETE_FAIL = 3, |     COMPLETION_COMPLETE_FAIL = 3, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export enum CoreCourseCompletionType { | ||||||
|  |     MANUAL = 0, | ||||||
|  |     AUTO = 1, | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Completion tracking valid values. |  * 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. |      * This function must be in here instead of course helper to prevent circular dependencies. | ||||||
|      * |      * | ||||||
|      * @param course Course to open |      * @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. |      * @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(); |         const loading = await CoreDomUtils.showModalLoading(); | ||||||
| 
 | 
 | ||||||
|         // Wait for site plugins to be fetched.
 |         // Wait for site plugins to be fetched.
 | ||||||
| @ -1192,7 +1200,7 @@ export class CoreCourseProvider { | |||||||
|         if (!format || !CoreSitePlugins.sitePluginPromiseExists(`format_${format}`)) { |         if (!format || !CoreSitePlugins.sitePluginPromiseExists(`format_${format}`)) { | ||||||
|             // No custom format plugin. We don't need to wait for anything.
 |             // No custom format plugin. We don't need to wait for anything.
 | ||||||
|             loading.dismiss(); |             loading.dismiss(); | ||||||
|             await CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params); |             await CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions); | ||||||
| 
 | 
 | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @ -1203,7 +1211,7 @@ export class CoreCourseProvider { | |||||||
| 
 | 
 | ||||||
|             // The format loaded successfully, but the handlers wont be registered until all site plugins have loaded.
 |             // The format loaded successfully, but the handlers wont be registered until all site plugins have loaded.
 | ||||||
|             if (CoreSitePlugins.sitePluginsFinishedLoading) { |             if (CoreSitePlugins.sitePluginsFinishedLoading) { | ||||||
|                 return CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params); |                 return CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Wait for plugins to be loaded.
 |             // Wait for plugins to be loaded.
 | ||||||
| @ -1212,7 +1220,7 @@ export class CoreCourseProvider { | |||||||
|             const observer = CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => { |             const observer = CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => { | ||||||
|                 observer?.off(); |                 observer?.off(); | ||||||
| 
 | 
 | ||||||
|                 CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params) |                 CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions) | ||||||
|                     .then(deferred.resolve).catch(deferred.reject); |                     .then(deferred.resolve).catch(deferred.reject); | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -13,10 +13,10 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Injectable, Type } from '@angular/core'; | import { Injectable, Type } from '@angular/core'; | ||||||
| import { Params } from '@angular/router'; |  | ||||||
| 
 | 
 | ||||||
| import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; | import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; | ||||||
| import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | ||||||
|  | import { CoreNavigationOptions } from '@services/navigator'; | ||||||
| import { makeSingleton } from '@singletons'; | import { makeSingleton } from '@singletons'; | ||||||
| import { CoreCourseWSSection } from './course'; | import { CoreCourseWSSection } from './course'; | ||||||
| import { CoreCourseSection } from './course-helper'; | import { CoreCourseSection } from './course-helper'; | ||||||
| @ -66,13 +66,22 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler { | |||||||
|      */ |      */ | ||||||
|     displayEnableDownload?(course: CoreCourseAnyCourseData): boolean; |     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. |      * Whether the default section selector should be displayed. Defaults to true. | ||||||
|      * |      * | ||||||
|      * @param course The course to check. |      * @param course The course to check. | ||||||
|      * @return Whether the default section selector should be displayed. |      * @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, |      * 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>; |     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. |      * 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 |      * 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. |      * Your page should include the course handlers using CoreCoursesDelegate. | ||||||
|      * |      * | ||||||
|      * @param course The course to open. It should contain a "format" attribute. |      * @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. |      * @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. |      * 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>; |     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 |      * 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. |      * 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. |      * @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]); |         return !!this.executeFunctionOnEnabled<boolean>(course.format || '', 'displaySectionSelector', [course]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -276,8 +290,8 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm | |||||||
|      * @param sections List of sections. |      * @param sections List of sections. | ||||||
|      * @return Course title. |      * @return Course title. | ||||||
|      */ |      */ | ||||||
|     getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseWSSection[]): string | undefined { |     getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseWSSection[]): string { | ||||||
|         return this.executeFunctionOnEnabled(course.format || '', 'getCourseTitle', [course, sections]); |         return this.executeFunctionOnEnabled(course.format || '', 'getCourseTitle', [course, sections]) || ''; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -287,9 +301,9 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm | |||||||
|      * @param sections List of sections. |      * @param sections List of sections. | ||||||
|      * @return Promise resolved with current section. |      * @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 { |         try { | ||||||
|             const section = await this.executeFunctionOnEnabled<CoreCourseSection>( |             const section = await this.executeFunctionOnEnabled<T>( | ||||||
|                 course.format || '', |                 course.format || '', | ||||||
|                 'getCurrentSection', |                 'getCurrentSection', | ||||||
|                 [course, sections], |                 [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. |      * @param course The course to get the text. | ||||||
|      * @return Promise resolved with component to use, undefined if not found. |      * @return The name for the highlighted section based on the given course format. | ||||||
|      */ |      */ | ||||||
|     async getSectionSelectorComponent(course: CoreCourseAnyCourseData): Promise<Type<unknown> | undefined> { |     getSectionHightlightedName(course: CoreCourseAnyCourseData): string | undefined { | ||||||
|         try { |         return this.executeFunctionOnEnabled<string>( | ||||||
|             return await this.executeFunctionOnEnabled<Type<unknown>>(course.format || '', 'getSectionSelectorComponent', [course]); |             course.format || '', | ||||||
|         } catch (error) { |             'getSectionHightlightedName', | ||||||
|             this.logger.error('Error getting section selector component', error); |         ); | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -346,11 +359,11 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm | |||||||
|      * Open a course. Should not be called directly. Call CoreCourseHelper.openCourse instead. |      * 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 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. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     async openCourse(course: CoreCourseAnyCourseData, params?: Params): Promise<void> { |     async openCourse(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise<void> { | ||||||
|         await this.executeFunctionOnEnabled(course.format || '', 'openCourse', [course, params]); |         await this.executeFunctionOnEnabled(course.format || '', 'openCourse', [course, navOptions]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -13,12 +13,10 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
| import { Params } from '@angular/router'; |  | ||||||
| 
 |  | ||||||
| import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses'; | 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 { CoreUtils } from '@services/utils/utils'; | ||||||
| import { CoreCourseWSSection } from '../course'; | import { Translate } from '@singletons'; | ||||||
| import { CoreCourseSection } from '../course-helper'; | import { CoreCourseSection } from '../course-helper'; | ||||||
| import { CoreCourseFormatHandler } from '../format-delegate'; | import { CoreCourseFormatHandler } from '../format-delegate'; | ||||||
| 
 | 
 | ||||||
| @ -32,19 +30,14 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { | |||||||
|     format = 'default'; |     format = 'default'; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Whether or not the handler is enabled on a site level. |      * @inheritdoc | ||||||
|      * |  | ||||||
|      * @return Promise resolved with true if enabled. |  | ||||||
|      */ |      */ | ||||||
|     async isEnabled(): Promise<boolean> { |     async isEnabled(): Promise<boolean> { | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Get the title to use in course page. |      * @inheritdoc | ||||||
|      * |  | ||||||
|      * @param course The course. |  | ||||||
|      * @return Title. |  | ||||||
|      */ |      */ | ||||||
|     getCourseTitle(course: CoreCourseAnyCourseData): string { |     getCourseTitle(course: CoreCourseAnyCourseData): string { | ||||||
|         if (course.displayname) { |         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. |      * @inheritdoc | ||||||
|      * |  | ||||||
|      * @param course The course to check. |  | ||||||
|      * @return Whether it can view all sections. |  | ||||||
|      */ |      */ | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 |     canViewAllSections(): boolean { | ||||||
|     canViewAllSections(course: CoreCourseAnyCourseData): boolean { |  | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Whether the option blocks should be displayed. Defaults to true. |      * @inheritdoc | ||||||
|      * |  | ||||||
|      * @param course The course to check. |  | ||||||
|      * @return Whether it can display blocks. |  | ||||||
|      */ |      */ | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 |     displayBlocks(): boolean { | ||||||
|     displayBlocks(course: CoreCourseAnyCourseData): boolean { |  | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Whether the default section selector should be displayed. Defaults to true. |      * @inheritdoc | ||||||
|      * |  | ||||||
|      * @param course The course to check. |  | ||||||
|      * @return Whether the default section selector should be displayed. |  | ||||||
|      */ |      */ | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 |     displayCourseIndex(): boolean { | ||||||
|     displaySectionSelector(course: CoreCourseAnyCourseData): boolean { |  | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format, |      * @inheritdoc | ||||||
|      * 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. |  | ||||||
|      */ |      */ | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 |     displayRefresher(): boolean { | ||||||
|     displayRefresher(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): boolean { |  | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Given a list of sections, get the "current" section that should be displayed first. |      * @inheritdoc | ||||||
|      * |  | ||||||
|      * @param course The course to get the title. |  | ||||||
|      * @param sections List of sections. |  | ||||||
|      * @return Current section (or promise resolved with current section). |  | ||||||
|      */ |      */ | ||||||
|     async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> { |     async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> { | ||||||
|         let marker: number | undefined; |         let marker: number | undefined; | ||||||
| @ -137,48 +108,40 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Invalidate the data required to load the course format. |      * @inheritdoc | ||||||
|      * |  | ||||||
|      * @param course The course to get the title. |  | ||||||
|      * @param sections List of sections. |  | ||||||
|      * @return Promise resolved when the data is invalidated. |  | ||||||
|      */ |      */ | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 |     getSectionHightlightedName(): string { | ||||||
|     async invalidateData(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): Promise<void> { |         return Translate.instant('core.course.highlighted'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     async invalidateData(course: CoreCourseAnyCourseData): Promise<void> { | ||||||
|         await CoreCourses.invalidateCoursesByField('id', course.id); |         await CoreCourses.invalidateCoursesByField('id', course.id); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened. |      * @inheritdoc | ||||||
|      * 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. |  | ||||||
|      */ |      */ | ||||||
|     async openCourse(course: CoreCourseAnyCourseData, params?: Params): Promise<void> { |     async openCourse(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise<void> { | ||||||
|         params = params || {}; |         navOptions = navOptions || {}; | ||||||
|         Object.assign(params, { course: course }); | 
 | ||||||
|  |         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.
 |         // Don't return the .push promise, we don't want to display a loading modal during the page transition.
 | ||||||
|         const currentTab = CoreNavigator.getCurrentMainMenuTab(); |         const currentTab = CoreNavigator.getCurrentMainMenuTab(); | ||||||
|         const routeDepth = CoreNavigator.getRouteDepth(`/main/${currentTab}/course/${course.id}`); |         const routeDepth = CoreNavigator.getRouteDepth(`/main/${currentTab}/course/${course.id}`); | ||||||
|         const deepPath = '/deep'.repeat(routeDepth); |         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 |      * @inheritdoc | ||||||
|      * activity completion then you should return false. |  | ||||||
|      * |  | ||||||
|      * @param course The course. |  | ||||||
|      * @return Whether course view should be refreshed when an activity completion changes. |  | ||||||
|      */ |      */ | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 |     async shouldRefreshWhenCompletionChanges(): Promise<boolean> { | ||||||
|     async shouldRefreshWhenCompletionChanges(course: CoreCourseAnyCourseData): Promise<boolean> { |  | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -171,7 +171,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On | |||||||
|             CoreCourseHelper.openCourse(this.course); |             CoreCourseHelper.openCourse(this.course); | ||||||
|         } else { |         } else { | ||||||
|             CoreNavigator.navigateToSitePath( |             CoreNavigator.navigateToSitePath( | ||||||
|                 '/course/' + this.course.id + '/preview', |                 `/course/${this.course.id}/preview`, | ||||||
|                 { params: { course: this.course } }, |                 { params: { course: this.course } }, | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -156,7 +156,7 @@ export class CoreCoursesCourseLinkHandlerService extends CoreContentLinksHandler | |||||||
|         modal.dismiss(); |         modal.dismiss(); | ||||||
| 
 | 
 | ||||||
|         // Now open the course.
 |         // Now open the course.
 | ||||||
|         CoreCourseHelper.openCourse(course, pageParams); |         CoreCourseHelper.openCourse(course, { params: pageParams }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -38,8 +38,9 @@ export class CoreSitePluginsCourseFormatHandler extends CoreSitePluginsBaseHandl | |||||||
|     /** |     /** | ||||||
|      * @inheritdoc |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     displaySectionSelector(): boolean { |     displayCourseIndex(): boolean { | ||||||
|         return this.handlerSchema.displaysectionselector ?? true; |         // 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 { IonRefresher } from '@ionic/angular'; | ||||||
| 
 | 
 | ||||||
| import { CoreCourseFormatComponent } from '@features/course/components/format/format'; | 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 { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; | ||||||
| import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; | ||||||
| import { CoreSitePlugins, CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins'; | import { CoreSitePlugins, CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins'; | ||||||
| @ -33,7 +33,7 @@ import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin- | |||||||
| export class CoreSitePluginsCourseFormatComponent implements OnChanges { | export class CoreSitePluginsCourseFormatComponent implements OnChanges { | ||||||
| 
 | 
 | ||||||
|     @Input() course?: CoreCourseAnyCourseData; // The course to render.
 |     @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() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
 | ||||||
|     @Input() initialSectionId?: number; // The section to load first (by ID).
 |     @Input() initialSectionId?: number; // The section to load first (by ID).
 | ||||||
|     @Input() initialSectionNumber?: number; // The section to load first (by number).
 |     @Input() initialSectionNumber?: number; // The section to load first (by number).
 | ||||||
|  | |||||||
| @ -884,7 +884,11 @@ export type CoreSitePluginsCourseModuleHandlerData = CoreSitePluginsHandlerCommo | |||||||
| export type CoreSitePluginsCourseFormatHandlerData = CoreSitePluginsHandlerCommonData & { | export type CoreSitePluginsCourseFormatHandlerData = CoreSitePluginsHandlerCommonData & { | ||||||
|     canviewallsections?: boolean; |     canviewallsections?: boolean; | ||||||
|     displayenabledownload?: boolean; |     displayenabledownload?: boolean; | ||||||
|  |     /** | ||||||
|  |      * @deprecated on 4.0, use displaycourseindex instead. | ||||||
|  |      */ | ||||||
|     displaysectionselector?: boolean; |     displaysectionselector?: boolean; | ||||||
|  |     displaycourseindex?: boolean; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | |||||||
| @ -301,6 +301,7 @@ | |||||||
|     "strftimetime24": "%H:%M", |     "strftimetime24": "%H:%M", | ||||||
|     "submit": "Submit", |     "submit": "Submit", | ||||||
|     "success": "Success", |     "success": "Success", | ||||||
|  |     "summary": "Summary", | ||||||
|     "tablet": "Tablet", |     "tablet": "Tablet", | ||||||
|     "teachers": "Teachers", |     "teachers": "Teachers", | ||||||
|     "thereisdatatosync": "There are offline {{$a}} to be synchronised.", |     "thereisdatatosync": "There are offline {{$a}} to be synchronised.", | ||||||
|  | |||||||
| @ -1118,7 +1118,7 @@ export class CoreDomUtilsProvider { | |||||||
|             content.scrollToPoint(position[0], position[1], duration || 0); |             content.scrollToPoint(position[0], position[1], duration || 0); | ||||||
| 
 | 
 | ||||||
|             return true; |             return true; | ||||||
|         } catch (error) { |         } catch { | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ import { CoreFilepoolComponentFileEventData } from '@services/filepool'; | |||||||
| import { CoreNavigationOptions } from '@services/navigator'; | import { CoreNavigationOptions } from '@services/navigator'; | ||||||
| import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; | import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; | ||||||
| import { CoreScreenOrientation } from '@services/screen'; | import { CoreScreenOrientation } from '@services/screen'; | ||||||
|  | import { CoreCourseCompletionType } from '@features/course/services/course'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Observer instance to stop listening to an event. |  * Observer instance to stop listening to an event. | ||||||
| @ -48,6 +49,7 @@ export interface CoreEventsData { | |||||||
|     [CoreEvents.SELECT_COURSE_TAB]: CoreEventSelectCourseTabData; |     [CoreEvents.SELECT_COURSE_TAB]: CoreEventSelectCourseTabData; | ||||||
|     [CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData; |     [CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData; | ||||||
|     [CoreEvents.MANUAL_COMPLETION_CHANGED]: CoreEventManualCompletionChangedData; |     [CoreEvents.MANUAL_COMPLETION_CHANGED]: CoreEventManualCompletionChangedData; | ||||||
|  |     [CoreEvents.COMPLETION_CHANGED]: CoreEventCompletionChangedData; | ||||||
|     [CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData; |     [CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData; | ||||||
|     [CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData; |     [CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData; | ||||||
|     [CoreEvents.IAB_LOAD_START]: InAppBrowserEvent; |     [CoreEvents.IAB_LOAD_START]: InAppBrowserEvent; | ||||||
| @ -77,7 +79,11 @@ export class CoreEvents { | |||||||
|     static readonly SITE_UPDATED = 'site_updated'; |     static readonly SITE_UPDATED = 'site_updated'; | ||||||
|     static readonly SITE_DELETED = 'site_deleted'; |     static readonly SITE_DELETED = 'site_deleted'; | ||||||
|     static readonly COMPLETION_MODULE_VIEWED = 'completion_module_viewed'; |     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 MANUAL_COMPLETION_CHANGED = 'manual_completion_changed'; | ||||||
|  |     static readonly COMPLETION_CHANGED = 'completion_changed'; | ||||||
|     static readonly USER_DELETED = 'user_deleted'; |     static readonly USER_DELETED = 'user_deleted'; | ||||||
|     static readonly PACKAGE_STATUS_CHANGED = 'package_status_changed'; |     static readonly PACKAGE_STATUS_CHANGED = 'package_status_changed'; | ||||||
|     static readonly COURSE_STATUS_CHANGED = 'course_status_changed'; |     static readonly COURSE_STATUS_CHANGED = 'course_status_changed'; | ||||||
| @ -347,6 +353,14 @@ export type CoreEventManualCompletionChangedData = { | |||||||
|     completion: CoreCourseModuleCompletionData; |     completion: CoreCourseModuleCompletionData; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Data passed to COMPLETION_CHANGED event. | ||||||
|  |  */ | ||||||
|  | export type CoreEventCompletionChangedData = { | ||||||
|  |     completion: CoreCourseModuleCompletionData; | ||||||
|  |     type: CoreCourseCompletionType; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Data passed to SECTION_STATUS_CHANGED event. |  * Data passed to SECTION_STATUS_CHANGED event. | ||||||
|  */ |  */ | ||||||
|  | |||||||
| @ -382,17 +382,21 @@ ion-alert.core-nohead { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ion-alert .alert-wrapper { | ion-alert { | ||||||
|     overflow: auto; |     --border-radius: var(--huge-radius); | ||||||
| } |     .alert-wrapper { | ||||||
|  |         overflow: auto; | ||||||
|  |         border-radius: var(--border-radius) !important; | ||||||
| 
 | 
 | ||||||
| ion-alert .alert-message { |         button.alert-button { | ||||||
|     user-select: text; |             color: var(--primary); | ||||||
|     flex-shrink: 0; |         } | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| ion-alert .alert-wrapper button.alert-button { |     .alert-message { | ||||||
|     color: var(--primary); |         user-select: text; | ||||||
|  |         flex-shrink: 0; | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Ionic list. | // Ionic list. | ||||||
| @ -844,6 +848,8 @@ ion-select-popover ion-item.core-select-option-title { | |||||||
| 
 | 
 | ||||||
| ion-chip { | ion-chip { | ||||||
|     line-height: 1.1; |     line-height: 1.1; | ||||||
|  |     border-radius: var(--medium-radius); | ||||||
|  |     @include padding-horizontal(16px); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ion-searchbar { | 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 { | ion-item-divider { | ||||||
|     --inner-padding-end: 0px; |     --inner-padding-end: 8px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Change default outline. | // Change default outline. | ||||||
| @ -1225,6 +1230,13 @@ ion-grid.core-no-grid > ion-row { | |||||||
|     display: block; |     display: block; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .core-underheader { | ||||||
|  |     position: absolute; | ||||||
|  |     top: 0; | ||||||
|  |     left: 0; | ||||||
|  |     right: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| ion-header[collapsible] { | ion-header[collapsible] { | ||||||
|     @include core-transition(all, 500ms); |     @include core-transition(all, 500ms); | ||||||
| 
 | 
 | ||||||
| @ -1247,7 +1259,14 @@ ion-header[collapsible] { | |||||||
| 
 | 
 | ||||||
| .collapsible-title { | .collapsible-title { | ||||||
|     overflow: visible; |     overflow: visible; | ||||||
|     *, h1, h2, .subheading { |     --inner-padding-top: 0px; | ||||||
|  |     --padding-top: 0px; | ||||||
|  | 
 | ||||||
|  |     ion-label { | ||||||
|  |         margin-top: 0px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     *, h1 { | ||||||
|         @include core-transition(all, 200ms, linear); |         @include core-transition(all, 200ms, linear); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -1255,9 +1274,12 @@ ion-header[collapsible] { | |||||||
|         overflow: visible !important; |         overflow: visible !important; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     h1, h2, .subheading { |     h1 { | ||||||
|         --max-width: none; |         --max-width: none; | ||||||
|     } |     } | ||||||
|  |     h1.cloned { | ||||||
|  |         opacity: 0 !important; | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ion-app.ios .collapsible-title h1 { | ion-app.ios .collapsible-title h1 { | ||||||
| @ -1269,7 +1291,7 @@ ion-app.md .collapsible-title h1 { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .collapsible-title.collapsible-title-collapsed { | .collapsible-title.collapsible-title-collapsed { | ||||||
|     ion-label, h1, h2, ion-row, ion-col, .subheading { |     ion-label, h1, ion-row, ion-col { | ||||||
|         opacity: 0; |         opacity: 0; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -1279,13 +1301,13 @@ ion-app.md .collapsible-title h1 { | |||||||
|         opacity: var(--collapse-opacity, 0); |         opacity: var(--collapse-opacity, 0); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     ion-label, h1, h2, ion-row, ion-col, .subheading { |     ion-label, h1, ion-row, ion-col, .subheading { | ||||||
|         opacity: 1; |         opacity: 1; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .collapsible-title.collapsible-title-collapse-nowrap { | .collapsible-title.collapsible-title-collapse-nowrap { | ||||||
|     h1, h2, .subheading { |     h1:not(.cloned) { | ||||||
|         max-width: var(--max-width); |         max-width: var(--max-width); | ||||||
|         white-space: nowrap; |         white-space: nowrap; | ||||||
|         overflow: hidden; |         overflow: hidden; | ||||||
|  | |||||||
| @ -105,7 +105,9 @@ | |||||||
|     --subdued-text-color: #595959; |     --subdued-text-color: #595959; | ||||||
| 
 | 
 | ||||||
|     --small-radius: 4px; |     --small-radius: 4px; | ||||||
|     --big-radius: 8px; |     --medium-radius: 8px; | ||||||
|  |     --big-radius: 16px; | ||||||
|  |     --huge-radius: 24px; | ||||||
| 
 | 
 | ||||||
|     --ion-card-color: var(--text-color); |     --ion-card-color: var(--text-color); | ||||||
|     ion-card { |     ion-card { | ||||||
| @ -113,7 +115,7 @@ | |||||||
|         --border-style: solid; |         --border-style: solid; | ||||||
|         --border-color: var(--stroke); |         --border-color: var(--stroke); | ||||||
|         --box-shadow: none; |         --box-shadow: none; | ||||||
|         --border-radius: var(--big-radius); |         --border-radius: var(--medium-radius); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     --text-hightlight-background-color: #{$core-text-hightlight-background-color}; |     --text-hightlight-background-color: #{$core-text-hightlight-background-color}; | ||||||
| @ -181,7 +183,7 @@ | |||||||
|             height: var(--a11y-min-target-size); |             height: var(--a11y-min-target-size); | ||||||
|             border: 1px solid var(--stroke); |             border: 1px solid var(--stroke); | ||||||
|             box-shadow: none; |             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-color: var(--gray-900); | ||||||
|     --core-combobox-border-color: var(--stroke); |     --core-combobox-border-color: var(--stroke); | ||||||
|     --core-combobox-border-width: 1px; |     --core-combobox-border-width: 1px; | ||||||
|     --core-combobox-radius: var(--big-radius); |     --core-combobox-radius: var(--medium-radius); | ||||||
|     --core-combobox-box-shadow: none; |     --core-combobox-box-shadow: none; | ||||||
| 
 | 
 | ||||||
|     --selected-item-color: var(--primary); |     --selected-item-color: var(--primary); | ||||||
| @ -301,7 +303,7 @@ | |||||||
|     --core-send-message-input-background: var(--gray-200); |     --core-send-message-input-background: var(--gray-200); | ||||||
|     --core-send-message-input-color: var(--gray-900); |     --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-max-height: 56px; | ||||||
|     --core-course-module-navigation-background: var(--contrast-background); |     --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 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. |     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. |     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 === | === 3.9.5 === | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user