/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable no-prototype-builtins */ import { html as langHtml } from '@codemirror/lang-html'; import { githubDark as codeThemeDark } from '@ddietr/codemirror-themes/github-dark.js'; import { githubLight as codeTheme } from '@ddietr/codemirror-themes/github-light.js'; import { Package, CustomElement } from 'custom-elements-manifest/schema'; import { css, html, nothing, PropertyValueMap, TemplateResult } from 'lit'; import { customElement, property, queryAll, state } from 'lit/decorators.js'; import { live } from 'lit/directives/live.js'; import OmniElement from '../core/OmniElement.js'; import { TextField } from '../text-field/TextField.js'; import { CodeEditor, CodeMirrorSourceUpdateEvent } from './CodeEditor.js'; import { ifNotEmpty } from './Directives.js'; import { loadCustomElementsModuleFor, loadCustomElements, loadCustomElementsCodeMirrorCompletionsRemote, ComponentStoryFormat } from './StoryUtils.js'; import '../label/Label.js'; import '../text-field/TextField.js'; import '../icons/Loading.icon.js'; import '../switch/Switch.js'; import './CodeEditor.js'; /** * @ignore */ @customElement('live-property-editor') export class LivePropertyEditor extends OmniElement { // eslint-disable-next-line @typescript-eslint/no-explicit-any @property({ type: Object, reflect: false }) data?: ComponentStoryFormat; @property({ type: String, reflect: true }) element?: string; @property({ type: Boolean, reflect: true }) disabled!: boolean; @property({ type: String, attribute: 'ignore-attributes', reflect: true }) ignoreAttributes?: string; @property({ type: String, attribute: 'custom-elements', reflect: true }) customElementsPath: string = './custom-elements.json'; @state() customElements?: Package; @queryAll('.slot-code') slotCodeEditors?: NodeListOf; private _firstRenderCompleted!: boolean; private theme?: string; override async connectedCallback() { super.connectedCallback(); this.customElements = await loadCustomElements(this.customElementsPath); 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(); }); } }); this.theme = getComputedStyle(document.documentElement).getPropertyValue('--code-editor-theme')?.trim(); } static override get styles() { return [ super.styles, css` :host { } :host([disabled]) { pointer-events: none; background-color: #f9f9f9; } .loading { max-width: 25px; max-height: 100px; } .css-prop { margin: 5px; } .collapsible { background-color: #777; color: white; cursor: pointer; padding: 18px; width: 100%; border: none; text-align: left; outline: none; font-size: 15px; } .active, .collapsible:hover { background-color: #555; } .expandable { padding: 0 18px; display: none; overflow: hidden; background-color: #f1f1f1; flex-direction: column; } .tooltip { position: relative; display: inline-block; border-bottom: 1px dotted black; } .tooltip .tooltiptext { visibility: hidden; width: 120px; background-color: black; color: #fff; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1; bottom: 150%; left: 50%; margin-left: -60px; } .tooltip .tooltiptext::after { content: ''; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: black transparent transparent transparent; } .tooltip:hover .tooltiptext { visibility: visible; } .docs-text-field { width: 100%; /* --omni-form-border-color: var(--docs-border-color); --omni-theme-border-width: 10px; --omni-form-hover-color: transparent; --omni-form-focussed-border-width: 1px; --omni-form-focussed-border-color: var(--docs-border-color); */ /*border: 1px solid var(--docs-border-color);*/ } .docs-select { /* padding: 5px; cursor: pointer; border-radius: 6px; border: 1px solid var(--docs-border-color); display: flex; min-width: 191px; min-height: 41px; width: 100%; background-color: var(--omni-theme-background-color, inherit); color: var(--omni-theme-font-color, inherit);*/ } .docs-select:focus-visible { outline: none; } .live-header { margin-top: 15px; padding: 6px 0; font-weight: 600; font-size: 16px; } .live-header:first-of-type { margin-top: 0; } ` ]; } public resetSlots() { if (this.slotCodeEditors) { this.slotCodeEditors.forEach(async (codeEditor) => { const slotName = codeEditor.getAttribute('data-slot-name'); if (slotName) { const newCode = this.data && this.data.args![slotName] ? this.data.args![slotName] : undefined; await codeEditor.refresh(() => newCode); } }); } } protected override render() { if (!this.customElements) { return html``; } const module = loadCustomElementsModuleFor(this.element as string, this.customElements); const attributes: { html: TemplateResult; name: string }[] = []; const slots: { html: TemplateResult; name: string }[] = []; module?.declarations?.forEach((d) => { const declaration = d as unknown as CustomElement & { cssCategory: string }; if (declaration.slots) { declaration.slots.forEach((slot) => { if (slots.find((s) => s.name === slot.name) || (this.data && !Object.prototype.hasOwnProperty.call(this.data.args, slot.name))) return; slots.push({ name: slot.name, html: html` ` }); }); } if (declaration.attributes) { declaration.attributes.forEach((attribute) => { if ( (this.ignoreAttributes && this.ignoreAttributes.split(',').includes(attribute.name)) || attributes.find((a) => a.name === attribute.name) ) return; let attributeEditor: TemplateResult = undefined as never; try { if (attribute?.type?.text?.replace('| undefined', '')?.trim() === 'boolean') { attributeEditor = html` `; } else if ( attribute.type?.text?.replace('| undefined', '')?.trim() !== 'object' && attribute.type?.text?.replace('| undefined', '')?.trim() !== 'string' && attribute.type?.text?.replace('| undefined', '')?.trim() !== 'boolean' && !attribute.type?.text?.replace('| undefined', '')?.trim().includes('Promise') && attribute.type?.text?.replace('| undefined', '')?.trim().includes("'") ) { const typesRaw = attribute.type?.text?.replace('| undefined', '').split(' | '); const types = []; for (const type in typesRaw) { const typeValue = typesRaw[type] .replaceAll('| ', '') .replace(/(\r\n|\n|\r)/gm, '') .replaceAll(' ', ''); types.push(typeValue.substring(1, typeValue.length - 1)); } const startValue = this.data ? this.data.args![attribute.name] ?? this.data.args![attribute.fieldName ?? attribute.name] : undefined; attributeEditor = html` `; } else if ( attribute.type?.text?.replace('| undefined', '')?.trim() === 'object' || attribute.type?.text?.replace('| undefined', '')?.trim().includes('Promise') || (this.data?.args && this.data.args[attribute.name] && (typeof this.data.args[attribute.name] === 'function' || typeof this.data.args[attribute.name].then === 'function')) ) { return; } else { const val = this.data ? this.data.args![attribute.name] ?? this.data.args![attribute.fieldName ?? attribute.name] ?? '' : ''; let boundValue = ''; if (typeof val === 'string') { boundValue = val; } else { boundValue = JSON.stringify(val); } attributeEditor = html` `; } } catch (error) { console.error(error); return; } if (attributeEditor) { attributes.push({ html: html` ${attributeEditor} `, name: attribute.name }); } }); } }); if (attributes.length === 0 && slots.length === 0) { return nothing; } return html`
${attributes.map((a) => a.html)} ${slots.map((s) => s.html)}
`; } private _propertyChanged(propertyChangeDetail: PropertyChangeEvent) { this.dispatchEvent( new CustomEvent('property-change', { detail: propertyChangeDetail }) ); } private _currentCodeTheme() { if (this.theme?.toLowerCase() === 'dark') { return codeThemeDark; } return codeTheme; } protected override async updated(_changedProperties: PropertyValueMap | Map): Promise { if (_changedProperties.has('disabled') && this.slotCodeEditors) { this.resetSlots(); } if (_changedProperties.has('data') && _changedProperties.get('data')) { if (!this._firstRenderCompleted) { // console.log('await updateComplete'); await this.updateComplete; // console.log('awaited updateComplete'); this.dispatchEvent( new CustomEvent('component-render-complete', { bubbles: true }) ); this._firstRenderCompleted = true; } } } } export type PropertyChangeEvent = { property: string; newValue: string | number | boolean; oldValue: string | number | boolean }; declare global { interface HTMLElementTagNameMap { 'live-property-editor': LivePropertyEditor; } }