From 533fe8e1b443968ab41c99bba2b984fd29530885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 24 Feb 2022 10:42:12 +0100 Subject: [PATCH] MOBILE-3996 navbar: Add navbar at the bottom which disappears on scroll --- .../mod/book/pages/contents/contents.html | 12 +- .../mod/book/pages/contents/contents.scss | 8 +- .../index/addon-mod-imscp-index.html | 7 +- src/addons/mod/scorm/pages/player/player.html | 7 +- src/core/classes/tabs.ts | 3 +- .../navigation-bar/navigation-bar.scss | 6 +- .../navigation-bar/navigation-bar.ts | 126 +++++++++++++++++- src/theme/theme.base.scss | 12 +- 8 files changed, 163 insertions(+), 18 deletions(-) diff --git a/src/addons/mod/book/pages/contents/contents.html b/src/addons/mod/book/pages/contents/contents.html index 9cb91f696..a25501544 100644 --- a/src/addons/mod/book/pages/contents/contents.html +++ b/src/addons/mod/book/pages/contents/contents.html @@ -21,9 +21,8 @@ - +
- @@ -31,10 +30,6 @@ - - -
@@ -49,4 +44,9 @@
+ + + diff --git a/src/addons/mod/book/pages/contents/contents.scss b/src/addons/mod/book/pages/contents/contents.scss index de654bcac..823126ed0 100644 --- a/src/addons/mod/book/pages/contents/contents.scss +++ b/src/addons/mod/book/pages/contents/contents.scss @@ -1,6 +1,12 @@ :host { + core-loading ::ng-deep .core-loading-content { + min-height: 100%; + display: flex; + flex-direction: column; + } + .core-swipe-slides-container { - ion-card, core-navigation-bar { + ion-card { flex: none; } } diff --git a/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html b/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html index a3ce89920..ea00f00ce 100644 --- a/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html +++ b/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html @@ -10,7 +10,7 @@ - + @@ -24,12 +24,13 @@
- -
+ + + diff --git a/src/addons/mod/scorm/pages/player/player.html b/src/addons/mod/scorm/pages/player/player.html index 73bb4de59..f50283787 100644 --- a/src/addons/mod/scorm/pages/player/player.html +++ b/src/addons/mod/scorm/pages/player/player.html @@ -20,13 +20,14 @@ - - - +

{{ errorMessage | translate }}

+ + +
diff --git a/src/core/classes/tabs.ts b/src/core/classes/tabs.ts index 76527de07..ca644c64e 100644 --- a/src/core/classes/tabs.ts +++ b/src/core/classes/tabs.ts @@ -85,7 +85,8 @@ export class CoreTabsBaseComponent implements OnInit, Aft protected firstSelectedTab?: string; // ID of the first selected tab to control history. protected backButtonFunction: (event: BackButtonEvent) => void; 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 slidesSwiperLoaded = false; protected scrollElements: Record = {}; // Scroll elements for each loaded tab. diff --git a/src/core/components/navigation-bar/navigation-bar.scss b/src/core/components/navigation-bar/navigation-bar.scss index a007f12b3..42a6491d7 100644 --- a/src/core/components/navigation-bar/navigation-bar.scss +++ b/src/core/components/navigation-bar/navigation-bar.scss @@ -20,5 +20,9 @@ margin-top: var(--button-vertical-margin); margin-bottom: var(--button-vertical-margin); } - +} + +:host-context(.core-iframe-fullscreen) { + opacity: 0 !important; + height: 0 !important; } diff --git a/src/core/components/navigation-bar/navigation-bar.ts b/src/core/components/navigation-bar/navigation-bar.ts index 4f1f76b80..4fa88cfe6 100644 --- a/src/core/components/navigation-bar/navigation-bar.ts +++ b/src/core/components/navigation-bar/navigation-bar.ts @@ -12,8 +12,12 @@ // See the License for the specific language governing permissions and // 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 { CoreMath } from '@singletons/math'; /** * 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', styleUrls: ['navigation-bar.scss'], }) -export class CoreNavigationBarComponent implements OnChanges { +export class CoreNavigationBarComponent implements OnDestroy, OnChanges { @Input() items: CoreNavigationBarItem[] = []; // List of items. @Input() previousTranslate = 'core.previous'; // Previous translatable text, can admit $a variable. @@ -48,9 +52,118 @@ export class CoreNavigationBarComponent implements OnChanges { progress = 0; 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. @Output() action: EventEmitter = new EventEmitter(); + 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 { + // 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): 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 */ @@ -76,6 +189,8 @@ export class CoreNavigationBarComponent implements OnChanges { if (this.previousIndex >= 0) { 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); } + /** + * @inheritdoc + */ + async ngOnDestroy(): Promise { + this.content?.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px'); + } + } export type CoreNavigationBarItem = { diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index e2fbd0a46..822b5b991 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -1540,9 +1540,19 @@ body.core-iframe-fullscreen ion-content { .core-swipe-slides-container { display: flex; flex-direction: column; - height: 100%; + flex-grow: 1; + min-height: 100%; core-swipe-slides { + display: flex; + flex-direction: column; flex-grow: 1; + + ion-slides { + flex-grow: 1; + max-width: 100%; + } } + + }