2021-02-08 15:45:55 +01:00
|
|
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
2022-02-24 10:42:12 +01:00
|
|
|
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChange } from '@angular/core';
|
|
|
|
import { IonContent } from '@ionic/angular';
|
|
|
|
import { ScrollDetail } from '@ionic/core';
|
|
|
|
import { CoreUtils } from '@services/utils/utils';
|
2021-11-30 13:58:42 +01:00
|
|
|
import { Translate } from '@singletons';
|
2022-02-24 10:42:12 +01:00
|
|
|
import { CoreMath } from '@singletons/math';
|
2021-02-08 15:45:55 +01:00
|
|
|
|
|
|
|
/**
|
2021-11-30 13:58:42 +01:00
|
|
|
* Component to show a "bar" with arrows to navigate forward/backward and an slider to move around.
|
2021-02-08 15:45:55 +01:00
|
|
|
*
|
|
|
|
* This directive will show two arrows at the left and right of the screen to navigate to previous/next item when clicked.
|
2021-11-30 13:58:42 +01:00
|
|
|
* If no previous/next item is defined, that arrow won't be shown.
|
2021-02-08 15:45:55 +01:00
|
|
|
*
|
|
|
|
* Example usage:
|
2021-11-30 13:58:42 +01:00
|
|
|
* <core-navigation-bar [items]="items" (action)="goTo($event)"></core-navigation-bar>
|
2021-02-08 15:45:55 +01:00
|
|
|
*/
|
|
|
|
@Component({
|
|
|
|
selector: 'core-navigation-bar',
|
|
|
|
templateUrl: 'core-navigation-bar.html',
|
|
|
|
styleUrls: ['navigation-bar.scss'],
|
|
|
|
})
|
2022-02-24 10:42:12 +01:00
|
|
|
export class CoreNavigationBarComponent implements OnDestroy, OnChanges {
|
2021-11-30 13:58:42 +01:00
|
|
|
|
|
|
|
@Input() items: CoreNavigationBarItem[] = []; // List of items.
|
|
|
|
@Input() previousTranslate = 'core.previous'; // Previous translatable text, can admit $a variable.
|
|
|
|
@Input() nextTranslate = 'core.next'; // Next translatable text, can admit $a variable.
|
2021-02-08 15:45:55 +01:00
|
|
|
@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.
|
2021-11-30 13:58:42 +01:00
|
|
|
|
|
|
|
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;
|
2022-02-24 09:55:40 +01:00
|
|
|
progress = 0;
|
|
|
|
progressText = '';
|
2021-11-30 13:58:42 +01:00
|
|
|
|
2022-02-24 10:42:12 +01:00
|
|
|
protected element: HTMLElement;
|
|
|
|
protected initialHeight = 0;
|
|
|
|
protected initialPaddingBottom = 0;
|
|
|
|
protected previousTop = 0;
|
|
|
|
protected previousHeight = 0;
|
|
|
|
protected stickTimeout?: number;
|
|
|
|
protected content?: HTMLIonContentElement | null;
|
|
|
|
|
2021-11-30 13:58:42 +01:00
|
|
|
// Function to call when arrow is clicked. Will receive as a param the item to load.
|
|
|
|
@Output() action: EventEmitter<unknown> = new EventEmitter<unknown>();
|
|
|
|
|
2022-02-24 10:42:12 +01:00
|
|
|
constructor(el: ElementRef, protected ionContent: IonContent) {
|
|
|
|
this.element = el.nativeElement;
|
|
|
|
this.element.setAttribute('slot', 'fixed'); // Just in case somebody forgets to add it.
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Setup scroll event listener.
|
|
|
|
*
|
|
|
|
* @param retries Number of retries left.
|
|
|
|
*/
|
|
|
|
protected async listenScrollEvents(retries = 3): Promise<void> {
|
|
|
|
// Already initialized.
|
|
|
|
if (this.initialHeight > 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.initialHeight = this.element.getBoundingClientRect().height;
|
|
|
|
|
|
|
|
if (this.initialHeight == 0 && retries > 0) {
|
|
|
|
await CoreUtils.nextTicks(50);
|
|
|
|
|
|
|
|
this.listenScrollEvents(retries - 1);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Set a minimum height value.
|
|
|
|
this.initialHeight = this.initialHeight || 48;
|
|
|
|
this.previousHeight = this.initialHeight;
|
|
|
|
|
|
|
|
this.content = this.element.closest('ion-content');
|
|
|
|
|
|
|
|
if (!this.content) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.content.classList.add('has-core-navigation');
|
|
|
|
|
|
|
|
// Move element to the nearest ion-content if it's not the parent.
|
|
|
|
if (this.element.parentElement?.nodeName != 'ION-CONTENT') {
|
|
|
|
this.content.appendChild(this.element);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set a padding to not overlap elements.
|
|
|
|
this.initialPaddingBottom = parseFloat(this.content.style.getPropertyValue('--padding-bottom') || '0');
|
|
|
|
this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + this.initialHeight + 'px');
|
|
|
|
const scroll = await this.content.getScrollElement();
|
|
|
|
this.content.scrollEvents = true;
|
|
|
|
|
|
|
|
this.setBarHeight(this.initialHeight);
|
|
|
|
this.content.addEventListener('ionScroll', (e: CustomEvent<ScrollDetail>): void => {
|
|
|
|
if (!this.content) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.onScroll(e.detail, scroll);
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* On scroll function.
|
|
|
|
*
|
|
|
|
* @param scrollDetail Scroll detail object.
|
|
|
|
* @param scrollElement Scroll element to calculate maxScroll.
|
|
|
|
*/
|
|
|
|
protected onScroll(scrollDetail: ScrollDetail, scrollElement: HTMLElement): void {
|
|
|
|
const maxScroll = scrollElement.scrollHeight - scrollElement.offsetHeight;
|
|
|
|
if (scrollDetail.scrollTop <= 0 || scrollDetail.scrollTop >= maxScroll) {
|
|
|
|
// Reset.
|
|
|
|
this.setBarHeight(this.initialHeight);
|
|
|
|
} else {
|
|
|
|
let newHeight = this.previousHeight - (scrollDetail.scrollTop - this.previousTop);
|
|
|
|
newHeight = CoreMath.clamp(newHeight, 0, this.initialHeight);
|
|
|
|
|
|
|
|
this.setBarHeight(newHeight);
|
|
|
|
}
|
|
|
|
this.previousTop = scrollDetail.scrollTop;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the bar height.
|
|
|
|
*
|
|
|
|
* @param height The new bar height.
|
|
|
|
*/
|
|
|
|
protected setBarHeight(height: number): void {
|
|
|
|
if (this.stickTimeout) {
|
|
|
|
clearTimeout(this.stickTimeout);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.element.style.opacity = height <= 0 ? '0' : '1';
|
|
|
|
this.content?.style.setProperty('--core-navigation-height', height + 'px');
|
|
|
|
this.previousHeight = height;
|
|
|
|
|
|
|
|
if (height > 0 && height < this.initialHeight) {
|
|
|
|
// Finish opening or closing the bar.
|
|
|
|
const newHeight = height < this.initialHeight / 2 ? 0 : this.initialHeight;
|
|
|
|
|
|
|
|
this.stickTimeout = window.setTimeout(() => this.setBarHeight(newHeight), 500);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-30 13:58:42 +01:00
|
|
|
/**
|
|
|
|
* @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;
|
|
|
|
}
|
|
|
|
|
2022-02-24 09:55:40 +01:00
|
|
|
this.progress = ((this.currentIndex + 1) / this.items.length) * 100;
|
|
|
|
this.progressText = `${this.currentIndex + 1} / ${this.items.length}`;
|
|
|
|
|
2021-11-30 13:58:42 +01:00
|
|
|
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 || '' });
|
|
|
|
}
|
2022-02-24 10:42:12 +01:00
|
|
|
|
|
|
|
this.listenScrollEvents();
|
2021-11-30 13:58:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
|
2022-02-24 10:42:12 +01:00
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
async ngOnDestroy(): Promise<void> {
|
|
|
|
this.content?.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px');
|
|
|
|
}
|
|
|
|
|
2021-02-08 15:45:55 +01:00
|
|
|
}
|
2021-11-30 13:58:42 +01:00
|
|
|
|
|
|
|
export type CoreNavigationBarItem<T = unknown> = {
|
|
|
|
item: T;
|
|
|
|
title?: string;
|
|
|
|
current: boolean;
|
|
|
|
enabled: boolean;
|
|
|
|
};
|