23
23
//
24
24
// In this version of the library, behaviours are enabled on a per-document basis.
25
25
// 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.
26
29
//
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:
28
31
//
29
32
// const behaviour = getBehaviour(element, MyBehaviour);
30
33
//
@@ -115,7 +118,17 @@ function addMicrotaskProperty(cls, methodName) {
115
118
} ) ;
116
119
}
117
120
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
+ * */
119
132
120
133
/**
121
134
* Base class for attribute behaviours
@@ -147,6 +160,26 @@ export class Behaviour {
147
160
disconnected ( ) { }
148
161
}
149
162
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
+
150
183
const INSTANCES = Symbol ( "behaviour-instances" ) ;
151
184
152
185
/**
@@ -185,6 +218,21 @@ function* allElements(elements) {
185
218
}
186
219
}
187
220
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
+
188
236
/**
189
237
* Connects a behaviour of the specified type to an element
190
238
* if the element doesn't already have a behaviour of that exact type.
@@ -241,13 +289,15 @@ export function getBehaviour(element, behaviourType) {
241
289
return undefined ;
242
290
}
243
291
292
+ /** @typedef { { key: BehaviourKey, elementType: typeof HTMLElement, behaviourType: typeof Behaviour, existingBehaviourType: typeof Behaviour } } Warning */
293
+
244
294
class BehaviourRegistry {
245
295
246
296
/**
247
297
* This contains the behaviours directly applied to a particular element type.
248
298
* To get the effective behaviours for a particular element type, we need to
249
299
* 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 >
251
301
* */
252
302
elementTypeToBehaviourRecord = new Map ( ) ;
253
303
@@ -328,7 +378,7 @@ class BehaviourRegistry {
328
378
* Returns the effective record of behaviours/attributes that have been
329
379
* applied to the specified element type and any base classes.
330
380
* @param { typeof HTMLElement } elementType
331
- * @returns { Record<AttributeName, typeof Behaviour> }
381
+ * @returns { BehaviourRecord }
332
382
*/
333
383
getBehaviourRecord ( elementType ) {
334
384
const elementTypeToBehaviourRecord = this . elementTypeToBehaviourRecord ;
@@ -348,23 +398,25 @@ class BehaviourRegistry {
348
398
* or they may indicate that you have a conflict with two behaviours both trying to use the same attribute names.
349
399
* @param { typeof Behaviour } behaviourType
350
400
* @param { typeof HTMLElement } elementType
351
- * @param { string [] } attributes
401
+ * @param { BehaviourKey [] } keys An array of attribute names or non-attribute strings or Symbols
352
402
*/
353
- register ( behaviourType , elementType , attributes ) {
354
- if ( ! Array . isArray ( attributes ) ) {
403
+ register ( behaviourType , elementType , keys ) {
404
+ if ( ! Array . isArray ( keys ) ) {
355
405
throw "attributes should be an array" ;
356
406
}
357
407
408
+ /** @type Warning[] */
358
409
const warnings = [ ] ;
359
410
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 ] ;
362
414
if ( existingBehaviourType !== undefined ) {
363
- warnings . push ( { attribute , elementType, behaviourType, existingBehaviourType } ) ;
415
+ warnings . push ( { key , elementType, behaviourType, existingBehaviourType } ) ;
364
416
}
365
417
366
418
const localBehaviours = this . getOrCreateBehaviourRecord ( elementType ) ;
367
- localBehaviours [ attribute ] = behaviourType ;
419
+ localBehaviours [ key ] = behaviourType ;
368
420
}
369
421
370
422
// 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 {
390
442
*/
391
443
connect ( elements ) {
392
444
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 ) ) {
398
449
connect ( element , behaviour ) ;
399
450
}
400
451
}
@@ -477,18 +528,41 @@ addMicrotaskProperty(BehaviourRegistry, "update");
477
528
478
529
const customBehaviours = new BehaviourRegistry ( ) ;
479
530
531
+ /**
532
+ * Starts watching the document and connecting behaviours to relevant elements
533
+ * @param { Document } document
534
+ */
480
535
export function enableBehaviours ( document ) {
481
536
customBehaviours . enable ( document ) ;
482
537
}
483
538
539
+ /**
540
+ * Stops watching the document. Does not disconnect already connected behaviours.
541
+ * @param { Document } document
542
+ */
484
543
export function disableBehaviours ( document ) {
485
544
customBehaviours . disable ( document ) ;
486
545
}
487
546
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
+ */
488
553
export function getBehaviourRecord ( elementType ) {
489
554
return customBehaviours . getBehaviourRecord ( elementType ) ;
490
555
}
491
556
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 ) ;
494
568
}
0 commit comments