From ac5e4b1d796a1965ca2d8b5da2507f9a9ba91a6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 22 Mar 2022 10:09:54 +0100 Subject: [PATCH 1/3] MOBILE-3814 dom: Fix scroll to element with selector --- .../components/course-format/course-format.ts | 1 + .../components/course-index/course-index.ts | 1 + src/core/services/utils/dom.ts | 81 ++++++++++++++++--- 3 files changed, 71 insertions(+), 12 deletions(-) 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 4f100845e..83ff3280f 100644 --- a/src/core/features/course/components/course-format/course-format.ts +++ b/src/core/features/course/components/course-format/course-format.ts @@ -514,6 +514,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { CoreDomUtils.scrollViewToElement( this.elementRef.nativeElement, '#core-course-module-' + moduleId, + { addYAxis: -10 }, ); } 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 6643c02f8..2d2141df6 100644 --- a/src/core/features/course/components/course-index/course-index.ts +++ b/src/core/features/course/components/course-index/course-index.ts @@ -112,6 +112,7 @@ export class CoreCourseCourseIndexComponent implements OnInit { CoreDomUtils.scrollViewToElement( this.elementRef.nativeElement, '.item.item-current', + { addYAxis: -10 }, ); } diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 797faa9fb..ccb08d6f6 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -94,7 +94,7 @@ export class CoreDomUtilsProvider { } /** - * Wait an element to be in dom of another element. + * Wait an element to be added to the root DOM. * * @param element Element to wait. * @return Cancellable promise. @@ -130,6 +130,44 @@ export class CoreDomUtilsProvider { ); } + /** + * Wait an element to be in dom of another element using a selector + * + * @param container Element to wait. + * @return Cancellable promise. + */ + async waitToBeInsideElement(container: HTMLElement, selector: string): Promise> { + await CoreDomUtils.waitToBeInDOM(container); + + let element = container.querySelector(selector); + if (element) { + // Already in DOM. + return CoreCancellablePromise.resolve(element); + } + + let observer: MutationObserver; + + return new CoreCancellablePromise( + (resolve) => { + observer = new MutationObserver(() => { + element = container.querySelector(selector); + + if (!element) { + return; + } + + observer?.disconnect(); + resolve(element); + }); + + observer.observe(container, { subtree: true, childList: true }); + }, + () => { + observer?.disconnect(); + }, + ); + } + /** * Wait an element to be in dom and visible. * @@ -889,7 +927,7 @@ export class CoreDomUtilsProvider { * * @param findFunction The function used to find the element. * @return Resolved if found, rejected if too many tries. - * @deprecated since app 4.0 Use waitToBeInDOM instead. + * @deprecated since app 4.0 Use waitToBeInsideElement instead. */ waitElementToExist(findFunction: () => HTMLElement | null): Promise { const promiseInterval = CoreUtils.promiseDefer(); @@ -1320,14 +1358,12 @@ export class CoreDomUtilsProvider { * * @param element The element to scroll to. * @param selector Selector to find the element to scroll to inside the defined element. - * @param duration Duration of the scroll animation in milliseconds. + * @param scrollOptions Scroll Options. * @return Wether the scroll suceeded. */ - async scrollViewToElement(element: HTMLElement, selector?: string, duration = 0): Promise { - await CoreDomUtils.waitToBeInDOM(element); - + async scrollViewToElement(element: HTMLElement, selector?: string, scrollOptions: CoreScrollOptions = {}): Promise { if (selector) { - const foundElement = element.querySelector(selector); + const foundElement = await CoreDomUtils.waitToBeInsideElement(element, selector); if (!foundElement) { // Element not found. return false; @@ -1336,16 +1372,28 @@ export class CoreDomUtilsProvider { element = foundElement; } + await CoreDomUtils.waitToBeVisible(element); + const content = element.closest('ion-content') ?? undefined; if (!content) { + // Content to scroll, not found. return false; } try { const position = CoreDomUtils.getRelativeElementPosition(element, content); + const scrollElement = await content.getScrollElement(); - await content.scrollToPoint(position.x, position.y, duration); + scrollOptions.duration = scrollOptions.duration ?? 200; + scrollOptions.addXAxis = scrollOptions.addXAxis ?? 0; + scrollOptions.addYAxis = scrollOptions.addYAxis ?? 0; + + await content.scrollToPoint( + position.x + scrollElement.scrollLeft + scrollOptions.addXAxis, + position.y + scrollElement.scrollTop + scrollOptions.addYAxis, + scrollOptions.duration, + ); return true; } catch { @@ -1373,8 +1421,8 @@ export class CoreDomUtilsProvider { * @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); + scrollToElement(content: IonContent, element: HTMLElement, scrollParentClass?: string, duration?: number): boolean { + CoreDomUtils.scrollViewToElement(element, undefined, { duration }); return true; } @@ -1395,13 +1443,13 @@ export class CoreDomUtilsProvider { content: unknown | null, selector: string, scrollParentClass?: string, - duration = 0, + duration?: number, ): boolean { if (!container || !content) { return false; } - CoreDomUtils.scrollViewToElement(container, selector, duration); + CoreDomUtils.scrollViewToElement(container, selector, { duration }); return true; @@ -2435,3 +2483,12 @@ export type CoreCoordinates = { x: number; // X axis coordinates. y: number; // Y axis coordinates. }; + +/** + * Scroll options. + */ +export type CoreScrollOptions = { + duration?: number; + addYAxis?: number; + addXAxis?: number; +}; From 6a1b692dc52fcc612a6c52f8ec273c2635efef88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 22 Mar 2022 10:47:14 +0100 Subject: [PATCH 2/3] MOBILE-3814 dom: Move new function to a singleton class --- .../pages/discussion/discussion.page.ts | 3 +- src/addons/mod/data/pages/edit/edit.ts | 3 +- src/addons/mod/forum/components/post/post.ts | 3 +- .../forum/pages/discussion/discussion.page.ts | 3 +- .../preflight-modal/preflight-modal.ts | 3 +- .../mod/quiz/pages/player/player.page.ts | 3 +- .../mod/quiz/pages/review/review.page.ts | 3 +- .../ddimageortext/classes/ddimageortext.ts | 7 +- src/addons/qtype/ddmarker/classes/ddmarker.ts | 5 +- .../qtype/ddmarker/classes/graphics_api.ts | 4 +- src/addons/qtype/ddwtos/classes/ddwtos.ts | 13 +- .../navbar-buttons/navbar-buttons.ts | 3 +- src/core/directives/auto-focus.ts | 3 +- src/core/directives/collapsible-footer.ts | 5 +- src/core/directives/collapsible-item.ts | 3 +- src/core/directives/fab.ts | 6 +- src/core/directives/format-text.ts | 5 +- src/core/directives/link.ts | 3 +- src/core/directives/on-appear.ts | 4 +- src/core/features/compile/services/compile.ts | 2 + .../components/course-format/course-format.ts | 3 +- .../components/course-index/course-index.ts | 4 +- .../rich-text-editor/rich-text-editor.ts | 5 +- .../grades/pages/course/course.page.ts | 3 +- .../login/pages/email-signup/email-signup.ts | 3 +- .../components/user-tour/user-tour.ts | 3 +- src/core/services/utils/dom.ts | 393 +----------------- src/core/singletons/dom.ts | 390 +++++++++++++++++ 28 files changed, 468 insertions(+), 420 deletions(-) create mode 100644 src/core/singletons/dom.ts diff --git a/src/addons/messages/pages/discussion/discussion.page.ts b/src/addons/messages/pages/discussion/discussion.page.ts index 257881e38..0d4896837 100644 --- a/src/addons/messages/pages/discussion/discussion.page.ts +++ b/src/addons/messages/pages/discussion/discussion.page.ts @@ -45,6 +45,7 @@ import { CoreIonLoadingElement } from '@classes/ion-loading'; import { ActivatedRoute } from '@angular/router'; import { AddonMessagesConversationInfoComponent } from '../../components/conversation-info/conversation-info'; import { CoreConstants } from '@/core/constants'; +import { CoreDom } from '@singletons/dom'; /** * Page that displays a message discussion page. @@ -1109,7 +1110,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView if (this.newMessages > 0) { const messages = Array.from(this.hostElement.querySelectorAll('.addon-message-not-mine')); - CoreDomUtils.scrollViewToElement(messages[messages.length - this.newMessages]); + CoreDom.scrollToElement(messages[messages.length - this.newMessages]); } } diff --git a/src/addons/mod/data/pages/edit/edit.ts b/src/addons/mod/data/pages/edit/edit.ts index 87e4d0323..53a73c691 100644 --- a/src/addons/mod/data/pages/edit/edit.ts +++ b/src/addons/mod/data/pages/edit/edit.ts @@ -40,6 +40,7 @@ import { AddonModDataEntryWSField, } from '../../services/data'; import { AddonModDataHelper } from '../../services/data-helper'; +import { CoreDom } from '@singletons/dom'; /** * Page that displays the view edit page. @@ -448,7 +449,7 @@ export class AddonModDataEditPage implements OnInit { * Scroll to first error or to the top if not found. */ protected async scrollToFirstError(): Promise { - const scrolled = await CoreDomUtils.scrollViewToElement(this.formElement.nativeElement, '.addon-data-error'); + const scrolled = await CoreDom.scrollToElement(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 7d2a3e31c..5f0acd153 100644 --- a/src/addons/mod/forum/components/post/post.ts +++ b/src/addons/mod/forum/components/post/post.ts @@ -51,6 +51,7 @@ import { CoreRatingInfo } from '@features/rating/services/rating'; import { CoreForms } from '@singletons/form'; import { CoreFileEntry } from '@services/file-helper'; import { AddonModForumSharedPostFormData } from '../../pages/discussion/discussion.page'; +import { CoreDom } from '@singletons/dom'; /** * Components that shows a discussion post, its attachments and the action buttons allowed (reply, etc.). @@ -540,7 +541,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges * @return Promise resolved when done. */ protected async scrollToForm(): Promise { - await CoreDomUtils.scrollViewToElement( + await CoreDom.scrollToElement( this.elementRef.nativeElement, '#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 605821169..a59703a50 100644 --- a/src/addons/mod/forum/pages/discussion/discussion.page.ts +++ b/src/addons/mod/forum/pages/discussion/discussion.page.ts @@ -33,6 +33,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { Network, NgZone, Translate } from '@singletons'; import { CoreArray } from '@singletons/array'; +import { CoreDom } from '@singletons/dom'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { Subscription } from 'rxjs'; import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source'; @@ -187,7 +188,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes const scrollTo = this.postId || this.parent; if (scrollTo) { // Scroll to the post. - CoreDomUtils.scrollViewToElement( + CoreDom.scrollToElement( 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 69685cdf3..150ac1df1 100644 --- a/src/addons/mod/quiz/components/preflight-modal/preflight-modal.ts +++ b/src/addons/mod/quiz/components/preflight-modal/preflight-modal.ts @@ -21,6 +21,7 @@ import { CoreForms } from '@singletons/form'; import { ModalController, Translate } from '@singletons'; import { AddonModQuizAccessRuleDelegate } from '../../services/access-rules-delegate'; import { AddonModQuizAttemptWSData, AddonModQuizQuizWSData } from '../../services/quiz'; +import { CoreDom } from '@singletons/dom'; /** * Modal that renders the access rules for a quiz. @@ -115,7 +116,7 @@ export class AddonModQuizPreflightModalComponent implements OnInit { if (!this.preflightForm.valid) { // Form not valid. Scroll to the first element with errors. - const hasScrolled = await CoreDomUtils.scrollViewToInputError( + const hasScrolled = await CoreDom.scrollToInputError( this.elementRef.nativeElement, ); diff --git a/src/addons/mod/quiz/pages/player/player.page.ts b/src/addons/mod/quiz/pages/player/player.page.ts index 4a01e09de..c793a18f1 100644 --- a/src/addons/mod/quiz/pages/player/player.page.ts +++ b/src/addons/mod/quiz/pages/player/player.page.ts @@ -46,6 +46,7 @@ import { AddonModQuizAttempt, AddonModQuizHelper } from '../../services/quiz-hel import { AddonModQuizSync } from '../../services/quiz-sync'; import { CanLeave } from '@guards/can-leave'; import { CoreForms } from '@singletons/form'; +import { CoreDom } from '@singletons/dom'; /** * Page that allows attempting a quiz. @@ -687,7 +688,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { * @param slot Slot of the question to scroll to. */ protected scrollToQuestion(slot: number): void { - CoreDomUtils.scrollViewToElement( + CoreDom.scrollToElement( this.elementRef.nativeElement, '#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 2ebdebe5e..5443ffe30 100644 --- a/src/addons/mod/quiz/pages/review/review.page.ts +++ b/src/addons/mod/quiz/pages/review/review.page.ts @@ -22,6 +22,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; +import { CoreDom } from '@singletons/dom'; import { AddonModQuizNavigationModalComponent, AddonModQuizNavigationModalReturn, @@ -247,7 +248,7 @@ export class AddonModQuizReviewPage implements OnInit { * @param slot Slot of the question to scroll to. */ protected scrollToQuestion(slot: number): void { - CoreDomUtils.scrollViewToElement( + CoreDom.scrollToElement( this.elementRef.nativeElement, `#addon-mod_quiz-question-${slot}`, ); diff --git a/src/addons/qtype/ddimageortext/classes/ddimageortext.ts b/src/addons/qtype/ddimageortext/classes/ddimageortext.ts index bdebeaabf..3adb8a067 100644 --- a/src/addons/qtype/ddimageortext/classes/ddimageortext.ts +++ b/src/addons/qtype/ddimageortext/classes/ddimageortext.ts @@ -14,6 +14,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; +import { CoreDom } from '@singletons/dom'; import { CoreEventObserver } from '@singletons/events'; import { CoreLogger } from '@singletons/logger'; import { AddonModQuizDdImageOrTextQuestionData } from '../component/ddimageortext'; @@ -83,7 +84,7 @@ export class AddonQtypeDdImageOrTextQuestion { return bgImgXY; } - const position = CoreDomUtils.getRelativeElementPosition(bgImg, ddArea); + const position = CoreDom.getRelativeElementPosition(bgImg, ddArea); // Render the position related to the current image dimensions. bgImgXY[0] *= this.proportion; @@ -419,7 +420,7 @@ export class AddonQtypeDdImageOrTextQuestion { return; } - const position = CoreDomUtils.getRelativeElementPosition(drop, ddArea); + const position = CoreDom.getRelativeElementPosition(drop, ddArea); const choice = drag.getAttribute('choice'); drag.style.left = position.x + 'px'; drag.style.top = position.y + 'px'; @@ -473,7 +474,7 @@ export class AddonQtypeDdImageOrTextQuestion { return; } - const position = CoreDomUtils.getRelativeElementPosition(dragItemHome, ddArea); + const position = CoreDom.getRelativeElementPosition(dragItemHome, ddArea); drag.style.left = position.x + 'px'; drag.style.top = position.y + 'px'; drag.classList.remove('placed'); diff --git a/src/addons/qtype/ddmarker/classes/ddmarker.ts b/src/addons/qtype/ddmarker/classes/ddmarker.ts index 89b5a997d..a1c79281f 100644 --- a/src/addons/qtype/ddmarker/classes/ddmarker.ts +++ b/src/addons/qtype/ddmarker/classes/ddmarker.ts @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreCoordinates, CoreDomUtils } from '@services/utils/dom'; +import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreCoordinates, CoreDom } from '@singletons/dom'; import { CoreEventObserver } from '@singletons/events'; import { CoreLogger } from '@singletons/logger'; import { AddonQtypeDdMarkerQuestionData } from '../component/ddmarker'; @@ -134,7 +135,7 @@ export class AddonQtypeDdMarkerQuestion { return []; } - const position = CoreDomUtils.getRelativeElementPosition(element, ddArea); + const position = CoreDom.getRelativeElementPosition(element, ddArea); return [position.x, position.y]; } diff --git a/src/addons/qtype/ddmarker/classes/graphics_api.ts b/src/addons/qtype/ddmarker/classes/graphics_api.ts index 1c66def35..4b865f6c0 100644 --- a/src/addons/qtype/ddmarker/classes/graphics_api.ts +++ b/src/addons/qtype/ddmarker/classes/graphics_api.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreDom } from '@singletons/dom'; import { AddonQtypeDdMarkerQuestion } from './ddmarker'; /** @@ -59,7 +59,7 @@ export class AddonQtypeDdMarkerGraphicsApi { return; } - const position = CoreDomUtils.getRelativeElementPosition(bgImg, ddArea); + const position = CoreDom.getRelativeElementPosition(bgImg, ddArea); dropZones.style.left = position.x + 'px'; dropZones.style.top = position.y + 'px'; diff --git a/src/addons/qtype/ddwtos/classes/ddwtos.ts b/src/addons/qtype/ddwtos/classes/ddwtos.ts index 60be1af6d..7366d4d84 100644 --- a/src/addons/qtype/ddwtos/classes/ddwtos.ts +++ b/src/addons/qtype/ddwtos/classes/ddwtos.ts @@ -13,10 +13,11 @@ // limitations under the License. import { CoreFormatTextDirective } from '@directives/format-text'; -import { CoreCoordinates, CoreDomUtils } from '@services/utils/dom'; +import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreCoordinates, CoreDom } from '@singletons/dom'; import { CoreEventObserver } from '@singletons/events'; import { CoreLogger } from '@singletons/logger'; import { AddonModQuizDdwtosQuestionData } from '../component/ddwtos'; @@ -389,13 +390,13 @@ export class AddonQtypeDdwtosQuestion { const choiceNo = this.getChoice(drag) ?? -1; const dragHome = this.container.querySelector(this.selectors.dragHome(groupNo, choiceNo)); if (dragHome) { - position = CoreDomUtils.getRelativeElementPosition(dragHome, parent); + position = CoreDom.getRelativeElementPosition(dragHome, parent); } } else { // Get the drop zone position. const dropZone = this.container.querySelector(this.selectors.dropForPlace(placeNo)); if (dropZone) { - position = CoreDomUtils.getRelativeElementPosition(dropZone, parent); + position = CoreDom.getRelativeElementPosition(dropZone, parent); // Avoid the border. position.x++; position.y++; @@ -425,13 +426,13 @@ export class AddonQtypeDdwtosQuestion { * @return Promise resolved when ready in the DOM. */ protected async waitForReady(): Promise { - await CoreDomUtils.waitToBeInDOM(this.container); + await CoreDom.waitToBeInDOM(this.container); await CoreComponentsRegistry.waitComponentsReady(this.container, 'core-format-text', CoreFormatTextDirective); const drag = Array.from(this.container.querySelectorAll(this.selectors.dragHomes()))[0]; - await CoreDomUtils.waitToBeInDOM(drag); + await CoreDom.waitToBeInDOM(drag); } /** @@ -480,7 +481,7 @@ export class AddonQtypeDdwtosQuestion { return; } - await CoreDomUtils.waitToBeInDOM(groupItems[0]); + await CoreDom.waitToBeInDOM(groupItems[0]); let maxWidth = 0; let maxHeight = 0; diff --git a/src/core/components/navbar-buttons/navbar-buttons.ts b/src/core/components/navbar-buttons/navbar-buttons.ts index 96bcf0c21..8655a1bae 100644 --- a/src/core/components/navbar-buttons/navbar-buttons.ts +++ b/src/core/components/navbar-buttons/navbar-buttons.ts @@ -26,6 +26,7 @@ import { CoreLogger } from '@singletons/logger'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreContextMenuComponent } from '../context-menu/context-menu'; import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDom } from '@singletons/dom'; const BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden'; @@ -197,7 +198,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { * @return Promise resolved with the header element. */ protected async searchHeader(): Promise { - await CoreDomUtils.waitToBeInDOM(this.element); + await CoreDom.waitToBeInDOM(this.element); let parentPage: HTMLElement | null = this.element; while (parentPage && parentPage.parentElement) { diff --git a/src/core/directives/auto-focus.ts b/src/core/directives/auto-focus.ts index 152d3a5e4..9a37a208f 100644 --- a/src/core/directives/auto-focus.ts +++ b/src/core/directives/auto-focus.ts @@ -16,6 +16,7 @@ import { Directive, Input, ElementRef, AfterViewInit } from '@angular/core'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; +import { CoreDom } from '@singletons/dom'; /** * Directive to auto focus an element when a view is loaded. @@ -46,7 +47,7 @@ export class CoreAutoFocusDirective implements AfterViewInit { return; } - await CoreDomUtils.waitToBeInDOM(this.element); + await CoreDom.waitToBeInDOM(this.element); let focusElement = this.element; diff --git a/src/core/directives/collapsible-footer.ts b/src/core/directives/collapsible-footer.ts index 57f24d707..4a44fa0b9 100644 --- a/src/core/directives/collapsible-footer.ts +++ b/src/core/directives/collapsible-footer.ts @@ -23,6 +23,7 @@ import { CoreEventObserver } from '@singletons/events'; import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; +import { CoreDom } from '@singletons/dom'; /** * Directive to make an element fixed at the bottom collapsible when scrolling. @@ -62,7 +63,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { async ngOnInit(): Promise { // Only if not present or explicitly falsy it will be false. this.appearOnBottom = !CoreUtils.isFalseOrZero(this.appearOnBottom); - this.domPromise = CoreDomUtils.waitToBeInDOM(this.element); + this.domPromise = CoreDom.waitToBeInDOM(this.element); await this.domPromise; await this.waitLoadingsDone(); @@ -72,7 +73,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { await this.calculateHeight(); - CoreDomUtils.onElementSlot(this.element, () => { + CoreDom.onElementSlot(this.element, () => { this.calculateHeight(); }); diff --git a/src/core/directives/collapsible-item.ts b/src/core/directives/collapsible-item.ts index caffa6270..f0839d8eb 100644 --- a/src/core/directives/collapsible-item.ts +++ b/src/core/directives/collapsible-item.ts @@ -19,6 +19,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDom } from '@singletons/dom'; import { CoreEventObserver } from '@singletons/events'; import { CoreFormatTextDirective } from './format-text'; @@ -100,7 +101,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { * @return Promise resolved when loadings are done. */ protected async waitLoadingsDone(): Promise { - this.domPromise = CoreDomUtils.waitToBeInDOM(this.element); + this.domPromise = CoreDom.waitToBeInDOM(this.element); await this.domPromise; diff --git a/src/core/directives/fab.ts b/src/core/directives/fab.ts index 2fb399c2e..90f700fc5 100644 --- a/src/core/directives/fab.ts +++ b/src/core/directives/fab.ts @@ -14,7 +14,7 @@ import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreDom } from '@singletons/dom'; /** * Directive to move ion-fab components as direct children of the nearest ion-content. @@ -42,7 +42,7 @@ export class CoreFabDirective implements OnInit, OnDestroy { * @inheritdoc */ async ngOnInit(): Promise { - this.domPromise = CoreDomUtils.waitToBeInDOM(this.element); + this.domPromise = CoreDom.waitToBeInDOM(this.element); await this.domPromise; this.content = this.element.closest('ion-content'); @@ -56,7 +56,7 @@ export class CoreFabDirective implements OnInit, OnDestroy { await this.calculatePlace(); - CoreDomUtils.onElementSlot(this.element, () => { + CoreDom.onElementSlot(this.element, () => { this.calculatePlace(); }); } diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 3000967d3..f134eddd6 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -45,6 +45,7 @@ import { CoreCollapsibleItemDirective } from './collapsible-item'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { AsyncComponent } from '@classes/async-component'; import { CoreText } from '@singletons/text'; +import { CoreDom } from '@singletons/dom'; /** * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective @@ -552,7 +553,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo */ protected async getElementWidth(): Promise { if (!this.domElementPromise) { - this.domElementPromise = CoreDomUtils.waitToBeInDOM(this.element); + this.domElementPromise = CoreDom.waitToBeInDOM(this.element); } await this.domElementPromise; @@ -704,7 +705,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo newUrl += `&h=${privacyHash}`; } - const domPromise = CoreDomUtils.waitToBeInDOM(iframe); + const domPromise = CoreDom.waitToBeInDOM(iframe); this.domPromises.push(domPromise); await domPromise; diff --git a/src/core/directives/link.ts b/src/core/directives/link.ts index 18a26de30..37043ffc8 100644 --- a/src/core/directives/link.ts +++ b/src/core/directives/link.ts @@ -28,6 +28,7 @@ import { CoreCustomURLSchemes } from '@services/urlschemes'; import { DomSanitizer } from '@singletons'; import { CoreFilepool } from '@services/filepool'; import { CoreUrl } from '@singletons/url'; +import { CoreDom } from '@singletons/dom'; /** * Directive to open a link in external browser or in the app. @@ -144,7 +145,7 @@ export class CoreLinkDirective implements OnInit { href = href.substring(1); const container = this.element.closest('ion-content'); if (container) { - CoreDomUtils.scrollViewToElement( + CoreDom.scrollToElement( container, `#${href}, [name='${href}']`, ); diff --git a/src/core/directives/on-appear.ts b/src/core/directives/on-appear.ts index d3e9bd2da..4dff653c9 100644 --- a/src/core/directives/on-appear.ts +++ b/src/core/directives/on-appear.ts @@ -14,7 +14,7 @@ import { Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreDom } from '@singletons/dom'; /** * Directive to listen when an element becomes visible. @@ -37,7 +37,7 @@ export class CoreOnAppearDirective implements OnInit, OnDestroy { * @inheritdoc */ async ngOnInit(): Promise { - this.visiblePromise = CoreDomUtils.waitToBeInViewport(this.element); + this.visiblePromise = CoreDom.waitToBeInViewport(this.element); await this.visiblePromise; diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 929371c91..0f9f72c22 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -151,6 +151,7 @@ import { ADDON_PRIVATEFILES_SERVICES } from '@addons/privatefiles/privatefiles.m // Import some addon modules that define components, directives and pipes. Only import the important ones. import { AddonModAssignComponentsModule } from '@addons/mod/assign/components/components.module'; import { AddonModWorkshopComponentsModule } from '@addons/mod/workshop/components/components.module'; +import { CoreDom } from '@singletons/dom'; /** * Service to provide functionalities regarding compiling dynamic HTML and Javascript. @@ -341,6 +342,7 @@ export class CoreCompileProvider { instance['Md5'] = Md5; instance['CoreSyncBaseProvider'] = CoreSyncBaseProvider; instance['CoreArray'] = CoreArray; + instance['CoreDom'] = CoreDom; instance['CoreText'] = CoreText; instance['CoreUrl'] = CoreUrl; instance['CoreWindow'] = CoreWindow; 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 83ff3280f..dca3d5443 100644 --- a/src/core/features/course/components/course-format/course-format.ts +++ b/src/core/features/course/components/course-format/course-format.ts @@ -48,6 +48,7 @@ import { CoreCourseModuleDelegate } from '@features/course/services/module-deleg import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course'; import { CoreUserTours, CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours'; import { CoreCourseCourseIndexTourComponent } from '../course-index-tour/course-index-tour'; +import { CoreDom } from '@singletons/dom'; /** * Component to display course contents using a certain format. If the format isn't found, use default one. @@ -511,7 +512,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * @param moduleId Module ID. */ protected scrollToModule(moduleId: number): void { - CoreDomUtils.scrollViewToElement( + CoreDom.scrollToElement( this.elementRef.nativeElement, '#core-course-module-' + moduleId, { addYAxis: -10 }, 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 2d2141df6..f2a66fdde 100644 --- a/src/core/features/course/components/course-index/course-index.ts +++ b/src/core/features/course/components/course-index/course-index.ts @@ -21,8 +21,8 @@ import { 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 { CoreDomUtils } from '@services/utils/dom'; import { ModalController } from '@singletons'; +import { CoreDom } from '@singletons/dom'; /** * Component to display course index modal. @@ -109,7 +109,7 @@ export class CoreCourseCourseIndexComponent implements OnInit { this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course); - CoreDomUtils.scrollViewToElement( + CoreDom.scrollToElement( this.elementRef.nativeElement, '.item.item-current', { addYAxis: -10 }, 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 370816d78..76e9a1c5b 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 @@ -41,6 +41,7 @@ import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreScreen } from '@services/screen'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; +import { CoreDom } from '@singletons/dom'; /** * Component to display a rich text editor if enabled. @@ -290,7 +291,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * @return Promise resolved when loadings are done. */ protected async waitLoadingsDone(): Promise { - this.domPromise = CoreDomUtils.waitToBeInDOM(this.element); + this.domPromise = CoreDom.waitToBeInDOM(this.element); await this.domPromise; @@ -846,7 +847,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, // Cancel previous one, if any. this.buttonsDomPromise?.cancel(); - this.buttonsDomPromise = CoreDomUtils.waitToBeInDOM(this.toolbar.nativeElement); + this.buttonsDomPromise = CoreDom.waitToBeInDOM(this.toolbar.nativeElement); await this.buttonsDomPromise; const width = this.toolbar.nativeElement.getBoundingClientRect().width; diff --git a/src/core/features/grades/pages/course/course.page.ts b/src/core/features/grades/pages/course/course.page.ts index ccb3c92d3..cdecf5322 100644 --- a/src/core/features/grades/pages/course/course.page.ts +++ b/src/core/features/grades/pages/course/course.page.ts @@ -31,6 +31,7 @@ import { Translate } from '@singletons'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreGradesCoursesSource } from '@features/grades/classes/grades-courses-source'; +import { CoreDom } from '@singletons/dom'; /** * Page that displays a course grades. @@ -170,7 +171,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { if (row) { this.toggleRow(row, true); - CoreDomUtils.scrollViewToElement( + CoreDom.scrollToElement( this.element.nativeElement, '#grade-' + row.id, ); 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 7e18428b1..8f130cd45 100644 --- a/src/core/features/login/pages/email-signup/email-signup.ts +++ b/src/core/features/login/pages/email-signup/email-signup.ts @@ -35,6 +35,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreForms } from '@singletons/form'; import { CoreRecaptchaComponent } from '@components/recaptcha/recaptcha'; import { CoreText } from '@singletons/text'; +import { CoreDom } from '@singletons/dom'; /** * Page to signup using email. @@ -284,7 +285,7 @@ export class CoreLoginEmailSignupPage implements OnInit { this.changeDetector.detectChanges(); // Scroll to the first element with errors. - const errorFound = await CoreDomUtils.scrollViewToInputError( + const errorFound = await CoreDom.scrollToInputError( this.elementRef.nativeElement, ); diff --git a/src/core/features/usertours/components/user-tour/user-tour.ts b/src/core/features/usertours/components/user-tour/user-tour.ts index 65f6e63dd..fae112402 100644 --- a/src/core/features/usertours/components/user-tour/user-tour.ts +++ b/src/core/features/usertours/components/user-tour/user-tour.ts @@ -21,6 +21,7 @@ import { CoreUserTours, CoreUserToursAlignment, CoreUserToursSide } from '@featu import { CoreDomUtils } from '@services/utils/dom'; import { AngularFrameworkDelegate } from '@singletons'; import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDom } from '@singletons/dom'; const ANIMATION_DURATION = 200; const USER_TOURS_BACK_BUTTON_PRIORITY = 100; @@ -86,7 +87,7 @@ export class CoreUserToursUserTourComponent implements AfterViewInit { await CoreDomUtils.waitForImages(tour); // Calculate focus styles or dismiss if the element is gone. - if (this.focus && !CoreDomUtils.isElementVisible(this.focus)) { + if (this.focus && !CoreDom.isElementVisible(this.focus)) { await this.dismiss(false); return; diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index ccb08d6f6..b42da0973 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -54,7 +54,7 @@ import { filter } from 'rxjs/operators'; import { Subscription } from 'rxjs'; import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreEventObserver } from '@singletons/events'; -import { CoreCancellablePromise } from '@classes/cancellable-promise'; +import { CoreDom } from '@singletons/dom'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -93,218 +93,6 @@ export class CoreDomUtilsProvider { this.debugDisplay = debugDisplay != 0; } - /** - * Wait an element to be added to the root DOM. - * - * @param element Element to wait. - * @return Cancellable promise. - */ - waitToBeInDOM(element: HTMLElement): CoreCancellablePromise { - const root = element.getRootNode({ composed: true }); - - if (root === document) { - // Already in DOM. - return CoreCancellablePromise.resolve(); - } - - let observer: MutationObserver; - - return new CoreCancellablePromise( - (resolve) => { - observer = new MutationObserver(() => { - const root = element.getRootNode({ composed: true }); - - if (root !== document) { - return; - } - - observer?.disconnect(); - resolve(); - }); - - observer.observe(document.body, { subtree: true, childList: true }); - }, - () => { - observer?.disconnect(); - }, - ); - } - - /** - * Wait an element to be in dom of another element using a selector - * - * @param container Element to wait. - * @return Cancellable promise. - */ - async waitToBeInsideElement(container: HTMLElement, selector: string): Promise> { - await CoreDomUtils.waitToBeInDOM(container); - - let element = container.querySelector(selector); - if (element) { - // Already in DOM. - return CoreCancellablePromise.resolve(element); - } - - let observer: MutationObserver; - - return new CoreCancellablePromise( - (resolve) => { - observer = new MutationObserver(() => { - element = container.querySelector(selector); - - if (!element) { - return; - } - - observer?.disconnect(); - resolve(element); - }); - - observer.observe(container, { subtree: true, childList: true }); - }, - () => { - observer?.disconnect(); - }, - ); - } - - /** - * Wait an element to be in dom and visible. - * - * @param element Element to wait. - * @return Cancellable promise. - */ - waitToBeVisible(element: HTMLElement): CoreCancellablePromise { - const domPromise = CoreDomUtils.waitToBeInDOM(element); - - let interval: number | undefined; - - // Mutations did not observe for visibility properties. - return new CoreCancellablePromise( - async (resolve) => { - await domPromise; - - if (CoreDomUtils.isElementVisible(element)) { - return resolve(); - } - - interval = window.setInterval(() => { - if (!CoreDomUtils.isElementVisible(element)) { - return; - } - - resolve(); - window.clearInterval(interval); - }, 50); - }, - () => { - domPromise.cancel(); - window.clearInterval(interval); - }, - ); - } - - /** - * Wait an element to be in dom and visible. - * - * @param element Element to wait. - * @param intersectionRatio Intersection ratio (From 0 to 1). - * @return Cancellable promise. - */ - waitToBeInViewport(element: HTMLElement, intersectionRatio = 1): CoreCancellablePromise { - const visiblePromise = CoreDomUtils.waitToBeVisible(element); - - let intersectionObserver: IntersectionObserver; - let interval: number | undefined; - - return new CoreCancellablePromise( - async (resolve) => { - await visiblePromise; - - if (CoreDomUtils.isElementInViewport(element, intersectionRatio)) { - - return resolve(); - } - - if ('IntersectionObserver' in window) { - intersectionObserver = new IntersectionObserver((observerEntries) => { - const isIntersecting = observerEntries - .some((entry) => entry.isIntersecting && entry.intersectionRatio >= intersectionRatio); - if (!isIntersecting) { - return; - } - - resolve(); - intersectionObserver?.disconnect(); - }); - - intersectionObserver.observe(element); - } else { - interval = window.setInterval(() => { - if (!CoreDomUtils.isElementInViewport(element, intersectionRatio)) { - return; - } - - resolve(); - window.clearInterval(interval); - }, 50); - } - }, - () => { - visiblePromise.cancel(); - intersectionObserver?.disconnect(); - window.clearInterval(interval); - }, - ); - } - - /** - * 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. @@ -609,11 +397,9 @@ export class CoreDomUtilsProvider { * @return Selection contents. Undefined if not found. */ getContentsOfElement(element: HTMLElement, selector: string): string | undefined { - if (element) { - const selected = element.querySelector(selector); - if (selected) { - return selected.innerHTML; - } + const selected = element.querySelector(selector); + if (selected) { + return selected.innerHTML; } } @@ -762,7 +548,7 @@ export class CoreDomUtilsProvider { * @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. + * @deprecated since app 4.0. Use CoreDom.getRelativeElementPosition instead. */ getElementXY(element: HTMLElement, selector?: string, positionParentClass = 'inner-scroll'): [number, number] | null { if (selector) { @@ -780,7 +566,7 @@ export class CoreDomUtilsProvider { return null; } - const position = CoreDomUtils.getRelativeElementPosition(element, parent); + const position = CoreDom.getRelativeElementPosition(element, parent); // Calculate the top and left positions. return [ @@ -789,25 +575,6 @@ export class CoreDomUtilsProvider { ]; } - /** - * 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, - }; - } - /** * Given a message, it deduce if it's a network error. * @@ -927,7 +694,7 @@ export class CoreDomUtilsProvider { * * @param findFunction The function used to find the element. * @return Resolved if found, rejected if too many tries. - * @deprecated since app 4.0 Use waitToBeInsideElement instead. + * @deprecated since app 4.0 Use CoreDom.waitToBeInsideElement instead. */ waitElementToExist(findFunction: () => HTMLElement | null): Promise { const promiseInterval = CoreUtils.promiseDefer(); @@ -1031,63 +798,6 @@ export class CoreDomUtilsProvider { return elementPoint > window.innerHeight || elementPoint < scrollTopPos; } - /** - * Check whether an element has been added to the DOM. - * - * @param element Element. - * @return True if element has been added to the DOM, false otherwise. - */ - isElementInDom(element: HTMLElement): boolean { - return element.getRootNode({ composed: true }) === document; - } - - /** - * Check whether an element is visible or not. - * - * @param element Element. - * @return True if element is visible inside the DOM. - */ - isElementVisible(element: HTMLElement): boolean { - if (element.clientWidth === 0 || element.clientHeight === 0) { - return false; - } - - const style = getComputedStyle(element); - if (style.opacity === '0' || style.display === 'none' || style.visibility === 'hidden') { - return false; - } - - return CoreDomUtils.isElementInDom(element); - } - - /** - * Check whether an element is intersecting the intersectionRatio in viewport. - * - * @param element - * @param intersectionRatio Intersection ratio (From 0 to 1). - * @return True if in viewport. - */ - isElementInViewport(element: HTMLElement, intersectionRatio = 1): boolean { - const elementRectangle = element.getBoundingClientRect(); - - const elementArea = elementRectangle.width * elementRectangle.height; - if (elementArea == 0) { - return false; - } - - const intersectionRectangle = { - top: Math.max(0, elementRectangle.top), - left: Math.max(0, elementRectangle.left), - bottom: Math.min(window.innerHeight, elementRectangle.bottom), - right: Math.min(window.innerWidth, elementRectangle.right), - }; - - const intersectionArea = (intersectionRectangle.right - intersectionRectangle.left) * - (intersectionRectangle.bottom - intersectionRectangle.top); - - return intersectionArea / elementArea >= intersectionRatio; - } - /** * Check if rich text editor is enabled. * @@ -1289,7 +999,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. */ - scrollToBottom(content: IonContent, duration?: number): Promise { + scrollToBottom(content: IonContent, duration = 0): Promise { return content.scrollToBottom(duration); } @@ -1353,64 +1063,6 @@ export class CoreDomUtilsProvider { } } - /** - * Scroll to a certain element. - * - * @param element The element to scroll to. - * @param selector Selector to find the element to scroll to inside the defined element. - * @param scrollOptions Scroll Options. - * @return Wether the scroll suceeded. - */ - async scrollViewToElement(element: HTMLElement, selector?: string, scrollOptions: CoreScrollOptions = {}): Promise { - if (selector) { - const foundElement = await CoreDomUtils.waitToBeInsideElement(element, selector); - if (!foundElement) { - // Element not found. - return false; - } - - element = foundElement; - } - - await CoreDomUtils.waitToBeVisible(element); - - const content = element.closest('ion-content') ?? undefined; - if (!content) { - - // Content to scroll, not found. - return false; - } - - try { - const position = CoreDomUtils.getRelativeElementPosition(element, content); - const scrollElement = await content.getScrollElement(); - - scrollOptions.duration = scrollOptions.duration ?? 200; - scrollOptions.addXAxis = scrollOptions.addXAxis ?? 0; - scrollOptions.addYAxis = scrollOptions.addYAxis ?? 0; - - await content.scrollToPoint( - position.x + scrollElement.scrollLeft + scrollOptions.addXAxis, - position.y + scrollElement.scrollTop + scrollOptions.addYAxis, - scrollOptions.duration, - ); - - return true; - } catch { - return false; - } - } - - /** - * 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. - */ - async scrollViewToInputError(container: HTMLElement): Promise { - return this.scrollViewToElement(container, '.core-input-error'); - } - /** * Scroll to a certain element. * @@ -1419,10 +1071,10 @@ export class CoreDomUtilsProvider { * @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. + * @deprecated since app 4.0 Use CoreDom.scrollToElement instead. */ scrollToElement(content: IonContent, element: HTMLElement, scrollParentClass?: string, duration?: number): boolean { - CoreDomUtils.scrollViewToElement(element, undefined, { duration }); + CoreDom.scrollToElement(element, undefined, { duration }); return true; } @@ -1436,7 +1088,7 @@ export class CoreDomUtilsProvider { * @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. + * @deprecated since app 4.0 Use CoreDom.scrollToElement instead. */ scrollToElementBySelector( container: HTMLElement | null, @@ -1449,7 +1101,7 @@ export class CoreDomUtilsProvider { return false; } - CoreDomUtils.scrollViewToElement(container, selector, { duration }); + CoreDom.scrollToElement(container, selector, { duration }); return true; @@ -1460,14 +1112,14 @@ export class CoreDomUtilsProvider { * * @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. + * @deprecated since app 4.0 Use CoreDom.scrollToInputError instead. */ scrollToInputError(container: HTMLElement | null): boolean { if (!container) { return false; } - this.scrollViewToInputError(container); + CoreDom.scrollToInputError(container); return true; } @@ -2475,20 +2127,3 @@ export enum VerticalPoint { MID = 'mid', BOTTOM = 'bottom', } - -/** - * Coordinates of an element. - */ -export type CoreCoordinates = { - x: number; // X axis coordinates. - y: number; // Y axis coordinates. -}; - -/** - * Scroll options. - */ -export type CoreScrollOptions = { - duration?: number; - addYAxis?: number; - addXAxis?: number; -}; diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts new file mode 100644 index 000000000..7bb8be177 --- /dev/null +++ b/src/core/singletons/dom.ts @@ -0,0 +1,390 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreCancellablePromise } from '@classes/cancellable-promise'; + +/** + * Singleton with helper functions for dom. + */ +export class CoreDom { + + // Avoid creating singleton instances. + private constructor() { + // Nothing to do. + } + + /** + * 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. + */ + static 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, + }; + } + + /** + * Check whether an element has been added to the DOM. + * + * @param element Element. + * @return True if element has been added to the DOM, false otherwise. + */ + static isElementInDom(element: HTMLElement): boolean { + return element.getRootNode({ composed: true }) === document; + } + + /** + * Check whether an element is intersecting the intersectionRatio in viewport. + * + * @param element + * @param intersectionRatio Intersection ratio (From 0 to 1). + * @return True if in viewport. + */ + static isElementInViewport(element: HTMLElement, intersectionRatio = 1): boolean { + const elementRectangle = element.getBoundingClientRect(); + + const elementArea = elementRectangle.width * elementRectangle.height; + if (elementArea == 0) { + return false; + } + + const intersectionRectangle = { + top: Math.max(0, elementRectangle.top), + left: Math.max(0, elementRectangle.left), + bottom: Math.min(window.innerHeight, elementRectangle.bottom), + right: Math.min(window.innerWidth, elementRectangle.right), + }; + + const intersectionArea = (intersectionRectangle.right - intersectionRectangle.left) * + (intersectionRectangle.bottom - intersectionRectangle.top); + + return intersectionArea / elementArea >= intersectionRatio; + } + + /** + * Check whether an element is visible or not. + * + * @param element Element. + * @return True if element is visible inside the DOM. + */ + static isElementVisible(element: HTMLElement): boolean { + if (element.clientWidth === 0 || element.clientHeight === 0) { + return false; + } + + const style = getComputedStyle(element); + if (style.opacity === '0' || style.display === 'none' || style.visibility === 'hidden') { + return false; + } + + return CoreDom.isElementInDom(element); + } + + /** + * 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. + */ + static 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);; + } + + /** + * Scroll to a certain element. + * + * @param element The element to scroll to. + * @param selector Selector to find the element to scroll to inside the defined element. + * @param scrollOptions Scroll Options. + * @return Wether the scroll suceeded. + */ + static async scrollToElement(element: HTMLElement, selector?: string, scrollOptions: CoreScrollOptions = {}): Promise { + if (selector) { + const foundElement = await CoreDom.waitToBeInsideElement(element, selector); + if (!foundElement) { + // Element not found. + return false; + } + + element = foundElement; + } + + await CoreDom.waitToBeVisible(element); + + const content = element.closest('ion-content') ?? undefined; + if (!content) { + + // Content to scroll, not found. + return false; + } + + try { + const position = CoreDom.getRelativeElementPosition(element, content); + const scrollElement = await content.getScrollElement(); + + scrollOptions.duration = scrollOptions.duration ?? 200; + scrollOptions.addXAxis = scrollOptions.addXAxis ?? 0; + scrollOptions.addYAxis = scrollOptions.addYAxis ?? 0; + + await content.scrollToPoint( + position.x + scrollElement.scrollLeft + scrollOptions.addXAxis, + position.y + scrollElement.scrollTop + scrollOptions.addYAxis, + scrollOptions.duration, + ); + + return true; + } catch { + return false; + } + } + + /** + * 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. + */ + static async scrollToInputError(container: HTMLElement): Promise { + return CoreDom.scrollToElement(container, '.core-input-error'); + } + + /** + * Wait an element to be added to the root DOM. + * + * @param element Element to wait. + * @return Cancellable promise. + */ + static waitToBeInDOM(element: HTMLElement): CoreCancellablePromise { + const root = element.getRootNode({ composed: true }); + + if (root === document) { + // Already in DOM. + return CoreCancellablePromise.resolve(); + } + + let observer: MutationObserver; + + return new CoreCancellablePromise( + (resolve) => { + observer = new MutationObserver(() => { + const root = element.getRootNode({ composed: true }); + + if (root !== document) { + return; + } + + observer?.disconnect(); + resolve(); + }); + + observer.observe(document.body, { subtree: true, childList: true }); + }, + () => { + observer?.disconnect(); + }, + ); + } + + /** + * Wait an element to be in dom of another element using a selector + * + * @param container Element to wait. + * @return Cancellable promise. + */ + static async waitToBeInsideElement(container: HTMLElement, selector: string): Promise> { + await CoreDom.waitToBeInDOM(container); + + let element = container.querySelector(selector); + if (element) { + // Already in DOM. + return CoreCancellablePromise.resolve(element); + } + + let observer: MutationObserver; + + return new CoreCancellablePromise( + (resolve) => { + observer = new MutationObserver(() => { + element = container.querySelector(selector); + + if (!element) { + return; + } + + observer?.disconnect(); + resolve(element); + }); + + observer.observe(container, { subtree: true, childList: true }); + }, + () => { + observer?.disconnect(); + }, + ); + } + + /** + * Wait an element to be in dom and visible. + * + * @param element Element to wait. + * @param intersectionRatio Intersection ratio (From 0 to 1). + * @return Cancellable promise. + */ + static waitToBeInViewport(element: HTMLElement, intersectionRatio = 1): CoreCancellablePromise { + const visiblePromise = CoreDom.waitToBeVisible(element); + + let intersectionObserver: IntersectionObserver; + let interval: number | undefined; + + return new CoreCancellablePromise( + async (resolve) => { + await visiblePromise; + + if (CoreDom.isElementInViewport(element, intersectionRatio)) { + + return resolve(); + } + + if ('IntersectionObserver' in window) { + intersectionObserver = new IntersectionObserver((observerEntries) => { + const isIntersecting = observerEntries + .some((entry) => entry.isIntersecting && entry.intersectionRatio >= intersectionRatio); + if (!isIntersecting) { + return; + } + + resolve(); + intersectionObserver?.disconnect(); + }); + + intersectionObserver.observe(element); + } else { + interval = window.setInterval(() => { + if (!CoreDom.isElementInViewport(element, intersectionRatio)) { + return; + } + + resolve(); + window.clearInterval(interval); + }, 50); + } + }, + () => { + visiblePromise.cancel(); + intersectionObserver?.disconnect(); + window.clearInterval(interval); + }, + ); + } + + /** + * Wait an element to be in dom and visible. + * + * @param element Element to wait. + * @return Cancellable promise. + */ + static waitToBeVisible(element: HTMLElement): CoreCancellablePromise { + const domPromise = CoreDom.waitToBeInDOM(element); + + let interval: number | undefined; + + // Mutations did not observe for visibility properties. + return new CoreCancellablePromise( + async (resolve) => { + await domPromise; + + if (CoreDom.isElementVisible(element)) { + return resolve(); + } + + interval = window.setInterval(() => { + if (!CoreDom.isElementVisible(element)) { + return; + } + + resolve(); + window.clearInterval(interval); + }, 50); + }, + () => { + domPromise.cancel(); + window.clearInterval(interval); + }, + ); + } + +} + +/** + * Coordinates of an element. + */ +export type CoreCoordinates = { + x: number; // X axis coordinates. + y: number; // Y axis coordinates. +}; + +/** + * Scroll options. + */ +export type CoreScrollOptions = { + duration?: number; + addYAxis?: number; + addXAxis?: number; +}; From d5dcf98e455c18f441340f67f2760d069c3f6dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 22 Mar 2022 11:40:37 +0100 Subject: [PATCH 3/3] MOBILE-3814 course: Reorder module summary info --- .../module-summary/module-summary.html | 72 +++++++++---------- .../module-summary/module-summary.scss | 13 ++++ 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/src/core/features/course/components/module-summary/module-summary.html b/src/core/features/course/components/module-summary/module-summary.html index 11bb659c0..7af594e4b 100644 --- a/src/core/features/course/components/module-summary/module-summary.html +++ b/src/core/features/course/components/module-summary/module-summary.html @@ -54,44 +54,9 @@ - - - -

- - {{ 'addon.storagemanager.downloads' | translate }} -

-
-
- - -

{{ 'addon.storagemanager.totalspaceusage' | translate }}

- {{ sizeReadable | coreBytesToSize }} -
- - - - -
- - -

{{ 'core.lastdownloaded' | translate }} {{ downloadTimeReadable }}

-
-
- - - - - {{ 'core.download' | translate }} - - -
- - +

{{ 'core.grades.gradebook' | translate @@ -188,6 +153,41 @@ + + + +

+ + {{ 'addon.storagemanager.downloads' | translate }} +

+
+
+ + +

{{ 'addon.storagemanager.totalspaceusage' | translate }}

+ {{ sizeReadable | coreBytesToSize }} +
+ + + + +
+ + +

{{ 'core.lastdownloaded' | translate }} {{ downloadTimeReadable }}

+
+
+ + + + + {{ 'core.download' | translate }} + + +
+ diff --git a/src/core/features/course/components/module-summary/module-summary.scss b/src/core/features/course/components/module-summary/module-summary.scss index 5f86396c2..a4bb647d2 100644 --- a/src/core/features/course/components/module-summary/module-summary.scss +++ b/src/core/features/course/components/module-summary/module-summary.scss @@ -22,3 +22,16 @@ ion-item ion-label ion-icon { @include margin-horizontal(0, 4px); vertical-align: text-top; } + + +ion-item.card-header { + --padding-start: 8px; + --padding-end: 8px; + --inner-padding-start: 0px; + --inner-padding-end: 0px; + --min-height: 40px; + + ion-label { + margin: 0px; + } +}