diff --git a/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts index 129c1ca6d..9b9a2f2bb 100644 --- a/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts +++ b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts @@ -21,6 +21,7 @@ import { ContextLevel, CoreConstants } from '@/core/constants'; import { Translate } from '@singletons'; import { CoreUtils } from '@services/utils/utils'; import { CoreNavigator } from '@services/navigator'; +import { CoreCourseHelper } from '@features/course/services/course-helper'; /** * Component to render an "activity modules" block. @@ -67,7 +68,7 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i } section.modules.forEach((mod) => { - if (mod.uservisible === false || !CoreCourse.moduleHasView(mod) || + if (!CoreCourseHelper.canUserViewModule(mod, section) || !CoreCourse.moduleHasView(mod) || modFullNames[mod.modname] !== undefined) { // Ignore this module. return; diff --git a/src/addons/calendar/pages/edit-event/edit-event.html b/src/addons/calendar/pages/edit-event/edit-event.html index 57b4eb6f4..fe0a30baa 100644 --- a/src/addons/calendar/pages/edit-event/edit-event.html +++ b/src/addons/calendar/pages/edit-event/edit-event.html @@ -183,7 +183,7 @@ -

{{ 'addon.calendar.repeatweeksl' | translate }}

+

{{ 'addon.calendar.repeatweeksl' | translate }}

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