mirror of
https://github.com/pkimpel/retro-b5500.git
synced 2026-02-11 10:55:09 +00:00
291 lines
9.3 KiB
JavaScript
291 lines
9.3 KiB
JavaScript
/*
|
|
* ComposeJS, object composition for JavaScript, featuring
|
|
* JavaScript-style prototype inheritance and composition, multiple inheritance,
|
|
* mixin and traits-inspired conflict resolution and composition
|
|
*/
|
|
(function(define){
|
|
"use strict";
|
|
define([], function(){
|
|
// function for creating instances from a prototype
|
|
function Create(){
|
|
}
|
|
var delegate = Object.create ?
|
|
function(proto){
|
|
return Object.create(typeof proto == "function" ? proto.prototype : proto || Object.prototype);
|
|
} :
|
|
function(proto){
|
|
Create.prototype = typeof proto == "function" ? proto.prototype : proto;
|
|
var instance = new Create();
|
|
Create.prototype = null;
|
|
return instance;
|
|
};
|
|
function validArg(arg){
|
|
if(!arg){
|
|
throw new Error("Compose arguments must be functions or objects");
|
|
}
|
|
return arg;
|
|
}
|
|
// this does the work of combining mixins/prototypes
|
|
function mixin(instance, args, i){
|
|
// use prototype inheritance for first arg
|
|
var value, argsLength = args.length;
|
|
for(; i < argsLength; i++){
|
|
var arg = args[i];
|
|
if(typeof arg == "function"){
|
|
// the arg is a function, use the prototype for the properties
|
|
var prototype = arg.prototype;
|
|
for(var key in prototype){
|
|
value = prototype[key];
|
|
var own = prototype.hasOwnProperty(key);
|
|
if(typeof value == "function" && key in instance && value !== instance[key]){
|
|
var existing = instance[key];
|
|
if(value == required){
|
|
// it is a required value, and we have satisfied it
|
|
value = existing;
|
|
}
|
|
else if(!own){
|
|
// if it is own property, it is considered an explicit override
|
|
// TODO: make faster calls on this, perhaps passing indices and caching
|
|
if(isInMethodChain(value, key, getBases([].slice.call(args, 0, i), true))){
|
|
// this value is in the existing method's override chain, we can use the existing method
|
|
value = existing;
|
|
}else if(!isInMethodChain(existing, key, getBases([arg], true))){
|
|
// the existing method is not in the current override chain, so we are left with a conflict
|
|
console.error("Conflicted method " + key + ", final composer must explicitly override with correct method.");
|
|
}
|
|
}
|
|
}
|
|
if(value && value.install && own && !isInMethodChain(existing, key, getBases([arg], true))){
|
|
// apply modifier
|
|
value.install.call(instance, key);
|
|
}else{
|
|
instance[key] = value;
|
|
}
|
|
}
|
|
}else{
|
|
// it is an object, copy properties, looking for modifiers
|
|
for(var key in validArg(arg)){
|
|
var value = arg[key];
|
|
if(typeof value == "function"){
|
|
if(value.install){
|
|
// apply modifier
|
|
value.install.call(instance, key);
|
|
continue;
|
|
}
|
|
if(key in instance){
|
|
if(value == required){
|
|
// required requirement met
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
// add it to the instance
|
|
instance[key] = value;
|
|
}
|
|
}
|
|
}
|
|
return instance;
|
|
}
|
|
// allow for override (by es5 module)
|
|
Compose._setMixin = function(newMixin){
|
|
mixin = newMixin;
|
|
};
|
|
function isInMethodChain(method, name, prototypes){
|
|
// searches for a method in the given prototype hierarchy
|
|
for(var i = 0; i < prototypes.length;i++){
|
|
var prototype = prototypes[i];
|
|
if(prototype[name] == method){
|
|
// found it
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
// Decorator branding
|
|
function Decorator(install, direct){
|
|
function Decorator(){
|
|
if(direct){
|
|
return direct.apply(this, arguments);
|
|
}
|
|
throw new Error("Decorator not applied");
|
|
}
|
|
Decorator.install = install;
|
|
return Decorator;
|
|
}
|
|
Compose.Decorator = Decorator;
|
|
// aspect applier
|
|
function aspect(handler){
|
|
return function(advice){
|
|
return Decorator(function install(key){
|
|
var baseMethod = this[key];
|
|
(advice = this[key] = baseMethod ? handler(this, baseMethod, advice) : advice).install = install;
|
|
}, advice);
|
|
};
|
|
};
|
|
// around advice, useful for calling super methods too
|
|
Compose.around = aspect(function(target, base, advice){
|
|
return advice.call(target, base);
|
|
});
|
|
Compose.before = aspect(function(target, base, advice){
|
|
return function(){
|
|
var results = advice.apply(this, arguments);
|
|
if(results !== stop){
|
|
return base.apply(this, results || arguments);
|
|
}
|
|
};
|
|
});
|
|
var stop = Compose.stop = {};
|
|
var undefined;
|
|
Compose.after = aspect(function(target, base, advice){
|
|
return function(){
|
|
var results = base.apply(this, arguments);
|
|
var adviceResults = advice.apply(this, arguments);
|
|
return adviceResults === undefined ? results : adviceResults;
|
|
};
|
|
});
|
|
|
|
// rename Decorator for calling super methods
|
|
Compose.from = function(trait, fromKey){
|
|
if(fromKey){
|
|
return (typeof trait == "function" ? trait.prototype : trait)[fromKey];
|
|
}
|
|
return Decorator(function(key){
|
|
if(!(this[key] = (typeof trait == "string" ? this[trait] :
|
|
(typeof trait == "function" ? trait.prototype : trait)[fromKey || key]))){
|
|
throw new Error("Source method " + fromKey + " was not available to be renamed to " + key);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Composes an instance
|
|
Compose.create = function(base){
|
|
// create the instance
|
|
var instance = mixin(delegate(base), arguments, 1);
|
|
var argsLength = arguments.length;
|
|
// for go through the arguments and call the constructors (with no args)
|
|
for(var i = 0; i < argsLength; i++){
|
|
var arg = arguments[i];
|
|
if(typeof arg == "function"){
|
|
instance = arg.call(instance) || instance;
|
|
}
|
|
}
|
|
return instance;
|
|
}
|
|
// The required function, just throws an error if not overriden
|
|
function required(){
|
|
throw new Error("This method is required and no implementation has been provided");
|
|
};
|
|
Compose.required = required;
|
|
// get the value of |this| for direct function calls for this mode (strict in ES5)
|
|
|
|
function extend(){
|
|
var args = [this];
|
|
args.push.apply(args, arguments);
|
|
return Compose.apply(0, args);
|
|
}
|
|
// Compose a constructor
|
|
function Compose(base){
|
|
var args = arguments;
|
|
var prototype = (args.length < 2 && typeof args[0] != "function") ?
|
|
args[0] : // if there is just a single argument object, just use that as the prototype
|
|
mixin(delegate(validArg(base)), args, 1); // normally create a delegate to start with
|
|
function Constructor(){
|
|
var instance;
|
|
if(this instanceof Constructor){
|
|
// called with new operator, can proceed as is
|
|
instance = this;
|
|
}else{
|
|
// we allow for direct calls without a new operator, in this case we need to
|
|
// create the instance ourself.
|
|
Create.prototype = prototype;
|
|
instance = new Create();
|
|
}
|
|
// call all the constructors with the given arguments
|
|
for(var i = 0; i < constructorsLength; i++){
|
|
var constructor = constructors[i];
|
|
var result = constructor.apply(instance, arguments);
|
|
if(typeof result == "object"){
|
|
if(result instanceof Constructor){
|
|
instance = result;
|
|
}else{
|
|
for(var j in result){
|
|
if(result.hasOwnProperty(j)){
|
|
instance[j] = result[j];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return instance;
|
|
}
|
|
// create a function that can retrieve the bases (constructors or prototypes)
|
|
Constructor._getBases = function(prototype){
|
|
return prototype ? prototypes : constructors;
|
|
};
|
|
// now get the prototypes and the constructors
|
|
var constructors = getBases(args),
|
|
constructorsLength = constructors.length;
|
|
if(typeof args[args.length - 1] == "object"){
|
|
args[args.length - 1] = prototype;
|
|
}
|
|
var prototypes = getBases(args, true);
|
|
Constructor.extend = extend;
|
|
if(!Compose.secure){
|
|
prototype.constructor = Constructor;
|
|
}
|
|
Constructor.prototype = prototype;
|
|
return Constructor;
|
|
};
|
|
|
|
Compose.apply = function(thisObject, args){
|
|
// apply to the target
|
|
return thisObject ?
|
|
mixin(thisObject, args, 0) : // called with a target object, apply the supplied arguments as mixins to the target object
|
|
extend.apply.call(Compose, 0, args); // get the Function.prototype apply function, call() it to apply arguments to Compose (the extend doesn't matter, just a handle way to grab apply, since we can't get it off of Compose)
|
|
};
|
|
Compose.call = function(thisObject){
|
|
// call() should correspond with apply behavior
|
|
return mixin(thisObject, arguments, 1);
|
|
};
|
|
|
|
function getBases(args, prototype){
|
|
// this function registers a set of constructors for a class, eliminating duplicate
|
|
// constructors that may result from diamond construction for classes (B->A, C->A, D->B&C, then D() should only call A() once)
|
|
var bases = [];
|
|
function iterate(args, checkChildren){
|
|
outer:
|
|
for(var i = 0; i < args.length; i++){
|
|
var arg = args[i];
|
|
var target = prototype && typeof arg == "function" ?
|
|
arg.prototype : arg;
|
|
if(prototype || typeof arg == "function"){
|
|
var argGetBases = checkChildren && arg._getBases;
|
|
if(argGetBases){
|
|
iterate(argGetBases(prototype)); // don't need to check children for these, this should be pre-flattened
|
|
}else{
|
|
for(var j = 0; j < bases.length; j++){
|
|
if(target == bases[j]){
|
|
continue outer;
|
|
}
|
|
}
|
|
bases.push(target);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
iterate(args, true);
|
|
return bases;
|
|
}
|
|
// returning the export of the module
|
|
return Compose;
|
|
});
|
|
})(typeof define != "undefined" ?
|
|
define: // AMD/RequireJS format if available
|
|
function(deps, factory){
|
|
if(typeof module !="undefined"){
|
|
module.exports = factory(); // CommonJS environment, like NodeJS
|
|
// require("./configure");
|
|
}else{
|
|
Compose = factory(); // raw script, assign to Compose global
|
|
}
|
|
});
|