import { createImplementationRegister, createLookup, emptyObject, IServiceLocator, isString, resolve } from '@aurelia/kernel'; import { getObserverLookup, IDirtyChecker, INodeObserverLocator, IObserverLocator, PropertyAccessor, SetterObserver, } from '@aurelia/runtime'; import { IPlatform } from '../platform'; import { AttributeNSAccessor } from './attribute-ns-accessor'; import { CheckedObserver } from './checked-observer'; import { ClassAttributeAccessor } from './class-attribute-accessor'; import { attrAccessor } from './data-attribute-accessor'; import { SelectValueObserver } from './select-value-observer'; import { StyleAttributeAccessor } from './style-attribute-accessor'; import { ISVGAnalyzer } from './svg-analyzer'; import { ValueAttributeObserver } from './value-attribute-observer'; import { atLayout, atNode, isDataAttribute, objectAssign } from '../utilities'; import type { IIndexable } from '@aurelia/kernel'; import type { AccessorType, IAccessor, IObserver, ICollectionObserver, CollectionKind } from '@aurelia/runtime'; import type { INode } from '../dom.node'; import { createMappedError, ErrorNames } from '../errors'; const nsAttributes = (() => { // https://infra.spec.whatwg.org/#namespaces // const htmlNS = 'http://www.w3.org/1999/xhtml'; // const mathmlNS = 'http://www.w3.org/1998/Math/MathML'; // const svgNS = 'http://www.w3.org/2000/svg'; const xlinkNS = 'http://www.w3.org/1999/xlink'; const xmlNS = 'http://www.w3.org/XML/1998/namespace'; const xmlnsNS = 'http://www.w3.org/2000/xmlns/'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 return objectAssign( createLookup<[string, string]>(), { 'xlink:actuate': ['actuate', xlinkNS], 'xlink:arcrole': ['arcrole', xlinkNS], 'xlink:href': ['href', xlinkNS], 'xlink:role': ['role', xlinkNS], 'xlink:show': ['show', xlinkNS], 'xlink:title': ['title', xlinkNS], 'xlink:type': ['type', xlinkNS], 'xml:lang': ['lang', xmlNS], 'xml:space': ['space', xmlNS], 'xmlns': ['xmlns', xmlnsNS], 'xmlns:xlink': ['xlink', xmlnsNS], }, ); })(); const elementPropertyAccessor = new PropertyAccessor(); elementPropertyAccessor.type = (atNode | atLayout) as AccessorType; export interface INodeObserverConfigBase { /** * Indicates the list of events can be used to observe a particular property */ readonly events: string[]; /** * Indicates whether this property is readonly, so observer wont attempt to assign value * example: input.files */ readonly readonly?: boolean; /** * A default value to assign to the corresponding property if the incoming value is null/undefined */ readonly default?: unknown; } export interface INodeObserver extends IObserver { /** * Instruct this node observer event observation behavior */ useConfig(config: INodeObserverConfigBase): void; } export type INodeObserverConstructor = new ( el: INode, key: PropertyKey, config: INodeObserverConfig, observerLocator: IObserverLocator, locator: IServiceLocator, ) => INodeObserver; export interface INodeObserverConfig { /** * The observer constructor to use */ readonly type?: INodeObserverConstructor; /** * Indicates the list of events can be used to observe a particular property */ readonly events: string[]; /** * Indicates whether this property is readonly, so observer wont attempt to assign value * example: input.files */ readonly readonly?: boolean; /** * A default value to assign to the corresponding property if the incoming value is null/undefined */ readonly default?: unknown; } export class NodeObserverLocator implements INodeObserverLocator { public static register = /*@__PURE__*/ createImplementationRegister(INodeObserverLocator); /** * Indicates whether the node observer will be allowed to use dirty checking for a property it doesn't know how to observe */ public allowDirtyCheck: boolean = true; /** @internal */ private readonly _events: Record> = createLookup(); /** @internal */ private readonly _globalEvents: Record = createLookup(); /** @internal */ private readonly _overrides: Record> = createLookup(); /** @internal */ private readonly _globalOverrides: Record = createLookup(); /** @internal */ private readonly _locator = resolve(IServiceLocator); /** @internal */ private readonly _platform = resolve(IPlatform); /** @internal */ private readonly _dirtyChecker = resolve(IDirtyChecker); /** @internal */ private readonly svg = resolve(ISVGAnalyzer); public constructor() { // todo: atm, platform is required to be resolved too eagerly for the `.handles()` check // also a lot of tests assume default availability of observation // those 2 assumptions make it not the right time to extract the following line into a // default configuration for NodeObserverLocator yet // but in the future, they should be, so apps that don't use check box/select, or implement a different // observer don't have to pay the of the default implementation const inputEvents = ['change', 'input']; const inputEventsConfig: INodeObserverConfig = { events: inputEvents, default: '' }; this.useConfig({ INPUT: { value: inputEventsConfig, valueAsNumber: { events: inputEvents, default: 0 }, checked: { type: CheckedObserver, events: inputEvents } , files: { events: inputEvents, readonly: true }, }, SELECT: { value: { type: SelectValueObserver, events: ['change'], default: '' }, }, TEXTAREA: { value: inputEventsConfig, }, }); const contentEventsConfig: INodeObserverConfig = { events: ['change', 'input', 'blur', 'keyup', 'paste'], default: '' }; const scrollEventsConfig: INodeObserverConfig = { events: ['scroll'], default: 0 }; this.useConfigGlobal({ scrollTop: scrollEventsConfig, scrollLeft: scrollEventsConfig, textContent: contentEventsConfig, innerHTML: contentEventsConfig, }); this.overrideAccessorGlobal('css', 'style', 'class'); this.overrideAccessor({ INPUT: ['value', 'checked', 'model'], SELECT: ['value'], TEXTAREA: ['value'], }); } // deepscan-disable-next-line public handles(obj: unknown, _key: PropertyKey): boolean { return obj instanceof this._platform.Node; } public useConfig(config: Record>): void; public useConfig(nodeName: string, key: PropertyKey, events: INodeObserverConfig): void; public useConfig(nodeNameOrConfig: string | Record>, key?: PropertyKey, eventsConfig?: INodeObserverConfig): void { const lookup = this._events; let existingMapping: Record; if (isString(nodeNameOrConfig)) { existingMapping = lookup[nodeNameOrConfig] ??= createLookup(); if (existingMapping[key as string] == null) { existingMapping[key as string] = eventsConfig!; } else { throwMappingExisted(nodeNameOrConfig, key!); } } else { for (const nodeName in nodeNameOrConfig) { existingMapping = lookup[nodeName] ??= createLookup(); const newMapping = nodeNameOrConfig[nodeName]; for (key in newMapping) { if (existingMapping[key] == null) { existingMapping[key] = newMapping[key]; } else { throwMappingExisted(nodeName, key); } } } } } public useConfigGlobal(config: Record): void; public useConfigGlobal(key: PropertyKey, events: INodeObserverConfig): void; public useConfigGlobal(configOrKey: PropertyKey | Record, eventsConfig?: INodeObserverConfig): void { const lookup = this._globalEvents; if (typeof configOrKey === 'object') { for (const key in configOrKey) { if (lookup[key] == null) { lookup[key] = configOrKey[key]; } else { throwMappingExisted('*', key); } } } else { if (lookup[configOrKey as string] == null) { lookup[configOrKey as string] = eventsConfig!; } else { throwMappingExisted('*', configOrKey); } } } // deepscan-disable-nextline public getAccessor(obj: HTMLElement, key: PropertyKey, requestor: IObserverLocator): IAccessor | IObserver { if (key in this._globalOverrides || (key in (this._overrides[obj.tagName] ?? emptyObject))) { return this.getObserver(obj, key, requestor); } switch (key) { // class / style / css attribute will be observed using .getObserver() per overrides // // TODO: there are (many) more situation where we want to default to DataAttributeAccessor case 'src': case 'href': case 'role': case 'minLength': case 'maxLength': case 'placeholder': case 'size': case 'pattern': case 'title': case 'popovertarget': case 'popovertargetaction': /* istanbul-ignore-next */ if (__DEV__) { if ((key === 'popovertarget' || key === 'popovertargetaction') && obj.nodeName !== 'INPUT' && obj.nodeName !== 'BUTTON') { // eslint-disable-next-line no-console console.warn(`[DEV:aurelia] Popover API are only valid on or