2323//
2424// In this version of the library, behaviours are enabled on a per-document basis.
2525// Call `enableBehaviours(document)` to enable behaviour handling for that document.
26+ // It is not necessary to call `enableBehaviours(document)` for the main document since it will be enabled automatically.
27+ // You can still call it if you want to control when the document behaviours are updated, or you can call `disableBehaviours(document)` if you
28+ // prefer behaviours not to be enabled for the main document.
2629//
27- // You can get a behaviour from an element if you know the type of the behavior:
30+ // You can get a behaviour from an element if you know the type (or base type) of the behavior:
2831//
2932// const behaviour = getBehaviour(element, MyBehaviour);
3033//
@@ -115,7 +118,17 @@ function addMicrotaskProperty(cls, methodName) {
115118 } ) ;
116119}
117120
118- /** @typedef { string } AttributeName */
121+ /**
122+ * Attribute names allow us to know when a behaviour should apply to an element.
123+ * @typedef { string } AttributeName
124+ * */
125+
126+ /**
127+ * A string that cannot be an attribute name (it ends with an equals sign).
128+ * They're used to say that a behaviour will always be available on a relevant element.
129+ * There's still a name here so type inheritance & overriding works appropriately, but the name doesn't have to be used as an attribute.
130+ * @typedef { `${string}=` } NotAttributeName
131+ * */
119132
120133/**
121134 * Base class for attribute behaviours
@@ -147,6 +160,26 @@ export class Behaviour {
147160 disconnected ( ) { }
148161}
149162
163+ /**
164+ * Behaviour keys that do not require a matching element to have any attributes
165+ * @typedef { NotAttributeName | Symbol } UniversalBehaviourKey
166+ * */
167+
168+ /**
169+ * A behaviour key is used to associate a behaviour with an element
170+ * @typedef { AttributeName | UniversalBehaviourKey } BehaviourKey
171+ * */
172+
173+ /** @typedef { Record<BehaviourKey, typeof Behaviour> } BehaviourRecord */
174+
175+ /**
176+ * @param { BehaviourKey } key
177+ * @returns { key is AttributeName }
178+ */
179+ function isAttributeName ( key ) {
180+ return ! ( ( typeof key === "symbol" ) || key . endsWith ( "=" ) ) ;
181+ }
182+
150183const INSTANCES = Symbol ( "behaviour-instances" ) ;
151184
152185/**
@@ -185,6 +218,21 @@ function* allElements(elements) {
185218 }
186219}
187220
221+ /**
222+ * A list of all entries including Symbol-keyed ones
223+ * @param { object } o
224+ * @returns { Iterable<[string | Symbol, any]> }
225+ */
226+ function * allEntries ( o ) {
227+ for ( const e of Object . entries ( o ) ) {
228+ yield e ;
229+ }
230+
231+ for ( const s of Object . getOwnPropertySymbols ( o ) ) {
232+ yield [ s , o [ s ] ] ;
233+ }
234+ }
235+
188236/**
189237 * Connects a behaviour of the specified type to an element
190238 * if the element doesn't already have a behaviour of that exact type.
@@ -241,13 +289,15 @@ export function getBehaviour(element, behaviourType) {
241289 return undefined ;
242290}
243291
292+ /** @typedef { { key: BehaviourKey, elementType: typeof HTMLElement, behaviourType: typeof Behaviour, existingBehaviourType: typeof Behaviour } } Warning */
293+
244294class BehaviourRegistry {
245295
246296 /**
247297 * This contains the behaviours directly applied to a particular element type.
248298 * To get the effective behaviours for a particular element type, we need to
249299 * traverse the class hierarchy (prototype chain) which is handled by `getBehaviourRecord`
250- * @type Map<typeof HTMLElement, Record<AttributeName, typeof Behaviour> >
300+ * @type Map<typeof HTMLElement, BehaviourRecord >
251301 * */
252302 elementTypeToBehaviourRecord = new Map ( ) ;
253303
@@ -328,7 +378,7 @@ class BehaviourRegistry {
328378 * Returns the effective record of behaviours/attributes that have been
329379 * applied to the specified element type and any base classes.
330380 * @param { typeof HTMLElement } elementType
331- * @returns { Record<AttributeName, typeof Behaviour> }
381+ * @returns { BehaviourRecord }
332382 */
333383 getBehaviourRecord ( elementType ) {
334384 const elementTypeToBehaviourRecord = this . elementTypeToBehaviourRecord ;
@@ -348,23 +398,25 @@ class BehaviourRegistry {
348398 * or they may indicate that you have a conflict with two behaviours both trying to use the same attribute names.
349399 * @param { typeof Behaviour } behaviourType
350400 * @param { typeof HTMLElement } elementType
351- * @param { string [] } attributes
401+ * @param { BehaviourKey [] } keys An array of attribute names or non-attribute strings or Symbols
352402 */
353- register ( behaviourType , elementType , attributes ) {
354- if ( ! Array . isArray ( attributes ) ) {
403+ register ( behaviourType , elementType , keys ) {
404+ if ( ! Array . isArray ( keys ) ) {
355405 throw "attributes should be an array" ;
356406 }
357407
408+ /** @type Warning[] */
358409 const warnings = [ ] ;
359410 const behaviours = this . getBehaviourRecord ( elementType ) ;
360- for ( const attribute of attributes ) {
361- const existingBehaviourType = behaviours [ attribute ] ;
411+ for ( const key of keys ) {
412+ /** @type { (typeof Behaviour) | undefined } */
413+ const existingBehaviourType = behaviours [ key ] ;
362414 if ( existingBehaviourType !== undefined ) {
363- warnings . push ( { attribute , elementType, behaviourType, existingBehaviourType } ) ;
415+ warnings . push ( { key , elementType, behaviourType, existingBehaviourType } ) ;
364416 }
365417
366418 const localBehaviours = this . getOrCreateBehaviourRecord ( elementType ) ;
367- localBehaviours [ attribute ] = behaviourType ;
419+ localBehaviours [ key ] = behaviourType ;
368420 }
369421
370422 // For the first registration, we add the main document to the queue so that users cannot forget to call enable on it
@@ -390,11 +442,10 @@ class BehaviourRegistry {
390442 */
391443 connect ( elements ) {
392444 for ( const element of elements ) {
393- const behaviours = getBehaviourRecord ( element . constructor ) ;
394- if ( behaviours !== undefined ) {
395- const entries = Object . entries ( behaviours ) ;
396- for ( const [ attribute , behaviour ] of entries ) {
397- if ( element . hasAttribute ( attribute ) ) {
445+ const behaviourRecord = this . getBehaviourRecord ( element . constructor ) ;
446+ if ( behaviourRecord !== undefined ) {
447+ for ( const [ key , behaviour ] of allEntries ( behaviourRecord ) ) {
448+ if ( ! isAttributeName ( key ) || element . hasAttribute ( key ) ) {
398449 connect ( element , behaviour ) ;
399450 }
400451 }
@@ -477,18 +528,41 @@ addMicrotaskProperty(BehaviourRegistry, "update");
477528
478529const customBehaviours = new BehaviourRegistry ( ) ;
479530
531+ /**
532+ * Starts watching the document and connecting behaviours to relevant elements
533+ * @param { Document } document
534+ */
480535export function enableBehaviours ( document ) {
481536 customBehaviours . enable ( document ) ;
482537}
483538
539+ /**
540+ * Stops watching the document. Does not disconnect already connected behaviours.
541+ * @param { Document } document
542+ */
484543export function disableBehaviours ( document ) {
485544 customBehaviours . disable ( document ) ;
486545}
487546
547+ /**
548+ * Returns the effective record of behaviours/attributes that have been
549+ * applied to the specified element type and any base classes.
550+ * @param { typeof HTMLElement } elementType
551+ * @returns { BehaviourRecord }
552+ */
488553export function getBehaviourRecord ( elementType ) {
489554 return customBehaviours . getBehaviourRecord ( elementType ) ;
490555}
491556
492- export function registerBehaviour ( behaviourType , elementType , attributes ) {
493- return customBehaviours . register ( behaviourType , elementType , attributes ) ;
557+ /**
558+ * Registers a behaviour and a set of attributes as being usable with a particular element type (and its derived classes).
559+ * Returns a list of warnings if there are existing behaviours using the same attribute names anywhere in the element class hierarchy.
560+ * The warnings may be completely benign (if you want to provide a different behaviour for an attribute in a derived class, for example)
561+ * or they may indicate that you have a conflict with two behaviours both trying to use the same attribute names.
562+ * @param { typeof Behaviour } behaviourType
563+ * @param { typeof HTMLElement } elementType
564+ * @param { BehaviourKey[] } keys An array of attribute names or non-attribute strings or Symbols
565+ */
566+ export function registerBehaviour ( behaviourType , elementType , keys ) {
567+ return customBehaviours . register ( behaviourType , elementType , keys ) ;
494568}
0 commit comments