diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index afd53beae..4582f38ce 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -90,8 +90,8 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo protected element: HTMLElement; protected emptyText = ''; - protected contentSpan: HTMLElement; - protected domPromise?: CoreCancellablePromise; + protected domPromises: CoreCancellablePromise[] = []; + protected domElementPromise?: CoreCancellablePromise; constructor( element: ElementRef, @@ -101,18 +101,10 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo CoreComponentsRegistry.register(element.nativeElement, this); this.element = element.nativeElement; - this.element.classList.add('core-format-text-loading'); // Hide contents until they're treated. - - const placeholder = document.createElement('span'); - placeholder.classList.add('core-format-text-loader'); - this.element.appendChild(placeholder); - - this.contentSpan = document.createElement('span'); - this.contentSpan.classList.add('core-format-text-content'); - this.element.appendChild(this.contentSpan); + this.element.classList.add('core-loading'); // Hide contents until they're treated. this.emptyText = this.hideIfEmpty ? '' : ' '; - this.contentSpan.innerHTML = this.emptyText; + this.element.innerHTML = this.emptyText; this.afterRender = new EventEmitter(); @@ -134,14 +126,15 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo * @inheritdoc */ ngOnDestroy(): void { - this.domPromise?.cancel(); + this.domElementPromise?.cancel(); + this.domPromises.forEach((promise) => { promise.cancel();}); } /** * @inheritdoc */ async ready(): Promise { - if (!this.element.classList.contains('core-format-text-loading')) { + if (!this.element.classList.contains('core-loading')) { return; } @@ -229,7 +222,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo * Add magnifying glass icons to view adapted images at full size. */ async addMagnifyingGlasses(): Promise { - const imgs = Array.from(this.contentSpan.querySelectorAll('.core-adapted-img-container > img')); + const imgs = Array.from(this.element.querySelectorAll('.core-adapted-img-container > img')); if (!imgs.length) { return; } @@ -311,7 +304,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo */ protected async finishRender(): Promise { // Show the element again. - this.element.classList.remove('core-format-text-loading'); + this.element.classList.remove('core-loading'); await CoreUtils.nextTick(); @@ -324,7 +317,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo */ protected async formatAndRenderContents(): Promise { if (!this.text) { - this.contentSpan.innerHTML = this.emptyText; // Remove current contents. + this.element.innerHTML = this.emptyText; // Remove current contents. await this.finishRender(); @@ -342,10 +335,10 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo // Disable media adapt to correctly calculate the height. this.element.classList.add('core-disable-media-adapt'); - this.contentSpan.innerHTML = ''; // Remove current contents. + this.element.innerHTML = ''; // Remove current contents. // Move the children to the current element to be able to calculate the height. - CoreDomUtils.moveChildren(result.div, this.contentSpan); + CoreDomUtils.moveChildren(result.div, this.element); await CoreUtils.nextTick(); @@ -362,7 +355,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo if (result.options.filter) { // Let filters handle HTML. We do it here because we don't want them to block the render of the text. CoreFilterDelegate.handleHtml( - this.contentSpan, + this.element, result.filters, this.viewContainerRef, result.options, @@ -557,9 +550,10 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo * @return The width of the element in pixels. */ protected async getElementWidth(): Promise { - this.domPromise = CoreDomUtils.waitToBeInDOM(this.element); - - await this.domPromise; + if (!this.domElementPromise) { + this.domElementPromise = CoreDomUtils.waitToBeInDOM(this.element); + } + await this.domElementPromise; let width = this.element.getBoundingClientRect().width; if (!width) { @@ -709,12 +703,15 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo newUrl += `&h=${privacyHash}`; } + const domPromise = CoreDomUtils.waitToBeInDOM(iframe); + this.domPromises.push(domPromise); + + await domPromise; + // Width and height are mandatory, we need to calculate them. let width: string | number; let height: string | number; - await CoreDomUtils.waitToBeInDOM(iframe, 5000); - if (iframe.width) { width = iframe.width; } else { diff --git a/src/core/directives/tests/format-text.test.ts b/src/core/directives/tests/format-text.test.ts index 8be19f17e..48cb3ca44 100644 --- a/src/core/directives/tests/format-text.test.ts +++ b/src/core/directives/tests/format-text.test.ts @@ -58,7 +58,7 @@ describe('CoreFormatTextDirective', () => { ); // Assert - const text = fixture.nativeElement.querySelector('core-format-text .core-format-text-content'); + const text = fixture.nativeElement.querySelector('core-format-text'); expect(text).not.toBeNull(); expect(text.innerHTML).toEqual(sentence); }); diff --git a/src/theme/components/format-text.scss b/src/theme/components/format-text.scss index 3b35bff23..a242fe175 100644 --- a/src/theme/components/format-text.scss +++ b/src/theme/components/format-text.scss @@ -5,77 +5,38 @@ core-format-text { --core-format-text-background: var(--background, var(--ion-item-background)); --core-format-text-viewer-icon-background: rgba(255, 255, 255, .5); - --core-format-text-loader-shine: 251,251,251; } body.dark core-format-text { --core-format-text-viewer-icon-background: rgba(0, 0, 0, .5); - --core-format-text-loader-shine: 90,90,90; } core-format-text { display: contents; + user-select: text; + word-break: break-word; + word-wrap: break-word; - .core-format-text-loader { - opacity: 0; - @include core-transition(opacity, 200ms); - display: contents; - } - - &.core-format-text-loading { - position: relative; + @include core-transition(background-color color, 200ms); + &.core-loading { width: 100%; - height: 100%; - opacity: 1; - background-color: rgba(0,0,0,.1); - overflow: hidden; - display: block; - border-radius: var(--small-radius); - - .core-format-text-loader { - position: absolute; - left: -45%; - height: 100%; - width: 45%; - background-image: -webkit-linear-gradient(to left, rgba(var(--core-format-text-loader-shine), .05), rgba(var(--core-format-text-loader-shine), .3), rgba(var(--core-format-text-loader-shine), .6), rgba(var(--core-format-text-loader-shine), .3), rgba(var(--core-format-text-loader-shine), .05)); - background-image: linear-gradient(to left, rgba(var(--core-format-text-loader-shine), .05), rgba(var(--core-format-text-loader-shine), .3), rgba(var(--core-format-text-loader-shine), .6), rgba(var(--core-format-text-loader-shine), .3), rgba(var(--core-format-text-loader-shine), .05)); - animation: loading 1s infinite; - opacity: 1; - display: inline; - } - - .core-format-text-content { + &:empty:before { + content: 'E'; // Set a minimum empty text to have a minimum height of one line. opacity: 0; - display: inline; } } - .core-format-text-content { - opacity: 1; - @include core-transition(opacity, 200ms); - - display: contents; - user-select: text; - word-break: break-word; - word-wrap: break-word; - } - &.collapsible-item { display: block; // This is to allow clicks in radio/checkbox content. cursor: pointer; pointer-events: auto; - .core-format-text-content { - display: block; - } - &.collapsible-enabled { - .core-format-text-content { - display: block; - max-height: none; - } - &.collapsible-collapsed .core-format-text-content { + display: block; + max-height: none; + + &.collapsible-collapsed { overflow: hidden; } } @@ -97,7 +58,7 @@ core-format-text { } &.collapsible-item.inline { display: inline-block; - &.collapsible-enabled .core-format-text-content { + &.collapsible-enabled { display: inline-block; } } @@ -141,7 +102,7 @@ core-format-text { } // Disable clicks in links inside MathJax equations. - .core-format-text-content .filter_mathjaxloader_equation .MathJax_Preview a { + .filter_mathjaxloader_equation .MathJax_Preview a { pointer-events: none; } @@ -243,7 +204,7 @@ core-format-text { } -core-format-text .core-format-text-content, +core-format-text, core-rich-text-editor .core-rte-editor { @include core-headings(); @@ -657,14 +618,10 @@ core-rich-text-editor .core-rte-editor { // h1 is too big and ugly, reduce size when loading. ion-header.ios h1 core-format-text { - &.core-format-text-loading { + &.core-loading { max-height: 30px; margin-top: 10px; } - &.core-format-text-content { - display: block; - margin-top: -10px; - } } body.dark core-format-text select, diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index 250f07ba4..ac9e6756d 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -120,15 +120,15 @@ body { } // Correctly inherit ion-text-wrap onto labels. -.item ion-label core-format-text .core-format-text-content > *, -.fake-ion-item core-format-text .core-format-text-content > * { +.item ion-label core-format-text > *, +.fake-ion-item core-format-text > * { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.item.ion-text-wrap > ion-label core-format-text .core-format-text-content > *, -.fake-ion-item.ion-text-wrap core-format-text .core-format-text-content > * { +.item.ion-text-wrap > ion-label core-format-text > *, +.fake-ion-item.ion-text-wrap core-format-text > * { white-space: normal; overflow: inherit; } @@ -334,7 +334,7 @@ ion-button.button-solid { --box-shadow: none; } -ion-button core-format-text .core-format-text-content { +ion-button core-format-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -350,7 +350,7 @@ ion-button > * { ion-button.ion-text-wrap { white-space: normal; - core-format-text .core-format-text-content { + core-format-text { white-space: normal; display: contents; } @@ -1516,3 +1516,36 @@ ion-header.no-title { flex-grow: 1; } } + + +// Loader animation. +.core-loading { + position: relative; + background-color: var(--loader-background-color); + color: transparent; // Hide contents. + overflow: hidden; + display: var(--loader-display); + border-radius: var(--loader-radius); + @include core-transition(all, 200ms); + min-height: 8px; + min-width: 50px; + + // Hide contents. + > * { + opacity: 0; + @include core-transition(opacity, 200ms); + } + + &::after { + content: ''; + position: absolute; + left: -45%; + height: 100%; + width: 45%; + background-image: linear-gradient(to left, rgba(var(--loader-shine), .05), rgba(var(--loader-shine), .3), rgba(var(--loader-shine), .6), rgba(var(--loader-shine), .3), rgba(var(--loader-shine), .05)); + animation: loading 1s infinite; + display: block; + top: 0; + bottom: 0; + } +} diff --git a/src/theme/theme.dark.scss b/src/theme/theme.dark.scss index 15d9b7e14..ecd188b0c 100644 --- a/src/theme/theme.dark.scss +++ b/src/theme/theme.dark.scss @@ -41,6 +41,8 @@ --contrast-background: var(--gray-900); + --loader-shine: 90, 90, 90; + --drop-shadow-color: 0, 0, 0, 1; --drop-shadow-top: 0px 2px 5px rgba(var(--drop-shadow-color)); --drop-shadow-bottom: 0px -2px 5px rgba(var(--drop-shadow-color)); diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss index 533c22f1d..284d216cb 100644 --- a/src/theme/theme.light.scss +++ b/src/theme/theme.light.scss @@ -85,6 +85,11 @@ --contrast-background: white; + --loader-background-color: rgba(0, 0, 0, .1); + --loader-shine: 251, 251, 251; + --loader-radius: var(--small-radius); + --loader-display: block; + --drop-shadow-color: 0, 0, 0, 0.5; --drop-shadow-top: 0px 2px 5px rgba(var(--drop-shadow-color)); --drop-shadow-bottom: 0px -2px 5px rgba(var(--drop-shadow-color));