diff --git a/scripts/langindex.json b/scripts/langindex.json index 3a9d7ae12..aba8a3680 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -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", diff --git a/src/addons/calendar/components/calendar/calendar.scss b/src/addons/calendar/components/calendar/calendar.scss index b5c40fd10..1fe5598cc 100644 --- a/src/addons/calendar/components/calendar/calendar.scss +++ b/src/addons/calendar/components/calendar/calendar.scss @@ -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 { diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.scss b/src/addons/mod/bigbluebuttonbn/components/index/index.scss index 61fe68559..c6f581f99 100644 --- a/src/addons/mod/bigbluebuttonbn/components/index/index.scss +++ b/src/addons/mod/bigbluebuttonbn/components/index/index.scss @@ -1,3 +1,3 @@ ion-item > p[slot="end"] { - font-size: 14px; + font-size: var(--text-size); } diff --git a/src/addons/mod/feedback/pages/form/form.html b/src/addons/mod/feedback/pages/form/form.html index 6ca2f58d8..e16c65d39 100644 --- a/src/addons/mod/feedback/pages/form/form.html +++ b/src/addons/mod/feedback/pages/form/form.html @@ -158,7 +158,7 @@ {{ 'core.continue' | translate }} - {{ 'core.course.gotonextactivity' | translate }} + {{ 'core.course.nextactivity' | translate }} diff --git a/src/addons/mod/folder/components/index/addon-mod-folder-index.html b/src/addons/mod/folder/components/index/addon-mod-folder-index.html index 3a121644d..e41323e5d 100644 --- a/src/addons/mod/folder/components/index/addon-mod-folder-index.html +++ b/src/addons/mod/folder/components/index/addon-mod-folder-index.html @@ -32,6 +32,6 @@ - + diff --git a/src/addons/mod/resource/components/index/addon-mod-resource-index.html b/src/addons/mod/resource/components/index/addon-mod-resource-index.html index 213d21523..6291b1a09 100644 --- a/src/addons/mod/resource/components/index/addon-mod-resource-index.html +++ b/src/addons/mod/resource/components/index/addon-mod-resource-index.html @@ -81,6 +81,12 @@
+ + + {{ 'core.openwith' | translate }} + + @@ -91,12 +97,6 @@ {{ 'addon.mod_resource.openthefile' | translate }} - - - - {{ 'core.openwith' | translate }} -
diff --git a/src/addons/mod/scorm/components/index/index.scss b/src/addons/mod/scorm/components/index/index.scss index 636b83653..f8fc150a1 100644 --- a/src/addons/mod/scorm/components/index/index.scss +++ b/src/addons/mod/scorm/components/index/index.scss @@ -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 { diff --git a/src/addons/mod/survey/components/index/index.scss b/src/addons/mod/survey/components/index/index.scss index 6f557a178..bdb061e94 100644 --- a/src/addons/mod/survey/components/index/index.scss +++ b/src/addons/mod/survey/components/index/index.scss @@ -3,7 +3,7 @@ --even-background: var(--gray-200); .option-name { - font-size: 14px; + font-size: var(--text-size); } .addon-mod_survey-question { diff --git a/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html b/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html index 4c220849f..56336ba50 100644 --- a/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html +++ b/src/addons/mod/wiki/components/index/addon-mod-wiki-index.html @@ -30,8 +30,8 @@ - +
@@ -54,7 +54,7 @@
-
+

{{pageTitle}}

- + diff --git a/src/addons/mod/wiki/components/index/index.scss b/src/addons/mod/wiki/components/index/index.scss index f0251ad37..64a882770 100644 --- a/src/addons/mod/wiki/components/index/index.scss +++ b/src/addons/mod/wiki/components/index/index.scss @@ -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; } diff --git a/src/addons/notifications/pages/list/list.scss b/src/addons/notifications/pages/list/list.scss index da15978c7..e58fa6b12 100644 --- a/src/addons/notifications/pages/list/list.scss +++ b/src/addons/notifications/pages/list/list.scss @@ -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; diff --git a/src/addons/notifications/pages/notification/notification.scss b/src/addons/notifications/pages/notification/notification.scss index 05f459851..7b0943218 100644 --- a/src/addons/notifications/pages/notification/notification.scss +++ b/src/addons/notifications/pages/notification/notification.scss @@ -17,7 +17,7 @@ .core-notification-body { core-format-text { - font-size: 14px; + font-size: var(--text-size); } h2 { diff --git a/src/core/classes/tabs.ts b/src/core/classes/tabs.ts index fbb1d0f52..61f006286 100644 --- a/src/core/classes/tabs.ts +++ b/src/core/classes/tabs.ts @@ -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 implements OnInit, AfterViewInit, OnChanges, OnDestroy { +export class CoreTabsBaseComponent 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 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 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 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 = {}; // Scroll elements for each loaded tab. - protected lastScroll = 0; - protected previousLastScroll = 0; + protected subscriptions: Subscription[] = []; + protected onReadyPromise = new CorePromisedValue(); tabAction: CoreTabsRoleTab; - 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 { 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 { + 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 implements OnInit, Aft */ // eslint-disable-next-line @typescript-eslint/no-unused-vars ngOnChanges(changes: Record): 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 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 implements OnInit, Aft } /** - * Initialize the tabs, determining the first tab to be shown. + * Init the component. */ - protected async initializeTabs(): Promise { - // 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 { + 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 { + 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 = 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 { + 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 implements OnInit, Aft * Method executed when the slides are changed. */ async slideChanged(): Promise { - if (!this.slidesSwiperLoaded) { + if (!this.slidesElement) { return; } @@ -368,19 +355,15 @@ export class CoreTabsBaseComponent implements OnInit, Aft * Updates the number of slides to show. */ protected async updateSlides(): Promise { - 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 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 implements OnInit, Aft * Calculate the number of slides that can fit on the screen. */ protected async calculateMaxSlides(): Promise { - 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 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 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 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 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 { - 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): void => { - this.showHideTabs(e.detail.scrollTop, scroll); - }); + async ready(): Promise { + 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 extends CoreAriaRoleTab tab.enabled).map((tab) => ({ - id: tab.id!, - findIndex: tab.id!, + id: tab.id || '', + findIndex: tab.id || '', })); } diff --git a/src/core/components/iframe/iframe.scss b/src/core/components/iframe/iframe.scss index bc8a94691..e71baa682 100644 --- a/src/core/components/iframe/iframe.scss +++ b/src/core/components/iframe/iframe.scss @@ -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; +} diff --git a/src/core/components/loading/loading.scss b/src/core/components/loading/loading.scss index 82ab20289..d6b31d067 100644 --- a/src/core/components/loading/loading.scss +++ b/src/core/components/loading/loading.scss @@ -93,4 +93,5 @@ :host-context(.limited-width > ):not([slot]) { --contents-display: flex; flex-direction: column; + min-height: 100%; } diff --git a/src/core/components/show-password/show-password.scss b/src/core/components/show-password/show-password.scss index b5feb9e38..07280df85 100644 --- a/src/core/components/show-password/show-password.scss +++ b/src/core/components/show-password/show-password.scss @@ -11,6 +11,7 @@ margin-top: 0; margin-bottom: 0; z-index: 3; + bottom: 0; } } diff --git a/src/core/components/tabs-outlet/core-tabs-outlet.html b/src/core/components/tabs-outlet/core-tabs-outlet.html index 7ef200d94..ff4c3f825 100644 --- a/src/core/components/tabs-outlet/core-tabs-outlet.html +++ b/src/core/components/tabs-outlet/core-tabs-outlet.html @@ -1,38 +1,36 @@ - + - - - - - - - - - - - - {{ tab.title | translate}} - - {{ tab.badge }} - - {{ tab.badgeA11yText | translate: {$a : tab.badge } }} - - - - - - - - - - - - + + + + + + + + + + + {{ tab.title | translate}} + + {{ tab.badge }} + + {{ tab.badgeA11yText | translate: {$a : tab.badge } }} + + + + + + + + + + + diff --git a/src/core/components/tabs-outlet/tabs-outlet.ts b/src/core/components/tabs-outlet/tabs-outlet.ts index 4a75166a9..279ddde43 100644 --- a/src/core/components/tabs-outlet/tabs-outlet.ts +++ b/src/core/components/tabs-outlet/tabs-outlet.ts @@ -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; 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 { if (!this.isCurrentView) { return; @@ -118,14 +110,6 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent { this.lastActiveComponent = this.ionTabs.outlet.component; @@ -232,7 +216,7 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent + - - - - - - - - - - - - {{ tab.title | translate}} - - {{ tab.badge }} - - {{ tab.badgeA11yText | translate: {$a : tab.badge } }} - - - - - - - - - - - - + + + + + + + + + + + {{ tab.title | translate}} + + {{ tab.badge }} + + {{ tab.badgeA11yText | translate: {$a : tab.badge } }} + + + + + + + + + + +
diff --git a/src/core/components/tabs/tab.ts b/src/core/components/tabs/tab.ts index 2d3166fd5..c98e10472 100644 --- a/src/core/components/tabs/tab.ts +++ b/src/core/components/tabs/tab.ts @@ -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 = new EventEmitter(); @ContentChild(TemplateRef) template?: TemplateRef; // 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!); } /** diff --git a/src/core/components/tabs/tabs.scss b/src/core/components/tabs/tabs.scss index 04a50ba1b..69d09150f 100644 --- a/src/core/components/tabs/tabs.scss +++ b/src/core/components/tabs/tabs.scss @@ -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; -} diff --git a/src/core/components/tabs/tabs.ts b/src/core/components/tabs/tabs.ts index a0fef9e06..a603c3ef8 100644 --- a/src/core/components/tabs/tabs.ts +++ b/src/core/components/tabs/tabs.ts @@ -51,12 +51,6 @@ export class CoreTabsComponent extends CoreTabsBaseComponent 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 i return; } - this.tabsElement = this.element.nativeElement; this.originalTabsContainer = this.originalTabsRef?.nativeElement; } @@ -85,21 +78,13 @@ export class CoreTabsComponent extends CoreTabsBaseComponent 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 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(); diff --git a/src/core/directives/collapsible-footer.ts b/src/core/directives/collapsible-footer.ts index 7ead926a9..f0b7c90ad 100644 --- a/src/core/directives/collapsible-footer.ts +++ b/src/core/directives/collapsible-footer.ts @@ -50,6 +50,9 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { protected endContentScrollListener?: EventListener; protected resizeListener?: CoreEventObserver; protected slotPromise?: CoreCancellablePromise; + 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 { + 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('.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(); diff --git a/src/core/directives/collapsible-header.ts b/src/core/directives/collapsible-header.ts index 1cb15b1d6..5bb3d05cf 100644 --- a/src/core/directives/collapsible-header.ts +++ b/src/core/directives/collapsible-header.ts @@ -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(); 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 { + 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, + ); } /** diff --git a/src/core/directives/collapsible-item.ts b/src/core/directives/collapsible-item.ts index 3bac8a97c..6e95242f2 100644 --- a/src/core/directives/collapsible-item.ts +++ b/src/core/directives/collapsible-item.ts @@ -56,6 +56,9 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { protected darkModeListener?: Subscription; protected domPromise?: CoreCancellablePromise; protected uniqueId: string; + protected calcPending = false; + protected pageDidEnterListener?: EventListener; + protected page?: HTMLElement; constructor(el: ElementRef) { 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('.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); + } } } diff --git a/src/core/features/course/components/course-format/course-format.scss b/src/core/features/course/components/course-format/course-format.scss index 869e69a96..da38b60b6 100644 --- a/src/core/features/course/components/course-format/course-format.scss +++ b/src/core/features/course/components/course-format/course-format.scss @@ -10,6 +10,8 @@ overflow: hidden; text-transform: none; flex: 1; + margin-left: 4px; + margin-right: 4px; } } diff --git a/src/core/features/course/components/course-index/course-index.scss b/src/core/features/course/components/course-index/course-index.scss index f4575ba22..7804c82f3 100644 --- a/src/core/features/course/components/course-index/course-index.scss +++ b/src/core/features/course/components/course-index/course-index.scss @@ -74,7 +74,7 @@ ion-item.item { } &.restricted { - font-size: 14px; + font-size: var(--text-size); } } } diff --git a/src/core/features/course/components/module-info/course-module-info.scss b/src/core/features/course/components/module-info/course-module-info.scss index 8b6c8feb0..d6aa6ad69 100644 --- a/src/core/features/course/components/module-info/course-module-info.scss +++ b/src/core/features/course/components/module-info/course-module-info.scss @@ -29,6 +29,10 @@ margin: 8px; padding: 8px; + &:empty { + display: none; + } + ::ng-deep ion-item { --ion-item-background: var(--light); --background: var(--light); diff --git a/src/core/features/course/components/module-navigation/core-course-module-navigation.html b/src/core/features/course/components/module-navigation/core-course-module-navigation.html index 5ca66de46..67afb3dc5 100644 --- a/src/core/features/course/components/module-navigation/core-course-module-navigation.html +++ b/src/core/features/course/components/module-navigation/core-course-module-navigation.html @@ -1,17 +1,16 @@ - - - - - {{ 'core.previous' | translate }} + + + + +
{{ 'core.course.previousactivity' | translate }}
- - - {{ 'core.next' | translate }} - + + +
{{ 'core.course.nextactivity' | translate }}
+
diff --git a/src/core/features/course/components/module-navigation/module-navigation.scss b/src/core/features/course/components/module-navigation/module-navigation.scss index 4b4eb2a8c..6b8ce25f6 100644 --- a/src/core/features/course/components/module-navigation/module-navigation.scss +++ b/src/core/features/course/components/module-navigation/module-navigation.scss @@ -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); +} diff --git a/src/core/features/course/components/module-navigation/module-navigation.ts b/src/core/features/course/components/module-navigation/module-navigation.ts index 63211dbc2..cf1327ebd 100644 --- a/src/core/features/course/components/module-navigation/module-navigation.ts +++ b/src/core/features/course/components/module-navigation/module-navigation.ts @@ -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, ); diff --git a/src/core/features/course/components/module/module.scss b/src/core/features/course/components/module/module.scss index 2ef99974b..3485350cf 100644 --- a/src/core/features/course/components/module/module.scss +++ b/src/core/features/course/components/module/module.scss @@ -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); diff --git a/src/core/features/course/lang.json b/src/core/features/course/lang.json index 1103a9902..8195622c0 100644 --- a/src/core/features/course/lang.json +++ b/src/core/features/course/lang.json @@ -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 at least {{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 at least {{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", diff --git a/src/core/features/course/pages/index/index.ts b/src/core/features/course/pages/index/index.ts index fdd5e47c8..462a97c56 100644 --- a/src/core/features/course/pages/index/index.ts +++ b/src/core/features/course/pages/index/index.ts @@ -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); } } diff --git a/src/core/features/grades/pages/course/course.scss b/src/core/features/grades/pages/course/course.scss index bed25f18e..fff8ae719 100644 --- a/src/core/features/grades/pages/course/course.scss +++ b/src/core/features/grades/pages/course/course.scss @@ -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); diff --git a/src/core/singletons/components-registry.ts b/src/core/singletons/components-registry.ts index 32b08e3dd..0a5e18cea 100644 --- a/src/core/singletons/components-registry.ts +++ b/src/core/singletons/components-registry.ts @@ -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 = new WeakMap(); + protected static logger = CoreLogger.getInstance('CoreComponentsRegistry'); /** * Register a component instance. @@ -78,6 +80,8 @@ export class CoreComponentsRegistry { ): Promise { 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, ): Promise { + let elements: Element[] = []; + if (element.matches(selector)) { // Element to wait is myself. - await CoreComponentsRegistry.waitComponentReady(element, componentClass); + elements = [element]; } else { - await Promise.all(Array - .from(element.querySelectorAll(selector)) - .map(element => CoreComponentsRegistry.waitComponentReady(element, componentClass))); + elements = Array.from(element.querySelectorAll(selector)); } + if (!elements.length) { + return; + } + + await Promise.all(elements.map(element => CoreComponentsRegistry.waitComponentReady(element, componentClass))); + // Wait for next tick to ensure components are completely rendered. await CoreUtils.nextTick(); } diff --git a/src/theme/components/collapsible-header.scss b/src/theme/components/collapsible-header.scss index 04304a443..eed43f951 100644 --- a/src/theme/components/collapsible-header.scss +++ b/src/theme/components/collapsible-header.scss @@ -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 { diff --git a/src/theme/components/collapsible-item.scss b/src/theme/components/collapsible-item.scss index a40ea19c4..616bcb02e 100644 --- a/src/theme/components/collapsible-item.scss +++ b/src/theme/components/collapsible-item.scss @@ -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; diff --git a/src/theme/components/format-text.scss b/src/theme/components/format-text.scss index 808d38dbc..d2a47fe51 100644 --- a/src/theme/components/format-text.scss +++ b/src/theme/components/format-text.scss @@ -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 { diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index a97d2eea2..b10ac0d47 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -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. diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss index 21e9485e1..14da0eacf 100644 --- a/src/theme/theme.light.scss +++ b/src/theme/theme.light.scss @@ -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);