diff --git a/scripts/langindex.json b/scripts/langindex.json
index 6d97093db..70e2ef078 100644
--- a/scripts/langindex.json
+++ b/scripts/langindex.json
@@ -1536,19 +1536,23 @@
"core.course.confirmpartialdownloadsize": "local_moodlemobileapp",
"core.course.couldnotloadsectioncontent": "local_moodlemobileapp",
"core.course.couldnotloadsections": "local_moodlemobileapp",
+ "core.course.courseindex": "courseformat",
"core.course.coursesummary": "moodle",
+ "core.course.done": "completion",
"core.course.downloadcourse": "tool_mobile",
"core.course.downloadcoursesprogressdescription": "local_moodlemobileapp",
"core.course.downloadsectionprogressdescription": "local_moodlemobileapp",
"core.course.errordownloadingcourse": "local_moodlemobileapp",
"core.course.errordownloadingsection": "local_moodlemobileapp",
"core.course.errorgetmodule": "local_moodlemobileapp",
+ "core.course.failed": "completion",
"core.course.gotonextactivity": "local_moodlemobileapp",
"core.course.gotonextactivitynotfound": "local_moodlemobileapp",
"core.course.gotopreviousactivity": "local_moodlemobileapp",
"core.course.gotopreviousactivitynotfound": "local_moodlemobileapp",
"core.course.hiddenfromstudents": "moodle",
"core.course.hiddenoncoursepage": "moodle",
+ "core.course.highlighted": "moodle",
"core.course.insufficientavailablequota": "local_moodlemobileapp",
"core.course.insufficientavailablespace": "local_moodlemobileapp",
"core.course.manualcompletionnotsynced": "local_moodlemobileapp",
@@ -1557,7 +1561,8 @@
"core.course.overriddennotice": "grades",
"core.course.refreshcourse": "local_moodlemobileapp",
"core.course.section": "moodle",
- "core.course.sections": "moodle",
+ "core.course.thisweek": "format_weeks/currentsection",
+ "core.course.todo": "completion",
"core.course.useactivityonbrowser": "local_moodlemobileapp",
"core.course.warningmanualcompletionmodified": "local_moodlemobileapp",
"core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp",
@@ -2257,6 +2262,7 @@
"core.strftimetime24": "langconfig",
"core.submit": "moodle",
"core.success": "moodle",
+ "core.summary": "moodle",
"core.tablet": "local_moodlemobileapp",
"core.tag.defautltagcoll": "tag",
"core.tag.errorareanotsupported": "local_moodlemobileapp",
diff --git a/src/addons/mod/resource/services/handlers/module.ts b/src/addons/mod/resource/services/handlers/module.ts
index a889ac9ed..0eaab6861 100644
--- a/src/addons/mod/resource/services/handlers/module.ts
+++ b/src/addons/mod/resource/services/handlers/module.ts
@@ -95,7 +95,7 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase
this.getResourceData(module, courseId, handlerData).then((extra) => {
handlerData.extraBadge = extra;
- handlerData.extraBadgeColor = 'light';
+ handlerData.extraBadgeColor = '';
return;
}).catch(() => {
diff --git a/src/core/classes/page-transition.ts b/src/core/classes/page-transition.ts
index 4ae2d83c9..4260515e0 100644
--- a/src/core/classes/page-transition.ts
+++ b/src/core/classes/page-transition.ts
@@ -93,6 +93,7 @@ export const moodleTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp
}
rootAnimation.addAnimation(enteringContentAnimation);
+ enteringContentAnimation.beforeAddClass('animating').afterRemoveClass('animating');
if (backDirection) {
enteringContentAnimation
@@ -214,6 +215,8 @@ export const moodleTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp
// setup leaving view
if (leavingEl) {
const leavingContent = createAnimation();
+ leavingContent.beforeAddClass('animating').afterRemoveClass('animating');
+
const leavingContentEl = leavingEl.querySelector(':scope > ion-content');
const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar');
const leavingHeaderEls = leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *');
diff --git a/src/core/components/tabs-outlet/tabs-outlet.ts b/src/core/components/tabs-outlet/tabs-outlet.ts
index 54da61022..d7575f2fe 100644
--- a/src/core/components/tabs-outlet/tabs-outlet.ts
+++ b/src/core/components/tabs-outlet/tabs-outlet.ts
@@ -62,7 +62,7 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent {
+ this.stackEventsSubscription = this.ionTabs.outlet.stackEvents.subscribe(async (stackEvent: StackEvent) => {
if (!this.isCurrentView) {
return;
}
+ // Add tabid to the tab content element.
+ if (stackEvent.enteringView.element.id == '') {
+ const tab = this.tabs.find((tab) => tab.page == stackEvent.enteringView.url);
+ stackEvent.enteringView.element.id = tab?.id || '';
+ }
+
this.showHideNavBarButtons(stackEvent.enteringView.element.tagName);
await this.listenContentScroll(stackEvent.enteringView.element, stackEvent.enteringView.id);
@@ -111,8 +117,8 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent {
- this.lastActiveComponent = this.ionTabs?.outlet.component;
+ this.outletActivatedSubscription = this.ionTabs.outlet.activateEvents.subscribe(() => {
+ this.lastActiveComponent = this.ionTabs.outlet.component;
});
}
@@ -140,8 +146,8 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent).ionViewDidEnter?.();
+ if (this.existsInNavigationStack && this.ionTabs.outlet.isActivated) {
+ (this.ionTabs.outlet.component as Partial).ionViewDidEnter?.();
}
// After the view has entered for the first time, we can assume that it'll always be in the navigation stack
@@ -180,10 +186,9 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent {
- const instance = domUtils.getInstanceByElement(element);
+ const instance = CoreDomUtils.getInstanceByElement(element);
if (instance) {
const pagetagName = element.closest('.ion-page')?.tagName;
diff --git a/src/core/directives/collapsible-header.ts b/src/core/directives/collapsible-header.ts
index 24ba81a03..e0927de10 100644
--- a/src/core/directives/collapsible-header.ts
+++ b/src/core/directives/collapsible-header.ts
@@ -43,6 +43,10 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
protected headerSubHeadingFontSize = 0;
protected contentSubHeadingFontSize = 0;
protected subHeadingStartDifference = 0;
+ protected inContent = true;
+ protected title?: HTMLElement | null;
+ protected titleHeight = 0;
+ protected contentH1?: HTMLElement | null;
constructor(el: ElementRef) {
this.header = el.nativeElement;
@@ -67,8 +71,6 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
/**
* Gets the loading content id to wait for the loading to finish.
*
- * @TODO: If no core-loading is present, load directly. Take into account content needs to be initialized.
- *
* @return Promise resolved with Loading Id, if any.
*/
protected async getLoadingId(): Promise {
@@ -80,6 +82,17 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
return;
}
+
+ const title = this.header.parentElement?.querySelector('.collapsible-title') || null;
+
+ if (title) {
+ // Title already found, no need to wait for loading.
+ this.loadingObserver.off();
+ this.setupRealTitle();
+
+ return;
+ }
+
}
return this.content.querySelector('core-loading.core-loading-loaded:not(.core-loading-inline) .core-loading-content')?.id;
@@ -89,6 +102,7 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
* Call this function when header is not collapsible.
*/
protected cannotCollapse(): void {
+ this.content = undefined;
this.loadingObserver.off();
this.header.classList.add('core-header-collapsed');
}
@@ -112,16 +126,25 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
await animation.finished;
}));
- const title = this.content.querySelector('.collapsible-title, h1');
- const contentH1 = this.content.querySelector('h1');
+ let title = this.content.querySelector('.collapsible-title');
+ if (!title) {
+ // Title is outside the ion-content.
+ title = this.header.parentElement?.querySelector('.collapsible-title') || null;
+ this.inContent = false;
+ }
+ this.contentH1 = title?.querySelector('h1');
const headerH1 = this.header.querySelector('h1');
- if (!title || !contentH1 || !headerH1) {
+ if (!title || !this.contentH1 || !headerH1 || !this.contentH1.parentElement) {
this.cannotCollapse();
return;
}
- this.titleTopDifference = contentH1.getBoundingClientRect().top - headerH1.getBoundingClientRect().top;
+ this.title = title;
+ this.titleHeight = title.getBoundingClientRect().height;
+
+ this.titleTopDifference = this.contentH1.getBoundingClientRect().top - headerH1.getBoundingClientRect().top;
+
if (this.titleTopDifference <= 0) {
this.cannotCollapse();
@@ -141,17 +164,17 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
}
const headerH1Styles = getComputedStyle(headerH1);
- const contentH1Styles = getComputedStyle(contentH1);
+ const contentH1Styles = getComputedStyle(this.contentH1);
if (Platform.isRTL) {
// Checking position over parent because transition may not be finished.
- const contentH1Position = contentH1.getBoundingClientRect().right - this.content.getBoundingClientRect().right;
+ const contentH1Position = this.contentH1.getBoundingClientRect().right - this.content.getBoundingClientRect().right;
const headerH1Position = headerH1.getBoundingClientRect().right - this.header.getBoundingClientRect().right;
this.h1StartDifference = Math.round(contentH1Position - (headerH1Position - parseFloat(headerH1Styles.paddingRight)));
} else {
// Checking position over parent because transition may not be finished.
- const contentH1Position = contentH1.getBoundingClientRect().left - this.content.getBoundingClientRect().left;
+ const contentH1Position = this.contentH1.getBoundingClientRect().left - this.content.getBoundingClientRect().left;
const headerH1Position = headerH1.getBoundingClientRect().left - this.header.getBoundingClientRect().left;
this.h1StartDifference = Math.round(contentH1Position - (headerH1Position + parseFloat(headerH1Styles.paddingLeft)));
@@ -165,10 +188,10 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
if (styleName != 'font-size' &&
styleName != 'font-family' &&
(styleName.startsWith('font-') || styleName.startsWith('letter-'))) {
- contentH1.style.setProperty(styleName, headerH1Styles.getPropertyValue(styleName));
+ this.contentH1?.style.setProperty(styleName, headerH1Styles.getPropertyValue(styleName));
}
});
- contentH1.style.setProperty(
+ this.contentH1.style.setProperty(
'--max-width',
(parseFloat(headerH1Styles.width)
-parseFloat(headerH1Styles.paddingLeft)
@@ -176,47 +199,70 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
+'px'),
);
- contentH1.setAttribute('aria-hidden', 'true');
+ this.contentH1.setAttribute('aria-hidden', 'true');
+
+ // Clone element to let the other elements be static.
+ const contentH1Clone = this.contentH1.cloneNode(true) as HTMLElement;
+ contentH1Clone.classList.add('cloned');
+ this.contentH1.parentElement.insertBefore(contentH1Clone, this.contentH1);
+ this.contentH1.style.setProperty(
+ 'top',
+ (contentH1Clone.getBoundingClientRect().top -
+ this.contentH1.parentElement.getBoundingClientRect().top +
+ parseInt(getComputedStyle(this.contentH1.parentElement).marginTop || '0', 10)) + 'px',
+ );
+ this.contentH1.style.setProperty('position', 'absolute');
+
+ this.setupContent();
+ }
+
+ /**
+ * Setup content scroll.
+ *
+ * @param parentId Parent id to recalculate content
+ * @param retries Retries to find content in case it's loading.
+ */
+ async setupContent(parentId?: string, retries = 5): Promise {
+ if (parentId) {
+ this.content = this.header.parentElement?.querySelector(`#${parentId} ion-content:not(.disable-scroll-y)`);
+ this.inContent = false;
+ if (!this.content && retries > 0) {
+ await CoreUtils.nextTick();
+ await this.setupContent(parentId, --retries);
+
+ return;
+ }
+
+ this.onScroll(this.content?.scrollTop || 0);
+ }
+
+ if (!this.title || !this.content) {
+ return;
+ }
// Add something under the hood to change the page background.
- let color = getComputedStyle(title).getPropertyValue('backgroundColor').trim();
+ let color = getComputedStyle(this.title).getPropertyValue('backgroundColor').trim();
if (color == '') {
- color = getComputedStyle(title).getPropertyValue('--background').trim();
+ color = getComputedStyle(this.title).getPropertyValue('--background').trim();
}
const underHeader = document.createElement('div');
underHeader.classList.add('core-underheader');
underHeader.style.setProperty('height', this.header.clientHeight + 'px');
underHeader.style.setProperty('background', color);
- this.content.shadowRoot?.querySelector('#background-content')?.prepend(underHeader);
-
- this.content.style.setProperty('--offset-top', this.header.clientHeight + 'px');
-
- // Subheading.
- const headerSubHeading = this.header.querySelector('h2,.subheading');
- const contentSubHeading = title.querySelector('h2,.subheading');
- if (headerSubHeading && contentSubHeading) {
- const headerSubHeadingStyles = getComputedStyle(headerSubHeading);
- this.headerSubHeadingFontSize = parseFloat(headerSubHeadingStyles.fontSize);
-
- const contentSubHeadingStyles = getComputedStyle(contentSubHeading);
- this.contentSubHeadingFontSize = parseFloat(contentSubHeadingStyles.fontSize);
-
- if (Platform.isRTL) {
- this.subHeadingStartDifference = contentSubHeading.getBoundingClientRect().right -
- (headerSubHeading.getBoundingClientRect().right - parseFloat(headerSubHeadingStyles.paddingRight));
- } else {
- this.subHeadingStartDifference = contentSubHeading.getBoundingClientRect().left -
- (headerSubHeading.getBoundingClientRect().left + parseFloat(headerSubHeadingStyles.paddingLeft));
+ if (this.inContent) {
+ this.content.shadowRoot?.querySelector('#background-content')?.prepend(underHeader);
+ this.content.style.setProperty('--offset-top', this.header.clientHeight + 'px');
+ } else {
+ if (!this.header.closest('.ion-page')?.querySelector('.core-underheader')) {
+ this.header.closest('.ion-page')?.insertBefore(underHeader, this.header);
}
-
- contentSubHeading.setAttribute('aria-hidden', 'true');
}
this.content.scrollEvents = true;
this.content.addEventListener('ionScroll', (e: CustomEvent): void => {
if (e.target == this.content) {
- this.onScroll(title, contentH1, contentSubHeading, e.detail);
+ this.onScroll(e.detail.scrollTop);
}
});
}
@@ -224,54 +270,45 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
/**
* On scroll function.
*
- * @param title Title on ion content.
- * @param contentH1 Heading 1 of title, if found.
- * @param scrollDetail Event details.
+ * @param scrollTop Scroll top measure.
*/
protected onScroll(
- title: HTMLElement,
- contentH1: HTMLElement,
- contentSubheading: HTMLElement | null,
- scrollDetail: ScrollDetail,
+ scrollTop: number,
): void {
- const progress = CoreMath.clamp(scrollDetail.scrollTop / this.titleTopDifference, 0, 1);
+ if (!this.title || !this.contentH1) {
+ return;
+ }
+
+ const progress = CoreMath.clamp(scrollTop / this.titleTopDifference, 0, 1);
const collapsed = progress >= 1;
+ if (!this.inContent) {
+ this.title.style.transform = 'translateY(-' + scrollTop + 'px)';
+ const height = this.titleHeight - scrollTop;
+ this.title.style.height = (height > 0 ? height : 0) + 'px';
+ }
+
// Check total collapse.
this.header.classList.toggle('core-header-collapsed', collapsed);
- title.classList.toggle('collapsible-title-collapsed', collapsed);
- title.classList.toggle('collapsible-title-collapse-started', scrollDetail.scrollTop > 0);
- title.classList.toggle('collapsible-title-collapse-nowrap', progress > 0.5);
- title.style.setProperty('--collapse-opacity', (1 - progress) +'');
+ this.title.classList.toggle('collapsible-title-collapsed', collapsed);
+ this.title.classList.toggle('collapsible-title-collapse-started', scrollTop > 0);
+ this.title.classList.toggle('collapsible-title-collapse-nowrap', progress > 0.5);
+ this.title.style.setProperty('--collapse-opacity', (1 - progress) +'');
if (collapsed) {
- contentH1.style.transform = 'translateX(-' + this.h1StartDifference + 'px)';
- contentH1.style.setProperty('font-size', this.headerH1FontSize + 'px');
-
- if (contentSubheading) {
- contentSubheading.style.transform = 'translateX(-' + this.subHeadingStartDifference + 'px)';
- contentSubheading.style.setProperty('font-size', this.headerSubHeadingFontSize + 'px');
- }
+ this.contentH1.style.transform = 'translateX(-' + this.h1StartDifference + 'px)';
+ this.contentH1.style.setProperty('font-size', this.headerH1FontSize + 'px');
return;
}
// Zoom font-size out.
const newFontSize = this.contentH1FontSize - ((this.contentH1FontSize - this.headerH1FontSize) * progress);
- contentH1.style.setProperty('font-size', newFontSize + 'px');
+ this.contentH1.style.setProperty('font-size', newFontSize + 'px');
// Move.
const newStart = - this.h1StartDifference * progress;
- contentH1.style.transform = 'translateX(' + newStart + 'px)';
-
- if (contentSubheading) {
- const newFontSize = this.contentSubHeadingFontSize -
- ((this.contentSubHeadingFontSize - this.headerSubHeadingFontSize) * progress);
- contentSubheading.style.setProperty('font-size', newFontSize + 'px');
-
- const newStart = - this.subHeadingStartDifference * progress;
- contentSubheading.style.transform = 'translateX(' + newStart + 'px)';
- }
+ this.contentH1.style.transform = 'translateX(' + newStart + 'px)';
}
/**
diff --git a/src/core/features/block/components/side-blocks-button/side-blocks-button.scss b/src/core/features/block/components/side-blocks-button/side-blocks-button.scss
index 579196d50..76b104a1a 100644
--- a/src/core/features/block/components/side-blocks-button/side-blocks-button.scss
+++ b/src/core/features/block/components/side-blocks-button/side-blocks-button.scss
@@ -4,6 +4,7 @@
@include position(50%, 0px, null, null);
position: fixed;
z-index: 10;
+ transform: translateY(-50%);
ion-button {
margin: 0;
diff --git a/src/core/features/course/components/components.module.ts b/src/core/features/course/components/components.module.ts
index 516a43111..739dc216e 100644
--- a/src/core/features/course/components/components.module.ts
+++ b/src/core/features/course/components/components.module.ts
@@ -20,7 +20,7 @@ import { CoreCourseFormatComponent } from './format/format';
import { CoreCourseModuleComponent } from './module/module';
import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion';
import { CoreCourseModuleDescriptionComponent } from './module-description/module-description';
-import { CoreCourseSectionSelectorComponent } from './section-selector/section-selector';
+import { CoreCourseCourseIndexComponent } from './course-index/course-index';
import { CoreCourseTagAreaComponent } from './tag-area/tag-area';
import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module';
import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy';
@@ -37,7 +37,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module-
CoreCourseModuleDescriptionComponent,
CoreCourseModuleInfoComponent,
CoreCourseModuleManualCompletionComponent,
- CoreCourseSectionSelectorComponent,
+ CoreCourseCourseIndexComponent,
CoreCourseTagAreaComponent,
CoreCourseUnsupportedModuleComponent,
CoreCourseModuleNavigationComponent,
@@ -54,7 +54,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module-
CoreCourseModuleDescriptionComponent,
CoreCourseModuleInfoComponent,
CoreCourseModuleManualCompletionComponent,
- CoreCourseSectionSelectorComponent,
+ CoreCourseCourseIndexComponent,
CoreCourseTagAreaComponent,
CoreCourseUnsupportedModuleComponent,
CoreCourseModuleNavigationComponent,
diff --git a/src/core/features/course/components/course-index/course-index.html b/src/core/features/course/components/course-index/course-index.html
new file mode 100644
index 000000000..7a97a6b41
--- /dev/null
+++ b/src/core/features/course/components/course-index/course-index.html
@@ -0,0 +1,79 @@
+
+
+
+ {{ 'core.course.courseindex' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{highlighted}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/core/features/course/components/course-index/course-index.scss b/src/core/features/course/components/course-index/course-index.scss
new file mode 100644
index 000000000..8ef120471
--- /dev/null
+++ b/src/core/features/course/components/course-index/course-index.scss
@@ -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;
+}
diff --git a/src/core/features/course/components/course-index/course-index.ts b/src/core/features/course/components/course-index/course-index.ts
new file mode 100644
index 000000000..712e87e37
--- /dev/null
+++ b/src/core/features/course/components/course-index/course-index.ts
@@ -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 {
+
+ 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 & {
+ highlighted?: boolean;
+ expanded?: boolean;
+ modules: (CoreCourseModuleData & {
+ completionStatus?: CoreCourseModuleCompletionStatus;
+ })[];
+};
+
+export type CoreCourseIndexSectionWithModule = {
+ event: Event;
+ section: CourseIndexSection;
+ module?: CoreCourseModuleData;
+};
diff --git a/src/core/features/course/components/format/core-course-format.html b/src/core/features/course/components/format/core-course-format.html
index 7e549a3c2..3c8036beb 100644
--- a/src/core/features/course/components/format/core-course-format.html
+++ b/src/core/features/course/components/format/core-course-format.html
@@ -1,64 +1,14 @@
-
+
-
-
-
-
-
-
-
- {{ 'core.course.sections' | translate }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ 'core.course.hiddenfromstudents' | translate }}
-
-
- {{ 'core.notavailable' | translate }}
-
-
-
-
-
-
-
-
-
@@ -84,63 +34,75 @@
+ *ngIf="displayCourseIndex && sections?.length">
-
+
-
+
-
-
-
+
+
+
+
+
+
+
+
+ {{'core.course.courseindex' | translate }}
+
+
+
-
-
+
-
-
+
+
-
-
+
+
+
{{ 'core.course.hiddenfromstudents' | translate }}
-
+
+
{{ 'core.notavailable' | translate }}
-
-
+
+
+
+ {{highlighted}}
-
+
+ [showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions">
diff --git a/src/core/features/course/components/format/format.scss b/src/core/features/course/components/format/format.scss
index e7d109381..7c33a30cc 100644
--- a/src/core/features/course/components/format/format.scss
+++ b/src/core/features/course/components/format/format.scss
@@ -1,70 +1,17 @@
-@import '~theme/globals.scss';
+.core-course-section-nav-buttons {
+ display: flex;
+ justify-content: flex-end;
-:host {
-
- .core-format-progress-list {
- margin-bottom: 0;
-
- .item {
- background: transparent;
-
- .label {
- margin-top: 0;
- margin-bottom: 0;
- }
- }
- }
-
- .core-course-thumb {
- display: none;
- height: var(--core-courseimage-on-course-height);
- width: 100%;
+ core-format-text {
+ white-space: nowrap;
+ text-overflow: ellipsis;
overflow: hidden;
- cursor: pointer;
- pointer-events: auto;
- position: relative;
- background: var(--ion-item-background);
- border-bottom: 1px solid var(--stroke);
-
- img {
- position: absolute;
- top: 0;
- bottom: 0;
- margin: auto;
- width: 100%;
- }
+ text-transform: none;
+ }
+}
+
+.course-section {
+ ion-badge {
+ text-align: start;
}
-
- @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;
- }
- }
-
}
diff --git a/src/core/features/course/components/format/format.ts b/src/core/features/course/components/format/format.ts
index 7d93a2084..25a3cd218 100644
--- a/src/core/features/course/components/format/format.ts
+++ b/src/core/features/course/components/format/format.ts
@@ -26,7 +26,6 @@ import {
Type,
ElementRef,
} from '@angular/core';
-import { ModalOptions } from '@ionic/core';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
@@ -44,8 +43,10 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { IonContent, IonRefresher } from '@ionic/angular';
import { CoreUtils } from '@services/utils/utils';
-import { CoreCourseSectionSelectorComponent } from '../section-selector/section-selector';
+import { CoreCourseCourseIndexComponent, CoreCourseIndexSectionWithModule } from '../course-index/course-index';
import { CoreBlockHelper } from '@features/block/services/block-helper';
+import { CoreNavigator } from '@services/navigator';
+import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
/**
* Component to display course contents using a certain format. If the format isn't found, use default one.
@@ -66,8 +67,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
static readonly LOAD_MORE_ACTIVITIES = 20; // How many activities should load each time showMoreActivities is called.
- @Input() course?: CoreCourseAnyCourseData; // The course to render.
- @Input() sections?: CoreCourseSection[]; // List of course sections.
+ @Input() course!: CoreCourseAnyCourseData; // The course to render.
+ @Input() sections: CoreCourseSectionToDisplay[] = []; // List of course sections.
@Input() initialSectionId?: number; // The section to load first (by ID).
@Input() initialSectionNumber?: number; // The section to load first (by number).
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
@@ -78,7 +79,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
// All the possible component classes.
courseFormatComponent?: Type;
courseSummaryComponent?: Type;
- sectionSelectorComponent?: Type;
singleSectionComponent?: Type;
allSectionsComponent?: Type;
@@ -86,7 +86,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
showSectionId = 0;
data: Record = {}; // Data to pass to the components.
- displaySectionSelector = false;
+ displayCourseIndex = false;
displayBlocks = false;
hasBlocks = false;
selectedSection?: CoreCourseSection;
@@ -95,17 +95,12 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID;
stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
loaded = false;
- hasSeveralSections?: boolean;
- imageThumb?: string;
progress?: number;
- sectionSelectorModalOptions: ModalOptions = {
- component: CoreCourseSectionSelectorComponent,
- componentProps: {},
- };
+ highlighted?: string;
protected selectTabObserver?: CoreEventObserver;
+ protected completionObserver?: CoreEventObserver;
protected lastCourseFormat?: string;
- protected sectionSelectorExpanded = false;
constructor(
protected content: IonContent,
@@ -116,9 +111,17 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
}
/**
- * Component being initialized.
+ * @inheritdoc
*/
ngOnInit(): void {
+ if (this.course === undefined) {
+ CoreDomUtils.showErrorModal('Course not set');
+
+ CoreNavigator.back();
+
+ return;
+ }
+
// Listen for select course tab events to select the right section if needed.
this.selectTabObserver = CoreEvents.on(CoreEvents.SELECT_COURSE_TAB, (data) => {
if (data.name) {
@@ -137,6 +140,37 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.sectionChanged(section);
}
});
+
+ // The completion of any of the modules have changed.
+ this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => {
+ if (data.completion.courseId != this.course.id) {
+ return;
+ }
+
+ // Emit a new event for other components.
+ this.completionChanged.emit(data.completion);
+
+ if (data.completion.valueused !== false || !this.course || !('progress' in this.course) ||
+ typeof this.course.progress != 'number') {
+ return;
+ }
+
+ // If the completion value is not used, the page won't be reloaded, so update the progress bar.
+ const completionModules = ( [])
+ .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 {
this.setInputData();
- this.sectionSelectorModalOptions.componentProps!.course = this.course;
- this.sectionSelectorModalOptions.componentProps!.sections = this.sections;
if (changes.course && this.course) {
// Course has changed, try to get the components.
this.getComponents();
- this.displaySectionSelector = CoreCourseFormatDelegate.displaySectionSelector(this.course);
+ this.displayCourseIndex = CoreCourseFormatDelegate.displayCourseIndex(this.course);
this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course);
this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id);
this.updateProgress();
-
- if ('overviewfiles' in this.course) {
- this.imageThumb = this.course.overviewfiles?.[0]?.fileurl;
- }
}
if (changes.sections && this.sections) {
- this.sectionSelectorModalOptions.componentProps!.sections = this.sections;
this.treatSections(this.sections);
}
}
@@ -192,10 +219,13 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
// Format has changed or it's the first time, load all the components.
this.lastCourseFormat = this.course.format;
+ this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
+ const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
+ currentSection.highlighted = true;
+
await Promise.all([
this.loadCourseFormatComponent(),
this.loadCourseSummaryComponent(),
- this.loadSectionSelectorComponent(),
this.loadSingleSectionComponent(),
this.loadAllSectionsComponent(),
]);
@@ -207,7 +237,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* @return Promise resolved when done.
*/
protected async loadCourseFormatComponent(): Promise {
- this.courseFormatComponent = await CoreCourseFormatDelegate.getCourseFormatComponent(this.course!);
+ this.courseFormatComponent = await CoreCourseFormatDelegate.getCourseFormatComponent(this.course);
}
/**
@@ -216,16 +246,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* @return Promise resolved when done.
*/
protected async loadCourseSummaryComponent(): Promise {
- this.courseSummaryComponent = await CoreCourseFormatDelegate.getCourseSummaryComponent(this.course!);
- }
-
- /**
- * Load section selector component.
- *
- * @return Promise resolved when done.
- */
- protected async loadSectionSelectorComponent(): Promise {
- this.sectionSelectorComponent = await CoreCourseFormatDelegate.getSectionSelectorComponent(this.course!);
+ this.courseSummaryComponent = await CoreCourseFormatDelegate.getCourseSummaryComponent(this.course);
}
/**
@@ -234,7 +255,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* @return Promise resolved when done.
*/
protected async loadSingleSectionComponent(): Promise {
- this.singleSectionComponent = await CoreCourseFormatDelegate.getSingleSectionComponent(this.course!);
+ this.singleSectionComponent = await CoreCourseFormatDelegate.getSingleSectionComponent(this.course);
}
/**
@@ -243,7 +264,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* @return Promise resolved when done.
*/
protected async loadAllSectionsComponent(): Promise {
- 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 {
const hasAllSections = sections[0].id == CoreCourseProvider.ALL_SECTIONS_ID;
- this.hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections);
+ const hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections);
if (this.selectedSection) {
+ const selectedSection = this.selectedSection;
// We have a selected section, but the list has changed. Search the section in the list.
- let newSection = sections.find(section => this.compareSections(section, this.selectedSection!));
+ let newSection = sections.find(section => this.compareSections(section, selectedSection));
if (!newSection) {
// Section not found, calculate which one to use.
- newSection = await CoreCourseFormatDelegate.getCurrentSection(this.course!, sections);
+ newSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
}
this.sectionChanged(newSection);
@@ -271,7 +293,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
}
// There is no selected section yet, calculate which one to load.
- if (!this.hasSeveralSections) {
+ if (!hasSeveralSections) {
// Always load "All sections" to display the section title. If it isn't there just load the section.
this.loaded = true;
this.sectionChanged(sections[0]);
@@ -289,7 +311,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
if (!this.loaded) {
// No section specified, not found or not visible, get current section.
- const section = await CoreCourseFormatDelegate.getCurrentSection(this.course!, sections);
+ const section = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
this.loaded = true;
this.sectionChanged(section);
@@ -299,20 +321,31 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
}
/**
- * Display the section selector modal.
+ * Display the course index modal.
*/
- async showSectionSelector(): Promise {
- if (this.sectionSelectorExpanded) {
- return;
- }
+ async openCourseIndex(): Promise {
+ const data = await CoreDomUtils.openModal({
+ component: CoreCourseCourseIndexComponent,
+ componentProps: {
+ course: this.course,
+ sections: this.sections,
+ selectedId: this.selectedSection?.id,
+ },
+ });
- this.sectionSelectorExpanded = true;
-
- const data = await CoreDomUtils.openModal(this.sectionSelectorModalOptions);
-
- this.sectionSelectorExpanded = false;
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 {
const previousValue = this.selectedSection;
this.selectedSection = newSection;
- this.sectionSelectorModalOptions.componentProps!.selected = this.selectedSection;
this.data.section = this.selectedSection;
if (newSection.id != this.allSectionsId) {
// Select next and previous sections to show the arrows.
- const i = this.sections!.findIndex((value) => this.compareSections(value, this.selectedSection!));
+ const i = this.sections.findIndex((value) => this.compareSections(value, newSection));
let j: number;
for (j = i - 1; j >= 1; j--) {
- if (this.canViewSection(this.sections![j])) {
+ if (this.canViewSection(this.sections[j])) {
break;
}
}
- this.previousSection = j >= 1 ? this.sections![j] : undefined;
+ this.previousSection = j >= 1 ? this.sections[j] : undefined;
- for (j = i + 1; j < this.sections!.length; j++) {
- if (this.canViewSection(this.sections![j])) {
+ for (j = i + 1; j < this.sections.length; j++) {
+ if (this.canViewSection(this.sections[j])) {
break;
}
}
- this.nextSection = j < this.sections!.length ? this.sections![j] : undefined;
+ this.nextSection = j < this.sections.length ? this.sections[j] : undefined;
} else {
this.previousSection = undefined;
this.nextSection = undefined;
@@ -368,7 +400,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
if (!previousValue || previousValue.id != newSection.id) {
// First load or section changed, add log in Moodle.
CoreUtils.ignoreErrors(
- CoreCourse.logView(this.course!.id, newSection.section, undefined, this.course!.fullname),
+ CoreCourse.logView(this.course.id, newSection.section, undefined, this.course.fullname),
);
}
@@ -475,7 +507,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
}
/**
- * Component destroyed.
+ * @inheritdoc
*/
ngOnDestroy(): void {
this.selectTabObserver && this.selectTabObserver.off();
@@ -510,35 +542,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
}
- /**
- * The completion of any of the modules have changed.
- */
- onCompletionChange(completionData: CoreCourseModuleCompletionData): void {
- // Emit a new event for other components.
- this.completionChanged.emit(completionData);
-
- if (completionData.valueused !== false || !this.course || !('progress' in this.course) ||
- typeof this.course.progress != 'number') {
- return;
- }
-
- // If the completion value is not used, the page won't be reloaded, so update the progress bar.
- const completionModules = ( [])
- .concat(...this.sections!.map((section) => section.modules))
- .map((module) => module.completion && module.completion > 0 ? 1 : module.completion)
- .reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0);
-
- const moduleProgressPercent = 100 / (completionModules || 1);
- // Use min/max here to avoid floating point rounding errors over/under-flowing the progress bar.
- if (completionData.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) {
- this.course.progress = Math.min(100, this.course.progress + moduleProgressPercent);
- } else {
- this.course.progress = Math.max(0, this.course.progress - moduleProgressPercent);
- }
-
- this.updateProgress();
- }
-
/**
* Update course progress.
*/
@@ -559,3 +562,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
}
}
+
+type CoreCourseSectionToDisplay = CoreCourseSection & {
+ highlighted?: boolean;
+};
diff --git a/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts b/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts
index 1b9e0b453..023967422 100644
--- a/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts
+++ b/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts
@@ -12,14 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component } from '@angular/core';
+import { Component, OnDestroy, OnInit } from '@angular/core';
import { CoreUser } from '@features/user/services/user';
-import { CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking } from '@features/course/services/course';
+import {
+ CoreCourseCompletionType,
+ CoreCourseModuleCompletionStatus,
+ CoreCourseModuleCompletionTracking,
+} from '@features/course/services/course';
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
import { Translate } from '@singletons';
import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion';
import { CoreCourseHelper } from '@features/course/services/course-helper';
+import { CoreEventObserver, CoreEvents } from '@singletons/events';
/**
* Component to handle activity completion in sites previous to 3.11.
@@ -35,11 +40,29 @@ import { CoreCourseHelper } from '@features/course/services/course-helper';
templateUrl: 'core-course-module-completion-legacy.html',
styleUrls: ['module-completion-legacy.scss'],
})
-export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleCompletionBaseComponent {
+export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleCompletionBaseComponent
+ implements OnInit, OnDestroy {
completionImage?: string;
completionDescription?: string;
+ protected completionObserver?: CoreEventObserver;
+
+ /**
+ * @inheritdoc
+ */
+ ngOnInit(): void {
+ this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => {
+ if (!this.completion || this.completion.cmid != data.completion.cmid && data.type != CoreCourseCompletionType.MANUAL) {
+ return;
+ }
+
+ this.completion = data.completion;
+ this.calculateData();
+ this.completionChanged.emit(this.completion);
+ });
+ }
+
/**
* @inheritdoc
*/
@@ -52,30 +75,28 @@ export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleC
let langKey: string | undefined;
let image: string | undefined;
- if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL &&
- this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) {
- image = 'completion-manual-n';
- langKey = 'core.completion-alt-manual-n';
- } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL &&
- this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) {
- image = 'completion-manual-y';
- langKey = 'core.completion-alt-manual-y';
- } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC &&
- this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) {
- image = 'completion-auto-n';
- langKey = 'core.completion-alt-auto-n';
- } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC &&
- this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) {
- image = 'completion-auto-y';
- langKey = 'core.completion-alt-auto-y';
- } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC &&
- this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) {
- image = 'completion-auto-pass';
- langKey = 'core.completion-alt-auto-pass';
- } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC &&
- this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL) {
- image = 'completion-auto-fail';
- langKey = 'core.completion-alt-auto-fail';
+ if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL) {
+ if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) {
+ image = 'completion-manual-n';
+ langKey = 'core.completion-alt-manual-n';
+ } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) {
+ image = 'completion-manual-y';
+ langKey = 'core.completion-alt-manual-y';
+ }
+ } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC) {
+ if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) {
+ image = 'completion-auto-n';
+ langKey = 'core.completion-alt-auto-n';
+ } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) {
+ image = 'completion-auto-y';
+ langKey = 'core.completion-alt-auto-y';
+ } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) {
+ image = 'completion-auto-pass';
+ langKey = 'core.completion-alt-auto-pass';
+ } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL) {
+ image = 'completion-auto-fail';
+ langKey = 'core.completion-alt-auto-fail';
+ }
}
if (image) {
@@ -128,9 +149,16 @@ export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleC
await CoreCourseHelper.changeManualCompletion(this.completion, event);
- this.calculateData();
+ // @deprecated MANUAL_COMPLETION_CHANGED is deprecated since 4.0 use COMPLETION_CHANGED instead.
+ CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion });
+ CoreEvents.trigger(CoreEvents.COMPLETION_CHANGED, { completion: this.completion, type: CoreCourseCompletionType.MANUAL });
+ }
- this.completionChanged.emit(this.completion);
+ /**
+ * @inheritdoc
+ */
+ ngOnDestroy(): void {
+ this.completionObserver?.off();
}
}
diff --git a/src/core/features/course/components/module-completion/core-course-module-completion.html b/src/core/features/course/components/module-completion/core-course-module-completion.html
index 571e9f15a..dbdc18616 100644
--- a/src/core/features/course/components/module-completion/core-course-module-completion.html
+++ b/src/core/features/course/components/module-completion/core-course-module-completion.html
@@ -4,16 +4,19 @@
+
{{ 'core.course.completion_automatic:done' | translate }} {{ rule.rulevalue.description }}
+
{{ 'core.course.completion_automatic:failed' | translate }} {{ rule.rulevalue.description }}
-
+
+
{{ 'core.course.completion_automatic:todo' | translate }} {{ rule.rulevalue.description }}
diff --git a/src/core/features/course/components/module-info/core-course-module-info.html b/src/core/features/course/components/module-info/core-course-module-info.html
index 88060b6ca..864dd98a5 100644
--- a/src/core/features/course/components/module-info/core-course-module-info.html
+++ b/src/core/features/course/components/module-info/core-course-module-info.html
@@ -19,12 +19,21 @@
-
-
+
+
-
- {{ date.label }} {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }}
-
+
+
+
+ {{ date.label }} {{ date.timestamp * 1000 |
+ coreFormatDate:'strftimedatetime' }}
+
+
+
+
+
diff --git a/src/core/features/course/components/module-info/course-module-info.scss b/src/core/features/course/components/module-info/course-module-info.scss
index 514315b48..f540001d2 100644
--- a/src/core/features/course/components/module-info/course-module-info.scss
+++ b/src/core/features/course/components/module-info/course-module-info.scss
@@ -16,4 +16,9 @@
core-mod-icon {
align-self: flex-start;
}
+
+ .core-module-dates ion-icon {
+ @include margin-horizontal(null, 8px);
+ }
+
}
diff --git a/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html b/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html
index d205a5a65..8f8e5d6e3 100644
--- a/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html
+++ b/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html
@@ -2,22 +2,20 @@
-
+
{{ 'core.course.completion_manual:done' | translate }}
-
+
{{ 'core.course.completion_manual:markdone' | translate }}
-
+
{{ 'core.course.completion_manual:markdone' | translate }}
diff --git a/src/core/features/course/components/module-manual-completion/module-manual-completion.ts b/src/core/features/course/components/module-manual-completion/module-manual-completion.ts
index ede613119..926349adf 100644
--- a/src/core/features/course/components/module-manual-completion/module-manual-completion.ts
+++ b/src/core/features/course/components/module-manual-completion/module-manual-completion.ts
@@ -13,6 +13,7 @@
// limitations under the License.
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange } from '@angular/core';
+import { CoreCourseCompletionType } from '@features/course/services/course';
import { CoreCourseHelper, CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
import { CoreUser } from '@features/user/services/user';
@@ -34,14 +35,14 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan
accessibleDescription: string | null = null;
- protected manualChangedObserver?: CoreEventObserver;
+ protected completionObserver?: CoreEventObserver;
/**
* @inheritdoc
*/
ngOnInit(): void {
- this.manualChangedObserver = CoreEvents.on(CoreEvents.MANUAL_COMPLETION_CHANGED, (data) => {
- if (!this.completion || this.completion.cmid != data.completion.cmid) {
+ this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => {
+ if (!this.completion || this.completion.cmid != data.completion.cmid && data.type != CoreCourseCompletionType.MANUAL) {
return;
}
@@ -98,14 +99,16 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan
await CoreCourseHelper.changeManualCompletion(this.completion, event);
+ // @deprecated MANUAL_COMPLETION_CHANGED is deprecated since 4.0 use COMPLETION_CHANGED instead.
CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion });
+ CoreEvents.trigger(CoreEvents.COMPLETION_CHANGED, { completion: this.completion, type: CoreCourseCompletionType.MANUAL });
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
- this.manualChangedObserver?.off();
+ this.completionObserver?.off();
}
}
diff --git a/src/core/features/course/components/module-navigation/core-course-module-navigation.html b/src/core/features/course/components/module-navigation/core-course-module-navigation.html
index 7d0fde92d..497b0e42e 100644
--- a/src/core/features/course/components/module-navigation/core-course-module-navigation.html
+++ b/src/core/features/course/components/module-navigation/core-course-module-navigation.html
@@ -6,12 +6,10 @@
-
+
+ [moduleId]="currentModule.id" [showManualCompletion]="true" (completionChanged)="completionChanged.emit($event)">
diff --git a/src/core/features/course/components/module/core-course-module.html b/src/core/features/course/components/module/core-course-module.html
index 1f5af1171..adfa73f2a 100644
--- a/src/core/features/course/components/module/core-course-module.html
+++ b/src/core/features/course/components/module/core-course-module.html
@@ -18,25 +18,26 @@
[courseId]="module.course" [attr.aria-label]="module.handlerData.a11yTitle + ', ' + modNameTranslated">
-
-
-
-
- {{ 'core.course.hiddenfromstudents' | translate }}
-
-
- {{ 'core.course.hiddenoncoursepage' | translate }}
-
-
- {{ 'core.restricted' | translate }}
-
-
-
-
- {{ 'core.course.manualcompletionnotsynced' | translate }}
-
+
+
+
+ {{ 'core.course.todo' | translate }}
+
+
+
+
+ {{'core.course.done' | translate }}
+
+
+
+ {{'core.course.failed' | translate }}
+
+
+
+
+
+
+
+
- {{ date.label }} {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }}
+ {{ date.label }} {{ date.timestamp *
+ 1000 | coreFormatDate:'strftimedatetime' }}
@@ -76,9 +82,31 @@
[showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
-
-
+
+
+ {{ 'core.course.manualcompletionnotsynced' | translate }}
+
+
+
+
+
+ {{ 'core.course.hiddenfromstudents' | translate }}
+
+
+
+
+ {{ 'core.course.hiddenoncoursepage' | translate }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/core/features/course/components/module/module.scss b/src/core/features/course/components/module/module.scss
index 2253562e5..faf96e525 100644
--- a/src/core/features/course/components/module/module.scss
+++ b/src/core/features/course/components/module/module.scss
@@ -1,3 +1,5 @@
+@import "~theme/globals";
+
:host {
.item.core-module-main-item {
@@ -59,4 +61,9 @@
--inner-border-width: 0px;
}
+ .core-module-availabilityinfo ion-icon,
+ .core-module-dates ion-icon {
+ @include margin-horizontal(null, 8px);
+ }
+
}
diff --git a/src/core/features/course/components/module/module.ts b/src/core/features/course/components/module/module.ts
index 936901f5f..287c2a484 100644
--- a/src/core/features/course/components/module/module.ts
+++ b/src/core/features/course/components/module/module.ts
@@ -20,7 +20,7 @@ import {
CoreCourseModuleCompletionData,
CoreCourseSection,
} from '@features/course/services/course-helper';
-import { CoreCourse } from '@features/course/services/course';
+import { CoreCourse, CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking } from '@features/course/services/course';
import { CoreCourseModuleDelegate, CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate';
/**
@@ -47,6 +47,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
hasInfo = false;
showLegacyCompletion = false; // Whether to show module completion in the old format.
showManualCompletion = false; // Whether to show manual completion when completion conditions are disabled.
+ completionStatus?: CoreCourseModuleCompletionStatus;
/**
* Component being initialized.
@@ -61,13 +62,22 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
}
this.module.handlerData.a11yTitle = this.module.handlerData.a11yTitle ?? this.module.handlerData.title;
+ this.completionStatus = this.module.completiondata === undefined ||
+ this.module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE
+ ? undefined
+ : this.module.completiondata.state;
+
this.hasInfo = !!(
this.module.description ||
(this.showActivityDates && this.module.dates && this.module.dates.length) ||
(this.module.completiondata &&
((this.showManualCompletion && !this.module.completiondata.isautomatic) ||
(this.showCompletionConditions && this.module.completiondata.isautomatic))
- )
+ ) ||
+ this.module.completiondata?.offline ||
+ (this.module.visible === 0 && (!this.section || this.section.visible)) ||
+ (this.module.visible !== 0 && this.module.isStealth) ||
+ (this.module.availabilityinfo)
);
}
diff --git a/src/core/features/course/components/section-selector/section-selector.html b/src/core/features/course/components/section-selector/section-selector.html
deleted file mode 100644
index 82b2bf8b0..000000000
--- a/src/core/features/course/components/section-selector/section-selector.html
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
- {{ 'core.course.sections' | translate }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- = 0" [progress]="section.progress"
- a11yText="core.course.aria:sectionprogress">
-
-
-
- {{ 'core.course.hiddenfromstudents' | translate }}
-
-
- {{ 'core.notavailable' | translate }}
-
-
-
-
-
-
-
-
-
-
diff --git a/src/core/features/course/components/section-selector/section-selector.scss b/src/core/features/course/components/section-selector/section-selector.scss
deleted file mode 100644
index 828f42727..000000000
--- a/src/core/features/course/components/section-selector/section-selector.scss
+++ /dev/null
@@ -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;
-}
diff --git a/src/core/features/course/components/section-selector/section-selector.ts b/src/core/features/course/components/section-selector/section-selector.ts
deleted file mode 100644
index e71790478..000000000
--- a/src/core/features/course/components/section-selector/section-selector.ts
+++ /dev/null
@@ -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;
-};
diff --git a/src/core/features/course/format/singleactivity/components/single-activity.scss b/src/core/features/course/format/singleactivity/components/single-activity.scss
new file mode 100644
index 000000000..1352a5254
--- /dev/null
+++ b/src/core/features/course/format/singleactivity/components/single-activity.scss
@@ -0,0 +1,3 @@
+:host ::ng-deep .collapsible-title {
+ display: none;
+}
diff --git a/src/core/features/course/format/singleactivity/components/singleactivity.ts b/src/core/features/course/format/singleactivity/components/singleactivity.ts
index 01da69058..499d7f805 100644
--- a/src/core/features/course/format/singleactivity/components/singleactivity.ts
+++ b/src/core/features/course/format/singleactivity/components/singleactivity.ts
@@ -19,7 +19,7 @@ import { CoreCourseUnsupportedModuleComponent } from '@features/course/component
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import { IonRefresher } from '@ionic/angular';
-import { CoreCourseModuleCompletionData, CoreCourseSectionWithStatus } from '@features/course/services/course-helper';
+import { CoreCourseModuleCompletionData, CoreCourseSection } from '@features/course/services/course-helper';
import { CoreBlockHelper } from '@features/block/services/block-helper';
import { CoreCourse } from '@features/course/services/course';
@@ -31,11 +31,12 @@ import { CoreCourse } from '@features/course/services/course';
@Component({
selector: 'core-course-format-single-activity',
templateUrl: 'core-course-format-single-activity.html',
+ styleUrls: ['single-activity.scss'],
})
export class CoreCourseFormatSingleActivityComponent implements OnChanges {
@Input() course?: CoreCourseAnyCourseData; // The course to render.
- @Input() sections?: CoreCourseSectionWithStatus[]; // List of course sections.
+ @Input() sections?: CoreCourseSection[]; // List of course sections.
@Input() initialSectionId?: number; // The section to load first (by ID).
@Input() initialSectionNumber?: number; // The section to load first (by number).
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
diff --git a/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts b/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts
index 945b99e4c..8e4b1f6d3 100644
--- a/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts
+++ b/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts
@@ -71,7 +71,7 @@ export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseF
/**
* @inheritdoc
*/
- displaySectionSelector(): boolean {
+ displayCourseIndex(): boolean {
return false;
}
diff --git a/src/core/features/course/format/weeks/services/handlers/weeks-format.ts b/src/core/features/course/format/weeks/services/handlers/weeks-format.ts
index 10de07200..3af676006 100644
--- a/src/core/features/course/format/weeks/services/handlers/weeks-format.ts
+++ b/src/core/features/course/format/weeks/services/handlers/weeks-format.ts
@@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreCourseFormatHandler } from '@features/course/services/format-delegate';
-import { makeSingleton } from '@singletons';
+import { makeSingleton, Translate } from '@singletons';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import { CoreCourseWSSection } from '@features/course/services/course';
import { CoreConstants } from '@/core/constants';
@@ -32,20 +32,14 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
format = 'weeks';
/**
- * Whether or not the handler is enabled on a site level.
- *
- * @return True or promise resolved with true if enabled.
+ * @inheritdoc
*/
async isEnabled(): Promise
{
return true;
}
/**
- * Given a list of sections, get the "current" section that should be displayed first.
- *
- * @param course The course to get the title.
- * @param sections List of sections.
- * @return Current section (or promise resolved with current section).
+ * @inheritdoc
*/
async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise {
const now = CoreTimeUtils.timestamp();
@@ -71,6 +65,13 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
return sections[0];
}
+ /**
+ * @inheritdoc
+ */
+ getSectionHightlightedName(): string {
+ return Translate.instant('core.course.thisweek');
+ }
+
/**
* Return the start and end date of a section.
*
@@ -83,7 +84,7 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
startDate = startDate + 7200;
const dates = {
- start: startDate + (CoreConstants.SECONDS_WEEK * (section.section! - 1)),
+ start: startDate + (CoreConstants.SECONDS_WEEK * ((section.section || 0) - 1)),
end: 0,
};
dates.end = dates.start + CoreConstants.SECONDS_WEEK;
diff --git a/src/core/features/course/lang.json b/src/core/features/course/lang.json
index f6f144b2d..b3b0f24be 100644
--- a/src/core/features/course/lang.json
+++ b/src/core/features/course/lang.json
@@ -26,21 +26,25 @@
"confirmdownloadzerosize": "You are about to start downloading.{{availableSpace}} Are you sure you want to continue?",
"confirmpartialdownloadsize": "You are about to download at least {{size}}.{{availableSpace}} Are you sure you want to continue?",
"confirmlimiteddownload": "You are not currently connected to Wi-Fi. ",
- "gotonextactivity": "Continue to next activity",
- "gotonextactivitynotfound": "Next activity not found. It's possible that it has been hidden or deleted.",
- "gotopreviousactivity": "Continue to previous activity",
- "gotopreviousactivitynotfound": "Previous activity not found. It's possible that it has been hidden or deleted.",
+ "courseindex": "Course index",
"couldnotloadsectioncontent": "Could not load the section content. Please try again later.",
"couldnotloadsections": "Could not load the sections. Please try again later.",
"coursesummary": "Course summary",
+ "done": "Done",
"downloadcourse": "Download course",
"downloadcoursesprogressdescription": "Downloading courses: downloaded {{count}} out of {{total}}.",
"downloadsectionprogressdescription": "Downloading section: downloaded {{count}} out of {{total}}.",
"errordownloadingcourse": "Error downloading course.",
"errordownloadingsection": "Error downloading section.",
"errorgetmodule": "Error getting activity data.",
+ "failed": "Failed",
+ "gotonextactivity": "Continue to next activity",
+ "gotonextactivitynotfound": "Next activity not found. It's possible that it has been hidden or deleted.",
+ "gotopreviousactivity": "Continue to previous activity",
+ "gotopreviousactivitynotfound": "Previous activity not found. It's possible that it has been hidden or deleted.",
"hiddenfromstudents": "Hidden from students",
"hiddenoncoursepage": "Available but not shown on course page",
+ "highlighted": "Highlighted",
"insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.",
"insufficientavailablequota": "Your device could not allocate space to save this download. It may be reserving space for app and system updates. Please clear some storage space first.",
"manualcompletionnotsynced": "Manual completion not synchronised.",
@@ -49,7 +53,8 @@
"overriddennotice": "Your final grade from this activity was manually adjusted.",
"refreshcourse": "Refresh course",
"section": "Section",
- "sections": "Sections",
+ "thisweek": "This week",
+ "todo": "To do",
"useactivityonbrowser": "You can still use it using your device's web browser.",
"warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.",
"warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}"
diff --git a/src/core/features/course/pages/contents/contents.html b/src/core/features/course/pages/contents/contents.html
index 676c330b5..88716c7ea 100644
--- a/src/core/features/course/pages/contents/contents.html
+++ b/src/core/features/course/pages/contents/contents.html
@@ -15,7 +15,8 @@
+ [moduleId]="moduleId" (completionChanged)="onCompletionChange($event)" class="core-course-format-{{course.format}}"
+ *ngIf="dataLoaded">
diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts
index a4de288f3..199599a4c 100644
--- a/src/core/features/course/pages/contents/contents.ts
+++ b/src/core/features/course/pages/contents/contents.ts
@@ -384,8 +384,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
*/
openCourseSummary(): void {
CoreNavigator.navigateToSitePath(
- '/course/' + this.course.id + '/preview',
- { params: { course: this.course } },
+ `/course/${this.course.id}/preview`,
+ { params: { course: this.course, avoidOpenCourse: true } },
);
}
diff --git a/src/core/features/course/pages/index/index.html b/src/core/features/course/pages/index/index.html
index b1776e63f..8ca38de7f 100644
--- a/src/core/features/course/pages/index/index.html
+++ b/src/core/features/course/pages/index/index.html
@@ -1,4 +1,4 @@
-
+
@@ -12,4 +12,31 @@
-
+
+
+
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/core/features/course/pages/index/index.module.ts b/src/core/features/course/pages/index/index.module.ts
index 1e1d26938..314528baa 100644
--- a/src/core/features/course/pages/index/index.module.ts
+++ b/src/core/features/course/pages/index/index.module.ts
@@ -17,7 +17,7 @@ import { RouterModule, ROUTES, Routes } from '@angular/router';
import { resolveModuleRoutes } from '@/app/app-routing.module';
import { CoreSharedModule } from '@/core/shared.module';
-import { CoreCourseIndexPage } from './index.page';
+import { CoreCourseIndexPage } from '.';
import { COURSE_INDEX_ROUTES } from './index-routing.module';
function buildRoutes(injector: Injector): Routes {
diff --git a/src/core/features/course/pages/index/index.scss b/src/core/features/course/pages/index/index.scss
new file mode 100644
index 000000000..da91e59c2
--- /dev/null
+++ b/src/core/features/course/pages/index/index.scss
@@ -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;
+ }
+}
diff --git a/src/core/features/course/pages/index/index.page.ts b/src/core/features/course/pages/index/index.ts
similarity index 69%
rename from src/core/features/course/pages/index/index.page.ts
rename to src/core/features/course/pages/index/index.ts
index 096de4843..963df1009 100644
--- a/src/core/features/course/pages/index/index.page.ts
+++ b/src/core/features/course/pages/index/index.ts
@@ -26,6 +26,8 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreTextUtils } from '@services/utils/text';
import { CoreNavigator } from '@services/navigator';
import { CONTENTS_PAGE_NAME } from '@features/course/course.module';
+import { CoreDomUtils } from '@services/utils/dom';
+import { CoreCollapsibleHeaderDirective } from '@directives/collapsible-header';
/**
* Page that displays the list of courses the user is enrolled in.
@@ -33,23 +35,28 @@ import { CONTENTS_PAGE_NAME } from '@features/course/course.module';
@Component({
selector: 'page-core-course-index',
templateUrl: 'index.html',
+ styleUrls: ['index.scss'],
})
export class CoreCourseIndexPage implements OnInit, OnDestroy {
@ViewChild(CoreTabsOutletComponent) tabsComponent?: CoreTabsOutletComponent;
+ @ViewChild(CoreCollapsibleHeaderDirective) ionCollapsibleHeader?: CoreCollapsibleHeaderDirective;
- title?: string;
+ title = '';
+ category = '';
course?: CoreCourseAnyCourseData;
tabs: CourseTab[] = [];
loaded = false;
+ imageThumb?: string;
+ progress?: number;
protected currentPagePath = '';
protected selectTabObserver: CoreEventObserver;
protected firstTabName?: string;
protected module?: CoreCourseModuleData;
protected modParams?: Params;
- protected isGuest?: boolean;
- protected contentsTab: CoreTabsOutletTab = {
+ protected isGuest = false;
+ protected contentsTab: CoreTabsOutletTab & { pageParams: Params } = {
page: CONTENTS_PAGE_NAME,
title: 'core.course',
pageParams: {},
@@ -60,10 +67,10 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
if (!data.name) {
// If needed, set sectionId and sectionNumber. They'll only be used if the content tabs hasn't been loaded yet.
if (data.sectionId) {
- this.contentsTab.pageParams!.sectionId = data.sectionId;
+ this.contentsTab.pageParams.sectionId = data.sectionId;
}
if (data.sectionNumber) {
- this.contentsTab.pageParams!.sectionNumber = data.sectionNumber;
+ this.contentsTab.pageParams.sectionNumber = data.sectionNumber;
}
// Select course contents.
@@ -79,7 +86,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
}
/**
- * Component being initialized.
+ * @inheritdoc
*/
async ngOnInit(): Promise {
// Increase route depth.
@@ -87,12 +94,19 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
CoreNavigator.increaseRouteDepth(path.replace(/(\/deep)+/, ''));
- // Get params.
- this.course = CoreNavigator.getRouteParam('course');
+ try {
+ this.course = CoreNavigator.getRequiredRouteParam('course');
+ } catch (error) {
+ CoreDomUtils.showErrorModal(error);
+ CoreNavigator.back();
+
+ return;
+ }
+
this.firstTabName = CoreNavigator.getRouteParam('selectedTab');
this.module = CoreNavigator.getRouteParam('module');
this.modParams = CoreNavigator.getRouteParam('modParams');
- this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest');
+ this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest');
this.currentPagePath = CoreNavigator.getCurrentPath();
this.contentsTab.page = CoreTextUtils.concatenatePaths(this.currentPagePath, this.contentsTab.page);
@@ -104,7 +118,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
};
if (this.module) {
- this.contentsTab.pageParams!.moduleId = this.module.id;
+ this.contentsTab.pageParams.moduleId = this.module.id;
}
this.tabs.push(this.contentsTab);
@@ -112,20 +126,23 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
await Promise.all([
this.loadCourseHandlers(),
- this.loadTitle(),
+ this.loadBasinInfo(),
]);
}
/**
* A tab was selected.
*/
- tabSelected(): void {
- if (this.module) {
- // Now that the first tab has been selected we can load the module.
- CoreCourseHelper.openModule(this.module, this.course!.id, this.contentsTab.pageParams!.sectionId, this.modParams);
+ tabSelected(tabToSelect: CoreTabsOutletTab): void {
+ this.ionCollapsibleHeader?.setupContent(tabToSelect.id);
- delete this.module;
+ if (!this.module || !this.course) {
+ return;
}
+ // Now that the first tab has been selected we can load the module.
+ CoreCourseHelper.openModule(this.module, this.course.id, this.contentsTab.pageParams.sectionId, this.modParams);
+
+ delete this.module;
}
/**
@@ -134,8 +151,12 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
* @return Promise resolved when done.
*/
protected async loadCourseHandlers(): Promise {
+ if (!this.course) {
+ return;
+ }
+
// Load the course handlers.
- const handlers = await CoreCourseOptionsDelegate.getHandlersToDisplay(this.course!, false, this.isGuest);
+ const handlers = await CoreCourseOptionsDelegate.getHandlersToDisplay(this.course, false, this.isGuest);
let tabToLoad: number | undefined;
@@ -169,23 +190,34 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
*
* @return Promise resolved when done.
*/
- protected async loadTitle(): Promise {
+ protected async loadBasinInfo(): Promise {
+ if (!this.course) {
+ return;
+ }
+
// Get the title to display initially.
- this.title = CoreCourseFormatDelegate.getCourseTitle(this.course!);
+ this.title = CoreCourseFormatDelegate.getCourseTitle(this.course);
+ this.category = 'categoryname' in this.course ? this.course.categoryname : '';
+
+ if ('overviewfiles' in this.course) {
+ this.imageThumb = this.course.overviewfiles?.[0]?.fileurl;
+ }
+
+ this.updateProgress();
// Load sections.
- const sections = await CoreUtils.ignoreErrors(CoreCourse.getSections(this.course!.id, false, true));
+ const sections = await CoreUtils.ignoreErrors(CoreCourse.getSections(this.course.id, false, true));
if (!sections) {
return;
}
// Get the title again now that we have sections.
- this.title = CoreCourseFormatDelegate.getCourseTitle(this.course!, sections);
+ this.title = CoreCourseFormatDelegate.getCourseTitle(this.course, sections);
}
/**
- * Page destroyed.
+ * @inheritdoc
*/
ngOnDestroy(): void {
const path = CoreNavigator.getRouteFullPath(this.route.snapshot);
@@ -208,6 +240,39 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
this.tabsComponent?.ionViewDidLeave();
}
+ /**
+ * Open the course summary
+ */
+ openCourseSummary(): void {
+ if (!this.course) {
+ return;
+ }
+
+ CoreNavigator.navigateToSitePath(
+ `/course/${this.course.id}/preview`,
+ { params: { course: this.course, avoidOpenCourse: true } },
+ );
+ }
+
+ /**
+ * Update course progress.
+ */
+ protected updateProgress(): void {
+ if (
+ !this.course ||
+ !('progress' in this.course) ||
+ typeof this.course.progress !== 'number' ||
+ this.course.progress < 0 ||
+ this.course.completionusertracked === false
+ ) {
+ this.progress = undefined;
+
+ return;
+ }
+
+ this.progress = this.course.progress;
+ }
+
}
type CourseTab = CoreTabsOutletTab & {
diff --git a/src/core/features/course/pages/module-preview/module-preview.html b/src/core/features/course/pages/module-preview/module-preview.html
index 711f8c740..bd5b02506 100644
--- a/src/core/features/course/pages/module-preview/module-preview.html
+++ b/src/core/features/course/pages/module-preview/module-preview.html
@@ -27,33 +27,35 @@
-
-
-
-
+
+
+
+
-
-
- {{ 'core.course.hiddenfromstudents' | translate }}
-
+
+
+
+ {{ 'core.course.hiddenfromstudents' | translate }}
+
-
-
- {{ 'core.course.hiddenoncoursepage' | translate }}
-
+
+
+
+ {{ 'core.course.hiddenoncoursepage' | translate }}
+
-
-
{{ 'core.restricted' | translate }}
-
-
-
- {{ 'core.course.manualcompletionnotsynced' | translate }}
-
+
+
+ {{ 'core.course.manualcompletionnotsynced' | translate }}
+
diff --git a/src/core/features/course/pages/preview/preview.html b/src/core/features/course/pages/preview/preview.html
index 4969b265b..4cb9bad62 100644
--- a/src/core/features/course/pages/preview/preview.html
+++ b/src/core/features/course/pages/preview/preview.html
@@ -5,7 +5,7 @@
-
+ {{'core.course.coursesummary' | translate}}
@@ -16,44 +16,49 @@
-
-
-
+
-
-
-
-
+
+
+
+
{{course.startdate * 1000 | coreFormatDate:"strftimedatefullshort" }}
- {{course.enddate * 1000 | coreFormatDate:"strftimedatefullshort" }}
+
+
+
+
+
+ {{'core.summary' | translate}}
+
-
-
+
+
{{ 'core.teachers' | translate }}
-
@@ -62,7 +67,7 @@
-
+
@@ -83,7 +88,8 @@
-
+
+
{{ instance.name }}
@@ -92,23 +98,24 @@
-
-
-
- {{ 'core.courses.paypalaccepted' | translate }}
- {{ 'core.paymentinstant' | translate }}
-
- {{ 'core.courses.sendpaymentbutton' | translate }}
-
-
-
-
-
- {{ 'core.courses.notenrollable' | translate }}
-
-
-
+
+
+ {{ 'core.courses.paypalaccepted' | translate }}
+ {{ 'core.paymentinstant' | translate }}
+
+ {{ 'core.courses.sendpaymentbutton' | translate }}
+
+
+
+
+
+ {{ 'core.courses.notenrollable' | translate }}
+
+
+
+
+
@@ -116,23 +123,24 @@
[name]="prefetchCourseData.icon" color="success" aria-hidden="true" role="status">
-
- {{ 'core.course.downloadcourse' | translate }}
- {{ 'core.course.refreshcourse' | translate }}
-
-
-
+ {{ 'core.course.downloadcourse' | translate }}
+ {{ 'core.course.refreshcourse' | translate }}
+
+
+
- {{ 'core.course' | translate }}
+ {{ 'core.course' | translate }}
-
-
+
+
+
- {{ 'core.openinbrowser' | translate }}
+ {{ 'core.openinbrowser' | translate }}
-
+
+
diff --git a/src/core/features/course/pages/preview/preview.page.ts b/src/core/features/course/pages/preview/preview.page.ts
index 923eb7109..8c1f9b9ce 100644
--- a/src/core/features/course/pages/preview/preview.page.ts
+++ b/src/core/features/course/pages/preview/preview.page.ts
@@ -20,8 +20,8 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import {
+ CoreCourseCustomField,
CoreCourseEnrolmentMethod,
- CoreCourseGetCoursesData,
CoreCourses,
CoreCourseSearchedData,
CoreCoursesProvider,
@@ -34,6 +34,8 @@ import { Translate } from '@singletons';
import { CoreConstants } from '@/core/constants';
import { CoreCoursesSelfEnrolPasswordComponent } from '../../../courses/components/self-enrol-password/self-enrol-password';
import { CoreNavigator } from '@services/navigator';
+import { CoreUtils } from '@services/utils/utils';
+import { CoreCourseWithImageAndColor } from '@features/courses/services/courses-helper';
/**
* Page that allows "previewing" a course and enrolling in it if enabled and not enrolled.
@@ -45,12 +47,13 @@ import { CoreNavigator } from '@services/navigator';
})
export class CoreCoursePreviewPage implements OnInit, OnDestroy {
- course?: CoreCourseSearchedData;
+ course?: CoreCourseSummaryData;
isEnrolled = false;
canAccessCourse = true;
selfEnrolInstances: CoreCourseEnrolmentMethod[] = [];
paypalEnabled = false;
dataLoaded = false;
+ avoidOpenCourse = false;
prefetchCourseData: CorePrefetchStatusInfo = {
icon: '',
statusTranslatable: 'core.loading',
@@ -64,6 +67,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
courseUrl = '';
courseImageUrl?: string;
isMobile: boolean;
+ progress?: number;
protected isGuestEnabled = false;
protected useGuestAccess = false;
@@ -74,6 +78,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
protected paypalReturnUrl = '';
protected pageDestroyed = false;
protected courseStatusObserver?: CoreEventObserver;
+ protected courseId!: number;
constructor(
protected zone: NgZone,
@@ -84,7 +89,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
if (this.downloadCourseEnabled) {
// Listen for status change in course.
this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => {
- if (data.courseId == this.course!.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
+ if (data.courseId == this.courseId || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
this.updateCourseStatus(data.status);
}
}, CoreSites.getCurrentSiteId());
@@ -92,27 +97,25 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
}
/**
- * View loaded.
+ * @inheritdoc
*/
async ngOnInit(): Promise
{
- this.course = CoreNavigator.getRouteParam('course');
-
- if (!this.course) {
+ try {
+ this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
+ } catch (error) {
+ CoreDomUtils.showErrorModal(error);
CoreNavigator.back();
return;
}
- const currentSite = CoreSites.getCurrentSite();
- const currentSiteUrl = currentSite && currentSite.getURL();
+ this.avoidOpenCourse = !!CoreNavigator.getRouteBooleanParam('avoidOpenCourse');
+ this.course = CoreNavigator.getRouteParam('course');
- this.paypalEnabled = this.course!.enrollmentmethods?.indexOf('paypal') > -1;
- this.enrolUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'enrol/index.php?id=' + this.course!.id);
- this.courseUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'course/view.php?id=' + this.course!.id);
- this.paypalReturnUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'enrol/paypal/return.php');
- if (this.course.overviewfiles.length > 0) {
- this.courseImageUrl = this.course.overviewfiles[0].fileurl;
- }
+ const currentSiteUrl = CoreSites.getRequiredCurrentSite().getURL();
+ this.enrolUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'enrol/index.php?id=' + this.courseId);
+ this.courseUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'course/view.php?id=' + this.courseId);
+ this.paypalReturnUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'enrol/paypal/return.php');
try {
await this.getCourse();
@@ -120,11 +123,11 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
if (this.downloadCourseEnabled) {
// Determine course prefetch icon.
- this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.course!.id);
+ this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.courseId);
if (this.prefetchCourseData.loading) {
// Course is being downloaded. Get the download promise.
- const promise = CoreCourseHelper.getCourseDownloadPromise(this.course!.id);
+ const promise = CoreCourseHelper.getCourseDownloadPromise(this.courseId);
if (promise) {
// There is a download promise. If it fails, show an error.
promise.catch((error) => {
@@ -134,7 +137,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
});
} else {
// No download, this probably means that the app was closed while downloading. Set previous status.
- CoreCourse.setCoursePreviousStatus(this.course!.id);
+ CoreCourse.setCoursePreviousStatus(this.courseId);
}
}
}
@@ -177,13 +180,15 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
this.selfEnrolInstances = [];
try {
- this.enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(this.course!.id);
+ this.enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(this.courseId);
this.enrolmentMethods.forEach((method) => {
if (method.type === 'self') {
this.selfEnrolInstances.push(method);
} else if (method.type === 'guest') {
this.isGuestEnabled = true;
+ } else if (method.type === 'paypal') {
+ this.paypalEnabled = true;
}
});
} catch (error) {
@@ -191,22 +196,17 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
}
try {
- let course: CoreEnrolledCourseData | CoreCourseGetCoursesData;
-
// Check if user is enrolled in the course.
try {
- course = await CoreCourses.getUserCourse(this.course!.id);
+ this.course = await CoreCourses.getUserCourse(this.courseId);
this.isEnrolled = true;
} catch {
// The user is not enrolled in the course. Use getCourses to see if it's an admin/manager and can see the course.
this.isEnrolled = false;
-
- course = await CoreCourses.getCourse(this.course!.id);
+ this.course = await CoreCourses.getCourse(this.courseId);
}
// Success retrieving the course, we can assume the user has permissions to view it.
- this.course!.fullname = course.fullname || this.course!.fullname;
- this.course!.summary = course.summary || this.course!.summary;
this.canAccessCourse = true;
this.useGuestAccess = false;
} catch {
@@ -219,14 +219,37 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
}
}
- if (!CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.7')) {
- try {
- const course = await CoreCourses.getCourseByField('id', this.course!.id);
+ if (this.course && 'overviewfiles' in this.course && this.course.overviewfiles?.length) {
+ this.courseImageUrl = this.course.overviewfiles[0].fileurl;
+ }
- this.course!.customfields = course.customfields;
- } catch {
- // Ignore errors.
+ try {
+ const courseByField = await CoreCourses.getCourseByField('id', this.courseId);
+ if (this.course) {
+ this.course.customfields = courseByField.customfields;
+ this.course.contacts = courseByField.contacts;
+ this.course.displayname = courseByField.displayname;
+ this.course.categoryname = courseByField.categoryname;
+ this.course.overviewfiles = courseByField.overviewfiles;
+ } else {
+ this.course = courseByField;
}
+
+ this.paypalEnabled = !this.isEnrolled && courseByField.enrollmentmethods?.indexOf('paypal') > -1;
+
+ } catch {
+ // Ignore errors.
+ }
+
+ if (!this.course ||
+ !('progress' in this.course) ||
+ typeof this.course.progress !== 'number' ||
+ this.course.progress < 0 ||
+ this.course.completionusertracked === false
+ ) {
+ this.progress = undefined;
+ } else {
+ this.progress = this.course.progress;
}
this.dataLoaded = true;
@@ -234,13 +257,15 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
/**
* Open the course.
+ *
+ * @param replaceCurrentPage If current place should be replaced in the navigation stack.
*/
- openCourse(): void {
- if (!this.canAccessCourse) {
+ openCourse(replaceCurrentPage = false): void {
+ if (!this.canAccessCourse || !this.course || this.avoidOpenCourse) {
return;
}
- CoreCourseHelper.openCourse(this.course!, { isGuest: this.useGuestAccess });
+ CoreCourseHelper.openCourse(this.course, { params: { isGuest: this.useGuestAccess }, replace: replaceCurrentPage });
}
/**
@@ -279,7 +304,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
};
// Open the enrolment page in InAppBrowser.
- const window = await CoreSites.getCurrentSite()!.openInAppWithAutoLogin(this.enrolUrl);
+ const window = await CoreSites.getRequiredCurrentSite().openInAppWithAutoLogin(this.enrolUrl);
// Observe loaded pages in the InAppBrowser to check if the enrol process has ended.
const inAppLoadSubscription = window.on('loadstart').subscribe((event) => {
@@ -319,7 +344,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
const modal = await CoreDomUtils.showModalLoading('core.loading', true);
try {
- await CoreCourses.selfEnrol(this.course!.id, password, instanceId);
+ await CoreCourses.selfEnrol(this.courseId, password, instanceId);
// Close modal and refresh data.
this.isEnrolled = true;
@@ -331,13 +356,13 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
await this.refreshData().finally(() => {
// My courses have been updated, trigger event.
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
- courseId: this.course!.id,
+ courseId: this.courseId,
course: this.course,
action: CoreCoursesProvider.ACTION_ENROL,
}, CoreSites.getCurrentSiteId());
});
- this.openCourse();
+ this.openCourse(true);
modal?.dismiss();
} catch (error) {
@@ -378,12 +403,10 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
const promises: Promise[] = [];
promises.push(CoreCourses.invalidateUserCourses());
- promises.push(CoreCourses.invalidateCourse(this.course!.id));
- promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.course!.id));
- promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(this.course!.id));
- if (CoreSites.getCurrentSite() && !CoreSites.getCurrentSite()!.isVersionGreaterEqualThan('3.7')) {
- promises.push(CoreCourses.invalidateCoursesByField('id', this.course!.id));
- }
+ promises.push(CoreCourses.invalidateCourse(this.courseId));
+ promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.courseId));
+ promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(this.courseId));
+ promises.push(CoreCourses.invalidateCoursesByField('id', this.courseId));
if (this.guestInstanceId) {
promises.push(CoreCourses.invalidateCourseGuestEnrolmentInfo(this.guestInstanceId));
}
@@ -419,14 +442,10 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
}
// Check if user is enrolled in the course.
- try {
- CoreCourses.invalidateUserCourses();
- } catch {
- // Ignore errors.
- }
+ await CoreUtils.ignoreErrors(CoreCourses.invalidateUserCourses());
try {
- await CoreCourses.getUserCourse(this.course!.id);
+ await CoreCourses.getUserCourse(this.courseId);
} catch {
// Not enrolled, wait a bit and try again.
if (this.pageDestroyed || (Date.now() - this.waitStart > 60000)) {
@@ -451,7 +470,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
*/
async prefetchCourse(): Promise {
try {
- await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course!, {
+ await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course as CoreEnrolledCourseData, {
isGuest: this.useGuestAccess,
});
} catch (error) {
@@ -462,14 +481,20 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
}
/**
- * Page destroyed.
+ * @inheritdoc
*/
ngOnDestroy(): void {
this.pageDestroyed = true;
-
- if (this.courseStatusObserver) {
- this.courseStatusObserver.off();
- }
+ this.courseStatusObserver?.off();
}
}
+
+type CoreCourseSummaryData = CoreCourseWithImageAndColor & (CoreEnrolledCourseData | CoreCourseSearchedData) & {
+ contacts?: { // Contact users.
+ id: number; // Contact user id.
+ fullname: string; // Contact user fullname.
+ }[];
+ customfields?: CoreCourseCustomField[]; // Custom fields and associated values.
+ categoryname?: string; // Category name.
+};
diff --git a/src/core/features/course/pages/preview/preview.scss b/src/core/features/course/pages/preview/preview.scss
index 161f3647a..8f159c333 100644
--- a/src/core/features/course/pages/preview/preview.scss
+++ b/src/core/features/course/pages/preview/preview.scss
@@ -1,41 +1,43 @@
:host {
- --scroll-factor: 0.5;
- --translate-z: calc(-2 * var(--scroll-factor))px;
- --scale: calc(1 + var(--scroll-factor) * 2);
+ ion-content:not(.animating) {
+ &::part(scroll) {
+ perspective: 1px;
+ perspective-origin: center top;
+ transform-style: preserve-3d;
+ }
- perspective: 1px;
- perspective-origin: center top;
- transform-style: preserve-3d;
+ .core-course-thumb {
+ transform-origin: center top;
- // @todo This parallax effect caused the image to be scaled during page transitions,
- // and in some devices it seems like the problem persisted even after the transition.
- // We should decide whether we want to keep this parallax or not, and if we do fix
- // the problem or find an alternative implementation. For now, it's disabled.
+ --scroll-factor: 0.5;
+ --translate-z: calc(-2 * var(--scroll-factor))px;
+ --scale: calc(1 + var(--scroll-factor) * 2);
+
+ /**
+ * Calculated with scroll-factor: 0.5;
+ * translate-z: -2 * $scroll-factor px;
+ * scale: 1 + $scroll-factor * 2;
+ */
+ transform: translateZ(-1px) scale(2);
+ }
+ }
+
+ .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 {
overflow: hidden;
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);
}
diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts
index 737a405ae..56dee5790 100644
--- a/src/core/features/course/services/course-helper.ts
+++ b/src/core/features/course/services/course-helper.ts
@@ -68,7 +68,7 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreSiteHome } from '@features/sitehome/services/sitehome';
-import { CoreNavigator } from '@services/navigator';
+import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreSiteHomeHomeHandlerService } from '@features/sitehome/services/handlers/sitehome-home';
import { CoreStatusWithWarningsWSResponse } from '@services/ws';
@@ -1178,7 +1178,7 @@ export class CoreCourseHelperProvider {
modal?.dismiss();
- return this.openCourse(course, params, siteId);
+ return this.openCourse(course, { params , siteId });
}
/**
@@ -2020,20 +2020,25 @@ export class CoreCourseHelperProvider {
* they will see the result immediately.
*
* @param course Course to open
- * @param params Params to pass to the course page.
- * @param siteId Site ID. If not defined, current site.
+ * @param navOptions Navigation options that includes params to pass to the page.
* @return Promise resolved when done.
*/
- async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params, siteId?: string): Promise {
+ async openCourse(
+ course: CoreCourseAnyCourseData | { id: number },
+ navOptions?: CoreNavigationOptions & { siteId?: string },
+ ): Promise {
+ const siteId = navOptions?.siteId;
if (!siteId || siteId == CoreSites.getCurrentSiteId()) {
// Current site, we can open the course.
- return CoreCourse.openCourse(course, params);
+ return CoreCourse.openCourse(course, navOptions);
} else {
// We need to load the site first.
- params = params || {};
- Object.assign(params, { course: course });
+ navOptions = navOptions || {};
- await CoreNavigator.navigateToSitePath(`course/${course.id}`, { siteId, params });
+ navOptions.params = navOptions.params || {};
+ Object.assign(navOptions.params, { course: course });
+
+ await CoreNavigator.navigateToSitePath(`course/${course.id}`, navOptions);
}
}
diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts
index 24984dfc3..fa30f2991 100644
--- a/src/core/features/course/services/course.ts
+++ b/src/core/features/course/services/course.ts
@@ -43,7 +43,7 @@ import { CoreCourseLogCronHandler } from './handlers/log-cron';
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync';
import { CoreTagItem } from '@features/tag/services/tag';
-import { CoreNavigator } from '@services/navigator';
+import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreCourseModuleDelegate } from './module-delegate';
const ROOT_CACHE_KEY = 'mmCourse:';
@@ -62,7 +62,7 @@ declare module '@singletons/events' {
}
/**
- * Completion status valid values.
+ * Course Module completion status enumeration.
*/
export enum CoreCourseModuleCompletionStatus {
COMPLETION_INCOMPLETE = 0,
@@ -71,6 +71,11 @@ export enum CoreCourseModuleCompletionStatus {
COMPLETION_COMPLETE_FAIL = 3,
}
+export enum CoreCourseCompletionType {
+ MANUAL = 0,
+ AUTO = 1,
+}
+
/**
* Completion tracking valid values.
*/
@@ -1172,10 +1177,13 @@ export class CoreCourseProvider {
* This function must be in here instead of course helper to prevent circular dependencies.
*
* @param course Course to open
- * @param params Other params to pass to the course page.
+ * @param navOptions Navigation options that includes params to pass to the page.
* @return Promise resolved when done.
*/
- async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params): Promise {
+ async openCourse(
+ course: CoreCourseAnyCourseData | { id: number },
+ navOptions?: CoreNavigationOptions,
+ ): Promise {
const loading = await CoreDomUtils.showModalLoading();
// Wait for site plugins to be fetched.
@@ -1192,7 +1200,7 @@ export class CoreCourseProvider {
if (!format || !CoreSitePlugins.sitePluginPromiseExists(`format_${format}`)) {
// No custom format plugin. We don't need to wait for anything.
loading.dismiss();
- await CoreCourseFormatDelegate.openCourse( course, params);
+ await CoreCourseFormatDelegate.openCourse( course, navOptions);
return;
}
@@ -1203,7 +1211,7 @@ export class CoreCourseProvider {
// The format loaded successfully, but the handlers wont be registered until all site plugins have loaded.
if (CoreSitePlugins.sitePluginsFinishedLoading) {
- return CoreCourseFormatDelegate.openCourse( course, params);
+ return CoreCourseFormatDelegate.openCourse( course, navOptions);
}
// Wait for plugins to be loaded.
@@ -1212,7 +1220,7 @@ export class CoreCourseProvider {
const observer = CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => {
observer?.off();
- CoreCourseFormatDelegate.openCourse( course, params)
+ CoreCourseFormatDelegate.openCourse( course, navOptions)
.then(deferred.resolve).catch(deferred.reject);
});
diff --git a/src/core/features/course/services/format-delegate.ts b/src/core/features/course/services/format-delegate.ts
index 295691a0e..94a4bcc4c 100644
--- a/src/core/features/course/services/format-delegate.ts
+++ b/src/core/features/course/services/format-delegate.ts
@@ -13,10 +13,10 @@
// limitations under the License.
import { Injectable, Type } from '@angular/core';
-import { Params } from '@angular/router';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
+import { CoreNavigationOptions } from '@services/navigator';
import { makeSingleton } from '@singletons';
import { CoreCourseWSSection } from './course';
import { CoreCourseSection } from './course-helper';
@@ -66,13 +66,22 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
*/
displayEnableDownload?(course: CoreCourseAnyCourseData): boolean;
+ /**
+ * Whether the default course index should be displayed. Defaults to true.
+ *
+ * @deprecated on 4.0. Please use displayCourseIndex instead.
+ * @param course The course to check.
+ * @return Whether the default course index should be displayed.
+ */
+ displaySectionSelector?(course: CoreCourseAnyCourseData): boolean;
+
/**
* Whether the default section selector should be displayed. Defaults to true.
*
* @param course The course to check.
* @return Whether the default section selector should be displayed.
*/
- displaySectionSelector?(course: CoreCourseAnyCourseData): boolean;
+ displayCourseIndex?(course: CoreCourseAnyCourseData): boolean;
/**
* Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format,
@@ -93,6 +102,13 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
*/
getCurrentSection?(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise;
+ /**
+ * Returns the name for the highlighted section.
+ *
+ * @return The name for the highlighted section based on the given course format.
+ */
+ getSectionHightlightedName?(): string;
+
/**
* Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened.
* Implement it only if you want to create your own page to display the course. In general it's better to use the method
@@ -100,10 +116,10 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
* Your page should include the course handlers using CoreCoursesDelegate.
*
* @param course The course to open. It should contain a "format" attribute.
- * @param params Params to pass to the course page.
+ * @param navOptions Navigation options that includes params to pass to the page.
* @return Promise resolved when done.
*/
- openCourse?(course: CoreCourseAnyCourseData, params?: Params): Promise;
+ openCourse?(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise;
/**
* 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 | 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 | undefined>;
-
/**
* Return the Component to use to display a single section. This component will only be used if the user is viewing a
* single section. If all the sections are displayed at once then it won't be used.
@@ -218,12 +225,19 @@ export class CoreCourseFormatDelegateService extends CoreDelegate(course.format || '', 'displayCourseIndex', [course]);
+
+ if (display !== undefined) {
+ return display;
+ }
+
+ // Use displaySectionSelector while is not completely deprecated.
return !!this.executeFunctionOnEnabled(course.format || '', 'displaySectionSelector', [course]);
}
@@ -276,8 +290,8 @@ export class CoreCourseFormatDelegateService extends CoreDelegate {
+ async getCurrentSection(course: CoreCourseAnyCourseData, sections: T[]): Promise {
try {
- const section = await this.executeFunctionOnEnabled(
+ const section = await this.executeFunctionOnEnabled(
course.format || '',
'getCurrentSection',
[course, sections],
@@ -303,17 +317,16 @@ export class CoreCourseFormatDelegateService extends CoreDelegate | undefined> {
- try {
- return await this.executeFunctionOnEnabled>(course.format || '', 'getSectionSelectorComponent', [course]);
- } catch (error) {
- this.logger.error('Error getting section selector component', error);
- }
+ getSectionHightlightedName(course: CoreCourseAnyCourseData): string | undefined {
+ return this.executeFunctionOnEnabled(
+ course.format || '',
+ 'getSectionHightlightedName',
+ );
}
/**
@@ -346,11 +359,11 @@ export class CoreCourseFormatDelegateService extends CoreDelegate {
- await this.executeFunctionOnEnabled(course.format || '', 'openCourse', [course, params]);
+ async openCourse(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise {
+ await this.executeFunctionOnEnabled(course.format || '', 'openCourse', [course, navOptions]);
}
/**
diff --git a/src/core/features/course/services/handlers/default-format.ts b/src/core/features/course/services/handlers/default-format.ts
index 4ef53ca4b..38c68d3df 100644
--- a/src/core/features/course/services/handlers/default-format.ts
+++ b/src/core/features/course/services/handlers/default-format.ts
@@ -13,12 +13,10 @@
// limitations under the License.
import { Injectable } from '@angular/core';
-import { Params } from '@angular/router';
-
import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses';
-import { CoreNavigator } from '@services/navigator';
+import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreUtils } from '@services/utils/utils';
-import { CoreCourseWSSection } from '../course';
+import { Translate } from '@singletons';
import { CoreCourseSection } from '../course-helper';
import { CoreCourseFormatHandler } from '../format-delegate';
@@ -32,19 +30,14 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
format = 'default';
/**
- * Whether or not the handler is enabled on a site level.
- *
- * @return Promise resolved with true if enabled.
+ * @inheritdoc
*/
async isEnabled(): Promise {
return true;
}
/**
- * Get the title to use in course page.
- *
- * @param course The course.
- * @return Title.
+ * @inheritdoc
*/
getCourseTitle(course: CoreCourseAnyCourseData): string {
if (course.displayname) {
@@ -57,57 +50,35 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
}
/**
- * Whether it allows seeing all sections at the same time. Defaults to true.
- *
- * @param course The course to check.
- * @return Whether it can view all sections.
+ * @inheritdoc
*/
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- canViewAllSections(course: CoreCourseAnyCourseData): boolean {
+ canViewAllSections(): boolean {
return true;
}
/**
- * Whether the option blocks should be displayed. Defaults to true.
- *
- * @param course The course to check.
- * @return Whether it can display blocks.
+ * @inheritdoc
*/
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- displayBlocks(course: CoreCourseAnyCourseData): boolean {
+ displayBlocks(): boolean {
return true;
}
/**
- * Whether the default section selector should be displayed. Defaults to true.
- *
- * @param course The course to check.
- * @return Whether the default section selector should be displayed.
+ * @inheritdoc
*/
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- displaySectionSelector(course: CoreCourseAnyCourseData): boolean {
+ displayCourseIndex(): boolean {
return true;
}
/**
- * Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format,
- * and the doRefresh method of CoreCourseSectionPage must be called on refresh. Defaults to true.
- *
- * @param course The course to check.
- * @param sections List of course sections.
- * @return Whether the refresher should be displayed.
+ * @inheritdoc
*/
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- displayRefresher(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): boolean {
+ displayRefresher(): boolean {
return true;
}
/**
- * Given a list of sections, get the "current" section that should be displayed first.
- *
- * @param course The course to get the title.
- * @param sections List of sections.
- * @return Current section (or promise resolved with current section).
+ * @inheritdoc
*/
async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise {
let marker: number | undefined;
@@ -137,48 +108,40 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
}
/**
- * Invalidate the data required to load the course format.
- *
- * @param course The course to get the title.
- * @param sections List of sections.
- * @return Promise resolved when the data is invalidated.
+ * @inheritdoc
*/
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- async invalidateData(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): Promise {
+ getSectionHightlightedName(): string {
+ return Translate.instant('core.course.highlighted');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ async invalidateData(course: CoreCourseAnyCourseData): Promise {
await CoreCourses.invalidateCoursesByField('id', course.id);
}
/**
- * Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened.
- * Implement it only if you want to create your own page to display the course. In general it's better to use the method
- * getCourseFormatComponent because it will display the course handlers at the top.
- * Your page should include the course handlers using CoreCoursesDelegate.
- *
- * @param course The course to open. It should contain a "format" attribute.
- * @param params Params to pass to the course page.
- * @return Promise resolved when done.
+ * @inheritdoc
*/
- async openCourse(course: CoreCourseAnyCourseData, params?: Params): Promise {
- params = params || {};
- Object.assign(params, { course: course });
+ async openCourse(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise {
+ navOptions = navOptions || {};
+
+ navOptions.params = navOptions.params || {};
+ Object.assign(navOptions.params, { course: course });
// Don't return the .push promise, we don't want to display a loading modal during the page transition.
const currentTab = CoreNavigator.getCurrentMainMenuTab();
const routeDepth = CoreNavigator.getRouteDepth(`/main/${currentTab}/course/${course.id}`);
const deepPath = '/deep'.repeat(routeDepth);
- CoreNavigator.navigateToSitePath(`course${deepPath}/${course.id}`, { params });
+ CoreNavigator.navigateToSitePath(`course${deepPath}/${course.id}`, navOptions);
}
/**
- * Whether the view should be refreshed when completion changes. If your course format doesn't display
- * activity completion then you should return false.
- *
- * @param course The course.
- * @return Whether course view should be refreshed when an activity completion changes.
+ * @inheritdoc
*/
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- async shouldRefreshWhenCompletionChanges(course: CoreCourseAnyCourseData): Promise {
+ async shouldRefreshWhenCompletionChanges(): Promise {
return true;
}
diff --git a/src/core/features/courses/components/course-list-item/course-list-item.ts b/src/core/features/courses/components/course-list-item/course-list-item.ts
index bf9d4419f..5a5f5fa24 100644
--- a/src/core/features/courses/components/course-list-item/course-list-item.ts
+++ b/src/core/features/courses/components/course-list-item/course-list-item.ts
@@ -171,7 +171,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
CoreCourseHelper.openCourse(this.course);
} else {
CoreNavigator.navigateToSitePath(
- '/course/' + this.course.id + '/preview',
+ `/course/${this.course.id}/preview`,
{ params: { course: this.course } },
);
}
diff --git a/src/core/features/courses/services/handlers/course-link.ts b/src/core/features/courses/services/handlers/course-link.ts
index 5e1800ddb..5c7347844 100644
--- a/src/core/features/courses/services/handlers/course-link.ts
+++ b/src/core/features/courses/services/handlers/course-link.ts
@@ -156,7 +156,7 @@ export class CoreCoursesCourseLinkHandlerService extends CoreContentLinksHandler
modal.dismiss();
// Now open the course.
- CoreCourseHelper.openCourse(course, pageParams);
+ CoreCourseHelper.openCourse(course, { params: pageParams });
}
/**
diff --git a/src/core/features/siteplugins/classes/handlers/course-format-handler.ts b/src/core/features/siteplugins/classes/handlers/course-format-handler.ts
index 89cfaff07..c6b7914d8 100644
--- a/src/core/features/siteplugins/classes/handlers/course-format-handler.ts
+++ b/src/core/features/siteplugins/classes/handlers/course-format-handler.ts
@@ -38,8 +38,9 @@ export class CoreSitePluginsCourseFormatHandler extends CoreSitePluginsBaseHandl
/**
* @inheritdoc
*/
- displaySectionSelector(): boolean {
- return this.handlerSchema.displaysectionselector ?? true;
+ displayCourseIndex(): boolean {
+ // Use displaysectionselector while is not completely deprecated.
+ return this.handlerSchema.displaycourseindex ?? this.handlerSchema.displaysectionselector ?? true;
}
/**
diff --git a/src/core/features/siteplugins/components/course-format/course-format.ts b/src/core/features/siteplugins/components/course-format/course-format.ts
index 025c1036e..9fd9e6459 100644
--- a/src/core/features/siteplugins/components/course-format/course-format.ts
+++ b/src/core/features/siteplugins/components/course-format/course-format.ts
@@ -16,7 +16,7 @@ import { Component, OnChanges, Input, ViewChild, Output, EventEmitter } from '@a
import { IonRefresher } from '@ionic/angular';
import { CoreCourseFormatComponent } from '@features/course/components/format/format';
-import { CoreCourseModuleCompletionData, CoreCourseSectionWithStatus } from '@features/course/services/course-helper';
+import { CoreCourseModuleCompletionData, CoreCourseSection } from '@features/course/services/course-helper';
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import { CoreSitePlugins, CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins';
@@ -33,7 +33,7 @@ import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-
export class CoreSitePluginsCourseFormatComponent implements OnChanges {
@Input() course?: CoreCourseAnyCourseData; // The course to render.
- @Input() sections?: CoreCourseSectionWithStatus[]; // List of course sections. The status will be calculated in this component.
+ @Input() sections?: CoreCourseSection[]; // List of course sections. The status will be calculated in this component.
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
@Input() initialSectionId?: number; // The section to load first (by ID).
@Input() initialSectionNumber?: number; // The section to load first (by number).
diff --git a/src/core/features/siteplugins/services/siteplugins.ts b/src/core/features/siteplugins/services/siteplugins.ts
index 70e85ea13..eded0cd07 100644
--- a/src/core/features/siteplugins/services/siteplugins.ts
+++ b/src/core/features/siteplugins/services/siteplugins.ts
@@ -884,7 +884,11 @@ export type CoreSitePluginsCourseModuleHandlerData = CoreSitePluginsHandlerCommo
export type CoreSitePluginsCourseFormatHandlerData = CoreSitePluginsHandlerCommonData & {
canviewallsections?: boolean;
displayenabledownload?: boolean;
+ /**
+ * @deprecated on 4.0, use displaycourseindex instead.
+ */
displaysectionselector?: boolean;
+ displaycourseindex?: boolean;
};
/**
diff --git a/src/core/lang.json b/src/core/lang.json
index 6690dc4bb..624038e0b 100644
--- a/src/core/lang.json
+++ b/src/core/lang.json
@@ -301,6 +301,7 @@
"strftimetime24": "%H:%M",
"submit": "Submit",
"success": "Success",
+ "summary": "Summary",
"tablet": "Tablet",
"teachers": "Teachers",
"thereisdatatosync": "There are offline {{$a}} to be synchronised.",
diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts
index 945871287..0dbd3e638 100644
--- a/src/core/services/utils/dom.ts
+++ b/src/core/services/utils/dom.ts
@@ -1118,7 +1118,7 @@ export class CoreDomUtilsProvider {
content.scrollToPoint(position[0], position[1], duration || 0);
return true;
- } catch (error) {
+ } catch {
return false;
}
}
diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts
index ecc3a9ad8..288897bbd 100644
--- a/src/core/singletons/events.ts
+++ b/src/core/singletons/events.ts
@@ -20,6 +20,7 @@ import { CoreFilepoolComponentFileEventData } from '@services/filepool';
import { CoreNavigationOptions } from '@services/navigator';
import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
import { CoreScreenOrientation } from '@services/screen';
+import { CoreCourseCompletionType } from '@features/course/services/course';
/**
* Observer instance to stop listening to an event.
@@ -48,6 +49,7 @@ export interface CoreEventsData {
[CoreEvents.SELECT_COURSE_TAB]: CoreEventSelectCourseTabData;
[CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData;
[CoreEvents.MANUAL_COMPLETION_CHANGED]: CoreEventManualCompletionChangedData;
+ [CoreEvents.COMPLETION_CHANGED]: CoreEventCompletionChangedData;
[CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData;
[CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData;
[CoreEvents.IAB_LOAD_START]: InAppBrowserEvent;
@@ -77,7 +79,11 @@ export class CoreEvents {
static readonly SITE_UPDATED = 'site_updated';
static readonly SITE_DELETED = 'site_deleted';
static readonly COMPLETION_MODULE_VIEWED = 'completion_module_viewed';
+ /**
+ * Deprecated on 4.0 use COMPLETION_CHANGED instead.
+ */
static readonly MANUAL_COMPLETION_CHANGED = 'manual_completion_changed';
+ static readonly COMPLETION_CHANGED = 'completion_changed';
static readonly USER_DELETED = 'user_deleted';
static readonly PACKAGE_STATUS_CHANGED = 'package_status_changed';
static readonly COURSE_STATUS_CHANGED = 'course_status_changed';
@@ -347,6 +353,14 @@ export type CoreEventManualCompletionChangedData = {
completion: CoreCourseModuleCompletionData;
};
+/**
+ * Data passed to COMPLETION_CHANGED event.
+ */
+export type CoreEventCompletionChangedData = {
+ completion: CoreCourseModuleCompletionData;
+ type: CoreCourseCompletionType;
+};
+
/**
* Data passed to SECTION_STATUS_CHANGED event.
*/
diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss
index 85eac3689..cfd6a58fa 100644
--- a/src/theme/theme.base.scss
+++ b/src/theme/theme.base.scss
@@ -382,17 +382,21 @@ ion-alert.core-nohead {
}
}
-ion-alert .alert-wrapper {
- overflow: auto;
-}
+ion-alert {
+ --border-radius: var(--huge-radius);
+ .alert-wrapper {
+ overflow: auto;
+ border-radius: var(--border-radius) !important;
-ion-alert .alert-message {
- user-select: text;
- flex-shrink: 0;
-}
+ button.alert-button {
+ color: var(--primary);
+ }
+ }
-ion-alert .alert-wrapper button.alert-button {
- color: var(--primary);
+ .alert-message {
+ user-select: text;
+ flex-shrink: 0;
+ }
}
// Ionic list.
@@ -844,6 +848,8 @@ ion-select-popover ion-item.core-select-option-title {
ion-chip {
line-height: 1.1;
+ border-radius: var(--medium-radius);
+ @include padding-horizontal(16px);
}
ion-searchbar {
@@ -1167,9 +1173,8 @@ ion-item.item-input ion-input.has-focus {
}
}
-// Ionic set this value to 0 without px that provoked miscalculations.
ion-item-divider {
- --inner-padding-end: 0px;
+ --inner-padding-end: 8px;
}
// Change default outline.
@@ -1225,6 +1230,13 @@ ion-grid.core-no-grid > ion-row {
display: block;
}
+.core-underheader {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+}
+
ion-header[collapsible] {
@include core-transition(all, 500ms);
@@ -1247,7 +1259,14 @@ ion-header[collapsible] {
.collapsible-title {
overflow: visible;
- *, h1, h2, .subheading {
+ --inner-padding-top: 0px;
+ --padding-top: 0px;
+
+ ion-label {
+ margin-top: 0px;
+ }
+
+ *, h1 {
@include core-transition(all, 200ms, linear);
}
@@ -1255,9 +1274,12 @@ ion-header[collapsible] {
overflow: visible !important;
}
- h1, h2, .subheading {
+ h1 {
--max-width: none;
}
+ h1.cloned {
+ opacity: 0 !important;
+ }
}
ion-app.ios .collapsible-title h1 {
@@ -1269,7 +1291,7 @@ ion-app.md .collapsible-title h1 {
}
.collapsible-title.collapsible-title-collapsed {
- ion-label, h1, h2, ion-row, ion-col, .subheading {
+ ion-label, h1, ion-row, ion-col {
opacity: 0;
}
}
@@ -1279,13 +1301,13 @@ ion-app.md .collapsible-title h1 {
opacity: var(--collapse-opacity, 0);
}
- ion-label, h1, h2, ion-row, ion-col, .subheading {
+ ion-label, h1, ion-row, ion-col, .subheading {
opacity: 1;
}
}
.collapsible-title.collapsible-title-collapse-nowrap {
- h1, h2, .subheading {
+ h1:not(.cloned) {
max-width: var(--max-width);
white-space: nowrap;
overflow: hidden;
diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss
index ff855bb82..6d0ff6d15 100644
--- a/src/theme/theme.light.scss
+++ b/src/theme/theme.light.scss
@@ -105,7 +105,9 @@
--subdued-text-color: #595959;
--small-radius: 4px;
- --big-radius: 8px;
+ --medium-radius: 8px;
+ --big-radius: 16px;
+ --huge-radius: 24px;
--ion-card-color: var(--text-color);
ion-card {
@@ -113,7 +115,7 @@
--border-style: solid;
--border-color: var(--stroke);
--box-shadow: none;
- --border-radius: var(--big-radius);
+ --border-radius: var(--medium-radius);
}
--text-hightlight-background-color: #{$core-text-hightlight-background-color};
@@ -181,7 +183,7 @@
height: var(--a11y-min-target-size);
border: 1px solid var(--stroke);
box-shadow: none;
- border-radius: var(--big-radius);
+ border-radius: var(--medium-radius);
}
}
@@ -282,7 +284,7 @@
--core-combobox-color: var(--gray-900);
--core-combobox-border-color: var(--stroke);
--core-combobox-border-width: 1px;
- --core-combobox-radius: var(--big-radius);
+ --core-combobox-radius: var(--medium-radius);
--core-combobox-box-shadow: none;
--selected-item-color: var(--primary);
@@ -301,7 +303,7 @@
--core-send-message-input-background: var(--gray-200);
--core-send-message-input-color: var(--gray-900);
- --core-courseimage-on-course-height: 150px;
+ --core-courseimage-on-course-size: 72px;
--core-course-module-navigation-max-height: 56px;
--core-course-module-navigation-background: var(--contrast-background);
diff --git a/upgrade.txt b/upgrade.txt
index 79ccced1d..8b8313511 100644
--- a/upgrade.txt
+++ b/upgrade.txt
@@ -15,6 +15,8 @@ information provided here is intended especially for developers.
The function CoreUserDelegate.getProfileHandlersFor must now receive a context + contextId instead of a courseId.
The user handler function isEnabledForCourse is now called isEnabledForContext and receives a context + contextId instead of a courseId.
Some user handler's functions have also changed to accept context + contextId instead of a courseId: isEnabledForUser, getDisplayData, action.
+- CoreCourseHelperProvider.openCourse parameters changed, now it admits CoreNavigationOptions + siteId on the same object that includes Params passed to page.
+- displaySectionSelector has been deprecated on CoreCourseFormatHandler, use displayCourseIndex instead.
=== 3.9.5 ===