@lumjs/core 1.26.0 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,537 @@
1
+ "use strict";
2
+
3
+ const {S,F,isObj,def,isIterable} = require('../types');
4
+ const Listener = require('./listener');
5
+
6
+ const DEF_EXTENDS =
7
+ {
8
+ registry: 'events',
9
+ listen: 'on',
10
+ emit: 'emit',
11
+ remove: null,
12
+ once: null,
13
+ }
14
+
15
+ const DEF_OPTIONS =
16
+ {
17
+ delimiter: /\s+/,
18
+ multiMatch: false,
19
+ wildcard: '*',
20
+ }
21
+
22
+ /**
23
+ * A class that handles events for target objects
24
+ *
25
+ * @prop {module:@lumjs/core/events~GetTargets} getTargets
26
+ * A constructor-assigned callback method that returns a set of targets.
27
+ * @prop {object} options - Registry-level options
28
+ * @prop {Set.<module:@lumjs/core/events.Listener>} allListeners
29
+ * All registered event listeners
30
+ * @prop {Map.<string,Set.<module:@lumjs/core/events.Listener>>} listenersFor
31
+ * Each key is a single event name, and the value is a Set of
32
+ * listener objects that handle that event.
33
+ *
34
+ * @alias module:@lumjs/core/events.Registry
35
+ */
36
+ class LumEventRegistry
37
+ {
38
+ /**
39
+ * Create a new registry instance for one or more target objects
40
+ *
41
+ * @param {(object|module:@lumjs/core/events~GetTargets)} targets
42
+ *
43
+ * If this is a `function`, it will be called to dynamically get a
44
+ * list of target objects whenever an event is triggered.
45
+ *
46
+ * If this is an `object`, then any kind of `Iterable` may be used
47
+ * to represent multiple targets, while any non-Iterable object will
48
+ * be considered as single target.
49
+ *
50
+ * @param {object} [opts] Options (saved to `options` property).
51
+ *
52
+ * @param {(RegExp|string)} [opts.delimiter=/\s+/] Used to split event names
53
+ *
54
+ * @param {boolean} [opts.multiMatch=false]
55
+ * If a registered listener has multiple event names, and a call
56
+ * to `emit()` also has multiple event names, the value of this
57
+ * option will determine if the same listener will have its
58
+ * handler function called more than once.
59
+ *
60
+ * If this is `true`, the handler will be called once for every
61
+ * combination of target and event name.
62
+ *
63
+ * If this is `false` (default), then only the first matching event
64
+ * name will be called for each target.
65
+ *
66
+ * @param {(object|boolean)} [opts.extend]
67
+ * This option determines the rules for adding wrapper methods and
68
+ * other extension properties to the target objects.
69
+ *
70
+ * If this is `true` (default when `targets` is an `object`), then
71
+ * the target objects will be extended using the default property names.
72
+ *
73
+ * If this is set to `false` (default when `targets` is a `function`),
74
+ * it disables adding extension properties entirely.
75
+ *
76
+ * If it is an `object` then each nested property may be set to a string
77
+ * to override the default, or `null` to skip adding that property.
78
+ *
79
+ * @param {?string} [opts.extend.registry="events"] Registry property
80
+ * @param {?string} [opts.extend.emit="emit"] `emit()` proxy method
81
+ * @param {?string} [opts.extend.listen="on"] `listen()` proxy method
82
+ * @param {?string} [opts.extend.once=null] `once()` proxy method
83
+ * @param {?string} [opts.extend.remove=null] `remove()` proxy method
84
+ *
85
+ * @param {boolean} [opts.overwrite=false] Overwrite existing properties?
86
+ *
87
+ * If `true` then when adding wrapper methods, the properties from
88
+ * `opts.extend` will replace any existing ones in each target.
89
+ *
90
+ * @param {function} [opts.setupEvent] Initialize each Event object?
91
+ *
92
+ * If this is specified (either here or in individual listeners),
93
+ * it will be called and passed the Event object at the very end of
94
+ * its constructor.
95
+ *
96
+ * @param {string} [opts.wildcard='*'] Wildcard event name.
97
+ *
98
+ * - If you use this in `listen()` the handler will be used regardless
99
+ * as to what event name was triggered. You can always see which
100
+ * event name was actually triggered by using `event.name`.
101
+ * - If you use this in `remove()` it calls `removeAll()` to remove all
102
+ * registered listeners.
103
+ */
104
+ constructor(targets, opts={})
105
+ {
106
+ let defExt; // Default opts.extend value
107
+ if (typeof targets === F)
108
+ { // A dynamic getter method
109
+ this.getTargets = targets;
110
+ defExt = false;
111
+ }
112
+ else
113
+ { // Simple getter for a static value
114
+ if (!isIterable(targets))
115
+ {
116
+ targets = [targets];
117
+ }
118
+ this.getTargets = () => targets;
119
+ defExt = true;
120
+ }
121
+
122
+ this.options = Object.assign({}, DEF_OPTIONS, opts);
123
+
124
+ this.allListeners = new Set();
125
+ this.listenersFor = new Map();
126
+
127
+ const extOpts = opts.extend ?? defExt;
128
+ if (extOpts !== false)
129
+ {
130
+ const intNames = Object.keys(DEF_EXTENDS);
131
+ const extNames = Object.assign({}, DEF_EXTENDS, extOpts);
132
+ const targets = this.getTargets();
133
+
134
+ for (const iname of intNames)
135
+ {
136
+ if (typeof extNames[iname] === S && extNames[iname].trim() !== '')
137
+ {
138
+ const ename = extNames[iname];
139
+ const value = iname === 'registry'
140
+ ? this // The registry instance itself
141
+ : (...args) => this[iname](...args) // A proxy method
142
+ for (const target of targets)
143
+ {
144
+ if (opts.overwrite || target[ename] === undefined)
145
+ {
146
+ def(target, ename, {value});
147
+ }
148
+ else
149
+ {
150
+ console.error("Won't overwrite existing property",
151
+ {target,iname,ename,registry: this});
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+ } // constructor()
158
+
159
+ /**
160
+ * Build a new Listener instance; used by `listen()` method.
161
+ *
162
+ * @param {(string|object)} eventNames
163
+ * What this does depends on the type, and the number of arguments passed.
164
+ *
165
+ * If this is an `object` **AND** is *the only argument* passed,
166
+ * it will be used as the `spec`, and the `spec.eventNames`
167
+ * and `spec.listener` properties will become mandatory.
168
+ *
169
+ * If it's a string or there is more than one argument, this
170
+ * will be used as the `spec.eventNames` property.
171
+ *
172
+ * @param {module:@lumjs/core/events~Handler} [handler]
173
+ * Used as the `spec.handler` property if specified.
174
+ *
175
+ * This is mandatory if `eventNames` argument is a `string`!
176
+ *
177
+ * @param {object} [spec] The listener specification rules
178
+ *
179
+ * @param {(string|Iterable)} [spec.eventNames] Event names to listen for
180
+ *
181
+ * See {@link module:@lumjs/core/events.Registry#getEventNames} for details.
182
+ *
183
+ * @param {module:@lumjs/core/events~Handler} [spec.handler] Event handler.
184
+ *
185
+ * @param {(function|object)} [spec.listener] An alias for `handler`
186
+ *
187
+ * @param {object} [spec.options] Options for the listener
188
+ *
189
+ * The option properties can be included directly in the `spec` itself
190
+ * for brevity, but a nested `options` object is supported to be more
191
+ * like the `DOM.addEventListener()` method. Either way works fine.
192
+ *
193
+ * If `spec.options` is used, the properties in it take precedence over
194
+ * those directly in the `spec` object. Note that you cannot use the
195
+ * names `listener`, `handler` or `eventNames` as option properties,
196
+ * and if found, they will be removed.
197
+ *
198
+ * You may also override the `setupEvent` registry option here.
199
+ *
200
+ * @param {boolean} [spec.options.once=false] Only use the listener once?
201
+ *
202
+ * If this is set to `true`, then the first time this listener is used in
203
+ * an {@link module:@lumjs/core/events.Registry#emit emit()} call, it will
204
+ * be removed from the registry at the end of the emit process (after all
205
+ * events for all targets have been triggered).
206
+ *
207
+ * @returns {module:@lumjs/core/events.Listener} A new `Listener` instance
208
+ */
209
+ makeListener(...args)
210
+ {
211
+ let spec;
212
+
213
+ if (args.length === 0 || args.length > 3)
214
+ {
215
+ console.error({args, registry: this});
216
+ throw new RangeError("Invalid number of arguments");
217
+ }
218
+ else if (args.length === 1 && isObj(args[0]))
219
+ { // listen(spec)
220
+ spec = Object.assign({}, args[0]);
221
+ }
222
+ else
223
+ { // listen(eventNames, listener, [spec])
224
+ spec = Object.assign({}, args[2]);
225
+ spec.eventNames = args[0];
226
+ spec.handler = args[1];
227
+ }
228
+
229
+ return new Listener(this, spec);
230
+ }
231
+
232
+ /**
233
+ * Assign a new event listener.
234
+ *
235
+ * Calls `this.makeListener()` passing all arguments to it.
236
+ * Then calls `this.add(listener)` passing the newly make `Listener`.
237
+ *
238
+ * @param {...mixed} args
239
+ * @returns {module:@lumjs/core/events.Listener}
240
+ */
241
+ listen()
242
+ {
243
+ const listener = this.makeListener(...arguments)
244
+ this.add(listener);
245
+ return listener;
246
+ }
247
+
248
+ /**
249
+ * Assign a new event listener that will only run once.
250
+ *
251
+ * Calls `this.listen()` passing all arguments to it,
252
+ * then sets the `listener.options.once` to `true`.
253
+ *
254
+ * @param {...mixed} args
255
+ * @returns {module:@lumjs/core/events.Listener}
256
+ */
257
+ once()
258
+ {
259
+ const listener = this.listen(...arguments);
260
+ listener.options.once = true;
261
+ return listener;
262
+ }
263
+
264
+ /**
265
+ * Add a Listener instance.
266
+ *
267
+ * You'd generally use `listen()` or `once()` rather than this, but if
268
+ * you need to (re-)add an existing instance, this is the way to do it.
269
+ *
270
+ * @param {module:@lumjs/core/events.Listener} listener - Listener instance
271
+ *
272
+ * If the same instance is passed more than once it will have no affect,
273
+ * as we store the instances in a `Set` internally, so it'll only ever
274
+ * be stored once.
275
+ *
276
+ * @returns {module:@lumjs/core/events.Registry} `this`
277
+ */
278
+ add(listener)
279
+ {
280
+ if (!(listener instanceof Listener))
281
+ {
282
+ console.error({listener, registry: this});
283
+ throw new TypeError("Invalid listener instance");
284
+ }
285
+
286
+ this.allListeners.add(listener);
287
+
288
+ for (const ename of listener.eventNames)
289
+ {
290
+ let lset;
291
+ if (this.listenersFor.has(ename))
292
+ {
293
+ lset = this.listenersFor.get(ename);
294
+ }
295
+ else
296
+ {
297
+ lset = new Set();
298
+ this.listenersFor.set(ename, lset);
299
+ }
300
+
301
+ lset.add(listener);
302
+ }
303
+
304
+ return this;
305
+ }
306
+
307
+ /**
308
+ * Remove **ALL** registered event listeners!
309
+ * @returns {module:@lumjs/core/events.Registry} `this`
310
+ */
311
+ removeAll()
312
+ {
313
+ this.allListeners.clear();
314
+ this.listenersFor.clear();
315
+ return this;
316
+ }
317
+
318
+ /**
319
+ * Remove specific event names.
320
+ *
321
+ * It will remove any of the the specified event names
322
+ * from applicable listener instances, and clear the
323
+ * associated `listenersFor` set.
324
+ *
325
+ * If a listener has no more event names left, that listener
326
+ * will be removed from the `allListeners` set as well.
327
+ *
328
+ * @param {...string} names - Event names to remove
329
+ *
330
+ * If the `wildcard` string is specified here, this will simply
331
+ * remove any wildcard listeners currently registered.
332
+ * See `remove(wildcard)` or `removeAll()` if you really want
333
+ * to remove **ALL** listeners.
334
+ *
335
+ * @returns {module:@lumjs/core/events.Registry} `this`
336
+ */
337
+ removeEvents(...names)
338
+ {
339
+ for (const name of names)
340
+ {
341
+ if (this.listenersFor.has(name))
342
+ {
343
+ const eventListeners = this.listenersFor.get(name);
344
+ for (const lsnr of eventListeners)
345
+ {
346
+ lsnr.eventNames.delete(name);
347
+ if (!lsnr.hasEvents)
348
+ { // The last event name was removed.
349
+ this.removeListeners(lsnr);
350
+ }
351
+ }
352
+ eventListeners.clear();
353
+ }
354
+ }
355
+ return this;
356
+ }
357
+
358
+ /**
359
+ * Remove specific Listener instances
360
+ * @param {...module:@lumjs/core/events.Listener} listeners
361
+ * @returns {module:@lumjs/core/events.Registry} `this`
362
+ */
363
+ removeListeners(...listeners)
364
+ {
365
+ for (const listener of listeners)
366
+ {
367
+ if (this.allListeners.has(listener))
368
+ { // First remove it from allListeners
369
+ this.allListeners.delete(listener);
370
+
371
+ for (const ename of listener.eventNames)
372
+ {
373
+ if (this.listenersFor.has(ename))
374
+ {
375
+ const lset = this.listenersFor.get(ename);
376
+ lset.delete(listener);
377
+ }
378
+ }
379
+ }
380
+ }
381
+ return this;
382
+ }
383
+
384
+ /**
385
+ * Remove listeners based on the value type used.
386
+ *
387
+ * @param {(string|module:@lumjs/core/events.Listener)} what
388
+ *
389
+ * - If this is the `wildcard` string, then this will call `removeAll()`.
390
+ * - If this is any other `string` it will be split using `splitNames()`,
391
+ * and the resulting strings passed as arguments to `removeEvents()`.
392
+ * - If this is a `Listener` instance, its passed to `removeListeners()`.
393
+ *
394
+ * @returns {module:@lumjs/core/events.Registry} `this`
395
+ * @throws {TypeError} If `what` is none of the above values.
396
+ */
397
+ remove(what)
398
+ {
399
+ if (what === this.options.wildcard)
400
+ {
401
+ return this.removeAll();
402
+ }
403
+ else if (typeof what === S)
404
+ {
405
+ const events = this.splitNames(what);
406
+ return this.removeEvents(...events);
407
+ }
408
+ else if (what instanceof Listener)
409
+ {
410
+ return this.removeListeners(what);
411
+ }
412
+ else
413
+ {
414
+ console.error({what, registry: this});
415
+ throw new TypeError("Invalid event name or listener instance");
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Emit (trigger) one or more events.
421
+ *
422
+ * @param {(string|string[])} eventNames - Events to emit.
423
+ *
424
+ * If this is a single `string` it will be split via `splitNames()`.
425
+ *
426
+ * @param {object} [data] A data object (highly recommended);
427
+ * will be assigned to `event.data` if specified.
428
+ *
429
+ * @param {...any} [args] Any other arguments;
430
+ * will be assigned to `event.args`.
431
+ *
432
+ * Note: if a `data` object argument was passed, it will always
433
+ * be the first item in `event.args`.
434
+ *
435
+ * @returns {module:@lumjs/core/events~Status}
436
+ */
437
+ emit(eventNames, ...args)
438
+ {
439
+ const sti =
440
+ {
441
+ eventNames: this.getEventNames(eventNames),
442
+ multiMatch: this.options.multiMatch,
443
+ onceRemoved: new Set(),
444
+ stopEmitting: false,
445
+ emitted: [],
446
+ }
447
+
448
+ { // Get the targets.
449
+ const tgs = this.getTargets(sti);
450
+ sti.targets = (tgs instanceof Set) ? tgs : new Set(tgs);
451
+ }
452
+
453
+ const wilds = this.listenersFor.get(this.options.wildcard);
454
+
455
+ emitting: for (const tg of sti.targets)
456
+ {
457
+ const called = sti.targetListeners = new Set();
458
+ for (const ename of sti.eventNames)
459
+ {
460
+ if (!this.listenersFor.has(ename)) continue;
461
+
462
+ let listeners = this.listenersFor.get(ename);
463
+ if (wilds) listeners = listeners.union(wilds);
464
+
465
+ for (const lsnr of listeners)
466
+ {
467
+ if (sti.multiMatch || !called.has(lsnr))
468
+ { // Let's emit an event!
469
+ called.add(lsnr);
470
+ const event = lsnr.emitEvent(ename, tg, args, sti);
471
+ sti.emitted.push(event);
472
+ if (sti.stopEmitting)
473
+ {
474
+ break emitting;
475
+ }
476
+ }
477
+ }
478
+ }
479
+ }
480
+
481
+ // Nix the targetListeners property.
482
+ delete sti.targetListeners;
483
+
484
+ // Handle any `onceRemoved` listeners.
485
+ for (const lsnr of sti.onceRemoved)
486
+ {
487
+ this.removeListeners(lsnr);
488
+ }
489
+
490
+ // Return the final status.
491
+ return sti;
492
+ }
493
+
494
+ /**
495
+ * Get a Set of event names from various kinds of values
496
+ * @param {(string|Iterable)} names - Event names source
497
+ *
498
+ * If this is a string, it'll be passed to `splitNames()`.
499
+ * If it's any kind of `Iterable`, it'll be converted to a `Set`.
500
+ *
501
+ * @returns {Set}
502
+ * @throws {TypeError} If `names` is not a valid value
503
+ */
504
+ getEventNames(names)
505
+ {
506
+ if (typeof names === S)
507
+ {
508
+ return this.splitNames(names);
509
+ }
510
+ else if (names instanceof Set)
511
+ {
512
+ return names;
513
+ }
514
+ else if (isIterable(names))
515
+ {
516
+ return new Set(names);
517
+ }
518
+ else
519
+ {
520
+ console.error({names, registry: this});
521
+ throw new TypeError("Invalid event names");
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Split a (trimmed) string using `this.options.delimiter`
527
+ * @param {string} names - String to split
528
+ * @returns {Set}
529
+ */
530
+ splitNames(names)
531
+ {
532
+ return new Set(names.trim().split(this.options.delimiter));
533
+ }
534
+
535
+ }
536
+
537
+ module.exports = LumEventRegistry;
package/lib/index.js CHANGED
@@ -22,6 +22,7 @@ const types = require('./types');
22
22
  /**
23
23
  * Define properties on an object or function
24
24
  * @name module:@lumjs/core.def
25
+ * @function
25
26
  * @see module:@lumjs/core/types.def
26
27
  */
27
28
  const def = types.def;
@@ -29,6 +30,7 @@ const def = types.def;
29
30
  /**
30
31
  * Define *lazy* properties on an object or function
31
32
  * @alias module:@lumjs/core.lazy
33
+ * @function
32
34
  * @see module:@lumjs/core/types.lazy
33
35
  */
34
36
  const lazy = types.lazy;
@@ -80,7 +82,7 @@ lazy(exports, 'flags', () => require('./flags'));
80
82
  lazy(exports, 'obj', () => require('./obj'));
81
83
 
82
84
  /**
83
- * A wrapper around the Javascript console.
85
+ * A wrapper around the Javascript console «Lazy»
84
86
  * @name module:@lumjs/core.console
85
87
  * @type {module:@lumjs/core/console}
86
88
  */
@@ -98,24 +100,19 @@ lazy(exports, 'traits', () => require('./traits'));
98
100
  * @name module:@lumjs/core.opt
99
101
  * @type {module:@lumjs/core/opt}
100
102
  */
101
- const optOpts = {enumerable: true, def:{autoDesc: false}}
102
- lazy(exports, 'opt', () => require('./opt'), optOpts);
103
+ lazy(exports, 'opt', () => require('./opt'), {def:{autoDesc: false}});
103
104
 
104
105
  // Get a bunch of properties from a submodule.
105
- function from(submod, ...libs)
106
+ function from(submod)
106
107
  {
107
- for (const lib of libs)
108
+ for (const key in submod)
108
109
  {
109
- def(exports, lib, submod[lib]);
110
+ def(exports, key, submod[key]);
110
111
  }
111
112
  }
112
113
 
113
114
  // ObjectID stuff is imported directly without registering a sub-module.
114
- const objectid = require('./objectid');
115
- from(objectid,
116
- 'randomNumber',
117
- 'UniqueObjectIds',
118
- 'InternalObjectId');
115
+ from(require('./objectid'));
119
116
 
120
117
  /**
121
118
  * Get a simplistic debugging stacktrace
@@ -143,15 +140,29 @@ from(objectid,
143
140
  * @see module:@lumjs/core/meta.NYI
144
141
  */
145
142
 
143
+ /**
144
+ * A function indicating that something is deprecated
145
+ * @name module:@lumjs/core.deprecated
146
+ * @function
147
+ * @see module:@lumjs/core/meta.deprecated
148
+ */
149
+
150
+ /**
151
+ * Assign a getter property to an object that calls the `deprecated`
152
+ * function with a specific message before returning the proxied value.
153
+ * @name module:@lumjs/core.wrapDepr
154
+ * @function
155
+ * @see module:@lumjs/core/meta.wrapDepre
156
+ */
157
+
146
158
  // These are exported directly, but a meta sub-module also exists.
147
159
  // Unlike most sub-modules there is no `meta` property in the main library.
148
160
  const meta = require('./meta');
149
- from(meta,
150
- 'stacktrace',
151
- 'AbstractClass',
152
- 'AbstractError',
153
- 'Functions',
154
- 'NYI');
161
+ from(meta);
162
+ def(exports, 'AbstractClass',
163
+ {
164
+ get() { return meta.AbstractClass; }
165
+ });
155
166
 
156
167
  /**
157
168
  * Create a magic *Enum* object «Lazy»
@@ -169,3 +180,9 @@ lazy(exports, 'Enum', () => require('./enum'));
169
180
  */
170
181
  lazy(exports, 'observable', () => require('./observable'));
171
182
 
183
+ /**
184
+ * The Events module «Lazy»
185
+ * @name module:@lumjs/core.events
186
+ * @see module:@lumjs/core/events
187
+ */
188
+ lazy(exports, 'events', () => require('./events'));