// Defining these after observable.
const {B,F,S,def,isObj,isComplex,TYPES,console} = require('./types');
const {duplicateAll: clone} = require('./obj/copyall');
const lock = Object.freeze;
/**
* Make an object support the *Observable* API.
*
* Adds `on()`, `off()`, `one()`, and `trigger()` methods.
*
* @param {object} el - The object we are making observable.
* @param {object} [opts] Options that define behaviours.
* @param {string} [opts.wildcard='*'] The event name used as a wildcard.
*
* @param {boolean} [opts.wrapargs=false] If `true`, the event handlers will
* be passed a wrapper object as the sole argument:
*
* ```js
* {
* isObservable:
* { // Immutable;
* event: true, // This is an event data object.
* target: false, // This is not the target object.
* },
* wildcard: bool, // Will be true if this was a wildcard event handler.
* func: function, // The function being called.
* args: array, // The arguments passed to trigger.
* self: el, // The target object.
* name: event, // The event name that was triggered.
* target: el, // An alias to `self`.
* type: event, // An alias to `name`.
* // ...
* }
* ```
*
* @param {boolean} [opts.wrapthis=false] If `true`, `this` in event
* handlers will be the same object as `opts.wrapargs`.
*
* @param {?function} [opts.wrapsetup=null] Setup function for wrapper data.
*
* This function will be called with the target `el` as `this`,
* and will be passed the wrapper object (before it is locked),
* so it can examine the event name and arguments passed to
* `trigger()` and adjust the object accordingly.
*
* This allows the wrapper objects to have a lot more custom properties,
* and feel more like the standard Event objects used by the `DOM`.
*
* If this is specified, but neither `opts.wrapargs` or `opts.wrapthis`
* is `true`, then `opts.wrapargs` will be changed to `true` implicitly.
*
* @param {boolean} [opts.wraplock=true] If `true`, the wrapper object
* will be made immutable using the `Object.freeze()` method.
*
* @param {boolean} [opts.addname] If `true` callbacks with
* multiple events will have the name of the triggered event added as
* the first parameter.
*
* If either `wrapthis` or `wrapargs` are `true`, then this will default
* to `false`, otherwise it will default to `true`.
*
* @param {boolean} [opts.addis] If `true` add a read-only, frozen
* object property named `isObservable` with the value:
*
* ```js
* {
* event: false, // This is not an event data object.
* target: true, // This is the target object.
* }
* ```
*
* If either `wrapthis` or `wrapargs` are `true`, then this will default
* to `true`, otherwise it will default to `false`.
*
* @param {string} [opts.addme] If set, add a method with this name
* to the `el` object, which is a version of `observable()` with the
* default options being the same as the current `opts`.
*
* @param {string} [opts.addre] If set, add a method with this name
* to the `el` object, which is a function that can re-build the
* observable API methods with new `opts` replacing the old ones.
*
* @returns {object} el
*
* @exports module:@lumjs/core/observable
*/
function observable (el={}, opts={})
{
//console.debug("observable", el, opts);
if (!isComplex(el))
{ // Don't know how to handle this, sorry.
throw new Error("non-object sent to observable()");
}
if (observable.is(el))
{ // It's already observable.
return el;
}
if (typeof opts === B)
{ // Assume it's the wrapthis option.
opts = {wrapthis: opts};
}
else if (!isObj(opts))
{
opts = {};
}
const noSpace = /^\S+$/;
const wildcard = (typeof opts.wildcard === S
&& noSpace.test(opts.wildcard))
? opts.wildcard
: '*';
const wrapsetup = (typeof opts.wrapsetup === F)
? opts.wrapsetup
: null;
const wrapthis = (typeof opts.wrapthis === B)
? opts.wrapthis
: false;
const wrapargs = (typeof opts.wrapargs === B)
? opts.wrapargs
: (wrapsetup ? !wrapthis : false);
const wrapped = (wrapthis || wrapargs);
const wraplock = (typeof opts.wraplock === B)
? opts.wraplock
: true;
const addname = (typeof opts.addname === B)
? opts.addname
: !wrapped;
const addis = (typeof opts.addis === B)
? opts.addis
: wrapped;
const validIdent = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
const addme = (typeof opts.addme === S
&& validIdent.test(opts.addme))
? opts.addme
: null;
const addre = (typeof opts.addre === S
&& validIdent.test(opts.addre))
? opts.addre
: null;
const slice = Array.prototype.slice;
function onEachEvent (e, fn)
{
const es = e.split(/\s+/);
const me = es.length > 1;
for (e of es)
{
fn(e, me);
}
}
const add = def(el);
function runCallback (name, fn, args)
{
if (fn.busy) return;
fn.busy = 1;
let fobj;
if (wrapped)
{ // Something is going to use our wrapper object.
const isWild = (name === wildcard);
const fname = isWild ? (addname ? args[0] : args.shift()) : name;
fobj =
{
isObservable: lock({event: true, target: false}),
self: el,
target: el,
name: fname,
type: fname,
func: fn,
wildcard: isWild,
args,
};
if (wrapsetup)
{
wrapsetup.call(el, fobj);
}
if (wraplock)
{
lock(fobj);
}
}
const fthis = wrapthis ? fobj : el;
const fargs = wrapargs ? [fobj]
: ((fn.typed && addname) ? [name].concat(args) : args);
fn.apply(fthis, fargs);
fn.busy = 0;
}
let callbacks = {};
/**
* Assign an event handler
*
* Listen to the given space separated list of `events` and execute
* the `callback` each time an event is triggered.
* @param {string} events - events ids
* @param {function} fn - callback function
* @returns {object} el
*/
add('on', function(events, fn)
{
if (typeof fn !== F)
{
console.error("non-function passed to on()");
return el;
}
onEachEvent(events, function(name, typed)
{
(callbacks[name] = callbacks[name] || []).push(fn);
fn.typed = typed;
});
return el;
});
/**
* Removes the given space separated list of `events` listeners
*
* @param {string} events - events ids
* @param {function} fn - callback function
* @returns {object} el
*/
add('off', function(events, fn)
{
if (events === wildcard && !fn)
{ // Clear all callbacks.
callbacks = {};
}
else
{
onEachEvent(events, function(name)
{
if (fn)
{ // Find a specific callback to remove.
var arr = callbacks[name]
for (var i = 0, cb; cb = arr && arr[i]; ++i)
{
if (cb == fn) arr.splice(i--, 1);
}
}
else
{ // Remove all callbacks for this event.
delete callbacks[name];
}
});
}
return el
});
/**
* Add a one-shot event handler.
*
* Listen to the given space separated list of `events` and execute
* the `callback` at most once.
*
* @param {string} events - events ids
* @param {function} fn - callback function
* @returns {object} el
*/
add('one', function(events, fn)
{
function on()
{
el.off(events, on)
fn.apply(el, arguments)
}
return el.on(events, on);
});
/**
* Execute all callback functions that listen to the given space
* separated list of `events`
* @param {string} events - events ids
* @returns {object} el
*/
add('trigger', function(events)
{
// getting the arguments
// skipping the first one
const args = slice.call(arguments, 1);
onEachEvent(events, function(name)
{
const fns = slice.call(callbacks[name] || [], 0);
for (var i = 0, fn; fn = fns[i]; ++i)
{
runCallback(name, fn, args);
if (fns[i] !== fn) { i-- }
}
if (callbacks[wildcard] && name != wildcard)
{ // Trigger the wildcard.
el.trigger.apply(el, ['*', name].concat(args));
}
});
return el
});
if (addis)
{
add('isObservable', lock({event: false, target: true}));
}
if (addme)
{ // Add a wrapper for observable() that sets new default options.
add(addme, function (obj=null, mopts={})
{
return observable(obj, clone(opts, mopts));
});
}
if (addre)
{ // Add a method to change the observable options.
add(addre, function(opts={})
{
return observable(el, opts);
});
}
// Metadata
add('$$observable$$', lock({opts, observable}));
return el
} // observable()
module.exports = observable;
/**
* See if a value implements the *Observable* interface.
*
* @function module:@lumjs/core/observable.is
* @param {*} obj - The expected object/function to test.
* @returns {boolean}
*/
function isObservable(obj)
{
return (isObj(obj)
&& typeof obj.trigger === F
&& typeof obj.on === F);
}
// Add an 'is()' method to `observable` itself.
def(observable, 'is', isObservable);
/**
* Does a value implement the Observable interface?
* @name module:@lumjs/core/types.doesObservable
* @function
* @param {*} v - The expected object/function to test.
* @returns {boolean}
* @see module:@lumjs/core/observable.is
*/
/**
* Extension type for the {@link module:@lumjs/core/observable} interface.
* @memberof module:@lumjs/core/types.TYPES
* @member {string} OBSERV - Implements the *Observable* interface.
*/
TYPES.add('OBSERV', 'observable', isObservable, 'doesObservable');