Merge pull request #3195 from crazyserver/MOBILE-3814

Mobile 3814
main
Dani Palou 2022-03-22 11:56:55 +01:00 committed by GitHub
commit ef296a70d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 521 additions and 401 deletions

View File

@ -45,6 +45,7 @@ import { CoreIonLoadingElement } from '@classes/ion-loading';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { AddonMessagesConversationInfoComponent } from '../../components/conversation-info/conversation-info'; import { AddonMessagesConversationInfoComponent } from '../../components/conversation-info/conversation-info';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CoreDom } from '@singletons/dom';
/** /**
* Page that displays a message discussion page. * Page that displays a message discussion page.
@ -1109,7 +1110,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
if (this.newMessages > 0) { if (this.newMessages > 0) {
const messages = Array.from(this.hostElement.querySelectorAll<HTMLElement>('.addon-message-not-mine')); const messages = Array.from(this.hostElement.querySelectorAll<HTMLElement>('.addon-message-not-mine'));
CoreDomUtils.scrollViewToElement(messages[messages.length - this.newMessages]); CoreDom.scrollToElement(messages[messages.length - this.newMessages]);
} }
} }

View File

@ -40,6 +40,7 @@ import {
AddonModDataEntryWSField, AddonModDataEntryWSField,
} from '../../services/data'; } from '../../services/data';
import { AddonModDataHelper } from '../../services/data-helper'; import { AddonModDataHelper } from '../../services/data-helper';
import { CoreDom } from '@singletons/dom';
/** /**
* Page that displays the view edit page. * 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. * Scroll to first error or to the top if not found.
*/ */
protected async scrollToFirstError(): Promise<void> { protected async scrollToFirstError(): Promise<void> {
const scrolled = await CoreDomUtils.scrollViewToElement(this.formElement.nativeElement, '.addon-data-error'); const scrolled = await CoreDom.scrollToElement(this.formElement.nativeElement, '.addon-data-error');
if (!scrolled) { if (!scrolled) {
this.content?.scrollToTop(); this.content?.scrollToTop();
} }

View File

@ -51,6 +51,7 @@ import { CoreRatingInfo } from '@features/rating/services/rating';
import { CoreForms } from '@singletons/form'; import { CoreForms } from '@singletons/form';
import { CoreFileEntry } from '@services/file-helper'; import { CoreFileEntry } from '@services/file-helper';
import { AddonModForumSharedPostFormData } from '../../pages/discussion/discussion.page'; 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.). * 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. * @return Promise resolved when done.
*/ */
protected async scrollToForm(): Promise<void> { protected async scrollToForm(): Promise<void> {
await CoreDomUtils.scrollViewToElement( await CoreDom.scrollToElement(
this.elementRef.nativeElement, this.elementRef.nativeElement,
'#addon-forum-reply-edit-form-' + this.uniqueId, '#addon-forum-reply-edit-form-' + this.uniqueId,
); );

View File

@ -33,6 +33,7 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { Network, NgZone, Translate } from '@singletons'; import { Network, NgZone, Translate } from '@singletons';
import { CoreArray } from '@singletons/array'; import { CoreArray } from '@singletons/array';
import { CoreDom } from '@singletons/dom';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source'; import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
@ -187,7 +188,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
const scrollTo = this.postId || this.parent; const scrollTo = this.postId || this.parent;
if (scrollTo) { if (scrollTo) {
// Scroll to the post. // Scroll to the post.
CoreDomUtils.scrollViewToElement( CoreDom.scrollToElement(
this.elementRef.nativeElement, this.elementRef.nativeElement,
'#addon-mod_forum-post-' + scrollTo, '#addon-mod_forum-post-' + scrollTo,
); );

View File

@ -21,6 +21,7 @@ import { CoreForms } from '@singletons/form';
import { ModalController, Translate } from '@singletons'; import { ModalController, Translate } from '@singletons';
import { AddonModQuizAccessRuleDelegate } from '../../services/access-rules-delegate'; import { AddonModQuizAccessRuleDelegate } from '../../services/access-rules-delegate';
import { AddonModQuizAttemptWSData, AddonModQuizQuizWSData } from '../../services/quiz'; import { AddonModQuizAttemptWSData, AddonModQuizQuizWSData } from '../../services/quiz';
import { CoreDom } from '@singletons/dom';
/** /**
* Modal that renders the access rules for a quiz. * Modal that renders the access rules for a quiz.
@ -115,7 +116,7 @@ export class AddonModQuizPreflightModalComponent implements OnInit {
if (!this.preflightForm.valid) { if (!this.preflightForm.valid) {
// Form not valid. Scroll to the first element with errors. // Form not valid. Scroll to the first element with errors.
const hasScrolled = await CoreDomUtils.scrollViewToInputError( const hasScrolled = await CoreDom.scrollToInputError(
this.elementRef.nativeElement, this.elementRef.nativeElement,
); );

View File

@ -46,6 +46,7 @@ import { AddonModQuizAttempt, AddonModQuizHelper } from '../../services/quiz-hel
import { AddonModQuizSync } from '../../services/quiz-sync'; import { AddonModQuizSync } from '../../services/quiz-sync';
import { CanLeave } from '@guards/can-leave'; import { CanLeave } from '@guards/can-leave';
import { CoreForms } from '@singletons/form'; import { CoreForms } from '@singletons/form';
import { CoreDom } from '@singletons/dom';
/** /**
* Page that allows attempting a quiz. * 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. * @param slot Slot of the question to scroll to.
*/ */
protected scrollToQuestion(slot: number): void { protected scrollToQuestion(slot: number): void {
CoreDomUtils.scrollViewToElement( CoreDom.scrollToElement(
this.elementRef.nativeElement, this.elementRef.nativeElement,
'#addon-mod_quiz-question-' + slot, '#addon-mod_quiz-question-' + slot,
); );

View File

@ -22,6 +22,7 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreTimeUtils } from '@services/utils/time'; import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreDom } from '@singletons/dom';
import { import {
AddonModQuizNavigationModalComponent, AddonModQuizNavigationModalComponent,
AddonModQuizNavigationModalReturn, AddonModQuizNavigationModalReturn,
@ -247,7 +248,7 @@ export class AddonModQuizReviewPage implements OnInit {
* @param slot Slot of the question to scroll to. * @param slot Slot of the question to scroll to.
*/ */
protected scrollToQuestion(slot: number): void { protected scrollToQuestion(slot: number): void {
CoreDomUtils.scrollViewToElement( CoreDom.scrollToElement(
this.elementRef.nativeElement, this.elementRef.nativeElement,
`#addon-mod_quiz-question-${slot}`, `#addon-mod_quiz-question-${slot}`,
); );

View File

@ -14,6 +14,7 @@
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreDom } from '@singletons/dom';
import { CoreEventObserver } from '@singletons/events'; import { CoreEventObserver } from '@singletons/events';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { AddonModQuizDdImageOrTextQuestionData } from '../component/ddimageortext'; import { AddonModQuizDdImageOrTextQuestionData } from '../component/ddimageortext';
@ -83,7 +84,7 @@ export class AddonQtypeDdImageOrTextQuestion {
return bgImgXY; return bgImgXY;
} }
const position = CoreDomUtils.getRelativeElementPosition(bgImg, ddArea); const position = CoreDom.getRelativeElementPosition(bgImg, ddArea);
// Render the position related to the current image dimensions. // Render the position related to the current image dimensions.
bgImgXY[0] *= this.proportion; bgImgXY[0] *= this.proportion;
@ -419,7 +420,7 @@ export class AddonQtypeDdImageOrTextQuestion {
return; return;
} }
const position = CoreDomUtils.getRelativeElementPosition(drop, ddArea); const position = CoreDom.getRelativeElementPosition(drop, ddArea);
const choice = drag.getAttribute('choice'); const choice = drag.getAttribute('choice');
drag.style.left = position.x + 'px'; drag.style.left = position.x + 'px';
drag.style.top = position.y + 'px'; drag.style.top = position.y + 'px';
@ -473,7 +474,7 @@ export class AddonQtypeDdImageOrTextQuestion {
return; return;
} }
const position = CoreDomUtils.getRelativeElementPosition(dragItemHome, ddArea); const position = CoreDom.getRelativeElementPosition(dragItemHome, ddArea);
drag.style.left = position.x + 'px'; drag.style.left = position.x + 'px';
drag.style.top = position.y + 'px'; drag.style.top = position.y + 'px';
drag.classList.remove('placed'); drag.classList.remove('placed');

View File

@ -12,8 +12,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { CoreCoordinates, CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreCoordinates, CoreDom } from '@singletons/dom';
import { CoreEventObserver } from '@singletons/events'; import { CoreEventObserver } from '@singletons/events';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { AddonQtypeDdMarkerQuestionData } from '../component/ddmarker'; import { AddonQtypeDdMarkerQuestionData } from '../component/ddmarker';
@ -134,7 +135,7 @@ export class AddonQtypeDdMarkerQuestion {
return []; return [];
} }
const position = CoreDomUtils.getRelativeElementPosition(element, ddArea); const position = CoreDom.getRelativeElementPosition(element, ddArea);
return [position.x, position.y]; return [position.x, position.y];
} }

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDom } from '@singletons/dom';
import { AddonQtypeDdMarkerQuestion } from './ddmarker'; import { AddonQtypeDdMarkerQuestion } from './ddmarker';
/** /**
@ -59,7 +59,7 @@ export class AddonQtypeDdMarkerGraphicsApi {
return; return;
} }
const position = CoreDomUtils.getRelativeElementPosition(bgImg, ddArea); const position = CoreDom.getRelativeElementPosition(bgImg, ddArea);
dropZones.style.left = position.x + 'px'; dropZones.style.left = position.x + 'px';
dropZones.style.top = position.y + 'px'; dropZones.style.top = position.y + 'px';

View File

@ -13,10 +13,11 @@
// limitations under the License. // limitations under the License.
import { CoreFormatTextDirective } from '@directives/format-text'; 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 { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreCoordinates, CoreDom } from '@singletons/dom';
import { CoreEventObserver } from '@singletons/events'; import { CoreEventObserver } from '@singletons/events';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { AddonModQuizDdwtosQuestionData } from '../component/ddwtos'; import { AddonModQuizDdwtosQuestionData } from '../component/ddwtos';
@ -389,13 +390,13 @@ export class AddonQtypeDdwtosQuestion {
const choiceNo = this.getChoice(drag) ?? -1; const choiceNo = this.getChoice(drag) ?? -1;
const dragHome = this.container.querySelector<HTMLElement>(this.selectors.dragHome(groupNo, choiceNo)); const dragHome = this.container.querySelector<HTMLElement>(this.selectors.dragHome(groupNo, choiceNo));
if (dragHome) { if (dragHome) {
position = CoreDomUtils.getRelativeElementPosition(dragHome, parent); position = CoreDom.getRelativeElementPosition(dragHome, parent);
} }
} else { } else {
// Get the drop zone position. // Get the drop zone position.
const dropZone = this.container.querySelector<HTMLElement>(this.selectors.dropForPlace(placeNo)); const dropZone = this.container.querySelector<HTMLElement>(this.selectors.dropForPlace(placeNo));
if (dropZone) { if (dropZone) {
position = CoreDomUtils.getRelativeElementPosition(dropZone, parent); position = CoreDom.getRelativeElementPosition(dropZone, parent);
// Avoid the border. // Avoid the border.
position.x++; position.x++;
position.y++; position.y++;
@ -425,13 +426,13 @@ export class AddonQtypeDdwtosQuestion {
* @return Promise resolved when ready in the DOM. * @return Promise resolved when ready in the DOM.
*/ */
protected async waitForReady(): Promise<void> { protected async waitForReady(): Promise<void> {
await CoreDomUtils.waitToBeInDOM(this.container); await CoreDom.waitToBeInDOM(this.container);
await CoreComponentsRegistry.waitComponentsReady(this.container, 'core-format-text', CoreFormatTextDirective); await CoreComponentsRegistry.waitComponentsReady(this.container, 'core-format-text', CoreFormatTextDirective);
const drag = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.dragHomes()))[0]; const drag = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.dragHomes()))[0];
await CoreDomUtils.waitToBeInDOM(drag); await CoreDom.waitToBeInDOM(drag);
} }
/** /**
@ -480,7 +481,7 @@ export class AddonQtypeDdwtosQuestion {
return; return;
} }
await CoreDomUtils.waitToBeInDOM(groupItems[0]); await CoreDom.waitToBeInDOM(groupItems[0]);
let maxWidth = 0; let maxWidth = 0;
let maxHeight = 0; let maxHeight = 0;

View File

@ -26,6 +26,7 @@ import { CoreLogger } from '@singletons/logger';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreContextMenuComponent } from '../context-menu/context-menu'; import { CoreContextMenuComponent } from '../context-menu/context-menu';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDom } from '@singletons/dom';
const BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden'; 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. * @return Promise resolved with the header element.
*/ */
protected async searchHeader(): Promise<HTMLIonHeaderElement> { protected async searchHeader(): Promise<HTMLIonHeaderElement> {
await CoreDomUtils.waitToBeInDOM(this.element); await CoreDom.waitToBeInDOM(this.element);
let parentPage: HTMLElement | null = this.element; let parentPage: HTMLElement | null = this.element;
while (parentPage && parentPage.parentElement) { while (parentPage && parentPage.parentElement) {

View File

@ -16,6 +16,7 @@ import { Directive, Input, ElementRef, AfterViewInit } from '@angular/core';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreDom } from '@singletons/dom';
/** /**
* Directive to auto focus an element when a view is loaded. * Directive to auto focus an element when a view is loaded.
@ -46,7 +47,7 @@ export class CoreAutoFocusDirective implements AfterViewInit {
return; return;
} }
await CoreDomUtils.waitToBeInDOM(this.element); await CoreDom.waitToBeInDOM(this.element);
let focusElement = this.element; let focusElement = this.element;

View File

@ -23,6 +23,7 @@ import { CoreEventObserver } from '@singletons/events';
import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreLoadingComponent } from '@components/loading/loading';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { CoreDom } from '@singletons/dom';
/** /**
* Directive to make an element fixed at the bottom collapsible when scrolling. * 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<void> { async ngOnInit(): Promise<void> {
// Only if not present or explicitly falsy it will be false. // Only if not present or explicitly falsy it will be false.
this.appearOnBottom = !CoreUtils.isFalseOrZero(this.appearOnBottom); this.appearOnBottom = !CoreUtils.isFalseOrZero(this.appearOnBottom);
this.domPromise = CoreDomUtils.waitToBeInDOM(this.element); this.domPromise = CoreDom.waitToBeInDOM(this.element);
await this.domPromise; await this.domPromise;
await this.waitLoadingsDone(); await this.waitLoadingsDone();
@ -72,7 +73,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
await this.calculateHeight(); await this.calculateHeight();
CoreDomUtils.onElementSlot(this.element, () => { CoreDom.onElementSlot(this.element, () => {
this.calculateHeight(); this.calculateHeight();
}); });

View File

@ -19,6 +19,7 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons'; import { Translate } from '@singletons';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDom } from '@singletons/dom';
import { CoreEventObserver } from '@singletons/events'; import { CoreEventObserver } from '@singletons/events';
import { CoreFormatTextDirective } from './format-text'; import { CoreFormatTextDirective } from './format-text';
@ -100,7 +101,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
* @return Promise resolved when loadings are done. * @return Promise resolved when loadings are done.
*/ */
protected async waitLoadingsDone(): Promise<void> { protected async waitLoadingsDone(): Promise<void> {
this.domPromise = CoreDomUtils.waitToBeInDOM(this.element); this.domPromise = CoreDom.waitToBeInDOM(this.element);
await this.domPromise; await this.domPromise;

View File

@ -14,7 +14,7 @@
import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core'; import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core';
import { CoreCancellablePromise } from '@classes/cancellable-promise'; 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. * 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 * @inheritdoc
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.domPromise = CoreDomUtils.waitToBeInDOM(this.element); this.domPromise = CoreDom.waitToBeInDOM(this.element);
await this.domPromise; await this.domPromise;
this.content = this.element.closest('ion-content'); this.content = this.element.closest('ion-content');
@ -56,7 +56,7 @@ export class CoreFabDirective implements OnInit, OnDestroy {
await this.calculatePlace(); await this.calculatePlace();
CoreDomUtils.onElementSlot(this.element, () => { CoreDom.onElementSlot(this.element, () => {
this.calculatePlace(); this.calculatePlace();
}); });
} }

View File

@ -45,6 +45,7 @@ import { CoreCollapsibleItemDirective } from './collapsible-item';
import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { AsyncComponent } from '@classes/async-component'; import { AsyncComponent } from '@classes/async-component';
import { CoreText } from '@singletons/text'; 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 * 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<number> { protected async getElementWidth(): Promise<number> {
if (!this.domElementPromise) { if (!this.domElementPromise) {
this.domElementPromise = CoreDomUtils.waitToBeInDOM(this.element); this.domElementPromise = CoreDom.waitToBeInDOM(this.element);
} }
await this.domElementPromise; await this.domElementPromise;
@ -704,7 +705,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
newUrl += `&h=${privacyHash}`; newUrl += `&h=${privacyHash}`;
} }
const domPromise = CoreDomUtils.waitToBeInDOM(iframe); const domPromise = CoreDom.waitToBeInDOM(iframe);
this.domPromises.push(domPromise); this.domPromises.push(domPromise);
await domPromise; await domPromise;

View File

@ -28,6 +28,7 @@ import { CoreCustomURLSchemes } from '@services/urlschemes';
import { DomSanitizer } from '@singletons'; import { DomSanitizer } from '@singletons';
import { CoreFilepool } from '@services/filepool'; import { CoreFilepool } from '@services/filepool';
import { CoreUrl } from '@singletons/url'; import { CoreUrl } from '@singletons/url';
import { CoreDom } from '@singletons/dom';
/** /**
* Directive to open a link in external browser or in the app. * 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); href = href.substring(1);
const container = this.element.closest('ion-content'); const container = this.element.closest('ion-content');
if (container) { if (container) {
CoreDomUtils.scrollViewToElement( CoreDom.scrollToElement(
container, container,
`#${href}, [name='${href}']`, `#${href}, [name='${href}']`,
); );

View File

@ -14,7 +14,7 @@
import { Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; import { Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
import { CoreCancellablePromise } from '@classes/cancellable-promise'; 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. * Directive to listen when an element becomes visible.
@ -37,7 +37,7 @@ export class CoreOnAppearDirective implements OnInit, OnDestroy {
* @inheritdoc * @inheritdoc
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.visiblePromise = CoreDomUtils.waitToBeInViewport(this.element); this.visiblePromise = CoreDom.waitToBeInViewport(this.element);
await this.visiblePromise; await this.visiblePromise;

View File

@ -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 some addon modules that define components, directives and pipes. Only import the important ones.
import { AddonModAssignComponentsModule } from '@addons/mod/assign/components/components.module'; import { AddonModAssignComponentsModule } from '@addons/mod/assign/components/components.module';
import { AddonModWorkshopComponentsModule } from '@addons/mod/workshop/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. * Service to provide functionalities regarding compiling dynamic HTML and Javascript.
@ -341,6 +342,7 @@ export class CoreCompileProvider {
instance['Md5'] = Md5; instance['Md5'] = Md5;
instance['CoreSyncBaseProvider'] = CoreSyncBaseProvider; instance['CoreSyncBaseProvider'] = CoreSyncBaseProvider;
instance['CoreArray'] = CoreArray; instance['CoreArray'] = CoreArray;
instance['CoreDom'] = CoreDom;
instance['CoreText'] = CoreText; instance['CoreText'] = CoreText;
instance['CoreUrl'] = CoreUrl; instance['CoreUrl'] = CoreUrl;
instance['CoreWindow'] = CoreWindow; instance['CoreWindow'] = CoreWindow;

View File

@ -48,6 +48,7 @@ import { CoreCourseModuleDelegate } from '@features/course/services/module-deleg
import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course'; import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course';
import { CoreUserTours, CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours'; import { CoreUserTours, CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours';
import { CoreCourseCourseIndexTourComponent } from '../course-index-tour/course-index-tour'; 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. * Component to display course contents using a certain format. If the format isn't found, use default one.
@ -511,9 +512,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* @param moduleId Module ID. * @param moduleId Module ID.
*/ */
protected scrollToModule(moduleId: number): void { protected scrollToModule(moduleId: number): void {
CoreDomUtils.scrollViewToElement( CoreDom.scrollToElement(
this.elementRef.nativeElement, this.elementRef.nativeElement,
'#core-course-module-' + moduleId, '#core-course-module-' + moduleId,
{ addYAxis: -10 },
); );
} }

View File

@ -21,8 +21,8 @@ import {
import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper'; import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper';
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import { CoreDomUtils } from '@services/utils/dom';
import { ModalController } from '@singletons'; import { ModalController } from '@singletons';
import { CoreDom } from '@singletons/dom';
/** /**
* Component to display course index modal. * Component to display course index modal.
@ -109,9 +109,10 @@ export class CoreCourseCourseIndexComponent implements OnInit {
this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course); this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
CoreDomUtils.scrollViewToElement( CoreDom.scrollToElement(
this.elementRef.nativeElement, this.elementRef.nativeElement,
'.item.item-current', '.item.item-current',
{ addYAxis: -10 },
); );
} }

View File

@ -54,44 +54,9 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-card *ngIf="(canPrefetch && displayOptions.displayPrefetch) || (sizeReadable && displayOptions.displaySize)">
<ion-item lines="full" class="ion-text-wrap">
<ion-label>
<h2>
<ion-icon name="fas-cloud-download-alt" aria-hidden="true"></ion-icon>
{{ 'addon.storagemanager.downloads' | translate }}
</h2>
</ion-label>
</ion-item>
<ion-item *ngIf="sizeReadable && displayOptions.displaySize" class="ion-text-wrap">
<ion-label>
<p class="item-heading ion-text-wrap">{{ 'addon.storagemanager.totalspaceusage' | translate }}</p>
<ion-badge color="light">{{ sizeReadable | coreBytesToSize }}</ion-badge>
</ion-label>
<ion-button *ngIf="!removeFilesLoading" [disabled]="prefetchLoading" (click)="removeFiles()" color="danger" fill="clear"
[attr.aria-label]="'core.clearstoreddata' | translate:{$a: sizeReadable}" slot="end">
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<ion-spinner *ngIf="removeFilesLoading" slot="end" aria-hidden="true"></ion-spinner>
</ion-item>
<ion-item *ngIf="downloadTimeReadable" class="ion-text-wrap">
<ion-label>
<p class="ion-text-wrap">{{ 'core.lastdownloaded' | translate }} {{ downloadTimeReadable }}</p>
</ion-label>
</ion-item>
<ion-button fill="outline" expand="block" *ngIf="canPrefetch && displayOptions.displayPrefetch" class="ion-text-wrap"
(click)="prefetch()" [disabled]="prefetchDisabled">
<ion-icon *ngIf="!prefetchLoading" name="fas-cloud-download-alt" slot="start" aria-hidden="true"></ion-icon>
<ion-spinner *ngIf="prefetchLoading" slot="start" aria-hidden="true"></ion-spinner>
<ion-label>
{{ 'core.download' | translate }}
</ion-label>
</ion-button>
</ion-card>
<ion-card *ngIf="displayOptions.displayGrades && grades?.length > 0"> <ion-card *ngIf="displayOptions.displayGrades && grades?.length > 0">
<ion-list> <ion-list>
<ion-item lines="full" class="ion-text-wrap"> <ion-item lines="full" class="ion-text-wrap card-header">
<ion-label> <ion-label>
<h2> <h2>
<ion-icon name="fas-chart-bar" slot="end" aria-hidden="true"></ion-icon>{{ 'core.grades.gradebook' | translate <ion-icon name="fas-chart-bar" slot="end" aria-hidden="true"></ion-icon>{{ 'core.grades.gradebook' | translate
@ -188,6 +153,41 @@
</ion-list> </ion-list>
</ion-card> </ion-card>
<ion-card *ngIf="(canPrefetch && displayOptions.displayPrefetch) || (sizeReadable && displayOptions.displaySize)">
<ion-item lines="full" class="ion-text-wrap card-header">
<ion-label>
<h2>
<ion-icon name="fas-cloud-download-alt" aria-hidden="true"></ion-icon>
{{ 'addon.storagemanager.downloads' | translate }}
</h2>
</ion-label>
</ion-item>
<ion-item *ngIf="sizeReadable && displayOptions.displaySize" class="ion-text-wrap">
<ion-label>
<p class="item-heading ion-text-wrap">{{ 'addon.storagemanager.totalspaceusage' | translate }}</p>
<ion-badge color="light">{{ sizeReadable | coreBytesToSize }}</ion-badge>
</ion-label>
<ion-button *ngIf="!removeFilesLoading" [disabled]="prefetchLoading" (click)="removeFiles()" color="danger" fill="clear"
[attr.aria-label]="'core.clearstoreddata' | translate:{$a: sizeReadable}" slot="end">
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<ion-spinner *ngIf="removeFilesLoading" slot="end" aria-hidden="true"></ion-spinner>
</ion-item>
<ion-item *ngIf="downloadTimeReadable" class="ion-text-wrap">
<ion-label>
<p class="ion-text-wrap">{{ 'core.lastdownloaded' | translate }} {{ downloadTimeReadable }}</p>
</ion-label>
</ion-item>
<ion-button fill="outline" expand="block" *ngIf="canPrefetch && displayOptions.displayPrefetch" class="ion-text-wrap"
(click)="prefetch()" [disabled]="prefetchDisabled">
<ion-icon *ngIf="!prefetchLoading" name="fas-cloud-download-alt" slot="start" aria-hidden="true"></ion-icon>
<ion-spinner *ngIf="prefetchLoading" slot="start" aria-hidden="true"></ion-spinner>
<ion-label>
{{ 'core.download' | translate }}
</ion-label>
</ion-button>
</ion-card>
<ion-item button *ngIf="blog && displayOptions.displayBlog" (click)="gotoBlog()" [detail]="true"> <ion-item button *ngIf="blog && displayOptions.displayBlog" (click)="gotoBlog()" [detail]="true">
<ion-icon name="far-newspaper" slot="start" aria-hidden="true"></ion-icon> <ion-icon name="far-newspaper" slot="start" aria-hidden="true"></ion-icon>
<ion-label> <ion-label>

View File

@ -22,3 +22,16 @@ ion-item ion-label ion-icon {
@include margin-horizontal(0, 4px); @include margin-horizontal(0, 4px);
vertical-align: text-top; 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;
}
}

View File

@ -41,6 +41,7 @@ import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreLoadingComponent } from '@components/loading/loading';
import { CoreScreen } from '@services/screen'; import { CoreScreen } from '@services/screen';
import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { CoreDom } from '@singletons/dom';
/** /**
* Component to display a rich text editor if enabled. * 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. * @return Promise resolved when loadings are done.
*/ */
protected async waitLoadingsDone(): Promise<void> { protected async waitLoadingsDone(): Promise<void> {
this.domPromise = CoreDomUtils.waitToBeInDOM(this.element); this.domPromise = CoreDom.waitToBeInDOM(this.element);
await this.domPromise; await this.domPromise;
@ -846,7 +847,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
// Cancel previous one, if any. // Cancel previous one, if any.
this.buttonsDomPromise?.cancel(); this.buttonsDomPromise?.cancel();
this.buttonsDomPromise = CoreDomUtils.waitToBeInDOM(this.toolbar.nativeElement); this.buttonsDomPromise = CoreDom.waitToBeInDOM(this.toolbar.nativeElement);
await this.buttonsDomPromise; await this.buttonsDomPromise;
const width = this.toolbar.nativeElement.getBoundingClientRect().width; const width = this.toolbar.nativeElement.getBoundingClientRect().width;

View File

@ -31,6 +31,7 @@ import { Translate } from '@singletons';
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreGradesCoursesSource } from '@features/grades/classes/grades-courses-source'; import { CoreGradesCoursesSource } from '@features/grades/classes/grades-courses-source';
import { CoreDom } from '@singletons/dom';
/** /**
* Page that displays a course grades. * Page that displays a course grades.
@ -170,7 +171,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
if (row) { if (row) {
this.toggleRow(row, true); this.toggleRow(row, true);
CoreDomUtils.scrollViewToElement( CoreDom.scrollToElement(
this.element.nativeElement, this.element.nativeElement,
'#grade-' + row.id, '#grade-' + row.id,
); );

View File

@ -35,6 +35,7 @@ import { CoreNavigator } from '@services/navigator';
import { CoreForms } from '@singletons/form'; import { CoreForms } from '@singletons/form';
import { CoreRecaptchaComponent } from '@components/recaptcha/recaptcha'; import { CoreRecaptchaComponent } from '@components/recaptcha/recaptcha';
import { CoreText } from '@singletons/text'; import { CoreText } from '@singletons/text';
import { CoreDom } from '@singletons/dom';
/** /**
* Page to signup using email. * Page to signup using email.
@ -284,7 +285,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
this.changeDetector.detectChanges(); this.changeDetector.detectChanges();
// Scroll to the first element with errors. // Scroll to the first element with errors.
const errorFound = await CoreDomUtils.scrollViewToInputError( const errorFound = await CoreDom.scrollToInputError(
this.elementRef.nativeElement, this.elementRef.nativeElement,
); );

View File

@ -21,6 +21,7 @@ import { CoreUserTours, CoreUserToursAlignment, CoreUserToursSide } from '@featu
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { AngularFrameworkDelegate } from '@singletons'; import { AngularFrameworkDelegate } from '@singletons';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDom } from '@singletons/dom';
const ANIMATION_DURATION = 200; const ANIMATION_DURATION = 200;
const USER_TOURS_BACK_BUTTON_PRIORITY = 100; const USER_TOURS_BACK_BUTTON_PRIORITY = 100;
@ -86,7 +87,7 @@ export class CoreUserToursUserTourComponent implements AfterViewInit {
await CoreDomUtils.waitForImages(tour); await CoreDomUtils.waitForImages(tour);
// Calculate focus styles or dismiss if the element is gone. // 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); await this.dismiss(false);
return; return;

View File

@ -54,7 +54,7 @@ import { filter } from 'rxjs/operators';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreEventObserver } from '@singletons/events'; 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. * "Utils" service with helper functions for UI, DOM elements and HTML code.
@ -93,180 +93,6 @@ export class CoreDomUtilsProvider {
this.debugDisplay = debugDisplay != 0; this.debugDisplay = debugDisplay != 0;
} }
/**
* Wait an element to be in dom of another element.
*
* @param element Element to wait.
* @return Cancellable promise.
*/
waitToBeInDOM(element: HTMLElement): CoreCancellablePromise<void> {
const root = element.getRootNode({ composed: true });
if (root === document) {
// Already in DOM.
return CoreCancellablePromise.resolve();
}
let observer: MutationObserver;
return new CoreCancellablePromise<void>(
(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 and visible.
*
* @param element Element to wait.
* @return Cancellable promise.
*/
waitToBeVisible(element: HTMLElement): CoreCancellablePromise<void> {
const domPromise = CoreDomUtils.waitToBeInDOM(element);
let interval: number | undefined;
// Mutations did not observe for visibility properties.
return new CoreCancellablePromise<void>(
async (resolve) => {
await domPromise;
if (CoreDomUtils.isElementVisible(element)) {
return resolve();
}
interval = window.setInterval(() => {
if (!CoreDomUtils.isElementVisible(element)) {
return;
}
resolve();
window.clearInterval(interval);
}, 50);
},
() => {
domPromise.cancel();
window.clearInterval(interval);
},
);
}
/**
* 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<void> {
const visiblePromise = CoreDomUtils.waitToBeVisible(element);
let intersectionObserver: IntersectionObserver;
let interval: number | undefined;
return new CoreCancellablePromise<void>(
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. * Window resize is widely checked and may have many performance issues, debouce usage is needed to avoid calling it too much.
* This function helps setting up the debounce feature and remove listener easily. * This function helps setting up the debounce feature and remove listener easily.
@ -571,11 +397,9 @@ export class CoreDomUtilsProvider {
* @return Selection contents. Undefined if not found. * @return Selection contents. Undefined if not found.
*/ */
getContentsOfElement(element: HTMLElement, selector: string): string | undefined { getContentsOfElement(element: HTMLElement, selector: string): string | undefined {
if (element) { const selected = element.querySelector(selector);
const selected = element.querySelector(selector); if (selected) {
if (selected) { return selected.innerHTML;
return selected.innerHTML;
}
} }
} }
@ -724,7 +548,7 @@ export class CoreDomUtilsProvider {
* @param selector Selector to find the element to gets the position. * @param selector Selector to find the element to gets the position.
* @param positionParentClass Parent Class where to stop calculating the position. Default inner-scroll. * @param positionParentClass Parent Class where to stop calculating the position. Default inner-scroll.
* @return positionLeft, positionTop of the element relative to. * @return positionLeft, positionTop of the element relative to.
* @deprecated since app 4.0. Use getRelativeElementPosition instead. * @deprecated since app 4.0. Use CoreDom.getRelativeElementPosition instead.
*/ */
getElementXY(element: HTMLElement, selector?: string, positionParentClass = 'inner-scroll'): [number, number] | null { getElementXY(element: HTMLElement, selector?: string, positionParentClass = 'inner-scroll'): [number, number] | null {
if (selector) { if (selector) {
@ -742,7 +566,7 @@ export class CoreDomUtilsProvider {
return null; return null;
} }
const position = CoreDomUtils.getRelativeElementPosition(element, parent); const position = CoreDom.getRelativeElementPosition(element, parent);
// Calculate the top and left positions. // Calculate the top and left positions.
return [ return [
@ -751,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. * Given a message, it deduce if it's a network error.
* *
@ -889,7 +694,7 @@ export class CoreDomUtilsProvider {
* *
* @param findFunction The function used to find the element. * @param findFunction The function used to find the element.
* @return Resolved if found, rejected if too many tries. * @return Resolved if found, rejected if too many tries.
* @deprecated since app 4.0 Use waitToBeInDOM instead. * @deprecated since app 4.0 Use CoreDom.waitToBeInsideElement instead.
*/ */
waitElementToExist(findFunction: () => HTMLElement | null): Promise<HTMLElement> { waitElementToExist(findFunction: () => HTMLElement | null): Promise<HTMLElement> {
const promiseInterval = CoreUtils.promiseDefer<HTMLElement>(); const promiseInterval = CoreUtils.promiseDefer<HTMLElement>();
@ -993,63 +798,6 @@ export class CoreDomUtilsProvider {
return elementPoint > window.innerHeight || elementPoint < scrollTopPos; 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. * Check if rich text editor is enabled.
* *
@ -1251,7 +999,7 @@ export class CoreDomUtilsProvider {
* @return Returns a promise which is resolved when the scroll has completed. * @return Returns a promise which is resolved when the scroll has completed.
* @deprecated since 3.9.5. Use directly the IonContent class. * @deprecated since 3.9.5. Use directly the IonContent class.
*/ */
scrollToBottom(content: IonContent, duration?: number): Promise<void> { scrollToBottom(content: IonContent, duration = 0): Promise<void> {
return content.scrollToBottom(duration); return content.scrollToBottom(duration);
} }
@ -1315,54 +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 duration Duration of the scroll animation in milliseconds.
* @return Wether the scroll suceeded.
*/
async scrollViewToElement(element: HTMLElement, selector?: string, duration = 0): Promise<boolean> {
await CoreDomUtils.waitToBeInDOM(element);
if (selector) {
const foundElement = element.querySelector<HTMLElement>(selector);
if (!foundElement) {
// Element not found.
return false;
}
element = foundElement;
}
const content = element.closest<HTMLIonContentElement>('ion-content') ?? undefined;
if (!content) {
// Content to scroll, not found.
return false;
}
try {
const position = CoreDomUtils.getRelativeElementPosition(element, content);
await content.scrollToPoint(position.x, position.y, 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<boolean> {
return this.scrollViewToElement(container, '.core-input-error');
}
/** /**
* Scroll to a certain element. * Scroll to a certain element.
* *
@ -1371,10 +1071,10 @@ export class CoreDomUtilsProvider {
* @param scrollParentClass Not used anymore. * @param scrollParentClass Not used anymore.
* @param duration Duration of the scroll animation in milliseconds. * @param duration Duration of the scroll animation in milliseconds.
* @return True if the element is found, false otherwise. * @return 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 = 0): boolean { scrollToElement(content: IonContent, element: HTMLElement, scrollParentClass?: string, duration?: number): boolean {
CoreDomUtils.scrollViewToElement(element, undefined, duration); CoreDom.scrollToElement(element, undefined, { duration });
return true; return true;
} }
@ -1388,20 +1088,20 @@ export class CoreDomUtilsProvider {
* @param scrollParentClass Not used anymore. * @param scrollParentClass Not used anymore.
* @param duration Duration of the scroll animation in milliseconds. * @param duration Duration of the scroll animation in milliseconds.
* @return True if the element is found, false otherwise. * @return 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( scrollToElementBySelector(
container: HTMLElement | null, container: HTMLElement | null,
content: unknown | null, content: unknown | null,
selector: string, selector: string,
scrollParentClass?: string, scrollParentClass?: string,
duration = 0, duration?: number,
): boolean { ): boolean {
if (!container || !content) { if (!container || !content) {
return false; return false;
} }
CoreDomUtils.scrollViewToElement(container, selector, duration); CoreDom.scrollToElement(container, selector, { duration });
return true; return true;
@ -1412,14 +1112,14 @@ export class CoreDomUtilsProvider {
* *
* @param container The element that contains the element that must be scrolled. * @param container The element that contains the element that must be scrolled.
* @return True if the element is found, false otherwise. * @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 { scrollToInputError(container: HTMLElement | null): boolean {
if (!container) { if (!container) {
return false; return false;
} }
this.scrollViewToInputError(container); CoreDom.scrollToInputError(container);
return true; return true;
} }
@ -2427,11 +2127,3 @@ export enum VerticalPoint {
MID = 'mid', MID = 'mid',
BOTTOM = 'bottom', BOTTOM = 'bottom',
} }
/**
* Coordinates of an element.
*/
export type CoreCoordinates = {
x: number; // X axis coordinates.
y: number; // Y axis coordinates.
};

View File

@ -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<boolean> {
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<HTMLIonContentElement>('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<boolean> {
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<void> {
const root = element.getRootNode({ composed: true });
if (root === document) {
// Already in DOM.
return CoreCancellablePromise.resolve();
}
let observer: MutationObserver;
return new CoreCancellablePromise<void>(
(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<CoreCancellablePromise<HTMLElement>> {
await CoreDom.waitToBeInDOM(container);
let element = container.querySelector<HTMLElement>(selector);
if (element) {
// Already in DOM.
return CoreCancellablePromise.resolve(element);
}
let observer: MutationObserver;
return new CoreCancellablePromise<HTMLElement>(
(resolve) => {
observer = new MutationObserver(() => {
element = container.querySelector<HTMLElement>(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<void> {
const visiblePromise = CoreDom.waitToBeVisible(element);
let intersectionObserver: IntersectionObserver;
let interval: number | undefined;
return new CoreCancellablePromise<void>(
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<void> {
const domPromise = CoreDom.waitToBeInDOM(element);
let interval: number | undefined;
// Mutations did not observe for visibility properties.
return new CoreCancellablePromise<void>(
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;
};