spritzr
Version:
Provides an inheritance/traits/talents library
329 lines (279 loc) • 8.64 kB
JavaScript
/*
* Spritzr.js
* Copyright (c) 2014 Surevine (https://www.surevine.com/)
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
require("./Polyfill");
(function() {
var Spritzr = {
_superFunction : function() {
if (typeof arguments.callee.caller.prototype == "function") {
return arguments.callee.caller.prototype.apply(this, arguments);
}
},
/**
* This is a marker for tracking methods overwritten by talents
*/
_noPreviousMethod : {},
/**
* Extends SubClass with superClass in a single-inheritance model. Note
* that this implements prototype inheritance (like Prototype.js) rather
* than just copying properties (like jQuery). <aside>Not sure how good
* an idea this is, but let's roll with it!</aside>
* <p>
* Also adds a property '<code>$super</code>' to the prototype to
* allow access to the superclass methods.
*
* @param subject
* the sub class to extend
* @param obj
* the super class to extend the sub class with
*/
extend : function(subClass, superClass) {
if (typeof (superClass) == "function") {
// Set up the prototype chain
var prevProto = subClass.prototype;
subClass.prototype = (function() {
var proto = function() {
this.constructor = subClass;
this.$_spritzr_superClass = superClass;
// Copy any existing prototype properties across
for ( var i in prevProto) {
if (prevProto.hasOwnProperty(i)) {
this[i] = prevProto[i];
}
}
// Set up the faux "$super" reference
var $super = function() {
// Cache a reference to the real object while we can
if (this instanceof subClass) {
$super.$_spritzr_instance = this;
}
var superConstructor;
if(arguments.callee.caller.prototype.$_spritzr_superClass) {
superConstructor = arguments.callee.caller.prototype.$_spritzr_superClass;
} else if(Object.getPrototypeOf) {
superConstructor = Object.getPrototypeOf(arguments.callee.caller.prototype).constructor;
} else if(arguments.callee.caller.prototype.__proto__) {
superConstructor = arguments.callee.caller.prototype.__proto__.constructor;
}
if (typeof superConstructor == "function") {
return superConstructor.apply(this, arguments);
}
};
// And copy the superclass prototype functions across
for ( var i in superClass.prototype) {
if (typeof superClass.prototype[i] == "function") {
$super[i] = function() {
var t;
if ($super.$_spritzr_instance && (this == $super)) {
t = $super.$_spritzr_instance;
} else {
t = this;
}
return superClass.prototype[i].apply(t,
arguments);
};
}
}
;
this.$super = $super;
};
proto.prototype = superClass.prototype;
var p = new proto();
return p;
})();
// And then
var spritz = Spritzr._getSpritzrVarCreate(subClass);
spritz.parent = superClass;
} else {
throw new Error("Can only extend 'classes'");
}
},
/**
* Spritzes (mixes in) an existing class or instance with the given
* object.<br />
* If subject is a class/function then obj will become a 'trait' and
* apply to the class.<br />
* If subject is an instance then obj becomes a talent and will only
* apply to this one instance.
*/
spritz : function(subject, obj) {
if (typeof (subject) == "function") {
var spritz = Spritzr._getSpritzrVarCreate(subject);
if (typeof (obj) == "function") {
for ( var key in obj.prototype) {
subject.prototype[key] = obj.prototype[key];
}
} else {
for ( var key in obj) {
subject.prototype[key] = obj[key];
}
}
spritz.traits.push(obj);
} else {
var spritz = Spritzr._getSpritzrVarCreate(subject);
var getPropsFrom, clazz;
if (typeof (obj) == "function") {
getPropsFrom = obj.prototype;
clazz = obj;
} else {
getPropsFrom = obj;
clazz = obj.constructor;
}
for ( var key in getPropsFrom) {
if (typeof spritz.removed[key] == "undefined") {
if ((typeof subject[key] !== "undefined")
&& subject.hasOwnProperty(key)) {
spritz.removed[key] = subject[key];
} else {
spritz.removed[key] = Spritzr._noPreviousMethod;
}
}
subject[key] = getPropsFrom[key];
}
if (typeof (obj) == "function") {
obj.call(subject); // Call the constructor with the subject
// as 'this'
}
spritz.talents.push(obj);
spritz.talentClasses.push(clazz);
}
},
/**
* Does the opposite of <code>spritz()</code>, although currently
* only removes talents from instances (does not work on traits on
* classes).
*
* @param subject
* @param obj
*/
unspritz : function(subject, obj) {
if (typeof (subject) != "function") {
var spritz = Spritzr._getSpritzrVar(subject);
if (spritz) {
var i = spritz.talentClasses.indexOf(obj);
if (i > -1) {
spritz.talents.splice(i, 1);
spritz.talentClasses.splice(i, 1);
}
}
// Go through all the talent properties and reinstate existing
// methods
for (key in obj.prototype) {
// Check if the defined method is actually the trait method
if (subject[key] === obj.prototype[key]) {
var found = false;
for (var i = spritz.talents.length - 1; i >= 0; --i) {
var talent = spritz.talents[i];
if (typeof talent.prototype[key] !== "undefined") {
subject[key] = talent.prototype[key];
found = true;
break;
}
}
if (!found) {
if (typeof spritz.removed[key] !== 'undefined') {
if (spritz.removed[key] === Spritzr._noPreviousMethod) {
// The object previously didn't have the
// method defined.
delete subject[key];
} else {
subject[key] = spritz.removed[key];
}
delete spritz.removed[key];
} else {
delete subject[key];
}
}
}
}
}
},
/**
* Returns <code>true</code> if: <br>
* <ul>
* <li><code>instance instanceof type</code> is <code>true</code>
* <li>instance has the given type as a class trait
* <li>instance has the given type as an instance talent
* </ul>
*
* @param instance
* the instance to test
* @param type
* the type to test <code>subject</code>
*/
isa : function(instance, type) {
if (instance instanceof type) {
return true;
}
var spritz = Spritzr._getSpritzrVar(instance);
if (spritz && (spritz.talentClasses.indexOf(type) > -1)) {
return true;
}
return Spritzr.inherits(instance.constructor, type);
},
/**
* Checks whether the given class has inherited the given type (either
* as a super class or a trait)
*
* @param clazz
* the class to test
* @param type
* the type to test against
*/
inherits : function(clazz, type) {
// TODO Add cycle checking
if (clazz === type) {
return true;
}
var spritz = Spritzr._getSpritzrVar(clazz);
// If it's not been spritzed then we won't do anything
if (!spritz) {
return false;
}
if (spritz.parent === type) {
return true;
}
// If we have that trait
if (spritz.traits.indexOf(type) > -1) {
return true;
}
// If the parent inherits it
if (spritz.parent && Spritzr.inherits(spritz.parent, type)) {
return true;
}
// If one of the traits inherits it
for ( var i in spritz.traits) {
if (Spritzr.inherits(spritz.traits[i], type)) {
return true;
}
}
return false;
},
_getSpritzrVarCreate : function(obj) {
if (!obj._$spritz) {
obj._$spritz = {
parent : null,
traits : [],
talents : [], // The actual talent instances
talentClasses : [],
removed : {}
};
}
return obj._$spritz;
},
_getSpritzrVar : function(obj) {
return obj._$spritz;
}
};
module.exports = Spritzr;
})();