can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
764 lines (747 loc) • 25.7 kB
JavaScript
// steal-clean
steal('can/util/string', function (can) {
// ## construct.js
// `can.Construct`
// _This is a modified version of
// [John Resig's class](http://ejohn.org/blog/simple-javascript-inheritance/).
// It provides class level inheritance and callbacks._
// A private flag used to initialize a new class instance without
// initializing it's bindings.
var initializing = 0;
var canGetDescriptor;
try {
Object.getOwnPropertyDescriptor({});
canGetDescriptor = true;
} catch(e) {
canGetDescriptor = false;
}
var getDescriptor = function(newProps, name) {
var descriptor = Object.getOwnPropertyDescriptor(newProps, name);
if(descriptor && (descriptor.get || descriptor.set)) {
return descriptor;
}
return null;
},
inheritGetterSetter = function(newProps, oldProps, addTo) {
addTo = addTo || newProps;
var descriptor;
for (var name in newProps) {
if( (descriptor = getDescriptor(newProps, name)) ) {
this._defineProperty(addTo, oldProps, name, descriptor);
} else {
can.Construct._overwrite(addTo, oldProps, name, newProps[name]);
}
}
},
simpleInherit = function (newProps, oldProps, addTo) {
addTo = addTo || newProps;
for (var name in newProps) {
can.Construct._overwrite(addTo, oldProps, name, newProps[name]);
}
};
/**
* @add can.Construct
*/
can.Construct = function () {
if (arguments.length) {
return can.Construct.extend.apply(can.Construct, arguments);
}
};
/**
* @static
*/
can.extend(can.Construct, {
/**
* @property {Boolean} can.Construct.constructorExtends
* @parent can.Construct.static
*
* @description
* Toggles the behavior of a constructor function called
* without the `new` keyword to extend the constructor function or
* create a new instance.
*
* ```
* var animal = Animal();
* // vs
* var animal = new Animal();
* ```
*
* @body
*
* If `constructorExtends` is:
*
* - `true` - the constructor extends
* - `false` - a new instance of the constructor is created
*
* This property defaults to false.
*
* Example of constructExtends is true:
* ```
* var Animal = can.Construct.extend({
* constructorExtends: true // the constructor extends
* },{
* sayHi: function() {
* console.log("hai!");
* }
* });
*
* var Pony = Animal({
* gallop: function () {
* console.log("Galloping!!");
* }
* }); // Pony is now a constructor function extended from Animal
*
* var frank = new Animal(); // frank is a new instance of Animal
*
* var gertrude = new Pony(); // gertrude is a new instance of Pony
* gertrude.sayHi(); // "hai!" - sayHi is "inherited" from Animal
* gertrude.gallop(); // "Galloping!!" - gallop is unique to instances of Pony
*```
*
* The default behavior is shown in the example below:
* ```
* var Animal = can.Construct.extend({
* constructorExtends: false // the constructor does NOT extend
* },{
* sayHi: function() {
* console.log("hai!");
* }
* });
*
* var pony = Animal(); // pony is a new instance of Animal
* var frank = new Animal(); // frank is a new instance of Animal
*
* pony.sayHi() // "hai!"
* frank.sayHi() // "hai!"
*```
* By default to extend a constructor, you must use [can.Construct.extend extend].
*/
constructorExtends: true,
/**
* @function can.Construct.newInstance newInstance
* @parent can.Construct.static
*
* @description Returns an instance of `can.Construct`. This method
* can be overridden to return a cached instance.
*
* @signature `can.Construct.newInstance([...args])`
*
* @param {*} [args] arguments that get passed to [can.Construct::setup] and [can.Construct::init]. Note
* that if [can.Construct::setup] returns an array, those arguments will be passed to [can.Construct::init]
* instead.
* @return {class} instance of the class
*
* @body
* Creates a new instance of the constructor function. This method is useful for creating new instances
* with arbitrary parameters. Typically, however, you will simply want to call the constructor with the
* __new__ operator.
*
* ## Example
*
* The following creates a `Person` Construct and overrides `newInstance` to cache all
* instances of Person to prevent duplication. If the properties of a new Person match an existing one it
* will return a reference to the previously created object, otherwise it returns a new object entirely.
*
* ```
* // define and create the Person constructor
* var Person = can.Construct.extend({
* init : function(first, middle, last) {
* this.first = first;
* this.middle = middle;
* this.last = last;
* }
* });
*
* // store a reference to the original newInstance function
* var _newInstance = Person.newInstance;
*
* // override Person's newInstance function
* Person.newInstance = function() {
* // if cache does not exist make it an new object
* this.__cache = this.__cache || {};
* // id is a stingified version of the passed arguments
* var id = JSON.stringify(arguments);
*
* // look in the cache to see if the object already exists
* var cachedInst = this.__cache[id];
* if(cachedInst) {
* return cachedInst;
* }
*
* //otherwise call the original newInstance function and return a new instance of Person.
* var newInst = _newInstance.apply(this, arguments);
* this.__cache[id] = newInst;
* return newInst;
* }
*
* // create two instances with the same arguments
* var justin = new Person('Justin', 'Barry', 'Meyer'),
* brian = new Person('Justin', 'Barry', 'Meyer');
*
* console.log(justin === brian); // true - both are references to the same instance
* ```
*
*/
newInstance: function () {
// Get a raw instance object (`init` is not called).
var inst = this.instance(),
args;
// Call `setup` if there is a `setup`
if (inst.setup) {
inst.__inSetup = true;
args = inst.setup.apply(inst, arguments);
delete inst.__inSetup;
}
// Call `init` if there is an `init`
// If `setup` returned `args`, use those as the arguments
if (inst.init) {
inst.init.apply(inst, args || arguments);
}
return inst;
},
// Overwrites an object with methods. Used in the `super` plugin.
// `newProps` - New properties to add.
// `oldProps` - Where the old properties might be (used with `super`).
// `addTo` - What we are adding to.
_inherit: canGetDescriptor ? inheritGetterSetter : simpleInherit,
// Adds a `defineProperty` with the given name and descriptor
// Will only ever be called if ES5 is supported
_defineProperty: function(what, oldProps, propName, descriptor) {
Object.defineProperty(what, propName, descriptor);
},
// used for overwriting a single property.
// this should be used for patching other objects
// the super plugin overwrites this
_overwrite: function (what, oldProps, propName, val) {
what[propName] = val;
},
// Set `defaults` as the merger of the parent `defaults` and this
// object's `defaults`. If you overwrite this method, make sure to
// include option merging logic.
/**
* @function can.Construct.setup setup
* @parent can.Construct.static
*
* @description Perform initialization logic for a constructor function.
*
* @signature `can.Construct.setup(base, fullName, staticProps, protoProps)`
*
* A static `setup` method provides inheritable setup functionality
* for a Constructor function. The following example
* creates a Group constructor function. Any constructor
* functions that inherit from Group will be added to
* `Group.childGroups`.
*
*
* Group = can.Construct.extend({
* setup: function(Construct, fullName, staticProps, protoProps){
* this.childGroups = [];
* if(Construct !== can.Construct){
* this.childGroups.push(Construct)
* }
* Construct.setup.apply(this, arguments)
* }
* },{})
* var Flock = Group.extend(...)
* Group.childGroups[0] //-> Flock
*
* @param {constructor} base The base constructor that is being inherited from.
* @param {String} fullName The name of the new constructor.
* @param {Object} staticProps The static properties of the new constructor.
* @param {Object} protoProps The prototype properties of the new constructor.
*
* @body
* The static `setup` method is called immediately after a constructor
* function is created and
* set to inherit from its base constructor. It is useful for setting up
* additional inheritance work.
* Do not confuse this with the prototype `[can.Construct::setup]` method.
*
* ## Example
*
* This `Parent` class adds a reference to its base class to itself, and
* so do all the classes that inherit from it.
*
* ```
* Parent = can.Construct.extend({
* setup : function(base, fullName, staticProps, protoProps){
* this.base = base;
*
* // call base functionality
* can.Construct.setup.apply(this, arguments)
* }
* },{});
*
* Parent.base; // can.Construct
*
* Child = Parent({});
*
* Child.base; // Parent
* ```
*/
setup: function (base, fullName) {
this.defaults = can.extend(true, {}, base.defaults, this.defaults);
},
// Create's a new `class` instance without initializing by setting the
// `initializing` flag.
instance: function () {
// Prevents running `init`.
initializing = 1;
var inst = new this();
// Allow running `init`.
initializing = 0;
return inst;
},
// Extends classes.
/**
* @function can.Construct.extend extend
* @parent can.Construct.static
*
* @signature `can.Construct.extend([name,] [staticProperties,] instanceProperties)`
*
* Extends `can.Construct`, or constructor functions derived from `can.Construct`,
* to create a new constructor function. Example:
*
* var Animal = can.Construct.extend({
* sayHi: function(){
* console.log("hi")
* }
* })
* var animal = new Animal()
* animal.sayHi();
*
* @param {String} [name] Creates the necessary properties and
* objects that point from the `window` to the created constructor function. The following:
*
* can.Construct.extend("company.project.Constructor",{})
*
* creates a `company` object on window if it does not find one, a
* `project` object on `company` if it does not find one, and it will set the
* `Constructor` property on the `project` object to point to the constructor function.
*
* Finally, it sets "company.project.Constructor" as [can.Construct.fullName fullName]
* and "Constructor" as [can.Construct.shortName shortName].
*
* @param {Object} [staticProperties] Properties that are added the constructor
* function directly. For example:
*
* ```
* var Animal = can.Construct.extend({
* findAll: function(){
* return can.ajax({url: "/animals"})
* }
* },{}); // need to pass an empty instanceProperties object
*
* Animal.findAll().then(function(json){ ... })
* ```
*
* The [can.Construct.setup static setup] method can be used to
* specify inheritable behavior when a Constructor function is created.
*
* @param {Object} instanceProperties Properties that belong to
* instances made with the constructor. These properties are added to the
* constructor's `prototype` object. Example:
*
* var Animal = can.Construct.extend({
* findAll: function() {
* return can.ajax({url: "/animals"});
* }
* },{
* init: function(name) {
* this.name = name;
* },
* sayHi: function() {
* console.log(this.name," says hai!");
* }
* })
* var pony = new Animal("Gertrude");
* pony.sayHi(); // "Gertrude says hai!"
*
* The [can.Construct::init init] and [can.Construct::setup setup] properties
* are used for initialization.
*
* @return {function} The constructor function.
* ```
* var Animal = can.Construct.extend(...);
* var pony = new Animal(); // Animal is a constructor function
* ```
* @body
* ## Inheritance
* Creating "subclasses" with `can.Construct` is simple. All you need to do is call the base constructor
* with the new function's static and instance properties. For example, we want our `Snake` to
* be an `Animal`, but there are some differences:
*
*
* var Snake = Animal.extend({
* legs: 0
* }, {
* init: function() {
* Animal.prototype.init.call(this, 'ssssss');
* },
* slither: function() {
* console.log('slithering...');
* }
* });
*
* var baslisk = new Snake();
* baslisk.speak(); // "ssssss"
* baslisk.slither(); // "slithering..."
* baslisk instanceof Snake; // true
* baslisk instanceof Animal; // true
*
*
* ## Static properties and inheritance
*
* If you pass all three arguments to can.Construct, the second one will be attached directy to the
* constructor, allowing you to imitate static properties and functions. You can access these
* properties through the `[can.Construct::constructor this.constructor]` property.
*
* Static properties can get overridden through inheritance just like instance properties. In the example below,
* we override both the legs static property as well as the the init function for each instance:
*
* ```
* var Animal = can.Construct.extend({
* legs: 4
* }, {
* init: function(sound) {
* this.sound = sound;
* },
* speak: function() {
* console.log(this.sound);
* }
* });
*
* var Snake = Animal.extend({
* legs: 0
* }, {
* init: function() {
* this.sound = 'ssssss';
* },
* slither: function() {
* console.log('slithering...');
* }
* });
*
* Animal.legs; // 4
* Snake.legs; // 0
* var dog = new Animal('woof');
* var blackMamba = new Snake();
* dog.speak(); // 'woof'
* blackMamba.speak(); // 'ssssss'
* ```
*/
extend: function (name, staticProperties, instanceProperties) {
var fullName = name,
klass = staticProperties,
proto = instanceProperties;
// Figure out what was passed and normalize it.
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,
Constructor, parts, current, _fullName, _shortName, propName, shortName, namespace, prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor).
prototype = this.instance();
// Copy the properties over onto the new prototype.
can.Construct._inherit(proto, _super, prototype);
if(fullName) {
parts = fullName.split('.');
shortName = parts.pop();
} else if(klass && klass.shortName) {
shortName = klass.shortName;
} else if(this.shortName) {
shortName = this.shortName;
}
//!steal-remove-start
/* jshint ignore:start */
// In dev builds we want constructor.name to be the same as shortName.
// The only way to do that right now is using eval. jshint does not like
// this at all so we hide it
// Strip semicolons
var constructorName = shortName ? shortName.replace(/;/g, '') : 'Constructor';
// Assign a name to the constructor
eval('Constructor = function ' + constructorName + '() { return init.apply(this, arguments); }');
/* jshint ignore:end */
//!steal-remove-end
// Make sure Constructor is still defined when the constructor name
// code is removed.
if(typeof constructorName === 'undefined') {
Constructor = function() {
return init.apply(this, arguments);
};
}
// The dummy class constructor.
function init() {
// All construction is actually done in the init method.
if (!initializing) {
//!steal-remove-start
if(this.constructor !== Constructor &&
// We are being called without `new` or we are extending.
arguments.length && Constructor.constructorExtends) {
can.dev.warn('can/construct/construct.js: extending a can.Construct without calling extend');
}
//!steal-remove-end
return this.constructor !== Constructor &&
// We are being called without `new` or we are extending.
arguments.length && Constructor.constructorExtends ? Constructor.extend.apply(Constructor, arguments) :
// We are being called with `new`.
Constructor.newInstance.apply(Constructor, arguments);
}
}
// Copy old stuff onto class (can probably be merged w/ inherit)
for (propName in _super_class) {
if (_super_class.hasOwnProperty(propName)) {
Constructor[propName] = _super_class[propName];
}
}
// Copy new static properties on class.
can.Construct._inherit(klass, _super_class, Constructor);
// Setup namespaces.
if (fullName) {
current = can.getObject(parts.join('.'), window, true);
namespace = current;
_fullName = can.underscore(fullName.replace(/\./g, "_"));
_shortName = can.underscore(shortName);
//!steal-remove-start
if (current[shortName]) {
can.dev.warn("can/construct/construct.js: There's already something called " + fullName);
}
//!steal-remove-end
current[shortName] = Constructor;
}
// Set things that shouldn't be overwritten.
can.extend(Constructor, {
constructor: Constructor,
prototype: prototype,
/**
* @property {String} can.Construct.namespace namespace
* @parent can.Construct.static
*
* The `namespace` property returns the namespace your constructor is in.
* This provides a way organize code and ensure globally unique types. The
* `namespace` is the [can.Construct.fullName fullName] you passed without the [can.Construct.shortName shortName].
*
* ```
* can.Construct("MyApplication.MyConstructor",{},{});
* MyApplication.MyConstructor.namespace // "MyApplication"
* MyApplication.MyConstructor.shortName // "MyConstructor"
* MyApplication.MyConstructor.fullName // "MyApplication.MyConstructor"
* ```
*/
namespace: namespace,
/**
* @property {String} can.Construct.shortName shortName
* @parent can.Construct.static
*
* If you pass a name when creating a Construct, the `shortName` property will be set to the
* name you passed without the [can.Construct.namespace namespace].
*
* ```
* can.Construct("MyApplication.MyConstructor",{},{});
* MyApplication.MyConstructor.namespace // "MyApplication"
* MyApplication.MyConstructor.shortName // "MyConstructor"
* MyApplication.MyConstructor.fullName // "MyApplication.MyConstructor"
* ```
*/
_shortName: _shortName,
/**
* @property {String} can.Construct.fullName fullName
* @parent can.Construct.static
*
* If you pass a name when creating a Construct, the `fullName` property will be set to
* the name you passed. The `fullName` consists of the [can.Construct.namespace namespace] and
* the [can.Construct.shortName shortName].
*
* ```
* can.Construct("MyApplication.MyConstructor",{},{});
* MyApplication.MyConstructor.namespace // "MyApplication"
* MyApplication.MyConstructor.shortName // "MyConstructor"
* MyApplication.MyConstructor.fullName // "MyApplication.MyConstructor"
* ```
*/
fullName: fullName,
_fullName: _fullName
});
// Dojo and YUI extend undefined
if (shortName !== undefined) {
Constructor.shortName = shortName;
}
// Make sure our prototype looks nice.
Constructor.prototype.constructor = Constructor;
// Call the class `setup` and `init`
var t = [_super_class].concat(can.makeArray(arguments)),
args = Constructor.setup.apply(Constructor, t);
if (Constructor.init) {
Constructor.init.apply(Constructor, args || t);
}
/**
* @prototype
*/
return Constructor; //
/**
* @property {Object} can.Construct.prototype.constructor constructor
* @parent can.Construct.prototype
*
* A reference to the constructor function that created the instance. This allows you to access
* the constructor's static properties from an instance.
*
* @body
* ## Example
*
* This can.Construct has a static counter that counts how many instances have been created:
*
* ```
* var Counter = can.Construct.extend({
* count: 0
* }, {
* init: function() {
* this.constructor.count++;
* }
* });
*
* var childCounter = new Counter();
* console.log(childCounter.constructor.count); // 1
* console.log(Counter.count); // 1
* ```
*/
}
});
/**
* @function can.Construct.prototype.setup setup
* @parent can.Construct.prototype
*
* @signature `construct.setup(...args)`
*
* A setup function for the instantiation of a constructor function.
*
* @param {*} args The arguments passed to the constructor.
*
* @return {Array|undefined} If an array is returned, the array's items are passed as
* arguments to [can.Construct::init init]. The following example always makes
* sure that init is called with a jQuery wrapped element:
*
* WidgetFactory = can.Construct.extend({
* setup: function(element){
* return [$(element)]
* }
* })
*
* MyWidget = WidgetFactory.extend({
* init: function($el){
* $el.html("My Widget!!")
* }
* })
*
* Otherwise, the arguments to the
* constructor are passed to [can.Construct::init] and the return value of `setup` is discarded.
*
* @body
*
* ## Deciding between `setup` and `init`
*
*
* Usually, you should use [can.Construct::init init] to do your constructor function's initialization.
* You should, instead, use `setup` when:
*
* - there is initialization code that you want to run before the inheriting constructor's
* `init` method is called.
* - there is initialization code that should run whether or not inheriting constructors
* call their base's `init` methods.
* - you want to modify the arguments that will get passed to `init`.
*
* ## Example
*
* This code is a simplified version of the code in [can.Control]'s setup
* method. It converts the first argument to a jQuery collection and
* extends the controller's defaults with the options that were passed.
*
*
* can.Control = can.Construct.extend({
* setup: function(domElement, rawOptions) {
* // set up this.element
* this.element = $(domElement);
*
* // set up this.options
* this.options = can.extend({},
* this.constructor.defaults,
* rawOptions
* );
*
* // pass this.element and this.options to init.
* return [this.element, this.options];
* }
* });
*
*/
can.Construct.prototype.setup = function () {};
/**
* @function can.Construct.prototype.init init
* @parent can.Construct.prototype
*
* @description Called when a new instance of a can.Construct is created.
*
* @signature `construct.init(...args)`
* @param {*} args the arguments passed to the constructor (or the items of the array returned from [can.Construct::setup])
*
* @body
* If a prototype `init` method is provided, `init` is called when a new Construct is created---
* after [can.Construct::setup]. The `init` method is where the bulk of your initialization code
* should go. A common thing to do in `init` is save the arguments passed into the constructor.
*
* ## Examples
*
* First, we'll make a Person constructor that has a first and last name:
*
* ```
* var Person = can.Construct.extend({
* init: function(first, last) {
* this.first = first;
* this.last = last;
* }
* });
*
* var justin = new Person("Justin", "Meyer");
* justin.first; // "Justin"
* justin.last; // "Meyer"
* ```
*
* Then, we'll extend Person into Programmer, and add a favorite language:
*
* ```
* var Programmer = Person.extend({
* init: function(first, last, language) {
* // call base's init
* Person.prototype.init.apply(this, arguments);
*
* // other initialization code
* this.language = language;
* },
* bio: function() {
* return "Hi! I'm " + this.first + " " + this.last +
* " and I write " + this.language + ".";
* }
* });
*
* var brian = new Programmer("Brian", "Moschel", 'ECMAScript');
* brian.bio(); // "Hi! I'm Brian Moschel and I write ECMAScript.";
* ```
*
* ## Modified Arguments
*
* [can.Construct::setup] is able to modify the arguments passed to `init`.
* If you aren't receiving the arguments you passed to `new Construct(args)`,
* check that they aren't being changed by `setup` along
* the inheritance chain.
*/
can.Construct.prototype.init = function () {};
return can.Construct;
});