MOBILE-3915 course: Improve Course index page

main
Pau Ferrer Ocaña 2022-01-25 17:33:20 +01:00
parent 6b46f48b3c
commit 0af808571c
16 changed files with 264 additions and 95 deletions

View File

@ -1536,19 +1536,23 @@
"core.course.confirmpartialdownloadsize": "local_moodlemobileapp", "core.course.confirmpartialdownloadsize": "local_moodlemobileapp",
"core.course.couldnotloadsectioncontent": "local_moodlemobileapp", "core.course.couldnotloadsectioncontent": "local_moodlemobileapp",
"core.course.couldnotloadsections": "local_moodlemobileapp", "core.course.couldnotloadsections": "local_moodlemobileapp",
"core.course.courseindex": "courseformat",
"core.course.coursesummary": "moodle", "core.course.coursesummary": "moodle",
"core.course.done": "completion",
"core.course.downloadcourse": "tool_mobile", "core.course.downloadcourse": "tool_mobile",
"core.course.downloadcoursesprogressdescription": "local_moodlemobileapp", "core.course.downloadcoursesprogressdescription": "local_moodlemobileapp",
"core.course.downloadsectionprogressdescription": "local_moodlemobileapp", "core.course.downloadsectionprogressdescription": "local_moodlemobileapp",
"core.course.errordownloadingcourse": "local_moodlemobileapp", "core.course.errordownloadingcourse": "local_moodlemobileapp",
"core.course.errordownloadingsection": "local_moodlemobileapp", "core.course.errordownloadingsection": "local_moodlemobileapp",
"core.course.errorgetmodule": "local_moodlemobileapp", "core.course.errorgetmodule": "local_moodlemobileapp",
"core.course.failed": "completion",
"core.course.gotonextactivity": "local_moodlemobileapp", "core.course.gotonextactivity": "local_moodlemobileapp",
"core.course.gotonextactivitynotfound": "local_moodlemobileapp", "core.course.gotonextactivitynotfound": "local_moodlemobileapp",
"core.course.gotopreviousactivity": "local_moodlemobileapp", "core.course.gotopreviousactivity": "local_moodlemobileapp",
"core.course.gotopreviousactivitynotfound": "local_moodlemobileapp", "core.course.gotopreviousactivitynotfound": "local_moodlemobileapp",
"core.course.hiddenfromstudents": "moodle", "core.course.hiddenfromstudents": "moodle",
"core.course.hiddenoncoursepage": "moodle", "core.course.hiddenoncoursepage": "moodle",
"core.course.highlighted": "moodle",
"core.course.insufficientavailablequota": "local_moodlemobileapp", "core.course.insufficientavailablequota": "local_moodlemobileapp",
"core.course.insufficientavailablespace": "local_moodlemobileapp", "core.course.insufficientavailablespace": "local_moodlemobileapp",
"core.course.manualcompletionnotsynced": "local_moodlemobileapp", "core.course.manualcompletionnotsynced": "local_moodlemobileapp",
@ -1557,7 +1561,8 @@
"core.course.overriddennotice": "grades", "core.course.overriddennotice": "grades",
"core.course.refreshcourse": "local_moodlemobileapp", "core.course.refreshcourse": "local_moodlemobileapp",
"core.course.section": "moodle", "core.course.section": "moodle",
"core.course.sections": "moodle", "core.course.thisweek": "format_weeks/currentsection",
"core.course.todo": "completion",
"core.course.useactivityonbrowser": "local_moodlemobileapp", "core.course.useactivityonbrowser": "local_moodlemobileapp",
"core.course.warningmanualcompletionmodified": "local_moodlemobileapp", "core.course.warningmanualcompletionmodified": "local_moodlemobileapp",
"core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp", "core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp",

View File

@ -13,49 +13,66 @@
<ion-content> <ion-content>
<ion-list id="core-course-section-selector" role="listbox" aria-labelledby="core-course-section-selector-label"> <ion-list id="core-course-section-selector" role="listbox" aria-labelledby="core-course-section-selector-label">
<ng-container *ngFor="let section of sections"> <ng-container *ngFor="let section of sections">
<ion-item-divider *ngIf="!section.hiddenbynumsections && section.id != stealthModulesSectionId" class="ion-text-wrap" <ion-item *ngIf="allSectionId == section.id" class="ion-text-wrap divider" (click)="selectSection($event, section)" button
(click)="selectSection(section)" [attr.aria-current]="selected?.id == section.id ? 'page' : 'false'" [class.item-current]="selectedId === section.id" detail="false">
[class.item-dimmed]="section.visible === 0 || section.uservisible === false" detail="false"
[attr.aria-hidden]="section.uservisible === false" button sticky="true">
<ion-icon name="fas-folder" slot="start" aria-hidden="true"></ion-icon>
<ion-label> <ion-label>
<p class="item-heading"> <p class="item-heading">
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id"> <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id">
</core-format-text> </core-format-text>
</p> </p>
<core-progress-bar *ngIf="section.progress >= 0" [progress]="section.progress"
a11yText="core.course.aria:sectionprogress">
</core-progress-bar>
<ion-badge color="info" *ngIf="section.visible === 0 && section.uservisible !== false" class="ion-text-wrap">
{{ 'core.course.hiddenfromstudents' | translate }}
</ion-badge>
<ion-badge color="info" *ngIf="section.visible === 0 && section.uservisible === false" class="ion-text-wrap">
{{ 'core.notavailable' | translate }}
</ion-badge>
<ion-badge color="info" *ngIf="section.availabilityinfo" class="ion-text-wrap">
<core-format-text [text]=" section.availabilityinfo" contextLevel="course" [contextInstanceId]="course?.id">
</core-format-text>
</ion-badge>
</ion-label> </ion-label>
</ion-item-divider> </ion-item>
<ng-container *ngFor="let module of section.modules"> <ng-container *ngIf="allSectionId != section.id && !section.hiddenbynumsections &&
<ion-item *ngIf="module.visibleoncoursepage !== 0" class="ion-text-wrap"> section.id != stealthModulesSectionId && section.uservisible !== false">
<!-- TODO Add Aria, styles when disabled, etc. --> <ion-item class="ion-text-wrap divider section" (click)="selectSection($event, section)"
<ion-icon name="" *ngIf="module.completionStatus === undefined" slot="start"></ion-icon> [button]="section.visible !== 0 && section.uservisible !== false" [class.item-current]="selectedId === section.id"
<ion-icon name="far-circle" *ngIf="module.completionStatus === 0" slot="start"></ion-icon> [class.item-dimmed]="section.visible === 0" detail="false" sticky="true">
<ion-icon name="fas-circle" *ngIf="module.completionStatus === 1" color="success" slot="start"></ion-icon> <ion-icon [name]="section.expanded ? 'fas-chevron-down' : 'fas-chevron-right'" flip-rtl slot="start"
<ion-icon name="fas-circle" *ngIf="module.completionStatus === 2" color="success" slot="start"></ion-icon> class="expandable-status-icon" (click)="toggleExpand($event, section)"
<ion-icon name="fas-circle" *ngIf="module.completionStatus === 3" color="danger" slot="start"></ion-icon> [attr.aria-label]="(section.expanded ? 'core.collapse' : 'core.expand') | translate"
[attr.aria-expanded]="section.expanded" [attr.aria-controls]="'core-course-index-section-' + section.id">
</ion-icon>
<ion-label> <ion-label>
<p class="item-heading"> <p class="item-heading">
<core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id" <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id">
[courseId]="module.courseid">
</core-format-text> </core-format-text>
</p> </p>
</ion-label> </ion-label>
<ion-badge *ngIf="section.highlighted && highlighted">{{highlighted}}</ion-badge>
<ion-icon name="fas-lock" *ngIf="section.availabilityinfo" slot="end" class="restricted"
[attr.aria-label]="'core.restricted' | translate"></ion-icon>
</ion-item> </ion-item>
<div [hidden]="!section.expanded" [id]="'core-course-index-section-' + section.id">
<ng-container *ngIf="section.expanded">
<ng-container *ngFor="let module of section.modules">
<ion-item [class.item-dimmed]="module.visible === 0"
*ngIf="module.visibleoncoursepage !== 0 && !module.noviewlink"
(click)="selectModule($event, section, module)" button>
<ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined"
slot="start" aria-hidden="true"></ion-icon>
<ion-icon class="completioninfo completion_incomplete" name="far-circle"
*ngIf="module.completionStatus === 0" slot="start" [attr.aria-label]="'core.course.todo' | translate">
</ion-icon>
<ion-icon class="completioninfo completion_complete" name="fas-circle"
*ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start"
[attr.aria-label]="'core.course.done' | translate">
</ion-icon>
<ion-icon class="completioninfo completion_fail" name="fas-circle" *ngIf="module.completionStatus === 3"
color="danger" slot="start" [attr.aria-label]="'core.course.failed' | translate">
</ion-icon>
<ion-label>
<p class="item-heading">
<core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="module.courseid">
</core-format-text>
</p>
</ion-label>
<ion-icon name="fas-lock" *ngIf="module.uservisible === false" slot="end" class="restricted"
[attr.aria-label]="'core.restricted' | translate"></ion-icon>
</ion-item>
</ng-container>
</ng-container>
</div>
</ng-container> </ng-container>
</ng-container> </ng-container>
</ion-list> </ion-list>

View File

@ -2,6 +2,7 @@
core-progress-bar { core-progress-bar {
--bar-margin: 8px 0 4px 0; --bar-margin: 8px 0 4px 0;
--line-height: 20px; --line-height: 20px;
--background: var(--contrast-background);
} }
@if ($core-hide-progress-on-section-selector) { @if ($core-hide-progress-on-section-selector) {
@ -10,6 +11,25 @@ core-progress-bar {
} }
} }
ion-badge { ion-icon.completioninfo {
text-align: start; 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;
} }

View File

@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, Input, OnInit } from '@angular/core'; import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { CoreCourseModuleData, CoreCourseSection, CoreCourseSectionWithStatus } from '@features/course/services/course-helper'; import { CoreCourseModuleData, CoreCourseSectionWithStatus } from '@features/course/services/course-helper';
import { import {
CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionStatus,
CoreCourseModuleCompletionTracking, CoreCourseModuleCompletionTracking,
@ -23,6 +23,9 @@ import {
import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { ModalController } from '@singletons'; 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 to display course index modal.
@ -34,19 +37,30 @@ import { ModalController } from '@singletons';
}) })
export class CoreCourseCourseIndexComponent implements OnInit { export class CoreCourseCourseIndexComponent implements OnInit {
@Input() sections?: SectionWithProgress[]; @ViewChild(IonContent) content?: IonContent;
@Input() selected?: CoreCourseSection;
@Input() sections?: CourseIndexSection[];
@Input() selectedId?: number;
@Input() course?: CoreCourseAnyCourseData; @Input() course?: CoreCourseAnyCourseData;
stealthModulesSectionId = CoreCourseProvider.STEALTH_MODULES_SECTION_ID; stealthModulesSectionId = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
allSectionId = CoreCourseProvider.ALL_SECTIONS_ID;
highlighted?: string;
constructor(
protected elementRef: ElementRef,
) {
}
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnInit(): void { async ngOnInit(): Promise<void> {
if (!this.course || !this.sections || !this.course.enablecompletion || !('courseformatoptions' in this.course) || if (!this.course || !this.sections || !this.course.enablecompletion || !('courseformatoptions' in this.course) ||
!this.course.courseformatoptions) { !this.course.courseformatoptions) {
this.closeModal();
return; return;
} }
@ -55,32 +69,48 @@ export class CoreCourseCourseIndexComponent implements OnInit {
if (!formatOptions || formatOptions.completionusertracked === false) { if (!formatOptions || formatOptions.completionusertracked === false) {
return; 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) => { this.sections.forEach((section) => {
let complete = 0;
let total = 0;
section.modules.forEach((module) => { section.modules.forEach((module) => {
console.error(module); module.completionStatus = module.completiondata === undefined ||
if (!module.uservisible || module.completiondata === undefined || module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE
module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE) { ? undefined
module.completionStatus = undefined; : module.completiondata.state;
return;
}
module.completionStatus = module.completiondata.state;
total++;
if (module.completiondata.state == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE ||
module.completiondata.state == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) {
complete++;
}
}); });
if (total > 0) {
section.progress = complete / total * 100;
}
}); });
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();
} }
/** /**
@ -93,19 +123,40 @@ export class CoreCourseCourseIndexComponent implements OnInit {
/** /**
* Select a section. * Select a section.
* *
* @param event Event.
* @param section Selected section object. * @param section Selected section object.
*/ */
selectSection(section: SectionWithProgress): void { selectSection(event: Event, section: CoreCourseSectionWithStatus): void {
if (section.uservisible !== false) { if (section.uservisible !== false) {
ModalController.dismiss(section); 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: CoreCourseSectionWithStatus, module: CoreCourseModuleData): void {
if (module.uservisible !== false) {
ModalController.dismiss({ event, section, module });
} }
} }
} }
type SectionWithProgress = Omit<CoreCourseSectionWithStatus, 'modules'> & { type CourseIndexSection = Omit<CoreCourseSectionWithStatus, 'modules'> & {
progress?: number; highlighted?: boolean;
expanded?: boolean;
modules: (CoreCourseModuleData & { modules: (CoreCourseModuleData & {
completionStatus?: CoreCourseModuleCompletionStatus; completionStatus?: CoreCourseModuleCompletionStatus;
})[]; })[];
}; };
export type CoreCourseIndexSectionWithModule = {
event: Event;
section: CourseIndexSection;
module?: CoreCourseModuleData;
};

View File

@ -66,7 +66,8 @@
<!-- Template to render a section. --> <!-- Template to render a section. -->
<ng-template #sectionTemplate let-section="section"> <ng-template #sectionTemplate let-section="section">
<section *ngIf="!section.hiddenbynumsections && section.id != allSectionsId && section.id != stealthModulesSectionId"> <section *ngIf="!section.hiddenbynumsections && section.id != allSectionsId && section.id != stealthModulesSectionId">
<ion-item-divider class="ion-text-wrap" color="light" [class.item-dimmed]="section.visible === 0 || section.uservisible === false"> <ion-item-divider class="course-section ion-text-wrap" color="light"
[class.item-dimmed]="section.visible === 0 || section.uservisible === false">
<ion-icon name="fas-folder" aria-label="hidden" slot="start"></ion-icon> <ion-icon name="fas-folder" aria-label="hidden" slot="start"></ion-icon>
<ion-label> <ion-label>
<h2 *ngIf="section.name"> <h2 *ngIf="section.name">

View File

@ -9,3 +9,9 @@
text-transform: none; text-transform: none;
} }
} }
.course-section {
ion-badge {
text-align: start;
}
}

View File

@ -44,9 +44,11 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { IonContent, IonRefresher } from '@ionic/angular'; import { IonContent, IonRefresher } from '@ionic/angular';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreCourseCourseIndexComponent } from '../course-index/course-index'; import { CoreCourseCourseIndexComponent, CoreCourseIndexSectionWithModule } from '../course-index/course-index';
import { CoreBlockHelper } from '@features/block/services/block-helper'; import { CoreBlockHelper } from '@features/block/services/block-helper';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { database } from 'faker';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
/** /**
* Component to display course contents using a certain format. If the format isn't found, use default one. * Component to display course contents using a certain format. If the format isn't found, use default one.
@ -182,7 +184,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
// Course has changed, try to get the components. // Course has changed, try to get the components.
this.getComponents(); this.getComponents();
this.displayCourseIndex = CoreCourseFormatDelegate.displaySectionSelector(this.course); this.displayCourseIndex = CoreCourseFormatDelegate.displayCourseIndex(this.course);
this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course); this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course);
this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id); this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id);
@ -319,17 +321,28 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* Display the course index modal. * Display the course index modal.
*/ */
async openCourseIndex(): Promise<void> { async openCourseIndex(): Promise<void> {
const data = await CoreDomUtils.openModal<CoreCourseSection>({ const data = await CoreDomUtils.openModal<CoreCourseIndexSectionWithModule>({
component: CoreCourseCourseIndexComponent, component: CoreCourseCourseIndexComponent,
componentProps: { componentProps: {
course: this.course, course: this.course,
sections: this.sections, sections: this.sections,
selected: this.selectedSection, selectedId: this.selectedSection?.id,
}, },
}); });
if (data) { if (data) {
this.sectionChanged(data); this.sectionChanged(data.section);
if (data.module) {
if (!data.module.handlerData) {
data.module.handlerData =
await CoreCourseModuleDelegate.getModuleDataFor(data.module.modname, data.module, this.course.id);
}
if (data.module.uservisible !== false && data.module.handlerData?.action) {
data.module.handlerData.action(data.event, data.module, data.module.course);
}
this.moduleId = data.module.id;
}
} }
} }

View File

@ -71,7 +71,7 @@ export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseF
/** /**
* @inheritdoc * @inheritdoc
*/ */
displaySectionSelector(): boolean { displayCourseIndex(): boolean {
return false; return false;
} }

View File

@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
import { CoreTimeUtils } from '@services/utils/time'; import { CoreTimeUtils } from '@services/utils/time';
import { CoreCourseFormatHandler } from '@features/course/services/format-delegate'; import { CoreCourseFormatHandler } from '@features/course/services/format-delegate';
import { makeSingleton } from '@singletons'; import { makeSingleton, Translate } from '@singletons';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import { CoreCourseWSSection } from '@features/course/services/course'; import { CoreCourseWSSection } from '@features/course/services/course';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
@ -32,20 +32,14 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
format = 'weeks'; format = 'weeks';
/** /**
* Whether or not the handler is enabled on a site level. * @inheritdoc
*
* @return True or promise resolved with true if enabled.
*/ */
async isEnabled(): Promise<boolean> { async isEnabled(): Promise<boolean> {
return true; return true;
} }
/** /**
* Given a list of sections, get the "current" section that should be displayed first. * @inheritdoc
*
* @param course The course to get the title.
* @param sections List of sections.
* @return Current section (or promise resolved with current section).
*/ */
async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> { async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> {
const now = CoreTimeUtils.timestamp(); const now = CoreTimeUtils.timestamp();
@ -71,6 +65,13 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
return sections[0]; return sections[0];
} }
/**
* @inheritdoc
*/
getSectionHightlightedName(): string {
return Translate.instant('core.course.thisweek');
}
/** /**
* Return the start and end date of a section. * Return the start and end date of a section.
* *
@ -83,7 +84,7 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
startDate = startDate + 7200; startDate = startDate + 7200;
const dates = { const dates = {
start: startDate + (CoreConstants.SECONDS_WEEK * (section.section! - 1)), start: startDate + (CoreConstants.SECONDS_WEEK * ((section.section || 0) - 1)),
end: 0, end: 0,
}; };
dates.end = dates.start + CoreConstants.SECONDS_WEEK; dates.end = dates.start + CoreConstants.SECONDS_WEEK;

View File

@ -27,21 +27,24 @@
"confirmpartialdownloadsize": "You are about to download <strong>at least</strong> {{size}}.{{availableSpace}} Are you sure you want to continue?", "confirmpartialdownloadsize": "You are about to download <strong>at least</strong> {{size}}.{{availableSpace}} Are you sure you want to continue?",
"confirmlimiteddownload": "You are not currently connected to Wi-Fi. ", "confirmlimiteddownload": "You are not currently connected to Wi-Fi. ",
"courseindex": "Course index", "courseindex": "Course index",
"gotonextactivity": "Continue to next activity",
"gotonextactivitynotfound": "Next activity not found. It's possible that it has been hidden or deleted.",
"gotopreviousactivity": "Continue to previous activity",
"gotopreviousactivitynotfound": "Previous activity not found. It's possible that it has been hidden or deleted.",
"couldnotloadsectioncontent": "Could not load the section content. Please try again later.", "couldnotloadsectioncontent": "Could not load the section content. Please try again later.",
"couldnotloadsections": "Could not load the sections. Please try again later.", "couldnotloadsections": "Could not load the sections. Please try again later.",
"coursesummary": "Course summary", "coursesummary": "Course summary",
"done": "Done",
"downloadcourse": "Download course", "downloadcourse": "Download course",
"downloadcoursesprogressdescription": "Downloading courses: downloaded {{count}} out of {{total}}.", "downloadcoursesprogressdescription": "Downloading courses: downloaded {{count}} out of {{total}}.",
"downloadsectionprogressdescription": "Downloading section: downloaded {{count}} out of {{total}}.", "downloadsectionprogressdescription": "Downloading section: downloaded {{count}} out of {{total}}.",
"errordownloadingcourse": "Error downloading course.", "errordownloadingcourse": "Error downloading course.",
"errordownloadingsection": "Error downloading section.", "errordownloadingsection": "Error downloading section.",
"errorgetmodule": "Error getting activity data.", "errorgetmodule": "Error getting activity data.",
"failed": "Failed",
"gotonextactivity": "Continue to next activity",
"gotonextactivitynotfound": "Next activity not found. It's possible that it has been hidden or deleted.",
"gotopreviousactivity": "Continue to previous activity",
"gotopreviousactivitynotfound": "Previous activity not found. It's possible that it has been hidden or deleted.",
"hiddenfromstudents": "Hidden from students", "hiddenfromstudents": "Hidden from students",
"hiddenoncoursepage": "Available but not shown on course page", "hiddenoncoursepage": "Available but not shown on course page",
"highlighted": "Highlighted",
"insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.", "insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.",
"insufficientavailablequota": "Your device could not allocate space to save this download. It may be reserving space for app and system updates. Please clear some storage space first.", "insufficientavailablequota": "Your device could not allocate space to save this download. It may be reserving space for app and system updates. Please clear some storage space first.",
"manualcompletionnotsynced": "Manual completion not synchronised.", "manualcompletionnotsynced": "Manual completion not synchronised.",
@ -50,6 +53,8 @@
"overriddennotice": "Your final grade from this activity was manually adjusted.", "overriddennotice": "Your final grade from this activity was manually adjusted.",
"refreshcourse": "Refresh course", "refreshcourse": "Refresh course",
"section": "Section", "section": "Section",
"thisweek": "This week",
"todo": "To do",
"useactivityonbrowser": "You can still use it using your device's web browser.", "useactivityonbrowser": "You can still use it using your device's web browser.",
"warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.", "warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.",
"warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}" "warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}"

View File

@ -66,13 +66,22 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
*/ */
displayEnableDownload?(course: CoreCourseAnyCourseData): boolean; displayEnableDownload?(course: CoreCourseAnyCourseData): boolean;
/**
* Whether the default course index should be displayed. Defaults to true.
*
* @deprecated on 4.0. Please use displayCourseIndex instead.
* @param course The course to check.
* @return Whether the default course index should be displayed.
*/
displaySectionSelector?(course: CoreCourseAnyCourseData): boolean;
/** /**
* Whether the default section selector should be displayed. Defaults to true. * Whether the default section selector should be displayed. Defaults to true.
* *
* @param course The course to check. * @param course The course to check.
* @return Whether the default section selector should be displayed. * @return Whether the default section selector should be displayed.
*/ */
displaySectionSelector?(course: CoreCourseAnyCourseData): boolean; displayCourseIndex?(course: CoreCourseAnyCourseData): boolean;
/** /**
* Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format, * Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format,
@ -93,6 +102,13 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
*/ */
getCurrentSection?(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection>; getCurrentSection?(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection>;
/**
* Returns the name for the highlighted section.
*
* @return The name for the highlighted section based on the given course format.
*/
getSectionHightlightedName?(): string;
/** /**
* Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened. * Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened.
* Implement it only if you want to create your own page to display the course. In general it's better to use the method * Implement it only if you want to create your own page to display the course. In general it's better to use the method
@ -209,12 +225,19 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
} }
/** /**
* Whether the default section selector should be displayed. Defaults to true. * Whether the default course index should be displayed. Defaults to true.
* *
* @param course The course to check. * @param course The course to check.
* @return Whether the section selector should be displayed. * @return Whether the course index should be displayed.
*/ */
displaySectionSelector(course: CoreCourseAnyCourseData): boolean { displayCourseIndex(course: CoreCourseAnyCourseData): boolean {
const display = this.executeFunctionOnEnabled<boolean>(course.format || '', 'displayCourseIndex', [course]);
if (display !== undefined) {
return display;
}
// Use displaySectionSelector while is not completely deprecated.
return !!this.executeFunctionOnEnabled<boolean>(course.format || '', 'displaySectionSelector', [course]); return !!this.executeFunctionOnEnabled<boolean>(course.format || '', 'displaySectionSelector', [course]);
} }
@ -278,9 +301,9 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
* @param sections List of sections. * @param sections List of sections.
* @return Promise resolved with current section. * @return Promise resolved with current section.
*/ */
async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> { async getCurrentSection<T = CoreCourseSection>(course: CoreCourseAnyCourseData, sections: T[]): Promise<T> {
try { try {
const section = await this.executeFunctionOnEnabled<CoreCourseSection>( const section = await this.executeFunctionOnEnabled<T>(
course.format || '', course.format || '',
'getCurrentSection', 'getCurrentSection',
[course, sections], [course, sections],
@ -293,6 +316,19 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
} }
} }
/**
* Returns the name for the highlighted section.
*
* @param course The course to get the text.
* @return The name for the highlighted section based on the given course format.
*/
getSectionHightlightedName(course: CoreCourseAnyCourseData): string | undefined {
return this.executeFunctionOnEnabled<string>(
course.format || '',
'getSectionHightlightedName',
);
}
/** /**
* Get the component to use to display a single section. This component will only be used if the user is viewing * Get 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. * a single section. If all the sections are displayed at once then it won't be used.

View File

@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses'; import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreCourseSection } from '../course-helper'; import { CoreCourseSection } from '../course-helper';
import { CoreCourseFormatHandler } from '../format-delegate'; import { CoreCourseFormatHandler } from '../format-delegate';
@ -65,7 +66,7 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
/** /**
* @inheritdoc * @inheritdoc
*/ */
displaySectionSelector(): boolean { displayCourseIndex(): boolean {
return true; return true;
} }
@ -106,6 +107,13 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
return sections[0]; return sections[0];
} }
/**
* @inheritdoc
*/
getSectionHightlightedName(): string {
return Translate.instant('core.course.highlighted');
}
/** /**
* @inheritdoc * @inheritdoc
*/ */

View File

@ -38,8 +38,9 @@ export class CoreSitePluginsCourseFormatHandler extends CoreSitePluginsBaseHandl
/** /**
* @inheritdoc * @inheritdoc
*/ */
displaySectionSelector(): boolean { displayCourseIndex(): boolean {
return this.handlerSchema.displaysectionselector ?? true; // Use displaysectionselector while is not completely deprecated.
return this.handlerSchema.displaycourseindex ?? this.handlerSchema.displaysectionselector ?? true;
} }
/** /**

View File

@ -884,7 +884,11 @@ export type CoreSitePluginsCourseModuleHandlerData = CoreSitePluginsHandlerCommo
export type CoreSitePluginsCourseFormatHandlerData = CoreSitePluginsHandlerCommonData & { export type CoreSitePluginsCourseFormatHandlerData = CoreSitePluginsHandlerCommonData & {
canviewallsections?: boolean; canviewallsections?: boolean;
displayenabledownload?: boolean; displayenabledownload?: boolean;
/**
* @deprecated on 4.0, use displaycourseindex instead.
*/
displaysectionselector?: boolean; displaysectionselector?: boolean;
displaycourseindex?: boolean;
}; };
/** /**

View File

@ -1118,7 +1118,7 @@ export class CoreDomUtilsProvider {
content.scrollToPoint(position[0], position[1], duration || 0); content.scrollToPoint(position[0], position[1], duration || 0);
return true; return true;
} catch (error) { } catch {
return false; return false;
} }
} }

View File

@ -16,6 +16,7 @@ information provided here is intended especially for developers.
The user handler function isEnabledForCourse is now called isEnabledForContext and receives a context + contextId instead of a courseId. The user handler function isEnabledForCourse is now called isEnabledForContext and receives a context + contextId instead of a courseId.
Some user handler's functions have also changed to accept context + contextId instead of a courseId: isEnabledForUser, getDisplayData, action. Some user handler's functions have also changed to accept context + contextId instead of a courseId: isEnabledForUser, getDisplayData, action.
- CoreCourseHelperProvider.openCourse parameters changed, now it admits CoreNavigationOptions + siteId on the same object that includes Params passed to page. - CoreCourseHelperProvider.openCourse parameters changed, now it admits CoreNavigationOptions + siteId on the same object that includes Params passed to page.
- displaySectionSelector has been deprecated on CoreCourseFormatHandler, use displayCourseIndex instead.
=== 3.9.5 === === 3.9.5 ===