1. Tim Molendijk
  2. App Layer

Commits

Tim Molendijk  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
  • Branches default

Comments (0)

Files changed (23)

File demo/control.html

View file
+<!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

View file
+<!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

View file
+<!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

View file
+(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

View file
+(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

View file
+(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

View file
 	
 	_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

View file
 - 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

View file
+// 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

View file
 (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

View file
 	
 	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

View file
+(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

View file
 (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 */
-
-/* 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 '#' */