MOBILE-3996 navbar: Move collapsible footer feature to a new directive
parent
533fe8e1b4
commit
b5e2071318
|
@ -127,6 +127,6 @@
|
|||
</addon-mod-assign-submission>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -112,6 +112,6 @@
|
|||
</ng-container>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -41,6 +41,6 @@
|
|||
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
</div>
|
||||
</core-loading>
|
||||
|
||||
<core-navigation-bar *ngIf="loaded && displayNavBar && navigationItems.length > 1" [items]="navigationItems"
|
||||
<core-navigation-bar collapsible-footer *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>
|
||||
|
|
|
@ -30,6 +30,6 @@
|
|||
</ng-container>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -135,7 +135,7 @@
|
|||
</ion-card>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
||||
|
|
|
@ -120,7 +120,7 @@
|
|||
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
</core-tabs>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
||||
|
|
|
@ -32,6 +32,6 @@
|
|||
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
</ng-container>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
</core-infinite-loading>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
||||
|
|
|
@ -68,6 +68,6 @@
|
|||
</core-h5p-iframe>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -29,8 +29,9 @@
|
|||
</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 collapsible-footer *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 collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -279,6 +279,6 @@
|
|||
</core-tabs>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -21,6 +21,6 @@
|
|||
</div>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -32,6 +32,6 @@
|
|||
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -206,6 +206,6 @@
|
|||
</ion-card>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -97,6 +97,6 @@
|
|||
</ng-container>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -217,6 +217,6 @@
|
|||
</ng-container>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
<p *ngIf="!src && errorMessage">{{ errorMessage | translate }}</p>
|
||||
</core-loading>
|
||||
|
||||
<core-navigation-bar *ngIf="loaded && navigationItems.length > 1" [items]="navigationItems" (action)="loadSco($event)" slot="fixed">
|
||||
<core-navigation-bar collapsible-footer *ngIf="loaded && navigationItems.length > 1" [items]="navigationItems"
|
||||
(action)="loadSco($event)" slot="fixed">
|
||||
</core-navigation-bar>
|
||||
</ion-content>
|
||||
|
|
|
@ -131,6 +131,6 @@
|
|||
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -46,6 +46,6 @@
|
|||
</ion-list>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
</div>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
||||
|
|
|
@ -233,6 +233,6 @@
|
|||
</div>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host {
|
||||
--height: var(--core-navigation-height, var(--core-navigation-max-height));
|
||||
--height: var(--core-navigation-max-height);
|
||||
--background: var(--core-navigation-background);
|
||||
--button-vertical-margin: 2px;
|
||||
|
||||
|
@ -9,20 +9,11 @@
|
|||
width: 100%;
|
||||
background-color: var(--background);
|
||||
display: block;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
border-top: 1px solid var(--stroke);
|
||||
|
||||
@include core-transition(all, 200ms);
|
||||
|
||||
ion-button,
|
||||
::ng-deep ion-button {
|
||||
margin-top: var(--button-vertical-margin);
|
||||
margin-bottom: var(--button-vertical-margin);
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.core-iframe-fullscreen) {
|
||||
opacity: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
|
|
@ -12,18 +12,14 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
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 { Component, EventEmitter, Input, OnChanges, Output, SimpleChange } from '@angular/core';
|
||||
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 progressbar to see the status.
|
||||
*
|
||||
* 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.
|
||||
* If no previous/next item is defined, that arrow will be disabled.
|
||||
*
|
||||
* Example usage:
|
||||
* <core-navigation-bar [items]="items" (action)="goTo($event)"></core-navigation-bar>
|
||||
|
@ -33,7 +29,7 @@ import { CoreMath } from '@singletons/math';
|
|||
templateUrl: 'core-navigation-bar.html',
|
||||
styleUrls: ['navigation-bar.scss'],
|
||||
})
|
||||
export class CoreNavigationBarComponent implements OnDestroy, OnChanges {
|
||||
export class CoreNavigationBarComponent implements OnChanges {
|
||||
|
||||
@Input() items: CoreNavigationBarItem[] = []; // List of items.
|
||||
@Input() previousTranslate = 'core.previous'; // Previous translatable text, can admit $a variable.
|
||||
|
@ -52,118 +48,9 @@ export class CoreNavigationBarComponent implements OnDestroy, 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<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
|
||||
*/
|
||||
|
@ -189,8 +76,6 @@ export class CoreNavigationBarComponent implements OnDestroy, OnChanges {
|
|||
if (this.previousIndex >= 0) {
|
||||
this.previousTitle = Translate.instant(this.previousTranslate, { $a: this.items[this.previousIndex].title || '' });
|
||||
}
|
||||
|
||||
this.listenScrollEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -207,13 +92,6 @@ export class CoreNavigationBarComponent implements OnDestroy, OnChanges {
|
|||
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> = {
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
// (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.
|
||||
|
||||
import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ScrollDetail } from '@ionic/core';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreMath } from '@singletons/math';
|
||||
|
||||
/**
|
||||
* Directive to make an element fixed at the bottom collapsible when scrolling.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <div collapsible-footer>
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[collapsible-footer]',
|
||||
})
|
||||
export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
|
||||
|
||||
protected element: HTMLElement;
|
||||
protected initialHeight = 0;
|
||||
protected initialPaddingBottom = 0;
|
||||
protected previousTop = 0;
|
||||
protected previousHeight = 0;
|
||||
protected stickTimeout?: number;
|
||||
protected content?: HTMLIonContentElement | null;
|
||||
|
||||
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 = 5): 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-collapsible-footer');
|
||||
|
||||
// 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.classList.toggle('footer-collapsed', height <= 0);
|
||||
this.element.classList.toggle('footer-expanded', height >= this.initialHeight);
|
||||
this.content?.style.setProperty('--core-collapsible-footer-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
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.listenScrollEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnDestroy(): Promise<void> {
|
||||
this.content?.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px');
|
||||
}
|
||||
|
||||
}
|
|
@ -30,6 +30,7 @@ import { CoreDownloadFileDirective } from './download-file';
|
|||
import { CoreCollapsibleHeaderDirective } from './collapsible-header';
|
||||
import { CoreSwipeNavigationDirective } from './swipe-navigation';
|
||||
import { CoreCollapsibleItemDirective } from './collapsible-item';
|
||||
import { CoreCollapsibleFooterDirective } from './collapsible-footer';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -49,6 +50,7 @@ import { CoreCollapsibleItemDirective } from './collapsible-item';
|
|||
CoreCollapsibleHeaderDirective,
|
||||
CoreSwipeNavigationDirective,
|
||||
CoreCollapsibleItemDirective,
|
||||
CoreCollapsibleFooterDirective,
|
||||
],
|
||||
exports: [
|
||||
CoreAutoFocusDirective,
|
||||
|
@ -67,6 +69,7 @@ import { CoreCollapsibleItemDirective } from './collapsible-item';
|
|||
CoreCollapsibleHeaderDirective,
|
||||
CoreSwipeNavigationDirective,
|
||||
CoreCollapsibleItemDirective,
|
||||
CoreCollapsibleFooterDirective,
|
||||
],
|
||||
})
|
||||
export class CoreDirectivesModule {}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host {
|
||||
--height: var(--core-navigation-height, var(--core-navigation-max-height));
|
||||
--height: var(--core-navigation-max-height);
|
||||
--background: var(--core-navigation-background);
|
||||
--button-vertical-margin: 2px;
|
||||
|
||||
|
@ -9,12 +9,8 @@
|
|||
width: 100%;
|
||||
background-color: var(--background);
|
||||
display: block;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
border-top: 1px solid var(--stroke);
|
||||
|
||||
@include core-transition(all, 200ms);
|
||||
|
||||
core-loading {
|
||||
text-align: center;
|
||||
--loading-inline-min-height: var(--height);
|
||||
|
@ -27,11 +23,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
:host-context(.core-iframe-fullscreen) {
|
||||
opacity: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
:host-context(core-course-format.core-course-format-singleactivity) {
|
||||
opacity: 0 !important;
|
||||
height: 0 !important;
|
||||
|
|
|
@ -17,13 +17,11 @@ import { CoreCourse, CoreCourseProvider, CoreCourseWSSection } from '@features/c
|
|||
import { CoreCourseModuleCompletionData, CoreCourseModuleData } from '@features/course/services/course-helper';
|
||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
import { ScrollDetail } from '@ionic/core';
|
||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreMath } from '@singletons/math';
|
||||
|
||||
/**
|
||||
* Component to show a button to go to the next resource/activity.
|
||||
|
@ -51,21 +49,11 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
|
|||
loaded = false;
|
||||
showCompletion = false; // Whether to show completion.
|
||||
|
||||
protected element: HTMLElement;
|
||||
protected initialHeight = 0;
|
||||
protected initialPaddingBottom = 0;
|
||||
protected previousTop = 0;
|
||||
protected previousHeight = 0;
|
||||
protected stickTimeout?: number;
|
||||
protected content?: HTMLIonContentElement | null;
|
||||
protected completionObserver: CoreEventObserver;
|
||||
|
||||
constructor(el: ElementRef, protected ionContent: IonContent) {
|
||||
const siteId = CoreSites.getCurrentSiteId();
|
||||
|
||||
this.element = el.nativeElement;
|
||||
this.element.setAttribute('slot', 'fixed');
|
||||
|
||||
this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_MODULE_VIEWED, async (data) => {
|
||||
if (data && data.courseId == this.courseId) {
|
||||
// Check if now there's a next module.
|
||||
|
@ -88,76 +76,14 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
|
|||
await this.setNextAndPreviousModules(CoreSitesReadingStrategy.PREFER_CACHE);
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
|
||||
await CoreUtils.nextTicks(50);
|
||||
this.listenScrollEvents();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup scroll event listener.
|
||||
*
|
||||
* @param retries Number of retries left.
|
||||
*/
|
||||
protected async listenScrollEvents(retries = 3): Promise<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
// Special case where there's no navigation.
|
||||
const courseFormat = this.element.closest('core-course-format.core-course-format-singleactivity');
|
||||
if (courseFormat) {
|
||||
this.element.remove();
|
||||
this.ngOnDestroy();
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnDestroy(): Promise<void> {
|
||||
this.completionObserver.off();
|
||||
this.content?.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -316,46 +242,4 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -44,6 +44,6 @@
|
|||
<core-course-unsupported-module *ngIf="unsupported" [module]="module"></core-course-unsupported-module>
|
||||
</core-loading>
|
||||
|
||||
<core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModule]="module"
|
||||
<core-course-module-navigation collapsible-footer [hidden]="!loaded" [courseId]="courseId" [currentModule]="module"
|
||||
(completionChanged)="onCompletionChange()" [showManualCompletion]="showManualCompletion"></core-course-module-navigation>
|
||||
</ion-content>
|
||||
|
|
|
@ -14,5 +14,5 @@
|
|||
(onLoadingContent)="contentLoading()">
|
||||
</core-site-plugins-plugin-content>
|
||||
|
||||
<core-course-module-navigation *ngIf="module" [courseId]="courseId" [currentModule]="module">
|
||||
<core-course-module-navigation collapsible-footer *ngIf="module" [courseId]="courseId" [currentModule]="module">
|
||||
</core-course-module-navigation>
|
||||
|
|
|
@ -1124,7 +1124,7 @@ ion-fab[core-fab] {
|
|||
}
|
||||
}
|
||||
|
||||
ion-content.has-core-navigation ion-fab {
|
||||
ion-content.has-collapsible-footer ion-fab {
|
||||
bottom: calc(var(--core-navigation-height, 0px) + 10px);
|
||||
@include core-transition(all, 200ms);
|
||||
}
|
||||
|
@ -1442,6 +1442,30 @@ ion-grid.core-no-grid > ion-row {
|
|||
@include collapsible-item();
|
||||
}
|
||||
|
||||
[collapsible-footer] {
|
||||
&.footer-collapsed {
|
||||
--core-collapsible-footer-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
&.footer-expanded {
|
||||
--core-collapsible-footer-height: auto;
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
height: var(--core-collapsible-footer-height, auto);
|
||||
background-color: var(--core-collapsible-footer-background);
|
||||
display: block;
|
||||
border-top: 1px solid var(--stroke);
|
||||
@include core-transition(all, 200ms);
|
||||
}
|
||||
|
||||
.core-iframe-fullscreen [collapsible-footer] {
|
||||
opacity: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
ion-header.no-title {
|
||||
--core-header-toolbar-border-width: 0;
|
||||
--core-header-toolbar-background: transparent;
|
||||
|
|
|
@ -131,6 +131,8 @@
|
|||
|
||||
--core-navigation-background: var(--contrast-background);
|
||||
|
||||
--core-collapsible-footer-background: var(--contrast-background);
|
||||
|
||||
--addon-messages-message-bg: var(--gray-800);
|
||||
--addon-messages-message-activated-bg: var(--gray-700);
|
||||
--addon-messages-message-note-text: var(--subdued-text-color);
|
||||
|
|
|
@ -310,9 +310,11 @@
|
|||
--core-courseimage-on-course-size: 72px;
|
||||
--core-courseimage-radius: var(--medium-radius);
|
||||
|
||||
--core-navigation-max-height: 48px;
|
||||
--core-navigation-height: 48px;
|
||||
--core-navigation-background: var(--contrast-background);
|
||||
|
||||
--core-collapsible-footer-background: var(--contrast-background);
|
||||
|
||||
--core-user-menu-site-logo-max-height: 32px;
|
||||
|
||||
--addon-calendar-today-border-color: var(--primary);
|
||||
|
|
Loading…
Reference in New Issue