@lumjs/core 1.34.0 → 1.35.1

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.
@@ -10,6 +10,9 @@
10
10
  *
11
11
  * @module @lumjs/core/events
12
12
  */
13
+
14
+ const lazy = require('../types/lazy');
15
+
13
16
  exports = module.exports =
14
17
  {
15
18
  Registry: require('./registry'),
@@ -50,7 +53,41 @@ exports = module.exports =
50
53
  */
51
54
  observable(target, opts)
52
55
  {
53
- const {makeObservable} = require('./observable');
56
+ const {makeObservable} = exports.Observable;
54
57
  return makeObservable(target, opts);
55
58
  },
59
+
60
+ /**
61
+ * See if an object has a known trigger method.
62
+ *
63
+ * Currently will look for one of:
64
+ *
65
+ * - `emit`
66
+ * - `trigger`
67
+ *
68
+ * @param {object} obj - Object we want to find a trigger method on
69
+ *
70
+ * @returns {?string} The name of the method found,
71
+ * or null if none found.
72
+ */
73
+ hasTrigger(obj)
74
+ {
75
+ for (const trigger of exports.KNOWN_TRIGGERS)
76
+ {
77
+ if (typeof obj[trigger] === F)
78
+ {
79
+ return trigger;
80
+ }
81
+ }
82
+
83
+ return null;
84
+ },
85
+
86
+ /**
87
+ * The list of known trigger method names for `hasTrigger()`
88
+ */
89
+ KNOWN_TRIGGERS: ['emit','trigger'],
90
+
56
91
  }
92
+
93
+ lazy(exports, 'Observable', () => require('./observable'));
@@ -4,7 +4,7 @@
4
4
  */
5
5
  "use strict";
6
6
 
7
- const {B,F,S,def} = require('../types');
7
+ const {B,F,S,def,isComplex,TYPES} = require('../types');
8
8
  const lock = Object.freeze;
9
9
  const copy = Object.assign;
10
10
  const Registry = require('./registry');
@@ -41,12 +41,10 @@ const OBS_OPTIONS =
41
41
  * A few obscure features that I don't think are particularly
42
42
  * important and won't affect *most* exising uses will be dropped.
43
43
  *
44
- * @param {(object|function)} el - The target we're making observable
45
- * @param {object} [oo]
46
- * {@link module:@lumjs/core/observable Observable} options;
44
+ * @param {(object|function)} el - The target to add observable API to.
47
45
  *
48
- * The `addre` and `reinherit` options are not supported.
49
- * All the other ones should try to work mostly the same.
46
+ * @param {object} [oo]
47
+ * {@link module:@lumjs/core/observable Observable} options
50
48
  *
51
49
  * @param {boolean} [oo.addwrap] If this is `true`,
52
50
  * and `oo.wrapargs` is `false`, then the `event` object
@@ -69,10 +67,16 @@ const OBS_OPTIONS =
69
67
  *
70
68
  * @param {boolean} [wantReg=false] Return the Registry?
71
69
  * @returns {object} normally `el`, unless `wantReg` was `true`
70
+ * @see module:@lumjs/core/observable~API
72
71
  * @alias module:@lumjs/core/events/observable.makeObservable
73
72
  */
74
73
  function makeObservable(el, oo, ro, wantReg=false)
75
74
  {
75
+ if (!isComplex(el))
76
+ {
77
+ throw new TypeError("el was not an object or function");
78
+ }
79
+
76
80
  ro = copy({}, ro);
77
81
  ro.extend = copy({}, OBS_EXTENDS, ro.extend);
78
82
  ro.setupEvent = setupEvent;
@@ -126,6 +130,7 @@ function makeObservable(el, oo, ro, wantReg=false)
126
130
  /**
127
131
  * Event setup for observable compatibility
128
132
  * @type {module:@lumjs/core/events~SetupEvent}
133
+ * @alias module:@lumjs/core/events/observable.setupEvent
129
134
  */
130
135
  function setupEvent(ev)
131
136
  {
@@ -150,6 +155,7 @@ function setupEvent(ev)
150
155
  /**
151
156
  * Listener setup for observable compatibility
152
157
  * @type {module:@lumjs/core/events~SetupListener}
158
+ * @alias module:@lumjs/core/events/observable.setupListener
153
159
  */
154
160
  function setupListener(ln)
155
161
  {
@@ -195,7 +201,43 @@ function setupListener(ln)
195
201
  }
196
202
  }
197
203
 
204
+ /**
205
+ * See if a value appears to implement the observable API.
206
+ *
207
+ * This is a simple function that relies on duck-typing,
208
+ * and only looks for `trigger` and `on` methods.
209
+ *
210
+ * @param {(object|function)} obj - Value to check for observable API
211
+ * @returns {boolean}
212
+ * @alias module:@lumjs/core/events/observable.isObservable
213
+ */
214
+ function isObservable(obj)
215
+ {
216
+ return (isComplex(obj)
217
+ && typeof obj.trigger === F
218
+ && typeof obj.on === F);
219
+ }
220
+
221
+ // Undocumented alias for the sake of compatibility.
222
+ def(makeObservable, 'is', isObservable);
223
+
224
+ /**
225
+ * Does a value implement the Observable interface?
226
+ * @name module:@lumjs/core/types.doesObservable
227
+ * @function
228
+ * @param {*} v - The expected object/function to test.
229
+ * @returns {boolean}
230
+ * @see module:@lumjs/core/events/observable.isObservable
231
+ */
232
+
233
+ /**
234
+ * Extension type for the {@link module:@lumjs/core/observable~API} interface.
235
+ * @memberof module:@lumjs/core/types.TYPES
236
+ * @member {string} OBSERV - Implements the *Observable* interface.
237
+ */
238
+ TYPES.add('OBSERV', 'observable', isObservable, 'doesObservable');
239
+
198
240
  module.exports =
199
241
  {
200
- makeObservable, setupEvent, setupListener,
242
+ makeObservable, setupEvent, setupListener, isObservable,
201
243
  }
@@ -225,19 +225,16 @@ class LumEventRegistry
225
225
  const value = iname === 'registry'
226
226
  ? this // The registry instance itself
227
227
  : (...args) => this[iname](...args) // A proxy method
228
- for (const target of targets)
228
+ if (opts.overwrite || target[ename] === undefined)
229
229
  {
230
- if (opts.overwrite || target[ename] === undefined)
231
- {
232
- def(target, ename, {value});
233
- tps[ename] = iname;
234
- tpm.p[ename] = this;
235
- }
236
- else
237
- {
238
- console.error("Won't overwrite existing property",
239
- {target,iname,ename,registry: this});
240
- }
230
+ def(target, ename, {value});
231
+ tps[ename] = iname;
232
+ tpm.p[ename] = this;
233
+ }
234
+ else
235
+ {
236
+ console.error("Won't overwrite existing property",
237
+ {target,iname,ename,registry: this});
241
238
  }
242
239
  }
243
240
  }
package/lib/observable.js CHANGED
@@ -1,15 +1,36 @@
1
- // Defining these after observable.
2
- const {B,F,S,def,isObj,isComplex,TYPES} = require('./types');
3
- const lock = Object.freeze;
4
- const copy = Object.assign;
1
+ /**
2
+ * Observable API module
3
+ * @module @lumjs/core/observable
4
+ */
5
+ "use strict";
6
+
7
+ const {B,S,def} = require('./types');
8
+ const orig = require('./old/observable');
9
+ const evob = require('./events/observable');
5
10
 
6
11
  /**
7
- * Make a target object (or function) support the *Observable* API.
8
- *
9
- * Adds `on()`, `off()`, `one()`, and `trigger()` methods.
12
+ * Make a target object (or function) support the observable API.
13
+ *
14
+ * There are currently two implementations of the observable API,
15
+ * and this function will determine which one to call based on the
16
+ * arguments passed (if old options or positional arguments are
17
+ * detected, the original implementation will be called, otherwise
18
+ * the new events-based implementation will be called.)
10
19
  *
11
- * @param {(object|function)} el - The target we are making observable.
20
+ * When the older implementation is removed, this function will be
21
+ * replaced by a simple alias to `makeObservable`.
22
+ *
23
+ * NOTE: this function is the *actual* exported value of the `observable`
24
+ * module, but it's also available as `observable.auto` and that is how
25
+ * it's being documented (as jsdoc doesn't like functions being treated
26
+ * like objects with methods defined on them, but I do that a lot.)
27
+ *
28
+ * @param {(object|function)} el - The target to add observable API to.
12
29
  * @param {object} [opts] Options that define behaviours.
30
+ *
31
+ * If the `addre` and `reinherit` options are detected, then the original
32
+ * implementation will be used.
33
+ *
13
34
  * @param {string} [opts.wildcard='*'] The event name used as a wildcard.
14
35
  *
15
36
  * @param {boolean} [opts.wrapargs=false] If `true`, the event handlers will
@@ -76,335 +97,57 @@ const copy = Object.assign;
76
97
  * to the `el` object, which is a version of `observable()` with the
77
98
  * default options being the same as the current `opts`.
78
99
  *
79
- * @param {string} [opts.addre] If set, add a method with this name
80
- * to the `el` object, which is a function that can re-build the
81
- * observable API methods with new `opts` replacing the old ones.
82
- *
83
- * The method added takes two arguments, the first being an `object`
84
- * representing the new options to set on the target.
100
+ * @param {(boolean|object)} [arg3] Version dependent argument.
85
101
  *
86
- * The second is an optional `boolean` value that determines if the
87
- * existing `opts` should be used as defaults for any options not
88
- * specified in the first argument.
102
+ * If this is a `boolean` then the original observable implementation
103
+ * will be called and this will be the `redefine` argument.
89
104
  *
90
- * @param {boolean} [opts.reinherit=false] Used as the default value of
91
- * the second argument of the method added by `opts.addre`.
105
+ * Otherwise it's assumed to be the `ro` argument of the `makeObservable`
106
+ * compatibility implementation.
92
107
  *
93
- * @param {boolean} [redefine=false] If `true` allow targets already
94
- * implementing the `on()` and `trigger()` methods to be re-initialized.
95
- *
96
- * Generally only needed if you need to change the `opts` for some reason.
97
- * This is forced to `true` by the method added by `opts.addre`.
98
- *
99
108
  * @returns {object} el
100
109
  *
101
- * @exports module:@lumjs/core/observable
110
+ * @see module:@lumjs/core/observable.orig
111
+ * @see module:@lumjs/core/events/observable.makeObservable
112
+ * @alias module:@lumjs/core/observable.auto
102
113
  */
103
- function observable (el={}, opts={}, redefine=false)
114
+ function observable (el={}, opts={}, arg3)
104
115
  {
105
- //console.debug("observable", el, opts);
106
-
107
- if (!isComplex(el))
108
- { // Don't know how to handle this, sorry.
109
- throw new Error("non-object sent to observable()");
110
- }
111
-
112
- if (isObservable(el) && !redefine)
113
- { // It's already observable.
114
- return el;
115
- }
116
-
117
- if (typeof opts === B)
118
- { // Assume it's the wrapthis option.
119
- opts = {wrapthis: opts};
120
- }
121
- else if (!isObj(opts))
122
- {
123
- opts = {};
124
- }
125
-
126
- const noSpace = /^\S+$/;
127
-
128
- const wildcard = (typeof opts.wildcard === S
129
- && noSpace.test(opts.wildcard))
130
- ? opts.wildcard
131
- : '*';
132
-
133
- const wrapsetup = (typeof opts.wrapsetup === F)
134
- ? opts.wrapsetup
135
- : null;
136
-
137
- const wrapthis = (typeof opts.wrapthis === B)
138
- ? opts.wrapthis
139
- : false;
140
-
141
- const wrapargs = (typeof opts.wrapargs === B)
142
- ? opts.wrapargs
143
- : (wrapsetup ? !wrapthis : false);
144
-
145
- const wrapped = (wrapthis || wrapargs);
146
-
147
- const wraplock = (typeof opts.wraplock === B)
148
- ? opts.wraplock
149
- : true;
150
-
151
- const addname = (typeof opts.addname === B)
152
- ? opts.addname
153
- : !wrapped;
154
-
155
- const addis = (typeof opts.addis === B)
156
- ? opts.addis
157
- : wrapped;
158
-
159
- const validIdent = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
160
-
161
- const addme = (typeof opts.addme === S
162
- && validIdent.test(opts.addme))
163
- ? opts.addme
164
- : null;
165
-
166
- const addre = (typeof opts.addre === S
167
- && validIdent.test(opts.addre))
168
- ? opts.addre
169
- : null;
170
-
171
- const slice = Array.prototype.slice;
172
-
173
- function onEachEvent (e, fn)
116
+ if (typeof arg3 === B
117
+ || typeof opts.addre === S
118
+ || typeof opts.reinherit === B)
174
119
  {
175
- const es = e.split(/\s+/);
176
- const me = es.length > 1;
177
- for (e of es)
178
- {
179
- fn(e, me);
180
- }
181
- }
182
-
183
- const add = def(el);
184
-
185
- function runCallback (name, fn, args)
186
- {
187
- if (fn.busy) return;
188
- fn.busy = 1;
189
-
190
- let fobj;
191
-
192
- if (wrapped)
193
- { // Something is going to use our wrapper object.
194
- const isWild = (name === wildcard);
195
- const fname = isWild ? (addname ? args[0] : args.shift()) : name;
196
-
197
- fobj =
198
- {
199
- isObservable: lock({event: true, target: false}),
200
- self: el,
201
- target: el,
202
- name: fname,
203
- type: fname,
204
- func: fn,
205
- wildcard: isWild,
206
- args,
207
- };
208
-
209
- if (wrapsetup)
210
- {
211
- wrapsetup.call(el, fobj);
212
- }
213
-
214
- if (wraplock)
215
- {
216
- lock(fobj);
217
- }
218
- }
219
-
220
- const fthis = wrapthis ? fobj : el;
221
- const fargs = wrapargs ? [fobj]
222
- : ((fn.typed && addname) ? [name].concat(args) : args);
223
-
224
- fn.apply(fthis, fargs);
225
- fn.busy = 0;
120
+ return orig(el, opts, arg3);
226
121
  }
227
-
228
- let callbacks = {};
229
-
230
- /**
231
- * Assign an event handler
232
- *
233
- * Listen to the given space separated list of `events` and execute
234
- * the `callback` each time an event is triggered.
235
- * @param {string} events - events ids
236
- * @param {function} fn - callback function
237
- * @returns {object} el
238
- */
239
- add('on', function(events, fn)
240
- {
241
- if (typeof fn !== F)
242
- {
243
- console.error("non-function passed to on()");
244
- return el;
245
- }
246
-
247
- onEachEvent(events, function(name, typed)
248
- {
249
- (callbacks[name] = callbacks[name] || []).push(fn);
250
- fn.typed = typed;
251
- });
252
-
253
- return el;
254
- });
255
-
256
- /**
257
- * Removes the given space separated list of `events` listeners
258
- *
259
- * @param {string} events - events ids
260
- * @param {function} fn - callback function
261
- * @returns {object} el
262
- */
263
- add('off', function(events, fn)
264
- {
265
- if (events === wildcard && !fn)
266
- { // Clear all callbacks.
267
- callbacks = {};
268
- }
269
- else
270
- {
271
- onEachEvent(events, function(name)
272
- {
273
- if (fn)
274
- { // Find a specific callback to remove.
275
- var arr = callbacks[name]
276
- for (var i = 0, cb; cb = arr && arr[i]; ++i)
277
- {
278
- if (cb == fn) arr.splice(i--, 1);
279
- }
280
- }
281
- else
282
- { // Remove all callbacks for this event.
283
- delete callbacks[name];
284
- }
285
- });
286
- }
287
- return el
288
- });
289
-
290
- /**
291
- * Add a one-shot event handler.
292
- *
293
- * Listen to the given space separated list of `events` and execute
294
- * the `callback` at most once.
295
- *
296
- * @param {string} events - events ids
297
- * @param {function} fn - callback function
298
- * @returns {object} el
299
- */
300
- add('one', function(events, fn)
301
- {
302
- function on()
303
- {
304
- el.off(events, on)
305
- fn.apply(el, arguments)
306
- }
307
- return el.on(events, on);
308
- });
309
-
310
- /**
311
- * Execute all callback functions that listen to the given space
312
- * separated list of `events`
313
- * @param {string} events - events ids
314
- * @returns {object} el
315
- */
316
- add('trigger', function(events)
317
- {
318
- // getting the arguments
319
- // skipping the first one
320
- const args = slice.call(arguments, 1);
321
-
322
- onEachEvent(events, function(name)
323
- {
324
- const fns = slice.call(callbacks[name] || [], 0);
325
-
326
- for (var i = 0, fn; fn = fns[i]; ++i)
327
- {
328
- runCallback(name, fn, args);
329
- if (fns[i] !== fn) { i-- }
330
- }
331
-
332
- if (callbacks[wildcard] && name != wildcard)
333
- { // Trigger the wildcard.
334
- el.trigger.apply(el, ['*', name].concat(args));
335
- }
336
-
337
- });
338
-
339
- return el
340
- });
341
-
342
- if (addis)
122
+ else
343
123
  {
344
- add('isObservable', lock({event: false, target: true}));
345
- }
346
-
347
- if (addme)
348
- { // Add a wrapper for observable() that sets new default options.
349
- add(addme, function (obj=null, mopts)
350
- {
351
- return observable(obj, copy({}, opts, mopts));
352
- });
353
- }
354
-
355
- if (addre)
356
- { // Add a method to change the observable options.
357
- const reinherit = opts.reinherit ?? false;
358
-
359
- add(addre, function(replacementOpts={}, inherit=reinherit)
360
- {
361
- const newOpts
362
- = inherit
363
- ? Object.assign({}, opts, replacementOpts)
364
- : replacementOpts;
365
- return observable(el, replacementOpts, true);
366
- });
124
+ return evob.makeObservable(el, opts, arg3);
367
125
  }
368
-
369
- // Metadata
370
- add('$$observable$$', lock({opts, observable}));
371
-
372
- return el
373
-
374
- } // observable()
126
+ }
375
127
 
376
128
  module.exports = observable;
377
129
 
378
- /**
379
- * See if a value implements the *Observable* interface.
380
- *
381
- * @function module:@lumjs/core/observable.is
382
- * @param {*} obj - The expected object/function to test.
383
- * @returns {boolean}
384
- */
385
- function isObservable(obj)
386
- {
387
- return (isComplex(obj)
388
- && typeof obj.trigger === F
389
- && typeof obj.on === F);
390
- }
391
-
392
- // Add an 'is()' method to `observable` itself.
393
- def(observable, 'is', isObservable);
130
+ def(observable)
131
+ ('is', evob.isObservable)
132
+ ('auto', observable)
133
+ ('orig', orig)
134
+ ('wrap', evob.makeObservable)
394
135
 
395
136
  /**
396
- * Does a value implement the Observable interface?
397
- * @name module:@lumjs/core/types.doesObservable
398
- * @function
399
- * @param {*} v - The expected object/function to test.
137
+ * Does a value implement the observable interface?
138
+ * @function module:@lumjs/core/observable.is
139
+ * @param {*} v - Value to test
400
140
  * @returns {boolean}
401
- * @see module:@lumjs/core/observable.is
141
+ * @see module:@lumjs/core/events/observable.isObservable
402
142
  */
403
143
 
404
144
  /**
405
- * Extension type for the {@link module:@lumjs/core/observable} interface.
406
- * @memberof module:@lumjs/core/types.TYPES
407
- * @member {string} OBSERV - Implements the *Observable* interface.
145
+ * An alias to
146
+ * {@link module:@lumjs/core/events/observable.makeObservable makeObservable()}
147
+ *
148
+ * @function module:@lumjs/core/observable.wrap
149
+ * @param {(object|function)} el
150
+ * @param {object} oo
151
+ * @param {object} ro
152
+ * @returns {object}
408
153
  */
409
-
410
- TYPES.add('OBSERV', 'observable', isObservable, 'doesObservable');
@@ -0,0 +1,291 @@
1
+ // Defining these after observable.
2
+ const {B,F,S,def,isObj,isComplex} = require('../types');
3
+ const {isObservable} = require('../events/observable');
4
+ const lock = Object.freeze;
5
+ const copy = Object.assign;
6
+
7
+ /**
8
+ * The original implementation of the observable API.
9
+ *
10
+ * This older implementation will be removed entirely when `v2.0` is
11
+ * released, and possibly sooner if I don't find anything broken with
12
+ * the compatibility wrapper. For now it's able to be loaded on demand.
13
+ *
14
+ * @param {(object|function)} el - The target to add observable API to.
15
+ *
16
+ * @param {object} [opts]
17
+ * {@link module:@lumjs/core/observable.observable Observable} options.
18
+ *
19
+ * Includes a few additional options no longer supported by the
20
+ * new events-based implementation. Only the options exclusive to this
21
+ * version will be documented here.
22
+ *
23
+ * @param {string} [opts.addre] If set, add a method with this name
24
+ * to the `el` object, which is a function that can re-build the
25
+ * observable API methods with new `opts` replacing the old ones.
26
+ *
27
+ * The method added takes two arguments, the first being an `object`
28
+ * representing the new options to set on the target.
29
+ *
30
+ * The second is an optional `boolean` value that determines if the
31
+ * existing `opts` should be used as defaults for any options not
32
+ * specified in the first argument.
33
+ *
34
+ * @param {boolean} [opts.reinherit=false] Used as the default value of
35
+ * the second argument of the method added by `opts.addre`.
36
+ *
37
+ * @param {boolean} [redefine=false] If `true` allow targets already
38
+ * implementing the `on()` and `trigger()` methods to be re-initialized.
39
+ *
40
+ * Generally only needed if you need to change the `opts` for some reason.
41
+ * This is forced to `true` by the method added by `opts.addre`.
42
+ *
43
+ * @returns {object} el
44
+ * @see module:@lumjs/core/observable~API
45
+ * @alias module:@lumjs/core/observable.orig
46
+ */
47
+ function observable (el={}, opts={}, redefine=false)
48
+ {
49
+ //console.debug("observable", el, opts);
50
+
51
+ if (!isComplex(el))
52
+ { // Don't know how to handle this, sorry.
53
+ throw new Error("non-object sent to observable()");
54
+ }
55
+
56
+ if (isObservable(el) && !redefine)
57
+ { // It's already observable.
58
+ return el;
59
+ }
60
+
61
+ if (typeof opts === B)
62
+ { // Assume it's the wrapthis option.
63
+ opts = {wrapthis: opts};
64
+ }
65
+ else if (!isObj(opts))
66
+ {
67
+ opts = {};
68
+ }
69
+
70
+ const noSpace = /^\S+$/;
71
+
72
+ const wildcard = (typeof opts.wildcard === S
73
+ && noSpace.test(opts.wildcard))
74
+ ? opts.wildcard
75
+ : '*';
76
+
77
+ const wrapsetup = (typeof opts.wrapsetup === F)
78
+ ? opts.wrapsetup
79
+ : null;
80
+
81
+ const wrapthis = (typeof opts.wrapthis === B)
82
+ ? opts.wrapthis
83
+ : false;
84
+
85
+ const wrapargs = (typeof opts.wrapargs === B)
86
+ ? opts.wrapargs
87
+ : (wrapsetup ? !wrapthis : false);
88
+
89
+ const wrapped = (wrapthis || wrapargs);
90
+
91
+ const wraplock = (typeof opts.wraplock === B)
92
+ ? opts.wraplock
93
+ : true;
94
+
95
+ const addname = (typeof opts.addname === B)
96
+ ? opts.addname
97
+ : !wrapped;
98
+
99
+ const addis = (typeof opts.addis === B)
100
+ ? opts.addis
101
+ : wrapped;
102
+
103
+ const validIdent = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
104
+
105
+ const addme = (typeof opts.addme === S
106
+ && validIdent.test(opts.addme))
107
+ ? opts.addme
108
+ : null;
109
+
110
+ const addre = (typeof opts.addre === S
111
+ && validIdent.test(opts.addre))
112
+ ? opts.addre
113
+ : null;
114
+
115
+ const slice = Array.prototype.slice;
116
+
117
+ function onEachEvent (e, fn)
118
+ {
119
+ const es = e.split(/\s+/);
120
+ const me = es.length > 1;
121
+ for (e of es)
122
+ {
123
+ fn(e, me);
124
+ }
125
+ }
126
+
127
+ const add = def(el);
128
+
129
+ function runCallback (name, fn, args)
130
+ {
131
+ if (fn.busy) return;
132
+ fn.busy = 1;
133
+
134
+ let fobj;
135
+
136
+ if (wrapped)
137
+ { // Something is going to use our wrapper object.
138
+ const isWild = (name === wildcard);
139
+ const fname = isWild ? (addname ? args[0] : args.shift()) : name;
140
+
141
+ fobj =
142
+ {
143
+ isObservable: lock({event: true, target: false}),
144
+ self: el,
145
+ target: el,
146
+ name: fname,
147
+ type: fname,
148
+ func: fn,
149
+ wildcard: isWild,
150
+ args,
151
+ };
152
+
153
+ if (wrapsetup)
154
+ {
155
+ wrapsetup.call(el, fobj);
156
+ }
157
+
158
+ if (wraplock)
159
+ {
160
+ lock(fobj);
161
+ }
162
+ }
163
+
164
+ const fthis = wrapthis ? fobj : el;
165
+ const fargs = wrapargs ? [fobj]
166
+ : ((fn.typed && addname) ? [name].concat(args) : args);
167
+
168
+ fn.apply(fthis, fargs);
169
+ fn.busy = 0;
170
+ }
171
+
172
+ let callbacks = {};
173
+
174
+ add('on', function(events, fn)
175
+ {
176
+ if (typeof fn !== F)
177
+ {
178
+ console.error("non-function passed to on()");
179
+ return el;
180
+ }
181
+
182
+ onEachEvent(events, function(name, typed)
183
+ {
184
+ (callbacks[name] = callbacks[name] || []).push(fn);
185
+ fn.typed = typed;
186
+ });
187
+
188
+ return el;
189
+ });
190
+
191
+ add('off', function(events, fn)
192
+ {
193
+ if (events === wildcard && !fn)
194
+ { // Clear all callbacks.
195
+ callbacks = {};
196
+ }
197
+ else
198
+ {
199
+ onEachEvent(events, function(name)
200
+ {
201
+ if (fn)
202
+ { // Find a specific callback to remove.
203
+ var arr = callbacks[name]
204
+ for (var i = 0, cb; cb = arr && arr[i]; ++i)
205
+ {
206
+ if (cb == fn) arr.splice(i--, 1);
207
+ }
208
+ }
209
+ else
210
+ { // Remove all callbacks for this event.
211
+ delete callbacks[name];
212
+ }
213
+ });
214
+ }
215
+ return el
216
+ });
217
+
218
+ add('one', function(events, fn)
219
+ {
220
+ function on()
221
+ {
222
+ el.off(events, on)
223
+ fn.apply(el, arguments)
224
+ }
225
+ return el.on(events, on);
226
+ });
227
+
228
+ add('trigger', function(events)
229
+ {
230
+ // getting the arguments
231
+ // skipping the first one
232
+ const args = slice.call(arguments, 1);
233
+
234
+ onEachEvent(events, function(name)
235
+ {
236
+ const fns = slice.call(callbacks[name] || [], 0);
237
+
238
+ for (var i = 0, fn; fn = fns[i]; ++i)
239
+ {
240
+ runCallback(name, fn, args);
241
+ if (fns[i] !== fn) { i-- }
242
+ }
243
+
244
+ if (callbacks[wildcard] && name != wildcard)
245
+ { // Trigger the wildcard.
246
+ el.trigger.apply(el, ['*', name].concat(args));
247
+ }
248
+
249
+ });
250
+
251
+ return el
252
+ });
253
+
254
+ if (addis)
255
+ {
256
+ add('isObservable', lock({event: false, target: true}));
257
+ }
258
+
259
+ if (addme)
260
+ { // Add a wrapper for observable() that sets new default options.
261
+ add(addme, function (obj=null, mopts)
262
+ {
263
+ return observable(obj, copy({}, opts, mopts));
264
+ });
265
+ }
266
+
267
+ if (addre)
268
+ { // Add a method to change the observable options.
269
+ const reinherit = opts.reinherit ?? false;
270
+
271
+ add(addre, function(replacementOpts={}, inherit=reinherit)
272
+ {
273
+ const newOpts
274
+ = inherit
275
+ ? Object.assign({}, opts, replacementOpts)
276
+ : replacementOpts;
277
+ return observable(el, replacementOpts, true);
278
+ });
279
+ }
280
+
281
+ // Metadata
282
+ add('$$observable$$', lock({opts, observable}));
283
+
284
+ return el
285
+
286
+ } // observable()
287
+
288
+ // Add an 'is()' method to `observable` itself.
289
+ def(observable, 'is', isObservable);
290
+
291
+ module.exports = observable;
package/lib/traits.js CHANGED
@@ -10,13 +10,18 @@ const getProp = require('./obj/getproperty');
10
10
  const
11
11
  {
12
12
  def,F,B,S,
13
- isObj,isArray,isConstructor,isProperty,
13
+ isObj,isArray,isConstructor,isProperty,isClassObject,
14
14
  needObj,needType,
15
15
  } = require('./types');
16
16
 
17
17
  // Symbol for private storage of composed traits.
18
18
  const COMPOSED_TRAITS = Symbol.for('@lumjs/core/traits~ComposedTraits');
19
19
 
20
+ const FLAG_ET = 1; // Flag for ensure target
21
+ const FLAG_ES = 2; // Flag for ensure source
22
+ const FLAGS_S = 0; // Default flags for static mode
23
+ const FLAGS_O = 3; // Default flags for object mode
24
+
20
25
  /**
21
26
  * For when we need a prototype object exclusively.
22
27
  *
@@ -30,7 +35,7 @@ const COMPOSED_TRAITS = Symbol.for('@lumjs/core/traits~ComposedTraits');
30
35
  * If `from` is a `function` then `from.prototype` will be returned.
31
36
  *
32
37
  * @throws {TypeError} If `from` was not a valid value;
33
- * including if it is a `function` but does not have a valid
38
+ * including if it is a function but does not have a valid
34
39
  * prototype object to return.
35
40
  *
36
41
  * @alias module:@lumjs/core/traits.ensureProto
@@ -47,7 +52,42 @@ function ensureProto(from)
47
52
  }
48
53
  else
49
54
  {
50
- throw new TypeError("Invalid class constructor or object instance");
55
+ throw new TypeError("Invalid class constructor or instance object");
56
+ }
57
+ }
58
+
59
+ /**
60
+ * For when we need a constructor function exclusively.
61
+ *
62
+ * @param {(function|object)} from - Target
63
+ *
64
+ * May either be a class constructor `function`, or an `object` instance.
65
+ *
66
+ * @returns {function}
67
+ *
68
+ * If `from` is a constructor function it will be returned as-is.
69
+ * If `from` is an object instance (with a constructor other than `Object`),
70
+ * then the `from.constructor` value will be returned.
71
+ *
72
+ * @throws {TypeError} If `from` was not a valid value;
73
+ * including if it was a function without a prototype,
74
+ * or an object with a constructor of `Object`.
75
+ *
76
+ * @alias module:@lumjs/core/traits.ensureConstructor
77
+ */
78
+ function ensureConstructor(from)
79
+ {
80
+ if (isConstructor(from))
81
+ { // Good to go
82
+ return from;
83
+ }
84
+ else if (isClassObject(from))
85
+ { // An instance of a class (other than `Object` itself)
86
+ return from.constructor;
87
+ }
88
+ else
89
+ {
90
+ throw new TypeError("Invalid class constructor or instance object");
51
91
  }
52
92
  }
53
93
 
@@ -125,13 +165,16 @@ function getComposed(target, source, tryClass=true)
125
165
  composed = target[COMPOSED_TRAITS];
126
166
  }
127
167
 
128
- if (composed && source && composed.has(source))
129
- { // Let's get definitions for a specific trait.
130
- return composed.get(source);
131
- }
132
- else if (composed && !source)
133
- { // No trait specified, but compose rules found.
134
- return composed;
168
+ if (composed)
169
+ {
170
+ if (!source)
171
+ {
172
+ return composed;
173
+ }
174
+ if (composed.has(source))
175
+ { // Let's get definitions for a specific trait.
176
+ return composed.get(source);
177
+ }
135
178
  }
136
179
 
137
180
  if (tryClass && isObj(target))
@@ -224,9 +267,27 @@ function mapComposed(target, source)
224
267
  * or use the {@link module:@lumjs/core/traits.composeFully} function.
225
268
  *
226
269
  * Only useful if `source` is a class constructor (always recommended).
270
+ * The exact behaviour may be customized further with the `.flags` option.
227
271
  *
228
272
  * Default is `false`.
229
273
  *
274
+ * @param {number} [opts.flags] Binary flags for advanced behaviours
275
+ *
276
+ * The default `.flags` value, and the _ensure function_ used depends
277
+ * on the value of the `.static` option:
278
+ *
279
+ * | .static | Default | Ensure function |
280
+ * | ------- | ------- | --------------------------------------------------- |
281
+ * | `false` | `3` | {@link module:@lumjs/core/traits.ensureProto} |
282
+ * | `true` | `0` | {@link module:@lumjs/core/traits.ensureConstructor} |
283
+ *
284
+ * The actual flags are fairly straightforward:
285
+ *
286
+ * | Flag | Description |
287
+ * | ---- | ---------------------------------------------------------------- |
288
+ * | `1` | Pass `target` through _ensure function_ |
289
+ * | `2` | Pass `source` through _ensure function_ |
290
+ *
230
291
  * @param {boolean} [opts.symbols=false] Auto-compose `Symbol` properties?
231
292
  *
232
293
  * Calls `Object.getOwnPropertySymbols()` when generating a list of
@@ -260,8 +321,7 @@ function compose(specTarget, specSource, opts={})
260
321
  = getComposed(specTarget, specSource)
261
322
  ?? mapComposed(specTarget, specSource);
262
323
 
263
- const isStatic = opts.static ?? false;
264
-
324
+ const isStatic = opts.static ?? false;
265
325
  const mapId = isStatic ? 'static' : 'proto';
266
326
 
267
327
  if (composedMaps[mapId])
@@ -270,8 +330,11 @@ function compose(specTarget, specSource, opts={})
270
330
  return composedMaps;
271
331
  }
272
332
 
273
- const target = isStatic ? specTarget : ensureProto(specTarget);
274
- const source = isStatic ? specSource : ensureProto(specSource);
333
+ const flags = parseInt(opts.flags) ?? (isStatic ? FLAGS_S : FLAGS_O);
334
+ const ensure = isStatic ? ensureConstructor : ensureProto;
335
+
336
+ const target = (flags & FLAG_ET) ? specTarget : ensure(specTarget);
337
+ const source = (flags & FLAG_ES) ? specSource : ensure(specSource);
275
338
 
276
339
  let props = opts.props;
277
340
 
@@ -382,7 +445,6 @@ function compose(specTarget, specSource, opts={})
382
445
  composed.set(tprop, cdef);
383
446
  }
384
447
 
385
- composedMaps[mapId] = composed;
386
448
  return composedMaps;
387
449
  }
388
450
 
@@ -642,9 +704,10 @@ class CoreTrait
642
704
  * @param {boolean} info.ok - Will always be `true` initially.
643
705
  *
644
706
  * If an overridden `removeTrait()` method sets this to `false`,
645
- * then the `decomposeFrom()` operation will be cancelled before
646
- * decomposing the trait.
707
+ * then the decomposeFrom() operation will skip the step of
708
+ * actually decomposing the trait.
647
709
  *
710
+ * @returns {*} Return value is not used.
648
711
  */
649
712
  static removeTrait(info)
650
713
  {
@@ -714,7 +777,7 @@ class CoreTrait
714
777
  * @returns {function} The `registerTrait()` function created above.
715
778
  * @alias module:@lumjs/core/traits.makeRegistry
716
779
  */
717
- function makeTraitRegistry(registry)
780
+ function makeTraitRegistry(registry={})
718
781
  {
719
782
  needObj(registry, false, 'invalid trait registry object');
720
783
 
@@ -749,56 +812,10 @@ function makeTraitRegistry(registry)
749
812
  module.exports =
750
813
  {
751
814
  compose, composeFully, getComposed, decompose,
752
- Trait: CoreTrait, IGNORE_STATIC, ensureProto,
815
+ Trait: CoreTrait, IGNORE_STATIC,
816
+ ensureProto, ensureConstructor,
753
817
  makeRegistry: makeTraitRegistry,
754
818
 
755
819
  // Undocumented:
756
820
  hasOwn,
757
821
  }
758
-
759
- /**
760
- * Composed property maps.
761
- *
762
- * @typedef {object} module:@lumjs/core/traits~Composed
763
- *
764
- * @prop {?Map} proto - Map of composed *prototype* properties.
765
- *
766
- * Keys are the `string` or `symbol` for each composed property.
767
- * Values are {@link module:@lumjs/core/traits~ComposedProperty} objects.
768
- *
769
- * If no applicable trait properties are currently composed,
770
- * this will be `null`.
771
- *
772
- * @prop {?Map} static - Map of composed *static* properties.
773
- *
774
- * Exactly the same description as `proto`, but for static properties.
775
- *
776
- * @prop {boolean} forClass - Are these property maps from a class?
777
- *
778
- * Will be `true` if the original target was a `function`, or `false`
779
- * otherwise. Only really useful when trait functions need to be called
780
- * differently depending on if the trait was applied to an individual
781
- * instance or a class constructor.
782
- *
783
- */
784
-
785
- /**
786
- * Composed property definitions.
787
- *
788
- * @typedef {object} module:@lumjs/core/traits~ComposedProperty
789
- *
790
- * @prop {(string|Symbol)} id - The property key on the `target`
791
- * @prop {(string|Symbol)} [srcKey] The property key from the `source`;
792
- * only included if different from `propKey`.
793
- *
794
- * @prop {?object} newDescriptor - The property descriptor to be added;
795
- * will be `null` if the property did not exist.
796
- *
797
- * @prop {object} [oldDescriptor] The replaced property descriptor;
798
- * only included if there was an existing property in the `target`
799
- * and the `opts.overwrite` option was `true`.
800
- *
801
- * @prop {boolean} found - Was the property found in the `source` ?
802
- * @prop {boolean} added - Was the property added to the `target` ?
803
- *
804
- */
@@ -144,6 +144,9 @@ function isIterable(v)
144
144
  /**
145
145
  * Does a value appear to be a class constructor?
146
146
  *
147
+ * For the purposes of this test, a class constructor is
148
+ * any Function that has a `prototype` Object property.
149
+ *
147
150
  * @param {*} v - Value to test
148
151
  * @returns {boolean}
149
152
  * @alias module:@lumjs/core/types/basics.isConstructor
@@ -153,6 +156,35 @@ function isConstructor(v)
153
156
  return (typeof v === F && isObj(v.prototype));
154
157
  }
155
158
 
159
+ /**
160
+ * Is a value a _plain_ object?
161
+ *
162
+ * A plain object is one which has a `constructor` property
163
+ * that **IS** the `globalThis.Object` function.
164
+ *
165
+ * @param {*} v - Value to test
166
+ * @returns {boolean}
167
+ */
168
+ function isPlainObject(v)
169
+ {
170
+ return (isObj(v) && v.constructor === Object);
171
+ }
172
+
173
+ /**
174
+ * Is a value a class instance object?
175
+ *
176
+ * This is a counterpart to isPlainObject() that only returns
177
+ * true if the `constructor` property exists, but is **NOT**
178
+ * the `globalThis.Object` function.
179
+ *
180
+ * @param {*} v - Value to test
181
+ * @returns {boolean}
182
+ */
183
+ function isClassObject(v)
184
+ {
185
+ return (isObj(v) && typeof v.constructor === F && v.constructor !== Object);
186
+ }
187
+
156
188
  /**
157
189
  * See if an object can be used as a valid *descriptor*.
158
190
  *
@@ -246,7 +278,8 @@ module.exports =
246
278
  {
247
279
  isObj, isComplex, isNil, notNil, isScalar, isArray, isTypedArray,
248
280
  isIterable, nonEmptyArray, isArguments, isProperty, isConstructor,
249
- doesDescriptor, doesDescriptorTemplate, JS,
281
+ isPlainObject, isClassObject, doesDescriptor, doesDescriptorTemplate,
282
+ JS,
250
283
  }
251
284
 
252
285
  JS.addTo(module.exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumjs/core",
3
- "version": "1.34.0",
3
+ "version": "1.35.1",
4
4
  "main": "lib/index.js",
5
5
  "exports":
6
6
  {