Merge pull request #3183 from crazyserver/MOBILE-3814

Mobile 3814
main
Noel De Martin 2022-03-16 14:04:07 +01:00 committed by GitHub
commit 52f0399b99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 431 additions and 281 deletions

View File

@ -1,29 +1,39 @@
@import "~theme/globals"; @import "~theme/globals";
:host { :host {
::ng-deep { --extra-icon-size: 16px;
core-user-avatar .core-avatar-extra {
::ng-deep core-user-avatar {
.core-avatar-extra-img,
core-mod-icon {
margin: 0 !important; margin: 0 !important;
position: absolute; position: absolute;
right: -4px; right: -4px;
bottom: -4px; bottom: -4px;
}
core-user-avatar img.core-avatar-extra {
background: none;
width: 24px;
height: 24px;
border-radius: 0 !important;
}
core-user-avatar core-mod-icon.core-avatar-extra {
--size: 16px;
padding: 0.2rem; padding: 0.2rem;
} }
.core-avatar-extra-img {
background: var(--background-color);
border-radius: var(--medium-radius);
img {
max-width: var(--extra-icon-size);
max-height: var(--extra-icon-size);
display: block;
}
}
core-mod-icon {
--size: var(--extra-icon-size);
}
} }
.core-notification-icon { .core-notification-icon {
width: 34px; width: var(--core-avatar-size);
height: 34px; height: var(--core-avatar-size);
margin: 10px !important; @include margin(6px, 8px, 6px, 0px);
background: var(--background-color);
border-radius: var(--small-radius);
} }
.item core-format-text ::ng-deep { .item core-format-text ::ng-deep {

View File

@ -36,10 +36,11 @@
<core-user-avatar *ngIf="notification.useridfrom > 0" [user]="notification" slot="start" <core-user-avatar *ngIf="notification.useridfrom > 0" [user]="notification" slot="start"
[profileUrl]="notification.profileimageurlfrom" [fullname]="notification.userfromfullname" [profileUrl]="notification.profileimageurlfrom" [fullname]="notification.userfromfullname"
[userId]="notification.useridfrom"> [userId]="notification.useridfrom">
<img *ngIf="notification.iconurl && !notification.modname" [src]="notification.iconurl" alt="" role="presentation" <div class="core-avatar-extra-img" *ngIf="notification.iconurl && !notification.modname">
class="core-avatar-extra"> <img [src]="notification.iconurl" alt="" role="presentation">
</div>
<core-mod-icon *ngIf="notification.modname" [modicon]="notification.iconurl" [modname]="notification.modname" <core-mod-icon *ngIf="notification.modname" [modicon]="notification.iconurl" [modname]="notification.modname"
[showAlt]="false" class="core-avatar-extra"> [showAlt]="false">
</core-mod-icon> </core-mod-icon>
</core-user-avatar> </core-user-avatar>

View File

@ -13,8 +13,10 @@
<ion-item class="ion-text-wrap" [attr.aria-label]="subject"> <ion-item class="ion-text-wrap" [attr.aria-label]="subject">
<core-user-avatar *ngIf="userIdFrom > 0" slot="start" [userId]="userIdFrom" [profileUrl]="profileImageUrlFrom" <core-user-avatar *ngIf="userIdFrom > 0" slot="start" [userId]="userIdFrom" [profileUrl]="profileImageUrlFrom"
[fullname]="userFromFullName"> [fullname]="userFromFullName">
<img *ngIf="iconUrl && !modname" [src]="iconUrl" alt="" role="presentation" class="core-avatar-extra"> <div class="core-avatar-extra-img" *ngIf="iconUrl && !modname">
<core-mod-icon *ngIf="modname" [modicon]="iconUrl" [modname]="modname" [showAlt]="false" class="core-avatar-extra"> <img [src]="iconUrl" alt="" role="presentation">
</div>
<core-mod-icon *ngIf="modname" [modicon]="iconUrl" [modname]="modname" [showAlt]="false">
</core-mod-icon> </core-mod-icon>
</core-user-avatar> </core-user-avatar>

View File

@ -0,0 +1,24 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* Component that is not rendered immediately after being mounted.
*/
export interface AsyncComponent {
/**
* Wait until the component is fully rendered and ready.
*/
ready(): Promise<void>;
}

View File

@ -20,6 +20,7 @@ import { CoreAnimations } from '@components/animations';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CorePromisedValue } from '@classes/promised-value'; import { CorePromisedValue } from '@classes/promised-value';
import { AsyncComponent } from '@classes/async-component';
/** /**
* Component to show a loading spinner and message while data is being loaded. * Component to show a loading spinner and message while data is being loaded.
@ -47,7 +48,7 @@ import { CorePromisedValue } from '@classes/promised-value';
styleUrls: ['loading.scss'], styleUrls: ['loading.scss'],
animations: [CoreAnimations.SHOW_HIDE], animations: [CoreAnimations.SHOW_HIDE],
}) })
export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit { export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, AsyncComponent {
@Input() hideUntil: unknown; // Determine when should the contents be shown. @Input() hideUntil: unknown; // Determine when should the contents be shown.
@Input() message?: string; // Message to show while loading. @Input() message?: string; // Message to show while loading.
@ -58,7 +59,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
uniqueId: string; uniqueId: string;
protected element: HTMLElement; // Current element. protected element: HTMLElement; // Current element.
loaded = false; // Only comes true once. loaded = false; // Only comes true once.
protected firstLoadedPromise = new CorePromisedValue<string>(); protected onReadyPromise = new CorePromisedValue<void>();
constructor(element: ElementRef) { constructor(element: ElementRef) {
this.element = element.nativeElement; this.element = element.nativeElement;
@ -108,7 +109,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
if (!this.loaded && loaded) { if (!this.loaded && loaded) {
this.loaded = true; // Only comes true once. this.loaded = true; // Only comes true once.
this.firstLoadedPromise.resolve(this.uniqueId); this.onReadyPromise.resolve();
} }
// Event has been deprecated since app 4.0. // Event has been deprecated since app 4.0.
@ -119,12 +120,10 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
} }
/** /**
* Wait the loading to finish. * @inheritdoc
*
* @return Promise resolved with the uniqueId when done.
*/ */
async whenLoaded(): Promise<string> { async ready(): Promise<void> {
return await this.firstLoadedPromise; return await this.onReadyPromise;
} }
} }

View File

@ -9,7 +9,7 @@
</core-progress-bar> </core-progress-bar>
</ion-col> </ion-col>
<ion-col class="ion-text-end ion-no-padding core-navigation-arrow" size="auto"> <ion-col class="ion-text-end ion-no-padding core-navigation-arrow" size="auto">
<ion-button [disabled]="nextIndex < 0" fill="clear" [attr.aria-label]="nextTitle" (click)="navigate(nextIndex)"> <ion-button [disabled]="nextIndex >= items.length" fill="clear" [attr.aria-label]="nextTitle" (click)="navigate(nextIndex)">
<ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon> <ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
</ion-col> </ion-col>

View File

@ -67,15 +67,21 @@ export class CoreNavigationBarComponent implements OnChanges {
this.progress = ((this.currentIndex + 1) / this.items.length) * 100; this.progress = ((this.currentIndex + 1) / this.items.length) * 100;
this.progressText = `${this.currentIndex + 1} / ${this.items.length}`; this.progressText = `${this.currentIndex + 1} / ${this.items.length}`;
this.nextIndex = this.items[this.currentIndex + 1]?.enabled ? this.currentIndex + 1 : -1; this.nextIndex = this.currentIndex + 1;
if (this.nextIndex >= 0) { while (this.items[this.nextIndex] && !this.items[this.nextIndex].enabled) {
this.nextTitle = Translate.instant(this.nextTranslate, { $a: this.items[this.nextIndex].title || '' }); this.nextIndex++;
} }
this.nextTitle = this.items[this.nextIndex]
? Translate.instant(this.nextTranslate, { $a: this.items[this.nextIndex].title || '' })
: '';
this.previousIndex = this.items[this.currentIndex - 1]?.enabled ? this.currentIndex - 1 : -1; this.previousIndex = this.currentIndex - 1;
if (this.previousIndex >= 0) { while (this.items[this.previousIndex] && !this.items[this.previousIndex].enabled) {
this.previousTitle = Translate.instant(this.previousTranslate, { $a: this.items[this.previousIndex].title || '' }); this.previousIndex--;
} }
this.previousTitle = this.items[this.previousIndex]
? Translate.instant(this.previousTranslate, { $a: this.items[this.previousIndex].title || '' })
: '';
} }
/** /**

View File

@ -78,8 +78,6 @@
} }
:host-context(ion-item) { :host-context(ion-item) {
margin-top: 6px;
margin-bottom: 6px;
@include margin(6px, 8px, 6px, 0px); @include margin(6px, 8px, 6px, 0px);
img { img {
min-width: var(--core-avatar-size); min-width: var(--core-avatar-size);

View File

@ -66,7 +66,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
await this.domPromise; await this.domPromise;
await this.waitLoadingsDone(); await this.waitLoadingsDone();
await this.waitFormatTextsRendered(this.element); await this.waitFormatTextsRendered();
this.content = this.element.closest('ion-content'); this.content = this.element.closest('ion-content');
@ -156,15 +156,9 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
/** /**
* Wait until all <core-format-text> children inside the element are done rendering. * Wait until all <core-format-text> children inside the element are done rendering.
*
* @param element Element.
*/ */
protected async waitFormatTextsRendered(element: Element): Promise<void> { protected async waitFormatTextsRendered(): Promise<void> {
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreFormatTextDirective>( await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective);
element,
'core-format-text',
'rendered',
);
} }
/** /**
@ -210,13 +204,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
const scrollElement = await this.ionContent.getScrollElement(); const scrollElement = await this.ionContent.getScrollElement();
await Promise.all([ await Promise.all([
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent> await CoreComponentsRegistry.waitComponentsReady(scrollElement, 'core-loading', CoreLoadingComponent),
(scrollElement, 'core-loading', 'whenLoaded'), await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-loading', CoreLoadingComponent),
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(
this.element,
'core-loading',
'whenLoaded',
),
]); ]);
} }

View File

@ -76,6 +76,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
protected enabled = true; protected enabled = true;
protected isWithinContent = false; protected isWithinContent = false;
protected enteredPromise = new CorePromisedValue<void>(); protected enteredPromise = new CorePromisedValue<void>();
protected mutationObserver?: MutationObserver;
constructor(el: ElementRef) { constructor(el: ElementRef) {
this.collapsedHeader = el.nativeElement; this.collapsedHeader = el.nativeElement;
@ -126,6 +127,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
} }
this.resizeListener?.off(); this.resizeListener?.off();
this.mutationObserver?.disconnect();
} }
/** /**
@ -161,6 +163,31 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
this.subscriptions.push(CoreSettingsHelper.onDarkModeChange().subscribe(() => { this.subscriptions.push(CoreSettingsHelper.onDarkModeChange().subscribe(() => {
this.initializeFloatingTitle(); this.initializeFloatingTitle();
})); }));
this.mutationObserver = new MutationObserver(() => {
if (!this.expandedHeader) {
return;
}
const originalTitle = this.expandedHeader.querySelector('h1.collapsible-header-original-title') ||
this.expandedHeader.querySelector('h1') as HTMLHeadingElement;
const floatingTitleWrapper = originalTitle.parentElement as HTMLElement;
const floatingTitle = floatingTitleWrapper.querySelector('.collapsible-header-floating-title') as HTMLHeadingElement;
if (!floatingTitle || !originalTitle) {
return;
}
// Original title changed, change the contents.
const newFloatingTitle = originalTitle.cloneNode(true) as HTMLHeadingElement;
newFloatingTitle.classList.add('collapsible-header-floating-title');
newFloatingTitle.classList.remove('collapsible-header-original-title');
floatingTitleWrapper.replaceChild(newFloatingTitle, floatingTitle);
this.initializeFloatingTitle();
});
} }
/** /**
@ -247,6 +274,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
floatingTitleWrapper.insertBefore(floatingTitle, originalTitle); floatingTitleWrapper.insertBefore(floatingTitle, originalTitle);
originalTitle.classList.add('collapsible-header-original-title'); originalTitle.classList.add('collapsible-header-original-title');
this.mutationObserver?.observe(originalTitle, { childList: true, subtree: true });
} }
const floatingTitleBoundingBox = floatingTitle.getBoundingClientRect(); const floatingTitleBoundingBox = floatingTitle.getBoundingClientRect();
@ -304,15 +332,13 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
/** /**
* Wait until all <core-loading> children inside the page. * Wait until all <core-loading> children inside the page.
*
* @return Promise resolved when loadings are done.
*/ */
protected async waitLoadingsDone(): Promise<void> { protected async waitLoadingsDone(): Promise<void> {
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>( if (!this.page) {
this.page, return;
'core-loading', }
'whenLoaded',
); await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-loading', CoreLoadingComponent);
} }
/** /**
@ -322,11 +348,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
* @return Promise resolved when texts are rendered. * @return Promise resolved when texts are rendered.
*/ */
protected async waitFormatTextsRendered(element: Element): Promise<void> { protected async waitFormatTextsRendered(element: Element): Promise<void> {
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreFormatTextDirective>( await CoreComponentsRegistry.waitComponentsReady(element, 'core-format-text', CoreFormatTextDirective);
element,
'core-format-text',
'rendered',
);
} }
/** /**

View File

@ -16,7 +16,6 @@ import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreLoadingComponent } from '@components/loading/loading';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreEventObserver } from '@singletons/events'; import { CoreEventObserver } from '@singletons/events';
@ -103,28 +102,18 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
const page = this.element.closest('.ion-page'); const page = this.element.closest('.ion-page');
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(page, 'core-loading', 'whenLoaded'); if (!page) {
return;
}
await CoreComponentsRegistry.waitComponentsReady(page, 'core-loading', CoreLoadingComponent);
} }
/** /**
* Wait until all <core-format-text> children inside the element are done rendering. * Wait until all <core-format-text> children inside the element are done rendering.
*
* @param element Element.
*/ */
protected async waitFormatTextsRendered(element: Element): Promise<void> { protected async waitFormatTextsRendered(): Promise<void> {
let formatTextElements: HTMLElement[] = []; await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective);
if (this.element.tagName == 'CORE-FORMAT-TEXT') {
formatTextElements = [this.element];
} else {
formatTextElements = Array.from(element.querySelectorAll('core-format-text'));
}
const formatTexts = formatTextElements.map(element => CoreComponentsRegistry.resolve(element, CoreFormatTextDirective));
await Promise.all(formatTexts.map(formatText => formatText?.rendered()));
await CoreUtils.nextTick();
} }
/** /**
@ -134,7 +123,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
// Remove max-height (if any) to calculate the real height. // Remove max-height (if any) to calculate the real height.
this.element.classList.add('collapsible-loading-height'); this.element.classList.add('collapsible-loading-height');
await this.waitFormatTextsRendered(this.element); await this.waitFormatTextsRendered();
this.expandedHeight = this.element.getBoundingClientRect().height; this.expandedHeight = this.element.getBoundingClientRect().height;

View File

@ -33,6 +33,7 @@ import { CoreCollapsibleItemDirective } from './collapsible-item';
import { CoreCollapsibleFooterDirective } from './collapsible-footer'; import { CoreCollapsibleFooterDirective } from './collapsible-footer';
import { CoreContentDirective } from './content'; import { CoreContentDirective } from './content';
import { CoreOnAppearDirective } from './on-appear'; import { CoreOnAppearDirective } from './on-appear';
import { CoreUpdateNonReactiveAttributesDirective } from './update-non-reactive-attributes';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -55,6 +56,7 @@ import { CoreOnAppearDirective } from './on-appear';
CoreCollapsibleItemDirective, CoreCollapsibleItemDirective,
CoreCollapsibleFooterDirective, CoreCollapsibleFooterDirective,
CoreContentDirective, CoreContentDirective,
CoreUpdateNonReactiveAttributesDirective,
], ],
exports: [ exports: [
CoreAutoFocusDirective, CoreAutoFocusDirective,
@ -76,6 +78,7 @@ import { CoreOnAppearDirective } from './on-appear';
CoreCollapsibleItemDirective, CoreCollapsibleItemDirective,
CoreCollapsibleFooterDirective, CoreCollapsibleFooterDirective,
CoreContentDirective, CoreContentDirective,
CoreUpdateNonReactiveAttributesDirective,
], ],
}) })
export class CoreDirectivesModule {} export class CoreDirectivesModule {}

View File

@ -43,6 +43,7 @@ import { CoreSubscriptions } from '@singletons/subscriptions';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreCollapsibleItemDirective } from './collapsible-item'; import { CoreCollapsibleItemDirective } from './collapsible-item';
import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { AsyncComponent } from '@classes/async-component';
/** /**
* Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective
@ -56,7 +57,7 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise';
@Directive({ @Directive({
selector: 'core-format-text', selector: 'core-format-text',
}) })
export class CoreFormatTextDirective implements OnChanges, OnDestroy { export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncComponent {
@ViewChild(CoreCollapsibleItemDirective) collapsible?: CoreCollapsibleItemDirective; @ViewChild(CoreCollapsibleItemDirective) collapsible?: CoreCollapsibleItemDirective;
@ -137,9 +138,9 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy {
} }
/** /**
* Wait until the text is fully rendered. * @inheritdoc
*/ */
async rendered(): Promise<void> { async ready(): Promise<void> {
if (!this.element.classList.contains('core-format-text-loading')) { if (!this.element.classList.contains('core-format-text-loading')) {
return; return;
} }

View File

@ -27,7 +27,7 @@ export class CoreOnAppearDirective implements OnInit, OnDestroy {
@Output() onAppear = new EventEmitter(); @Output() onAppear = new EventEmitter();
private element: HTMLElement; private element: HTMLElement;
protected domPromise?: CoreCancellablePromise<void>; protected visiblePromise?: CoreCancellablePromise<void>;
constructor(element: ElementRef) { constructor(element: ElementRef) {
this.element = element.nativeElement; this.element = element.nativeElement;
@ -37,9 +37,9 @@ export class CoreOnAppearDirective implements OnInit, OnDestroy {
* @inheritdoc * @inheritdoc
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.domPromise = CoreDomUtils.waitToBeInDOM(this.element); this.visiblePromise = CoreDomUtils.waitToBeVisible(this.element);
await this.domPromise; await this.visiblePromise;
this.onAppear.emit(); this.onAppear.emit();
} }
@ -48,7 +48,7 @@ export class CoreOnAppearDirective implements OnInit, OnDestroy {
* @inheritdoc * @inheritdoc
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.domPromise?.cancel(); this.visiblePromise?.cancel();
} }
} }

View File

@ -0,0 +1,68 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core';
/**
* Directive to observe mutations on some attributes and propagate them inside.
* Current supported attributes: ion-button.aria-label
*
* This is necessary in order to update some attributes that are not reactive, for example aria-label.
*
* @see https://github.com/ionic-team/ionic-framework/issues/21534
*/
@Directive({
selector: 'ion-button',
})
export class CoreUpdateNonReactiveAttributesDirective implements OnInit, OnDestroy {
protected element: HTMLIonButtonElement;
protected mutationObserver: MutationObserver;
constructor(element: ElementRef<HTMLIonButtonElement>) {
this.element = element.nativeElement;
this.mutationObserver = new MutationObserver(() => {
const ariaLabel = this.element.getAttribute('aria-label');
if (!ariaLabel) {
// Aria label unset by ionButton component (when first created).
return;
}
// Propagate label to button.
const button = this.element.shadowRoot?.querySelector('button');
button?.setAttribute('aria-label', ariaLabel);
});
}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
await this.element.componentOnReady();
if (!this.element.getAttribute('aria-label')) {
return;
}
this.mutationObserver.observe(this.element, { attributes: true, attributeFilter: ['aria-label'] });
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.mutationObserver.disconnect();
}
}

View File

@ -24,7 +24,7 @@
<core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)"></core-infinite-loading> <core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)"></core-infinite-loading>
</div> </div>
<div collapsible-footer appearOnBottom *ngIf="displayCourseIndex && hasPreviousOrNextSections" slot="fixed"> <div collapsible-footer appearOnBottom *ngIf="displayCourseIndex && (previousSection || nextSection)" slot="fixed">
<div class="core-course-section-nav-buttons safe-area-padding-horizontal list-item-limited-width"> <div class="core-course-section-nav-buttons safe-area-padding-horizontal list-item-limited-width">
<ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" expand="block" <ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" expand="block"
[attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name"> [attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name">

View File

@ -92,7 +92,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
selectedSection?: CoreCourseSection; selectedSection?: CoreCourseSection;
previousSection?: CoreCourseSection; previousSection?: CoreCourseSection;
nextSection?: CoreCourseSection; nextSection?: CoreCourseSection;
hasPreviousOrNextSections = false;
allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID; allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID;
stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID; stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
loaded = false; loaded = false;
@ -489,8 +488,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.showMoreActivities(); this.showMoreActivities();
} }
this.hasPreviousOrNextSections = !!this.previousSection || !!this.nextSection;
// Scroll to module if needed. Give more priority to the input. // Scroll to module if needed. Give more priority to the input.
const moduleIdToScroll = this.moduleId && previousValue === undefined ? this.moduleId : moduleId; const moduleIdToScroll = this.moduleId && previousValue === undefined ? this.moduleId : moduleId;
if (moduleIdToScroll) { if (moduleIdToScroll) {
@ -507,8 +504,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
CoreCourse.logView(this.course.id, newSection.section, undefined, this.course.fullname), CoreCourse.logView(this.course.id, newSection.section, undefined, this.course.fullname),
); );
} }
this.invalidateSectionButtons();
} }
/** /**
@ -563,25 +558,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
done?.(); done?.();
} }
/**
* Invalidate section buttons so that they are rendered again. This is necessary in order to update
* some attributes that are not reactive, for example aria-label.
*
* @see https://github.com/ionic-team/ionic-framework/issues/21534
*/
protected async invalidateSectionButtons(): Promise<void> {
const previousSection = this.previousSection;
const nextSection = this.nextSection;
this.previousSection = undefined;
this.nextSection = undefined;
await CoreUtils.nextTick();
this.previousSection = previousSection;
this.nextSection = nextSection;
}
/** /**
* Show more activities (only used when showing all the sections at the same time). * Show more activities (only used when showing all the sections at the same time).
* *

View File

@ -18,6 +18,7 @@ ion-item.item {
&.item-current { &.item-current {
--background: var(--primary-tint); --background: var(--primary-tint);
--color: var(--gray-900);
border: 0; border: 0;
} }
@ -34,6 +35,7 @@ ion-item.item {
&:hover { &:hover {
background: var(--primary-shade); background: var(--primary-shade);
color: var(--gray-100);
} }
} }
} }
@ -43,7 +45,7 @@ ion-item.item {
margin: 3px; margin: 3px;
border-radius: 50%; border-radius: 50%;
&:hover { &:hover {
background: var(--gray-300); background: var(--secondary);
} }
} }

View File

@ -23,6 +23,7 @@
</ion-icon> </ion-icon>
</div> </div>
<div *ngIf="course" class="course-container"> <div *ngIf="course" class="course-container">
<div class="list-item-limited-width">
<ion-item class="ion-text-wrap course-name"> <ion-item class="ion-text-wrap course-name">
<ion-label> <ion-label>
<p *ngIf="course.displayname && course.shortname && course.fullname != course.displayname" <p *ngIf="course.displayname && course.shortname && course.fullname != course.displayname"
@ -38,7 +39,8 @@
<ion-chip color="primary" *ngIf="course.categoryname" class="core-course-category ion-text-nowrap"> <ion-chip color="primary" *ngIf="course.categoryname" class="core-course-category ion-text-nowrap">
<span class="sr-only">{{ 'core.courses.aria:coursecategory' | translate }}</span> <span class="sr-only">{{ 'core.courses.aria:coursecategory' | translate }}</span>
<ion-label> <ion-label>
<core-format-text [text]="course.categoryname" contextLevel="coursecat" [contextInstanceId]="course.categoryid"> <core-format-text [text]="course.categoryname" contextLevel="coursecat"
[contextInstanceId]="course.categoryid">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
</ion-chip> </ion-chip>
@ -125,10 +127,12 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
</div> </div>
</div>
</core-loading> </core-loading>
</ion-content> </ion-content>
<ion-footer *ngIf="course && dataLoaded"> <ion-footer *ngIf="course && dataLoaded">
<div class="list-item-limited-width">
<ng-container *ngIf="canAccessCourse"> <ng-container *ngIf="canAccessCourse">
<ion-button *ngFor="let item of courseMenuHandlers" (click)="openMenuItem(item)" [class]="'ion-text-wrap '+ item.data.class" <ion-button *ngFor="let item of courseMenuHandlers" (click)="openMenuItem(item)" [class]="'ion-text-wrap '+ item.data.class"
expand="block"> expand="block">
@ -153,4 +157,5 @@
<ion-icon name="fas-eye" slot="start" aria-hidden="true"></ion-icon> <ion-icon name="fas-eye" slot="start" aria-hidden="true"></ion-icon>
{{ 'core.course.viewcourse' | translate }} {{ 'core.course.viewcourse' | translate }}
</ion-button> </ion-button>
</div>
</ion-footer> </ion-footer>

View File

@ -39,11 +39,14 @@
.course-container { .course-container {
position: relative; position: relative;
top: calc(var(--thumb-height) - var(--big-radius)); top: calc(var(--thumb-height) - var(--big-radius));
border-radius: var(--big-radius) var(--big-radius) 0 0;
background-color: var(--ion-background-color);
box-shadow: var(--drop-shadow-top);
clip-path: inset(-5px 0px 0px 0px);
} }
.course-name { .course-name {
border-radius: var(--big-radius) var(--big-radius) 0 0; --background: transparent;
box-shadow: var(--drop-shadow-top);
ion-label { ion-label {
margin-bottom: 0px; margin-bottom: 0px;
} }

View File

@ -16,6 +16,7 @@
</ion-buttons> </ion-buttons>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<div class="core-course-header">
<ion-item class="core-format-progress-list ion-text-wrap list-item-limited-width" collapsible> <ion-item class="core-format-progress-list ion-text-wrap list-item-limited-width" collapsible>
<ng-container *ngIf="course"> <ng-container *ngIf="course">
<div *ngIf="!course.courseImage" #courseThumb slot="start" class="core-course-thumb"> <div *ngIf="!course.courseImage" #courseThumb slot="start" class="core-course-thumb">
@ -34,6 +35,6 @@
</core-progress-bar> </core-progress-bar>
</div> </div>
</ion-label> </ion-label>
</ion-item> </ion-item>
</div>
<core-tabs-outlet [tabs]="tabs" [hideUntil]="loaded" (ionChange)="tabSelected()"></core-tabs-outlet> <core-tabs-outlet [tabs]="tabs" [hideUntil]="loaded" (ionChange)="tabSelected()"></core-tabs-outlet>

View File

@ -44,4 +44,8 @@
h1 { h1 {
font-size: 20px; font-size: 20px;
} }
.core-course-header {
background: var(--ion-item-background);
}
} }

View File

@ -1064,15 +1064,13 @@ export class CoreCourseHelperProvider {
* *
* @param courses Courses array to get info from. * @param courses Courses array to get info from.
* @param prefetch Prefetch information. * @param prefetch Prefetch information.
* @param minCourses Min course to show icon.
* @return Resolved with the prefetch information updated when done. * @return Resolved with the prefetch information updated when done.
*/ */
async initPrefetchCoursesIcons( async initPrefetchCoursesIcons(
courses: CoreCourseBasicData[], courses: CoreCourseBasicData[],
prefetch: CorePrefetchStatusInfo, prefetch: CorePrefetchStatusInfo,
minCourses: number = 2,
): Promise<CorePrefetchStatusInfo> { ): Promise<CorePrefetchStatusInfo> {
if (!courses || courses.length < minCourses) { if (!courses || courses.length <= 0) {
// Not enough courses. // Not enough courses.
prefetch.icon = ''; prefetch.icon = '';

View File

@ -294,8 +294,11 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
await this.domPromise; await this.domPromise;
const page = this.element.closest('.ion-page'); const page = this.element.closest('.ion-page');
if (!page) {
return;
}
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(page, 'core-loading', 'whenLoaded'); await CoreComponentsRegistry.waitComponentsReady(page, 'core-loading', CoreLoadingComponent);
} }
/** /**

View File

@ -100,7 +100,7 @@ export class CoreDomUtilsProvider {
* @param timeout If defined, timeout to wait before rejecting the promise. * @param timeout If defined, timeout to wait before rejecting the promise.
* @return Cancellable promise. * @return Cancellable promise.
*/ */
waitToBeInDOM(element: Element, timeout?: number): CoreCancellablePromise<void> { waitToBeInDOM(element: HTMLElement, timeout?: number): CoreCancellablePromise<void> {
const root = element.getRootNode({ composed: true }); const root = element.getRootNode({ composed: true });
if (root === document) { if (root === document) {
@ -140,6 +140,41 @@ export class CoreDomUtilsProvider {
); );
} }
/**
* Wait an element to be in dom and visible.
*
* @param element Element to wait.
* @return Cancellable promise.
*/
waitToBeVisible(element: HTMLElement): CoreCancellablePromise<void> {
const domPromise = CoreDomUtils.waitToBeInDOM(element);
let interval: number | undefined;
return new CoreCancellablePromise<void>(
async (resolve) => {
await domPromise;
if (CoreDomUtils.isElementVisible(element)) {
return resolve();
}
interval = window.setInterval(() => {
if (!CoreDomUtils.isElementVisible(element)) {
return;
}
resolve();
window.clearInterval(interval);
}, 50);
},
() => {
domPromise.cancel();
window.clearInterval(interval);
},
);
}
/** /**
* Window resize is widely checked and may have many performance issues, debouce usage is needed to avoid calling it too much. * Window resize is widely checked and may have many performance issues, debouce usage is needed to avoid calling it too much.
* This function helps setting up the debounce feature and remove listener easily. * This function helps setting up the debounce feature and remove listener easily.

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { AsyncComponent } from '@classes/async-component';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
/** /**
@ -39,7 +40,7 @@ export class CoreComponentsRegistry {
* @param componentClass Component class. * @param componentClass Component class.
* @returns Component instance. * @returns Component instance.
*/ */
static resolve<T = Component>(element?: Element | null, componentClass?: ComponentConstructor<T>): T | null { static resolve<T>(element?: Element | null, componentClass?: ComponentConstructor<T>): T | null {
const instance = (element && this.instances.get(element) as T) ?? null; const instance = (element && this.instances.get(element) as T) ?? null;
return instance && (!componentClass || instance instanceof componentClass) return instance && (!componentClass || instance instanceof componentClass)
@ -65,36 +66,46 @@ export class CoreComponentsRegistry {
} }
/** /**
* Waits all elements to be rendered. * Get a component instances and wait to be ready.
* *
* @param element Parent element where to search. * @param element Root element.
* @param selector Selector to search on parent. * @param componentClass Component class.
* @param fnName Component function that have to be resolved when rendered.
* @param params Params of function that have to be resolved when rendered.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
static async finishRenderingAllElementsInside<T = Component>( static async waitComponentReady<T extends AsyncComponent>(
element: Element | undefined | null, element: Element | null,
selector: string, componentClass?: ComponentConstructor<T>,
fnName: string,
params?: unknown[],
): Promise<void> { ): Promise<void> {
if (!element) { const instance = this.resolve(element, componentClass);
if (!instance) {
return; return;
} }
const components = Array await instance.ready();
}
/**
* Waits all elements matching to be ready.
*
* @param element Element where to search.
* @param selector Selector to search on parent.
* @param componentClass Component class.
* @return Promise resolved when done.
*/
static async waitComponentsReady<T extends AsyncComponent>(
element: Element,
selector: string,
componentClass?: ComponentConstructor<T>,
): Promise<void> {
if (element.matches(selector)) {
// Element to wait is myself.
await CoreComponentsRegistry.waitComponentReady<T>(element, componentClass);
} else {
await Promise.all(Array
.from(element.querySelectorAll(selector)) .from(element.querySelectorAll(selector))
.map(element => CoreComponentsRegistry.resolve<T>(element)); .map(element => CoreComponentsRegistry.waitComponentReady<T>(element, componentClass)));
await Promise.all(components.map(component => {
if (!component) {
return;
} }
return component[fnName].apply(component, params);
}));
// Wait for next tick to ensure components are completely rendered. // Wait for next tick to ensure components are completely rendered.
await CoreUtils.nextTick(); await CoreUtils.nextTick();
} }
@ -105,4 +116,4 @@ export class CoreComponentsRegistry {
* Component constructor. * Component constructor.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ComponentConstructor<T> = { new(...args: any[]): T }; export type ComponentConstructor<T = Component> = { new(...args: any[]): T };

View File

@ -80,7 +80,7 @@
body.dark { body.dark {
$dark: map-get($base, 'dark'); $dark: map-get($base, 'dark');
$dark: mix($light, white, 40%) !default; $dark: mix($light, white, 80%) !default;
@include generate-color-variants($color-name, $dark); @include generate-color-variants($color-name, $dark);
} }
} }