/** * gamecore.js - Copyright 2012 Playcraft Labs, Inc. (see licence.txt) * class.js * Classes and objects */ /** * @Class * A modified version of class.js to cater to static inheritance and deep object cloning * Based almost completely on class.js (Javascript MVC -- Justin Meyer, Brian Moschel, Michael Mayer and others) * (http://javascriptmvc.com/contribute.html) * Some portions adapted from Prototype JavaScript framework, version 1.6.0.1 (c) 2005-2007 Sam Stephenson *

* Class system for javascript *

* * var Fighter = gamecore.Base.extend('Fighter', * { * // static (this is inherited as well) * firingSpeed: 1000 * }, * { * // instance * * hp: 0, * lastFireTime: 0, * * init: function(hp) * { * this.hp = hp; * }, * * fire: function() * { * this._super(); // super methods! * * // do firing! * } * }); * * var gunship = new Fighter(100); * * * Introspection: * * gamecore.Base.extend(‘Fighter.Gunship’); * Fighter.Gunship.shortName; // ‘Gunship’ * Fighter.Gunship.fullName; // ‘Fighter.Gunship’ * Fighter.Gunship.namespace; // ‘Fighter’ * *

* Setup method will be called prior to any init -- nice if you want to do things without needing the * users to call _super in the init, as well as for normalizing parameters. * * setup: function() * { * this.objectId = this.Class.totalObjects++; * this.uniqueId = this.Class.fullName + ':' + this.objectId; * } * */ // compatible with jquery classing (function ($) { var regs = { undHash: /_|-/, colons: /::/, words: /([A-Z]+)([A-Z][a-z])/g, lowUp: /([a-z\d])([A-Z])/g, dash: /([a-z\d])([A-Z])/g, replacer: /\{([^\}]+)\}/g, dot: /\./ }, getNext = function (current, nextPart, add) { return current[nextPart] || ( add && (current[nextPart] = {}) ); }, isContainer = function (current) { var type = typeof current; return type && ( type == 'function' || type == 'object' ); }, getObject = function (objectName, roots, add) { var parts = objectName ? objectName.split(regs.dot) : [], length = parts.length, currents = $.isArray(roots) ? roots : [roots || window], current, ret, i, c = 0, type; if (length == 0) { return currents[0]; } while (current = currents[c++]) { for (i = 0; i < length - 1 && isContainer(current); i++) { current = getNext(current, parts[i], add); } if (isContainer(current)) { ret = getNext(current, parts[i], add); if (ret !== undefined) { if (add === false) { delete current[parts[i]]; } return ret; } } } }, /** * @class jQuery.String * * A collection of useful string helpers. * */ str = $.String = $.extend($.String || {}, { /** * @function * Gets an object from a string. * @param {String} name the name of the object to look for * @param {Array} [roots] an array of root objects to look for the name * @param {Boolean} [add] true to add missing objects to * the path. false to remove found properties. undefined to * not modify the root object */ getObject: getObject, /** * Capitalizes a string * @param {String} s the string. * @return {String} a string with the first character capitalized. */ capitalize: function (s, cache) { return s.charAt(0).toUpperCase() + s.substr(1); }, /** * Capitalizes a string from something undercored. Examples: * @codestart * jQuery.String.camelize("one_two") //-> "oneTwo" * "three-four".camelize() //-> threeFour * @codeend * @param {String} s * @return {String} a the camelized string */ camelize: function (s) { s = str.classize(s); return s.charAt(0).toLowerCase() + s.substr(1); }, /** * Like camelize, but the first part is also capitalized * @param {String} s * @return {String} the classized string */ classize: function (s, join) { var parts = s.split(regs.undHash), i = 0; for (; i < parts.length; i++) { parts[i] = str.capitalize(parts[i]); } return parts.join(join || ''); }, /** * Like [jQuery.String.classize|classize], but a space separates each 'word' * @codestart * jQuery.String.niceName("one_two") //-> "One Two" * @codeend * @param {String} s * @return {String} the niceName */ niceName: function (s) { return str.classize(s, ' '); }, /** * Underscores a string. * @codestart * jQuery.String.underscore("OneTwo") //-> "one_two" * @codeend * @param {String} s * @return {String} the underscored string */ underscore: function (s) { return s.replace(regs.colons, '/').replace(regs.words, '$1_$2').replace(regs.lowUp, '$1_$2').replace(regs.dash, '_').toLowerCase(); }, /** * Returns a string with {param} replaced values from data. * * $.String.sub("foo {bar}",{bar: "far"}) * //-> "foo far" * * @param {String} s The string to replace * @param {Object} data The data to be used to look for properties. If it's an array, multiple * objects can be used. * @param {Boolean} [remove] if a match is found, remove the property from the object */ sub: function (s, data, remove) { var obs = []; obs.push(s.replace(regs.replacer, function (whole, inside) { //convert inside to type var ob = getObject(inside, data, typeof remove == 'boolean' ? !remove : remove), type = typeof ob; if ((type === 'object' || type === 'function') && type !== null) { obs.push(ob); return ""; } else { return "" + ob; } })); return obs.length <= 1 ? obs[0] : obs; } }); })(jQuery); (function ($) { // if we are initializing a new class var initializing = false, makeArray = $.makeArray, isFunction = $.isFunction, isArray = $.isArray, extend = $.extend, /** * */ cloneObject = function(object) { if (!object || typeof(object) != 'object') return object; // special case handling of array (deep copy them) if (object instanceof Array) { var clone = []; for (var c = 0; c < object.length; c++) clone[c] = cloneObject(object[c]); return clone; } else // otherwise, it's a normal object, clone it's properties { var cloneObj = {}; for (var prop in object) cloneObj[prop] = cloneObject(object[prop]); return cloneObj; } }, concatArgs = function (arr, args) { return arr.concat(makeArray(args)); }, // tests if we can get super in .toString() fnTest = /xyz/.test(function () { xyz; }) ? /\b_super\b/ : /.*/, // overwrites an object with methods, sets up _super // newProps - new properties // oldProps - where the old properties might be // addTo - what we are adding to inheritProps = function (newProps, oldProps, addTo) { addTo = addTo || newProps for (var name in newProps) { // Check if we're overwriting an existing function addTo[name] = isFunction(newProps[name]) && isFunction(oldProps[name]) && fnTest.test(newProps[name]) ? (function (name, fn) { return function () { var tmp = this._super, ret; // Add a new ._super() method that is the same method but on the super-class this._super = oldProps[name]; // The method only need to be bound temporarily, so we remove it when we're done executing ret = fn.apply(this, arguments); this._super = tmp; return ret; }; })(name, newProps[name]) : newProps[name]; } }, /** * @class jQuery.Class * @plugin jquery/class * @tag core * @download dist/jquery/jquery.class.js * @test jquery/class/qunit.html * * Class provides simulated inheritance in JavaScript. Use clss to bridge the gap between * jQuery's functional programming style and Object Oriented Programming. It * is based off John Resig's [http://ejohn.org/blog/simple-javascript-inheritance/|Simple Class] * Inheritance library. Besides prototypal inheritance, it includes a few important features: * * - Static inheritance * - Introspection * - Namespaces * - Setup and initialization methods * - Easy callback function creation * * * ## Static v. Prototype * * Before learning about Class, it's important to * understand the difference between * a class's __static__ and __prototype__ properties. * * //STATIC * MyClass.staticProperty //shared property * * //PROTOTYPE * myclass = new MyClass() * myclass.prototypeMethod() //instance method * * A static (or class) property is on the Class constructor * function itself * and can be thought of being shared by all instances of the * Class. Prototype propertes are available only on instances of the Class. * * ## A Basic Class * * The following creates a Monster class with a * name (for introspection), static, and prototype members. * Every time a monster instance is created, the static * count is incremented. * * @codestart * $.Class.extend('Monster', * /* @static *| * { * count: 0 * }, * /* @prototype *| * { * init: function( name ) { * * // saves name on the monster instance * this.name = name; * * // sets the health * this.health = 10; * * // increments count * this.Class.count++; * }, * eat: function( smallChildren ){ * this.health += smallChildren; * }, * fight: function() { * this.health -= 2; * } * }); * * hydra = new Monster('hydra'); * * dragon = new Monster('dragon'); * * hydra.name // -> hydra * Monster.count // -> 2 * Monster.shortName // -> 'Monster' * * hydra.eat(2); // health = 12 * * dragon.fight(); // health = 8 * * @codeend * * * Notice that the prototype init function is called when a new instance of Monster is created. * * * ## Inheritance * * When a class is extended, all static and prototype properties are available on the new class. * If you overwrite a function, you can call the base class's function by calling * this._super. Lets create a SeaMonster class. SeaMonsters are less * efficient at eating small children, but more powerful fighters. * * * Monster.extend("SeaMonster",{ * eat: function( smallChildren ) { * this._super(smallChildren / 2); * }, * fight: function() { * this.health -= 1; * } * }); * * lockNess = new SeaMonster('Lock Ness'); * lockNess.eat(4); //health = 12 * lockNess.fight(); //health = 11 * * ### Static property inheritance * * You can also inherit static properties in the same way: * * $.Class.extend("First", * { * staticMethod: function() { return 1;} * },{}) * * First.extend("Second",{ * staticMethod: function() { return this._super()+1;} * },{}) * * Second.staticMethod() // -> 2 * * ## Namespaces * * Namespaces are a good idea! We encourage you to namespace all of your code. * It makes it possible to drop your code into another app without problems. * Making a namespaced class is easy: * * @codestart * $.Class.extend("MyNamespace.MyClass",{},{}); * * new MyNamespace.MyClass() * @codeend *

Introspection

* Often, it's nice to create classes whose name helps determine functionality. Ruby on * Rails's [http://api.rubyonrails.org/classes/ActiveRecord/Base.html|ActiveRecord] ORM class * is a great example of this. Unfortunately, JavaScript doesn't have a way of determining * an object's name, so the developer must provide a name. Class fixes this by taking a String name for the class. * @codestart * $.Class.extend("MyOrg.MyClass",{},{}) * MyOrg.MyClass.shortName //-> 'MyClass' * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' * @codeend * The fullName (with namespaces) and the shortName (without namespaces) are added to the Class's * static properties. * * *

Setup and initialization methods

*

* Class provides static and prototype initialization functions. * These come in two flavors - setup and init. * Setup is called before init and * can be used to 'normalize' init's arguments. *

*
PRO TIP: Typically, you don't need setup methods in your classes. Use Init instead. * Reserve setup methods for when you need to do complex pre-processing of your class before init is called. * *
* @codestart * $.Class.extend("MyClass", * { * setup: function() {} //static setup * init: function() {} //static constructor * }, * { * setup: function() {} //prototype setup * init: function() {} //prototype constructor * }) * @codeend * *

Setup

*

Setup functions are called before init functions. Static setup functions are passed * the base class followed by arguments passed to the extend function. * Prototype static functions are passed the Class constructor function arguments.

*

If a setup function returns an array, that array will be used as the arguments * for the following init method. This provides setup functions the ability to normalize * arguments passed to the init constructors. They are also excellent places * to put setup code you want to almost always run.

*

* The following is similar to how [jQuery.Controller.prototype.setup] * makes sure init is always called with a jQuery element and merged options * even if it is passed a raw * HTMLElement and no second parameter. *

* @codestart * $.Class.extend("jQuery.Controller",{ * ... * },{ * setup: function( el, options ) { * ... * return [$(el), * $.extend(true, * this.Class.defaults, * options || {} ) ] * } * }) * @codeend * Typically, you won't need to make or overwrite setup functions. *

Init

* *

Init functions are called after setup functions. * Typically, they receive the same arguments * as their preceding setup function. The Foo class's init method * gets called in the following example: *

* @codestart * $.Class.Extend("Foo", { * init: function( arg1, arg2, arg3 ) { * this.sum = arg1+arg2+arg3; * } * }) * var foo = new Foo(1,2,3); * foo.sum //-> 6 * @codeend *

Callbacks

*

Similar to jQuery's proxy method, Class provides a * [jQuery.Class.static.callback callback] * function that returns a callback to a method that will always * have * this set to the class or instance of the class. *

* The following example uses this.callback to make sure * this.name is available in show. * @codestart * $.Class.extend("Todo",{ * init: function( name ) { this.name = name } * get: function() { * $.get("/stuff",this.callback('show')) * }, * show: function( txt ) { * alert(this.name+txt) * } * }) * new Todo("Trash").get() * @codeend *

Callback is available as a static and prototype method.

* *

Typing

* Classes are automatically populating with three type related components: * * _types: a variable that contains an array of types of this class (extends history) * _fullTypeName: a string representation of the extends hierarchy * isA(string): a function you can call which will return true if the class is of a given type string. *

* Example: *

* Animal.extend('Tiger', {}, {}); * Tiger._types; // ['Animal', 'Tiger'] * Tiger._fullTypeName; // 'Animal | Tiger |" * Tiger.isA('Animal'); // true *

* @constructor Creating a new instance of an object that has extended jQuery.Class * calls the init prototype function and returns a new instance of the class. * */ clss = $.Class = function () { if (arguments.length) { return clss.extend.apply(clss, arguments); } }; /* @Static*/ extend(clss, { /** * @function callback * Returns a callback function for a function on this Class. * The callback function ensures that 'this' is set appropriately. * @codestart * $.Class.extend("MyClass",{ * getData: function() { * this.showing = null; * $.get("data.json",this.callback('gotData'),'json') * }, * gotData: function( data ) { * this.showing = data; * } * },{}); * MyClass.showData(); * @codeend *

Currying Arguments

* Additional arguments to callback will fill in arguments on the returning function. * @codestart * $.Class.extend("MyClass",{ * getData: function( callback ) { * $.get("data.json",this.callback('process',callback),'json'); * }, * process: function( callback, jsonData ) { //callback is added as first argument * jsonData.processed = true; * callback(jsonData); * } * },{}); * MyClass.getData(showDataFunc) * @codeend *

Nesting Functions

* Callback can take an array of functions to call as the first argument. When the returned callback function * is called each function in the array is passed the return value of the prior function. This is often used * to eliminate currying initial arguments. * @codestart * $.Class.extend("MyClass",{ * getData: function( callback ) { * //calls process, then callback with value from process * $.get("data.json",this.callback(['process2',callback]),'json') * }, * process2: function( type,jsonData ) { * jsonData.processed = true; * return [jsonData]; * } * },{}); * MyClass.getData(showDataFunc); * @codeend * @param {String|Array} fname If a string, it represents the function to be called. * If it is an array, it will call each function in order and pass the return value of the prior function to the * next function. * @return {Function} the callback function. */ callback: function (funcs) { //args that should be curried var args = makeArray(arguments), self; funcs = args.shift(); if (!isArray(funcs)) { funcs = [funcs]; } self = this; return function class_cb() { var cur = concatArgs(args, arguments), isString, length = funcs.length, f = 0, func; for (; f < length; f++) { func = funcs[f]; if (!func) continue; isString = typeof func == "string"; if (isString && self._set_called) self.called = func; cur = (isString ? self[func] : func).apply(self, cur || []); if (f < length - 1) cur = !isArray(cur) || cur._use_call ? [cur] : cur } return cur; } }, /** * @function getObject * Gets an object from a String. * If the object or namespaces the string represent do not * exist it will create them. * @codestart * Foo = {Bar: {Zar: {"Ted"}}} * $.Class.getobject("Foo.Bar.Zar") //-> "Ted" * @codeend * @param {String} objectName the object you want to get * @param {Object} [current=window] the object you want to look in. * @return {Object} the object you are looking for. */ getObject: $.String.getObject, /** * @function newInstance * Creates a new instance of the class. This method is useful for creating new instances * with arbitrary parameters. *

Example

* @codestart * $.Class.extend("MyClass",{},{}) * var mc = MyClass.newInstance.apply(null, new Array(parseInt(Math.random()*10,10)) * @codeend * @return {class} instance of the class */ newInstance: function () { var inst = this.rawInstance(); var args; if (inst.setup) args = inst.setup.apply(inst, arguments); // Added by martin@playcraftlabs.com -- fix for deep cloning of properties for (var prop in inst.__proto__) inst[prop] = cloneObject(inst[prop]); if (inst.init) inst.init.apply(inst, isArray(args) ? args : arguments); return inst; }, /** * Setup gets called on the inherting class with the base class followed by the * inheriting class's raw properties. * * Setup will deeply extend a static defaults property on the base class with * properties on the base class. For example: * * $.Class("MyBase",{ * defaults : { * foo: 'bar' * } * },{}) * * MyBase("Inheriting",{ * defaults : { * newProp : 'newVal' * } * },{} * * Inheriting.defaults -> {foo: 'bar', 'newProp': 'newVal'} * * @param {Object} baseClass the base class that is being inherited from * @param {String} fullName the name of the new class * @param {Object} staticProps the static properties of the new class * @param {Object} protoProps the prototype properties of the new class */ setup: function (baseClass, fullName) { this.defaults = extend(true, {}, baseClass.defaults, this.defaults); if (this._types == undefined) this._types = []; this._types.push(this.fullName); if (this._fullTypeName == undefined) this._fullTypeName = '|'; this._fullTypeName += this.fullName + '|'; return arguments; }, rawInstance: function () { initializing = true; var inst = new this(); initializing = false; return inst; }, /** * Extends a class with new static and prototype functions. There are a variety of ways * to use extend: * @codestart * //with className, static and prototype functions * $.Class.extend('Task',{ STATIC },{ PROTOTYPE }) * //with just classname and prototype functions * $.Class.extend('Task',{ PROTOTYPE }) * //With just a className * $.Class.extend('Task') * @codeend * @param {String} [fullName] the classes name (used for classes w/ introspection) * @param {Object} [klass] the new classes static/class functions * @param {Object} [proto] the new classes prototype functions * @return {jQuery.Class} returns the new class */ extend: function (fullName, klass, proto) { // figure out what was passed if (typeof fullName != 'string') { proto = klass; klass = fullName; fullName = null; } if (!proto) { proto = klass; klass = null; } proto = proto || {}; var _super_class = this, _super = this.prototype, name, shortName, namespace, prototype; // append the isA function this.isA = function(typeName) { return this._fullTypeName.indexOf('|'+typeName+'|') != -1; }; // Instantiate a base class (but only create the instance, // don't run the init constructor) initializing = true; prototype = new this(); initializing = false; // Copy the properties over onto the new prototype inheritProps(proto, _super, prototype); // The dummy class constructor function Class() { // All construction is actually done in the init method if (initializing) return; if (this.constructor !== Class && arguments.length) { //we are being called w/o new return arguments.callee.extend.apply(arguments.callee, arguments) } else { //we are being called w/ new // copy objects return this.Class.newInstance.apply(this.Class, arguments) } } // Copy old stuff onto class for (name in this) if (this.hasOwnProperty(name)) Class[name] = cloneObject(this[name]); // copy new props on class inheritProps(klass, this, Class); // do namespace stuff if (fullName) { var parts = fullName.split(/\./); var shortName = parts.pop(); // Martin Wells (playcraft): bug fix. Don't add a namespace object if the class name // has no namespace elements (i.e. it's just "MyClass", not "MyProject.MyClass") if (parts.length > 0) { current = clss.getObject(parts.join('.'), window, true), namespace = current; } current[shortName] = Class; } // set things that can't be overwritten extend(Class, { prototype: prototype, namespace: namespace, shortName: shortName, constructor: Class, fullName: fullName }); //make sure our prototype looks nice Class.prototype.Class = Class.prototype.constructor = Class; /** * @attribute fullName * The full name of the class, including namespace, provided for introspection purposes. * @codestart * $.Class.extend("MyOrg.MyClass",{},{}) * MyOrg.MyClass.shortName //-> 'MyClass' * MyOrg.MyClass.fullName //-> 'MyOrg.MyClass' * @codeend */ var args = Class.setup.apply(Class, concatArgs([_super_class], arguments)); if (Class.init) { Class.init.apply(Class, args || []); } /* @Prototype*/ return Class; /** * @function setup * If a setup method is provided, it is called when a new * instances is created. It gets passed the same arguments that * were given to the Class constructor function ( new Class( arguments ... )). * * $.Class("MyClass", * { * setup: function( val ) { * this.val = val; * } * }) * var mc = new MyClass("Check Check") * mc.val //-> 'Check Check' * * Setup is called before [jQuery.Class.prototype.init init]. If setup * return an array, those arguments will be used for init. * * $.Class("jQuery.Controller",{ * setup : function(htmlElement, rawOptions){ * return [$(htmlElement), * $.extend({}, this.Class.defaults, rawOptions )] * } * }) * *
PRO TIP: * Setup functions are used to normalize constructor arguments and provide a place for * setup code that extending classes don't have to remember to call _super to * run. *
* * Setup is not defined on $.Class itself, so calling super in inherting classes * will break. Don't do the following: * * $.Class("Thing",{ * setup : function(){ * this._super(); // breaks! * } * }) * * @return {Array|undefined} If an array is return, [jQuery.Class.prototype.init] is * called with those arguments; otherwise, the original arguments are used. */ //break up /** * @function init * If an init method is provided, it gets called when a new instance * is created. Init gets called after [jQuery.Class.prototype.setup setup], typically with the * same arguments passed to the Class * constructor: ( new Class( arguments ... )). * * $.Class("MyClass", * { * init: function( val ) { * this.val = val; * } * }) * var mc = new MyClass(1) * mc.val //-> 1 * * [jQuery.Class.prototype.setup Setup] is able to modify the arguments passed to init. Read * about it there. * */ //Breaks up code /** * @attribute Class * References the static properties of the instance's class. *

Quick Example

* @codestart * // a class with a static classProperty property * $.Class.extend("MyClass", {classProperty : true}, {}); * * // a new instance of myClass * var mc1 = new MyClass(); * * // * mc1.Class.classProperty = false; * * // creates a new MyClass * var mc2 = new mc.Class(); * @codeend * Getting static properties via the Class property, such as it's * [jQuery.Class.static.fullName fullName] is very common. */ } }) clss.prototype. /** * @function callback * Returns a callback function. This does the same thing as and is described better in [jQuery.Class.static.callback]. * The only difference is this callback works * on a instance instead of a class. * @param {String|Array} fname If a string, it represents the function to be called. * If it is an array, it will call each function in order and pass the return value of the prior function to the * next function. * @return {Function} the callback function */ callback = clss.callback; })(jQuery);