/* eslint-disable no-control-regex */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { html as langHtml } from '@codemirror/lang-html'; import { javascript as langJs } from '@codemirror/lang-javascript'; import { githubDark as codeThemeDark } from '@ddietr/codemirror-themes/github-dark.js'; import { githubLight as codeTheme } from '@ddietr/codemirror-themes/github-light.js'; import { html, LitElement, nothing, PropertyValueMap, render } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { live } from 'lit/directives/live.js'; import { ColorField } from '../color-field/ColorField.js'; import { SearchField } from '../search-field/SearchField.js'; import { TextField } from '../text-field/TextField.js'; import { CodeEditor } from './CodeEditor.js'; import { LivePropertyEditor, PropertyChangeEvent } from './LivePropertyEditor.js'; import { StoryController } from './StoryController.js'; import { loadCustomElementsCodeMirrorCompletionsRemote, loadCustomElements, loadCssProperties, Package, ComponentStoryFormat, FrameworkOption, transformSource, getSourceFromLit, frameworkStorageKey } from './StoryUtils.js'; import '../label/Label.js'; import '../button/Button'; import '../icon/Icon.js'; import '../icons/Loading.icon.js'; import './CodeEditor.js'; import '../text-field/TextField.js'; import '../color-field/ColorField.js'; import './LivePropertyEditor.js'; /** * @ignore */ @customElement('story-renderer') export class StoryRenderer extends LitElement { @property({ type: String, reflect: true }) path?: string; @property({ type: String, reflect: true }) tag?: string; @property({ type: String, reflect: true }) key?: string; @property({ type: Boolean, reflect: true }) interactive?: boolean; @state() _interactiveSrc?: string; @state() _showStylesDialog?: boolean; @query('.primary-source-code') codeEditor?: CodeEditor; @query('.secondary-js-source-code') secondaryCodeEditor?: CodeEditor; @query('.live-props') propertyEditor?: LivePropertyEditor; private controller?: StoryController; private customCss?: HTMLStyleElement; // eslint-disable-next-line @typescript-eslint/no-explicit-any private story?: ComponentStoryFormat & { // eslint-disable-next-line @typescript-eslint/no-explicit-any originalArgs: any; }; private customElements?: Package; private cssVariables?: CSSVariable[]; private modal?: HTMLDivElement; private theme?: string; readonly sourceFallbacks: [ { fallbackFramework: FrameworkOption; frameworks: FrameworkOption[]; allowRenderFromResult?: boolean; } ] = [ { fallbackFramework: 'HTML', frameworks: ['HTML', 'Lit', 'Vue'], allowRenderFromResult: true } ]; readonly noInteractiveCodePen: FrameworkOption[] = ['React']; override async connectedCallback() { super.connectedCallback(); this.controller = new StoryController(this, this.path as string); this.customCss = document.head.querySelector('#custom-css-vars') as HTMLStyleElement; if (!this.customCss) { this.customCss = document.createElement('style'); this.customCss.id = 'custom-css-vars'; document.head.appendChild(this.customCss); const storedCssOverrides = sessionStorage.getItem(`custom-css-${this.tag}`); if (storedCssOverrides) { this.customCss.innerHTML = storedCssOverrides; } } if (!this.modal) { this.modal = document.createElement('div'); document.body.appendChild(this.modal); } this.customElements = await loadCustomElements(); const cssProperties = loadCssProperties(this.tag as string, this.customElements); this.cssVariables = Object.keys(cssProperties) .filter((key) => cssProperties[key].subcategory === 'Component Variables') .map((key) => { const cssProp = cssProperties[key]; return { name: key, ...cssProp }; }); document.addEventListener('omni-docs-theme-change', () => { this.theme = getComputedStyle(document.documentElement).getPropertyValue('--code-editor-theme')?.trim(); const codeEditors = this.renderRoot.querySelectorAll('code-editor'); if (codeEditors) { codeEditors.forEach((ce) => { ce.updateExtensions(); }); } }); document.addEventListener('omni-docs-framework-change', async () => { const codeEditors = this.renderRoot.querySelectorAll('code-editor'); if (codeEditors) { codeEditors.forEach((ce) => { ce.updateExtensions(); }); } let sourceTab = (window.localStorage.getItem(frameworkStorageKey) ?? 'HTML') as FrameworkOption; let frameworkDefinition = this.story!.frameworkSources?.find((fs) => fs.framework === sourceTab); if (!frameworkDefinition) { sourceTab = this.sourceFallbacks.find((sf) => sf.frameworks.includes(sourceTab))?.fallbackFramework ?? sourceTab; frameworkDefinition = this.story!.frameworkSources?.find((fs) => fs.framework === sourceTab); } if (this.codeEditor) { await this.codeEditor.refresh(() => { const source = frameworkDefinition?.sourceParts?.htmlFragment ? typeof frameworkDefinition?.sourceParts?.htmlFragment === 'string' ? frameworkDefinition?.sourceParts?.htmlFragment : frameworkDefinition?.sourceParts?.htmlFragment(this.story!.args) : frameworkDefinition?.load ? frameworkDefinition.load(this.story!.args, frameworkDefinition) : this.sourceFallbacks.find((sf) => sf.frameworks.includes(sourceTab))?.allowRenderFromResult ? getSourceFromLit(this.story!.render!(this.story!.args)) : ''; if (source) { this.renderRoot.querySelector('.primary-code-block')?.classList.remove('no-display'); } else { this.renderRoot.querySelector('.primary-code-block')?.classList.add('no-display'); } return source; }); if (this.secondaryCodeEditor) { await this.secondaryCodeEditor.refresh(() => { const source = frameworkDefinition?.sourceParts?.jsFragment ? typeof frameworkDefinition?.sourceParts?.jsFragment === 'string' ? frameworkDefinition?.sourceParts?.jsFragment : frameworkDefinition?.sourceParts?.jsFragment(this.story!.args) : ''; if (source) { this.renderRoot.querySelector('.secondary-code-block')?.classList.remove('no-display'); this.renderRoot.querySelectorAll('.code-title').forEach((t) => t?.classList.remove('no-display')); } else { this.renderRoot.querySelector('.secondary-code-block')?.classList.add('no-display'); this.renderRoot.querySelectorAll('.code-title').forEach((t) => t?.classList.add('no-display')); } return source; }); } } const codePenGen = this.renderRoot.querySelector('.code-pen-gen-btn'); if (codePenGen) { const noDisplay = frameworkDefinition?.disableCodePen || (this.noInteractiveCodePen.includes(sourceTab) && this.interactive); if (noDisplay) { codePenGen?.classList.add('no-display'); } else { codePenGen?.classList.remove('no-display'); } } }); document.addEventListener(interactiveUpdate, () => { this.requestUpdate(); }); this.theme = getComputedStyle(document.documentElement).getPropertyValue('--code-editor-theme')?.trim(); } override disconnectedCallback() { if (this.modal) { document.body.removeChild(this.modal); this.modal = undefined; } } protected override updated(_changedProperties: PropertyValueMap | Map): void { super.updated(_changedProperties); window.Prism?.highlightAll(); } protected override render() { if (!this.controller?.story) { return html``; } render( html` ${ this._showStylesDialog ? html` ` : nothing } `, this.modal as HTMLDivElement ); this.story = this.controller.story[this.key as string]; this.story!.originalArgs = this.story?.originalArgs ?? JSON.parse(JSON.stringify(this.story?.args ?? {})); Object.keys(this.story?.args ?? {}).forEach((o) => { if (this.story!.args![o] === undefined) { this.story!.originalArgs[o] = undefined; } }); const res = this.story!.render!(this.story!.args); let sourceTab = (window.localStorage.getItem(frameworkStorageKey) ?? 'HTML') as FrameworkOption; let frameworkDefinition = this.story!.frameworkSources?.find((fs) => fs.framework === sourceTab); const getSourceTab = () => { sourceTab = (window.localStorage.getItem(frameworkStorageKey) ?? 'HTML') as FrameworkOption; frameworkDefinition = this.story!.frameworkSources?.find((fs) => fs.framework === sourceTab); if (!frameworkDefinition) { sourceTab = this.sourceFallbacks.find((sf) => sf.frameworks.includes(sourceTab))?.fallbackFramework ?? sourceTab; frameworkDefinition = this.story!.frameworkSources?.find((fs) => fs.framework === sourceTab); } return sourceTab; }; getSourceTab(); const frameworkSource = frameworkDefinition?.sourceParts?.htmlFragment ? typeof frameworkDefinition?.sourceParts?.htmlFragment === 'string' ? frameworkDefinition?.sourceParts?.htmlFragment : frameworkDefinition?.sourceParts?.htmlFragment(this.story!.args) : frameworkDefinition?.load ? frameworkDefinition.load(this.story!.args, frameworkDefinition) : this.sourceFallbacks.find((sf) => sf.frameworks.includes(sourceTab))?.allowRenderFromResult ? getSourceFromLit(res) : ''; const secondarySource = frameworkDefinition?.sourceParts?.jsFragment ? typeof frameworkDefinition?.sourceParts?.jsFragment === 'string' ? frameworkDefinition?.sourceParts?.jsFragment : frameworkDefinition?.sourceParts?.jsFragment(this.story!.args) : ''; return html`
${this.story?.description && typeof this.story?.description === 'function' ? this.story.description() : this.story?.description}
${res}
${ this.interactive ? html`
` : nothing }
HTML
JS
`; } handleCustomThemeCSSVariableSearch(e: Event) { const filterValue = (e.target as SearchField).value ?? ''; const table = document.querySelector(`[data-target=custom-css-table-${this.tag}]`) as HTMLTableSectionElement; const cssPropRows = table.children; for (let index = 0; index < cssPropRows.length; index++) { const element = cssPropRows[index] as HTMLElement; if (element.innerText && element.innerText.toLowerCase().includes((filterValue).toLowerCase())) { element.classList.remove('hidden'); } else { element.classList.add('hidden'); } } } renderCssVariable(variable: CSSVariable) { const css = this.customCss?.sheet; if (variable.name) { let rootCss: CSSStyleRule = undefined as never; if (css?.cssRules.length === 0) { const index = css.insertRule(':root {}'); rootCss = css.cssRules.item(index) as CSSStyleRule; } else { for (let index = 0; index < css!.cssRules.length; index++) { const rule = css!.cssRules[index] as CSSStyleRule; if (rule.selectorText === ':root') { rootCss = rule; break; } } } if (rootCss) { variable.value = rootCss.style.getPropertyValue(`--${variable.name}`); } if (variable.control === 'color') { return html` `; } else { return html` `; } } return nothing; } protected override createRenderRoot(): HTMLElement | DocumentFragment { return this; } private _sortCssVariables(a: CSSVariable, b: CSSVariable) { const css = this.customCss?.sheet; let rootCss: CSSStyleRule = undefined as never; if (css?.cssRules.length === 0) { const index = css.insertRule(':root {}'); rootCss = css.cssRules.item(index) as CSSStyleRule; } else { for (let index = 0; index < css!.cssRules.length; index++) { const rule = css?.cssRules[index] as CSSStyleRule; if (rule.selectorText === ':root') { rootCss = rule; break; } } } if (rootCss) { a.value = rootCss.style.getPropertyValue(`--${a.name}`); b.value = rootCss.style.getPropertyValue(`--${b.name}`); } return a.value ? (b.value ? 0 : -1) : b.value ? 1 : 0; } private _cssChanged(changed: CSSVariable) { const css = this.customCss?.sheet; let rootCss: CSSStyleRule = undefined as never; if (css?.cssRules.length === 0) { const index = css.insertRule(':root {}'); rootCss = css.cssRules.item(index) as CSSStyleRule; } else { for (let index = 0; index < css!.cssRules.length; index++) { const rule = css?.cssRules[index] as CSSStyleRule; if (rule.selectorText === ':root') { rootCss = rule; break; } } } if (changed.value) { if (rootCss) { rootCss.style.setProperty(`--${changed.name}`, changed.value); } } else { if (rootCss) { rootCss.style.removeProperty(`--${changed.name}`); } } const storedCssOverrides = rootCss.cssText; sessionStorage.setItem(`custom-css-${this.tag}`, storedCssOverrides); } private _showComponentStyles() { this._showStylesDialog = true; } private _checkCloseModal(e: Event) { const containerElement = this.modal?.querySelector(`div.modal-container`) as Element; if (!e.composedPath().includes(containerElement)) { this._showStylesDialog = false; } } private async _resetLivePropertyEditor() { this.story!.args = JSON.parse(JSON.stringify(this.story?.originalArgs)); Object.keys(this.story?.originalArgs ?? {}).forEach((o) => { if (this.story!.originalArgs![o] === undefined) { this.story!.args![o] = undefined; } }); const css = this.customCss?.sheet; for (let index = 0; index < css!.cssRules.length; index++) { const rule = css?.cssRules[index] as CSSStyleRule; if (rule.selectorText === ':root') { css?.deleteRule(index); break; } } sessionStorage.removeItem(`custom-css-${this.tag}`); this.requestUpdate(); this.dispatchEvent( new CustomEvent(interactiveUpdate, { bubbles: true, composed: true }) ); await this.updateComplete; if (this.codeEditor) { let sourceTab = (window.localStorage.getItem(frameworkStorageKey) ?? 'HTML') as FrameworkOption; let frameworkDefinition = this.story!.frameworkSources?.find((fs) => fs.framework === sourceTab); if (!frameworkDefinition) { sourceTab = this.sourceFallbacks.find((sf) => sf.frameworks.includes(sourceTab))?.fallbackFramework ?? sourceTab; frameworkDefinition = this.story!.frameworkSources?.find((fs) => fs.framework === sourceTab); } await this.codeEditor.refresh(() => frameworkDefinition?.sourceParts?.htmlFragment ? typeof frameworkDefinition?.sourceParts?.htmlFragment === 'string' ? frameworkDefinition?.sourceParts?.htmlFragment : frameworkDefinition?.sourceParts?.htmlFragment(this.story!.args) : frameworkDefinition?.load ? frameworkDefinition.load(this.story!.args, frameworkDefinition) : this.sourceFallbacks.find((sf) => sf.frameworks.includes(sourceTab))?.allowRenderFromResult ? getSourceFromLit(this.story!.render!(this.story!.args)) : '' ); if (this.secondaryCodeEditor) { this.secondaryCodeEditor.refresh(() => { const source = frameworkDefinition?.sourceParts?.jsFragment ? typeof frameworkDefinition?.sourceParts?.jsFragment === 'string' ? frameworkDefinition?.sourceParts?.jsFragment : frameworkDefinition?.sourceParts?.jsFragment(this.story!.args) : ''; if (source) { this.renderRoot.querySelector('.secondary-code-block')?.classList.remove('no-display'); this.renderRoot.querySelectorAll('.code-title').forEach((t) => t?.classList.remove('no-display')); } else { this.renderRoot.querySelector('.secondary-code-block')?.classList.add('no-display'); this.renderRoot.querySelectorAll('.code-title').forEach((t) => t?.classList.add('no-display')); } return source; }); } } if (this.propertyEditor) { this.propertyEditor.resetSlots(); } } private async _generateCodePen(source: FrameworkOption) { const version = (document.getElementById('header-version-indicator')?.innerText ?? '').toLowerCase(); const frameworkDefinition = this.story!.frameworkSources?.find((fs) => fs.framework === source) ?? this.story!.frameworkSources?.find( (fs) => fs.framework === this.sourceFallbacks.find((sf) => sf.frameworks.includes(source))?.fallbackFramework ); const sourceCode = frameworkDefinition?.sourceParts?.htmlFragment ? typeof frameworkDefinition?.sourceParts?.htmlFragment === 'string' ? frameworkDefinition?.sourceParts?.htmlFragment : frameworkDefinition?.sourceParts?.htmlFragment(this.story!.args) : frameworkDefinition?.load ? frameworkDefinition.load(this.story!.args, frameworkDefinition) : this.sourceFallbacks.find((sf) => sf.frameworks.includes(source))?.allowRenderFromResult ? getSourceFromLit(this.story!.render!(this.story!.args)) : ''; const esmVersion = version && version !== 'latest' && version !== 'local' ? !['alpha', 'beta', 'next'].includes(version) ? `${version}-esm` : `esm-${version}` : 'esm-alpha'; let html = ''; let css = ''; for (let index = 0; index < document.styleSheets.length; index++) { const sheet = document.styleSheets[index]; try { if (sheet.cssRules) { for (let idx = 0; idx < sheet.cssRules.length; idx++) { const rule = sheet.cssRules[idx]; css += ` ${rule.cssText}`; } } } catch (error) { continue; } } const themeOption = document.documentElement.getAttribute('theme'); let js = ''; let js_pre = 'none'; let js_externals = ''; switch (true) { case source === 'Vue' /*&& Boolean(frameworkDefinition?.codePenData?.htmlFragment)*/ && !frameworkDefinition?.sourceParts?.fallbackFramework: html = `
${(frameworkDefinition?.sourceParts?.htmlFragment ? typeof frameworkDefinition.sourceParts.htmlFragment === 'function' ? frameworkDefinition.sourceParts.htmlFragment(this.story!.args) : frameworkDefinition.sourceParts.htmlFragment : sourceCode ) // Cater for module imports in