UNPKG

@reallyland/really-elements

Version:

A collection of opinionated custom elements for the web

333 lines (328 loc) 12.1 kB
import { __decorate } from "tslib"; import '@material/mwc-button'; import { highlight, languages } from '@reallyland/esm'; import { html, LitElement, nothing } from 'lit'; import { property } from 'lit/decorators.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { parts } from './constants.js'; import { contentCopied, contentCopy } from './icons.js'; import { codeConfigurationStyles, prismVscodeStyles } from './styles.js'; function toFunctionType(type) { switch (type) { case 'boolean': return Boolean; case 'number': return Number; case 'string': default: return String; } } function toInputType(type) { switch (type) { case 'boolean': return 'checkbox'; case 'number': return 'number'; case 'string': default: return 'text'; } } function toPropertiesAttr(properties) { const mapped = properties .reduce((p, n) => { const { name, type = 'string', value } = n; const fnType = toFunctionType(type); const val = fnType(value); if ((type === 'string' && !val) || (type === 'boolean' && !val)) return p; const attrName = name.toLowerCase(); p.push(type === 'boolean' ? attrName : `${attrName}="${String(val)}"`); return p; }, []) .filter(Boolean) .join('\n '); return mapped ? `\n ${mapped}\n` : ''; } function toCSSProperties(cssProperties) { return cssProperties .reduce((p, n) => { const { name, value } = n; if (!value) return p; p.push(` ${name}: ${value};\n`); return p; }, []) .join(''); } function renderCode(code, grammar, language) { return unsafeHTML(highlight(code, languages[grammar], language)); } export class CodeConfigurator extends LitElement { constructor() { super(...arguments); this._propsCopied = false; this._cssPropsCopied = false; this._cssProperties = []; this.copiedDuration = 3e3; this._properties = []; } static { this.styles = [codeConfigurationStyles, prismVscodeStyles]; } get cssProperties() { return this._cssProperties; } set cssProperties(properties) { this._cssProperties = Array.isArray(properties) ? properties : this._cssProperties; this.requestUpdate('cssProperties'); } get properties() { return this._properties; } set properties(properties) { this._properties = Array.isArray(properties) ? properties : this._properties; this.requestUpdate('properties'); } get _slot() { return this.shadowRoot?.querySelector('slot'); } updated() { if (this.customElement) { const slottedElements = this._slottedElements; if (slottedElements) { const properties = this._properties; const cssProperties = this._cssProperties; properties.forEach((n) => { slottedElements.forEach((o) => { Object.assign(o, { [n.name]: n.value, }); }); }); cssProperties.forEach((n) => { slottedElements.forEach((o) => { o.style.setProperty(n.name, n.value); }); }); } else void this._updateSlotted(); } } render() { const elName = this.customElement; const properties = this._properties; const cssProperties = this._cssProperties; return html ` <div part=${parts.slot}> <slot @slotchange=${this._updateSlotted} class=slot ></slot> </div> <div part=${parts.content}>${elName ? this._renderProperties(elName, properties, cssProperties) : nothing}</div> `; } async _updateSlotted() { const slotted = this._slot; const customElementName = this.customElement; if (slotted && typeof customElementName === 'string' && customElementName.length > 0) { const assignedNodes = Array.from(slotted.assignedNodes()).filter((n) => n.nodeType === Node.ELEMENT_NODE); const matchedCustomElements = assignedNodes.reduce((p, n) => { if (n.localName === customElementName) { p.push(n); } else if (n?.querySelectorAll) { const allCustomElements = Array.from(n.querySelectorAll(customElementName)); p.push(...allCustomElements); } return p; }, []); const hasMatchedCustomElements = matchedCustomElements.length > 0; this._slottedElements = hasMatchedCustomElements ? matchedCustomElements : []; if (hasMatchedCustomElements) { const elementsUpdateComplete = matchedCustomElements.map((n) => n?.updateComplete?.then(() => n?.requestUpdate())); await Promise.all(elementsUpdateComplete); } this.requestUpdate(); } } _renderProperties(elName, properties, cssProperties) { const propsContent = toPropertiesAttr(properties); const cssPropsContent = toCSSProperties(cssProperties); const idPrefix = Math.random().toString(32).slice(-7); const cssPropertiesId = `cssPropertiesFor${idPrefix}`; const propertiesId = `propertiesFor${idPrefix}`; return html ` <div class=all-properties-container part=${parts.allPropertiesConfigurator}> ${propsContent ? html `<section part=${parts.propertiesConfigurator}> <h2 class=properties>Properties</h2> <div class=configurators part=${parts.configurators}>${this._renderPropertiesConfigurator(properties)}</div> </section>` : nothing} ${cssPropsContent ? html `<section part=${parts.cssPropertiesConfigurator}> <h2 class=css-properties>CSS Properties</h2> <div class=configurators part=${parts.configurators}>${this._renderPropertiesConfigurator(cssProperties, true)}</div> </section>` : nothing} </div> <div class=all-code-snippets-container part=${parts.allCodeSnippets}> ${propsContent && cssPropsContent ? html `<h2>Code snippet</h2>` : nothing} ${propsContent ? html `<section part=${parts.propertiesCodeSnippet}> <h3 class=properties>Properties</h3> <div class=code-container> <mwc-button class=copy-btn for=${propertiesId} aria-label="Copy properties" @click=${this._copyCode}> ${this._propsCopied ? contentCopied : contentCopy} <span class=copy-text>${this._propsCopied ? 'Copied' : 'Copy'}</span> </mwc-button> <pre class=language-html id=${propertiesId}>${renderCode(`<${elName}${propsContent}></${elName}>`, 'html', 'html')}</pre> </div> </section>` : nothing} ${cssPropsContent ? html `<section part=${parts.cssPropertiesCodeSnippet}> <h3 class=css-properties>CSS Properties</h3> <div class=code-container> <mwc-button class=copy-btn for=${cssPropertiesId} aria-label="Copy CSS properties" @click=${this._copyCode}> ${this._cssPropsCopied ? contentCopied : contentCopy} <span class=copy-text>${this._cssPropsCopied ? 'Copied' : 'Copy'}</span> </mwc-button> <pre class=language-css id=${cssPropertiesId}>${renderCode(`${elName} {\n${cssPropsContent}}`, 'css', 'css')}</pre> </div> </section>` : nothing} </div> `; } _renderPropertiesConfigurator(properties, isCSS = false) { const content = properties.map((n) => { const { name, options, type, value } = n; const valueStr = value; const elementId = `${options ? 'select' : 'input'}_${type}_${name}`; const element = options ? html `<select .value=${valueStr} @input=${(ev) => this._updateProps(ev, isCSS)} id=${elementId} name=${name} part=${parts.select} >${options.map((o) => html `<option value="${o.value}" ?selected="${o.value === value}">${o.label}</option>`)}</select>` : html `<input ?checked=${type === 'boolean' && Boolean(valueStr)} @input=${(ev) => this._updateProps(ev, isCSS)} id=${elementId} name=${name} part=${parts.input} type=${toInputType(type)} value=${valueStr} />`; return html `<div class=configurator> <label for=${elementId}>${name}</label> ${element} </div>`; }); return content; } _updateProps(ev, isCSS) { const currentTarget = ev.currentTarget; const propertyName = currentTarget.getAttribute('name'); const properties = isCSS ? this._cssProperties : this._properties; const val = currentTarget.tagName === 'INPUT' && currentTarget.type === 'checkbox' ? currentTarget.checked : currentTarget.value; const updatedProperties = properties.map((n) => { if (n.name === propertyName) { return { ...n, value: toFunctionType(n.type)(val) }; } return n; }); const propName = isCSS ? 'cssProperties' : 'properties'; this[propName] = updatedProperties; this.requestUpdate(propName); this.dispatchEvent(new CustomEvent('property-changed', { bubbles: true, detail: { eventFrom: ev.currentTarget, isCSS, propertyName, propertyValue: toFunctionType(properties.find((n) => n.name === propertyName)?.type)(val), }, composed: true, })); } _copyCode(ev) { const currentTarget = ev.currentTarget; const attrFor = currentTarget.getAttribute('for'); const copiedProp = attrFor?.startsWith('propertiesFor') ? '_propsCopied' : '_cssPropsCopied'; if (this[copiedProp]) return; const copyNode = this.shadowRoot?.querySelector(`#${attrFor}`); const selection = getSelection(); const range = document.createRange(); selection?.removeAllRanges(); range.selectNodeContents(copyNode); selection?.addRange(range); document.execCommand('copy'); selection?.removeAllRanges(); this.dispatchEvent(new CustomEvent('content-copied')); this[copiedProp] = true; window.setTimeout(() => { this[copiedProp] = false; }, this.copiedDuration); } } __decorate([ property({ type: Array, converter: { fromAttribute(value) { try { return JSON.parse(value); } catch { return []; } }, }, }) ], CodeConfigurator.prototype, "cssProperties", null); __decorate([ property({ type: Array, converter: { fromAttribute(value) { try { return JSON.parse(value); } catch { return []; } }, }, }) ], CodeConfigurator.prototype, "properties", null); __decorate([ property({ type: String }) ], CodeConfigurator.prototype, "customElement", void 0); __decorate([ property({ type: Boolean }) ], CodeConfigurator.prototype, "_propsCopied", void 0); __decorate([ property({ type: Boolean }) ], CodeConfigurator.prototype, "_cssPropsCopied", void 0); //# sourceMappingURL=code-configurator.js.map