MOBILE-3099 course: Improve navigation bar with an slide bar

main
Pau Ferrer Ocaña 2021-11-30 13:58:42 +01:00
parent 2e31220942
commit ba5697b4e7
14 changed files with 232 additions and 228 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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