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