commit
0143c23395
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
ion-item > p[slot="end"] {
|
||||
font-size: 14px;
|
||||
font-size: var(--text-size);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
--even-background: var(--gray-200);
|
||||
|
||||
.option-name {
|
||||
font-size: 14px;
|
||||
font-size: var(--text-size);
|
||||
}
|
||||
|
||||
.addon-mod_survey-question {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
.core-notification-body {
|
||||
core-format-text {
|
||||
font-size: 14px;
|
||||
font-size: var(--text-size);
|
||||
}
|
||||
|
||||
h2 {
|
||||
|
|
|
@ -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 || '',
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -93,4 +93,5 @@
|
|||
:host-context(.limited-width > ):not([slot]) {
|
||||
--contents-display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
z-index: 3;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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!);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
overflow: hidden;
|
||||
text-transform: none;
|
||||
flex: 1;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ ion-item.item {
|
|||
}
|
||||
|
||||
&.restricted {
|
||||
font-size: 14px;
|
||||
font-size: var(--text-size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,10 @@
|
|||
margin: 8px;
|
||||
padding: 8px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::ng-deep ion-item {
|
||||
--ion-item-background: var(--light);
|
||||
--background: var(--light);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue