MOBILE-3099 course: Add module navigation component
parent
ad6c7367ff
commit
a06f64832b
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -27,8 +27,6 @@ import { AddonModChatUsersModalComponent } from './users-modal/users-modal';
|
||||||
CoreSharedModule,
|
CoreSharedModule,
|
||||||
CoreCourseComponentsModule,
|
CoreCourseComponentsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
|
||||||
],
|
|
||||||
exports: [
|
exports: [
|
||||||
AddonModChatIndexComponent,
|
AddonModChatIndexComponent,
|
||||||
AddonModChatUsersModalComponent,
|
AddonModChatUsersModalComponent,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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!);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue