MOBILE-3099 course: Add module navigation component

main
Pau Ferrer Ocaña 2021-12-03 13:45:23 +01:00
parent ad6c7367ff
commit a06f64832b
35 changed files with 490 additions and 14 deletions

View File

@ -1507,6 +1507,10 @@
"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.gotonextactivity": "local_moodlemobileapp",
"core.course.gotonextactivitynotfound": "local_moodlemobileapp",
"core.course.gotopreviousactivity": "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.insufficientavailablequota": "local_moodlemobileapp", "core.course.insufficientavailablequota": "local_moodlemobileapp",

View File

@ -147,3 +147,5 @@
[moduleId]="module.id"> [moduleId]="module.id">
</addon-mod-assign-submission> </addon-mod-assign-submission>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"></core-course-module-navigation>

View File

@ -52,3 +52,6 @@
</div> </div>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -27,8 +27,6 @@ import { AddonModChatUsersModalComponent } from './users-modal/users-modal';
CoreSharedModule, CoreSharedModule,
CoreCourseComponentsModule, CoreCourseComponentsModule,
], ],
providers: [
],
exports: [ exports: [
AddonModChatIndexComponent, AddonModChatIndexComponent,
AddonModChatUsersModalComponent, AddonModChatUsersModalComponent,

View File

@ -47,3 +47,6 @@
</ion-button> </ion-button>
</ng-container> </ng-container>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -155,6 +155,9 @@
</ion-card> </ion-card>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>
<!-- Template to render a choice option label. --> <!-- Template to render a choice option label. -->
<ng-template #optionLabelTemplate let-option="option"> <ng-template #optionLabelTemplate let-option="option">
<p> <p>

View File

@ -138,6 +138,9 @@
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAdd"> <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAdd">
<ion-fab-button (click)="gotoAddEntries()" [attr.aria-label]="'addon.mod_data.addentries' | translate"> <ion-fab-button (click)="gotoAddEntries()" [attr.aria-label]="'addon.mod_data.addentries' | translate">
<ion-icon name="fas-plus" aria-hidden="true"></ion-icon> <ion-icon name="fas-plus" aria-hidden="true"></ion-icon>

View File

@ -55,6 +55,9 @@
</core-tabs> </core-tabs>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>
<ng-template #basicInfo> <ng-template #basicInfo>
<ion-list *ngIf="access && access.canviewanalysis && !access.isempty"> <ion-list *ngIf="access && access.canviewanalysis && !access.isempty">
<ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)"> <ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">

View File

@ -48,3 +48,6 @@
[message]=" 'addon.mod_folder.emptyfilelist' | translate"></core-empty-box> [message]=" 'addon.mod_folder.emptyfilelist' | translate"></core-empty-box>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -140,6 +140,9 @@
</ng-container> </ng-container>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="forum && canAddDiscussion"> <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="forum && canAddDiscussion">
<ion-fab-button (click)="openNewDiscussion()" [attr.aria-label]="addDiscussionText"> <ion-fab-button (click)="openNewDiscussion()" [attr.aria-label]="addDiscussionText">
<ion-icon name="fas-plus" aria-hidden="true"></ion-icon> <ion-icon name="fas-plus" aria-hidden="true"></ion-icon>

View File

@ -96,6 +96,9 @@
</core-infinite-loading> </core-infinite-loading>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAdd"> <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAdd">
<ion-fab-button (click)="openNewEntry()" [attr.aria-label]="'addon.mod_glossary.addentry' | translate"> <ion-fab-button (click)="openNewEntry()" [attr.aria-label]="'addon.mod_glossary.addentry' | translate">
<ion-icon name="fas-plus" aria-hidden="true"></ion-icon> <ion-icon name="fas-plus" aria-hidden="true"></ion-icon>

View File

@ -84,3 +84,6 @@
[trackComponent]="trackComponent" [contextId]="h5pActivity?.context"> [trackComponent]="trackComponent" [contextId]="h5pActivity?.context">
</core-h5p-iframe> </core-h5p-iframe>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -45,7 +45,6 @@ import {
} from '../../services/h5pactivity-sync'; } from '../../services/h5pactivity-sync';
import { CoreFileHelper } from '@services/file-helper'; import { CoreFileHelper } from '@services/file-helper';
import { AddonModH5PActivityModuleHandlerService } from '../../services/handlers/module'; import { AddonModH5PActivityModuleHandlerService } from '../../services/handlers/module';
import { CoreMainMenuPage } from '@features/mainmenu/pages/menu/menu';
/** /**
* Component that displays an H5P activity entry page. * Component that displays an H5P activity entry page.
@ -87,7 +86,6 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
protected messageListenerFunction: (event: MessageEvent) => Promise<void>; protected messageListenerFunction: (event: MessageEvent) => Promise<void>;
constructor( constructor(
protected mainMenuPage: CoreMainMenuPage,
protected content?: IonContent, protected content?: IonContent,
@Optional() courseContentsPage?: CoreCourseContentsPage, @Optional() courseContentsPage?: CoreCourseContentsPage,
) { ) {

View File

@ -47,3 +47,6 @@
<core-iframe [src]="src"></core-iframe> <core-iframe [src]="src"></core-iframe>
</div> </div>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -297,3 +297,6 @@
</core-tab> </core-tab>
</core-tabs> </core-tabs>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -32,3 +32,6 @@
</ion-button> </ion-button>
</div> </div>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -48,3 +48,6 @@
</div> </div>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -226,3 +226,6 @@
</ion-list> </ion-list>
</ion-card> </ion-card>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -18,7 +18,7 @@
</core-navbar-buttons> </core-navbar-buttons>
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="safe-area-padding core-loading-fullheight"> <core-loading [hideUntil]="loaded" class="safe-area-padding">
<!-- Activity info. --> <!-- Activity info. -->
<core-course-module-info [module]="module" [courseId]="courseId" (completionChanged)="onCompletionChange()" <core-course-module-info [module]="module" [courseId]="courseId" (completionChanged)="onCompletionChange()"
@ -59,5 +59,7 @@
{{ 'core.openwith' | translate }} {{ 'core.openwith' | translate }}
</ion-button> </ion-button>
</ng-container> </ng-container>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -236,3 +236,6 @@
</ion-card> </ion-card>
</ng-container> </ng-container>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -147,3 +147,6 @@
</form> </form>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -13,7 +13,7 @@
</core-navbar-buttons> </core-navbar-buttons>
<!-- Content. --> <!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-fullheight"> <core-loading [hideUntil]="loaded">
<!-- Activity info. --> <!-- Activity info. -->
<core-course-module-info [module]="module" (completionChanged)="onCompletionChange()" [description]="displayDescription && description" <core-course-module-info [module]="module" (completionChanged)="onCompletionChange()" [description]="displayDescription && description"
@ -52,3 +52,6 @@
</ion-item> </ion-item>
</ion-list> </ion-list>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -89,6 +89,9 @@
</div> </div>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canEdit"> <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canEdit">
<ion-fab-button (click)="goToNewPage()" [attr.aria-label]="'addon.mod_wiki.createpage' | translate"> <ion-fab-button (click)="goToNewPage()" [attr.aria-label]="'addon.mod_wiki.createpage' | translate">
<ion-icon name="fas-plus" aria-hidden="true"></ion-icon> <ion-icon name="fas-plus" aria-hidden="true"></ion-icon>

View File

@ -253,3 +253,6 @@
</ion-card> </ion-card>
</div> </div>
</core-loading> </core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -45,13 +45,18 @@ export class CoreModuleHandlerBase implements Partial<CoreCourseModuleHandler> {
title: module.name, title: module.name,
class: 'addon-mod_' + module.modname + '-handler', class: 'addon-mod_' + module.modname + '-handler',
showDownloadButton: true, showDownloadButton: true,
action: (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void => { action: async (
event: Event,
module: CoreCourseModule,
courseId: number,
options?: CoreNavigationOptions,
): Promise<void> => {
options = options || {}; options = options || {};
options.params = options.params || {}; options.params = options.params || {};
Object.assign(options.params, { module }); Object.assign(options.params, { module });
const routeParams = '/' + courseId + '/' + module.id; const routeParams = '/' + courseId + '/' + module.id;
CoreNavigator.navigateToSitePath(this.pageName + routeParams, options); await CoreNavigator.navigateToSitePath(this.pageName + routeParams, options);
}, },
}; };
} }

View File

@ -26,6 +26,7 @@ import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsup
import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy'; import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy';
import { CoreCourseModuleInfoComponent } from './module-info/module-info'; import { CoreCourseModuleInfoComponent } from './module-info/module-info';
import { CoreCourseModuleManualCompletionComponent } from './module-manual-completion/module-manual-completion'; import { CoreCourseModuleManualCompletionComponent } from './module-manual-completion/module-manual-completion';
import { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -39,6 +40,7 @@ import { CoreCourseModuleManualCompletionComponent } from './module-manual-compl
CoreCourseSectionSelectorComponent, CoreCourseSectionSelectorComponent,
CoreCourseTagAreaComponent, CoreCourseTagAreaComponent,
CoreCourseUnsupportedModuleComponent, CoreCourseUnsupportedModuleComponent,
CoreCourseModuleNavigationComponent,
], ],
imports: [ imports: [
CoreBlockComponentsModule, CoreBlockComponentsModule,
@ -55,6 +57,7 @@ import { CoreCourseModuleManualCompletionComponent } from './module-manual-compl
CoreCourseSectionSelectorComponent, CoreCourseSectionSelectorComponent,
CoreCourseTagAreaComponent, CoreCourseTagAreaComponent,
CoreCourseUnsupportedModuleComponent, CoreCourseUnsupportedModuleComponent,
CoreCourseModuleNavigationComponent,
], ],
}) })
export class CoreCourseComponentsModule {} export class CoreCourseComponentsModule {}

View File

@ -0,0 +1,16 @@
<core-loading [hideUntil]="loaded" [fullscreen]="false">
<ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding" *ngIf="previousModule || nextModule">
<ion-col size="auto">
<ion-button fill="clear" class="core-course-previous-module" *ngIf="previousModule" (click)="goToActivity(false)"
[attr.aria-label]="'core.course.gotopreviousactivity' | translate">
<ion-icon name="fas-arrow-left" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-col>
<ion-col size="auto">
<ion-button fill="clear" class="core-course-next-module" *ngIf="nextModule" (click)="goToActivity(true)"
[attr.aria-label]="'core.course.gotonextactivity' | translate">
<ion-icon name="fas-arrow-right" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</core-loading>

View File

@ -0,0 +1,43 @@
@import "~theme/globals";
:host {
--height: var(--core-course-module-navigation-height, var(--core-course-module-navigation-max-height));
--background: var(--core-course-module-navigation-background);
height: var(--height);
width: 100%;
background-color: var(--background);
display: block;
bottom: 0;
z-index: 3;
box-shadow: 0px -3px 3px rgba(var(--drop-shadow));
@include core-transition(all, 200ms);
ion-col {
padding: 2px;
}
core-loading {
text-align: center;
}
ion-buttom {
margin-top: 5px;
margin-bottom: 5px;
}
core-loading {
--loading-inline-min-height: var(--height);
}
}
:host-context(.core-iframe-fullscreen) {
opacity: 0 !important;
height: 0 !important;
}
:host-context(core-course-format.core-course-format-singleactivity) {
opacity: 0 !important;
height: 0 !important;
}

View File

@ -0,0 +1,335 @@
// (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, OnDestroy, OnInit } from '@angular/core';
import { CoreCourse, CoreCourseProvider, CoreCourseWSSection } from '@features/course/services/course';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { IonContent } from '@ionic/angular';
import { ScrollDetail } from '@ionic/core';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
/**
* Component to show a button to go to the next resource/activity.
*
* Example usage:
* <core-course-module-navigation [courseId]="courseId" [currentModuleId]="module.id"></core-course-module-navigation>
*/
@Component({
selector: 'core-course-module-navigation',
templateUrl: 'core-course-module-navigation.html',
styleUrls: ['module-navigation.scss'],
})
export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
@Input() courseId!: number; // Course ID.
@Input() currentModuleId!: number; // Current module ID.
nextModule?: CoreCourseModule;
previousModule?: CoreCourseModule;
loaded = false;
protected element: HTMLElement;
protected initialHeight = 0;
protected initialPaddingBottom = 0;
protected previousTop = 0;
protected content?: HTMLIonContentElement | null;
protected completionObserver: CoreEventObserver;
constructor(el: ElementRef, protected ionContent: IonContent) {
const siteId = CoreSites.getCurrentSiteId();
this.element = el.nativeElement;
this.element.setAttribute('slot', 'fixed');
this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_MODULE_VIEWED, async (data) => {
if (data && data.courseId == this.courseId) {
// Check if now there's a next module.
await this.setNextAndPreviousModules(
CoreSitesReadingStrategy.PREFER_NETWORK,
!this.nextModule,
!this.previousModule,
);
}
}, siteId);
}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
try {
await this.setNextAndPreviousModules(CoreSitesReadingStrategy.PREFER_CACHE);
} finally {
this.loaded = true;
await CoreUtils.nextTicks(50);
this.listenScrollEvents();
}
}
/**
* Setup scroll event listener.
*
* @param retries Number of retries left.
*/
protected async listenScrollEvents(retries = 3): Promise<void> {
this.initialHeight = this.element.getBoundingClientRect().height;
if (this.initialHeight == 0 && retries > 0) {
await CoreUtils.nextTicks(50);
this.listenScrollEvents(retries - 1);
return;
}
// Set a minimum height value.
this.initialHeight = this.initialHeight || 56;
this.content = this.element.closest('ion-content');
if (!this.content) {
return;
}
// Special case where there's no navigation.
const courseFormat = this.element.closest('core-course-format.core-course-format-singleactivity');
if (courseFormat) {
this.element.remove();
this.ngOnDestroy();
return;
}
// Move element to the nearest ion-content if it's not the parent.
if (this.element.parentElement?.nodeName != 'ION-CONTENT') {
this.content.appendChild(this.element);
}
// Set a padding to not overlap elements.
this.initialPaddingBottom = parseFloat(this.content.style.getPropertyValue('--padding-bottom') || '0');
this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + this.initialHeight + 'px');
const scroll = await this.content.getScrollElement();
this.content.scrollEvents = true;
this.setBarHeight(this.initialHeight);
this.content.addEventListener('ionScroll', (e: CustomEvent<ScrollDetail>): void => {
if (!this.content) {
return;
}
this.onScroll(e.detail.scrollTop, scroll.scrollHeight - scroll.offsetHeight);
});
}
/**
* @inheritdoc
*/
async ngOnDestroy(): Promise<void> {
this.completionObserver.off();
this.content?.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px');
}
/**
* Set previous and next modules.
*
* @param readingStrategy Reading strategy.
* @param checkNext Check next module.
* @param checkPrevious Check previous module.
* @return Promise resolved when done.
*/
protected async setNextAndPreviousModules(
readingStrategy: CoreSitesReadingStrategy,
checkNext = true,
checkPrevious = true,
): Promise<void> {
if (!checkNext && !checkPrevious) {
return;
}
const preSets = CoreSites.getReadingStrategyPreSets(readingStrategy);
const sections = await CoreCourse.getSections(this.courseId, false, true, preSets);
// Search the next module.
let currentModuleIndex = -1;
const currentSectionIndex = sections.findIndex((section) => {
if (!this.isSectionAvailable(section)) {
// User cannot view the section, skip it.
return false;
}
currentModuleIndex = section.modules.findIndex((module: CoreCourseModule) => module.id == this.currentModuleId);
return currentModuleIndex >= 0;
});
if (currentSectionIndex < 0) {
// Nothing found. Return.
return;
}
if (checkNext) {
// Find next Module.
this.nextModule = undefined;
for (let i = currentSectionIndex; i < sections.length && this.nextModule == undefined; i++) {
const section = sections[i];
if (!this.isSectionAvailable(section)) {
// User cannot view the section, skip it.
continue;
}
const startModule = i == currentSectionIndex ? currentModuleIndex + 1 : 0;
for (let j = startModule; j < section.modules.length && this.nextModule == undefined; j++) {
const module = section.modules[j];
const found = await this.isModuleAvailable(module, section.id);
if (found) {
this.nextModule = module;
}
}
}
}
if (checkPrevious) {
// Find previous Module.
this.previousModule = undefined;
for (let i = currentSectionIndex; i >= 0 && this.previousModule == undefined; i--) {
const section = sections[i];
if (!this.isSectionAvailable(section)) {
// User cannot view the section, skip it.
continue;
}
const startModule = i == currentSectionIndex ? currentModuleIndex - 1 : section.modules.length - 1;
for (let j = startModule; j >= 0 && this.previousModule == undefined; j--) {
const module = section.modules[j];
const found = await this.isModuleAvailable(module, section.id);
if (found) {
this.previousModule = module;
}
}
}
}
}
/**
* Module is visible by the user and it has a specific view (e.g. not a label).
*
* @param module Module to check.
* @param sectionId Section ID the module belongs to.
* @return Wether the module is available to the user or not.
*/
protected async isModuleAvailable(module: CoreCourseModule, sectionId: number): Promise<boolean> {
if (module.uservisible === false || !CoreCourse.instance.moduleHasView(module)) {
return false;
}
if (!module.handlerData) {
module.handlerData =
await CoreCourseModuleDelegate.getModuleDataFor(module.modname, module, this.courseId, sectionId);
}
return !!module.handlerData?.action;
}
/**
* Section is visible by the user and its not stealth
*
* @param section Section to check.
* @return Wether the module is available to the user or not.
*/
protected isSectionAvailable(section: CoreCourseWSSection): boolean {
return section.uservisible !== false && section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
}
/**
* Go to next/previous module.
*
* @return Promise resolved when done.
*/
async goToActivity(next = true): Promise<void> {
if (!this.loaded) {
return;
}
const modal = await CoreDomUtils.showModalLoading();
// Re-calculate module in case a new module was made visible.
await CoreUtils.ignoreErrors(this.setNextAndPreviousModules(CoreSitesReadingStrategy.PREFER_NETWORK, next, !next));
modal.dismiss();
const module = next ? this.nextModule : this.previousModule;
if (!module) {
// It seems the module was hidden. Show a message.
CoreDomUtils.instance.showErrorModal(
next ? 'core.course.gotonextactivitynotfound' : 'core.course.gotopreviousactivitynotfound',
true,
);
return;
}
if (!module.handlerData?.action) {
return;
}
module.handlerData.action(new Event('click'), module, this.courseId, { replace: true });
}
/**
* On scroll function.
*
* @param top Scroll top measure.
* @param maxScroll Scroll height.
*/
protected onScroll(top: number, maxScroll: number): void {
if (top == 0 || top == maxScroll) {
// Reset.
this.setBarHeight(this.initialHeight);
} else {
const diffHeight = this.element.clientHeight - (top - this.previousTop);
this.setBarHeight(diffHeight);
}
this.previousTop = top;
}
/**
* Sets the bar height.
*
* @param height The new bar height.
*/
protected setBarHeight(height: number): void {
if (height <= 0) {
height = 0;
} else if (height > this.initialHeight) {
height = this.initialHeight;
}
this.element.style.opacity = height == 0 ? '0' : '1';
this.content?.style.setProperty('--core-course-module-navigation-height', height + 'px');
}
}

View File

@ -161,7 +161,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
button.action(event, this.module!, this.courseId!); button.action(event, this.module, this.courseId!);
} }
/** /**

View File

@ -27,6 +27,10 @@
"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. ",
"contents": "Contents", "contents": "Contents",
"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",

View File

@ -167,8 +167,9 @@ export interface CoreCourseModuleHandlerData {
* @param module The module object. * @param module The module object.
* @param courseId The course ID. * @param courseId The course ID.
* @param options Options for the navigation. * @param options Options for the navigation.
* @return Promise resolved when done.
*/ */
action?(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void; action?(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise<void> | void;
/** /**
* Updates the status of the module. * Updates the status of the module.
@ -236,8 +237,10 @@ export interface CoreCourseModuleHandlerButton {
* @param event The click event. * @param event The click event.
* @param module The module object. * @param module The module object.
* @param courseId The course ID. * @param courseId The course ID.
* @param options Options for the navigation.
* @return Promise resolved when done.
*/ */
action(event: Event, module: CoreCourseModule, courseId: number): void; action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise<void> | void;
} }
/** /**

View File

@ -28,6 +28,7 @@ import { CoreSitePluginsAssignSubmissionComponent } from './assign-submission/as
import { CoreSitePluginsWorkshopAssessmentStrategyComponent } from './workshop-assessment-strategy/workshop-assessment-strategy'; import { CoreSitePluginsWorkshopAssessmentStrategyComponent } from './workshop-assessment-strategy/workshop-assessment-strategy';
import { CoreSitePluginsBlockComponent } from './block/block'; import { CoreSitePluginsBlockComponent } from './block/block';
import { CoreSitePluginsOnlyTitleBlockComponent } from './only-title-block/only-title-block'; import { CoreSitePluginsOnlyTitleBlockComponent } from './only-title-block/only-title-block';
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -47,6 +48,7 @@ import { CoreSitePluginsOnlyTitleBlockComponent } from './only-title-block/only-
imports: [ imports: [
CoreSharedModule, CoreSharedModule,
CoreCompileHtmlComponentModule, CoreCompileHtmlComponentModule,
CoreCourseComponentsModule,
], ],
exports: [ exports: [
CoreSitePluginsPluginContentComponent, CoreSitePluginsPluginContentComponent,

View File

@ -11,8 +11,7 @@
</core-context-menu-item> </core-context-menu-item>
<core-context-menu-item [hidden]="!displayRefresh || ( <core-context-menu-item [hidden]="!displayRefresh || (
content?.compileComponent?.componentInstance?.displayRefresh === false)" [priority]="700" content?.compileComponent?.componentInstance?.displayRefresh === false)" [priority]="700"
[content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false">
[closeOnClick]="false">
</core-context-menu-item> </core-context-menu-item>
<core-context-menu-item [hidden]="!displayPrefetch || !prefetchStatusIcon || ( <core-context-menu-item [hidden]="!displayPrefetch || !prefetchStatusIcon || (
content?.compileComponent?.componentInstance?.displayPrefetch === false)" [priority]="600" [content]="prefetchText" content?.compileComponent?.componentInstance?.displayPrefetch === false)" [priority]="600" [content]="prefetchText"
@ -30,3 +29,5 @@
[initResult]="initResult" [data]="jsData" [pageTitle]="pageTitle" [preSets]="preSets" (onContentLoaded)="contentLoaded($event)" [initResult]="initResult" [data]="jsData" [pageTitle]="pageTitle" [preSets]="preSets" (onContentLoaded)="contentLoaded($event)"
(onLoadingContent)="contentLoading()"> (onLoadingContent)="contentLoading()">
</core-site-plugins-plugin-content> </core-site-plugins-plugin-content>
<core-course-module-navigation *ngIf="module" [courseId]="courseId" [currentModuleId]="module.id"></core-course-module-navigation>

View File

@ -257,6 +257,9 @@
--core-courseimage-on-course-height: 150px; --core-courseimage-on-course-height: 150px;
--core-course-module-navigation-max-height: 56px;
--core-course-module-navigation-background: var(--contrast-background);
--addon-calendar-event-category-color: var(--purple); --addon-calendar-event-category-color: var(--purple);
--addon-calendar-event-course-color: var(--red); --addon-calendar-event-course-color: var(--red);
--addon-calendar-event-group-color: var(--yellow); --addon-calendar-event-group-color: var(--yellow);