2
0
Fork 0

Merge pull request #3013 from crazyserver/MOBILE-3099

Mobile 3099
main
Dani Palou 2021-12-03 14:40:22 +01:00 committed by GitHub
commit 994374c0b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 1093 additions and 420 deletions

View File

@ -1507,6 +1507,10 @@
"core.course.errordownloadingcourse": "local_moodlemobileapp",
"core.course.errordownloadingsection": "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.hiddenoncoursepage": "moodle",
"core.course.insufficientavailablequota": "local_moodlemobileapp",

View File

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

View File

@ -35,20 +35,23 @@
</ion-item>
</ion-card>
<div class="ion-padding safe-area-padding-horizontal">
<core-navigation-bar *ngIf="displayNavBar" [previous]="previousChapter?.id" [previousTitle]="previousNavBarTitle"
[next]="nextChapter?.id" [nextTitle]="nextNavBarTitle" (action)="changeChapter($event)">
<div class="safe-area-padding-horizontal">
<core-navigation-bar *ngIf="displayNavBar" [items]="navigationItems" [showTitles]="displayTitlesInNavBar"
previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)">
</core-navigation-bar>
<div class="ion-padding">
<core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent" contextLevel="module"
[contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
<div class="ion-margin-top" *ngIf="tagsEnabled && tags?.length > 0">
<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"
[next]="nextChapter?.id" [nextTitle]="nextNavBarTitle" (action)="changeChapter($event)"></core-navigation-bar>
</div>
</div>
</core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -26,11 +26,11 @@ import {
import { CoreTag, CoreTagItem } from '@features/tag/services/tag';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { Translate } from '@singletons';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourse } from '@features/course/services/course';
import { AddonModBookTocComponent } from '../toc/toc';
import { CoreConstants } from '@/core/constants';
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
/**
* Component that displays a book.
@ -45,19 +45,16 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
component = AddonModBookProvider.COMPONENT;
chapterContent?: string;
previousChapter?: AddonModBookTocChapter;
nextChapter?: AddonModBookTocChapter;
tagsEnabled = false;
displayNavBar = true;
previousNavBarTitle?: string;
nextNavBarTitle?: string;
warning = '';
tags?: CoreTagItem[];
displayNavBar = true;
navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
displayTitlesInNavBar = false;
protected chapters: AddonModBookTocChapter[] = [];
protected currentChapter?: number;
protected book?: AddonModBookBookWSData;
protected displayTitlesInNavBar = false;
protected contentsMap: AddonModBookContentsMap = {};
constructor(
@ -148,14 +145,18 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
}
}
if (typeof this.currentChapter == 'undefined') {
if (this.currentChapter === undefined) {
// Load the first chapter.
this.currentChapter = AddonModBook.getFirstChapter(this.chapters);
}
if (this.currentChapter === undefined) {
return;
}
// Show chapter.
try {
await this.loadChapter(this.currentChapter!, refresh);
await this.loadChapter(this.currentChapter, refresh);
this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : '';
} catch {
@ -199,15 +200,10 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
this.tags = this.tagsEnabled ? this.contentsMap[this.currentChapter].tags : [];
this.chapterContent = content;
this.previousChapter = AddonModBook.getPreviousChapter(this.chapters, chapterId);
this.nextChapter = AddonModBook.getNextChapter(this.chapters, chapterId);
this.previousNavBarTitle = this.previousChapter && this.displayTitlesInNavBar
? Translate.instant('addon.mod_book.navprevtitle', { $a: this.previousChapter.title })
: '';
this.nextNavBarTitle = this.nextChapter && this.displayTitlesInNavBar
? Translate.instant('addon.mod_book.navnexttitle', { $a: this.nextChapter.title })
: '';
if (this.displayNavBar) {
this.navigationItems = this.getNavigationItems(chapterId);
}
// 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(
@ -216,8 +212,11 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
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.
if (!this.nextChapter) {
if (isLastChapter) {
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
} 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,
}));
}
}

View File

@ -216,36 +216,6 @@ export class AddonModBookProvider {
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.
*

View File

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

View File

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

View File

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

View File

@ -138,6 +138,9 @@
</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-button (click)="gotoAddEntries()" [attr.aria-label]="'addon.mod_data.addentries' | translate">
<ion-icon name="fas-plus" aria-hidden="true"></ion-icon>

View File

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

View File

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

View File

@ -140,6 +140,9 @@
</ng-container>
</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-button (click)="openNewDiscussion()" [attr.aria-label]="addDiscussionText">
<ion-icon name="fas-plus" aria-hidden="true"></ion-icon>

View File

@ -96,6 +96,9 @@
</core-infinite-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-button (click)="openNewEntry()" [attr.aria-label]="'addon.mod_glossary.addentry' | translate">
<ion-icon name="fas-plus" aria-hidden="true"></ion-icon>

View File

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

View File

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

View File

@ -40,10 +40,11 @@
</ion-card>
<div class="addon-mod-imscp-container">
<core-navigation-bar [previous]="previousItem" [next]="nextItem" (action)="loadItem($event)" [info]="description"
[title]="'core.description' | translate" [component]="component" [componentId]="componentId" contextLevel="module"
[contextInstanceId]="module.id" [courseId]="courseId">
<core-navigation-bar [items]="navigationItems" (action)="loadItem($event)">
</core-navigation-bar>
<core-iframe [src]="src"></core-iframe>
</div>
</core-loading>
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -1,6 +1,4 @@
.addon-mod-imscp-container {
position: absolute;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;

View File

@ -14,6 +14,7 @@
import { Component, OnInit, Optional } from '@angular/core';
import { CoreSilentError } from '@classes/errors/silenterror';
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
@ -32,22 +33,19 @@ import { AddonModImscpTocComponent } from '../toc/toc';
export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit {
component = AddonModImscpProvider.COMPONENT;
items: AddonModImscpTocItem[] = [];
currentItem?: string;
src = '';
warning = '';
navigationItems: CoreNavigationBarItem<AddonModImscpTocItem>[] = [];
// Initialize empty previous/next to prevent showing arrows for an instant before they're hidden.
previousItem = '';
nextItem = '';
protected items: AddonModImscpTocItem[] = [];
protected currentHref?: string;
constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) {
super('AddonModImscpIndexComponent', courseContentsPage);
}
/**
* Component being initialized.
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
super.ngOnInit();
@ -90,19 +88,19 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
this.items = AddonModImscp.createItemList(contents);
if (this.items.length && typeof this.currentItem == 'undefined') {
this.currentItem = this.items[0].href;
if (this.items.length && this.currentHref === undefined) {
this.currentHref = this.items[0].href;
}
try {
await this.loadItem(this.currentItem);
await this.loadItemHref(this.currentHref);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_imscp.deploymenterror', true);
throw new CoreSilentError(error);
}
this.warning = downloadResult!.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult!.error!) : '';
this.warning = downloadResult.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : '';
} finally {
// Pass false because downloadResourceIfNeeded already invalidates and refresh data if refresh=true.
@ -113,14 +111,18 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
/**
* Loads an item.
*
* @param itemId Item ID.
* @param itemHref Item Href.
* @return Promise resolved when done.
*/
async loadItem(itemId?: string): Promise<void> {
const src = await AddonModImscp.getIframeSrc(this.module, itemId);
this.currentItem = itemId;
this.previousItem = itemId ? AddonModImscp.getPreviousItem(this.items, itemId) : '';
this.nextItem = itemId ? AddonModImscp.getNextItem(this.items, itemId) : '';
async loadItemHref(itemHref?: string): Promise<void> {
const src = await AddonModImscp.getIframeSrc(this.module, itemHref);
this.currentHref = itemHref;
this.navigationItems = this.items.map((item) => ({
item: item,
current: item.href == this.currentHref,
enabled: !!item.href,
}));
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.
@ -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.
*/
@ -142,12 +153,12 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
component: AddonModImscpTocComponent,
componentProps: {
items: this.items,
selected: this.currentItem,
selected: this.currentHref,
},
});
if (modalData) {
this.loadItem(modalData);
this.loadItemHref(modalData);
}
}

View File

@ -70,63 +70,6 @@ export class AddonModImscpProvider {
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.
*
@ -242,7 +185,7 @@ export class AddonModImscpProvider {
const siteId = CoreSites.getCurrentSiteId();
try {
const dirPath = await CoreFilepool.getPackageDirUrlByUrl(siteId, module!.url!);
const dirPath = await CoreFilepool.getPackageDirUrlByUrl(siteId, module.url!);
return CoreTextUtils.concatenatePaths(dirPath, itemHref);
} catch (error) {

View File

@ -39,6 +39,7 @@ export class AddonModLabelModuleHandlerService extends CoreModuleHandlerBase imp
[CoreConstants.FEATURE_GRADE_OUTCOMES]: false,
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
[CoreConstants.FEATURE_NO_VIEW_LINK]: true,
};
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,8 +20,8 @@
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded" class="core-loading-fullheight">
<core-navigation-bar [previous]="previousSco" [next]="nextSco" (action)="loadSco($event)"></core-navigation-bar>
<core-loading [hideUntil]="loaded">
<core-navigation-bar [items]="navigationItems" (action)="loadSco($event)"></core-navigation-bar>
<core-iframe *ngIf="loaded && src" [src]="src" [iframeWidth]="scormWidth" [iframeHeight]="scormHeight"
[showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="true">

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
import { CoreMainMenuPage } from '@features/mainmenu/pages/menu/menu';
import { CoreNavigator } from '@services/navigator';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
@ -50,8 +51,6 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
loadingToc = true; // Whether the TOC is being loaded.
toc: AddonModScormTOCScoWithIcon[] = []; // List of SCOs.
loaded = false; // Whether the data has been loaded.
previousSco?: AddonModScormScoWithData; // Previous SCO.
nextSco?: AddonModScormScoWithData; // Next SCO.
src?: string; // Iframe src.
errorMessage?: string; // Error message.
accessInfo?: AddonModScormGetScormAccessInformationWSResponse; // Access information.
@ -60,6 +59,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
incomplete = false; // Whether last attempt is incomplete.
cmId!: number; // Course module ID.
courseId!: number; // Course ID.
navigationItems: CoreNavigationBarItem<AddonModScormTOCScoWithIcon>[] = [];
protected siteId!: string;
protected mode!: string; // Mode to play the SCORM.
@ -110,6 +110,8 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
await this.fetchData();
if (!this.currentSco) {
CoreNavigator.back();
return;
}
@ -176,14 +178,20 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
}, this.siteId);
this.launchNextObserver = CoreEvents.on(AddonModScormProvider.LAUNCH_NEXT_SCO_EVENT, (data) => {
if (data.scormId === this.scorm.id && this.nextSco) {
this.loadSco(this.nextSco);
if (data.scormId === this.scorm.id && this.currentSco) {
const nextSco = AddonModScormHelper.getNextScoFromToc(this.toc, this.currentSco.id);
if (nextSco) {
this.loadSco(nextSco);
}
}
}, this.siteId);
this.launchPrevObserver = CoreEvents.on(AddonModScormProvider.LAUNCH_PREV_SCO_EVENT, (data) => {
if (data.scormId === this.scorm.id && this.previousSco) {
this.loadSco(this.previousSco);
if (data.scormId === this.scorm.id && this.currentSco) {
const previousSco = AddonModScormHelper.getPreviousScoFromToc(this.toc, this.currentSco.id);
if (previousSco) {
this.loadSco(previousSco);
}
}
}, this.siteId);
@ -211,9 +219,16 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
*
* @param scoId Current SCO ID.
*/
protected calculateNextAndPreviousSco(scoId: number): void {
this.previousSco = AddonModScormHelper.getPreviousScoFromToc(this.toc, scoId);
this.nextSco = AddonModScormHelper.getNextScoFromToc(this.toc, scoId);
protected calculateNavigationItems(scoId: number): void {
this.navigationItems = this.toc
.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.title = sco.title || this.scorm.name; // Try to use SCO title.
this.calculateNextAndPreviousSco(sco.id);
this.calculateNavigationItems(sco.id);
// Load the SCO source.
this.loadScoSrc(sco);
@ -540,7 +555,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
}
/**
* Component being destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
// Empty src when leaving the state so unload event is triggered in the iframe.

View File

@ -280,19 +280,14 @@ export class AddonModScormHelperProvider {
* @return Next SCO.
*/
getNextScoFromToc(toc: AddonModScormScoWithData[], scoId: number): AddonModScormScoWithData | undefined {
for (let i = 0; i < toc.length; i++) {
if (toc[i].id != scoId) {
continue;
}
const currentTocIndex = toc.findIndex((item) => item.id == scoId);
// 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) {
return toc[j];
}
}
break;
}
}
/**
@ -303,19 +298,14 @@ export class AddonModScormHelperProvider {
* @return Previous SCO.
*/
getPreviousScoFromToc(toc: AddonModScormScoWithData[], scoId: number): AddonModScormScoWithData | undefined {
for (let i = 0; i < toc.length; i++) {
if (toc[i].id != scoId) {
continue;
}
const currentTocIndex = toc.findIndex((item) => item.id == scoId);
// 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) {
return toc[j];
}
}
break;
}
}
/**

View File

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

View File

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

View File

@ -19,7 +19,7 @@ import { CoreModuleHandlerBase } from '@features/course/classes/module-base-hand
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseModule } from '@features/course/services/course-helper';
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 { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
@ -90,12 +90,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
if (shouldOpen) {
openUrl(module, courseId);
} else {
options = options || {};
options.params = options.params || {};
Object.assign(options.params, { module });
const routeParams = '/' + courseId + '/' + module.id;
CoreNavigator.navigateToSitePath(AddonModUrlModuleHandlerService.PAGE_NAME + routeParams, options);
this.openActivityPage(module, courseId, options);
}
} finally {
modal.dismiss();

View File

@ -89,6 +89,9 @@
</div>
</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-button (click)="goToNewPage()" [attr.aria-label]="'addon.mod_wiki.createpage' | translate">
<ion-icon name="fas-plus" aria-hidden="true"></ion-icon>

View File

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

View File

@ -44,6 +44,7 @@
}
.core-loading-content {
display: contents;
@include core-transition(opacity, 200ms);
}
@ -51,10 +52,6 @@
@include margin(10px, 0, 0, 0);
}
&.core-loading-fullheight .core-loading-content {
height: 100%;
}
&.core-loading-loaded {
position: unset;
display: contents;

View File

@ -1,27 +1,27 @@
<ion-grid class="ion-no-padding ion-padding-bottom" *ngIf="previous || info || next">
<ion-row>
<ion-col class="ion-text-start ion-padding-end" [size]="info ? 4 : 6">
<ion-button *ngIf="previous" class="core-navigation-bar-arrow" fill="outline"
[attr.aria-label]="previousTitle || ('core.previous' | translate)" (click)="action?.emit(previous)">
<ion-icon name="fas-arrow-left" [slot]="previousTitle ? 'start' : 'icon-only'" aria-hidden="true"></ion-icon>
<core-format-text *ngIf="previousTitle" [text]="previousTitle" [component]="component" [componentId]="componentId"
<ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding"
*ngIf="previousIndex >= 0 || nextIndex >= 0 || items.length > 1">
<ion-col class="ion-text-start ion-padding-end" [size]="showTitles ? 4 : 3">
<ion-button *ngIf="previousIndex >=0" class="core-navigation-bar-arrow" fill="clear" [attr.aria-label]="previousTitle"
(click)="navigate(previousIndex)">
<ion-icon name="fas-arrow-left" [slot]="showTitles ? 'start' : 'icon-only'" aria-hidden="true"></ion-icon>
<core-format-text *ngIf="showTitles" [text]="previousTitle" [component]="component" [componentId]="componentId"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" aria-hidden="true">
</core-format-text>
</ion-button>
</ion-col>
<ion-col class="ion-text-center" size="4" *ngIf="info">
<ion-button fill="clear" (click)="showInfo()" [attr.aria-label]="title">
<ion-icon slot="icon-only" name="fas-info-circle" aria-hidden="true"></ion-icon>
</ion-button>
<ion-col class="ion-text-center" [size]="showTitles ? 4 : 6">
<ion-range min="0" [max]="items.length -1" debounce="500" snaps="true" (ionChange)="navigateOnRange($event.target)"
[value]="currentIndex">
<p slot="end">{{currentIndex + 1}} / {{items.length}}</p>
</ion-range>
</ion-col>
<ion-col class="ion-text-end ion-padding-start" [size]="info ? 4 : 6">
<ion-button *ngIf="next" class="core-navigation-bar-arrow" [attr.aria-label]="nextTitle || ('core.next' | translate)"
(click)="action?.emit(next)">
<core-format-text *ngIf="nextTitle" [text]="nextTitle" [component]="component" [componentId]="componentId"
<ion-col class="ion-text-end ion-padding-start" [size]="showTitles ? 4 : 3">
<ion-button fill="clear" *ngIf="nextIndex >= 0" class="core-navigation-bar-arrow" [attr.aria-label]="nextTitle"
(click)="navigate(nextIndex)">
<core-format-text *ngIf="showTitles" [text]="nextTitle" [component]="component" [componentId]="componentId"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId" aria-hidden="true">
</core-format-text>
<ion-icon name="fas-arrow-right" [slot]="nextTitle ? 'end' : 'icon-only'" aria-hidden="true"></ion-icon>
<ion-icon name="fas-arrow-right" [slot]="showTitles ? 'end' : 'icon-only'" aria-hidden="true"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</ion-row>

View File

@ -1,7 +1,15 @@
.core-navigation-bar-arrow {
:host {
--background: var(--core-course-module-navigation-background);
width: 100%;
background-color: var(--background);
display: block;
.core-navigation-bar-arrow {
text-transform: none;
max-width: 100%;
ion-icon {
flex-shrink: 0;
}
}
}

View File

@ -12,48 +12,103 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { CoreTextUtils } from '@services/utils/text';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange } from '@angular/core';
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.
* 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:
* <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({
selector: 'core-navigation-bar',
templateUrl: 'core-navigation-bar.html',
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() previousTitle?: string; // Previous item title. If not defined, only the arrow will be shown.
@Input() next?: unknown; // Next item. If not defined, the next arrow won't be shown.
@Input() nextTitle?: string; // Next item title. If not defined, only the arrow will be shown.
@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() items: CoreNavigationBarItem[] = []; // List of items.
@Input() showTitles = false; // Display titles on buttons.
@Input() previousTranslate = 'core.previous'; // Previous translatable text, can admit $a variable.
@Input() nextTranslate = 'core.next'; // Next translatable text, can admit $a variable.
@Input() component?: string; // Component the bar belongs to.
@Input() componentId?: number; // Component ID.
@Input() contextLevel?: string; // The context level.
@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.
@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 {
CoreTextUtils.viewText(this.title, this.info, {
component: this.component,
componentId: this.componentId,
filter: true,
contextLevel: this.contextLevel,
instanceId: this.contextInstanceId,
courseId: this.courseId,
});
previousTitle?: string; // Previous item title.
nextTitle?: string; // Next item title.
previousIndex = -1; // Previous item index. If -1, the previous arrow won't be shown.
nextIndex = -1; // Next item index. If -1, the next arrow won't be shown.
currentIndex = 0;
// Function to call when arrow is clicked. Will receive as a param the item to load.
@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;
};

View File

@ -22,6 +22,7 @@
.menu,
.content-outlet {
top: var(--offset-top);
height: calc(100% - var(--offset-top));
right: 0;
bottom: 0;
left: 0;

View File

@ -48,8 +48,12 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy {
this.header = el.nativeElement;
this.loadingObserver = CoreEvents.on(CoreEvents.CORE_LOADING_CHANGED, async (data) => {
if (!data.loaded) {
return;
}
const loadingId = await this.getLoadingId();
if (loadingId && data.loaded && data.uniqueId == loadingId) {
if (loadingId && data.uniqueId == loadingId) {
// Remove event when loading is done.
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;
}
/**

View File

@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Directive, ElementRef, OnDestroy } from '@angular/core';
import { IonContent } from '@ionic/angular';
import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core';
import { CoreUtils } from '@services/utils/utils';
/**
* Directive to move ion-fab components as direct children of the nearest ion-content.
@ -25,47 +25,50 @@ import { IonContent } from '@ionic/angular';
@Directive({
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 content?: HTMLIonContentElement | null;
protected initialPaddingBottom = 0;
constructor(el: ElementRef, protected content: IonContent) {
constructor(el: ElementRef) {
this.element = el.nativeElement;
this.asyncInit();
this.element.setAttribute('slot', 'fixed');
}
/**
* Initialize Component.
* @inheritdoc
*/
async asyncInit(): Promise<void> {
if (this.content) {
this.scrollElement = await this.content.getScrollElement();
if (!this.done) {
async ngOnInit(retries = 3): Promise<void> {
this.content = this.element.closest('ion-content');
if (!this.content) {
if(retries > 0) {
await CoreUtils.nextTicks(50);
this.ngOnInit(retries - 1);
}
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') {
const ionContent = this.element.closest('ion-content');
ionContent?.appendChild(this.element);
this.content.appendChild(this.element);
}
// Add space at the bottom to let the user see the whole content.
const bottom = parseInt(this.scrollElement.style.paddingBottom, 10) || 0;
this.scrollElement.style.paddingBottom = (bottom + CoreFabDirective.PADDINGBOTTOM) + 'px';
this.done = true;
}
}
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 {
if (this.done && this.scrollElement) {
const bottom = parseInt(this.scrollElement.style.paddingBottom, 10) || 0;
this.scrollElement.style.paddingBottom = (bottom - CoreFabDirective.PADDINGBOTTOM) + 'px';
if (this.content) {
this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px');
}
}

View File

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

View File

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

View File

@ -13,24 +13,25 @@
<ion-item class="ion-text-wrap" *ngIf="description" lines="none">
<ion-label>
<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>
</ion-label>
</ion-item>
<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>
<!-- 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">
<strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }}
</p>
</div>
<!-- Module completion. -->
<core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata" [moduleName]="module.name"
[moduleId]="module.id" [showCompletionConditions]="true" [showManualCompletion]="true"
(completionChanged)="completionChanged.emit($event)">
<core-course-module-completion *ngIf="module.completiondata && module.uservisible" [completion]="module.completiondata"
[moduleName]="module.name" [moduleId]="module.id" [showCompletionConditions]="true"
[showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
</core-course-module-completion>
</ion-label>
</ion-item>

View File

@ -36,12 +36,14 @@ import { CoreSites } from '@services/sites';
export class CoreCourseModuleInfoComponent implements OnInit {
@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() component!: string; // Component for format text directive.
@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() expandDescription = false; // If the description should be expanded by default.
@Input() hasDataToSync = false; // If the activity has any data to be synced.

View File

@ -2,20 +2,22 @@
<ng-container *ngIf="completion.istrackeduser">
<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>
{{ 'core.course.completion_manual:done' | translate }}
</ion-button>
</ng-container>
<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 }}
</ion-button>
</ng-container>
</ng-container>
<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 }}
</ion-button>
</ng-container>

View File

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

View File

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

View File

@ -0,0 +1,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');
}
}

View File

@ -75,8 +75,8 @@
</div>
<!-- Module completion. -->
<core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata" [moduleName]="module.name"
[moduleId]="module.id" [showCompletionConditions]="showCompletionConditions"
<core-course-module-completion *ngIf="module.completiondata && module.uservisible" [completion]="module.completiondata"
[moduleName]="module.name" [moduleId]="module.id" [showCompletionConditions]="showCompletionConditions"
[showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
</core-course-module-completion>

View File

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

View File

@ -1,6 +1,3 @@
<core-course-module-info [description]="module?.description" [courseId]="courseId" [module]="module">
</core-course-module-info>
<div class="ion-padding">
<h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2>
<h2 *ngIf="isDisabledInSite || !isSupportedByTheApp">{{ 'core.uhoh' | translate }}</h2>

View File

@ -23,9 +23,9 @@ const routes: Routes = [
loadChildren: () => import('./pages/index/index.module').then( m => m.CoreCourseIndexPageModule),
},
{
path: ':courseId/unsupported-module',
loadChildren: () => import('./pages/unsupported-module/unsupported-module.module')
.then( m => m.CoreCourseUnsupportedModulePageModule),
path: ':courseId/:cmId/module-preview',
loadChildren: () => import('./pages/module-preview/module-preview.module')
.then( m => m.CoreCourseModulePreviewPageModule),
},
{
path: ':courseId/list-mod-type',

View File

@ -27,6 +27,10 @@
"confirmpartialdownloadsize": "You are about to download <strong>at least</strong> {{size}}.{{availableSpace}} Are you sure you want to continue?",
"confirmlimiteddownload": "You are not currently connected to Wi-Fi. ",
"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.",
"couldnotloadsections": "Could not load the sections. Please try again later.",
"coursesummary": "Course summary",

View File

@ -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>

View File

@ -16,13 +16,13 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
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';
const routes: Routes = [
{
path: '',
component: CoreCourseUnsupportedModulePage,
component: CoreCourseModulePreviewPage,
},
];
@ -33,8 +33,8 @@ const routes: Routes = [
CoreCourseComponentsModule,
],
declarations: [
CoreCourseUnsupportedModulePage,
CoreCourseModulePreviewPage,
],
exports: [RouterModule],
})
export class CoreCourseUnsupportedModulePageModule {}
export class CoreCourseModulePreviewPageModule { }

View File

@ -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?.();
}
}

View File

@ -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>

View File

@ -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,
});
}
}

View File

@ -44,6 +44,7 @@ import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync';
import { CoreTagItem } from '@features/tag/services/tag';
import { CoreNavigator } from '@services/navigator';
import { CoreCourseModuleDelegate } from './module-delegate';
const ROOT_CACHE_KEY = 'mmCourse:';
@ -1058,6 +1059,14 @@ export class CoreCourseProvider {
* @return Whether the module has a view page.
*/
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;
}

View File

@ -49,14 +49,11 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler {
icon: await CoreCourse.getModuleIconSrc(module.modname, module.modicon),
title: module.name,
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.stopPropagation();
options = options || {};
options.params = { module };
CoreNavigator.navigateToSitePath('course/' + courseId + '/unsupported-module', options);
await this.openActivityPage(module, courseId, options);
},
};
@ -92,4 +89,15 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler {
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);
}
}

View File

@ -102,6 +102,16 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler {
* @return Promise resolved with boolean: whether the manual completion should always be displayed.
*/
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 courseId The course ID.
* @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.
@ -236,8 +247,10 @@ export interface CoreCourseModuleHandlerButton {
* @param event The click event.
* @param module The module object.
* @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.
*

View File

@ -11,7 +11,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="readyToCapture" class="core-loading-fullheight">
<core-loading [hideUntil]="readyToCapture">
<div class="core-av-wrapper">
<!-- Video stream for image and video. -->
<video *ngIf="!isAudio" [hidden]="hasCaptured" class="core-webcam-stream" autoplay #streamVideo></video>

View File

@ -15,7 +15,7 @@
import { Type } from '@angular/core';
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 { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index';
@ -92,17 +92,16 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp
if (this.handlerSchema.method) {
// 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.stopPropagation();
options = options || {};
options.params = {
title: module.name,
module,
};
CoreNavigator.navigateToSitePath(`siteplugins/module/${courseId}/${module.id}`, options);
await this.openActivityPage(module, courseId, options);
};
}
@ -229,4 +228,22 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp
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);
}
}

View File

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

View File

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

View File

@ -9,7 +9,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="finalUrl" class="core-loading-fullheight">
<core-loading [hideUntil]="finalUrl">
<core-iframe *ngIf="finalUrl" [src]="finalUrl"></core-iframe>
</core-loading>
</ion-content>

View File

@ -975,7 +975,7 @@ export class CoreDomUtilsProvider {
* @deprecated since 3.9.5. Use directly the IonContent class.
*/
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.
*/
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.
*/
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> {
try {
const scrollElement = await content?.getScrollElement();
const scrollElement = await content.getScrollElement();
return scrollElement?.clientHeight || 0;
return scrollElement.clientHeight || 0;
} catch (error) {
return 0;
}
@ -1026,9 +1026,9 @@ export class CoreDomUtilsProvider {
*/
async getScrollHeight(content: IonContent): Promise<number> {
try {
const scrollElement = await content?.getScrollElement();
const scrollElement = await content.getScrollElement();
return scrollElement?.scrollHeight || 0;
return scrollElement.scrollHeight || 0;
} catch (error) {
return 0;
}
@ -1042,9 +1042,9 @@ export class CoreDomUtilsProvider {
*/
async getScrollTop(content: IonContent): Promise<number> {
try {
const scrollElement = await content?.getScrollElement();
const scrollElement = await content.getScrollElement();
return scrollElement?.scrollTop || 0;
return scrollElement.scrollTop || 0;
} catch (error) {
return 0;
}
@ -1065,7 +1065,7 @@ export class CoreDomUtilsProvider {
return false;
}
content?.scrollToPoint(position[0], position[1], duration || 0);
content.scrollToPoint(position[0], position[1], duration || 0);
return true;
}
@ -1097,7 +1097,7 @@ export class CoreDomUtilsProvider {
return false;
}
content?.scrollToPoint(position[0], position[1], duration || 0);
content.scrollToPoint(position[0], position[1], duration || 0);
return true;
} catch (error) {

View File

@ -906,15 +906,18 @@ ion-back-button.md::part(text) {
display: none;
}
// Hide close button because when present is read on voice over.
ion-fab[core-fab] {
position: fixed;
// Hide close button because when present is read on voice over.
ion-fab-button::part(close-icon) {
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 {
max-width: 100%;
}

View File

@ -257,6 +257,9 @@
--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-course-color: var(--red);
--addon-calendar-event-group-color: var(--yellow);

View File

@ -4,6 +4,8 @@ information provided here is intended especially for developers.
=== 3.9.6 ===
- 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 ===