$(function() {
var Frame = {};
$(function() {
var Frame = {};
Helper methods
var __emptyHash = {}, __has, __extend, __currentGID = 0,
__has = function(object, property) { return __emptyHash.hasOwnProperty.call(object, property); },
__keys = function(object) {
var keys = []
for(var k in object) {
if(object.hasOwnProperty(k)) {
keys.push(k);
}
}
return keys;
},
__merge = function(hash, otherHash) {
for (var key in otherHash) {
if (__has(otherHash, key) && !__has(hash, key)) {
hash[key] = otherHash[key];
}
}
},
__makeArray = function(object) {
if( !(object instanceof Array) ) {
object = [object];
}
return object;
},
__isBasicObject = function(object) {
return __has(object, 'basicObjectDefined');
},
__isString = function(object) {
return typeof(object) == "string";
},
Returns a new GID with the given prefix. GIDs are always returned as string
__gid = function(prefix) {
var GID = (++__currentGID).toString();
if(prefix) {
GID = prefex+"-"+GID;
}
return GID;
}
Creates a shared instance method
function createDefaultInstanceMethodOn(object) {
Object.defineProperty(object, 'default', {
get: function() {
if(!this.__sharedInstance) { this.__sharedInstance = new object(); }
return this.__sharedInstance;
},
set: function(){}
});
}
__extend = function(propertiesOrProtoProps, protoProps) {
var parent = this;
var child, properties;
if(propertiesOrProtoProps instanceof Array) {
properties = properties;
} else {
protoProps = propertiesOrProtoProps;
}
if (protoProps && __has(protoProps, 'constructor')) {
child = protoProps.constructor;
} else {
child = function(){ return parent.apply(this, arguments); };
}
Set the prototype chain to inherit from parent
, without calling
parent
's constructor function.
var Surrogate = function(){ this.constructor = child; };
Surrogate.prototype = parent.prototype;
Copy over static methods
child.prototype = new Surrogate;
if (protoProps) _.extend(child.prototype, protoProps);
Copy over static methods
for (var key in parent) {
if (__has(parent, key)) {
child[key] = parent[key];
}
}
Set a convenience property in case the parent's prototype is needed later.
child.__super__ = parent.prototype;
Set the properties on the object if any was given
if(properties) {
child.property(properties);
}
return child;
};
Basic low level object. Contains most low level object functions such as KVO & KVC.
var BasicObject = function(options) {
/* These values are not meant to be KVO. */
Determines whether this object is a basic object so you can distinguish them from regular objects.
this.basicObjectDefined=true
The current id of the object
this.gid = __gid();
};
BasicObject.extend = __extend;
Public property setter. Creates specialized properties which can be accessed through KVC and make use of KVO
BasicObject.property = function(propertyNames) {
propertyNames = __makeArray(propertyNames);
for(var i = 0; i < propertyNames.length; i++) {
var propertyName = propertyNames[i];
if( !(propertyNames in this.prototype) ) {
Object.defineProperty(this.prototype, propertyName, {
enumerable: true,
get: function() {
return this.getProperty(propertyName)
},
set: function(value) {
this.setProperty(propertyName, value)
}
});
}
}
}
BasicObject.prototype.properties = function() {
return this._properties || (this._properties = {})
}
BasicObject.prototype.observers = function() {
return this._observers || (this._observers = {})
}
Specialized setter. Calls callbacks on observed values
BasicObject.prototype.setProperty = function(key, value) {
this.properties()[key] = value
if(this.observers[key] && this.observers[key].length > 0) {
for(var i = 0; i < this.observers[key].length; i++) {
var observer = this.observers[key][i]
if(observer !== undefined) {
Signal the observer or call the callback
if($.isFunction(observer)) {
observer.call(this, key, value);
} else {
observer.observeValueForKey.call(observer, key, value)
}
}
}
}
}
Basic object getter
BasicObject.prototype.getProperty = function(key) {
return this.properties()[key]
}
Returns the given or sets the given key with value (Key Value Coding, KVC)
BasicObject.prototype.valueForKey = function(value_or_key, key) {
if(key === undefined) {
return this.getProperty(value_or_key)
}
this.setProperty(key, value_or_key)
}
Observe the given key. (Key Value Observing, KVO)
BasicObject.prototype.addObserverForKey = function(observerOrKey, keyOrCallback, options) {
var observer, key;
if(__isString(observerOrKey) && $.isFunction(keyOrCallback)) {
observer = keyOrCallback;
key = observerOrKey;
} else {
observer = observerOrKey;
key = keyOrCallback;
}
if(this.observers[key] === undefined) {
this.observers[key] = []
}
this.observers[key].push(observer)
}
Remove the given observer with the specified key
BasicObject.prototype.removeObserverForKey = function(observer, key) {
if(this.observers[key]) {
for(var i = 0; i < this.observers[key].length; i++) {
if(this.observers[key][i] === observer) {
delete this.observers[key][i]
break
}
}
}
}
/*
* Frame.Model data object.
*/
The basic model. Inherit from this object.
var Model = BasicObject.extend({
constructor: function(attributes, options) {
Call the basic object's constructor.
BasicObject.call(this, options);
Define this as a new model.
this.isNew = true;
Serialize the attributes
this.serialize(attributes);
},
Load data from the remote source
fetch: function(parameters, options) {
$this = this;
var url = ($.isFunction(this.url) ? this.url() : this.url);
$.ajax(url, {
type: "GET",
success: function(data, textStatus, xhr) {
Serialize the attributes
$this.serialize(data);
Call the success callback if it was specified
if(options.success) {
options.success.call($this, data, textStatus, xhr);
}
}
});
},
save: function(parameters, options) {
if(!options) options = {};
if(this.isNew) {
Frame.dataStore.add(this, options);
} else {
TODO
}
},
serialize: function(serializableAttributes) {
Get the current object's constructor so we can bind values to it.
var objectClass = this.constructor;
Iterate over the retrieved attributes
for(var key in serializableAttributes) {
Model attributes are 'lazily' added.
objectClass.property(key);
Assign the attributes value
this.valueForKey(serializableAttributes[key], key);
}
},
Copies the attributes of the model to a new object.
toJSON: function() {
var attributes = {};
for(var k in this.properties()) {
if(this.properties().hasOwnProperty(k)) {
attributes[k] = this.valueForKey(k);
}
}
return attributes;
},
Validates the model (not the model's fields), such as URL. TODO move this to the datastore instead
__validateModel: function(){
if(!this.url) throw "No URL specified for model";
},
});
/*
* Views
*/
/*
* Basic view
*/
function splitEventSelector(selectorEvent) {
var s = {};
if(selectorEvent.indexOf(' ') != -1) {
s.event = selectorEvent.substr(0, selectorEvent.indexOf(' ')),
s.selector = selectorEvent.substr(selectorEvent.indexOf(' ')+1)
} else {
s.event = selectorEvent;
}
return s;
}
var View = BasicObject.extend({
subviews: [],
element: 'div',
Basic view contstructor
constructor: function(options) {
if(!options) options = {};
Call the basic object's constructor.
BasicObject.call(this, options);
Set the element of the controller
this.el = options.el;
Loop through the events if specified
if(this.events) {
for(var key in this.events) {
if(this.events.hasOwnProperty(key)) {
Bind the event to the view (including any sub-queries) this.events[key] returns a function 'pointer' (key)
this.on(key, this[this.events[key]]);
}
}
}
},
Add a subview to the current view. Takes care of drawing the view.
addSubview: function(subview, options) {
Add the subview to this view
this.subviews.push(subview);
Set this as the super view.
subview.superView = this;
Draw the view
subview.draw();
If the subview's el is present it latched on to something.
if(!subview.el) {
this.$.append(subview.$);
}
},
Remove given view from this view. Takes care of unbinding
removeSubview: function(subview) {
Get the index of the current view
var idx = this.subviews.indexOf(subview);
if(idx !== -1) {
var subview = this.subviews[idx];
delete this.subviews[idx];
subview.wasRemovedFromSuperView();
}
},
Easy function to remove a view of a super view
removeFromSuperview: function() {
if(this.superView) {
this.superView.removeSubview(this);
}
},
On DOM event. Uses basic jQuery event handling.
on: function(event, cb) {
Try and split events such as 'click #selector' and perform any find queries necessary.
var eventSelector = splitEventSelector(event);
var callback = _.bind(cb, this);
if(eventSelector.selector) {
this.$.on(eventSelector.event, eventSelector.selector, callback);
} else {
this.$.on(eventSelector.event, callback);
}
},
Remove DOM event.
off: function(event, cb) {
Try and split events such as 'click #selector' and perform any find queries necessary.
var eventSelector = splitEventSelector(event);
var callback = _.bind(cb, this);
if(eventSelector.selector) {
this.$.find(eventSelector.selector).off(eventSelector.event, callback);
} else {
this.$.off(eventSelector.event, callback);
}
},
Function will be called when the view has been removed from the super view.
wasRemovedFromSuperView: function() {
Remove from DOM
this.$.remove();
},
Basic draw method. Should be overwritten for custom drawing
draw: function() {},
});
Generic accessor for the view's element (either created or 'fetched').
Object.defineProperty(View.prototype, "$", {
get: function() {
Get the bound element or create a new element.
var element = this.el || '<'+this.element+'/>';
if(!this.__collection) this.__collection = $(element);
return this.__collection;
}
});
/*
* Canvas view.
*/
var CanvasView = View.extend({
Create a canvas element if no element is given
element: 'canvas',
constructor: function(options) {
if(!options) options = {};
Call the super view's constructor
View.call(this, options);
this.$.attr({width: options.width, height: options.height});
this.context = this.$[0].getContext('2d');
}
});
Generic button view
var Button = View.extend({
element: 'button',
constructor: function(options) {
if(!options) options = {};
View.call(this, options);
Initialize default text
if('text' in options) this.text(options.text);
},
Convenient method to set the button's text
text: function(text) {
this.text = text;
this.draw();
},
draw: function() {
this.$.text(this.text);
},
});
/*
* Controllers
*/
var ViewController = BasicObject.extend(
[
The root view of this controller.
'view',
Child view controllers
'viewControllers',
Parent view controller or undefined
'parentViewController'], {
constructor: function(options) {
if(!options) options = {};
Call the basic object's constructor.
BasicObject.call(this, options);
this.el = options.el;
this.viewControllers = [];
this.parentViewController
},
loadView: function() {
Notify the inherited controller that the view is about to be loaded.
this.viewWillLoad();
Create a new View and pass it the el (el may be undefined)
this.view = new View({el: this.el});
Notify the inherited controller that the view has been loaded and is ready.
this.viewDidLoad();
Return the view so that who ever called this function can add the DOM Nodes.
return this.view;
},
Add a child view controller. This method will take care of calling the appropriate methods to load the child controllers view.
addChildViewController: function(controller) {
if(controller.parentViewController) {
controller.removeFromParentViewController();
}
this.viewControllers.push(controller);
Set this as the parent view controller
controller.parentViewController = this;
Load the view of the child view controller
controller.loadView();
},
Remove a child view controller from the stack
removeChildViewController: function(controller) {
var idx = this.viewControllers.indexOf(controller);
if(idx != -1) {
delete this.viewControllers[idx];
controller.parentViewController = undefined;
}
},
removeFromParentViewController: function() {
if(this.parentViewController) {
this.parentViewController.removeChildViewController(this);
}
},
Will be called when the view is done loading and is set up.
viewDidLoad: function(){},
Will be called before any view loading is done.
viewWillLoad: function(){},
});
/*
* Basic DataStore. All New datastores inherit from this object
*/
var DataStore = BasicObject.extend({
open: function(name, callback, options) {},
add: function(object, options) {},
addByStoreName: function(storeName, data, options) {},
deleteByStoreName: function(storeName, id, options) {},
getAllByStoreName: function(storeName, options) {},
});
/*
* Application class, root of the application stack.
*/
var Application = BasicObject.extend({
didFinishLoading: function() {
throw Error("didFinishLoading was not everwritten by subclass or Frame.Application was used directly as application class.");
},
willTerminate: function(){}
});
Objects
Frame.BasicObject = BasicObject;
Frame.Model = Model;
Views
Frame.View = View;
Frame.CanvasView = CanvasView;
Frame.Button = Button;
Controllers
Frame.ViewController = ViewController;
Frame.DataStore = DataStore;
Frame.dataStore = undefined;
Frame.Application = Application;
Methods
Frame.createDefaultInstanceMethodOn = createDefaultInstanceMethodOn;
Frame.has = __has;
Frame.keys = __keys;
Frame.makeArray = __makeArray;
Frame.isBasicObject = __isBasicObject;
Frame.gid = __gid;
/*
* Define the application accessor.
* Once the application property is set it will use it to create
* a new instance of that object and uses that as the root of your application.
*/
Object.defineProperty(Frame, 'application', {
set: function(ApplicationObject) {
var app;
Create a new application instance from the given application class.
app = new ApplicationObject();
Set Frame's application instance.
this.applicationInstance = app;
app.didFinishLaunching();
If a rootViewController is set call the conrollers loadView method
if(app.rootViewController) {
Set the body if no element has been set on the root controller.
if(!app.rootViewController.el) app.rootViewController.el = 'body';
Load the view and draw it
var view = app.rootViewController.loadView();
view.draw();
}
Set up the terminate callback
$(window).on('beforeunload', function() {
app.willTerminate();
});
},
});
window.Frame = Frame;
});