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}
`;
}
if (this.type === 'font') {
const triggerText = current?.label || this.label || 'Font';
return html`
${triggerText}
`;
}
const triggerText = current?.label || this.label || 'Select';
return html`
${triggerText}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'nile-rte-select': NileRteSelect;
}
}