From 389a1c896411fd5a4900e079ffce3ebdb1701e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 1 Mar 2022 14:23:29 +0100 Subject: [PATCH] MOBILE-3814 format-text: Fix expandable items height --- src/core/directives/collapsible-footer.ts | 81 ++++++++++++++++------- src/core/directives/collapsible-item.ts | 62 +++++++++++------ src/core/directives/format-text.ts | 35 ++++++---- src/theme/globals.mixins.scss | 14 ++-- 4 files changed, 126 insertions(+), 66 deletions(-) diff --git a/src/core/directives/collapsible-footer.ts b/src/core/directives/collapsible-footer.ts index 71fd0b7fd..4f87f379f 100644 --- a/src/core/directives/collapsible-footer.ts +++ b/src/core/directives/collapsible-footer.ts @@ -17,6 +17,10 @@ import { ScrollDetail } from '@ionic/core'; import { IonContent } from '@ionic/angular'; import { CoreUtils } from '@services/utils/utils'; import { CoreMath } from '@singletons/math'; +import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreFormatTextDirective } from './format-text'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents } from '@singletons/events'; /** * Directive to make an element fixed at the bottom collapsible when scrolling. @@ -32,11 +36,12 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { protected element: HTMLElement; protected initialHeight = 0; - protected initialPaddingBottom = 0; + protected initialPaddingBottom = '0px'; protected previousTop = 0; protected previousHeight = 0; protected stickTimeout?: number; protected content?: HTMLIonContentElement | null; + protected loadingChangedListener?: CoreEventObserver; constructor(el: ElementRef, protected ionContent: IonContent) { this.element = el.nativeElement; @@ -44,30 +49,28 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { } /** - * Setup scroll event listener. - * - * @param retries Number of retries left. + * Calculate the height of the footer. */ - protected async listenScrollEvents(retries = 5): Promise { - // Already initialized. - if (this.initialHeight > 0) { - return; - } + protected async calculateHeight(): Promise { + await this.waitFormatTextsRendered(this.element); - this.initialHeight = this.element.getBoundingClientRect().height; - - if (this.initialHeight == 0 && retries > 0) { - await CoreUtils.nextTicks(50); - - this.listenScrollEvents(retries - 1); - - return; - } + await CoreUtils.nextTick(); // Set a minimum height value. - this.initialHeight = this.initialHeight || 48; + this.initialHeight = this.element.getBoundingClientRect().height || 48; this.previousHeight = this.initialHeight; + this.setBarHeight(this.initialHeight); + } + + /** + * Setup scroll event listener. + */ + protected async listenScrollEvents(): Promise { + if (this.content) { + return; + } + this.content = this.element.closest('ion-content'); if (!this.content) { @@ -82,12 +85,15 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { } // Set a padding to not overlap elements. - this.initialPaddingBottom = parseFloat(this.content.style.getPropertyValue('--padding-bottom') || '0'); - this.content.style.setProperty('--padding-bottom', this.initialPaddingBottom + this.initialHeight + 'px'); + this.initialPaddingBottom = this.content.style.getPropertyValue('--padding-bottom') || this.initialPaddingBottom; + this.content.style.setProperty( + '--padding-bottom', + `calc(${this.initialPaddingBottom} + var(--core-collapsible-footer-height, 0px))`, + ); + const scroll = await this.content.getScrollElement(); this.content.scrollEvents = true; - this.setBarHeight(this.initialHeight); this.content.addEventListener('ionScroll', (e: CustomEvent): void => { if (!this.content) { return; @@ -98,6 +104,19 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { } + /** + * Wait until all children inside the element are done rendering. + * + * @param element Element. + */ + protected async waitFormatTextsRendered(element: Element): Promise { + const formatTexts = Array + .from(element.querySelectorAll('core-format-text')) + .map(element => CoreComponentsRegistry.resolve(element, CoreFormatTextDirective)); + + await Promise.all(formatTexts.map(formatText => formatText?.rendered())); + } + /** * On scroll function. * @@ -144,15 +163,29 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { /** * @inheritdoc */ - ngOnInit(): void { + async ngOnInit(): Promise { + // Calculate the height now. + await this.calculateHeight(); + setTimeout(() => this.calculateHeight(), 200); // Try again, sometimes the first calculation is wrong. + this.listenScrollEvents(); + + // Recalculate the height if a parent core-loading displays the content. + this.loadingChangedListener = + CoreEvents.on(CoreEvents.CORE_LOADING_CHANGED, async (data: CoreEventLoadingChangedData) => { + if (data.loaded && CoreDomUtils.closest(this.element.parentElement, '#' + data.uniqueId)) { + // The format-text is inside the loading, re-calculate the height. + await this.calculateHeight(); + setTimeout(() => this.calculateHeight(), 200); + } + }); } /** * @inheritdoc */ async ngOnDestroy(): Promise { - this.content?.style.setProperty('--padding-bottom', this.initialPaddingBottom + 'px'); + this.content?.style.setProperty('--padding-bottom', this.initialPaddingBottom); } } diff --git a/src/core/directives/collapsible-item.ts b/src/core/directives/collapsible-item.ts index 5556d8669..3188421e9 100644 --- a/src/core/directives/collapsible-item.ts +++ b/src/core/directives/collapsible-item.ts @@ -14,8 +14,11 @@ import { Directive, ElementRef, Input, OnInit } from '@angular/core'; import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; +import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreFormatTextDirective } from './format-text'; const defaultMaxHeight = 56; const buttonHeight = 44; @@ -54,7 +57,7 @@ export class CoreCollapsibleItemDirective implements OnInit { /** * @inheritdoc */ - ngOnInit(): void { + async ngOnInit(): Promise { if (typeof this.height === 'string') { this.maxHeight = this.height === '' ? defaultMaxHeight @@ -70,31 +73,44 @@ export class CoreCollapsibleItemDirective implements OnInit { } // Calculate the height now. - this.calculateHeight(); + await this.calculateHeight(); setTimeout(() => this.calculateHeight(), 200); // Try again, sometimes the first calculation is wrong. - this.setExpandButtonEnabled(false); - // Recalculate the height if a parent core-loading displays the content. this.loadingChangedListener = - CoreEvents.on(CoreEvents.CORE_LOADING_CHANGED, (data: CoreEventLoadingChangedData) => { + CoreEvents.on(CoreEvents.CORE_LOADING_CHANGED, async (data: CoreEventLoadingChangedData) => { if (data.loaded && CoreDomUtils.closest(this.element.parentElement, '#' + data.uniqueId)) { - // The format-text is inside the loading, re-calculate the height. - this.calculateHeight(); + // The element is inside the loading, re-calculate the height. + await this.calculateHeight(); setTimeout(() => this.calculateHeight(), 200); } }); } + /** + * Wait until all children inside the element are done rendering. + * + * @param element Element. + */ + protected async waitFormatTextsRendered(element: Element): Promise { + const formatTexts = Array + .from(element.querySelectorAll('core-format-text')) + .map(element => CoreComponentsRegistry.resolve(element, CoreFormatTextDirective)); + + await Promise.all(formatTexts.map(formatText => formatText?.rendered())); + } + /** * Calculate the height and check if we need to display show more or not. */ - protected calculateHeight(): void { - // @todo: Work on calculate this height better. + protected async calculateHeight(): Promise { + await this.waitFormatTextsRendered(this.element); // Remove max-height (if any) to calculate the real height. const initialMaxHeight = this.element.style.maxHeight; - this.element.style.maxHeight = ''; + this.element.style.maxHeight = 'none'; + + await CoreUtils.nextTick(); const height = CoreDomUtils.getElementHeight(this.element) || 0; @@ -102,7 +118,7 @@ export class CoreCollapsibleItemDirective implements OnInit { this.element.style.maxHeight = initialMaxHeight; // If cannot calculate height, shorten always. - this.setExpandButtonEnabled(!height || height > this.maxHeight); + this.setExpandButtonEnabled(!height || height >= this.maxHeight); } /** @@ -115,9 +131,7 @@ export class CoreCollapsibleItemDirective implements OnInit { this.element.classList.toggle('collapsible-enabled', enable); if (!enable || this.element.querySelector('ion-button.collapsible-toggle')) { - this.element.style.maxHeight = !enable || this.expanded - ? '' - : this.maxHeight + 'px'; + this.setMaxHeight(!enable || this.expanded? undefined : this.maxHeight); return; } @@ -141,6 +155,19 @@ export class CoreCollapsibleItemDirective implements OnInit { this.toggleExpand(this.expanded); } + /** + * Set max height to element. + * + * @param maxHeight Max height if collapsed or undefined if expanded. + */ + protected setMaxHeight(maxHeight?: number): void { + if (maxHeight) { + this.element.style.setProperty('--max-height', maxHeight + buttonHeight + 'px'); + } else { + this.element.style.removeProperty('--max-height'); + } + } + /** * Expand or collapse text. * @@ -151,13 +178,8 @@ export class CoreCollapsibleItemDirective implements OnInit { expand = !this.expanded; } this.expanded = expand; - this.element.classList.toggle('collapsible-expanded', expand); this.element.classList.toggle('collapsible-collapsed', !expand); - if (expand) { - this.element.style.setProperty('--max-height', this.maxHeight + buttonHeight + 'px'); - } else { - this.element.style.removeProperty('--max-height'); - } + this.setMaxHeight(!expand? this.maxHeight: undefined); const toggleButton = this.element.querySelector('ion-button.collapsible-toggle'); const toggleText = toggleButton?.querySelector('.collapsible-toggle-text'); diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 5171ae54b..8d93e24d6 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -272,15 +272,19 @@ export class CoreFormatTextDirective implements OnChanges { /** * Calculate the height and check if we need to display show more or not. */ - protected calculateHeight(): void { + protected async calculateHeight(): Promise { // @todo: Work on calculate this height better. if (!this.maxHeight) { return; } + await this.rendered(); + // Remove max-height (if any) to calculate the real height. const initialMaxHeight = this.element.style.maxHeight; - this.element.style.maxHeight = ''; + this.element.style.maxHeight = 'none'; + + await CoreUtils.nextTick(); const height = this.getElementHeight(this.element); @@ -288,7 +292,20 @@ export class CoreFormatTextDirective implements OnChanges { this.element.style.maxHeight = initialMaxHeight; // If cannot calculate height, shorten always. - this.setExpandButtonEnabled(!height || height > this.maxHeight); + this.setExpandButtonEnabled(!height || height >= this.maxHeight); + } + + /** + * Set max height to element. + * + * @param maxHeight Max height if collapsed or undefined if expanded. + */ + protected setMaxHeight(maxHeight?: number): void { + if (maxHeight) { + this.element.style.setProperty('--max-height', maxHeight + 'px'); + } else { + this.element.style.removeProperty('--max-height'); + } } /** @@ -301,9 +318,7 @@ export class CoreFormatTextDirective implements OnChanges { this.element.classList.toggle('collapsible-enabled', enable); if (!enable || this.element.querySelector('ion-button.collapsible-toggle')) { - this.element.style.maxHeight = !enable || this.expanded - ? '' - : this.maxHeight + 'px'; + this.setMaxHeight(!enable || this.expanded? undefined : this.maxHeight); return; } @@ -337,13 +352,9 @@ export class CoreFormatTextDirective implements OnChanges { expand = !this.expanded; } this.expanded = expand; - this.element.classList.toggle('collapsible-expanded', expand); this.element.classList.toggle('collapsible-collapsed', !expand); - if (expand) { - this.element.style.setProperty('--max-height', this.maxHeight + 'px'); - } else { - this.element.style.removeProperty('--max-height'); - } + this.setMaxHeight(!expand? this.maxHeight: undefined); + const toggleButton = this.element.querySelector('ion-button.collapsible-toggle'); const toggleText = toggleButton?.querySelector('.collapsible-toggle-text'); if (!toggleButton || !toggleText) { diff --git a/src/theme/globals.mixins.scss b/src/theme/globals.mixins.scss index e77e74181..cbde34e92 100644 --- a/src/theme/globals.mixins.scss +++ b/src/theme/globals.mixins.scss @@ -235,6 +235,7 @@ @include media-breakpoint-down(sm) { &.collapsible-enabled { position:relative; + padding-bottom: var(--collapsible-min-button-height); // So the Show less button can fit. --display-toggle: block; .collapsible-toggle { @@ -262,6 +263,8 @@ background-position: center; background-repeat: no-repeat; background-size: 14px 14px; + transform: rotate(-90deg); + @include core-transition(transform, 500ms); @include push-arrow-color(626262, true); @@ -275,7 +278,7 @@ &.collapsible-collapsed { overflow: hidden; min-height: calc(var(--collapsible-min-button-height) + 12px); - max-height: calc(var(--max-height), auto); + max-height: calc(var(--max-height, auto)); .collapsible-toggle-arrow { transform: rotate(90deg); @@ -291,15 +294,6 @@ z-index: 6; } } - - &.collapsible-expanded { - max-height: none !important; - padding-bottom: var(--collapsible-min-button-height); // So the Show less button can fit. - - .collapsible-toggle-arrow { - transform: rotate(-90deg); - } - } } } }