/* eslint-disable @typescript-eslint/unified-signatures */ import { PluginContainer } from '@arcmantle/injector'; import type { Interface, Writeable } from '@arcmantle/library/types'; import { type Signal, signal } from '@preact/signals-core'; import type { AdapterBase, AdapterElement } from './adapter-element.ts'; import type { AdapterMetadata, PropertyType } from './types.ts'; export interface ClassDecorator { // legacy decorator signature any>( target: Class, ): Class | void; // decorator signature any>( target: Class, context: ClassDecoratorContext>, ): Class | void; } export interface PropertyDecorator { // accessor decorator signature , V>( target: ClassAccessorDecoratorTarget, context: ClassAccessorDecoratorContext ): ClassAccessorDecoratorResult; // setter decorator signature , V>( target: (value: V) => void, context: ClassSetterDecoratorContext ): (this: C, value: V) => void; // legacy decorator signature ( protoOrDescriptor: AdapterElement, name: PropertyKey, descriptor?: PropertyDescriptor ): any; } // Ensure metadata is enabled. TypeScript does not polyfill // Symbol.metadata, so we must ensure that it exists. (Symbol as { metadata: symbol; }).metadata ??= Symbol('metadata'); export const customElement = ( tagName: string, ): ClassDecorator => void; }>( base: C, ): void => { const writeable = (base as Writeable); if (writeable.tagName) return; writeable.tagName = tagName; queueMicrotask(() => base.register()); }; const createStateMetadata = ( metadata: AdapterMetadata, propName: string, ) => { metadata.signalProps ??= []; if (!metadata.signalProps.includes(propName)) metadata.signalProps.push(propName); }; const legacyState = ( ) => ( target: object, property: string, ): any => { const hasOwnProperty = target.hasOwnProperty(property); const metadata = ((target.constructor as any)[Symbol.metadata] ??= {}) as AdapterMetadata; const propName = property; createStateMetadata(metadata, propName); const descriptor: PropertyDescriptor = { get() { const me = this as Record; const sig = me['__' + propName] ??= signal(me['__' + propName]); return sig.value; }, set(v: any) { const me = this as Record; const sig = me['__' + propName] ??= signal(me['__' + propName]); sig.value = v; }, }; Object.defineProperty(target, property, descriptor); return hasOwnProperty ? Object.getOwnPropertyDescriptor(target, propName) : undefined; }; const standardState = ( ) => , V>( target: ClassAccessorDecoratorTarget, context: ClassAccessorDecoratorContext, ): ClassAccessorDecoratorResult => { const { get } = target; const metadata = context.metadata as any as AdapterMetadata; const propName = context.name.toString(); createStateMetadata(metadata, propName); return { get() { const sig = (get.call(this) as Signal); return sig.value; }, set(value: V) { const sig = (get.call(this) as Signal); sig.value = value; }, init(value: any): any { return signal(value); }, }; }; export const state = ( ): PropertyDecorator => , V>( protoOrTarget: | object | ClassAccessorDecoratorTarget | ((value: V) => void), nameOrContext: | PropertyKey | ClassAccessorDecoratorContext | ClassSetterDecoratorContext, ): any => { if (typeof nameOrContext === 'string') return legacyState()(protoOrTarget, nameOrContext); return standardState()( protoOrTarget as ClassAccessorDecoratorTarget, nameOrContext as ClassAccessorDecoratorContext, ); }; const createPropertyMetadata = ( type: PropertyType = String, options: { reflect?: boolean; }, metadata: AdapterMetadata, propName: string, ): string => { metadata.signalProps ??= []; if (!metadata.signalProps.includes(propName)) metadata.signalProps.push(propName); metadata.observedAttributes ??= []; const attrName = propName //.replace(/([A-Z])/g, '-$1') .toLowerCase(); if (!metadata.observedAttributes.includes(attrName)) { metadata.observedAttributes.push(attrName); metadata.propertyMetadata ??= {}; metadata.propertyMetadata[attrName] = { propName, type, reflect: options?.reflect ?? false, }; } return attrName; }; const legacyProperty = ( type: PropertyType = String, options: { reflect?: boolean; } = {}, ) => ( target: object, property: string, ): any => { const hasOwnProperty = target.hasOwnProperty(property); const metadata = ((target.constructor as any)[Symbol.metadata] ??= {}) as AdapterMetadata; const propName = property; const attrName = createPropertyMetadata(type, options, metadata, propName); const descriptor: PropertyDescriptor = { get() { const me = this as Record; const sig = me['__' + propName] ??= signal(me['__' + propName]); return sig.value; }, set(v: any) { const me = this as Record; const sig = me['__' + propName] ??= signal(me['__' + propName]); sig.value = v; if (metadata.propertyMetadata[attrName]?.reflect) { (this as any as { __element?: WeakRef; }) .__element ?.deref() ?.setAttribute(propName, String(v)); } }, }; Object.defineProperty(target, property, descriptor); return hasOwnProperty ? Object.getOwnPropertyDescriptor(target, propName) : undefined; }; const standardProperty = ( type: PropertyType = String, options: { reflect?: boolean; } = {}, ) => , V>( target: ClassAccessorDecoratorTarget, context: ClassAccessorDecoratorContext, ): ClassAccessorDecoratorResult => { const { get } = target; const metadata = context.metadata as any as AdapterMetadata; const propName = context.name.toString(); const attrName = createPropertyMetadata(type, options, metadata, propName); return { get() { const sig = (get.call(this) as Signal); return sig.value; }, set(value: V) { const sig = (get.call(this) as Signal); sig.value = value; if (metadata.propertyMetadata[attrName]?.reflect) { (this as any as { __element: WeakRef; }) .__element.deref() ?.setAttribute(propName, String(value)); } }, init(value: any): any { return signal(value); }, }; }; export const property = ( type: PropertyType = String, options?: { reflect?: boolean; }, ): PropertyDecorator => , V>( protoOrTarget: | object | ClassAccessorDecoratorTarget | ((value: V) => void), nameOrContext: | PropertyKey | ClassAccessorDecoratorContext | ClassSetterDecoratorContext, ): any => { if (typeof nameOrContext === 'string') return legacyProperty(type, options)(protoOrTarget, nameOrContext); return standardProperty(type, options)( protoOrTarget as ClassAccessorDecoratorTarget, nameOrContext as ClassAccessorDecoratorContext, ); }; const legacyQuery = ( selector: string, cache: boolean = false, ) => ( target: object, property: string, ) => { const hasOwnProperty = target.hasOwnProperty(property); const propName = property; const descriptor: PropertyDescriptor = { get() { const me = (this as Interface & Record); if (cache) { const cached = me['__cached' + propName]; if (cached !== undefined) return cached; } const el = me.query(selector); if (cache) me['__cached' + propName] = el; return el; }, set(v: any) {}, }; Object.defineProperty(target, property, descriptor); return hasOwnProperty ? Object.getOwnPropertyDescriptor(target, propName) : undefined; }; const standardQuery = ( selector: string, cache: boolean = false, ) => , V>( target: ClassAccessorDecoratorTarget, context: ClassAccessorDecoratorContext, ): ClassAccessorDecoratorResult => { const { get, set } = target; return { get() { if (cache) { const cached = get.call(this); if (cached !== undefined) return cached as unknown as V; } const el = this.query(selector) as unknown as V; if (cache) set.call(this, el); return el; }, set(value: V) {}, init(value: any): any {}, }; }; export const query = ( selector: string, ): PropertyDecorator => , V>( protoOrTarget: | object | ClassAccessorDecoratorTarget | ((value: V) => void), nameOrContext: | PropertyKey | ClassAccessorDecoratorContext | ClassSetterDecoratorContext, ): any => { if (typeof nameOrContext === 'string') return legacyQuery(selector)(protoOrTarget, nameOrContext); return standardQuery(selector)( protoOrTarget as ClassAccessorDecoratorTarget, nameOrContext as ClassAccessorDecoratorContext, ); }; export const provider = ( ): ClassDecorator => ( base: C, context?: ClassDecoratorContext>, ): any => { const metadata = context ? (context.metadata as any) as AdapterMetadata : ((base.constructor as any)[Symbol.metadata] ??= {}) as AdapterMetadata; metadata.pluginContainer = new PluginContainer(); return base; };