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

View File

@ -26,11 +26,11 @@ import {
import { CoreTag, CoreTagItem } from '@features/tag/services/tag';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { Translate } from '@singletons';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourse } from '@features/course/services/course';
import { AddonModBookTocComponent } from '../toc/toc';
import { CoreConstants } from '@/core/constants';
import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar';
/**
* Component that displays a book.
@ -45,19 +45,16 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
component = AddonModBookProvider.COMPONENT;
chapterContent?: string;
previousChapter?: AddonModBookTocChapter;
nextChapter?: AddonModBookTocChapter;
tagsEnabled = false;
displayNavBar = true;
previousNavBarTitle?: string;
nextNavBarTitle?: string;
warning = '';
tags?: CoreTagItem[];
displayNavBar = true;
navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
displayTitlesInNavBar = false;
protected chapters: AddonModBookTocChapter[] = [];
protected currentChapter?: number;
protected book?: AddonModBookBookWSData;
protected displayTitlesInNavBar = false;
protected contentsMap: AddonModBookContentsMap = {};
constructor(
@ -148,14 +145,18 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
}
}
if (typeof this.currentChapter == 'undefined') {
if (this.currentChapter === undefined) {
// Load the first chapter.
this.currentChapter = AddonModBook.getFirstChapter(this.chapters);
}
if (this.currentChapter === undefined) {
return;
}
// Show chapter.
try {
await this.loadChapter(this.currentChapter!, refresh);
await this.loadChapter(this.currentChapter, refresh);
this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : '';
} catch {
@ -199,15 +200,10 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
this.tags = this.tagsEnabled ? this.contentsMap[this.currentChapter].tags : [];
this.chapterContent = content;
this.previousChapter = AddonModBook.getPreviousChapter(this.chapters, chapterId);
this.nextChapter = AddonModBook.getNextChapter(this.chapters, chapterId);
this.previousNavBarTitle = this.previousChapter && this.displayTitlesInNavBar
? Translate.instant('addon.mod_book.navprevtitle', { $a: this.previousChapter.title })
: '';
this.nextNavBarTitle = this.nextChapter && this.displayTitlesInNavBar
? Translate.instant('addon.mod_book.navnexttitle', { $a: this.nextChapter.title })
: '';
if (this.displayNavBar) {
this.navigationItems = this.getNavigationItems(chapterId);
}
// Chapter loaded, log view. We don't return the promise because we don't want to block the user for this.
await CoreUtils.ignoreErrors(AddonModBook.logView(
@ -216,8 +212,11 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
this.module.name,
));
const currentChapterIndex = this.chapters.findIndex((chapter) => chapter.id == chapterId);
const isLastChapter = currentChapterIndex < 0 || this.chapters[currentChapterIndex + 1] === undefined;
// Module is completed when last chapter is viewed, so we only check completion if the last is reached.
if (!this.nextChapter) {
if (isLastChapter) {
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
} catch (error) {
@ -230,4 +229,19 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
}
}
/**
* Converts chapters to navigation items.
*
* @param chapterId Current chapter Id.
* @return Navigation items.
*/
protected getNavigationItems(chapterId: number): CoreNavigationBarItem<AddonModBookTocChapter>[] {
return this.chapters.map((chapter) => ({
item: chapter,
title: chapter.title,
current: chapter.id == chapterId,
enabled: true,
}));
}
}

View File

@ -216,36 +216,6 @@ export class AddonModBookProvider {
return chapters[0].id;
}
/**
* Get the next chapter to the given one.
*
* @param chapters The chapters list.
* @param chapterId The current chapter.
* @return The next chapter.
*/
getNextChapter(chapters: AddonModBookTocChapter[], chapterId: number): AddonModBookTocChapter | undefined {
const currentChapterIndex = chapters.findIndex((chapter) => chapter.id == chapterId);
if (currentChapterIndex >= 0 && typeof chapters[currentChapterIndex + 1] != 'undefined') {
return chapters[currentChapterIndex + 1];
}
}
/**
* Get the previous chapter to the given one.
*
* @param chapters The chapters list.
* @param chapterId The current chapter.
* @return The next chapter.
*/
getPreviousChapter(chapters: AddonModBookTocChapter[], chapterId: number): AddonModBookTocChapter | undefined {
const currentChapterIndex = chapters.findIndex((chapter) => chapter.id == chapterId);
if (currentChapterIndex > 0) {
return chapters[currentChapterIndex - 1];
}
}
/**
* Get the book toc as an array.
*

View File

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

View File

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

View File

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

View File

@ -70,63 +70,6 @@ export class AddonModImscpProvider {
return items;
}
/**
* Get the previous item to the given one.
*
* @param items The items list.
* @param itemId The current item.
* @return The previous item id.
*/
getPreviousItem(items: AddonModImscpTocItem[], itemId: string): string {
const position = this.getItemPosition(items, itemId);
if (position == -1) {
return '';
}
for (let i = position - 1; i >= 0; i--) {
if (items[i] && items[i].href) {
return items[i].href;
}
}
return '';
}
/**
* Get the next item to the given one.
*
* @param items The items list.
* @param itemId The current item.
* @return The next item id.
*/
getNextItem(items: AddonModImscpTocItem[], itemId: string): string {
const position = this.getItemPosition(items, itemId);
if (position == -1) {
return '';
}
for (let i = position + 1; i < items.length; i++) {
if (items[i] && items[i].href) {
return items[i].href;
}
}
return '';
}
/**
* Get the position of a item.
*
* @param items The items list.
* @param itemId The item to search.
* @return The item position.
*/
protected getItemPosition(items: AddonModImscpTocItem[], itemId: string): number {
return items.findIndex((item) => item.href == itemId);
}
/**
* Check if we should ommit the file download.
*
@ -242,7 +185,7 @@ export class AddonModImscpProvider {
const siteId = CoreSites.getCurrentSiteId();
try {
const dirPath = await CoreFilepool.getPackageDirUrlByUrl(siteId, module!.url!);
const dirPath = await CoreFilepool.getPackageDirUrlByUrl(siteId, module.url!);
return CoreTextUtils.concatenatePaths(dirPath, itemHref);
} catch (error) {

View File

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

View File

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

View File

@ -280,19 +280,14 @@ export class AddonModScormHelperProvider {
* @return Next SCO.
*/
getNextScoFromToc(toc: AddonModScormScoWithData[], scoId: number): AddonModScormScoWithData | undefined {
for (let i = 0; i < toc.length; i++) {
if (toc[i].id != scoId) {
continue;
}
const currentTocIndex = toc.findIndex((item) => item.id == scoId);
// We found the current SCO. Now search the next visible SCO with fulfilled prerequisites.
for (let j = i + 1; j < toc.length; j++) {
for (let j = currentTocIndex + 1; j < toc.length; j++) {
if (toc[j].isvisible && toc[j].prereq && toc[j].launch) {
return toc[j];
}
}
break;
}
}
/**
@ -303,19 +298,14 @@ export class AddonModScormHelperProvider {
* @return Previous SCO.
*/
getPreviousScoFromToc(toc: AddonModScormScoWithData[], scoId: number): AddonModScormScoWithData | undefined {
for (let i = 0; i < toc.length; i++) {
if (toc[i].id != scoId) {
continue;
}
const currentTocIndex = toc.findIndex((item) => item.id == scoId);
// We found the current SCO. Now let's search the previous visible SCO with fulfilled prerequisites.
for (let j = i - 1; j >= 0; j--) {
for (let j = currentTocIndex - 1; j >= 0; j--) {
if (toc[j].isvisible && toc[j].prereq && toc[j].launch) {
return toc[j];
}
}
break;
}
}
/**

View File

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

View File

@ -1,3 +1,10 @@
: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%;
@ -5,3 +12,4 @@
flex-shrink: 0;
}
}
}

View File

@ -12,48 +12,103 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { CoreTextUtils } from '@services/utils/text';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange } from '@angular/core';
import { Translate } from '@singletons';
/**
* Component to show a "bar" with arrows to navigate forward/backward and a "info" icon to display more data.
* Component to show a "bar" with arrows to navigate forward/backward and an slider to move around.
*
* This directive will show two arrows at the left and right of the screen to navigate to previous/next item when clicked.
* If no previous/next item is defined, that arrow won't be shown. It will also show a button to show more info.
* If no previous/next item is defined, that arrow won't be shown.
*
* Example usage:
* <core-navigation-bar [previous]="prevItem" [next]="nextItem" (action)="goTo($event)"></core-navigation-bar>
* <core-navigation-bar [items]="items" (action)="goTo($event)"></core-navigation-bar>
*/
@Component({
selector: 'core-navigation-bar',
templateUrl: 'core-navigation-bar.html',
styleUrls: ['navigation-bar.scss'],
})
export class CoreNavigationBarComponent {
export class CoreNavigationBarComponent implements OnChanges {
@Input() previous?: unknown; // Previous item. If not defined, the previous arrow won't be shown.
@Input() previousTitle?: string; // Previous item title. If not defined, only the arrow will be shown.
@Input() next?: unknown; // Next item. If not defined, the next arrow won't be shown.
@Input() nextTitle?: string; // Next item title. If not defined, only the arrow will be shown.
@Input() info = ''; // Info to show when clicking the info button. If not defined, the info button won't be shown.
@Input() title = ''; // Title to show when seeing the info (new page).
@Input() items: CoreNavigationBarItem[] = []; // List of items.
@Input() showTitles = false; // Display titles on buttons.
@Input() previousTranslate = 'core.previous'; // Previous translatable text, can admit $a variable.
@Input() nextTranslate = 'core.next'; // Next translatable text, can admit $a variable.
@Input() component?: string; // Component the bar belongs to.
@Input() componentId?: number; // Component ID.
@Input() contextLevel?: string; // The context level.
@Input() contextInstanceId?: number; // The instance ID related to the context.
@Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters.
@Output() action?: EventEmitter<unknown> =
new EventEmitter<unknown>(); // Function to call when arrow is clicked. Will receive as a param the item to load.
showInfo(): void {
CoreTextUtils.viewText(this.title, this.info, {
component: this.component,
componentId: this.componentId,
filter: true,
contextLevel: this.contextLevel,
instanceId: this.contextInstanceId,
courseId: this.courseId,
});
previousTitle?: string; // Previous item title.
nextTitle?: string; // Next item title.
previousIndex = -1; // Previous item index. If -1, the previous arrow won't be shown.
nextIndex = -1; // Next item index. If -1, the next arrow won't be shown.
currentIndex = 0;
// Function to call when arrow is clicked. Will receive as a param the item to load.
@Output() action: EventEmitter<unknown> = new EventEmitter<unknown>();
/**
* @inheritdoc
*/
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
if (!changes.items || !this.items.length) {
return;
}
this.currentIndex = this.items.findIndex((item) => item.current);
if (this.currentIndex < 0) {
return;
}
this.nextIndex = this.items[this.currentIndex + 1]?.enabled ? this.currentIndex + 1 : -1;
if (this.nextIndex >= 0) {
this.nextTitle = Translate.instant(this.nextTranslate, { $a: this.items[this.nextIndex].title || '' });
}
this.previousIndex = this.items[this.currentIndex - 1]?.enabled ? this.currentIndex - 1 : -1;
if (this.previousIndex >= 0) {
this.previousTitle = Translate.instant(this.previousTranslate, { $a: this.items[this.previousIndex].title || '' });
}
}
/**
* Navigate to an item.
*
* @param itemIndex Selected item index.
*/
navigate(itemIndex: number): void {
if (this.currentIndex == itemIndex || !this.items[itemIndex].enabled) {
return;
}
this.currentIndex = itemIndex;
this.action.emit(this.items[itemIndex].item);
}
/**
* Navigate to an item with the range component.
*
* @param target: Element changed.
*/
navigateOnRange(target: HTMLIonRangeElement): void {
const selectedIndex = target.value as number; // Single value, use number.
if (!this.items[selectedIndex].enabled) {
target.value = this.currentIndex;
return;
}
this.navigate(selectedIndex);
}
}
export type CoreNavigationBarItem<T = unknown> = {
item: T;
title?: string;
current: boolean;
enabled: boolean;
};

View File

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