(function(window) {
var slice = Array.prototype.slice, isArray = Array.isArray, keys = Object.keys,
getProto = Object.getPrototypeOf, create = Object.create, sudo;
(function(window) {
var slice = Array.prototype.slice, isArray = Array.isArray, keys = Object.keys,
getProto = Object.getPrototypeOf, create = Object.create, sudo;
uncomment these lines if using the template module (and change the above semi to a comma) escapes = {}, htmlEscapes = {'&': '&','<': '<','>': '>','"': '"',"'": ''','/': '/'}, escaper = /\|'|\r|\n|\t|\u2028|\u2029/g, htmlEscaper = /[&<>"'\/]/g, unescaper = /\(\|'|r|n|t|u2028|u2029)/g;
sudo = {
delegates: {},
The sudo.extensions namespace holds the objects that are stand alone modules
which
can be implemented
(mixed-in) in sudo Class Objects
namespace
extensions: {},
Extract a value located at path
relative to the passed in object
param
{String} path
. The key in the form of a dot-delimited path.
param
{object} obj
. An object literal to operate on.
returns
{*|undefined}. The value at keypath or undefined if not found.
getPath: function getPath(path, obj) {
var key, p;
p = path.split('.');
for (; p.length && (key = p.shift());) {
if(!p.length) return obj[key];
else obj = obj[key] || {};
}
return obj;
},
Inherit the prototype from a parent to a child.
Set the childs constructor for subclasses of child.
Subclasses of the library base classes will not
want to use this function in most use-cases. Why? User Sudo Class Objects
possess their own constructors and any call back to a superclass
constructor
will generally be looking for the library Object's constructor.
param
{function} parent
param
{function} child
inherit: function inherit(parent, child) {
child.prototype = create(parent.prototype);
child.prototype.constructor = child;
},
Notice there is no need to extrinsically instruct how to make the sandwich, just the elegant single command.
returns
{string}
makeMeASandwich: function makeMeASandwich() {return 'Okay.';},
Method for assuring a Namespace is defined.
param
{string} path
. The path that leads to a blank Object.
namespace: function namespace(path) {
if (!this.getPath(path, window)) {
this.setPath(path, {}, window);
}
},
Traverse the keypath and get each object (or make blank ones) eventually setting the value at the end of the path
param
{string} path
. The path to traverse when setting a value.
param
{*} value
. What to set.
param
{Object} obj
. The object literal to operate on.
setPath: function setPath(path, value, obj) {
var p = path.split('.'), key;
for (; p.length && (key = p.shift());) {
if(!p.length) obj[key] = value;
else if (obj[key]) obj = obj[key];
else obj = obj[key] = {};
}
},
Some sudo Objects use a unique integer as a tag
for identification.
(Views for example). This ensures they are indeed unique.
uid: 0,
An integer used as 'tags' by some sudo Objects as well as a unique string for views when needed
param
{string} prefix. Optional string identifier
unique: function unique(prefix) {
return prefix ? prefix + this.uid++ : this.uid++;
},
Remove a key:value pair from this object's data store
located at
param
{String} path
param
{Object} obj
The object to operate on.
unsetPath: function unsetPath(path, obj) {
var p = path.split('.'), key;
for (; p.length && (key = p.shift());) {
if(!p.length) delete obj[key];
this can fail if a faulty path is passed. using getPath beforehand can prevent that
else obj = obj[key];
}
}
};
All sudo.js objects inherit base, giving the ability
to utilize delegation, the base
function and the
construct
convenience method.
constructor
sudo.Base = function() {
can delegate
this.delegates = [];
a beautiful and unique snowflake
this.uid = sudo.unique();
};
Push an instance of a Class Object into this object's _delegates_
list.
param
{Object} del
. An instance of a sudo.delegates Class Object
returns
{Object} this
sudo.Base.prototype.addDelegate = function(del) {
del.delegator = this;
this.delegates.push(del);
if('addedAsDelegate' in del) del.addedAsDelegate(this);
return this;
};
Lookup the function matching the name passed in and call it with
any passed in argumets scoped to the calling object.
This method will avoid the recursive-loop problem by making sure
that the first match is not the function that called base
.
params
{*} any other number of arguments to be passed to the looked up method
along with the initial method name
sudo.Base.prototype.base = function() {
var args = slice.call(arguments),
name = args.shift(),
found = false,
obj = this,
curr;
find method on the prototype, excluding the caller
while(!found) {
curr = getProto(obj);
if(curr[name] && curr[name] !== this[name]) found = true;
keep digging
else obj = curr;
}
return curr[name].apply(this, args);
};
A convenience method that alleviates the need to place:
Object.getPrototypeOf(this).consturctor.apply(this, arguments)
in every constructor
sudo.Base.prototype.construct = function() {
getProto(this).constructor.apply(this, arguments || []);
};
From this object's list of delegates find the object whose _role_
matches
the passed name
and:
meth
is falsy return the delegate.
2 if meth
is truthy bind its method (to the delegate) and return the methodparam
{String} role
The role property to match in this object's delegates list
param
{String} meth
Optional method to bind to the action this delegate is being used for
returns
sudo.Base.prototype.delegate = function(role, meth) {
var del = this.delegates, i;
for(i = 0; i < del.length; i++) {
if(del[i].role === role) {
if(!meth) return del[i];
return del[i][meth].bind(del[i]);
}
}
};
Fetch a delegate whose role property matches the passed in argument.
Uses the delegate
method in its 'single argument' form, included for
API consistency
param
{String} role
'returns' {Object|undefined}
sudo.Base.prototype.getDelegate = function(role) {return this.delegate(role);};
From this objects delegates
list remove the object (there should only ever be 1)
whose role matches the passed in argument
param
{String} role
returns
{Object} this
sudo.Base.prototype.removeDelegate = function(role) {
var del = this.delegates, i;
for(i = 0; i < del.length; i++) {
if(del[i].role === role) {
no delegator for you
del[i].delegator = void 0;
del.splice(i, 1);
return this;
}
}
return this;
};
private
Model Objects expose methods for setting and getting data, and
can be observed if implementing the Observable Extension
param
{object} data. An initial state for this model.
constructor
sudo.Model = function(data) {
sudo.Base.call(this);
this.data = data || {};
only models are observable
this.callbacks = [];
this.changeRecords = [];
};
Model inherits from sudo.Base
private
sudo.inherit(sudo.Base, sudo.Model);
Returns the value associated with a key.
param
{String} k
. The name of the key
returns
{*}. The value associated with the key or false if not found.
sudo.Model.prototype.get = function(k) {return this.data[k];};
Uses the sudo namespace's getpath function operating on the model's data hash.
param
{string} path
returns
{*|undefined}. The value at keypath or undefined if not found.
sudo.Model.prototype.getPath = function(path) {
return sudo.getPath(path, this.data);
};
Assembles and returns an object of key:value pairs for each key contained in the passed in Array.
param
{array} ary
. An array of keys.
returns
{object}
sudo.Model.prototype.gets = function(ary) {
var obj = {};
ary.forEach(function(str) {
obj[str] = !~str.indexOf('.') ? this.get(str) : this.getPath(str);
}.bind(this));
return obj;
};
private
sudo.Model.prototype.role = 'model';
Set a key:value pair.
param
{String} k
. The name of the key.
param
{*} v
. The value associated with the key.
returns
{Object} this
sudo.Model.prototype.set = function(k, v) {
NOTE: intentional possibilty of setting a falsy value
this.data[k] = v;
return this;
};
Uses the sudo namespace's setpath function operating on the model's data hash.
param
{String} path
param
{*} v
returns
{Object} this.
sudo.Model.prototype.setPath = function(path, v) {
sudo.setPath(path, v, this.data);
return this;
};
Invokes set()
or setPath()
for each key value pair in obj
.
Any listeners for those keys or paths will be called.
param
{Object} obj
. The keys and values to set.
returns
{Object} this
sudo.Model.prototype.sets = function(obj) {
keys(obj).forEach(function(k) {
!~k.indexOf('.') ? this.set(k, obj[k]) : this.setPath(k, obj[k]);
}.bind(this));
return this;
};
Remove a key:value pair from this object's data store
param
{String} k
returns
{Object} this
sudo.Model.prototype.unset = function(k) {
delete this.data[k];
return this;
};
Uses sudo.unsetPath
operating on this models data hash
param
{String} path
returns
{Object} this
sudo.Model.prototype.unsetPath = function(path) {
sudo.unsetPath(path, this.data);
return this;
};
Deletes a number of keys or paths from this object's data store
param
{array} ary
. An array of keys or paths.
returns
{Objaect} this
sudo.Model.prototype.unsets = function(ary) {
ary.forEach(function(k) {
!~k.indexOf('.') ? this.unset(k) : this.unsetPath(k);
}.bind(this));
return this;
};
A container is any object that can both contain other objects and itself be contained.
param
{Array|Object} 'arg'. Optional array or hash
of child objects which the Container will add as child objects
via addChildren
constructor
sudo.Container = function(arg) {
sudo.Base.call(this);
this.children = [];
this.childNames = {};
if(arg) this.addChildren(arg);
};
Container is a subclass of sudo.Base
sudo.inherit(sudo.Base, sudo.Container);
Adds a View to this container's list of children.
Also adds an 'index' property and an entry in the childNames hash.
If addedToParent
if found on the child, call it, sending this
as an argument.
param
{Object} c
. View (or View subclass) instance.
param
{String} name
. An optional name for the child that will go in the childNames hash.
returns
{Object} this
sudo.Container.prototype.addChild = function(c, name) {
var ch = this.children;
c.parent = this;
c.index = ch.length;
if(name) {
c.name = name;
this.childNames[name] = c.index;
}
ch.push(c);
if('addedToParent' in c) c.addedToParent(this);
return this;
};
Allows for multiple children to be added to this Container by passing either an Array or an Object literal.
see addChild
param
{Array|Object} arg
. An array of children to add or an
Object literal in the form {name: child}
returns
{Object} this
sudo.Container.prototype.addChildren = function(arg) {
normalize the arg
var _keys = isArray(arg) ? undefined : keys(arg), ary = _keys || arg;
ary.forEach(function(c) {
_keys ? this.addChild(arg[c], c) : this.addChild(c);
}.bind(this));
return this;
};
By default, bubble
returns the current view's parent (if it has one)
returns
{Object|undefined}
sudo.Container.prototype.bubble = function() {return this.parent;};
Call a named method and pass any args to each child in a container's collection of children
param
{*} Any number of arguments the first of which must be
The named method to look for and call. Other args are passed through
returns
{object} this
sudo.Container.prototype.eachChild = function(/*args*/) {
var args = slice.call(arguments), meth = args.shift();
this.children.forEach(function(c) {if(meth in c) c[meth].apply(c, args);});
return this;
};
If a child was added with a name, via addChild
,
that object can be fetched by name. This prevents us from having to reference a
containers children by index. That is possible however, though not preferred.
param
{String|Number} id
. The string name
or numeric index
of the child to fetch.
returns
{Object|undefined} The found child
sudo.Container.prototype.getChild = function(id) {
return typeof id === 'string' ? this.children[this.childNames[id]] :
this.children[id];
};
Method is called with the index
property of a subview that is being removed.
Beginning at i
decrement subview indices.
param
{Number} i
private
sudo.Container.prototype._indexChildren_ = function(i) {
var c = this.children, obj = this.childNames, len;
for (len = c.length; i < len; i++) {
c[i].index--;
adjust any entries in childNames
if(c[i].name in obj) obj[c[i].name] = c[i].index;
}
};
Find the intended child from my list of children and remove it, removing the name reference and re-indexing
remaining children. This method does not remove the child's DOM.
Override this method, doing whatever you want to the child's DOM, then call base('removeChild')
to do so.
If the child being removed has a removedFromParent
method it will be called after the parenth has
finished, passing itself(the parent) as an argument.
param
{String|Number|Object} arg
. Children will always have an index
number, and optionally a name
.
If passed a string name
is assumed, so be sure to pass an actual number if expecting to use index.
An object will be assumed to be an actual sudo Class Object.
returns
{Object} this
sudo.Container.prototype.removeChild = function(arg) {
var i, t = typeof arg, c;
normalize the input
if(t === 'object') c = arg;
else c = t === 'string' ? this.children[this.childNames[arg]] : this.children[arg];
i = c.index;
remove from the children Array
this.children.splice(i, 1);
remove from the named child hash if present
delete this.childNames[c.name];
child is now an orphan
delete c.parent;
delete c.index;
delete c.name;
this._indexChildren_(i);
if('removedFromParent' in c) c.removedFromParent(this);
return this;
};
sudo.Container.prototype.removeChildren = function() {
var n = this.children.length;
while(n > 0) {
this.removeChild(this.children[n - 1]);
n--;
}
return this;
};
This is a container, yes.
sudo.Container.prototype.role = 'container';
The call to the specific method on a (un)specified target happens here.
If this Object is part of a sudo.Container
maintained hierarchy
the 'target' may be left out, causing the bubble()
method to be called.
What this does is allow children of a sudo.Container
to simply pass
events upward, delegating the responsibility of deciding what to do to the parent.
TODO Currently, only the first target method found is called, then the bubbling is stopped. Should bubbling continue all the way up the 'chain'?
param
{*} Any number of arguments is supported, but the first is the only one searched for info.
A sendMethod will be located by:
sendMethod
property if it is an object
In the case a specified target exists at this.data.sendTarget
it will be used
Any other args will be passed to the sendMethod after this
returns
{Object} this
sudo.Container.prototype.send = function(/*args*/) {
var args = slice.call(arguments),
d = this.data, meth, targ, fn;
normalize the input, common use cases first
if(d && 'sendMethod' in d) meth = d.sendMethod;
else if(typeof args[0] === 'string') meth = args.shift();
less common but viable options
if(!meth) {
passed as a custom data attr bound in events
meth = 'data' in args[0] ? args[0].data.sendMethod :
passed in a hash from something or not passed at all
args[0].sendMethod || undefined;
}
target is either specified or my parent
targ = d && d.sendTarget || this.bubble();
obvious chance for errors here, don't be dumb
fn = targ[meth];
while(!fn && (targ = targ.bubble())) {
fn = targ[meth];
}
sendMethods expect a signature (sender, ...)
if(fn) {
args.unshift(this);
fn.apply(targ, args);
}
return this;
};
Create an instance of a sudo.View object. A view is any object
that maintains its own el
, that being some type of DOM element.
Pass in a string selector or an actual dom node reference to have the object
set that as its el
. If no el
is specified one will be created upon instantiation
based on the tagName
(div
by default) if present in the data
arg.
Specify className
, id
(or other attributes if desired)
as an (optional) attributes
object literal on the data
arg.
The this.el
and this.qs
methods scope queries to this objects el
, i.e it's
a shortcut for this.el.querySelector(selector)
in addition this.qsa
does the same for the querySelectorAll method
param
{string|element} el
. Otional el for the View instance.
param
{object} data
. Optional data object-literal which becomes the initial state
of this.data
on the View.
constructor
sudo.View = function(el, data) {
sudo.Container.call(this);
this.data = data;
this.setEl(el);
};
View inherits from Container
private
sudo.inherit(sudo.Container, sudo.View);
the el needs to be normalized before use
private
sudo.View.prototype._normalizedEl_ = function(el) {
var _el = typeof el === 'string' ? document.querySelector(el) : el;
if there is not a top level query returned the desired node may be in a document fragment not in the DOM yet. We will check the parent's el if available, or return the empty query
return _el ? _el : (this.parent ? this.parent.qs(el) : _el);
};
private
sudo.View.prototype.role = 'view';
A view must have an element, set that here.
Node is always then available as this.el
.
param
{string=|element} el
returns
{Object} this
sudo.View.prototype.setEl = function(el) {
var d = this.data, a, t;
if(!el) {
normalize any relevant data
t = d ? d.tagName || 'div': 'div';
this.el = document.createElement(t);
if(d && (a = d.attributes)) {
iterate and set the attributes
keys(a).forEach(function(k) {
this.el.setAttribute(k, a[k]);
}.bind(this));
}
} else this.el = this._normalizedEl_(el);
return this;
};
Return a single Element matching sel
scoped to this View's el.
This is an alias to this.el.querySelector(sel)
.
param
{string} sel
. A DOM compatible selector
returns
{node|null} A 'querified' result matching the selector
sudo.View.prototype.qs = function(sel) {
return this.el.querySelector(sel);
};
Return multiple Elements (a NodeList) matching sel
scoped to this View's el.
This is an alias to this.el.querySelectorAll(sel)
.
param
{string} sel
. A querySelectorAll compatible selector
returns
{Elements | undefined} Results matching the selector (or undefined if not)
sudo.View.prototype.qsa = function(sel) {
return this.el.querySelectorAll(sel);
};
Implementaion of the ES6 Harmony Observer pattern.
Extend a sudo.Model
class with this object if
data-mutation-observation is required
sudo.extensions.observable = {
_deliver_: function(obj) {
this.callbacks.forEach(function(cb) {
cb(obj);
});
},
Iterate through the changeRecords array(emptying it as you go), delivering them to the observers. You can override this method to change the standard delivery behavior.
returns
{Object} this
deliverChangeRecords: function() {
var rec, cr = this.changeRecords;
FIFO
for(; cr.length && (rec = cr.shift());) {this._deliver_(rec);}
return this;
},
In a quasi-ES6 Object.observe pattern, calling observe on an observable
and
passing a callback will cause that callback to be called whenever any
property on the observable's data store is set, changed or deleted
via set, unset, setPath or unsetPath with an object containing:
{
type:
param
{Function} fn
The callback to be called with changeRecord(s)
returns
{Function} the Function passed in as an argument
observe: function(fn) {
this will fail if mixed-in and no callbacks
created so don't do that.
Per the spec, do not allow the same callback to be added
var d = this.callbacks;
if(!~d.indexOf(fn)) d.push(fn);
return fn;
},
Allow an array of callbacks to be registered as changeRecord recipients
param
{Array} ary
returns
{Array} the Array passed in to observe
observes: function(ary) {
ary.forEach(function(fn) {this.observe(fn);}.bind(this));
return ary;
},
Overrides sudo.Base.set to check for observers
param
{String} key
. The name of the key
param
{*} value
param
{Bool} hold
Call deliver (falsy) or store the change notification
to be delivered upon a call to deliverChangeRecords (truthy)
returns
{Object|*} this
or calls deliverChangeRecords
set: function(k, v, hold) {
var obj = {name: k, object: this.data};
did this key exist already
if(k in this.data) {
obj.type = 'updated';
then there is an oldValue
obj.oldValue = this.data[k];
} else obj.type = 'new';
now actually set the value
this.data[k] = v;
this.changeRecords.push(obj);
call the observers or not
if(hold) return this;
return this.deliverChangeRecords();
},
Overrides sudo.Base.setPath to check for observers.
Change records originating from a setPath
operation
send back the passed in path
as name
as well as the
top level object being observed (this observable's data).
this allows for easy filtering either manually or via a
change delegate
param
{String} path
param
{} value
param
{Bool} hold
Call deliver (falsy) or store the change notification
to be delivered upon a call to deliverChangeRecords (truthy)
returns
{Object|} this
or calls deliverChangeRecords
setPath: function(path, v, hold) {
var curr = this.data, obj = {name: path, object: this.data},
p = path.split('.'), k;
for(; p.length && (k = p.shift());) {
if(!p.length) {
reached the last refinement, pre-existing?
if (k in curr) {
obj.type = 'updated';
obj.oldValue = curr[k];
} else obj.type = 'new';
curr[k] = v;
} else if (curr[k]) {
curr = curr[k];
} else {
curr = curr[k] = {};
}
}
this.changeRecords.push(obj);
call all observers or not
if(hold) return this;
return this.deliverChangeRecords();
},
Overrides Base.sets to hold the call to deliver until all operations are done
returns
{Object|*} this
or calls deliverChangeRecords
sets: function(obj, hold) {
keys(obj).forEach(function(k) {
!~k.indexOf('.') ? this.set(k, obj[k], true) :
this.setPath(k, obj[k], true);
}.bind(this));
if(hold) return this;
return this.deliverChangeRecords();
},
Remove a particular callback from this observable
param
{Function} the function passed in to observe
returns
{Object} this
unobserve: function(fn) {
var cb = this.callbacks, i = cb.indexOf(fn);
if(~i) cb.splice(i, 1);
return this;
},
Allow an array of callbacks to be unregistered as changeRecord recipients
param
{Array} ary
returns
{Object} this
unobserves: function(ary) {
ary.forEach(function(fn) {this.unobserve(fn);}.bind(this));
return this;
},
Overrides sudo.Base.unset to check for observers
param
{String} key
. The name of the key
param
{Bool} hold
returns
{Object|*} this
or calls deliverChangeRecords
unset: function(key, hold) {
var obj = {name: key, object: this.data, type: 'deleted'},
val = !!this.data[key];
delete this.data[key];
call the observers if there was a val to delete
return this._unset_(obj, val, hold);
},
_unset_: function(o, v, h) {
if(v) {
this.changeRecords.push(o);
if(h) return this;
return this.deliverChangeRecords();
}
return this;
},
Overrides sudo.Base.unsetPath to check for observers
param
{String} path
param
{*} value
param
{bool} hold
returns
{Object|*} this
or calls deliverChangeRecords
unsetPath: function(path, hold) {
var obj = {name: path, object: this.data, type: 'deleted'},
curr = this.data, p = path.split('.'),
k, v;
for (; p.length && (k = p.shift());) {
if(!p.length) {
reached the last refinement
v = !!curr[k];
delete curr[k];
} else {
this can obviously fail, but can be prevented by checking
with getPath
first.
curr = curr[k];
}
}
return this._unset_(obj, v, hold);
},
Override of Base.unsets to hold the call to deliver until done
param
ary
param
hold
returns
{Object|*} this
or calls deliverChangeRecords
unsets: function(ary, hold) {
ary.forEach(function(k) {
!~k.indexOf('.') ? this.unset(k, true) : this.unsetPath(k, true);
}.bind(this));
if(hold) return this;
return this.deliverChangeRecords();
}
};
sudo.version = "0.9.9";
window.sudo = sudo;
if(!window._) window._ = sudo;
}(window));