MOBILE-3996 navbar: Add navbar at the bottom which disappears on scroll
parent
294c94b934
commit
533fe8e1b4
|
@ -21,9 +21,8 @@
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
</ion-refresher>
|
</ion-refresher>
|
||||||
|
|
||||||
<core-loading [hideUntil]="loaded" class="core-loading-full-height">
|
<core-loading [hideUntil]="loaded">
|
||||||
<div class="safe-area-padding-horizontal core-swipe-slides-container">
|
<div class="safe-area-padding-horizontal core-swipe-slides-container">
|
||||||
|
|
||||||
<ion-card class="core-warning-card" *ngIf="warning">
|
<ion-card class="core-warning-card" *ngIf="warning">
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
|
||||||
|
@ -31,10 +30,6 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
<core-navigation-bar *ngIf="displayNavBar" [items]="navigationItems" previousTranslate="addon.mod_book.navprevtitle"
|
|
||||||
nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)">
|
|
||||||
</core-navigation-bar>
|
|
||||||
|
|
||||||
<core-swipe-slides [manager]="manager" [options]="slidesOpts">
|
<core-swipe-slides [manager]="manager" [options]="slidesOpts">
|
||||||
<ng-template let-chapter="item">
|
<ng-template let-chapter="item">
|
||||||
<div class="ion-padding">
|
<div class="ion-padding">
|
||||||
|
@ -49,4 +44,9 @@
|
||||||
</core-swipe-slides>
|
</core-swipe-slides>
|
||||||
</div>
|
</div>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
||||||
|
<core-navigation-bar *ngIf="loaded && displayNavBar && navigationItems.length > 1" [items]="navigationItems"
|
||||||
|
previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)"
|
||||||
|
slot="fixed">
|
||||||
|
</core-navigation-bar>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
:host {
|
:host {
|
||||||
|
core-loading ::ng-deep .core-loading-content {
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.core-swipe-slides-container {
|
.core-swipe-slides-container {
|
||||||
ion-card, core-navigation-bar {
|
ion-card {
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</core-navbar-buttons>
|
</core-navbar-buttons>
|
||||||
|
|
||||||
<!-- Content. -->
|
<!-- Content. -->
|
||||||
<core-loading [hideUntil]="!showLoading" class="safe-area-padding">
|
<core-loading [hideUntil]="!showLoading" class="safe-area-padding core-loading-full-height">
|
||||||
|
|
||||||
<!-- Activity info. -->
|
<!-- Activity info. -->
|
||||||
<core-course-module-info [module]="module">
|
<core-course-module-info [module]="module">
|
||||||
|
@ -24,12 +24,13 @@
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
<div class="addon-mod-imscp-container">
|
<div class="addon-mod-imscp-container">
|
||||||
<core-navigation-bar [items]="navigationItems" (action)="loadItem($event)">
|
|
||||||
</core-navigation-bar>
|
|
||||||
<core-iframe *ngIf="!showLoading" [src]="src" [showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="true"></core-iframe>
|
<core-iframe *ngIf="!showLoading" [src]="src" [showFullscreenOnToolbar]="true" [autoFullscreenOnRotate]="true"></core-iframe>
|
||||||
</div>
|
</div>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
||||||
|
<!-- TODO Add a contents page to avoid having both bars -->
|
||||||
|
<core-navigation-bar *ngIf="!showLoading && navigationItems.length > 1 && false" [items]="navigationItems" (action)="loadItem($event)">
|
||||||
|
</core-navigation-bar>
|
||||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||||
(completionChanged)="onCompletionChange()">
|
(completionChanged)="onCompletionChange()">
|
||||||
</core-course-module-navigation>
|
</core-course-module-navigation>
|
||||||
|
|
|
@ -20,13 +20,14 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-loading [hideUntil]="loaded" class="core-loading-full-height">
|
||||||
<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">
|
||||||
</core-iframe>
|
</core-iframe>
|
||||||
|
|
||||||
<p *ngIf="!src && errorMessage">{{ errorMessage | translate }}</p>
|
<p *ngIf="!src && errorMessage">{{ errorMessage | translate }}</p>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
||||||
|
<core-navigation-bar *ngIf="loaded && navigationItems.length > 1" [items]="navigationItems" (action)="loadSco($event)" slot="fixed">
|
||||||
|
</core-navigation-bar>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -85,7 +85,8 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
|
||||||
protected firstSelectedTab?: string; // ID of the first selected tab to control history.
|
protected firstSelectedTab?: string; // ID of the first selected tab to control history.
|
||||||
protected backButtonFunction: (event: BackButtonEvent) => void;
|
protected backButtonFunction: (event: BackButtonEvent) => void;
|
||||||
protected languageChangedSubscription?: Subscription;
|
protected languageChangedSubscription?: Subscription;
|
||||||
protected isInTransition = false; // Weather Slides is in transition.
|
// Swiper 6 documentation: https://swiper6.vercel.app/
|
||||||
|
protected isInTransition = false; // Wether Slides is in transition.
|
||||||
protected slidesSwiper: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
protected slidesSwiper: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
protected slidesSwiperLoaded = false;
|
protected slidesSwiperLoaded = false;
|
||||||
protected scrollElements: Record<string | number, HTMLElement> = {}; // Scroll elements for each loaded tab.
|
protected scrollElements: Record<string | number, HTMLElement> = {}; // Scroll elements for each loaded tab.
|
||||||
|
|
|
@ -20,5 +20,9 @@
|
||||||
margin-top: var(--button-vertical-margin);
|
margin-top: var(--button-vertical-margin);
|
||||||
margin-bottom: var(--button-vertical-margin);
|
margin-bottom: var(--button-vertical-margin);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.core-iframe-fullscreen) {
|
||||||
|
opacity: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,12 @@
|
||||||
// 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, OnChanges, Output, SimpleChange } from '@angular/core';
|
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';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
|
import { CoreMath } from '@singletons/math';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to show a "bar" with arrows to navigate forward/backward and an slider to move around.
|
* Component to show a "bar" with arrows to navigate forward/backward and an slider to move around.
|
||||||
|
@ -29,7 +33,7 @@ import { Translate } from '@singletons';
|
||||||
templateUrl: 'core-navigation-bar.html',
|
templateUrl: 'core-navigation-bar.html',
|
||||||
styleUrls: ['navigation-bar.scss'],
|
styleUrls: ['navigation-bar.scss'],
|
||||||
})
|
})
|
||||||
export class CoreNavigationBarComponent implements OnChanges {
|
export class CoreNavigationBarComponent implements OnDestroy, OnChanges {
|
||||||
|
|
||||||
@Input() items: CoreNavigationBarItem[] = []; // List of items.
|
@Input() items: CoreNavigationBarItem[] = []; // List of items.
|
||||||
@Input() previousTranslate = 'core.previous'; // Previous translatable text, can admit $a variable.
|
@Input() previousTranslate = 'core.previous'; // Previous translatable text, can admit $a variable.
|
||||||
|
@ -48,9 +52,118 @@ export class CoreNavigationBarComponent implements OnChanges {
|
||||||
progress = 0;
|
progress = 0;
|
||||||
progressText = '';
|
progressText = '';
|
||||||
|
|
||||||
|
protected element: HTMLElement;
|
||||||
|
protected initialHeight = 0;
|
||||||
|
protected initialPaddingBottom = 0;
|
||||||
|
protected previousTop = 0;
|
||||||
|
protected previousHeight = 0;
|
||||||
|
protected stickTimeout?: number;
|
||||||
|
protected content?: HTMLIonContentElement | null;
|
||||||
|
|
||||||
// Function to call when arrow is clicked. Will receive as a param the item to load.
|
// Function to call when arrow is clicked. Will receive as a param the item to load.
|
||||||
@Output() action: EventEmitter<unknown> = new EventEmitter<unknown>();
|
@Output() action: EventEmitter<unknown> = new EventEmitter<unknown>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
@ -76,6 +189,8 @@ export class CoreNavigationBarComponent implements OnChanges {
|
||||||
if (this.previousIndex >= 0) {
|
if (this.previousIndex >= 0) {
|
||||||
this.previousTitle = Translate.instant(this.previousTranslate, { $a: this.items[this.previousIndex].title || '' });
|
this.previousTitle = Translate.instant(this.previousTranslate, { $a: this.items[this.previousIndex].title || '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.listenScrollEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -92,6 +207,13 @@ export class CoreNavigationBarComponent implements OnChanges {
|
||||||
this.action.emit(this.items[itemIndex].item);
|
this.action.emit(this.items[itemIndex].item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnDestroy(): Promise<void> {
|
||||||
|
this.content?.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CoreNavigationBarItem<T = unknown> = {
|
export type CoreNavigationBarItem<T = unknown> = {
|
||||||
|
|
|
@ -1540,9 +1540,19 @@ body.core-iframe-fullscreen ion-content {
|
||||||
.core-swipe-slides-container {
|
.core-swipe-slides-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
flex-grow: 1;
|
||||||
|
min-height: 100%;
|
||||||
|
|
||||||
core-swipe-slides {
|
core-swipe-slides {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
ion-slides {
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue