forked from EVOgeek/Vmeda.Online
commit
994374c0b3
|
@ -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>
|
||||||
|
|
|
@ -35,20 +35,23 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
<div class="ion-padding safe-area-padding-horizontal">
|
<div class="safe-area-padding-horizontal">
|
||||||
<core-navigation-bar *ngIf="displayNavBar" [previous]="previousChapter?.id" [previousTitle]="previousNavBarTitle"
|
<core-navigation-bar *ngIf="displayNavBar" [items]="navigationItems" [showTitles]="displayTitlesInNavBar"
|
||||||
[next]="nextChapter?.id" [nextTitle]="nextNavBarTitle" (action)="changeChapter($event)">
|
previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)">
|
||||||
</core-navigation-bar>
|
</core-navigation-bar>
|
||||||
|
|
||||||
<core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent" contextLevel="module"
|
<div class="ion-padding">
|
||||||
[contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
|
<core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent" contextLevel="module"
|
||||||
<div class="ion-margin-top" *ngIf="tagsEnabled && tags?.length > 0">
|
[contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
|
||||||
<strong>{{ 'core.tag.tags' | translate }}: </strong>
|
|
||||||
<core-tag-list [tags]="tags"></core-tag-list>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<core-navigation-bar *ngIf="displayNavBar" [previous]="previousChapter?.id" [previousTitle]="previousNavBarTitle"
|
<div class="ion-margin-top" *ngIf="tagsEnabled && tags?.length > 0">
|
||||||
[next]="nextChapter?.id" [nextTitle]="nextNavBarTitle" (action)="changeChapter($event)"></core-navigation-bar>
|
<strong>{{ 'core.tag.tags' | translate }}: </strong>
|
||||||
|
<core-tag-list [tags]="tags"></core-tag-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
||||||
|
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
|
||||||
|
</core-course-module-navigation>
|
||||||
|
|
|
@ -26,11 +26,11 @@ import {
|
||||||
import { CoreTag, CoreTagItem } from '@features/tag/services/tag';
|
import { CoreTag, CoreTagItem } from '@features/tag/services/tag';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||||
import { Translate } from '@singletons';
|
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreCourse } from '@features/course/services/course';
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { AddonModBookTocComponent } from '../toc/toc';
|
import { AddonModBookTocComponent } from '../toc/toc';
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
|
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays a book.
|
* Component that displays a book.
|
||||||
|
@ -45,19 +45,16 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
|
|
||||||
component = AddonModBookProvider.COMPONENT;
|
component = AddonModBookProvider.COMPONENT;
|
||||||
chapterContent?: string;
|
chapterContent?: string;
|
||||||
previousChapter?: AddonModBookTocChapter;
|
|
||||||
nextChapter?: AddonModBookTocChapter;
|
|
||||||
tagsEnabled = false;
|
tagsEnabled = false;
|
||||||
displayNavBar = true;
|
|
||||||
previousNavBarTitle?: string;
|
|
||||||
nextNavBarTitle?: string;
|
|
||||||
warning = '';
|
warning = '';
|
||||||
tags?: CoreTagItem[];
|
tags?: CoreTagItem[];
|
||||||
|
displayNavBar = true;
|
||||||
|
navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
|
||||||
|
displayTitlesInNavBar = false;
|
||||||
|
|
||||||
protected chapters: AddonModBookTocChapter[] = [];
|
protected chapters: AddonModBookTocChapter[] = [];
|
||||||
protected currentChapter?: number;
|
protected currentChapter?: number;
|
||||||
protected book?: AddonModBookBookWSData;
|
protected book?: AddonModBookBookWSData;
|
||||||
protected displayTitlesInNavBar = false;
|
|
||||||
protected contentsMap: AddonModBookContentsMap = {};
|
protected contentsMap: AddonModBookContentsMap = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -148,14 +145,18 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof this.currentChapter == 'undefined') {
|
if (this.currentChapter === undefined) {
|
||||||
// Load the first chapter.
|
// Load the first chapter.
|
||||||
this.currentChapter = AddonModBook.getFirstChapter(this.chapters);
|
this.currentChapter = AddonModBook.getFirstChapter(this.chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.currentChapter === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Show chapter.
|
// Show chapter.
|
||||||
try {
|
try {
|
||||||
await this.loadChapter(this.currentChapter!, refresh);
|
await this.loadChapter(this.currentChapter, refresh);
|
||||||
|
|
||||||
this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : '';
|
this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : '';
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -199,15 +200,10 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
this.tags = this.tagsEnabled ? this.contentsMap[this.currentChapter].tags : [];
|
this.tags = this.tagsEnabled ? this.contentsMap[this.currentChapter].tags : [];
|
||||||
|
|
||||||
this.chapterContent = content;
|
this.chapterContent = content;
|
||||||
this.previousChapter = AddonModBook.getPreviousChapter(this.chapters, chapterId);
|
|
||||||
this.nextChapter = AddonModBook.getNextChapter(this.chapters, chapterId);
|
|
||||||
|
|
||||||
this.previousNavBarTitle = this.previousChapter && this.displayTitlesInNavBar
|
if (this.displayNavBar) {
|
||||||
? Translate.instant('addon.mod_book.navprevtitle', { $a: this.previousChapter.title })
|
this.navigationItems = this.getNavigationItems(chapterId);
|
||||||
: '';
|
}
|
||||||
this.nextNavBarTitle = this.nextChapter && this.displayTitlesInNavBar
|
|
||||||
? Translate.instant('addon.mod_book.navnexttitle', { $a: this.nextChapter.title })
|
|
||||||
: '';
|
|
||||||
|
|
||||||
// Chapter loaded, log view. We don't return the promise because we don't want to block the user for this.
|
// Chapter loaded, log view. We don't return the promise because we don't want to block the user for this.
|
||||||
await CoreUtils.ignoreErrors(AddonModBook.logView(
|
await CoreUtils.ignoreErrors(AddonModBook.logView(
|
||||||
|
@ -216,8 +212,11 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
this.module.name,
|
this.module.name,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const currentChapterIndex = this.chapters.findIndex((chapter) => chapter.id == chapterId);
|
||||||
|
const isLastChapter = currentChapterIndex < 0 || this.chapters[currentChapterIndex + 1] === undefined;
|
||||||
|
|
||||||
// Module is completed when last chapter is viewed, so we only check completion if the last is reached.
|
// Module is completed when last chapter is viewed, so we only check completion if the last is reached.
|
||||||
if (!this.nextChapter) {
|
if (isLastChapter) {
|
||||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -230,4 +229,19 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts chapters to navigation items.
|
||||||
|
*
|
||||||
|
* @param chapterId Current chapter Id.
|
||||||
|
* @return Navigation items.
|
||||||
|
*/
|
||||||
|
protected getNavigationItems(chapterId: number): CoreNavigationBarItem<AddonModBookTocChapter>[] {
|
||||||
|
return this.chapters.map((chapter) => ({
|
||||||
|
item: chapter,
|
||||||
|
title: chapter.title,
|
||||||
|
current: chapter.id == chapterId,
|
||||||
|
enabled: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -216,36 +216,6 @@ export class AddonModBookProvider {
|
||||||
return chapters[0].id;
|
return chapters[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the next chapter to the given one.
|
|
||||||
*
|
|
||||||
* @param chapters The chapters list.
|
|
||||||
* @param chapterId The current chapter.
|
|
||||||
* @return The next chapter.
|
|
||||||
*/
|
|
||||||
getNextChapter(chapters: AddonModBookTocChapter[], chapterId: number): AddonModBookTocChapter | undefined {
|
|
||||||
const currentChapterIndex = chapters.findIndex((chapter) => chapter.id == chapterId);
|
|
||||||
|
|
||||||
if (currentChapterIndex >= 0 && typeof chapters[currentChapterIndex + 1] != 'undefined') {
|
|
||||||
return chapters[currentChapterIndex + 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the previous chapter to the given one.
|
|
||||||
*
|
|
||||||
* @param chapters The chapters list.
|
|
||||||
* @param chapterId The current chapter.
|
|
||||||
* @return The next chapter.
|
|
||||||
*/
|
|
||||||
getPreviousChapter(chapters: AddonModBookTocChapter[], chapterId: number): AddonModBookTocChapter | undefined {
|
|
||||||
const currentChapterIndex = chapters.findIndex((chapter) => chapter.id == chapterId);
|
|
||||||
|
|
||||||
if (currentChapterIndex > 0) {
|
|
||||||
return chapters[currentChapterIndex - 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the book toc as an array.
|
* Get the book toc as an array.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -40,10 +40,11 @@
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
<div class="addon-mod-imscp-container">
|
<div class="addon-mod-imscp-container">
|
||||||
<core-navigation-bar [previous]="previousItem" [next]="nextItem" (action)="loadItem($event)" [info]="description"
|
<core-navigation-bar [items]="navigationItems" (action)="loadItem($event)">
|
||||||
[title]="'core.description' | translate" [component]="component" [componentId]="componentId" contextLevel="module"
|
|
||||||
[contextInstanceId]="module.id" [courseId]="courseId">
|
|
||||||
</core-navigation-bar>
|
</core-navigation-bar>
|
||||||
<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>
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
.addon-mod-imscp-container {
|
.addon-mod-imscp-container {
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
import { Component, OnInit, Optional } from '@angular/core';
|
import { Component, OnInit, Optional } from '@angular/core';
|
||||||
import { CoreSilentError } from '@classes/errors/silenterror';
|
import { CoreSilentError } from '@classes/errors/silenterror';
|
||||||
|
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
|
||||||
import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component';
|
import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component';
|
||||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||||
import { CoreCourse } from '@features/course/services/course';
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
@ -32,22 +33,19 @@ import { AddonModImscpTocComponent } from '../toc/toc';
|
||||||
export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit {
|
export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit {
|
||||||
|
|
||||||
component = AddonModImscpProvider.COMPONENT;
|
component = AddonModImscpProvider.COMPONENT;
|
||||||
|
|
||||||
items: AddonModImscpTocItem[] = [];
|
|
||||||
currentItem?: string;
|
|
||||||
src = '';
|
src = '';
|
||||||
warning = '';
|
warning = '';
|
||||||
|
navigationItems: CoreNavigationBarItem<AddonModImscpTocItem>[] = [];
|
||||||
|
|
||||||
// Initialize empty previous/next to prevent showing arrows for an instant before they're hidden.
|
protected items: AddonModImscpTocItem[] = [];
|
||||||
previousItem = '';
|
protected currentHref?: string;
|
||||||
nextItem = '';
|
|
||||||
|
|
||||||
constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) {
|
constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) {
|
||||||
super('AddonModImscpIndexComponent', courseContentsPage);
|
super('AddonModImscpIndexComponent', courseContentsPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
|
@ -90,19 +88,19 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
|
||||||
|
|
||||||
this.items = AddonModImscp.createItemList(contents);
|
this.items = AddonModImscp.createItemList(contents);
|
||||||
|
|
||||||
if (this.items.length && typeof this.currentItem == 'undefined') {
|
if (this.items.length && this.currentHref === undefined) {
|
||||||
this.currentItem = this.items[0].href;
|
this.currentHref = this.items[0].href;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.loadItem(this.currentItem);
|
await this.loadItemHref(this.currentHref);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_imscp.deploymenterror', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_imscp.deploymenterror', true);
|
||||||
|
|
||||||
throw new CoreSilentError(error);
|
throw new CoreSilentError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.warning = downloadResult!.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult!.error!) : '';
|
this.warning = downloadResult.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : '';
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
// Pass false because downloadResourceIfNeeded already invalidates and refresh data if refresh=true.
|
// Pass false because downloadResourceIfNeeded already invalidates and refresh data if refresh=true.
|
||||||
|
@ -113,14 +111,18 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
|
||||||
/**
|
/**
|
||||||
* Loads an item.
|
* Loads an item.
|
||||||
*
|
*
|
||||||
* @param itemId Item ID.
|
* @param itemHref Item Href.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async loadItem(itemId?: string): Promise<void> {
|
async loadItemHref(itemHref?: string): Promise<void> {
|
||||||
const src = await AddonModImscp.getIframeSrc(this.module, itemId);
|
const src = await AddonModImscp.getIframeSrc(this.module, itemHref);
|
||||||
this.currentItem = itemId;
|
this.currentHref = itemHref;
|
||||||
this.previousItem = itemId ? AddonModImscp.getPreviousItem(this.items, itemId) : '';
|
|
||||||
this.nextItem = itemId ? AddonModImscp.getNextItem(this.items, itemId) : '';
|
this.navigationItems = this.items.map((item) => ({
|
||||||
|
item: item,
|
||||||
|
current: item.href == this.currentHref,
|
||||||
|
enabled: !!item.href,
|
||||||
|
}));
|
||||||
|
|
||||||
if (this.src && src == this.src) {
|
if (this.src && src == this.src) {
|
||||||
// Re-loading same page. Set it to empty and then re-set the src in the next digest so it detects it has changed.
|
// Re-loading same page. Set it to empty and then re-set the src in the next digest so it detects it has changed.
|
||||||
|
@ -133,6 +135,15 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an item.
|
||||||
|
*
|
||||||
|
* @param item Item.
|
||||||
|
*/
|
||||||
|
loadItem(item: AddonModImscpTocItem): void {
|
||||||
|
this.loadItemHref(item.href);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the TOC.
|
* Show the TOC.
|
||||||
*/
|
*/
|
||||||
|
@ -142,12 +153,12 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
|
||||||
component: AddonModImscpTocComponent,
|
component: AddonModImscpTocComponent,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
items: this.items,
|
items: this.items,
|
||||||
selected: this.currentItem,
|
selected: this.currentHref,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (modalData) {
|
if (modalData) {
|
||||||
this.loadItem(modalData);
|
this.loadItemHref(modalData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,63 +70,6 @@ export class AddonModImscpProvider {
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the previous item to the given one.
|
|
||||||
*
|
|
||||||
* @param items The items list.
|
|
||||||
* @param itemId The current item.
|
|
||||||
* @return The previous item id.
|
|
||||||
*/
|
|
||||||
getPreviousItem(items: AddonModImscpTocItem[], itemId: string): string {
|
|
||||||
const position = this.getItemPosition(items, itemId);
|
|
||||||
|
|
||||||
if (position == -1) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = position - 1; i >= 0; i--) {
|
|
||||||
if (items[i] && items[i].href) {
|
|
||||||
return items[i].href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the next item to the given one.
|
|
||||||
*
|
|
||||||
* @param items The items list.
|
|
||||||
* @param itemId The current item.
|
|
||||||
* @return The next item id.
|
|
||||||
*/
|
|
||||||
getNextItem(items: AddonModImscpTocItem[], itemId: string): string {
|
|
||||||
const position = this.getItemPosition(items, itemId);
|
|
||||||
|
|
||||||
if (position == -1) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = position + 1; i < items.length; i++) {
|
|
||||||
if (items[i] && items[i].href) {
|
|
||||||
return items[i].href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the position of a item.
|
|
||||||
*
|
|
||||||
* @param items The items list.
|
|
||||||
* @param itemId The item to search.
|
|
||||||
* @return The item position.
|
|
||||||
*/
|
|
||||||
protected getItemPosition(items: AddonModImscpTocItem[], itemId: string): number {
|
|
||||||
return items.findIndex((item) => item.href == itemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if we should ommit the file download.
|
* Check if we should ommit the file download.
|
||||||
*
|
*
|
||||||
|
@ -242,7 +185,7 @@ export class AddonModImscpProvider {
|
||||||
const siteId = CoreSites.getCurrentSiteId();
|
const siteId = CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dirPath = await CoreFilepool.getPackageDirUrlByUrl(siteId, module!.url!);
|
const dirPath = await CoreFilepool.getPackageDirUrlByUrl(siteId, module.url!);
|
||||||
|
|
||||||
return CoreTextUtils.concatenatePaths(dirPath, itemHref);
|
return CoreTextUtils.concatenatePaths(dirPath, itemHref);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -39,6 +39,7 @@ export class AddonModLabelModuleHandlerService extends CoreModuleHandlerBase imp
|
||||||
[CoreConstants.FEATURE_GRADE_OUTCOMES]: false,
|
[CoreConstants.FEATURE_GRADE_OUTCOMES]: false,
|
||||||
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
|
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
|
||||||
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
|
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
|
||||||
|
[CoreConstants.FEATURE_NO_VIEW_LINK]: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -20,8 +20,8 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<core-loading [hideUntil]="loaded" class="core-loading-fullheight">
|
<core-loading [hideUntil]="loaded">
|
||||||
<core-navigation-bar [previous]="previousSco" [next]="nextSco" (action)="loadSco($event)"></core-navigation-bar>
|
<core-navigation-bar [items]="navigationItems" (action)="loadSco($event)"></core-navigation-bar>
|
||||||
|
|
||||||
<core-iframe *ngIf="loaded && src" [src]="src" [iframeWidth]="scormWidth" [iframeHeight]="scormHeight"
|
<core-iframe *ngIf="loaded && src" [src]="src" [iframeWidth]="scormWidth" [iframeHeight]="scormHeight"
|
||||||
[showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="true">
|
[showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="true">
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||||
|
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
|
||||||
import { CoreMainMenuPage } from '@features/mainmenu/pages/menu/menu';
|
import { CoreMainMenuPage } from '@features/mainmenu/pages/menu/menu';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||||
|
@ -50,8 +51,6 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
|
||||||
loadingToc = true; // Whether the TOC is being loaded.
|
loadingToc = true; // Whether the TOC is being loaded.
|
||||||
toc: AddonModScormTOCScoWithIcon[] = []; // List of SCOs.
|
toc: AddonModScormTOCScoWithIcon[] = []; // List of SCOs.
|
||||||
loaded = false; // Whether the data has been loaded.
|
loaded = false; // Whether the data has been loaded.
|
||||||
previousSco?: AddonModScormScoWithData; // Previous SCO.
|
|
||||||
nextSco?: AddonModScormScoWithData; // Next SCO.
|
|
||||||
src?: string; // Iframe src.
|
src?: string; // Iframe src.
|
||||||
errorMessage?: string; // Error message.
|
errorMessage?: string; // Error message.
|
||||||
accessInfo?: AddonModScormGetScormAccessInformationWSResponse; // Access information.
|
accessInfo?: AddonModScormGetScormAccessInformationWSResponse; // Access information.
|
||||||
|
@ -60,6 +59,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
|
||||||
incomplete = false; // Whether last attempt is incomplete.
|
incomplete = false; // Whether last attempt is incomplete.
|
||||||
cmId!: number; // Course module ID.
|
cmId!: number; // Course module ID.
|
||||||
courseId!: number; // Course ID.
|
courseId!: number; // Course ID.
|
||||||
|
navigationItems: CoreNavigationBarItem<AddonModScormTOCScoWithIcon>[] = [];
|
||||||
|
|
||||||
protected siteId!: string;
|
protected siteId!: string;
|
||||||
protected mode!: string; // Mode to play the SCORM.
|
protected mode!: string; // Mode to play the SCORM.
|
||||||
|
@ -110,6 +110,8 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
|
||||||
await this.fetchData();
|
await this.fetchData();
|
||||||
|
|
||||||
if (!this.currentSco) {
|
if (!this.currentSco) {
|
||||||
|
CoreNavigator.back();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,14 +178,20 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
|
||||||
}, this.siteId);
|
}, this.siteId);
|
||||||
|
|
||||||
this.launchNextObserver = CoreEvents.on(AddonModScormProvider.LAUNCH_NEXT_SCO_EVENT, (data) => {
|
this.launchNextObserver = CoreEvents.on(AddonModScormProvider.LAUNCH_NEXT_SCO_EVENT, (data) => {
|
||||||
if (data.scormId === this.scorm.id && this.nextSco) {
|
if (data.scormId === this.scorm.id && this.currentSco) {
|
||||||
this.loadSco(this.nextSco);
|
const nextSco = AddonModScormHelper.getNextScoFromToc(this.toc, this.currentSco.id);
|
||||||
|
if (nextSco) {
|
||||||
|
this.loadSco(nextSco);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, this.siteId);
|
}, this.siteId);
|
||||||
|
|
||||||
this.launchPrevObserver = CoreEvents.on(AddonModScormProvider.LAUNCH_PREV_SCO_EVENT, (data) => {
|
this.launchPrevObserver = CoreEvents.on(AddonModScormProvider.LAUNCH_PREV_SCO_EVENT, (data) => {
|
||||||
if (data.scormId === this.scorm.id && this.previousSco) {
|
if (data.scormId === this.scorm.id && this.currentSco) {
|
||||||
this.loadSco(this.previousSco);
|
const previousSco = AddonModScormHelper.getPreviousScoFromToc(this.toc, this.currentSco.id);
|
||||||
|
if (previousSco) {
|
||||||
|
this.loadSco(previousSco);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, this.siteId);
|
}, this.siteId);
|
||||||
|
|
||||||
|
@ -211,9 +219,16 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
|
||||||
*
|
*
|
||||||
* @param scoId Current SCO ID.
|
* @param scoId Current SCO ID.
|
||||||
*/
|
*/
|
||||||
protected calculateNextAndPreviousSco(scoId: number): void {
|
protected calculateNavigationItems(scoId: number): void {
|
||||||
this.previousSco = AddonModScormHelper.getPreviousScoFromToc(this.toc, scoId);
|
this.navigationItems = this.toc
|
||||||
this.nextSco = AddonModScormHelper.getNextScoFromToc(this.toc, scoId);
|
.filter((item) => item.isvisible)
|
||||||
|
.map<CoreNavigationBarItem<AddonModScormTOCScoWithIcon>>((item) =>
|
||||||
|
({
|
||||||
|
item: item,
|
||||||
|
title: item.title,
|
||||||
|
current: item.id == scoId,
|
||||||
|
enabled: !!(item.prereq && item.launch),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -398,7 +413,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
|
||||||
this.currentSco = sco;
|
this.currentSco = sco;
|
||||||
this.title = sco.title || this.scorm.name; // Try to use SCO title.
|
this.title = sco.title || this.scorm.name; // Try to use SCO title.
|
||||||
|
|
||||||
this.calculateNextAndPreviousSco(sco.id);
|
this.calculateNavigationItems(sco.id);
|
||||||
|
|
||||||
// Load the SCO source.
|
// Load the SCO source.
|
||||||
this.loadScoSrc(sco);
|
this.loadScoSrc(sco);
|
||||||
|
@ -540,7 +555,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being destroyed.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
// Empty src when leaving the state so unload event is triggered in the iframe.
|
// Empty src when leaving the state so unload event is triggered in the iframe.
|
||||||
|
|
|
@ -280,18 +280,13 @@ export class AddonModScormHelperProvider {
|
||||||
* @return Next SCO.
|
* @return Next SCO.
|
||||||
*/
|
*/
|
||||||
getNextScoFromToc(toc: AddonModScormScoWithData[], scoId: number): AddonModScormScoWithData | undefined {
|
getNextScoFromToc(toc: AddonModScormScoWithData[], scoId: number): AddonModScormScoWithData | undefined {
|
||||||
for (let i = 0; i < toc.length; i++) {
|
const currentTocIndex = toc.findIndex((item) => item.id == scoId);
|
||||||
if (toc[i].id != scoId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We found the current SCO. Now search the next visible SCO with fulfilled prerequisites.
|
// We found the current SCO. Now search the next visible SCO with fulfilled prerequisites.
|
||||||
for (let j = i + 1; j < toc.length; j++) {
|
for (let j = currentTocIndex + 1; j < toc.length; j++) {
|
||||||
if (toc[j].isvisible && toc[j].prereq && toc[j].launch) {
|
if (toc[j].isvisible && toc[j].prereq && toc[j].launch) {
|
||||||
return toc[j];
|
return toc[j];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,18 +298,13 @@ export class AddonModScormHelperProvider {
|
||||||
* @return Previous SCO.
|
* @return Previous SCO.
|
||||||
*/
|
*/
|
||||||
getPreviousScoFromToc(toc: AddonModScormScoWithData[], scoId: number): AddonModScormScoWithData | undefined {
|
getPreviousScoFromToc(toc: AddonModScormScoWithData[], scoId: number): AddonModScormScoWithData | undefined {
|
||||||
for (let i = 0; i < toc.length; i++) {
|
const currentTocIndex = toc.findIndex((item) => item.id == scoId);
|
||||||
if (toc[i].id != scoId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We found the current SCO. Now let's search the previous visible SCO with fulfilled prerequisites.
|
// We found the current SCO. Now let's search the previous visible SCO with fulfilled prerequisites.
|
||||||
for (let j = i - 1; j >= 0; j--) {
|
for (let j = currentTocIndex - 1; j >= 0; j--) {
|
||||||
if (toc[j].isvisible && toc[j].prereq && toc[j].launch) {
|
if (toc[j].isvisible && toc[j].prereq && toc[j].launch) {
|
||||||
return toc[j];
|
return toc[j];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { CoreModuleHandlerBase } from '@features/course/classes/module-base-hand
|
||||||
import { CoreCourse } from '@features/course/services/course';
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { CoreCourseModule } from '@features/course/services/course-helper';
|
import { CoreCourseModule } from '@features/course/services/course-helper';
|
||||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
||||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
import { CoreNavigationOptions } from '@services/navigator';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
|
@ -90,12 +90,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
|
||||||
if (shouldOpen) {
|
if (shouldOpen) {
|
||||||
openUrl(module, courseId);
|
openUrl(module, courseId);
|
||||||
} else {
|
} else {
|
||||||
options = options || {};
|
this.openActivityPage(module, courseId, options);
|
||||||
options.params = options.params || {};
|
|
||||||
Object.assign(options.params, { module });
|
|
||||||
const routeParams = '/' + courseId + '/' + module.id;
|
|
||||||
|
|
||||||
CoreNavigator.navigateToSitePath(AddonModUrlModuleHandlerService.PAGE_NAME + routeParams, options);
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
modal.dismiss();
|
modal.dismiss();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-loading-content {
|
.core-loading-content {
|
||||||
|
display: contents;
|
||||||
@include core-transition(opacity, 200ms);
|
@include core-transition(opacity, 200ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,10 +52,6 @@
|
||||||
@include margin(10px, 0, 0, 0);
|
@include margin(10px, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.core-loading-fullheight .core-loading-content {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.core-loading-loaded {
|
&.core-loading-loaded {
|
||||||
position: unset;
|
position: unset;
|
||||||
display: contents;
|
display: contents;
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
<ion-grid class="ion-no-padding ion-padding-bottom" *ngIf="previous || info || next">
|
<ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding"
|
||||||
<ion-row>
|
*ngIf="previousIndex >= 0 || nextIndex >= 0 || items.length > 1">
|
||||||
<ion-col class="ion-text-start ion-padding-end" [size]="info ? 4 : 6">
|
<ion-col class="ion-text-start ion-padding-end" [size]="showTitles ? 4 : 3">
|
||||||
<ion-button *ngIf="previous" class="core-navigation-bar-arrow" fill="outline"
|
<ion-button *ngIf="previousIndex >=0" class="core-navigation-bar-arrow" fill="clear" [attr.aria-label]="previousTitle"
|
||||||
[attr.aria-label]="previousTitle || ('core.previous' | translate)" (click)="action?.emit(previous)">
|
(click)="navigate(previousIndex)">
|
||||||
<ion-icon name="fas-arrow-left" [slot]="previousTitle ? 'start' : 'icon-only'" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-arrow-left" [slot]="showTitles ? 'start' : 'icon-only'" aria-hidden="true"></ion-icon>
|
||||||
<core-format-text *ngIf="previousTitle" [text]="previousTitle" [component]="component" [componentId]="componentId"
|
<core-format-text *ngIf="showTitles" [text]="previousTitle" [component]="component" [componentId]="componentId"
|
||||||
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" aria-hidden="true">
|
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" aria-hidden="true">
|
||||||
</core-format-text>
|
</core-format-text>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-col>
|
</ion-col>
|
||||||
<ion-col class="ion-text-center" size="4" *ngIf="info">
|
<ion-col class="ion-text-center" [size]="showTitles ? 4 : 6">
|
||||||
<ion-button fill="clear" (click)="showInfo()" [attr.aria-label]="title">
|
<ion-range min="0" [max]="items.length -1" debounce="500" snaps="true" (ionChange)="navigateOnRange($event.target)"
|
||||||
<ion-icon slot="icon-only" name="fas-info-circle" aria-hidden="true"></ion-icon>
|
[value]="currentIndex">
|
||||||
</ion-button>
|
<p slot="end">{{currentIndex + 1}} / {{items.length}}</p>
|
||||||
</ion-col>
|
</ion-range>
|
||||||
<ion-col class="ion-text-end ion-padding-start" [size]="info ? 4 : 6">
|
</ion-col>
|
||||||
<ion-button *ngIf="next" class="core-navigation-bar-arrow" [attr.aria-label]="nextTitle || ('core.next' | translate)"
|
<ion-col class="ion-text-end ion-padding-start" [size]="showTitles ? 4 : 3">
|
||||||
(click)="action?.emit(next)">
|
<ion-button fill="clear" *ngIf="nextIndex >= 0" class="core-navigation-bar-arrow" [attr.aria-label]="nextTitle"
|
||||||
<core-format-text *ngIf="nextTitle" [text]="nextTitle" [component]="component" [componentId]="componentId"
|
(click)="navigate(nextIndex)">
|
||||||
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" aria-hidden="true">
|
<core-format-text *ngIf="showTitles" [text]="nextTitle" [component]="component" [componentId]="componentId"
|
||||||
</core-format-text>
|
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" aria-hidden="true">
|
||||||
<ion-icon name="fas-arrow-right" [slot]="nextTitle ? 'end' : 'icon-only'" aria-hidden="true"></ion-icon>
|
</core-format-text>
|
||||||
</ion-button>
|
<ion-icon name="fas-arrow-right" [slot]="showTitles ? 'end' : 'icon-only'" aria-hidden="true"></ion-icon>
|
||||||
</ion-col>
|
</ion-button>
|
||||||
</ion-row>
|
</ion-col>
|
||||||
</ion-grid>
|
</ion-row>
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
.core-navigation-bar-arrow {
|
:host {
|
||||||
text-transform: none;
|
--background: var(--core-course-module-navigation-background);
|
||||||
max-width: 100%;
|
|
||||||
ion-icon {
|
width: 100%;
|
||||||
flex-shrink: 0;
|
background-color: var(--background);
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.core-navigation-bar-arrow {
|
||||||
|
text-transform: none;
|
||||||
|
max-width: 100%;
|
||||||
|
ion-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,48 +12,103 @@
|
||||||
// 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, EventEmitter, Input, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange } from '@angular/core';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { Translate } from '@singletons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to show a "bar" with arrows to navigate forward/backward and a "info" icon to display more data.
|
* Component to show a "bar" with arrows to navigate forward/backward and an slider to move around.
|
||||||
*
|
*
|
||||||
* This directive will show two arrows at the left and right of the screen to navigate to previous/next item when clicked.
|
* This directive will show two arrows at the left and right of the screen to navigate to previous/next item when clicked.
|
||||||
* If no previous/next item is defined, that arrow won't be shown. It will also show a button to show more info.
|
* If no previous/next item is defined, that arrow won't be shown.
|
||||||
*
|
*
|
||||||
* Example usage:
|
* Example usage:
|
||||||
* <core-navigation-bar [previous]="prevItem" [next]="nextItem" (action)="goTo($event)"></core-navigation-bar>
|
* <core-navigation-bar [items]="items" (action)="goTo($event)"></core-navigation-bar>
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'core-navigation-bar',
|
selector: 'core-navigation-bar',
|
||||||
templateUrl: 'core-navigation-bar.html',
|
templateUrl: 'core-navigation-bar.html',
|
||||||
styleUrls: ['navigation-bar.scss'],
|
styleUrls: ['navigation-bar.scss'],
|
||||||
})
|
})
|
||||||
export class CoreNavigationBarComponent {
|
export class CoreNavigationBarComponent implements OnChanges {
|
||||||
|
|
||||||
@Input() previous?: unknown; // Previous item. If not defined, the previous arrow won't be shown.
|
@Input() items: CoreNavigationBarItem[] = []; // List of items.
|
||||||
@Input() previousTitle?: string; // Previous item title. If not defined, only the arrow will be shown.
|
@Input() showTitles = false; // Display titles on buttons.
|
||||||
@Input() next?: unknown; // Next item. If not defined, the next arrow won't be shown.
|
@Input() previousTranslate = 'core.previous'; // Previous translatable text, can admit $a variable.
|
||||||
@Input() nextTitle?: string; // Next item title. If not defined, only the arrow will be shown.
|
@Input() nextTranslate = 'core.next'; // Next translatable text, can admit $a variable.
|
||||||
@Input() info = ''; // Info to show when clicking the info button. If not defined, the info button won't be shown.
|
|
||||||
@Input() title = ''; // Title to show when seeing the info (new page).
|
|
||||||
@Input() component?: string; // Component the bar belongs to.
|
@Input() component?: string; // Component the bar belongs to.
|
||||||
@Input() componentId?: number; // Component ID.
|
@Input() componentId?: number; // Component ID.
|
||||||
@Input() contextLevel?: string; // The context level.
|
@Input() contextLevel?: string; // The context level.
|
||||||
@Input() contextInstanceId?: number; // The instance ID related to the context.
|
@Input() contextInstanceId?: number; // The instance ID related to the context.
|
||||||
@Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters.
|
@Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters.
|
||||||
@Output() action?: EventEmitter<unknown> =
|
|
||||||
new EventEmitter<unknown>(); // Function to call when arrow is clicked. Will receive as a param the item to load.
|
|
||||||
|
|
||||||
showInfo(): void {
|
previousTitle?: string; // Previous item title.
|
||||||
CoreTextUtils.viewText(this.title, this.info, {
|
nextTitle?: string; // Next item title.
|
||||||
component: this.component,
|
previousIndex = -1; // Previous item index. If -1, the previous arrow won't be shown.
|
||||||
componentId: this.componentId,
|
nextIndex = -1; // Next item index. If -1, the next arrow won't be shown.
|
||||||
filter: true,
|
currentIndex = 0;
|
||||||
contextLevel: this.contextLevel,
|
|
||||||
instanceId: this.contextInstanceId,
|
// Function to call when arrow is clicked. Will receive as a param the item to load.
|
||||||
courseId: this.courseId,
|
@Output() action: EventEmitter<unknown> = new EventEmitter<unknown>();
|
||||||
});
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
|
||||||
|
if (!changes.items || !this.items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentIndex = this.items.findIndex((item) => item.current);
|
||||||
|
if (this.currentIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nextIndex = this.items[this.currentIndex + 1]?.enabled ? this.currentIndex + 1 : -1;
|
||||||
|
if (this.nextIndex >= 0) {
|
||||||
|
this.nextTitle = Translate.instant(this.nextTranslate, { $a: this.items[this.nextIndex].title || '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previousIndex = this.items[this.currentIndex - 1]?.enabled ? this.currentIndex - 1 : -1;
|
||||||
|
if (this.previousIndex >= 0) {
|
||||||
|
this.previousTitle = Translate.instant(this.previousTranslate, { $a: this.items[this.previousIndex].title || '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to an item.
|
||||||
|
*
|
||||||
|
* @param itemIndex Selected item index.
|
||||||
|
*/
|
||||||
|
navigate(itemIndex: number): void {
|
||||||
|
if (this.currentIndex == itemIndex || !this.items[itemIndex].enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentIndex = itemIndex;
|
||||||
|
this.action.emit(this.items[itemIndex].item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to an item with the range component.
|
||||||
|
*
|
||||||
|
* @param target: Element changed.
|
||||||
|
*/
|
||||||
|
navigateOnRange(target: HTMLIonRangeElement): void {
|
||||||
|
const selectedIndex = target.value as number; // Single value, use number.
|
||||||
|
if (!this.items[selectedIndex].enabled) {
|
||||||
|
target.value = this.currentIndex;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.navigate(selectedIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CoreNavigationBarItem<T = unknown> = {
|
||||||
|
item: T;
|
||||||
|
title?: string;
|
||||||
|
current: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
.menu,
|
.menu,
|
||||||
.content-outlet {
|
.content-outlet {
|
||||||
top: var(--offset-top);
|
top: var(--offset-top);
|
||||||
|
height: calc(100% - var(--offset-top));
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
@ -48,8 +48,12 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
||||||
this.header = el.nativeElement;
|
this.header = el.nativeElement;
|
||||||
|
|
||||||
this.loadingObserver = CoreEvents.on(CoreEvents.CORE_LOADING_CHANGED, async (data) => {
|
this.loadingObserver = CoreEvents.on(CoreEvents.CORE_LOADING_CHANGED, async (data) => {
|
||||||
|
if (!data.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const loadingId = await this.getLoadingId();
|
const loadingId = await this.getLoadingId();
|
||||||
if (loadingId && data.loaded && data.uniqueId == loadingId) {
|
if (loadingId && data.uniqueId == loadingId) {
|
||||||
// Remove event when loading is done.
|
// Remove event when loading is done.
|
||||||
this.loadingObserver.off();
|
this.loadingObserver.off();
|
||||||
|
|
||||||
|
@ -78,7 +82,7 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.content.querySelector('core-loading .core-loading-content')?.id;
|
return this.content.querySelector('core-loading.core-loading-loaded:not(.core-loading-inline) .core-loading-content')?.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
// 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 { Directive, ElementRef, OnDestroy } from '@angular/core';
|
import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { IonContent } from '@ionic/angular';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directive to move ion-fab components as direct children of the nearest ion-content.
|
* Directive to move ion-fab components as direct children of the nearest ion-content.
|
||||||
|
@ -25,47 +25,50 @@ import { IonContent } from '@ionic/angular';
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: 'ion-fab[core-fab]',
|
selector: 'ion-fab[core-fab]',
|
||||||
})
|
})
|
||||||
export class CoreFabDirective implements OnDestroy {
|
export class CoreFabDirective implements OnInit, OnDestroy {
|
||||||
|
|
||||||
protected static readonly PADDINGBOTTOM = 56;
|
|
||||||
|
|
||||||
protected scrollElement?: HTMLElement;
|
|
||||||
protected done = false;
|
|
||||||
protected element: HTMLElement;
|
protected element: HTMLElement;
|
||||||
|
protected content?: HTMLIonContentElement | null;
|
||||||
|
protected initialPaddingBottom = 0;
|
||||||
|
|
||||||
constructor(el: ElementRef, protected content: IonContent) {
|
constructor(el: ElementRef) {
|
||||||
this.element = el.nativeElement;
|
this.element = el.nativeElement;
|
||||||
this.asyncInit();
|
this.element.setAttribute('slot', 'fixed');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Component.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async asyncInit(): Promise<void> {
|
async ngOnInit(retries = 3): Promise<void> {
|
||||||
if (this.content) {
|
this.content = this.element.closest('ion-content');
|
||||||
this.scrollElement = await this.content.getScrollElement();
|
if (!this.content) {
|
||||||
if (!this.done) {
|
if(retries > 0) {
|
||||||
// Move element to the nearest ion-content if it's not the parent
|
await CoreUtils.nextTicks(50);
|
||||||
if (this.element.parentElement?.nodeName != 'ION-CONTENT') {
|
|
||||||
const ionContent = this.element.closest('ion-content');
|
|
||||||
ionContent?.appendChild(this.element);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add space at the bottom to let the user see the whole content.
|
this.ngOnInit(retries - 1);
|
||||||
const bottom = parseInt(this.scrollElement.style.paddingBottom, 10) || 0;
|
|
||||||
this.scrollElement.style.paddingBottom = (bottom + CoreFabDirective.PADDINGBOTTOM) + 'px';
|
|
||||||
this.done = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initialHeight = this.element.getBoundingClientRect().height || 56;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add space at the bottom to let the user see the whole content.
|
||||||
|
this.initialPaddingBottom = parseFloat(this.content.style.getPropertyValue('--padding-bottom') || '0');
|
||||||
|
this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + initialHeight + 'px');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy component.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
if (this.done && this.scrollElement) {
|
if (this.content) {
|
||||||
const bottom = parseInt(this.scrollElement.style.paddingBottom, 10) || 0;
|
this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px');
|
||||||
this.scrollElement.style.paddingBottom = (bottom - CoreFabDirective.PADDINGBOTTOM) + 'px';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,15 +45,37 @@ 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 (
|
||||||
options = options || {};
|
event: Event,
|
||||||
options.params = options.params || {};
|
module: CoreCourseModule,
|
||||||
Object.assign(options.params, { module });
|
courseId: number,
|
||||||
const routeParams = '/' + courseId + '/' + module.id;
|
options?: CoreNavigationOptions,
|
||||||
|
): Promise<void> => {
|
||||||
CoreNavigator.navigateToSitePath(this.pageName + routeParams, options);
|
await this.openActivityPage(module, courseId, options);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the activity page.
|
||||||
|
*
|
||||||
|
* @param module The module object.
|
||||||
|
* @param courseId The course ID.
|
||||||
|
* @param options Options for the navigation.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise<void> {
|
||||||
|
if (!CoreCourse.moduleHasView(module)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options = options || {};
|
||||||
|
options.params = options.params || {};
|
||||||
|
Object.assign(options.params, { module });
|
||||||
|
|
||||||
|
const routeParams = '/' + courseId + '/' + module.id;
|
||||||
|
|
||||||
|
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 {}
|
||||||
|
|
|
@ -13,24 +13,25 @@
|
||||||
<ion-item class="ion-text-wrap" *ngIf="description" lines="none">
|
<ion-item class="ion-text-wrap" *ngIf="description" lines="none">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<core-format-text [text]="description" [component]="component" [componentId]="componentId" contextLevel="module"
|
<core-format-text [text]="description" [component]="component" [componentId]="componentId" contextLevel="module"
|
||||||
[contextInstanceId]="module.id" [courseId]="courseId" [maxHeight]="120">
|
[contextInstanceId]="module.id" [courseId]="courseId" [maxHeight]="expandDescription ? null : 120">
|
||||||
</core-format-text>
|
</core-format-text>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ng-content select="[description]"></ng-content>
|
<ng-content select="[description]"></ng-content>
|
||||||
<ion-item class="ion-text-wrap" lines="none" *ngIf="showCompletion && (module.dates?.length || module.completiondata)">
|
<ion-item class="ion-text-wrap" lines="none" *ngIf="showCompletion && (module.dates?.length ||
|
||||||
|
(module.completiondata && (module.completiondata.isautomatic || showManualCompletion) && module.uservisible))">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<!-- Activity dates. -->
|
<!-- Activity dates. -->
|
||||||
<div *ngIf="module.dates && module.dates.length" class="core-module-dates">
|
<div *ngIf="module.dates?.length" class="core-module-dates">
|
||||||
<p *ngFor="let date of module.dates">
|
<p *ngFor="let date of module.dates">
|
||||||
<strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }}
|
<strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Module completion. -->
|
<!-- Module completion. -->
|
||||||
<core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata" [moduleName]="module.name"
|
<core-course-module-completion *ngIf="module.completiondata && module.uservisible" [completion]="module.completiondata"
|
||||||
[moduleId]="module.id" [showCompletionConditions]="true" [showManualCompletion]="true"
|
[moduleName]="module.name" [moduleId]="module.id" [showCompletionConditions]="true"
|
||||||
(completionChanged)="completionChanged.emit($event)">
|
[showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
|
||||||
</core-course-module-completion>
|
</core-course-module-completion>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
|
@ -36,12 +36,14 @@ import { CoreSites } from '@services/sites';
|
||||||
export class CoreCourseModuleInfoComponent implements OnInit {
|
export class CoreCourseModuleInfoComponent implements OnInit {
|
||||||
|
|
||||||
@Input() module!: CoreCourseModule; // The module to render.
|
@Input() module!: CoreCourseModule; // The module to render.
|
||||||
|
@Input() showManualCompletion = true; // Whether to show manual completion, true by default.
|
||||||
@Input() courseId!: number; // The courseId the module belongs to.
|
@Input() courseId!: number; // The courseId the module belongs to.
|
||||||
|
|
||||||
@Input() component!: string; // Component for format text directive.
|
@Input() component!: string; // Component for format text directive.
|
||||||
@Input() componentId!: string | number; // Component ID to use in conjunction with the component.
|
@Input() componentId!: string | number; // Component ID to use in conjunction with the component.
|
||||||
|
|
||||||
@Input() description?: string | false; // The description to display. If false, no description will be shown.
|
@Input() description?: string | false; // The description to display. If false, no description will be shown.
|
||||||
|
@Input() expandDescription = false; // If the description should be expanded by default.
|
||||||
|
|
||||||
@Input() hasDataToSync = false; // If the activity has any data to be synced.
|
@Input() hasDataToSync = false; // If the activity has any data to be synced.
|
||||||
|
|
||||||
|
|
|
@ -2,20 +2,22 @@
|
||||||
|
|
||||||
<ng-container *ngIf="completion.istrackeduser">
|
<ng-container *ngIf="completion.istrackeduser">
|
||||||
<ng-container *ngIf="completion.state">
|
<ng-container *ngIf="completion.state">
|
||||||
<ion-button color="success" fill="outline" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)">
|
<ion-button color="success" expand="block" fill="outline" [attr.aria-label]="accessibleDescription"
|
||||||
|
(click)="completionClicked($event)">
|
||||||
<ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon>
|
||||||
{{ 'core.course.completion_manual:done' | translate }}
|
{{ 'core.course.completion_manual:done' | translate }}
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!completion.state">
|
<ng-container *ngIf="!completion.state">
|
||||||
<ion-button color="light" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)">
|
<ion-button color="dark" expand="block" fill="outline" [attr.aria-label]="accessibleDescription"
|
||||||
|
(click)="completionClicked($event)">
|
||||||
{{ 'core.course.completion_manual:markdone' | translate }}
|
{{ 'core.course.completion_manual:markdone' | translate }}
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="!completion.istrackeduser">
|
<ng-container *ngIf="!completion.istrackeduser">
|
||||||
<ion-button disabled="true" color="light">
|
<ion-button disabled="true" color="dark" expand="block" fill="outline">
|
||||||
{{ 'core.course.completion_manual:markdone' | translate }}
|
{{ 'core.course.completion_manual:markdone' | translate }}
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
@ -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,338 @@
|
||||||
|
// (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, CoreCourseSection } 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 { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||||
|
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;
|
||||||
|
nextModuleSection?: CoreCourseSection;
|
||||||
|
previousModuleSection?: CoreCourseSection;
|
||||||
|
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);
|
||||||
|
if (found) {
|
||||||
|
this.nextModule = module;
|
||||||
|
this.nextModuleSection = section;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (found) {
|
||||||
|
this.previousModule = module;
|
||||||
|
this.previousModuleSection = section;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module is visible by the user and it has a specific view (e.g. not a label).
|
||||||
|
*
|
||||||
|
* @param module Module to check.
|
||||||
|
* @return Wether the module is available to the user or not.
|
||||||
|
*/
|
||||||
|
protected async isModuleAvailable(module: CoreCourseModule): Promise<boolean> {
|
||||||
|
return CoreCourse.instance.moduleHasView(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.uservisible === false) {
|
||||||
|
const section = next ? this.nextModuleSection : this.previousModuleSection;
|
||||||
|
const options: CoreNavigationOptions = {
|
||||||
|
replace: true,
|
||||||
|
params: {
|
||||||
|
module,
|
||||||
|
section,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
CoreNavigator.navigateToSitePath('course/' + this.courseId + '/' + module.id +'/module-preview', options);
|
||||||
|
} else {
|
||||||
|
CoreCourseModuleDelegate.openActivityPage(module.modname, 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -75,8 +75,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Module completion. -->
|
<!-- Module completion. -->
|
||||||
<core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata" [moduleName]="module.name"
|
<core-course-module-completion *ngIf="module.completiondata && module.uservisible" [completion]="module.completiondata"
|
||||||
[moduleId]="module.id" [showCompletionConditions]="showCompletionConditions"
|
[moduleName]="module.name" [moduleId]="module.id" [showCompletionConditions]="showCompletionConditions"
|
||||||
[showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
|
[showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
|
||||||
</core-course-module-completion>
|
</core-course-module-completion>
|
||||||
|
|
||||||
|
|
|
@ -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!);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
<core-course-module-info [description]="module?.description" [courseId]="courseId" [module]="module">
|
|
||||||
</core-course-module-info>
|
|
||||||
|
|
||||||
<div class="ion-padding">
|
<div class="ion-padding">
|
||||||
<h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2>
|
<h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2>
|
||||||
<h2 *ngIf="isDisabledInSite || !isSupportedByTheApp">{{ 'core.uhoh' | translate }}</h2>
|
<h2 *ngIf="isDisabledInSite || !isSupportedByTheApp">{{ 'core.uhoh' | translate }}</h2>
|
||||||
|
|
|
@ -23,9 +23,9 @@ const routes: Routes = [
|
||||||
loadChildren: () => import('./pages/index/index.module').then( m => m.CoreCourseIndexPageModule),
|
loadChildren: () => import('./pages/index/index.module').then( m => m.CoreCourseIndexPageModule),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':courseId/unsupported-module',
|
path: ':courseId/:cmId/module-preview',
|
||||||
loadChildren: () => import('./pages/unsupported-module/unsupported-module.module')
|
loadChildren: () => import('./pages/module-preview/module-preview.module')
|
||||||
.then( m => m.CoreCourseUnsupportedModulePageModule),
|
.then( m => m.CoreCourseModulePreviewPageModule),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':courseId/list-mod-type',
|
path: ':courseId/list-mod-type',
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
<ion-header collapsible>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>
|
||||||
|
<h1>
|
||||||
|
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</h1>
|
||||||
|
</ion-title>
|
||||||
|
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<core-context-menu>
|
||||||
|
<core-context-menu-item [priority]="900" *ngIf="module.url" [href]="module!.url"
|
||||||
|
[content]="'core.openinbrowser' | translate" iconAction="fas-external-link-alt">
|
||||||
|
</core-context-menu-item>
|
||||||
|
</core-context-menu>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
<core-course-module-info [module]="module" [courseId]="courseId" [description]="module.description" [component]="module.modname"
|
||||||
|
[componentId]="module.id" (completionChanged)="onCompletionChange()" [expandDescription]="true"
|
||||||
|
[showManualCompletion]="showManualCompletion">
|
||||||
|
|
||||||
|
<div class="safe-area-padding-horizontal ion-padding" *ngIf="module.handlerData?.extraBadge">
|
||||||
|
<ion-badge class="ion-text-wrap ion-text-start" [color]="module.handlerData?.extraBadgeColor">
|
||||||
|
<span [innerHTML]="module.handlerData?.extraBadge"></span>
|
||||||
|
</ion-badge>
|
||||||
|
</div>
|
||||||
|
<div class="safe-area-padding-horizontal ion-padding" *ngIf="module.visible === 0 && (!section || section.visible)">
|
||||||
|
<ion-badge class="ion-text-wrap">
|
||||||
|
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||||
|
</ion-badge>
|
||||||
|
</div>
|
||||||
|
<div class="safe-area-padding-horizontal ion-padding" *ngIf="module.visible !== 0 && module.isStealth">
|
||||||
|
<ion-badge class="ion-text-wrap">
|
||||||
|
{{ 'core.course.hiddenoncoursepage' | translate }}
|
||||||
|
</ion-badge>
|
||||||
|
</div>
|
||||||
|
<div class="safe-area-padding-horizontal ion-padding core-module-availabilityinfo" *ngIf="module.availabilityinfo">
|
||||||
|
<ion-badge class="ion-text-wrap">{{ 'core.restricted' | translate }}</ion-badge>
|
||||||
|
<div>
|
||||||
|
<core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id"
|
||||||
|
[courseId]="courseId" class="ion-text-wrap">
|
||||||
|
</core-format-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="safe-area-padding-horizontal ion-padding" *ngIf="module.completiondata?.offline">
|
||||||
|
<ion-badge color="warning" class="ion-text-wrap">
|
||||||
|
{{ 'core.course.manualcompletionnotsynced' | translate }}
|
||||||
|
</ion-badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<core-course-unsupported-module *ngIf="unsupported" [module]="module" [courseId]="courseId"></core-course-unsupported-module>
|
||||||
|
</core-course-module-info>
|
||||||
|
</core-loading>
|
||||||
|
|
||||||
|
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"></core-course-module-navigation>
|
||||||
|
</ion-content>
|
|
@ -16,13 +16,13 @@ import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
import { CoreSharedModule } from '@/core/shared.module';
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
import { CoreCourseUnsupportedModulePage } from './unsupported-module.page';
|
import { CoreCourseModulePreviewPage } from './module-preview.page';
|
||||||
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: CoreCourseUnsupportedModulePage,
|
component: CoreCourseModulePreviewPage,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -33,8 +33,8 @@ const routes: Routes = [
|
||||||
CoreCourseComponentsModule,
|
CoreCourseComponentsModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CoreCourseUnsupportedModulePage,
|
CoreCourseModulePreviewPage,
|
||||||
],
|
],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class CoreCourseUnsupportedModulePageModule {}
|
export class CoreCourseModulePreviewPageModule { }
|
|
@ -0,0 +1,118 @@
|
||||||
|
// (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, OnInit } from '@angular/core';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
import { CoreCourseHelper, CoreCourseModule, CoreCourseSection } from '@features/course/services/course-helper';
|
||||||
|
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||||
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays a module preview.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-core-course-module-preview',
|
||||||
|
templateUrl: 'module-preview.html',
|
||||||
|
})
|
||||||
|
export class CoreCourseModulePreviewPage implements OnInit {
|
||||||
|
|
||||||
|
title!: string;
|
||||||
|
module!: CoreCourseModule;
|
||||||
|
section?: CoreCourseSection; // The section the module belongs to.
|
||||||
|
courseId!: number;
|
||||||
|
loaded = false;
|
||||||
|
unsupported = false;
|
||||||
|
showManualCompletion = false;
|
||||||
|
|
||||||
|
protected debouncedUpdateModule?: () => void; // Update the module after a certain time.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.module = CoreNavigator.getRequiredRouteParam<CoreCourseModule>('module');
|
||||||
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
|
this.section = CoreNavigator.getRouteParam<CoreCourseSection>('section');
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
CoreNavigator.back();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.debouncedUpdateModule = CoreUtils.debounce(() => {
|
||||||
|
this.doRefresh();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
await this.fetchModule();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch module.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchModule(refresh = false): Promise<void> {
|
||||||
|
if (refresh) {
|
||||||
|
this.module = await CoreCourse.getModule(this.module.id, this.courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreCourseHelper.calculateModuleCompletionData(this.module, this.courseId);
|
||||||
|
|
||||||
|
await CoreCourseHelper.loadModuleOfflineCompletion(this.courseId, this.module);
|
||||||
|
|
||||||
|
this.unsupported = !CoreCourseModuleDelegate.getHandlerName(this.module.modname);
|
||||||
|
if (!this.unsupported) {
|
||||||
|
this.module.handlerData =
|
||||||
|
await CoreCourseModuleDelegate.getModuleDataFor(this.module.modname, this.module, this.courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.title = this.module.name;
|
||||||
|
|
||||||
|
this.showManualCompletion = await CoreCourseModuleDelegate.manualCompletionAlwaysShown(this.module);
|
||||||
|
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async doRefresh(refresher?: IonRefresher): Promise<void> {
|
||||||
|
|
||||||
|
await CoreCourse.invalidateModule(this.module.id);
|
||||||
|
|
||||||
|
this.fetchModule(true);
|
||||||
|
|
||||||
|
refresher?.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The completion of the modules has changed.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async onCompletionChange(): Promise<void> {
|
||||||
|
// Update the module data after a while.
|
||||||
|
this.debouncedUpdateModule?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,27 +0,0 @@
|
||||||
<ion-header collapsible>
|
|
||||||
<ion-toolbar>
|
|
||||||
<ion-buttons slot="start">
|
|
||||||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
|
||||||
</ion-buttons>
|
|
||||||
<ion-title>
|
|
||||||
<h1>
|
|
||||||
<core-format-text [text]="module?.name" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
|
|
||||||
</core-format-text>
|
|
||||||
</h1>
|
|
||||||
</ion-title>
|
|
||||||
|
|
||||||
<ion-buttons slot="end">
|
|
||||||
<core-context-menu>
|
|
||||||
<core-context-menu-item [priority]="900" *ngIf="module?.url" [href]="module!.url"
|
|
||||||
[content]="'core.openinbrowser' | translate" iconAction="fas-external-link-alt">
|
|
||||||
</core-context-menu-item>
|
|
||||||
<core-context-menu-item [priority]="800" *ngIf="module?.description" [content]="'core.moduleintro' | translate"
|
|
||||||
(action)="expandDescription()" iconAction="fas-arrow-right">
|
|
||||||
</core-context-menu-item>
|
|
||||||
</core-context-menu>
|
|
||||||
</ion-buttons>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
<ion-content>
|
|
||||||
<core-course-unsupported-module [module]="module" [courseId]="courseId"></core-course-unsupported-module>
|
|
||||||
</ion-content>
|
|
|
@ -1,54 +0,0 @@
|
||||||
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
|
|
||||||
import { CoreCourseWSModule } from '@features/course/services/course';
|
|
||||||
import { CoreNavigator } from '@services/navigator';
|
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
|
||||||
import { Translate } from '@singletons';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page that displays info about an unsupported module.
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'page-core-course-unsupported-module',
|
|
||||||
templateUrl: 'unsupported-module.html',
|
|
||||||
})
|
|
||||||
export class CoreCourseUnsupportedModulePage implements OnInit {
|
|
||||||
|
|
||||||
module?: CoreCourseWSModule;
|
|
||||||
courseId?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritDoc
|
|
||||||
*/
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.module = CoreNavigator.getRouteParam('module');
|
|
||||||
this.courseId = CoreNavigator.getRouteNumberParam('courseId');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expand the description.
|
|
||||||
*/
|
|
||||||
expandDescription(): void {
|
|
||||||
CoreTextUtils.viewText(Translate.instant('core.description'), this.module!.description!, {
|
|
||||||
filter: true,
|
|
||||||
contextLevel: 'module',
|
|
||||||
instanceId: this.module!.id,
|
|
||||||
courseId: this.courseId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -44,6 +44,7 @@ import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
|
||||||
import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync';
|
import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync';
|
||||||
import { CoreTagItem } from '@features/tag/services/tag';
|
import { CoreTagItem } from '@features/tag/services/tag';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreCourseModuleDelegate } from './module-delegate';
|
||||||
|
|
||||||
const ROOT_CACHE_KEY = 'mmCourse:';
|
const ROOT_CACHE_KEY = 'mmCourse:';
|
||||||
|
|
||||||
|
@ -1058,6 +1059,14 @@ export class CoreCourseProvider {
|
||||||
* @return Whether the module has a view page.
|
* @return Whether the module has a view page.
|
||||||
*/
|
*/
|
||||||
moduleHasView(module: CoreCourseModuleSummary | CoreCourseWSModule): boolean {
|
moduleHasView(module: CoreCourseModuleSummary | CoreCourseWSModule): boolean {
|
||||||
|
if ('modname' in module) {
|
||||||
|
// noviewlink was introduced in 3.8.5, use supports feature as a fallback.
|
||||||
|
if (module.noviewlink ||
|
||||||
|
CoreCourseModuleDelegate.supportsFeature(module.modname, CoreConstants.FEATURE_NO_VIEW_LINK, false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return !!module.url;
|
return !!module.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,14 +49,11 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler {
|
||||||
icon: await CoreCourse.getModuleIconSrc(module.modname, module.modicon),
|
icon: await CoreCourse.getModuleIconSrc(module.modname, module.modicon),
|
||||||
title: module.name,
|
title: module.name,
|
||||||
class: 'core-course-default-handler core-course-module-' + module.modname + '-handler',
|
class: 'core-course-default-handler core-course-module-' + module.modname + '-handler',
|
||||||
action: (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => {
|
action: async (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
options = options || {};
|
await this.openActivityPage(module, courseId, options);
|
||||||
options.params = { module };
|
|
||||||
|
|
||||||
CoreNavigator.navigateToSitePath('course/' + courseId + '/unsupported-module', options);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -92,4 +89,15 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise<void> {
|
||||||
|
options = options || {};
|
||||||
|
options.params = options.params || {};
|
||||||
|
Object.assign(options.params, { module });
|
||||||
|
|
||||||
|
await CoreNavigator.navigateToSitePath('course/' + courseId + '/' + module.id +'/module-preview', options);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,6 +102,16 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler {
|
||||||
* @return Promise resolved with boolean: whether the manual completion should always be displayed.
|
* @return Promise resolved with boolean: whether the manual completion should always be displayed.
|
||||||
*/
|
*/
|
||||||
manualCompletionAlwaysShown?(module: CoreCourseModule): Promise<boolean>;
|
manualCompletionAlwaysShown?(module: CoreCourseModule): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the activity page.
|
||||||
|
*
|
||||||
|
* @param module The module object.
|
||||||
|
* @param courseId The course ID.
|
||||||
|
* @param options Options for the navigation.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -167,8 +177,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 +247,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -292,6 +305,27 @@ export class CoreCourseModuleDelegateService extends CoreDelegate<CoreCourseModu
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the activity page.
|
||||||
|
*
|
||||||
|
* @param module The module object.
|
||||||
|
* @param courseId The course ID.
|
||||||
|
* @param options Options for the navigation.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async openActivityPage(
|
||||||
|
modname: string,
|
||||||
|
module: CoreCourseModule,
|
||||||
|
courseId: number,
|
||||||
|
options?: CoreNavigationOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
return await this.executeFunctionOnEnabled<void>(
|
||||||
|
modname,
|
||||||
|
'openActivityPage',
|
||||||
|
[module, courseId, options],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a certain module type is disabled in a site.
|
* Check if a certain module type is disabled in a site.
|
||||||
*
|
*
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<core-loading [hideUntil]="readyToCapture" class="core-loading-fullheight">
|
<core-loading [hideUntil]="readyToCapture">
|
||||||
<div class="core-av-wrapper">
|
<div class="core-av-wrapper">
|
||||||
<!-- Video stream for image and video. -->
|
<!-- Video stream for image and video. -->
|
||||||
<video *ngIf="!isAudio" [hidden]="hasCaptured" class="core-webcam-stream" autoplay #streamVideo></video>
|
<video *ngIf="!isAudio" [hidden]="hasCaptured" class="core-webcam-stream" autoplay #streamVideo></video>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
import { Type } from '@angular/core';
|
import { Type } from '@angular/core';
|
||||||
|
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
import { CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course';
|
import { CoreCourse, CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course';
|
||||||
import { CoreCourseModule } from '@features/course/services/course-helper';
|
import { CoreCourseModule } from '@features/course/services/course-helper';
|
||||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
||||||
import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index';
|
import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index';
|
||||||
|
@ -92,17 +92,16 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp
|
||||||
|
|
||||||
if (this.handlerSchema.method) {
|
if (this.handlerSchema.method) {
|
||||||
// There is a method, add an action.
|
// There is a method, add an action.
|
||||||
handlerData.action = (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => {
|
handlerData.action = async (
|
||||||
|
event: Event,
|
||||||
|
module: CoreCourseModule,
|
||||||
|
courseId: number,
|
||||||
|
options?: CoreNavigationOptions,
|
||||||
|
) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
options = options || {};
|
await this.openActivityPage(module, courseId, options);
|
||||||
options.params = {
|
|
||||||
title: module.name,
|
|
||||||
module,
|
|
||||||
};
|
|
||||||
|
|
||||||
CoreNavigator.navigateToSitePath(`siteplugins/module/${courseId}/${module.id}`, options);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,4 +228,22 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise<void> {
|
||||||
|
if (!CoreCourse.moduleHasView(module)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options = options || {};
|
||||||
|
options.params = options.params || {};
|
||||||
|
Object.assign(options.params, {
|
||||||
|
title: module.name,
|
||||||
|
module,
|
||||||
|
});
|
||||||
|
|
||||||
|
CoreNavigator.navigateToSitePath(`siteplugins/module/${courseId}/${module.id}`, options);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<core-loading [hideUntil]="finalUrl" class="core-loading-fullheight">
|
<core-loading [hideUntil]="finalUrl">
|
||||||
<core-iframe *ngIf="finalUrl" [src]="finalUrl"></core-iframe>
|
<core-iframe *ngIf="finalUrl" [src]="finalUrl"></core-iframe>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -975,7 +975,7 @@ export class CoreDomUtilsProvider {
|
||||||
* @deprecated since 3.9.5. Use directly the IonContent class.
|
* @deprecated since 3.9.5. Use directly the IonContent class.
|
||||||
*/
|
*/
|
||||||
scrollTo(content: IonContent, x: number, y: number, duration?: number): Promise<void> {
|
scrollTo(content: IonContent, x: number, y: number, duration?: number): Promise<void> {
|
||||||
return content?.scrollToPoint(x, y, duration || 0);
|
return content.scrollToPoint(x, y, duration || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -987,7 +987,7 @@ export class CoreDomUtilsProvider {
|
||||||
* @deprecated since 3.9.5. Use directly the IonContent class.
|
* @deprecated since 3.9.5. Use directly the IonContent class.
|
||||||
*/
|
*/
|
||||||
scrollToBottom(content: IonContent, duration?: number): Promise<void> {
|
scrollToBottom(content: IonContent, duration?: number): Promise<void> {
|
||||||
return content?.scrollToBottom(duration);
|
return content.scrollToBottom(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -999,7 +999,7 @@ export class CoreDomUtilsProvider {
|
||||||
* @deprecated since 3.9.5. Use directly the IonContent class.
|
* @deprecated since 3.9.5. Use directly the IonContent class.
|
||||||
*/
|
*/
|
||||||
scrollToTop(content: IonContent, duration?: number): Promise<void> {
|
scrollToTop(content: IonContent, duration?: number): Promise<void> {
|
||||||
return content?.scrollToTop(duration);
|
return content.scrollToTop(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1010,9 +1010,9 @@ export class CoreDomUtilsProvider {
|
||||||
*/
|
*/
|
||||||
async getContentHeight(content: IonContent): Promise<number> {
|
async getContentHeight(content: IonContent): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const scrollElement = await content?.getScrollElement();
|
const scrollElement = await content.getScrollElement();
|
||||||
|
|
||||||
return scrollElement?.clientHeight || 0;
|
return scrollElement.clientHeight || 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -1026,9 +1026,9 @@ export class CoreDomUtilsProvider {
|
||||||
*/
|
*/
|
||||||
async getScrollHeight(content: IonContent): Promise<number> {
|
async getScrollHeight(content: IonContent): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const scrollElement = await content?.getScrollElement();
|
const scrollElement = await content.getScrollElement();
|
||||||
|
|
||||||
return scrollElement?.scrollHeight || 0;
|
return scrollElement.scrollHeight || 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -1042,9 +1042,9 @@ export class CoreDomUtilsProvider {
|
||||||
*/
|
*/
|
||||||
async getScrollTop(content: IonContent): Promise<number> {
|
async getScrollTop(content: IonContent): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const scrollElement = await content?.getScrollElement();
|
const scrollElement = await content.getScrollElement();
|
||||||
|
|
||||||
return scrollElement?.scrollTop || 0;
|
return scrollElement.scrollTop || 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -1065,7 +1065,7 @@ export class CoreDomUtilsProvider {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
content?.scrollToPoint(position[0], position[1], duration || 0);
|
content.scrollToPoint(position[0], position[1], duration || 0);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -1097,7 +1097,7 @@ export class CoreDomUtilsProvider {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
content?.scrollToPoint(position[0], position[1], duration || 0);
|
content.scrollToPoint(position[0], position[1], duration || 0);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -906,13 +906,16 @@ ion-back-button.md::part(text) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide close button because when present is read on voice over.
|
||||||
ion-fab[core-fab] {
|
ion-fab[core-fab] {
|
||||||
position: fixed;
|
|
||||||
|
|
||||||
// Hide close button because when present is read on voice over.
|
|
||||||
ion-fab-button::part(close-icon) {
|
ion-fab-button::part(close-icon) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
core-course-module-navigation + ion-fab {
|
||||||
|
bottom: calc(var(--core-course-module-navigation-height, 0px) + 10px);
|
||||||
|
@include core-transition(all, 200ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-media-adapt-width {
|
.core-media-adapt-width {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -4,6 +4,8 @@ information provided here is intended especially for developers.
|
||||||
=== 3.9.6 ===
|
=== 3.9.6 ===
|
||||||
|
|
||||||
- The parameters of the functions confirmAndPrefetchCourse and confirmAndPrefetchCourses have changed, they now accept an object with options.
|
- The parameters of the functions confirmAndPrefetchCourse and confirmAndPrefetchCourses have changed, they now accept an object with options.
|
||||||
|
- Component core-navigation-bar changed to add an slider inside. previous, previousTitle, next, nextTitle, info and title have been removed.
|
||||||
|
Now you have to pass all items and 3 optional params have been added.
|
||||||
|
|
||||||
=== 3.9.5 ===
|
=== 3.9.5 ===
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue