blob: 5d1bf7e9d3605a72dcf487bc4dbd1b5966298ef1 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
var $ = require('./jquery');
/**
* <p>Plays a similar role to other npm private encapsulation facilities, but
* exposes private members on `this` via per-instance bindings. A class
* definition can contain the following members:
* <ul>
* <li><code>init</code>: constructor/initializer function for an instance. It
* will be called when the class is instantiated via <code>new</code>. Fields
* can be initialized in this function. Private functions and events can also
* be defined within this function.
* <li><code>privates</code>: map of private functions or private static
* constants, with access to other members via <code>this</code>. These
* members are not publicly visible. This is equivalent to associating these
* members explicitly within <code>init</code>.
* <li><code>publics</code>: map of public functions, with access to other
* members via <code>this</code>. These members are publicly visible.
* <li><code>constants</code>: list of names of instance constants initialized
* in <code>init</code> to be exposed.
* <li><code>statics</code>: map of public static constants, accessible from
* the private context, the public context, and on the constructor function.
* <li><code>events</code>: list of event names, some of which can actually be
* a singleton map with the event name and a string of flags, or a map of
* event names to flags. Flags are those to
* <a href="https://api.jquery.com/jQuery.Callbacks/">jQuery Callbacks</a>,
* plus the "private" flag, which hides the event from the public interface
* entirely, and the "public" flag, which exposes the event trigger to the
* public interface.
* </ul>
*
* <p>Furthermore, all functions and events are thus bound statically to the
* appropriate instance, and so can be passed as callbacks without ad-hoc
* proxying/binding.
*
* <p>Care should be taken not to be tempted to declare instance constants
* within <code>private</code>, as any instantiations done on the initial
* values is done at class definition time rather than class instantiation
* time. (As such, using that mechanism to declare private static constants does
* work.)
*/
module.exports = defineClass;
function defineClass(def) {
var constructor = function() {
var ifc = this;
var pthis = $.extend({
ifc: ifc //expose reflexive public interface for private use
},
//extend in inverse precedence
def.statics);
if (def.publics) {
polyBind(pthis, pthis, def.publics, false);
}
if (def.privates) {
polyBind(pthis, pthis, def.privates, false);
}
if (def.events) {
if ($.isArray(def.events)) {
$.each(def.events, function(i, event) {
if ($.type(event) === 'string') {
pthis[event] = defineEvent(ifc, event);
} else {
defineEventsFromObject(pthis, ifc, event);
}
});
} else {
defineEventsFromObject(pthis, ifc, def.events);
}
}
if (def.statics) {
$.extend(ifc, def.statics);
}
if (def.publics) {
polyBind(ifc, pthis, def.publics, true);
}
if (def.init) {
def.init.apply(pthis, arguments);
}
if (def.constants) {
$.each(def.constants, function(i, constant) {
ifc[constant] = pthis[constant];
});
}
};
if (def.statics) {
$.extend(constructor, def.statics);
}
// The function bodies aren't actually useful but the function objects provide
// useful reflective properties.
constructor.ifc = def.publics;
return constructor;
}
defineClass.innerClass = function(def) {
var init = def.init;
def.init = function(outer, constructorArgs) {
this.outer = outer;
init.apply(this, constructorArgs);
};
var InnerClass = defineClass(def);
return function() {
return new InnerClass(this, arguments);
};
};
/**
* Decorates a member function with like-signatured functions to be called
* before and/or after the main invocation.
*/
defineClass.decorate = function(context, name, before, after) {
var proto = context[name];
context[name] = function() {
if (before) {
before.apply(context, arguments);
}
var ret = proto.apply(context, arguments);
if (after) {
after.apply(context, arguments);
}
return ret;
};
};
/**
* Late-bind proxies to maximize flexibility at negligible performance cost.
*/
function lateBind(context, name) {
return function() {
return context[name].apply(context, arguments);
};
}
function polyBind(proxy, context, members, lateBinding) {
$.each(members, $.isArray(members)?
function() {
proxy[this] =
lateBinding? lateBind(context, this) : this.bind(context);
} :
function(name, member) {
proxy[name] =
lateBinding? lateBind(context, name) : member.bind(context);
});
return proxy;
}
/**
* Replaces "this" returns with proxy.
*/
function polyReflexiveLateBind(proxy, context, members) {
$.each(members, function(i, name) {
proxy[name] = function() {
context[name].apply(context, arguments);
return proxy;
};
});
return proxy;
}
defineClass.event = defineEvent;
function defineEvent(ifc, name, flags) {
var dispatcher = $.Callbacks(flags);
//Use polyBind on function that fires to add the callable syntactic sugar
var callableDispatcher = polyBind(function() {
dispatcher.fireWith.call(dispatcher, ifc, arguments);
}, dispatcher, dispatcher, false);
if (!(flags && flags.indexOf('private') > -1)) {
if (flags && flags.indexOf('public') > -1) {
ifc[name] = callableDispatcher;
} else {
var publicEvent = {};
/* We'll want the context to actually be callableDispatcher even though
* the interface and functionality of dispatcher suffice so that we can
* late-bind to the instance exposed to private this. */
polyBind(publicEvent, callableDispatcher,
['disabled', 'fired', 'has', 'locked'], true);
polyReflexiveLateBind(publicEvent, callableDispatcher,
['add', 'disable', 'empty', 'lock', 'remove']);
ifc[name] = publicEvent;
}
}
return callableDispatcher;
}
function defineEventsFromObject(pthis, ifc, events) {
$.each(events, function(event, flags) {
pthis[event] = defineEvent(ifc, event, flags);
});
}