forked from EVOgeek/Vmeda.Online
		
	
						commit
						52f0399b99
					
				@ -271,7 +271,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.route.queryParams.subscribe(async (params) => {
 | 
			
		||||
            // When a child page loads this callback is triggered too.
 | 
			
		||||
            const conversationId =CoreNavigator.getRouteNumberParam('conversationId', { params });
 | 
			
		||||
            const conversationId = CoreNavigator.getRouteNumberParam('conversationId', { params });
 | 
			
		||||
            const userId = CoreNavigator.getRouteNumberParam('userId', { params });
 | 
			
		||||
            if (conversationId || userId) {
 | 
			
		||||
                // Update the selected ones.
 | 
			
		||||
 | 
			
		||||
@ -1,29 +1,39 @@
 | 
			
		||||
@import "~theme/globals";
 | 
			
		||||
 | 
			
		||||
:host {
 | 
			
		||||
    ::ng-deep {
 | 
			
		||||
        core-user-avatar .core-avatar-extra {
 | 
			
		||||
    --extra-icon-size: 16px;
 | 
			
		||||
 | 
			
		||||
    ::ng-deep core-user-avatar {
 | 
			
		||||
        .core-avatar-extra-img,
 | 
			
		||||
        core-mod-icon {
 | 
			
		||||
            margin: 0 !important;
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            right: -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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .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 {
 | 
			
		||||
        width: 34px;
 | 
			
		||||
        height: 34px;
 | 
			
		||||
        margin: 10px !important;
 | 
			
		||||
        width: var(--core-avatar-size);
 | 
			
		||||
        height: var(--core-avatar-size);
 | 
			
		||||
        @include margin(6px, 8px, 6px, 0px);
 | 
			
		||||
        background: var(--background-color);
 | 
			
		||||
        border-radius: var(--small-radius);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .item core-format-text ::ng-deep {
 | 
			
		||||
 | 
			
		||||
@ -36,10 +36,11 @@
 | 
			
		||||
                    <core-user-avatar *ngIf="notification.useridfrom > 0" [user]="notification" slot="start"
 | 
			
		||||
                        [profileUrl]="notification.profileimageurlfrom" [fullname]="notification.userfromfullname"
 | 
			
		||||
                        [userId]="notification.useridfrom">
 | 
			
		||||
                        <img *ngIf="notification.iconurl && !notification.modname" [src]="notification.iconurl" alt="" role="presentation"
 | 
			
		||||
                            class="core-avatar-extra">
 | 
			
		||||
                        <div class="core-avatar-extra-img" *ngIf="notification.iconurl && !notification.modname">
 | 
			
		||||
                            <img [src]="notification.iconurl" alt="" role="presentation">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <core-mod-icon *ngIf="notification.modname" [modicon]="notification.iconurl" [modname]="notification.modname"
 | 
			
		||||
                            [showAlt]="false" class="core-avatar-extra">
 | 
			
		||||
                            [showAlt]="false">
 | 
			
		||||
                        </core-mod-icon>
 | 
			
		||||
                    </core-user-avatar>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,8 +13,10 @@
 | 
			
		||||
        <ion-item class="ion-text-wrap" [attr.aria-label]="subject">
 | 
			
		||||
            <core-user-avatar *ngIf="userIdFrom > 0" slot="start" [userId]="userIdFrom" [profileUrl]="profileImageUrlFrom"
 | 
			
		||||
                [fullname]="userFromFullName">
 | 
			
		||||
                <img *ngIf="iconUrl && !modname" [src]="iconUrl" alt="" role="presentation" class="core-avatar-extra">
 | 
			
		||||
                <core-mod-icon *ngIf="modname" [modicon]="iconUrl" [modname]="modname" [showAlt]="false" class="core-avatar-extra">
 | 
			
		||||
                <div class="core-avatar-extra-img" *ngIf="iconUrl && !modname">
 | 
			
		||||
                    <img [src]="iconUrl" alt="" role="presentation">
 | 
			
		||||
                </div>
 | 
			
		||||
                <core-mod-icon *ngIf="modname" [modicon]="iconUrl" [modname]="modname" [showAlt]="false">
 | 
			
		||||
                </core-mod-icon>
 | 
			
		||||
            </core-user-avatar>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								src/core/classes/async-component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/core/classes/async-component.ts
									
									
									
									
									
										Normal 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>;
 | 
			
		||||
}
 | 
			
		||||
@ -20,6 +20,7 @@ import { CoreAnimations } from '@components/animations';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
 | 
			
		||||
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.
 | 
			
		||||
@ -47,7 +48,7 @@ import { CorePromisedValue } from '@classes/promised-value';
 | 
			
		||||
    styleUrls: ['loading.scss'],
 | 
			
		||||
    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() message?: string; // Message to show while loading.
 | 
			
		||||
@ -58,7 +59,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
 | 
			
		||||
    uniqueId: string;
 | 
			
		||||
    protected element: HTMLElement; // Current element.
 | 
			
		||||
    loaded = false; // Only comes true once.
 | 
			
		||||
    protected firstLoadedPromise = new CorePromisedValue<string>();
 | 
			
		||||
    protected onReadyPromise = new CorePromisedValue<void>();
 | 
			
		||||
 | 
			
		||||
    constructor(element: ElementRef) {
 | 
			
		||||
        this.element = element.nativeElement;
 | 
			
		||||
@ -108,7 +109,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
 | 
			
		||||
 | 
			
		||||
        if (!this.loaded && loaded) {
 | 
			
		||||
            this.loaded = true; // Only comes true once.
 | 
			
		||||
            this.firstLoadedPromise.resolve(this.uniqueId);
 | 
			
		||||
            this.onReadyPromise.resolve();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Event has been deprecated since app 4.0.
 | 
			
		||||
@ -119,12 +120,10 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Wait the loading to finish.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved with the uniqueId when done.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async whenLoaded(): Promise<string> {
 | 
			
		||||
        return await this.firstLoadedPromise;
 | 
			
		||||
    async ready(): Promise<void> {
 | 
			
		||||
        return await this.onReadyPromise;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@
 | 
			
		||||
        </core-progress-bar>
 | 
			
		||||
    </ion-col>
 | 
			
		||||
    <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-button>
 | 
			
		||||
    </ion-col>
 | 
			
		||||
 | 
			
		||||
@ -67,15 +67,21 @@ export class CoreNavigationBarComponent implements OnChanges {
 | 
			
		||||
        this.progress = ((this.currentIndex + 1) / this.items.length) * 100;
 | 
			
		||||
        this.progressText = `${this.currentIndex + 1} / ${this.items.length}`;
 | 
			
		||||
 | 
			
		||||
        this.nextIndex = this.items[this.currentIndex + 1]?.enabled ? this.currentIndex + 1 : -1;
 | 
			
		||||
        if (this.nextIndex >= 0) {
 | 
			
		||||
            this.nextTitle = Translate.instant(this.nextTranslate, { $a: this.items[this.nextIndex].title || '' });
 | 
			
		||||
        this.nextIndex = this.currentIndex + 1;
 | 
			
		||||
        while (this.items[this.nextIndex] && !this.items[this.nextIndex].enabled) {
 | 
			
		||||
            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;
 | 
			
		||||
        if (this.previousIndex >= 0) {
 | 
			
		||||
            this.previousTitle = Translate.instant(this.previousTranslate, { $a: this.items[this.previousIndex].title || '' });
 | 
			
		||||
        this.previousIndex = this.currentIndex - 1;
 | 
			
		||||
        while (this.items[this.previousIndex] && !this.items[this.previousIndex].enabled) {
 | 
			
		||||
            this.previousIndex--;
 | 
			
		||||
        }
 | 
			
		||||
        this.previousTitle = this.items[this.previousIndex]
 | 
			
		||||
            ? Translate.instant(this.previousTranslate, { $a: this.items[this.previousIndex].title || '' })
 | 
			
		||||
            : '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -78,8 +78,6 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:host-context(ion-item) {
 | 
			
		||||
    margin-top: 6px;
 | 
			
		||||
    margin-bottom: 6px;
 | 
			
		||||
    @include margin(6px, 8px, 6px, 0px);
 | 
			
		||||
    img {
 | 
			
		||||
        min-width: var(--core-avatar-size);
 | 
			
		||||
 | 
			
		||||
@ -66,7 +66,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        await this.domPromise;
 | 
			
		||||
        await this.waitLoadingsDone();
 | 
			
		||||
        await this.waitFormatTextsRendered(this.element);
 | 
			
		||||
        await this.waitFormatTextsRendered();
 | 
			
		||||
 | 
			
		||||
        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.
 | 
			
		||||
     *
 | 
			
		||||
     * @param element Element.
 | 
			
		||||
     */
 | 
			
		||||
    protected async waitFormatTextsRendered(element: Element): Promise<void> {
 | 
			
		||||
        await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreFormatTextDirective>(
 | 
			
		||||
            element,
 | 
			
		||||
            'core-format-text',
 | 
			
		||||
            'rendered',
 | 
			
		||||
        );
 | 
			
		||||
    protected async waitFormatTextsRendered(): Promise<void> {
 | 
			
		||||
        await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -210,13 +204,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
 | 
			
		||||
        const scrollElement = await this.ionContent.getScrollElement();
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>
 | 
			
		||||
            (scrollElement, 'core-loading', 'whenLoaded'),
 | 
			
		||||
            await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(
 | 
			
		||||
                this.element,
 | 
			
		||||
                'core-loading',
 | 
			
		||||
                'whenLoaded',
 | 
			
		||||
            ),
 | 
			
		||||
            await CoreComponentsRegistry.waitComponentsReady(scrollElement, 'core-loading', CoreLoadingComponent),
 | 
			
		||||
            await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-loading', CoreLoadingComponent),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -76,6 +76,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
 | 
			
		||||
    protected enabled = true;
 | 
			
		||||
    protected isWithinContent = false;
 | 
			
		||||
    protected enteredPromise = new CorePromisedValue<void>();
 | 
			
		||||
    protected mutationObserver?: MutationObserver;
 | 
			
		||||
 | 
			
		||||
    constructor(el: ElementRef) {
 | 
			
		||||
        this.collapsedHeader = el.nativeElement;
 | 
			
		||||
@ -126,6 +127,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.resizeListener?.off();
 | 
			
		||||
        this.mutationObserver?.disconnect();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -161,6 +163,31 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
 | 
			
		||||
        this.subscriptions.push(CoreSettingsHelper.onDarkModeChange().subscribe(() => {
 | 
			
		||||
            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);
 | 
			
		||||
 | 
			
		||||
            originalTitle.classList.add('collapsible-header-original-title');
 | 
			
		||||
            this.mutationObserver?.observe(originalTitle, { childList: true, subtree: true });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const floatingTitleBoundingBox = floatingTitle.getBoundingClientRect();
 | 
			
		||||
@ -304,15 +332,13 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Wait until all <core-loading> children inside the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when loadings are done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async waitLoadingsDone(): Promise<void> {
 | 
			
		||||
        await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(
 | 
			
		||||
            this.page,
 | 
			
		||||
            'core-loading',
 | 
			
		||||
            'whenLoaded',
 | 
			
		||||
        );
 | 
			
		||||
        if (!this.page) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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.
 | 
			
		||||
     */
 | 
			
		||||
    protected async waitFormatTextsRendered(element: Element): Promise<void> {
 | 
			
		||||
        await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreFormatTextDirective>(
 | 
			
		||||
            element,
 | 
			
		||||
            'core-format-text',
 | 
			
		||||
            'rendered',
 | 
			
		||||
        );
 | 
			
		||||
        await CoreComponentsRegistry.waitComponentsReady(element, 'core-format-text', CoreFormatTextDirective);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@ import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreCancellablePromise } from '@classes/cancellable-promise';
 | 
			
		||||
import { CoreLoadingComponent } from '@components/loading/loading';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
 | 
			
		||||
import { CoreEventObserver } from '@singletons/events';
 | 
			
		||||
@ -103,28 +102,18 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        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.
 | 
			
		||||
     *
 | 
			
		||||
     * @param element Element.
 | 
			
		||||
     */
 | 
			
		||||
    protected async waitFormatTextsRendered(element: Element): Promise<void> {
 | 
			
		||||
        let formatTextElements: HTMLElement[] = [];
 | 
			
		||||
 | 
			
		||||
        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();
 | 
			
		||||
    protected async waitFormatTextsRendered(): Promise<void> {
 | 
			
		||||
        await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -134,7 +123,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
 | 
			
		||||
        // Remove max-height (if any) to calculate the real height.
 | 
			
		||||
        this.element.classList.add('collapsible-loading-height');
 | 
			
		||||
 | 
			
		||||
        await this.waitFormatTextsRendered(this.element);
 | 
			
		||||
        await this.waitFormatTextsRendered();
 | 
			
		||||
 | 
			
		||||
        this.expandedHeight = this.element.getBoundingClientRect().height;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -33,6 +33,7 @@ import { CoreCollapsibleItemDirective } from './collapsible-item';
 | 
			
		||||
import { CoreCollapsibleFooterDirective } from './collapsible-footer';
 | 
			
		||||
import { CoreContentDirective } from './content';
 | 
			
		||||
import { CoreOnAppearDirective } from './on-appear';
 | 
			
		||||
import { CoreUpdateNonReactiveAttributesDirective } from './update-non-reactive-attributes';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
    declarations: [
 | 
			
		||||
@ -55,6 +56,7 @@ import { CoreOnAppearDirective } from './on-appear';
 | 
			
		||||
        CoreCollapsibleItemDirective,
 | 
			
		||||
        CoreCollapsibleFooterDirective,
 | 
			
		||||
        CoreContentDirective,
 | 
			
		||||
        CoreUpdateNonReactiveAttributesDirective,
 | 
			
		||||
    ],
 | 
			
		||||
    exports: [
 | 
			
		||||
        CoreAutoFocusDirective,
 | 
			
		||||
@ -76,6 +78,7 @@ import { CoreOnAppearDirective } from './on-appear';
 | 
			
		||||
        CoreCollapsibleItemDirective,
 | 
			
		||||
        CoreCollapsibleFooterDirective,
 | 
			
		||||
        CoreContentDirective,
 | 
			
		||||
        CoreUpdateNonReactiveAttributesDirective,
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
export class CoreDirectivesModule {}
 | 
			
		||||
 | 
			
		||||
@ -43,6 +43,7 @@ import { CoreSubscriptions } from '@singletons/subscriptions';
 | 
			
		||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
 | 
			
		||||
import { CoreCollapsibleItemDirective } from './collapsible-item';
 | 
			
		||||
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
 | 
			
		||||
@ -56,7 +57,7 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise';
 | 
			
		||||
@Directive({
 | 
			
		||||
    selector: 'core-format-text',
 | 
			
		||||
})
 | 
			
		||||
export class CoreFormatTextDirective implements OnChanges, OnDestroy {
 | 
			
		||||
export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncComponent {
 | 
			
		||||
 | 
			
		||||
    @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')) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@ export class CoreOnAppearDirective implements OnInit, OnDestroy {
 | 
			
		||||
    @Output() onAppear = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
    private element: HTMLElement;
 | 
			
		||||
    protected domPromise?: CoreCancellablePromise<void>;
 | 
			
		||||
    protected visiblePromise?: CoreCancellablePromise<void>;
 | 
			
		||||
 | 
			
		||||
    constructor(element: ElementRef) {
 | 
			
		||||
        this.element = element.nativeElement;
 | 
			
		||||
@ -37,9 +37,9 @@ export class CoreOnAppearDirective implements OnInit, OnDestroy {
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    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();
 | 
			
		||||
    }
 | 
			
		||||
@ -48,7 +48,7 @@ export class CoreOnAppearDirective implements OnInit, OnDestroy {
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.domPromise?.cancel();
 | 
			
		||||
        this.visiblePromise?.cancel();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										68
									
								
								src/core/directives/update-non-reactive-attributes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/core/directives/update-non-reactive-attributes.ts
									
									
									
									
									
										Normal 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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -24,7 +24,7 @@
 | 
			
		||||
 | 
			
		||||
            <core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)"></core-infinite-loading>
 | 
			
		||||
        </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">
 | 
			
		||||
                <ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" expand="block"
 | 
			
		||||
                    [attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name">
 | 
			
		||||
 | 
			
		||||
@ -92,7 +92,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    selectedSection?: CoreCourseSection;
 | 
			
		||||
    previousSection?: CoreCourseSection;
 | 
			
		||||
    nextSection?: CoreCourseSection;
 | 
			
		||||
    hasPreviousOrNextSections = false;
 | 
			
		||||
    allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID;
 | 
			
		||||
    stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
 | 
			
		||||
    loaded = false;
 | 
			
		||||
@ -489,8 +488,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
            this.showMoreActivities();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.hasPreviousOrNextSections = !!this.previousSection || !!this.nextSection;
 | 
			
		||||
 | 
			
		||||
        // Scroll to module if needed. Give more priority to the input.
 | 
			
		||||
        const moduleIdToScroll = this.moduleId && previousValue === undefined ? this.moduleId : moduleId;
 | 
			
		||||
        if (moduleIdToScroll) {
 | 
			
		||||
@ -507,8 +504,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
                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?.();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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).
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ ion-item.item {
 | 
			
		||||
 | 
			
		||||
    &.item-current {
 | 
			
		||||
        --background: var(--primary-tint);
 | 
			
		||||
        --color: var(--gray-900);
 | 
			
		||||
        border: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -34,6 +35,7 @@ ion-item.item {
 | 
			
		||||
 | 
			
		||||
                &:hover {
 | 
			
		||||
                    background: var(--primary-shade);
 | 
			
		||||
                    color: var(--gray-100);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@ -43,7 +45,7 @@ ion-item.item {
 | 
			
		||||
            margin: 3px;
 | 
			
		||||
            border-radius: 50%;
 | 
			
		||||
            &:hover {
 | 
			
		||||
                background: var(--gray-300);
 | 
			
		||||
                background: var(--secondary);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -23,134 +23,139 @@
 | 
			
		||||
            </ion-icon>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div *ngIf="course" class="course-container">
 | 
			
		||||
            <ion-item class="ion-text-wrap course-name">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <p *ngIf="course.displayname && course.shortname && course.fullname != course.displayname"
 | 
			
		||||
                        class="core-course-shortname">
 | 
			
		||||
                        <core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id">
 | 
			
		||||
                        </core-format-text>
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <h1>
 | 
			
		||||
                        <span class="sr-only">{{ 'core.courses.aria:coursename' | translate }}</span>
 | 
			
		||||
                        <core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id">
 | 
			
		||||
                        </core-format-text>
 | 
			
		||||
                    </h1>
 | 
			
		||||
                    <ion-chip color="primary" *ngIf="course.categoryname" class="core-course-category ion-text-nowrap">
 | 
			
		||||
                        <span class="sr-only">{{ 'core.courses.aria:coursecategory' | translate }}</span>
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <core-format-text [text]="course.categoryname" contextLevel="coursecat" [contextInstanceId]="course.categoryid">
 | 
			
		||||
                            </core-format-text>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-chip>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
                <ion-button fill="clear" [href]="courseUrl" core-link [showBrowserWarning]="false"
 | 
			
		||||
                    [attr.aria-label]="'core.openinbrowser' | translate" slot="end">
 | 
			
		||||
                    <ion-icon name="fas-external-link-alt" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
                </ion-button>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="progress !== undefined || course.startdate || course.enddate">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <div class="core-course-progress" *ngIf="progress !== undefined">
 | 
			
		||||
                        <core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">
 | 
			
		||||
                        </core-progress-bar>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div *ngIf="course.startdate || course.enddate" class="core-course-dates">
 | 
			
		||||
                        <p *ngIf="course.startdate">
 | 
			
		||||
                            <ion-icon name="fas-calendar" aria-hidden="true"></ion-icon>
 | 
			
		||||
                            <strong>{{ 'core.course.startdate' | translate }}</strong><br>
 | 
			
		||||
                            {{ course.startdate * 1000 | coreFormatDate:'strftimedaydatetime' }}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p *ngIf="course.enddate">
 | 
			
		||||
                            <ion-icon name="fas-calendar" aria-hidden="true"></ion-icon>
 | 
			
		||||
                            <strong>{{ 'core.course.enddate' | translate }}</strong><br>
 | 
			
		||||
                            {{ course.enddate * 1000 | coreFormatDate:'strftimedaydatetime' }}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="course.summary" detail="false">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <p class="item-heading">
 | 
			
		||||
                        {{'core.course.coursesummary' | translate}}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <core-format-text [text]="course.summary" collapsible-item contextLevel="course" [contextInstanceId]="course.id">
 | 
			
		||||
                    </core-format-text>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <ion-list *ngIf="course.contacts && course.contacts.length">
 | 
			
		||||
                <ion-item [button]="course.contacts.length >= 5" class="ion-text-wrap" (click)="toggleContacts()"
 | 
			
		||||
                    [attr.aria-label]="(contactsExpanded ? 'core.collapse' : 'core.expand') | translate" detail="false">
 | 
			
		||||
                    <ion-icon *ngIf="course.contacts.length >= 5" name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true"
 | 
			
		||||
                        class="expandable-status-icon" [class.expandable-status-icon-expanded]="contactsExpanded">
 | 
			
		||||
                    </ion-icon>
 | 
			
		||||
            <div class="list-item-limited-width">
 | 
			
		||||
                <ion-item class="ion-text-wrap course-name">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <p class="item-heading">
 | 
			
		||||
                            {{ 'core.teachers' | translate }}
 | 
			
		||||
                        <p *ngIf="course.displayname && course.shortname && course.fullname != course.displayname"
 | 
			
		||||
                            class="core-course-shortname">
 | 
			
		||||
                            <core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id">
 | 
			
		||||
                            </core-format-text>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <h1>
 | 
			
		||||
                            <span class="sr-only">{{ 'core.courses.aria:coursename' | translate }}</span>
 | 
			
		||||
                            <core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id">
 | 
			
		||||
                            </core-format-text>
 | 
			
		||||
                        </h1>
 | 
			
		||||
                        <ion-chip color="primary" *ngIf="course.categoryname" class="core-course-category ion-text-nowrap">
 | 
			
		||||
                            <span class="sr-only">{{ 'core.courses.aria:coursecategory' | translate }}</span>
 | 
			
		||||
                            <ion-label>
 | 
			
		||||
                                <core-format-text [text]="course.categoryname" contextLevel="coursecat"
 | 
			
		||||
                                    [contextInstanceId]="course.categoryid">
 | 
			
		||||
                                </core-format-text>
 | 
			
		||||
                            </ion-label>
 | 
			
		||||
                        </ion-chip>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-button fill="clear" [href]="courseUrl" core-link [showBrowserWarning]="false"
 | 
			
		||||
                        [attr.aria-label]="'core.openinbrowser' | translate" slot="end">
 | 
			
		||||
                        <ion-icon name="fas-external-link-alt" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
                    </ion-button>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="progress !== undefined || course.startdate || course.enddate">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <div class="core-course-progress" *ngIf="progress !== undefined">
 | 
			
		||||
                            <core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">
 | 
			
		||||
                            </core-progress-bar>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div *ngIf="course.startdate || course.enddate" class="core-course-dates">
 | 
			
		||||
                            <p *ngIf="course.startdate">
 | 
			
		||||
                                <ion-icon name="fas-calendar" aria-hidden="true"></ion-icon>
 | 
			
		||||
                                <strong>{{ 'core.course.startdate' | translate }}</strong><br>
 | 
			
		||||
                                {{ course.startdate * 1000 | coreFormatDate:'strftimedaydatetime' }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p *ngIf="course.enddate">
 | 
			
		||||
                                <ion-icon name="fas-calendar" aria-hidden="true"></ion-icon>
 | 
			
		||||
                                <strong>{{ 'core.course.enddate' | translate }}</strong><br>
 | 
			
		||||
                                {{ course.enddate * 1000 | coreFormatDate:'strftimedaydatetime' }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ng-container *ngIf="contactsExpanded || course.contacts.length < 5">
 | 
			
		||||
                    <ion-item button class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link [userId]="contact.id"
 | 
			
		||||
                        [courseId]="isEnrolled ? course.id : null" [attr.aria-label]="'core.viewprofile' | translate" detail="true">
 | 
			
		||||
                        <core-user-avatar [user]="contact" slot="start" [userId]="contact.id" [courseId]="isEnrolled ? course.id : null"
 | 
			
		||||
                            [linkProfile]="false">
 | 
			
		||||
                        </core-user-avatar>
 | 
			
		||||
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="course.summary" detail="false">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <p class="item-heading">
 | 
			
		||||
                            {{'core.course.coursesummary' | translate}}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <core-format-text [text]="course.summary" collapsible-item contextLevel="course" [contextInstanceId]="course.id">
 | 
			
		||||
                        </core-format-text>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
 | 
			
		||||
                <ion-list *ngIf="course.contacts && course.contacts.length">
 | 
			
		||||
                    <ion-item [button]="course.contacts.length >= 5" class="ion-text-wrap" (click)="toggleContacts()"
 | 
			
		||||
                        [attr.aria-label]="(contactsExpanded ? 'core.collapse' : 'core.expand') | translate" detail="false">
 | 
			
		||||
                        <ion-icon *ngIf="course.contacts.length >= 5" name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true"
 | 
			
		||||
                            class="expandable-status-icon" [class.expandable-status-icon-expanded]="contactsExpanded">
 | 
			
		||||
                        </ion-icon>
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
                            <p class="item-heading">{{contact.fullname}}</p>
 | 
			
		||||
                            <p class="item-heading">
 | 
			
		||||
                                {{ 'core.teachers' | translate }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-item>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
                <core-spacer></core-spacer>
 | 
			
		||||
            </ion-list>
 | 
			
		||||
 | 
			
		||||
            <ion-item class="ion-text-wrap" *ngIf="course.customfields">
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <ng-container *ngFor="let field of course.customfields">
 | 
			
		||||
                        <div *ngIf="field.value"
 | 
			
		||||
                            class="core-customfield core-customfield_{{field.type}} core-customfield_{{field.shortname}}">
 | 
			
		||||
                            <span class="core-customfieldname">
 | 
			
		||||
                                <core-format-text [text]="field.name" contextLevel="course" [contextInstanceId]="course.id">
 | 
			
		||||
                                </core-format-text>
 | 
			
		||||
                                <span class="core-customfieldseparator">: </span>
 | 
			
		||||
                            </span>
 | 
			
		||||
                            <span class="core-customfieldvalue">
 | 
			
		||||
                                <core-format-text [text]="field.value" [collapsible-item]="field.type == 'textarea'  ? '' : null"
 | 
			
		||||
                                    contextLevel="course" [contextInstanceId]="course.id">
 | 
			
		||||
                                </core-format-text>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    <ng-container *ngIf="contactsExpanded || course.contacts.length < 5">
 | 
			
		||||
                        <ion-item button class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link [userId]="contact.id"
 | 
			
		||||
                            [courseId]="isEnrolled ? course.id : null" [attr.aria-label]="'core.viewprofile' | translate" detail="true">
 | 
			
		||||
                            <core-user-avatar [user]="contact" slot="start" [userId]="contact.id" [courseId]="isEnrolled ? course.id : null"
 | 
			
		||||
                                [linkProfile]="false">
 | 
			
		||||
                            </core-user-avatar>
 | 
			
		||||
                            <ion-label>
 | 
			
		||||
                                <p class="item-heading">{{contact.fullname}}</p>
 | 
			
		||||
                            </ion-label>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
                    </ng-container>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
                    <core-spacer></core-spacer>
 | 
			
		||||
                </ion-list>
 | 
			
		||||
 | 
			
		||||
                <ion-item class="ion-text-wrap" *ngIf="course.customfields">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <ng-container *ngFor="let field of course.customfields">
 | 
			
		||||
                            <div *ngIf="field.value"
 | 
			
		||||
                                class="core-customfield core-customfield_{{field.type}} core-customfield_{{field.shortname}}">
 | 
			
		||||
                                <span class="core-customfieldname">
 | 
			
		||||
                                    <core-format-text [text]="field.name" contextLevel="course" [contextInstanceId]="course.id">
 | 
			
		||||
                                    </core-format-text>
 | 
			
		||||
                                    <span class="core-customfieldseparator">: </span>
 | 
			
		||||
                                </span>
 | 
			
		||||
                                <span class="core-customfieldvalue">
 | 
			
		||||
                                    <core-format-text [text]="field.value" [collapsible-item]="field.type == 'textarea'  ? '' : null"
 | 
			
		||||
                                        contextLevel="course" [contextInstanceId]="course.id">
 | 
			
		||||
                                    </core-format-text>
 | 
			
		||||
                                </span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </ng-container>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
 | 
			
		||||
<ion-footer *ngIf="course && dataLoaded">
 | 
			
		||||
    <ng-container *ngIf="canAccessCourse">
 | 
			
		||||
        <ion-button *ngFor="let item of courseMenuHandlers" (click)="openMenuItem(item)" [class]="'ion-text-wrap '+ item.data.class"
 | 
			
		||||
            expand="block">
 | 
			
		||||
            <ion-icon *ngIf="item.data.icon" [name]="item.data.icon" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
            <ion-label>{{item.data.title | translate }}</ion-label>
 | 
			
		||||
    <div class="list-item-limited-width">
 | 
			
		||||
        <ng-container *ngIf="canAccessCourse">
 | 
			
		||||
            <ion-button *ngFor="let item of courseMenuHandlers" (click)="openMenuItem(item)" [class]="'ion-text-wrap '+ item.data.class"
 | 
			
		||||
                expand="block">
 | 
			
		||||
                <ion-icon *ngIf="item.data.icon" [name]="item.data.icon" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
                <ion-label>{{item.data.title | translate }}</ion-label>
 | 
			
		||||
            </ion-button>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
 | 
			
		||||
        <ion-button expand="block" (click)="enrolMe()" *ngIf="!isEnrolled && (selfEnrolInstances.length > 0 || otherEnrolments)"
 | 
			
		||||
            class="ion-text-wrap">
 | 
			
		||||
            {{ 'core.courses.enrolme' | translate }}
 | 
			
		||||
        </ion-button>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
    <ion-button expand="block" (click)="enrolMe()" *ngIf="!isEnrolled && (selfEnrolInstances.length > 0 || otherEnrolments)"
 | 
			
		||||
        class="ion-text-wrap">
 | 
			
		||||
        {{ 'core.courses.enrolme' | translate }}
 | 
			
		||||
    </ion-button>
 | 
			
		||||
        <ion-card class="core-info-card ion-text-wrap" *ngIf="!isEnrolled && !selfEnrolInstances.length && !otherEnrolments">
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
                <ion-label>{{ 'core.courses.notenrollable' | translate }}</ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
 | 
			
		||||
    <ion-card class="core-info-card ion-text-wrap" *ngIf="!isEnrolled && !selfEnrolInstances.length && !otherEnrolments">
 | 
			
		||||
        <ion-item>
 | 
			
		||||
            <ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
            <ion-label>{{ 'core.courses.notenrollable' | translate }}</ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
    </ion-card>
 | 
			
		||||
 | 
			
		||||
    <ion-button (click)="openCourse()" *ngIf="!isModal && canAccessCourse" expand="block" fill="outline" class="ion-text-wrap">
 | 
			
		||||
        <ion-icon name="fas-eye" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
        {{ 'core.course.viewcourse' | translate }}
 | 
			
		||||
    </ion-button>
 | 
			
		||||
        <ion-button (click)="openCourse()" *ngIf="!isModal && canAccessCourse" expand="block" fill="outline" class="ion-text-wrap">
 | 
			
		||||
            <ion-icon name="fas-eye" slot="start" aria-hidden="true"></ion-icon>
 | 
			
		||||
            {{ 'core.course.viewcourse' | translate }}
 | 
			
		||||
        </ion-button>
 | 
			
		||||
    </div>
 | 
			
		||||
</ion-footer>
 | 
			
		||||
 | 
			
		||||
@ -39,11 +39,14 @@
 | 
			
		||||
    .course-container {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        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 {
 | 
			
		||||
        border-radius: var(--big-radius) var(--big-radius) 0 0;
 | 
			
		||||
        box-shadow: var(--drop-shadow-top);
 | 
			
		||||
        --background: transparent;
 | 
			
		||||
        ion-label {
 | 
			
		||||
            margin-bottom: 0px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -16,24 +16,25 @@
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-item class="core-format-progress-list ion-text-wrap list-item-limited-width" collapsible>
 | 
			
		||||
    <ng-container *ngIf="course">
 | 
			
		||||
        <div *ngIf="!course.courseImage" #courseThumb slot="start" class="core-course-thumb">
 | 
			
		||||
            <ion-icon *ngIf="!course.courseImage" name="fas-graduation-cap" class="course-icon" aria-hidden="true">
 | 
			
		||||
            </ion-icon>
 | 
			
		||||
        </div>
 | 
			
		||||
        <ion-avatar *ngIf="course.courseImage" slot="start" class="core-course-thumb">
 | 
			
		||||
            <img [src]="course.courseImage" core-external-content alt="" />
 | 
			
		||||
        </ion-avatar>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
<div class="core-course-header">
 | 
			
		||||
    <ion-item class="core-format-progress-list ion-text-wrap list-item-limited-width" collapsible>
 | 
			
		||||
        <ng-container *ngIf="course">
 | 
			
		||||
            <div *ngIf="!course.courseImage" #courseThumb slot="start" class="core-course-thumb">
 | 
			
		||||
                <ion-icon *ngIf="!course.courseImage" name="fas-graduation-cap" class="course-icon" aria-hidden="true">
 | 
			
		||||
                </ion-icon>
 | 
			
		||||
            </div>
 | 
			
		||||
            <ion-avatar *ngIf="course.courseImage" slot="start" class="core-course-thumb">
 | 
			
		||||
                <img [src]="course.courseImage" core-external-content alt="" />
 | 
			
		||||
            </ion-avatar>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
 | 
			
		||||
    <ion-label>
 | 
			
		||||
        <h1>{{ title }}</h1>
 | 
			
		||||
        <div class="core-course-progress" *ngIf="progress !== undefined">
 | 
			
		||||
            <core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">
 | 
			
		||||
            </core-progress-bar>
 | 
			
		||||
        </div>
 | 
			
		||||
    </ion-label>
 | 
			
		||||
 | 
			
		||||
</ion-item>
 | 
			
		||||
        <ion-label>
 | 
			
		||||
            <h1>{{ title }}</h1>
 | 
			
		||||
            <div class="core-course-progress" *ngIf="progress !== undefined">
 | 
			
		||||
                <core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">
 | 
			
		||||
                </core-progress-bar>
 | 
			
		||||
            </div>
 | 
			
		||||
        </ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
</div>
 | 
			
		||||
<core-tabs-outlet [tabs]="tabs" [hideUntil]="loaded" (ionChange)="tabSelected()"></core-tabs-outlet>
 | 
			
		||||
 | 
			
		||||
@ -44,4 +44,8 @@
 | 
			
		||||
    h1 {
 | 
			
		||||
        font-size: 20px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .core-course-header {
 | 
			
		||||
        background: var(--ion-item-background);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1064,15 +1064,13 @@ export class CoreCourseHelperProvider {
 | 
			
		||||
     *
 | 
			
		||||
     * @param courses Courses array to get info from.
 | 
			
		||||
     * @param prefetch Prefetch information.
 | 
			
		||||
     * @param minCourses Min course to show icon.
 | 
			
		||||
     * @return Resolved with the prefetch information updated when done.
 | 
			
		||||
     */
 | 
			
		||||
    async initPrefetchCoursesIcons(
 | 
			
		||||
        courses: CoreCourseBasicData[],
 | 
			
		||||
        prefetch: CorePrefetchStatusInfo,
 | 
			
		||||
        minCourses: number = 2,
 | 
			
		||||
    ): Promise<CorePrefetchStatusInfo> {
 | 
			
		||||
        if (!courses || courses.length < minCourses) {
 | 
			
		||||
        if (!courses || courses.length <= 0) {
 | 
			
		||||
            // Not enough courses.
 | 
			
		||||
            prefetch.icon = '';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -294,8 +294,11 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
 | 
			
		||||
        await this.domPromise;
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -100,7 +100,7 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
     * @param timeout If defined, timeout to wait before rejecting the promise.
 | 
			
		||||
     * @return Cancellable promise.
 | 
			
		||||
     */
 | 
			
		||||
    waitToBeInDOM(element: Element, timeout?: number): CoreCancellablePromise<void> {
 | 
			
		||||
    waitToBeInDOM(element: HTMLElement, timeout?: number): CoreCancellablePromise<void> {
 | 
			
		||||
        const root = element.getRootNode({ composed: true });
 | 
			
		||||
 | 
			
		||||
        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.
 | 
			
		||||
     * This function helps setting up the debounce feature and remove listener easily.
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component } from '@angular/core';
 | 
			
		||||
import { AsyncComponent } from '@classes/async-component';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -39,7 +40,7 @@ export class CoreComponentsRegistry {
 | 
			
		||||
     * @param componentClass Component class.
 | 
			
		||||
     * @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;
 | 
			
		||||
 | 
			
		||||
        return instance && (!componentClass || instance instanceof componentClass)
 | 
			
		||||
@ -65,35 +66,45 @@ 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 selector Selector to search on parent.
 | 
			
		||||
     * @param fnName Component function that have to be resolved when rendered.
 | 
			
		||||
     * @param params Params of function that have to be resolved when rendered.
 | 
			
		||||
     * @param element Root element.
 | 
			
		||||
     * @param componentClass Component class.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    static async finishRenderingAllElementsInside<T = Component>(
 | 
			
		||||
        element: Element | undefined | null,
 | 
			
		||||
        selector: string,
 | 
			
		||||
        fnName: string,
 | 
			
		||||
        params?: unknown[],
 | 
			
		||||
    static async waitComponentReady<T extends AsyncComponent>(
 | 
			
		||||
        element: Element | null,
 | 
			
		||||
        componentClass?: ComponentConstructor<T>,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        if (!element) {
 | 
			
		||||
        const instance = this.resolve(element, componentClass);
 | 
			
		||||
        if (!instance) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const components = Array
 | 
			
		||||
            .from(element.querySelectorAll(selector))
 | 
			
		||||
            .map(element => CoreComponentsRegistry.resolve<T>(element));
 | 
			
		||||
        await instance.ready();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        await Promise.all(components.map(component => {
 | 
			
		||||
            if (!component) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return component[fnName].apply(component, params);
 | 
			
		||||
        }));
 | 
			
		||||
    /**
 | 
			
		||||
     * 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))
 | 
			
		||||
                .map(element => CoreComponentsRegistry.waitComponentReady<T>(element, componentClass)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Wait for next tick to ensure components are completely rendered.
 | 
			
		||||
        await CoreUtils.nextTick();
 | 
			
		||||
@ -105,4 +116,4 @@ export class CoreComponentsRegistry {
 | 
			
		||||
 * Component constructor.
 | 
			
		||||
 */
 | 
			
		||||
// 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 };
 | 
			
		||||
 | 
			
		||||
@ -80,7 +80,7 @@
 | 
			
		||||
 | 
			
		||||
    body.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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user