forked from CIT/Vmeda.Online
		
	
						commit
						be28e1c2e4
					
				@ -21,6 +21,7 @@ import { ContextLevel, CoreConstants } from '@/core/constants';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component to render an "activity modules" block.
 | 
			
		||||
@ -67,7 +68,7 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            section.modules.forEach((mod) => {
 | 
			
		||||
                if (mod.uservisible === false || !CoreCourse.moduleHasView(mod) ||
 | 
			
		||||
                if (!CoreCourseHelper.canUserViewModule(mod, section) || !CoreCourse.moduleHasView(mod) ||
 | 
			
		||||
                    modFullNames[mod.modname] !== undefined) {
 | 
			
		||||
                    // Ignore this module.
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
@ -183,7 +183,7 @@
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ion-item class="ion-text-wrap">
 | 
			
		||||
                    <ion-label position="stacked">
 | 
			
		||||
                        <p>{{ 'addon.calendar.repeatweeksl' | translate }}</p>
 | 
			
		||||
                        <p class="item-heading">{{ 'addon.calendar.repeatweeksl' | translate }}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                    <ion-input type="number" name="repeats" formControlName="repeats" [disabled]="!form.controls.repeat.value">
 | 
			
		||||
                    </ion-input>
 | 
			
		||||
@ -231,23 +231,17 @@
 | 
			
		||||
                <ion-input type="text" name="location" [placeholder]="'core.location' | translate" formControlName="location">
 | 
			
		||||
                </ion-input>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <ion-row>
 | 
			
		||||
                        <ion-col *ngIf="hasOffline && eventId && eventId < 0">
 | 
			
		||||
                            <ion-button expand="block" fill="outline" (click)="discard()">
 | 
			
		||||
                                {{ 'core.discard' | translate }}
 | 
			
		||||
                            </ion-button>
 | 
			
		||||
                        </ion-col>
 | 
			
		||||
                        <ion-col>
 | 
			
		||||
                            <ion-button expand="block" (click)="submit()" [disabled]="!form.valid" type="submit">
 | 
			
		||||
                                {{ 'core.save' | translate }}
 | 
			
		||||
                            </ion-button>
 | 
			
		||||
                        </ion-col>
 | 
			
		||||
                    </ion-row>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </form>
 | 
			
		||||
        <div collapsible-footer appearOnBottom *ngIf="loaded && !error" slot="fixed">
 | 
			
		||||
            <div class="list-item-limited-width adaptable-buttons-row">
 | 
			
		||||
                <ion-button *ngIf="hasOffline && eventId && eventId < 0" expand="block" fill="outline" (click)="discard()"
 | 
			
		||||
                    class="ion-margin ion-text-wrap">
 | 
			
		||||
                    {{ 'core.discard' | translate }}
 | 
			
		||||
                </ion-button>
 | 
			
		||||
                <ion-button expand="block" (click)="submit()" [disabled]="!form.valid" type="submit" class="ion-margin ion-text-wrap">
 | 
			
		||||
                    {{ 'core.save' | translate }}
 | 
			
		||||
                </ion-button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,6 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter';
 | 
			
		||||
import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreUrlUtils } from '@services/utils/url';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
@ -32,19 +31,10 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
 | 
			
		||||
    protected template = document.createElement('template'); // A template element to convert HTML to element.
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Filter some text.
 | 
			
		||||
     *
 | 
			
		||||
     * @param text The text to filter.
 | 
			
		||||
     * @param filter The filter.
 | 
			
		||||
     * @param options Options passed to the filters.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Filtered text (or promise resolved with the filtered text).
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    filter(
 | 
			
		||||
        text: string,
 | 
			
		||||
        filter: CoreFilterFilter, // eslint-disable-line @typescript-eslint/no-unused-vars
 | 
			
		||||
        options: CoreFilterFormatTextOptions, // eslint-disable-line @typescript-eslint/no-unused-vars
 | 
			
		||||
        siteId?: string, // eslint-disable-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    ): string | Promise<string> {
 | 
			
		||||
        this.template.innerHTML = text;
 | 
			
		||||
 | 
			
		||||
@ -60,7 +50,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
 | 
			
		||||
    /**
 | 
			
		||||
     * Treat video filters. Currently only treating youtube video using video JS.
 | 
			
		||||
     *
 | 
			
		||||
     * @param el Video element.
 | 
			
		||||
     * @param video Video element.
 | 
			
		||||
     */
 | 
			
		||||
    protected treatVideoFilters(video: HTMLElement): void {
 | 
			
		||||
        // Treat Video JS Youtube video links and translate them to iframes.
 | 
			
		||||
 | 
			
		||||
@ -1106,10 +1106,10 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
 | 
			
		||||
     * Scroll to the first new unread message.
 | 
			
		||||
     */
 | 
			
		||||
    scrollToFirstUnreadMessage(): void {
 | 
			
		||||
        if (this.newMessages > 0 && this.content) {
 | 
			
		||||
            const messages = Array.from(this.hostElement.querySelectorAll('.addon-message-not-mine'));
 | 
			
		||||
        if (this.newMessages > 0) {
 | 
			
		||||
            const messages = Array.from(this.hostElement.querySelectorAll<HTMLElement>('.addon-message-not-mine'));
 | 
			
		||||
 | 
			
		||||
            CoreDomUtils.scrollToElement(this.content, <HTMLElement> messages[messages.length - this.newMessages]);
 | 
			
		||||
            CoreDomUtils.scrollViewToElement(messages[messages.length - this.newMessages]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -159,7 +159,7 @@ export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
        this.messages[this.messages.length - 1].showTail = true;
 | 
			
		||||
 | 
			
		||||
        // New messages or beeps, scroll to bottom.
 | 
			
		||||
        setTimeout(() => this.scrollToBottom());
 | 
			
		||||
        this.scrollToBottom();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async loadMessageBeepWho(message: AddonModChatFormattedMessage): Promise<void> {
 | 
			
		||||
@ -341,13 +341,12 @@ export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
    /**
 | 
			
		||||
     * Scroll bottom when render has finished.
 | 
			
		||||
     */
 | 
			
		||||
    scrollToBottom(): void {
 | 
			
		||||
    async scrollToBottom(): Promise<void> {
 | 
			
		||||
        // Need a timeout to leave time to the view to be rendered.
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            if (!this.viewDestroyed) {
 | 
			
		||||
                this.content?.scrollToBottom();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        await CoreUtils.nextTick();
 | 
			
		||||
        if (!this.viewDestroyed) {
 | 
			
		||||
            this.content?.scrollToBottom();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -352,9 +352,7 @@ export class AddonModDataEditPage implements OnInit {
 | 
			
		||||
                    }
 | 
			
		||||
                    this.jsData!.errors = this.errors;
 | 
			
		||||
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        this.scrollToFirstError();
 | 
			
		||||
                    });
 | 
			
		||||
                    this.scrollToFirstError();
 | 
			
		||||
                }
 | 
			
		||||
            } finally {
 | 
			
		||||
                modal.dismiss();
 | 
			
		||||
@ -449,8 +447,9 @@ export class AddonModDataEditPage implements OnInit {
 | 
			
		||||
    /**
 | 
			
		||||
     * Scroll to first error or to the top if not found.
 | 
			
		||||
     */
 | 
			
		||||
    protected scrollToFirstError(): void {
 | 
			
		||||
        if (!CoreDomUtils.scrollToElementBySelector(this.formElement.nativeElement, this.content, '.addon-data-error')) {
 | 
			
		||||
    protected async scrollToFirstError(): Promise<void> {
 | 
			
		||||
        const scrolled = await CoreDomUtils.scrollViewToElement(this.formElement.nativeElement, '.addon-data-error');
 | 
			
		||||
        if (!scrolled) {
 | 
			
		||||
            this.content?.scrollToTop();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,6 @@ import {
 | 
			
		||||
    OnChanges,
 | 
			
		||||
    OnDestroy,
 | 
			
		||||
    OnInit,
 | 
			
		||||
    Optional,
 | 
			
		||||
    Output,
 | 
			
		||||
    SimpleChange,
 | 
			
		||||
    ViewChild,
 | 
			
		||||
@ -41,7 +40,6 @@ import {
 | 
			
		||||
import { CoreTag } from '@features/tag/services/tag';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { AddonModForumSync } from '../../services/forum-sync';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
@ -94,7 +92,6 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected elementRef: ElementRef,
 | 
			
		||||
        @Optional() protected content?: IonContent,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    get showForm(): boolean {
 | 
			
		||||
@ -308,8 +305,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
 | 
			
		||||
                this.post.id > 0 ? this.post.id : undefined,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            this.scrollToForm(5);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            this.scrollToForm();
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Cancelled.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -540,19 +537,11 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
 | 
			
		||||
    /**
 | 
			
		||||
     * Scroll to reply/edit form.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ticksToWait Number of ticks to wait before scrolling.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async scrollToForm(ticksToWait = 1): Promise<void> {
 | 
			
		||||
        if (!this.content) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.nextTicks(ticksToWait);
 | 
			
		||||
 | 
			
		||||
        CoreDomUtils.scrollToElementBySelector(
 | 
			
		||||
    protected async scrollToForm(): Promise<void> {
 | 
			
		||||
        await CoreDomUtils.scrollViewToElement(
 | 
			
		||||
            this.elementRef.nativeElement,
 | 
			
		||||
            this.content,
 | 
			
		||||
            '#addon-forum-reply-edit-form-' + this.uniqueId,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -187,13 +187,10 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
 | 
			
		||||
        const scrollTo = this.postId || this.parent;
 | 
			
		||||
        if (scrollTo) {
 | 
			
		||||
            // Scroll to the post.
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                CoreDomUtils.scrollToElementBySelector(
 | 
			
		||||
                    this.elementRef.nativeElement,
 | 
			
		||||
                    this.content,
 | 
			
		||||
                    '#addon-mod_forum-post-' + scrollTo,
 | 
			
		||||
                );
 | 
			
		||||
            });
 | 
			
		||||
            CoreDomUtils.scrollViewToElement(
 | 
			
		||||
                this.elementRef.nativeElement,
 | 
			
		||||
                '#addon-mod_forum-post-' + scrollTo,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,6 @@
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit, ViewChild, ElementRef, Input, Type } from '@angular/core';
 | 
			
		||||
import { FormBuilder, FormGroup } from '@angular/forms';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
@ -32,7 +31,6 @@ import { AddonModQuizAttemptWSData, AddonModQuizQuizWSData } from '../../service
 | 
			
		||||
})
 | 
			
		||||
export class AddonModQuizPreflightModalComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(IonContent) content?: IonContent;
 | 
			
		||||
    @ViewChild('preflightFormEl') formElement?: ElementRef;
 | 
			
		||||
 | 
			
		||||
    @Input() title!: string;
 | 
			
		||||
@ -111,15 +109,14 @@ export class AddonModQuizPreflightModalComponent implements OnInit {
 | 
			
		||||
     *
 | 
			
		||||
     * @param e Event.
 | 
			
		||||
     */
 | 
			
		||||
    sendData(e: Event): void {
 | 
			
		||||
    async sendData(e: Event): Promise<void> {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        if (!this.preflightForm.valid) {
 | 
			
		||||
            // Form not valid. Scroll to the first element with errors.
 | 
			
		||||
            const hasScrolled = CoreDomUtils.scrollToInputError(
 | 
			
		||||
            const hasScrolled = await CoreDomUtils.scrollViewToInputError(
 | 
			
		||||
                this.elementRef.nativeElement,
 | 
			
		||||
                this.content,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (!hasScrolled) {
 | 
			
		||||
 | 
			
		||||
@ -318,10 +318,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
 | 
			
		||||
            if (slot !== undefined) {
 | 
			
		||||
                // Scroll to the question. Give some time to the questions to render.
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    this.scrollToQuestion(slot);
 | 
			
		||||
                }, 2000);
 | 
			
		||||
                // Scroll to the question.
 | 
			
		||||
                this.scrollToQuestion(slot);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -689,9 +687,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
 | 
			
		||||
     * @param slot Slot of the question to scroll to.
 | 
			
		||||
     */
 | 
			
		||||
    protected scrollToQuestion(slot: number): void {
 | 
			
		||||
        CoreDomUtils.scrollToElementBySelector(
 | 
			
		||||
        CoreDomUtils.scrollViewToElement(
 | 
			
		||||
            this.elementRef.nativeElement,
 | 
			
		||||
            this.content,
 | 
			
		||||
            '#addon-mod_quiz-question-' + slot,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -133,10 +133,8 @@ export class AddonModQuizReviewPage implements OnInit {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
 | 
			
		||||
            if (slot !== undefined) {
 | 
			
		||||
                // Scroll to the question. Give some time to the questions to render.
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    this.scrollToQuestion(slot);
 | 
			
		||||
                }, 2000);
 | 
			
		||||
                // Scroll to the question.
 | 
			
		||||
                this.scrollToQuestion(slot);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -249,9 +247,8 @@ export class AddonModQuizReviewPage implements OnInit {
 | 
			
		||||
     * @param slot Slot of the question to scroll to.
 | 
			
		||||
     */
 | 
			
		||||
    protected scrollToQuestion(slot: number): void {
 | 
			
		||||
        CoreDomUtils.scrollToElementBySelector(
 | 
			
		||||
        CoreDomUtils.scrollViewToElement(
 | 
			
		||||
            this.elementRef.nativeElement,
 | 
			
		||||
            this.content,
 | 
			
		||||
            `#addon-mod_quiz-question-${slot}`,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -78,13 +78,18 @@ export class AddonQtypeDdImageOrTextQuestion {
 | 
			
		||||
            return bgImgXY;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const position = CoreDomUtils.getElementXY(bgImg, undefined, 'ddarea');
 | 
			
		||||
        const ddArea = this.container.querySelector<HTMLElement>('.ddarea');
 | 
			
		||||
        if (!ddArea) {
 | 
			
		||||
            return bgImgXY;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const position = CoreDomUtils.getRelativeElementPosition(bgImg, ddArea);
 | 
			
		||||
 | 
			
		||||
        // Render the position related to the current image dimensions.
 | 
			
		||||
        bgImgXY[0] *= this.proportion;
 | 
			
		||||
        bgImgXY[1] *= this.proportion;
 | 
			
		||||
 | 
			
		||||
        return [Number(bgImgXY[0]) + position[0] + 1, Number(bgImgXY[1]) + position[1] + 1];
 | 
			
		||||
        return [bgImgXY[0] + position.x + 1, bgImgXY[1] + position.y + 1];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -360,7 +365,7 @@ export class AddonQtypeDdImageOrTextQuestion {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.resizeListener = CoreDomUtils.onWindowResize(() => {
 | 
			
		||||
            this.windowResized();
 | 
			
		||||
            this.repositionDragsForQuestion();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -409,10 +414,15 @@ export class AddonQtypeDdImageOrTextQuestion {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Now position the draggable and set it to the input.
 | 
			
		||||
        const position = CoreDomUtils.getElementXY(drop, undefined, 'ddarea');
 | 
			
		||||
        const ddArea = this.container.querySelector<HTMLElement>('.ddarea');
 | 
			
		||||
        if (!ddArea) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const position = CoreDomUtils.getRelativeElementPosition(drop, ddArea);
 | 
			
		||||
        const choice = drag.getAttribute('choice');
 | 
			
		||||
        drag.style.left = position[0] - 1 + 'px';
 | 
			
		||||
        drag.style.top = position[1] - 1 + 'px';
 | 
			
		||||
        drag.style.left = position.x + 'px';
 | 
			
		||||
        drag.style.top = position.y + 'px';
 | 
			
		||||
        drag.classList.add('placed');
 | 
			
		||||
 | 
			
		||||
        if (choice) {
 | 
			
		||||
@ -458,13 +468,14 @@ export class AddonQtypeDdImageOrTextQuestion {
 | 
			
		||||
 | 
			
		||||
        // Move the element to its original position.
 | 
			
		||||
        const dragItemHome = this.doc.dragItemHome(Number(drag.getAttribute('dragitemno')));
 | 
			
		||||
        if (!dragItemHome) {
 | 
			
		||||
        const ddArea = this.container.querySelector<HTMLElement>('.ddarea');
 | 
			
		||||
        if (!dragItemHome || !ddArea) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const position = CoreDomUtils.getElementXY(dragItemHome, undefined, 'ddarea');
 | 
			
		||||
        drag.style.left = position[0] + 'px';
 | 
			
		||||
        drag.style.top = position[1] + 'px';
 | 
			
		||||
        const position = CoreDomUtils.getRelativeElementPosition(dragItemHome, ddArea);
 | 
			
		||||
        drag.style.left = position.x + 'px';
 | 
			
		||||
        drag.style.top = position.y + 'px';
 | 
			
		||||
        drag.classList.remove('placed');
 | 
			
		||||
 | 
			
		||||
        drag.setAttribute('inputid', '');
 | 
			
		||||
@ -679,15 +690,6 @@ export class AddonQtypeDdImageOrTextQuestion {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Window resized.
 | 
			
		||||
     */
 | 
			
		||||
    async windowResized(): Promise<void> {
 | 
			
		||||
        await CoreDomUtils.waitForResizeDone();
 | 
			
		||||
 | 
			
		||||
        this.repositionDragsForQuestion();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -12,21 +12,13 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreCoordinates, CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreEventObserver } from '@singletons/events';
 | 
			
		||||
import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
import { AddonQtypeDdMarkerQuestionData } from '../component/ddmarker';
 | 
			
		||||
import { AddonQtypeDdMarkerGraphicsApi } from './graphics_api';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Point type.
 | 
			
		||||
 */
 | 
			
		||||
export type AddonQtypeDdMarkerQuestionPoint = {
 | 
			
		||||
    x: number; // X axis coordinates.
 | 
			
		||||
    y: number; // Y axis coordinates.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class to make a question of ddmarker type work.
 | 
			
		||||
 */
 | 
			
		||||
@ -36,8 +28,7 @@ export class AddonQtypeDdMarkerQuestion {
 | 
			
		||||
 | 
			
		||||
    protected logger: CoreLogger;
 | 
			
		||||
    protected afterImageLoadDone = false;
 | 
			
		||||
    protected drops;
 | 
			
		||||
    protected topNode;
 | 
			
		||||
    protected topNode?: HTMLElement | null;
 | 
			
		||||
    protected nextColourIndex = 0;
 | 
			
		||||
    protected proportion = 1;
 | 
			
		||||
    protected selected?: HTMLElement; // Selected element (being "dragged").
 | 
			
		||||
@ -123,7 +114,7 @@ export class AddonQtypeDdMarkerQuestion {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const position = CoreDomUtils.getElementXY(bgImg, undefined, 'ddarea');
 | 
			
		||||
        const position = this.getElementCoordinates(bgImg);
 | 
			
		||||
        let coordsNumbers = this.parsePoint(bgImgXY);
 | 
			
		||||
 | 
			
		||||
        coordsNumbers = this.makePointProportional(coordsNumbers);
 | 
			
		||||
@ -131,13 +122,30 @@ export class AddonQtypeDdMarkerQuestion {
 | 
			
		||||
        return [coordsNumbers.x + position[0], coordsNumbers.y + position[1]];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns elements coordinates relative to ddarea container.
 | 
			
		||||
     *
 | 
			
		||||
     * @param element Element.
 | 
			
		||||
     * @return Array of X and Y coordinates.
 | 
			
		||||
     */
 | 
			
		||||
    protected getElementCoordinates(element: HTMLElement): number[] {
 | 
			
		||||
        const ddArea = this.container.querySelector<HTMLElement>('.ddarea');
 | 
			
		||||
        if (!ddArea) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const position = CoreDomUtils.getRelativeElementPosition(element, ddArea);
 | 
			
		||||
 | 
			
		||||
        return [position.x, position.y];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if some coordinates (X, Y) are inside the background image.
 | 
			
		||||
     *
 | 
			
		||||
     * @param coords Coordinates to check.
 | 
			
		||||
     * @return Whether they're inside the background image.
 | 
			
		||||
     */
 | 
			
		||||
    coordsInImg(coords: AddonQtypeDdMarkerQuestionPoint): boolean {
 | 
			
		||||
    coordsInImg(coords: CoreCoordinates): boolean {
 | 
			
		||||
        const bgImg = this.doc.bgImg();
 | 
			
		||||
        if (!bgImg) {
 | 
			
		||||
            return false;
 | 
			
		||||
@ -177,13 +185,13 @@ export class AddonQtypeDdMarkerQuestion {
 | 
			
		||||
            const dragging = this.selected;
 | 
			
		||||
            if (dragging && !drag.classList.contains('unplaced')) {
 | 
			
		||||
 | 
			
		||||
                const position = CoreDomUtils.getElementXY(drag, undefined, 'ddarea');
 | 
			
		||||
                const position = this.getElementCoordinates(drag);
 | 
			
		||||
                const bgImg = this.doc.bgImg();
 | 
			
		||||
                if (!bgImg) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const bgImgPos = CoreDomUtils.getElementXY(bgImg, undefined, 'ddarea');
 | 
			
		||||
                const bgImgPos = this.getElementCoordinates(bgImg);
 | 
			
		||||
 | 
			
		||||
                position[0] = position[0] - bgImgPos[0] + e.offsetX;
 | 
			
		||||
                position[1] = position[1] - bgImgPos[1] + e.offsetY;
 | 
			
		||||
@ -217,7 +225,7 @@ export class AddonQtypeDdMarkerQuestion {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const position = CoreDomUtils.getElementXY(dragItemHome, undefined, 'ddarea');
 | 
			
		||||
        const position = this.getElementCoordinates(dragItemHome);
 | 
			
		||||
 | 
			
		||||
        return [position[0], position[1]];
 | 
			
		||||
    }
 | 
			
		||||
@ -317,7 +325,7 @@ export class AddonQtypeDdMarkerQuestion {
 | 
			
		||||
     * @param colour Colour of the circle.
 | 
			
		||||
     * @return X and Y position of the center of the circle.
 | 
			
		||||
     */
 | 
			
		||||
    drawShapeCircle(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint | null {
 | 
			
		||||
    drawShapeCircle(dropZoneNo: number, coordinates: string, colour: string): CoreCoordinates | null {
 | 
			
		||||
        if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?$/)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
@ -356,7 +364,7 @@ export class AddonQtypeDdMarkerQuestion {
 | 
			
		||||
     * @param colour Colour of the rectangle.
 | 
			
		||||
     * @return X and Y position of the center of the rectangle.
 | 
			
		||||
     */
 | 
			
		||||
    drawShapeRectangle(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint | null {
 | 
			
		||||
    drawShapeRectangle(dropZoneNo: number, coordinates: string, colour: string): CoreCoordinates | null {
 | 
			
		||||
        if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?,\d+(\.\d+)?$/)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
@ -399,7 +407,7 @@ export class AddonQtypeDdMarkerQuestion {
 | 
			
		||||
     * @param colour Colour of the polygon.
 | 
			
		||||
     * @return X and Y position of the center of the polygon.
 | 
			
		||||
     */
 | 
			
		||||
    drawShapePolygon(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint | null {
 | 
			
		||||
    drawShapePolygon(dropZoneNo: number, coordinates: string, colour: string): CoreCoordinates | null {
 | 
			
		||||
        if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?(?:;\d+(\.\d+)?,\d+(\.\d+)?)*$/)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
@ -449,7 +457,7 @@ export class AddonQtypeDdMarkerQuestion {
 | 
			
		||||
     * @param coordinates "x,y".
 | 
			
		||||
     * @return Coordinates to the point.
 | 
			
		||||
     */
 | 
			
		||||
    parsePoint(coordinates: string): AddonQtypeDdMarkerQuestionPoint {
 | 
			
		||||
    parsePoint(coordinates: string): CoreCoordinates {
 | 
			
		||||
        const bits = coordinates.split(',');
 | 
			
		||||
        if (bits.length !== 2) {
 | 
			
		||||
            throw coordinates + ' is not a valid point';
 | 
			
		||||
@ -464,7 +472,7 @@ export class AddonQtypeDdMarkerQuestion {
 | 
			
		||||
     * @param point Point coordinates.
 | 
			
		||||
     * @return Converted point.
 | 
			
		||||
     */
 | 
			
		||||
    makePointProportional(point: AddonQtypeDdMarkerQuestionPoint): AddonQtypeDdMarkerQuestionPoint {
 | 
			
		||||
    makePointProportional(point: CoreCoordinates): CoreCoordinates {
 | 
			
		||||
        return {
 | 
			
		||||
            x: Math.round(point.x * this.proportion),
 | 
			
		||||
            y: Math.round(point.y * this.proportion),
 | 
			
		||||
@ -542,10 +550,10 @@ export class AddonQtypeDdMarkerQuestion {
 | 
			
		||||
     * @return Coordinates.
 | 
			
		||||
     */
 | 
			
		||||
    getDragXY(dragItem: HTMLElement): number[] {
 | 
			
		||||
        const position = CoreDomUtils.getElementXY(dragItem, undefined, 'ddarea');
 | 
			
		||||
        const position = this.getElementCoordinates(dragItem);
 | 
			
		||||
        const bgImg = this.doc.bgImg();
 | 
			
		||||
        if (bgImg) {
 | 
			
		||||
            const bgImgXY = CoreDomUtils.getElementXY(bgImg, undefined, 'ddarea');
 | 
			
		||||
            const bgImgXY = this.getElementCoordinates(bgImg);
 | 
			
		||||
 | 
			
		||||
            position[0] -= bgImgXY[0];
 | 
			
		||||
            position[1] -= bgImgXY[1];
 | 
			
		||||
@ -611,7 +619,7 @@ export class AddonQtypeDdMarkerQuestion {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.resizeListener = CoreDomUtils.onWindowResize(() => {
 | 
			
		||||
            this.windowResized();
 | 
			
		||||
            this.redrawDragsAndDrops();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -879,15 +887,6 @@ export class AddonQtypeDdMarkerQuestion {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Window resized.
 | 
			
		||||
     */
 | 
			
		||||
    async windowResized(): Promise<void> {
 | 
			
		||||
        await CoreDomUtils.waitForResizeDone();
 | 
			
		||||
 | 
			
		||||
        this.redrawDragsAndDrops();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -23,12 +23,6 @@ export class AddonQtypeDdMarkerGraphicsApi {
 | 
			
		||||
    protected readonly NS = 'http://www.w3.org/2000/svg';
 | 
			
		||||
    protected dropZone?: SVGSVGElement;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create the instance.
 | 
			
		||||
     *
 | 
			
		||||
     * @param instance Question instance.
 | 
			
		||||
     * @param domUtils Dom Utils provider.
 | 
			
		||||
     */
 | 
			
		||||
    constructor(protected instance: AddonQtypeDdMarkerQuestion) { }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -60,20 +54,20 @@ export class AddonQtypeDdMarkerGraphicsApi {
 | 
			
		||||
        const bgImg = this.instance.doc?.bgImg();
 | 
			
		||||
        const dropZones = this.instance.doc?.topNode?.querySelector<HTMLElement>('div.ddarea div.dropzones');
 | 
			
		||||
        const markerTexts = this.instance.doc?.markerTexts();
 | 
			
		||||
 | 
			
		||||
        if (!bgImg || !dropZones || !markerTexts) {
 | 
			
		||||
        const ddArea = this.instance.doc?.topNode?.querySelector<HTMLElement>('.ddarea');
 | 
			
		||||
        if (!bgImg || !dropZones || !markerTexts || !ddArea) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const position = CoreDomUtils.getElementXY(bgImg, undefined, 'ddarea');
 | 
			
		||||
        const position = CoreDomUtils.getRelativeElementPosition(bgImg, ddArea);
 | 
			
		||||
 | 
			
		||||
        dropZones.style.left = position[0] + 'px';
 | 
			
		||||
        dropZones.style.top = position[1] + 'px';
 | 
			
		||||
        dropZones.style.left = position.x + 'px';
 | 
			
		||||
        dropZones.style.top = position.y + 'px';
 | 
			
		||||
        dropZones.style.width = bgImg.width + 'px';
 | 
			
		||||
        dropZones.style.height = bgImg.height + 'px';
 | 
			
		||||
 | 
			
		||||
        markerTexts.style.left = position[0] + 'px';
 | 
			
		||||
        markerTexts.style.top = position[1] + 'px';
 | 
			
		||||
        markerTexts.style.left = position.x + 'px';
 | 
			
		||||
        markerTexts.style.top = position.y + 'px';
 | 
			
		||||
        markerTexts.style.width = bgImg.width + 'px';
 | 
			
		||||
        markerTexts.style.height = bgImg.height + 'px';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,9 +12,11 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreFormatTextDirective } from '@directives/format-text';
 | 
			
		||||
import { CoreCoordinates, CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
 | 
			
		||||
import { CoreEventObserver } from '@singletons/events';
 | 
			
		||||
import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
import { AddonModQuizDdwtosQuestionData } from '../component/ddwtos';
 | 
			
		||||
@ -26,7 +28,7 @@ export class AddonQtypeDdwtosQuestion {
 | 
			
		||||
 | 
			
		||||
    protected logger: CoreLogger;
 | 
			
		||||
    protected nextDragItemNo = 1;
 | 
			
		||||
    protected selectors!: AddonQtypeDdwtosQuestionCSSSelectors; // Result of cssSelectors.
 | 
			
		||||
    protected selectors = new AddonQtypeDdwtosQuestionCSSSelectors(); // Result of cssSelectors.
 | 
			
		||||
    protected placed: {[no: number]: number} = {}; // Map that relates drag elements numbers with drop zones numbers.
 | 
			
		||||
    protected selected?: HTMLElement; // Selected element (being "dragged").
 | 
			
		||||
    protected resizeListener?: CoreEventObserver;
 | 
			
		||||
@ -80,8 +82,8 @@ export class AddonQtypeDdwtosQuestion {
 | 
			
		||||
     * Invisible 'drag homes' are output in the question. These have the same properties as the drag items but are invisible.
 | 
			
		||||
     * We clone these invisible elements to make the actual drag items.
 | 
			
		||||
     */
 | 
			
		||||
    cloneDragItems(): void {
 | 
			
		||||
        const dragHomes = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.dragHomes()));
 | 
			
		||||
    async cloneDragItems(): Promise<void> {
 | 
			
		||||
        const dragHomes = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.dragHomes()));
 | 
			
		||||
        for (let x = 0; x < dragHomes.length; x++) {
 | 
			
		||||
            this.cloneDragItemsForOneChoice(dragHomes[x]);
 | 
			
		||||
        }
 | 
			
		||||
@ -110,7 +112,7 @@ export class AddonQtypeDdwtosQuestion {
 | 
			
		||||
     */
 | 
			
		||||
    deselectDrags(): void {
 | 
			
		||||
        // Remove the selected class from all drags.
 | 
			
		||||
        const drags = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.drags()));
 | 
			
		||||
        const drags = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.drags()));
 | 
			
		||||
        drags.forEach((drag) => {
 | 
			
		||||
            drag.classList.remove('selected');
 | 
			
		||||
        });
 | 
			
		||||
@ -192,19 +194,13 @@ export class AddonQtypeDdwtosQuestion {
 | 
			
		||||
     * Initialize the question.
 | 
			
		||||
     */
 | 
			
		||||
    async initializer(): Promise<void> {
 | 
			
		||||
        this.selectors = new AddonQtypeDdwtosQuestionCSSSelectors();
 | 
			
		||||
 | 
			
		||||
        const container = <HTMLElement> this.container.querySelector(this.selectors.topNode());
 | 
			
		||||
        if (this.readOnly) {
 | 
			
		||||
            container.classList.add('readonly');
 | 
			
		||||
        } else {
 | 
			
		||||
            container.classList.add('notreadonly');
 | 
			
		||||
        }
 | 
			
		||||
        const container = this.container.querySelector<HTMLElement>(this.selectors.topNode());
 | 
			
		||||
        container?.classList.add(this.readOnly ? 'readonly' : 'notreadonly');
 | 
			
		||||
 | 
			
		||||
        // Wait for the elements to be ready.
 | 
			
		||||
        await this.waitForReady();
 | 
			
		||||
 | 
			
		||||
        this.setPaddingSizesAll();
 | 
			
		||||
        await this.setPaddingSizesAll();
 | 
			
		||||
        this.cloneDragItems();
 | 
			
		||||
        this.initialPlaceOfDragItems();
 | 
			
		||||
        this.makeDropZones();
 | 
			
		||||
@ -212,7 +208,7 @@ export class AddonQtypeDdwtosQuestion {
 | 
			
		||||
        this.positionDragItems();
 | 
			
		||||
 | 
			
		||||
        this.resizeListener = CoreDomUtils.onWindowResize(() => {
 | 
			
		||||
            this.windowResized();
 | 
			
		||||
            this.positionDragItems();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -220,7 +216,7 @@ export class AddonQtypeDdwtosQuestion {
 | 
			
		||||
     * Initialize drag items, putting them in their initial place.
 | 
			
		||||
     */
 | 
			
		||||
    initialPlaceOfDragItems(): void {
 | 
			
		||||
        const drags = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.drags()));
 | 
			
		||||
        const drags = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.drags()));
 | 
			
		||||
 | 
			
		||||
        // Add the class 'unplaced' to all elements.
 | 
			
		||||
        drags.forEach((drag) => {
 | 
			
		||||
@ -292,15 +288,15 @@ export class AddonQtypeDdwtosQuestion {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create all the drop zones.
 | 
			
		||||
        const drops = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.drops()));
 | 
			
		||||
        const drops = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.drops()));
 | 
			
		||||
        drops.forEach((drop) => {
 | 
			
		||||
            this.makeDropZone(drop);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // If home answer zone is clicked, return drag home.
 | 
			
		||||
        const home = <HTMLElement> this.container.querySelector(this.selectors.topNode() + ' .answercontainer');
 | 
			
		||||
        const home = this.container.querySelector<HTMLElement>(this.selectors.topNode() + ' .answercontainer');
 | 
			
		||||
 | 
			
		||||
        home.addEventListener('click', () => {
 | 
			
		||||
        home?.addEventListener('click', () => {
 | 
			
		||||
            const drag = this.selected;
 | 
			
		||||
            if (!drag) {
 | 
			
		||||
                // No element selected, nothing to do.
 | 
			
		||||
@ -379,33 +375,37 @@ export class AddonQtypeDdwtosQuestion {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let position;
 | 
			
		||||
 | 
			
		||||
        const placeNo = this.placed[this.getNo(drag) ?? -1];
 | 
			
		||||
        const parent = this.container.querySelector<HTMLElement>('.addon-qtype-ddwtos-container');
 | 
			
		||||
        if (!parent) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let position: CoreCoordinates | undefined;
 | 
			
		||||
 | 
			
		||||
        if (!placeNo) {
 | 
			
		||||
            // Not placed, put it in home zone.
 | 
			
		||||
            const groupNo = this.getGroup(drag) ?? -1;
 | 
			
		||||
            const choiceNo = this.getChoice(drag) ?? -1;
 | 
			
		||||
 | 
			
		||||
            position = CoreDomUtils.getElementXY(
 | 
			
		||||
                this.container,
 | 
			
		||||
                this.selectors.dragHome(groupNo, choiceNo),
 | 
			
		||||
                'answercontainer',
 | 
			
		||||
            );
 | 
			
		||||
            drag.classList.add('unplaced');
 | 
			
		||||
            const dragHome = this.container.querySelector<HTMLElement>(this.selectors.dragHome(groupNo, choiceNo));
 | 
			
		||||
            if (dragHome) {
 | 
			
		||||
                position = CoreDomUtils.getRelativeElementPosition(dragHome, parent);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // Get the drop zone position.
 | 
			
		||||
            position = CoreDomUtils.getElementXY(
 | 
			
		||||
                this.container,
 | 
			
		||||
                this.selectors.dropForPlace(placeNo),
 | 
			
		||||
                'addon-qtype-ddwtos-container',
 | 
			
		||||
            );
 | 
			
		||||
            drag.classList.remove('unplaced');
 | 
			
		||||
            const dropZone = this.container.querySelector<HTMLElement>(this.selectors.dropForPlace(placeNo));
 | 
			
		||||
            if (dropZone) {
 | 
			
		||||
                position = CoreDomUtils.getRelativeElementPosition(dropZone, parent);
 | 
			
		||||
                // Avoid the border.
 | 
			
		||||
                position.x++;
 | 
			
		||||
                position.y++;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        drag.classList.toggle('unplaced', !placeNo);
 | 
			
		||||
 | 
			
		||||
        if (position) {
 | 
			
		||||
            drag.style.left = position[0] + 'px';
 | 
			
		||||
            drag.style.top = position[1] + 'px';
 | 
			
		||||
            drag.style.left = position.x + 'px';
 | 
			
		||||
            drag.style.top = position.y + 'px';
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -413,36 +413,25 @@ export class AddonQtypeDdwtosQuestion {
 | 
			
		||||
     * Postition, or reposition, all the drag items. They're placed in the right drop zone or in the home zone.
 | 
			
		||||
     */
 | 
			
		||||
    positionDragItems(): void {
 | 
			
		||||
        const drags = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.drags()));
 | 
			
		||||
        const drags = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.drags()));
 | 
			
		||||
        drags.forEach((drag) => {
 | 
			
		||||
            this.positionDragItem(drag);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Wait for the drag items to have an offsetParent. For some reason it takes a while.
 | 
			
		||||
     * Wait for the drag home items to be in DOM.
 | 
			
		||||
     *
 | 
			
		||||
     * @param retries Number of times this has been retried.
 | 
			
		||||
     * @return Promise resolved when ready or if it took too long to load.
 | 
			
		||||
     * @return Promise resolved when ready in the DOM.
 | 
			
		||||
     */
 | 
			
		||||
    protected async waitForReady(retries: number = 0): Promise<void> {
 | 
			
		||||
        const drag = <HTMLElement | null> Array.from(this.container.querySelectorAll(this.selectors.drags()))[0];
 | 
			
		||||
        if (drag?.offsetParent || retries >= 10) {
 | 
			
		||||
            // Ready or too many retries, stop.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    protected async waitForReady(): Promise<void> {
 | 
			
		||||
        await CoreDomUtils.waitToBeInDOM(this.container);
 | 
			
		||||
 | 
			
		||||
        const deferred = CoreUtils.promiseDefer<void>();
 | 
			
		||||
        await CoreComponentsRegistry.waitComponentsReady(this.container, 'core-format-text', CoreFormatTextDirective);
 | 
			
		||||
 | 
			
		||||
        setTimeout(async () => {
 | 
			
		||||
            try {
 | 
			
		||||
                await this.waitForReady(retries + 1);
 | 
			
		||||
            } finally {
 | 
			
		||||
                deferred.resolve();
 | 
			
		||||
            }
 | 
			
		||||
        }, 20);
 | 
			
		||||
        const drag = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.dragHomes()))[0];
 | 
			
		||||
 | 
			
		||||
        return deferred.promise;
 | 
			
		||||
        await CoreDomUtils.waitToBeInDOM(drag);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -452,7 +441,7 @@ export class AddonQtypeDdwtosQuestion {
 | 
			
		||||
     */
 | 
			
		||||
    removeDragFromDrop(drag: HTMLElement): void {
 | 
			
		||||
        const placeNo = this.placed[this.getNo(drag) ?? -1];
 | 
			
		||||
        const drop = <HTMLElement> this.container.querySelector(this.selectors.dropForPlace(placeNo));
 | 
			
		||||
        const drop = this.container.querySelector<HTMLElement>(this.selectors.dropForPlace(placeNo));
 | 
			
		||||
 | 
			
		||||
        this.placeDragInDrop(null, drop);
 | 
			
		||||
    }
 | 
			
		||||
@ -473,9 +462,9 @@ export class AddonQtypeDdwtosQuestion {
 | 
			
		||||
    /**
 | 
			
		||||
     * Set the padding size for all groups.
 | 
			
		||||
     */
 | 
			
		||||
    setPaddingSizesAll(): void {
 | 
			
		||||
    async setPaddingSizesAll(): Promise<void> {
 | 
			
		||||
        for (let groupNo = 1; groupNo <= 8; groupNo++) {
 | 
			
		||||
            this.setPaddingSizeForGroup(groupNo);
 | 
			
		||||
            await this.setPaddingSizeForGroup(groupNo);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -484,19 +473,25 @@ export class AddonQtypeDdwtosQuestion {
 | 
			
		||||
     *
 | 
			
		||||
     * @param groupNo Group number.
 | 
			
		||||
     */
 | 
			
		||||
    setPaddingSizeForGroup(groupNo: number): void {
 | 
			
		||||
        const groupItems = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.dragHomesGroup(groupNo)));
 | 
			
		||||
    async setPaddingSizeForGroup(groupNo: number): Promise<void> {
 | 
			
		||||
        const groupItems = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.dragHomesGroup(groupNo)));
 | 
			
		||||
 | 
			
		||||
        if (!groupItems.length) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreDomUtils.waitToBeInDOM(groupItems[0]);
 | 
			
		||||
 | 
			
		||||
        let maxWidth = 0;
 | 
			
		||||
        let maxHeight = 0;
 | 
			
		||||
 | 
			
		||||
        // Find max height and width.
 | 
			
		||||
        groupItems.forEach((item) => {
 | 
			
		||||
            item.innerHTML = CoreTextUtils.decodeHTML(item.innerHTML);
 | 
			
		||||
        });
 | 
			
		||||
        // Wait to render in order to calculate size.
 | 
			
		||||
        await CoreUtils.nextTick();
 | 
			
		||||
 | 
			
		||||
        groupItems.forEach((item) => {
 | 
			
		||||
            maxWidth = Math.max(maxWidth, Math.ceil(item.offsetWidth));
 | 
			
		||||
            maxHeight = Math.max(maxHeight, Math.ceil(item.offsetHeight));
 | 
			
		||||
        });
 | 
			
		||||
@ -507,21 +502,12 @@ export class AddonQtypeDdwtosQuestion {
 | 
			
		||||
            this.padToWidthHeight(item, maxWidth, maxHeight);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const dropsGroup = <HTMLElement[]> Array.from(this.container.querySelectorAll(this.selectors.dropsGroup(groupNo)));
 | 
			
		||||
        const dropsGroup = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.dropsGroup(groupNo)));
 | 
			
		||||
        dropsGroup.forEach((item) => {
 | 
			
		||||
            this.padToWidthHeight(item, maxWidth + 2, maxHeight + 2);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Window resized.
 | 
			
		||||
     */
 | 
			
		||||
    async windowResized(): Promise<void> {
 | 
			
		||||
        await CoreDomUtils.waitForResizeDone();
 | 
			
		||||
 | 
			
		||||
        this.positionDragItems();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -103,7 +103,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
 | 
			
		||||
                    selector += '[slot="' + slot + '"]';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const buttonsContainer = <HTMLElement> header.querySelector(selector);
 | 
			
		||||
                const buttonsContainer = header.querySelector<HTMLIonButtonsElement>(selector);
 | 
			
		||||
                if (buttonsContainer) {
 | 
			
		||||
                    this.mergeContextMenus(buttonsContainer);
 | 
			
		||||
 | 
			
		||||
@ -147,7 +147,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
 | 
			
		||||
     *
 | 
			
		||||
     * @param buttonsContainer The container where the buttons will be moved.
 | 
			
		||||
     */
 | 
			
		||||
    protected mergeContextMenus(buttonsContainer: HTMLElement): void {
 | 
			
		||||
    protected mergeContextMenus(buttonsContainer: HTMLIonButtonsElement): void {
 | 
			
		||||
        // Check if both button containers have a context menu.
 | 
			
		||||
        const secondaryContextMenu = this.element.querySelector('core-context-menu');
 | 
			
		||||
        if (!secondaryContextMenu) {
 | 
			
		||||
@ -194,60 +194,26 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
 | 
			
		||||
    /**
 | 
			
		||||
     * Search the ion-header where the buttons should be added.
 | 
			
		||||
     *
 | 
			
		||||
     * @param retries Number of retries so far.
 | 
			
		||||
     * @return Promise resolved with the header element.
 | 
			
		||||
     */
 | 
			
		||||
    protected async searchHeader(retries: number = 0): Promise<HTMLElement> {
 | 
			
		||||
    protected async searchHeader(): Promise<HTMLIonHeaderElement> {
 | 
			
		||||
        await CoreDomUtils.waitToBeInDOM(this.element);
 | 
			
		||||
 | 
			
		||||
        let parentPage: HTMLElement | null = this.element;
 | 
			
		||||
 | 
			
		||||
        while (parentPage) {
 | 
			
		||||
            if (!parentPage.parentElement) {
 | 
			
		||||
                // No parent, stop.
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        while (parentPage && parentPage.parentElement) {
 | 
			
		||||
            // Get the next parent page.
 | 
			
		||||
            parentPage = parentPage.parentElement.closest('.ion-page');
 | 
			
		||||
            if (parentPage) {
 | 
			
		||||
                // Check if the page has a header. If it doesn't, search the next parent page.
 | 
			
		||||
                const header = this.searchHeaderInPage(parentPage);
 | 
			
		||||
                if (header && getComputedStyle(header, null).display != 'none') {
 | 
			
		||||
                    return header;
 | 
			
		||||
                }
 | 
			
		||||
            // Check if the page has a header. If it doesn't, search the next parent page.
 | 
			
		||||
            const header  = parentPage?.querySelector<HTMLIonHeaderElement>(':scope > ion-header');
 | 
			
		||||
            if (header && getComputedStyle(header).display !== 'none') {
 | 
			
		||||
                return header;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Header not found.
 | 
			
		||||
        if (retries < 5) {
 | 
			
		||||
            // If the component or any of its parent is inside a ng-content or similar it can be detached when it's initialized.
 | 
			
		||||
            // Try again after a while.
 | 
			
		||||
            return new Promise((resolve, reject): void => {
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    // eslint-disable-next-line promise/catch-or-return
 | 
			
		||||
                    this.searchHeader(retries + 1).then(resolve, reject);
 | 
			
		||||
                }, 200);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // We've waited enough time, reject.
 | 
			
		||||
        // Header not found, reject.
 | 
			
		||||
        throw Error('Header not found.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Search ion-header inside a page. The header should be a direct child.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page Page to search in.
 | 
			
		||||
     * @return Header element. Undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    protected searchHeaderInPage(page: HTMLElement): HTMLElement | undefined {
 | 
			
		||||
        for (let i = 0; i < page.children.length; i++) {
 | 
			
		||||
            const child = page.children[i];
 | 
			
		||||
            if (child.tagName == 'ION-HEADER') {
 | 
			
		||||
                return <HTMLElement> child;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show or hide all the elements.
 | 
			
		||||
     */
 | 
			
		||||
@ -280,7 +246,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component destroyed.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        // This component was destroyed, remove all the buttons that were moved.
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,6 @@
 | 
			
		||||
<ng-content></ng-content>
 | 
			
		||||
<ion-button fill="clear" [attr.aria-label]="label | translate" core-suppress-events (onClick)="toggle($event)">
 | 
			
		||||
    <ion-icon [name]="iconName" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
<ion-button fill="clear" [attr.aria-label]="label | translate" core-suppress-events (onClick)="toggle($event)"
 | 
			
		||||
    (mousedown)="doNotBlur($event)" (keydown)="doNotBlur($event)" (keyup)="toggle($event)">
 | 
			
		||||
    <ion-icon [name]=" iconName" slot="icon-only" aria-hidden="true">
 | 
			
		||||
    </ion-icon>
 | 
			
		||||
</ion-button>
 | 
			
		||||
 | 
			
		||||
@ -44,12 +44,11 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
 | 
			
		||||
    @Input() initialShown?: boolean | string; // Whether the password should be shown at start.
 | 
			
		||||
    @ContentChild(IonInput) ionInput?: IonInput;
 | 
			
		||||
 | 
			
		||||
    shown!: boolean; // Whether the password is shown.
 | 
			
		||||
    label!: string; // Label for the button to show/hide.
 | 
			
		||||
    iconName!: string; // Name of the icon of the button to show/hide.
 | 
			
		||||
    selector = ''; // Selector to identify the input.
 | 
			
		||||
    shown = false; // Whether the password is shown.
 | 
			
		||||
    label = ''; // Label for the button to show/hide.
 | 
			
		||||
    iconName = ''; // Name of the icon of the button to show/hide.
 | 
			
		||||
 | 
			
		||||
    protected input?: HTMLInputElement | null; // Input affected.
 | 
			
		||||
    protected input?: HTMLInputElement; // Input affected.
 | 
			
		||||
    protected element: HTMLElement; // Current element.
 | 
			
		||||
 | 
			
		||||
    constructor(element: ElementRef) {
 | 
			
		||||
@ -57,58 +56,51 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.shown = CoreUtils.isTrueOrOne(this.initialShown);
 | 
			
		||||
        this.selector = 'input[name="' + this.name + '"]';
 | 
			
		||||
        this.setData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * View has been initialized.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngAfterViewInit(): void {
 | 
			
		||||
        this.searchInput();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Search the input to show/hide.
 | 
			
		||||
     */
 | 
			
		||||
    protected async searchInput(): Promise<void> {
 | 
			
		||||
    async ngAfterViewInit(): Promise<void> {
 | 
			
		||||
        if (this.ionInput) {
 | 
			
		||||
            // It's an ion-input, use it to get the native element.
 | 
			
		||||
            this.input = await this.ionInput.getInputElement();
 | 
			
		||||
            this.setData(this.input);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Search the input.
 | 
			
		||||
        this.input = <HTMLInputElement> this.element.querySelector(this.selector);
 | 
			
		||||
        this.input = this.element.querySelector<HTMLInputElement>('input[name="' + this.name + '"]') ?? undefined;
 | 
			
		||||
 | 
			
		||||
        if (this.input) {
 | 
			
		||||
            // Input found. Set the right type.
 | 
			
		||||
            this.input.type = this.shown ? 'text' : 'password';
 | 
			
		||||
        if (!this.input) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            // By default, don't autocapitalize and autocorrect.
 | 
			
		||||
            if (!this.input.getAttribute('autocorrect')) {
 | 
			
		||||
                this.input.setAttribute('autocorrect', 'off');
 | 
			
		||||
            }
 | 
			
		||||
            if (!this.input.getAttribute('autocapitalize')) {
 | 
			
		||||
                this.input.setAttribute('autocapitalize', 'none');
 | 
			
		||||
            }
 | 
			
		||||
        this.setData(this.input);
 | 
			
		||||
 | 
			
		||||
        // By default, don't autocapitalize and autocorrect.
 | 
			
		||||
        if (!this.input.getAttribute('autocorrect')) {
 | 
			
		||||
            this.input.setAttribute('autocorrect', 'off');
 | 
			
		||||
        }
 | 
			
		||||
        if (!this.input.getAttribute('autocapitalize')) {
 | 
			
		||||
            this.input.setAttribute('autocapitalize', 'none');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set label, icon name and input type.
 | 
			
		||||
     *
 | 
			
		||||
     * @param input The input element.
 | 
			
		||||
     */
 | 
			
		||||
    protected setData(): void {
 | 
			
		||||
    protected setData(input: HTMLInputElement): void {
 | 
			
		||||
        this.label = this.shown ? 'core.hide' : 'core.show';
 | 
			
		||||
        this.iconName = this.shown ? 'fas-eye-slash' : 'fas-eye';
 | 
			
		||||
        if (this.input) {
 | 
			
		||||
            this.input.type = this.shown ? 'text' : 'password';
 | 
			
		||||
        }
 | 
			
		||||
        input.type = this.shown ? 'text' : 'password';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -117,20 +109,49 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
 | 
			
		||||
     * @param event The mouse event.
 | 
			
		||||
     */
 | 
			
		||||
    toggle(event: Event): void {
 | 
			
		||||
        if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        const isFocused = document.activeElement === this.input;
 | 
			
		||||
 | 
			
		||||
        this.shown = !this.shown;
 | 
			
		||||
        this.setData();
 | 
			
		||||
 | 
			
		||||
        if (!this.input) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.setData(this.input);
 | 
			
		||||
        // In Android, the keyboard is closed when the input type changes. Focus it again.
 | 
			
		||||
        if (isFocused && CoreApp.isAndroid()) {
 | 
			
		||||
            // In Android, the keyboard is closed when the input type changes. Focus it again.
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                CoreDomUtils.focusElement(this.input!);
 | 
			
		||||
            }, 400);
 | 
			
		||||
            CoreDomUtils.focusElement(this.input);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Do not loose focus.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event The mouse event.
 | 
			
		||||
     */
 | 
			
		||||
    doNotBlur(event: Event): void {
 | 
			
		||||
        if (event.type == 'keydown' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if Space or Enter have been pressed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Keyboard Event.
 | 
			
		||||
     * @returns Wether space or enter have been pressed.
 | 
			
		||||
     */
 | 
			
		||||
    protected isValidKeyboardKey(event: KeyboardEvent): boolean {
 | 
			
		||||
        return event.key == ' ' || event.key == 'Enter';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@ export class CoreAutoFocusDirective implements AfterViewInit {
 | 
			
		||||
 | 
			
		||||
    @Input('core-auto-focus') autoFocus: boolean | string = true;
 | 
			
		||||
 | 
			
		||||
    protected element: HTMLElement;
 | 
			
		||||
    protected element: HTMLIonInputElement | HTMLIonTextareaElement | HTMLIonSearchbarElement | HTMLElement;
 | 
			
		||||
 | 
			
		||||
    constructor(element: ElementRef) {
 | 
			
		||||
        this.element = element.nativeElement;
 | 
			
		||||
@ -41,51 +41,27 @@ export class CoreAutoFocusDirective implements AfterViewInit {
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngAfterViewInit(): void {
 | 
			
		||||
    async ngAfterViewInit(): Promise<void> {
 | 
			
		||||
        if (CoreUtils.isFalseOrZero(this.autoFocus)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.setFocus();
 | 
			
		||||
    }
 | 
			
		||||
        await CoreDomUtils.waitToBeInDOM(this.element);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function to focus the element.
 | 
			
		||||
     *
 | 
			
		||||
     * @param retries Internal param to stop retrying on 0.
 | 
			
		||||
     */
 | 
			
		||||
    protected setFocus(retries = 10): void {
 | 
			
		||||
        if (retries == 0) {
 | 
			
		||||
        let focusElement = this.element;
 | 
			
		||||
 | 
			
		||||
        if ('getInputElement' in focusElement) {
 | 
			
		||||
            // If it's an Ionic element get the right input to use.
 | 
			
		||||
            focusElement.componentOnReady && await focusElement.componentOnReady();
 | 
			
		||||
            focusElement = await focusElement.getInputElement();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!focusElement) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Wait a bit to make sure the view is loaded.
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            // If it's a ion-input or ion-textarea, search the right input to use.
 | 
			
		||||
            let element: HTMLElement | null = null;
 | 
			
		||||
        CoreDomUtils.focusElement(focusElement);
 | 
			
		||||
 | 
			
		||||
            if (this.element.tagName == 'ION-INPUT') {
 | 
			
		||||
                element = this.element.querySelector('input');
 | 
			
		||||
            } else if (this.element.tagName == 'ION-TEXTAREA') {
 | 
			
		||||
                element = this.element.querySelector('textarea');
 | 
			
		||||
            } else {
 | 
			
		||||
                element = this.element;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!element) {
 | 
			
		||||
                this.setFocus(retries - 1);
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            CoreDomUtils.focusElement(element);
 | 
			
		||||
 | 
			
		||||
            if (element != document.activeElement) {
 | 
			
		||||
                this.setFocus(retries - 1);
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }, 200);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -72,6 +72,10 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        await this.calculateHeight();
 | 
			
		||||
 | 
			
		||||
        CoreDomUtils.onElementSlot(this.element, () => {
 | 
			
		||||
            this.calculateHeight();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.listenScrollEvents();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -146,7 +150,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
 | 
			
		||||
                    ? this.finalHeight
 | 
			
		||||
                    : this.initialHeight;
 | 
			
		||||
 | 
			
		||||
                this.setBarHeight(newHeight);            }
 | 
			
		||||
                this.setBarHeight(newHeight);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.resizeListener = CoreDomUtils.onWindowResize(() => {
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ 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';
 | 
			
		||||
@ -50,11 +51,14 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
 | 
			
		||||
    protected expandedHeight = 0;
 | 
			
		||||
    protected resizeListener?: CoreEventObserver;
 | 
			
		||||
    protected domPromise?: CoreCancellablePromise<void>;
 | 
			
		||||
    protected uniqueId: string;
 | 
			
		||||
 | 
			
		||||
    constructor(el: ElementRef<HTMLElement>) {
 | 
			
		||||
        this.element = el.nativeElement;
 | 
			
		||||
 | 
			
		||||
        this.element.addEventListener('click', this.elementClicked.bind(this));
 | 
			
		||||
        this.uniqueId = 'collapsible-item-' + CoreUtils.getUniqueId('CoreCollapsibleItemDirective');
 | 
			
		||||
        this.element.id = this.uniqueId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -153,6 +157,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
 | 
			
		||||
        const toggleButton = document.createElement('ion-button');
 | 
			
		||||
        toggleButton.classList.add('collapsible-toggle');
 | 
			
		||||
        toggleButton.setAttribute('fill', 'clear');
 | 
			
		||||
        toggleButton.setAttribute('aria-controls', this.uniqueId);
 | 
			
		||||
 | 
			
		||||
        const toggleText = document.createElement('span');
 | 
			
		||||
        toggleText.classList.add('collapsible-toggle-text');
 | 
			
		||||
@ -163,7 +168,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
 | 
			
		||||
        expandArrow.classList.add('collapsible-toggle-arrow');
 | 
			
		||||
        toggleButton.appendChild(expandArrow);
 | 
			
		||||
 | 
			
		||||
        this.element.appendChild(toggleButton);
 | 
			
		||||
        this.element.append(toggleButton);
 | 
			
		||||
 | 
			
		||||
        this.toggleExpand(this.expanded);
 | 
			
		||||
    }
 | 
			
		||||
@ -194,6 +199,9 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
 | 
			
		||||
            expand = !this.expanded;
 | 
			
		||||
        }
 | 
			
		||||
        this.expanded = expand;
 | 
			
		||||
 | 
			
		||||
        // Reset scroll inside the element to show always the top part.
 | 
			
		||||
        this.element.scrollTo(0, 0);
 | 
			
		||||
        this.element.classList.toggle('collapsible-collapsed', !expand);
 | 
			
		||||
        this.setHeight(!expand ? this.maxHeight: undefined);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,8 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreCancellablePromise } from '@classes/cancellable-promise';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Directive to move ion-fab components as direct children of the nearest ion-content.
 | 
			
		||||
@ -30,6 +31,7 @@ export class CoreFabDirective implements OnInit, OnDestroy {
 | 
			
		||||
    protected element: HTMLElement;
 | 
			
		||||
    protected content?: HTMLIonContentElement | null;
 | 
			
		||||
    protected initialPaddingBottom = 0;
 | 
			
		||||
    protected domPromise?: CoreCancellablePromise<void>;
 | 
			
		||||
 | 
			
		||||
    constructor(el: ElementRef) {
 | 
			
		||||
        this.element = el.nativeElement;
 | 
			
		||||
@ -39,15 +41,31 @@ export class CoreFabDirective implements OnInit, OnDestroy {
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(retries = 3): Promise<void> {
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        this.domPromise = CoreDomUtils.waitToBeInDOM(this.element);
 | 
			
		||||
        await this.domPromise;
 | 
			
		||||
 | 
			
		||||
        this.content = this.element.closest('ion-content');
 | 
			
		||||
 | 
			
		||||
        if (!this.content) {
 | 
			
		||||
            if(retries > 0) {
 | 
			
		||||
                await CoreUtils.nextTicks(50);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
                this.ngOnInit(retries - 1);
 | 
			
		||||
            }
 | 
			
		||||
        // Add space at the bottom to let the user see the whole content.
 | 
			
		||||
        this.initialPaddingBottom = parseFloat(this.content.style.getPropertyValue('--padding-bottom') || '0');
 | 
			
		||||
 | 
			
		||||
        await this.calculatePlace();
 | 
			
		||||
 | 
			
		||||
        CoreDomUtils.onElementSlot(this.element, () => {
 | 
			
		||||
            this.calculatePlace();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Calculate the height of the footer.
 | 
			
		||||
     */
 | 
			
		||||
    protected async calculatePlace(): Promise<void> {
 | 
			
		||||
        if (!this.content) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -58,8 +76,6 @@ export class CoreFabDirective implements OnInit, OnDestroy {
 | 
			
		||||
            this.content.appendChild(this.element);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add space at the bottom to let the user see the whole content.
 | 
			
		||||
        this.initialPaddingBottom = parseFloat(this.content.style.getPropertyValue('--padding-bottom') || '0');
 | 
			
		||||
        this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + initialHeight + 'px');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -70,6 +86,7 @@ export class CoreFabDirective implements OnInit, OnDestroy {
 | 
			
		||||
        if (this.content) {
 | 
			
		||||
            this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px');
 | 
			
		||||
        }
 | 
			
		||||
        this.domPromise?.cancel();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -142,11 +142,13 @@ export class CoreLinkDirective implements OnInit {
 | 
			
		||||
        if (href.charAt(0) == '#') {
 | 
			
		||||
            // Look for id or name.
 | 
			
		||||
            href = href.substring(1);
 | 
			
		||||
            CoreDomUtils.scrollToElementBySelector(
 | 
			
		||||
                this.element.closest('ion-content'),
 | 
			
		||||
                this.content,
 | 
			
		||||
                `#${href}, [name='${href}']`,
 | 
			
		||||
            );
 | 
			
		||||
            const container = this.element.closest('ion-content');
 | 
			
		||||
            if (container) {
 | 
			
		||||
                CoreDomUtils.scrollViewToElement(
 | 
			
		||||
                    container,
 | 
			
		||||
                    `#${href}, [name='${href}']`,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -34,6 +34,7 @@ import {
 | 
			
		||||
    CoreCourseProvider,
 | 
			
		||||
} from '@features/course/services/course';
 | 
			
		||||
import {
 | 
			
		||||
    CoreCourseHelper,
 | 
			
		||||
    CoreCourseSection,
 | 
			
		||||
} from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
 | 
			
		||||
@ -444,7 +445,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
                            await CoreCourseModuleDelegate.getModuleDataFor(module.modname, module, this.course.id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (module.uservisible !== false && module.handlerData?.action) {
 | 
			
		||||
        if (CoreCourseHelper.canUserViewModule(module, section) && module.handlerData?.action) {
 | 
			
		||||
            module.handlerData.action(data.event, module, module.course);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -491,9 +492,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
        // Scroll to module if needed. Give more priority to the input.
 | 
			
		||||
        const moduleIdToScroll = this.moduleId && previousValue === undefined ? this.moduleId : moduleId;
 | 
			
		||||
        if (moduleIdToScroll) {
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                this.scrollToModule(moduleIdToScroll);
 | 
			
		||||
            }, 200);
 | 
			
		||||
            this.scrollToModule(moduleIdToScroll);
 | 
			
		||||
        } else {
 | 
			
		||||
            this.content.scrollToTop(0);
 | 
			
		||||
        }
 | 
			
		||||
@ -512,9 +511,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
     * @param moduleId Module ID.
 | 
			
		||||
     */
 | 
			
		||||
    protected scrollToModule(moduleId: number): void {
 | 
			
		||||
        CoreDomUtils.scrollToElementBySelector(
 | 
			
		||||
        CoreDomUtils.scrollViewToElement(
 | 
			
		||||
            this.elementRef.nativeElement,
 | 
			
		||||
            this.content,
 | 
			
		||||
            '#core-course-module-' + moduleId,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
@ -574,7 +572,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            modulesLoaded += sections[i].modules.reduce((total, module) => module.visibleoncoursepage !== 0 ? total + 1 : total, 0);
 | 
			
		||||
            modulesLoaded += sections[i].modules.reduce((total, module) =>
 | 
			
		||||
                !CoreCourseHelper.isModuleStealth(module, sections[i]) ? total + 1 : total, 0);
 | 
			
		||||
 | 
			
		||||
            if (modulesLoaded >= CoreCourseFormatComponent.LOAD_MORE_ACTIVITIES) {
 | 
			
		||||
                break;
 | 
			
		||||
@ -632,8 +631,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
     * @return Whether the section can be viewed.
 | 
			
		||||
     */
 | 
			
		||||
    canViewSection(section: CoreCourseSection): boolean {
 | 
			
		||||
        return section.uservisible !== false && !section.hiddenbynumsections &&
 | 
			
		||||
                section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
 | 
			
		||||
        return CoreCourseHelper.canUserViewSection(section) && !CoreCourseHelper.isSectionStealth(section);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -12,16 +12,15 @@
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { Component, ElementRef, Input, OnInit } from '@angular/core';
 | 
			
		||||
import {
 | 
			
		||||
    CoreCourseModuleCompletionStatus,
 | 
			
		||||
    CoreCourseModuleCompletionTracking,
 | 
			
		||||
    CoreCourseProvider,
 | 
			
		||||
} from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseSection } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
 | 
			
		||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { ModalController } from '@singletons';
 | 
			
		||||
 | 
			
		||||
@ -35,8 +34,6 @@ import { ModalController } from '@singletons';
 | 
			
		||||
})
 | 
			
		||||
export class CoreCourseCourseIndexComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(IonContent) content?: IonContent;
 | 
			
		||||
 | 
			
		||||
    @Input() sections: CoreCourseSection[] = [];
 | 
			
		||||
    @Input() selectedId?: number;
 | 
			
		||||
    @Input() course?: CoreCourseAnyCourseData;
 | 
			
		||||
@ -77,11 +74,10 @@ export class CoreCourseCourseIndexComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
        // Clone sections to add information.
 | 
			
		||||
        this.sectionsToRender = this.sections
 | 
			
		||||
            .filter((section) => !section.hiddenbynumsections &&
 | 
			
		||||
                section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID)
 | 
			
		||||
            .filter((section) => !CoreCourseHelper.isSectionStealth(section))
 | 
			
		||||
            .map((section) => {
 | 
			
		||||
                const modules = section.modules
 | 
			
		||||
                    .filter((module) => module.visibleoncoursepage !== 0 && !module.noviewlink)
 | 
			
		||||
                    .filter((module) => !CoreCourseHelper.isModuleStealth(module, section) && !module.noviewlink)
 | 
			
		||||
                    .map((module) => {
 | 
			
		||||
                        const completionStatus = !completionEnabled || module.completiondata === undefined ||
 | 
			
		||||
                        module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE
 | 
			
		||||
@ -93,7 +89,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
 | 
			
		||||
                            name: module.name,
 | 
			
		||||
                            course: module.course,
 | 
			
		||||
                            visible: !!module.visible,
 | 
			
		||||
                            uservisible: !!module.uservisible,
 | 
			
		||||
                            uservisible: CoreCourseHelper.canUserViewModule(module, section),
 | 
			
		||||
                            completionStatus,
 | 
			
		||||
                        };
 | 
			
		||||
                    });
 | 
			
		||||
@ -103,7 +99,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
 | 
			
		||||
                    name: section.name,
 | 
			
		||||
                    availabilityinfo: !!section.availabilityinfo,
 | 
			
		||||
                    visible: !!section.visible,
 | 
			
		||||
                    uservisible: section.uservisible !== false,
 | 
			
		||||
                    uservisible: CoreCourseHelper.canUserViewSection(section),
 | 
			
		||||
                    expanded: section.id === this.selectedId,
 | 
			
		||||
                    highlighted: currentSectionData.section.id === section.id,
 | 
			
		||||
                    hasVisibleModules: modules.length > 0,
 | 
			
		||||
@ -113,13 +109,10 @@ export class CoreCourseCourseIndexComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
        this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
 | 
			
		||||
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            CoreDomUtils.scrollToElementBySelector(
 | 
			
		||||
                this.elementRef.nativeElement,
 | 
			
		||||
                this.content,
 | 
			
		||||
                '.item.item-current',
 | 
			
		||||
            );
 | 
			
		||||
        }, 300);
 | 
			
		||||
        CoreDomUtils.scrollViewToElement(
 | 
			
		||||
            this.elementRef.nativeElement,
 | 
			
		||||
            '.item.item-current',
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@
 | 
			
		||||
                        </ion-label>
 | 
			
		||||
                    </ion-chip>
 | 
			
		||||
 | 
			
		||||
                    <ion-chip *ngIf="rule.statusincomplete" color="dark" role="listitem" [attr.aria-label]="rule.accessibleDescription"
 | 
			
		||||
                    <ion-chip *ngIf="rule.statusincomplete" color="secondary" role="listitem" [attr.aria-label]="rule.accessibleDescription"
 | 
			
		||||
                        class="completioninfo completion_incomplete">
 | 
			
		||||
                        <ion-icon name="fas-edit" [attr.aria-label]="'core.course.completion_automatic:todo' | translate "></ion-icon>
 | 
			
		||||
                        <ion-label>
 | 
			
		||||
@ -42,7 +42,7 @@
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <ng-container *ngIf="mode == 'basic' && completion.istrackeduser">
 | 
			
		||||
            <ion-chip class="completioninfo completion_incomplete" *ngIf="completionStatus === 0" color="dark">
 | 
			
		||||
            <ion-chip class="completioninfo completion_incomplete" *ngIf="completionStatus === 0" color="secondary">
 | 
			
		||||
                <ion-icon name="fas-edit" aria-hidden="true"></ion-icon>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    {{ 'core.course.todo' | translate }}
 | 
			
		||||
 | 
			
		||||
@ -13,8 +13,8 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreCourse, CoreCourseProvider, CoreCourseWSSection } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseModuleData } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreCourse, CoreCourseWSSection } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
 | 
			
		||||
@ -177,7 +177,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
 | 
			
		||||
     * @return Wether the module is available to the user or not.
 | 
			
		||||
     */
 | 
			
		||||
    protected async isModuleAvailable(module: CoreCourseModuleData): Promise<boolean> {
 | 
			
		||||
        return CoreCourse.instance.moduleHasView(module);
 | 
			
		||||
        return !CoreCourseHelper.isModuleStealth(module) && CoreCourse.instance.moduleHasView(module);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -187,7 +187,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
 | 
			
		||||
     * @return Wether the module is available to the user or not.
 | 
			
		||||
     */
 | 
			
		||||
    protected isSectionAvailable(section: CoreCourseWSSection): boolean {
 | 
			
		||||
        return section.uservisible !== false && section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
 | 
			
		||||
        return CoreCourseHelper.canUserViewSection(section) && !CoreCourseHelper.isSectionStealth(section);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -223,7 +223,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
 | 
			
		||||
            animationDirection: next ? 'forward' : 'back',
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (module.uservisible === false) {
 | 
			
		||||
        if (!CoreCourseHelper.canUserViewModule(module)) {
 | 
			
		||||
            const section = next ? this.nextModuleSection : this.previousModuleSection;
 | 
			
		||||
            options.params = {
 | 
			
		||||
                module,
 | 
			
		||||
 | 
			
		||||
@ -35,10 +35,10 @@
 | 
			
		||||
                    </ion-chip>
 | 
			
		||||
 | 
			
		||||
                    <!-- Hidden badges -->
 | 
			
		||||
                    <ion-badge color="warning" *ngIf="module.visible === 0">
 | 
			
		||||
                    <ion-badge color="warning" *ngIf="module.visible === 0" class="ion-text-wrap">
 | 
			
		||||
                        {{ 'core.course.hiddenfromstudents' | translate }}
 | 
			
		||||
                    </ion-badge>
 | 
			
		||||
                    <ion-badge color="warning" *ngIf="module.visible !== 0 && module.isStealth">
 | 
			
		||||
                    <ion-badge color="warning" *ngIf="module.visible !== 0 && module.isStealth" class="ion-text-wrap">
 | 
			
		||||
                        {{ 'core.course.hiddenoncoursepage' | translate }}
 | 
			
		||||
                    </ion-badge>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ import {
 | 
			
		||||
    CoreCourseModuleData,
 | 
			
		||||
    CoreCourseModuleCompletionData,
 | 
			
		||||
    CoreCourseSection,
 | 
			
		||||
    CoreCourseHelper,
 | 
			
		||||
} from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreCourse, CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseModuleDelegate, CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate';
 | 
			
		||||
@ -166,7 +167,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
 | 
			
		||||
     * @param event Click event.
 | 
			
		||||
     */
 | 
			
		||||
    moduleClicked(event: Event): void {
 | 
			
		||||
        if (this.module.uservisible !== false && this.module.handlerData?.action) {
 | 
			
		||||
        if (CoreCourseHelper.canUserViewModule(this.module, this.section) && this.module.handlerData?.action) {
 | 
			
		||||
            this.module.handlerData.action(event, this.module, this.module.course);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,7 @@ export class CoreCourseListModTypePage implements OnInit {
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                section.modules = section.modules.filter((mod) => {
 | 
			
		||||
                    if (mod.uservisible === false || !CoreCourse.moduleHasView(mod)) {
 | 
			
		||||
                    if (!CoreCourseHelper.canUserViewModule(mod, section) || !CoreCourse.moduleHasView(mod)) {
 | 
			
		||||
                        // Ignore this module.
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
@ -33,10 +33,10 @@
 | 
			
		||||
                </ion-chip>
 | 
			
		||||
 | 
			
		||||
                <!-- Hidden badges -->
 | 
			
		||||
                <ion-badge color="warning" *ngIf="module.visible === 0">
 | 
			
		||||
                <ion-badge color="warning" *ngIf="module.visible === 0" class="ion-text-wrap">
 | 
			
		||||
                    {{ 'core.course.hiddenfromstudents' | translate }}
 | 
			
		||||
                </ion-badge>
 | 
			
		||||
                <ion-badge color="warning" *ngIf="module.visible !== 0 && module.isStealth">
 | 
			
		||||
                <ion-badge color="warning" *ngIf="module.visible !== 0 && module.isStealth" class="ion-text-wrap">
 | 
			
		||||
                    {{ 'core.course.hiddenoncoursepage' | translate }}
 | 
			
		||||
                </ion-badge>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@ -198,7 +198,7 @@ export class CoreCourseHelperProvider {
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Check if the module is stealth.
 | 
			
		||||
                    module.isStealth = module.visibleoncoursepage === 0 || (!!module.visible && !section.visible);
 | 
			
		||||
                    module.isStealth = CoreCourseHelper.isModuleStealth(module, section);
 | 
			
		||||
                }));
 | 
			
		||||
 | 
			
		||||
                return section;
 | 
			
		||||
@ -208,6 +208,50 @@ export class CoreCourseHelperProvider {
 | 
			
		||||
        return { hasContent, sections: formattedSections };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Module is stealth.
 | 
			
		||||
     *
 | 
			
		||||
     * @param module Module to check.
 | 
			
		||||
     * @param section Section to check.
 | 
			
		||||
     * @return Wether the module is stealth.
 | 
			
		||||
     */
 | 
			
		||||
    isModuleStealth(module: CoreCourseModuleData, section?: CoreCourseWSSection): boolean {
 | 
			
		||||
        // visibleoncoursepage can be 1 for teachers when the section is hidden.
 | 
			
		||||
        return !!module.visible && (!module.visibleoncoursepage || (!!section && !section.visible));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Module is visible by the user.
 | 
			
		||||
     *
 | 
			
		||||
     * @param module Module to check.
 | 
			
		||||
     * @param section Section to check. Omitted if not defined.
 | 
			
		||||
     * @return Wether the section is visible by the user.
 | 
			
		||||
     */
 | 
			
		||||
    canUserViewModule(module: CoreCourseModuleData, section?: CoreCourseWSSection): boolean {
 | 
			
		||||
        return module.uservisible !== false && (!section || CoreCourseHelper.canUserViewSection(section));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Section is stealth.
 | 
			
		||||
     * This should not be true on Moodle 4.0 onwards.
 | 
			
		||||
     *
 | 
			
		||||
     * @param section Section to check.
 | 
			
		||||
     * @return Wether section is stealth (accessible but not visible to students).
 | 
			
		||||
     */
 | 
			
		||||
    isSectionStealth(section: CoreCourseWSSection): boolean {
 | 
			
		||||
        return section.hiddenbynumsections === 1 || section.id === CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Section is visible by the user.
 | 
			
		||||
     *
 | 
			
		||||
     * @param section Section to check.
 | 
			
		||||
     * @return Wether the section is visible by the user.
 | 
			
		||||
     */
 | 
			
		||||
    canUserViewSection(section: CoreCourseWSSection): boolean {
 | 
			
		||||
        return section.uservisible !== false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Calculate completion data of a module.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,7 @@ import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreWSFile, CoreWSExternalWarning } from '@services/ws';
 | 
			
		||||
import { CHECK_UPDATES_TIMES_TABLE, CoreCourseCheckUpdatesDBRecord } from './database/module-prefetch';
 | 
			
		||||
import { CoreFileSizeSum } from '@services/plugin-file-delegate';
 | 
			
		||||
import { CoreCourseModuleData } from './course-helper';
 | 
			
		||||
import { CoreCourseHelper, CoreCourseModuleData } from './course-helper';
 | 
			
		||||
 | 
			
		||||
const ROOT_CACHE_KEY = 'mmCourse:';
 | 
			
		||||
 | 
			
		||||
@ -956,7 +956,7 @@ export class CoreCourseModulePrefetchDelegateService extends CoreDelegate<CoreCo
 | 
			
		||||
     * @return Promise resolved with true if downloadable, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    async isModuleDownloadable(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> {
 | 
			
		||||
        if ('uservisible' in module && module.uservisible === false) {
 | 
			
		||||
        if ('uservisible' in module && !CoreCourseHelper.canUserViewModule(module)) {
 | 
			
		||||
            // Module isn't visible by the user, cannot be downloaded.
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,6 @@ import {
 | 
			
		||||
    ViewChild,
 | 
			
		||||
    ElementRef,
 | 
			
		||||
    OnInit,
 | 
			
		||||
    AfterContentInit,
 | 
			
		||||
    OnDestroy,
 | 
			
		||||
    Optional,
 | 
			
		||||
    AfterViewInit,
 | 
			
		||||
@ -56,7 +55,7 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise';
 | 
			
		||||
    templateUrl: 'core-editor-rich-text-editor.html',
 | 
			
		||||
    styleUrls: ['rich-text-editor.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy {
 | 
			
		||||
export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    // Based on: https://github.com/judgewest2000/Ionic3RichText/
 | 
			
		||||
    // @todo: Anchor button, fullscreen...
 | 
			
		||||
@ -149,7 +148,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.canScanQR = CoreUtils.canScanQR();
 | 
			
		||||
@ -159,9 +158,9 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Init editor.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngAfterContentInit(): Promise<void> {
 | 
			
		||||
    async ngAfterViewInit(): Promise<void> {
 | 
			
		||||
        this.rteEnabled = await CoreDomUtils.isRichTextEditorEnabled();
 | 
			
		||||
 | 
			
		||||
        await this.waitLoadingsDone();
 | 
			
		||||
@ -202,13 +201,14 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
 | 
			
		||||
 | 
			
		||||
            this.deleteDraftOnSubmitOrCancel();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngAfterViewInit(): Promise<void> {
 | 
			
		||||
        const label = this.element.closest('ion-item')?.querySelector('ion-label');
 | 
			
		||||
        const ionItem = this.element.closest<HTMLIonItemElement>('ion-item');
 | 
			
		||||
        if (!ionItem) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        ionItem.classList.add('item-rte');
 | 
			
		||||
 | 
			
		||||
        const label = ionItem.querySelector('ion-label');
 | 
			
		||||
 | 
			
		||||
        if (!label) {
 | 
			
		||||
            return;
 | 
			
		||||
@ -773,7 +773,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn
 | 
			
		||||
     * @param event Event.
 | 
			
		||||
     */
 | 
			
		||||
    stopBubble(event: Event): void {
 | 
			
		||||
        if (event.type != 'touchend' &&event.type != 'mouseup' && event.type != 'keyup') {
 | 
			
		||||
        if (event.type != 'touchend' && event.type != 'mouseup' && event.type != 'keyup') {
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
        }
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,7 @@ import { makeSingleton } from '@singletons';
 | 
			
		||||
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
 | 
			
		||||
import { CoreLogger } from '@singletons/logger';
 | 
			
		||||
import { CoreSite } from '@classes/site';
 | 
			
		||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper service to provide filter functionalities.
 | 
			
		||||
@ -159,7 +160,7 @@ export class CoreFilterHelperProvider {
 | 
			
		||||
        sections.forEach((section) => {
 | 
			
		||||
            if (section.modules) {
 | 
			
		||||
                section.modules.forEach((module) => {
 | 
			
		||||
                    if (module.uservisible) {
 | 
			
		||||
                    if (CoreCourseHelper.canUserViewModule(module, section)) {
 | 
			
		||||
                        contexts.push({
 | 
			
		||||
                            contextlevel: 'module',
 | 
			
		||||
                            instanceid: module.id,
 | 
			
		||||
 | 
			
		||||
@ -13,8 +13,8 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
import { AfterViewInit, Component, ElementRef, OnDestroy, Optional } from '@angular/core';
 | 
			
		||||
import { IonContent, IonRefresher } from '@ionic/angular';
 | 
			
		||||
import { AfterViewInit, Component, ElementRef, OnDestroy } from '@angular/core';
 | 
			
		||||
import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreGrades } from '@features/grades/services/grades';
 | 
			
		||||
@ -59,7 +59,6 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected route: ActivatedRoute,
 | 
			
		||||
        protected element: ElementRef<HTMLElement>,
 | 
			
		||||
        @Optional() protected content?: IonContent,
 | 
			
		||||
    ) {
 | 
			
		||||
        try {
 | 
			
		||||
            this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId', { route });
 | 
			
		||||
@ -170,11 +169,9 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
                if (row) {
 | 
			
		||||
                    this.toggleRow(row, true);
 | 
			
		||||
                    await CoreUtils.nextTick();
 | 
			
		||||
 | 
			
		||||
                    CoreDomUtils.scrollToElementBySelector(
 | 
			
		||||
                    CoreDomUtils.scrollViewToElement(
 | 
			
		||||
                        this.element.nativeElement,
 | 
			
		||||
                        this.content,
 | 
			
		||||
                        '#grade-' + row.id,
 | 
			
		||||
                    );
 | 
			
		||||
                    this.gradeId = undefined;
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@
 | 
			
		||||
 | 
			
		||||
import { Component, ViewChild, ElementRef, OnInit, ChangeDetectorRef } from '@angular/core';
 | 
			
		||||
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
 | 
			
		||||
import { IonContent, IonRefresher } from '@ionic/angular';
 | 
			
		||||
import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
@ -46,7 +46,6 @@ import { CoreText } from '@singletons/text';
 | 
			
		||||
})
 | 
			
		||||
export class CoreLoginEmailSignupPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(IonContent) content?: IonContent;
 | 
			
		||||
    @ViewChild(CoreRecaptchaComponent) recaptchaComponent?: CoreRecaptchaComponent;
 | 
			
		||||
    @ViewChild('ageForm') ageFormElement?: ElementRef;
 | 
			
		||||
    @ViewChild('signupFormEl') signupFormElement?: ElementRef;
 | 
			
		||||
@ -285,9 +284,8 @@ export class CoreLoginEmailSignupPage implements OnInit {
 | 
			
		||||
            this.changeDetector.detectChanges();
 | 
			
		||||
 | 
			
		||||
            // Scroll to the first element with errors.
 | 
			
		||||
            const errorFound = CoreDomUtils.scrollToInputError(
 | 
			
		||||
            const errorFound = await CoreDomUtils.scrollViewToInputError(
 | 
			
		||||
                this.elementRef.nativeElement,
 | 
			
		||||
                this.content,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (!errorFound) {
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ import { Type } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreCourseModuleData } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
 | 
			
		||||
import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index';
 | 
			
		||||
import {
 | 
			
		||||
@ -105,7 +105,7 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (forCoursePage && this.handlerSchema.coursepagemethod && module.visibleoncoursepage !== 0) {
 | 
			
		||||
        if (forCoursePage && this.handlerSchema.coursepagemethod && !CoreCourseHelper.isModuleStealth(module)) {
 | 
			
		||||
            // Call the method to get the course page template.
 | 
			
		||||
            const method = this.handlerSchema.coursepagemethod;
 | 
			
		||||
            this.loadCoursePageTemplate(module, courseId, handlerData, method);
 | 
			
		||||
 | 
			
		||||
@ -220,6 +220,53 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Runs a function when an element has been slotted.
 | 
			
		||||
     *
 | 
			
		||||
     * @param element HTML Element inside an ion-content to wait for slot.
 | 
			
		||||
     * @param callback Function to execute on resize.
 | 
			
		||||
     */
 | 
			
		||||
    onElementSlot(element: HTMLElement, callback: (ev?: Event) => void): void {
 | 
			
		||||
        if (!element.slot) {
 | 
			
		||||
            // Element not declared to be slotted.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const slotName = element.slot;
 | 
			
		||||
        if (element.assignedSlot?.name === slotName) {
 | 
			
		||||
            // Slot already assigned.
 | 
			
		||||
            callback();
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const content = element.closest('ion-content');
 | 
			
		||||
        if (!content || !content.shadowRoot) {
 | 
			
		||||
            // Cannot find content.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const slots = content.shadowRoot.querySelectorAll('slot');
 | 
			
		||||
        const slot = Array.from(slots).find((slot) => slot.name === slotName);
 | 
			
		||||
 | 
			
		||||
        if (!slot) {
 | 
			
		||||
            // Slot not found.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const slotListener = () => {
 | 
			
		||||
            if (element.assignedSlot?.name !== slotName) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            callback();
 | 
			
		||||
            // It would happen only once.
 | 
			
		||||
            slot.removeEventListener('slotchange', slotListener);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        slot.addEventListener('slotchange', slotListener);;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
@ -229,7 +276,9 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
     * @return Event observer to call off when finished.
 | 
			
		||||
     */
 | 
			
		||||
    onWindowResize(resizeFunction: (ev?: Event) => void, debounceDelay = 20): CoreEventObserver {
 | 
			
		||||
        const resizeListener = CoreUtils.debounce((ev?: Event) => {
 | 
			
		||||
        const resizeListener = CoreUtils.debounce(async (ev?: Event) => {
 | 
			
		||||
            await this.waitForResizeDone();
 | 
			
		||||
 | 
			
		||||
            resizeFunction(ev);
 | 
			
		||||
        }, debounceDelay);
 | 
			
		||||
 | 
			
		||||
@ -461,15 +510,30 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
    /**
 | 
			
		||||
     * Focus an element and open keyboard.
 | 
			
		||||
     *
 | 
			
		||||
     * @param el HTML element to focus.
 | 
			
		||||
     * @param focusElement HTML element to focus.
 | 
			
		||||
     */
 | 
			
		||||
    focusElement(el: HTMLElement): void {
 | 
			
		||||
        if (el?.focus) {
 | 
			
		||||
            el.focus();
 | 
			
		||||
            if (CoreApp.isAndroid() && this.supportsInputKeyboard(el)) {
 | 
			
		||||
                // On some Android versions the keyboard doesn't open automatically.
 | 
			
		||||
                CoreApp.openKeyboard();
 | 
			
		||||
    async focusElement(focusElement: HTMLElement): Promise<void> {
 | 
			
		||||
        let retries = 10;
 | 
			
		||||
 | 
			
		||||
        if (!focusElement.focus) {
 | 
			
		||||
            throw new CoreError('Element to focus cannot be focused');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        while (retries > 0 && focusElement !== document.activeElement) {
 | 
			
		||||
            focusElement.focus();
 | 
			
		||||
 | 
			
		||||
            if (focusElement === document.activeElement) {
 | 
			
		||||
                await CoreUtils.nextTick();
 | 
			
		||||
                if (CoreApp.isAndroid() && this.supportsInputKeyboard(focusElement)) {
 | 
			
		||||
                    // On some Android versions the keyboard doesn't open automatically.
 | 
			
		||||
                    CoreApp.openKeyboard();
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // @TODO Probably a Mutation Observer would get this working.
 | 
			
		||||
            await CoreUtils.wait(50);
 | 
			
		||||
            retries--;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -656,50 +720,54 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve the position of a element relative to another element.
 | 
			
		||||
     *
 | 
			
		||||
     * @param container Element to search in.
 | 
			
		||||
     * @param element Element to search in.
 | 
			
		||||
     * @param selector Selector to find the element to gets the position.
 | 
			
		||||
     * @param positionParentClass Parent Class where to stop calculating the position. Default inner-scroll.
 | 
			
		||||
     * @return positionLeft, positionTop of the element relative to.
 | 
			
		||||
     * @deprecated since app 4.0. Use getRelativeElementPosition instead.
 | 
			
		||||
     */
 | 
			
		||||
    getElementXY(container: HTMLElement, selector: undefined, positionParentClass?: string): number[];
 | 
			
		||||
    getElementXY(container: HTMLElement, selector: string, positionParentClass?: string): number[] | null;
 | 
			
		||||
    getElementXY(container: HTMLElement, selector?: string, positionParentClass?: string): number[] | null {
 | 
			
		||||
        let element: HTMLElement | null = <HTMLElement> (selector ? container.querySelector(selector) : container);
 | 
			
		||||
        let positionTop = 0;
 | 
			
		||||
        let positionLeft = 0;
 | 
			
		||||
    getElementXY(element: HTMLElement, selector?: string, positionParentClass = 'inner-scroll'): [number, number] | null {
 | 
			
		||||
        if (selector) {
 | 
			
		||||
            const foundElement = element.querySelector<HTMLElement>(selector);
 | 
			
		||||
            if (!foundElement) {
 | 
			
		||||
                // Element not found.
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        if (!positionParentClass) {
 | 
			
		||||
            positionParentClass = 'inner-scroll';
 | 
			
		||||
            element = foundElement;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!element) {
 | 
			
		||||
        const parent = element.closest<HTMLElement>(`.${positionParentClass}`);
 | 
			
		||||
        if (!parent) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        while (element) {
 | 
			
		||||
            positionLeft += (element.offsetLeft - element.scrollLeft + element.clientLeft);
 | 
			
		||||
            positionTop += (element.offsetTop - element.scrollTop + element.clientTop);
 | 
			
		||||
        const position = CoreDomUtils.getRelativeElementPosition(element, parent);
 | 
			
		||||
 | 
			
		||||
            const offsetElement = element.offsetParent;
 | 
			
		||||
            element = element.parentElement;
 | 
			
		||||
        // Calculate the top and left positions.
 | 
			
		||||
        return [
 | 
			
		||||
            Math.ceil(position.x),
 | 
			
		||||
            Math.ceil(position.y),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
            // Every parent class has to be checked but the position has to be got form offsetParent.
 | 
			
		||||
            while (offsetElement != element && element) {
 | 
			
		||||
                // If positionParentClass element is reached, stop adding tops.
 | 
			
		||||
                if (element.className.indexOf(positionParentClass) != -1) {
 | 
			
		||||
                    element = null;
 | 
			
		||||
                } else {
 | 
			
		||||
                    element = element.parentElement;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve the position of a element relative to another element.
 | 
			
		||||
     *
 | 
			
		||||
     * @param element Element to get the position.
 | 
			
		||||
     * @param parent Parent element to get relative position.
 | 
			
		||||
     * @return X and Y position.
 | 
			
		||||
     */
 | 
			
		||||
    getRelativeElementPosition(element: HTMLElement, parent: HTMLElement): CoreCoordinates {
 | 
			
		||||
        // Get the top, left coordinates of two elements
 | 
			
		||||
        const elementRectangle = element.getBoundingClientRect();
 | 
			
		||||
        const parentRectangle = parent.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
            // Finally, check again.
 | 
			
		||||
            if (element?.className.indexOf(positionParentClass) != -1) {
 | 
			
		||||
                element = null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return [positionLeft, positionTop];
 | 
			
		||||
        // Calculate the top and left positions.
 | 
			
		||||
        return {
 | 
			
		||||
            x: elementRectangle.x - parentRectangle.x,
 | 
			
		||||
            y: elementRectangle.y - parentRectangle.y,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1032,11 +1100,9 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
     * @param selector Selector to search.
 | 
			
		||||
     */
 | 
			
		||||
    removeElement(element: HTMLElement, selector: string): void {
 | 
			
		||||
        if (element) {
 | 
			
		||||
            const selected = element.querySelector(selector);
 | 
			
		||||
            if (selected) {
 | 
			
		||||
                selected.remove();
 | 
			
		||||
            }
 | 
			
		||||
        const selected = element.querySelector(selector);
 | 
			
		||||
        if (selected) {
 | 
			
		||||
            selected.remove();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1134,9 +1200,9 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Treat video posters.
 | 
			
		||||
            if (media.tagName == 'VIDEO' && media.getAttribute('poster')) {
 | 
			
		||||
                const currentPoster = media.getAttribute('poster');
 | 
			
		||||
                const newPoster = paths[CoreTextUtils.decodeURIComponent(currentPoster!)];
 | 
			
		||||
            const currentPoster = media.getAttribute('poster');
 | 
			
		||||
            if (media.tagName == 'VIDEO' && currentPoster) {
 | 
			
		||||
                const newPoster = paths[CoreTextUtils.decodeURIComponent(currentPoster)];
 | 
			
		||||
                if (newPoster !== undefined) {
 | 
			
		||||
                    media.setAttribute('poster', newPoster);
 | 
			
		||||
                }
 | 
			
		||||
@ -1173,8 +1239,8 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
     * @return Returns a promise which is resolved when the scroll has completed.
 | 
			
		||||
     * @deprecated since 3.9.5. Use directly the IonContent class.
 | 
			
		||||
     */
 | 
			
		||||
    scrollTo(content: IonContent, x: number, y: number, duration?: number): Promise<void> {
 | 
			
		||||
        return content.scrollToPoint(x, y, duration || 0);
 | 
			
		||||
    scrollTo(content: IonContent, x: number, y: number, duration = 0): Promise<void> {
 | 
			
		||||
        return content.scrollToPoint(x, y, duration);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -1197,7 +1263,7 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
     * @return Returns a promise which is resolved when the scroll has completed.
 | 
			
		||||
     * @deprecated since 3.9.5. Use directly the IonContent class.
 | 
			
		||||
     */
 | 
			
		||||
    scrollToTop(content: IonContent, duration?: number): Promise<void> {
 | 
			
		||||
    scrollToTop(content: IonContent, duration = 0): Promise<void> {
 | 
			
		||||
        return content.scrollToTop(duration);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1244,7 +1310,7 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
            const scrollElement = await content.getScrollElement();
 | 
			
		||||
 | 
			
		||||
            return scrollElement.scrollTop || 0;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
        } catch {
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -1252,51 +1318,34 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
    /**
 | 
			
		||||
     * Scroll to a certain element.
 | 
			
		||||
     *
 | 
			
		||||
     * @param content The content that must be scrolled.
 | 
			
		||||
     * @param element The element to scroll to.
 | 
			
		||||
     * @param scrollParentClass Parent class where to stop calculating the position. Default inner-scroll.
 | 
			
		||||
     * @param selector Selector to find the element to scroll to inside the defined element.
 | 
			
		||||
     * @param duration Duration of the scroll animation in milliseconds.
 | 
			
		||||
     * @return True if the element is found, false otherwise.
 | 
			
		||||
     * @return Wether the scroll suceeded.
 | 
			
		||||
     */
 | 
			
		||||
    scrollToElement(content: IonContent, element: HTMLElement, scrollParentClass?: string, duration?: number): boolean {
 | 
			
		||||
        const position = this.getElementXY(element, undefined, scrollParentClass);
 | 
			
		||||
        if (!position) {
 | 
			
		||||
            return false;
 | 
			
		||||
    async scrollViewToElement(element: HTMLElement, selector?: string, duration = 0): Promise<boolean> {
 | 
			
		||||
        await CoreDomUtils.waitToBeInDOM(element);
 | 
			
		||||
 | 
			
		||||
        if (selector) {
 | 
			
		||||
            const foundElement = element.querySelector<HTMLElement>(selector);
 | 
			
		||||
            if (!foundElement) {
 | 
			
		||||
                // Element not found.
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            element = foundElement;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        content.scrollToPoint(position[0], position[1], duration || 0);
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Scroll to a certain element using a selector to find it.
 | 
			
		||||
     *
 | 
			
		||||
     * @param container The element that contains the element that must be scrolled.
 | 
			
		||||
     * @param content The content that must be scrolled.
 | 
			
		||||
     * @param selector Selector to find the element to scroll to.
 | 
			
		||||
     * @param scrollParentClass Parent class where to stop calculating the position. Default inner-scroll.
 | 
			
		||||
     * @param duration Duration of the scroll animation in milliseconds.
 | 
			
		||||
     * @return True if the element is found, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    scrollToElementBySelector(
 | 
			
		||||
        container: HTMLElement | null,
 | 
			
		||||
        content: IonContent | undefined,
 | 
			
		||||
        selector: string,
 | 
			
		||||
        scrollParentClass?: string,
 | 
			
		||||
        duration?: number,
 | 
			
		||||
    ): boolean {
 | 
			
		||||
        if (!container || !content) {
 | 
			
		||||
        const content = element.closest<HTMLIonContentElement>('ion-content') ?? undefined;
 | 
			
		||||
        if (!content) {
 | 
			
		||||
            // Content to scroll, not found.
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const position = this.getElementXY(container, selector, scrollParentClass);
 | 
			
		||||
            if (!position) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            const position = CoreDomUtils.getRelativeElementPosition(element, content);
 | 
			
		||||
 | 
			
		||||
            content.scrollToPoint(position[0], position[1], duration || 0);
 | 
			
		||||
            await content.scrollToPoint(position.x, position.y, duration);
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        } catch {
 | 
			
		||||
@ -1308,12 +1357,71 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
     * Search for an input with error (core-input-error directive) and scrolls to it if found.
 | 
			
		||||
     *
 | 
			
		||||
     * @param container The element that contains the element that must be scrolled.
 | 
			
		||||
     * @param content The content that must be scrolled.
 | 
			
		||||
     * @param scrollParentClass Parent class where to stop calculating the position. Default inner-scroll.
 | 
			
		||||
     * @return True if the element is found, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    scrollToInputError(container: HTMLElement | null, content?: IonContent, scrollParentClass?: string): boolean {
 | 
			
		||||
        return this.scrollToElementBySelector(container, content, '.core-input-error', scrollParentClass);
 | 
			
		||||
    async scrollViewToInputError(container: HTMLElement): Promise<boolean> {
 | 
			
		||||
        return this.scrollViewToElement(container, '.core-input-error');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Scroll to a certain element.
 | 
			
		||||
     *
 | 
			
		||||
     * @param content Not used anymore.
 | 
			
		||||
     * @param element The element to scroll to.
 | 
			
		||||
     * @param scrollParentClass Not used anymore.
 | 
			
		||||
     * @param duration Duration of the scroll animation in milliseconds.
 | 
			
		||||
     * @return True if the element is found, false otherwise.
 | 
			
		||||
     * @deprecated since app 4.0 Use scrollViewToElement instead.
 | 
			
		||||
     */
 | 
			
		||||
    scrollToElement(content: IonContent, element: HTMLElement, scrollParentClass?: string, duration = 0): boolean {
 | 
			
		||||
        CoreDomUtils.scrollViewToElement(element, undefined, duration);
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Scroll to a certain element using a selector to find it.
 | 
			
		||||
     *
 | 
			
		||||
     * @param container The element that contains the element that must be scrolled.
 | 
			
		||||
     * @param content Not used anymore.
 | 
			
		||||
     * @param selector Selector to find the element to scroll to.
 | 
			
		||||
     * @param scrollParentClass Not used anymore.
 | 
			
		||||
     * @param duration Duration of the scroll animation in milliseconds.
 | 
			
		||||
     * @return True if the element is found, false otherwise.
 | 
			
		||||
     * @deprecated since app 4.0 Use scrollViewToElement instead.
 | 
			
		||||
     */
 | 
			
		||||
    scrollToElementBySelector(
 | 
			
		||||
        container: HTMLElement | null,
 | 
			
		||||
        content: unknown | null,
 | 
			
		||||
        selector: string,
 | 
			
		||||
        scrollParentClass?: string,
 | 
			
		||||
        duration = 0,
 | 
			
		||||
    ): boolean {
 | 
			
		||||
        if (!container || !content) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoreDomUtils.scrollViewToElement(container, selector, duration);
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Search for an input with error (core-input-error directive) and scrolls to it if found.
 | 
			
		||||
     *
 | 
			
		||||
     * @param container The element that contains the element that must be scrolled.
 | 
			
		||||
     * @return True if the element is found, false otherwise.
 | 
			
		||||
     * @deprecated since app 4.0 Use scrollViewToInputError instead.
 | 
			
		||||
     */
 | 
			
		||||
    scrollToInputError(container: HTMLElement | null): boolean {
 | 
			
		||||
        if (!container) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.scrollViewToInputError(container);
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -2106,6 +2214,7 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * In iOS the resize event is triggered before the window size changes. Wait for the size to change.
 | 
			
		||||
     * Use of this function is discouraged. Please use onWindowResize to check window resize event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param windowWidth Initial window width.
 | 
			
		||||
     * @param windowHeight Initial window height.
 | 
			
		||||
@ -2318,3 +2427,11 @@ export enum VerticalPoint {
 | 
			
		||||
    MID = 'mid',
 | 
			
		||||
    BOTTOM = 'bottom',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Coordinates of an element.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreCoordinates = {
 | 
			
		||||
    x: number; // X axis coordinates.
 | 
			
		||||
    y: number; // Y axis coordinates.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -60,7 +60,7 @@ export class CoreForms {
 | 
			
		||||
    /**
 | 
			
		||||
     * Trigger form cancelled event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param form Form element.
 | 
			
		||||
     * @param formRef Form element.
 | 
			
		||||
     * @param siteId The site affected. If not provided, no site affected.
 | 
			
		||||
     */
 | 
			
		||||
    static triggerFormCancelledEvent(formRef?: ElementRef | HTMLFormElement | undefined, siteId?: string): void {
 | 
			
		||||
@ -77,7 +77,7 @@ export class CoreForms {
 | 
			
		||||
    /**
 | 
			
		||||
     * Trigger form submitted event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param form Form element.
 | 
			
		||||
     * @param formRef Form element.
 | 
			
		||||
     * @param online Whether the action was done in offline or not.
 | 
			
		||||
     * @param siteId The site affected. If not provided, no site affected.
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -72,7 +72,7 @@
 | 
			
		||||
 | 
			
		||||
                &:before {
 | 
			
		||||
                    content: '';
 | 
			
		||||
                    height: 60px;
 | 
			
		||||
                    height: 100%;
 | 
			
		||||
                    position: absolute;
 | 
			
		||||
                    @include position(null, 0, 0, 0);
 | 
			
		||||
                    background: linear-gradient(to bottom, rgba(var(--background-gradient-rgb), 0) calc(100% - var(--gradient-size)), rgba(var(--background-gradient-rgb), 1) calc(100% - 4px));
 | 
			
		||||
 | 
			
		||||
@ -985,6 +985,18 @@ ion-chip {
 | 
			
		||||
            color: var(--ion-color-base);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.ion-color {
 | 
			
		||||
        &.ion-color-light,
 | 
			
		||||
        &.ion-color-medium,
 | 
			
		||||
        &.ion-color-dark,
 | 
			
		||||
        &.ion-color-secondary {
 | 
			
		||||
            color: var(--gray-900);
 | 
			
		||||
            &.chip-outline {
 | 
			
		||||
                color: var(--text-color);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ion-searchbar {
 | 
			
		||||
@ -1243,7 +1255,7 @@ ion-datetime.datetime-disabled {
 | 
			
		||||
 | 
			
		||||
// Make links clickable when inside radio or checkbox items. Style part.
 | 
			
		||||
@media (hover: hover) {
 | 
			
		||||
    ion-item.item-multiple-inputs:hover::part(native) {
 | 
			
		||||
    ion-item.item-multiple-inputs:not(.item-rte):hover::part(native) {
 | 
			
		||||
        color: var(--color-hover);
 | 
			
		||||
 | 
			
		||||
        &::after {
 | 
			
		||||
@ -1264,7 +1276,7 @@ ion-datetime.datetime-disabled {
 | 
			
		||||
 | 
			
		||||
// It fixes the click on links where ion-ripple-effect is present.
 | 
			
		||||
// Make links clickable when inside radio or checkbox items. Pointer and cursor part.
 | 
			
		||||
ion-item.item-multiple-inputs:not(.only-links),
 | 
			
		||||
ion-item.item-multiple-inputs:not(.only-links):not(.item-rte),
 | 
			
		||||
ion-item.ion-activatable:not(.only-links) {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    ion-label {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user