import { firstDefined, getResourceKeyFor, mergeArrays, resource, resourceBaseName, ResourceType, isFunction, isString } from '@aurelia/kernel'; import { type Scope } from '@aurelia/runtime'; import { objectFreeze } from '../utilities'; import { aliasRegistration, singletonRegistration } from '../utilities-di'; import { defineMetadata, getAnnotationKeyFor, getMetadata, hasMetadata } from '../utilities-metadata'; import type { Constructable, IContainer, IServiceLocator, PartialResourceDefinition, ResourceDefinition, StaticResourceType } from '@aurelia/kernel'; import { createMappedError, ErrorNames } from '../errors'; import { getDefinitionFromStaticAu, type IResourceKind } from './resources-shared'; import { IBinding } from '../binding/interfaces-bindings'; export type PartialBindingBehaviorDefinition = PartialResourceDefinition; export type BindingBehaviorStaticAuDefinition = PartialBindingBehaviorDefinition & { type: 'binding-behavior'; }; export type BindingBehaviorType = ResourceType; export type BindingBehaviorInstance = { type?: 'instance' | 'factory'; bind?(scope: Scope, binding: IBinding, ...args: unknown[]): void; unbind?(scope: Scope, binding: IBinding, ...args: unknown[]): void; } & T; export type BindingBehaviorKind = IResourceKind & { isType(value: T): value is (T extends Constructable ? BindingBehaviorType : never); define(name: string, Type: T): BindingBehaviorType; define(def: PartialBindingBehaviorDefinition, Type: T): BindingBehaviorType; define(nameOrDef: string | PartialBindingBehaviorDefinition, Type: T): BindingBehaviorType; getDefinition(Type: T): BindingBehaviorDefinition; find(container: IContainer, name: string): BindingBehaviorDefinition | null; get(container: IServiceLocator, name: string): BindingBehaviorInstance; }; export type BindingBehaviorDecorator = (Type: T, context: ClassDecoratorContext) => BindingBehaviorType; export function bindingBehavior(definition: PartialBindingBehaviorDefinition): BindingBehaviorDecorator; export function bindingBehavior(name: string): BindingBehaviorDecorator; export function bindingBehavior(nameOrDef: string | PartialBindingBehaviorDefinition): BindingBehaviorDecorator; export function bindingBehavior(nameOrDef: string | PartialBindingBehaviorDefinition): BindingBehaviorDecorator { return function (target: T, context: ClassDecoratorContext): BindingBehaviorType { context.addInitializer(function (this) { BindingBehavior.define(nameOrDef, this as Constructable); }); return target as BindingBehaviorType; }; } export class BindingBehaviorDefinition implements ResourceDefinition { private constructor( public readonly Type: BindingBehaviorType, public readonly name: string, public readonly aliases: readonly string[], public readonly key: string, ) {} public static create( nameOrDef: string | PartialBindingBehaviorDefinition, Type: BindingBehaviorType, ): BindingBehaviorDefinition { let name: string; let def: PartialBindingBehaviorDefinition; if (isString(nameOrDef)) { name = nameOrDef; def = { name }; } else { name = nameOrDef.name; def = nameOrDef; } return new BindingBehaviorDefinition( Type, firstDefined(getBehaviorAnnotation(Type, 'name'), name), mergeArrays(getBehaviorAnnotation(Type, 'aliases'), def.aliases, Type.aliases), BindingBehavior.keyFrom(name), ); } public register(container: IContainer, aliasName?: string | undefined): void { const $Type = this.Type; const key = typeof aliasName === 'string' ? getBindingBehaviorKeyFrom(aliasName) : this.key; const aliases = this.aliases; if (!container.has(key, false)) { container.register( container.has($Type, false) ? null : singletonRegistration($Type, $Type), aliasRegistration($Type, key), ...aliases.map(alias => aliasRegistration($Type, getBindingBehaviorKeyFrom(alias))), ); } /* istanbul ignore next */ else if (__DEV__) { // eslint-disable-next-line no-console console.warn(`[DEV:aurelia] ${createMappedError(ErrorNames.binding_behavior_existed, this.name)}`); } } } /** @internal */ export const behaviorTypeName = 'binding-behavior'; const bbBaseName = /*@__PURE__*/getResourceKeyFor(behaviorTypeName); const getBehaviorAnnotation = ( Type: Constructable, prop: K, ): PartialBindingBehaviorDefinition[K] | undefined => getMetadata(getAnnotationKeyFor(prop), Type); const getBindingBehaviorKeyFrom = (name: string): string => `${bbBaseName}:${name}`; export const BindingBehavior = /*@__PURE__*/ objectFreeze({ name: bbBaseName, keyFrom: getBindingBehaviorKeyFrom, isType(value: T): value is (T extends Constructable ? BindingBehaviorType : never) { return isFunction(value) && (hasMetadata(bbBaseName, value) || (value as StaticResourceType).$au?.type === behaviorTypeName); }, define>(nameOrDef: string | PartialBindingBehaviorDefinition, Type: T): BindingBehaviorType { const definition = BindingBehaviorDefinition.create(nameOrDef, Type as Constructable); const $Type = definition.Type as BindingBehaviorType; // registration of resource name is a requirement for the resource system in kernel (module-loader) defineMetadata(definition, $Type, bbBaseName, resourceBaseName); return $Type; }, getDefinition(Type: T): BindingBehaviorDefinition { const def: BindingBehaviorDefinition = getMetadata>(bbBaseName, Type) ?? getDefinitionFromStaticAu(Type as BindingBehaviorType, behaviorTypeName, BindingBehaviorDefinition.create); if (def === void 0) { throw createMappedError(ErrorNames.binding_behavior_def_not_found, Type); } return def; }, find(container, name) { const Type = container.find(behaviorTypeName, name); return Type == null ? null : getMetadata(bbBaseName, Type) ?? getDefinitionFromStaticAu(Type, behaviorTypeName, BindingBehaviorDefinition.create) ?? null; }, get(container, name) { if (__DEV__) { try { return container.get(resource(getBindingBehaviorKeyFrom(name))); } catch (ex) { // eslint-disable-next-line no-console console.error('[DEV:aurelia] Cannot retrieve binding behavior with name', name); throw ex; } } return container.get(resource(getBindingBehaviorKeyFrom(name))); }, });