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.errordownloadingsection": "local_moodlemobileapp",
"core.course.errorgetmodule": "local_moodlemobileapp", "core.course.errorgetmodule": "local_moodlemobileapp",
"core.course.failed": "completion", "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.hiddenfromstudents": "moodle",
"core.course.hiddenoncoursepage": "moodle", "core.course.hiddenoncoursepage": "moodle",
"core.course.highlighted": "moodle", "core.course.highlighted": "moodle",
@ -1570,8 +1566,12 @@
"core.course.lastaccessedactivity": "local_moodlemobileapp", "core.course.lastaccessedactivity": "local_moodlemobileapp",
"core.course.manualcompletionnotsynced": "local_moodlemobileapp", "core.course.manualcompletionnotsynced": "local_moodlemobileapp",
"core.course.modulenotfound": "local_moodlemobileapp", "core.course.modulenotfound": "local_moodlemobileapp",
"core.course.nextactivity": "local_moodlemobileapp",
"core.course.nextactivitynotfound": "local_moodlemobileapp",
"core.course.nocontentavailable": "local_moodlemobileapp", "core.course.nocontentavailable": "local_moodlemobileapp",
"core.course.overriddennotice": "grades", "core.course.overriddennotice": "grades",
"core.course.previousactivity": "local_moodlemobileapp",
"core.course.previousactivitynotfound": "local_moodlemobileapp",
"core.course.refreshcourse": "local_moodlemobileapp", "core.course.refreshcourse": "local_moodlemobileapp",
"core.course.section": "moodle", "core.course.section": "moodle",
"core.course.startdate": "moodle", "core.course.startdate": "moodle",

View File

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

View File

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

View File

@ -158,7 +158,7 @@
</ion-button> </ion-button>
<ion-button expand="block" (click)="continue()" class="ion-text-wrap ion-margin"> <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.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> </ion-button>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@ -30,8 +30,8 @@
<core-loading [hideUntil]="!showLoading"> <core-loading [hideUntil]="!showLoading">
<!-- Activity info. --> <!-- Activity info. -->
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId" <core-course-module-info *ngIf="isMainPage" [module]="module" [description]="description" [component]="component"
[courseId]="courseId" (completionChanged)="onCompletionChange()"> [componentId]="componentId" [courseId]="courseId" (completionChanged)="onCompletionChange()">
</core-course-module-info> </core-course-module-info>
<div *ngIf="pageIsOffline || hasOffline || pageWarning"> <div *ngIf="pageIsOffline || hasOffline || pageWarning">
@ -54,7 +54,7 @@
</ion-item> </ion-item>
</ion-card> </ion-card>
</div> </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> <h2 *ngIf="pageTitle">{{pageTitle}}</h2>
<article [ngClass]="{'addon-mod_wiki-noedit': !canEdit}"> <article [ngClass]="{'addon-mod_wiki-noedit': !canEdit}">
<core-format-text *ngIf="pageContent" [component]="component" [componentId]="componentId" [text]="pageContent" <core-format-text *ngIf="pageContent" [component]="component" [componentId]="componentId" [text]="pageContent"
@ -71,7 +71,8 @@
</div> </div>
</core-loading> </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> </core-course-module-navigation>
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canEdit"> <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 { .addon-mod_wiki-page-content {
background-color: var(--ion-item-background); background-color: var(--ion-item-background);
border-top: 1px solid var(--stroke);
padding-bottom: 10px; padding-bottom: 10px;
} }

View File

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

View File

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

View File

@ -22,11 +22,11 @@ import {
OnDestroy, OnDestroy,
AfterViewInit, AfterViewInit,
ViewChild, ViewChild,
ElementRef,
SimpleChange, SimpleChange,
ElementRef,
} from '@angular/core'; } from '@angular/core';
import { IonSlides } from '@ionic/angular'; import { IonSlides } from '@ionic/angular';
import { BackButtonEvent, ScrollDetail } from '@ionic/core'; import { BackButtonEvent } from '@ionic/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { Platform, Translate } from '@singletons'; 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 { CoreAriaRoleTab, CoreAriaRoleTabFindable } from './aria-role-tab';
import { CoreEventObserver } from '@singletons/events'; import { CoreEventObserver } from '@singletons/events';
import { CoreDom } from '@singletons/dom'; 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. * Class to abstract some common code for tabs.
@ -41,13 +46,10 @@ import { CoreDom } from '@singletons/dom';
@Component({ @Component({
template: '', 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. // Minimum tab's width.
protected static readonly MIN_TAB_WIDTH = 107; 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() selectedIndex = 0; // Index of the tab to select.
@Input() hideUntil = false; // Determine when should the contents be shown. @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. tabs: T[] = []; // List of tabs.
hideTabs = false;
selected?: string; // Selected tab id. selected?: string; // Selected tab id.
showPrevButton = false; showPrevButton = false;
showNextButton = false; showNextButton = false;
@ -68,15 +71,12 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
initialSlide: 0, initialSlide: 0,
slidesPerView: 3, slidesPerView: 3,
centerInsufficientSlides: true, centerInsufficientSlides: true,
threshold: 10,
}; };
protected slidesElement?: HTMLIonSlidesElement;
protected initialized = false; 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 resizeListener?: CoreEventObserver;
protected isDestroyed = false; protected isDestroyed = false;
protected isCurrentView = true; 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 firstSelectedTab?: string; // ID of the first selected tab to control history.
protected backButtonFunction: (event: BackButtonEvent) => void; protected backButtonFunction: (event: BackButtonEvent) => void;
protected languageChangedSubscription?: Subscription;
// Swiper 6 documentation: https://swiper6.vercel.app/ // Swiper 6 documentation: https://swiper6.vercel.app/
protected isInTransition = false; // Wether Slides is in transition. protected isInTransition = false; // Wether Slides is in transition.
protected slidesSwiper: any; // eslint-disable-line @typescript-eslint/no-explicit-any protected subscriptions: Subscription[] = [];
protected slidesSwiperLoaded = false; protected onReadyPromise = new CorePromisedValue<void>();
protected scrollElements: Record<string | number, HTMLElement> = {}; // Scroll elements for each loaded tab.
protected lastScroll = 0;
protected previousLastScroll = 0;
tabAction: CoreTabsRoleTab<T>; tabAction: CoreTabsRoleTab<T>;
constructor( constructor(element: ElementRef) {
protected element: ElementRef,
) {
this.backButtonFunction = this.backButtonClicked.bind(this); this.backButtonFunction = this.backButtonClicked.bind(this);
this.tabAction = new CoreTabsRoleTab(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'; this.direction = Platform.isRTL ? 'rtl' : 'ltr';
// Change the side when the language changes. // Change the side when the language changes.
this.languageChangedSubscription = Translate.onLangChange.subscribe(() => { this.subscriptions.push(Translate.onLangChange.subscribe(() => {
setTimeout(() => { setTimeout(() => {
this.direction = Platform.isRTL ? 'rtl' : 'ltr'; this.direction = Platform.isRTL ? 'rtl' : 'ltr';
}); });
}); }));
} }
/** /**
* View has been initialized. * @inheritdoc
*/ */
async ngAfterViewInit(): Promise<void> { ngAfterViewInit(): void {
if (this.isDestroyed) { if (this.isDestroyed) {
return; return;
} }
this.afterViewInitTriggered = true; this.init();
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;
} }
/** /**
@ -188,14 +131,7 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
ngOnChanges(changes: Record<string, SimpleChange>): void { ngOnChanges(changes: Record<string, SimpleChange>): void {
// Wait for ngAfterViewInit so it works in the case that each tab has its own component. this.init();
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();
});
}
} }
/** /**
@ -260,14 +196,15 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
return; return;
} }
if (window.innerHeight >= CoreTabsBaseComponent.MAX_HEIGHT_TO_HIDE_TABS) { this.numTabsShown = this.tabs.reduce((prev: number, current) => current.enabled ? prev + 1 : prev, 0);
// Ensure tabbar is shown.
this.applyScroll(true, 0); if (this.numTabsShown <= 1) {
this.calculateTabBarHeight(); this.hideTabs = true;
} else if (!this.tabsShown) {
// Don't recalculate. // Only one, nothing to do here.
return; return;
} }
this.hideTabs = false;
await this.calculateMaxSlides(); 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> { protected async init(): Promise<void> {
// Initialize slider. if (!this.hideUntil) {
this.slidesSwiper = await this.slides?.getSwiper(); // Hidden, do nothing.
this.slidesSwiper.once('progress', () => {
this.slidesSwiperLoaded = true;
this.calculateSlides();
});
const selectedTab = this.calculateInitialTab();
if (!selectedTab) {
return; return;
} }
this.firstSelectedTab = selectedTab.id!; try {
this.selectTab(this.firstSelectedTab); 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; 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. // Check which arrows should be shown.
this.calculateSlides(); 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. * Method executed when the slides are changed.
*/ */
async slideChanged(): Promise<void> { async slideChanged(): Promise<void> {
if (!this.slidesSwiperLoaded) { if (!this.slidesElement) {
return; return;
} }
@ -368,19 +355,15 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
* Updates the number of slides to show. * Updates the number of slides to show.
*/ */
protected async updateSlides(): Promise<void> { 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.slidesOpts = { ...this.slidesOpts, slidesPerView: Math.min(this.maxSlides, this.numTabsShown) };
this.slideChanged(); await this.slideChanged();
this.calculateTabBarHeight(); await this.slides.update();
// @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();
if (!this.hasSliddenToInitial && this.selectedIndex && this.selectedIndex >= this.slidesOpts.slidesPerView) { if (!this.hasSliddenToInitial && this.selectedIndex && this.selectedIndex >= this.slidesOpts.slidesPerView) {
this.hasSliddenToInitial = true; this.hasSliddenToInitial = true;
@ -388,7 +371,7 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
setTimeout(() => { setTimeout(() => {
if (this.shouldSlideToInitial) { if (this.shouldSlideToInitial) {
this.slides!.slideTo(this.selectedIndex, 0); this.slides?.slideTo(this.selectedIndex, 0);
this.shouldSlideToInitial = false; this.shouldSlideToInitial = false;
} }
}, 400); }, 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. * Calculate the number of slides that can fit on the screen.
*/ */
protected async calculateMaxSlides(): Promise<void> { protected async calculateMaxSlides(): Promise<void> {
if (!this.slidesSwiperLoaded) { if (!this.slidesElement || !this.slides) {
return; return;
} }
this.maxSlides = 3; this.maxSlides = 3;
let width = this.slidesSwiper.width; await CoreUtils.nextTick();
if (!width) {
this.slidesSwiper.updateSize();
width = this.slidesSwiper.width;
let width: number = this.slidesElement.getBoundingClientRect().width;
if (!width) { if (!width) {
const slidesSwiper = await this.slides.getSwiper();
await slidesSwiper.updateSize();
await CoreUtils.nextTick();
width = slidesSwiper.width;
if (!width) {
return; 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. * Select a tab by ID.
* *
@ -579,12 +513,12 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
return; return;
} }
if (this.selected) { if (this.selected && this.slides) {
// Check if we need to slide to the tab because it's not visible. // 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; const lastVisibleTab = firstVisibleTab + this.slidesOpts.slidesPerView - 1;
if (index < firstVisibleTab || index > lastVisibleTab) { 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; return;
} }
const ok = await this.loadTab(tabToSelect); const suceeded = await this.loadTab(tabToSelect);
if (ok !== false) { if (suceeded !== false) {
this.tabSelected(tabToSelect, index); 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). * @inheritdoc
*
* @param element Element to search ion-content in.
* @param id ID of the tab/page.
* @return Promise resolved when done.
*/ */
async listenContentScroll(element: HTMLElement, id: number | string): Promise<void> { async ready(): Promise<void> {
if (this.scrollElements[id]) { return await this.onReadyPromise;
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);
});
} }
/** /**
* Adapt tabs to a window resize. * @inheritdoc
*/
protected windowResized(): void {
setTimeout(() => {
this.calculateSlides();
}, 200);
}
/**
* Component destroyed.
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.isDestroyed = true; this.isDestroyed = true;
this.resizeListener?.off(); 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[] { getSelectableTabs(): CoreAriaRoleTabFindable[] {
return this.componentInstance.tabs.filter((tab) => tab.enabled).map((tab) => ({ return this.componentInstance.tabs.filter((tab) => tab.enabled).map((tab) => ({
id: tab.id!, id: tab.id || '',
findIndex: tab.id!, findIndex: tab.id || '',
})); }));
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@
position: relative; position: relative;
} }
ion-tab-bar.core-tabs-bar { ion-tab-bar {
position: relative; position: relative;
background: var(--tabs-background); background: var(--tabs-background);
@include safe-area-padding-end(null, 0px); @include safe-area-padding-end(null, 0px);
@ -22,24 +22,44 @@
color: var(--tabs-color); color: var(--tabs-color);
border-bottom: 1px solid var(--stroke); border-bottom: 1px solid var(--stroke);
display: flex; display: flex;
align-items: flex-end; flex-direction: row;
justify-content: space-between;
flex-shrink: 0; flex-shrink: 0;
ion-row { ion-spinner {
width: 100%; flex-grow: 1;
} }
.tab-slide { 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-slides {
text-align: center;
line-height: 1.6rem;
flex-grow: 1;
ion-slide {
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
min-width: 100px; min-width: 100px;
min-height: var(--height); height: var(--height);
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
ion-tab-button { ion-tab-button {
max-width: 100%; max-width: 100%;
ion-label { ion-label {
font-size: 16px; font-size: 14px;
font-weight: 400; font-weight: 400;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -47,8 +67,6 @@
word-wrap: break-word; word-wrap: break-word;
max-width: 100%; max-width: 100%;
line-height: 1.2em; line-height: 1.2em;
margin-top: 16px;
margin-bottom: 16px;
} }
} }
@ -64,16 +82,6 @@
} }
} }
} }
ion-col {
text-align: center;
line-height: 1.6rem;
&.col-with-arrow {
display: flex;
justify-content: center;
align-items: center;
}
} }
&.tabs-hidden { &.tabs-hidden {
@ -109,8 +117,3 @@
position: relative; 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. 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. * View has been initialized.
*/ */
@ -67,7 +61,6 @@ export class CoreTabsComponent extends CoreTabsBaseComponent<CoreTabComponent> i
return; return;
} }
this.tabsElement = this.element.nativeElement;
this.originalTabsContainer = this.originalTabsRef?.nativeElement; this.originalTabsContainer = this.originalTabsRef?.nativeElement;
} }
@ -85,21 +78,13 @@ export class CoreTabsComponent extends CoreTabsBaseComponent<CoreTabComponent> i
*/ */
addTab(tab: CoreTabComponent): void { addTab(tab: CoreTabComponent): void {
// Check if tab is already in the list. // 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.tabs.push(tab);
this.sortTabs(); this.sortTabs();
setTimeout(() => { setTimeout(() => {
this.calculateSlides(); 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. * @param tab The tab to remove.
*/ */
removeTab(tab: CoreTabComponent): void { removeTab(tab: CoreTabComponent): void {
const index = this.getTabIndex(tab.id!); const index = this.getTabIndex(tab.id);
this.tabs.splice(index, 1); this.tabs.splice(index, 1);
this.calculateSlides(); this.calculateSlides();

View File

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

View File

@ -16,6 +16,7 @@ import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChang
import { CorePromisedValue } from '@classes/promised-value'; import { CorePromisedValue } from '@classes/promised-value';
import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreLoadingComponent } from '@components/loading/loading';
import { CoreTabsOutletComponent } from '@components/tabs-outlet/tabs-outlet'; import { CoreTabsOutletComponent } from '@components/tabs-outlet/tabs-outlet';
import { CoreTabsComponent } from '@components/tabs/tabs';
import { CoreSettingsHelper } from '@features/settings/services/settings-helper'; import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
import { ScrollDetail } from '@ionic/core'; import { ScrollDetail } from '@ionic/core';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
@ -77,6 +78,8 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
protected isWithinContent = false; protected isWithinContent = false;
protected enteredPromise = new CorePromisedValue<void>(); protected enteredPromise = new CorePromisedValue<void>();
protected mutationObserver?: MutationObserver; protected mutationObserver?: MutationObserver;
protected firstEnter = true;
protected initPending = false;
constructor(el: ElementRef) { constructor(el: ElementRef) {
this.collapsedHeader = el.nativeElement; this.collapsedHeader = el.nativeElement;
@ -145,14 +148,19 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
this.page.addEventListener( this.page.addEventListener(
'ionViewDidEnter', 'ionViewDidEnter',
this.pageDidEnterListener = () => { this.pageDidEnterListener = () => {
if (this.firstEnter) {
this.firstEnter = false;
clearTimeout(timeout); clearTimeout(timeout);
this.enteredPromise.resolve(); this.enteredPromise.resolve();
} else if (this.initPending) {
this.initializeFloatingTitle();
}
}, },
{ once: true },
); );
// Timeout in case event is never fired. // Timeout in case event is never fired.
const timeout = window.setTimeout(() => { const timeout = window.setTimeout(() => {
this.firstEnter = false;
this.enteredPromise.reject(new Error('[collapsible-header] Waiting for ionViewDidEnter timeout reached')); this.enteredPromise.reject(new Error('[collapsible-header] Waiting for ionViewDidEnter timeout reached'));
}, 5000); }, 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. * Search the page content, initialize it, and wait until it's ready for the transition to trigger on scroll.
*/ */
protected async initializeContent(): Promise<void> { protected async initializeContent(): Promise<void> {
if (!this.page) {
return;
}
// Initialize from tabs. // 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) { if (tabs) {
const outlet = tabs.getOutlet(); const outlet = tabs.getOutlet();
@ -242,7 +254,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
} }
// Initialize from page content. // 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) { if (!content) {
throw new Error('[collapsible-header] Couldn\'t get 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'); 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'); this.page.classList.remove('collapsible-header-page-is-active');
CoreUtils.nextTick(); CoreUtils.nextTick();
@ -342,7 +362,19 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
return; return;
} }
// Wait loadings to finish.
await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-loading', CoreLoadingComponent); 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 darkModeListener?: Subscription;
protected domPromise?: CoreCancellablePromise<void>; protected domPromise?: CoreCancellablePromise<void>;
protected uniqueId: string; protected uniqueId: string;
protected calcPending = false;
protected pageDidEnterListener?: EventListener;
protected page?: HTMLElement;
constructor(el: ElementRef<HTMLElement>) { constructor(el: ElementRef<HTMLElement>) {
this.element = el.nativeElement; this.element = el.nativeElement;
@ -93,6 +96,15 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
await this.calculateHeight(); await this.calculateHeight();
this.page?.addEventListener(
'ionViewDidEnter',
this.pageDidEnterListener = () => {
if (this.calcPending) {
this.calculateHeight();
}
},
);
this.resizeListener = CoreDom.onWindowResize(() => { this.resizeListener = CoreDom.onWindowResize(() => {
this.calculateHeight(); this.calculateHeight();
}, 50); }, 50);
@ -112,13 +124,12 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
await this.domPromise; await this.domPromise;
const page = this.element.closest('.ion-page'); this.page = this.element.closest<HTMLElement>('.ion-page') || undefined;
if (!this.page) {
if (!page) {
return; 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(); 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; this.expandedHeight = this.element.getBoundingClientRect().height;
// Restore the max height now. // Restore the max height now.
@ -278,6 +298,10 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
this.resizeListener?.off(); this.resizeListener?.off();
this.darkModeListener?.unsubscribe(); this.darkModeListener?.unsubscribe();
this.domPromise?.cancel(); this.domPromise?.cancel();
if (this.page && this.pageDidEnterListener) {
this.page.removeEventListener('ionViewDidEnter', this.pageDidEnterListener);
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
:host { :host {
--height: var(--core-navigation-max-height); --height: var(--core-navigation-max-height);
--background: var(--core-navigation-background); --background: var(--core-navigation-background);
--button-vertical-margin: 2px; --button-color: var(--gray-700);
height: var(--height); height: var(--height);
width: 100%; width: 100%;
@ -16,18 +16,42 @@
--loading-inline-min-height: var(--height); --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 { &.empty {
display: none; 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) { :host-context(core-course-format.core-course-format-singleactivity) {
opacity: 0 !important; opacity: 0 !important;
height: 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) { if (!module) {
// It seems the module was hidden. Show a message. // It seems the module was hidden. Show a message.
CoreDomUtils.instance.showErrorModal( CoreDomUtils.instance.showErrorModal(
next ? 'core.course.gotonextactivitynotfound' : 'core.course.gotopreviousactivitynotfound', next ? 'core.course.nextactivitynotfound' : 'core.course.previousactivitynotfound',
true, true,
); );

View File

@ -3,7 +3,6 @@
:host { :host {
--horizontal-margin: 10px; --horizontal-margin: 10px;
--vertical-margin: 10px; --vertical-margin: 10px;
--core-course-module-not-viewed-border-color: var(--gray-500);
ion-card { ion-card {
margin: var(--vertical-margin) var(--horizontal-margin); margin: var(--vertical-margin) var(--horizontal-margin);
@ -93,10 +92,6 @@
display: none; 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 { .core-course-last-module-viewed {
padding: 8px 12px; padding: 8px 12px;
color: var(--subdued-text-color); 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?", "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?", "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?", "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. ", "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.", "couldnotloadsectioncontent": "Could not load the section content. Please try again later.",
"couldnotloadsections": "Could not load the sections. Please try again later.", "couldnotloadsections": "Could not load the sections. Please try again later.",
"courseindex": "Course index",
"coursesummary": "Course summary", "coursesummary": "Course summary",
"done": "Done", "done": "Done",
"downloadcourse": "Download course", "downloadcourse": "Download course",
@ -35,20 +35,20 @@
"errordownloadingsection": "Error downloading section.", "errordownloadingsection": "Error downloading section.",
"errorgetmodule": "Error getting activity data.", "errorgetmodule": "Error getting activity data.",
"failed": "Failed", "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", "hiddenfromstudents": "Hidden from students",
"hiddenoncoursepage": "Available but not shown on course page", "hiddenoncoursepage": "Available but not shown on course page",
"highlighted": "Highlighted", "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.", "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", "lastaccessedactivity": "Last accessed activity",
"manualcompletionnotsynced": "Manual completion not synchronised.", "manualcompletionnotsynced": "Manual completion not synchronised.",
"modulenotfound": "Resource or activity not found, please make sure you're online and it's still available.", "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.", "nocontentavailable": "No content available at the moment.",
"overriddennotice": "Your final grade from this activity was manually adjusted.", "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", "refreshcourse": "Refresh course",
"section": "Section", "section": "Section",
"startdate": "Course start date", "startdate": "Course start date",

View File

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

View File

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

View File

@ -15,6 +15,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { AsyncComponent } from '@classes/async-component'; import { AsyncComponent } from '@classes/async-component';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreLogger } from './logger';
/** /**
* Registry to keep track of component instances. * Registry to keep track of component instances.
@ -22,6 +23,7 @@ import { CoreUtils } from '@services/utils/utils';
export class CoreComponentsRegistry { export class CoreComponentsRegistry {
private static instances: WeakMap<Element, unknown> = new WeakMap(); private static instances: WeakMap<Element, unknown> = new WeakMap();
protected static logger = CoreLogger.getInstance('CoreComponentsRegistry');
/** /**
* Register a component instance. * Register a component instance.
@ -78,6 +80,8 @@ export class CoreComponentsRegistry {
): Promise<void> { ): Promise<void> {
const instance = this.resolve(element, componentClass); const instance = this.resolve(element, componentClass);
if (!instance) { if (!instance) {
this.logger.error('No instance registered for element ' + componentClass, element);
return; return;
} }
@ -97,15 +101,21 @@ export class CoreComponentsRegistry {
selector: string, selector: string,
componentClass?: ComponentConstructor<T>, componentClass?: ComponentConstructor<T>,
): Promise<void> { ): Promise<void> {
let elements: Element[] = [];
if (element.matches(selector)) { if (element.matches(selector)) {
// Element to wait is myself. // Element to wait is myself.
await CoreComponentsRegistry.waitComponentReady<T>(element, componentClass); elements = [element];
} else { } else {
await Promise.all(Array elements = Array.from(element.querySelectorAll(selector));
.from(element.querySelectorAll(selector))
.map(element => CoreComponentsRegistry.waitComponentReady<T>(element, componentClass)));
} }
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. // Wait for next tick to ensure components are completely rendered.
await CoreUtils.nextTick(); 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-progress: 0;
--collapsible-header-collapsed-height: 0px; --collapsible-header-collapsed-height: 0px;
--collapsible-header-expanded-y-delta: 0px; --collapsible-header-expanded-y-delta: 0px;
@ -27,10 +27,10 @@
&:not(.collapsible-header-page-is-collapsed) .collapsible-header-collapsed { &:not(.collapsible-header-page-is-collapsed) .collapsible-header-collapsed {
--core-header-toolbar-border-width: 0; --core-header-toolbar-border-width: 0;
ion-toolbar {
--background: transparent;
--core-header-buttons-background: var(--ion-background-color); --core-header-buttons-background: var(--ion-background-color);
--core-header-buttons-color: var(--text-color); --core-header-buttons-color: var(--text-color);
ion-toolbar {
--background: transparent;
} }
h1 { h1 {

View File

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

View File

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

View File

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

View File

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