import { LitElement, html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; type HeadingTag = 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; type GenericOption = { value: string; label?: string; icon?: string }; type HeadingOption = { value: HeadingTag; label?: string; icon?: string }; type NormalizedOption = { value: string; label: string; icon?: string }; const HEADING_ALLOWLIST: ReadonlySet = new Set([ 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', ]); function isHeadingTag(v: string): v is HeadingTag { return HEADING_ALLOWLIST.has(v as HeadingTag); } @customElement('nile-rte-select') export class NileRteSelect extends LitElement { protected createRenderRoot() { return this; } @property({ type: String, attribute: true, reflect: true }) type = ''; @property({ type: String, attribute: true, reflect: true }) options = '[]'; @property({ attribute: false }) optionsObj?: Array; @property({ type: String, attribute: true, reflect: true }) label = ''; @property({ type: Boolean, attribute: true, reflect: true }) disabled = false; @state() private selectedValue = ''; private mapAlignIcon(v: string) { const map: Record = { left: 'format_align_left', center: 'format_align_middle', right: 'format_align_right', justify: 'format_align_justify', }; return map[v] || 'format_align_left'; } private get parsedOptions(): NormalizedOption[] { const source: unknown = this.optionsObj ?? this.options; let rawArray: any[] = (() => { if (Array.isArray(source)) return source; try { return JSON.parse(String(source)); } catch { return []; } })(); if (this.type === 'align' && rawArray.length === 0) { rawArray = [ { value: 'left', label: 'Align Left' }, { value: 'center', label: 'Align Center' }, { value: 'right', label: 'Align Right' }, { value: 'justify', label: 'Justify' }, ]; } let items: NormalizedOption[] = rawArray.map((o: any) => { const value: string = o?.value ?? o; const label: string = o?.label ?? o?.value ?? o; const icon: string | undefined = o?.icon ?? (this.type === 'align' ? this.mapAlignIcon(String(value)) : undefined); return { value, label, icon }; }); if (this.type === 'heading') { const before = items.length; items = items.filter(i => isHeadingTag(i.value)); if (items.length !== before) { } if (this.selectedValue && !isHeadingTag(this.selectedValue)) { this.selectedValue = ''; } } return items; } private ensureDefault() { if (!this.selectedValue) { const first = this.parsedOptions[0]; if (first) this.selectedValue = first.value; } } private onSelect(value: string) { if (this.type === 'heading' && !isHeadingTag(value)) { console.warn( `[nile-rte-select] Ignoring invalid heading value: ${value}` ); return; } this.selectedValue = value; this.dispatchEvent( new CustomEvent('change', { detail: value, bubbles: true, composed: true, }) ); } connectedCallback(): void { super.connectedCallback(); this.injectLocalStyles(); } private injectLocalStyles() { if (this.querySelector('style[data-rte-select-style]')) return; const style = document.createElement('style'); style.setAttribute('data-rte-select-style', 'true'); style.textContent = ` nile-menu.rte-align-menu::part(menu__items-wrapper) { display: flex; } nile-menu.rte-align-menu, nile-menu.rte-default-menu { margin-top: 0px; } nile-button.rte-align-trigger::part(base), nile-button.rte-default-trigger::part(base) { min-width: 32px; height: 32px; padding: 0px 6px; box-shadow: none; } nile-button.rte-align-trigger::part(base) { border: none; } `; this.insertBefore(style, this.firstChild); } render() { const opts = this.parsedOptions; this.ensureDefault(); const current = opts.find(o => o.value === this.selectedValue); if (this.type === 'align') { const trigger = current?.icon ? html`` : this.label || 'Align'; return html` ${trigger} ${opts.map( o => html` this.onSelect(o.value)} > ` )} `; } if (this.type === 'font') { const triggerText = current?.label || this.label || 'Font'; return html` ${triggerText} ${opts.map( o => html` this.onSelect(o.value)} > ${o.label} ` )} `; } const triggerText = current?.label || this.label || 'Select'; return html` ${triggerText} ${opts.map( o => html` this.onSelect(o.value)} > ${o.label} ` )} `; } } declare global { interface HTMLElementTagNameMap { 'nile-rte-select': NileRteSelect; } }