Merge pull request #3140 from crazyserver/MOBILE-3996

Mobile 3996
main
Dani Palou 2022-02-24 16:04:36 +01:00 committed by GitHub
commit 0f3aa84aec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 291 additions and 230 deletions

View File

@ -127,6 +127,6 @@
</addon-mod-assign-submission> </addon-mod-assign-submission>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -112,6 +112,6 @@
</ng-container> </ng-container>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -41,6 +41,6 @@
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -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,11 +30,6 @@
</ion-item> </ion-item>
</ion-card> </ion-card>
<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>
<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">
@ -50,4 +44,9 @@
</core-swipe-slides> </core-swipe-slides>
</div> </div>
</core-loading> </core-loading>
<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>
</ion-content> </ion-content>

View File

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

View File

@ -62,7 +62,6 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy {
warning = ''; warning = '';
displayNavBar = true; displayNavBar = true;
navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = []; navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
displayTitlesInNavBar = false;
slidesOpts: CoreSwipeSlidesOptions = { slidesOpts: CoreSwipeSlidesOptions = {
autoHeight: true, autoHeight: true,
scrollOnChange: 'top', scrollOnChange: 'top',
@ -135,7 +134,6 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy {
const downloadResult = await this.downloadResourceIfNeeded(module, refresh); const downloadResult = await this.downloadResourceIfNeeded(module, refresh);
this.displayNavBar = book.navstyle != AddonModBookNavStyle.TOC_ONLY; this.displayNavBar = book.navstyle != AddonModBookNavStyle.TOC_ONLY;
this.displayTitlesInNavBar = book.navstyle == AddonModBookNavStyle.TEXT;
this.title = book.name; this.title = book.name;
// Get contents. No need to refresh, it has been done in downloadResourceIfNeeded. // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded.

View File

@ -30,6 +30,6 @@
</ng-container> </ng-container>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -135,7 +135,7 @@
</ion-card> </ion-card>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -120,7 +120,7 @@
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -35,7 +35,7 @@
</core-tabs> </core-tabs>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -32,6 +32,6 @@
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -114,7 +114,7 @@
</ng-container> </ng-container>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -72,7 +72,7 @@
</core-infinite-loading> </core-infinite-loading>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -68,6 +68,6 @@
</core-h5p-iframe> </core-h5p-iframe>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -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,14 @@
</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>
<core-course-module-navigation [hidden]="showLoading" [courseId]="courseId" [currentModule]="module" <!-- TODO Add a contents page to avoid having both bars -->
<core-navigation-bar collapsible-footer *ngIf="!showLoading && navigationItems.length > 1 && false" [items]="navigationItems"
(action)="loadItem($event)">
</core-navigation-bar>
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModule]="module"
(completionChanged)="onCompletionChange()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -279,6 +279,6 @@
</core-tabs> </core-tabs>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -21,6 +21,6 @@
</div> </div>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -32,6 +32,6 @@
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -206,6 +206,6 @@
</ion-card> </ion-card>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -97,6 +97,6 @@
</ng-container> </ng-container>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -217,6 +217,6 @@
</ng-container> </ng-container>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -20,13 +20,15 @@
</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 collapsible-footer *ngIf="loaded && navigationItems.length > 1" [items]="navigationItems"
(action)="loadSco($event)" slot="fixed">
</core-navigation-bar>
</ion-content> </ion-content>

View File

@ -131,6 +131,6 @@
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -46,6 +46,6 @@
</ion-list> </ion-list>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -71,7 +71,7 @@
</div> </div>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

@ -233,6 +233,6 @@
</div> </div>
</core-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()"> (completionChanged)="onCompletionChange()">
</core-course-module-navigation> </core-course-module-navigation>

View File

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

View File

@ -1,27 +1,17 @@
<ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding" <ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding ion-wrap" *ngIf="items.length > 1">
*ngIf="previousIndex >= 0 || nextIndex >= 0 || items.length > 1"> <ion-col class="ion-text-start ion-no-padding core-navigation-arrow" size="auto">
<ion-col class="ion-text-start ion-padding-end" [size]="showTitles ? 4 : 3"> <ion-button [disabled]="previousIndex < 0" fill="clear" color="dark" [attr.aria-label]="previousTitle"
<ion-button *ngIf="previousIndex >=0" class="core-navigation-bar-arrow" fill="clear" [attr.aria-label]="previousTitle"
(click)="navigate(previousIndex)"> (click)="navigate(previousIndex)">
<ion-icon name="fas-arrow-left" [slot]="showTitles ? 'start' : 'icon-only'" aria-hidden="true"></ion-icon> <ion-icon name="fas-chevron-left" slot="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-button>
</ion-col> </ion-col>
<ion-col class="ion-text-center" [size]="showTitles ? 4 : 6"> <ion-col class="ion-text-center">
<ion-range min="0" [max]="items.length -1" debounce="500" snaps="true" (ionChange)="navigateOnRange($event.target)" <core-progress-bar [progress]="progress" [text]="progressText" *ngIf="currentIndex >= 0">
[value]="currentIndex"> </core-progress-bar>
<p slot="end">{{currentIndex + 1}} / {{items.length}}</p>
</ion-range>
</ion-col> </ion-col>
<ion-col class="ion-text-end ion-padding-start" [size]="showTitles ? 4 : 3"> <ion-col class="ion-text-end ion-no-padding core-navigation-arrow" size="auto">
<ion-button fill="clear" *ngIf="nextIndex >= 0" class="core-navigation-bar-arrow" [attr.aria-label]="nextTitle" <ion-button [disabled]="nextIndex < 0" fill="clear" color="dark" [attr.aria-label]="nextTitle" (click)="navigate(nextIndex)">
(click)="navigate(nextIndex)"> <ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
<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]="showTitles ? 'end' : 'icon-only'" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
</ion-col> </ion-col>
</ion-row> </ion-row>

View File

@ -1,15 +1,19 @@
:host { @import "~theme/globals";
--background: var(--core-course-module-navigation-background);
:host {
--height: var(--core-navigation-max-height);
--background: var(--core-navigation-background);
--button-vertical-margin: 2px;
height: var(--height);
width: 100%; width: 100%;
background-color: var(--background); background-color: var(--background);
display: block; display: block;
border-top: 1px solid var(--stroke);
.core-navigation-bar-arrow { ion-button,
text-transform: none; ::ng-deep ion-button {
max-width: 100%; margin-top: var(--button-vertical-margin);
ion-icon { margin-bottom: var(--button-vertical-margin);
flex-shrink: 0;
}
} }
} }

View File

@ -16,10 +16,10 @@ import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange } from
import { Translate } from '@singletons'; import { Translate } from '@singletons';
/** /**
* 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. * 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: * Example usage:
* <core-navigation-bar [items]="items" (action)="goTo($event)"></core-navigation-bar> * <core-navigation-bar [items]="items" (action)="goTo($event)"></core-navigation-bar>
@ -32,7 +32,6 @@ import { Translate } from '@singletons';
export class CoreNavigationBarComponent implements OnChanges { export class CoreNavigationBarComponent implements OnChanges {
@Input() items: CoreNavigationBarItem[] = []; // List of items. @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() previousTranslate = 'core.previous'; // Previous translatable text, can admit $a variable.
@Input() nextTranslate = 'core.next'; // Next 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() component?: string; // Component the bar belongs to.
@ -46,6 +45,8 @@ export class CoreNavigationBarComponent implements OnChanges {
previousIndex = -1; // Previous item index. If -1, the previous arrow won't be shown. 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. nextIndex = -1; // Next item index. If -1, the next arrow won't be shown.
currentIndex = 0; currentIndex = 0;
progress = 0;
progressText = '';
// 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>();
@ -63,6 +64,9 @@ export class CoreNavigationBarComponent implements OnChanges {
return; return;
} }
this.progress = ((this.currentIndex + 1) / this.items.length) * 100;
this.progressText = `${this.currentIndex + 1} / ${this.items.length}`;
this.nextIndex = this.items[this.currentIndex + 1]?.enabled ? this.currentIndex + 1 : -1; this.nextIndex = this.items[this.currentIndex + 1]?.enabled ? this.currentIndex + 1 : -1;
if (this.nextIndex >= 0) { if (this.nextIndex >= 0) {
this.nextTitle = Translate.instant(this.nextTranslate, { $a: this.items[this.nextIndex].title || '' }); this.nextTitle = Translate.instant(this.nextTranslate, { $a: this.items[this.nextIndex].title || '' });
@ -88,22 +92,6 @@ export class CoreNavigationBarComponent implements OnChanges {
this.action.emit(this.items[itemIndex].item); 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> = { export type CoreNavigationBarItem<T = unknown> = {

View File

@ -4,7 +4,7 @@
</progress> </progress>
<div class="core-progress-text"> <div class="core-progress-text">
<span class="sr-only" *ngIf="a11yText">{{ a11yText | translate }}</span> <span class="sr-only" *ngIf="a11yText">{{ a11yText | translate }}</span>
{{ 'core.percentagenumber' | translate: {$a: text} }} {{ text }}
</div> </div>
</ng-container> </ng-container>

View File

@ -86,7 +86,7 @@ export class CoreProgressBarComponent implements OnInit, OnChanges {
this.progress = Math.floor(this.progress); this.progress = Math.floor(this.progress);
if (!this.textSupplied) { if (!this.textSupplied) {
this.text = String(this.progress); this.text = Translate.instant('core.percentagenumber', { $a: this.progress });
} }
this.width = DomSanitizer.bypassSecurityTrustStyle(this.progress + '%'); this.width = DomSanitizer.bypassSecurityTrustStyle(this.progress + '%');
@ -94,8 +94,7 @@ export class CoreProgressBarComponent implements OnInit, OnChanges {
} }
if (changes.text || changes.progress || changes.a11yText) { if (changes.text || changes.progress || changes.a11yText) {
this.progressBarValueText = (this.a11yText ? Translate.instant(this.a11yText) + ' ' : '') + this.progressBarValueText = (this.a11yText ? Translate.instant(this.a11yText) + ' ' : '') + this.text;
Translate.instant('core.percentagenumber', { $a: this.text });
} }
} }

View File

@ -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');
}
}

View File

@ -30,6 +30,7 @@ import { CoreDownloadFileDirective } from './download-file';
import { CoreCollapsibleHeaderDirective } from './collapsible-header'; import { CoreCollapsibleHeaderDirective } from './collapsible-header';
import { CoreSwipeNavigationDirective } from './swipe-navigation'; import { CoreSwipeNavigationDirective } from './swipe-navigation';
import { CoreCollapsibleItemDirective } from './collapsible-item'; import { CoreCollapsibleItemDirective } from './collapsible-item';
import { CoreCollapsibleFooterDirective } from './collapsible-footer';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -49,6 +50,7 @@ import { CoreCollapsibleItemDirective } from './collapsible-item';
CoreCollapsibleHeaderDirective, CoreCollapsibleHeaderDirective,
CoreSwipeNavigationDirective, CoreSwipeNavigationDirective,
CoreCollapsibleItemDirective, CoreCollapsibleItemDirective,
CoreCollapsibleFooterDirective,
], ],
exports: [ exports: [
CoreAutoFocusDirective, CoreAutoFocusDirective,
@ -67,6 +69,7 @@ import { CoreCollapsibleItemDirective } from './collapsible-item';
CoreCollapsibleHeaderDirective, CoreCollapsibleHeaderDirective,
CoreSwipeNavigationDirective, CoreSwipeNavigationDirective,
CoreCollapsibleItemDirective, CoreCollapsibleItemDirective,
CoreCollapsibleFooterDirective,
], ],
}) })
export class CoreDirectivesModule {} export class CoreDirectivesModule {}

View File

@ -1,7 +1,7 @@
<core-loading [hideUntil]="loaded" [fullscreen]="false"> <core-loading [hideUntil]="loaded" [fullscreen]="false">
<ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding ion-wrap" *ngIf="previousModule || nextModule"> <ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding ion-wrap" *ngIf="previousModule || nextModule">
<ion-col size="auto" class="ion-no-padding core-course-module-navigation-arrow"> <ion-col size="auto" class="ion-no-padding core-course-module-navigation-arrow">
<ion-button fill="clear" class="core-course-previous-module" *ngIf="previousModule" (click)="goToActivity(false)" <ion-button fill="clear" class="core-course-previous-module" [disabled]="!previousModule" (click)="goToActivity(false)"
[attr.aria-label]="'core.course.gotopreviousactivity' | translate"> [attr.aria-label]="'core.course.gotopreviousactivity' | translate">
<ion-icon name="fas-arrow-left" slot="icon-only" aria-hidden="true"></ion-icon> <ion-icon name="fas-arrow-left" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
@ -14,7 +14,7 @@
</core-course-module-completion> </core-course-module-completion>
</ion-col> </ion-col>
<ion-col size="auto" class="ion-no-padding core-course-module-navigation-arrow"> <ion-col size="auto" class="ion-no-padding core-course-module-navigation-arrow">
<ion-button fill="clear" class="core-course-next-module" *ngIf="nextModule" (click)="goToActivity(true)" <ion-button fill="clear" class="core-course-next-module" [disabled]="!nextModule" (click)="goToActivity(true)"
[attr.aria-label]="'core.course.gotonextactivity' | translate"> [attr.aria-label]="'core.course.gotonextactivity' | translate">
<ion-icon name="fas-arrow-right" slot="icon-only" aria-hidden="true"></ion-icon> <ion-icon name="fas-arrow-right" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>

View File

@ -1,20 +1,16 @@
@import "~theme/globals"; @import "~theme/globals";
:host { :host {
--height: var(--core-course-module-navigation-height, var(--core-course-module-navigation-max-height)); --height: var(--core-navigation-max-height);
--background: var(--core-course-module-navigation-background); --background: var(--core-navigation-background);
--button-vertical-margin: 2px; --button-vertical-margin: 2px;
height: var(--height); height: var(--height);
width: 100%; width: 100%;
background-color: var(--background); background-color: var(--background);
display: block; display: block;
bottom: 0;
z-index: 3;
border-top: 1px solid var(--stroke); border-top: 1px solid var(--stroke);
@include core-transition(all, 200ms);
core-loading { core-loading {
text-align: center; text-align: center;
--loading-inline-min-height: var(--height); --loading-inline-min-height: var(--height);
@ -25,15 +21,6 @@
margin-top: var(--button-vertical-margin); margin-top: var(--button-vertical-margin);
margin-bottom: var(--button-vertical-margin); margin-bottom: var(--button-vertical-margin);
} }
.core-course-module-navigation-arrow {
min-width: 48px;
}
}
:host-context(.core-iframe-fullscreen) {
opacity: 0 !important;
height: 0 !important;
} }
:host-context(core-course-format.core-course-format-singleactivity) { :host-context(core-course-format.core-course-format-singleactivity) {

View File

@ -17,13 +17,11 @@ import { CoreCourse, CoreCourseProvider, CoreCourseWSSection } from '@features/c
import { CoreCourseModuleCompletionData, CoreCourseModuleData } from '@features/course/services/course-helper'; import { CoreCourseModuleCompletionData, CoreCourseModuleData } from '@features/course/services/course-helper';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { IonContent } from '@ionic/angular'; import { IonContent } from '@ionic/angular';
import { ScrollDetail } from '@ionic/core';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreMath } from '@singletons/math';
/** /**
* Component to show a button to go to the next resource/activity. * Component to show a button to go to the next resource/activity.
@ -51,21 +49,11 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
loaded = false; loaded = false;
showCompletion = false; // Whether to show completion. 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; protected completionObserver: CoreEventObserver;
constructor(el: ElementRef, protected ionContent: IonContent) { constructor(el: ElementRef, protected ionContent: IonContent) {
const siteId = CoreSites.getCurrentSiteId(); const siteId = CoreSites.getCurrentSiteId();
this.element = el.nativeElement;
this.element.setAttribute('slot', 'fixed');
this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_MODULE_VIEWED, async (data) => { this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_MODULE_VIEWED, async (data) => {
if (data && data.courseId == this.courseId) { if (data && data.courseId == this.courseId) {
// Check if now there's a next module. // Check if now there's a next module.
@ -88,76 +76,14 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
await this.setNextAndPreviousModules(CoreSitesReadingStrategy.PREFER_CACHE); await this.setNextAndPreviousModules(CoreSitesReadingStrategy.PREFER_CACHE);
} finally { } finally {
this.loaded = true; 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-course-module-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 * @inheritdoc
*/ */
async ngOnDestroy(): Promise<void> { async ngOnDestroy(): Promise<void> {
this.completionObserver.off(); 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-course-module-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);
}
}
} }

View File

@ -44,6 +44,6 @@
<core-course-unsupported-module *ngIf="unsupported" [module]="module"></core-course-unsupported-module> <core-course-unsupported-module *ngIf="unsupported" [module]="module"></core-course-unsupported-module>
</core-loading> </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> (completionChanged)="onCompletionChange()" [showManualCompletion]="showManualCompletion"></core-course-module-navigation>
</ion-content> </ion-content>

View File

@ -14,5 +14,5 @@
(onLoadingContent)="contentLoading()"> (onLoadingContent)="contentLoading()">
</core-site-plugins-plugin-content> </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> </core-course-module-navigation>

View File

@ -1124,8 +1124,8 @@ ion-fab[core-fab] {
} }
} }
ion-content.has-core-course-module-navigation ion-fab { ion-content.has-collapsible-footer ion-fab {
bottom: calc(var(--core-course-module-navigation-height, 0px) + 10px); bottom: calc(var(--core-navigation-height, 0px) + 10px);
@include core-transition(all, 200ms); @include core-transition(all, 200ms);
} }
@ -1442,6 +1442,30 @@ ion-grid.core-no-grid > ion-row {
@include collapsible-item(); @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 { ion-header.no-title {
--core-header-toolbar-border-width: 0; --core-header-toolbar-border-width: 0;
--core-header-toolbar-background: transparent; --core-header-toolbar-background: transparent;
@ -1540,9 +1564,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%;
}
} }
} }

View File

@ -129,6 +129,10 @@
--core-send-message-input-background: var(--gray-900); --core-send-message-input-background: var(--gray-900);
--core-send-message-input-color: var(--white); --core-send-message-input-color: var(--white);
--core-navigation-background: var(--contrast-background);
--core-collapsible-footer-background: var(--contrast-background);
--addon-messages-message-bg: var(--gray-800); --addon-messages-message-bg: var(--gray-800);
--addon-messages-message-activated-bg: var(--gray-700); --addon-messages-message-activated-bg: var(--gray-700);
--addon-messages-message-note-text: var(--subdued-text-color); --addon-messages-message-note-text: var(--subdued-text-color);

View File

@ -310,8 +310,10 @@
--core-courseimage-on-course-size: 72px; --core-courseimage-on-course-size: 72px;
--core-courseimage-radius: var(--medium-radius); --core-courseimage-radius: var(--medium-radius);
--core-course-module-navigation-max-height: 48px; --core-navigation-height: 48px;
--core-course-module-navigation-background: var(--contrast-background); --core-navigation-background: var(--contrast-background);
--core-collapsible-footer-background: var(--contrast-background);
--core-user-menu-site-logo-max-height: 32px; --core-user-menu-site-logo-max-height: 32px;