Commits

Anonymous committed 0b6ce45

Huge and fuzzy iteration involving progress on many fronts, but most prominently the new control plugin (formerly known as employ, state and silo) and a collection of core classes such as $.al.Field and $.al.Meta.

  • Participants
  • Parent commits 376ebdb

Comments (0)

Files changed (23)

File demo/control.html

+<!DOCTYPE html>
+<html lang="en" class="no-js">
+
+	<head>
+		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+		
+		<title>
+			Control
+		</title>
+		
+	    <script type="text/javascript" src="../lib/modernizr-1.5.min.js"></script>
+		<style type="text/css">
+		</style>
+	</head>
+	
+	<body>
+		<div id="header" class="stateful">
+			Demo
+		</div>
+		
+		<ul id="nav" class="stateful">
+			<li class="campaigns">
+				<a href="#/campaigns">Campaigns</a>
+			</li>
+			<li class="contacts">
+				<a href="#/contacts">Contacts</a>
+				<ul id="tag-list" class="stateful">
+					<li class="group">My contacts</li>
+					<!--
+						<li class="tag"><%= name %> (<%= count %>)</li>
+					-->
+				</ul>
+			</li>
+			<li class="notes">
+				<a href="#/notes">Notes</a>
+			</li>
+		</ul>
+		
+		<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js"></script>
+		<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/jquery-ui.js"></script>
+		<script type="text/javascript" src="../lib/underscore.js"></script>
+		<script type="text/javascript" src="../src/jquery.al.field.js"></script>
+		<script type="text/javascript" src="../src/jquery.al.controller.js"></script>
+		<script type="text/javascript">
+		</script>
+	</body>
+
+</html>

File demo/pre.html

+<!DOCTYPE html>
+<html lang="en" class="no-js">
+
+	<head>
+		<meta http-equiv="X-UA-Compatible" content="IE=Edge">
+		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+		
+		<title>
+			pre-wrap
+		</title>
+		
+	    <script type="text/javascript" src="../lib/modernizr-1.5.min.js"></script>
+		<style type="text/css">
+			
+			div {
+				white-space: pre-wrap;
+				border: 1px solid red;
+			}
+			
+		</style>
+	</head>
+	
+	<body>
+		
+<div>line1
+	
+line3</div>
+<div>line1\n\nline3</div>
+		
+	</body>
+
+</html>

File demo/state.html

+<!DOCTYPE html>
+<html lang="en" class="no-js">
+
+	<head>
+		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+		
+		<title>
+			State
+		</title>
+		
+	    <script type="text/javascript" src="../lib/modernizr-1.5.min.js"></script>
+		<style type="text/css">
+			
+			body, body > * {
+				position: absolute;
+				overflow-x: hidden;
+				overflow-y: hidden;
+			}
+			
+			body {
+				top: 0;
+				right: 0;
+				bottom: 0;
+				left: 0;
+				margin: 0;
+				font-family: Helvetica;
+				font-size: 80%;
+			}
+			
+			.stateful {
+				display: none;
+			}
+			
+			#nav {
+				top: 0;
+				bottom: 0;
+				left: 0;
+				width: 200px;
+				background-color: #eee;
+				margin: 0;
+				list-style-type: none;
+			}
+			#nav > li {
+				position: absolute;
+				right: 0;
+				left: 0;
+			}
+			#nav > li > a {
+				position: absolute;
+				top: 0;
+				right: 0;
+				left: 0;
+				height: 15px;
+				padding: 4px;
+				font-weight: bold;
+				color: #222;
+				text-decoration: none;
+				border-top: 1px solid #fff;
+				border-bottom: 1px solid #ccc;
+			}
+			#nav.campaigns > li.campaigns > a,
+			#nav.contacts > li.contacts > a,
+			#nav.notes > li.notes > a {
+				border-style: none;
+				padding: 5px 4px;
+				background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc));
+			}
+			
+			#nav > li.campaigns {
+				top: 0;
+			}
+			
+			#nav.campaigns > li.campaigns {
+				bottom: 4em;
+			}
+			#nav.campaigns > li.contacts {
+				bottom: 2em;
+				height: 2em;
+			}
+			#nav.campaigns > li.notes {
+				bottom: 0;
+				height: 2em;
+			}
+			
+			#nav.contacts > li.campaigns {
+				height: 2em;
+			}
+			#nav.contacts > li.contacts {
+				top: 2em;
+				bottom: 2em;
+			}
+			#nav.contacts > li.notes {
+				bottom: 0;
+				height: 2em;
+			}
+			
+			#nav.notes > li.campaigns {
+				height: 2em;
+			}
+			#nav.notes > li.contacts {
+				top: 2em;
+				height: 2em;
+			}
+			#nav.notes > li.notes {
+				top: 4em;
+				bottom: 0;
+			}
+			
+			#tag-list {
+				visibility: hidden;
+				position: absolute;
+				top: 2em;
+				right: 0;
+				bottom: 0;
+				left: 0;
+				overflow-x: hidden;
+				overflow-y: auto;
+				margin: 0;
+				padding: 0;
+				background-color: #fafafa;
+				list-style-type: none;
+			}
+			#tag-list.ready {
+				visibility: visible;
+			}
+			#tag-list li {
+				cursor: default;
+				padding: .3em .8em .3em 1em;
+			}
+			#tag-list li.group {
+				padding: .5em .8em .5em .8em;
+				font-weight: bold;
+				color: #222;
+				text-decoration: none;
+			}
+			#tag-list li.tag {
+				padding-left: 2em;
+				font-size: .85em;
+				font-style: italic;
+			}
+			#tag-list li.checked {
+				color: white;
+				background: -webkit-gradient(linear, left top, left bottom, from(#aaf), to(#00f));
+			}
+			
+		</style>
+	</head>
+	
+	<body>
+		<div id="header" class="stateful">
+			Demo
+		</div>
+		
+		<ul id="nav" class="stateful">
+			<li class="campaigns">
+				<a href="#/campaigns">Campaigns</a>
+			</li>
+			<li class="contacts">
+				<a href="#/contacts">Contacts</a>
+				<ul id="tag-list" class="stateful">
+					<li class="group">My contacts</li>
+					<!--
+						<li class="tag"><%= name %> (<%= count %>)</li>
+					-->
+				</ul>
+			</li>
+			<li class="notes">
+				<a href="#/notes">Notes</a>
+			</li>
+		</ul>
+		
+		<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js"></script>
+		<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/jquery-ui.js"></script>
+		<script type="text/javascript" src="../lib/underscore.js"></script>
+		<script type="text/javascript" src="../src/jquery.al.util.js"></script>
+		<script type="text/javascript" src="../src/jquery.al.data.js"></script>
+		<script type="text/javascript" src="../src/jquery.al.rest.js"></script>
+		<script type="text/javascript" src="../src/jquery.al.state.js"></script>
+		<script type="text/javascript" src="../src/jquery.al.route.js"></script>
+		<script type="text/javascript" src="../src/jquery.al.flirt.js"></script>
+		<script type="text/javascript" src="../src/jquery.al.dataview.js"></script>
+		<script type="text/javascript" src="../src/jquery.al.listview.js"></script>
+		<script type="text/javascript" src="../src/jquery.al.flaggable.js"></script>
+		<script type="text/javascript" src="http://assets.smartpr.dev/jquery.smartpr/src/jquery.smartpr.js"></script>
+		<script type="text/javascript">
+			
+			jQuery(function($) {
+				
+				$.route({
+					name: 'campaigns',
+					pattern: /^\/campaigns$/
+				}, {
+					name: 'contacts',
+					pattern: /^\/contacts$/
+				}, {
+					name: 'notes',
+					pattern: /^\/notes$/
+				});
+				
+				$('#nav').state({
+					
+					observe: {
+						routechange: window
+					},
+					
+					value: function() {
+						return {
+							name: $.route('get') ? $.route('get').route.name : undefined
+						};
+					},
+					
+					isActive: function(value) {
+						return !!$.route('get');
+					},
+					
+					init: function(e, data) {
+						$(this).addClass(data.value.name);
+					},
+					
+					change: function(e, data) {
+						$(this).
+							removeClass().addClass('stateful').	// FIXME
+							addClass(data.value.name);
+					}
+					
+				});
+				
+				$('#tag-list').state({
+					
+					observe: {
+						'statechange stateinit': '#nav'
+					},
+					
+					isActive: function() {
+						var nav = $('#nav').state('value');
+						
+						return nav !== null && nav.name === 'contacts';
+					},
+					
+					init: function() {
+						var $this = $(this);
+						
+						$this.
+							listview({
+								key: 'id',
+								data: function(cb) {
+									$.smartpr.tags.get(function(data) {
+										cb(data);
+										$this.addClass('ready');
+									});
+								}
+							}).
+							bind('flaggableinvalidateflagged flaggableinvalidateunflagged', function(e, data) {
+								$(data.elements)[(e.type === 'flaggableinvalidateflagged' ? 'add' : 'remove') + 'Class']('checked');
+							}).
+							flaggable({
+								id: 'id',
+								elements: 'li',
+								data: function() {
+									var data = $(this).dataview('get');
+									return data === undefined ? 'all' : data;
+								}
+							}).
+							flaggable('change', 'all').
+							delegate('li', 'click', function() {
+								$this.flaggable('change', $(this).dataview('get') === undefined ? 'all' : $(this).dataview('get'));	// FIXME
+							});
+					}
+					
+				});
+				
+			});
+			
+		</script>
+	</body>
+
+</html>

File doc/reserved

Binary file added.

File src/jquery.al.control.js

+(function($) {
+
+$.control = {};
+
+$.control.field = function(initial) {
+	return $.al.Meta($.al.Field).
+		sleep(true).
+		val(initial);
+};
+
+$.control.conjunction = function(initial) {
+	return $.al.Meta($.al.ConjunctionField).
+		sleep(true).
+		val(initial);
+};
+
+$.control.Controller = $.al.ResigClass.extend({
+	
+	init: function(element) {
+		var self = this;
+		
+		if (!(element instanceof $)) {
+			element = $(element);
+		}
+		this.element = element;
+		
+		var member,
+			fieldDefinitions = {};
+		
+		// Make this a valid controller instance by replacing all field
+		// definitions with actual fields. From this moment on, controller
+		// members can safely be bound and observed.
+		for (member in this) {
+			if (this[member] instanceof $.al.Meta) {
+				fieldDefinitions[member] = this[member];
+				this[member] = this[member].instance(undefined, this);
+				// FIXME: we should be able to chain this onto the previous
+				// line of code. But this caused problems, so look into this
+				// upon testing.
+				this[member].triggersOn(this.element);
+				// 
+				// fieldDefinitions[member] = this[member];
+				// this[member] = fieldDefinitions
+				// // W00t this is TEM-PO-RA-RY!
+				// if (fieldDefinitions[member].playbackConjunction) {
+				// 	this[member] = $.al.ConjunctionField(undefined, this);
+				// } else {
+				// 	this[member] = $.al.Field(undefined, this);
+				// }
+			}
+		}
+		
+		var prefix, observed, observer;
+		
+		// Setup some automagical method bindings.
+		for (member in this) {
+			
+			if ($.isFunction(this[member])) {
+				
+				prefix = 'observe';
+				if (member.length > prefix.length && member.substr(0, prefix.length) === prefix) {
+					observed = member.substr(prefix.length, 1).toLowerCase() + member.substr(prefix.length + 1);
+					if (this[observed] instanceof $.al.Field) {
+						observer = this[member];
+						this[observed].observe(observer);
+					}
+				}
+			
+				// Bind any method that starts with 'on' as a handler to the
+				// corresponding event on the controller's element.
+				prefix = 'on';
+				if (member.length > prefix.length && member.substr(0, prefix.length) === prefix) {
+					this.element.bind(member.substr(2, 1).toLowerCase() + member.substr(3), $.proxy(this, member));
+				}
+				
+			}
+			
+		}
+		
+		// Add baseline display events before recorder is played-back, to
+		// allow controller definitions to adjust or clear them if desired.
+		if (this.display instanceof $.al.Field) {
+			this.display.
+				triggers('controldisplaychange').
+				triggers({
+					controlshow: true,
+					controlhide: false
+				});
+		}
+		
+		for (member in fieldDefinitions) {
+			fieldDefinitions[member].playback(this[member]);
+		}
+		
+	},
+	
+	displayInit: $.noop,
+	
+	observeDisplay: function(value) {
+		this.element[value ? 'show' : 'hide']();
+		if (value && this.displayed !== true) {
+			this.displayed = true;
+			this.displayInit();
+		}
+	}
+	
+});
+
+$.fn.control = function(action, value) {
+	if (typeof action !== 'string') {
+		value = action;
+		this.control('extend', value);
+		return this.control('init');
+	}
+	
+	return this.each(function() {
+		var $this = $(this);
+		
+		// TODO: if instance exists, we do nothing. It would be nice if we could
+		// just override earlier instances, but that's scary as those are potentially
+		// already referenced in a gazillion places, which would result in the instance
+		// remaining active but no within reachable.
+		if ($this.fetch('control', 'instance')) {
+			return true;
+		}
+		
+		var Class = $this.fetch('control', 'class') || $.control.Controller;
+
+		if (action === 'extend') {
+			$this.store('control', 'class', Class.extend(value));
+			return true;
+		}
+
+		if (action === 'init') {
+			var instance = new Class(this);
+			for (member in instance) {
+				if (instance[member] instanceof $.al.Field) {
+					instance[member].sleep(false);
+				}
+			}
+			$this.store('control', 'instance', instance);
+			return true;
+		}
+		
+	});
+	
+};
+
+}(jQuery));

File src/jquery.al.core.js

+(function($) {
+
+$.al = $.al || {};
+
+var initializing = false;
+var Flag = function() {};
+var flag = new Flag();
+$.al.MolendijkClass = function() {
+	if (!(this instanceof $.al.MolendijkClass)) {
+		return new $.al.MolendijkClass();
+	}
+};
+$.al.MolendijkClass.extend = function(wrap) {
+	var Class = this;
+	var Subclass = function() {
+		// idea: i can also instantiate Subclass without arguments if
+		// this is not an instance of Subclass, and then use the supplied
+		// arguments to this function call (as it's not an instantiation)
+		// in the remainder. Then at last return the instantiated Subclass.
+		if (!(this instanceof Subclass)) {
+			return new Subclass(flag, arguments);
+		}
+		if (!initializing) {
+			var args;
+			if (arguments[0] === flag) {
+				args = arguments[1];
+			} else {
+				args = $.makeArray(arguments);
+			}
+			var parent = Class.apply(undefined, args);
+			// TODO: don't copy properties that are only present
+			// on the parent's prototype (and not on its instance) -- one of
+			// them: constructor
+			for (p in parent) {
+				this[p] = parent[p];
+			}
+			wrap.apply(this, args);
+		}
+	};
+	initializing = true;
+	Subclass.prototype = new Class();
+	initializing = false;
+	Subclass.extend = this.prototype.constructor.extend;
+	Subclass.prototype.constructor = Subclass;
+	return Subclass;
+};
+// TODO: add 'create' class method.
+
+// TODO: Support for supplying an object instance as value for methods.
+$.al.Meta = $.al.MolendijkClass.extend(function(type, instance, playback) {
+	var self = this;
+	
+	if (typeof instance !== 'string') {
+		instance = 'instance';
+	}
+	if (typeof playback !== 'string') {
+		playback = 'playback';
+	}
+	
+	self[instance] = function() {
+		return type.apply(this, arguments);
+	};
+	
+	self[playback] = function(on) {
+		if (!(on instanceof type)) {
+			on = self[instance].apply(self, arguments);
+		}
+		for (var i = 0, l = record.length; i < l; i++) {
+			on[record[i].method].apply(record[i].context, record[i].arguments);
+		}
+		return on;
+	};
+	
+	var methods = _.keys(self[instance]()),
+		record = [];
+	$.each(methods, function(i, method) {
+		self[method] = function() {
+			record.push({
+				method: method,
+				context: this,
+				arguments: arguments
+			});
+			return self;
+		};
+	});
+	
+});
+
+$.al.Field = $.al.MolendijkClass.extend(function(base, context) {
+	var self = this,
+		$registry = $('<div />');
+	
+	var lastSignal = {};
+	var signal = function(type) {
+		var change = {
+			from: type in lastSignal ? lastSignal[type] : self.base(),
+			to: self.val()
+		};
+		if (change.from !== change.to) {
+			lastSignal[type] = change.to;
+			$registry.trigger('fieldchange' + type, change);
+		}
+	};
+	var notify = function() {
+		if (self.sleep()) {
+			return;
+		}
+		signal('silent');
+		if (self.notifies()) {
+			signal('notify');
+		}
+	};
+	
+	// The idea behind base is that it is the field's baseline value; the
+	// value against which is decided if a value change has occurred or not.
+	// Setting the base value will never cause notifications.
+	// FIXME: Get rid of this concept.
+	self.base = function(i) {
+		if (arguments.length === 0) {
+			return base;
+		}
+		base = i;
+		return self;
+	};
+	
+	var val;
+	self.val = function(v) {
+		if (arguments.length === 0) {
+			return val === undefined ? self.base() : val;
+		}
+		val = v;
+		notify();
+		return self;
+	};
+	
+	self.context = function(c) {
+		if (arguments.length === 0) {
+			return context === undefined ? self : context;
+		}
+		context = c;
+		return self;
+	};
+	
+	// TODO: We can move this one to prototype (but do we want to?)
+	self.bind = function() {
+		var args = arguments,
+			binding;
+		
+		// Scenario 1: binding is defined in a setup function.
+		if (args.length === 1 && $.isFunction(args[0])) {
+			binding = args[0];
+		}
+		
+		// Scenario 2: binding is defined as an event handler and optional
+		// data tree path.
+		else if (args.length >= 2 && typeof args[1] === 'string') {
+			binding = function(val) {
+				var on = args[0];
+				if (!(on instanceof $)) {
+					on = $(on);
+				}
+				on.bind(args[1], function(e, data) {
+					for (var i = 2, l = args.length; i < l; i++) {
+						if ($.isFunction(args[i])) {
+							return args[i].apply(this, $.merge([val], arguments));
+						}
+						data = data[args[i]];
+					}
+					val(data);
+				});
+			};
+		}
+		
+		// Scenario 3: binding is defined as an observer for a field which is
+		// held in a property of our context.
+		else if (args.length >= 1 && typeof args[0] === 'string') {
+			binding = function(val) {
+				this[args[0]].observe(function(v) {
+					if ($.isFunction(args[1])) {
+						return args[1].call(this, val, v);
+					}
+					val(v);
+				}, args[2]);
+			};
+		}
+		
+		if ($.isFunction(binding)) {
+			binding.call(self.context(), self.val);
+		}
+		
+		return self;
+	};
+	
+	self.observe = function(observer, silent) {
+		$registry.bind('fieldchange' + (silent ? 'silent' : 'notify'), function(e, data) {
+			// TODO: Supply of data.from is not documented/tested.
+			observer.call(self.context(), data.to, data.from);
+		});
+		return self;
+	};
+	
+	var triggersOn;
+	self.triggersOn = function(on) {
+		if (arguments.length === 0) {
+			return triggersOn === undefined ? self.context() : triggersOn;
+		}
+		triggersOn = on;
+		return self;
+	};
+	
+	var triggers = {};
+	var always = function() {
+		return true;
+	};
+	self.triggers = function(events) {
+		if (arguments.length === 0) {
+			return triggers;
+		}
+		
+		var extension = {};
+		
+		// Short-cut to trigger on every change.
+		if (typeof events === 'string') {
+			extension[events] = always;
+		}
+		// Any sort of object is interpreted as a collection of trigger
+		// behavior.
+		else if (typeof events === 'object') {
+			$.each(events, function(key, value) {
+				if ($.isFunction(value)) {
+					extension[key] = value;
+				} else {
+					extension[key] = function(v) {
+						return v === value;
+					}
+				}
+			});
+		}
+		
+		if (!$.isEmptyObject(extension)) {
+			$.extend(triggers, extension);
+		}
+		// A falsy value means trigger no events at all.
+		else if (!events) {
+			triggers = {};
+		}
+		
+		return self;
+	};
+	
+	// We do not create a field instance before notifies is explicitly set
+	// because otherwise we would end up in a field instantiation loop.
+	var notifies;
+	self.notifies = function(condition) {
+		if (arguments.length === 0) {
+			return notifies === undefined ? true : !!notifies.val();
+		}
+		// TODO: Functions should not be evaluated now but every time the
+		// notifies value is needed. It would be nice if we can implement this
+		// behavior by adding this feature to the general field
+		// implementation.
+		if ($.isFunction(condition)) {
+			condition = condition.call(self.context());
+		}
+		// String values are interpreted as properties of our context.
+		else if (typeof condition === 'string') {
+			condition = self.context()[condition];
+		}
+		if (!(condition instanceof $.al.Field)) {
+			condition = $.al.Field(condition);
+		}
+		// notifies is now guaranteed to be a field.
+		notifies = condition.observe(function() {
+			// notify can be safely called at any time, regardless of the
+			// value of notifies and the value that was last notified, as it
+			// will check for itself if it needs to do actual signaling.
+			notify();
+		});
+		// Observer is not called for current value of notifies, so call
+		// notify right now as well.
+		notify();
+		return self;
+	};
+	
+	var sleep = false;
+	self.sleep = function(s) {
+		if (arguments.length < 1) {
+			return sleep;
+		}
+		sleep = !!s;
+		notify();
+		return self;
+	};
+	
+	// Triggering events is always done along with notification, so we can
+	// define one in terms of the other.
+	self.observe(function(v) {
+		var eventData = {
+			to: v
+		};
+		$.each(self.triggers(), function(type, condition) {
+			if (condition.call(self.context(), v) === true) {
+				var on = self.triggersOn();
+				if (!(on instanceof $)) {
+					on = $(on);
+				}
+				on.trigger(type, eventData);
+			}
+		});
+	});
+	
+});
+
+$.al.ConjunctionField = $.al.Field.extend(function() {
+	var self = this,
+		operands = [];
+	
+	delete self.bind;
+	
+	self.operand = function() {
+		operands.push($.al.Field(false, self.context()).
+			observe(function(v) {
+				var conjunction = true;
+				for (var i = 0, l = operands.length; i < l; i++) {
+					conjunction = conjunction && !!operands[i].val();
+				}
+				self.val(conjunction);
+			}).
+			bind.apply(undefined, arguments));
+		return self;
+	};
+	
+});
+
+}(jQuery));
+
+/* Simple JavaScript Inheritance
+ * By John Resig http://ejohn.org/
+ * MIT Licensed.
+ */
+// Inspired by base2 and Prototype
+(function(){
+  var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
+  // The base Class implementation (does nothing)
+  this.al.ResigClass = function(){};
+  
+  // Create a new Class that inherits from this class
+  this.al.ResigClass.extend = function(prop) {
+    var _super = this.prototype;
+    
+    // Instantiate a base class (but only create the instance,
+    // don't run the init constructor)
+    initializing = true;
+    var prototype = new this();
+    initializing = false;
+    
+    // Copy the properties over onto the new prototype
+    for (var name in prop) {
+      // Check if we're overwriting an existing function
+      prototype[name] = typeof prop[name] == "function" && 
+        typeof _super[name] == "function" && fnTest.test(prop[name]) ?
+        (function(name, fn){
+          return function() {
+            var tmp = this._super;
+            
+            // Add a new ._super() method that is the same method
+            // but on the super-class
+            this._super = _super[name];
+            
+            // The method only need to be bound temporarily, so we
+            // remove it when we're done executing
+            var ret = fn.apply(this, arguments);        
+            this._super = tmp;
+            
+            return ret;
+          };
+        })(name, prop[name]) :
+        prop[name];
+    }
+    
+    // The dummy class constructor
+    function Class() {
+      // All construction is actually done in the init method
+      if ( !initializing && this.init )
+        this.init.apply(this, arguments);
+    }
+    
+    // Populate our constructed prototype object
+    Class.prototype = prototype;
+    
+    // Enforce the constructor to be what we expect
+    Class.constructor = Class;
+
+    // And make this class extendable
+    Class.extend = arguments.callee;
+    
+    return Class;
+  };
+}).call(jQuery);

File src/jquery.al.employ.js

+(function($) {
+
+// TODO: Bring back employready
+
+$.fn.employ = function(status) {
+	var state = status === true ? $.makeArray(arguments).slice(1) : undefined;
+	
+	return this.each(function() {
+		var $this = $(this),
+			oldState = $this.fetch('employ', 'state'),
+			oldStatus = oldState !== undefined && $this.is(':visible'),
+			stateChange = !_.isEqual(state, oldState);
+		
+		if (status === oldStatus && !stateChange) {
+			return true;
+		}
+		if (status === false) {
+			$this.
+				hide().
+				trigger('employsleep');
+		}
+		if (status === true) {
+			$this.show();
+			// console.log(state);
+			if (stateChange) {
+				if (oldState !== undefined) {
+					$this.trigger('employdeconstruct', oldState);
+				}
+				$this.
+					store('employ', 'state', state).
+					trigger('employconstruct', state);
+			}
+			$this.trigger('employready');
+		}
+		
+	});
+	
+};
+
+}(jQuery));
+
+
+
+
 /*
 (function($) {
 
 // which is not the same as "unemploy"
 // TODO: Implement status getter: $('...').employ() ...? or revise all
 // interfaces to sth like: .employ('change', true|false), .employ('status')
-$.fn.employ = function(status) {
+$.fn.employ_old = function(status) {
 	var $this = this,
 		state = status === true ? $.makeArray(arguments).slice(1) : undefined;
 	
 							$(this).trigger('employdeconstruct', $.merge([fcb], currentState));
 						}
 					}, function(fcb) {
-						// console.log('construct');
+						console.log('construct');
 						$(this).store('employ', 'state', state);
 						$(this).trigger('employconstruct', $.merge([fcb], state));
 					}, function(fcb) {
+						console.log('after construct');
 						mfcb.call();
 					}]).
 					dequeue('employ');
 	return $this;
 };
 
+var getStatus = function() {
+	var $this = $(this);
+	
+	return $this.fetch('employ', 'state') !== undefined && $this.is(':visible');
+};
+
+var undisplay = function() {
+	$(this).hide();
+};
+var display = function() {
+	var $this = $(this);
+	
+	$this.store('manipulate', 'visibility', this.style.visibility);
+	this.style.visibility = 'hidden';
+	$this.show();
+};
+var restoreVisibility = function() {
+	var $this = $(this);
+	
+	this.style.visibility = $this.fetch('manipulate', 'visibility');
+	$this.del('manipulate', 'visibility');
+};
+var sleep = function(fcb) {
+	$(this).trigger('employsleep', [fcb]);
+};
+var ready = function(fcb) {
+	$(this).trigger('employready', [fcb]);
+};
+
+$.fn.employ_overlycomplicated = function(status, cb) {
+	var $this = this,
+		state = status === true ? $.makeArray(arguments).slice(2) : undefined;
+	cb = $.isFunction(cb) ? cb : $.noop;
+	if (typeof status !== 'boolean') {
+		cb();
+		return $this.chain(getStatus);
+	}
+	
+	// TODO: Rename to employOuter.
+	$().
+		flexiqueue('employ.outer', function(fcbOuter) {
+			fcbOuter.expect($this.length);
+		
+			$this.each(function() {
+				var $this = $(this),
+					oldState = $this.fetch('employ', 'state'),
+					oldStatus = $this.chain(getStatus),
+					queue = [];
+			
+				if (status === false) {
+					queue.push(undisplay, sleep);
+				} else {
+					queue.push(display);
+					if (!_.isEqual(state, oldState)) {
+						if (oldState !== undefined) {
+							queue.push(function(fcbInner) {
+								$(this).trigger('employdeconstruct', $.merge([fcbInner], oldState));
+							}, function() {
+								// TODO: We might just remove this function,
+								// as the state value will be updated in the
+								// upcoming employconstruct function. But; if
+								// we want to support unemploy with forced
+								// deconstruct, we will be needing it.
+								$(this).del('employ', 'state');
+							});
+						}
+						queue.push(function(fcbInner) {
+							$(this).
+								store('employ', 'state', state).
+								trigger('employconstruct', $.merge([fcbInner], state));
+						});
+					}
+					queue.push(restoreVisibility, ready);
+				}
+				
+				queue.push(function() {
+					fcbOuter.call();
+				});
+				
+				$this.flexiqueue('employ.inner', queue).dequeue('employ.inner');
+			
+			});
+		
+		}).flexiqueue('employ.outer', function() {
+			cb();
+		}).dequeue('employ.outer');
+	
+};
+
 }(jQuery));

File src/jquery.al.flaggable.js

 	
 	_create: function() {
 		var self = this,
-			flag = self.options.flag || $.noop,
-			unflag = self.options.unflag || $.noop;
+			flag = $.isFunction(self.options.flag) ? self.options.flag : $.noop,
+			unflag = $.isFunction(self.options.unflag) ? self.options.unflag : $.noop;
 		
 		if (self.options.data === true) {
 			self.options.data = function() {
 			};
 		}
 		
-		// $.extend(self.options, {
-		// 	flag: function(e, data) {
-		// 		self._trigger('invalidateFlagged', e, {elements: self._elementsWithData(data.items)});
-		// 		return flag.apply(this, arguments);
-		// 	},
-		// 	unflag: function(e, data) {
-		// 		self._trigger('invalidateUnflagged', e, {elements: self._elementsWithData(data.items)});
-		// 		return unflag.apply(this, arguments);
-		// 	}
-		// });
+		// TODO: Introduce invalidatechanged event(?)
+		$.extend(self.options, {
+			flag: function(e, data) {
+				self._trigger('invalidateflagged', e, {elements: self._elementsWithData(data.items)});
+				return flag.apply(this, arguments);
+			},
+			unflag: function(e, data) {
+				self._trigger('invalidateunflagged', e, {elements: self._elementsWithData(data.items)});
+				return unflag.apply(this, arguments);
+			}
+		});
 		
 		self._flagged = [];	// $.RecordSet(self.options.id);
 		self._inverted = false;

File src/jquery.al.flexicallback.js

 - always use "new Flexicallback" as it's more efficient
 - don't use $.extend(this, {}) inside Flexicallback as it adds to the noise
 - put Flexicallback in a var declaration of its own to reduce indentation
+- we cannot do fcb.call.call(anything) because fcb.call expects itself as the
+	context... can't we make this more robust?
+- make a $.flexiqueue for general (non-element-based) queues(??)
 */
 
 (function($) {
 		};
 	};
 
-$.fn.flexiqueue = function() {
-	var args = $.makeArray(arguments),
-		d = typeof args[0] === 'string' ? 1 : 0,
-		data = args[d];
-	
-	if ($.isFunction(data)) {
-		args[d] = flexify(data);
-	} else if ($.isArray(data)) {
-		for (var i = 0, l = data.length; i < l; i++) {
-			data[i] = flexify(data[i]);
-		}
+$.fn.flexiqueue = function(name, queue) {
+	var args = [];
+	if (typeof name !== 'string') {
+		queue = name;
+	} else {
+		args.push(name);
 	}
-	
+	if ($.isFunction(queue)) {
+		args.push(flexify(queue));
+	} else if ($.isArray(queue)) {
+		args.push($.map(queue, function(item) {
+			return flexify(item);
+		}));
+	}
 	return $.fn.queue.apply(this, args);
 };
 

File src/jquery.al.flirt.js

+// TODO: Add a function to the flirt run-time environment which can tell us if
+// we are at the first or last item of a list. Use case: allow for easy
+// generation of comma-separated lists.
+
+// TODO: Find out why it does not work on <title />, and if we can change
+// this situation.
+
 (function($) {
 
 var settings = {
 					});
 				}
 				
+				// TODO: Think about progressive DOM insertion -- questionable
+				// when this is worth complicating stuff for... what exactly
+				// would we expect to achieve by progressive DOM insertion?
 				$template.before(flirt.parse(data, cb).store('flirt', 'clearable', true));
 			});
 			

File src/jquery.al.listview.js

 (function($) {
 
 // TODO: rename 'threshold' option to 'display' ?
+// TODO: add option to cancel autoload (upon create)
+
 $.widget('al.listview', {
 	
 	options: {
 	reload: function(cb) {
 		var self = this;
 		
+		// FIXME: Data should not be deleted until right before new data is to
+		// be appended, inside the data callback. Else we can have the
+		// scenario in which another reload call is done while the request is
+		// still out there, resulting in the data from the second reload being
+		// appended to the data from the first.
 		delete self._data;
 		self._expectCount = null;
 		self.load(cb);
 	// options.key being null or not.
 	// TODO: Get rid of return value, as it makes calling on multiple instances
 	// simultaneously impossible.
+	// TODO: There should be a way to load data so that it is prepended to the
+	// existing data set.
 	load: function(data, cb) {
 		var self = this;
 		if ($.isFunction(data)) {

File src/jquery.al.rest.js

 	
 	this.request = function(verb, handler, data, success) {
 		setTimeout(function() {
+			var contentType = (verb === 'POST' || verb === 'PUT') ?
+				'application/json' :
+				'application/x-www-form-urlencoded';
+			
 			$.ajax({
 				type: verb,
 				url: url + handler,
 				dataType: dataType,
-				data: data,
-				// contentType: 'application/json',
+				contentType: contentType,
+				processData: contentType === 'application/x-www-form-urlencoded',
+				data: contentType === 'application/json' ? JSON.stringify(data) : data,
+				// TODO: smartpr api expects traditional (I think?)
 				// traditional: true,
 				complete: function(xhr, textStatus) {
 					// console.log('$.ajax complete:');
 		this.request('GET', handler, data, success);
 	},
 	post: function(handler, data, success) {
-		this.request('POST', handler + '?callback=?', data, success);
+					// TODO: add callback to handler in case of jsonp?
+		this.request('POST', handler, data, success);
 	},
 	put: function(handler, data, success) {
-		this.request('PUT', handler + '?callback=?',  data, success);
+		this.request('PUT', handler, data, success);
 	}
 	
 };

File src/jquery.al.route.js

+(function($) {
+
+$.route = function(action) {
+	if (action === 'get') {
+		return $(window).fetch('route', 'match');
+	}
+	
+	var routes = $.makeArray(arguments);
+	
+	$(window).bind('hashchange', function() {
+		var $this = $(this),
+			hash = location.hash,
+			oldMatch = $this.fetch('route', 'match'),	// TODO: Use $.fetch(?)
+			newMatch;
+		
+		if (hash.length > 0 && hash[0] === '#') {
+			hash = hash.substr(1);
+		}
+		
+		for (var i = 0, l = routes.length, route, match; i < l; i++) {
+			route = routes[i];
+			match = route.pattern.exec(hash);
+			if (match !== null) {
+				newMatch = {
+					route: route,
+					params: match.slice(1)
+				};
+				break;
+			}
+		}
+		
+		var eventData = {};
+		if (oldMatch) {
+			eventData.from = oldMatch;
+			$this.del('route', 'match');
+		}
+		if (newMatch) {
+			eventData.to = newMatch;
+			$this.store('route', 'match', newMatch);
+		}
+		$this.trigger('routechange', eventData);
+		
+	});
+	
+};
+
+}(jQuery));
+
+
+(function($) {
+
+// TODO: Introduce statechange event which supplies both leaving and entering
+// state as data.
+
+// TODO: supersimplify:
+//	- rename to something with 'hash'
+//	- no more block employments
+//	- exactly one route matches (or none)
+
+var base = {
+	// Unnamed routes are non-final routes.
+	name: null,
+	// A pattern that matches nothing effectively disables a route.
+	pattern: /$./,
+	employ: []
+};
+
+$.state_hash = function() {
+	var routes = $.makeArray(arguments);
+	
+	// Create route definitions based on supplied arguments.
+	for (var i = 0, l = routes.length, route; i < l; i++) {
+		route = routes[i] = $.extend({}, base, routes[i]);
+		if (typeof route.pattern === 'string') {
+			route.pattern = new RegExp(route.pattern);
+		}
+		route.employ = $(route.employ).get();
+	}
+	
+	$(window).bind('hashchange', function() {
+		var $this = $(this),
+			uri = location.hash,
+			oldState = $this.fetch('state', 'current'),	// TODO: Use $.fetch(?)
+			newState;	// TODO: Rename to state (in line with naming in
+						// employ)
+		
+		if (uri.length > 0 && uri[0] === '#') {
+			uri = uri.substr(1);
+		}
+		
+		// Construct new state definition from matching route definitions.
+		for (var i = 0, l = routes.length, route, match, matches = []; i < l; i++) {
+			route = routes[i];
+			match = route.pattern.exec(uri);
+			if (match === null) {
+				continue;
+			}
+			matches.push({
+				route: route,
+				params: match.slice(1)
+			});
+			if (typeof route.name === 'string') {
+				// Final route found; state definition complete.
+				newState = matches;
+				break;
+			}
+		}
+		
+		if (oldState) {
+			for (var i = 0, l = oldState.length, elements = []; i < l; i++) {
+				$.merge(elements, oldState[i].route.employ);
+			}
+			$(elements).employ(false, oldState[oldState.length - 1].route.name);
+			$this.del('state', 'current');
+		}
+		
+		if (newState) {
+			$this.store('state', 'current', newState);
+			for (var i = 0, l = newState.length, elements = []; i < l; i++) {
+				$.merge(elements, newState[i].route.employ);
+			}
+			$(elements).employ(true, newState[newState.length - 1].route.name);
+		}
+		
+	});
+	
+};
+
+}(jQuery));
+
+
+
+
+
+
+/* APPLICABILITY:
+	basic rule of thumb: if you want a state to be part of browser history, model
+	it using this plugin. (and then it's automatically permalinkable.)
+	
+	*/
+
+/* possible scenario's for a content block, when going from current to new state
+
+// block that is not part of current but part of new state:
+// show & if state is different from last time block was active; change state
+visibility=hidden
+display=block
+handle visible
+	(stateleave
+	stateenter)
+handle perceivable
+	visibility=visible
+
+// block that is part of current but not part of new state:
+hide
+
+// block that is part of both states:
+// change state (and do smoothhide and smoothshow to hide the process of dom changing,
+// but that's not something the user should care about)
+visibility=hidden
+handle unperceivable
+	stateleave
+	[display=none		// this is not necessary, and undesirable if we want to allow users
+	display=block]		// to engage with visible and hidden events (which could be useful for disabling polling f.e.)
+	handle visible
+		stateenter
+	handle perceivable
+		visibility=visible	
+
+*/
+
+/* idea: also triggers stateenter and stateleave on the window */
+
+/* idea: resolves any state uris in <a href="state:tweets" ...>
+occurences in the dom at document.ready (or at $.state.define?) */
+
+/* provide navigation functions:
+	- $.state.go('tweets', 'timmolendijk') or  $.state.go.tweets('timmolendijk')  or  $.state.go('state:tweets(timmolendijk)')
+	- $.state.replace ... like go but replace item in history (is this useful??) ... we could call it 'redirect' ?
+	- $.state.back()
+	- $.state.forward()
+
+/* question: how do html-anchors blend in and behave with this system? */
+
+/* question: we need to add a way to define start-state ...
+also, we need consistency and as it looks like there is no way to completely get rid of a hash
+in the url as soon as you started using one (you can only get back to '#'), i'd say we want
+to always do a redirect: to a specified start-state, or else to '#' */
+
+/* problem: if we want to support anything else than raw state uris ...
+like f.e. state:tweets(timmolendijk) or $.state.go('tweets', 'timmolendijk'),
+we need to stop using regex patterns but move to custom (reversible) patterns instead.
+which suck
+or try an attempt at building a reversing algorithm... which is hard and will never be perfect
+but it's probably our best option (as it can be postponed to some undetermined moment in the future */
+
+/* API sugar idea:
+		// TODO: Define stateenter and stateleave as special events via
+		//       $.event.special to enable binding 'stateenter.*' and
+		//       'stateleave.*' as nicer ways of denoting 'stateenter._' and
+		//       'stateleave._'.
+*/
+
+/* API sugar idea:
+support sinatra pattern syntax (translate to regexp internally)
+*/
+
+/* idea: allow for a 'reload' state, which defines a pattern that results in reloading the previous
+	state in history */
+
+/* define states:
+
+$.state(
+	// define a start state (how to call this? start, init, empty??)
+	$.state.start('tweets(timmolendijk)'), // or ('tweets', 'timmolendijk') or ('state:tweets(timmolendijk)')
+	// keeps looking for more matches
+	// does not trigger dedicated events
+	{
+		pattern: '^/',
+		elements: '#nav',
+	},
+	// actual state: match means destination has been found => don't look further
+	// triggers dedicated events
+	{
+		state: 'tweetsoverview',
+		pattern: '^/tweets/overview$',
+		elements: '#overview, #tags, #basket'
+	},
+	// actual state, triggers dedicated events
+	// is not reached in case of '/tweets/overview'
+	{
+		state: 'tweets',
+		pattern: '^/tweets/(\w+)$',
+		elements: '#contacts, #tags, #basket'
+	},
+	// actual state, triggers dedicated events
+	// does not tolerate other matches, any others found so far are cancelled
+	{
+		state: 'ad',
+		pattern: '^/ad$',
+		elements: '#screenfiller',
+		singleton: true
+	},
+	// actual state, triggers dedicated events
+	// singleton not necessary, as we haven't defined non-final states
+	// in this layer
+	// operates in another layer than the default layer, which means that
+	// elements that are in the default layer (and all layers other than 'layername')
+	// are not hidden (and they won't be notified about state change either, but
+	// that is already intrinsic to how the state system works)
+	{
+		state: 'subscribe',
+		pattern: '^/subscribe$',
+		elements: '#dialog',
+		layer: 'layername'
+	},
+	// an alternative approach is to work with 'direct' controller functions,
+	// which may be more familiar to many people and works fine in simple setups.
+	// this can be mixed with defining elements
+	// one thing that may be confusing though: enter and leave are called when
+	// the events on window are triggered... which can be very different from
+	// when events on elements are triggered.
+	// question: maybe we should use different names for the events on the
+	// elements, to communicate the fact that they are different from the events
+	// on the window. something like stategain and statelose oid... then again,
+	// it's really the same message, just on another level / from another viewpoint.
+	{
+		state: 'author',
+		pattern: '^/about/(\w+)$',
+		enter: function(author) {
+			// create/show element(s) about supplied author
+		},
+		leave: function(author) {
+			// delete/hide element(s) about supplied author
+			// add author to list of visited bios (or whatever)
+		}
+	}
+	// pre-defined state (state: '404', pattern: '', singleton: true)
+	// defaults can be overridden
+	$.state.404({
+		elements: '#error-404',
+		singleton: false
+	}
+);
+
+// Conclusion: a state definition has the following signature with corresponding defaults:
+{
+	state: null,
+	pattern: null,	// can be /pat\/tern/ig or 'pat\/tern' or 'pat/tern'
+	elements: [],	// can be 'selector' or [elem1, elem2] or elem or $jQueryObj
+	singleton: false,	// can't we think of a better name for this one?
+	layer: 'base',
+	enter: $.noop,
+	leave: $.noop
+}
+
+// A couple of useful pre-defined states (alterations of the defaults) (with overridable options):
+$.state.404 = {
+	state: '404',
+	pattern: '',
+	singleton: true
+}
+$.state.overlay = {
+	layer: 'overlay'
+}
+$.state.start = {
+	pattern: '^$',
+	enter: function() {
+		$.state.redirect(*args);
+	}
+}
+
+
+// Or... we could go with a design like this:
+$.state.partial('^/', '#nav', {...});
+$.state.finite('tweets', '^/tweets/(\w+)$', '#contacts, #tags, #basket', {...})
+$.state.404('#error-404', {...});
+// ... ehrm... nope don't think that makes things better
+// perhaps leaving out partial and finite and using shorthand just for pre-defined states?
+
+
+// This is how you would/could work with element-level state changes.
+
+$('#nav').bind({
+	stateenter: function(e) {
+		// set tab selection based on e.data.state
+	},
+	stateleave: function(e) {
+		// unset tab selection based on e.data.state
+	}
+});
+
+
+
+
+
+
+//====================== OLD STUFF BELOW ====================================
+
+// idea: we could also opt for a design where we handle 'stateenter.name'
+// with 'name' being the name of the state that we enter.
+// does this work? are only the right namespaced handlers called?
+//		answer: yes!
+//		additional idea: trigger both non-namespaced and namespaced events
+// todo: define $.event.special.stateenter & $.event.special.stateleave
+$('#contacts').bind({
+	stateenter: function(e) {
+		// e.type === 'enterstate';
+		// e.data === {state: 'name', args: ['timmolendijk']};
+		if (e.data.state === 'name') {
+			// init ul.lazyloadable using args
+		}
+		// e.data.callback() or callback(), but only if all (async) stuff is done
+	},
+	stateleave: ...
+});
+	
+
+var r = /^#\/contacts\/(\w+)$/,
+	controller = function(user) {
+		$('#contacts').show().
+			find('> ul').lazyloadable({
+				load: function(callback) {
+					var $this = this;
+					$.getJSON('http://twitter.com/statuses/user_timeline/timmolendijk.json?count=100&callback=?', function(data) {
+						callback($this.flirt(data, false));
+					});
+				}
+			});
+	};
+*/
+// START ROUTING/PAGES ---> move to widget/plugin
+/*
+	$.route({
+		'routename': ['regex', '#nav, #contacts, ...'],
+		...
+	});
+	
+	$.routeUri('routename', args...) ===> '#/<parsed regex>'
+	
+	HOW TO DEAL WITH ARGS??
+*/
+/*
+var enable = function() {
+	var parts = Array.prototype.slice.call(arguments);
+	// TODO: Loop elements because perceivable cannot handle
+	// multiple elements at once yet.
+	$(parts.join(', ')).each(function() {
+		$(this).show();
+	});
+};
+
+route('#').bind(function() {
+	$('#error-404').flirt({url: window.routes.args.path});
+	enable('#nav', '#error-404');
+});
+route('#/contacts').bind(function() {
+	enable('#nav', '#tags', '#basket', '#contacts');
+});
+route('#/dpl').bind(function() {
+	enable('#nav', '#basket', '#dpl-lists');
+});
+
+$(window).bind('hashchange', function(e) {
+	$('body > div').css('display', '');
+//					route(location.hash).run();
+	
+	if (r.test(location.hash)) {
+		location.hash.replace(r, function() {
+			controller.apply(this, Array.prototype.slice.call(arguments, 1, arguments.length - 2));
+		});
+	}
+	
+});
+
+// END ROUTING/PAGES
+*/
+
+
+(function($) {
+
+var ns = 'state_old',
+	definition = {
+		// Unnamed states are non-final states.
+		name: null,
+		// A pattern that matches nothing effectively disables a state.
+		pattern: /$./,
+		elements: [],
+		enter: $.noop,
+		leave: $.noop
+	},
+	$trigger = function(type, states, flexicallback) {
+		var e = {states: states, callback: flexicallback},
+			l = states.length,
+			named = states[l - 1],
+			args = [named.state.name];
+		for (var i = 0; i < l; i++) {
+			$.merge(args, states[i].params);
+		}
+		this.
+			trigger($.extend({type: type + '._'}, e), args).
+			trigger($.extend({type: type + '.' + named.state.name}, e), args);
+	};
+
+$[ns] = function() {
+	// TODO: Exit if states are yet defined, perhaps switch to alternate
+	//       behavior (getter?)
+	var states = Array.prototype.slice.call(arguments);
+	
+	// Create condensed state definitions based on supplied arguments.
+	// TODO: Validate characters in name as it should be a valid event namespace
+	//       and not conflicting with the identifier for 'any namespace': '_'. 
+	for (var i = 0, l = states.length, state; i < l; i++) {
+		state = states[i] = $.extend({}, definition, states[i]);
+		if (typeof state.pattern === 'string') {
+			state.pattern = new RegExp(state.pattern);
+		}
+		state.elements = $(state.elements).get();
+	}
+	
+	$(window).bind('hashchange', function() {
+		var $this = $(this),
+			uri = location.hash,
+			matches = [],
+			i, l, state, match;
+		
+		if (uri.length > 0 && uri[0] === '#') {
+			uri = uri.substr(1);
+		}
+		
+		// Collect matching state definitions.
+		for (i = 0, l = states.length; i < l; i++) {
+			state = states[i];
+			match = state.pattern.exec(uri);
+			if (match === null) {
+				continue;
+			}
+			matches.push({
+				state: state,
+				params: match.slice(1)
+			});
+			if (state.name && typeof state.name === 'string') {
+				break;
+			}
+		}
+		
+		// SCENARIOS:
+		// 0. page is loaded
+		// 1. state x with block1 & block2 is entered; block1 is perceivable, block2 is hidden
+		// 2. state y(a) with block2 & block3 is entered; block3 is visible yet unperceivable
+		// 3. state y(b) is entered
+		// 4. state z with block4 is entered
+		// 5. state x is entered
+		
+		// CHANGE TO matches:
+		
+		// window.bindone:
+		//		stateleave:
+		//			for match in current:
+		//				match.state.leave
+		//		stateenter:
+		//			for match in matches:
+		//				match.state.enter
+		//		stateready:
+		//			for match in matches:
+		//				match.state.ready
+		// TODO: move to element loop?
+		// window.queue:
+		//		if current:
+		//			stateleave
+		//		stateenter
+		//		stateready
+		// TODO: use $.fetch
+		var current = $this.fetch(ns, 'current');
+		$this.
+			flexiqueue(ns, [function(flexicallback) {
+				if (current) {
+					$(this).chain($trigger, 'stateleave', current, flexicallback);
+				}
+			}, function(flexicallback) {
+				if (matches.length > 0) {
+					$(this).chain($trigger, 'stateenter', matches, flexicallback);
+				}
+			}/*, function(flexicallback) {
+				// TODO: Wasn't the idea behind stateready that it would only
+				// be triggered when all blocks are ready too?
+				$(this).chain($trigger, 'stateready', matches, flexicallback);
+			}*/]).
+			dequeue(ns);
+		
+		var $matches = [];
+		for (i = 0, l = matches.length; i < l; i++) {
+			$.merge($matches, matches[i].state.elements);
+		}
+		$matches = $($.unique($matches));
+		var $current = [];
+		for (i = 0, l = $.isArray(current) ? current.length : 0; i < l; i++) {
+			$.merge($current, current[i].state.elements);
+		}
+		$current = $($.unique($current));
+		
+		// hide all elements in current that do not occur in matches
+		$current.filter(function() {
+			return $matches.index(this) === -1;
+		}).hide();
+				
+		// for elem in elements-in(matches):
+		//		if matches-that-contain(matches, elem) != elem.current:
+		//			elem.queue:
+		//				visible & unperceivable
+		//				if elem.current:
+		//					stateleave
+		//				stateenter
+		//				visible & perceivable
+		//				stateready
+		
+		// TODO: set element.current somewhere (in a stateenter handler, or right after triggering stateleave?)
+		$matches.
+			filter(function() {
+				// TODO: Write comparator. Can't we use the :data selector? < like how??
+				return !_.isEqual($(this).fetch(ns, 'current'), matches);
+			}).
+				flexiqueue(ns, [function() {
+					// TODO: if this code is indeed synchronous, we could move
+					// it out of the queue... (but do we want to? we don't want
+					// to hide a block any longer than absolutely necessary)
+					$(this).
+						css('visibility', 'hidden').
+						show();
+				}, function(fcb) {
+					var $this = $(this),
+						current = $this.fetch(ns, 'current');
+					if (current) {
+						$this.chain($trigger, 'stateleave', current, fcb);
+					}
+				}, function(fcb) {
+					$(this).store(ns, 'current', matches.length > 0 ? matches : undefined);
+					if (matches.length > 0) {
+						$(this).chain($trigger, 'stateenter', matches, fcb);
+					}
+				}]).
+				end().
+			flexiqueue(ns, function() {
+				$(this).
+					show().
+					css('visibility', 'visible');
+			}).
+			flexiqueue(ns, function(fcb) {
+				$(this).chain($trigger, 'stateready', matches, fcb);
+			}).
+			dequeue(ns);
+		
+		// current = matches
+		// TODO: use $.store
+		$this.store(ns, 'current', matches.length > 0 ? matches : undefined);
+		
+		/*
+		// Change state; trigger events and callbacks.
+		// TODO: Use $.fetch() and $.store() as soon as we have implemented them.
+		// Leave current state.
+		var current = $(this).fetch(ns, 'current'),
+			$active = current ? current.state.elements : $();
+		if (current) {
+			current.state.leave.apply(current.state, current.params);
+			$this.
+				trigger($.extend({type: 'stateleave._'}, current), current.params).
+				trigger($.extend({type: 'stateleave.' + current.state.name}, current), current.params);
+		}
+		// Set and enter new current state.
+		// TODO: Is it ok that stored current state is updated only after the
+		//       enter events and callbacks are triggered?
+		current = undefined;
+		for (i = 0, l = matches.length; i < l; i++) {
+			current = matches[i];
+			
+			// REGARDLESS OF VISIBILITY:
+			// for element in current.state.elements:
+			//		if element.current:
+			//			if element.current.state.name !== current.state.name || element.current.params !== current.params:
+			//				trigger stateleave
+			//				trigger stateenter
+			//		else:
+			//			trigger stateenter
+			
+			// REGARDFUL OF VISIBILITY:
+			// TODO: whenever depending on visibility event handlers: make sure they are always triggered
+			// $active.subtract(current.state.elements).hide()
+			// for async element in current.state.elements:
+			//		if !element.current:
+			//			element.bind visible:
+			//				enable callback
+			//				trigger stateenter, next
+			//			TODO: element should be able to do something on perceivability as well.
+			//				simply bind visibility event? then why not simply bind visible
+			//				event as well (instead of stateenter event)?
+			//				afterthought: should it really? isn't perceivability a matter for
+			//				the global scope? i should investigate what, besides focus, needs
+			//				perceivability.
+			//		if element.current !== current:
+			//			element.bind hidden:
+			//				element.show
+			//			element.bind unperceivable:
+			//				enable callback
+			//				trigger stateleave, callback
+			//			element.bind visible:
+			//				trigger stateenter, next
+			//		TODO: simply hide and then show is too course-grained for the scenario in which the element is already visible,
+			//			in which case a visibility switch would suffice.
+			//		TODO: what to do when element is already hidden, resulting in no unperceivable event?
+			//		element.hide
+			
+			current.state.elements.
+				each(function() {
+					var activeIdx = $active.index(this);
+					if (activeIdx !== -1) {
+						if (JSON.stringify($active.eq(activeIdx).fetch(ns, 'current').params) === JSON.stringify(current.params)) {
+							return true;
+						}
+					}
+					var $this = $(this),
+						elemState = $this.fetch(ns, 'current');
+					if (elemState && (elemState.state !== current.state || JSON.stringify(elemState.params) !== JSON.stringify(current.params))) {
+						$this.trigger($.extend({type: 'stateleave._'}, elemState), elemState.params);
+						if (elemState.state.name !== null) {
+							$this.trigger($.extend({type: 'stateleave.' + elemState.state.name}, elemState), elemState.params);
+						}
+					}
+					$this.store(ns, 'current', current);
+					$this.trigger($.extend({type: 'stateenter._'}, current), current.params);
+					if (current.state.name !== null) {
+						$this.trigger($.extend({type: 'stateenter.' + current.state.name}, current), current.params);
+					}
+				});
+			current.state.enter.apply(current.state, current.params);
+		}
+		if (current) {
+			// TODO: i think we shouldn't trigger the (or any) global event until all block-level
+			//		stuff is done (and perceivable). same applies to enter and leave controllers(?)
+			$this.
+				trigger($.extend({type: 'stateenter._'}, current), current.params).
+				trigger($.extend({type: 'stateenter.' + current.state.name}, current), current.params);
+		}
+		$(this).store(ns, 'current', current);
+		*/
+	});
+};
+
+}(jQuery));
+
+

File src/jquery.al.state.js

 (function($) {
 
-var base = {
-	// Unnamed routes are non-final routes.
-	name: null,
-	// A pattern that matches nothing effectively disables a route.
-	pattern: /$./,
-	employ: []
-};
-
-$.state = function() {
-	var routes = $.makeArray(arguments);
+$.widget('al.state', {
 	
-	// Create route definitions based on supplied arguments.
-	for (var i = 0, l = routes.length, route; i < l; i++) {
-		route = routes[i] = $.extend({}, base, routes[i]);
-		if (typeof route.pattern === 'string') {
-			route.pattern = new RegExp(route.pattern);
-		}
-		route.employ = $(route.employ).get();
-	}
+	options: {
+		observe: {},
+		active: true,
+		value: null
+	},
 	
-	$(window).bind('hashchange', function() {
-		var $this = $(this),
-			uri = location.hash,
-			oldState = $this.fetch('state', 'current'),	// TODO: Use $.fetch
-			newState;
+	_create: function() {
+		var self = this;
 		
-		if (uri.length > 0 && uri[0] === '#') {
-			uri = uri.substr(1);
-		}
+		// TODO: Validate types (i.e. prevent that it contains commas, a.o.).
+		$.each(self.options.observe, function(types, elements) {
+			$(elements).bind(types, $.proxy(self, 'update'));
+		});
+	},
+	
+	// TODO: Add option to force update, even if state value has not changed.
+	update: function() {
+		var self = this;
 		
-		// Construct new state definition from matching route definitions.
-		for (var i = 0, l = routes.length, route, match, matches = []; i < l; i++) {
-			route = routes[i];
-			match = route.pattern.exec(uri);
-			if (match === null) {
-				continue;
+		var toValue = $.isFunction(self.options.value) ? self.options.value.call(self.element[0]) : self.options.value;
+		
+		var fromActive = self.isActive(),
+			toActive = !!($.isFunction(self.options.active) ? self.options.active.call(self.element[0], toValue) : self.options.active);
+		
+		// Show or hide regardless of current state to make sure visibility
+		// correlates with activity.
+		self.element[toActive ? 'show' : 'hide']();
+		
+		if (!toActive) {
+			if (fromActive) {
+				self._trigger('deactivate');
 			}
-			matches.push({
-				route: route,
-				params: match.slice(1)
-			});
-			if (typeof route.name === 'string') {
-				// Final route found; state definition complete.
-				newState = matches;
-				break;
+			return;
+		} else {
+			var isInit = self.isInit(),
+				fromValue = isInit ? self._value : null,
+				eventData = {
+					value: toValue
+				};
+			
+			if (!isInit || !_.isEqual(fromValue, toValue)) {
+				self._value = toValue;
+			}
+			
+			if (!fromActive) {
+				self._trigger('activate');
+			}
+			
+			if (!isInit || !_.isEqual(fromValue, toValue)) {
+				// TODO: Always trigger change event, unless init handler(s)
+				// trigger its cancelation.
+				if (!isInit) {
+					self._trigger('init', undefined, eventData);
+				} else {
+					self._trigger('change', undefined, $.extend({from: fromValue}, eventData));
+				}
 			}
 		}
 		
-		if (oldState) {
-			for (i = 0, l = oldState.length; i < l; i++) {
-				$(oldState[i].route.employ).employ(false);
-			}
-			$this.del('state', 'current');
-		}
+	},
+	
+	isInit: function() {
+		var self = this;
 		
-		if (newState) {
-			$this.store('state', 'current', newState);
-			for (i = 0, l = newState.length; i < l; i++) {
-				$(newState[i].route.employ).employ(true);
-			}
-		}
+		return '_value' in self;
+	},
+	
+	isActive: function() {
+		var self = this;
 		
-	});
+		// TODO: Determining activity based on visibility is not feasible, as
+		// visibility may be affected by a parent element, effectively
+		// deactivating this state block without it ever triggering a
+		// deactivate event.
+		return self.isInit() && self.element.is(':visible');
+	},
 	
-};
-
-}(jQuery));
-
-
-
-
-
-
-/* APPLICABILITY:
-	basic rule of thumb: if you want a state to be part of browser history, model
-	it using this plugin. (and then it's automatically permalinkable.)
-	
-	*/
-
-/* possible scenario's for a content block, when going from current to new state
-
-// block that is not part of current but part of new state:
-// show & if state is different from last time block was active; change state
-visibility=hidden
-display=block
-handle visible
-	(stateleave
-	stateenter)
-handle perceivable
-	visibility=visible
-
-// block that is part of current but not part of new state:
-hide
-
-// block that is part of both states:
-// change state (and do smoothhide and smoothshow to hide the process of dom changing,
-// but that's not something the user should care about)
-visibility=hidden
-handle unperceivable
-	stateleave
-	[display=none		// this is not necessary, and undesirable if we want to allow users
-	display=block]		// to engage with visible and hidden events (which could be useful for disabling polling f.e.)
-	handle visible
-		stateenter
-	handle perceivable
-		visibility=visible	
-
-*/
-
-/* idea: also triggers stateenter and stateleave on the window */