Merge pull request #3215 from crazyserver/MOBILE-3833

Mobile 3833
main
Dani Palou 2022-03-31 12:33:10 +02:00 committed by GitHub
commit 0143c23395
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 479 additions and 495 deletions

View File

@ -1558,10 +1558,6 @@
"core.course.errordownloadingsection": "local_moodlemobileapp",
"core.course.errorgetmodule": "local_moodlemobileapp",
"core.course.failed": "completion",
"core.course.gotonextactivity": "local_moodlemobileapp",
"core.course.gotonextactivitynotfound": "local_moodlemobileapp",
"core.course.gotopreviousactivity": "local_moodlemobileapp",
"core.course.gotopreviousactivitynotfound": "local_moodlemobileapp",
"core.course.hiddenfromstudents": "moodle",
"core.course.hiddenoncoursepage": "moodle",
"core.course.highlighted": "moodle",
@ -1570,8 +1566,12 @@
"core.course.lastaccessedactivity": "local_moodlemobileapp",
"core.course.manualcompletionnotsynced": "local_moodlemobileapp",
"core.course.modulenotfound": "local_moodlemobileapp",
"core.course.nextactivity": "local_moodlemobileapp",
"core.course.nextactivitynotfound": "local_moodlemobileapp",
"core.course.nocontentavailable": "local_moodlemobileapp",
"core.course.overriddennotice": "grades",
"core.course.previousactivity": "local_moodlemobileapp",
"core.course.previousactivitynotfound": "local_moodlemobileapp",
"core.course.refreshcourse": "local_moodlemobileapp",
"core.course.section": "moodle",
"core.course.startdate": "moodle",

View File

@ -17,7 +17,7 @@
.addon-calendar-months {
background-color: var(--contrast-background);
padding: 0;
font-size: 14px;
font-size: var(--text-size);
}
.addon-calendar-day {

View File

@ -1,3 +1,3 @@
ion-item > p[slot="end"] {
font-size: 14px;
font-size: var(--text-size);
}

View File

@ -158,7 +158,7 @@
</ion-button>
<ion-button expand="block" (click)="continue()" class="ion-text-wrap ion-margin">
<ng-container *ngIf="!siteAfterSubmit">{{ 'core.continue' | translate }}</ng-container>
<ng-container *ngIf="siteAfterSubmit">{{ 'core.course.gotonextactivity' | translate }}</ng-container>
<ng-container *ngIf="siteAfterSubmit">{{ 'core.course.nextactivity' | translate }}</ng-container>
</ion-button>
</div>
</div>

View File

@ -32,6 +32,6 @@
</core-loading>
<core-course-module-navigation collapsible-footer appearOnBottom [hidden]="showLoading" [courseId]="courseId" [currentModuleId]="module.id"
(completionChanged)="onCompletionChange()">
<core-course-module-navigation collapsible-footer appearOnBottom *ngIf="!subfolder" [hidden]="showLoading" [courseId]="courseId"
[currentModuleId]="module.id" (completionChanged)="onCompletionChange()">
</core-course-module-navigation>

View File

@ -81,6 +81,12 @@
<div collapsible-footer appearOnBottom *ngIf="!showLoading" slot="fixed">
<div class="list-item-limited-width" *ngIf="mode == 'external'">
<ion-button *ngIf="isIOS && (!shouldOpenInBrowser || !isOnline)" expand="block" fill="outline"
(click)="open(openFileAction.OPEN_WITH)" class="ion-margin ion-text-wrap">
<ion-icon name="far-share-square" slot="start" aria-hidden="true"></ion-icon>
{{ 'core.openwith' | translate }}
</ion-button>
<ion-button expand="block" (click)="open(openFileAction.OPEN)" class="ion-margin ion-text-wrap">
<ng-container *ngIf="isStreamedFile">
<ion-icon name="fas-play" slot="start" aria-hidden="true"></ion-icon>
@ -91,12 +97,6 @@
{{ 'addon.mod_resource.openthefile' | translate }}
</ng-container>
</ion-button>
<ion-button *ngIf="isIOS && (!shouldOpenInBrowser || !isOnline)" expand="block" (click)="open(openFileAction.OPEN_WITH)"
class="ion-margin ion-text-wrap">
<ion-icon name="far-share-square" slot="start" aria-hidden="true"></ion-icon>
{{ 'core.openwith' | translate }}
</ion-button>
</div>
<core-course-module-navigation [courseId]="courseId" [currentModuleId]="module.id">
</core-course-module-navigation>

View File

@ -2,7 +2,7 @@
:host {
.addon-mod_scorm-attempt-summary ion-item > p {
font-size: 14px;
font-size: var(--text-size);
}
.addon-mod_scorm-toc {

View File

@ -3,7 +3,7 @@
--even-background: var(--gray-200);
.option-name {
font-size: 14px;
font-size: var(--text-size);
}
.addon-mod_survey-question {

View File

@ -30,8 +30,8 @@
<core-loading [hideUntil]="!showLoading">
<!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"
[courseId]="courseId" (completionChanged)="onCompletionChange()">
<core-course-module-info *ngIf="isMainPage" [module]="module" [description]="description" [component]="component"
[componentId]="componentId" [courseId]="courseId" (completionChanged)="onCompletionChange()">
</core-course-module-info>
<div *ngIf="pageIsOffline || hasOffline || pageWarning">
@ -54,7 +54,7 @@
</ion-item>
</ion-card>
</div>
<div class="ion-padding addon-mod_wiki-page-content">
<div class="ion-padding-horizontal addon-mod_wiki-page-content">
<h2 *ngIf="pageTitle">{{pageTitle}}</h2>
<article [ngClass]="{'addon-mod_wiki-noedit': !canEdit}">
<core-format-text *ngIf="pageContent" [component]="component" [componentId]="componentId" [text]="pageContent"
@ -71,7 +71,8 @@
</div>
</core-loading>
<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModuleId]="module.id">
<core-course-module-navigation collapsible-footer *ngIf="isMainPage" [hidden]="showLoading" [courseId]="courseId"
[currentModuleId]="module.id">
</core-course-module-navigation>
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canEdit">

View File

@ -11,7 +11,6 @@ $addon-mod-wiki-toc-level-padding: 12px !default;
.addon-mod_wiki-page-content {
background-color: var(--ion-item-background);
border-top: 1px solid var(--stroke);
padding-bottom: 10px;
}

View File

@ -5,7 +5,7 @@ ion-item {
margin-top: 8px;
margin-bottom: 8px;
p.item-heading {
font-size: 14px;
font-size: var(--text-size);
-webkit-line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;

View File

@ -17,7 +17,7 @@
.core-notification-body {
core-format-text {
font-size: 14px;
font-size: var(--text-size);
}
h2 {

View File

@ -22,11 +22,11 @@ import {
OnDestroy,
AfterViewInit,
ViewChild,
ElementRef,
SimpleChange,
ElementRef,
} from '@angular/core';
import { IonSlides } from '@ionic/angular';
import { BackButtonEvent, ScrollDetail } from '@ionic/core';
import { BackButtonEvent } from '@ionic/core';
import { Subscription } from 'rxjs';
import { Platform, Translate } from '@singletons';
@ -34,6 +34,11 @@ import { CoreSettingsHelper } from '@features/settings/services/settings-helper'
import { CoreAriaRoleTab, CoreAriaRoleTabFindable } from './aria-role-tab';
import { CoreEventObserver } from '@singletons/events';
import { CoreDom } from '@singletons/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreError } from './errors/error';
import { CorePromisedValue } from './promised-value';
import { AsyncComponent } from './async-component';
import { CoreComponentsRegistry } from '@singletons/components-registry';
/**
* Class to abstract some common code for tabs.
@ -41,13 +46,10 @@ import { CoreDom } from '@singletons/dom';
@Component({
template: '',
})
export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, AfterViewInit, OnChanges, OnDestroy {
export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, AfterViewInit, OnChanges, OnDestroy, AsyncComponent {
// Minimum tab's width.
protected static readonly MIN_TAB_WIDTH = 107;
// @todo [4.0]
// Max height that allows tab hiding. WARNING: Hide tabs on scroll disabled. If confirmed, remove the associated code.
protected static readonly MAX_HEIGHT_TO_HIDE_TABS = 0;
@Input() selectedIndex = 0; // Index of the tab to select.
@Input() hideUntil = false; // Determine when should the contents be shown.
@ -57,6 +59,7 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
tabs: T[] = []; // List of tabs.
hideTabs = false;
selected?: string; // Selected tab id.
showPrevButton = false;
showNextButton = false;
@ -68,15 +71,12 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
initialSlide: 0,
slidesPerView: 3,
centerInsufficientSlides: true,
threshold: 10,
};
protected slidesElement?: HTMLIonSlidesElement;
protected initialized = false;
protected afterViewInitTriggered = false;
protected tabBarHeight = 0;
protected tabsElement?: HTMLElement; // The tabs parent element. It's the element that will be "scrolled" to hide tabs.
protected tabBarElement?: HTMLIonTabBarElement; // The top tab bar element.
protected tabsShown = true;
protected resizeListener?: CoreEventObserver;
protected isDestroyed = false;
protected isCurrentView = true;
@ -86,101 +86,44 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
protected firstSelectedTab?: string; // ID of the first selected tab to control history.
protected backButtonFunction: (event: BackButtonEvent) => void;
protected languageChangedSubscription?: Subscription;
// 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<string | number, HTMLElement> = {}; // Scroll elements for each loaded tab.
protected lastScroll = 0;
protected previousLastScroll = 0;
protected subscriptions: Subscription[] = [];
protected onReadyPromise = new CorePromisedValue<void>();
tabAction: CoreTabsRoleTab<T>;
constructor(
protected element: ElementRef,
) {
constructor(element: ElementRef) {
this.backButtonFunction = this.backButtonClicked.bind(this);
this.tabAction = new CoreTabsRoleTab(this);
CoreComponentsRegistry.register(element.nativeElement, this);
}
/**
* Component being initialized.
* @inheritdoc
*/
ngOnInit(): void {
async ngOnInit(): Promise<void> {
this.direction = Platform.isRTL ? 'rtl' : 'ltr';
// Change the side when the language changes.
this.languageChangedSubscription = Translate.onLangChange.subscribe(() => {
this.subscriptions.push(Translate.onLangChange.subscribe(() => {
setTimeout(() => {
this.direction = Platform.isRTL ? 'rtl' : 'ltr';
});
});
}));
}
/**
* View has been initialized.
* @inheritdoc
*/
async ngAfterViewInit(): Promise<void> {
ngAfterViewInit(): void {
if (this.isDestroyed) {
return;
}
this.afterViewInitTriggered = true;
this.tabBarElement = this.element.nativeElement.querySelector('ion-tab-bar');
if (!this.initialized && this.hideUntil) {
// Tabs should be shown, initialize them.
await this.initializeTabs();
}
this.resizeListener = CoreDom.onWindowResize(() => {
this.windowResized();
});
}
/**
* Calculate the tab bar height.
*/
protected calculateTabBarHeight(): void {
if (!this.tabBarElement) {
return;
}
this.tabBarHeight = this.tabBarElement.offsetHeight;
this.applyScroll(this.tabsShown, this.lastScroll);
}
/**
* Apply scroll to hiding tabs.
*
* @param showTabs Show or completely hide tabs.
* @param scroll Scroll position.
*/
protected applyScroll(showTabs: boolean, scroll?: number): void {
if (!this.tabBarElement || !this.tabBarHeight) {
return;
}
if (showTabs) {
// Smooth translation.
this.tabBarElement.classList.remove('tabs-hidden');
if (scroll === 0) {
this.tabBarElement.style.height = '';
this.previousLastScroll = this.lastScroll;
this.lastScroll = 0;
} else if (scroll !== undefined) {
this.tabBarElement.style.height = (this.tabBarHeight - scroll) + 'px';
}
} else {
this.tabBarElement.classList.add('tabs-hidden');
this.tabBarElement.style.height = '';
}
this.tabsShown = showTabs;
this.init();
}
/**
@ -188,14 +131,7 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ngOnChanges(changes: Record<string, SimpleChange>): void {
// Wait for ngAfterViewInit so it works in the case that each tab has its own component.
if (!this.initialized && this.hideUntil && this.afterViewInitTriggered) {
// Tabs should be shown, initialize them.
// Use a setTimeout so child components update their inputs before initializing the tabs.
setTimeout(() => {
this.initializeTabs();
});
}
this.init();
}
/**
@ -260,14 +196,15 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
return;
}
if (window.innerHeight >= CoreTabsBaseComponent.MAX_HEIGHT_TO_HIDE_TABS) {
// Ensure tabbar is shown.
this.applyScroll(true, 0);
this.calculateTabBarHeight();
} else if (!this.tabsShown) {
// Don't recalculate.
this.numTabsShown = this.tabs.reduce((prev: number, current) => current.enabled ? prev + 1 : prev, 0);
if (this.numTabsShown <= 1) {
this.hideTabs = true;
// Only one, nothing to do here.
return;
}
this.hideTabs = false;
await this.calculateMaxSlides();
@ -296,31 +233,81 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
}
/**
* Initialize the tabs, determining the first tab to be shown.
* Init the component.
*/
protected async initializeTabs(): Promise<void> {
// Initialize slider.
this.slidesSwiper = await this.slides?.getSwiper();
this.slidesSwiper.once('progress', () => {
this.slidesSwiperLoaded = true;
this.calculateSlides();
});
const selectedTab = this.calculateInitialTab();
if (!selectedTab) {
protected async init(): Promise<void> {
if (!this.hideUntil) {
// Hidden, do nothing.
return;
}
this.firstSelectedTab = selectedTab.id!;
this.selectTab(this.firstSelectedTab);
try {
await this.initializeSlider();
await this.initializeTabs();
} catch {
// Something went wrong, ignore.
}
}
// Setup tab scrolling.
this.calculateTabBarHeight();
/**
* Initialize the slider elements.
*/
protected async initializeSlider(): Promise<void> {
if (this.initialized) {
return;
}
if (this.slidesElement) {
// Already initializated, await for ready.
await this.slidesElement.componentOnReady();
return;
}
if (!this.slides) {
await CoreUtils.nextTick();
}
const slidesSwiper = await this.slides?.getSwiper();
if (!slidesSwiper || !this.slides) {
throw new CoreError('Swiper not found, will try on next change.');
}
this.slidesElement = <HTMLIonSlidesElement>slidesSwiper.el;
await this.slidesElement.componentOnReady();
this.initialized = true;
// Subscribe to changes.
this.subscriptions.push(this.slides.ionSlideDidChange.subscribe(() => {
this.slideChanged();
}));
}
/**
* Initialize the tabs, determining the first tab to be shown.
*/
protected async initializeTabs(): Promise<void> {
if (!this.initialized || !this.slidesElement) {
return;
}
const selectedTab = this.calculateInitialTab();
if (!selectedTab) {
// No enabled tabs, return.
throw new CoreError('No enabled tabs.');
}
this.firstSelectedTab = selectedTab.id;
if (this.firstSelectedTab !== undefined) {
this.selectTab(this.firstSelectedTab);
}
// Check which arrows should be shown.
this.calculateSlides();
this.resizeListener = CoreDom.onWindowResize(() => {
this.calculateSlides();
});
}
/**
@ -343,7 +330,7 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
* Method executed when the slides are changed.
*/
async slideChanged(): Promise<void> {
if (!this.slidesSwiperLoaded) {
if (!this.slidesElement) {
return;
}
@ -368,19 +355,15 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
* Updates the number of slides to show.
*/
protected async updateSlides(): Promise<void> {
this.numTabsShown = this.tabs.reduce((prev: number, current) => current.enabled ? prev + 1 : prev, 0);
if (!this.slides) {
return;
}
this.slidesOpts = { ...this.slidesOpts, slidesPerView: Math.min(this.maxSlides, this.numTabsShown) };
this.slideChanged();
await this.slideChanged();
this.calculateTabBarHeight();
// @todo: This call to update() can trigger JS errors in the console if tabs are re-loaded and there's only 1 tab.
// For some reason, swiper.slides is undefined inside the Slides class, and the swiper is marked as destroyed.
// Changing *ngIf="hideUntil" to [hidden] doesn't solve the issue, and it causes another error to be raised.
// This can be tested in lesson as a student, play a lesson and go back to the entry page.
await this.slides!.update();
await this.slides.update();
if (!this.hasSliddenToInitial && this.selectedIndex && this.selectedIndex >= this.slidesOpts.slidesPerView) {
this.hasSliddenToInitial = true;
@ -388,7 +371,7 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
setTimeout(() => {
if (this.shouldSlideToInitial) {
this.slides!.slideTo(this.selectedIndex, 0);
this.slides?.slideTo(this.selectedIndex, 0);
this.shouldSlideToInitial = false;
}
}, 400);
@ -407,17 +390,23 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
* Calculate the number of slides that can fit on the screen.
*/
protected async calculateMaxSlides(): Promise<void> {
if (!this.slidesSwiperLoaded) {
if (!this.slidesElement || !this.slides) {
return;
}
this.maxSlides = 3;
let width = this.slidesSwiper.width;
if (!width) {
this.slidesSwiper.updateSize();
width = this.slidesSwiper.width;
await CoreUtils.nextTick();
let width: number = this.slidesElement.getBoundingClientRect().width;
if (!width) {
const slidesSwiper = await this.slides.getSwiper();
await slidesSwiper.updateSize();
await CoreUtils.nextTick();
width = slidesSwiper.width;
if (!width) {
return;
}
}
@ -484,61 +473,6 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
}
}
/**
* Show or hide the tabs. This is used when the user is scrolling inside a tab.
*
* @param scrollTop Scroll top.
* @param scrollElement Content scroll element to check measures.
*/
showHideTabs(scrollTop: number, scrollElement: HTMLElement): void {
if (!this.tabBarElement || !this.tabsElement || !scrollElement) {
return;
}
// Always show on very tall screens.
if (window.innerHeight >= CoreTabsBaseComponent.MAX_HEIGHT_TO_HIDE_TABS) {
return;
}
if (!this.tabBarHeight && this.tabBarElement.offsetHeight != this.tabBarHeight) {
// Wrong tab height, recalculate it.
this.calculateTabBarHeight();
}
if (!this.tabBarHeight) {
// We don't have the tab bar height, this means the tab bar isn't shown.
return;
}
if (scrollTop <= 0) {
// Ensure tabbar is shown.
this.applyScroll(true, 0);
return;
}
if (scrollTop == this.lastScroll || scrollTop == this.previousLastScroll) {
// Ensure scroll has been modified to avoid flicks.
return;
}
if (this.tabsShown && scrollTop > this.tabBarHeight) {
// Hide tabs.
this.applyScroll(false);
} else if (!this.tabsShown && scrollTop <= this.tabBarHeight) {
this.applyScroll(true);
}
if (this.tabsShown && scrollElement.scrollHeight > scrollElement.clientHeight + (this.tabBarHeight - scrollTop)) {
// Smooth translation.
this.applyScroll(true, scrollTop);
}
// Use lastScroll after moving the tabs to avoid flickering.
this.previousLastScroll = this.lastScroll;
this.lastScroll = scrollTop;
}
/**
* Select a tab by ID.
*
@ -579,12 +513,12 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
return;
}
if (this.selected) {
if (this.selected && this.slides) {
// Check if we need to slide to the tab because it's not visible.
const firstVisibleTab = await this.slides!.getActiveIndex();
const firstVisibleTab = await this.slides.getActiveIndex();
const lastVisibleTab = firstVisibleTab + this.slidesOpts.slidesPerView - 1;
if (index < firstVisibleTab || index > lastVisibleTab) {
await this.slides!.slideTo(index, 0, true);
await this.slides.slideTo(index, 0, true);
}
}
@ -593,11 +527,12 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
return;
}
const ok = await this.loadTab(tabToSelect);
const suceeded = await this.loadTab(tabToSelect);
if (ok !== false) {
if (suceeded !== false) {
this.tabSelected(tabToSelect, index);
}
this.onReadyPromise.resolve();
}
/**
@ -627,55 +562,20 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
}
/**
* Listen scroll events in an element's inner ion-content (if any).
*
* @param element Element to search ion-content in.
* @param id ID of the tab/page.
* @return Promise resolved when done.
* @inheritdoc
*/
async listenContentScroll(element: HTMLElement, id: number | string): Promise<void> {
if (this.scrollElements[id]) {
return; // Already set.
}
let content = element.querySelector('ion-content');
if (!content) {
return;
}
// Search the inner ion-content if there's more than one.
let childContent = content.querySelector('ion-content') || null;
while (childContent != null) {
content = childContent;
childContent = content.querySelector('ion-content') || null;
}
const scroll = await content.getScrollElement();
content.scrollEvents = true;
this.scrollElements[id] = scroll;
content.addEventListener('ionScroll', (e: CustomEvent<ScrollDetail>): void => {
this.showHideTabs(e.detail.scrollTop, scroll);
});
async ready(): Promise<void> {
return await this.onReadyPromise;
}
/**
* Adapt tabs to a window resize.
*/
protected windowResized(): void {
setTimeout(() => {
this.calculateSlides();
}, 200);
}
/**
* Component destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
this.isDestroyed = true;
this.resizeListener?.off();
this.languageChangedSubscription?.unsubscribe();
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
}
}
@ -697,8 +597,8 @@ class CoreTabsRoleTab<T extends CoreTabBase> extends CoreAriaRoleTab<CoreTabsBas
*/
getSelectableTabs(): CoreAriaRoleTabFindable[] {
return this.componentInstance.tabs.filter((tab) => tab.enabled).map((tab) => ({
id: tab.id!,
findIndex: tab.id!,
id: tab.id || '',
findIndex: tab.id || '',
}));
}

View File

@ -1,14 +1,5 @@
:host {
> div {
max-width: 100%;
max-height: 100%;
}
iframe {
border: 0;
display: block;
max-width: 100%;
background-color: var(--ion-background-color);
}
flex-grow: 1;
}
:host-context(.core-iframe-fullscreen) {
@ -21,3 +12,8 @@
height: 100%;
z-index: 9999;
}
:host-context(.limited-width > :not([slot])) {
display: flex;
flex-direction: column;
}

View File

@ -93,4 +93,5 @@
:host-context(.limited-width > ):not([slot]) {
--contents-display: flex;
flex-direction: column;
min-height: 100%;
}

View File

@ -11,6 +11,7 @@
margin-top: 0;
margin-bottom: 0;
z-index: 3;
bottom: 0;
}
}

View File

@ -1,38 +1,36 @@
<ion-tabs class="hide-header">
<ion-tab-bar slot="top" class="core-tabs-bar" [hidden]="!tabs || numTabsShown <= 1" #tabBar>
<ion-tab-bar slot="top" class="core-tabs-bar" [hidden]="hideUntil && (!tabs || numTabsShown <= 1)">
<ion-spinner *ngIf="!hideUntil" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
<ion-row *ngIf="hideUntil">
<ion-col class="col-with-arrow ion-no-padding" (click)="slidePrev()" size="1" [class.clickable]="showPrevButton">
<ion-icon *ngIf="showPrevButton" name="fas-chevron-left" [attr.aria-label]="'core.previous' | translate"></ion-icon>
</ion-col>
<ion-col class="ion-no-padding" size="10">
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slidesOpts" [dir]="direction" role="tablist"
[attr.aria-label]="description">
<ng-container *ngFor="let tab of tabs">
<ion-slide role="presentation" [id]="tab.id! + '-tab'" class="tab-slide" tabindex="-1"
[class.selected]="selected == tab.id">
<ion-tab-button (ionTabButtonClick)="selectTab(tab.id, $event)" (keydown)="tabAction.keyDown($event)"
(keyup)="tabAction.keyUp(tab.id, $event)" [tab]="tab.page" [layout]="layout" class="{{tab.class}}"
role="tab" [attr.aria-controls]="tab.id" [attr.aria-selected]="selected == tab.id"
[tabindex]="selected == tab.id ? 0 : -1">
<ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
<ion-label>
{{ tab.title | translate}}
<ion-badge *ngIf="tab.badge">
<span [attr.aria-hidden]="!!tab.badgeA11yText">{{ tab.badge }}</span>
<span *ngIf="tab.badgeA11yText" class="sr-only">
{{ tab.badgeA11yText | translate: {$a : tab.badge } }}
</span>
</ion-badge>
</ion-label>
</ion-tab-button>
</ion-slide>
</ng-container>
</ion-slides>
</ion-col>
<ion-col class="col-with-arrow ion-no-padding" (click)="slideNext()" size="1" [class.clickable]="showNextButton">
<ion-icon *ngIf="showNextButton" name="fas-chevron-right" [attr.aria-label]="'core.next' | translate"></ion-icon>
</ion-col>
</ion-row>
<ng-container *ngIf="hideUntil">
<ion-button fill="clear" class="arrow-button" (click)="slidePrev()" [disabled]="!showPrevButton"
[attr.aria-label]="'core.previous' | translate">
<ion-icon *ngIf="showPrevButton" name="fas-chevron-left" aria-hidden="true" slot="icon-only"></ion-icon>
</ion-button>
<ion-slides [options]="slidesOpts" [dir]="direction" role="tablist" [attr.aria-label]="description">
<ng-container *ngFor="let tab of tabs">
<ion-slide role="presentation" [id]="tab.id! + '-tab'" tabindex="-1" [class.selected]="selected == tab.id">
<ion-tab-button (ionTabButtonClick)="selectTab(tab.id, $event)" (keydown)="tabAction.keyDown($event)"
(keyup)="tabAction.keyUp(tab.id, $event)" [tab]="tab.page" [layout]="layout" class="{{tab.class}}" role="tab"
[attr.aria-controls]="tab.id" [attr.aria-selected]="selected == tab.id"
[tabindex]="selected == tab.id ? 0 : -1">
<ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
<ion-label>
{{ tab.title | translate}}
<ion-badge *ngIf="tab.badge">
<span [attr.aria-hidden]="!!tab.badgeA11yText">{{ tab.badge }}</span>
<span *ngIf="tab.badgeA11yText" class="sr-only">
{{ tab.badgeA11yText | translate: {$a : tab.badge } }}
</span>
</ion-badge>
</ion-label>
</ion-tab-button>
</ion-slide>
</ng-container>
</ion-slides>
<ion-button fill="clear" class="arrow-button" (click)="slideNext()" [disabled]="!showNextButton"
[attr.aria-label]="'core.next' | translate">
<ion-icon *ngIf="showNextButton" name="fas-chevron-right" aria-hidden="true" slot="icon-only"></ion-icon>
</ion-button>
</ng-container>
</ion-tab-bar>
</ion-tabs>

View File

@ -20,7 +20,6 @@ import {
OnDestroy,
AfterViewInit,
ViewChild,
ElementRef,
SimpleChange,
} from '@angular/core';
import { IonRouterOutlet, IonTabs, ViewDidEnter, ViewDidLeave } from '@ionic/angular';
@ -69,12 +68,6 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle
protected lastActiveComponent?: Partial<ViewDidLeave>;
protected existsInNavigationStack = false;
constructor(element: ElementRef) {
super(element);
CoreComponentsRegistry.register(element.nativeElement, this);
}
/**
* Init tab info.
*
@ -97,7 +90,6 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle
return;
}
this.tabsElement = this.element.nativeElement.querySelector('ion-tabs');
this.stackEventsSubscription = this.ionTabs.outlet.stackEvents.subscribe(async (stackEvent: StackEvent) => {
if (!this.isCurrentView) {
return;
@ -118,14 +110,6 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle
}
this.showHideNavBarButtons(stackEvent.enteringView.element.tagName);
await this.listenContentScroll(stackEvent.enteringView.element, stackEvent.enteringView.id);
const scrollElement = this.scrollElements[stackEvent.enteringView.id];
if (scrollElement) {
// Show or hide tabs based on the new page scroll.
this.showHideTabs(scrollElement.scrollTop, scrollElement);
}
});
this.outletActivatedSubscription = this.ionTabs.outlet.activateEvents.subscribe(() => {
this.lastActiveComponent = this.ionTabs.outlet.component;
@ -232,7 +216,7 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle
}
/**
* Component destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
super.ngOnDestroy();

View File

@ -1,38 +1,35 @@
<ion-tab-bar slot="top" class="core-tabs-bar" [hidden]="!tabs || numTabsShown <= 1" #tabBar>
<ion-tab-bar slot="top" class="core-tabs-bar" [hidden]="hideUntil && (!tabs || numTabsShown <= 1)">
<ion-spinner *ngIf="!hideUntil" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
<ion-row *ngIf="hideUntil">
<ion-col class="col-with-arrow ion-no-padding" (click)="slidePrev()" size="1" [class.clickable]="showPrevButton">
<ion-icon *ngIf="showPrevButton" name="fas-chevron-left" [attr.aria-label]="'core.previous' | translate"></ion-icon>
</ion-col>
<ion-col class="ion-no-padding" size="10">
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slidesOpts" [dir]="direction" role="tablist"
[attr.aria-label]="description">
<ng-container *ngFor="let tab of tabs">
<ion-slide *ngIf="tab.enabled" role="presentation" [hidden]="!hideUntil" class="tab-slide" [id]="tab.id! + '-tab'"
[class.selected]="selected == tab.id">
<ion-tab-button (click)="selectTab(tab.id, $event)" (keydown)="tabAction.keyDown($event)"
(keyup)="tabAction.keyUp(tab.id, $event)" class="{{tab.class}}" [layout]="layout" role="tab"
[attr.aria-controls]="tab.id" [attr.aria-selected]="selected == tab.id"
[tabindex]="selected == tab.id ? 0 : -1">
<ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
<ion-label>
{{ tab.title | translate}}
<ion-badge *ngIf="tab.badge">
<span [attr.aria-hidden]="!!tab.badgeA11yText">{{ tab.badge }}</span>
<span *ngIf="tab.badgeA11yText" class="sr-only">
{{ tab.badgeA11yText | translate: {$a : tab.badge } }}
</span>
</ion-badge>
</ion-label>
</ion-tab-button>
</ion-slide>
</ng-container>
</ion-slides>
</ion-col>
<ion-col class="col-with-arrow ion-no-padding" (click)="slideNext()" size="1" [class.clickable]="showNextButton">
<ion-icon *ngIf="showNextButton" name="fas-chevron-right" [attr.aria-label]="'core.next' | translate"></ion-icon>
</ion-col>
</ion-row>
<ng-container *ngIf="hideUntil">
<ion-button fill="clear" class="arrow-button" (click)="slidePrev()" [disabled]="!showPrevButton"
[attr.aria-label]="'core.previous' | translate">
<ion-icon *ngIf="showPrevButton" name="fas-chevron-left" aria-hidden="true" slot="icon-only"></ion-icon>
</ion-button>
<ion-slides [options]="slidesOpts" [dir]="direction" role="tablist" [attr.aria-label]="description">
<ng-container *ngFor="let tab of tabs">
<ion-slide *ngIf="tab.enabled" role="presentation" [id]="tab.id! + '-tab'" [class.selected]="selected == tab.id">
<ion-tab-button (click)="selectTab(tab.id, $event)" (keydown)="tabAction.keyDown($event)"
(keyup)="tabAction.keyUp(tab.id, $event)" class="{{tab.class}}" [layout]="layout" role="tab"
[attr.aria-controls]="tab.id" [attr.aria-selected]="selected == tab.id" [tabindex]="selected == tab.id ? 0 : -1">
<ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
<ion-label>
{{ tab.title | translate}}
<ion-badge *ngIf="tab.badge">
<span [attr.aria-hidden]="!!tab.badgeA11yText">{{ tab.badge }}</span>
<span *ngIf="tab.badgeA11yText" class="sr-only">
{{ tab.badgeA11yText | translate: {$a : tab.badge } }}
</span>
</ion-badge>
</ion-label>
</ion-tab-button>
</ion-slide>
</ng-container>
</ion-slides>
<ion-button fill="clear" class="arrow-button" (click)="slideNext()" [disabled]="!showNextButton"
[attr.aria-label]="'core.next' | translate">
<ion-icon *ngIf="showNextButton" name="fas-chevron-right" aria-hidden="true" slot="icon-only"></ion-icon>
</ion-button>
</ng-container>
</ion-tab-bar>
<div class="core-tabs-content-container" #originalTabs>
<ng-content></ng-content>

View File

@ -65,7 +65,7 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase {
return this.isEnabled;
}
@Input() id?: string; // An ID to identify the tab.
@Input() id = ''; // An ID to identify the tab.
@Output() ionSelect: EventEmitter<CoreTabComponent> = new EventEmitter<CoreTabComponent>();
@ContentChild(TemplateRef) template?: TemplateRef<unknown>; // Template defined by the content.
@ -82,7 +82,7 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase {
element: ElementRef,
) {
this.element = element.nativeElement;
this.id = this.id || 'core-tab-' + CoreUtils.getUniqueId('CoreTabComponent');
this.element.setAttribute('role', 'tabpanel');
this.element.setAttribute('tabindex', '0');
this.element.setAttribute('aria-hidden', 'true');
@ -92,7 +92,6 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase {
* Component being initialized.
*/
ngOnInit(): void {
this.id = this.id || 'core-tab-' + CoreUtils.getUniqueId('CoreTabComponent');
this.element.setAttribute('aria-labelledby', this.id + '-tab');
this.element.setAttribute('id', this.id);
@ -120,9 +119,6 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase {
this.loaded = true;
this.ionSelect.emit(this);
this.showHideNavBarButtons(true);
// Setup tab scrolling.
this.tabs.listenContentScroll(this.element, this.id!);
}
/**

View File

@ -14,7 +14,7 @@
position: relative;
}
ion-tab-bar.core-tabs-bar {
ion-tab-bar {
position: relative;
background: var(--tabs-background);
@include safe-area-padding-end(null, 0px);
@ -22,57 +22,65 @@
color: var(--tabs-color);
border-bottom: 1px solid var(--stroke);
display: flex;
align-items: flex-end;
flex-direction: row;
justify-content: space-between;
flex-shrink: 0;
ion-row {
width: 100%;
ion-spinner {
flex-grow: 1;
}
.tab-slide {
border-bottom: 2px solid transparent;
min-width: 100px;
min-height: var(--height);
cursor: pointer;
overflow: hidden;
ion-tab-button {
max-width: 100%;
ion-label {
font-size: 16px;
font-weight: 400;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
word-wrap: break-word;
max-width: 100%;
line-height: 1.2em;
margin-top: 16px;
margin-bottom: 16px;
}
}
&[aria-selected=true],
&.selected {
color: var(--color-active);
border-bottom-color: var(--border-color-active);
ion-tab-button {
color: var(--color-active);
ion-label {
font-weight: var(--font-weight-active);
}
}
ion-button.arrow-button {
flex-shrink: 1;
margin: 0;
padding: 0;
--padding-start: 0;
--padding-end: 0;
min-width: 30px;
height: var(--height);
--border-radius: 0;
ion-icon {
font-size: 16px;
}
}
ion-col {
ion-slides {
text-align: center;
line-height: 1.6rem;
flex-grow: 1;
&.col-with-arrow {
display: flex;
justify-content: center;
align-items: center;
ion-slide {
border-bottom: 2px solid transparent;
min-width: 100px;
height: var(--height);
cursor: pointer;
overflow: hidden;
ion-tab-button {
max-width: 100%;
ion-label {
font-size: 14px;
font-weight: 400;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
word-wrap: break-word;
max-width: 100%;
line-height: 1.2em;
}
}
&[aria-selected=true],
&.selected {
color: var(--color-active);
border-bottom-color: var(--border-color-active);
ion-tab-button {
color: var(--color-active);
ion-label {
font-weight: var(--font-weight-active);
}
}
}
}
}
@ -109,8 +117,3 @@
position: relative;
}
}
:host-context(.ios) {
--height: 53px;
}

View File

@ -51,12 +51,6 @@ export class CoreTabsComponent extends CoreTabsBaseComponent<CoreTabComponent> i
protected originalTabsContainer?: HTMLElement; // The container of the original tabs. It will include each tab's content.
constructor(
element: ElementRef,
) {
super(element);
}
/**
* View has been initialized.
*/
@ -67,7 +61,6 @@ export class CoreTabsComponent extends CoreTabsBaseComponent<CoreTabComponent> i
return;
}
this.tabsElement = this.element.nativeElement;
this.originalTabsContainer = this.originalTabsRef?.nativeElement;
}
@ -85,21 +78,13 @@ export class CoreTabsComponent extends CoreTabsBaseComponent<CoreTabComponent> i
*/
addTab(tab: CoreTabComponent): void {
// Check if tab is already in the list.
if (this.getTabIndex(tab.id!) == -1) {
if (this.getTabIndex(tab.id) === -1) {
this.tabs.push(tab);
this.sortTabs();
setTimeout(() => {
this.calculateSlides();
});
if (this.initialized && this.tabs.length > 1 && this.tabBarHeight == 0) {
// Calculate the tabBarHeight again now that there is more than 1 tab and the bar will be seen.
// Use timeout to wait for the view to be rendered. 0 ms should be enough, use 50 to be sure.
setTimeout(() => {
this.calculateTabBarHeight();
}, 50);
}
}
}
@ -109,7 +94,7 @@ export class CoreTabsComponent extends CoreTabsBaseComponent<CoreTabComponent> i
* @param tab The tab to remove.
*/
removeTab(tab: CoreTabComponent): void {
const index = this.getTabIndex(tab.id!);
const index = this.getTabIndex(tab.id);
this.tabs.splice(index, 1);
this.calculateSlides();

View File

@ -50,6 +50,9 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
protected endContentScrollListener?: EventListener;
protected resizeListener?: CoreEventObserver;
protected slotPromise?: CoreCancellablePromise<void>;
protected calcPending = false;
protected pageDidEnterListener?: EventListener;
protected page?: HTMLElement;
constructor(el: ElementRef, protected ionContent: IonContent) {
this.element = el.nativeElement;
@ -82,6 +85,14 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
* Calculate the height of the footer.
*/
protected async calculateHeight(): Promise<void> {
if (!CoreDom.isElementVisible(this.element)) {
this.calcPending = true;
return;
}
this.calcPending = false;
this.element.classList.remove('is-active');
await CoreUtils.nextTick();
@ -159,6 +170,16 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
this.resizeListener = CoreDom.onWindowResize(() => {
this.calculateHeight();
}, 50);
this.page = this.content.closest<HTMLElement>('.ion-page') || undefined;
this.page?.addEventListener(
'ionViewDidEnter',
this.pageDidEnterListener = () => {
if (this.calcPending) {
this.calculateHeight();
}
},
);
}
/**
@ -228,6 +249,9 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
if (this.content && this.endContentScrollListener) {
this.content.removeEventListener('ionScrollEnd', this.endContentScrollListener);
}
if (this.page && this.pageDidEnterListener) {
this.page.removeEventListener('ionViewDidEnter', this.pageDidEnterListener);
}
this.resizeListener?.off();
this.slotPromise?.cancel();

View File

@ -16,6 +16,7 @@ import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChang
import { CorePromisedValue } from '@classes/promised-value';
import { CoreLoadingComponent } from '@components/loading/loading';
import { CoreTabsOutletComponent } from '@components/tabs-outlet/tabs-outlet';
import { CoreTabsComponent } from '@components/tabs/tabs';
import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
import { ScrollDetail } from '@ionic/core';
import { CoreUtils } from '@services/utils/utils';
@ -77,6 +78,8 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
protected isWithinContent = false;
protected enteredPromise = new CorePromisedValue<void>();
protected mutationObserver?: MutationObserver;
protected firstEnter = true;
protected initPending = false;
constructor(el: ElementRef) {
this.collapsedHeader = el.nativeElement;
@ -145,14 +148,19 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
this.page.addEventListener(
'ionViewDidEnter',
this.pageDidEnterListener = () => {
clearTimeout(timeout);
this.enteredPromise.resolve();
if (this.firstEnter) {
this.firstEnter = false;
clearTimeout(timeout);
this.enteredPromise.resolve();
} else if (this.initPending) {
this.initializeFloatingTitle();
}
},
{ once: true },
);
// Timeout in case event is never fired.
const timeout = window.setTimeout(() => {
this.firstEnter = false;
this.enteredPromise.reject(new Error('[collapsible-header] Waiting for ionViewDidEnter timeout reached'));
}, 5000);
@ -223,8 +231,12 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
* Search the page content, initialize it, and wait until it's ready for the transition to trigger on scroll.
*/
protected async initializeContent(): Promise<void> {
if (!this.page) {
return;
}
// Initialize from tabs.
const tabs = CoreComponentsRegistry.resolve(this.page?.querySelector('core-tabs-outlet'), CoreTabsOutletComponent);
const tabs = CoreComponentsRegistry.resolve(this.page.querySelector('core-tabs-outlet'), CoreTabsOutletComponent);
if (tabs) {
const outlet = tabs.getOutlet();
@ -242,7 +254,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
}
// Initialize from page content.
const content = this.page?.querySelector('ion-content:not(.disable-scroll-y)');
const content = this.page.querySelector('ion-content:not(.disable-scroll-y)');
if (!content) {
throw new Error('[collapsible-header] Couldn\'t get content');
@ -259,6 +271,14 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
throw new Error('[collapsible-header] Couldn\'t create floating title');
}
if (!CoreDom.isElementVisible(this.expandedHeader)) {
this.initPending = true;
return;
}
this.initPending = false;
this.page.classList.remove('collapsible-header-page-is-active');
CoreUtils.nextTick();
@ -342,7 +362,19 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
return;
}
// Wait loadings to finish.
await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-loading', CoreLoadingComponent);
// Wait tabs to be ready.
await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-tabs', CoreTabsComponent);
await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-tabs-outlet', CoreTabsOutletComponent);
// Wait loadings to finish, inside tabs (if any).
await CoreComponentsRegistry.waitComponentsReady(
this.page,
'core-tab core-loading, ion-router-outlet core-loading',
CoreLoadingComponent,
);
}
/**

View File

@ -56,6 +56,9 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
protected darkModeListener?: Subscription;
protected domPromise?: CoreCancellablePromise<void>;
protected uniqueId: string;
protected calcPending = false;
protected pageDidEnterListener?: EventListener;
protected page?: HTMLElement;
constructor(el: ElementRef<HTMLElement>) {
this.element = el.nativeElement;
@ -93,6 +96,15 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
await this.calculateHeight();
this.page?.addEventListener(
'ionViewDidEnter',
this.pageDidEnterListener = () => {
if (this.calcPending) {
this.calculateHeight();
}
},
);
this.resizeListener = CoreDom.onWindowResize(() => {
this.calculateHeight();
}, 50);
@ -112,13 +124,12 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
await this.domPromise;
const page = this.element.closest('.ion-page');
if (!page) {
this.page = this.element.closest<HTMLElement>('.ion-page') || undefined;
if (!this.page) {
return;
}
await CoreComponentsRegistry.waitComponentsReady(page, 'core-loading', CoreLoadingComponent);
await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-loading', CoreLoadingComponent);
}
/**
@ -137,6 +148,15 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
await this.waitFormatTextsRendered();
if (!this.element.clientHeight) {
this.calcPending = true;
this.element.classList.remove('collapsible-loading-height');
return;
}
this.calcPending = false;
this.expandedHeight = this.element.getBoundingClientRect().height;
// Restore the max height now.
@ -278,6 +298,10 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
this.resizeListener?.off();
this.darkModeListener?.unsubscribe();
this.domPromise?.cancel();
if (this.page && this.pageDidEnterListener) {
this.page.removeEventListener('ionViewDidEnter', this.pageDidEnterListener);
}
}
}

View File

@ -10,6 +10,8 @@
overflow: hidden;
text-transform: none;
flex: 1;
margin-left: 4px;
margin-right: 4px;
}
}

View File

@ -74,7 +74,7 @@ ion-item.item {
}
&.restricted {
font-size: 14px;
font-size: var(--text-size);
}
}
}

View File

@ -29,6 +29,10 @@
margin: 8px;
padding: 8px;
&:empty {
display: none;
}
::ng-deep ion-item {
--ion-item-background: var(--light);
--background: var(--light);

View File

@ -1,17 +1,16 @@
<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-col size="auto" class="ion-no-padding core-course-module-navigation-arrow">
<ion-button fill="clear" class="core-course-previous-module" [disabled]="!previousModule" (click)="goToActivity(false)"
[attr.aria-label]="'core.course.gotopreviousactivity' | translate">
<ion-icon name="fas-arrow-left" slot="start" aria-hidden="true"></ion-icon>
{{ 'core.previous' | translate }}
<ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding" *ngIf="previousModule || nextModule">
<ion-col size="6" class="ion-no-padding core-course-module-navigation-arrow">
<ion-button fill="clear" class="core-course-previous-module ion-text-wrap" [disabled]="!previousModule"
(click)="goToActivity(false)">
<ion-icon name="fas-chevron-left" slot="start" aria-hidden="true"></ion-icon>
<div class="button-text">{{ 'core.course.previousactivity' | translate }}</div>
</ion-button>
</ion-col>
<ion-col size="auto" class="ion-no-padding core-course-module-navigation-arrow">
<ion-button fill="clear" class="core-course-next-module" [disabled]="!nextModule" (click)="goToActivity(true)"
[attr.aria-label]="'core.course.gotonextactivity' | translate">
{{ 'core.next' | translate }}
<ion-icon name="fas-arrow-right" slot="end" aria-hidden="true"></ion-icon>
<ion-col size="6" class="ion-no-padding core-course-module-navigation-arrow">
<ion-button fill="clear" class="core-course-next-module ion-text-wrap" [disabled]=" !nextModule" (click)="goToActivity(true)">
<div class="button-text">{{ 'core.course.nextactivity' | translate }}</div>
<ion-icon name="fas-chevron-right" slot="end" aria-hidden="true"></ion-icon>
</ion-button>
</ion-col>
</ion-row>

View File

@ -3,7 +3,7 @@
:host {
--height: var(--core-navigation-max-height);
--background: var(--core-navigation-background);
--button-vertical-margin: 2px;
--button-color: var(--gray-700);
height: var(--height);
width: 100%;
@ -16,18 +16,42 @@
--loading-inline-min-height: var(--height);
}
ion-button,
::ng-deep ion-button {
margin-top: var(--button-vertical-margin);
margin-bottom: var(--button-vertical-margin);
}
&.empty {
display: none;
}
.core-course-module-navigation-arrow {
ion-button {
margin: 0;
--border-radius: 0;
width: 100%;
text-transform: none;
font-size: 12px;
font-weight: normal;
--color: var(--button-color);
.button-text {
width:100%;
}
ion-icon {
font-size: 12px;
}
}
.core-course-previous-module {
text-align: start;
}
.core-course-next-module {
text-align: end;
}
}
}
:host-context(core-course-format.core-course-format-singleactivity) {
opacity: 0 !important;
height: 0 !important;
}
:host-context(body.dark) {
--button-color: var(--gray-100);
}

View File

@ -215,7 +215,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
if (!module) {
// It seems the module was hidden. Show a message.
CoreDomUtils.instance.showErrorModal(
next ? 'core.course.gotonextactivitynotfound' : 'core.course.gotopreviousactivitynotfound',
next ? 'core.course.nextactivitynotfound' : 'core.course.previousactivitynotfound',
true,
);

View File

@ -3,7 +3,6 @@
:host {
--horizontal-margin: 10px;
--vertical-margin: 10px;
--core-course-module-not-viewed-border-color: var(--gray-500);
ion-card {
margin: var(--vertical-margin) var(--horizontal-margin);
@ -93,10 +92,6 @@
display: none;
}
&.core-course-module-not-viewed ion-card.core-course-module-with-view {
--ion-card-border-color: var(--core-course-module-not-viewed-border-color);
}
.core-course-last-module-viewed {
padding: 8px 12px;
color: var(--subdued-text-color);

View File

@ -20,11 +20,11 @@
"confirmdownload": "You are about to download {{size}}.{{availableSpace}} Are you sure you want to continue?",
"confirmdownloadunknownsize": "It was not possible to calculate the size of the download.{{availableSpace}} Are you sure you want to continue?",
"confirmdownloadzerosize": "You are about to start downloading.{{availableSpace}} Are you sure you want to continue?",
"confirmpartialdownloadsize": "You are about to download <strong>at least</strong> {{size}}.{{availableSpace}} Are you sure you want to continue?",
"confirmlimiteddownload": "You are not currently connected to Wi-Fi. ",
"courseindex": "Course index",
"confirmpartialdownloadsize": "You are about to download <strong>at least</strong> {{size}}.{{availableSpace}} Are you sure you want to continue?",
"couldnotloadsectioncontent": "Could not load the section content. Please try again later.",
"couldnotloadsections": "Could not load the sections. Please try again later.",
"courseindex": "Course index",
"coursesummary": "Course summary",
"done": "Done",
"downloadcourse": "Download course",
@ -35,20 +35,20 @@
"errordownloadingsection": "Error downloading section.",
"errorgetmodule": "Error getting activity data.",
"failed": "Failed",
"gotonextactivity": "Go to next activity",
"gotonextactivitynotfound": "Next activity not found. It's possible that it has been hidden or deleted.",
"gotopreviousactivity": "Go to previous activity",
"gotopreviousactivitynotfound": "Previous activity not found. It's possible that it has been hidden or deleted.",
"hiddenfromstudents": "Hidden from students",
"hiddenoncoursepage": "Available but not shown on course page",
"highlighted": "Highlighted",
"insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.",
"insufficientavailablequota": "Your device could not allocate space to save this download. It may be reserving space for app and system updates. Please clear some storage space first.",
"insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.",
"lastaccessedactivity": "Last accessed activity",
"manualcompletionnotsynced": "Manual completion not synchronised.",
"modulenotfound": "Resource or activity not found, please make sure you're online and it's still available.",
"nextactivity": "Next activity",
"nextactivitynotfound": "Next activity not found. It's possible that it has been hidden or deleted.",
"nocontentavailable": "No content available at the moment.",
"overriddennotice": "Your final grade from this activity was manually adjusted.",
"previousactivity": "Previous activity",
"previousactivitynotfound": "Previous activity not found. It's possible that it has been hidden or deleted.",
"refreshcourse": "Refresh course",
"section": "Section",
"startdate": "Course start date",

View File

@ -131,6 +131,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
} catch (error) {
CoreDomUtils.showErrorModal(error);
CoreNavigator.back();
this.loaded = true;
return;
}
@ -224,9 +225,9 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
// Select the tab if needed.
this.firstTabName = undefined;
if (tabToLoad) {
setTimeout(() => {
this.tabsComponent?.selectByIndex(tabToLoad!);
});
await CoreUtils.nextTick();
this.tabsComponent?.selectByIndex(tabToLoad);
}
}

View File

@ -98,7 +98,7 @@
}
.expandable-status-icon {
font-size: 14px;
font-size: var(--text-size);
@include margin-horizontal(0, 2px);
@include core-transition(transform, 200ms);

View File

@ -15,6 +15,7 @@
import { Component } from '@angular/core';
import { AsyncComponent } from '@classes/async-component';
import { CoreUtils } from '@services/utils/utils';
import { CoreLogger } from './logger';
/**
* Registry to keep track of component instances.
@ -22,6 +23,7 @@ import { CoreUtils } from '@services/utils/utils';
export class CoreComponentsRegistry {
private static instances: WeakMap<Element, unknown> = new WeakMap();
protected static logger = CoreLogger.getInstance('CoreComponentsRegistry');
/**
* Register a component instance.
@ -78,6 +80,8 @@ export class CoreComponentsRegistry {
): Promise<void> {
const instance = this.resolve(element, componentClass);
if (!instance) {
this.logger.error('No instance registered for element ' + componentClass, element);
return;
}
@ -97,15 +101,21 @@ export class CoreComponentsRegistry {
selector: string,
componentClass?: ComponentConstructor<T>,
): Promise<void> {
let elements: Element[] = [];
if (element.matches(selector)) {
// Element to wait is myself.
await CoreComponentsRegistry.waitComponentReady<T>(element, componentClass);
elements = [element];
} else {
await Promise.all(Array
.from(element.querySelectorAll(selector))
.map(element => CoreComponentsRegistry.waitComponentReady<T>(element, componentClass)));
elements = Array.from(element.querySelectorAll(selector));
}
if (!elements.length) {
return;
}
await Promise.all(elements.map(element => CoreComponentsRegistry.waitComponentReady<T>(element, componentClass)));
// Wait for next tick to ensure components are completely rendered.
await CoreUtils.nextTick();
}

View File

@ -1,4 +1,4 @@
.collapsible-header-page {
body:not(.core-iframe-fullscreen) .collapsible-header-page {
--collapsible-header-progress: 0;
--collapsible-header-collapsed-height: 0px;
--collapsible-header-expanded-y-delta: 0px;
@ -27,10 +27,10 @@
&:not(.collapsible-header-page-is-collapsed) .collapsible-header-collapsed {
--core-header-toolbar-border-width: 0;
--core-header-buttons-background: var(--ion-background-color);
--core-header-buttons-color: var(--text-color);
ion-toolbar {
--background: transparent;
--core-header-buttons-background: var(--ion-background-color);
--core-header-buttons-color: var(--text-color);
}
h1 {

View File

@ -36,6 +36,8 @@
color: var(--collapsible-toggle-text);
min-height: var(--toggle-size);
min-width: var(--toggle-size);
height: var(--toggle-size);
width: var(--toggle-size);
--border-radius: var(--huge-radius);
border-radius: var(--border-radius);
--padding-start: 0px;

View File

@ -210,7 +210,7 @@ core-rich-text-editor .core-rte-editor {
p, ul, ol, li {
// Normalize font-size inside formatted text.
font-size: 14px;
font-size: var(--text-size);
}
p {

View File

@ -96,7 +96,7 @@ body {
&.item-heading-secondary {
@include margin(2px, 0);
font-size: 14px;
font-size: var(--text-size);
font-weight: normal;
line-height: normal;
@ -112,7 +112,7 @@ body {
&.item-heading-secondary {
@include margin(0, 0, 3px);
font-size: 14px;
font-size: var(--text-size);
font-weight: normal;
line-height: normal;
@ -154,9 +154,6 @@ ion-header {
z-index: 12; // To hide ion-slides on scroll.
ion-toolbar {
--core-header-buttons-background: var(--core-header-toolbar-background);
--core-header-buttons-color: var(--core-header-toolbar-color);
ion-spinner {
margin: 10px;
}
@ -225,7 +222,7 @@ ion-header {
h1 + h2,
h1 + .subheading {
font-size: 14px;
font-size: var(--text-size);
font-weight: 400;
}
@ -245,7 +242,7 @@ ion-header {
h1 + h2,
h1 + .subheading {
font-size: 14px;
font-size: var(--text-size);
font-weight: 400;
}
}
@ -579,14 +576,19 @@ body.core-iframe-fullscreen ion-router-outlet {
}
}
--core-header-toolbar-height: 48px;
--core-header-toolbar-color: white;
--core-header-toolbar-background: black;
--core-header-toolbar-border-width: 0px;
.ion-page ion-header {
--core-header-toolbar-height: 48px;
--core-header-toolbar-color: white;
--core-header-toolbar-background: black;
--core-header-buttons-background: var(--core-header-toolbar-background);
--core-header-buttons-background: var(--core-header-toolbar-background);
--core-header-buttons-color: var(--core-header-toolbar-color);
--core-header-toolbar-border-width: 0px;
ion-header ion-toolbar {
h1, ion-back-button {
display: none;
ion-toolbar {
h1, ion-back-button {
display: none;
}
}
}
@ -688,7 +690,7 @@ body.core-iframe-fullscreen ion-router-outlet {
font-style: italic;
margin-top: 0;
margin-bottom: 10px;
font-size: 14px;
font-size: var(--text-size);
}
// Item styles
@ -712,7 +714,7 @@ body.core-iframe-fullscreen ion-router-outlet {
}
p.item-heading {
font-size: 14px;
font-size: var(--text-size);
}
p {
@ -765,6 +767,7 @@ body.core-iframe-fullscreen ion-router-outlet {
--color: var(--color-shade);
--inner-border-width: 0px;
--border-width: 0px;
font-size: var(--text-size);
ion-label, ion-label > p {
--color: var(--color-shade);
@ -860,6 +863,7 @@ ion-content.limited-width > :not([slot]) {
ion-content.limited-width > :not([slot]) {
display: flex;
flex-direction: column;
min-height: 100%;
}
ion-toolbar h1 img.core-bar-button-image,
@ -1225,6 +1229,7 @@ audio.core-media-adapt-width {
}
ion-item {
font-size: var(--text-size);
--inner-border-width: 0px;
}
@ -1281,7 +1286,7 @@ html.md div.fake-ion-item {
h6 {
@include margin(2px, 0);
font-size: 14px;
font-size: var(--text-size);
font-weight: normal;
line-height: normal;
@ -1289,7 +1294,7 @@ html.md div.fake-ion-item {
p {
@include margin(0, 0, 2px);
font-size: 14px;
font-size: var(--text-size);
line-height: 20px;
text-overflow: inherit;
overflow: inherit;
@ -1297,7 +1302,7 @@ html.md div.fake-ion-item {
}
html.ios div.fake-ion-item {
font-size: 14px;
font-size: var(--text-size);
@include padding(null, 10px, null, 20px);
@include margin(10px, 8px, 10px, null);
@ -1318,14 +1323,14 @@ html.ios div.fake-ion-item {
h5,
h6 {
@include margin(0, 0, 3px);
font-size: 14px;
font-size: var(--text-size);
font-weight: normal;
line-height: normal;
}
p {
@include margin(0, 0, 2px 0);
font-size: 14px;
font-size: var(--text-size);
line-height: normal;
text-overflow: inherit;
overflow: inherit;
@ -1496,6 +1501,7 @@ ion-content.disable-scroll-y::part(scroll) {
}
iframe {
flex-grow: 1;
border: 0;
display: block;
max-width: 100%;
@ -1585,11 +1591,8 @@ ion-header.no-title {
--core-header-toolbar-border-width: 0;
--core-header-toolbar-background: transparent;
--core-header-shadow: none !important;
ion-toolbar {
--core-header-buttons-background: var(--ion-background-color);
--core-header-buttons-color: var(--text-color);
}
--core-header-buttons-background: var(--ion-background-color);
--core-header-buttons-color: var(--text-color);
}
// To make core-swipe-slides fill the remaining height.

View File

@ -59,6 +59,7 @@
--huge-radius: 24px;
--text-color: #{$text-color};
--text-size: 14px;
--background-color: #{$background-color};
--stroke: var(--gray-300);
@ -148,6 +149,8 @@
--core-header-toolbar-color: var(--text-color);
--core-header-toolbar-height: 48px;
--core-header-shadow: none;
--core-header-buttons-background: var(--core-header-toolbar-background);
--core-header-buttons-color: var(--core-header-toolbar-color);
ion-header {
box-shadow: var(--core-header-shadow, none);