diff --git a/src/core/preview/preview.less b/src/core/preview/preview.less index f091f742..21ae9abe 100644 --- a/src/core/preview/preview.less +++ b/src/core/preview/preview.less @@ -41,7 +41,7 @@ &-iframe { width: 100%; - min-height: 100%; + height: 100%; border: none; } diff --git a/src/core/preview/preview.tsx b/src/core/preview/preview.tsx index 7c855fb1..f864c4f0 100644 --- a/src/core/preview/preview.tsx +++ b/src/core/preview/preview.tsx @@ -1,6 +1,6 @@ import React from 'jsx-dom'; -import {attr, listen, memoize} from '@exadel/esl/modules/esl-utils/decorators'; +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'; @@ -19,6 +19,9 @@ export class UIPPreview extends UIPPlugin { static is = 'uip-preview'; static observedAttributes: string[] = ['dir', 'resizable', 'isolation', 'isolation-template']; + /** Sync height with inner iframe content height */ + @prop(true) public resizeLoop: boolean; + /** Marker to use iframe isolated rendering */ @attr({parser: parseBoolean, serializer: toBooleanAttribute}) public isolation: boolean; /** Template to use for isolated rendering */ @@ -51,19 +54,6 @@ export class UIPPreview extends UIPPlugin { ) as HTMLElement; } - /** Updates preview content from the model state changes */ - @listen({event: 'uip:change', target: ($this: UIPPreview) => $this.$root}) - protected _onRootStateChange(): void { - this.$container.style.minHeight = `${this.$inner.offsetHeight}px`; - this.isolation ? this.writeContentIsolated() : this.writeContent(); - - afterNextRender(() => this.$container.style.minHeight = '0px'); - skipOneRender(() => { - if (this.$container.clientHeight !== this.$inner.offsetHeight) return; - this.$container.style.removeProperty('min-height'); - }); - } - protected override connectedCallback(): void { super.connectedCallback(); this.appendChild(this.$container); @@ -74,6 +64,15 @@ export class UIPPreview extends UIPPlugin { super.disconnectedCallback(); } + protected override attributeChangedCallback(attrName: string, oldVal: string, newVal: string): void { + if (attrName === 'resizable' && newVal === null) this.clearInlineSize(); + if (attrName === 'dir') this.updateDir(); + if (attrName === 'isolation' || attrName === 'isolation-template') { + this.$$on(this._onIframeLoad); + this._onRootStateChange(); + } + } + /** Writes the content directly to the inner area (non-isolated frame) */ protected writeContent(): void { this.$inner.innerHTML = UIPHTMLRenderingPreprocessors.preprocess(this.model!.html); @@ -85,21 +84,15 @@ export class UIPPreview extends UIPPlugin { if (this.$iframe.parentElement !== this.$inner) { this.$inner.innerHTML = ''; this.$inner.appendChild(this.$iframe); - this.startIframeResizeLoop(); } - - const title = this.model!.activeSnippet?.label || 'UI Playground'; - const script = UIPJSRenderingPreprocessors.preprocess(this.model!.js); - const content = UIPHTMLRenderingPreprocessors.preprocess(this.model!.html); - const html = UIPRenderingTemplatesService.render(this.isolationTemplate, {title, content, script}); - - this.$iframe.contentWindow?.document.open(); - this.$iframe.contentWindow?.document.write(html); - this.$iframe.contentWindow?.document.close(); + this.stopIframeResizeLoop(); + this.$iframe.src = ''; + this.startIframeResizeLoop(); } /** Start and do a resize sync-loop iteration. Recall itself on the next frame. */ protected startIframeResizeLoop(): void { + if (!this.resizeLoop) return; // Prevents multiple loops if (this._iframeResizeRAF) cancelAnimationFrame(this._iframeResizeRAF); // Addition loop fallback for iframe removal @@ -116,12 +109,6 @@ export class UIPPreview extends UIPPlugin { this._iframeResizeRAF = 0; } - protected override attributeChangedCallback(attrName: string, oldVal: string, newVal: string): void { - if (attrName === 'resizable' && newVal === null) this.clearInlineSize(); - if (attrName === 'dir') this.updateDir(); - if (attrName === 'isolation' || attrName === 'isolation-template') this._onRootStateChange(); - } - /** Resets element both inline height and width properties */ protected clearInlineSize(): void { this.$inner.style.removeProperty('height'); @@ -133,6 +120,37 @@ export class UIPPreview extends UIPPlugin { isChanged && this.$$fire('uip:dirchange'); } + @listen({ + event: 'load', + target: ($this: UIPPreview) => $this.$iframe, + condition: ($this: UIPPreview) => $this.isolation + }) + protected _onIframeLoad(): void { + if (!this.$iframe.contentWindow) return; + + const title = this.model!.activeSnippet?.label || 'UI Playground'; + const script = UIPJSRenderingPreprocessors.preprocess(this.model!.js); + const content = UIPHTMLRenderingPreprocessors.preprocess(this.model!.html); + const html = UIPRenderingTemplatesService.render(this.isolationTemplate, {title, content, script}); + + this.$iframe.contentWindow?.document.close(); + this.$iframe.contentWindow?.document.write(html); + this.$iframe.contentWindow?.document.close(); + } + + /** Updates preview content from the model state changes */ + @listen({event: 'uip:change', target: ($this: UIPPreview) => $this.$root}) + protected _onRootStateChange(): void { + this.$container.style.minHeight = `${this.$inner.offsetHeight}px`; + this.isolation ? this.writeContentIsolated() : this.writeContent(); + + afterNextRender(() => this.$container.style.minHeight = '0px'); + skipOneRender(() => { + if (this.$container.clientHeight !== this.$inner.offsetHeight) return; + this.$container.style.removeProperty('min-height'); + }); + } + /** Handles end of animation playing while the demo content change */ @listen({ event: 'transitionend', diff --git a/src/core/processors/templates.ts b/src/core/processors/templates.ts index a3fe1f73..ab392f1c 100644 --- a/src/core/processors/templates.ts +++ b/src/core/processors/templates.ts @@ -32,6 +32,7 @@ UIPRenderingTemplatesService.add('default', `