diff --git a/src/core/preview/preview.less b/src/core/preview/preview.less index bf5849ee..81ef5f35 100644 --- a/src/core/preview/preview.less +++ b/src/core/preview/preview.less @@ -34,21 +34,30 @@ } &-inner { + position: relative; grid-area: uip-content; max-width: 100%; max-height: 100%; + + opacity: 1; + transition: opacity 0.2s linear; + } + + &[loading] &-inner { + opacity: 0; } &-iframe { + display: block; width: 100%; height: 100%; border: none; - - animation: uip-iframe-fade-in 0.4s ease-in; - &[hidden] { - display: none; - animation: ui-iframe-fade-out 0.4s ease-out; - } + } + &[loading] &-iframe { + position: absolute; + visibility: hidden; + max-height: 100%; + inset: 0; } &.centered-content &-inner { @@ -85,27 +94,3 @@ } } } - -@keyframes uip-iframe-fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes uip-iframe-fade-out { - from { - display: block; - opacity: 1; - } - 99% { - display: block; - opacity: 0; - } - to { - display: none; - opacity: 0; - } -} diff --git a/src/core/preview/preview.tsx b/src/core/preview/preview.tsx index 1cc51f9c..3c1011cf 100644 --- a/src/core/preview/preview.tsx +++ b/src/core/preview/preview.tsx @@ -3,7 +3,12 @@ import React from 'jsx-dom'; import {attr, listen, memoize, prop} from '@exadel/esl/modules/esl-utils/decorators'; import {ESLIntersectionTarget} from '@exadel/esl/modules/esl-event-listener/core'; import {parseBoolean, toBooleanAttribute} from '@exadel/esl/modules/esl-utils/misc'; -import {afterNextRender, skipOneRender} from '@exadel/esl/modules/esl-utils/async'; +import { + afterNextRender, + promisifyEvent, + promisifyNextRender, + promisifyTimeout, +} from '@exadel/esl/modules/esl-utils/async'; import {UIPPlugin} from '../base/plugin'; import {UIPRenderingTemplatesService} from '../processors/templates'; @@ -26,6 +31,9 @@ export class UIPPreview extends UIPPlugin { /** Marker to force iframe rerendering */ @attr({parser: parseBoolean, serializer: toBooleanAttribute}) public forceUpdate: boolean; + /** Delay to show new content after isolated full refresh */ + @attr({defaultValue: 150, parser: parseInt}) public refreshDelay: number; + protected _iframeResizeRAF: number = 0; /** {@link UIPPlugin} section wrapper */ @@ -68,30 +76,34 @@ export class UIPPreview extends UIPPlugin { if (attrName === 'dir') this.updateDir(); } - protected update(e?: UIPChangeEvent): void { + protected async update(e?: UIPChangeEvent): Promise { const isolated = this.model!.activeSnippet?.isolated || false; if (!isolated) return this.writeContent(); if (!e || this.forceUpdate || e.force) return this.writeIsolatedContent(); - this.updateIsolatedContent(); + return this.updateIsolatedContent(); } /** Writes the content directly to the inner area (non-isolated frame) */ - protected writeContent(): void { + protected async writeContent(): Promise { this.$inner.innerHTML = UIPHTMLRenderingPreprocessors.preprocess(this.model!.html); this.stopIframeResizeLoop(); + return promisifyNextRender(); } - protected updateIsolatedContent(): void { + protected async updateIsolatedContent(): Promise { if (!this.$iframe.contentWindow) return; const $document = this.$iframe.contentWindow?.document; const $root = $document?.querySelector('[uip-content-root]') || $document?.body; - if ($root) $root.innerHTML = UIPHTMLRenderingPreprocessors.preprocess(this.model!.html); + if ($root) { + $root.innerHTML = UIPHTMLRenderingPreprocessors.preprocess(this.model!.html); + return promisifyNextRender(); + } } /** Writes the content to the iframe inner (isolated frame) */ - protected writeIsolatedContent(): void { + protected async writeIsolatedContent(): Promise { if (this.$iframe.parentElement !== this.$inner) { this.$inner.innerHTML = ''; this.$inner.appendChild(this.$iframe); @@ -100,6 +112,9 @@ export class UIPPreview extends UIPPlugin { this.$iframe.src = 'about:blank'; this.$iframe.hidden = true; this.startIframeResizeLoop(); + + await promisifyEvent(this.$iframe, 'load'); + await promisifyTimeout(this.refreshDelay); } /** Start and do a resize sync-loop iteration. Recall itself on the next frame. */ @@ -150,19 +165,21 @@ export class UIPPreview extends UIPPlugin { this.$iframe.contentWindow?.document.close(); this.$iframe.contentWindow?.document.write(html); this.$iframe.contentWindow?.document.close(); + this.$iframe.hidden = false; this.$iframe.title = title; - - setTimeout(() => this.$iframe.hidden = false, 100); } /** Updates preview content from the model state changes */ @listen({event: 'uip:change', target: ($this: UIPPreview) => $this.$root}) - protected _onRootStateChange(e?: UIPChangeEvent): void { + protected async _onRootStateChange(e?: UIPChangeEvent): Promise { this.$container.style.minHeight = `${this.$inner.offsetHeight}px`; - this.update(e); + this.$$attr('loading', true); + + await this.update(e); - afterNextRender(() => this.$container.style.minHeight = '0px'); - skipOneRender(() => { + this.$container.style.minHeight = '0px'; + this.$$attr('loading', false); + afterNextRender(() => { if (this.$container.clientHeight !== this.$inner.offsetHeight) return; this.$container.style.removeProperty('min-height'); }); diff --git a/src/core/processors/normalization.ts b/src/core/processors/normalization.ts index 9e9231d2..c02b9efa 100644 --- a/src/core/processors/normalization.ts +++ b/src/core/processors/normalization.ts @@ -38,6 +38,3 @@ UIPNoteNormalizationPreprocessors.add('remove-leading-indent', removeIndent); UIPJSNormalizationPreprocessors.add('trim', (content: string) => content.trim()); UIPHTMLNormalizationPreprocessors.add('trim', (content: string) => content.trim()); UIPNoteNormalizationPreprocessors.add('trim', (content: string) => content.trim()); - -/** Removes extra spaces inside the content. Applicable for HTML only */ -UIPHTMLNormalizationPreprocessors.addRegexReplacer('remove-trailing', /\s*?$/gm, ''); // TODO: possibly problem here