Skip to content

Instantly share code, notes, and snippets.

@nfarina
Created January 5, 2011 21:36
Show Gist options
  • Save nfarina/767062 to your computer and use it in GitHub Desktop.
Save nfarina/767062 to your computer and use it in GitHub Desktop.
A minimal jQuery-based JavaScript UI framework. In-progress and a bit raw.
(function(){ // begin private function
var copyProperties = function(props, o, overwrite, wrapFunctions) {
var wrap = wrapFunctions ? wrapFunctionWithSelf : nowrapFunction;
if (props) {
for (key in props) if (overwrite || o[key] === undefined) {
var getter = props.__lookupGetter__(key);
var setter = props.__lookupSetter__(key);
var baseGetter = o.__lookupGetter__(key);
var baseSetter = o.__lookupSetter__(key);
if (getter) o.__defineGetter__(key,wrap(getter,o,key,true));
if (setter) o.__defineSetter__(key,wrap(setter,o,key,false,true));
// weird things happen when only one side of a subclassed property is added, so
// we'll make the other side if necessary.
if (getter && !setter && baseSetter) o.__defineSetter__(key,baseSetter);
if (setter && !getter && baseGetter) o.__defineGetter__(key,baseGetter);
if (!getter && !setter && props.hasOwnProperty(key))
if (typeof props[key] === 'function')
o[key] = wrap(props[key],o,key);
else
o[key] = props[key];
}
}
}
var nowrapFunction = function(func) { return func; }
// Management of the "base" function. "base" is a global function attached to window
// that you can call while one of your methods is executing, in order to call
// a function on your superclass with the correct context. We store the correct
// context in our private baseContext var. Since "base" and "baseContext" are global,
// we need to manage their visibility using a stack - so that if you call a method
// from inside another method, the current base and context are pushed and popped
// off a global stack as needed.
var baseContext;
var baseStack = [];
function pushBaseStack(wrapper, context) {
baseStack.push([wrapper,context]);
window.base = wrapper;
baseContext = context;
}
function popBaseStack() {
baseStack.pop();
if (baseStack.length > 0) {
var array = baseStack[baseStack.length-1];
window.base = array[0];
baseContext = array[1];
}
else {
delete window.base;
baseContext = null;
}
}
var wrapFunctionWithSelf = function(func,o,key,getter,setter) {
var baseWrapper;
if (getter)
baseWrapper = function() { return o.base.__lookupGetter__(key).apply(baseContext,arguments); };
else if (setter)
baseWrapper = function() { return o.base.__lookupSetter__(key).apply(baseContext,arguments); };
else
baseWrapper = function() { return o.base[key].apply(baseContext,arguments); };
return function() {
pushBaseStack(baseWrapper,this);
var ret = func.apply(this,[this].concat(Array.prototype.slice.call(arguments)));
popBaseStack();
return ret;
}
}
//Object.create should exist already, in ECMA5 or whatever
if (typeof Object.create !== 'function') {
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
}
var ignoredWindowMembers = {}
for (var key in window)
ignoredWindowMembers[key] = true;
// Our ultimate prototype, has classish helper methods.
// The methods "prototypeName" and "prototypeWithName" only work if you define your
// objects on the global object ("window").
Base = {
init: function() {},
derive: function(props) {
var o = Object.create(this);
copyProperties(props,o,true,true);
//o.__base__ = this;
return o;
},
create: function() {
var derived = this.derive();
derived.init.apply(derived, arguments);
return derived;
},
get base() {
return this.__proto__;
//return this.__base__;
},
get baseName() {
// Slow but useful. TODO-SPEEDUP: cache result
for (var p = this; p; p = p.base)
for (var key in window)
if (!ignoredWindowMembers[key] && window[key] === p) return key;
},
baseWithName: function(name) {
return window[name];
},
toString: function() {
return '[object ' + (this.baseName || "Object") + ']';
},
}
// Replace builtin Object.toString() function, so Webkit will call it when printing things to the console
var nativeToString = Object.prototype.toString;
Object.prototype.toString = function() {
if (typeof this === 'object')
return Base.toString.apply(this)
else
return nativeToString.apply(this);
}
// If jQuery is available, create the Widget class.
if (window.jQuery) {
// big dictionary of all known templates registered by Widget.addTemplates
var allTemplates = {};
// Widget's true prototype is the jQuery.fn object, meaning that your Widget
// subclasses get automatic access to all jQuery functions like addClass(), etc.
// Base's own methods and properties are mixed in below.
Widget = Base.derive.call(jQuery.fn,{
init: function(self, selector) {
if (selector === undefined)
selector = '<div>'; // default element
jQuery.fn.init.call(self,selector);
// add CSS class names to represent our full class hierarchy - TODO-SPEEDUP: cache result
for (var base = self.base; base !== Widget; base = base.base)
self.addClass(base.baseName);
// does there exist a template with an ID matching our class name? if so, consume it automatically
var template = self.getTemplate(self.baseName);
if (template)
self.applyTemplate(self.baseName);
self.widget(self); // backreference
},
get element(self) { return self[0]; },
addTemplates: function(cls, html) {
if (!html) throw "Widget.addTemplates(html) called without a proper html argument.";
$('<div>').html(html).children().each(function(i,template) {
if (template.tagName.toLowerCase() !== 'template') throw "Found an element <"+template.tagName+"> in a templates file, expected <template>.";
if (!template.id) throw "All <template> elements must have an id.";
allTemplates[template.id] = template;
})
},
getTemplate: function(cls,id) {
return allTemplates[id];
},
applyTemplate: function(self,id,target) {
var template = self.getTemplate(id);
if (!template) throw "Template with id '"+id+"' was not found.";
// add template contents to the target if specified, or self if not
target = target || self;
target.html($(template).html());
// create instance vars for any named elements in the template for easy access.
target.find('[id!=""]').each(function(i,namedElement) {
self[namedElement.id] = $(namedElement);
})
},
addStylesheet: function(cls, css) {
if (!css) throw "Widget.addStylesheet(css) called without a proper css argument.";
//var link = $('<link rel="stylesheet" type="text/css">').attr('href','/media/mapping/mapping.css');
//$('head').append(link);
$('<style>').text(css).appendTo($('head'));
},
})
// A jQuery plugin to get a DOM element's associated Widget, if any.
jQuery.fn.widget = function(target) {
return this.data('widget',target);
}
// A jQuery plugin to get the nearest parent that is a Widget.
jQuery.fn.parentWidget = function() {
for (var p = this.parent(); p.length; p = p.parent()) {
var w = p.widget();
if (w) return w;
}
}
// A jQuery plugin to get the nearest parent that is a Widget of a particular type.
jQuery.fn.parentWidgetWithBase = function(base) {
if (base)
for (var w = this.parentWidget(); w; w = w.parentWidget())
if (w && base.isPrototypeOf(w))
return w;
}
// Like jQuery's .each() function, but instead of handling DOM elements it
// looks up the associated Widget on each using .widget().
jQuery.fn.eachWidget = function(callback) {
var length = this.length, i=0;
for (var value = $(this[0]).widget();
i < length && callback.call(value, i, value) !== false; value = $(this[++i]).widget() ) {}
}
// mixin members of Base to Widget
copyProperties(Base, Widget);
}
// This is pretty much critical for OO-style event delegates.
Function.prototype.bind = function(scope) {
var _function = this;
return function() {
return _function.apply(scope, arguments);
}
}
})(); // end private function
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment