MOBILE-3099 course: Improve navigation bar with an slide bar
parent
2e31220942
commit
ba5697b4e7
|
@ -35,20 +35,20 @@
|
|||
</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>
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -40,9 +40,7 @@
|
|||
</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>
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
.addon-mod-imscp-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</ion-header>
|
||||
<ion-content>
|
||||
<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"
|
||||
[showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="true">
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 ===
|
||||
|
||||
|
|
Loading…
Reference in New Issue