From 30d24f99e346da618b7882cf25a6ca64874a3380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 14 Mar 2022 14:05:44 +0100 Subject: [PATCH] MOBILE-3814 chore: Deprecate getElementMeasure --- src/addons/mod/page/components/index/index.ts | 2 +- src/addons/qtype/ddmarker/classes/ddmarker.ts | 14 +- .../infinite-loading/infinite-loading.ts | 26 +--- src/core/directives/collapsible-footer.ts | 10 +- src/core/directives/format-text.ts | 59 ++++---- .../rich-text-editor/rich-text-editor.scss | 2 +- .../rich-text-editor/rich-text-editor.ts | 136 +++++++----------- src/core/services/utils/dom.ts | 10 +- 8 files changed, 110 insertions(+), 149 deletions(-) diff --git a/src/addons/mod/page/components/index/index.ts b/src/addons/mod/page/components/index/index.ts index bbcac903a..dd9dc2ff7 100644 --- a/src/addons/mod/page/components/index/index.ts +++ b/src/addons/mod/page/components/index/index.ts @@ -33,7 +33,7 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp component = AddonModPageProvider.COMPONENT; contents?: string; - displayDescription = true; + displayDescription = false; displayTimemodified = true; timemodified?: number; page?: AddonModPagePage; diff --git a/src/addons/qtype/ddmarker/classes/ddmarker.ts b/src/addons/qtype/ddmarker/classes/ddmarker.ts index a99b4387c..e1a1ddc06 100644 --- a/src/addons/qtype/ddmarker/classes/ddmarker.ts +++ b/src/addons/qtype/ddmarker/classes/ddmarker.ts @@ -272,8 +272,18 @@ export class AddonQtypeDdMarkerQuestion { return; } - const width = CoreDomUtils.getElementMeasure(markerSpan, true, true, false, true); - const height = CoreDomUtils.getElementMeasure(markerSpan, false, true, false, true); + const computedStyle = getComputedStyle(markerSpan); + const width = markerSpan.getBoundingClientRect().width + + CoreDomUtils.getComputedStyleMeasure(computedStyle, 'borderLeftWidth') + + CoreDomUtils.getComputedStyleMeasure(computedStyle, 'borderRightWidth') + + CoreDomUtils.getComputedStyleMeasure(computedStyle, 'paddingLeft') + + CoreDomUtils.getComputedStyleMeasure(computedStyle, 'paddingRight'); + + const height = markerSpan.getBoundingClientRect().height + + CoreDomUtils.getComputedStyleMeasure(computedStyle, 'borderTopWidth') + + CoreDomUtils.getComputedStyleMeasure(computedStyle, 'borderBottomWidth') + + CoreDomUtils.getComputedStyleMeasure(computedStyle, 'paddingTop') + + CoreDomUtils.getComputedStyleMeasure(computedStyle, 'paddingBottom'); markerSpan.style.opacity = '0.6'; markerSpan.style.left = (xyForText.x - (width / 2)) + 'px'; markerSpan.style.top = (xyForText.y - (height / 2)) + 'px'; diff --git a/src/core/components/infinite-loading/infinite-loading.ts b/src/core/components/infinite-loading/infinite-loading.ts index 33eb9d81b..370c2473a 100644 --- a/src/core/components/infinite-loading/infinite-loading.ts +++ b/src/core/components/infinite-loading/infinite-loading.ts @@ -14,7 +14,6 @@ import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange, ViewChild, ElementRef } from '@angular/core'; import { IonInfiniteScroll } from '@ionic/angular'; -import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; const THRESHOLD = .15; // % of the scroll element height that must be close to the edge to consider loading more items necessary. @@ -36,15 +35,12 @@ export class CoreInfiniteLoadingComponent implements OnChanges { @Input() position: 'top' | 'bottom' = 'bottom'; @Output() action: EventEmitter<() => void>; // Will emit an event when triggered. - @ViewChild('topbutton') topButton?: ElementRef; - @ViewChild('bottombutton') bottomButton?: ElementRef; - @ViewChild('spinnercontainer') spinnerContainer?: ElementRef; @ViewChild(IonInfiniteScroll) infiniteScroll?: IonInfiniteScroll; loadingMore = false; // Hide button and avoid loading more. hostElement: HTMLElement; - constructor(protected element: ElementRef) { + constructor(element: ElementRef) { this.action = new EventEmitter(); this.hostElement = element.nativeElement; } @@ -142,11 +138,7 @@ export class CoreInfiniteLoadingComponent implements OnChanges { * @deprecated since 3.9.5 */ getHeight(): number { - return (this.position == 'top' ? - this.getElementHeight(this.topButton?.nativeElement) : - this.getElementHeight(this.bottomButton?.nativeElement)) + - this.getElementHeight(this.infiniteScrollElement) + - this.getElementHeight(this.spinnerContainer?.nativeElement); + return this.hostElement.getBoundingClientRect().height; } /** @@ -158,18 +150,4 @@ export class CoreInfiniteLoadingComponent implements OnChanges { return this.hostElement.querySelector('ion-infinite-scroll'); } - /** - * Get the height of an element. - * - * @param element Element ref. - * @return Height. - */ - protected getElementHeight(element?: HTMLElement | null): number { - if (element) { - return CoreDomUtils.getElementHeight(element, true, true, true); - } - - return 0; - } - } diff --git a/src/core/directives/collapsible-footer.ts b/src/core/directives/collapsible-footer.ts index 601a12f5a..bc4505320 100644 --- a/src/core/directives/collapsible-footer.ts +++ b/src/core/directives/collapsible-footer.ts @@ -65,6 +65,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { await this.waitLoadingsDone(); await this.waitFormatTextsRendered(this.element); + this.content = this.element.closest('ion-content'); + await this.calculateHeight(); this.listenScrollEvents(); @@ -97,13 +99,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { * Setup scroll event listener. */ protected async listenScrollEvents(): Promise { - if (this.content) { - return; - } - - this.content = this.element.closest('ion-content'); - - if (!this.content) { + if (!this.content || this.content?.classList.contains('has-collapsible-footer')) { return; } diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index a879c3c83..c5c041f4b 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -217,14 +217,14 @@ export class CoreFormatTextDirective implements OnChanges { /** * Add magnifying glass icons to view adapted images at full size. */ - addMagnifyingGlasses(): void { + async addMagnifyingGlasses(): Promise { const imgs = Array.from(this.contentSpan.querySelectorAll('.core-adapted-img-container > img')); if (!imgs.length) { return; } // If cannot calculate element's width, use viewport width to avoid false adapt image icons appearing. - const elWidth = this.getElementWidth(this.element) || window.innerWidth; + const elWidth = await this.getElementWidth(); imgs.forEach((img: HTMLImageElement) => { // Skip image if it's inside a link. @@ -543,41 +543,40 @@ export class CoreFormatTextDirective implements OnChanges { /** * Returns the element width in pixels. * - * @param element Element to get width from. - * @return The width of the element in pixels. When 0 is returned it means the element is not visible. + * @return The width of the element in pixels. */ - protected getElementWidth(element: HTMLElement): number { - let width = CoreDomUtils.getElementWidth(element); + protected async getElementWidth(): Promise { + await CoreUtils.ignoreErrors(CoreDomUtils.waitToBeInDOM(this.element)); + let width = this.element.getBoundingClientRect().width; if (!width) { // All elements inside are floating or inline. Change display mode to allow calculate the width. - const parentWidth = element.parentElement ? - CoreDomUtils.getElementWidth(element.parentElement, true, false, false, true) : 0; - const previousDisplay = getComputedStyle(element, null).display; + const previousDisplay = getComputedStyle(this.element).display; - element.style.display = 'inline-block'; + this.element.style.display = 'inline-block'; + await CoreUtils.nextTick(); - width = CoreDomUtils.getElementWidth(element); + width = this.element.getBoundingClientRect().width; - // If width is incorrectly calculated use parent width instead. - if (parentWidth > 0 && (!width || width > parentWidth)) { - width = parentWidth; - } - - element.style.display = previousDisplay; + this.element.style.display = previousDisplay; } - return width; - } + // Aproximate using parent elements. + let element = this.element; + while (!width && element.parentElement) { + element = element.parentElement; + const computedStyle = getComputedStyle(element); - /** - * Returns the element height in pixels. - * - * @param elementAng Element to get height from. - * @return The height of the element in pixels. When 0 is returned it means the element is not visible. - */ - protected getElementHeight(element: HTMLElement): number { - return CoreDomUtils.getElementHeight(element) || 0; + const padding = CoreDomUtils.getComputedStyleMeasure(computedStyle, 'paddingLeft') + + CoreDomUtils.getComputedStyleMeasure(computedStyle, 'paddingRight'); + + // Use parent width as an aproximation. + width = element.getBoundingClientRect().width - padding; + } + + return width > 0 && width < window.innerWidth + ? width + : window.innerWidth; } /** @@ -701,10 +700,12 @@ export class CoreFormatTextDirective implements OnChanges { let width: string | number; let height: string | number; + await CoreDomUtils.waitToBeInDOM(iframe); + if (iframe.width) { width = iframe.width; } else { - width = this.getElementWidth(iframe); + width = iframe.getBoundingClientRect().width; if (!width) { width = window.innerWidth; } @@ -713,7 +714,7 @@ export class CoreFormatTextDirective implements OnChanges { if (iframe.height) { height = iframe.height; } else { - height = this.getElementHeight(iframe); + height = iframe.getBoundingClientRect().height; if (!height) { height = width; } diff --git a/src/core/features/editor/components/rich-text-editor/rich-text-editor.scss b/src/core/features/editor/components/rich-text-editor/rich-text-editor.scss index d869f7aae..d990a190e 100644 --- a/src/core/features/editor/components/rich-text-editor/rich-text-editor.scss +++ b/src/core/features/editor/components/rich-text-editor/rich-text-editor.scss @@ -16,7 +16,7 @@ } :host { - height: 40vh; + height: var(--core-rte-height, auto); overflow: hidden; min-height: 200px; /* Just in case vh is not supported */ min-height: 40vh; 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 2b82866d7..2a4097c55 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 @@ -38,6 +38,8 @@ import { CoreUtils } from '@services/utils/utils'; import { Platform, Translate } from '@singletons'; import { CoreEventFormActionData, CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEditorOffline } from '../../services/editor-offline'; +import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreLoadingComponent } from '@components/loading/loading'; /** * Component to display a rich text editor if enabled. @@ -101,7 +103,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn protected resizeFunction?: () => Promise; protected selectionChangeFunction?: () => void; protected languageChangedSubscription?: Subscription; - protected resizeObserver?: IntersectionObserver; + protected resizeListener?: CoreEventObserver; rteEnabled = false; isPhone = false; @@ -140,14 +142,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn this.contentChanged = new EventEmitter(); this.element = elementRef.nativeElement as HTMLDivElement; this.pageInstance = 'app_' + Date.now(); // Generate a "unique" ID based on timestamp. - - if ('IntersectionObserver' in window) { - this.resizeObserver = new IntersectionObserver((observerEntry: IntersectionObserverEntry[]) => { - if (observerEntry[0].boundingClientRect.width > 0) { - this.updateToolbarButtons(); - } - }); - } } /** @@ -180,14 +174,9 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn // Use paragraph on enter. document.execCommand('DefaultParagraphSeparator', false, 'p'); - let i = 0; - this.initHeightInterval = window.setInterval(async () => { - const height = await this.maximizeEditorSize(); - if (i >= 5 || height != 0) { - clearInterval(this.initHeightInterval); - } - i++; - }, 750); + await this.waitLoadingsDone(); + + this.maximizeEditorSize(); this.setListeners(); this.updateToolbarButtons(); @@ -256,14 +245,14 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn ); }); - this.resizeFunction = this.windowResized.bind(this); - window.addEventListener('resize', this.resizeFunction!); + this.resizeListener = CoreDomUtils.onWindowResize(() => { + this.windowResized(); + }, 50); // Start observing the target node for configured mutations this.resizeObserver?.observe(this.element); - this.selectionChangeFunction = this.updateToolbarStyles.bind(this); - document.addEventListener('selectionchange', this.selectionChangeFunction!); + document.addEventListener('selectionchange', this.selectionChangeFunction = this.updateToolbarStyles.bind(this)); this.keyboardObserver = CoreEvents.on(CoreEvents.KEYBOARD_CHANGE, () => { // Opening or closing the keyboard also calls the resize function, but sometimes the resize is called too soon. @@ -281,65 +270,57 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn /** * Resize editor to maximize the space occupied. - * - * @return Resolved with calculated editor size. */ - protected async maximizeEditorSize(): Promise { - const contentVisibleHeight = await CoreDomUtils.getContentHeight(this.content); - - if (contentVisibleHeight <= 0) { - return 0; - } - - await CoreUtils.wait(100); - + protected async maximizeEditorSize(): Promise { // Editor is ready, adjust Height if needed. - const contentHeight = await CoreDomUtils.getContentHeight(this.content); - const height = contentHeight - this.getSurroundingHeight(this.element); + const blankHeight = await this.getBlankHeightInContent(); + const newHeight = blankHeight + this.element.getBoundingClientRect().height; - if (height > this.minHeight) { - this.element.style.height = CoreDomUtils.formatPixelsSize(height - 1); + if (newHeight > this.minHeight) { + this.element.style.setProperty('--core-rte-height', (newHeight - 1) + 'px'); } else { - this.element.style.height = ''; + this.element.style.removeProperty('--core-rte-height'); } - - return height; } /** - * Get the height of the surrounding elements from the current to the top element. + * Wait until all children inside the page. * - * @param element Directive DOM element to get surroundings elements from. - * @return Surrounding height in px. + * @return Promise resolved when loadings are done. */ - protected getSurroundingHeight(element: HTMLElement): number { - let height = 0; + protected async waitLoadingsDone(): Promise { + await CoreDomUtils.waitToBeInDOM(this.element); - while (element.parentElement?.tagName != 'ION-CONTENT') { - const parent = element.parentElement!; - if (element.tagName && element.tagName != 'CORE-LOADING') { - for (let x = 0; x < parent.children.length; x++) { - const child = parent.children[x]; - if (child.tagName && child != element) { - height += CoreDomUtils.getElementHeight(child, false, true, true); - } - } - } - element = parent; + const page = this.element.closest('.ion-page'); + + await CoreComponentsRegistry.finishRenderingAllElementsInside(page, 'core-loading', 'whenLoaded'); + } + + /** + * Get the height of the space in blank at the end of the page. + * + * @return Blank height in px. Will be negative if no blank space. + */ + protected async getBlankHeightInContent(): Promise { + await CoreUtils.nextTicks(5); // Ensure content is completely loaded in the DOM. + + let content: Element | null = this.element.closest('ion-content'); + const contentHeight = await CoreDomUtils.getContentHeight(this.content); + + // Get first children with content, not fixed. + let scrollContentHeight = 0; + while (scrollContentHeight == 0 && content?.children) { + const children = Array.from(content.children) + .filter((element) => element.slot !== 'fixed' && !element.classList.contains('core-loading-container')); + + scrollContentHeight = children + .map((element) => element.getBoundingClientRect().height) + .reduce((a,b) => a + b, 0); + + content = children[0]; } - const computedStyle = getComputedStyle(element); - height += CoreDomUtils.getComputedStyleMeasure(computedStyle, 'paddingTop') + - CoreDomUtils.getComputedStyleMeasure(computedStyle, 'paddingBottom'); - - if (element.parentElement?.tagName == 'ION-CONTENT') { - const cs2 = getComputedStyle(element); - - height -= CoreDomUtils.getComputedStyleMeasure(cs2, 'paddingTop') + - CoreDomUtils.getComputedStyleMeasure(cs2, 'paddingBottom'); - } - - return height; + return contentHeight - scrollContentHeight; } /** @@ -625,7 +606,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn this.editorElement.innerHTML = '

'; this.textarea.value = ''; } else { - this.editorElement.innerHTML = value!; + this.editorElement.innerHTML = value || ''; this.textarea.value = value; this.treatExternalContent(); } @@ -759,10 +740,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn * Show the toolbar. */ showToolbar(event: Event): void { - if (!('IntersectionObserver' in window)) { - // Fallback if IntersectionObserver is not supported. - this.updateToolbarButtons(); - } + this.updateToolbarButtons(); this.element.classList.add('ion-touched'); this.element.classList.remove('ion-untouched'); @@ -851,14 +829,9 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn const length = await this.toolbarSlides.length(); - const width = CoreDomUtils.getElementWidth(this.toolbar.nativeElement); + await CoreDomUtils.waitToBeInDOM(this.toolbar.nativeElement); - if (!width) { - // Width is not available yet, try later. - setTimeout(this.updateToolbarButtons.bind(this), 100); - - return; - } + const width = this.toolbar.nativeElement.getBoundingClientRect().width; if (length > 0 && width > length * this.toolbarButtonWidth) { this.slidesOpts = { ...this.slidesOpts, slidesPerView: length }; @@ -1107,15 +1080,14 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn ngOnDestroy(): void { this.valueChangeSubscription?.unsubscribe(); this.languageChangedSubscription?.unsubscribe(); - window.removeEventListener('resize', this.resizeFunction!); - document.removeEventListener('selectionchange', this.selectionChangeFunction!); + this.selectionChangeFunction && document.removeEventListener('selectionchange', this.selectionChangeFunction); clearInterval(this.initHeightInterval); clearInterval(this.autoSaveInterval); clearTimeout(this.hideMessageTimeout); - this.resizeObserver?.disconnect(); this.resetObserver?.off(); this.keyboardObserver?.off(); this.labelObserver?.disconnect(); + this.resizeListener?.off(); } } diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index c9c2aa227..e02c40a90 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -111,8 +111,8 @@ export class CoreDomUtilsProvider { return new Promise((resolve, reject) => { // Disconnect observer for performance reasons. const timeout = window.setTimeout(() => { - reject(new Error('Waiting for DOM timeout reached')); observer.disconnect(); + reject(new Error('Waiting for DOM timeout reached')); }, 5000); const observer = new MutationObserver(() => { @@ -454,6 +454,7 @@ export class CoreDomUtilsProvider { * @param useBorder Whether to use borders to calculate the measure. * @param innerMeasure If inner measure is needed: padding, margin or borders will be substracted. * @return Height in pixels. + * @deprecated since app 4.0 Use getBoundingClientRect.height instead. */ getElementHeight( element: HTMLElement, @@ -475,6 +476,7 @@ export class CoreDomUtilsProvider { * @param useBorder Whether to use borders to calculate the measure. * @param innerMeasure If inner measure is needed: padding, margin or borders will be substracted. * @return Measure in pixels. + * @deprecated since app 4.0 Use getBoundingClientRect.height or width instead. */ getElementMeasure( element: HTMLElement, @@ -547,6 +549,7 @@ export class CoreDomUtilsProvider { * @param useBorder Whether to use borders to calculate the measure. * @param innerMeasure If inner measure is needed: padding, margin or borders will be substracted. * @return Width in pixels. + * @deprecated since app 4.0 Use getBoundingClientRect.width instead. */ getElementWidth( element: HTMLElement, @@ -726,6 +729,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. */ waitElementToExist(findFunction: () => HTMLElement | null): Promise { const promiseInterval = CoreUtils.promiseDefer(); @@ -1077,7 +1081,7 @@ export class CoreDomUtilsProvider { const scrollElement = await content.getScrollElement(); return scrollElement.clientHeight || 0; - } catch (error) { + } catch { return 0; } } @@ -1093,7 +1097,7 @@ export class CoreDomUtilsProvider { const scrollElement = await content.getScrollElement(); return scrollElement.scrollHeight || 0; - } catch (error) { + } catch { return 0; } }