MOBILE-3099 course: Improve navigation bar with an slide bar
parent
2e31220942
commit
ba5697b4e7
|
@ -35,20 +35,20 @@
|
||||||
</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>
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -40,9 +40,7 @@
|
||||||
</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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<core-loading [hideUntil]="loaded">
|
<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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
@ -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