diff --git a/src/core/base/model.ts b/src/core/base/model.ts index 439b054e..f8e66943 100644 --- a/src/core/base/model.ts +++ b/src/core/base/model.ts @@ -2,7 +2,7 @@ import {SyntheticEventTarget} from '@exadel/esl/modules/esl-utils/dom'; import {decorate} from '@exadel/esl/modules/esl-utils/decorators'; import {microtask} from '@exadel/esl/modules/esl-utils/async'; -import {UIPHtmlNormalizationService} from '../processors/normalization'; +import {UIPNormalizationService} from '../processors/normalization'; import {UIPSnippetItem} from './snippet'; import type {UIPRoot} from './root'; @@ -42,18 +42,33 @@ export class UIPStateModel extends SyntheticEventTarget { /** Snippets {@link UIPSnippetItem} value objects */ private _snippets: UIPSnippetItem[]; + /** Current js state */ + private _js: string = ''; /** Current markup state */ private _html = new DOMParser().parseFromString('', 'text/html').body; /** Last {@link UIPPlugin} element which changed markup */ private _lastModifier: UIPPlugin | UIPRoot; + /** + * Sets current js state to the passed one + * @param js - new state + * @param modifier - plugin, that initiates the change + */ + public setJS(js: string, modifier: UIPPlugin | UIPRoot): void { + const script = UIPNormalizationService.normalize(js, false); + if (this._js === script) return; + this._js = script; + this._lastModifier = modifier; + this.dispatchChange(); + } + /** * Sets current markup state to the passed one * @param markup - new state * @param modifier - plugin, that initiates the change */ public setHtml(markup: string, modifier: UIPPlugin | UIPRoot): void { - const html = UIPHtmlNormalizationService.normalize(markup); + const html = UIPNormalizationService.normalize(markup); const root = new DOMParser().parseFromString(html, 'text/html').body; if (!root || root.innerHTML.trim() !== this.html.trim()) { this._html = root; @@ -62,6 +77,11 @@ export class UIPStateModel extends SyntheticEventTarget { } } + /** Current js state getter */ + public get js(): string { + return this._js; + } + /** Current markup state getter */ public get html(): string { return this._html ? this._html.innerHTML : ''; @@ -98,6 +118,7 @@ export class UIPStateModel extends SyntheticEventTarget { if (!snippet) return; this._snippets.forEach((s) => (s.active = s === snippet)); this.setHtml(snippet.html, modifier); + this.setJS(snippet.js, modifier); this.dispatchEvent( new CustomEvent('uip:model:snippet:change', {detail: this}) ); diff --git a/src/core/base/snippet.ts b/src/core/base/snippet.ts index 87de9d6e..d55c6dc3 100644 --- a/src/core/base/snippet.ts +++ b/src/core/base/snippet.ts @@ -7,6 +7,15 @@ export type UIPSnippetTemplate = HTMLTemplateElement | HTMLScriptElement; export class UIPSnippetItem { public constructor(protected readonly $element: UIPSnippetTemplate) {} + @memoize() + public get $elementJS(): UIPSnippetTemplate | null { + const $root = this.$element.closest('uip-root') || document.body; + const selectors = []; + if (this.$element.id) selectors.push(`[uip-js-snippet="${this.$element.id}"]`); + if (this.label) selectors.push(`[uip-js-snippet="${this.label}"]`, `[uip-js-snippet][label="${this.label}"]`); + return $root.querySelector(selectors.join(',')) as UIPSnippetTemplate; + } + /** @returns snippet's label */ @memoize() public get label(): string { @@ -19,6 +28,11 @@ export class UIPSnippetItem { return this.$element.innerHTML; } + @memoize() + public get js(): string { + return this.$elementJS ? this.$elementJS.innerHTML : ''; + } + /** @returns if the snippet is in active state */ public get active(): boolean { return this.$element.hasAttribute('active'); @@ -26,7 +40,7 @@ export class UIPSnippetItem { /** Sets the snippet active state */ public set active(active: boolean) { - if (active) this.$element.setAttribute('active', ''); - else this.$element.removeAttribute('active'); + this.$element.toggleAttribute('active', active); + this.$elementJS?.toggleAttribute('active', active); } } diff --git a/src/core/preview/preview.tsx b/src/core/preview/preview.tsx index f1c658e5..075993e0 100644 --- a/src/core/preview/preview.tsx +++ b/src/core/preview/preview.tsx @@ -89,8 +89,9 @@ export class UIPPreview extends UIPPlugin { } const title = this.model!.activeSnippet?.label || 'UI Playground'; + const script = this.model!.js; const content = UIPRenderingPreprocessorService.preprocess(this.model!.html); - const html = UIPRenderingTemplatesService.render(this.isolationTemplate, {title, content}); + const html = UIPRenderingTemplatesService.render(this.isolationTemplate, {title, content, script}); this.$iframe.contentWindow?.document.open(); this.$iframe.contentWindow?.document.write(html); diff --git a/src/core/processors/normalization.ts b/src/core/processors/normalization.ts index cf866284..54cdbc16 100644 --- a/src/core/processors/normalization.ts +++ b/src/core/processors/normalization.ts @@ -1,29 +1,34 @@ /** Function to process html string for normalization*/ -export type UIPNormalizationProcessor = (input: string) => string; +export type UIPNormalizationProcessor = ((input: string) => string) & { + htmlOnly?: boolean; +}; -export class UIPHtmlNormalizationService { +export class UIPNormalizationService { /** Processor storage */ protected static processors: Record = {}; /** Add processor function to normalization process */ public static addProcessor( name: string, - processor: UIPNormalizationProcessor + processor: UIPNormalizationProcessor, + htmlOnly = false ): void { - UIPHtmlNormalizationService.processors[name] = processor; + Object.assign(processor, {htmlOnly}); + UIPNormalizationService.processors[name] = processor; } - /** Normalizes passes html string by running all registered processors in chain */ - public static normalize(html: string): string { - return Object.keys(UIPHtmlNormalizationService.processors) - .map((name) => UIPHtmlNormalizationService.processors[name]) + /** Normalizes passes content string by running all registered processors in chain */ + public static normalize(content: string, isHtml = true): string { + return Object.keys(UIPNormalizationService.processors) + .map((name) => UIPNormalizationService.processors[name]) .filter((processor) => typeof processor === 'function') - .reduce((input, processor) => processor(input), html); + .filter((processor) => !processor.htmlOnly || isHtml) + .reduce((input, processor) => processor(input), content); } } /** Removes extra indents */ -UIPHtmlNormalizationService.addProcessor( +UIPNormalizationService.addProcessor( 'remove-indent', (input: string): string => { // Get all indents from text @@ -40,14 +45,16 @@ UIPHtmlNormalizationService.addProcessor( ); /** Removes extra spaces */ // TODO: handle case with inline script with literal double spaces -UIPHtmlNormalizationService.addProcessor('remove-trailing', (input: string) => - input.replace(/\s*?$/gm, '') +UIPNormalizationService.addProcessor( + 'remove-trailing', + (input: string) => input.replace(/\s*?$/gm, ''), + true ); /** Remove beginning spaces */ -UIPHtmlNormalizationService.addProcessor('left-trim', (input: string) => +UIPNormalizationService.addProcessor('left-trim', (input: string) => input.replace(/^\s+/, '') ); /** Remove ending spaces */ -UIPHtmlNormalizationService.addProcessor('right-trim', (input: string) => +UIPNormalizationService.addProcessor('right-trim', (input: string) => input.replace(/\s+$/, '') ); diff --git a/src/core/processors/templates.ts b/src/core/processors/templates.ts index 7c47438c..a3fe1f73 100644 --- a/src/core/processors/templates.ts +++ b/src/core/processors/templates.ts @@ -2,6 +2,7 @@ import {format} from '@exadel/esl/modules/esl-utils/misc'; interface UIPRenderingTemplateParams { title?: string; + script?: string; content: string; [additional: string]: string | number | boolean | undefined | null; } @@ -31,9 +32,8 @@ UIPRenderingTemplatesService.add('default', ` {title} + - - {content} - + {content} `);